Android記憶體優化(三)避免可控的記憶體洩漏

劉望舒發表於2017-06-26

相關文章
Android效能優化系列
Java虛擬機器系列

前言

記憶體洩漏向來都是記憶體優化的重點,它如同幽靈一般存於我們的應用當中,有時它不會現身,但一旦現身就會讓你頭疼不已。因此,如何避免、發現和解決記憶體洩漏就變得尤為重要。這一篇我們先來學習如何避免記憶體洩漏。

1.什麼是記憶體洩漏

我們知道,每個應用程式都需要記憶體來完成工作,為了確保Android系統的每個應用都有足夠的記憶體,Android系統需要有效地管理記憶體分配。當記憶體不足時,Android執行時就會觸發GC,GC採用的垃圾標記演算法為根搜尋演算法,
Java虛擬機器(三)垃圾標記演算法與Java物件的生命週期這篇文章中講到了根搜尋演算法,如下圖所示。

未命名檔案.png
未命名檔案.png

從上圖看以看出,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相關原創技術乾貨。
掃一掃下方二維碼或者長按識別二維碼,即可關注。

相關文章