背景
58APP現(xiàn)階段所有的業(yè)務(wù)都融合在一個可執(zhí)行文件中。其弊端在于所有的類都在啟動時同時加載,如某SDK在啟動階段hook大量的系統(tǒng)方法,其中一個load方法的耗時就已經(jīng)達(dá)到了29ms。而其業(yè)務(wù)處于二級頁面,甚至更深的入口,甚至很多用戶在APP使用過程中都不會觸發(fā)此業(yè)務(wù)場景,這就造成了啟動任務(wù)的浪費(fèi)。除此之外,58同城一直在致力于降低APP的下載大小。經(jīng)過與蘋果的開發(fā)者關(guān)系部的多次溝通,蘋果建議我們使用動態(tài)庫來實現(xiàn)APP的增量更新。在58APP中,有些SDK的更新頻率較低,也就是說用戶更新前后的兩個版本的SDK其實并無差異。App Store上提供了增量更新的功能,當(dāng)APP中的文件前后兩個版本不發(fā)生變化時,此部分?jǐn)?shù)據(jù)不會被重復(fù)下載。我們通過測試發(fā)現(xiàn),在iPhone 11 pro max等機(jī)型上,58同城APP的10.3.1版本全量下載需要88MB流量,但是從10.2.5版本更新到10.3.1版本時只需要74MB的流量。
目前我們只檢測到App Store具備diff能力,TestFlight包并不具備此能力?;谝陨媳尘埃覀冮_始將58APP的部分靜態(tài)庫轉(zhuǎn)為動態(tài)庫。
現(xiàn)狀
既然要做APP的動態(tài)庫化,做到什么程度是首先要探討的問題。首先我們調(diào)研了業(yè)界的一部分APP,動態(tài)庫在一些新APP上使用還是較為廣泛的,尤其是引入Swift的APP。但是在一些大型APP上,則相對較為謹(jǐn)慎。結(jié)合58APP的架構(gòu)特征,我們是把所有的代碼都打包成動態(tài)庫,還是把一部分業(yè)務(wù)作出動態(tài)庫,還是選取耗時且穩(wěn)定的底層庫作為動態(tài)庫?這涉及到技術(shù)成本和收益的問題。由于動態(tài)庫的吸附性,動態(tài)庫經(jīng)過編譯鏈接后會將所依賴的代碼和靜態(tài)庫一并打包到動態(tài)庫中,因此需要避免這種情況,防止代碼的重復(fù)引入(或者通過添加鏈接參數(shù)不將靜態(tài)庫打包到動態(tài)庫中:other link flags 設(shè)置 -undefined dynamic_lookup)。因此現(xiàn)階段的技術(shù)方案為,從架構(gòu)底層開始,自下而上的進(jìn)行動態(tài)庫化。我們決定先將底層的不常變更的SDK優(yōu)先制作成動態(tài)庫。目前58同城已經(jīng)將13個SDK做成了動態(tài)庫,并對其中的10個進(jìn)行懶加載。
方案及過程
在實際開發(fā)過程中,我們遇到了很多的問題。之前提到,很多新的APP都可以使用動態(tài)庫,為什么老的業(yè)務(wù)復(fù)雜的APP不太容易做到全面動態(tài)庫化呢?這里涉及到工程配置、項目架構(gòu)、性能損耗及收益的權(quán)衡、動態(tài)庫的特性等問題。在進(jìn)行動態(tài)庫化之前,58同城的CocoaPods 版本為1.2版本,CocoaPods 1.2版本對動態(tài)庫的兼容性略差,很多腳本和流程都需要自己引入。好在CocoaPods1.8 對動態(tài)庫的支持很友好,架構(gòu)剝離、重簽名等流程已經(jīng)實現(xiàn)了自動化。為了避免多個動態(tài)庫吸附相同靜態(tài)庫導(dǎo)致包增大,我們目前采用的方式是從架構(gòu)底層開始,自下而上的進(jìn)行動態(tài)庫化。現(xiàn)階段采用的方式是將靜態(tài)庫放到獨(dú)立的工程中編譯鏈接成對應(yīng)的動態(tài)庫,然后將該動態(tài)庫接入到項目中。
“自下而上的進(jìn)行動態(tài)庫化”看起來很簡單,但是實際操作中遇到了很多的問題。
如何識別SDK之間的依賴關(guān)系
為什么要識別SDK之間的依賴關(guān)系?在58項目中,三方SDK以及TEG、信安等部門提供的靜態(tài)庫文件數(shù)量總數(shù)達(dá)到了上百個。這些SDK之間存在復(fù)雜的依賴關(guān)系。這些依賴關(guān)系隱含在SDK內(nèi)部,由于缺乏源碼,我們很難直接通過文件來確定。但是如果不確定SDK之間的依賴關(guān)系,那么在生成動態(tài)庫時會將只能目標(biāo)SDK放入工程中,當(dāng)缺乏目標(biāo)SDK所依賴的其他SDK時會報錯,然后通過報錯提示我們再將所依賴的SDK轉(zhuǎn)成動態(tài)庫,此種方式效率較為低下。因此我們直接解析靜態(tài)庫的二進(jìn)制文件,通過對靜態(tài)庫中所有目標(biāo)文件的內(nèi)外符號的識別,整理出靜態(tài)庫之間的依賴關(guān)系。
- 首先將靜態(tài)庫中的目標(biāo)文件的符號表進(jìn)行整合,梳理出每個目標(biāo)文件的內(nèi)部符號表和外部符號表。目標(biāo)文件中存在一個符號表,符號表用于記錄符號的類型和符號對應(yīng)的地址。以iOS系統(tǒng)中的目標(biāo)文件的符號表為例,其符號表結(jié)構(gòu)體如下:
struct nlist_64 {
union {
uint32_t n_strx; /* index into the string table */
} n_un;
uint8_t n_type; /* type flag, see below */
uint8_t n_sect; /* section number or NO_SECT */
uint16_t n_desc; /* see <mach-o/stab.h> */
uint64_t n_value; /* value of this symbol (or stab offset) */
};
/*
* Values for N_TYPE bits of the n_type field.
*/
#define N_UNDF 0x0 /* undefined, n_sect == NO_SECT */
#define N_ABS 0x2 /* absolute, n_sect == NO_SECT */
#define N_SECT 0xe /* defined in section number n_sect */
#define N_PBUD 0xc /* prebound undefined (defined in a dylib) */
#define N_INDR 0xa /* indirect */
在iOS中符號的n_type類型分為:N_UNDF(未定義)、N_ABS(絕對)、N_SECT(有定義)、N_PBUD(在動態(tài)庫中定義)、N_INDR(間接類型)共5種類型。首先我們?yōu)槊總€目標(biāo)文件創(chuàng)建兩個集合表:外部符號表和內(nèi)部符號表。我們將符號表中每個n_type為N_UNDF的符號放入外部符號表中。其他類型的符號放入內(nèi)部符號表中。
- 隨后我們將靜態(tài)庫的每個目標(biāo)文件的內(nèi)部符號表和外部符號表進(jìn)行整合,形成靜態(tài)庫級別的內(nèi)部符號表和外部符號表。最終根據(jù)靜態(tài)庫的內(nèi)部符號和外部符號確立彼此的依賴關(guān)系。
-
在此過程中,我們也發(fā)現(xiàn)2個小插曲,我們發(fā)現(xiàn)輸出的結(jié)果存在一定的異常,如58集團(tuán)內(nèi)的SDK的依賴了第三方的某個SDK,這顯然與事實不符。其原因為外部三方SDK自定義了系統(tǒng)庫C函數(shù),導(dǎo)致項目中所有的調(diào)用此函數(shù)的地方全部走到了這個SDK定義的函數(shù)中。此外,還有符號沖突報警告,并沒有報錯,這點還是挺意外的,之前一直以為符號沖突必定報錯。
符號沖突警告
動態(tài)庫的依賴配置
假設(shè)有兩個靜態(tài)庫A和B,A依賴B。如果A要做成動態(tài)庫則需要將B打成動態(tài)庫,然后將A的靜態(tài)庫和B的動態(tài)庫放入一個動態(tài)庫的工程項目中,編譯鏈接后將生成A的動態(tài)庫。假設(shè)B不放入動態(tài)庫工程,則A無法鏈接成功,也就無法生成動態(tài)庫。如果將問題進(jìn)一步復(fù)雜化,假設(shè)有N個上層SDK,隨機(jī)依賴了K個底層SDK,那么如何保證這上層SDK所依賴的每個底層SDK版本都是一致的呢?如果一旦更新一個底層SDK,那么是不是要同步修改N個動態(tài)庫工程呢?這個問題最簡單的處理方式就是通過CocoaPods來實現(xiàn)管理,創(chuàng)建動態(tài)庫工程組,當(dāng)前CocoaPods 1.8 版本非常友好的解決了動態(tài)庫的問題,在podfile中使用use_frameworks創(chuàng)建N+K個pod,Pod之間的依賴關(guān)系交給CocoaPods來管理。
降低業(yè)務(wù)代碼的改動
為了保持上層業(yè)務(wù)代碼的保持不變,因此我們盡量保持頭文件不發(fā)生變化。當(dāng).a文件轉(zhuǎn)成.framework時,頭文件單獨(dú)剝離,不存放到framework中。如果是.framework靜態(tài)庫轉(zhuǎn)為.framework動態(tài)庫,則保持framework名稱相同。頭文件與原來的方式一致。資源方面不要打到動態(tài)庫的framework中。如果資源加入asserts文件在framework下,那么在編譯成動態(tài)庫原有代碼的訪問方式無法訪問到(在我的另一篇文章中有示例http://www.lxweimin.com/p/cb5a43bce796
),可能會造成一些問題。在這里需要提一下,如果靜態(tài)庫轉(zhuǎn)為動態(tài)庫時如果為同名framework,并且你的工程中將Other Linker Flags設(shè)置為-all_load,那么可能會存在報xxx_vers.o符號沖突錯誤。
這是由于在生成framework時,Xcode動態(tài)的寫入了xxx_vers.m文件并參與了編譯和鏈接。
那么在進(jìn)行二次打包成framework時,編譯器會再次寫入xxx_vers.m。導(dǎo)致鏈接沖突。目前的解決方案為:將靜態(tài)庫中每個架構(gòu)下的xxx_vers.o文件剔除(ar -x 將靜態(tài)庫拆分為目標(biāo)文件,然后刪除指定文件),再通過
libtool -static -o
將目標(biāo)文件重新合并為靜態(tài)庫。或者不要使用-all_load,改為-Objc即可。
development target 與bundle id
在創(chuàng)建動態(tài)庫時需要額外注意一點,動態(tài)庫工程的development target 版本務(wù)必要小于等于iOS工程的版本號。這是由于在某些高版本打包出來的二進(jìn)制與低版本的二進(jìn)制不同。如果高版本的動態(tài)庫運(yùn)行在低版本的機(jī)型上會造成崩潰,這里與代碼的API兼容無關(guān),從底層的二進(jìn)制就不兼容。另外每個動態(tài)庫的bundle id必須保持唯一,如果一個APP中兩個動態(tài)庫的bundle id 相同,則無法安裝成功。
利用CocoaPods管理則不會出現(xiàn)此類問題。
懶加載如何配置
總所周知,蘋果不推薦過多的使用自定義動態(tài)庫,因為非懶加載的動態(tài)庫會造成額外的啟動時間消耗。
接入上述8個靜態(tài)庫demo時的pre main耗時
System Interface | Static Runtime Initialization |
---|---|
154ms | 37ms |
147ms | 38ms |
157ms | 34ms |
接入上述8個靜態(tài)庫demo對應(yīng)的動態(tài)庫
System Interface | Static Runtime Initialization |
---|---|
224ms | 40ms |
224ms | 42ms |
224ms | 43ms |
業(yè)界通常認(rèn)為的優(yōu)化方案為進(jìn)行動態(tài)庫合并,將多個動態(tài)庫合并為1個。經(jīng)過測試后發(fā)現(xiàn),動態(tài)庫合并在iOS13系統(tǒng)的中高端設(shè)備上優(yōu)化并不明顯,但是iOS11上,用5s測試就能看出效果來。(推測有兩方面原因:1、中高端設(shè)備性能太快,測試數(shù)據(jù)又太小,數(shù)據(jù)不明顯。2、iOS11上使用dyld2加載動態(tài)庫,ios 13上使用dyld3加載動態(tài)庫,dyld3優(yōu)化了啟動時間。)
58沒有采用動態(tài)庫合并的方式,倒不是由于性能的原因,動態(tài)庫合并違背了我們的架構(gòu)解耦原則。如果實現(xiàn)動態(tài)庫合并,那么在多APP代碼復(fù)用的背景下,業(yè)務(wù)APP使用了其中的一個SDK,就需要將整個集合引入到APP中,這一點不符合我們的業(yè)務(wù)背景。因此我們經(jīng)過調(diào)研后,我們發(fā)現(xiàn)業(yè)界有知名APP采用懶加載的方式引入動態(tài)庫,并且懶加載的比例還很高。
接入上述8個demo動態(tài)庫并懶加載
System Interface | Static Runtime Initialization |
---|---|
150ms | 8ms |
125ms | 7ms |
126ms | 7ms |
采用懶加載的話,動態(tài)庫在啟動時并不加載,而是在業(yè)務(wù)使用時再進(jìn)行加載,這樣大大優(yōu)化了啟動耗時。
那么如何進(jìn)行懶加載呢?
在升級到CocoaPods1.8后,CocoaPods1.2上可以配置動態(tài)庫懶加載的入口已經(jīng)沒了,自定義動態(tài)庫體現(xiàn)在哪里呢?最開始懷疑在CocoaPods 自動生成的Pods-xxx-frameworks.sh 腳本中,但是實際上這個腳本并不控制鏈接,這個腳本負(fù)責(zé)架構(gòu)剔除及重簽名等。靜態(tài)庫與動態(tài)庫的鏈接在xcconfig文件中配置,修改xcconfig每次編譯立即生效。
因此可以在pod install 后每次都修改xcconfig文件,讓懶加載的動態(tài)庫只參與簽名與拷貝,不參與鏈接。
在這里需要注意的是如果你的動態(tài)庫沒有剝離符號表,那么請在發(fā)版前將其從動態(tài)庫中剝離,像58同城這樣在未完全借助CocoaPods來打包動態(tài)庫的方案,符號表需要自己管理。
懶加載如何調(diào)用
動態(tài)庫懶加載由于動態(tài)庫在代碼編譯時不參與鏈接,因此通過原有的方式調(diào)用代碼會報鏈接錯誤,找不到動態(tài)庫對應(yīng)的符號。因此調(diào)用動態(tài)庫的地方需要修改為runtime動態(tài)調(diào)用。目前的懶加載實現(xiàn)方式比較簡單,當(dāng)使用某個類時,動態(tài)獲取這個類,如果獲取不到這個類,則加載相應(yīng)的動態(tài)庫。例如:
+ (void *)lazyLoadFrameWork:(NSString *)frameworkName{
NSString *path = [[[NSBundle mainBundle] bundlePath] stringByAppendingFormat:@"/Frameworks/%@.framework/%@",frameworkName,frameworkName];
void *rest = dlopen([path UTF8String], RTLD_LAZY);
return rest;
}
這可能有同學(xué)比較關(guān)注dlopen可能帶來的風(fēng)險,蘋果是不推薦使用dlopen的,但是目前沒有遇到相關(guān)審核問題。另外可能一部分同學(xué)比較關(guān)注dlopen的失敗率,通過埋點我們監(jiān)測到,dlopen確實會存在不到萬分之一的卡死概率,核心業(yè)務(wù)謹(jǐn)慎使用。
其實系統(tǒng)庫內(nèi)部也存在一些懶加載,如果感興趣的話可以通過注冊監(jiān)聽來斷點查看
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
由于懶加載動態(tài)庫的代碼調(diào)用為動態(tài)調(diào)用的方式,因此在接口較多、變更頻繁、調(diào)用處較多的SDK并不適合做動態(tài)庫懶加載。而那些代碼穩(wěn)定、接口較為收斂,業(yè)務(wù)調(diào)用收斂的SDK較適合做動態(tài)庫懶加載。也就是說,如果你能掌握SDK所有的調(diào)用處,并且這些SDK即無法推動下線,又不經(jīng)常使用,那么把它作為動態(tài)庫懶加載還是比較合適的。
在SDK接口收斂的前提下,我們可以將SDK的類映射成對應(yīng)的協(xié)議(如果類的參數(shù)遞歸擴(kuò)展,那么想辦法用ID代替,如果還不能解決收斂問題,那么就不要使用懶加載了,否則可能由1個協(xié)議引申出多個協(xié)議,工作量大大增加)
以科大訊飛為例,最終的代碼調(diào)用如下
DylibRedefineClass(IFlySpeechRecognizer) iflySpeechRecognizer = nil;
iflySpeechRecognizer = [DylibObtainClass(IFlySpeechRecognizer) sharedInstance];
只需要聲明變量和獲取類,API和代碼調(diào)用都不需要變動。
runtime雖然靈活,但是也帶來1個問題:
-
如果動態(tài)庫升級了,API存在變動怎么辦?
雖然懶加載動態(tài)庫的前提是不做頻繁升級和變更,但是一旦升級則可能引起災(zāi)難性的后果,即API發(fā)生變更,編譯卻正常通過。因為我們編譯是依賴協(xié)議進(jìn)行的,及時API發(fā)生了變更很可能負(fù)責(zé)升級的同學(xué)并沒有對協(xié)議進(jìn)行升級。這就容易讓后續(xù)升級維護(hù)的同學(xué)掉進(jìn)一個大坑。
慘遭不幸的同學(xué)
因此58針對這種情況作了自動檢測,在啟動時通過runtime進(jìn)行檢測。
//懶加載 iflyMSC 庫中的IFlySpeechRecognizer 類
DylibHelperobtainClassMethod(IFlySpeechRecognizer,iflyMSC)
//在debug 環(huán)境下自動插入測試代碼
#if BladesToolEnable
#define DylibHelperTestMethod(__classname__,__framework__)\
__attribute__((constructor)) void dylibTest##__classname__##Class(){\
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{\
[WBDylibHelper checkClass:@#__classname__ framework:@#__framework__];\
});\
}
#else
//在release 環(huán)境下不插入測試代碼
#define DylibHelperTestMethod(__classname__,__framework__)
#endif
//定義頭文件
#define DylibHelperExportObtainClassMethod(__classname__)\
+ (Class)obtain##__classname__##Class
//函數(shù)實現(xiàn)
#define DylibHelperobtainClassMethod(__classname__,__framework__)\
+ (Class)obtain##__classname__##Class{\
Class class = NSClassFromString(@#__classname__);\
if (!class) {\
[self lazyLoadFrameWork:@#__framework__];\
class = NSClassFromString(@#__classname__);\
}\
return class;\
}\
DylibHelperTestMethod(__classname__,__framework__)
在開發(fā)階段,每個動態(tài)懶加載的類都會被自動插入一個自檢函數(shù),在啟動時會檢測該類是否都能響應(yīng)其對應(yīng)協(xié)議的所有函數(shù)。協(xié)議方法獲取方式如下:
//實例方法 required
methods = protocol_copyMethodDescriptionList(p, YES, YES, &methodCount);
//實例方法 optional
methods = protocol_copyMethodDescriptionList(p, NO, YES, &methodCount);
//類方法 required
methods = protocol_copyMethodDescriptionList(p, YES, NO, &methodCount);
//類方法 optional
methods = protocol_copyMethodDescriptionList(p, NO, NO, &methodCount);
這里有個問題需要注意下,如果協(xié)議沒有被任何類標(biāo)記為遵循,那么通過runtime是獲取不到的,如果你在開發(fā)時發(fā)現(xiàn)通過runtime獲取不到協(xié)議,那么試著<協(xié)議>一下
另外,我們在開發(fā)中還遇到一個小問題,有些SDK在打成動態(tài)庫后如果不采用懶加載的方式會報鏈接失敗,這是由于SDK中有些類或符號被標(biāo)記為隱藏
__attribute__((visibility("hidden"))) 設(shè)置為符號不導(dǎo)出,在export info 中沒有相關(guān)符號
遇到這種情況就只能采用懶加載或者是保持靜態(tài)庫的方式了。
如何量化收益呢?
收益可以分為2部分,一個是APP更新大小的減少,另一個是啟動耗時的減少。
- APP更新大小的減少
很遺憾,由于只有App Store包才具備diff下載的能力,通過TF包沒法檢測,因此這部分?jǐn)?shù)據(jù)很難量化。并且,App Store在下載時會對下載數(shù)據(jù)進(jìn)行壓縮,可能單臺設(shè)備200MB左右的安裝大小,其實下載大小還不到100MB,各個機(jī)型下載大小的數(shù)據(jù)可以在App Store后臺看到。 - 啟動優(yōu)化量化
啟動優(yōu)化量化這塊還是有現(xiàn)成方案可以借鑒的(可以參考手淘和字節(jié)相關(guān)的文章),但是我們最終還是使用instrument來進(jìn)行統(tǒng)計。之所以使用instrument有以下兩點原因:
1、我們不需要運(yùn)行期間收集數(shù)據(jù), 開發(fā)階段單臺設(shè)備采集數(shù)據(jù)即可。
2、rebase 和 bind的優(yōu)化時間目前還監(jiān)控不到。
在這里提一個小問題,大家都知道dyld會先加載可執(zhí)行程序所依賴的多個動態(tài)庫,那么在這里提一個小問題,程序在啟動時是加載完一個動態(tài)庫后立即調(diào)用這個庫中的load方法嗎?
之所以提出這個問題是因為最開始我們想通過代碼拿到動態(tài)庫從在load方法調(diào)用前的耗時。最開始選擇的技術(shù)方案是通過_dyld_register_func_for_add_image注冊image的加載時間,當(dāng)系統(tǒng)加載image時會將事件回調(diào)給我們。那么我們在回調(diào)開始時打點計時,當(dāng)連續(xù)兩個回調(diào)打點時,其時間差即可認(rèn)為是這個動態(tài)庫的加載耗時。但是在實踐時發(fā)現(xiàn),當(dāng)我們進(jìn)行注冊時,回調(diào)函數(shù)密集回調(diào),時間間隔極其短,這是因為_dyld_register_func_for_add_image在注冊時會將已經(jīng)加載的鏡像一并返回。
原因是image在加載后并不是同步加載這個庫中的所有l(wèi)oad方法。如何論證呢?創(chuàng)建一個哨兵動態(tài)庫,讓這個動態(tài)庫最先參與鏈接。在哨兵動態(tài)庫的類的load方法中注冊_dyld_register_func_for_add_image回調(diào)。運(yùn)行后發(fā)現(xiàn),在執(zhí)行哨兵庫的類的load方法執(zhí)行時,_dyld_register_func_for_add_image會同步執(zhí)行多次回調(diào),獲取image的名字會發(fā)現(xiàn)我們自定義的動態(tài)庫都已經(jīng)回調(diào),只是對應(yīng)的動態(tài)庫的load方法都還沒有執(zhí)行。因此通過_dyld_register_func_for_add_image無法獲取到每個動態(tài)庫的加載時間。
從dyld的源碼中也可以看出來dyld是先通知runtime讓動態(tài)庫先初始化,然后再讓主應(yīng)用進(jìn)行初始化。
void initializeMainExecutable()
{
// record that we've reached this step
gLinkContext.startedInitializingMainExecutable = true;
// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}
// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);
// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}
動態(tài)庫懶加載優(yōu)化主要在三個方面:
1、將代碼從可執(zhí)行文件中剝離出來,減少了可執(zhí)行文件rebase 和 bind的時間;
2、懶加載動態(tài)庫的load、contructor函數(shù)、靜態(tài)變量初始化的時機(jī)延后,在dlopen后調(diào)用;
3、還有隱藏的一塊是唯一被該SDK依賴的系統(tǒng)動態(tài)庫,可能由于我們懶加載相應(yīng)的SDK后也被間接懶加載。
其實減少系統(tǒng)動態(tài)庫的依賴數(shù)量要比單純的SDK動態(tài)庫懶加載要更為有效。如果要做到減少對系統(tǒng)庫的依賴,首先我們就要確定哪些SDK或代碼依賴了哪些系統(tǒng)庫,如果不太復(fù)雜那么可以考慮將其處理成動態(tài)庫進(jìn)行懶加載。這一步可能還需要跟業(yè)務(wù)結(jié)合做評估。
總結(jié)
58同城目前通過動態(tài)庫懶加載所達(dá)到的優(yōu)化在6s設(shè)備上是240ms 左右,6p上能達(dá)到800ms左右,相當(dāng)于優(yōu)化了12%的啟動速度。但是動態(tài)庫懶加載與其他方案相比,其具備的優(yōu)勢是具有長期優(yōu)化的空間。另外在降低更新大小上,不太好量化具體的收益,但是根據(jù)蘋果的答復(fù)應(yīng)該是有效果的,只是效果可能并不明顯。