JSPatch用法剖析
目录
一、JSPatch介绍
用途
iOS产品开发之中常常会遇到这种情况: 新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,付出巨大的人力和时间成本,才能完成此次bug的修复。
JSPatch的出现解决了这样的问题,只需要在项目中引入极小的JSPatch引擎,就可以使用JavaScript语言调用Objective-C的原生接口,获得脚本语言的能力:动态更新iOS APP,替换项目原生代码、快速修复bug。
技术核心
JSPatch核心主要是JSBinding和Objective-C中的runtime技术。一方面,它采用Apple在iOS7中发布的JavaScriptCore.framework作为Javascript引擎解析JavaScript脚本,执行JavaSript代码并与Objective-C端的代码进行桥接。另一方面则是使用Objective-C runtime中的method swizzling的方式达到使用JavaScript脚本动态替换原有Objective-C方法的目的,并利用ForwardInvocation标准消息转发机制使得在JavaScript脚本中调用Objective-C的方法成为可能。
二、JSPatch VS lua Wax
wax是可以实现动态打补丁快速修补Crash的另外一种解决方案,初衷是为了使用lua来编写iOS原生应用而诞生的一个框架。它利用lua的C语言API(可以让C代码与lua进行交互的函数集,包括读写lua全局变量的函数,调用lua函数的函数,运行lua代码片段的函数,注册C函数然后可以在lua中被调用的函数,等等)和 Objective-C 强大的runtime使lua能调用原生Objective-C接口,可以使用lua创建,继承,扩展oc类,使用lua实现oc所能实现的所有功能。
lua wax由几个部分组成:
wax stdLib,是一个lua脚本库,利用前面提到的C API和Objective-C runtime向lua脚本提供与Objective-C类交互的接口;
Wax Engine,提供使用Objective-C加载运行lua脚本和传递变量给lua脚本的接口;
lua Compiler,即lua解释器,wax Engine调用解释器加载并编译运行lua脚本。
相比于wax, JSPatch有以下的优势
Javascript比lua在应用开发领域有更广泛的应用。 目前前端开发和终端开发有融合的趋势,作为扩展的脚本语言,JavaScript是不二之选。
更符合Apple的规则。iOS Developer Program License Agreement里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的。
小巧。 使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧。而Wax需要导入c代码写的lua引擎。
不需要担心内存回收的问题。JavascriptCore.framework通过GC来对垃圾进行回收。而lua wax需要显式调用内存回收方法。
支持armv7 armv7s arm64框架。wax并不支持arm64框架。
而JSPatch也有自身的缺点:
不支持iOS6及以下,因为JSPatch依赖于iOS7及以后的JavascriptCore.framework (这点现在可以忽略,因为微信最低的版本要求已经是iOS7)
调用OC方法的性能慢于lua wax
启动JSPatch所占用的内存多于wax
三、JSPatch核心原理解析
startEngine
1 2 3 4 |
|
使用JSPatch框架首先要调用JPEngine
中的类方法startEngine
,这个方法的是为了初始化JSContext,JSContext是JS脚本的运行环境。JS脚本可以调用在JSContext中预先定义的方法,方法的参数/返回值都会被JavaScriptCore.framework自动转换,OC里的NSArray,NSDictionary,NSString,NSNumber,NSBlock,[NSNull null]会分别转为JS端的Array/Object/String/Number/function/null。
那其他无法通过JavascriptCore.framework进行bridge转换的数据类型,比如自定义的类的对象,Class类型,指针,要如何在JS和OC两端进行传递呢?
JSPatch中使用了一个叫做JPBoxing的类去封装id、指针、Class类型变量,封装完以后这个Boxing对象会被放在一个NSDictionary里(NSDictionary可转化为JS中的Object类型),传递给JS代码。后面会对JPBoxing进行详细的介绍.
回到startEngine方法:
1 2 3 4 5 6 7 8 9 10 11 |
|
在这里定义的函数主要是负责处理转换从JS端传过来的参数,然后在OC端运用runtime里的方法实现生成新的类、替换旧的类、调用方法等等功能。
其中_OC_defineClass
负责定义新的类或替换原有的类,_OC_callI
负责调用实例方法,_OC_callC
负责调用类方法。
除了这三个函数之外,startEngine中还封装了一些常用GCD方法、console.log、sizeof、Javascript异常捕获函数等等。
准备完JSContext之后,就可以加载从网络中下载的JS补丁,调用[JPEngeine evaluateScript:script]
方法执行脚本。
defineClass
接下来讲解JSPatch中如何定义一个类以及怎么覆盖原方法或新增一个方法。
1 2 3 4 5 6 |
|
defineClass
函数可接受三个参数:
- 字符串:”需要替换或者新增的类名:继承的父类名 <实现的协议1,实现的协议2>”
- {实例方法}
- {类方法}
将这三个参数通过bridging传入到OC后,执行以下步骤:
- 使用NSScanner分离classDeclaration,分离成三部分
- 类名 : className
- 父类名 : superClassName
- 实现的协议名 : protocalNames
- 使用NSClassFromString(className)获得该Class对象。
- 若该Class对象为nil,则说明JS端要添加一个新的类,使用
objc_allocateClassPair
与objc_registerClassPair
注册一个新的类。 - 若该Class对象不为nil,则说明JS端要替换一个原本已存在的类
- 若该Class对象为nil,则说明JS端要添加一个新的类,使用
- 根据从JS端传递来的实例方法与类方法参数,为这个类对象添加/替换实例方法与类方法
- 添加实例方法时,直接使用上一步得到class对象; 添加类方法时需要调用
objc_getMetaClass
方法获得元类。 - 如果要替换的类已经定义了该方法,则直接对该方法替换和实现消息转发。
- 否则根据以下两种情况进行判断
- 遍历protocalNames,通过
objc_getProtocol
方法获得协议对象,再使用protocol_copyMethodDescriptionList
来获得协议中方法的type和name。匹配JS中传入的selectorName,获得typeDescription字符串,对该协议方法的实现消息转发。 - 若不是上述两种情况,则js端请求添加一个新的方法。构造一个typeDescription为”@@:\@****”(返回类型为id,参数值根据JS定义的参数个数来决定。新增方法的返回类型和参数类型只能为id类型,因为在JS端只能定义对象)的IMP。将这个IMP添加到类中。
- 遍历protocalNames,通过
- 添加实例方法时,直接使用上一步得到class对象; 添加类方法时需要调用
- 为该类添加
setProp:forKey
和getProp:
方法,使用objc_getAssociatedObject
与objc_setAssociatedObject
让JS脚本拥有设置property的能力 - 返回{className:cls}回JS脚本。
overrideMethod方法
不管是替换方法还是新增方法,都是使用overrideMethod
方法。
它接受五个参数:
- 类名
- 要替换的方法名
- JS中定义的方法
- 是否类方法
- 方法的typeDescription
原型如下
1
|
|
逻辑步骤如下
- 初始化:更具selectorName获取对应的Selector;typeDescription获得NSMethodSignature方法签名。
- 保存原有方法的IMP,添加名为
@"ORIG" + selectorName
的方法,IMP为原方法的IMP。 - 将原方法的IMP设置为消息转发
- 若该方法的返回值为特殊的struct类型,则需要将IMP设置为
(IMP)_objc_msgForward_stret
- 否则的话将IMP设置为
_objc_msgForward
- 若该方法的返回值为特殊的struct类型,则需要将IMP设置为
- 保存原有转发方法
forwardInvocation:
的IMP,添加selectorName为@”ORIGforwardInvocation:”,IMP为原转发方法IMP的方法。 - 将原转发方法替换为自己的转发方法
JPForwardInvocation
- 根据替换/添加方法的返回类型,选择不同的替换IMP(使用宏的形式定义),替换原方法。
callSelector方法
在JS端调用OC方法时,都需要通过在OC端通过callSelector
方法进行方法的查找以及参数类型、返回类型的转换和处理。
该方法接受五个参数
- 调用对象的类名
- 被调用的selectorName
- JS中传递过来的参数
- JS端封装的实例对象
- 是否调用的是super类的方法
方法的原型:
1
|
|
逻辑步骤如下
- 初始化
- 将JS封装的instance对象进行拆装,得到OC的对象;
- 根据类名与selectorName获得对应的类对象与selector;
- 通过类对象与selector构造对应的NSMethodSignature签名,再根据签名构造NSInvocation对象,并为invocation对象设置target与Selector
- 根据方法签名,获悉方法每个参数的实际类型,将JS传递过来的参数进行对应的转换(比如说参数的实际类型为int类型,但是JS只能传递NSNumber对象,需要通过
[[jsObj toNumber] intValue]
进行转换)。转换后使用setArgument方法
为NSInvocation对象设置参数。 - 执行invoke方法。
- 通过getReturnValue方法获取到返回值。
- 根据返回值类型,封装成JS中对应的对象(因为JS并不识别OC对象,所以返回值为OC对象的话需封装成{className:className, obj:obj})返回给JS端。
JPForwardInvocation方法
JPForwardInvocation方法替换了原有-forwardInvocation
方法的实现,使得消息转发都通过该方法,并将消息转发给JS脚本中定义的方法,通过JavascriptCore.frameWork中提供的callWithArguments
方法调用JS方法达到替换原方法,添加新方法的目的。是实现替换和新增方法的核心。
它的原型与ForwardInvocation方法相同
1
|
|
它的内部逻辑并不复杂,主要是读取出传入的invocation对象中的所有参数,根据实际参数的类型将JSValue类型的参数转换成对应的OC类型,最后将参数添加到_TMPInvocationArguments数组以供JS调用。
那如果有一些类确实有用到这个方法进行消息转发(比如为了实现多继承),那原来的逻辑该怎么办?
JSPatch在替换-forwardInvocation:
方法前会新建一个方法-ORIGforwardInvocation:
,保存原来的实现IMP,在新的-forwardInvocation:
实现里做了个判断,如果转发的方法是JS脚本中想改写的,就走-JPForwardInvocation:
逻辑,若不是,就调用-ORIGforwardInvocation:
走原来的流程。
对象的持有/转换
原作者bang在他的博文中,有较为详细的说明。下面引用了他文章中关于对象持有/转换的细节.
UIView.alloc() 通过上述消息传递后会到OC执行 [UIView alloc],并返回一个UIView实例对象给JS,这个OC实例对象在JS是怎样表示的呢?怎样可以在JS拿到这个实例对象后可以直接调用它的实例方法 (UIView.alloc().init())?
对于一个自定义id对象,JavaScriptCore会把这个自定义对象的指针传给JS,这个对象在JS无法使用,但在回传给OC时OC可以找到这个对象。对于这个对象生命周期的管理,按我的理解如果JS有变量引用时,这个OC对象引用计数就加1 ,JS变量的引用释放了就减1,如果OC上没别的持有者,这个OC对象的生命周期就跟着JS走了,会在JS进行垃圾回收时释放。
传回给JS的变量是这个OC对象的指针,如果不经过任何处理,是无法通过这个变量去调用实例方法的。所以在返回对象时,JSPatch会对这个对象进行封装。
首先,告诉JS这是一个OC对象:
1 2 3 4 5 |
|
用__isObj表示这是一个OC对象,对象指针也一起返回。接着在JS端会把这个对象转为一个 JSClass 实例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
接着看看对象是怎样回传给OC的。上述例子中,view.setBackgroundColor(require(‘UIColor’).grayColor()),这里生成了一个 UIColor 实例对象,并作为参数回传给OC。根据上面说的,这个 UIColor 实例在JS中的表示是一个 JSClass 实例,所以不能直接回传给OC,这里的参数实际上会在 c 函数进行处理,会把对象的 .obj 原指针回传给OC。
整个对象的持有/转换的流程图如下:
四、JSPatch Extension机制
如何在JSPatch中预定义C API供JS调用
上面已经介绍过JSPatch是运用Objective-C runtime和JSBinding技术来在JS中调用Objective-C的方法,但是C API是没法通过runtime技术来获取的。一开始的时候我想使用dlsym
函数通过函数名来获取对应的函数指针,通过JS脚本传入C函数的函数名来进行函数调用。但实际上还需要预先定义一个相同类型的函数指针才能调用,做不到完全的动态调用。而且还有一个问题就是像CGRectMake这种,实质上是内联函数,并没有对应的函数地址。更关键的是,没有办法获取C函数的签名,而JS中调用函数是没有具体类型的,传递到OC是以JSValue对象的形式,必须通过转换才能调用对应的C函数。最后的解决方法便是预先在JSContext中提供JS方法和C函数的桥接方法。
这里以定义CGRectMake()来作为例子,如果想在JS中使用CGRectMake()函数,则需要在JPEngine启动的时候,将CGRectMake预定义在JSContext之中。
而且有一点要注意的,CGRectMake返回的并不是一个对象,而是一个struct类型的变量。struct类型是无法返回到JS环境的,所以要转换成NSDictionary的形式。
Extension中需要定义对应的方法来将struct转换成NSDictionary
1 2 3 4 5 6 7 8 9 |
|
这样就可以在startEngine中定义CGRectMake方法了,具体如下
1 2 3 4 |
|
在JS中就可以如此调用桥接函数
1
|
|
这时候就需要一个Boxing对象对指针和Class这些在JS中无法使用的变量类型进行装箱(box);在JS中调用OC或C方法后,传递回到Objective-C端的再进行拆箱(unbox)。
Boxing对象的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
注意到unbox里的一个return self
的写法,这里是一个trick。因为前面介绍到的formatJSToOC
函数的定义如下
1
|
|
这个函数需要负责处理JS到OC端的类型转换,但是如果变量类型是指针或者Class类型的话就和无法和id类型写在同一个处理函数里。所以如果是JPBoxing中的obj为nil,则说明是非id类型,直接返回这个JPBoxing。外部得到的这个JPBoxing对象,则再进行相应类型拆箱。
使用这个Boxing类,调用Extension中的C API时,对指针拆箱,再调用实际的C方法; 返回时,对JS中无法使用的类型进行装箱后再返回; 根据这个机制便可实现对大部分C API的封装。下面以UIGraphicsGetCurrentContext()
为例:
效果如下:
使用JPExtesnion扩展机制对C API和Struct进行扩展
在上一节,我对如何在JSPatch中调用C API进行了介绍。 但是面对大量的C API,需要一个满足以下需求的扩展机制:
- 可模块化加载
- js脚本可动态加载
- 可以在extension中添加struct类型
以下是JPExtension协议的定义,所有的C API扩展都需要继承JPExtension协议
1 2 3 4 5 6 7 8 |
|
开发者可在- (void)main:(JSContext * )context
中添加C API,C API会被添加到JS所在的执行环境中。而后面的三个方法从方法名可以知道,extension中如果要定义struct的话则需要实现这三个方法。因为JS中是无法定义和使用c struct的,所以需要提供相应的互相转换方法(struct与NSDictionary互相转换),具体实现以CGAffineTransform
为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
实现了这三个方法后,JPEngine会将实现了这三个方法extesnion放入_structExtension内。当在JS中调用含有相关struct的方法时,JSEngine会遍历整个_structExtension,找到相应的转换方法。
根据JPExtension协议,模块化加载就变得非常简单:
1 2 3 4 5 6 7 |
|
在JS脚本则可以这样调用:
1 2 3 4 |
|
当然,为了提高项目的性能,你也可以只调用你需要的模块。
C API定义时需要注意的问题
C API中,有大量的参数或者是返回类型都是指针,包括像CGContextRef这种也是指针,而OC对象在JS环境中也是无法使用的。上面的章节已经提到了从OC端返回给JS端时必须用一个封装对象(JPBoxing)来将指针和对象封装起来。JPExtension提供了以下API来封装OC中的对象和指针成JPBoxing和将JPBoxing对象。
1 2 3 4 |
|
C API封装实例:
1 2 3 4 5 6 7 8 9 10 |
|
注意到UIGraphicsGetCurrentContext()
中返回的是一个CGContextRef类型,所以添加这个扩展API的时候需要将返回类型改为id类型,并将CGContextRef指针封装在JPBoxing中。而UIGraphicsBeginImageContext()
需要的是一个CGSize参数,这时候需要在JS端传入一个{x:100, y:100}
的Javascript object,这个object会在OC中被转换为NSDictionary.
C API的返回值也需要判断返回值的类型来进行不同的封装,当返回的结果是JavascriptCore.Framework所不支持转换的类型(NSArray,NSDictionary,NSString,NSNumber,NSBlock),则需要通过formatOCToJS:
方法来封装返回。而且返回类型是NSArray,NSDictionary,NSString时,如果你直接返回,JavascriptCore会将返回值转换为JS中的Array,Object,String,你就无法再使用OC的方法。如果你想在JS中使用这三种类型的方法,也需要用formatOCToJS:
方法进行封装。
在JSPatch中的操作内存与&取地址运算符
与C语言不同,Javascript不能显式的声明一个指向某块内存的指针,也没有&
取地址运算符,Javascript是根据参数是引用类型还是基本类型决定传递引用还是传参。但是指针与取地址在C语言以及OC中都会时常被用到。比如如下的情况:
1 2 3 4 |
|
JPMemory扩展解决了这个问题,其中封装了内存操作中常用的一些常用的c函数。包括malloc
,memset
,free
,memcpy
,memncpy
,memmove
。
而对&
取地址运算符,JPMemory扩展也进行了函数封装,在JS补丁中可以对调用getpointer
方法获取对象的指针、指针的指针,针对上述的代码,现在便可以以以下的形式调用。
1 2 3 4 |
|
getpointer的底层源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
而通过JPMemory中的pval或添加pvalWithXXX便可获得指针所指的对象或XXX类型的变量。
1 2 3 4 5 6 7 8 9 10 11 |
|
include函数
在JSPatch最新的更新中,支持了在JS中调用include方法。可以在一个JS文件中加载其他JS文件,包括补丁脚本、第三方脚本。
使用方法如下:
1 2 3 4 5 |
|
在我自己的分支中,include函数支持加载选项。默认加载选项是兼容方式加载(为满足支持OC,会通过正则表达式替换部分函数的调用方法),而第三方库是不需要被改变的。第二个参数是加载选项,默认是0或者不传入第二个参数,加载第三方库是1。
1 2 3 |
|
五、JSPatch中的实现技巧
GCD的实现
JSPatch采用的是预先在JSContext中封装了对GCD的调用,才能在JS中使用GCD,其代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
其中有三点需要注意:
在block里是不能直接使用context的,因为会造成循环引用。所以在这里有两个处理方式,要么是使用__weak修饰符,要么就是使用JavascriptCore.framework提供的api
[JSContext currentContext]
。在调用JSContext的
callWithArguments:
实例方法时,需要先保存JSContext中的实例对象self
,调用完之后再重新赋值回去。否则在调用完JS方法后,self
会变成nil还有一点就是在
dispatch_sync_main
这个方法里,作者对代码所在的运行线程进行了一个判断,如果已经在主线程中就直接执行这个block,防止了死锁的发生。
处理JS脚本的异常
如果JS脚本出现了异常的话,在OC这边是不会知道的,需要使用JavaScriptCore.framwork中的exceptionHandler才能捕获这个异常,具体代码如下
1 2 3 4 |
|
使用#pragma来抑制warning
作者使用#pargama宏来对一些warning进行了抑制,详细的介绍可以看参考Matt Thomson写的一篇关于clang diagnostics的文章,里面提供了一个网站详细地记录了抑制各种warning的写法。
使用宏来预定义IMP函数
由于要替换原有的函数实现,所以要预先定义好各种返回类型的IMP函数。如果全部写出来的话,将会耗费大量篇幅来写差不多的函数实现,这里作者使用了宏来进行替换,具体代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
六、遇到的问题与解决方法
在了解JSPatch的过程中,也遇到过一些比较棘手的问题,这里总结一下。
1. 在JSPatch中初始化UIWebView导致HTML无法渲染
这个问题最初是由一个JSPatch用户在issue中提出。具体的表现是当在JSPatch中声明一个UIWebView,并对一个URL调用loadRequest方法后,无法渲染该HTML页面。
后来经过查资料发现在JSContext中初始化UIWebView就会出现这个问题。
我猜测是UIWebView
在初始化的过程中会初始化一个全局的JSContext
对象,但是JSPatch也有用到JSContext
,这其中造成了某种冲突。导致在UIWebView
初始化JSContext失败。
解决方法:
1、在JSPatch之外启动调用一次UIWebView
的init
方法。
1 2 3 4 5 6 7 8 9 |
|
2、在JSPatch中显式地创建UIWebView的JSContext
1 2 3 4 5 6 |
|
2. 无法调用参数为id * 类型的方法
在上面的章节中我介绍了我是如何在JS中调用一个参数是指针的方法:
- 通过
JPMemoery
Extension中的malloc
声明一个指针或者调用getpointer
去获得一个对象的地址。 - 调用方法后使用
pval
方法来获得指针所指的对象。
这种方法调用像NSInvocation中的- (void)getReturnValue:(void * )retLoc
、- (void)setArgument:(void * )argumentLocation atIndex:(NSInteger)idx;
以及一些C API没有任何问题。
但是如果调用NSString中的- (BOOL)writeToFile:(NSString * )path atomically:(BOOL)useAuxiliaryFile encoding:(NSStringEncoding)enc error:(NSError * * )error
则会发现传进去的指针参数所指向的NSError对象在回到JS环境的时候已经被释放了。
实际上NSError * 参数在编译器中会被解释成NSError \–__ autoreleasing*类型,该对象生成后将会被加入到autoreleasing- pool中,开发者无法控制他的释放时机。随后,我对各个关键点进行打断点对这个自动释放对象进行跟中,发现这个对象一回到JSContext的执行环境就会被释放。
解决方法:
解决这个问题的思路是在调用完参数为id * 类型的方法后(callSelector
中调用完[invocation inoke]
后),对该自动释放对象进行强引用,使得这个对象在回到JS环境的时候依然存在。这里我使用了一个NSDictonry作为临时自动释放对象的内存池,当生成一个自动释放对象后,将其添加到内存池中(key为其内存地址),使得这个自动释放对象被内存池强引用。保证了在JS环境中可以访问到该对象,不需要该对象时再手动释放该对象(remove掉内存池的对象)。具体逻辑如下:
1 2 3 4 5 6 7 8 9 10 11 |
|
当参数类型是指针的时候进行一个判断,如果指针所指的是id类型。则将JPBoxing加入到一个_markArray中。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
如果_markArray大小不为零,则将数组中的JPBoxing中指针所指对象加入到内存池中(key为对象的hashCode),使得自动释放对象被强引用。
1 2 3 4 5 6 7 8 9 |
|
JSPatch脚本中调用releaseTmpObj
方法手动释放该临时对象。
七、总结
JSPatch中我的总结如下:
1、 为支持C API的扩展,增加了对指针和Class变量的封装以及拆解。因为JS中并不能使用以及声明一个指针,而在C API中,指针作为参数是非常常见的,所以需要将指针封装在一个JPBoxing中,并以{“__ className”:”JPBoxing”,”__ obj”:[JPBoxing boxPointer:pointer]}的形式回传回JS。下次调用的时候再将__obj中的指针拆解出来。
2、 支持对第三方库的调用。在include方法中增加了对第三方js库调用的支持。
3、 添加了sizeof方法。通过遍历加载到JS中的Extension中实现JPExtension协议的- (size_t)sizeOfStructWithTypeName:(NSString * )typeName
方法,使得在JS端可以通过传入一个字符串的形式:sizeof('CGRect')
来获取不同strut类型变量的大小,配合JPMemory扩展中的方法可以malloc出一块指定大小的内存,传入到某些C API中。
4、 添加JPMemory扩展,Javascript中是无法操作具体的内存也没有指针的概念,但是通过JPMemory扩展,可以让JSPatch拥有操作与访问内存的功能,以及获取对象的指针、指针的指针的功能。
5、 添加了保存的__autoreleasing对象机制,使自动释放对象在返回到JS运行环境的时候不会被自动释放,在JS脚本可以继续访问。使用一个NSDictonry作为临时自动释放对象的内存池,当生成一个自动释放对象后,将其添加到内存池中(key为其内存地址),使得这个自动释放对象被内存池强引用。保证了在JS环境中可以访问到该对象,不需要的时候再手动释放该对象(remove掉内存池的对象)。
维护:
1、 修复了在JS中传递nil参数时造成的崩溃。JavaScript中并没有nil这个类型的参数,只有undefined以及null类型。桥接的时候JavascriptCore.framework会将js中的null类型转换为OC中的[NSNull null]类型,所以在调用方法时必须加以判断,将[NSNull null]转换为nil。为了防止真需要传递[NSNull null]参数,在JS中设置了一个名为nsnull的全局变量,开发者如要传递[NSNull null]则可以使用nsnull。
2、 修复了使用JSPatch判断struct时造成的误判。原作者bang原本使用的@encode()方法产生的C字符串来遍历加载到JS中的所有extension的- (size_t)sizeOfStructWithTypeEncode:(NSString * )typeName
方法,@encode产生结果是这样的形式:{CGPoint=dd}
,并使用
1
|
|
进行匹配。但事实上这样的方法是会导致CGPoint被误判为CGRect,因为CGRect进行@encode的结果为{CGRect={CGPoint=dd}{CGSize=dd}}
。而后bang提议改为
1
|
|
但是这样会与sizeof功能有所冲突,而且在随后的测试中我发现@encode(NSRange)的结果为{_NSRange=QQ}
,所以检测location==1也是不可以行的。最后我通过提取出typeEncode中第一个Struct的名字解决了这一问题
3、 修复了JSPatch添加Extension时造成无限循环加载的问题。
代码提交情况: 1916++ / 348— 及 commit log
未来计划
JSPatch功能上已能满足hot fix动态修复的基本需求。接下来的工作重点主要添加脚本的传输和脚本加密的机制,防止未授权的第三方对脚本进行串改;集成进项目中测试实际性能和内存占用情况,改善性能;根据业务需求继续添加常用扩展API。
(持续更新中…)