神奇的Block

神奇的Block

本文不做Block的基本介绍和底层实现原理,有兴趣的同学直接戳这篇文章,写得灰常好,本文只在应用层面上带领读者进行思考,并整理出一些结论.这些结论是我从书上和上网资料收集所得,并通过实践进行验证而来,希望能和高手们共同探讨 :)

在看例子之前,至少要知道block有几个类型.

  • _NSConcreteGlobalBlock(全局块)
  • _NSConcreteStackBlock(栈块)
  • _NSConcreteMallocBlock(堆块)

废话不说,直接看例子.测试环境为ARC,就不做MRC的测试了.

精神病入门

例子一:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {

    blk_t block = ^{
        printf("I'm just a block\n");
    };
    block();
    
    return 0;
}

很简单的一段代码,执行block之后结果是I'm just a block.但如果问你,这个block是什么类型的block,你会怎么回答?

在代码中打一个断点,通过打印blockisa,可以知道该block是什么类型的.

第一步:打个断点

第二步:打印isa

然后就能看到结果了:

看到结果,尼玛居然是个全局块,可是我明明是在栈上创建的一个block呀!

再来看一个例子,这时定义了一个局部变量,并在block中使用了这个局部变量.

例子二:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {
    int i = 1;
    blk_t block = ^{
        printf("%d\n",i);
    };
    block();
    
    return 0;
}

按照以上步骤再看看blockisa.

……我去,怎么成堆块了?

别急,再举个🌰.

例子三:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {
    int i = 1;
    __weak blk_t block = ^{
        printf("%d\n",i);
    };
    block();
    
    return 0;
}

虽然编译器在__weak blk_t block = ^{这行爆出了警告,但是程序还是能够正常运行.block并未因为一个弱引用立即释放.然后看看结果:


终于看到栈块了,全家福终于齐人了.

下面开始总结了.

在哪些情况下,Block_NSConcreteGlobalBlock类对象?

  • 记述全局变量的地方创建的Block,比如下面的例子.

    blk_t block = ^{
        printf("I'm just a block");
    };
    int main(int argc, const char * argv[]) {
        block();
        return 0;
    }
    
  • 不截获自动变量的时候.

    例子一这种情况下.虽然是在栈上创建的一个block,但由于闭包内不截获外部的自动变量(局部变量),将会被编译器编译为_NSConcreteGlobalBlock.

再来总结一下第二个例子.之所以是一个堆块,是因为编译器为块进行了copy操作(实质上是调用_Block_copy函数).以下方式会让块从栈复制到堆上.

  • 调用Blockcopy实例方法.

    [^{
            printf("a heap block");
        } copy]; 
    // 对block调用copy,会把栈上的block复制到堆上.
    
  • Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时.

    也就是说,有个__strong修饰的变量指向这个block就会让编译器为block调用copy方法.

    例子二就是将Block赋值给了一个__strong(默认都是strong)修饰的Block类型成员变量——blk_t block.

    因此,在例子三中,我将强引用变成弱引用,创建了一个栈上的block.虽然编译器会有警告,因为编译器在这里可能还不知道那个块也是栈上的,而这个栈上的块,显然不会立即释放.

  • Block作为函数返回值时

    例如:

    blk_t return_A_Block(){
        int val = 10;
        return ^{NSLog(@"%d",val);};
    }
    
    int main(int argc, const char * argv[]) {
        NSLog(@"%@",return_A_Block());
        return 0;
    }
    

    打印所得是一个堆块.

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

精神病进阶

例子四:

typedef void (^blk_t) (id obj);

int main(int argc, const char * argv[]) {
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = ^(id obj){
            [array addObject:obj];
            NSLog(@"%ld",[array count]);
        };
    }
   // array超出了作用域,在括号外已经不能被使用了  
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);
    
    return 0;
}

打印台输出结果:

可以看到,在超出了作用域后,array依旧能够被访问到.

例子五:

int main(int argc, const char * argv[]) {
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        id __weak array2 = array;
        blk = ^(id obj){
            [array2 addObject:obj];
            NSLog(@"%ld",[array2 count]);
        };
    }
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);
    
    return 0;
}

打印台结果:


大相径庭的结果.

在例子四中,Block中截获了外部的自动变量,并且根据上面说过的结论,编译器为我们调用了copy方法,这个Block是个堆块.

我们将例子四稍加改写,将块改为栈块(即不让编译器为我们调用copy方法):

int main(int argc, const char * argv[]) {
    __weak blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = ^(id obj){
            [array addObject:obj];
            NSLog(@"%ld",[array count]);
        };
    }
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);
    
    return 0;
}

打印出来的结果和例子五一致.

从该例子得出的结论是:

  • 只有调用了Blockcopy方法,才能持有截获的附有__strong修饰符的对象类型的自动变量值.

基于这个结论,我们还可以得出,在ARC环境下,定义block类型的属性时,可以用strong,并不是非得用copy才是正确的.

// 两者效果一样
@property (strong, nonatomic)   blk_t   *block;
@property (copy, nonatomic)     blk_t   *block;

在例子五中,虽然截获的自动变量是__weak修饰符修饰的对象类型.但是作用域过后array被释放,nil被赋值给了array2,并不能持有对象.这让我们想起了平时为了防止循环引用,我们会用一个弱指针指向self,并让block捕获弱指针而不是让block持有self.

注意,即使你不使用self.object访问实例变量,而是通过_object访问,也同样会造成循环引用.因为无论用什么形式访问实例变量,经过编译后,最终都会转换成self+变量内存偏移的形式来进行访问,还是会造成循环引用.

那么,截获和__block修饰有何不同呢?

block本质也是一个结构体,截获的对象会成为结构体成员的一部分.

例如:

int main(int argc, const char * argv[]) {
    id obj;
    ^{
        obj;
    };
    
    return 0;
}

其中生成的block会是这样子的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  id __strong obj;
};

注:在C语言中,结构体不能含有附有__strong修饰的变量.因为编译器不知道应何时进行C语言结构体的初始化和废弃操作,不能很好的管理内存.

而OC却可以,它能够准确的把握block从栈复制到堆以及堆上的block被废弃的时机.

如果是通过__block修饰的一个变量呢?

int main(int argc, const char * argv[]) {
    __block int a;
    ^{
        a = 10;
    };
    
    return 0;
}

其中生成的block会是这样子的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
};

此时,被__block修饰的变量变成了一个结构体(__Block_byref_a_0类型).至于这个结构体又长什么样,就不贴代码了,只需知道a被包装到了这个结构体中,成为其中一个成员变量,其他成员变量描述了该结构体的一些信息.

  • Block被拷贝到堆上的时候,附有__strong修饰的变量因为Block结构体内有强指针持有,使得该指针所指向的对象在作用域外还有引用计数,因此存活着.
  • Block被拷贝到堆上的时候,被__block修饰的变量被包装到了一个新的结构体中,被block结构体持有,该结构体跟随Block也被拷贝到堆上了.
  • 截获的方式并不能修改截获的变量本身,而__block修饰的方式却可以,因为它本质是复制了一份该变量.

根据结论,可以知道用__block修饰的方式也能够避免循环引用.只要在块中将需要避免循环引用的�变量置为nil.

如:

- (id)init{
  self = [super init];
  __block id tmp = self;
  blk_t blk = ^{
    tmp.name = @"ye";
    tmp = nil;
  }
  return self;
}

如果最后不置为nil,那么self持有block结构体,block结构体持有__block变量结构体,__block变量结构体持有self,只有在最后将_block变量结构体中的self置空,才能手动破除循环.这个方式比weak方式优点的地方在于可控制对象的持有期间.

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

推荐阅读更多精彩内容