美文网首页iOS面试专题
一个iOS APP的启动过程

一个iOS APP的启动过程

作者: 随风流逝 | 来源:发表于2019-12-25 14:04 被阅读0次

    一个程序的入口是什么?

    那肯定main()函数啊。这么说是没有错的,main()函数是我们的程序的入口,但我们应该知道的是,在开始执行我们的程序之前还有很多的准备工作要做,只有准备工作圆满完成,我们的程序才能被正确的执行。

    main()函数执行之前都需要做哪些准备工作

    1.读取可执行文件(Mach-O文件),从Mach-O文件中找到动态链接库dyld的地址,把dyld加载进来。
    2.dyld先设置一下运行环境,配置环境变量,然后加载可执行文件和依赖动态库。
    3.对可执行文件和相关动态库进行链接--用于修证符号指针,使其指向正确地址。
    4.递归的对相关动态库进行初始化,对可执行文件初始化,这个过程中会注册Objc类;把category定义的各种方法、属性、协议等加入类的数组中;调用各个类的load方法。
    5.dyld返回一个主程序的main()函数,开始执行main()函数。

    一个断点引发的研究

    我们先随便找个类加一个+load方法,然后在这个+load方法上打上断点,运行程序,看一下函数调用栈,你会发现什么


    函数调用栈

    程序的运行是从dyld开始的,我们可以从系统源码库中找到dyld的源码,从源码的dyldStartup.s文件中我们可以找到__dyld_start函数,这是一个汇编语言的代码,大家看注释就好了,可以看到在这个函数中调用了dyldbootstrap::start函数。

    __dyld_start:
        pushq   $0      # push a zero for debugger end of frames marker
        movq    %rsp,%rbp   # pointer to base of kernel frame
        andq    $-16,%rsp       # force SSE alignment
        
        # call dyldbootstrap::start(app_mh, argc, argv, slide)
        movq    8(%rbp),%rdi    # param1 = mh into %rdi
        movl    16(%rbp),%esi   # param2 = argc into %esi
        leaq    24(%rbp),%rdx   # param3 = &argv[0] into %rdx
        movq    __dyld_start_static(%rip), %r8
        leaq    __dyld_start(%rip), %rcx
        subq     %r8, %rcx  # param4 = slide into %rcx
        call    __ZN13dyldbootstrap5startEPK11mach_headeriPPKcl 
    
            # clean up stack and jump to result
        movq    %rbp,%rsp   # restore the unaligned stack pointer
        addq    $16,%rsp    # remove the mh argument, and debugger end frame marker
        movq    $0,%rbp     # restore ebp back to zero
        jmp *%rax       # jump to the entry point
    

    start函数是在dyldInitialization.cpp文件中,这个函数主要是一个dyld的自举,然后调用main()函数

    //
    //  This is code to bootstrap dyld.  This work in normally done for a program by dyld and crt.
    //  In dyld we have to do this manually.
    //
    uintptr_t start(const struct mach_header* appsMachHeader, int argc, const char* argv[], intptr_t slide)
    {
          //自举dyld,设置一些运行参数
        .....
            
    
        // run all C++ initializers inside dyld
        runDyldInitializers(dyldsMachHeader, slide, argc, argv, envp, apple);
        
        // if main executable was linked -pie, then randomize its load address
        if ( appsMachHeader->flags & MH_PIE )
            appsMachHeader = randomizeExecutableLoadAddress(appsMachHeader, &appsSlide);
    
        //完成自举,调用dyld的main()函数
        // now that we are done bootstrapping dyld, call dyld's main
        return dyld::_main(appsMachHeader, appsSlide, argc, argv, envp, apple);
    }
    

    至此,我们之前说的第一步1.读取可执行文件(Mach-O文件),从Mach-O文件中找到动态链接库dyld的地址,把dyld加载进来。已经完成了。
    接下来就到了_main()函数,看到这里可能有人会想不是main()函数之前还有好多流程没走吗?怎么就到_main()函数了,其实这个_main()函数并不是我们程序中的main()函数,这个是dyld_main()函数,而且这个函数的返回值就是我们的主程序入口main()函数的地址,所以其实我们执行main()函数之前的准备工作中剩下的几步,都是在dyld_main()函数中执行的。

    //
    // 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 struct mach_header* mainExecutableMH, uintptr_t mainExecutableSlide, int argc, const char* argv[], const char* envp[], const char* apple[])
    {
    #pragma mark 2.dyld先设置一下运行环境,配置环境变量,然后加载可执行文件和依赖动态库。
        //2.1设置上下文
        setContext(mainExecutableMH, argc, argv, envp, apple);
        
        //2.2获取可执行文件路径
        sExecPath = apple[0];
        bool ignoreEnvironmentVariables = false;
    #if __i386__
        if ( isRosetta() ) {
            // under Rosetta (x86 side)
            // When a 32-bit ppc program is run under emulation on an Intel processor,
            // we want any i386 dylibs (e.g. any used by Rosetta) to not load in the shared region
            // because the shared region is being used by ppc dylibs
            gLinkContext.sharedRegionMode = ImageLoader::kDontUseSharedRegion;
            ignoreEnvironmentVariables = true;
        }
    #endif
        
        if ( sExecPath[0] != '/' ) {
            // have relative path, use cwd to make absolute
            char cwdbuff[MAXPATHLEN];
            if ( getcwd(cwdbuff, MAXPATHLEN) != NULL ) {
                // maybe use static buffer to avoid calling malloc so early...
                char* s = new char[strlen(cwdbuff) + strlen(sExecPath) + 2];
                strcpy(s, cwdbuff);
                strcat(s, "/");
                strcat(s, sExecPath);
                sExecPath = s;
            }
        }
        //2.3设置运行环境
        uintptr_t result = 0;
        sMainExecutableMachHeader = mainExecutableMH;
        sMainExecutableIsSetuid = issetugid();
        if ( sMainExecutableIsSetuid )
            pruneEnvironmentVariables(envp, &apple);
        else
            checkEnvironmentVariables(envp, ignoreEnvironmentVariables);
        if ( sEnv.DYLD_PRINT_OPTS ) 
            printOptions(argv);
        if ( sEnv.DYLD_PRINT_ENV ) 
            printEnvironmentVariables(envp);
        getHostInfo();
        // install gdb notifier
        // 2.4注册gdb的监听
        stateToHandlers(dyld_image_state_dependents_mapped, sBatchHandlers)->push_back(notifyGDB);
        // make initial allocations large enough that it is unlikely to need to be re-alloced
        sAllImages.reserve(200);
        sImageRoots.reserve(16);
        sAddImageCallbacks.reserve(4);
        sRemoveImageCallbacks.reserve(4);
        sImageFilesNeedingTermination.reserve(16);
        sImageFilesNeedingDOFUnregistration.reserve(8);
        
        try {
            // instantiate ImageLoader for main executable
            //2.5实例化一个主程序可执行文件的镜像
            //主程序镜像会被加入`sAllImages`数组中
            sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
            sMainExecutable->setNeverUnload();
            // 2.6设置链接(就是我们上面第三步提到的链接)的上下文,设置其 mainExecutable 为上文中的
            gLinkContext.mainExecutable = sMainExecutable;
            // load shared cache
            // 2.7加载共享缓存
            checkSharedRegionDisable();
        #if DYLD_SHARED_CACHE_SUPPORT
            if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion )
                mapSharedCache();
        #endif
            // load any inserted libraries
            // 2.8加载插入的动态库,动态库的镜像全部加入`sAllImages`数组中
            // `DYLD_INSERT_LIBRARIES` 是环境变量, 调用`loadInsertedDylib`方法加载所有要插入的库
            if  ( sEnv.DYLD_INSERT_LIBRARIES != NULL ) {
                for (const char* const* lib = sEnv.DYLD_INSERT_LIBRARIES; *lib != NULL; ++lib) 
                    loadInsertedDylib(*lib);
            }
            // record count of inserted libraries so that a flat search will look at 
            // inserted libraries, then main, then others.
            // 2.9记录插入的动态库的数量,方便去查找这些库
            sInsertedDylibCount = sAllImages.size()-1;
            
    #pragma mark 3.对可执行文件和相关动态库进行链接--用于修证符号指针,使其指向正确地址。
    
            // link main executable
            // 3.1链接主程序可执行文件
            gLinkContext.linkingMainExecutable = true;
            link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
            gLinkContext.linkingMainExecutable = false;
            if ( sMainExecutable->forceFlat() ) {
                gLinkContext.bindFlat = true;
                gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
            }
            // 3.2返回主程序`mian()`函数的入口地址
            result = (uintptr_t)sMainExecutable->getMain();
            
            // 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
            // 3.3链接插入的动态库(除了主程序)
            // 在链接主可执行文件之后执行此操作,以保证动态库都在主程序后面被链接
            if ( sInsertedDylibCount > 0 ) {
                for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
                    ImageLoader* image = sAllImages[i+1];
                    link(image, sEnv.DYLD_BIND_AT_LAUNCH, ImageLoader::RPathChain(NULL, NULL));
                }
            }
            
        #if SUPPORT_OLD_CRT_INITIALIZATION
            // Old way is to run initializers via a callback from crt1.o
            if ( ! gRunInitializersOldWay ) 
        #endif
    #pragma mark 4.递归的对相关动态库进行初始化,对可执行文件初始化,这个过程中会注册Objc类;把category定义的各种方法、属性、协议等加入类的数组中;调用各个类的load方法。
            // 执行初始化方法
            // 其中`+load` 和constructor方法就是在这里执行
            initializeMainExecutable(); // run all initializers
        }
        catch(const char* message) {
            halt(message);
        }
        catch(...) {
            dyld::log("dyld: launch failed\n");
        }
    #pragma mark 5.dyld返回一个主程序的main()函数,开始执行main()函数。
        //返回主程序`main()`函数地址
        return result;
    }
    

    2.dyld先设置一下运行环境,配置环境变量,然后加载可执行文件和依赖动态库。

    刚开始先是为上下文和运行环境进行了大量的配置,以保证后面加载镜像、符号链接、初始化等工作的正常进行,上面代码都有注释,就不再说了。

    • 加载主程序

    这一步主要是在instantiateFromLoadedImage()函数中执行,首先在isCompatibleMachO()函数中判断可执行文件与当前机型的兼容性,如果兼容接下来就调用ImageLoaderMachO()实例化一个主程序,紧接着调用addImage()函数把主程序镜像添加到sAllImages数组中,这些都是在instantiateFromLoadedImage()函数中完成,这个函数会把实例化的主程序做为返回值返回给sMainExecutable
    之后符号链接上下文中的mainExecutable设置为刚刚我们实例化出来的主程序--sMainExecutable

    • 加载共享缓存

    这一步主要是在mapSharedCache()函数中映射共享缓存,首先通过_shared_region_check_np()快速检查缓存是否已真正映射到共享区域,如果已经映射了, 就更新共享缓存。
    反之, 判断系统是否处于安全启动模式(safe-boot mode)下,如果我们处于安全启动模式,并且在此启动周期中未创建缓存,就删除缓存文件并重新生成它。
    接下来调用openSharedCacheFile()打开缓存文件, 接着读取缓存文件的前4096字节,解析缓存头dyld_cache_header的信息, 将解析好的缓存信息存入mappings变量,最后调用_shared_region_map_np完成真正的映射工作,并在sSharedCache记录dyld_cache_header头文件。
    如果共享缓存创建成功,就在符号链接上下文中记录dyld地址与共享缓存地址是否相同,然后告诉gdb共享缓存在哪。

    • 加载插入的动态库

    首先循环遍历DYLD_INSERT_LIBRARIES环境变量中指定的动态库,然后调用loadInsertedDylib()函数将其加载,在该函数中先配置加载上下文环境,再调用load()函数进行加载。load()函数会调用loadPhase0()函数,loadPhase0()会尝试从所有的可能路径加载动态库,直到loadPhase6()函数,查找的顺序为DYLD_ROOT_PATH->LD_LIBRARY_PATH->DYLD_FRAMEWORK_PATH->原始路径->DYLD_FALLBACK_LIBRARY_PATH,找到后调用ImageLoaderMachO()函数来实例化一个ImageLoader,之后调用checkandAddImage()验证镜像并将其加入到全局镜像数组sAllImages中。
    如果image为空,表示没有找到动态库,则会抛出异常。

    3.对可执行文件和相关动态库进行链接--用于修证符号指针,使其指向正确地址。

    • 链接主程序

    首先设置链接上下文的linkingMainExecutable参数为true,然后调用link()函数完成主程序的链接操作,该函数经过一系列的检查,之后调用了ImageLoader::link()函数,该函数中调用recursiveLoadLibraries()函数递归加载所依赖的动态库。
    recursiveLoadLibraries()函数中首先找到依赖库的数量、名称、版本等等信息,然后根据这些信息调用LinkContextloadLibrary()函数去加载这些动态库,然后获取这些动态库信息,判断版本信息是否兼容,如果不兼容就抛出异常。
    依赖库递归加载完成之后会进行一个排序,以便后续的工作都是从最低级动态库开始,然后就开始真正的链接工作,先是调用recursiveRebase()函数对动态库进行基地址的复位,然后调用
    recursiveBind()函数对动态库进行non-lazy符号绑定,一般的情况下多数符号都是lazy的,他们在第一次使用的时候才进行绑定。
    主程序链接完成之后就可以得到主程序入口函数,赋值给result,待所有工作完成之后,把result做为返回值返回。

    • 链接插入的动态库

    和主程序链接流程是一样的。

    4.递归的对相关动态库进行初始化,对可执行文件初始化,这个过程中会注册Objc类;把category定义的各种方法、属性、协议等加入类的数组中;调用各个类的load方法。

    首先对link上下文进行一个设置,记录一下我们当前进度;然后对除了主程序之外的动态库进行初始化;然后再单独对主程序进行初始化;最后注册atexit()以在进程退出时加载所有镜像的终止符。

    • 初始化动态库

    经过initializeMainExecutable()->ImageLoader::runInitializers()->ImageLoader::recursiveInitialization()一系列的调用进入recursiveInitialization ()函数,再此函数中,首先自底向上的递归初始化依赖库,然后发出通知,当前状态发生变化--动态库的依赖已经完全加载;然后才是初始化当前动态库--调用doInitialization ()函数,在这个函数中会调用函数的"Initializer"符号,实际上就是镜像内部的真正初始化函数;然后继续发出通知,当前镜像状态发生变化--动态库本身已经初始化完成。

    initializeMainExecutable()的过程

    在前面初始化动态库一小节说过,doInitialization ()函数会调用doImageInit()doModInitFunctions()函数,而在这两个函数中调用的镜像的Initializer方法,这个Initializer只是一个符号指向,并不是指一个名字为Initializer的方法,而是C++静态对象初始化构造器,atribute((constructor))进行修饰的方法,在镜像中Initializer指针所指向该初始化方法的地址。
    我们可以通过一个环境变量来查看我们调用的各个依赖库的Initializer方法,Edit Scheme -> Run ->Arguments中增加一个DYLD_PRINT_INITIALIZERS的环境变量,设置值为YES,然后运行我们的程序,可以在控制台看到动态库Initializer调用名称。

    环境变量
    dyld: calling initializer function 0x180feba94 in /usr/lib/libSystem.B.dylib
    dyld: calling -init function 0x10616cd38 in /Developer/usr/lib/libBacktraceRecording.dylib
    dyld: calling initializer function 0x181003620 in /usr/lib/libc++.1.dylib
    dyld: calling -init function 0x181dd1be4 in /System/Library/Frameworks/CoreFoundation.framework/CoreFoundation
    ...
    dyld: calling initializer function 0x10650884c in /Developer/Library/PrivateFrameworks/GPUTools.framework/libglInterpose.dylib
    dyld: calling initializer function 0x1068636b8 in /Developer/Library/PrivateFrameworks/MTLToolsDeviceSupport.framework/libMTLInterpose.dylib
    

    我们可以看到一个巨长的调用信息,我们可以看到每个Image都有不同的初始化方法名称。
    我们看到最开始调用的是一个名字为libSystem.dylib动态库,就是在这个动态库中,调用了libdispatch进行了初始化,在libdispatch源码中我们看到到经过libdispatch_init()->_os_object_init()->_objc_init()一系列的调用_objc_init()进行了runtime的初始化,我们可以在工程中打上符号断点进行验证:

    符号断点
    运行程序,查看函数调用栈(Xcode显示不完整,利用lldb查看):
    frame #0: 0x0000000190942350 libobjc.A.dylib`_objc_init
    frame #1: 0x0000000103700238 libdispatch.dylib`_os_object_init + 16
    frame #2: 0x000000010370ebc4 libdispatch.dylib`libdispatch_init + 372
    frame #3: 0x00000001908bcb04 libSystem.B.dylib`libSystem_initializer + 136
    frame #4: 0x0000000102d410e0 dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 412
    frame #5: 0x0000000102d41314 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 36
    frame #6: 0x0000000102d3c398 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 464
    frame #7: 0x0000000102d3c328 dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 352
    frame #8: 0x0000000102d3b3dc dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 136
    frame #9: 0x0000000102d3b498 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 84
    frame #10: 0x0000000102d2a688 dyld`dyld::initializeMainExecutable() + 140
    frame #11: 0x0000000102d2f2a0 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 4304
    frame #12: 0x0000000102d29044 dyld`_dyld_start + 68
    

    runtime初始化后在_objc_init中注册了几个通知,然后初始化相应依赖库里的类结构,调用依赖库里所有的load方法。
    以我们的主程序镜像为例,它的initializer方法是最后调用的,当initializer方法被调用前,dyld会通知runtime进行类结构初始化,然后再通知调用load方法,这些目前还发生在main函数前,但由于lazy bind机制,依赖库多数都是在使用时才进行绑定,所以这些依赖库的类结构初始化都是发生在程序里第一次使用到该依赖库时才进行的。

    • 返回APP的main()函数地址

    在第二步的链接主程序中,我们说过主程序(最后一个动态库)链接完成之后,就通过getMain()函数拿到了主程序main()地址,到这一步直接把拿到的result返回就可以了。

    启动优化结论

    根据以上过程,我们可以找到我们程序在启动过程中可以优化的点:

    1. 减少依赖不必要的库,不管是动态库还是静态库;如果可以的话,把动态库改造成静态库;如果必须依赖动态库,则把多个非系统的动态库合并成一个动态库;
    2. 检查下framework应当设为optionalrequired,如果该framework在当前App支持的所有iOS系统版本都存在,那么就设为required,否则就设为optional,因为`optional``会有些额外的检查;
    3. 合并或者删减一些OC类和函数;关于清理项目中没用到的类,使用工具AppCode代码检查功能,查到当前项目中没有用到的类(也可以用根据linkmap文件来分析,但是准确度不算很高)。
    4. 删减一些无用的静态变量,
    5. 删减没有被调用到或者已经废弃的方法。
    6. 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数(创建虚函数表有开销)
    7. 类和方法名不要太长:iOS每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的;因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来;
    8. 用dispatch_once()代替所有的 attribute((constructor)) 函数、C++静态对象初始化、ObjC的+load函数;
    9. 在设计师可接受的范围内压缩图片的大小,会有意外收获。压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了,比较靠谱的压缩算法是TinyPNG。

    相关文章

      网友评论

        本文标题:一个iOS APP的启动过程

        本文链接:https://www.haomeiwen.com/subject/gepibctx.html