重學iOS系列之APP啟動(一)流程

導讀

本文將帶您了解iOS APP從點擊圖標到顯示畫面的大致過程,本文只不深入解析相關源碼,相關源碼解析會在后續的章節詳細講解。

我們為什么要了解APP啟動流程?

在開發過程中,隨著業務的不斷增加,代碼量也不斷增加,APP體量越來越大,APP的啟動速度越來越慢,啟動速度慢導致用戶體驗差,那么新需求就來了,降低APP的啟動時間!

如何降低APP的啟動時間呢?

這個問題可以拆分成

1、APP啟動時間降低到多少才能讓用戶有飛一般的體驗

2、如何降低APP啟動時間?

在回答問題之前,我們先了解下怎么測量啟動時間:

在Xcode菜單欄:Product->Scheme->Edit Scheme->Environment Variables

設置:key:DYLD_PRINT_STATISTICS? ? value:1

然后運行工程,就會將啟動時間以及過程耗時打印出來了。

當然了,打印的時間都是分模塊的,如果要具體要每個函數的調用時間,就需要借助其他的工具,比如自己寫一套hook objc_msgSend(),這個難度非常大,簡單點的方式就是借助Xcode自帶的Instrument工具,具體怎么操作就不展開講解,有興趣的同學可以自己找找資料。

回到之前的問題上來,先回答第一個問題:

在WWDC2016上蘋果官方給出的APP啟動時間建議是低于400ms,這樣可以確保在 Springboard 的應用啟動動畫結束前,你的應用就做好準備可以使用了。而且如果啟動時間超過20秒則會被系統殺死,意味著你的APP將啟動失敗。

Apple suggest to aim for a total app launch time of under 400ms and you must do it in less than 20 seconds or the system will kill your app

問題1已經解決,那么我們回到問題2。

App啟動一般分兩種,冷啟動和熱啟動。

熱啟動是指 ,App 在冷啟動后用戶將 App 退后臺,在 App 的進程還在系統里的情況 下,用戶重新啟動進入 App 的過程,這個過程做的事情非常少。(即有數據緩存為熱啟動)

冷啟動是指, App 點擊啟動前,它的進程不在系統里,需要系統新創建一個進程分配給 它啟動的情況。這是一次完整的啟動過程。(重啟設備后,任意APP的啟動都為冷啟動)

一般情況下,熱啟動由于有緩存的原因,速度都非常快,肯定是低于400ms的,這種情況我們不需要優化。

OK,重點來了,冷啟動!

現在問題2變成了:如何降低冷啟動的時間?

不知道!game over結束!


身為一個求生欲非常強的猿,我們肯定不能就這樣回答結束。

想解決問題,就要先了解問題的本質。進入正題,啟動流程!

我們先用Xcode新建一個Single View App工程,取名Launch_Demo,在工程中能接觸到最開始執行的代碼在main.m文件中的main()函數,我們先在main函數中打個斷點,然后啟動模擬器:



修改debug選項,用于顯示匯編指令代碼,然后點擊左側的start


可以看到圖中的匯編代碼是斷在一個libdyld.dylib動態庫的一個start()函數中,那libdyld.dylib這又是什么呢?

在這之前我們先了解下什么是dyld,dyld(dynamic link editor)是蘋果系統內核中的動態連接器,是蘋果系統的重要組成部分,負責加載可執行文件到內存中。而且它是開源的,任何人可以通過蘋果官網下載它的源碼來閱讀理解它的運作方式,了解系統加載動態庫的細節,dyld下載地址:http://opensource.apple.com/tarballs/dyld

我們先把源碼下載下來,目前最新版本是dyld-852.2

打開dyld源碼工程,全局搜索_dyld_start,然后在dyldStartup.s這個匯編文件中找到了具體的實現,代碼包含了各種架構的實現,我們只需要關注iOS的實現,摘取了arm64架構的實現如下:


可以看到有一行代碼

// call dyldbootstrap::start(app_mh, argc, argv, dyld_mh, &startGlue)

bl __ZN13dyldbootstrap5startEPKN5dyld311MachOLoadedEiPPKcS3_Pm

從注釋我們可以知道這句代碼就是調用了start()函數,咦,這不就是上面我們斷點里的那個start()函數嗎?

再全局搜索start(,結果非常多,我們找跟bootstrap有關系的,然后我們在dyldInitialization.cpp文件中找到函數的具體實現:


start()函數做了一堆初始化的事情,具體的先不管,看重點,return dyld::_main()

初始化工作完成后,此函數調用到了dyld::_main(),再將返回值傳遞給__dyld_start去調用真正的main()函數。

光標放到_main()的位置,右鍵選中Jump to Definition直接跳轉到main()函數的具體實現。然后就懵逼了,900行代碼的具體實現(心里一陣草泥馬狂奔而過)。


代碼太長了,我們找重點,一番查找,找到如下一段代碼:


看注釋:

// run all initializers (執行所有的初始化器)

initializeMainExecutable();?

// notify any montoring proccesses that this process is about to enter main() (通知任何正在監聽的進程,當前進程要進入main()函數了)

notifyMonitoringDyldMain();

很好,已經快要接近我們的目標了,繼續往下讀源碼找重點:


這是什么?

看注釋:find entry point for main executable (找到主程序的入口,就是我們的工程里的main()函數)。

目前都還是靜態代碼的分析,我們用一個demo來驗證一下,我們都知道load()方法會在main之前調用,所以我們通過在load方法里打一個斷點的方式追蹤一下程序的調用堆棧。

打開之前創建的demo工程,在ViewController中新增一個+(void)Load方法,然后將斷點打到方法里面,運行程序。


從圖中可以看到,函數的調用順序和我們分析的差不多,我們只分析到dyld::_main()函數,上圖的很多函數調用都是_main()內部調用的。細心的同學會發現dyld最終調用了notifySingle,然后就進入libobjc.A.dylib的load_image函數了,我們熟悉的runtime就是在這個動態庫里的,那么這部分是怎么跳轉的呢?


看到這里,很多讀者肯定會覺得很困惑,這個流程感覺什么都沒有啊,沒有干貨啊,被大家熟知的runtime在哪啟動的,類在哪加載的,category什么時候加載,load方法什么時候調用等等。

不要急,APP的啟動是個非常復雜的過程,涉及到的源碼非常多,不僅僅是dyld庫被調用,還有上圖的libobjc.A.dylib,以及還沒有出現的libSystem.dylib libdispatch.dylib等。

我們目前了解到的還只是冰山一角,真正的想要完全吃透這一塊是要有很多的知識積累,比如用戶態和內核態,mach-o的一些知識,靜態庫與動態庫相關知識,虛擬內存,共享緩存等等等等。

當然我們目前分析的都是基于用戶態的啟動過程,在內核態還有另外一套啟動過程來啟動dyld,由于iOS開發不涉及內核開發,所以不在此深入探究,有興趣的同學可以自己找資料學習相關知識。

總結下今天的收獲,目前我們了解到的調用流程是這樣的:

_dyld_start -->?dyldbootstrap::start()--> dyld::_main() -->_main()函數里一堆邏輯代碼 -->主程序main()

中間省略的調用邏輯,最少包含了:

1、Xcode環境配置相關信息的檢查

2、靜態庫、動態庫的加載以及鏈接

3、runtime的初始化

4、類的初始化

5、catagory的初始化

。。。。。。。。。等等

這些流程將會分為幾個小節進行詳細的源碼分析帶領大家了解。


下一篇:重學iOS系列之APP啟動-dyld(二)將帶大家拉開dyld的神秘面紗,領略下蘋果官方的開發是怎么編寫一個app的啟動邏輯的。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容