1、APP启动
1.1、APP启动为什么这么重要
- App 启动是和用户的第一个交互过程,所以要尽量缩短这个过程的时间,给用户一个良好的第一印象
- 启动代表了你的代码的整体性能,如果启动的性能不好,其他部分的性能可能也不会太好
- 启动会占用 CPU 和内存,从而影响系统性能和电池
1.2、启动类型
-
Cold Launch 也就是冷启动,冷启动需要满足以下几个条件:
- 重启之后
- App 不在内存中
- 没有相关的进程存在
-
Warm Launch 也就是热启动,热启动需要满足以下几个条件:
- App 刚被终止
- App 还没完全从内存中移除
- 没有相关的进程存在
-
Resume Launch 指的是被挂起的 App 继续的过程,需要满足以下几个条件:
- App 被挂起
- App 还全部都在内存中
- 还存在相关的进程
1.4、App 启动阶段
App 启动分为三个阶段
- 初始化 App 的准备工作
- 绘制第一帧 App 的准备工作及绘制(这里的第一帧并不是获取到数据之后的第一帧,可以是一张占位视图),这时候用户与App已经可以交互了,比如 tabbar 切换
- 获取到页面的所有数据之后的完整的绘制第一帧页面
在这个地方,苹果再次强调了一下,建议「用户从点击 App 图标到可以再次交互,也就是第二阶段结束」的时间最好在 400ms 以内。目前来看,大部分 App 都没有达到这个目标。
下面我们把上面的三个阶段分成下面6个部分,讲一下这几个阶段做了什么以及有什么可以优化的地方
15.png1.4.1、System Interface
初始化APP的准备工作,系统主要做了两件事:Load dylibs 和 libSystem init
在Load dylibs 阶段,开发者还可以做一下优化:
- 避免连接无用的framworks,在Xcode中检查一下项目中「Linked Frameworks and Librares」部分是否有无用的连接
- 避免在启动时加载动态库,将项目的Pods以静态编译的方式打包,尤其是Swift项目,这地方时间损耗是很大的
- 硬链接你的依赖库,这里做了缓存优化
LibSystem init 部分,主要是加载一些优先级比较低的系统组件,这部分时间是一个固定的成本,所以我们开发人员不需要关心
Static Runtime Initalization
这个阶段主要是OC和Swift Runtime的初始化时间,会调用所有的 +load 方法,将类的信息注册到Runtime中
在这个阶段原则上不建议卡发着做任何事情,所以为了避免一些启动时间的损耗,你可以做一下几个事情:
- 在Frameworks 开发时,公用专用的初始化API
- 减少在 +load 中做事情
- 使用 initialize进行来加载初始化工作
1.4.2、UIKit Initalization
这个阶段主要做了两件事情:
- 实例化 UIApplication和UIApplicationDelegate
- 开始事件处理和系统集成
所以这个阶段的优化也比较简单,你需要做两件事:
- 最大限度的减少UIApplication子类初始化时候的工作
- 减少UIApplicationDelegate的初始化工作
1.4.3、Application Initialization
这个阶段主要是生命周期方法回调,也正是开发者熟悉的部分
调用UIApplicationDelegate的APP生命周期方法:
application:willFinishLaunchingWithOptions:
application:didFinishLaunchingWithOptions:
和 UIApplicationDelegate 的 UI 生命周期方法:
applicationDidBecomeActive:
同时,iOS 13 针对 UISceneDelegate 增加了新的回调:
scene:willConnectToSession:options:
sceneWillEnterForeground:
sceneDidBecomeActive:
也会在这个阶段调用。感兴趣的可以关注一下 Getting the Most out of Multitasking 这个 Session,暂时没有视频资源,怀疑是现场演示翻车了,所以没有把视频资源放出来。
在这个阶段,开发者可以做的优化:
- 推迟和启动时无关的工作
- Senens 之间共享资源
1.4.4、Fisrt Frame Render
这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:
loadView
viewDidLoad
layoutSubviews
在这个阶段,开发者可以做的优化:
- 减少视图层级,懒加载一些不需要的视图
- 优化布局,减少约束
1.4.5、Extend
大部分 App 都会通过异步的方式获取数据,并最终呈现给用户。我们把这一部分称为 Extend。
2、动态库转静态库
苹果建议将应用程序的总启动时间设定在400毫秒以下,并且我们必须在20秒之内完成启动,否则系统会杀死我们的应用程序。我们可以尽量优化应用main函数到didFinishLaunchingWithOptions的时间,但如何调试在调用代码之前发生的启动速度慢的情况呢?
1.1、Pre-main时间的查看
在系统执行应用程序的main函数并调用应用程序委托函数(applicationWillFinishLaunching)之前,会发生很多事情。我们可以将DYLD_PRINT_STATISTICS环境变量添加到项目scheme中。
1.png优化前
Total pre-main time: 2.0 seconds (100.0%)
dylib loading time: 1.7 seconds (84.3%)
rebase/binding time: 35.25 milliseconds (1.7%)
ObjC setup time: 40.95 milliseconds (1.9%)
initializer time: 244.57 milliseconds (11.9%)
slowest intializers :
libSystem.B.dylib : 12.97 milliseconds (0.6%)
Alamofire : 106.12 milliseconds (5.1%)
优化后
Total pre-main time: 1.2 seconds (100.0%)
dylib loading time: 1.1 seconds (89.1%)
rebase/binding time: 23.51 milliseconds (1.8%)
ObjC setup time: 25.41 milliseconds (1.9%)
initializer time: 91.64 milliseconds (7.0%)
slowest intializers :
libSystem.B.dylib : 7.15 milliseconds (0.5%)
FellorliSwift : 35.71 milliseconds (2.7%)
这是我使用iPhone 5c的运行结果 ,这只是通过staticlib优化从启动2秒时间降低到1.2秒。这里讲一下各部分的作用
注意:如果你要测试应用的最慢启动时间,记得使用你支持的最慢的设备来进行测试。
输出显示系统调用应用程序main时所用的总时间,然后是主要步骤的分解。
WWDC 2016 Session 406优化应用程序启动时间详细介绍了每个步骤以及改进时间的提示,以下是简要的总结说明:
• dylib loading time 动态加载程序查找并读取应用程序使用的依赖动态库。每个库本身都可能有依赖项。虽然苹果系统框架的加载是高度优化的,但加载嵌入式框架可能会很耗时。为了加快动态库的加载速度,苹果建议您使用更少的动态库,或者考虑合并它们。
* 建议的目标是六个额外的(非系统)框架。
• Rebase/binding time 修正调整镜像内的指针(重新调整)和设置指向图像外符号的指针(绑定)。为了加快重新定位/绑定时间,我们需要更少的指针修复。
* 如果有大量(大的是20000)Objective-C类、选择器和类别的应用程序可以增加800ms的启动时间。
* 如果应用程序使用C++代码,那么使用更少的虚拟函数。
* 使用Swift结构体通常也更快。
• ObjC setup time Objective-C运行时需要进行设置类、类别和选择器注册。我们对重新定位绑定时间所做的任何改进也将优化这个设置时间。
• initializer time 运行初始化程序。如果使用了Objective-C的 +load
方法,请将其替换为 +initialize
方法。
在系统调用main之后,main将依次调用UIApplicationMain和应用程序委托方法。
1.2、动态库与静态库
1.2.1、动态库
我们先来看看工程里面有多少动态库:
• 在项目的Product文件夹找到我们的工程.app文件,右键选择Show in Finder。
• 来到相应目录后右键选择显示包内容。
• 找到Frameworks文件夹,打开。
• 项目是纯Swift编写,下面都是系统Swift库,我们没法优化,可以不管。
image-20201026163408822.png1.2.2、静态库
在Pod的工程中,选择我们使用的库,然后点击Build Setting,搜索或者找到Mach-O Type设置,修改Mach-O Type为static Library
image-20201026163814646.png按照上面的步骤,把我们的动态库都转化成静态库,先执行一次Clean Build Folder ,然后重新构建一次
image-20201026164051274.png这里保留了连个OC的库
1.2.3、遇到的坑
2.png其实这里是CocoaPods的一个配置问题,CocoaPods会在项目中的Build Phases添加一个[CP] Embed Pods Frameworks执行脚本
"${PODS_ROOT}/Target Support Files/Pods-项目名/Pods-项目名-frameworks.sh"
我们在执行pod install后会生成一个Pods-项目名-frameworks.sh的脚本文件。由于我们是手动修改的Mach-O Type类型,这个脚本中的install_framework仍然会执行,所以我们要把转换成静态库的这些库从Pods-项目名-frameworks.sh文件中删除。
我们先看一下install_framework 到底干了啥
# Copies and strips a vendored framework
install_framework()
{
# 设置source变量,三方库构建之后的路径
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
local source="${BUILT_PRODUCTS_DIR}/$1"
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
elif [ -r "$1" ]; then
local source="$1"
fi
# 设置destination变量,三方库需要移动到的路径
local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}"
# 判断source是否为链接文件,需要指向原来的文件
if [ -L "${source}" ]; then
echo "Symlinked..."
source="$(readlink "${source}")"
fi
# rsync --delete无差异同步,可以简单理解为网盘同步,或者复制
# 想详细了解rsync,可以在命令行中输入man rsync
# 这里相当于把source的文件(文件夹)同步到destination
# 即把*.framework复制到Frameworks文件夹下
# Use filter instead of exclude so missing patterns don't throw errors.
echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\""
rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}"
# 下面是找到二进制文件,即framework的Mach-O
local basename
basename="$(basename -s .framework "$1")"
binary="${destination}/${basename}.framework/${basename}"
if ! [ -r "$binary" ]; then
binary="${destination}/${basename}"
elif [ -L "${binary}" ]; then
echo "Destination binary is symlinked..."
dirname="$(dirname "${binary}")"
binary="${dirname}/$(readlink "${binary}")"
fi
# 去掉无效的架构
# Strip invalid architectures so "fat" simulator / device frameworks work on device
if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then
strip_invalid_archs "$binary"
fi
# 进行代码签名
# Resign the code if required by the build settings to avoid unstable apps
code_sign_if_enabled "${destination}/$(basename "$1")"
# Swift的运行时库,Xcode 7之后就用不到了,可以不管
# Embed linked Swift runtime libraries. No longer necessary as of Xcode 7.
if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then
local swift_runtime_libs
swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u)
for lib in $swift_runtime_libs; do
echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\""
rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}"
code_sign_if_enabled "${destination}/${lib}"
done
fi
}
install_framework是把构建好的 *.framework
包复制到App的Frameworks文件夹下
出现上面的报错就是因为资源没有从 *.framework中转移到App中。
解决办法:
既然现在拿到的Bundle是Main Bundle,我们构建之后利用脚本把资源拷贝到APP文件夹不就好了
install_framework_bundle()
{
# 设置source变量,三方库构建之后的路径
if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then
local source="${BUILT_PRODUCTS_DIR}/$1"
elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then
local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")"
elif [ -r "$1" ]; then
local source="$1"
fi
# 设置destination变量,三方库需要移动到的路径
local destination="${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}"
# 遍历framework下的文件,找到bundle和图片,有其他资源自己改一下
for filename in `ls ${source} | grep ".*\.bundle\|.*\.jpg\|.*\.jpeg\|.*\.png"`
do
full_path=${source}/${filename}
# 把资源同步到Main Bundle中
rsync -abrv --suffix .conflict "${full_path}" "${destination}"
done
}
现在我们的操作就是把被静态化的三方库从install_framework方法改为install_framework_bundle:
if [[ "$CONFIGURATION" == "Debug" ]]; then
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Hero/Hero.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MQTTClient/MQTTClient.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
fi
if [[ "$CONFIGURATION" == "Release" ]]; then
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Alamofire/Alamofire.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/HandyJSON/HandyJSON.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Hero/Hero.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/IQKeyboardManagerSwift/IQKeyboardManagerSwift.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MJRefresh/MJRefresh.framework"
install_framework "${BUILT_PRODUCTS_DIR}/MQTTClient/MQTTClient.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/PKHUD/PKHUD.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxAlamofire/RxAlamofire.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxCocoa/RxCocoa.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxRelay/RxRelay.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/RxSwift/RxSwift.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/SnapKit/SnapKit.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/SwiftyUserDefaults/SwiftyUserDefaults.framework"
install_framework_bundle "${BUILT_PRODUCTS_DIR}/Toast-Swift/Toast_Swift.framework"
fi
3、修改Mach-O Type到底改变了什么
3.pngPodfile文件中配置了use_frameworks!,然后进行pod install,这样生成的就是动态库。
首先,看一下这个库的Mach-O Type是动态库
4.png执行⌘+B构建之后,我们还是来到Products文件中的app:
5.png在生成的Demo.app文件包上面点右键,选择显示包内容:
6.png打开Framewoks文件夹,我们可以看到里面有我们创建的两个动态Pod1.framework和Pod2.framework。文件夹里面有代码签名、资源、Info.plist、Pod1(Mach-O)、bundle。
也就是说,如果我们使用的是动态库,在Framewoks文件夹就会看到它的身影,同时主工程的Mach-O文件中是没有相关的代码的。
7.png下面我们修改Build Settings中的Mach-O Type,将其设置为静态库Static Library。
8.png和上面一样我们这边直接替换Pods-Demo-frameworks.sh中install_framework:
image-20201026170729212.png我们看到我们在两个库中创建的类Pod1Object和Pod2Object来到了主工程的Mach-O文件中!
现在应该明白了:
• 动态库会和主工程的Mach-O分开存放。
• 静态库会和主工程的Mach-O合并在一起
4、静态库带来的问题
我们看到我们在两个库中创建的类Pod1Object和Pod2Object来到了主工程的Mach-O文件中!
现在应该明白了:
• 动态库会和主工程的Mach-O分开存放。
• 静态库会和主工程的Mach-O合并在一起
4.1、符号冲突
回顾下 -ObjC 、 -all_load 、-force_load这三个flag的区别:
• -ObjC 链接器会加载静态库中所有的Objective-C类和Category;(导致可执行文件变大)
• -all_load 链接器会加载静态库中所有的Objective-C类和Category(这里和上面一样);当静态库只有Category时 -ObjC会失效,需要使用这个flag;
• -force_load 加载特定静态库的全部类,与 -all_load类似但是只限定于特定静态库,所以 -force_load需要指定静态库;当两个静态库存在同样的符号时,使用 -all_load会出现 duplicate symbol的错误,此时可以根据情况选择将其中一个库 -force_load。
我们在Pod1库中复制一份Pod2Object.{h,m},同时在Build Settings中的Other Linker Flags中添加 -all_load。
9.png先执行Clean Build Folder(或⇧+⌘+K),然后再⌘+B进行构建,这时就会出现duplicate symbols报错:
10.png11.png解决办法:
任意一个或者都不使用静态库。虽然这么说,其实这也是不安全的。如果能改名字就改一下吧。
4.2、Bundle的获取
我们在Pod1Object和Pod2Object中添加以下方法:
- (nullable NSBundle *)getBundle {
return [NSBundle bundleForClass:[self class]];
}
再在主工程的ViewController中添加:
- (void)viewDidLoad {
[super viewDidLoad];
NSBundle *main = [NSBundle mainBundle];
NSBundle *pod1 = [[Pod1Object new] getBundle];
NSBundle *pod2 = [[Pod2Object new] getBundle];
NSLog(@"%@", main);
NSLog(@"%@", pod1);
NSLog(@"%@", pod2);
}
我们先看一下动态库的情况:
12.png我们看到Main Bundle是我们的App,而我们的Pod1 Bundle和Pod2 Bundle分别是其对应的framework,类似于它们有自己的沙盒。
我们再来看看静态库:
13.png可以看到3个Bundle都变成了我们的Main Bundle!
这是因为静态库被合并到了主工程Mach-O文件中:
[NSBundle bundleForClass:[self class]];
[self class]
现在在主工程的Mach-O中,那么上面找到的自然是主工程的Bundle,即Main Bundle。
这个问题解决起来比符号冲突简单一些,但解决这个问题前,我要先讲一下CocoaPods。
5、动态库和静态库的选择
14.png参考资料
[1] WWDC 2019 keynote: https://developer.apple.com/videos/play/wwdc2019/101/
[2] WWDC2019 - 423 - Optimizing App Launch: https://developer.apple.com/videos/play/wwdc2019/423/
[3] dyld启动流程: https://leylfl.github.io/2018/05/28/dyld启动流程/
[4]WWDC2017 - 413 - App Startup Time: Past, Present, and Future: https://developer.apple.com/videos/play/wwdc2017/413/
[5] Static linking vs dyld3: https://allegro.tech/2018/05/Static-linking-vs-dyld3.html
[6] WWDC2018 - 220 - High Performance Auto Layout: https://developer.apple.com/videos/play/wwdc2018/220/
[7] WWDC2019 - 417 - Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/
[8] WWDC2017 - 706 - Modernizing Grand Central Dispatch Usage: https://developer.apple.com/videos/play/wwdc2017/706/
[9] The Talk Show Live From WWDC 2019, With Craig Federighi and Greg Joswiak: https://daringfireball.net/2019/06/the_talk_show_live_from_wwdc_2019
[10] MetricKit: https://developer.apple.com/documentation/metrickit
[11 ]WWDC2019 - 417 -Improving Battery Life and Performance: https://developer.apple.com/videos/play/wwdc2019/417/
网友评论