本篇探索 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属性aprivate属性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作为Dictionarykey进行保存,而Dictionary在设置key-value时会拷贝 key值,所以会导致给一个不遵循NSCoying协议的对象发送了copyWithZone:消息,导致崩溃。

Swift 原生类热修复难点

到这里「方法替换」的步骤已经进行不下去了。JSPatchSwift原生类的热修复已经无能为力了。但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 调用。

参考资料:
Swift Runtime分析:还像OC Runtime一样吗?
JSPatch Github Wiki