最近公司C輪融資成功了,移動團隊準備擴大一下,須要招聘Android開發工程師,陸陸續續面試了幾位Android應聘者,面試過程當中聊到性能優化中如何避免內存泄漏問題時,不多有人全面的回答上來。因此決定抽空學習總結一下這方面的知識,以及分享一下咱們是如何檢測內存泄漏的。咱們公司使用開源框架LeakCanary來檢測內存泄漏。html
有些對象只有有限的生命週期。當它們的任務完成以後,它們將被垃圾回收。若是在對象的生命週期本該結束的時候,這個對象還被一系列的引用,這就會致使內存泄漏。隨着泄漏的累積,app將消耗完內存。java
它是形成應用程序OOM的主要緣由之一。因爲android系統爲每一個應用程序分配的內存有限,當一個應用中產生的內存泄漏比較多時,就不免會致使應用所須要的內存超過這個系統分配的內存限額,這就形成了內存溢出而致使應用Crash。android
leakCanary是Square開源框架,是一個Android和Java的內存泄露檢測庫,若是檢測到某個 activity 有內存泄露,LeakCanary 就是自動地顯示一個通知,因此能夠把它理解爲傻瓜式的內存泄露檢測工具。經過它能夠大幅度減小開發中遇到的oom問題,大大提升APP的質量。web
本篇不介紹如何使用LeakCanary,關於LeakCanary使用參考如下文章:面試
1.LeakCanary: 讓內存泄露無所遁形
2.LeakCanary 中文使用說明設計模式
在平時開發中單例設計模式是咱們常用的一種設計模式,而在開發中單例常常須要持有Context對象,若是持有的Context對象生命週期與單例生命週期更短時,或致使Context沒法被釋放回收,則有可能形成內存泄漏,錯誤寫法以下:性能優化
public class LoginManager { private static LoginManager mInstance; private Context mContext; private LoginManager(Context context) { this.mContext = context; } public static LoginManager getInstance(Context context) { if (mInstance == null) { synchronized (LoginManager.class) { if (mInstance == null) { mInstance = new LoginManager(context); } } } return mInstance; } public void dealData() { } }
弱咱們在一個Activity中調用的,而後關閉該Activity則會出現內存泄漏。app
LoginManager.getInstance(this).dealData();
LeakCanary檢測結果以下:框架
解決 辦法要保證Context和AppLication的生命週期同樣,修改後代碼以下:異步
public class LoginManager { private static LoginManager mInstance; private Context mContext; private LoginManager(Context context) { this.mContext = context.getApplicationContext(); } public static LoginManager getInstance(Context context) { if (mInstance == null) { synchronized (LoginManager.class) { if (mInstance == null) { mInstance = new LoginManager(context); } } } return mInstance; } public void dealData() { } }
早些年Handler的使用頻率仍是蠻高的,它是工做線程與UI線程之間通信的橋樑,只是如今大量開源框架對其進行了封裝,咱們這裏模擬一種常見使用方式來模擬內存泄漏情形。
public class MainActivity extends AppCompatActivity { private Handler mHandler = new Handler(); private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text);//模擬內存泄露 mHandler.postDelayed(new Runnable() { @Override public void run() { mTextView.setText("lcj"); } }, 3 * 60 * 1000); finish(); } @Override protected void onDestroy() { super.onDestroy(); LApplication.getRefWatcher().watch(this); } }
上述代碼經過內部類的方式建立mHandler對象,此時mHandler會隱式地持有一個外部類對象引用這裏就是MainActivity,當執行postDelayed方法時,該方法會將你的Handler裝入一個Message,並把這條Message推到MessageQueue中,MessageQueue是在一個Looper線程中不斷輪詢處理消息,那麼當這個Activity退出時消息隊列中還有未處理的消息或者正在處理消息,而消息隊列中的Message持有mHandler實例的引用,mHandler又持有Activity的引用,因此致使該Activity的內存資源沒法及時回收,引起內存泄漏。
LeakCanary檢測結果以下:
要想避免Handler引發內存泄漏問題,須要咱們在Activity關閉退出的時候的移除消息隊列中全部消息和全部的Runnable。上述代碼只需在onDestroy()函數中調用mHandler.removeCallbacksAndMessages(null);就好了。
public class MainActivity1 extends AppCompatActivity { private Handler mHandler = new Handler(); private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text); //模擬內存泄露 mHandler.postDelayed(new Runnable() { @Override public void run() { mTextView.setText("lcj"); } }, 3 * 60 * 1000); finish(); } @Override protected void onDestroy() { super.onDestroy(); mHandler.removeCallbacksAndMessages(null);
mHandler=null; LApplication.getRefWatcher().watch(this); } }
最先時期的時候處理耗時操做多數都是採用Thread+Handler的方式,後來逐步被AsyncTask取代,直到如今採用RxJava的方式來處理異步。這裏以AsyncTask爲例,可能大部分人都會這樣處理一個耗時操做而後通知UI更新結果:
public class MainActivity extends AppCompatActivity { private AsyncTask<Void, Void, Integer> asyncTask; private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text); testAsyncTask(); finish(); } private void testAsyncTask() { asyncTask = new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { int i = 0; //模擬耗時操做 while (!isCancelled()) { i++; if (i > 1000000000) { break; } Log.e("LeakCanary", "asyncTask---->" + i); } return i; } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mTextView.setText(String.valueOf(integer)); } }; asyncTask.execute(); } @Override protected void onDestroy() { super.onDestroy(); LApplication.getRefWatcher().watch(this); } }
對於上面的例子來講,在處理一個比較耗時的操做時,可能還沒處理結束MainActivity就執行了退出操做,可是此時AsyncTask依然持有對MainActivity的引用就會致使MainActivity沒法釋放回收引起內存泄漏。
LeakCanary檢測結果:
如何解決這種內存泄漏呢?在使用AsyncTask時,在Activity銷燬時候也應該取消相應的任務AsyncTask.cancel()方法,避免任務在後臺執行浪費資源,進而避免內存泄漏的發生。
public class MainActivity3 extends AppCompatActivity { private AsyncTask<Void, Void, Integer> asyncTask; private TextView mTextView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mTextView = (TextView) findViewById(R.id.text); testAsyncTask(); finish(); } private void testAsyncTask() { asyncTask = new AsyncTask<Void, Void, Integer>() { @Override protected Integer doInBackground(Void... params) { int i = 0; //模擬耗時操做 while (!isCancelled()) { i++; if (i > 1000000000) { break; } Log.e("LeakCanary", "asyncTask---->" + i); } return i; } @Override protected void onPostExecute(Integer integer) { super.onPostExecute(integer); mTextView.setText(String.valueOf(integer)); } }; asyncTask.execute(); } private void destroyAsyncTask() { if (asyncTask != null && !asyncTask.isCancelled()) { asyncTask.cancel(true); } asyncTask = null; } @Override protected void onDestroy() { super.onDestroy(); destroyAsyncTask(); LApplication.getRefWatcher().watch(this); } }
有時咱們須要一個能夠隨着屏幕旋轉的Activity,好比視頻播放Activity,這時咱們爲了防止屢次調用onCreate方法致使某些參數從新初始化,咱們通常會選擇建立一個內部類和一個靜態實例來保存這些參數,好比如下實現:
public class MainActivity extends AppCompatActivity { private static Config mConfig; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //模擬內存泄露 if (mConfig == null) { mConfig = new Config(); mConfig.setSize(18); mConfig.setTitle("老九門"); } finish(); } @Override protected void onDestroy() { super.onDestroy(); LApplication.getRefWatcher().watch(this); } class Config { private int size; private String title; public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } }
上述代碼看着沒有任何問題,其實內部類都會持有一個外部類引用,這裏這個外部類就是MainActivity,然而內部類實例又是static靜態變量其生命週期與Application生命週期同樣,因此在MainActivity關閉的時候,內部類靜態實例依然持有對MainActivity的引用,致使MainActivity沒法被回收釋放,引起內存泄漏。LeakCanary檢測內存泄漏結果以下:
對於這種泄漏的解決辦法就是將內部類改爲靜態內部類,再也不持有對MainActivity的引用便可,修改後的代碼以下:
public class MainActivity extends AppCompatActivity { private static Config mConfig; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //模擬內存泄露 if (mConfig == null) { mConfig = new Config(); mConfig.setSize(18); mConfig.setTitle("老九門"); } finish(); } @Override protected void onDestroy() { super.onDestroy(); LApplication.getRefWatcher().watch(this); } static class Config { private int size; private String title; public int getSize() { return size; } public void setSize(int size) { this.size = size; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } } }
在目前的開發中多多少少會用到Hybrid開發方式,這樣咱們就會用WebView去承載Html網頁,就以下面這種方式:
java代碼:
public class MainActivity5 extends AppCompatActivity { private WebView mWebView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web); mWebView = (WebView) findViewById(R.id.web); mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html"); } @Override protected void onDestroy() { super.onDestroy(); LApplication.getRefWatcher().watch(this); } }
xml佈局文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/activity_main" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <WebView android:id="@+id/web" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </LinearLayout>
WebView解析網頁時會申請Native堆內存用於保存頁面元素,當頁面較複雜時會有很大的內存佔用。若是頁面包含圖片,內存佔用會更嚴重。而且打開新頁面時,爲了能快速回退,以前頁面佔用的內存也不會釋放。有時瀏覽十幾個網頁,都會佔用幾百兆的內存。這樣加載網頁較多時,會致使系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啓。及時Activity關閉時在onDestroy中調用以下代碼也是沒有任何做用。
private void destroyWebView() { if (mWebView != null) { mLinearLayout.removeView(mWebView); mWebView.pauseTimers(); mWebView.removeAllViews(); mWebView.destroy(); mWebView = null; } }
先看下LeakCanary檢測到的結果以下:
該如何解決呢?這個查了很多資料,其中一種就是使用getApplicationgContext做爲參數構建WebView,而後動態添加到一個ViewGroup中,最後退出的時候調用webView的銷燬的函數,雖然也達到了防止內存溢出的效果,可是在有些網頁彈出時候須要記住密碼的對話框的時候,會出現Unable to add window -- token null is not for an application 的錯誤,因此這裏採用的解決辦法是經過把使用了WebView的Activity(或者Service)放在單獨的進程裏。而後在檢測到應用佔用內存過大有可能被系統幹掉或者它所在的Activity(或者Service)結束後,調用android.os.Process.killProcess(android.os.Process.myPid());
,主動Kill掉進程。因爲系統的內存分配是以進程爲準的,進程關閉後,系統會自動回收全部內存。
修改後的代碼以下:
public class MainActivity5 extends AppCompatActivity { private WebView mWebView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_web); mWebView = (WebView) findViewById(R.id.web); mWebView.loadUrl("http://www.cnblogs.com/whoislcj/p/5720202.html"); } @Override protected void onDestroy() { destroyWebView(); android.os.Process.killProcess(android.os.Process.myPid()); super.onDestroy(); LApplication.getRefWatcher().watch(this); } private void destroyWebView() { if (mWebView != null) { mWebView.pauseTimers(); mWebView.removeAllViews(); mWebView.destroy(); mWebView = null; } } }
manifest中對應的activity配置以下:
<activity android:name=".MainActivity5" android:process="com.whoislcj.webview"/>
對於使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等資源的使用,應該在Activity銷燬時及時關閉或者註銷,不然這些資源將不會被回收,形成內存泄漏。例如獲取媒體庫圖片地址代碼在查詢結束的時候必定要調用
Cursor 的關閉方法防止形成內存泄漏。
String columns[] = new String[]{ MediaStore.Images.Media.DATA, MediaStore.Images.Media._ID, MediaStore.Images.Media.TITLE, MediaStore.Images.Media.DISPLAY_NAME }; Cursor cursor = this.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, null, null, null); if (cursor != null) { int photoIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); //顯示每張圖片的地址,可是首先要判斷一下,Cursor是否有值 while (cursor.moveToNext()) { String photoPath = cursor.getString(photoIndex); //這裏獲取到的就是圖片存儲的位置信息 Log.e("LeakCanary", "photoPath---->" + photoPath); } cursor.close(); }
以上就是經過LeakCanary檢測到的內存泄漏狀況以及解決辦法。