你的应用内存优化了吗?

目录

前言

一、内存优化介绍

1.1、为什么要做内存优化?

1.2、内存问题表现形式

二、内存优化工具

2.1、Memory Profiler

2.2、Memory Analyzer(MAT)

2.3、LeakCanary

三、Android内存管理机制

3.1、Java内存分配

3.2、Java内存回收算法

3.3、Android内存管理机制

四、实战内存抖动解决

4.1、内存抖动简介

4.2、内存抖动导致OOM

4.3、实战分析

五、实战内存泄露解决

5.1、内存泄露简介

5.2、实战分析

六、线上内存监控方案

6.1、常规方案

6.2、LeakCanary定制

6.3、线上监控完整方案

七、内存优化技巧


前言

Sorry,忘记了我的脸已经过了五四青年节了。。。。。。小小幽默一下,接下来说点正事吧!

本篇是《性能优化》专题的第二篇,上一篇中介绍了Android的启动优化,有兴趣的朋友可以去翻看一下:

《Android启动优化你真的了解吗?》

今天来说一下Android的内存优化,关于内存优化基本上可以说是面试必问的一个知识点,可见掌握Android的内存优化是何等的重要,接下来我会针对Android内存优化工具、内存管理机制、内存抖动、内存泄露、线上内存监控等几个方面做全方位的介绍。

一、内存优化介绍

1.1、为什么要做内存优化?

内存优化一直是一个很重要但却缺乏关注的点,我们所写的每一行代码其实都涉及到了内存的申请以及回收等过程,对于内存问题它的表现形式相对隐蔽,又由于安卓使用Java语言开发,大家都知道Java的内存回收机制是自动的,所以大多数情况下开发者都是不重视的。于是乎当你在某些平台上看到应用内存的一些堆栈信息,比如出现了OutOfMemory,在你认真跟踪下来可能会发现内存出现问题的地方仅仅只是一个表现的地方,并非深层次的原因,因为内存问题相对比较复杂,它是一个逐渐挤压的过程,正好在你出现问题的代码那里爆了,所以针对应用的内存问题开发者必须多加关注。

1.2、内存问题表现形式

  • 内存抖动:锯齿状、GC频繁导致卡顿
  • 内存泄露:可用内存逐渐减少、频繁GC
  • 内存溢出:OOM、程序异常

二、内存优化工具

2.1、Memory Profiler

2.1.1、Profiler简介(Android Studio自带的工具)

  • 实时图表展示应用内存使用量(非常直观)
  • 识别内存泄露(这里的识别只是一个简单判断)、抖动(这个相对比较简单、锯齿状图形)等
  • 提供捕获堆转储、强制GC以及跟踪内存分配的能力

2.1.2、Profiler用法介绍

首先找到Profiler面板,如下图中所示:View——>Tool Windows——>Profiler即可调出该面板。

在Profiler面板的左上角,点击➕加号,可以选择想要跟踪的哪台设备的哪个进程:

然后在面板的中间大的区域一共分为了四块:CPU、MEMORY、NETWORK、ENERGY,这里我们关注的是MEMORY这个条目,你可以直接点击MEMORY这一行,然后面板中展示的就是内存的使用情况了:


接着来看一下它的具体使用情况,这里用一张图来介绍一下相关的工具按钮的使用说明:

2.1.3、Dump Java heap

先说明一点,因为我的电脑屏幕较小,为了展示整体的效果,图片里面有些英文字母出现了省略号,大家根据解释说明结合自己的工具将对应的单词对号入座就OK了!

点击向下的那个箭头,然后它就会开始Dump内存信息,然后以文件的形式展现出来:

  • app heap:这一列展示了内存中所存在的这些类
  • Allocations:是分配了多少对象
  • Native Size:主要反映Bitmap所使用的像素内存
  • Shallow Size:该实例的大小
  • Retained Size:该实例所支配的内存大小

然后随便选取一个条目点击查看,这里我选了一个Bitmap的条目,然后右侧会展示出这个Bitmap所创建的对象,右键可以跟到具体的源码位置,上方同样的有Shallow Size等数据的展示,其中Depth的意思是:从任何GC根到所选实例的最短跳数:

2.1.4、Allocation Tracking

对于8.0之前其实是有一个start record和stop record的按钮用来统计一段时间内的内存分配情况的,8.0之后可以直接到Memory面板区域用鼠标直接拖拽一段距离,然后就可以生成对象内存分配的统计结果了:

左侧是刚刚操作app所分配的内存情况,点击头部Class Name可以按字母排序,然后随便点击一个条目选中了StringBuilder,然后右侧Instance View面板中就出现了它的一些实例,点击某个实例可以看到调用栈信息和调用的过程,下方的Allocation Call Stack中可以看到这个对象是在哪里创建的,比如这里的FeedAdapter类中的onBindViewHolder方法的75行,右键jump to source就可以跳到对应的源码位置。

2.1.5、Memory Profiler总结

  • 方便直观:整个内存使用情况可以通过图表的方式直观展示出来,同时也可以知道内存的分配情况,并且还可以知道分配某个对象具体的堆栈信息,方便跟踪
  • 线下平时使用:Android Studio自带的工具,开发过程使用较为方便

2.2、Memory Analyzer(MAT)

2.2.1、MAT简介

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

官网下载地址:http://www.eclipse.org/mat/downloads.php ,这个地址是不是有你熟悉的单词,嗯,没错啦,MAT是Eclipse中的一个插件,因为现在开发过程中很多人都使用了IDEA或者Android Studio,所以你不想下载Eclipse的话呢,你可以去下载MAT的独立版,解压之后里面有一个MemoryAnalyzer.exe的可执行文件,直接点击就可以使用了。

福利来啦:MAT版下载地址(无需积分哦):https://download.csdn.net/download/JArchie520/12488709

这个工具很多时候我们需要结合Android Studio的堆转储能力配合使用,但是需要注意,AS3.0之后生成的hprof文件不是标准的hprof文件了,需要使用命令转换一下:hprof-conv 原文件路径 转换后文件路径 

2.2.2、MAT用法简介

①、Overview:概览信息

Top Consumers

  • 通过图表展示出占用内存比较多的对象,此栏在做降低内存占用时比较有帮助
  • Biggest Objects:相对详细的信息

Leak Suspects

  • 快速查看内存泄露的可疑点


②、 Histogram:直方图

  • Class Name:具体检索某一个类
  • Objects:某一个具体的Class有多少实例
  • Shallow Heap:某单一实例自己占了多少内存  
  • Retained Heap:在这个引用链之上这些对象总共占了多少内存

Group by packge:将类对象以包名形式展示

List objects

  • with outgoing references:自身引用了哪些类
  • with incoming references:自身被哪些类引用

③、dominator_tree

  • 每个对象的支配树
  • percentage:占所有对象的百分比

在条目上右键它也有List objects,它和Histogram之间有啥区别呢?主要区别就是下面两点:

  • Histogram:基于类的角度分析
  • dominator_tree:基于实例的角度分析

④、OQL:对象查询语言,类似于从数据库中检索内容

⑤、thread_overview:详细的展示线程信息,可以查看出当前内存中存在多少线程

2.3、LeakCanary

2.3.1、LeakCanary简介

2.3.2、LeakCanary使用

首先在build.gradle文件中添加依赖:

//LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.2'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.2'

然后在你自己的Application类的onCreate()方法中初始化:

if (LeakCanary.isInAnalyzerProcess(this)) {
    return;
}
LeakCanary.install(this);

然后当你的App中出现内存泄露时,手机桌面会出现一个Leaks的图标,点击进去可以看到产生泄露的记录列表,点击列表条目可以展开具体的泄露信息:

2.3.3、LeakCanary原理

  • 监控生命周期,onDestroy添加RefWatcher检测(其实就是设置怀疑点)
  • 二次确认断定发生内存泄露
  • 分析泄露,找引用链
  • 监控组件+分析组件

三、Android内存管理机制

由于Android开发很多还是用的Java语言,所以先来说一下Java的内存管理机制,然后再来说Android的内存管理机制。关于Java的内存管理机制这一部分,我在之前的一篇文章中有图文的详细介绍,建议大家去看看,这里就只简单的介绍一下了:

文章详情见《带你认识JVM》:https://blog.csdn.net/JArchie520/article/details/103734810

3.1、Java内存分配

  • 方法区:存储的是Java的类信息、常量和静态变量等,这块区域是所有线程都共享的
  • 虚拟机栈:存储的是局部变量表和操作数栈等
  • 本地方法栈:对于它的理解可以结合虚拟机栈进行对比,虚拟机栈是为Java方法服务的,而本地方法栈是为Native方法服务的
  • 堆:最大的一块区域,是所有线程共享的,每个对象的实际内存分配都是在堆中进行的,虚拟机栈中分配的只是引用,这些引用会指向堆中真正创建的对象,同时它是GC主要作用的一块区域,我们平时说的内存泄漏也是发生在这块区域的
  • 程序计数器:知道有这个概念就OK,用来存储当前线程执行的方法执行到的具体位置

3.2、Java内存回收算法

(一)、标记-清除算法

  • 标记出所有需要回收的对象
  • 统一回收所有被标记的对象

总结:

  • 标记和清除效率不高
  • 产生大量不连续的内存碎片

(二)、复制算法

  • 将内存划分为大小相等的两块
  • 一块内存用完之后复制存活对象到另一块
  • 清理另一块内存

总结:

  • 实现简单,运行高效(相较于第一种来说)
  • 浪费一半空间,代价大

(三)、标记-整理算法

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

总结:

  • 避免标记-清除导致的内存碎片
  • 避免复制算法的空间浪费

(四)、分代收集算法

  • 结合多种收集算法优势将它们应用于不同的生命周期
  • 新生代对象存活率低,复制(复制比例可以调整)
  • 老年代对象存活率高,标记-整理

3.3、Android内存管理机制

(一)、Android内存管理机制特点

  • 内存弹性分配,分配值与最大值受具体设备影响(高端机和低端机单个app可以使用的内存是不同的)
  • OOM场景:内存真正不足、可用内存不足

(二)、Dalvik与Art区别

  • Dalvik仅固定一种回收算法(手机出厂之前烧ROM的时候就已经确定好了,运行期间无法改变)
  • Art回收算法可运行期间选择(不同的情况下选择合适的回收算法)
  • Art具备内存整理能力,减少内存空洞

(三)、Low Memory Killer

  这套机制是针对所有进程来说的,如果手机内存不足的情况下,这套机制会针对所有进程进行回收。

  • 进程分类(前台、可见、服务、后台、空进程五大类,优先级由高到低排列,优先回收低优先级的)
  • 回收收益(具体回收的大小)

四、实战内存抖动解决

4.1、内存抖动简介

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

4.2、内存抖动导致OOM

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

4.3、实战分析

这一部分我会模拟一次内存抖动,并通过Profiler分析内存情况,定位到具体内存抖动的代码。

首先先来创建一个布局文件activity_memory.xml,里面就一个按钮,用来触发模拟内存抖动的那部分代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        android:id="@+id/btn_memory"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="模拟内存抖动"/>
</LinearLayout>

然后定义一个MemoryShakeActivity页面,加载刚才的布局,并且在页面中定义一个Handler,当点击模拟内存抖动的按钮时,我们定时执行handleMessage中的模拟抖动的代码,整个代码都是很容易能看懂的那种:

/**
 * 作者:created by Jarchie
 * 时间:2020/05/31 09:22:17
 * 邮箱:[email protected]
 * 说明:模拟内存抖动页面
 */
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {

    @SuppressLint("HandlerLeak")
    private static Handler mHandler = new Handler(){
        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            //模拟内存抖动的场景,每隔10毫秒执行一次,循环执行100次,每次通过new分配大内存
            for (int i=0;i<100;i++){
                String[] obj = new String[100000];
            }
            mHandler.sendEmptyMessageDelayed(0,10);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memory);
        findViewById(R.id.btn_memory).setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        if (view.getId() == R.id.btn_memory){
            mHandler.sendEmptyMessage(0);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

然后将我们的“爱仆”跑起来,我截了两张图给大家看一下,第一张是没有执行模拟抖动的代码之前的,第二张是执行之后的:

从上面两张图中可以清晰的看到第一张内存比较平稳,第二张内存图有锯齿状出现,突然出现了频繁的GC,看到下面好多小垃圾桶了没,这个时候可以初步判定应该是出现了内存抖动现象,因为比较符合它的特征,然后在面板上拖动一段距离它就会将这段时间内的内存分配情况给我们展示出来:

首先双击Allocations,然后将这一列按照从大到小的顺序排列好,然后你会发现String数组居然有这么多,它占用的内存大小也是最高的(值得关注的点我都用矩形标出了),此时我们就应该锁定这个目标,为什么String类型的数组会有这么多,这里很有可能是有问题的。然后排查究竟是哪里导致的这个问题,很简单点击String[]这一行,在右侧Instance View面板中随便点击一行,下方Allocation Call Stack面板中就出现了对应的堆栈信息,上面也列出了具体哪个类的哪一行,右键jupm to source就可以跳转到指定的源码位置,这样就找到了内存抖动出现的位置,然后我们分析代码作相应的修改即可。

流程总结:①、使用Memory Profiler初步排查;②、使用Memory Profiler或CPU Profiler结合代码排查

内存抖动解决技巧:找循环或者频繁调用的地方

五、实战内存泄露解决

5.1、内存泄露简介

定义:内存中存在已经没有用的对象

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

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

5.2、实战分析

这里还是通过代码来真实的模拟一次内存泄露的场景,对于一般的APP程序来说,最大的问题往往都是在Bitmap上,因为它消耗的内存比较多,拿它来模拟会看的比较明显。好首先来看布局文件activity_memoryleak.xml,里面就一个ImageView控件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <ImageView
        android:id="@+id/iv_memoryleak"
        android:layout_width="50dp"
        android:layout_height="50dp" />
</LinearLayout>

然后定义了一个模拟处理某些业务的Callback回调接口,和一个统一管理这些回调接口的Manager类:

//模拟回调处理某些业务场景
public interface CallBack {
    void dpOperate();
}

//统一管理Callback
public class CallBackManager {
    public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

    public static void addCallBack(CallBack callBack) {
        sCallBacks.add(callBack);
    }

    public static void removeCallBack(CallBack callBack) {
        sCallBacks.remove(callBack);
    }
}

然后在我们的模拟内存泄露的页面上设置Bitmap,并设置回调监听:

/**
 * 作者:created by Jarchie
 * 时间:2020/6/2 10:48:19
 * 邮箱:[email protected]
 * 说明:模拟内存泄露页面
 */
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.big_bg);
        imageView.setImageBitmap(bitmap);
        CallBackManager.addCallBack(this);
    }

    @Override
    public void dpOperate() {

    }
}

OK,我们的代码就写完了,现在来实际运行一下,然后将这个页面连续打开关闭多次,看看这段代码会不会造成内存泄露呢?

这是我用Profiler截取的内存图片,可以看到整个内存在经过了我的反复开关页面之后虽然有的地方出现了一个小抖动,但是整体是呈阶梯状上升的,可用内存在逐渐减少,此时基本上可以断定这个界面出现了内存泄露。Profiler工具虽然可以初步帮我们断定出现了内存泄露,但是它却无法断定具体是哪里出现了内存泄露,意思就是我们还是不知道该修改哪里的代码,所以此时需要用到强大的Java Heap工具了,来有请MAT出场。

首先需要在Profiler中点击Dump Java Heap按钮,使用堆转储功能转换成一个文件,然后点击保存按钮将文件保存到本地目录下,比如我这里保存为H盘中的memoryleak.hprof文件,然后使用hprof-conv命令将其转换为标准的hprof文件,我这里是转换后的名称是:memoryleak_transed.hprof,如下所示:

然后打开MAT工具,导入刚刚生成的转换后的文件:

点击Histogram查看内存中所有存活的对象,然后我们在Class Name中可以输入内容搜索想要查找的对象:

然后可以看到该对象的具体信息,以及数量和所占用的内存大小,我这里发现内存中居然存在6个MemoryLeakActivity对象:

然后右键List objects---->with incoming references找到所有引向它的强引用:

然后右键Path To GC Roots----->with all references,将所有引用都计算在内然后算出来这个对象和GCRoot之间的路径:

来看结果,最后是到了sCallBacks这里,而且它左下角有个小圆圈,这就是我们真正要找的位置,也就是说MemoryLeakActivity是被CallBackManager这个类的sCallBacks这个对象引用了:

根据上面找的结果到代码中去找CallBackManager的sCallBacks看看这里究竟是做了什么引发的?

public static ArrayList<CallBack> sCallBacks = new ArrayList<>();

MemoryLeakActivity是被sCallBacks这个静态变量引用着,由于被static关键字修饰的变量的生命周期是和App的整个生命周期一样长的,所以当MemoryLeakActivity这个页面关闭时,我们应该将变量的引用关系给释放掉,否则就出现了上面的内存泄露的问题。所以解决这个问题也很简单了,添加如下几行代码:

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

流程总结:①、使用Memory Profiler初步观察(可用内存逐渐减少);②、通过Memory Analyzer结合代码确认

六、线上内存监控方案

线上内存问题最大的就是内存泄露,对于内存抖动和内存溢出它们一般都和内存泄露导致的内存无法释放相关,如果能够解决内存泄露,则线上内存问题就会减少很多。线上内存监控其实还是比较困难的,因为我们无法使用线下的这些工具来直观的发现分析问题。

6.1、常规方案

①、设定场景线上Dump

比如你的App已经占用到单个App最大可用内存的较高百分比,比如80%,通过:Debug.dumpHprofData();这行代码可以实现将当前内存信息转化为本地文件。

整个流程如下超过内存80%——>内存Dump——>回传文件(注意文件可能很大,保持在wifi状态回传)——>MAT手动分析

总结:

  • Dump文件太大,和对象数正相关,可裁剪
  • 上传失败率高、分析困难

②、LeakCanary线上使用

  • LeakCanary带到线上
  • 预设泄露怀疑点
  • 发现泄露回传

总结:

  • 不适合所有情况,必须预设怀疑点,限制了全面性
  • 分析比较耗时,也容易OOM(实践发现LeakCanary分析过程较慢,很有可能自己在分析的过程中自身发生OOM)

6.2、LeakCanary定制

  • 预设怀疑点——》自动找怀疑点(谁的内存占用大就怀疑谁,大内存对象出现问题的概率更大)
  • 分析泄露链路慢(分析预设对象的每一个对象)——》分析Retain Size大的对象(减少它的分析工作量,提高分析速度)
  • 分析OOM(将内存堆栈生成的所有文件全部映射到内存中,比较占用内存)——》对象裁剪,不全部加载到内存

6.3、线上监控完整方案

  • 待机内存、重点模块内存、OOM率
  • 整体及重点模块GC次数、GC时间
  • 增强的LeakCanary自动化内存泄露分析

七、内存优化技巧

优化大方向:

  • 内存泄露
  • 内存抖动
  • Bitmap

优化细节:

  • LargeHeap属性(虽然有点耍流氓,但还是应该向系统申请)
  • onTrimMemory、onLowMemory(系统给的低内存的回调,可以根据不同的回调等级去处理一些逻辑)
  • 使用优化过的集合:SparseArray
  • 谨慎使用SharedPreference(一次性load到内存中)
  • 谨慎使用外部库(尽量选择经过大规模验证的外部库)
  • 业务架构设计合理(加载的数据是你能用到的,不浪费内存加载无用数据)

今天的内容又有点多了,先写到这里吧,留着慢慢消化啦,各位,下期再会!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章