新建 Swift 工程 SwiftJSPatch 。 AppDelegate.swift :
// in AppDelegate.swift ----------------
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
let path = NSBundle.mainBundle().pathForResource("main", ofType: "js")
do {
let patch = try String(contentsOfFile: path!)
JPEngine.startEngine()
JPEngine.evaluateScript(patch)
} catch {}
return true
}
ViewController 中设置两个自定义属性: public 属性 a , private 属性 pa :
// in ViewController.swift ---------------------
class ViewController: UIViewController {
var a = "a"
dynamic private var pa = "pa"
override func viewDidLoad() {
print("ORIG title:/(self.title!)")
print("ORIG a:/(a)")
print("ORIG pa:/(pa)")
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
main.js 中去获取这两个自定义属性并各自赋新值,此外为 ViewController 继承自父类 UIViewController 的属性 title 设置新值:
// in main.js --------------------------
defineClass('SwiftJSPatch.ViewController', {
viewDidLoad: function() {
self.setTitle('NEW VC')
console.log('title: '+self.title().toJS())
var a = self.a()
console.log('a: ' + a.toJS())
var pa = self.pa()
console.log('pa: ' + pa.toJS())
self.setA('new_a')
self.setPa('new_pa')
var a = self.a()
console.log('a: ' + a.toJS())
var pa = self.pa()
console.log('pa: ' + pa.toJS())
self.ORIGviewDidLoad();
}
});
运行结果输出:
2016-07-29 11:19:26.165 SwiftJSPatch[3789:222439] JSPatch.log: a: a 2016-07-29 11:19:26.169 SwiftJSPatch[3789:222439] *** Assertion failure in _exceptionBlock_block_invoke(), /Users/Leon/Desktop/SwiftJSPatch/SwiftJSPatch/JPEngine.m:142 2016-07-29 11:19:26.174 SwiftJSPatch[3789:222439] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'unrecognized selector pa for instance <SwiftJSPatch.ViewController: 0x7a760bb0>'
title 修改成功,
a 获取成功,
pa 访问失败:找不到
selector(pa) ,查看 OC 端调用堆栈:
js端调试:
经过
_evaluateScript:withSourceURL: 处理,
main.js 中的方法都被替换成
__C('methodName') 。
defineClass 对js对象method的改写也没问题。 由以上信息可知,JSPatch 方法替换成功,方法调用环节js调用oc私有方法 pa() 也就是在 callSelector 环节出错,获取不到方法签名导致后续消息转发无法进行。 public 方法则可以成功替换实现并调用。
现在在 private 变量前声明 dynamic :
dynamic private var pa = "pa"
输出:
2016-07-29 14:54:01.374 SwiftJSPatch[5368:357904] JSPatch.log: title: NEW VC 2016-07-29 14:54:01.381 SwiftJSPatch[5368:357904] JSPatch.log: a: a 2016-07-29 14:54:01.382 SwiftJSPatch[5368:357904] JSPatch.log: pa: pa 2016-07-29 14:54:01.384 SwiftJSPatch[5368:357904] JSPatch.log: a: new_a 2016-07-29 14:54:01.384 SwiftJSPatch[5368:357904] JSPatch.log: pa: new_pa ORIG title:NEW VC ORIG a:new_a ORIG pa:new_pa
变量都被成功修改,也就是说方法替换和调用都没问题。
结论1: JSPatch 作用于继承自 NSObject 的类,其继承自父类的属性/自定义 public 变量可以直接访问和修改,自定义 private 变量需要加上 dynamic 。
从上个修改属性的案例已经看出对于继承自 NSObject 的类的 继承自父类的方法 , JSPatch 实现热更新是没问题的。所以直接看自定义函数的情况。
在 ViewController 自定义两个函数,其中一个是 private 方法:
// in ViewController ---------------------
class ViewController: UIViewController {
var a = "a"
dynamic private var pa = "pa"
override func viewDidLoad() {
super.viewDidLoad()
self.fun()
self.pfun()
}
func fun() {
print("ORIG fun self.a: /(self.a)")
}
private func pfun() {
print("ORIG pfun self.pa: /(self.pa)")
}
}
main.js 中对这两个自定义函数实现进行修改。 fun() 给a赋新值, pfun() 给 pa 赋新值:
// in main.js------------------------
defineClass('SwiftJSPatch.ViewController', {
fun: function() {
var a = self.a()
console.log('a: ' + a.toJS())
self.setA('new_a')
var a = self.a()
console.log('a: ' + a.toJS())
self.ORIGfun();
},
pfun: function() {
var pa = self.pa()
console.log('pa: ' + pa.toJS())
self.setPa('new_pa')
var pa = self.pa()
console.log('pa: ' + pa.toJS())
self.ORIGpfun();
}
});
运行:
ORIG fun self.a: a ORIG pfun self.pa: pa
热更新失败!
从js调试结果看脚本是被执行过的,且「方法替换」成功,说明是OC端「方法调用」时 没有走运行时的消息转发流程 。 为两个函数添加 dynamic 声明:
dynamic func fun() {
print("ORIG fun self.a: /(self.a)")
}
dynamic private func pfun() {
print("ORIG pfun self.pa: /(self.pa)")
}
hook成功:
2016-07-29 15:49:14.903 SwiftJSPatch[5639:391073] JSPatch.log: a: a 2016-07-29 15:49:14.906 SwiftJSPatch[5639:391073] JSPatch.log: a: new_a ORIG fun self.a: new_a 2016-07-29 15:49:14.909 SwiftJSPatch[5639:391073] JSPatch.log: pa: pa 2016-07-29 15:49:14.910 SwiftJSPatch[5639:391073] JSPatch.log: pa: new_pa ORIG pfun self.pa: new_pa
Swift 中静态函数分两种:class 函数/static 函数:
override func viewDidLoad() {
super.viewDidLoad()
ViewController.sfun()
ViewController.cfun()
}
dynamic static func sfun() {
print("ORIG static func.")
}
dynamic class func cfun() {
print("ORIG class func.")
}
从结果看出,class 函数得到替换并调用成功,static 函数调用时没有进行消息转发:
ORIG static func. 2016-07-29 16:01:16.186 SwiftJSPatch[5701:398350] JSPatch.log: NEW class fun.
新建 Pure 类:
// in Pure.swift ---------------------------
class Pure {
var a = "a"
dynamic private var pa = "pa"
func call() {
self.fun()
self.pfun()
}
dynamic func fun() {
print("ORIG fun self.a: /(self.a)")
}
dynamic private func pfun() {
print("ORIG pfun self.pa: /(self.pa)")
}
}
main.js 修改 fun() 和 pfun() 的实现:
// in main.js ---------------------------
defineClass('SwiftJSPatch.Pure', {
fun: function() {
console.log('NEW static fun.')
},
pfun: function() {
console.log('NEW class fun.')
}
});
调用 call() 结果: 直接崩溃:
由上图知, JSPatch 在进行到 overrideMethod 进行方法实现IMP替换时要求 class 实现 NSCoping 协议,而不继承自 NSObject 的swift类是不遵循该协议的,因此崩溃。
回到崩溃代码:
if (!_JSOverideMethods[cls]) {
_JSOverideMethods[(id<NSCopying>)cls] = [[NSMutableDictionary alloc] init];
}
此处 JSPatch 在初始化缓冲区的时候将 Class 作为 Dictionary 的 key 进行保存,而 Dictionary 在设置 key-value 时会拷贝 key 值,所以会导致给一个不遵循 NSCoying 协议的对象发送了 copyWithZone: 消息,导致崩溃。
到这里「方法替换」的步骤已经进行不下去了。 JSPatch 对 Swift 原生类的热修复已经无能为力了。但 Swift 热修复的真正难点其实并不在这里,假如我们越过 NSCoping 通过某种 swift style 的方式实现了对类中方法名和对应js实现的缓存,也就是完成「方法替换」的话,热修复就能成功了吗?
「方法调用」才是 swift 热修复中目前真正无解的地方,最大原因是 swift 中 runtime 相对OC中的 runtime 动态性大大减弱。
另外最要命的一点: objc_msgSend 函数无法用于 Swift object。这个导致 JSPatch 实现方法调用(消息转发)的基础机制在 Swift 中失效了。
总结一下 Swift 项目中使用 JSPatch 需要注意的几点: - 只支持调用继承自 NSObject 的 Swift 类。 - 继承自 NSObject 的 Swift 类,其继承自父类的方法和属性可以在 JS 调用,其他自定义方法和属性同样需要加 dynamic 关键字才行。 - 若方法的参数/属性类型为 Swift 特有(如 Character / Tuple),则此方法和属性无法通过 JS 调用。
参考资料: Swift Runtime分析:还像OC Runtime一样吗? JSPatch Github Wiki 相关文章 iOS 热更新解读(一)APatch & JavaScriptCore iOS 热更新解读(二)—— JSPatch 源码解析