iOS啟動速度最佳化

來源:互聯網
上載者:User

標籤:顯示   時間   靜態   www   資料   使用者   DApp   tail   虛函數   

背景

7月26號我們阿里資料iOS端發布了4.4.0版本,這次版本主要是最佳化了效能,其中main()階段的啟動耗時最佳化成果比較明顯,從之前的0.5-0.7秒,降低為目前的0.1-0.2秒(main()第一行代碼到didFinishLaunchingWithOptions最後一行代碼的耗時),使用者體驗提升明顯。在這裡梳理一下最佳化的一些經驗,歡迎大家一起交流。

應用啟動流程

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. 調用UIApplicationMain() 

2.3. 調用applicationWillFinishLaunching

2.4. 調用didFinishLaunchingWithOptions

啟動耗時的測量

在進行最佳化之前,我們首先應該能測量各階段的耗時。

1. pre-main階段

對於pre-main階段,Apple提供了一種測量方法,在 Xcode 中 Edit scheme -> Run -> Auguments 將環境變數DYLD_PRINT_STATISTICS 設為1 :

pre-main階段啟動耗時測量.png

設定好後把程式跑起來,控制台會有如下輸出,pre-main階段各過程的耗時一覽無餘(Apple這個Demo有點過於誇張...)

pre-main階段啟動耗時測量.png

2. main()階段

對於main()階段,主要是測量main()函數開始執行到didFinishLaunchingWithOptions執行結束的耗時,就需要自己插入代碼到工程中了。先在main()函數裡用變數StartTime記錄目前時間:

123 CFAbsoluteTime StartTime;int main(int argc, char * argv[]) {      StartTime = CFAbsoluteTimeGetCurrent();

再在AppDelegate.m檔案中用extern聲明全域變數StartTime

1 extern CFAbsoluteTime StartTime;

最後在didFinishLaunchingWithOptions裡,再擷取一下目前時間,與StartTime的差值即是main()階段運行耗時。

1 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越少越好。在這一步,我們可以做的最佳化有:

  1. 盡量不使用內嵌(embedded)的dylib,載入內嵌dylib效能開銷較大

  2. 合并已有的dylib和使用靜態庫(static archives),減少dylib的使用個數

  3. 懶載入dylib,但是要注意dlopen()可能造成一些問題,且實際上懶載入做的工作會更多

2. Rebase/Bind

在dylib的載入過程中,系統為了安全考慮,引入了ASLR(Address Space Layout Randomization)技術和程式碼簽署。由於ASLR的存在,鏡像(Image,包括可執行檔、dylib和bundle)會在隨機的地址上載入,和之前指標指向的地址(preferred_address)會有一個偏差(slide),dyld需要修正這個偏差,來指向正確的地址。

Rebase在前,Bind在後,Rebase做的是將鏡像讀入記憶體,修正鏡像內部的指標,效能消耗主要在IO。Bind做的是查詢符號表,設定指向鏡像外部的指標,效能消耗主要在CPU計算。

所以,指標數量越少越好。在這一步,我們可以做的最佳化有:

  1. 減少ObjC類(class)、方法(selector)、分類(category)的數量

  2. 減少C++虛函數的的數量(建立虛函數表有開銷)

  3. 使用Swift structs(內部做了最佳化,符號數量更少)

3. Objc setup

大部分ObjC初始化工作已經在Rebase/Bind階段做完了,這一步dyld會註冊所有聲明過的ObjC類,將分類插入到類的方法列表裡,再檢查每個selector的唯一性。

在這一步倒沒什麼最佳化可做的,Rebase/Bind階段最佳化好了,這一步的耗時也會減少。

4. Initializers

到了這一階段,dyld開始運行程式的初始化函數,調用每個Objc類和分類的+load方法,調用C/C++ 中的構造器函數(用attribute((constructor))修飾的函數),和建立非基本類型的C++靜態全域變數。Initializers階段執行完後,dyld開始調用main()函數。

在這一步,我們可以做的最佳化有:

  1. 少在類的+load方法裡做事情,盡量把這些事情延遲到+initiailize

  2. 減少構造器函數個數,在構造器函數裡少做些事情

  3. 減少C++靜態全域變數的個數

main()階段的最佳化

這一階段的最佳化主要是減少didFinishLaunchingWithOptions方法裡的工作,在didFinishLaunchingWithOptions方法裡,我們會建立應用的window,指定其rootViewController,調用window的makeKeyAndVisible方法讓其可見。由於業務需要,我們會初始化各個二方/三方庫,設定系統UI風格,檢查是否需要顯示引導頁、是否需要登入、是否有新版本等,由於曆史原因,這裡的代碼容易變得比較龐大,啟動耗時難以控制。

所以,滿足業務需要的前提下,didFinishLaunchingWithOptions在主線程裡做的事情越少越好。在這一步,我們可以做的最佳化有:

  1. 梳理各個二方/三方庫,找到可以消極式載入的庫,做消極式載入處理,比如放到首頁控制器的viewDidAppear方法裡。

  2. 梳理商務邏輯,把可以順延強制的邏輯,做順延強制處理。比如檢查新版本、註冊推播通知等邏輯。

  3. 避免複雜/多餘的計算。

  4. 避免在首頁控制器的viewDidLoad和viewWillAppear做太多事情,這2個方法執行完,首頁控制器才能顯示,部分可以延遲建立的視圖應做延遲建立/懶載入處理。

  5. 採用效能更好的API。

  6. 首頁控制器用純程式碼方式來構建。

阿里資料iOS端最佳化實踐

在以上的認知指導下,阿里資料iOS端開始著手最佳化,在pre-main階段和main()階段分別做了一系列最佳化,取得了一定的成果。

1. pre-main階段的最佳化

1.1. 排查無用的dylib,移除不再使用的libicucore.tbd

1.2. 刪除無用檔案&庫,合并重複檔案(多個重複的分類)。移除不再使用的庫UMSocial、PSTCollectionView、MCSwipeTableViewCell,移除功能重複的庫Mantle。

1.3. 梳理各個類的+load方法,將多個類中+load方法做的事延遲到+initiailize裡去做。

最佳化前pre-main階段耗時:

最佳化前pre-main階段耗時.png

最佳化後pre-main階段耗時:

最佳化後pre-main階段耗時.png

測試環境:Xcode8.3.3 iOS10.2的模擬器,暖開機。

備忘:測試發現,pre-main階段耗時有一定波動,冷啟動時波動更大,這裡貼的是一個中位元水平。

可以看到暖開機下,pre-main階段耗時有一定下降。

2. main()階段的最佳化

2.1. 去掉其中100ms的dispatch_after...檢查代碼發現之前會故意讓啟動圖多顯示100ms,不知道是什麼邏輯...

2.2. 將多個二方/三方庫消極式載入。包括TBCrashReporter、TBAccsSDK、UT、TRemoteDebugger、ATSDK等。

2.3. 將若干系統UI配置、商務邏輯順延強制。包括註冊推送、檢查新版本、更新Orange配置等。

2.4. 避免多餘的計算。之前會前後兩次擷取是否要顯示廣告圖,每次擷取都需要還原序列化Orange中的配置資訊,再比較配置中的開始/結束時間,大約耗時20ms。目前的解決方案是第一次計算後,用一個BOOL屬性緩衝起來,下次直接取用。

2.5. 消極式載入&懶載入部分視圖。快捷密碼驗證頁是啟動圖消失後使用者看到的第一個頁面,這個頁面由於涉及到圖片的解碼、多個視圖的建立&布局,viewDidLoad階段會耗時100ms左右。目前的解決方案是把其中密碼輸入框視圖延遲到viewDidAppear裡載入,對密碼錯誤提示視圖做成懶載入,耗時降低到30m左右。

通過instruments的Time Profiler分析,最佳化後啟動速度有明顯提升,didFinishLaunchingWithOptions耗時在75ms左右(iPhone6s iOS10.3.3)

啟動耗時..png

其中目前耗時最多的是快捷密碼驗證頁(PAPasscodeViewController)的建立&布局,其次是DTLaunchViewControlle裡對是否要顯示廣告頁的判斷代碼。可以看到PAPasscodeViewController的viewDidAppear耗時了78ms,但已經沒有太大關係,此時使用者已經看到了頁面,準備去驗證指紋/密碼了。

總結&後續規劃

1. 總結

總結起來,好像啟動速度最佳化就一句話:讓系統在啟動期間少做一些事。當然我們得先清楚工程裡做的哪些事是在啟動期間做的、對啟動速度的影響有多大,然後case by case地分析工程代碼,通過放到子線程、消極式載入、懶載入等方式讓系統在啟動期間更輕鬆些。

2. 後續規劃

2.1. 替代部分龐大的庫,採用更輕量級的解決方案。

2.2. 整理代碼,去除重複的實現,避免出現功能重複的類&分類&方法。

2.3. 梳理和移除已經下線的業務涉及的類&分類&方法。

2.4. 監控好灰階版本啟動速度的變化趨勢,儘早發現&解決拖慢啟動速度的問題。

參考資料

  • WWDC Optimizing App Startup Time

  • attribute 總結

  • dyld 載入 Mach-O

  • 最佳化 App 的啟動時間

  • 今日頭條iOS用戶端啟動速度最佳化

iOS啟動速度最佳化

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.