转载

iOS热更新解读(三)—— JSPatch 之于 Swift

继承自 NSObject 的 Swift 类

修改属性

新建 Swift 工程 SwiftJSPatchAppDelegate.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 端调用堆栈: iOS热更新解读(三)—— JSPatch 之于 Swift

js端调试:

iOS热更新解读(三)—— JSPatch 之于 Swift 经过 _evaluateScript:withSourceURL: 处理, main.js 中的方法都被替换成 __C('methodName')iOS热更新解读(三)—— JSPatch 之于 Swift

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

热更新失败! iOS热更新解读(三)—— JSPatch 之于 Swift 从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() 结果: 直接崩溃: iOS热更新解读(三)—— JSPatch 之于 Swift 由上图知, JSPatch 在进行到 overrideMethod 进行方法实现IMP替换时要求 class 实现 NSCoping 协议,而不继承自 NSObject 的swift类是不遵循该协议的,因此崩溃。 iOS热更新解读(三)—— JSPatch 之于 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 热修复中目前真正无解的地方,最大原因是 swiftruntime 相对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 相关文章 iOS 热更新解读(一)APatch & JavaScriptCore iOS 热更新解读(二)—— JSPatch 源码解析

原文  http://zltunes.com/iosre-geng-xin-jie-du-san-jspatch-zhi-yu-swift/
正文到此结束
Loading...