ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Appの起動を最適化する
Appの起動が遅いとストレスがたまるものです。このセッションでは、App起動用の新しいinstrumentと、Appの起動を速くする方法について説明します。Appの起動時に行われる事柄や、この重要な時間における作業を最小限にし、優先順位を付け、最適化する方法についてご確認ください。エンジニアによる、iOS Appの起動を速くするためのヒントやコツも紹介します。
リソース
-
ダウンロード
(音楽)
(拍手) こんにちは パフォーマンスチームの スペンサー・リューソンです 今日のテーマは アプリケーションの起動最適化です
主なトピックは4つ まずは起動について 起動の種類を説明し サブフェーズに分割します 次はApp起動の正しい計測方法 iOSデバイスは 多様な状態や状況で使用され 条件によって起動速度が変わります その影響を抑えて 測定することが重要です
そして3つ目
Instrumentsで プロファイルと分析を行い 改善点を見つけます 最後は起動のモニタリング 実環境での起動を継続的に監視して 全ユーザに快適なUXを 提供する方法を紹介します
まずはApp起動の概要です Appの起動中はUXが中断されます 実際にお見せしましょう いきますよ 用意 スタート
このiPhone 6s Plusでは 起動に約2.5秒かかりました 快適な起動とは言えませんね Appの起動回数は多いので 改善すべきです iOSデバイス全体では 1日に何十億回も起動されています では起動時間を― 1回あたり 1ミリ秒短縮したとしましょう すると合計では 162日もの短縮になります
言い換えると… (拍手) どうも この数字は ロケットが火星に行くのに 要する日数です
高速化する理由は他にもあります
第一に 起動がスムーズだと Appの第一印象が良くなります デベロッパは 新しいデバイスを使用しがちです 重要なのは自分が体感しているUXを ユーザのデバイスでも 実現することです iOSデバイスの種類や 条件に関係なくです
さらに起動は下処理や初期化 ビュー作成を含み コードの大部分を占めます よって起動が ユーザの満足するものでなければ 他の部分も 好ましくない状態でしょう
また 起動時はCPUやメモリなどの 処理負荷が高まります その負荷を抑え 電池寿命と パフォーマンスを改善しましょう
次は起動の種類についてです
コールドスタートとウォームスタート 起動に似たレジュームです
再起動後や久々のApp起動時は コールドスタートです Appをディスクからメモリに読み込み 必要なシステムサービスを起動し 処理を始めます 時間はかかりますが 一度 行うと以降は ウォームスタートになります
その場合 Appの開始は必要ですが メモリへの読み込みと システムサービスは開始済みです よって速度と安定性が増します
最後はレジューム ホーム画面やApp Switcherから 再表示する場合です Appはすでに起動済みなので 非常に高速です 計測時はレジュームと起動を 必ず区別しましょう
では本題です
この起動速度を―
レジューム並みに高速化しましょう
そのためには― 最初のフレームを400ミリ秒以内に レンダリングします 起動アニメーション中に ピクセルを表示し 起動完了までに Appをインタラクティブにします
まずは起動処理を理解しましょう
使うのはマップ 通常 ホーム画面の アイコンをタップして起動します
iOSは最初の100ミリ秒で 初期化のため システム側の処理を実行
つまりデータロード ビューや最初のフレームの生成には 300ミリ秒しか使えません
なお フレームに プレースホルダを使って 非同期ロードしても構いません ただ インタラクティブにする 必要はあります マップの場合はタイルのロード中も 検索やお気に入りの表示が可能です
次の200ミリ秒で 非同期ロードしたデータを表示し フレームを完成させます
詳細を見ましょう
フェーズは全部で6つ システムの初期化やAppの初期化 ビュー生成やレイアウトを行い Appによっては拡張フェーズで 非同期データをロードします
システムインターフェイスの 前半はdyld 共有ライブラリとフレームワークを ロードする動的リンカです 2017年には システム最適化を推進するため dyld3を導入しました iOS 13では皆さんのAppに この最適化が適用されます つまり ランタイム依存関係の キャッシュにより 高速起動が可能になります
(拍手) どうも
新しいリンカの効果を 最大限に生かすための助言があります まず 負荷を減らすために 不要なフレームワークのリンクと― dlopenやnsBundleLoadなどの 動的ライブラリのロードを避けます でないと キャッシュによる 短縮時間が無駄になります 結果的に全依存関係の ハードリンクが必要になりますが それでも速度は増します
システムインターフェイスの 後半はlibSystemの初期化 ローレベルのシステム コンポーネントの初期化です システム側の処理なので 皆さんが対策する必要はありません
次はランタイムの静的初期化
Objective-Cと Swiftランタイムを初期化します 通常Appは何もしません ただしコードや リンク先のフレームワークに 静的初期化メソッドが ある場合は別です
基本的に静的初期化は非推奨なので 影響を抑えましょう
フレームワークで初期化する場合は APIを公開し スタックを早期に初期化します
静的初期化が必須の場合は 起動のたびに呼ばれるクラスロードから クラス初期化部分に処理を移します これでメソッドの初回使用時に 遅延処理されます
次のUIKitの初期化では UIApplicationDelegateと UIApplicationをインスタンス化します
大半はイベント処理などの システム側の処理です ただ UIApplicationサブクラスの処理や UIApplicationDelegateの 初期化は改善可能です
次はAppの初期化
ここは起動高速化のために できることが多数あります
まず 新しいUIScene APIの 動作を説明します Appの初期化時は デリゲートコールバックのapplication: willFinishLaunchingwithOptionsと application:didFinishLaunchingwith Optionsメソッドが呼ばれます
Appが表示されると applicationDidBecomeActiveが 呼ばれます UISceneを未使用の場合は didFinishLaunchingWithOptionsと ビューコントローラの作成が必要です UISceneの有無で 初期化処理が変わるためです
いずれにせよwillFinishLaunchingと didFinishLaunchingが呼ばれ Appが表示されると― 新しいUISceneDelegateライフサイクル コールバックが呼ばれます 具体的には scene:willConnectToSession:optionsと sceneWillEnterForeground sceneDidBecomeActiveです ここで作成が必要なのは ビューコントローラと― scene:willConnectToSession:options didFinishLaunchingwith Optionsのみです これを誤るとパフォーマンス低下や 予測不能なバグの原因になります
このフェーズでの助言は UISceneの有無に関係なく同じです
最初のフレームに不要な処理は 後回しにして バックグラウンドキューに入れるか あとで実行しましょう
UISceneを使用している場合は 必ずSceneとリソースを共有します 不要な反復処理による負荷を 低減させるためです
詳細は関連する講演をご覧ください
次は 最初のフレームの描画 ここでは単純にビューの生成 レイアウト 描画を行い その情報を基に最初のフレームを レンダリングします
有効な対策は 階層内のビュー数の削減です 具体的にはビューをフラットにして 起動時に見せないビューは あとでロードします
自動レイアウトの 制約も減らしましょう
最後は拡張フェーズ App固有のフェーズで 初回コミットから非同期データのロード 最終フレーム表示を含みます 有無はAppによります
この間 Appはインタラクティブである 必要があります
拡張フェーズでは 処理内容を把握することが重要です OSSignpost APIを使用して 処理を細分化して 計測を行いましょう
では 次は適切な計測について 説明します
iOSデバイスが使われる状態や 状況は様々なので 起動にも大きな差が生じます よって分析や比較をする時は 同じ条件にすることが 非常に重要です また 改善効果を測るには 動作を予測可能にする必要があります
まず ネットワークインターフェイスや バックグラウンドプロセスなど 差異の元を取り除きます こうすると実環境とは かけ離れてしまうと 感じるかもしれません でも 問題はありません 効果測定で最も重要なのは 一貫性です
我々もこの方法で修正の影響を確認し 起動時間を短縮をして テレメトリデータを集め 有効性を検証しています
では 適切な環境の整え方を説明します
まずはデバイスの再起動 不要な状態をクリアし 起動処理が終わるまで 数分放置します
ネットワークについては 機内モードを有効にするか コードで依存性を無効にします これで差異が かなり減ります
次はiCloud バックグラウンドで動作し シームレスなUXを実現しますが Appの起動に影響を及ぼします 従って 不変のiCloudアカウントと データを使用するか 完全にログアウトします
次のポイントはAppの リリースビルドを使用することです 理由は2つあります
不要なデバッグコードによる負荷と コンパイル時間を削減するためです
最後はウォームスタートで測ることです メモリへの読み出しと システムサービスの開始が 完了しているので安定しています
次はテストデータです 一貫性のある仮データを用意します 様々なユーザを想定して 量の異なるデータセットを複数 準備しましょう
ただ 幅広いデータ量に対応するため ロードするのは― 最初のフレームで使う分だけにします
次はデバイスの選定です 需要の高いデバイスを複数選び 一貫性を保ちます 最古のサポートOSが載った 最古のデバイスも含めます デバイスの古さによって RAMやCPUのコア数が異なり パフォーマンス特性にも 差が出るためです このようにして全ユーザ 全デバイスの起動を最適化します
あとは計測です Xcode 11で新しいXCTestを使い 測定します 数行コードを書けば 起動が繰り返され 統計データが得られます
詳細はあとで紹介します
起動の概要と計測方法を紹介したので 次は改善方法です
コードやInstrumentsで 起動処理を見直す時のポイントは3つ 処理の最小化 優先順位付け そして最適化です
最小化では 非表示のビューや 未使用の準備済み機能など― 最初のフレームの生成に 無関係なものは後回しにします
メインスレッドをブロックする ネットワークやファイルの入出力は バックグラウンドスレッドへ移します
また メモリの割り当てや操作は 時間がかかるので減らします
次は優先順位付け 各処理に正しい優先度を設定します iOS 13では 最適化されたスケジューラにより Appの起動速度が上がります ただ 複数スレッドで処理を行うので 優先度管理が重要になります
WWDC 2017の「Modernizing Grand Central Dispatch Usage」で― 並列処理の詳細をご確認ください
次は最後のプロセス 最小化と優先順位付けのあとは 最適化です つまり単純化と制限です 例えば起動中は 必要なデータのみ取得して 計算処理は後回しにします
次にメソッドとアルゴリズムを 見直します 計算方法やデータ構造を変えると 効果的です
あとはリソースや 複雑な要素をキャッシュし 不要な反復処理を避け CPUとメモリを節約します
このあとはダンがXcode Instrumentsの App Launchテンプレートを使用して 起動の改善方法を説明します (拍手) ありがとう こんにちは パフォーマンスエンジニアの ダン・サワダです 今日は典型的なAppの 起動フローを説明します 最小化 優先順位付け 最適化できる処理を見つけ ユーザのために 快適な起動を実現しましょう では始めます
デモで使うのは Star Searcher Appです 本セッションのために作りました UIは典型的なテーブルビューで 架空の星が表示されています 任意の星をクリックすると 説明文と画像が表示されます ただ 問題があります 起動してみます 用意 スタート
起動するのに2.5秒もかかりました 快適とは言えませんね ではXcodeとInstrumentsを用いて 改善点を探しましょう
Star SearcherのXcodeプロジェクトです パフォーマンス分析を始める前に 実施すべきことが1つあります Xcodeの“Profile”を選択して Appをリリースモードで 再コンパイルします こうするとコンパイル時間を 短縮できます 完了すると Appがデバイスにインストールされ Instrumentsが起動します iOS 13すなわちXcode 11以降では App Launchテンプレを利用できます これにより優先度を判別して 起動の問題点を特定できます では App Launchを ダブルクリックします
まず 記録ボタンを―
クリックします
Star Searcherが自動で起動します メトリクスやテレメトリデータを 取得して分析し すべての起動フェーズを可視化します ご覧ください 最初の紫色のフェーズは Appのmain関数の呼び出し前に 発生するフェーズです
緑のフェーズは main関数の先頭で発生し Appの起動が終わると 最初のフレームが描画されます
レーンを展開すると Appプロセスで応答する 全スレッドの詳細な状態が表示されます 最も重要なのはメインスレッド 別名UIスレッドです ユーザ入力を処理してUIを描画します 重要なレーンを固定しましょう まずは起動フェーズから メインスレッド 起動中に大量の処理をする このスレッドも― 固定表示しておきます
スレッドの状態は…
スレッドの色は 灰色が何もしていないブロック状態
赤は実行可能で スケジュール済みですが CPUリソース不足の状態 オレンジはプリエンプション より優先度の高い処理のために 処理を中断された状態です そして水色は 実際にCPUコアで実行中の状態です
では各フェーズを見ていきます まずシステムインターフェイスの初期化
フェーズをトリプルクリックすると ハイライト表示され 詳細情報が画面下に表示されます 左側は その間に実行している 全処理の詳細なスタックトレース 右側は集約されたスタックトレース CPUサンプルサイズの順に 全シンボルが表示されます この初期フェーズでは システムインターフェイス設定が 6ミリ秒で完了しています これはdyld3の導入や サードパーティApp 他のシステムレイヤの 改善による効果です この状態を保つために コードは 一切変更しないでおきます
次に進む前に 注目すべき点が1つあります このフェーズの実行時間は CPUクロックでは6ミリ秒です しかしウォールクロックでは 149ミリ秒もかかっています この差はプロファイルにかかる時間です 有益な情報を得られる分 リソースを消費するのです よってプロファイルと計測を 区別する必要があります 詳細はあとで説明します
次はランタイムの静的初期化です このフェーズには なんと375ミリ秒もかかっています ちょっと長すぎますね
スタックトレースの詳細を見ましょう 青いアイコンのシンボルを見ると CPU上の処理時間は370ミリ秒です 青いシンボルは ソース内で 宣言されたコードを意味します
クリックします
スタックトレースを展開すると SLSuperFastLoggerがあります “SuperFast”というライブラリ名は 合わない気がしますね
このSLSuperFastLoggerは Star Searcherにインポートする 外部フレームワークです 便利なログ機能が備わっていますが 呼び出し元は1ヵ所です テーブルビューコントローラの didSelectRowAtコールバックのみです これはセルタップ時にのみ呼び出され 起動パスには含まれません では なぜmain関数が呼ばれる前から 300ミリ秒も 処理をしているのでしょうか 理由を探ります
シンボルを検索すると SLSuperFastLoggerクラス内の +loadメソッドがヒットします これは静的イニシャライザです つまり実行されるのは 起動開始直後で― main関数が呼び出される前です ここで把握すべきことは 利用するフレームワークの 依存性がもたらす影響です
外部ライブラリやフレームワークは 非常に便利ですが リソースを多く消費します
起動中の処理に 300ミリ秒もかかるようでは デメリットが上回ってしまいます なので代替手段を検討しましょう 代用するのは軽くて効果的なos.log iOSなどのAppleプラットフォームと 相性抜群です
依存性を削除したら もう1つ 必ず実施すべきことがあります
実際のリンクの削除です 静的イニシャライザが 時間を食っているので―
その影響を取り除くため リンクを削除します
では トレースに戻りましょう 次のフェーズはUIKitの初期化です 所要時間は28ミリ秒 これは全Appに共通の固定コストです
UIapplicationのサブクラス化や UIApplicationDelegateで独自の 初期化をしていなければ対処不要です
次のAppの初期化は 制御できる部分が 非常に多くあります didFinishLaunchingWithOptions コールバックでは大量の処理が行われ 実行に791ミリ秒もかかっています では処理内容を見てきましょう
このフェーズでは多くの処理が StarDataProviderクラスで 実行されています “loadStars”とあります メインスレッドは ずっとブロックされています これは起動の遅延を意味します ブロック時間は754ミリ秒です では― この問題を調査しましょう
状態を詳しく調べるために イベントリストを見ます
ブロック状態が754ミリ秒も 続いていたことが分かります このブロックを解除して 実行可能にしたのが スレッド0x12253です このワーカースレッドは 多くの処理をしています 関連性を見ましょう メインスレッドに戻ります 優先度は47に設定されています 47はユーザインタラクティブQoSに 相当します
この赤い部分はすべて CPU不足で多くの処理が 実行できない状態です 理由を調べましょう ワーカースレッドをクリックすると 優先度4の処理が 多数あることが分かります 4はバックグラウンドQoSに相当します この現象は “優先順位の逆転”と呼ばれます QoSの低いスレッドにより QoSの高いスレッドの処理が ブロックされる現象です つまり 起動が遅れるため 好ましくない状況です では改善しましょう
問題の根源である StarDataProviderです SQLiteのデータベースから― 星のデータを取得する とてもシンプルなクラスです バックグラウンドQoSの 専用ディスパッチキューにより データの取得とUIの競合を 回避しています 公開APIが2つあります Grand Central Dispatch(GCD)の 非同期プリミティブを使い― データを非同期ロードするAPI もう1つは同期的に データをロードするAPIです
didFinishLaunchingWithOptions内の 呼び出し部分を見ると― 非同期APIに加えて ディスパッチセマフォも使用しています すべてのデータを取得してから テーブルビューの最初のフレームを 描画するためです
この処理には 適切な並列プリミティブが必要です 具体的にはGCDの同期プリミティブです 適切な並列プリミティブを使うと GCDはメインスレッドの優先度を― 一時的にワーカースレッドに伝え ユーザインタラクティブ同等に上げます
これで優先度の逆転は 解決できそうですが もう1つ問題があります LoadStarDataSync APIは 一連のデータ行を読み込んでいます ここでは0行目から最終行まで すべての行をロードしています しかし最初のフレームには 一部のセルしか収まりません 画面サイズにもよりますが Star Searcherでは10~15個です では 最適化しましょう ロードするのは― 先頭の20行だけにします 最初のフレームの 同期的な描画に必要な分です 残りはあとで バックグラウンドでロードし 起動後にテーブルビューを更新します
トレースに戻りましょう 残りは 最初のフレームを レンダリングするフェーズです
1フレームのレイアウト処理と レンダリングをするだけですが 951ミリ秒もかかっています
処理をしているのは…
StarTableViewControllerです
スタックトレースを見ましょう 多くの処理や cellForRowAtコールバックがあります ここでセルのレイアウトを行います 展開します スタックトレースを見ていくと StarDetailViewController の 初期化処理が多数あります CPUの使用時間は882ミリ秒です つまり これがボトルネックです
コードを見ましょう
cellForRowAtコールバック内の TableViewControllerでは―
カスタムセルを生成 さらにレイアウト中に 投機的最適化を図ります DetailVewのDetailViewControllerの 事前準備とキャッシュを実施して テーブルビューから 詳細ビューへの遷移を効率化します ただ トレースを見る限り このコストは軽微です 冷静に考えると 詳細ビューはタップ後に表示されるので 初期フレームとは無関係です よって この処理を後回しにします
移動先はdidSelectRowAt コールバックにしましょう
これはセルがタップされると 呼び出されます ではStar Searcherに いくつか修正を加えたので 再プロファイルしましょう
ここで重要なのは―
変更のたびに再プロファイルして 効果を測定することです 1つずつ変更を加え その影響を正確に把握するのです ただ 今回はデモなので 時間短縮のために 一括で再プロファイルします まだUIの問題は少しありますが― 起動時間は500ミリ秒以下です
先ほど話したように― プロファイル処理にも 時間がかかります よって 次はユーザの体感速度を 詳しく調べましょう
新しいXCTest APIを使用して 起動パフォーマンスをテストします XCTestでは 数行のコードを追加するだけで 様々なパフォーマンステストができます
では実行しましょう
まずは1回 Appが起動されます 時間のかかる コールドスタートの測定値を― 除外するためです そのあと起動と 所要時間の計測を繰り返します デフォルトでは5回行います 処理がすべて完了すると― 有用な統計データが出力されます 数分ほどかかります さて… Star Searcherの起動時間は2.5秒から わずか約300ミリ秒に短縮されました (拍手) デモの締めくくりに 実際のUIで確認しましょう 一度 終了させます
高速ですね (拍手)
どうも スペンサーに返します (拍手) ありがとう ダン XcodeやInstruments App Launchテンプレを使い 高速化する方法を学びました
皆さんのコードには 数行を修正するだけで 劇的に改善する部分があります そうした5~10ミリ秒の短縮が 積み重なると― 全体として大幅な改善になるのです 我々も支援します 皆さんが何もしなくても Appの起動速度が上がるように iOSの改良を重ねています
具体例を挙げましょう
まずdyld3による ランタイム依存性のキャッシュ デモで示した通り効果絶大です そして起動処理の 優先度設定に役立つスケジューラ AutoLayoutとObjective-Cも精査し 大幅に改善しました 年内にはAppパッケージングも 改良します こうした様々な変更により 皆さんのAppは自然と高速化されます
最後に快適なApp動作を 維持するためのコツをお伝えします
まずパフォーマンスを優先すること バグ修正やリファクタリング 機能の作成時は パフォーマンスを 第一に考えましょう 2ミリ秒程度の軽微なリグレッションは よく発生します その蓄積が大きな問題になるので 気づいた時点で対処しましょう
リグレッションを見つけるには 定期的にプロファイリングと テストを行います これにより目標との差異が 一目で分かります
さらに新しいXcode Organizerで Appの実用性能を把握しましょう iOS 13では ユーザの許可後 電力や性能のデータが収集されます 管理者は24時間分のデータを受信し ソフトやデバイスの バージョン分布を確認できます
さらにデータを管理するには MetricKitがお勧めです
電力やパフォーマンスの メトリクスを指定できます Organizerと同様に 24時間 収集したデータを デリゲートメソッドを介し デベロッパに送ります データは自由に利用できます 詳細は今年の「Improving Battery Life and Performance」をご覧ください
まとめです
今回はXcode Instrumentsの AppLauchテンプレで起動処理を分析し 最小化 優先順位付け 最適化できる場所を探しました
DetailViewコントローラの 事前準備など 改善効果がない場合もありました よって変更後は必ず パフォーマンスを測定しましょう リグレッションは 思わぬ所で発生します
測定は各開発フェーズで 行ってください 様々なデバイスを使用して 新しいXCTestで起動テストを 継続的に実施しましょう 全ユーザの全デバイスで― 常に高速でAppが 起動するようにするためです
詳細は前述の講演を ご覧ください ありがとうございました (拍手)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。