Android學習系列(36)--App除錯記憶體洩露之Context篇(上)

謙虛的天下發表於2014-04-09

Context作為最基本的上下文,承載著Activity,Service等最基本元件。當有物件引用到Activity,並不能被回收釋放,必將造成大範圍的物件無法被回收釋放,進而造成記憶體洩漏。

下面針對一些常用場景逐一分析。

1. CallBack物件的引用

    先看一段程式碼:

@Override
protectedvoid onCreate(Bundle state){
  super.onCreate(state);
  
  TextView label =new TextView(this);
  label.setText("Leaks are bad");
  
  setContentView(label);
}

    大家看看有什麼問題嗎?

    沒問題是吧,繼續看:

private static Drawable sBackground;
  
@Override
protected void onCreate(Bundle state){
  super.onCreate(state);
  
  TextView label =new TextView(this);
  label.setText("Leaks are bad");
  
  if(sBackground ==null){
    sBackground = getDrawable(R.drawable.large_bitmap);
  }
  label.setBackgroundDrawable(sBackground);
  
  setContentView(label);
}

    有問題嗎?

    哈哈,先Hold住一下,先來說一下android各版本釋出的歷史:

/*
2.2        2010-3-20,Froyo 
2.3        2010-12-6, Gingerbread
3.0        2011-2-22, Honeycomb
4.0        2011-10-11 Ice Cream Sandwich
*/

    瞭解原始碼的歷史,是很有益於我們分析android程式碼的。

    好,開始分析程式碼。

    首先,檢視setBackgroundDrawable(Drawable background)方法原始碼裡面有一行程式碼引起我們的注意:

public void setBackgroundDrawable(Drawable background) {
    // ... ...
    background.setCallback(this);
    // ... ...
}

    所以sBackground對view保持了一個引用,view對activity保持了一個引用。

    當退出當前Activity時,當前Activity本該釋放,但是因為sBackground是靜態變數,它的生命週期並沒有結束,而sBackground間接保持對Activity的引用,導致當前Activity物件不能被釋放,進而導致記憶體洩露。

    所以結論是:有記憶體洩露!

    這是Android官方文件的例子:http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

    到此結束了嗎?

    我發現網上太多直接抄或者間接抄這篇文章,一搜一大片,並且吸引了大量的Android初學者不斷的轉載學習。

    但是經過本人深入分析Drawable原始碼,事情發生了一些變化。

    Android官方文件的這篇文章是寫於2009年1月的,當時的Android Source至少是Froyo之前的。

    Froyo的Drawable的setCallback()方法的實現是這樣的:

public final void setCallback(Callback cb) {
        mCallback = cb;
}

    在GingerBread的程式碼還是如此的。

    但是當進入HoneyComb,也就是3.0之後的程式碼我們發現Drawable的setCallback()方法的實現變成了:

public final void setCallback(Callback cb) {
        mCallback = new WeakReference<Callback>(cb);
}

    也就是說3.0之後,Drawable使用了軟引用,把這個洩露的例子問題修復了。(至於軟引用怎麼解決了以後有機會再分析吧)

    所以最終結論是,在android3.0之前是有記憶體洩露,在3.0之後無記憶體洩露!

    如果認真比較程式碼的話,Android3.0前後的程式碼改進了大量類似程式碼,前面的Cursor篇裡的例子也是在3.0之後修復了。

    從這個例子中,我們很好的發現了記憶體是怎麼通過回撥洩露的,同時通過官方程式碼的update也瞭解到了怎麼修復類似的記憶體洩露。

 

2. System Service物件

    通過各種系統服務,我們能夠做一些系統設計好的底層功能:

//ContextImpl.java
@Override
public Object getSystemService(String name) {
    ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
    return fetcher == null ? null : fetcher.getService(this);
}

static {
    registerService(ACCESSIBILITY_SERVICE, new ServiceFetcher() {
            public Object getService(ContextImpl ctx) {
            return AccessibilityManager.getInstance(ctx);
            }}); 

    registerService(CAPTIONING_SERVICE, new ServiceFetcher() {
            public Object getService(ContextImpl ctx) {
            return new CaptioningManager(ctx);
            }}); 

    registerService(ACCOUNT_SERVICE, new ServiceFetcher() {
            public Object createService(ContextImpl ctx) {
            IBinder b = ServiceManager.getService(ACCOUNT_SERVICE);
            IAccountManager service = IAccountManager.Stub.asInterface(b);
            return new AccountManager(ctx, service);
            }});
    // ... ...
}

  這些其實就是定義在Context裡的,按理說這些都是系統的服務,應該都沒問題,但是程式碼到了各家廠商一改,事情發生了一些變化。

      一些廠商定義的服務,或者廠商自己修改了一些新的程式碼導致系統服務引用了Context物件不能及時釋放,我曾經碰到過Wifi,Storage服務都有記憶體洩露。

     我們改不了這些系統級應用,我們只能修改自己的應用。

     解決方案就是:使用ApplicationContext代替Context。

     舉個例子吧:

// For example
mStorageManager = (StorageManager) getSystemService(Context.STORAGE_SERVICE);
改成:
mStorageManager = (StorageManager) getApplicationContext().getSystemService(Context.STORAGE_SERVICE);

 

3. Handler物件

    先看一段程式碼:

public class MainActivity extends QActivity {
        // lint tip: This Handler class should be static or leaks might occur 
	class MyHandler extends Handler {
	    ... ...
	}
}

    Handler洩露的關鍵點有兩個:

    1). 內部類

    2). 生命週期和Activity不一定一致

    第一點,Handler使用的比較多,經常需要在Activity中建立內部類,所以這種場景還是很多的。

    內部類持有外部類Activity的引用,當Handler物件有Message在排隊,則無法釋放,進而導致Activity物件不能釋放。

    如果是宣告為static,則該內部類不持有外部Acitivity的引用,則不會阻塞Activity物件的釋放。

    如果宣告為static後,可在其內部宣告一個弱引用(WeakReference)引用外部類。

public class MainActivity extends Activity {
	private CustomHandler mHandler;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		mHandler = new CustomHandler(this);
	}

	static class CustomHandlerextends Handler {
		// 內部宣告一個弱引用,引用外部類
		private WeakReference<MainActivity > activityWeakReference;
		public MyHandler(MyActivity activity) {
			activityWeakReference= new WeakReference<MainActivity >(activity);
		}
                // ... ...    
	}
}

    第二點,其實不單指內部類,而是所有Handler物件,如何解決上面說的Handler物件有Message在排隊,而不阻塞Activity物件釋放?

    解決方案也很簡單,在Activity onStop或者onDestroy的時候,取消掉該Handler物件的Message和Runnable。

    通過檢視Handler的API,它有幾個方法:removeCallbacks(Runnable r)和removeMessages(int what)等。

    // 一切都是為了不要讓mHandler拖泥帶水
    @Override
    public void onDestroy() {
        mHandler.removeMessages(MESSAGE_1);
        mHandler.removeMessages(MESSAGE_2);
        mHandler.removeMessages(MESSAGE_3);
        mHandler.removeMessages(MESSAGE_4);

        // ... ... 

        mHandler.removeCallbacks(mRunnable);

        // ... ...
    }

    上面的程式碼太長?好吧,出大招:

    @Override
    public void onDestroy() {
        //  If null, all callbacks and messages will be removed.
        mHandler.removeCallbacksAndMessages(null);
    }

    有人會問,當Activity退出的時候,我還有好多事情要做,怎麼辦?我想一定有辦法的,比如用Service等等.

 

4. Thread物件

    同Handler物件可能造成記憶體洩露的原理一樣,Thread的生命週期不一定是和Activity生命週期一致。

    而且因為Thread主要面向多工,往往會造成大量的Thread例項。

    據此,Thread物件有2個需要注意的洩漏點:

    1). 建立過多的Thread物件

    2). Thread物件在Activity退出後依然在後臺執行

    解決方案是:

    1). 使用ThreadPoolExecutor,在同時做很多非同步事件的時候是很常用的,這個不細說。

    2). 當Activity退出的時候,退出Thread

    第一點,例子太多,建議大家參考一下afinal中AsyncTask的實現學習。

    第二點,如何正常退出Thread,我在之前的博文中也提到過。示例程式碼如下:

    // ref http://docs.oracle.com/javase/1.5.0/docs/guide/misc/threadPrimitiveDeprecation.html
    private volatile Thread blinker;

    public void stop() {
        blinker = null;
    }

    public void run() {
        Thread thisThread = Thread.currentThread();
        while (blinker == thisThread) {
            try {
                thisThread.sleep(interval);
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

    有人會問,當Activity退出的時候,我還有好多事情要做,怎麼辦?請看上面Handler的分析最後一行。

    (未完待續)

相關文章