Leakage reason
Anonymous inner classes hold class references to the outer class by default. If the external class is an Activity or Fragment, it can cause a memory leak. However, there are some differences between using Kotlin and Java in anonymous inner classes.
- In Java, the generated anonymous inner class holds a reference to the external class, whether or not it is called in the interface callback
- In Kotlin, kotlin has some related optimizations that if the external class is not called in the interface callback, the generated anonymous inner class will not hold references to the external class and will not cause a memory leak. Conversely, if an external class is called in the interface callback, the generated anonymous inner class holds the reference to the external class
Let’s look at a common example:
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"
}
})
}
}
Copy the code
In the test method of the Activity, a web request is made, and the Activity’s textView is manipulated in the callback for a successful web request. Of course, in this scenario, the Callback returns a thread that is not the main thread and cannot manipulate the UI directly. For simple verification of the memory leak problem, let’s not do thread switching. If you look at the compiled bytecode, this callback generates the anonymous inner class.
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; }}}Copy the code
The MainActivity$test$1 helper class is generated by default, which holds references to the external Activity. When enQueue is actually called, the request is added to the queue of requests.
private val readyAsyncCalls = ArrayDeque<AsyncCall>()
private val runningAsyncCalls = ArrayDeque<AsyncCall>()
Copy the code
While the network request is waiting, the callback is added to the readyAsyncCalls queue, while the network request is initiated but not completed, the callback is added to the runningAsyncCalls queue. It is removed from the queue only after the network request has finished and after a callback. When a page is destroyed, a memory leak occurs when the network request does not end successfully, and the entire reference link is shown below:
Network requests are just one example, and almost all anonymous inner classes can cause this memory leak problem.
The solution
Since the memory leak scenario caused by anonymous inner classes is so common, is there a general solution to this problem? We use dynamic proxies to solve the problem of memory leaks caused by anonymous inner classes. We abstract the Activity and Fragment as ICallbackHolder.
public interface ICallbackRegistry {
void registerCallback(Object callback);
void unregisterCallback(Object callback);
boolean isFinishing();
}
Copy the code
Three capabilities are provided
- RegisterCallback: Registers the Callback
- UnregisterCallback: unregisterCallback
- IsFinishing: Whether the current page has been destroyed
We need these three apis to solve memory leaks.
Using the network request example above, we can solve this memory leak problem by using dynamic proxies. Take a look at the dependency diagram with dynamic proxiesSolid lines indicate strong references and dashed lines indicate weak references
- With dynamic proxies, anonymous inner classes are used to decouple okHTTP-Dispatcher from dynamic proxy objects referenced directly by okHTTP-Dispatcher that do not rely directly on the original callback and activity, but rather rely on them as weak references.
- At this point, the callback is not strongly referenced by another object, and if nothing is done, the callback may be recycled after the corresponding method has finished running.
- Therefore, there is a step required to bind the callback to the corresponding Activity and Fragment. In this case, we need to use the ICallbackHolder defined above, and register the callback with the corresponding Activity and Fragment through registerCallback.
- Finally, the invoke method in InvocationHandler determines whether the current Activity or Fragment has finished. If it has finished, the callback will not be performed. Otherwise, the callback will be called.
- After the Callback is complete, if the current Callback is not one-time, it is removed from the callbackList.
Let’s see how we can build this dependency with calls:
Using CallbackUtil
When you create the anonymous inner class, pass in the corresponding 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))
Copy the code
Create dynamic proxy objects
Dynamic proxy objects are weak references to both ICallbackHolder and callback, and register callback with 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);
}
}
}
Copy the code
Invoke method handling
@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); }Copy the code
The original callback is not called when the page is destroyed. This also avoids memory leaks caused by members visiting the page after the page is destroyed, such as views annotated by ButterKnife.
The idea comes from Android guru – Li.