JSPatch在Swift中的应用(一)

前言

上一篇文章中我们给出Swift中使用Method Swizzling有几个原则:

  1. 继承自NSObject的Swift类,其继承自父类的方法具有动态性,其他自定义方法、属性需要加dynamic修饰才可以获得动态性。
  2. 若方法的参数、属性类型为Swift特有、无法映射到Objective-C的类型(如Character、Tuple),则此方法、属性无法添加dynamic修饰(会编译错误)。
  3. 纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。

现在我们来做一些测试。

一、分析继承于NSObject的类

1.1 属性分析

我们先实现一个测试类,里面包含public和private变量。

class DemoViewController : UIViewController {
    
    var testVariable = "testVariable"
    private var privateTestVariable = "privateTestVariable"
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
        dispatch_after(delay, dispatch_get_main_queue()) {
            self.view .addSubview(self.testView)
            UIView.animateWithDuration(4, animations: {
                self.testView.transform = CGAffineTransformMakeTranslation(100, 100);
                }, completion: { (finished:Bool) in
                    print("Origin Function: \(#function) Line: \(#line)");
                    print("Test data output: \(self.testVariable)")
                    print("Test data output: \(self.privateTestVariable)")
            })
        }
    }
}

我们再实现一个Demo的JS脚本。它所实现的功能是运行时重新赋值我们上面定义的2个变量,testVariable和privateTestVariable。

defineClass('DemoProject.DemoViewController', {
    viewDidAppear: function(animated) {
        self.super().viewDidAppear(animated);
        var data = self.testVariable()     
        console.log('testVariable output: ' + data.toJS())

        var privateData = self.privateTestVariable()     
        console.log('privateTestVariable output: ' + privateData.toJS())

        self.setTestVariable('JSPatch')
        self.setPrivateTestVariable('Private JSPatch')

        var data = self.testVariable()     
        console.log('testVariable output: ' + data.toJS())

        var privateData = self.privateTestVariable()     
        console.log('privateTestVariable output: ' + privateData.toJS())

        self.ORIGviewDidAppear(animated);
    }
});

2016-04-14 20:33:58.907 DemoProject[25486:2704401] testVariable output: testVariable

2016-04-14 20:33:58.908 DemoProject[25486:2704401] exception=unrecognized selector privateTestVariable for instance <DemoProject.DemoViewController: 0x7fefb34a1970>

2016-04-14 20:33:58.908 DemoProject[25486:2704401] privateTestVariable output: false

2016-04-14 20:33:58.908 DemoProject[25486:2704401] exception=unrecognized selector setPrivateTestVariable: for instance <DemoProject.DemoViewController: 0x7fefb34a1970>

2016-04-14 20:33:58.908 DemoProject[25486:2704401] testVariable output: JSPatch

2016-04-14 20:33:58.909 DemoProject[25486:2704401] exception=unrecognized selector privateTestVariable for instance 

Origin Function: viewDidAppear Line: 170
Test data output: JSPatch
Test data output: privateTestVariable

从运行结果可以看到,DemoViewController的public变量testVariable成功的替换成了JSPatch,而访问private变量privateTestVariable则抛出了exception,并且其输出值为false,说明没有取到任何值,原因如下:

在JS里面判断是否为空要判断false:

var url = "";
var rawData = NSData.dataWithContentsOfURL(NSURL.URLWithString(url));
if (rawData == null) {} 
//这样判断是错误的应该如下判断:if (!rawData){}在JSPatch.js源码里_formatOCToJS方法对undefined,null,isNil转换成了false。

现在,我们在private变量前面加上dynamic。看看会发生什么?

//private var privateTestVariable = "privateTestVariable"前面加上dynamic
dynamic private var privateTestVariable = "privateTestVariable"
2016-04-14 20:57:49.882 DemoProject[26287:2716081] testVariable output: testVariable
2016-04-14 20:57:49.883 DemoProject[26287:2716081] privateTestVariable output: privateTestVariable
2016-04-14 20:57:49.883 DemoProject[26287:2716081] testVariable output: JSPatch
2016-04-14 20:57:49.883 DemoProject[26287:2716081] privateTestVariable output: Private JSPatch

Origin Function: viewDidAppear Line: 170
Test data output: JSPatch
Test data output: Private JSPatch

我们发现变量内容都实现了替换,所以在继承于NSObject的类中,public变量可以直接修改,而private变量需要加上dynamic。

1.2 自定义函数分析

我们在DemoViewController中加入2个测试函数。

class DemoViewController : UIViewController {
    
    var testVariable = "testVariable"
    dynamic private var privateTestVariable = "privateTestVariable"
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        self.testFunction("")
        self.privateTestFunction("")

        print("Origin Function: \(#function) Line: \(#line)");
        print("Test data output: \(self.testVariable)")
        print("Test data output: \(self.privateTestVariable)")
    }
    
    func testFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Test data output: \(self.testVariable)")
    }
    
    private func privateTestFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Test data output: \(self.privateTestVariable)")
    }
}

我们再实现一个替换上面2个函数的JS脚本,在testFunction函数中实现public变量的替换,在privateTestFunction函数中实现private变量的替换

defineClass('DemoProject.DemoViewController', {

    testFunction: function(string) {

        var data = self.testVariable()     
        console.log('testVariable output: ' + data.toJS())

        self.setTestVariable('JSPatch')

        var data = self.testVariable()     
        console.log('testVariable output: ' + data.toJS())

        self.ORIGtestFunction(string);

    },

    privateTestFunction: function(string) {

        var privateData = self.privateTestVariable()     
        console.log('privateTestVariable output: ' + privateData.toJS())

        self.setPrivateTestVariable('Private JSPatch')

        var privateData = self.privateTestVariable()     
        console.log('privateTestVariable output: ' + privateData.toJS())

        self.ORIGprivateTestFunction(string);
    }
});

运行结果如下:

Origin Function: testFunction Line: 183
Test data output: testVariable

Origin Function: privateTestFunction Line: 189
Test data output: privateTestVariable

Origin Function: viewDidAppear Line: 175
Test data output: testVariable
Test data output: privateTestVariable

从运行结果看,什么也没发生,脚本并没有执行,这也符合我们的预期。我们在2个函数前面加上dynamic再运行一次。

dynamic func testFunction(string:String) -> Void {
    print("Origin Function: \(#function) Line: \(#line)");
    print("Test data output: \(self.testVariable)")
}
    
dynamic private func privateTestFunction(string:String) -> Void {
    print("Origin Function: \(#function) Line: \(#line)");
    print("Test data output: \(self.privateTestVariable)")
}

运行结果如下:

2016-04-14 23:20:43.575 DemoProject[29236:2805444] testVariable output: testVariable

2016-04-14 23:20:43.576 DemoProject[29236:2805444] testVariable output: JSPatch

Origin Function: testFunction Line: 183
Test data output: JSPatch

2016-04-14 23:20:43.576 DemoProject[29236:2805444] privateTestVariable output: privateTestVariable

2016-04-14 23:20:43.577 DemoProject[29236:2805444] privateTestVariable output: Private JSPatch

Origin Function: privateTestFunction Line: 189
Test data output: Private JSPatch

Origin Function: viewDidAppear Line: 175
Test data output: JSPatch
Test data output: Private JSPatch

可以看到,所有的函数都实现了运行时替换,这也和我们的预期一致。

1.3 静态函数分析

我们测试一下静态函数的情况。在Swift中,静态函数有2种写法,一种是static func,一种是class func。先看测试代码:

class DemoTest : NSObject{
    
    dynamic static func staticTestFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Static Fucntion output: !!!!!!!!!!")
    }
    
    dynamic class func classTestFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Class Fucntion output: !!!!!!!!!!")
    }
}
defineClass('DemoProject.DemoTest', {


}, {
    staticTestFunction: function(string) {
        console.log('JSPatch staticTestFunction output:!!!!!!!!!!!')
    },
    classTestFunction: function(string) {
        console.log('JSPatch classTestFunction output:!!!!!!!!!!!')
    }
})

运行结果如下:

Origin Function: staticTestFunction Line: 172
Static Fucntion output: !!!!!!!!!!
2016-04-15 17:45:24.786 DemoProject[62603:3255589] JSPatch classTestFunction output:!!!!!!!!!!!

从测试结果可以看出,class写法的静态函数得到了替换。但是static写法的静态函数并没有得到替换。因此,如果想让Swift APP获得动态性,多用class的写法去描述静态函数。

二、分析纯Swift类

由于自定义变量上一章已经分析过了,我们就不再测试变量的动态性,直接测试函数的动态性。因此,直接在private变量前加上dynamic进行函数测试。测试代码如下:

class DemoTest {
    
    var testVariable = "testVariable"
    dynamic private var privateTestVariable = "privateTestVariable"
    
    
    func testCall() {
        self.testFunction("")
        
        self.privateTestFunction("")
    }
    
    func testFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Test data output: \(self.testVariable)")
    }
    
    private func privateTestFunction(string:String) -> Void {
        print("Origin Function: \(#function) Line: \(#line)");
        print("Test data output: \(self.privateTestVariable)")
    }
}```

defineClass('DemoProject.DemoTest', {

testFunction: function(string) {

    var data = self.testVariable()     
    console.log('testVariable output: ' + data.toJS())

    self.setTestVariable('JSPatch')

    var data = self.testVariable()     
    console.log('testVariable output: ' + data.toJS())
},

privateTestFunction: function(string) {

    var privateData = self.privateTestVariable()     
    console.log('privateTestVariable output: ' + privateData.toJS())

    self.setPrivateTestVariable('Private JSPatch')

    var privateData = self.privateTestVariable()     
    console.log('privateTestVariable output: ' + privateData.toJS())
}

})

编译运行之后,直接出现了崩溃,崩溃日志如下:

2016-04-17 14:11:53.283 DemoProject[64822:3373602] NSForwarding: warning: object 0x100e8a918 of class 'DemoProject.DemoTest' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector +[DemoProject.DemoTest copyWithZone:]

查看崩溃的源码之后发现,是因为JSPatch在进行方法替换记录时,使用了NSCopying协议,而不继承NSObject的实例是没有这个方法的,所以产生了崩溃。
我也在github上给JSPatch的作者提交了issue,暂时没有什么解决办法,希望swift 3.0出来之后能够有好的解决办法。[issue地址](https://github.com/bang590/JSPatch/issues/317)

static void _initJPOverideMethods(Class cls) {
if (!_JSOverideMethods) {
_JSOverideMethods = [[NSMutableDictionary alloc] init];
}
if (!_JSOverideMethods[cls]) {
//因为调用了NSCopying协议,所以替换Swift时会崩溃
_JSOverideMethods[(id<NSCopying>)cls] = [[NSMutableDictionary alloc] init];
}
}

#三、通用性测试

我们写一段稍微复杂的程序,试试JSPatch对Swift的支持。里面主要涉及block的使用,view的简单动画。运行结果我就不贴出来了,运行情况是可以达到原生的动画效果。JSPatch对Struct的支持请参照这里:[JSPatch对Struct的支持](https://github.com/bang590/JSPatch/wiki/%E6%B7%BB%E5%8A%A0-struct-%E7%B1%BB%E5%9E%8B%E6%94%AF%E6%8C%81)

override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)

let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(3 * Double(NSEC_PER_SEC)))
dispatch_after(delay, dispatch_get_main_queue()) {
    self.view .addSubview(self.testView)
    UIView.animateWithDuration(4, animations: {
        self.testView.center = CGPointMake(100, 100)
        }, completion: { (finished:Bool) in
    })
}

}


require('UIView')

defineClass('DemoProject.DemoTest', {
viewDidAppear: function(animated) {
self.super().viewDidAppear(animated);

    self.view().addSubview(self.testView())
    console.log('!!!!!!!!!!!!!!!!!!!!')

    dispatch_after(1.0, function(){
        UIView.animateWithDuration_animations_completion(4, block(function(){
             self.testView().setCenter({x: 100, y: 400})
        }), block("Bool", function(finished){

        }))
    })
},

})


#四、总结
这篇文章主要针对Swift中Class的动态性研究,包括继承于NSObject的类和原生的Swift的类,而关于Protocol和C++函数的动态性研究还没测试。有时间,我会给出其他方面的动态性研究。

推荐阅读更多精彩内容