iOS 冷启动

iOS 冷启动分为几个阶段,每个阶段的的过程是什么

冷启动的过程定义为:从用户点击 App 图标开始到 appDelegate didFinishLaunching 方法执行完成为止。然后 di dFinishLaunchingWithOptions 执行完成时,用户还没看到 App 的主界面,也不能使用 App,此时 App 还需要做一些初始化工作,然后完成首页请求、首页渲染等过程,用户才能真正看到数据内容并开始使用,这个时候冷启动才算完成。因此主要分为三个阶段:

  • Premain T1: main() 函数之前,即操作系统加载 App 可执行文件到内存,然后执行一系列的加载、链接工作,最后执行至 App 的 main() 函数
  • Aftermain T2:main() 函数之后,即从 main() 开始,到 appDelegate didFinishLaunching 方法执行完毕
  • 到用户看到主界面 T3
    db061a267f6dc2b14ff7f9120020a5c261253.png
Premain 阶段 T1

main() 之前操作系统所做的工作是把可执行文件( Mach-O 格式)加载到内存空间,然后加载动态链接器(dyld) ,再执行一系列动态链接操作和初始化操作的过程(加载、绑定、初始化方法)。加载过程--从 exec() 到 main()

真正的加载过程从 exec() 函数开始,exec() 是一个系统调用。操作系统首先为进程分配一段内存空间,然后执行如下操作:

  1. 把 App 对应的可执行文件( Mach-O 格式)加载到内存
  2. 把 Dyld 加载到内存
  3. Dyld 进行动态链接

dyld 在各个阶段所做的事情如下

阶段 工作
加载动态库 dyld 从主执行文件的 header 获取到需要加载的所依赖的动态库列表,然后找到每个 dylib,而 dylib 也可能依赖其他 dylib,所以这是一个递归加载依赖的过程
Rebase 和 Bind 1. Rebase 在 Image 内部调整指针的指向。由于地址空间布局是随机的,需要在原来地址的基础上根据随机的偏移量做一下修正 2. Bind 把指针正确的指向 Image 外部的内容。这些指向外部的指针被符号绑定,dyld 需要去符号表里查找,找到 symbol 对应的实现
Objc setup 1. 注册 Objc 类 2. 把 category 的定义插入方法列表 3. 保证每个 selector 唯一
Initializers 1. Objc 的 +load 函数 2. C++ 构造函数属性函数 3. 非基本类型的 C++ 静态全局变量的创建(通常是类或结构体)

备注:image 表示一个二进制文件(可执行文件或 so 文件),里面是被编译过的符号、代码等,所以 ImageLoader 作用是将这些文件加载进内存,并且每一个文件对应一个 ImageLoader 实例来负责加载

整个事件由 dyld 主导,完成运行环境的初始化后,配合 imageLoader 将二进制文件按格式加载到内存,动态链接依赖库,并由 runtime 负责加载成 objc 定义的结构,所有初始化工作结束后,dyld 调用真正的 main 函数。因此影响 T1 时间的因素:

  1. 动态库越多,启动越慢
  2. Objc 类、方法越多,启动越慢
  3. Objc 的 +load 越多,启动越慢
代码瘦身

因此随着业务的迭代,要及时将废弃无用的代码和资源文件清理掉。Match-O 文件中,___TEXT:__objc_methname: 中包含了代码中的所有方法,而 __DATA__objc_selrefs 中包含了所有被使用的方法的引用,通过两个集合的差集就可以得到所有未被使用的代码(参考自 objc_cover),具体如下:

def referenced_selectors(path):
    re_sel = re.compile("__TEXT:__objc_methname:(.+)") //获取所有方法
    refs = set()
    lines = os.popen("/usr/bin/otool -v -s __DATA __objc_selrefs %s" % path).readlines() ## ios & mac //真正被使用的方法
    for line in lines:
        results = re_sel.findall(line)
        if results:
            refs.add(results[0])
    return refs
}
+ load 优化

过多的 +load 方法会拖慢启动速度。具体优化方法

  1. 如果可能的话,将 +load 方法中的内容放到渲染完成后做

  2. 使用 +initialize 方法代替 +load ,注意把逻辑移动到 +initialize 时,要注意避免 +initialize 重复调用问题,可以使用 dispatch_once 让逻辑只执行一次

    备注:+load+initialize 的区别

    +load 方法会在 main() 函数之前调用,而 +initialize 是在类第一次使用时才会调用

    +load 方法调用优先级:父类>子类>分类,并且不会被覆盖,均会调用

    +initialize 调用优先级:分类>父类,父类>子类,父类的分类重写了 +initialize 方法会覆盖父类的 +initialize 方法。即:1. 如果分类和父类均实现了 +initialize,则只有分类的 +initialize 会被调用;2. 如果父类和子类均实现了 +initialize,第一次引用 子类时,先调用父类的 +initialize,再调用子类的 +initialize;3. 如果父类实现了 +initialize,则第一次引用子类时,会调用两次父类的 +initialize

    +load 方法在 main() 函数之前调用,所有的类文件都会加载,分类也会加载

  3. 合并多个动态库

    苹果建议使用更少的动态库

  4. 优化类、方法、全局变量

after main 优化
  1. 优化首屏渲染前的功能初始化

    main 函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。首屏渲染外的其他功能放到首屏渲染完成后去初始化

  2. 优化主线程耗时操作,防止屏幕卡顿

    将耗时操作滞后、异步处理。通常的耗时操作有网络加载、编辑、存储图片和文件资源

优化耗时操作

可以使用 Xcode 自带的 Time Profiler 时间性能分析工具,可以参考 Instruments Tutorial with Swift: Getting Started

冷启动开始、结束时间点
  • 结束时间点:结束时间点比较好确定,可以将首页某些视图元素的展示作为首页加载完成的标志

  • 开始时间点:

    1. 以可执行文件中任意一个类的 +load 方法的执行时间为起始点
    2. 可以以 App 的进程创建时间即(exec 函数执行时间)作为冷启动的起始时间。因为系统允许我们通过 sysctl 函数获得进程的有关信息,其中就包括进程创建的时间戳
    #import <sys/sysctl.h>
    #import <mach/mach.h>
    
    + (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo
    {
        int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
        size_t size = sizeof(*procInfo);
        return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
    }
    
    + (NSTimeInterval)processStartTime
    {
        struct kinfo_proc kProcInfo;
        if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
            return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0;
        } else {
            NSAssert(NO, @"无法取得进程的信息");
            return 0;
        }
    }
    
关于 +load 方法的几个 QA
  1. 重载自己 class 的 +load 方法时需不需要调用父类?

    runtime 负责按继承顺序递归调用,所以我们不能调用 super

  2. 在自己 Class 的 +load 方法时能不能替换 framework 中的某个类方法的实现?

    可以,因为动态链接过程中,所有依赖库加载完毕后,才会注册 Objc 类

  3. 想让一个类的 +load 方法被调用是否需要在某个地方 import 这额文件?

    不需要,只要这个类的符号被编译到最后的可执行文件中,+load 方法就会被调用

Match-O 文件

对于 OSX 和 iOS 来说,Match-O 是可执行文件的格式,主要包括以下几种类型

  • Executable: 应用的主要二进制文件
  • Dylib:动态链接库
  • Bundle:不能被链接,只能在运行时使用 dlopen 加载
  • Image:包含 Execuable、Dylib、Bundle
  • Framework: 包含Dylib、资源文件、头文件

Match-O 文件都包含 3 个段:

  • __TEXT: 包含 Match header,被执行的代码和只读常量(如 C 字符串),只读可执行
  • __DATA: 包含全局变量、静态变量,可读可写
  • __LINKEDIT: 包含加载程序的元数据,如函数的名称、地址,只读

iOS Runtime 是什么

Objective-C 扩展了 C 语言,并加入了面向对象特性和消息传递机制,而这个扩展的核心是一个用 C 和编译语言写的 Runtime 库。 Runtime 库使我们可以在程序运行时动态的创建对象、检查对象、修改类和对象的方法。

参考

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