Heads up:
本文主要記錄本人開發(fā)中所遇到內(nèi)存泄漏情況,然后介紹相關(guān)內(nèi)存泄漏的檢測方法,并搜集了一些大神的理論分析,從而由淺到深、實際結(jié)合理論,盡可能減少內(nèi)存泄漏的出現(xiàn),并提醒大家和我自己寫代碼時多注意內(nèi)存泄漏的情況。
遇到過泄漏的場景:
==小注:我是用LeakCanary來檢測的,下面會介紹 ==
1. Volley網(wǎng)絡(luò)請求中的泄漏
代碼如下:
public class ScrollingActivity extends AppCompatActivity {
private String mUrl = "http://www.itdecent.cn/users/dd2b86a1f116/latest_articles";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_scrolling);
// do a volley request
RequestQueue queue = Volley.newRequestQueue(this);
// 內(nèi)部類Listener的對象持有外部類實例的引用
StringRequest request = new StringRequest(Request.Method.GET, mUrl, new Response.Listener<String>() {
@Override
public void onResponse(String response) {
Log.i("Log", "onResponse: " + response);
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
queue.add(request);
}
}
檢測到泄漏,如圖:

原因分析:
a. 在Activity或Fragment中進行網(wǎng)絡(luò)請求,在宿主生命周期結(jié)束時未對相應(yīng)的request進行cancel,由于volley中的listener對象仍然持有宿主一些屬性的引用,使得GC不會回收,從而導(dǎo)致leak
b. 在官方的volley包中,NetworkDispatcher中的run方法中,當請求隊列里面無請求時,本地request仍然持有最后一個請求的引用,使得GC不會回收,導(dǎo)致leak
//NetworkDispatcher
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
// Take a request from the queue
request = mQueue.take(); //PriorityBlockingQueue
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
...
------------------------------------
// PriorityBlockingQueue
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
//這是關(guān)鍵,當request==null的時候,當前線程處于等待狀態(tài),
//也就造成了NetworkDispatcher的run方法中的局部request變量一直
//帶有最后一個request的引用,直到有新的請求進來或者線程被打斷
while ( (result = dequeue()) == null)
notEmpty.await();
} finally {
lock.unlock();
}
return result;
}
解決方案:
a. 及時調(diào)用volley quene的cancel方法
b. 使用改良版的volley包,如mcxiaoke
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
// release previous request object to avoid leaking request object when mQueue is drained.
// 這個就是關(guān)鍵的制空
request = null;
try {
// Take a request from the queue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interrupted because it was time to quit.
if (mQuit) {
return;
}
continue;
}
2. 單例Dialog導(dǎo)致的泄露
這個是我自己寫的一個ProgressDialog工具類,主要用在網(wǎng)絡(luò)請求時的等待顯示,我覺得挺好用,直到檢查內(nèi)存泄漏,真是too naive了,代碼如下:
public class ProgressUtils {
public static ProgressDialog progressDialog = null;
public static void dismissProgressDialog() {
if (null != progressDialog && progressDialog.isShowing()) {
progressDialog.dismiss();
}
}
public static void showProgressDialog(Context context, String message) {
if (null != progressDialog && progressDialog.isShowing()) {
return;
}
if (progressDialog == null || progressDialog.getContext() != context) {
progressDialog = new ProgressDialog(context);
}
progressDialog.setMessage(message);
progressDialog.show();
}
}
檢測到泄漏,如圖

原因分析:
由于在Activity生命周期結(jié)束之后,單例Dialog內(nèi)部的變量mContext中的mBase仍然持有該Activity對像的引用,雖然下次其他Activity來調(diào)用Dialog時上個Activity對象的引用會被踢掉,但是還是會有一個Activity對象的泄漏,簡單的代碼分析如下:
// 1. ProgressUtils
progressDialog = new ProgressDialog(context);
// 2. 一路點擊構(gòu)造方法下去,會到Dialog的這個方法中(createContextThemeWrapper為true)
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == 0) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
// 走這一步
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
···
}
// 3. 接著是mContext指向的對象的創(chuàng)建,會走到ContextWrapper中
public class ContextWrapper extends Context {
//就是這個拿著Activity的引用
Context mBase;
public ContextWrapper(Context base) {
mBase = base;
}
···
解決方案:
由于Dialog比較特別(如下圖),只能用Activity才能啟動(當然有一些用其他context也可以,但是要申請權(quán)限什么的,不適用于一般app),所以不要這種單例ProgressDialog工具類,在Activity或Fragment的生命中控制dialog的show和dismiss;
注: 圖片來自reference-2
3. 單例Toast導(dǎo)致的泄露(同單例dialog)
使用全局ApplicaitonContext
4. 百度地圖LocationClient定位服務(wù)內(nèi)存泄漏
使用全局ApplicaitonContext
5. Handler內(nèi)存泄漏
原因就不說,也是內(nèi)部引用的沒有跟引用對象的生命周期同步,具體可以看reference中大神分析;
解決方案:
- 用靜態(tài)Handler和WeakReference,例如:
//子類繼承這個類,然后正常用就好了
public abstract class WeakReferenceHandler<T> extends Handler {
private WeakReference<T> mReference;
public WeakReferenceHandler(T reference) {
mReference = new WeakReference<T>(reference);
}
@Override
public void handleMessage(Message msg) {
if (mReference.get() == null) {
return;
}
handleMessage(mReference.get(), msg);
}
protected abstract void handleMessage(T reference, Message msg);
}
- 在引用對象生命周期結(jié)束時,調(diào)用Handler的removeCallbacksAndMessages()或removeCallbacks()或removeMessages();
5. Thread造成的內(nèi)存泄漏
也是一樣,注意引用對象的生命周期
Toooooooooools
- MAT(有點麻煩,無愛,就不說了)
- LeakCanary
下面說下LeakCanary的簡單使用,真的很簡單
使用方法
- 在build.gradle中加依賴
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.4-beta2'
// release 中的版本里面就9個空方法,放心大膽的去用吧,用proguard這些就會被剝掉為0,更不用擔心了
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.4-beta2'
}
- 在Application中添加如下代碼,別忘了在AndroidManifest.xml中使用
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
- 在需要監(jiān)控的地方加如下代碼(以Fragment為例)
public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
總之,注意引用對象的生命周期,注意引用對象的生命周期,注意引用對象的生命周期這個是重點。
經(jīng)驗總結(jié)----來自Bugly
- 對 Activity 等組件的引用應(yīng)該控制在 Activity 的生命周期之內(nèi);如果不能就考慮使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部長生命周期的對象引用而泄露。
- 在代碼復(fù)審的時候關(guān)注長生命周期對象:全局性的集合、單例模式的使用、類的 static 變量等等。
- 盡量不要在靜態(tài)變量或者靜態(tài)內(nèi)部類中使用非靜態(tài)外部成員變量(包括context ),即使要使用,也要考慮適時把外部成員變量置空;也可以在內(nèi)部類中使用弱引用來引用外部類的變量。
- Handler 的持有的引用對象最好使用弱引用,資源釋放時也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的時候,取消掉該 Handler 對象的 Message和 Runnable,removeCallbacks(Runnable r) 或removeMessages(int what),或 removeCallbacksAndMessages(null)等。
- 線程 Runnable 執(zhí)行耗時操作,注意在頁面返回時及時取消或者把 Runnable 寫成靜態(tài)類。
a) 如果線程類是內(nèi)部類,改為靜態(tài)內(nèi)部類。
b) 線程內(nèi)如果需要引用外部類對象如 context,需要使用弱引用。 - 在 Java 的實現(xiàn)過程中,也要考慮其對象釋放,最好的方法是在不使用某對象時,顯式地將此對象賦空,如清空對圖片等資源有直接引用或者間接引用的數(shù)組(使用 array.clear() ; array = null),最好遵循誰創(chuàng)建誰釋放的原則。