iOS編寫高質量Objective-C程式碼(六)

思齊巴巴發表於2018-10-29

《編寫高質量OC程式碼》已經順利完成一二三四五六七篇!
附上鍊接:

iOS 編寫高質量Objective-C程式碼(一)—— 簡介
iOS 編寫高質量Objective-C程式碼(二)—— 物件導向
iOS 編寫高質量Objective-C程式碼(三)—— 介面和API設計
iOS 編寫高質量Objective-C程式碼(四)—— 協議與分類
iOS 編寫高質量Objective-C程式碼(五)—— 記憶體管理機制
iOS 編寫高質量Objective-C程式碼(六)—— block專欄
iOS 編寫高質量Objective-C程式碼(七)—— GCD專欄


本篇的主題是iOS中的 “Block的原理及應用”

先簡單介紹一下今天的主角:block

  • block(塊):是一種 “ 詞法閉包 ”,通過block,開發者可將程式碼塊像物件一樣傳遞。

一、理解“block”的概念:

1. block的資料結構:

通過clang命令列工具(OC轉C++),我們先來看一下block的內部資料結構大概是什麼樣子的?

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

解析:很顯然,Block_layout是一個結構體:裡面有一個isa指標,指向Class物件。還有一個函式指標,指向了塊的實現程式碼。

block的資料結構


2. block的三種型別:全域性塊、棧塊、堆塊。

根據block在記憶體中的位置,block被分成三種型別:

型別 記憶體位置 介紹
__NSStackBlock__ 棧區 棧內有效,出棧後銷燬。
__NSMallocBlock__ 堆區 copy到堆空間上。可以在定義的那個範圍之外使用。
__NSGlobalBlock__ 全域性區 不捕捉任何外部變數,全部資訊在編譯器就已確定。

  • 1. NSStackBlock 棧塊:
    棧塊儲存於棧區,超出變數作用域,棧上的block以及宣告的_block都會被銷燬。

例如:

__block NSString *name = @"QiShare";
void (^block)(void) = ^{
    NSLog(@"%@ is an iOS team which loves to share technology.", name);
};
NSLog(@"block = %@", block);

小知識點:當block內部需要修改或訪問外部變數時,外部變數需要額外用__block修飾。否則修改不了。

我們來看下列印:

ARC的場景

什麼?居然是__NSMallocBlock__(堆塊)?

那是因為ARC環境下,編譯器自動幫我們加了copy操作。

這時我們關掉ARC:設定Objective-C Automatic Reference Counting = NO。再來看下列印:

MRC場景


  • 2. NSMallocBlock 堆塊:
    堆block記憶體位於堆區,在變數作用域結束時依然可以使用。

通過上面的例子:

在ARC下,block會預設加上copy操作:變成__NSMallocBlock__


  • 3. NSGlobalBlock 全域性塊:
    塊中無任何外界物件,所需的記憶體在編譯時就可以確定,記憶體位於全域性區。

類似於“單例”,copy是一個空操作。

例如:

void (^qiShare)(void) = ^{
    
    NSLog(@"We love sharing.");
};
NSLog(@"%@",qiShare);

二、為常用的block型別建立typedef

為了增加程式碼的可讀性可擴充性
需要為常用的block起個別名。

typedef為塊起別名,也可令塊變數用起來更加簡單~
比如:

- (void)getDataWithToken:(NSString *)token success:(void (^)(id responseDic))success;

//! 以上要改成下面這種
typedef void (^SuccessBlock)(id responseDic);
- (void)getDataWithToken:(NSString *)token success:(SuccessBlock)success;

三、用handler塊降低程式碼分散程度

在我們iOS開發中,經常會非同步執行一些任務,等待任務執行結束後再通知物件呼叫相關方法。
一般有兩種做法:

  • 第一種:使用NSNotificationCenter:NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
  • 第二種:使用委託協議:詳情見[iOS 編寫高質量Objective-C程式碼(四)]()。
  • 第三種:使用block回撥:直接把block物件當做引數傳給相關方法執行。

舉個例子:AFNetworking的API設計及使用就是block回撥

  • 介面設計:
- (NSURLSessionDataTask *)POST:(NSString *)URLString
                    parameters:(id)parameters
                       success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                       failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure {

    return [self POST:URLString parameters:parameters progress:nil success:success failure:failure];
}
  • 使用:
    AFHTTPSessionManager *manger =[AFHTTPSessionManager manager];
    NSString *urlString = @"";
    NSMutableDictionary *parameter= @{@"":@"",@"":@""};
    
    [manger POST:urlString
            parameters:parameter 
            success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
                NSLog(@"成功");
            } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
                NSLog(@"%@",error);
            }];
}

四、用block引用其所屬物件時避免出現迴圈引用

在我們日常開發中,如果block使用不當,很容易導致記憶體洩漏。

  • 理由:如果block被當前ViewController(self)持有,這時,如果block內部再持有ViewController(self),就會造成迴圈引用。
  • 解決方案:在block外部弱化self,再在block內部強化已經弱化的weakSelf

For Example:

__weak typeof(self) weakSelf = self;

[self.operationQueue addOperationWithBlock:^{

    __strong typeof(weakSelf) strongSelf = weakSelf;

    if (completionHandler) {

        KTVHCLogDataStorage(@"serial reader async end, %@", request.URLString);      
        completionHandler([strongSelf serialReaderWithRequest:request]);
    }
}];

當然,也不是所有block中使用到self都要先弱化成weakSelf,再強化成strongSelf
只要block沒有被self所持有的,在block中就可以使用self
比如下面:

[QiNetwork requestBlock:^(id responsObject) {
      NSLog(@"%@",self.name);
  }];

另外,記憶體洩漏檢測相關詳情請看:iOS 記憶體洩漏排查方法及原因分析


相關文章