Instrumentsを使用したアプリのパフォーマンス向上方法〜Instruments Tutorials〜
Nagoya.swiftで登壇した内容をさらに詳しくして記事にしました。
スライドとLT動画もよかったらみてください!
Instrumentsとは
Instrumentsとはアプリのパフォーマンス、リソース使用量、動作を分析するツールです。Instrumentsを活用することで、応答性を向上させ、メモリ使用量を削減し、複雑な動作を時間の経過とともに分析することができます。ハングアップの解消に使用できます。
ハングアップとは
ユーザーインタラクションの処理に顕著な遅延が発生する場合、応答のない状態をハングアップと呼ぶ。
もっと具体的にいうと、ユーザーがボタンを押したのに画面が切り替わらない。そうするとユーザーはボタンが押せているのか不安に思ってしまう。このような状態をハングアップと言います。
理想的にはメインスレッドでの作業は100ミリ秒以上中断されずに実行する必要がある。
InstrumentsはTime Profilerでは250ミリ秒を超えるハングが警告として検出される。
Instruments Tutorials
この記事はInstruments TutorialsのサンプルアプリやWWDCの動画をもとに書いています。Instruments TutorialsとWWDCの動画を並行して読み進めることでInstrumentsの使用方法の理解が深まりました。
ハングの種類
ハングの種類はメインスレッドがビジー状態とメインスレッドがブロック状態大きく2つに分けることができます。
メインスレッドがビジー状態であるということは、アプリが常にデータを処理しており、CPU 使用率が高いことを意味します。
メインスレッドがブロック状態は、メインスレッドでの作業が停止し、代わりに他のスレッドにCPUアクセスを与えている状態
Instrumentsを使用し、ハング中のメインスレッドのCPU使用率を解析することで、メインスレッドがビジー状態かブロック状態かを判断することができます。
ハングの例
Instruments Tutorialsのサンプルコードでハングの例をみてみましょう!
Listの状態のTabBarを切り替えるためにCollectionのTabBarを押します。しかし、下記の動画のようにボタンを押してから約5秒しないとCollectionの画面に切り替わらないというハングが起こっています。
ハングの解析方法
まず、InstrumentsのTime Profilerを使用しましょう!
XcodeのProduct > Profile または、ショートカット:command + I
でテンプレート選択ウィンドウを表示し、Time Profilerを選択する。
ハングの特定
下記の画像の左上の赤色の録画ボタンを押し、シミレーターでハングを再現し、録画停止ボタンを押す。
下記の画像では5.63sのハングが起こっていること・CPU使用率が高いことからメインスレッドがビジー状態であることがわかります。
メインスレッドの分析
Call Tree > ✅Hide System Libraries
システムライブラリを非表示にし、Xcodeのコードの該当箇所を分析しやすくする。メソッドやXcode上のコードのみを表示する。
ThumbnailViewのボディゲッターの呼び出しにCPU時間の100%費やされていることがわかる。
これがハングの原因である。
ハングを修正
実行頻度と継続時間を測定することで改善方法を考える。
ハングの解消方法として下記の2つがある。
①処理量を減らす
②バックグラウンドスレッドに処理を移す
実行頻度は先ほど使用したTime Profilerでわからないので下記の画像の+ Instrumentボタンを押し、View Body Instrumentsを追加。Viewのゲッターの実行頻度を調べる。
View Bodyを追加した後、録画ボタンを押し、シミレーターで再度ハングを再現する。
ThumbnailViewのbodyの実行に1つの画像あたり43ms、合計129回画像が呼ばれており、5.6sかかっている。
現在のレイアウトでiPhoneの画面に収まるサムネイルは30~40個程度であるにもかかわらず、129回のbody実行は多すぎる。
ハングを修正: ①処理量を減らす
struct ImageCollectionsView: View {
var images: ImageCollection
var body: some View {
let groupNames = images.groupedImages.keys.sorted()
NavigationStack {
List(groupNames, id: \.self) { groupName in
Section(groupName) {
let images = images.groupedImages[groupName]!
ScrollView(.horizontal) {
// ✅HStack → LazyHStackに変更
LazyHStack {
ForEach(images) { imageFile in
ThumbnailView(imageFile: imageFile)
}
}
}
}
}
HStackからLazyHStackに書き換えます。
HStackを使用しているときはHang 5.69s
LazyHStackを使用しているときはHang 2.03s
LazyHStack画面に見えている画像だけ読み込むのでThumbnailViewの呼び出しが129回から38回になっている。
ハングを修正: ②バックグラウンドスレッドに処理を移す
makeThumbnail(displayScale:)
を同期関数からmakeThumbnail(displayScale:) async
非同期関数にする。
struct ImageFile: Identifiable {
let fileURL: URL
/// Maximum thumbnail height in points.
static let maxThumbnailHeight: CGFloat = 50
var id: URL {
fileURL
}
var name: String {
fileURL.deletingPathExtension().lastPathComponent
}
var image: UIImage? {
UIImage(contentsOfFile: fileURL.path)
}
// ✅makeThumbnail(displayScale:)を同期関数から非同期関数にする
func makeThumbnail(displayScale: CGFloat) async -> UIImage? {
guard let image else { return nil }
let thumbnailSize = thumbnailSize(for: image.size, displayScale: displayScale)
return image.preparingThumbnail(of: thumbnailSize)
}.....
struct ThumbnailView: View {
@Environment(\.displayScale) private var displayScale: CGFloat
var imageFile: ImageFile
@State private var loadedThumbnail: Image?
var body: some View {
content
.task(id: displayScale) {
✅非同期関数にしたのでawaitをつける
guard let thumbnail = await imageFile.makeThumbnail(displayScale: displayScale) else {
loadedThumbnail = Image(systemName: "x.square")
return
}
loadedThumbnail = Image(uiImage: thumbnail)
}
}
.task修飾子のクロージャでラップされているにも関わらず、makeThumbnail(displayScale:)
のような同期関数の場合はメインアクターで実行されるのはなぜか?
コードのこの部分がメインアクターで実行される理由は2つあります。1つ目はSwiftUIのビューのbodyプロパティは、Viewプロトコルによって暗黙的にメインアクターにバインドされています。そして第2つ目は.task修飾子のクロージャは、init(priority:operation:)
と同じで、周囲のコンテキストからアクターコンテキストを継承します。bodyプロパティがメインアクターにバインドされているため、.task()
に渡されるクロージャもメインアクターにバインドされる。
※Task.init(priority:operation:)によって作成されたタスクは、呼び出し元の優先順位とアクターコンテキストを継承します。
メインアクターから切り離して実行できるようにするため、makeThumbnail(displayScale:) async
非同期関数にする。非同期関数自体はどのアクターにも束縛されません。task修飾子によって生成された非同期タスクの中で呼び出されるため、バックグラウンドのスレッドプールで実行される。
逆に、同期関数は呼び出された場所で実行され、.task修飾子のクロージャはメインアクターになってしまっていた。
対照的に、非同期関数は、メインスレッドではなく、Swiftの並行実行ランタイム (並行スレッドプール) によって管理されるいくつかのワーカースレッドの 1 つで実行されます。
メインスレッドではなく、Swift concurrencyランタイム (concurrentスレッドプール) によって管理されるいくつかのワーカースレッドの1つで実行されます。このセクションでは、makeThumbnail(displayScale:) 関数を非同期にし、メインアクタではなく、主にバックグラウンドのconcurrentスレッドプールで実行するようにします。
makeThumbnail(displayScale:)
を同期関数: 実行時間 1.69s
makeThumbnail(displayScale:) async
非同期関数: 実行時間 412ms
非同期関数にすることで1.69sから412msに実行時間を短縮でき、Instruments上のハングも解消される。
参考文献
Instruments Tutorial https://developer.apple.com/tutorials/instruments
Analyze hangs with Instruments https://developer.apple.com/videos/play/wwdc2023/10248/
SwiftUIのご紹介 https://developer.apple.com/jp/videos/play/wwdc2020/10119/
SwiftUIの基本 https://developer.apple.com/jp/videos/play/wwdc2024/10150/
Swift UIにおけるデータの重要事項 https://developer.apple.com/jp/videos/play/wwdc2020/10040/
SwiftUIの徹底解説 https://developer.apple.com/jp/videos/play/wwdc2021/10022/
SwiftUIのパフォーマンスを解明 https://developer.apple.com/jp/videos/play/wwdc2023/10160
Discussion