Android開發常見的Activity中內存泄漏及解決辦法


上一篇文章樓主提到由Context引發的內存泄漏,在這一篇文章里,我們來談談Android開發中常見的Activity內存泄漏及解決辦法。本文將會以“為什么”“怎么解決”的方式來介紹這幾種內存泄漏。
在開篇之前,先來了解一下什么是內存泄漏。

什么是內存泄漏?

內存泄漏是當程序不再使用到的內存時,釋放內存失敗而產生了無用的內存消耗。內存泄漏並不是指物理上的內存消失,這里的內存泄漏是值由程序分配的內存但是由於程序邏輯錯誤而導致程序失去了對該內存的控制,使得內存浪費。

怎樣會導致內存泄漏?

  • 資源對象沒關閉造成的內存泄漏,如查詢數據庫后沒有關閉游標cursor
  • 構造Adapter時,沒有使用 convertView 重用
  • Bitmap對象不在使用時調用recycle()釋放內存
  • 對象被生命周期長的對象引用,如activity被靜態集合引用導致activity不能釋放

在接下來的篇幅里,我們重點講有關Activity常見的內存泄漏。

內存泄漏1:靜態Activities(static Activities)

代碼如下:
MainActivity.Java

public class MainActivity extends AppCompatActivity { private static MainActivity activity; TextView saButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); saButton = (TextView) findViewById(R.id.text); saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticActivity(); nextActivity(); } }); } void setStaticActivity() { activity = this; } void nextActivity(){ startActivity(new Intent(this,RegisterActivity.class)); SystemClock.sleep(1000); finish(); } @Override protected void onDestroy() { super.onDestroy(); //使用LeakCanary觀察是否有內存泄漏 MyApplication.getRefWatcher().watch(this); } }

LeakCanary檢測出的內存泄漏:

這里寫圖片描述

為什么?
在上面代碼中,我們聲明了一個靜態的Activity變量並且在TextView的OnClick事件里引用了當前正在運行的Activity實例,所以如果在activity的生命周期結束之前沒有清除這個引用,則會引起內存泄漏。因為聲明的activity是靜態的,會常駐內存,如果該對象不清除,則垃圾回收器無法回收變量。

怎么解決?
最簡單的方法是在onDestory方法中將靜態變量activity置空,這樣垃圾回收器就可以將靜態變量回收。

@Override protected void onDestroy() { super.onDestroy(); activity = null; //使用LeakCanary觀察是否有內存泄漏 MyApplication.getRefWatcher().watch(this); }

內存泄漏2:靜態View

代碼如下:
MainActivity.java

    ...
    private static View view; TextView saButton; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); saButton = (TextView) findViewById(R.id.text); saButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { setStaticView(); nextActivity(); } }); } void setStaticView() { view = findViewById(R.id.sv_view); } ...

LeakCanary檢測到的內存泄漏

這里寫圖片描述

為什么?
上面代碼看似沒有問題,在Activity里聲明一個靜態變量view,然后初始化,當Activity生命周期結束了內存也釋放了,但是LeakCanary卻顯示出現了內存泄漏,為什么?問題出在這里,View一旦被加載到界面中將會持有一個Context對象的引用,在這個例子中,這個context對象是我們的Activity,聲明一個靜態變量引用這個View,也就引用了activity,所以當activity生命周期結束了,靜態View沒有清除掉,還持有activity的引用,因此內存泄漏了。

怎么解決?
在onDestroy方法里將靜態變量置空。

@Override protected void onDestroy() { super.onDestroy(); view = null; MyApplication.getRefWatcher().watch(this); } 

內存泄漏3:內部類

代碼如下:
MainActivity.java

private static Object inner; void createInnerClass() { class InnerClass { } inner = new InnerClass(); } View icButton = findViewById(R.id.ic_button); icButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createInnerClass(); nextActivity(); } }); 

使用LeakCanary檢測到的內存泄漏:

這里寫圖片描述

為什么?
非靜態內部類會持有外部類的引用,在上面代碼中內部類持有Activity的引用,因此inner會一直持有Activity,如果Activity生命周期結束沒有清除這個引用,這樣就發生了內存泄漏。

怎么解決?
因為非靜態內部類隱式持有外部類的強引用,所以我們將內部類聲明成靜態的就可以了。

void createInnerClass() { static class InnerClass { } inner = new InnerClass(); }

內存泄漏4:匿名類

void startAsyncTask() { new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... params) { while(true); } }.execute(); } super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); View aicButton = findViewById(R.id.at_button); aicButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { startAsyncTask(); nextActivity(); } });

使用LeakCanary檢測到的內存泄漏:

這里寫圖片描述

為什么?
上面代碼在activity中創建了一個匿名類AsyncTask,匿名類和非靜態內部類相同,會持有外部類對象,這里也就是activity,因此如果你在Activity里聲明且實例化一個匿名的AsyncTask對象,則可能會發生內存泄漏,如果這個線程在Activity銷毀后還一直在后台執行,那這個線程會繼續持有這個Activity的引用從而不會被GC回收,直到線程執行完成。

怎么解決?
自定義靜態AsyncTask類,並且讓AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期結束時要將AsyncTask cancel掉。

內存泄漏5:Handler

代碼如下:
MainActivity.java

...
void createHandler() { new Handler() { @Override public void handleMessage(Message message) { super.handleMessage(message); } }.postDelayed(new Runnable() { @Override public void run() { while(true); } }, 1000); } ... View hButton = findViewById(R.id.h_button); hButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { createHandler(); nextActivity(); } }); ...

為什么?
當Android Application啟動以后,framework會首先幫助我們完成UI線程的消息循環,也就是在UI線程中,Loop、MessageQueue、Message等等這些實例已經由framework幫我們實現了。所有的Application主要事件,比如Activity的生命周期方法、Button的點擊事件都包含在這個Message里面,這些Message都會加入到MessageQueue中去,所以,UI線程的消息循環貫穿於整個Application生命周期,所以當你在UI線程中生成Handler的實例,就會持有Loop以及MessageQueue的引用。並且在Java中非靜態內部類和匿名內持有外部類的引用,而靜態內部類則不會持有外部類的引用。

怎么解決?
可以由上面的結論看出,產生泄漏的根源在於匿名類持有Activity的引用,因此可以自定義Handler和Runnable類並聲明成靜態的內部類,來解除和Activity的引用。

內存泄漏6:Thread

代碼如下:
MainActivity.java

void spawnThread() { new Thread() { @Override public void run() { while(true); } }.start(); } View tButton = findViewById(R.id.t_button); tButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { spawnThread(); nextActivity(); } }); 

為什么?
同AsyncTask一樣,這里就不過多贅述。

怎么解決?
那我們自定義Thread並聲明成static這樣可以嗎?其實這樣的做法並不推薦,因為Thread位於GC根部,DVM會和所有的活動線程保持hard references關系,所以運行中的Thread絕不會被GC無端回收了,所以正確的解決辦法是在自定義靜態內部類的基礎上給線程加上取消機制,因此我們可以在Activity的onDestroy方法中將thread關閉掉。

內存泄漏7:Timer Tasks

代碼如下:
MainActivity.java

void scheduleTimer() { new Timer().schedule(new TimerTask() { @Override public void run() { while(true); } },1000); } View ttButton = findViewById(R.id.tt_button); ttButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { scheduleTimer(); nextActivity(); } });

為什么?
這里內存泄漏在於Timer和TimerTask沒有進行Cancel,從而導致Timer和TimerTask一直引用外部類Activity。

怎么解決?
在適當的時機進行Cancel。

內存泄漏8:Sensor Manager

代碼如下:
MainActivity.java

void registerListener() { SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE); Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL); sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST); } View smButton = findViewById(R.id.sm_button); smButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { registerListener(); nextActivity(); } }); 

為什么?
通過Context調用getSystemService獲取系統服務,這些服務運行在他們自己的進程執行一系列后台工作或者提供和硬件交互的接口,如果Context對象需要在一個Service內部事件發生時隨時收到通知,則需要把自己作為一個監聽器注冊進去,這樣服務就會持有一個Activity,如果開發者忘記了在Activity被銷毀前注銷這個監聽器,這樣就導致內存泄漏。

怎么解決?
在onDestroy方法里注銷監聽器。

總結

在開發中,內存泄漏最壞的情況是app耗盡內存導致崩潰,但是往往真實情況不是這樣的,相反它只會耗盡大量內存但不至於閃退,可分配的內存少了,GC便會更多的工作釋放內存,GC是非常耗時的操作,因此會使得頁面卡頓。我們在開發中一定要注意當在Activity里實例化一個對象時看看是否有潛在的內存泄漏,一定要經常對內存泄漏進行檢測。

題外

在本篇中,樓主用LeakCanary來對內存泄漏進行檢測。LeakCanary是非常好用的第三方庫用來進行內存泄漏檢測,感興趣的朋友可以去查閱LeakCanary使用方法,使用它來監測App中的內存泄漏。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM