oc篇- Block底层原理(二)

上篇讲到了Block其实就是oc对象

[toc]

本文主要讲解Block值捕获以及如何修改block捕获的变量,说明下本文的目录结构(简书竟然没做目录功能,有点失望,给个简书生成目录的链接吧)

注意把示例中的这行代码改成下面的形式,否则无效

// @match        https://www.jianshu.com/p/*

一、Block是如何捕获变量的

1.1 局部变量

1.1.1 auto类型变量

1.1.1.1 基本数据类型

修改下main.m的代码

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        int a  = 10;
        void (^block) (void) = ^{
            NSLog(@"----%d-",a);
        };
        a = 20;
        block();
    }
    return 0;
}

发现打印的是

10

转成C++代码发现有些变化了

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; //变化1
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { // 这也是c++的语法表示 将 参数`_a` 赋值给 变量`a`
    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; // 变化3

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_4fb442_mi_0,a);
        }


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

        int a = 10;
        void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));// 变化2
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

变化1:发现block多了个int类型的变量a;
变化2:调用block的时候除了传递block本身外,把a的值也就是10传递给了里面的变量a
变化3:最后方法执行的地方__main_block_func_0也是把block的变量a取出

1.1.1.2 对象数据类型

原来block的结构还不是定的,会随着拥有的变量改变内存结构

再举个例子来进一步理解下值捕获,改造下代码

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
       NSString *name  = @"jack";
        NSLog(@"进入block前的地址----%p",name);
        void (^block) (void) = ^{
            NSLog(@"进入block的地址----%@--%p",name,name);
        };
        name = @"rose";
        NSLog(@"修改name之后的地址----%p",name);
        block();
    }
    return 0;
}

输出

进入block前的地址----0x100001078
修改name之后的地址----0x1000010d8
进入block的地址----jack--0x100001078

c++代码,这次简单写下就是多了个*name属性

struct __main_block_impl_0 {
 ...
  NSString *name;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *_name, int flags=0) : name(_name) {
...
  }
};

解释下为什么是输出jack

在进入block前name地址为0x100001078,当到了__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name))的时候相当于把name的值也就是0x100001078给了__main_block_impl_0里面的name变量,当再次name = @"rose";的时候之前的name的值已经不是0x100001078,而是0x1000010d8了,所以后来当调用的时候访问的是__main_block_impl_0name变量的值0x100001078所存的值是jack

弄了一张图帮助理解下

15341544298315.jpg

1.1.2 static类型变量

1.1.2.1 对象类型

接着我们再次修改下代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
       static NSString *name  = @"jack";
        void (^block) (void) = ^{
            NSLog(@"----%@--",name);
        };
        name = @"rose";
        block();
    }
    return 0;
}

就是加了一个static

输出

----rose--

这次的结果完全不一样了,这又是为什么呢,继续看c++的实现

struct __main_block_impl_0 {
    ...
    NSString **name;  // 1
    ...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString **name = __cself->name; // 3

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_1,(*name));//4
        }

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

       static NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_0;

        void (*block) (void) = &__main_block_impl_0(__main_block_func_0, 
                                                &__main_block_desc_0_DATA,
                                                 &name, //2
                                                 570425344));
        name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_2;

    block->FuncPtr(block);
    }
    return 0;
}

看到代码1处已经是二重指针,2处这个时候给的不是name指向的内存地址,而是name变量的地址,3处在调用的时候取出的是指向name变量地址的内存,而不是name所指向的内存,所以要想获取name所指向的内存,4处通过*name取值进行传参

也就是红色箭头的变化


15341552229497.jpg

1.2 全局变量

1.2.1 非static修饰的全局变量

1.2.1.1 对象类型

当变量是全局的变量时


NSString *name  = @"jack";

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        void (^block) (void) = ^{
            NSLog(@"----%@--",name);
        };
        name = @"rose";
        block();
    }
    return 0;
}

发现也是输出

---rose--

额,这又是怎么回事呢,看源码吧

NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_0;


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) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_2,name);
        }

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; 

        void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        
        name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_3;

        block)->FuncPtr(block);
    }
    return 0;
}

我们可以发现这个时候block内部并没有像之前那样生成一个同名的变量,也就是对于全局变量block是不会捕获的,当变量是static全局变量时也和全局变量一样,留给读者自行测试了

1.2.1.2 基本类型

略...

1.2.2 static修饰的全局变量

略... 读者自行写demo

说了这么多我们来梳理下,我们思考一个问题,block为什么要捕获变量,是因为里面有个方法,方法需要使用变量,

  1. 如果是局部变量的话,如果不持有他的话是不是过了作用域就释放了,那就不能完成方法的正常调用,所以对于局部变量,一定会捕获的;
  2. 对于全局变量,刚才说了block捕获变量的原因要使用变量,既然是全局变量,那在哪都可以访问,所以不需要捕获;
  3. 那为什么局部变量有的是传地址有的是传值呢,对于非static修饰的局部变量其实是auto的,这种变量是放在栈区的,过了作用域就会被系统回收,如果block捕获变量的地址的话,那可能捕获的地址已经被系统回收,或者已经被其他的对象占用了,这个时候程序会出现无法预料的异常,但是如果是static修饰的,是放在数据区的,不会随着作用域的而销毁,从而放地址是安全的

总结就是下面这张图


15341565010715.jpg

二、 Block捕获对对象的引用计数的影响

2.1 __NSMallocBlock__对对象的引用计数的影响 ARC环境

我们知道基本数据类型是放在栈中的,回收是由系统自动回收的无需考虑,所以我们这里只考虑auto类型的对象类型的引用计数

需要新增一个MyPerson

//MyPerson.h
#import <Foundation/Foundation.h>

@interface MyPerson : NSObject

@end

//MyPerson.m

#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc {
    NSLog(@"-%s",__func__);
}
@end

main.m代码如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
  
        void (^myBlock) (void);
        {
            MyPerson *p = [[MyPerson alloc] init];
           myBlock = ^ {
                NSLog(@"----%@",p);
            };
            
            NSLog(@"block类型---%@",[myBlock class]);
        }
     
        NSLog(@"----");

    }
    return 0;
}


15351680937915.jpg

输出

__NSMallocBlock__

这里p并没有释放掉,按道理过了120行应该就释放的,其实此时MyPerson *p = [[MyPerson alloc] init];__strong MyPerson *p = [[MyPerson alloc] init];是等价的

说明__NSMallocBlock__类型的myBlock会对__strong修饰的p对象的引用计数产生影响

再修改main.m函数

15351682736470.jpg

此时输出

__NSMallocBlock__
--[MyPerson dealloc]

发现p正常释放了

说明__NSMallocBlock__类型的myBlock不会对__weak修饰的 p对象的引用计数产生影响


2.2 __NSStackBlock__对对象的引用计数的影响 MRC环境

把项目改成MRC

15351618498306.jpg

MyPerson

#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc {
    [super dealloc];
    NSLog(@"-%s",__func__);
}
@end

mian.m如图

15351615827663.jpg

此时打印

block类型---__NSStackBlock__
--[MyPerson dealloc]

发现p正常释放了,此时的myBlock的类型为__NSStackBlock__类型的,

说明__NSStackBlock__类型的myBlock不会对__strong修饰的 p对象的引用计数产生影响

再修改一下main.m的代码如图,新增的代码看红色处

会发现此时只会输出

block类型---__NSMallocBlock__

但是p并没有释放

说明__NSMallocBlock__类型的myBlock会对__strong修饰的p对象的引用计数产生影响

2.3 结论

得出以下结论:

  1. 当block内部访问了对象类型的auto变量时
    如果block是在栈上,将不会对auto变量产生强引用

  2. 如果block被拷贝到堆上
    会调用block内部的copy函数;
    copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

  3. 如果block从堆上移除
    会调用block内部的dispose函数;
    dispose函数内部会调用_Block_object_dispose函数_Block_object_dispose函数会自动释放引用的auto变量(release)

三、__block如何做到可以修改变量的

由前面的了解我们知道block要想可以修改变量,那么就不能值捕获,也就是不能放在栈内存中,因为栈内存是的释放无法控制,所以要买放在全局区,要么放在堆区,来看下苹果是放在哪里的。

3.1 __block变量的内存结构

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        MyPerson *p = [[MyPerson alloc] init];
        p.name = @"jack";
        
        void (^myBlock) (void) = ^ {
            p = [[MyPerson alloc] init];
            NSLog(@"---%@",p.name);
        };
        
        myBlock();
    }
    return 0;
}

像这样在block的内部直接修改变量是会报错的, 要想修改需要借助__block修饰符

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
      __block  MyPerson *p = [[MyPerson alloc] init];
        p.name = @"jack";
        
        void (^myBlock) (void) = ^ {
            p = [[MyPerson alloc] init];
            NSLog(@"---%@",p.name);
        };
        
        myBlock();
    }
    return 0;
}

我们看看转成的c++的代码

15351801025148.jpg

可以看到和之前没有__block修饰的不同的是这次的p变成了类型为__Block_byref_p_0 *p
再看初始化的地方

15351802434170.jpg

一行__block MyPerson *p = [[MyPerson alloc] init];就变成了初识化一个__Block_byref_p_0类型的结构体,然后把该结构体的指针给到myBlock,而我们初始化的那个p则给了__Block_byref_p_0内部的p对象

3.2 __block变量的内存管理

  1. 当__block变量在栈上时,不会对指向的对象产生强引用

  2. 当__block变量被copy到堆时
    会调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(MRC除外,MRC时不会retain)

  3. 如果__block变量从堆上移除
    会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放指向的对象(release)

和没有__block修饰的auto对象变量差不多,只是第二条中对MRC不起作用

15351806310781.jpg
15351808003801.jpg

这里的Block0相当于myBlock__block变量就是p,15行的时候变量MyPerson类型的变量p,就变成了指向__Block_byref_p_0类型的指针了,且处于栈中,到了21行结束,由于是arc环境,myBlock就是为右边的blockcopy后的处于上了,这是变量p也会被拷贝到堆上,当23执行的时候调用的就是堆上的block,访问的也是堆上的内容,对于block内部的NSLog(@"---%@",p.name);则是结构体p内部的MyPerson类型的p对象

3.3 __block的__forwarding指针

可以看到__Block_byref_p_0结构如下

15351812044975.jpg

有一个__forwarding,在main.m中初始化的时候传的就是__Block_byref_p_0自身的地址,取值的时候也是通过p->__forwarding->p去取值岂不是多此一举?

15351812636865.jpg

15351813367663.jpg

其实这是不管当__block修饰的结构体变量,处于栈上还是被复制到堆上,都可以访问到同一个p变量

我们把代码改下

15351815132066.jpg

此时会输出rose

代码过了21行,myBlock就已经在上了,到了23行访问还是栈中的结构体变量,那为何还是打印rose呢,就是因为有了__forwarding指针的作用,保证了此时不管在中还是在中都可以访问到同一个MyPerson类型的变量p,当__block修饰的变量从栈中copy到堆中的时候发送的事情入下图

15351817089007.jpg

可以看出苹果的实现是通过把变量放在堆区的方式来实现修改__block捕获的变量的,也可以看出__block对对象的内存影响还是蛮大的。

(完)

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

推荐阅读更多精彩内容

  • 前言 Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这...
    小人不才阅读 3,750评论 0 23
  • block.png iOS代码块Block 概述 代码块Block是苹果在iOS4开始引入的对C语言的扩展,用来实...
    全栈农民工阅读 588评论 0 1
  • 上一篇文章iOS底层原理总结 - 探寻block的本质(一)中已经介绍过block的底层本质实现以及了解了变量的捕...
    xx_cc阅读 10,595评论 8 54
  • 谈职业这个问题,对下学期读大二的小谭来说,好像有点远,或者说,她自己还觉得有点远。 其实也不远了吧,回想一下,过去...
    不与周郎便阅读 329评论 0 3
  • 1、有男人的独立担当(儿子像爸爸这点) 2、为人善良(儿子像爸爸) 3、对人真诚 4、孝顺父母(儿子像爸爸) 5、...
    丽人d阅读 400评论 0 0