热点新闻
iOS App启动流程优化
2023-07-05 05:45  浏览:273  搜索引擎搜索“爱农网”
温馨提示:信息一旦丢失不一定找得到,请务必收藏信息以备急用!本站所有信息均是注册会员发布如遇到侵权请联系文章中的联系方式或客服删除!
联系我时,请说明是在爱农网看到的信息,谢谢。
展会发布 展会网站大全 报名观展合作 软文发布

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
    1. 注册ObjC类
    2. 把category的定义插入方法列表
    3. selector唯一性检查
  • initializer
    1. 调用所有类、分类的+load方法
    2. 调用__attribute__((constructor))修饰的函数
    3. 非基本类型的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进行分析。





ab8ac863d.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(@"Pre 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文件格式




mach-o format.png





Load Commands.png


可以用MachOView查看Mach-O文件,其中__TEXT segment 包含可执行代码块和只读数据,__DATA segment是可读可写的。

启动优化思路

  • pre-main流程优化
    1. 第三方动态库不宜过多,加载越多的第三方动态库,启动越慢。且由于iOS的沙盒机制,第三方动态库需要采用嵌入的方式置入app内,并不能减少app的体积。
    2. 代码瘦身,删除无用的代码和资源,减少ObjC类以提高ObjC setup的速度。
    3. 减少+load方法。尽量用+initialize或者其他替代实现。
    4. 减少__attribute__((constructor))函数和非基本类型的C++静态全局变量的创建。
  • main流程优化

main阶段从main函数开始直到application:didFinishLaunchingWithOptions:执行完才结束。在这个阶段主要做的工作有:初始化配置、启动项注册、rootViewController创建等。优化思路如下:

  1. 减少耗时操作,如果必须在启动时执行,那么在情况允许的情况下应将其放在并发队列中异步执行,避免阻塞主线程。
  2. 减少IO操作,如大图的读取等,从磁盘读取数据会耗费大量时间。
  3. 对启动项进行分类,部分启动项注册可以延后执行。
  4. 缓存首页数据

等。

  • 利用App Launch定位耗时代码

Instrument—App Launch,选择需要分析的app,点击左上角按钮就能进行分析。Call Tree建议将 Separate by ThreadHide System Libraries勾选上,分析之后的调用栈会忽略掉系统调用和按线程划分,便于我们分析自己的代码。




AppLaunch_01.png





AppLaunch_02.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。





fc7a108e58.png

启动项注册

随着业务的发展,启动项难免越来越多。如果把启动项的注册都写在一个方法内的话,那将造成代码臃肿。另外不同的启动项的注册时机并不相同,部分启动项需要尽早注册(例如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, };

写入的效果如下:





截屏2023-01-10 11.44.13.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] objectForKey:(NSString *)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)executeLaunchersForStage:(FBLaunchStage)stage; - (id)getModuleByName:(NSString *)moduleName; @end

执行不同阶段启动项的代码如下:

- (void)executeLaunchersForStage:(FBLaunchStage)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[i]; module.moduleInstance = module.startFunc(); module.alreadStart = YES; } }

如果一个启动项被声明为FBLaunchStageLazyLoad,那么只有在使用它的时候才初始化,在getModuleByName:中实现了懒加载的逻辑。

- (id)getModuleByName:(NSString *)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)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[FBLaunchManager sharedInstance] executeLaunchersForStage:FBLaunchStageDidFinishLaunch]; return YES; }

  • 总结

通过这种思路,我们就可以实现组件自注册与分阶段启动,一定程度上做到模块解耦。需要注意的是,这种注入方式主工程几乎是无知觉的,所以需要自注册的组件必须明确自己的启动阶段与启动的必要性。对于非必要的启动项,无需注册或者注册时声明为LazyLoad。
为了安全性考虑,可以再getAllModules方法内做一些校验工作,例如模块名合法性检测、同名模块去重等。模块start方法本身也需要做一些检测,比如模块依赖检测、路由检测等。

Demo代码:https://github.com/linjunyi/LaunchManagerDemo

发布人:50bf****    IP:120.244.04.***     举报/删稿
展会推荐
让朕来说2句
评论
收藏
点赞
转发