1、内存问题
- 内存抖动:锯齿状,GC导致卡顿
- 内存泄漏:可用内存减少,频繁GC
- 内存溢出:OOM、程序异常
2、工具选择
- Memory Profiler
实时图表展示应用内存使用量,识别内存泄露,抖动等,提供捕获堆转储、强制GC以及跟踪内存分配的能力 - Memory Analyzer
强大的Java Heap分析工具,查找内存泄漏及内存占用,,生成整体报告,分析问题等,线下深入使用 - LeakCanary
自动内存泄漏检测,只能线下使用
3、内存管理机制
Java 内存分配
方法区:类信息,常量,静态变量等,所有线程共享
虚拟机栈:局部变量表,操作数栈等,分配的引用指向堆中真正的对象
本地方法栈:native方法
堆:所有线程共享,对象分配的内存都在堆上,GC主要作用与此区域,内存泄漏也是发生在堆上
程序计数器:线程执行到了第几行
内存回收算法
- 标记清除算法:标记出所有需要回收的对象,统一回收所有被标记的对象,效率不高,会产生大量内存碎片
- 复制算法:将内存划分为大小相等的两块,一块内存用完后复制存活对象到另一块,清理另一块内存。实现简单,运行高效,浪费一半空间,代价大
- 标记整理算法:标记过程与“标记清除算法”一样,存活对象往一端移动,清理剩余内存。避免内存碎片,避免空间浪费
- 分代收集算法:结合多种算法优势,新生代对象存活率低,复制,老年代对象存活率高,标记整理。
Android内存管理机制 - 内存弹性分配,分配值与最大值受具体设备影响
- OOM场景:内存真正不足,可用内存不足
- Dalvak和Art区别:Dalvak仅固定一种回收算法,Art回收算法可运行期选择(比如应用在前台,响应速度最重要,应该选择简单的算法,标记清除。当应用切换到后台,可以用标记整理算法)Art具备内存整理能力,减少内存空洞
- Low Memory Killer:当手机内存不足时候,会对所有进程回收,根据进程分类优先级和回收收益
4、内存抖动实战
内存频繁分配和回收导致内存不稳定,表现:频繁GC,内存曲线呈锯齿状,危害:导致卡顿,OOM(频繁创建对象,导致内存不足及碎片,不连续的内存碎片无法被分配,导致OOM)
写一个模拟内存抖动的activity
/**
* 模拟内存抖动的界面
*/
public class MemoryShakeActivity extends AppCompatActivity implements View.OnClickListener {
@SuppressLint("HandlerLeak")
private static Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 创造内存抖动
for (int index = 0; index <= 100; index++){
String arg[] = new String[100000];
}
mHandler.sendEmptyMessageDelayed(0,30);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory);
findViewById(R.id.bt_memory).setOnClickListener(this);
}
@Override
public void onClick(View v) {
mHandler.sendEmptyMessage(0);
}
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
}
执行之后发现内存分配非常频繁
可以通过record功能记录内存分配情况可以发现是string在大量分配内存,并且可以查看到代码的位置
内存抖动解决技巧:找循环或者频繁调用的地方
5、内存泄漏实战
内存中存在已经没有用的对象,表现:内存抖动,可用内存逐渐变少,危害:内存不足,GC频繁,OOM
确认问题要使用Memory Analyzer,先通过hprof-conv工具转换格式
下载地址https://www.eclipse.org/mat/downloads.php
CallBackManager 来持有activity的引用
public class CallBackManager {
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);
}
}
/**
* 模拟内存泄露的Activity
*/
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.iv_memoryleak);
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
CallBackManager.addCallBack(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
// CallBackManager.removeCallBack(this);
}
@Override
public void dpOperate() {
// do sth
}
}
由于callback是静态的,和应用的生命周期一样,没有释放activity的引用,造成了内存泄漏
使用堆转储功能,将文件保存下来使用在platform-tools中的命令转换一下文件
hprof-conv .\memory-20190814T190845.hprof memory_leak_1.hprof
然后使用工具打开文件
打开Histogram,筛选出MemoryLeakActivity,选择一个Class,右键选择List objects > with incoming references,选择出哪些强引指向了我
在新页面会显示通过这个class创建的对象信息,选择一个对象,右键选择Path to GC Roots > ****,通常在排查内存泄漏的时候,我们会选择exclude all phantom/weak/soft etc.references,意思是查看排除虚引用/弱引用/软引用等的引用链,因为被虚引用/弱引用/软引用的对象可以直接被GC给回收,我们要看的就是某个对象否还存在Strong 引用链(在导出HeapDump之前要手动出发GC来保证),如果有,则说明存在内存泄漏,然后再去排查具体引用。
找到了内存泄漏的位置
6、ARTHook检测不合理图片
Bitmap内存模型
- API10之前Bitmap在Dalvik Head堆中,像素在Native中,好处是不占用应用分配的内存,缺点是Java中的Bitmap被回收了但是无法控制Native内存
- API10之后像素也在Dalvik Head堆中
- API26后像素在Native中,加入机制,Java中的Bitmap被回收后通知Native回收
获取Bitmap占用内存
- getByteCount 在运行时计算出
- 宽x高x 一像素占用内存
常规方式
图片对内存优化很重要,图片宽高大于控件宽高,继承ImageView,覆写实现计算大小
ARTHook介绍
挂钩,将额外代码钩住原有方法,执行修改逻辑
Epic使用
Epic 是一个在虚拟机层面、以 Java Method 为粒度的 运行时 AOP Hook 框架。简单来说,Epic 就是 ART 上的 Dexposed(支持 Android 5.0 ~ 11)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析、安全审计等。
build.gradle 中添加如下依赖
compile 'com.github.tiann:epic:0.11.2'
继承XC_MethodHook实现逻辑,注入hook
DexposedBridge.findAndHookMethod
新建ImageHook 类
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) {
//图片宽高大于控件宽高2倍以上
if (bitmap.getWidth() >= (width << 1) && bitmap.getHeight() >= (height << 1)) {
Log.d("ImageHookb", bitmap.getWidth() + " " + bitmap.getHeight() + " " + width + " " + height);
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(() -> {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1) && bitmap.getHeight() >= (h << 1)) {
Log.d("ImageHookb", bitmap.getWidth() + " " + bitmap.getHeight() + " " + w+ " " + h);
}
//view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
});
}
}
}
}
}
}
在application的onCreate中
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());
}
});
在MemoryLeakActivity中
ImageView imageView = findViewById(R.id.iv_memoryleak);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
imageView.setImageBitmap(bitmap);
//imageViewe布局的宽高很小
运行之后会打印日志
7、线上内存监控方案
1、设定场景线上Dump:Debug.dumpHprofData()
。 超过内存80%,dump内存,上传文件,MAT工具分析
问题:dump文件太大,和对象数相关,可剪裁。上传失败率高,分析问题困难
2、LeakCanary带到线上,预设泄漏怀疑点,发现泄漏回传
问题:不适合所有情况,分析较慢,可能会OOM
优化大方向
1、内存泄漏,2、内存抖动 ,3、bitmap
优化细节
1、LargeHeap属性 2、onTrimMemory 3、优化的集合,SparseArray 4、谨慎使用SharedPreferences 5、谨慎使用外部库 6、业务架构合理