首页 > 系统相关 >Android:教你如何避免解决WebView内存泄漏

Android:教你如何避免解决WebView内存泄漏

时间:2023-06-22 14:32:40浏览次数:49  
标签:调用 return 内存 destroy Android WebView


一直听说 WebView 使用不当容易造成内存泄漏,网上有很多针对内存泄漏的解决方案,比较多的是在 Activity.onDestroy 的时候将 WebView 从 View 树中移除,然后再调用 WebView.destroy 方法:

override fun onDestroy() {
    val parent = webView?.parent
    if (parent is ViewGroup) {
        parent.removeView(webView)
    }
    webView?.destroy()
    super.onDestroy()
}
复制代码

于是我写了一个简单的包含一个 WebView 的 Activity,然后在 Activity.onDestroy 中分别尝试 啥也不干只调用 WebView.destroy 方法,接着项目里面集成了 leakcanary 用来检测内存泄漏,启动 App 后,反复横屏竖屏,发现 Activity.onDestroy 有被正常调用,但是 leakcanary 并没有提示有内存泄漏,因此猜想 WebView 高版本应该把这个问题修复了。我用的测试机是 Android 9 版本的,于是想着换个低版本的机型试试,就弄了个 Android 6 的手机一跑,发现还是没有发生内存泄漏,看了下网上这些讲 WebView 内存泄漏的文章,有的还是 2019 年的,既然都 2019 年了还在谈 WebView 会造成内存泄漏,那感觉 Android 6 的机型不应该表现正常呀,一脸懵逼。。。秉着不弄明白不罢休的原则,遇到这种问题好办,Read The Fucking Source Code 就完事了。

WebView销毁时做了什么

既然网上的解决方案说先调用 removeView 移除 WebView,然后再调用 WebView.destroy 方法,那想着内存泄漏应该可以从 onDetachedFromWindow(从 Window 中 detach) 和 destroy(销毁) 这两个逻辑里找原因,看一下 WebView 中的这两个方法:

public void destroy() {
    checkThread();
    mProvider.destroy();
}

protected void onDetachedFromWindowInternal() {
    mProvider.getViewDelegate().onDetachedFromWindow();
    super.onDetachedFromWindowInternal();
}
复制代码

一般而言 destroy 方法应该在 Activity.onDestroy 时手动调用,而 onDetachedFromWindowInternal 方法在 View detach 的时候会由系统回调。注意 onDestroy 的调用时机早于 onDetachedFromWindow,相关的源码可以参考 Android图形系统综述 中 View 系列的文章自行跟踪。

上面这两个方法都出现了一个叫 mProvider 的对象,这个对象是啥呢?在 WebView.java 中搜索了一下 mProvider = 发现只有一处赋值:

private WebViewProvider mProvider;

mProvider = getFactory().createWebView(this, new PrivateAccess());
复制代码

它是一个 WebViewProvider 类型的实例,接着看它是怎么被赋值的,首先看一看 getFactory 返回的工厂对象是什么:

private static WebViewFactoryProvider getFactory() {
    return WebViewFactory.getProvider();
}

// WebViewFactory
static WebViewFactoryProvider getProvider() {
    if (sProviderInstance != null) return sProviderInstance;
    Class<WebViewFactoryProvider> providerClass = getProviderClass();
    // CHROMIUM_WEBVIEW_FACTORY_METHOD = "create"
    staticFactory = providerClass.getMethod(CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
    sProviderInstance = (WebViewFactoryProvider) staticFactory.invoke(null, new WebViewDelegate());
    return sProviderInstance;
}
复制代码

上面的 WebViewFactory.getProvider() 方法看上去是通过调用 providerClass 中的 create 方法拿到了 sProviderInstance 实例,于是得继续看 getProviderClass 方法到底是返回了一个什么类型的类:

private static Class<WebViewFactoryProvider> getProviderClass() {
    // ...
    return getWebViewProviderClass(clazzLoader);
}

public static Class<WebViewFactoryProvider> getWebViewProviderClass(ClassLoader clazzLoader) throws ClassNotFoundException {
    return (Class<WebViewFactoryProvider>) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true, clazzLoader);
}
复制代码

查看源码,可以发现 CHROMIUM_WEBVIEW_FACTORY 取值为 com.android.webview.chromium.WebViewChromiumFactoryProviderForP,我查看的源码版本是 Android P 的,所以这里是 WebViewChromiumFactoryProviderForP,看了一下其它 Android 版本的源码,发现都有一个对应的 WebViewChromiumFactoryProviderForX 值。这个 WebViewChromiumFactoryProviderForP 类在 AOSP 中是没有的,那应该去哪里找呢?

参考 Chrome developer 的文档: WebView for Android,可以看到从 Android 4.4 开始,WebView 组件基于 Chromium open source project 项目,新的 Webview 与 Android 端的 Chrome 浏览器共享同样的渲染引擎,因此 WebView 和 Chrome 之间的渲染应该会更加一致。而从 Android 5.0(Lollipop) 版本开始将 WebView 迁移到了一个独立的 APK — Android System WebView,因此可以单独在 Android 平台更新。这个 APP 可以在应用管理中看到,看到这里我大概明白了之前为啥用 Android 6 的机器也没有测试出内存泄漏,猜想应该是它的 Android System WebView 应用版本已经把内存泄漏的问题解决了吧,看了一下其应用版本是 86.0.4240.198(可以在应用管理中查看 Android System WebView 应用的版本,另外也可以在浏览器中打开这个 网址 也会显示版本)。于是我们验证一下这个猜想。

关于 Chromium open source project 的源码可以在这里查看: Chromium open source project Ref,在这里可以查看目标版本的源码,我选择 86.0.4240.198 版本的源码进行解析。接着上面的 WebViewChromiumFactoryProviderForP 开始:

public class WebViewChromiumFactoryProviderForP extends WebViewChromiumFactoryProvider {
    public static WebViewChromiumFactoryProvider create(android.webkit.WebViewDelegate delegate) {
        return new WebViewChromiumFactoryProviderForP(delegate);
    }

    protected WebViewChromiumFactoryProviderForP(android.webkit.WebViewDelegate delegate) {
        super(delegate);
    }
}
复制代码

可以看到返回了一个 WebViewChromiumFactoryProviderForP 实例,其 createWebView 方法在父类 WebViewChromiumFactoryProvider 中:

public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
    return new WebViewChromium(this, webView, privateAccess, mShouldDisableThreadChecking);
}
复制代码

因此上面的 mProvider 是 WebViewChromium 实例,来看一下它的 onDetachedFromWindow 和 destroy 方法:

public WebViewProvider.ViewDelegate getViewDelegate() {
    return this;
}

public void onDetachedFromWindow() {
    // ...
    mAwContents.onDetachedFromWindow();
}

public void destroy() {
    // ...
    mAwContents.destroy();
}
复制代码

这俩都会调用到 AwContents 中对应的方法,所以上面 WebView 销毁的时候,其 destroy 和 onDetachedFromWindowInternal 方法最后会调用到 AwContents 中对应的方法,低版本的内存泄漏就发生在这里。

AwContents中的内存泄漏

我们先看一下 mAwContents 的创建:

mAwContents = new AwContents(mFactory.getBrowserContextOnUiThread(), mWebView, mContext, ...);
复制代码

86.0.4240.198版本

首先看看 86.0.4240.198 版本中的 AwContents 类中的几个相关方法:

public void destroy() {
    if (isDestroyed(NO_WARN)) return;
    // ...
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    if (mIsAttachedToWindow) {
        // 如果此时没有 detach 则先调用 onDetachedFromWindow 方法,然后才将 mIsDestroyed 置为 true
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        onDetachedFromWindow();
    }
    mIsDestroyed = true;
}

// onAttachedToWindow 时会调用
public void onAttachedToWindow() {
    if (isDestroyed(NO_WARN)) return;
    if (mIsAttachedToWindow) {
        Log.w(TAG, "onAttachedToWindow called when already attached. Ignoring");
        return;
    }
    mIsAttachedToWindow = true;
    // ...
    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    // 注册 ComponentCallbacks
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

// onDetachedFromWindow 时会调用
public void onDetachedFromWindow() {
    if (isDestroyed(NO_WARN)) return;
    if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    // ...
    if (mComponentCallbacks != null) {
        // 将 ComponentCallbacks 解注册
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
}
复制代码

在 View attach 到 Window 中的时候会调用上面的 onAttachedToWindow 方法,在 View detach 的时候会调用到 onDetachedFromWindow 方法,这两个方法中调用了一个 registerComponentCallbacks 和 unregisterComponentCallbacks 函数分别注册和解注册了一个 Callback,低版本会发生内存泄漏的原因就在此!

所以我们再来看一下 ComponentCallbacks 相关的逻辑:

// Context
public void registerComponentCallbacks(ComponentCallbacks callback) {
    getApplicationContext().registerComponentCallbacks(callback);
}

// Application
public void registerComponentCallbacks(ComponentCallbacks callback) {
    synchronized (mComponentCallbacks) {
        mComponentCallbacks.add(callback);
    }
}
复制代码

所以假设在 AwContents 中只调用了 registerComponentCallbacks 注册方法而没有调用 unregisterComponentCallbacks 方法来解注册,那么会出现什么情况呢?我们看一下这个 AwComponentCallbacks 类的实现,发现它是 AwContents 中的一个非静态内部类,因此它会持有外部 AwContents 实例的引用,而 AwContents 持有 WebView 的 Context 上下文,对于 xml 中的 WebView 布局而言,这个上下文就是其所在的 Activity,因此如果在 Activity 生命周期结束后没有调用 unregisterComponentCallbacks 方法解注册的话,便可能会发生内存泄漏

86.0.4240.198 版本中,如果在 Activity.onDestroy 方法中啥也不干,那么在 View detach 的时候依旧会调用 unregisterComponentCallbacks 方法解注册;而如果在 Activity.onDestroy 方法中只手动调用了 WebView.destroy 方法,那么还是会先通过调用 onDetachedFromWindow 来解注册,此时的 if (isDestroyed(NO_WARN)) return; 判断是 false,可以正常执行到解注册的逻辑,然后才会标记为已销毁。

54.0.2805.1版本

接着我们再看一个旧版本 54.0.2805.1 中的 AwContents 这几个方法:

public void destroy() {
    if (isDestroyed(NO_WARN)) return;
    // Remove pending messages
    mContentsClient.getCallbackHelper().removeCallbacksAndMessages();
    // ...
    if (mIsAttachedToWindow) {
        Log.w(TAG, "WebView.destroy() called while WebView is still attached to window.");
        nativeOnDetachedFromWindow(mNativeAwContents);
    }
    mIsDestroyed = true;
}

public void onAttachedToWindow() {
    if (isDestroyed(NO_WARN)) return;
    // ...
    if (mComponentCallbacks != null) return;
    mComponentCallbacks = new AwComponentCallbacks();
    mContext.registerComponentCallbacks(mComponentCallbacks);
}

public void onDetachedFromWindow() {
    if (isDestroyed(NO_WARN)) return;
    nativeOnDetachedFromWindow(mNativeAwContents);
    // ...
    if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
}
复制代码

可以看到如果在 Activity.onDestroy 中只调用了 WebView.destroy 方法的话,那么此时还没有调用到 onDetachedFromWindow 方法去解注册,却已经将 mIsDestroyed 置为了 true,于是当 detach 的时候,onDetachedFromWindow 判断到 isDestroyed 为 true 则不会走接下来解注册的逻辑了,于是内存泄漏也随之而来。

而如果在 Activity.onDestroy 中不手动调用 WebView.destroy 的话,理论上在 WebView detach 的时候能调用 onDetachedFromWindow 方法解注册 Callback,那么这个内存泄漏问题应该不会发生,但是没有调用 WebView.destroy 方法的话,很可能会发生其它问题,比如说不会调用 mContentsClient.getCallbackHelper().removeCallbacksAndMessages() 去移除 pending 的消息,说不定又有新的内存泄漏之类的。。。

要测试低版本 Chromium 的内存泄漏,可以找一个低版本的 Android 手机,然后将其 Android System WebView 应用卸载到装机版本,然后查看对应版本的 AwContents 类源码,如果源码中有内存泄漏的可能的话就可以测试了。另外如果手里头有 Root 的手机,可以尝试将 Android System WebView 最新版卸载,然后在 apkmirror 中下载一个低版本的 Android System WebView APK 安装到手机上;或者直接从源码中编译出一个指定版本的 Android System WebView 应用,源码编译时间有限我也没试过,可以参考 build-instructions

总结

WebView 中的内存泄漏其实与 Chromium 内核版本有关,在新版本的 Chromium 内核中内存泄漏问题已经被解决了,而且从 Android 5.0(Lollipop) 版本开始将 Chromium WebView 迁移到了一个独立的 APP – Android System WebView,随着 Android System WebView 的独立发布,低版本 Android 系统(Android 5以上)上搭载的 Chromium 内核一般来说也不会太旧,所以出现内存泄漏的概率应该是比较小的。如果仍需要兼容这很小的一部分机型,可以通过文章开头的方式销毁 WebView,即先移除 WebView 组件,确保先调用到 onDetachedFromWindow 方法解注册,然后再通过 WebView.destroy 方法处理其它销毁逻辑。

标签:调用,return,内存,destroy,Android,WebView
From: https://blog.51cto.com/u_16163480/6534875

相关文章

  • 【建议私藏】Android进阶开发面试必背300题,都在这里了~
    Android的技术面试的本质与考试无差,许多知识点你可能之前没有涉及,之后也不会用到,但面试官提问时,你一定得会。如果你只是精专于之前业务中的内容,那无疑所掌握的知识点会非常会非常片面,也会极大的限制你的发展性,减少你可选择的选项。Android开发面试必问经典题目Handler相关知识,面试......
  • 最新Android音视频开发学习指南,建立自己的技术护城河
    我们常说音视频是程序员小众领域,但其实音视频技术在日常生活中随处可见:直播中要保证在各种网络状况下实现超低价延时、降低卡顿率,就需要用到音视频中的RTC和直播技术;上百人的视频会议若要保证流畅度和清晰的画质就要用到RTC和转码合流服务等技术…Android音视频开发进阶指南目......
  • Android模仿微博的LazyFragment懒加载
    本文会从头开始一步一步带你去写一个LazyFragment,根据写的过程中一步一步记录,你也可以自己试一试,跟着一起写写。最后也根据遇到的问题去完善了,网上搜的都是不完善的,还是自己写一个吧!懒加载是在加载啥?这个问题显得很愚蠢。但是想一下,懒加载到底是加载数据和视图,还是数据呢??(一开始我也......
  • 【干货分享】全套Android学习笔记+最新大厂面试真题合集,打包领取
    笔者是一名普通的软件开发人员,一向不喜欢高高在上或者晦涩难懂的理论。我认为知识的本身也应该是通俗易懂的,用晦涩难懂的东西去描述,是对人类进步的阻碍,是知识垄断。笔者希望此系列教程能够以工程实现为出发点和落脚点,简化理论知识,化繁为简地解析Android相关知识点,为各位读者成长为......
  • 【Android】iOS开发中xconfig和script脚本的使用
    利用Xcode进行开发时需要进行很多buildsetting的设置以便能让项目按照设置的进行编译,同时有时候需要在编译时利用script脚本进行一些设置,本文主要介绍xconfig文件和script脚本在Xcode开发中使用。作者:MambaYongXcode编译在使用xconfig时有几个关于Xcode的概念是需要理解的,这里我进......
  • Android AIDL 跨进程通信超详版
    来了新公司,公司项目里用了很多的独立进程的服务与他们之间存在了很多跨进程的通信。之前有很长一段时间没有实际去做跨进程通信AIDL了,查阅了一些资料和文章看了些Demo把温习的心路历程介绍一下。来模拟一个ktv播控系统(client)控制大屏上的歌曲的播放、暂停动作KtvAIDLClientK......
  • Android13(T) 的Target适配问题总结
    最近在做Android13(T)的Target适配,整理了适配过程中遇到的问题分以下三部分:影响所有应用的变更(包含target33),只影响TargetSdkVersion=33的变更,其他更改(新增或者改善的功能).1.影响所有应用的变更1.1必须要适配此项1.1.1通知的运行时权限Android13中引入了一种新的......
  • Android app的启动优化总结
    工欲善其事必先利其器,最近在启动优化上踩了不少坑,写篇文章记录下,也给大伙避避坑,节省些时间。启动优化是什么,完全可以顾名思义,本文就不赘述了。至于为什么要做性能优化–QAQ,大家dddd问题场景主要分为如下两种场景,笔者主要在第一种场景下进行实操哈1、项目中已有性能启动相关埋点以及......
  • 95后Android开发:“我现在是真想躺平...“
    我是真想躺平…说实话,我现在每天上班都很难受,我也不知道为啥反正就很丧,很想当一条咸鱼,就想躺着。最近疫情、裁员…坏消息很多,大环境不好,我本就打算今年换工作的,现在这环境就有点烦…其他行业可能不知道,程序员跳槽最佳的时间就是3-4月,或者9-10月,被称为金三银四和金九银十,但是今年这......
  • 【工程化】Android开发电脑中都装了哪些软件
    写在前面工欲善其事,必先利其器。作为一名Android开发者,在开始正式开发之前,给电脑安装各种开发相关软件是必不可少的。今天来罗列下我电脑中装的那些开发相关的软件,一来换新电脑时,可以方便根据应用清单安装软件,二来如果你是刚从事Android开发,也可以参考着安装这些软件,希望可以帮助到......