iOS App 啓動性能優化

本文來自於騰訊Bugly公衆號(weixinBugly),未經做者贊成,請勿轉載,原文地址:https://mp.weixin.qq.com/s/Kf3EbDIUuf0aWVT-UCEmbA程序員

做者:samsonxu算法

導語

本文介紹瞭如何優化iOS App的啓動性能。設計模式

本文分爲四個部分:緩存

  • 第一部分科普了一些和App啓動性能相關的前置知識
  • 第二部分主要講如何定製啓動性能的優化目標
  • 第三部分經過在WiFi管家這個具體項目的優化過程,分享一些有用的經驗
  • 第四部分是關鍵點的總結。

【第一部分】一些小科普

由於篇幅的限制,沒有辦法很詳盡的說明一些原理性的東西,只是方便你們瞭解哪些事情可能跟啓動性能有關。同時,內容相對也比較入門,大神們請跳過這一部分。安全

1. App啓動過程

  • 解析Info.plist
    • 加載相關信息,例如如閃屏
    • 沙箱創建、權限檢查
  • Mach-O加載
  • 若是是胖二進制文件,尋找合適當前CPU類別的部分
  • 加載全部依賴的Mach-O文件(遞歸調用Mach-O加載的方法)
  • 定位內部、外部指針引用,例如字符串、函數等
  • 執行聲明爲__attribute__((constructor))的C函數
  • 加載類擴展(Category)中的方法
  • C++靜態對象加載、調用ObjC的 +load 函數
  • 程序執行
  • 調用main()
  • 調用UIApplicationMain()
  • 調用applicationWillFinishLaunching

2. 如何測量啓動過程耗時

冷啓動比熱啓動重要

當用戶按下home鍵的時候,iOS的App並不會立刻被kill掉,還會繼續存活若干時間。理想狀況下,用戶點擊App的圖標再次回來的時候,App幾乎不須要作什麼,就能夠還原到退出前的狀態,繼續爲用戶服務。這種持續存活的狀況下啓動App,咱們稱爲熱啓動,相對而言冷啓動就是App被kill掉之後一切從頭開始啓動的過程。咱們這裏只討論App冷啓動的狀況。性能優化

main()函數以前

在不越獄的狀況下,以往很難精確的測量在main()函數以前的啓動耗時,於是咱們也每每容易忽略掉這部分數據。小型App確實不須要太過關注這部分。但若是是大型App(自定義的動態庫超過50個、或編譯結果二進制文件超過30MB),這部分耗時將會變得突出。所幸,蘋果已經在Xcode中加入這部分的支持。微信

蘋果提供的方法
  • 在Xcode的菜單中選擇ProjectSchemeEdit Scheme...,而後找到 RunEnvironment Variables+,添加name爲DYLD_PRINT_STATISTICSvalue1的環境變量。
    網絡

  • 在Xcode運行App時,會在console中獲得一個報告。例如,我在WiFi管家中加入以上設置以後,會獲得這樣一個報告:
Total pre-main time:  94.33 milliseconds (100.0%)
           dylib loading time:  61.87 milliseconds (65.5%)
          rebase/binding time:   3.09 milliseconds (3.2%)
              ObjC setup time:  10.78 milliseconds (11.4%)
             initializer time:  18.50 milliseconds (19.6%)
             slowest intializers :
               libSystem.B.dylib :   3.59 milliseconds (3.8%)
     libBacktraceRecording.dylib :   3.65 milliseconds (3.8%)
                      GTFreeWifi :   7.09 milliseconds (7.5%)
如何解讀
  1. main()函數以前總共使用了94.33ms
  2. 在94.33ms中,加載動態庫用了61.87ms,指針重定位使用了3.09ms,ObjC類初始化使用了10.78ms,各類初始化使用了18.50ms。
  3. 在初始化耗費的18.50ms中,用時最多的三個初始化是libSystem.B.dylib、libBacktraceRecording.dylib以及GTFreeWifi。
main()函數以後

main()函數開始至applicationWillFinishLaunching結束,咱們統一稱爲main()函數以後的部分。架構

3. 影響啓動性能的因素

App啓動過程當中每個步驟都會影響啓動性能,可是有些部分所消耗的時間少之又少,另外有些部分根本沒法避免,考慮到投入產出比,咱們只列出咱們能夠優化的部分:併發

main()函數以前耗時的影響因素
  • 動態庫加載越多,啓動越慢。
  • ObjC類越多,啓動越慢
  • C的constructor函數越多,啓動越慢
  • C++靜態對象越多,啓動越慢
  • ObjC的+load越多,啓動越慢

實驗證實,在ObjC類的數目同樣多的狀況下,須要加載的動態庫越多,App啓動就越慢。一樣的,在動態庫同樣多的狀況下,ObjC的類越多,App的啓動也越慢。須要加載的動態庫從1個上升到10個的時候,用戶幾乎感知不到任何分別,但從10個上升到100個的時候就會變得十分明顯。同理,100個類和1000個類,可能也很難查察以爲出,但1000個類和10000個類的分別就開始明顯起來。

一樣的,儘可能不要寫__attribute__((constructor))的C函數,也儘可能不要用到C++的靜態對象;至於ObjC的+load方法,彷佛你們已經習慣不用它了。任何狀況下,能用dispatch_once()來完成的,就儘可能不要用到以上的方法。

main()函數以後耗時的影響因素
  • 執行main()函數的耗時
  • 執行applicationWillFinishLaunching的耗時
  • rootViewController及其childViewController的加載、view及其subviews的加載
applicationWillFinishLaunching的耗時

若是有這樣這樣的代碼:

//AppDelegate.m
@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.rootViewController = [[[MQQTabBarController alloc] init] autorelease];

    self.window = [[[UIWindow alloc] init] autorelease];
    [self.window makeKeyAndVisible];
    self.window.rootViewController = self.rootViewController;

    UITabBarController *tabBarViewController = [[[UITabBarController alloc] init] autorelease];


    NSLog(@"%s", __PRETTY_FUNCTION__);
    return YES;
}

...

//MQQTabBarController.m
@implementation MQQTabBarController

- (void)viewDidLoad {
    NSLog(@"%s", __PRETTY_FUNCTION__);
    [super viewDidLoad];
    // Do any additional setup after loading the view.

    UIViewController *tab1 = [[[MQQTab1ViewController alloc] init] autorelease];
    tab1.tabBarItem.title = @"red";
    [self addChildViewController:tab1];

    UIViewController *tab2 = [[[MQQTab2ViewController alloc] init] autorelease];
    tab2.tabBarItem.title = @"blue";
    [self addChildViewController:tab2];

    UIViewController *tab3 = [[[MQQTab3ViewController alloc] init] autorelease];
    tab3.tabBarItem.title = @"green";
    [self addChildViewController:tab3];
}

...
@end

那麼-[MQQTabBarController viewDidLoad]-[AppDelegate application:didFinishLaunchingWithOptions:]-[MQQTab1ViewController viewDidLoad]-[MQQTab2ViewController viewDidLoad]-[MQQTab2ViewController viewDidLoad] 完成的前後順序是怎樣的呢?

答案是:

  1. -[MQQTabBarController viewDidLoad]
  2. -[MQQTab1ViewController viewDidLoad]
  3. -[AppDelegate application:didFinishLaunchingWithOptions:]
  4. -[MQQTab2ViewController viewDidLoad] (點擊了第二個tab以後加載)
  5. -[MQQTab3ViewController viewDidLoad] (點擊了第三個tab以後加載)

通常而言,大部分狀況下咱們都會把界面的初始化過程放在viewDidLoad,可是這個過程會影響消耗啓動的時間。特別是在相似TabBarController這種會嵌套childViewController的ViewController的狀況,它也會把部分children也初始化,所以各類viewDidLoad會遞歸的進行。

最簡單的解決的方法,是把viewController延後加載,但實際上這屬於一種掩耳盜鈴,確實,applicationWillFinishLaunching的耗時是降下來了,但用戶體驗上並無感受變快。

更好一點的解決方法有點相似facebook,主視圖會第一時間加載,但裏面的數據和界面都會延後加載,這樣用戶就會階段性的得到視覺上的變化,從而在視覺體驗上感受App啓動得很快。

【第二部分】優化的目標

因爲每一個App的狀況有所不一樣,須要加載的數據量也有所不一樣,事實上咱們沒法使用一種統一的標準來衡量不一樣的App。蘋果。

  • 應該在400ms內完成main()函數以前的加載
  • 總體過程耗時不能超過20秒,不然系統會kill掉進程,App啓動失敗

400ms內完成main()函數前的加載的建議值是怎樣定出來的呢?其實我也沒有太深究過這個問題,可是,當用戶點擊了一個App的圖標時,iOS作動畫到閃屏圖出現的時長正好是這個數字,我想也許跟這個有關。

針對不一樣規模的App,咱們的目標應該有所取捨。例如,對於像手機QQ這種集整個SNG的代碼大成擼出來的App,對動態庫的使用在所不免,但對於WiFi管家,因爲在用戶鏈接WiFi的時候須要很是快速的響應,因此快速啓動就很是重要。

那麼,如何定製優化的目標呢?首先,要肯定啓動性能的界限,例如,在各類App性能的指標中,哪一此屬於啓動性能的範疇,哪一些則於App的流暢度性能?我認爲應該首先把啓動過程分爲四個部分:

  1. main()函數以前
  2. main()函數以後至applicationWillFinishLaunching完成
  3. App完成全部本地數據的加載並將相應的信息展現給用戶
  4. App完成全部聯網數據的加載並將相應的信息展現給用戶

1+2一塊兒決定了咱們須要用戶等待多久才能出現一個主視圖,同時也是技術上能夠精確測量的時長,1+2+3決定了用戶視覺上的等待出現有用信息所須要的時長,1+2+3+4決定了咱們須要多少時間才能讓咱們須要展現給用戶的全部信息所有出現。

淘寶的iOS客戶端無疑是各部分都作得很是優秀的典型。它所承載的業務徹底不比微信和手機QQ少,但幾乎瞬間完成了啓動,並利用緩存機制使得用戶立刻看到「貌似完整」的界面,而後當即又刷新了剛剛聯網更新回來的信息。也就是說,不管是技術上仍是視覺上,它都很是的「快」。

【第三部分】WiFi管家啓動優化實踐

先show一下成果:

1. 移除不須要用到的動態庫

由於WiFi管家是個小項目,用到的動態庫很少,自動化處理的優點不大,我這裏也就簡單的把依賴的動態移除出項目,再根據編譯錯誤一個一個加回來。若是有靠譜的方法,歡迎你們補充一下。

2. 移除不須要用到的類

項目作久了總有一些弔詭的類像幽靈同樣驅之不去,因爲【不要相信產品經理】的思想做怪,需求變動後,有些類可能用不上了,但卻由於擔憂需求再變回來就沒有移除掉,後來就完全忘記要移除了。

爲了解決這個歷史問題,在這個過程當中我試過多種方法來掃描沒有用到的類,其中有一種是編譯後對ObjC類的指針引用進行反向掃描,惋惜實際上收穫不是很明顯,並且還要寫不少例外代碼來處理一些特殊狀況。後來發現一個叫作fui(Find Unused Imports)的開源項目能很好的分析出再也不使用的類,準確率很是高,惟一的問題是它處理不了動態庫和靜態庫裏提供的類,也處理不了C++的類模板。

使用方法是在Terminal中cd到項目所在的目錄,而後執行fui find,而後等上那麼幾分鐘(是的你沒有看錯,真的須要好幾分鐘甚至須要更長的時間),就能夠獲得一個列表了。因爲這個工具還不是100%靠譜,可根據這個列表,在Xcode中手動檢查並刪除再也不用到的類。

實際上,平常對代碼工程的維護很是重要,若是制定好一套半廢棄代碼的維護方法,小問題就不會積累成大問題。有時候對於一些暫時再也不使用的代碼,我也很糾結於要不要svn rm,由於從代碼歷史中找刪除掉的文件仍是不太方便。不知道你們有沒有相關的經驗能夠分享,也請不吝賜教。

3. 合併功能相似的類和擴展(Category)

因爲Category的實現原理,和ObjC的動態綁定有很強的關係,因此實際上類的擴展是比較佔用啓動時間的。儘可能合併一些擴展,會對啓動有必定的優化做用。不過我的認爲也不能由於它佔用啓動時間而去逃避使用擴展,畢竟程序員的時間比CPU的時間值錢,這裏只是強調要合併一些在工程、架構上沒有太大意義的擴展。

4. 壓縮資源圖片

壓縮圖片爲何能加快啓動速度呢?由於啓動的時候大大小小的圖片加載個十來二十個是很正常的,圖片小了,IO操做量就小了,啓動固然就會快了。

事實上,Xcode在編譯App的時候,已經自動把須要打包到App裏的資源圖片壓縮過一遍了。然而Xcode的壓縮會相對比較保守。另外一方面,咱們正常的設計師因爲須要符合其正常的審美須要生成的正常的PNG圖片,所以圖片大小是比較大的,然而若是以程序員的直男審美而採用過激的壓縮會直接激怒設計師。

解決各類矛盾的方法就是要找出一種至關靠譜的壓縮方法,並且最好是基本無損的,並且壓縮率還要特別高,至少要比Xcode自動壓縮的效果要更好纔有意義。通過各類試驗,最後發現惟一可靠的壓縮算法是TinyPNG,其它各類方法,要麼沒效果,要麼產生色差或模糊。可是很是惋惜的是TinyPNG並非徹底免費的,並且須要經過網絡請求來壓縮圖片(應該是爲了保護其牛逼的壓縮算法)。

爲了解決這個問題,我寫了一個類來執行這個請求,請參見閱讀原文裏的SSTinyPNGRequest和SSPNGCompressor。由於這個項目只有我一我的在用因此代碼寫得有點隨意,有問題能夠私聊也能夠在評論裏問,有改進的方法也很是歡迎指正。另外說明一下,使用這個類須要你自行到 這裏 申請APIKey,每個用戶每個月有500張圖片壓縮是免費的,而每一個郵箱能夠註冊一個用戶,你懂的。

5. 優化applicationWillFinishLaunching

隨着項目作的時間長了,applicationWillFinishLaunching裏要處理的代碼會越積越多,WiFi管家的iOS版本有一段時間沒有控制好,裏面的邏輯亂得有點丟人。由於可能涉及到一些項目的安全性問題,這裏不能分享全部的優化細節及發現的思路。僅列出在applicationWillFinishLaunching中主要須要處理的業務及相關問題的改進方案。

這裏大部分都是一些苦逼活,但有一點特別值得分享的是,有一些優化,是沒法在數據上體現的,可是視覺上卻能給用戶較大的提高。例如在【各類業務請求配置更新】的部分,通過分析優化後,啓動過程併發的http請求數量從66條壓縮到了23條,如此一來爲啓動成功後新聞資訊及其圖片的加載留出了更多的帶寬,從而保證了在第一時間完成新聞資訊的加載。實際測試代表,光作KPI的事情是不夠的,人仍是須要有點理想,通過優化,在視覺體驗上進步很是明顯。

另外,過程當中請教過SNG的大牛們,據說他們由於須要在applicationWillFinishLaunching裏處理的業務更多,因此還作了管理器管理這些任務,不過由於WiFi管家是個小項目,有點殺雞用牛刀的感受,所以沒有深刻研究。

6. 優化rootViewController加載

考慮到我做爲一隻高級程序猴,工資很高,爲了給公司節約成本,在優化以前,固然須要先測試一下哪些ViewController的加載耗時比較大,而後再深刻到具體業務中看哪些部分存在較大的優化空間。同時,先作優化效果明顯的部分也有利於加強本身的信心。

在開始講述問題以前,咱們先來看一下WiFi管家的UI層次結構:

一個看似簡單的界面因爲承載了不少業務需求,代碼量其實已經很是驚人。這裏我不具體講述這些驚人的業務量了,抽象而言可WiFi管家的UI架構整體而言基於TabBarController的框架,三個tab分別是「鏈接」、「發現」及「個人」。App啓動的時候,根據加載原理,會加載TabBarController、第一個Tab(「鏈接」)的ViewController及其全部childViewController。

UI構架請看以下示意圖,其中藍色的部分須要在App啓動的時候當即加載:

對全部啓動相關的模塊打錨點計算耗時後,發現tabBarController和connectingViewController分別佔用了applicationWillFinishLaunching耗時的31%和24%。加載耗費了大量時間,這跟它所須要承載的邏輯任務彷佛並不對稱。因而檢查相關代碼進行深刻分析,發現了幾個問題比較嚴重:

  1. 有些程序員可能架構意識不是太強,直接在tabBarController的啓動過程當中插入了各類奇怪的業務,例如檢查WiFi鏈接狀態變化、配置拉取,而這些業務顯然應該在另外的某些地方統一處理,而不該該在一個ViewController上。

  2. 因爲一些歷史緣由,鏈接頁的視圖控制器connectingViewController包含了三個childViewController:WiFiViewController、3GViewController、errorViewController,分別在WiFi狀態、3G狀態和出錯狀態下展現界面(三選一,其中一個展現的時候其它兩個視圖會隱藏)。

  3. 大部分view都是直接加載完的。有些界面的加載很是複雜,好比再進入App時會展現一個檢查WiFi可用性和安全性的動畫,因爲須要疊加較多圖片,這部分視圖的加載耗時較多。

因爲隨着幾回改版以後,鏈接頁的UI架構已經變得很不合理,歷史包袱仍是比較重的,並且耦合比較嚴重,幾乎沒法改動,所以決定重構。至於tabBarController,檢查代碼後決定簡單的把不相關的業務作一些遷移,優化childViewController的加載過程,不做重構。

改進後的結構大體以下圖,其中藍色部分須要在App啓動的時候當即加載:

因爲本篇主要講啓動性能優化,重構涉及的軟件工程和設計模式方面的東西就不詳細論述了,對啓動優化的過程,主要是使用了更合理的分層結構,使得啓動得以在更短的時間內完成。

至此,WiFi管家的啓動性能基本優化完畢。

7. 挖掘最後一點性能優化

因爲WiFi管家是一個具備WiFi鏈接能力的App,所以有可能在後臺過程當中完成冷啓動過程(其實是在用戶進入系統的WiFi設置時,iOS會啓動WiFi管家,以便請求WiFi密碼)。在這種狀況下,整個rootViewController都是不須要加載的。

【第四部分】總結

  • 利用DYLD_PRINT_STATISTICS分析main()函數以前的耗時
  • 從新梳理架構,減小動態庫、ObjC類的數目,減小Category的數目
  • 按期掃描再也不使用的動態庫、類、函數,例如每兩個迭代一次
  • 用dispatch_once()代替全部的 attribute((constructor)) 函數、C++靜態對象初始化、ObjC的+load
  • 在設計師可接受的範圍內壓縮圖片的大小,會有意外收穫
  • 利用錨點分析applicationWillFinishLaunching的耗時
  • 將不須要立刻在applicationWillFinishLaunching執行的代碼延後執行
  • rootViewController的加載,適當將某一級的childViewController或subviews延後加載
  • 若是你的App可能會被後臺拉起並冷啓動,可考慮不加載rootViewController
  • 不該放過的一些小細節
  • 異步操做並不影響指標,但有可能影響交互體驗,例如大量網絡請求致使數據擁堵
  • 有時候一些交互上的優化比技術手段效果更明顯,視覺上的快決不是冰冷的數據能夠解釋的,好好和大家的設計師談談動畫

更多精彩內容歡迎關注騰訊 Bugly的微信公衆帳號:

騰訊 Bugly是一款專爲移動開發者打造的質量監控工具,幫助開發者快速,便捷的定位線上應用崩潰的狀況以及解決方案。智能合併功能幫助開發同窗把天天上報的數千條 Crash 根據根因合併分類,每日日報會列出影響用戶數最多的崩潰,精準定位功能幫助開發同窗定位到出問題的代碼行,實時上報能夠在發佈後快速的瞭解應用的質量狀況,適配最新的 iOS, Android 官方操做系統,鵝廠的工程師都在使用,快來加入咱們吧!

相關文章
相關標籤/搜索