当前位置:   article > 正文

A站 的 Swift 实践 ,收获总结。_swift 实践 改造

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就需要进行OC和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里。

如何设置CocoaPods

Swift 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主要是通过自己的dsl配置来完成这些编译参数的设置,所以就需要先了解些混编设置的编译参数和概念:

  • 前面提到的Defines Module,需要设置为YES。
  • Module Map File表示 Module Map的路径。
  • Header Search Paths代表Module Map定义的OC头文件路径。
  • Product Module Name的默认设置和Target Name一样。
  • Framework Search Paths是设置依赖Framework的搜索路径。
  • Other C Flags可以用来配置依赖其它Module文件路径。
  • Other Swift Flags可以配置其Module Map文件路径。

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所处流程位置,如下图所示:
在这里插入图片描述
按照上图的逻辑,Integrates这一步主要是用来配置Module的。先检查Targets,主要是对于包括Swift版本和Module依赖等问题的检查,然后再使用Xcodeproj组件做Module的工程配置。

完成以上工作后,如果我们想要在Swift里使用OC开发的库FMDB,就可以直接使用Import来导入,代码如下

import UIKit
import FMDB

class SwiftTestClass: NSObject {    
var db:FMDB.FMDatabase?
   
   override init() {        
    super.init()        
    self.db = FMDB.FMDatabase(path: "dbname")       
    print("init ok")
    
    }
 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

可以看到,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只会发生在头文件包含的任何头文件有变动,或者依赖另外一个模块有更新的时候。比如下面的代码:

#import <FMDB/FMDatabase.h>
  • 1

Clang会先从FMDB.framework的Headers目录里查找FMDatabase.h,再去FMDB.framework的Modules目录里查找module.modulemap文件,分析module.modulemap来判断FMDatabase.h是否是模块的一部分。Module Map用来定义Module和头文件之间的关系。FMDB.framework的module.modulemap的内容如下:

framework module FMDB {  
umbrella header "FMDB-umbrella.h"
  
  export * 
  module * { export * }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

想要确定FMDatabase.h是否是Module的一部分就要看module.modulemap里的Umbrella Header文件,即FMDB-umbrella.h目录里是否包含了FMDatabase.h。在Headers目录里查看FMDB-umbrella.h文件,内容如下:

__#import <UIKit/UIKit.h>
#else
#ifndef FOUNDATION_EXPORT
#if defined(__cplusplus)
#define FOUNDATION_EXPORT extern "C"
#else
#define FOUNDATION_EXPORT extern
#endif
#endif
#endif

#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "FMDatabasePool.h"
#import "FMDatabaseQueue.h"
#import "FMDB.h"
#import "FMResultSet.h"

FOUNDATION_EXPORT double FMDBVersionNumber;FOUNDATION_EXPORT const unsigned char FMDBVersionString[];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

上面代码中可以看到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会生成更多信息,比如下面的代码:

public func int2string(number: Int) -> String {    
return "(number)"
}
  • 1
  • 2
  • 3

Swiftc编译后,使用nm -g查看生成如下的信息:

0000000000003de0 T _$s8demotest10int2string6numberSSSi_tF
  • 1

如上所示,信息中的$s表示全局,8demotest的demotest是Module名,8是Module名的长度。int2string是函数名,前面的10是类名长度,6number是参数名。SS表示参数类型是Int。Si表示的是String类型,_tF表示前面的Si是返回类型。

接下来对比一下Clang和Swiftc的编译过程,首先是Clang的编译过程,如下图
在这里插入图片描述
其次是Swift的编译过程,如下图:
在这里插入图片描述
从两者的对比中可以看出,Swift编译过程缺少了头文件,因为它通过分组编译模糊了文件的概念,减少了很多重复查找声明的工作,这样不仅仅可以简化代码的编写,还可以给编译器更多的发挥空间。

至于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接口做些修改适配,比如将函数改成使用OC泛型,NSArray paths转成Swift是open var paths:[Any];如果使用了泛型,将其改成 NSArray paths,那对应的Swift就是open var paths:[KSPath],这种接口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结构里,比如下面的代码:

CFStringRef showTwoString(CFStringRef s1, CFStringRef s2)
  • 1

转成Swift就是:


func showTwoString(_: CFString!, _: CFString!) -> Unmanaged<CFString>! {  
// ...
}
  • 1
  • 2
  • 3
  • 4

如上面代码所示,Core Fundation 类型的名字转换后会去掉后缀Ref,这是因为在Swift中所有类都是引用类型,Ref后缀比较多余。上面的Unmanaged结构有两个方法,一个是takeUnretainedValue(),另一个是takeRetainedValue(),这两个方法都是用来返回对象的原始未封装类型。如果对象之前没有Retain就用takeUnretainedValue(),已经Retain了,就用takeRetainedValue()。

在Swift里用getVaList(::

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/代码探险家/article/detail/789257
推荐阅读
相关标签