iOS性能優(yōu)化-APP啟動

前言:本文旨在介紹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í)踐

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刚盈,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子挂脑,更是在濱河造成了極大的恐慌藕漱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件崭闲,死亡現(xiàn)場離奇詭異肋联,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)刁俭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門橄仍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牍戚,你說我怎么就攤上這事侮繁。” “怎么了如孝?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵宪哩,是天一觀的道長。 經(jīng)常有香客問我第晰,道長锁孟,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任茁瘦,我火速辦了婚禮品抽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘甜熔。我一直安慰自己桑包,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布纺非。 她就那樣靜靜地躺著,像睡著了一般赘方。 火紅的嫁衣襯著肌膚如雪烧颖。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天窄陡,我揣著相機(jī)與錄音炕淮,去河邊找鬼。 笑死跳夭,一個(gè)胖子當(dāng)著我的面吹牛涂圆,可吹牛的內(nèi)容都是我干的们镜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼润歉,長吁一口氣:“原來是場噩夢啊……” “哼模狭!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起踩衩,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤嚼鹉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后驱富,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锚赤,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年褐鸥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了线脚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡叫榕,死狀恐怖浑侥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情翠霍,我是刑警寧澤锭吨,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站寒匙,受9級特大地震影響零如,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜锄弱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一考蕾、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧会宪,春花似錦肖卧、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至巍沙,卻和暖如春葵姥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背句携。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工榔幸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓削咆,卻偏偏與公主長得像牍疏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子拨齐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內(nèi)容