App性能优化:内存优化
目录:
- 内存优化工具
- 内存管理机制
- 内存抖动解决
- 内存泄漏解决
- MAT详解
- 小结
背景介绍
- 内存是大问题但缺乏关注
- 压死骆驼的最后一根稻草
表现形式
- 内存抖动:锯齿状,GC导致卡顿
- 内存泄漏:可用内存减少、频繁GC
- 内存溢出:OOM、程序异常
1、工具选择
- Memory Profiler
- Memory Analyzer
- LeakCanary
Memory Profiler
- 实时图标展示内存使用量
- 识别内存泄漏、抖动
- 提供捕获堆转储、强制GC以及跟踪内存分配的能力
Memory Profiler的使用
- 非常直观
- 线下平时使用
Memory Analyzer(MAT)
- 强大的Java heap 分析工具,帮助查找内存泄漏和内存占用
- 生成整体报告,分析问题
- 线下深入使用
LeakCanary
- 自动内存泄漏检测
- 线下集成
2、android内存管理机制
Java内存管理机制
Java内存回收算法-标记清除算法
- 标记出所有需要回收的对象
- 统一回收所有被标记的对象
标记回收算法存在的问题
- 标记和清除的效率不高
- 产生大量的内存碎片
Java内存回收算法之 复制算法
- 将内存分成两份,只将数据存储在其中一块上。
- 当需要垃圾回收时,也是标记出废弃的数据,然后将有用数据复制到另一块内存上,最后将第一块内存全部清除
复制算法的问题
- 实现简单,运行高效
- 浪费一半空间,代价大
Java内存回收算法之 标记整理算法
- 标记过程与“标记-清除算法”一样
- 存活对象往一端移动
- 清理其余内存
标记整理算法的特点
- 避免标记-清理导致的内存碎片
- 避免复制算法的空间浪费
Java内存回收算法之 分代收集算法
- 结合多种收集算法优势
- 新生带对象存活率低:复制算法
- 老年代对象存活率高:标记-整理算法
Android内存管理机制
- 内存弹性分配,分配值和最大值受具体设备影响
- OOM场景:内存真正不足,可用内存不足
Dalvik 与Art区别
- Dalvik仅固定一种回收算法
- Art回收算法可运行期选择
- Art具备内存整理能力,减少内存空洞
Low Memory killer
- 进程分类:前端、可见、服务、后台、空进程,优先级依次降低
- 回收收益
内存抖动案例
######制造内存抖动
companion object {
var datas: ArrayList<TestClass>? = null
val handler = object : Handler() {
override fun handleMessage(msg: Message) {
//创造内存抖动
for (i in 0..100) {
datas = ArrayList<TestClass>(100000)
}
sendEmptyMessageDelayed(0, 1000)
}
}
}
内存抖动解决技巧
- 找循环或者频繁调用的地方
- 通过Profiler定位,锯齿状或者频繁的GC
内存泄漏
- 定义:内存中存在没有用的对象,又无法回收
- 表现:内存抖动、可用内存逐渐减少
内存泄漏案例
- 静态变量持有Activity引用导致Activity无法被回收
//定义一个接口
public interface Callback {
void doOperation();
}
//定义持有Activity引用的类
public class CallbackMananger {
public static ArrayList<Callback> sCallbacks = new ArrayList();
public static void addCallback(Callback callback) {
sCallbacks.add(callback);
}
public static void removeCallBack(Callback callback) {
sCallbacks.remove(callback);
}
}
- 在生成LeakCanaryActivity,显示一张图片
class MemoryLeakActivity : AppCompatActivity(), Callback {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_memory_leak)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_leaktest)
iv.setImageBitmap(bitmap)
//保存Activity引用
CallbackMananger.addCallback(this)
}
override fun doOperation() {
}
}
-
重复打开关闭 页面,即可在Profiler中看到内存成阶梯状上升,如下图所示:
显然:这里每次GC的的时候并没有导致MemoryLeakActivity被回收,内存图形呈现阶梯状显示。 -
使用MAT分析内存泄漏
1. 进入官网下载MAT工具:MAT下载地址
2. 下载对应系统版本
-
安装MAT工具,双击zip文件解压后得到mat.app
-
获取堆文件 Heap Dump
-
将文件存到本地
-
注意这里是Android的hprof文件,需要使用android 的platform-tool里面的hprof-conv工具对文件进行一次格式转化,转化命令如下:
./hprof-conv /Users/hudingpeng/Desktop/memory-20200221T114049.hprof 2.hprof
-
这样在platform-tools目录下会生成一个2.hprof文件,如下图所示:
-
打开MAT工具:
- 直接点击mat.app会报错,按照以下步骤运行
- 右键mat.app => 显示包内容 =>进入 mat.app/Contents/MacOS ,此目录下就有工具MemoryAnalyzer =>双击直接打开工具就行了
-
10.点击open a Heap Dump选择我们的转化后的文件2.hprof 文件即可,下面分析MAT工具的一些常用用法
MAT工具分析内存泄漏
-
打开hprot文件,选择Histogram
-
搜索我们的泄漏的MemoryLeakActivity,可以看到有4个MemoryLeakActivity对象,表示已经泄漏了
-
右键选择查看with incoming references
-
选择一个,点击查看到GC Root的路径,看到具体引用的地方
这样我们就看到了具体泄漏的地方就是,CallbackManager类里面的静态变量sCallbacks -
点击Group by package 可以按包名排序
这样也可以很方便看到我们的包名下的类对象生成的情况,这里也可以看到MemoryLeakActivity在内存里有4个对象 -
第三个菜单项:dominator_tree 视图,显示对象占用内存的百分比
可以看到对象占用内存的百分比,给我们的内存优化提供一定的参考 -
第四个菜单项:OQL,相当于一种数据查询语言,我们直接搜索我们的类,点击感叹号查询,如下图所示
-
Top Consumer条目:通过图表的形式显示占用内存最多的对象
这对我们的内存占用优化也是一个不错的参考
关于BitMap的优化
Btimap的内存模型
-
获取Bitmap占用的内存
- getByteCount()方法
- 宽 x 高 x 一像素占用的内存
-
常规方式:
背景:图片对内存大小至关重要、图片宽高大于控件宽高 -
通过Epic ARTHook优雅检测不合理的图片
Epic:Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 4.0 ~ 10.0)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
Epic 被 VirtualXposed 以及 太极 使用,用来实现非 Root 场景下的 Xposed 功能,已经经过了相当广泛的验证。
非常厉害的AOP框架,参考连接:Epic AOP框架
这里我们Hook setImageBitmap方法:
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.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);
}
}
在App onCreate方法里注册要Hook的方法,传入ImageHook
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());
}
});
more:LeakCanary 监控内存泄漏
内存优化总结
优化大方向
- 内存泄漏
- 内存抖动
- Btimap
优化细节
- LargeHeap属性 (建议开启,大概率能申请到更多内存)
- onTrimMemory (系统块干掉app了,可以手动清理界面,跳转到主界面)
- 使用系统优化过的集合:SparseArray
- 谨慎使用SharedPreferences
- 谨慎使用外部库
- 业务架构设计合理