【Android面试周】性能优化之内存篇

【Android面试周】系列文章会持续更新,订阅后的读者若在阅读中存在问题可随时添加评论,我会及时回复。


性能优化一直是高级Android工程师必问的,这也往往涉及到对Java、JVM、Android运行机制、监控工具使用等全方位的知识点,是很好的面试方向。

本篇会围绕Android与Java的性能优化专项之内存展开并深入。


内存是Android运行性能至关重要的一项指标,每个进程能使用的内存是有限的。不合理的使用内存会导致频繁的GC、甚至发生OOM,过多GC会导致App卡顿,而内存泄漏或者内存抖动都可以导致OOM,这是无法接受的。

因此,对于一个合格的高级Android工程师,必须保持对内存的高度敏感性,本文会针对内存提出一系列性能优化手段。

什么时候会导致频繁GC

  1. 内存抖动 短时间内创建了大量对象同时又被快速释放。比如在一个大循环里去不断创建对象,会导致频繁gc;
  2. 内存泄漏 内存泄漏会导致可用内存逐渐变少,而且内存碎片加多,这也会增多gc次数,甚至可能发生OOM
  3. 一次申请太大内存空间 由于内存碎片的存在,就算内存本身足够,但由于碎片导致无法找到一块大空间,这也会触发gc;

内存优化准则

1. 能不创建的对象就不创建

比如字符串拼接,可以手动使用StringBuilder,而不是使用"+","+"被编译器优化后会每次创建StringBuilder对象,造成浪费;

而且,尤其注意在主线程里不要过多创建对象。因为在GC时会锁住堆内存,此时请求分配的线程也会被挂起,这显然会导致主线程的卡顿。所以在一些主线程高频函数,如onDraw,onTouchEvent里不要去创建对象。

2. 尽可能复用已经创建的对象

还是StringBuilder的例子,基于一个StringBuilder可以通过SetLength(0)支持很多次的字符串拼接。

多使用系统提供的对象池,比如线程池,Long、Integer、Short等包类型里的缓存值(通过valueOf取),列表view的复用等

3. 防止内存泄漏

内存一旦发生泄漏,意味着堆里有一块区域持续被不再使用的变量占据,这自然会导致可用内存减少而发生gc,甚至OOM。

一般内存泄漏有几下几种情况:

I. 长生命周期持有短生命周期引用,如Activity泄漏

一般是长生命周期的变量持有Activity引用,导致在gc时无法标记Activity从而无法回收,而Activity本身一般会引用到非常多的资源如View, Image,则这些大块资源均无法回收。

这种时候,我们可以用WeakReference来弱引用;针对Activity这种场景可以使用LeakCanary来进行监控,它会在Activity onDestroy后监控其引用是否被释放,若未释放则主动触发一次gc,gc后如果仍未释放,则会通知开发者。

II. 静态变量和单例滥用

静态变量一般是在装载类里的链接时进行内存分配,初始化时进行赋值。关键是,静态变量会伴随Android进程整个生命周期,如果引用了某块堆内存,则该内存无法被回收。单例也是类似。我们在开发中不应该依赖太多静态变量和单例。

III. finalize来兜底

对于一些较大内存的对象,可以考虑利用finalize方法,防止在忘记释放时主动进行一些清理工作。像SQLiteDatabase、FileInputStream等都会在finalize方法里面进行一些清理工作,如关闭数据库。

不过,finalize方法并不是万能的,它也会导致其他问题,后续再说。

IV. 合理应用Java各种引用

Java里提供了四种引用:

  • 强引用StrongReference:这种引用的对象不会被GC回收,在堆里存活时间最久;
  • 弱引用WeakReference:在GC时一旦遇到WeakReference,无论堆内存空间是否足够都会进行回收,不过考虑到GC线程优先级很低,所以WeakReference能存活较长时间;(附:当WeakReference对象被回收后,它自身可被放入一个ReferenceQueue里,LeakCanary里便应用了这个特性来检测Activity是否已被回收)
  • 软引用SoftReference:当堆内存不足时,会对SoftReference进行回收,它存活的时间一般长于WeakReference。(附:SoftReference是在第三次内存分配失败时进行回收。流程:第一次分配失败->GC->第二次分配失败->扩容->第三次仍分配失败->清理SoftReference)
  • 虚引用PhantomReference:等于没有引用,随时随地都会被回收

Dalvik与Art的垃圾回收区别

© 著作权归作者所有
这个作品真棒,我要支持一下!
我是wingjay,目前就职于阿里。 本系列会基于我的工作与学习经验,通过横向、纵向的Android与Java知...
1条评论

比如字符串拼接,可以手动使用StringBuilder,而不是使用"+","+"被编译器优化后会每次创建StringBuilder对象,造成浪费;
这个说法并不完全正确普通,
比如下边这个例子:
String namePrefix = "demon";
String nameSplitSymbol = ".";
String nameSuffix = "li";
String name=namePrefix+nameSplitSymbol+nameSuffix;
编译器优化之后只会创建一个new StringBuilder()对象
(new StringBuilder()).append(namePrefix).append(nameSplitSymbol).append(nameSuffix).toString();

top Created with Sketch.