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

前言:本文旨在介紹iOS性能優(yōu)化中有關(guān)APP啟動(dòng)流程的介紹和優(yōu)化。

一、APP啟動(dòng)流程

1、APP的冷啟動(dòng)流程

  • 點(diǎn)擊圖標(biāo)之后,系統(tǒng)加載APP可執(zhí)行文件
  • 啟動(dòng)Dyld(動(dòng)態(tài)加載器) ,然后Dyld遞歸加載程序所需的動(dòng)態(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的冷啟動(dòng)流程的3大階段

APP的冷啟動(dòng)可以概括為3大階段:Dyld ---> Runtime ---> main
Dyld(dynamic link editor):Apple的動(dòng)態(tài)鏈接器,可以用來裝載Mach-O文件(可執(zhí)行文件、動(dòng)態(tài)庫等)

2.1、啟動(dòng)APP時(shí),Dyld所做的事情有:
  • 系統(tǒng)裝載APP的可執(zhí)行文件后,啟動(dòng)Dyld,之后Dyld會遞歸加載所有依賴的動(dòng)態(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í)行文件和動(dòng)態(tài)庫中所有的符號(Class,Protocol,Selector,IMP,…)都已經(jīng)按格式成功加載到內(nèi)存中,被Runtime 所管理。
2.3、main函數(shù)

接下來就是UIApplicationMain函數(shù),AppDelegate的application:didFinishLaunchingWithOptions:方法

3、Dyld在各階段所做的事情:

二、影響main()之前的啟動(dòng)加載時(shí)間的因素:

  • 動(dòng)態(tài)庫加載越多,啟動(dòng)越慢。
  • ObjC類,方法越多,啟動(dòng)越慢。
  • ObjC的+load越多,啟動(dòng)越慢。
  • C的constructor函數(shù)越多,啟動(dòng)越慢。
  • C++靜態(tài)對象越多,啟動(dòng)越慢。

三、APP的啟動(dòng)優(yōu)化

按照不同的階段

1、Dyld
  • 減少動(dòng)態(tài)庫、合并一些動(dòng)態(tài)庫(定期清理不必要的動(dòng)態(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的啟動(dòng)優(yōu)化:替換 load方法

目前iOS App中或多或少的都會寫一些+load方法,用于在App啟動(dòng)執(zhí)行一些操作,+load方法在Initializers階段被執(zhí)行,但過多+load方法則會拖慢啟動(dòng)速度,對于大中型的App更是如此。通過對App中+load的方法分析,發(fā)現(xiàn)很多代碼雖然需要在App啟動(dòng)時(shí)較早的時(shí)機(jī)進(jìn)行初始化,但并不需要在+load這樣非常靠前的位置,完全是可以延遲到App冷啟動(dòng)后的某個(gè)時(shí)間節(jié)點(diǎn),例如一些路由操作、webview的bridge方法的注冊。其實(shí)+load也可以被當(dāng)做一種啟動(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段呢?原因就是為了能夠覆蓋所有的啟動(dòng)階段,例如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代表不同的啟動(dòng)階段), *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方法:

// 啟動(dòng)速度優(yōu)化 +load替換
AppPluginRegister(BrowserOtherPlugin)() {
    // 注冊bridge方法代碼
}

2、替換load方法來注冊路由的具體實(shí)現(xiàn)

2.1、App啟動(dòng)后進(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方法:

// 啟動(dòng)速度優(yōu)化 +load替換
AppLaunchReLoadFunc(NewController)(){
    // 注冊路由代碼
};

五、APP的啟動(dòng)優(yōu)化:二進(jìn)制重排

1、原理:

假設(shè)在啟動(dòng)時(shí)期我們需要調(diào)用兩個(gè)函數(shù) method1 與 method4,函數(shù)編譯在 mach-O 中的位置是根據(jù)ld ( Xcode 的鏈接器) 的編譯順序并非調(diào)用順序來的,因此很可能這兩個(gè)函數(shù)分布在不同的內(nèi)存頁上。

如上圖,那么啟動(dòng)時(shí),page1 與 page2 都需要從無到有加載到物理內(nèi)存中,從而觸發(fā)兩次 Page Fault。

2、操作

二進(jìn)制重排 的做法就是將 method1 與 method4 放到一個(gè)內(nèi)存頁中,那么啟動(dòng)時(shí)則只需要加載一次 page 即可,也就是只觸發(fā)一次 Page Fault。 在實(shí)際項(xiàng)目中,我們可以將啟動(dòng)時(shí)需要調(diào)用的函數(shù)放到一起 ( 比如 前10頁中 ) 以盡可能減少啟動(dòng)耗時(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啟動(dòng)中的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ì)算。

七、啟動(dòng)過程中動(dòng)態(tài)鏈接器階段,為什么合并動(dòng)態(tài)庫能提高優(yōu)化時(shí)間?

Dyld loading 階段,加載動(dòng)態(tài)庫,這個(gè)階段會去裝載APP使用的動(dòng)態(tài)庫,而每一個(gè)動(dòng)態(tài)庫有它自己的依賴關(guān)系,所以會消耗時(shí)間去查找和讀取。對于Apple提供的的系統(tǒng)動(dòng)態(tài)庫,做了高度的優(yōu)化。而對于開發(fā)者定義導(dǎo)入的動(dòng)態(tài)庫,則需要在花費(fèi)更多的時(shí)間。Apple官方建議盡量少的使用自定義的動(dòng)態(tài)庫,或者考慮合并多個(gè)動(dòng)態(tài)庫,其中一個(gè)建議是當(dāng)大于6個(gè)的時(shí)候,則需要考慮合并它們。

八、靜態(tài)鏈接庫與動(dòng)態(tài)鏈接庫

1、介紹

靜態(tài)鏈接庫與動(dòng)態(tài)鏈接庫都是共享代碼的方式,如果采用靜態(tài)鏈接庫,則無論你愿不愿意,lib 中的指令都全部被直接包含在最終生成的包文件中了。但是若使用動(dòng)態(tài)鏈接庫,該動(dòng)態(tài)鏈接庫不必被包含在最終包里,包文件執(zhí)行時(shí)可以“動(dòng)態(tài)”地引用和卸載這個(gè)與安裝包獨(dú)立的動(dòng)態(tài)鏈接庫文件。

2、區(qū)別
  • 靜態(tài)鏈接庫和動(dòng)態(tài)鏈接庫的一個(gè)區(qū)別在于靜態(tài)鏈接庫中不能再包含其他的動(dòng)態(tài)鏈接庫或者靜態(tài)庫,而在動(dòng)態(tài)鏈接庫中還可以再包含其他的動(dòng)態(tài)或靜態(tài)鏈接庫。

  • iOS開發(fā)中靜態(tài)庫和動(dòng)態(tài)庫是相對編譯期和運(yùn)行期的。靜態(tài)庫在程序編譯時(shí)會被鏈接到目標(biāo)代碼中,程序運(yùn)行時(shí)將不再需要載入靜態(tài)庫。而動(dòng)態(tài)庫在程序編譯時(shí)并不會被鏈接到目標(biāo)代碼中,只是在程序運(yùn)行時(shí)才被載入,因?yàn)樵诔绦蜻\(yùn)行期間還需要?jiǎng)討B(tài)庫的存在。

  • iOS中靜態(tài)庫可以用.a或.Framework文件表示,動(dòng)態(tài)庫的形式有.dylib和.framework。系統(tǒng)的.framework是動(dòng)態(tài)庫,一般自己建立的.framework是靜態(tài)庫。.a是一個(gè)純二進(jìn)制文件,.framework中除了有二進(jìn)制文件之外還有資源文件。.a文件不能直接使用,至少要有.h文件配合。.framework文件可以直接使用,.a + .h + sourceFile = .framework。


以上是有關(guān)APP啟動(dòng)的介紹,歡迎補(bǔ)充和指正。

參考:
iOS App 啟動(dòng)優(yōu)化
ios啟動(dòng)優(yōu)化:二進(jìn)制重排
iOS App冷啟動(dòng)治理:來自美團(tuán)外賣的實(shí)踐

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

相關(guān)閱讀更多精彩內(nèi)容

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