前言
記憶體洩漏向來都是記憶體優化的重點,它如同幽靈一般存於我們的應用當中,有時它不會現身,但一旦現身就會讓你頭疼不已。因此,如何避免、發現和解決記憶體洩漏就變得尤為重要。這一篇我們先來學習如何避免記憶體洩漏。
1.什麼是記憶體洩漏
我們知道,每個應用程式都需要記憶體來完成工作,為了確保Android系統的每個應用都有足夠的記憶體,Android系統需要有效地管理記憶體分配。當記憶體不足時,Android執行時就會觸發GC,GC採用的垃圾標記演算法為根搜尋演算法,
在Java虛擬機器(三)垃圾標記演算法與Java物件的生命週期這篇文章中講到了根搜尋演算法,如下圖所示。
從上圖看以看出,Obj4是可達的物件,表示它正被引用,因此不會標記為可回收的物件。Obj5、Obj6和Obj7都是不可達的物件,其中Obj5和Obj6雖然互相引用,但是因為他們到GC Roots是不可達的所以它們仍舊會標記為可回收的物件。
記憶體洩漏就是指沒有用的物件到GC Roots是可達的(物件被引用),導致GC無法回收該物件。此時,如果Obj4是一個沒有用的物件,但它仍與GC Roots是可達的,那麼Obj4就會記憶體洩漏。
記憶體洩漏產生的原因,主要分為三大類:
1.由開發人員自己編碼造成的洩漏。
2.第三方框架造成的洩漏。
3.由Android 系統或者第三方ROM造成的洩漏。
其中第二種和第三種有時是不可控的,但是第一種是可控的,既然是可控的,我們就要儘量在編碼時避免造成記憶體洩漏,下面就來列舉出常見的記憶體洩漏的場景。
2.記憶體洩漏的場景
2.1 非靜態內部類的靜態例項
非靜態內部類會持有外部類例項的引用,如果非靜態內部類的例項是靜態的,就會間接的長期維持著外部類的引用,阻止被系統回收。
public class SecondActivity extends AppCompatActivity {
private static Object inner;
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
createInnerClass();
finish();
}
});
}
void createInnerClass() {
class InnerClass {
}
inner = new InnerClass();//1
}
}複製程式碼
當點選Button時,會在註釋1處建立了非靜態內部類InnerClass的靜態例項inner,該例項的生命週期會和應用程式一樣長,並且會一直持有SecondActivity 的引用,導致SecondActivity無法被回收。
2.2 匿名內部類的靜態例項
和前面的非靜態內部類一樣,匿名內部類也會持有外部類例項的引用。
public class AsyncTaskActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncTask();
finish();
}
});
}
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {//1
@Override
protected Void doInBackground(Void... params) {
while (true) ;
}
}.execute();
}
}複製程式碼
在註釋1處例項化了一個AsyncTask,當AsyncTask的非同步任務在後臺執行耗時任務期間,AsyncTaskActivity 被銷燬了,被AsyncTask持有的AsyncTaskActivity例項不會被垃圾收集器回收,直到非同步任務結束。
解決辦法就是自定義一個靜態的AsyncTask,如下所示。
public class AsyncTaskActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_async_task);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncTask();
finish();
}
});
}
void startAsyncTask() {
new MyAsyncTask().execute();
}
private static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... params) {
while (true) ;
}
}
}複製程式碼
與AsyncTask類似的還有TimerTask,這裡就不再舉例。
2.3 Handler記憶體洩漏
Handler的Message被儲存在MessageQueue中,有些Message並不能馬上被處理,它們在MessageQueue中存在的時間會很長,這就會導致Handler無法被回收。如果Handler 是非靜態的,則Handler也會導致引用它的Activity或者Service不能被回收。
public class HandlerActivity extends AppCompatActivity {
private Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
button = (Button) findViewById(R.id.bt_next);
final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mHandler.sendMessageDelayed(Message.obtain(), 60000);
finish();
}
});
}
}複製程式碼
Handler 是非靜態的匿名內部類的例項,它會隱性引用外部類HandlerActivity 。上面的例子就是當我們點選Button時,HandlerActivity 會finish,但是Handler中的訊息還沒有被處理,因此HandlerActivity 無法被回收。
解決方法就是要使用一個靜態的Handler內部類,Handler持有的物件要使用弱引用,並且在Activity的Destroy方法中移除MessageQueue中的訊息,如下所示。
public class HandlerActivity extends AppCompatActivity {
private Button button;
private MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
myHandler.sendMessageDelayed(Message.obtain(), 60000);
finish();
}
});
}
public void show() {
}
private static class MyHandler extends Handler {
private final WeakReference<HandlerActivity> mActivity;
public MyHandler(HandlerActivity activity) {
mActivity = new WeakReference<HandlerActivity2>(activity);
}
@Override
public void handleMessage(Message msg) {
if (mActivity != null && mActivity.get() == null) {
mActivity.get().show();
}
}
}
@Override
public void onDestroy() {
if (myHandler != null) {
myHandler.removeCallbacksAndMessages(null);
}
super.onDestroy();
}
}複製程式碼
MyHandler是一個靜態的內部類,它持有的 HandlerActivity物件使用了弱引用,並且在onDestroy方法中將Callbacks和Messages全部清除掉。
如果覺得麻煩,也可以使用避免記憶體洩漏的Handler開源庫WeakHandler。
2.4 未正確使用Context
對於不是必須使用Activity Context的情況(Dialog的Context就必須是Activity Context),我們可以考慮使用Application Context來代替Activity的Context,這樣可以避免Activity洩露,比如如下的單例模式:
public class AppSettings {
private Context mAppContext;
private static AppSettings mAppSettings = new AppSettings();
public static AppSettings getInstance() {
return mAppSettings;
}
public final void setup(Context context) {
mAppContext = context;
}
}複製程式碼
mAppSettings作為靜態物件,其生命週期會長於Activity。當進行螢幕旋轉時,預設情況下,系統會銷燬當前Activity,因為當前Activity呼叫了setup方法,並傳入了Activity Context,使得Activity被一個單例持有,導致垃圾收集器無法回收,進而產生了記憶體洩露。
解決方法就是使用Application的Context:
public final void setup(Context context) {
mAppContext = context.getApplicationContext();
}複製程式碼
2.5 靜態View
使用靜態View可以避免每次啟動Activity都去讀取並渲染View,但是靜態View會持有Activity的引用,導致Activity無法被回收,解決的辦法就是在onDestory方法中將靜態View置為null。
public class SecondActivity extends AppCompatActivity {
private static Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.bt_next);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
}
}複製程式碼
2.6 WebView
不同的Android版本的WebView會有差異,加上不同廠商的定製ROM的WebView的差異,這就導致WebView存在著很大的相容性問題。WebView都會存在記憶體洩漏的問題,在應用中只要使用一次WebView,記憶體就不會被釋放掉。通常的解決辦法就是為WebView單開一個程式,使用AIDL與應用的主程式進行通訊。WebView程式可以根據業務需求,在合適的時機進行銷燬。
2.7 資源物件未關閉
資源物件比如Cursor、File等,往往都用了緩衝,不使用的時候應該關閉它們。把他們的引用置為null,而不關閉它們,往往會造成記憶體洩漏。因此,在資源物件不使用時,一定要確保它已經關閉,通常在finally語句中關閉,防止出現異常時,資源未被釋放的問題。
2.8 集合中物件沒清理
通常把一些物件的引用加入到了集合中,當不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就會更加嚴重。
2.9 Bitmap物件
臨時建立的某個相對比較大的bitmap物件,在經過變換得到新的bitmap物件之後,應該儘快回收原始的bitmap,這樣能夠更快釋放原始bitmap所佔用的空間。
避免靜態變數持有比較大的bitmap物件或者其他大的資料物件,如果已經持有,要儘快置空該靜態變數。
2.10 監聽器未關閉
很多系統服務(比如TelephonyMannager、SensorManager)需要register和unregister監聽器,我們需要確保在合適的時候及時unregister那些監聽器。自己手動add的Listener,要記得在合適的時候及時remove這個Listener。
參考資料
Eight Ways Your Android App Can Leak Memory
Memory Leak Patterns in Android
Handler導致記憶體洩露分析
Android App 記憶體洩露之Handler
[譯]Android記憶體洩漏的八種可能(上)
[譯]Android防止記憶體洩漏的八種方法(下)
Android 應用記憶體洩漏的定位、分析與解決策略
《Android應用效能優化最佳實踐》
歡迎關注我的微信公眾號,第一時間獲得部落格更新提醒,以及更多成體系的Android相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。