2010-06-28 8 views
69

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

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

+0

Это происходит, если вы динамически создаете WebView? – ktingle

+1

Я только что проверил это, но не имеет значения. –

+1

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

ответ

51

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

webView = new WebView(getApplicationContext()); 

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

+1

ty для этого getApplicationContext() фактически работал над утечкой памяти при создании WebView. Но когда я добавляю веб-просмотр в другую ViewGroup, снова появляется сообщение об утечке памяти. Моя дикая догадка заключается в том, что используется baseContext родителя. Есть ли какая-нибудь работа? Я также создаю родительский элемент с getApplicationContext() ... поэтому я думаю, что я не в теории – weakwire

+25

Обратите внимание, что использование контекста приложения означает, что вы не сможете щелкнуть ссылки в своем веб-обзоре, так как это приведет к авария: «Вызов startActivity() извне контекста Activity требует флаг FLAG_ACTIVITY_NEW_TASK. Это действительно то, что вы хотите?» – emmby

+0

@emmby Спасибо за подсказку, не думал об этом, так как мне просто понадобилось веб-представление для отображения более сложного html (помимо того, что вы можете делать с TextView), но в нем нет ссылок. Поэтому я не столкнулся с проблемой, о которой вы говорили, но спасибо за то, что она снова указала. +1 –

30

Я имел некоторые удачи с этим методом:

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

+1

Это абсолютно сработало для меня. Большое спасибо! Единственное усовершенствование, которое я сделал, это использовать контекст активности вместо контекста приложения, который, как я ожидаю, спасет меня от тех других упоминаемых сбоев, которые возникают, когда Вспышка или диалоги появляются в веб-просмотре. – SilithCrowe

+0

+1 Это похоже на более подходящее обходное решение для меня. –

+0

Жаль, что это не Мне кажется, что работает для меня .. В sConfigCallback все еще есть ссылка:/ –

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

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

+0

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

+0

Если у вас есть Flash-контент в Webview, вы получите ClassCastException из com.adobe.flashplayer.FlashPaintSurface ... по крайней мере, на Kindle Fire. –

+0

Обновлено 1 февраля 2013 г., чтобы обойти дополнительную утечку в BrowserFrame.sConfigCallback – emmby

1

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

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

+0

Другой случай (воспроизведенный на Galaxy Nexus, HTC Hero, но не на Galaxy Ace): выбирая из раскрывающегося списка (этот также вызывает WebView для отображения диалога) , –

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) { 
    } 
} 

Вызов выше метода в деятельности

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

public void onDestroy() { 
    setConfigCallback(null); 
    super.onDestroy(); 
} 
+0

Кто-нибудь проверил это полностью и может подтвердить, что он работает надежно? – jenzz

+0

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

+0

Я думаю, что это хороший способ исправить это, когда первый созданный Activity с экземпляром WebView будет уничтожен. Я сильно пострадал от этой проблемы. Но он МОЖЕТ создавать проблемы, когда вы пытаетесь запустить другое мероприятие с помощью WebView. – qiuping345

2

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

7

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

+0

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

3

Я исправил проблему утечки памяти неудовлетворенности и WebView, как это:

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

Основы:

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

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

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

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

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

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

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

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

Всего два вида деятельности: замолвите & Аргументы 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> 

Start 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

и забирает WebView, на который ссылается на него

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

+0

Как можно обрабатывать навигацию в веб-просмотре с помощью этого метода (т. Е. Перенаправления, логин, ссылки, формы и т. Д.)? – Megakoresh

1

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

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

+0

Это то, что решило проблему для меня. Добавьте его в FrameLayout, а затем удалите веб-представление из этого FrameLayout в onDestroy() активности. –