Block 詳解

原文連接:www.imlifengfeng.comgit

1、概述github

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

其中,Block:session

  • 能夠嵌套定義,定義 Block 方法和定義函數方法類似
  • Block 能夠定義在方法內部或外部
  • 只有調用 Block 時候,纔會執行其{}體內的代碼
  • 本質是對象,使代碼高聚合

使用 clang 將 OC 代碼轉換爲 C++ 文件查看 block 的方法:數據結構

  • 在命令行輸入代碼 clang -rewrite-objc 須要編譯的OC文件.m
  • 這時查看當前的文件夾裏 多了一個相同的名稱的 .cpp 文件,在命令行輸入 open main.cpp 查看文件

2、Block的定義與使用閉包

一、無參數無返回值框架

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

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

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

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

二、有參數無返回值async

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

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

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

  };  
MyblockTwo(100);

三、有參數有返回值函數

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

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

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

 };  
MyBlockThree(12,56);

四、無參數有返回值(不多用到)學習

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

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

五、實際開發中經常使用typedef 定義Block

例如,用typedef定義一個block:

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

這時,MyBlock就成爲了一種Block類型
在定義類的屬性時能夠這樣:

@property (nonatomic,copy) MyBlock myBlockOne;

使用時:

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

3、Block與外界變量

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

(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進行詳解)

4、Block的copy操做

一、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不會被廢棄。

二、__block變量與__forwarding

在copy操做以後,既然__block變量也被copy到堆上去了, 那麼訪問該變量是訪問棧上的仍是堆上的呢?__forwarding 終於要閃亮登場了,以下圖:

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

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

5、防止 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,內存泄露會一直在

6、Block的使用示例

一、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 變量

二、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);

三、做爲 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 是實現代碼中傳入的,在調用時只能使用,不能傳值    

}];

四、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清爽了很多?


相關文章
相關標籤/搜索