书籍推荐
市面上android性能优化的书籍不多
因为性能优化这块稍微深入一点,涉及知识的深度和广度就比较大了,而且性能优化依赖很多的平台相关的工具和分析技巧,导致通用性和实效性又不太高,所以以下书籍的内容也比较浅尝辄止
- 总体不错,但是给出的实例场景对于日常的开发工作来说比较不常见到
- 网络优化这章写的感觉最好,初看一遍感觉有不少启示,正在继续啃
- 工具介绍的比较多
- 给出的例子比较常见,但是介绍的比较潦草
- 夹带私货
- jni层的知识和优化讲的比较多
- 除去jni的部分之后的内容比较像一个个tips,内容比较散
内存性能分析及优化的意义
Overview of memory management
内存管理介绍
OOM
- 系统分配给app的堆内存是有上限的,不是系统空闲多少内存app就可以用多少,getMemoryClass()可以获取到这个值
- 可以在manifest文件中设置largeHeap为true,这样会增大堆内存上限,getLargeMemoryClass()可以获取到这个值
- 超出虚拟机堆内存上限会造成OOM
Low Memory Killer
- android内存管理使用了分页(paging)和内存映射(memory-mapping)技术,但是没有使用swap,而是使用Low Memory Killer策略来提升物理内存的利用率 ,导致除了gc和杀死进程回收物理内存之外没有其他方式来利用已经被占用的内存
- 当前台应用切换到后台后,系统并不结束它的进程,而是把它缓存起来,供下次启动。当系统内存不足时,按最近最少使用+优先释放内存使用密集的策略释放缓存进程。
GC
- 内存使用的多也会造成GC速度变慢,造成卡顿
- 内存占用过高,在创建对象时内存不足,很容易造成触发GC影响APP性能
综上
关注并减少应用的内存消耗可以减少oom的概率,在内存紧张的场景下获得更好的用户体验,也可以增加应用的后台存活时间
工具介绍
- GC-LOG
- dumpsys meminfo
- Profiler
- jhat
dumpsys procstats
用来衡量一段时间内应用消耗内存的情况
- PSS:Proportional Set Size按比例分配共享内存的实际内存
- USS:Unique Set Size进程私有内存
PSS=USS+共享内存/共享内存的进程数
(最小PSS-平均PSS-最大PSS/最小USS-平均USS-最大USS)
LeakCanary
检测内存泄漏的工具
MAT
比较常用的内存dump文件分析工具
使用方法
- 使用Memory Profiler Dump内存数据
- 导出的hprof文件不是MAT的标准文件,需要使用sdk带的hprof-conv转换
hprof-conv -z src dst //-z可以排除android框架创建的对象
使用场景
- 总体性找出内存优化的瓶颈
- 只有dump文件的现实场景,或者无法定位具体问题等只有现场而没有线索的情况下庖丁解牛的工具
- 对于专项功能的内存优化感觉不如代码调试+profiler
分析场景构建
性能测试的一些注意点
- 需要考虑尽量真实的场景
- 需要关闭log等调试组件避免干扰
常见的性能测试方式
- 切换到后台
- 反复执行功能
- 长时间执行功能
- 多个场景来回切换
容易出现内存问题的场景
- 包含了图片显示的界面
- 网络传输大量数据的场景
- 需要缓存数据的场景
常见的内存问题
内存泄漏
内存泄漏产生的原因
一个对象的生命周期已经结束了,但是有其他对象持有了它的实例导致无法在GC时被回收,在Android中通常是Activity在finish之后依然有对象引用它导致内存泄漏
内存泄漏的常见场景
- 异步操作中异步逻辑未结束,而Activity结束或者重建了
- Thread/Handler/AsyncTask/Rxjava/Timer等
- 使用静态变量或者单例直接或者间接的保存Activty实例但是未及时释放
- 注册广播未注销
- ObjectAnimator未调用cancel
- I/O操作等完成后未及时关闭或者释放
- WebView造成的内存泄漏 Android 5.1 WebView内存泄漏分析
内存泄漏在分析工具上的表现
每次activity的重建都会造成内存上升且gc不会使内存使用降低
内存泄漏的避免
- LeakCanary
- StrictMode
- 没有必要使用Activity作为Context的地方全部使用ApplicationContext
- 使用WeakReference
- 使用ViewModel+LiveData/RxJava+Rxlifecycle等工具实现异步逻辑避免内存泄漏
- 对需要销毁时进行处理的操作进行检查,如xxx.cancel()/xxx.close()/xxx.unregister()/xxx.remove()等操作
内存抖动
内存抖动的原因
内存抖动一般是瞬间创建了大量对象,会在短时间内触发多次GC,产生卡顿
内存抖动的场景
- IM通知需要转发到所有WebView界面,当刚打开APP时多个通知同时到达,或者在群聊中消息很多的场景下,会造成短时间内频繁GC,同时伴随界面卡顿
内存抖动的在分析工具上的表现
制造了一个内存抖动的场景
public void testThrashing(boolean needLog) {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//优化以前
for (int i = 0; i < lotsOfInts.length; i++) {
String rowAsStr = "";
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
rowAsStr += sorted[j];
if (j < (lotsOfInts[i].length - 1)) {
rowAsStr += ", ";
}
}
if (needLog) {
Log.i(TAG, "Row " + i + ": " + rowAsStr);
}
}
}
public void optimizeThrashing() {
int dimension = 300;
int[][] lotsOfInts = new int[dimension][dimension];
Random randomGenerator = new Random();
for (int i = 0; i < lotsOfInts.length; i++) {
for (int j = 0; j < lotsOfInts[i].length; j++) {
lotsOfInts[i][j] = randomGenerator.nextInt();
}
}
//优化以后
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lotsOfInts.length; i++) {
sb.delete(0, sb.length());
int[] sorted = getSorted(lotsOfInts[i]);
for (int j = 0; j < lotsOfInts[i].length; j++) {
sb.append(sorted[j]);
if (j < (lotsOfInts[i].length - 1)) {
sb.append(", ");
}
}
Log.e(TAG, "Row " + i + ": " + sb);
}
}
- 自己试验的感受,gc带来的卡顿其实并不明显(也可能是demo不太复杂,GC耗时不长)
- 个人感觉卡顿主要是因为内存抖动大多出现在一些复杂场景,通常伴随着主线程的大量操作已经出现了卡顿,而内存抖动引起的频繁GC会加剧卡顿的程度
解决方案
- 最简单的做法就是把之前的主线程操作放到子线程去,虽然内存抖动依然存在,但是卡顿问题可以大大缓解
- 对于内存抖动本身
- 尽量避免在循环体内创建对象,应该把对象创建移到循环体外
- 需要大量使用Bitmap和其他大型对象时,尽量尝试复用之前创建的对象
- 对于黑盒子(例如之前例子中im通知造成的webview的内存抖动和主线程耗时操作)
- 控制触发频率,减轻卡顿程度
- 添加注册机制,需要接收通知的页面才发送通知
图片加载的内存占用
不同dpi文件夹对图片内存的影响
不同dpi限定符对应的dpi
xxxhdpi-640
xxhdpi-480
xhdpi-320
mdpi-160通过resId加载的Bitmap的宽高计算
bitmap宽高=图片实际宽高*屏幕dpi/文件夹对应的dpinodpi
从这个文件夹中加载的图片资源生成的Bitmap会保持图片本身的尺寸1920*1080图片资源放在不同的文件夹下加载的Bitmap大小计算
使用设备小米note,设备dpi为440
文件夹 | 对应dpi | bitmap width | height | size | 倍数 |
---|---|---|---|---|---|
nodpi | 1920 | 1080 | 8294400 | 1 | |
xxxhdpi | 640 | 1320 | 743 | 3923040 | 0.47 |
xxhdpi | 480 | 1760 | 990 | 6969600 | 0.84 |
xhdpi | 320 | 2640 | 1485 | 15681600 | 1.89 |
mdpi | 160 | 5280 | 2970 | 62726400 | 7.56 |
- 使用图片的建议
- 尽量使用1080p的尺寸下的切图
- 图片尽量放xxhdpi以上的文件夹下
- 大图如Splash页和引导页的图片放在nodpi文件夹下,通过控制ImageView大小来限制图片大小
- 按照上面操作会导致apk大小增加,可以将图片转成webp并进行压缩
RGB565
除了图片资源的文件夹,加载图片时使用的色彩模式也影响了Bitmap大小。ARGB8888使用了32bit,所以一个像素需要4byte;RGB565使用了16bit,一个像素只需要2byte
但是因为RGB565少了alpha通道,对有透明度的图片显示有问题,而且显示效果上还是有些区别,所以并不建议修改这个属性,只是在对内存有严格要求的场景下可以作为特殊手段进行优化
ProGuard对内存的影响
- ProGuard可以对类、方法和变量重命名,剔除无用代码和资源,减小dex大小,除了减小了apk的大小,同时也减小了加载dex所需的内存
- 因为虚拟机加载dex文件是按需加载的,而内存分配的最小单位是页,所以加载一个功能的代码时同一个内存页中也会加载dex文件中该功能前后不相关的代码,ProGuard可以重新排序类的字节码在dex文件中位置,使得有相互调用关系的类在dex中更加紧凑,加载相同功能所需的内存更小
内存碎片
Davik的内存回收算法不能移动对象,所以会造成一个小对象占据整个内存页,产生内存碎片
而ART虚拟机的可以在GC时对内存空间进行整理,随着5.0以上系统的占有率逐渐提升,内存碎片造成的内存消耗可以不必过于关心
其他内存问题
- 页面不可见收到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)时释放UI资源
- 通过getMemoryInfo()获取内存信息,保证自己不开辟大内存导致oom
- 谨慎的使用Service
- 使用IntentService
- 使用JobScheduler进行后台调度
- 使用优化的容器如SparseArray等
- 代码抽象会带来额外的内存消耗
- 使用@IntDef、@StringDef代替枚举
...
to be continue...
- 流畅度
- 耗电
- 网络
- apk瘦身
- Matrix