從Android資源角度談Android程式碼記憶體優化
這篇文章主要介紹在實際Android應用程式的開發中,容易導致記憶體洩露的一些情況。開發人員如果在進行程式碼編寫之前就有記憶體洩露方面的基礎知 識,那麼寫出來的程式碼會強壯許多,寫這篇文章也是這個初衷。本文從Android開發中的資源使用情況入手,介紹瞭如何在Bitmap、資料庫查詢、9- patch、過渡繪製等方面優化記憶體的使用。
Android資源優化
1. Bitmap優化
Android中的大部分記憶體問題歸根結底都是Bitmap的問題,如果開啟MAT(Memory analyzer tool)來看,實際佔用記憶體大的都是一些Bitmap(以byte陣列的形式儲存)。所以Bitmap的優化應該是我們著重去解決的。Google在其 官方有針對Bitmap的使用專門寫了一個專題 : Displaying Bitmaps Efficiently , 對應的中文翻譯在 : displaying-bitmaps , 在優化Bitmap資源之前,請先看看這個系列的文件,以確保自己正確地使用了Bitmap。
Bitmap如果沒有被釋放,那麼一般只有兩個問題:
- 使用者在使用完這個Bitmap之後,沒有主動去釋放Bitmap資源。
- 這個Bitmap資源被引用所以無法被釋放 。
1.1 主動釋放Bitmap資源
當你確定這個Bitmap資源不會再被使用的時候(當然這個Bitmap不釋放可能會讓程式下一次啟動或者resume快一些,但是其佔用的記憶體 資源太大,可能導致程式在後臺的時候被殺掉,反而得不償失),我們建議手動呼叫recycle()方法,釋放其Native記憶體:
if(bitmap != null && !bitmap.isRecycled()){ bitmap.recycle(); bitmap = null; }
我們也可以看一下Bitmap.java中recycle()方法的說明:
/** * Free the native object associated with this bitmap, and clear the * reference to the pixel data. This will not free the pixel data synchronously; * it simply allows it to be garbage collected if there are no other references. * The bitmap is marked as "dead", meaning it will throw an exception if * getPixels() or setPixels() is called, and will draw nothing. This operation * cannot be reversed, so it should only be called if you are sure there are no * further uses for the bitmap. This is an advanced call, and normally need * not be called, since the normal GC process will free up this memory when * there are no more references to this bitmap. */ public void recycle() { if (!mRecycled) { if (nativeRecycle(mNativeBitmap)) { // return value indicates whether native pixel object was actually recycled. // false indicates that it is still in use at the native level and these // objects should not be collected now. They will be collected later when the // Bitmap itself is collected. mBuffer = null; mNinePatchChunk = null; } mRecycled = true; } } ...... //如果使用過程中丟擲異常的判斷 if (bitmap.isRecycled()) { throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap); }
呼叫bitmap.recycle之後,這個Bitmap如果沒有被引用到,那麼就會被垃圾回收器回收。如果不主動呼叫這個方法,垃圾回收器也會進 行回收工作,只不過垃圾回收器的不確定性太大,依賴其自動回收不靠譜(比如垃圾回收器一次性要回收好多Bitmap,那麼需要的時間就會很多,導致回收的 時候會卡頓)。所以我們需要主動呼叫recycle。
1.2 主動釋放ImageView的圖片資源
由於我們在實際開發中,很多情況是在xml佈局檔案中設定ImageView的src或者在程式碼中呼叫 ImageView.setImageResource/setImageURI/setImageDrawable等方法設定影像,下面程式碼可以回收這 個ImageView所對應的資源:
private static void recycleImageViewBitMap(ImageView imageView) { if (imageView != null) { BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable(); rceycleBitmapDrawable(bd); } } private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) { if (bitmapDrawable != null) { Bitmap bitmap = bitmapDrawable.getBitmap(); rceycleBitmap(bitmap); } bitmapDrawable = null; } private static void rceycleBitmap(Bitmap bitmap) { if (bitmap != null && !bitmap.isRecycled()) { bitmap.recycle(); bitmap = null; } }
1.3 主動釋放ImageView的背景資源
如果你的ImageView是有Background,那麼下面的程式碼可以釋放他:
public static void recycleBackgroundBitMap(ImageView view) { if (view != null) { BitmapDrawable bd = (BitmapDrawable) view.getBackground(); rceycleBitmapDrawable(bd); } } public static void recycleImageViewBitMap(ImageView imageView) { if (imageView != null) { BitmapDrawable bd = (BitmapDrawable) imageView.getDrawable(); rceycleBitmapDrawable(bd); } } private static void rceycleBitmapDrawable(BitmapDrawable bitmapDrawable) { if (bitmapDrawable != null) { Bitmap bitmap = bitmapDrawable.getBitmap(); rceycleBitmap(bitmap); } bitmapDrawable = null; }
1.4 儘量少用Png圖,多用NinePatch的圖
現在手機的解析度越來越高,圖片資源在被載入後所佔用的記憶體也越來越大,所以要儘量避免使用大的PNG圖,在產品設計的時候就要儘量避免用一張大圖來進行展示,儘量多用NinePatch資源。
Android中的NinePatch指的是一種拉伸後不會變形的特殊png圖,NinePatch的拉伸區域可以自己定義。這種圖的優點是體積 小,拉伸不變形,可以適配多機型。Android SDK中有自帶NinePatch資源製作工具,Android-Studio中在普通png圖片點選右鍵可以將其轉換為NinePatch資源,使用起 來非常方便。
1.5 使用大圖之前,儘量先對其進行壓縮
圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現出來的要大很多。例如,系統的Gallery程式會顯示那些你使用裝置camera拍攝的圖片,但是那些圖片的解析度通常都比你的裝置螢幕解析度要高很多。
考慮到程式是在有限的記憶體下工作,理想情況是你只需要在記憶體中載入一個低解析度的版本即可。這個低解析度的版本應該是與你的UI大小所匹配的,這 樣才便於顯示。一個高解析度的圖片不會提供任何可見的好處,卻會佔用寶貴的(precious)的記憶體資源,並且會在快速滑動圖片時導致(incurs) 附加的效率問題。
Google官網的Training中,有一篇文章專門介紹如何有效地載入大圖,裡面提到了兩個比較重要的技術:
- 在圖片載入前獲取其寬高和型別
- 載入一個按比例縮小的版本到記憶體中
原文地址: Loading Large Bitmaps Efficiently ,中文翻譯地址: 有效地載入大尺寸點陣圖 ,強烈建議每一位Android開發者都去看一下,並在自己的實際專案中使用到。
更多關於Bitmap的使用和優化,可以參考Android官方Training專題的 displaying-bitmaps
2 查詢資料庫沒有關閉遊標
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。示例程式碼:
Cursor cursor = getContentResolver().query(uri ...); if (cursor.moveToNext()) { ... ... }
修正示例程式碼:
Cursor cursor = null; try { cursor = getContentResolver().query(uri ...); if (cursor != null && cursor.moveToNext()) { ... ... } } finally { if (cursor != null) { try { cursor.close(); } catch (Exception e) { //ignore this } } }
3 構造Adapter時,沒有使用快取的convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提供了方法:
public View getView(int position, View convertView, ViewGroup parent)
來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的螢幕佈局例項化一 定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。由此可以看出,如果我們不去使用 convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費資源也浪費時間,也會使得記憶體佔用越來越大。 ListView回收list item的view物件的過程可以檢視:android.widget.AbsListView.java —> void addScrapView(View scrap) 方法。
示例程式碼:
public View getView(int position, View convertView, ViewGroup parent) { View view = new Xxx(...); ... ... return view; }
`示例修正程式碼:
public View getView(int position, View convertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; populate(view, getItem(position)); ... } else { view = new Xxx(...); ... } return view; }
關於ListView的使用和優化,可以參考這兩篇文章:
4 釋放物件的引用
前面有說過,一個物件的記憶體沒有被釋放是因為他被其他的物件所引用,系統不回去釋放這些有GC Root的物件。
示例A:假設有如下操作
public class DemoActivity extends Activity { ... ... private Handler mHandler = ... private Object obj; public void operation() { obj = initObj(); ... [Mark] mHandler.post(new Runnable() { public void run() { useObj(obj); } }); } }
我們有一個成員變數 obj,在operation()中我們希望能夠將處理obj例項的操作post到某個執行緒的MessageQueue中。在以上的程式碼中,即便是 mHandler所在的執行緒使用完了obj所引用的物件,但這個物件仍然不會被垃圾回收掉,因為DemoActivity.obj還保有這個物件的引用。 所以如果在DemoActivity中不再使用這個物件了,可以在[Mark]的位置釋放物件的引用,而程式碼可以修改為:
public void operation() { obj = initObj(); ... final Object o = obj; obj = null; mHandler.post(new Runnable() { public void run() { useObj(o); } } }
示例B:假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen 中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要 顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。
但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen 無法被垃圾回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得 system_ui程式掛掉。
總之當一個生命週期較短的物件A,被一個生命週期較長的物件B保有其引用的情況下,在A的生命週期結束時,要在B中清除掉對A的引用。
使用MAT可以很方便地檢視物件之間的引用,
5 在Activity的生命週期中釋放資源
Android應用程式中最典型的需要注意釋放資源的情況是在Activity的生命週期中,在onPause()、onStop()、 onDestroy()方法中需要適當的釋放資源的情況。由於此情況很基礎,在此不詳細說明,具體可以檢視官方文件對Activity生命週期的介紹,以 明確何時應該釋放哪些資源。
6 消除過渡繪製
過渡繪製指的是在螢幕一個畫素上繪製多次(超過一次),比如一個TextView後有背景,那麼顯示文字的畫素至少繪了兩次,一次是背景,一次是 文字。GPU過度繪製或多或少對效能有些影響,裝置的記憶體頻寬是有限的,當過度繪製導致應用需要更多的頻寬(超過了可用頻寬)的時候效能就會降低。頻寬的 限制每個裝置都可能是不一樣的。
過渡繪製的原因:
- 同一層級的View疊加
- 複雜的層級疊加
減少過渡繪製能去掉一些無用的View,能有效減少GPU的負載,也可以減輕一部分記憶體壓力。關於過渡繪製我專門寫了一篇文章來介紹:過渡繪製及其優化
7 使用Android系統自帶的資源
在Android應用開發過程中,螢幕上控制元件的佈局程式碼和程式的邏輯程式碼通常是分開的。介面的佈局程式碼是放在一個獨立的xml檔案中的,這個檔案 裡面是樹型組織的,控制著頁面的佈局。通常,在這個頁面中會用到很多控制元件,控制元件會用到很多的資源。Android系統本身有很多的資源,包括各種各樣的字 符串、圖片、動畫、樣式和佈局等等,這些都可以在應用程式中直接使用。這樣做的好處很多,既可以減少記憶體的使用,又可以減少部分工作量,也可以縮減程式安 裝包的大小。
比如下面的程式碼就是使用系統的ListView:
<ListView android:id="@android:id/list" android:layout_width="fill_parent" android:layout_height="fill_parent"/>
8 使用記憶體相關工具檢測
在開發中,不可能保證一次就開發出一個記憶體管理非常棒的應用,所以在開發的每一個階段,都要有意識地去針對記憶體進行專門的檢查。目前Android提供了許多佈局、記憶體相關的工具,比如Lint、MAT等。學會這些工具的使用是一個Android開發者必不可少的技能。
相關文章
- 從OnTrimMemory角度談Android程式碼記憶體優化Android記憶體優化
- 淺談Android記憶體優化Android記憶體優化
- Android記憶體優化雜談Android記憶體優化
- Android效能優化篇:從程式碼角度進行優化Android優化
- Android開發優化之——從程式碼角度進行優化Android優化
- Android記憶體優化Android記憶體優化
- Android 記憶體優化Android記憶體優化
- Android效能優化 - 記憶體優化Android優化記憶體
- Android Note - 記憶體優化Android記憶體優化
- android 記憶體優化篇Android記憶體優化
- Android 效能優化之記憶體優化Android優化記憶體
- Android記憶體優化之記憶體快取Android記憶體優化快取
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- Android記憶體優化全解析Android記憶體優化
- android,記憶體優化詳解Android記憶體優化
- Android記憶體優化之圖片優化Android記憶體優化
- Android記憶體優化(一):Java記憶體區域Android記憶體優化Java
- 淺談Android開發中記憶體洩露與優化Android記憶體洩露優化
- Android APP 記憶體優化之圖片優化AndroidAPP記憶體優化
- Android應用記憶體優化方式Android記憶體優化
- Android效能優化之記憶體篇Android優化記憶體
- Android 是如何管理 App 記憶體的 — Android 記憶體優化第二彈AndroidAPP記憶體優化
- Android 是如何管理 App 記憶體的 -- Android 記憶體優化第二彈AndroidAPP記憶體優化
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)Android優化記憶體
- Android 效能優化(四)之記憶體優化實戰Android優化記憶體
- Android記憶體優化(三)避免可控的記憶體洩漏Android記憶體優化
- Android記憶體優化(五)詳解記憶體分析工具MATAndroid記憶體優化
- Android記憶體優化——記憶體洩露檢測分析方法Android優化記憶體洩露
- android效能評測與優化-記憶體Android優化記憶體
- android記憶體管理機制與優化Android記憶體優化
- Android效能優化(三)之記憶體管理Android優化記憶體
- Android效能優化之記憶體洩漏Android優化記憶體
- Android應用優化之記憶體概念Android優化記憶體
- Android 記憶體洩露優化處理Android記憶體洩露優化
- ANDROID記憶體優化(大彙總——上)Android記憶體優化
- ANDROID記憶體優化(大彙總——中)Android記憶體優化