iOS block深入淺出

概要

block就是帶有自動(dòng)變量的匿名函數(shù)。

語法結(jié)構(gòu)如下:

^ 返回值類型 參數(shù)列表 表達(dá)式

其中返回值類型void時(shí)可省略,同理參數(shù)列表;

block變量結(jié)構(gòu)同C語言函數(shù)指針類似,只是將*換為^符號(hào):

int (^block)(int) = ^(int)(int a){
        NSLog(@"a:%d", a);
};

與通常的變量相同,block變量可賦值操作,也可作為函數(shù)的參數(shù)傳遞,也可作為返回值傳遞;

對(duì)于block類型變量,通常使用typedef定義,如下:

typedef int (^Block) (int);
//上面可修改為
Block block = ^(int)(int a){
        NSLog(@"a:%d", a);
}

對(duì)于截獲自動(dòng)變量說明,類似c語言中的值傳遞拷貝:若想對(duì)截獲的自動(dòng)變量數(shù)值類型變量進(jìn)行同步修改,需要使用__block說明符。

原理分析

block本質(zhì)

通過clang(v1100.0.33.17)編譯器自帶的-rewirte-objc選項(xiàng)將objc代碼轉(zhuǎn)換為c++進(jìn)行分析,且都是基于ARC模式(添加-fobjc-arc),代碼如下:

void (^block)(void) = ^{
    printf("block fired\n");
};

block();

轉(zhuǎn)換后的代碼核心代碼如下:

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

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("block fired\n");
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
  
  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

先從block實(shí)現(xiàn)最基本的結(jié)構(gòu)體說起,其結(jié)構(gòu)體信息如下:

struct __block_impl {
  void *isa;//isa指針
  int Flags;//標(biāo)志位
  int Reserved;//保留位
  void *FuncPtr;//函數(shù)指針
};

其中初始化構(gòu)造完成后的isa指針指向_NSConcreteStackBlockFuncPtr函數(shù)指針指向具體的block實(shí)現(xiàn);如果對(duì)runtime原理熟悉的話,這個(gè)isa是不是似曾相識(shí),其指向的是類對(duì)象進(jìn)而指向元對(duì)象,最后指向root object;runtime通過isa指針使用c語言構(gòu)造了一套完整的面向?qū)ο髣?dòng)態(tài)語言。為保持完整的面向?qū)ο蟮捏w系,block也使用了isa來指向類對(duì)象,這里指向的是_NSConcreteStackBlock,后續(xù)會(huì)對(duì)該類對(duì)象重點(diǎn)分析,因此,block其實(shí)也是一個(gè)object對(duì)象;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __main_block_impl_0結(jié)構(gòu)體其實(shí)就是c++構(gòu)造的public對(duì)象,包含了__block_impl類實(shí)例對(duì)象及struct __main_block_desc_0結(jié)構(gòu)體對(duì)象(包含了類實(shí)例對(duì)象的大小);

block的調(diào)用函數(shù)如下,其中入?yún)⒛J(rèn)包含了struct __main_block_impl_0 *類型的__cself,與objcc++中的selfthis不謀而合,即是隱含傳遞了block的實(shí)例對(duì)象;

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  printf("block fired\n");
}

具體的block()調(diào)用就是調(diào)用實(shí)例對(duì)象中指向的函數(shù)指針來實(shí)現(xiàn);

block對(duì)象源碼分析
在源碼Block_private.h中的定義如下:

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

typedef void(*BlockInvokeFunction)(void *, ...);
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
};

c++重寫的結(jié)構(gòu)體其實(shí)是一樣的,結(jié)構(gòu)圖(較老版本)如下所示:

Block_layout結(jié)構(gòu)圖

__block說明符

對(duì)于block中具有截獲的自動(dòng)變量值未使用__block說明符時(shí),其實(shí)就是在__main_block_impl_0中添加相應(yīng)的實(shí)例成員,并通過構(gòu)造函數(shù)時(shí)通過值傳遞捕獲自動(dòng)變量值;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;//截獲的自動(dòng)變量
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {//構(gòu)造時(shí)初始化實(shí)例變量
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  printf("block fired, a=%d\n", a);
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
  int a = 1;
  //值傳參
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

對(duì)用使用__block說明符后的轉(zhuǎn)換代碼如下:

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

  printf("block fired, a=%d\n", (a->__forwarding->a));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) 
{_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, const char * argv[]) {
  __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
  void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

  ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

  return 0;
}

與不含__block說明符不同之處在于:__main_block_impl_0結(jié)構(gòu)體中使用了struc __Block_byref_a_0對(duì)象及__block對(duì)象__main_block_copy_0__main_block_dispose_0對(duì)象copy/dispose拷貝/釋放函數(shù),替換了簡(jiǎn)單地對(duì)應(yīng)的實(shí)例變量成員;并且struc __Block_byref_a_0結(jié)構(gòu)體中的成員__Block_byref_a_0 *__forwarding初始化指向了block實(shí)例對(duì)象成員自身(這個(gè)為啥多了該成員變量且指向自身后文闡述),如下圖所示

Block_byref結(jié)構(gòu)圖

Block_private.h__block源碼實(shí)現(xiàn)如下:

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

c++轉(zhuǎn)換的實(shí)質(zhì)的一樣的;

上面截獲的自動(dòng)變量需要使用__block說明符來修改其值,但對(duì)于靜態(tài)變量、靜態(tài)全局變量及全局變量如何呢?

可以從值傳遞拷貝的原理分析,block匿名函數(shù)主要就是用于保存block函數(shù)內(nèi)部變量值用于后續(xù)調(diào)用,因此存在作用域的問題,對(duì)于靜態(tài)全局變量及全局變量而言,因此不存在訪問不到這些變量的情況,即block不會(huì)主動(dòng)去截獲這些變量,也就不需要使用__block說明符;但對(duì)于靜態(tài)變量而言,超過作用域就無法訪問,因此需要截獲此類型變量的地址,具體如下:

//源碼如下:
{
    static int a = 1;
  void (^block)(void) = ^{
  printf("block fired, a=%d\n", a);
  };
}
//轉(zhuǎn)換后的c++代碼如下
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

block存儲(chǔ)域

以上分析的都是局部變量類型的block變量,其類對(duì)象類型為_NSConcreteStackBlock,若是靜態(tài)類型或者全局類型則如何,是否與變量類型存儲(chǔ)類型一致。

直接上代碼并clang轉(zhuǎn)換分析:

typedef void (^Block)(int);
Block g_block1 = ^(int count){
    printf("block1, count:%d", count);
};
Block g_block2, g_block3;

int main(int argc, const char * argv[]) {
    int a = 1, b = 2;
    g_block2 = ^(int count) {
        printf("block2, count:%d, b:%d", count, b);
    };
  g_block3 = ^(int count){
    printf("block3, count:%d", count);
  };
    
    g_block1(a);
    g_block2(a);
    
    return 0;
}

轉(zhuǎn)換后的關(guān)鍵結(jié)構(gòu)體如下:

//g_block1對(duì)應(yīng)的結(jié)構(gòu)體
struct __g_block1_block_impl_0 {
  struct __block_impl impl;
  struct __g_block1_block_desc_0* Desc;
  __g_block1_block_impl_0(void *fp, struct __g_block1_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//g_block2對(duì)應(yīng)的結(jié)構(gòu)體
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _b, int flags=0) : b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//g_block3對(duì)應(yīng)的結(jié)構(gòu)體
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

g_block2 = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, b));
g_block3 = ((void (*)(int))&__main_block_impl_1((void *)__main_block_func_1, &__main_block_desc_1_DATA));

都是全局變量類型block但是其類對(duì)象類型不同,對(duì)于未截獲自動(dòng)變量的g_block1則為_NSConcreteGlobalBlock,對(duì)于需要截獲自動(dòng)變量的g_block2則為_NSConcreteStackBlock;但block類對(duì)象類型與《objective-C 高級(jí)編程 iOS與OSX多線程和內(nèi)存管理》存在差異,并且轉(zhuǎn)換后的c++代碼未發(fā)現(xiàn)引用計(jì)數(shù)相關(guān)的函數(shù)調(diào)用,如_objc_retainBlock,帶著疑惑將源碼轉(zhuǎn)換為匯編實(shí)現(xiàn)(通過xcode->Product->Perform Action->Assemble "xxxxx")一探究竟:

Assemble

Assemble

而實(shí)際的匯編實(shí)現(xiàn)block類對(duì)象類型為_NSConcreteGlobalBlock,并且對(duì)于ARC模式下自動(dòng)添加了_objc_retainBlock/_objc_release函數(shù)調(diào)用來實(shí)現(xiàn)自動(dòng)引用計(jì)數(shù),實(shí)際的xcode編譯環(huán)節(jié)選項(xiàng)中也不存在-rewrite-objc中間生成c++代碼的過程(可能老的編譯器存在),因此,clang -rewrite-objc轉(zhuǎn)換后的代碼可用于參考內(nèi)部實(shí)現(xiàn),具體要以實(shí)際生成的匯編代碼為準(zhǔn);

【補(bǔ)充說明】在ARC模式下,_NSConcreteStackBlock類型的block對(duì)象都變成了_NSConcreteMallocBlock類型(官方說明中也提到Transitioning to ARC Release Notes),也可以通過demo打印block對(duì)象以驗(yàn)證;

#import <Foundation/Foundation.h>
typedef void (^Block)(int);
int main(int argc, const char * argv[])
{
    int a = 1;
    Block blk = ^(int count){
        printf("%d\n", a);
    };
    blk(a);
    NSLog(@"%@", blk);
  
    __weak Block blk1 = ^(int count){
        printf("%d\n", count);
    };
    NSLog(@"%@", blk1);
  
    __weak Block blk2 = ^(int count){
        printf("%d\n", a);
    };
    NSLog(@"%@", blk2);
    
    NSLog(@"%@", ^{printf("%d\n", a);});
    
    return 0;
}

打印結(jié)果如下:

block類對(duì)象類型

因此,對(duì)于ARC模式下,id類型以及對(duì)象類型變量隱含著__strong修飾符(默認(rèn)使用了Block_copy函數(shù)拷貝),若block變量表達(dá)式含有外部變量,則為_NSConcreteMallocBlock;若表達(dá)式不含有外部變量,則為_NSConcreteGlobalBlock;若使用了__weak修飾符或者未賦值給隱含__strong修飾符的變量時(shí),則為_NSConcreteStackBlock;

具體生成_NSConcreteGlobalBlock類對(duì)象類型的block場(chǎng)景如下:

  • 全局block變量且定義表達(dá)式內(nèi)部不使用應(yīng)被截獲的自動(dòng)變量;
  • block表達(dá)式中不使用應(yīng)被截獲的自動(dòng)變量

與之_NSConcreteStackBlock相對(duì)應(yīng)的存儲(chǔ)類型,包括如下:

  • _NSConcreteStackBlock
  • _NSConcreteGlobalBlock
  • _NSConcreteMallocBlock

存儲(chǔ)類型如圖:


bloc類對(duì)象存儲(chǔ)布局

那何種場(chǎng)景會(huì)是_NSConcreteMallocBlock類型呢,這個(gè)不難想象。對(duì)于上面的存儲(chǔ)類型,若是_NSConcreteStackBlock棧類型,若超過其作用域,則內(nèi)存會(huì)被釋放,即無法再使用;要是需要超過其作用域調(diào)用,則需要定義為_NSConcreteGlobalBlock或者_NSConcreteMallocBlock,但_NSConcreteGlobalBlock類型受限于定義位置使用不能截獲自動(dòng)變量,因此_NSConcreteMallocBlock堆類型應(yīng)運(yùn)而生。

借用《objective-C 高級(jí)編程 iOS與OSX多線程和內(nèi)存管理》書的插圖就很容易理解:

block棧copy到堆原理圖

那何時(shí)block變量會(huì)被從棧上復(fù)制到堆上?總結(jié)如下:

  • 使用copy方法

    如上面demo中使用__weak修飾符且表達(dá)式中會(huì)被截獲自動(dòng)變量作為參數(shù)傳遞時(shí)

  • block作為函數(shù)參數(shù)返回時(shí)

    return ^{};

  • block變量賦值給__strong修飾符id類型的類或者block成員變量時(shí)

    對(duì)于未賦值的block對(duì)象默認(rèn)是棧類型,(ARC模式)賦值給__strong修飾符id類型的類或者block成員變量會(huì)自動(dòng)copy到堆上;

  • GCD相關(guān)的API

  • Cocoa框架方法中含有usingBlock時(shí)

對(duì)于手動(dòng)調(diào)用copy方法時(shí),若重復(fù)調(diào)用會(huì)如何?

重復(fù)copy

__block變量存儲(chǔ)域

上面說明了block變量存儲(chǔ)域,對(duì)于其表達(dá)式中持有__block變量時(shí),__block類型變量(隱含為__strong類型)是否也會(huì)被復(fù)制到堆上?答案:是,且上文中提到的__forwarding成員變量會(huì)指向堆中的結(jié)構(gòu)體實(shí)例,因此無論棧上或者堆上的__block變量都可以訪問同一個(gè)__block變量,如圖所示:

__forwarding變化圖

參考clang官方文檔也可以說明__block變量會(huì)自動(dòng)調(diào)用Block_copy()函數(shù)拷貝到堆上:

In garbage collected environments, the __weak variable is set to nil when the object it references is collected, as long as the __block variable resides in the heap (either by default or via Block_copy()). The initial Apple implementation does in fact start __block variables on the stack and migrate them to the heap only as a result of a Block_copy() operation.

但對(duì)于使用__weak修飾符的__block變量,則不會(huì)進(jìn)行拷貝;

循環(huán)引用

ARC末實(shí)現(xiàn)block表達(dá)式會(huì)自動(dòng)持有外部對(duì)象,若外部對(duì)象又持有該block對(duì)象,就會(huì)導(dǎo)致”循環(huán)引用“問題,如圖所示:

#import <Foundation/Foundation.h>
typedef void (^Block)(int);
@interface MyObject : NSObject
@property (nonatomic, strong) Block blk;
@end

@implementation MyObject
- (id)init {
    self = [super init];
    if (self) {
        _blk = nil;
    }
    
    return self;
}

- (void)dealloc {
    NSLog(@"dealloc");
}
@end

int main(int argc, const char * argv[]) {
    MyObject *obj = [[MyObject alloc]init];
    //方式一:使用__weak修飾符避免引用計(jì)數(shù)增加
    __weak MyObject *weakObj = obj;
    Block blk = ^(int count){
        NSLog(@"count:%d, blk:%@", count, weakObj.blk);
        //方式二:手動(dòng)nil釋放obj對(duì)象,解決循環(huán)引用
//        NSLog(@"count:%d, blk:%@", count, obj.blk);
//        obj.blk = nil;
    };
    NSLog(@"blk:%@", blk);
    
    obj.blk = blk;
    obj.blk(1);
    
    return 0;
}
循環(huán)引用

解決方法:

  • 使用__weak弱引用修飾符避免增加引用計(jì)數(shù)
  • block表達(dá)式內(nèi)部手動(dòng)置持有對(duì)象為nil

小知識(shí)

xcode關(guān)閉arc

對(duì)于單個(gè)源文件關(guān)閉arc使用fno-objc-arc編譯標(biāo)志,對(duì)于整個(gè)工程則修改Objective-C Automatic Reference Counting修改為No;

Reference

libclosure-74

《objective-C 高級(jí)編程 iOS與OSX多線程和內(nèi)存管理》

【LLVM】LLVM編譯流程

談Objective-C block的實(shí)現(xiàn)

淺談 block - clang 改寫后的 block 結(jié)構(gòu)

史上最詳細(xì)的Block源碼剖析

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

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

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