MUI的原生与网页交互研究
本文由 小茗同学 发表于 2016-06-27 浏览(3129)
最后修改 2016-07-15 标签:mui android webview 交互 javascript

基本介绍

MUI首页:http://dev.dcloud.net.cn/mui/
HTML5+SDK首页:http://www.html5plus.org/doc/h5p.html
DCloud首页:http://www.dcloud.io/

注入

一开始一直很好奇,mui的类似plus.device.getVolume()这样的三级方法是怎么注入的,因为安卓中注入时mWebView.addJavascriptInterface(object, name)中的name是不能有“.”的:

//这种写法是错误的,注入的方法会调用不了
mWebView.addJavascriptInterface(object, 'plus.device');

今天无意中在调试的时候发现,输入plus.device.getVolume时返回的不是[native code],而是具体的代码,第一反应就是这个代码是在js里面写好的,而不是注入的,但是查看前端代码没有发现类似的js,然后就猜想这个js是不是放到安卓那边去了,于是就到HTML5+sdk中找到一个名为pdr.jar的文件,最后还真在里面发现了一个压缩过的js:

打开一看,还真是,所有的plus.xxx.xxx代码都在里面,只是因为加密了,看起来比较费力。

通过全局搜索找到了如下代码:

具体是怎么把这个js注入进去的呢?具体完整流程我没有走通,但是整体思路应该是下面这样的。

io.dcloud.feature.b.java有如下方法,以下方法将all.js的内容读取到了this.f里面:

/*  77 */   public b(ICore paramICore) { super(paramICore, "featuremgr", IMgr.MgrType.FeatureMgr);
/*     */     try
/*     */     {
/*  80 */       if ((BaseInfo.ISDEBUG) && (DHFile.isExist("/sdcard/dcloud/all.js")))
/*  81 */         this.f = new String(PlatformUtil.getFileContent("/sdcard/dcloud/all.js", 2));
/*     */       else
/*  83 */         this.f = new String(PlatformUtil.getFileContent(BaseInfo.sRuntimeJsPath, 1));
/*     */     }
/*     */     catch (IOException localIOException)
/*     */     {
/*  87 */       localIOException.printStackTrace();
/*     */     }
/*     */   }

然后就是这个方法:

/*     */   private String a(IApp paramIApp, IFrameView paramIFrameView)
/*     */   {
/* 211 */     IWebview localIWebview = paramIFrameView.obtainWebView();
/* 212 */     String str1 = "__load__plus__";
/* 213 */     StringBuffer localStringBuffer = new StringBuffer("javascript:");
/* 214 */     localStringBuffer.append("function ").append(str1).append("(){try{");
/* 215 */     localStringBuffer.append("window._____isDebug_____=" + BaseInfo.ISDEBUG + ";");
/* 216 */     localStringBuffer.append("window._____platform_____=1;");
/* 217 */     localStringBuffer.append(this.f); // 这里很关键的代码,将all.js的内容进行了拼接
                // 省略大部分代码
/* 268 */     return localStringBuffer.toString();
/*     */   }

WebLoadEvent中的onPageFinished调用了一个名为loadAllJSContent的方法:

/*     */   private void loadAllJSContent(WebView paramWebView, String paramString1, String paramString2)
/*     */   {
/* 327 */     onLoadPlusJSContent(paramWebView, paramString1, paramString2);
/*     */ 
/* 329 */     onPreloadJSContent(paramWebView, paramString1, paramString2);
/*     */ 
/* 331 */     onPlusreadyEvent(paramWebView, paramString1, paramString2);
/*     */ 
/* 333 */     onExecuteEvalJSStatck(paramWebView, paramString1, paramString2);
/*     */   }

好家伙,找了这么久终于找到mui注册事件的代码了:

/*     */   private void onPlusreadyEvent(WebView paramWebView, String paramString1, String paramString2)
/*     */   {
/* 273 */     StringBuffer localStringBuffer1 = new StringBuffer();
/* 274 */     localStringBuffer1.append(String.format("javascript:(function(){if((!window.plus) || (window.plus && (!window.plus.isReady))){window.__load__plus__&&window.__load__plus__();}var e = document.createEvent('HTMLEvents');var evt = '%s';e.initEvent(evt, false, true);/*console.log('dispatch ' + evt + ' event');*/document.dispatchEvent(e);})();", new Object[] { "plusready" }));
/* 275 */     StringBuffer localStringBuffer2 = new StringBuffer();
/* 276 */     localStringBuffer2.append(String.format("(function (){var b,c,d,e,a=document.getElementsByTagName('iframe');if(a && a.length) for(b=0;b<a.length;b++)c=a[b],d=c.contentWindow.document.createEvent('HTMLEvents'),e='%s',d.initEvent(e,!1,!0),c.contentWindow.plus=window.plus,c.contentWindow.document.dispatchEvent(d)})();", new Object[] { "plusready" }));
/* 277 */     completeLoadJs(paramWebView, paramString1, paramString2, new String[] { localStringBuffer1.toString(), localStringBuffer2.toString() }, "(function(){/*console.log('plusready event loading href=' + location.href);*/if(location.__page__load__over__){return 2;}if(location.href.indexOf('%s') >= 0 || (location.href.startsWith && location.href.startsWith('data:'))){if(location.__plusready__){if(!location.__plusready__event__){location.__plusready__event__=true;return 1;}else{return 2;}}}else if(location.__plusready__event__){return 2;} return 0;})();", new String[] { paramString1 });
/*     */   }

这其中,最关键的代码:

var e = document.createEvent('HTMLEvents');
e.initEvent('plusready', false, true);
document.dispatchEvent(e);

交互

这里简单介绍几个mui交互的方法:

plus.bridge.execSync

        execSync: function(service, action, args, fn) {
            var json, sync, ret;
            if (T.IOS == T.platform) {
                try {
                    if (json = T.stringify([[window.__HtMl_Id__, service, action, null , args]]),
                    sync = B.synExecXhr,
                    sync.open("post", "http://localhost:13131/cmds", !1),
                    sync.setRequestHeader("Content-Type", "multipart/form-data"),
                    sync.send(json),
                    fn)
                        return fn(sync.responseText)
                } catch (e) {
                    console.log("sf:" + action + "-" + service)
                }
                return window.eval(sync.responseText)
            }
            return T.ANDROID == T.platform ? (ret = window.prompt(T.stringify(args), "pdr:" + T.stringify([service, action, !1])),
            fn ? fn(ret) : eval(ret)) : void 0
        },

根据字面意思理解,这个方法是JS同步执行一个原生方法,看代码最后发现,这个竟然是用prompt来实现的:

prompt(JSON.stringify([]), 'pdr:'+JSON.stringify(['Device', 'getVolume', false]))
// 输出:"(function(){return 0.46666667;})()"

可能是因为prompt这个方法是同步的原因吧。

有一些并不是all.js中的

比如plus.device.xxx,plus.screen.xxx,这些在all.js中并不存在,这些方法(或者属性)应该是动态生成的。

系统事件的实现

说明
本小节写在上面all.js注入机制之前,所以当时还不明白mui的事件是如何注入的,已经明白的读者可以跳过本小节。

mui的很多事件可以像浏览器自带的事件那样,直接使用addEventListener完美监听,一直想知道它底层是如何实现的,不过到目前为止还没有搞透,主要是不但没有源码,而且源码都是混淆过的,反编译之后很难看懂。

document.addEventListener( "netchange", netchangeCallback, capture );

通过在prd.jar中搜索netchange,最后只是找到了一个注册系统事件的方法,在io.dcloud.common.a.d.java里面:

/*      */   public void registerSysEventListener(ISysEventListener paramISysEventListener, ISysEventListener.SysEventType paramSysEventType)
/*      */   {
/*  908 */     if (this.y == null) {
/*  909 */       this.y = new HashMap(1);
/*      */     }
/*  911 */     ArrayList localArrayList = (ArrayList)this.y.get(paramSysEventType);
/*  912 */     if (localArrayList == null) {
/*  913 */       localArrayList = new ArrayList();
/*  914 */       this.y.put(paramSysEventType, localArrayList);
/*      */     }
/*  916 */     localArrayList.add(paramISysEventListener);
/*      */   }

这些方法都在io.dcloud.common.DHInterface.IApp的抽象类里面,d.java实现了IApp,另外,在io.dcloud.common.adapter.ui.WebLoadEvent.java中找到如下代码:

/* 368 */     if (i != 0) {
/* 369 */       localObject = String.format("javascript:(function(){var b=document.createEvent('HTMLEvents');var a='%s';b.url='%s';b.href='%s';b.initEvent(a,false,true);console.error(a);document.dispatchEvent(b);})();", new Object[] { "error", this.mAdaWebview.getOriginalUrl(), this.mAdaWebview.errorPageUrl });
/* 370 */       this.mAdaWebview.executeScript((String)localObject);
/* 371 */       this.mAdaWebview.errorPageUrl = null;
/* 372 */       this.mAdaWebview.hasErrorPage = false;
/*     */     }

可以发现,mui可能也是使用js自定义事件然后触发的。

另外有一个io.dcloud.common.adapter.ui.AdaWebview.executeScript方法,貌似执行js都是都过这个方法的,有一段代码是这样的:

/* 416 */   public MessageHandler.IMessages executeScriptListener = new MessageHandler.IMessages()
/*     */   {
/*     */     public void execute(Object paramAnonymousObject) {
/* 419 */       String str = (String)paramAnonymousObject;
/*     */ 
/* 423 */       AdaWebview.this.mWebViewImpl.loadUrl("javascript:" + str);
/*     */     }
/* 416 */   };

另外,在pdr.jar中的all.js中也没有搜索到像cordova中用到的onlineoffline的字符串,所以,mui执行js可能是采用传统的loadUrl方式来实现的。