作者:無名小子的雜貨鋪
連結:
https://www.jianshu.com/p/fd61e8f4049e
本文由作者授權發布。
文章非常長,但是乾貨很多,前面內容較為基礎,大家如果對WebView比較熟悉,建議拖到第6節開始繼續往下看。
總結 Android WebView 常用的相關知識點,令包含以下乾貨內容分析:Js注入漏洞、WebView 遇到的坑、JsBridge 原理以及框架使用(JsBridge,DSBridge-Android)、緩存機制應用、性能優化、騰訊開源框架 VasSonic (之後會進行代碼分析)。
https://github.com/lzyzsd/JsBridge
https://github.com/whiskeyfei/DSBridge-Android
目錄這部分主要介紹下 WebView,WebView 是一個用來顯示 Web 網頁的控制項,繼承自 AbsoluteLayout,和使用系統其他控制項沒什麼區別,只是 WeView 控制項方法比較多比較豐富。因為它就是一個微型瀏覽器,包含一個瀏覽器該有的基本功能,例如:滾動、縮放、前進、後退下一頁、搜索、執行 Js等功能。
在 Android 4.4 之前使用 WebKit 作為渲染內核,4.4 之後採用 chrome 內核。Api 使用兼容低版本。
官方 WebView.html
https://developer.android.com/reference/android/webkit/WebView.html
A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.
//配置網絡權限
<uses-permission android:name="android.permission.INTERNET"/>
//布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.webview.SafeWebView
android:id="@+id/web_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</LinearLayout>
// BaseWebView 是我自己封裝的 WebView
//實際使用時請採用 new 的方式
mWebView = (SafeWebView) findViewById(R.id.web_view);
mWebView.addJavascriptInterface(new NativeInterface(this), "AndroidNative");
mWebView.loadUrl("http://www.jianshu.com/u/fa272f63280a");
主要包含 WebView 的使用方法。 我們基於這些方法能擴展很多其他功能,例如:JsBridge、緩存等。
void loadUrl(String url):加載網絡連結 url
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():清除網頁查找的高亮匹配字符。
removeJavascriptInterface(String interfaceName):刪除interfaceName 對應的注入對象
addJavascriptInterface(Object object,String interfaceName):注入 java 對象。
setVerticalScrollBarEnabled(boolean verticalScrollBarEnabled):設置垂直方向滾動條。
setHorizontalScrollBarEnabled(boolean horizontalScrollBarEnabled):設置橫向滾動條。
loadUrl(String url, Map<String, String> additionalHttpHeaders):加載制定url並攜帶http header數據。
evaluateJavascript(String script, ValueCallback<String> resultCallback):Api 19 之後可以採用此方法之行 Js。
stopLoading():停止 WebView 當前加載。
clearView():在Android 4.3及其以上系統這個api被丟棄了, 並且這個api大多數情況下會有bug,經常不能清除掉之前的渲染數據。官方建議通過loadUrl("about:blank")來實現這個功能,陰雨需要重新加載一個頁面自然時間會收到影響。
freeMemory():釋放內存,不過貌似不好用。
clearFormData():清除自動完成填充的表單數據。需要注意的是,該方法僅僅清除當前表單域自動完成填充的表單數據,並不會清除WebView存儲到本地的數據。
我這裡在介紹下下面幾組方法,比較重要,項目當中可能會遇到坑
onPause() 盡力嘗試暫停可以暫停的任何處理,如動畫和地理位置。 不會暫停JavaScript。 要全局暫停JavaScript,可使用pauseTimers。
onResume() 恢復onPause() 停掉的操作;
pauseTimers() 暫停所有WebView的布局,解析和JavaScript定時器。 這個是一個全局請求,不僅限於這個WebView。
resumeTimers() 恢復所有WebView的所有布局,解析和JavaScript計時器,將恢復調度所有計時器.
另外注意 JS 端setTimeout()、setInterval() 方法使用,自測來看,當不使用 pauseTimers() 和 pauseTimers() ,從 Activity 返回上一個包含WebView 的Activity時,頁面裡的 setTimeout() 是不執行的,setInterval() 是可以恢復執行的。
在適當的生命周期使用 pauseTimers() 和 pauseTimers() 既可以恢復setTimeout() 執行。
一份 WebView 方法使用清單mWebView.loadUrl("http://www.jianshu.com/u/fa272f63280a");
mWebView.setWebViewClient(new SafeWebViewClient());
mWebView.setWebChromeClient(new SafeWebChromeClient());
mWebView.onResume();
mWebView.resumeTimers();
mWebView.onPause();
mWebView.pauseTimers();
mWebView.stopLoading();
mWebView.clearMatches();
mWebView.clearHistory();
mWebView.clearSslPreferences();
mWebView.clearCache(true);
mWebView.loadUrl("about:blank");
mWebView.removeAllViews();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
mWebView.removeJavascriptInterface("AndroidNative");
}
mWebView.destroy();
主要包含三部分:WebSettings、WebViewClient、WebChromeClient。
setJavaScriptEnabled(boolean flag):是否支持 Js 使用。
setCacheMode(int mode):設置 WebView 的緩存模式。
setAppCacheEnabled(boolean flag):是否啟用緩存模式。
setAppCachePath(String appCachePath):Android 私有緩存存儲,如果你不調用setAppCachePath方法,WebView將不會產生這個目錄。
setSupportZoom(boolean support):是否支持縮放。
setTextZoom(int textZoom):Sets the text zoom of the page in percent. The default is 100。
setAllowFileAccess(boolean allow):是否允許加載本地 html 文件/false。
setDatabaseEnabled(boolean flag):是否開啟資料庫緩存
setDomStorageEnabled(boolean flag):是否開啟DOM緩存。
setUserAgentString(String ua):設置 UserAgent 屬性。
setLoadsImagesAutomatically(boolean flag):支持自動加載圖片
setAllowFileAccessFromFileURLs(boolean flag::允許通過 file url 加載的 Javascript 讀取其他的本地文件,Android 4.1 之前默認是true,在 Android 4.1 及以後默認是false,也就是禁止。
setAllowUniversalAccessFromFileURLs(boolean flag):允許通過 file url 加載的 Javascript 可以訪問其他的源,包括其他的文件和 http,https 等其他的源,Android 4.1 之前默認是true,在 Android 4.1 及以後默認是false,也就是禁止如果此設置是允許,則 setAllowFileAccessFromFileURLs 不起做用。
boolean getLoadsImagesAutomatically():是否支持自動加載圖片。
WebSettings webSettings = mWebView.getSettings();
if (webSettings == null) return;
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
webSettings.setLoadsImagesAutomatically(hasKitkat());
webSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
webSettings.setAppCacheEnabled(true);
webSettings.setAppCacheMaxSize(8 * 1024 * 1024);
webSettings.setAppCachePath(getCacheDir().getAbsolutePath());
if (!hasKitkat()) {
webSettings.setDatabasePath(getDatabasePath("html").getPath());
}
webSettings.setSavePassword(false);
webSettings.setSupportZoom(true);
webSettings.setUserAgentString("");
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(false);
webSettings.setAllowUniversalAccessFromFileURLs(false);
onPageStarted(WebView view, String url, Bitmap favicon):WebView 開始加載頁面時回調,一次Frame加載對應一次回調。
onLoadResource(WebView view, String url):WebView 加載頁面資源時會回調,每一個資源產生的一次網絡加載,除非本地有當前 url 對應有緩存,否則就會加載。
shouldInterceptRequest(WebView view, String url):WebView 可以攔截某一次的 request 來返回我們自己加載的數據,這個方法在後面緩存會有很大作用。
shouldInterceptRequest(WebView view, android.webkit.WebResourceRequest request):WebView 可以攔截某一次的 request 來返回我們自己加載的數據,這個方法在後面緩存會有很大作用。
shouldOverrideUrlLoading(WebView view, String url):是否在 WebView 內加載頁面。
onReceivedSslError(WebView view, SslErrorHandler handler, SslError error):WebView ssl 訪問證書出錯,handler.cancel()取消加載,handler.proceed()對然錯誤也繼續加載。
onPageFinished(WebView view, String url):WebView 完成加載頁面時回調,一次Frame加載對應一次回調。
onReceivedError(WebView view, int errorCode, String description, String failingUrl):WebView 訪問 url 出錯。
public class SafeWebViewClient extends WebViewClient {
@Override
public void onScaleChanged(WebView view, float oldScale, float newScale) {
super.onScaleChanged(view, oldScale, newScale);
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
}
@Override
public void onLoadResource(WebView view, String url) {
super.onLoadResource(view, url);
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return super.shouldInterceptRequest(view, request);
}
@Override
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
super.onReceivedError(view, request, error);
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
super.onReceivedSslError(view, handler, error);
}
}
onConsoleMessage(String message, int lineNumber,String sourceID):輸出 Web 端日誌。
onProgressChanged(WebView view, int newProgress):當前 WebView 加載網頁進度。
onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):處理 JS 中的 Prompt對話框
onJsAlert(WebView view, String url, String message, JsResult result): Js 中調用 alert() 函數,產生的對話框。
onReceivedTitle(WebView view, String title):接收web頁面的 Title。
onReceivedIcon(WebView view, Bitmap icon):接收web頁面的icon。
public class SafeWebChromeClient extends WebChromeClient {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
return super.onConsoleMessage(consoleMessage);
}
@Override
public void onProgressChanged(WebView view, int newProgress) {
super.onProgressChanged(view, newProgress);
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
return super.onJsPrompt(view, url, message, defaultValue, result);
}
@Override
public void onReceivedIcon(WebView view, Bitmap icon) {
super.onReceivedIcon(view, icon);
}
@Override
public void onReceivedTitle(WebView view, String title) {
super.onReceivedTitle(view, title);
}
}
註:這裡著重介紹下第一種標準方式,後面會介紹其他兩種方式。
1、使用系統方法 addJavascriptInterface 注入 java 對象來實現。
2、利用 WebViewClient 中 shouldOverrideUrlLoading (WebView view, String url) 接口,攔截操作。這個就是很多公司在用的 scheme 方式,通過制定url協議,雙方各自解析,使用iframe來調用native代碼,實現互通。
3、利用 WebChromeClient 中的 onJsAlert、onJsConfirm、onJsPrompt 提示接口,同樣也是攔截操作。
mWebView.getSettings().setJavaScriptEnabled(true);
public class NativeInterface {
private Context mContext;
public NativeInterface(Context context) {
mContext = context;
}
@JavascriptInterface
public void hello() {
Toast.makeText(mContext, "hello", Toast.LENGTH_SHORT).show();
}
@JavascriptInterface
public void hello(String params) {
Toast.makeText(mContext, params, Toast.LENGTH_SHORT).show();
}
@JavascriptInterface
public String getAndroid() {
Toast.makeText(mContext, "getAndroid", Toast.LENGTH_SHORT).show();
return "Android data";
}
}
mWebView.addJavascriptInterface(new NativeInterface(this), "AndroidNative");
<script>
function callHello(){
AndroidNative.hello();
}
function callHello1(){
AndroidNative.hello('hello Android');
}
function callAndroid(){
var temp = AndroidNative.getAndroid();
console.log(temp);
alert(temp);
}
</script>
Native 調用 Js:mWebView.loadUrl(js);
Js 調用 Native :AndroidNative.getAndroid();
4.2版本以下會存在漏洞,4.2以上需要添加 @JavascriptInterface 註解才能被調用到,Js 調用方式不變。
雖然可以通過注入方式來實現 WebView 和 JS 交互,但是實現功能的同時也帶了安全問題,通過注入的 Java 類作為橋梁,JS 就可以利用這個漏洞。
目前已知的 WebView 漏洞有 4 個,分別是:
1、CVE-2012-6636,揭露了 WebView 中 addJavascriptInterface 接口會引起遠程代碼執行漏洞;
2、CVE-2013-4710,針對某些特定機型會存在 addJavascriptInterface API 引起的遠程代碼執行漏洞;
3、CVE-2014-1939 爆出 WebView 中內置導出的 「searchBoxJavaBridge_」 Java Object 可能被利用,實現遠程任意代碼;
4、CVE-2014-7224,類似於 CVE-2014-1939 ,WebView 內置導出 「accessibility」 和 「accessibilityTraversal」 兩個 Java Object 接口,可被利用實現遠程任意代碼執行。
1、Android 4.2 以下不要在使用 JavascriptInterface方式,4.2 以上需要添加註解 @JavascriptInterface 才能調用。(這部分和JsBrige 有關,更詳細的內容後面會介紹)
2、同1解決;
3、在創建 WebView 時,使用 removeJavascriptInterface 方法將系統注入的 searchBoxJavaBridge_ 對象刪除。
4、當系統輔助功能服務被開啟時,在 Android 4.4 以下的系統中,由系統提供的 WebView 組件都默認導出 」accessibility」 和 」accessibilityTraversal」 這兩個接口,這兩個接口同樣存在遠程任意代碼執行的威脅,同樣的需要通過 removeJavascriptInterface 方法將這兩個對象刪除。
super.removeJavascriptInterface("searchBoxJavaBridge_");
super.removeJavascriptInterface("accessibility");
super.removeJavascriptInterface("accessibilityTraversal");
以上都是系統機制層面上的漏洞,還有一些是使用 WebView 不擋產生的漏洞。
5、通過 WebSettings.setSavePassword(false) 關閉密碼保存提醒功能,防止明文密碼存在本地被盜用。
6、WebView 默認是可以使用 File 協議的,也就是 setAllowFileAccess(true),我們應該是主動設置為 setAllowFileAccess(false),防止加載本地文件,移動版的 Chrome 默認禁止加載 file 協議的文件。
setAllowFileAccess(true);//設置為 false 將不能加載本地 html 文件
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
if (url.startsWith("file://") {
setJavaScriptEnabled(false);
} else {
setJavaScriptEnabled(true);
}
安全修復案例
推薦 SafeWebView 這個庫中解決了 Android WebView 中 Js 注入漏洞問題,另外還包含了一些異常處理。可以自行下載閱讀源碼。
https://github.com/seven456/SafeWebView
主要總結 WebView 相關的疑難 bug,由於 Android 版本嚴重碎片化,在使用 WebView 的時候也會遇到各種個樣的坑,特別是 4.4 之後更換了 WebView 內核,4.2 以下有部分漏洞,所以想把經歷過的 WebView 這些坑記錄下來,僅供參考。
1、android.webkit.AccessibilityInjector$TextToSpeechWrapperjava.lang.NullPointerException
at android.webkit.AccessibilityInjector$TextToSpeechWrapper$1.onInit(AccessibilityInjector.java:753)
at android.speech.tts.TextToSpeech.dispatchOnInit(TextToSpeech.java:640)
at android.speech.tts.TextToSpeech.initTts(TextToSpeech.java:619)
at android.speech.tts.TextToSpeech.<init>(TextToSpeech.java:553)
at android.webkit.AccessibilityInjector$TextToSpeechWrapper.<init>(AccessibilityInjector.java:676)
at android.webkit.AccessibilityInjector.addTtsApis(AccessibilityInjector.java:480)
at android.webkit.AccessibilityInjector.addAccessibilityApisIfNecessary(AccessibilityInjector.java:168)
at android.webkit.AccessibilityInjector.onPageStarted(AccessibilityInjector.java:340)
at android.webkit.WebViewClassic.onPageStarted(WebViewClassic.java:4480)
at android.webkit.CallbackProxy.handleMessage(CallbackProxy.java:366)
at android.os.Handler.dispatchMessage(Handler.java:107)
at android.os.Looper.loop(Looper.java:194)
at android.app.ActivityThread.main(ActivityThread.java:5407)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:525)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:833)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:600)
at dalvik.system.NativeStart.main(Native Method)
此問題在4.2.1和4.2.2比較集中,關閉輔助功能,google 下很多結果都是一樣的。
修複方法:在初始化 WebView 時調用disableAccessibility方法即可。
public static void disableAccessibility(Context context) {
if (Build.VERSION.SDK_INT == 17) {
if (context != null) {
try {
AccessibilityManager am = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
if (!am.isEnabled()) {
return;
}
Method setState = am.getClass().getDeclaredMethod("setState", int.class);
setState.setAccessible(true);
setState.invoke(am, 0);
} catch (Exception ignored) {
ignored.printStackTrace();
}
}
}
}
2、android.content.pm.PackageManager$NameNotFoundException
AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.google.android.webview
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4604)
at android.app.ActivityThread.access$1500(ActivityThread.java:154)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1389)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5302)
at java.lang.reflect.Method.invoke(Native Method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:916)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:711)
Caused by: android.util.AndroidRuntimeException: android.content.pm.PackageManager$NameNotFoundException: com.google.android.webview
at android.webkit.WebViewFactory.getFactoryClass(WebViewFactory.java:174)
at android.webkit.WebViewFactory.getProvider(WebViewFactory.java:109)
at android.webkit.WebView.getFactory(WebView.java:2194)
at android.webkit.WebView.ensureProviderCreated(WebView.java:2189)
at android.webkit.WebView.setOverScrollMode(WebView.java:2248)
at android.view.View.<init>(View.java:3588)
at android.view.View.<init>(View.java:3682)
at android.view.ViewGroup.<init>(ViewGroup.java:497)
at android.widget.AbsoluteLayout.<init>(AbsoluteLayout.java:55)
at android.webkit.WebView.<init>(WebView.java:544)
at android.webkit.WebView.<init>(WebView.java:489)
at android.webkit.WebView.<init>(WebView.java:472)
at android.webkit.WebView.<init>(WebView.java:459)
at android.webkit.WebView.<init>(WebView.java:449)
現象:在創建 WebView 時崩潰,跟進棧信息,我們需要在 setOverScrollMode 方法上加異常保護處理
修複方法:
try {
super.setOverScrollMode(mode);
} catch (Throwable e) {
e.printStackTrace();
}
不過上面捕獲的異常範圍有點廣,在github上找到一個更全面的修複方法
try{
super.setOverScrollMode(mode);
} catch(Throwable e){
String messageCause = e.getCause() == null ? e.toString() : e.getCause().toString();
String trace = Log.getStackTraceString(e);
if (trace.contains("android.content.pm.PackageManager$NameNotFoundException")
|| trace.contains("java.lang.RuntimeException: Cannot load WebView")
|| trace.contains("android.webkit.WebViewFactory$MissingWebViewPackageException: Failed to load WebView provider: No WebView installed")) {
e.printStackTrace();
}else{
throw e;
}
}
3、android.webkit.WebViewClassic.clearView
at android.webkit.BrowserFrame.nativeLoadUrl(Native Method)
System.err: at android.webkit.BrowserFrame.loadUrl(BrowserFrame.java:279)
System.err: at android.webkit.WebViewCore.loadUrl(WebViewCore.java:2011)
System.err: at android.webkit.WebViewCore.access$1900(WebViewCore.java:57)
System.err: at android.webkit.WebViewCore$EventHub$1.handleMessage(WebViewCore.java:1303)
System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
System.err: at android.os.Looper.loop(Looper.java:137)
System.err: at android.webkit.WebViewCore$WebCoreThread.run(WebViewCore.java:812)
System.err: at java.lang.Thread.run(Thread.java:856)
webcoreglue: *** Uncaught exception returned from Java call!
System.err: java.lang.NullPointerException
System.err: at android.webkit.WebViewClassic.clearView(WebViewClassic.java:2868)
System.err: at android.webkit.WebViewCore.setupViewport(WebViewCore.java:2497)
System.err: at android.webkit.WebViewCore.updateViewport(WebViewCore.java:2479)
System.err: at android.webkit.BrowserFrame.nativeLoadUrl(Native Method)
System.err: at android.webkit.BrowserFrame.loadUrl(BrowserFrame.java:279)
System.err: at android.webkit.WebViewCore.loadUrl(WebViewCore.java:2011)
System.err: at android.webkit.WebViewCore.access$1900(WebViewCore.java:57)
System.err: at android.webkit.WebViewCore$EventHub$1.handleMessage(WebViewCore.java:1303)
System.err: at android.os.Handler.dispatchMessage(Handler.java:99)
System.err: at android.os.Looper.loop(Looper.java:137)
這個bug是在某些設備上發生的,是在調用webView.destroy() 之前調用了loadurl操作發生的,也不是畢現問題,所以只能跟進源碼查看,在清空 webview destroy 時,調用清理方法,內部可能時機有問題,會出現,WebViewClassic 中 mWebViewCore 對象為null,其內部為handler消息機制。
修複方法:
public void logdUrl(final String url) {
try {
super.loadUrl(url);
} catch (NullPointerException e) {
e.printStackTrace();
}
}
相信很多都或多或少的了解 JsBridge,不管是 iOS 平臺還是 Android平臺,特別是 Hybrid 應用,肯定是要用的 JsBridge 這個機制來建立 Native 和 Web 端的通信。
本部分簡單闡述下 JsBridge 原理,以及分析兩個實際案例。
JsBridge 介紹:JSBridge 我們可以比喻成一座橋或者一根管道,一端是 Web一端是 Native。我們搭建這個通道的目的就是讓 Native 和 Web 之間互相調用更為方便統一和簡潔。
JSBridge 做得好的一個典型就是微信,微信給開發者提供了 JSSDK,該SDK中暴露了很多微信native層的方法,比如支付,定位等。使用起來非常方便。
前面我們分析了 WebView 如何於 JavaScript 交互的,JSBridge 就是在這些基礎之上做擴展使它支持更複雜的功能,三種形式兩種原理分析如下:
1、使用 addJavascriptInterface
原理:這是Android提供的Js與Native通信的官方解決方案,將 java 對象注入到 Js 中直接作為window的某一變量來使用。
2、WebViewClient 中 shouldOverrideUrlLoading (WebView view, String url)。
利用 scheme iframe 機制,只要有iframe 加載,shouldOverrideUrlLoading 方法就會有回調。可以構造一個特殊格式的url,使用shouldOverrideUrlLoading 方法攔截url,根據解析url來之行native方法邏輯。
3、利用 WebChromeClient 中的 onJsAlert、onJsConfirm、onJsPrompt 提示接口,同樣也是攔截操作。
利用 js調用window對象的對應的方法,即 window.alert,window.confirm,window.prompt,WebChromeClient 對象中的三個方法 onJsAlert、onJsConfirm、onJsPrompt 就會被觸發,有了js到native的通道,那麼我們就可以制定協議來約束對方。最終我們選擇使用 prompt 方法,onJsPrompt()方法的message參數的值正是Js的方法window.prompt()的message的值。
匯總:後面兩種雖然形式不同,但是原理是相同的,都是對url或者參數做文章,通過制定參數協議,不管是url還是message,到native攔截處理。native 調用 Js 只有一種方式,就是使用loadUrl(js),js 為在web端定義好的javascript 函數。
以上就是所有 JsBridge 的原理,自己可以寫給demo跑一下,下面看幾個問題:
1、如何避免 JS、Android、iOS 相互調用時,需要事先「約定」方法名稱和參數?
2、原生調用 JS 方法,能否類似原生開發一樣,使用 Callback(block) 做為回調方式?
3、JS 調用原生能否使用 function 獲得返回值?
問題來源於網絡,基本上都是這幾個疑問,下面我們帶著疑問去分析三個個方案。
https://tech.youzan.com/jsbridge/
本案例為有贊技術團隊博客分享的H5與Native交互之JSBridge技術,並沒有最終完全的代碼,不過很清楚的分析了IOS和Android與Javascript的底層交互原理。
1、實現原理
通過schema方式,使用shouldOverrideUrlLoading方法對url協議進行解析。
var url = 'jsbridge://doAction?title=分享標題&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
iframe.remove();
}, 100);
可以到看有贊技術是通過自定義url協議來作為傳輸媒介,這樣 Android 就可以攔截這個請求,從而解析出相應的方法和參數.
不過此 url 中的參數是以鍵值對的方式傳遞,我是建議使用 Json 作為傳輸參數比較好,靈活清楚。
2、庫的封裝
有贊將 Js與 Native 通訊封裝了一個通用的方法,我這裡直接複製過來分析下:
YouzanJsBridge = {
doCall: function(functionName, data, callback) {
var _this = this;
if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
setTimeout(function() {
_this.doCall(functionName, data, callback);
}, 100);
return;
}
this.lastCallTime = Date.now();
data = data || {};
if (callback) {
$.extend(data, { callback: callback });
}
if (UA.isIOS()) {
$.each(data, function(key, value) {
if ($.isPlainObject(value) || $.isArray(value)) {
data[key] = JSON.stringify(value);
}
});
var url = Args.addParameter('youzanjs://' + functionName, data);
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
iframe.remove();
}, 100);
} else if (UA.isAndroid()) {
window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
} else {
console.error('未獲取platform信息,調取api失敗');
}
}
}
這樣不管是和iOS通信還是 Android,都只需要調用 YouzanJsBridge.doCall() 方法即可,講兩個平臺的不同屏蔽在封裝基礎上,這樣也有利於 Web 端代碼的整潔和代碼兼容。
當然這裡的Android 平臺是沒有 callback 回調的,如果你想實現兩端互調的機制,請參考下一個案例,裡面會詳細介紹這部分。
3、一些優化
將項目通用方法抽象:
例如:
1.getData(datatype, callback, extra) H5從Native APP獲取數據
使用場景:H5需要從Native APP獲取某些數據的時候,可以調用這個方法。
2.putData(datatype, data) H5告訴Native APP一些數據
使用場景:H5告訴Native APP一些數據,可以調用這個方法。
3.gotoWebview(url, page, data) Native APP新開一個Webview窗口,並打開相應網頁
4.doAction(action, data) 功能上的一些操作
等等其他方法,我相信如果你自己寫過 native和js 調用demo,上面抽象出來的方法並不陌生,所以,如果你的業務沒有那麼複雜,沒有像微信那樣,需要提供給數以萬計開發者去用去擴展,這種抽象出一些通用方法的方式不是為一種節省成本,快速迭代,方便的方式。
小結:總之萬變不離其宗,所有封裝或者框架的東西,使用的東西都還是最基本的方法,只是對基礎做一個什麼樣程度擴展,或深或淺,唯一的只要用著舒服就行。
https://github.com/lzyzsd/JsBridge
使用方法:
注意Android 和 Web 使用方式
webView.registerHandler("submitFromWeb", new BridgeHandler() {
@Override
public void handler(String data, CallBackFunction function) {
function.onCallBack("native submitFromWeb 方法, 返回 data");
}
});
webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
@Override
public void onCallBack(String data) {
Log.i(TAG, "onCallBack : " + data);
}
});
window.WebViewJavascriptBridge.callHandler(
'submitFromWeb'
, {'param': '中文測試'}
, function(responseData) {
document.getElementById("show").innerHTML = "send get responseData from Android 端, data = " + responseData
}
);
bridge.registerHandler("functionInJs", function(data, responseCallback) {
document.getElementById("show").innerHTML = ("data from Java: = " + data);
var responseData = "call functionInJs success,return android!";
responseCallback(responseData);
});
由於篇幅限制,此處省略了源碼和流程分析。
小結
通過url攔截方式,注入一個本地js文件,來橋接native和web,屏蔽了一些通性工作,在使用上方式相同,好理解,通過兩次來回調用實現了可回調function。
github 上也有很多類似的方案,這裡就不一一分析了,如果你不是看的這個庫,建議好好看看,挺巧妙的機制。
這塊邏輯也是看了好久,挺繞的,可以結合打log和debug來分析。
三:DSBridge-Androidhttps://github.com/wendux/DSBridge-Android
1、使用方法
注意Android 和 Web 使用方式
webView.callHandler("addValue",new Object[]{1,"hello"},new CallBackFunction(){
@Override
public void onCallBack(String retValue) {
Log.d("jsbridge","call succeed,return value is "+retValue);
}
});
webView.callHandler("test",null);
dsBridge.call("testNever", {msg: "testSyn"});
dsBridge.call("testNoArgAsyn", function (v) {
alert(v);
});
由於篇幅限制,此處省略了源碼和流程分析。
小結:
這個方案比較簡單,使用了系統的注入方式,雖然 4.2以下存在漏洞,但是它裡面只能反射包含 @JavascriptInterface 註解的方法,所以和4.2以上注入是一樣的,也是安全的。
相對案例一,案例二實現方式比較簡單粗暴,也比較容易懂。
不過這個庫在我的 4.2 設備上有 bug,下面函數執行時找不到 dsBridge 對象,導致 Native 調用 Js 失敗。
dsBridge.register('addValue',function(l,r){
return l+r;
})
可能和內核執行有關係,我這做了修復,放到 function 函數中執行就可以了,如果你也遇到,可以參考我的修複方法 DSBridge-Android。
https://github.com/whiskeyfei/DSBridge-Android
這部分內容可以參考這兩篇博文:
寫的已經很清楚了,我這裡就不贅述了。
主要包含以下內容:
1、WebView 的5中緩存類型,以及每個緩存類型工作原理、相同點和不同點、。
2、緩存在手機上的存儲。
3、每種緩存機制案例。
如果你想通過過濾來減緩 WebView 請求網絡,可以參考 rexxar-android 中關於攔截url操作讀取本地操作。
參考美團:WebView性能、體驗分析與優化
https://tech.meituan.com/WebViewPerf.html
這部分美團的技術博客已經寫的很好了,不僅從性能、內存消耗、體驗、安全幾個維度,來系統的分析客戶端默認 WebView 的問題,還給出了對應的優化方案。
我的感受:文章中也提到了 QQ 的 Hybrid 架構演進,主要的優化方向和內容和下面的Hybrid 開源框架 VasSonic 基本一直,當然都是騰訊東東,應該是有所借鑑的。而且關於 WebView 的優化也就是那幾部分,串行該並行、緩存 WebView、客戶端代替 WebView 網絡請求,WebView 攔截url加載本地資源,還有Web 端(cdn 神馬的,哈哈,不太熟悉)等等幾個主要方面,但是能將上數幾個方面都完美的結合到一起的市面上很少,VasSonic就做到了。
1、VasSonichttps://github.com/Tencent/VasSonic
VasSonic 是騰訊出品的一個輕量級的高性能的Hybrid框架,專注於提升頁面首屏加載速度,完美支持靜態直出頁面和動態直出頁面,兼容離線包等方案。
我的感受:比豆瓣的框架複雜很多,不是一個量級的,可能和業務也相關吧,畢竟鵝廠用戶很多服務也很多啊。我也是斷斷續續看了很久,邏輯還是挺複雜的,不過思路挺清晰,就是能利用上的資源統統要利用,不能讓 CPU 或者網絡空等,主要是在WebView 和加載之間做一個完美的橋接,讓內容不管在什麼情況下都能銜接自如。不過,其中並不包含 JsBridge 部分內容。
VasSonic/wiki wiki 寫的也特別清楚,值得好好去研究,另外這個庫的QQ群也特別活躍,裡面有負責維護的同學幫忙解答疑問,值得點讚!
辛辛苦苦開源了,作為平時想提高 WebView 訪問速度的朋友肯定不會錯過這麼好的內容,強烈建議大家去閱讀源碼,庫更新也很快。
2、rexxar-androidhttps://github.com/douban/rexxar-android
rexxar-android 是豆瓣的混合開發框架,包含 Web、iOS、Android 三端,Android 部分主要內容圍繞路由表、容器、緩存。其他部分可以閱讀豆瓣的混合開發框架 -- Rexxar 更詳細介紹。
我的感覺:rexxar-android 在緩存部分還是值得學習的,緩存 Cache 分為兩部分:本地預裝、本地 file 緩存。其中將每一個需要 Native 完成的功能抽象成一個 widget ,通過制定url協議,過濾並解析url的攜帶的參數。
有興趣的可以下載源碼學習。
Rexxar Android 系列學習其他文章
完整dmeohttp://blog.csdn.net/self_study/article/details/54928371
http://blog.csdn.net/self_study/article/details/55046348
https://tech.youzan.com/jsbridge
https://juejin.im/entry/573534f82e958a0069b27646
https://yq.aliyun.com/articles/32559
http://tutorials.jenkov.com/android/android-web-apps-using-android-webview.html#android-web-app-or-android-hybrid-app
https://jiandanxinli.github.io/2016-08-31.html
http://dev.qq.com/topic/591d537a5bf956911a014c63
https://tech.meituan.com/WebViewPerf.html
http://unclechen.github.io/2017/05/13/WebView%E7%BC%93%E5%AD%98%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90%E5%92%8C%E5%BA%94%E7%94%A8/
https://github.com/Tencent/VasSonic/wiki
長的我的不想寫推薦閱讀了:
"巧"仿螞蟻森林水滴動效
一點見解: Android嵌套滑動和NestedScrollView
如果你想要跟大家分享你的文章,歡迎投稿~