第一:以“類族模式“隱藏實現細節
在iOS開發中,我們也會使用“類族”(class cluster)這一設計模式,通過“抽象基類”來實例化不同的實體子類。
舉個? :
- (UIButton *)buttonWithType:(UIButtonType)type;
在這裏,我們只需要輸入不同的按鈕類型(UIButtonType)就可以得到不同的UIButton的子類。在OC框架中普遍使用這一設計模式。
爲什麼要這麼做呢?
筆者認爲這麼做的原因是爲了“弱化”子類的具體類型,讓開發者無需關心創建出來的子類具體屬於哪個類。(這裏覺得還有點什麼,但是還沒有想到,歡迎補充!)
我們可以看一個具體的例子:
對於“員工”這個類,可以有各種不同的“子類型”:開發員工,設計員工和財政員工。這些“實體類”可以由“員工”這個抽象基類來獲得:
- 抽象基類
//EOCEmployee.h
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance, };
@interface EOCEmployee : NSObject
@property (copy) NSString *name; @property NSUInteger salary;
// Helper for creating Employee objects
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type;
// Make Employees do their respective day's work
- (void)doADaysWork;
@end
//EOCEmployee.m
@implementation EOCEmployee
+ (EOCEmployee*)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// 需要子類來實現
}
@end
我們可以看到,將EOCEmployee作爲抽象基類,這個抽象基類有一個初始化方法,通過這個方法,我們可以得到多種基於這個抽象基類的實體子類:
- 實體子類(concrete subclass):
@interface EOCEmployeeDeveloper : EOCEmployee
@end
@implementation EOCEmployeeDeveloper
- (void)doADaysWork {
[self writeCode];
}
@end
注意:
如果對象所屬的類位於某個類族中,那麼在查詢類型信息時就要小心。因爲類族中的實體子類並不與其基類屬於同一個類。
在既有類中使用關聯對象存放自定義數據
我們可以通“關聯對象”機制來把兩個對象連接起來。這樣我們就可以從某個對象中獲取相應的關聯對象的值。
先看一下關聯對象的語法:
- 爲某個對象設置關聯對象的值:
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
這裏,第一個參數是主對象,第二個參數是鍵,第三個參數是關聯的對象,第四個參數是存儲策略:是枚舉,定義了內存管理語義。
- 根據給定的鍵從某對象中獲取相應的關聯對象值:
id objc_getAssociatedObject(id object, void *key)
- 移除指定對象的關聯對象:
void objc_removeAssociatedObjects(id object)
舉個例子:
#import <objc/runtime.h>
static void *EOCMyAlertViewKey = "EOCMyAlertViewKey";
- (void)askUserAQuestion {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Question"
message:@"What do you want to do?"
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex){
if (buttonIndex == 0) {
[self doCancel];
} else {
[self doContinue];
}
};
//將alert和block關聯在了一起
objc_setAssociatedObject(alert,EOCMyAlertViewKey,block, OBJC_ASSOCIATION_COPY);
[alert show];
}
// UIAlertViewDelegate protocol method
- (void)alertView:(UIAlertView*)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
//alert取出關聯的block
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, EOCMyAlertViewKey)
//給block傳入index值
block(buttonIndex);
}
用“方法調配技術”調試“黑盒方法”
與選擇子名稱相對應的方法是可以在運行期被改變的,所以,我們可以不用通過繼承類並覆寫方法就能改變這個類本身的功能。
那麼如何在運行期改變選擇子對應的方法呢?
答:通過操縱類的方法列表的IMP指針
什麼是類方法表?什麼是IMP指針呢?
類的方法列表會把選擇子的名稱映射到相關的方法實現上,使得“動態消息派發系統”能夠據此找到應該調用的方法。這些方法均以函數指針的形式來表示,這些指針叫做IMP。
例如NSString類的選擇子列表:
有了這張表,OC的運行期系統提供的幾個方法就能操縱它。開發者可以向其中增加選擇子,也可以改變某選擇子對應的方法實現,也可以交換兩個選擇子所映射到的指針以達到交換方法實現的目的。
舉個 :交換lowercaseString和uppercaseString方法的實現:
Method originalMethod = class_getInstanceMethod([NSString class],
@selector(lowercaseString)); Method swappedMethod =
class_getInstanceMethod([NSString
class],@selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
這樣一來,類方法表的映射關係就變成了下圖:
這時,如果我們調用lowercaseString方法就會實際調用uppercaseString的方法,反之亦然。
然而!
在實際應用中,只交換已經存在的兩個方法是沒有太大意義的。我們應該利用這個特性來給既有的方法添加新功能(聽上去吊吊的):
它的實現原理是:先通過分類增加一個新方法,然後將這個新方法和要增加功能的舊方法替換(舊方法名 對應新方法的實現),這樣一來,如果我們調用了舊方法,就會實現新方法了。
不知道這麼說是否抽象。還是舉個 :
需求:我們要在原有的lowercaseString方法中添加一條輸出語句。
步驟一:我們先將新方法寫在NSString的分類裏:
@interface NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString;
@end
@implementation NSString (EOCMyAdditions)
- (NSString*)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];//eoc_myLowercaseString方法會在將來方法調換後執行lowercaseString的方法
NSLog(@"%@ => %@", self, lowercase);//輸出語句,便於調試
return lowercase;
}
@end
步驟二:交換兩個方法的實現(操縱調換IMP指針)
Method originalMethod =
class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod =
class_getInstanceMethod([NSString class],
@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
這樣一來,我們如果交換了lowercaseString和eoc_myLowercaseString的方法實現,那麼在調用原來的lowercaseString方法後就可以輸出新增的語句了。
“NSString *string = @"ThIs iS tHe StRiNg";
NSString *lowercaseString = [string lowercaseString];
// Output: ThIs iS tHe StRiNg => this is the string”
提供"全能初始化方法"
有時,由於要實現各種設計需求,一個類可以有多個創建實例的初始化方法。我們應該選定其中一個作爲全能初始化方法,令其他初始化方法都來調用它。
注意:
- 只有在這個全能初始化方法裏面才能存儲內部數據。這樣一來,當底層數據存儲機制改變時,只需修改此方法的代碼就好,無需改動其他初始化方法。
- 全能初始化方法是所有初始化方法裏參數最多的一個,因爲它使用了儘可能多的初始化所需要的參數,以便其他的方法來調用自己。
- 在我們擁有了一個全能初始化方法後,最好還是要覆寫init方法來設置默認值。
//全能初始化方法
- (id)initWithWidth:(float)width andHeight:(float)height
{
if ((self = [super init])) {
_width = width;
_height = height;
}
return self;
}
//init方法也調用了全能初始化方法
- (id)init {
return [self initWithWidth:5.0f andHeight:10.0f];
}
現在,我們要創造一個squre類繼承這上面這個ractangle類,它有自己的全能初始化方法:
- (id)initWithDimension: (float)dimension{
return [super initWithWidth:dimension andHeight:dimension];
}
這裏有問題!
然而,因爲square類是rectangle類的子類,那麼它也可以使用**initWithWidth: andHeight:**方法,更可以使用init方法。那麼這兩種情況下,顯然是無法確保初始化的圖形是正方形。
因此,我們需要在這裏覆寫square的父類rectangle的全能初始化方法:
- (id)initWithWidth:(float)width andHeight:(float)height
{
float dimension = MAX(width, height);
return [self initWithDimension:dimension];
}
這樣一來,當square用initWithWidth: andHeight:方法初始化時,就會得到一個正方形。
並且,如果用init方法來初始化square的話,我們也可以得到一個默認的正方形。因爲在rectangle類裏覆寫了init方法,而這個init方法又調用了initWithWidth: andHeight:方法,並且square類又覆寫了**initWithWidth: andHeight:**方法,所以我們仍然可以得到一個正方形。
而且,爲了讓square的init方法得到一個默認的正方形,我們也可以覆寫它自己的初始化方法:
- (id)init{
return [self initWithDimension:5.0f];
}
我們做個總結:
因爲子類的全能初始化方法(initWithDimension:)和其父類的初始化方法並不同,所以我們需要在子類裏覆寫initWithWidth: andHeight:方法。
還差一點:initWithCoder:的初始化
有時,需要定義兩種全能初始化方法,因爲對象有可能有兩種完全不同的創建方式,例如initWithCoder:方法。
我們仍然需要調用超類的初始化方法:
在rectangle類:
// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {
// Call through to super's designated initializer
if ((self = [super init])) {
_width = [decoder decodeFloatForKey:@"width"];
_height = [decoder decodeFloatForKey:@"height"];
}
return self;
}
在square類:
// Initializer from NSCoding
- (id)initWithCoder:(NSCoder*)decoder {
// Call through to super's designated initializer
if ((self = [super initWithCoder:decoder])) {
// EOCSquare's specific initializer
}
return self;
}
每個子類的全能初始化方法都應該調用其超類的對應方法,並逐層向上。在調用了超類的初始化方法後,再執行與本類相關的方法。
實現description方法
在打印我們自己定義的類的實例對象時,在控制檯輸出的結果往往是這樣的:
object = <EOCPerson: 0x7fd9a1600600>
這裏只包含了類名和內存地址,它的信息顯然是不具體的,遠達不到調試的要求。
但是!如果在我們自己定義的類覆寫description方法,我們就可以在打印這個類的實例時輸出我們想要的信息。
例如:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@ %@>", [self class], self, firstName, lastName];
}
在這裏,顯示了內存地址,還有該類的所有屬性。
而且,如果我們將這些屬性值放在字典裏打印,則更具有可讀性:
- (NSString*)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",[self class],self,
@{ @"title":_title,
@"latitude":@(_latitude),
@"longitude":@(_longitude)}
];
}
輸出結果:
location = <EOCLocation: 0x7f98f2e01d20, {
latitude = "51.506";
longitude = 0;
title = London;
}>
我們可以看到,通過重寫description方法可以讓我們更加了解對象的情況,便於後期的調試,節省開發時間。
通過協議提供匿名對象
匿名對象(Annonymous object),可以理解爲“沒有名字的對象”。有時我們用協議來提供匿名對象,目的在於說明它僅僅表示“遵從某個協議的對象”,而不是“屬於某個類的對象”。
它的表示方法爲:id。
通過協議提供匿名對象的主要使用場景有兩個:
作爲屬性
作爲方法參數
-
匿名對象作爲屬性
在設定某個類爲自己的代理屬性時,可以不聲明代理的類,而是用id,因爲成爲代理的終點並不是某個類的實例,而是遵循了某個協議。
舉個 :
@property (nonatomic, weak) id <EOCDelegate> delegate;
在這裏使用匿名對象的原因有兩個:- 將來可能會有很多不同類的實例對象作爲該類的代理。
- 我們不想指明具體要使用哪個類來作爲這個類的代理。
也就是說,能作爲該類的代理的條件只有一個:它遵從了 <EOCDelegate>
協議。
2. 匿名對象作爲方法參數
有時,我們不會在意方法裏某個參數的具體類型,而是遵循了某種協議,這個時候就可以使用匿名對象來作爲方法參數。
舉個 :
- (void)setObject:(id)object forKey:(id<NSCopying>)key;
這個方法是NSDictionary的設值方法,它的參數只要遵從了協議,就可以作爲參數傳進去,作爲NSDictionary的鍵。
編寫“異常安全代碼”時留意內存管理問題
在發生異常時的內存管理需要仔細考慮內存管理的問題:
在try塊中,如果先保留了某個對象,然後在釋放它之前又拋出了異常,那麼除非在catch塊中能處理此問題,否則對象所佔內存就將泄漏。
在MRC環境下:
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
[object release];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
這裏,我們用release
方法釋放了try中的對象,但是這樣做仍然有問題:如果在doSomthingThatMayThrow
方法中拋出了異常了呢?
這樣就無法執行release方法了。
解決辦法是使用@finnaly
塊,無論是否拋出異常,其中的代碼都能運行:
EOCSomeClass *object;
@try {
object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
@finally {
[object release];
}
在ARC環境下呢?
@try {
EOCSomeClass *object = [[EOCSomeClass alloc] init];
[object doSomethingThatMayThrow];
}
@catch (...) {
NSLog(@"Whoops, there was an error. Oh well...");
}
這時,我們無法手動使用release
方法了,解決辦法是使用:-fobjc-arc-exceptions
標誌來加入清理代碼,不過會導致應用程序變大,而且會降低運行效率。
以弱引用避免保留環
對象之間都用強指針引用對方的話會造成保留環。
兩個對象的保留環:
兩個對象都有一個對方的實例來作爲自己的屬性:
@interface EOCClassA : NSObject
@property (nonatomic, strong) EOCClassB *other;
@end
@interface EOCClassB : NSObject
@property (nonatomic, strong) EOCClassA *other;
@end
兩個對象都有指向對方的強指針,這樣會導致這兩個屬性裏的對象無法被釋放掉。
多個對象的保留環:
如果保留環連接了多個對象,而這裏其中一個對象被外界引用,那麼當這個引用被移除後,整個保留環就泄漏了。
解決方案是使用弱引用:
//EOCClassB.m
//第一種弱引用:unsafe_unretained
@property (nonatomic, unsafe_unretained) EOCClassA *other;
//第二種弱引用:weak
@property (nonatomic, weak) EOCClassA *other;
這兩種弱引用有什麼區別呢?
unsafe_unretained:當指向EOCClassA實例的引用移除後,unsafe_unretained屬性仍然指向那個已經回收的實例,
而weak指向nil:
顯然,用weak字段應該是更安全的,因爲不再使用的對象按理說應該設置爲nil,而不應該產生依賴。
以“自動釋放池快”降低內存峯值
釋放對象的兩種方式:
- 調用release:保留計數遞減
- 調用autorelease將其加入自動釋放池中。在將來清空自動釋放池時,系統會向其中的對象發送release消息。
內存峯值(high-memory waterline)是指應用程序在某個限定時段內的最大內存用量(highest memory footprint)。新增的自動釋放池塊可以減少這個峯值:
不用自動釋放池減少峯值:
for (int i = 0; i < 100000; i++) {
[self doSomethingWithInt:i];
}
在這裏,doSomethingWithInt:
方法可能會創建臨時對象。隨着循環次數的增加,臨時對象的數量也會飆升,而只有在整個for循環結束後,這些臨時對象纔會得意釋放。
這種情況是不理想的,尤其在我們無法控制循環長度的情況下,我們會不斷佔用內存並突然釋放掉它們。
因此,我們需要用自動釋放池來降低這種突兀的變化:
NSArray *databaseRecords = /* ... */;
NSMutableArray *people = [NSMutableArray new];
for (NSDictionary *record in databaseRecords) {
@autoreleasepool {
EOCPerson *person = [[EOCPerson alloc] initWithRecord:record];
[people addObject:person];
}
}
這樣一來,每次循環結束,我們都會將臨時對象放在這個池裏面,而不是線程的主池裏面。
用“殭屍對象”調試內存管理問題
某個對象被回收後,再向它發送消息是不安全的,這並不一定會引起程序崩潰。
如果程序沒有崩潰,可能是因爲:
- 該內存的部分原數據沒有被覆寫。
- 該內存恰好被另一個對象佔據,而這個對象可以應答這個方法。
如果被回收的對象佔用的原內存被新的對象佔據,那麼收到消息的對象就不會是我們預想的那個對象。在這樣的情況下,如果這個對象無法響應那個方法的話,程序依舊會崩潰。
因此,我們希望可以通過一種方法捕捉到對象被釋放後收到消息的情況。
這種方法就是利用殭屍對象!
Cocoa提供了“殭屍對象”的功能。如果開啓了這個功能,運行期系統會把所有已經回收的實例轉化成特殊的“殭屍對象”(通過修改isa指針,令其指向特殊的殭屍類),而不會真正回收它們,而且它們所佔據的核心內存將無法被重用,這樣也就避免了覆寫的情況。
在殭屍對象收到消息後,會拋出異常,它會說明發送過來的消息,也會描述回收之前的那個對象。
爲常用的塊類型創建typedef
如果我們需要重複創建某種塊(相同參數,返回值)的變量,我們就可以通過typedef來給某一種塊定義屬於它自己的新類型
例如:
int (^variableName)(BOOL flag, int value) =^(BOOL flag, int value){
// Implementation
return someInt;
}
這個塊有一個bool參數和一個int參數,並返回int類型。我們可以給它定義類型:
typedef int(^EOCSomeBlock)(BOOL flag, int value);
再次定義的時候,就可以通過簡單的賦值來實現:
EOCSomeBlock block = ^(BOOL flag, int value){ // Implementation };
定義作爲參數的塊:
- (void)startWithCompletionHandler: (void(^)(NSData *data, NSError *error))completion;
這裏的塊有一個NSData參數,一個NSError參數並沒有返回值
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
- (void)startWithCompletionHandler:(EOCCompletionHandler)completion;”
通過typedef定義塊簽名的好處是:如果要某種塊增加參數,那麼只修改定義簽名的那行代碼即可。
用handler塊降低代碼分散程度
下載網絡數據時,如果使用代理方法,會使得代碼分佈不緊湊,而且如果有多個下載任務的話,還要在回調的代理中判斷當前請求的類型。但是如果使用block的話,就可以讓網絡下載的代碼和回調處理的代碼寫在一起,這樣就可以同時解決上面的兩個問題:
用代理下載:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
_fooFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
_fooFetcher.delegate = self;
[_fooFetcher start];
}
- (void)fetchBarData {
NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
_barFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
_barFetcher.delegate = self;
[_barFetcher start];
}
- (void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{ //判斷下載器類型
if (networkFetcher == _fooFetcher) {
_fetchedFooData = data;
_fooFetcher = nil;
} else if (networkFetcher == _barFetcher) {
_fetchedBarData = data;
_barFetcher = nil;
}
}
用塊下載:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetchedFooData = data;
}];
}
- (void)fetchBarData {
NSURL *url = [[NSURL alloc] initWithString: @"http://www.example.com/bar.dat"];
EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetchedBarData = data;
}];
}
還可以將處理成功的代碼放在一個塊裏,處理失敗的代碼放在另一個塊中:
“#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler: (EOCNetworkFetcherCompletionHandler)completion failureHandler: (EOCNetworkFetcherErrorHandler)failure;
@end
EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:^(NSData *data){
// Handle success
}
failureHandler:^(NSError *error){
// Handle failure
}];
這樣寫的好處是,我們可以將處理成功和失敗的代碼分開來寫,看上去更加清晰。
我們還可以將 成功和失敗的代碼都放在同一個塊裏:
“#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data, NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;
@end
EOCNetworkFetcher *fetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:
^(NSData *data, NSError *error){
if (error) {
// Handle failure
} else {
// Handle success
}
}];
這樣做的好處是,如果及時下載失敗或中斷了,我們仍然可以取到當前所下載的data。而且,如果在需求上指出:下載成功後得到的數據很少,也視爲失敗,那麼單一塊的寫法就很適用,因爲它可以取得數據後(成功)再判斷其是否是下載成功的。
用塊引用其所屬對象時不要出現保留環
如果塊捕獲的對象直接或間接地保留了塊本身,那麼就需要小心保留環問題:
@implementation EOCClass {
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
_networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchedData = data;
}];
}
在這裏出現了保留環:塊要設置_fetchedData變量,就需要捕獲self變量。而self(EOCClass實例)通過實例變量保留了獲取器_networkFetcher,而_networkFetcher又保留了塊。
解決方案是:在塊中取得了data後,將_networkFetcher設爲nil。
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com/something.dat"];
_networkFetcher =[[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchedData = data;
_networkFetcher = nil;
}];
}
多用派發隊列,少用同步鎖
多個線程執行同一份代碼時,很可能會造成數據不同步。作者建議使用GCD來爲代碼加鎖的方式解決這個問題。
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", NULL);
//讀取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//設置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_sync(_syncQueue, ^{
_someString = someString;
});
}
這樣一來,讀寫操作都在串行隊列進行,就不容易出錯。
但是,還有一種方法可以讓性能更高:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//讀取字符串
- (NSString*)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
//設置字符串
- (void)setSomeString:(NSString*)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
顯然,數據的正確性主要取決於寫入操作,那麼只要保證寫入時,線程是安全的,那麼即便讀取操作是併發的,也可以保證數據是同步的。
這裏的
dispatch_barrier_async
方法使得操作放在了同步隊列裏“有序進行”,保證了寫入操作的任務是在串行隊列裏。
多用GCD,少用performSelector系列方法
在iOS開發中,有時會使用performSelector來執行某個方法,但是performSelector系列的方法能處理的選擇子很侷限:
- 它無法處理帶有多個參數的選擇子。
- 返回值只能是void或者對象類型。
但是如果將方法放在塊中,通過GCD來操作就能很好地解決這些問題。尤其是我們如果想要讓一個任務在另一個線程上執行,最好應該將任務放到塊裏,交給GCD來實現,而不是通過performSelector方法。
舉幾個 來比較這兩種方案:
1. 延後執行某個任務的方法:
// 使用 performSelector:withObject:afterDelay:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
// 使用 dispatch_after
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
2. 將任務放在主線程執行:
// 使用 performSelectorOnMainThread:withObject:waitUntilDone:
[self performSelectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
// 使用 dispatch_async
// (or if waitUntilDone is YES, then dispatch_sync)
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
注意:
如果waitUntilDone的參數是Yes,那麼就對應GCD的dispatch_sync方法。
我們可以看到,使用GCD的方式可以將線程操作代碼和方法調用代碼寫在同一處,一目瞭然;而且完全不受調用方法的選擇子和方法參數個數的限制。
掌握GCD及操作隊列的使用時機
除了GCD,操作隊列(NSOperationQueue)也是解決多線程任務管理問題的一個方案。對於不同的環境,我們要採取不同的策略來解決問題:有時候使用GCD好些,有時則是使用操作隊列更加合理。
使用NSOperation和NSOperationQueue的優點:
- 可以取消操作:在運行任務前,可以在NSOperation對象調用cancel方法,標明此任務不需要執行。但是GCD隊列是無法取消的,因爲它遵循“安排好之後就不管了(fire and forget)”的原則。
- 可以指定操作間的依賴關係:例如從服務器下載並處理文件的動作可以用操作來表示。而在處理其他文件之前必須先下載“清單文件”。而後續的下載工作,都要依賴於先下載的清單文件這一操作。
- 監控NSOperation對象的屬性:可以通過KVO來監聽NSOperation的屬性:可以通過isCancelled屬性來判斷任務是否已取消;通過isFinished屬性來判斷任務是否已經完成。
- 可以指定操作的優先級:操作的優先級表示此操作與隊列中其他操作之間的優先關係,我們可以指定它。
通過Dispath Group機制,根據系統資源狀況來執行任務
有時需要等待多個並行任務結束的那一刻執行某個任務,這個時候就可以使用dispath group函數來實現這個需求:
通過dispath group函數,可以把併發執行的多個任務合爲一組,於是調用者就可以知道這些任務何時才能全部執行完畢。
//一個優先級低的併發隊列
dispatch_queue_t lowPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
//一個優先級高的併發隊列
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//創建dispatch_group
dispatch_group_t dispatchGroup = dispatch_group_create();
//將優先級低的隊列放入dispatch_group
for (id object in lowPriorityObjects) {
dispatch_group_async(dispatchGroup,lowPriorityQueue,^{ [object performTask]; });
}
//將優先級高的隊列放入dispatch_group
for (id object in highPriorityObjects) {
dispatch_group_async(dispatchGroup,highPriorityQueue,^{ [object performTask]; });
}
//dispatch_group裏的任務都結束後調用塊中的代碼
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(dispatchGroup,notifyQueue,^{
// Continue processing after completing tasks
});
使用dispatch_once來執行只需運行一次的線程安全代碼
有時我們可能只需要將某段代碼執行一次,這時可以通過dispatch_once函數來解決。
dispatch_once函數比較重要的使用例子是單例模式:
我們在創建單例模式的實例時,可以使用dispatch_once函數來令初始化代碼只執行一次,並且內部是線程安全的。
而且,對於執行一次的block來說,每次調用函數時傳入的標記都必須完全相同,通常標記變量聲明在static或global作用域裏。
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
我們可以這麼理解:在dispatch_once塊中的代碼在程序啓動到終止的過程裏,只要運行了一次後,就給自己加上了註釋符號,不再存在了。
對自定義其內存管理語義的collection使用無縫橋接
通過無縫橋接技術,可以再Foundation框架中的OC對象和CoreFoundation框架中的C語言數據結構之間來回轉換。
創建CoreFoundation中的collection時,可以指定如何處理其中的元素。然後利用無縫橋接技術,可以將其轉換爲OCcollection。
簡單的無縫橋接演示:
NSArray *anNSArray = @[@1, @2, @3, @4, @5];
CFArrayRef aCFArray = (__bridge CFArrayRef)anNSArray;
NSLog(@"Size of array = %li", CFArrayGetCount(aCFArray));
這裏,__bridge
表示ARC仍然具備這個OC對象的所有權。CFArrayGetCount
用來獲取數組的長高度。
爲什麼要使用無縫橋接技術呢?因爲有些OC對象的特性是其對應的CF數據結構不具備的,反之亦然。因此我們需要通過無縫橋接技術來讓這兩者進行功能上的“互補”。
小編在這裏給大家推薦一個優秀的iOS交流平臺,平臺裏的夥伴們都是非常優秀的iOS開發人員,我們專注於技術的分享與技巧的交流,大家可以在平臺上討論技術,交流學習。歡迎大家的加入(密碼:001)
本文轉自第三方,如有侵權,請聯繫刪除
作者:J_Knight_
鏈接:https://www.jianshu.com/p/c8da794079cb