Objective-C高級編程(中):Block

《Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理》是iOS開發(fā)中一本經(jīng)典書籍,書中有關(guān)ARC、Block、GCD的梳理是iOS開發(fā)進階路上必不可少的知識儲備。筆者讀完此書后為了加強理解,特以筆記記之。本文為中篇,主要談?wù)揙bjective-C中的Block。

Block

鑒于本書翻譯自日文原版且翻譯偏向書面,筆者希望采用通俗的語言記錄,文章結(jié)構(gòu)略有調(diào)整。

本文首發(fā)于Rachal's blog

Block概要

定義Block是帶有自動變量(局部變量)的匿名函數(shù)。

匿名函數(shù)就是不帶名稱的函數(shù)。

C語言的函數(shù)中可能使用的變量:

  • 自動變量(局部變量)
  • 函數(shù)的參數(shù)
  • 靜態(tài)變量(靜態(tài)局部變量)
  • 靜態(tài)全局變量
  • 全局變量

前兩種在超出作用域會銷毀,后三種可以在函數(shù)的多次調(diào)用之間傳遞。

在計算機科學(xué)中,“帶有自動變量值的匿名函數(shù)”這一概念稱為閉包,Block就是Objective-C對閉包的實現(xiàn)。

Block模式

Block語法

Block的完整語法:^ 返回值類型 參數(shù)列表 表達式

^ int (int count) {return count + 1;}

完整形式的Block語法與一般C語言函數(shù)定義相比,僅有兩點不同:

  • 沒有函數(shù)名
  • 帶有^

其中返回值類型和參數(shù)列表可省略,省略后為:^ 表達式

^ {printf("Blocks\n");}

Block變量類型

在Block語法中下,可將Block語法賦值給聲明為Block類型的變量。

int (^blk)(int);// 聲明Block類型

int (^blk1)(int) = ^(int count) {return count + 1};// 變量blk1
int (^blk2)(int);// 變量blk2

blk2 = blk1;// Block類型變量賦值

通過typedef可聲明Block類型變量,函數(shù)定義就變得更容易理解。

typedef int (^blk)(int);

blk blk1 = ^(int count) {return count + 1;};
blk blk2 = blk1; // blk類型變量賦值

截獲自動變量

  • Block可以截獲自動變量的值。
NSInteger aa = 10;

void (^blk)(void) = ^{
    NSLog(@"%ld",aa);
};

aa = 2;

blk();

// 輸出結(jié)果為10
  • Block中使用時向截獲的自動變量進行賦值會出錯。
id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    id obj = [[NSObject alloc] init];

    /* 使用array變量調(diào)方法,沒問題 */
    [array addObject:obj];
};

blk();
id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    /* 想array變量賦值,出錯 */
    array = [[NSMutableArray alloc] init];
};

blk();

__block說明符

若想在Block語法的表達式中將值賦給在Block語法外聲音的自動變量,需要在該自動變量上附加__block說明符。

__block int val = 0;

void (^blk)(void) = ^{val = 1;};

blk();

printf("val = %d\n",val);// 輸出結(jié)果為val = 1;
__block id array = [[NSMutableArray alloc] init];

void (^blk) (void) = ^{

    /* 向array變量賦值,不會出錯 */
    array = [[NSMutableArray alloc] init];
};

blk();

Block實現(xiàn)

Block的實質(zhì)

將Objective-C代碼轉(zhuǎn)化成可讀源碼(C++)的方法:

// 終端cd 源代碼文件夾
clang -rewrite-objc 源代碼文件名

書中將一段Block代碼轉(zhuǎn)化為可讀的C++源碼,簡化源碼,關(guān)注其中的結(jié)構(gòu)體:

struct __main_block_imp_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
}

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
}

Block的結(jié)構(gòu)體中含有isa指針,足以證明Block即為Objective-C的對象

isa = &_NSConcreteStackBlock;

isa被初始化,說明在將Block作為Objective-C的對象處理時,關(guān)于該類的信息放置于_NSCocreteStackBlock中。

三種Block

前面提到了_NSConcreteStackBlock類型的Block,與之對應(yīng)的還有_NSConcreteGlobalBlock_NSConcreteMallocBlock

名稱 Block的類 對象存儲域 復(fù)制效果
棧Block _NSConcreteStackBlock 從棧復(fù)制到堆
堆Block _NSConcreteMallocBlock 引用計數(shù)增加
全局Block _NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)(.data區(qū)) 什么也不做

全局Block產(chǎn)生途徑:

  • 記述全局變量的地方有Block語法時
  • Block語法的表達式中不使用應(yīng)截獲的自動變量時

除此之外的Block語法生成的Block為棧Block,將棧上的Block復(fù)制到堆上,Block就會變成堆Block。

Blobk如何截獲自動變量值

通過clang源碼轉(zhuǎn)換得知:

Block語法表達式中使用的自動變量被作為成員變量追加到Block的結(jié)構(gòu)體中。Block語法表達式中沒有使用的自動變量不會被追加。Block的自動變量截獲只針對Block中使用的自動變量。

所謂“截獲自動變量值”意味著在執(zhí)行Block語法時,Block語法表達式所使用的的自動變量值被保存到Block的結(jié)構(gòu)體實例(即Block自身)中。

__block變量

__block說明符能用來指定Block中想變更值的自動變量

準確的表述方式為“__block存儲域類說明符”,C語言中有以下存儲域類說明符:

  • typedef
  • extern
  • static
  • auto
  • register

它們用于指定將變量值設(shè)置在哪個存儲域中。例如:auto表示作為自動變量存儲在棧中,static表示作為靜態(tài)變量存儲在數(shù)據(jù)區(qū)。

自動變量加上__block,源代碼會急劇增加,變量的轉(zhuǎn)換如下:

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

自動變量轉(zhuǎn)換后竟然變成了結(jié)構(gòu)體實例,即棧上生成的__Block_byref_val_0結(jié)構(gòu)體實例。

結(jié)構(gòu)體聲明如下:

struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *__forwarding;
    int __flags;
    int __size;
    int val;
};

__Block_byref_val_0結(jié)構(gòu)體實例的成員變量__forwarding持有指向該實例自身的指針,也就是原自動變量。

訪問__block變量

另外,__block變量的__Block_byref_val_0結(jié)構(gòu)體并不在Block的結(jié)構(gòu)體中,這樣做是為了在多個Block中使用__block變量。

Block從棧復(fù)制到堆時,__block變量也會一并從棧復(fù)制到堆并被該Block所持有。在多個Block中使用__block變量時,被復(fù)制的Block持有__block變量,并增加__block變量的引用計數(shù)。如果配置在堆上的Block被廢棄,那么它所使用的__block變量也就被釋放。

棧上的__block變量的結(jié)構(gòu)體實例在復(fù)制到堆上時,會將成員變量__forwarding的值替換為復(fù)制目標堆上的__block變量的結(jié)構(gòu)體的地址。

復(fù)制__block變量

通過該功能,無論是在Block語法中、Block語法外使用__block變量,還是__block變量配置在棧上或堆上,都可以順利訪問同一個__block變量

Block截獲的對象和__block變量

- (void)blockTest {

    blk_t blk;

    {
        id array = [[NSMutableArray alloc] init];

        blk = ^(id obj){

        [array addObject:obj];

            NSLog(@"array count is %ld", [array count]);
        };
    }

    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
    blk([[NSObject alloc] init]);
}

以上blockTest方法在ARC無效和有效情況下執(zhí)行結(jié)果:

  • ARC無效時,程序會強制結(jié)束。因為對象array超出作用域后不存在。
  • ARC有效時,輸出如下:
array count is 1
array count is 2
array count is 3

ARC有效時,大部分情況下編譯器會適當?shù)倪M行判斷,自動生成將Block從棧上復(fù)制到堆上的代碼。被棧上Block截獲的對象可以超出作用域而存在。

通過編譯器轉(zhuǎn)換后的源碼查看Block結(jié)構(gòu)體,發(fā)現(xiàn)Block結(jié)構(gòu)體中有__strong修飾的成員變量array

struct __main_block_impl_0 {
    struct __blobk_impl impl;
    struct __main_block_desc_0* Desc;
    id __strong array;
}

C語言結(jié)構(gòu)體中不能含有__strong修飾的變量,因為編譯器不能很好地對其內(nèi)存管理。但是Objective-C運行時庫能夠準確把握從棧復(fù)制到堆上的Block被廢棄的時機,恰當?shù)貙ψ兞窟M行初始化和廢棄

在此對比截獲對象和使用__block變量時持有(copy)和廢棄(dispose)函數(shù)源碼:

  • 截獲對象:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
  • 使用__block變量:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
    _Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

通過對比發(fā)現(xiàn):

  • 截獲對象和__block變量使用_Block_object_assign函數(shù)
  • 廢棄對象和__block變量使用_Block_object_dispose函數(shù)
  • 通過Block_FIELD_IS_OBJECTBLOCK_FIELD_IS_BYREF參數(shù)區(qū)分copydispose函數(shù)的對象類型是對象還是__block變量。

由此可知:Block中使用的賦值給__strong修飾(ARC下常省略)的自動變量的對象和復(fù)制到堆上的__block變量,由于被堆上的Block所持有,因而可超出其變量作用域而存在。

棧上的Block復(fù)制到堆上的時機

  • 調(diào)用Block的copy方法時
  • Block作為函數(shù)返回值返回時
  • 將Block賦值給__strong修飾的id類型或Block類型變量時
  • 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時

棧上的Block為何需要復(fù)制到堆上

將棧上的Block復(fù)制到堆上,這樣即使Block記述的變量作用域結(jié)束,堆上的Block還可以繼續(xù)存在。

- (void)blockTest {
    id obj = [self getBlockArray];

    typedef void (^blk_t)(void);

    blk_t blk = (blk_t)[obj objectAtIndex:0];

    blk();
}

- (id)getBlockArray {
    int val = 10;

    return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);},nil];
}

執(zhí)行以上blockTest方法會出錯,原因在于變量作用域。如下修改后沒問題:

- (id)getBlockArray {
    int val = 10;

    return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy],[^{NSLog(@"blk1:%d",val);} copy],nil];
}

注意:將Block從棧上復(fù)制到堆上是相當消耗CPU的

Block循環(huán)引用

Block為何會引起循環(huán)引用

Block中使用__strong修飾的對象類型自動變量,當Block從棧復(fù)制到堆時,該對象為Block所持有,這樣容易引起循環(huán)引用。

typedef void(^blk_t) (void);

    @interface MyObject : NSObject {
    blk_t _blk;
}

@end


@implementation MyObject

- (id)init {

    self = [super init];

    _blk = ^{
        NSLog(@"self = %@", self);//循環(huán)引用警告
        };

    return self;
}

@end


int main() {

    id o = [[MyObject alloc] init];
    NSLog(@"%@", o);

    return 0;
}

MyObject類對象實例self持有Block,Block語法中使用了self,Block從棧復(fù)制到堆并持有所使用的self。self和Block相互持有,引起循環(huán)引用。

__weak解決循環(huán)引用

為避免循環(huán)引用,可聲明__weak修飾的變量,并將self賦值使用。

- (id)init {

    self = [super init];

    id __weak tmp = self;

    _blk = ^{
        NSLog(@"self = %@", tmp);
        };

    return self;
}

__weak打破了Block對self的強引用,解決了循環(huán)引用問題。

另外,Block內(nèi)使用MyObject類的實例變量也會引起循環(huán)引用,因為會造成Block對self的間接持有。同樣使用__weak可解決循環(huán)引用問題。

typedef void(^blk_t) (void);

@interface MyObject : NSObject {
    blk_t _blk;
    id _obj;
}

@end


@implementation MyObject

- (id)init {

    self = [super init];

    id __weak tmp = _obj;

    _blk = ^{
        NSLog(@"_obj = %@", _obj);
    };

    return self;
}

@end

__block解決循環(huán)引用

使用__block變量也可以避免循環(huán)引用。

typedef void(^blk_t) (void);

@interface MyObject : NSObject {
    blk_t _blk;
}

- (void)execBlock;

@end


@implementation MyObject

- (id)init {

    self = [super init];

    __block id tmp = self;

    _blk = ^{

        NSLog(@"self = %@", tmp);
        tmp = nil;
    };

    return self;
}

- (void)execBlock {
    blk();
}

@end


int main() {

    id o = [[MyObject alloc] init];
    [o execBlock];

    return 0;
}

使用__block變量來避免循環(huán)引用注意以下兩點:

  • __block變量在使用完時要被賦值nil
  • 必須執(zhí)行Block

PS:筆者認為這種方式其實是在模擬__weak功能。因為__weak修飾的變量在超出作用域銷毀時會自動被賦值nil,而執(zhí)行Block剛好就是促使變量被賦值nil這一操作執(zhí)行。

注意:ARC有效和無效時,__block說明符的區(qū)別很大。ARC無效時,__block說明符用來避免Block中的循環(huán)引用,因為有__block說明符的變量不會被retain

以上為Block篇的學(xué)習(xí)內(nèi)容。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 世界上最美妙的聲音是來自孩子的,因為純真。我們每個人都曾經(jīng)純真過,只是歲月的風(fēng)一點點把它吹走,我們自以為滄海桑田的...
    遇見思享閱讀 292評論 0 0
  • 今天大年初二,那我們就接著說說大年初二那些事。 NO.1 回娘家。 大年初二,出嫁的女...
    大唐坐在白日夢上閱讀 252評論 0 0
  • 我不是一個擅長堅持的人,從來都不是。 曾經(jīng)喜歡過吉他,兜兜轉(zhuǎn)轉(zhuǎn)到大學(xué)才報個了班學(xué),不到兩個月便棄掉了,原因無非骨子...
    無理七閱讀 141評論 3 1