iOSアプリケーションの起動時間の最適化

「マップには次のシナリオがあります。外出先での携帯電話の取得、アプリケーションの起動、現在地の確認、コンパスのナビゲート、移動先、携帯電話の取り外し。



アプリケーションを短時間開いた場合、多くの同様のシナリオがあります。 したがって、アプリケーションがすぐに起動することが非常に重要です。 最近、起動時間を最適化するために多くの作業を行いました。 この経験をあなたと共有したいと思います。」







この資料は、 Mobius 2017カンファレンスでのiOS用モバイルYandex.Mapsの開発責任者であるNikolai Likhogrudによるプレゼンテーションに基づいています。



likhogrudは、Yandexブログにこのトピックに関する投稿を既に書いていますが、会議の最高のレポートの1つをリリースするしかありませんでした。 ビデオ、カットの下のテキスト、およびプレゼンテーションがあり、好みを確認してください。



起動時間を短縮する理由



スティーブジョブズは、人間の最も貴重なリソースは自分の時間だと言いました。 SteveがMacintoshオペレーティングシステムを30分間起動することを強制し、それを10秒だけ加速すると年間3億ユーザー時間を節約できるという動機付けの話を聞いたことがあるかもしれません。 スティーブは、アプリケーションの起動はパフォーマンスの非常に重要な側面だと考えました。



明らかに、同じ機能セットを持つ2つのアプリケーションから、ユーザーはすぐに起動するアプリケーションを選択します。 代替手段がなく、アプリケーションが長時間起動する場合、ユーザーを悩ませ、ユーザーがアプリケーションに戻って悪いレビューを書く可能性が低くなります。 逆に、アプリケーションがすぐに起動する場合は、すべてが素晴らしいものになります。



20秒の起動時間制限を忘れないでください。その後、システムはアプリケーションの読み込みを中断します。 弱いデバイスでは、これらの20秒は本当に超えるのに十分です。



過去6か月または1年で起動時間の最適化について最も積極的に話しているのはなぜですか?アプリケーションの起動時間を最適化するときと並行して、いくつかのチームがプロジェクトで同じことをしましたか? 私たちが愛し、使用しているフレームワークはますますSwiftに書き込まれています。 また、Swiftは静的ライブラリにコンパイルできません。 したがって、時間の経過とともに、アプリケーション内の動的ライブラリの数は増え続けています。



すべてが非常に悪いので、Appleは最新のWWDC( WWDC 2016:Optimizing App Startup Time )での起動時間に別の記事を捧げ、そこで動的ローディングの仕組みの詳細を明らかにしました。

チカは、それがどのようにプロファイリングされ、ローンチ時に一般的に何ができるかを話しました。



スタートアップの最適化に効果的に取り組むために知っておくべきこと

番目のアプリケーション?





開始時間測定



まず、起動時間とその測定方法があります。 起動時間とは、ユーザーがアプリケーションアイコンをクリックしてから、アプリケーションの使用準備が整うまでの時間です。



最も簡単なケースでは、DidFinishLaunching関数の最後、つまりメインアプリケーションインターフェイスがロードされたときに、アプリケーションが使用できる状態にあると想定できます。 ただし、開始時にアプリケーションがデータのどこかに移動し、データベースとやり取りし、UIを更新する必要がある場合は、これも考慮する必要があります。 したがって、打ち上げの終了を考慮すべきことは、各開発者の個人的な問題です。



開始時間があると判断し、測定を開始しました。 時間は非常に跳ね上がることがわかります。 Yandex.Mapsの場合、2回ジャンプします。











ここでは、コールドスタートとウォームスタートの概念を紹介する必要があります。 コールドスタートとは、アプリケーションがかなり前に作業を完了し、オペレーティングシステムのキャッシュから削除されたときです。 コールドスタートは、アプリケーションの再起動後に常に発生します。原則として、WWDCはテストでのモデリングを推奨しています。 ウォームスタートは、アプリケーションが最近完了したときです。



この違いはどこから来たのですか?



打ち上げは、2つの大きな段階で構成されています。





アプリケーションイメージを準備するとき、システムは次のことを行う必要があります。





コールドスタートの場合、このプリメインはウォームスタートの場合よりも1桁長くなる可能性があります。











それだけで、コールドスタートとウォームスタートには大きな違いがあります。



Swiftの場合、動的ライブラリのロード時間は特に長くなります。 したがって、今日はこの段階の最適化に多くの時間を費やします。



したがって、アプリケーションの起動を測定するときは、コードが機能するセグメントだけでなく、システムがWebアプリケーションを収集するときのプリメインも考慮し、コールドスタートも考慮する必要があります。



プリメインメータリング



pre-mainを測定することは、私たちのコードがそこで機能しないため、重要な作業です。 幸いなことに、最新のiOS(9および10)では、AppleはDYLD_PRINT_STATISTICS環境変数を追加し、オンにすると、システムブートローダーの統計がコンソールに表示されます。



















フルタイムのプリメインが出力され、その後段階的に:





メイン測定後



プリメインを測定するための便利なツールがありますが、今はアフターメインを正しく測定する必要があります。



よくある間違いは、didFinishLaunchingのみを測定することです。 ただし、didFinishLaunching、UIApplication、UIApplicationDelegateがまだ初期化されている前に、そこに複雑なコンストラクターを作成することができます。これもすべて考慮する必要があります。 したがって、メインの開始から時間を測定する必要があります。



プロジェクトにmain.swiftファイルがない場合は、ファイルを追加して最初の行に測定開始を配置し、UIApplicationMainを明示的に呼び出す必要があります。









そこで、フルタイムを正しく測定する方法を学びました。 ただし、同じ状況でも、起動時間は大幅にスキップされる可能性があり、デバイスはバックグラウンドで何かを実行できるため、どのような方法でも制御できません。 競馬は20%に達することがあります。









アプリケーションをかなりの金額で改善しようとする場合、これは受け入れられません。 ノイズを取り除くには、多くの開始を行う必要があります。 特に多くのデバイスがある場合は、手動で行うのは高価なので、すべてを自動化したいと思います。

さいわい、libimobiledeviceユーティリティはこの問題を解決します。 ジェイルブレイクを必要としない一方で、xcodeやiTunesと同じプロトコルを介してデバイスと直接やり取りします。 このユーティリティを使用すると、必要なすべてを実行できます。









まず、接続されたデバイスとそのUIDのリストを取得できます。 次に、特定のデバイスにアプリケーションをインストールし、アプリケーションを起動してデバイスを再起動します。 コールドスタートを測定することが重要です。



特に重要なことは、環境変数を渡してアプリケーションを起動できることです(pre-mainを測定するには、変数DYLD_PRINT_STATISTICSを渡す必要があります)。









テスト用アセンブリ



これをすべて使用する方法を理解しましょう。 テスト用にアセンブリを準備する必要があります。 これは、最適化が有効になっているリリース構成である必要があります。





また、環境変数でDYLD_PRINT_STATISTICSが指定されている場合、アプリケーションをロード後に自動的に終了させる必要があります。









次に、各反復で次のスクリプトを作成します。





すべてがうまくいくようです。 しかし、電話の再起動には非常に長い時間がかかるという事実に直面しています。









そしてその後、携帯電話をロードした後、長時間バックグラウンドで何かを行うため、起動時間が非常にジャンプします(時間が40%までジャンプします)。 これは受け入れられません。



幸いなことに、デバイスを再起動せずに、単にアプリケーションを再インストールするだけで、コールドスタートにほぼ似た画像が得られます。 これは、アプリケーションを再インストールするときにキャッシュから削除する必要があるため、論理的です。



再インストール後、明らかにウォームスタートではありません。 同時に、再起動後の開始時間と再インストール後の開始時間が接続され、一方を減らすと、他方も同時に減少します。 また、再インストールははるかに高速であり、それほど広い範囲の値はありません。



したがって、スクリプトを少し変更します。





このようなスクリプトを使用して、プリメイン、コールドスタート、ウォームスタートを考慮に入れて、さまざまなデバイスの起動統計を収集し、これらの統計をさらに構築できます。









Swiftの空のプロジェクト



すぐに最適化に進むことができるように思えるかもしれません。 しかし、実際にはそうではありません。 最初に、私たちが努力する必要があるものを見る必要があります。



また、空のプロジェクトよりも速くアプリケーションを起動することはできないため、空のプロジェクトを起動する時間を確保する必要があります。



そして、ここで私たちは不快な驚きスウィフトに出会います。 Objective-Cで簡単なアプリケーションを使用して、そのプリメインを測定します(たとえば、iPhone 5Sで)。









動的ライブラリライブラリをロードするのに1ミリ秒もかかりません。 Swiftで同じアプリケーションを作成し、同じデバイスで実行します。









動的ライブラリのロード-200ミリ秒-2桁以上。 iPhone 5でアプリケーションを実行すると、動的ライブラリのロードには約800ミリ秒かかります。









何のために、理解する必要があります。 これを行うには、ダイナミックライブラリのロードチェックボックスを有効にして、ロードされたダイナミックライブラリをダウンロードできるようにします。









そしてログを見てください:









Objective-CとSwiftのプロジェクトログを比較します。 どちらの場合も、システムライブラリである146個の動的ライブラリとバイナリアプリケーションがロードされていることがわかります。 ただし、Swiftはさらに、libswift ***。DylibというFrameworksフォルダーから、アプリケーションバンドルからさらに9つの不審なライブラリーをロードします。 これらは、いわゆる迅速な標準ライブラリです。

アプリケーションのコンパイルログを確認したことがある場合、最後の手順の1つは迅速な標準ライブラリの対処です。 彼らはどこから来たのですか?



実際、Swiftは非常に迅速に開発されており、その開発者はバイナリの下位互換性を気にしません。 したがって、swift 3.0でモジュールをビルドすると、swift 3.01でもモジュールを使用できなくなります。 コンパイラーは、これができないと書いています。 SwiftはまだiOSの一部になれません。そうしないと、古いiOSでは新しいswiftが起動しません。 したがって、アプリケーションは常にSwiftランタイムを実行します。これは、システムの一部であったObjective-Cとは異なり、迅速なサポートライブラリです。



したがって、次の結論が得られます。





さあ、ハードコアな最適化に移りましょう。



事前メイン最適化



理論的に何が影響を受けますか? WWDCはプレゼンテーションで何を推奨していますか? まず、手順を思い出してください。





Appleの推奨事項:





迅速なアプリケーションがあります。 大きいです。 すでに少しのObjective-Cがあります-SDKを使用してシステムとやり取りする必要がある場合のみ。 後者では、swift + loadはすでに禁止されています。 グローバルなC ++変数はありません。 したがって、残念ながら、これらの推奨事項のほとんどはもはや関係ありません。 動的ライブラリを処理し、開始時にロードされるバイナリファイルのサイズを何らかの方法で削減すること、つまり、シンボルを動的ライブラリにエクスポートして遅延ロードすることのみを目的としています。 これにより、必然的に起動時間が短縮されます(リベース/バインディング、objc-setupが減少します)。



動的ライブラリ最適化



テストアプリケーションを最適化してみましょう。









swiftで記述された多くのポッドを含むテストアプリケーションがあり、その内部のどこかで静的ライブラリであるYandex MapKitとYandexSpeechKit、およびiOS SDKのMapKitを使用するとします(この仮定は後で明らかになります)。









開始時間を測定します。









空のアプリケーションの3倍の高さです。 主に動的ライブラリ、つまり 空のアプリケーションの場合は200ミリ秒、テストアプリケーションの場合は既に600ミリ秒が読み込まれます。この理由は何ですか?











最も単純なものから始めましょう:炉から来る不要な動的ライブラリの負荷を取り除く方法は? これを行うには、cocoapods-amimonoと呼ばれるココアポッドプラグインを使用します。 ポッドによって生成されたスクリプトとxcconfigsにパッチを適用し、podovskieライブラリのライブラリをコンパイルした後、残りのオブジェクトをすぐにアプリケーションのバイナリファイルにリンクし、動的ライブラリとリンクする必要性を取り除きます。 絶対に素晴らしいソリューション。 そして、それは素晴らしい作品です。 使い方は?



Podfileで、プラグインの使用を追加し、インストール後。









運がよければ、すべてがすぐにコンパイルされます。 そうでない場合は、いじる必要があります。 しかし、最終的には、起動時間はほぼ半分になりました。動的ライブラリの読み込み時間は600秒から320秒に短縮されます。









これは、炉からのすべての動的ライブラリが私たちから消えたという事実によるものです。



残念ながら、このソリューションには大きなコミュニティが存在しないという事実に関連するいくつかの欠点がありました(人々はほとんど自分で作成しました)。





あなたはこれすべてで生きることができます。 このツールをかなり大きな3つのアプリケーションで試しましたが、毎回約1時間半の間、リンクを修正するのに時間がかかりました。 しかし、最終的にはすべてがうまく機能します。



Objective-Cラッパー



そのため、ダイナミックライブラリのロード時間は320ミリ秒になりました。 これは、空のプロジェクトの1.5倍です。 なぜこれが起こっているのですか?



バンドルのFrameworksフォルダーに残っているものを見てみましょう。 5つの新しい動的ライブラリが追加されました。









これらは、libswiftAVFoundation.dylib、libswiftCoreAudio.dylib、libswiftCoreLocation.dylib、libswiftCoreMedia.dylib、libswiftMapKit.dylibと呼ばれています。 彼らはどこから来たのですか?



迅速なコードのどこかにCoreLocationをインポートするか、ブリッジングヘッダーに<CoreLocation / CoreLocation.h>をインポートすると、システムはlibswiftCoreLocation.dylibライブラリをアプリケーションバンドルに自動的に追加します。 したがって、ローディング時間が長くなります。 残念ながら、迅速にCoreLocationを使用しないこと以外に解決策は見つかりませんでした。









したがって、Objective-Cでそれをラップします。



リファクタリングを簡素化するために、使用するCoreLocationの一部のみを取得し、まったく同じラッパーを記述できます。ただし、CoreLocationの代わりに、CLWLocationManager(コアロケーションのラップを意味する)、CLWLocation、CLWHeadingなどを使用します。 その後、Swiftではこれらのラッパーのみを使用できます-ライブラリは追加されていないようです。



私はそうしましたが、すぐに「離陸しませんでした」。



CoreLocationは、ブリッジングヘッダーに追加されるヘッダーファイルの依存関係をインポートできることが判明しました。 また、Objective-Cでラップするか、ブリッジングヘッダーによって何らかの形でリファクタリングする必要があります。 また、CoreLocationは、MapKitなどの他のSDKライブラリの依存関係としてインポートできます。 MapKitは、libswiftCoreLocation.dylibとlibswiftMapKit.dylibの2つのライブラリを同時にドラッグし、AVFoundationは通常3つのライブラリを同時にドラッグします。









つまり、import AVFoundationを記述し、SwiftファイルのどこかにMapKitをインポートすると、このAPIを使用しなくても、動的ライブラリのロード時間はすぐに1.5倍になります。 したがって、ラッパーを作成し、その後、空のプロジェクトの動的ライブラリのロード時間に戻ります。









さらに先へ進む場所はありません-10ミリ秒かかりました。 残りの段階を減らすために少し苦しむことは残っています。









dlopenからダウンロード



私が言ったように、ここでは個々のステージを最適化するためのAppleの推奨事項はもはや適切ではありません。



ここでは、スタート画面を表示するのに必要なキャラクターだけがスタート時にロードされるべきであるという平凡な考えに導かれる必要があります。 理想的には、一般に、アプリケーションバイナリは、開始時に必要な文字のみで構成する必要があります。 他の誰もが遅延して出荷するのは素晴らしいことです。 テストアプリケーションでこれを行う方法

前に、アプリケーション内でYandex MapKit、YandexSpeechkitを使用すると述べました。 これらの静的ライブラリを動的にし、dlopenを介して遅延ロードするのは素晴らしいことです。 これにより、起動時にロードされるObjective-Cの文字が少なくなるため、リベース/バインド、オブジェクトの起動、および初期化の時間が短縮されます。 便利な方法でそれを行う方法は?

まず、それらを動的ライブラリに変換します。 これを行うには、静的ライブラリごとに、アプリケーションで個別のCocoa Touch Frameworkターゲットを作成し、Podfileで作成したターゲットの下にターゲットを追加します。 リンクを修正するためだけに残ります(仕様に適切に記述されている場合、すぐにリンクされますが、仕様に不十分に記述されている場合は、リンクを修正する必要があります)。



メインターゲットでは、それぞれ:





実際、後者は最も難しい瞬間です。 恐ろしいほど複雑ではありませんが、ここではdlfcnのdlopen APIを使用する必要があるためです。









以前、このAPIを非常に恐れていました。 そこではすべてが怖くて複雑であるように思えました。 しかし、実際には、すべてがそれほど悪くはありません。



開始するには、dlopenを使用して動的ライブラリをロードできます。 最初のパラメーターとしてライブラリーへのパスのみが必要です(ライブラリーはバンドルのFrameworksフォルダーにあります)。 dlopenは、後で個々の文字をロードするdlsym関数で使用されるハンドルを返します。



ここで、関数シンボルが必要な場合は、dlsymを使用して、関数の名前(ソースライブラリにあった)を渡します。 dlsymは、この関数へのポインターを返します。









さらに、ポインターによる通常の構文でこの関数を呼び出すことができます。 同じ方法でグローバル変数を使用して-名前でdlsymを介してロードすると、このグローバル変数のアドレスが返されます。 次に、それを間接参照する必要があります。 すべてが最初に思われたほど複雑ではありません。



クラスはもう少し複雑ですが、あなたはまだ生きることができます。









まず、以前の静的ライブラリをインポートできます。 これにより、ダウンロードは行われず、リンクエラーも発生しません。 これらはコンパイラに対する単なる一種の宣言であり、直接使用するまでリンクエラーは発生しません。 それでも、このh-nicknameはインポートする価値があります。



次に、何らかのクラスが必要だと仮定します。 シンボルの名前は、クラスの名前と接頭辞OBJC_CLASS_ $で構成されます。 dlsymは、クラスをメタクラス全体のインスタンスとして返します。 次に、このメタクラスインスタンスに対して、allocを呼び出すことができます。 これにより、タイプIDのオブジェクトが返されます。 そして、Objective-Cの魔法が始まります。ここでは、コンパイラーが知っているタイプIDのオブジェクトでセレクターを呼び出すことができるからです。 そして、セレクターはライブラリーのインポートされたh-nicknameで宣言されています。 次に、目的のオブジェクトにキャストし、ライブラリのAPIを使用できます。 すなわち 全体の問題は、クラスシンボルをロードする必要があるだけで、それを通常どおり使用することです。



これにより何が得られますか?



実際、私たちのテストプロジェクトでは、これはあまり効果がなく、わずか30ミリ秒です。









ただし、それでも、そのような最適化のソースを検討する必要があります。 おそらく、開始時に必要ではないが、開始時のシンボルがまだロードされている大きな静的ライブラリをどこかで使用している可能性があります-アプリケーションに依存します。



当然、dlopenを使用すると、いくつかの依存関係をロードできるだけでなく、コードをモジュールに分割して遅延的にロードすることもできます。 つまり、アプリケーションバイナリを可能な限り小さくするよう努めています。 もちろん、これには強力なリファクタリングが必要です。 しかし、私が書いたことは十分に迅速に行うことができます。



以下に、アプリケーションの起動を3段階で3回最適化した方法の概要を示します。











Objective-Cにプロジェクトがある場合、最初の2つの段階は、おそらく動的ライブラリがないため、おそらく必要ありません。 しかし、dlopenについて考える必要があります。



dlopenを手書きで作成するのではなく、どのように自動化できますか? Facebook SDKを見ることができます。 そこでは、マクロシステムを通じて行われます。



クラスの各シンボルを使用して、動的ライブラリのロード(まだロードされていない場合)およびシンボルのロード(まだロードされていない場合)でラップする必要があります。 はい、かさばる構造が取得されますが、これをすべてマクロで実行できるため、文字をロードするためのコードはマクロを使用する1行にまとめられます。



メイン後の最適化



アフターメインについて少し話すことは残っています。 少し-実際、これはiOSアプリケーションを最適化するための一般的なタスクであり、その多くについてはすでに記述されているためです。 基本的には、アプリケーション自体に依存します。 どのような用途にも適した、ある種の超一般的なスキームを思い付くことはできません。









しかし、それでも、Mapsで遭遇したことと何が機能したかについてお話します。



アプリケーションにある3つのことを確認することをお勧めします。





マップの場合、どのように表示されますか?



過剰な依存関係の最適化



MapsにはRootViewControllerクラスがありました。これは、依存関係注入の標準的なアプローチに従って、コンストラクターを通じて依存関係を受け入れました。









つまり、RouteControllerを作成するには、最初にFacade SearchおよびFacade Routingを作成する必要があります。 , .



, . . , , -, , -, , , .



, . :





それはどのように見えますか:









RootViewControllerDeps, , SearchFacade RoutingFacade. RootViewController .



lazy var:









searchFacade routingFacade , RootViewController, .. , . Composition Root — , .









.



?





UI



view, . :





NavigationController. NavigationController, Push.









NavigationController NavigationBar — . , NavigationController UI. ?









. - , NavigationController, , — , NavigationController , . , , NavigationController.









SplitViewController ( iPad), iPhone . iPad , . , . , - .



View . , view-, , .., .









, . , , , . Navigation Bar, . . Navigation Bar , , , . .



— . , Image, background. , . — - SplitViewController, - . , , .



UI , :





, .





, after-main.

. , , . , UI, , , , . , ? , - 200-300 , UI, ( ).



:





. , . , - .





- ( ) — . , .

- continuous integration, . すなわち - , , , . , mail.ru, . .



-, Composition Root, .









generic-, -, instance . - — , . composition root, , , ( , ).









, trackCreation . .



どうしてですか? , - , , .



swift — . , . Objective-C runtime, .









objc_copyImageNames , . , — , , . , . , , . - , , - iOS: - , - , , swift, - Objective-C . , . , .



:









— sysctl. . , , , , , , , .. pre-main.









, ? DYLD_PRINT_STATISTICS. , , DYLD_PRINT_STATISTICS . , . sysctl, , gettimeofday, - , , start main. .



結論の代わりに



, .









30%.



. Objective-C AVFoundation, MapKit CoreLocation ( , ). swift- , S5 — 2,3 . , , swift . — . Obj setup, rebase/bind - SpeechKit. — - .



— after-main, : main didFinishLaunching didFinishLaunching. main didFinishLaunching , , didFinishLaunching 40-50% view-tree 200-300 .



Apple , !









, , . , . iPhone 5S 30% , iPhone 7, 6S 5 .



, .









, , pre-main . after-main — , , , . , didFinishLaunching 30%, 40%, , 37%. .









5S . iPhone 6S 7 2 .



, , , . . AppStore.



, . — .









- , - , . (, iPhone, iPhone). - .



: pre-main after-main. , pre-main- — DYLD ( ), after-main — .

pre-main , -, cocoapods swift standard libraries. -, .



after-main .., . ( UI, ), .



.






, Mobius 2017 Moscow :






All Articles