Part 3 App内存优化
一 内存优化介绍及工具选择
1、内存优化介绍
内存问题
内存抖动:锯齿状、GC导致的卡顿
内存泄漏:可用内存减少、频繁GC
内存溢出:OOM、程序异常
2、工具选择
Memory Profiler
实时图表展示应用内存使用情况
识别内存泄漏、抖动等
提供捕获堆转储、强制GC以及跟踪内存分配的能力
总结:方便直观、线下平时使用
Memory Analyzer(MAT)
强大的java heap分析工具、查找内存泄漏及内存占用
生成整体报告、分析问题等
线下深入使用
LeakCanary
自动内存泄漏检测
线下集成
二 Android内存管理机制
1、java内存管理机制
java的内存分配:
方法区(存放java的类信息,常量,静态变量 等,是所有线程共享的)
虚拟机栈(局部变量表和操作数栈等,为java方法调用服务的,线程私有)
本地方法栈(为native方法服务的,线程私有)
堆(所有线程共享,真正的进行内存分配,GC主要作用的区域)
程序计数器(存储当前线程到多少行)
java的内存回收算法:
标记-清除算法总结:标记清除算法:标记处所有需要回收的对象,同一回收所有被标记的对象。效率不高,产生大量不连续的碎片(内存空洞)
复制算法总结:复制算法:将内存划分为大小相等的两块,一块内存用完之后复制存活的对象到另一块,清理另一块内存。实现简单,运行高效,浪费一半内存,代价大
标记-清理总结:标记整理算法:标记过程与标记清除算法一样,存活对象往一端进行移动,清理其余内存。避免标记清除导致的内存碎片,避免复制算法的控件浪费。
分代收集算法:
集合多种收集算法的优势,新生代对象存活率低(复制算法),老年代对象存活率高(标记整理)
Android的内存管理机制:内存弹性分配,分配值和最大值受具体设备影响。oom场景:内存真正不足,可用内存不足。
Dalvik与Art区别:Dalvik仅固定一种回收算法,Art回收算法可运行期间选择,Art具备内存整理能力,减少内存空洞。
Low Memory Killer 进行分类,回收收益。
2、Android内存管理机制
内存弹性分配。分配值和最大值受具体设备影响(如高端机和低端机的不同)
OOM场景:内存真正不足、可用内存不足(整个系统内存都不足)
Dalvik与Art区别:
Dalvik仅固定一种回收算法(运行期没法改变)
Art回收算法可运行期选择
Art具备内存整理能力,减少内存空洞
Low Memory killer
进程分类(前台-可见进程-桌面进程-服务进程-后台进程-空进程)
回收收益
三 内存抖动解决实战
1、内存抖动介绍
定义:内存频繁分配和回收导致内存不稳定
表现:频繁GC、内存曲线呈现锯齿状
危害:导致卡顿、OOM
内存抖动导致OOM的原因:频繁创建对象,导致内存不足及碎片(不连续),不连续的内存片无法被分配,导致OOM
2、内存抖动怎么解决
使用Memory Profiler(可以观察内存曲线)
内存抖动解决技巧:找循环或者频繁调用的地方
四 内存泄露解决实战
1、内存泄漏介绍
定义:内存中存在已经没有用的对象
表现:内存抖动、可用内存逐渐减少
危害:内存不足、频繁GC、OOM
2、内存泄漏怎么解决
使用Memory Analyzer
下载地址:https://www.eclipse.org/mat/downloads.php
转换:hprof-conv 原文件路径 转换后文件路径
使用MAT工具进行分析哪些对象存在泄漏(下一章介绍)
五 全面理解MAT
1、MAT的介绍
参考文档
https://www.jianshu.com/p/c8e0f8748ac0 (MAT使用进阶)
https://www.cnblogs.com/larack/p/6071209.html (内存泄漏 之 MAT工具的使用
)
六 ARTHook优雅检测不合理图片
1、Bitmap内存模型
API10之前Bitmap自身在Dalvik Heap中,像素在Native
优点:不占用java层的内存,不易出现OOM
缺点:bitmap被回收,native像素没有被回收,回收时机不确定
API10之后像素也放在Dalvik Heap中
优点:bitmap回收native像素也被及时回收
缺点:占用java层内存
API26之后像素在Native
优点:不占用java层的内存,不易出现OOM
Google解决了不能及时回收的问题
获取bitmap占用内存
getByteCount 运行时动态计算出来
宽高一像素占用内存 直接计算结果
2、常规方式
背景
图片对内存优化至关重要、图片宽高大于控件宽高(证明图片尺寸不合理)
实现
继承ImageView,复写实现计算大小
总结:侵入性强,不通用
3、ART方式
ARTHooK介绍
挂钩,将额外的代码挂住原有的方法,修改执行逻辑
运行时插桩
性能分析
Epic介绍
Epic是一个虚拟机层面、以java Method为粒度的运行时Hook框架
支持Android 4.0-9.0机型
https://github.com/tiann/epic
使用方式:
dependencies {
compile ‘me.weishu:epic:0.3.6’
}
继承XC_MethodHook,实现相应的逻辑
注入Hook:DexposedBridge.findAndHookMethod
几个例子
1.监控Java线程的创建和销毁:
class ThreadMethodHook extends XC_MethodHook{
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", started..");
}
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread t = (Thread) param.thisObject;
Log.i(TAG, "thread:" + t + ", exit..");
}
}
DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
Thread thread = (Thread) param.thisObject;
Class<?> clazz = thread.getClass();
if (clazz != Thread.class) {
Log.d(TAG, "found class extend Thread:" + clazz);
DexposedBridge.findAndHookMethod(clazz, "run", new ThreadMethodHook());
}
Log.d(TAG, "Thread: " + thread.getName() + " class:" + thread.getClass() + " is created.");
}
});
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
以上代码拦截了 Thread 类以及 Thread 类所有子类的 run方法,在 run 方法开始执行和退出的时候进行拦截,就可以知道进程内部所有Java线程创建和销毁的时机;更进一步,你可以结合Systrace等工具,来生成整个过程的执行流程图,比如:
2.监控dex文件的加载:
DexposedBridge.findAndHookMethod(DexFile.class, "loadDex", String.class, String.class, int.class, new XC_MethodHook() {
@Override
protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
super.beforeHookedMethod(param);
String dex = (String) param.args[0];
String odex = (String) param.args[1];
Log.i(TAG, "load dex, input:" + dex + ", output:" + odex);
}
});
3.完整版的图片尺寸监控
3.1 首先实现XC_MethodHook,并实现我们的逻辑
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 实现我们的逻辑
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}
private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 图标宽高都大于view带下的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();
LogUtils.i(warnInfo);
}
}
3.2 在Application中或者Activity中初始化
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});
3.3 log 信息
03-04 12:08:54.035 21103-21103/com.kaolafm.kradio.k_radio_horizontal E/zsj: Bitmap size too large:
real size: (1280,1424)
desired size: (409,469)
call stack trace:
java.lang.RuntimeException: Bitmap size too large
at com.kaolafm.kradio.lib.hook.ImageHook.checkBitmap(ImageHook.java:34)
at com.kaolafm.kradio.lib.hook.ImageHook.afterHookedMethod(ImageHook.java:20)
at com.taobao.android.dexposed.DexposedBridge.handleHookedArtMethod(DexposedBridge.java:273)
at me.weishu.epic.art.entry.Entry.onHookVoid(Entry.java:73)
at me.weishu.epic.art.entry.Entry.referenceBridge(Entry.java:167)
at me.weishu.epic.art.entry.Entry.voidBridge(Entry.java:87)
at android.support.v7.widget.AppCompatImageView.setImageBitmap(AppCompatImageView.java:107)
at com.kaolafm.kradio.k_kaolafm.horizontal.RadioPlayerFragment.lambda$showCover$4$RadioPlayerFragment(RadioPlayerFragment.java:349)
at com.kaolafm.kradio.k_kaolafm.horizontal.RadioPlayerFragment$$Lambda$3.onBitmap(Unknown Source)
at com.kaolafm.kradio.lib.utils.imageloader.GlideImageLoaderStrategy$2.onResourceReady(GlideImageLoaderStrategy.java:153)
at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:579)
at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:549)
at com.bumptech.glide.load.engine.EngineJob.handleResultOnMainThread(EngineJob.java:218)
at com.bumptech.glide.load.engine.EngineJob$MainThreadCallback.handleMessage(EngineJob.java:324)
at android.os.Handler.dispatchMessage(Handler.java:98)
at android.os.Looper.loop(Looper.java:157)
at android.app.ActivityThread.main(ActivityThread.java:5653)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:746)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:636)
ARTHook实现
无侵入性,通用性强,兼容问题大,开源方案不能带到线上环境
七 线上内存监控方案
1、常规实现一
设定场景线上Dump:Debug.dumpHprofData(“fileName”);
如超过最大内存的80% -> 内存dump -> 回传文件(文件比较大) -> MAT手动分析
总结:Dump文件太大,和对象数正相关,可裁剪。上传失败率高,分析困难。配合一定的策略,有一些效果。
2、常规实现二
LeakCanary带到线上
预设泄漏怀疑点
发现泄漏回传
总结:不适合所有场景,必须预设怀疑点。分析比较耗时,也容易OOM。
2、LeakCanary原理
监控生命周期,onDestroy添加RefWatcher检测
二次确认断定发生内存泄漏
分析泄漏,找引用链
监控组件+分析组件
3、LeakCanary定制
预设怀疑点->自动找怀疑点(谁占用内存大)
分析泄漏链路慢->只分析Retain size大的对象
分析OOM(内存堆栈生成所有文件全部映射到内存中,占用内存)->对象裁剪,不全部加载到内存
4、线上内存监控完整方案
待机内存、重点模块内存、OOM率
整体及重点模块GC次数、GC时间
增强LeakCanary自动化内存泄漏分析
八 内存优化技巧总结
1、优化大方向
内存泄漏、内存抖动、Bitmap
2、优化细节
LargeHeap属性(提升分配内存的上限,但是更容易提升被杀的概率,然而大家都开)
onTrimMemory(低内存状态,在最严重的状态下情况图片和界面跳转到主界面,影响用户体验但是比被系统干掉要好)
使用优化过的集合:SparseArray
谨慎的使用sharepreference(第一次加载将所有数据load到内存中)
谨慎使用外部库(使用的要是经过验证的,比较成熟的库)
业务架构设计合理(如:城市数据结构,一次加载很多,可以分级为省市县等)