1. 程式人生 > >Android WebView的使用、實現與渲染引擎的關聯啟動過程

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來載入和渲染網頁了。

歡迎加入Android進階交流群;701740775。進群可免費領取一份最新技術大綱和Android進階資料。請備註csdn