現在的app都開始流行混合開發了,這是一個app開發的新技術,作爲android程序猿的我們也應該要了解並且掌握他。那麼在使用之前,我們一定要搞清楚,我們的哪些場景使用混合開發好一些呢?這個問題一定要搞清楚,因爲現在的混合開發還不成熟,Web頁面的渲染效率目前還無法和Native的體驗相比,而大家如果只是爲了採用新技術就盲目的使用混合開發,最後遇到一些體驗問題的話,肯定會得不償失。
那麼什麼情況適合Html 5開發呢?像一些活動頁面,比如秒殺、團購等適合做Html 5,因爲這些頁面可能涉及的非常炫而且複雜,Html 5開發或許會簡單點,關鍵是這些頁面時效性短,更新更快,因爲一個活動說不定就一週時間,下週換活動,如果這樣的話,你還做Native是肯定不行的,這些場景就需要使用混合開發了。以下是網上能找到的一些比較好的入門介紹,大家可以學習一下。
混合開發的實質就是在JS和Native之間相互調用,其中的第一篇博客中也提到了,實現混合開發的方式主要的有兩種:1、js調用Native中的代碼;2、WebView攔截頁面跳轉。第二種方式因爲在Android 4.2(API 17)一下存在高危的漏洞,漏洞的原理就是Android系統通過 WebView.addJavascriptInterface(Object o, String interface) 方法註冊可供js調用的Java對象,但是系統並沒有對註冊的Java對象方法調用做限制。導致攻擊者可以利用反射調用未註冊的其他任何Java對象,攻擊者可以根據客戶端的能力做任何事情,如下的文章詳細講解了漏洞產生的根本原因:
上面的博主使用的是別人封裝好的一個js框架,git地址如下:
而現在介紹較多的還有Facebook的混合開發框架React Native,大家也可以去看一下:
我們本節要分析的就是safe-java-js-webview-bridge框架了,這裏的代碼也非常簡潔,主界面是WebActivity,看了下我這裏的項目源碼,有兩個WebView類,一個是在frameworks/base/tools/layoutlib/bridge/src/android/webkit路徑下的WebView,它是繼承MockView的,還有一個是在vendor/letv/webview/base/core/java/android/webkit路徑下,開始看到vendor目錄,還以爲把原生的東西重寫的,後來問了下瀏覽器模塊的同事,才知道這不是重寫,而是把原生的移動了個目錄而已,我們後面的分析也都是在這個包下面的類。
我們從斷點可以看到,獲取回來的WebSettings的實現類是一個名稱爲ContentSettingsAdapter的對象,整個源碼搜遍,找不到任何相關的東西,看來還是沒有源碼。這些可能也是谷歌Chrom瀏覽器的一些核心技術了,如果有哪位精通的,請指點我一下。
我們先來看一下整個代碼的執行邏輯:
重點的地方我也標紅出來了,整個WebView上的事件響應、界面顯示都是在WebViewChromiumFactoryProvider類中處理的,WebViewChromiumFactoryProvider類是通過反射生成的,後邊分析的過程中,大家會看到它的產生過程,這些沒有源碼,我們也無從得知它的處理邏輯;還有一個重點的地方,就是最後標紅的那塊,就是在構造JsCallJava對象時,通過StringBuilder拼接一個javascript的角本出來,拼接過程當中,就會通過調用genJavaMethodSign方法,把我們要回調的類的所有方法連接成string字符串注入到javascript角本當中,這裏也就是爲什麼WebView瀏覽器能回調我們java代碼,並且我們可以通過返回來的參數知道是要調用哪個方法的原因了。
好了,下面我們就一起來看一下整個代碼的執行過程。
首先,通過new構造一個WebView對象,WebView的構造方法不斷的轉調,最終調用了protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, Map<String, Object> javaScriptInterfaces, boolean privateBrowsing)構造方法,我們來看一下它的實現:
/**
* @hide
*/
@SuppressWarnings("deprecation") // for super() call into deprecated base class constructor.
protected WebView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
Map javaScriptInterfaces, boolean privateBrowsing) {
super(context, attrs, defStyleAttr, defStyleRes);
if (context == null) {
throw new IllegalArgumentException("Invalid context argument");
}
sEnforceThreadChecking = context.getApplicationInfo().targetSdkVersion >=
Build.VERSION_CODES.JELLY_BEAN_MR2;
checkThread();
if (TRACE) Log.d(LOGTAG, "WebView");
ensureProviderCreated();
mProvider.init(javaScriptInterfaces, privateBrowsing);
// Post condition of creating a webview is the CookieSyncManager.getInstance() is allowed.
CookieSyncManager.setGetInstanceIsAllowed();
}
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) {
throw new UnsupportedOperationException(
"For security reasons, WebView is not allowed in privileged processes");
}
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Class providerClass = getProviderClass();
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "providerClass.newInstance()");
try {
sProviderInstance = providerClass.getConstructor(WebViewDelegate.class)
.newInstance(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);
StrictMode.setThreadPolicy(oldPolicy);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
}
}
private static Class getProviderClass() {
try {
// First fetch the package info so we can log the webview package version.
sPackageInfo = fetchPackageInfo();
Log.i(LOGTAG, "Loading " + sPackageInfo.packageName + " version " +
sPackageInfo.versionName + " (code " + sPackageInfo.versionCode + ")");
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.loadNativeLibrary()");
loadNativeLibrary();
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getChromiumProviderClass()");
try {
return getChromiumProviderClass();
} 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) Class.forName(NULL_WEBVIEW_FACTORY);
} catch (ClassNotFoundException e2) {
// Ignore.
}
Log.e(LOGTAG, "Chromium WebView package does not exist", e);
throw new AndroidRuntimeException(e);
}
}
private static final String CHROMIUM_WEBVIEW_FACTORY =
"com.android.webview.chromium.WebViewChromiumFactoryProvider";
private static Class getChromiumProviderClass()
throws ClassNotFoundException {
Application initialApplication = AppGlobals.getInitialApplication();
try {
// Construct a package context to load the Java code into the current app.
Context webViewContext = initialApplication.createPackageContext(
sPackageInfo.packageName,
Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
initialApplication.getAssets().addAssetPath(
webViewContext.getApplicationInfo().sourceDir);
ClassLoader clazzLoader = webViewContext.getClassLoader();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "Class.forName()");
try {
return (Class) Class.forName(CHROMIUM_WEBVIEW_FACTORY, true,
clazzLoader);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
}
} catch (PackageManager.NameNotFoundException e) {
throw new MissingWebViewPackageException(e);
}
}
上面的過程爲我們創建了最重要的處理類,後邊的邏輯基本就很簡單了,繼續調用createWebView生成一個WebViewProvider對象,並賦值給成員變量mProvider,然後調用init方法初始化,這樣WebView對象就創建好了。然後調用wv.getSettings()獲取一個WebSettings對象,並通過setJavaScriptEnabled方法設置它支持javascript,然後調用setWebChromeClient將我們的回調注入進去,最後設置WebView要加載的url地址。
我們主要來看一下setWebChromeClient將我們的回調類注入進去的過程。CustomChromeClient的構造方法中直接調用父類InjectedChromeClient的構造方法,傳入的兩個參數"HostApp", HostJsScope.class,分別就是注入到H5頁面中的名稱和回調Native的Java類,在這裏就利用這兩個參數構造一個JsCallJava對象,我們再來看一下JsCallJava類的構造方法的實現:
public JsCallJava (String injectedName, Class injectedCls) {
try {
if (TextUtils.isEmpty(injectedName)) {
throw new Exception("injected name can not be null");
}
mInjectedName = injectedName;
mMethodsMap = new HashMap();
//獲取自身聲明的所有方法(包括public private protected), getMethods會獲得所有繼承與非繼承的方法
Method[] methods = injectedCls.getDeclaredMethods();
StringBuilder sb = new StringBuilder("javascript:(function(b){console.log(\"");
sb.append(mInjectedName);
sb.append(" initialization begin\");var a={queue:[],callback:function(){var d=Array.prototype.slice.call(arguments,0);var c=d.shift();var e=d.shift();this.queue[c].apply(this,d);if(!e){delete this.queue[c]}}};");
for (Method method : methods) {
String sign;
if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (sign = genJavaMethodSign(method)) == null) {
continue;
}
mMethodsMap.put(sign, method);
sb.append(String.format("a.%s=", method.getName()));
}
sb.append("function(){var f=Array.prototype.slice.call(arguments,0);if(f.length<1){throw\"");
sb.append(mInjectedName);
sb.append(" call error, message:miss method name\"}var e=[];for(var h=1;h
看到javascript的角本看是頭大了,語法好亂,可能也是自己不懂吧,以後還得好好學習。那麼當我們在H5頁面上點擊的時候,就會通過javascript角本處理,然後回調WebView的onJsPrompt方法,也就是InjectedChromeClient類的onJsPrompt方法了,相應的參數都會通過這個方法中的message參數以string字符串的形式傳過來,我們可以看一下獲取IMSI方法的參數:
這樣就能保證Native和H5溝通無阻了,既然已經調用回來了,參數也都給我們了,那麼接下來在native中執行就簡單了,根據我們之前保存好的方法去匹配,找到目標後就直接執行,最後把結果返回給H5,返回數據給H5當然也是系統已經給我們把框架搭建好了的,我們只需要把數據傳進去就OK了,真是太妙了!!!
好了,理解完整個過程,那我們要自己去實現一個也就就簡單了,最後我們來把要點總結一下:
1:一定要支持javascript,可以通過ws.setJavaScriptEnabled(true)來設置
2:要設置一個H5回調Native的ChromeClient對象,可以通過wv.setWebChromeClient來完成
3:實現好你的Native回調類,也就是我要在這個類中幹些什麼事情,比如我要打開Activity,顯示對話框,或者獲取手機信息返回給H5等等
4:要將你的回調類的所有方法通過javascript角本注入到ChromeClient當中,注入是在ChromeClient的onProgressChanged方法中完成的,注入的數據是以string拼接出來的