iOS 测试 iOS 的 App 启动性能测试自动化 - 排除入侵和宿主模式监控,拥抱原生 Xcode Instruments

李鹏 · August 16, 2019 · Last by 李鹏 replied at September 02, 2019 · 717 hits

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

使用:

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

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

  • 安装 app 文件
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
  • -v 详细输出日志
  • -t template 使用哪个性能测试模板,可以是自定义的模板
  • -w udid, 指定你的 iPhone 即可
  • -l 进行性能监控的时长

此次运行结束,会根据系统默认命名规则,在当前文件夹产生结果 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 这样来调用可执行文件了!

共收到 4 条回复 时间 点赞

这个是电脑上面的应用吧,如果是手机上的app,是怎么命令获取性能数据的

李鹏 #2 · August 29, 2019 作者
TD 回复

就是文中的下一条命令呢,使用-w参数指定iOS设备的uuid呀

在xcode10启动后crash了 “libc++abi.dylib: terminating with uncaught exception of type NSException” 这个错误 楼主遇到过吗?

xinxi 回复

一般就是重启手机并重新插上数据线之类。同时完整退出下xcode或者重启电脑试试

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up