聲明:原創(chuàng)作品,轉(zhuǎn)載請(qǐng)注明出處http://www.lxweimin.com/p/87beb3b34771
作為一名Android開(kāi)發(fā)者,對(duì)APP內(nèi)存優(yōu)化必須要有一定的了解,今天就總結(jié)下Android內(nèi)存優(yōu)化那些事。
什么是內(nèi)存
首先看下這里的內(nèi)存到底指的是什么?可以看下面這張圖:
手機(jī)中主要的存儲(chǔ)部分分兩塊RAM和ROM,RAM存儲(chǔ)程序的運(yùn)行時(shí)數(shù)據(jù),設(shè)備關(guān)機(jī)就會(huì)清空,我們也稱之為內(nèi)存;ROM也就是磁盤(pán),存放一些永久的數(shù)據(jù)。
上圖我們看到這個(gè)RAM中還有一個(gè)zRAM分區(qū),這個(gè)zRAM分區(qū)會(huì)在內(nèi)存不足時(shí)發(fā)揮作用,稍后會(huì)說(shuō)到。
到這里簡(jiǎn)單介紹了手機(jī)的內(nèi)存是指什么,當(dāng)我們不斷打開(kāi)APP時(shí),手機(jī)的內(nèi)存會(huì)被占的越來(lái)越多,而我們知道我們的手機(jī)總內(nèi)存是一定的,那么當(dāng)內(nèi)存不夠時(shí)手機(jī)會(huì)發(fā)生什么呢?
內(nèi)存不夠怎么辦
當(dāng)我們手機(jī)內(nèi)存不足時(shí),系統(tǒng)會(huì)有兩套機(jī)制發(fā)揮作用。分別是內(nèi)核交換守護(hù)進(jìn)程
和低內(nèi)存終止守護(hù)進(jìn)程
內(nèi)核交換守護(hù)進(jìn)程(kswapd)
內(nèi)核交換守護(hù)進(jìn)程 (kswapd) 是 Linux 內(nèi)核的一部分,用于將已使用內(nèi)存轉(zhuǎn)換為可用內(nèi)存。當(dāng)設(shè)備上的可用內(nèi)存不足時(shí),該守護(hù)進(jìn)程將變?yōu)榛顒?dòng)狀態(tài)。Linux 內(nèi)核設(shè)有可用內(nèi)存上下限閾值。當(dāng)可用內(nèi)存降至下限閾值以下時(shí),kswapd 開(kāi)始回收內(nèi)存。當(dāng)可用內(nèi)存達(dá)到上限閾值時(shí),kswapd 停止回收內(nèi)存。
kswapd可以刪除不再被使用到的內(nèi)存,如下圖:
kswapd也可以對(duì)暫時(shí)不用的內(nèi)存移到zRAM進(jìn)行壓縮,如果被用到時(shí),會(huì)解壓重新移到到RAM中,如下圖:
低內(nèi)存終止守護(hù)進(jìn)程
很多時(shí)候,kswapd
不能為系統(tǒng)釋放足夠的內(nèi)存。在這種情況下,系統(tǒng)會(huì)使用 onTrimMemory()
通知應(yīng)用內(nèi)存不足,應(yīng)該減少其分配量。如果這還不夠,內(nèi)核會(huì)開(kāi)始終止進(jìn)程以釋放內(nèi)存。它會(huì)使用低內(nèi)存終止守護(hù)進(jìn)程 (LMK) 來(lái)執(zhí)行此操作。
LMK 使用一個(gè)名為 oom_adj_score
的“內(nèi)存不足”分值來(lái)確定正在運(yùn)行的進(jìn)程的優(yōu)先級(jí),以此決定要終止的進(jìn)程。最高得分的進(jìn)程最先被終止。后臺(tái)應(yīng)用最先被終止,系統(tǒng)進(jìn)程最后被終止。下表列出了從高到低的 LMK 評(píng)分類別。評(píng)分最高的類別,即第一行中的項(xiàng)目將最先被終止:
[站外圖片上傳中...(image-c78455-1621768365460)]
以下是上表中各種類別的說(shuō)明:
后臺(tái)應(yīng)用:之前運(yùn)行過(guò)且當(dāng)前不處于活動(dòng)狀態(tài)的應(yīng)用。LMK 將首先從具有最高 oom_adj_score 的應(yīng)用開(kāi)始終止后臺(tái)應(yīng)用。
上一個(gè)應(yīng)用:最近用過(guò)的后臺(tái)應(yīng)用。上一個(gè)應(yīng)用比后臺(tái)應(yīng)用具有更高的優(yōu)先級(jí)(得分更低),因?yàn)橄啾饶硞€(gè)后臺(tái)應(yīng)用,用戶更有可能切換到上一個(gè)應(yīng)用。
主屏幕應(yīng)用:這是啟動(dòng)器應(yīng)用。終止該應(yīng)用會(huì)使壁紙消失。
服務(wù):服務(wù)由應(yīng)用啟動(dòng),可能包括同步或上傳到云端。
可覺(jué)察的應(yīng)用:用戶可通過(guò)某種方式察覺(jué)到的非前臺(tái)應(yīng)用,例如運(yùn)行一個(gè)顯示小界面的搜索進(jìn)程或聽(tīng)音樂(lè)。
前臺(tái)應(yīng)用:當(dāng)前正在使用的應(yīng)用。終止前臺(tái)應(yīng)用看起來(lái)就像是應(yīng)用崩潰了,可能會(huì)向用戶提示設(shè)備出了問(wèn)題。
持久性(服務(wù)):這些是設(shè)備的核心服務(wù),例如電話和 WLAN。
系統(tǒng):系統(tǒng)進(jìn)程。這些進(jìn)程被終止后,手機(jī)可能看起來(lái)即將重新啟動(dòng)。
原生:系統(tǒng)使用的極低級(jí)別的進(jìn)程(例如,kswapd)。
設(shè)備制造商可以更改 LMK 的行為。
設(shè)備對(duì)內(nèi)存的影響
接下來(lái)可以看下不同設(shè)備,內(nèi)存使用情況:
2G內(nèi)存
簡(jiǎn)單分析下,上圖展示了2G內(nèi)存設(shè)備的內(nèi)存使用情況,當(dāng)可用內(nèi)存下降到某一閾值,即圖中的
kswapd threshold
,這時(shí)kswapd會(huì)發(fā)揮作用,將緩存的內(nèi)存轉(zhuǎn)為使用內(nèi)存。如果使用的內(nèi)存越來(lái)越多,達(dá)到lmk threshold
,那么低內(nèi)存終止守護(hù)進(jìn)程就會(huì)發(fā)揮作用,根據(jù)上面的優(yōu)先級(jí)來(lái)殺死后臺(tái)進(jìn)程獲取更多內(nèi)存。
512M內(nèi)存
上圖顯示了只有512M內(nèi)存的設(shè)備內(nèi)存使用情況,可以看到由于總內(nèi)存很小,隨著使用時(shí)長(zhǎng)的增加,內(nèi)存很快就到了低內(nèi)存終止守護(hù)進(jìn)程閾值線,基本打開(kāi)一個(gè)應(yīng)用就會(huì)殺死后臺(tái)一個(gè)應(yīng)用,使用體驗(yàn)很差。
接下來(lái)看下下面這張圖:
上圖顯示了應(yīng)用數(shù)據(jù)量和內(nèi)存占用關(guān)系PSS(下面會(huì)講到),一般數(shù)據(jù)量越多內(nèi)存占用越多,紅黃綠分別代表不同的手機(jī)設(shè)備,手機(jī)配置依次升高。
RSS、PSS和USS
接下來(lái)看幾個(gè)內(nèi)存概念:RSS、PSS和USS。在講這幾個(gè)概念前首先來(lái)了解下內(nèi)存的占用量是如何計(jì)算的,內(nèi)存是分頁(yè)計(jì)算的,一個(gè)應(yīng)用內(nèi)存可能會(huì)占用好幾頁(yè),如下圖所示:
當(dāng)然,也會(huì)存在幾個(gè)應(yīng)用共享內(nèi)存頁(yè)面,例如,Google Play 服務(wù)和某個(gè)游戲應(yīng)用可能會(huì)共享位置信息服務(wù),如下所示:
為了確定應(yīng)用的內(nèi)存占用量,可以使用以下任一指標(biāo):
- 常駐內(nèi)存大小 (RSS):應(yīng)用使用的共享和非共享頁(yè)面的數(shù)量
- 按比例分?jǐn)偟膬?nèi)存大小 (PSS):應(yīng)用使用的非共享頁(yè)面的數(shù)量加上共享頁(yè)面的均勻分?jǐn)倲?shù)量(例如,如果三個(gè)進(jìn)程共享 3MB,則每個(gè)進(jìn)程的 PSS 為 1MB)
- 獨(dú)占內(nèi)存大小 (USS):應(yīng)用使用的非共享頁(yè)面數(shù)量(不包括共享頁(yè)面)
如果操作系統(tǒng)想要知道所有進(jìn)程使用了多少內(nèi)存,那么 PSS 非常有用,因?yàn)轫?yè)面只會(huì)統(tǒng)計(jì)一次。計(jì)算 PSS 需要花很長(zhǎng)時(shí)間,因?yàn)橄到y(tǒng)需要確定共享的頁(yè)面以及共享頁(yè)面的進(jìn)程數(shù)量。RSS 不區(qū)分共享和非共享頁(yè)面(因此計(jì)算起來(lái)更快),更適合跟蹤內(nèi)存分配量的變化。
我們可以通過(guò)adb來(lái)直觀的看下某個(gè)APP的內(nèi)存占用情況,adb命令如下:
adb shell dumpsys meminfo 應(yīng)用完整包名
顯示結(jié)果如下:
Pss Private Private SwapPss Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 6736 6680 4 114 22528 17971 4556
Dalvik Heap 0 0 0 0 5502 2751 2751
Stack 80 80 0 0
Ashmem 21 0 20 0
Gfx dev 280 280 0 0
Other dev 2 0 0 0
.so mmap 8081 196 5848 11
.apk mmap 319 0 72 0
.ttf mmap 55 0 28 0
.dex mmap 6261 16 5180 0
.oat mmap 75 0 20 0
.art mmap 7913 7180 248 69
Other mmap 68 4 0 0
EGL mtrack 18496 18496 0 0
GL mtrack 3348 3348 0 0
Unknown 7139 7064 28 40
TOTAL 59108 43344 11448 234 28030 20722 7307
App Summary
Pss(KB)
------
Java Heap: 7428
Native Heap: 6680
Code: 11360
Stack: 80
Graphics: 22124
Private Other: 7120
System: 4316
TOTAL: 59108 TOTAL SWAP PSS: 234
Objects
Views: 16 ViewRootImpl: 1
AppContexts: 5 Activities: 1
Assets: 7 AssetManagers: 0
Local Binders: 16 Proxy Binders: 31
Parcel memory: 4 Parcel count: 17
Death Recipients: 2 OpenSSL Sockets: 0
WebViews: 0
SQL
MEMORY_USED: 0
PAGECACHE_OVERFLOW: 0 MALLOC_SIZE: 0
我們重點(diǎn)看下``App Summary`部分,這部分展示了APP內(nèi)存(PSS)總占有量,以及各個(gè)內(nèi)存占用明細(xì):
Java:從 Java 或 Kotlin 代碼分配的對(duì)象的內(nèi)存。
Native:從 C 或 C++ 代碼分配的對(duì)象的內(nèi)存。即使您的應(yīng)用中不使用 C++,您也可能會(huì)看到此處使用了一些原生內(nèi)存,因?yàn)榧词鼓帉?xiě)的代碼采用 Java 或 Kotlin 語(yǔ)言,Android 框架仍使用原生內(nèi)存代表您處理各種任務(wù),如處理圖像資源和其他圖形。
Graphics:圖形緩沖區(qū)隊(duì)列為向屏幕顯示像素(包括 GL 表面、GL 紋理等等)所使用的內(nèi)存。(請(qǐng)注意,這是與 CPU 共享的內(nèi)存,不是 GPU 專用內(nèi)存。)
Stack:您的應(yīng)用中的原生堆棧和 Java 堆棧使用的內(nèi)存。這通常與您的應(yīng)用運(yùn)行多少線程有關(guān)。
Code:您的應(yīng)用用于處理代碼和資源(如 dex 字節(jié)碼、經(jīng)過(guò)優(yōu)化或編譯的 dex 代碼、.so 庫(kù)和字體)的內(nèi)存。
Others:您的應(yīng)用使用的系統(tǒng)不確定如何分類的內(nèi)存。
System:系統(tǒng)內(nèi)存。
減小應(yīng)用內(nèi)存占用
接下來(lái)看下如何減小我們APP內(nèi)存的占用,這里有兩種方式:
- 減小Java Heap內(nèi)存
- 減小應(yīng)用包體積
減小Java Heap內(nèi)存
這里你可能會(huì)比較好奇,上面我們可以看到內(nèi)存有很多部分組成,為什么這里偏偏只減小Java Heap的內(nèi)存就可以。主要是其他內(nèi)存分析比較困難,Android官方也不太推薦分析除了Java Heap之外的內(nèi)存,主要有以下幾個(gè)原因:
- 工具不能很好支持
- 用戶接口不友好
- 需要非常深的系統(tǒng)底層知識(shí)
- root設(shè)備,或者重新編譯源碼
- 很多內(nèi)存無(wú)法控制
下面這張是Android系統(tǒng)架構(gòu)圖,從圖中也可以看出,系統(tǒng)底層都是直接或者間接和應(yīng)用層有關(guān),就是說(shuō)底層占用的內(nèi)存和Java Heap內(nèi)存都是有關(guān)聯(lián)的,優(yōu)化了Java Heap內(nèi)存也就間接優(yōu)化了其他內(nèi)存部分。
接下來(lái)具體看下如何減小Java Heap內(nèi)存,首先來(lái)了解下什么是Java Heap,Java Heap即Java堆,也是虛擬機(jī)對(duì)象主要存放的地方,一般也是垃圾回收的重點(diǎn)位置。當(dāng)然Java 虛擬機(jī)除了Java堆外還有其他分區(qū):程序計(jì)數(shù)器、方法區(qū)、虛擬機(jī)棧和本地方法棧,這里由于篇幅有限這幾個(gè)分區(qū)作用就不過(guò)多介紹了,如下:
由于Java虛擬機(jī)是分代回收垃圾,在Java 7中Java Heap被分為新生代、老年代和永久代,新生代又分為Eden、From Survivor和To Survivor區(qū),他們的大小比例是8:1:1,來(lái)說(shuō)下它的工作機(jī)制,首先新生成的對(duì)象會(huì)被分配到Eden區(qū),當(dāng)Eden區(qū)內(nèi)存滿了時(shí)會(huì)發(fā)生一次小型的垃圾回收,回收時(shí)會(huì)將Eden區(qū)中存活的對(duì)象復(fù)制到From Survivor區(qū)中,然后把Eden區(qū)清空。當(dāng)From Survivor也滿了,就會(huì)把Eden區(qū)和From Survivor中存活的對(duì)象復(fù)制到To Survivor區(qū)中,然后清空Eden和From Survivor區(qū),接著會(huì)把From Survivor區(qū)和To Survivor區(qū)做交換,保持To Survivor區(qū)中的對(duì)象為空,就這樣不斷重復(fù),當(dāng)To Survivor中的空間也滿了,無(wú)法存在Eden和From Survivor區(qū)中的對(duì)象時(shí),就會(huì)把他們存入老年代。另外當(dāng)某個(gè)對(duì)象在Survivor區(qū)中經(jīng)歷一次GC而存活下來(lái),那么他的年齡就會(huì)加一,默認(rèn)情況下當(dāng)超過(guò)15歲時(shí)就會(huì)被丟入老年代中。當(dāng)老年代空間也滿了虛擬機(jī)會(huì)發(fā)生一次Full GC,一般大對(duì)象會(huì)直接放入老年代,比如大數(shù)組這種需要連續(xù)存儲(chǔ)空間的。永久代一般存放靜態(tài)對(duì)象比如class文件,靜態(tài)方法、常量等。永久代對(duì)垃圾回收沒(méi)有顯著的影響,主要回收無(wú)用類和廢棄常量。在Java 8中移除了永久代改為了元空間(Metaspace),因此不會(huì)再出現(xiàn)“java.lang.OutOfMemoryError: PermGen error”錯(cuò)誤。
對(duì)象存活檢測(cè)
上面我們了解了有關(guān)Java Heap內(nèi)存分配和回收相關(guān)原理,接下來(lái)你可能會(huì)有疑問(wèn),Java虛擬機(jī)是如何判斷一個(gè)對(duì)象是否是存活狀態(tài)。換句話說(shuō)一個(gè)對(duì)象在什么情況下該被回收呢,這個(gè)問(wèn)題自然而然就是對(duì)象不再用到的時(shí)候就可以被回收,而這個(gè)“不再被用到”就是這個(gè)對(duì)象不被任何一個(gè)對(duì)象所引用的意思。這里主要有兩種方式來(lái)判斷一個(gè)對(duì)象有沒(méi)有被其他對(duì)象引用:引用計(jì)數(shù)法
和根搜索算法
。
引用計(jì)數(shù)法
引用計(jì)數(shù)法顧名思義,就是在這個(gè)對(duì)象里有一個(gè)計(jì)數(shù)的量,當(dāng)這個(gè)對(duì)象被其他對(duì)象引用時(shí)這個(gè)計(jì)數(shù)量就會(huì)加1,比如被2兩個(gè)對(duì)象引用就是2,并且當(dāng)其他對(duì)象不再引用這個(gè)對(duì)象時(shí),這個(gè)計(jì)數(shù)量會(huì)相應(yīng)減1。當(dāng)這個(gè)計(jì)數(shù)量為0時(shí)就代表這個(gè)對(duì)象不被任何對(duì)象引用,那么虛擬機(jī)在下次垃圾回收時(shí)就會(huì)回收這個(gè)對(duì)象。不過(guò)這種方式有一個(gè)弊端,就是互相引用,也就是有兩個(gè)對(duì)象互相引用對(duì)方,那么他們的計(jì)數(shù)值都是1,然而這兩個(gè)對(duì)象除了互相引用外就沒(méi)有其他對(duì)象引用了,其實(shí)針對(duì)這種情況,這兩個(gè)對(duì)象都應(yīng)該被回收的,但是他們的計(jì)數(shù)值都是1,導(dǎo)致虛擬機(jī)無(wú)法對(duì)他們進(jìn)行回收。
為了解決這個(gè)問(wèn)題,虛擬機(jī)使用了另一種方式也就是 根搜索算法
。
根搜索算法
這種算法的基本思路:
(1)通過(guò)一系列名為“GC Roots”的對(duì)象作為起始點(diǎn),尋找對(duì)應(yīng)的引用節(jié)點(diǎn)。
(2)找到這些引用節(jié)點(diǎn)后,從這些節(jié)點(diǎn)開(kāi)始向下繼續(xù)尋找它們的引用節(jié)點(diǎn)。
(3)重復(fù)(2)。
(4)搜索所走過(guò)的路徑稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí),就證明此對(duì)象是不可用的。
在java語(yǔ)言中,可作為GCRoot的對(duì)象包括以下幾種:
java虛擬機(jī)棧(棧幀中的本地變量表)中的引用的對(duì)象。
方法區(qū)中的類靜態(tài)屬性引用的對(duì)象。
方法區(qū)中的常量引用的對(duì)象。
-
本地方法棧中JNI本地方法的引用對(duì)象。
根搜索算法
內(nèi)存泄漏和內(nèi)存溢出
了解了根搜索算法后,我們知道當(dāng)一個(gè)對(duì)象不在引用鏈上的時(shí)候就可以對(duì)它進(jìn)行回收了,但是可能存在一種情況就是假如我們想回收一個(gè)對(duì)象,但是由于某些原因?qū)е逻@個(gè)對(duì)象一直在引用鏈上,這樣這個(gè)對(duì)象就一直無(wú)法被回收了。我們稱這種現(xiàn)象為內(nèi)存泄漏
。當(dāng)我們的應(yīng)用很多地方存在這種內(nèi)存泄漏現(xiàn)象時(shí),隨著應(yīng)用的使用,內(nèi)存占用會(huì)越來(lái)越高,當(dāng)內(nèi)存使用量達(dá)到系統(tǒng)規(guī)定的上限,APP就會(huì)報(bào)一個(gè)OutOfMemery的異常,我們稱這種現(xiàn)象為內(nèi)存溢出。
那么什么情況會(huì)導(dǎo)致內(nèi)存泄漏呢,這里簡(jiǎn)單羅列下:
- 集合類的不規(guī)范使用
- static修飾的成員變量
- 非靜態(tài)內(nèi)部類或者匿名內(nèi)部類
- 資源對(duì)象使用后未關(guān)閉
上面只是簡(jiǎn)單的列了下可能會(huì)導(dǎo)致內(nèi)存泄漏的情況,更加詳細(xì)的原因可以參看其他相關(guān)內(nèi)存泄漏的文章。
知道內(nèi)存泄漏的現(xiàn)象及其后果后,接下來(lái)我們就應(yīng)該去解決我們應(yīng)用中的內(nèi)存泄漏問(wèn)題,但是導(dǎo)致內(nèi)存泄漏的代碼往往很隱蔽我們一時(shí)是很難排查的,這是就可以借助一些內(nèi)存分析工具。比如可以用Android Studio自帶的性能分析工具Profiler
,或者也可以在應(yīng)用工程中集成一個(gè)內(nèi)存分析的三方庫(kù)LeakCanary
,這個(gè)庫(kù)具體的使用方式可以上他們的官網(wǎng)了解LeakCanary
減小包體積
上面我們用大量篇幅來(lái)分析了減小Java Heap大小來(lái)優(yōu)化內(nèi)存,其實(shí)應(yīng)用的安裝包大小對(duì)內(nèi)存也是由影響的,比如如果安裝包中有很多圖片等資源的話,這會(huì)增加Java Heap、Native Heap和Graphics的內(nèi)存占用量。當(dāng)然還有其他一些文件也會(huì)有影響,具體表格如下: