Android內(nèi)存優(yōu)化雜談
Android內(nèi)存優(yōu)化是我們性能優(yōu)化工作中比較重要的一環(huán),這里其實主要包括兩方面的工作:
1、優(yōu)化RAM,即降低運行時內(nèi)存。這里的目的是防止程序發(fā)生OOM異常,以及降低程序由于內(nèi)存過大被LMK機制殺死的概率。另一方面,不合理的內(nèi)存使用會使GC大大增多,從而導(dǎo)致程序變卡。
2、優(yōu)化ROM,即降低程序占ROM的體積。這里主要是為了降低程序占用的空間,防止由于ROM空間不足導(dǎo)致程序無法安裝。
本文的著重點為第一點,總結(jié)概述降低應(yīng)用運行內(nèi)存的技巧。在這里我們不再細述PSS、USS等概念與Android應(yīng)用的內(nèi)存管理,如對這部分內(nèi)容感興趣,可自行閱讀文末的參考文章。
內(nèi)存泄露的檢測與修改
內(nèi)存泄露:簡單來說對象由于編碼錯誤或系統(tǒng)原因,仍然存在著對其直接或間接的引用,導(dǎo)致系統(tǒng)無法進行回收。內(nèi)存泄露,容易留下邏輯隱患,同時增加了應(yīng)用內(nèi)存峰值與發(fā)生OOM的概率。它屬于bug issue,是我們一定要修改的。
下面是造成內(nèi)存泄露的一些常見原因,但是如何建立一套發(fā)現(xiàn)內(nèi)存泄露、解決內(nèi)存泄露的閉環(huán)方案,才是我們工作的重點。
一. 內(nèi)存泄露的監(jiān)控方案
Square的開源庫leakcanry是一個非常不錯的選擇,它通過弱引用方式偵查Activity或?qū)ο蟮纳芷?,若發(fā)現(xiàn)內(nèi)存泄露自動dump Hprof文件,通過HAHA庫得到泄露的最短路徑,最后通過notification展示。
內(nèi)存泄露判斷與處理的流程如下圖 ,各自運行的進程空間(主進程通過idlehandler,HAHA分析使用的是單獨的進程):
微信在leakcanry推出之前已經(jīng)有了自己的內(nèi)存泄露監(jiān)控體系,與leakcanry大致有以下的區(qū)別:
- 在微信中,對于4.0以上的機型也是采用通過注冊ActivityLifecycleCallbacks接口,對于4.0以下的機型我們會嘗試反射ActivityThread中的mInstrumentation對象。當然,現(xiàn)在微信也改成只支持android-15以上,美美噠。
- leakcanry盡管使用了idlehandler與分進程,但是dumphprof依然會造成應(yīng)用明顯的卡頓(SuspendAll Thread)。而在三星等一些手機,系統(tǒng)會緩存最后一個Activity,所以在微信,我們采取了更嚴格的檢測模式,即泄露三次確認以及經(jīng)過5個新建的Activity,確保不是由于系統(tǒng)緩存的原因造成。
- 在微信中,當發(fā)現(xiàn)疑似內(nèi)存泄露時會彈出對話框,當我們主動點擊時才會去做dumpHprof以及上傳Hprof快照的操作,而是否誤報、泄露鏈等分析工作也是放于服務(wù)器端。
事實上,通過對leakcanry做簡單的定制,我們就可以實現(xiàn)以下一個內(nèi)存泄露監(jiān)控閉環(huán)。
二. 對系統(tǒng)內(nèi)存泄露的Hack Fix
AndroidExcludedRefs列出了一些由于系統(tǒng)原因?qū)е乱脽o法釋放的例子,同時對于大多數(shù)的例子,都會提供建議如何通過hack的建議去修復(fù)。在微信中,對TextLine、InputMethodManager、AudioManger、android.os.Message也采用了類似Hack的方式。
三. 通過兜底回收內(nèi)存
Activity泄漏會導(dǎo)致該Activity引用到的Bitmap、DrawingCache等無法釋放,對內(nèi)存造成大的壓力,兜底回收是指對于已泄漏Activity,嘗試回收其持有的資源,泄漏的僅僅是一個Activity空殼,從而降低對內(nèi)存的壓力。
做法也非常簡單,在Activity onDestory時候從view的rootview開始,遞歸釋放所有子view涉及的圖片,背景,DrawingCache,監(jiān)聽器等等資源,讓Activity成為一個不占資源的空殼,泄露了也不會導(dǎo)致圖片資源被持有。
…
…
Drawable d = iv.getDrawable();
if (d != null) {
d.setCallback(null);
}
iv.setImageDrawable(null);
...
...
總的來說,我們不是只懂得一些內(nèi)存泄露解決方法就可以,更重要的是通過日常測試與監(jiān)控,得到內(nèi)存泄露檢測與修改的一整套閉環(huán)體系。
降低運行時內(nèi)存的一些方法
當我們能確保應(yīng)用中不會出現(xiàn)內(nèi)存泄露時,我們需要一些其他的方法來降低運行時的內(nèi)存。更多的時候,我們其實只希望降低應(yīng)用發(fā)生OOM的概率。
Android OOM:
- Android 2.x系統(tǒng),當dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值時候就會發(fā)生OOM。其中bitmap是放于external中 。
- Android 4.x系統(tǒng),廢除了external的計數(shù)器,類似bitmap的分配改到dalvik的java heap中申請,只要allocated + 新分配的內(nèi)存 >= dalvik heap 最大值的時候就會發(fā)生OOM(art運行環(huán)境的統(tǒng)計規(guī)則還是和dalvik保持一致)
一. 減少bitmap占用的內(nèi)存
說到內(nèi)存,bitmap必然是這里的大頭。對于bitmap內(nèi)存占用,想說的有以下幾點:
1、防止bitmap占用資源多大導(dǎo)致OOM
Android 2.x 系統(tǒng) BitmapFactory.Options 里面隱藏的的inNativeAlloc反射打開后,申請的bitmap就不會算在external中。對于Android 4.x系統(tǒng),可采用facebook的fresco庫,即可把圖片資源放于native中。
2、圖片按需加載
即圖片的大小不應(yīng)該超過view的大小。在把圖片載入內(nèi)存之前,我們需要先計算出一個合適的inSampleSize縮放比例,避免不必要的大圖載入。對此,我們可以重載drawable與ImageView,例如在Activity ondestroy時,檢測圖片大小與View的大小,若超過,可以上報或提示。
3、統(tǒng)一的bitmap加載器
Picasso、Fresco都是比較出名的加載庫,同樣微信也有自己的庫ImageLoader。加載庫的好處在于將版本差異、大小處理對使用者不感知。有了統(tǒng)一的bitmap加載器,我們可以在加載bitmap時,若發(fā)生OOM(try catch方式),可以通過清除cache,降低bitmap format(ARGB8888/RBG565/ARGB4444/ALPHA8)等方式,重新嘗試。
4、圖片存在像素浪費
對于.9圖,美工可能在出圖時在拉伸與非拉伸區(qū)域都有大量的像素重復(fù)。通過獲取圖片的像素ARGB值,計算連續(xù)相同的像素區(qū)域,自定義算法判定這些區(qū)域是否可以縮放。關(guān)鍵也是需要將這些工作做到系統(tǒng)化,可及時發(fā)現(xiàn)問題,解決問題。
一個好的imageLoader,可以將2.X、4.X或5.X對圖片加載的處理對使用者隱藏,同時也可以將自適應(yīng)大小、質(zhì)量等放于框架中。
二. 自身內(nèi)存占用監(jiān)控
對于系統(tǒng)函數(shù)onLowMemory等函數(shù)是針對整個系統(tǒng)而已的,對于本進程來說,其dalvik內(nèi)存距離OOM的差值并沒有體現(xiàn),也沒有回調(diào)函數(shù)供我們及時釋放內(nèi)存。假若能有那么一套機制,可以實時監(jiān)控進程的堆內(nèi)存使用率,達到設(shè)定值即關(guān)于通知相關(guān)模塊進行內(nèi)存釋放,這會大大的降低OOM。
- 實現(xiàn)原理
這個其實比較簡單,通過Runtime獲得maxMemory,而totalMemory-freeMemory即為當前真正使用的dalvik內(nèi)存。
Runtime.getRuntime().maxMemory(); Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
- 操作方式
我們可以定期(前臺每隔3分鐘)去得到這個值,當我們這個值達到危險值時(例如80%),我們應(yīng)當主要去釋放我們的各種cache資源(bitmap的cache為大頭),同時顯示的去Trim應(yīng)用的memory,加速內(nèi)存收集。
三. 使用多進程
對于webview,圖庫等,由于存在內(nèi)存系統(tǒng)泄露或者占用內(nèi)存過多的問題,我們可以采用單獨的進程。微信當前也會把它們放在單獨的tools進程中
四. 上報OOM詳細信息
當系統(tǒng)發(fā)生OOM的crash時,我們應(yīng)當上傳更加詳細的內(nèi)存相關(guān)信息,方便我們定位當時內(nèi)存的具體情況。
其他例如使用large heap、inBitmap、SparseArray、Protobuf等不再一一細述,對代碼采用優(yōu)化--埋坑--優(yōu)化--埋坑的方式并不推薦。我們應(yīng)該著力于建立一套合理的框架與監(jiān)控體系,能及時的發(fā)現(xiàn)諸如bitmap過大、像素浪費、內(nèi)存占用過大、應(yīng)用OOM等問題。
GC優(yōu)化
Java擁有GC的機制,不同的系統(tǒng)版本GC的實現(xiàn)可能有比較大的差異。但是無論哪種版本,大量的GC操作則會顯著占用幀間隔時間(16ms)。如果在幀間隔時間里面做了過多的GC操作,那么自然其他類似計算,渲染等操作的可用時間就變得少了。
一. GC的類型
GC的類型有以下幾種,其中GC_FOR_ALLOC是同步方式進行,對應(yīng)用幀率的影響最大。
- GC_FOR_ALLOC
當堆內(nèi)存不夠的時候容易被觸發(fā),尤其是new一個對象的時候,很容易被觸發(fā)到,所以如果要加速啟動,可以提高dalvik.vm.heapstartsize的值,這樣在啟動過程中可以減少GC_FOR_ALLOC的次數(shù)。注意這個觸發(fā)是以同步的方式進行的。如果GC后仍然沒有空間,則堆進行擴張
- GC_EXPLICIT
這個gc是被可以調(diào)用的,比如system.gc, 一般gc線程的優(yōu)先級比較低,所以這個垃圾回收的過程不一定會馬上觸發(fā), 千萬不要認為調(diào)用了system.gc,內(nèi)存的情況就能有所好轉(zhuǎn)
- GC_CONCURRENT
當分配的對象大小超過384K時觸發(fā),注意這是以異步的方式進行回收的.如果發(fā)現(xiàn)大量反復(fù)的Concurrent GC出現(xiàn),說明系統(tǒng)中可能一直有大于384K的對象被分配,而這些往往是一些臨時對象,被反復(fù)觸發(fā)了。給到我們的暗示是:對象的復(fù)用不夠。
- GC_EXTERNAL_ALLOC (在3.0系統(tǒng)之后被廢了)
Native層的內(nèi)存分配失敗了,這類GC就會被觸發(fā)。如果GPU的紋理、bitmap、或者java.nio.ByteBuffers的使用沒有釋放,這種類型的GC往往會被頻繁觸發(fā)。
二. 內(nèi)存抖動現(xiàn)象
Memory Churn內(nèi)存抖動,內(nèi)存抖動是因為在短時間內(nèi)大量的對象被創(chuàng)建又馬上被釋放。瞬間產(chǎn)生大量的對象會嚴重占用內(nèi)存區(qū)域,當達到閥值,剩余空間不夠的時候,會觸發(fā)GC從而導(dǎo)致剛產(chǎn)生的對象又很快被回收。即使每次分配的對象占用了很少的內(nèi)存,但是他們疊加在一起會增加Heap的壓力,從而觸發(fā)更多其他類型的GC。這個操作有可能會影響到幀率,并使得用戶感知到性能問題。
通過Memory Monitor,我們可以跟蹤整個app的內(nèi)存變化情況。若短時間發(fā)生了多次內(nèi)存的漲跌,這意味著很有可能發(fā)生了內(nèi)存抖動。
三. GC優(yōu)化
通過Heap Viewer,我們可以查看當前內(nèi)存快照,便于對比分析哪些對象有可能發(fā)生了泄漏。更重要的工具是Allocation Tracker,追蹤內(nèi)存對象的類型、堆棧、大小等。手Q有做一個統(tǒng)計工具,對Allocation Tracker的原始數(shù)據(jù),按照(類型&堆棧)的組合(堆棧取棧頂?shù)?層)統(tǒng)計某一種對象分配的大小、次數(shù)。同時按照次數(shù)、大小的排序,從多/大到少/小結(jié)合代碼分析,并自頂向下的逐輪進行優(yōu)化。
這樣,我們就可以快速知道發(fā)生內(nèi)存抖動時,是因為哪些變量的創(chuàng)建造成頻繁GC。一般來說我們需要注意以下幾個方面:
字符串拼接優(yōu)化
減少字符串使用加號拼接,改為使用StringBuilder。減少StringBuilder.enlarge,初始化時設(shè)置capacity;這里需要注意的是,若打開Looper中Printer回調(diào),也會存在較多的字符串拼接。
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
- 讀文件優(yōu)化 讀文件使用ByteArrayPool,初始設(shè)置capacity,減少expand
- 資源重用
建立全球緩存池,對頻繁申請、釋放的對象類型重用
- 減少不必要或不合理的對象
例如在ondraw、getview中應(yīng)減少對象申請,盡量重用。更多是一些邏輯上的東西,例如循環(huán)中不斷申請局部變量等
- 選用合理的數(shù)據(jù)格式 使用SparseArray, SparseBooleanArray, and LongSparseArray來代替Hashmap
總結(jié)
我們并不能將內(nèi)存優(yōu)化中用到的所有技巧都一一說明,而且隨著Android版本的更替,可能很多方法都會變的過時。我在想更重要的是我們能持續(xù)的發(fā)現(xiàn)問題,精細化的監(jiān)控,而不是一直處于"哪個有坑填哪里的"的窘?jīng)r。在這里給大家的建議有:
1、率先考慮采用已有的工具;中國人喜歡重復(fù)造輪子,我們更推薦花精力去優(yōu)化已有工具,為廣大碼農(nóng)做貢獻。生活已不易,碼農(nóng)何為為難碼農(nóng)!
2、不拘泥于點,更重要在于如何建立合理的框架避免發(fā)生問題,或者是能及時的發(fā)現(xiàn)問題。
當前微信內(nèi)存監(jiān)控體系中也存在一些不盡人意的地方,在未來的日子里也同樣需要努力去優(yōu)化。
以上就是本文的全部內(nèi)容,希望對大家優(yōu)化Android內(nèi)存有所幫助。
相關(guān)文章
Android編程自定義AlertDialog樣式的方法詳解
這篇文章主要介紹了Android編程自定義AlertDialog樣式的方法,結(jié)合實例形式詳細分析了Android自定義AlertDialog樣式的具體布局與功能實現(xiàn)相關(guān)操作技巧,需要的朋友可以參考下2018-02-02
學習使用Material Design控件(二)使用DrawerLayout實現(xiàn)側(cè)滑菜單欄效果
這篇文章主要為大家介紹了學習使用Material Design控件的詳細教程,使用DrawerLayout和NavigationView實現(xiàn)側(cè)滑菜單欄效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
Android使用Rotate3dAnimation實現(xiàn)3D旋轉(zhuǎn)動畫效果的實例代碼
利用Android的ApiDemos的Rotate3dAnimation實現(xiàn)了個圖片3D旋轉(zhuǎn)的動畫,圍繞Y軸進行旋轉(zhuǎn),還可以實現(xiàn)Z軸的縮放。點擊開始按鈕開始旋轉(zhuǎn),點擊結(jié)束按鈕停止旋轉(zhuǎn)。2018-05-05
Android應(yīng)用動態(tài)修改主題的方法示例
今天小編就為大家分享一篇關(guān)于Android應(yīng)用動態(tài)修改主題的方法示例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-03-03
月下載量上千次Android實現(xiàn)二維碼生成器app源碼分享
既然是二維碼生成器那么我們?nèi)绾沃谱鞫S碼呢?這篇文章為大家分享了月下載量上千次Android實現(xiàn)二維碼生成器app源碼,希望大家喜歡2015-12-12
Mono for Android 實現(xiàn)高效的導(dǎo)航(Effective Navigation)
Android 4.0 系統(tǒng)定義了一系列的高效導(dǎo)航方式 (Effective Navigation), 主要包括標簽、下拉列表、以及向上和返回等, 本文介紹如何用 Mono for Android 實現(xiàn)這些的導(dǎo)航方式2012-12-12

