QCon北京「鸿蒙专场」火热来袭!即刻报名,与创新同行~ 了解详情
写点什么

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:0710557

评论

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

Netty引导器Bootstrap学习笔记

风翱

Netty 5月日更

全国大学生物联网设计竞赛(华为杯)巡回技术讲座“八校联动”,HarmonyOS与校园开发者面对面

Geek_283163

鸿蒙

开源流媒体服务器SRS学习笔记 - 安装、推流、拉流

赖猫

音视频 SRS SRS流媒体服务器

Spark为什么会流行

大数据技术指南

大数据 5月日更

ES_her0

5月日更

量化网格交易策略系统,马丁倍投策略软件源码

通过实践带你揭开TCP中CLOSE_WAIT和TIME_WAIT的神秘面纱

linux大本营

c++ Linux TCP 网络协议 epoll

腾讯iOS开发要达到咋样的水准?

一意孤行的程序员

ios 程序员 腾讯 面试 ios开发

企业数字化转型分为三个阶段

石云升

数字化 5月日更

Golang 程序实体

escray

学习 极客时间 Go 语言 5月日更

用Python找出100以内的质数 - DAY6

Qien Z.

5月日更 ptyhon 找质数 质数

20个不良用户体验

老猿Python

程序员 用户体验 程序开发

全国大学生物联网设计竞赛(华为杯)巡回技术讲座“八校联动”,HarmonyOS与校园开发者面对面

科技汇

高性能JavaScriptの笔记(二)

空城机

JavaScript 大前端 5月日更

全文检索的介绍

五分钟学大数据

大数据 5月日更

把Redis当作队列来用,真的合适吗?

Kaito

数据库 redis 队列

区块链—重构新商业体系和全新商业生态环境

CECBC

【LeetCode】罗马数字转整数Java题解

Albert

算法 LeetCode 5月日更

ceph-csi源码分析(7)-rbd driver-IdentityServer分析

良凯尔

Kubernetes 源码分析 Ceph CSI

[Netty]- Reactor实现

topsion

reactor Netty io nio

数据科学指南#基础篇 Bokeh入门

Lev

Python 学习 数据分析 数据科学

工业互联网产业数字化,区块链如何协同发力?

CECBC

网络攻防学习笔记 Day15

穿过生命散发芬芳

5月日更 网络攻防

ceph-csi源码分析(8)-cephfs driver分析

良凯尔

Kubernetes 源码分析 Ceph CSI

k8s POD hosts 文件怎么与主机保持同步那?

Geek_f24c45

Docker Kubernetes

MySQL数据库学习笔记(1)

lenka

5月日更

Android设备实时音视频传输有效方案

小驰笔记

android 音视频 引航计划

深入Linux C/C++ Timer定时器的实现核心原理

Linux服务器开发

Linux 后端 网络编程 定时器 Linux服务器开发

数据挖掘从入门到放弃(二):决策树

数据社

机器学习 5月日更

消息队列系统架构设计

王硕

架构训练营

iOS 面试策略之系统框架-UIScrollView及其子类

iOSer

ios 面试 UIScrollView UICollectionView

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