77

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?

Mathias Conradt
  • 28,420
  • 21
  • 138
  • 192
  • Does this occur if you dynamically create the WebView? – ktingle Jun 28 '10 at 08:40
  • 1
    I just tested that, but doesn't make a difference. – Mathias Conradt Jun 28 '10 at 10:55
  • 1
    Meanwhile I filed a bug report at http://code.google.com/p/android/issues/detail?id=9375 but maybe somebody has a workaround for it; then please post it. – Mathias Conradt Jun 28 '10 at 11:03
  • 3
    Ok. Another test with little modification: when I create the WebView programatically and set 'this' (the activity) as the context, the activity would still remain in memory. When I use getApplicationContext() though, it's ok and the activity get's removed without any kept references. – Mathias Conradt Jun 28 '10 at 12:54

9 Answers9

57

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.

Mathias Conradt
  • 28,420
  • 21
  • 138
  • 192
  • 1
    ty for that getApplicationContext() actually worked on my memory leak when creating the WebView. But when i add the webview to another ViewGroup the memory Leak appears again. My wild guess is that adopts the parent's baseContext. Is there anyworkAround to that? I create the parent with getApplicationContext() too... so i guess i'm out of theories – weakwire Sep 20 '11 at 01:34
  • 28
    Note that using the application context means that you won't be able to click on links in your webview, since doing so will result in a crash: "Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?" – emmby Jan 20 '12 at 23:47
  • @emmby Thanks for the hint, wasn't thinking of that, as I just needed the webview to display more complex html (beyond what you can do with a TextView), but no links in it. Therefore, I didn't come across the issue you mentioned, but thanks for pointing it out again. +1 – Mathias Conradt Jun 18 '12 at 15:27
  • 5
    Also, any time the webview attempts to create a dialog (e.g., to remember a password, etc), the webview will crash as it expects an activity context. – markshiz Aug 30 '12 at 18:13
  • 1
    It crush app when webView want to display dialog for example asking "Do you want save passwaord" then it will crush :( – Gelldur Dec 30 '12 at 20:52
  • Sadly, this fix isn't sufficient due to an additional leak in BrowserFrame.sConfigCallback. See my solution before for a workaround. – emmby Feb 02 '13 at 02:42
  • The "public void onDetach()" answer below seems to work a lot better (at least for me) – Tobias81 Oct 10 '14 at 12:42
  • 2
    this bug is still active in android 5.0 lilpops. So we need to keep that in mind that we still need to work around for this bug issue. – Soma Hesk May 18 '15 at 09:40
36

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.

caller9
  • 2,207
  • 1
  • 18
  • 11
  • 1
    This absolutely worked for me. Thanks so much! The only improvement I made was to use the activity context instead of the application context, which I expect will save me from those elsewhere-mentioned crashes that occur when Flash or dialogs appear in the webview. – SilithCrowe Nov 05 '12 at 21:11
  • Too bad it doesn't seem to work for me.. The sConfigCallback is still there holding the reference :/ – Surya Wijaya Madjid Jan 10 '13 at 12:19
10

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.

emmby
  • 99,783
  • 65
  • 191
  • 249
  • Found only one issue with that way of implementation: having WebView in DialogFragment I've received ContextThemeWrapper in the constructor instead of Activity ans so ClassCastException. – sandrstar Mar 22 '12 at 10:29
  • If you have Flash content in the Webview you'll get a ClassCastException from com.adobe.flashplayer.FlashPaintSurface... at least, on Kindle Fire. – Christopher Perry Aug 16 '12 at 18:27
  • Updated Feb 1 2013 to work around the additional leak in BrowserFrame.sConfigCallback – emmby Feb 02 '13 at 02:40
  • For me memory is still not releasing – Tarun Tak Feb 08 '13 at 12:17
  • make sure you're calling NonLeakingWebView.destroy() in your activity's onDestroy() – emmby Feb 08 '13 at 22:54
  • This is not a very good solution, I have to say. Once you destroy one, BrowserFrame.sConfigCallback is null, and open another activity with NonLeakingWebView, then a new ConfigCallback is created and referenced by a static ArrayList in ViewRoot or ViewRootImpl, and another Activity would be leaked. Please check the code of Android 2.3.5r1: Line 209-215 [android.webkit.BrowserFrame.ConfigCallback] Line 298-301 [android.view.ViewRoot.addConfigCallback] – qiuping345 Nov 19 '13 at 07:51
  • On my phone I got exception every time in static block code in line: `sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");` but every thing works good so could you explain it ? `java.lang.ClassNotFoundException: android.webkit.BrowserFrame` – mac229 Nov 16 '15 at 19:14
9

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).

Community
  • 1
  • 1
  • Actually I found this to be the best solution (atleast for me). I'm using Xamarin Android and was losing ~1 MB each time an Activity with a WebView was closed. – Tobias81 Oct 10 '14 at 12:41
  • I don't understand the reflection here. Why would a class that you've made need to be reflected to find this "webView" field? – android developer Oct 13 '22 at 21:12
3

After reading http://code.google.com/p/android/issues/detail?id=9375, maybe we could use reflection to set ConfigCallback.mWindowManager to null on Activity.onDestroy and restore it on Activity.onCreate. I'm unsure though if it requires some permissions or violates any policy. This is dependent on android.webkit implementation and it may fail on later versions of Android.

public void setConfigCallback(WindowManager windowManager) {
    try {
        Field field = WebView.class.getDeclaredField("mWebViewCore");
        field = field.getType().getDeclaredField("mBrowserFrame");
        field = field.getType().getDeclaredField("sConfigCallback");
        field.setAccessible(true);
        Object configCallback = field.get(null);

        if (null == configCallback) {
            return;
        }

        field = field.getType().getDeclaredField("mWindowManager");
        field.setAccessible(true);
        field.set(configCallback, windowManager);
    } catch(Exception e) {
    }
}

Calling the above method in Activity

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setConfigCallback((WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
}

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}
  • There is no policy to violate (regarding hidden APIs or whatnot), you just don't have any guarantees that the underlying system wont change in an update. Potentially you could wrap this code in an API level check and only allow it for new SDK revisions after testing with them. – powerj1984 May 20 '13 at 21:24
  • I think this is a good way to fix it when the first created Activity with WebView instance is going to be destroyed. I suffered a lot from this problem. But it MAY make troubles when you try to start another Activity with WebView. – qiuping345 Nov 19 '13 at 07:34
3

I fixed memory leak issue of frustrating Webview like this:

(I hope this may help many)

Basics:

  • To create a webview, a reference (say an activity) is needed.
  • To kill a process:

android.os.Process.killProcess(android.os.Process.myPid()); can be called.

Turning point:

By default, all activities run in same process in one application. (the process is defined by package name). But:

Different processes can be created within same application.

Solution: If a different process is created for an activity, its context can be used to create a webview. And when this process is killed, all components having references to this activity (webview in this case) are killed and the main desirable part is :

GC is called forcefully to collect this garbage (webview).

Code for help: (one simple case)

Total two activities: say A & B

Manifest file:

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:process="com.processkill.p1" // can be given any name 
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.processkill.A"
            android:process="com.processkill.p2"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity
            android:name="com.processkill.B"
            android:process="com.processkill.p3"
            android:label="@string/app_name" >
        </activity>
    </application>

Start A then B

A > B

B is created with webview embedded.

When backKey is pressed on activity B, onDestroy is called:

@Override
    public void onDestroy() {
        android.os.Process.killProcess(android.os.Process.myPid());
        super.onDestroy();
    }

and this kills the current process i.e. com.processkill.p3

and takes away the webview referenced to it

NOTE: Take extra care while using this kill command. (not recommended due to obvious reasons). Don't implement any static method in the activity (activity B in this case). Don't use any reference to this activity from any other (as it will be killed and no longer available).

vipulfb
  • 63
  • 1
  • 7
  • How would one handle navigation within the webview using this method (i.e. redirects, login, links, forms etc.)? – Megakoresh Aug 17 '16 at 09:32
  • You should also point out that activities running in separate processes cannot access the same static values, shared preferences, or write to the same sqlite db at the same time. – CamHart Jul 12 '20 at 05:14
3

You need to remove the WebView from the parent view before calling WebView.destroy().

WebView's destroy() comment - "This method should be called after this WebView has been removed from the view system."

qjinee
  • 133
  • 1
  • 1
  • 6
  • This is what solved the problem for me. Add it to a FrameLayout, and then remove the web view from that FrameLayout in onDestroy() of the activity. – Carl B Jan 30 '17 at 18:04
  • Worked for me as well - see https://www.alibabacloud.com/forum/read-520 for the detailed analysis – Paul W Sep 23 '18 at 14:29
2

You can try putting the web activity in a seperate process and exit when the activity is destroyed, if multiprocess handling is not a big effort to you.

Fanny
  • 181
  • 1
  • 1
  • 6
1

There is an issue with "app context" workaround: crash when WebView tries to show any dialog. For example "remember the password" dialog on login/pass forms submition (any other cases?).

It could be fixed with WebView settings' setSavePassword(false) for the "remember the password" case.

biegleux
  • 13,179
  • 11
  • 45
  • 52
Denis Gladkiy
  • 2,084
  • 1
  • 26
  • 40
  • Another case (reproduced on Galaxy Nexus, HTC Hero, but not on Galaxy Ace): choosing from the drop-down list (this one also triggers WebView to show a dialog). – Denis Gladkiy Sep 27 '12 at 05:36