| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 移动开发 -> A站 的 Swift 实践 收获总结。 -> 正文阅读 |
|
[移动开发]A站 的 Swift 实践 收获总结。 |
前言学如逆水行舟,不进则退。共勉!!! 今天给大家分享一篇A站的Swift实践。学习资料|领取地址 经过不断迭代,Swift如今已成iOS乃至苹果全平台首选开发语言,A站也已经完全投入到Swift浪潮中,享受到Swift语言带来的舒适和高效开发体验。《A站的Swift实践——上篇》介绍了Swift的技术背景、Swift的架构演进过程以及对最新框架SwiftUI和Combine等技术的选型。 如何混编昨天刚刚结束的Google I/O让人想起了Kotlin在三年前曾经上过一次热搜,Google I/O官宣Kotlin替代Java,正式成为Android开发的首选语言。正所谓演进的力量,这一切都要归功于苹果公司在2014年推出的Swift替代了Objective-C,成为iOS乃至苹果全平台首选的开发语言,从而提高了iOS开发者的热情。上篇介绍了Swift的技术背景以及如何选择开发框架。下篇的内容会介绍大多数以OC为主体的工程如何与Swift共舞,以及如何利用Swift动态性解决工程难题。 然而,混编开发应该怎么开始呢?有没有什么前置条件? 前置条件混编本质上就是把OC语法的声明通过编译工具生成Swift语法的声明,这样Swift就可以通过生成的声明直接调用OC接口。反之,OC调用Swift接口也可以通过相同的方法,把Swift语法的声明生成OC语法的头文件。这些转换生成的编译工具都集成在开发工具Xcode里。 Xcode其实就是执行多命令行的工具,比如Clang、ld等等。Xcode、Project文件里包含了这些命令的参数和它们执行的顺序,也有所有待编译文件和它们的依赖关系。llbuild[1] 是低等级构建系统,根据Xcode Project里的配置按顺序执行命令。命令行工具的参数配置是在Xcode的Build Settings里进行设置的。如果是在同一个Project里混编,首先需要将Build Settings里Always Embed Swift Standard Libraries设置为YES,然后在桥接文件,也就是ProductName-Bridging-Header.h里导入需要暴露给Swift的OC类。如果Swift要调用的OC在不同Project里,则需要将OC的Project设置为Module,将Defines Module设为YES,再把Module里的头文件导入到OC Modulemap文件里的Umbrella Header里。 如何设置CocoaPodsSwift Pod的Podspec需要写明对OC Pod的依赖。在工程Podfile中,OC Pod后面要写 :modular_headers => true。开启Modular Header就是把Pod转换为Module。那CocoaPods究竟做了什么?执行 Pod Install – Verbose就可以看到,在生成Pod Targets时,CocoaPods会生成Module Map File和Umbrella Header。
CocoaPods的主要组件有解析命令的CLAide[2] 、 用来解析Pod描述文件,比如Podfile、Podfile.lock和PodSpec文件的Cocoapods-core[3] 、 拉仓库代码和资源的Co**coapods-downloader[4] 、 分析依赖的Molinillo[5] 、 以及创建和编辑Xcode的.xcodeproj和.xcworkspace文件的Xcodeproj[6] 。在执行了Pod Install以后,组件调用流程以及配置Module所处流程位置,如下图所示: 完成以上工作后,如果我们想要在Swift里使用OC开发的库FMDB,就可以直接使用Import来导入,代码如下
可以看到,Import FMDB将FMDB的Module倒入进来后,接口依然能够直接使用Swift语法调用。 这里需要注意的是,Module依赖的Pod也需要是Module。因此改造时需要从底向上地改造成Module。另外,开启Module后,如果某个头文件在Umbrella Header里,那么其它包含这个头文件的Pod也需要打开Module。 为什么要用Module?在Module被使用之前,开发者们需要对要导入的C语言编译器处理方式类头文件进行预处理,查找头文件里还导入了哪些头文件,递归直到找到全部头文件。但是,预处理的方式会遇到许多问题。其一,编译的复杂度高且耗时长,这是因为每个可编译的文件都会单独编译进行预处理,所以在预处理过程中递归查找导入头文件的工作会重复很多次,尤其是当包含关系很深的头文件被很多.m所导入的时候;其二,会出现宏定义冲突时需要重新排序以及和解依赖的问题等。 Module相对来说更加简易,它的头文件只需要解析一次,所以编译的复杂度会指数级降低,且编译器对Module的处理方式和C语言的预处理方式是完全不同的。编译器会将要编译的文件导入的头文件生成二进制格式,存储在Module Cache中,编译时如果碰到需要导入模块时,会先检查Module Cache,有对应的二进制文件就直接加载,没有才会解析,以此来保证Module解析只有一次。重新解析编译Module只会发生在头文件包含的任何头文件有变动,或者依赖另外一个模块有更新的时候。比如下面的代码:
Clang会先从FMDB.framework的Headers目录里查找FMDatabase.h,再去FMDB.framework的Modules目录里查找module.modulemap文件,分析module.modulemap来判断FMDatabase.h是否是模块的一部分。Module Map用来定义Module和头文件之间的关系。FMDB.framework的module.modulemap的内容如下:
想要确定FMDatabase.h是否是Module的一部分就要看module.modulemap里的Umbrella Header文件,即FMDB-umbrella.h目录里是否包含了FMDatabase.h。在Headers目录里查看FMDB-umbrella.h文件,内容如下:
上面代码中可以看到FMDatabase.h已经包含在文件中,因此Clang会将FMDB作为Module导入。Umbrella框架是对框架的一个封装,目的是隐藏各个框架之间的复杂依赖关系。构建完的Module会被存放到 ~/Library/Developer/Xcode/DerivedData/ModuleCache.noindex/ 这个目录下面。 Clang编译单个OC文件是通过导入头文件方式进行的,而Swift没有头文件,所以Swift编译器Swiftc就需要先查找声明,再来生成接口。除此之外,Swiftc还会在Module Map文件和Umbrella Header文件中暴露的声明里查找OC声明。 如果工程要构建二进制库,需要支持Swift 5.1加的Module Stability和Library Evolution。 Name Mangling找到OC声明后,Swiftc就需要进行Name Mangling。Name Mangling的作用在一方面是会像C++那样防止命名冲突,另外一方面是会对OC接口命名进行Swift风格化重命名。如果对Name Mangling命名的效果不满意,还可以回到OC源码中用NS_SWIFT_NAME重新定义想要在Swift使用的名字。 Swiftc的Name Mangling相比较于C和C++的Name Mangling会生成更多信息,比如下面的代码:
Swiftc编译后,使用nm -g查看生成如下的信息:
如上所示,信息中的$s表示全局,8demotest的demotest是Module名,8是Module名的长度。int2string是函数名,前面的10是类名长度,6number是参数名。SS表示参数类型是Int。Si表示的是String类型,_tF表示前面的Si是返回类型。 接下来对比一下Clang和Swiftc的编译过程,首先是Clang的编译过程,如下图 至于OC怎样调用Swift接口,Swiftc会生成一个头文件,代码中有Public的声明会先按文件生成Swiftmodule,文件链接完会合并Swiftmodule,最后整体生成到一个头文件里。过程如下图所示: 为什么可以调OC接口?Swift代码之所以可以调OC接口,是因为OC的接口会被编译器自动生成为Swift语法接口文件。在Xcode中,在OC头文件中点击左上角的 Related Items,选择Generated Interface,就可以选择查看生成的Swift版本接口文件。自动转换成的Swift接口文件可以直接供Swift调用,在转换过程中,编译器会将NSString这种OC的基础库转换成Swift里对应的String、Date等Swift库。OC的初始化方法也会被转换成Swift的构造器方法。错误处理也会被转换成Swift风格。下面是OC和Swift转换对应的类型:
苹果公司也提供了一些宏来帮助生成好用的Swift接口。 众所周知,OC之前一直缺少是非空的类型信息,可以通过 NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包起来,这样就不用逐个去指定是非空了。NS_DESIGNATED_INITIALIZER宏可以将初始化设置为Designated,不加这个宏为Convenience。NS_SWIFT_NAME用来重命名Swift中使用的名称,NS_REFINED_FOR_SWIFT可以解决数据不一致的问题。 在iOS开发的过程中不可避免地需要访问 Core Foundation 类型,Core Fundation框架一旦导入到Swift混编环境中,它的类型就会被自动转为Swift类,Swift也会自动管理Annotated Core Foundation对象的内存,而不用像在OC中那样手动调用CFRetain、CFRelease或者CFAutorelease函数。Unannotated的对象会被包装在一个Unmanaged结构里,比如下面的代码:
转成Swift就是:
如上面代码所示,Core Fundation 类型的名字转换后会去掉后缀Ref,这是因为在Swift中所有类都是引用类型,Ref后缀比较多余。上面的Unmanaged结构有两个方法,一个是takeUnretainedValue(),另一个是takeRetainedValue(),这两个方法都是用来返回对象的原始未封装类型。如果对象之前没有Retain就用takeUnretainedValue(),已经Retain了,就用takeRetainedValue()。 在Swift里用getVaList(::😃 或withVaList(:😃 函数调用C的Variadic函数,比如 vasprintf(::😃。 调用指针参数的C函数,和Swift映射如下图: Swift写出来的Module也可以给OC来调用。但是这样的调用会有很多限制,因为Swift中有很多类型是没法给OC用的,比如在Swift里定义的枚举、Swift定义的结构体、顶层定义的函数、全局变量、Typealiases、Nested类型,但是如果绕过这些类型,Swift也变得不那么Swift了。\ 即使是实现了混编,开发者们还需要面对许多难题。因为在OC时代的很多问题,例如Hook,无痕埋点等可以在OC运行时很方便地实现,而Swift却缺少天然的支持。下面介绍一下Swift的动态性,在官方完善前,我们应该怎么使用它。 动态性Swift在处理纯粹的Swift语言时是有自己的运行时的,但是对于“这个运行时是不提供访问的接口”的问题,Swift核心团队不是不做动态特性,而是因为如果想要支持动态特性就需要处理虚函数表(Virtual Method Table)的动态调用对SIL函数优化的影响,比如类没有被Override就会自动优化到静态调用,而这需要大量的时间。现阶段还有优先级更高的事情要做,比如并发模型、系统编程、静态分析支持类型状态等。因此,有人选择自己去实现一套Swift运行时,使得Swift代码具有动态特性。Jordan Rose[7] 实现了一个精简版的Swift [8] 运行时,更加严谨的运行时实现可以参考Echo[9] 和Runtime[10] 。 有人可能会问,SwiftUI的Preview不就是典型的在运行时替换方法的吗?他是怎么做到的呢?其实他使用的是@_dynamicReplacement属性,这是一个可以直接拿着用来进行方法替换的内部使用属性。
如果想要把上面的代码放到一个库中,并且在运行时加载这个库进行运行时方法替换可以通过这样的方式:
除了这个方法以外,还有其他办法可以进行运行时的方法替换吗? 值类型的方法替换通过 AnyClass和class_getSuperclass方法可以查看Swift对象的继承链,没有继承NSObject的Swift类,会有一个隐含的Super Class,这个类会带有一个生成的带前缀的SwiftObject,比如_TtCs12_SwiftObject。Swift是实现了NSObject的一个objc运行时的类型,这个类型不能和OC交互。但是如果继承了NSObject就可以和OC交互。 如果方法或属性声明了 @objc dynamic,那么就可以在运行时通过动态派发在Swift对象上去调用,方法是:使用AnyObject的Perform方法去执行NSSelectorFromString里传入的方法或属性名。 对于Swift里的值类型,比如Struct、Enum、Array等,可以遵循_ObjectiveCBridgeable协议,经过Type Casting(显示或隐式)转成对应的OC对象类型。举个例子,如果想要查看Array的类继承关系,代码如下:
如上面代码所示,Swift的Array最终都是继承自NSObject,其它值类型也类似。可以看出,所有Swift类型都是可兼容objc运行时的。因此可以给这些值类型添加objc运行时方法,代码如下:
如上面代码所示,取出函数闭包可以通过 @convertion(block)转换成C函数Call Convention来调用,C函数也可以直接去执行这个指针。使用 Memory Dump 工具可以查看Swift函数内存结构,以及解析出符号信息DL_Info。Memory Dump工具有Mikeash的memorydumper2[11] ,源码解读可以参考Swift Memory Dumping[12] 。逆向查看内存布局可以参考 《初探Swift Runtime:使用Frida实现针对Alamofire的抓包工具》[13] 类的方法替换在运行时进行类方法的替换时,先将方法的Block以AnyObject类型传入imp_implementationWithBlock方法,返回一个imp,然后使用 class_getInstanceMethod 来获取实例的原方法,再通过 class_replaceMethod 进行方法替换,完整代码可以参看InterposeKit[14] ,另外还有一个使用libffi的方法替换库,参见SwiftHook[15] 。 另外,通过获取函数地址来改变函数指向位置的方法在Swift里实现比较困难,这是因为NSInvocation不可用了,因此需要通过C的函数来Hook Swift。在Swift的AnyClass中有类似OC的布局,记录了指向类和类成员函数的数据,这样就可以使用汇编来做函数指针替换的事情。思路是:保存寄存器,调用新函数,然后恢复寄存器,还原函数。具体可以参考项目SwiftTrace[16] 。 插桩使用编译插桩的方式也可以实现运行中的方法替换,关键步骤在于编译时,需要使用DYLD_INSERT_LIBRARIES进行拦截,CommandLine.arguments可以得到Swiftc的执行参数,以查找待编译的Swift文件。通过苹果公司的SwiftSyntax[17] 源代码解析、生成和转换的工具可以查出所有方法,并插入特定的方法替换逻辑代码。修改完通过-output-file-map来获取mach-o的地址去覆盖先前产物。使用self.originalImplementation(…)调用原始的实现作为闭包传入execute(arguments:originalImpl:)方法。 ClassContextDescriptorBuilderSwift运行时给每个类型保留了Metadata信息。Metadata是由编译器静态生成的,有了Metadata的调试才能够发现类型的信息。Metadata偏移-1是Witness table 指针,Witness Table 提供分配、复制和销毁类型的值,Witness Table 还记录了类型大小、对齐、Stride等其它属性。Metadata偏移量0的地方是Kind字段,其描述了Metadata所描述的类型的种类,例如Class、Struct、Enum、Optional、Opaque、Tuple、Function、Protocol等类型。这些类型的Metadata具体详述可见Type Metadata 的官方文档[18] ,代码描述可以在include/swift/ABI/MetadataValues.h[19] 里看到。比如在Metadata里类的方法数量会比实际代码里写的方法数量要多,那是因为编译器会自动生成一些方法,这些方法的种类在MethodDescriptorFlags类中Kind里描述了,代码如下:
可以看到,Getter、Setter以及线程相关读写的ModifyCoroutine、ReadCoroutine类型都是自动生成的。 Class的内存结构生成方法可以在 /lib/IRGen/GenMeta.cpp [20] 里找到:
内存布局指的是使用一个Struct或者Tuple,根据每个字段的大小和对齐方式决定怎样来安排内存中的字段,在这个过程中,不仅需要描述清楚每个字段的偏移量,还有Struct或Tuple整体的大小和对齐方式。下面就是GenMeta里和Class类型相关的内存方法代码:
根据GenMeta可以看到Swift的Class类型内存布局是根据ContextDescriptorBuilderBase、TypeContextDescriptorBuilderBase再到ClassContextDescriptorBuilder继承层层叠加的,因此对应Class类型的Nominal Type Descriptor就可以用如下C结构来描述:
代码中可见,add的前缀就是增加的偏移记录,addFlags后面的addParent就是下一个偏移的记录。FieldDescriptor换成ReflectionFieldDescriptor是苹果公司在5.0版本对Metadata做的改变,官方Mirror反射目前还不完善,有些信息还没法提供,因此在Metadata里增加了一些反射相关信息。 OC动态调用方法会把_cmd作为第一个参数,第二个参数是Self,后面是可变参数列表,动态调度可以在运行时添加类、变量和方法。而在Swift中动态调用方法是基于VTable的,运行时没法对方法进行动态搜索,地址在编译时静态写在了VTable里,运行时不能改,可以用静态地址调用,或dlsym来搜索名称。 VTable的地址在TypeContextDescriptor之后,OverrideTable存储位置在VTable之后,有三个字段来描述,第一个是记录哪个类被重写,第二个是被重写的函数,第三个是用来重写的函数相对的地址。因此通过OverrideTable就可以找到重写前和重写后函数指针,这样就有机会在VTable里找到对应函数进行函数指针的替换,达到Hook的效果。要注意,在Swift编译器设置优化时VTable的函数地址可能会清空或使用直接地址调用,这两种情况发生的话就没法通过VTable进行方法替换。 那么还有其它思路吗?这里先提供一波资料包。 Mach_override使用Wolf Rentzsch[21] 写的M ach_override[22] 也是一种方法,可以在原始函数的汇编里加个jmp,跳到自定义函数,然后再跳回原始函数。Mach_override_ptr的三个参数分别是,一,要覆盖函数的指针;二,去覆盖函数的指针;三,参数可以设置为原函数的指针地址,待Mach_override_ptr返回成功,就可以调原函数。Mach_override会分配一个虚拟内存页,使其可写可执行。需要注意的是,Mach_override_ptr初始函数和重入函数指针相同,调用后,重入函数将调用替换函数而不是原始函数。在Swift中如何使用Mach_override可参考SwiftOverride[22] 。 总结通过上下篇的介绍,想必你已经了解到A站为拥抱Swift都做了哪些事情。基于A站以及快手主站的一些架构师对于Swift的热爱,以及为之付于的实践,A站的开发体验才得以蜕变。
根据Swift类型推导的特性,按道理str类型加上感叹符号后,strCopy就会被自动推导为非可选String类型。但实际情况是,按照官方文档[27] 的说法,strCopy没有直接指明类型,即隐式可选值时,str类型是String后加上感叹号,这种是属于隐含解包可选值String无法推导出非可选String类型,因此Swift会先将strCopy作为一个普通可选值来用,这样和直观的感觉非常不一样。 本以为5.0的ABI在稳定后,Swift学起来会更容易,但是其实新的SwiftUI和Combine这样重量级的框架需要开发者继续钻研,真是“Write Swift, Learn Every Year”。Swift不断从其它语言中吸取精髓,接下来的async/await,你准备好了吗?要用上,先得看咱家APP系统最低版本是不是能够支持这些新特性。 虽说不容易,但为了稳定和效率,终究跟上了时代的步伐。 推荐阅读:iOS自动编译 |
|
移动开发 最新文章 |
Vue3装载axios和element-ui |
android adb cmd |
【xcode】Xcode常用快捷键与技巧 |
Android开发中的线程池使用 |
Java 和 Android 的 Base64 |
Android 测试文字编码格式 |
微信小程序支付 |
安卓权限记录 |
知乎之自动养号 |
【Android Jetpack】DataStore |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 7:36:29- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |