編輯:關於Android編程
這篇文章主要介紹在實際Android應用程序的開發中,容易導致內存洩露的一些情況。開發人員如果在進行代碼編寫之前就有內存洩露方面的基礎知 識,那麼寫出來的代碼會強壯許多,寫這篇文章也是這個初衷。本文從Android開發中的資源使用情況入手,介紹了如何在Bitmap、數據庫查詢、9- patch、過渡繪制等方面優化內存的使用。
Android中的大部分內存問題歸根結底都是Bitmap的問題,如果打開MAT(Memory analyzer tool)來看,實際占用內存大的都是一些Bitmap(以byte數組的形式存儲)。所以Bitmap的優化應該是我們著重去解決的。Google在其 官方有針對Bitmap的使用專門寫了一個專題 : Displaying Bitmaps Efficiently , 對應的中文翻譯在 : displaying-bitmaps , 在優化Bitmap資源之前,請先看看這個系列的文檔,以確保自己正確地使用了Bitmap。
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。
由於我們在實際開發中,很多情況是在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; } }
如果你的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; }
現在手機的分辨率越來越高,圖片資源在被加載後所占用的內存也越來越大,所以要盡量避免使用大的PNG圖,在產品設計的時候就要盡量避免用一張大圖來進行展示,盡量多用NinePatch資源。
Android中的NinePatch指的是一種拉伸後不會變形的特殊png圖,NinePatch的拉伸區域可以自己定義。這種圖的優點是體積 小,拉伸不變形,可以適配多機型。Android SDK中有自帶NinePatch資源制作工具,Android-Studio中在普通png圖片點擊右鍵可以將其轉換為NinePatch資源,使用起 來非常方便。
圖片有不同的形狀與大小。在大多數情況下它們的實際大小都比需要呈現出來的要大很多。例如,系統的Gallery程序會顯示那些你使用設備camera拍攝的圖片,但是那些圖片的分辨率通常都比你的設備屏幕分辨率要高很多。
考慮到程序是在有限的內存下工作,理想情況是你只需要在內存中加載一個低分辨率的版本即可。這個低分辨率的版本應該是與你的UI大小所匹配的,這 樣才便於顯示。一個高分辨率的圖片不會提供任何可見的好處,卻會占用寶貴的(precious)的內存資源,並且會在快速滑動圖片時導致(incurs) 附加的效率問題。
Google官網的Training中,有一篇文章專門介紹如何有效地加載大圖,裡面提到了兩個比較重要的技術:
原文地址: Loading Large Bitmaps Efficiently ,中文翻譯地址: 有效地加載大尺寸位圖 ,強烈建議每一位Android開發者都去看一下,並在自己的實際項目中使用到。
更多關於Bitmap的使用和優化,可以參考Android官方Training專題的 displaying-bitmaps
程序中經常會進行查詢數據庫的操作,但是經常會有使用完畢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 } } }
以構造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的使用和優化,可以參考這兩篇文章:
前面有說過,一個對象的內存沒有被釋放是因為他被其他的對象所引用,系統不回去釋放這些有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可以很方便地查看對象之間的引用,
Android應用程序中最典型的需要注意釋放資源的情況是在Activity的生命周期中,在onPause()、onStop()、 onDestroy()方法中需要適當的釋放資源的情況。由於此情況很基礎,在此不詳細說明,具體可以查看官方文檔對Activity生命周期的介紹,以 明確何時應該釋放哪些資源。
過渡繪制指的是在屏幕一個像素上繪制多次(超過一次),比如一個TextView後有背景,那麼顯示文本的像素至少繪了兩次,一次是背景,一次是 文本。GPU過度繪制或多或少對性能有些影響,設備的內存帶寬是有限的,當過度繪制導致應用需要更多的帶寬(超過了可用帶寬)的時候性能就會降低。帶寬的 限制每個設備都可能是不一樣的。
過渡繪制的原因:
減少過渡繪制能去掉一些無用的View,能有效減少GPU的負載,也可以減輕一部分內存壓力。關於過渡繪制我專門寫了一篇文章來介紹:過渡繪制及其優化
在Android應用開發過程中,屏幕上控件的布局代碼和程序的邏輯代碼通常是分開的。界面的布局代碼是放在一個獨立的xml文件中的,這個文件 裡面是樹型組織的,控制著頁面的布局。通常,在這個頁面中會用到很多控件,控件會用到很多的資源。Android系統本身有很多的資源,包括各種各樣的字 符串、圖片、動畫、樣式和布局等等,這些都可以在應用程序中直接使用。這樣做的好處很多,既可以減少內存的使用,又可以減少部分工作量,也可以縮減程序安 裝包的大小。
比如下面的代碼就是使用系統的ListView:
在開發中,不可能保證一次就開發出一個內存管理非常棒的應用,所以在開發的每一個階段,都要有意識地去針對內存進行專門的檢查。目前Android提供了許多布局、內存相關的工具,比如Lint、MAT等。學會這些工具的使用是一個Android開發者必不可少的技能。
android 中的 日歷控件 public class MainActivity extends Activ
今天要做一個任務,要求圖片做按鈕開關,點擊出發相應事件。點擊打開,圖片左邊顯示幾行字體,這幾行字體是延時顯示的。下面將主要代碼附上。以下是main.xml
SparseArray 目前有很多地方從性能優化方說使用SparseArray來替換hashMap,來節省內存,提高性能。Linkify.addLinks() 這個類可以
在Android中實現異步任務機制有兩種方式,Handler和AsyncTask。Handler模式需要為每一個任務創建一個新的線程,任務完成後通過Handler實例向U