アリデータiOS端起動速度最適化の心得


背景
7月26日に私達のアリデータiOS端末は4.4.4バージョンを発表しました。今回のバージョンは主に性能を最適化しました。ここでmain()段階の起動時間の最適化の結果が明らかになりました。前の0.5秒から0.7秒まで、現在の0.1秒から0.2秒まで減少しました。ここで最適化したいくつかの経験を整理して、みんなで交流することを歓迎します。
アプリケーション起動フロー
iOSアプリケーションの起動はpre-main段階とmain()段階に分けることができます。ここでシステムがすることは順次以下の通りです。
1.pre-main段階
1.1.アプリケーションをロードする実行可能ファイル
1.2.ダイナミックリンクライブラリキャリアdyld(dynamic loader)をロードする
1.3.dyld再帰的にすべての依存dylibを適用する(dynamic libraryダイナミックリンクライブラリ)
2.main()段階
2.1.dyld呼び出しmain()
2.2.UApplication Mainを呼び出します()
2.3.appication WillFinish Launchingを呼び出します。
2.4.didFinish LaunchingWithOptionsを呼び出します。
起動時間の測定
最適化の前に,まず各段階の時間消費を測定できるべきである。
1.pre-main段階
pre−main段階に対して、Appleは環境変数DYLD_をXcodeにおいてEdit scheme->Run->Augmentsで測定する方法を提供している。PRINT_STATISTICSは1に設定します。

pre-main段階起動時間計測.png
セッティングしてプログラムを走らせば、コンソールから次のように出力されます。pre-main段階では、各プロセスの時間がかかります。

pre-main段階起動時間計測.png
2.main()段階
メインフェイズについては、主に測定main()関数がdidFinish LaunchingWithOptionsに実行され始めた時に、自分でコードを入れて工程に入ります。まずmain()関数に変数StartTimeで現在の時間を記録します。

CFAbsoluteTime StartTime;
int main(int argc, char * argv[]) {
   StartTime = CFAbsoluteTimeGetCurrent();
また、AppDelegate.mファイルでグローバル変数StartTimeをexternで宣言します。

extern CFAbsoluteTime StartTime;
最後にdidFinish LaunchingWithOptionsで現在の時間を取得してください。StartTimeとの差がmain()段階での運行時間がかかります。

double launchTime = (CFAbsoluteTimeGetCurrent() - StartTime);
pre-main段階の最適化
pre-main段階の時間消費を最適化するには、dyldローディングの過程をもっと勉強しなければなりません。AppleのWWDCでの紹介によると、dyldのローディングは主に4ステップに分けられます。
1.Load dylibs
この段階でdyldはアプリケーションに依存するdylibを分析して、mach-oファイルを見つけて、これらのファイルを開けて読んで、その有効性を検証します。続いてコード署名を見つけてカーネルに登録します。最後にdylibのsegmentにmmapを呼び出します。
一般的には、iOSアプリケーションは100-400個のdylibsをロードします。大部分はシステムライブラリです。この部分はdylibのローディングシステムが最適化されています。
だから、依存のdylibは少なければ少ないほどいいです。この段階で、私達ができる最適化は以下の通りです。
インライン(embedded)のdylibを使用しないようにします。インラインdylibの性能オーバーヘッドが大きいです。
既存のdylibとスタティックライブラリを統合し、dylibの使用個数を減らす。
怠惰でdylibをロードしますが、dleopen()が問題を起こす可能性があります。実際に怠惰にやっている仕事がもっと多いです。
2.Rebase/Bind
dylibのローディング中、システムは安全を考慮するためにASLR技術とコード署名を導入した。ASLRの存在により、ミラー像は、実行可能ファイル、dylib、及びbundleを含む。ランダムなアドレスにロードされ、前のポインタが指すアドレス(preferred gau)となる。addressは偏差があります。dyldはこの偏差を修正して正確な住所を指す必要があります。
Rebaseは前に、Bindは後に、Rebaseはイメージをメモリに取り込んで、ミラー内部のポインタを修正して、性能は主にIOに消耗します。Bindは照会符号表を作成し、ミラーの外部を指すポインタを設定し、性能消耗は主にCPUで計算する。
だから、針の数が少ないほどいいです。この段階で、私達ができる最適化は以下の通りです。
ObjCクラス、メソッド、分類の数を減らす
C++虚関数の数を減らす(虚函数表作成にはオーバーヘッドがある)
Swift stuctsを使う(内部で最適化され、符号の数が少ない)
3.Objc setup
ObjC初期化のほとんどはRebase/Bind段階で完了しました。この段階でdyldは全声明のObjC類を登録して、分類をクラスの方法リストに挿入して、各selectorの一意性を確認します。
この段階で最適化することは何もありませんが、Rebase/Bind段階で最適化されました。このステップの消費時間も減少します。
4.Initializers
この段階になると、dyldはプログラムの初期化関数を起動して、Objcクラスと分類の+ロード方法を呼び出して、C/C++の中のコンストラクタ関数(atribute(constructor)で修飾された関数を呼び出して、非基本タイプのC++静的なグローバル変数を作成します。Initializers段階の実行が完了したら、dyldからmain()関数を呼び出します。
この段階で、私達ができる最適化は以下の通りです。
これらのことをできるだけ+initializeに延期します。
コンストラクタの数を減らして、コンストラクタの関数の中で少しの事をします。
C++静的大域変数の個数を減らす
メインステージの最適化
この段階の最適化は主にdidFinish LaunchingWithOptionsの方法の中の仕事を減らすことで、didFinish Launching WithOptionsの方法の中で、私達は応用のwindowを創建して、そのroot ViewControllerを指定して、windowのmakeyAndVisibleの方法を呼び出してそれを可視させます。業務上の必要性から、各二方/三方倉庫を初期化し、システムUIスタイルを設定し、案内ページを表示する必要があるかどうか、ログインする必要があるかどうか、新しいバージョンがあるかどうかなどを確認します。
ですから、業務の必要を満たす前提で、didFinish LaunchingWithOptionsがメインスレッドですることが少ないほどいいです。この段階で、私達ができる最適化は以下の通りです。
各二乗/三方ライブラリを整理して、最初のページのコントローラのviewDidAppar方法に入れるなど、ロードを遅延させるライブラリを見つけます。
業務ロジックを整理して、実行を遅らせることができるロジックを、遅延実行処理します。例えば、新しいバージョンをチェックしたり、登録したり、プッシュ通知したりするロジックです。
複雑・余分な計算を避ける。
トップページのコントローラのview DidLoadとview Willapearで多すぎることを避けるために、この2つの方法は実行し終わって、トップページのコントローラは表示することができます。一部は作成を遅らせることができます。
より性能の良いAPIを採用する。
トップページのコントローラは純粋なコードで構成されています。
アリデータiOS端最適化実践
以上の認知指導の下、アリデータiOS端末は最適化に着手し、pre-main段階とmain段階でそれぞれ一連の最適化を行い、一定の成果を上げました。
1.pre-main段階の最適化
1.1.不要なdylibを検査し、使用しないlibicucore.tbdを除去する。
1.2無駄なファイル&ライブラリを削除し、重複したファイル(複数の重複した分類)をマージします。使用しないライブラリUMSocial、PSTCollection View、MCSwipeTable ViewCellを除去し、機能が重複するライブラリMantleを除去する。
1.3.各類の+ロード方法を整理して、複数種類の中+ロード方法ですることを遅延+initializeにします。
最適化前のpre-main段階の時間消費:

最適化前のpre-main段階の時間消費.png
最適化後のpre-main段階の所要時間:

最適化後pre-main段階は時間がかかります。png
テスト環境:Xcode 8.3.3 iOS 10.2のシミュレータ、熱起動。
備考:テストで発見されたのですが、pre-mainの段階では一定の変動があり、寒い起動時にはもっと変動があります。ここでスクリーンショットを貼るのはその中の桁数のレベルです。
熱起動下にpre-main段階の消費時間は一定の低下が見られます。
2.main()段階の最適化
2.1.その中の100 msのdispatch_を取り除く。after…コードが発見される前にわざと起動図を100 ms多く表示させます。どんなロジックですか?
2.2.複数の二乗/三方倉庫を荷重を遅延します。TB Crash Report、TB Access SDK、UT、TRement Debugger、ATSDKなどを含みます。
2.3.いくつかのシステムUI構成、トラヒックロジックを遅延して実行する。登録推送、新バージョンの確認、Orange構成の更新などがあります。
2.4.余分な計算は避ける。広告図を表示するかどうかは前後2回取得しますが、取得するたびに、順序を逆にするOrangeの設定情報を必要とします。また、設定中の開始/終了時間を比較すると、約20 msかかります。現在の解決策は初めて計算した後、一つのBOOL属性でキャッシュして、今度直接取ります。
2.5.遅延負荷&怠惰ロード部分図。ショートカットパスワードの検証ページは起動図が消えた後にユーザーが見た最初のページで、このページはピクチャの復号、複数のビューの作成&レイアウトに関わるため、view DidLoad段階は100 msぐらいかかります。現在のソリューションは、パスワード入力ボックスの表示をview DidApparに遅くしてロードし、パスワードのエラー表示を怠惰にロードし、時間を30 mぐらいに低減します。
instruumentsのTime Profiler分析によって、最適化後の起動速度は明らかに向上し、didFinish LaunchingWithOptionsは75 ms前後で消費される(iPhone 6 s iOS 10.3.3)

起動時間がかかります
その中で最も時間がかかっているのはショートカットパスワード検証ページの作成とレイアウトで、次にDorch View Controlleで広告ページを表示するかどうかの判定コードです。PAPasscodeview Controllerのview DidApppearは78 msかかりましたが、もう大きな関係はありません。この時、ユーザはすでにページを見て、指紋/パスワードを検証するつもりです。
まとめ&フォローアップ企画
1.まとめ
まとめてみると、起動速度の最適化は一言で言えば、システムを起動中に少なくするようにします。もちろん私達は先に工事の中でするどれらの事がスタートの時にするので、スタートのスピードに対する影響はどれだけ大きいかを明確にして、それからcase by caseは工事のコードを分析して、子スレッドに置いて、遅延して、怠惰にロードしますを通じて(通って)システムをスタートの時にもっと楽になります。
2.後続計画
2.1.代替部分の巨大な倉庫は、より軽量級のソリューションを採用する。
2.2.コードを整理し、重複した実装を除去し、機能が重複するクラス&分類&方法が現れないようにする。
2.3.オフラインされた業務に関するクラスと分類と方法を整理し、除去する。
2.4.階調バージョンの起動速度の変化傾向を監視し、早期に起動速度を遅くする問題を発見し解決する。