Android 内存泄露和性能检测

  
  

Android Studio的内存分析界面

一般分析内存泄露, 首先运行程序,打开日志控制台,有一个标签Memory ,我们可以在这个界面分析当前程序使用的内存情况, 一目了然, 我们再也不需要苦苦的在logcat中寻找内存的日志了。

图中蓝色区域,就是程序使用的内存, 灰色区域就是空闲内存

当然,Android内存分配机制是对每个应用程序逐步增加, 比如你程序当前使用30M内存, 系统可能会给你分配40M, 当前就有10M空闲, 如果程序使用了50M了,系统会紧接着给当前程序增加一部分,比如达到了80M, 当前你的空闲内存就是30M了。 当然,系统如果不能再给你分配额外的内存,程序自然就会OOM(内存溢出)了。 每个应用程序最高可以申请的内存和手机密切相关,比如我当前使用的华为Mate7,极限大概是200M,算比较高的了, 一般128M 就是极限了, 甚至有的手机只有可怜的16M或者32M,这样的手机相对于内存溢出的概率非常大了。

我们怎么检测内存泄露呢

首先需要明白一个概念, 内存泄露就是指,本应该回收的内存,还驻留在内存中。一般情况下,高密度的手机,一个页面大概就会消耗20M内存,如果发现退出界面,程序内存迟迟不降低的话,可能就发生了严重的内存泄露。我们可以反复进入该界面,然后点击dump java heap 这个按钮,然后Android Studio就开始干活了,下面的图就是正在dump

正在dump

dump成功后会自动打开 hprof文件,文件以Snapshot+时间来命名

内存分析结果

通过Android Studio自带的界面,查看内存泄露还不是很智能,我们可以借助第三方工具,常见的工具就是MAT了,下载地址 http://eclipse.org/mat/downloads.php ,这里我们需要下载独立版的MAT. 下图是MAT一开始打开的界面, 这里需要提醒大家的是,MAT并不会准确地告诉我们哪里发生了内存泄漏,而是会提供一大堆的数据和线索,我们需要自己去分析这些数据来去判断到底是不是真的发生了内存泄漏。

   
   

LeakCanary

上面介绍了MAT检测内存泄露, 再给大家介绍LeakCanary。 项目地址:https://github.com/square/leakcanaryLeakCanary 会检测应用的内存回收情况,如果发现有垃圾对象没有被回收,就会去分析当前的内存快照,也就是上边MAT用到的.hprof文件,找到对象的引用链,并显示在页面上。这款插件的好处就是,可以在手机端直接查看内存泄露的地方,可以辅助我们检测内存泄露.

手机端查看内存泄露.png

使用: 在build.gradle文件中添加,不同的编译使用不同的引用:

dependencies {  
      debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3' 
      releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
 }

在应用的Application onCreate方法中添加LeakCanary.install(this),如下:

public class ExampleApplication extends Application  
  @Override  
  public void onCreate() { 
      super.onCreate();  
      LeakCanary.install(this);
   } 
}

应用运行起来后,LeakCanary会自动去分析当前的内存状态,如果检测到泄漏会发送到通知栏,点击通知栏就可以跳转到具体的泄漏分析页面。Tips:就目前使用的结果来看,绝大部分泄漏是由于使用单例模式hold住了Activity的引用,比如传入了context或者将Activity作为listener设置了进去,所以在使用单例模式的时候要特别注意,还有在Activity生命周期结束的时候将一些自定义监听器的Activity引用置空。

作者:于连林520wcf链接:https://www.jianshu.com/p/216b03c22bb8來源:简书著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

是什么?

一言以蔽之:LeakCanary是一个傻瓜化并且可视化的内存泄露分析工具

为什么需要LeakCanary?

因为它简单,易于发现问题,人人可参与。

  • 简单:只需设置一段代码即可,打开应用运行一下就能够发现内存泄露。而MAT分析需要Heap Dump,获取文件,手动分析等多个步骤。
  • 易于发现问题:在手机端即可查看问题即引用关系,而MAT则需要你分析,找到Path To GC Roots等关系。
  • 人人可参与:开发人员,测试测试,产品经理基本上只要会用App就有可能发现问题。而传统的MAT方式,只有部分开发者才有精力和能力实施。

如何集成

尽量在app下的build.gradle中加入以下依赖

1
2
3
4
5
 dependencies {
   debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
   releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
   testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
 }

在Application中加入类似如下的代码

1
2
3
4
5
6
7
public class ExampleApplication extends Application {

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

到这里你就可以检测到Activity的内容泄露了。其实现原理是设置Application的ActivityLifecycleCallbacks方法监控所有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
32
33
34
35
public final class ActivityRefWatcher {
    private final ActivityLifecycleCallbacks lifecycleCallbacks = new ActivityLifecycleCallbacks() {
        public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
        }

        public void onActivityStarted(Activity activity) {
        }

        public void onActivityResumed(Activity activity) {
        }

        public void onActivityPaused(Activity activity) {
        }

        public void onActivityStopped(Activity activity) {
        }

        public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
        }

        public void onActivityDestroyed(Activity activity) {
            ActivityRefWatcher.this.onActivityDestroyed(activity);
        }
    };
    private final Application application;
    private final RefWatcher refWatcher;

    public static void installOnIcsPlus(Application application, RefWatcher refWatcher) {
        if(VERSION.SDK_INT >= 14) {
            ActivityRefWatcher activityRefWatcher = new ActivityRefWatcher(application, refWatcher);
            activityRefWatcher.watchActivities();
        }
    }
....
}

想要检测更多?

首先我们需要获得一个RefWatcher,用来后续监控可能发生泄漏的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyApplication extends Application {
    private static RefWatcher sRefWatcher;


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

    public static RefWatcher getRefWatcher() {
        return sRefWatcher;
    }
}

监控某个可能存在内存泄露的对象

1
MyApplication.getRefWatcher().watch(sLeaky);

哪些需要进行监控

默认情况下,是对Activity进行了检测。另一个需要监控的重要对象就是Fragment实例。因为它和Activity实例一样可能持有大量的视图以及视图需要的资源(比如Bitmap)即在Fragment onDestroy方法中加入如下实现

1
2
3
4
5
6
7
public class MainFragment extends Fragment {
    @Override
    public void onDestroy() {
        super.onDestroy();
        MyApplication.getRefWatcher().watch(this);
    }
}

其他也可以监控的对象

  • BroadcastReceiver
  • Service
  • 其他有生命周期的对象
  • 直接间接持有大内存占用的对象(即Retained Heap值比较大的对象)

何时进行监控

首先,我们需要明确什么是内存泄露,简而言之,某个对象在该释放的时候由于被其他对象持有没有被释放,因而造成了内存泄露。

因此,我们监控也需要设置在对象(很快)被释放的时候,如Activity和Fragment的onDestroy方法。

一个错误示例,比如监控一个Activity,放在onCreate就会大错特错了,那么你每次都会收到Activity的泄露通知。

如何解决

常用的解决方法思路如下

  • 尽量使用Application的Context而不是Activity的
  • 使用弱引用或者软引用
  • 手动设置null,解除引用关系
  • 将内部类设置为static,不隐式持有外部的实例
  • 注册与反注册成对出现,在对象合适的生命周期进行反注册操作。
  • 如果没有修改的权限,比如系统或者第三方SDK,可以使用反射进行解决持有关系

加入例外

有些特殊情况,我们需要忽略一些问题,这时候就需要添加例外规则。比如ExampleClass.exampleField会导致内存泄漏,我们想要忽略,如下操作即可。

1
2
3
4
5
6
7
8
9
// ExampleApplication is defined in "Customizing and using the no-op dependency"
public class DebugExampleApplication extends ExampleApplication {
  protected RefWatcher installLeakCanary() {
    ExcludedRefs excludedRefs = AndroidExcludedRefs.createAppDefaults()
        .instanceField("com.example.ExampleClass", "exampleField")
        .build();
    return LeakCanary.install(this, DisplayLeakService.class, excludedRefs);
  }
}

如何实现的

LeakCanary实际上就是在本机上自动做了Heap dump,然后对生成的hprof文件分析,进行结果展示。和手工进行MAT分析步骤基本一致。

如何不影响对外版APK

是的,这个问题确实值得关注,因为LeakCanary确实是影响程序运行的,尤其是heap dump操作,不过好在这件事Square已经考虑了,即在我们增加依赖时

1
2
3
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1' // or 1.4-beta1
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1
testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3.1' // or 1.4-beta1

其中releaseCompile和testCompile这两个的依赖明显不同于debugCompile的依赖。它们的依赖属于NOOP操作。

NOOP,即No Operation Performed,无操作指令。常用的编译器技术会检测无操作指令并出于优化的目的将无操作指令剔除。

因而,只要配置好releaseCompile和testCompile的依赖,就无需担心对外版本的性能问题了。

实践中的问题

  • 目前LeakCanary已经完美支持运行时权限,大家可以放心使用。

注意

  • 目前LeakCanary一次只能报一个泄漏问题,如果存在内存泄漏但不是你的模块,并不能说明这个模块没有问题。建议建议将非本模块的泄漏解决之后,再进行检测。
转载:http://droidyue.com/blog/2016/03/28/android-leakcanary/
Android内存泄漏解决方案(OOM)

为什么会有内存泄漏?

一个不会被使用的对象,因为另一个正在使用的对象持有该对象的引用,导致它不能正常被回收,而停留在堆内存中,内存泄漏就产生了

Android系统为每个应用分配的内存是有限的,内存泄漏会使我们的应用内存随着时间不断的增加,造成应用OOM(Out Of Memory)错误,使应用崩溃.

如何解决内存泄漏?

当我们在解决内存泄漏的时候常常使用 LeakCanary工具,它是一个自动检测内存泄漏的开源工具,使用它我们就可以明确的知道那个地方发生了泄漏

持有Context造成的内存泄漏

在Android中有两种context对象:Activity和Application.当我们给一个类传递context的时候经常使用第一种,而这样就导致了改类持有对Activity的全部引用,当Activity关闭的时候因为被其他类持有,而导致无法正常被回收,而导致内存泄漏

解决方案:

在给其他给传递context的时候使用Application对象,这个对象的生命周期和共存亡,而不依赖activity的声明周期.  而对context的引用不要超过他本身的生命周期,谨慎对context使用static关键字.

Handler造成的内存泄漏

  public class SampleActivity extends Activity {
  private final Handler mLeakyHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
      // ... 
    }
  }
}

这样来使用Handler会造成严重的内存泄漏. 
假设Hanlder中有延迟的任务或是等在执行的任务队列过长,由于消息队列持有对handler的引用,而handler又持有activity的隐式引用,这个引用会保持到消息得到处理,而导致activity无法被垃圾回收器进行回收,而导致内存泄漏

解决方案:

  1. 可以把Handler放到单独的类中,或者使用静态的内部类(静态内部类不会引用activity)避免泄漏
  2. 如果想要在handler内部去调用Activity中的资源,可以在Handler中使用弱引用的方式指向所在的Activity,使用static+WeakReference的方式断开handler与activity的关系

最终代码:

public static class MyHandler extends Handler {
    //声明一个弱引用对象
    WeakReference<MainActivity> mReference;

    MyHandler(MainActivity activity) {
        //在构造器中传入Activity,创建弱引用对象
        mReference = new WeakReference<MainActivity>(activity);
    }

    public void handleMessage(Message msg) {
        //在使用activity之前先判空处理
        if (mReference != null && mReference.get() != null) {
            mReference.get().text.setText("hello word");
        }
    }
}

使用单利模式造成的内存泄漏

在我们使用单利模式的时候如果使用不当也会造成内存泄漏.因为单利模式的静态特征使得单利模式的生命周期和应用一样的长,这说明了当一个对象不需要使用了,而单利对象还存在该对象的引用,那么这个对象就不能正常的被回收,就造成了内存泄漏

解决方案:

XXUtils.getInstance(this);

这句代码默认传入的是Activity的Context,而Activity是间接继承自Context的,当Activity退出之后,单利对象还持有他的引用,所以在为了避免传Activity的Context,在单利中通过传入的context获取到全局的上下文对象,而不使用Activity的Context就解决了这个问题.

public class XXUtils {
    private Context mContext;
    private XXUtils(Context context) {
        mContext = context.getApplicationContext();
    }
    private static XXUtils instance;
    public static XXUtils getInstance(Context context) {
        if (instance == null) {
            synchronized (XXUtils.class) {
                if (instance == null) {
                    instance = new XXUtils(context);
                }
            }
        }
        return instance;
    }
}

非静态内部类创建静态实例造成的内存泄漏

在项目中我们为了避免多次的初始化资源,常常会使用静态对象去保存这些对象,这种情况也很容易引发内存泄漏.

why?

  1. 非静态的内部类默认会持有外部类的引用
  2. 而我们又使用非静态内部类创建了一个静态的实例
  3. 该静态实例的声明周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity不能正常回收

解决方案:

  1. 将内部类修改成静态的,这样它对外部类就没有引用
  2. 将该对象抽取出来封装成一个单利.

最终代码:

private static TestResource mTestResource;
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    findViewById(R.id.btn).setOnClickListener(this);
}
private void initData() {
    if (mTestResource == null) {
        mTestResource = new TestResource();
    }
}
public void onClick(View v) {
    initData();
}
//非静态内部类默认会持有外部类的引用
//修改成就太之后正常被回收,是因为静态的内部类不会对Activity有引用
private static class TestResource {
}

线程造成的内存泄漏

当我们在使用线程的时候,一般都使用匿名内部类,而匿名内部类会对外部类持有默认的引用,当Acticity关闭之后如果现成中的任务还没有执行完毕,就会导致Activity不能正常回收,造成内存泄漏

解决方案

创建一个静态的类,实现Runnable方法,在使用的时候实例化他.

最终代码:

private void loadData() {
    new Thread(new MyThread()).start();
}
private static class MyThread implements Runnable {
    public void run() {
        SystemClock.sleep(20000);
    }
}

资源未关闭造成的内存泄漏

对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的代码,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

监听器没有注销造成的内存泄漏

在Android程序里面存在很多需要register与unregister的监听器,我们需要确保及时unregister监听器。

集合中的内存泄漏

我们通常把一些对象的引用加入到了集合容器(比如ArrayList)中,当我们不需要该对象时, 
并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。 
所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。

转载:http://blog.csdn.net/imuhao/article/details/51694144

猜你喜欢

转载自blog.csdn.net/u012482178/article/details/78988176