抖音 Android 性能優(yōu)化系列:Java 內(nèi)存優(yōu)化篇

內(nèi)存作為計(jì)算機(jī)程序運(yùn)行最重要的資源之一,需要運(yùn)行過(guò)程中做到合理的資源分配與回收,不合理的內(nèi)存占用輕則使得用戶應(yīng)用程序運(yùn)行卡頓、ANR、黑屏,重則導(dǎo)致用戶應(yīng)用程序發(fā)生 OOM(out of memory)崩潰。抖音作為一款用戶使用廣泛的產(chǎn)品,需要在各種機(jī)器資源上保持優(yōu)秀的流暢性和穩(wěn)定性,內(nèi)存優(yōu)化是必須要重視的環(huán)節(jié)。

本文從抖音 Java OOM 內(nèi)存優(yōu)化的治理實(shí)踐出發(fā),嘗試給大家分享一下抖音團(tuán)隊(duì)關(guān)于 Java 內(nèi)存優(yōu)化中的一些思考,包括工具建設(shè)、優(yōu)化方法論。

抖音 Java OOM 背景

在未對(duì)抖音內(nèi)存進(jìn)行專項(xiàng)治理之前我們梳理了一下整體內(nèi)存指標(biāo)的絕對(duì)值和相對(duì)崩潰,發(fā)現(xiàn)占比都很高。另外,內(nèi)存相關(guān)指標(biāo)在去年春節(jié)活動(dòng)時(shí)又再次激增達(dá)到歷史新高,所以整體來(lái)看內(nèi)存問(wèn)題相當(dāng)嚴(yán)峻,必須要對(duì)其進(jìn)行專項(xiàng)治理。抖音這邊通過(guò)前期歸因、工具建設(shè)以及投入一個(gè)雙月的內(nèi)存專項(xiàng)治理將整體 Java OOM 優(yōu)化了百分之 80。

Java OOM Top 堆棧歸因

在對(duì)抖音的 Java 內(nèi)存優(yōu)化治理之前我們先根據(jù)平臺(tái)上報(bào)的堆棧異常對(duì)當(dāng)前的 OOM 進(jìn)行歸因,主要分為下面幾類:

圖 1. OOM 分類

其中 pthread_create 問(wèn)題占到了總比例大約在百分之 50,Java 堆內(nèi)存超限為百分之 40 多,剩下是少量的 fd 數(shù)量超限。其中 pthread_create 和 fd 數(shù)量不足均為 native 內(nèi)存限制導(dǎo)致的 Java 層崩潰,我們對(duì)這部分的內(nèi)存問(wèn)題也做了針對(duì)性優(yōu)化,主要包括:

  • 線程收斂、監(jiān)控

  • 線程棧泄漏自動(dòng)修復(fù)

  • FD 泄漏監(jiān)控

  • 虛擬內(nèi)存監(jiān)控、優(yōu)化

  • 抖音 64 位專項(xiàng)

治理之后 pthread_create 問(wèn)題降低到了 0.02‰以下,這方面的治理實(shí)踐會(huì)在下一篇抖音 Native 內(nèi)存治理實(shí)踐中詳細(xì)介紹,大家敬請(qǐng)期待。本文重點(diǎn)介紹 Java 堆內(nèi)存治理。

堆內(nèi)存治理思路

從 Java 堆內(nèi)存超限的分類來(lái)看,主要有兩類問(wèn)題:

1. 堆內(nèi)存單次分配過(guò)大/多次分配累計(jì)過(guò)大。

觸發(fā)這類問(wèn)題的原因有數(shù)據(jù)異常導(dǎo)致單次內(nèi)存分配過(guò)大超限,也有一些是 StringBuilder 拼接累計(jì)大小過(guò)大導(dǎo)致等等。這類問(wèn)題的解決思路比較簡(jiǎn)單,問(wèn)題就在當(dāng)前的堆棧。

2. ****堆內(nèi)存累積分配觸頂。

這類問(wèn)題的問(wèn)題堆棧會(huì)比較分散,在任何內(nèi)存分配的場(chǎng)景上都有可能會(huì)被觸發(fā),那些高頻的內(nèi)存分配節(jié)點(diǎn)發(fā)生的概率會(huì)更高,比如 Bitmap 分配內(nèi)存。這類 OOM 的根本原因是內(nèi)存累積占用過(guò)多,而當(dāng)前的堆棧只是壓死駱駝的最后一根稻草,并不是問(wèn)題的根本所在。所以這類問(wèn)題我們需要分析整體的內(nèi)存分配情況,從中找到不合理的內(nèi)存使用(比如內(nèi)存泄露、大對(duì)象、過(guò)多小對(duì)象、大圖等)。

工具建設(shè)

工具思路

工欲善其事,必先利其器。從上面的內(nèi)存治理思路看,工具需要主要解決的問(wèn)題是分析整體的內(nèi)存分配情況,發(fā)現(xiàn)不合理的內(nèi)存使用(比如內(nèi)存泄露、大對(duì)象、過(guò)多小對(duì)象等)。

我們從線下和線上兩個(gè)維度來(lái)建設(shè)工具:

線下

線下工具是最先考慮的,在研發(fā)和測(cè)試的時(shí)候能夠提前發(fā)現(xiàn)內(nèi)存泄漏問(wèn)題。業(yè)界的主流工具也是這個(gè)思路,比如 Android Studio Memory Profiler、LeakCanary、Memory Analyzer (MAT)。

我們基于 LeakCanary 核心庫(kù)在線下設(shè)計(jì)了一套自動(dòng)分析上報(bào)內(nèi)存泄露的工具,主要流程如下:

圖 2.線下自動(dòng)分析流程

抖音在運(yùn)行了一段線下的內(nèi)存泄漏工具之后,發(fā)現(xiàn)了線下工具的各種弊端:

  1. 檢測(cè)出來(lái)的內(nèi)存泄漏過(guò)多,并且也沒(méi)有比較好的優(yōu)先級(jí)排序,研發(fā)消費(fèi)不過(guò)來(lái),歷史問(wèn)題就一直堆積。另外也很難和業(yè)務(wù)研發(fā)溝通問(wèn)題解決的收益,大家針對(duì)解決線下的內(nèi)存泄漏問(wèn)題的 ROI(投入產(chǎn)出比)比較難對(duì)齊。
  2. 線下場(chǎng)景能跑到的場(chǎng)景有限,很難把所有用戶場(chǎng)景窮盡。抖音用戶基數(shù)很大,我們經(jīng)常遇到一些線上的 OOM 激增問(wèn)題,因?yàn)槿鄙倬€上數(shù)據(jù)而無(wú)從查起。
  3. Android 端的 HPORF 的獲取依賴原生的 Debug.dumpHporf,dump 過(guò)程會(huì)掛起主線程導(dǎo)致明顯卡頓,線下使用體驗(yàn)較差,經(jīng)常會(huì)有研發(fā)反饋影響測(cè)試。
  4. LeakCanary 基于 Shark 分析引擎分析,分析速度較慢,通常在 5 分鐘以上才能分析完成,分析過(guò)程會(huì)影響進(jìn)程內(nèi)存占用。
  5. 分析結(jié)果較為單一,僅僅只能分析出 Fragment、Activity 內(nèi)存泄露,像大對(duì)象、過(guò)多小對(duì)象問(wèn)題導(dǎo)致的內(nèi)存 OOM 無(wú)法分析。

線上

正是由于上述一些弊端,抖音最早的線下工具和治理流程并沒(méi)有起到什么太大作用,我們不得不重新審視一下,工具建設(shè)的重心從線下轉(zhuǎn)成了線上。線上工具的核心思路是:在發(fā)生 OOM 或者內(nèi)存觸頂?shù)扔|發(fā)條件下,dump 內(nèi)存的 HPROF 文件,對(duì) HPROF 文件進(jìn)行分析,分析出內(nèi)存泄漏、大對(duì)象、小對(duì)象、圖片問(wèn)題并按照泄露鏈路自動(dòng)歸因,將大數(shù)據(jù)問(wèn)題按照用戶發(fā)生次數(shù)、泄露大小、總大小等緯度排序,推進(jìn)業(yè)務(wù)研發(fā)按照優(yōu)先級(jí)順序來(lái)建立消費(fèi)流程。為此我們研發(fā)了一套基于 HPORF 分析的線下、線上閉環(huán)的自動(dòng)化分析工具 Liko(寓意 ko 內(nèi)存 Leak 問(wèn)題)。

Liko 介紹

Liko 整體架構(gòu)

圖 3. Liko 架構(gòu)圖

整體架構(gòu)由客戶端、Server 端和核心分析引擎三部分構(gòu)成。

  • 客戶端

在客戶端完成 HPROF 數(shù)據(jù)采集和分析(針對(duì)端上分析模式),這里線上和線下策略不同。

線上:主要在 OOM 和內(nèi)存觸頂時(shí)通過(guò)用戶無(wú)感知 dump 來(lái)獲取 HPROF 文件,當(dāng) App 退出到后臺(tái)且內(nèi)存充足的情況進(jìn)行分析,為了盡量減少對(duì) App 運(yùn)行時(shí)影響,主要通過(guò)裁剪 HPROF 回傳進(jìn)行分析,為減輕服務(wù)器壓力,對(duì)部分比例用戶采用端上分析作為 Backup。

線下:dump 策略配置較為激進(jìn),在 OOM、內(nèi)存觸頂、內(nèi)存激增、監(jiān)測(cè) Activity、Fragment 泄漏數(shù)量達(dá)到一定閾值多種場(chǎng)景下觸發(fā) dump,并實(shí)時(shí)在端上分析上傳至后臺(tái)并在本地自動(dòng)生成 html 報(bào)表,幫助研發(fā)提前發(fā)現(xiàn)可能存在的內(nèi)存問(wèn)題。

  • Server 端

Server 端根據(jù)線上回傳的大數(shù)據(jù)完成鏈路聚合、還原、分配,并根據(jù)用戶發(fā)生次數(shù)、泄露大小、總大小等緯度促進(jìn)研發(fā)測(cè)消費(fèi),對(duì)于回傳分析模式則會(huì)另外進(jìn)行 HPORF 分析。

  • 分析引擎

基于 MAT 分析引擎完成內(nèi)存泄露、大對(duì)象、小對(duì)象、圖片等自動(dòng)歸因,同時(shí)支持在線下自動(dòng)生成 Html 報(bào)表。

Liko 流程圖

圖 4. Liko 流程圖

整體流程分為:

  1. Hprof 收集

  2. 分析時(shí)機(jī)

  3. 分析策略

Hprof 收集

收集過(guò)程我們?cè)O(shè)置了多種策略可以自由組合,主要有 OOM、內(nèi)存觸頂、內(nèi)存激增、監(jiān)測(cè) Activity、Fragment 泄漏數(shù)量達(dá)到一定閾值時(shí)觸發(fā),線下線上策略配置不同。

為了解決 dump 掛起進(jìn)程問(wèn)題,我們采用了子進(jìn)程 dump+fileObsever 的方式完成 dump 采集和監(jiān)聽。

在 fork 子進(jìn)程之前先 Suspend 獲取主進(jìn)程中的線程拷貝,通過(guò) fork 系統(tǒng)調(diào)用創(chuàng)建子進(jìn)程讓子進(jìn)程擁有父進(jìn)程的拷貝,然后 fork 出的子進(jìn)程中調(diào)用 Hprof 的 DumpHeap 函數(shù)即可完成把耗時(shí)的 dump 操作在放在子進(jìn)程。由于 suspendresume 是系統(tǒng)函數(shù),我們這里通過(guò)自研的 native hook 工具對(duì) libart.so hook 獲取系統(tǒng)調(diào)用。由于寫入是在子進(jìn)程完成的,我們通過(guò) Android 提供的 fileObsever 文件寫入進(jìn)行監(jiān)控獲取 dump 完成時(shí)機(jī)。

圖 5.子進(jìn)程 dump 流程圖

Hprof 分析時(shí)機(jī)

為了達(dá)到分析過(guò)程對(duì)于用戶無(wú)感,我們?cè)诰€上、線下配置了不同的分析時(shí)機(jī)策略,線下在 dump 分析完成后根據(jù)內(nèi)存狀態(tài)主動(dòng)觸發(fā)分析,線上當(dāng)用戶下次冷啟退出應(yīng)用后臺(tái)且內(nèi)存充足的情況下觸發(fā)分析。

分析策略

分析策略我們提供了兩種,一種在 Android 客戶端分析,一種回傳至 Server 端分析,均通過(guò) MAT 分析引擎進(jìn)行分析。

端上分析
分析引擎

端上分析引擎的性能很重要,這里我們主要對(duì)比了 LeakCanary 的分析引擎 Shark 和 Haha 庫(kù)的 MAT。

圖 6. Shark VS MAT

我們?cè)谙嗤蛻舳谁h(huán)境對(duì) 160M 的 HPROF 多次分析對(duì)比發(fā)現(xiàn) MAT 分析速度明顯優(yōu)于 Shark,另外針對(duì) MAT 分析后仍持有統(tǒng)治者樹占用內(nèi)存我們也做了主動(dòng)釋放,對(duì)比性能收益后采用基于 MAT 庫(kù)的分析引擎進(jìn)行分析,對(duì)內(nèi)存泄漏引用鏈路自動(dòng)歸并、大對(duì)象小對(duì)象引用鏈自動(dòng)分析、大圖線下自動(dòng)還原線上過(guò)濾無(wú)用鏈路,分析結(jié)果如下:

內(nèi)存泄漏

圖 7. 內(nèi)存泄漏鏈路

對(duì)泄漏的 Activity 的引用鏈進(jìn)行了聚合分析,方便一次性解決該 Activity 的泄漏鏈釋放內(nèi)存。

大對(duì)象

圖 8. 大對(duì)象鏈路

大對(duì)象不止分析了引用鏈路,還遞歸分析了內(nèi)部 top 持有對(duì)象(InRefrenrece)的 RetainedSize。

小對(duì)象

圖 9. 小對(duì)象鏈路

小對(duì)象我們對(duì) top 的外部持有對(duì)象(OutRefrenrece)進(jìn)行聚合得到占有小對(duì)象最多的鏈路。

圖片

圖 10. 圖片鏈路

圖片我們過(guò)濾了圖片庫(kù)等無(wú)效引用且對(duì) Android 8.0 以下的大圖在線下進(jìn)行了還原。

回傳分析

為了最大限度的節(jié)省用戶流量且規(guī)避隱私風(fēng)險(xiǎn),我們通過(guò)自研 HPROF 裁剪工具 Tailor 在 dump 過(guò)程對(duì) HPROF 進(jìn)行了裁剪。

裁剪過(guò)程

圖 11. Tailor 裁剪流程

去除了無(wú)用信息

  • 跳過(guò) header

  • 分 tag 裁剪

  • 裁剪無(wú)用信息:char[]; byte[]; timestamp; stack trace serial number; class serial number;

  • 壓縮數(shù)據(jù)信息

同時(shí)對(duì)數(shù)據(jù)進(jìn)行 zlib 壓縮,在 server 端數(shù)據(jù)還原,整體裁剪效果:180M--->50M---->13M

優(yōu)化實(shí)踐

內(nèi)存泄漏

除了通過(guò)后臺(tái)根據(jù) GCROOT+ 引用鏈自動(dòng)分配研發(fā)跟進(jìn)解決我們常見的內(nèi)存泄漏外,我們還對(duì)系統(tǒng)導(dǎo)致一些內(nèi)存泄漏進(jìn)行了分析和修復(fù)。

系統(tǒng)異步 UI 泄漏

根據(jù)上傳聚合的引用鏈我們發(fā)現(xiàn)在 Android 6.0 以下有一個(gè) HandlerThread 作為 GCROOT 持有大量 Activity 導(dǎo)致內(nèi)存泄漏,根據(jù)引用發(fā)現(xiàn)這些泄漏的 Activity 都被一個(gè) Runnable(這里是 Runnable 是一個(gè)系統(tǒng)事件 SendViewStateChangedAccessibilityEvent)持有,這些 Runnable 被添加到一個(gè) RunQueuel 中,這個(gè)隊(duì)列本身被 TheadLocal 持有。

圖 12. HandlerThread 泄露鏈路

我們從 SendViewStateChangedAccessibilityEvent 入手對(duì)源碼進(jìn)行了分析發(fā)現(xiàn)它在 notifyViewAccessibilityStateChangedIfNeeded 中被拋出,系統(tǒng)的大量 view 都會(huì)在自身的一些 UI 方法(eg: setChecked)中觸發(fā)該函數(shù)。

SendViewStateChangedAccessibilityEventrunOrPost 方法會(huì)走到我們常用的 View 的 postDelay 方法中,這個(gè)方法在當(dāng) view 還未被 attched 到根 view 的時(shí)候會(huì)加入到一個(gè) runQueue 中。

這個(gè) runQueue 會(huì)在主線程下一次的 performTraversals() 中消費(fèi)掉。

如果這個(gè) runQueue 不在主線程那就沒(méi)有消費(fèi)的機(jī)會(huì)。

根據(jù)上面的分析發(fā)現(xiàn)造成這種內(nèi)存泄漏需要滿足一些條件:

  1. view 調(diào)用了 postDelay 方法 (這里是 notifyViewAccessisbilityStateChangeIfNeeded 觸發(fā))

  2. view 處于 detached 狀態(tài)

  3. 上述過(guò)程是在非主線程里面操作的,ThreadLocal 非 UIThread,持有的 runQueue 不會(huì)走 performTraversals 消費(fèi)掉。

抖音這邊大量使用了異步 UI 框架來(lái)優(yōu)化渲染性能,框架內(nèi)部由一個(gè) HandlerThread 驅(qū)動(dòng),完全符合上述條件。針對(duì)該問(wèn)題,我們通過(guò)反射獲取非主線程的 ThreadLocal,在每次異步渲染完主動(dòng)清理內(nèi)部的 RunQueue。

圖 13. 反射清理流程

另外,Google 在 6.0 上也修復(fù)了 notifyViewAccessisbilityStateChangeIfNeeded 的判斷不嚴(yán)謹(jǐn)問(wèn)題。

內(nèi)存泄漏兜底

大量的內(nèi)存泄漏,如果我們都靠推進(jìn)研發(fā)解決,經(jīng)常會(huì)出現(xiàn)生產(chǎn)大于消費(fèi)的情況,針對(duì)這些未被消費(fèi)的內(nèi)存泄漏我們?cè)诳蛻舳俗隽吮O(jiān)控和止損,將 onDestory 的 Activity 添加到 WeakRerefrence 中,延遲 60s 監(jiān)控是否回收,未回收則主動(dòng)釋放泄漏的 Activity 持有的 ViewTree 的背景圖和 ImageView 圖片。

大對(duì)象

主要對(duì)三種類型的大對(duì)象進(jìn)行優(yōu)化

  • 全局緩存:針對(duì)全局緩存我們按需釋放和降級(jí)了不需要的緩存,盡量使用弱引用代替強(qiáng)引用關(guān)系,比如針對(duì)頻繁泄漏的 EventBus 我們將內(nèi)部的訂閱者關(guān)系改為弱引用解決了大量的 EventBus 泄漏。

  • 系統(tǒng)大對(duì)象:系統(tǒng)大對(duì)象如 PreloadDrawable、JarFile 我們通過(guò)源碼分析確定主動(dòng)釋放并不干擾原有邏輯,在啟動(dòng)完成或在內(nèi)存觸頂時(shí)主動(dòng)反射釋放。

  • 動(dòng)畫:用原生動(dòng)畫代替了內(nèi)存占用較大的幀動(dòng)畫,并對(duì) Lottie 動(dòng)畫泄漏做了手動(dòng)釋放。

圖 14. 大對(duì)象優(yōu)化點(diǎn)

小對(duì)象

小對(duì)象優(yōu)化我們集中在字段優(yōu)化、業(yè)務(wù)優(yōu)化、緩存優(yōu)化三個(gè)緯度,不同的緯度有不同的優(yōu)化策略。

圖 15. 小對(duì)象優(yōu)化思路

通用類優(yōu)化

在抖音的業(yè)務(wù)中,視頻是最核心且通用的 Model,抖音業(yè)務(wù)層的數(shù)據(jù)存儲(chǔ)分散在各個(gè)業(yè)務(wù)維護(hù)了各自視頻的 Model,Model 本身由于聚合了各個(gè)業(yè)務(wù)需要的屬性很多導(dǎo)致單個(gè)實(shí)例內(nèi)存占用就不低,隨著用戶使用過(guò)程實(shí)例增長(zhǎng)內(nèi)存占用越來(lái)越大。對(duì) Model 本身我們可以從屬性優(yōu)化和拆分這兩種思路來(lái)優(yōu)化。

  • 字段優(yōu)化:針對(duì)一次性的屬性字段,在使用完之后及時(shí)清理掉緩存,比如在視頻 Model 內(nèi)部存在一個(gè) Json 對(duì)象,在反序列完成之后 Json 對(duì)象就沒(méi)有使用價(jià)值了,可以及時(shí)清理。
  • 類拆分:針對(duì)通用 Model 冗雜過(guò)多的業(yè)務(wù)屬性,嘗試對(duì) Model 本身進(jìn)行治理,將各個(gè)業(yè)務(wù)線需要用到的屬性進(jìn)行梳理,將 Model 拆分成多個(gè)業(yè)務(wù) Model 和一個(gè)通用 Model,采用組合的方式讓各個(gè)業(yè)務(wù)線最小化依賴自己的業(yè)務(wù) Model,減少大雜燴 Model 不必要的內(nèi)存浪費(fèi)。

業(yè)務(wù)優(yōu)化

  • 按需加載:抖音這邊 IM 會(huì)全局保存會(huì)話,App 啟動(dòng)時(shí)會(huì)一次性 Load 所有會(huì)話,當(dāng)用戶的會(huì)話過(guò)多時(shí)相應(yīng)全局占用的內(nèi)存就會(huì)較大,為了解決該問(wèn)題,會(huì)話列表分兩次加載,首次只加載一定數(shù)量到內(nèi)存,需要時(shí)再加載全部。

  • 內(nèi)存緩存限制或清理:首頁(yè)推薦列表的每一次 Loadmore 操作,都不會(huì)清理之前緩存起來(lái)的視頻對(duì)象,導(dǎo)致用戶長(zhǎng)時(shí)間停留在推薦 Feed 時(shí),緩存起來(lái)的視頻對(duì)象過(guò)多會(huì)導(dǎo)致內(nèi)存方面的壓力。在通過(guò)實(shí)驗(yàn)驗(yàn)證不會(huì)對(duì)業(yè)務(wù)產(chǎn)生負(fù)面影響情況下對(duì)首頁(yè)的緩存進(jìn)行了一定數(shù)量的限制來(lái)減小內(nèi)存壓力。

緩存優(yōu)化

上面提到的視頻 Model,抖音最早使用 Manager 來(lái)管理通用的視頻實(shí)例。Manager 使用 HashMap 存儲(chǔ)了所有的視頻對(duì)象,最初的方案里面沒(méi)有對(duì)內(nèi)存大小進(jìn)行限制且沒(méi)有清除邏輯,隨著使用時(shí)間的增加而不斷膨脹,最終出現(xiàn) OOM 異常。為了解決視頻 Model 無(wú)限膨脹的問(wèn)題設(shè)計(jì)了一套緩存框架主要流程如下:

圖 16. 視頻緩存框架

使用 LRU 緩存機(jī)制來(lái)緩存視頻對(duì)象。在內(nèi)存中緩存最近使用的 100 個(gè)視頻對(duì)象,當(dāng)視頻對(duì)象從內(nèi)存緩存中移除時(shí),將其緩存至磁盤中。在獲取視頻對(duì)象時(shí),首先從內(nèi)存中獲取,若內(nèi)存中沒(méi)有緩存該對(duì)象,則從磁盤緩存中獲取。在退出 App 時(shí),清除 Manager 的磁盤緩存,避免磁盤空間占用不斷增長(zhǎng)。

圖片

關(guān)于圖片優(yōu)化,我們主要從圖片庫(kù)的管理和圖片本身優(yōu)化兩個(gè)方面思考。同時(shí)對(duì)不合理的圖片使用也做了兜底和監(jiān)控。

圖片庫(kù)

針對(duì)應(yīng)用內(nèi)圖片的使用狀況對(duì)圖片庫(kù)設(shè)置了合理的緩存,同時(shí)在應(yīng)用 or 系統(tǒng)內(nèi)存吃緊的情況下主動(dòng)釋放圖片緩存。

圖片自身優(yōu)化

我們知道圖片內(nèi)存大小公式 = 圖片分辨率 * 每個(gè)像素點(diǎn)的大小。

圖片分辨率我們通過(guò)設(shè)置合理的采樣來(lái)減少不必要的像素浪費(fèi)。

//開啟采樣ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)    .setDownsampleEnabled(true)    .build();Fresco.initialize(context, config);//請(qǐng)求圖片時(shí),傳入resize的大小,一般直接取View的寬高ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)    .setResizeOptions(new ResizeOptions(50, 50))    .build();mSimpleDraweeView.setController(    Fresco.newDraweeControllerBuilder()        .setOldController(mSimpleDraweeView.getController())        .setImageRequest(request)        .build());

而單個(gè)像素大小,我們通過(guò)替換系統(tǒng) drawable 默認(rèn)色彩通道,將部分沒(méi)有透明通道的圖片格式由 ARGB_8888 替換為 RGB565,在圖片質(zhì)量上的損失幾乎肉眼不可見,而在內(nèi)存上可以直接節(jié)省一半。

圖片兜底

針對(duì)因 activity、fragment 泄漏導(dǎo)致的圖片泄漏,我們?cè)?onDetachedFromWindow 時(shí)機(jī)進(jìn)行了監(jiān)控和兜底,具體流程如下:

圖 17. 圖片兜底流程

圖片監(jiān)控

關(guān)于對(duì)不合理的大圖 or 圖片使用我們?cè)谧止?jié)碼層面進(jìn)行了攔截和監(jiān)控,在原生 Bitmap or 圖片庫(kù)創(chuàng)建時(shí)機(jī)記錄圖片信息,對(duì)不合理的大圖進(jìn)行上報(bào);另外在 ImageView 的設(shè)置過(guò)程中針對(duì) Bitmap 遠(yuǎn)超過(guò) view 本身超過(guò)大小的場(chǎng)景也進(jìn)行了記錄和上報(bào)。

圖 18. 圖片字節(jié)碼監(jiān)控方案

更多思考

是不是解決了 OOM 內(nèi)存問(wèn)題就告一段落了呢?作為一只追求極致的團(tuán)隊(duì),我們除了解決靜態(tài)的內(nèi)存占用外也自研了 Kenzo(Memory Insight)工具嘗試解決動(dòng)態(tài)內(nèi)存分配造成的 GC 卡頓。

Kenzo 原理

Kenzo 采用 JVMTI 完成對(duì)內(nèi)存監(jiān)控工作,JVMTI(JVM Tool Interface)是 Java 虛擬機(jī)所提供的 native 編程接口。JVMTI 開發(fā)時(shí),應(yīng)用建立一個(gè) Agent 使用 JVMTI,可以使用 JVMTI 函數(shù),設(shè)置回調(diào)函數(shù),并從 Java 虛擬機(jī)中得到當(dāng)前的運(yùn)行態(tài)信息,并作出自己的業(yè)務(wù)判斷。

圖 19. Agent 時(shí)序圖

Jvmti SetEventCallbacks 方法可以設(shè)置目標(biāo)虛擬機(jī)內(nèi)部事件回調(diào),可以根據(jù) jvmtiCapabilities 支持的能力和我們關(guān)注的事件來(lái)定義需要 hook 的事件。

Kenzo 采用 Jvmti 完成如下事件回調(diào):

  • 類加載準(zhǔn)備事件 -> 監(jiān)控類加載

  • ClassPrepare:某個(gè)類的準(zhǔn)備階段完成。

  • GC -> 監(jiān)控 GC 事件與時(shí)間

  • GarbageCollectionStart:GC 啟動(dòng)時(shí)。

  • GarbageCollectionFinish:GC 結(jié)束后。

  • 對(duì)象事件 -> 監(jiān)控內(nèi)存分配

  • ObjectFree:GC 釋放一個(gè)對(duì)象時(shí)。

  • VMObjectAlloc:虛擬機(jī)分配一個(gè)對(duì)象的時(shí)候。

框架設(shè)計(jì)

Kenzo 整體分為兩個(gè)部分:

生產(chǎn)端

  • 采集內(nèi)存數(shù)據(jù)

  • 以 sdk 形式集成到宿主 App

消費(fèi)端

  • 處理生產(chǎn)端的數(shù)據(jù)

  • 輸入 Kenzo 監(jiān)控的內(nèi)存數(shù)據(jù)

  • 輸出可視化報(bào)表

圖 20. kenzo 框架

生產(chǎn)端主要以 Java 進(jìn)行 API 調(diào)用,C++完成底層檢測(cè)邏輯,通過(guò) JNI 完成底層邏輯控制。

消費(fèi)端主要以 Python 完成數(shù)據(jù)的解析、視圖合成,以 HTML 完成頁(yè)面內(nèi)容展示。

工作流

圖 21. kenzo 框架

可視化展示

圖 22. kenzo 聚合展示

啟動(dòng)階段內(nèi)存歸因

基于動(dòng)態(tài)內(nèi)存監(jiān)控我們對(duì)最為核心的啟動(dòng)場(chǎng)景的內(nèi)存分配進(jìn)行了歸因分析,優(yōu)化了一些頭部的內(nèi)存節(jié)點(diǎn)分配:

圖 23.啟動(dòng)階段內(nèi)存節(jié)點(diǎn)歸因

另外我們也發(fā)現(xiàn)啟動(dòng)階段存在大量的字符串拼接操作,雖然編譯器已經(jīng)優(yōu)化成了 StringBuider append,但是深入 StringBuider 源碼分析仍在存在大量的動(dòng)態(tài)擴(kuò)容動(dòng)作(System.copy),為了優(yōu)化高頻場(chǎng)景觸發(fā)動(dòng)態(tài)擴(kuò)容的性能損耗,在 StringBuilder 在 append的時(shí)候,不直接往 char[]里塞東西,而是先拿一個(gè) String[]把它們都存起來(lái),到了最后才把所有 String 的 length 加起來(lái),構(gòu)造一個(gè)合理長(zhǎng)度的 StringBuilder。通過(guò)使用編譯時(shí)字節(jié)碼替換的方式,替換所有 StringBuilder 的 append 方法使用自定義實(shí)現(xiàn),優(yōu)化后首次安裝首頁(yè) Feed 滑動(dòng) 1min 的 FPS 提升 1 幀/S,非首次安裝啟動(dòng),滑動(dòng) 1min 的 FPS 提升 0.6 幀/S。

推薦閱讀

為什么阿里巴巴的程序員成長(zhǎng)速度這么快,看完他們的內(nèi)部資料我懂了

字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放下載

刷Github時(shí)發(fā)現(xiàn)了一本阿里大神的算法筆記!標(biāo)星70.5K

程序員50W年薪的知識(shí)體系與成長(zhǎng)路線。

月薪在30K以下的Java程序員,可能聽不懂這個(gè)項(xiàng)目;

字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開放分享

關(guān)于【暴力遞歸算法】你所不知道的思路

開辟鴻蒙,誰(shuí)做系統(tǒng),聊聊華為微內(nèi)核

看完三件事??

如果你覺得這篇內(nèi)容對(duì)你還蠻有幫助,我想邀請(qǐng)你幫我三個(gè)小忙:

點(diǎn)贊,轉(zhuǎn)發(fā),有你們的 『點(diǎn)贊和評(píng)論』,才是我創(chuàng)造的動(dòng)力。

關(guān)注公眾號(hào) 『 Java斗帝 』,不定期分享原創(chuàng)知識(shí)。

同時(shí)可以期待后續(xù)文章ing??

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容