内存优化深入版

1,877 阅读14分钟

内存优化之路

最近一直想着自己以后的路如何走,Android的坑位越来越少,对于能力的要求也越来越高。曾想着换一个方向,但是最终都放弃了,毕竟这是自己喜欢的东西。所以,继续下去,不断的在Android方向发展吧。机会是给准备的人的,不断的充实自己,时刻准备着~

进入正题。优化工作是一个开发工程师进阶必备的一种能力。包括内存优化,电量优化,网络优化等等。这些优化所需要的能力,其实是对于各种知识的一种综合运用处理能力。

img

概念

内存优化,是对于应用程序的内存使用、空间占用进行一定的优化处理。

作用

通过内存优化,能够有效的避免内存泄漏、内存溢出、内存占用过大等问题,从而最大程度上保证应用的流畅性、稳定性、以及存活概率

基础知识

内存管理机制

内存的管理,主要是对于进程、对象以及变量分配以及回收

内存区域

Android将内存分为了:堆区、方法区、栈区。其中堆区是内存优化的主要区域。

对象的生命周期以及大小都是有区别的。为了能够更合理的利用堆区,将其又按照生命周期的长短区分为了年轻代、老年代和持久代

img

内存的分配

Android应用在启动时,会通过Zygote进行来孵化应用所需要的进程。当进程创建之后,会交由Framework层来进行托管。

而对于对象和变量的内存分配,采用一种动态内存分配策略,但是并不会说可以无限的增长,会有一个上限的限制。而这个最大值则跟具体的设备有关。毕竟随着手机性能的增加,手机的处理能力更强了,从原来的512M到现在的6G内存,单个应用的可处理能力也在增加。

对于应用中创建的不同对象的具体分配策略,则如下图所示

img

这里强调一下静态分配区域中的静态常量,这种常量会一直存活于程序的整个运行期间。后面我们会讲到这种常量导致的问题。

内存的回收

对于Android系统,依赖gc来自动的执行对于内存的回收工作。而回收工作主要依赖于各种回收算法。在Android的ART虚拟机,用到的算法主要有4种:

img

ART虚拟机会自动的根据实际的情况,自动的选择回收算法来进行内存的回收工作。比如说,当我们的应用处于前台的时候,显示速度对于我们来说是最重要的,这时候ART虚拟机会选择简单的标记清除算法。当应用处于后台的时候,对于速度要求就低一些,这时候可能会采用标记整理算法来进行垃圾的回收工作。

ART虚拟机还具备对于内存的整理能力,从而减少内存空洞的问题

Low Memory Killer 机制

应用进程创建以后,ActivityManagerService就会根据进程的状态计算一个其对应的OomAdj值,然后将这个值传递给Kernel中,kernel存在一个低内存回收机制(LMK)。当内存达到一定阈值时,触发清理OomAdj较高的进程。

img

对于一些后台占用过大的程序,其回收之后的效益最大,其OomAdj对应的值高一些,回收的概率更大。

通过LMK机制能够保证资源的合理利用,防止过大的后台应用影响到前台程序的正常使用。

四种引用

在Java中,对象的引用主要分为四种。

image-20200614133204982

强引用无法回收,当对象用完以后,不移除对应的引用关系,就会导致对象无法回收,而发生内存泄漏等情况。所以在Android中要注意这种问题。

内存问题

对于对象的使用不当的话,可能会导致3种内存问题:内存抖动、内存泄漏,内存溢出

内存抖动

定义

内存抖动是指内存的频繁分配和回收会导致内存不稳定。在内存上通常呈现一种锯齿状频繁进行GC

内存抖动通常会导致应用的页面卡顿,长时间的内存抖动可能会导致应用的OOM。

抖动导致OOM?
  • 当应用在前台运行时,为了保证流畅度,会使用标记清除算法,这种算法会导致大量的内存碎片。
  • 内存碎片可能会因为空间特别小而无法被分配使用,当积少成多的时候,可能会导致OOM

实战解决

对于内存抖动的问题,一般可以使用Memory Profiler来进行排查处理即可处理。

测试案例:

    @SuppressLint("HandlerLeak")
    private static Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            for (int i = 0; i < 100; i++) {
                String[] strings = new String[100000];
            }
            handler.sendEmptyMessageDelayed(0, 100);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        handler.sendEmptyMessage(0);
    }

这里我们模拟了一创建大对象的操作。当程序运行起来以后,我们点击Android Studio中的Profile按钮,就可以看到我们的内存情况。这里看到内存呈现了锯齿状,也就是所谓的内存抖动。

image-20200614154041323

当遇到这种情况的时候,就可以知道发生了内存的抖动,那么就需要查找哪些占用内存比较高。

image-20200614160302585

如上图,通过顺序操作以后,在4这个位置我们发现了创建大批量对象位置是在SplashActivity的handlerMessage的方法中。通过双击就可以跳转到我们上面写的那一部分代码了。

解决技巧

对于内存抖动,应该重点关注:循环或者频繁调用的地方。因为这两个地方很容易造成对象的频繁分配会回收。

常见的内存抖动案例

有一些常见的内存抖动的案例,在我们进行代码编写的时候,应该尽量避免的。

  • 尽量避免在循环体内部创建对象,把对象的创建移到循环体以外。
  • 在自定义View时,onDraw方法会频繁调用,尽量不要在方法内创建对象
  • 对于大对象的使用(Bitmap,线程)等,要使用缓存池技术,通过复用防止频繁的申请和释放。在结束使用的时候,需要手动的释放池中的对象。
  • 当涉及到字符串的变更时,使用StringBuilder来替代,而且要合理化进行初始化。

内存泄漏

定义

虚拟机进行GC时,会选择一些还存活的对象作为GC Roots,然后通过可达性分析来判断对象是否回收。而内存泄漏是指一些不再使用的对象被长期持有,导致内存无法被释放,其本质是因为不再使用的对象被其他生命周期更长的对象所持有。而在内存上的表现则是应用的内存逐渐上升,可用内存逐渐变小

发生内存泄漏会导致我们的内存不足,频繁GC,如果泄漏情况严重,会导致OOM。

实战解决

对于内存泄漏通常会使用Memory Profile+MAT来进行内存泄漏的分析。

这里我们写一个简单的内存泄漏的案例,然后通过案例排查。

    public class CallBackManager {
        private static List<Activity> list=new ArrayList<Activity>();
        public static void addCallBack(Activity activity){
            list.add(activity);
        }
    }
    public class SecondActivity extends AppCompatActivity {
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_sedsd);
            CallBackManager.addCallBack(this);
        }
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_splash);
        findViewById(R.id.iv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(SecondActivity.class);
            }
        });
    }

这里我们准备了两个页面,第二个页面注册回调管理。我们看一下内存的情况。

image-20200614174532108

当我们进入第二个页面,然后退出。经过循环多次的点击之后,内存慢慢的上升了~~~这种情况基本就是内存泄漏导致的。

image-20200614175439168

按照上图的步骤点开以后,会看到内存中存在着18个对应的SecondActivity对象。也就是SecondActivity发生了内存泄漏。但是对于如何发生了泄漏,现在什么实例发生泄漏,在这里是无法看出来的,需要通过MAT来帮助我们定位问题。

在AndroidStudio中,将dump的文件导出。然后通过Android SDK的自带转换工具hprof-conv.exe,将文件转化为能够被MAT识别的hprof文件。转化语句为:

./hprof-conv file.hprof converted.hprof

然后通过MAT打开转化后的.hprof文件。

GIF 2020-6-14 18-49-28

gif可能效果不太好,这里只是演示一下如何去使用MAT,我们看一下最后找到的结果。

image-20200614185316940

这里左边有小圆点的是我们的gcRoot,也就是持有SecondActivity的实例,导致其无法回收的根源。可以看到持有实例的是CallBackManager的list对象

这时候我们就可以回到项目中去找这个对象,然后进行一个处理了。

常见的内存泄漏案例

集合类

集合类添加元素后,会持有集合对象的引用信息,导致集合对象无法回收而内存泄漏。在程序退出,或者集合不再使用的时候,先clear,再置为空。

List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
        // 释放objectList
        objectList.clear();
        objectList=null;

Static修饰的成员变量

Static修饰的成员变量的周期=应用的生命周期。如果static修饰的成员变量引用耗费资源的实例(例如Context),那么当实例结束生命周期的时候,因为静态变量的持有而无法被回收,从而出现内存泄漏。

解决方案:

  • 尽量避免Static修饰的成员变量引用耗费资源过多的实例。如果是Context,尽量使用ApplicationContext
  • 使用*弱引用(WakReference)*代替强引用持有实例。

典型:单例中的静态变量

对于单例中的,可能第二种弱引用并不是很合适,这时候只能使用第一种方案。

非静态内部类/匿名内部类

非静态内部类或者匿名类都会持有外部类的引用。而静态的内部类则不会。

所以如果非静态内部类的实例如果无法回收的话,比如说用Static修饰,也会导致外部类无法释放而内存泄漏。

解决方案:

  • 非静态内部类设置为静态内部类。
  • 将内部类抽取出来封装为一个单例,如果需要使用Context的时候,尽量使用ApplicationContext
  • 尽量避免static修饰的内部非静态类实例对象

如果是Runnable,Thread等匿名类,如果内部耗时工作,那么工作线程实例持有外部类引用,也会造成实例无法回收的问题。

Handler临时性的内存泄漏

Handler在发送出去Message之后,会保存在MessageQueue中。由于Message中会保存着Handler的引用,所以如果是延迟处理的消息,那么很可能导致Handler无法回收,如果再使用了匿名内部类,那么所对应的的Activity实例也就无法回收,从而导致内存泄漏。

解决方案:

  • 使用静态的Handler内部类,然后使用弱引用,在使用的时候,使用get()获取引用的Activity信息进行判空处理。
  • 在Activity进行销毁的时候,通过removeCallbacksAndMessages移除对应的消息队列。
  • Android Weak Handler:可以避免内存泄漏的Handler库,参考:www.jcodecraeer.com/a/anzhuokai…

资源型文件未关闭

对于资源的使用(广播,文件流,数据库游标,图片资源BitMap等),如果不关闭或者注销,那么资源就不会回收,从而导致内存泄漏。应该在资源对象不再使用的时候,调用其close()方法将其关闭,然后再置为null。

WebView

WebView由于其特性问题,只要在应用中使用一次,那么内存就不会进行释放。可以为WebView创建单独的进程,通过AIDL方式和主进程进行通信,当WebView需要销毁时,直接销毁其进程来达到内存释放的目的。

Adapter

在快速滑动ListView或者RecyclerView的时候,会频繁的创建大量对象,不仅浪费资源和时间,内存也会越来越大。可以使用缓存的convertView和ViewHolder来进行缓存。

内存溢出

当内存使用过大的时候,会导致内存的溢出,也就是OOM。这种情况不一定发生在相对应的代码处。内存抖动和内存泄漏都有可能会导致最后的内存溢出。对于内存溢出的情况,除了要考虑内存抖动和内存泄漏的问题,还应该尽量使用合适的类型来优化代码

常见内存优化案:
  • 使用Android优化过后的SparseArray,SparseInt等集合类。代替HashMap。

  • 使用IntDef、StringDef等,来替代枚举。

  • 通过LruCache方法来实现对大资源的缓存功能

  • BitMap优化。位图使用RGB_565或者ARGB_4444。

  • 重写 onTrimMemory/onLowMemory 方法去释放掉图片缓存、静态缓存来自保

  • RecyclView和ListView不可见时,释放掉对图片的

    • ListView的3级缓存:在ImageView的DetachFromWindows时释放掉

    • RecyclerView的5级缓存:在放入到mRecyclerPool时回收(重写Adapter的onViewRecycled方法)

  • 其他

    • 使用基本数据类型

    • 使用For增强

    • 使用软引用和弱引用

    • 采用内存缓存或者磁盘缓存

    • 对于创建复杂的使用池技术

    • 尽量使用静态内部类

优化工具

哪怕对于常见的内存泄漏案例了如指掌,但是肯定还是会难免出现内存泄漏现象。这时候我们就需要借用工具来检测内存的泄漏情况了。最常用的工具分别

  • MAT
  • Memory Profile
  • LeakCanary

对于前两种工具,我们在之前的案例中进行了一部分的功能展示,这里我们做一些总结的信息。

Memory Profiler:
  • 实时的内存使用情况,方便直观

  • 识别内存抖动、泄漏等,

  • 提供堆转储、强制GC以及跟踪内存分配的能力

  • 线下平时使用

Memory Analyzer(MAT)
  • 强大的Java Heap 分析工具,查找内存泄漏以及内存占用

  • 生成整体的报告,分析问题等等

  • 线下的深入使用工具

  • 可以和Memory Profiler结合使用,一个定位问题,一个深入分析

LeakCanary

LeakCanary是一种自动内存泄漏检测的神器,仅推荐使用于线下集成。但是其缺点也是比较明显的。

  • 使用了多进程以及idleHandler,但是进行堆转储和引用分析的时候,仍然会导致应用的卡顿。具体的可以看一下之前做的一个关于LeakCanary的源码解析

总结

这篇文章对于内存的常见问题以及处理方案进行了总结。

但是内存优化,如果深挖,会涉及到很多很多的知识点。

一个最需要优化的Bitmap,其内存占是如何计算的等等,为什么就占用了那么多的内存;对于LMK机制,我们如何保证程序在后台运行的时候能够保证最大程度不被杀掉;线上如何形成一套APM的机制,内存出现问题以后上报等等都是值得研究的地方。

参考

Android性能优化之内存优化

深入探索 Android 内存优化(炼狱级别)

MAT使用教程

Android性能优化:手把手带你全面了解 内存泄露 & 解决方案

Top团队大牛带你玩转Android性能分析与优化

lowmemorykiller总结

这是一份全面&详细的内存优化指南

本文由 开了肯 发布!

同步公众号[开了肯]

image-20200404120045271