Утечка памяти в 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, который, даже если он не использовался, мешает собирать память после скрининга / перезапуска приложения … это ошибка текущей реализации, или есть что-то Которые нужно делать при использовании WebViews

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

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

 webView = new WebView(getApplicationContext()); 

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

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

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

Вот подкласс 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 вашей активности.

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

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

 @Override public void onDetach(){ super.onDetach(); webView.removeAllViews(); webView.destroy(); } 

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

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

Я исправил проблему утечки памяти в результате разочарования 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). Не используйте ссылки на эту деятельность из других (поскольку она будет убита и больше не будет доступна).

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

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

Его можно было установить с WebView параметров WebView « setSavePassword(false) для случая« запомнить пароль ».

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

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