Android | 编写优质程序从内存管理开始
前言
Java 没有指针;Java 自带内存回收机制,不用管理内存。
这大概是很多不负责任的大学老师教给同学们的错误概念。
虽然 Java 虚拟机有一套内存回收机制,但是我们写程序仍然需要注意内存管理,并不是传说中的只申请不释放,内存和性能是程序永恒的话题,Android 开发中卡顿往往都是由于内存的问题。
最近在着手公司项目的内存优化,做个总结,下文全是干货。
Java 对象的生命周期
上学的时候老师讲课为了让同学们容易理解把 Java 内存分为堆(Heap)和栈(Stack),在《深入 Java 虚拟机》书中讲到完整版应该是这样的,在 Java 世界中,内存分为:
- 虚拟机栈(java stacks)
- 堆(heap)
- 方法区(method area)
- PC 寄存器/程序计数器(pc registers)
- 本地方法栈(native method stacks)
这里就不过多介绍每一个内存区域都是做什么用的,详情查看《深入 Java 虚拟机》一书。
出生
死亡
引用计数
根集算法
Java 语言的特性
多态
1 | List<String> strs = new ArrayList<String>(); |
父类引用指向子类对象,使其可以调用子类的方法。其原理是虚表,学过 C++的同学对这个应该很熟悉。
JIT 影响 for 循环的执行效率
JIT - just in time
,即时编译技术。使用该技术,能够加速 java 程序的执行速度。
1 | static class Foo { |
1 | public void zero() { |
1 | public void one() { |
1 | public void two() { |
有上面三个zero()
one()
two()
三个 for 循环的函数,其中:
zero()
最慢,因为 JIT 不能去优化 mArray.length,无论有没有 JIT 都是最慢的;one()
比zero()
快,无论有没有 JIT;two()
在没有 JIT 的情况下最快,而如果有 JIT,其速度几乎和zero()
一样慢。
一般情况下,应该使用two()
这种增强型 for 循环,如果性能敏感的情况下,遍历 ArrayList 这样的线性结构的数据,应该采用one()
。
内部类
非静态内部类天生持有宿主类的一个引用,所以如果内部类泄露了会导致宿主类也跟着遭殃。
Android 平台的特性
onTrimMemory()
当内存不足时,系统会主动触发onTrimMemory()
,这时你应该主动回收掉一些不用的内存。
数据结构选择
Bitmap —> LruCache
HashMap — > ArrayMap / SparseArray
List —> LinkedList / ArrayList
不推荐使用:
Vector
Enum
findView
这个是个比较耗时的操作,所以应该避免一个 View 多次 find。
Android 平台内存调优方法
通过 adb shell 查看内存泄露
1 | adb shell dumpsys meminfo 包名 |
Dump HPROF file
MAT(Eclipse Memory Analysis Tool)
常见案例
单例
这个比较常见,就是声明了单例,如果里面有大内存数据,在程序退出的时候应该主动去回收掉。
内部类 泄露
经典例子为 Handler 作为内部类时发生的泄露。
静态引用
1 | public class ProgressHelper { |
有时候业务需要使用静态应用会得到一定的便利,但是请注意这是很危险的,如果一定要这样做,请使用 WeakReference 包一层。
1 | public class ProgressHelper { |
Context 泄露
一般来说等同于 Activity 泄露
Bitmap 泄露
- Bitmap 对象需要手动调用 recycle
- 切记不要把一张巨大的图,不缩放就放入控件里了。
如果一张图是
2000*2000
,比手机分辨率还大,但是你程序的 ImageView 只有300*300
的大小,你需要先把2000*2000
的图缩小成300*300
再设置给 ImageView。这样一定不会出现 OOM 的情况。
飞线
一个类使用了另一个类的静态资源。
1 | public static boolean isOk = false; |
1 | if (A.isOK) { |
这种情况和静态引用一样,使用 WeakReference 包一层,即可。
内存抖动
内存的不停申请和释放会导致内存抖动,最可能出问题的就是频繁的使用 “” + “” 的方式生产新的字符串。
1 | String newStr = ""; |
因为字符串相加得到的是一个新的字符串,所以如果遇到这种情况,应该使用StringBuilder
1 | StringBuilder sb = new StringBuilder(); |