iOS app 启动时间优化分析

程序和进程

广义上的程序就是一个静态的可执行文件,是由一个已经编译好的指令和数据集合的一个文件。就像是我们通过Xcode编译好的macho文件。而进程则是一个动态的概念,是程序的运行时的一个过程。

虚拟地址空间

每个进程运行的时候都有自己独立的虚拟地址空间,这个空间的大小是由计算机的硬件决定的,比如在32位硬件平台上,它的寻址空间大小是2^32 - 1,现在iPhone都是64位的,寻址空间为2^64-1 。

冷启动和热启动

热启动是由于某种原因,APP的状态由running切换为suspend,但是此时APP并没有被系统kill掉,当我们再次把APP切换到前台的时候,APP会恢复之前的状态继续运行,这种就是热启动,我们平时所说的APP在后台的存活时间,其实就是APP能执行热启动的最大时间间隔。而冷启动则是APP从被加载到内存到运行的状态,下面我们要讲的主要是冷启动。

孤独的main函数

大概是从我们学习编程开始就知道main函数是程序的入口,但是真的是这样吗?在平时的面试过程中我也有问一些面试者这个问题,但是回答的都比较模糊。其实我们通过代码可以看出,在iOS里面 main只是简单的返回一个UIApplicationMain对象,里面的有一个重要的参数就是实现了UIApplicationDelegate代理的类。

// UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nonnull * _Null_unspecified argv, NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);

int main(int argc, char *argv[])
{
  @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([UIAppDelegate class]));
  }
}

APP启动流程时间主要包括两部分,main函数之前和main函数执行之后到-(BOOL)Application:(UIApplication *)Application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions方法执行完成。其中main函数执行之后优化主要是让上面的方法尽快执行完,不要有什么block主线程的操作。所以我们可以看出,其实在main里面处理的事情还是比较简单的。最重要的还是在main函数执行之前。

概述

从WWDC的视频我们可以得出简单的结论:系统先读取App的可执行文件,从里面获得dyld的路径,然后加载dyld,当所有依赖库的初始化后,轮到最后一位(程序可执行文件)进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回main函数地址,main函数被调用,我们便来到了熟悉的程序入口。

启动时间

在Xcode中可以通过设置DYLD_PRINT_STATISTICS环境变量来查看APP的启动时间详细信息:

statistics.png

然后就可以在控制台看到如下信息:

Total pre-main time: 282.69 milliseconds (100.0%)
         dylib loading time: 107.37 milliseconds (37.9%)
        rebase/binding time:  44.92 milliseconds (15.8%)
            ObjC setup time:  64.72 milliseconds (22.8%)
           initializer time:  65.56 milliseconds (23.1%)
           slowest intializers :
               libSystem.dylib :   7.98 milliseconds (2.8%)
    libMainThreadChecker.dylib :  23.55 milliseconds (8.3%)
                  AFNetworking :  19.46 milliseconds (6.8%)

从上面可以看出时间区域主要分为下面几个部分:

  • dylib loading time
  • rebase/binding time
  • ObjC setup time
  • initializer time

dyld

(the dynamic link editor)动态链接器,是一个专门用来加载动态链接库的库,它是开源的,源码在这里。在 xnu 内核为程序启动做好准备后,执行由内核态切换到用户态,由dyld完成后面的加载工作,dyld的主要是初始化运行环境,开启缓存策略,加载程序依赖的动态库(其中也包含我们的可执行文件),并对这些库进行链接(主要是rebaseing和binding),最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。

obj_init.png

ImageLoader是用于加载可执行文件格式的类,程序中对应实例可简称为image(如程序可执行文件macho,Framework,bundle等)。

Rebasing 和 Binding

ASLR(Address Space Layout Randomization),地址空间布局随机化。在ASLR技术出现之前,程序都是在固定的地址加载的,这样hacker可以知道程序里面某个函数的具体地址,植入某些恶意代码,修改函数的地址等,带来了很多的危险性。ASLR就是为了解决这个的,程序每次启动后地址都会随机变化,这样程序里所有的代码地址都需要需要重新对进行计算修复才能正常访问。rebasing这一步主要就是调整镜像内部指针的指向。

Binding:将指针指向镜像外部的内容。

ObjC setup

上面最后一步调用的objc_init方法,这个事runtime的初始化方法,在这个方法里面主要的操作就是加载类:

/***********************************************************************
* _objc_init
* Bootstrap initialization. Registers our image notifier with dyld.
* Called by libSystem BEFORE library initialization time
**********************************************************************/

void _objc_init(void)
{
    static bool initialized = false;
    if (initialized) return;
    initialized = true;
    
    // fixme defer initialization until an objc-using image is found?
    environ_init();
    tls_init();
    static_init();
    lock_init();
    exception_init();

    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

_dyld_objc_notify_register(&map_images, load_images, unmap_image);向dyld注册了一个通知事件,当有新的image加载到内存的时候,就会触发load_images方法,这个方法里面就是加载对应image里面的类,并调用load方法。

load_images(const char *path __unused, const struct mach_header *mh)
{
    // Return without taking locks if there are no +load methods here.
    if (!hasLoadMethods((const headerType *)mh)) return;

    recursive_mutex_locker_t lock(loadMethodLock);

    // Discover load methods
    {
        rwlock_writer_t lock2(runtimeLock);
        prepare_load_methods((const headerType *)mh);
    }

    // Call +load methods (without runtimeLock - re-entrant)
    call_load_methods();
}

/***********************************************************************
* call_load_methods
* Call all pending class and category +load methods.
* Class +load methods are called superclass-first. 
* Category +load methods are not called until after the parent class's +load.
* 
* This method must be RE-ENTRANT, because a +load could trigger 
* more image mapping. In addition, the superclass-first ordering 
* must be preserved in the face of re-entrant calls. Therefore, 
* only the OUTERMOST call of this function will do anything, and 
* that call will handle all loadable classes, even those generated 
* while it was running.
*
* The sequence below preserves +load ordering in the face of 
* image loading during a +load, and make sure that no 
* +load method is forgotten because it was added during 
* a +load call.
* Sequence:
* 1. Repeatedly call class +loads until there aren't any more
* 2. Call category +loads ONCE.
* 3. Run more +loads if:
*    (a) there are more classes to load, OR
*    (b) there are some potential category +loads that have 
*        still never been attempted.
* Category +loads are only run once to ensure "parent class first" 
* ordering, even if a category +load triggers a new loadable class 
* and a new loadable category attached to that class. 
*
* Locking: loadMethodLock must be held by the caller 
*   All other locks must not be held.
**********************************************************************/
void call_load_methods(void)
{
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. Repeatedly call class +loads until there aren't any more
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. Call category +loads ONCE
        more_categories = call_category_loads();

        // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0  ||  more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
}

如果有继承的类,那么会先调用父类的load方法,然后调用子类的,但是在load里面不能调用[super load]。最后才是调用category的load方法。所以在这一步,所有的load都会被调用到。

C++ initializer

在这一步,如果我们代码里面使用了clang的__attribute__((constructor))构造方法,都会调用到。

优化点

那么如何尽可能的减少pre-main花费的时间呢,主要就从上面给出的几个阶段下手:

  • 动态库加载的时间优化。每个App都进行动态库加载,其中系统级别的动态库占据了绝大数,而针对系统级别的动态库都是经过系统高度优化的,不用担心时间的花费。开发者应该关注于自己集成到App的那些动态库,这也是最能消耗加载时间的地方。对此Apple建议减少在App里开发者的动态库集成或者有可能地将其多个动态库最终集成一个动态库后进行导入, 尽量保证将App现有的非系统级的动态库个数保证在6个以内;

  • (Rebase/binding)时间优化。减少App的Objective-C类,分类和Selector的个数。这样做主要是为了加快程序的整个动态链接, 在进行动态库的重定位和绑定(Rebase/binding)过程中减少指针修正的使用,加快程序机器码的生成;

  • objc init 优化。用+initialize方法替换+load方法,从而加快所有类文件的加载速度。

refrence

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

推荐阅读更多精彩内容

  • 应用启动时间,直接影响用户对一款应用的判断和使用体验。头条主app本身就包含非常多并且复杂度高的业务模块(如新闻、...
    hgl阅读 426评论 0 0
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,544评论 25 707
  • 丁酉鸡年的第一天,2016年的第四周,大年初一,电视里正在演着北京春晚吧,外面的鞭炮声也不多了,今年貌似鞭炮声没有...
    赵自律阅读 222评论 0 0
  • 最近看书进度很慢,《出走》看完后,这周都是断断续续的看一些书,因为即将成为母亲的原因,所以开始关注一些教育类书籍,...
    Lylian_啦啦啦阅读 284评论 0 0
  • 上周六加班了。这还是第一次周末上班,偌大的办公区塞满了格子间,窗户敞亮着,没有开灯,这样我正好喜欢,因为电脑屏幕不...
    WaiWaii阅读 195评论 0 0