得物 App 白屏优化系列|归因篇
一、前言
本系列前面两篇文章已经分别在图片库和网络库的角度介绍了诸多白屏问题的定位和解决方案,但都是相对独立的问题,并且像 OSCP,CDN 节点异常之类的第三方问题无法彻底根治,因此为了长治白屏并发掘更多问题,就需要一套相对完善的白屏检测+问题归因体系。
本文将介绍从用户视角出发的白屏检测方案以及线上白屏问题的大致归因思路。
二、白屏归因平台概览
三、客户端
检测思路
直接将白屏检测写到图片库里似乎是比较合适的方案,但是基础库的改动也可能出 bug 导致图片加载失败,例如图片请求的 url 被某个 bug 置空,这样展示的效果就是接口正常但是图片全都展示占位图,如果写在图片库里将无法发现该问题。
因此为了规避类似的事件发生,我们需要一个完全使用系统 API 来实现的白屏检测方案,站在用户视角来判断当前是否发生了白屏,而图片库和网络库提供的信息仅作为前置判断和现场日志。
整体流程图
屏上图片获取
既然是站在用户视角,那么我们就只需要检测屏上的 ImageView 即可,根据线上用户反馈的信息,白屏问题主要集中在动态流和商品流这类存在大量图片加载的 recyclerView 中,那么根据 recyclerView 的特性,我们最终是采用了 View 类的 onAttachedToWindow 和 onDetachedFromWindow 回调来作为对单张图片检测的开始和结束。
这样可以做到和用户视角完全一致,即在图片可见时开始检测,完全不可见时停止检测。此外图片库的上屏请求发起时机也是同样的方法,这样后续获取现场信息也无需再做时间对齐。
像素抽样检测
为了检测 ImageView 的展示内容是否正常,我们需要获取到其真实展示的 Bitmap。为了减少对内存的占用,我们通过 draw 方法将屏上的 ImageView 绘制到指定的 50*50 的 Bitmap 中,这里注意必须要使用 draw 来获取 Bitmap,不可以从图片库直接获取,因为主线程卡顿也有可能造成白屏,如果从图片库获取就会掩盖这一问题。
由于此处是异步执行 ImageView 的 draw 方法,并且我们持有的其实是 ImageView 的弱引用,因此需要在 draw 之前判断下其内部的 bitmap 是否已经被回收,如果是圆角图还需要判断下 path 对象是否已经被回收,防止出现访问已经回收的 native 对象的崩溃。
之后从该 Bitmap 中居中均匀的取出 NN 个像素点(这里以 33 为例),按其色值进行统计,找出占比最高的色值的比例,如果其占比超过一定阈值,说明他是白图(不一定是白色,因为占位图背景是淡灰色)。
性能优化
白屏问题作为仅次于 crash 和 ANR 的稳定性问题,线上将会全量开启功能,因此对检测性能要求比较严格。
尽管像素抽样检测能够在一定程度上降低内存使用,但是在异步现场频繁调用 view 的 draw 方法还是会有性能损耗,如果恰好检测的同时主线程在绘制某一帧,对帧绘制较慢的低端机而言势必会影响体验,因此需要尽可能降低像素抽样检测频次。
离屏检测
onAttach 和 onDetach 仅适用于 recyclerview,对于其他的布局在图片上屏/离屏时未必会触发这两个回调,因此需要做些适配。
页面可见检测
目前大多数 App 首页的设计都是底部导航栏+多 Fragment 的组合,而在 tab 之间切换时并不会触发 View 的 attach 和 detach,但是切换后前一个页面中 view 已经不在屏上。
因此需要额外注册 Activity 和 Fragment 的生命周期的监听,并记录所有 ImageView 归属的页面,在 onResume 时,将当前 Activity/Fragment 标记为可检测状态,同时在 onPause(fragment 而言是 onPaused)标记为不可检测状态,这样即可解决 Fragment 切换这类场景的检测,同时还兼顾了 App 切后台之后的暂停检测处理。
view 可见性检测
与此相似的还有 View 的 Visibility 问题,例如 viewPager 实现的轮播图,在图片轮播切换时,只是把图片设置为 INVISIBLE,并不会执行 onDetach,且 ImageView 可能是被包含在自定义的布局中,因此在检测之前需要从当前 ImageView 向上遍历其父 View 直到 View 树根节点,如果途中有 INVISIBLE 或者 GONE 状态的 View 则无需检测。
图片库 &网络库预检
图片白屏最常见就是弱网或者 IO 阻塞这类网络/图片库问题,因此在做像素抽检之前需要通过图片库,网络库查询到该图片对应的请求进度,如果加载异常或者耗时异常则无需检测直接判定为白图,同时获取这些基础库中关键的现场快照信息跟随白屏日志上传即可。如果二者均表示加载正常则再做像素抽样检测。
单张图片检测流程示意图:
频次控制
用户正常使用过程中,屏上图片的变更较为频繁,因此需要将检测周期限制为 3s 一次,并且经检测确认正常或白屏的图片不再参与检测。
现场日志
白屏检测的方案只是发现问题,重点在于如何获取充足的现场信息提供给归因平台。
图片网络请求信息
网络请求阶段信息通常是重写 okhttp 的 eventListener 抽象类来获取到各个阶段的执行回调,但是常规的方案一般只关注各个阶段的耗时和基础信息,但是针对白屏问题,我们需要额外关注 connectFailed,requestFailed 和 responseFailed 这三个特殊的回调,因为他们对应的 TCP 建连,request 构建,response 传输阶段都是会因为失败而重试的,因此需要记录下每一次重试的详细信息。
例如下图中记录的就是 TCP 建连阶段对不同 ip 的多次尝试,如果不单独记录的话将只有一条建连记录:
流量监控
方案有两套,分别是系统 API TrafficStats.getUidRxBytes 来获取和通过 NetworkStatsManager.querySummary 获取,两者各有优劣:
前者能确保在弱网环境下哪怕非常小的流量消耗都能记录,但是它会包含本地 socket 通信的流量,如果 App 中使用 PCDN 之类的 SDK 则会对数据造成干扰。
后者不会包含 localSocket 的通信流量,但是系统为了优化性能对记录流量的 bucket 文件写入频率做了限制,在流量消耗非常低的情况下可能获取不到最新的数据。
我们需要将两套方案结合起来看,但是实际归因时为了确保准确性还采用后者来计算 App 网速,这样会漏掉一小部分弱网日志,但是不会误判。
图片库阶段信息
图片库采用的是 Facebook 的 Fresco 开源库,具体的图片库阶段已经在系列一图片库中有过介绍。核心日志主要以 ProducerContext 的 hashCode 作为唯一键值,串联起图片库的 Producer 信息、Request 信息、Submit 信息。
Producer 信息中记录图片经历所有任务处理阶段: 线程切换、内存缓存、磁盘缓存、网络请求、编解码等。
Request 信息中记录了图片开始请求、取消、失败的核心时间节点。
Submit 信息中记录了图片上屏、完成加载、离开屏幕等核心时间节点。
针对每个 Producer 我们也补充了自定义的字段属性,如:
网络队列、解码队列信息。
图片元信息、业务调用标签。
动图帧耗时、帧数、单帧大小等。
基础库队列信息
图片库,网络库的各个关键队列的状态,包含:
业务接口请求中队列
业务接口等待队列
图片库高优队列(即屏上图片请求队列)
图片库低优队列(即离屏/预加载请求队列)
图片网络请求中队列
图片网络请求等待队列
结合队列状况可以分析出一些队列阻塞问题,例如本系列在图片库篇提到过的图片库请求队列被某个异常的 CDN 请求打满导致另一个 CDN 的请求无法发起的问题。
最近 N 分钟的 CDN 异常记录
针对图片请求使用的几个 CDN 域名,以及 App 主站业务接口的域名,分别对成功,失败,慢请求的数量和异常信息单独记录,考虑到内存占用可以改成只记录最近 1 分钟的请求信息。
火焰图
主线程卡顿/慢消息同样会导致白屏问题,多发生在冷启动的首页首帧绘制时,此类问题与图片库/网络库无关,因此针对图片库网络库均正常的情况下,如果像素抽样检测结果显示为白屏,则需要在日志中额外添加最近 20s 的火焰图日志。
现场快照
除去基础库的现场快照信息之外,还需要一个直截了当的证据表明确实发生了白屏,因此在判断出白屏并准备上报日志时需要获取当前 App 页面内容(仅限首页),这样可以直观的看出是否有白屏发生。我们采用的是系统提供的 PixelCopy 类,可以获取当前页面最近一帧的 Bitmap,系统在 native 层做了异步处理,最终会通过入参的 handler 返回获取结果,因此无需考虑多线程问题。
其底层实现是获取当前 window 最近一帧的绘制缓存,可以缩放到入参中指定的 Bitmap,因此无需担心内存占用和性能损耗问题,但是会存在一定几率获取失败,做好防护即可。
并且其内部实现了超时机制,超过 500ms 后会回调异常码,但是由于在子线程执行,时间片调度未必及时,实测经常会超过 500ms,因此需要自行计算耗时,如果超过一定阈值(例如 2s),说明设备此时性能不佳,应当关闭白屏检测或者暂停一段时间,避免进一步影响用户体验。
由于首页页面大小和设备相近,因此需要采取一定比例进行压缩,能够勉强看清文案即可,这里建议使用 270*480,符合大部分移动设备的屏幕比例:
四、平台
归因思路
白屏这种综合性问题绝大部分都是环境异常导致,因此在归因优先级上更倾向于环境类问题,但是像弱网这类环境问题想要精准归因势必要划分一个相对严格的阈值,这就会存在有些和阈值非常相近的弱网问题没有被归为弱网,那么这类问题就只能按照耗时异常的阶段来归因,例如 DNS 长耗时,TCP 建连长耗时等。
如果一个日志不符合任意一种环境问题,那么就需要对白屏中的所有图片单独做归因,最后再取占比最高的问题类型作为整体的白屏归因。
归因策略
特殊异常问题
++OCSP 问题(网络篇有介绍),解码异常,证书校验异常++
此类问题都伴有特殊的基础库异常,可以直接归因,不像 CDN 节点异常和弱网之间存在着重叠部分,还需要现场信息佐证。
案例:
下图中图片库抛出了 OCSP 问题特有的异常信息:javax.net.ssl.SSLHandshakeException: Unacceptable certificate,则可直接被判断为 OCSP 问题。
环境问题
++弱网/无网-流量监控++
由于白屏问题的滞后性,导致白屏的故障往往是发生在十几秒之前(例如进电梯弱网),因此网络诊断这类检测到白屏之后的后续检测的结果仅能作为参考信息,不能作为弱网的直接证据。
因此我们通过流量的消耗来计算 App 网速,通过在内存中维护一个记录最近 3s,6s,9s 的流量消耗的队列,可以算出最近 App 的网络下行速率。同时判定弱网的最低阈值,可以线下用 charles 限速来找出能够满足 App 加载页面的最小带宽。
采用三个 3s 阶段中平均速率的最大值 40KB/s 作为下行速率。
我们区分弱网和无网并不是依据是否有网速,而是最近 3 分钟的业务接口是否有成功记录,如果无一成功则判定无网,否则判定弱网。
案例:
该用户的下行速率低于我们设置的最低阈值 30KB/s,并且业务接口能正常请求,图片 CDN 请求却都失败,因此判定为弱网导致白屏。
++CDN 节点不通 -CDN 异常记录++
CDN 单节点不通出现概率较低,其具体表现为 TCP 建连超时,往往难以和常见的弱网/无网等问题区分开来,因此我们需要让多个 CDN 厂商的请求横向进行对比。
通过客户端提供的最近 1 分钟内 CDN 的异常记录,横向对比各个域名的状态,如果某个 CDN 域名全部是失败或者慢请求,而其他域名均正常,则足以证明该 CDN 节点异常。
案例:
仅 CDN A 的 45 个请求均失败,其他 CDN 和业务接口均正常,则可判定为 CDN 节点异常。
++主线程慢消息 - 火焰图++
在白屏检测上线后我们发现了一些特殊的白屏问题,即在图片库已经调用了 ImageView.setBitmap 之后过了数秒之后 App 仍旧处于白屏状态,因此推测是主线程卡顿导致的帧绘制延迟,补充火焰图之后再进行排查,定位并治理了数个主线程耗时消息导致的白屏。
案例:
主线程连续读取磁盘缓存导致的卡顿。
++图片解码线程池阻塞 - 图片库阶段信息++
由于图片库解码存在一些性能问题,部分图片解码较慢,最终会导致图片库的解码线程池阻塞。此类问题记录下解码线程池等待队列中任务数量即可,如果好过一定阈值并且解码总耗时超时,则可判定为线程池阻塞问题。
案例:
解码任务等待超过 10s 未开始执行,并且线程池等待队列中请求超过 30 个,足以证明解码线程池已经因之前某些解码慢的任务堵死。
共性问题
图片库磁盘缓存读写慢
图片库磁盘缓存锁耗时
以上都是针对单张图片白图诊断出来的问题类型,白屏问题都包含多张白图,因此可以采用在这些白图中占比最高的问题类型作为白屏的归因。
清洗脏数据
前文提到的像素抽样检测方案,我们线上使用的是 10*10 的采样,到这个数量已经可以准确的识别出占位图和正常图,但是部分细长商品的主图空白部分较多,很容易被误判为占位图,具体表现为图片请求正常,现场快照也正常,但是上报白屏。
因此需要在服务端做二次 check,即下载这张图的原图并按 100%采样做同样的统计,如果得到的比例数据和客户端检测结果相近则标记为脏数据。
上方图为占位图,下方图为刀剑模型商品图。
案例:
脏数据这个问题分类的日志量有段时间上涨许多,经排查发现多为首饰类的商品,其商品图同样有这大部分空白,确实属于脏数据。
而当时恰逢有首饰相关推广,App 内项链首饰这个类目的 tab 提到了靠前的位置,这一类的图片曝光都提升了很多从而引起指标上涨,且随着推广到期结束之后该问题日志量又回落到正常水平。这也证明了该策略对脏数据归因的准确性。
归因优先级
我们目前问题归因的优先级从高到低如下,主要按归因证据的可信度来排序。
问题治理
以下是可以优先推进治理的问题类型:
CDN 单点问题
可批量导出异常节点的 IP 地址后联系 CDN 厂商排查。
主线程慢消息导致白屏
大部分都是主线程任务阻塞导致帧绘制的消息没有及时执行,和卡顿检测的日志比较重合,可以借助火焰图和主线程消息队列日志来分析排查问题。
问题分析工具
分析单个白屏日志的工具:
网络库和图片库的现场信息
便于定位图片加载耗时集中在哪些阶段
白屏现场页面内容
问题比例
下图是目前得物 App 线上白屏问题的分布,问题分配的比例在 99.7%以上。且大头还是在网络和磁盘缓存阻塞等设备问题上,CDN 问题仅占 1.24%,整体处于相对稳定的状态,后续网络资源调度和图片库磁盘锁优化落地之后将得到明显改善。
五、总结
白屏作为长链路综合性问题,任意环节出问题都会引发最终的白屏,尤其是客观因素较多的网络侧。通过白屏归因平台可以对线上问题分而治之,对弱网,CDN 节点异常这类无法根治的问题可以通过配置告警来持续关注防劣化,对图片解码超时,主线程卡顿这类可以专项进行治理优化,对线上反馈的单用户白屏则可以通过诊断工具快速定位到根因。
*文 / Jordas
本文属得物技术原创,更多精彩文章请看:得物技术
未经得物技术许可严禁转载,否则依法追究法律责任!
版权声明: 本文为 InfoQ 作者【得物技术】的原创文章。
原文链接:【http://xie.infoq.cn/article/464b4eddc001b0cbec3a078c0】。文章转载请联系作者。
评论