Android 内存性能优化

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

  1. 多线程相关的匿名内部类/非静态内部类

匿名内部类也会持有外部类实例的引用。多线程相关的类有AsyncTask、Thread和实现Runnable接口的类等。

它们的匿名内部类/非静态内部类如果做耗时操作就可能发生内存泄漏。

  1. Handler内存泄漏

Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,它们在MessageQueue中存在的时间会很长,这就会导致Handler无法被回收。如果Handler是非静态的,则Handler也会导致引用它的Activity或者Service不能被回收。解决方法有两个:

(1)一个是使用静态的Handler内部类,Handler持有的对象要使用弱引用

(2)在Activity的donDestroy去 移除MessageQueue中的消息通过使用: removeCallbacksAndMessages(null)

  1. 未正确使用Context

=对于不是必须使用Activity的Context的情况,我们可以考虑使用 Application Context来代替Activity的Context,这样可以避免Activity的泄漏,比如单例模式。

  1. 静态View

使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致Activity无法被回收,解决办法就是在onDestory中将静态View置位null。

  1. WebView

不同Android版本的WebView会有差异,加上不同厂商定制的ROM的WebView的差异,这就导致WebView存在着很大的兼容性问题。WebView都会存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。通常解决办法是为WebView单开一个进程,使用AIDL与应用的主进程进行通信。然后这个进程可以在适当的时机销毁。

  1. 资源对象未关闭

资源对象比如Cursor、File等,往往都使用了缓冲,会造成内存泄漏。因此,在资源对象不使用时,一定要确保它们已经关闭并将它们的引用置为null。通常都在 finally语句中进行关闭,防止出现异常时,资源未被释放的问题。

  1. 集合中对象没有清理

通常把一些对象的引用加入到了集合中,当不需要该对象的时候,如果没有把他它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是 static的话,那情况就会更加严重。

  1. Bitmap对象

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

  1. 监听器未关闭

很多系统服务需要 registerungister监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动添加的Listener要记得在合适的时候及时移除这个 Listener

2. Memory Monitor

===================================================================================

2.1 使用Profiler


这里就开始讲解内存分析的工具,本节就来讲解Memory Monitor。

在AS中Android Monitor是一个主窗口,它包含了Locat、Memory Monitor,CPU Monitor、GPU Monitor和Network Monitor。

但是在Android Stduio3.0后,AS优化了Android Monitor,把Android Moniter的东西整合到了 Profiler中,在Android Studio中连接手机然后打开应用,我们在Profiler中可以去查看这个窗口:

在这里插入图片描述

可以看到它分为四大块,CPU、MEMORY(内存)、NETWORK(网络)、ENERGY(电量)。

其中 Memory Monitor可以轻松监视应用程序的性能和内存使用情况,以便找到被分配的对象,定位内存泄漏,并跟踪连接设备中正在使用的内存数量,我们点开MEMORY,它的界面是这样的:

在这里插入图片描述

Memory Monitor可以报告出你的应用程序的内存分配情况,更形象地呈现出引用程序使用的内存。

它的作用如下:

  • 实时显示可用的和分配的Java内存的图表

  • 实时显示GC事件

  • 启动垃圾收集事件

  • 快速测试应用程序的缓慢是否和过度的垃圾收集事件有关

  • 快速测试应用程序崩溃是否与内存耗尽有关

而在上面的截屏中我标注了几个常用的该工具的功能,按照标注解释如下:

  1. Force garbage collection

手动触发GC

  1. Dump Java Heap

保存内存快照

  1. Allocation Tracking

后面解释

  1. Total

当前应用总共使用的内存

  1. Allocated

系统为当前应用分配的内存,图中标识N/A,说明应用没被限制使用内存

其中横轴是时间,纵轴是分配的内存。

2.2 大内存申请与GC


下图是一个大内存分配的场景--------分配得内存在短时间内急速上升:

在这里插入图片描述

我们需要判断这次是否是合理分配的内存,是Bitmap还是其他的大数据,并且对这种大数据进行优化,减少内存开销。

接下来分配得内存突然下降,这表示发生了GC,用来释放内存。

2.3 内存抖动


内存抖动一般是指 在很短的时间内发生了多次的内存分配和释放:

在这里插入图片描述

严重的内存抖动会导致应用卡顿。

内存抖动出现的原因是在短时间内频繁的创建对象(比如在循环中创建很大的对象),内存为了应对这种情况,也会频繁的进行GC,其他线程都会被挂起,等待GC操作完成后恢复工作。

如果是频繁的GC就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过16ms,最后导致界面卡顿。

如果出现了内存抖动,我们要查看我们的 循环、onDraw等会被执行多次的函数体。

因为 new一次对象是个耗时耗空间的操作,值得引起重视。

3. Allocation Tracker

=======================================================================================

Allocation Tracker用来跟踪内存分配,它允许你在执行某些操作的同时监视在何处分配对象,了解这些分配使你能够调整与这些操作相关的方法调用,用来优化程序性能和内存使用。

Allocation Tracker能够做到如下的事情:

  • 显示代码分配对象类型、大小、分配线程、堆栈跟踪的时间和位置

  • 通过重复的分配/释放模式帮助识别内存的变化

  • 当与HPROF Viewer结合使用时,可以帮助你跟踪内存泄漏。例如,你在堆上看到一个Bitmap对象,你可以使用Allocation Tracker来找到其分配的位置。

3.1 使用Allocation Tracker


我们在柱状图中,随便的截取一段,就能产生内存的分析:

在这里插入图片描述

我们能够看到一些对象,它总共的个数、大小,我们还可以根据类、包和线程来进行排序。

这些对象,分为可控和不可控两种类型:

  1. 不可控

一些对象是必然会产生的,是伴随着一些对象的产生而产生,并不是我们直接创建,比如我们必须的创建一个对象,而这个对象的构造函数里面会必然的形成一个 int[]String或其他,在一般情况下,我们不会去考虑这些 int[]String变量

  1. 可控

由我们直接的,手动的创建出来,比如通过 new等声明一个变量,它是在我们程序中直观的表示的出来的。

在上面的图片中,我们随便点击一个对象,如果这个对象是可控的,那么我们就能追溯到代码中,如下图:

在这里插入图片描述

图中,我选取了一个 LinearLayout(标注1),可以看到LinearLayout在程序中有多个Instance(标注2),也就是说在我们程序中写了很多个LinearLayout出来,然后我们随便点击一个一个,可以看到它创建的代码栈(标注3),然后我们在代码栈中,可以找到我们在程序中创建出来的地方(标注4)。

可以看到我是在一个RecyclerViewAdapter里面去new出了一个item,我的每一个Item的构造就是一个LinearLayout,而一个RecyclerView里面是有多个item,这也是为什么,在标注2中显示我在很短的时间里面创建出了这么多的 LinearLayout。

3.2 Heap Dump


Heap Dump的主要功能就是查看不同的数据类型在内存中的使用情况。

它可以帮助我们找到大对象,也可以通过数据的变化发生内存泄漏。

我们通过点击 Dump Java Heap或者 Crtl + D来捕获一段Heap Dump:

在这里插入图片描述

可以看到其分析原理也差不多,不过它可以通过点击最左边的图标的保存文件,保存成一个 .hprof文件,便于我们后面使用MAT工具进行分析。

我们可以看到这个图和之前分析内存那个有点不一样,这个图最右边的 Refernces表示的是该对象的信息:

  • Depth

GC Roots到达该对象的层数。

也就说GC Roots构成的树里面,这个对象在第n层

  • Native Size

C/C++层中的内存大小(B)

  • Shallow Size

这个对象在Java层中的内存大小(B)

  • Retained Size

这个类中所引用到的对象的总大小(B)

分析你的heap,按照一下步骤.

  1. 浏览Class Name列表,看看有没有大量对象存在,并且这些对象你认为是不应该存在的,可能存在内存泄漏的情况. 点击类名可以看到详细的对象信息.

  2. 在这个Instance View面板中,点击一个实例References面板就会显示出来,里面都是使用该Instance的Reference,点击箭头可以看到引用它的所有区域。点击鼠标右键可以选择go to instance去看到引用该引用的引用,或者jump to source去看调用的源代码.

4. 内存分析工具MAT

==============================================================================

在上面的Heap Dump中我们提到了一个 .hprof文件,在我们分析内存的时候,我们有时候无法很确定内存泄漏的地方,我们会通过 Heap Dump来生成一个 疑似发生内存泄漏时所产生的 堆存储文件,它就是 .hprof文件。

MAT就是用来分析 .hprof文件的。

MAT的全称为 Memory Analysis Tool,是对内存进行详细分析的工具,它是Eclipse的插件,AS需要单独下载。

下载地址在 MAT官网下载网址

我们打开来运行一下刚刚生成的 .hprof文件,可能报以下的错误:

…Unknown HPROF Version (JAVA PROFILE 1.0.3)…

这个意思的是我们由Android虚拟机转出来的.hprof文件和 MAT所分析的 .hprof(用JVM)的格式不一样,我们需要转化文件格式。

我们先通过命令行,进入到 该.hprof所在目录,比如我有一个叫做 android.hprof由Android导出来的 ,然后执行下面命令:

hprof-conv android.hprof mat.hprof

就会在该目下出现一个 mat.hprof的文件,我们就可以用这个文件导入啦:

在这里插入图片描述

下面我们来举个实例,来介绍怎么使用MAT。

4.1 生成一个hprof文件


我们先准备一份内存泄漏的代码出来:

public class MainActivity extends AppCompatActivity {

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

LeakThread leakThread = new LeakThread();

leakThread.start();

}

class LeakThread extends Thread {

@Override

public void run() {

try {

Thread.sleep(60 * 60 * 1000);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

}

上面做了一个 非静态内部类LeakThread持有外部 MainActivity的引用,LeakThread中做了耗时操作,导致MainActivity无法被释放。

接着我们运行它,产生一个 .hprof文件。然后我们打开Profiler,横竖屏切换,然后导出一个 .hprof文件,然后通过 hprof-cov转换它,最后拿转换出来的文件导入到 MAT中。

在这里插入图片描述

4.2 MAT分析hprof文件


用MAT打开hprof文件后,MAT就会产生一个报告,分为两个标签页,分别是 OverviewLeak Suspects

在Leak Suspects中给出了MAT认为可能出现内存泄漏问题的地方,在上图中共给出了2个内存泄漏的猜想,通过单击每个猜想的Details可以看到更深入的分析情况。如果内存泄漏不是特别明显,通过Leak Suspects很难发现内存泄漏的位置。

打开Overview标签页,首先看到的是一个饼状图:

在这里插入图片描述

它主要用来显示内存的消耗,饼状图的彩色区域代表被分配的内存,灰色区域表示空闲内存区。

再往下看,能看到饼状图下面有 Actions

在这里插入图片描述

其中分析内存泄漏最常用的就是 HistogramDominator Tree。下面我来介绍它怎么使用。

4.3 Dominator Tree


Dominator Tree的意思是支配树,它更善于去分析对象的引用关系,如下图所示:

在这里插入图片描述

左边主要是Class Name类名,右边是三列之前都看到过:

  • Shallow Heap

对象自身占用的内存大小,不包括它引用的对象。如果是数组类型的对象,它的大小由数组元素的类型和数组长度决定。如果是非数组类型的对象,它的大小由其成员变量的数量和类型决定。

  • Retained Heap

一个对象的Retained Set包含对象所占内存的总大小。

换句话说,Retained Heap就是当前对象被GC后,从Heap上总共能释放掉都少的内存。

Retained Set指的是这个对象本身和它持有引用的对象以及这些对象的Retained Set所占内存大小的总和,引用树官方的图解如下图:

在这里插入图片描述

从上图可以看出 E的Retained Set为E和G,C的Retained Set为C、D、E、F、G、H。

MAT所定义的支配树就是从上图中的树演化而来。

在引用树中,如果一条到Y的路径必然会经过X,则称为X支配Y。X直接支配Y则指的是在所有支配Y的对象中,X是Y最近的一个对象。支配树反映的就是这种直接支配关系,在支配树中,父节点直接支配子节点。

下图就是官方提供的一个从引用树到支配树的转换示意图。

在这里插入图片描述

上图中 由于C直接到达D和E,所以C是D和E的父节点。而到达H有两条路径 DF和EG,但是这两条是互斥的,对于互相来说都是不必要的,抽象一层,相当于C能直接到达H,所以C是H的父节点~

通过支配树,我们就能很容易的分析一个Retained Set所以 假如 E被回收,E、G的内存会被释放,而H不会。因为可能F还引用着H

所以通过MAT提供的Dominator Tree,我们可以很清晰地得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄漏。在Dominator Tree的顶部Regex可以输入过滤条件,如果查找的是Activity内存泄漏,可以在Regex中输入Activity的名称,比如这个例子可以输入MainActivity:

在这里插入图片描述

可以看到MainActivity出现了14个实例,而项目中MainActivity是不能有那么多实例的,基本可以判定发生了内存泄漏。

具体内存泄漏的原因,可以查看GC引用链,在MainActivity选项上单击右键->Path To GC Roots。

在这里插入图片描述

可以看到根据引用类型会有多种选项,比如 with all references就是包含所有的引用,这里我们选择exclude all phantom/weak/soft etc. references,因为这个选项排除了虚引用,弱引用和软引用,这些引用一般是可以被回收的,这是MAT就会给出MainActivity的GC引用链,如下图所示:

在这里插入图片描述

可以看到,引用MainActivity的就是LeakThread,this$0的含义就是内部类自动保留一个指向所在外部类的引用,而这个外部类就是MainActivity,这将会导致MainActivity无法被GC。

4.4 Histogram


Histogram与Dominator Tree不同的是,Dominator Tree是从对象实例的角度进行分析,注重引用关系分析,而Histogram则从类的角度进行分析,注重量的分析。

在这里插入图片描述

这里有四列,Obejcts表代表对象实例的个数。我们同样可以在这里输入过滤条件 MainActivity。

在这里插入图片描述

可以看到MainActivity和LeakThread的示实例各有6个,基本上可以断定发生了内存泄漏,具体内存泄漏的原因,同样可以查看GC引用链。在MainActivity选项上单击鼠标右键->Merge Shortest Paths to GC roots,并在选项中选择 exclude all phantom/weak/soft ets. references,Path To GC Roots的信息如下图所示:

在这里插入图片描述

Histogram是从类的角度进行分析,而Path To GC Roots是用来分析单个对象的,因此在Histogram中无法使用Path To GC Roots查询,可以使用 Merge Shortest Paths to GC Roots查询,他表示从GC roots到一个或一组对象的公共路径。得出的结果和之前是相同的,引用MainActivity的是Leak Thread。

4.5 OQL


OQL全称为Object Query Language,类似于SQL语句的查询语言,能够用来查询当前内存中满足指定条件的所有对象。

它的查询语句的基本格式如下:

SELECT * FROM [ INSTANCEOF ] <class_name> [ EHERE ]

我们输入 select * from instanceof android.app.Activity并按F5,将当前内存中所有的Activity都显示出来:

在这里插入图片描述

当然如果Activity太多,也可以输入具体完整的名称,然后就可以右键他们去查看GC引用链。

4.6 对比hprof文件


因为之前举得例子很简单,可以通过上面的方法可以找到内存泄漏的原因,但是复杂的情况就需要通过对比hprof文件来进行分析了。

我们再生成一个hprof文件然后用MAT打开。对比步骤为:

最后

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
往是自己摸索成长,自己不成体系的自学效果低效漫长且无助**。

因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-XGMik40b-1715339993245)]

[外链图片转存中…(img-jk1R7ds5-1715339993246)]

[外链图片转存中…(img-WGXRAs3K-1715339993247)]

[外链图片转存中…(img-2dC9y2P5-1715339993248)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

  • 19
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值