上一篇文章《iOS開發系列--Swift語言》中對Swift的語法特點以及它和C、ObjC等其他語言的用法區別進行了介紹。當然,這只是Swift的入門基礎,但是僅僅了解這些對于使用Swift進行iOS開發還是不夠的。在這篇文章中將繼續介紹一些Swift開發中一些不常關注但是又必備的知識點,以便對Swift有進一步的了解。
和其他高級語言一樣Swift中也增加了訪問控制,在Swift中提供了PRivate、internal、public三種訪問級別,但是不同的是Swift中的訪問級別是基于模塊(module,或者target)和源文件(.swift文件)的,而不是基于類型、命名空間聲明。
下面是關于Swift關于不同成員訪問級別的約定規則:
?上面這些規則看上去比較繁瑣,但其實很多內容理解起來也是順理成章的(如果你是一個語言設計者相信大部分規則也會這么設計),下面通過一個例子對于規則3做一解釋,這一點和其他語言有所不同但是卻更加實用。在使用ObjC開發時大家通常會有這樣的經驗:在一個類中希望某個屬性對外界是只讀的,但是自己又需要在類中對屬性進行寫操作,此時只能直接訪問屬性對應的成員變量,而不能直接訪問屬性進行設置。但是Swift為了讓語法盡可能精簡,并沒有成員變量的概念,此時就可以通過訪問控制來實現。
Person.swift
import Foundationpublic 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 Foundationvar 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 Foundationpublic 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 Foundationclass 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 IOclass ViewController: UIViewController { let url = "http://www.companysz.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 Foundationstruct 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 Foundationstruct 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 Foundationlet url = "http://www.companysz.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的映射關系幫助大家理解:
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呢?此時就需要使用一個關鍵字“@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 UIKitclass 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 UIKitpublic 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類型 | 說明 |
---|---|---|
基本類型 | ? | ? |
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> | 對于類類型 |
函數指針 | 閉包 | ? |
?對于其他類型的映射關系都很容易理解,這里主要說一下指針的內容。通過上表可以看到在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)#includevoid 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 != '/0') { *pSource++ = *pAppend++; }}
UseCInSwift-Bridging-Header.h
#import "string.h"
然后在Swift中調用上面的C函數
import Foundationvar sourceStr:String = "Hello"var appendStr:String = ",World!"var sourceCStr = (sourceStr as NSString).UTF8Stringvar 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 ) -> Voidvar sourceStr:String = "Hello"var appendStr:String = ",World!"var sourceCStr = (sourceStr as NSString).UTF8Stringvar 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 UIKitstruct 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 Foundationclass 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.0p.account = accountp.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 Foundationclass 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 Foundationclass 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 Foundationclass 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 Cuiwait...*/
Swift中使用閉包捕獲列表來解決閉包中的循環引用問題,這種方式有點類似于ObjC中的weakSelf方式,當時語法更加優雅, 具體實現如下:
import Foundationclass 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 CuiPerson deinit...wait... */
除了循環引用問題,Swift之所以將指針類型標識為“unsafe”是因為指針沒辦法像其他類型一樣進行自動內存管理,因此有必要了解一下指針和內存的關系。在Swift中初始化一個指針必須通過alloc和initialize兩步,而回收一個指針需要調用destroy和dealloc(通常dealloc之后還會將指針設置為nil)。
import Foundationclass 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...")/**打印結果KaoruPersonKaoru 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 Foundationvar array:[String] = ["Kenshin","Kaorsu","Tom"]//UnsafeBufferPointer和UnsafeMutableBufferPointer用于表示一段連續內存的指針,例如:數組或字典//下面創建一個指向數組的指針var pointer = UnsafeMutableBufferPointer(start: &array, count: 3)//baseAddress屬性表示內存首地址var baseAddress = pointer.baseAddress as UnsafeMutablePointerprintln(baseAddress.memory) //結果:Kenshin//利用指針遍歷數組for index in 1...pointer.count { println(baseAddress.memory) //向后移動指針,向前移動使用baseAddress.predecessor() baseAddress = baseAddress.successor()}/**打印結果KenshinKaorsuTom*/
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.companysz.com/kenshincui/p/3870325.html
新聞熱點
疑難解答