上一篇文章《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的擴展),而協議的訪問級別不可以明確聲明;元組的訪問級別是元組中各個元素的最低訪問級別,注意:元組的訪問級別是自動推導的,無法直接使用以上三個關鍵字修飾其訪問級別;函數的訪問級是函數的參數、返回值的最低級別,並且如果其訪問級別和默認訪問級別(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)。
熟悉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沉重的歷史包袱,畢竟ObjC的歷史太過悠久,相比於很多現代化語言它缺少一些很酷的語法特性,而且ObjC的語法和其他語言相比差別很大。但是Apple同時也不能忽視ObjC的地位,畢竟ObjC經過二十多年的歷史積累了大量的資源(開發者、框架、類庫等),因此在Swift推出的初期必須考慮兼容ObjC。但同時Swift和ObjC是基於兩種不同的方式來實現的(例如ObjC可以在運行時決定對象類型,但是Swift為了提高效率要求在編譯時就必須確定對象類型),所以要無縫兼容需要做大量的工作。而作為開發人員我們有必要了解兩種語言之間的轉化關系才能對Swift有更深刻的理解。
其實從前面的例子中大家不難發現Swift和ObjC必然存在著一定的映射關系,例如對於文件的操作使用了字符串的writeToFile方法,在網絡請求時使用的NSURLSession,雖然調用方式不同但是其參數完全和做ObjC開發時調用方式一致。原因就是Swift編譯器自動做了映射,下面列舉了部分Swift和ObjC的映射關系幫助大家理解:
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中要修改外部變量必須聲明為__blockSwift兼容大部分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的兼容性允許你在一個項目中使用兩種語言混合編程(稱為“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() } }
運行效果
從前面的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中根本不會生成對應的方法聲明。
由於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中使用時會有對應的類型,但是如果一個參數為某種指針類型,實際調用時應該使用何種Swift數據類型的數據作為參數調用呢?例如參數為UnsafePointer<Type>,是否只能傳入UnsafePointer<Type>呢,其實也可以傳入nil,並且最終調用時將會轉化為null指針來調用。下表列出了這種參數調用對應關系:
下面不妨看一下如何在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。當然,這僅限於類,對應結構體無能為力。
和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,因為方法的替換也需要動態派發。
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,因此它用來修飾可選類型)。
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作為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