MENU

Objective-C之Blocks(四)

March 27, 2017 • Read: 557 • iOS

前言

本文在Objective-C之Blocks(三)的基础上进行对OC语法中Block一些细节的探讨。

关于Block的存储域

在之前文章里,我们看到过__main_block_impl0结构体中的impl.isa = &_NSConcreteStackBlock这样的语句。因为isa指针的存在,使得Block成为了一个OC的对象。而根据_NSConcreteStackBlock类很容易得出,此Block结构体分配在栈(stack)上。但是Block除了在栈上之外,还存在于数据区域、堆区。

对象的存储域
_NSConcreteStackBlock
_NSConcreteGlobalBlock程序的数据区域(.data区)
_NSConcreteMallocBlock

那么什么时候会分配这些区域的Block呢?我们来验证一下。

分配在栈区的Block

分配在栈区的Block是前文出现最多的。一般情况下,Block是生成在栈区的,栈区的Block是一种比较原始的状态,另一种比较原始的状态是在程序的数据区域。此处的原始的意思是,一产生的状态。当你生成一个Block的时候,一般不是栈区就是程序数据区。而堆区的Block则需要一定的转化,所以不是原始的。

// 声明一个Block,利用 clang -rewrite-objc xxx.m 来查看底层实现
for (int i=0; i<10; i++) {
        int(^blk)(int) = ^(int i){return 5*i;};
    }
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;
  }
};

impl.isa = &_NSConcreteStackBlock;,我们可以知道,此Block是分配在栈区的。

分配在程序数据区的Block

程序数据区存放静态变量和全局变量等。那么我们想要Block分配在程序数据区,可以声明一个全局的Block。

// 声明一个全局的Block,此Block在程序数据区
typedef int(^blk_t)(int);
blk_t blk = ^(int i){return i;};

int main(int argc, const char * argv[]) {
    // ....
    NSLog(@"%d",blk(5));
}
struct __blk_block_impl_0 {
  struct __block_impl impl;
  struct __blk_block_desc_0* Desc;
  __blk_block_impl_0(void *fp, struct __blk_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

转换过后可以得:Block分配在程序数据区。但是如果在Block内部使用了截获的自动变量的时候,Block就会被分配在栈区。在《Objective-C高级编程》中说到:

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

个人认为这两句话是一样的,因为在使用全局变量的地方是无法使用自动变量的,而能使用自动变量的地方一定不是在全局变量处。所以个人认为:分配在数据区域的Block一定声明在全局变量处。* (希望有其他理解的同学能够给我讲讲怎么回事。)
我试过在函数内声明一个Block,即使没有使用自动变量,也还是被分配在栈区。书上说转换后虽然是_NSConcreteStackBlock类对象,但实现有所不同(个人理解:可以分配在数据区,但实际被分配在了栈区)。*

分配在堆区的Block

前文说到,Block被分配到堆区需要一定的转化。这个转换就是复制。当我们执行Block的复制操作时候,会将Block从堆区拷贝到栈区,同时拷贝的还有__block变量还会对__strong修饰符的变量进行合适的管理。__block修饰的变量的拷贝和__strong修饰的变量的管理操作其实就是__main_block_copy_0函数和__main_block_dispose_0函数的调用。前者相当于retain实例方法(copy方法内部使用了__Block_object_assign()函数),而后者相当于release方法(dispose方法内部使用了__Block_object_dispose()函数)。二者的调用时机分别是Block从栈区复制到堆区,堆上的Block被废弃的时候。
那么什么时候Block会从栈区复制到堆区呢?

  1. 调用Block的copy实例方法
  2. Block作为函数返回值返回
  3. 将Block赋值给附有__strong修饰符的id类型变量或Block类型成员变量
  4. 在方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递的Block
    下面禁用ARC,测试Block从栈区复制到堆区。正如我们所知,栈区的变量在变量作用域结束后,就会被释放,如果这个时候去访问就会出错。那么我们可以利用这个特性来验证Block是否从栈区复制到了堆区。
// 堆区Block
int main{
    //....
    id obj = getBlockArrary();
    typedef void (^blk_t)(void);
    blk_t blk = (blk_t)[obj objectAtIndex:0];
    blk();
    return 0;
}
NSArray* getBlockArrary(){
    int val = 10;
    return [[NSArray alloc] initWithObjects:
            ^{NSLog(@"blk0:%d",val);} ,
            ^{NSLog(@"blk1:%d",val);} ,nil];
}

此时运行程序,在执行blk()的时候,程序崩溃。当修改getBlockArrary()函数为

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

当执行了copy操作之后,Block从栈区转移到堆区,程序就可以正常执行。也就间接证明copy操作是将Block从栈区赋值到堆区。(当我使用clang -rewrite-objc main.m 指令后,发现impl.isa依然是_NSConcreteStackBlock类,此处不明白。请大神不吝赐教)。

关于__block变量的的存储域

前文说到,在Block复制到堆的时候,__block变量也会随之从栈区复制到堆。

__block变量的配置存储域Block从栈复制到堆的影响
栈区从栈复制到堆并被Block持有
堆区被Block持有

Objective-C之Blocks(三)中一直有一个困惑,为什么需要blockVal->__forwarding->blockVal来访问blockVal变量呢?为了不管__block变量配置在栈上还是在堆上,都能够正确的访问该变量
下面以图说明,当__block变量在栈上的时候,__forwarding指向自己,然后再获取自己的blockVal变量。

在栈上的__block变量

当__blokc变量复制到堆区的时候,情况就变了。__forwarding就指向堆上的__block变量结构体。

堆上的__block结构体%

此时,无论是在Block语法中、Block语法外使用__block变量,还是__block变量在栈区或是堆区,都能访问同一个__block变量。

结语

  • 关于Block中间还是有一些困惑,但是无伤大雅。待以后水平长进的时候再来进行验证。
  • 如有错误,欢迎指正。

参考

《Objective-C高级编程》

Tags: iOS开发
Archives QR Code
QR Code for this page
Tipping QR Code
Leave a Comment

已有 1 条评论
  1. 吴先生 吴先生

    #(脸红)