2021-09-24

2021-09-24  本文已影响0人  _水蓝

Android深度性能优化--内存优化

一、背景

在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,因此不需要像使用C/C++一样在代码中分配和释放某一块内存。Android系统的内存管理类似于JVM,通过new关键字来为对象分配内存,内存的释放由GC来回收。并且Android系统在内存管理上有一个Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如内存泄漏、内存抖动、短时间内分配大量的内存对象等等。

二、优化工具

2.1 Memory Profiler

Memory profiler是Android Studio自带的一个内存检测工具,通过实时图表的方式展示内存信息,具有可以识别内存泄露,内存抖动等现象,并可以将捕获到的内存信息进行堆转储、强制GC以及跟踪内存分配的能力。

Android Studio打开Profiler工具

1.jpg

观察Memory曲线,比较平缓即为内存分配正常,如果出现大的波动有可能发生了内存泄露。

GC:可手动触发GC

Dump:Dump出当前Java Heap信息

Record:记录一段时间内的内存信息

点击Dump后

2.jpg

可查看当前内存分配对象

Allocations:分配对象个数

Native Size:Native内存大小

Shallow Size:对象本身占用内存的大小,不包含其引用的对象

Retained Size: 对象的Retained Size = 对象本身的Shallow Size + 对象能直接或间接访问到的对象的Shallow Size,也就是说 Retained Size 就是该对象被 Gc 之后所能回收内存的总和

点击Bitmap Preview可以进行预览图片,对查看图片占用内存情况比较有帮助

点击Record后

3.jpg

可以记录一段时间内内存分配情况,可查看各对象分配大小及调用栈、对象生成位置

2.2 Memory Analyzer(MAT)

比Memory Profiler更强大的Java Heap分析工具,可以准确查找内存泄露以及内存占用情况,还可以生成整体报告,用来分析问题等。

MAT一般用来线下结合Memory Profiler分析问题使用,Memory Profiler可以直观看出内存抖动,然后生成的hdprof文件,通过MAT深入分析及定位内存泄露问题。

具体使用下面会结合实例讲解一下

2.3 LeakCannary

Leak Cannary是一个能自动监测内存泄露的线下监测工具,具体原理可自行了解下。

github链接:https://github.com/square/leakcanary

三、内存管理

3.1 内存区域

Java内存划分为方法区、堆、程序计数器、本地方法栈、虚拟机栈五个区域;

线程维度分为线程共享区和线程隔离区,方法区和堆是线程共享的,程序计数器、本地方法栈、虚拟机栈是线程隔离的,如下图

4.jpg

方法区

虚拟机栈

本地方法栈

程序计数器

3.2 对象存活判断

引用计数法

可达性分析法

GC Root有以下几种:

3.3 垃圾回收算法

标记清除算法

标记清除算法有两个阶段,首先标记出需要回收的对象,在标记完成后统一回收所有标记的对象;

缺点:

复制算法

将可用内存按空间分为大小相同的两小块,每次只使用其中的一块,等这块内存使用完了将还存活的对象复制到另一块内存上,然后将这块内存区域对象整体清除掉。每次对整个半区进行内存回收,不会导致碎片问题,实现简单高效。

缺点:

标记整理算法

标记整理算法标记过程和标记清除算法一样,但清除过程并不是对可回收对象直接清理,而是将所有存活对象像一端移动,然后集中清理到端边界以外的内存。

分代收集算法

当代虚拟机垃圾回收算法都采用分代收集算法来收集,根据对象存活周期不同将内存划分为新生代和老年代,再根据每个年代的特点采用最合适的算法。

四、内存抖动

内存频繁分配和回收导致内存不稳定

4.1 模拟内存抖动

执行此段代码

private static Handler mShakeHandler = new Handler() {
    @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        // 频繁创建对象,模拟内存抖动
        for(int index = 0;index <= 100;index ++) {
            String strArray[] = new String[100000];
        }

        mShakeHandler.sendEmptyMessageDelayed(0,30);
    }
};

4.2 分析并定位

利用Memory Profiler工具查看内存信息

5.jpg

发现内存曲线由原来的平稳曲线变成锯齿状

6.jpg

点击record记录内存信息,查找发生内存抖动位置,发现String对象ShallowSize非常异常,可直接通过Jump to Source定位到代码位置

7.jpg

五、内存泄露

定义:内存中存在已经没有用确无法回收的对象

现象:会导致内存抖动,可用内存减少,进而导致GC频繁、卡顿、OOM

5.1 模拟内存泄露

模拟内存泄露代码,反复进入退出该Activity

/**
 * 模拟内存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);

        // 添加静态类引用
        CallBackManager.addCallBack(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }

    @Override
    public void dpOperate() {
        // do sth
    }

5.2 分析并定位

通过Memory Profiler工具查看内存曲线,发现内存在不断的上升

8.jpg

如果想分析定位具体发生内存泄露位置需要借助MAT工具

首先生成hprof文件

点击dump将当前内存信息转成hprof文件,需要对生成的文件转换成MAT可读取文件

执行一下转换命令(Android/sdk/platorm-tools路径下)

hprof-conv 刚刚生成的hprof文件 memory-mat.hprof

使用mat打开刚刚转换的hprof文件

9.jpg

点击Historygram,搜索MemoryLeakActivity

10.jpg

可以看到有8个MemoryLeakActivity未释放

11.jpg

查看所有引用对象

12.jpg

查看到GC Roots的引用链

13.jpg

可以看到GC Roots是CallBackManager

14.jpg

解决问题,当Activity销毁时将当前引用移除

@Override
protected void onDestroy() {
    super.onDestroy();
    CallBackManager.removeCallBack(this);
}

六、MAT分析工具

Overview

当前内存整体信息

[图片上传失败...(image-c0bad7-1632471126749)]

Histogram

列举对象所有的实例及实例所占大小,可按package排序

[图片上传失败...(image-83fb18-1632471126749)]

可以查看应用包名下Activity存在实例个数,可以查看是否存在内存泄露,这里发现内存中有8个Activity实例未释放

[图片上传失败...(image-d501f9-1632471126749)]

查看未被释放的Activity的引用链

[图片上传失败...(image-4347e3-1632471126749)]

Dominator_tree

当前所有实例的支配树,和Histogram区别时Histogram是类维度,dominator_tree是实例维度,可以查看所有实例的所占百分比和引用链

[图片上传失败...(image-34cb1a-1632471126749)]

SQL

通过sql语句查询相关类信息

[图片上传失败...(image-c55ac1-1632471126749)]

Thread_overview

查看当前所有线程信息

[图片上传失败...(image-f62dea-1632471126749)]

Top Consumers

通过图形方式展示占用内存较高的对象,对降低内存栈优化可用内存比较有帮助

[图片上传失败...(image-d91270-1632471126749)]

[图片上传失败...(image-7c14ca-1632471126749)]

Leak Suspects

内存泄露分析页面

[图片上传失败...(image-e1c2e4-1632471126749)]

直接定位到内存泄露位置

[图片上传失败...(image-90bec3-1632471126749)]

七、通过ARTHook检测不合理图片

7.1 获取Bitmap占用内存

7.2 检测大图

当图片控件load图片大小超过控件自身大小时会造成内存浪费,所以检测出不合理图片对内存优化是很重要的。

ARTHook方式检测不合理图片

通过ARTHook方法可以优雅的获取不合理图片,侵入性低,但是因为兼容性问题一般在线下使用。

引入epic开源库

implementation 'me.weishu:epic:0.3.6'

实现Hook方法

public class CheckBitmapHook extends XC_MethodHook {

    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);

        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView,imageView.getDrawable());
    }

    private static void checkBitmap(Object o,Drawable drawable) {
        if(drawable instanceof BitmapDrawable && o instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if(bitmap != null) {
                final View view = (View)o;
                int width = view.getWidth();
                int height = view.getHeight();
                if(width > 0 && height > 0) {
                    if(bitmap.getWidth() > (width <<1) && bitmap.getHeight() > (height << 1)) {
                        warn(bitmap.getWidth(),bitmap.getHeight(),width,height,
                                new RuntimeException("Bitmap size is 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);

Application初始化时注入Hook

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap", Bitmap.class,
                new CheckBitmapHook());
    }
});

八、线上内存监控

8.1 常规方案

常规方案一

在特定场景中获取当前占用内存大小,如果当前内存大小超过系统最大内存80%,对当前内存进行一次Dump(Debug.dumpHprofData()),选择合适时间将hprof文件进行上传,然后通过MAT工具手动分析该文件。

缺点:

常规方案二

将LeakCannary带到线上,添加预设怀疑点,对怀疑点进行内存泄露监控,发现内存泄露回传到server。

缺点:

8.2 LeakCannary定制改造

  1. 将需要预设怀疑点改为自动寻找怀疑点,自动将前内存中所占内存较大的对象类中设置怀疑点。
  2. LeakCanary分析泄露链路比较慢,改造为只分析Retain size大的对象。
  3. 分析过程会OOM,是因为LeakCannary分析时会将分析对象全部加载到内存当中,我们可以记录下分析对象的个数和占用大小,对分析对象进行裁剪,不全部加载到内存当中。

8.3 完整方案

  1. 监控常规指标:待机内存、重点模块占用内存、OOM率
  2. 监控APP一个生命周期内和重点模块界面的生命周期内的GC次数、GC时间等
  3. 将定制的LeakCanary带到线上,自动化分析线上的内存泄露
上一篇下一篇

猜你喜欢

热点阅读