引言
最近在Github上看到一个面试Issue的讨论: 自定义 Handler 时如何有效地避免内存泄漏问题? 里面有高赞的答案,有详细的文字分析,也有盖楼灌水,但是读后感觉缺少了什么----直观,直观的显示内存是否泄漏,想着结合自己在实际项目中使用Profiler的经历,把他们混合在一起,(中年待业时间多 手动拍脸.jpg)
本篇文章你能get什么?
- Java/Kotlin内部类、外部类
- Android Studio Profiler使用
- 图文并茂展示自定义内部类Handler内存泄漏的情况
- 自定义Handler的最佳实践
Java/Kotlin内部类&外部类
1. Java内部类
Java内部类按照是否静态来分类: 静态内部类,非静态内部类。
csharp
public class JavaOuter {
// Class member variables and function
private String name;
private int age;
public void printInfo() {
System.out.println("name = " + name + ", age = " + age);
}
// Class lass static properties and method
private static String staticName;
private static int staticAge;
static void printStaticInfo() {
System.out.println("static name = " + staticName + ", static age = " + staticAge);
}
// Non-static inner class
class InnerClass {
public void testInnerClass() {
name = "tancolo";
age = 35;
printInfo();
}
}
// Static inner class
static class StaticInnerClass {
public void testInnerClass() {
staticName = "tancolo";
staticAge = 35;
printStaticInfo();
}
}
}
Java内部类的使用测试代码:
java
public class JavaDemo {
public static void main(String[] args) {
// Create an object of normal inner class
JavaOuter.InnerClass innerClass = new JavaOuter().new InnerClass();
innerClass.testInnerClass();
// Create an object of static inner class
JavaOuter.StaticInnerClass staticInnerClass = new JavaOuter.StaticInnerClass();
staticInnerClass.testInnerClass();
}
}
注意:
- Java的非静态内部类的实例化需要依赖构建外部类的实例, 静态内部类不需要。
- Java的非静态内部类可以访问外部类实例的非静态属性/方法,静态内部类无法访问。
2. Kotlin内部类
kotlin
// Use static variables & function with global variable and function in Kotlin.
private const val staticName = ""
private const val staticAge = 0
fun printStaticName() {
println("staticName = $staticName, staticAge = $staticAge")
}
class KotlinOuter {
private var name: String = ""
private var age: Int = 0
fun printName() {
println("name = $name, age = $age")
}
// Inner class (non-static inner class)
inner class RealInnerClass {
fun testInner() {
name = "tancolo"
age = 35
printName()
}
}
// Nest inner class, the same with static inner class of Java
class NestClass {
fun testInner() {
printStaticName()
}
}
}
注意:
- Kotlin没有staic关键字,没有静态内部类这个说法, 取代的是 内部类 + 嵌套类,对应Java中的非静态内部类 + 静态内部类;
- Kotlin默认写的是嵌套类(贴心),要想声明为普通的内部类,需要加上
inner
关键字。- Kotlin的内部类、嵌套类同样需要遵守Java非静态/静态内部类的规则。(因为他们是一伙的)
Kotlin是在Java上添加了"魔法",以嵌套类为例,查看反编译后的字节码,还是Java的静态内部类那套。
Okay, 做了这么长的铺垫,是为了告知Java/Kotlin中的普通内部类的对象会持有外部类的对象引用,存在内存泄漏的风险。于是,关联上了前文的引言,Activity之类的组件中自定义内部类Handler,是如何导致内存泄漏的?铺垫不嫌多,一个接一个,Android Studio Profiler性能分析工具,来一个。
Profiler工具
不记得是从Android Studio 3.x 还是 4.x开始,Profiler
取代了以前的Monitor
, 集成度高、观察直观、使用方便。本文只涉及Profiler快速上手部分,目的是直观的展示自定义Handler是否有泄漏的不同情况。
Profiler快速入门图鉴
自定义Handler可能导致内存泄漏情况分析
泄漏的原因?
在正常情况中 Activity等组件 类中有个自定义Handler,Handler实例化中有Activity实例,然后Handler发送消息,handlerMessage
处理消息,处理完了, 当Activity finish后,这些资源也就会被GC掉。那么怎么就泄漏了呢?
这里的泄漏是指Activity等组件 当要被GC回收时候,发现该Activity被其他的对象实例持有着,就没办法去回收掉,假如这种Activity不断的被创建,又不能被回收,内存就这么被消耗掉了。
原因是明确的:从Java/Kotlin语言层面来说内部类(非静态内部类)持有外部类对象实例这个性质,在内存泄漏方面存在隐患,需要规范编程。具体到自定义Handler这个例子。
- 自定义内部类Handler持有外部类Activity的引用,
- Message持有该Handler的引用, MessageQueue持有Message, Looper持有MessageQueue.
- 在某些case下,比如消息延迟执行或者Activity中的其他耗时线程要跟Handler协作,会导致当前Activity即使finish掉,走了
onDestroy()
方法,然后遇到GC,还是不能被回收,因为这个持有链
存在,该Activity是被引用的。
如何复现?采用Kotlin
知道原因后就好复现了,采用Handler发送延时消息的方式来复现,这里涉及到几个测试因素:
- Inner class (内部类)
- Nest class (嵌套类)
- WeakReference
- RemoveCallbackAndMessage (后文用
remove
替代)
根据上述测试因素,我将他们分为8种情况,目的是测试出最佳的工程实践 , 代码在我Github仓库中有详细的说明 => 传送门
- 内部类 ===> 会泄漏
- 内部类 + WeakReference ===> 会泄漏
- 内部类 + remove ===> 没有泄漏
- 内部类 + WeakReference + remove ===> 没有泄漏
- 嵌套类 ===> 会泄漏
- 嵌套类 + WeakReference ===> 没有泄漏
- 嵌套类 + remove ===> 没有泄漏
- 嵌套类 + WeakReference + remove ===> 没有泄漏
内部类(Only)
自定义Handler
kotlin
inner class InnerClassHandlerCase1(activity: AppCompatActivity) :
Handler(Looper.getMainLooper()) {
private val mActivity: AppCompatActivity
init {
mActivity = activity
}
override fun handleMessage(msg: Message) {
println("Inner class case 1 ===> msg arg is ${msg.arg1}")
Toast.makeText(
this@TestMemoryLeakHandlerAndActivity,
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testInnerClassHandlerCase1() {
mInnerClassHandlerCase1 = InnerClassHandlerCase1(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mInnerClassHandlerCase1.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延时
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
如上描述,单独使用内部类,在我们设置的测试条件中会导致Activity泄漏。Note:这里有个小坑,mActivity: AppCompatActivity
是多余的,为了8个测试整体好看,在内部类构造时把Activity实例也传入,内部类本身是可以通过this@xxx访问外部类属性以及方法的。后面不再赘述
内部类 + WeakReference
自定义Handler
kotlin
inner class InnerClassHandlerCase2(activity: AppCompatActivity) :
Handler(Looper.getMainLooper()) {
private val mWeakReferenceActivity: WeakReference<AppCompatActivity>
init {
mWeakReferenceActivity = WeakReference(activity)
}
override fun handleMessage(msg: Message) {
println("Inner class case 2 ===> msg arg is ${msg.arg1}")
Toast.makeText(
this@TestMemoryLeakHandlerAndActivity,
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testInnerClassHandlerCase2() {
mInnerClassHandlerCase2 = InnerClassHandlerCase2(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mInnerClassHandlerCase2.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延时
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
不是说加入了弱引用就不会泄漏, 这里的根本因素是内部类Handler持有外部类Activity实例,跟弱引用mWeakReferenceActivity: WeakReference<AppCompatActivity>
没有关系,在设置的测试条件中还是会导致Activity泄漏。
内部类 + remove
自定义Handler
kotlin
inner class InnerClassHandlerCase3(activity: AppCompatActivity) :
Handler(Looper.getMainLooper()) {
private val mActivity: AppCompatActivity = activity
override fun handleMessage(msg: Message) {
println("Inner class case 3 ===> msg arg is ${msg.arg1}")
Toast.makeText(
mActivity,
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testInnerClassHandlerCase3() {
mInnerClassHandlerCase3 = InnerClassHandlerCase3(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mInnerClassHandlerCase3.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延迟
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
没有泄漏!这个case没有泄漏的原因是当Activity onDestroy
时候把Handler对应的消息以及回调都remove了。这么一来,Activity的持有链
不存在,就可以被GC回收。
内部类 + WeakReference + remove
自定义Handler
kotlin
inner class InnerClassHandlerCase4(activity: AppCompatActivity) :
Handler(Looper.getMainLooper()) {
private val mWeakReferenceActivity = WeakReference(activity)
override fun handleMessage(msg: Message) {
println("Inner class case 4 ===> msg arg is ${msg.arg1}")
Toast.makeText(
mWeakReferenceActivity.get(),
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testInnerClassHandlerCase4() {
mInnerClassHandlerCase4 = InnerClassHandlerCase4(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mInnerClassHandlerCase4.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
没有泄漏!这种 内部类 + WeakReference + remove 是没有必要的, 起作用的还是因为remove了Handler中的消息,把Activity的持有链
打破。
嵌套类(Only)
自定义Handler
kotlin
class NestClassHandlerCase1(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
private val mActivity: AppCompatActivity
init {
mActivity = activity
}
override fun handleMessage(msg: Message) {
println("Nest class case 1 ===> msg arg is ${msg.arg1}")
Toast.makeText(
mActivity,
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testNestClassHandlerCase1() {
mNestClassHandlerCase1 = NestClassHandlerCase1(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mNestClassHandlerCase1.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
会泄漏!嵌套类是无法访问外部类对象内的方法的,需要传入Activity(Context)到嵌套类中,这在实践开发中普遍。这样一来,外部类Activity的强引用的持有链
一直存在,即使Activity finish了,还是无法被GC回收。
嵌套类 + WeakReference
自定义Handler
kotlin
class NestClassHandlerCase2(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
private val mWeakReferenceActivity = WeakReference(activity)
private val mWeakReferenceApplicationContext = WeakReference(activity.applicationContext)
override fun handleMessage(msg: Message) {
println("Nest class case 2 ===> msg arg is ${msg.arg1}")
Toast.makeText(
//mWeakReferenceActivity.get(), // ERROR
mWeakReferenceApplicationContext.get(), // CORRECT
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testNestClassHandlerCase2() {
mNestClassHandlerCase2 = NestClassHandlerCase2(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mNestClassHandlerCase2.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
没有泄漏,从之前的分析上看,MessageQueue持有Message,Message持有Handler,Handler通过传入的Activity对象持有Activity引用,按说也是会泄漏的。其实不然,嵌套内实例本身是不依赖外部类Activity对象引用的,即使MessageQueue会持有传入的Activity的引用,因为弱引用的存在,该Activty的引用可以被GC回收。
嵌套类 + remove
自定义Handler
kotlin
class NestClassHandlerCase3(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
private val mActivity = activity
override fun handleMessage(msg: Message) {
println("Nest class case 3 ===> msg arg is ${msg.arg1}")
Toast.makeText(
mActivity,
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testNestClassHandlerCase3() {
mNestClassHandlerCase3 = NestClassHandlerCase3(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mNestClassHandlerCase3.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
没有泄漏!原因也简单,没有WeakReference
,但是Activity的持有链
被打破了。
嵌套类 + WeakReference + remove
自定义Handler
kotlin
class NestClassHandlerCase4(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
private val mWeakReferenceActivity = WeakReference(activity)
override fun handleMessage(msg: Message) {
println("Nest class case 4 ===> msg arg is ${msg.arg1}")
Toast.makeText(
mWeakReferenceActivity.get(),
"Receive the delay ${msg.arg1} seconds message",
Toast.LENGTH_SHORT
).show()
}
}
Handler发送延时消息
kotlin
private fun testNestClassHandlerCase4() {
mNestClassHandlerCase4 = NestClassHandlerCase4(this)
findViewById<Button>(R.id.btn_send_message).setOnClickListener {
// Send message
Message.obtain().let {
it.arg1 = MSG_DELAY_TIME
mNestClassHandlerCase4.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
.show()
finish()
}
}
}
测试结果
没有泄漏,这个是双重buffer
(弱引用 + remove消息回调)叠加啊。
总结 自定义Handler最佳实践
关于最佳实践,在引言中提到的issue讨论中,就有写道,本文"洋洋洒洒"的图文只是他们讨论的总结,基于Kotlin开发。
- 自定义的嵌套Handler
- 加入WeakReference 把传入的Context作为若引用
- 当Activity被销毁的时候如果还有消息没有发出去,在
onDestroy()
removeCallbacksAndMessages(null)清除Message和Runnable
不过对于3.
之前在实际项目中,并没有去remove, 一是,我需要去handler特定Message,即使Activity消失了。二是,这些handler message以及Runnable没有耗时操作,结束就结束了,资源也就会被释放掉。
Android Studio智能提醒
在Android Studio Hedgehog | 2023.1.1 Patch 2这个版本中,发现有很贴心的提醒,之前的版本估计也有。当用Kotlin写内部类时候,IDE会给出提醒,提示会有泄漏,并给出了解决方法,类似上面的 1.
和 2.
求点赞、关注、Star
老实说,写技术博客是件耗时耗力的累活,既要保证写的正确,有逻辑条理,还得是图文并茂。本来觉得1天工夫足矣, 奈何断断续续耗费了几天。本文中所有的代码都会在 github.com/tancolo/wid... 这个仓库中。该仓库我会分门别类的去整理各种widget
不限于自定义view啦,纯Java/Kotlin代码片段啦。感谢掘金社区提供的平台,感谢亲们的阅读,我的首发文章终于完成了。