什么是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 GNU RELRO Section文件。这个Chromium GNU RELRO 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导出的函数android dlopen ext在Zyogote进程保留的地址空间中加载Chromium动态库的。注意,App进程是Zygote进程fork出来的,因此它同样会获得Zygote进程预留的地址空间。不过,函数DoLoadWithRelroFile会将告诉函数android dlopen ext在加载Chromium动态库的时候,将参数relro描述的Chromium GNU RELRO Section文件内存映射到内存来,并且代替掉已经加载的Chromium动态库的GNU RELRO Section。这是通过将指定一个ANDROID DLEXT USE RELRO标志实现的。之所以可以这样做,是因为参数relro描述的Chromium GNU RELRO 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) {
...
}
...
}
...
}
由于并不是所有的系统都支持在加载动态库时,以文件内存映射的方式代替它的GNU RELRO Section,因此Chromium自己提供了一个Linker。通过这个Linker加载动态库时,能够以文件内存映射的方式代替要加载的动态库的GNU RELRO Section,也就是实现前面提到的函数android dlopen ext的功能。在高于Android 5.0中,由于系统已经提供了函数android dlopen ext,因此,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再加载时,仅仅是只会触发它导出的函数JNI OnLoad被调用,而不会重新被加载。Chromium动态库导出的JNI OnLoad被调用的时候,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的android webview模块中。Android WebView是通过Chromium的android webview模块加载和渲染网页的。Chromium加载和渲染网页的功能又是实现在Content层的,因此,Chromium的android webview模块又要通过Content层实现加载和渲染网页功能。这样,Chromium的android webview模块就可以设置一个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保存在全局变量g content main_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来加载和渲染网页了。