iOS晋级知识汇总(九)Block

block

  • Block介绍
  • 截获变量
  • __block修饰符
  • Block的内存管理
  • Block的循环引用

什么是Block?

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

//想要更彻底的理解block需要clang它的中间码
clang -rewrite-objc file.m  查看编译之后的文件内容 
  • Block是一个对象
  • 对象封装了函数和执行上下文

block的调用

Block调用就是函数的调用

截获变量

下述代码的结果是16,因为block对局部变量multiplier进行了截获

int multiplier = 8;
    
int(^MuBlock)(int) = ^int(int num){
    
    return num * multiplier;
    
};
    
multiplier = 6
    
MuBlock(2);
  • 局部变量
    • 基本数据类型
    • 对象类型
  • 静态局部变量
  • 全局变量
  • 静态全局变量

什么是block截获变量?

  • 基本数据类型的局部变量截获其值
  • 对于对象类型的局部变量联通所有权修饰符一起截获
  • 以指针形式截获局部静态变量
  • 不截获全局变量、静态全局变量

如何理解我们需要编译中间码来帮我理解

//因为在ARC中的block跟MRC不一样
//因为MRC中没有block循环引用问题,下面会有解释
clang -rewrite-objc -fobjc-arc file.m

编译代码,以及OC代码如下:

struct __MyBlock__MyMethod_block_impl_0 {
  struct __block_impl impl;
  struct __MyBlock__MyMethod_block_desc_0* Desc;
  //截获局部变量的值(基本数据类型)
  int var;
  //连同所有权修饰符一起截获
  __unsafe_unretained id unsafe_obj;
  __strong id strong_obj;
  
  //以指针形式截获静态局部变量
  int *static_var;
  
  //对全局变量、静态全局变量不截获
  
  __MyBlock__MyMethod_block_impl_0(void *fp, struct __MyBlock__MyMethod_block_desc_0 *desc, int _var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int *_static_var, int flags=0) : var(_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//全局变量
int global_var = 6;
//静态全局变量
static int static_global_var = 8;

- (void)MyMethod{
    
    //基本数据类型的变量
    int var = 1;
    //对象类型的局部变量
    __unsafe_unretained id unsafe_obj = nil;
    __strong id strong_obj = nil;
    
    //静态局部变量
    static int static_var = 3;
    
    void(^Block)(void) = ^{
        
        NSLog(@"局部变量<基本数据类型> var %d",var);
        NSLog(@"局部变量<__unsafe_unretained 对象类型> unsafe_obj %@",unsafe_obj);
        NSLog(@"局部变量<__strong 对象类型> strong_obj %@",strong_obj);
        NSLog(@"静态局部变量 static_var %d",static_var);
        NSLog(@"全局变量 global_var %d",global_var);
        NSLog(@"静态全局变量 static_global_var %d",static_global_var);
        
    };
    
    Block();

}

截获变量的总结

  • 局部变量
    • 基本数据类型
      • 截获其值
    • 对象类型
      • 指针连同所有权一起截获
  • 静态局部变量
    • 截获其指针
  • 全局变量
    • 不截获
  • 静态全局变量
    • 不截获

__block修饰符

什么场景需要使用__block?

一般情况下,对截获变量进行赋值(跟使用操作不一样)操作需添加__block修饰符。

例子:下面代码有什么问题吗?

- (void)test{
    
    NSMutableArray *array = [NSMutableArray array];
    void(^Block)(void) = ^{
        [array addObject:@21];
    };
    
    Block();
    
}

答案:没有问题,因为array并没有去赋值。不需要使用__block。

再来看一个例子:下面代码有什么问题吗?

- (void)test{
    
    NSMutableArray *array = nil;
    void(^Block)(void) = ^{
        array = [NSMutableArray array];
    };
    
    Block();
    
}

答案: array需要添加__block修饰符。

对变量进行赋值时,合适需要添加__block?

  • 需要添加__block修饰符的情况?
    • 局部变量
      • 基本数据类型
      • 对象类型
  • 不需要添加__block修饰符
    • 静态局部变量
    • 全局变量
    • 静态全局变量

__block 修饰的变量最终变成了什么?

还是通过clang 命令来编译出中间码

- (void)MyMethod{
    __block int multiplier = 8;
    int(^MuBlock)(int) = ^int(int num){
        return num * multiplier;
    };
    
    multiplier = 6;
    NSLog(@"%d",MuBlock(2));
}
// @implementation MyBlock
struct __Block_byref_multiplier_0 {
//因为有isa指针
  void *__isa;  
  //指向同类指针的__forwarding
__Block_byref_multiplier_0 *__forwarding;
 int __flags;
 int __size;
 //我们截获的变量
 int multiplier;
};

综合上述说明通过__block修饰的变量最终变成了对象,如果在OC代码中直接修改这个变量的值,就会走下面的步骤

static void _I_MyBlock_MyMethod(MyBlock * self, SEL _cmd) {

    __attribute__((__blocks__(byref))) __Block_byref_multiplier_0 multiplier = {(void*)0,(__Block_byref_multiplier_0 *)&multiplier, 0, sizeof(__Block_byref_multiplier_0), 8};

    int(*MuBlock)(int) = ((int (*)(int))&__MyBlock__MyMethod_block_impl_0((void *)__MyBlock__MyMethod_block_func_0, &__MyBlock__MyMethod_block_desc_0_DATA, (__Block_byref_multiplier_0 *)&multiplier, 570425344));
    //打印代码之前,我们给这个__block修饰的变量对象的值赋值到了6
    (multiplier.__forwarding->multiplier) = 6;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_5bd0fd_mi_0,((int (*)(__block_impl *, int))((__block_impl *)MuBlock)->FuncPtr)((__block_impl *)MuBlock, 2));

}
//block方法实现
static int __MyBlock__MyMethod_block_func_0(struct __MyBlock__MyMethod_block_impl_0 *__cself, int num) {
  __Block_byref_multiplier_0 *multiplier = __cself->multiplier; // bound by ref


        return num * (multiplier->__forwarding->multiplier);

    }

打印代码之前,给这个__block修饰的变量对象的值赋值到了6,并且在block中的实现是直接通过(multiplier->__forwarding->multiplier)来进行操作,所以打印结果应该会是修改过后的值。也就是12.

栈的__Block修饰的变量.png

__forwarding存在的意义?

  • 不论在任何内存位置,都可以访问同一个__block变量。

__block相关的笔试题

- (void)MyMethod{
    
    __block int multiplier = 8;
    int(^MuBlock)(int) = ^int(int num){
        return num * multiplier;
    };
    multiplier = 6;
    NSLog(@"%d",MuBlock(2));
}

上述调用结果是12.

Block的内存管理

  • _NSConcreteGlobalBlock 全局block
  • _NSConcreteStackBlock 栈block
  • _NSConcreteMallocBlock 堆block

Block的内粗分布:

全局、栈、堆Block的内存分布.png

Block的copy操作

Block的copy操作.png

声明一个成员变量是block,如果这个block使用的是assign,通过成员变量去访问block的话,可能栈所对应的函数退出之后,在内存中就销毁掉了,所以继续访问可能崩溃

栈上Block的copy

当我们在栈上面的block进行copy以后,那么在MRC环境下是否会引起内存泄漏?
产生内存泄漏。

栈上block的copy操作.png

栈上的Block的销毁

栈上,等作用域

栈block的销毁.png

栈上__block变量的copy

__block修饰的变量都会转换成一个对象,而这个对象被copy的时候会在堆上面开辟一块空间生成一个全新的__block修饰的变量对象,而这两个对象的__forwarding指针都指向了堆上面的对象。如果没有做copy操作那么就使用的是栈上面的__block对象。

栈的__Block修饰的变量.png

Block代码的解读

@property(nonatomic,copy)MuBlock blk;
- (void)MyMethod{
    
    //用__block修饰的变量会在栈上生成一个block对象,里面有__forwarding指针和成员变量multiplier。__forwarding指针指向这个block对象自己
    __block int multiplier = 8;
    
    //当我向_blk属性赋值的时候实际上是对这个block进行copy,所以这个block会在堆上开辟一个空间。在堆上有另外一个副本
    _blk = ^int(int num){
        return num * multiplier;
    };
    
    /这个时候修改的值是__forwarding指向的对象的成员变量multiplier,因为_blk实际上是堆上面的block,下面的代码实际上是修改堆上面的__block对象
    multiplier = 6;
    [self excMuBlock];
    
}

- (void)excMuBlock{
    int result = _blk(4);
    NSLog(@"%d",result);
    结果应该是24.
}

代码分析题

block循环引用的问题

下面代码有何问题?

//在ARC环境下
- (void)MyMethod{
    //默认是strong属性
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    //默认是copy属性
    //_array、 _strblk都是当前属性的_array是strong,_strblk是copy
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@",_array[0]];
    };
    _strblk(@"hello");
}

下面继续分析clang代码


static void _I_MyBlock_MyMethod(MyBlock * self, SEL _cmd) {

    (*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_MyBlock$_array)) = ((NSMutableArray * _Nonnull (*)(id, SEL, ObjectType  _Nonnull __strong, ...))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("arrayWithObjects:"), (id _Nonnull)(NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_e9f218_mi_0, __null);

    (*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)) = ((NSString *(*)(NSString *__strong))&__MyBlock__MyMethod_block_impl_0((void *)__MyBlock__MyMethod_block_func_0, &__MyBlock__MyMethod_block_desc_0_DATA, self, 570425344));

    ((NSString *(*)(__block_impl *, NSString *__strong))((__block_impl *)(*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)))->FuncPtr)((__block_impl *)(*(__strong xhBlock *)((char *)self + OBJC_IVAR_$_MyBlock$_strblk)), (NSString *)&__NSConstantStringImpl__var_folders_8z_j2rlh3t16m9cdrljdz_r67380000gn_T_Myblock_e9f218_mi_2);

}

由中间码我们可以分析出来,当在block中使用_array的时候实际上是通过(*(NSMutableArray *__strong *)((char *)self + OBJC_IVAR_$_MyBlock$_array))这样的形式获取到_array,这样的话,相当于,在self中的block中又强引用了self这样就是循环引用。自循环引用

解决方案:

//第一种
- (void)MyMethod{
    
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    
    __weak NSArray *weakarray = _array;
    
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@",weakarray[0]];
    };
    
    _strblk(@"hello");
    
}
//第二种
- (void)MyMethod{
    
    _array = [NSMutableArray arrayWithObjects:@"111", nil];
    
     __weak typeof(self)weakself = self;
    
    _strblk = ^NSString *(NSString * str){
        return [NSString stringWithFormat:@"%@", weakself.array[0]];
    };
    
    _strblk(@"hello");
    
}


创建一个弱引用指针指向_array,这样在block中,使用的就是弱引用指针,当前对象释放后,弱引用也就会自动释放,当前当前对象的block也会被释放掉。

分析下面代码:

- (void)MyMethod{
    
    __block MyBlock *blockself = self;
    
    _blk = ^int(int num){

        return num * blockself.var;

    };
    
    _blk(3);
    
}

分析

  • MRC下,不会产生循环引用
  • 在ARC下会产生循环引用,引起内存泄漏

在self修饰为__block的时候,系统会自动生成一个对象__Block_byref_blockself_0
而这个对象强持有self。最终导致了,当前对象持有block,block持有__block对象,__block对象对象持有self造成大环引用。

ARC下的解决方案

大环循环引用.png

断环,当我block中的__block使用完成以后,需要直接赋值为nil这样就可以断环了。

- (void)MyMethod{
    
    __block MyBlock *blockself = self;
    
    _blk = ^int(int num){

         int = num * blockself.var;
         blockself = nil;
        return num * blockself.var;

    };
    
    _blk(3);
    
}

BLock面试题

什么是block?

block就是一个对象,这个对象封装了函数以及其上下文。

为什么block会产生循环引用?

  • 如果当前block对当前对象的成员变量有一个强引用,对象的成员变量归根究底的还是需要通过self去访问它,那么就是当前对象强引用了blcok,而block中又调用了self并将其强引用,这样就造成了自循环引用,解决方案使用__weak修饰你的成员变量,或者修饰当前对象。在block直接调用weak变量,

  • 大环引用,当使用__block修饰self,并且self强引用了block。这样就造成了大环引用。
    self强持有block,block强持有__block对象,__block对象强持有了self,这就是大环循环引用。解决方案就是使用断环的方式,但是有一个缺点就是,如果这个block不去执行的话永远不会解除这个循环引用。将__block修饰的self在block中使用完成置nil。

怎么样理解Block截获变量的特性?

  • 基本数据局部变量
  • 对象类型局部变量
    • 对其持有一个强引用,连同它的所有权
  • 对于静态局部变量
    • 对其指针进行截获
  • 静态全局变量、全局变量
    • 不产生截获的

你都遇到过哪些循环引用?你又是如何解决的?

block捕获的变量是当前的成员变量,block也是当前对象的成员变量,就会造成自循环引用,可以避免这种情况,对当前当前成员变量使用__weak所有权修饰符去解除
__block的情况也会造成循环引用,这种情况跟自循环引用不太一样,大环循环引用有个弊端就是当你在不调用block的情况下,这个循环引用会一直解不开。

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

推荐阅读更多精彩内容