皆さんこんにちは、エンジニアのおうです。aumoでは主にiOSアプリの開発をやっています。
先月子供が産まれて、生活のリズムがだいぶ変わりました。やることが多すぎで結構寝不足です。子育てが大変だなと思いながら、赤ちゃんの成長を見て幸せな日々を過ごしています。
さて、今日お話するのはアプリの起動速度に関しての改善プロジェクトについてです。
目次
背景
アプリの起動速度はユーザーのサービス体験に影響与えます。また、起動が遅くなればなるほど、ユーザーを失望させてしまいます。ストアで低評価がつく原因になったり、ユーザーに使われなくなる可能性があります。
aumoのアプリでは、起動時にユーザー認証、天気情報の取得、DM通知の受信、ABテストなどを行っています。 加えて外部のsdkを入れている、使わないコードが残こってる、重複しているメソッドがあったりと、アプリのサイズが大きくなり、起動スピードが遅くなってしまう原因が存在します。 今回これらを改善したプロセスを紹介します。
アプリ起動の流れ
アプリの起動は、pre-main部分とmain()部分2段階があります。
アプリ総起動時間T= pre-main部分の起動時間T1 + main()後の起動時間T2
- pre-main time:アプリの最初の機能(main)が実行されるまでの時間。この時間は、アプリが正しく起動するために必要なリソースをバインドするシステムに予約されています。
- main() time:アプリがUIApplicationMain(またはメイン関数)からユーザーに表示される対話可能な最初の画面に制御を移している時間。簡単に言うと、 main()が呼ばれた後から、AppDelegateクラスの didFinishLaunchingWithOptions launchOptionsメソッドの終わりまでの時間です。メインの仕事は各機能の初期化、また最初の画面構築などを行います。
速度の測定
T1時間
ここの計測はAppleから測定方法を提供され、Xcodeから、「Edit scheme」→ 「Run」→ Augumentsで環境変数DYLD_PRINT_STATISTICSを1に設定します。
これでアプリを起動するとpre-main部分の時間が出てきます、こんな感じです。(AppleのこのDemoがちょっとやり過ぎかもです)
T2時間
main()段階では、main()メソッドからdidFinishLaunchingWithOptionsが終わるまでの時間を測るので、自分でコードを入れる必要があります。まずstartTimeを定義します。
private var startTime: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()
didFinishLaunchingWithOptionsの終わりのところに
let launchTime = (CFAbsoluteTimeGetCurrent() - startTime)
これで現在の時間とstartTimeの差分がT2が発生した時間になります。
aumoのログはこんな感じです。
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 177.57 milliseconds (14.6%)
rebase/binding time: 65.50 milliseconds (5.3%)
ObjC setup time: 332.50 milliseconds (27.3%)
initializer time: 638.39 milliseconds (52.5%)
launch time: 1.0095289945602417
改善の実践
現状を把握した上で、これからそれぞれの部分について改善を行います。
T1部分でやったこと
・使わないクラス、メソッドなどを排除する
ここのやり方について、ネットでいろんな情報が載っていますが、個人的に「unused.rb」このツールがおすすめです。
$ ruby unused.rb
実行するだけで、重複するクラス、メソッドなどの一覧が出ます。
Item< let limit [private static] from: Aumo/Classes/ViewModels/Home/Ranking/XXXXXXViewModel.swift:13:0>
Item< let relatedArticleLimit [private static] from: Aumo/Classes/ViewModels/Contents/XXXXXXViewModel.swift:15:0>
Item< let relatedArticleLessLimit [private static] from: Aumo/Classes/ViewModels/Contents/XXXXXXViewModel.swift:16:0>
Item< func insertAds [private] from: Aumo/Classes/ViewModels/Contents/XXXXXXViewModel.swift:627:0>
Item< func insertRanking [private] from: Aumo/Classes/ViewModels/Contents/XXXXXXViewModel.swift:633:0>
Item< func loadMoreRelatedPhoto [private] from: Aumo/Classes/ViewModels/Contents/XXXXXXViewModel.swift:272:0>
ただ、外部からのcallback、delegateも対象になるため、適当に削除すると機能しなくなるので、そこだけ気をつければかなり便利なツールになります。また、Appleでは「Using Code Coverage」も提供されているので、メソッドがどのぐらい使われるかがわかります。興味のある方はそっちも試してみるのがよいかと思います。
・要らないdylib、libicucore.tbdなどを削除する
・ライブラリー管理について、aumoではCocoapodsとCarthage両方使っていますが、Mach-O Typeをなるべくstatic Libraryを使う
これを実践した結果、時間を約0.4秒短縮できました。
Total pre-main time: 828.20 milliseconds (100.0%)
dylib loading time: 67.65 milliseconds (8.1%)
rebase/binding time: 70.57 milliseconds (8.5%)
ObjC setup time: 81.94 milliseconds (9.8%)
initializer time: 608.03 milliseconds (73.4%)
T2部分でやったこと
ここでは、なるべくdidFinishLaunchingWithOptions内の仕事を減らします。didFinishLaunchingWithOptionsでは、windowを作って、rootViewControllerを指定して、外部ライブラリーの初期化、ユーザー認証、バージョンチェックなども行います。気をつけないとファットになりがちな部分です。
・各初期化のタイミングを見直し
外部のsdkでは、didFinishLaunchingWithOptions時に初期化するのがスタンダードですが、そこを見直して初期画面に要らない初期化は後回しにしました。
・バージョンチェック、DM情報、天気情報など、初期画面に不要な処理を後回し
・初期画面の処理はなるべくviewWillAppearではなく、viewDidAppearに移動した
・static変数を減らして、またclassじゃなくてstructをなるべく使うようにした
・XcodeのTime Profilerツールを使って、重い処理を細かく改善を行なった
画面右側の箇所に入ったところにメソッドそれぞれの重い箇所と重みの割合が表示されます。
そこは地道に一個づつ改善するしかないので、結構体力と時間がかかります。ただ全部やると膨大な量になるので、まず今回は気になっていた一部分だけ対応しました。
上記のように実施したところ、T2は約0.35秒速くなりました(思ったより少ない。。)
launch time: 0.6698949337005615
おわりに
当たり前ですが、起動時にやることが少なくなればなるほど速度が上がります。今回は割と最小限のことしかやってないですが、まぁ効果がありました。やはり開発時に速度に対する意識と普段からのメンテナンスが大事だなと改めて思いまいた。また今後実践してみたいことはこちらです。
・重い、古いライブラリーの差し替え、もっと軽いものを選ぶ
・コードの整理、重複したコードなどを消す
・重い処理の見直し
・初期画面の表示はstoryboard、xibを使うと重くなるので、画面をコードだけで作り直す
・XcodeまたiOSのバージョンによって起動スピードも変わるので、定期的にチェックする