Swift和ObjC互相调用

来源:互联网 发布:playclub脸部数据 编辑:程序博客网 时间:2024/05/15 07:00

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

Swift和ObjC映射关系

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

SwiftObjC备注AnyObjectid(ObjC中的对象任意类型)由于ObjC中的对象可能为nil,所以Swift中如果用到ObjC中类型的参数会标记为对应的可选类型Array、Dictionary、SetNSArray、NSDictionary、NSSet注意:ObjC中的数组和字典不能存储基本数据类型,只能存储对象类型,这样一来对于Swift中的Int、UInt、Float、Double、Bool转化时会自动桥接成NSNumberIntNSInteger、NSUInteger其他基本类型情况类似,不再一一列举NSObjectProtocolNSObject协议(注意不是NSObject类)由于Swift在继承或者实现时没有类的命名空间的概念,而ObjC中既有NSObject类又有NSObject协议,所以在Swift中将NSObject协议对应成了NSObjectProtocolCGContextCGContextRef

Core Foundation中其他情况均是如此,由于Swift本身就是引用类型,在Swift不需要再加上“Ref”

ErrorTypeNSError “ab:"@selector(ab:)

Swift可以自动将字符串转化成成selector

@NSCopyingcopy属性 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中存在的方法进行extensionClosure(闭包)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调用ObjC 

当前ObjC已经积累了大量的第三方库,相信在Swift发展的前期调用已经存在的ObjC是比较常见的。在Swift和ObjC的兼容性允许你在一个项目中使用两种语言混合编程(称为“mix and match”),而不管这个项目原本是基于Swift的还是ObjC的。无论是Swift中调用ObjC还是ObjC中调用Swift都是通过头文件暴漏对应接口的,下图说明了这种交互方式:

SwiftInteractObjC

不难发现,要在Swift中调用ObjC必须借助于一个桥接头文件,在这个头文件中将ObjC接口暴漏给Swift。例如你可以创建一个“xx.h”头文件,然后使用“#import”导入需要在Swift中使用的ObjC类,同时在Build Settings的“Objective-C Bridging Header”中配置桥接文件“xx.h”。但是好在这个过程Xcode可以帮助你完成,你只需要在Swift项目中添加ObjC文件,Xcode就会询问你是否创建桥接文件,你只需要点击“Yes”就可以帮你完成上面的操作:

CreateObjCBridgingHeaderTip

为了演示Swift中调用ObjC的简洁性, 下面创建一个基于Swift的Single View Application类型的项目,现在有一个基于ObjC的“KCLoadingView”类,它可以在网络忙时显示一个加载动画。整个类的实现很简单,就是通过一个基础动画实现一个图片的旋转。

KCLoadingView.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <UIKit/UIKit.h>
 
/**
 *  加载视图,显示加载效果
 */
@interface KCLoadingView UIImageView
 
/**
 *  启动,开始旋转
 */
- (void)start;
 
/**
 *  停止
 */
- (void)stop;
 
@end

KCLoadingView.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#import "KCLoadingView.h"
 
static NSString *const kAnimationKey = @"rotationAnimation";
@interface KCLoadingView ()
@property(strong, nonatomic) CABasicAnimation *rotationAnimation;
@end
 
@implementation KCLoadingView
#pragma mark - 生命周期及其基类方法
- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        [self setup];
    }
    return self;
}
 
#pragma mark - 公共方法
- (void)start {
    [self.layer addAnimation:self.rotationAnimation forKey:kAnimationKey];
}
 
- (void)stop {
    [self.layer removeAnimationForKey:kAnimationKey];
}
 
#pragma mark - 私有方法
- (void)setup {
    self.image = [UIImage imageNamed:@"loading"];
 
    CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    rotationAnimation.toValue = [NSNumber numberWithFloat:M_PI * 2.0];
    rotationAnimation.duration = 0.7;
    rotationAnimation.cumulative = YES;
    rotationAnimation.repeatCount = HUGE_VALF;
    self.rotationAnimation = rotationAnimation;
    [self.layer addAnimation:rotationAnimation forKey:kAnimationKey];
}
 
@end

当将这个类加入到项目时就会提示你是否创建一个桥接文件,在这个文件中导入上面的“KCLoadingView”类。现在这个文件只有一行代码

ObjCBridge-Bridging-Header.h

1
#import "KCLoadingView.h" 

接下来就可以调用这个类完成一个加载动画,调用关系完全顺其自然,开发者根本感觉不到这是在调用一个ObjC类。

ViewController.swfit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import UIKit
 
class ViewControllerUIViewController {
    lazy var loadingView:KCLoadingView = {
        var size=UIScreen.mainScreen().bounds.size
        var lv KCLoadingView()
        lv.frame.size=CGSizeMake(37.037.0)
        lv.center=CGPointMake(size.width*0.5size.height*0.5)
        return lv
    }()
     
    lazy private var converView:UIView = {
        var cv UIView(frameUIScreen.mainScreen().bounds)
        cv.backgroundColor UIColor(red0.0green0.0blue0.0alpha0.5)
        return cv
    }()
     
    override func loadView() {
        //设置背景
        var image UIImage(named"iOS9")
        var background UIImageView(frameUIScreen.mainScreen().bounds)
        background.userInteractionEnabled=true
        background.image=image
        self.view background
    }
     
    override func viewDidLoad() {
        super.viewDidLoad()
         
        //设置蒙层
        self.view.addSubview(self.converView)
         
        //添加加载控件
        self.view.addSubview(self.loadingView)
         
        loadingView.start()
    }
     
    override func touchesBegan(touchesSetwithEvent eventUIEvent) {
        loadingView.stop()
    }
}

运行效果

SwiftUseObjC 

ObjC调用Swift

从前面的Swift和ObjC之间的交互图示可以看到ObjC调用Swift是通过Swift生成的一个头文件实现的,好在这个头文件是由编译器自动完成的,开发者不需要关注,只需要记得他的格式即可“项目名称-Swift.h”。如果在ObjC项目中使用了Swift,只要在ObjC的“.m”文件中导入这个头文件就可以直接调用Swift,注意这个生成的文件并不在项目中,它在项目构建的一个文件夹中(可以按住Command点击头文件查看)。同样通过前面的例子演示如何在ObjC中调用Swift,新建一个基于ObjC的项目(项目名称“UseSwiftInObjC”),并且这次加载动画控件使用Swift编写。

LoadingView.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import UIKit
 
public class LoadingView:UIImageView {
    let basicAnimationKey "rotationAnimation"
    lazy var rotationAnimation:CABasicAnimation = {
        var animation CABasicAnimation(keyPath"transform.rotation.z")
        animation.toValue 2*M_PI
        animation.duration 0.7
        animation.cumulative true
        animation.repeatCount = .infinity
        return animation
    }()
     
    convenience init(){
        self.init(frameCGRectZero)
    }
     
    override init(frameCGRect) {
        super.init(frameframe)
        self.image UIImage(named"loading")
    }
 
    required public init(coder aDecoderNSCoder) {
        super.init(coderaDecoder)
        self.image UIImage(named"loading")
    }
     
    public func start() {
        self.layer.addAnimation(self.rotationAnimationforKeybasicAnimationKey)
    }
     
    public func stop() {
        self.layer.removeAnimationForKey(basicAnimationKey)
    }
}

 然后可以直接在ObjC代码中导入自动生成的文件“UseSwiftInObjC-Swift.h”并调用。

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#import "ViewController.h"
#import "UseSwiftInObjC-Swift.h"
 
@interface ViewController ()
@property (strong,nonatomic) UIView *converView;
@property (strong,nonatomic) LoadingView *loadingView;
@end
 
@implementation ViewController
-(void)loadView{
    UIImage *image = [UIImage imageNamed:@"iOS9"];
    UIImageView *background = [[UIImageView alloc]initWithImage:image];
    background.userInteractionEnabled = YES;
    self.view = background;
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
     
    [self.view addSubview:self.converView];
    [self.view addSubview:self.loadingView];
    [self.loadingView start];
}
 
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    [self.loadingView stop];
}
 
#pragma mark - 属性
/**
 *  遮罩层
 */
-(UIView *)converView{
    if (!_converView) {
        _converView = [[UIView alloc] initWithFrame:[UIScreen mainScreen].bounds];
        _converView.backgroundColor = [UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.5];
    }
    return _converView;
}
/**
 *  加载指示器
 */
-(LoadingView *)loadingView{
    if(!_loadingView){
        CGSize screenSize = [UIScreen mainScreen].bounds.size;
        CGFloat loadingViewWidth = 37.0;
        _loadingView=[[LoadingView alloc]init];
        _loadingView.frame=CGRectMake((screenSize.width-loadingViewWidth)*0.5, (screenSize.height - loadingViewWidth)*0.5, loadingViewWidth, loadingViewWidth);
    }
    return _loadingView;
}
 
@end

虽然生成的头文件并不会直接放到项目中,但是可以直接按着Command键查看生成的文件内容,当然这个文件比较长,里面使用了很多宏定义判断,这里只关心最主要部分。

UseSwiftInObjC-Swift.h

1
2
3
4
5
6
SWIFT_CLASS("_TtC14UseSwiftInObjC11LoadingView")
@interface LoadingView : UIImageView
- (SWIFT_NULLABILITY(nonnull) instancetype)initWithCoder:(NSCoder * __nonnull)aDecoder OBJC_DESIGNATED_INITIALIZER;
- (void)start;
- (void)stop;
@end

可以清晰的看到Swift确实进行了桥接,通过头文件将接口暴漏给了ObjC。但是注意前面说过的访问控制,如果类和方法在Swift中不声明为public,那么在ViewController.m中是无法调用的。事实上,如果方法不是public在UseSwiftInObjC-Swift.h中根本不会生成对应的方法声明。

扩展—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 charCChar类似的unsigned char对应CUnsignedCharintCInt类似的unsigned int对应CUnsignedIntshortCShort类似的unsigned short对应CUnsignedShortlongCLong类似的unsigned long对应CUnsignedLonglong longCLongLong类似的unsigned long long 对应 CUnsignedLongLongfloatCFloat doubleCDouble 构造体类型 注意:结构体实现枚举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则可以代表任何类型 nilnull  UnsafeMutablePointer<Type> UnsafeMutablePointer<Type>  &typeValue 元素地址 Type类型的数组的地址(&[typeValue1,typeValue2]) 数组地址 AutoreleasingUnsafeMutablePointer<Type>  nilnull  AutoreleasingUnsafeMutablePointer<Type> AutoreleasingUnsafeMutablePointer<Type> &typeValue 元素地址

下面不妨看一下如何在Swift中使用C语言,假设现在有一个用于字符串拼接的C库函数“stringAppend(char*,const char *)”,将其对应的文件导入到一个Swift项目中按照提示添加桥接头文件并在桥接头文件中引入对应的C文件。

string.h

1
2
3
4
5
6
7
8
#ifndef __UseCInSwift__Common__
#define __UseCInSwift__Common__void stringAppend(char *source, char *toAppend)
 
#include
 
void stringAppend(char *source,const char *toAppend);
 
#endif

string.c

1
2
3
4
5
6
7
8
9
10
#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-Bridging-Header.h

1
#import "string.h"

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

1
2
3
4
5
6
7
8
9
10
11
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!

 可以看到“char *”参数转化成了Swift中的UnsafeMutablePointer<Int8>,而将”const char *”转化成了UnsafePointer<Int8>。根据上面表格中的调用关系,如果参数为UnsafeMutablePointer<Type>可以传入nil、UnsafeMutablePointer<Type>或者元素地址,很明显这里需要使用UnsafeMutablePointer<Int8>;而如果参数为UnsafePointer<Type>并且Type为Int8或者Int则可以直接传入String类型的参数,因此也就有了上面的调用关系。

当然,上面这种方式适合所有在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语言函数相同):

 main.swift

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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!

更多Swift标准类库信息可以查看:https://github.com/andelf/Defines-Swift 

反射

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
protocol MirrorType {
     
    /// 被反射的成员,类似于一个实例做了as Any操作
    var valueAny get }
     
    /// 被反射成员的类型
    var valueTypeAny.Type get }
     
    /// 被反射成员的唯一标识
    var objectIdentifierObjectIdentifier? { get }
     
    /// 被反射成员的子成员数(例如结构体的成员个数,数组的元素个数等)
    var countInt get }
     
    //  取得被反射成员的字成员,返回值对应字成员的名称和值信息
    subscript (iInt) -> (StringMirrorType) { get }
     
    /// 对于反射成员的描述
    var summaryString get }
     
    /// 显示在Playground中的“值”信息
    var quickLookObjectQuickLookObject? { get }
     
    /// 被反射成员的类型的种类(例如:基本类型、结构体、枚举、类等)
    var dispositionMirrorDisposition get }
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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"age29)
//先查看一下对象描述信息,然后对照结果是否正确
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关键字修饰。下面的演示中说明了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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(selfforKeyPath"balance"options: .Oldcontextnil);
            }
        }
    }
     
    init(name:String){
        self.name name
        super.init()
    }
     
    override func observeValueForKeyPath(keyPathStringofObject objectAnyObjectchange: [NSObject AnyObject], contextUnsafeMutablePointer) {
        if keyPath == "balance" {
            var oldValue change[NSKeyValueChangeOldKeyasDouble
            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两个对象均不会被释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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...")

两个对象之间的引用关系如下图:

CircularRefrence

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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...")

现在两个对象之间的引用关系如下图:

WeakRefrence

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import Foundation
 
class Person {
    let name:String
     
    //下面的默认闭包实现中使用了self,会引起循环引用
    lazy var description:()->NSString = {
        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
wait...
*/

Swift中使用闭包捕获列表来解决闭包中的循环引用问题,这种方式有点类似于ObjC中的weakSelf方式,当时语法更加优雅, 具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Foundation
 
class Person {
    let name:String
     
    //使用闭包捕获列表解决循环引用
    lazy var description:()->NSString = {
        [unowned selfin
        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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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就可以采用下面的方式:

1
2
3
4
5
6
7
8
9
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。这个类表示一段连续内存,通常用于表示数组或字典的指针类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import Foundation
 
var array:[String] = ["Kenshin","Kaorsu","Tom"]
 
//UnsafeBufferPointer和UnsafeMutableBufferPointer用于表示一段连续内存的指针,例如:数组或字典
//下面创建一个指向数组的指针
var pointer UnsafeMutableBufferPointer(start&arraycount3)
 
//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来进行标注以便可以进行自动内存管理

0 0