前言:本文旨在介紹iOS性能優(yōu)化中有關(guān)APP啟動流程的介紹和優(yōu)化炫狱。
一乱凿、APP啟動流程
1倍试、APP的冷啟動流程
- 點(diǎn)擊圖標(biāo)之后谷市,系統(tǒng)加載APP可執(zhí)行文件
- 啟動Dyld(動態(tài)加載器) ,然后Dyld遞歸加載程序所需的動態(tài)庫
- Dyld 對程序進(jìn)行 rebase 以及 bind 操作
- Runtime加載類和分類的load方法
- 進(jìn)行各種Objc結(jié)構(gòu)的初始化(注冊O(shè)bjc類 嚎于、初始化類對象等等)
- 調(diào)用C++靜態(tài)初始化器和attribute((constructor))修飾的函數(shù)掘而。
- 執(zhí)行程序的 main 函數(shù)、AppDelegate的application:didFinishLaunchingWithOptions:方法
2于购、APP的冷啟動流程的3大階段
APP的冷啟動可以概括為3大階段:Dyld ---> Runtime ---> main
Dyld(dynamic link editor):Apple的動態(tài)鏈接器袍睡,可以用來裝載Mach-O文件(可執(zhí)行文件、動態(tài)庫等)
2.1肋僧、啟動APP時(shí)斑胜,Dyld所做的事情有:
- 系統(tǒng)裝載APP的可執(zhí)行文件后,啟動Dyld嫌吠,之后Dyld會遞歸加載所有依賴的動態(tài)庫止潘;
- 然后Dyld 對程序進(jìn)行 rebase 以及 bind 操作
- 會通知Runtime進(jìn)行下一步的處理。
2.2辫诅、Runtime所做的事情有:
- 調(diào)用map_images進(jìn)行可執(zhí)行文件內(nèi)容的解析和處理凭戴;
- 在load_images中調(diào)用call_load_methods,調(diào)用所有Class和Category的+load方法炕矮;
- 進(jìn)行各種Objc結(jié)構(gòu)的初始化(注冊O(shè)bjc類 么夫、初始化類對象等等);
- 調(diào)用C++靜態(tài)初始化器和attribute((constructor))修飾的函數(shù)吧享。
- 到此為止魏割,可執(zhí)行文件和動態(tài)庫中所有的符號(Class,Protocol钢颂,Selector钞它,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中殊鞭,被Runtime 所管理遭垛。
2.3、main函數(shù)
接下來就是UIApplicationMain函數(shù)操灿,AppDelegate的application:didFinishLaunchingWithOptions:方法
3锯仪、Dyld在各階段所做的事情:
二、影響main()之前的啟動加載時(shí)間的因素:
- 動態(tài)庫加載越多趾盐,啟動越慢庶喜。
- ObjC類小腊,方法越多,啟動越慢久窟。
- ObjC的+load越多秩冈,啟動越慢。
- C的constructor函數(shù)越多斥扛,啟動越慢入问。
- C++靜態(tài)對象越多,啟動越慢稀颁。
三芬失、APP的啟動優(yōu)化
按照不同的階段
1、Dyld
- 減少動態(tài)庫匾灶、合并一些動態(tài)庫(定期清理不必要的動態(tài)庫)
- 減少Objc類棱烂、分類的數(shù)量、減少Selector數(shù)量(定期清理不必要的類阶女、分類)
- 減少C++虛函數(shù)數(shù)量
- Swift盡量使用struct
2垢啼、runtime
- 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++靜態(tài)構(gòu)造器张肾、ObjC的+load
3、main
- 在不影響用戶體驗(yàn)的前提下锚扎,盡可能將一些操作延遲吞瞪,不要全部都放在finishLaunching方法中
- 按需加載
四、APP的啟動優(yōu)化:替換 load方法
目前iOS App中或多或少的都會寫一些+load方法驾孔,用于在App啟動執(zhí)行一些操作芍秆,+load方法在Initializers階段被執(zhí)行,但過多+load方法則會拖慢啟動速度翠勉,對于大中型的App更是如此妖啥。通過對App中+load的方法分析,發(fā)現(xiàn)很多代碼雖然需要在App啟動時(shí)較早的時(shí)機(jī)進(jìn)行初始化对碌,但并不需要在+load這樣非尘J靠前的位置,完全是可以延遲到App冷啟動后的某個(gè)時(shí)間節(jié)點(diǎn)朽们,例如一些路由操作怀读、webview的bridge方法的注冊。其實(shí)+load也可以被當(dāng)做一種啟動項(xiàng)來處理骑脱,所以在替換+load方法的具體實(shí)現(xiàn)上菜枷,我們?nèi)匀徊捎昧讼旅娣绞健?/p>
核心思想:
核心思想就是在編譯時(shí)把數(shù)據(jù)(如函數(shù)指針)寫入到可執(zhí)行文件的__DATA段中,運(yùn)行時(shí)再從__DATA段取出數(shù)據(jù)進(jìn)行相應(yīng)的操作(調(diào)用函數(shù))叁丧。
為什么要用借用__DATA段呢啤誊?原因就是為了能夠覆蓋所有的啟動階段岳瞭,例如main()之前的階段。
實(shí)現(xiàn)原理:
實(shí)現(xiàn)原理簡述:Clang 提供了很多的編譯器函數(shù)蚊锹,它們可以完成不同的功能瞳筏。其中一種就是 section() 函數(shù),section()函數(shù)提供了二進(jìn)制段的讀寫能力枫耳,它可以將一些編譯期就可以確定的常量寫入數(shù)據(jù)段乏矾。 在具體的實(shí)現(xiàn)中,主要分為編譯期和運(yùn)行時(shí)兩個(gè)部分迁杨。在編譯期钻心,編譯器會將標(biāo)記了 attribute((section())) 的數(shù)據(jù)寫到指定的數(shù)據(jù)段中,例如寫一個(gè){key(key代表不同的啟動階段), *pointer}對到數(shù)據(jù)段铅协。到運(yùn)行時(shí)捷沸,在合適的時(shí)間節(jié)點(diǎn),在根據(jù)key讀取出函數(shù)指針狐史,完成函數(shù)的調(diào)用痒给。
1、替換load方法來注冊bridge方法的具體實(shí)現(xiàn)
1.1骏全、webview browser注冊入口苍柏,在合適的時(shí)機(jī)進(jìn)行初始化
+ (void)initialize {
[HYPluginRegisterManager registerPlugins];
}
1.2、初始化相關(guān)代碼
#import "HYPluginRegisterManager.h"
#import <objc/runtime.h>
#import <objc/message.h>
#include <mach-o/getsect.h>
#include <mach-o/loader.h>
#include <mach-o/dyld.h>
#include <dlfcn.h>
static void PluginRegisterRun(const char * segmentName,const char *sectionName){
Dl_info info;
int ret = dladdr(PluginRegisterRun, &info);
if(ret == 0){
// fatal error
}
#ifndef __LP64__
const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
unsigned long size = 0;
uint32_t *memory = (uint32_t*)getsectiondata(mhp, segmentName, sectionName, & size);
#else /* defined(__LP64__) */
const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
unsigned long size = 0;
uint64_t *memory = (uint64_t*)getsectiondata(mhp, segmentName, sectionName, & size);
#endif /* defined(__LP64__) */
if(size == 0){
return;
}
for(int idx = 0; idx < size/sizeof(void*); ++idx){
PluginRegisterCallback func = (PluginRegisterCallback)memory[idx];
func();
}
}
@implementation HYPluginRegisterManager
+ (void)registerPlugins {
PluginRegisterRun(KPY_PluginRegister_SegmentName,KPY_PLUGIN_REGISTER_SECTIONNAME);
}
@end
1.3姜贡、聲明可以替換load方法的宏定義
#define KPY_PLUGIN_REGISTER_SECTIONNAME "__browser_plugin"
#define KPY_PluginRegister_SegmentName "__DATA"
#define KPY_PLUGINREGISTER_DATA __attribute((used, section(KPY_PluginRegister_SegmentName "," KPY_PLUGIN_REGISTER_SECTIONNAME )))
// 編譯保存Plugin
#define AppPluginRegister(pluginName) \
static void PluginRegister##pluginName();\
static PluginRegisterCallback varPluginRegister##pluginName KPY_PLUGINREGISTER_DATA = PluginRegister##pluginName;\
static void PluginRegister##pluginName
1.4试吁、webview bridge方法注冊使用
使用對應(yīng)的宏定義,替換對應(yīng)的load方法:
// 啟動速度優(yōu)化 +load替換
AppPluginRegister(BrowserOtherPlugin)() {
// 注冊bridge方法代碼
}
2楼咳、替換load方法來注冊路由的具體實(shí)現(xiàn)
2.1熄捍、App啟動后進(jìn)行初始化
static dispatch_once_t appLaunchOnces;
dispatch_once(&appLaunchOnces, ^{
[AppLaunchManager run];
});
2.2、初始化相關(guān)代碼
#import "AppLaunchManager.h"
#import "AppLaunchHeader.h"
#import <objc/runtime.h>
#import <objc/message.h>
#include <mach-o/getsect.h>
#include <mach-o/loader.h>
#include <mach-o/dyld.h>
#include <dlfcn.h>
static void AppLoadableRun(const char * segmentName,const char *sectionName){
Dl_info info;
int ret = dladdr(AppLoadableRun, &info);
if(ret == 0){
// fatal error
}
#ifndef __LP64__
const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
unsigned long size = 0;
uint32_t *memory = (uint32_t*)getsectiondata(mhp, segmentName, sectionName, & size);
#else /* defined(__LP64__) */
const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
unsigned long size = 0;
uint64_t *memory = (uint64_t*)getsectiondata(mhp, segmentName, sectionName, & size);
#endif /* defined(__LP64__) */
if(size == 0){
return;
}
for(int idx = 0; idx < size/sizeof(void*); ++idx){
AppLaunchFuncCallback func = (AppLaunchFuncCallback)memory[idx];
func();
}
}
@implementation AppLaunchManager
+ (void)run{
AppLoadableRun(KPY_SegmentName,KPY_FUNCTION_DATASectionName);
}
+ (void)runFuncWithSectionName:(char *)sectionName {
AppLoadableRun(KPY_SegmentName,sectionName);
}
@end
2.3母怜、聲明可以替換load方法的宏定義
#define KPY_STRING_DATASectionName "__pystrstore"
#define KPY_FUNCTION_DATASectionName "__pyfuncstore"
#define KPY_SegmentName "__DATA"
#define KPY_DATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define KPY_PYFUNCTION_DATA __attribute((used, section(KPY_SegmentName "," KPY_FUNCTION_DATASectionName )))
#define AppLaunchReLoadFunc(functionName) \
static void AppLaunch##functionName();\
static AppLaunchFuncCallback varQWLoadable##functionName KPY_PYFUNCTION_DATA = AppLaunch##functionName;\
static void AppLaunch##functionName
2.4余耽、vc中路由注冊使用
使用對應(yīng)的宏定義,替換對應(yīng)的load方法:
// 啟動速度優(yōu)化 +load替換
AppLaunchReLoadFunc(NewController)(){
// 注冊路由代碼
};
五苹熏、APP的啟動優(yōu)化:二進(jìn)制重排
1碟贾、原理:
假設(shè)在啟動時(shí)期我們需要調(diào)用兩個(gè)函數(shù) method1 與 method4,函數(shù)編譯在 mach-O 中的位置是根據(jù)ld ( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來的柜裸,因此很可能這兩個(gè)函數(shù)分布在不同的內(nèi)存頁上缕陕。
如上圖,那么啟動時(shí)疙挺,page1 與 page2 都需要從無到有加載到物理內(nèi)存中扛邑,從而觸發(fā)兩次 Page Fault。
2铐然、操作
二進(jìn)制重排 的做法就是將 method1 與 method4 放到一個(gè)內(nèi)存頁中蔬崩,那么啟動時(shí)則只需要加載一次 page 即可恶座,也就是只觸發(fā)一次 Page Fault。 在實(shí)際項(xiàng)目中沥阳,我們可以將啟動時(shí)需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少啟動耗時(shí)跨琳。
實(shí)際上 二進(jìn)制重排就是對即將生成的可執(zhí)行文件重新排列,即它發(fā)生在鏈接階段桐罕。 首先脉让,Xcode 用的鏈接器叫做 ld ,ld 有一個(gè)參數(shù)叫 Order File功炮,我們可以通過這個(gè)參數(shù)配置一個(gè) 后綴名 為order的文件路徑溅潜。在這個(gè) order 文件中,將你需要的符號按順序?qū)懺诶锩嫘椒.?dāng)工程 build 的時(shí)候滚澜,Xcode 會讀取這個(gè)文件,打的二進(jìn)制包就會按照這個(gè)文件中的符號順序進(jìn)行生成對應(yīng)的 mach-O嫁怀。
備注:Build Setting/All Combined/搜 order file 查看APP的二進(jìn)制重排文件
六设捐、APP啟動中的rebase和bind
Rebase和Bind。Rebase修復(fù)的是指向當(dāng)前鏡像內(nèi)部的資源指針塘淑;?Bind指向的是鏡像外部的資源指針
在dylib的加載過程中萝招,系統(tǒng)為了安全考慮,引了ASLR (Address Space Layout Randomization)技術(shù)和 代碼簽名存捺。由于ASLR的存在即寒,鏡像(Image,包括可執(zhí)件召噩、 dylib和bundle)會在隨機(jī)的地址上加載,和 之前指針指向的地址(preferred_address)會有個(gè)偏差(slide)逸爵, dyld需要修正這個(gè)偏差具滴,來指向正確的 地址。 Rebase在前师倔, Bind在后构韵, Rebase做的是將鏡像讀內(nèi)存,修正鏡像內(nèi)部的指針趋艘,性能消耗主要在 IO疲恢。 Bind做的是查詢符號表,設(shè)置指向鏡像外部的指針瓷胧,性能消耗主要在CPU計(jì)算显拳。
七、啟動過程中動態(tài)鏈接器階段搓萧,為什么合并動態(tài)庫能提高優(yōu)化時(shí)間杂数?
Dyld loading 階段宛畦,加載動態(tài)庫,這個(gè)階段會去裝載APP使用的動態(tài)庫揍移,而每一個(gè)動態(tài)庫有它自己的依賴關(guān)系次和,所以會消耗時(shí)間去查找和讀取。對于Apple提供的的系統(tǒng)動態(tài)庫那伐,做了高度的優(yōu)化踏施。而對于開發(fā)者定義導(dǎo)入的動態(tài)庫,則需要在花費(fèi)更多的時(shí)間罕邀。Apple官方建議盡量少的使用自定義的動態(tài)庫畅形,或者考慮合并多個(gè)動態(tài)庫,其中一個(gè)建議是當(dāng)大于6個(gè)的時(shí)候燃少,則需要考慮合并它們束亏。
八、靜態(tài)鏈接庫與動態(tài)鏈接庫
1阵具、介紹
靜態(tài)鏈接庫與動態(tài)鏈接庫都是共享代碼的方式碍遍,如果采用靜態(tài)鏈接庫,則無論你愿不愿意阳液,lib 中的指令都全部被直接包含在最終生成的包文件中了怕敬。但是若使用動態(tài)鏈接庫,該動態(tài)鏈接庫不必被包含在最終包里帘皿,包文件執(zhí)行時(shí)可以“動態(tài)”地引用和卸載這個(gè)與安裝包獨(dú)立的動態(tài)鏈接庫文件东跪。
2、區(qū)別
靜態(tài)鏈接庫和動態(tài)鏈接庫的一個(gè)區(qū)別在于靜態(tài)鏈接庫中不能再包含其他的動態(tài)鏈接庫或者靜態(tài)庫鹰溜,而在動態(tài)鏈接庫中還可以再包含其他的動態(tài)或靜態(tài)鏈接庫虽填。
iOS開發(fā)中靜態(tài)庫和動態(tài)庫是相對編譯期和運(yùn)行期的。靜態(tài)庫在程序編譯時(shí)會被鏈接到目標(biāo)代碼中曹动,程序運(yùn)行時(shí)將不再需要載入靜態(tài)庫斋日。而動態(tài)庫在程序編譯時(shí)并不會被鏈接到目標(biāo)代碼中,只是在程序運(yùn)行時(shí)才被載入墓陈,因?yàn)樵诔绦蜻\(yùn)行期間還需要動態(tài)庫的存在恶守。
iOS中靜態(tài)庫可以用.a或.Framework文件表示,動態(tài)庫的形式有.dylib和.framework贡必。系統(tǒng)的.framework是動態(tài)庫兔港,一般自己建立的.framework是靜態(tài)庫。.a是一個(gè)純二進(jìn)制文件仔拟,.framework中除了有二進(jìn)制文件之外還有資源文件衫樊。.a文件不能直接使用,至少要有.h文件配合利花。.framework文件可以直接使用橡伞,.a + .h + sourceFile = .framework盒揉。
以上是有關(guān)APP啟動的介紹,歡迎補(bǔ)充和指正兑徘。
參考:
iOS App 啟動優(yōu)化
ios啟動優(yōu)化:二進(jìn)制重排
iOS App冷啟動治理:來自美團(tuán)外賣的實(shí)踐