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

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

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

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

webView.destroy();
webView = null;

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

В моем дампе hprof моя активность (названная 'Browser') имеет следующие оставшиеся корни 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, который, даже если он не использовался, предотвращал сбор памяти после поворота экрана / перезапуска приложения ... это ошибка текущей реализации или есть что-то конкретно, что нужно делать при использовании WebViews

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


person Mathias Conradt    schedule 28.06.2010    source источник
comment
Это происходит, если вы динамически создаете WebView?   -  person ktingle    schedule 28.06.2010
comment
Я только что это проверил, но без разницы.   -  person Mathias Conradt    schedule 28.06.2010
comment
Тем временем я отправил отчет об ошибке на code.google.com/p/android / issues / detail? id = 9375, но, может быть, у кого-нибудь есть обходной путь; тогда, пожалуйста, опубликуйте это.   -  person Mathias Conradt    schedule 28.06.2010
comment
Ok. Еще один тест с небольшими изменениями: когда я создаю WebView программно и устанавливаю this (действие) в качестве контекста, действие все равно остается в памяти. Когда я использую getApplicationContext (), все в порядке, и действие удаляется без каких-либо сохраненных ссылок.   -  person Mathias Conradt    schedule 28.06.2010


Ответы (9)


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

webView = new WebView(getApplicationContext());

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

person Mathias Conradt    schedule 28.06.2010
comment
ty для этого getApplicationContext () действительно работал с моей утечкой памяти при создании WebView. Но когда я добавляю веб-просмотр в другую ViewGroup, снова появляется утечка памяти. Мое безумное предположение заключается в том, что он принимает родительский baseContext. Есть ли какая-нибудь работа по этому поводу? Я также создаю родительский элемент с помощью getApplicationContext () ... так что я думаю, что у меня нет теорий - person weakwire; 20.09.2011
comment
Обратите внимание, что использование контекста приложения означает, что вы не сможете нажимать ссылки в своем веб-просмотре, поскольку это приведет к сбою: для вызова startActivity () из-за пределов контекста действия требуется флаг FLAG_ACTIVITY_NEW_TASK. Вы действительно этого хотите? - person emmby; 21.01.2012
comment
@emmby Спасибо за подсказку, я не думал об этом, так как мне просто нужен веб-просмотр для отображения более сложного html (помимо того, что вы можете сделать с TextView), но без ссылок в нем. Поэтому я не столкнулся с проблемой, о которой вы упомянули, но спасибо, что указали на нее еще раз. +1 - person Mathias Conradt; 18.06.2012
comment
Кроме того, каждый раз, когда веб-просмотр пытается создать диалог (например, для запоминания пароля и т. Д.), Веб-просмотр будет аварийно завершен, поскольку он ожидает контекста активности. - person markshiz; 30.08.2012
comment
Он разрушает приложение, когда webView хочет отобразить диалоговое окно, например, спрашивая, хотите ли вы сохранить пароль, тогда оно будет раздавлено :( - person Dawid Drozd; 31.12.2012
comment
К сожалению, этого исправления недостаточно из-за дополнительной утечки в BrowserFrame.sConfigCallback. См. Мое решение раньше для обходного пути. - person emmby; 02.02.2013
comment
Ответ public void onDetach () ниже, кажется, работает намного лучше (по крайней мере, для меня) - person Tobias81; 10.10.2014
comment
эта ошибка все еще активна в lilpops Android 5.0. Поэтому нам нужно иметь в виду, что нам все еще нужно работать над этой проблемой. - person GFxJamal; 18.05.2015

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

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

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

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

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

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.

person caller9    schedule 04.11.2011
comment
Это абсолютно сработало для меня. Огромное спасибо! Единственное улучшение, которое я сделал, - это использование контекста активности вместо контекста приложения, что, как я ожидаю, избавит меня от упомянутых в другом месте сбоев, которые происходят, когда Flash или диалоговые окна появляются в веб-просмотре. - person SilithCrowe; 06.11.2012
comment
Жаль, что это не работает для меня .. sConfigCallback все еще там, содержащий ссылку: / - person Surya Wijaya Madjid; 10.01.2013

Вот подкласс 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'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;
        }
    }
}

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

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

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

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

person emmby    schedule 20.01.2012
comment
Обнаружена только одна проблема с таким способом реализации: имея WebView в DialogFragment, я получил ContextThemeWrapper в конструкторе вместо Activity и ClassCastException. - person sandrstar; 22.03.2012
comment
Если у вас есть Flash-контент в Webview, вы получите исключение ClassCastException от com.adobe.flashplayer.FlashPaintSurface ... по крайней мере, в Kindle Fire. - person Christopher Perry; 16.08.2012
comment
Обновлено 1 февраля 2013 г., чтобы обойти дополнительную утечку в BrowserFrame.sConfigCallback. - person emmby; 02.02.2013
comment
Для меня память все еще не освобождает - person Tarun Tak; 08.02.2013
comment
убедитесь, что вы вызываете NonLeakingWebView.destroy () в onDestroy () вашей активности - person emmby; 09.02.2013
comment
Я должен сказать, что это не очень хорошее решение. После того, как вы уничтожите один, BrowserFrame.sConfigCallback имеет значение null и откроете другое действие с помощью NonLeakingWebView, тогда будет создан новый ConfigCallback, на который будет ссылаться статический список ArrayList в ViewRoot или ViewRootImpl, и произойдет утечка другого действия. Пожалуйста, проверьте код Android 2.3.5r1: Строка 209-215 [android.webkit.BrowserFrame.ConfigCallback] Строка 298-301 [android.view.ViewRoot.addConfigCallback] - person qiuping345; 19.11.2013
comment
На моем телефоне я получал исключение каждый раз в статическом коде блока в строке: sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");, но все работает хорошо, не могли бы вы это объяснить? java.lang.ClassNotFoundException: android.webkit.BrowserFrame - person mac229; 16.11.2015

Основываясь на ответе пользователя 1668939 на этот пост (https://stackoverflow.com/a/12408703/1369016), вот как Я исправил утечку WebView внутри фрагмента:

@Override
public void onDetach(){

    super.onDetach();

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

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

## ОБНОВЛЕНИЕ ##

Если вы похожи на меня и имеете 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, что я не хочу делать из-за проблем с производительностью).

person Bitcoin Cash - ADA enthusiast    schedule 15.10.2013
comment
На самом деле я нашел это лучшим решением (по крайней мере для меня). Я использую Xamarin Android и теряю ~ 1 МБ каждый раз, когда действие с WebView закрывается. - person Tobias81; 10.10.2014

После прочтения 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) {
    }
}

Вызов вышеуказанного метода в Activity

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

public void onDestroy() {
    setConfigCallback(null);
    super.onDestroy();
}
person raijintatsu    schedule 29.11.2012
comment
Нет никакой политики, которую можно нарушать (в отношении скрытых API-интерфейсов или чего-то еще), у вас просто нет никаких гарантий, что базовая система не изменится в обновлении. Потенциально вы можете обернуть этот код проверкой уровня API и разрешить его только для новых версий SDK после тестирования с ними. - person powerj1984; 21.05.2013
comment
Я думаю, что это хороший способ исправить это, когда первый созданный экземпляр Activity с WebView будет уничтожен. Я очень страдал от этой проблемы. Но это МОЖЕТ создать проблемы, когда вы попытаетесь запустить другое действие с помощью WebView. - person qiuping345; 19.11.2013

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

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

Основы:

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

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

Переломный момент:

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

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

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

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

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

Всего два действия: скажем 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 создается со встроенным веб-просмотром.

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

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

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

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

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

person vipulfb    schedule 06.02.2015
comment
Как с помощью этого метода обрабатывать навигацию в веб-просмотре (т.е. перенаправления, вход в систему, ссылки, формы и т. Д.)? - person Megakoresh; 17.08.2016
comment
Вы также должны указать, что действия, выполняемые в отдельных процессах, не могут одновременно обращаться к одним и тем же статическим значениям, общим настройкам или записывать в одну и ту же базу данных sqlite. - person CamHart; 12.07.2020

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

person Fanny    schedule 11.03.2013

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

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

person qjinee    schedule 17.05.2016
comment
Это то, что решило для меня проблему. Добавьте его в FrameLayout, а затем удалите веб-представление из этого FrameLayout в onDestroy () действия. - person Carl B; 30.01.2017
comment
Мне тоже помогло - см. alibabacloud.com/forum/read-520, подробный анализ - person Paul W; 23.09.2018

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

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

person Denis Gladkiy    schedule 27.09.2012
comment
Другой случай (воспроизведенный на Galaxy Nexus, HTC Hero, но не на Galaxy Ace): выбор из раскрывающегося списка (в этом случае также запускается WebView для отображения диалогового окна). - person Denis Gladkiy; 27.09.2012