说到Android的内存泄漏,很多人下意识想到:LeakCanary ,导入依赖,运行后直接看通知栏结果。
但是,你们有没有想过,LeakCanary 检查内存泄漏的范围?其实,LeakCanary 这家伙能且只能检测Activity的内存泄漏
为什么呢?
因为LeakCanary 通过 ActivityLifecycleCallbacks 是监听了整个应用的Activity的创建和销毁,当一个Activity销毁后,
就对这个Activity赋上弱引用,然后手动GC,去促使垃圾回收器回收垃圾。然后在垃圾队列里查看是否有刚才销毁的Activity,
如果没有,就说明这个Activity泄漏了。
这个设计到很多源码的跟踪,还有Java垃圾回收器的机制,在这就不详解了。我之前的博客说过垃圾回收机制的。可以看去。
但是问题来了,如果老子不是 Activity泄漏呢?是一个Fragment泄漏呢?是一个实体对象泄漏了呢?怎么检测呢?
这个LeakCanary 都是检测不出来的。。。。。。所以,这个时候需要另外的工具,
噔噔瞪!!!!!!Profiler 出场了。Android studio 自带的工具。
特别说明一下,Profiler 的样式每个版本都有差异的,这个帖子以Android studio3.4为基准
运行了项目,就可以在底部栏看到 Profiler了
好了,我们的应用运行起来了。我们点一下Profiler,看到的效果
可以看到 Profiler栏目有三个指数,CPU,MEMORY,NETWORK,分别指:CPU使用状况,内存使用状况和网络状况
现在我们是要分析内存。所以我们点一下 MEMORY,他会单独展示 内存使用,如下面:
这个界面,是应用运行时的内存状态。看截图即可。。
通过Profiler分析内存一般有两种方法,
好了,我们先构造一个会内存泄漏的页面,比如:
class LeakActivity : AppCompatActivity() {
companion object {
fun launch(context: Context) {
context.startActivity()
}
lateinit var leakActivity: LeakActivity
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
leakActivity = this
}
}
静态变量引用了当前的Activity。
然后我们的步骤是:
1>进入当前泄漏的页面,然后退出去
2>手动点击垃圾桶按钮,触发GC,触发回收垃圾对象。(这样保证需要回收的对象都不在内存里面)
3> 过3秒下载点击heap堆内存数据,(为什么过3秒呢?因为保证垃圾回收器有足够时间回收完所有需要被回收的对象)
4>等待几秒后,Android studio 会自动生成堆内存数据。比如下面截图:
会生成这么一个内存数据分析图。只要在这个分析目录里面的,就是说明在垃圾回收器回收后的内存数据。如果你的对象按理应该在内存中消失的,但却在这个目录里出现了,就说明你的对象无法被垃圾回收器回收,泄露了。
这个目录,选择按照我们包名目录去展示数据。
比如我现在应用的包名叫:com.leo.dicaprio.myutilapp 我们就会展开com目录去找。
其他目录可能是系统或者其他一些模块,我们基本上没必要去分析。我们只需要分析我们的应用有没有泄露即可。
然后,从上面代码看出,我们怀疑 LeakActivity 泄露,因为从代码上看他被静态变量引用。
所以我们一直依循这个 LeakActivity所在包路径找下去,看他是否还在内存里?看下截图
果不其然,他还在内存里面。没有被回收,泄露了。我们双击一个这个泄露的LeakActivity,
他会 弹出 instance View,然后再点击里面的 LeakActivity,会看到 Reference窗口,
Reference 列表就是展示所有对LeakActivity的引用列表,我们就要冲这个列表找出那个引用导致LeakActivity无法被回收。
来,上一波图:
看到了把。一个名字叫 leakActivity 变量(看清楚大小写),引用了LeakActivity(看清楚大小写),
然后再继续点击去,发现 leakActivity 变量 是在 LeakActivity里面的。这样就非常清晰了。。。。
在这里说一下,heap Dump右边有四个参数,具体是:
Alloc Count:Java堆中的实例个数
Native Size:native层分配的内存大小。
Shallow Size:Java堆中分配实际大小
Retained Size:这个类的所有实例保留的内存总大小(并非实际大小)
前面一种是手动触发gc,然后看内存。但是,你们有没有发现,引用队列有很多乱七八糟的系统引用。这个需要一定得功底才能查找出那些是系统里面的,那些是自己应用里面的引用。
这个时候,我们想要看下第二种办法。好,我们来第二种分析:就是内存录制。录制某一时间内存的状况。然后也是通过
引用队列去定位分析,但是他有一个好处,什么好处呢?先看下面,先构造一个内存泄漏的例子:
public class Leak2Activity extends AppCompatActivity {
public static void launch(Context context) {
Intent intent = new Intent(context, Leak2Activity.class);
context.startActivity(intent);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak2);
postHandler();
}
private void postHandler() {
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
}
},999999999);
}
}
然后操作步骤是:
1:点击record按钮,开始录制内存
2:进入Leak2Activity,然后退出
3:手动GC,3秒后点击按钮停止录制。
4:等待生成内存数据,分析。。。。
即:
【1:点击record按钮,开始录制内存】
【2:进入Leak2Activity,然后退出】
此处省略截图
【3:手动GC,3秒后点击按钮停止录制】
【4:等待生成内存数据,分析】
看分析图,录制出来的内存数据分析图。
还是选择包名去分配目录
然后看到我们的 Leak2Activity 还在内存,说明泄漏。然后我们点击Leak2Activity&1,弹开InstanceView
然后再点击Leak2Activity&1,看Allocation Call Back,神奇的一幕出现了,
看到Leak2Activity 类里面的方法名。没错,就说明这两个类型里的这两个方法 对 Leak2Activity引用了。
然后刚才不说了这个方法有好处吗,说的好处就是它给你精确定位到那个方法进行对这个类引用了
然后我们双击这其中一个方法名,Android Studio 直接跳到这个方法的代码处。
然后看代码就知道这个方法是为什么会造成内存泄漏了。
所以最后总结一下,这两种方法的却别:
方式1:强制GC,生成heap Dump,分析内存引用。
好处:不需要知道对象创建时间,任何时候可以查看堆内存数据
缺点:定位不到相关代码,太多系统的混淆引用。
方式2:录制内存数据,进出页面,分析内存数据
好处:可以定位到相关代码,不用看太多系统的混淆引用。
缺点:需要知道对象创建时间,只能在内存录制时间内可以查看
当然,现在展示的是Activity,其实同样可以检测任何类的实例是否泄漏。原理都是一样的。
以上代码亲测没问题,有问题请留言谢谢!