swift开发

Swift与OC交互

2016-07-31  本文已影响606人  Isy

为了方面查阅特转一篇文章

Swift的设计的初衷就是摆脱ObjC沉重的历史包袱,毕竟ObjC的历史太过悠久,相比于很多现代化语言它缺少一些很酷的语法特性,而且ObjC的语法和其他语言相比差别很大。但是Apple同时也不能忽视ObjC的地位,毕竟ObjC经过二十多年的历史积累了大量的资源(开发者、框架、类库等),因此在Swift推出的初期必须考虑兼容ObjC。但同时Swift和ObjC是基于两种不同的方式来实现的(例如ObjC可以在运行时决定对象类型,但是Swift为了提高效率要求在编译时就必须确定对象类型),所以要无缝兼容需要做大量的工作。而作为开发人员我们有必要了解两种语言之间的转化关系才能对Swift有更深刻的理解。

Swift与OC之间的映射

其实从前面的例子中大家不难发现Swift和ObjC必然存在着一定的映射关系,例如对于文件的操作使用了字符串的writeToFile方法,在网络请求时使用的NSURLSession,虽然调用方式不同但是其参数完全和做ObjC开发时调用方式一致。原因就是Swift编译器自动做了映射,下面列举了部分Swift和ObjC的映射关系帮助大家理解:

|Swift|Objc|备注|
|::|::|::|
|AnyObject|id|Swift中用到Objc中id类型的参数被标记为对应的可选类型|
|Array、Dictionary、Set|NSArray、NSDictionary、NSSet|Swift中的Int、UInt、Float、Double、Bool会自动桥接为NSNumber|
|Int|NSInteger、NSUInteger|其他基本类型情况类似,不再一一列举|
|NSObjectProtocol|NSObject协议(注意不是NSObject类)|由于Swift在继承或者实现时没有类的命名空间的概念,而ObjC中既有NSObject类又有NSObject协议,所以在Swift中将NSObject协议对应成了NSObjectProtocol|
|CGContext|CGContextRef|Core Foundation中其他情况均是如此,由于Swift本身就是引用类型,在Swift不需要再加上“Ref”|
|ErrorType|NSError||
|“ab:"|@selector(ab:)|Swift可以自动将字符串转化成成selector|
|@NSCopying|copy属性|init(x:X,y:Y) initWithX:(X)x y:(Y)y 构造方法映射,Swift会去掉“With”并且第一个字母小写作为其第一个参数,同时也不需要调用alloc方法,但是需要注意ObjC中的便利工厂方法(构建对象的静态方法)对应成了Swift的便利构造方法|
|func xY(a:A,b:B) |void xY:(A)a b:(B)b| |
|extension(扩展) |category(分类) |注意:不能为ObjC中存在的方法进行extension|
|Closure(闭包)| block(块) |注意:Swift中的闭包可以直接修改外部变量,但是block中要修改外部变量必须声明为__block|

Swift兼容大部分ObjC(通过类似上面的对应关系),多数ObjC的功能在Swift中都能使用。当然,还是有个别地方Swift并没有考虑兼容ObjC,例如:Swift中无法使用预处理指令(例如:宏定义,事实上在Swift中推举使用常量定义);Swift中也无法使用performSelector来执行一个方法,因为Swift认为这么做是不安全的。

相反,如果在ObjC中使用Swift也同样是可行的(除了个别Swift新增的高级功能)。Swift中如果一个类继承于NSObject,那么他会自动和ObjC兼容,这样ObjC就可以按照上面的对应关系调用Swift的方法、属性等。但是如果Swift中的类没有继承于NSObject呢?此时就需要使用一个关键字“@objc”进行标注,ObjC就可以像使用正常的ObjC编码一样调用Swift了(事实上继承于NSObject的类之所以在ObjC中能够直接调用也是因为编译器会自动给类和非private成员添加上@objc,类似的@IBoutlet、@IBAction、@NSManaged修饰的方法属性Swift编译器也会自动添加@objc标记)。

Swift中调用C

由于ObjC是C的超集,使得在ObjC可以无缝访问C语言。但是Swift的产生就是ObjC without C,因此在Swift中不可能像在ObjC中混编入C一样简单。但是考虑到C语言的强大以及历时那么多年留下了丰富的类库,有时候又不得不使用它,Swift中还是保留了与一定数量的C语言类型和特性的兼容。前面介绍过关于如何在Swift中使用ObjC的知识,事实上在Swift中使用C也是类似的(因为ObjC是C的超集,ObjC既然可以桥接,C自然也可以),你需要一个桥接文件,不同的是ObjC中的很多内容在桥接到Swift时都是类似,很容易上手。例如ObjC中使用的NSObject,在Swift中仍然对应NSObject,很多时候开发人员感觉不到这种转化,只是编程语言发生了变化。但是C导入Swift就需要必须要了解具体的对应关系:

|C类型|Swift类型 |说明|
|::|::|::|
|基本类型| | |
|char,signed |char CChar|类似的unsigned char对应CUnsignedChar|
|int|CInt|类似的unsigned int对应CUnsignedInt|
|short|CShort|类似的unsigned short对应CUnsignedShort|
|long |CLong|类似的unsigned long对应CUnsignedLong|
|long long |CLongLong|类似的unsigned long long 对应 CUnsignedLongLong|
|float |CFloat| |
|double|CDouble||
|构造体类型| | 注意:结构体实现|
|枚举typedef NS_ENUM(NSInteger,A){AB,AC} |enum A:Int{case B,C}|去掉对应的前缀 ,注意C中的NS_Options会对应成Swift中实现OptionSetType的结构体|
|结构体| |对应Swift中的结构体 |
|联合 | |Swift中不能完全支持联合,建议使用枚举关联值代替|
|指针类型||C语言中的指针类型映射成了Swift中的泛型|
|Type *|UnsafeMutablePointer<Type> |作为返回类型、变量、参数类型时|
|const Type *|UnsafePointer<Type>| 作为返回类型、变量、参数类型时|
|Type *const *|UnsafePointer<Type>| 对于类类型|
|Type *__strong *|UnsafeMutablePointer<Type>|对于类类型|
|Type * *| AutoreleasingUnsafePointer<Type>|对于类类型|
|函数指针|闭包|||

对于其他类型的映射关系都很容易理解,这里主要说一下指针的内容。通过上表可以看到在C中定义的一些指针类型当在Swift中使用时会有对应的类型,但是如果一个参数为某种指针类型,实际调用时应该使用何种Swift数据类型的数据作为参数调用呢?例如参数为UnsafePointer<Type>,是否只能传入UnsafePointer<Type>呢,其实也可以传入nil,并且最终调用时将会转化为null指针来调用。下表列出了这种参数调用对应关系:

|可用类型|最终转化类型|
|::|::|
|UnsafePointer<Type> | 注意:如果Type为Void则可以代表任何类型|
| nil | null|
|UnsafePointer<Type>、UnsafeMutablePointer<Type>、AutoreleasingUnsafeMutablePointer<Type>| UnsafePointer<Type>|
| String |如果Type为Int、或者Int8将最终转化为UTF8字符串|
| &typeValue| 元素地址|
| Type类型的数组([typeValue1,typeValue2])| 数组首地址|
| UnsafeMutablePointer<Type> | 注意:如果Type为Void则可以代表任何类型|
| nil| null |
|UnsafeMutablePointer<Type> | UnsafeMutablePointer<Type> |
| &typeValue| 元素地址|
|Type类型的数组的地址(&[typeValue1,typeValue2]) |数组地址|
| AutoreleasingUnsafeMutablePointer<Type>||
| nil| null |
| AutoreleasingUnsafeMutablePointer<Type> | AutoreleasingUnsafeMutablePointer<Type>|
| &typeValue | 元素地址|
下面不妨看一下如何在Swift中使用C语言,假设现在有一个用于字符串拼接的C库函数“stringAppend(char*,const char *)”,将其对应的文件导入到一个Swift项目中按照提示添加桥接头文件并在桥接头文件中引入对应的C文件。
string.h

#ifndef __UseCInSwift__Common__
#define __UseCInSwift__Common__void stringAppend(char *source, char *toAppend)
 
#include
 
void stringAppend(char *source,const char *toAppend);
 
#endif

string.c

#include "string.h"
 
void stringAppend(char *source,const char *toAppend) {
    unsigned long sourceLen = strlen(source);
    char *pSource = source + sourceLen;
    const char *pAppend = toAppend;
    while (*pAppend != '\0') {
        *pSource++ = *pAppend++;
    }
}

UseCinSwift-Briding-Header.h

#import "string.h"

然后在Swift中调用上面的C函数

import Foundation
 
var sourceStr:String = "Hello"
var appendStr:String = ",World!"
 
var sourceCStr = (sourceStr as NSString).UTF8String
var sourceMutablePointer:UnsafeMutablePointer = UnsafeMutablePointer(sourceCStr)
 
stringAppend(sourceMutablePointer,appendStr)
 
println(String.fromCString(sourceMutablePointer)!) //结果:Hello,World!

当然,上面这种方式适合所有在Swift中引入C语言的情况,但是为了方便调用,在Swift中默认已经module了常用的C语言类库Darwin,这个类库就作为了标准的Swift类库不需要再进行桥接,可以直接导入模块(例如import Darwin,但是事实上Foundation模块已经默认导入了Darwin,而UIKit又导入了Foundation模块,因此通常不需要手动导入Darwin)。那么对于没有模块化的C语言类库(包括第三方类库和自己定义的C语言文件等)能不能不使用桥接文件呢?答案就是使用隐藏符号“@asmname”,通过@asmname可以将C语言的函数不经过桥接文件直接映射为Swift函数。例如可以移除上面的桥接头文件,修改main.swift函数,通过@asmname加stringAppend映射成为Swift函数(注意重新映射的Swift函数名称不一定和C语言函数相同):

import Foundation
 
//通过asmname将C函数stringAppend()映射到Swift函数,事实上这里的Swift函数名可以任意命名
@asmname("stringAppend") func stringAppend(var sourceStr:UnsafeMutablePointer,var apendStr:UnsafePointer ) -> Void
 
var sourceStr:String = "Hello"
var appendStr:String = ",World!"
 
var sourceCStr = (sourceStr as NSString).UTF8String
var sourceMutablePointer:UnsafeMutablePointer = UnsafeMutablePointer(sourceCStr)
 
stringAppend(sourceMutablePointer,appendStr)
 
println(String.fromCString(sourceMutablePointer)!) //结果:Hello,World!

反射

熟悉C#、Java的朋友不难理解反射的概念,所谓反射就是可以动态获取类型、成员信息,在运行时可以调用方法、属性等行为的特性。 在使用ObjC开发时很少强调其反射概念,因为ObjC的Runtime要比其他语言中的反射强大的多。在ObjC中可以很简单的实现字符串和类型的转换(NSClassFromString()),实现动态方法调用(performSelector: withObject:),动态赋值(KVC)等等,这些功能大家已经习以为常,但是在其他语言中要实现这些功能却要跨过较高的门槛,而且有些根本就是无法实现的。不过在Swift中并不提倡使用Runtime,而是像其他语言一样使用反射(Reflect),即使目前Swift中的反射还没有其他语言中的反射功能强大(Swift还在发展当中,相信后续版本会加入更加强大的反射功能)。

在Swift中反射信息通过MirrorType协议来描述,而Swift中所有的类型都能通过reflect函数取得MirrorType信息。先看一下MirrorType协议的定义(为了方便大家理解,添加了相关注释说明):

protocol MirrorType {
     
    /// 被反射的成员,类似于一个实例做了as Any操作
    var value: Any { get }
     
    /// 被反射成员的类型
    var valueType: Any.Type { get }
     
    /// 被反射成员的唯一标识
    var objectIdentifier: ObjectIdentifier? { get }
     
    /// 被反射成员的子成员数(例如结构体的成员个数,数组的元素个数等)
    var count: Int { get }
     
    //  取得被反射成员的字成员,返回值对应字成员的名称和值信息
    subscript (i: Int) -> (String, MirrorType) { get }
     
    /// 对于反射成员的描述
    var summary: String { get }
     
    /// 显示在Playground中的“值”信息
    var quickLookObject: QuickLookObject? { get }
     
    /// 被反射成员的类型的种类(例如:基本类型、结构体、枚举、类等)
    var disposition: MirrorDisposition { get }
}

获取到一个变量(或常量)的MirrorType之后就可以访问其类型、值、类型种类等元数据信息。在下面的示例中将编写一个函数简单实现一个类似于ObjC中“valueForKey:”的函数。

import UIKit
 
struct Person {
    var name:String
    var age:Int = 0
     
    func showMessage(){
        print("name=\(name),age=\(age)")
    }
}
 
 
//定义一个方法获取实例信息
func valueForKey(key:String,obj:Any) -> Any?{
    //获取元数据信息
    var objInfo:MirrorType = reflect(obj)
    //遍历子成员
    for index in 0..<objInfo.count {
        //如果子成员名称等于key则获取对应值
        let (name,mirror) = objInfo[index]
        if name == key {
            return mirror.value
        }
    }
    return nil;
}
 
var p = Person(name: "Kenshin", age: 29)
//先查看一下对象描述信息,然后对照结果是否正确
dump(p)
/*结果:
__lldb_expr_103.Person
- name: Kenshin
- age: 29
*/
 
var name = valueForKey("name", p)
print("p.name=\(name)") //结果:p.name=Optional("Kenshin")

可以看到,通过反射可以获取到变量(或常量)的信息,并且能够读取其成员的值,但是Swift目前原生并不支持给某个成员动态设置值(MirrorType的value属性是只读的)。如果想要进行动态设置,可以利用前面介绍的Swift和ObjC兼容的知识来实现,Swift目前已经导入了Foundation,只要这个类是继承于NSObject就会有对应的setValue:forKey:方法来使用KVC。当然,这仅限于类,对应结构体无能为力。

KVO

和KVC一样,在Swift中使用KVO也仅限于NSObject及其子类,因为KVO本身就是基于KVC进行动态派发的,这些都属于运行时的范畴。Swift要实现这些动态特性需要在类型或者成员前面加上@objc(继承于NSObject的子类及非私有成员会自动添加),但并不是说加了@objc就可以动态派发,因为Swift为了性能考虑会优化为静态调用。如果确实需要使用这些特性Swift提供了dynamic关键字来修饰,例如这里要想使用KVO除了继承于NSObject之外就必须给监控的属性加上dynamic关键字修饰。下面的演示中说明了这一点:

import Foundation
 
class Acount:NSObject {
    dynamic var balance:Double = 0.0
}
 
class Person:NSObject {
    var name:String
    var account:Acount?{
        didSet{
            if account != nil {
                account!.addObserver(self, forKeyPath: "balance", options: .Old, context: nil);
            }
        }
    }
     
    init(name:String){
        self.name = name
        super.init()
    }
     
    override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer) {
        if keyPath == "balance" {
            var oldValue = change[NSKeyValueChangeOldKey] as! Double
            var newValue = (account?.balance)!
            print("oldValue=\(oldValue),newValue=\(newValue)")
        }
    }
}
 
var p = Person(name: "Kenshin Cui")
var account = Acount()
account.balance = 10000000.0
p.account = account
p.account!.balance = 999999999.9 //结果:oldValue=10000000.0,newValue=999999999.9

注意:对于系统类(或一些第三方框架)由于无法修改其源代码如果要进行KVO监听,可以先继承此类然后进行使用dynamic重写;此外,并非只有KVO需要加上dynamic关键字,对于很多动态特性都是如此,例如要在Swift中实现Swizzle方法替换,方法前仍然要加上dynamic,因为方法的替换也需要动态派发。

内存管理

循环引用

Swift使用ARC来自动管理内存,大多数情况下开发人员不需要手动管理内存,但在使用ObjC开发时,大家都会遇到循环引用的问题,在Swift中也不可避免。 举例来说,人员有一个身份证(Person有idCard属性),而身份证就有一个拥有者(IDCard有owner属性),那么对于一个Person对象一旦建立了这种关系之后就会和IDCard对象相互引用而无法被正确的释放。

例如下面的代码在执行完test之后p和idCard两个对象均不会被释放:

import Foundation
 
class Person {
    var name:String
    var idCard:IDCard
     
    init(name:String,idCard:IDCard){
        self.name = name
        self.idCard = idCard
        idCard.owner = self
    }
     
    deinit{
        println("Person deinit...")
    }
}
 
class IDCard {
    var no:String
    var owner:Person?
     
    init(no:String){
        self.no = no
    }
     
    deinit{
        println("IDCard deinit...")
    }
}
 
func test(){
    var idCard = IDCard(no:"100188888888888888")
    var p = Person(name: "Kenshin Cui",idCard:idCard)
}
 
//注意test执行完之后p和idCard均不会被释放(无法执行deinit方法)
test()
 
println("wait...")

为了避免这个问题Swift采用了和ObjC中同样的概念:弱引用,通常将被动的一方的引用设置为弱引用来解决循环引用问题。例如这里可以将IDCard中的owner设置为弱引用。因为IDCard对于Person的引用变成了弱引用,而Person持有IDCard的强引用,这样一来Person作为主动方,只要它被释放后IDCard也会跟着释放。如要声明弱引用可以使用weak和unowned关键字,前者用于可选类型后者用于非可选类型,相当于ObjC中的__weak和__unsafe_unretained(因为weak声明的对象释放后会设置为nil,因此它用来修饰可选类型)。

import Foundation
 
class Person {
    var name:String
    var idCard:IDCard
     
    init(name:String,idCard:IDCard){
        self.name = name
        self.idCard = idCard
        idCard.owner = self
    }
     
    deinit{
        println("Person deinit...")
    }
}
 
class IDCard {
    var no:String
    //声明为弱引用
    weak var owner:Person?
     
    init(no:String){
        self.no = no
    }
     
    deinit{
        println("IDCard deinit...")
    }
}
 
func test(){
    var idCard = IDCard(no:"100188888888888888")
    var p = Person(name: "Kenshin Cui",idCard:idCard)
}
 
//注意test执行完之后p会被释放,其后idCard跟着被释放
test()
 
println("wait...")

当然类似于上面的引用关系实际遇到的并不多,更多的还是存在于闭包之中(ObjC中多出现于Block中),因为闭包会持有其内部引用的元素。下面简单修改一下上面的例子,给Person添加一个闭包属性,并且在其中访问self,这样闭包自身就和Person类之间形成循环引用。

import Foundation
 
class Person {
    let name:String
     
    //使用闭包捕获列表解决循环引用
    lazy var description:()->NSString = {
        [unowned self] in
        return "name = \(self.name)"
    }
     
    init(name:String){
        self.name = name
    }
     
    deinit{
        println("Person deinit...")
    }
}
 
func test(){
    var p = Person(name: "Kenshin Cui")
    println(p.description())
}
 
test()
 
println("wait...")
/**打印结果
name = Kenshin Cui
Person deinit...
wait...
 */

指针与内存

除了循环引用问题,Swift之所以将指针类型标识为“unsafe”是因为指针没办法像其他类型一样进行自动内存管理,因此有必要了解一下指针和内存的关系。在Swift中初始化一个指针必须通过alloc和initialize两步,而回收一个指针需要调用destroy和dealloc(通常dealloc之后还会将指针设置为nil)。

import Foundation
 
class Person {
    var name:String
 
    init(name:String){
        self.name = name
    }
 
    deinit{
        println("Person\(name) deinit...")
    }
}
 
func test(){
    var p = Person(name: "Kenshin Cui")
     
    //虽然可以使用&p作为参数进行inout参数传递,但是无法直接获取其地址,下面的做法是错误的
    //var address = &p
     
    /*创建一个指向Person的指针pointer*/
    //申请内存(alloc参数代表申请n个Person类型的内存)
    var pointer:UnsafeMutablePointer = UnsafeMutablePointer.alloc(1)
    //初始化
    pointer.initialize(p)
     
    //获取指针指向的对象
    var p2 = pointer.memory
    println(p===p2) //结果:true,因为p和p2指向同一个对象
    //修改对象的值
    p2.name = "Kaoru"
    println(p.name) //结果:Kaoru
     
     
    //销毁指针
    pointer.destroy()
    //释放内存
    pointer.dealloc(1)
    //指向空地址
    pointer = nil
}
 
test()
 
println("waiting...")
/**打印结果
 
Kaoru
PersonKaoru deinit...
waiting...
 
*/

运行程序可以看到p对象在函数执行结束之后被销毁,但是如果仅仅将pointer设置为nil是无法销毁Person对象的,这很类似于之前的MRC内存管理,在Swift中使用指针需要注意:谁创建(alloc,malloc,calloc)谁释放。 当然上面演示中显然对于指针的操作略显麻烦,如果需要对一个变量进行指针操作可以借助于Swift中提供的一个方法withUnsafePointer。例如想要利用指针修改Person的name就可以采用下面的方式:

var p = Person(name: "Kenshin Cui")
 
var p2 = withUnsafeMutablePointer(&p, {
    (pointer:UnsafeMutablePointer) -> Person in
    pointer.memory.name = "Kaoru"
    return pointer.memory
})
 
println(p.name) //结果:Kaoru

在前面的C语言系列文章中有一部分内容用于介绍如何利用指针遍历一个数组,当然在Swift中仍然可以采用这种方式,但是在Swift中如果想要使用指针操作数组中每个元素的话通常借助于另一个类型UnsafeMutableBufferPointer。这个类表示一段连续内存,通常用于表示数组或字典的指针类型。

import Foundation
 
var array:[String] = ["Kenshin","Kaorsu","Tom"]
 
//UnsafeBufferPointer和UnsafeMutableBufferPointer用于表示一段连续内存的指针,例如:数组或字典
//下面创建一个指向数组的指针
var pointer = UnsafeMutableBufferPointer(start: &array, count: 3)
 
//baseAddress属性表示内存首地址
var baseAddress = pointer.baseAddress as UnsafeMutablePointer
println(baseAddress.memory) //结果:Kenshin
 
//利用指针遍历数组
for index in 1...pointer.count {
    println(baseAddress.memory)
    //向后移动指针,向前移动使用baseAddress.predecessor()
    baseAddress = baseAddress.successor()
}
/**打印结果
Kenshin
Kaorsu
Tom
*/

Core Foundation

Core Foundation作为iOS开发中最重要的框架之一,在iOS开发中有着重要的地位,但是它是一组C语言接口,在使用时需要开发人员自己管理内存。在Swift中使用Core Foundation框架(包括其他Core开头的框架)需要区分这个API返回的对象是否进行了标注:

1.如果已经标注则在使用时完全不用考虑内存管理(它可以自动管理内存)。

2.如果没有标注则编译器不会进行内存管理托管,此时需要将这个非托管对象转化为托管对象(当然你也可以使用retain()、release()或者autorelease()手动管理内存,但是不推荐这么做)。当然,苹果开发工具组会尽可能的标注这些API以实现C代码和Swift的自动桥接,但是在此之前未标注的API会返回Unmanaged<Type>结构,可以调用takeUnretainedValue()和takeRetainedValue()方法将其转化为可以自动进行内存管理的托管对象(具体是调用前者还是后者,需要根据是否需要开发者自己进行内存管理而定,其本质是使用takeRetainedValue()方法,在对象使用完之后会调用一次release()方法。按照Core Foundation的命名标准,通常如果函数名中含“Create”、“Copy”、“Retain”关键字需要调用takeRetainedValue()方法来转化成托管对象)。

当然,上述两种方式均是针对系统框架而言,如果是开发者编写的类或者第三方类库,应该尽可能按照Cocoa规范命名并且在合适的地方使用CF_RETURNS_RETAINED和CF_RETURNS_NOT_RETAINED来进行标注以便可以进行自动内存管理

上一篇下一篇

猜你喜欢

热点阅读