前言

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

webview执行JS阻塞

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

[UIWebView stringByEvaluatingJavaScriptFromString:jsstring]

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

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

Why no WebViewJavascriptBridge

当给出第一版SDK给h5同事联调的时候,h5同事反馈了几个意见:
1、桥接依赖于协议定制和iframe,数据传输透明,存在安全隐患;
2、调用方式过于硬编码,调用时需要匹对填入方法名和参数,希望我这边设计出类似微信web api;
3、webview出现偶现的crash;
4、希望支持命名空间;
有人会问为什么不用业界更加成熟桥接框架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。

XDMicroJSBridge简概

关键类

JSContext: JSContext是JavaScript的执行环境;
JSValue: JSValue代表一个JavaScript实体,一个JSValue可以表示很多JavaScript原始类型例如boolean、 integers、doubles甚至包括对象和函数;

实现原理

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

- (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保存了函数回调的信息。

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。

初始化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终端相关知识可以前往终端杂谈。

JSBridge的思考相关推荐

  1. jsbridge实现及原理_JsBridge使用和原理

    What is JsBridge 近期在做一个项目,使用的是Native+H5的方式实现的.众所周知的是在Android中,Webview所实现的java与js的交互存在一些安全问题,并且这样的使用方 ...

  2. 关于python导入模块和package的一些深度思考

    背景 在python中有导入模块和导入package一说,这篇文章主要介绍导入模块和package的一些思考. 首先什么是模块?什么是package? 模块:用来从逻辑上组织python代码(变量,函 ...

  3. 站在巨人的肩膀上“思考”问题,重在思考而不是拿来主义

    米老师按:觉得值得讨论的小文!我还要认真地想一想 主题:围绕职责链设计模式-计算收费有效时间博客展开讨论 参与人: 讨论时间: 讨论内容 这次讨论主要分为以下几点: 一.职责链模式应用于机房收费系统计 ...

  4. 由Node.js事件驱动模型引发的思考

    引言 近段时间听说了Node.js,很多文章表述这个事件驱动模型多么多么优秀,应用在服务器开发中有很大的优势,本身对此十分感性去,决定深入了解一下,由此也引发了一些对程序设计的思考,记录下来. 什么是 ...

  5. 看了极光推送技术原理的几点思考

    看了极光推送技术原理的几点思考 分类: android2012-11-26 20:50 16586人阅读 评论(18) 收藏 举报 目录(?)[+] 移动互联网应用现状 因为手机平台本身.电量.网络流 ...

  6. C++ 从双重检查锁定问题 到 内存屏障的一些思考

    文章目录 1. 问题描述 2. DCLP 的问题 和 指令执行顺序 2.1 Volatile 关键字 2.2 C++11 的内存模型 3. C++11内存模型 解决DCLP问题 3.1 内存屏障和获得 ...

  7. 关于大型网站技术演进的思考(五)--存储的瓶颈(5)

    上文里我遗留了两个问题,一个问题是数据库做了水平拆分以后,如果我们对主键的设计采取一种均匀分布的策略,那么它对于被水平拆分出的表后续的查询操作将有何种影响,第二个问题就是水平拆分的扩容问题.这两个问题 ...

  8. 对WEB前端的几段思考(一)——界面设计和性能优化(整理中)

    尽管我并非艺术出生,既没有任何设计基础,又没有较高艺术涵养,也深谙在短时间内创造一定艺术造诣并非易事,但是既然当初选择从事网站前端开发,我的目光不能仅停留在前端代码上.作为一名志向在前端领域发展的人员 ...

  9. 从0开始搭建编程框架——思考

    需求来源于问题.(转载请指明出于breaksoftware的csdn博客) 之前有个人做前端开发的同学在群里问"C语言能做什么?能写网页么?",然后大家就开始基于这个问题展开争辩. ...

最新文章

  1. Linux里的21的理解
  2. 2017西安交大ACM小学期数据结构 [树状数组]
  3. 【C++ Priemr | 15】派生类向基类转换的可访问性
  4. 几个ubuntu16.04镜像下载地址
  5. 领扣简单版--两数之和(Two Sum)
  6. Linux下mySql的安装和使用
  7. 在Mac OS X 通过抓包、“第三方下载工具”加速下载、安装APP或系统
  8. SHELL下如何去掉字串里的空格(或指定字符)
  9. Linux虚拟网络设备之bridge(桥)
  10. dtu连接虚拟服务器,DTU连接HTTP网页
  11. 【Android 教程系列第 31 篇】通过 adb install 命令安装 apk 时提示 signatures do not match previously installed version
  12. win10安装visual studio2008
  13. 读《VR虚拟现实与AR增强现实的技术原理与商业应用》
  14. 机器人快跑!伯克利和CMU联合开发两足机器人,两条细腿,一马平川
  15. 2006年4月度国际标准化组织C++会议纪要[原文发表时间:2006年5月3日]
  16. 源中瑞区块链溯源系统,溯源行业生态信息化解决方案
  17. 有哪些开源能源信息管理系统
  18. 恢复原厂设置,清除SD卡数据
  19. 有7g,2g砝码各一个,天平一只,如何只用这些物品三次将140g的盐分成50g,90g各一份?
  20. 转:组织敏捷不是一种选择,而是一种必须!

热门文章

  1. 简单而易忽视的http 404
  2. golang中的strings.LastIndexAny
  3. golang操作mysql用例
  4. C++深复制(深拷贝)、浅复制(浅拷贝)和复制构造函数(拷贝构造函数)详解+实例
  5. char N2Char(int n)函数:将一个整数转换为字符串,并放入一个字符串中
  6. typedef的用法总结
  7. 常考数据结构与算法:二叉树的最大深度
  8. jvm十四:类的卸载
  9. c# 连接Redis报错:WRONGTYPE Operation against a key holding the wrong kind of value:类型搞混弄出的错误...
  10. pycharm 格式化代码