優(yōu)化安卓Android應(yīng)用內(nèi)存使用的小竅門

簡(jiǎn)介

Android 系統(tǒng)中的內(nèi)存分配和釋放總是會(huì)帶來一定的代價(jià)。 中國有句話叫做 “由儉入奢易,由奢入儉難”,真實(shí)地反應(yīng)了內(nèi)存的使用情況。

我們?cè)O(shè)想這樣一種最壞的場(chǎng)景,當(dāng)您在編譯包含數(shù)百億行代碼的應(yīng)用時(shí),突然出現(xiàn)內(nèi)存溢出 (OOM) 的情況并導(dǎo)致系統(tǒng)崩潰。 于是您開始調(diào)試應(yīng)用,分析 hprof 文件。 幸運(yùn)的話,您可以找到問題的根源,并修復(fù)占用內(nèi)存最多的進(jìn)程 (memory killer)。 但有時(shí)您可能不那么走運(yùn),您會(huì)發(fā)現(xiàn)系統(tǒng)中有如此多的小型變量和臨時(shí)文件占用了內(nèi)存資源,以至于簡(jiǎn)單的修補(bǔ)也無濟(jì)于事。這意味著您必須重構(gòu)代碼,但卻只能節(jié)省幾千字節(jié)甚至幾字節(jié)的內(nèi)存,而其中還存在潛在的風(fēng)險(xiǎn)。

這篇文章詳細(xì)介紹了 Android 內(nèi)存管理,解釋了管理系統(tǒng)中非常重要的幾個(gè)方面。 另外本文也會(huì)涉及改進(jìn)內(nèi)存管理、檢測(cè)和避免內(nèi)存泄漏,以及分析內(nèi)存使用情況等內(nèi)容。

Android 內(nèi)存管理

Android 使用分頁和 mmap 而非提供交換區(qū)來管理內(nèi)存,即除非釋放所有引用對(duì)象,否則凡是應(yīng)用所占的內(nèi)存都不能被調(diào)用。

面向應(yīng)用進(jìn)程的 Dalvik* 虛擬機(jī)堆內(nèi)存是有限的。 應(yīng)用啟動(dòng)時(shí)為 2MB,最大分配內(nèi)存(標(biāo)記為 "largeHeap" )不得超過 36MB(因具體設(shè)備配置而異)。 典型的大堆應(yīng)用包括圖片/視頻編輯器、攝像機(jī)、圖庫和主屏幕。

Android 采用 LRU 高速緩存來存儲(chǔ)后臺(tái)應(yīng)用進(jìn)程。 當(dāng)系統(tǒng)運(yùn)行內(nèi)存較低時(shí),它會(huì)根據(jù) LRU 策略“殺死”進(jìn)程,但同時(shí)也會(huì)考慮哪些應(yīng)用占用了最多的內(nèi)存。 現(xiàn)在 Android 最多可支持 20 個(gè)后臺(tái)進(jìn)程(因具體設(shè)備配置而異)。 如果需要應(yīng)用在后臺(tái)所處的時(shí)間更長(zhǎng),那么您需要在轉(zhuǎn)至后臺(tái)前先釋放不必要的內(nèi)存,這樣 Android 系統(tǒng)發(fā)出錯(cuò)誤信息甚至終止應(yīng)用的可能性會(huì)大大降低。

如何提高內(nèi)存利用率

Android 是一款全球性移動(dòng)平臺(tái),數(shù)百億的 Android 開發(fā)人員都致力于構(gòu)建穩(wěn)定而又可擴(kuò)展的應(yīng)用。 以下是提高 Android 應(yīng)用內(nèi)存利用率的一些方法和最佳實(shí)踐:

請(qǐng)慎重使用 “抽象化” 設(shè)計(jì)模式。 盡管從設(shè)計(jì)模式的角度來看,抽象化能夠幫助構(gòu)建更加靈活的軟件架構(gòu), 但是在移動(dòng)領(lǐng)域,抽象化可能會(huì)帶來副作用,因?yàn)檫@需要執(zhí)行額外的代碼,不僅費(fèi)時(shí)而又多占內(nèi)存。 除非抽象化能為您的應(yīng)用帶來巨大的優(yōu)勢(shì),否則最好不要使用。

避免使用 "enum"。 Enum 比普通靜態(tài)常數(shù)的內(nèi)存分配多出一倍,所以請(qǐng)不要使用。

嘗試使用經(jīng)過優(yōu)化的 SparseArray、SparseBooleanArray 和 LongSparseArray 集合來代替 HashMap。 HashMap 在每次映射中都會(huì)分配一個(gè)條目對(duì)象,這會(huì)引起內(nèi)存效率降低,并且導(dǎo)致性能低下的 “自動(dòng)裝箱與拆箱” 操作貫穿整個(gè)使用過程。 相反,SparseArray 這類的集合會(huì)將關(guān)鍵值映射到普通數(shù)組。 但是請(qǐng)謹(jǐn)記,這些經(jīng)過優(yōu)化的集合不適用于大量條目。在執(zhí)行添加/刪除/搜索操作時(shí),如果您的數(shù)據(jù)集中的記錄超過幾千條,那么其速度比 Hashmap 還要慢。

避免創(chuàng)建不必要的對(duì)象。 如果可能,請(qǐng)不要為短期的臨時(shí)對(duì)象專門分配內(nèi)存;創(chuàng)建對(duì)象越少則垃圾回收越少。

檢查您應(yīng)用的可用堆。 調(diào)用 ActivityManager::getMemoryClass() 以查詢應(yīng)用的可用堆值 (MB)。 如果您的分配內(nèi)存多于可用內(nèi)存,將會(huì)發(fā)生內(nèi)存溢出異常。 如果您的應(yīng)用在 AndroidManifest.xml 中申請(qǐng)了 "largeHeap",那么您可以調(diào)用 ActivityManager::getMemoryClass() 以查詢大堆的估值。

通過執(zhí)行 onTrimMemory() 回調(diào)來保持與系統(tǒng)協(xié)調(diào)。 在您的 Activity/Service/ContentProvider 中執(zhí)行 ComponentCallbacks2::onTrimMemory(int),根據(jù)最新系統(tǒng)限制逐步釋放內(nèi)存。 onTrimMemory(int) 不僅能夠幫助提高整體系統(tǒng)響應(yīng)速度,還能讓您的進(jìn)程在系統(tǒng)中存活更久。

當(dāng)出現(xiàn) TRIM_MEMORY_UI_HIDDEN 時(shí),表明您應(yīng)用中的所有 UI 已被隱藏,您需要釋放 UI 資源。 當(dāng)您的應(yīng)用在前臺(tái)運(yùn)行時(shí),您可能會(huì)收到 TRIM_MEMORY_RUNNING[MODERATE/LOW/CRITICAL],或者在后臺(tái)運(yùn)行時(shí)收到 TRIM_MEMORY_[BACKGROUND/MODERATE/COMPLETE]。 當(dāng)系統(tǒng)內(nèi)存緊缺時(shí),您可以根據(jù)釋放內(nèi)存策略釋放非關(guān)鍵資源。

請(qǐng)謹(jǐn)慎使用 service(服務(wù))。 如果您需要一個(gè) service 在后臺(tái)運(yùn)行任務(wù),那么除非它需要主動(dòng)執(zhí)行任務(wù),否則請(qǐng)避免使其持續(xù)運(yùn)行。 嘗試使用 IntentService 來縮短 service 的生命周期,它會(huì)在完成任務(wù)以后終止自己。 謹(jǐn)慎使用 service,確保在不需要時(shí)全部終止運(yùn)行。 否則最壞的結(jié)果將會(huì)是整個(gè)系統(tǒng)性能極為低下,用戶只能卸載應(yīng)用(如果可能的話)。

但是如果您想創(chuàng)建一個(gè)需要長(zhǎng)期運(yùn)行的應(yīng)用,如音樂播放器服務(wù),那您應(yīng)該在 AndroidManifest.xml 中為您的 Service 設(shè)置屬性 "android:process",以便將其拆分為兩個(gè)進(jìn)程:一個(gè)針對(duì) UI,一個(gè)針對(duì)后臺(tái)服務(wù)。 UI 進(jìn)程中的資源可在隱藏之后釋放,同時(shí)運(yùn)行后臺(tái)播放服務(wù)。 切記,后臺(tái)服務(wù)進(jìn)程不得訪問任何 UI,否則內(nèi)存分配將會(huì)增加一倍甚至兩倍!

謹(jǐn)慎使用 External Libraries。 External Libraries 通常為非移動(dòng)設(shè)備編寫,在 Android 中使用時(shí)效率較低。 在使用之前,您必須要考慮為移動(dòng)設(shè)備導(dǎo)入和優(yōu)化 library 所費(fèi)的周折。 如果您使用 library 僅是為了實(shí)現(xiàn)其數(shù)千用途中的一兩個(gè)功能,那么您最好還是自己動(dòng)手實(shí)施吧。

使用具有合適分辨率的 bitmap(位圖)。 加載一個(gè)具有您所需分辨率的 bitmap,或者如果初始 bitmap 分辨率過高,則按需縮小。

使用 Proguard* 和 zipalign。 Proguard 工具能夠刪除未調(diào)用的代碼,并混淆代碼中的類、函數(shù)和字段。 它能壓縮您的代碼以減少映射時(shí)所需的 RAM 頁。 Zipalign 工具能夠重新對(duì)齊您的 APK。 如果不運(yùn)行 zipalign 的話,資源文件就無法從 APK 映射,因而會(huì)需要更多的內(nèi)存。

如何避免內(nèi)存泄漏

借助以上方法巧妙使用內(nèi)存能讓您的應(yīng)用逐步受益,并且提高應(yīng)用在系統(tǒng)中的存活時(shí)間。 但是,一旦發(fā)生內(nèi)存泄漏,所有這些優(yōu)勢(shì)都不復(fù)存在。 下面是開發(fā)人員需謹(jǐn)記的一些常見潛在泄漏。

查詢完數(shù)據(jù)庫后切記關(guān)閉光標(biāo)(cursor)。 如果您需要長(zhǎng)期打開光標(biāo),那么您必須謹(jǐn)慎使用,并且在數(shù)據(jù)庫任務(wù)結(jié)束時(shí)立即關(guān)閉。

記得在調(diào)用 registerReceiver() 后調(diào)用 unregisterReceiver()。

避免 Context 泄漏。 如果您在 Activity 中申請(qǐng)了一個(gè)靜態(tài)成員變量 "Drawable",然后在 onCreate() 中調(diào)用 view.setBackground(drawable),那么由于 drawable 將 view 設(shè)置為回調(diào),而 view 引用了已存在的 Activity (Context),因此即使在旋轉(zhuǎn)屏幕后創(chuàng)建了一個(gè)新的 Activity 實(shí)例,之前的 Activity 實(shí)例也未從內(nèi)存中釋放。 泄漏的 Activity 實(shí)例則意味著占用大量的內(nèi)存,也將極易導(dǎo)致 OOM。

有兩種方式可以避免這種泄漏:

不要長(zhǎng)期引用一個(gè) context-activity。 對(duì) activity 的引用期限應(yīng)該與 activity 的生命周期保持同步。

嘗試使用 context-application 來代替 context-activity。

避免在 Activity 中使用非靜態(tài)的內(nèi)部類。 在 Java 中,非靜態(tài)匿名類能夠隱式引用外部類。 如果不小心存儲(chǔ)了這種引用,會(huì)引起 Activity 駐留,妨礙對(duì)其進(jìn)行垃圾回收。 因此,使用靜態(tài)內(nèi)部類,并弱引用內(nèi)部的 activity 對(duì)象。

謹(jǐn)慎使用線程。 Java 中的線程是垃圾回收的根源,即 Dalvik 虛擬機(jī) (DVM) 對(duì)運(yùn)行時(shí)系統(tǒng)中的所有活動(dòng)線程保持強(qiáng)引用,因此保持運(yùn)行狀態(tài)的線程將無法實(shí)現(xiàn)垃圾回收。 除非被 Android 系統(tǒng)明令關(guān)閉,或者整個(gè)進(jìn)程被 Android 系統(tǒng)“殺死”,否則 Java 線程會(huì)一直存在。 與之不同,Android 應(yīng)用架構(gòu)提供了多種類,以便開發(fā)人員更輕松地管理后臺(tái)線程:

使用 Loader 代替線程,以執(zhí)行短期非同步后臺(tái)查詢操作,同時(shí)協(xié)調(diào) Activity 生命周期。

使用 Service 并通過使用 BroadcastReceiver 向 Activity 返回結(jié)果報(bào)告。

使用 AsyncTask 執(zhí)行短期操作。

如何分析內(nèi)存的使用情況

為了在線/離線均能了解更多關(guān)于內(nèi)存使用量的信息,您可以通過在 Android Debug Bridge (ADB) 上使用 logcat 命令來檢查 Android 的系統(tǒng)日志,或者捕捉轉(zhuǎn)儲(chǔ)內(nèi)存信息并將其命名為特定包,或者使用 Dalvik 調(diào)試監(jiān)測(cè)程序服務(wù)器 (DDMS) 和內(nèi)存分析工具 (MAT) 等其他工具。下面是分析應(yīng)用內(nèi)存使用情況的一些方法簡(jiǎn)介。

1, 充分了解有關(guān) Dalvik 虛擬機(jī)的垃圾回收 (GC) 日志信息,具體示例和定義如下所示:

GC 的原因: 是什么觸發(fā)了垃圾回收?回收類型是什么? 原因包括:

GC_CONCURRENT: 當(dāng)您的堆開始填滿時(shí),觸發(fā)并發(fā)垃圾回收以釋放內(nèi)存。

GC_FOR_ALLOC: 當(dāng)您的堆已經(jīng)占滿時(shí),您的應(yīng)用又試圖分配內(nèi)存,所以系統(tǒng)必須停止您的應(yīng)用并重新分配內(nèi)存,此時(shí)便發(fā)生垃圾回收。

GC_HPROF_DUMP_HEAP: 當(dāng)您創(chuàng)建 HPROF 文件來分析堆時(shí),發(fā)生垃圾回收。

GC_EXPLICIT: 例如當(dāng)您調(diào)用 gc() 時(shí)產(chǎn)生的顯式垃圾回收(在需要時(shí)您不應(yīng)該調(diào)用,而是應(yīng)該相信垃圾回收器的運(yùn)行作用)

Amount freed(釋放的內(nèi)存量): 本次垃圾回收所釋放出來的內(nèi)存。

Heap stats(堆數(shù)據(jù)): 空閑內(nèi)存的百分比和(活動(dòng)對(duì)象數(shù)量) / (堆總量)

External memory stats(外部?jī)?nèi)存數(shù)據(jù)): API level 10 及以下版本的外部空間分配內(nèi)存量(分配內(nèi)存量) / (發(fā)生垃圾回收的臨界值)

Pause time(暫停時(shí)間): 堆越大,則暫停時(shí)間越長(zhǎng)。 并發(fā)的暫停時(shí)間包括兩次暫停:一次是垃圾回收之初,一次是回收接近完畢時(shí)。

GC 日志越大,則您的應(yīng)用中分配/釋放的內(nèi)存越多,同時(shí)也意味著用戶體驗(yàn)會(huì)有所下降。

2.使用 DDMS 來查看堆更新并追蹤分配記錄。

利用 DDMS 能夠便捷地查看具體進(jìn)程的實(shí)時(shí)堆分配情況。 嘗試在 "Heap" 選項(xiàng)卡中與您的應(yīng)用進(jìn)行交互,并查看堆分配的更新情況。 這能夠幫助您識(shí)別哪些操作占用了過多內(nèi)存。 "Allocation Tracker" 選項(xiàng)卡展示了最近所有的內(nèi)存分配,提供了許多信息,其中包括對(duì)象類型,分配所在的線程、類和文件以及線路等。 欲了解更多關(guān)于使用 DDMS 進(jìn)行堆分析的信息,請(qǐng)參閱本文結(jié)尾的參考資料章節(jié)。 以下截圖展示了運(yùn)行中的 DDMS,其中包括當(dāng)前進(jìn)程的狀況和具體進(jìn)程的內(nèi)存堆統(tǒng)計(jì)情況。


3.查看整體內(nèi)存分配情況。

通過執(zhí)行 adb 命令: “adb shell dumpsys meminfo ”,您可以看到您所有應(yīng)用的當(dāng)前內(nèi)存分配情況,單位為 KB。


一般您只需關(guān)注 "Pss Total" 和 "Private Dirty" 欄即可。 "Pss Total" 一欄包括所有的 Zygote 分配(如以上 PPS 定義所述,根據(jù)其在進(jìn)程中的份額進(jìn)行加權(quán)) "Private Dirty" 數(shù)是指專門用于您應(yīng)用的堆、您自己的應(yīng)用以及從 Zygote 中分離應(yīng)用進(jìn)程以后進(jìn)行修改的所有 Zygote 分配頁的實(shí)際 RAM。

此外,"ViewRootImpl" 展示了進(jìn)程中活躍的根視圖數(shù)量。 每個(gè)根視圖均與一個(gè)窗口相連,因此有助于您識(shí)別與對(duì)話框或其他窗口相關(guān)的內(nèi)存泄漏。 "AppContexts" 和 "Activities" 則顯示了當(dāng)前活躍在您進(jìn)程中的應(yīng)用 Context 和 Activity 對(duì)象的數(shù)量。 這將十分有助于快速發(fā)現(xiàn)因?qū)ζ溥M(jìn)行靜態(tài)引用而無法進(jìn)行垃圾回收的 Activity 對(duì)象泄漏(這種情況十分常見)。 這些對(duì)象通常具有很多其他與之相關(guān)的內(nèi)存分配,是一種追蹤大規(guī)模內(nèi)存泄漏的好方法。

4.捕捉堆轉(zhuǎn)儲(chǔ) (Heap Dump) 文件并利用 Eclipse* 內(nèi)存分析工具 (MAT) 對(duì)其進(jìn)行分析。

您可以通過使用 DDMS 或者在源代碼中調(diào)用 Debug::dumpHprofData() 來直接捕捉一個(gè)堆轉(zhuǎn)儲(chǔ)文件,以獲取更精確的結(jié)果。 然后您需要使用 hprof-conv 工具來生成轉(zhuǎn)換的 HPROF 文件。 以下截圖是 MAT 中顯示的內(nèi)存分析結(jié)果。


總結(jié)

為了創(chuàng)建更多內(nèi)存友好型應(yīng)用,Android 開發(fā)人員需要對(duì) Android 內(nèi)存管理有一個(gè)基本的認(rèn)識(shí)。 開發(fā)人員應(yīng)該踐行有效的內(nèi)存使用方法、使用分析工具并且實(shí)施本文所提供的方法。 在實(shí)施期間,最好先創(chuàng)建一個(gè)穩(wěn)定而又可擴(kuò)展的應(yīng)用,而非專注于應(yīng)用修復(fù)方法。

參考資料

http://developer.android.com/training/articles/memory.html

https://developer.android.com/tools/debugging/debugging-memory.html

http://developer.android.com/training/articles/perf-tips.html

http://android-developers.blogspot.co.uk/2009/01/avoiding-memory-leaks.html

http://developer.android.com/tools/debugging/ddms.html

http://www.curious-creature.com/2008/12/18/avoid-memory-leaks-on-android/comment-page-1/

https://eclipse.org/mat/

最后編輯于
?著作權(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)容