OC之block

1.什么是block?

block是将函数及其执行上下文封装起来的【对象】。

    int age = 10;
    void (^block)(void) = ^ {
        NSLog(@"this is a block: %d", age);
    };

上面的block在编译阶段,会被编译成如下的_block_impl结构体:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc; # block的描述信息,内部记录着block的size 所占内存大小
        int age;  # 捕获的外部变量
    }

其中,__block_impl结构体记录着block内部函数执行的起始地址:

    struct __block_impl {
        void *isa;
        int Flags;
        int Reserved;
        void *FuncPtr;  # 记录着block内部函数执行的起始地址
    }


2.block对【基本数据类型】的捕获

2.1 block对基本数据类型 局部变量的捕获:

局部变量分为:

  1. 局部自动变量
    首先,我们在使用局部变量的时候,会默认省略auto关键字,auto关键字表示自动变量,即该变量离开当前作用域后,就会自动销毁:
{
  int age = 10;
  # 相当于: auto int age = 10;
}
  1. 局部静态变量
{
  static int age = 10;
}
对基本数据类型 局部变量的捕获:
  • 对于局部自动变量(auto),block会捕获到内部,是值传递:
    int age = 10;
    void (^block)(void) = ^ {
        NSLog(@"age is: %d", age);
    };

编译后为:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        int age;
    }

外部值的更改,不影响block内部值。

  • 对于局部静态变量(static), block会捕获到内部,是指针传递:
    static int age = 10;
    void (^block)(void) = ^ {
        NSLog(@"age is: %d", age);
    };

编译后为:

    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        int *age;
    }

外部值的更改,影响block内部值。

2.2 block对基本数据类型 全局变量的捕获:
{
   int _age = 10;
   static int height = 20;
}
- (void)func {
    void (^block)(void) = ^ {
        NSLog(@"%d, %d",  _age, _height);
    };
}

对于全局变量,blokc不捕获,直接访问。

总结: block对于基本数据类型 变量的捕获
1).对于局部auto变量,采用值传递的方式捕获到block内部
2).对于局部static变量,采用指针传递的方式捕获到block内部
3).对于全局变量和全局静态变量,不捕获,直接访问


3.block对 对象数据类型 的捕获机制

当block内部访问了对象类型(NSOject)的auto变量时,

  • 如果block在栈上,将不会对auto变量产出强引用
  • 如果block是被拷贝到堆上,
    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据auto变量自身的修饰符(__storng、__weak、_unsafe_unretained),做出相应的操作,类似于retain,形成强引用或者弱引用
  • 如果block从堆上移除
    • 会调用block内部的dispose函数
    • dispose函数会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的auto变量,类似于release

4.block的种类

block有三种类型,可通过class方法或者isa指针查看具体类型,均继承自NSBlock类型,NSBlock继承自NSObject

  • __NSGlobalBlock__(即_NSConcreteGlobalBlock),存放于数据data区域,没有访问auto变量
  • __NSStackBlock__(即_NSConcreteStackBlock),存放于栈区,访问了auto变量
  • __NSMallocBlock__(即_NSConcreteMallocBlock),存放于堆区,__NSStackBlock__调用了copy或者strong
三种block使用copy后的结果:
  • _NSConcreteGlobalBlock使用copy后,什么也不做
  • _NSConcreteStackBlock使用copy后,从栈区拷贝到堆区
  • _NSConcreteMallocBlock使用copy后,引用计数增加

注意:
在arc下,编译器在一些情况下会默认对block进行拷贝操作:

  • block作为函数返回值时
  • 将block赋值给__strong指针时
  • Cocoa API的方法中,含有usingBlock的方法参数时
  • block作为GCDAPI的方法参数时


【知识补充】修饰符__block详解

一般情况下,对block截获的变量进行赋值操作时,需要给外部变量添加__block修饰符。

如下一个普通的block:

    int age = 10;
    void (^block)(void) = ^ {
        NSLog(@"age is: %d", age);
    };
            ⬇️编译后⬇️
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        int age;
    }

在添加了__block修饰符后:

    __block int age = 10;
    void (^block)(void) = ^ {
        age = 20;
    };  
                ⬇️编译后⬇️
    struct __main_block_impl_0 {
        struct __block_impl impl;
        struct __main_block_desc_0 *Desc;
        __Block_byref_age_0 *age;
    }
    
    struct __Block_byref_age_0 {
        void *__isa;
        __Block_byref_age_0 *__forwarding;
        int __flags;
        int __size;
        int age;
    }

会产生一个__Block_byref_age_0结构体

  • 该结构体的age保存着外部变量age的值,在block内部修改age值的时候,也是修改此结构体的age值;
  • 该结构体还有一个__Block_byref_age_0的指针,指向__Block_byref_age_0结构体自身
  • 访问__Block_byref_age_0结构体的age值,是访问__main_block_impl_0结构体的age结构体的__forwarding指针指向的对象的age字段,即age->__forwarding->age

需要添加__block修饰符:

  • 局部基本数据类型的变量
  • 局部对象类型的变量

不需要添加__block修饰符:

  • 局部静态变量
  • 全局变量
  • 全局静态变量

注意
赋值操作不等于使用,比如在block内部使用外部变量NSMutableArrayaddObject方法时候,就不需要给外部的NSMutableArray添加__block修饰符

使用__block后的内存管理跟block捕获对象的内存管理类似:
相同点:

  • 如果block在栈上,将不会对使用__block产生的变量进行引用
  • 如果block是被拷贝到堆上,会调用block内部的copy函数
  • 如果block从堆上移除,会调用block内部的dispose函数

不同点:

  • 堆上的block捕获对象,会根据外部对象的修饰符来决定是强引用还是弱引用
  • 而使用了__block后,堆上的block捕获对象,ARC下一定是强引用,MRC下不会强引用

__forwarding指针作用
使用__block后生成的对象(结构体),为什么内部会存在一个指向自身的__forwarding指针?

栈上的block
拷贝到堆上后的block
  • 栈上block的__forwarding指针指向自身

  • 栈上block拷贝到堆上后,栈上block的__forwarding会指向堆上的block,堆上block的__forwarding会指向自身

这样设计,保证了无论是在堆上还是在栈上访问,修改的都是同一个值。


常见面试题:

面试题1:

    __block int mul = 10;
    _block = ^int (int num) {
        return num * mul;
    };
    mul = 6;
    
    [self execulteBlock];
}

- (void)execulteBlock {
    int result = _block(4);
    思考 result的值
}

答案:24
在栈上通过__block修饰后,创建的局部变量就变成了对象,所以multiplier=6这一步赋值其实是变成了通过对生成的对象的__forwarding指针,对其成员变量multiplier进行赋值。_blk = ^...这一步操作,是对block进行了拷贝,在堆上同时生成一份拷贝。拷贝后的multiplier=6这一步,就变成了对堆上的数据操作。包括后续的executeBlock都是对堆上的block对象取值操作。

面试题2:以下方式使用__block,会产生循环引用吗?

    __block MyBlock *myBlock = self;
    _block = ^int(int num) {
        return num * myBlock.var;
    };
    _block(3);

答案:

  • MRC下不会产生循环引用
  • ARC下会产生循环引用

ARC下需要手动解环,修改如下:

    __block MyBlock *myBlock = self;
    _block = ^int(int num) {
        int result = num * myBlock.var;
        myBlock = nil;  # 解环操作
        return result;
    };
    _block(3);

⚠️⚠️⚠️注意:这种方法,需要执行block才可以破环,否则无法断开。

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

推荐阅读更多精彩内容