天天看點

Block 詳解

Block 詳解

原文連結:

www.imlifengfeng.com

一、概述

閉包 = 一個函數「或指向函數的指針」+ 該函數執行的外部的上下文變量「也就是自由變量」;Block 是 Objective-C 對于閉包的實作。

其中,Block:

  • 可以嵌套定義,定義 Block 方法和定義函數方法相似
  • Block 可以定義在方法内部或外部
  • 隻有調用 Block 時候,才會執行其{}體内的代碼
  • 本質是對象,使代碼高聚合

使用 clang 将 OC 代碼轉換為 C++ 檔案檢視 block 的方法:

  • 在指令行輸入代碼 clang -rewrite-objc 需要編譯的OC檔案.m
  • 這時檢視目前的檔案夾裡 多了一個相同的名稱的 .cpp 檔案,在指令行輸入 open main.cpp 檢視檔案

二、Block的定義與使用

1、無參數無傳回值

//1,無參數,無傳回值,聲明和定義

void(^MyBlockOne)(void) = ^(void){

NSLog(@"無參數,無傳回值");  

};  
MyBlockOne();//block的調用           

2、有參數無傳回值

//2,有參數,無傳回值,聲明和定義

void(^MyblockTwo)(int a) = ^(int a){

NSLog(@"@ = %d我就是block,有參數,無傳回值",a);

  };  
MyblockTwo(100);           

3、有參數有傳回值

//3,有參數,有傳回值

int(^MyBlockThree)(int,int) = ^(int a,int b){    

  NSLog(@"%d我就是block,有參數,有傳回值",a + b);returna + b; 

 };  
MyBlockThree(12,56);           

4、無參數有傳回值(很少用到)

//4,無參數,有傳回值

int(^MyblockFour)(void) = ^{NSLog(@"無參數,有傳回值");
        return45;
  };
MyblockFour();           

5、實際開發中常用typedef 定義Block

例如,用typedef定義一個block:

typedef int (^MyBlock)(int , int);           

這時,MyBlock就成為了一種Block類型

在定義類的屬性時可以這樣:

@property (nonatomic,copy) MyBlock myBlockOne;           

使用時:

self.myBlockOne = ^int (int ,int){
    //TODO
}           

三、Block與外界變量

1、截獲自動變量(局部變量)值

(1)預設情況

對于 block 外的變量引用,block 預設是将其複制到其資料結構中來實作通路的。也就是說block的自動變量截獲隻針對block内部使用的自動變量, 不使用則不截獲, 因為截獲的自動變量會存儲于block的結構體内部, 會導緻block體積變大。特别要注意的是預設情況下block隻能通路不能修改局部變量的值。

[圖檔上傳中...(image-73164a-1566284363727-7)]

int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();           

輸出結果:

age = 10           

(2) __block 修飾的外部變量

對于用 __block 修飾的外部變量引用,block 是複制其引用位址來實作通路的。block可以修改__block 修飾的外部變量的值。

[圖檔上傳中...(image-d7e15-1566284363727-6)]

__block int age = 10;
myBlock block = ^{
    NSLog(@"age = %d", age);
};
age = 18;
block();           

輸出為:

age = 18           

為什麼使用__block 修飾的外部變量的值就可以被block修改呢?

我們使用 clang 将 OC 代碼轉換為 C++ 檔案:

clang -rewrite-objc 源代碼檔案名           

便可揭開其真正面紗:

__block int val = 10;
轉換成
__Block_byref_val_0 val = {
    0,
    &val,
    0,
    sizeof(__Block_byref_val_0),
    10
};           

會發現一個局部變量加上__block修飾符後竟然跟block一樣變成了一個__Block_byref_val_0結構體類型的自動變量執行個體!!!!

此時我們在block内部通路val變量則需要通過一個叫__forwarding的成員變量來間接通路val變量(下面會對__forwarding進行詳解)

四、Block的copy操作

1、Block的存儲域及copy操作

在開始研究Block的copy操作之前,先來思考一下:Block是存儲在棧上還是堆上呢?

我們先來看看一個由C/C++/OBJC編譯的程式占用記憶體分布的結構:

[圖檔上傳中...(image-2a5d0-1566284363727-5)]

其實,block有三種類型:

  • 全局塊(_NSConcreteGlobalBlock)
  • 棧塊(_NSConcreteStackBlock)
  • 堆塊(_NSConcreteMallocBlock)

這三種block各自的存儲域如下圖:

[圖檔上傳中...(image-8a9af1-1566284363727-4)]

  • 全局塊存在于全局記憶體中, 相當于單例.
  • 棧塊存在于棧記憶體中, 超出其作用域則馬上被銷毀
  • 堆塊存在于堆記憶體中, 是一個帶引用計數的對象, 需要自行管理其記憶體

簡而言之,存儲在棧中的Block就是棧塊、存儲在堆中的就是堆塊、既不在棧中也不在堆中的塊就是全局塊。

遇到一個Block,我們怎麼這個Block的存儲位置呢?

(1)Block不通路外界變量(包括棧中和堆中的變量)

Block 既不在棧又不在堆中,在代碼段中,ARC和MRC下都是如此。此時為全局塊。

(2)Block通路外界變量

MRC 環境下:通路外界變量的 Block 預設存儲棧中。

ARC 環境下:通路外界變量的 Block 預設存儲在堆中(實際是放在棧區,然後ARC情況下自動又拷貝到堆區),自動釋放。

ARC下,通路外界變量的 Block為什麼要自動從棧區拷貝到堆區呢?

棧上的Block,如果其所屬的變量作用域結束,該Block就被廢棄,如同一般的自動變量。當然,Block中的__block變量也同時被廢棄。如下圖:

[圖檔上傳中...(image-ea535c-1566284363727-3)]

為了解決棧塊在其變量作用域結束之後被廢棄(釋放)的問題,我們需要把Block複制到堆中,延長其生命周期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要将Block從棧複制到堆,如果有,自動生成将Block從棧上複制到堆上的代碼。Block的複制操作執行的是copy執行個體方法。Block隻要調用了copy方法,棧塊就會變成堆塊。

如下圖:

[圖檔上傳中...(image-a178b3-1566284363727-2)]

例如下面一個傳回值為Block類型的函數:

typedef int (^blk_t)(int);

blk_t func(int rate) {
    return ^(int count) { return rate * count; };
}           

分析可知:上面的函數傳回的Block是配置在棧上的,是以傳回函數調用方時,Block變量作用域就結束了,Block會被廢棄。但在ARC有效,這種情況編譯器會自動完成複制。

在非ARC情況下則需要開發者調用copy方法手動複制,由于開發中幾乎都是ARC模式,是以手動複制内容不再過多研究。

将Block從棧上複制到堆上相當消耗CPU,是以當Block設定在棧上也能夠使用時,就不要複制了,因為此時的複制隻是在浪費CPU資源。

Block的複制操作執行的是copy執行個體方法。不同類型的Block使用copy方法的效果如下表:

[圖檔上傳中...(image-cc36bd-1566284363727-1)]

根據表得知,Block在堆中copy會造成引用計數增加,這與其他Objective-C對象是一樣的。雖然Block在棧中也是以對象的身份存在,但是棧塊沒有引用計數,因為不需要,我們都知道棧區的記憶體由編譯器自動配置設定釋放。關于堆區和棧區詳細内容可以參考下峰哥之前的文章:《

總結:堆、棧、隊列

不管Block存儲域在何處,用copy方法複制都不會引起任何問題。在不确定時調用copy方法即可。

在ARC有效時,多次調用copy方法完全沒有問題:

blk = [[[[blk copy] copy] copy] copy];
// 經過多次複制,變量blk仍然持有Block的強引用,該Block不會被廢棄。           

2、__block變量與__forwarding

在copy操作之後,既然__block變量也被copy到堆上去了, 那麼通路該變量是通路棧上的還是堆上的呢?__forwarding 終于要閃亮登場了,如下圖:

[圖檔上傳中...(image-16f5c5-1566284363726-0)]

通過__forwarding, 無論是在block中還是 block外通路__block變量, 也不管該變量在棧上或堆上, 都能順利地通路同一個__block變量。

五、防止 Block 循環引用

Block 循環引用的情況:

某個類将 block 作為自己的屬性變量,然後該類在 block 的方法體裡面又使用了該類本身,如下:

self.someBlock = ^(Type var){
    [self dosomething];
};           

解決辦法:

(1)ARC 下:使用 __weak

__weak typeof(self) weakSelf = self;
self.someBlock = ^(Type var){
   [weakSelf dosomething];
};           

(2)MRC 下:使用 __block

__block typeof(self) blockSelf = self;
self.someBlock = ^(Type var){
   [blockSelf dosomething];
};           

值得注意的是,在ARC下,使用 __block 也有可能帶來的循環引用,如下:

// 循環引用 self -> _attributBlock -> tmp -> self
typedef void (^Block)();
@interface TestObj : NSObject
{
    Block _attributBlock;
}
@end

@implementation TestObj
- (id)init {
    self = [super init];
    __block id tmp = self;
    self.attributBlock = ^{
        NSLog(@"Self = %@",tmp);
        tmp = nil;
   };
}

- (void)execBlock {
    self.attributBlock();
}
@end

// 使用類
id obj = [[TestObj alloc] init];
[obj execBlock]; // 如果不調用此方法,tmp 永遠不會置 nil,記憶體洩露會一直在           

六、Block的使用示例

1、Block作為變量(Xcode快捷鍵:inlineBlock)

int (^sum) (int, int); // 定義一個 Block 變量 sum
// 給 Block 變量指派
// 一般 傳回值省略:sum = ^(int a,int b)…
sum = ^int (int a,int b){  
    return a+b;
}; // 指派語句最後有 分号
int a = sum(10,20); // 調用 Block 變量           

2、Block作為屬性(Xcode 快捷鍵:typedefBlock)

// 1\. 給  Calculate 類型 sum變量 指派「下定義」
typedef int (^Calculate)(int, int); // calculate就是類型名
Calculate sum = ^(int a,int b){ 
    return a+b;
};
int a = sum(10,20); // 調用 sum變量

// 2\. 作為對象的屬性聲明,copy 後 block 會轉移到堆中和對象一起
@property (nonatomic, copy) Calculate sum;    // 使用   typedef
@property (nonatomic, copy) int (^sum)(int, int); // 不使用 typedef

// 聲明,類外
self.sum = ^(int a,int b){
    return a+b;
};
// 調用,類内
int a = self.sum(10,20);           

3、作為 OC 中的方法參數

// ---- 無參數傳遞的 Block ---------------------------
// 實作
- (CGFloat)testTimeConsume:(void(^)())middleBlock {
    // 執行前記錄下目前的時間
    CFTimeInterval startTime = CACurrentMediaTime();
    middleBlock();
    // 執行後記錄下目前的時間
    CFTimeInterval endTime = CACurrentMediaTime();
    return endTime - startTime;

}

// 調用
[self testTimeConsume:^{
       // 放入 block 中的代碼 

}];

// ---- 有參數傳遞的 Block ---------------------------
// 實作
- (CGFloat)testTimeConsume:(void(^)(NSString * name))middleBlock {
    // 執行前記錄下目前的時間
    CFTimeInterval startTime = CACurrentMediaTime();
    NSString *name = @"有參數";
    middleBlock(name);
    // 執行後記錄下目前的時間
    CFTimeInterval endTime = CACurrentMediaTime();
    return endTime - startTime;
}

// 調用
[self testTimeConsume:^(NSString *name) {
   // 放入 block 中的代碼,可以使用參數 name
   // 參數 name 是實作代碼中傳入的,在調用時隻能使用,不能傳值    

}];           

4、Block回調

Block回調是關于Block最常用的内容,比如網絡下載下傳,我們可以用Block實作下載下傳成功與失敗的回報。開發者在block沒釋出前,實作回調基本都是通過代理的方式進行的,比如負責網絡請求的原生類NSURLConnection類,通過多個協定方法實作請求中的事件處理。而在最新的環境下,使用的NSURLSession已經采用block的方式處理任務請求了。各種第三方網絡請求架構也都在使用block進行回調處理。這種轉變很大一部分原因在于block使用簡單,邏輯清晰,靈活等原因。

如下:

//DownloadManager.h
#import <Foundation/Foundation.h>

@interface DownloadManager : NSObject <NSURLSessionDownloadDelegate>

// block 重命名
typedef void (^DownloadHandler)(NSData * receiveData, NSError * error);

- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler ;

@end           
//DownloadManager.m
#import "DownloadManager.h"

@implementation DownloadManager

- (void)downloadWithURL:(NSString *)URL parameters:(NSDictionary *)parameters handler:(DownloadHandler)handler
{
    NSURLRequest * request = [NSURLRequest requestWithURL:[NSURL URLWithString:URL]];
    NSURLSession * session = [NSURLSession sharedSession];

    //執行請求任務
    NSURLSessionDataTask * task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (handler) {
            dispatch_async(dispatch_get_main_queue(), ^{
                handler(data,error);
            });
        }
    }];
    [task resume];

}           

上面通過封裝NSURLSession的請求,傳入一個處理請求結果的block對象,就會自動将請求任務放到工作線程中執行實作,我們在網絡請求邏輯的代碼中調用如下:

- (IBAction)buttonClicked:(id)sender {
    #define DOWNLOADURL @"https://codeload.github.com/AFNetworking/AFNetworking/zip/master"
    //下載下傳類
    DownloadManager * downloadManager = [[DownloadManager alloc] init];
    [downloadManager downloadWithURL: DOWNLOADURL parameters:nil handler:^(NSData *receiveData, NSError *error) {
        if (error) {
            NSLog(@"下載下傳失敗:%@",error);
        }else {
            NSLog(@"下載下傳成功,%@",receiveData);
        }
    }];
}           

為了加深了解,再來一個簡單的小例子:

A,B兩個界面,A界面中有一個label,一個buttonA。點選buttonA進入B界面,B界面中有一個UITextfield和一個buttonB,點選buttonB退出B界面并将B界面中UITextfield的值傳到A界面中的label。

A界面中,也就是ViewController類中:

//關鍵demo:
- (IBAction)buttonAction {  
    MyFirstViewController *myVC = [[MyFirstViewController alloc] init];
    [self presentViewController:myVC animated:YES completion:^{    
    }];
    __weak typeof(self) weakSelf = self;//防止循環引用
//用屬性定義的注意:這裡屬性是不會自動補全的,方法就會自動補全
    [myVC setBlock:^(NSString *string){
        weakSelf.labelA.text = string;
    }];
}           

B界面中,也就是MyFirstViewController類中.m檔案:

- (IBAction)buttonBAction {
    [self dismissViewControllerAnimated:YES completion:^{
    }];
      self.block(_myTextfielf.text);
}           

.h檔案:

#import <UIKit/UIKit.h>

//typedef定義一下block,為了更好用
typedef void(^MyBlock)(NSString *string);

@interface MyFirstViewController : UIViewController

@property (nonatomic, copy) MyBlock block;

@end           

看了以上兩個Block回調示例,是不是感覺比delegate清爽了不少?

推薦文集

iOS面試題合集

iOS基礎通用面經