Утечка памяти в WebView - PullRequest
72 голосов
/ 28 июня 2010

У меня есть действие, использующее макет 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 / избежать утечек памяти на андроиде /

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

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

Ответы [ 9 ]

53 голосов
/ 28 июня 2010

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

webView = new WebView(getApplicationContext());

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

32 голосов
/ 04 ноября 2011

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

Поместите 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.

10 голосов
/ 21 января 2012

Вот подкласс 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 /2836691/utechka-pamyati-v-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 вашей деятельности.

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

7 голосов
/ 16 октября 2013

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

@Override
public void onDetach(){

    super.onDetach();

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

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

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

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

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

3 голосов
/ 06 февраля 2015

Я исправил проблему с утечкой памяти из-за разочарования в 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 создается со встроенным веб-представлением.

Когда нажата кнопка BackKey для действия B, вызывается onDestroy:

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

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

и забирает веб-ссылку, на которую ссылаются

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

3 голосов
/ 29 ноября 2012

После прочтения 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();
}
2 голосов
/ 17 мая 2016

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

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

2 голосов
/ 11 марта 2013

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

1 голос
/ 27 сентября 2012

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...