Утечка памяти в WebView

У меня есть активность с использованием xml-макета, где встроен WebView. Я вообще не использую WebView в своем коде активности, все, что он делает, сидит там в моем макете xml и видимо.

Теперь, когда я закончу действие, я обнаружил, что моя активность не очищается от памяти. (Я проверяю с помощью hprof дампа). Активность полностью очищается, хотя если я удалю WebView из макета xml.

Я уже пробовал

webView.destroy();
webView = null;

в onDestroy() моей активности, но это мало помогает.

В моем дампе hprof моя активность (с именем "Браузер" ) имеет следующие оставшиеся корни GC (после того, как она набрала destroy()):

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]  

Я обнаружил, что другой разработчик испытал подобную вещь, см. ответ Филипе Абранта: http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/

Действительно, очень интересный пост. Недавно мне было очень тяжело устранение утечки памяти на моем Приложение для Android. В итоге оказалось что мой xml-макет включает WebView компонент, который, даже если не использовался, был предотвращение памяти g-собранный после вращения экрана/приложения перезагрузка... это ошибка текущего реализации, или есть что-то которые нужно делать, когда с помощью WebViews

Теперь, к сожалению, в блоге или списке рассылки по этому вопросу еще не было ответа. Поэтому мне интересно, это ошибка в SDK (возможно, похожая на ошибку MapView, как сообщалось http://code.google.com/p/android/issues/detail?id=2181) или как полностью исключить активность из памяти с помощью встроенного webview?

Ответ 1

Из вышеприведенных комментариев и дальнейших тестов я заключаю, что проблема является ошибкой в ​​SDK: при создании WebView через XML-макет деятельность передается как контекст для WebView, а не контекста приложения. По завершении операции WebView сохраняет ссылки на активность, поэтому активность не удаляется из памяти. Я написал отчет об ошибке, см. Ссылку в комментарии выше.

webView = new WebView(getApplicationContext());

Обратите внимание, что это обходное решение работает только в определенных случаях использования, т.е. вам просто нужно отображать html в веб-просмотре без каких-либо ссылок href и ссылок на диалоги и т.д. См. комментарии ниже.

Ответ 2

Мне повезло с этим методом:

Поместите FrameLayout в свой xml в качестве контейнера, позвоните ему в web_container. Затем программно объявите WebView, как указано выше. onDestroy, удалите его из FrameLayout.

Скажите, что это где-то в вашем файле макета xml, например. макет /your _layout.xml

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

Затем после того, как вы надуте представление, добавьте WebView, созданный с помощью контекста приложения, в ваш FrameLayout. onDestroy, вызовите метод уничтожения webview и удалите его из иерархии представлений или вы пропустите.

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();
    }
}

Также FrameLayout, а также layout_width и layout_height были произвольно скопированы из существующего проекта, где он работает. Я предполагаю, что будет работать другая ViewGroup, и я уверен, что другие макеты будут работать.

Это решение также работает с RelativeLayout вместо FrameLayout.

Ответ 3

Здесь находится подкласс WebView, который использует вышеупомянутый взломать, чтобы легко избежать утечек памяти:

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 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;
        }
    }
}

Чтобы использовать его, просто замените WebView на NonLeakingWebView в своих макетах

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

Затем обязательно вызовите NonLeakingWebView.destroy() из вашей функции onDestroy.

Обратите внимание, что этот веб-клиент должен обрабатывать общие случаи, но он может быть не таким полнофункциональным, как обычный веб-клиент. Например, я не тестировал его для таких вещей, как flash.

Ответ 4

На основании user1668939 ответ на этот пост (fooobar.com/questions/80153/...), я зафиксировал утечку WebView внутри фрагмента:

@Override
public void onDetach(){

    super.onDetach();

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

Отличие от user1668939 заключается в том, что я не использовал никаких заполнителей. Просто вызов removeAllViews() в самой WebvView-ссылке сделал трюк.

## UPDATE ##

Если вы похожи на меня и имеете WebView внутри нескольких фрагментов (и вы не хотите повторять вышеуказанный код во всех своих фрагментах), вы можете использовать отражение для его решения. Просто сделайте свои фрагменты следующими:

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();
        }
    }
}

Я предполагаю, что вы вызываете свое поле WebView "webView" (и да, ваша ссылка WebView должна быть полем, к сожалению). Я не нашел другого способа сделать это, который был бы независим от имени поля (если я не прокручу все поля и не проверю, есть ли у каждого из класса WebView, что я не хочу делать для проблем с производительностью).

Ответ 5

После прочтения http://code.google.com/p/android/issues/detail?id=9375, возможно, мы могли бы использовать отражение, чтобы установить ConfigCallback.mWindowManager в null на Activity.onDestroy и восстановить его на Activity. OnCreate. Я не уверен, но если он требует некоторых разрешений или нарушает какую-либо политику. Это зависит от реализации android.webkit, и она может не работать в более поздних версиях 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) {
    }
}

Вызов вышеуказанного метода в действии

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

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}

Ответ 6

Устранена проблема с утечкой памяти, которая вызывает разочарование в Webview:

(надеюсь, это может помочь многим)

Основы

  • Чтобы создать веб-просмотр, необходима ссылка (например, действие).
  • Чтобы убить процесс:

android.os.Process.killProcess(android.os.Process.myPid()); можно вызвать.

Точка поворота:

По умолчанию все действия выполняются в одном приложении в одном приложении. (процесс определяется именем пакета). Но:

В одном приложении могут быть созданы различные процессы.

Решение: Если для активности создается другой процесс, его контекст можно использовать для создания веб-представления. И когда этот процесс убит, все компоненты, имеющие ссылки на эту активность (веб-просмотр в этом случае), погибают, а основная желательная часть:

GC называется принудительно собирать этот мусор (webview).

Код для справки: (один простой случай)

Всего два действия: скажите A и B

Файл манифеста:

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

Запустите A, затем B

A > B

B создается с встроенным webview.

Когда backKey нажата на активность B, onDestroy вызывается:

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

и это убивает текущий процесс, то есть com.processkill.p3

и удаляет веб-просмотр, на который он ссылается

ПРИМЕЧАНИЕ. Будьте особенно осторожны при использовании этой команды kill. (не рекомендуется по очевидным причинам). Не применяйте какой-либо статический метод в активности (в этом случае активность B). Не используйте ссылки на эту деятельность из любого другого (поскольку он будет убит и больше не доступен).

Ответ 7

Вы можете попытаться поместить веб-активность в отдельный процесс и выйти, когда действие будет уничтожено, если обработка многопроцессорности не представляет для вас большого труда.

Ответ 8

Возникла проблема с обходным контекстом приложения: сбой, когда WebView пытается показать любой диалог. Например, диалог "запомнить пароль" в форме ввода логина/пароля (любые другие случаи?).

Он может быть исправлен с помощью WebView настроек 'setSavePassword(false) для случая "запомнить пароль".

Ответ 9

Вам нужно удалить WebView из родительского представления перед вызовом WebView.destroy().

Комментарий WebView destroy() - "Этот метод следует вызывать после того, как этот WebView был удален из системы просмотра".