标签: leakcanary

Android内存优化8 内存检测工具2 LeakCanary——直白的展现Android中的内存泄露

Android内存优化8 内存检测工具2 LeakCanary——直白的展现Android中的内存泄露

之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用。直到今天终于发现了这个新工具:

当我们的App中存在内存泄露时会在通知栏弹出通知:

这里写图片描述

当点击该通知时,会跳转到具体的页面,展示出Leak的引用路径,如下图所示:

这里写图片描述

LeakCanary 可以用更加直白的方式将内存泄露展现在我们的面前。

 

工程包括:

  1. LeakCanary库代码
  2. LeakCanaryDemo示例代码

使用步骤:

  1. 将LeakCanary import 入自己的工程
  2. 添加依赖:

    compile project(':leakcanary')

  3. 在Application中进行配置
    public class ExampleApplication extends Application {
    
      ......
      //在自己的Application中添加如下代码
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context
                .getApplicationContext();
        return application.refWatcher;
    }
    
      //在自己的Application中添加如下代码
    private RefWatcher refWatcher;
    
    @Override
    public void onCreate() {
        super.onCreate();
        ......
            //在自己的Application中添加如下代码
        refWatcher = LeakCanary.install(this);
        ......
    }
    
    .....
    }
    
  4. 在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. 在AndroidMainfest.xml 中进行配置,添加如下代码
        <service
            android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
            android:enabled="false"
            android:process=":leakcanary" />
        <service
            android:name="com.squareup.leakcanary.DisplayLeakService"
            android:enabled="false" />

        <activity
            android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
            android:enabled="false"
            android:icon="@drawable/__leak_canary_icon"
            android:label="@string/__leak_canary_display_activity_label"
            android:taskAffinity="com.squareup.leakcanary"
            android:theme="@style/__LeakCanary.Base" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

5、测试结果

a、Toast显示(大概10秒左右显示)

%title插图%num

b、通知显示

%title插图%num

c、桌面自动添加的图表

%title插图%num

d、内存泄露列表

%title插图%num

e、内存泄露详细

%title插图%num

LogCat可以看到日志日下(hprof文件可以用MAT打开进行分析):

  1. 01-04 11:49:41.815 12967-13004/com.micky.leakcanarysamples I/dalvikvm: hprof: dumping heap strings to “/storage/emulated/0/Download/leakcanary/suspected_leak_heapdump.hprof”.
  2. 01-04 11:49:42.020 12967-13004/com.micky.leakcanarysamples I/dalvikvm: hprof: heap dump completed (28850KB)

查看自动生成的AndroidManifest文件,LeakCanarySamples/app/build/intermediates/manifests/full/debug/AndroidManifest.xml

  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <manifest xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     package=“com.micky.leakcanarysamples”  
  4.     android:versionCode=“1”  
  5.     android:versionName=“1.0” >  
  6.     <uses-sdk  
  7.         android:minSdkVersion=“10”  
  8.         android:targetSdkVersion=“23” />  
  9.     <!– To store the heap dumps and leak analysis results. –>  
  10.     <uses-permission android:name=“android.permission.WRITE_EXTERNAL_STORAGE” />  
  11.     <android:uses-permission android:name=“android.permission.READ_EXTERNAL_STORAGE” />  
  12.     <application  
  13.         android:name=“com.micky.leakcanarysamples.BaseApplication”  
  14.         android:allowBackup=“true”  
  15.         android:icon=“@mipmap/ic_launcher”  
  16.         android:label=“@string/app_name”  
  17.         android:supportsRtl=“true”  
  18.         android:theme=“@style/AppTheme” >  
  19.         <activity  
  20.             android:name=“com.micky.leakcanarysamples.MainActivity”  
  21.             android:label=“@string/app_name”  
  22.             android:theme=“@style/AppTheme.NoActionBar” >  
  23.             <intent-filter>  
  24.                 <action android:name=“android.intent.action.MAIN” />  
  25.                 <category android:name=“android.intent.category.LAUNCHER” />  
  26.             </intent-filter>  
  27.         </activity>  
  28.         <activity android:name=“com.micky.leakcanarysamples.TestActivity” />  
  29.         <service  
  30.             android:name=“com.squareup.leakcanary.internal.HeapAnalyzerService”  
  31.             android:enabled=“false”  
  32.             android:process=“:leakcanary” />  
  33.         <service  
  34.             android:name=“com.squareup.leakcanary.DisplayLeakService”  
  35.             android:enabled=“false” />  
  36.         <activity  
  37.             android:name=“com.squareup.leakcanary.internal.DisplayLeakActivity”  
  38.             android:enabled=“false”  
  39.             android:icon=“@drawable/__leak_canary_icon”  
  40.             android:label=“@string/__leak_canary_display_activity_label”  
  41.             android:taskAffinity=“com.squareup.leakcanary”  
  42.             android:theme=“@style/__LeakCanary.Base” >  
  43.             <intent-filter>  
  44.                 <action android:name=“android.intent.action.MAIN” />  
  45.                 <category android:name=“android.intent.category.LAUNCHER” />  
  46.             </intent-filter>  
  47.         </activity>  
  48.     </application>  
  49. </manifest>  

如上所示LeakCanary给我们自动添加了两个Service和一个Activity,并添加了对SD卡的读写权限





It 's so simple.

注:

1、如果在Release模式下请使用RefWatcher.DISABLED

2、在Activity或Fragment 的 Destroy方法中添加检测(很好理解,就是判断一个Activity或Fragment想要被销毁的时候,是否还有其他对象持有其引用导致Activity或Fragment不能被回收,从而导致内存泄露)

 

LeakCanary,30分钟从入门到精通

LeakCanary,30分钟从入门到精通

简述

在性能优化中,内存是一个不得不聊的话题;然而内存泄漏,显示已经成为内存优化的一个重量级的方向。当前流行的内存泄漏分析工具中,不得不提的就是LeakCanary框架;这是一个集成方便, 使用便捷,配置超级简单的框架,实现的功能却是*为强大的。


不骗你,真的,使用就是这么简单 ?!

1. 你需要添加到配置的只有这个

dependencies {

debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.3’

releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.3’

}

2. 你肯定需要初始化一下,当然, 推荐在Application中

public class MyApplicationextends Application {

@Override

public void onCreate() {

super.onCreate();

LeakCanary.install(this);

}

}

3. 什么?你还在下一步?已经结束了!通过以上配置,你就可以轻松使用LeakCanary检测内存泄漏了

    关于LeakCanary的详细使用教程,建议去看:LeakCanary中文使用说明


闲聊结束,我想你们来肯定不是看我这些废话的,那么现在进入正题!本篇我们所想的,就是LeakCanary为什么可以这么神奇,它是怎么检测内存泄漏的?下面我们解开谜题!


《LeakCanary原理》  核心类分析

01    LeakCanary                                        源码解析

02    LeakCanary                                        SDK提供类

03    DisplayLeakActivity                           内存泄漏的查看页面

04    HeapAnalyzerService                         内存堆分析服务, 为了保证App进程不会因此受影响变慢&内存溢出,运行于独立的进程

05    HeapAnalyzer                                     分析由RefWatcher生成的堆转储信息, 验证内存泄漏是否真实存在

06    HeapDump                                          堆转储信息类,存储堆转储的相关信息

07    ServiceHeapDumpListener                一个监听,包含了开启分析的方法

08    RefWatcher                                           核心类, 翻译自官方: 检测不可达引用(可能地),当发现不可达引用时,它会触发                                                                         HeapDumper(堆信息转储)

09    ActivityRefWatcher                               Activity引用检测, 包含了Activity生命周期的监听执行与停止

通过以上列表,让大家对LeakCanary框架的主要类有个大体的了解,并基于以上列表,对这个框架的大体功能有一个模糊的猜测。


漫无目的的看源码,很容易迷失在茫茫的Code Sea中,无论是看源码,还是接手别人的项目,都是如此;因此,带着问题与目的性来看这些复杂的东西是很有必要的,也使得我们阅读效率大大提高;想要了解LeakCanary,我们*大的疑惑是什么,我列出来,看看是与你不约而同。

Question1:    在Application中初始化之后,它是如何检测所有的Activity页面的 ?

Question2:    内存泄漏的判定条件是什么 ? 检测内存泄漏的机制原理是什么?

Question3:    检测出内存泄漏后,它又是如何生成泄漏信息的? 内存泄漏的输出轨迹是怎么得到的?

回顾一下这个框架,其实我们想了解的机制不外乎三:

1. 内存泄漏的检测机制

2. 内存泄漏的判定机制

3. 内存泄漏的轨迹生成机制

我们会在源码分析*后,依次回答以上的三个问题,可能在阅读源码之前,我们先要对内存泄漏做一些基础概念与原理的理解。


什么是内存泄漏(MemoryLeak)?

大家对这个概念应该不陌生吧,当我们使用一个Bitmap,使用完成后,没有recycle回收;当我们使用Handler, 在Activity销毁时没有处理;当我们使用Cursor,*后没有close并置空;以上这些都会导致一定程度上的内存泄漏问题。那么,什么是内存泄漏?

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

以上是百度百科的解释,总结下为:内存泄漏是不使用或用完的内存,因为某些原因无法回收,造成的一种内存浪费;内存泄漏的本质是内存浪费。以个人理解来解释,通俗一点就是

1. GC回收的对象必须是当前没有任何引用的对象

2.当对象在使用完成后(对我们而言已经是垃圾对象了), 我们没有释放该对象的引用,导致GC不能回收该对象而继续占用内存

3.垃圾对象依旧占用内存,这块内存空间便浪费了

内存泄漏与内存溢出的区别是什么?

从名称来看,一个泄漏,一个溢出,其实很好理解。

内存泄漏: 垃圾对象依旧占据内存,如水龙头的泄漏,水本来是属于水源的, 但是水龙头没关紧,那么泄漏到了水池;再来看内存,内存本来应                     该被回收,但是依旧在内存堆中;总结一下就是内存存在于不该存在的地方(没用的地方)

内存溢出: 内存占用达到*大值,当需要分配内存时,已经没有内存可以分配了,就是溢出;依旧以水池为例, 水池的水如果满了,那么如果继                      续需要从水龙头流水的话,水就会溢出。总结一下就是,内存的分配超出*大阀值,导致了一种异常

明白了两者的概念,那么两者有什么关系呢?

内存的溢出是内存分配达到了*大值,而内存泄漏是无用内存充斥了内存堆;因此内存泄漏是导致内存溢出的元凶之一,而且是很大的元凶;因为内存分配完后,哪怕占用再大,也会回收,而泄漏的内存则不然;当清理掉无用内存后,内存溢出的阀值也会相应降低。


JVM如何判定一个对象是垃圾对象?

该问题也即垃圾对象搜索算法,JVM采用图论的可达遍历算法来判定一个对象是否是垃圾对象, 如果对象A是可达的,则认为该对象是被引用的,GC不会回收;如果对象A或者块B(多个对象引用组成的对象块)是不可达的,那么该对象或者块则判定是不可达的垃圾对象,GC会回收。

 

%title插图%num

 


以上科普的两个小知识:1) 内存泄漏  2) JVM搜索算法 是阅读LeakCanary源码的基本功,有助于源码的理解与记忆。好了,下面来看一下LeakCanary的源码,看看LeakCanary是怎么工作的吧!

既然LeakCanary的初始化是从install()开始的,那么从init开始看

%title插图%num
%title插图%num

回顾一下核心类模块可知,内存分析模块是在独立进程中执行的,这么设计是为了保证内存分析过程不会对App进程造成消*的影响,如使App进程变慢或导致out of Memory问题等。因此

*步: 判断APP进程与内存分析进程是否属于同一进程;如果是, 则返回空的RefWatcher DISABLED;如果不是,往下走,看第二步

%title插图%num
是否与分析进程是同一个, isInAnalyzerProcess
%title插图%num
与分析进程一致,返回一个空的DISABLED

第二步: enableDisplayLeakActivity 开启显示内存泄漏信息的页面

 

%title插图%num
调用了setEnabled,记住参数DisplayLeakActivity就是查看泄漏的页面,enable传true,开启Activity组件

 

%title插图%num
这个方法的作用是设置四大组件开启或禁用,从上图传的参数看,是开启了查看内存泄漏的页面

第三步:初始化一个ServiceHeapDumpListener,这是一个开启分析的接口实现类,类中定义了analyze方法,用于开启一个DisplayLeakService服务,从名字就可以看出,这是一个显示内存泄漏的辅助服务

%title插图%num
看注释,这个服务的作用是分析HeapDump,写入一个记录文件,并弹出一个Notification

第四步:初始化两个Watcher, RefWatcher和ActivityRefWatcher. 这两个Watcher的作用分别为分析内存泄漏与监听Activity生命周期

%title插图%num
ActivityRefWatcher监听Activity生命周期,在初始化时开始监听Activity生命周期(watchActivities)
%title插图%num
watchActivities中注册了所有Activity的生命周期统一监听;onActiityDestroy会在onDestroy时执行,执行watch,检测内存泄漏

 

通过以上代码分析,我们可以得出*个问题的答案。LeakCanary通过ApplicationContext统一注册监听的方式,来监察所有的Activity生命周期,并在Activity的onDestroy时,执行RefWatcher的watch方法,该方法的作用就是检测本页面内是否存在内存泄漏问题。


下面我们继续来分析核心类RefWatcher中的源码,检测机制的核心逻辑便在RefWatcher中;相信阅读完这个类后,第二个问题的答案便呼之欲出了。

既然想弄明白RefWatcher做了什么,那么先来看一下官方的解释

%title插图%num
监听可能不可达的引用,当RefWatcher判定一个引用可能不可达后,会触发HeapDumper(堆转储)

从上面图可以看出官方的解释。 RefWatcher是一个引用检测类,它会监听可能会出现泄漏(不可达)的对象引用,如果发现该引用可能是泄漏,那么会将它的信息收集起来(HeapDumper).

从RefWatcher源码来看,核心方法主要有两个: watch() 和 ensureGone()。如果我们想单独监听某块代码,如fragment或View等,我们需要手动去调用watch()来检测;因为上面讲过,默认的watch()仅执行于Activity的Destroy时。watch()是我们直接调用的方法,ensureGone()则是具体如何处理了,下面我们来看一下

%title插图%num
watch 检测核心方法

上图为watch()的源码, 我们先来看一下官方的注释

监听提供的引用,检查该引用是否可以被回收。这个方法是非阻塞的,因为检测功能是在Executor中的异步线程执行的

 

从上述源码可以看出,watch里面只是执行了一定的准备工作,如判空(checkNotNull), 为每个引用生成一个唯一的key, 初始化KeyedWeakReference;关键代码还是在watchExecutor中异步执行。引用检测是在异步执行的,因此这个过程不会阻塞线程。

 

%title插图%num
检测核心代码 gone()判定WeakReference中是否包含当前引用

以上是检测的核心代码实现,从源码可以看出,检测的流程:

1) 移除不可达引用,如果当前引用不存在了,则不继续执行

2) 手动触发GC操作,gcTrigger中封装了gc操作的代码

3) 再次移除不可达引用,如果引用不存在了,则不继续执行

4) 如果两次判定都没有被回收,则开始分析这个引用,*终生成HeapDump信息

总结一下原理:

1. 弱引用与ReferenceQueue联合使用,如果弱引用关联的对象被回收,则会把这个弱引用加入到ReferenceQueue中;通过这个原理,可以看出removeWeaklyReachableReferences()执行后,会对应删除KeyedWeakReference的数据。如果这个引用继续存在,那么就说明没有被回收。

2. 为了确保*大保险的判定是否被回收,一共执行了两次回收判定,包括一次手动GC后的回收判定。两次都没有被回收,很大程度上说明了这个对象的内存被泄漏了,但并不能100%保证;因此LeakCanary是存在*小程度的误差的。

 

上面的代码,总结下流程就是

判定是否回收(KeyedWeakReference是否存在该引用), Y -> 退出, N -> 向下执行

手动触发GC

判定是否回收, Y -> 退出, N-> 向下执行

两次未被回收,则分析引用情况:

1) humpHeap :  这个方法是生成一个文件,来保存内存分析信息 

2) analyze: 执行分析

通过以上的代码分析,第二个问题的答案已经浮出水面了吧!


接下来分析内存泄漏轨迹的生成~

*终的调用,是在RefWatcher中的ensureGone()中的*后,如图

%title插图%num
分析*终调用,在ensureGone()中

很明显,走的是heapdumpListener中的analyze方法,继续追踪heapdumpListener是在LeakCanary初始化的时候初始化并传入RefWatcher的,如图

%title插图%num
在install中初始化并传入RefWatcher

打开进入ServiceHeapDumpListener,看里面实现,如图

%title插图%num
ServiceHeapDumpListener中的analyze

调用了HeapAnalyzerService,在单独的进程中进行分析,如图

%title插图%num
HeapAnalyzerService分析进程

HeapAnalyzerService中通过HeapAnalyzer来进行具体的分析,查看HeapAnalyzer源码,如图

%title插图%num
HeapAnalyzer

进行分析时,调用了openSnapshot方法,里面用到了SnapshotFactory

%title插图%num
org.eclipse.mat

从上图可以看出,这个版本的LeakCanary采用了MAT对内存信息进行分析,并生成结果。其中在分析时,分为findLeakingReference与findLeakTrace来查找泄漏的引用与轨迹,根据GCRoot开始按树形结构依次建议当前引用的轨迹信息。


通过上述分析,*终得出的结果为:

1. Activity检测机制是什么?

答: 通过application.registerActivityLifecycleCallbacks来绑定Activity生命周期的监听,从而监控所有Activity; 在Activity执行onDestroy时,开始检测当前页面是否存在内存泄漏,并分析结果。因此,如果想要在不同的地方都需要检测是否存在内存泄漏,需要手动添加。

2. 内存泄漏检测机制是什么?

答: KeyedWeakReference与ReferenceQueue联合使用,在弱引用关联的对象被回收后,会将引用添加到ReferenceQueue;清空后,可以根据是否继续含有该引用来判定是否被回收;判定回收, 手动GC, 再次判定回收,采用双重判定来确保当前引用是否被回收的状态正确性;如果两次都未回收,则确定为泄漏对象。

3. 内存泄漏轨迹的生成过程 ?

答: 该版本采用eclipse.Mat来分析泄漏详细,从GCRoot开始逐步生成引用轨迹。

 

通过整篇文章分析,你还在疑惑么?

Android开发常见的Activity中内存泄漏及解决办法

上一篇文章楼主提到由Context引发的内存泄漏,在这一篇文章里,我们来谈谈Android开发中常见的Activity内存泄漏及解决办法。本文将会以“为什么”“怎么解决”的方式来介绍这几种内存泄漏。
在开篇之前,先来了解一下什么是内存泄漏。

什么是内存泄漏?

内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。内存泄漏并不是指物理上的内存消失,这里的内存泄漏是值由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

怎样会导致内存泄漏?

  • 资源对象没关闭造成的内存泄漏,如查询数据库后没有关闭游标cursor
  • 构造Adapter时,没有使用 convertView 重用
  • Bitmap对象不在使用时调用recycle()释放内存
  • 对象被生命周期长的对象引用,如activity被静态集合引用导致activity不能释放

在接下来的篇幅里,我们重点讲有关Activity常见的内存泄漏。

内存泄漏1:静态Activities(static Activities)

代码如下:
MainActivity.Java

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }
}

LeakCanary检测出的内存泄漏:

这里写图片描述

为什么?
在上面代码中,我们声明了一个静态的Activity变量并且在TextView的OnClick事件里引用了当前正在运行的Activity实例,所以如果在activity的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的activity是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

怎么解决?
*简单的方法是在onDestory方法中将静态变量activity置空,这样垃圾回收器就可以将静态变量回收。

@Override
    protected void onDestroy() {
        super.onDestroy();
        activity = null;
        //使用LeakCanary观察是否有内存泄漏
        MyApplication.getRefWatcher().watch(this);
    }

内存泄漏2:静态View

代码如下:
MainActivity.java

    ...
    private static View view;
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

LeakCanary检测到的内存泄漏

这里写图片描述

为什么?
上面代码看似没有问题,在Activity里声明一个静态变量view,然后初始化,当Activity生命周期结束了内存也释放了,但是LeakCanary却显示出现了内存泄漏,为什么?问题出在这里,View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。

怎么解决?
在onDestroy方法里将静态变量置空。

@Override
protected void onDestroy() {
    super.onDestroy();
    view = null;
    MyApplication.getRefWatcher().watch(this);
} 

内存泄漏3:内部类

代码如下:
MainActivity.java

private static Object inner;
void createInnerClass() {
    class InnerClass {
    }
    inner = new InnerClass();
}

View icButton = findViewById(R.id.ic_button);
icButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createInnerClass();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
非静态内部类会持有外部类的引用,在上面代码中内部类持有Activity的引用,因此inner会一直持有Activity,如果Activity生命周期结束没有清除这个引用,这样就发生了内存泄漏。

怎么解决?
因为非静态内部类隐式持有外部类的强引用,所以我们将内部类声明成静态的就可以了。

void createInnerClass() {
    static class InnerClass {
    }
    inner = new InnerClass();
}

内存泄漏4:匿名类

void startAsyncTask() {
    new AsyncTask<Void, Void, Void>() {
        @Override protected Void doInBackground(Void... params) {
            while(true);
        }
    }.execute();
}

super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
View aicButton = findViewById(R.id.at_button);
aicButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        startAsyncTask();
        nextActivity();
    }
});

使用LeakCanary检测到的内存泄漏:

这里写图片描述

为什么?
上面代码在activity中创建了一个匿名类AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,因此如果你在Activity里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

怎么解决?
自定义静态AsyncTask类,并且让AsyncTask的周期和Activity周期保持一致,也就是在Activity生命周期结束时要将AsyncTask cancel掉。

内存泄漏5:Handler

代码如下:
MainActivity.java

...
void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, 1000);
}

...
View hButton = findViewById(R.id.h_button);
hButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        createHandler();
        nextActivity();
    }
});
...

为什么?
当Android Application启动以后,framework会首先帮助我们完成UI线程的消息循环,也就是在UI线程中,Loop、MessageQueue、Message等等这些实例已经由framework帮我们实现了。所有的Application主要事件,比如Activity的生命周期方法、Button的点击事件都包含在这个Message里面,这些Message都会加入到MessageQueue中去,所以,UI线程的消息循环贯穿于整个Application生命周期,所以当你在UI线程中生成Handler的实例,就会持有Loop以及MessageQueue的引用。并且在Java中非静态内部类和匿名内持有外部类的引用,而静态内部类则不会持有外部类的引用。

怎么解决?
可以由上面的结论看出,产生泄漏的根源在于匿名类持有Activity的引用,因此可以自定义Handler和Runnable类并声明成静态的内部类,来解除和Activity的引用。

内存泄漏6:Thread

代码如下:
MainActivity.java

void spawnThread() {
    new Thread() {
        @Override public void run() {
            while(true);
        }
    }.start();
}

View tButton = findViewById(R.id.t_button);
tButton.setOnClickListener(new View.OnClickListener() {
  @Override public void onClick(View v) {
      spawnThread();
      nextActivity();
  }
});

为什么?
同AsyncTask一样,这里就不过多赘述。

怎么解决?
那我们自定义Thread并声明成static这样可以吗?其实这样的做法并不推荐,因为Thread位于GC根部,DVM会和所有的活动线程保持hard references关系,所以运行中的Thread*不会被GC无端回收了,所以正确的解决办法是在自定义静态内部类的基础上给线程加上取消机制,因此我们可以在Activity的onDestroy方法中将thread关闭掉。

内存泄漏7:Timer Tasks

代码如下:
MainActivity.java

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    },1000);
}

View ttButton = findViewById(R.id.tt_button);
ttButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        scheduleTimer();
        nextActivity();
    }
});

为什么?
这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。

怎么解决?
在适当的时机进行Cancel。

内存泄漏8:Sensor Manager

代码如下:
MainActivity.java

void registerListener() {
       SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
       Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
       sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
}

View smButton = findViewById(R.id.sm_button);
smButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
        registerListener();
        nextActivity();
    }
});

为什么?
通过Context调用getSystemService获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果Context对象需要在一个Service内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个Activity,如果开发者忘记了在Activity被销毁前注销这个监听器,这样就导致内存泄漏。

怎么解决?
在onDestroy方法里注销监听器。

总结

在开发中,内存泄漏*坏的情况是app耗尽内存导致崩溃,但是往往真实情况不是这样的,相反它只会耗尽大量内存但不至于闪退,可分配的内存少了,GC便会更多的工作释放内存,GC是非常耗时的操作,因此会使得页面卡顿。我们在开发中一定要注意当在Activity里实例化一个对象时看看是否有潜在的内存泄漏,一定要经常对内存泄漏进行检测。

LeakCanary: 让内存泄露无所遁形

LeakCanary: 让内存泄露无所遁形

java.lang.OutOfMemoryError
        at android.graphics.Bitmap.nativeCreate(Bitmap.java:-2)
        at android.graphics.Bitmap.createBitmap(Bitmap.java:689)
        at com.squareup.ui.SignView.createSignatureBitmap(SignView.java:121)

谁也不会喜欢 OutOfMemoryError

在 Square Register 中, 在签名页面,我们把客户的签名画在 bitmap cache 上。 这个 bitmap 的尺寸几乎和屏幕的尺寸一样大,在创建这个 bitmap 对象时,经常会引发 OutOfMemoryError,简称OOM

%title插图%num

当时,我们尝试过一些解决方案,但都没解决问题

  • 使用 Bitmap.Config.ALPHA_8 因为,签名仅有黑色。
  • 捕捉 OutOfMemoryError, 尝试 GC 并重试(受 GCUtils 启发)。
  • 我们没想过在 Java heap 内存之外创建 bitmap 。苦逼的我们,那会 Fresco 还不存在。

路子走错了

其实 bitmap 的尺寸不是真正的问题,当内存吃紧的时候,到处都有可能引发 OO。在创建大对象,比如 bitmap 的时候,更有可能发生。OOM 只是一个表象,更深层次的问题可能是: 内存泄露

什么是内存泄露

一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。

比如,当 Activity.onDestroy 被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这*终将会导致内存耗尽,然后因为 OOM 而 crash。

对战内存泄露

排查内存泄露是一个全手工的过程,这在 Raizlabs 的 Wrangling Dalvik 系列文章中有详细描述。

以下几个关键步骤:

  1. 通过 Bugsnag, Crashlytics 或者 Developer Console 等统计平台,了解 OutOfMemoryError 情况。
  2. 重现问题。为了重现问题,机型非常重要,因为一些问题只在特定的设备上会出现。为了找到特定的机型,你需要想尽一切办法,你可能需要去买,去借,甚至去偷。 当然,为了确定复现步骤,你需要一遍一遍地去尝试。一切都是非常原始和粗暴的。
  3. 在发生内存泄露的时候,把内存 Dump 出来。具体看这里。
  4. 然后,你需要在 MAT 或者 YourKit 之类的内存分析工具中反复查看,找到那些原本该被回收掉的对象。
  5. 计算这个对象到 GC roots 的*短强引用路径。
  6. 确定引用路径中的哪个引用是不该有的,然后修复问题。

很复杂对吧?

如果有一个类库能在发生 OOM 之前把这些事情全部都搞定,然后你只要修复这些问题就好了,岂不妙哉!

LeakCanary

LeakCanary 是一个检测内存泄露的开源类库。你可以在 debug 包种轻松检测内存泄露。

先看一个例子:

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);
  }
}

然后你会在通知栏看到这样很漂亮的一个界面:

%title插图%num

结论

使用 LeakCanary 之后,我们修复了我们 APP 中相当多的内存泄露。我们甚至发现了 Android SDK 中的一些内存泄露问题。

结果是惊艳的,我们减少了 94% 的由 OOM 导致的 crash。

%title插图%num

如果你也想消灭 OOM crash,那还犹豫什么,赶快使用 LeakCanary

LeakCanary-展现Android中的内存泄露

之前碰到的OOM问题,终于很直白的呈现在我的眼前:我尝试了MAT,但是发现不怎么会用。直到今天终于发现了这个新工具:

当我们的App中存在内存泄露时会在通知栏弹出通知:

这里写图片描述

当点击该通知时,会跳转到具体的页面,展示出Leak的引用路径,如下图所示:

这里写图片描述

LeakCanary 可以用更加直白的方式将内存泄露展现在我们的面前。

以下是我找到的学习资料,写的非常棒:
1、LeakCanary: 让内存泄露无所遁形
2、LeakCanary 中文使用说明

AndroidStudio (官方)上使用LeakCanary 请移步:
https://github.com/square/leakcanary

Eclipse 上使用LeakCanary 请移步我的:
https://github.com/SOFTPOWER1991/LeakcanarySample-Eclipse

Android studio (自己弄的)上使用LeakCanary也可以看这个:

leakcanarySample_androidStudio

工程包括:

  1. LeakCanary库代码
  2. LeakCanaryDemo示例代码

使用步骤:

  1. 将LeakCanary import 入自己的工程
  2. 添加依赖:

    compile project(':leakcanary')

  3. 在Application中进行配置
    public class ExampleApplication extends Application {
    
      ......
      //在自己的Application中添加如下代码
    public static RefWatcher getRefWatcher(Context context) {
        ExampleApplication application = (ExampleApplication) context
                .getApplicationContext();
        return application.refWatcher;
    }
    
      //在自己的Application中添加如下代码
    private RefWatcher refWatcher;
    
    @Override
    public void onCreate() {
        super.onCreate();
        ......
            //在自己的Application中添加如下代码
        refWatcher = LeakCanary.install(this);
        ......
    }
    
    .....
    }
    
  4. 在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. 在AndroidMainfest.xml 中进行配置,添加如下代码
        <service
            android:name="com.squareup.leakcanary.internal.HeapAnalyzerService"
            android:enabled="false"
            android:process=":leakcanary" />
        <service
            android:name="com.squareup.leakcanary.DisplayLeakService"
            android:enabled="false" />

        <activity
            android:name="com.squareup.leakcanary.internal.DisplayLeakActivity"
            android:enabled="false"
            android:icon="@drawable/__leak_canary_icon"
            android:label="@string/__leak_canary_display_activity_label"
            android:taskAffinity="com.squareup.leakcanary"
            android:theme="@style/__LeakCanary.Base" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

 

LeakCanary 中文使用说明

 


LeakCanary

Android 和 Java 内存泄露检测。

“A small leak will sink a great ship.” – Benjamin Franklin

千里之堤, 毁于蚁穴。 — 《韩非子·喻老》

%title插图%num

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'
 }

在 Application 中:

public class ExampleApplication extends Application {

  @Override public void onCreate() {
    super.onCreate();
    LeakCanary.install(this);
  }
}

这样,就万事俱备了! 在 debug build 中,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知。

为什么需要使用 LeakCanary?

问得好,看这个文章LeakCanary: 让内存泄露无所遁形

如何使用

使用 RefWatcher 监控那些本该被回收的对象。

RefWatcher refWatcher = {...};

// 监控
refWatcher.watch(schrodingerCat);

LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。

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);
  }
}

工作机制

  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, 并以通知的形式展示出来。

如何复制 leak trace?

在 Logcat 中,你可以看到类似这样的 leak trace:

In com.example.leakcanary:1.0:1 com.example.leakcanary.MainActivity has leaked:

* GC ROOT thread java.lang.Thread.<Java Local> (named 'AsyncTask #1')
* references com.example.leakcanary.MainActivity$3.this$0 (anonymous class extends android.os.AsyncTask)
* leaks com.example.leakcanary.MainActivity instance

* Reference Key: e71f3bf5-d786-4145-8539-584afaecad1d
* Device: Genymotion generic Google Nexus 6 - 5.1.0 - API 22 - 1440x2560 vbox86p
* Android Version: 5.1 API: 22
* Durations: watch=5086ms, gc=110ms, heap dump=435ms, analysis=2086ms

你甚至可以通过分享按钮把这些东西分享出去。

SDK 导致的内存泄露

随着时间的推移,很多SDK 和厂商 ROM 中的内存泄露问题已经被尽快修复了。但是,当这样的问题发生时,一般的开发者能做的事情很有限。

LeakCanary 有一个已知问题的忽略列表,AndroidExcludedRefs.java,如果你发现了一个新的问题,请提一个 issue 并附上 leak trace, reference key, 机器型号和 SDK 版本。如果可以附带上 dump 文件的 链接那就再好不过了。

对于*新发布的 Android,这点尤其重要。你有机会在帮助在早期发现新的内存泄露,这对整个 Android 社区都有*大的益处。

开发版本的 Snapshots 包在这里: Sonatype’s snapshots repository。

leak trace 之外

有时,leak trace 不够,你需要通过 MAT 或者 YourKit 深挖 dump 文件。

通过以下方法,你能找到问题所在:

  1. 查找所有的 com.squareup.leakcanary.KeyedWeakReference 实例。
  2. 检查 key 字段
  3. Find the KeyedWeakReference that has a key field equal to the reference key reported by LeakCanary.
  4. 找到 key 和 和 logcat 输出的 key 值一样的 KeyedWeakReference
  5. referent 字段对应的就是泄露的对象。
  6. 剩下的,就是动手修复了。*好是检查到 GC root 的*短强引用路径开始。

自定义

UI 样式

DisplayLeakActivity 有一个默认的图标和标签,你只要在你自己的 APP 资源中,替换以下资源就可。

res/
  drawable-hdpi/
    __leak_canary_icon.png
  drawable-mdpi/
    __leak_canary_icon.png
  drawable-xhdpi/
    __leak_canary_icon.png
  drawable-xxhdpi/
    __leak_canary_icon.png
  drawable-xxxhdpi/
    __leak_canary_icon.png
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <string name="__leak_canary_display_activity_label">MyLeaks</string>
</resources>

保存 leak trace

DisplayLeakActivity saves up to 7 heap dumps & leak traces in the app directory. You can change that number by providing R.integer.__leak_canary_max_stored_leaks in your app:

在 APP 的目录中,DisplayLeakActivity 保存了 7 个 dump 文件和 leak trace。你可以在你的 APP 中,定义 R.integer.__leak_canary_max_stored_leaks 来覆盖类库的默认值。

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <integer name="__leak_canary_max_stored_leaks">20</integer>
</resources>

上传 leak trace 到服务器

你可以改变处理完成的默认行为,将 leak trace 和 heap dump 上传到你的服务器以便统计分析。

创建一个 LeakUploadService, *简单的就是继承 DisplayLeakService :

public class LeakUploadService extends DisplayLeakService {
  @Override
  protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
    if (!result.leakFound || result.excludedLeak) {
      return;
    }
    myServer.uploadLeakBlocking(heapDump.heapDumpFile, leakInfo);
  }
}

请确认 release 版本 使用 RefWatcher.DISABLED

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 = installLeakCanary();
  }

  protected RefWatcher installLeakCanary() {
    return RefWatcher.DISABLED;
  }
}

自定义 RefWatcher

public class DebugExampleApplication extends ExampleApplication {
  protected RefWatcher installLeakCanary() {
    return LeakCanary.install(app, LeakUploadService.class);
  }
}

别忘了注册 service:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    >
  <application android:name="com.example.DebugExampleApplication">
    <service android:name="com.example.LeakUploadService" />
  </application>
</manifest>
%title插图%num

LeakCanary工具使用

LeakCanary工具使用

添加LeakCanary依赖包
https://github.com/square/leakcanary
在主模块app下的build.gradle下添加如下依赖:
debugCompile ‘com.squareup.leakcanary:leakcanary-android:1.3.1’
releaseCompile ‘com.squareup.leakcanary:leakcanary-android-no-op:1.3.1’

%title插图%num

开启LeakCanary
添加Application子类
首先创建一个ExampleApplication,该类继承于Application,在该类的onCreate方法中添加如下代码开启LeakCanary监控:
LeakCanary.install(this);

%title插图%num

在配置文件中注册ExampleApplication
在AndroidManifest.xml中的application标签中添加如下信息:
android:name=”.ExampleApplication”

%title插图%num

这个时候安装应用到手机,会自动安装一个Leaks应用,如下图:

%title插图%num
制造一个内存泄漏的点
建立一个ActivityManager类,单例模式,里面有一个数组用来保存Activity:
package com.example.android.sunshine.app;
import android.app.Activity;
import android.util.SparseArray;
import android.view.animation.AccelerateInterpolator;
import java.util.List;
public class ActivityManager {
private SparseArray container = new SparseArray();
private int key = 0;
private static ActivityManager mInstance;
private ActivityManager(){}
public static ActivityManager getInstance(){
if(mInstance == null){
mInstance = new ActivityManager();
}
return mInstance;
}

public void addActivity(Activity activity){
container.put(key++,activity);
}

}
然后在DetailActivity中的onCreate方法中将当前activity添加到ActivityManager的数组中:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
ActivityManager.getInstance().addActivity(this);
if (savedInstanceState == null) {
// Create the detail fragment and add it to the activity
// using a fragment transaction.

Bundle arguments = new Bundle();
arguments.putParcelable(DetailFragment.DETAIL_URI, getIntent().getData());

DetailFragment fragment = new DetailFragment();
fragment.setArguments(arguments);

getSupportFragmentManager().beginTransaction()
.add(R.id.weather_detail_container, fragment)
.commit();
}
}

我们从首页跳转到详情页的时候会进入DetailActivity的onCreate的方法,然后就将当前activity添加到了数组中,当返回时,我们没把他从数组中删除。再次进入的时候,会创建新的activity 并添加到数组中,但是之前的activity仍然被引用,无法释放,但是这个activity不会再被使用,这个时候就造成了内存泄漏。我们来看看LeakCanary是如何报出这个问题的。
演示

%title插图%num

解析的过程有点耗时,所以需要等待一会才会在Leaks应用中,当我们点开某一个信息时,会看到详细的泄漏信息:

%title插图%num

————————————————

LeakCanary原理解析

简介

LeakCanary是一款开源的内存泄漏检查工具,在项目中,可以使用它来检测Activity是否能够被GC及时回收。github的地址为https://github.com/square/leakcanary

使用方式解析

将LeakCanary引入AS,在Application中调用如下方法,可以跟踪Activity是否被GC回收。

%title插图%num

入口函数

LeakCanary.install()方法的调用流程如下所示:

%title插图%num

install方法调用流程

Install方法如下:

%title插图%num

install方法

其中listenerServiceClass方法传入了展示分析结果的Service(DisplayLeakService);excludedRefs方法排除了开发中可以忽略的泄漏路径;buildAndInstall是主要的函数,实现了activity是否能被释放的监听。

%title插图%num

buildAndInstall

buildAndInstall会调用ActivityRefWatcher.install来监测Activity。

%title插图%num

install

*终调用了watchActivities():

%title插图%num

watchActivities

通过registerActivityLifecycleCallbacks来监听Activity的生命周期:

%title插图%num

lifecycleCallbacks

lifecycleCallbacks监听Activity的onDestroy方法,正常情况下activity在onDestroy后需要立即被回收,onActivityDestroyed方法*终会调用RefWatcher.watch方法:

%title插图%num

watch

监测机制利用了Java的WeakReference和ReferenceQueue,通过将Activity包装到WeakReference中,被WeakReference包装过的Activity对象如果被回收,该WeakReference引用会被放到ReferenceQueue中,通过监测ReferenceQueue里面的内容就能检查到Activity是否能够被回收。检查方法如下:

%title插图%num

ensureGone

1、  首先通过removeWeaklyReachablereference来移除已经被回收的Activity引用

2、 通过gone(reference)判断当前弱引用对应的Activity是否已经被回收,如果已经回收说明activity能够被GC,直接返回即可。

3、  如果Activity没有被回收,调用GcTigger.runGc方法运行GC,GC完成后在运行第1步,然后运行第2步判断Activity是否被回收了,如果这时候还没有被回收,那就说明Activity可能已经泄露。

4、  如果Activity泄露了,就抓取内存dump文件(Debug.dumpHprofData)

%title插图%num

dumpHeap

5、  之后通过HeapAnalyzerService.runAnalysis进行分析内存文件分析

%title插图%num

分析dump

接着通过HeapAnalyzer(checkForLeak—findLeakingReference—findLeakTrace)来进行内存泄漏分析。

6、  *后通过DisplayLeakService进行内存泄漏的展示。

LeakCanary原理分析

导语:

提到Java语言的特点,无论是教科书还是程序员一般都会罗列出面向对象、可移植性及安全等特点。但如果你是一位刚从C/C++转到Java的程序员,对Java语言的特性除了面向对象之外,*外直接的应当是在Java虚拟机(JVM)在内存管理方面给我们变成带来的便利。JVM的这一大特性使Java程序员从繁琐的内存管理工作中得到了一定解放,但是JVM的这个特点的实现也是有代价的,并且它也并非万能。因此如果一个编程习惯不好的Java程序员如果完全将内存回收寄希望于JVM,那么OOM(Out Of Memory)就已经悄悄潜伏在了他的程序之中。

Android应用基于Java实现,因此它也将Java的优缺点继承了过来。相对来说,移动设备对于内存问题更为敏感,程序在申请一定的内存但又没有及时得到释放后就很容易发生OOM而导致crash。因此Android程序员开发过程中一般都会定时排查自己程序中可能出现的这些雷点,尽可能地避免因为crash问题而影响用户体验。

1.LeakCanary简介

目前Java程序*常用的内存分析工具应该是MAT(Memory Analyzer Tool),它是一个Eclipse插件,同时也有单独的RCP客户端,也可以通过官网的SVN下载到它的源码(具体见另一篇《compile-MAT》)并编译成jar包。LeakCanary本质上就是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,通过集成这个工具代码到自己的Android工程当中就能够在程序调试开发过程中通过一个漂亮的界面(如下图)随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

LeakCanary_result

总的来说,LeakCanary有如下几个明显优点:

  • 针对Android Activity组件完全自动化的内存泄漏检查。
  • 可定制一些行为(dump文件和leaktrace对象的数量、自定义例外、分析结果的自定义处理等)。
  • 集成到自己工程并使用的成本很低。
  • 友好的界面展示和通知。

假如你现在想集成LeakCanary到自己的工程中,那么你只需要做以下工作:1. 导入leakcanary的jar包到自己工程(下载链接:leakcanary.zip)2. 在4.0以上,只需要在工程的Application的onCreate函数中按照如下的方式加入一行代码:

  1. public class ExampleApplication extends Application {
  2. @Override
  3. public void onCreate() {
  4. super.onCreate();
  5. LeakCanary.install(this);
  6. }
  7. }

4.0以下在需要进行内存泄漏监控的Activity的onDestroy方法中按如下加入代码:

  1. protected void onDestroy() {
  2. super.onDestroy();
  3. // start watch
  4. HeapDump.Listener heapDumpListener =
  5. new ServiceHeapDumpListener(this, listenerServiceClass);
  6. DebuggerControl debuggerControl = new AndroidDebuggerControl();
  7. AndroidHeapDumper heapDumper = new AndroidHeapDumper();
  8. heapDumper.cleanup();
  9. ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults().build();
  10. RefWatcher refWatcher = new RefWatcher(new AndroidWatchExecutor(), debuggerControl, GcTrigger.DEFAULT,
  11. heapDumper, heapDumpListener, excludedRefs);
  12. }   第二种情况下,在有多个Activity需要检测的情况看起来稍显繁琐,实际上可以用以上方法实现一个基类Activity,之后需要内存泄漏检测的Activity直接继承这个基类Activity就不需要每次都重复处理oonDestroy方法了。并且以上代码只作为示例,实际上每次watch的时候并不需要重新new一个RefWatcher对象,因为这个对象是可以重复使用的。

完成了以上两个步骤后,LeakCanary就可以为你的工程服务了,这之中需要我们自己处理的工作很少,相比较我们自己手工用MAT进行内存泄漏检测而言,确实方便了很多。## 2.LeakCanary原理分析 ##这么强大的工具,它是如何实现的呢,引用LeakCanary中文使用说明,它的基本工作原理如下:

  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, 并以通知的形式展示出来。

但事实上一切并没那么简单,LeakCanary的设计者在实现的时候实际上为我们考虑了很多细节。可以通过源码分析来走一遍一次内存泄漏检查的流程。在一个Activity生命周期结束调用oonDestroy方法的时候会触发LeakCanary进行一次内存泄漏检查,LeakCanary开始进行检查的入口函数实际上是RefWatcher类的,watch方法,其源码如下:

  1. public void watch(Object watchedReference, String referenceName) {
  2. String key = UUID.randomUUID().toString();
  3. retainedKeys.add(key);
  4. final KeyedWeakReference reference = new KeyedWeakReference(watchedReference, key, referenceName, queue);
  5. watchExecutor.execute(new Runnable() {
  6. @Override
  7. public void run() {
  8. ensureGone(reference, watchStartNanoTime);
  9. }
  10. });
  11. } 这个函数做的主要工作就是为需要进行泄漏检查的Activity创建一个带有唯一key标志的弱引用,并将这个弱引用key保存至retainedKeys中,然后将后面的工作交给watchExecutor来执行。这个watchExecutor在LeakCanary中是AndroidWatchExecutor的实例,调用它的execute方法实际上就是向主线程的消息队列中插入了一个IdleHandler消息,这个消息只有在对应的消息队列为空的时候才会去执行,因此RefWatcher的watch方法就保证了在主线程空闲的时候才会去执行ensureGone方法,防止因为内存泄漏检查任务而严重影响应用的正常执行。ensureGone的主要源码如下:
  12. void ensureGone(KeyedWeakReference reference, long watchStartNanoTime) {
  13. removeWeaklyReachableReferences();
  14. if (gone(reference) || debuggerControl.isDebuggerAttached()) {
  15. return;
  16. }
  17. gcTrigger.runGc(); // 手动执行一次gc
  18. removeWeaklyReachableReferences();
  19. if (!gone(reference)) {
  20. long startDumpHeap = System.nanoTime();
  21. long gcDurationMs = NANOSECONDS.toMillis(startDumpHeap – gcStartNanoTime);
  22. File heapDumpFile = heapDumper.dumpHeap();
  23. if (heapDumpFile == null) {
  24. // Could not dump the heap, abort.
  25. Log.d(TAG, “Could not dump the heap, abort.”);
  26. return;
  27. }
  28. long heapDumpDurationMs = NANOSECONDS.toMillis(System.nanoTime() – startDumpHeap);
  29. heapdumpListener.analyze(new HeapDump(heapDumpFile, reference.key, reference.name, excludedRefs,
  30. watchDurationMs, gcDurationMs, heapDumpDurationMs));
  31. }
  32. } 因为这个方法是在主线程中执行的,因此一般执行到这个方法中的时候之前被destroy的那个Activity的资源应该被JVM回收了,因此这个方法首先调用removeWeaklyReachableReferences方法来将引用队列中存在的弱引用从retainedKeys中删除掉,这样,retainedKeys中保留的就是还没有被回收对象的弱引用key。之后再用gone方法来判断我们需要检查的Activity的弱引用是否在retainedKeys中,如果没有,说明这个Activity对象已经被回收,检查结束。否则,LeakCanary主动触发一次gc,再进行以上两个步骤,如果发现这个Activity还没有被回收,就认为这个Activity很有可能泄漏了,并dump出当前的内存文件供之后进行分析。

之后的工作就是对内存文件进行分析,由于这个过程比较耗时,因此*终会把这个工作交给运行在另外一个进程中的HeapAnalyzerService来执行。HeapAnalyzerService通过调用HeapAnalyzer的checkForLeak方法进行内存分析,其源码如下:

  1. public AnalysisResult checkForLeak(File heapDumpFile, String referenceKey) {
  2. ISnapshot snapshot = null;
  3. try {
  4. snapshot = openSnapshot(heapDumpFile);
  5. IObject leakingRef = findLeakingReference(referenceKey, snapshot);
  6. // False alarm, weak reference was cleared in between key check and heap dump.
  7. if (leakingRef == null) {
  8. return noLeak(since(analysisStartNanoTime));
  9. }
  10. String className = leakingRef.getClazz().getName();
  11. AnalysisResult result =
  12. findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, true);
  13. if (!result.leakFound) {
  14. result = findLeakTrace(analysisStartNanoTime, snapshot, leakingRef, className, false);
  15. }
  16. return result;
  17. } catch (SnapshotException e) {
  18. return failure(e, since(analysisStartNanoTime));
  19. } finally {
  20. cleanup(heapDumpFile, snapshot);
  21. }
  22. }

这个方法进行的*步就是利用HAHA将之前dump出来的内存文件解析成Snapshot对象,其中调用到的方法包括SnapshotFactory的parse和HprofIndexBuilder的fill方法。解析得到的Snapshot对象直观上和我们使用MAT进行内存分析时候罗列出内存中各个对象的结构很相似,它通过对象之间的引用链关系构成了一棵树,我们可以在这个树种查询到各个对象的信息,包括它的Class对象信息、内存地址、持有的引用及被持有的引用关系等。到了这一阶段,HAHA的任务就算完成,之后LeakCanary就需要在Snapshot中找到一条有效的到被泄漏对象之间的引用路径。首先它调用findLeakTrace方法来从Snapshot中找到被泄漏对象,源码如下:

  1. private IObject findLeakingReference(String key, ISnapshot snapshot) throws SnapshotException {
  2. Collection<IClass> refClasses =
  3. snapshot.getClassesByName(KeyedWeakReference.class.getName(), false);
  4. if (refClasses.size() != 1) {
  5. throw new IllegalStateException(
  6. “Expecting one class for “ + KeyedWeakReference.class.getName() + ” in “ + refClasses);
  7. }
  8. IClass refClass = refClasses.iterator().next();
  9. int[] weakRefInstanceIds = refClass.getObjectIds();
  10. for (int weakRefInstanceId : weakRefInstanceIds) {
  11. IObject weakRef = snapshot.getObject(weakRefInstanceId);
  12. String keyCandidate =
  13. PrettyPrinter.objectAsString((IObject) weakRef.resolveValue(“key”), 100);
  14. if (keyCandidate.equals(key)) { // 匹配key
  15. return (IObject) weakRef.resolveValue(“referent”); // 定位泄漏对象
  16. }
  17. }
  18. throw new IllegalStateException(“Could not find weak reference with key “ + key);
  19. }

为了能够准确找到被泄漏对象,LeakCanary通过被泄漏对象的弱引用来在Snapshot中定位它。因为,如果一个对象被泄漏,一定也可以在内存中找到这个对象的弱引用,再通过弱引用对象的referent就可以直接定位被泄漏对象。下一步的工作就是找到一条有效的到被泄漏对象的*短的引用,这通过findLeakTrace来实现,实际上寻找*短路径的逻辑主要是封装在PathsFromGCRootsComputerImpl这个类的getNextShortestPath和processCurrentReferrefs这两个方法当中,其源码如下:

  1. public int[] getNextShortestPath() throws SnapshotException {
  2. switch (state) {
  3. case 0: // INITIAL
  4. {
  5. }
  6. case 1: // FINAL
  7. return null;
  8. case 2: // PROCESSING GC ROOT
  9. {
  10. }
  11. case 3: // NORMAL PROCESSING
  12. {
  13. int[] res;
  14. // finish processing the current entry
  15. if (currentReferrers != null) {
  16. res = processCurrentReferrefs(lastReadReferrer + 1);
  17. if (res != null) return res;
  18. }
  19. // Continue with the FIFO
  20. while (fifo.size() > 0) {
  21. currentPath = fifo.getFirst();
  22. fifo.removeFirst();
  23. currentId = currentPath.getIndex();
  24. currentReferrers = inboundIndex.get(currentId);
  25. if (currentReferrers != null) {
  26. res = processCurrentReferrefs(0);
  27. if (res != null) return res;
  28. }
  29. }
  30. return null;
  31. }
  32. default:
  33. }
  34. }
  35. private int[] processCurrentReferrefs(int fromIndex) throws SnapshotException {
  36. GCRootInfo[] rootInfo = null;
  37. for (int i = fromIndex; i < currentReferrers.length; i++) {
  38. }
  39. for (int referrer : currentReferrers) {
  40. if (referrer >= 0 && !visited.get(referrer) && !roots.containsKey(referrer)) {
  41. if (excludeMap == null) {
  42. fifo.add(new Path(referrer, currentPath));
  43. visited.set(referrer);
  44. } else {
  45. if (!refersOnlyThroughExcluded(referrer, currentId)) {
  46. fifo.add(new Path(referrer, currentPath));
  47. visited.set(referrer);
  48. }
  49. }
  50. }
  51. }
  52. return null;
  53. }
  54. }

为了是逻辑更清晰,在这里省略了对GCRoot的处理。这个类将整个内存映像信息抽象成了一个以GCRoot为根的树,getNextShortestPath的状态3是对一般节点的处理,由于之前已经定位了被泄漏的对象在这棵树中的位置,为了找到一条到GCRoot*短的路径,PathsFromGCRootsComputerImpl采用的方法是类似于广度优先的搜索策略,在getNextShortestPath中从被泄漏的对象开始,调用一次processCurrentReferrefs将持有它引用的节点(父节点),加入到一个FIFO队列中,然后依次再调用getNextShortestPath和processCurrentReferrefs来从FIFO中取节点及将这个节点的父节点再加入FIFO队列中,一层一层向上寻找,哪条路径*先到达GCRoot就表示它应该是一条*短路径。由于FIFO保存了查询信息,因此如果要找次*短路径只需要再调用一次getNextShortestPath触发下一次查找即可,其算法原理如下图所示。

LeakCanary_result

至此,主要的工作就完成了,后面就是调用buildLeakTrace构建查询结果,这个过程相对简单,仅仅是将之前查找的*短路径转换成*后需要显示的LeakTrace对象,这个对象中包括了一个由路径上各个节点LeakTraceElement组成的链表,代表了检查到的*短泄漏路径。*后一个步骤就是将这些结果封装成AnalysisResult对象然后交给DisplayLeakService进行处理。这个service主要的工作是将检查结果写入文件,以便之后能够直接看到*近几次内存泄露的分析结果,同时以notification的方式通知用户检测到了一次内存泄漏。使用者还可以继承这个service类来并实现afterDefaultHandling来自定义对检查结果的处理,比如将结果上传刚到服务器等。

以上就是对LeakCanary源码的分析,中间省略了一些细节处理的说明,但不得不提的是LeakCanary支持自定义泄漏豁对象ExcludedRefs的集合,这些豁免对象一般都是一些已知的系统泄漏问题或者自己工程中已知但又需要被排除在检查之外的泄漏问题构成的。LeakCanary在findLeakTrace方法中如果发现这个集合中的对象存在于泄漏路径上,就会排除掉这条泄漏路径并尝试寻找下一条。

LeakCanary原理及使用

一、LeakCanary简介
LeakCanary是Square公司为Android开发者提供的一个自动检测内存泄漏的工具,LeakCanary本质上是一个基于MAT进行Android应用程序内存泄漏自动化检测的的开源工具,我们可以通过集成LeakCanary提供的jar包到自己的工程中,一旦检测到内存泄漏,LeakCanary就会dump Memory信息,并通过另一个进程分析内存泄漏的信息并展示出来,随时发现和定位内存泄漏问题,而不用每次在开发流程中都抽出专人来进行内存泄漏问题检测,*大地方便了Android应用程序的开发。

二、LeakCanary工作机制
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
在另外一个进程中的 HeapAnalyzerService 有一个HeapAnalyzer 使用HAHA 解析这个文件。
得益于唯一的 reference key, HeapAnalyzer 找到KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算到 GC roots的*短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。
三、LeakCanary的Android Studio集成
1. 在build.gradle中添加LeakCanary的依赖包
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:1.6.3’
releaseImplementation ‘com.squareup.leakcanary:leakcanary-android-no-op:1.6.3’

注意: debug和release版本要一致,否则会报错。

2. 在我们自定义Application的onCreate方法中注册LeakCanary
public class LeakApplication extends Application {
@Override public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) { //1
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
}
}

注释1处的代码用来进行过滤操作,如果当前的进程是用来给LeakCanary 进行堆分析的则return,否则会执行LeakCanary的install方法。这样我们就可以使用LeakCanary了,如果检测到某个Activity 有内存泄露,LeakCanary 就会给出提示。

3. 重写Application
上述代码只能够检测Activity的内存泄漏,当然还存在其他类的内存泄漏,这时我们就需要使用RefWatcher来进行监控。重写Application,如下所示:

public class LeakApplication extends Application {
private RefWatcher refWatcher;
@Override
public void onCreate() {
super.onCreate();
refWatcher= setupLeakCanary();
}
private RefWatcher setupLeakCanary() {
if (LeakCanary.isInAnalyzerProcess(this)) {
return RefWatcher.DISABLED;
}
return LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
return leakApplication.refWatcher;
}
}

install方法会返回RefWatcher用来监控对象,LeakApplication中还要提供getRefWatcher静态方法来返回全局RefWatcher。
注意: 需要在AndroidManifest.xml文件中添加android:name=”.LeakApplication”,指定Application子类,当应用启动时,这个类的实例被*个创建。这个属性是可选的,大多数APP都不需要这个属性。在没有这个属性的时候,Android会启动一个Application类的实例。

%title插图%num
4. 在activity或fragment中使用leak canary举例
public class SearchActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
LeakThread leakThread = new LeakThread();
leakThread.start();
}
class LeakThread extends Thread {
@Override
public void run() {
try {
Thread.sleep(6 * 60 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = LeakApplication.getRefWatcher(this);//1
refWatcher.watch(this);
}
}

SearchActivity存在内存泄漏,原因就是非静态内部类LeakThread持有外部类SearchActivity的引用,LeakThread中做了耗时操作,导致SearchActivity无法被释放。
在注释1处得到RefWatcher,并调用它的watch方法,watch方法的参数就是要监控的对象。当然,在这个例子中onDestroy方法是多余的,因为LeakCanary在调用install方法时会启动一个ActivityRefWatcher类,它用于自动监控Activity执行onDestroy方法之后是否发生内存泄露。这里只是为了方便举例,如果想要监控Fragment,在Fragment中添加如上的onDestroy方法是有用的。

public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}

运行程序,这时会在界面生成一个名为Leaks的应用图标。接下来不断的切换横竖屏,这时会闪出一个提示框,提示内容为:“Dumping memory, app will freeze.Brrrr.”。再稍等片刻,内存泄漏信息就会通过Notification展示出来,比如荣耀magic的通知栏如下图1所示。
Notification中提示了SearchActivity发生了内存泄漏。点击Notification就可以进入内存泄漏详细页,除此之外也可以通过Leaks应用的列表界面进入,列表界面如下图2所示。内存泄漏详细页如下图3所示。
点击加号就可以查看具体类所在的包名称。整个详情就是一个引用链:SearchActivity的内部类LeakThread引用了LeakThread的this$0,this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是详情*后一行所给出的SearchActivity的实例,这将会导致SearchActivity无法被GC(garbage collection,垃圾回收),从而产生内存泄漏。
图1:%title插图%num

图2:%title插图%num

图3:%title插图%num

解决该内存泄露的方法就是将LeakThread改为静态内部类。再次运行程序LeakThread就不会给出内存泄漏的提示了。


static class LeakThread extends Thread {

}

四、LeakCanary2使用
1. 和 LeakCanary1 相比,LeakCanary2 有以下改动
完全使用 Kotlin 重写。
使用新的Heap分析工具Shark,替换到之前的haha,按官方的说法,内存占用减少了10倍。
泄露类型分组。
2. LeakCanary2集成
只需要增加以下依赖即可:

debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.2’
1
LeakCanary2 实现了自动调用 install() 方法,实现方式是使用的 ContentProvider,相关代码位于 leakcanary-object-watcher-android 模块中的 AppWatcherInstaller.kt 中。
AppWatcherInstaller 继承 ContentProvider,重写了 onCreate() 方法,这里利用的是,注册在 Manifest 文件中的 ContentProvider,会在应用启动时,由 ActivityThread 创建并初始化。
————————————————

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速