写点什么

Swift Runtime 动态性分析

2016 年 3 月 31 日

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 年 3 月 31 日 12:079297

评论

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

读闲书自由和财务自由

池建强

读书 财务自由

创新管理体系标准ISO56002介绍

涛哥

数字化转型 创新

聊一聊程序员如何增加收入

张小方

程序员 互联网 面试 副业赚钱 薪资

SpringBatch系列之Remote-Chunking

稻草鸟人

大数据 Spring Boot SpringBatch 批量任务

ARTS 打卡 Week 05

teoking

科学计算软件被禁,阿里达摩院能研发Matlab?

飞哥

阿里巴巴 matlab

MySQL 实战 45 讲笔记(2)-查询优化

程序员老王

MySQL

深入理解Kubernetes的Service:回归本源的场景需求

韩超

Kubernetes 微服务 服务

第四章总结

一二线城市知名 IT 互联网公司名单(新版)

程序员生活志

互联网 IT 大厂

计算机操作系统基础(五)---Linux的进程管理

书旅

php 线程 多线程 操作系统 进程

架构演化

满山李子

大型互联网架构与集群技术

cxy

架构师训练营第四周学习总结

张明森

实现简单的"纤程"

Near

架构师训练营第 4 周作业

在野

极客大学架构师训练营

极客大学架构师训练营第四周学习总结

竹森先生

极客大学 极客大学架构师训练营

嗨,兄弟,别担心,这年头谁还没有一点焦虑!

攀鱼飞岩

管理 程序员人生 成长 个人感想 程序员素养

奈学:数据湖和数据仓库的区别有哪些?

古月木易

数据仓库 数据湖

从0开始设计Flutter独立APP | 第二篇: 完整的国际化语言支持

渔子长

flutter 前端

谈谈架构和微服务<一>

Gabriel

架构 微服务 微服务架构 领域驱动设计 软件设计

架构师训练营第 4 周——学习总结

在野

极客大学架构师训练营

奈学:数据湖和数据仓库的区别有哪些?

奈学教育

数据仓库 数据湖

如何学 Java,我说点不太一样的学习方式

四猿外

学习 程序员 个人成长 程序员成长

如何成为一名合格的 C/C++ 开发者?

张小方

c++ Linux 编程语言 架构设计 后端开发

我写了一本操作系统词典送给你

cxuan

操作系统 计算机

大型互联网应用系统使用技术方案和手段

来了!8M/S+速度,Pdown复活!

程序员生活志

架构师训练营第四周课后作业

竹森先生

极客时间 极客大学架构师训练营

动态规划算法重点在于找上一个的公式,Google Code Review,John 易筋 ARTS 打卡 Week 06

John(易筋)

ARTS 打卡计划

如何进行高效学习

淡蓝色

深度思考 方法论 感悟 随笔杂谈

Hummer 轻量级跨端技术框架详解及实战

Hummer 轻量级跨端技术框架详解及实战

Swift Runtime动态性分析-InfoQ