1. 程式人生 > >Android記憶體優化全解

Android記憶體優化全解

要學習Android的記憶體優化,首先要了解Java虛擬機器。Android基於的是Dalvik虛擬機器,簡稱DVM,與Java虛擬機器JVM並不一樣。另外,Android 4.4以後基於的是ART虛擬機器;

1、DVM:DVM是基於暫存器的,它沒有基於棧的虛擬機器在拷貝資料而使用的大量的出入棧指令,同時指令更緊湊更簡潔。執行的位元組碼是.dex形式的,DVM會用dx工具將所有的.class檔案轉換為一個.dex檔案,然後DVM會從該.dex檔案讀取指令和資料。執行順序為:.java檔案 –>.class檔案-> .dex檔案

2、JVM:基於棧讀寫資料,執行順序為: .java檔案 -> .class檔案 -> .jar檔案

3、ART:Android在4.4時候推出ART,在5.0以後正式採用ART的,在ART中,系統在安裝應用時會進行一次預編譯(AOT,ahead of time),將位元組碼預先編譯成機器碼並存儲在本地,這樣應用每次執行時就不需要執行編譯了,執行效率也大大提升。

記憶體洩漏向來是記憶體優化的重點,因此在Android開發中,如何避免、發現和解決記憶體洩漏就變得尤為重要。

首先,我們應該避免可控的記憶體洩漏:

從上圖看以看出,Obj4是可達的物件,表示它正被引用,因此不會標記為可回收的物件。Obj5、Obj6和Obj7都是不可達的物件,其中Obj5和Obj6雖然互相引用,但是因為他們到GC Roots是不可達的所以它們仍舊會標記為可回收的物件。

記憶體洩漏產生的原因,主要分為三大類:
1.由開發人員自己編碼造成的洩漏。
2.第三方框架造成的洩漏。
3.由Android 系統或者第三方ROM造成的洩漏。

而往往第二、第三種記憶體洩漏不可控,那麼我們應該首先做到在編碼中儘量減少由於自身原因造成的洩漏

記憶體洩漏的幾大場景

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 匿名內部類的靜態例項

和前面的非靜態內部類一樣,匿名內部類也會持有外部類例項的引用。

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,這裡就不再舉例。

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

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

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

6 WebView

不同的Android版本的WebView會有差異,加上不同廠商的定製ROM的WebView的差異,這就導致WebView存在著很大的相容性問題。WebView都會存在記憶體洩漏的問題,在應用中只要使用一次WebView,記憶體就不會被釋放掉。通常的解決辦法就是為WebView單開一個程序,使用AIDL與應用的主程序進行通訊。WebView程序可以根據業務需求,在合適的時機進行銷燬。

7 資源物件未關閉

資源物件比如Cursor、File等,往往都用了緩衝,不使用的時候應該關閉它們。把他們的引用置為null,而不關閉它們,往往會造成記憶體洩漏。因此,在資源物件不使用時,一定要確保它已經關閉,通常在finally語句中關閉,防止出現異常時,資源未被釋放的問題。

8 集合中物件沒清理

通常把一些物件的引用加入到了集合中,當不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,那情況就會更加嚴重。

9 Bitmap物件

臨時建立的某個相對比較大的bitmap物件,在經過變換得到新的bitmap物件之後,應該儘快回收原始的bitmap,這樣能夠更快釋放原始bitmap所佔用的空間。
避免靜態變數持有比較大的bitmap物件或者其他大的資料物件,如果已經持有,要儘快置空該靜態變數。

10 監聽器未關閉

很多系統服務(比如TelephonyMannager、SensorManager)需要register和unregister監聽器,我們需要確保在合適的時候及時unregister那些監聽器。自己手動add的Listener,要記得在合適的時候及時remove這個Listener。

接下來,就是使用記憶體分析工具對程式碼中可能存在的記憶體洩漏進行分析了。我們常用的記憶體洩漏分析工具有LeakCanary、MAT等等。我主要在專案中使用LeakCanary較多,LeakCanary的使用方法也簡單,具體如下:

首先配置build.gradle:

dependencies { debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.2' releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.2'}

接下來在Application的onCreate()方法中加入如下程式碼。

if (LeakCanary.isInAnalyzerProcess(this)) {//1 // This process is dedicated to LeakCanary for heap analysis. // You should not init your app in this process. return; } LeakCanary.install(this);但是以上程式碼僅僅能檢測activity的記憶體洩漏,因此改寫上面的方法,使用RefWatcher來監控檢測其他類的洩漏 private RefWatcher refWatcher; @Override public void onCreate() { super.onCreate(); refWatcher= setupLeakCanary();//在oncreate中初始化並設定RefWatcher } private RefWatcher setupLeakCanary() { if (LeakCanary.isInAnalyzerProcess(this)) { return RefWatcher.DISABLED; } return LeakCanary.install(this); } public static RefWatcher getRefWatcher(Context context) { LeakApplication leakApplication = (LeakApplication) context.getApplicationContext(); return leakApplication.refWatcher; }

install方法會返回RefWatcher用來監控物件,LeakApplication中還要提供getRefWatcher靜態方法來返回全域性RefWatcher。
最後為了舉例,我們在一段存在記憶體洩漏的程式碼中引入LeakCanary監控,如下所示。

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); LeakThread leakThread = new LeakThread(); leakThread.start(); } class LeakThread extends Thread { @Override public void run() { try { Thread.sleep(6 * 60 * 1000); } catch (InterruptedException e) { e.printStackTrace(); } } } @Override protected void onDestroy() { super.onDestroy(); RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1 refWatcher.watch(this); }}

MainActivity存在記憶體洩漏,原因就是非靜態內部類LeakThread持有外部類MainActivity的引用,LeakThread中做了耗時操作,導致MainActivity無法被釋放。
在註釋1處得到RefWatcher,並呼叫它的watch方法,watch方法的引數就是要監控的物件。當然,在這個例子中onDestroy方法是多餘的,因為LeakCanary在呼叫install方法時會啟動一個ActivityRefWatcher類,它用於自動監控Activity執行onDestroy方法之後是否發生記憶體洩露。這裡只是為了方便舉例,如果想要監控Fragment,在Fragment中新增如上的onDestroy方法是有用的。
執行程式,這時會在介面生成一個名為Leaks的應用圖示。接下來不斷的切換橫豎屏,這時會閃出一個提示框,提示內容為:“Dumping memory app will freeze.Brrrr.”。再稍等片刻,記憶體洩漏資訊就會通過Notification展示出來,比如三星S8的通知欄如下所示。

Notification中提示了MainActivity發生了記憶體洩漏, 洩漏的記憶體為787B。點選Notification就可以進入記憶體洩漏詳細頁,除此之外也可以通過Leaks應用的列表介面進入,列表介面如下圖所示。

記憶體洩漏詳細頁如下圖所示。

點選加號就可以檢視具體類所在的包名稱。整個詳情就是一個引用鏈:MainActiviy的內部類LeakThread引用了LeakThread的this$0this$0的含義就是內部類自動保留的一個指向所在外部類的引用,而這個外部類就是詳情最後一行所給出的MainActiviy的例項,這將會導致MainActivity無法被GC,從而產生記憶體洩漏。


參考連結:

http://blog.csdn.net/itachi85/article/details/77826112

http://blog.csdn.net/itachi85/article/details/73522042