Swift和ObjC互相调用

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

Swift和ObjC映射关系

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

Swift ObjC 备注
AnyObject id(ObjC中的对象任意类型) 由于ObjC中的对象可能为nil,所以Swift中如果用到ObjC中类型的参数会标记为对应的可选类型
Array、Dictionary、Set NSArray、NSDictionary、NSSet 注意:ObjC中的数组和字典不能存储基本数据类型,只能存储对象类型,这样一来对于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调用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  ViewController UIViewController  {
     lazy  var  loadingView : KCLoadingView  = {
         var  size = UIScreen . mainScreen (). bounds . size
         var  lv  KCLoadingView ()
         lv . frame . size = CGSizeMake ( 37.0 37.0 )
         lv . center = CGPointMake ( size . width * 0.5 size . height * 0.5 )
         return  lv
     }()
     
     lazy  private  var  converView : UIView  = {
         var  cv  UIView ( frame UIScreen . mainScreen (). bounds )
         cv . backgroundColor  UIColor ( red 0.0 green 0.0 blue 0.0 alpha 0.5 )
         return  cv
     }()
     
     override  func  loadView () {
         //设置背景
         var  image  UIImage ( named "iOS9" )
         var  background  UIImageView ( frame UIScreen . 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 ( touches Set withEvent  event UIEvent ) {
         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 ( frame CGRectZero )
     }
     
     override  init ( frame CGRect ) {
         super . init ( frame frame )
         self . image  UIImage ( named "loading" )
     }
 
     required  public  init ( coder  aDecoder NSCoder ) {
         super . init ( coder aDecoder )
         self . image  UIImage ( named "loading" )
     }
     
     public  func  start () {
         self . layer . addAnimation ( self . rotationAnimation forKey basicAnimationKey )
     }
     
     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 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

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  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:”的函数。

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" 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关键字修饰。下面的演示中说明了这一点:

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 ( 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两个对象均不会被释放:

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  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)。

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 & 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来进行标注以便可以进行自动内存管理

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值