點(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 路線
虛擬機(jī)中的對(duì)象: Java 虛擬機(jī) | 拿放大鏡看對(duì)象
Java 內(nèi)存分配: Java 虛擬機(jī) | 內(nèi)存分配模型
Android 路線
程序執(zhí)行: Android 虛擬機(jī) | 從類加載到程序執(zhí)行
內(nèi)存指標(biāo): Android | 內(nèi)存指標(biāo)與分析方法
Android 垃圾回收: Android 虛擬機(jī) | 垃圾回收機(jī)制
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的橋梁
參考資料
- 《Android 內(nèi)存優(yōu)化雜談》 —— 張紹文 著
- 《Android 開發(fā)高手課 · 內(nèi)存優(yōu)化(上)》《下》 —— 張紹文著
- 《深入探索 Android 內(nèi)存優(yōu)化(煉獄級(jí)別 - 上)》《下》 —— JsonChao(平安)著
- 《必知必會(huì) | Android 性能優(yōu)化的方面方面都在這兒》 —— 鴻洋(懂車帝)著
- 《實(shí)踐 App 內(nèi)存優(yōu)化:如何有序地做內(nèi)存分析與優(yōu)化》 —— 舒大飛(攜程)著
- 《Android 內(nèi)存優(yōu)化之 OOM》 —— 胡凱 著 (多篇)
- 《Android 內(nèi)存分析命令》 —— Gityuan(字節(jié)跳動(dòng))著
- 《Linux 內(nèi)存管理》 —— Gityuan(字節(jié)跳動(dòng))著
- 《進(jìn)程間的內(nèi)存分配》 —— Android Developers
- 《Android 移動(dòng)性能實(shí)戰(zhàn)》—— 騰訊 SNG 專項(xiàng)測(cè)試團(tuán)隊(duì) 著
創(chuàng)作不易谋梭,你的「三連」是丑丑最大的動(dòng)力,我們下次見倦青!