Android 性能优化(五)Crash治理之OOM,内存泄漏检测工具

系列推荐:

Android性能优化(一)闪退治理、卡顿优化、耗电优化、APK瘦身

Android 性能优化(二)Handler运行机制原理,源码分析  

Android 性能优化(三)认识错误Error和异常Exception及栈轨迹StackTrace

Android 性能优化(四)Crash治理之路,UncaughtException

前言

性能优化第一篇中的Crash治理说过:“出现Crash应用闪退和崩溃一般有三个原因:ANR(程序无响应)、Exception(异常)、LMK(低内存杀死机制)。”

上一篇Exception异常的捕捉和处理方式主要讲解如何打造一款不会Crash的APP,已经讲完了,这一篇该说一说程序无法拦截处理的“LMK”问题了。其实它属于虚拟机抛出的error,这个在系列第三篇讲过,error是程序无法进行拦截和处理的。因此,唯一的解决的办法就只能是防范。

那么该如何防范?出问题了如何检测?如何解决?这篇文将会带大家对此方向,有一个恍然开朗的认识。

Android 性能优化(五)Crash治理之OOM,内存泄漏检测工具_第1张图片

一、LMK 和 OOM

KMK,(Low Memory Killer )低内存杀死机制。由于Android应用的沙箱机制,每个应用程序都运行在一个独立的进程中,各自拥有独立的Dalvik虚拟机实例,系统默认分配给虚拟机的内存是有限度的,当系统内存太低依然会触发LMK机制,即出现闪退、崩溃现象。

不同厂商不同,如:华为mate7,192m ;小米4,128m ;红米,128m 。而在,Android4.0以后,可以通过在application节点中设置属性android:largeHeap=”true”来设置最大可分配多少内存空间就可以突破一定限制。

 

OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草。导致OOM的两个主要原因:

  1、内存泄漏,大量无用对象未及时回收,导致后续申请内存失败。

  2、BitMap大对象,几个大图同时加载很容易触发OOM。

为了更好探究如何避免内存紧张出现Crash的情况,我们就先研究一些内存的回收机制。

 

二、JAVA GC机制

垃圾回收器—GC(Garbage Collection),它与“java面向编程”一样是Java语言的特性之一。

GC 主要是处理 Java堆(Heap) ,也就是处理在Java虚拟机用于存放对象实例的内存区域。JVM能够完成内存分配和内存回收,虽然降低了开发难度,避免了像C/C++直接操作内存的危险。但也正因为太过于依赖JVM去完成内存管理,导致很多Java开发者不再关心内存分配,导致很多程序低效、耗内存问题。

因此开发者需要主动了解GC机制,充分利用有限的内存,才能写出更高效的程序。

那么,GC怎么回收的?

Java垃圾回收机制的最基本的做法就是分代回收。内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。一般的实现是划分成三个年代:年轻、年老、永久。内存的分配是发生在年轻世代中的,当一个对象存活的时间够久的时候,它就会被复制到老年代中。

对于不同世代可以使用不同的垃圾回收算法。进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。一般来说,一个应用中的大部分对象的存活时间都很短。比如局部变量的存活时间就只在方法的执行过程中。因为年轻世代的对象很快会进入不可达状态,因此要求回收频率高且回收速度快,基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。如下:

年轻代“复制式”回收算法:划分两个区域,分别是Eden 区和 Survive 区。大多数对象先分配到Eden区,内存大的对象会直接被分配到老年代中;Survive 区又分Form、To两个小区,一个用来保存对象,另一个是空。每次进行年轻代垃圾回收的时候,就把E大区和From小区中的可达对象都复制到To区域中,一些生存时间长的就直接复制到了老年代。最后,清理回收E大区和From小区的内存空间,原来的To空间变为From空间,原来的From空间变为To空间。

有没有看了上面一大堆文字后,两眼冒金星的感觉?!哎哎哎!~  其实很简单,大致就是说对象被分配到了堆中,堆中又分了一些小的内存区域,会根据对象自身的存活时长进行分配。

不可达状态,什么意思?

  • 可达状态:在一个对象创建后,有一个以上的引用变量引用它,那它就处于可达状态。
  • 可恢复状态:对象不再有任何的引用变量引用它,它将先进入可恢复状态,系统会调用finalize()方法进行资源整理,发现有一个以上引用变量引用该对象,则这个对象又再次变为可达状态,否则会变成不可达状态。
  • 不可达状态:当对象的所有引用都被切断,且系统调用 finalize() 方法进行资源整理后该对象依旧没变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。

当某些对象是可达状态时,但程序以后不会再使用了,它们仍然占用内存不会被GC所回收,就造成了内存泄漏。一般是由错误的程序代码逻辑引起的。在Android平台上,最常见也是最严重的内存泄漏就是Activity对象泄漏。Activity承载了App的整个界面功能,Activity的泄漏同时也意味着它持有的大量资源对象都无法被回收,极其容易造成OOM。

 

三、防止内存泄漏

 

1. 长生命周期对象持有 Activity

这基本是最常见的内存泄漏了,比如

  • 内部类形式使用 Handler 同时发送延时消息,或者在 Handler 里面执行耗时任务,在任务还没完成的时候 Activity 需要销毁。这时候由于 Handler 持有 Activity 的强引用导致 Activity 无法被回收。
  • 同理内部类形式的使用 AsyncTask 执行耗时任务也会导致内存泄漏的发生。
  • 单例作为最长生命周期的对象,自然不应该持有 Activity 从而导致内存泄漏发生;

针对上面这种情况,基本不必多说了,不要使用内部类或者匿名内部类做这样的处理就好了,实际上 IDE 也会弹出警告,我想大家应该还是都知道采用静态内部类或者在销毁页面的时候使用相关方法移除处理的。实际上,使用 Kotlin 或者 Java 8 的 Lambda 表达式同样不会导致内存泄漏的发生,这是因为实际上它也是使用的静态内部类,没有持有外部引用。

Activity 中匿名使用 Handler 实际上会导致 Handler 内部类持有外部类的引用,而 SendMessage() 的时候 Message 会持有 HandlerenqueueMessage 机制又会导致 MeassageQueue 持有 Message。所以当发送的是延迟消息那么 Message 并不会立即的遍历出来处理而是阻塞到对应的 Message 触发时间以后再处理。那么阻塞的这段时间中页面销毁一定会造成内存泄漏。

2. 各种注册操作没有对应的反注册

这一点基本不必多说,相信大家刚刚开始学习广播和 Service 的时候一定对此有所接触,然后就是比如我们常用的第三方框架 EventBus 也是一样的。平时使用的时候注意在对应的生命周期方法中进行反注册。

3. Bitmap大对象 使用完没有注意 recycle()

Bitmap 作为大对象,在使用完毕一定要注意调用 recycle()方法 进行回收。TypedArray 、Cursor、各种流同理,一定要在最后调用自己的回收关闭方法处理。

随着手机屏幕尺寸越来越大,屏幕分辨率也越来越高,1080p和更高的2k屏已经占了大半份额,为了达到更好的视觉效果,我们往往需要使用大量高清图片,同时也为OOM埋下了祸根。对于图片内存优化,我们有几个常用的思路:

  尽量使用成熟的图片库,比如Glide,图片库会提供很多通用方面的保障,减少不必要的人为失误。根据实际需要,也就是View尺寸来加载图片,可以在分辨率较低的机型上尽可能少地占用内存。

4. WebView 使用不当

WebView 是非常常用的控件,但稍有不注意也会导致内存泄漏。内存泄漏的场景: 很多人使用 Webview 都喜欢采用布局引用方式, 这其实也是作为内存泄漏的一个隐患。当 Activity 被关闭时,Webview 不会被 GC 马上回收,而是提交给事务,进行队列处理,这样就造成了内存泄漏, 导致 Webview 无法及时回收。

目前所知的比较安全的方案是:

  • 在布局中动态添加 WebView。
  • 采用下面的方法。
override fun onDestroy() {
    webView?.apply {
        val parent = parent
        if (parent is ViewGroup) {
            parent.removeView(this)
        }
        stopLoading()
        // 退出时调用此方法,移除绑定的服务,否则某些特定系统会报错
        settings.javaScriptEnabled = false
        clearHistory()
        removeAllViews()
        destroy()
    }
}

5. 循环引用

循环引用导致内存泄漏比较少见,正常来讲不会有人写出 A 持有 B,B 持有 C,C 又持有A 这样的代码,不过总还是需要注意。

总的来说,内存泄漏很常见,但分析App内存的详细情况是解决问题的第一步,我们需要对App运行时到底占用了多少内存、哪些类型的对象有多少个有大致了解,并根据实际情况做出预测,这样才能在分析时做到有的放矢。Android Studio也提供了非常好用的Memory Profiler,堆转储和分配跟踪器功能可以帮我们迅速定位问题。

四、Memory Monitor 工具

Android Studio自带的一个内存监视工具,它可以很好地帮助我们进行内存实时分析。通过点击Android Studio右下角的Memory Monitor标签,打开工具可以看见较浅蓝色代表free的内存,而深色的部分代表使用的内存从内存变换的走势图变换,可以判断关于内存的使用状态,例如当内存持续增高时,可能发生内存泄漏;当内存突然减少时,可能发生GC等,如下图所示。

五、Memory Analyzer 工具:

    MAT(Memory Analyzer Tool) 是一个快速,功能丰富的 Java Heap 分析工具,通过分析 Java 进程的内存快照 HPROF 分析,从众多的对象中分析,快速计算出在内存中对象占用的大小,查看哪些对象不能被垃圾收集器回收,并可以通过视图直观地查看可能造成这种结果的对象。

    检测步骤如下:案例参考

(a)屏幕多次翻转,出现内存持续增高时。点击 Dump java Heap就会生成运行内存快照hprof文件。

(b)然后将APP完全退出,重新启动,打开Android Monitor 再次点击Dump java Heap 生成一份还没操作(旋转屏幕)前的内存快照hprof文件。现在就已经生成好了2份hprof文件, 一份是没有旋转过屏幕的 ,一份是旋转过屏幕多次的。


(c)然后选中Android Studio 最左边的Captures 进行将hprof文件导出。导出的时候需要选择保存的目录以及文件名。

 d)打开MAT ,导入我们的2个hprof文件 Open File-->选择文件-->Leak Suspects Report-->Finish:*

可以通过检索包名,查看某个类的实例个数和所在内存数据,还可以查看被引用的内存数据。如下:

 

Objects:实例个数
Shallow Heap:所占内存大小
Retained Heap:释放后能回收多少内存


六、LeakCanary工具:

    简单,傻瓜式操作,最重要的是LeakCanary 只在debug版本下检测,正版先上线后自动跳过检测这就方便开发者无需操作每次上线时注释检测代码。这个工具是Square公司在Github开源的。行业内不是有一句话嘛,Square出品必属精品,主流的库像okhttp、Picasso、retrofit、Dagger等都出自Square之手。说到这不得不让我联想到一位在Android开发领域神一般存在的人物,他就是大名鼎鼎的Jake Wharton(杰克.沃顿),ButterKnife的创造者,也参与贡献了Retrofit, okhttp等。

如何使用?GitHup官网https://github.com/square/leakcanary,首先在Gradle文件里添加依赖。

在Application中写方法:
 

    private RefWatcher setupLeakCanary() {
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }
 
    public static RefWatcher getRefWatcher(Context context) {
        MyApplication leakApplication = (MyApplication) context.getApplicationContext();
        return leakApplication.refWatcher;
    }

然后onCreate()中调用即可:refWatcher = setupLeakCanary();

在Activity中单独调用检测:

@Override
    protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = MyApplication.getRefWatcher(this);//1
        refWatcher.watch(this);
    }

你可能感兴趣的:(Android)