或许,因为开发周期的原因;因为自身知识水平的原因;因为经验的原因;又或者是你接了个烂摊子。我们写出了并不太理想的代码,这都是可以接受的,只要你会去持续优化,这些问题都会得到改善。而有些人是心有余而力不足,“我也想优化,可是怎么去优化呢?”。本篇文章将给你带来一点启示,让你从力不从心到知道怎么去入手优化。
01
一、为什么需要做内存优化?
尽管现在的手机硬件越来越好,手机的RAM也是越来越大。但是,分配给应用的每个进程的内存也是有限的。如果我们对开发的APP占用手机的内存大小无动于衷,轻则频繁的内存泄漏,重则引起用户操作卡顿甚至引发OOM导致应用崩溃,导致用户流失。也许我们没办法做到完美,但是很多时候我们只要持续的追求卓越,比竞争对手优秀一点,成功的或许就是你。优秀的人总是不满足于现状,总是精益求精,总是想把事情做到更好。保持谦逊!保持进步!
02
二、讲内存之前不得不讲虚拟机
Android在4.4之前一直用的都是Dalvik虚拟机(以下以DVM简称),在Android 4.4的时候推出可选择的ART虚拟机并且在5.0的时候全面抛弃DVM而完全使用ART代替之。在对比这两者之间的区别之前,笔者想先给大家普及一些基础知识。为什么会有虚拟机这东西?清楚的可以自行跳过这段。
我们都知道,我们的电脑或者其他硬件设备只认识二进制的机器码(例如0101)的。当我们用一个高级语言(C/C++/Java等等)写出的程序机器是没有办法识别的。所以才有了编译器的作用,例如当你用C/C++写了一段漂亮的程序,通过编译器将这段代码翻译成了机器能识别的机器码(0101),然后机器便识别到0101代表了某一条指令就去执行了。那么问题来了,当我们想让机器去干某件事的时候,例如显示一个警告弹窗。在windows上的指令可能是010101(举例,并非真实指令),而在Linux上定义警告弹窗是101111(同样为举例,以下所有涉及的指令仅为举例需要)。所以我用C/C++写出了弹一个警告窗的代码,在Windows的编译器编译下生成了010101的代码,当我们拿着010101的代码去在Linux系统中执行时,糟糕!可能在linux系统中010101代表关机指令,更有甚者根本就没有这条指令。所以我们需要在Linux系统中重新编译生成101111指令,这就非常繁杂。如果在不同的系统平台上我都要分别去使用所在平台的编译器编译生成它们对应的机器码去执行(参考下图)。这就给应用的移植带来很大的困难。
聪明的人类总是能想到好办法,虚拟机的概念从空而降。以Java来讲,当我们用Java写出了一段Java代码,编译器讲Java编译成Java虚拟机(JVM)能识别的.class文件。只要生成了.class文件,我们无论放在Windows上还是Linux中,只要对应平台安装了Java虚拟机,.class文件都能够愉快的被虚拟机执行。我们前面不是讲不同平台机器指令不同的嘛!怎么这里同样的.class文件就能在不同的平台上执行了呢?这就归功于Java虚拟机了,当我们在不同的平台上安装了虚拟机,Java虚拟机会将同样的.class文件,在不同的平台上使用不同的指令去执行。也就是讲,JVM帮我们去处理不同平台的指令关系了,我们只需要写出统一的JVM能识别的class文件就可以了(见下图)。这就极大的方便了我们开发跨平台的应用了。
总结:所以虚拟机的出现就是方便开发者更容易便捷的开发出跨平台的应用。
我们都知道Android最终是将Java代码编译成.dex文件装载到虚拟机中去的,DVM是基于JIT(Just In Time),即在执行的时候实时的将部分dex文件翻译成机器码进行执行的,这样带来的问题是执行比较耗时,慢!所以Android推出了基于AOT(Ahead Of Time)的ART。它是在应用被安装的时候提前将.dex文件翻译成机器码放入手机中,当程序被执行的时候无需在实时的翻译,而是直接执行。速度较与DVM来说更快速。由于减少了在运行时的翻译工作,减少了CPU的占用,因为CPU的消耗减少从而间接的减少电量的消耗。
在谈论如何管理内存的时候,我们通常都会在内存分配和回收两个方面阐述。每当我们的一个应用程序启动时,zygote进程就会folk一个进程作为应用程序的进程,并且与zygote进程共享分配内存的堆。当发生应用程序或者对对堆进行写操作时,就会对当前的堆分别做拷贝应用进程和zygote进程。因为拷贝工作是一件费时的事情,所以Dalvik在第一次执行拷贝之前会将当前堆分为两个部分:zygote堆(zygote进程已经使用的部分,主要是一些预加载的类、资源、和对象)和active堆(未使用的部分)。当应用程序进程需要分配对象的时候,会在active堆中分配。如下图所示:
①(此阶段不允许其它线程工作):标记根集对象,所谓根集指的是被全局变量、栈变量和寄存器等引用的对象
②(此阶段允许其它线程正常工作): a、标记被根集对象引用的对象; b、使用Card Table标记在执行当前阶段的时候,有线程修改了的对象,被修改过就置为DIRTY,未被修改过的置为CLEAN。
③(此阶段不允许其它线程工作):对在第②阶段标记为DIRTY的对象重新标记(因为引用关系可能发生了变化)
Dalvik分别使用Live Heap Bitmap和Mark Heap Bitmap分别进行标记。为什么需要两个对象来标记呢?
举个栗子:一个酒店的十间房子住了十位客人,我们用LiveHeapBitmap分别对十间房间标记为1,当有一位客人退房离开时,我们将房间重新打扫,并且将该房间标记0,表示房间为可用状态。当某一天又有部分客人需要退房时,我们只会对剩下的9间房子重新查看是哪几位客人需要退房,并使用MarkHeapBitMap将剩下的未退房的标记为1,没有被标记为1的默认都是0。显然如果有3位客人退房,MarkHeapBitmap中标记为1的有6间房,0的有4间房,LiveHeapBitmap中标记为1的有九间房,显然我们需要重新打扫房间的是LiveBit中的九间房减去MarkBit中的6间房,剩下的三间房才是需要去清理的。
文字描述有点模糊抽象,我们以图来说明:
首先,第一步。我们当前的状况是九间住房,一间已经清洁过的房间。
然后,第二步。有三间房客退房了,我们需要判断是哪三间需要做清理工作。
上图表示,当前markHeapBitmap中扫描到有1~6号房有占用,将其markBits标记为1,剩下4间标记为0;而LiveBits还是9间标记为1(只有执行清洁后的房间才能标记为0,如果退房了,但没有清洁当然还是1)。所以,简单点讲。当前markBits告诉我们又4四间房是空的,但是我们显然不需要都去清洁,因为有一间房是清洁过的,所以我们只要判断liveBits为1,markBits为0的房(7~9号房)是我们需要去清洁的。上面退房的过程可以理解为从被引用到未被引用的过程,清洁的过程就是GC清理的过程。所以可以简单理解是Dalvik这么设计标记清理的思路了。
上图的流程图基本反应了ART分配内存的过程,这里不对细节去做更详细的剖析了。有兴趣的读者可以针对源码去一探究竟。
03
三、优化内存
如果是同样的效果能够使用更少的内存分配,更少的触发GC的发生。这样从源头上解决了内存不够使用的问题。
a、使用字符串拼接的时候优先考虑StringBuffer。如果你使用String会都分配很多次内存,而使用StringBuffer只会分配一次内存,后面如果要追加也是在原有的地方进行追加,不会像String需要重新开辟新的内存存放。
b、变量在使用的时候才去去申明和实例化。局部变量的生命周期比全局变量的生命周期总会短,尽量不要过早或者不必要占用内存。
c、尽量避免使用static,这个要尽量,有些必须为static的不要强求。
当虚拟机为你分配的一块内存在你不需要的时候无法回收,这就是内存发生泄漏。发生内存泄漏不会立刻导致你的应用发生崩溃,但如果内存泄漏多了,势必会造成内存不够用导致OOM的崩溃发生了。那么哪些情况下容易发生内存泄漏呢?
a.Context别乱传 当你有一个单例类,构造方法里面千万别将Activity的Context作为参数传进去,如果必须要Context,可以使用Application的Context代替。还有如果是普通的类,需要传入Activity的Context,最好使用弱引用,以便内存的释放。
b.非静态匿名内部类 因为非静态内部类自动获得外部类的强引用,而且它的生命周期甚至比外部类更长,这就可能会出现内存泄露。如果一个 Activity 的非静态内部类的生命周期比 Activity 更长,那么 Activity 的内存便无法被回收导致泄漏,而且还有可能发生空指针问题。我们可以使用静态的内部类代替,这样就和外部类的实例断绝了引用关系。
c.静态集合要置空 集合会引用存储的对象,静态的集合生命周期与应用一样,导致存储的对象的内存无法释放,所以在不用的时候一定要将集合置空。
d.注册和反注册
当我们注册一些receiver或者EventBus等等,一定要在activity销毁的时候反注册,不然很容易导致activity还在被引用而无法释放内存。
e.文件流 使用文件流操作时,结束的时候务必一定要关闭。
f.Bitmap 如果你的Activity大量的使用Bitmap时,记得一定要在Activity被销毁前做释放操作。
04
四、内存分析工具
我们可以借助 ook开源的内存检查工具,当你的应用发生内存泄漏时,它能记录内存泄漏的地方,以及整个引用栈。这个是非常棒的工具,简单明了,笔者从15年的项目到现在的项目都有在用。
AndroidStudio 3.0推出了这个分析工具,其中的Memory Profile可以帮助我们分析内存的占用情况。这里不做详细介绍了,读者可以去官方的中文文档中学习下方法:
--https://developer.android.google.cn/studio/profile/memory-profiler