从探索到实践,iOS动态库懒加载实录

收益可以分为2部分,一个是APP更新大小的减少,另一个是启动耗时的减少。

• APP更新大小的减少 很遗憾,由于只有App Store包才具备diff下载的能力,通过TF包没法检测,因此这部分数据很难量化。并且,App Store在下载时会对下载数据进行压缩,可能单台设备200MB左右的安装大小,其实下载大小还不到100MB,各个机型下载大小的数据可以在App Store后台看到。

• 启动优化量化 启动优化量化这块还是有现成方案可以借鉴的,但是我们最终还是使用instrument来进行统计。之所以使用instrument有以下两点原因:


1)、我们不需要运行期间收集数据, 开发阶段单台设备采集数据即可。
(2)、rebase 和 bind的优化时间目前通过代码还监控不到

在这里提一个小问题,大家都知道dyld会先加载可执行程序所依赖的多个动态库,程序在启动时是加载完一个动态库后立即调用这个库中的load方法吗?

之所以提出这个问题是因为最开始我们想通过代码拿到动态库从在load方法调用前的耗时。最开始选择的技术方案是通过:

_dyld_register_func_for_add_image

注册image的加载时间,当系统加载image时会将事件回调给我们。那么我们在回调开始时打点计时,当连续两个回调打点时,其时间差即可认为是这个动态库的加载耗时。但是在实践时发现,当我们进行注册时,回调函数密集回调,时间间隔极其短,这是因为_dyld_register_func_for_add_image在注册时会将已经加载的镜像一并返回。原因是image在加载后并不是同步立即加载这个库中的所有load方法。如何论证呢?创建一个哨兵动态库,让这个动态库最先参与链接。在哨兵动态库的类的load方法中注册_dyld_register_func_for_add_image回调。运行后发现,在执行哨兵库的类的load方法执行时,_dyld_register_func_for_add_image会同步执行多次回调,获取image的名字会发现我们自定义的动态库都已经回调,只是对应的动态库的load方法都还没有执行。因此通过_dyld_register_func_for_add_image无法获取到每个动态库的加载时间。从dyld的源码中也可以看出来dyld是先通知runtime让动态库先初始化,然后再让主应用进行初始化。

void initializeMainExecutable()
{
// record that we\'ve reached this step
gLinkContext.startedInitializingMainExecutable = true;


// run initialzers for any inserted dylibs
ImageLoader::InitializerTimingList initializerTimes[allImagesCount()];
initializerTimes[0].count = 0;
const size_t rootCount = sImageRoots.size();
if ( rootCount > 1 ) {
for(size_t i=1; i < rootCount; ++i) {
sImageRoots[i]->runInitializers(gLinkContext, initializerTimes[0]);
}
}

// run initializers for main executable and everything it brings up
sMainExecutable->runInitializers(gLinkContext, initializerTimes[0]);

// register cxa_atexit() handler to run static terminators in all loaded images when this process exits
if ( gLibSystemHelpers != NULL )
(*gLibSystemHelpers->cxa_atexit)(&runAllStaticTerminators, NULL, NULL);


// dump info if requested
if ( sEnv.DYLD_PRINT_STATISTICS )
ImageLoader::printStatistics((unsigned int)allImagesCount(), initializerTimes[0]);
if ( sEnv.DYLD_PRINT_STATISTICS_DETAILS )
ImageLoaderMachO::printStatisticsDetails((unsigned int)allImagesCount(), initializerTimes[0]);
}

动态库懒加载优化主要在三个方面:

• 
将代码从可执行文件中剥离出来,减少了可执行文件rebase 和 bind的时间;

• 懒加载动态库的load、contructor函数、静态变量初始化的时机延后,在dlopen后调用;

• 还有隐藏的一块是唯一被该SDK依赖的系统动态库可能由于我们懒加载相应的SDK后也被间接懒加载。

其实减少系统动态库的依赖数量要比单纯的SDK动态库懒加载要更为有效。如果要做到减少对系统库的依赖,首先我们就要确定哪些SDK或代码依赖了哪些系统库,如果不太复杂那么可以考虑将其处理成动态库进行懒加载。这一步可能还需要跟业务结合做评估。在启动优化方面,这也是个值得努力的方向。

总结