貌似很久很久没写博客了,996
的生活过了太久,自己也有点颓废了,感觉这样子的状态不行,有一种莫名的负罪感。虽然中间都有断断续续的记录一些东西,学习一些东西,但都感觉还不成气候,就一直没有发到博客上来,但每次想到我的博客都难免有些难受。今天,我选择了面对,而不是逃避,刚好,最近在忙新项目的事情,也涉及到了性能优化方面的事情,就来记录一下最近又重新学习了的,和之前用过的一些经验,记录一下,整理一下。
1.内存泄露的定义
所谓内存泄露
:就是一些本该被回收的资源,被一些对象错误的引用着,而导致GC
没有办法对这些资源进行回收,而造成的一种内存资源浪费的现象。直白的说,就是GC
失去了对这些资源的掌控。
为什么说被引用的对象无法被回收呢?
这就要说起jvm
的垃圾回收机制了。早前,jvm
使用的是引用计数器的方法来判断当前对象是否需要被回收的,而这个方法会引发一个问题,就是孤岛问题,就是说,一些应该被回收的对象,相互引用着,造成对象的计数器,不为0,也就表示该对象还是被其它对象引用着,实际上这一系列对象都应该被回收。
而为了解决这个问题,后面引入了根搜索法(GC ROOT)
的方法来判断一个对象是否应该被回收,大概意思就是说,从GC ROOT
开始将所有还用到的对象,以树的形式连接到一起,而不在这个树上面的对象,就表示为需要被gc
回收的无用对象,从而解决了孤岛问题
。
2.常见的内存泄露的情况
1.错误的Context引用(不同生命周期的对象相互引用)
/**
* Created by allen on 17/3/26.
*/
public class CommonUtils {
private static CommonUtils mCommonUtils;
private Context mContext;
private CommonUtils(Context context) {
this.mContext = context;
}
public static CommonUtils getInstance(Context context) {
if (mCommonUtils == null) {
synchronized (CommonUtils.class) {
if (mCommonUtils == null) {
mCommonUtils = new CommonUtils(context);
}
}
}
return mCommonUtils;
}
}
在activity
中通过下列方式进行应用:
CommonUtils.getInstance(this);
上面的这种情况是典型的使用场景,而上面的这种用法,就造成了内存泄漏。每一次调用getInstance
方法的activity
会被CommonUtils
中的Context
一直引用着,而当其被调用onDestory
后,依然,无法被回收。
解决方法:
使用getApplicationContext
代替Context
,主要是单例对象是静态对象,其生命周期和Application
一致。
2.handle造成的内存泄露
public class MainActivity extends AppCompatActivity {
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Message message = Message.obtain();
message.arg1 = 1;
mHandler.sendMessage(message);
}
}
上面的代码就是Handler
应用的典型场景。
handler
是通过android
消息机制来进行运转的,MessageQueue
中的每个消息都持有一个Handler
对象的引用,而非静态内部类持有外部类的一个引用。当MessageQueue
还有没有发送的消息时,该Message
就持有该Handler
的引用,而该Handler
对外部类Activity
也持有其引用,则此时,如果Activity
退出时,由于被引用,而无法被回收,从而造成了内存泄露。
解决方法:
使用static
来修辞Handler
来解决内部类持有对外部类引用的问题。
3.容器的不当使用,造成的内存泄露
上面的MessageQueue
中Message
持有对Handler
的引用,也是因为容器不当持有需要被销毁对象的引用,而造成的内存泄露。
多见于观察者模式下,没有及时移除引用的情况下,导致的内存泄露。
解决方法:
在相应的生命周期内移除容器内的资源。比如:在onDestory
中,移除容器内部对该Activity
的引用。
4.内部类的内存泄露
还是同样的意思,当外部类需要被销毁时,内部类还有任务在执行,而无法被销毁时,任务引用了内部类,而内部类又引用了外部类,导致外部类无法被回收,而造成的内存泄露。
解决方法
将内部类使用static
修辞。
5.资源未正常关闭造成的内存泄露
对于使用了BraodcastReceiver
,ContentObserver
,File
,Cursor
,Stream
,Bitmap
等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
解决方法
正常关闭相应资源。
……
3.常用的内存泄露分析工具
- Android Monitor
- MAT
- LeakCanary
- Allocation tracking
Android Monitor
Android Monitor
是Android Studio
中默认的性能优化工具。它提供了对Memory
,CPU
,Network
,GPU
的监视。我们今天主要是用来对Memory
的监控。
以单例模式下Context
泄漏为例子来说下其使用.
多次翻转屏幕后,造成MainActivity
的多次回收和创建,然后,按下Initiate GC
按钮后,再按dump java heap
按钮来观察当前heap
中MainActivity
的情况。
如图所示,上面出现了两个MainActivity
的实例,我们都知道,如果在翻转屏幕的时候,如果没有在Androidmainfest.xml
中配置横竖屏不敏感的话,当前Activity
会被销毁,而新的Activity
会被创建,也就是说,正常情况是不会出现两个Activity
的实例的,所以,说明是出现了内存泄漏。
这里可以通过包结构的方式来看相关的泄露情况,更加的直观方便。
图上的信息是通过多次GC
之后,再dump
出来的。从图上可以看到存在两个MainActivity
的对象,说明MainActivity
存在泄露,而为什么下面会多了一个MainActivity$1
,而且个数正好也是两个呢?
其实,这里的MainActivity$1
只是MainActivity
对象的引用,这一个可以从MainActivity$1
中的this
所指向的内存地址正好是MainActivity
的两个地址。
MAT
MAT(memory Analyzer tool)
:内存分析工具,这个是曾经在Eclipse
时代,非常好用的一款内存分析工具,而且有单独版本的。我们这里就使用单独版本的来说明一下其使用方法。
同样的,我们使用Android studio
中dump
出.hprof
格式的文件,然后使用hprof-conv 源文件 目标文件
如:hprof-conv 1.hropf 2.hprof
来生成MAT
可以使用的目标文件2.hprof
。
文件转换完成后,我们导入文件,导入的时候选择内存泄露分析。
这里我主要说两个我们常用的功能,一个是histogram
功能,还有一个就是内存对比。还是以上面MainActivity
泄露的问题来分析。
打开histogram
功能后,就看到了下面的结果:
像上面的东西,我们基本上是看不到任何有价值的东西的,这时,需要我们自己去搜索感兴趣的对象,看其存在的情况了。比如,我们可以搜索MainActivity
大小写区分,来看看MainActitivy
在内存中的存在情况。
图上可以看出,存在两个MainActivity
对象,而其占用的内存大小为528B
.接下来我们要看看是谁引用了我们的引用。
查看列表,我们发现MainActivity
被CommonUtils
中的mContext
引用着。
我们使用merge shortest path(最短路径)
也可以看出,其被CommonUtils
引用着。
同样的,下面这种方式也能找到MainActivity
被谁引用着。
下面,我们来说说,通过内存映射对比来分析其内存泄露情况。
分别打开new27
和new28
两个.hprof
文件,然后,分别打开histogram
功能。
分别将histogram
添加到basket
中。
添加完成后,要点击一下执行结果,才会出现对比的结果。
看到下面的内存对比的情况,这里new27
和new28
分别表示无内存泄露和内存泄露。
通过搜索MainActivity
,我们可以看出#0出现了内存泄露的情况。
Allocation Tracking(分配内存追踪)
这个工具主要是用来分析内存的变化情况,比如:你打开了一个新的页面,此时,新的页面创建了多少对象,哪些对象被创建了多少次都会被追踪到,但如果调用gc
,被回收的对象,也会显示在内存变化中,也就是它关心的是这段时间内,内存的变化,只做加法,也就是只记录最大的变化数,而被回收的对象,也会显示在列表中。这个工具不能直接发现内存泄露,而是作用辅助工具使用,关注内存的变化,可以和hprof
配合使用。
这里我们还是以MainActivity
泄露为例,先点击start allocation tracking
,然后将手机屏幕翻转三次,再调用gc
后的内存变化。
从图上可以看出,在翻转了三次屏幕后,MainActivity
被创建了三次,而当我们调用了GC
后,也是如此,也就说明了,allocation tracking
只关注内存的一个最大变化。此时,我们就可以结合MAT
来关注MainActivity
的内存泄露情况了。
在Android Device Monitor
中的Allocation Tracker
也是同样的效果,不同的只是操作上,以及展示的信息。
LeakCanary
leakCanary
是square
开源的一款用于集成手机端来提醒内存泄漏的框架,只能用于观察Activity
的泄露情况,会自动在消息通知栏,给出提示。
集成进app非常简单,先添加依赖:
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
这里的no-op
版本,说的是在测试编译或者正式版本编译不会被引入项目。
然后在Application
中添加初始化,就开始正常工作了。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
当我翻转了两次屏幕后,就接收到了内存泄露的提示信息了,非常的好用。