现在的app都开始流行混合开发了,这是一个app开发的新技术,作为android程序猿的我们也应该要了解并且掌握他。那么在使用之前,我们一定要搞清楚,我们的哪些场景使用混合开发好一些呢?这个问题一定要搞清楚,因为现在的混合开发还不成熟,Web页面的渲染效率目前还无法和Native的体验相比,而大家如果只是为了采用新技术就盲目的使用混合开发,最后遇到一些体验问题的话,肯定会得不偿失。
那么什么情况适合Html 5开发呢?像一些活动页面,比如秒杀、团购等适合做Html 5,因为这些页面可能涉及的非常炫而且复杂,Html 5开发或许会简单点,关键是这些页面时效性短,更新更快,因为一个活动说不定就一周时间,下周换活动,如果这样的话,你还做Native是肯定不行的,这些场景就需要使用混合开发了。以下是网上能找到的一些比较好的入门介绍,大家可以学习一下。
谈谈Android App混合开发
Android 混合开发 的一些心得
混合开发的实质就是在JS和Native之间相互调用,其中的第一篇博客中也提到了,实现混合开发的方式主要的有两种:1、js调用Native中的代码;2、WebView拦截页面跳转。第二种方式因为在Android 4.2(API 17)一下存在高危的漏洞,漏洞的原理就是Android系统通过 WebView.addJavascriptInterface(Object o, String interface) 方法注册可供js调用的Java对象,但是系统并没有对注册的Java对象方法调用做限制。导致攻击者可以利用反射调用未注册的其他任何Java对象,攻击者可以根据客户端的能力做任何事情,如下的文章详细讲解了漏洞产生的根本原因:
WebView 远程代码执行漏洞浅析
上面的博主使用的是别人封装好的一个js框架,git地址如下:
safe-java-js-webview-bridge
而现在介绍较多的还有Facebook的混合开发框架React Native,大家也可以去看一下:
Use React Native
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
/**
* @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
在这里我们可以看到,首先根据传进来的Class,利用反射获取到它的所有可执行方法,将这些方法保存在成员变量mMethodsMap当中,后边H5页面回调回来了,我们就根据传回来的参数对比一下,就知道需要执行哪个方法了,获取到的所有的方法也会通过string字符串的形式拼接成一个完整的javascript角本注入到WebView当中,这里执行完,那么所有的环境都准备好了,在WebView加载过程中,会回调onProgressChanged方法,也就是在这里,把我们拼接好的javascript角本注入进去的。我们把拼接完成的javascript角本字符串打印出来,用HBuilder格式化整齐看一下,如下图: 看到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拼接出来的