先看问题
下面代码是一个简单的自定义View,很常用是吧,你们先看下这段代码有没有问题?
class TestView : FrameLayout {
// 前面是一个堆构造方法,最后都会调用这个init()方法
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
// 主要是这里用了一个use方法,原意是想自动调用recycle方法释放内存的
context.obtainStyledAttributes(attrs, R.styleable.TestView, defStyleAttr, 0).use {
// 一堆处理逻辑
}
}
}
代码就是这么个代码,这个代码有问题吗?
答案无非是三种:
- 代码100%没问题:那么问题就大了,我们的应用在上线前也是经过好几轮测试的,也都没有测出什么问题,即使线上80%的奔溃率后复测,也没复现。也不能怪测试,因为我们测试机确实有限,都是比较新的机器,这个问题在
Android11
以下才会有。 - 代码100%有问题:那么问题在哪里呢?但从上面的代码来看,也不一定有问题哦。
- 无法判断有没有问题:这就对了,因为后来我才发现,这个use方法就不止一个,还要看上面代码import了哪个use方法。
这个问题坑在哪里呢?
- 自定义View是一个非常常用的操作,这种写法也是很常用的,但是IDE不会报错,编译也不会报错。
- 这是在Android11以下才会报错,如果刚好你手上的测试机是Android12以上,那么你在开发阶段发现不了这个问题。
- 如果刚好测试们也没有用Android11以下的机子来测试,那么恭喜你,你会在上线后才发现这个问题。
问题分析
首先 context.obtainStyledAttributes(attrs, R.styleable.InputView2, defStyleAttr, 0)
会返回一个 TypedArray
对象,这个对象最后是要调用 recycle()
方法释放的。
然后我们懒惰用了kotlin提供的 use()
方法,让它自动释放,看看这个use长什么样:
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
// 省略代码
} finally {
// 省略代码
// 反正最后就是调用 close() 方法,也就是 Closeable 的 close() 方法
close()
}
}
从上面代码可以看出最后是调用了 Closeable
的 close()
方法,而 TypedArray
实现了这个接口:
public class TypedArray implements AutoCloseable {
public void close() {
recycle();
}
}
一直到这里都是正常的,所以编译器也不会报错,测试也没有测出来。
那么问题来了,我去找了Android11的 TypedArray
的源码,并没有实现 Closeable
接口:
public class TypedArray {
// 省略省略
}
解决方法
既然是 use()
引起的问题,那么不用它就是啦,直接用原始的 try catch
就行了。
但是,我发现 Jetpack 帮我们写了扩展方法:
public inline fun <R> TypedArray.use(block: (TypedArray) -> R): R {
return block(this).also {
recycle()
}
}
所以,我们直接用就行啦,不过一定要记住 import 对了:
import androidx.core.content.res.use
class TestView : FrameLayout {
private fun init(context: Context, attrs: AttributeSet?, defStyleAttr: Int) {
context.obtainStyledAttributes(attrs, R.styleable.TestView, defStyleAttr, 0).use {
}
}
}