Android應用記憶體洩露分析以及優化方案
本篇部落格是介紹Android記憶體優化方面的知識,在讀本篇部落格之前需要你熟練掌握Java 基礎知識(例如,靜態變數的生命週期,匿名內部類的使用,匿名物件等),並且具有一定的Android應用開發經驗(Android多執行緒程式設計,Android非同步回撥等)。
話不多說,本篇部落格分為三個部分:
一,記憶體洩露的危害。
二,記憶體洩露情景分析。
三,如何優化。
先來談談記憶體洩露的危害。
談到記憶體洩露的危害,筆者第一時間想到了記憶體洩露最終會導致應用記憶體不夠用,畢竟Android系統不會給應用分配無限多的執行記憶體的,所以,記憶體洩露的問題不解決必定會引發一些不可預見的危害。那麼記憶體溢位究竟會帶來哪些危害呢?筆者抽出了打一盤dota的時間總結出兩個方面:
1,記憶體洩露越積越多,最終應用無可用記憶體,記憶體溢位,應用崩潰。
2,一些物件由於常駐在記憶體中,例如Activity,在長時間的停留後,由於系統本身記憶體不足最終被回收,但是其他非同步的任務在返回結果是呼叫了直接引用了該Activity,導致空指標異常。
先談談記憶體溢位情況。由於筆者一時間沒有辦法模擬出一個真實環境的記憶體溢位(懶),所以使用最簡單粗暴的方法,如下程式碼所示:
[html] view plain copyprint?- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- while(true) {
- new Thread().start();
- }
- }
- }
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); while(true) { new Thread().start(); } } }
當執行這段程式碼後,不出意料,應用直接掛了,並且丟擲如下圖所示的錯誤資訊:
這就是我們既熟悉又陌生的記憶體溢位錯誤:OutOfMemoryError(OOM)。記憶體溢位會直接導致應用發生錯誤並結束執行,在沒有任何防護措施的情況下,應用直接停止執行必定會影響到使用者的操作體驗,使用者可能一氣之下就解除安裝了你的應用(事實證明,這個真的會,筆者自己就解除安裝過微信很多次... 無奈還是得用)。一般運氣好的情況下,在除錯階段日誌會清晰告訴你哪裡的程式碼發生了記憶體溢位,但是也僅僅是告訴你程式執行到那幾行程式碼時發生記憶體溢位,這其實並不代表那幾行程式碼是有問題的,想反,可能是程式其他地方已經發生了大量的記憶體洩露,之後程式執行到這裡時,應用記憶體剛好達到了系統分配給你的應用執行記憶體的最大值從而發生記憶體洩露。可是一旦應用安裝到使用者手機上。那麼,迎接你的可能就是一堆bug的修復過程,甚至主管,老闆的一頓批評啊。
再來說第二種情況,記憶體洩露導致空指標的情況。由於這種情況需要太多的時間去模擬,所以筆者借用Android中另一個相似的空指標情況來說明這個問題。不知道讀者在做Android開發的時候有沒有做過呼叫系統相機拍照功能,如果做過的同學應該遇到過,在呼叫系統拍照的時候由於旋轉螢幕,原本拍的照片,照片檔案路徑也正確的獲取到了,但是在使用是,發現儲存照片路徑的字串變數為空了?我的天吶(捂住嘴巴...),太神奇了 !於是上網百度一通發現,原來旋轉螢幕導致Activity重新建立進而導致Activity裡面的變數重新初始化了,於是那個儲存照片路徑的變數為空了,於是同學們針對這個問題,做出了防止螢幕旋轉,或者通過onSaveInstance方法來儲存資料等等。那麼記憶體洩露如何和這個例子相關呢?其實如上面所說,假如某一個Activity發生了記憶體洩露,由於長時間的在後臺執行,當系統記憶體不足時,系統優先殺死後臺執行的Activity,那麼假如其它介面的一個非同步的任務完成後回撥中呼叫了這個Activity內部的成員變數,那麼呼叫一個被殺死的Activity肯定會發生記憶體洩露。同樣的道理,加入開啟系統拍照介面的哪個Activity由於旋轉螢幕被重新建立了(一個全新的Activity),拍照結束時使用這個Activity裡面的變數肯定也會報空指標異常的。感覺這個例子並不是很恰當,但是意思應該達到了,也能說明這個問題,讀者不用太過糾結。
類似的記憶體溢位錯誤還有StackOverFlow(棧溢位),這個錯誤同樣是記憶體溢位,不過一般發生在大量的遞迴過程中,導致棧記憶體變數快速增加。這個錯誤型別讀者可以自己百度一下含義,筆者在這裡就不在多說了。
接著我們分析一些常見的的記憶體洩露場景。
以下分析的情況如不特殊說明,均指Context(Activity)記憶體洩露。
好了,記憶體洩露的危害就說到這裡了,記憶體洩露的問題是迫在眉睫啊,解決了記憶體洩露,就可以有效的避免記憶體溢位和空指標現象。筆者再抽出一盤dota的時間整理並列舉了幾個記憶體洩露的高發情況。其他情況暫時不討論。那麼,常見的記憶體洩露大致可以分為以下兩種情況:
1,在非同步情況下,非靜態內部類物件持有外部類引用引發的記憶體洩露。
2,靜態變數直接或者間接持有非靜態物件引用。
常見的記憶體洩露大部分是Context記憶體洩露,進而導致資源洩漏(本地圖片尤其危險),小部分記憶體洩露就是一下野生的物件了沒有被回收,例如不需要靜態物件的情況下new了太多靜態物件。
下面分析非同步情況下,非靜態內部類物件持有外部類引用引發的記憶體洩露
假設有這樣一個需求:一個Activity內部有個無限輪播圖,每隔3s切換一次圖片,百度一下輪播圖效果會出來很多實現,目前大部分輪播圖效果不外乎使用定時器或者訊息佇列來實現延遲更新操作。筆者這裡採用訊息佇列模擬了一個輪播圖效果(並沒有圖片),程式碼如下:
[html] view plain copyprint?- public class HandleLeakActivity extends AppCompatActivity {
- public static final String TAG = HandleLeakActivity.class.getSimpleName();
- MyHandler handler = null;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_handle_leak);
- handler = new MyHandler();
- }
- class MyHandler extends Handler {
- final int TOTAL = 4;
- int count = 0;
- public MyHandler() {
- startLoop();
- }
- @Override
- public void handleMessage(Message msg) {
- Log.w(TAG, "切換到第 " + count + " 張圖片");
- count = ++count % TOTAL;
- sendEmptyMessageDelayed(0, 2000);
- }
- }
- public void startLoop() {
- handler.sendEmptyMessageDelayed(0, 2000);
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- Log.w(TAG, "退出當前Activity");
- if (handler != null) {
- handler.removeCallbacksAndMessages(null);
- handler = null;
- }
- }
- }
public class HandleLeakActivity extends AppCompatActivity {
public static final String TAG = HandleLeakActivity.class.getSimpleName();
MyHandler handler = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handle_leak);
handler = new MyHandler();
}
class MyHandler extends Handler {
final int TOTAL = 4;
int count = 0;
public MyHandler() {
startLoop();
}
@Override
public void handleMessage(Message msg) {
Log.w(TAG, "切換到第 " + count + " 張圖片");
count = ++count % TOTAL;
sendEmptyMessageDelayed(0, 2000);
}
}
public void startLoop() {
handler.sendEmptyMessageDelayed(0, 2000);
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.w(TAG, "退出當前Activity");
if (handler != null) {
handler.removeCallbacksAndMessages(null);
handler = null;
}
}
}
佈局程式碼:[html] view plain copyprint?- <RelativeLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:tools="http://schemas.android.com/tools"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="@mipmap/img_dota"
- android:paddingBottom="@dimen/activity_vertical_margin"
- android:paddingLeft="@dimen/activity_horizontal_margin"
- android:paddingRight="@dimen/activity_horizontal_margin"
- android:paddingTop="@dimen/activity_vertical_margin"
- tools:context="com.example.memoryoptimization.HandleLeakActivity">
- </RelativeLayout>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@mipmap/img_dota"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.example.memoryoptimization.HandleLeakActivity">
</RelativeLayout>
上面貼出了模擬一個無線輪播的實現。從程式碼中,我們並沒有看到有什麼問題,同時,模擬的輪播效果也正常的執行起來。如下圖所示:
那麼當我們退出這個介面會發生什麼呢?再來看下關閉當前介面的結果:
從我們測試用的程式碼可以看出,介面finish後會列印一句 “退出當前Activity”,同時你也會發現輪播的Log依然在列印,這是不是太奇怪了?我的介面都finish了,輪播圖盡然還再繼續?其實這裡輪播圖依然在繼續輪播,就表明當前這個Activity發生記憶體洩露了,那麼上面幾張圖並沒有清晰的體現記憶體洩露的具體證據,所以筆者再貼出有關這個介面finish前後的記憶體變化對比圖。
介面退出前:
介面退出後:
在介面退出前的截圖上,紅色箭頭所指的是輪播圖所在介面載入完成後記憶體的波動情況,由於筆者為了測試記憶體佔用,在這個介面加了一張背景圖片,所以記憶體有個顯著的提升,但是從介面退出後在觀察記憶體情況,發現記憶體並沒有降低,這個記憶體測試結果是筆者在app靜置了5分鐘後截圖到的,那麼很明顯,這裡的記憶體洩露了。我們都知道,非靜態內部類會自動持有外部類物件的引用,那麼在這個測試程式碼中,MyHandler物件會持有HandleLeakActivity.this的引用,(正式因為這個HandleLeakActivity.this,我們才能順利的呼叫Activity的成員方法而不受約束) ,因此在MyHandler的構造方法中我們可以呼叫外部Activity的startLoop方法開啟輪播模式,由於Handler的訊息佇列本身牽扯到非同步的過程(並不一定是在子執行緒),在多執行緒的情況下,我們要在子執行緒獲取Handler物件來和主執行緒進行通訊,當Activity結束後,非同步任務還在繼續執行(多執行緒情況下亦是如此),Java GC 在回收 Activity物件(這裡Activity只是一個物件,並沒有Android四大元件的概念了)時,發現任然有其他物件引用了(這個Handler物件),所以GC就放棄了對Activity物件的回收,進而引發Activity物件永續性的佔用著記憶體。
下面分析靜態變數引發的記憶體洩露
分析了第一種在非同步情況下,非靜態內部類引發的記憶體洩露後,我們繼續來分析靜態變數引發的記憶體洩露。有人可能壓根沒聽說過?靜態變數會引發記憶體洩露?我可是經常使用靜態變數的,感覺沒什麼啊。這裡筆者要說明一下,這裡的靜態變數值得是可以指向通過new關鍵字建立的物件 變數(字串變數除外)。接下來,我們具體看幾個常見的靜態變數直接或者間接引用非靜態物件導致的記憶體洩露:
第一種情況,也是最簡單的情況:程式碼如下:
[html] view plain copyprint?- public class HandleLeakActivity extends AppCompatActivity {
- public static final String TAG = HandleLeakActivity.class.getSimpleName();
- private static TextView sTextView;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_handle_leak);
- sTextView = new TextView(this);
- sTextView.setText("我是一個TextView");
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- Log.w(TAG, "退出當前Activity");
- }
- }
public class HandleLeakActivity extends AppCompatActivity {
public static final String TAG = HandleLeakActivity.class.getSimpleName();
private static TextView sTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handle_leak);
sTextView = new TextView(this);
sTextView.setText("我是一個TextView");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.w(TAG, "退出當前Activity");
}
}
佈局程式碼依然是上面提供的佈局程式碼....此處就不貼了。
上面程式碼我們在Activity內部聲明瞭一個靜態修飾的TextView變數sTextView,並且在onCreate方法內部new 了一個TextView賦值給它,此時這個sTextView指向了一個非靜態的TextView物件(也就是我們new 出來的這個)。讀者有沒從這段程式碼裡面看出什麼問題?或者說讀者自己就曾經寫過這樣的程式碼呢?不賣關子了,我們直接開啟這個介面,從記憶體波動圖來分析吧,如下圖所示。
退出介面前:
退出介面後:
上面兩圖中,退出介面前的圖中的紅色箭頭依然是我為了測試記憶體波動給佈局加的背景圖片(佈局程式碼中的RelativeLayout的背景圖:img_dota),很明顯這個介面載入進來後,由於圖片的載入記憶體增加的很明顯,而通過觀察退出介面後的記憶體波動發現,記憶體並沒有降低,這是怎麼回事呢?這裡筆者就要強調下Java基礎知識的重要性了。Java中,靜態變數會優先物件而分配記憶體,並且其生命週期是和類的生命週期是相同的,也就是說靜態變數的生命週期大於非靜態變數的生命週期,也就是說靜態變數是不會隨著物件的回收而回收的。那麼很容易理解這裡的記憶體洩露了,當Activity結束後,由於這裡sTextView變數是靜態修飾的,指向一個非靜態的TextView物件,同時這個Textview物件在建立的時候傳遞了一個this(也就是當前Activity),也就是說這個TextView物件持有了當前Activity的引用。sTextView持有TextView,而TextView物件又持有Activity,那麼由於sTextView是靜態的,並不會被回收(直到應用停止執行被回收),那麼Java GC 並不會回收一個任然具有引用指向的物件,也就是說這個TextView不會被回收,那麼TextView既然不會被回收,那Activity也不會被回收了,Activity都不會被回收了,這裡就明顯發生記憶體洩漏了。
第二種情況:全域性Toast工具類引發的記憶體洩露。筆者在優化專案的過程被一個Toast工具類困擾了很久,筆者是真的知道這個工具類有記憶體洩露,但是缺無法對它就地正法,因為專案用到的地方太多,牽一髮就會動全身啊,那麼究竟是什麼的程式碼呢?
[html] view plain copyprint?- public class ToastUtil {
- public static Toast toast = null;
- private static View v;
- private static TextView text;
- public static void show(Context context, String msg) {
- if (toast == null) {
- toast = new Toast(context);
- v = LayoutInflater.from(context).inflate(R.layout.toast, null);
- text = (TextView) v.findViewById(R.id.text);
- text.setText(msg);
- toast.setDuration(Toast.LENGTH_SHORT);
- toast.setView(v);
- toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
- toast.show();
- } else {
- text.setText(msg);
- toast.show();
- }
- }
- public static void show(Context context, int resId) {
- if (toast == null) {
- toast = new Toast(context);
- v = LayoutInflater.from(context).inflate(R.layout.toast, null);
- text = (TextView) v.findViewById(R.id.text);
- text.setText(resId);
- toast.setDuration(Toast.LENGTH_SHORT);
- toast.setView(v);
- toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
- toast.show();
- } else {
- text.setText(resId);
- toast.show();
- }
- }
- }
public class ToastUtil {
public static Toast toast = null;
private static View v;
private static TextView text;
public static void show(Context context, String msg) {
if (toast == null) {
toast = new Toast(context);
v = LayoutInflater.from(context).inflate(R.layout.toast, null);
text = (TextView) v.findViewById(R.id.text);
text.setText(msg);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(v);
toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
toast.show();
} else {
text.setText(msg);
toast.show();
}
}
public static void show(Context context, int resId) {
if (toast == null) {
toast = new Toast(context);
v = LayoutInflater.from(context).inflate(R.layout.toast, null);
text = (TextView) v.findViewById(R.id.text);
text.setText(resId);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(v);
toast.setGravity(Gravity.BOTTOM, Gravity.LEFT, 240);
toast.show();
} else {
text.setText(resId);
toast.show();
}
}
}
這是一個真實而悲傷的故事.....不知道各位看官有沒有第一時間注意到那個靜態的TextView,在上面第一種情況中,我們分析過那個靜態的sTextView引發的記憶體洩露,那麼這裡又是這樣一個靜態的TextView,讀者能根據上面的分析過程分析下這個ToastUtil類的問題嗎?不管你有沒有分析,我就當你分析了....,可是筆者卻不是第一時間觀察到這TextView,而是這裡的靜態Toast引用,很明顯這個ToastUtil犯的錯誤和上面的是同一個錯誤,這裡的Toast引用通過new了一個Toast物件來初始化了,各位看官應該看到了這Toast物件也是需要傳遞一個Context作為引數的,那麼這Context肯定99.9%是Activity的引用這個沒人反對吧。既然這個靜態的Toast引用直接引用了Activity,那麼結果就如上面那種情況,Toast在應用生命週期內活著,在不主動置為空的情況不會輕易被回收,因此Activity也不會被回收,那麼記憶體洩露再一次來臨了。
我們總結下上面兩種情況,發現都是靜態的變數直接或者間接引用了Activity而引發的記憶體洩露,那麼平時的程式碼中還有其他的情況嗎?答案是肯定的。下面筆者會再舉幾個平時開發常見的寫法,各位看過看好了...
[html] view plain copyprint?- public class SingleInstance {
- private Context mContext;
- private static SingleInstance sSingleInstance;
- private SingleInstance(Context context) {
- mContext = context;
- }
- public SingleInstance getInstance(Context context) {
- if (sSingleInstance == null) {
- synchronized (SingleInstance.class) {
- if (sSingleInstance == null) {
- sSingleInstance = new SingleInstance(context);
- }
- }
- }
- return sSingleInstance;
- }
- public void doSomething() {
- //do some thing
- }
- }
public class SingleInstance {
private Context mContext;
private static SingleInstance sSingleInstance;
private SingleInstance(Context context) {
mContext = context;
}
public SingleInstance getInstance(Context context) {
if (sSingleInstance == null) {
synchronized (SingleInstance.class) {
if (sSingleInstance == null) {
sSingleInstance = new SingleInstance(context);
}
}
}
return sSingleInstance;
}
public void doSomething() {
//do some thing
}
}
這段程式碼不知道看官有沒有寫過,反正我是寫過的... 這不就是普通的單例模式嘛,沒什麼問題啊,Context也不是靜態的,確實沒什麼問題啊。如果你認為這段程式碼沒什麼為題,那我還貼它幹嘛啊。事實上在仔細觀察一下,你會發現,確實沒有靜態的Context了,也沒有靜態的TextView了,但是這個類是單例啊,單例都是靜態的才能保持應用唯一啊,那麼唯一可能的為題就是這個單例了。事實上,問題就是處在這個單例,這個單例在第一次初始化的時候new 了一個自己賦值給sSingleInstance變數上,但是有沒有發現這個物件傳遞了一個Context啊,單例在應用內唯一,那麼單例模式中具體幹活的物件也就不會輕易的回收了,那麼這個物件在第一次呼叫時傳遞了哪個Context(Activity),那麼這個Activity就會被這個單例物件持有了,那麼這個Activity就洩露了。就是這麼霸道,洩露了...那是不是我寫的單例沒有用到Context是不是就不會有記憶體洩露了?這個不一定哦,比方舉另一個單例。
在沒有EventBus的時代,遠端通知還可以使用單例觀察者來實現觀察者模式(不知道是不是這樣叫...),程式碼如下:
[html] view plain copyprint?- public final class DataChangeObserverProxy {
- private static DataChangeObserverProxy instance = null;
- public static synchronized final DataChangeObserverProxy getInstance() {
- if (instance == null) {
- instance = new DataChangeObserverProxy();
- }
- return instance;
- }
- private DataChangeObserverProxy() { }
- public interface DataChangeObserver {
- void onDataChanged();
- }
- private DataChangeObserver dataChangeObserver = null;
- public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {
- this.dataChangeObserver = dataChangeObserver;
- }
- public synchronized void notifyBeKilled() {
- if (dataChangeObserver != null) {
- dataChangeObserver.notifyDataChanged();
- }
- }
- }
public final class DataChangeObserverProxy {
private static DataChangeObserverProxy instance = null;
public static synchronized final DataChangeObserverProxy getInstance() {
if (instance == null) {
instance = new DataChangeObserverProxy();
}
return instance;
}
private DataChangeObserverProxy() { }
public interface DataChangeObserver {
void onDataChanged();
}
private DataChangeObserver dataChangeObserver = null;
public void setDataChangeObserver(DataChangeObserver dataChangeObserver) {
this.dataChangeObserver = dataChangeObserver;
}
public synchronized void notifyBeKilled() {
if (dataChangeObserver != null) {
dataChangeObserver.notifyDataChanged();
}
}
}
有沒有發現很想觀察者模式呢?沒錯,它就是一個觀察者模式,不過是個單例模式實現遠端通知,這個類裡面並沒有任何Context引用,靜態sInstance的初始化也沒有傳遞Context來建立單例物件,那麼這個類是不是沒有問題了?給你們2分鐘考慮一下。..............................
好了,這個類的生命確實沒有問題,但是要看使用場景,我們都知道,觀察者模式是通過呼叫實現了觀察者介面的實現類來來通知的,那麼前提是被通知者要實現觀察者的介面方法。在使用的時候你需要呼叫setDataChangeObserver方法來傳遞一個DataChangeObserver物件賦值給本例中的dataChangeObse變數。一般而言我們有如下的做法:
[html] view plain copyprint?- DataChangeObserverProxy.getsInstance().setDataChangeObserver(new DataChangeObserver() {
- @Override
- public void notifyDataChanged() {
- // do something...
- }
- });
DataChangeObserverProxy.getsInstance().setDataChangeObserver(new DataChangeObserver() {
@Override
public void notifyDataChanged() {
// do something...
}
});
或者這樣:[html] view plain copyprint?- public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener(){
- @Override
- public void onClick(View v) {
- startActivity(new Intent(MainActivity.this, HandleLeakActivity.class));
- }
- });
- DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);
- }
- @Override
- public void onDataChanged() {
- }
- }
public class MainActivity extends AppCompatActivity implements DataChangeObserverProxy.DataChangeObserver {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, HandleLeakActivity.class));
}
});
DataChangeObserverProxy.getsInstance().setDataChangeObserver(this);
}
@Override
public void onDataChanged() {
}
}
不管是哪種情況,都需要提供一個DataChangeObserver介面的實現賦值給本例中的dataChangeObserver變數。那麼會出現什麼問題呢?第一種情況,new 了一個DataChangeObserver的匿名物件,我們知道匿名內部類物件會持有外部類引用(往前翻!),一旦dataChangeObserver指向這個匿名物件後,就相當於間接引用了Activity物件,而這個dataChageObserver物件是單例的成員變數,單例前面已經說過是不容易回收的,那麼直接導致這個Activity無法回收,這就發生了記憶體洩露了。但是如果這個單例並沒有使用在任何Context相關的元件中(Activity,自定義控制元件,Service,廣播等等),那麼這個單例倒是沒有多大問題。
單例說完了,繼續看....
很多應用裡面會有那種一次性清空所有Activity的管理類,通常這樣的管理類也是單例的(也有可能是靜態呼叫,並不使用單例),內部持有一個集合來持有所有開啟的Activity,
具體的寫法是這樣的:
[html] view plain copyprint?- public class ActivityStackManager {
- private static List<Activity>sActivityList = new ArrayList<>();
- public static void addActivity(Activity activity) {
- if (activity != null) {
- sActivityList.add(activity);
- }
- }
- public static void removeAllActivity() {
- for (Activity activity : sActivityList) {
- activity.finish();
- }
- }
- }
public class ActivityStackManager {
private static List<Activity> sActivityList = new ArrayList<>();
public static void addActivity(Activity activity) {
if (activity != null) {
sActivityList.add(activity);
}
}
public static void removeAllActivity() {
for (Activity activity : sActivityList) {
activity.finish();
}
}
}
可能這段程式碼和你寫的不太一樣,例如初始化的順序有些不同,但是基本相同吧。有同學就說了,這段程式碼我用過,寫的沒錯啊,為了防止Activity沒有結束,可以在最後一個介面直接呼叫removeAllActivity方法關閉所有可能沒有關閉的Activity啊。筆者認為說的完全沒有錯,很好的做法啊。
但是同學,你就沒有想過,為啥Activity沒有finish,為啥需要收集這些Activity,難道你的應用切換幾個頁面後,最終回到了最開始的頁面的時候,應用還隱藏了很多其他的頁面沒有finish嗎?我告訴你,你這樣是會被主管罵的!
這麼一想這個想法完全行不通吧,為什麼你的應用會隱藏了一堆介面沒有finish...這是需要反思的地方。再者,既然是說記憶體洩露,相信各位看官應該能觀察那個靜態的sActivityList,一看到是個靜態的,必然就會警惕起來,前面已經分析過幾種靜態變數引發記憶體洩露了,那麼這裡應該很容看出,就是sActivityList持有了很多Activity引用,最終導致很多Activity無法回收進而發生記憶體洩露。還有,這樣的工具類,有add方法,沒有remove方法,你會氣死java的設計者們的,無論是觀察者模式,集合類,都會存在例如add,remove,register,unregister 這樣的方法互相對應的,所以這個類應該有個removeActivity方法才對。寫完你就會發現,有了add,remove,和removeAll,是不是使用的時候在Activity的onCreate裡面add,然後onDestroy裡面remove呢?很合理吧?那既然先add,隨後又remove了,就這個工具類到底幹了啥?它到底幹了啥?
存在即合理,我卻不這麼認為。我對這段程式碼的看法:並沒有什麼卵用!我不認為一個android程式設計師在處理介面回收的問題會出現有Activity沒有finish掉。有人可能覺得我說的太絕對,有時候有的需求是這樣的 A ->B->C->D......Z 然後需要在Z介面處理完某些業務邏輯後關閉前面所有的Activity?且不說產品設計這樣的是否合理,就算讓你實現起來也很費勁,而且就算真的有這樣的需求,我覺得你讓A介面前面的介面設定成singleTask啟動模式,也能解決問題吧。
說了這麼多,差不多也說完了情景分析了,總的來說,在使用單例,靜態變數的時候,但凡涉及到Context的引用,一定要慎重。決不能隨手就寫了,而不考慮後果。其實真正在做專案的時候,基本很難遇到需要使用靜態變數的時候,單例確實會常見,下面會具體針對上面發生的各種記憶體洩露而給出解決措施。
最後,我們來聊聊如何處理記憶體洩露的問題了。
其實,在分析完記憶體洩露幾種常見的場景後,如何處理記憶體洩露以及不是難事了,畢竟你已經從知其然到了知其所以然的地步了。那麼我們一個一個來。
先說輪播圖例子,輪播圖的例子中,由於Handler是非靜態內部類物件,因此會持有當前Activity的引用,在非同步任務下,進而引發記憶體洩露。這個例子中,handleMessage方法不斷的處理髮送過的訊息,同時,再一次傳送訊息,這樣形成永久迴圈。處理這樣的問題有兩種方法:
第一,在Activityfinishi的時候我們溢位這個Handle先關的訊息佇列中的暫存的所有訊息,沒有訊息處理後,在將handler值為空就可以。安全的做法是這樣的:
[html] view plain copyprint?- <spanstyle="font-size:12px;"> @Override
- protected void onDestroy() {
- super.onDestroy();
- if (handler != null) {
- handler.removeCallbacksAndMessages(null);
- handler = null;
- }
- }</span>
<span style="font-size:12px;"> @Override
protected void onDestroy() {
super.onDestroy();
if (handler != null) {
handler.removeCallbacksAndMessages(null);
handler = null;
}
}</span>
第二,非靜態內部類是通過持有外部類的引用而發生的記憶體洩露,那麼我們可以讓這個內部類靜態化,因為靜態的內部類不會持有外部類引用。當你把MyHandler類的宣告靜態化後,你會發現startLoop方法無法呼叫了,這就對了,因為靜態的內部類不持有外部類物件了,也就沒法呼叫外部類物件的方法了。那麼還是有其他辦法的,比如:[html] view plain copyprint?- static class MyHandler extends Handler {
- private WeakReference<HandleLeakActivity> reference;
- private HandleLeakActivity activity;
- final int TOTAL = 4;
- int count = 0;
- public MyHandler(HandleLeakActivity outerObject) {
- reference = new WeakReference<>(outerObject);
- activity = reference.get();
- activity.startLoop();
- }
- @Override
- public void handleMessage(Message msg) {
- Log.w(TAG, "切換到第 " + count + " 張圖片");
- count = ++count % TOTAL;
- sendEmptyMessageDelayed(0, 2000);
- }
- }