iOS 热更新解读(三)—— JSPatch 之于 Swift
本篇探索 jspatch 能否用于 swift 项目实现热更新。
继承自 NSObject 的 Swift 类
修改属性
新建 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.
纯 Swift 类
新建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:
消息,导致崩溃。
Swift 原生类热修复难点
到这里「方法替换」的步骤已经进行不下去了。JSPatch
对Swift
原生类的热修复已经无能为力了。但Swift
热修复的真正难点其实并不在这里,假如我们越过NSCoping
通过某种 swift style 的方式实现了对类中方法名和对应js实现的缓存,也就是完成「方法替换」的话,热修复就能成功了吗?
「方法调用」才是 swift 热修复中目前真正无解的地方,最大原因是swift中runtime相对OC中的runtime动态性大大减弱。
- 纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。
- 继承自NSObject的Swift类,其继承自父类的方法具有动态性,其他自定义方法、属性需要加dynamic修饰才可以获得动态性。
- 若方法的参数、属性类型为Swift特有、无法映射到Objective-C的类型(如Character、Tuple),则此方法、属性无法添加dynamic修饰(会编译错误)
- Swift类在Objective-C中会有模块前缀。
另外最要命的一点:objc_msgSend
函数无法用于 Swift object。这个导致JSPatch
实现方法调用(消息转发)的基础机制在 Swift 中失效了。
总结一下 Swift
项目中使用JSPatch
需要注意的几点:
- 只支持调用继承自 NSObject 的 Swift 类。
- 继承自 NSObject 的 Swift 类,其继承自父类的方法和属性可以在 JS 调用,其他自定义方法和属性同样需要加 dynamic 关键字才行。
- 若方法的参数/属性类型为 Swift 特有(如 Character / Tuple),则此方法和属性无法通过 JS 调用。