Swift 是苹果 2014 年发布的编程开发语言,可与 Objective-C 共同运行于 Mac OS 和 iOS 平台,用于搭建基于苹果平台的应用程序。Swift 已经开源,目前最新版本为 2.2。我们知道 Objective-C 是具有动态性的,能够通过 runtime API 调用和替换任意方法,那 Swift 也具有这些动态性吗?
分析用例
我们拿一个纯 Swift 类
和一个继承自 NSObject 的类
来做分析,这两个类里包含尽量多的 Swift 的类型比如 Character、String、AnyObject、Tuple。
代码如下:
(点击放大图像)
方法、属性
动态性比较重要的一点就是能够拿到某个类所有的方法、属性,我们使用如下代码来打印方法和属性列表。
(点击放大图像)
调用showClsRuntime 的代码如下:
let aSwiftClass:TestASwiftClass = TestASwiftClass(); showClsRuntime(object_getClass(aSwiftClass)); print("\n"); showClsRuntime(object_getClass(self));
看看我们得到什么结果?
(点击放大图像)
* 对于纯 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 获取到的方法: viewDidAppear
和testReturnVoidWithaId
(点击放大图像)
打印的日志为
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 获取 Swift runtime 信息
在 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 信息得这样写:
id cls = objc_getClass("TestSwift.TestASwiftClass"); showClsRuntime(cls); id cls2 = objc_getClass("TestSwift.TestSwiftVC"); showClsRuntime(cls2);
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 中会有模块前缀
本文作者
尹峥伟(花名 君展),来自手机淘宝技术团队的资深无线开发工程师,主要负责手机淘宝基础架构研发,github 开源库 Wax 的维护者,微信号 yzwlvzxh,微博 @君展。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论