2015

2015-12-31 评论(7) 分类:生活

照例在12月31日写年终回顾。

工作

JSPatch

我一直热衷于做各种东西,从以前的网站FLASH游戏,到推特中文圈,到伊书等APP,一直喜欢这种做一个完整产品的感觉,算是我一大爱好。之前做的都是实际使用的产品,今年的捣鼓多了种类型:开源项目,对我来说开源项目也是一个完整的产品。

JSPatch 可以说是压力下的产物,做这么久技术都没有什么拿得出手的东西,有些捉急,年初就在琢磨做个什么东西,最初的想法是既然 OC 都可以动态调用和替换方法,那可以试试做个东西让 JS 传信息过来去调用和替换方法,这样就可以实时下发脚本修复bug了,产生这个念头的时候我还不知道有 waxPatch 这个开源库,真是孤陋寡闻,后来真正动手做时才知道,不过发现它已经年久失修,而且 JS 又有语言和自带引擎的优势,觉得值得一试,就开始做了。

匆匆把第一个版本放上 github 后,得到很不错的反响,让我有些始料未及,于是投入更多精力去完善它和运营它,不断改进核心代码,日渐完善。产品上除了核心代码,在测试用例和文档上也下了不少功夫,开源项目要真正让人使用,文档必须完善,核心代码只是开源项目的一部分,周边的配套完善后才能是一个完整的开源项目。运营上主要靠技术文章,先后发表了四五篇 JSPatch 相关文章,对开源项目来说写技术文章是个不错的推广方式,再加上微博和一些技术文章平台助力,效果不错。另一种常见的推广方式是技术演讲,这个还没尝试去做,主要是我个人不太擅长。比较遗憾的是我英文水平不行,没法用同样的推广方式推广到国外。

JSPatch 核心上我觉得最重要的就是一个点子和一个理念。点子就是在运行 JS 前用正则替换代码模拟函数转发,这一点是做这个库最大的瓶颈,也可以说是这么久都没出现这样一个库的原因之一,这个原理很简单,但现实世界中很多事物追溯到源头就是一个小点子。理念是保持整个库的精巧简洁,JSPatch 开源以来经历过两百次代码提交,在不断完善的过程中还是保持了极小的代码量,一千多行 OC 和接近两百行 JS,在加功能和修改功能时的代码量和整洁度是我很看重的一个点,让项目接入无压力,尽量让整个库保持精简小而美,是我的一点小追求。

除了主程序,另外还做了两个外围产品:JSPatch 代码自动转换工具JSPatch 服务平台,代码自动转换工具花了一周时间做,过程比较烧脑,做完挺有成就感,虽然有些特性不支持,但用起来还是挺好的。JSPatch 服务平台提供了托管 JSPatch 脚本的后台,封装了脚本下发逻辑,可以让中小 APP 不用搭建后台也能很容易用上 JSPatch,不过后续一直没找到可持续发展路线,导致一直在内测状态,没有投入多大精力在上面。

至今 JSPatch 在开源社区运作得还算不错,github 上已有 3200+ star,虽说 github star 已通货膨胀,但还是能反映一些热度的。另外也得到不少开源社区的帮助,已有17个人参与开发,Q群交流也有300多人,没有统计到有多少产品使用 JSPatch,做了个页面让使用的人自愿提交,使用的朋友可以有兴趣可以提交下:http://using.jspatch.org

JSPatch 相对 wax 有较多优势,有自信可以说是 iOS 动态修复 bug 最佳解决方案,接下来会继续改善一些小缺陷。对于下一步发展计划,我一直想让 JSPatch 不止用于修复 bug,还可以用于动态添加模块,相对于 ReactNative 它还是有优势的,就是对客户端开发的同学来说学习成本低,延续 OC 的开发思维就行。不过年末忙碌导致最近没时间在这上面花精力,也还没找到比较好的路子,后续再研究研究。

APP

今年没有做一个新 APP,比去年更疏于打理,因为时间精力都用在其他方面了,也没想出什么比较想做的产品。对于已有的 APP,某天想起来更新个版本,结果运气极差,说我的 APP 可以下载音乐被拒,被拒也就算了,老的版本竟然直接被下架,直接从AppStore下架!简直对审核人员无语,折腾了个把月才恢复,已是元气大伤,真是时运不济,命途多舛,不过这样的状况也可以让我抛弃过往重新尝试新的玩法,希望接下来能玩得好。

公司

掐指一算,今年竟然是我第一次在腾讯工作满一整年,往年都是各种间断。工作上迎来毕业以来最大的顺境,一扫阴霾,自我感觉状态不错,拿了2次优秀员工,顺利晋级T3,下半年开始也不焦虑了,工作也算开心。

产品上做的是微信读书,去年就开始做的产品,今年8月才上线,过程比较曲折。产品理念上我是很认同的,就是不想做 another book reader,若跟其他读书 APP 没有区别就没必要做了,所以会有一些别的读书APP没有的尝试,这也是为什么捣鼓那么久才上线的原因之一。但是细节上走了不少弯路,费了挺多时间和资源,还有一些不可控力量,各种拍脑袋,希望接下来能克服这些困难把这个产品做起来。

生活

生娃

年末诞下小女生一枚,当爸爸了,正式从二人世界变成家庭生活,影响确实是很大,结婚对生活没多大影响,有孩子就不一样了,很多时候要围绕着孩子转,小孩前几个月应该是最辛苦的,不乖的时候日夜哭闹,我因为要上班还好,最辛苦的还是当妈妈的,还有专门过来照顾的老妈,真是不容易。

变成家庭生活后,两代人的代沟也就逐渐显示出来,对带小孩的方式会有不同意见,总结起来,代沟最大的就是四个字:温饱迷信,似乎每个奶奶都会怕孙子吃不饱穿不暖,可能是上个年代物质匮乏导致,会特别注重这两点,导致有时过了,另外小孩一有不乖的情况,就会怀疑是不是一些风水迷信方面的缘故,我们表示不信就会有一些摩擦,再加上观念固定,说服不了对方,比较难搞。

小家伙出生俩月体重涨了一倍,长势喜人,有时看着觉得很神奇,年初还没有,年末就长这么胖了,还挺可爱,最近几天已经能跟我有些互动了,逗她偶尔会笑,很期待再过一两个月能跟她玩耍,小孩三岁以内是最美好的,希望接下来能多些时间陪她度过这段美好的时光。

车子

有小孩有就开始考虑买个车,我对车不怎么感冒,如果没小孩应该是不会那么快买的,现在买车也不是什么大事了,相对于房子车实在太便宜。当时对车一窍不通,挑车只看外貌,在朗动/福克斯/雷凌中选,最后选了丰田雷凌,中规中矩的车。买完后开的次数较少,半年多过去才2K公里,上班的地方没停车位,比较蛋疼,偶尔去市区也不会开,因为停车费比打车费还贵,论便宜肯定还是不买车只打车便宜,特别是现在有滴滴打车,不过自己偶尔自由地开开车感觉还是挺爽的。

旅游

有了小屁孩,基本宣布跟旅游告别至少两年:(,本来计划去日本,现在遥遥无期了。今年两次出行,都算不上旅游,年中第一次自驾游去了下南沙,自驾游感觉挺不错的,不过南沙比较一般,能玩的地方很少,就去吃吃喝喝走走散散心。另外公司旅游去了下澳门,全因耗时短还能顺便买奶粉才选澳门,十分坑爹,对澳门印象不太好,可能我本来就对赌博印象不好吧,澳门也确实没什么值得游玩的,印象就是路很窄,房子很贵,导游很拜金,充满一种俗气。赌场也跟想象的不太一样,没什么吸引力,转两圈没玩,只在附近商场逛逛了事。只能说,今年旅游方面有些憋坏了。

股市

今年年初股市大热,我也跳入这个坑,结果大家都能猜到了,当时亏到无心工作。A股倒是不亏不赚,主要是港股的坑太大,真是杯具,还好没有投入特别多,像我这样什么都不懂进入股市其实跟赌博差别不大,不应该这样玩的,但环境的影响实在太厉害,迷失了,以后还是要注意点。

其他

因为做微信读书,导致我看书也变多了,看了28本书,不过光《新宋》就占了11本。读书相关的已另写了篇博客,就不再说了。电影倒是看得少了,因为有了小孩,影院也去不了了,在家也没什么娱乐时间,算下来只看了12部,有印象的就只有《大圣归来》。上下班路上还是继续听电台,还是听矮大紧和罗胖,外加了个吴晓波,几乎每一期都听了,感觉还是增长不少见识的,虽然大部分都忘了。

今年几乎都没有锻炼,只有寥寥几次游泳和羽毛球,还好身体还算争气,没出现什么病。明天开始要好好锻炼~

今年博客写得更少了,只有两篇非技术文章,扯淡扯得少了,要是没有“每月至少一篇”的承诺,可能就这样荒废了。小事都写在日记上,日记已连续写了2122天,已成一种习惯停不下来,大多时候写是为了不破坏这个连续性。

2016照旧没什么计划,希望在自由/体验/创造这三点上有更多收获。

读书

2015-11-28 评论(6) 分类:随记

近一年来在做微信读书,导致自己看书也多了些,看了《新宋》系列十几本,东野圭吾的几部经典作品,以及一些杂七杂八的社科类书。

第一次接触《新宋》这样的设定,可以说是穿越小说,也可以说是架空历史小说,讲的是在宋朝王安石变法这个中国历史转折时期,如果矫正变法的弊端,用上现代化的思想和制度去改革会是怎样,包括改善纺织生产,重商言利,创办现代化学校,引入数学物理知识格物致知,写书办报掌握舆论,兵器研究生产火药,探索南海扩张海上贸易,改良青苗法等系列法例,充分考虑当时人的接受程度,循序渐进,中间有很多君臣间,权臣间的博弈,在权术描写上很靠谱。假想在宋代这个史上读书人地位最高,言论最自由的时期,又有锐意改革意图大展宏图的君主,若有合适的方法,会产生什么样的繁荣。虽然不是历史,但也能看到那个时代的社会风貌,士大夫阶层的气节,在宋朝之后包括今天都不再有了。这书最大的缺点在于太拖沓,特别是对战争的描写十分冗长,并且还未完结,另外整部书都十分理性,缺少感情上的描写,没有多少值得回味的地方。

另外东野圭吾系列中,印象最深的是《白夜行》,也是他的代表作,虽然很阴暗,但很多细节和情感丰富细腻,描述方式平淡又很有震撼力,很能影响人心境,看完有不少可以回味的地方,好的小说就是这样吧,上一次有这种感觉的书还得追溯到《三体》和《挪威的森林》。

社科书籍方面,最近看的《人类简史》可以说是我最喜欢的书籍Top3,作者以绝对理性的方式阐述人类的发展史以及宗教/金钱/国家/信念/欲望这些重要事物的由来,内有大量毁三观的观点,虽然不能让人全部赞同,但让人大开眼界,绝对推荐。

现在要看完一本书有点难度,因为看书已经不算是休闲娱乐了,需要用点脑,而现在大多数人从事脑力劳动,脑子都快不够用了,休闲时喜欢做一些不用动脑的事,例如刷微博朋友圈看鸡汤玩休闲游戏,闲了还要耗脑去看书确实不是什么好差事。日常休闲娱乐是要的,但分点精力给看书还是有不少好处的,小说可以给你不一样的体验和想象,好的小说还会留下余味,而社科类非小说书籍多少可以增长点见识,满足好奇心,避免被一些人牵着鼻子走。应该大部分人都认可读书有益的观点,只是时间不知不觉就被微博朋友圈吸走了。既然读书被大众认同是好的,只是不如无脑娱乐那么轻松,那如果要做一个新型读书产品,我觉得应该是要给读书多一些额外的乐趣和鼓励,以此吸引人多读书,对抗无脑娱乐,像微信读书的每周读书时长排名就是一个例子,一些健身产品也是这种思路,希望后续可以做出更多的尝试。

JSPatch Convertor 实现原理详解

2015-10-13 评论(7) 分类:技术文章 Tags:

简介

JSPatch Convertor 可以自动把 Objective-C 代码转为 JSPatch 脚本。

JSPatch 是以方法为单位进行代码替换的,若 OC 上某个方法里有一行出了bug,就需要把这个方法用 JS 重写一遍才能进行替换,这就需要很多人工把 Objective-C 代码翻译成 JS 的过程,而这种代码转换的过程遵循着固定的模式,应该是可以做到自动完成的,于是想尝试实现这样的代码自动转换工具,从 Objective-C 自动转为 JSPatch 脚本。

方案 / Antlr

做这样的代码转换工具,最简单的实现方式是什么?最初考虑是否能用正则表达式搞定,如果可以那是最简单的,后来发现像 方法声明 / get property / NSArray / NSString 等这些是可以用正则处理的,但需要匹配括号的像 block / 方法调用 /set property 这些难以用正则处理,于是只能转向其他途径。

Antlr

接下来的思路是对 Objective-C 进行词法语法解析,再遍历语法树生成对应的 JS 代码。Objective-C 词法语法解析 clang 可以做到 ,但在找方案过程中发现了 antlr 这个神器,以及为 antlr 定制的几乎所有语言的语法描述文件,更符合我的需求。antlr 可以根据语法描述文件生成对应的词法语法解析程序,生成的程序可以是 Java / Python / C# / JavaScript 这四种之一。

也就是说,我们拿 ObjC.g4 这个语法文件,就可以通过 antlr 生成 Objective-C 语法解析程序,程序语言可以在上述四种语言中任挑,我挑选的是 JavaScript,生成的程序可以在 [这里] 看到。官方文档有生成的流程和使用方法,可以自己试下。
(更多…)

逻辑推理的陷阱

2015-9-16 评论(6) 分类:随记

人们喜欢用一个单一的逻辑推理链去解释一个结果,因为人脑太傻,信息越简单就越容易被人理解,但实际上现实世界不像数学世界那么严谨和单纯,导致一个现象或结果的原因通常是非常多的,有些还无法用简单的逻辑推理去推导,这导致现实中的逻辑推理有很多陷阱,看似很有道理的推理实际上根本不成立。

臆想

其中一类逻辑陷阱就是按自己臆想去推理出结论,原因和结论没有关系,但因为现实世界的模糊,经常还能让人听起来觉得挺有道理,不经深究不知道是扯淡。罗辑思维经常做这样的事,举个小例子,有一天罗胖说为什么几乎只有人类有眼白?因为动物要隐藏眼神所以不能有眼白,而人类第一需求是其他人的协助,眼白有助于让别人看懂自己的喜怒哀乐,有利于社交所以发展出眼白。听起来好像有道理,仔细想想却不是这么回事,很多人用进化论去解释一些人的特征时忽略了它的条件,就是特征要对生存率有较大的提高才能进行优胜劣汰。眼白符合这个条件吗?试想一个部落里,每个人都没有眼白,突然基因突变出一个有眼白的人,然后有眼白的人会大受欢迎,大家只跟TA繁衍后代流传基因,没眼白的就会被淘汰掉?完全说不通。若出现眼白的过程不是突变,而是逐渐变淡退去,那更跟什么眼白有助于社交没半毛钱关系,它根本不决定生存率,更何况哈士奇先生也有眼白,所以这不过是在扯淡。

再说说进化论,进化论有点被滥用的迹象,按这种思维方式,很容易用“进化论”推理出跟现实不符的结论出来:作为一个原始人,在婴儿夭折率高的情况下,怎样挑女朋友可以使后代的生存率高,使自己的基因有竞争优势?当然是选择强壮的女士,怀孕期间可以更好保护胎儿,生下来的孩子身体强壮几率高,生存率自然也高,另外原始人吃穿无保障,女朋友最好还要有点胖,有点脂肪能抵御突如其来的食物荒,这样一代代淘汰下来,喜欢强壮肥胖女性的男性后代生存率高,不喜欢肥胖强壮女性的男性基因慢慢被淘汰,所以按推论现在大部分男性应该都更喜欢强壮肥胖女性。这种臆想在日常玩笑时扯扯淡忽悠忽悠挺有意思,要当成论点说服别人时就要注意了。

捏造现实

第二种是为了推导出自己想要的结论,忽视了现实,或者对一部分现实进行捏造。例如周鸿祎之前在推广他微创新的理论时,经常说一个例子,就是 iPhone 是怎么来的呢?它不是啪一声就出现了,而是一个渐进微创新的过程,先做一个小小的只能听歌的 iPod,然后想能不能在 iPod 上加一个屏幕,变成 iPod touch,接着想能不能加个通话功能,才变成 iPhone,这里一步一个微创新,众多微创新一起最后才出现一个 iPhone。好有道理的感觉,但现实是这样吗,现实是 iPod Touch 是在 iPhone 推出后8个月才出现的产品,是 iPhone 的精简版,另外 iPhone 的系统是由 Mac 系统精简得来,跟 iPod 也没什么关系,做这个逻辑推理时完全忽视了这些现实,捏造了“iPhone是由iPod一步步改造得来”这样的现实,老周想当然地为推理自己的论点拼凑论据随意捏造现实,让人听起来逻辑性很强,这很容易说服不了解事实,只考虑逻辑是否成立的人,实际上又是另一种形式的扯淡。

以偏概全

第三种是所有论点结论都是事实,但从导致结果的众多原因中挑出有限的几个,断定就是这几个原因导致的结果,也就是以偏概全。典型的应用是成功学,导致成功的原因非常多,只挑其中的勤奋/情商高/有胆识等几个点推导出某个人必然成功。还有像某些养生文章,XX活到90岁,因为她天天练太极。像一些社会观点,美国好,是因为民主。以偏概全是应用最广的,因为说的都是现实,而且都有一定的关系,推理起来很有道理,人脑又更倾向于相信简单的逻辑,对于错综复杂的真实世界往往处理不过来,所以以偏概全大受欢迎,这也给很多争论提供了广大空间。

JSPatch 部署安全策略

2015-8-31 评论(25) 分类:技术文章

使用 JSPatch 有两个安全问题:

  1. 传输安全:JS 脚本可以调用任意 OC 方法,权限非常大,若被中间人攻击替换代码,会造成较大的危害。
  2. 执行安全:下发的 JS 脚本灵活度大,相当于一次小型更新,若未进行充分测试,可能会出现 crash 等情况对 APP 稳定性造成影响。

接下来说下这两个问题的解决方案。

传输安全

方案一:对称加密

若要让 JS 代码传输过程中不轻易被中间人截获替换,很容易想到的方式就是对代码进行加密,可以用 zip 的加密压缩,也可以用 AES 等加密算法。这个方案的优点是非常简单,缺点是安全性低,容易被破解。因为密钥是要保存在客户端的,只要客户端被人拿去反编译,把密码字段找出来,就完成破解了。

对此也有一些改进方案,例如:

1.可以把密码保存到 keychain 上,但这种方式也是不可靠的,只要随便找一台机器越狱装了这个 APP,用 hook 的方式在 APP 上添加一些代码,获得 keychain 里的密钥值,就可以用于其他所有机器的传输解密了。

2.给每个用户下发不同的密钥。但这样就非常繁琐,需要对下发密钥的请求做好保护,后台需要每次都对脚本进行不同密钥的加密操作,复杂性高了。

综上,对称加密安全性低,若要稍微提高点安全性,就会提升程序复杂度。 (更多…)

JSPatch实现原理详解<二>

2015-7-6 评论(14) 分类:技术文章 Tags:

注:本文较早撰写,随着 JSPatch 的改进,有些内容已与最新代码对不上,建议转看重新整理后的JSPatch实现原理详解


距离上次写的<JSPatch实现原理详解>有一个月的时间,在这段时间里 JSPatch 在不断地完善和改进,代码已经有很多变化,有一些修改值得写一下,作为上一篇的补充。

Special Struct

先说下 _objc_msgForward,在上一篇提到为了让替换的方法走 forwardInvocation,把它指向一个不存在的 IMP: class_getMethodImplementation(cls, @selector(__JPNONImplementSelector)),实际上这样实现是多余的,若 class_getMethodImplementation 找不到 class / selector 对应的 IMP,会返回 _objc_msgForward 这个 IMP,所以更直接的方式是把要替换的方法都指向 _objc_msgForward,省去查找方法的时间。

接着出现另一个问题,如果替换方法的返回值是某些 struct,使用 _objc_msgForward(或者之前的 @selector(__JPNONImplementSelector))会 crash。几经辗转,找到了解决方法:对于某些架构某些 struct,必须使用 _objc_msgForward_stret 代替 _objc_msgForward。为什么要用 _objc_msgForward_stret 呢,找到一篇说明 objc_msgSend_stretobjc_msgSend 区别的文章),说得比较清楚,原理是一样的,是C的一些底层机制的原因,简单复述一下:

大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self / _cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值:

-(int) method:(id)arg;
    r3 = self
    r4 = _cmd, @selector(method:)
    r5 = arg
    (on exit) r3 = returned int

普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些 struct 是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self / _cmd 在寄存器的位置就变了:

-(struct st) method:(id)arg;
    r3 = &amp;struct_var (in caller&#39;s stack frame)
    r4 = self
    r5 = _cmd, @selector(method:)
    r6 = arg
    (on exit) return value written into struct_var

objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法 objc_msgSend_stret 代替。原理大概就是这样。

上面说某些架构某些 struct 有问题,那具体是哪些呢?iOS 架构中非 arm64 的都有这问题,而怎样的 struct 需要走上述流程用 xxx_stret 代替原方法则没有明确的规则,OC 也没有提供接口,只有在一个奇葩的接口上透露了这个天机,于是有这样一个神奇的判断:

if ([methodSignature.debugDescription rangeOfString:@&quot;is special struct return? YES&quot;].location != NSNotFound)

NSMethodSignaturedebugDescription 上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward

(更多…)

JSPatch实现原理详解

2015-6-2 评论(41) 分类:技术文章

注:本文较早撰写,随着 JSPatch 的改进,有些内容已与最新代码对不上,建议转看重新整理后的JSPatch实现原理详解


JSPatch以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具备热更新的能力,在实现 JSPatch 过程中遇到过很多困难也踩过很多坑,有些还是挺值得分享的。本篇文章从基础原理、方法调用和方法替换三块内容介绍整个 JSPatch 的实现原理,并把实现过程中的想法和碰到的坑也尽可能记录下来。

基础原理

能做到通过JS调用和改写OC方法最根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:

Class class = NSClassFromString("UIViewController");
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString("viewDidLoad");
[viewController performSelector:selector];

也可以替换某个类的方法为新的实现:

static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @"");

还可以新注册一个类,为类添加方法:

Class cls = objc_allocateClassPair(superCls, "JPObject", 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);

对于 Objective-C 对象模型和动态消息发送的原理已有很多文章阐述得很详细,例如这篇,这里就不详细阐述了。理论上你可以在运行时通过类名/方法名调用到任何OC方法,替换任何类的实现以及新增任意类。所以 JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。这是最基础的原理,实际实现过程还有很多怪要打,接下来看看具体是怎样实现的。
(更多…)

JSPatch – 动态更新iOS APP

2015-5-25 评论(38) 分类:作品 技术文章 Tags:

JSPatch是最近业余做的项目,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C的原生接口,获得脚本语言的能力:动态更新APP,替换项目原生代码修复bug。

用途

是否有过这样的经历:新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,付出巨大的人力和时间成本,才能完成此次bug的修复。

使用JSPatch可以解决这样的问题,只需在项目中引入JSPatch,就可以在发现bug时下发JS脚本补丁,替换原生方法,无需更新APP即时修复bug。

例子

@implementation JPTableViewController
...
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSString *content = self.dataSource[[indexPath row]];  //可能会超出数组范围导致crash
  JPViewController *ctrl = [[JPViewController alloc] initWithContent:content];
  [self.navigationController pushViewController:ctrl];
}
...
@end

上述代码中取数组元素处可能会超出数组范围导致crash。如果在项目里引用了JSPatch,就可以下发JS脚本修复这个bug:

#import “JPEngine.m"
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [JPEngine startEngine];
    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://cnbang.net/bugfix.JS"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
    NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    if (script) {
      [JPEngine evaluateScript:script];
    }
}];
   ….
    return YES;
}
@end

//JS
defineClass("JPTableViewController", {
  //instance method definitions
  tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
    var row = indexPath.row()
    if (self.dataSource().length > row) {  //加上判断越界的逻辑
      var content = self.dataArr()[row];
      var ctrl = JPViewController.alloc().initWithContent(content);
      self.navigationController().pushViewController(ctrl);
    }
  }
}, {})

这样 JPTableViewController 里的 -tableView:didSelectRowAtIndexPath: 就替换成了这个JS脚本里的实现,在用户无感知的情况下修复了这个bug。

更多的使用文档和demo请参考github项目主页

原理

JSPatch用iOS内置的JavaScriptCore.framework作为JS引擎,但没有用它JSExport的特性进行JS-OC函数互调,而是通过Objective-C Runtime,从JS传递要调用的类名函数名到Objective-C,再使用NSInvocation动态调用对应的OC方法。详细的实现原理以及实现过程中遇到的各种坑和hack方法会另有文章介绍。

方案对比

目前已经有一些方案可以实现动态打补丁,例如WaxPatch,可以用Lua调用OC方法,相对于WaxPatch,JSPatch的优势是:

1.JS语言
JS比Lua在应用开发领域有更广泛的应用,目前前端开发和终端开发有融合的趋势,作为扩展的脚本语言,JS是不二之选。

2.符合Apple规则
JSPatch更符合Apple的规则。iOS Developer Program License Agreement里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的。

3.小巧
使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧。

4.支持block
wax在几年前就停止了开发和维护,不支持Objective-C里block跟Lua程序的互传,虽然一些第三方已经实现block,但使用时参数上也有比较多的限制。

相对于WaxPatch,JSPatch劣势在于不支持iOS6,因为需要引入JavaScriptCore.framework。另外目前内存的使用上会高于wax,持续改进中。

风险

JSPatch让脚本语言获得调用所有原生OC方法的能力,不像web前端把能力局限在浏览器,使用上会有一些安全风险:

1.若在网络传输过程中下发明文JS,可能会被中间人篡改JS脚本,执行任意方法,盗取APP里的相关信息。可以对传输过程进行加密,或用直接使用https解决。

2.若下载完后的JS保存在本地没有加密,在未越狱的机器上用户也可以手动替换或篡改脚本。这点危害没有第一点大,因为操作者是手机拥有者,不存在APP内相关信息被盗用的风险。若要避免用户修改代码影响APP运行,可以选择简单的加密存储。

其他用途

JSPatch可以动态打补丁,自由修改APP里的代码,理论上还可以完全用JSPatch实现一个业务模块,甚至整个APP,跟wax一样,但不推荐这么做,因为:

  1. JSPatch和wax一样都是通过Objective-C Runtime的接口通过字符串反射找到对应的类和方法进行调用,这中间的字符串处理会损耗一定的性能,另外两种语言间的类型转换也有性能损耗,若用来做一个完整的业务模块,大量的频繁来回互调,可能有性能问题。
  2. 开发过程中需要用OC的思维写JS/Lua,丧失了脚本语言自己的特性。
  3. JSPatch和wax都没有IDE支持,开发效率低。

若想动态为APP添加模块,目前React Native给出了很好的方案,解决了上述三个问题:

  1. JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。(详见React Native通信机制详解
  2. 开发过程无需考虑OC的感受,遵从React框架的思想进行纯JS开发就行,剩下的事情React Native帮你处理好了。
  3. React Native连IDE都准备好了。

所以动态添加业务模块目前还是推荐尝试React Native,但React Native并不会提供原生OC接口的反射调用和方法替换,无法做到修改原生代码,JSPatch以小巧的引擎补足这个缺口,配合React Native用统一的JS语言让一个原生APP时刻处于可扩展可修改的状态。

目前JSPatch处于开发阶段,稳定性和功能还存在一些问题,欢迎大家提建议/bug/PR,一起来做这个项目。

(更多…)

iOS富文本组件的实现—DTCoreText源码解析 渲染篇

2015-4-14 评论(13) 分类:技术文章 Tags:

上一篇介绍了DTCoreText怎样把HTML+CSS解析转换成NSAttributeString,本篇接着看看怎样把NSAttributeString渲染出来。

CoreText

先简单介绍下CoreText,CoreText是iOS/OSX里的文字渲染引擎,在iOS/OSX上看到的所有文字在底层都是由CoreText去渲染。

dt1

CoreText会把一行里连在一起相同属性的文字合在一起作为一个CTRun,每一行是一个CTLine,多行合在一起组成CTFrame。如上图,第一行的文字有两种样式,第一部分是加粗,第二部分是斜体,因为样式不同所以分成了两个CTRun,CTLine包含了这两个CTRun,CTFrame包含了所有CTLine。

dt2

一个NSAttributeString可以通过CoreText提供的方法生成CTFramesetter,CTFramesetter是用于创建CTFrame的工厂,给CTFramesetter一个CGPath,或者简单理解为给他一个框框,它就会通过它持有的CTTypesetter生成CTFrame,CTFrame生成时里面包含的CTLine和CTRun就全部生成好了,可以直接绘制到画布上。CTFrame/CTLine/CTRun都提供了渲染接口,但前两者是封装,最后实际都是调用到CTRun的渲染接口去绘制。

如果要用CoreText渲染NSAttributeString,可以简单生成CTFramesetter,再生成CTFrame,在UIView的drawRect方法里直接把CTFrame绘制到当前画布上:

- (void) drawRect:(CGRect)rect
{
     UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 320, 400)];
     CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)content);
     CTFrame frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0), [path CGPath] , NULL);
     CGContextRef ctx = UIGraphicsGetCurrentContext();
     CTFrameDraw(frame, ctx);
}

CoreText会按NSAttributeString里的样式属性把文字渲染出来。这种是最简单的粗粒度的渲染方式,但如果需要对文字渲染再做进一步处理,例如添加背景色等这些CoreText没有支持的属性,或者要在文字中间插入图片,就不能简单绘制CTFrame,需要逐行或逐个CTRun处理。

概览

DTCoreText需要处理穿插在文字里的各类Attachment,并支持文字背景色,段缩进等CoreText不支持的属性,不能简单把NSAttributeString扔给CoreText渲染了事,需要做更细致的处理。DTCoreText分了几层,整体结构图:

dt7

最上层是使用者,可以是Controller,例如项目里示例的DemoTextViewController,也可以是某视图类。接着是DTCoreText封装好的各个控件,自带的有Label,TextView和Cell,这些控件的文字渲染都由DTAttributedTextContentView负责,非文字部分例如图片/视频等元素会在上层使用者那里通过delegate传给DTAttributedTextContentView。DTCoreTextLayouter / DTCoreTextLayoutFrame / DTCoreTextLayoutLine / DTCoreTextGlyphRun这四个类分别对应CoreText里的CTFramesetter / CTFrame / CTLine / CTRun,模仿了CoreText的模式,功能和作用一样,只是在它们基础上添加了功能。接下来看看每一个类具体做了什么事情。

DTAttributedTextContentView

DTAttributedTextContentView继承自UIView,作为DTCoreTextLayoutFrame和上层控件的中间层,负责按需求绘制内容,大致做了以下几件事:

1.支持CATiledLayer分段渲染

把UIView的layerClass设为CATiledLayer就能实现分区域渲染,即只渲染显示在屏幕上的区域,类似那些地图APP的效果,主要用于像TextView这样可能内容很长的控件,避免一次性把全部内容渲染出来,只渲染能看到的部分,提高性能。使用CATiledLayer后,在-drawLayer:inContext:方法里用CGContextGetClipBoundingBox通过context取得当前显示的区域,DTCoreTextLayoutFrame只渲染这个区域的内容就行了。

2.生成DTCoreTextLayoutFrame并绘制

通过上层传进来的NSAttributeString生成DTCoreTextLayouter和DTCoreTextLayoutFrame,进行各种配置后用DTCoreTextLayoutFrame渲染文字到当前layer上,这些配置包括 是否显示图片链接/限定行数/断行规则等。

3.处理Attachment和Link

-layoutSubviewsInRect:方法里遍历DTCoreTextLayoutFrame里的每一个DTCoreTextGlyphRun,找出有附件和链接的Run进行处理,附件包括图片/视频等,创建这些附件对应的view,把这些view按DTCoreTextGlyphRun计算好的位置添加到专门存放附件和链接的customViews上完事。

实际上这些附件view的创建是在上层使用者那里,DTAttributedTextContentView通过delegate把每个附件的内容和对应的frame传到上层生成相应的view再给回来,这样做估计是因为对附件的处理每个使用者的需求都不一样,不应该直接写死在底层,例如有些使用者要求图片需要点击后放大,视频需要用自己的控件等。

DTCoreTextLayouter

DTCoreTextLayouter负责生成和缓存DTCoreTextLayoutFrame,相当于CTFramesetter和CTFrame的关系,做的事很简单,就是通过NSAttributeString生成CTFramesetter,再根据不同的rect生成DTCoreTextLayoutFrame,并缓存这些frame。

DTCoreTextLayoutFrame

DTCoreTextLayoutFrame是最重要的一个类,负责渲染文字,主要做了两件事:生成行和渲染每一行。

生成DTCoreTextLayoutLine

-_buildLinesWithTypesetter:会创建出当前frame范围内可见的每一行DTCoreTextLayoutLine,创建过程中做的处理包括:

1.支持整段缩进

从NSAttributeString里取出当前行是否有表示缩进的DTTextBlock,如果需要缩进,要计算出当前行缩进后的宽度和位置。

2.支持截断加省略号

上层像Label/TextView这样的控件是限制了宽高的,如果内容超出了宽高,就需要对最后一行进行处理,在合适的位置加”…”。

这里有个问题,就是必须在渲染到超出宽高的那一行时,才知道要处理的最后一行是什么。例如一个TextView高40,文字每行高15,在渲染第三行时高已经到45,发现已经超出了TextView的高度,这时知道只能渲染到第二行,但当前已经处理到第三行了,需要把第二行拿出来截断加”…”。

另外除了超出高度,在超出外部传进来的numberOfLines时也要截断,为了统一流程,这里的做法是在渲染超出高度时记录总共可以渲染多少行(_numberLinesFitInFrame),然后全部重新来,从头到尾再生成每一行,这时已经知道总共有多少行,在生成最后一行时处理就行了。这样做优点是简单粗暴避免重复代码,缺点是浪费性能,前面所有行都要重新排一遍。

3.支持hyphen

hyphen是连字符号,就是让英文单词在合适的位置换行并加上破折号”-“。CoreText原生不支持hyphen,断行方式只有按单词断行和安字母断行。这里hyphen的实现方式是:在所有英文单词里可以加破折号的位置全部加上占位符0x00AD,例如location->lo-ca-tion->lo0x00ADca0x00ADtion。0x00AD是不可见字符,CoreText不会渲染这个字符,但在这个字符的位置是可以断行的,CoreText不再认为location是一个单词,会在占位符处换行。DTCoreText做的处理就是如果发现换行处是占位符0x00AD,就替换成破折号”-“,所以要支持hyphen,传进来的内容就必须是所有单词都写好占位符的,否则无效。

4.计算每一行在当前frame的位置

在生成每一行时是不知道这一行在当前frame的位置的,需要自己手动计算。每一行的x坐标容易确定,但y坐标的计算就要费一番功夫。要考虑的因素有当前行高,上一行位置,行距,段间距,padding,baseline等。

dt5

如图,每一行以baseline为基准,需要计算出这一行的baseline在当前frame的Y坐标值,asent与descent是CoreText给出的值,asent+descent就是行高。推算当前行baseline位置的流程是:

  • A.计算上一行的行末位置,即baseline+descent
  • B.计算上一行行间距的一半,例如1.5倍行间距,就是 ((1.5 – 1)*asent+descent)/2
  • C.计算当前行行间距的一半,算法同上,只是这一行的行间距不一定与上一行一致。这里两行各算一半也是为了不同行间距的中和。
  • D.上述计算结果相加,再加上当前行asent值,就得到当前行的baseline Y坐标值。

除了上述主流程,还针对首行,段首段尾,DTTextBlock的留白和附件Attachment做了处理,计算的逻辑在-_algorithmWebKit_BaselineOriginToPositionLine

5.处理对齐

要对每一种对齐方式进行处理,右对齐和居中对齐需要计算出行的x坐标值,两端对齐需要通过CTLineCreateJustifiedLine方法重新创建出一个两端对齐的行,针对两端对齐这里还要了两件事,一是段末不做两端对齐,二是若内容长度不够(默认是不足行宽的60%)也不做两端对齐,避免文字间距拉伸得太厉害效果差。

6.封装成DTCoreTextLayoutLine

经过上述处理,每一行的CTLine对象以及这一行的位置信息都有了,把这些封装成DTCoreTextLayoutLine保存起来,任务就完成了。

渲染

DTCoreTextLayoutFrame对外提供了-drawInContext:options:方法,用于把上述生成的每一行都渲染到传进来的context画布上。做的处理包括:

1.绘制DTTextBlock样式

DTCoreText支持段落加背景色,在这里会先找出所有DTTextBlock,通过一系列麻烦的方法取到这些block的坐标和大小,把它们对应的背景色画出来。

2.绘制附件

实现了DTTextAttachmentDrawing接口的附件可以在这里跟文字一起绘制出来,在DTCoreText里图片附件就是实现了DTTextAttachmentDrawing接口,可以直接把图片在这里绘制出来。实际上图片附件的渲染DTCoreText提供了两种方式,上面介绍DTAttributedTextContentView时说图片附件也可以在上层让用户自行添加,若要在上层自行添加,可以传参数告诉DTCoreTextLayoutFrame绘制时不要处理图片附件。

3.绘制文字和阴影

最后就是再遍历每一行DTCoreTextLayoutLine以及行里的每一个DTCoreTextGlyphRun,调用它的-drawInContext:方法逐个run绘制到画布上。绘制时需要算好每个Run的位置,调用CGContextSetTextPosition定位到指定位置绘制文字。绘制文字同时还处理了阴影效果,CoreText不直接支持文字阴影效果,但可以用CoreGraphic的接口在绘制时加上阴影,这里还支持同时存在多个shadow -_-!

DTCoreTextLayoutLine

DTCoreTextLayoutLine封装了CTLine,做的事包括:

1.生成GlyphRun

通过CTLine可以取出所这一行里的CTRun,计算每个CTRun的位置,封装生成DTCoreTextGlyphRun。

2.计算属性和提供辅助方法

计算并保存了这一行asent/descent/lineHeight等属性,提供各种辅助方法方便获取这一行里的信息,包括通过stringIndex获取对应文字的坐标等,CTLine相关的几个方法例如CTLineGetOffsetForStringIndex() / CTLineGetStringIndexForPosition()也有相应的封装。

DTCoreTextGlyphRun

DTCoreTextGlyphRun里做的事跟DTCoreTextLayoutLine差不多,只是在渲染方法里额外做了一些事,首先支持文字背景色,这是CoreText原生不支持的,如果Attribute里有背景色的属性,这里会绘制出来。然后支持iOS6以下文字的下划线和删除线,iOS6以前CoreText是不支持下划线和删除线的,这里自己做了处理把它画上去。

总结

整个流程最核心的就是DTCoreTextLayoutFrame生成行和渲染的实现,相当于把CoreText原生的CTFramesetterCreateFrame / CTFrameDraw再自己实现了一遍,在实现的过程加上自己特殊的需求,从中我们也可以大致了解到CTFrame/CTLine内部大致实现是怎样的。CoreText已经提供了足够细粒度的接口让使用者可以按自己意愿去随意排版,DTCoreText这一系列的处理给出了很好的示例可供参考。

React Native通信机制详解

2015-3-30 评论(64) 分类:技术文章 Tags:

React Native是facebook刚开源的框架,可以用javascript直接开发原生APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本篇先来看看它最基础的JavaScript-ObjectC通信机制(以下简称JS/OC)。

概览

React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的情况下也可以用webview代替,实际上项目里就已经有了用webview作为解析引擎的实现,应该是用于兼容iOS7以下没有JavascriptCore的版本。

普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。React Native也是以此为基础,通过各种手段,实现了在OC定义一个模块方法,JS可以直接调用这个模块方法并还可以无缝衔接回调。

举个例子,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果。:

//OC:
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
{
     RCT_EXPORT();
     NSString *ret = @"ret"
     responseSender(ret);
}
@end
//JS:
RCTSQLManager.query("SELECT * FROM table", function(result) {
     //result == "ret";
});

接下来看看它是怎样实现的。

模块配置表

首先OC要告诉JS它有什么模块,模块里有什么方法,JS才知道有这些方法后才有可能去调用这些方法。这里的实现是OC生成一份模块配置表传给JS,配置表里包括了所有模块和模块里方法的信息。例:

{
    "remoteModuleConfig": {
        "RCTSQLManager": {
            "methods": {
                "query": {
                    "type": "remote",
                    "methodID": 0
                }
            },
            "moduleID": 4
        },
        ...
     },
}

OC端和JS端分别各有一个bridge,两个bridge都保存了同样一份模块配置表,JS调用OC模块方法时,通过bridge里的配置表把模块方法转为模块ID和方法ID传给OC,OC通过bridge的模块配置表找到对应的方法执行之,以上述代码为例,流程大概是这样(先不考虑callback):

ReactNative1

在了解这个调用流程之前,我们先来看看OC的模块配置表式怎么来的。我们在新建一个OC模块时,JS和OC都不需要为新的模块手动去某个地方添加一些配置,模块配置表是自动生成的,只要项目里有一个模块,就会把这个模块加到配置表上,那这个模块配置表是怎样自动生成的呢?分两个步骤:

1.取所有模块类

每个模块类都实现了RCTBridgeModule接口,可以通过runtime接口objc_getClassList或objc_copyClassList取出项目里所有类,然后逐个判断是否实现了RCTBridgeModule接口,就可以找到所有模块类,实现在RCTBridgeModuleClassesByModuleID()方法里。

2.取模块里暴露给JS的方法

一个模块里可以有很多方法,一些是可以暴露给JS直接调用的,一些是私有的不想暴露给JS,怎样做到提取这些暴露的方法呢?我能想到的方法是对要暴露的方法名制定一些规则,比如用RCTExport_作为前缀,然后用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_为前缀的方法,但这样做恶心的地方是每个方法必须加前缀。React Native用了另一种黑魔法似的方法解决这个问题:编译属性__attribute__。

在上述例子中我们看到模块方法里有句代码:RCT_EXPORT(),模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,那这个宏做了什么呢?来看看它的定义:

#define RCT_EXPORT(JS_name) __attribute__((used, section("__DATA,RCTExport" \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }

这个宏的作用是用编译属性__attribute__给二进制文件新建一个section,属于__DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到__attribute__进行处理,为生成的可执行文件加入相应的内容。效果可以从linkmap看出来:

# Sections:
# Address Size Segment Section
0x100001670 0x000C0180 __TEXT __text
...
0x10011EFA0 0x00000330 __DATA RCTExport
0x10011F2D0 0x00000010 __DATA __common
0x10011F2E0 0x000003B8 __DATA __bss
...

0x10011EFA0 0x00000010 [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__
0x10011EFB0 0x00000010 [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__
0x10011EFC0 0x00000010 [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__
0x10011EFD0 0x00000010 [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__
...

可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的RCTExportedMethodsByModuleID()方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。

整体的模块类/方法提取实现在RCTRemoteModulesConfig()方法里。

调用流程

接下来看看JS调用OC模块方法的详细流程,包括callback回调。这时需要细化一下上述的调用流程图:

ReactNative2

看起来有点复杂,不过一步步说明,应该很容易弄清楚整个流程,图中每个流程都标了序号,从发起调用到执行回调总共有11个步骤,详细说明下这些步骤:

1.JS端调用某个OC模块暴露出来的方法。

2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。

在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的_createBridgedModule里,整个实现区区24行代码,感受下JS的魔力吧。

3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。

4.把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。

5.OC接收到消息,通过模块配置表拿到对应的模块和方法。

实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的RCTModuleMethod对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。

6.RCTModuleMethod对JS传过来的每一个参数进行处理。

RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。

例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。

这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。

7.OC模块方法调用完,执行block回调。

8.调用到第6步说明的RCTModuleMethod生成的block。

9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。

10.MessageQueue通过CallbackID找到相应的JS callback方法。

11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。

整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行

事件响应

上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?

答案是通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。

一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。

说到OC调用JS,再补充一下,实际上模块配置表除了有上述OC的模块remoteModules外,还保存了JS模块localModules,OC调JS某些模块的方法时,也是通过传递ModuleID和MethodID去调用的,都会走到-enqueueJSCall:args:方法把两个ID和参数传给JS的BatchedBridge.callFunctionReturnFlushedQueue,跟JS调OC原理差不多,就不再赘述了。

总结

整个React Native的JS-OC通信机制大致就是这样了,关键点在于:模块化,模块配置表,传递ID,封装调用,事件响应,其设计思想和实现方法很值得学习借鉴。