Block、__block修饰符的实质

1. 将Block转换为普通C语言
通过支持Block的编译器,可以将含有Block语法的源代码转换为一般C语言编译器能给处理的源代码,并作为极为普通的C语言源代码被编译

以下命令在终端中将Objective-C转换为C语言源代码:

clang -rewrite-obj 源码文件

2. Block分析
以下列源码为例:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int val = 50;
        const char *text = "val = %d\n";
        void (^blk)(void) = ^{
            printf(text, val);
        };
        val = 100;
        blk();

    }
    return 0;
}

转换后:

//Block的实现类
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//Block的封装类
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *text;     //block截获的自动变量
  int val;              //block截获的自动变量

  //构造函数,将语法表达时所使用的自动变量保存到结构体内,实现自动变量值的截获
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_text, int _val, int flags=0) : text(_text), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//Block体的实现,__cself相当于C++的this,或Objective-C的self
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *text = __cself->text; 
  int val = __cself->val;
  printf(text, val);
}

//__main_block_desc_0类,__main_block_desc_0_DATA为其对象
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[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int val = 50;
        const char *text = "val = %d\n";
        void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, text, val));
        val = 100;
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    }
    return 0;
}

所以Block的实质为Objective-C的对象,对不同形式Block进行转换后可以的到如下的Block大致结构:

Block结构

上图的变量部分,即是Block截取的自动变量,"截获自动变量"意味着在执行Block语法时,Block语法表达式所使用的自动变量值被保存到Block结构体实例(即Block自身)中,可以理解为Block中存放了一个自动变量的副本

  • Block中的Var部分可以有多个变量,这取决于Block中截取的自动变量的个数
  • 副本类型基本与自动变量的类型一致,若自动变量为静态变量,则副本类型为指针,若自动变量为有__block修饰符,则副本类型为__Block_byref_val类型
  • Block不会截取全局变量和静态全局变量

源码:

int global_val = 1;
static int static_global_val = 2;

int main() {
    static int static_val = 3;
    __block int val = 16;
    void (^blk)(void) = ^{
        global_val *= 1;
        static_global_val *= 2;
        static_val *= 3;
        val *= 16;
    }
}

转换后的Block类结构:

带有`__block`修饰符的变量实际上是存放在`__Block_byref_val`中的
struct __Block_byref_val_0 {
    void *__isa;
    __Block_byref_val_0 *forwarding;
    int __flags;
    int __size;
    int val;
}

struct __main_block_impl_0 {
    //Impl
    struct __block_impl impl;   
    
    //Desc       
    struct __main_block_desc_0 *desc;    
        
    //Var(变量部分)
    int *static_val;
    __Block_byref_val_0 *val;
    
    //这里__Block_byref_val_0对象的调用都是通过内部的forwarding指针操作
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags):val(_val->forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int *static_val = __cself->static_val;
    __Block_byref_val_0 *val = __cself->val;
  
    global_val *= 1;
    static_global_val *= 2;
    (*static_val) *= 3;
    
    //__Block_byref_val类型
    (val->forwarding->val) *= 16;
}
__Block_byref_val_0的结构

Block内部实现套路总结

//Block的实现类
struct Impl {
  void *isa;
  int Flags;
  int Reserved;
  void *funcPtr;
};

//Block的封装类
struct Block {
  struct Impl impl;
  struct Desc * Desc;
  Var val;  //截获的自动变量
 
  //构造函数,将语法表达时所使用的自动变量保存到结构体内,实现自动变量值的截获
  Block(void *funcPtr, struct Desc *desc, Var _val) : val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//funcPtr指向的函数
static void func(struct Block *__cself) {
    //从Block中读取val
    Var val = __cself->val;
    
    //block方法体中的操作
    printf(text, val);
}

//Desc类
static Desc {
  size_t reserved;
  size_t Block_size;
} descData = { 0, sizeof(struct Block)};


int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        Var val = 50;
        const char *text = "val = %d\n";
        blk = &Block(&func, descData, val);
        blk->funcPtr(blk);
    }
    return 0;
}

像静态变量截获为指针的方式能够达到与__block修饰符一样的效果,为什么不使用这种的方式?

变量作用域结束的同时,原来的自动变量被废弃,因此Block中超过变量作用域而存在的变量将不能通过指针访问原来的自动变量

Block的isa指针与存储域

在之前的转换block的代码里,会出现impl.isa = &_NSConcreteStackBlock;,isa指向的是_NSConcreteStackBlock类,还有一些与之类似的类:

  • _NSConcreteStackBlock:表示该类的Block对象设置在栈上
  • _NSConcreteGlobalBlock:表示该类的Block对象与全局变量一样设置在数据区域(.data区)
  • _NSConcreteMallocBlock:表示该类的Block对象由malloc分配在堆上

Block分配在数据区域
在记述全局变量的地方使用Block,生成的Block为_NSConcreteGlobalBlock类对象

void (^blk)(void) = ^{print("Global Block\n");}
int main() {
    blk();
}

该Block的类为_NSConcreteGlobalBlock,该Block实例则设置在数据区域中,因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量的截获。

typedef int(^blk_t)(int);
for(int rate = 0; reate < 10; ++rate) {
    blk_t blk = ^(int count) {return count;}
}

在for循环里Block并没有去截获自动变量,所以blk实际是设置在程序的数据区域,虽然通过clang转换的代码通常是_NSConcreteStackBlock。所以总结如下:

  • 记述全局变量的地方有Block语法
  • Block语法表达式中不使用应截获的自动变量时

Block的isa指向的是_NSConcreteGlobalBlock

Block分配在堆
设置在栈上的Block,如果其所说变量作用域结束,该Block就被废弃,由于__block变量也在栈上,如果其所属变量作用域结束,__block变量也会被废弃掉

栈上的Block与__block变量

Block提供了将Block和__block从栈复制到堆上的方法,当ARC有效时大多数情况下编译器会进行恰当地判断,自动将Block从栈复制到堆上

Block从栈复制到堆上相当消耗CPU

源码:

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

转换后:

blk_t func(int rate) {
    //通过Block语法生成Block类型变量tmp
    blk_t tmp = &__func_block_impl_0 (
        __func_block_func_0, &__func_block_desc_0_DATA, rate);
        
    //通过调用Block_copy将栈上的Block变量复制到堆上,将指针赋值给变量tmp
    tmp = objc_retainBlock(tmp);
        
    //将堆上的Block作为Objective-C对象注册到autoreleasepool中,然后返回该对象
    return objc_autoreleaseReturnValue(tmp);
}
Block从栈复制到堆上

Block需要从栈复制到堆上的情形:

  1. 向方法或者函数的参数中传递Block(需手动调用copy方法)
  2. Block作为方法或者函数的返回值(编译器自动复制)
  3. 将Block赋值给附有__strong修饰符的id类型的类或者Block类型的变量(编译器自动复制)
  4. Cocoa框架的方法且方法中含有usingBlock等时(编译器自动复制)
  5. Grand Centeal Dispatch的API(编译器自动复制)

除了第一种情形需要手动调用copy方法外,其他几种编译器会自动进行判断

- (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc]initWithObject: 
                    ^{NSLog(@"block0:%d, val")}, 
                    ^{NSLog(@"block0:%d, val")}, nil];
}

id obj = getBlockArray();
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();

blk()在执行的时候会发生异常,应用程序强制结束,正确方式如下:

- (id)getBlockArray {
    int val = 10;
    return [[NSArray alloc]initWithObject: 
                    [^{NSLog(@"block0:%d, val")} copy], 
                    [^{NSLog(@"block0:%d, val")} copy], nil];
}

copy操作对于不同Block类的效果如下:

Block的类 副本源的配置存储域 复制效果
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock 程序的数据区域 什么也不做
_NSConcreteStackBlock 引用计数增加

__block变量存储域

  1. 若1个Block中使用__block变量,则当该Block从栈复制到堆时,使用的__block变量也全部从栈复制到堆
  2. 多个Block中使用__block变量时,因为最先会将所有Block配置在栈上,所以__block变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block变量也会一并从栈复制到堆,并被该Block所持有。而当剩下的Block从栈中复制到堆时,被复制的Block持有堆中的__block变量,并增加__block变量的引用计数

在多个Block使用__block变量

如果配置在堆上的Block被废弃,那么它所使用的__block变量也就被释放了
Block的废弃和__block 变量的释放

栈上的__block变量在从栈复制到堆上时会将成员变量_forwarding的值替换为复制到堆上的__block变量的地址

复制__变量block

通过该功能,无论是在Block语法内还是在Block语法外,或者是在栈上或者堆上,都能正确访问同一个__block变量

在将__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 *dst, struct __main_block_impl_0 *src) {
    _Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

BLOCK_FIELE_IS_BYREF:表示该变量是__block变量
BLOCK_FIELE_IS_OBJECT:表示该变量是对象

__block与__strong、__weak

_block id obj = [[NSObject alloc] init];

等价于

__block id __strong obj = [[NSObject alloc] init];

ARC有效时,id类型以及对象类型变量必定附加所有权修饰符,缺省为附有__strong修饰符的变量

blk_t blk1;
blk_t blk2;
{
    //array1被复制到堆上
    id array1 = [[NSMutableArray alloc] init];
    blk1 = [^(id obj) {
        [array1 addObject:obj];
       NSLog(@"array1 count = %ld", [array1 count]);
   } copy];
   
   id __weak array2 = [[NSMutableArray alloc] init];
   blk2 = [^(id obj) {
        [array2 addObject:obj];
       NSLog(@"array2 count = %ld", [array2 count]);
   } copy];
}
blk1([[NSObject alloc] init]);
blk1([[NSObject alloc] init]);
blk1([[NSObject alloc] init]);
blk2([[NSObject alloc] init]);
blk2([[NSObject alloc] init]);
blk2([[NSObject alloc] init]);

执行结果如下:

array1 count = 1
array1 count = 2
array1 count = 3
array2 count = 0
array2 count = 0
array2 count = 0

参考

  • Objective-C高级编程(iOS与OS X多线程和内存管理)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容