iOS性能优化-APP启动

前言:本文旨在介绍iOS性能优化中有关APP启动流程的介绍和优化。

一、APP启动流程

1、APP的冷启动流程

  • 点击图标之后,系统加载APP可执行文件
  • 启动Dyld(动态加载器) ,然后Dyld递归加载程序所需的动态库
  • Dyld 对程序进行 rebase 以及 bind 操作
  • Runtime加载类和分类的load方法
  • 进行各种Objc结构的初始化(注册Objc类 、初始化类对象等等)
  • 调用C++静态初始化器和attribute((constructor))修饰的函数。
  • 执行程序的 main 函数、AppDelegate的application:didFinishLaunchingWithOptions:方法

2、APP的冷启动流程的3大阶段

APP的冷启动可以概括为3大阶段:Dyld ---> Runtime ---> main
Dyld(dynamic link editor):Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

2.1、启动APP时,Dyld所做的事情有:
  • 系统装载APP的可执行文件后,启动Dyld,之后Dyld会递归加载所有依赖的动态库;
  • 然后Dyld 对程序进行 rebase 以及 bind 操作
  • 会通知Runtime进行下一步的处理。
2.2、Runtime所做的事情有:
  • 调用map_images进行可执行文件内容的解析和处理;
  • 在load_images中调用call_load_methods,调用所有Class和Category的+load方法;
  • 进行各种Objc结构的初始化(注册Objc类 、初始化类对象等等);
  • 调用C++静态初始化器和attribute((constructor))修饰的函数。
  • 到此为止,可执行文件和动态库中所有的符号(Class,Protocol,Selector,IMP,…)都已经按格式成功加载到内存中,被Runtime 所管理。
2.3、main函数

接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

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

二、影响main()之前的启动加载时间的因素:

  • 动态库加载越多,启动越慢。
  • ObjC类,方法越多,启动越慢。
  • ObjC的+load越多,启动越慢。
  • C的constructor函数越多,启动越慢。
  • C++静态对象越多,启动越慢。

三、APP的启动优化

按照不同的阶段

1、Dyld
  • 减少动态库、合并一些动态库(定期清理不必要的动态库)
  • 减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
  • 减少C++虚函数数量
  • Swift尽量使用struct
2、runtime
  • 用+initialize方法和dispatch_once取代所有的attribute((constructor))、C++静态构造器、ObjC的+load
3、main
  • 在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishLaunching方法中
  • 按需加载

四、APP的启动优化:替换 load方法

目前iOS App中或多或少的都会写一些+load方法,用于在App启动执行一些操作,+load方法在Initializers阶段被执行,但过多+load方法则会拖慢启动速度,对于大中型的App更是如此。通过对App中+load的方法分析,发现很多代码虽然需要在App启动时较早的时机进行初始化,但并不需要在+load这样非常靠前的位置,完全是可以延迟到App冷启动后的某个时间节点,例如一些路由操作、webview的bridge方法的注册。其实+load也可以被当做一种启动项来处理,所以在替换+load方法的具体实现上,我们仍然采用了下面方式。

核心思想:

核心思想就是在编译时把数据(如函数指针)写入到可执行文件的__DATA段中,运行时再从__DATA段取出数据进行相应的操作(调用函数)。
为什么要用借用__DATA段呢?原因就是为了能够覆盖所有的启动阶段,例如main()之前的阶段。

实现原理:

实现原理简述:Clang 提供了很多的编译器函数,它们可以完成不同的功能。其中一种就是 section() 函数,section()函数提供了二进制段的读写能力,它可以将一些编译期就可以确定的常量写入数据段。 在具体的实现中,主要分为编译期和运行时两个部分。在编译期,编译器会将标记了 attribute((section())) 的数据写到指定的数据段中,例如写一个{key(key代表不同的启动阶段), *pointer}对到数据段。到运行时,在合适的时间节点,在根据key读取出函数指针,完成函数的调用。

1、替换load方法来注册bridge方法的具体实现

1.1、webview browser注册入口,在合适的时机进行初始化
+ (void)initialize {
    [HYPluginRegisterManager registerPlugins];
}
1.2、初始化相关代码
#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方法注册使用

使用对应的宏定义,替换对应的load方法:

// 启动速度优化 +load替换
AppPluginRegister(BrowserOtherPlugin)() {
    // 注册bridge方法代码
}

2、替换load方法来注册路由的具体实现

2.1、App启动后进行初始化
    static dispatch_once_t appLaunchOnces;
    dispatch_once(&appLaunchOnces, ^{
        [AppLaunchManager run];
    });
2.2、初始化相关代码
#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中路由注册使用

使用对应的宏定义,替换对应的load方法:

// 启动速度优化 +load替换
AppLaunchReLoadFunc(NewController)(){
    // 注册路由代码
};

五、APP的启动优化:二进制重排

1、原理:

假设在启动时期我们需要调用两个函数 method1 与 method4,函数编译在 mach-O 中的位置是根据ld ( Xcode 的链接器) 的编译顺序并非调用顺序来的,因此很可能这两个函数分布在不同的内存页上。

如上图,那么启动时,page1 与 page2 都需要从无到有加载到物理内存中,从而触发两次 Page Fault。

2、操作

二进制重排 的做法就是将 method1 与 method4 放到一个内存页中,那么启动时则只需要加载一次 page 即可,也就是只触发一次 Page Fault。 在实际项目中,我们可以将启动时需要调用的函数放到一起 ( 比如 前10页中 ) 以尽可能减少启动耗时。

实际上 二进制重排就是对即将生成的可执行文件重新排列,即它发生在链接阶段。 首先,Xcode 用的链接器叫做 ld ,ld 有一个参数叫 Order File,我们可以通过这个参数配置一个 后缀名 为order的文件路径。在这个 order 文件中,将你需要的符号按顺序写在里面。当工程 build 的时候,Xcode 会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O。

备注:Build Setting/All Combined/搜 order file 查看APP的二进制重排文件

六、APP启动中的rebase和bind

  • Rebase和Bind。Rebase修复的是指向当前镜像内部的资源指针;⽽Bind指向的是镜像外部的资源指针

  • 在dylib的加载过程中,系统为了安全考虑,引了ASLR (Address Space Layout Randomization)技术和 代码签名。由于ASLR的存在,镜像(Image,包括可执件、 dylib和bundle)会在随机的地址上加载,和 之前指针指向的地址(preferred_address)会有个偏差(slide), dyld需要修正这个偏差,来指向正确的 地址。 Rebase在前, Bind在后, Rebase做的是将镜像读内存,修正镜像内部的指针,性能消耗主要在 IO。 Bind做的是查询符号表,设置指向镜像外部的指针,性能消耗主要在CPU计算。

七、启动过程中动态链接器阶段,为什么合并动态库能提高优化时间?

Dyld loading 阶段,加载动态库,这个阶段会去装载APP使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。

八、静态链接库与动态链接库

1、介绍

静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的包文件中了。但是若使用动态链接库,该动态链接库不必被包含在最终包里,包文件执行时可以“动态”地引用和卸载这个与安装包独立的动态链接库文件。

2、区别
  • 静态链接库和动态链接库的一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。

  • iOS开发中静态库和动态库是相对编译期和运行期的。静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要载入静态库。而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

  • iOS中静态库可以用.a或.Framework文件表示,动态库的形式有.dylib和.framework。系统的.framework是动态库,一般自己建立的.framework是静态库。.a是一个纯二进制文件,.framework中除了有二进制文件之外还有资源文件。.a文件不能直接使用,至少要有.h文件配合。.framework文件可以直接使用,.a + .h + sourceFile = .framework。


以上是有关APP启动的介绍,欢迎补充和指正。

参考:
iOS App 启动优化
ios启动优化:二进制重排
iOS App冷启动治理:来自美团外卖的实践

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容