Android WebView的使用、实现与渲染引擎的关联启动过程

陈尔展

什么是Android WebView

WebView 是一个用来显示 Web 网页的控件,继承自 AbsoluteLayout,和使用系统其他控件没什么区别,只是 WeView 控件方法比较多比较丰富。因为它就是一个微型浏览器,包含一个浏览器该有的基本功能,例如:滚动、缩放、前进、后退下一页、搜索、执行 Js等功能。

有个比较重要的变化是:

在 Android 4.4 之前使用 WebKit 作为渲染内核,4.4 之后采用 chrome 内核。Api 使用兼容低版本。

Android WebView的主要方法

  • void loadUrl(String url):加载网络链接 url
  • removeJavascriptInterface(String interfaceName):删除interfaceName 对应的注入对象
  • addJavascriptInterface(Object object,String interfaceName):注入 java 对象。
  • boolean canGoBack():判断 WebView 当前是否可以返回上一页
  • goBack():回退到上一页
  • boolean canGoForward():判断 WebView 当前是否可以向前一页
  • goForward():回退到前一页
  • onPause():类似 Activity 生命周期,页面进入后台不可见状态
  • pauseTimers():该方法面向全局整个应用程序的webview,它会暂停所有webview的layout,parsing,JavaScript Timer。当程序进入后台时,该方法的调用可以降低CPU功耗。
  • onResume():在调用 onPause()后,可以调用该方法来恢复 WebView 的运行。
  • resumeTimers():恢复pauseTimers时的所有操作。(注:pauseTimers和resumeTimers 方法必须一起使用,否则再使用其它场景下的 WebView 会有问题)
  • destroy():销毁 WebView
  • clearHistory():清除当前 WebView 访问的历史记录。
  • clearCache(boolean includeDiskFiles):清空网页访问留下的缓存数据。需要注意的时,由于缓存是全局的,所以只要是WebView用到的缓存都会被清空,即便其他地方也会使用到。该方法接受一个参数,从命名即可看出作用。若设为false,则只清空内存里的资源缓存,而不清空磁盘里的。
  • reload():重新加载当前请求
  • setLayerType(int layerType, Paint paint):设置硬件加速、软件加速
  • removeAllViews():清除子view。
  • clearSslPreferences():清除ssl信息。
  • clearMatches():清除网页查找的高亮匹配字符。
  • setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled):设置垂直方向滚动条。
  • setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled):设置横向滚动条。
  • loadUrl(String url, Map additionalHttpHeaders):加载制定url并携带http header数据。
  • evaluateJavascript(String script, ValueCallback resultCallback):Api 19 之后可以采用此方法之行 Js。
  • stopLoading():停止 WebView 当前加载。
  • loadUrl("about:blank")来实现这个功能,阴雨需要重新加载一个页面自然时间会收到影响。
  • freeMemory():释放内存,不过貌似不好用。
  • clearFormData():清除自动完成填充的表单数据。需要注意的是,该方法仅仅清除当前表单域自动完成填充的表单数据,并不会清除WebView存储到本地的数据。

Android WebView的具体实现与Chromium渲染引擎启动过程

这里以Android 8.0的源码为来说明,是先找到WebView的真正的构造函数:

protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,  
            Map<String, Object> javaScriptInterfaces, boolean privateBrowsing) {
    ....
    ensureProviderCreated();
    mProvider.init(javaScriptInterfaces, privateBrowsing);
    ....
}

这个构造函数会调用另外一个成员函数ensureProviderCreated()确保Chromium动态库已经加载。在Chromium动态库已经加载的情况下,WebView类的成员函数ensureProviderCreated还会创建一个WebViewProvider对象,并且保存在成员变量mProvider中。这个WebViewProvider其实才是真正用来实现WebView的功能的幕后大佬。上诉那些Android WebView主要的方式基本都是通过mProvider来实现的,例如loadUrl(String url)等方法。

    public void loadUrl(String url) {
        checkThread();
        mProvider.loadUrl(url);
    }

有了这个mProvider之后,WebView类的构造函数就会继续调用mProvider.init(javaScriptInterfaces, privateBrowsing)启动网页渲染引擎。对于基于Chromium实现的WebView来说,它使用的WebViewProvider是一个WebViewChromium对象。当这个WebViewChromium对象的成员函数init被调用的时候,它就会启动Chromium的网页渲染引擎。 所以,我们接下来看一下ensureProviderCreated的实现:

    private void ensureProviderCreated() {
        checkThread();
        if (mProvider == null) {
            // As this can get called during the base class constructor chain, pass the minimum
            // number of dependencies here; the rest are deferred to init().
            mProvider = getFactory().createWebView(this, new PrivateAccess());
        }
    }

 WebView类的成员函数ensureProviderCreated首先调用成员函数checkThread确保它是在WebView的创建线程中调用的,接下来又会判断成员变量mProvider的值是否为null。如果为null,就表示它还没有当前创建的WebView创建过Provider。在这种情况下,它首先会调用成员函数getFactory获得一个WebViewFactory。有了这个WebViewFactory之后,就可以调用它的成员函数createWebView创建一个WebViewProvider。

接下来我们再看一下getFactory()方法以及它的实现:

    private static WebViewFactoryProvider getFactory() {
        return WebViewFactory.getProvider();
    }
    static WebViewFactoryProvider getProvider() {
        synchronized (sProviderLock) {
            // For now the main purpose of this function (and the factory abstraction) is to keep
            // us honest and minimize usage of WebView internals when binding the proxy.
            if (sProviderInstance != null) return sProviderInstance;

            final int uid = android.os.Process.myUid();
            if (uid == android.os.Process.ROOT_UID || uid == android.os.Process.SYSTEM_UID
                    || uid == android.os.Process.PHONE_UID || uid == android.os.Process.NFC_UID
                    || uid == android.os.Process.BLUETOOTH_UID) {
                throw new UnsupportedOperationException(
                        "For security reasons, WebView is not allowed in privileged processes");
            }

            StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
            try {
                Class<WebViewFactoryProvider> providerClass = getProviderClass();
                Method staticFactory = null;
                try {
                    staticFactory = providerClass.getMethod(
                        CHROMIUM_WEBVIEW_FACTORY_METHOD, WebViewDelegate.class);
                } catch (Exception e) {
                    if (DEBUG) {
                        Log.w(LOGTAG, "error instantiating provider with static factory method", e);
                    }
                }

                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactoryProvider invocation");
                try {
                    sProviderInstance = (WebViewFactoryProvider)
                            staticFactory.invoke(null, new WebViewDelegate());
                    if (DEBUG) Log.v(LOGTAG, "Loaded provider: " + sProviderInstance);
                    return sProviderInstance;
                } catch (Exception e) {
                    Log.e(LOGTAG, "error instantiating provider", e);
                    throw new AndroidRuntimeException(e);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
                }
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
                StrictMode.setThreadPolicy(oldPolicy);
            }
        }
    }

getFactory返回的WebView Factory是通过调用WebViewFactory类的静态成员函数getProvider获得的,getProvider首先是判断静态成员变量sProviderInstance的值是否等于null。如果等于null,那么就说明当前的App进程还没有加载过Chromium动态库。在这种情况下,就需要加载Chromium动态库,并且创建一个WebView Factory,保存在静态成员变量sProviderInstance。接下来我们就先分析Chromium动态库的加载过程,然后再分析WebView Factory的创建过程。

加载Chromium动态库是通过调用WebViewFactory类的静态成员函数loadNativeLibrary实现的:

    private static int loadNativeLibrary(ClassLoader clazzLoader, PackageInfo packageInfo) {
        if (!sAddressSpaceReserved) {
            Log.e(LOGTAG, "can't load with relro file; address space not reserved");
            return LIBLOAD_ADDRESS_SPACE_NOT_RESERVED;
        }

        String[] args = getWebViewNativeLibraryPaths(packageInfo);
        int result = nativeLoadWithRelroFile(args[0] /* path32 */,
                                             args[1] /* path64 */,
                                             CHROMIUM_WEBVIEW_NATIVE_RELRO_32,
                                             CHROMIUM_WEBVIEW_NATIVE_RELRO_64,
                                             clazzLoader);
        if (result != LIBLOAD_SUCCESS) {
            Log.w(LOGTAG, "failed to load with relro file, proceeding without");
        } else if (DEBUG) {
            Log.v(LOGTAG, "loaded with relro file");
        }
        return result;
    }

loadNativeLibrary首先会调用成员函数getWebViewNativeLibraryPaths获得要加载的Chromium动态库的文件路径,然后再调用另外一个静态成员函数nativeLoadWithRelroFile对它进行加载。在加载的时候,会指定一个Chromium GNURELRO Section文件。这个Chromium GNURELRO Section文件是系统启动时候,通过启动一个临时进程生成的。其中静态成员函数nativeLoadWithRelroFile是一个JNI方法,它由C++层的函数LoadWithRelroFile实现:

jboolean LoadWithRelroFile(JNIEnv* env, jclass, jstring lib32, jstring lib64,  
                           jstring relro32, jstring relro64) {
#ifdef __LP64__
  jstring lib = lib64;
  jstring relro = relro64;
  (void)lib32; (void)relro32;
#else
  jstring lib = lib32;
  jstring relro = relro32;
  (void)lib64; (void)relro64;
#endif
  jboolean ret = JNI_FALSE;
  const char* lib_utf8 = env->GetStringUTFChars(lib, NULL);
  if (lib_utf8 != NULL) {
    const char* relro_utf8 = env->GetStringUTFChars(relro, NULL);
    if (relro_utf8 != NULL) {
      ret = DoLoadWithRelroFile(lib_utf8, relro_utf8);
      env->ReleaseStringUTFChars(relro, relro_utf8);
    }
    env->ReleaseStringUTFChars(lib, lib_utf8);
  }
  return ret;
}

LoadWithRelroFile判断自己是32位还是64位的实现,然后从参数lib32和lib64中选择对应的Chromium动态库进行加载。这个加载过程是通过调用另外一个函数DoLoadWithRelroFile实现的:

jboolean DoLoadWithRelroFile(const char* lib, const char* relro) {  
  int relro_fd = TEMP_FAILURE_RETRY(open(relro, O_RDONLY));
  ......

  android_dlextinfo extinfo;
  extinfo.flags = ANDROID_DLEXT_RESERVED_ADDRESS | ANDROID_DLEXT_USE_RELRO;
  extinfo.reserved_addr = gReservedAddress;
  extinfo.reserved_size = gReservedSize;
  extinfo.relro_fd = relro_fd;
  void* handle = android_dlopen_ext(lib, RTLD_NOW, &extinfo);
  close(relro_fd);
  ......

  return JNI_TRUE;
}

 函数DoLoadWithRelroFile的实现是通过Linker导出的函数androiddlopenext在Zyogote进程保留的地址空间中加载Chromium动态库的。注意,App进程是Zygote进程fork出来的,因此它同样会获得Zygote进程预留的地址空间。不过,函数DoLoadWithRelroFile会将告诉函数androiddlopenext在加载Chromium动态库的时候,将参数relro描述的Chromium GNURELRO Section文件内存映射到内存来,并且代替掉已经加载的Chromium动态库的GNURELRO Section。这是通过将指定一个ANDROIDDLEXTUSERELRO标志实现的。之所以可以这样做,是因为参数relro描述的Chromium GNURELRO Section文件对应的Chromium动态库的加载地址与当前App进程加载的Chromium动态库的地址一致。只要两个相同的动态库在两个不同的进程中的加载地址一致,它们的链接和重定位信息就是完全一致的,因此就可以通过文件内存映射的方式进行共享。共享之后,就可以达到节省内存的目的了。

这一步执行完成之后,App进程就加载完成Chromium动态库了。回到前面分析的WebViewFactory类的静态成员函数getProvider,它接下来继续创建一个WebViewFactory。这个WebViewFactory以后就可以用来创建WebViewProvider。

WebViewFactory类的静态成员函数getProvider首先要确定要创建的WebView Factory的类型。这个类型是通过调用另外一个静态成员函数getFactoryClass获得的:

    private static Class<WebViewFactoryProvider> getProviderClass() {
        Context webViewContext = null;
        Application initialApplication = AppGlobals.getInitialApplication();

        try {
            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW,
                    "WebViewFactory.getWebViewContextAndSetProvider()");
            try {
                webViewContext = getWebViewContextAndSetProvider();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
            Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
                    sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");

            Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
            try {
                initialApplication.getAssets().addAssetPathAsSharedLibrary(
                        webViewContext.getApplicationInfo().sourceDir);
                ClassLoader clazzLoader = webViewContext.getClassLoader();

                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
                loadNativeLibrary(clazzLoader, sPackageInfo);
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);

                Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
                try {
                    return getWebViewProviderClass(clazzLoader);
                } finally {
                    Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
                }
            } catch (ClassNotFoundException e) {
                Log.e(LOGTAG, "error loading provider", e);
                throw new AndroidRuntimeException(e);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
            }
        } catch (MissingWebViewPackageException e) {
            // If the package doesn't exist, then try loading the null WebView instead.
            // If that succeeds, then this is a device without WebView support; if it fails then
            // swallow the failure, complain that the real WebView is missing and rethrow the
            // original exception.
            try {
                return (Class<WebViewFactoryProvider>) Class.forName(NULL_WEBVIEW_FACTORY);
            } catch (ClassNotFoundException e2) {
                // Ignore.
            }
            Log.e(LOGTAG, "Chromium WebView package does not exist", e);
            throw new AndroidRuntimeException(e);
        }
    }

从这里可以看到,WebViewFactory类的静态成员函数getFactoryClass返回的WebView Factory的类型为com.android.webview.chromium.WebViewChromiumFactoryProviderForO。这个com.android.webview.chromium.WebViewChromiumFactoryProviderForO类是由前面提到的WebView Package提供的。这意味着WebViewFactory类的静态成员函数getProvider创建的WebView Factory是一个WebViewChromiumFactoryProvider对象:

    public WebViewChromiumFactoryProvider() {
        ...
        AwBrowserProcess.loadLibrary();
        ...

WebViewChromiumFactoryProvider类的构造函数会调用AwBrowserProcess类的静态成员函数loadLibrary对前面加载的Chromium动态库进行初始化:

public abstract class AwBrowserProcess {  
    ...
    public static void loadLibrary() {
        ...
        try {
            LibraryLoader.loadNow();
        } catch (ProcessInitException e) {
            throw new RuntimeException("Cannot load WebView", e);
        }
    }

    ...
}

AwBrowserProcess类的静态成员函数loadLibrary又调用LibraryLoader类的静态成员函数loadNow对前面加载的Chromium动态库进行初始化:

public class LibraryLoader {  
    ...
    public static void loadNow() throws ProcessInitException {
        loadNow(null, false);
    }
    ...
}

LibraryLoader类的静态成员函数loadNow又调用另外一个重载版本的静态成员函数loadNow对前面加载的Chromium动态库进行初始化:

public class LibraryLoader {  
    ...
    public static void loadNow(Context context, boolean shouldDeleteOldWorkaroundLibraries)
            throws ProcessInitException {
        synchronized (sLock) {
            loadAlreadyLocked(context, shouldDeleteOldWorkaroundLibraries);
        }
    }
    ...
}

LibraryLoader类重载版本的静态成员函数loadNow又调用另外一个静态成员函数loadAlreadyLocked对前面加载的Chromium动态库进行初始化:

public class LibraryLoader {  
    ...

    // One-way switch becomes true when the libraries are loaded.
    private static boolean sLoaded = false;
    ...

    private static void loadAlreadyLocked(
            Context context, boolean shouldDeleteOldWorkaroundLibraries)
            throws ProcessInitException {
        try {
            if (!sLoaded) {
                ...

                boolean useChromiumLinker = Linker.isUsed();

                if (useChromiumLinker) Linker.prepareLibraryLoad();

                for (String library : NativeLibraries.LIBRARIES) {
                    Log.i(TAG, "Loading: " + library);
                    if (useChromiumLinker) {
                        Linker.loadLibrary(library);
                    } else {
                        try {
                            System.loadLibrary(library);
                        } catch (UnsatisfiedLinkError e) {
                            ...
                        }
                    }
                }
                if (useChromiumLinker) Linker.finishLibraryLoad();

                ...
                sLoaded = true;
            }
        } catch (UnsatisfiedLinkError e) {
            ...
        }
        ...
    }
    ...
}

由于并不是所有的系统都支持在加载动态库时,以文件内存映射的方式代替它的GNURELRO Section,因此Chromium自己提供了一个Linker。通过这个Linker加载动态库时,能够以文件内存映射的方式代替要加载的动态库的GNURELRO Section,也就是实现前面提到的函数androiddlopenext的功能。在高于Android 5.0中,由于系统已经提供了函数androiddlopenext,因此,Chromium就不会使用自己的Linker加载动态库,而是使用Android系统提供的Linker来加载动态库。通过调用System类的静态成员函数loadLibrary即可以使用系统提供的Linker来加载动态库。LibraryLoader类的静态成员函数loadAlreadyLocked要加载的动态库由NativeLibraries类的静态成员变量LIBRARIES指定:

public class NativeLibraries {  
    ...

    static final String[] LIBRARIES = { "webviewchromium" };

    ...
}

从这里可以知道,LibraryLoader类的静态成员函数loadAlreadyLocked要加载的动态库就是Chromium动态库。这个Chromium动态库前面已经加载过了,因此这里通过调用System类的静态成员函数loadLibrary再加载时,仅仅是只会触发它导出的函数JNIOnLoad被调用,而不会重新被加载。Chromium动态库导出的JNIOnLoad被调用的时候,Chromium动态库就会执行初始化工作:

JNI_EXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) {  
  ...

  content::SetContentMainDelegate(new android_webview::AwMainDelegate());
  ...

  return JNI_VERSION_1_4;
}

其中的一个初始化操作是给Chromium的Content层设置一个类型为AwMainDelegate的Main Delegate。这个AwMainDelegate实现在Chromium的androidwebview模块中。Android WebView是通过Chromium的androidwebview模块加载和渲染网页的。Chromium加载和渲染网页的功能又是实现在Content层的,因此,Chromium的androidwebview模块又要通过Content层实现加载和渲染网页功能。这样,Chromium的androidwebview模块就可以设置一个Main Delegate给Content层,以便它们可以互相通信。给Chromium的Content层设置一个Main Delegate是通过调用函数SetContentMainDelegate实现的:

LazyInstance<scoped_ptr<ContentMainDelegate> > g_content_main_delegate =  
    LAZY_INSTANCE_INITIALIZER;

......

void SetContentMainDelegate(ContentMainDelegate* delegate) {  
  DCHECK(!g_content_main_delegate.Get().get());
  g_content_main_delegate.Get().reset(delegate);
}

从前面的分析可以知道,参数delegate指向的是一个AwMainDelegate对象,这个AwMainDelegate对象会被函数SetContentMainDelegate保存在全局变量gcontentmain_delegate中。这一步执行完成后,Chromium动态库就在App进程中加载完毕,并且也已经完成了初始化工作。与此同时,系统也为App进程创建了一个类型为WebViewChromiumFactoryProvider的WebViewFactory。回到前面分析的WebView类的成员函数ensureProviderCreated中,这时候就它会通过调用上述类型为WebViewChromiumFactoryProvider的WebViewFactory的成员函数createWebView为当前创建的WebView创建一个WebView Provider:

public class WebViewChromiumFactoryProvider implements WebViewFactoryProvider {  
    ...

    @Override
    public WebViewProvider createWebView(WebView webView, WebView.PrivateAccess privateAccess) {
        WebViewChromium wvc = new WebViewChromium(this, webView, privateAccess);

        ...

        return wvc;
    }

    ...
}

WebViewChromiumFactoryProvider类的成员函数createWebView创建的是一个类型为WebViewChromium的WebView Provider。这个WebView Provider将会返回给WebView类的成员函数ensureProviderCreated。WebView类的成员函数ensureProviderCreated再将该WebView Provider保存在成员变量mProvider中。这样,正在创建的WebView就获得了一个类型为WebViewChromium的WebView Provider。以后通过这个WebView Provider,就可以通过Chromium来加载和渲染网页了。