iOS - block - 捕获__block基本类型

[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 将局部变量 var 包装成了对象(结构体) __Block_byref_val_0, 并生成其 var, 我们访问的 var, 实际是通过结构体实例 var 的成员指针 __forwarding 间接访问成员变量 var。

  • 变量 val 所包装成的的结构体 __Block_byref_val_0 实例 (对象的指针) :

    • 在 block 定义前, 在栈区。

    • 在 block 定义后, 结构体 val 的地址 被 block 捕获为成员变量, 随 block 拷贝到堆 ;

      此时, 包外的结构体 val 的 __forwarding 指向堆中val的新地址。<详见本文C++分析>

    • 后续对 val 的读写也都是在这个堆地址上进行, 无论是否触发block;

      故而block包内可以修改局部变量 val 的值。

可见:

__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*/);
}

分析

首先提几点疑问:

  • 当 block 作为回调执行时, 局部变量 val 已经出栈了, 此时代码为什么还能work呢?
  • 为什么是通过成员变量 __forwarding , 而不是直接去访问结构体中需要修改的变量呢?

  • 从上述C++代码中可以看到, __main_block_impl_0 结构体构造函数中, isa指向的是 _NSConcreteStackBlock, 即在栈上生成。

    分配在全局的 block, 在作用域外也可以通过指针安全的访问。但栈上的 block, 如果它所属的作用域结束, 该Block就被废弃。

    同样, __block 变量在定义时, 也分配在栈上, 当超出该变量的作用域时, 该 __block 变量也会被废弃。

    咋办呢?

_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 变量, 实质就是相应结构体的实例。

  • __block 变量在栈上时, __forwarding 指针就指向自己。

  • block 拷贝到堆上后, __block 变量随 block 也拷贝到堆上一份, 此时有两个 __block 变量:

    • 堆上的这个 __block 变量的 __forwarding 指针指向自己;

    • 原先栈上的 __block 变量的 __forwarding 指针改为指向堆上的 __block 变量。

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


val.__forwarding->val

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

  • 如果此时结构体 val 还未被拷贝到堆, 那<活跃>的结构体 val 就是最开始定义的, 在栈上;

  • 如果此时结构体 val 已经被拷贝到堆, 那<活跃>的结构体 val 就是这个被拷贝到堆上的结构体;

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

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容