先回答标题
原理是通过减少启动时加载的动态库数量达到目的。那一分钟就可以?是的,因为 CocoaPods
默认是会加载所有的依赖库,可以在一分钟内通过删除 Other Linker Flags
中的链接标记,取消启动时自动加载的动态库。那 300ms 是不是夸张了?没有一点点夸张,不信大家往下看。(文章结尾有惊喜!)
准备工作
如何测量启动时间?
咱们首选还是 instruments
,它的 App Launch 工具 可将启动过程拆解为关键阶段(如 dyld
加载、Runtime
初始化、首帧渲染等),并量化各阶段的耗时。
如图,这里精确的量化了本次程序运行总的时长、加载可执行文件时长(包含主程序、动态连接器、动态库、各种初始化等)、加载三个动态库的运行时长。这里重点关注加载的动态库,它们的特点是描述以 Map Image + <动态库路径>
。
如何知道加载了哪些动态库?
如上图,其中以 Map Image
开头的描述表明这就是一个动态库。dlopen()
函数调用后也会生成一个同样的任务。 除了 instruments
还有其他手段。这里就给出一个笔者常用的一个工具。 新建一个类 LazyLoader
,将以下代码拷贝到全局调用区。
C
// 打印动态库名称(兼容 M1/Mac 和 iOS)
void filterThirdPartyLibs(void) {
uint32_t count = _dyld_image_count();
for (uint32_t i = 0; i < count; i++) {
const char* path = _dyld_get_image_name(i);
if (!strstr(path, "/usr/lib/") &&
!strstr(path, "/System/Library/")) {
NSString* pathStr = [NSString stringWithCString:path encoding:NSUTF8StringEncoding];
NSLog(@"Third-party: %@\n", pathStr.lastPathComponent);
}
}
}
// 在程序启动时自动调用
__attribute__((constructor)) static void runtime_init(void) {
filterThirdPartyLibs();
}
见证奇迹的时刻即将来到,开始发车了。
首先通过 pod
新建一个私有 LoadAFDemo
。
BASH
pod lib create LoadAFDemo
接下来在 podfile
中添加以下依赖,并执行pod install
RUBY
pod 'AFNetworking'
然后将工具 LazyLoader
拖入工程中并进行桥接。现在开始运行项目,可以得到如下结果:
可以看到
AFNetworking
在其中,这里过滤了系统的库,其他两个是主工程相关,因为开发环境被拆分为两个。 这个时候可以看到主工程的 Other Linker Flags
很简单,只有 AFNetworking
。
接下来打开App Launch 工具 查看总的时长 242.06ms, 这个数字很重要,它代表了 pre-main
阶段的启动耗时。
如图,动态库的加载描述都是以
Map image
开头,其中第一个是笔者添加的依赖库 AFNetworking
,下面三个都是系统库的。
- 在 podfile 中添加一些依赖库。
RUBY
pod 'SDWebImage'
pod 'Realm'
pod 'RxTheme','4.1.1'
pod 'RxGesture','3.0.2'
pod 'NSObject+Rx','5.1.0'
pod 'Moya','~> 14.0.0'
pod 'Alamofire', '~> 5.2.1'
pod 'SwiftyJSON','5.0.0'
pod 'R.swift','5.2.2'
pod 'CryptoSwift','1.4.2'
pod 'SnapKit','5.6.0'
pod 'FlexLayout','1.3.20'
pod 'PinLayout','1.9.2'
pod 'SwifterSwift/Foundation','5.2.0'
pod 'SwifterSwift/UIKit','5.2.0'
pod 'WMPageController','2.5.2'
pod 'CYLTabBarController'
pod 'KMNavigationBarTransition'
pod 'Firebase/AnalyticsWithoutAdIdSupport','9.6.0'
pod 'Firebase/Crashlytics','9.6.0'
pod 'FirebaseCoreDiagnostics','9.6.0'
pod 'FirebaseInstallations','9.6.0'
pod 'FirebasePerformance','9.6.0'
pod 'AppsFlyerFramework','6.5.2'
pod 'MoEngage-iOS-SDK','7.1.4'
pod 'MoEngageInApp', '2.1.2'
pod 'MORichNotification','5.2.0'
pod 'ZLPhotoBrowser', '4.2.5'
pod 'FCUUID','1.3.1'
此时运行pod install
再观察,发现刚才添加的依赖库都被添加进来。
接下来运行项目,查看控制台输出:
总计44个三方库加载了,这里只截取部分。
接下来再次打开App Launch 工具 查看总的时长为 1.47s, 比原来多出了近 1.2s。是不是很惊讶,这里的三方库加载耗时基本在 1ms 左右,共 44 个那也最多 100ms 才对。这里先卖个关子,请看接下来的表演。
接下来打开主工程,删除 Other Linker Flags
中的所有内容,再次运行项目。
发现三方库一个也没有加载。再次打开App Launch 工具 查看总的时长为 197.07ms
看到这里的你,是不是惊讶,从 1.47s 到 197.07ms,启动效能完全是飞升了。那这个做法可以直接运用到项目中去吗?接下来通过一些问题来解开谜题。
一些疑点
-
清空
Other Linker Flags
中的依赖后,代码能运行吗?答:能运行,不过相关的三方库会在
pre-main
阶段加载。请看下图,上图中首先
import AFNetworking
库,然后使用AFHTTPSessionManager
获取其实例,且callAF
方法没有任何地方调用,只是定义了。运行项目可以看到,控制台中输出了AFNetworking
,没有编译和运行报错。(如果只是import
是不会触发链接操作的)。这里暂且把这种行为定义为Real Import
。 -
那为什么会这样呢?
其实答案很明显,获取
AFHTTPSessionManager
实例是编译期就确定的行为。而编译没有报错,那英爱是系统在pre-main
阶段通过LC_LOAD_DYLIB
指令加载AFNetworking
。 -
在私有
podspec
中添加了AFNetworking
依赖, 还需要清除私有podspec
中的Other Linker Flags
吗?答:在私有库没有
Real Import
的前提下,不需要清除Other Linker Flags
也可以实现依赖的三方库不被加载。就是说,私有podspec
断开了和主工程链接关系,那么这个AFNetworking
也就没有断开了和主工程的联系,所以即使Other Linker Flags
添加了链接标记,也只是AFNetworking
和当前私有库的。 -
把
AFNetworking
换成Alamofire
也是一样的吗?答:是的
-
没有加载
AFNetworking
的情况下,通过反射(NSClassFromString(@"AFHTTPSessionManager")
)可以拿到实例吗? 答:当然不可以,需要时先动态加载AFNetworking
。 -
说了这么多那
dyld
的加载动态库的原则是什么 ?答:这个问题主要与
Mach-O
的文件结构有关。可以使用otool -l <动态库路径>
查看文件中所以依赖的动态库。主要有两个命令LC_LOAD_DYLIB
和LC_LOAD_WEAK_DYLIB
, 其实上面的问题中提到的Real Import
实际上是执行LC_LOAD_DYLIB
命令将动态库加载到强依赖库列表 中,也就对应在Other Linker Flags
中标记为-framework
的动态库。LC_LOAD_WEAK_DYLIB
命令将动态库加载到弱依赖库列表 中,对应于Other Linker Flags
中的-weak_framework
标记的动态库。而无论是强依赖还是弱依赖,正常情况情况下都会加载。 -
你搞这么麻烦,直接用系统支持的
-weak_framework
标记不就行了? 答:这个笔者经过实测,在目前的系统版本(iOS18)是不行的。经过查阅官方文档发现最后更新日期是2013年,应该是很久没有更新了。而且现在大部分使用的是dyld3
。
惊喜
为了更加直观的观察依赖关系,笔者专门开发了一个脚本工具。优点是:非常直观和方便。下面以咸鱼 iOS 为例。传送门