ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MTHawkeye实战:iOS性能监控与卡顿定位全攻略

MTHawkeye实战:iOS性能监控与卡顿定位全攻略 凡是做iOS性能优化的几乎都有过这种经历线上用户反馈App卡成PPT登录界面转圈十几秒自己拿真机测却一切正常或者好不容易复现了问题打开Instruments正要录制现场已经过去了再加上海量线上设备、不同系统版本、不同网络环境性能问题就像鬼一样你知道它存在但就是抓不住。MTHawkeye就是为解决这类问题而生的。它是美团开源的一款iOS应用性能监控工具我接触它大概在两年前当时团队正被一个诡异的启动卡顿问题折磨了三周最后就是靠它抓到了真凶——一个在后台线程高频读写UserDefaults导致的读写锁竞争。从那以后MTHawkeye就成了我性能排查工具箱里的常备武器。这篇内容适合正在做iOS性能优化、想搞清线上问题根因的开发者。我会从工具的能力边界、集成方式、核心模块用法、以及我在实际项目中踩过的坑四个维度来讲尽量做到讲原理也讲操作让你看完就能上手而不是只收藏一个README链接。1. MTHawkeye的定位比Instruments更适合线上问题排查先说清楚一个很多人没搞明白的事MTHawkeye不是来替代Instruments的它俩的职责完全不同。Instruments是你主动去抓性能问题的工具而MTHawkeye更像是给App装了一台行车记录仪事情发生之后你可以回放当时的现场。1.1 为什么说性能监控不该只依赖Instruments很多团队做性能优化的流程是这样的用户投诉卡顿 → 产品反馈给开发 → 开发跑一遍Instruments找问题 → 找到了就改找不到就说你再试试。这套流程最大的问题是Instruments需要手动连接真机、手动点击录制你拿到的是你希望看到的那段时间的性能数据而不是用户实际遇到问题的那段时间的性能数据。比如线上用户反馈说在朋友圈图片很多的时候滑动很卡但你在办公室用Instruments复现时网络环境是公司Wi-Fi、图片是从CDN命中的缓存、手机上就装了那一两个调试应用性能表现自然完全不同。MTHawkeye的价值在于它常驻在App里按照你配置的规则持续采样把历史数据存下来当用户遇到问题时可以上报数据你拿到的就是真实设备、真实网络、真实操作下的真实表现。1.2 MTHawkeye的核心模块能回答哪些问题MTHawkeye把性能监控拆成了几个可以独立启用的模块我一一说下它们能干什么TimeRecorder耗时记录监控冷启动、页面加载、异步任务等关键路径的耗时能自动记录每个页面的生命周期耗时。EnergyMonitor能耗监控检测异常的CPU占用、频繁的Wakeup、高功耗网络请求等。这类问题用户感知强但很难用Instruments直接定位。MemoryMonitor内存监控记录内存分配、内存增长趋势可以定位大块内存分配、循环引用导致的内存持续增长。NetworkMonitor网络监控如果引入Pinter的代码插桩能记录每个网络请求的URL、耗时、状态码、流量大小对定位弱网问题很有帮助。IOMonitorIO监控检测主线程上的磁盘读写、大文件IO、数据库查询耗时等。ANRMonitor / Watchdog卡顿监控监控主线程卡顿记录卡顿时的调用栈。这是我最常用的模块后面会细讲。这些模块之间相互独立你在接入时可以根据需要裁剪不需要全部打开这也是MTHawkeye做得好的一点——全是模块化的不会说为了监控网络还得把内存监控一并带上。2. 从零集成MTHawkeyeCocoaPods是首选但有几处坑要先避开MTHawkeye的GitHub仓库提供了两种集成方式CocoaPods和Carthage。我推荐用CocoaPods一是因为国内团队普遍都在用二是MTHawkeye对CocoaPods的支持更完整Carthage方式在某些版本上会有资源文件缺失的问题。2.1 Podfile配置与依赖处理在你的Podfile里加上一行pod MTHawkeye然后执行pod install。这里有个容易踩的坑MTHawkeye依赖了fishhook用于hook系统函数而fishhook本身是很底层的库如果你的项目里其他库也用了fishhook或者你自己也集成了fishhook可能会出现符号冲突。我当时的处理办法是在Podfile里统一指定fishhook版本pod MTHawkeye pod fishhook, 0.2如果你用的是Swift混编项目MTHawkeye本身是OC写的Swift项目直接通过桥接文件引入头文件就能用不需要额外做别的。但注意MTHawkeye的代码是基于OC运行时的一些特性写的如果你的项目开启了严格Swift Concurrency检查可能在编译时会有警告这个不影响运行。2.2 初始化与默认配置的开和关集成之后官方推荐在didFinishLaunchingWithOptions里初始化最简单的方式是#import MTHawkeye/MTHawkeye.h - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [MTHawkeyeClient sharedInstance].isEnabled YES; return YES; }但这里我不建议你直接这样写因为默认把所有模块都开了会有两个副作用一是性能数据量非常大几分钟就能吃掉几十MB存储二是监控本身也会消耗性能导致你在调试时看到的性能数据本身就掺入了监控的干扰。我建议的初始化方式是只开你当前要排查的模块比如我要查网络问题就只开NetworkMonitorMTHawkeyePluginConfig *config [MTHawkeyePluginConfig new]; config.networkMonitorEnabled YES; config.memoryMonitorEnabled NO; config.ioMonitorEnabled NO; config.timeMonitorEnabled NO; config.energyMonitorEnabled NO; config.anrMonitorEnabled YES; [[MTHawkeyeClient sharedInstance] startServer];启动之后MTHawkeye会在App内显示一个浮窗点开浮窗就能看到实时的性能数据看板包括CPU、内存、网络上行下行速度等。这个浮窗在调试阶段很有用但注意它是不是被隐藏了。如果你在release包上开启一定要加上条件编译避免把调试浮窗暴露给用户。2.3 UIDebug工具不是性能监控别混为一谈MTHawkeye仓库里除了性能监控还带了一套UIDebug工具可以查看视图层级、查看ViewController栈等。这个工具定位是辅助开发调试的不属于性能监控范畴。很多新手第一次接入时以为这个也是性能相关的费半天劲研究怎么用它看内存布局方向就偏了。你在接入时如果只是想做性能监控UI调试工具可以不打开节省编译和启动时间。3. 首次实战一次真实的启动卡顿排查流程理论讲再多不如走一遍实操。我拿一个真实项目的案例来讲这家项目的App启动阶段有接近2秒的卡顿用户反馈集中在小内存设备上中高端设备基本感觉不到。启动卡顿这个问题之所以难排查是因为它发生的时间窗口非常短正常手段很难抓到现场Instruments的Time Profiler能录到现象但很难快速定位到是哪个线程的哪个操作引起的。3.1 用ANRMONITOR抓主线程卡顿现场我接入了MTHawkeye的ANR监控模块它的原理是通过一个独立的监控线程每隔一定时间默认是1秒去检测主线程的状态如果主线程超过阈值默认3秒没有响应就记录当前主线程的调用栈和各个线程的状态快照。关键点在于MTHawkeye记录的不是卡顿开始前的调用栈而是卡顿过程中主线程正在执行的调用栈。它利用信号机制强制获取主线程的当前调用栈即使主线程卡死在某个系统调用里也能把那个调用栈抓出来。配置阈值的方式如下MTHawkeyeANRConfig *anrConfig [MTHawkeyeANRConfig new]; anrConfig.threshold 2.0; // 2秒无响应视为卡顿 anrConfig.belowMainThreadThreshold 0.5; // 判断卡顿时的采样间隔我当时设置了2秒的阈值跑了一下午的复现路径后在浮窗里看到了两次卡顿记录。点进去看调用栈发现主线程卡在了CFPreferencesCopyAppValue上。3.2 顺着调用栈找到真凶调用栈只能告诉你卡死在哪要定位为什么卡死还得往深挖。CFPreferencesCopyAppValue是读取UserDefaults的底层实现这个调用正常情况下是微秒级的为什么会卡到2秒MTHawkeye的调用栈记录里不仅包含主线程的调用栈还包含了各子线程当时的状态。我翻了一下子线程的信息发现有一个后台线程正在频繁调用CFPreferencesSetAppValue。两个线程同时操作Preferences底层会加锁如果写操作太频繁读操作就得等锁等锁时间一长主线程就被卡住了。这个问题的根因是某个数据上报模块在后台线程每500毫秒就写入一次时间戳到UserDefaults用于计算用户停留时长。写频率太高导致主线程读的时候发生锁竞争。解决方案也很直接把高频写入的内存缓存化每5秒批量落盘一次。这类问题用Instruments不是不能定位但需要你在卡顿发生的瞬间恰好开着录制这就很看运气了。MTHawkeye的优势就是把现场留住了你随时可以回看。3.3 卡顿监控带来的额外收获大IO操作在排查过程中我发现MTHawkeye的IO监控也在报警。IO模块监测到主线程有几次超过100ms的磁盘写入操作打开详情一看是某个SDK在初始化时同步写了一个配置文件。这里我要提一个IO监控独有的价值它能区分是主线程发起IO还是子线程IO影响主线程。前者直接产生卡顿后者虽然不在主线程但会引发磁盘带宽竞争。MTHawkeye的IO模块会自动记录这两种情况如果你的App有大量的SQLite读写、CoreData存储、图片缓存写入IO模块值得长期开着因为这类问题往往要等线上设备存储接近满时才会集中爆发开发阶段很难察觉。4. 网络与能耗监控的实用细节从数据到结论网络和能耗这两个模块看似不如卡顿监控刚需但实际用起来它们对线上体验优化帮助非常大。我分别说下使用中的一些心得。4.1 NetworkMonitor的URL过滤与流量统计MTHawkeye的NetworkMonitor在未引入代码插桩的情况下是基于NSURLProtocol或者hook NSURLSession来实现的。具体来说它通过fishhook hook了NSURLConnection和NSURLSession的底层方法所以能拿到每个请求的完整信息不需要你改动现有的网络层代码。开启方式config.networkMonitorEnabled YES;跑起来之后你会在浮窗里看到一个网络列表包含了URL、HTTP方法、状态码、请求耗时、上传下载流量等。这里有一个实用技巧按耗时排序找慢请求按流量排序找浪费流量的请求。我实际遇到过一种情况一个页面在弱网环境下加载特别慢用Charles抓包能看到请求但看不到为什么慢。MTHawkeye的NetworkMonitor能看到每个阶段的时间分解——DNS查询耗时、连接建立耗时、TLS握手耗时、等待响应耗时、下载耗时。一分解就发现大部分时间耗在了TLS握手阶段进一步排查发现是某个海外SDK在每次请求时都重新进行证书校验没有复用会话。这个问题单靠Charles看URL和响应时间是发现不了的。另外NetworkMonitor还支持设置阈值超过阈值会自动记录并触发告警这个在持续集成和夜间回归测试中很有用。4.2 EnergyMonitor的常见误报与降噪处理EnergyMonitor能耗监控是我觉得MTHawkeye里最容易引起误报的模块。它通过检查CPU使用率、Wakeup频率、网络状态切换等方式来评估能耗但实际使用中你会发现很多正常操作也会被标记为异常比如视频播放时CPU持续高占用就是正常的地图导航时的GPS唤醒也是正常的。所以使用EnergyMonitor时建议不要只看它的告警而是把它当成一个热点发现工具结合业务场景去判断。比如它在某个页面大量告警高频率Wakeup你用NetworkMonitor对照发现那个页面有大量轮询请求每隔3秒发起一次这就合理猜测是能耗问题的元凶。实际上能耗监控的典型用途是发现明明屏幕没有操作、用户也没在用App但电量却在消耗的这类问题。MTHawkeye会记录屏幕状态和CPU状态如果屏幕是暗的但CPU还在大量工作说明App在后台有不合理的任务。4.3 数据导出与分析别只盯着浮窗MTHawkeye的数据存在本地的SQLite数据库中浮窗只是查看数据的其中一种方式。它的存储位置在沙盒的Library/MTHawkeye目录下数据文件是SQLite格式。官方提供了从模拟器里直接导出数据的方式也可以用xcrun simctl命令直接把沙盒里的文件拉出来。我通常的流程是在真机上跑一段时间 → 用Xcode的Devices窗口下载容器 → 用SQLite工具打开分析 → 把关键数据导成CSV做趋势图。浮窗适合现场快速看导出分析才是定位问题的关键一步。这里提醒一点因为MTHawkeye开启后每个网络请求、每次内存分配都会被记录下来数据量增长非常快。如果长时间开启建议定期清理数据或者通过配置限制记录的最大条数。我通常在一天的调试结束后清一次数据避免下次调试时数据太杂影响判断。5. 生产环境与调试环境的双轨部署方案很多团队把MTHawkeye当调试工具用完就关了其实它的更大价值在于线上问题收集。但线上和调试场景的需求不同需要一套双轨方案。5.1 调试包全量开启追求信息完整性调试场景下所有模块全开配置为能开则开目标是抓取最完整的现场数据。我常用的调试配置模块状态阈值 / 配置ANR监控开启主线程阈值2s采样间隔0.8s内存监控开启每1分钟记录一次快照IO监控开启主线程IO耗时超过50ms记录网络监控开启记录完整请求详情能耗监控开启默认配置耗时监控开启自动记录启动和各页面加载这种配置下跑个性能测试用例基本任何问题都能留痕。5.2 Release包精准模块采样上报策略线上包如果全量开启一是数据量巨大二是隐私合规风险三是性能开销影响用户体验。所以线上包的策略应该是最小必要采集加触发式上报。我的做法是线上只开ANR监控和内存监控并且ANR阈值调高比如3秒内存监控降低采样频率每5分钟一次。CPU和网络监控默认关闭遇到线上疑难问题时通过远程配置动态开启等收集到数据再关闭。上报策略上不要每一条数据都上传那是自杀式打流量。可以本地存一份当检测到App即将被系统杀掉生命周期回调或者下一次启动时再上传前一次会话的关键数据。上传的时机要选在网络空闲的时候比如App进入后台后延迟几秒再传。5.3 隐私合规的底线问题用MTHawkeye采集数据必须明确它采集的数据类型。网络监控模块会记录完整的URL、请求头、响应体如果配置了这里面很可能包含用户个人信息比如用户手机号、头像URL、地理位置参数等。在发布线上版本时这些数据很可能违反各大应用商店的隐私政策条款。我的处理办法是线上包的NetworkMonitor模块不开启只保留性能相关的聚合数据上报。如果一定要采集网络信息也要在采集前做脱敏处理比如去掉URL中的查询参数、无视请求体内容、只记录域名和耗时。这不是技术问题是合规底线问题千万别忽略。6. 常见问题排查与二次开发建议最后这部分我集中说下使用MTHawkeye过程中最常见的几个问题以及如果你需要对它做二次开发时的切入思路。6.1 符号不全导致调用栈不可读这是最让人头疼的问题。MTHawkeye抓到卡顿调用栈后如果显示的都是内存地址而不是函数名说明符号表没配置好。解决方式是在项目的Build Settings里把DWARF with dSYM File开启然后在MTHawkeye的配置里指定dSYM文件路径。如果你是通过CocoaPods集成的还需要确保MTHawkeye和你自己工程的dSYM都上传到了符号收集服务。一个调试技巧卡顿发生时立即在Xcode里暂停App然后手动调用image lookup -rn MTHawkeye这类命令来验证符号是否已经加载。如果能看到符号说明是MTHawkeye内部读取符号的问题如果看不到说明是dSYM没有配置好。这个排查思路比瞎改配置快得多。6.2 浮窗显示但不展示数据浮窗正常显示但点开看各个模块都没有数据99%的情况是初始化时没有调用startServer。isEnabled YES只是打开了浮窗和基础监控但各模块的数据采集服务是通过startServer启动的。我当时在这个问题上卡了大半天最后翻源码才发现。要注意另外一点MTHawkeye默认配置下浮窗是半透明的点按浮窗上的不同区块可以切换不同的监控页面。如果你发现点按没有反应检查一下是不是浮窗被某个系统弹窗挡住了。6.3 二次开发如何新增一个自定义监控模块MTHawkeye的模块化设计很清晰新增一个自定义监控模块的流程是实现MTHawkeyePlugin协议在didStart方法里启动你的监控逻辑在didStop里停止并清理资源然后通过MTHawkeyePluginManager注册。我做过一个自定义模块专门监控主线程的RunLoop耗时分布。原理是在RunLoop的Observer回调里记录各个阶段耗时数据存到MTHawkeye的存储框架里。这样做的好处是能和MTHawkeye自带的数据统一展示、统一导出不用自己再造一套存储和展示轮子。如果只是想加一个数据维度不想做完整的自定义模块也可以直接调用MTHawkeye提供的MTHawkeyeStorage来存储自定义数据。它的存储API很简洁本质就是SQLite的封装不需要你用SQL直接操作。6.4 几个需要警惕的开销问题最后说三个在实际使用中可能被忽视的性能开销点网络监控开启时每个请求都会经过hook层这个开销虽然小但在高并发请求的场景下比如图片列表快速滑动还是会有可见的CPU增长。如果只做短时抓取问题不大长期开着全局网络监控就要评估了。内存监控记录内存分配时如果开启了所有malloc类型内存分配记录会非常多对内存本身也会造成压力。建议在非排查内存问题时关掉这个细分选项只保留内存总量和增长趋势的记录。MTHawkeye的浮窗本身是基于UIWindow的透明浮层它会在所有页面上方增加一层视图对某些对UI层级敏感的页面可能会造成影响。如果在处理绘制相关的bug时最好先关掉浮窗再复现避免浮窗本身干扰判断。MTHawkeye这个工具我用了两年多最大的体会是它解决的不是怎么抓性能问题的问题而是当用户遇到问题时你如何还原现场的问题。从卡顿监控到网络耗时分解从能耗热点到内存增长它覆盖了线上性能问题的绝大多数场景而且是开箱即用的。如果你从一个模块开始接触它我的建议是先跑通ANR监控因为卡顿是用户感知最强的性能问题也是MTHawkeye做得最成熟的功能。用熟了之后再逐步打开网络、内存、IO模块你会发现性能问题从靠猜变成了靠数据这才是监控工具的真正价值。
返回列表