Swift 进阶
2015-11-07 11:42
393 查看
iOS开发系列--Swift进阶
2015-09-21 00:01 by KenshinCui, 3072 阅读, 12 评论, 收藏, 编辑概述
上一篇文章《iOS开发系列--Swift语言》中对Swift的语法特点以及它和C、ObjC等其他语言的用法区别进行了介绍。当然,这只是Swift的入门基础,但是仅仅了解这些对于使用Swift进行iOS开发还是不够的。在这篇文章中将继续介绍一些Swift开发中一些不常关注但是又必备的知识点,以便对Swift有进一步的了解。访问控制
Swift命名空间
Swift和ObjC互相调用
Swift和ObjC映射关系
Swift调用ObjC
ObjC调用Swift
扩展—Swift调用C
反射
扩展—KVO
内存管理
循环引用
指针与内存
扩展—Core Foundation
目 录
访问控制
和其他高级语言一样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的扩展),而协议的访问级别不可以明确声明;
元组的访问级别是元组中各个元素的最低访问级别,注意:元组的访问级别是自动推导的,无法直接使用以上三个关键字修饰其访问级别;
函数的访问级是函数的参数、返回值的最低级别,并且如果其访问级别和默认访问级别(internal)不符需要明确声明;
枚举成员的访问级别等同于枚举的访问级别(无法单独设置),同时枚举的原始值、关联值的访问级别不能低于枚举的访问级别;
泛型类型或泛型函数的访问级别是泛型类型、泛型函数、泛型类型参数三者中最低的一个;
类型别名的访问级别不能高于原类型的访问级别;
上面这些规则看上去比较繁琐,但其实很多内容理解起来也是顺理成章的(如果你是一个语言设计者相信大部分规则也会这么设计),下面通过一个例子对于规则3做一解释,这一点和其他语言有所不同但是却更加实用。在使用ObjC开发时大家通常会有这样的经验:在一个类中希望某个属性对外界是只读的,但是自己又需要在类中对属性进行写操作,此时只能直接访问属性对应的成员变量,而不能直接访问属性进行设置。但是Swift为了让语法尽可能精简,并没有成员变量的概念,此时就可以通过访问控制来实现。
Person.swift
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
Network.HttpRequest.swift
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 |
相反,如果在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都是通过头文件暴漏对应接口的,下图说明了这种交互方式:![](http://images2015.cnblogs.com/blog/62046/201509/62046-20150921000042914-1025562533.png)
不难发现,要在Swift中调用ObjC必须借助于一个桥接头文件,在这个头文件中将ObjC接口暴漏给Swift。例如你可以创建一个“xx.h”头文件,然后使用“#import”导入需要在Swift中使用的ObjC类,同时在Build Settings的“Objective-C Bridging Header”中配置桥接文件“xx.h”。但是好在这个过程Xcode可以帮助你完成,你只需要在Swift项目中添加ObjC文件,Xcode就会询问你是否创建桥接文件,你只需要点击“Yes”就可以帮你完成上面的操作:
![](http://images2015.cnblogs.com/blog/62046/201509/62046-20150921000044586-333657296.png)
为了演示Swift中调用ObjC的简洁性, 下面创建一个基于Swift的Single View Application类型的项目,现在有一个基于ObjC的“KCLoadingView”类,它可以在网络忙时显示一个加载动画。整个类的实现很简单,就是通过一个基础动画实现一个图片的旋转。
KCLoadingView.h
ObjCBridge-Bridging-Header.h
ViewController.swfit
![](http://images2015.cnblogs.com/blog/62046/201509/62046-20150921000046304-389176500.gif)
ObjC调用Swift
从前面的Swift和ObjC之间的交互图示可以看到ObjC调用Swift是通过Swift生成的一个头文件实现的,好在这个头文件是由编译器自动完成的,开发者不需要关注,只需要记得他的格式即可“项目名称-Swift.h”。如果在ObjC项目中使用了Swift,只要在ObjC的“.m”文件中导入这个头文件就可以直接调用Swift,注意这个生成的文件并不在项目中,它在项目构建的一个文件夹中(可以按住Command点击头文件查看)。同样通过前面的例子演示如何在ObjC中调用Swift,新建一个基于ObjC的项目(项目名称“UseSwiftInObjC”),并且这次加载动画控件使用Swift编写。LoadingView.swift
ViewController.m
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> | 对于类类型 |
函数指针 | 闭包 |
可用类型 | 最终转化类型 |
---|---|
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 | 元素地址 |
string.h
当然,上面这种方式适合所有在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
更多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协议的定义(为了方便大家理解,添加了相关注释说明):
扩展--KVO
和KVC一样,在Swift中使用KVO也仅限于NSObject及其子类,因为KVO本身就是基于KVC进行动态派发的,这些都属于运行时的范畴。Swift要实现这些动态特性需要在类型或者成员前面加上@objc(继承于NSObject的子类及非私有成员会自动添加),但并不是说加了@objc就可以动态派发,因为Swift为了性能考虑会优化为静态调用。如果确实需要使用这些特性Swift提供了dynamic关键字来修饰,例如这里要想使用KVO除了继承于NSObject之外就必须给监控的属性加上dynamic关键字修饰。下面的演示中说明了这一点:注意:对于系统类(或一些第三方框架)由于无法修改其源代码如果要进行KVO监听,可以先继承此类然后进行使用dynamic重写;此外,并非只有KVO需要加上dynamic关键字,对于很多动态特性都是如此,例如要在Swift中实现Swizzle方法替换,方法前仍然要加上dynamic,因为方法的替换也需要动态派发。
内存管理
循环引用
Swift使用ARC来自动管理内存,大多数情况下开发人员不需要手动管理内存,但在使用ObjC开发时,大家都会遇到循环引用的问题,在Swift中也不可避免。 举例来说,人员有一个身份证(Person有idCard属性),而身份证就有一个拥有者(IDCard有owner属性),那么对于一个Person对象一旦建立了这种关系之后就会和IDCard对象相互引用而无法被正确的释放。例如下面的代码在执行完test之后p和idCard两个对象均不会被释放:
![](http://images2015.cnblogs.com/blog/62046/201509/62046-20150921000039648-1562374080.png)
为了避免这个问题Swift采用了和ObjC中同样的概念:弱引用,通常将被动的一方的引用设置为弱引用来解决循环引用问题。例如这里可以将IDCard中的owner设置为弱引用。因为IDCard对于Person的引用变成了弱引用,而Person持有IDCard的强引用,这样一来Person作为主动方,只要它被释放后IDCard也会跟着释放。如要声明弱引用可以使用weak和unowned关键字,前者用于可选类型后者用于非可选类型,相当于ObjC中的__weak和__unsafe_unretained(因为weak声明的对象释放后会设置为nil,因此它用来修饰可选类型)。
![](http://images2015.cnblogs.com/blog/62046/201509/62046-20150921000041195-1647330934.png)
当然类似于上面的引用关系实际遇到的并不多,更多的还是存在于闭包之中(ObjC中多出现于Block中),因为闭包会持有其内部引用的元素。下面简单修改一下上面的例子,给Person添加一个闭包属性,并且在其中访问self,这样闭包自身就和Person类之间形成循环引用。
指针与内存
除了循环引用问题,Swift之所以将指针类型标识为“unsafe”是因为指针没办法像其他类型一样进行自动内存管理,因此有必要了解一下指针和内存的关系。在Swift中初始化一个指针必须通过alloc和initialize两步,而回收一个指针需要调用destroy和dealloc(通常dealloc之后还会将指针设置为nil)。扩展—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来进行标注以便可以进行自动内存管理。
备注:
1.在Swift中内存的申请除了使用alloc其实也可以使用malloc或者calloc,此时释放时使用free函数;
2.关于更多内存管理的内容可以参见前面的文章:iOS开发系列http://www.cnblogs.com/kenshincui/p/3870325.html
相关文章推荐
- oc swift 混编
- 【Swift】SpriteKit开发iOS游戏——场景切换
- Swift,函数,枚举
- swift - 闭包
- iOS_Swift初识之使用三种回调方式自定义Button
- swift学习Day04
- 开始swift学习之路
- Swift -- 属性
- swift 2.0 语法 函数
- swift 2.0 语法 字符串
- swift 2.0 语法 字典
- swift 2.0 语法 数组
- swift 2.0 语法 循环
- swift 2.0 语法 可选类型
- swift 2.0 语法 分支
- swift 2.0语法 元组
- swift 2.0 语法 常量变量
- Swift的继承和结构体的初始化
- 苹果新贵 Swift 之前世今生
- Swift中获取字符串子串