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