天天看点

Swift和C/Objective-C混编超详解

    文章可能写得有点晚了,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中使用的。在创建了头文件以后,还需要确认已经把头文件加入到了工程编译路径中(如下图)

Swift和C/Objective-C混编超详解

      然后,只要声明在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> T * UnsafeMutablePointer<Int8> char *
不可变指针 UnsafePointer<T> T *__nonnull  UnsafePointer<Float> 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<Int8> char *a;
var a: UnsafeMutablePointer<Int8> const char *a;
let a: UnsafeMutablePointer<Int8> char *const a;
var a: UnsafeMutablePointer<UnsafeMutablePointer<Int8>> 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<Int8>,而不是String。

    另外,一些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<Int>) {// 省略实现}

// 函数调用
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语言的特性优势,编写出更优秀的代码。

    如果读者还有什么问题,欢迎留言讨论,本文的一切权利归本人所有,如果读者希望转发,请在开头标注出转发字样以及出处,多谢配合!