概述
上一篇文章《iOS开发系列--Swift语言》中对Swift的语法特点以及它和C、ObjC等其它语言的使用方法区别进行了介绍。当然。这仅仅是Swift的入门基础。可是仅仅了解这些对于使用Swift进行iOS开发还是不够的。在这篇文章中将继续介绍一些Swift开发中一些不常关注可是又必备的知识点,以便对Swift有进一步的了解。
訪问控制
和其它高级语言一样Swift中也添加了訪问控制。在Swift中提供了private、internal、public三种訪问级别。可是不同的是Swift中的訪问级别是基于模块(module,或者target)和源文件(.swift文件)的。而不是基于类型、命名空间声明。
- private:仅仅能訪问当前源文件里的实体(注意Swift中的private和其它语言不太一样,它是基于源文件的。作用范围是整个源文件,假设一个源文件里有两个类,那么一个类能够訪问另外一个类的私有成员)。
- internal:能够訪问当前模块中的其它不论什么实体,可是在模块外无法訪问,这是全部实体的默认訪问级别(通常在一个单目标Application中不须要自行设置訪问级别)。
- public:能够訪问当前模块及其它模块中的不论什么实体(通经常使用于Framework)。
以下是关于Swift关于不同成员訪问级别的约定规则:
- 假设一个类的訪问级别是private那么该类的全部成员都是private(此时成员无法改动訪问级别),假设一个类的訪问级别是internal或者public那么它的全部成员都是internal(假设类的訪问级别是public,成员默认internal,此时能够单独改动成员的訪问级别),类成员的訪问级别不能高于类的訪问级别(注意:嵌套类型的訪问级别也符合此条规则)。
- 常量、变量、属性、下标脚本訪问级别低于其所声明的类型级别,而且假设不是默认訪问级别(internal)要明白声明訪问级别(比如一个常量是一个private类型的类类型,那么此常量必须声明为private)。
- 在不违反1、2两条规则的情况下,setter的訪问级别能够低于getter的訪问级别(比如一个属性訪问级别是internal。那么能够加入private(set)修饰将setter权限设置为private,在当前模块中仅仅有此源文件能够訪问。对外部是仅仅读的);
- 必要构造方法(required修饰)的訪问级别必须和类訪问级别同样,结构体的默认逐一构造函数的訪问级别不高于其成员的訪问级别(比如一个成员是private那么这个构造函数就是private。可是能够通过自定义来声明一个public的构造函数),其它方法(包含其它构造方法和普通方法)的訪问级别遵循规则1;
- 子类的訪问级别不高于父类的訪问级别,可是在遵循三种訪问级别作用范围的前提下子类能够将父类低訪问级别的成员重写成更高的訪问级别(比如父类A和子类B在同一个源文件,A的訪问级别是public,B的訪问级别是internal,当中A有一个private方法。那么A能够覆盖其private方法并重写为internal)。
- 协议中全部必须实现的成员的訪问级别和协议本身的訪问级别同样。其子协议的訪问级别不高于父协议;
- 假设一个类继承于还有一个类的同一时候实现了某个协议那么这个类的訪问级别为父类和协议的最低訪问级别,而且此类中方法訪问级别和所实现的协议中的方法同样;
- 扩展的成员訪问级别遵循规则1,可是对于类、结构体、枚举的扩展能够明白声明訪问级别而且能够更低(比如对于internal的类,你能够声明一个private的扩展)。而协议的訪问级别不能够明白声明。
- 元组的訪问级别是元组中各个元素的最低訪问级别,注意:元组的訪问级别是自己主动推导的,无法直接使用以上三个keyword修饰其訪问级别;
- 函数的訪问级是函数的參数、返回值的最低级别。而且假设其訪问级别和默认訪问级别(internal)不符须要明白声明;
- 枚举成员的訪问级别等同于枚举的訪问级别(无法单独设置),同一时候枚举的原始值、关联值的訪问级别不能低于枚举的訪问级别;
- 泛型类型或泛型函数的訪问级别是泛型类型、泛型函数、泛型类型參数三者中最低的一个;
- 类型别名的訪问级别不能高于原类型的訪问级别;
上面这些规则看上去比較繁琐,但其实非常多内容理解起来也是顺理成章的(假设你是一个语言设计者相信大部分规则也会这么设计),以下通过一个样例对于规则3做一解释。这一点和其它语言有所不同可是却更加有用。在使用ObjC开发时大家一般会有这样的经验:在一个类中希望某个属性对外界是仅仅读的,可是自己又须要在类中对属性进行写操作,此时仅仅能直接訪问属性相应的成员变量。而不能直接訪问属性进行设置。
可是Swift为了让语法尽可能精简,并没有成员变量的概念。此时就能够通过訪问控制来实现。
Person.swift
import Foundation public class Person { //设置setter私有,可是getter为public public private(set) var name:String public init(name:String){ self.name = name } public func showMessage(){ println("name=(name)") } }
main.swift
import Foundation var p = Person(name:"Kenshin") //此时不能设置name属性。可是可读 //p.name = "Kaoru" println("name=(p.name)") p.showMessage()
Xcode中的每一个构建目标(Target)能够当做是一个模块(Module),这个构建目标能够是一个Application。也能够是一个通用的Framework(很多其它的时候是一个Application)。
Swift命名空间
熟悉ObjC的朋友都知道ObjC没有命名空间。为了避免类名反复苹果官方推荐使用类名前缀,这样的做法从一定程度上避免了大部分问题。可是当你在项目中引入一个第三方库而这个第三方库引用了一个和你当前项目中用到的同一个库时就会出现故障。由于静态库终于会编译到同一个域,终于导致编译出错。当然作为一个现代化语言Swift一定会解决问题,可是假设查看Swift的官方文档,里面关于Swift的命名空间并没有太多详细的说明。可是Swift的作者Chris Lattner在Twitter中回答了这个问题:
Namespacing is implicit in swift, all classes (etc) are implicitly scoped by the module (Xcode target) they are in. no class prefixes needed
Swift中是实现了命名空间功能的。仅仅是这个命名空间不像C#的namespace或者Java中的package那样须要显式在文件里指定,而是採用模块(Module)的概念:在同一个模块中全部的Swift类处于同一个命名空间,它们之间不须要导入就能够相互訪问。非常明显Swift的这样的做法是为了最大限度的简化Swift编程。
其实一个module就能够看成是一个project中的一个target,在创建项目的时候默认就会创建一个target,这个target的默认模块名称就是这个项目的名称(能够在target的Build Settings—Product Module Name配置)。
以下最好还是看一个命名空间的样例,创建一个Single View Application应用“NameSpaceDemo”。默认情况下模块名称为“NameSpaceDemo”,这里改动为“Network”。而且加入”HttpRequest.swift"。然后加入一个Cocoa Touch Framework类型的target并命名为“IO”,加入“File.swift”。然后在ViewController.swift中调用HttpRequest发送请求。将请求结果利用File类来保存起来。
File.swift
import Foundation public class File { public var path:String! public init(path:String) { self.path = path } public func write(content:String){ var error:NSError?content.writeToFile(path, atomically: true, encoding:NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } } public func read() ->String?{ var error:NSError? var content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } return content } }
HttpRequest.swift
import Foundation class HttpRequest { class func request(urlStr:String,complete:(responseText:String?)->()){ var url = NSURL(string: urlStr) let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in var str:String? if error == nil { str = NSString(data: data, encoding: NSUTF8StringEncoding) as? String } complete(responseText: str) } task.resume() } }
ViewController.swift
import UIKit //导入模块 import IO class ViewController: UIViewController { let url = "http://www.cnblogs.com/kenshincui" let filePath = "/Users/KenshinCui/Desktop/file.txt" override func viewDidLoad() { super.viewDidLoad() //加上命名空间Network调用,注意这里命名空间能够省略 Network.HttpRequest.request(url, complete: { (responseText) -> () in if let txt = responseText { //调用模块中的类和方法 var file = File(path: self.filePath) file.write(txt) // println(file.read()!) }else{ println("error...") } }) } }
能够看到首先同一个Module中的HttpRequest类能够加上命名空间调用(当然这里能够省略),另外对于不同Modle下的File类通过导入IO模块能够直接使用File类,可是这里须要注意訪问控制,能够看到File类及其成员均声明为了public訪问级别。 用模块进行命名空间划分的方式优点就是能够不用显式指定命名空间,然而这样的方式无法在同一个模块中再进行划分,只是这个问题能够使用Swift中的嵌套类型来解决。在以下的样例中仍然使用前面的两个类HttpRequest和File类来演示,不同的是两个类分别嵌套在两个结构体Network和IO之中。
Network.HttpRequest.swift
import Foundation struct Network { class HttpRequest { class func request(urlStr:String,complete:(responseText:String?)->()){ var url = NSURL(string: urlStr) let task = NSURLSession.sharedSession().dataTaskWithURL(url!) { (data, response, error) -> Void in var str:String? if error == nil { str = NSString(data: data, encoding: NSUTF8StringEncoding) as?
String } complete(responseText: str) } task.resume() } } }
IO.File.swift
import Foundation struct IO { class File { var path:String! init(path:String) { self.path = path } func write(content:String){ var error:NSError? content.writeToFile(path, atomically: true, encoding:NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } } func read() ->String?{ var error:NSError?
var content = String(contentsOfFile: path, encoding: NSUTF8StringEncoding, error: &error) if error != nil { println("write failure...") } return content } } }
main.swift
import Foundation let url = "http://www.cnblogs.com/kenshincui" let filePath = "/Users/KenshinCui/Desktop/file.txt" Network.HttpRequest.request(url, complete: { (responseText) -> () in if let txt = responseText { var file = IO.File(path: filePath) file.write(txt) //println(file.read()!) }else{ println("error...") } }) sleep(30) //延迟30s避免命令行程序执行完进程结束,等待网络请求
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呢?此时就须要使用一个keyword“@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都是通过头文件暴漏相应接口的。下图说明了这样的交互方式:
不难发现。要在Swift中调用ObjC必须借助于一个桥接头文件。在这个头文件里将ObjC接口暴漏给Swift。比如你能够创建一个“xx.h”头文件,然后使用“#import”导入须要在Swift中使用的ObjC类,同一时候在Build Settings的“Objective-C Bridging Header”中配置桥接文件“xx.h”。
可是好在这个过程Xcode能够帮助你完毕,你仅仅须要在Swift项目中加入ObjC文件,Xcode就会询问你是否创建桥接文件,你仅仅须要点击“Yes”就能够帮你完毕上面的操作:
为了演示Swift中调用ObjC的简洁性, 以下创建一个基于Swift的Single View Application类型的项目,如今有一个基于ObjC的“KCLoadingView”类,它能够在网络忙时显示一个载入动画。
整个类的实现非常简单,就是通过一个基础动画实现一个图片的旋转。
KCLoadingView.h
#import <UIKit/UIKit.h> /** * 载入视图,显示载入效果 */ @interface KCLoadingView : UIImageView /** * 启动,開始旋转 */ - (void)start; /** * 停止 */ - (void)stop; @end
KCLoadingView.m
#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
#import "KCLoadingView.h"
接下来就能够调用这个类完毕一个载入动画,调用关系全然顺其自然,开发人员根本感觉不到这是在调用一个ObjC类。
ViewController.swfit
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() } }
执行效果
ObjC调用Swift
从前面的Swift和ObjC之间的交互图示能够看到ObjC调用Swift是通过Swift生成的一个头文件实现的,好在这个头文件是由编译器自己主动完毕的,开发人员不须要关注,仅仅须要记得他的格式就可以“项目名称-Swift.h”。假设在ObjC项目中使用了Swift。仅仅要在ObjC的“.m”文件里导入这个头文件就能够直接调用Swift,注意这个生成的文件并不在项目中,它在项目构建的一个目录中(能够按住Command点击头文件查看)。同样通过前面的样例演示怎样在ObjC中调用Swift,新建一个基于ObjC的项目(项目名称“UseSwiftInObjC”),而且这次载入动画控件使用Swift编写。
LoadingView.swift
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
#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
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时都是相似。非常easy上手。
比如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> | 对于类类型 |
函数指针 | 闭包 |
对于其它类型的映射关系都非常easy理解,这里主要说一下指针的内容。通过上表能够看到在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 != ' ') { *pSource++ = *pAppend++; } }
UseCInSwift-Bridging-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!
能够看到“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
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协议的定义(为了方便大家理解,加入了相关凝视说明):
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提供了dynamickeyword来修饰。比如这里要想使用KVO除了继承于NSObject之外就必须给监控的属性加上dynamickeyword修饰。
以下的演示中说明了这一点:
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须要加上dynamickeyword,对于非常多动态特性都是如此。比如要在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和unownedkeyword,前者用于可选类型后者用于非可选类型。相当于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 //以下的默认闭包实现中使用了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方式,当时语法更加优雅。 详细实现例如以下:
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”keyword须要调用takeRetainedValue()方法来转化成托管对象)。
当然。上述两种方式均是针对系统框架而言,假设是开发人员编写的类或者第三方类库,应该尽可能依照Cocoa规范命名而且在合适的地方使用CF_RETURNS_RETAINED和CF_RETURNS_NOT_RETAINED来进行标注以便能够进行自己主动内存管理。
备注:
1.在Swift中内存的申请除了使用alloc其实也能够使用malloc或者calloc。此时释放时使用free函数。
2.关于很多其它内存管理的内容能够參见前面的文章:iOS开发系列http://www.cnblogs.com/kenshincui/p/3870325.html