程序性能优化之内存优化(三)上篇

☆樱花仙子☆ 提交于 2019-12-06 02:45:42

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将继续从以下两个内容来介绍内存优化:

  • [内存抖动]
  • [内存泄漏]
    <meta charset="utf-8">

其实大多数App或多或少都存在一定的内存泄漏情况,这些内存泄漏可能存在于特定的运行环境时才会发生。而内存泄漏堆积会引发严重后果OOM。内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时和内存泄漏一样会导致OOM。

接下来我们一起讨论该如何查看以及解决这部分问题思路。

一、内存泄漏

内存泄露是指程序中间动态分配了内存,但在程序结束时没有释放这部分内存,从而造成那部分内存不可用的情况,重启计算机可以解决,但也有可能再次发生内存泄露,内存泄露和硬件没有关系,它是由软件设计缺陷引起的。

简单点说:应该被释放的资源没有被释放。

1、内存泄漏的种类

1)常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

2)偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

3) 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

4) 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

2、为什么要修复内存泄漏

少量的内存泄漏可能不会引发什么问题;但是内存泄漏累积,再多的内存也会被耗尽,最终导致OOM。

二、定位内存泄漏

1、初步定位是否发生内存泄漏

借助Android Studio的Monitor查看是否发生了内存泄漏情况

通过反复的执行同一个功能,触发GC操作,观察内存前后变化情况。

 
2573196-7deed7484cab765e.png
image

如果内存前后未发生明显变化(增加)此时可以初步判断未发生内存泄漏。

 
2573196-c79dc76e16353525.png
image

比如此时内存使用情况为:21.04MB,然后我们打开一个新的Activity,然后返回执行GC操作,观察此时的内存使用情况。

2、Monitor栏基本功能说明:

 
2573196-46c65aba0f1b14ad.png
image

序号1、手动触发GC操作;

序号2、Dump Java Heap,获取当前的堆栈信息,生成一个.hprof文件,AndroidStudio会自动使用HeapViewer打开;一般用于操作之后检测内存泄漏的情况;

序号3、Start Allocation Tracking 内存分配追踪工具,用于追踪一段时间的内存分配使用情况,能够知道执行一些列操作后,有哪些对象被分配空间。一般用于追踪某项操作之后的内存分配,调整相关的方法调用来优化app性能与内存使用;

序号4、剩余可用 内存;

序号5、已经使用的内存;

3、Dump Java Heap进一步定位内存泄漏

通过Monitor栏只能初步粗略的观察是否发生内存泄漏,然而要真正的发现内存泄漏以及精确定位内存泄漏位置还需要借助相关工具分析排查。

点击Memory Monitor的Dump Java Heap,会生成一个.hprof文件,AndroidStudio会自动使用HeapViewer打开。

 
2573196-7ff6debb21b1cba3.png
image

面板说明:

面板1:

Detect Leaked Activities :检测泄漏的Activity

Find Duplicate Strings :查找重复的字符串

默认两个选项都是勾选的。

 
2573196-246ccfebd47eda46.png
image

点击绿色箭头,此时大家会看到Leaked Activities下有一个LaunchActvity@31...的信息,没错发生了内存泄漏,稍后我们分析如何发生的内存泄漏。

面板2:

Total Count:该类的实例个数

Heap Count:选定的Heap中实例的个数

Sizeof:每个实例占用的内存大小

Shallow Size:所有该类的实例占用的内存大小

Retained Size:该类的所有实例可支配的内存大小

面板3:

Instance:该类的所有实例对象(左侧Total Count为15,此处就有15个对象)

Depth:深度, GC Root点到该实例的最短链路数

Dominating Size:该实例可支配的内存大小

 
2573196-434e3448f9461b03.png
image

此时发现面板下有个实例存在。

面板4:

Reference Tree:引用树

 
2573196-237c77938db9d230.png
image

通过面板1我们发现有一个Activity发生了泄漏。我们可以通过Reference Tree面板就可以跟踪到该实例的引用树关系。

首先第一行我们发现一个LaunchActivity实例存在,然后展开该实例进一步查看该实例的引用关系,第二行我们可以看出它是被LaunchActity匿名内部类持有(this$0),这个匿名内部类实例是callBack,紧接着会发现该实例在mPermissionUtil实例中持有。

此时,我们可以进入代码查看该callBack是什么,然后在mPermissionUtils的持有。

LaunchActivity中:

 
2573196-cc7f0c20145844b7.png
image

PermissionUitl中:

 
2573196-81b2592a082cb940.png
image

跟踪代码发现callback是一个接口,然后在LaunchActvity中调用了PermissionUtil的requestPermission(callback),然后将该callback赋值给PermissionUtil中成员引用,由于PermissionUtil是一个单例,然后new PermissionCallBack()匿名内部类会默认持有外部类引用,此时它将持有外部类LaunchActivity的实例,然后有赋值给了PermissionUtil中的成员引用,所以造成的内存泄漏。这种内存泄漏称之为一次性内存泄漏,只会发生一次且只会泄漏最后一次调用者。

通过使用Androd Studio自带的Dump Java Heap排查内存泄漏问题对于相对简单的泄漏场景比较适合,如果发生较为复杂的泄漏场景可能使用Dump Java Heap不太容易查找问题。此时我们可以借助另外一个工具:MAT (Memory Analyzer Tool)

4、MAT

Memory Analyzer Tool是Eclipse的一个插件,它的使用以及安装这部分资料非常多,故篇幅原因不在展开分析介绍。

下载地址:https://www.eclipse.org/mat/downloads.php

荐:https://blog.csdn.net/u010335298/article/details/52233689

荐:https://blog.csdn.net/itachi85/article/details/77075455

 
2573196-09b193ebd27d1961.png
image
 
2573196-5306db4d67b20687.png
image

5、其他

我们也可以借助第三方检测库,在运行期间检查内存泄漏情况:LeakCanary

LeakCanary是square出品的一个检测内存泄漏的库,集成到App之后便无需关心,在发生内存泄漏之后会Toast、通知栏弹出等方式提示,可以指出泄漏的引用路径,而且可以抓取当前的堆栈信息供详细分析。

 
2573196-cccf6cb485473b9d.png
 

分析内存泄漏主要是定位GC Root,只有明白GC Root点才能够准确分析定位内存泄漏问题。

三、内存抖动

内存抖动是指内存在短时间内频繁地分配和回收,而频繁的gc会导致卡顿,严重时和内存泄漏一样会导致OOM。

内存抖动为什么会造成OOM这关系到Java的垃圾回收。

1、常见内存抖动场景

循环中创建大量临时对象;

onDraw中创建Paint或Bitmap对象等;

2、内存抖动后果

瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短****。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap的压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。

 
2573196-3f43b4229a2e5b9e.png
 

四、onTrimMemory与onLowMemory

Android系统的每个进程都有一个最大内存限制,如果申请的内存资源超过这个限制,系统就会抛出OOM错误。

onTrimMemory

所以在实际开发过程中我们要尽可能避免内存泄漏与内存抖动之外,还要格外注意内存使用情况。根据《Manage Your App's Memory》,我们可以对内存的状态进行监听,我们的Application、Acivity、Service、ContentProvider与Fragment都实现了ComponentCallbacks2接口。所以能够重写onTrimMemory与onLowMemory函数。

 
2573196-b10dc8ba79ef4a2a.png
 

onTrimMemory的参数是一个int数值,代表不同的内存状态:

TRIM_MEMORY_RUNNING_MODERATE:

你的应用正在运行并且不会被列为可杀死的。但是设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache中的Process的机制。

TRIM_MEMORY_RUNNING_LOW:

你的应用正在运行且没有被列为可杀死的。但是设备正运行于更低内存的状态下,你应该释放不用的资源用来提升系统性能。

TRIM_MEMORY_RUNNING_CRITICAL:

你的应用仍在运行,但是系统已经把LRU Cache中的大多数进程都已经杀死,因此你应该立即释放所有非必须的资源。如果系统不能回收到足够的RAM数量,系统将会清除所有的LRU缓存中的进程,并且开始杀死那些之前被认为不应该杀死的进程,例如那个包含了一个运行态Service的进程。

当应用进程退到后台正在被Cached的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:

TRIM_MEMORY_BACKGROUND:

系统正运行于低内存状态并且你的进程正处于LRU缓存名单中最不容易杀掉的位置。尽管你的应用进程并不是处于被杀掉的高危险状态,系统可能已经开始杀掉LRU缓存中的其他进程了。你应该释放那些容易恢复的资源,以便于你的进程可以保留下来,这样当用户回退到你的应用的时候才能够迅速恢复。

TRIM_MEMORY_MODERATE:

系统正运行于低内存状态并且你的进程已经已经接近LRU名单的中部位置。如果系统开始变得更加内存紧张,你的进程是有可能被杀死的。

TRIM_MEMORY_COMPLETE:

系统正运行于低内存的状态并且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释放任何不影响你的应用恢复状态的资源。

TRIM_MEMORY_UI_HIDDEN:

UI不可见了,应该释放占用大量内存的UI数据。

比如说一个Bitmap,我们缓存下来是为了可能的(不一定)再次显示。但是如果接到这个回调,那么还是将它释放掉,如果回到前台,再显示会比较好。

onLowMemory

这个函数看名字就是低内存。这个函数的回调意味着后台进程已经被干掉了。这个回调可以作为4.0兼容onTrimMemory的TRIM_MEMORY_COMPLETE来使用

如果希望在其他组件中也能接收到这些回调可以使用上下文的registerComponentCallbacks注册接收,

unRegisterComponentCallbacks反注册

五、OutOfMemeory

OOM就是申请的内存超过了Heap的最大值。

 
2573196-2ffeb0e0902045e4.png
 

OOM的产生不一定是一次申请的内存就超过了最大值,导致oom的原因基本上都是一般情况,我们的不良代码平时”积累”下来的。

我们知道Android应用的进程都是从一个叫做Zygote的进程fork出来的。并且每个应Aandroid会对其进行内存限制。我们可以查看:

 
2573196-b1f8edb837e48161.png
 

六、有效减少内存占用的建议

1、使用Android优化过后的集合

在Android开发时,我们使用的大部分都是Java的api。其中我们经常会用到java中的集合,比如HashMap。

使用HashMap非常舒服,但是对于Android这种内存敏感的移动平台,很多时候使用这些Java的API并不能达到更好的性能,相反反而更消耗内存,所以针对Android,google也推出了更符合自己的API,比如SparseArray、ArrayMap用来代替HashMap在有些情况下能带来更好的性能提升。

注意:此处仅考虑内存占用情况,并且在一定的长度的数据集,并不是适合所有场景下。

2、集合初始长度

如:HashMap,他的默认长度为16,负载因子为0.75,如果我们知道要存放数据的长度如5,此时最合适的HashMap的初始容量为:5/0.75 = 7;

故:HashMap map = new HashMap(7)

3、Bitmap

Bitmap可以说是一个内存中的大胖子,作为现在Android开发程序是比较幸福,有很多关于图片加载优秀的库,如Glide。

有关于Bitmap的优化我们会在后续单独专题中介绍,故不在此处展开介绍。

荐:《Handling Bitmaps》

4、try{}cacth(Error){}

对高风险OOM代码块如展示高清大图等进行try catch,在catch块加载非高清的图片并做相应内存回收的处理。注意OOM是OutOfMemoryError,不能使用Exception进行捕获。

5、解决所有内存泄漏问题

少量的内存泄漏可能不会带来较为明显的影响,但是内存泄漏堆积的后果是非常严重的,再多的内存也会被耗尽,最终导致OOM发生。

6、避免内存抖动

尽量避免在循环体或者频繁调用的函数内创建对象,应该把对象创建移到循环体外。

另外还有一个经典的String拼接创建大量小的对象造成的内存抖动。

有时会发现频繁的调用Log打印日志,App会变卡顿。

Log.i(TAG,width+”x”+height);

这里会产生2个新对象,width+”x”与width+”x”+height。

而TAG与x是编译时就存在的字符串常量池,所以不算新对像。

所以一般来说我们会对日志输出Log进行控制,或者使用StringBuilder进行优化。

7、onTrimMemory根据不同的内存状态做相应处理

对于未实现ComponentCallbacks2组件,我们需要为其注册ComponentCallbacks2。

 
2573196-de185f1c8ae1ab75.png!thumbnail
 

七、总结

性能优化是一个长期实践过程;大多数问题都是由一般问题造成的,然后这部分一般问题的积累最终会引发严重后果;压死骆驼的可能就是最后一根稻草****。在项目实际开发过程中:要特别注意内存泄漏与内存抖动的场景,注意配合使用onTrimMemory完成内存的管理工作。

阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
原文链接:https://www.jianshu.com/p/607914695e9e

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!