Android | 如何搭建內(nèi)存優(yōu)化體系

點(diǎn)贊關(guān)注鼻百,不再迷路,你的支持對(duì)我意義重大姆打!

?? Hi件余,我是丑丑讥脐。本文 「Android 路線」| 導(dǎo)讀 —— 從零到無(wú)窮大 已收錄遭居,這里有 Android 進(jìn)階成長(zhǎng)路線筆記 & 博客,歡迎跟著彭丑丑一起成長(zhǎng)旬渠。(聯(lián)系方式在 GitHub)


目錄


前置知識(shí)

這篇文章的內(nèi)容會(huì)涉及以下前置 / 相關(guān)知識(shí)俱萍,貼心的我都幫你準(zhǔn)備好了,請(qǐng)享用~

Java 路線

Android 路線

adj 進(jìn)程優(yōu)先級(jí) (high)
Linux內(nèi)核OOM killer機(jī)制:https://juejin.cn/post/6844903878178111502

  • App 內(nèi)存組成 & 限制(如何查看告丢、監(jiān)控)
  • Dalvik & ART 內(nèi)存分配與垃圾回收

1. 重新認(rèn)識(shí)內(nèi)存

“一切性能問題最終都會(huì)變成內(nèi)存問題枪蘑。” 舉個(gè)例子,為了避免資源重復(fù)下載,可以緩存到本地存儲(chǔ)迈喉,而從本地存儲(chǔ)加載進(jìn)內(nèi)存又需要磁盤 I/O叮雳。為了避免重復(fù)磁盤 I/O,可以緩存的內(nèi)存麻敌,緩存的數(shù)據(jù)越來(lái)越大栅炒,最后變成內(nèi)存問題。

1.1 什么是內(nèi)存术羔?

現(xiàn)代 Android 手機(jī)內(nèi)存分為 運(yùn)行時(shí)內(nèi)存 RAM & 非運(yùn)行時(shí)內(nèi)存 ROM

  • 運(yùn)行內(nèi)存 RAM: 相當(dāng)于 PC 中的內(nèi)存條赢赊,是暫存 App 臨時(shí)數(shù)據(jù)的存儲(chǔ)介質(zhì)。RAM 越大手機(jī)就能運(yùn)行更多程序级历,且更佳流暢释移。考慮到體積和功耗寥殖,手機(jī) RAM 不會(huì)使用 PC 中的 DDR RAM 玩讳,而是采用 LPDDR RAM(低功耗雙倍數(shù)據(jù)速率內(nèi)存);

  • 非運(yùn)行內(nèi)存 ROM: 相當(dāng)于 PC 中的磁盤嚼贡,是持久化存儲(chǔ)數(shù)據(jù)的存儲(chǔ)介質(zhì)熏纯。ROM 越大手機(jī)能存儲(chǔ)更多數(shù)據(jù)。

提示:今天我們討論的內(nèi)存優(yōu)化指 “運(yùn)行內(nèi)存優(yōu)化”粤策,而 “非運(yùn)行內(nèi)存優(yōu)化” 我們將在 “存儲(chǔ)優(yōu)化” 專題中討論樟澜。

更多內(nèi)容:內(nèi)存指標(biāo) —— Android | 內(nèi)存指標(biāo)與測(cè)量方法

1.2 內(nèi)存優(yōu)化的維度

分別針對(duì)上面提到的 RAM 和 ROM 兩種內(nèi)存,Android 內(nèi)存優(yōu)化是分為兩方面的工作:

  • 優(yōu)化 RAM: 降低程序運(yùn)行內(nèi)存占用叮盘,防止程序發(fā)生 OOM秩贰,以及降低被 LMK 機(jī)制殺死的概率。同時(shí)不合理的內(nèi)存使用會(huì)增大 GC 發(fā)生頻率柔吼,從而導(dǎo)致程序卡頓萍膛;

  • 優(yōu)化包體積: Resource 資源、so 庫(kù)以及 Dex 文件都會(huì)占用內(nèi)存嚷堡,包體積越大會(huì)占用更多運(yùn)行內(nèi)存蝗罗;

1.3 內(nèi)存優(yōu)化的誤區(qū)

對(duì)內(nèi)存優(yōu)化的錯(cuò)誤認(rèn)識(shí)需要注意規(guī)避艇棕,主要有:

  • 內(nèi)存占用越少越好?

內(nèi)存優(yōu)化不完全是追求于降低內(nèi)存占用串塑,當(dāng)系統(tǒng)內(nèi)存較充足 / 機(jī)型較高端的時(shí)候沼琉,我們完全可以多使用一些內(nèi)存來(lái)?yè)Q取更好的體驗(yàn);而當(dāng)系統(tǒng)內(nèi)存不足 / 機(jī)型較低端的時(shí)候桩匪,我們應(yīng)該更保守打瘪,做到 “用時(shí)分配,及時(shí)釋放”傻昙。

  • 本地(native)內(nèi)存不用管闺骚?

本地內(nèi)存是不受 Java 堆大小限制,例如 Android 8.0 就重新把 Bitmap 的圖片數(shù)據(jù)放在本地內(nèi)存妆档。但也不能濫用本地內(nèi)存僻爽,主要原因是當(dāng)系統(tǒng)物理內(nèi)存不足時(shí),LMK 機(jī)制也會(huì)開始?xì)⑦M(jìn)程贾惦,內(nèi)存占用越高越可能被殺死胸梆。

1.4 內(nèi)存優(yōu)化的意義

優(yōu)化內(nèi)存的意義可以歸結(jié)為如下三點(diǎn):

  • 1、穩(wěn)定性:防止程序發(fā)生 OOM须板,提高應(yīng)用穩(wěn)定性碰镜;
  • 2、順暢度:減少 GC 頻率习瑰,降低卡頓绪颖;
  • 3、碧鹧伲活:減少內(nèi)存占用菠发,降低被 LMK 機(jī)制殺死的概率。

需要注意的是贺嫂,發(fā)生 OOM 的代碼往往是 “壓死駱駝的最后一棵稻草” 滓鸠,但不一定是導(dǎo)致 OOM 的主要代碼,完全可能只是剛好執(zhí)行到這行代碼發(fā)生 OOM第喳。

1.5 內(nèi)存優(yōu)化到底要做什么糜俗?

理解了內(nèi)存優(yōu)化的重要性,現(xiàn)在我們來(lái)討論內(nèi)存優(yōu)化到底要做什么呢曲饱,主要是優(yōu)化三大問題:

  • 內(nèi)存抖動(dòng)(memory thrashing)

內(nèi)存抖動(dòng)是因?yàn)楦哳l率的內(nèi)存分配與回收悠抹,在內(nèi)存波動(dòng)圖上往往呈現(xiàn)鋸齒狀,并伴隨著程序卡頓扩淀。具體見 第 4 節(jié)楔敌。

  • 內(nèi)存泄漏(memory leak)

內(nèi)存泄露簡(jiǎn)單來(lái)說(shuō)就是沒有回收不再使用的內(nèi)存,導(dǎo)致內(nèi)存占用居高不下驻谆,泄露的內(nèi)存分為兩種:Java 內(nèi)存泄露 & Native 內(nèi)存泄露卵凑。具體見 第 5 節(jié)庆聘。

  • 內(nèi)存溢出(out of memory)

內(nèi)存溢出是引用內(nèi)存申請(qǐng)超過(guò)了系統(tǒng)限制的最大堆內(nèi)存,引發(fā) OutOfMemoryError 異常勺卢。具體見 第 6 節(jié)伙判。


2. Android 內(nèi)存管理

LMK


3. 內(nèi)存指標(biāo)與分析方法

在進(jìn)行具體的內(nèi)存優(yōu)化之前,我們應(yīng)該掌握基本的 Android 內(nèi)存指標(biāo)和相應(yīng)的分析方法黑忱,這部分內(nèi)容我單獨(dú)放在這篇文章里:《Android | 內(nèi)存指標(biāo)與分析方法》宴抚,請(qǐng)享用~


4. 內(nèi)存抖動(dòng)問題

內(nèi)存抖動(dòng)(memory thrashing)是因?yàn)楦哳l率的內(nèi)存分配與回收,在內(nèi)存波動(dòng)圖上往往呈現(xiàn)鋸齒狀甫煞。由于高頻率 GC 行為會(huì)頻繁 stop-the-world菇曲,雖然暫停的時(shí)間很短,但終歸是有成本的抚吠,程序整體會(huì)變得卡頓常潮。此外還有可能進(jìn)一步引發(fā) OOM,這是更嚴(yán)重的情況埃跷。

這個(gè)問題在 Dalvik 虛擬機(jī)上會(huì)更加明顯蕊玷,而 ART 虛擬機(jī)在內(nèi)存管理跟回收策略上都做了大量?jī)?yōu)化邮利,內(nèi)存分配和 GC 效率相比提升了 5~10 倍弥雹,出現(xiàn)內(nèi)存抖動(dòng)的概率會(huì)小很多。

出現(xiàn)內(nèi)存抖動(dòng)時(shí)延届,內(nèi)存分配之后馬上就回收了剪勿,為什么還可能引發(fā) OOM 呢?

主要原因是虛擬機(jī)可能會(huì)采用了 “無(wú)整理功能” 的垃圾收集器方庭,頻繁創(chuàng)建對(duì)象時(shí)會(huì)導(dǎo)致堆中的碎片急劇增加厕吉,直到虛擬機(jī)在分配內(nèi)存時(shí)無(wú)法找到足夠大小的連續(xù)內(nèi)存時(shí),就會(huì)引發(fā) OOM械念。

Dalvik 虛擬機(jī)主要使用標(biāo)記清除算法头朱,也可以選擇使用拷貝算法。ART 也有多個(gè)不同的 GC 方案龄减,默認(rèn)方案是 CMS项钮。

4.1 問題定位

代碼 review 是避免程序發(fā)生內(nèi)存抖動(dòng)的有力保障,但是我們更傾向于使用工具來(lái)快速定位希停,因?yàn)橛袝r(shí)候我們并不熟悉相關(guān)的源碼烁巫。使用 Android Studio 中的 Profiler 工具 可以幫助我們。

首先使用 Profiler 工具錄制一段時(shí)間的內(nèi)存占用情況宠能,如果發(fā)現(xiàn)內(nèi)存波動(dòng)圖呈現(xiàn)明顯的鋸齒狀亚隙,或者存在高頻率的 GC 事件,說(shuō)明存在內(nèi)存抖動(dòng)违崇。例如:

既然內(nèi)存抖動(dòng)是由于頻繁分配與回收內(nèi)存導(dǎo)致的阿弃,那么我們就有兩種排查思路:

  • 1诊霹、排查占用內(nèi)存最多的類

既然某一些對(duì)象的分配和回收有能力導(dǎo)致內(nèi)存抖動(dòng),那么說(shuō)明這些對(duì)象一定是占據(jù)了比較高的內(nèi)存恤浪,所以第一種思路就是找出占用內(nèi)存最多的類畅哑。

步驟如下:

  • 1、錄制一段 App 內(nèi)存占用信息水由,選取一小段時(shí)間荠呐;
  • 2、觀察內(nèi)存占用情況砂客,可以發(fā)現(xiàn) String[] 占用的內(nèi)存和個(gè)數(shù)都是最多的泥张;
    • Allocations:分配數(shù)量
    • Shallow Size:內(nèi)存占用
  • 3、點(diǎn)擊String[]條目鞠值,在Instance View窗口中會(huì)顯示對(duì)象的實(shí)例媚创;
  • 4、點(diǎn)擊對(duì)象的實(shí)例彤恶,在Allocation Call Stack窗口中會(huì)顯示創(chuàng)建對(duì)象的堆棧钞钙;
  • 5、點(diǎn)擊Jump to Source声离,直接跳轉(zhuǎn)到問題代碼芒炼;
  • 6、最后定位到代碼如下:
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    重復(fù)創(chuàng)建 String 數(shù)組對(duì)象
    handler = Handler {
        val array = Array(1000000) { "" }
        handler.sendEmptyMessageDelayed(0, 5);
        false
    }

    handler.sendEmptyMessageDelayed(0, 30);
}
  • 2术徊、排查頻繁調(diào)用的方法

程序會(huì)頻繁分配內(nèi)存的背后本刽,其實(shí)也是在頻繁調(diào)用分配內(nèi)存的那個(gè)方法,所以第二種思路就是找出頻繁調(diào)用的方法赠涮。

步驟如下:

  • 1子寓、錄制一段 App 方法調(diào)用信息,選取一小段時(shí)間笋除;
  • 2斜友、選擇Top Down
  • 3垃它、檢查是否存在頻繁調(diào)用的方法鲜屏;
  • 4、最終定位到問題代碼嗤瞎。

由于程序中頻繁調(diào)用的代碼可能會(huì)比較多墙歪,而且方法頻繁調(diào)用并不是內(nèi)存抖動(dòng)的充分條件,所以思路二沒有思路一明顯贝奇。

4.2 常見案例

  • 1虹菲、對(duì)象復(fù)用

在 View#onDraw()、循環(huán)等場(chǎng)景中的對(duì)象應(yīng)提高到外部進(jìn)行復(fù)用掉瞳;

  • 2毕源、字符串拼接

使用顯式 StringBuilder 替代 + 號(hào)浪漠,因?yàn)楹笳呔幾g后也是采用了前者的方式,會(huì)生成太多中間變量霎褐;

  • 3址愿、資源緩存池

采用緩資源存池(例如對(duì)象緩存池、線程池冻璃、位圖緩存池)响谓,以重用頻繁申請(qǐng)的資源。需要注意在使用結(jié)束后手動(dòng)釋放資源省艳。


5. 內(nèi)存泄露問題

內(nèi)存泄露( memory leaks)簡(jiǎn)單來(lái)說(shuō)就是沒有回收不再使用的內(nèi)存娘纷,導(dǎo)致內(nèi)存占用居高不下,泄露的內(nèi)存分為兩種:

  • Java 內(nèi)存泄露: 無(wú)用對(duì)象被生命周期更長(zhǎng)的 GC Root 引用跋炕,導(dǎo)致無(wú)法判定為垃圾對(duì)象赖晶;
  • Native 內(nèi)存泄露: Native 內(nèi)存沒有垃圾回收機(jī)制,需要手動(dòng)進(jìn)行回收辐烂。

5.2 Android 內(nèi)存泄露的案例

實(shí)際開發(fā)中內(nèi)存泄漏的案例是非常多的遏插,需要分門別類,根據(jù)我的總結(jié)主要有以下幾類:

5.2.1 生命周期誤用

大多數(shù)內(nèi)存泄漏是因?yàn)?對(duì)象的生命周期誤用而引發(fā)的纠修,例如:

  • 1胳嘲、監(jiān)聽器對(duì)象未注銷: 例如對(duì)象的監(jiān)聽器、BroadcastReceiver 和 EventBus 訂閱者分瘾,通常監(jiān)聽器對(duì)象是采用強(qiáng)引用胎围,需要手動(dòng)注銷吁系;

  • 2德召、類的靜態(tài)變量 / 單例: 類的靜態(tài)變量 / 單例的生命周期是全局的,需要手動(dòng)置空汽纤;

  • 3上岗、ViewModel 中持有 Activity 對(duì)象引用:ViewModel 的生命周期是跨越重建的 Activity 的,如果將 Activity 對(duì)象存儲(chǔ)在 ViewModel 中蕴坪,那么在 Activity 重建時(shí)肴掷,原有的 Activity 對(duì)象會(huì)依然被 ViewModel;

  • 4背传、延時(shí)任務(wù)阻塞: 當(dāng)對(duì)象在延遲任務(wù)阻塞等待時(shí)也會(huì)引起內(nèi)存泄漏呆瞻。例如在 MessageQueue 中等待執(zhí)行的延遲任務(wù)會(huì)導(dǎo)致就會(huì)導(dǎo)致 Handler 無(wú)法被回收(Message 持有 Handler 的引用:target),而此時(shí)如果 Handler 還持有了 Activity 的引用径玖,那么在 Activity 退出時(shí)痴脾,Activity 出現(xiàn)內(nèi)存泄漏了。解決方法有:

    • 1梳星、(必選)Handler 使用靜態(tài)內(nèi)部類赞赖,并且只持有 Activity 的弱引用滚朵;
    • 2、(可選)在 Activity 退出時(shí)前域,移除消息隊(duì)列中的延遲消息辕近。

5.2.2 資源未釋放

當(dāng)資源無(wú)法使用時(shí),應(yīng)該及時(shí)釋放匿垄,例如:

  • 1移宅、Closable 對(duì)象: 例如文件流、數(shù)據(jù)庫(kù)連接椿疗;

  • 2吞杭、集合中的對(duì)象: 集合容器中對(duì)象如果不再使用,應(yīng)該及時(shí) clear 清空变丧;

  • 3芽狗、資源緩存池: 例如對(duì)象緩存池、線程池痒蓬、位圖緩存池童擎。

5.2.3 WebView

WebView 啟動(dòng)一次之后內(nèi)核是不會(huì)釋放的」ド梗可以用一個(gè)單獨(dú)的進(jìn)程承載 WebView顾复,并使用 AIDL 與主進(jìn)程通信。WebView 所在進(jìn)程可以根據(jù)業(yè)務(wù)需要在合適的時(shí)候銷毀鲁捏。

5.2 Java 內(nèi)存泄露監(jiān)控

建立類似自動(dòng)化檢查方案芯砸,至少在 Activity 和 Fragment 泄漏時(shí)自動(dòng)彈出對(duì)話框提醒開發(fā)者發(fā)現(xiàn)問題,類似 LeakCanary 方案给梅。在線上環(huán)境上報(bào)需要優(yōu)化 Hprof 內(nèi)存快照文件大小假丧,文件越小上傳的成功率越高,主要方法是裁剪圖片對(duì)應(yīng)的 byte 數(shù)組动羽。

有一個(gè) “內(nèi)存泄漏自動(dòng)化鏈路分析組件 Probe”包帚,

5.3 Native 內(nèi)存泄露監(jiān)控

高手課 下


6. 內(nèi)存溢出問題

內(nèi)存溢出(out of memory)是引用內(nèi)存申請(qǐng)超過(guò)了系統(tǒng)限制的最大堆內(nèi)存,引發(fā) OutOfMemoryError 異常运吓。Android 設(shè)備出廠后渴邦,最大堆內(nèi)存就已經(jīng)確定,相關(guān)的配置位于系統(tǒng)根目錄/system/build.prop文件中的dalvik.vm.heapgrowthlimit拘哨。

MAT

重要概念

incoming references
outgoing references

內(nèi)存指標(biāo)

當(dāng)前設(shè)備內(nèi)存占用情況 / 當(dāng)前應(yīng)用內(nèi)存占用情況

ViewRootImpl 是Activity與Window的橋梁


參考資料


創(chuàng)作不易谋梭,你的「三連」是丑丑最大的動(dòng)力,我們下次見倦青!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末瓮床,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌纤垂,老刑警劉巖矾策,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異峭沦,居然都是意外死亡贾虽,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門吼鱼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蓬豁,“玉大人,你說(shuō)我怎么就攤上這事菇肃〉胤啵” “怎么了?”我有些...
    開封第一講書人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵琐谤,是天一觀的道長(zhǎng)蟆技。 經(jīng)常有香客問我,道長(zhǎng)斗忌,這世上最難降的妖魔是什么质礼? 我笑而不...
    開封第一講書人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮织阳,結(jié)果婚禮上眶蕉,老公的妹妹穿的比我還像新娘。我一直安慰自己唧躲,他們只是感情好造挽,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著弄痹,像睡著了一般饭入。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上界酒,一...
    開封第一講書人閱讀 51,708評(píng)論 1 305
  • 那天圣拄,我揣著相機(jī)與錄音嘴秸,去河邊找鬼毁欣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛岳掐,可吹牛的內(nèi)容都是我干的凭疮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼串述,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼执解!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤衰腌,失蹤者是張志新(化名)和其女友劉穎新蟆,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體右蕊,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡琼稻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了饶囚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帕翻。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖萝风,靈堂內(nèi)的尸體忽然破棺而出嘀掸,到底是詐尸還是另有隱情,我是刑警寧澤规惰,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布睬塌,位于F島的核電站,受9級(jí)特大地震影響歇万,放射性物質(zhì)發(fā)生泄漏衫仑。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一堕花、第九天 我趴在偏房一處隱蔽的房頂上張望文狱。 院中可真熱鬧,春花似錦缘挽、人聲如沸瞄崇。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)苏研。三九已至,卻和暖如春腮郊,著一層夾襖步出監(jiān)牢的瞬間摹蘑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工轧飞, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留衅鹿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓过咬,卻偏偏與公主長(zhǎng)得像大渤,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子掸绞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355