Android混合开发全解析

时间:2023-02-02 16:46:33

     现在的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目录,还以为把原生的东西重写的,后来问了下浏览器模块的同事,才知道这不是重写,而是把原生的移动了个目录而已,我们后面的分析也都是在这个包下面的类。

Android混合开发全解析

     我们从断点可以看到,获取回来的WebSettings的实现类是一个名称为ContentSettingsAdapter的对象,整个源码搜遍,找不到任何相关的东西,看来还是没有源码。这些可能也是谷歌Chrom浏览器的一些核心技术了,如果有哪位精通的,请指点我一下。

Android混合开发全解析

     我们先来看一下整个代码的执行逻辑:

Android混合开发全解析

     重点的地方我也标红出来了,整个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();
}
     调用super父类的构造方法来初始化一些成员变量,这个过程我们就不分析了,跟之前动画全解析中的初始化的道理基本是一样的。最重要的就是ensureProviderCreated()这句了,它是对成员变量mProvider进行赋值的,这个mProvider也就是WebView最核心的东西了。ensureProviderCreated方法当中先通过调用getFactory()来获取一个WebViewFactoryProvider对象,然后再用它的createWebView方法为给成员变量mProvider赋值,getFactory方法中的实现又是调用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) {
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);
}
}
}
     这个方法的返回值是WebViewFactoryProvider,它是一个接口,那么在这个过程中,肯定是生成了一个实体对象的,可以看到try代码块中的逻辑,调用getProviderClass()方法获取到一个providerClass,然后通过反射调用providerClass.getConstructor(WebViewDelegate.class).newInstance(new WebViewDelegate())来生成一个该类的对象sProviderInstance返回给调用者。那我们接下来就看一下getProviderClass方法的实现:

    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);
}
}
     首先调用loadNativeLibrary()加载动态库,然后调用getChromiumProviderClass方法去生成我们要的class对象,那么继续跟踪看一下这个方法的实现:

    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);
}
}
     在这个方法中,我们就看到要加载的目标类CHROMIUM_WEBVIEW_FACTORY,也就是我们流程图中红色标注的重点,生成这个类是用的当前webViewContext的类加载器来生成的,在Java的双亲委派模型中,大家可以知道,使用instanceof判断两个类是否相同时,相同的条件有两个:1、类的路径完全相同;2、加载这个类的加载器完全相同,只有这两个条件同时满足,instanceof判断才会为true,而最终的重量级类WebViewChromiumFactoryProvider没有源码,这样我们后边的很多分析过程也就无从得知了。

     上面的过程为我们创建了最重要的处理类,后边的逻辑基本就很简单了,继续调用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格式化整齐看一下,如下图:

Android混合开发全解析

     看到javascript的角本看是头大了,语法好乱,可能也是自己不懂吧,以后还得好好学习。那么当我们在H5页面上点击的时候,就会通过javascript角本处理,然后回调WebView的onJsPrompt方法,也就是InjectedChromeClient类的onJsPrompt方法了,相应的参数都会通过这个方法中的message参数以string字符串的形式传过来,我们可以看一下获取IMSI方法的参数:

Android混合开发全解析

    这样就能保证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拼接出来的