JSPatch进阶

JSPatch 开源以来大部分被用于 hotfix,替换原生方法修复线上bug,但实际上 JSPatch 一直拥有动态添加功能模块的能力,因为 JSPatch 可以创建和调用任意 OC 类和方法,完全可以用 JSPatch 写功能模块,然后动态下发加载。只是之前在性能和开发体验上有些问题,还没有太多这方面的应用。这次 JSPatch 做了较大的更新,扫除这些问题,让用纯 JS 写功能模块变得实用。这里有个用 JS 写的 Dribbble 客户端 Demo,可以体验下效果。
来看看这次更新做了什么。

性能优化

通过工具可以看到使用 JSPatch 写功能模块时,耗时较多的点在于 JS 和 OC 的通信,以及通信过程中参数的转换,于是在这块寻找优化点。写功能时需要新增很多类和方法,例如:
'
defineClass('JPDribbbleView:UIView', { renderItem: function(item) { ... },})defineClass('JPDribbbleViewController:UIViewController', { render: function(){ var view = JPDribbbleView.alloc().init(); view.renderItem(item); }});

上面两个都是新增的类,两个方法也是新增的,按之前的流程,这里的定义会传入 OC,在 OC 生成这两个类,并在这个类上添加这里定义的方法,调用时进入 OC 寻找这些方法调用。
例如上面的view.renderItem(item)
这句,调用流程是:
进入 __c 函数 -> 转换参数item类型(JS-OC) -> 进入 OC -> callSelector -> 字符串转 Class & Selector 并调用 -> 进入 JPForwardInvocation -> 包装参数 & 转换类型(OC-JS) -> 调用 JS 上的 renderItem 方法 -> 转换返回值类型(OC-JS)。

一个简单的方法调用要经过这么多处理,对于 hotfix 来说这样的新方法调用不常见,调用次数也少,但如果用于做业务模块,就会有大量这种新方法的调用,影响性能。实际上这么多处理都是不必要的,在 JS 定义的方法还要跑到 OC 绕一圈再回来调用,所以优化思路很明显,就是不要绕去 OC,直接在 JS 调用。
经过一轮优化,更新后的 JSPatch 上述的调用流程变成:
进入 __c 函数 -> 调用 JS renderItem 方法。

很清新的流程,去除了所有多余的处理,极大地提高了性能。实现原理就是在 JS 端用一个表保存 className 对应的 JS 方法,调用时如果在这个表找到要调用的方法,就直接调用,不再去到 OC,细节上可以直接看代码。
这个优化不需要使用者做什么修改,书写这些方法的接口并没有变,跟原来一样。实际上实现过程中碰到最大的问题就是接口问题,有机会再分享下这个过程。
那么经过这次优化,这种调用性能提高多少呢?
经测试,不带参数的方法调用提高45倍
,带一个普通参数的方法调用提高70倍
,带像 NSDictionary / NSArray 这些需要转换的参数时提高700倍
。测试用例可以在 Demo 工程找到。
Property
之前 JSPatch 给类新增 property 是通过-getProp:
,-setProp:forKey:
这两个接口:
defineClass('JPTableViewController : UITableViewController', { dataSource: function() { return self.getProp('data'); }, setup: function() { self.setProp_forKey([1,2,3], 'data') }}

这里有两个问题:
接口不友好,与 OC 原生property 的写法不一致,写起来别扭。

每个 property 都是 OC 里的一个 associatedObject,每次存取都要与 OC 通信,大量调用时性能低。

对于hotfix,很少有新增 property 的需求,接口挫点没关系,但若是用来写新功能,property 是家常便饭,就得好好优化了。 这次更新后,可以这样新增property:
defineClass('JPTableViewController : UITableViewController', [ 'data', 'name',], { dataSource: function() { return self.data(); }, setup: function() { self.setData([1,2,3]) }}

接口上做到跟原有 property 一致,解决第一个问题。
对于第二个问题,具体实现上不再是一个 property 对应一个associatedObject
,而是每个对象只有一个对应的associatedObject
,这个associatedObject
的值是一个 JS 对象,每一个 property 都存在这个JS对象上。
[图片上传中。。。(1)]
如图,左边是修改之前的,右边是修改后的。修改前每一个 property 都单独保存在 OC,一个 property 对应一个associatedObject
,JS 通过接口去存取。修改后一个 OC 对象只有一个associatedObject
,这个associatedObject
是个 JS 对象,所有 property 集中在这个 JS 对象里面,JS 可以直接对它进行存取操作。
这样做的好处在于在存取 property 时减少了 JS 与 OC 的通信,不需要每次都与 OC 通信,只需要第一次取出这个关联对象,后续对所有 property 的存取操作都是在 JS 内部进行,提高了性能。这个主意来自老郭(samurai-native作者)的脑洞,在此感谢~
defineJSClass()
经过上述优化,defineClass()
里方法调用的性能是提高了,但像数据层的 dataSource / manager 这些不需要依赖 OC 的类也使用defineClass()
定义还是会比较浪费,因为定义后会生成对应的 OC 类,并在alloc
时还是要去到 OC 生成这个对象,property 的存取还是要通过associatedObject
,这些都是没必要的。
这种类型的类与 OC 没有联系,不需要继承 OC 类,只在 JS 使用,所以直接使用 JS 原生类就行了,可以减少上述性能上的浪费。只是 JS 原生类定义和对象生成的那套写法与defineClass()
的写法相去甚远,两种风格混在一起开发体验不太好,于是加了个defineJSClass()
接口,辅助创建纯 JS 类:
defineJSClass('DBDataSource', { init: function(){ this.data = 'xxx'; return this; }, readData: function() { this.super().loadData(); return this.data; }}, { shareInstance: function(){ ... }})var dataSource = DBDataSource.alloc().init();DBDataSource.shareInstance();

可以看到defineJSClass()
的写法与defineClass()
几乎完全一样,包括实例方法/类方法/继承的写法/super调用/对象生成都是一样的,只有有两个地方不同:
用 this 关键字代替 self

property 不用 getter/setter,直接存取。

这种方式定义类和使用是比defineClass()
性能高的,推荐不需要继承 OC 类时都用这个接口。
autoConvertOCType()
还有一个棘手问题,也是使用 JSPatch 时最让人迷惑的一点,就是NSDictionary
/NSArray
/NSString
这几个类型与 JS 原生Object
/Array
/String
互转的问题。
以数组为例,OCNSArray
数组传回给 JS 时都会当成一个普通的 OC 对象,可以调用它的 OC 方法(像-objectAtIndex
),而 JS 上创建的数组是 JS 数组,可以用[]
取数组元素,不能调用 OC 方法。JS 数组传入 OC 会自动转为NSArray
,传出来会变成NSArray
。于是在 JS 端数组就会有两种类型,你知道某个变量是数组后,还需要知道它是从哪里来的,以此判断它的类型,再用相应的方法。
初期这样做是为了保持功能的完整性,像NSMutableDictionary
/NSMutableArray
/NSMutableString
如果传到 JS 时自动转为 JS 类型就没法对这个对象进行修改了。
好在对于 hotfix 来说问题还不算大,因为代码量小很容易看出来源判断它的类型,但对于写功能模块,这里就很容易会被绕晕了。于是加了个开关autoConvertOCType()
,可以自由开启和关闭自动类型转换。只要在 JS 脚本开头加上autoConvertOCType(1)
这句调用,上述几个类型在通信过程中都会自动转为 JS 类型,在 JS 上不再存在两种类型,只有一种 JS 类型,无需多考虑,这样开发起来就轻松多了。
那若需要调用这些类型 OC 对象的一些方法时怎么办?在调用前后先关后开即可:
autoConvertOCType(0)var str = NSString.stringWithString('xx');var data = str.dataUsingEncoding(4);autoConvertOCType(1)

其他

这次更新还包括完善了super
的调用,解决某些情况下调用super
死循环的问题。另外原先放在扩展的include()
接口合入作为核心功能提供,会自动把主脚本所在目录作为根目录去寻找 include 的文件路径,并保证只 include 一次,还增加了resourcePath()
接口用于静态资源文件的获取。

后续

之前说到阻碍 JSPatch 用于动态更新的障碍有两个:性能问题和开发效率,这次更新后 JSPatch 在这两个方面都有所提升,接下来继续在使用的过程中挖掘更多的优化点,提供一些常用的静态变量和方法封装,并尝试做 XCode 代码自动补全插件提高开发效率。

最后

现在可以通过 JSPatch 用 JS 写完整的功能模块,再动态下发给 APP 执行,JSPatch 的这套动态化方案相对于 React Native 有以下优势:
小巧。只需引入JPEngine.h
JPEngine.m
JSPatch.js
三个小文件,体积小巧,也无需搭建环境。

学习成本低。可以继续沿用原来 OC 的思维写程序,无需学习新一套规则,即刻上手。

限制少。可以说完全没有限制,OC / JS 上玩出花的各种模式都可以照搬使用,不会被某一框架思维和写法限定。所有 OC / JS 库直接使用,无需适配。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • http://blog.cnbang.net/tech/2808/ JSPatch实现原理详解 注:本文较早撰写,...
    hypercode阅读 1,179评论 0 1
  • 转载:原文链接 http://blog.cnbang.net/tech/2808/ JSPatch以小巧的体积做到...
    made_China阅读 390评论 0 0
  • JSPatch是一个可以在线修复bug的轻量级框架,项目中嵌入这个框架可以让你的app具有热更新的能力。你可以通过...
    daixunry阅读 6,003评论 5 38
  • 目标发现力,持续学习力,专业构筑力。具备业界通用的专业知识和技能,积累作为领导者的扎实经验和技巧。匠心,创新...
    喜光一日阅读 175评论 0 0
  • 曾祖父去年90岁,曾祖母去世已经整整三年。 有人说,:从前车马很慢,书信很远,一生只够爱一人。他们二十岁结婚,相守...
    瓦舍阅读 749评论 14 24