android 開發如何做記憶體優化
不少人認為JAVA程式,因為有垃圾回收機制,應該沒有記憶體洩露。其實如果我們一個程式中,已經不再使用某個物件,但是因為仍然有引用指向它,垃圾回收器就無法回收它,當然該物件佔用的記憶體就無法被使用,這就造成了記憶體洩露。如果我們的java執行很久,而這種記憶體洩露不斷的發生,最後就沒記憶體可用了。當然java的,記憶體洩漏和C/C++是不一樣的。如果java程式完全結束後,它所有的物件就都不可達了,系統就可以對他們進行垃圾回收,它的記憶體洩露僅僅限於它本身,而不會影響整個系統的。C/C++的記憶體洩露就比較糟糕了,它的記憶體洩露是系統級,即使該C/C++程式退出,它的洩露的記憶體也無法被系統回收,永遠不可用了,除非重啟機器。
一、引用沒釋放造成的記憶體洩露
1.1註冊沒取消造成的記憶體洩露
這種Android的記憶體洩露比純java的記憶體洩露還要嚴重,因為其他一些Android程式可能引用我們的Anroid程式的物件(比如註冊機制)。即使我們的Android程式已經結束了,但是別的引用程式仍然還有對我們的Android程式的某個物件的引用,洩露的記憶體依然不能被垃圾回收。
比如示例1:
假設我們希望在鎖屏介面(LockScreen)中,監聽系統中的電話服務以獲取一些資訊(如訊號強度等),則可以在LockScreen中定義一個PhoneStateListener的物件,同時將它註冊到TelephonyManager服務中。對於LockScreen物件,當需要顯示鎖屏介面的時候就會建立一個LockScreen物件,而當鎖屏介面消失的時候LockScreen物件就會被釋放掉。
但是如果在釋放LockScreen物件的時候忘記取消我們之前註冊的PhoneStateListener物件,則會導致LockScreen無法被垃圾回收。如果不斷的使鎖屏介面顯示和消失,則最終會由於大量的LockScreen物件沒有辦法被回收而引起OutOfMemory,使得system_process程序掛掉。
雖然有些系統程式,它本身好像是可以自動取消註冊的(當然不及時),但是我們還是應該在我們的程式中明確的取消註冊,程式結束時應該把所有的註冊都取消掉。
1.2集合中物件沒清理造成的記憶體洩露
我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
二、資源物件沒關閉造成的記憶體洩露
資源性物件比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該呼叫它的close()函式,將其關閉掉,然後才置為null.在我們的程式退出時一定要確保我們的資源性物件已經關閉。
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在常時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。
三、一些不良程式碼成記憶體壓力
有些程式碼並不造成記憶體洩露,但是它們,或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的,容易迫使虛擬機器不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。
3.1,Bitmap沒呼叫recycle()
Bitmap物件在不使用時,我們應該先呼叫recycle()釋放記憶體,然後才它設定為null.雖然recycle()從原始碼上看,呼叫它應該能立即釋放Bitmap的主要記憶體,但是測試結果顯示它並沒能立即釋放記憶體。但是我它應該還是能大大的加速Bitmap的主要記憶體的釋放。
3.2,構造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物件的過程可以檢視:
view plaincopy to clipboardprint?
android.widget.AbsListView.java --> void addScrapView(View scrap) 方法。
示例程式碼:
1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 View view = new Xxx(...); 4 5 ... ... 6 7 return view; 8 9 }
修正示例程式碼:
Android記憶體管理
1 public View getView(int position, View convertView, ViewGroup parent) { 2 3 View view = null; 4 5 if (convertView != null) { 6 7 view = convertView; 8 9 populate(view, getItem(position)); 10 11 ... 12 13 } else { 14 15 view = new Xxx(...); 16 17 ... 18 19 } 20 21 return view; 22 23 }
概述:
在android的開發中,要時刻主要記憶體的分配和垃圾回收,因為系統為每一個dalvik虛擬機器分配的記憶體是有限的,在google的G1中,分配的最大堆大小隻有16M,後來的機器一般都為24M,實在是少的可憐。這樣就需要我們在開發過程中要時刻注意。不要因為自己的程式碼問題而造成OOM錯誤。
JAVA的記憶體管理:
大家都知道,android應用層是由java開發的,android的davlik虛擬機器與jvm也類似,只不過它是基於暫存器的。因此要了解android的記憶體管理就必須得了解java的記憶體分配和垃圾回收機制。
在java中,是通過new關鍵字來為物件分配記憶體的,而記憶體的釋放是由垃圾收集器(GC)來回收的,工程師在開發的過程中,不需要顯式的去管理記憶體。但是這樣有可能在不知不覺中就會浪費了很多記憶體,最終導致java虛擬機器花費很多時間去進行垃圾回收,更嚴重的是造成JVM的OOM。因此,java工程師還是有必要了解JAVA的記憶體分配和垃圾回收機制。
記憶體結構
file: ///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6926.png
上面這張圖是JVM的結構圖,它主要四個部分組成:Class Loader子系統和執行引擎,執行時方法區和本地方法區,我們主要來看下RUNTIME DATA AREA區,也就是我們常說的JVM記憶體。從圖中可以看出,RUNTIMEDATA AREA區主要由 5 個部分組成:
· Method Area:被裝載的 class 的元資訊儲存在Method Area中,它是執行緒共享的
· Heap(堆):一個java虛擬機器例項中只存在一個堆空間,存放一些物件資訊,它是執行緒共享的
· Java棧: java虛擬機器直接對java棧進行兩種操作,以幀為單位的壓棧和出棧(非執行緒共享)
· 程式計數器(非執行緒共享)
· 本地方法棧(非執行緒共享)
JVM的垃圾回收(GC)
file: ///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-12485.png
JVM的垃圾原理是這樣的,它把物件分為年輕代(Young)、年老代(Tenured)、持久代(Perm),對不同生命週期的物件使用不同的垃圾回收演算法。
· 年輕代(Young)
年輕代分為三個區,一個eden區,兩個Survivor區。程式中生成的大部分新的物件都在Eden區中,當Eden區滿時,還存活的物件將被複制到其中一個Survivor區,當此Survivor區的物件佔用空間滿了時,此區存活的物件又被複制到另外一個Survivor區,當這個Survivor區也滿了的時候,從第一個Survivor區複製過來的並且此時還存活的物件,將被複制到年老代。
· 年老代(Tenured)
年老代存放的是上面年輕代複製過來的物件,也就是在年輕代中還存活的物件,並且區滿了複製過來的。一般來說,年老代中的物件生命週期都比較長。
· 持久代(Perm)
用於存放靜態的類和方法,持久代對垃圾回收沒有顯著的影響。
Android中記憶體洩露監測
在瞭解了JVM的記憶體管理後,我們再回過頭來看看,在android中應該怎樣來監測記憶體,從而看在應用中是否存在記憶體分配和垃圾回收問題而造成記憶體洩露情況。
在android中,有一個相對來說還不錯的工具,可以用來監測記憶體是否存在洩露情況:DDMS—Heap
file: ///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-22715.png
使用方法比較簡單:
· 選擇DDMS檢視,並開啟Devices檢視和Heap檢視
· 點選選擇要監控的程序,比如:上圖中我選擇的是system_process
· 選中Devices檢視介面上的 "update heap"
圖示
· 點選Heap檢視中的 "Cause GC"
按鈕(相當於向虛擬機發送了一次GC請求的操作)
在Heap檢視中選擇想要監控的Type,一般我們會觀察dataobject的 total size的變化,正常情況下total size的值會穩定在一個有限的範圍內,也就說程式中的程式碼良好,沒有造成程式中的物件不被回收的情況。如果程式碼中存在沒有釋放物件引用的情況,那麼data object的total size在每次GC之後都不會有明顯的回落,隨著操作次數的增加而total size也在不斷的增加。(說明:選擇好data
object後,不斷的操作應用,這樣才可以看出total size的變化)。如果totalsize確實是在不斷增加而沒有回落,說明程式中有沒有被釋放的資源引用。那麼我們應該怎麼來定位呢?
Android中記憶體洩露定位
Mat(memory analyzer tools)是我們常用的用來定位記憶體洩露的工具,如果你使用ADT,並且安裝了MAT的eclipse外掛,你需要做的是進入DDMS檢視的Devices檢視:
file: ///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-2165.png
點選 "dump HPROF file" 按鈕,然後使用MAT分析下載下來的檔案。
file: ///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6565.png
|
下面列出了存在的問題,點選detail進去,會列出詳細的,可能會存在問題的程式碼:
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-32625.png
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-21158.png
這位兄弟寫的比較詳細。
總結
不管是java還是android,都應該瞭解記憶體分配和垃圾回收機制,工程師要做到寫的程式碼中沒有bad code很難,關鍵是在出現問題的時候該怎麼去排查Android記憶體優化
一、 Android的記憶體機制
Android的程式由Java語言編寫,所以Android的記憶體管理與Java的記憶體管理相似。程式設計師通過new為物件分配記憶體,所有物件在java堆內分配空間;然而物件的釋放是由垃圾回收器來完成的。C/C++中的記憶體機制是“誰汙染,誰治理”,java的就比較人性化了,給我們請了一個專門的清潔工(GC)。
那麼GC怎麼能夠確認某一個物件是不是已經被廢棄了呢?Java採用了有向圖的原理。Java將引用關係考慮為圖的有向邊,有向邊從引用者指向引用物件。執行緒物件可以作為有向圖的起始頂點,該圖就是從起始頂點開始的一棵樹,根頂點可以到達的物件都是有效物件,GC不會回收這些物件。如果某個物件 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)物件不再被引用,可以被GC回收。
二、Android的記憶體溢位
Android的記憶體溢位是如何發生的?
Android的虛擬機器是基於暫存器的Dalvik,它的最大堆大小一般是16M,有的機器為24M。因此我們所能利用的記憶體空間是有限的。如果我們的記憶體佔用超過了一定的水平就會出現OutOfMemory的錯誤。
為什麼會出現記憶體不夠用的情況呢?我想原因主要有兩個:
由於我們程式的失誤,長期保持某些資源(如Context)的引用,造成記憶體洩露,資源造成得不到釋放。
儲存了多個耗用記憶體過大的物件(如Bitmap),造成記憶體超出限制。
三、萬惡的static
static是Java中的一個關鍵字,當用它來修飾成員變數時,那麼該變數就屬於該類,而不是該類的例項。所以用static修飾的變數,它的生命週期是很長的,如果用它來引用一些資源耗費過多的例項(Context的情況最多),這時就要謹慎對待了。
1 public class ClassName { 2 3 private static Context mContext; 4 5 //省略 6 7 }
以上的程式碼是很危險的,如果將Activity賦值到麼mContext的話。那麼即使該Activity已經onDestroy,但是由於仍有物件儲存它的引用,因此該Activity依然不會被釋放。
我們舉Android文件中的一個例子。
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
sBackground是一個靜態的變數,但是我們發現,我們並沒有顯式的儲存Contex的引用,但是,當Drawable與View連線之後,Drawable就將View設定為一個回撥,由於View中是包含Context的引用的,所以,實際上我們依然儲存了Context的引用。這個引用鏈如下:
Drawable->TextView->Context
所以,最終該Context也沒有得到釋放,發生了記憶體洩露。
如何才能有效的避免這種引用的發生呢?
應該儘量避免static成員變數引用資源耗費過多的例項,比如Context。
Context儘量使用Application Context,因為Application的Context的生命週期比較長,引用它不會出現記憶體洩露的問題。
使用WeakReference代替強引用。比如可以使用WeakReference<Context> mContextRef;
該部分的詳細內容也可以參考Android文件中Article部分。
四、都是執行緒惹的禍
執行緒也是造成記憶體洩露的一個重要的源頭。執行緒產生記憶體洩露的主要原因在於執行緒生命週期的不可控。我們來考慮下面一段程式碼。
1 public class MyActivity extends Activity { 2 3 @Override 4 5 public void onCreate(Bundle savedInstanceState) { 6 7 super.onCreate(savedInstanceState); 8 9 setContentView(R.layout.main); 10 11 new MyThread().start(); 12 13 } 14 15 16 private class MyThread extends Thread{ 17 18 @Override 19 20 public void run() { 21 22 super.run(); 23 24 //do somthing 25 26 } 27 28 } 29 30 }
這段程式碼很平常也很簡單,是我們經常使用的形式。我們思考一個問題:假設MyThread的run函式是一個很費時的操作,當我們開啟該執行緒後,將裝置的橫屏變為了豎屏,一般情況下當螢幕轉換時會重新建立Activity,按照我們的想法,老的Activity應該會被銷燬才對,然而事實上並非如此。
由於我們的執行緒是Activity的內部類,所以MyThread中儲存了Activity的一個引用,當MyThread的run函式沒有結束時,MyThread是不會被銷燬的,因此它所引用的老的Activity也不會被銷燬,因此就出現了記憶體洩露的問題。
file:///C:/DOCUME~1/ADMINI~1/LOCALS~1/Temp/ksohtml/wps_clip_image-6439.png
有些人喜歡用Android提供的AsyncTask,但事實上AsyncTask的問題更加嚴重,Thread只有在run函式不結束時才出現這種記憶體洩露問題,然而AsyncTask內部的實現機制是運用了ThreadPoolExcutor,該類產生的Thread物件的生命週期是不確定的,是應用程式無法控制的,因此如果AsyncTask作為Activity的內部類,就更容易出現記憶體洩露的問題。
這種執行緒導致的記憶體洩露問題應該如何解決呢?
將執行緒的內部類,改為靜態內部類。
線上程內部採用弱引用儲存Context引用。
解決的模型如下:
1 public abstract class WeakAsyncTask<Params, Progress, Result, WeakTarget> extends 2 AsyncTask<Params, Progress, Result> { 3 protected WeakReference<WeakTarget> mTarget; 4 5 public WeakAsyncTask(WeakTarget target) { 6 mTarget = new WeakReference<WeakTarget>(target); 7 } 8 9 /** {@inheritDoc} */ 10 @Override 11 protected final void onPreExecute() { 12 final WeakTarget target = mTarget.get(); 13 if (target != null) { 14 this.onPreExecute(target); 15 } 16 } 17 18 /** {@inheritDoc} */ 19 @Override 20 protected final Result doInBackground(Params... params) { 21 final WeakTarget target = mTarget.get(); 22 if (target != null) { 23 return this.doInBackground(target, params); 24 } else { 25 return null; 26 } 27 } 28 29 /** {@inheritDoc} */ 30 @Override 31 protected final void onPostExecute(Result result) { 32 final WeakTarget target = mTarget.get(); 33 if (target != null) { 34 this.onPostExecute(target, result); 35 } 36 } 37 38 protected void onPreExecute(WeakTarget target) { 39 // No default action 40 } 41 42 protected abstract Result doInBackground(WeakTarget target, Params... params); 43 44 protected void onPostExecute(WeakTarget target, Result result) { 45 // No default action 46 } 47 }
事實上,執行緒的問題並不僅僅在於記憶體洩露,還會帶來一些災難性的問題。由於本文討論的是記憶體問題,所以在此不做討論。
由於51cto不讓我一次傳完,說我的字數太多了,所以分開傳了。
五、超級大胖子Bitmap
可以說出現OutOfMemory問題的絕大多數人,都是因為Bitmap的問題。因為Bitmap佔用的記憶體實在是太多了,它是一個“超級大胖子”,特別是解析度大的圖片,如果要顯示多張那問題就更顯著了。
如何解決Bitmap帶給我們的記憶體問題?
及時的銷燬。
雖然,系統能夠確認Bitmap分配的記憶體最終會被銷燬,但是由於它佔用的記憶體過多,所以很可能會超過java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
設定一定的取樣率。
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的取樣率,那麼就可以大大減小佔用的記憶體。如下面的程式碼:
1 private ImageView preview; 2 3 4 BitmapFactory.Options options = new BitmapFactory.Options(); 5 6 7 options.inSampleSize = 2;//圖片寬高都為原來的二分之一,即圖片為原來的四分之一 8 9 Bitmap bitmap = BitmapFactory.decodeStream(cr.openInputStream(uri), null, options); 10 11 12 preview.setImageBitmap(bitmap);
巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法呼叫Recycle函式。這時候巧妙的運用軟引用,可以使Bitmap在記憶體快不足時得到有效的釋放。如下例:
/**本例子為博主隨手一寫,來說明用法,並未驗證*/
1 private class MyAdapter extends BaseAdapter { 2 3 private ArrayList<SoftReference<Bitmap>> mBitmapRefs = new ArrayList<SoftReference<Bitmap>>(); 4 private ArrayList<Value> mValues; 5 private Context mContext; 6 private LayoutInflater mInflater; 7 8 MyAdapter(Context context, ArrayList<Value> values) { 9 mContext = context; 10 mValues = values; 11 mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 12 } 13 public int getCount() { 14 return mValues.size(); 15 } 16 public Object getItem(int i) { 17 return mValues.get(i); 18 } 19 20 public long getItemId(int i) { 21 return i; 22 } 23 24 public View getView(int i, View view, ViewGroup viewGroup) { 25 View newView = null; 26 if(view != null) { 27 newView = view; 28 } else { 29 newView =(View)mInflater.inflate(R.layout.image_view, false); 30 } 31 32 Bitmap bitmap = BitmapFactory.decodeFile(mValues.get(i).fileName); 33 mBitmapRefs.add(new SoftReference<Bitmap>(bitmap)); //此處加入ArrayList 34 ((ImageView)newView).setImageBitmap(bitmap); 35 36 return newView; 37 } 38 }
六、行蹤詭異的Cursor
Cursor是Android查詢資料後得到的一個管理資料集合的類,正常情況下,如果查詢得到的資料量較小時不會有記憶體問題,而且虛擬機器能夠保證Cusor最終會被釋放掉。
然而如果Cursor的資料量特表大,特別是如果裡面有Blob資訊時,應該保證Cursor佔用的記憶體被及時的釋放掉,而不是等待GC來處理。並且Android明顯是傾向於程式設計者手動的將Cursor close掉,因為在原始碼中我們發現,如果等到垃圾回收器來回收時,會給使用者以錯誤提示。
所以我們使用Cursor的方式一般如下:
1 Cursor cursor = null; 2 try { 3 cursor = mContext.getContentResolver().query(uri,null, null,null,null); 4 if(cursor != null) { 5 cursor.moveToFirst(); 6 //do something 7 } 8 } catch (Exception e) { 9 e.printStackTrace(); 10 } finally { 11 if (cursor != null) { 12 cursor.close(); 13 } 14 }
有一種情況下,我們不能直接將Cursor關閉掉,這就是在CursorAdapter中應用的情況,但是注意,CursorAdapter在Acivity結束時並沒有自動的將Cursor關閉掉,因此,你需要在onDestroy函式中,手動關閉。
1 protected void onDestroy() { 2 3 if (mAdapter != null && mAdapter.getCurosr() != null) { 4 5 mAdapter.getCursor().close(); 6 7 } 8 9 super.onDestroy(); 10 11 }
CursorAdapter中的changeCursor函式,會將原來的Cursor釋放掉,並替換為新的Cursor,所以你不用擔心原來的Cursor沒有被關閉。
你可能會想到使用Activity的managedQuery來生成Cursor,這樣Cursor就會與Acitivity的生命週期一致了,多麼完美的解決方法!然而事實上managedQuery也有很大的侷限性。
managedQuery生成的Cursor必須確保不會被替換,因為可能很多程式事實上查詢條件都是不確定的,因此我們經常會用新查詢的Cursor來替換掉原先的Cursor。因此這種方法適用範圍也是很小。
七、其它要說的。
其實,要減小記憶體的使用,其實還有很多方法和要求。比如不要使用整張整張的圖,儘量使用9path圖片。Adapter要使用convertView等等,好多細節都可以節省記憶體。這些都需要我們去挖掘,誰叫Android的記憶體不給力來著。