概念
根搜索算法
Android 虚拟机的垃圾回收采用的是根搜索算法。GC 会从根节点(GC Roots)开始对 heap 进行遍历。到最后,部分没有直接或者间接引用到 GC Roots 的就是需要回收的垃圾,会被 GC 回收掉。
根搜索算法相比引用计数法很好的解决了循环引用的问题。举个例子,Activity 有 View 的引用,View 也有 Activity 的引用,之前我还尝试去源代码里找 Activity 何时和 View 断开连接是大错特错了。当 Activity finish 掉之后,Activity 和 View 的循环引用已成孤岛,不再引用到 GC Roots,无需断开也会被回收掉。
内存泄漏
Android 内存泄漏指的是进程中某些对象(垃圾对象)已经没有使用价值了,但是它们却可以直接或间接地引用到 gc roots 导致无法被 GC 回收。无用的对象占据着内存空间,使得实际可使用内存变小,形象地说法就是内存泄漏了。
场景
-
类的静态变量持有大数据对象
静态变量长期维持到大数据对象的引用,阻止垃圾回收。 -
非静态内部类的静态实例
非静态内部类会维持一个到外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持着外部类的引用,阻止被回收掉。 -
资源对象未关闭
资源性对象如 Cursor、File、Socket,应该在使用后及时关闭。未在 finally 中关闭,会导致异常情况下资源对象未被释放的隐患。 -
注册对象未反注册
未反注册会导致观察者列表里维持着对象的引用,阻止垃圾回收。 -
Handler临时性内存泄露
Handler 通过发送 Message 与主线程交互,Message 发出之后是存储在 MessageQueue 中的,有些 Message 也不是马上就被处理的。在 Message 中存在一个 target,是 Handler 的一个引用,如果 Message 在 Queue 中存在的时间越长,就会导致 Handler 无法被回收。如果 Handler 是非静态的,则会导致 Activity 或者 Service 不会被回收。
由于 AsyncTask 内部也是 Handler 机制,同样存在内存泄漏的风险。
此种内存泄露,一般是临时性的。
预防
-
不要维持到 Activity 的长久引用,对 activity 的引用应该和 activity 本身有相同的生命周期。
-
尽量使用context-application代替context-activity
-
Activity 中尽量不要使用非静态内部类,可以使用静态内部类和WeakReference代替。
工具分析
Java 内存泄漏的分析工具有很多,但众所周知的要数 MAT(Memory Analysis Tools) 和 YourKit 了。由于篇幅问题,我这里就只对 MAT 的使用做一下介绍。--> MAT 的安装
-
MAT分析heap的总内存占用大小来初步判断是否存在泄露
打开 DDMS 工具,在左边 Devices 视图页面选中“Update Heap”图标,然后在右边切换到 Heap 视图,点击 Heap 视图中的“Cause GC”按钮,到此为止需检测的进程就可以被监视。
Heap视图中部有一个Type叫做data object,即数据对象,也就是我们的程序中大量存在的类类型的对象。在data object一行中有一列是“Total Size”,其值就是当前进程中所有Java数据对象的内存总量,一般情况下,这个值的大小决定了是否会有内存泄漏。可以这样判断:
进入某应用,不断的操作该应用,同时注意观察data object的Total Size值,正常情况下Total Size值都会稳定在一个有限的范围内,也就是说由于程序中的的代码良好,没有造成对象不被垃圾回收的情况。
所以说虽然我们不断的操作会不断的生成很多对象,而在虚拟机不断的进行GC的过程中,这些对象都被回收了,内存占用量会会落到一个稳定的水平;反之如果代码中存在没有释放对象引用的情况,则data object的Total Size值在每次GC后不会有明显的回落。随着操作次数的增多Total Size的值会越来越大,直到到达一个上限后导致进程被杀掉。
-
MAT分析hprof来定位内存泄露的原因所在
这是出现内存泄露后使用MAT进行问题定位的有效手段。
A) Dump出内存泄露当时的内存镜像hprof,分析怀疑泄露的类:
B) 分析持有此类对象引用的外部对象
C) 分析这些持有引用的对象的GC路径
D) 逐个分析每个对象的GC路径是否正常
从这个路径可以看出是一个antiRadiationUtil工具类对象持有了MainActivity的引用导致MainActivity无法释放。此时就要进入代码分析此时antiRadiationUtil的引用持有是否合理(如果antiRadiationUtil持有了MainActivity的context导致节目退出后MainActivity无法销毁,那一般都属于内存泄露了)。
-
MAT对比操作前后的hprof来定位内存泄露的根因所在
为查找内存泄漏,通常需要两个 Dump结果作对比,打开 Navigator History面板,将两个表的 Histogram结果都添加到 Compare Basket中去
A) 第一个HPROF 文件(usingFile > Open Heap Dump ).
B)打开Histogram view.
C)在NavigationHistory view里 (如果看不到就从Window >show view>MAT- Navigation History ), 右击histogram然后选择Add to Compare Basket .
D)打开第二个HPROF 文件然后重做步骤2和3.
E)切换到Compare Basket view, 然后点击Compare the Results (视图右上角的红色”!”图标)。
F)分析对比结果
可以看出两个hprof的数据对象对比结果。
通过这种方式可以快速定位到操作前后所持有的对象增量,从而进一步定位出当前操作导致内存泄露的具体原因是泄露了什么数据对象。
注意:
如果是用 MAT Eclipse 插件获取的 Dump文件,不需要经过转换则可在MAT中打开,Adt会自动进行转换。
而手机SDk Dump 出的文件要经过转换才能被 MAT识别,Android SDK提供了这个工具 hprof-conv (位于 sdk/tools下)
首先,要通过控制台进入到你的 android sdk tools 目录下执行以下命令:
./hprof-conv xxx-a.hprof xxx-b.hprof
例如 hprof-conv input.hprof out.hprof
此时才能将out.hprof放在eclipse的MAT中打开。
Ok,下面将给大家介绍一个屌炸天的工具 -- LeakCanary 。
使用 LeakCanary 检测 Android 的内存泄漏
什么是 LeakCanary 呢?为什么选择它来检测 Android 的内存泄漏呢?
别急,让我来慢慢告诉大家!
LeakCanary 是国外一位大神 Pierre-Yves Ricau 开发的一个用于检测内存泄露的开源类库。一般情况下,在对战内存泄露中,我们都会经过以下几个关键步骤:
1、了解 OutOfMemoryError 情况。
2、重现问题。
3、在发生内存泄露的时候,把内存 Dump 出来。
4、在发生内存泄露的时候,把内存 Dump 出来。
5、计算这个对象到 GC roots 的最短强引用路径。
6、确定引用路径中的哪个引用是不该有的,然后修复问题。
很复杂对吧?
如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了。LeakCanary 做的就是这件事情。你可以在 debug 包中轻松检测内存泄露。
一起来看这个例子(摘自 LeakCanary 中文使用说明,下面会附上所有的参考文档链接):
class Cat {
}
class Box {
Cat hiddenCat;
}
class Docker {
// 静态变量,将不会被回收,除非加载 Docker 类的 ClassLoader 被回收。
static Box container;
}
// ...
Box box = new Box();
// 薛定谔之猫
Cat schrodingerCat = new Cat();
box.hiddenCat = schrodingerCat;
Docker.container = box;
创建一个RefWatcher,监控对象引用情况。
// 我们期待薛定谔之猫很快就会消失(或者不消失),我们监控一下
refWatcher.watch(schrodingerCat);
当发现有内存泄露的时候,你会看到一个很漂亮的 leak trace 报告:
-
GC ROOT static Docker.container
-
references Box.hiddenCat
-
leaks Cat instance
我们知道,你很忙,每天都有一大堆需求。所以我们把这个事情弄得很简单,你只需要添加一行代码就行了。然后 LeakCanary 就会自动侦测 activity 的内存泄露了。
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
然后你会在通知栏看到这样很漂亮的一个界面:
以很直白的方式将内存泄露展现在我们的面前。
Demo
一个非常简单的 LeakCanary demo: 一个非常简单的 LeakCanary demo: https://github.com/liaohuqiu/leakcanary-demo
接入
在 build.gradle 中加入引用,不同的编译使用不同的引用:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
如何使用
使用 RefWatcher 监控那些本该被回收的对象。
RefWatcher refWatcher = {...};
// 监控
refWatcher.watch(schrodingerCat);
LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
在Application中进行配置 :
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) { ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
使用 RefWatcher 监控 Fragment:
public abstract class BaseFragment extends Fragment { @Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity()); refWatcher.watch(this); } }
使用 RefWatcher 监控 Activity:
public class MainActivity extends AppCompatActivity {
......
@Override
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//在自己的应用初始Activity中加入如下两行代码
RefWatcher refWatcher = ExampleApplication.getRefWatcher(this);
refWatcher.watch(this);
textView = (TextView) findViewById(R.id.tv);
textView.setOnClickListener(new View.OnClickListener(){ @Override
public void onClick(View v) {
startAsyncTask();
}
});
}
private void async() {
startAsyncTask();
}
private void startAsyncTask() {
// This async task is an anonymous class and therefore has a hidden reference to the outer
// class MainActivity. If the activity gets destroyed before the task finishes (e.g. rotation),
// the activity instance will leak.
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) { // Do some slow work in background
SystemClock.sleep(20000);
return null;
}
}.execute();
}
}
工作机制
1.RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
2.然后在后台线程检查引用是否被清除,如果没有,调用GC。
3.如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
4.在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
5.得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
6.HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
7.引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
常见的内存泄漏案例
case 1. 单例造成的内存泄露
单例的静态特性导致其生命周期同应用一样长。
解决方案:
将该属性的引用方式改为弱引用;
如果传入Context,使用ApplicationContext;
example:
泄漏代码片段
private static ScrollHelper mInstance;
private ScrollHelper() {
}
public static ScrollHelper getInstance() {
if (mInstance == null) {
synchronized (ScrollHelper.class) {
if (mInstance == null) {
mInstance = new ScrollHelper();
}
}
}
return mInstance;
}
/**
* 被点击的view
*/
private View mScrolledView = null;
public void setScrolledView(View scrolledView) {
mScrolledView = scrolledView;
}
Solution:使用WeakReference
private static ScrollHelper mInstance;
private ScrollHelper() {
}
public static ScrollHelper getInstance() {
if (mInstance == null) {
synchronized (ScrollHelper.class) {
if (mInstance == null) {
mInstance = new ScrollHelper();
}
}
}
return mInstance;
}
/**
* 被点击的view
*/
private WeakReference<View> mScrolledViewWeakRef = null;
public void setScrolledView(View scrolledView) {
mScrolledViewWeakRef = new WeakReference<View>(scrolledView);
}
case 2. InnerClass匿名内部类
在Java中,非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。
解决方案:
将内部类变成静态内部类;
如果有强引用Activity中的属性,则将该属性的引用方式改为弱引用;
在业务允许的情况下,当Activity执行onDestory时,结束这些耗时任务;
example:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
//这儿发生泄漏
public void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
Solution:
public class LeakAct extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.aty_leak);
test();
}
//加上static,变成静态匿名内部类
public static void test() {
new Thread(new Runnable() {
@Override
public void run() {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}).start();
}
}
case 3. Activity Context 的不正确使用
在Android应用程序中通常可以使用两种Context对象:Activity和Application。当类或方法需要Context对象的时候常见的做法是使用第一个作为Context参数。这样就意味着View对象对整个Activity保持引用,因此也就保持对Activty的所有的引用。
假设一个场景,当应用程序有个比较大的Bitmap类型的图片,每次旋转是都重新加载图片所用的时间较多。为了提高屏幕旋转是Activity的创建速度,最简单的方法时将这个Bitmap对象使用Static修饰。 当一个Drawable绑定在View上,实际上这个View对象就会成为这份Drawable的一个Callback成员变量。而静态变量的生命周期要长于Activity。导致了当旋转屏幕时,Activity无法被回收,而造成内存泄露。
解决方案:
使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;
对Context的引用不要超过它本身的生命周期,慎重的对Context使用“static”关键字。Context里如果有线程,一定要在onDestroy()里及时停掉。
example:
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
Solution:
private static Drawable sBackground;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getApplicationContext().getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
case 4. Handler引起的内存泄漏
当Handler中有延迟的的任务或是等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被垃圾回收器回收,而导致了内存泄露。
解决方案:
可以把Handler类放在单独的类文件中,或者使用静态内部类便可以避免泄露;
如果想在Handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向所在Activity.使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。
Solution
@Override
protected void doOnDestroy() {
super.doOnDestroy();
if (mHandler != null) {
mHandler.removeCallbacksAndMessages(null);
}
mHandler = null;
mRenderCallback = null;
}
Handler的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等api都应该会借助Handler来处理,对于Handler的使用代码编写一不规范即有可能造成内存泄漏,如下示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public
class
MainActivity
extends
AppCompatActivity {
private
Handler mHandler =
new
Handler() {
@Override
public
void
handleMessage(Message msg) {
//...
}
};
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadData();
}
private
void
loadData(){
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
|
这种创建Handler的方式会造成内存泄漏,由于mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Activity的引用,我们知道消息队列是在一个Looper线程中不断轮询处理消息,那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
public
class
MainActivity
extends
AppCompatActivity {
private
MyHandler mHandler =
new
MyHandler(
this
);
private
TextView mTextView ;
private
static
class
MyHandler
extends
Handler {
private
WeakReference<Context> reference;
public
MyHandler(Context context) {
reference =
new
WeakReference<>(context);
}
@Override
public
void
handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if
(activity !=
null
){
activity.mTextView.setText(
""
);
}
}
}
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private
void
loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
}
|
创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象,这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以我们在Activity的Destroy时或者Stop时应该移除消息队列中的消息,更准确的做法如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
public
class
MainActivity
extends
AppCompatActivity {
private
MyHandler mHandler =
new
MyHandler(
this
);
private
TextView mTextView ;
private
static
class
MyHandler
extends
Handler {
private
WeakReference<Context> reference;
public
MyHandler(Context context) {
reference =
new
WeakReference<>(context);
}
@Override
public
void
handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if
(activity !=
null
){
activity.mTextView.setText(
""
);
}
}
}
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextView = (TextView)findViewById(R.id.textview);
loadData();
}
private
void
loadData() {
//...request
Message message = Message.obtain();
mHandler.sendMessage(message);
}
@Override
protected
void
onDestroy() {
super
.onDestroy();
mHandler.removeCallbacksAndMessages(
null
);
}
}
|
使用mHandler.removeCallbacksAndMessages(null);是移除消息队列中所有消息和所有的Runnable。当然也可以使用mHandler.removeCallbacks();或mHandler.removeMessages();来移除指定的Runnable和Message。
case 5. 注册监听器的泄漏
系统服务可以通过Context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果Context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity 的引用,如果在Activity onDestory时没有释放掉引用就会内存泄漏。
解决方案:
使用ApplicationContext代替ActivityContext;
在Activity执行onDestory时,调用反注册;
mSensorManager = (SensorManager) this.getSystemService(Context.SENSOR_SERVICE);
Solution:
mSensorManager = (SensorManager) getApplicationContext().getSystemService(Context.SENSOR_SERVICE);
下面是容易造成内存泄漏的系统服务:
InputMethodManager imm = (InputMethodManager) context.getApplicationContext().getSystemService(Context.INPUT_METHOD_SERVICE);
Solution:
protected void onDetachedFromWindow() {
if (this.mActionShell != null) {
this.mActionShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mButtonShell != null) {
this.mButtonShell.setOnClickListener((OnAreaClickListener)null);
}
if (this.mCountShell != this.mCountShell) {
this.mCountShell.setOnClickListener((OnAreaClickListener)null);
}
super.onDetachedFromWindow();
}
case 6. Cursor,Stream没有close,View没有recyle
资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null. 在我们的程序退出时一定要确保我们的资源性对象已经关闭。
Solution:
调用onRecycled()
@Override
public void onRecycled() {
reset();
mSinglePicArea.onRecycled();
}
在View中调用reset()
public void reset() {
if (mHasRecyled) {
return;
}
...
SubAreaShell.recycle(mActionBtnShell);
mActionBtnShell = null;
...
mIsDoingAvatartRedPocketAnim = false;
if (mAvatarArea != null) {
mAvatarArea.reset();
}
if (mNickNameArea != null) {
mNickNameArea.reset();
}
}
case 7. 集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
解决方案:
在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。
Solution
private List<EmotionPanelInfo> data;
public void onDestory() {
if (data != null) {
data.clear();
data = null;
}
}
case 8. WebView造成的泄露
当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
解决方案:
为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。
case 9. 构造Adapter时,没有使用缓存的ConvertView
初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象 缓存起来。
当向上滚动ListView时,原先位于最上面的List Item的View对象会被回收,然后被用来构造新出现的最下面的List Item。
这个构造过程就是由getView()方法完成的,getView()的第二个形参View ConvertView就是被缓存起来的List Item的View对象(初始化时缓存中没有View对象则ConvertView是null)。