iOS启动优化思路
随着App业务不断地增加,也迭代了不少的版本,功能不断的完善,跟着而来的是用户对手机性能体验的不断提高。如果要想讲述App启动优化,一定要知道启动发生了什么事情!下面将讲述如何优化?
下面会先讲述Mach-O和Dyld的常识,否则一上来就讲述优化,有点不切实际,要将原理理解清楚最好
一、MachO文件
在讲述启动优化之前,先讲述MachO文件!
1. 概述
Mach-O是Mach Object文件格式的缩写,iOS以及Mac上可执行的文件格式,类似Window的exe格式,Linux上的elf格式。Mach-O是一个可执行文件、动态库以及目标代码的文件格式,是a.out格式的替代,提供了更高更强的扩展性。
2. 常见格式
Mach-O常见格式如下:
- 目标文件 .o
- 库文件
- .a
- .dylib
- .framework
- 可执行文件
- dyld
- .dsym
通过file文件路径查看文件类型
2.1目标文件.o
通过test.c 文件,可以使用clang命令将其编译成目标文件.o
我们再通过file命令(如下)查看文件类型
是个Mach-O文件。
2.2 dylib
通过cd /usr/lib命令查看dylib
通过file命令查看文件类型
3. 通用二进制文件
通用二进制文件是苹果自身发明的,基本内容如下
下面通过指令查看Macho文件来看下通用二进制文件
然后通过file指令查看文件类型
上面该MachO文件包含了3个架构分别是arm v7,arm v7s 以及arm 64 。
针对该MachO文件我们做几个操作,利用lipo命令拆分合并架构
3.1 利用lipo-info查看MachO文件架构
3.2 瘦身MachO文件,拆分
查看一下结果如下,多出来一个新建的MachO_armv7
3.3 增加架构,合并
利用lipo -create 合并多种架构
整理出lipo命令如下:
4. Mach-O文件结构
下面是苹果官方图解释MachO文件结构图
MachO文件的组成结构如上,看包括了三个部分
- Header包含了该二进制文件的一般信息,信息如下:
- 字节顺序、加载指令的数量以及架构类型
- 快速的确定一些信息,比如当前文件是32位或者64位,对应的文件类型和处理器是什么
-
Load commands 包含很多内容的表
包括区域的位置、动态符号表以及符号表等
-
Data一般是对象文件的最大部分
一般包含Segement具体数据
4.1 header的数据结构
在项目代码中,按下Command+ 空格,然后输入loader.h 查看loader .h,找到mach_header
上面是mach_header,对应结构体的意义如下
通过MachOView查看Mach64 Header头部信息
4.2 LoadCommands
LoadCommand包含了很多内容的表,通过MachOView查看LoadCommand的信息,图如下:
但是大家看的可能并不了解内容,下面有图进行注解,可以看下主要的意思
4.3 Data
Data包含Segement,存储具体数据,通过MachOView查看,地址映射内容
二、Dyld
2.1 dyld概述
dyld(the dynamic link editor)是苹果动态链接器,是苹果系统一个重要的组成部分,系统内核做好准备工作之后,剩下的就会交给了dyld。系统会先读取App的可执行性文件(MachO)从里面获取dyld的路径,然后加载dyld,dyld去初始化运行环境,开启缓存策略,加载程序相关的依赖库【包括可执行文件】,并对这些库进行链接,最后调用每个依赖库的初始化方法,在这一步,runtime被初始化。当所有依赖库的初始化后,轮到最后一位【程序的可执行性文件】进行初始化,在这时runtime会对项目中所有类进行类结构初始化,然后调用所有的load方法。最后dyld返回函数地址,main函数被调用,便来到了熟悉的程序入口。
2.2 dyld加载过程
程序的入口一般都是在main函数中,但是比较少的人关心main()函数之前发生了什么?这次我们先探索dyld的加载过程。(但是比在main函数之前,load方法就在main函数之前)
2.2.1 新建项目,在main函数下断点
main()之前有个libdyld.dylib start入口,但是不是我们想要的,根据dyld源码找到__dyld_start函数
2.2.2 dyld main函数
dyld main()函数是关键函数,下面是函数实现内容。(此时的main实现函数和程序App的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 macho_header* mainExecutableMH, uintptr_t mainExecutableSlide,
int argc, const char* argv[], const char* envp[], const char* apple[],
uintptr_t* startGlue)
{
// Grab the cdHash of the main executable from the environment
// 第一步,设置运行环境
uint8_t mainExecutableCDHashBuffer[20];
const uint8_t* mainExecutableCDHash = nullptr;
if ( hexToBytes(_simple_getenv(apple, "executable_cdhash"), 40, mainExecutableCDHashBuffer) )
// 获取主程序的hash
mainExecutableCDHash = mainExecutableCDHashBuffer;
// Trace dyld's load
notifyKernelAboutImage((macho_header*)&__dso_handle, _simple_getenv(apple, "dyld_file"));
#if !TARGET_IPHONE_SIMULATOR
// Trace the main executable's load
notifyKernelAboutImage(mainExecutableMH, _simple_getenv(apple, "executable_file"));
#endif
uintptr_t result = 0;
// 获取主程序的macho_header结构
sMainExecutableMachHeader = mainExecutableMH;
// 获取主程序的slide值
sMainExecutableSlide = mainExecutableSlide;
CRSetCrashLogMessage("dyld: launch started");
// 设置上下文信息
setContext(mainExecutableMH, argc, argv, envp, apple);
// Pickup the pointer to the exec path.
// 获取主程序路径
sExecPath = _simple_getenv(apple, "executable_path");
// <rdar://problem/13868260> Remove interim apple[0] transition code from dyld
if (!sExecPath) sExecPath = apple[0];
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;
}
}
// Remember short name of process for later logging
// 获取进程名称
sExecShortName = ::strrchr(sExecPath, '/');
if ( sExecShortName != NULL )
++sExecShortName;
else
sExecShortName = sExecPath;
// 配置进程受限模式
configureProcessRestrictions(mainExecutableMH);
// 检测环境变量
checkEnvironmentVariables(envp);
defaultUninitializedFallbackPaths(envp);
// 如果设置了DYLD_PRINT_OPTS则调用printOptions()打印参数
if ( sEnv.DYLD_PRINT_OPTS )
printOptions(argv);
// 如果设置了DYLD_PRINT_ENV则调用printEnvironmentVariables()打印环境变量
if ( sEnv.DYLD_PRINT_ENV )
printEnvironmentVariables(envp);
// 获取当前程序架构
getHostInfo(mainExecutableMH, mainExecutableSlide);
//-------------第一步结束-------------
// load shared cache
// 第二步,加载共享缓存
// 检查共享缓存是否开启,iOS必须开启
checkSharedRegionDisable((mach_header*)mainExecutableMH);
if ( gLinkContext.sharedRegionMode != ImageLoader::kDontUseSharedRegion ) {
mapSharedCache();
}
...
try {
// add dyld itself to UUID list
addDyldImageToUUIDList();
// instantiate ImageLoader for main executable
// 第三步 实例化主程序
sMainExecutable = instantiateFromLoadedImage(mainExecutableMH, mainExecutableSlide, sExecPath);
gLinkContext.mainExecutable = sMainExecutable;
gLinkContext.mainExecutableCodeSigned = hasCodeSignatureLoadCommand(mainExecutableMH);
// Now that shared cache is loaded, setup an versioned dylib overrides
#if SUPPORT_VERSIONED_PATHS
checkVersionedPaths();
#endif
// dyld_all_image_infos image list does not contain dyld
// add it as dyldPath field in dyld_all_image_infos
// for simulator, dyld_sim is in image list, need host dyld added
#if TARGET_IPHONE_SIMULATOR
// get path of host dyld from table of syscall vectors in host dyld
void* addressInDyld = gSyscallHelpers;
#else
// get path of dyld itself
void* addressInDyld = (void*)&__dso_handle;
#endif
char dyldPathBuffer[MAXPATHLEN+1];
int len = proc_regionfilename(getpid(), (uint64_t)(long)addressInDyld, dyldPathBuffer, MAXPATHLEN);
if ( len > 0 ) {
dyldPathBuffer[len] = '\0'; // proc_regionfilename() does not zero terminate returned string
if ( strcmp(dyldPathBuffer, gProcessInfo->dyldPath) != 0 )
gProcessInfo->dyldPath = strdup(dyldPathBuffer);
}
// load any inserted libraries
// 第四步 加载插入的动态库
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.
// 记录插入的动态库数量
sInsertedDylibCount = sAllImages.size()-1;
// link main executable
// 第五步 链接主程序
gLinkContext.linkingMainExecutable = true;
#if SUPPORT_ACCELERATE_TABLES
if ( mainExcutableAlreadyRebased ) {
// previous link() on main executable has already adjusted its internal pointers for ASLR
// work around that by rebasing by inverse amount
sMainExecutable->rebase(gLinkContext, -mainExecutableSlide);
}
#endif
link(sMainExecutable, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
sMainExecutable->setNeverUnloadRecursive();
if ( sMainExecutable->forceFlat() ) {
gLinkContext.bindFlat = true;
gLinkContext.prebindUsage = ImageLoader::kUseNoPrebinding;
}
// 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
// 第六步 链接插入的动态库
if ( sInsertedDylibCount > 0 ) {
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
link(image, sEnv.DYLD_BIND_AT_LAUNCH, true, ImageLoader::RPathChain(NULL, NULL), -1);
image->setNeverUnloadRecursive();
}
// only INSERTED libraries can interpose
// register interposing info after all inserted libraries are bound so chaining works
for(unsigned int i=0; i < sInsertedDylibCount; ++i) {
ImageLoader* image = sAllImages[i+1];
image->registerInterposing();
}
}
// <rdar://problem/19315404> dyld should support interposition even without DYLD_INSERT_LIBRARIES
for (long i=sInsertedDylibCount+1; i < sAllImages.size(); ++i) {
ImageLoader* image = sAllImages[i];
if ( image->inSharedCache() )
continue;
image->registerInterposing();
}
...
// apply interposing to initial set of images
for(int i=0; i < sImageRoots.size(); ++i) {
sImageRoots[i]->applyInterposing(gLinkContext);
}
gLinkContext.linkingMainExecutable = false;
// <rdar://problem/12186933> do weak binding only after all inserted images linked
// 第七步 执行弱符号绑定
sMainExecutable->weakBind(gLinkContext);
// If cache has branch island dylibs, tell debugger about them
if ( (sSharedCacheLoadInfo.loadAddress != NULL) && (sSharedCacheLoadInfo.loadAddress->header.mappingOffset >= 0x78) && (sSharedCacheLoadInfo.loadAddress->header.branchPoolsOffset != 0) ) {
uint32_t count = sSharedCacheLoadInfo.loadAddress->header.branchPoolsCount;
dyld_image_info info[count];
const uint64_t* poolAddress = (uint64_t*)((char*)sSharedCacheLoadInfo.loadAddress + sSharedCacheLoadInfo.loadAddress->header.branchPoolsOffset);
// <rdar://problem/20799203> empty branch pools can be in development cache
if ( ((mach_header*)poolAddress)->magic == sMainExecutableMachHeader->magic ) {
for (int poolIndex=0; poolIndex < count; ++poolIndex) {
uint64_t poolAddr = poolAddress[poolIndex] + sSharedCacheLoadInfo.slide;
info[poolIndex].imageLoadAddress = (mach_header*)(long)poolAddr;
info[poolIndex].imageFilePath = "dyld_shared_cache_branch_islands";
info[poolIndex].imageFileModDate = 0;
}
// add to all_images list
addImagesToAllImages(count, info);
// tell gdb about new branch island images
gProcessInfo->notification(dyld_image_adding, count, info);
}
}
CRSetCrashLogMessage("dyld: launch, running initializers");
...
// run all initializers
// 第八步 执行初始化方法
initializeMainExecutable();
// notify any montoring proccesses that this process is about to enter main()
dyld3::kdebug_trace_dyld_signpost(DBG_DYLD_SIGNPOST_START_MAIN_DYLD2, 0, 0);
notifyMonitoringDyldMain();
// find entry point for main executable
// 第九步 查找入口点并返回
result = (uintptr_t)sMainExecutable->getThreadPC();
if ( result != 0 ) {
// main executable uses LC_MAIN, needs to return to glue in libdyld.dylib
if ( (gLibSystemHelpers != NULL) && (gLibSystemHelpers->version >= 9) )
*startGlue = (uintptr_t)gLibSystemHelpers->startGlueToCallExit;
else
halt("libdyld.dylib support not present for LC_MAIN");
}
else {
// main executable uses LC_UNIXTHREAD, dyld needs to let "start" in program set up for main()
result = (uintptr_t)sMainExecutable->getMain();
*startGlue = 0;
}
}
catch(const char* message) {
syncAllImages();
halt(message);
}
catch(...) {
dyld::log("dyld: launch failed\n");
}
...
return result;
}
复制代码
折叠开dyld main函数,步骤总结如下
上面仅仅是自身的抽出来的。下面再画一张流程图,帮助大家理解。
讲述了MachO文件和Dyld动态链接库知识后,开始进入到今天的真正主题-启动优化。
三、iOS启动流程
App总启动时间 = pre-main耗时 + main耗时
iOS程序的启动可分为pre-main()阶段和main()阶段。
- pre-main阶段
- 通过递归调用加载所有依赖的Mach-O文件
- 加载动态链接库dyld
- 加载类扩展方法【Category】
- 调用objc的load函数,c++静态对象加载
- 执行声明attribute(constructor)函数
如下图:
- main
- 调用main()
- 调用UIApplicationMain()
-
调用applicationWillFinishLaunching
四、启动优化
4.1 pre-main阶段优化
通过上面的Dyld的讲述,了解到可以从如下地方优化pre-main阶段
- 删除无用的代码【未被使用的静态变量、类与方法】- 使用AppCode【对工程进行扫描,百度可以直接搜索Appcode,在这不做讲述】
- 未使用的本地变量
- 未使用的参数
-
为使用的值
- +load方法优化
+load方法做的事情延迟到+initialize中,或者在+load方法中不要做耗时的操作,删除不必要的load方法
我们可以Xcode工具Instrument来统计启动所有的+load方法,以及耗时时间。
下面有一张更能反映的图:
那么可以通过什么方式来优化+load方法呢? - 通过****__attribute优化+load方法
因为工程项目中存在很多的load方法,而一大部分是针对cell的,每一个cell对应着一个模板,而每一个模板对应着一个字符串。类似于字典的key-value方式。
此时可以尝试使用上面Mach-O文件结构中的Data段,使用__attribute((used, section("__DATA,"#sectname" ")))的方式在编译的时候写入"TempSection"的DATA段一个字符串。而这个字符串为key:value格式的字典转json分别对应着key和value。
#ifndef ZXYStoreListTemplateSectionName#define ZXYStoreListTemplateSectionName "ZYTempSection"#endif
#define ZXYStoreListTemplateDATA(sectname) __attribute((used, section("__DATA,"#sectname" ")))
#define ZXYStoreListTemplateRegister(templatename,templateclass) \
class NSObject; char * k##templatename##_register ZXYStoreListTemplateDATA(ZXYTempSection) = "{ \""#templatename"\" : \""#templateclass"\"}";
/**
通过ZXYStoreListTemplateRegister(key,classname)注册处理模板的类名(类必须是ZXYStoreListBaseTemplate子类)
【注意事项】
该方式通过__attribute属性在编译期间绑定注册信息,运行时读取速度快,注册信息在首次触发调用时读取,不影响pre-main时间
该方式注册时‘key’字段中不支持除下划线'_'以外的符号
【使用示例】
注册处理模板的类名:@ZYStoreListTemplateRegister(baseTemp,ZYStoreListBaseTemplate)
**/
复制代码
这样就可以优化大量的重复+load方法。并且使用__attribute属性为编译期间绑定注册信息,到了运行时读取速度快的优点,注册信息在首次触发调用时读取,不会影响pre-main时间。
- 减少不必要的framework【优化存在的framework】
对于pre-main阶段,抖音分享了二进制重排的方案,可以帮助提升启动速度15%,二进制重排的方案就不说了,可以参考文章mp.weixin.qq.com/s/Drmmx5Jtj…及juejin.im/post/684490…
4.2 main阶段优化
这一阶段主要是main函数开始到第一个界面渲染完成这段时间,优化出发点就是减少main函数开始到第一个界面出现的时间。
4.2.1 didFinishLaunchingWithOptions
- 配置App需要的环境
- 第三方SDK的集成【推送,卖点、支付等】
- 日志等
有时候可以采用懒加载操作,以及采取异步的方式加载方法,防止串行操作耗时。
可以通过Instruments的TimeProfile来统计启动的耗时的操作 ,Call Tree->Hide System Libraries过滤掉系统库可以查看主线程下方法的耗时。
也可以通过打印时间来判断函数的耗时操作
double launchTime = CFAbsoluteTimeGetCurrent();
[SDWebImageManager sharedManager];
NSLog(@"launchTime = %f秒", CFAbsoluteTimeGetCurrent() - launchTime);
复制代码
确认哪些是可以延迟加载的,哪些可以放在子线程加载,以及哪些是可以懒加载处理的。
4.2.2 首页的优化
-
很多App启动并不是一下子并不是直接进入首页,而是需要向用户展示一小段的闪屏页。因为当一个App比较复杂的时候,启动时首次构建App的UI就是一个比较耗时的过程,假定这个时间是0.3秒,如果我们是先构建首页UI,然后再在Window上加上这个闪屏页,那么冷启动时,App就会实实在在地卡住0.2秒,但是如果我们是先把闪屏页作为App的RootViewController,那么这个构建过程就会很快。因为闪屏页只有一个简单的ImageView,而这个ImageView则会向用户展示一小段时间,这时我们就可以利用这一段时间来构建首页UI了,一举两得。如下图:
-
不使用xib或者storyboard,直接使用代码
-
对于viewDidLoad以及viewWillAppear方法中尽量少做,晚做或者用异步的方式去做
本文更多是从优化的方法理论上阐述优化过程。