Swift runtime

分析用例


我们拿一个纯Swift类和一个继承自NSObject的类的类来做分析,这两个类里包含尽量多的Swift的类型比如Character、String、AnyObject、Tuple。 
代码如下: 

方法、属性

动态性比较重要的一点就是能够拿到某个类所有的方法、属性,我们使用如下代码来打印方法和属性列表。

Swift 3.0 之前的写法:


Swift 3.0 的写法:

func showClsRuntime(_ cls:AnyClass) {

        print("start methodList")

        var methodNum:UInt32 =0

        let methodList =class_copyMethodList(cls, &methodNum)

        for indexin0..<numericCast(methodNum) {

            guardlet method = methodList?[index]else {continue }

            print(String.init(cString:method_getTypeEncoding(method)))

            print(String.init(cString:method_copyReturnType(method)))

            print(String.init(cString:sel_getName(method_getName(method))))

        }

        print("end methodList")

        

        print("start propertyList")

        var propertyNum:UInt32 =0

        let propertyList =class_copyPropertyList(cls, &propertyNum)

        for indexin0..<numericCast(propertyNum) {

            guardlet property:objc_property_t = propertyList?[index]else {continue }

            print(String.init(cString:property_getName(property)))

            print(String.init(cString:property_getAttributes(property)))

        }

        print("end propertyList")

    }



调用showClsRuntime的代码如下:


看看我们得到什么结果?

Swift 3.0 之前的打印



Swift 3.0的打印

start methodList

end methodList

start propertyList

end propertyList



start methodList

B16@0:8

B

aBool

v20@0:8B16

v

setABool:

Q16@0:8

Q

aInt

v24@0:8Q16

v

setAInt:

f16@0:8

f

aFloat

v20@0:8f16

v

setAFloat:

d16@0:8

d

aDouble

v24@0:8d16

v

setADouble:

@16@0:8

@

aString

v24@0:8@16

v

setAString:

@16@0:8

@

aObject

v24@0:8@16

v

setAObject:

v24@0:8@16

v

testReturnVoidWithaIdWithAId:

v56@0:8B16Q20f28d32@40@48

v

testReturnVoidWithaBoolWithABool:aInteger:aFloat:aDouble:aString:aObject:

q32@0:8@16q24

q

tableViewWithTableView:numberOfRowsInSection:

v24@0:8#16

v

showClsRuntime:

@?

@?

.cxx_destruct

@32@0:8@16@24

@

initWithNibName:bundle:

v20@0:8B16

v

viewDidAppear:

v16@0:8

v

viewDidLoad

@24@0:8@16

@

initWithCoder:

end methodList


start propertyList

aBool

TB,N,VaBool

aInt

TQ,N,VaInt

aFloat

Tf,N,VaFloat

aDouble

Td,N,VaDouble

aString

T@"NSString",N,C,VaString

aObject

T@,N,&,VaObject

end propertyList


对于纯Swift的TestASwiftClass来说任何方法、属性都未获取到。

对于TestSwiftVC来说除testReturnTuple、testReturnVoidWithaCharacter两个方法外,其他的都获取成功了。

这是为什么?

  • 纯Swift类的函数调用已经不再是Objective-c的运行时发消息,而是类似C++的vtable,在编译时就确定了调用哪个函数,所以没法通过runtime获取方法、属性。

  • TestSwiftVC继承自UIViewController,基类NSObject,而Swift为了兼容Objective-C,凡是继承自NSObject的类都会保留其动态性,所以我们能通过runtime拿到他的方法。

但为什么testReturnTuple testReturnVoidWithaCharacter却又获取不到呢?

从Objective-c的runtime 特性可以知道,所有运行时方法都依赖TypeEncoding,也就是method_getTypeEncoding返回的结果,他指定了方法的参数类型以及在函数调用时参数入栈所要的内存空间,没有这个标识就无法动态的压入参数(比如testReturnVoidWithaId: Optional("v24@0:8@16") Optional("v"),表示此方法参数共需24个字节,返回值为void,第一个参数为id,第二个为selector,第三个为id),而Character和Tuple是Swift特有的,无法映射到OC的类型,更无法用OC的typeEncoding表示,也就没法通过runtime获取了。

Method Swizzling

动态性最常用的就是方法替换(Method Swizzling),将类的某个方法替换成自定义的方法,从而达到hook的作用。

  • 对于纯Swift类(如TestASwiftClass)来说,无法通过objc runtime替换方法,因为由上面的测试可知拿不到这些方法、属性

  • 对于继承自NSObject类(如TestSwiftVC)来说,无法通过runtime获取到的方法肯定没法替换了。那能通过runtime获取到的方法就都能被替换吗?我们测一把。 

Method Swizzling的代码如下


我们替换两个可以被runtime获取到的方法:viewDidAppeartestReturnVoidWithaId

Swift 3.0 之前的写法:


Swift 3.0 的写法:

    overridefunc viewDidLoad() {

        super.viewDidLoad()


        methodSwizze(cls:object_getClass(self), originalSelector:#selector(TestSwiftVC.viewDidAppear(_:)), swizzledSelector: #selector(TestSwiftVC.sz_viewDidAppear(_:)))

        methodSwizze(cls:object_getClass(self), originalSelector:#selector(TestSwiftVC.testReturnVoidWithaId(_:)), swizzledSelector: #selector(TestSwiftVC.sz_testReturnVoidWithaId(_:)))

        testReturnVoidWithaId(view)

    }


    override func viewDidAppear(_ animated:Bool) {

        super.viewDidAppear(animated)

        print("F:\(#function)\(#line)")

    }

    

    func sz_viewDidAppear(_ animated:Bool) {

        super.viewDidAppear(animated)

        print("F:\(#function)\(#line)")

    }

    func testReturnVoidWithaId(_ aId:UIView) {

        print("F:\(#function)\(#line)")

    }

    

    func sz_testReturnVoidWithaId(_ aId:UIView) {

        print("F:\(#function)\(#line)")

    }

打印的日志为

F:testReturnVoidWithaId L:50

F:sz_viewDidAppear L:46

说明viewDidAppear已经被替换,但是testReturnVoidWithaId却没有被替换,这是为何?

我们在方法里打个断点看看,如图: 


可以看到区别,调用sz_viewDidAppear栈的前一帧为@objc TestSwiftVC.sz_viewDidAppear(Bool) -> ()有个@objc标识,而调用testReturnVoidWithaId则没有此标识。 

@objc用来做什么的?与动态性有关吗?

@objc

找到官方文档读读。 

可以知道@objc是用来将Swift的API导出给Objective-C和Objective-C runtime使用的,如果你的类继承自Objective-c的类(如NSObject)将会自动被编译器插入@objc标识。 

我们在把TestASwiftClass(纯Swift类)的方法、属性前都加个@objc 试试,如图:


查看日志可以发现加了@objc的方法、属性均可以被runtime获取到了。


dynamic

文档里还有一句说明: 
加了@objc标识的方法、属性无法保证都会被运行时调用,因为Swift会做静态优化。要想完全被动态调用,必须使用dynamic修饰。使用dynamic修饰将会隐式的加上@objc标识。
这也就解释了为什么testReturnVoidWithaId无法被替换,因为写在Swift里的代码直接被编译优化成静态调用了。

而viewDidAppear是继承Objective-C类获得的方法,本身就被修饰为dynamic,所以能被动态替换。 

我们把TestSwiftVC方法前加上dynamic再测一把,如图:


从堆栈也可以看出,方法的调用前增加了@objc标识,testReturnVoidWithaId方法被替换成功了。

同样的做法,我们把TestASwiftClass的方法和属性也都加上dynamic修饰,做Method Swizzling,同样获得成功,如图 


在Objective-c代码里使用objc_getClass("TestSwiftVC");会发现返回值为空,这是为什么?Swift代码中的TestSwiftVC类,在OC中还是这个名字吗? 
我们初始化一个对象,并断点和打印看看,如下图: 


可以看到Swift中的TestSwiftVC类在OC中的类名已经变成TestSwift.TestSwiftVC,即规则为SWIFT_MODULE_NAME.类名称,在普通源码项目里SWIFT_MODULE_NAME即为ProductName,在打好的Cocoa Touch Framework里为则为导出的包名。

所以要想从Objective-c中获取Swift类的runtime信息得这样写:


Objective-C替换Swift函数


给TestSwiftVC和TestASwiftClass的testReturnVoidWithaId函数加上dynamic修饰,然后我们在Objective-C代码里替换为testReturnVoidWithaIdImp函数:


运行之后我们得到结果

F:void testReturnVoidWithaIdImp(__strong id, SEL, __strong id) L:20 self=<TestSwift.TestSwiftVC: 0x7fb4e1d148f0>
F:void testReturnVoidWithaIdImp(__strong id, SEL, __strong id) L:20 self=TestSwift.TestASwiftClass


说明两者的方法在加上dynamic修饰后,均能在Objective-c里被替换。(TestSwiftVC的testReturnVoidWithaId不加dynamic也会打印日志,为什么?留给读者思考)

总结

  • 纯Swift类没有动态性,但在方法、属性前添加dynamic修饰可以获得动态性。

  • 继承自NSObject的Swift类,其继承自父类的方法具有动态性,其他自定义方法、属性需要加dynamic修饰才可以获得动态性。

  • 若方法的参数、属性类型为Swift特有、无法映射到Objective-C的类型(如Character、Tuple),则此方法、属性无法添加dynamic修饰(会编译错误)

  • Swift类在Objective-C中会有模块前缀

原地址: https://mp.weixin.qq.com/s?__biz=MzA3ODg4MDk0Ng==&mid=403153173&idx=1&sn=c631f95b28a0eb4b842a9494e43a30e5&scene=1&srcid=0411nXUTkQ6iwljBxNeAy5jg&key=b28b03434249256b60dcb9aea88dce40e7a1ee0e66963b948073d936b9786aa8046c2a3ba77f72f6cd452d879551fca7&ascene=0&uin=MTIwODIw&devicetype=iMac+MacBookPro12%2C1+OSX+OSX+10.10.5+build(14F1713)&version=11020201&pass_ticket=pKQWNzQ2V2dg3tSKmDrWNBPOSe7PPshsFTP6OHAGVys%3D
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值