DSBridge-iOS源码解析
Objective-C与JavaScript间的交互方式
1. 自定义scheme, 拦截http请求
在iOS7以前, Objective-C与JavaScript间的交互采用的就是这种方式, JavaScript调用native的API时, 通过约定固定的scheme, 然后用window.location
的方式触发UIWebView的shouldStartLoadWithRequest:
的代理方法来达到调用native API的目的; native通过UIWebView提供的stringByEvaluatingJavaScriptFromString:
方法来调用JavaScript API.
2. 通过JavaScriptCore获取JavaScript运行环境
在iOS7时, 苹果推出了JavaScriptCore公共框架, 它提供了简单的方式实现在Objective-C与JavaScript间交互. 通过JavaScriptCore进行Objective-C与JavaScript间通讯时, 必须先用获取到UIWebView的JSContext
, 即JavaScript的运行环境, 然后将交互对象注入到JSContext
中, 这样Objective-C与JavaScript就能通过注入的交互对象进行通讯了.
注入Objective-C与JavaScript的交互对象的时机
1. UIWebView创建JSContext的时机
- 当UIWebView加载页面解析到
<script>
标签时会自动创建JSContext对象 - 当主动调用
[webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]
去获取JSContext
对象时, 即使UIWebView还没有解析到<script>
标签, 也会创建一个JSContext
对象, 并且这个JSContext
对象在UIWebView之后解析到<script>
标签时仍然有效, 并不会重新再次创建.
2. 为何不能在webViewDidFinishLoad:中注入
在webViewDidFinishLoad:
代理方法中, 页面还没有完全加载完成, 在注入交互对象时, 可能JavaScript中获取交互对象的代码已经执行, 导致获取交互对象失败而不能调到native API.
3. 在webView:didCreateJavaScriptContext:forFrame中注入
在JSContext
对象被创建时WebKit的回调方法webView:didCreateJavaScriptContext:forFrame
被调用, 通过实现这个回调方法, 就可以获取到JSContext
对象, 即在UIWebView创建JSContext
对象时, 获取该对象并注入交互对象.
DSBridge的实现
注入交互对象
register.png- Category NSObject + TS_JavaScriptContext 实现WebKit的
webView:didCreateJavaScriptContext:forFrame
代理方法
- Category NSObject + TS_JavaScriptContext 实现WebKit的
- (void) webView: (id) unused
didCreateJavaScriptContext: (JSContext*) ctx
forFrame: (id<TSWebFrame>) frame {
NSParameterAssert( [frame respondsToSelector: @selector( parentFrame )] );
if ( [frame respondsToSelector: @selector( parentFrame) ] && [frame parentFrame] != nil )
return;
void (^notifyDidCreateJavaScriptContext)() = ^{
for ( UIWebView* webView in g_webViews ) {
NSString* cookie = [NSString stringWithFormat: @"ts_jscWebView_%lud", (unsigned long)webView.hash ];
[webView stringByEvaluatingJavaScriptFromString: [NSString stringWithFormat: @"var %@ = '%@'", cookie, cookie ] ];
// 获取ctx所对应的webView对象
if ( [ctx[cookie].toString isEqualToString: cookie] ) {
[webView willChangeValueForKey: @"ts_javaScriptContext"];
objc_setAssociatedObject( webView , kTSJavaScriptContext, ctx, OBJC_ASSOCIATION_RETAIN);
[webView didChangeValueForKey: @"ts_javaScriptContext"];
// 注入交互对象
if ( [webView.delegate respondsToSelector: NSSelectorFromString(@"webView:didCreateJavaScriptContext:")] ) {
SuppressPerformSelectorLeakWarning(
[webView performSelector:NSSelectorFromString(@"webView:didCreateJavaScriptContext:") withObject:webView withObject:ctx ] ;//webView: webView didCreateJavaScriptContext: ctx];
);
}
return;
}
}
};
if ( [NSThread isMainThread] ) {
notifyDidCreateJavaScriptContext();
} else {
dispatch_async( dispatch_get_main_queue(), notifyDidCreateJavaScriptContext );
}
}
- 注入交互对象
- (void)webView:(UIWebView *)webView didCreateJavaScriptContext:(JSContext *)ctx {
__block JSContext * ctx_=ctx;
创建交互对象_dsbridge
ctx[@"_dsbridge"]=^(NSString * method,NSString * args){
return [JSBUtil call:method :args JavascriptInterfaceObject:JavascriptInterfaceObject jscontext:ctx_];
};
// 注入
[ctx evaluateScript:INIT_SCRIPT];
}
// INIT_SCRIPT
function getJsBridge() {
window._dsf = window._dsf || {};
return {
// b native API 方法名
// a 调用native API时传递给native方法的参数
// c JavaScript的回调函数, 当异步调用native API时会传递此参数
call: function(b, a, c) {
'function' == typeof a && (c = a, a = {});
if('function' == typeof c) {
window.dscb = window.dscb || 0;
var d = 'dscb' + window.dscb++;
// 在window下添加对象d, 保存JavaScript回调函数c
window[d] = c;
// 将d放置在参数列表a中, 传递给native方法, 最终在native方法里, 通过在参数列表的json字符串中取出d,
// 然后在window对象中通过d获取JavaScript的回调函数c来完成回调操作
a._dscbstub = d;
}
a = JSON.stringify(a || {});
// 通过_dsbridge交互对象调用native API
return window._dswk? prompt(window._dswk + b, a): 'function' == typeof _dsbridge? _dsbridge(b, a): _dsbridge.call(b, a)
},
// 注册JavaScript函数, 供native方法调用; 这里是将JavaScript函数挂到window对象上面,
// b为JavaScript的函数名, a为JavaScript的函数对象,
// native方法最终会通过在window对象上用函数名找到注册的JavaScript函数对象来实现调用JavaScript方法
register:function(b, a) {
'object' == typeof b? Object.assign(window._dsf, b): window._dsf[b] = a;
}
}
} dsBridge = getJsBridge()
native调用JavaScript API
原理图
call_javascript.png- 注册JavaScript函数
<script>
var bridge = getJsBridge();
dsBridge.register('addValue',function(r,l){
return r+l;
})
</script>
- native方法调用JavaScript函数
// addValue JavaScript函数
// arguments 参数
// completionHandler 回调
[_webview callHandler:@"addValue"
arguments:[[NSArray alloc] initWithObjects:@1,@"hello", nil]
completionHandler:^(NSString * value){
NSLog(@"%@",value);
}];
-(void)callHandler:(NSString *)methodName arguments:(NSArray *)args completionHandler:(void (^)(NSString *))completionHandler {
if(!args) {
args=[[NSArray alloc] init];
}
// 在window对象上通过函数名addValue找到h5注册的JavaScript函数, 最终通过webView的stringByEvaluatingJavaScriptFromString:方法调用
NSString *script=[NSString stringWithFormat:@"(window._dsf.%@||window.%@).apply(window._dsf||window,%@)",methodName,methodName,[JSBUtil objToJsonString:args]];
[self evaluateJavaScript:script completionHandler:^(NSString * value){
if(completionHandler) completionHandler(value);
}];
}
JavaScript调用native API
原理图
call_native.png- 1.H5发起调用请求
// 获取交互对象
var bridge = getJsBridge();
function callSyn() {
// 通过bridge的call方法调用native方法testSyn, 传递参数为{msg: "testSyn"}
bridge.call("testSyn", {msg: "testSyn"});
}
// b native API 方法名
// a 调用native API时传递给native方法的参数
// c JavaScript的回调函数, 当异步调用native API时会传递此参数
call: function(b, a, c) {
'function' == typeof a && (c = a, a = {});
if('function' == typeof c) {
window.dscb = window.dscb || 0;
var d = 'dscb' + window.dscb++;
// 在window下添加对象d, 保存JavaScript回调函数c
window[d] = c;
// 将d放置在参数列表a中, 传递给native方法, 最终在native方法里, 通过在参数列表的json字符串中取出d,
// 然后在window对象中通过d获取JavaScript的回调函数c来完成回调操作
a._dscbstub = d;
}
a = JSON.stringify(a || {});
// 通过_dsbridge交互对象调用native API
return window._dswk? prompt(window._dswk + b, a): 'function' == typeof _dsbridge? _dsbridge(b, a): _dsbridge.call(b, a)
}
- 2.触发交互对象block执行
ctx[@"_dsbridge"]=^(NSString * method,NSString * args){
return [JSBUtil call:method :args JavascriptInterfaceObject:JavascriptInterfaceObject jscontext:ctx_];
};
- 3.方法解析并调用
+(NSString *)call:(NSString*) method :(NSString*) args JavascriptInterfaceObject:(id) JavascriptInterfaceObject jscontext:(id) jscontext {
// 方法及参数解析
// methodOne为同步调用时的方法名称, 此时methodTwo为空
NSString *methodOne = [JSBUtil methodByNameArg:1 selName:method class:[JavascriptInterfaceObject class]];
// methodTwo为异步调用时的方法名称, 此时methodOne为空
NSString *methodTwo = [JSBUtil methodByNameArg:2 selName:method class:[JavascriptInterfaceObject class]];
SEL sel=NSSelectorFromString(methodOne);
SEL selasyn=NSSelectorFromString(methodTwo);
NSString *error=[NSString stringWithFormat:@"Error! \n Method %@ is not invoked, since there is not a implementation for it",method];
NSString *result=@"";
// 检查调用对象是否存在
if(!JavascriptInterfaceObject) {
NSLog(@"Js bridge method called, but there is not a JavascriptInterfaceObject, please set JavascriptInterfaceObject first!");
} else {
// json为发起调用时的参数列表
NSDictionary * json=[JSBUtil jsonStringToObject:args];
// cb 保存JavaScript的回调函数名称, 当发起异步调用时为有值
NSString * cb;
do{
if(json && (cb= [json valueForKey:@"_dscbstub"])) {
// 异步调用
if([JavascriptInterfaceObject respondsToSelector:selasyn]) {
void (^completionHandler)(NSString *,BOOL) = ^(NSString * value,BOOL complete) {
value=[value stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
NSString *del=@"";
// 发起连续回调时, complete为YES, 否则为NO
if (complete) {
// 删除保存在window对象中的cb对象, 这个对象是H5发起调用时保存到window中的
del=[@"delete window." stringByAppendingString:cb];
}
// 在window对象上通过cb调用JavaScript函数
NSString*js=[NSString stringWithFormat:@"try {%@(decodeURIComponent(\"%@\"));%@; } catch(e){};",cb,(value == nil) ? @"" : value,del];
if([jscontext isKindOfClass:JSContext.class]) {
[jscontext evaluateScript:js ];
} else if([jscontext isKindOfClass:WKWebView.class]) {
@synchronized(jscontext) {
UInt64 t=[[NSDate date] timeIntervalSince1970]*1000;
g_ds_js_cache=[g_ds_js_cache stringByAppendingString:js];
if(t-g_ds_last_call_time<50) {
if(!g_ds_have_pending){
[self evalJavascript:(WKWebView *)jscontext :50];
g_ds_have_pending=true;
}
} else {
[self evalJavascript:(WKWebView *)jscontext :0];
}
}
}
};
SuppressPerformSelectorLeakWarning(
[JavascriptInterfaceObject performSelector:selasyn withObject:json withObject:completionHandler];
);
//when performSelector is performing a selector that return value type is void,
//the return value of performSelector always seem to be the first argument of the selector in real device(simulator is nil).
//So,you should declare the return type of all api as NSString explicitly.
if (result==(id)json) {
result=@"";
}
break;
}
} else if ([JavascriptInterfaceObject respondsToSelector:sel]) {
// 同步调用
SuppressPerformSelectorLeakWarning(
result=[JavascriptInterfaceObject performSelector:sel withObject:json];
);
break;
}
NSString*js=[error stringByAddingPercentEscapesUsingEncoding: NSUTF8StringEncoding];
js=[NSString stringWithFormat:@"window.alert(decodeURIComponent(\"%@\"));",js];
if ([jscontext isKindOfClass:JSContext.class]) {
[jscontext evaluateScript:js ];
} else if ([jscontext isKindOfClass:WKWebView.class]) {
[(WKWebView *)jscontext evaluateJavaScript :js completionHandler:nil];
}
NSLog(@"%@",error);
}while (0);
}
if (result == nil||![result isKindOfClass:[NSString class]]) {
result=@"";
}
return result;
}
网友评论