IOSiOS学习笔记iOS程序猿

Swift进阶

2018-05-07  本文已影响296人  直男程序员

从16年初正式切入swift, 到现在使用了两年多了,大大小小的项目也做了十几个,基础知识感觉掌握的差不多,是时候对进阶内容做一个了解和深入学习了,在这里经过我查找和自己的总结,特写一篇Swift进阶的文章,来和大家一块学习下Swift更深入的知识.

Swift

1.访问级别

和其他高级语言一样, Swift中也增加了访问控制,在Swift中提供了五种访问级别, 级别从低到高依次为: privatefilePrivateinternalpublicopen,但是不同的是Swift中的访问级别是基于模块(module,或者target)和源文件(.swift文件)的,而不是基于类型、命名空间声明。

注: Xcode中的每个构建目标(Target)可以当做是一个模块(Module),这个构建目标可以是一个Application,也可以是一个通用的Framework(更多的时候是一个Application)。

成员访问级别约定规则
这里详细介绍下Swift关于不同成员访问级别的约定规则:
  1. 如果一个的访问级别是private那么该类的所有成员都是private(此时成员无法修改访问级别),如果一个类的访问级别是internal或者public那么它的所有成员都是internal(如果类的访问级别是public,成员默认internal,此时可以单独修改成员的访问级别),类成员的访问级别不能高于类的访问级别(注意:嵌套类型的访问级别也符合此条规则);

  2. 常量、变量、属性、下标脚本访问级别低于其所声明的类型级别,并且如果不是默认访问级别(internal)要明确声明访问级别(例如一个常量是一个private类型的类类型,那么此常量必须声明为private);

  3. 在不违反1、2两条规则的情况下,setter的访问级别可以低于getter的访问级别(例如一个属性访问级别是internal,那么可以添加private(set)修饰将setter权限设置为private,在当前模块中只有此源文件可以访问,对外部是只读的);

  4. 必要构造方法(required修饰)的访问级别必须和类访问级别相同,结构体的默认逐一构造函数的访问级别不高于其成员的访问级别(例如一个成员是private那么这个构造函数就是private,但是可以通过自定义来声明一个public的构造函数),其他方法(包括其他构造方法和普通方法)的访问级别遵循规则1;

  5. 子类的访问级别不高于父类的访问级别,但是在遵循三种访问级别作用范围的前提下子类可以将父类低访问级别的成员重写成更高的访问级别(例如父类A和子类B在同一个源文件,A的访问级别是public,B的访问级别是internal,其中A有一个private方法,那么B可以覆盖其private方法并重写为internal);

  6. 协议中所有必须实现的成员的访问级别和协议本身的访问级别相同,其子协议的访问级别不高于父协议

  7. 如果一个类继承于另一个类的同时实现了某个协议那么这个类的访问级别为父类和协议的最低访问级别,并且此类中方法访问级别和所实现的协议中的方法相同;

  8. 扩展的成员访问级别遵循规则1,但是对于类、结构体、枚举的扩展可以明确声明访问级别并且可以更低(例如对于internal的类,你可以声明一个private的扩展),而协议的访问级别不可以明确声明;

  9. 元组的访问级别是元组中各个元素的最低访问级别,注意:元组的访问级别是自动推导的,无法直接使用以上三个关键字修饰其访问级别;

  10. 函数的访问级是函数的参数、返回值的最低级别,并且如果其访问级别和默认访问级别(internal)不符需要明确声明

  11. 枚举成员的访问级别等同于枚举的访问级别(无法单独设置),同时枚举的原始值、关联值的访问级别不能低于枚举的访问级别;

  12. 泛型类型或泛型函数的访问级别是泛型类型、泛型函数、泛型类型参数三者中最低的一个;

  13. 类型别名的访问级别不能高于原类型的访问级别;

上面这些规则看上去比较繁琐,但其实很多内容理解起来也是顺理成章的(如果你是一个语言设计者相信大部分规则也会这么设计),下面通过一个例子对于规则3做一解释,这一点和其他语言有所不同但是却更加实用。在使用ObjC开发时大家通常会有这样的经验:在一个类中希望某个属性对外界是只读的,但是自己又需要在类中对属性进行写操作,此时只能直接访问属性对应的成员变量,而不能直接访问属性进行设置。但是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)")
    }
}

这时候在别处使用,name属性就是只读的,不能设置.

import Foundation
 
var p =  Person(name:"Kenshin")
//此时不能设置name属性,但是可读
//p.name = "Kaoru"
println("name=\(p.name)")
p.showMessage() 

2.命名空间

熟悉ObjC的朋友都知道ObjC没有命名空间,为了避免类名重复苹果官方推荐使用类名前缀,这种做法从一定程度上避免了大部分问题,但是当你在项目中引入一个第三方库而这个第三方库引用了一个和你当前项目中用到的同一个库时就会出现问题。因为静态库最终会编译到同一个域,最终导致编译出错。

作为一个现代化语言Swift解决了这个问题,实现了命名空间功能,只是这个命名空间不像C#的namespace或者Java中的package那样需要显式在文件中指定,而是采用模块(Module)的概念:在同一个模块中所有的Swift类处于同一个命名空间,它们之间不需要导入就可以相互访问。很明显Swift的这种做法是为了最大限度的简化Swift编程。其实一个module就可以看成是一个project中的一个target,在创建项目的时候默认就会创建一个target,这个target的默认模块名称就是这个项目的名称(可以在target的Build Settings—Product Module Name配置)。

3. Swift和ObjC互相调用

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

Swift和ObjC映射关系:
Swift ObjC 映射关系
AnyObject id 由于ObjC中的对象可能为nil,所以Swift中如果用到ObjC中类型的参数会标记为对应的可选类型
Array、Dictionary、Set NSArray、NSDictionary、NSSet ObjC中的数组和字典不能存储基本数据类型,只能存储对象类型,这样一来对于Swift中的Int、UInt、Float、Double、Bool转化时会自动桥接成NSNumber
Int NSInteger、NSUInteger swift中细分为Int8 Int32 Int64等, 使用Int则根据系统自行判断对应位数
NSObjectProtocol NSObject协议(注意不是NSObject类) 由于Swift在继承或者实现时没有类的命名空间的概念,而ObjC中既有NSObject类又有NSObject协议,所以在Swift中将NSObject协议对应成了NSObjectProtocol
CGContext CGContextRef Core Foundation中其他情况均是如此,由于Swift本身就是引用类型,在Swift不需要再加上“Ref”
ErrorType NSError
#selector(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(闭包) Closure(闭包) 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标记)。

当前ObjC已经积累了大量的第三方库,相信在Swift发展的前期调用已经存在的ObjC是比较常见的。在Swift和ObjC的兼容性允许你在一个项目中使用两种语言混合编程,而不管这个项目原本是基于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”就可以帮你完成上面的操作

桥接头文件

ObjC调用Swift是通过Swift生成的一个头文件实现的,好在这个头文件是由编译器自动完成的,开发者不需要关注,只需要记得他的格式即可“项目名称-Swift.h”。如果在ObjC项目中使用了Swift,只要在ObjC的“.m”文件中导入这个头文件就可以直接调用Swift,注意这个生成的文件并不在项目中,它在项目构建的一个文件夹中(可以按住Command点击头文件查看)

4. 反射

熟悉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,因为方法的替换也需要动态派发。

5. 内存管理

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也会跟着释放。如要声明弱引用可以使用weakunowned关键字,前者用于可选类型后者用于非可选类型,相当于ObjC中的__weak和__unsafe_unretained(因为weak声明的对象释放后会设置为nil,因此它用来修饰可选类型, unowned无主引用,如果对象为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
扩展—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来进行标注以便可以进行自动内存管理。

上述就是几点Swift进阶的知识,感觉文章还不错的话麻烦点赞关注下本作者哈, 本作者会持续更新更多的iOS文章.

本文部分内容转载自崔江涛技术博客.

上一篇下一篇

猜你喜欢

热点阅读