Android中的内存优化是我们性能优化中比较重要的一环,其主要包括两方面的工作:
- 优化RAM(Random Access Memory 也即主存,是与CPU直接交换数据的内部存储器,可以随时读写,而且速度快),即降低运行时内存。目的是防止程序发生OOM异常,以及降低程序由于内存过大被LMK机制杀死的概率。另外,不合理的内存使用会使GC大大增多,从而导致程序变卡。
- 优化ROM(Read-Only Memory 只能读出事先所存储的数据,之后无法改变或删除),即降低程序占ROM的体积,这里主要是为了降低程序占用的空间,防止由于ROM空间不足导致程序无法安装。
内存泄漏的监控方案:
Square的开源库leakcanary是一个不错的选择,其原理是,通过弱引用方式侦查Activity或对象的生命周期(将Activity或对象包装到WeakReference中,通过监听Application中activity生命周期回调,声明周期结束时,如果对象能被正常回收,弱引用会被放到ReferenceQueue队列里),若发现内存泄漏自动dump hprof文件,通过HAHA库得到泄漏的最短路径,最后通过notification展示。
通过兜底回收内存:
Activity泄漏会导致该Activity引用到的Bitmap、DrawingCache等无法释放,对内存造成大的压力,兜底回收是指对于已泄漏的Activity,尝试回收其持有的资源,泄漏的仅仅是一个Activity空壳,从而降低对内存的压力。做法也很简单,在Activity onDestory时候从view 的rootView开始,递归释放所有子view涉及的图片、背景、DrawingCache、监听器等资源,让Activity成为一个不占资源的空壳,泄漏了也不会导致图片资源被持有。
Drawable d = iv.getDrawable();
if(d != null){
d.setCallback(null);
}
iv.setImageDrawable(null);
我们不是只懂得一些内存泄漏解决方法就可以,更重要的是通过日常测试与监控,得到内存泄漏检测与修改的一整套闭环体系。
降低运行时内存的一些方法:当确保应用中不会出现内存泄漏后,我们还需要一些方法降低运行时内存,其实也是希望降低OOM的概率。
一、减少bitmap占用的内存。
- 防止bitmap占用资源太多导致OOM,可采用Facebook的fresco库,即可把图片资源放于Native中。
- 图片按需加载。即图片的大小不应该超过view的大小,在把图片载入内存之前,我们需要先计算出一个合适的inSampleSize缩放比例,避免不必要的大图载入。对此,我们可以重载drawable与ImageView,例如在Activity onDestroy时,检测图片大小与view的大小,若超过,可以上报或提示。
- 统一的bitmap加载器。有了统一的加载库,如果在加载时方式OOM,可以通过清除cache,降低bitmap format等方式,重新尝试。
- 图片存在像素浪费。对于.9图,美工可能在出图时拉伸与非拉伸区域都有大量的像素重复,通过获取图片的像素ARGB值,计算连续相同的像素区域,自定义算法判断这些区域是否可以缩放。关键也是需要将这些工作做到系统化,可及时发现问题,解决问题。
二、自身内存占用监控。
dalvik内存距离OOM的差值并没有体现,也没有回调函数供我们及时释放内存。如果能有一套机制,可是实时监控堆内存的使用率,达到设定值即通知相关模块进行释放,这会大大的降低OOM。
原理:通过Runtime获得maxMemory,而totalMemory-freeMemory即为当前真正使用的dalvik内存。
Runtime.getRuntime().maxMemory();
Runtime.getRuntime().totalMemory()-Runtime.getRuntime().freeMemory();
操作方式:我们可以定期(前台每隔3分钟)的去得到这个值,当我们这个值达到危险值(例如80%),我们应当主要去释放我们的各种cache资源(bitmap的cache为大头),同时显示的去trim应用的memory,加速内存收集。
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
三、使用多进程
对于WebView、图库等,由于存在内存泄漏或者占用过多的问题,我们可以采取单独的进程,微信会把它放在单独的tools进程中。
四、上报OOM详细信息
当系统放生OOM的crash时,我们应当上传更加详细的内存相关信息,方便我们定位当前内存的具体情况。
GC优化
大量的GC操作则会显著占用帧间隔时间(16ms),如果在帧间隔时间里做了过多的GC操作,那么其他类似计算、渲染等操作的可用时间就变得少了。
一、GC类型有以下几种,其中GC_FOR_ALLOC是同步方式进行的,对应用帧率的影响最大:
- GC_FOR_ALLOC:当堆内存不够的时候容易触发,尤其是new一个对象的时候,很容易被触发到,所以如果要加速启动,可以提高dalvik.vm.heapstartsize的值,这样在启动的过程中可以减少GC_FOR_ALLOC的次数。注意这个触发是以同步的方式进行的,如果GC后仍然没有空间,则堆进行扩张。
- GC_EXPLICIT:这个gc是可以被调用的,比如system.gc,一般gc线程的优先级比较低,所以这个垃圾回收的过程不一定会马上触发,千万不要认为调用了system.gc,内存的情况就有所好转。
- GC_CONCURRENT:当分配的对象大小超过384k时触发,注意这是以异步的方式进行回收的,如果发现大量反复的Concurrent GC出现,说明系统中可能一直有大于384k的对象被分配,而这些往往是一些临时对象,被反复触发了。给我们的暗示是:对象的复用不够。
二、内存抖动
Memory Churn,内存抖动是因为在短时间内大量的对象被创建又马上被释放。瞬间产生大量的对象会严重占用内存区域,当达到阀值,剩余空间不够的时候,会触发GC从而导致刚产生的对象又很快被回收。即使每次分配的对象占用了很少的内存,但是他们叠加在一起会增加Heap压力,从而触发更多其他类型的GC。这个操作有可能会影响到帧率,并使得用户感知到性能问题。
通过Memory Monitor(Android Studio3.0以后,被Android Profiler替代),我们可以跟踪整个APP的内存变化情况,若短时间内发生了多次内存的涨跌,这意味着很有可能发生了内存抖动。
三、GC优化
通过Heap Viewer,我们可以查看当前内存快照,便于对比分析哪些对象又可能发生了泄漏。更重要的工具是Allocation Tracker,追踪内存对象的类型、堆栈、大小等。这样,我们就可以快速的知道发生内存抖动时,是因为哪些变量的创建造成频繁GC。一般来说我们需要注意以下几个方面:
- 字符串拼接优化:减少字符串使用加号拼接,改为使用StringBuilder。减少StringBuilder.enlarge,初始化时设置capacity;
- 读文件优化:读文件使用ByteArrayPool,初始设置capacity,减少expand;
- 资源重用:建立全球资源池,对频繁申请、释放的对象类型重用;
- 减少不必要或不合理的创建对象:例如在onDraw、getView中应减少对象申请,尽量重用。更多是一些逻辑上的东西,例如循环中不断申请局部变量等。
- 选用合理的数据格式:使用SparseArray、LongSparseArray、SparseBooleanArray、SparseLongArray来代替HashMap。
Tip:SparseArray的key只能是int,LongSparseArray的key是long,能够存取的范围更大。其优缺点有如下,
优点:
- 避免存取元素时的拆箱、装箱,提高了效率。
- 频繁的插入和删除操作时效率高。
- 会定期通过gc来清理内存,内存利用率高。
- 放弃hash查找,利用二分查找,更轻量。
缺点:
- 二分查找时间复杂度为O(log n),没有HashMap效率高。
- key只能是int 或者long,而HashMap为Object。