iOS App啟動優(yōu)化

iOS的App啟動一般有兩個過程,發(fā)生在main函數(shù)前的過程和main函數(shù)之后的過程。

main函數(shù)前

App開始啟動后,系統(tǒng)首先加載可執(zhí)行文件(即App所有.o文件),然后加載動態(tài)鏈接庫dyld,dyld是一個專門負(fù)責(zé)加載動態(tài)鏈接庫的庫(the dynamic link editor工具),dyld會加載所有可執(zhí)行文件(.o文件)的依賴動態(tài)庫,包括App使用到的所有系統(tǒng) framework,還有一些runtime和系統(tǒng)級別的lib,如libobjc、libSystem等

簡單來說,上述所說的可執(zhí)行文件(.o文件)和動態(tài)庫都可以稱為image。

所有動態(tài)鏈接庫、App集成的靜態(tài)庫、所有類文件編譯后的.o文件最終都是由dyld這個工具加載到內(nèi)存中。每個image都是由一個叫做ImageLoader的類來負(fù)責(zé)加載。

ImageLoader

這里的image并不是代表圖片的意思,表示一個二進(jìn)制文件(可執(zhí)行文件或 so 文件),里面是被編譯過的符號、代碼等,所以 ImageLoader 作用是將這些文件加載進(jìn)內(nèi)存,且每一個文件對應(yīng)一個ImageLoader實(shí)例來負(fù)責(zé)加載。

大概流程

1、dyld 開始將程序二進(jìn)制文件初始化
2、交由 ImageLoader 讀取 image,其中包含了我們的類、方法等各種符號
3、由于 runtime 向 dyld 綁定了回調(diào),當(dāng) image 加載到內(nèi)存后,dyld 會通知 runtime 進(jìn)行處理
4、runtime 接手后調(diào)用 mapimages 做解析和處理,接下來 loadimages 中調(diào)用 callloadmethods 方法,遍歷所有加載進(jìn)來的 Class,按繼承層級依次調(diào)用 Class 的 +load 方法和其 Category 的+load 方法。

至此,可執(zhí)行文件中和動態(tài)庫所有的符號(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被 runtime 所管理,再這之后,runtime 的那些方法(動態(tài)添加 Class、swizzle 等等才能生效)。

簡單來說就是
dyld加載各種庫---->ImageLoader讀取 image ---- >runtime初始化環(huán)境

優(yōu)化

對于main()調(diào)用之前的耗時我們可以優(yōu)化的點(diǎn)有:
1、減少動態(tài)庫,官方建議6個,不要鏈接那些用不到的庫(包括系統(tǒng))。
2、檢查+load()方法是否合理
3、合并或者刪減一些OC類,關(guān)于清理項(xiàng)目中沒用到的類。
4、二進(jìn)制重排,減少page fault的次數(shù)

啟動時間計(jì)算

pre-main的大概過程可以簡述為

加載可執(zhí)行文件、加載動態(tài)鏈接器dyld、按照依賴加載動態(tài)庫

為了獲得啟動時間,可以在第一個動態(tài)庫開始記錄啟動時間,但是在加載動態(tài)庫之前還有一段時間我們記錄不到。
所以這時候可以換一個方法。
使用OC的NSProcessInfo類,獲取App創(chuàng)建進(jìn)程的時間戳。

#import <Foundation/NSProcessInfo.h>
#include <sys/types.h>
#include <sys/param.h>
#include <sys/sysctl.h>

//// 根據(jù)進(jìn)程ID,獲取進(jìn)程信息!
+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
{
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(*procInfo);
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}
//  根據(jù)進(jìn)程信息,獲取具體的進(jìn)程加載到內(nèi)存中的時間戳
+ (NSTimeInterval)processStartTime {
    struct kinfo_proc kProcInfo;
    if ([[self class] processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
        return (kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0);
    } else {
        NSAssert(NO, @"無法取得進(jìn)程的信息");
        return 0;
    }
}

啟動之后,進(jìn)入main函數(shù)之后到applicationDidBecomeActive,才算啟動結(jié)束。
所以統(tǒng)計(jì)創(chuàng)建進(jìn)程的時間戳到BecomeActive時間戳?xí)r間的時間差即可。

啟動函數(shù)耗時

Instrument中的 Time Profiler工具,可以查看App在某一段時間的方法耗時,大概原理如下。

1ms 采樣一次,只采集在運(yùn)行線程的調(diào)用棧,最后以統(tǒng)計(jì)學(xué)的方式匯總。注意,每1ms采樣,Profiler看堆棧的方法,如果有,那么這個方法就疊加 1ms,事實(shí)上并不是代碼實(shí)際執(zhí)行的時間,不是很準(zhǔn)確的,有可能會遺漏,而是棧在采樣統(tǒng)計(jì)中出現(xiàn)的時間

除了使用 Time Profiler工具,我們還可以使用fishhook工具,hook objc_msgSend方法,用來統(tǒng)計(jì)每個方法(包含第三方SDK靜態(tài)庫內(nèi)部的方法)的啟動耗時。本質(zhì)上是在方法的開始和末尾打兩個點(diǎn),就知道這個方法的耗時,然后轉(zhuǎn)換成 Chrome 的標(biāo)準(zhǔn)的火焰圖 json 格式,將該json文件傳入分析工具chrome://tracing/生成火焰圖,通過以下火焰圖,我們可以非常方便的看到啟動時執(zhí)行了哪些方法和耗時的多少。

軟件的性能分析,往往需要查看 CPU 耗時,了解瓶頸在哪里。
火焰圖(flame graph)是性能分析的利器。


image.png

main函數(shù)后

在main()被調(diào)用之后,
執(zhí)行
- (BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
然后在這個方法必要的服務(wù),配置第三方庫、顯示首頁內(nèi)容等。
可以優(yōu)化的點(diǎn):
1、盡量使用代碼布局UI,避免使用XIB、storyboard。
2、一些在didFinishLaunchingWithOptions的配置或者創(chuàng)建可以使用懶加載,或者延后創(chuàng)建。

參考文章

https://techblog.toutiao.com/2017/01/17/iosspeed/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容