iOS 之 Instruments 交互

Instruments 本身作为官方性能监控和交互工具,其全面性和可靠性是很优秀的。
很多开发者也是以此作为顺手的 app 体验优化工具。
本文探索一种方案。基于 iOS 打包流程之后,串联基于 Instruments 调用的性能测试统计。

美好的理想是这样的:

  1. 开发提交新的 app 版本,触发性能测试自动化 job。
  2. 该 job 把指定分支的源码进行编译打包,然后输出 dSYM 和 app 文件(二者包含在 xcarchive 文件中有一份)。
  3. 命令行:将 app 安装至 iPhone,驱动 Instruments 启动指定项目的性能测试,重复若干次,输出 trace 记录文件。
  4. 编写程序解析 trace 文件,分析关注的指标并统计输出成可视化结果,从而实现无人工参与的性能数据跟踪统计。

前置知识

1. dSYM 文件

全称 debug symbol 文件,即调试符号表文件。
这个文件的作用是,把内存地址等机器信息翻译成对应的类信息,代码位置信息等可读性更好的信息。

说人话:

同样的,Instruments 需要 dSYM 文件来进行类名函数名等信息的解构。

2. uuid & udid

uuid (Universally Unique Identifier): 通用唯一识别码

udid (Unique Device Identifier): 设备唯一识别码

识别 iPhone 的码就是 udid 喽,因为 iPhone 是个设备。

想象一下:
一本四年级上的语文书的笔记怎么能对应到四年级下的语文书上去理解呢。
所以 app 的 crashLog、性能数据都需要对应配套的 dSYM 文件来解构。
如何保证(检查)用于性能测试的 app 文件和 dSYM 文件是配套生成的?
使用者两个文件的 uuid 来验证是不是同一次打包的结果即可。

3. arm64/armv7 都是什么东东

armv7 / armv7s / arm64 是 arm 处理器的指令集
i386 / x86_64 是 Mac 或者说电脑处理器的指令集

iPhone 5s(含)以及之后的设备就都是 arm64 指令集了
在 Xcode 打包的配置中可以根据项目特点和需求来指定只编译某种指定指令集的结果,可以加快打包速度。

Xcode 项目设置中指令集相关的 build 选项

Architectures

指定工程被编译成可支持哪些指令集类型。

Valid Architectures

就是再次限制 Xcode 编译出来的二进制包的最终类型。

最终编译结果是哪些指令集,取 Architectures 与 Valid Architectures 的交集。


实践过程

一、打包并获取结果文件

打包过程这里就不细说了:
使用公司已有打包流程改造即可。网上的方案也比比皆是。
注意导出命令:

xcodebuild -exportArchive -archivePath /Users/mac/Desktop/xxx.xcarchive -exportOptionsPlist ${exportOptionsPlist} -exportPath ${yourPath} -allowProvisioningUpdates

其中 -archivePath 参数将 xcarchive 文件放到自己能找到的位置。

xcarchive 在 macOS 上看上去像一个文件,在 Linux 系统上则显示为文件夹。其实就是一种压缩包形式。
看下 xcarchive 文件的结构:

我们需要这一对 myDemo.app (.app 文件名在 macOS 上被省略了) 和 myDemo.app.dSYM 文件。

查看下 uuid:

dwarfdump --uuid myDemo.app/myDemo
dwarfdump --uuid myDemo.app.dSYM

输出:

UUID: 775498CF-17B6-3FD3-A51D-1AD4FA453A9B (armv7) myDemo.app/myDemo
UUID: 6ED57B46-DC49-37E9-9DAB-93AE2428D8CC (arm64) myDemo.app/myDemo

可以看到有两个 uuid 的取值,分别标注了 armv7 和 arm64。说明我们的编译参数设置生成了两种 CPU 指令集的编译结果。
打个比方 iPhone Xs 使用了 arm64,那么安装的时候就使用 arm64 结果,同样也能用 uuid 一致的 arm64 版本对应的 dSYM 文件来解构。

udid 的变化

iPhone Xs 系列的 udid :

00008020-000530CC1E90103A

之前的 iPhone 的 udid :

5d63296476926fad50f5c5724969ec6161a71ee0

可以看到格式是发生了明显变化的,然而这与 iOS 的系统版本没关系。

二、命令行驱动 iPhone 安装包

使用了 ilibmobiledevice

A cross-platform protocol library to communicate with iOS devices

它可以集成到命令行中与 iPhone 通讯,获得已安装的包信息、进行 app 安装卸载等操作。
安装:

brew install -HEAD libimobiledevice

使用:

ideviceinstaller -u [udid] -i [xxx.ipa] # 给指定连接的设备安装应用

如果是 iPhone Xs 这种新的 udid 格式,很不幸会报 udid 不合法的错误,等待更新吧。

ideviceinstaller -i xxx.app

三、Instruments 的命令行交互

查看已安装的设备列表

instruments -s devices

根据 uuid 查找 dSYM 文件

mdfind "com_apple_xcode_dsym_uuids == DD7A6D50-0486-39A1-9E0C-73439450360A"

使用指定模板启动 Instruments 性能测试

命令行调起 macOS 的 QQ 应用进程进行 Instruments 测试,通过!

instruments -v -t "Time Profiler" /Applications/QQ.app

此时没有指定设备参数,默认的是当前执行命令行所在的 Mac 设备。

Shell 命令的参数顺序问题

instruments -v -t "Time Profiler" -w 00008020-000530CC1E90003A -l 12000 myDemo.app

此次运行结束,会根据系统默认命名规则,在当前文件夹产生结果 trace 文件:

四、结果文件查看

直接双击 trace 文件会被 Instruments 程序识别并打开。
如图:

使用 Filter 可以方便地筛选出想要关注的内容,比如 didFinishLaunchingWithOptions 这个影响启动性能的函数耗时。

五、【重点环节】结果文件的自动解析

自动解析方案 - 站在巨人的肩膀上

对于 trace 文件的解析,这里尝试以下作者的方案:
TraceUtility(点我!点我!)

ps: 此处非常感谢作者的付出,如有侵权请联系我删除。

一个 trace 文件可以包含多个 template 的数据,每个 template 数据又可以是多个 run 的结果。这样就出现了一个树状数据结构。

有兴趣的同学可以阅读代码了解下实现过程。
通过一个递归函数来遍历函数调用树,将所有得到的节点放入一个数组,然后遍历输出自己想要的属性。

编译该项目可以得到可执行文件,见下图:

进入编译出的可执行文件所在的文件目录(右键 -> show in Finder ),通过命令行调用可以拿到 trace 文件的解析结果:

./TraceUtility /Users/lipeng/Downloads/TestCourseLaunch.trace >> TraceResult 2>&1

>> TraceResult 2>&1 表示把标准错误和输出以覆盖的方式写入到 TraceResult 文件中。

经过简单改造,定制了自己需要的输出格式

TraceResult 文件部分内容

UIKitCore -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] 0 ms weight 771 ms 
UIKitCore -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] 0 ms weight 771 ms 
UIKitCore -[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] 0 ms weight 770 ms 
UIKitCore -[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] 0 ms weight 770 ms 
UIKitCore _performActionsWithDelayForTransitionContext 0 ms weight 770 ms 
UIKitCore __125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke 0 ms weight 770 ms 
UIKitCore -[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] 0 ms weight 770 ms 
UIKitCore __82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke 0 ms weight 770 ms 
UIKitCore -[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] 0 ms weight 770 ms 
UIKitCore -[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] 0 ms weight 770 ms 
UIKitCore +[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] 0 ms weight 770 ms 
UIKitCore __111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke 0 ms weight 770 ms 
UIKitCore -[UIApplication _runWithMainScene:transitionContext:completion:] 0 ms weight 770 ms 
UIKitCore -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] 0 ms weight 762 ms 
UIKitCore -[UIApplication _handleDelegateCallbacksWithOptions:isSuspended:restoreState:] 0 ms weight 762 ms 
MyDemoApp -[AppDelegate application:didFinishLaunchingWithOptions:] 0 ms weight 762 ms 
MyDemoApp -[AppDelegate registPush] 0 ms weight  1 ms 
UIKitCore -[UIApplication registerUserNotificationSettings:] 0 ms weight  1 ms 
UserNotifications +[UNUserNotificationCenter currentNotificationCenter] 0 ms weight  1 ms 
libdispatch.dylib _dispatch_once_callout 0 ms weight  1 ms 
libdispatch.dylib _dispatch_client_callout 0 ms weight  1 ms 
UserNotifications __53+[UNUserNotificationCenter currentNotificationCenter]_block_invoke 0 ms weight  1 ms 
CoreServices +[LSBundleProxy bundleProxyForCurrentProcess] 0 ms weight  1 ms 
libdispatch.dylib _dispatch_lane_barrier_sync_invoke_and_complete 0 ms weight  1 ms 
libdispatch.dylib _dispatch_client_callout 0 ms weight  1 ms 
CoreServices __45+[LSBundleProxy bundleProxyForCurrentProcess]_block_invoke_2 0 ms weight  1 ms 
CoreServices -[_LSQueryContext(QueryResolution) resolveQueries:cachingStrategy:error:] 0 ms weight  1 ms 
CoreServices -[_LSQueryContext(Internal) _resolveQueries:cachingStrategy:XPCConnection:error:] 0 ms weight  1 ms 
CoreServices -[_LSXPCQueryResolver _resolveQueries:XPCConnection:error:] 0 ms weight  1 ms 
CoreServices __59-[_LSXPCQueryResolver _resolveQueries:XPCConnection:error:]_block_invoke 0 ms weight  1 ms 
CoreFoundation _CF_forwarding_prep_0 0 ms weight  1 ms 
CoreFoundation ___forwarding___ 0 ms weight  1 ms 

可以看到 [AppDelegate didFinishLaunchingWithOptions] 总耗时 762 ms,在版本迭代的过程当中,不应当给该函数带来明显压力使得耗时增加。通过该方案可以长期监控各函数的性能,从而自动化地完成指定内容的性能测试。

接下来,通过文本处理 awk、sed 等命令来提取所需内容:

grep didFinishLaunchingWithOptions TraceResult | awk -F "weight" '{print $2}'

得到 762 ms 的结果,再集成到 Vue 开发的图表系统中岂不是美哉。

结语

本文举例是启动时间相关监控,如果需要处理其它类型的指标 (启动网络流量、占用内存等),原理类似,欢迎指正以及交流!

番外篇

如何给自己制作一个 mac 命令工具并加入到自带的命令行

准备可执行文件,比如 TraceUtility 工具的编译结果。
假设文件如下图:

export PATH=/Users/lipeng/Documents/InstrumentsTest/:$PATH

保存文件后,在用户目录下执行

source ./bash_profile

使得配置生效,接下来就可以使用自己制作的命令啦

TraceUtility xxx.trace

看着就很舒爽,不用再进入指定目录执行 ./xxx 这样来调用可执行文件了!


↙↙↙阅读原文可查看相关链接,并与作者交流