公众号【刘桂林】

App进行到最终的测试的时候,往往会出现一些性能上,以及内存上的问题,需要优化,这也是一个Android高级工程师所需要了解并且掌握的知识点,内存这个小妮子比较调皮,每个月总有那么几次泄漏或者溢出(OOM),这篇文章所讲的是内存溢出,这里要注意,内存溢出和内存泄漏是两个概念,这点大家要清楚,当然,内存泄漏过多会导致内存泄漏,至于什么是内存泄漏呢,大家都知道我们的内存回收机制是GC,所以用一句话来概括:GC回收机制所无法回收的垃圾对象。

如果把垃圾回收机制比喻你在用餐,而服务员会来收盘的话,那么理想中的状态便是你吃完饭一走,服务员就把盘子收走了,即对象用完GC自动回收,但是这里却只是理想中的样子,实际上,对于Android的内存管理机制和回收机制,Android系统的一个内存管理机制,被称为Low Memorry Killer的一种管理机制,其实就是根据优先级去kill掉一些优先级较低的程序,而回收机制就比较佛系了,叫做GC,采用的也是懒人机制,你不需要用的变量,对象等,你放那里就好,系统会在Heap剩余空间不够的时候去回收,并且有一个隐患,即GC触发后,所有的线程都会被暂停。
内存

要了解内存泄漏,我们首先了解内存,我们都知道Android系统的底层是Linux,并且他运行是一个沙箱机制,即每个App对应独立运行在一个虚拟机中,并且有一个进程,这也延伸出了多任务机制,并且每个App都是独立的,即使你崩溃了也不会对系统造成影响,如果想看进程,可以使用ps命令:
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_android

并且每个进程都有一个pid,按照顺序分配的,可以发现,init进程就是第一个,这个我们不做深究,你知道有这么一回事儿就行了

我们可以再次输入一个命令:dumpsys meminfo packagename
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_ide_02
这里我们就可以看到更多的内存信息了,统计了一些物理内存,虚拟内存使用情况以及统计,里面有三个参数,Heap Size , Heap Alloc ,Heap Free ,指分配了多少内存,使用了多少内存,剩余多少内存,一般 Heap Size = Heap Alloc + Heap Free (1985 = 1374 + 611),这里单位是K。

讲完系统,我们再化大为小,说一下App,其实App在内存中安装后,系统会预分配一个最大内存,这跟沙箱机制有一定关系,每家的系统都是不一样的,我们可以通过代码去读取出来:

ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
//最大内存
int mc = am.getMemoryClass();
//Large最大内存
int lm = am.getLargeMemoryClass();

这里有一个Large,实际上我们之前就用过,也就是开启硬件加速后的最大内存,如何开启硬件加速,则需要在清单文件的Application根节点添加android:largeHeap=“true”

不过大部分厂商会禁止此功能

我们把应用跑在模拟器上可以得到如下的数据:mc = 96 lm = 256

也得到了一个结论,即我这套模拟器给我的这个Demo App分配的最大内存为96,开启硬件加速后为256,这里的单位是M,但是大部分真机,这两个数值都是一样的,这里不曾考究,自行探索下。

其实这里要提一下,现在大部分的App其实所考虑的什么所谓的内存优化,都是因为图片过多,所以我们真正考虑的,还是如何有效率的优化图片给App带来的负荷,图片吃内存比较大。

我们继续来说一下Low Memorry Killer内存管理机制,这里涉及要当你的App切入后台后的管理方式,实际上可以用四个字概括:先进先出,当你的应用进入后台并且开始启动kill机制或者内存不够的时候,会优先清理任务栈最底层的应用,也就是最先开启的应用,而近期应用则相当于保护起来。这种机制叫做:LRU Cache (缓存淘汰)算法。

并且当系统内存存在变化的时候,可以通过Application的onTrimMemory方法监听

@Override
public void onTrimMemory(int level) {
// level 等级
super.onTrimMemory(level);
}

这里的内存等级是这样划分的:

int TRIM_MEMORY_BACKGROUND = 40;
int TRIM_MEMORY_COMPLETE = 80;
int TRIM_MEMORY_MODERATE = 60;
int TRIM_MEMORY_RUNNING_CRITICAL = 15;
int TRIM_MEMORY_RUNNING_LOW = 10;
int TRIM_MEMORY_RUNNING_MODERATE = 5;
int TRIM_MEMORY_UI_HIDDEN = 20;

我们有好几种方式可以监听到内存的使用情况和波动,这里我一一道来,首先,我们知道,当我们打开手机的设置 - 应用 - 对应的某一个App的时候就可以看到这个App的使用情况,实际上我们可以通过代码的方式来获取:

float totalMemory = Runtime.getRuntime().totalMemory() * 1.0f / (1024 * 1024);
float freeMemory = Runtime.getRuntime().freeMemory() * 1.0f / (1024 * 1024);
float maxMemory = Runtime.getRuntime().maxMemory() * 1.0f / (1024 * 1024);

这样获取到运行时的内存情况了,我们可以看下数据:
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_内存泄漏_03
当然,你也可以通过Android Profile 查看

Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_MAT_04

也可以通过Android Monitor查看
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_性能优化_05

其实在面试中也经常有人会被问到内存优化的方法,只能说内存控制方面有很多的小技巧,但是归根结底还是要你自己有一个良好的代码习惯,当然,如果真发生了错误,比如内存泄漏或者溢出,那么你也应该知道如何去解决这些问题。

内存泄漏
如果想解决内存泄露,那么我们应该如何找到问题的根源尼?如果你只是一味的看内存增长是找不到问题所在的,应该内存泄漏如果不严重是察觉不到的,这里我们可以来写一段这样的代码:

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

findViewById(R.id.finish).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});

new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(15000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}

这段代码我只需要启动后点击退出按钮,再启动,再点击,那么Activity就会每次都finish掉,但是子线程却一在运行,Runnable是持有Activity对象的,这样我们就可以看到如下的Memory走势图
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_ide_06

我反反复复的启动后finish,最终的结果将原本15.7MB的内存变成25.2MB,并且还会无限增加,最终导致内存溢出
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_android_07

那好,如果项目庞大的话,光这样看是定位不到的,我们可以这样来:先点击Profile app
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_内存泄漏_08

然后在下面的Profiler中点击Record,然后开始使用App,当看到波形变动的时候再点击Stop
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_ide_09
这样就会出现如下的文件列表,这里可以选择按照包名分类
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_性能优化_10
可以看到,这里的MainActivity出现了3个实例,这肯定是有问题的,也就定位到了发生溢出的界面为MainActivity。但是到这里还只是定位到了Activity,我们还可以更加精确一点,我们点击Record按钮旁边的下载按钮,然后点击保存hprof文件
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_MAT_11

有了这个文件之后我们就可以进一步使用MAT工具来分析了

MAT
mat工具是Eclipse的,没有的话可以到这里去下载:

MAT工具下载:​​http://www.eclipse.org/mat/downloads.php​

下载后如图:
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_性能优化_12
打开之后就是我i们久违的Eclipse风格了
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_ide_13
但是这里还不能直接导入,因为Android Studio导出的hprof文件并不是MAT标准的文件,所以我们需要用到SDK目录下的platform-tools下hprof-conv.exe工具,在此目录下进入cmd,通过命令:hprof-conv old.hprof new.hprof 来转换文件:
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_性能优化_14
现在我们可以回到MAT点击菜单栏的File - Open Heap Dump 导入new.hpfof
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_内存泄漏_15
只需要点击Create a historam from an arbitray set of objects 也就是这个小图标,即可生成分析表
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_android_16
然后我们在这里输入过滤:
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_内存泄漏_17
到这里就很明朗了,我们继续缩小范围
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_android_18

右键选择 List object - with outgoing references ,这个的意思是查看外部所引用的对象
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_ide_19

然后继续过滤一下,并且右键 选择 Merge Shortest Paths to GC Roots - exclude all phantom/weak/soft etc .references 这个的意思是排查所有的弱引用,虚引用
Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_android_20
到这里你是否有一种恍然大悟的感觉,我们过滤之后只剩下一条Thread的错误,而所指向的对象为this$0,也就是他本身,意思是 子线程中所持有本类对象,那联想到内存溢出是我们退出后所引起的,所以最终得到的结论:Activity已经退出,但是子线程仍然持有本类对象所导致内存泄漏。

LeakCanary
当然,上述的方法我更多的倾向于你所了解这个一个追述的过程,毕竟有些繁琐,所以这里再教大家使用一款工具 —— LeakCanary

Github:​​http://github.com/square/leakcanary​

我们在app/build.gradle下配置:

implementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'

最新的v2.0-alpha-1貌似有些问题,所以我还是使用稳定版本,在Application中增加

public class BaseApp extends Application {

@Override
public void onCreate() {
super.onCreate();

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

这样我们就可以正常的运行了,当发生内存泄漏的时候就会通知栏提示:

Android 性能优化之内存泄漏,使用MAT&LeakCanary解决问题_内存泄漏_21

到这里,基本上本章内容也讲完了,当然,这也只是一些皮毛而已,当你的项目足够大的时候,做这项优化工作还是比较繁琐的,所以最好还是尽量保持良好的编码习惯才是最重要的。