美文网首页面试宝点
iOS性能优化-APP启动

iOS性能优化-APP启动

作者: wuyukobe | 来源:发表于2022-12-04 21:47 被阅读0次

    前言:本文旨在介绍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冷启动治理:来自美团外卖的实践

    相关文章

      网友评论

        本文标题:iOS性能优化-APP启动

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