起底 iOS 应用启动-Dyld篇

总览

  1. 利用已经被内核映射到内存中的可执行文件,instantiateFromLoadedImage生成 ImageLoader
  2. 将依赖库加载进内存,生成对应的 ImageLoader(loadInsertedDylib
  3. 链接可执行文件(link
  4. 链接依赖库(link
  5. 调用所有 Image 的初始化方法 Initializers,包括动态库和可执行文件,核心系统库、objc自举(initializeMainExecutable
  6. 返回程序入口函数 main 的地址(sMainExecutable->getMain()

dyld 源码地址

简化过的代码如下:

//
// Entry point for dyld.  The kernel loads dyld and jumps to __dyld_start which
// sets up some registers and call this function.
//
// Returns address of main() in target program which __dyld_start jumps to
//
uintptr_t
_main(const macho_header* mainExecutableMH, uintptr_t mainExecutableSlide, 
        int argc, const char* argv[], const char* envp[], const char* apple[], 
        uintptr_t* startGlue)
{
    ......

    // 1. instantiate ImageLoader for main executable
    sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);

    ......

    // 2. load any inserted libraries
    if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
        for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) {
            loadInsertedDylib(*lib);
        }
    }

    ......

    // 3. link main executable
    link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));

    ......

    // 4. link any inserted libraries
    // do this after linking main executable so that any dylibs pulled in by inserted
    // dylibs (e.g. libSystem) will not be in front of dylibs the program uses
    if ( sInsertedDylibCount > 0 ) {
        for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
            ImageLoader* image = sAllImages[i+1];
            link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL));
            image->setNeverUnloadRecursive();
        }
    }


    // 5. run all initializers
    initializeMainExecutable();

    // 6. main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
    result = (uintptr_t)sMainExecutable->getMain();

    return result;
}

可执行文件和依赖库的链接

void ImageLoader::link(const LinkContext& context, bool forceLazysBound, bool preflightOnly, bool neverUnload, const RPathChain& loaderRPaths)
{
    // 递归加载
    this->recursiveLoadLibraries(context, preflightOnly, loaderRPaths);
    // 递归Rebase 修复 ASLR 造成的地址错位的问题,增加一个偏移量
    // 主要是 IO 操作
    this->recursiveRebase(context);
    // 递归符号绑定,将指针指向 image 外部的内容。需要查询符号表,性能消耗主要是 CPU 计算
    this->recursiveBind(context, forceLazysBound, neverUnload);
}

Initializers

void initializeMainExecutable()
{
        ......
        
        // 首先执行动态库的 initialzers
        // run initialzers for any inserted dylibs
    ImageLoader::InitializerTimingList initializerTimes[sAllImages.size()];
    initializerTimes[0].count = 0;
    const size_t rootCount = sImageRoots.size();
    if ( rootCount > 1 ) {
        for(size_t i=1; i < rootCount; ++i) {
            sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
        }
    }

        // 随后执行可执行文件的 initialzers
    // run initializers for main executable and everything it brings up 
    sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

        // 进行终止化
        (*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);
        
        // 如果设置了环境变量 DYLD_PRINT_STATISTICS,可以在 Xcode debug 的时候在控制台打印
    // dump info if requested
    if ( sEnv.DYLD_PRINT_STATISTICS )
        ImageLoaderMachO::printStatistics((unsigned int)sAllImages.size(), initializerTimes[0]);
}

这里的 initialzers 注意不是 Objective-C 中的 initialzers 方法,而是 C++静态对象初始化构造器。

在 Xcode 中,可以通过设置环境变量 DYLD_PRINT_STATISTICS 打印所有 initialzers 方法。

设置打印

运行 App 后,可以看到控制台打印

控制台打印

通过打印可以发现,其中排第一个的是 libSystem.B.dylib,排在最后的是可执行文件的方法。

在 libSystem.B.dylib 的 initialzers 函数里的 libdispatch_init 调用到了 Runtime 初始化方法 _objc_init。

通过在 Xcode 中设置 _objc_init 符号断点,可以看到在控制台打印 dyld: calling initializer function 0x1ba93e7c0 in /usr/lib/libSystem.B.dylib 后, _objc_init 被调用了,不过调用栈将中间的调用过程全部隐去了。

objc 源码部分

查看 libdispatch源码,可以发现 objc 的踪迹。

libdispatch_init -> _os_object_init -> _objc_init

void
libdispatch_init(void)
{
    ......
    _os_object_init();
    .....
}

void
_os_object_init(void)
{
    _objc_init();
        ......
}

这下终于来到了熟悉的 objc4 的源码,这里我们重点关注在 dyld 中注册了的三个回调。
三个回调时机依次是 objc image 的 mapped、initialized、unmapped 三个阶段

void _objc_init(void)
{
    // so many init
    ......
    // 在 dyld 中注册了三个回调
    _dyld_objc_notify_register(&map_images, load_images, unmap_image);
}

map_images

当 dyld 完成映射(mapped)后,开始执行。其中核心函数是 _read_images
完成的工作主要是:

  1. 从库中对应的 segment 读取 class、protocol、category 信息,载入到 Runtime。
  2. 将 class、protocol、category 信息向 Runtime 注册结构
  3. 将 category 需要加到对应的 class 上。
  4. realized 类(重新确定布局,在 class_ro_t 基础上创建 class_rw_t)

其中需要说明的是 class 中 class_ro_t 为 unrealized class,class_rw_t 为 realized class。
如果想要使用 class,必须是 class_rw_t。class_ro_t 经过 resolve 后,会转化为 class_rw_t。non-lazy class 要求在初始化进行 realize。

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    // 注册 selector 到一个全局表中
    for(......) {
      SEL sel = sel_registerNameNoLock(name, isBundle);
    }
    
    // 获取 classes,并 realize
    classref_t const *classlist = _getObjc2ClassList(hi, &count);
    ......
    Class newCls = readClass(cls, headerIsBundle, headerIsPreoptimized);
  
    // protocol 
    protocol_t * const *protolist = _getObjc2ProtocolList(hi, &count);
    for (i = 0; i < count; i++) {
            // 注册结构
            readProtocol(protolist[i], cls, protocol_map, 
                         isPreoptimized, isBundle);
     }

    // categories
    for() {
        // 在 class 中注册 category,
        // 如果 class 已经 realize,重建 class
        attachCategories(cls, &lc, 1, ATTACH_EXISTING);
    }

    // Realize non-lazy classes (for +load methods and static instances)
    classref_t const *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
    for(......) {
        // 将类添加到 table 中
        addClassTableEntry(cls);
        realizeClassWithoutSwift(cls, nil);
    }

    // Realize newly-resolved future classes
    for(......) {
        realizeClassWithoutSwift(cls, nil);
    }
}

realizeClass 干的事情包括:

  1. 递归 Realize 父类和元类
  2. 重新设置 class、superclass、metaclass 之间的关系
  3. 重新计算 instance variable layout
  4. 将 class 中的编译时已经确定的 class_ro_t 的内容(method、property、protocol)放到 class_rw_t 结构体上,并增加 category 的部分
static Class realizeClass(Class cls)
{    
    // realize 父类和元类
    supercls = realizeClass(remapClass(cls->superclass));
    metacls = realizeClass(remapClass(cls->ISA()));

     // 重新设置关系
    cls->superclass = supercls;
    cls->initClassIsa(metacls);

    // 重新计算 layout
    // 如果 superclass 和原 class 空间重叠,需要对原 class 的实例重新计算位置并调整
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Connect this class to its superclass's subclass lists
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }

    // Attach categories
    methodizeClass(cls);
}

methodizeClass 函数干的事包括:

  1. 将 class 中的 class_ro_t 的内容(method、property、protocol)放到 class_rw_t 结构体上,
  2. 增加 category 的部分
static void methodizeClass(Class cls)
{
    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;
    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    // 方法增加
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
    property_list_t *proplist = ro->baseProperties;
    // 属性增加
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        // 协议增加
        rw->protocols.attachLists(&protolist, 1);
    }
    // Attach categories,将 method、property、protocol 插入
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}

load_images

是 dyld 的第二个注册回调,其核心是调用 load 方法。

  1. 查找并保存所有类,主类存在 loadable_classes 中,其排列顺序是父类在前、子类在后,分类存在 loadable_categories 中。
  2. 先调用 loadable_classes 中类的 load 方法,然后调用 loadable_categories 中分类的 load 方法
void
load_images(const char *path __unused, const struct mach_header *mh)
{
    
    prepare_load_methods((const headerType *)mh);

    call_load_methods();
}

void call_load_methods(void)
{
    ......

    void *pool = objc_autoreleasePoolPush();

    do {
        // 1. 首先调用类的 load
        while (loadable_classes_used > 0) {
            call_class_loads();
        }

        // 2. 调用分类的 load
        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);

   ......
}

// 调用 load 方法,并没有走消息发送机制,而是直接进行调用
(*load_method)(cls, @selector(load));

启动时间优化

目前为止是 App 启动时,pre-main 部分,也就是还没有走到 App 入口的 main 函数。

pre-main

load dylibs

iOS App 一般需要加载 100~400 个 dylibs。其中包括了系统和开发者引入的。加载 dylibs 会消耗 App 的启动时间。可以做优化的部分是自己引入的 dylib。Apple 在 WWDC 上建议,建议尽量将第三方 dylibs 个数控制在 6 个以内。

优化方案:

  1. 使用静态库替代动态库,如果使用 Cocoapods 管理第三方库的话,可以将 podfile 中的 use_frameworks! 注释掉,然后 pod install 来将动态库变更为静态库。
  2. 合并动态库,减少动态库的数量,这一块笔者没有进行尝试。

Rebase/Binding

这两步主要是对 image 内部指针的修复。因此只要指针数量越少,修复的耗时就会变少,其中的关键是减少 _DATA 段中指针的数量。

优化方案:

  1. 减少类、方法的数量,比如删除废弃的类、方法
  2. 使用 Swift 的 struct(WWDC 介绍结构内部有做优化,指针数量少)
  3. 减少 C++ 虚函数的数量(这块可优化的空间较小)

参考

libSystem

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