Витік пам'яті в WebView


75

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

Тепер, коли я закінчую діяльність, я виявляю, що моя діяльність не очищається з пам'яті. (Перевіряю через дамп hprof). Діяльність повністю очищена, хоча якщо я видалю WebView із макета xml.

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

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

1
Я просто тестував це, але не має значення.
Матіас Конрадт

1
Тим часом я подав звіт про помилку за адресою code.google.com/p/android/issues/detail?id=9375, але, можливо, хтось має для цього обхідне рішення; тоді, будь ласка, опублікуйте його.
Матіас Конрадт,

3
Гаразд. Ще один тест з невеликими змінами: коли я створюю WebView програмно і встановлюю в якості контексту "це" (діяльність), діяльність все одно залишається в пам'яті. Коли я використовую getApplicationContext (), це нормально, і дію get видаляється без збережених посилань.
Матіас Конрадт,

Відповіді:


57

З викладених вище коментарів та подальших тестів я роблю висновок, що проблема полягає в помилці SDK: при створенні WebView за допомогою XML-макета дія передається як контекст WebView, а не як контекст програми. Завершуючи діяльність, WebView все ще зберігає посилання на цю діяльність, тому активність не видаляється з пам'яті. Я подав звіт про помилку, див. Посилання в коментарі вище.

webView = new WebView(getApplicationContext());

Зверніть увагу, що це обхідне рішення працює лише для певних випадків використання, тобто якщо вам просто потрібно відобразити html у веб-перегляді, без будь-яких посилань href, посилань на діалоги тощо. Дивіться коментарі нижче.


1
ty для цього getApplicationContext () насправді працював на моєму витоку пам'яті під час створення WebView. Але коли я додаю веб-перегляд до іншої ViewGroup, витік пам’яті з’являється знову. Моя дика здогадка полягає в тому, що приймається батьківський baseContext. Чи є якась робота навколо цього? Я теж створюю батьківський за допомогою getApplicationContext () ... тому, мабуть, я не в
курсі

28
Зверніть увагу, що використання контексту програми означає, що ви не зможете натискати посилання у своєму веб-перегляді, оскільки це призведе до аварійного завершення: "Виклик startActivity () поза контекстом Activity вимагає позначки FLAG_ACTIVITY_NEW_TASK. Це насправді що ти хочеш?"
emmby

5
Крім того, щоразу, коли веб-перегляд намагається створити діалогове вікно (наприклад, запам’ятати пароль тощо), веб-перегляд вийде з ладу, оскільки очікує контексту діяльності.
markhiz

1
Він роздавлює додаток, коли webView хоче відобразити діалогове вікно, наприклад, запитуючи "Хочеш зберегти пароль", тоді він розчавить :(
Давид Дрозд,

2
ця помилка все ще активна у lilpops android 5.0. Тому ми повинні пам’ятати про те, що нам ще потрібно обійти цю проблему.
GFxJamal

35

Мені пощастило з цим методом:

Помістіть 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, зателефонуйте до методу знищення 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.


1
Це абсолютно спрацювало для мене. Дуже дякую! Єдиним покращенням, яке я зробив, було використання контексту активності замість контексту програми, який, як я очікую, позбавить мене від тих згаданих в інших місцях збоїв, які виникають, коли Flash або діалоги з’являються у веб-перегляді.
SilithCrowe

Шкода, що, здається, у мене не виходить .. sConfigCallback все ще там тримає посилання: /
Surya Wijaya Madjid

10

Ось підклас 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 вашої активності.

Зверніть увагу, що цей веб-клієнт повинен розглядати поширені випадки, але він може бути не таким повноцінним, як звичайний веб-клієнт. Я не тестував його для таких речей, як спалах, наприклад.


Знайшов лише одну проблему з таким способом реалізації: маючи WebView у DialogFragment, я отримав ContextThemeWrapper у конструкторі замість Activity ans так ClassCastException.
sandrstar

Якщо у вас є Flash-вміст у веб-перегляді, ви отримаєте ClassCastException від com.adobe.flashplayer.FlashPaintSurface ... принаймні, на Kindle Fire.
Крістофер Перрі,

Оновлено 1 лютого 2013 р. Для усунення додаткових витоків у BrowserFrame.sConfigCallback
emmby 02

Для мене пам'ять все ще не вивільняється
Тарун Так

переконайтеся, що ви телефонуєте NonLeakingWebView.destroy () у своїй діяльності onDestroy ()
emmby

8

На основі відповіді користувача user1668939 на цю публікацію ( https://stackoverflow.com/a/12408703/1369016 ), ось як я виправив витік WebView всередині фрагмента:

@Override
public void onDetach(){

    super.onDetach();

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

Відмінність від відповіді користувача1668939 полягає в тому, що я не використовував жодних заповнювачів. Просто виклик 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, чого я не хочу робити з проблемами продуктивності).


Насправді я знайшов це найкраще рішення (принаймні для мене). Я використовую Xamarin Android і втрачав ~ 1 МБ кожного разу, коли закривали діяльність із WebView.
Tobias81,

3

Прочитавши 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();
}

Немає жодної політики для порушення (щодо прихованих API чи чогось іншого), ви просто не маєте жодних гарантій того, що основна система не зміниться під час оновлення. Потенційно ви можете обернути цей код у перевірці рівня API і дозволити його для нових версій SDK лише після тестування з ними.
powerj1984

Я думаю, що це хороший спосіб виправити це, коли перша створена дія з екземпляром WebView буде знищена. Я дуже страждав від цієї проблеми. Але це МОЖЕ створити проблеми, коли ви намагаєтеся запустити іншу діяльність за допомогою WebView.
qiuping345

3

Я виправив проблему витоку пам'яті, яка викликає розчарування 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

B створюється за допомогою вбудованого веб-перегляду.

Коли натиснуто backKey для дії B, onDestroy викликається:

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

і це вбиває поточний процес, тобто com.processkill.p3

і забирає веб-перегляд, на який посилається

ПРИМІТКА. Будьте особливо обережні під час використання цієї команди kill. (не рекомендується із зрозумілих причин). Не застосовуйте будь-який статичний метод у діяльності (у цьому випадку - діяльність B). Не використовуйте жодних посилань на цю діяльність будь-якої іншої (оскільки вона буде вбита і більше не доступна).


Як можна обробляти навігацію в веб-перегляді за допомогою цього методу (тобто переадресації, входу, посилань, форм тощо)?
Мегакореш,

Слід також зазначити, що дії, що виконуються в окремих процесах, не можуть одночасно отримати доступ до одних і тих самих статичних значень, спільних налаштувань або писати в один і той же db sqlite.
CamHart

2

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


2

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

Коментар WebView знищення () - "Цей метод слід викликати після того, як цей WebView буде видалений із системи подання."


Це те, що вирішило проблему для мене. Додайте його до FrameLayout, а потім видаліть веб-перегляд із цього FrameLayout в onDestroy () діяльності.
Carl B

Працював і для мене - див. Alibabacloud.com/forum/read-520 для детального аналізу
Paul W

1

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

Це можна виправити за допомогою WebViewналаштувань " setSavePassword(false)для випадку" запам'ятати пароль ".


Інший випадок (відтворений на Galaxy Nexus, HTC Hero, але не на Galaxy Ace): вибір зі спадного списку (цей також запускає WebView для відображення діалогового вікна).
Денис Гладкий,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.