iOS App启动流程优化

源代码 2024-9-18 04:18:03 18 0 来自 中国
iOS App的启动流程可以分成两个阶段 pre-main阶段和main阶段。
pre-main阶段

体系将App的可实行文件(Mach-O文件)和dyld加载到内存,由dyld举办法态链接。

  • 设置相干情况变量

    根据情况变量设置相应的值以及获取当前运行架构。比方设置情况变量打印启动流程耗时: DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS。
  • 加载共享缓存库

    加载动态共享缓存库到动态库共享缓存区,比方UIKit、CoreFoundation等官方库。
  • 加载动态库

    把全部的可实行文件所依赖的动态库递归加载到内存中。
  • rebase和binding

    iOS接纳ASLR技能(地点空间结构随机化),加载App的内存地点是随机的,rebase会根据随机的偏移量对原来的地点做重定向。
    binding举行符号绑定。指向image外部动态库的指针被符号(symbol)绑定。dyld必要去符号表里查找,找到对应的实现。
  • Objc setup

    • 注册ObjC类
    • 把category的界说插入方法列表
    • selector唯一性查抄

  • initializer

    • 调用全部类、分类的+load方法
    • 调用__attribute__((constructor))修饰的函数
    • 非根本范例的C++静态全局变量的创建(通常是类或结构体)

map_images与load_images
map_images : dyld 将 image 加载进内存时 , 会触发该函数.
load_images : dyld 初始化 image 会触发该方法. ( 我们所熟知的 load 方法也是在此处调用 ) .
dyld在初始化其他动态库之前,会开始初始化体系库libsystem,运行Runtime。体系库libsystem初始化完成后,就会初始化其他动态库,然后由Runtime调用map_images来读取类、方法、协议以及分类并存储到对应的表中(注意:分类并不是直接存,而是通过attachLists方法把分类的数据添加到类内里),然后Runtime会继续调用load_images调用全部类的load方法以及分类的load方法,这些都做完之后,通过dyld提供的回调_dyld_objc_notify_register,告诉dyld加载完毕,然后dyld就开始找主步伐的入口main函数,末了进入步伐的main函数。
load方法的调用序次
+load方法是在load_images中调用的。
load方法调用序次为:先处理处罚类,后处理处罚分类;处理处罚类的序次是先父类,后子类
在调用类的load方法时,做了递归处理处罚,会先调用父类的load,然后再调用子类的load,全部类的load方法调用完成后,才会开始处理处罚全部类的分类,分类的处理处罚序次取决于Mach-O头文件,和类的序次没有直接关系。先后序次即:父类->子类->全部类的分类。
pre-main时间统计

iOS10至iOS14,可通过Edit Scheme->Arguments->Environment Variables添加情况变量 DYLD_PRINT_STATISTICS和DYLD_PRINT_STATISTICS_DETAILS,value都为YES。
iOS15以上可通过instrument->app launch举行分析。

1.png

  • 统计线上用户App启动时间
添加情况变量大概通过app launch,可以在开辟阶段举行分析,那么如安在App发布后,统计线上用户App的启动时间?
实际上,在App冷启动时体系会为App开启一个历程,而这个历程的信息可以通过代码得到,因此可以通过以下代码获取pre-main耗时。同理,只需在application:didFinishLaunchingWithOptions:实行完毕后调用statisticsLaunchTime方法即可得到整个app的启动时间。之后通过日记服务上传,即可统计线上数据。
BOOL getProcessInfo(int pid , struct kinfo_proc*procInfo){    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};    size_t size = sizeof(*procInfo);    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;}NSTimeInterval statisticsLaunchTime(void){    struct kinfo_proc kProcInfo;    if (getProcessInfo([[NSProcessInfo processInfo] processIdentifier],&kProcInfo)) {        NSTimeInterval startTime = kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; //转为毫秒        NSTimeInterval curTime = [[NSDate date] timeIntervalSince1970] * 1000;        return (curTime - startTime) / 1000.0;    }    return -1;}int main(int argc, char * argv[]) {    NSLog(@"re Main Launch Time : %.4f", statisticsLaunchTime());        NSString * appDelegateClassName;    @autoreleasepool {        // Setup code that might create autoreleased objects goes here.        appDelegateClassName = NSStringFromClass([AppDelegate class]);    }    return UIApplicationMain(argc, argv, nil, appDelegateClassName);}main阶段

在pre-main阶段完成之后,dyld 会调用 main() 函数,main() 会调用 UIApplicationMain(),直至application:didFinishLaunchingWithOptions:实行完毕,整个启动流程就完成了。固然从用户体验的角度来说,首屏渲染完成后才算是启动完成。
Mach-O文件格式



可以用MachOView检察Mach-O文件,此中__TEXT segment 包罗可实行代码块和只读数据,__DATA segment是可读可写的。
启动优化思路


  • pre-main流程优化

    • 第三方动态库不宜过多,加载越多的第三方动态库,启动越慢。且由于iOS的沙盒机制,第三方动态库必要接纳嵌入的方式置入app内,并不能镌汰app的体积。
    • 代码瘦身,删除无用的代码和资源,镌汰ObjC类以进步ObjC setup的速率。
    • 镌汰+load方法。尽量用+initialize大概其他替换实现。
    • 镌汰__attribute__((constructor))函数和非根本范例的C++静态全局变量的创建。

  • main流程优化
main阶段从main函数开始直到application:didFinishLaunchingWithOptions:实行完才竣事。在这个阶段告急做的工作有:初始化设置、启动项注册、rootViewController创建等。优化思路如下:

  • 镌汰耗时利用,假如必须在启动时实行,那么在情况允许的情况下应将其放在并发队列中异步实行,避免壅闭主线程。
  • 镌汰IO利用,如大图的读取等,从磁盘读取数据会淹灭大量时间。
  • 对启动项举行分类,部分启动项注册可以延后实行。
  • 缓存首页数据
等。

  • 利用App Launch定位耗期间码
Instrument—App Launch,选择必要分析的app,点击左上角按钮就能举行分析。Call Tree发起将 Separate by ThreadHide System Libraries勾选上,分析之后的调用栈会忽略掉体系调用和按线程分别,便于我们分析本身的代码。

4.png
5.png
此中p_checkServiceFinderDependences是DEBUG情况下检测模块依赖和路由正当性,必要遍历类表,淹灭大量时间。这个方法不会影响主流程,没须要在主线程里运行,故应将其放入并发队列中异步实行。
    dispatch_async(dispatch_get_global_queue(0, 0), ^{        [self p_checkServiceFinderDependences];    });getDeviceUserAgent则是获取User-Agent字符串的过程,这里本身AppConfig就必要初始化一个单例,而getDeviceUserAgent方法内部另有dispatch_once代码,必要淹灭肯定的时间。而且内部必要临时构造一个WKWebView,这就限定了其必须在主线程中实行。但该方法不会影响到后续步调,故放在主队列中异步实行就可。
    dispatch_async(dispatch_get_main_queue(), ^{        [[AppConfig sharedInstance] getDeviceUserAgent];    });优化之后Main Thread的时间降到1.88s。

启动项注册

随着业务的发展,启动项不免越来越多。假如把启动项的注册都写在一个方法内的话,那将造成代码痴肥。别的差别的启动项的注册机遇并不雷同,部分启动项必要尽早注册(比方crash统计,日记上报,热修复等),部分启动项则可以延后注册(在首屏渲染完成后注册大概利用时才注册)。再有,当把某个启动项对应的功能模块化做成独立的framework之后,每个App利用它都必须写一遍注册方法。
目前我们App的处理处罚方案是:利用plist记载启动项,并利用FBModuleManager对启动项举行管理。FBModuleManager会根据启动项的设置将其分成立即启动和LazyLoad两种。这里就不赘述。

  • 启动项注册
下面将先容另一种启动项管理的思路。
__attribute__((used, section("__DATA,__launch")))实现在编译期间往Mach-O文件写入字段,used防止在release情况下函数被链接器优化掉,section指定写入的位置,此处我们将数据写入__DATA segment下的__launch section。
为了编码方便,我们界说如下宏:
#define LAUNCH_MODULE_EXPORT(module, stage, priority) \static id _LAUNCH_START_##module(void); \__attribute__((used, section("__DATA,__launch"))) \static const struct LAUNCH_MODULE _LAUNCH_MODULE_##module = (struct LAUNCH_MODULE){(char *)&#module, stage, priority, (void *)(&_LAUNCH_START_##module)}; \static id _LAUNCH_START_##module(void) \struct LAUNCH_MODULE {    char *module;             //模块名    int stage;                //注册机遇    int priority;             //优先级    id (*startFunc)(void);    //启动方法,返回初始化后的模块实例,Nullable};之后我们便可以在模块内部简单地通过如下代码实现自注册,在这里我们注册了一个在preMain阶段的启动项。
LAUNCH_MODULE_EXPORT(TestPreMainModule, FBLaunchStagePreMain, FBLaunchPriorityLow) {    return [TestPreMainModule start];}对于启动阶段和实行优先级的罗列如下,同一个启动阶段下,越高的优先级越先实行代码。
typedef NS_ENUM(NSInteger, FBLaunchStage) {    FBLaunchStagePreMain = 0,    FBLaunchStageWillFinishLaunch = 1,    FBLaunchStageDidFinishLaunch = 2,    FBLaunchStageWillShowFirstScreen = 3,    FBLaunchStageDidShowFirstScreen = 4,    FBLaunchStageLazyLoad = 5,};typedef NS_ENUM(NSInteger, FBLaunchPriority) {    FBLaunchPriorityLow = 0,    FBLaunchPriorityMid = 1,    FBLaunchPriorityHigh = 2,};写入的结果如下:

7.png

  • 启动项读取
在App启动时,我们必要读取全部的Mach-O文件注册的启动项,关键代码如下:
@interface FBLaunchModule : NSObject@property (nonatomic, strong) NSString *module;@property (nonatomic, assign) FBLaunchStage stage;@property (nonatomic, assign) FBLaunchPriority priority;@property (nonatomic, assign) id(*startMethod)(void);@property (nonatomic, assign) BOOL alreadStart;@property (nonatomic, strong) id moduleInstance;@end- (void)getAllModules {    NSString *appName = [[[NSBundle mainBundle] infoDictionary] objectForKeyNSString *)kCFBundleExecutableKey];    NSString *fullAppName = [NSString stringWithFormat"/%@.app/", appName];    char *fullAppNameC = (char *)[fullAppName UTF8String];        NSMutableArray<FBLaunchModule *> *result = [[NSMutableArray alloc] init];    int num = _dyld_image_count();    for (int i = 0; i < num; i++) {        const char *name = _dyld_get_image_name(i);        if (strstr(name, fullAppNameC) == NULL) {            continue;        }                const struct mach_header *header = _dyld_get_image_header(i);                Dl_info info;        dladdr(header, &info);                const FBMachOExportValue dliFbase = (FBMachOExportValue)info.dli_fbase;        const FBMachOExportSection *section = FBGetSectByNameFromHeader(header, "__DATA", "__launch");        if (section == NULL) continue;        int addrOffset = sizeof(struct LAUNCH_MODULE);        for (FBMachOExportValue addr = section->offset;             addr < section->offset + section->size;             addr += addrOffset) {                        struct LAUNCH_MODULE entry = *(struct LAUNCH_MODULE *)(dliFbase + addr);            FBLaunchModule *module = [[FBLaunchModule alloc] init];            module.module = [NSString stringWithCString:entry.module encoding:NSUTF8StringEncoding];            module.stage = entry.stage;            module.priority = entry.priority;            module.checkFunc = entry.checkFunc;            module.startFunc = entry.startFunc;            [result addObject:module];        }    }        _modules = [NSArray arrayWithArray:result];}

  • 启动项实行
我们实现了一个管理类FBLaunchManager,用于同一读取、生存、实行启动项。
@interface FBLaunchManager : NSObject+ (id)sharedInstance;- (void)executeLaunchersForStageFBLaunchStage)stage;- (id)getModuleByNameNSString *)moduleName;@end实行差别阶段启动项的代码如下:
- (void)executeLaunchersForStageFBLaunchStage)stage {    if (_modules.count == 0) {        return;    }    NSMutableArray *moduleAry = [NSMutableArray new];        //阶段    for (FBLaunchModule *m in _modules) {        if (m.stage == stage) {            [moduleAry addObject:m];        }    }        //优先级    [moduleAry sortUsingComparator:^NSComparisonResult(FBLaunchModule * _Nonnull obj1, FBLaunchModule * _Nonnull obj2) {        return obj1.priority < obj2.priority;    }];        for (NSInteger i = 0; i < [moduleAry count]; i++) {        FBLaunchModule *module = moduleAry;        module.moduleInstance = module.startFunc();        module.alreadStart = YES;    }}假如一个启动项被声明为FBLaunchStageLazyLoad,那么只有在利用它的时间才初始化,在getModuleByName:中实现了懒加载的逻辑。
- (id)getModuleByNameNSString *)moduleName {    for (FBLaunchModule *m in _modules) {        if ([m.module isEqualToString:moduleName]) {            if (m.alreadStart) {                return m.moduleInstance;            }            m.moduleInstance = m.startFunc();            m.alreadStart = YES;            return m.moduleInstance;        }    }    return nil;}PreMain阶段启动:
__attribute__((constructor)) static void executePreMainLaunchers() {    [[FBLaunchManager sharedInstance] executeLaunchersForStage:FBLaunchStagePreMain];}此处之以是利用__attribute__((constructor)) 函数,是由于其会在全部类和分类的+load方法实行完毕后才调用,可以避免因代码实行时序而引起的题目。
雷同地,其他阶段启动的代码也是在相应机遇调用executeLaunchersForStage:方法。
- (BOOL)applicationUIApplication *)application didFinishLaunchingWithOptionsNSDictionary *)launchOptions {    // Override point for customization after application launch.       [[FBLaunchManager sharedInstance] executeLaunchersForStage:FBLaunchStageDidFinishLaunch];        return YES;}

  • 总结
通过这种思路,我们就可以实现组件自注册与分阶段启动,肯定程度上做到模块解耦。必要注意的是,这种注入方式主工程险些是无知觉的,以是必要自注册的组件必须明确本身的启动阶段与启动的须要性。对于非须要的启动项,无需注册大概注册时声明为LazyLoad。
为了安全性思量,可以再getAllModules方法内做一些校验工作,比方模块名正当性检测、同名模块去重等。模块start方法本身也必要做一些检测,好比模块依赖检测、路由检测等。
Demo代码:https://github.com/linjunyi/LaunchManagerDemo
您需要登录后才可以回帖 登录 | 立即注册

Powered by CangBaoKu v1.0 小黑屋藏宝库It社区( 冀ICP备14008649号 )

GMT+8, 2024-10-18 22:29, Processed in 0.190721 second(s), 35 queries.© 2003-2025 cbk Team.

快速回复 返回顶部 返回列表