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

Clean Android WebView caching

Vishal Ratna
Microsoft Mobile Engineering
5 min readAug 17, 2022

--

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 (sanda venu) was working on such apps, and was facing performance issues that we discussed above. Venu talked about this problem to ravi krishnan, 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.

SingularWebViewPool implementation

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.

What are the caveats of using this approach?

  1. You see that we are creating the web view in the application context, that is very crucial for the whole approach, otherwise context will leak. As the context used from webview creation is propagated down to the MockViewthat is in the core of web views. And it extracts some properties from the context such as theme, some attributes etc. So, those properties are extracted from application context in our case while the webview’s constructor gets called.
  2. But what if the pool hands over the webviews to the activity, what happens then. So, our webview uses MutableContextWrapper,and any runtime access of context inside the webview after activity has acquired the view will route the calls to the activity context.

This approach is a midway choice b/w using activity context and application context, with some tradeoffs in the functionality while giving us the performance benefits.

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

--

--

Vishal Ratna
Microsoft Mobile Engineering

Senior Android Engineer @ Microsoft. Fascinated by the scale and performance-based software design. A seeker, a learner. Loves cracking algorithmic problems.