Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Memory leak in WebView

I have an activity using an xml layout where a WebView is embedded. I am not using the WebView in my activity code at all, all it does is sitting there in my xml layout and being visible.

Now, when I finish the activity, I find that my activity is not being cleared from memory. (I check via hprof dump). The activity is entirely cleared though if I remove the WebView from the xml layout.

I already tried a

webView.destroy();
webView = null;

in onDestroy() of my activity, but that doesn't help much.

In my hprof dump, my activity (named 'Browser') has the following remaining GC roots (after having called destroy() on it):

com.myapp.android.activity.browser.Browser
  - mContext of android.webkit.JWebCoreJavaBridge
    - sJavaBridge of android.webkit.BrowserFrame [Class]
  - mContext of android.webkit.PluginManager
    - mInstance of android.webkit.PluginManager [Class]  

I found that another developer has experienced similar thing, see the reply of Filipe Abrantes on: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

Indeed a very interesting post. Recently I had a very hard time troubleshooting a memory leak on my Android app. In the end it turned out that my xml layout included a WebView component that, even if not used, was preventing the memory from being g-collected after screen rotations/app restart… is this a bug of the current implementation, or is there something specific that one needs to do when using WebViews

Now, unfortunately there has been no reply on the blog or the mailing list about this question yet. Therefore I am wondering, is that a bug in the SDK (maybe similar to the MapView bug as reported http://code.google.com/p/android/issues/detail?id=2181) or how to get the activity entirely off the memory with a webview embedded?

like image 241
Mathias Conradt Avatar asked Jun 28 '10 07:06

Mathias Conradt


4 Answers

I conclude from above comments and further tests, that the problem is a bug in the SDK: when creating a WebView via XML layout, the activity is passed as the context for the WebView, not the application context. When finishing the activity, the WebView still keeps references to the activity, therefore the activity doesn't get removed from the memory. I filed a bug report for that , see the link in the comment above.

webView = new WebView(getApplicationContext());

Note that this workaround only works for certain use cases, i.e. if you just need to display html in a webview, without any href-links nor links to dialogs, etc. See the comments below.

like image 105
Mathias Conradt Avatar answered Oct 06 '22 21:10

Mathias Conradt


I have had some luck with this method:

Put a FrameLayout in your xml as a container, lets call it web_container. Then programmatically ad the WebView as mentioned above. onDestroy, remove it from the FrameLayout.

Say this is somewhere in your xml layout file e.g. layout/your_layout.xml

<FrameLayout
    android:id="@+id/web_container"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"/>

Then after you inflate the view, add the WebView instantiated with the application context to your FrameLayout. onDestroy, call the webview's destroy method and remove it from the view hierarchy or you will leak.

public class TestActivity extends Activity {
    private FrameLayout mWebContainer;
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.your_layout);

        mWebContainer = (FrameLayout) findViewById(R.id.web_container);
        mWebView = new WebView(getApplicationContext());
        mWebContainer.addView(mWebView);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mWebContainer.removeAllViews();
        mWebView.destroy();
    }
}

Also FrameLayout as well as the layout_width and layout_height were arbitrarily copied from an existing project where it works. I assume another ViewGroup would work and I am certain other layout dimensions will work.

This solution also works with RelativeLayout in place of FrameLayout.

like image 45
caller9 Avatar answered Oct 06 '22 20:10

caller9


Here's a subclass of WebView that uses the above hack to seamlessly avoid memory leaks:

package com.mycompany.view;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.webkit.WebViewClient;

/**
 * see http://stackoverflow.com/questions/3130654/memory-leak-in-webview and http://code.google.com/p/android/issues/detail?id=9375
 * Note that the bug does NOT appear to be fixed in android 2.2 as romain claims
 *
 * Also, you must call {@link #destroy()} from your activity's onDestroy method.
 */
public class NonLeakingWebView extends WebView {
    private static Field sConfigCallback;

    static {
        try {
            sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");
            sConfigCallback.setAccessible(true);
        } catch (Exception e) {
            // ignored
        }

    }


    public NonLeakingWebView(Context context) {
        super(context.getApplicationContext());
        setWebViewClient( new MyWebViewClient((Activity)context) );
    }

    public NonLeakingWebView(Context context, AttributeSet attrs) {
        super(context.getApplicationContext(), attrs);
        setWebViewClient(new MyWebViewClient((Activity)context));
    }

    public NonLeakingWebView(Context context, AttributeSet attrs, int defStyle) {
        super(context.getApplicationContext(), attrs, defStyle);
        setWebViewClient(new MyWebViewClient((Activity)context));
    }

    @Override
    public void destroy() {
        super.destroy();

        try {
            if( sConfigCallback!=null )
                sConfigCallback.set(null, null);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    protected static class MyWebViewClient extends WebViewClient {
        protected WeakReference<Activity> activityRef;

        public MyWebViewClient( Activity activity ) {
            this.activityRef = new WeakReference<Activity>(activity);
        }

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            try {
                final Activity activity = activityRef.get();
                if( activity!=null )
                    activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
            }catch( RuntimeException ignored ) {
                // ignore any url parsing exceptions
            }
            return true;
        }
    }
}

To use it, just replace WebView with NonLeakingWebView in your layouts

                    <com.mycompany.view.NonLeakingWebView
                            android:layout_width="fill_parent"
                            android:layout_height="wrap_content"
                            ...
                            />

Then make sure to call NonLeakingWebView.destroy() from your activity's onDestroy method.

Note that this webclient should handle the common cases, but it may not be as full-featured as a regular webclient. I haven't tested it for things like flash, for example.

like image 22
emmby Avatar answered Oct 06 '22 21:10

emmby


Based on user1668939's answer on this post (https://stackoverflow.com/a/12408703/1369016), this is how I fixed my WebView leak inside a fragment:

@Override
public void onDetach(){

    super.onDetach();

    webView.removeAllViews();
    webView.destroy();
}

The difference from user1668939's answer is that I have not used any placeholders. Just calling removeAllViews() on the WebvView reference itself did the trick.

## UPDATE ##

If you are like me and have WebViews inside several fragments (and you do not want to repeat the above code across all of your fragments), you can use reflection to solve it. Just make your Fragments extend this one:

public class FragmentWebViewLeakFree extends Fragment{

    @Override
    public void onDetach(){

        super.onDetach();

        try {
            Field fieldWebView = this.getClass().getDeclaredField("webView");
            fieldWebView.setAccessible(true);
            WebView webView = (WebView) fieldWebView.get(this);
            webView.removeAllViews();
            webView.destroy();

        }catch (NoSuchFieldException e) {
            e.printStackTrace();

        }catch (IllegalArgumentException e) {
            e.printStackTrace();

        }catch (IllegalAccessException e) {
            e.printStackTrace();

        }catch(Exception e){
            e.printStackTrace();
        }
    }
}

I am assuming you are calling your WebView field "webView" (and yes, your WebView reference must be a field unfortunately). I have not found another way to do it that would be independent from the name of the field (unless I loop through all the fields and check if each one is from a WebView class, which I do not want to do for performance issues).

like image 41
Bitcoin Cash - ADA enthusiast Avatar answered Oct 06 '22 19:10

Bitcoin Cash - ADA enthusiast