一、matrix介绍
- Matrix 是一款微信研发并日常使用的应用性能接入框架,支持iOS, macOS和Android。 Matrix 通过接入各种性能监控方案,对性能监控项的异常数据进行采集和分析,输出相应的问题分析、定位与优化建议,从而帮助开发者开发出更高质量的应用。
二、监测范围
- 崩溃
- 卡顿
- 内存溢出
三、包含插件
- WCCrashBlockMonitorPlugin
- 基于 KSCrash 框架开发,具有业界领先的卡顿堆栈捕获能力,同时兼备崩溃捕获能力。
- WCMemoryStatPlugin
- 一款性能优化到极致的爆内存监控工具,能够全面捕获应用爆内存时的内存分配以及调用堆栈情况。
四、特性
- WCCrashBlockMonitorPlugin
- 接入简单,代码无侵入
- 通过检查 Runloop 运行状态判断应用是否卡顿,同时支持 iOS/macOS 平台
- 增加耗时堆栈提取,卡顿线程快照日志中附加最近时间最耗时的主线程堆栈
- WCMemoryStatPlugin
- 在应用运行期间获取对象存活以及相应的堆栈信息,在检测到应用爆内存时进行上报
- 使用平衡二叉树存储存活对象,使用 Hash Table 存储堆栈,将性能优化到极致
- 优势
- 退火算法:避免同一个卡顿写入多个文件的情况;避免检测线程遇到主线程卡死的情况下,不断写线程快照文件。
- 每次子线程检查到主线程卡顿,会先获得主线程的堆栈并保存到内存中(不会直接去获得线程快照保存到文件中);
- 将获得的主线程堆栈与上次卡顿获得的主线程堆栈进行比对:
- 如果堆栈不同,则获得当前的线程快照并写入文件中;
- 如果相同则会跳过,并按照斐波那契数列将检查时间递增直到没有遇到卡顿或者主线程卡顿堆栈不一样。
五、卡顿分析
- 什么是卡顿
- 卡顿就是在应用使用过程中出现界面不响应或者界面渲染粘滞的情况。而应用界面的渲染以及事件响应是在主线程完成的,出现卡顿的原因可以归结为主线程阻塞。
- 卡顿的可能原因
- 主线程在进行大量I/O操作:为了方便代码编写,直接在主线程去写入大量数据;
- 主线程在进行大量计算:代码编写不合理,主线程进行复杂计算;
- 大量UI绘制:界面过于复杂,UI绘制需要大量时间;
- 主线程在等锁:主线程需要获得锁A,但是当前某个子线程持有这个锁A,导致主线程不得不等待子线程完成任务。
- 卡顿监控
- 在 Runloop 的起始最开始和结束最末尾位置添加 Observer,从而获得主线程的开始和结束状态。当主线程的状态运行超过一定阈值则认为主线程卡顿,从而标记为一个卡顿。
- 默认主程序 Runloop 超时的阈值是 2 秒,子线程的检查周期是 1 秒。每隔 1 秒,子线程检查主线程的运行状态;如果检查到主线程 Runloop 运行超过 2 秒则认为是卡顿,并获得当前的线程快照。
- 同时如果cpu使用率超过80%,也会认为是卡顿
- 配置 WCCrashBlockMointorConfig
- appVersion & appShortVersion
- 定制监控返回数据中的版本号,如果不填,返回数据中的版本号会自动从 bundle 中获取。
- enableCrash
- 是否开启崩溃监控,默认值为 YES ;设置为 NO,plugin 将不会具有崩溃捕捉能力。
- enableBlockMonitor
- 是否开启卡顿监控,默认值为 YES ;设置为 NO,plugin 将不会具有卡顿监控能力。
- blockMonitorDelegate
- 实现 blockMonitorDelegate,可以获得卡顿监控过程中返回的事件,从而对卡顿监控有更精确的掌控。
- onHandleSignalCallBack
- 获得崩溃日志之后,通知有崩溃产生。慎用:回调中的运行代码中不要有任何内存操作,不然容易引起再次崩溃。
- 报告类型
- 默认是 EWCCrashBlockReportStrategy_All.
- 设置为 EWCCrashBlockReportStrategy_Auto,崩溃日志在应用下次重启之后会通过 onReportIssue: 回调;卡顿日志在应用每次进前台时,将当天一种卡顿类型的日志通过 onReportIssue: 回调。
- EWCCrashBlockReportStrategy_All
- 设置为 EWCCrashBlockReportStrategy_All,崩溃日志在应用下次重启之后会通过 onReportIssue: 回调;卡顿日志在应用每次进前台时,将全部卡顿日志通过 onReportIssue: 回调。
- EWCCrashBlockReportStrategy_Manual
- 设置为 EWCCrashBlockReportStrategy_Manual,onReportIssue:的回调需要主动进行触发,触发回调接口定义在 WCCrashBlockMonitorPlugin+Upload.h。
- 配置WCBlockMonitorConfiguration
- runloopTimeOut
- 单位是微秒,默认值是 2000000,即 2 秒; 定义卡顿时长,Runloop 阻塞时间超过 runloopTimeOut 即认为是卡顿。
- checkPeriodTime
- 单位是微秒,默认值是 1000000,即 1 秒; 定义子线程检查主线程 Runloop 状态的周期。
- bMainThreadHandle
- 默认值是 NO;耗时堆栈提取功能的开关,设置为 YES 即开启耗时堆栈提取,卡顿监控会定时获取主线程堆栈,获取频率以及保存个数由 perStackInterval 和 mainThreadCount 这两个参数决定。
- perStackInterval
- 单位是微秒,默认值是 50000,即 50 毫秒; 定义获取主线程堆栈的间隔。
- mainThreadCount
- 默认值是 10 个; 定义保存最近主线程堆栈的个数。
- limitCPUPercent
- 默认值是 80; 定义认为单核CPU占用超过多少即认为是CPU过高。设置为 80,那么在双核机器上,当 CPU 占用超过 160% 的时候,卡顿监控就会认为当前CPU过高,然后生成 CPU 过高的线程快照。
- dump_type卡顿类型
- EDumpType_MainThreadBlock = 2001
- 前台主线程卡顿;应用在前台时主线程 Runloop 处理事件超时了。
- EDumpType_BackgroundMainThreadBlock = 2002
- 后台主线程卡顿;应用在后台时主线程 Runloop 处理事件超时了。
- EDumpType_CPUBlock = 2003
- CPU过高堆栈;检测到当前应用占用CPU过高。
- EDumpType_LaunchBlock = 2007
- 启动卡顿;在应用启动 5 秒内,发生的主线程卡顿,归为启动卡顿。
- EDumpType_BlockThreadTooMuch = 2009
- 线程过多卡顿;从前台和后台主线程卡顿中细分出来的卡顿类型,将卡顿时线程数超过64的卡顿归为线程过多卡顿。
- EDumpType_BlockAndBeKilled = 2010
- 卡死卡顿;当应用重启时,检测到上次应用关闭是因为主线程卡顿导致应用被强制关闭,卡顿监控会把应用关闭前的卡顿标记为卡死卡顿。
- EDumpType_PowerConsume = 2011
- 耗电堆栈;检测到应用连续一分钟 CPU 占用超过80%,将一分钟内的耗 CPU 堆栈组合起来成为耗电堆栈。
六、关于内存检测
- 监控范围
- 应用启动后开启监控,直至应用exit
- 对象存储
- 用Hash Table来存储这些堆栈。思路是整个堆栈以链表的方式插入到table里,链表结点存放当前地址和上一个地址所在table的索引。每插入一个地址,先计算它的hash值,作为在table的索引,如果索引对应的slot没有存储数据,就记录这个链表结点;如果有存储数据,并且数据跟链表结点一致,hash命中,继续处理下一个地址;数据不一致,意味着hash冲突,需要重新计算hash值,直到满足存储条件
- 上报对象
- 由于内存监控是存储了当前所有存活对象的内存分配信息,数据量极大,所以当出现FOOM时,不可能全量上报,而是按某些规则有选择性的上报。
- 首先把所有对象按Category进行归类,统计每个Category的对象数和分配内存大小。这列表数据很少,可以做全量上报。接着对Category下所有相同堆栈做合并,计算每种堆栈的对象数和内存大小。对于某些Category,如分配大小TOP N,或者UI相关的(如UIViewController、UIView之类的),它里面分配大小TOP M的堆栈才做上报。
- 上报规则
- UIViewController数量是否异常
- UIView数量是否异常
- UIImage数量是否异常
- 其它Category分配大小是否异常,对象个数是否异常
- 配置WCMemoryStatConfig
- skipMinMallocSize
- 默认值是 PAGE_SIZE(16kb); 对象分配内存大小大于等于 skipMinMallocSize 将会记录跟踪。
- skipMaxStackDepth
- 默认值是 8; 如果分配内存大小小于 skipMinMallocSize,并且获取的堆栈在前 skipMaxStackDepth 个地址里,存在当前应用的符号,那么这次分配事件会被记录跟踪。
六、关于耗电检测
- 触发点
- Matrix 耗电监控在应用启动后开启一个检测子线程,检测线程不断去识别出当前应用哪个线程的CPU占用过高,将耗CPU多的线程的堆栈收集起来。当应用CPU占用达到阈值时,耗电监控将收集到的堆栈组合形成耗电堆栈。
- 范围
- 为了避免获得的堆栈无意义,只获取占用CPU超过 5% 的线程的堆栈
- 耗电堆栈的组成
- 耗电监控默认是当应用平均一分钟内CPU占用超过80%,把循环队列存储的堆栈组合成耗电堆栈。
- 配置
- WCBlockMonitorConfiguration
- bGetPowerConsumeStack 设置为 YES ,即能让应用开启耗电监控
- powerConsumeStackCPULimit 设置应用耗电的 CPU 阈值,默认值为 80%
- 耗电堆栈作为 WCCrashBlockMonitorPlugin 中的一种日志类型进行上报,文件类型2011
七、安装
- CocoaPod安装
- 添加 source 'https://github.com/CocoaPods/Specs.git'
- 添加 pod 'matrix-wechat'
- 执行pod install
- 如果提示no found ,再执行pod repo add master https://github.com/CocoaPods/Specs.git
八、数据解析
- issueTag(问题编号)
- 表明当前 issue 对象属于哪个 plugin;每个 plugin 的 tag 可以通过 +[MatrixPlugin getTag] 获得。
- issueID
- 当前 issue 的唯一标识。
- dataType:数据类型
- 指明当前 issue 返回数据的数据类型,定义在 EMatrixIssueDataType。 如果 dataType 为 EMatrixIssueDataType_Data,数据在 issueData 中; 如果 dataType 为 EMatrixIssueDataType_FilePath,数据在 filePath 对应的文件中。
- reportType
- reportType 是由每个 plugin 自行定义的。
- 在 WCCrashBlockMonitorPlugin 中:
- reportType 为 EMCrashBlockReportType_Crash 时,说明当前 issue 中携带的数据为崩溃日志;
- reportType 为 EMCrashBlockReportType_Lag 时,说明当前 issue 中携带的数据为卡顿日志。
- customInfo
- 某些 issue 可能会携带一些附加信息。
九、解析卡顿和耗电数据
- 卡顿
- 使用matrix/matrix-iOS/Script/ks2apple.py 对 JSON 文件进行解析。
- python2.7 /xx/rizhi/ks2apple.py -i /xx/rizhi/input.json -o /xx/rizhi/input-out.json
- 耗电
- 使用matrix/matrix-iOS/Script/battery2apple.py 对 JSON 文件进行解析。
- python xx/rizhi/battery2apple.py /xx/rizhi/intput.json /xx/rizhi/input-out.json
十、定位crash崩溃点
- crash 对象记录了crash发生时的信息
- threads 包括了crash发生时的堆栈信息
- 我们需要在threads中寻找crashed为true的backtrace对象
- 查找backtrace中的contents对象。找到object-name为项目名称的time,其中symbol_name即为相应的crash方法
十一、解析耗电、卡顿、OMM日志
- xcode安装生成dsym文件
- xCode -> Build Settings -> Code Generation -> Generate Debug Symbols -> Yes
- XCode -> Build Settings -> Build Option -> Debug Information Format -> DWARF with dSYM File
- 获取dysm文件
- 编译,重新运行在真机上
- 项目目录下-> product -> xxxxx.app -> show in finder ,在同级目录下
- atos解析卡顿日志
- 导出所需日志和dsym文件
- atos -o /Users/cqj/Desktop/prase/x x x x x x.app.dSYM /Contents/Resources/DWARF/x x x x x x -arch arm64 -l 0x102af0000 0x0000000102b3a1fc
- 格式 atos -o [dwarf文件地址] -arch arm64 -l [loadAddress] [instructionAddress]
- 如果报错:atos cannot load symbols for the file /Users/cqj/Desktop/prase/x x x x x x.app.dSYM for architecture arm64.
- 进入x x x x x x.app.dSYM -> 显示包内容 -> Contents/Resources/DWARF/, 拷贝 x x x x x x 文件,添加后缀.app.dSYM
- 再执行命令
- 解析成功则出现相应的.m文件和所在行
- __40-[MatrixTester generateMainThreadLagLog]_block_invoke (in x x x x x x.app.dSYM) (MatrixTester.mm:0)
- 卡顿ks2apple.py添加uuid用于比对dsym的uuid,确保日志和dsym文件是相对应的
- ks2apple.py 文件276行添加:headers.append(' app_uuid: {0}'.format(_s('app_uuid')))
- 重新生成文件
- atos解析OMM日志
- 由于matrix在内容日志里面只存储了uuid和offset,没有相对应的loadAddress,使得客户端在解析日志时无法使用atos命令去符号化日志
- 对dyld_image_info_mem进行扩展,新增uint64_t addr属性
- 使用cur_seg_cmd对象获取vmaddr值并对addr属性赋值
- 在生成文件文件的地方写入addr属性
- addr属性 + offset 即为instructionAddress
- 把addr 和 instructionAddress 转成16进制
- 分析
- 查找caller为dysm->uuid 的数据,在查找对应frame下 uuid 相同的数据
- 使用offset + addr 得出 instructionAddress,在转成16进制
- atos -o /Users/cqj/Desktop/prase/MatrixDemo.app.dSYM /Contents/Resources/DWARF/MatrixDemo -arch arm64 -l 0x1000000025 0x1000050fa1
- 得出__32-[TestOOMViewController testOOM]_block_invoke (in MatrixDemo.app.dSYM) (TestOOMViewController.mm:121)
网友评论