GitHub地址
https://github.com/realxz/MemoryLeak
GitHub 代碼只包含泄漏情況,不包括修改后的代碼,大家可以下載下來后,自行修改。
什么是內存
Android 系統我們的 APP 分配的內存大小是有限的,我現在用的手機小米 4c 為我自己開發的應用 分配的256MB的內存大小,不同的手機型號,不同的 ROM 分配的內存大小不一定一樣,這里面所提 到的內存一般是指 Android 手機的 RAM。
RAM 包含寄存器,棧,堆,靜態存儲區域,常量池。通常我們所說的 Android 內存泄漏中的內存, 指的是其中的堆內存。一般來來說,我們 new 出來的對 象都會存儲在堆內存中,這部分的內存由 GC 進行回收管理。
GC 是什么,它如何進行內存管理
GC 指垃圾回收器 「Garbage Collection」。Java 使用 GC 進行內存回收理,不用我們手動釋放內 存,提升了我們的開發效率。那GC回收對象的依據是什么呢 ?簡單的說,對于一個對象,若果不存 在從 GC 根節點到該對象的引用鏈 (從根節點不可到達的 (從根節點不可到達的),那么對于 GC 來說這個對象就是需要被回收的,反之該對象是從根節點可到達的,那么這個對象就不會被 GC 回 收。
根節點:在 Java 中可以作為根節點的對象有很多,這塊內容我理解的不是很到位。我很簡單的把它理解為 Android 應用的主線程,存活的子線程,棧中的對象以及靜態屬性引用的對象。
注意:這里的引用是指強引用,在 Java 當中存在4種引用類型分別是「強引用」、「軟引用」、「弱引用」、「虛引用」。如果沒有特別指定,我們所說的引用都是指強引用,GC 不會回收具有 強引用的對象。
什么是內存泄漏
我們已經知道了,如果某個對象,從根節點可到達,也就是存在從根節點到該對象的引用鏈,那么該對象是不會被 GC 回收的。如果說這個對象已經不會再被使用到了,是無用的,我們依然持有他的引用的話,就會造成內存泄漏,例如 一個長期在后臺運行的線程持有 Activity 的引用,這個時 候 Activity 執行了 onDestroy 方法,那么這個 Activity 就是從根節點可到達并且無用的對象, 這個 Activity 對象就是泄漏的對象,給這個對象分配的內存將無法被回收。
內存泄漏的影響
- 內存很寶貴,即使從效率,責任的角度上,我們也應該降低內存的使用,減少內存的浪費。
- 內存泄漏導致可用內存越來越少,最終導致OOM。
- 可用內存減少,GC 被觸發,雖然 GC 可以幫助我們回收無用內存,但是頻繁的觸發 GC 也會影響性能,可能造成程序卡頓。
如何查找、定位內存泄漏
- MAT「Memory AnalysisTools」,網上有很多的使用教程,個人感覺使用較繁瑣,需要耐心分析和一定的經驗才能定位出內存泄漏。
- LeakCanary,Square 公司開源作品,使用方便,可以直接定位到泄漏的對象,并且給出調用鏈。
內存泄漏事例
非靜態內部類
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class InnerThreadActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_inner_thread);
RunningThread runningThread = new RunningThread();
runningThread.start();
}
class RunningThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000*5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public void closeActivity(View view) {
this.finish();
}
}
當我們運行這段代碼的時候,LeakCanary 會幫我們檢測出來內存泄漏,如圖:
![image_1bbluquud1ndr12k41mdnpbmiv9.png-59.9kB][1]
在 Java 中,內部類會隱式的持有外部類的引用。我們可以很清楚的看見 RunningThread 對象持有 了 InnerThreadActivity 的引用,由于 RunningThread 線程會一直運行下去,我 finish 掉當前 的 Activity 就會導致 InnerThreadActivity 實例發生泄漏。我們可以采用靜態內部類的方式來解 除這種內存泄漏的隱患,代碼如下:
private static class RunningThread extends Thread {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
注意:盡量使用靜態內部類來替代內部類,同時避免讓長期運行的任務( 線程 )持有 Activity的引用。
匿名內部類
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
public class AnonymousThreadActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_anonymous_thread);
Thread anonymousThread = new Thread() {
@Override
public void run() {
while (true) {
//do something
try {
Thread.sleep(60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return;
}
}
};
anonymousThread.start();
}
public void closeActivity(View view) {
this.finish();
}
}
LeakCanary 同樣會檢測出內存泄漏,如圖:
![image_1bbluvkf51um53c04fucmr1g8cm.png-69.7kB][2]
在 Java 中,匿名內部類和非靜態內部類一樣,都會持有外部類的引用。上面的代碼正式由于 Thread 的匿名類持有了 AnonymousThreadActivity 的引用,并且匿名類的運行時間長達 1 分鐘, 在這段時間內,我 finish 掉了 Activity 導致了內存泄漏,解決方式和非靜態內部類的方法一樣,使用靜態內部類來代替匿名內部類,這里就不貼代碼了。
Handler 內存泄漏
package com.example.xiezhen.memoryleak;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class HandlerActivity extends AppCompatActivity {
private TextView tvShowMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
MemoryLeakHandler handler = new MemoryLeakHandler();
handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
}
class MemoryLeakHandler extends Handler {
@Override
public void handleMessage(Message msg) {
tvShowMessage.setText("MemoryLeak");
Toast.makeText(HandlerActivity.this, "memory leak", Toast.LENGTH_SHORT).show();
}
}
public void closeActivity(View view) {
this.finish();
}
}
![image_1bblv16hj148t1bvmjge13jd5if13.png-79.1kB][3]
LeakCanary 為我們展示了內存泄漏的引用鏈,這段代碼泄漏的原因也是因為非靜態內部類持有了外部類的引用。圖中的引用鏈涉及到 Android 中的消息機制 「Handler」、「MessageQueue」、 「Looper」。大致敘述一下,我們的 MemoryLeakHandler 因為內部類的關系會持有 HandlerActivity 實例的引用,我們使用 Handler 來發送消息,這個Handler 會被消息中 target 屬性引用,這個 Message 會在我們主線程的消息隊 列中存活 10 秒鐘,在這段時間內,我 finish 掉當前 Activity 就會造成內存泄漏,并且依然會彈出 Toast 盡管我們已經開不見這個 Activity了。
解決方案依然是采用靜態內部類來替代非靜態內部類,并且使用 WeakReference 來引用 Activity,如果對象只存在弱引用的話,GC 是會回收這部分內存的。
package com.example.xiezhen.memoryleak;
import android.os.Handler;
import android.os.Message;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import java.lang.ref.WeakReference;
public class HandlerActivity extends AppCompatActivity {
private TextView tvShowMessage;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
tvShowMessage = (TextView) findViewById(R.id.tv_show_message);
MemoryLeakHandler handler = new MemoryLeakHandler(this);
handler.sendMessageDelayed(Message.obtain(), 1000 * 10);
}
private static class MemoryLeakHandler extends Handler {
private WeakReference<HandlerActivity> weakReference;
public MemoryLeakHandler(HandlerActivity activity) {
this.weakReference = new WeakReference<HandlerActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
HandlerActivity activity = weakReference.get();
if (activity != null) {
activity.tvShowMessage.setText("MemoryLeak");
Toast.makeText(activity, "memory leak", Toast.LENGTH_SHORT).show();
}
}
}
public void closeActivity(View view) {
this.finish();
}
}
單例/靜態引用
static volatile EventBus defaultInstance;
public static EventBus getDefault() {
if (defaultInstance == null) {
synchronized (EventBus.class) {
if (defaultInstance == null) {
defaultInstance = new EventBus();
}
}
}
return defaultInstance;
}
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
public class EventBusActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_event_bus);
EventBus.getDefault().register(this);
EventBusThread thread=new EventBusThread();
thread.start();
}
public void closeActivity(View view) {
this.finish();
}
private static class EventBusThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
EventBus.getDefault().post("eventbus");
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void receiveMessage(String message) {
Toast.makeText(this, message, Toast.LENGTH_LONG).show();
}
}
EventBus 我相信大家都不陌生,我們一般在使用 EventBus 的時候,都會使用EventBus.getDefault( ) 方法來獲取一個 EventBus 單例,這個單例是靜態的,全局可訪問的。上面的代碼我們在獲取 EventBus 單例后調用 register 方法,將 EventBusActivity 注冊到 EventBus 中,這個時候 EventBus 就會持有 Activity 的引用,由于單例是靜態的,生命周期和整個 App 生命周期 一致,如果我們不調用 unRegister 方法的話,EventBusActivity 實例就會泄漏。上述代碼中,我特意沒有去調用 unRegister 方法,我們來看看 LeakCanary 的結果:
![image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png-88kB][4]
- 不要讓我們的對象被靜態屬性所引用,這很容易造成內存泄漏。
- 一般來說我們在使用注冊方法的時候,library 都會提供相對應的解除注冊方法,不要忘了調用!
Activity Context & Application Context
package com.example.xiezhen.memoryleak;
import android.content.Context;
import android.content.pm.PackageInfo;
/**
* Created by xiezhen on 2017/3/16.
*/
public class CommonHelper {
private Context context;
private static CommonHelper commonHelper = null;
private CommonHelper(Context context) {
this.context = context;
}
public static CommonHelper getCommonHelper(Context context) {
if (commonHelper == null) {
commonHelper = new CommonHelper(context);
}
return commonHelper;
}
public int getVersionCode() {
PackageInfo packInfo = getPackageInfo(context);
if (packInfo != null) {
return packInfo.versionCode;
} else {
return -1;
}
}
public String getVersionName() {
PackageInfo packInfo = getPackageInfo(context);
if (packInfo != null) {
return packInfo.versionName;
} else {
return "";
}
}
private PackageInfo getPackageInfo(Context context) {
try {
return context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (Exception e) {
return null;
}
}
}
package com.example.xiezhen.memoryleak;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
public class ContextActivity extends AppCompatActivity {
private TextView tvVersionName;
private TextView tvVersionCode;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_context);
tvVersionName = (TextView) findViewById(R.id.tv_version_name);
tvVersionCode = (TextView) findViewById(R.id.tv_version_code);
}
@Override
protected void onResume() {
super.onResume();
setVersionCode(CommonHelper.getCommonHelper(this).getVersionCode());
setVersionName(CommonHelper.getCommonHelper(this).getVersionName());
}
private void setVersionName(String versionName) {
tvVersionName.setText(versionName);
}
private void setVersionCode(int versionCode) {
tvVersionCode.setText(String.valueOf(versionCode));
}
public void closeActivity(View view) {
this.finish();
}
}
我寫了一個工具類,來獲取 App 的 Version Code 和 Version Name,這段代碼同樣會導致內存泄漏,下面是 LeakCanary 的泄露圖。
![image_1bblvctrqieh39h9asvsm9p91t.png-60.1kB][5]
發現泄露了一個 Context 實例,在調用 CommonHelper 中的方法時候的時候,我們將 ContextActivity 作為一個 Context 對象傳遞了進去,Context 對象的引用被長期持有導致內存泄漏。處理這種泄漏的方法很簡單,使用 Application Context 來代替 Activity Context 即可,Application Context 在整個 App 生命周期內適用。
結論
一般來說,內存泄漏都是因為泄漏對象的引用被傳遞到該對象的范圍之外,或者說內存泄漏是因為持有對象的長期引用,導致對象無法被 GC 回收。為了避免這種情況,我們可以選擇在對象生命周期結束的時候,解除綁定,將引用置為空,或者使用弱引用。
- 由于 Context 導致內存泄漏。使用 Application Context 代替 Activity Context,避免長期持有 Context 的引用,引用應該和 Context 自身的生命周期保持一致。
- 由于非靜態內部類、匿名內部類導致內存泄。它們會隱式的持有外部類的引用,一不小心長期持有該引用就會導致內存泄漏,使用靜態內部類來代替它們。
- Handler 導致內存泄漏。原因和第二點一樣,同樣使用靜態內部類的實現方式,同時對需要引用的對象/資源采用弱引用的方式。
- EventBus導致內存泄漏。EventBus 的單例特性,會長期持有注冊對象的引用,一定要在對象生命周期結束的時候,接觸注冊,釋放引用。同樣對于系統提供的一些成對出現的方法,我們也需要成對的調用,例如 BroadcastReceiver 的 registerReceiver( ) 方法和 unRegisterReceiver( ) 方法。
- 線程導致內存泄漏。我們經常會執行一些長期運行的任務,避免在這些任務中持有 Activity 對象的引用,如果持有了引用的話,我們應該在對象生命周期結束的時候,釋放引用。
參考鏈接
- http://www.androiddesignpatterns.com/2013/01/inner-class-handler-memory-leak.html
- http://www.androiddesignpatterns.com/2013/04/activitys-threads-memory-leaks.html
- https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html
- https://medium.com/freenet-engineering/memory-leaks-in-android-identify-treat-and-avoid-d0b1233acc8#.45ldsvqll
-
http://droidyue.com/blog/2016/11/23/memory-leaks-in-android/index.html
[1]: http://static.zybuluo.com/xiezhen/odev0wja0q36wc92pdrmpd22/image_1bbluquud1ndr12k41mdnpbmiv9.png
[2]: http://static.zybuluo.com/xiezhen/ak2lk5h73l0axpcf7kmz8ib5/image_1bbluvkf51um53c04fucmr1g8cm.png
[3]: http://static.zybuluo.com/xiezhen/ddv0e7hhwbtvf22ev4tkkt6e/image_1bblv16hj148t1bvmjge13jd5if13.png
[4]: http://static.zybuluo.com/xiezhen/58kb04ti3bgd5ttfgxcajx9b/image_1bblv9o5chuh4m6i6m1l8t1mnc1g.png
[5]: http://static.zybuluo.com/xiezhen/kvxq3e0vkmd7n5fxd8g4zlgf/image_1bblvctrqieh39h9asvsm9p91t.png