• 【Android 性能优化:内存篇】——WebView 内存泄露治理


    背景:笔者在公司项目中优化内存泄露时发现WebView 相关的内存泄露问题非常经典,一个 Fragment 页面使用的 WebView 有多条泄露路径,故记录下。

    Fragment、Activity 使用WebView不释放

    项目中一个Fragment 使用 Webview,在 Fragment onDestroyView 时候却没有释放,释放 WebView 还不简单嘛,于是笔者在 Fragment 的 onDestroyView 补充了如下代码:

    1. if (webView != null) {
    2. ViewGroup parent = (ViewGroup) webView.getParent();
    3. if (parent != null) {
    4. parent.removeView(webView);
    5. }
    6. webView.destroy();
    7. webview = null;
    8. }

    然而,这样其实释放不全,还是抓到其他的泄露路径

    如图GC 引用链:AwContents->WebVIew->View.LinsenerInfo->WebViewFragment

    原因是使用 WebView的时候,注册了OnFocusChangeListener

    1. webView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
    2. @Override
    3. public void onFocusChange(View v, boolean hasFocus) {
    4. //省略
    5. }
    6. });

    因此,释放 WebView的时候,还需要把注册的一些Listener 释放

    WebView 释放不全

    上面介绍了释放 WebView 资源的时候释放不全的例子,那么怎样才能将用到的WebView 资源释放完全呢?

    笔者封装了一个接口如下:

    1. public void destroyWebView(WebView webView) {
    2. try {
    3. if (webView != null) {
    4. ViewGroup parent = (ViewGroup) webView.getParent();
    5. if (parent != null) {
    6. parent.removeView(webView);
    7. }
    8. webView.setOnTouchListener(null);
    9. webView.setOnKeyListener(null);
    10. webView.setOnFocusChangeListener(null);
    11. webView.setWebChromeClient(null);
    12. webView.setWebViewClient(null);
    13. webView.loadUrl("about:blank");
    14. webView.onPause();
    15. webView.removeAllViews();
    16. webView.destroyDrawingCache();
    17. webView.destroy();
    18. webView = null;
    19. }
    20. } catch (Throwable e) {
    21. e.printStackTrace();
    22. }
    23. }

    这样释放真的释放完全了?如果你使用的WebView 还注册了其他的Listener,记得也需要释放

    网上,还有说需要调用

    1. webView.pauseTimers();
    2. webView.clearHistory();

    上面的接口慎用,因为它们是对全局生效的,不只当前WebView!

    按上面两个步骤解决完,笔者以为不会再发生泄漏,谁知道还是抓到第三条泄露路径!!

    GC 引用链:AwContents->BannerView->Banner->CardView->Container->AdView->匿名内部类AdListener->WebViewFragment

    匿名内部类导致 WebView泄露

    按上面描述的引用链,匿名内部类隐式持有外部类 Fragment 的引用,而这个匿名内部类AdShowListener 刚好是 AdView 持有的, AdView 本质上是一个 WebView.

    解法很常规:把匿名内部类改为静态内部类,然后静态内部类里使用的 Fragment 改为弱引用,并且 Fragment 销毁的时候,AdShowListener 置空。

    到此,笔者以为不会再发生内存泄露了,怎知,还是抓到了,这次抓的是包裹 Fragment 的Activity 作为 Context 被 webview 持有

    意不意外,惊不惊喜?

    GC 引用链:AwContents->WebView->WebViewActivity, WebViewActivity 作为 Conext 被 WebView 持有

    因为 Fragment 初始化 WebView 的时候 使用了 getActivity(),context 一直被 WebView 内核持有,笔者猜测部分系统会有这种问题。这种问题是否无解了?山重水复疑无路,柳暗花明又一寸,笔者意外发现有个类 MutableContextWrapper 可以使用。

    MutableContextWrapper 切换 Context

    初始化 WebView 的时候使用AppContext,在 Activity 使用 Webview 的时候切换为 Activity,最后销毁 WebView 之前再切换回 AppContext

    为什么在Activity 使用WebView的时候切换到Activity 呢?因为WebView 中的可能有些场景依赖 Activity 如:弹窗Dialog,Context 为AppContext 会发生崩溃。

    1. private WebView webview;
    2. //初始化Webview
    3. MutableContextWrapper contextWrapper = new MutableContextWrapper(getAppContext());
    4. webview = new WebView(contextWrapper);
    5. //在Activity中使用
    6. private WebView acquireWebView(Activity activity) {
    7. //缓存中的webview
    8. MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
    9. contextWrapper.setBaseContext(activity);
    10. return webView;
    11. }
    12. //销毁之前
    13. public void recycleWebView(WebView webView) {
    14. if (webView == null) {
    15. return;
    16. }
    17. MutableContextWrapper contextWrapper = (MutableContextWrapper) webView.getContext();
    18. contextWrapper.setBaseContext(getAppContext());
    19. destroyWebView(webview);
    20. }
    21. //销毁 webview 的接口
    22. public void destroyWebView(WebView webView) {
    23. try {
    24. if (webView != null) {
    25. ViewGroup parent = (ViewGroup) webView.getParent();
    26. if (parent != null) {
    27. parent.removeView(webView);
    28. }
    29. webView.setOnTouchListener(null);
    30. webView.setOnKeyListener(null);
    31. webView.setOnFocusChangeListener(null);
    32. webView.setWebChromeClient(null);
    33. webView.setWebViewClient(null);
    34. webView.loadUrl("about:blank");
    35. webView.onPause();
    36. webView.removeAllViews();
    37. webView.destroyDrawingCache();
    38. webView.destroy();
    39. webView = null;
    40. }
    41. } catch (Throwable e) {
    42. e.printStackTrace();
    43. }
    44. }

    至此,没有再抓到泄露路径。

    总结

    本文列举了项目中治理 WebView 内存泄露的手段:

    1)Fragment、Activity 销毁时释放WebView。

    2)释放WebView 需要释放完全,WebView 注册的各种监听器都需要释放。

    3)同时要考虑Fragment、Activity 有没用到匿名内部类,如果有要改成静态内部类,并且要静态内部类有使用Fragment、Activity的话要使用弱引用。

    4)初始化 WebView 的时候使用AppContext,在 Activity 使用 Webview 的时候切换为 Activity,最后销毁 WebView 之前再切换回 AppContext。

  • 相关阅读:
    Unity-GameFramework-202208最新踩坑记录(未完待续...不断完善中...)
    2024年6月23日 十二生肖 今日运势
    洛谷P6672 你的生命已如风中残烛
    【kafka】Timed out waiting for a node assignment
    Linux操作文档——常用脚本
    氨基修饰/偶联/功能化/接枝二氧化钛,NH2-TiO2,TiO2-NH2
    使用Docker安装ElasticSearch和可视化界面Kibana【图文教学】
    怎样在vue中隐藏el-form-item中的值、设置输入框的值是只读
    详细介绍windows自带Hyper-V安装虚拟机(windows11 / ubuntu22 / macos12)
    win11下的VS2022+QT6+VTK9.2+PCL1.13.1联合开发环境配置及踩坑记录
  • 原文地址:https://blog.csdn.net/xiaobaaidaba123/article/details/133842059