面试宝点

iOS性能优化-APP启动

2022-12-04  本文已影响0人  wuyukobe

前言:本文旨在介绍iOS性能优化中有关APP启动流程的介绍和优化。

一、APP启动流程

1、APP的冷启动流程

2、APP的冷启动流程的3大阶段

APP的冷启动可以概括为3大阶段:Dyld ---> Runtime ---> main
Dyld(dynamic link editor):Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等)

2.1、启动APP时,Dyld所做的事情有:
2.2、Runtime所做的事情有:
2.3、main函数

接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

3、Dyld在各阶段所做的事情:

二、影响main()之前的启动加载时间的因素:

三、APP的启动优化

按照不同的阶段

1、Dyld
2、runtime
3、main

四、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

七、启动过程中动态链接器阶段,为什么合并动态库能提高优化时间?

Dyld loading 阶段,加载动态库,这个阶段会去装载APP使用的动态库,而每一个动态库有它自己的依赖关系,所以会消耗时间去查找和读取。对于Apple提供的的系统动态库,做了高度的优化。而对于开发者定义导入的动态库,则需要在花费更多的时间。Apple官方建议尽量少的使用自定义的动态库,或者考虑合并多个动态库,其中一个建议是当大于6个的时候,则需要考虑合并它们。

八、静态链接库与动态链接库

1、介绍

静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib 中的指令都全部被直接包含在最终生成的包文件中了。但是若使用动态链接库,该动态链接库不必被包含在最终包里,包文件执行时可以“动态”地引用和卸载这个与安装包独立的动态链接库文件。

2、区别

以上是有关APP启动的介绍,欢迎补充和指正。

参考:
iOS App 启动优化
ios启动优化:二进制重排
iOS App冷启动治理:来自美团外卖的实践

上一篇下一篇

猜你喜欢

热点阅读