新建 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 源码解析