得物App白屏優化系列|圖片庫篇
一、背景
圖片加載作為重中之重的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質量監控、白屏平臺打磨等環節所落地的實踐。