Android內(nèi)存泄漏--記錄

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);
    }
}

檢測到泄漏,如圖:

volley_leak by keith

原因分析:
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();
    }
}

檢測到泄漏,如圖

progress_dialog_singleton_leak by keith

原因分析:
由于在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中大神分析;
解決方案:

  1. 用靜態(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);
}
  1. 在引用對象生命周期結(jié)束時,調(diào)用Handler的removeCallbacksAndMessages()或removeCallbacks()或removeMessages();

5. Thread造成的內(nèi)存泄漏
也是一樣,注意引用對象的生命周期

Toooooooooools

  1. MAT(有點麻煩,無愛,就不說了)
  2. LeakCanary

下面說下LeakCanary的簡單使用,真的很簡單

使用方法
  1. 在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'
 }
  1. 在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);
  }
}
  1. 在需要監(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)建誰釋放的原則。

Reference

  1. Android 內(nèi)存泄漏總結(jié)
  2. Android性能優(yōu)化之常見的內(nèi)存泄漏
  3. 內(nèi)存泄露從入門到精通三部曲之基礎(chǔ)知識篇
  4. 內(nèi)存泄露從入門到精通三部曲之排查方法篇
  5. 內(nèi)存泄露從入門到精通三部曲之常見原因與用戶實踐
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,115評論 25 709
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    _痞子閱讀 1,703評論 0 8
  • 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏大家都不陌生了,簡單粗俗的講,...
    宇宙只有巴掌大閱讀 2,492評論 0 12
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    apkcore閱讀 1,310評論 2 7
  • 說起來,今年5月份我才算是真正畢業(yè),以一場大家抱頭痛哭的狼狽場景為休止符。但是我卻常常有一種已經(jīng)畢業(yè)了好久的感覺,...
    菜白呀閱讀 366評論 0 2

友情鏈接更多精彩內(nèi)容