3.内存优化
1 2 3 4
| //内存优化:内存优化主要体现在两个地方,第一个是内存抖动,第二个是内存泄漏 1. 内存抖动: 主要是由于快速的内存创建和释放造成的,容易产生GC(Profiler); 2. 内存泄漏:主要是由于长生命周期的引用了短生命周期的对象造成的(leakcary); 3. 检测方式:AS的Profiler(Memory Profiler/Memory Analyzer)主动检测,leakcary被动的自动检测;
|
一、背景介绍
1 2
| 1.内存是大问题但缺乏关注 2.压死骆驼的最后一根稻草
|
内存问题
1 2 3
| 1.内存抖动:锯齿状、GC导致卡顿 2.内存泄露:可用内存减少、频繁GC 3.内存溢出:OOM、程序异常
|
工具选择
1 2 3
| 1.Memory Profiler 2.Memory Analyzer 3.LeakCanary
|
Memory Profiler(点击Studio底部Android Profiler界面中MEMORY区域的任意位置,即可开启Memory Profiler)
1 2 3 4 5 6
| 优点: 1.实时图表展示应用内存使用量 2.识别内存泄露、抖动等 3.提供捕获堆转储、强制GC以及跟踪内存分配的能力 4.方便直观 5.线下平时使用
|
![img](/../../../images/SouthEast.bmp)
1 2 3 4 5 6 7 8 9
| 1对应的按键用于强制内存回收。 2对应的按键用于抓取进程内存的dump信息。 3对应的按键用于记录内存的分配信息(连接Android 7.1及以下才会有此按键)。 初次点击时,对应统计的开始时间点;再次点击时,对应统计的结束时间点。 进程在两个时间点之间的内存分配信息,将被Memory Profiler记录和分析。 4对应的区域用于缩放时间轴。 5对应的按键用于显示实时的内存数据。 6对应的区域用于记录事件发生的时间点及大致持续的时间(例如activity状态改变、用户操作界面等事件)。 7对应的区域用于显示内存使用情况对应的时间轴(与标注6结合,就可以看出各事件带来的内存变化情况)。
|
![img](/../../../images/SouthEast-169502289652712.bmp)
1 2 3 4 5 6 7 8 9
| 如上图所示,其中: Java表示Java代码或Kotlin代码分配的内存; Native表示C或C++代码分配的内存(即使App没有native层,调用framework代码时,也有可能触发分配native内存); Graphics表示图像相关缓存队列占用的内存; Stack表示native和java占用的栈内存; Code表示代码、资源文件、库文件等占用的内存; Others表示无法明确分类的内存; Allocated表示Java或Kotlin分配对象的数量(Android8.0以下时,仅统计Memory Profiler启动后,进程再分配的对象数量; 8.0以上时,由于系统内置了统计工具,Memory Profiler可以得到整个app启动后分配对象的数量)。
|
点击左上角的标记2按钮,则出现下图可以查看各个对象的对应信息,如bitmap的大小和预览图![image-20230918154331037](/../../../images/image-20230918154331037.png)
Memory Analyzer
1 2 3
| 强大的Java Heap分析工具,查找内存泄露及内存占用 生成整体报告、分析问题等 线下深入使用
|
LeakCanary
1 2 3
| 1.自动内存泄露检测 文档:https://github.com/square/leakcanary 2.线下集成
|
二、Android内存管理机制
Java内存区域划分
1 2 3 4 5 6 7 8
| 1.方法区(Method Area):用于存储类的信息、常量、静态变量等,方法区在程序启动时就被创建,所有线程共享。 其中,常量池用于存放字符串常量、类和接口常量等. 2.堆(Heap):用于存储对象实例。堆在程序运行时动态分配和回收,由垃圾回收器负责管理。 堆可以分为新生代和老年代两部分新生代又分为Eden空间、Survivor 0空间和Survivor 1空间. 虚拟机栈(VM Stack):每个线程在运行时都会创建一个栈,用于存储方法的局部变量、操作数栈、动态连接等。 栈顺随着方法3的进入和退出而动态创建和销毁。 4.本地方法栈(Native Method Stack): 与虚拟机栈类似,但用于执行本地方法(即使用其他语言编写的方法) 5.程序计数器(Program Counter) : 用于记录当前线程执行的字节码指令地址。
|
java内存回收算法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 1. 标记-清除算法(Mark and Sweep): 首先标记所有活动对象,然后清除所有未标记的对象。这种算法存在的问题是标记和清除效率不高并会产生大量的碎片, 导致内存空间的浪费。 2. 复制算法(Copying): 2.1将内存划分为大小相等的两块 2.2一块内存用完之后复制存活对象到另一块 2.3清理另一块内存 2.4实现简单,运行高效(相对标记算法而言) 2.5浪费一半空间,代价大 将内存分为两个区域,每次只使用其中一个区域,当该区域满了之后,将所有活动对象复制到另一个区域中, 然后清除原来的区域。这种算法的优点是简单高效,但是会浪费一半的内存空间。 3. 标记-整理算法(Mark and Compact): 1.标记过程与“标记-清除”算法一样 2.存活对象往一端进行移动 3.清理其余内存 4.避免标记-清理导致的内存碎片 5.避免复制算法的空间浪费 首先标记所有活动对象,然后将它们移动到内存的一端,然后清除另一端的所有未标记对象。 这种算法可以避免碎片问题,但是需要移动对象,可能会影响程序的性能。
4. 分代收集算法(Generational): 1.结合多种收集算法优势 2.新生代对象存活率低,复制 3.老年代对象存活率高,标记-整理 将内存分为几个代,每个代的对象生命周期不同。新创建的对象放在第一代,经过多次回收后仍然存活的对象会被移到下一代, 最终存活的对象会被移到老年代。这种算法可以根据对象的生命周期来优化回收效率。
5. 引用计数算法(Reference Counting): 对每个对象维护一个引用计数器,当有新的引用指向该对象时,计数器加一,当引用失效时,计数器减一。 当计数器为零时,说明该对象已经不再被引用,可以回收。这种算法的问题是无法处理循环引用的情况
|
Android内存管理机制
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 1.内存弹性分配,分配值与最大值受具体设备影响 2.OOM场景:内存真正不足、可用内存不足 3.Dalvik与Art区别 1.Dalvik仅固定一种回收算法 2.Art回收算法可运行期选择(5.0之后都是这种算法) 3.Art具备内存整理能力,减少内存空洞 4.Low Memory Killer 1.进程分类(根据进程下面优先级,先考虑优先级低的进程进行回收) 1.前台进程 2.可见进程 3.服务进程 4.后台进程 5.空进程 2.回收收益
|
三、内存抖动实战
内存抖动介绍
1 2 3
| 定义:内存频繁分配和回收导致内存不稳定 表现:频繁GC、内存曲线呈锯齿状 危害:导致卡顿、OOM
|
内存抖动导致OOM
1 2
| 1.频繁创建对象,导致内存不足及碎片(不连续) 2.不连续的内存片无法被分配,导致OOM
|
内存抖动解决实战:使用Memory Profiler初步排查
创建一个模拟内存抖动的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| package com.optimize.performance.memory;
import android.annotation.SuppressLint; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.view.View;
import com.optimize.performance.R;
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); } }
|
点击执行后发送内存抖动时Memory Profiler的现象:![image-20230918171625886](/../../../images/image-20230918171625886.png)
查找具体引起抖动的方式:点击record开始记录,再点击stop停止记录(如下图);
![image-20230918172026002](/../../../images/image-20230918172026002.png)
左侧可以查看到不同对象的size大小,选中后右侧上面随意选择一个对象底部则会显示对象的调用信息,点击右键出现Jump to Source跳转到方法的调用处;
内存抖动解决技巧:找循环或者频繁调用的地方
四、内存泄漏实战
内存泄露介绍
1 2 3
| 定义:内存中存在已经没有用的对象 表现:内存抖动、可用内存逐渐变少 危害:内存不足、GC频繁、OOM
|
Memory Analyzer
1 2
| 文档:https: 转换: hprof-conv 原文件路径 转后文件路径
|
模拟内存泄漏代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| package com.optimize.performance.memory; import java.util.ArrayList; 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); } }
package com.optimize.performance.memory; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.widget.ImageView; import com.optimize.performance.R;
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_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() { } }
|
通过多次进入再退出上面的MemoryLeakActivity类则会出现下图这种内存泄漏情况:![image-20230919091415496](/../../../images/image-20230919091415496.png)
通过record左边的down按钮并保存文件,再通过命令转换文件格式:
![image-20230919091622418](/../../../images/image-20230919091622418.png)
在使用Eclipse Memory Analyzer软件打开转换后的文件
![image-20230919091803750](/../../../images/image-20230919091803750.png)
点击 Histogram 按钮
![image-20230919091925652](/../../../images/image-20230919091925652.png)
在上图位置可以搜索怀疑存在内存泄漏的类,并查看显示的该类剩余的对象数量;
选中类的对象右键点击List Objects->with incoming references(哪些强引用引向了我)->选中类对象->Path to GC->with all references(所有对象都计算在内)分析后如下图,有个小圆点的则是目标内存泄漏的类
![image-20230919092826271](/../../../images/image-20230919092826271.png)
总结
1 2
| 1.使用Memory Profiler初步观察 2.通过Memory Analyzer结合代码确认
|
五、MAT的使用
1 2 3
| MAT工具全称为Memory Analyzer Tool,一款详细分析Java堆内存的工具,该工具非常强大
代码太多,可百度查看使用教程:如https://www.jianshu.com/p/034b72b91d3a/
|
六、优雅的检测不合理的图片
Bitmap内存模型
1 2 3 4 5 6
| 1.API10之前Bitmap自身在Dalvik Heap中,像素在Native 2.API10之后像素也被放在Dalvik Heap中 3.API26之后像素在Native 4.获取Bitmap占用内存 - getByteCount - 宽*高*一像素占用内存
|
常规方式
1 2 3
| 当景:图片对内存优化至关重要、图片宽高大于控件宽高 实现:继承ImageView,覆写实现计算大小 缺点:侵入性强,不通用
|
ARTHook介绍
1 2 3 4 5 6 7 8
| 挂钩,将额外的代码钩住原有方法,修改执行逻辑 - 运行时插桩 - 性能分析 优点: - 无侵入性 - 通用性强 缺点: - 兼容问题大,开源方案不能带的线上环境
|
Epic简介
1 2 3 4 5 6 7
| Epic是一个虚拟机层面、以Java Method为粒度的运行时Hook框架 - 支持Android4.0-9.0 - https://github.com/tiann/epic Epic使用 - compile 'me.weishu:epic:0.3.6 - 继承XC_MethodHook,实现相应逻辑 - 注入Hook : DexposedBridge.findAndHookMethod
|
实战自定义Epic
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
|
package com.optimize.performance.memory; 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) { 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); } }
|
1 2 3 4 5 6 7 8
| //调用执行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()); } });
|
再在MemoryLeakActivity的activity_memoryleak的xml中将imageview宽高修改为50
1 2 3 4 5 6 7 8 9 10 11
| <?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">
<ImageView android:id="@+id/iv_memoryleak" android:layout_width="50dp" android:layout_height="50dp" />
</android.support.constraint.ConstraintLayout>
|
启动下面MemoryLeakActivity类,然后会执行检测,运行后弹出警告
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| package com.optimize.performance.memory; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.widget.ImageView; import com.optimize.performance.R; /** * 模拟内存泄露的Activity */ public class MemoryLeakActivity extends AppCompatActivity implements CallBack{ @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_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 } }
|
七、线上内存监控方案
常规实现一
1
| 设定场景线上Dump:Debug.dumpHprofData()
|
常规实现一流程
1
| 超过最大内存80%(只在特定情况下执行,如占用可用内存超过80%)->内存Dump->回传文件(wifi或无流量空闲时上传)->MAT手动分析
|
实现一问题:
1 2 3
| 1.Dump文件太大,和对象数正相关,可裁剪 2.上传失败率高、分析困难 3.配合一定策略,有一定效果
|
常规实现二
1 2 3
| 1.LeakCanary带到线上 2.预设泄露怀疑点 3.发现泄露回传
|
实现二问题:
1 2
| 1.不适合所有情况,必须预设怀疑点 2.分析比较耗时、也容易OOM
|
LeakCanary原理
1 2 3 4
| 1.监控生命周期,onDestroy添加RefWatcher检测 2.二次确认断定发生内存泄露 3.分析泄露,找引用链 4.监控组件+分析组件
|
LeakCanary定制
1 2 3
| 1.预设怀疑点->自动找怀疑点 2.分析泄露链路慢->分析Retain size大的对象 3.分析OOM->对象裁剪,不全部加载到内存
|
线上监控完整方案
1 2 3
| 1.待机内存、重点模块内存、OOM率 2.整体及重点模块GC次数、GC时间 3.增强的LeakCanary自动化内存泄露分析
|
八、内存优化技巧总结
优化大方向
优化细节
1 2 3 4 5 6
| 1.LargeHeap属性(如:系统默认256m,申请了LargeHeap属性则默认内存*2) 2.onTrimMemory(低内存回调,低内存时主动做一些处理,关闭一些功能等) 3.使用优化过的集合:SparseArray 4.谨慎使用SharedPreference(调用时会将缓存的东西全部加载到内存中去,或者大数据量的东西不要用SharedPreference) 5.谨慎使用外部库(怕存在隐患) 6.业务架构设计合理
|
九、常见问题
1 2 3 4 5 6 7 8 9 10 11
| 1.你们内存优化项目的过程是怎么做的 - 分析现状、确认问题 - 针对性优化 - 效率提升 2.你做了内存优化最大的感受是什么 - 磨刀不误砍柴工 - 技术优化必须结合业务代码 - 系统化完善解决方案 3.如何检测所有不合理的地方 - ARTHook - 重点强调区别
|