一个匿名内部类的导致内存泄漏的解决方案

泄漏原因

匿名内部类默认会持有外部类的类的引用。如果外部类是一个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导致的内存泄漏问题。

该想法来自于Android大佬-李同学。

猜你喜欢

转载自juejin.im/post/7074038402009530381