写点什么

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

评论

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

活动空间预订系统(源码+文档+部署+讲解)

深圳亥时科技

科大讯飞学习机和作业帮学习机哪个好

妙龙

科大讯飞 作业帮 学习机

2024最新最全面Java复习路线(含P5-P8)!已收录 GitHub

蓝蓝路

编程 java面试

面试必刷:阿里巴巴 内部 Java 高级架构师 1080 道面试题

采菊东篱下

Java 编程 计算机

Java后端最全面试攻略,吃透25个技术栈,阿里十万字内部面试题总结全网开源

架构师之道

编程 java面试

ARM版CentOS Linux系统镜像文件(苹果M1专用)

理理

meta llama 大模型一个基础语言模型的集合

测吧(北京)科技有限公司

测试

RWA代币:下一波财富增长的密码?

TechubNews

AI与数据分析|使用机器学习,轻松解决复杂的情感分析问题

Altair RapidMiner

机器学习 AI 数据分析 情感分析 altair

科大讯飞P20 Plus词典笔 怎么样

妙龙

科大讯飞 词典笔

科大讯飞T30 Lite和T30 Pro 对比

妙龙

科大讯飞 学习机

阿里Spring Security OAuth2.0认证授权笔记震撼开源!原理+实战+源码三飞

采菊东篱下

编程 java面试

腾讯云大神呕心沥血整理:redis深度笔记”,看完全面掌握redis核心技术

蓝蓝路

编程 java面试

精选的掘金文章汇总[2024.11月-12月]

安全乐谷

GitHub 架构 算法 前端 后端

阿里大牛强力推荐:springboot实战派文档,从入门到实战,样样具备

架构师之道

Java 编程

科大讯飞智能录音笔S8离线版 功能介绍

妙龙

科大讯飞 录音笔

让我们一起来建设 Fluent Editor 开源富文本编辑器吧!

OpenTiny社区

富文本 OpenTiny 前端开源

猿辅导和作业帮哪个更好

妙龙

作业帮 学习机 猿辅导

Mysql优化

EquatorCoco

MySQL

DevOps研发效能建设的六大“雷区”:你中招了吗?

嘉为蓝鲸

DevOps 研发度量 效能度量 研发效能管理

作业帮学习机T20Pro+ 怎么样

妙龙

作业帮 学习机

阿里P9重磅分享内部绝密《百亿级并发系统设计》手册!

程序员高级码农

Java 程序员 高并发 架构设计 架构师

作业帮X58和X28区别对比选哪个

妙龙

作业帮 学习机

编写 Java 单元测试最佳实践

cloud studio AI应用

Java行情崩盘了?传智播客收入下滑严重,Java之父和金角大王的IT课程白菜价贱卖

陆通

AI Agent:未来高效螺丝钉,谁用得好,谁先赚到钱

博文视点Broadview

怎么把域名解析到IP地址?流程有哪些?一文讲清域名解析那些事

国科云

科大讯飞air2pro和x3pro有什么区别

妙龙

科大讯飞 办公本

OpenAI从传统发布会改成12天直播:OpenAI Day1 带来了哪些惊喜?

测吧(北京)科技有限公司

测试

OpenAI从传统发布会改成12天直播:OpenAI Day1 带来了哪些惊喜?

测试人

2025上海国际机器人展(Tech G)

AIOTE智博会

消费电子展 消费电子展会 消费电子博览会 消费电子展览会

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