你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> 《禅與Objective-C編程藝術》讀書筆記(二)

《禅與Objective-C編程藝術》讀書筆記(二)

編輯:IOS開發綜合

五、Categories

我們應該要在我們的 category 方法前加上自己的小寫前綴以及下劃線,比如- (id)zoc_myCategoryMethod。 這種實踐同樣被蘋果推薦。這是非常必要的。因為如果在擴展的 category 或者其他 category 裡面已經使用了同樣的方法名,會導致不可預計的後果。實際上,實際被調用的是最後被實現的那個方法。如果想要確認你的分類方法沒有覆蓋其他實現的話,可以把環境變量 OBJC_PRINT_REPLACED_METHODS 設置為 YES,這樣那些被取代的方法名字會打印到 Console 中。現在 LLVM 5.1 不會為此發出任何警告和錯誤提示,所以自己小心不要在分類中重載方法。一個好的實踐是在 category 名中使用前綴。

* 例子 *

@interface NSDate (ZOCTimeExtensions)
- (NSString *)zoc_timeAgoShort;
@end

* 不要這樣做 *

@interface NSDate (ZOCTimeExtensions)
- (NSString *)timeAgoShort;
@end

六、Protocols

在 Objective-C 的世界裡面經常錯過的一個東西是抽象接口。接口(interface)這個詞通常指一個類的 .h 文件,但是它在 Java 程序員眼裡有另外的含義: 一系列不依賴具體實現的方法的定義。在 Objective-C 裡是通過 protocol 來實現抽象接口的。我們會解釋 protocol 的強大力量(用作抽象接口),用具體的例子來解釋:把非常糟糕的設計的架構改造為一個良好的可復用的代碼。這個例子是在實現一個 RSS 訂閱的閱讀器(它可是經常在技術面試中作為一個測試題呢)。
要求很簡單明了:把一個遠程的 RSS 訂閱展示在一個 tableview 中。
最小的步驟是遵從單一功能原則,創建至少兩個組成部分來完成這個任務:
(a)一個 feed 解析器來解析搜集到的結果
(b)一個 feed 閱讀器來顯示結果
這些類的接口可以是這樣的:

@interface ZOCFeedParser : NSObject

@property (nonatomic, weak) id  delegate;
@property (nonatomic, strong) NSURL *url;

- (id)initWithURL:(NSURL *)url;

- (BOOL)start;
- (void)stop;

@end

@interface ZOCTableViewController : UITableViewController

- (instancetype)initWithFeedParser:(ZOCFeedParser *)feedParser;

@end

ZOCFeedParser 用一個 NSURL 來初始化來獲取 RSS 訂閱(在這之下可能會使用 NSXMLParser 和 NSXMLParserDelegate 創建有意義的數據),ZOCTableViewController 會用這個 parser 來進行初始化。 我們希望它顯示 parser 接受到的指並且我們用下面的 protocol 實現委托:

@protocol ZOCFeedParserDelegate 
@optional
- (void)feedParserDidStart:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedInfo:(ZOCFeedInfoDTO *)info;
- (void)feedParser:(ZOCFeedParser *)parser didParseFeedItem:(ZOCFeedItemDTO *)item;
- (void)feedParserDidFinish:(ZOCFeedParser *)parser;
- (void)feedParser:(ZOCFeedParser *)parser didFailWithError:(NSError *)error;
@end

用合適的 protocol 來來處理 RSS 非常完美。view controller 會遵從它的公開的接口:

@interface ZOCTableViewController : UITableViewController 

最後創建的代碼是這樣子的:

NSURL *feedURL = [NSURL URLWithString:@"http://bbc.co.uk/feed.rss"];

ZOCFeedParser *feedParser = [[ZOCFeedParser alloc] initWithURL:feedURL];

ZOCTableViewController *tableViewController = [[ZOCTableViewController alloc] initWithFeedParser:feedParser];
feedParser.delegate = tableViewController;

七、NSNotification

當你定義你自己的 NSNotification 的時候你應該把你的通知的名字定義為一個字符串常量,就像你暴露給其他類的其他字符串常量一樣。你應該在公開的接口文件中將其聲明為 extern 的, 並且在對應的實現文件裡面定義。因為你在頭文件中暴露了符號,所以你應該按照統一的命名空間前綴法則,用類名前綴作為這個通知名字的前綴。同時,用一個 Did/Will 這樣的動詞以及用 “Notifications” 後綴來命名這個通知也是一個好的實踐。

// Foo.h
extern NSString * const ZOCFooDidBecomeBarNotification

// Foo.m
NSString * const ZOCFooDidBecomeBarNotification = @"ZOCFooDidBecomeBarNotification";

八、美化代碼

1.空格
(a)縮進使用 4 個空格。 永遠不要使用 tab, 確保你在 Xcode 的設置裡面是這樣設置的。
(b)方法的大括號和其他的大括號(if/else/switch/while 等) 總是在同一行開始,在新起一行結束。
推薦:

if (user.isHappy) {
    //Do something
}
else {
    //Do something else
}

//不推薦:

if (user.isHappy)
{
  //Do something
} else {
  //Do something else
}

(c)方法之間應該要有一個空行來幫助代碼看起來清晰且有組織。 方法內的空格應該用來分離功能,但是通常不同的功能應該用新的方法來定義。 優先使用 auto-synthesis。但是如果必要的話, @synthesize and @dynamic。
(d)在實現文件中的聲明應該新起一行。
(e)應該總是讓冒號對其。有一些方法簽名可能超過三個冒號,用冒號對齊可以讓代碼更具有可讀性。總是用冒號對其方法,即使有代碼塊存在。

推薦:

[UIView animateWithDuration:1.0
                 animations:^{
                     // something
                 }
                 completion:^(BOOL finished) {
                     // something
                 }];

不推薦:

[UIView animateWithDuration:1.0 animations:^{
    // something 
} completion:^(BOOL finished) {
    // something
}];

如果自動對齊讓可讀性變得糟糕,那麼應該在之前把 block 定義為變量,或者重新考慮你的代碼簽名設計。

2.Line Breaks 換行
本指南關注代碼顯示效果以及在線浏覽的可讀性,所以換行是一個重要的主題。
舉個例子:

self.productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:productIdentifiers];

一個像上面的長行的代碼在第二行以一個間隔(2個空格)延續

self.productsRequest = [[SKProductsRequest alloc] 
  initWithProductIdentifiers:productIdentifiers];

3.括號
在以下的地方使用 Egyptian風格 括號 (譯者注:又稱 K&R 風格,代碼段括號的開始位於一行的末尾,而不是另外起一行的風格。關於為什麼叫做 Egyptian Brackets,可以參考 http://blog.codinghorror.com/new-programming-jargon/ )
控制語句 (if-else, for, switch)
非 Egyptian 括號可以用在:
類的實現(如果存在)
方法的實現

九、代碼組織

1.利用代碼塊
一個 GCC 非常模糊的特性,以及 Clang 也有的特性是,代碼塊如果在閉合的圓括號內的話,會返回最後語句的值

NSURL *url = ({
    NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
    [NSURL URLWithString:urlString];
});

這個特性非常適合組織小塊的代碼,通常是設置一個類。他給了讀者一個重要的入口並且減少相關干擾,能讓讀者聚焦於關鍵的變量和函數中。此外,這個方法有一個優點,所有的變量都在代碼塊中,也就是只在代碼塊的區域中有效,這意味著可以減少對其他作用域的命名污染。

2.Pragma
2.1 Pragma Mark

#pragma mark - 是一個在類內部組織代碼並且幫助你分組方法實現的好辦法。 我們建議使用  #pragma mark - 來分離:

(1) 不同功能組的方法
(2) protocols 的實現
(3) 對父類方法的重寫

- (void)dealloc { /* ... */ }
- (instancetype)init { /* ... */ }

 #pragma mark - View Lifecycle (View 的生命周期)

- (void)viewDidLoad { /* ... */ }
- (void)viewWillAppear:(BOOL)animated { /* ... */ }
- (void)didReceiveMemoryWarning { /* ... */ }

 #pragma mark - Custom Accessors (自定義訪問器)

- (void)setCustomProperty:(id)value { /* ... */ }
- (id)customProperty { /* ... */ }

 #pragma mark - IBActions  

- (IBAction)submitData:(id)sender { /* ... */ }

 #pragma mark - Public 

- (void)publicMethod { /* ... */ }

 #pragma mark - Private

- (void)zoc_privateMethod { /* ... */ }

 #pragma mark - UITableViewDataSource

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { /* ... */ }

 #pragma mark - ZOCSuperclass

// ... 重載來自 ZOCSuperclass 的方法

 #pragma mark - NSObject

- (NSString *)description { /* ... */ }

上面的標記能明顯分離和組織代碼。你還可以用 cmd+Click 來快速跳轉到符號定義地方。 但是小心,即使 paragma mark 是一門手藝,但是它不是讓你類裡面方法數量增加的一個理由:類裡面有太多方法說明類做了太多事情,需要考慮重構了。

2.2 關於Pragma
大多數 iOS 開發者平時並沒有和很多編譯器選項打交道。一些選項是對控制嚴格檢查(或者不檢查)你的代碼或者錯誤的。有時候,你想要用 pragma 直接產生一個異常,臨時打斷編譯器的行為。當你使用ARC的時候,編譯器幫你插入了內存管理相關的調用。但是這樣可能產生一些煩人的事情。比如你使用 NSSelectorFromString 來動態地產生一個 selector 調用的時候,ARC不知道這個方法是哪個並且不知道應該用那種內存管理方法,你會被提示 performSelector may cause a leak because its selector is unknown(執行 selector 可能導致洩漏,因為這個 selector 是未知的).如果你知道你的代碼不會導致內存洩露,你可以通過加入這些代碼忽略這些警告

 #pragma clang diagnostic push

 #pragma clang diagnostic ignored "-Warc-performSelector-leaks"

[myObj performSelector:mySelector withObject:name];

 #pragma clang diagnostic pop

注意我們是如何在相關代碼上下文中用 pragma 停用 -Warc-performSelector-leaks 檢查的。這確保我們沒有全局禁用。如果全局禁用,可能會導致錯誤。

3.忽略沒用使用變量的編譯警告
這對表明你一個定義但是沒有使用的變量很有用。大多數情況下,你希望移除這些引用來(稍微地)提高性能,但是有時候你希望保留它們。為什麼?或許它們以後有用,或者有些特性只是暫時移除。無論如何,一個消除這些警告的好方法是用相關語句進行注解,使用 #pragma unused():

- (void)giveMeFive
{
    NSString *foo;
    #pragma unused (foo)

    return 5;
}

現在你的代碼不用任何編譯警告了。注意你的 pragma 需要標記到未定義的變量之下。

4.明確編譯器警告和錯誤
編譯器是一個機器人,它會標記你代碼中被 Clang 規則定義為錯誤的地方。但是,你總是比 Clang 更聰明。通常,你會發現一些討厭的代碼 會導致這個問題,而且不論怎麼做,你都解決不了。你可以這樣明確一個錯誤:

- (NSInteger)divide:(NSInteger)dividend by:(NSInteger)divisor
{
    #error Whoa, buddy, you need to check for zero here!
    return (dividend / divisor);
}

類似的,你可以這樣標明一個 警告

- (float)divide:(float)dividend by:(float)divisor
{
    #warning Dude, don't compare floating point numbers like this!
    if (divisor != 0.0) {
        return (dividend / divisor);
    }
    else {
        return NAN;
    }
}

5.字符串文檔
所有重要的方法,接口,分類以及協議定義應該有伴隨的注釋來解釋它們的用途以及如何使用。簡而言之:有長的和短的兩種字符串文檔。
短文檔適用於單行的文件,包括注釋斜槓。它適合簡短的函數,特別是(但不僅僅是)非 public 的 API:

// Return a user-readable form of a Frobnozz, html-escaped.

文本應該用一個動詞 (“return”) 而不是 “returns” 這樣的描述。

如果描述超出一行,你應該用長的字符串文檔: 一行斜槓和兩個星號來開始塊文檔 (/*, 之後是總結的一句話,可以用句號、問號或者感歎號結尾,然後空一行,在和第一句話對齊寫下剩下的注釋,然後用一個 (/)來結束。

/**
 This comment serves to demonstrate the format of a docstring.

 Note that the summary line is always at most one line long, and
 after the opening block comment, and each line of text is preceded
 by a single space.
*/

一個函數必須有一個字符串文檔,除非它符合下面的所有條件:
(a) 非公開
(b) 很短
(c) 顯而易見
字符串文檔應該描述函數的調用符號和語義,而不是它如何實現。

6.注釋
當它需要的時候,注釋應該用來解釋特定的代碼做了什麼。所有的注釋必須被持續維護或者干脆就刪除。塊注釋應該被避免,代碼本身應該盡可能就像文檔一樣表示意圖,只需要很少的打斷注釋 例外: 這不能適用於用來產生文檔的注釋

7.頭文檔
一個類的文檔應該只在 .h 文件裡用 Doxygen/AppleDoc 的語法書寫。 方法和屬性都應該提供文檔。

例子:

/**
 *  Designated initializer.
 *
 *  @param  store  The store for CRUD operations.
 *  @param  searchService The search service used to query the store.
 *
 *  @return A ZOCCRUDOperationsStore object.
 */
- (instancetype)initWithOperationsStore:(id)store
                          searchService:(id)searchService;

十、對象間的通訊

1.Blocks
Blocks 是 Objective-C 版本的 lambda 或者 closure(閉包)。使用 block 定義異步接口:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion;

當你定義一個類似上面的接口的時候,盡量使用一個單獨的 block 作為接口的最後一個參數。把需要提供的數據和錯誤信息整合到一個單獨 block 中,比分別提供成功和失敗的 block 要好。以下是你應該這樣做的原因:
(a) 通常這成功處理和失敗處理會共享一些代碼(比如讓一個進度條或者提示消失);
(b) Apple 也是這樣做的,與平台一致能夠帶來一些潛在的好處;
(c) block 通常會有多行代碼,如果不是在最後一個參數的話會打破調用點;
(d) 使用多個 block 作為參數可能會讓調用看起來顯得很笨拙,並且增加了復雜性。

看上面的方法,完成處理的 block 的參數很常見:第一個參數是調用者希望獲取的數據,第二個是錯誤相關的信息。這裡需要遵循以下兩點:
若 objects 不為 nil,則 error 必須為 nil
若 objects 為 nil,則 error 必須不為 nil

因為調用者更關心的是實際的數據,就像這樣:

- (void)downloadObjectsAtPath:(NSString *)path
                   completion:(void(^)(NSArray *objects, NSError *error))completion {
    if (objects) {
        // do something with the data
    }
    else {
        // some error occurred, 'error' variable should not be nil by contract
    }
}

此外,Apple 提供的一些同步接口在成功狀態下向 error 參數(如果非 NULL) 寫入了垃圾值,所以檢查 error 的值可能出現問題。

2.深入 Blocks
一些關鍵點:
(1) block 是在棧上創建的
(2) block 可以復制到堆上
(3) block 有自己的私有的棧變量(以及指針)的常量復制
(4) 可變的棧上的變量和指針必須用 __block 關鍵字聲明
如果 block 沒有在其他地方被保持,那麼它會隨著棧生存並且當棧幀(stack frame)返回的時候消失。當在棧上的時候,一個 block 對訪問的任何內容不會有影響。如果 block 需要在棧幀返回的時候存在,它們需要明確地被復制到堆上,這樣,block 會像其他 Cocoa 對象一樣增加引用計數。當它們被復制的時候,它會帶著它們的捕獲作用域一起,retain 他們所有引用的對象。如果一個 block指向一個棧變量或者指針,那麼這個block初始化的時候它會有一份聲明為 const 的副本,所以對它們賦值是沒用的。當一個 block 被復制後,__block 聲明的棧變量的引用被復制到了堆裡,復制之後棧上的以及產生的堆上的 block 都會引用這個堆上的變量。
最重要的事情是 __block 聲明的變量和指針在 block 裡面是作為顯示操作真實值/對象的結構來對待的。block 在 Objective-C 裡面被當作一等公民對待:他們有一個 isa 指針,一個類也是用 isa 指針來訪問 Objective-C 運行時來訪問方法和存儲數據的。在非 ARC 環境肯定會把它搞得很糟糕,並且懸掛指針會導致 Crash。__block 僅僅對 block 內的變量起作用,它只是簡單地告訴 block:嗨,這個指針或者原始的類型依賴它們在的棧。請用一個棧上的新變量來引用它。我是說,請對它進行雙重解引用,不要 retain 它。 謝謝,哥們。如果在定義之後但是 block 沒有被調用前,對象被釋放了,那麼 block 的執行會導致 Crash。 __block 變量不會在 block 中被持有,最後… 指針、引用、解引用以及引用計數變得一團糟。

3.self 的循環引用
當使用代碼塊和異步分發的時候,要注意避免引用循環。 總是使用 weak 引用會導致引用循環。 此外,把持有 blocks 的屬性設置為 nil (比如 self.completionBlock = nil) 是一個好的實踐。它會打破 blocks 捕獲的作用域帶來的引用循環。

例子:

__weak __typeof(self) weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
}];

不要這樣做:

[self executeBlock:^(NSData *data, NSError *error) {
    [self doSomethingWithData:data];
}];

多個語句的例子:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        [strongSelf doSomethingWithData:data];
        [strongSelf doSomethingWithData:data];
    }
}];

不要這樣做:

__weak __typeof(self)weakSelf = self;
[self executeBlock:^(NSData *data, NSError *error) {
    [weakSelf doSomethingWithData:data];
    [weakSelf doSomethingWithData:data];
}];

你應該把這兩行代碼作為 snippet 加到 Xcode 裡面並且總是這樣使用它們。

__weak __typeof(self)weakSelf = self;
__strong __typeof(weakSelf)strongSelf = weakSelf;

這裡我們來討論下 block 裡面的 self 的 __weak 和 __strong 限定詞的一些微妙的地方。簡而言之,我們可以參考 self 在 block 裡面的三種不同情況。
(1)直接在 block 裡面使用關鍵詞 self
如果我們直接在 block 裡面用 self 關鍵字,對象會在 block 的定義時候被 retain,(實際上 block 是 copied 但是為了簡單我們可以忽略這個)。一個 const 的對 self 的引用在 block 裡面有自己的位置並且它會影響對象的引用計數。如果 block 被其他 class 或者/並且傳送過去了,我們可能想要 retain self 就像其他被 block 使用的對象,從他們需要被block執行

dispatch_block_t completionBlock = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:completionHandler];

不是很麻煩的事情。但是, 當 block 被 self 在一個屬性 retain(就像下面的例子)呢

self.completionHandler = ^{
    NSLog(@"%@", self);
}

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                animated:YES
                       completion:self.completionHandler];

這就是有名的 retain cycle, 並且我們通常應該避免它。這種情況下我們收到 CLANG 的警告:

Capturing 'self' strongly in this block is likely to lead to a retain cycle (在 block 裡面發現了 `self` 的強引用,可能會導致循環引用)

所以可以用 weak 修飾

(2)在 block 外定義一個 __weak 的 引用到 self,並且在 block 裡面使用這個弱引用
這樣會避免循環引用,也是我們通常在 block 已經被 self 的 property 屬性裡面 retain 的時候會做的。

__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    NSLog(@"%@", weakSelf);
};

MyViewController *myController = [[MyViewController alloc] init...];
[self presentViewController:myController
                   animated:YES
                 completion:self.completionHandler];

這個情況下 block 沒有 retain 對象並且對象在屬性裡面 retain 了 block 。所以這樣我們能保證了安全的訪問 self。 不過糟糕的是,它可能被設置成 nil 的。問題是:如果和讓 self 在 block 裡面安全地被銷毀。舉個例子, block 被一個對象復制到了另外一個(比如 myControler)作為屬性賦值的結果。之前的對象在可能在被復制的 block 有機會執行被銷毀。

(3)在 block 外定義一個 __weak 的 引用到 self,並在在 block 內部通過這個弱引用定義一個 __strong 的引用
你可能會想,首先,這是避免 retain cycle 警告的一個技巧。然而不是,這個到 self 的強引用在 block 的執行時間 被創建。當 block 在定義的時候, block 如果使用 self 的時候,就會 retain 了 self 對象。Apple 文檔 中表示 “為了 non-trivial cycles ,你應該這樣” :

MyViewController *myController = [[MyViewController alloc] init...];
// ...
MyViewController * __weak weakMyController = myController;
myController.completionHandler =  ^(NSInteger result) {
    MyViewController *strongMyController = weakMyController;
    if (strongMyController) {
        // ...
        [strongMyController dismissViewControllerAnimated:YES completion:nil];
        // ...
    }
    else {
        // Probably nothing...
    }
};

首先,我覺得這個例子看起來是錯誤的。如果 block 本身被 completionHandler 屬性裡面 retain 了,那麼 self 如何被 delloc 和在 block 之外賦值為 nil 呢? completionHandler 屬性可以被聲明為 assign 或者 unsafe_unretained 的,來允許對象在 block 被傳遞之後被銷毀。
我不能理解這樣做的理由,如果其他對象需要這個對象(self),block 被傳遞的時候應該 retain 對象,所以 block 應該不被作為屬性存儲。這種情況下不應該用 __weak/__strong。
總之,其他情況下,希望 weakSelf 變成 nil 的話,就像第二種情況解釋那麼寫(在 block 之外定義一個弱應用並且在 block 裡面使用)。還有,Apple的 “trivial block” 是什麼呢。我們的理解是 trivial block 是一個不被傳送的 block ,它在一個良好定義和控制的作用域裡面,weak 修飾只是為了避免循環引用。
在 block 內用強引用的優點是,搶占執行的時候的魯棒性。看上面的三個例子,在 block 執行的時候
(a) 直接在 block 裡面使用關鍵詞 self
如果 block 被屬性 retain,self 和 block 之間會有一個循環引用並且它們不會再被釋放。如果 block 被傳送並且被其他的對象 copy 了,self 在每一個 copy 裡面被 retain
(b) 在 block 外定義一個 __weak 的 引用到 self,並且在 block 裡面使用這個弱引用
沒有循環引用的時候,block 是否被 retain 或者是一個屬性都沒關系。如果 block 被傳遞或者 copy 了,在執行的時候,weakSelf 可能會變成 nil。block 的執行可以搶占,並且後來的對 weakSelf 的不同調用可以導致不同的值(比如,在 一個特定的執行 weakSelf 可能賦值為 nil )

__weak typeof(self) weakSelf = self;
dispatch_block_t block =  ^{
    [weakSelf doSomething]; // weakSelf != nil
    // preemption, weakSelf turned nil
    [weakSelf doSomethingElse]; // weakSelf == nil
};

(c) 在 block 外定義一個 __weak 的 引用到 self,並在在 block 內部通過這個弱引用定義一個 __strong 的引用。
不論管 block 是否被 retain 或者是一個屬性,這樣也不會有循環引用。如果 block 被傳遞到其他對象並且被復制了,執行的時候,weakSelf 可能被nil,因為強引用被復制並且不會變成nil的時候,我們確保對象 在 block 調用的完整周期裡面被 retain了,如果搶占發生了,隨後的對 strongSelf 的執行會繼續並且會產生一樣的值。如果 strongSelf 的執行到 nil,那麼在 block 不能正確執行前已經返回了。

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    __strong typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
      [strongSelf doSomething]; // strongSelf != nil
      // preemption, strongSelf still not nil(搶占的時候,strongSelf 還是非 nil 的)
      [strongSelf doSomethingElse]; // strongSelf != nil
    }
    else {
        // Probably nothing...
        return;
    }
};

在一個 ARC 的環境中,如果嘗試用 ->符號來表示,編譯器會警告一個錯誤:

Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to a strong variable first. (對一個 __weak 指針的解引用不允許的,因為可能在競態條件裡面變成 null, 所以先把他定義成 strong 的屬性)

可以用下面的代碼展示

__weak typeof(self) weakSelf = self;
myObj.myBlock =  ^{
    id localVal = weakSelf->someIVar;
};

在最後:
Case 1: 只能在 block 不是作為一個 property 的時候使用,否則會導致 retain cycle。
Case 2: 當 block 被聲明為一個 property 的時候使用。
Case 3: 和並發執行有關。當涉及異步的服務的時候,block 可以在之後被執行,並且不會發生關於 self 是否存在的問題。

4.委托和數據源
委托是 Apple 的框架裡面使用廣泛的模式,同時它是一個重要的 四人幫的書“設計模式”中的模式。委托模式是單向的,消息的發送方(委托方)需要知道接收方(委托),反過來就不是了。對象之間沒有多少耦合,因為發送方只要知道它的委托實現了對應的 protocol。本質上,委托模式只需要委托提供一些回調方法,就是說委托實現了一系列空返回值的方法。
一些有 void 返回類型的方法就像回調

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath;
- (void)tableView:(UITableView *)tableView didHighlightRowAtIndexPath:(NSIndexPath *)indexPath;

但是其他的不是

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender;

當委托者詢問委托對象一些信息的時候,這就暗示著信息是從委托對象流向委托者,而不會反過來。 這個概念就和委托模式有些不同,它是一個另外的模式:數據源。
可能有人會說 Apple 有一個 UITableViewDataSouce protocol 來做這個(雖然使用委托模式的名字),但是實際上它的方法是用來提供真實的數據應該如何被展示的信息的。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;

此外,以上兩個方法 Apple 混合了展示層和數據層,這顯的非常糟糕,但是很少的開發者感到糟糕。而且我們在這裡把空返回值和非空返回值的方法都天真地叫做委托方法。為了分離概念,我們應該這樣做:
委托模式:事件發生的時候,委托者需要通知委托
數據源模式: 委托方需要從數據源對象拉取數據
這個是實際的例子:

@class ZOCSignUpViewController;

@protocol ZOCSignUpViewControllerDelegate 
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

@protocol ZOCSignUpViewControllerDataSource 
- (ZOCUserCredentials *)credentialsForSignUpViewController:(ZOCSignUpViewController *)controller;
@end

@interface ZOCSignUpViewController : UIViewController

@property (nonatomic, weak) id delegate;
@property (nonatomic, weak) id dataSource;

@end

在上面的例子裡面,委托方法需要總是有一個調用方作為第一個參數,否則委托對象可能被不能區別不同的委托者的實例。此外,如果調用者沒有被傳遞到委托對象,那麼就沒有辦法讓一個委托對象處理兩個不同的委托者了。所以,下面這樣的方法就是人神共憤的:

- (void)calculatorDidCalculateValue:(CGFloat)value;

默認情況下,委托對象需要實現 protocol 的方法。可以用@required 和 @optional 關鍵字來標記方法是否是必要的還是可選的。

@protocol ZOCSignUpViewControllerDelegate 
@required
- (void)signUpViewController:(ZOCSignUpViewController *)controller didProvideSignUpInfo:(NSDictionary *);
@optional
- (void)signUpViewControllerDidPressSignUpButton:(ZOCSignUpViewController *)controller;
@end

對於可選的方法,委托者必須在發送消息前檢查委托是否確實實現了特定的方法(否則會Crash):

if ([self.delegate respondsToSelector:@selector(signUpViewControllerDidPressSignUpButton:)]) {
    [self.delegate signUpViewControllerDidPressSignUpButton:self];
}

5.繼承
有時候你可能需要重載委托方法。考慮有兩個 UIViewController 子類的情況:UIViewControllerA 和 UIViewControllerB,有下面的類繼承關系。
UIViewControllerB < UIViewControllerA < UIViewController
UIViewControllerA 遵從 UITableViewDelegate 並且實現了 - (CGFloat)tableView:(UITableView )tableView heightForRowAtIndexPath:(NSIndexPath )indexPath.
你可能會想要提供一個和 UIViewControllerB 不同的實現。一個實現可能是這樣子的:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

但是如果超類(UIViewControllerA)沒有實現這個方法呢?
調用過程

[super respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]

會用 NSObject 的實現,尋找,在 self 的上下文中無疑有它的實現,但是 app 會在下一行 Crash 並且報下面的錯:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[UIViewControllerB tableView:heightForRowAtIndexPath:]: unrecognized selector sent to instance 0x8d82820'

這種情況下我們需要來詢問特定的類實例是否可以響應對應的 selector。下面的代碼提供了一個小技巧:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    CGFloat retVal = 0;
    if ([[UIViewControllerA class] instancesRespondToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
        retVal = [super tableView:self.tableView heightForRowAtIndexPath:indexPath];
    }
    return retVal + 10.0f;
}

6.多重委托
多重委托是一個非常基礎的概念,但是,大多數開發者對此非常不熟悉而使用 NSNotifications。就像你可能注意到的,委托和數據源是對象之間的通訊模式,但是只涉及兩個對象:委托者和委托。數據源模式強制一對一的關系,發送者來像一個並且只是一個對象來請求信息。但是委托模式不一樣,它可以完美得有多個委托來等待回調操作。至少兩個對象需要接收來自特定委托者的回調,並且後一個需要知道所有的委托,這個方法更好的適用於分布式系統並且更加廣泛用於大多數軟件的復雜信息流傳遞。

十一、面向切面編程

Aspect Oriented Programming (AOP,面向切面編程) 在 Objective-C 社區內沒有那麼有名,但是 AOP 在運行時可以有巨大威力。在 Objective-C 的世界裡,這意味著使用運行時的特性來為 切面 增加適合的代碼。通過切面增加的行為可以是:
(1)在類的特定方法調用前運行特定的代碼
(2)在類的特定方法調用後運行特定的代碼
(3)增加代碼來替代原來的類的方法的實現

+ (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;
- (id)aspect_hookSelector:(SEL)selector
                      withOptions:(AspectOptions)options
                       usingBlock:(id)block
                            error:(NSError **)error;

比如,下面的代碼會對於執行 MyClass 類的 myMethod: (實例或者類的方法) 執行塊參數。

[MyClass aspect_hookSelector:@selector(myMethod:)
                 withOptions:AspectPositionAfter
                  usingBlock:^(id aspectInfo) {
            ...
        }
                       error:nil];

換一句話說:這個代碼可以讓在 @selector 參數對應的方法調用之後,在一個 MyClass 的對象上(或者在一個類本身,如果方法是一個類方法的話)執行 block 參數。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved