(二)Android 性能优化 Memory Profiler
小酌鸡汤
学贵有疑,小疑则小进,大疑则大进。
本文来源《Android 性能优化 全家桶》
什么是内存泄漏?
内存泄露:无用对象持续占有内存或无用对象的内存得不到及时的释放(程序申请分配内存空间后,使用完毕后未释放。结果会导致一直占据该内存单元,也无法再使用该内存单元,直到程序结束)
内存泄漏的影响:容易使得应用程序发生内存溢出,即 OOM(Out of Memory) 。
内存溢出:OOM(这个就很严重了),程序向系统申请的内存空间超出了系统能给的最大内存单元。
内存溢出的影响:导致ANR,甚至应用Crash。
内存抖动:内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象,从而使UI线程被频繁阻塞,导致画面卡顿。
它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动。
内存泄漏的根本原因
本该回收的对象,因为某些原因(对象的强引用被另外一个正在使用的对象所持有,且没有及时释放),进而造成内存单元一直被占用,造成内存泄漏,甚至可能造成内存溢出!
简言之:持有引用者的生命周期 > 被引用者的生命周期。
原理基础知识:关于JVM的内存分区和GC机制(先请大家自行百度,后续我会为此单开一篇)
Android中内存泄漏分类
内存泄漏分类(一)线程未结束(举栗子:Handler)
- 主线程的Looper对象的生命周期 = 该应用程序的生命周期
- 在Java中,非静态内部类和匿名内部类都默认持有外部类的引用
- 引用关系: 未被处理 / 正处理的消息 -> Handler实例 -> 外部类
- Handler的生命周期 > 外部类的生命周期。外部类销毁时,外部类无法被垃圾回收器(GC)回收。
- 解决方案:(1)静态内部类 + 弱引用;(2)当外部类结束生命周期时,清空Handler内消息队列。
(二)非静态内部类
- 因为非静态内部类持有外部类的隐式引用,容易导致意料之外的泄漏。比如我们创建一个内部类,而且持有一个静态变量的引用。
- 解决方案:(1)使用静态内部类(静态内部类不持有外部类的引用,打破了链式引用);(2)注意管理引用的生命周期;(3)避免静态变量。
(三)单例模式
- 单例的特性导致它和应用的生命周期一样长。一般泄露发生都是因为传入了一个Activity的Context。
- 解决方案:需要使用Context,不要传入Activity的Context, 正确的做法是使用Application的Context。
(四)static变量引用
- 因为static变量的生命周期是在类加载时开始、类卸载时结束,也就是说static变量是在程序进程死亡时才释放,如果在static变量中 引用了Activity/View ,那么 这个Activity由于被引用,便会随static变量的生命周期一样,一直无法被释放,造成内存泄漏。
- 解决方案:(1)使用Application Context;(2)弱引用;(3)生命周期结束,主动断开引用链。
(五)集合中对象没清理造成的内存泄漏
- 当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。
- 解决方案:在Activity退出之前,将集合里的东西clear,然后置为null,再退出程序。
(六)BitMap占用过多内存
- bitmap的解析需要占用内存,但是内存只提供8M的空间给BitMap,如果图片过多,并且没有及时 recycle bitmap 那么就会造成内存溢出。
- 解决方案:(1)及时recycle,并显示置为null;(2)压缩图片之后加载图片;(3)当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
(七)属性动画
- 动画会持有view对象,进而持有activity
- 解决方案:(1)在页面退出(destroy)时取消动画;(2)如果同时从当前页面跳转到其他页面,也应该在onPause中暂停动画,以避免资源浪费,在回到该页面时在onResume中继续动画播放。
(八)资源未及时关闭
- 资源性对象比如(Cursor游标,Stream流,BroadCastReceiver等)往往都用了一些缓冲,我们在不使用的时候或者在使用完之后,应该及时关闭它们比如close()方法,以便它们的缓冲及时回收内存。
- 解决方案:及时关闭(close)。
(九)订阅/取消订阅不匹配(比如Service业务监听/广播反注册)
- Service注册监听如果不取消,Service会一致持有页面引用,导致内存泄漏。
- BroadcastReceiver不止被Activity引用,还可能会被AMS等系统服务、管理器等之类的引用,导致BroadcastReceiver无法被回收,而BroadcastReceiver中又持有着Activity的引用(即:onReceive方法中的参数Context),会导致Activity也无法被回收(虽然Activity回调了onDestroy方法,但并不意味着Activity被回收了),从而导致严重的内存泄漏。
- 解决方案:及时取消订阅。
(十)任务不及时取消(Service/WebView/Retrofit/Rxjava等)
- 任务未关闭,肯定会有内存泄漏,更可怕的是,如果在正常的销毁流程中做了一些重置工作,而再后续有任务继续反馈工作时,可能会空指针等各种异常。
- 解决方案:任务及时取消,follow宿主的生命周期,不要让其苟活……
(十一)执行频率高的地方或循环中创建对象(onMeasure/onDraw)
- 在循环体内创建对象、自定义View的onDraw()创建对象、大量初始化Bitmap、频繁创建大内存对象。
- 解决方案:(1) 尽量避免在循环体内创建对象,应该把对象创建移到循环体外;(2)自定义View的onDraw()方法会被频繁调用,在这里面不应该频繁的创建对象;(3)对于能够复用的对象,可以使用对象池将它们缓存起来。
(十二)Toast显示
- Toast里面有一个LinearLayout,这个Context就是作为LinearLayout初始化的参数,它会一直持有Activity,Toast显示是有时间限制的(异步任务),如果Toast还在显示Activity就销毁了,由于Toast显示没有结束不会结束生命周期,这个时候Activity就内存泄漏了。
- 解决方案:(1)不要直接Toast,自己封装一个ToastUtil,使用ApplicationContext来调用(或者通过getApplicationContext来调用);(2)还有一种通过toast变量的cancel来取消这个显示
小憩一下,正式开始
为什么要用 memory profiler?
Memory Profiler 是 Android Profiler中的一个组件,可帮助您识别可能会导致应用卡顿、冻结甚至崩溃的内存泄露和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。
Memory Profiler 加入了自动检查 Activity 和 Fragment 中的内存泄漏的功能。使用这一功能非常的简单。
现在,就一起实操体验profiler吧!
(1)profiler实操环境(可选项,用自己的环境和代码也一样)
- SamplePop代码下载
- SamplePop环境如下:
Android Studio 4.0
Gradle version 6.1.1
Android API version 30
(2)打开profiler
- profiler打开位置:View -> Tool Windows -> Profiler
- 当然也可以直接运行程序并启用profiler监控:Run -> Profiler
(3)来吧,一起预览一下吧:
profiler概览(4)点击MEMORY分类栏,就可以进入到memory profiler详情页:
memory-profiler详情页窗口详细说明:
- 窗口1:分别由以下几个功能按钮组成
性能分类切换按钮,包括:cpu、memeory、network、energy。
用于强制执行垃圾回收事件的按钮
用于捕获堆转储]的按钮
用于指定分析器多久捕获一次内存分配的下拉菜单:
Full:捕获内存中的所有对象分配
Sampled:定期对内存中的对象分配进行采样
None:停止跟踪应用的内存分配 - 窗口2:页面调整按钮集合,包括:缩小,放大、重置、暂停、开始等。
- 窗口3:事件时间轴,显示Activity的生命周期不同状态,用户交互事件,如点击,旋转等。
- 窗口4:内存计数图例:
Java:从 Java 或 Kotlin 代码分配的对象的内存
Native:从 C 或 C++ 代码分配的对象的内存
Graphics:图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存
Stack:您的应用中的原生堆栈和 Java 堆栈使用的内存
Code:您的应用用于处理代码和资源(如 dex 字节码、 dex 代码、.so 库和字体)的内存
Others:您的应用使用的系统不确定如何分类的内存
Allocated:您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象 - 窗口5:内存使用量时间轴,它会显示以下内容:
1.一个堆叠图表,显示每个内存类别当前使用多少内存,如左侧的 y 轴以及顶部的彩色键所示
2.一条虚线,表示分配的对象数,如右侧的 y 轴所示
3.每个垃圾回收事件的图标。
(5)随时查看内存分配(在时间轴上拖动以选择要查看哪个区域的分配):
memory-profiler查看内存分配对各个窗口进行说明:
- 1.选定范围:可以拖动选择想分析的数据部分
- 2.选择要检查的堆:
image heap:系统启动映像,包含启动期间预加载的类。此处的分配保证绝不会移动或消失
zygote heap:写时复制堆,其中的应用进程是从 Android 系统中派生的
app heap:您的应用在其中分配内存的主堆
JNI heap:显示 Java 原生接口 (JNI) 引用被分配和释放到什么位置的堆 - 3.选择如何安排分配:
Arrange by class:根据类名称对所有分配进行分组。这是默认选项
Arrange by package:根据软件包名称对所有分配进行分组
Arrange by callstack:将所有分配分组到其对应的调用堆栈 - 4.过滤器:搜索过滤,matchCase区分大小写,Regex使用正则表达式
- 5.类名窗口,点击可以进入Instance View(实例窗口)
- 6.实例窗口,点击进入Call Stack(实例分配的线程)
- 7.线程窗口,显示该实例被分配到何处以及在哪个线程中,右键 -> Jump to Source
从上图可以查看对象分配的以下信息:
- 分配了哪些类型的对象以及它们使用多少空间。
- 每个分配的堆栈轨迹,包括在哪个线程中。
- 对象在何时被取消分配。
(6)捕获堆转储:
memory-profiler捕获堆转储窗口说明:
- 1.执行堆转储:我在MemoryProfilerActivity页面做了一些操作后,back回到MainActivity,等待1s后,执行堆转储(本次主要分析MemoryProfilerActivity的泄漏问题)
- 2.页面泄漏过滤:筛选Activity 和 Fragment 实例存在内存泄露的分析数据,过滤器显示的数据类型包括:
已销毁但仍被引用的 Activity 实例(t调试技巧:旋转屏幕/应用切换)
没有有效的 FragmentManager 但仍被引用的 Fragment 实例 - 3.本项目类过滤:只显示本项目的类
- 4.类窗口详细信息:
Allocations:堆中的分配数
Native Size : 此对象类型使用的原生内存总量(以字节为单位),你会在此处看到采用 Java 分配的某些对象的内存,因为 Android 对某些框架类(如 Bitmap)使用原生内存
Shallow Size:此对象类型使用的 Java 内存总量(以字节为单位)
Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位) - 5.实例窗口详细信息:
Depth:从任意 GC 根到选定实例的最短跳数
Native Size:原生内存中此实例的大小
Shallow Size:Java 内存中此实例的大小
Retained Size:此实例所支配内存的大小 - 6.实例引用:显示对应实例对象的每个引用,可以右键 -> Jump to Source/Go to Instance(跳转到相对应的实例进行查看)
捕获堆转储后,您可以查看以下信息:
- 您的应用分配了哪些类型的对象,以及每种对象有多少。
- 每个对象当前使用多少内存。
- 在代码中的什么位置保持着对每个对象的引用。
- 对象所分配到的调用堆栈
(7)SamplePop示例代码:
(一)Handler内存泄漏演练,先看下代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d(TAG, "onCreate: ");
setContentView(R.layout.activity_memory_profiler);
mResultTv = findViewById(R.id.memory_type_result);
((RadioGroup) findViewById(R.id.memory_handler_type)).
setOnCheckedChangeListener(mOnCheckedChangeListener);
}
private RadioGroup.OnCheckedChangeListener mOnCheckedChangeListener =
new RadioGroup.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup radioGroup, int i) {
switch (i) {
case R.id.handler_no_static:
memoryTestType = MEMORY_ERROR_NO_STATIC;
break;
case R.id.handler_inner_class:
memoryTestType = MEMORY_ERROR_INNER_CLASS;
break;
case R.id.handler_static_weak:
memoryTestType = MEMORY_NORMAL_STATIC_WEAK;
break;
case R.id.handler_no_static_clear_message:
memoryTestType = MEMORY_NORMAL_MESSAGE_CLEAR;
break;
default:
break;
}
}
};
public void onMemoryHandlerMonitor(View view) {
Log.d(TAG, "onMemoryHandlerMonitor: ");
monitorHandler();
}
//模拟Handler泄漏(memory profiler以这个来演示实操)
private static final int MESSAGE_DELAY = 5 * 1000;
private static final int MEMORY_ERROR_NO_STATIC = 1;
private static final int MEMORY_ERROR_INNER_CLASS = 2;
private static final int MEMORY_NORMAL_STATIC_WEAK = 3;
private static final int MEMORY_NORMAL_MESSAGE_CLEAR = 4;
//只要动态修改此参数就行
private int memoryTestType = MEMORY_ERROR_NO_STATIC;
TextView mResultTv;
private Handler mHandler;
@Override
protected void onDestroy() {
Log.d(TAG, "onDestroy: ");
if (MEMORY_NORMAL_MESSAGE_CLEAR == memoryTestType) {
mHandler.removeCallbacksAndMessages(null);
}
super.onDestroy();
}
//Button click 触发模拟操作
private void monitorHandler() {
Log.d(TAG, "monitorHandler: ");
switch (memoryTestType) {
case MEMORY_ERROR_NO_STATIC:
//泄漏1 : 使用no-static内部类
mHandler = new NoStaticHandler();
break;
case MEMORY_ERROR_INNER_CLASS:
//泄漏2:使用匿名内部类
mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
Log.d(TAG, "inner class handler handleMessage: " + msg.what);
mResultTv.setText("MEMORY_ERROR_INNER_CLASS");
}
};
break;
case MEMORY_NORMAL_STATIC_WEAK:
//不泄露3:静态+弱引用(为了保证Handler中消息队列中的所有消息都能被执行,
//推荐使用 静态内部类 + 弱引用的方式)
mHandler = new StaticHandler(this);
break;
case MEMORY_NORMAL_MESSAGE_CLEAR:
//不泄露4:destroy()方法对handler进行消息队列清空,结束Handler生命周期
mHandler = new NoStaticHandler();
break;
default:
Log.e(TAG, "monitorHandler: default no handle");
return;
}
//执行事件延迟队列发送
mHandler.sendEmptyMessageDelayed(1, MESSAGE_DELAY);
}
//no-static内部类:自定义Handler子类
class NoStaticHandler extends Handler {
@Override
public void handleMessage(Message msg) {
Log.d(TAG, "NoStaticHandler handleMessage: " + msg.what);
mResultTv.setText(memoryTestType == MEMORY_ERROR_NO_STATIC ?
"MEMORY_ERROR_NO_STATIC" : "MEMORY_NORMAL_MESSAGE_CLEAR");
}
}
private static class StaticHandler extends Handler {
private WeakReference<Activity> reference;
public StaticHandler(Activity activity) {
super(activity.getMainLooper());
reference = new WeakReference<>(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
Log.d(TAG, "StaticHandler handleMessage: " + msg.what);
Activity activity = reference.get();
if (null != activity) {
Log.d(TAG, "StaticHandler handleMessage: ui update");
((TextView) activity.findViewById(R.id.memory_type_result)).
setText("MEMORY_NORMAL_STATIC_WEAK");
}
}
}
(二)MemoryProfilerActivity界面如下:
内存优化模拟界面(三)我们对第六步的截图【memory-profiler捕获堆转储】增加操作步骤说明:
上面的堆转储测试的是Handler的非静态内部类,流程如下:
1.进入主界面MainActivity,点击进入MemoryProfilerActivity
2.选择【handler使用no-static内部类】,点击【模拟handler泄漏】
3.点击【back】返回MainActivity,等待1秒,点击AS(AndroidStudio)的【堆转储按钮】
4.生成堆转储,准备下面的分析
(四)分析上面的堆转储文件
1.我们选择AS的【Activity/Fragment Leaks】查看发现确实有MemoryProfilerActivity泄漏;
2.在类名窗口中我们点击泄漏的Actvity类,在 Instance View中跟踪发现是NoStaticHandler持有了MemoryProfilerActivity的引用,而NoStaticHandler的消息队列中仍然存在消息,所以导致MemoryProfilerActivity在执行堆转储时,是不满足GC条件的。
3.AS的【Activity/Fragment Leaks】是指是否满足GC条件,而不是是否已经GC。举个栗子:在进入一个Activity后(不执行任何操作),然后退出,这当然不会引起内存泄漏,因为它满足GC条件,但是也不是说它会立马被GC掉,你会在堆转储文件中可以看到此实例任然存在(可能会存在很久),如果你尝试N次主动GC,然后再看堆转储文件,发现就被GC掉了。
下面就让我们去优化它,走起!
(五)优化Handler内存泄漏:静态内部类 + 弱引用的方式
1.同样按照第三步的操作顺序执行,只是要选择【handler静态内部类 + 弱引用】
2.查看堆转储文件,查看【Activity/Fragment Leaks】下面,是不是MemoryProfilerActivity没有内存泄漏了?
(六)技巧分享
AS这个IDE的代码风险提示做的很好,大家在开发过程中要注意特殊颜色的标识,然后按照提示将其优化掉,基本就能解决大部分问题。大家可以在SamplePop的代码中感受下那些可能会引起内存泄漏等问题的特殊颜色标志:
内存泄漏IDE提示-1 内存泄漏IDE提示-2(七)课后小练习(奥里给)
//模拟订阅/反订阅缺失泄漏
@SuppressLint("MissingPermission")
private void monitorRegister() {
Log.d(TAG, "monitorRegister: ");
LocationManager locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
TimeUnit.MINUTES.toMillis(5), 100, new LocationListener() {
@Override
public void onLocationChanged(@NonNull Location location) {
Log.d(TAG, "onLocationChanged: ");
}
});
}
//模拟非静态内部类
private static Object inner;
private void monitorNoStaticInnerClass() {
Log.d(TAG, "monitorNoStaticInnerClass: ");
class InnerClass {
}
inner = new InnerClass();
}
小编的扩展链接
参考链接
唯有美景不可辜负
举手之劳,赞有余香! ❤ 比心 ❤