標籤:目的 緩衝 call 設計 layout you sleep 修改 public
一、關於OOM與記憶體泄露的概念
我們在Android開發過程中經常會遇到OOM的錯誤,這是因為我們在APP中沒有考慮dalvik虛擬機器記憶體消耗的問題。
1、什麼是OOM
OOM:即OutOfMemoery,顧名思義就是指記憶體溢出了。記憶體溢出是指APP向系統申請超過最大閥值的記憶體請求,系統不會再分配多餘的空間,就會造成OOM error。在我們Android平台下,多數情況是出現在圖片不當處理載入的時候。
Android系統為每個應用程式分配的記憶體有限,當一個應用中產生的記憶體流失比較多時,就難免會導致應用所需要的記憶體超過這個系統分配的記憶體限額,這就造成了記憶體溢出而導致應用Crash。Android APP的所能申請的最大記憶體大小是多少,有人說是16MB,有人又說是24MB。其實這些答案都算對,因為Android是開源的作業系統,不同的手機廠商其實是擁有修改這部分許可權能力的,所以就造成了不同品牌和不同系統的手機,對於APP的記憶體支援也是不一樣的,不過我們可以通過Runtime這個類來擷取當前裝置的Android系統為每個應用所產生的記憶體大小。APP並不會為我們建立Runtime的執行個體,Java為我們提供了單例擷取的方式Runtime.getRuntime()。通過maxMemory()方法擷取系統可為APP分配的最大記憶體,totalMemory()擷取APP當前所分配的記憶體heap空間大小。
2、什麼是記憶體泄露
Java使用有向圖機制,通過GC自動檢查記憶體中的對象(什麼時候檢查由虛擬機器決定),如果GC發現一個或一組對象為不可到達狀態,則將該對象從記憶體中回收。也就是說,一個對象不被任何引用所指向,則該對象會在被GC發現的時候被回收;另外,如果一組對象中只包含互相的引用,而沒有來自它們外部的引用(例如有兩個對象A和B互相持有引用,但沒有任何外部對象持有指向A或B的引用),這仍然屬於不可到達,同樣會被GC回收。
在Android程式開發中,當一個對象已經不需要再使用了,本該被回收時,而另外一個正在使用的對象持有它的引用從而導致它不能被回收,這就導致本該被回收的對象不能被回收而停留在堆記憶體中,記憶體流失就產生了。
記憶體泄露的危害:只有一個,那就是虛擬機器佔用記憶體過高,導致OOM(記憶體溢出),程式出錯。對於Android應用來說,就是你的使用者開啟一個Activity,使用完之後關閉它,記憶體泄露;又開啟,又關閉,又泄露;幾次之後,程式佔用記憶體超過系統限制,FC。
瞭解了記憶體流失的原因及影響後,我們需要做的就是掌握常見的記憶體流失,並在以後的Android程式開發中,盡量避免它。
二、常見的記憶體流失及解決方案 1、單例造成的記憶體流失
Android的單例模式非常受開發人員的喜愛,不過使用的不恰當的話也會造成記憶體流失。
因為單例的靜態特性使得單例的生命週期和應用的生命週期一樣長,這就說明了如果一個對象已經不需要使用了,而單例對象還持有該對象的引用,那麼這個對象將不能被正常回收,這就導致了記憶體流失。
如下這個典例:
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context; } public static AppManager getInstance(Context context) { if (instance != null) { instance = new AppManager(context); } return instance; } }
這是一個普通的單例模式,當建立這個單例的時候,由於需要傳入一個Context,所以
這個Context的生命週期的長短至關重要:
1)、傳入的是Application的Context:這將沒有任何問題,因為單例的生命週期和Application的一樣長;
2)、傳入的是Activity的Context:當這個Context所對應的Activity退出時,由於該Context和Activity的生命週期一樣長(Activity間接繼承於Context),所以當前Activity退出時它的記憶體並不會被回收,因為單例對象持有該Activity的引用。
所以正確的單例應該修改為下面這種方式:
public class AppManager { private static AppManager instance; private Context context; private AppManager(Context context) { this.context = context.getApplicationContext(); } public static AppManager getInstance(Context context) { if (instance != null) { instance = new AppManager(context); } return instance; } }
這樣不管傳入什麼Context最終將使用Application的Context,而單例的生命週期和應用的一樣長,這樣就防止了記憶體流失。
2、非靜態內部類建立靜態執行個體造成的記憶體流失
在Java 中,非靜態匿名內部類會持有其外部類的隱式引用,如果你沒有考慮過這一點,那麼儲存該引用會導致Activity被保留,而不是被記憶體回收機制回收。Activity對象持有其View層以及相關聯的所有資源檔的引用,換句話說,如果你的記憶體流失發生在Activity中,那麼你將損失大量的記憶體空間。
有的時候我們可能會在啟動頻繁的Activity中,為了避免重複建立相同的資料資源,會出現這種寫法:
public class MainActivity extends AppCompatActivity { private static TestResource mResource = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(mManager == null){ mManager = new TestResource(); } //... } class TestResource { //... } }
這樣就在Activity內部建立了一個非靜態內部類的單例,每次啟動Activity時都會使用該單例的資料,這樣雖然避免了資源的重複建立,不過這種寫法卻會造成記憶體流失,因為非靜態內部類預設會持有外部類的引用,而又使用了該非靜態內部類建立了一個靜態執行個體,該執行個體的生命週期和應用的一樣長,這就導致了該靜態執行個體一直會持有該Activity的引用,導致Activity的記憶體資源不能正常回收。正確的做法為:
將該內部類設為靜態內部類或將該內部類抽取出來封裝成一個單例,如果需要使用Context,請使用ApplicationContext。
3、Handler造成的記憶體流失
Handler的使用造成的記憶體流失問題應該說最為常見了,平時在處理網路任務或者封裝一些請求回調等api都應該會藉助Handler來處理,對於Handler的使用代碼編寫一不規範即有可能造成記憶體流失,如下樣本:
Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { mImageView.setImageBitmap(mBitmap); } }
上面是一段簡單的Handler的使用。當使用內部類(包括匿名類)來建立Handler的時候,Handler對象會隱式地持有一個外部類對象(通常是一個Activity)的引用(不然你怎麼可能通過Handler來操作Activity中的View?)。而Handler通常會伴隨著一個耗時的後台線程(例如從網路拉取圖片)一起出現,這個後台線程在任務執行完畢(例片下載完畢)之後,通過訊息機制通知Handler,然後Handler把圖片更新到介面。然而,如果使用者在網路請求過程中關閉了Activity,正常情況下,Activity不再被使用,它就有可能在GC檢查時被回收掉,但由於這時線程尚未執行完,而該線程持有Handler的引用(不然它怎麼發訊息給Handler?),這個Handler又持有Activity的引用,就導致該Activity無法被回收(即記憶體泄露),直到網路請求結束(例片下載完畢)。另外,如果你執行了Handler的postDelayed()方法:
//要做的事情,這裡再次調用此Runnable對象,以實現每兩秒實現一次的定時器操作
handler.postDelayed(this, 2000);
該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,那麼在你設定的delay到達之前,會有一條MessageQueue -> Message -> Handler -> Activity的鏈,導致你的Activity被持有引用而無法被回收。
這種建立Handler的方式會造成記憶體流失,由於mHandler是Handler的非靜態匿名內部類的執行個體,所以它持有外部類Activity的引用,我們知道訊息佇列是在Looper中不斷輪詢處理訊息,那麼當這個Activity退出時訊息佇列中還有未處理的訊息或者正在處理訊息,而訊息佇列中的Message持有mHandler執行個體的引用,mHandler又持有Activity的引用,所以導致該Activity的記憶體資源無法及時回收,引發記憶體流失。
使用Handler導致記憶體泄露的解決方案
方法一:通過程式邏輯來進行保護。
1).在關閉Activity的時候停掉你的後台線程。線程停掉了,就相當於切斷了Handler和外部串連的線,Activity自然會在合適的時候被回收。
2).如果你的Handler是被delay的Message持有了引用,那麼使用相應的Handler的removeCallbacks()方法,把訊息對象從訊息佇列移除就行了。
方法二:將Handler聲明為靜態類。
靜態類不持有外部類的對象,所以你的Activity可以隨意被回收。代碼如下:
static class MyHandler extends Handler { @Override public void handleMessage(Message msg) { mImageView.setImageBitmap(mBitmap); } }
但其實沒這麼簡單。使用了以上代碼之後,你會發現,由於Handler不再持有外部類對象的引用,導致程式不允許你在Handler中操作Activity中的對象了。所以你需要在Handler中增加一個對Activity的弱引用(WeakReference):
static class MyHandler extends Handler { WeakReference<Activity > mActivityReference; MyHandler(Activity activity) { mActivityReference= new WeakReference<Activity>(activity); } @Override public void handleMessage(Message msg) { final Activity activity = mActivityReference.get(); if (activity != null) { mImageView.setImageBitmap(mBitmap); } } }
將代碼改為以上形式之後,就算完成了。
延伸:什麼是WeakReference?
WeakReference弱引用,與強引用(即我們常說的引用)相對,它的特點是,GC在回收時會忽略掉弱引用,即就算有弱引用指向某對象,但只要該對象沒有被強引用指向(實際上多數時候還要求沒有軟引用,但此處軟引用的概念可以忽略),該對象就會在被GC檢查到時回收掉。對於上面的代碼,使用者在關閉Activity之後,就算後台線程還沒結束,但由於僅有一條來自Handler的弱引用指向Activity,所以GC仍然會在檢查的時候把Activity回收掉。這樣,記憶體泄露的問題就不會出現了。
4、線程造成的記憶體流失
對於線程造成的記憶體流失,也是平時比較常見的,如下這兩個樣本可能每個人都這樣寫過:
//——————test1 new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { SystemClock.sleep(10000); return null; } }.execute(); //——————test2 new Thread(new Runnable() { @Override public void run() { SystemClock.sleep(10000); } }).start();
上面的非同步任務和Runnable都是一個匿名內部類,因此它們對當前Activity都有一個隱式引用。如果Activity在銷毀之前,任務還未完成, 那麼將導致Activity的記憶體資源無法回收,造成記憶體流失。正確的做法還是使用靜態內部類的方式,如下:
static class MyAsyncTask extends AsyncTask<Void, Void, Void> { private WeakReference<Context> weakReference; public MyAsyncTask(Context context) { weakReference = new WeakReference<>(context); } @Override protected Void doInBackground(Void... params) { SystemClock.sleep(10000); return null; } @Override protected void onPostExecute(Void aVoid) { super.onPostExecute(aVoid); MainActivity activity = (MainActivity) weakReference.get(); if (activity != null) { //... } } } static class MyRunnable implements Runnable{ @Override public void run() { SystemClock.sleep(10000); } } //—————— new Thread(new MyRunnable()).start(); new MyAsyncTask(this).execute();
通過上面的代碼,新線程再也不會持有一個外部Activity 的隱式引用,而且該Activity也會在配置改變後被回收。這樣就避免了Activity的記憶體資源泄漏,當然在Activity銷毀時候也應該取消相應的任務AsyncTask::cancel(),避免任務在後台執行浪費資源。
如果我們線程做的是一個無線迴圈更新UI的操作,如下代碼:
private static class MyThread extends Thread { @Override public void run() { while (true) { SystemClock.sleep(1000); } } }
這樣雖然避免了Activity無法銷毀導致的記憶體泄露,但是這個線程卻發生了記憶體泄露。在Java中線程是記憶體回收機制的根源,也就是說,在運行系統中DVM虛擬機器總會使硬體持有所有運行狀態的進程的引用,結果導致處於運行狀態的線程將永遠不會被回收。因此,你必須為你的後台線程實現銷毀邏輯!下面是一種解決辦法:
private static class MyThread extends Thread { private boolean mRunning = false; @Override public void run() { mRunning = true; while (mRunning) { SystemClock.sleep(1000); } } public void close() { mRunning = false; } }
我們在Activity退出時,可以在 onDestroy()方法中顯示調用mThread.close();以此來結束該線程,這就避免了線程的記憶體流失問題。
5、資來源物件沒關閉造成的記憶體流失
資源性對象比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於java虛擬機器內,還存在於java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體流失。因為有些資源性對象,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性對象在不使用的時候,應該調用它的close()函數,將其關閉掉,然後才置為null.在我們的程式退出時一定要確保我們的資源性對象已經關閉。
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢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 } } }
6、Bitmap沒有回收導致的記憶體溢出
Bitmap的不當處理極可能造成OOM,絕大多數情況都是因這個原因出現的。Bitamp位元影像是Android中當之無愧的胖小子,所以在操作的時候當然是十分的小心了。由於Dalivk並不會主動的去回收,需要開發人員在Bitmap不被使用的時候recycle掉。使用的過程中,及時釋放是非常重要的。同時如果需求允許,也可以去BItmap進行一定的縮放,通過BitmapFactory.Options的inSampleSize屬性進行控制。如果僅僅只想獲得Bitmap的屬性,其實並不需要根據BItmap的像素去分配記憶體,只需在解析讀取Bmp的時候使用BitmapFactory.Options的inJustDecodeBounds屬性。最後建議大家在載入網狀圖片的時候,使用軟引用或者弱引用並進行本機快取,推薦使用android-universal-imageloader或者xUtils,牛人出品,必屬精品。
7、構造Adapter時,沒有使用緩衝的convertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提供了方法:
public View getView(int position, ViewconvertView, 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 --> voidaddScrapView(View scrap)方法。
範例程式碼:
public View getView(int position, ViewconvertView, ViewGroup parent) { View view = new Xxx(...); ... ... return view; } 修正範例程式碼: public View getView(int position, ViewconvertView, ViewGroup parent) { View view = null; if (convertView != null) { view = convertView; populate(view, getItem(position)); ... } else { view = new Xxx(...); ... } return view; }
三、預防OOM的幾點建議
Android開發過程中,在 Activity的生命週期裡協調耗時任務可能會很困難,你一不小心就會導致記憶體流失問題。下面是一些小提示,能協助你預防記憶體流失問題的發生:
1、合理使用static:
每一個非靜態內部類執行個體都會持有一個外部類的引用,若該引用是Activity 的引用,那麼該Activity在被銷毀時將無法被回收。如果你的靜態內部類需要一個相關Activity的引用以確保功能能夠正常運行,那麼你得確保你在對象中使用的是一個Activity的弱引用,否則你的Activity將會發生意外的記憶體流失。但是要注意,當此類在全域多處用到時在這樣幹,因為static聲明變數的生命週期其實是和APP的生命週期一樣的,有點類似與Application。如果大量的使用的話,就會佔據記憶體空間不釋放,積少成多也會造成記憶體的不斷開銷,直至掛掉。static的合理使用一般用來修飾基礎資料型別 (Elementary Data Type)或者輕量級對象,盡量避免修複集合或者大對象,常用作修飾全域配置項、工具類方法、內部類。
2、善用SoftReference/WeakReference/LruCache
Java、Android中有沒有這樣一種機制呢,當記憶體吃緊或者GC掃過的情況下,就能及時把一些記憶體佔用給釋放掉,從而分配給需要分配的地方。答案是肯定的,java為我們提供了兩個解決方案。如果對記憶體的開銷比較關注的APP,可以考慮使用WeakReference,當GC回收掃過這塊記憶體地區時就會回收;如果不是那麼關注的話,可以使用SoftReference,它會在記憶體申請不足的情況下自動釋放,同樣也能解決OOM問題。同時Android自3.0以後也推出了LruCache類,使用LRU演算法就釋放記憶體,一樣的能解決OOM,如果相容3.0一下的版本,請匯入v4包。關於第二條的無關引用的問題,我們傳參可以考慮使用WeakReference封裝一下。
3、謹慎handler
在處理非同步作業的時候,handler + thread是個不錯的選擇。但是相信在使用handler的時候,大家都會遇到警告的情形,這個就是lint為開發人員的提醒。handler運行於UI線程,不斷處理來自MessageQueue的訊息,如果handler還有訊息需要處理但是Activity頁面已經結束的情況下,Activity的引用其實並不會被回收,這就造成了記憶體流失。解決方案,一是在Activity的onDestroy方法中調handler.removeCallbacksAndMessages(null);取消所有的訊息的處理,包括待處理的訊息;二是聲明handler的內部類為static。
4、不要總想著Java 的記憶體回收機制會幫你解決所有記憶體回收問題
就像上面的樣本,我們以為記憶體回收機制會幫我們將不需要使用的記憶體回收,例如:我們需要結束一個Activity,那麼它的執行個體和相關的線程都該被回收。但現實並不會像我們劇本那樣走。Java線程會一直存活,直到他們都被顯式關閉,抑或是其進程被Android系統殺死。所以,為你的後台線程實現銷毀邏輯是你在使用線程時必須時刻銘記的細節,此外,你在設計銷毀邏輯時要根據Activity的生命週期去設計,避免出現Bug。
考慮你是否真的需要使用線程。Android應用的架構層為我們提供了很多便於開發人員執行後台操作的類。例如:我們可以使用Loader 代替在Activity 的生命週期中用線程通過注入執行短暫的非同步後台查詢操作,考慮用Service將結構通知給UI的BroadcastReceiver。最後,記住,這篇博文中對線程進行的討論同樣適用於AsyncTask(因為AsyncTask使用ExecutorService執行它的任務)。然而,雖說ExecutorService只能在短暫操作(文檔說最多幾秒)中被使用,那麼這些方法導致的Activity記憶體流失應該永遠不會發生。
5、ListView和GridView的item緩衝
對於行動裝置,尤其硬體參差不齊的android生態,頁面的繪製其實是很耗時的,findViewById也是蠻慢的。所以不重用View,在有列表的時候就尤為顯著了,經常會出現滑動很卡的現象,所以我們要善於重複利用建立好的控制項。這裡主要注意兩點:
1)convertView重用
ListView中的每一個Item顯示都需要Adapter調用一次getView()的方法,這個方法會傳入一個convertView的參數,這個方法返回的View就是這個Item顯示的View。Android提供了一個叫做Recycler(反覆迴圈)的構件,就是當ListView的Item從滾出螢幕視角之外,對應Item的View會被緩衝到Recycler中,相應的會從產生一個Item,而此時調用的getView中的convertView參數就是滾出螢幕的緩衝Item的View,所以說如果能重用這個convertView,就會大大改善效能。
2)使用ViewHolder重用
我們都知道在getView()方法中的操作是這樣的:先從xml中建立view對象(inflate操作,我們採用了重用convertView方法最佳化),然後在這個view去findViewById,找到每一個item的子View的控制項對象,如:ImageView、TextView等。這裡的findViewById操作是一個樹尋找過程,也是一個耗時的操作,所以這裡也需要最佳化,就是使用ViewHolder,把每一個item的子View控制項對象都放在Holder中,當第一次建立convertView對象時,便把這些item的子View控制項對象findViewById執行個體化出來並儲存到ViewHolder對象中。然後用convertView的setTag將viewHolder對象設定到Tag中, 當以後載入ListView的item時便可以直接從Tag中取出複用ViewHolder對象中的,不需要再findViewById找item的子控制項對象了。這樣便大大提高了效能。
不過Android5.L為我們提供了RecyclerView,RecyclerView是經典的ListView的進化與升華,它比ListView更加靈活,但也因此引入了一定的複雜性。最新的v7支援包新添加了RecyclerView。RecyclerView提供了一種插拔式的體驗,高度的解耦,異常的靈活,通過設定它提供的不同LayoutManager,ItemDecoration , ItemAnimator實現令人瞠目的效果。而且RecyclerView內部為我們處理了item緩衝,所以用著效率更高,更安全,感興趣的讀者可以瞭解一下。
Android中記憶體泄露與如何有效避免OOM總結