iOS - block - 捕获__block基本类型

2019-04-12  本文已影响0人  felix6

[toc]

参考

block - 捕获__block基本类型

http://www.cocoachina.com/ios/20150106/10850.html

https://www.jianshu.com/p/404ff9d3cd42

OC代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSInteger val = 0; 
        
        // 访问 val, 实际访问的是 val.__forwarding->val , 此时在栈上
        NSLog(@"1_val = %ld - %p", val, &val); 
        
        void (^block)(void) = ^{
            // 这里捕获 val, 捕获的是被 __block 包装的 val 对象;
            // 与否被重新赋值没有关系;
            val = 1; 
            NSLog(@"2_val = %ld - %p", val, &val);
        };
      
        NSLog(@"3_val = %ld - %p", val, &val);

        block();
        NSLog(@"4_val = %ld - %p", val, &val);
    }
    return 0;
}

MRC 输出: (变量值 - 变量地址) 
// __block变量始终在栈上, 而且是同一份地址
// 因为block捕获的是val的地址, block没有被拷贝到堆, 那val也还是最初的val ★
1_val = 0 - 0x7ffeefbff408 
3_val = 0 - 0x7ffeefbff408
2_val = 1 - 0x7ffeefbff408
4_val = 1 - 0x7ffeefbff408

ARC 输出:
1_val = 0 - 0x7ffeefbff408 // block定义前: 栈地址
3_val = 0 - 0x103308438 // block定义后: 堆地址 ★
2_val = 1 - 0x103308438
4_val = 1 - 0x103308438
// block 访问 __block 修饰的局部变量, block定义前后, 局部变量指针的地址不一致, 且地址值相差较大, 说明变量 val 已拷贝到堆中, 且 block 外局部变量 val 的地址也被改为这个堆地址。
分析:
MRC下:

block 默认不被 copy, block 始终在栈上; <block - 存储域>

所以无论包内包外, 该对象都不被copy, 始终在栈上; 局部变量 (其结构体成员) 的地址也始终在栈上, 且始终是同一份地址;

ARC下:

可见:

__block 实现了变量堆栈地址的变更 , 而有些博客非所谓的 "写操作生效"。

block 访问 __block 修饰的局部变量, 会将该变量同 block 一起 copy 到堆区;


C++代码

MRC / ARC 编译后代码一致:

int main(int argc, const char * argv[]) {
    { __AtAutoreleasePool __autoreleasepool; 
     
        // 定义 __block局部变量  __block NSInteger val = 0; 
        // __block局部变量 val 被封装成了一个 __Block_byref_val_0 结构体类型的实例 val
        __attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 0};

        // 访问 val 实际都是 val.__forwarding->val ★★
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_0, (val.__forwarding->val), &(val.__forwarding->val));

        // 捕获的是结构体 val 的地址, 作为第3个入参
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_2, (val.__forwarding->val), &(val.__forwarding->val));
     
        // block调用
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_3, (val.__forwarding->val), &(val.__forwarding->val));
     
    }
    return 0;
}


// 捕获到的局部变量, 被封装成结构体类型 (包装成对象);
// 其名称 __Block_byref_val_0 是根据捕获到的局部变量名 val 命名的
// 这个结构体中包含了该实例本身的引用 __forwarding ★★
// 访问变量 val, 实质访问的是结构体 __Block_byref_val_0 的成员变量 val (val.__forwarding->val)
struct __Block_byref_val_0 {
     void *__isa; // 有isa, 对象的特征; 编译器将 __block 变量包装成了对象 ★
     __Block_byref_val_0 *__forwarding; // 该实例本身的引用 ★
     int __flags;
     int __size;
     NSInteger val; // 结构体内部保存的原始变量 ★
};

// block 本身被转换成了 __main_block_impl_0 结构体实例;
// 该实例持有 __Block_byref_val_0 结构体实例的指针。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
  
    // 包装了局部变量的对象(结构体指针) ★★
    __Block_byref_val_0 *val; // by ref 
  
    // 构造函数, 注意第3个入参
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__forwarding) {
        impl.isa = &_NSConcreteStackBlock; // 栈中的 block, 出栈时会被销毁 (见下面<分析>) ★ 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};


static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    // 通过入参 __cself 找到其成员变量, 结构体 __Block_byref_val_0 的实例 val ;
    __Block_byref_val_0 *val = __cself->val; // bound by ref;
  
    // 通过 __forwarding 找到<活跃>的结构体val, 拿到初始的局部变量val;  ★
    (val->__forwarding->val) = 1;
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_p5_mp3284bs2xb073r91w__n99r0000gn_T_main_060736_mi_1, (val->__forwarding->val), &(val->__forwarding->val));
}


// 注意, 使用了 __block 修饰基本数据类型的局部变量, desc结构体中多了 copy 和 dispoose 函数 ★
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 };


// (目的destination 源source) 
// 当 Block 从栈复制到堆时, 会调用 _Block_object_assign 函数持有该变量(相当于retain)。
static void __main_block_copy_0 (struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
// 当堆上的 Block 被废弃时, 会调用 _Block_object_dispose 函数释放该变量(相当于release)。
static void __main_block_dispose_0 (struct __main_block_impl_0*src)     {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}

分析

首先提几点疑问:

_NSConcreteMallocBlock 登场了!

苹果采用了将 Block 和 __block 变量从栈上复制到堆上, 来解决这个问题。

复制到堆上的Block, 它的结构体成员变量 isa 将变为: impl.isa = &_NSConcreteMallocBlock;

当 Block 被复制到堆上时, 其捕获的 __block 变量也会被复制到堆上, 此时堆上的 Block 持有相应的堆上的 __block 变量。

当栈上的 Block 及捕获的变量超出它原本作用域时, 堆上的 Block 还可以继续存在。

当堆上的 __block 变量没有持有者时, 它才会被销毁。(这里的思考方式和 objc 引用计数内存管理完全相同。)

此时, 只要原先栈上的 __block 变量的成员变量 __forwarding 指向堆上的结构体实例, 就能够安全地访问。

一般可以使用 copy 方法手动将 Block 或者 __block变量从栈复制到堆上。

  • 比如我们把 Block 做为类的属性访问时, 一般把该属性设为 copy。

  • 有些情况下我们可以不用手动复制, 参考<存储域 - 自动 copy到堆>


__forwarding 指针图解

栈上的 __block 变量访问自身, 如图:

<img src="https://cdn.jsdelivr.net/gh/coder-felix/image/20200609225314.png" style="zoom:25%;" />

__block变量被复制到堆, 此时栈上和堆上分别有一个 __block 变量 (结构体)

<img src="https://cdn.jsdelivr.net/gh/coder-felix/image/20200609225355.png" style="zoom:25%;" />

首先明确一点: block 和 __block 变量, 实质就是相应结构体的实例。

这个时候我们可以通过 val.__forwarding->val 访问变量。


val.__forwarding->val

拿到一个结构体 val, 然后通过它的 __forwarding 找到<活跃>的结构体 val, 从而访问结构体内部保存的原始局部变量 val

这保证了无论结构体 val 有没有被拷贝到堆, 无论是从堆上还是栈上访问, 访问到的原始局部变量始终是同一个。★

注: <活跃> 是个人为方便理解而定义的:

  • 如果未被拷贝到堆, 那<活跃>就是指栈上的;
  • 如果已被拷贝到堆, 那<活跃>就是指堆上的;

上一篇下一篇

猜你喜欢

热点阅读