JSBridge的思考

前言

最近在作一个web与原生交互的需求,需求背景是这样子的,提供一个SDK里面包含一个webview用于加载业务h5,原生这边赋予webview选择相片、相机、刷脸、关闭原生界面的能力。虽然这个功能逻辑都是“熟悉的配方”,但仍是有很多坑。html

webview执行JS阻塞

项目一开始使用的桥接框架是之前项目用的桥接框架,但这个项目里面有一功能点跟旧项目不同,旧项目只涉及到单图片的选择和上传而新项目须要支持多图片选择和上传,由于之前单图片选择上传整个过程响应较快,因此没关注执行JS时卡住了主线程,但此次项目是多图片选择上传并且h5多了ocr识别,致使整个处理相对耗时,原生这边执行JS一个回调将多张图片数据回传给h5处理,实例代码以下java

[UIWebView stringByEvaluatingJavaScriptFromString:jsstring]
复制代码

这个方法是一个同步方法,他会阻塞到JS方法执行结束才会返回,这时整个UI就会卡住。一开始的解决方案是经过原生这边异步派发队列解决同步的问题,但这又是一个坑,会致webview出现偶现的crash,这个稍后再详讲。原生这边不通,那就从JavaScript这一边着手,熟悉JavaScript的同窗都知道,setTimeout方法可以实现异步,若是代码中设定了一个 setTimeout,那么浏览器便会在合适的时间,将代码插入任务队列,若是这个时间设为 0,就表明当即插入队列,但不是当即执行,仍然要等待前面代码执行完毕,因此 setTimeout 并不能保证执行的时间,是否及时执行取决于 JavaScript 线程是拥挤仍是空闲,但它可以解决咱们执行JS代码致使的同步问题,在咱们原生调用JS回调以前用setTimeout作一层包装,至关于调用setTimeout方法,一调用就即刻返回,不阻塞线程,实例代码以下:ios

function asyncallback(callback,params) {if(typeof callback == 'function'){setTimeout(function () {callback(params);},0);}}
复制代码

Why no WebViewJavascriptBridge

当给出初版SDK给h5同事联调的时候,h5同事反馈了几个意见:
一、桥接依赖于协议定制和iframe,数据传输透明,存在安全隐患;
二、调用方式过于硬编码,调用时须要匹对填入方法名和参数,但愿我这边设计出相似微信web api;
三、webview出现偶现的crash;
四、但愿支持命名空间;
有人会问为何不用业界更加成熟桥接框架WebViewJavascriptBridge,咱们经过读源码可知WebViewJavascriptBridge底层仍是依赖于协议定制和iframe,并不支持命名空间,并且crash仍是会出现(网友反馈)。 综合上次的意见,咱们须要从新设计咱们的桥接框架,原框架的两端交互依赖iframe发请求、拦截请求来进行交互,iOS还有另一个方案来实现两端交互:JavaScriptCore,想深刻了解JavaScriptCore能够看这篇文章,并且经过JavaScriptCore设计的js api的代码风格能够作到微信web api的效果。JavaScriptCore框架是一个苹果在iOS7引入的框架,该框架让 Objective-C 和 JavaScript 代码直接的交互变得更加的简单方便,而JavaScriptCore是苹果Safari浏览器的JavaScript引擎。经过JavaScriptCore,咱们能够以写原生代码的方式写JavaScript,最终JavaScriptCore都会将咱们的原生代码顺滑、安全转化为JavaScript层的实现。咱们以这个JavaScriptCore框架为基础设计咱们的桥接组件XDMicroJSBridge。git

XDMicroJSBridge简概

关键类

JSContext: JSContext是JavaScript的执行环境;
JSValue: JSValue表明一个JavaScript实体,一个JSValue能够表示不少JavaScript原始类型例如boolean、 integers、doubles甚至包括对象和函数;github

实现原理

先在原生注册对应的暴露给h5使用js API函数名,经过[JSContext currentArguments]捕获方法的参数,参数的类型是JSValue,JSValue提供一系列方法将值转换成合适的Objective-C值或对象,方便这边原生处理,经过block包装原生调用方法(相机、相册等),将block注入JSContext当中,命名空间的实现是往JSContext注入一个空实现的类,须要赋予命名空间的方法则将对应包装的block注入到这个空实现的类中。想了解具体实现点击github.com/caixindong/…。实例代码以下:web

- (void)registerAction:(NSString *)action handler:(XDMCJSBHandle)handler {
    if (action && handler) {
        __weak typeof(self) weakSelf = self;
        _context[_nameSpace][action] = ^{
            NSLog(@"action is %@",action);
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.webThread = [NSThread currentThread];
            NSLog(@"webThread is %@",[NSThread currentThread]);
            NSArray *args = [JSContext currentArguments];
            JSValue *last = (JSValue *)[args lastObject];
            XDMCJSBCallback ncallback = nil;
            NSMutableArray *trueArgs = [NSMutableArray arrayWithArray:args];
            if ([last isObject] && [[last toDictionary] isEqualToDictionary:@{}]) {
                [trueArgs removeLastObject];
                ncallback = ^(NSDictionary *params){
                    [strongSelf performSelector:@selector(_callJSMethodWithArgs:) onThread:strongSelf.webThread withObject:@[last, params] waitUntilDone:NO];
                };
            }
            NSMutableArray *trueOCArgs = [NSMutableArray array];
            for (JSValue *value in trueArgs) {
                if ([value isObject]) {
                    [trueOCArgs addObject:[value toDictionary]];
                } else if ([value isString]) {
                    [trueOCArgs addObject:[value toString]];
                } else if ([value isNull]) {
                    [trueOCArgs addObject:[NSNull null]];
                } else if ([value isBoolean]) {
                    [trueOCArgs addObject:[NSNumber numberWithBool:[value toBool]]];
                }
            }
            handler([trueOCArgs copy], ncallback);
        };
    }
}
复制代码

实现难点

JSValue提供了JavaScript原始类型boolean、integers、doubles、对象转化方法,但没有提供函数的转化方法,由于JS函数参数通常都会包含回调,回调是function对象,因此这一块转化是颇有必要的,由代码可见我这边是经过一个oc的block保存了函数回调的信息。api

webthread crash

对于crash问题,通过我屡次调试发现,在web与原生交互屡次后再触发下一次交互会发现野指针crash,频次不定,crash栈定位到webview的webthread。两种实现方案都会出现这个问题。总所周知,JavaScript是以单线程的方式运行的,因此webview底层会维护一个线程用于处理JavaScript的交互,网上不少例子和教程在webview执行js代码的时候都会派发到主线程,但是webthread有时候并不在主线程,这是有隐患的,若是是频次低的交互可能不会触发这个bug,当频次高时,就例如我这个项目,h5内有不少表单须要上传选择图片这种跨端操做,就可能会触发webthread crash。网上资料和官方文档并无对这个crash作具体的解释,我猜想多是底层线程通讯派发出现问题,因此正确的作法应该是webview内JavaScript的执行和回调应始终在一个线程,以防止线程切换致使偶现crash。那怎么获取webthread,获取webthread的时机应该是JavaScript的执行环境初始化完成以后,因此能够在包装原生调用方法的block捕获这个webthread,由于h5触发原生封装的js api后会跑进封装原生方法block,这时候上下文已经初始化完成,并且也是在webview维护的webthread内。实例代码以下:浏览器

- (void)registerAction:(NSString *)action handler:(XDMCJSBHandle)handler {
    if (action && handler) {
        __weak typeof(self) weakSelf = self;
        _context[_nameSpace][action] = ^{
            NSLog(@"action is %@",action);
            __strong typeof(weakSelf) strongSelf = weakSelf;
            strongSelf.webThread = [NSThread currentThread];
            NSLog(@"webThread is %@",[NSThread currentThread]);
}
复制代码

而后在这个线程执行js相关逻辑代码,这样修改后,crash没再出现,实例代码以下:安全

[self performSelector:@selector(_callJSMethodWithArgs:) onThread:strongSelf.webThread withObject:@[callback, params] waitUntilDone:NO];
复制代码

最终框架实现效果

相比其余桥接框架,XDMicroJSBridge更加轻量(代码量不到100行),支持命名空间,原生专一原生代码,web专一JavaScript,维护一致的web thread。bash

初始化Bridge

#import "XDMicroJSBridge.h"
@property (nonatomic, strong) UIWebView *webview;
@property (nonatomic, strong) XDMicroJSBridge *bridge;
@property (nonatomic, copy) XDMCJSBCallback callback;
self.bridge = [XDMicroJSBridge bridgeForWebView:_webview];
复制代码

注册JS方法

__weak typeof(self) weakself = self;
[_bridge registerAction:@"camerapicker" handler:^(NSArray *params, XDMCJSBCallback callback) {
        dispatch_async(dispatch_get_main_queue(), ^{
            //if your javaScript method has callback, you should register this call like this.
            if (callback) {
                weakself.callback = callback;
            }
            UIImagePickerController *cameraVC = [[UIImagePickerController alloc] init];
            cameraVC.delegate = weakself;
            cameraVC.sourceType = UIImagePickerControllerSourceTypeCamera;
            [weakself presentViewController:cameraVC animated:YES completion:nil];
        });
    }];
复制代码

h5调用原生注册的JS方法

<script>
    function clickcamera() {
        XDMCBridge.camerapicker(function (response) {
            var photos = response['photos'];
            var insert = document.getElementById('insert');
            for(var i = 0; i < photos.length; i++) {
                var img = new Image(100,100);
                img.src = photos[i];
                insert.appendChild(img);
            }
        });
    }
</script>
复制代码

想了解更多iOS终端相关知识能够前往终端杂谈

相关文章
相关标签/搜索