1 前言
啟動(dòng)是App給用戶的第一印象,一款A(yù)pp的啟動(dòng)速度,不單單是用戶體驗(yàn)的事情,往往還決定了它能否獲取更多的用戶。所以到了一定階段App的啟動(dòng)優(yōu)化是必須要做的事情。App啟動(dòng)基本分為以下兩種
1.1 冷啟動(dòng)
App 點(diǎn)擊啟動(dòng)前,它的進(jìn)程不在系統(tǒng)里,需要系統(tǒng)新創(chuàng)建一個(gè)進(jìn)程分配給它啟動(dòng)的情況。這是一次完整的啟動(dòng)過(guò)程。
表現(xiàn):App第一次啟動(dòng),重啟,更新等
1.2 熱啟動(dòng)
App 在冷啟動(dòng)后用戶將 App 退后臺(tái),在 App 的進(jìn)程還在系統(tǒng)里的情況下,用戶重新啟動(dòng)進(jìn)入 App 的過(guò)程,這個(gè)過(guò)程做的事情非常少。
所以我們主要說(shuō)道說(shuō)道冷啟動(dòng)的優(yōu)化
2 啟動(dòng)流程
2.1 APP啟動(dòng)都干了什么
要對(duì)啟動(dòng)速度進(jìn)行優(yōu)化,我們需要知道啟動(dòng)過(guò)程中的大致流程是什么,做了什么事情,是否能針對(duì)性優(yōu)化。
下圖是啟動(dòng)流程的詳細(xì)分解
- 點(diǎn)擊圖標(biāo),創(chuàng)建進(jìn)程
- mmap 主二進(jìn)制,找到 dyld 的路徑
- mmap dyld,把入口地址設(shè)為_(kāi)dyld_start
dyld 是啟動(dòng)的輔助程序,是 in-process 的,即啟動(dòng)的時(shí)候會(huì)把 dyld 加載到進(jìn)程的地址空間里,然后把后續(xù)的啟動(dòng)過(guò)程交給 dyld。dyld 主要有兩個(gè)版本:dyld2 和 dyld3。
iOS 12之前主要是dyld2,iOS 13 開(kāi)始 Apple 對(duì)三方 App 啟用了 dyld3,dyld3 的最重要的特性就是啟動(dòng)閉包,閉包存儲(chǔ)在沙盒的 tmp/com.apple.dyld 目錄,清理緩存的時(shí)候切記不要清理這個(gè)目錄。
閉包里主要有以下內(nèi)容:
- dependends,依賴動(dòng)態(tài)庫(kù)列表
- fixup:bind & rebase 的地址
- initializer-order:初始化調(diào)用順序
- optimizeObjc: Objective C 的元數(shù)據(jù)
- 其他:main entry, uuid等等
上圖虛線之上的部分是out-of-process的,在App下載安裝和版本更新的時(shí)候會(huì)去執(zhí)行,直接從緩存中讀取數(shù)據(jù),加快加載速度
這些信息是每次啟動(dòng)都需要的,把信息存儲(chǔ)到一個(gè)緩存文件就能避免每次都解析,尤其是 Objective-C 的運(yùn)行時(shí)數(shù)據(jù)(Class/Method…)解析耗時(shí), 所以對(duì)啟動(dòng)速度是一個(gè)優(yōu)化提升
4.把沒(méi)有加載的動(dòng)態(tài)庫(kù) mmap 進(jìn)來(lái),動(dòng)態(tài)庫(kù)的數(shù)量會(huì)影響這個(gè)階段
dyld從主執(zhí)行文件的header獲取到需要加載的所依賴動(dòng)態(tài)庫(kù)列表,然后它需要找到每個(gè) dylib,而應(yīng)用所依賴的 dylib 文件可能會(huì)再依賴其他 dylib,所以所需要加載的是動(dòng)態(tài)庫(kù)列表一個(gè)遞歸依賴的集合
5.對(duì)動(dòng)態(tài)庫(kù)集合循環(huán)load, mmap 加載到虛擬內(nèi)存里,對(duì)每個(gè) Mach-O 做 fixup,包括 Rebase 和 Bind。
對(duì)每個(gè)二進(jìn)制做 bind 和 rebase,主要耗時(shí)在 Page In,影響 Page In 數(shù)量的是 objc 的元數(shù)據(jù)
- Rebase 在Image內(nèi)部調(diào)整指針的指向。在過(guò)去,會(huì)把動(dòng)態(tài)庫(kù)加載到指定地址,所有指針和數(shù)據(jù)對(duì)于代碼都是對(duì)的,而現(xiàn)在地址空間布局是隨機(jī)化(ASLR),所以需要在原來(lái)的地址根據(jù)隨機(jī)的偏移量做一下修正, 也就是說(shuō)Mach-O 在 mmap 到虛擬內(nèi)存的時(shí)候,起始地址會(huì)有一個(gè)隨機(jī)的偏移量 slide,需要把內(nèi)部的指針指向加上這個(gè) slide.
- Bind 是把指針正確地指向Image外部的內(nèi)容。這些指向外部的指針被符號(hào)(symbol)名稱(chēng)綁定,dyld需要去符號(hào)表里查找,找到symbol對(duì)應(yīng)的實(shí)現(xiàn), 像 printf 等外部函數(shù),只有運(yùn)行時(shí)才知道它的地址是什么,bind 就是把指針指向這個(gè)地址,這也是后面我們能用fishhook來(lái)hook一些動(dòng)態(tài)符號(hào)的核心
如下圖,編譯的時(shí)候,字符串 1234 在__cstring的 0x10 處,所以 DATA 段的指針指向 0x10。但是 mmap 之后有一個(gè)偏移量 slide=0x1000,這時(shí)候字符串在運(yùn)行時(shí)的地址就是 0x1010,那么 DATA 段的指針指向就不對(duì)了。Rebase 的過(guò)程就是把指針從 0x10,加上 slide 變成 0x1010。運(yùn)行時(shí)類(lèi)對(duì)象的地址已經(jīng)知道了,bind 就是把 isa 指向?qū)嶋H的內(nèi)存地址。
6.初始化 objc 的 runtime,由于閉包已經(jīng)初始化了大部分,這里只會(huì)注冊(cè) sel 和裝載 category
7.+load 和靜態(tài)初始化被調(diào)用,除了方法本身耗時(shí),這里可能還會(huì)引起大量 Page In,如果調(diào)用了dispatch_async則會(huì)延遲啟動(dòng)后的runloop開(kāi)啟后執(zhí)行
如果觸發(fā)靜態(tài)初始化,則會(huì)延遲到運(yùn)行時(shí)執(zhí)行
8.初始化 UIApplication,啟動(dòng) Main Runloop
可以在之前章節(jié)利用runloop統(tǒng)計(jì)首屏耗時(shí)
也可以在啟動(dòng)結(jié)束做一些預(yù)熱任務(wù)
9.執(zhí)行 will/didFinishLaunch,這里主要是業(yè)務(wù)代碼耗時(shí)
首頁(yè)的業(yè)務(wù)代碼都是要在這個(gè)階段,也就是首屏渲染前執(zhí)行的,主要包括了:首屏初始化所需配置文件的讀寫(xiě)操作;首屏列表大數(shù)據(jù)的讀??;首屏渲染的大量計(jì)算等;sdk的初始化;對(duì)于大型組件化工程,也包含了很多moudle的啟動(dòng)加載項(xiàng)
10.Layout,viewDidLoad 和Layoutsubviews 會(huì)在這里調(diào)用,Autolayout 太多會(huì)影響這部分時(shí)間
11.Display,drawRect 會(huì)調(diào)用
12.Prepare,圖片解碼發(fā)生在這一步
13.Commit,首幀渲染數(shù)據(jù)打包發(fā)給 RenderServer,走GPU渲染流水線流程,啟動(dòng)結(jié)束
(tips: 2.2.10-2.2.13這里主要是圖形渲染流水線的部分流程,Application產(chǎn)生圖元階段(CPU階段))。后續(xù)會(huì)交由單獨(dú)的RenderServer進(jìn)程,再調(diào)用渲染框架(Metal/OpenGL ES)來(lái)生成 bitmap,放到幀緩沖區(qū)里,硬件根據(jù)時(shí)鐘信號(hào)讀取幀緩沖區(qū)內(nèi)容,完成屏幕刷新
2.2 啟動(dòng)各階段時(shí)長(zhǎng)統(tǒng)計(jì)
上一小節(jié)對(duì)啟動(dòng)各個(gè)階段過(guò)程的詳細(xì)闡述,歸納起來(lái)大致分為6個(gè)階段(WWDC2019):
通過(guò)對(duì)各個(gè)階段進(jìn)行時(shí)長(zhǎng)統(tǒng)計(jì)分析,進(jìn)行優(yōu)化然后對(duì)比。
可以在Xcode中設(shè)置環(huán)境變量DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS看下啟動(dòng)階段和對(duì)應(yīng)的耗時(shí)(iOS15后環(huán)境變量失效)
也可以通過(guò)Xcode MetricKit 本身也可以看到啟動(dòng)耗時(shí):打開(kāi) Xcode -> Window -> Origanizer -> Launch Time
如果公司有對(duì)應(yīng)的成熟監(jiān)控體系最好,這里我們主要通過(guò)手動(dòng)無(wú)侵入埋點(diǎn)去統(tǒng)計(jì)啟動(dòng)時(shí)長(zhǎng),對(duì)啟動(dòng)流程pre main-> after main進(jìn)行統(tǒng)計(jì)分析
2.1.1 進(jìn)程創(chuàng)建時(shí)間打點(diǎn)
通過(guò) sysctl 系統(tǒng)調(diào)用拿到進(jìn)程創(chuàng)建的時(shí)間戳
#import <sys/sysctl.h>
#import <mach/mach.h>
+ (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;
}
+ (NSTimeInterval)processStartTime
{
struct kinfo_proc kProcInfo;
if ([self 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, @"無(wú)法取得進(jìn)程的信息");
return 0;
}
2.1.2 main()執(zhí)行時(shí)間打點(diǎn)
// main之前調(diào)用
// pre-main()階段結(jié)束時(shí)間點(diǎn):__t2
void static __attribute__ ((constructor)) before_main()
{
if (__t2 == 0)
{
__t2 = CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970;
}
}
2.1.3 首屏渲染時(shí)間打點(diǎn)
啟動(dòng)的終點(diǎn)對(duì)應(yīng)用戶感知到的 Launch Image 消失的第一幀
iOS 12 及以下:root viewController 的 viewDidAppear
iOS 13+:applicationDidBecomeActive
Apple 官方的統(tǒng)計(jì)方式是第一個(gè) CA::Transaction::commit,但對(duì)應(yīng)的實(shí)現(xiàn)在系統(tǒng)框架內(nèi)部,不過(guò)我們可以找到最接近這個(gè)的時(shí)間點(diǎn)
通過(guò) Runloop 源碼分析和調(diào)試,我們發(fā)現(xiàn) CFRunLoopPerformBlock,kCFRunLoopBeforeTimers 和 CA::Transaction::commit()為最近的時(shí)間點(diǎn),所以在這里打點(diǎn)即可.
具體就是可以通過(guò)在 didFinishLaunch 中向 Runloop 注冊(cè) block 或者 BeforeTimer 的 Observer 來(lái)獲取這兩個(gè)時(shí)間點(diǎn)的回調(diào),代碼如下:
注冊(cè)block:
//注冊(cè)block
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopPerformBlock(mainRunloop,NSDefaultRunLoopMode,^(){
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop block launch end:%f",stamp);
});
監(jiān)聽(tīng)BeforeTimer 的 Observer
//注冊(cè)kCFRunLoopBeforeTimers回調(diào)
CFRunLoopRef mainRunloop = [[NSRunLoop mainRunLoop] getCFRunLoop];
CFRunLoopActivity activities = kCFRunLoopAllActivities;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, activities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
if (activity == kCFRunLoopBeforeTimers) {
NSTimeInterval stamp = [[NSDate date] timeIntervalSince1970];
NSLog(@"runloop beforetimers launch end:%f",stamp);
CFRunLoopRemoveObserver(mainRunloop, observer, kCFRunLoopCommonModes);
}
});
CFRunLoopAddObserver(mainRunloop, observer, kCFRunLoopCommonModes);
綜上分析現(xiàn)有項(xiàng)目版本啟動(dòng)時(shí)間均值:
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):54]—————App啟動(dòng)————-耗時(shí):pre-main:4.147820
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):55]—————App啟動(dòng)————-耗時(shí):didfinish:0.654687
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):56]—————App啟動(dòng)————-耗時(shí):total:4.802507
3 啟動(dòng)優(yōu)化
上節(jié)我們主要分析了App啟動(dòng)流程和時(shí)長(zhǎng)統(tǒng)計(jì),下面就是我們要優(yōu)化的方向,盡可能對(duì)各個(gè)階段進(jìn)行優(yōu)化,當(dāng)然也不是過(guò)度優(yōu)化,項(xiàng)目不同階段、不同規(guī)模相應(yīng)的問(wèn)題會(huì)不一樣,做針對(duì)性分析優(yōu)化.
3.1 Pre Main 優(yōu)化
3.1.1 調(diào)整動(dòng)態(tài)庫(kù)
查看了現(xiàn)有工程,基本都以動(dòng)態(tài)庫(kù)進(jìn)行鏈接,總計(jì)48個(gè),所以思路如下
- 減少動(dòng)態(tài)庫(kù),自有動(dòng)態(tài)庫(kù)轉(zhuǎn)靜態(tài)庫(kù)
- 現(xiàn)有的庫(kù)是以CocoaPods管理的,所以通過(guò)hook pod構(gòu)建流程修改Xcode config將部分pod的Mach-O type改為Static Library;
- 同時(shí)對(duì)一些代碼較大的動(dòng)態(tài)庫(kù)進(jìn)行ROI分析,分析是否可以不依賴,在代碼內(nèi)即可實(shí)現(xiàn)替代邏輯,這樣刪除一些ROI很低的動(dòng)態(tài)庫(kù)
- 合并動(dòng)態(tài)庫(kù)
- 目前項(xiàng)目引入的動(dòng)態(tài)庫(kù)較為簡(jiǎn)單,不存在合并項(xiàng),對(duì)于有些中大型工程,有很多自己的基建UI庫(kù),很多過(guò)于分散,需要做的就是能聚合就聚合,譬如XXTableView, XXHUD, XXLabel,建議合并成一個(gè)XXUIKit;譬如一些工具庫(kù),也可以根據(jù)實(shí)際情況聚合為一個(gè)
- 動(dòng)態(tài)庫(kù)懶加載
- 經(jīng)過(guò)分析目前項(xiàng)目階段規(guī)模還沒(méi)必要進(jìn)行懶加載動(dòng)態(tài)庫(kù),畢竟優(yōu)化要考慮收益,僅做優(yōu)化思路參考
- 正常動(dòng)態(tài)庫(kù)都是會(huì)被主二進(jìn)制直接或者間接鏈接的,那么這些動(dòng)態(tài)庫(kù)會(huì)在啟動(dòng)的時(shí)候加載。如果只打包進(jìn) App,不參與鏈接,那么啟動(dòng)的時(shí)候就不會(huì)自動(dòng)加載,在運(yùn)行時(shí)需要用到動(dòng)態(tài)庫(kù)里面的內(nèi)容的時(shí)候,再手動(dòng)懶加載
- 運(yùn)行時(shí)通過(guò)-[NSBundle load]來(lái)加載,本質(zhì)上調(diào)用的是底層的 dlopen。
3.1.2 rebase&binding&objc setup階段
- 無(wú)關(guān)的Class、Method的符號(hào)加載耗時(shí)也會(huì)帶來(lái)額外的啟動(dòng)耗時(shí);所以我們要減少__DATA段中的指針數(shù)量;對(duì)項(xiàng)目代碼分析發(fā)現(xiàn)很多類(lèi)似的Category,每個(gè)Category里面可能只有一個(gè)功能函數(shù),所以具體根據(jù)項(xiàng)目情況分析進(jìn)行Category合并
- +load 除了方法本身的耗時(shí),還會(huì)引起大量 Page In,另外 +load 的存在對(duì) App 穩(wěn)定性也是沖擊,因?yàn)?Crash 了捕獲不到。
- 項(xiàng)目中不少類(lèi)似以下load函數(shù)邏輯,具體分析后很多可以作為啟動(dòng)器進(jìn)行治理管理,runloop空閑去執(zhí)行,
- 首屏后延時(shí)加載
- 另外一類(lèi)是load邏輯操作:很多組件化通訊解耦方案之一就是在load函數(shù)內(nèi)做協(xié)議和類(lèi)的綁定,這部分可以利用 clang attribute,將其遷移到編譯期:
typedef struct{
const char * cls;
const char * protocol;
}_di_pair;
#if DEBUG
#define DI_SERVICE(PROTOCOL_NAME,CLASS_NAME)\
__used static Class<PROTOCOL_NAME> _DI_VALID_METHOD(void){\
return [CLASS_NAME class];\
}\
__attribute((used, section(_DI_SEGMENT "," _DI_SECTION ))) static _di_pair _DI_UNIQUE_VAR = \
{\
_TO_STRING(CLASS_NAME),\
_TO_STRING(PROTOCOL_NAME),\
};\
#else
__attribute((used, section(_DI_SEGMENT "," _DI_SECTION ))) static _di_pair _DI_UNIQUE_VAR = \
{\
_TO_STRING(CLASS_NAME),\
_TO_STRING(PROTOCOL_NAME),\
};\
#endif
原理很簡(jiǎn)單:宏提供接口,編譯期把類(lèi)名和協(xié)議名寫(xiě)到二進(jìn)制的指定段里,運(yùn)行時(shí)把這個(gè)關(guān)系讀出來(lái)就知道協(xié)議是綁定到哪個(gè)類(lèi)了。
- 下線代碼
無(wú)用代碼刪除在所有的性能優(yōu)化手段里基本上是ROI最低的。但是幾乎所 有ROI較高的技術(shù)手段都是一次性優(yōu)化方案,經(jīng)過(guò)幾個(gè)版本迭代后再做優(yōu)化就會(huì)比較乏力。相比之下,針對(duì)代碼的檢測(cè)和刪除在很長(zhǎng)的一段時(shí)間內(nèi)提供了很大的優(yōu)化空間
檢測(cè)手段:靜態(tài)掃描Mach-O文件對(duì)classlist和classrefs做差集,形成初步的無(wú)用類(lèi)集合,并根據(jù)業(yè)務(wù)代碼特征做二次適配
當(dāng)然還有其他常用的技術(shù)手段包括AppCode工具檢測(cè)以及以例如Pecker這樣的基于 IndexStoreDB 、線上統(tǒng)計(jì)等。
不過(guò)以上方案對(duì)Swift的檢測(cè)方案不太適用(和OC存儲(chǔ)差異),這里可以參考github.com/wuba/WBBlad…
對(duì)項(xiàng)目進(jìn)行檢測(cè),發(fā)現(xiàn)還是很多無(wú)用類(lèi)的:
然后二次分析驗(yàn)證,進(jìn)行優(yōu)化
3.1.3 二進(jìn)制重排
iOS系統(tǒng)中虛擬內(nèi)存到物理內(nèi)存的映射都是以頁(yè)為最小單位的。當(dāng)進(jìn)程訪問(wèn)一個(gè)虛擬內(nèi)存Page而對(duì)應(yīng)的物理內(nèi)存卻不存在時(shí),就會(huì)出現(xiàn)Page Fault缺頁(yè)中斷,(對(duì)應(yīng)System Trace的File Backed Page In) 然后操作系統(tǒng)把數(shù)據(jù)加載到物理內(nèi)存中,如果已經(jīng)已經(jīng)加載到物理內(nèi)存了,則會(huì)觸發(fā)Page Cache Hit,后者是比較快的,這也是熱啟動(dòng)比冷啟動(dòng)快的原因之一。
雖然缺頁(yè)中斷異常這個(gè)處理速度是很快的,但是在一個(gè)App的啟動(dòng)過(guò)程中可能出現(xiàn)上千(甚至更多)次Page Fault,這個(gè)時(shí)間積累起來(lái)會(huì)比較明顯了。
基于上面原理. 我們的目標(biāo)就是在啟動(dòng)的時(shí)候增加Page Cache Hit,減少Page Fault,從而達(dá)到優(yōu)化啟動(dòng)時(shí)間的目的
我們需要確定,在啟動(dòng)的時(shí)候,執(zhí)行了哪些符號(hào),盡可能讓這些符號(hào)的內(nèi)存集中在一起,減少占用的頁(yè)數(shù),就能減少Page Fault的命中次數(shù)
程序默認(rèn)情況下是順序執(zhí)行的:
如果啟動(dòng)需要使用的方法分別在2頁(yè)P(yáng)age1和Page2中(method1和method3),為了執(zhí)行相應(yīng)的代碼,系統(tǒng)就必須進(jìn)行兩個(gè)Page Fault。
如果我們對(duì)方法進(jìn)行重新排列,讓method1和method3在一個(gè)Page,那么就可以較少一次Page Fault。
通過(guò)Instruments中的System Trace工具來(lái)看下當(dāng)前的page fault加載情況
這里有個(gè)注意點(diǎn),為了確保App是真正的冷啟動(dòng),需要把內(nèi)存清干凈,不然結(jié)果會(huì)不太準(zhǔn),下圖是我直接殺掉App,重新打開(kāi)得到的結(jié)果
可以看到,和第一次測(cè)試差的有點(diǎn)多,我們可以在殺掉App后,重新打開(kāi)多個(gè)其他的App(盡可能多),或者卸載重裝,這樣在重新打開(kāi)App的時(shí)候,就會(huì)冷啟動(dòng)
綜上我們要做的就是將啟動(dòng)時(shí)調(diào)用的函數(shù)符號(hào)集中靠前排列,減少缺頁(yè)中斷數(shù)量
- 獲取啟動(dòng)代碼執(zhí)行順序
- 確定App在啟動(dòng)的時(shí)候,調(diào)用了哪些函數(shù)(使用了哪些符號(hào)),這里推薦一個(gè)工具AppOrderFiles(GitHub - yulingtianxia/AppOrderFiles: The easiest way to generate order files for Mach-O using Clang. Improving App Performance.?),使用Clang SanitizerCoverage,通過(guò)編譯器插裝的方式,獲取到調(diào)用函數(shù)的符號(hào)順序(當(dāng)然我們也可以在Build Settings中修改Write Link Map File為YES編譯后會(huì)生成一個(gè)Link Map符號(hào)表txt,進(jìn)行分析,創(chuàng)建我們自己的order文件)在App啟動(dòng)后,到首屏VC的viewDidLoad方法內(nèi)輸出order file。
輸出的文件在App沙盒,用模擬器運(yùn)行更方便,得到文件app.order,這里面就是排好序的符號(hào)列表,根據(jù)App的執(zhí)行順序,如果項(xiàng)目比較大的話,會(huì)比較久.
把order文件放到工程目錄,配置到Xcode里面Build Setting -> Order File -> $(PROJECT_DIR)/xxx.order
- 驗(yàn)證\對(duì)比
Xcode里面Build Setting有個(gè)Write Link Map File,可以生成Link Map文件的選項(xiàng),路徑如下Link Map文件
Intermediates.noindex/xxxx.build/Debug-iphoneos/xxx.build/xxx-LinkMap-normal-arm64.txt
生成app文件路徑
Products/Debug-iphoneos/xxx.app
這里我們只關(guān)注Link Map File的符號(hào)表Symbols,這里的順序就是Mach-O文件對(duì)應(yīng)的順序,如果與xxx.order的順序一致,就表明改成功了
再次通過(guò)System Trace工具測(cè)試修改前后對(duì)比
優(yōu)化前后對(duì)比,缺頁(yè)中斷明顯減少
獲取函數(shù)調(diào)用符號(hào),采用Clang插樁可以直接hook到Objective-C方法、Swift方法、C函數(shù)、Block,可以不用區(qū)別對(duì)待
3.2 After Main優(yōu)化
這部分是個(gè)大頭的優(yōu)化項(xiàng),實(shí)際場(chǎng)景需要我們根據(jù)自己的具體項(xiàng)目來(lái)分析,但大體遵循一些相同的思路
3.2.1 功能/方法優(yōu)化
- 推遲&減少I(mǎi)/O操作
- 此處對(duì)項(xiàng)目after main后的啟動(dòng)邏輯分析不涉及IO操作未做優(yōu)化
- 控制線程數(shù)量
- 項(xiàng)目中啟動(dòng)階段線程數(shù)量不多且必要,影響不大就未動(dòng),但根據(jù)各自的項(xiàng)目情況進(jìn)行分析治理
- 啟動(dòng)加載項(xiàng)治理
- 這里主要是一些基建和三方/集團(tuán)SDK初始化任務(wù)以及各業(yè)務(wù)組件工程的啟動(dòng)加載項(xiàng), 包括前面部分load函數(shù)的邏輯放到這里的啟動(dòng)器來(lái)進(jìn)行調(diào)度管理。
- 我們可以把這部分做一個(gè)啟動(dòng)器進(jìn)行維護(hù)和監(jiān)控,防劣化。
- 啟動(dòng)器自注冊(cè),注冊(cè)項(xiàng)包括啟動(dòng)操作閉包,啟動(dòng)執(zhí)行優(yōu)先級(jí),啟動(dòng)操作是否后臺(tái)執(zhí)行等可選項(xiàng)。
- 自注冊(cè)服務(wù)無(wú)非還是:”啟動(dòng)項(xiàng):?jiǎn)?dòng)閉包 “ 這么一個(gè)綁定實(shí)現(xiàn),所以可以類(lèi)似前面(class-protocol綁定)所講的思路,將這部分操作寫(xiě)入到可執(zhí)行文件的DATA段中,運(yùn)行時(shí)再?gòu)?/strong>DATA段取出數(shù)據(jù)進(jìn)行相應(yīng)的操作(調(diào)用函數(shù)),這樣也能夠覆蓋所有的啟動(dòng)階段,例如main()之前的階段。
- 對(duì)項(xiàng)目分析后,將鍵盤(pán)初始化、地圖定位、意見(jiàn)反饋還有非首頁(yè)模塊初始化等非必要的啟動(dòng)項(xiàng)降低優(yōu)先級(jí)延后時(shí)機(jī)執(zhí)行。
- 串行->并行 同步->異步
- 對(duì)于一些耗時(shí)操作異步、并行操作,不阻塞主線程的執(zhí)行
- 方法耗時(shí)統(tǒng)計(jì)分析
- 統(tǒng)計(jì)啟動(dòng)過(guò)程業(yè)務(wù)代碼耗時(shí)并對(duì)耗時(shí)方法進(jìn)行分析治理
- 高頻次方法調(diào)用
- 有些方法的單個(gè)耗時(shí)不高,但是頻繁調(diào)用就會(huì)顯現(xiàn)耗時(shí),我們可以加內(nèi)存緩存,當(dāng)然了具體場(chǎng)景具體分析
- 利用閃屏頁(yè)的時(shí)間做一些首頁(yè)UI的預(yù)構(gòu)建
- 項(xiàng)目中有啟動(dòng)閃屏頁(yè),還有第一次啟動(dòng)彈框隱私頁(yè)這個(gè)間隙做一些首屏操作的前移
- 利用這一段時(shí)間來(lái)構(gòu)建首頁(yè)UI了、首屏網(wǎng)絡(luò)數(shù)據(jù)的預(yù)下載、緩存、啟動(dòng)Flutter引擎等工作
3.2.2 首屏渲染優(yōu)化
屏幕顯示遵循一套圖形渲染管線來(lái)完成最終的顯示工作:
1.Application階段(應(yīng)用內(nèi)):
Handle Events:
這個(gè)過(guò)程中會(huì)先處理點(diǎn)擊事件,這個(gè)過(guò)程中有可能會(huì)需要改變頁(yè)面的布局和界面層次。
Commit Transaction:
此時(shí) App 會(huì)通過(guò) CPU 處理顯示內(nèi)容的前置計(jì)算,比如布局計(jì)算、圖片解碼等任務(wù),之后將計(jì)算好的圖層進(jìn)行打包發(fā)給 Render Server。(核心Core Animation負(fù)責(zé))
Commit Transaction 這部分中主要進(jìn)行的是:Layout、Display、Prepare、Commit 等四個(gè)具體的操作, 最后形成一條事務(wù),通過(guò) CA::Transaction::commit()提交渲染
- Layout:
構(gòu)建視圖相關(guān),layoutSubviews、addSubview 方法添加子視圖、AutoLayout根據(jù) Layout Constraint 計(jì)算各個(gè)view的frame,文本計(jì)算(size)等。
layoutSubviews:在此階段會(huì)調(diào)用,但是滿足條件如frame,bounds,transform屬性改變、添加或者刪除view、顯式調(diào)用setNeedsLayout等 - Display:
繪制視圖:交給 Core Graphics 進(jìn)行視圖的繪制,得到圖元 primitives 數(shù)據(jù),注意不是位圖數(shù)據(jù),位圖是GPU階段根據(jù)圖元組合而得。但是如果重寫(xiě)了 drawRect: 方法,這個(gè)方法會(huì)直接調(diào)用 Core Graphics 繪制方法得到 bitmap 數(shù)據(jù),同時(shí)系統(tǒng)會(huì)額外申請(qǐng)一塊內(nèi)存,用于暫存繪制好的 bitmap,導(dǎo)致繪制過(guò)程從 GPU 轉(zhuǎn)移到了 CPU,這就導(dǎo)致了一定的效率損失。與此同時(shí),這個(gè)過(guò)程會(huì)額外使用 CPU 和內(nèi)存,因此需要高效繪制,否則容易造成 CPU 卡頓或者內(nèi)存爆炸。 - Prepare:
Core Animation 額外的工作,主要是圖片解碼和轉(zhuǎn)換,盡量使用GPU支持的格式, Apple推薦JPG和PNG
譬如在UIImageView中展示圖片,會(huì)經(jīng)歷如下過(guò)程: 加載、解碼、渲染 簡(jiǎn)單說(shuō)就是將普通的二進(jìn)制數(shù)據(jù) (存儲(chǔ)在dataBuffer 數(shù)據(jù)) 轉(zhuǎn)化成 RGB的數(shù)據(jù)(存儲(chǔ)在ImageBuffer), 這個(gè)被稱(chēng)為圖像的解碼decode, 它有如下特點(diǎn):
decode解碼過(guò)程是一個(gè)耗時(shí)過(guò)程, 并且是在CPU中完成的. 也就是我們這部分的prepare中完成。
解碼以后的RGB圖占用的內(nèi)存大小只與bitmap的像素格式(RGB32, RGB23, Gray8 …)和圖片寬高有關(guān), 常見(jiàn)bitmap大小: 每個(gè)像素點(diǎn)大小?width?height, 而與原來(lái)的壓縮格式PNG, JPG大小無(wú)關(guān).
2.GPU渲染階段:
主要是一些圖元的操作、幾何處理、光柵化、像素處理等,不一一細(xì)說(shuō),這部分操作我們能做的工作畢竟是有限的
所以,我們大致可以做的優(yōu)化點(diǎn)如下:
- 預(yù)渲染\異步渲染:
- 大致思路就是在子線程將所有的視圖繪制成一張位圖,然后回到主線程賦值給 layer的 contents
- 圖片異步解碼:
- 注意這里并不是將圖片加載放到異步線程中在異步線程中生成一個(gè) UIImage或者是 CGImage然后再主線程中設(shè)置給 UIImageView,而是在子線程中先將圖片繪制到CGBitmapContext,然后從bitmap 直接創(chuàng)建圖片,常用的圖片框架都類(lèi)似。
- 按需加載
- 不需要或者非首屏較為復(fù)雜的視圖延后加載,減少首屏圖層的層級(jí)
- 其他:
- 離屏渲染 盡量減少透明視圖個(gè)數(shù)等等一些細(xì)節(jié)也要注意
4 成果
經(jīng)過(guò)一些列優(yōu)化,還是有一些速度的提升,雖然工程還不是大型工程,不過(guò)及早持續(xù)優(yōu)化可以防止業(yè)務(wù)迭代到一定程度難以下手的地步。
iPhone 7p多次均值
優(yōu)化前
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):54]—————App啟動(dòng)————-耗時(shí):pre-main:4.147820
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):55]—————App啟動(dòng)————-耗時(shí):didfinish:0.654687
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):56]—————App啟動(dòng)————-耗時(shí):total:4.802507
優(yōu)化后
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):54]—————App啟動(dòng)————-耗時(shí):pre-main:3.047820
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):55]—————App啟動(dòng)————-耗時(shí):didfinish:0.254687
[函數(shù)名:+[LaunchTrace mark]_block_invoke][行號(hào):56]—————App啟動(dòng)————-耗時(shí):total:3.302507
pre main階段下降平均大概20%, after main階段平均下降大概60%, 總體均值下降30%.
當(dāng)然目前還處于未上線版本,后續(xù)上線后借助監(jiān)控平臺(tái)借助線上更多數(shù)據(jù),更多機(jī)型來(lái)更好的的進(jìn)行分析優(yōu)化
5 總結(jié)
啟動(dòng)速度瓶頸非一日之寒,需要持續(xù)的進(jìn)行優(yōu)化,這當(dāng)中也少不了監(jiān)控體系的持續(xù)建設(shè)和優(yōu)化,日常線上數(shù)據(jù)的分析,防止業(yè)務(wù)快速迭代中的啟動(dòng)速度劣化,對(duì)動(dòng)態(tài)庫(kù)的引入、新增 +load 和靜態(tài)初始化、啟動(dòng)任務(wù)的新增都要加入Code Review機(jī)制,優(yōu)化啟動(dòng)架構(gòu)為啟動(dòng)這些基礎(chǔ)性能保駕護(hù)航。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-607774.html
作者:京東物流 彭欣
來(lái)源:京東云開(kāi)發(fā)者社區(qū) 自猿其說(shuō)Tech文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-607774.html
到了這里,關(guān)于從iOS App啟動(dòng)速度看如何為基礎(chǔ)性能保駕護(hù)航 | 京東物流技術(shù)團(tuán)隊(duì)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!