文章可能写得有点晚了,Swift语言已经诞生很久的时间了,现在它已经挤掉了OC很大的市场了,但是,总是存在很多老项目,或者是第三方库还没有完全翻译成Swift,因此,混编还是需要的。虽然现在详解可能有点晚,不过还是希望能写一篇关于混编的详细讲解,方便那些遇到困惑的童鞋学习和查阅。
无论是在OC工程里插入Swift,还是在Swift工程里插入OC,其实都没什么区别,因为Swift编译器本来就是用OC写的,所谓的Swift工程,其背后其实有大量OC代码参与了编译,因此,效果是等价的,本文以Swift项目插入OC代码为例进行讲解。顺便强调一下,如果是嵌入纯C代码(.c后缀)的,它和嵌入OC代码(.m后缀)完全相同,只是不能够使用OC语法和函数库而已,因此本文的内容完全适用。至于与C++混编的问题,必须要OC++文件作为桥梁(.mm后缀),所以,Swift与C++混编问题可以拆解为Swift与OC混编,以及OC与C++混编问题,关于OC与C++混编问题,请移步至本人早些日子的播客文章,有一篇详细介绍C、C++和OC混编的问题,因此,在本文不再赘述,本文只针对于Swift与OC混编。
重要!!:本文很多地方用到了“转化”“转换”“变成”“等价于”这样的词语,只是说,在接口调用时,可以视为在这个语言中是这样写的,但是,并不是说两种写法等价,或者说真的有做转换,代码本身是没有进行转换的,它们都是独自编译成机器码,最后整体链接的,并不存在两种语言的转化过程,这个请读者一定要明白,不要被误导!具体来说,文中说OC中的函数[OC内容]会被转化为Swift中的[Swift内容],其实指的是,如果想在Swift中调用OC的函数的话,我们可以理解为,调用了Swift中的[Swift内容]这中形式的函数,但其实在代码中并不会出现[Swift内容]这样的代码。
重要的事情要再强调一遍!我说的语言1中的A转换成语言2中的B,意思是在语言2中要想调用语言1中的A,可以当做是调用了语言2中的B。而事实上并没有B这个东西的存在!切记切记!
例如,我们的工程中主流程存在于main.swift中,然后,我们创建了test.m和test.h,第一次在Swift工程中嵌入.c或者.m文件时Xcode可能会自动生成一个test-Bridging-Header.h的文件,如果没有自动生成,我们需要手动生成,注意,将test替换为任意的名字,后面的部分是不能够改变的,并且,并不需要为每个OC文件都配一个Bridge头文件,工程中只需出现一个即可(当然了,你想根据代码的内容创建多个也是完全可以的)。这个Bridge头文件的作用是什么呢?OC代码中,全局变量、全局函数都是需要声明才能使用的,因此我们通常使用头文件来保存函数声明、变量声明,当然还有一些宏定义的处理。需要注意的是,宏定义等这些预编译的东西是不能够在Swift中使用的。在创建了头文件以后,还需要确认已经把头文件加入到了工程编译路径中(如下图)
然后,只要声明在Bridge头文件中的(当然也包含Bridge头文件包含的其他普通头文件中的声明),就可以在Swift代码中使用,同理,如果在Swift代码中有实现,那么在Bridge头文件中加以适当的声明,那么,就可以在OC文件中使用(使用时OC文件需要包含Bridge头文件)。举一个简单的例子,在test.m中我们写了一个函数:
int test1(char a) {
return a + 5;
}
我们要想在Swift中使用这个函数,那么,我们就需要在Bridge头文件中写上:
int test1(char a);
那么,在main.swift中,可以直接这样调用:
let a = test1(a: 5)
并且,执行后a的值是10。
同理,如果我们在Swift代码中有这样一个函数:
func test2(a: Int) -> Double {
return Double(a) + 0.5
}
如果想在OC代码中使用,我们就在Bridge头文件中这样声明:
double test2(int a);
然后,我们就可以在OC中这样使用:
double b = test2(5);
b的值就是5.5,当然,使用之前需要包含头文件:
#import "test-Bridging-Header.h"
这个很好理解,但是,比较讨厌的就是,多数情况下,我们并不是使用这样简单的基本变量类型来传参,我们使用的是比较复杂的类型,因此,我整理了一个表格,表示Swift类型和OC类型的相互对应:
(1)基本变量类型
名称 | Swift类型 | OC类型 |
---|---|---|
字符型 | Int8 | char |
无符号字符型 | UInt8 | unsigned char |
短整型 | Int16 | short |
无符号短整型 | UInt16 | unsigned short |
长整型 | Int32 | long |
无符号长整型 | UInt32 | unsigned long |
超长整型 | Int64 | long long |
无符号超长整型 | UInt64 | unsigned long long |
浮点型 | Float | float |
双精度浮点型 | Double | double |
需要注意的是,Swift中的Int,UInt类型以及OC中的int, unsigned类型究竟会转化为16位的还是32位的,还是64位的整型,需要根据开发环境的情况,由编译器决定。因此,如果需要控制数据位数防止溢出的话,那么并不建议使用这些模棱两可的数据类型,这得根据实际开发的需求决定。
(2)结构体类型
OC中的结构体类型会被转化为Swift中的结构体类型,例如OC中的:
struct st_test {
long a;
char b, c;
};
对应了Swift中的:
struct str_test {
var a: Int32
var b: Int8, c: Int8
}
(3)共合体类型
Swift中不存在共合体类型,因此,OC中的union类型会转化为Swift中的struct,但是注意的是,虽然Swift中把他认为是结构体,但是实际上它还是共合体,因此所有成员是公用内存空间的。Swift中的struct只能在OC中声明为struct而不能声明为union。
(4)枚举类型
注意,Swift的enum类型不能转化为OC中的enum,只能将其转化为类,才能转化为OC的类,而OC中的enum会被转化为Swift中的全局变量以及类型重命名,例如OC中的:
enum en_test {
a, b,
c = 5, d
};
会被转化为Swift中的:
typealias en_test = Int32
let a: Int32 = 0
let b: Int32 = 1
let c: Int32 = 5
let d: Int32 = 6
这个是非常不一样的转化,一定一定一定要注意!
(5)数组类型
Swift中的数组(Array)会被转化为OC中的NSMutableArray,而OC中的数组会被转化为Swift中的元组,例如OC中的:
float arr[5];
会被转化为Swift中的:
var arr: (Float, Float, Float, Float, Float)
多维数组则会转化为嵌套元组,例如OC中的:
long arr_3[2][3][4];
会被转化为Swift中的:
var arr_3: (((Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32)), ((Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32), (Int32, Int32, Int32, Int32)))
嵌套顺序是OC中越靠后的数字在Swift中越靠内层。
关于Swift数组的转换见后面的“类与对象”部分。
(6)非对象指针
OC中的指针,会被转化为Swift中的UnsafePointer类型,并且,会根据关键字修饰的不同有不同的转化,见下表:
名称 | Swift类型 | OC类型 | 示例(Swift) | 示例(OC) |
---|---|---|---|---|
普通指针 | UnsafeMutablePointer |
T * | UnsafeMutablePointer |
char * |
不可变指针 | UnsafePointer |
T *__nonnull | UnsafePointer |
float *__nonnull |
(以下可变与不可变都是按照上面的方式,不再单独列出) | ||||
数组指针 | UnsafeMutablePointer<((T, T, ...))> | T (*)[n] | UnsafeMutablePointer<((Int16, Int16, Int16))> |
short (*)[3] |
函数指针 | (T1, T2, ...) -> T | T (*)(T1, T2, ...) | (a: Int32, b: Float) -> Double | double (*)(long a, float b) |
特别特别要注意的是,虽然数组会被转化为元组,但是,数组指针在转化后会加上一层元组,也就是说n维数组指针在转化后会被变成UnsafePointer的n+1维的元组的实例。
还有一点需要关注的是,如果数组在函数参数中,是会把最外层退化为指针的,那么转化成Swift的时候也要按数组指针来对待,例如OC中:
void func1(char a[2][3]);
会被转化为Swift中的:
func func1(a: UnsafeMutablePointer<((Int8, Int8, Int8))>)
因为这里的char a[2][3]其实是被转化为了char (*a)[3]。
(7)普通类/对象
这里的普通对象指的是非类库中的对象,以及一部分类库中的对象(例外将在后面“特殊类/对象”的部分中)。
普通的类将会被直接转化,例如OC中的如下代码:
// 类型示例
@interface Test_Cl : Test_PC {
long a;
}
- (char)func1;
- (void)func2:(char)arg1 andOA:(unsigned short)arg2;
+ (long)func3;
@end
// 省略实现
// 调用示例
Test_C1 *t1 = [[Test_C1 alloc] init];
char o1 = [t1 func1];
[t1 func2:'a' andOA:5];
[Test_C1 func3];
会被转化为Swift中的:
class Test_C1: Test_PC {
private var a: Int32
func func1() -> Int8 {// 省略实现}
func func2(_ arg1: Int8, andOA arg2: UInt16) {// 省略实现}
static func func3() {// 省略实现}
}
// 调用示例
var t1 = Test_C1()
var o1: Int8 = t1.fun1
t1.func2(Int8('a'), andOA 5)
Test_C1.func3()
内容比较多,希望读者仔细观察,OC中的方法名中后续部分会被转化为Swift方法中的外部变量名,并且,由于OC中的方法名会包含第一个参数的提示,因此转化为Swift后,第一个参数一定是无外部参数名的,提示部分会显示到方法名中(请仔细观察上面func2的示例)。OC中直接写在类头大括号里的变量全部都会被转化为Swift中的private,注意@public、@protected和@private关键字会被无视,统一都会转化为Swift中的private,而Swift中的private变量会被转化为OC中的@private修饰的变量。
很多关键字与类组合时,在转化时都会有变化,本节只是简单介绍一下,详细的情况请看后面部分。
(8)const
大体来说,OC中没有用const修饰的,会转化为Swift的var修饰,用const修饰的会被转化为Swift中的let,但是如果修饰在嵌套类型的非最外层则会被无视,例如:
Swift中 | OC中 |
---|---|
let a: Int | const int a; |
var a: UnsafeMutablePointer |
char *a; |
var a: UnsafeMutablePointer |
const char *a; |
let a: UnsafeMutablePointer |
char *const a; |
var a: UnsafeMutablePointer |
char *const *a; |
但是在搭配很多类库对象的时候,是会有不同的,详见“特殊类/对象”部分。
(9)Block与闭包
OC中的Block和函数指针一样,都会被转化为Swift中的闭包,但是,Swift的闭包只会被转化为OC中的Block而不是函数指针,例如OC中的:
int (^const f1)(char, char) = ^(char a, char b) {
return a + b;
}
会被转化为Swift中的:
let f1: (Int8, Int8) -> Int = {(a, b) in
return a + b
}
需要注意的是,OC的Block和函数指针会被转化为Swift的非逃逸闭包,相当于用@unscaping修饰。
(10)特殊类/对象
特殊的主要发生在基本类库中,这个和Swift版本有关系,第一版的Swift是没有这些所谓特殊的类的,而后面因为类库命名有很大更改,并且,Swift语法也在不断改变,为了让开发者使用方法,所以才出现了一些特殊的转换(注:写本文针对于Swift 4版本,不一定适用于其他版本,因此请读者关注自己使用的Swift版本,如果你在阅读本文时已经有了更新的版本,那么可能会有一些差别,但是这并不影响你阅读本节,因为这会给你一个思路,而具体细节可以查阅苹果的官方文档,有关于每个版本更新的语法改变说明)。
这部分比较复杂,所以,这里还是用表格的方式呈现:
说明 | Swift | OC | 示例(Swift) | 示例(OC) |
---|---|---|---|---|
同一类型元组 | (T, T, T...) | T[] | let a = (1, 2, 3, 4) | const int a[4] = {1, 2, 3, 4}; |
数组 | [T] | NSArray | let a = [1, 2, 3] | NSArray *a = @[1, 2, 3]; |
可变数组 | [T] | NSMutableArray | var a [1, 2, 3] | NSMutableArray *a = @[1, 2, 3]; |
字符串 | String | NSString | let a = "123" | NSString *a = @"123"; |
可变字符串 | String | NSMutableString | var a = "123" | NSMutableString *a = @"123"; |
字典 | [K, V] | NSDictionary | let a = [1: "one", 2: "" two] | NSDictionary *s = [NSDictionary dictionaryWithObjectsAndKeys: @1, @"one", @2, @"two"]; |
可变字典 | [K, V] | NSMutableDictionary | 略 | 略 |
任意 | Any | void * | let a: Any | void *const a; |
任意对象 | AnyObject | void *__nonnull | var a: AnyObject | void *__nonnull a; |
特别需要注意的是,C语言字符串(char数组)会被转化为UnsafePointer
另外,一些NS开头的类在Swift中也是可用的,他们会被直接转换,例如Swift中的NSString仍然会被转化为OC中的NSString,但是,OC中这些NS开头的则会转化为Swift中的基本类型,例如NSMutableString就会被转化为Swift中以var修饰的String。
对于嵌套类型,则全部会转化为对象,例如Swift的[Int:[Int]]类型就会被转化为NSArray类型,而其中的成员都已经被转化为了NSObject或其子类,Int会被转化为NSNumber,而[Int]则会被转化为NSArray,操作NSArray时利用多态性,其成员都是NSObject *类型。
(11) 指针与inout参数
这个说起来应该不算混编内容,应当是纯Swift内容,但是由于单纯使用Swift时真是太少使用指针了,反而是在混编时经常会遇到,因此在这里讲解。
在Swift中,如果函数参数是UnsafePointer类型(或其他的指针类型)时,可以直接传进inout参数,例如:
// 函数定义
func func1(a: UnsafeMutablePointer) {// 省略实现}
// 函数调用
var a = 0
func1(&a) // 合法
也就是说,参数是指针类型时,可以把它与用inout修饰的函数参数等同对待。因此,在混编时,如果一个OC函数参数需要传指针的话,在Swift里调用时可以直接传&变量,而不用去用复杂的语句包装成指针类型再传参。
(12)可变类型的函数参数
Swift函数的形参是不能够改变值的,曾经的Swift可以在参数前用var修饰使其可变,但是在Swift3中把这个语法取消了,因此,Swift函数转化成OC时,所有的参数都会默认带上const。例如Swift中:
func f1(a: Int, b: Int) {// 省略实现}
会被转化为OC中的:
void f1(const int a, const int b) {// 省略实现}
而当OC转Swift的时候,这里的const是会被丢弃的(因为在Swift中本来就不可变)。
(13)可选类型
通常情况下,OC转Swift的时候不会转为Optional类型,但是,在之前提到的那些特殊类的转化中,是存在可选类型的,因为这部分的let和var其实对应了不同的类,所以用于处理__nonnull关键字的,就需要可选类型来担当了,例如:
Swift | OC | 示例(Swift) | 示例(OC) |
---|---|---|---|
String? | NSString * | let a: String? = "123" | NSString *const a = @"123"; |
String | NSString *__nonnull | let a = "123" | NSString *__nonnull a = @"123" |
String | NSMutableString *__nonnull | var a = "123" | NSMutableString *__nonnull a = @"123" |
其他的那些特殊类都是一样的道理,let和var控制的是是否有Mutable,而是否可选控制的是有无__nonnull。
(14)成员变量
Swift中没有被private修饰的成员变量,会转化为OC中的@property,反之亦然。例如Swift中的:
class Test_Cl {
var a: Int
}
被转化为OC中的:
@interface Test_Cl
@property int a;
@end
也就是说,在OC中是会自动生成set和get方法的,但是如果手动写OC的get和set,是不会转化为Swift的成员的,仍然会转化为一个private成员加两个普通的方法。
(15)构造函数
类库中大部分类都对Swift重构了,所以构造函数这里可以放心调用,但是,如果没有进行重构的话,只有标准OC构造函数和Swift无参构造函数可以互相转化,而OC的非标准构造函数将会转化为Swift的类函数,Swift的有参构造函数则无法被转化。OC中的:
@interface Test_Cl : Test_PC
- (instancetype)init; // 只有这个函数头是标准的构造函数
- (instancetype)initWithName:(NSString *)name; // 这个就是普通函数了
@end
会被转化为Swift中的:
class Test_Cl: Test_PC {
init() {// 省略实现}
static func initWithName(_ name: String) {//这就是个普通函数了,省略实现}
}
(15)不能进行转化的
除了上文提到的一些会丢弃的部分,还有一些语法由于没有提供语法接口,是不能够跨语言调用的,例如:运算符重载,计算方法,逃逸闭包,含有外部引用的闭包,重载函数, 有参构造函数等等。所以在混编时要注意避免这些问题,否则这部分代码将不能跨语言调用。
读者应该能够发现,大多数场景下,都是在Swift工程中调用OC的接口,很少会有反过来的,基本上OC的都系在Swift中都能够得到比较好的兼容,而反过来的话则会有很多不能够调用的情况。
以上就是本人对Swift于OC混编的总结,希望能够帮到在此纠结的朋友,帮大家度过转化期。但是对于需要继续长久使用的OC工程或是第三方库,还是应当尽早完成对Swift语言的重构,这样就可以避免在混编过程中出现的问题,同时还可以充分利用Swift语言的特性优势,编写出更优秀的代码。
如果读者还有什么问题,欢迎留言讨论,本文的一切权利归本人所有,如果读者希望转发,请在开头标注出转发字样以及出处,多谢配合!