一、背景
图片加载作为重中之重的App体验指标,端侧的白屏问题则是其中最为严重的问题之一。想象一下如果你在浏览交易商品、社区帖子等核心场景下,图片无法完成加载是多么糟糕的体验,线上过往也陆续有一些白屏的用户反馈。
客户端架构侧从用户白屏问题反馈出发,深入分析每一个用户的实际情况,从0到1完成了白屏体系监控建设,借助独立搭建的白屏分析平台能力,从图片库、网络库、CDN质量等3个维度进行了专项治理优化。本文重点带你了解得物图片库是如何完成白屏精细化监控的基础能力、问题定位、治理优化,实现线上用户白屏问图片库相关Issue反馈基本清零。
二、白屏监控体系
众所周知,图片加载全链路是横跨图片库、网络库、多云CDN质量等核心基建的长链路过程。故白屏监控体系建设也是基于以上3个维度进行同步展开,实现白屏各类归因问题的点对点跟进,以下为当前得物侧在平台归因、端侧监控能力建设、端侧问题治理上的一些实践汇总。
平台归因问题
全局监控能力建设
端侧优化手段
三、图片库监控能力
图片库作为图片请求的起始发起者 & 图片加载的最终使用者,是唯一一个可以监控、汇总完整链路数据的承载者。而得物当前的白屏监控也是基于图片加载流程深度剖析,形成了多图、单图的加载白屏监控能力,将白屏问题拆解到多个细分子阶段,输出结构化的分析日志,最终打通到平台侧进行问题聚合分析。
图片
多图监控
定义为屏幕上10秒内有7张图未走完图片库的完整流程,则触发多图监控上报。
基于Fresco的producer(阶段信息)、reqeust(请求信息)、submit(加载信息)回调能力封装,我们记录了每一张图片阶段开始、结束、失败、取消等详细时间信息、额外图片参数信息等到内存Map中备用。
通过Handler消息循环模型,每秒发送1个探测消息,对加载成功、离开屏幕、内存缓存复用等无关场景进行剔除过滤,达到监控手机屏幕上失败请求、加载中请求的目的,触发到我们10s 7图的阈值后则进行所有图片信息的聚合上报,可以清晰直观的知道图片加载阻塞的卡点、失败的原因。
图片
图片核心信息记录:
图片
- Producer信息中记录图片经历所有任务处理阶段: 线程切换、内存缓存、磁盘缓存、网络请求、编解码等。
- Request信息中记录了图片开始请求、取消、失败的核心时间节点。
- Submit信息中记录了图片上屏、完成加载、离开屏幕等核心时间节点。
- PixelCopy信息记录了最近一次屏幕的截屏信息,可以较为真实反应当前屏上图片的时机情况,同时平台对此进行了像素点的分析,本篇重点介绍图片库相关的监控能力,在此不做过多的阐述。
以下为聚合格式化后图片加载的本地日志与平台实例参考,可以直接了当的知道图片是由于网络阶段导致的白屏。
pixelCopy像素图
图片
producer时序信息
单图慢加载监控
定义为单张图3s内未走完除网络外的子阶段、10s未完成完整阶段,则触发单图慢加载上报。
单图监控的逻辑相对简单,当图片加载状态变化时,基于图片库ImagePerfData的信息回调进行数据整合。
- 记录当前图片加载阶段、状态、线程、耗时、单图信息、配置信息等,仍旧通过handler消息循环每1s发送一个探测消息,确认屏上图片是否触发慢加载。
图片
大盘采样监控
等待图片完整加载阶段(成功、失败)后基于用户采样命中,将图片加载信息记录上报,以衡量图片全局加载质量。
图片大盘采样监控实现逻辑与多单图原理类似,此处主要介绍一些大盘核心元信息。
图片
四、图片库优化治理
基于图片库基础监控、白屏监控平台的建设以及线上用户的反馈,我们发现了各种bad case导致端侧白屏现象,并对其进行了精细化的点对点分析,包括图片、网络、CDN质量三大环节,其中66%为网络问题,4.5%为图片问题,1.3%为CDN问题。
本篇重点介绍图片库问题相关的治理优化。
图片
系统解码优化
来看一个有意思的bad case,在线下bvt体验过程中,我们发现在频繁刷新得物Tab的情况下,会出现端上图片均无法加载的极端情况。查看本地日志发现,图片库的阶段日志均停留在DecodeProducer解码阶段。
图片
通过本地多图监控日志可以看到,解码线程池任务已经提交,但实际解码处理并没有执行,说明解码线程池的当前执行任务出了问题。
由于是线下问题,debug发现极个别图片由于包含iccData数据,按现有逻辑会执行到系统解码流程中。而在对应设备上(尤其是android 8.1.0)系统频繁进行Heif解码,整个解码流程可能会变得极其缓慢。我们查看线程堆栈发现执行任务会直接Block在BitmapFactory.nativeDecodeStream方法中,导致解码线程池长时间阻塞,最终引起页面白屏。
图片
- iccData数据: 图片的颜色配置信息,告知端上渲染时的色彩特性。
现有LibHeif & 三方Heif解码库 均无法有效处理对应icc数据段,会导致个别图片存在一定程度的色差问题。
优化方案:
调整解码流程,忽略iccData数据可能会导致的色差问题,将所有的解码任务均托管到解码库执行。
图片
收益:
新版本系统慢解码(尤其android 8.1.0)导致的白屏未再出现。
图片请求优先级改造
为了提高屏上图片加载速度,得物侧使用了图片预加载能力。在过往的预加载、上屏请求管理中,使用的是Fresco PriorityNetworkFetcher来处理预加载与上屏请求的优先级,所有的图片请求共享一个最大并发数为12的网络请求队列,这在实践中我们发现了大量由于A域名不同,导致B域名请求无法成功导致的白屏。
图片
基于得物当前是多CDN、预加载&上屏请求互相转换 的请求现状,存在以下问题:
- 多域名请求间的竞争问题,A域名不通会导致并发队列被打满,B域名网络正常但请求始终处在等待入队状态。
- 预加载进入请求队列后,在弱网等状态下并不能为上屏请求让步,挤占了一部分的运行时请求队列空间。
- 预加载的请求会无上限堆积,快速滑动场景可能出现由于持续存在屏上图片加载,预加载请求会堆积到200-300+,但实际并没有有效利用到预加载转化后的Bitmap,浪费了带宽 & 流量。
优化方案:
去除图片库的优先级队列限制,统一交由网络库管理收口。
- 实现按域名拆解host请求,充分利用网络库的现有能力,打破域名间竞争。
- 支持预加载、离屏的"请求取消"能力 & 图片库上离屏标记、请求状态转换。
图片
收益:
- 端侧图片库的请求平均排队耗时降 88%,图片请求全链路降低 50%。
- 解决CDN单通导致的白屏问题,线上CDN单通异常数量呈下降趋势。
动图渲染帧处理重构
得物历史对动图渲染帧的准备、加载流程,不同于Fresco原生For循环实现,自定义实现了利用Handler消息调度来分发、准备渲染帧。
问题发现
利用自研的白屏平台监控能力,聚合分析了图片慢解码导致白屏的Issue,发现有一类case用户会出现解码任务始终不完成的情况,出现概率大概在万分之二。我们对解码流程监控进行了增强处理,进行线程池监控,记录解码线程的运行数量、id、等待队列数量等信息附带在多图监控的白屏上报中。
图片
图片
可以看到,解码线程池并发数8已经被打满,但等待队列中囤积的解码图片数量有1200+。
白屏问题转化为了监控解码线程池运行中的任务究竟在干什么,有了之前系统解码的分析经验,我们如果直接将白屏问题当做一个Crash问题来分析呢?那么很自然地会想到去查看堆栈,只要dump运行时中的线程池id所对应的堆栈即可。
图片
最终通过堆栈结合代码分析确认是由于动图帧渲染的countDownLatch锁在极端case下无法释放,线程池逐步被打满导致的等待队列解码任务无法得到执行造成的白屏,而背景恰好是对应版本的动图下发数量明显上涨导致此类case逐渐暴露,与我们的结论能够吻合。
图片
优化方案
历史代码存在以下几个问题:
- 单帧Bitmap毫秒级的准备处理周期内,在View OnPause情况下未完成便退出,会导致当前线程锁无法释放。
- countDownLatch的锁逻辑可通过回调的方式代替。
- 线程池复用解码线程池,导致动图加载逻辑异常会影响到所有图片。
图片
图片
优化后:对动图帧渲染逻辑进行去锁改造,采用callBack回调方式进行动图帧分发处理,同时对解码线程池进行隔离处理。
图片
收益:
App新版本动图闭锁逻辑导致的白屏归因数量从原有1000+实现清零。
磁盘全局锁优化
图片库的磁盘设计基于Fresco原生实现,支持大小缓存,但读、写、删、遍历操作同时共用Object Lock对象锁,且过往由于业务需要获取缓存文件的能力,图片库暴露了getCacheFile的API给到上层使用。
图片
白屏平台发现存在磁盘读写超时导致的图片白屏。通过对DiskProducer的深入代码分析,我们将问题锁定在了图片库存在大量本地磁盘文件的情况下,由于Fresco原生逻辑每隔30min要进行磁盘IO遍历更新图片库当前磁盘缓存大小,可能会导致长时间持有对象锁,最终引起图片加载流程持续等待遍历完成的白屏。
图片
优化方案
磁盘遍历是Fresco的磁盘缓存模型中唯一潜在的长耗时点。
由于此操作的调用频率极低(30min一次),我们尝试针对IO遍历进行标记,并调整了原有主线程获取磁盘缓存文件 & Fresco原生磁盘缓存查找的逻辑,对于在缓存遍历期间磁盘数量超过指定阈值的缓存读取操作进行适当忽略。
当然终极优化方案是降低磁盘缓存模型的锁粒度,将全局的对象锁降至单文件级别,但对现有的磁盘缓存设计改动过大暂时未上线。
图片
收益:
新版本磁盘锁导致的主线程卡顿清零 & 磁盘锁导致的图片加载长耗时降低50% 。
元信息读取适配
部分设备上Heif图编码时获取图片metadata宽高元信息失败,导致无法完成正常解码、resize裁剪等操作,最终形成图片加载白屏。
我们先来看下图片库历史编码流程环节,在网络请求完成后,会通过BitmapFactory进行图片metadata信息,获取期望的宽高数据。在历史逻辑中,我们只单独处理了webp图片宽高读取,Heif图等其他格式仍旧采用系统兜底实现,但实际针对加载失败的原图分析后,我们发现andorid系统BitmapFactory的解析逻辑对Heif图片的支持并不友好,会存在获取失败的bad case。
final Pair<Integer, Integer> dimensions;
if (DefaultImageFormats.isWebpFormat(imageFormat)) {
//webp图片单独处理
dimensions = readWebPImageSize();
} else {
//系统兜底实现
dimensions = readImageMetaData().getDimensions();
}
故我们需要重Heif图的宽高元信息获取逻辑,实际获取逻辑在三方SDK或者libHeif开源库在native层完成了封装,Fresco上层走调用结构解析,分别对应宽度、高度、旋转角度、旋转方向、是否动图等,同时在获取解析结果异常的情况下,我们仍旧以系统BitmapFactory进行流解析兜底。
final Pair<Integer, Integer> dimensions;
if (imageFormat.getName().equals(DefaultImageFormats.HEIFC.getName())) {
//部分设备的 系统实现读取很慢且无法解析
dimensions = readHeifFormatImageSizeForSimple(imageFormat);
} else if (DefaultImageFormats.isWebpFormat(imageFormat)) {
dimensions = readWebPImageSize();
} else {
dimensions = readImageMetaData().getDimensions();
}
//宽高、旋转方向、是否为多帧hefi动图等信息
try {
int[] parseResult = imageFormat.readHeifFormatImageSizeForSimple(inputStream);
if (parseResult != null) {
this.mWidth = parseResult[0];
this.mHeight = parseResult[1];
this.mRotationAngle = JfifUtil.transformFromClockWiseToAntiClockWise(parseResult[2]);
this.mExifOrientation = JfifUtil.getExifOrientationFromAutoRotateAngle(this.mRotationAngle);
int isSequence = parseResult[3];
if (isSequence == 0) {
this.mImageFormat = imageFormat.getHeifFormatAnimated();
}
} else {
//系统实现异常兜底
return readImageMetaData().getDimensions();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
inputStream.close();
} catch (IOException ignored) {
}
}
异常加载图截屏
开源libHeif的内部实现
三方Heif的jni实现
至此,个别问题设备的Heif慢加载、无法加载问题得到了有效解决。
收益:
个别Rom设备dimension(图片宽高)无法解析的异常上报基本清零,新版本该类型的白屏、慢加载从800+降至0。
内存触顶降级优化
除了大面积白屏外,内存不足导致的图片加载失败也是一个常见问题。
动图降级
Fresco原生提供了PoolStatsTracker类来监控在分配内存过程中是否触发到缓存池定义的内存上限,定义了当前缓存池的使用量,以及onHardCapReached的触顶回调。而我们当前则是在回调中,进行一次主动GC来保证系统回收未持有引用的bitmap & 其他内存,同时标记规定间隔内(如1min内)限制大动图的内存分配,将其强制转为静图。
图片
图片
低内存 & 低磁盘缓存清理
基于application ComponentCallbacks2的低内存回调进行磁盘&内存缓存清理。
override fun onLowMemory() {
val imagePipeline = Fresco.getImagePipelineFactory().imagePipeline
imagePipeline.config.executorSupplier.forBackgroundTasks().execute {
if (DuImageGlobalConfig.isDiskNervous) {
imagePipeline.clearDiskCaches()
}
imagePipeline.clearMemoryCaches()
}
}
override fun onTrimMemory(level: Int) {
val imagePipeline = Fresco.getImagePipelineFactory().imagePipeline
imagePipeline.config.executorSupplier.forBackgroundTasks().execute {
if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
if (DuImageGlobalConfig.isDiskNervous) {
imagePipeline.clearDiskCaches()
}
}
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
imagePipeline.clearMemoryCaches()
}
}
}
其他内存优化手段
关于图片内存得物还做了很多优化,但非触顶场景下往往不会直接导致白屏,在此可以关注后续技术博客,如:
- 三级缓存阈值配置分档
- Android8以下bitmap内存转移到Native层
- 大动图、大静图治理
- 图片Resize兜底裁剪、云端分级裁剪
- BitmapConfig低端机降级
- ....
主线程慢消息治理
在白屏平台监控中,还发现一类图片库已经完成onFinalImageSet回调,但最终pixelCopy的像素图展示为白屏的情况。我们先来了解一下原有的图片加载流程,根据流程可知我们已经调用了图片setImage设置,怀疑是页面慢渲染导致的白屏。
图片
//图片库完成所有producer处理流程后的回调
private void onNewResultInternal(
String id,
DataSource<T> dataSource,
@Nullable T image,
float progress,
boolean isFinished,
boolean wasImmediate,
boolean deliverTempResult) {
//....
if (isFinished) {
logMessageAndImage("set_final_result @ onNewResult", image);
mDataSource = null;
if (mSettableDraweeHierarchy != null){
mSettableDraweeHierarchy.setImage(drawable, 1f, wasImmediate); //setImage调用
}
reportSuccess(id, image, dataSource); //上报图片完成屏上设置,即onFinalImageSet完成
}
//....
}
白屏问题随即转化为卡顿问题,分析卡顿问题的利器则是APM火焰图能力,故我们对"图片加载完成"但仍旧白屏的case进行火焰图的聚合上报。发现主线程的确存在长耗时消息,问题最终转化为了主线程的慢消息治理,以下列举一下我们已经发现并实际解决的问题。
图片
- 页面未接入X2c导致inflate耗时
- GetCacheFile的主线程调用耗时
- Sp的主线程强制写文件
- LoadSo成功后的字体文件主线程直接加载
- 主线程查询native视频缓存
- ....
收益:
主线程慢消息导致图片View慢渲染白屏下降80%。
五、总结
白屏问题作为长链路综合型问题,往往问题都是万分之几的出现概率,这意味着线下想要复现排查问题的难度极大,所以如何有效分析归因解决白屏问题对我们来说是不小的挑战。
在历史监控能力缺失无法有效归因的背景下,我们通过线上白屏用户的问题分析,一步步完善端侧白屏全链路的日志能力,逐步摸索建立起基于端侧图片、网络、CDN信息的白屏监控平台,实现:
- 白屏问题的线上反馈归因率达到100%,无不明确或猜测的归因;
- 白屏问题的分析时效从无法分析到2-3小时级别,到现有的5-10min内;
- 白屏问题线上反馈从历史单月7~10例反馈,到截止目前2个月以来线上0反馈。
本文简要介绍了在白屏监控能力建设过程中,图片库在编解码流程、缓存读取策略、视图渲染加载等阶段所做的一些优化。但白屏问题的治理远远不局限于图片库本身,后续会陆续为大家介绍我们在网络优化、CDN质量监控、白屏平台打磨等环节所落地的实践。