iOS 關(guān)于Block代碼塊的詳解

概述

block

上圖就是一個block簡單使用,它包括了block的聲明、賦值實現(xiàn)調(diào)用 三個部分,其中,實現(xiàn)部分可以看作是一種匿名函數(shù);跟函數(shù)一樣,block也是需要調(diào)用才能執(zhí)行內(nèi)部代碼的;賦值的行為又讓block看起來跟數(shù)據(jù)類型類似

代碼塊Block是在iOS4開始引入的,是對C語言的擴(kuò)展,用來實現(xiàn)匿名函數(shù)的特性

Block是一種特殊的數(shù)據(jù)類型,可以像基本數(shù)據(jù)類型一樣定義成變量、作為參數(shù)、返回值來使用

Block還可以保存一段代碼,在需要的時候調(diào)用

在iOS開發(fā)中,Block被系統(tǒng)應(yīng)在很多地方,例如:GCD、UIView動畫、排序等,我們開發(fā)者也可以應(yīng)用在各類回調(diào)、傳值、傳消息等

Block的聲明、賦值實現(xiàn)、調(diào)用

Block的聲明樣式。

返回類型 (^Block名稱)(參數(shù)列表)

void(^myBlock)(NSString *, NSString *)

Block的返回類型分為有返回類型和無返回類型(void),參數(shù)列表也可有也可以沒有,具體看需求

// 無返回類型無參數(shù)列表
void(^block)();
// 無返回類型有參數(shù)列表
void(^block)(int);
// 有返回類型無參數(shù)列表
int(^block)();
// 有返回列表有參數(shù)列表
int(^block)(int);

Block的簡單使用:聲明、賦值、調(diào)用

Block變量的賦值格式為:

Block變量 = ^(參數(shù)列表){函數(shù)體}; // 這里的參數(shù)列表一定要和聲明時的參數(shù)列表一致

// 聲明
void(^block)();
// 賦值
block = ^(){
    NSLog(@"Hello World");
};
// 調(diào)用
block();

也可以在聲明時完成賦值

// 聲明、賦值
void(^block)() = ^(){
    NSLog(@"Hello World");
};
// 調(diào)用
block();

定義Block類型

前面提到過,Block是一種特殊的數(shù)據(jù)類型,我們可以使用 typedef 來定義 Block 類型,這樣我們就可以使用該類型來聲明很多相同的Block變量了。

typedef 返回類型(^Block名稱)(參數(shù)列表);

示例:

// 聲明一個Block類型
typedef void(^Block)();

// 使用定義兩個block變量
Block myBlock,myNewBlock;
// 賦值實現(xiàn)
myBlock = ^(){
    NSLog(@"Hello World");
};
myNewBlock = ^(){
    NSLog(@"Hello World, I am lolita0164.");
};
// 調(diào)用
myBlock();
myNewBlock();

ARC模式下簡單應(yīng)用

  • 作為對象屬性實現(xiàn)消息傳遞

前面說到,Block保存一段代碼,在需要的時候調(diào)用。我們可以將使用Block的三個步驟拆開,實現(xiàn)消息傳遞、傳值功能。

// 定義Block類型
typedef void(^Block)(NSString *);

@interface Person : NSObject
// 聲明Block變量
@property (nonatomic, copy) Block myBlock;
-(void)sayHello;
@end

@implementation Person
-(void)sayHello{
    // Block調(diào)用
    self.myBlock(@"Hello, I am lolita0164");
}
@end

在定義聲明、調(diào)用之后,還缺少實現(xiàn)的部分,這一步通常由外部實現(xiàn)。

Person *p = [Person new];
// Block賦值實現(xiàn)
p.myBlock = ^(NSString *string) {
    NSLog(@"%@",string);
};
[p sayHello];

這樣,我們就可以在Block的賦值實現(xiàn)部分里拿到 p類里的數(shù)據(jù)了。

  • 作為函數(shù)參數(shù)實現(xiàn)數(shù)據(jù)回調(diào)

我們將之前的例子稍加改動

// 定義Block類型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 將Block作為參數(shù)
-(void)sayHelloUseBlock:(Block)myBlock;
@end

@implementation Person
-(void)sayHelloUseBlock:(Block)myBlock{
    // Block調(diào)用
    myBlock(@"Hello, I am lolita0164");
}
@end

在外部進(jìn)行實現(xiàn)。

Person *p = [Person new];
// Block實現(xiàn)
[p sayHelloUseBlock:^(NSString *string) {
    NSLog(@"%@",string);
}];

作為參數(shù)和作為屬性傳遞消息,在應(yīng)用場景稍稍有些不同。
作為參數(shù)時,通常和當(dāng)前的方法有著緊密的聯(lián)系,函數(shù)體內(nèi)部需要與調(diào)用的外部進(jìn)行交互。例如在請求方法中,經(jīng)常會使用到block進(jìn)行回調(diào),而這個block和當(dāng)前的方法關(guān)系緊密,通常是該方法的結(jié)果回調(diào)。又或者是方法執(zhí)行期間需要外部提供一定的信息,從而通過block獲取外部提供的數(shù)據(jù)。
作為屬性時,通常是和當(dāng)前類相關(guān),作為類與類之間的交互代表。

  • 作為返回值實現(xiàn)鏈?zhǔn)秸Z法

將block作為返回值的經(jīng)典例子就是約束庫 masonry,這個庫在做完每次約束設(shè)置之后通過 block 將實例再次回調(diào),就形成了鏈?zhǔn)秸Z法。

[view makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(view1.mas_right).offset(10); 
    make.top.equalTo(view1).offset(0);
    make.right.equalTo(-10);
    make.size.equalTo(viewWidth);
}];

下面通過創(chuàng)建顏色類來演示 block 作為參數(shù)的使用。

// block 作為返回值
+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
    // block 的聲明和實現(xiàn)
    UIColor* (^rgbBlock)(CGFloat, CGFloat, CGFloat) = ^id(CGFloat r, CGFloat g, CGFloat b) {
        return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
    };
    return rgbBlock;
}

解析

返回值是一個有返回值參數(shù)有三個的block:UIColor * (^)(CGFloat, CGFloat, CGFloat)。我們在該方法的內(nèi)部進(jìn)行了block的聲明和具體實現(xiàn),并且將其作為返回值返回了出去,那么外部在調(diào)用該方法之后接收到的是一個block,可以使用該值。

那么外部使用情況如下。

// 接收 block 類型
UIColor* (^colorBlock)(CGFloat, CGFloat, CGFloat) = [UIColor rgb];
// 使用 block 獲取到顏色
UIColor* color = colorBlock(10,33,65);
self.view.backgroundColor = color;

在丟棄不需要的部分后,代碼如下。

+(UIColor* (^)(CGFloat, CGFloat, CGFloat))rgb{
    return ^id(CGFloat r, CGFloat g, CGFloat b) {
        return [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1];
    };
}
// 使用block作為參數(shù)的方法
self.view.backgroundColor = UIColor.rgb(10, 33, 65);

Block 和 變量

Block訪問局部變量問題

block 內(nèi)部可以訪問局部變量。

int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
Block(); // 輸出 "global = 100"

但是 block 會把變量 復(fù)制 為自己私有的const變量,也就是說block會捕獲棧上的變量(或指針),將其復(fù)制為自己私有的const變量,當(dāng)變量被修改時,不會影響到block自己私有的const變量。

int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block(); // 輸出 "global = 100"

在Block中不可以直接修改局部變量。

int global = 100;
void (^Block)() = ^(){
    global ++; // 這句報錯
    NSLog(@"global = %i", global);
};
Block();

但是可以通過 __block 修飾符修改局部變量。

__block int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block();   //輸出 "global = 101"
__block int global = 100;
void (^Block)() = ^(){
    global ++; // 這句正確
    NSLog(@"global = %i", global);
};
Block();   //輸出 "global = 101"

原因:在局部變量前使用 __block修飾 ,在Block定義時便是將局部變量的指針傳給Block變量所指向的結(jié)構(gòu)體,因此在調(diào)用Block之前對局部變量進(jìn)行修改會影響B(tài)lock內(nèi)部的值,同時內(nèi)部的值也是可以修改的。

Block訪問全局變量、靜態(tài)變量問題

可以訪問和修改。

全局變量所占用的內(nèi)存只有一份,供所有函數(shù)共同調(diào)用,在Block定義時并未將全局變量的值或者指針傳給Block變量所指向的結(jié)構(gòu)體,因此在調(diào)用Block之前對全局變量進(jìn)行修改會影響B(tài)lock內(nèi)部的值,同時內(nèi)部的值也是可以修改的。

在Block定義時便是將靜態(tài)變量的指針傳給Block變量所指向的結(jié)構(gòu)體,因此在調(diào)用Block之前對靜態(tài)變量進(jìn)行修改會影響B(tài)lock內(nèi)部的值,同時內(nèi)部的值也是可以修改的。

ARC下的內(nèi)存管理

在ARC默認(rèn)情況下,Block的內(nèi)存存儲在堆中,ARC會自動進(jìn)行內(nèi)存管理,我們只需要避免循環(huán)引用即可。

// 當(dāng)Block變量出了作用域,Block的內(nèi)存會被自動釋放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();

如果在Block中引用了外面的對象,會對所引用的對象進(jìn)行強(qiáng)引用,但是在Block被釋放時會自動去掉對該對象的強(qiáng)引用,因此比并不會造成內(nèi)存泄漏問題。

Person *p = [[Person alloc] init];
void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();
// Person對象在這里可以正常被釋放
// 注:這里的Block只是單方面的強(qiáng)引用,所以不會產(chǎn)生循環(huán)引用,也不會內(nèi)存泄漏

如果對象內(nèi)部引用一個Block屬性,而在Block內(nèi)部又訪問了該對象,那么會造成循環(huán)引用,導(dǎo)致內(nèi)存泄漏。

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

解決辦法:使用一個弱引用的指針指向該對象,然后在Block內(nèi)部使用該弱引用指針來進(jìn)行操作,這樣就避免了Block對對象進(jìn)行強(qiáng)引用。

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

提示:如果只是Block單方面地對外部變量進(jìn)行強(qiáng)引用,并不會造成內(nèi)存泄漏。

補(bǔ)充

1、聲明block屬性的時候為什么用copy呢?

在說明為什么要用copy前,先思考下block是存儲在棧區(qū)還是堆區(qū)呢?其實block有3種類型:

  1. 全局塊(_NSConcreteGlobalBlock)
  2. 棧塊(_NSConcreteStackBlock)
  3. 堆塊(_NSConcreteMallocBlock)

全局塊存儲在靜態(tài)區(qū)(也叫全局區(qū)),相當(dāng)于OC中的單例;棧塊存儲在棧區(qū),超出作用域則馬上被銷毀。堆塊存儲在堆區(qū)中,是一個帶引用計數(shù)的對象,需要自行管理其內(nèi)存。

關(guān)于內(nèi)存分配,請看這篇:C語言內(nèi)存分配。

怎么判斷一個block所在的存儲位置呢?

  • block不訪問外界變量(包括棧中和堆中的變量)

block既不在棧中也不在堆中,此時就為全局塊,ARC和MRC下都是如此。

  • block訪問外面變量

MRC環(huán)境下:默認(rèn)存儲在棧區(qū)
ARC環(huán)境下:默認(rèn)存儲在堆中,實際上是先放在棧區(qū),在ARC情況下自動又拷貝到堆區(qū),自動釋放

因此,使用 copy 修飾符的作用就是將block從棧區(qū)拷貝到堆區(qū)

為什么要這么做呢?官方給出的答案是:

復(fù)制到堆區(qū)的主要目的就是 保存 block 的狀態(tài),延長其聲明周期。因為block如果在棧上的話,其所屬的變量作用域結(jié)束,該block就被釋放掉了,block中的 __block 變量也同時被釋放掉了,為了解決超出作用域就被釋放的問題,我們就需要把block復(fù)制到堆中。

總結(jié)

OC 中的 block 是對 C 語言的匿名函數(shù)的一種特性是實現(xiàn)。block 具有函數(shù)特性,同時也可以作為變量使用。block 可以作為屬性、參數(shù)、返回值使用。想要在 Block 內(nèi)部修改外部變量時,需要使用 __Block 將變量指針傳遞給 block。在使用 Block 時需要特別注意內(nèi)存泄漏的問題。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容