Clean Android WebView caching

This post has been republished via RSS; it originally appeared at: Microsoft Mobile Engineering - Medium.

Want to learn how to reliably cache android webviews without causing context leaks? You are at the right place.

Photo by Richy Great on Unsplash

Initialising and configuring web views in android are costly, isn’t it? At some point in your career, you would have faced a situation, where you would have used WebView in multiple activities, and initialising it on per activity basis becomes costly. We will see how we can solve this problem, and how we at Microsoft Teams got the performance up by 70%.

To understand the problem and its scope, you need to understand how Teams android app acts as an ecosystem for multiple apps to thrive. Any external developer can build their apps(RN or WebApps) and integrate it with teams. To build that we provide a shell activity for them to host their application. Let’s call it ShellActivity for simplicity. They can interact with team's resources using that. Suppose you are building appA and the JS execution in WebViewswas taking good amount of time (This JS init must be done for every plugged-in web app). When you navigate through multiple apps, it creates multiple ShellActivityinstances, and each instance must initialise the WebViewspertaining to it. It was impacting the performance a lot, it took 3.8s P95 to execute that code. And paying this cost on every instance ofWebViewwas indeed costly.

One of your partner developers Venu was working on such apps, and was facing performance issues that we discussed above. Venu talked about this problem to Ravi, who is one of our peer developers who takes care of enabling partner developers to onboard seamlessly onto the teams app. Me and Ravi were having some coffee table discussion, and he mentioned this problem to me. I vaguely remembered that I solved this problem some years back using web view sharing. More details on that later.

How can we share web views? What is the solution that comes to our mind? Caching!

I said that we should be able to cache web views across activities but, his obvious question was, won’t it leak our activity’s context? And he was right!

WebViewsare android views, that when created, consume context. And if we cache activity context on the application level, we might end up leaking the activity that was used to create the web-view. So, what is the solution?

We looked at the official documentation of webviews and we found this in the constructor section of web view.

Note: WebView should always be instantiated with an Activity Context. If instantiated with an Application Context, WebView will be unable to provide several features, such as JavaScript dialogs and autofill.

It means we can create a WebView with an application context, and there is nothing wrong with it but, we will be missing some functionalities. We can use this capability to store the web views with application context.

But the problem is,

“How will we use it in the activity context? For solving this problem, we have MutableContextWrapper to our service”

According to android docs, it is a special version of ContextWrapper that allows the base context to be modified after it is initially set.

The solution was clear, we will use a MutableContextWrapper to create the WebView. Whenever we would need the cached instance we will ask the pool for it, with the activity context, and pool will plug activity context in the mutable wrapper and return the WebView back. Similar flipping to application context will be done while returning the WebView to the pool. Code will provide more clarity on this.

At any point in the pool, WebView will not be tied to any activity’s instance and that will prevent us from leaking activity context.

Now, we had a solution for this problem, the steps forward were coding it and testing it for any memory leaks, and seeing if we get gains as expected. Ravi started to work actively on this solution and did profiling for the existence of leaks, and we found that there were no leaks. The APIs for our actual solution looked little different than what we have shown in this blog as we cannot share the exact code. But things are like what we have done, and the provided solution would work the same way.

Now, think of our pool’s API like below.

public interface WebViewPool {
/**
* Provides a web-view for the given activity context.
* @param activity activity for which web view is asked.
* @return cached web view instance
*/
WebView obtain(@NonNull Context activity);

/**
* Release the cached web view back to the pool. Implementations should make sure that
* no web view that is a not a part of the pool is released.
* @param webView cached web view that we want to return
* @param borrower context that was used for obtaining the web view.
* @return true if released successfully, false otherwise
*/
boolean release(@NonNull WebView webView, @NonNull Context borrower);

}

We will now write a simple implementation of a SingularWebViewPool that contains a single WebView, and if that WebViewis already allocated, and a new request comes, it will crash. I have added crashes in other places also, you can change things and remain silent or log errors. This is just for demonstration purpose.

The APIs can be consumed like this. In most of the cases, pool will obtain() in onStart() and release() in onStop().

public void someWork() {
    // Pool should be a singleton. Creating it here for simplicity!
WebViewPool pool = new SingularWebViewPool(applicationContext);
    // Usually in onStart()
WebView cachedView = pool.obtain(this);

// some work.

// Release in onStop()
if(!pool.release(cachedView ,this)) {
// Some error happened in releasing.
}
}

Simple!

Similarly, if we want, we can create a pool implementation that can give out some fixed number of shared web views, something like a FixedSizeWebViewPool. We are free to do that. We must manage all obtain() and release()properly and the book-keeping, so that the pool does not go into an illegal state.

We shared the APIs with the Venu, and then, he built his use case on top of this.

When the code went to production and telemetry started to come in, the reduction in time was amazing. It came down to 1.2s. That was ~70% improvement in the performance.

It is a different story that why the JS code execution was taking 3.8 seconds, and we are optimising on that. But we got some breathing space with this solution.

Can you do same thing for other views, like TextView or a Button? Share your thoughts in the comments!


Clean Android WebView caching was originally published in Microsoft Mobile Engineering on Medium, where people are continuing the conversation by highlighting and responding to this story.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.