一个匿名内部类的导致内存泄漏的解决方案
泄漏原因
匿名内部类默认会持有外部类的类的引用。如果外部类是一个Activity或者Fragment,就有可能会导致内存泄漏。 不过在使用kotlin和java中在匿名内部类中有一些不同。
在java中,不论接口回调中是否调用到外部类,生成的匿名内部类都会持有外部类的引用
在kotlin中,kotlin有一些相关的优化,如果接口回调中不调用的外部类,那么生成的匿名内部类不会持有外部类的引用,也就不会造成内存泄漏。 反之,如果接口回调中调用到外部类,生成的匿名内部类就会持有外部类引用
我们可以看一个常见的例子:
class MainActivity : AppCompatActivity() {
private lateinit var textView: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
textView = findViewById(R.id.text)
test()
}
private fun test() {
val client = OkHttpClient()
val request = Request.Builder()
.url("www.baidu.com")
.build();
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) {
textView.text = "1111"
}
})
}
}
在Activity的test方法,发起网络请求,在网络请求成功的回调中操作Activity的textView。当然在这个场景中,Callback返回的线程非主线程,不能够直接操作UI。为了简单的验证内存泄漏的问题,先不做线程切换。 可以看看对应编译后的字节码,这个callback会生成匿名内部类。
public final class MainActivity$test$1 implements Callback {
final /* synthetic */ MainActivity this$0;
MainActivity$test$1(MainActivity $receiver) {
this.this$0 = $receiver;
}
public void onFailure(Call call, IOException e) {
Intrinsics.checkNotNullParameter(call, NotificationCompat.CATEGORY_CALL);
Intrinsics.checkNotNullParameter(e, "e");
}
public void onResponse(Call call, Response response) {
Intrinsics.checkNotNullParameter(call, NotificationCompat.CATEGORY_CALL);
Intrinsics.checkNotNullParameter(response, "response");
TextView access$getTextView$p = this.this$0.textView;
if (access$getTextView$p != null) {
access$getTextView$p.setText("1111");
} else {
Intrinsics.throwUninitializedPropertyAccessException("textView");
throw null;
}
}
}
默认生成了MainActivity$test$1辅助类,这个辅助类持有了外部Activity的引用。 当真正调用了enqueue时,会把这个请求添加请求的队列中。
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
网络请求处于等待中,callback会被添加到readyAsyncCalls队列中, 网络请求处于发起,但是未结束时,callback会被添加到runningAsyncCalls队列中。 只有网络请求结束之后,回调之后,才会从队列中移除。 当页面销毁时,网络请求未成功结束时,就会造成内存泄漏,整个引用链路如下图所示:
网络请求只是其中的一个例子,基本上所有的匿名内部类都可能会导致这个内存泄漏的问题。
解决方案
既然匿名内部类导致的内存泄漏场景这么常见,那么有没有一种通用的方案可以解决这类的问题呢?我们通过动态代理去解决匿名内部类导致的内存泄漏的问题。 我们把Activity和Fragment抽象为ICallbackHolder。
public interface ICallbackRegistry {
void registerCallback(Object callback);
void unregisterCallback(Object callback);
boolean isFinishing();
}
提供了三个能力
registerCallback: 注册Callback
unregisterCallback: 反注册Callback
isFinishing: 当前页面是否已经销毁
在我们解决内存泄漏时需要用到这三个API。
还是以上面网络请求的例子,我们可以通过动态代理来解决这个内存泄漏问题。 先看看使用了动态代理之后的依赖关系图
实线表示强引用 虚线表示弱引用
通过动态代理,将使用匿名内部类与okHttp-Dispatcher进行解耦,okHttp-Dispatcher直接引用的动态代理对象, 动态代理对象不直接依赖原始的callback和activity,而是以弱引用的形式依赖。
此时callback并没有被其他对象强引用,如果不做任何处理,这个callback在对应的方法运行结束之后就可能被回收。
所以需要有一个步骤,将这个callback和对应的Activity、Fragment进行绑定。此时就需要用到前面定义到的ICallbackHolder,通过registerCallback将callback注册到对应Activity、Fragment中。
最后在InvocationHandler中的invoke方法,判断当前的Activity、Fragment是否已经finish了,如果已经finish了,就不再进行回调调,否则进行调用。
回调完成后,如果当前的Callback是否是一次性的,就从callbackList中移除。
接下来可以看看我们怎么通过调用来构建这个依赖关系:
使用CallbackUtil
在创建匿名内部类时,同时传入对应的ICallbackHolder
client.newCall(request).enqueue(CallbackUtil.attachToRegistry(object : Callback {
override fun onFailure(call: Call, e: IOException) {}
override fun onResponse(call: Call, response: Response) {
textView.text = "1111"
}
}, this))
创建动态代理对象
动态代理对象对于ICallbackHolder和callback的引用都是弱引用,同时将callback注册到ICallbackHolder中。
private static class MyInvocationHandler<T> extends InvocationHandler {
private WeakReference<T> refCallback;
private WeakReference<ICallbackHolder> refRegistry;
private Class<?> wrappedClass;
public MyInvocationHandler(T reference, ICallbackRegistry callbackRegistry) {
refCallback = new WeakReference<>(reference);
wrappedClass = reference.getClass();
if (callbackRegistry != null) {
callbackRegistry.registerCallback(reference);
refRegistry = new WeakReference<>(callbackRegistry);
}
}
}
invoke方法处理
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
ICallbackRegistry callbackRegistry = callbackRegistry != null ? refRegistry.get() : null;
T callback = refCallback.get();
Method originMethod = ReflectUtils.getMethod(wrappedClass, method.getName(), method.getParameterTypes());
if (callback == null || holder != null && holder.isFinishing()) {
return getMethodDefaultReturn(originMethod);
}
if (holder != null && ....)
{
holder.unregisterCallback(callback);
}
...
return method.invoke(callback, args);
}
在页面销毁时,不回调原始callback。这样,也避免了出现因为页面销毁了之后,访问页面的成员,比如被butterknife标注的view导致的内存泄漏问题。
作者:谢谢谢_xie
来源:https://juejin.cn/post/7074038402009530381