iOS原生OC与JS互调
本文demo:git地址
由于前段时间刚刚完成一个比较复杂的本地与原生的交互应用,需求要做一个类似有道云笔记或这样的文本编辑器,在本地网页编辑各种复杂的文章然后保存到后台,需要随时从后台提取展现。现对这种混合应用如何实现,以及开发过程中遇到的坑进行一个总结。基于JavaScriptCore封装了一个新的sdk,文章开头给出了解决oc与js混合调用的demo,如果你觉得有所帮助,请给个星星。如果觉得代码有问题或者有疑惑地方请在评论区留言,我会第一时间回复。
一、理论基础
1. iOS跟JavaScript中的对象、方法
1.1 对象:OC我们已经非常熟悉了,这里稍微啰嗦下,OC是一门面向对象的动态语言,基于c语言增加一层面向对象的语法,让oc能满足所有面向对象的所有特点(封装(成员变量)、继承和多态)。oc中除了对象外还有一些基本数据类型,通过一些转换方法也可以将基本数据类型转换成对象类型,例如:NSNumber *number = [NSNumber numberWithInt:1]。
JavaScript :JS中事物都是对象:字符串、数值、数组、函数等等,JSt的对象是一种无序的集合数据类型,它由若干键值对组成。详细可参考:JS对象
1.2 方法:oc中的方法分为类方法跟对象方法,方法本身并不属于对象,而且无法当作参数传递,但是oc中可以用block当参数来传递代码块,实现回调。相对于oc,js方便很多,js中方法直接可以当作参数进行传递实现代码块的传递。
2.本地跟JS交互手段
2.1 iOS7前:OC调用JS只能通过函数stringByEvaluatingJavaScriptFromString,往JS里面注入一段代码,这里只能注入字符串,一般我们会把调用的方法写在里面,例如:
NSString *jsString = [[NSString alloc]initWithString:@"initStyle()"];[(UIWebView*)self.realWebView stringByEvaluatingJavaScriptFromString:jsString];
JS又是如何实现调用OC方法呢?其实iOS SDK 并没有原生提供 js 调用 native 代码的 API,但是 UIWebView 的一个 delegate 方法使我们可以做到让 js 需要调用时,通知 native。
- (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
这个方法可以拦截网页端发出的网络请求。具体做法就是让 js 通知 native 的方法是让 js 发起一次特殊的网络请求,PhoneGap 之前是使用加载一个隐藏的 iframe 来实现的,通过将 iframe 的 src 指定为一个特殊的 URL,实现在 delegate 方法中截获这次请求。通过分析截获的URL我们可以知道JS想干些什么基于URL的拦截进行的操作。大家现在用得比较多的是WebViewJavascriptBridge和EasyJSWebView这两个开源库,很多混合都采用的这种方式。这种方式调用OC方法,有篇文章有过介绍:关于UIWebView的总结
2.1 iOS7后:苹果开放了一个新的框架:JavaScriptCore框架
关于这类的文章网上写的很多,也很不错,这里就不重复介绍。具体可以参考:iOS7新JavaScriptCore框架介绍、JavaScriptCore使用。这里就不过多讲述了。本文主要目的是介绍基于JavaScriptCore封装新的第三方开源库:XHWebViewBridge
二、开源库XHWebViewBridge使用及介绍
1.OC调用JS
1.1)实现方法
框架初始化针对网页类型进行处理,通过初始化参数usingUIWebView:来确认初始化的网页初始化的是属于WKWebView还是UIWebView。不管是哪种类型框架对js调用只提供一个方法:
- (void)evaluateJavaScript:(NSString*)javaScriptString completionHandler:(void(^)(id,NSError*))completionHandler;
javaScriptString:需要注入的js代码 completionHandler:注入完成的回调
1.2 ) 使用场景----oc触发状态js调用方法作出响应
一般来讲我们会有这些场景:1、页面刚加载完成后传一些参数到js,网页拿到参数可以自己去请求数据或者做其他展示帮组js完成自己的初始化。2、点击本地的按钮需要网页作出相应的反应即调用js的方法。3、本地网络请求成功后,得到一些参数数据需要给网页让网页能够作出相应的回应。
1.3 )evaluateJavaScript方法解析
一般在方法里面注入一段js为一个js方法的调用(因为方法可以带参数,这样就能把oc的一下数据传递到js里面)。这里我们可能需要转变一下,一般我们oc调用某个方法必须是直接由一个类或者一个对象去调用相应的类方法或者对象方法,但js不仅可以这样还可以像c语言里面的内联函数一样直接调用(前提是这个js方法是全局方法)例如案例二直接调用js的“ocCalljsfun1”方法,
很多时候由于页面刚加载完成很多对象都还没有初始化需要oc传递过去的参数完成初始化,所以会让JS把传递初始化参数的方法写成全局的,这样就可以像案例二所示直接调用,然后带上oc需要给js的参数。
1.4 ) 传递参数注意事项
a、要注意双引号冲突,可以用单引号(JS中单引号跟双引号效果是一样的)@"initStyle('done')"或者对双引号转义一下@"initStyle(\"done\")"。
b、多个参数: 用逗号隔开例如:evaluateJavaScript:@"ocCalljsfun2('OC调用网页打印','传递一个参数')"
c、传递一个或者多个字典(字典在js里面也是对象):由于evaluateJavaScript只能注入字符串,所以我们需要办字典转变成字符串,最好的方法就是序列化,将字典专成json串然后js拿到js串后对json进行解析就能拿到准确的数据。可能是由于解析的原因字典转的json传递到js解析不错来,所以统一处理了下,不管是一个字典还是多个字典都添加进NSMutableArray里面,然后对数组进行转json这样处理后的的json是没问题的(已验证过)
array转json字符串:
//数组转json
+ (NSString*)arrayToJSONString:(NSMutableArray*)array
{
NSError*error =nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:&error];
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
returnjsonString;
}
转换完成后json会出现与html冲突的一些标签或特殊字符,所以我们对转换完成后的json需要再次处理,调用下面方法
+ (NSString*)removeQuotesFromHTML:(NSString*)html {
html = [htmlstringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
html = [htmlstringByReplacingOccurrencesOfString:@"“" withString:@"""];
html = [htmlstringByReplacingOccurrencesOfString:@"”" withString:@"""];
html = [htmlstringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
html = [htmlstringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
returnhtml;
}
这样处理后的json就可以直接传到js里面而且能保证数据的完整,js拿到后只需解析json就能得到一个数组对象,数组对象里面装一个或者多个字典对象。
至此oc调用js就没有什么问题了,不管是什么参数都能处理。
2.JS调用OC
2.1) 实现方法
由于iOS7以前的拦截URL方法过于麻烦,所以框架采用的是JavaScriptCore框架来实现js调用oc。
新建一个继承XHFunctionModule管理类,假设取名XHWebViewBridgeManager,这个新的类需要有一个继承自
+(NSString*)moduleName{
return @"XHWebViewBridgeManager";
}
这样你就往网页注入了XHWebViewBridgeManager对象,然后你在代理里面写的方法就是这个对象的方法例如你的.h文件是这样的
@protocolXHWebManagerProtocol
- (void)jsCallOC;
@end
@interfaceXHWebViewBridgeManager : XHFunctionModule
@end
在js里面就可以这样使用 XHWebViewBridgeManager.jsCallOC();这样就完成了js调用oc代码。如果是要传参数或者回调怎么办?请见本文开头demo有示例;
2.2 ) 使用场景
a.点击页面里面的按钮然后让本地处理事件,例如,点击按钮需要插一张本地图片或者需要调起本地相机等
b.网页经过某些操作获取的数据需要给本地进行后续的处理。
2.3)使用说明及注意事项
a.数据传递区别
上门就是类型对照,所以oc方法参数里面只能是上图左边类型,js调用代理里面的方法时传递上图右边相应的数据类型,大多数情况下js传给本地的字符或者对象,字符oc用NSString,对象用NSDictionary,由于js可以将方法作为参数传递,如果是js传递的是function OC里面需要用JSValue类型,例如:
上面为JS里面我们之前注入的XHWebViewBridgeManager对象调用OC代理里面的一个方法
OC在.m里面的实现用JSValue接收function类型参数,由于function里面还有参数,那么我们该如何回传这个参数?JavaScriptCore框架提供了callWithArguments这个方法,方法只能传NSArray类型,所以在js里面接收到的参数也是一个array或者叫对象(js除了基本类型都可以看着是对象)。
三、坑处理
1、在js调oc方法时,如果js传function作为参数有时候会卡屏。原因是在oc是多线程,但是刷新UI一定是在主线程里面,而js正常情况下一般只有一个线程,一旦出现js在回调里面的方法实现出现延迟(例如上传下载),而oc需要js立即返回数据否则就卡自己主线程了。解决方法:js实现里面加setTimeout方法,例如:
2.如果想注入多个对象怎么办?类似XHWebViewBridgeManager再新建新的类然后实现moduleName方法,然后返回新的对象名即可,框架会帮你注入进js的content上下文。
3.如果是页面想把所有编辑好的样式保存下来给后台,然后下次本地网页直接加载后台给的数据就能实现草稿续编功能。然而之前编辑的中文或者一些特殊符号在json序列化后会与html一些标签重合导致本地重新加载解析后的数据出现截断。解决方法:网页先保存html所有的样式,然后对样式进行编码(编码作用是对一些特殊字符、中文进行转换,转换成与html不冲突的字符),编码后然后进行json序列化存入后台。
下面是编码与解码方法
整体思路:将符号转换为ascii码,将中文进行两次encodeURI()编码。
;!function () {
String.prototype.AsciiEncode = function () {
var fh = "", dg = "", asc = 0, perfix = arguments[0] || '~', str = this;
for (i = 0; i < str.length; i++) {
dg = str.substring(i, i + 1);
try {
asc = parseInt(str.charCodeAt(i));
if ((asc < 48) || (asc > 90 && asc < 97) || (asc > 122 && asc < 127) || (asc > 57 && asc < 65)) {
var s000 = asc.toString();
if (asc < 100) { s000 = "0" + s000; }
fh += perfix + s000;
}
else {
fh += dg;
}
} catch (e) {
fh += dg;
}
}
return fh.replace(/[\u4E00-\u9FA5\uF900-\uFA2D]/g, function () {
return encodeURI(encodeURI(arguments[0]));
});
}
JS解码:
String.prototype.AsciiDecode = function () {
var fh = "", youb = "", str = decodeURI(decodeURI(this));
var array = str.split(arguments[0] || '~');
for (i = 0; i < array.length; i++) {
if (i > 0) {
try {
youb = array[i].substring(0, 3);
array[i] = array[i].replace(youb, String.fromCharCode(youb));
} catch (e) { }
}
fh += array[i];
}
return fh;
}
} ();
OC解码:
+ (NSString*)addQuotesFromHTML:(NSString*)html {
NSString*fh =@"";
NSString*youb;
NSString*decodeS = [self URLDecodedString:html];
NSString*newD = [self URLDecodedString:decodeS];
NSArray *array = [newD componentsSeparatedByString:@"~"];
NSMutableArray *newArray = [NSMutableArray arrayWithArray:array];
for(NSIntegeri =0; i < array.count; i++) {
if(i >0) {
NSString*string = array[i];
if(string.length==0) {
return string;
}else{
youb = [string substringToIndex:3];
}
intintyoub = [youb intValue];
NSString*newuu = [NSString stringWithFormat:@"%c",intyoub];
NSString *wee = [string stringByReplacingOccurrencesOfString:youb withString:newuu];
[newArray replaceObjectAtIndex:iwithObject:wee];
}
}
if(newArray.count>1) {
for(NSString*ssinnewArray) {
NSString*k = [fh stringByAppendingString:ss];
fh = k;
}
//对特殊字符处理,还原在html中形态
fh = [fhstringByReplacingOccurrencesOfString:@"&" withString:@"&"];
fh = [fhstringByReplacingOccurrencesOfString:@"<" withString:@" <"];
fh = [fhstringByReplacingOccurrencesOfString:@">" withString:@">"];
returnfh;
}else{
fh = newD;
returnfh;
}
}
+(NSString*)URLDecodedString:(NSString*)str
{
NSString*decodedString=(__bridge_transfer NSString*)CFURLCreateStringByReplacingPercentEscapesUsingEncoding(NULL, (__bridge CFStringRef)str,CFSTR(""),CFStringConvertNSStringEncodingToEncoding(NSUTF8StringEncoding));
returndecodedString;
}