Android內存優化(一)之Android常見的Java層內存泄露場景及合理的修復方案

首先解釋下內存泄露:
內存泄漏(Memory Leak)是指程序中己動態分配的堆內存由於某種原因程序未釋放或無法釋放,造成系統內存的浪費,導致程序運行速度減慢甚至系統崩潰等嚴重後果。
內存泄露分爲永久性泄露和臨時性泄露.永久性泄露是指只要泄露出現,泄露的內存永遠不會回收,此種情況一般問題比較嚴重,一旦發現,需快速解決.臨時性泄露是指泄露場景出現後,在在未來的某段時間內回收掉內存,有些人會感覺這種情況不是泄露問題,但其實也是內存泄露的一種,臨時性泄露會造成系統資源的浪費,對於手機性能,電量都是損害,很可能會出現頁面的卡頓情況,在低端機上表現尤爲明顯,因此也應需要嚴肅對待~
從當前內存工具內存自動化監測工具報的內存泄露問題,內存泄露場景主要有以下幾種,下面的分析會結合源碼按照此順利依次展開詳細說明:

  • (1)AsyncTask使用異常,最爲常見
  • (2)Handler, Runnable(Thread)生命週期異常, 常見
  • (3)HandlerThread使用異常,常見
  • (4)非靜態內部類持有外部類的引用,常見
  • (5)匿名內存類持有外部類的引用,常見
  • (6)static變量持有activity,常見
  • (7)static view持有activity,常見
  • (8)資源未關閉
  • (9)註冊的監聽器未註銷
  • (10)其他泄露場景,包含了最近出現的各種case集合,出現概率不高,不做統一分析
    其實AsyncTask泄露,Handler,Runnable(Thread)泄露,HandlerThread泄露基本都是由於非靜態內部類持有外部類的引用和匿名內存類持有外部類的引用這兩種方式泄露,只是這幾種泄露出現的頻率非常之高,因此決定把幾個場景單獨拎出來,着重說下,以及具體說下這幾種泄露場景的修復方式~

(1) AsyncTask的泄露問題

AsyncTask泄露問題非常之多,也從源碼運行的角度重點說下這個泄露問題,只要理解了源碼,就會明瞭其實AsyncTask的泄露問題也是蠻嚴重的~

關鍵引用:

GC ROOT thread java.lang.Thread. (named ‘AsyncTask #17’)

原理:

在使用AsyncTask時,一般會繼承AsyncTask並重寫doInBackground方法,onPostExecute方法,在doInBackground方法中做耗時操作,在onPostExecute方法中更新UI,這是日常用法~
泄露的場景是,當Activity onDestroy方法回調後,AsyncTask的方法沒有執行完成,或者是在doInBackground方法中,或者是在onPostExecute方法中,而AsyncTask持有Activity的引用(一般是非靜態內部類持有外部類的引用和匿名內存類持有外部類的引用兩種形式,具體可以看下面的解釋),導致Activity無法及時回收,從而導致內存泄露~
看過AsyncTask的源碼的同學應該會知道,AsyncTask的設計其實是對Handler+Thread的封裝,相比於Handler的使用,AsyncTask讓開發者用起來更加簡單與方便,而帶來的就是對其瞭解的不夠透徹
AsyncTask的運行是通過調用execute方法,可能很多人會知道AsyncTask的運行背後是有一個線程池,猜想就是多個AsyncTask可以在多核處理器上同時被調度

187    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
188    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
189    private static final int KEEP_ALIVE_SECONDS = 30;
190
191    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
192        private final AtomicInteger mCount = new AtomicInteger(1);
193
194        public Thread newThread(Runnable r) {
195            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
196        }
197    };
198
199    private static final BlockingQueue<Runnable> sPoolWorkQueue =
200            new LinkedBlockingQueue<Runnable>(128);

但其實已經不準確了,這是很老的觀點了~現在的AsyncTask的執行默認是靠sDefaultExecutor的調度,

558    @MainThread
559    public final AsyncTask<Params, Progress, Result> execute(Params... params) {
560        return executeOnExecutor(sDefaultExecutor, params);
561    }

sDefaultExecutor在AsyncTask中是以常量形式存在,而且整個App的AsyncTask實例會公用一個sDefaultExecutor,在AsyncTask叫SERIAL_EXECUTOR,翻譯過來是線性執行器的意思,其實就是化並行爲串行的意思~

219    public static final Executor SERIAL_EXECUTOR = new SerialExecutor();
224    private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
235    private static class SerialExecutor implements Executor {
236        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
237        Runnable mActive;
238
239        public synchronized void execute(final Runnable r) {
240            mTasks.offer(new Runnable() {
241                public void run() {
242                    try {
243                        r.run();
244                    } finally {
245                        scheduleNext();
246                    }
247                }
248            });
249            if (mActive == null) {
250                scheduleNext();
251            }
252        }
253
254        protected synchronized void scheduleNext() {
255            if ((mActive = mTasks.poll()) != null) {
256                THREAD_POOL_EXECUTOR.execute(mActive);
257            }
258        }
259    }

SerialExecutor使用雙端隊列ArrayDeque管理Runnable對象,如果一次性啓動了多個任務,首先第一個Task執行execute方法時,調用ArrayDeque的offer將傳入的Runnable對象添加至隊列尾部,然後判斷mActive是否爲null,第一次運行時爲null,會調用scheduleNext方法,在scheduleNext方法中賦值mActive,通過THREAD_POOL_EXECUTOR調度,之後再有新的任務被執行時,同樣會調用offer方法將傳入的Runnable對象添加至隊列的尾部,但此時mActive不在爲null,於是不會執行scheduleNext方法,也就是說不會得到立即執行,那什麼時候會執行呢?看finally中,同樣會調用scheduleNext方法,也就是說,當此Task執行完成後,會去執行下一個Task,SerialExecutor模仿的是單一線程池的效果,如果我們快速地啓動了很多任務,同一時刻只會有一個線程正在執行,其餘的均處於等待狀態。
那麼假設一下,如果用戶開啓某個頁面,而此頁面有Task在執行,再打開另外一個頁面,這個頁面還有Task需要執行,這個時候很可能會出現卡一個的情況,不是硬件配置差,而是軟件質量差導致的~那麼如何修復呢?

解決辦法

1: cancel + isCancelled ,強烈推薦
但需解釋下,cancel方法可能不會得到立即執行,在接口調用處也有如下說明:

/**
 * <p>Attempts to cancel execution of this task. This attempt will
 * fail if the task has already completed, already been cancelled,
 * or could not be cancelled for some other reason. If successful,
 * and this task has not started when <tt>cancel</tt> is called,
 * this task should never run. If the task has already started,
 * then the <tt>mayInterruptIfRunning</tt> parameter determines
 * whether the thread executing this task should be interrupted in
 * an attempt to stop the task.</p>
 * 
 * <p>Calling this method will result in {@link #onCancelled(Object)} being
 * invoked on the UI thread after {@link #doInBackground(Object[])}
 * returns. Calling this method guarantees that {@link #onPostExecute(Object)}
 * is never invoked. After invoking this method, you should check the
 * value returned by {@link #isCancelled()} periodically from
 * {@link #doInBackground(Object[])} to finish the task as early as
 * possible.</p>
 *  * @param mayInterruptIfRunning <tt>true</tt> if the thread executing this
 * task should be interrupted; otherwise, in-progress tasks are allowed
 * to complete.
 *  * @return <tt>false</tt> if the task could not be cancelled,
 * typically because it has already completed normally;
 * <tt>true</tt> otherwise
 *  * @see #isCancelled()
 * @see #onCancelled(Object)
 */
public final boolean cancel(boolean mayInterruptIfRunning) {
    mCancelled.set(true);
    return mFuture.cancel(mayInterruptIfRunning);
}

第一段的意思是如果任務沒有運行且cancel方法被調用,那麼任務會被立即取消且確保不會被執行,當任務已經啓動了,mayInterruptIfRunning參數決定是否嘗試去停止Task
第二段的意思是調用cancel方法能確保onPostdExecute方法不會被執行,執行了cancel方法,不會立即終止任務,會等doInBackground方法執行完成後返回,然後定期通過調用isCancelled方法檢查task狀態儘早的結束task~什麼意思呢?AsyncTask不會立即結束一個正在運行的線程,調用cancel方法只是給AsyncTask設置了”cancelled”狀態,並不是停止Task,那麼有人說是不是由mayInterruptIfRunning參數來控制?其實mayInterruptIfRunning只是執行線程的interrupt方法,並不是真正的中斷線程,而是通知線程應該中斷了~
什麼意思?具體來說,當一個線程調用interrupt方法,
如果線程處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼線程將立即退出被阻塞狀態,並拋出一個InterruptedException異常。僅此而已。
如果線程處於正常活動狀態,那麼會將該線程的中斷標誌設置爲 true,僅此而已。被設置中斷標誌的線程將繼續正常運行,不受影響。
真正決定任務取消的是需要手動調用isCancelled方法check task狀態,因此推薦的修復方案是在手動調用cancel方法的同時,能調用inCancelled方法檢測task狀態:

@Override
protected Integer doInBackground(Void... mgs) {
// Task被取消了,馬上退出
if(isCancelled()) return null;
.......
// Task被取消了,馬上退出
if(isCancelled()) return null;
}
...

2:建議在修復方案1的基礎上將AsyncTask作爲靜態內部類存在(與Handler處理方式相似),避免內部類的this$0持有外部類的引用
但不推薦只修改AsyncTask爲靜態內部類的方案,雖然不是泄露了,但沒有根本上解決問題~
3:如果AsyncTask中需要使用Context,建議使用weakreference
4:如果確實需要做相對耗時的操作,建議用service去做,而不要用AsyncTask,推薦

(2) Handler,Runnable(Thread)泄露問題

這個問題爆發的也是相當多,說下此問題

關鍵引用:

references android.os.MessageQueue.mMessages’
references android.os.Message.callback’

原理

作爲Android的一個通信利器,消息機制永遠都是不可忽略的,主要是靠Handler,Message,MessageQueue,Looper四個類來完成各自任務

  • Message:消息分爲硬件產生的消息(如按鈕、觸摸)和軟件生成的消息;

  • MessageQueue:消息隊列的主要功能向消息池投遞消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);

  • Handler:消息輔助類,主要功能向消息池發送各種消息事件(Handler.sendMessage)和處理相應消息事件(Handler.handleMessage);

  • Looper:不斷循環執行(Looper.loop),按分發機制將消息分發給目標處理者。
    架構圖(來源於gityuan的博客)如:

Handler泄露是怎麼發生的呢?當我們使用Handler時,往往會在Activity創建非靜態Handler實例,重寫handleMessage方法,此時會隱式持有外部Activity引用(如果不理解隱式持有外部Activity引用,可以看(4)非靜態內部類持有外部類的引用,裏面會詳細解釋),而MessageQueue持有Message引用,Message持有Handler引用,也就是說說Message不被消費,就不會釋放Activity~有很多業務組會postDelayed,在幾分鐘之後打點啊,統計啊或者做其他操作,都是不合理的~或者在Message中處理非常耗時的操作,最後造成消息堆積,無法得到及時處理,最後造成內存泄露~
Runnable(Thread)泄露相對更容易理解,主要是異步線程持有外部Activity 的引用,而回調Activity onDestroy方法時,線程沒有執行完成,導致內存泄露~

解決辦法

解決內存泄露就是剪斷引用鏈的過程,從引用鏈來看,能動的就是Handler和Activity的引用關係,因此解決方案主要有:
1:在Activity的onDestroy執行時,Handler泄露可手動調用Handler的removeCallbacksAndMessages,清除異步消息,Runnable(Thread)泄露則可通過終止線程(控制邏輯需要自己寫),切斷引用鏈~推薦
2:將Handler,Runnable(Thread)定義爲靜態內部類,推薦
通過此方式,不會隱式持有外部Activity的引用
3:如果確實需要使用Activity做相關操作,建議使用弱引用,或者使用ApplicationContext,推薦
4:如果確實有耗時操作,建議使用jobschedule去做,推薦

(3) HandlerThread 泄露

關鍵引用:

GC ROOT android.os.HandlerThread.< Java Local>

原理

HandlerThread的封裝其實也是爲了解決線程通信,本質還是線程,只不多內部建立了Looper,從上面的Handler瞭解到如果一個線程處理消息,需要Handler,Message,MessageQueue,Looper四者完成各自任務,一般使用的時候會手動調用Looper.prepare(),Looper.loop()方法(主線程除外,主線程會自動創建Looper對象並開啓消息循環),而使用HandlerThread時,相對簡單一些,一般創建HandlerThread並開啓,就可以進行線程通信了~
解決辦法
1:在Activity的onDestroy方法中,手動調用HandlerThread的quit方法,強烈推薦
當我們調用HandlerThread的quit方法方法時,實際就是執行了MessageQueue中的removeAllMessagesLocked方法,把MessageQueue消息池中的所有消息全部清空,無論是延時消息(延遲消息是指通過sendMessageDelayed或通過postDelayed等方法發送的需要延遲執行的消息)還是非延遲消息。
在HandlerThread中還有一個方法:quitSafely,簡單提一下,實際執行的是removeAllFutureMessageLocked方法,只會清空MessageQueue消息池中所有的延遲消息,並將所有非延遲消息派發出去讓Handler處理,相比於quit,更安全一些,這個看業務組的需求吧,一般Activity都退出了,消息派不派發都沒有實際意義了
2:將HandlerThread定義爲靜態內部類,推薦
3:使用ApplicationContext,推薦

(4) 非靜態內部類持有外部類的引用

這是個很籠統的概念,引起此種泄露的原因可能有很多,希望能找到真正泄露的東西,而不是簡單的將其改成static或者使用ApplicationContext

關鍵引用:

this$0

原理:

泄露出現的場景主要是非靜態內部類隱式持有外部Activity的引用,而非靜態內部類的生命週期超出了Activity的生命週期,在Activity執行onDestroy方法時,由於非靜態內部類隱式持有外部Activity的引用,導致Activity無法GC,從而導致內存泄露.
在這裏解釋下關鍵引用this$0是個啥?this$0的意思就是所說的隱式持有外部Activity引用,內部類可以訪問外部類的成員變量,靠的就是this$0,這個東西是編譯器自動加上的,不需要手動定義,在反編譯的smali文件中很容易看到~(當然如果有多層內部類的嵌套,會有this$1,this$2)

在AAAAAActivity.java文件中,SdkDialogFragment是AAAAAActivity的內部類,簡單看下

class BBBBBFragment extends DialogFragment {

    private Dialog mDialog;

    public BBBBBFragment(Dialog dialog) {
        mDialog = dialog;
    }
}

反編譯後,在AAAAAActivity$BBBBBFragment.smali文件中有如下片段

# instance fields
.field private mDialog:Landroid/app/Dialog;
.field final synthetic this$0:Lmiui/external/AAAAAActivity;

# direct methods
.method public constructor <init>(Lmiui/external/AAAAAActivity;Landroid/app/Dialog;)V
.locals 0
.param p1, "this$0" # Lmiui/external/AAAAAActivity;
.param p2, "dialog" # Landroid/app/Dialog;
.prologue
.line 107
iput-object p1, p0, Lmiui/external/AAAAAActivity$BBBBBFragment;->this$0:Lmiui/external/AAAAAActivity;
invoke-direct {p0}, Landroid/app/DialogFragment;-><init>()V
.line 108
iput-object p2, p0, Lmiui/external/AAAAAActivity$BBBBBFragment;->mDialog:Landroid/app/Dialog;
.line 107
return-void
.end method

解決辦法

1:非靜態內部類持有外部類的引用,主要是非靜態內部類的生命週期超過了Activity的生命週期,但原因可能有很多種,或者是AsyncTask問題,或者是Runnable問題,或者是未unregister問題,或者是資源未關閉問題
建議找到泄露的真兇,從引用鏈着手,如果沒有思路可以看看hprof做進一步的分析~強烈推薦~
2:如果想快速修復,可以有幾個辦法,如非靜態內部類改爲靜態內部類,如使用Application Context,但都不是很推薦~

(5) 匿名內存類持有外部類的引用

跟(4)很相似,也是個很籠統的概念,引起此種泄露的原因可能有很多,希望能找到真正泄露的東西,而不是簡單的將其改成static或者使用ApplicationContext

關鍵引用:

this$0;anonymous

原理:

泄露出現的場景主要是匿名內部類隱式持有外部Activity的引用,而匿名內部類的生命週期超出了Activity的生命週期,在Activity執行onDestroy方法時,由於匿名內部類隱式持有外部Activity的引用,導致Activity無法GC,從而導致內存泄露.與(4)基本是一個道理,與(4)的差別,在反編譯的時候經常會看到xxxxx1.class,xxxxxx 2.class,這些就是匿名內部類,經常的書寫格式一般是new xxxxxx() { 類的成員變量,成員方法 }.xxxxx();
其實我們經常用,

leak_single.setOnClickListener(new View.OnClickListener() {
    @Override
 public void onClick(View view) {
        Intent intent = new Intent(MainActivity.this,LeakSingleTestActivity.class);
        startActivity(intent);
    }
});

或者在創建Runnable,創建Handler,創建AsyncTask時,都有可能會使用匿名內部類,但這個時候一定要小心~,很有可能會出現內存泄露的問題

解決辦法:

不要貪圖用匿名內部類的代碼簡潔而忽略了內存泄露問題,對於有泄露風險的匿名內部類,建議都要改成內部類的形式~強烈推薦,然後再找到真正泄露的點,這就回到(4)的解決方案

(6) static變量持有activity

此種泄露相對簡單好解一些,從引用鏈上基本就可以判斷問題處在什麼地方

關鍵引用:

GC ROOT static 加上應用的包名
也有不是App自身的問題,比如系統的問題或者第三方sdk的問題

原理:

static成員變量的使用不恰當,在某個時機,將activity傳入當做context,從而導致Activity無法正常被回收~

解決辦法:

1:找到static成員變量泄露的Activity的時機,並將其引用鏈剪斷即可
2:如果可以,將static變量回復爲非static變量,使其可以正常GC
3:如果確實需要context,且需要保持static,則可使用Application Context

(7) static view

此種問題比較好確認,GC ROOT是包名相關的View名稱

關鍵引用:

GC ROOT static 包名**[View名稱]

原理:

當某個View初始化時耗費大量資源,而且要求Activity生命週期內保持不變,這個時候很多業務組可能會吧view變成static,加載到視圖樹上(View Hierachy),像這樣,當Activity被銷燬時,應當釋放資源。但很可能會帶來泄露問題,View是跟Context緊密關聯,使用不當就會出現泄露問題,需要特別注意.
可能有的朋友說,我在使用View的時候沒操作Context?怎麼會有Activity的引用呢?
其實View的代碼中是默認有Context

3988    public View(Context context) {
3989        mContext = context;

Context是什麼時候給到View的呢?View創建的時候,new View,有的時候是我們寫代碼時自己new,更多的時候是setContentView時將Activity作爲context傳給View

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

解決辦法:

1:通過設計改變static爲普通變量,不要在Android中使用static修飾View,完全避免此種可能,推薦
2:在onDestroy時將static view 置爲null

(8)資源未關閉

對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的代碼,應該在Activity銷燬時及時關閉或者註銷,否則這些資源將不會被回收,造成內存泄漏。

(9)註冊的監聽器未註銷

在Android程序裏面存在很多需要register與unregister的監聽器,我們需要確保及時unregister監聽器。

(10)其他泄露場景

1> 集合中的內存泄漏
我們通常把一些對象的引用加入到了集合容器(比如ArrayList)中,當我們不需要該對象時,
並沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就更嚴重了。
所以要在退出程序之前,將集合裏的東西clear,然後置爲null,再退出程序。
2> AccountManager使用不當導致的泄露
關鍵引用:GC ROOT static com.xiaomi.accounts.AccountManager.sThis
references com.xiaomi.accounts.AccountManagerAmsTask Response
addAccount的時候activity參數傳null, 這樣AmsTask不持有activity的引用, 避免泄露的產生.
但是如果不傳activity, 會導致無法啓動賬號的登錄界面, 所以activity還是必要的,
解決方案就是在addAccount參數的回調AccountManagerCallback中手動獲取intent, AccountManagerCallback持有一個activity的弱引用, 啓動activity時是判斷持有的activity是否存在
3> EventBus使用不當導致泄露
GC ROOT static org.greenrobot.eventbus.EventBus.b\
references org.greenrobot.eventbus.EventBus.f
4> InputMethodManager mCurRootView導致的泄露
關鍵引用:references android.view.inputmethod.InputMethodManager.mCurRootView
5> 疑似系統內存泄露
GC ROOT static android.app.ActivityThread.sCurrentActivityThread
6> 疑似webview泄露
7>NetWorkDispatcher

總結:

內存泄露問題的修復方案可能會有很多種,而且也會有快速修復方法,但希望各位能把泄露的root cause找到,簡單的修復方案可能只是將問題掩蓋了,檢測工具檢測不出來,但沒有實質性解決泄露問題~
希望各位在寫代碼的時候,能在腦海中模擬出手機運行這段代碼的場景,從開始到消亡,做好控制~一段好的代碼,高質量的代碼,就跟藝術品一樣,需要各位花些心思,這樣才能真正解決手機的內存問題或其他問題,一點一點讓手機更流暢~
附常見的修復方案:
1:使用Application的Context,替換Activity的context
2:使用弱引用,GC會來決定引用的對象何時回收並將對象從內存中移除
3:手動置空,解除引用關係
4:代碼控制異步線程的生命週期,及時cancel
5:將內部類設置爲static,因爲非靜態內部類會隱式持有外部類實例的引用
6:註冊和取消註冊成對出現,在對象合適的生命週期進行監聽的註銷
7:資源性對象(比如Cursor、File等)往往都做了一些緩衝,在不使用時應該及時關閉

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章