MXBridge - 插件式JS与OC交互框架
概述
MXBridge,提供一个插件式的JavaScript与Objective-C交互的框架,通过JavaScriptCore实现,插件式扩展Obejctive-C接口以供JavaScript调用.前往Github查看
主要的类
大致画了一下类图:

结合上图,先介绍一下这里几个类的方法:
- UIWebView(MXBridge) : category,持有一个
MXWebViewDelegateProxy以截获UIWebView的页面加载的回调,以触发JS注入和bridge环境初始化的操作. - MXWebviewDelegateProxy : 委托的代理.持有一个真正的
UIWebViewDelegate,并持有一个MXWebViewBridge,这样将bridge与UIWebView绑定在一起,一个UIWebView中只有一个bridge,并跟随UIWebView的释放一起释放代理和bridge. - MXWebViewBridge : 与JS交互,主要通过这个桥来实现. 持有
JSContext,也就是当前WebView的JS运行环境.通过JSExport暴露三个接口供JS直接调用,同时持有一个从js中获取的jsBridge对象,即对应了JS代码中的JSBridgeForOC以供异步回调时调用JS代码. 除了一个setupJSContext的初始化Webview的JS环境的方法外,还有一个cleanJSContext,在UIWebView释放时,释放持有的JS对象指针,以使对象正常释放. - MXWebViewConntext : 一个单例的全局上下文,放置一些全局的系统信息,以及加载
mxbridge.js的代码以字符串的形式放在内存中. 还持有一个插件列表,插件列表的信息放在应用中的plugins.plist文件中,以键值对形式储存插件名和插件对应的OC类名.还有一个setUp方法,用于初始化MXBridge的功能,调用这个方法后,会通过Method Swizzling来为应用中所有的UIWebView赋予该功能. - MXWebViewPlugin : 插件,所有OC对JS所提供的方法,都是基于插件的形式,即用户实现一个插件,然后JS代码就可以根据插件名和插件方法名来调用这个插件的功能.
- MXMethodInvocaton : 方法调用,JS对OC的一次方法调用中,将参数以及调用信息记录在这个Model中.
实现原理
结合上图,来介绍一下MXBridge的实现原理,在介绍实现原理之前,建议先去学习一下JavaScriptCore的使用方法,MXBridge是基于JavaScriptCore实现的,所以只支持iOS7以上的设备.
通过Method swizzling来替换了UIWebView的三个方法的实现:
- (instancetype)mx_initWithFrame:(CGRect)frame {
[self mx_initWithFrame:frame];
if (self) {
[self mx_setup];
}
return self;
}
- (nullable instancetype)mx_initWithCoder:(NSCoder *)aDecoder {
[self mx_initWithCoder:aDecoder];
if (self) {
[self mx_setup];
}
return self;
}
- (void)mx_setDelegate:(id)delegate {
// 设置上真正的代理。
if ([self.delegate isKindOfClass:[MXWebviewDelegateProxy class]]) {
((MXWebviewDelegateProxy *)self.delegate).realDelegate = delegate;
}else {
[self mx_setDelegate:delegate];
}
}
在初始化UIWebView的时候,就会为webview添加一个 MXWebviewDelegateProxy对象作为webviewDelegate,而在使用者使用 setDelegate方法时,将要设置的delegate赋值给MXWebviewDelegateProxy对象的realDelegate属性,以让这个设置的delegate能够正常运行.
method swizzling的执行是放在MXwebViewContext的setUp方法中的,这个方法作为在整个应用中初始化MXBridge环境,初始化后才能在应用里的UIWebView中进行JavaScript和Objective-C之间的交互.
而设置代理的主要目的,是为了给UIWebView当前界面的JSContext注入我们的MXBridge.js,以获取交互功能. 在JavaScriptCore中JS代码都是执行在JSContext这个运行环境中的,JSContext表示JS代码在OC中的运行环境,我们可以通过这个环境以执行JS代码,或者让JS直接调用OC方法,具体关于JavaScriptCore的一些简介,可以看一下这篇简陋的文章.
我们要获取这个JSContext,可以通过KVC :
JSContext *context = [_webview valueForKeyPath: @"documentView.webView.mainFrame.javaScriptContext"];
但是UIWebView中的这个JSContext是一直在变化的,我们通过观察,可以发现,在UIWebViewDelegate的三个状态中shouldStartLoadWithRequest , webViewDidStartLoad 和 webViewDidFinishLoad时,UIWebView的JSContext都是指向不同地址,对于这个问题,我们一开始是选取最后一个状态,即webViewDidFinishLoad中的JSContext来使用,因为这个JSContext也是UIWebView加载结束后一直使用的JSContext.所以我们设置一个delegateproxy对象,以获取webViewDidFinishLoad事件,在此时将所需的js注入到从UIWebView中获取的JSContext中,就可以赋予JS与OC交互的功能,而这个阶段的主要操作就是 :
// 获取js执行环境
JSContext *context = [_webview valueForKeyPath: @"documentView.webView.mainFrame.javaScriptContext"];
// 注入bridge.JS
[_context evaluateScript:[MXWebviewContext shareContext].bridgeJS];
// 从js环境中获取 JSbridgeForOC, 在MXWebviewBridge中持有
_jsBridge = [_context[@"mxbridge"] valueForProperty:@"JSbridgeForOC"];
// 将MXWebviewBridge放入js的环境中,由mxbridge持有
[_context[@"mxbridge"] setValue:self forProperty:@"OCBridgeForJS"];
但由于webViewDidStartLoad的限制,我们的mxbridge必须在页面加载完成后,才会初始化完成,而js有些代码会在页面加载过程中执行,为了处理这个时间差,我们有一个变量来表示mxbridge的加载状态,即mxbridge.isReady, 还有一个bridgeReady的Event会在初始化完成时发送出去.所以js调用插件时,首先需要检测mxbridge.isReady,如果mxbridge没有成功初始化,就需要等待bridgeReady事件发生了. 如:
execSafely : function (pluginName, functionName, args,successCallback,failCallback) {
if (window.mxbridge && window.mxbridge.isReady) {
window.mxbridge.exec(pluginName, functionName, args,successCallback,failCallback);
} else {
document.addEventListener("bridgeReady", function() {
window.mxbridge.exec(pluginName, functionName, args,successCallback,failCallback);
}, true);
}
},
继续讨论实现原理,刚才说到初始化js环境,OC端持有一个JS的桥,而JS端也持有了一个OC端的桥,这样我们就可以使用JavaScriptCore相关的知识进行两者之间的交互了.
Objective-C供JavaScript持有一个MXWebviewBridge对象,而这个对象实现了一个继承了JSExport协议的MXNativeBridgeExport ,继承JSExport后,可以将OC中的方法直接在JS中使用,所以提供了三个接口给JS使用:
// 在Native端打日志,方便调试
- (void)loggerWithLevel:(NSArray *)arguments;
// 异步调用插件
- (void)callAsyn:(NSDictionary *)arguments;
// 同步调用插件
- (JSValue *)callSync:(NSDictionary *)arguments;
JavaScript通过callAsyn:和callSync:调用OC提供的插件,这两个函数中的具体实现,也比较简单,以callAsyn:举例说明一下:
- (void)callAsyn:(NSDictionary *)arguments {
dispatch_async(dispatch_get_main_queue(), ^{
// 在主线程中执行。
MXMethodInvocation *invocation = [[MXMethodInvocation alloc] initWithJSCall:arguments];
if (invocation == nil) {
NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_PLUGIN_INIT_FAILED,@"errorMsg":@"传递参数错误,无法调用函数!"};
NSLog(@"异步调用 ,失败 %@",error);
}
MXWebviewPlugin *plugin = _pluginDictionarys[invocation.pluginName];
if (!plugin) {
Class cls = [MXWebviewContext shareContext].plugins[invocation.pluginName];
if (cls == NULL) {
NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_PLUGIN_NOT_FOUND,@"errorMsg":[NSString stringWithFormat:@"插件 %@ 并不存在 ",invocation.pluginName]};
[self callBackSuccess:NO withDictionary:error toInvocation:invocation];
}
plugin = [[cls alloc] initWithBridge:self];
_pluginDictionarys[invocation.pluginName] = plugin;
}
// 调用 插件中相应方法
SEL selector = NSSelectorFromString(invocation.functionName);
if (![plugin respondsToSelector:selector]) {
selector = NSSelectorFromString([invocation.functionName stringByAppendingString:@":"]);
if (![plugin respondsToSelector:selector]) {
NSDictionary *error = @{@"errorCode":MXBridge_ReturnCode_METHOD_NOT_FOUND_EXCEPTION,@"errorMsg":[NSString stringWithFormat:@"插件对应函数 %@ 并不存在 ",invocation.functionName]};
[self callBackSuccess:NO withDictionary:error toInvocation:invocation];
}
}
// 调用插件
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[plugin performSelector:selector withObject:invocation];
#pragma clang diagnostic pop
});
}
上段代码中,当JS调用OC的数据传到后,先将调用数据转换为一个MXMethodInvocation对象,然后检测参数合法性. 然后检测插件是否存在,不存在则去创建插件,但插件不在plugins.plist中或者类不存在,也会有相应地错误提示.拿到插件后,就可以根据方法名和js传递的参数调用插件相应地方法了.
对于异步调用的插件,js调用时,会传递调用成功和失败的回调 :
var list = {
"success":successCallback,
"fail":failCallback
};
mxbridge.JSbridgeForOC.callBackLists[jscall.invocationID] = list;
bridge将成功失败的回调以 一次调用的唯一标示记录在JSbridgeForOC, 而在异步回调JavaScript的处理函数时,也就是调用JSbridgeForOC的callbackAsyn方法时,就会从callBackLists中找到对应的回调函数,以执行相应的回调:
callbackAsyn : function (callbackID,status,args) {
// 执行异步调用,然后OC对JS的调用立即返回。
window.setTimeout(function() {
mxbridge.JSbridgeForOC._callbackAsyn(callbackID,status,args);
},0);
},
// 真正的回调函数.
_callbackAsyn : function(callbackID , status ,args) {
var callbackfuns = mxbridge.JSbridgeForOC.callBackLists[callbackID];
if (callbackfuns) {
if (status == mxbridge.OK) {
callbackfuns.success && callbackfuns.success(args);
} else {
callbackfuns.fail && callbackfuns.fail(args);
}
delete mxbridge.JSbridgeForOC.callBackLists[callbackID];
};
}
使用步骤
- 导入代码.
创建插件 ,插件的编写要注意以下几点 :
- 继承 MUWebviewPlugin 类,这个类中提供了几个在插件中常用的属性,
bridge,containerVC和webView,一些异步时的回调函数,如- (void)callBackSuccess:(BOOL)success withDictionary:(NSDictionary *)dict toInvocation:(MUOCMethodInvocation *)invocation;和- (void)callBackSuccess:(BOOL)success withString:(NSString *)string toCallbackID:(NSString *)callbackID;,返回给JS的值,可以是一个字符串,也可以是以NSDictionary表示的 JSON对象. - (instancetype)initWithBridge:(MUWebviewBridge *)bridge,可以在这个初始化函数中作一些初始化的操作.- 对于插件方法,形式是这样的 :
- (NSDictionary *)syncFunction(:(MUOCMethodInvocation *)invocation);,同步方法返回值必须是NSDictionary *,而参数可以有也可以没有. 对于异步方法- (void)asynFunction(:(MUOCMethodInvocation *)invocation),返回值类型为void,参数也可以有,可以没有. 传递的参数放在MUOCMethodInvocation中.
- 继承 MUWebviewPlugin 类,这个类中提供了几个在插件中常用的属性,
- 创建
plugins.plist文件,以 键值对的形式,插件名和插件类名的对应关系. - 在需要该功能时,调用
MUWebviewContext的setUp方法,激活功能,使项目中所有的webview都能进行交互. 在
MUWebViewContext中提供了几个接口,以供设置 :- appName,appVersion,osType,osVersion ,等应用系统信息.
loggerBlock,一个打日志的block,用于调试JS..
注意事项
- JS调用插件,传递的参数是json对象的形式.而调用参数传递到插件中时,是以
NSDictionary的形式.同理,在回调JS时,OC传递的类型是NSDictionary,而到达JS的返回值是 json对象. 这与JavaScriptCore相关. - 在
UIWebView页面加载完成时,才会初始化MXBridge以支持插件调用功能,所以,调用插件前,要进行检测,以确保mxbridge已经初始化完成.
MXBridge - 插件式JS与OC交互框架的更多相关文章
- 史上最全的 UIWebview 的 JS 与 OC 交互
来源:伯乐在线 - 键盘风筝 链接:http://ios.jobbole.com/89330/ 点击 → 申请加入伯乐在线专栏作者 其实一直想给大家整理一下JS与OC的交互,但是没有合适的机会,今天借 ...
- JS与OC交互--简单使用
直接上代码 .m文件 #import "ViewController.h" @interface ViewController () <UIWebViewDelegate&g ...
- 转载 【iOS开发】网页JS与OC交互(JavaScriptCore) OC ----->JS
目标 本文介绍利用苹果在iOS7时发布的JavaScriptCore.framework框架进行js与OC的交互.我们想要达到的目标是: OC调用网页上的js方法 网页js调用APP中的OC方法 ...
- UIWebView中JS与OC交互 WebViewJavascriptBridge的使用
一.综述 现在很多的应用都会在多种平台上发布,所以很多程序猿们都开始使用Hybrid App的设计模式.就是在app上嵌入网页,只要写一份网页代码,就可以跑在不同的系统上.在iOS中,app多是通过W ...
- iOS JS 和 OC交互 / JS 和 native 相互调用
现在app 上越来越多需求是通过UIWebView 来展示html 或者 html5的内容, js 和 native OC代码交互 就非常常见了. js 调用 native OC代码 第一种机制 ( ...
- JS 与OC 交互篇
完美记录交互 CSDN博客: (OC调用JS) http://blog.csdn.net/lwjok2007/article/details/47058101 (JS调用OC) http://blog ...
- 利用WKWebView实现js与OC交互注意事项
最近在写一些关于wkwebview的一些代码,发现了几点心得,记录一下. 1.js调用OC 我是利用wkwebview进行的开发实现,主要代码有三部分 1.向config注入OC对象 [config. ...
- JS与OC交互,JS中调用OC方法(获取JSContext的方式)
最近用到JS和OC原生方法调用的问题,查了许多资料都语焉不详,自己记录一下吧,如果有误欢迎联系我指出. JS中调用OC方法有三种方式: 1.通过获取JSContext的方式直接调用OC方法 2.通过继 ...
- JS 与 OC 交互
WebView与JS的几种交互 IOS中 使用JavaScriptCore 实现OC与JS的交互 JavaScriptCore 使用
随机推荐
- 转:Java IO流学习总结
Java流操作有关的类或接口: Java流类图结构: 流的概念和作用 流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象.即数据在两设备间的传输称为流,流的本质是数据传输,根据数据传输 ...
- 24点游戏详细截图介绍以及原型、Alpha、Beta对比
原型设计 图片展示 功能与界面设计 1.登录注册 2.手机号验证 3.24点游戏 4.粉色系女生界面 Alpha 图片展示 功能与界面设计 1.24点游戏 2.背景音乐 3.可查看多种可能的答案 4. ...
- 201521123074 《Java程序设计》第8周学习总结
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 本次作业题集集合 Q1.List中指定元素的删除(题目4-1) 1.1 实验总结 用Arrar ...
- 201521123039《Java程序设计》 第六周学习总结
1. 本周学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 1.2 可选:使用常规方法总结其他上课内容. 答:1.cl ...
- 201521123092《java程序设计》第五周学习总结
1.本周学习总结 #1.1 尝试使用思维导图总结有关多态与接口的知识点. 2.书面作业 #1.代码阅读:Child压缩包内源代码##1.1 com.parent包中Child.java文件能否编译通过 ...
- [BT5]信息收集1-1 Dnsenum
0.工具介绍 The purpose of Dnsenum is to gather as much information as possible about a domain. The progr ...
- ubuntu下php不能显示中文的问题的解决过程。
在阿里的ECS上的ubuntu平台上成功的安装了apache2和php5与mysql,并进行了测试. 如图所示:
- Java程序设计——学生基本信息管理系统(团队+个人)
学生信息管理系统(From:单身贵族) 团队部分 一.团队介绍("单身贵族"): 吴剑通[组长]:201521123056,唯一队员,网络1512班,团支书 二.项目git地址 三 ...
- 201521123009《Java程序设计》第14周学习总结
1. 本周学习总结 2. 书面作业 1. MySQL数据库基本操作 建立数据库,将自己的姓名.学号作为一条记录插入.(截图,需出现自己的学号.姓名) 在自己建立的数据库上执行常见SQL语句(截图) - ...
- Java多线程高并发学习笔记(三)——深入理解线程池
线程池最核心的一个类:ThreadPoolExecutor. 看一下该类的构造器: public ThreadPoolExecutor(int paramInt1, int paramInt2, lo ...