從16年初正式切入swift, 到現在使用了兩年多了,大大小小的項目也做了十幾個,基礎知識感覺掌握的差不多,是時候對進階內容做一個了解和深入學習了,在這里經過我查找和自己的總結,特寫一篇Swift進階的文章,來和大家一塊學習下Swift更深入的知識.
1.訪問級別
和其他高級語言一樣, Swift中也增加了訪問控制,在Swift中提供了五種訪問級別, 級別從低到高依次為: private、filePrivate、internal、public、open,但是不同的是Swift中的訪問級別是基于模塊(module,或者target)和源文件(.swift文件)的,而不是基于類型、命名空間聲明。
注: Xcode中的每個構建目標(Target)可以當做是一個模塊(Module),這個構建目標可以是一個Application,也可以是一個通用的Framework(更多的時候是一個Application)。
- private: 只能訪問當前源文件中的實體,切不能訪問擴展中對象(注意Swift中的private和其他語言不太一樣,它是基于源文件的,作用范圍是整個源文件,如果一個源文件中有兩個類,那么一個類可以訪問另外一個類的私有成員).
- filePrivate: 可訪問當前源文件中實體,包括擴展.
- internal: 可以訪問當前模塊中的其他任何實體,但是在模塊外無法訪問,這是所有實體的默認訪問級別(通常在一個單目標Application中不需要自行設置訪問級別).
- public: 可以訪問當前模塊及其他模塊中的任何實體(通常用于Framework).
- open: 可以被任何人使用,包括override和繼承.
成員訪問級別約定規則
這里詳細介紹下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方法,那么B可以覆蓋其private方法并重寫為internal);
協議中所有必須實現的成員的訪問級別和協議本身的訪問級別相同,其子協議的訪問級別不高于父協議;
如果一個類繼承于另一個類的同時實現了某個協議那么這個類的訪問級別為父類和協議的最低訪問級別,并且此類中方法訪問級別和所實現的協議中的方法相同;
擴展的成員訪問級別遵循規則1,但是對于類、結構體、枚舉的擴展可以明確聲明訪問級別并且可以更低(例如對于internal的類,你可以聲明一個private的擴展),而協議的訪問級別不可以明確聲明;
元組的訪問級別是元組中各個元素的最低訪問級別,注意:元組的訪問級別是自動推導的,無法直接使用以上三個關鍵字修飾其訪問級別;
函數的訪問級是函數的參數、返回值的最低級別,并且如果其訪問級別和默認訪問級別(internal)不符需要明確聲明;
枚舉成員的訪問級別等同于枚舉的訪問級別(無法單獨設置),同時枚舉的原始值、關聯值的訪問級別不能低于枚舉的訪問級別;
泛型類型或泛型函數的訪問級別是泛型類型、泛型函數、泛型類型參數三者中最低的一個;
類型別名的訪問級別不能高于原類型的訪問級別;
上面這些規則看上去比較繁瑣,但其實很多內容理解起來也是順理成章的(如果你是一個語言設計者相信大部分規則也會這么設計),下面通過一個例子對于規則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標記)。
-
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
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也會跟著釋放。如要聲明弱引用可以使用weak和unowned關鍵字,前者用于可選類型后者用于非可選類型,相當于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返回的對象是否進行了標注:
- 如果已經標注則在使用時完全不用考慮內存管理(它可以自動管理內存)。
- 如果沒有標注則編譯器不會進行內存管理托管,此時需要將這個非托管對象轉化為托管對象(當然你也可以使用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文章.
本文部分內容轉載自崔江濤技術博客.