作者:Tamic 更多文章关注开发者技术前线
上一篇<JsCore 原理和实践>
谷歌提供的系统组件WebView,用来加载和展现html网页,其采用webkit内核驱动,来实现网页浏览功能。 webkit的原理看上篇。Android从4.4 开始用Chromium动态加载实现网页渲染引擎。
Chromium 渲染引擎实际上是由Browser进程、Render进程和GPU进程组成的。其中,Browser进程负责将网页解析的UI合成 加速展现在页面上,Render 进程 只要负责加载和渲染网页,GPU 进程 负责 Browser 进程 和Render进程发出的GPU消息。
我们的创建WebView时候就会创建启动Chromium ,Chromium中有webCore引擎和JsCore引擎。
WebCore负责对HTML解析,CSS解析,渲染UI,调试信息等部分。
本篇介绍Android和JS交互几种方式,和基础核心类 JavascriptInterface。 JavascriptInterface是提供的一个JS引擎上一个接口,和WebView 一起 Android加载H5的重要组成部分。
Android Browser 用来主要用WebView来加载和渲染html网页,来实现网页浏览功能。 webkit的原理看上篇。
主要从网页的 URL 到构建完 DOM 树,接着 从 DOM 树到构建完 WebKit 的绘图上下文,从绘图上下文到生成最终的UI图像。
WebView 拥有load() URL和本地html文件的功能。
// 云端
webView.loadUrl("https://www.baidu.com");
// 本地
webView.loadUrl("file:///android_asset/demo.html");
WebViewClient主要辅助WebView执行处理各种响应请求事件的,比如:
onLoadResource
onPageStart
onPageFinish
onReceiveError
onReceivedHttpAuthRequest
shouldOverrideUrlLoading
这个方法的本来是拦截所有WebView的Url跳转的。开发者可以通过这个API 构造一个特殊自定义格式的Url跳转,shouldOverrideUrlLoading拦截Url后判断其格式,然后Native 解析到对应的方法去 能执行本地方法的功能。
public class BirdgeClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
if (isJsBridgeUrl(url)) {
handle(url)//自定义的本地处理逻辑
return true;
}
return super.shouldOverrideUrlLoading(view, url);
}
}
这个类提供还提供了2个特别的函数。供Js和Java交互。
1. WebChromeClient.onConsoleMessage()
2. WebChromeClient.onJsPrompt()
Android系统 提供了 Javascript 在调试Native代码里面打印日志的API,同时这个API也成了其中一种Javascript与Native代码交互的方式。
在 Javascript 代码中调用 console.log(‘xxx’)方法()。
console.log('我是tamic')
Native代码会回调 WebChromeClient.consoleMessage()。
public class BridgeClient extends WebChromeClient {
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
super.onConsoleMessage(consoleMessage);
String msg = consoleMessage.message();
}
}
consoleMessage.message() 获得的正是Javascript代码console.log(‘tamic’)的内容。
开发中一般通过将consoleMessage 做约束,通过key和Json对象的字符串来做通讯数据格式。
除了onJsPrompt(),还有onJsAlert() 和onJsConfirm()。顾名思义,这三个方法是Javascript给Native代码的回调接口的作用分别是提示信息,提示警告信息和展示确认信息。鉴于,alert和confirm在Javascript的使用率比较高,目前JSBridge框架 用的onJsPrompt()比较多。
JS中要三个API,alert(),confirm()和prompt() 用来个用户提示信息的。
JS中调用
window.prompt(message, value)
WebChromeClient.onJsPrompt()就会收到回调信息。
public classChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 处理JS 的调用逻辑
result.confirm();
return true;
}
}
onJsPrompt()的message的值正是JS的方法window.prompt()的message的值。
上面的代码块中的JsPromptResult是Js和native处理结果的重要部分。
JsPromptResult
是对js结果的回调包装类。该类主要只负责处理Js回调结果。
该对象用作底层javascript发起请求的句柄,并为客户端提供指示是否应继续执行此操作的方法。此类的一个实例在在 webcromeclient
操作中作为参数传递。
JsPromptResult提供了cancel()
,confirm()
JsPromptResult
拦截ResultReceiver
等。
ResultReceiver是结果接收器, 他是在AMS初始化时候创建的。 WebChromeClient 通过自己的quene()做消息循环。 实际是我们的WebViewChromium 实现用创建AwBrowserProcess的来执行将消息取出来。
我们可以借助这些API 巧妙的实现对js的数据的处理。
开发者都知道安卓API 4.4以前谷歌的webview存在安全漏洞,网站可以通过js注入就可以随便拿到客户端的重要信息,甚至轻而易举的调用本地代码进行流氓行为,谷歌后来发现有此漏洞后,增加了防御措施,如果要是js调用本地代码,开发者必须在代码申明JavascriptInterface,
源码如下:
@SuppressWarnings("javadoc")
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface JavascriptInterface {
}
列如在4.2之前我们要使得webView加载js只需如下代码:
mWebView.addJavascriptInterface(new JsBridge(), "JsBridge");
JsBridge()是JavascriptInterface实现类,JsBridge是我们给的别名,也就是Js能难道的java端的桥对象。
4.2之后调用需要在调用方法加入加入@JavascriptInterface
注解,如果代码无此申明,那么也就无法使得js生效,也就是说这样就可以避免恶意网页利用js对android客户端的窃取和攻击。
即使这样,我们很多时候需要在js加载本地代码的时候,要做一些判断和限制,或者有可能也会做些过滤和对用户友好提示!
android中的Js和Java的通讯主要流程
addJavascriptInterface 实际上由 WebViewProvider
来执行实现,WebViewProvider主要负责提供WebView后台程序接口, 每个WebView对象仅绑定到一个实现。
public void addJavascriptInterface(Object obj, String interfaceName);
public void removeJavascriptInterface(String interfaceName)
;
第一步addJavascriptInterface时候 只有Browser 进程才能有权限,否则会抛异常
源码部分:
private void checkThread() {
if (mWebViewThread != null && Looper.myLooper() != mWebViewThread) {
Throwable throwable = new Throwable(
"A WebView method was called on thread '" +
Thread.currentThread().getName() + "'. " +
"All WebView methods must be called on the same thread. " +
"(Expected Looper " + mWebViewThread + " called on " + Looper.myLooper() +
", FYI main Looper is " + Looper.getMainLooper() + ")");
Log.w(LOGTAG, Log.getStackTraceString(throwable));
StrictMode.onWebViewMethodCalledOnWrongThread(throwable);
if (sEnforceThreadChecking) {
throw new RuntimeException(throwable);
}
}
}
第二步
WebViewProvider提供的这两个抽象方法实际上是WebViewFactoryProvider创建出来的具体WebViewChromiumFactoryProvider实现的。
第三步 WebViewChromiumFactoryProvider创建WebViewChromium去实现。
源码:
class WebViewChromium implements WebViewProvider,
WebViewProvider.ScrollDelegate, WebViewProvider.ViewDelegate {
......
public void init(final Map<String, Object> javaScriptInterfaces,
final boolean privateBrowsing) {
......
// We will defer real initialization until we know which thread to do it on, unless:
// - we are on the main thread already (common case),
// - the app is targeting >= JB MR2, in which case checkThread enforces that all usage
// comes from a single thread. (Note in JB MR2 this exception was in WebView.java).
if (mAppTargetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
mFactory.startYourEngines(false);
checkThread();
} else if (!mFactory.hasStarted()) {
if (Looper.myLooper() == Looper.getMainLooper()) {
mFactory.startYourEngines(true);
}
}
......
mRunQueue.addTask(new Runnable() {
@Override
public void run() {
initForReal();
......
}
});
}
......
}
第四步。 WebViewChromium 实现用创建AwBrowserProcess的来执行BrowserStartupController的执行Js的函数,这个函数是个JNI方法,
static void SetCommandLineFlags(JNIEnv* env,
jclass clazz,
jint max_render_process_count,
jstring plugin_descriptor) {
std::string plugin_str =
(plugin_descriptor == NULL
? std::string()
: base::android::ConvertJavaStringToUTF8(env, plugin_descriptor));
SetContentCommandLineFlags(max_render_process_count, plugin_str);
JavascriptInterface 添加必须要准备一个JavascriptInterface的注解方法,开发者可准备一个简单的JsBridge :
public class JsBridge {
@JavascriptInterface
public void showToast(String toast) {
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
log("show toast sucess");
} else {
log("show toast error");
}
}
public void log(final String msg) {
javaCallJS("log", msg);
/* webView.post(new Runnable() {
@Override
public void run() {
webView.loadUrl("javascript: log(" + "'" + msg + "'" + ")");
}
});*/
}
/**
* js方法名 參數
* @param method
* @param parameter
*/
public void javaCallJS(String method, String parameter) {
webView.loadUrl("javascript:" + method + "('" + parameter + "')");
}
}
android页面通过webView. addJavascriptInterface中加入绑定桥。
WebSettings webSettings = webView.getSettings();
webSettings.setJavaScriptEnabled(true);
webView.addJavascriptInterface(new JsBridge(), "JsBridge");
webView.loadUrl("url");
在js调Java弹一个toast, Js代码
<script type="text/javascript">
function showToast(toast) {
javascript:jsbrige.showToast(toast);
}
</script>
接着 可以js可以通过javascript:jsbrige.showToast ()。来调用上面准备好的桥实现。这样就完成了js调用java的toast功能。
我们在Native中通过返回toast是否是成功的案列给Js给一个返回值。接着定义一个通用实现;
public void javaCallJS(String method, String parameter) {
webView.loadUrl("javascript:" + method + "('" + parameter + "')");
}
假如js中有一个Log方法如下:
function log(msg){
console.log(msg);
}
接着通过判断当前线程是否是UI线程来告知js是否执行Toast 成功。
@JavascriptInterface
public void showToast(String toast) {
if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
Toast.makeText(MainActivity.this, toast, Toast.LENGTH_SHORT).show();
log("show toast sucess");
} else {
log("show toast error");
}
}
本节主要学习了Js和natvie交互三种方式。其实上面介绍的案列就是一个简单的jsBridge的雏形,当然目前企业级的JsBridge 不止这简单,接下来下篇学习webVIew后,再给大家介绍Jsbrige的实现原理。