Android性能优化系列二:内存优化

内存优化方向以及工具简介

内存问题主要体现在以下三个方面

内存抖动:内存占用图呈现锯齿状,容易导致GC频繁,加大卡顿的隐患

内存泄露:可用内存减少、频繁GC

内存溢出:OOM、程序异常

工具选择

Memory Profiler

  • 实时图表展示应用内存使用情况

  • 识别内存泄露、抖动(呈现锯齿状)

  • 提供捕获堆转储(堆栈信息转为文件)、强制GC以及跟踪内存分配的能力

  • 方便直观(除了实时展示内存使用情况,甚至还能查看对象创建的地方)

  • 线下平时使用

Memory Analyzer (MAT)

  • 强大的Java Heap分析工具,查找内存泄露以及内存占用
  • 生成整体报告、分析问题等
  • 线下使用

Java内存管理机制

Java内存分配
在这里插入图片描述

Java内存回收算法

标记-清除算法

  • 标记处所有需要回收的对象

  • 统一回收所有被标记的对象
    在这里插入图片描述

缺点:

  • 标记和清除效率不高,因为需要对每个对象进行扫描,标记处要回收的对象
  • 产生大量不连续的内存碎片,不利于后续对象的内存分配

复制算法

  • 将内存划分为大小相等的两块
  • 一块内存用完之后复制存活的对象到另一块内存
  • 对复制前的那一块进行清除
    在这里插入图片描述

优点:当存活的对象比较少时,比较高效(是相对标记清除算法的,因为只需要对内存的一般扫描标记)

缺点:需要一块内存作为交换空间来进行对象的移动,或者说是浪费了一半空间,每次只用了一半的内存,代价大。

标记-整理算法

  • 标记过程与“标记-清除”算法一样
  • 存活的对象往一端进行移动
  • 清除其余内存

在这里插入图片描述
避免了标记-清除算法导致的内存碎片,避免了复制算法的空间浪费,但是有对象的移动(更新指针),成本也高。

分代收集算法

  • 结合多种收集算法优势
  • 新生代对象存活率低,就采用复制算法
  • 老年代对象存活率高,标记-整理算法

在虚拟机中,是多种算法相结合使用的,并不只是使用单一的算法。

Android内存管理机制

内存弹性分配:对Android设备来说,每打开一个App,并不是每个应用都给固定的内存,用的多也就给的多,是弹性分配的,分配值以及最大值受具体设备的影响,有些高端机给应用分配的内存可以达到500M,可能有的手机就只有100多M。

OOM场景:

  • 内存真正不足,一种是达到了应用使用内存的上限阀值就OOM了
  • 没有达到应用内存阀值,但是系统可用内存不足,再申请内存的时候就OOM了。

Dalvik与Art区别

  • Dalvik仅固定一种回收算法
  • Art(从5.0开始默认)回收算法可运行期选择
  • Art具备内存整理能力,减少内存空洞

Low Memory Killer

进程分类(优先级):Android系统将尽量长时间地保持应用进程,但为了新进程或运行更重要的进程,需要清除旧进程来回收内存。为了确定保留或终止哪些进程,系统会对进程进行分类,需要时,系统会首先消除重要性最低的进程,然后是清除重要性稍低一级的进程,以此类推,以回收系统资源。下面按优先级从高到低,进行简单的介绍。

前台进程/foreground process

  • 托管用户正在交互的Activity(已调用Activity的onResume()方法)

  • 托管某个Service绑定到用户正在交互的Activity

  • 托管正在“前台”运行的Service(服务已调用startForeground())

  • 托管正在执行一个生命周期回调的Service(onCreate()、onStartCommand()、或onDestroy())

  • 托管正执行其onReceiver()方法的BroadcastReceiver

可见进程/visible process

  • Activity处于onPause, (还没有进入onStop())
  • 绑定到前台Activity的Service(和前台进程的区别就是一个正在交互,一个仅仅可见)

服务进程/service process

  • 通过 startService() 方法启动的进程

后台进程/background process

  • 对用户没有直接影响的进程,Activity处于onStop的时候

空进程/empty process

  • 不含任何活动应用组件的进程
  • 保留这种进程的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间
  • 为使总体系统资源咋进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程

当内存不足时,回收进程从优先级低的开始,同时回收收益,比如到底是回收30M还是300M的进程,也是一种参考因素。

内存抖动

基本介绍

  • 定义:内存频繁分配和回收导致内存不稳定
  • 表现:频繁GC、内存曲线呈锯齿状
  • 危害:导致卡顿,严重时导致OOM

内存抖动导致OOM的原因分析

  • 频繁创建对象,导致内存不足以及碎片(不连续)
  • 不连续的内存片不易被分配,导致OOM

可通过Memory Profiler跟踪内存分配进行分析定位,内存呈现锯齿状,说明有内存抖动点击Record,随后stop,就可以查看堆栈信息,内存占用过多的对象,选中jump to source就可以定位到代码不规范的地方。此外还可以通过CPU Profiler结合代码进行排查,它可以记录定位哪一段代码消耗了时间,而我们内存抖动的代码一直在不断执行,结合起来是可以定位内存抖动的位置。内存抖动是由于频繁申请内存,频繁GC,所以出现内存抖动优先去找循环或者频繁调用的地方。

内存泄露

定义:垃圾对象无法被GC正常回收

表现:内存抖动、可用内存逐渐变少

危害:内存不足、GC频繁、OOM

​ 关于内存泄露这一块想多说一点,到底什么是内存泄露呢?就是内存不在GC掌控之内了,原因就是GC垃圾回收机制对垃圾对象无法回收,从而导致垃圾对象持的内存持续占用,无法释放,造成可用内存减少,如果可用内存不断减少,就容易引发OOM。那么问题来了,GC判定一个对象是否可回收的依据是什么呢?如果一个对象被别的对象引用了,就不能被GC回收,对吗?答案是否定的,提一下强引用、软引用、弱引用、虚引用,你就知道了。在内存不足的时候,后面三种引用都可以被回收的。

常说的GC(Garbage Collector) Roots,特指的是垃圾收集器(Garbage Collector)的对象,GC Roots就是Java虚拟机中所有引用的根对象。我们都知道,垃圾回收器不会回收GC Roots以及那些被它们间接强引用的对象

一个对象可以属于多个root,GC Roots有以下几种:

  • Class - 由系统类加载器(system class loader)加载的对象,这些类是不能够被回收的,他们可以以静态字段的方式保存持有其它对象。我们需要注意的一点就是,通过用户自定义的类加载器加载的类,除非相应的Java.lang.Class实例以其它的某种(或多种)方式成为roots,否则它们并不是roots,.
  • Thread - 活着的线程
  • Stack Local - Java方法的local变量或参数
  • JNI Local - JNI方法的local变量或参数
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的监控对象
  • Held by JVM - 用于JVM特殊目的由GC保留的对象,但实际上这个与JVM的实现是有关的。可能已知的一些类型是:系统类加载器、一些JVM知道的重要的异常类、一些用于处理异常的预分配对象以及一些自定义的类加载器等。然而,JVM并没有为这些对象提供其它的信息,因此需要去确定哪些是属于"JVM持有"的了。

Memory Analyzer

下载链接:https://www.eclipse.org/mat/downloads.php

转换:hprof-conv 原文件路径 转换后文件路径

下面举个例子,具体说说MAT的使用,在此之前还有一种方式可以简单的判断是否发生了内存泄露,就是通过adb shell dumpsys meminfo packageName -d 命令查看内存情况,会显示许多信息,但是只要查看Objects部分即可:

在这里插入图片描述
在自己测试demo中,在MainActivity跳转到TestActivity,并调用finish方法,结束掉了MainActivity,但是Activities为2,再看前面的AppContexts为4,盲猜是Context的使用出问题了。这个命令只是一个初步的判断,详细的分析还是通过MAT工具,下面回到MAT的具体使用

首先Dump Java heap信息,生成并导出导出hprof文件
在这里插入图片描述
在这里插入图片描述

此时的hprof还不能立即使用,还需要进行转换:

可通过hprof-conv -z 原文件名 转换后文件名
在这里插入图片描述

接下来就通过MAT的File—>Open Heap dump 打开文件,点击Histogram分析内存泄露问题。为了便于查看,可进行按包名分组查看

在这里插入图片描述
根据包名找到相关对象列表,选中本该回收的对象MainActivity,查看外部引用

在这里插入图片描述

选中结果,在Path To GC Root 中,选中除去软,弱,虚引用

在这里插入图片描述

高潮来了,我们看到是instance,位置在AppSettings类中,

在这里插入图片描述

再去排查AppSettings.java 代码,发现了问题所在,在MainActivity通过AppSettings.getInstance(this) 初始化AppSettings时,但是静态的instance对象间接的持有了MainActivity的引用,长生命周期的对象持有短生命周期的对象,导致MainActivity对象不能被回收,

传参时将context改为context.getApplicationContext()即可。

context.getApplicationCotext() 即可解决这个问题。

public class AppSettings {

    private static volatile AppSettings instance;
    private static Context mContext;

    public AppSettings(Context context) {
        mContext = context;
    }

    public static AppSettings getInstance(Context context) {
        if (instance == null) {
            synchronized (AppSettings.class) {
                if (instance == null) {
                    //造成内存泄露
                    instance = new AppSettings(context);
              // instance = new AppSettings(context.getApplicationContext());
                }
            }
        }

        return instance;
    }
}

这只是通过一个问题,进行简单的分析,真实的情况是,项目代码繁多,可选择重点页面的进入和退出进行Dump Java heap,转换hprof,然后进行前后比对分析,就可以得到发生内存泄露的对象。MAT的使用就记录到这里了。

ARTHook优雅检测不合理图片

Bitmap内存模型
  • API10之前Bitmap自身在Dalvik Heap中,像素信息保存在Native
  • API10之后像素也被放在Dalvik Heap中
  • API26之后像素在Native

2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用),而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,当java层bitmap被回收后,能及时回收native层的像素数据。8.0之后图像资源的管理更加优秀,极大降低了OOM。

获取bitmap占用内存的方式
  • getByteCount(): 运行时动态获取
  • 宽x高x一个像素占用内存:需要注意的是图片放在的drawable目录,宽高会有一个压缩(拉伸)比例,此外一个像素占用内存大小与色彩存储格式有关如RGB_565一个像素占两个字节,ARGB_8888表示一个像素占4个字节。
不合理图片处理的常规方式
  • 背景:图片对内存优化至关重要、图片宽高远大于控件宽高(如果直接load到内存,造成额外开销)
  • 实现:继承ImageView,复写onDraw方法,绘制时计算图片大小,如果超出控件大小一定比例,警告提示
  • 缺陷:侵入性强、不通用
ARTHook介绍

挂钩,将额外的代码钩住原有的方法,在执行原来的方法的时候,也会执行我们额外的代码,达到修改执行逻辑的目的。ARTHook就是这样一种方案,它有一些应用场景,如运行时插桩,做一些性能分析等等。

为了有一个更直观的认识,下面介绍一下Epic以及它的简单使用,Epic是一个虚拟机层面、以java Method为粒度的运行时Hook框架,添加依赖后,通过继承XC_MethodHook ,实现相应逻辑,通过DexposedBridge.findAndHookMethod注入Hook,让框架知道我们想要Hook哪个类的具体方法。

public class ImageHook extends XC_MethodHook {

    /**
    * Hook方法之后,添加比对宽高的逻辑
    */
    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        // 实现我们的逻辑
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
    }

    /**
     * 通过比比较宽高,判断是否合理
     * @param thiz
     * @param drawable
     */
    private static void checkBitmap(Object thiz, Drawable drawable) {
        if (drawable instanceof BitmapDrawable && thiz instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                final View view = (View) thiz;
                //控件的宽度
                int width = view.getWidth();
                //控件的高度
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    // Bitmap宽高都大于view宽高的2倍以上,则警告
                    if (bitmap.getWidth() >= (width << 1)
                            && bitmap.getHeight() >= (height << 1)) {
                        warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                    }
                } else {
                    final Throwable stackTrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                        && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }


    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();

        LogUtils.i(warnInfo);
    }

}

具体的注入Hook代码

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
            @Override
            protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                super.afterHookedMethod(param);
                //传入需要Hook的方法名,参数类型等等
                DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
            }
        });

ARTHook小结

  • 无侵入性

  • 通用性强

  • 兼容问题大,不能带到线上环境

避免可控的内存泄露

前面我们提到过内存泄露是因为垃圾对象无法被GC回收,本质是生命周期短的对象被生命周期长的对象引用,导致无法被正常回收。下面列举一些常见的内存泄露场景,以及如何避免。

非静态内部类导致的内存泄露

非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接的长期维持着外部类的引用,阻止被系统回收。

public class MemoryLeakActivity extends AppCompatActivity {

    private static Object inner;
    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = (Button) findViewById(R.id.bt_next);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                createInnerClass();
                finish();
            }
        });
    }

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

当我们点击button的时候,通过注释1处的代码创建了非静态内部类InnerClass的实例,赋值给静态变量inner后,该实例的生命周期和应用程序生命周期一样长,并且持有MemoryLeakActivity的引用,导致MemoryLeakActivity无法被回收。将非静态内部类改为静态,即可避免这种方式造成的内存泄露。涉及到内部类的时候,一定要注意静态问题。

Handler造成的内存泄露

如下代码,就是一种常见的使用场景。Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间不确定,有可能会很长,持有MHandler的引用,而mHandler又持有当前Activity的引用,所以当退出当前Activity的时候,如果MessageQueue中海油Message,Activity就不能被回收。

public class MemoryLeakActivity extends AppCompatActivity {
    
    private static int FLAG = 100;
    

    @SuppressLint("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(@NonNull Message msg) {
               mHandler.sendEmptyMessageDelayed(FLAG, 10000);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mHandler.sendEmptyMessage(FLAG);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeMessages(FLAG);//3
    }
}

而我们通常采用的方是添加注释3处的代码,在退出Activity的时候,移除Message,就不再持有它的引用,避免了内存泄露。还有采用静态的Handler , 以及对当前Activity采用WeakReference弱引用。当采用如注释1处静态的Handler的时候,和静态内部类一样,就不再持有Activity的引用了,避免了内存泄露,但是频繁使用static,就会导致jvm加载类的时候消耗过多。另外就是弱引用了,如注释2处代码,GC回收是,就会自动被清除,避免了内存泄露,但是如果你的业务场景是Activity关闭后,延时或定时执行某项任务需要调用Activity的方法,这个时候就有问题了,因为弱引用的Activity可能已经被回收了。因为不确定延时时间,和GC回收哪一个先执行。所以说没有最优的方案,只有最适合的场景,一定要结合自身业务场景进行选择和取舍。

没有正确使用Context

对于不是必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),我们可以考虑使用Application Context来代替Activity的Context,这样可以避免Activity泄露,比如如下的单例模式:

public class Singleton {
    private static volatile Singleton singleton;
    private Context mContext;
    private Singleton(Context context) {
        mContext = context;
    }
    public static Singleton getInstance(Context context) {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton(context);
                    //singleton = new Single(context.getApplicationContext());
                }
            }
        }
        return singleton;
    }
}

在Activity中调用了Singleton.getInstance(this); 作为静态的singleTon,它的生命周期长于Activity,当退出Activity的时候,由于singleTon持有Activity的引用,导致Activity不能被回收,将参数context改为context.getApplicationContext(),就避免了内存泄露。

注册监听未移除

很多服务需要register和unregister监听器,我们需要在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove。

Bitmap未及时释放

临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。
避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量。

资源对象未关闭

在Activity中调用了Singleton.getInstance(this); 作为静态的singleTon,它的生命周期长于Activity,当退出Activity的时候,由于singleTon持有Activity的引用,导致Activity不能被回收,将参数context改为context.getApplicationContext(),就避免了内存泄露。

原创文章 23 获赞 30 访问量 9570

猜你喜欢

转载自blog.csdn.net/my_csdnboke/article/details/104158378