写点什么

Swift Runtime 动态性分析

  • 2016-03-31
  • 本文字数:2836 字

    阅读完需:约 9 分钟

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 获取到的方法: viewDidAppeartestReturnVoidWithaId

(点击放大图像)

打印的日志为

复制代码
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 )关注我们。

2016-03-31 12:0710701

评论

发布
暂无评论
发现更多内容

聊聊dubbo协议

捉虫大师

微软 WSL 重装操作系统

HoneyMoose

融云2021 X-Meetup技术沙龙 探讨音视频技术新方向

融云 RongCloud

GitHub开源史上最大规模中文知识图谱

不脱发的程序猿

人工智能 自然语言处理 GitHub 开源 中文知识图谱

国内日志监控分析王者之sls

代码先生

分布式日志 海量数据分析 日志监控分析

拍乐云将亮相 QCon 大会,揭秘音视频“两高一低”体验背后的技术

拍乐云Pano

Hadoop定位问题日志跟踪

InfoQ_Springup

hadoop

“为爱而生 温暖陪伴”——2021年中国洗地机行业发展峰会圆满落幕

DT极客

墨奇科技宣布完成 2.5 亿元 B 轮融资

E科讯

一张图看懂嵌入式系统组成

不脱发的程序猿

硬件开发 嵌入式系统 嵌入式学习路线

RDBMS与HBase的对比

五分钟学大数据

大数据 HBase 5月日更

uniapp 使用原生子窗体进行视频聊天

anyRTC开发者

uni-app 音视频 WebRTC RTC

中公、马士兵、千锋、拉勾入局,腾讯课堂“薪选”IT人才培养按下加速键

DT极客

关于OpenHarmony 2.0共建进展

罗燕珊

开源 鸿蒙 OpenHarmony

并行文件存储和分布式 NFS 文件存储有何不同

焱融科技

容器 云原生 高性能 文件存储 技术博客

This BigData,Hadoop组成及生态

Simon郎

大数据 hdfs YARN hadoop3

这一次,彻底搞懂 Go Cond

HHFCodeRv

Go 语言

Pulumi 如何在 Windows 环境中设置

HoneyMoose

快成物流科技 x mPaaS | 小程序容器加持下的技术架构“提质增效”

蚂蚁集团移动开发平台 mPaaS

小程序 移动开发 mPaaS

Windows Terminal 安装和运行

HoneyMoose

云小课|DSC帮您管数据,保障您的云上数据安全

华为云开发者联盟

数据安全 华为云 数据安全中心 云上数据 DSC

亿级系统的Redis缓存如何设计?

Java架构师迁哥

Windows 任务管理器中如何显示 CPU 逻辑处理器情况

HoneyMoose

Too many open files 的四种解决办法

ilinux

多方安全计算:隐私保护集合求交技术

华为云开发者联盟

数据集 PSI 隐私保护集合交集 可信 伪随机函数

5G进京 “赶考”记:“占得上、保持稳、体验优、信号好”四道考题,分别打几分?

脑极体

🚄【Redis 干货领域】帮你完全搞定Sentinel运作原理

码界西柚

redis哨兵模式 redis哨兵 redis哨兵集群 redis sentinel 5月日更

【LeetCode】将句子排序Java题解

Albert

算法 LeetCode 5月日更

高并发调优backlog多大合适?

运维研习社

nginx 运维 5月日更 并发调优

CMMI V2.0丨如何通过CMMI真正在企业中的实施规模化敏捷开发

IPD产品研发管理

软件 研发管理 CMMI

不给电脑,我用手机敲命令十分钟完成了zabbix监控,面试官当场下offer

沐风

监控 zabbix

Swift Runtime动态性分析_语言 & 开发_尹峥伟_InfoQ精选文章