iOS Block学习与使用

Block

一.什么是block

block是将函数调用及其上下文封装的OC对象,内部也有isa指针

二.block有几种类型

  • block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
  • 注:auto变量(自动变量):离开当前作用域就销毁,默认省略
block类型

在ARC环境下,编译器会根据情况自动将栈上的block复制到堆上,比如以下情况

  1. block作为函数返回值时
  2. 将block赋值给__strong指针时
  3. block作为Cocoa API中方法名含有usingBlock的方法参数时
  4. block作为GCD API的方法参数时
  • 每一种类型的block调用copy后的结果如下所示


    block的复制效果

三.截获变量

  1. 对于基本数据类型的局部变量截获其值
  2. 对于对象类型的局部变量连同所有权修饰符一起截获
  3. 静态局部变量截取到内部为指针传递,外部修改其值后,block内部通过指针访问时获取最新值
  4. 全局变量和静态全局变量不捕获,直接访问


    block截获变量

eg:

   // 局部变量
   auto int a = 5;
   int(^block)(int) = ^int(int num){
       return a * num;
   };
   a = 7;
   NSLog(@"block 结果 = %d", block(4)); // 结果 = 20
   // 静态局部变量
   static int a = 5;
   int(^block)(int) = ^int(int num){
       return a * num;
   };
   a = 7;
   NSLog(@"block 结果 = %d", block(4)); // 结果 = 28
    // __block修饰符
   __block int a = 5;
    int(^block)(int) = ^int(int num){
        return a * num;
    };
    a = 7;
    NSLog(@"block 结果 = %d", block(4)); // 结果 = 28

局部变量需要捕获的原因是,block内部需要跨函数访问,所以需要先将局部变量存起来。全局变量不需要捕获,因为一直在内存中,可以直接访问。

问题:

  1. 下面这种情况block会捕获 self 吗?
- (void)test {
   void(^block)(void) = ^{
       NSLog(@"-------%p", self);
   }
}

答案是会捕获,因为在转换底层c语言会默认传递id self 和 SEL _cmd 两个参数,所以参数作为局部变量会被捕获。

  1. 下面这种情况block会捕获 _name 吗?
- (void)test {
  void(^block)(void) = ^{
      NSLog(@"-------%@", _name);
  }
}

其实block内部是访问了self里的_name, 所以是先对self进行捕获,再访问self中取_name(self->_name);所以并不是捕获_name。

截获对象类型的auto变量

  1. 如果block在栈上, 因为随时都有可能被销毁,所以无论是MRC还是ARC环境下,block内部访问对象类型变量时,不会产生循环引用

  2. 如果block被拷贝到堆上,会调用block内部的copy函数
    copy函数内部会调用 _Block_object_assign 函数

    _Block_object_assign 函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

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

  • 注意: 记住上面这两个函数,后面block的内存管理会提到这个两个函数
函数调用时机

四. block的属性修饰词

  1. MRC中建议使用copy

    @property (copy, nonatomic) void (^block)(void);
    
  2. ARC中建议使用strong和copy

    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

注意:block作为oc对象,如果用assign修饰,因为assign一般对基本数据类型修饰,基本数据类型存在栈区,有系统自己处理生命周期,如果assign作用在对象身上,只是单纯的指针赋值,当block对象释放后,指针没有被置nil,造成悬垂指针,再对其发送消息会造成崩溃。assign是指针赋值。
block一旦没有进行copy操作,就不会在堆上

如果向一个nil对象发消息不会crash的话,那么unrecognized selector sent to instance的错误是怎么回事?

这是因为这个对象已经被释放了(引用计数为0了),那么这个时候再去调用方法肯定是会Crash的,因为这个时候这个对象就是一个悬垂指针(指向僵尸对象(对象的引用计数为0,指针指向的内存已经不可用)的指针)了,安全的做法是释放后将对象重新置为nil,使它成为一个空指针,大家可以在关闭ARC后手动release对象验证一下。

OC中向nil发消息,程序是不会崩溃的。

因为OC的函数都是通过objc_msgSend进行消息发送来实现的,相对于C和C++来说,对于空指针的操作会引起crash问题,而objc_msgSend会通过判断self来决定是否发送消息,如果self为nil,那么selector也会为空,直接返回,不会出现问题。视方法返回值,向nil发消息可能会返回nil(返回值为对象),0(返回值为一些基础数据)或0X0(返回值为id)等。但对于[NSNull null]对象发送消息时,是会crash的,因为NSNull类只有一个null方法。

重点:这里要区别的是空指针、野指针和悬垂指针的区别!

空指针:

1> 没有存储任何内存地址的指针就称为空指针(NULL指针)
2> 空指针就是被赋值为0的指针,在没有被具体初始化之前,其值为0。

野指针:

野指针的产生是由于在首次使用之前没有进行必要的初始化。因此,严格地说,在编程语言中的所有为初始化的指针都是野指针。

悬垂指针

在许多编程语言中(比如C),显示地从内存中删除一个对象或者返回时通过销毁栈帧,并不会改变相关的指针的值。该指针仍旧指向内存中相同的位置,即使引用已经被删除,现在可能已经挪作他用。

参考assign修饰object类型会怎样?

@property (nonatomic ,assign) TestObject *property1;

- (void)test {
    TestObject *obj = [TestObject new];
    self.property1 = obj;
    self.property1.age = 1;
}
test方法执行完毕以后,该临时变量就会被释放,此时self.property1将变为悬垂指针。

五. __block 修饰符

使用block直接修改auto变量时出现下面这样的问题

是因为block截获auto变量时是值传递,不能访问到auto变量的指针地址,所以无法修改
有两种方案解决

// 第一种
__block int num = 10;
void(^block)(void) = ^{
    num = 20;
};
// 第二种
static int num = 10;
void(^block)(void) = ^{
    num = 20;
};
  1. static 修饰的auto变量被block捕获为指针捕获,所以可以在内部通过指针地址修改其值
  2. __block修饰的auto变量,在block内部被包装成一个对象
    __block不能修饰全局变量、静态变量(static)

eg: block内部调用age变量


下图可以看出在block内部被包装成一个 __Block_byref_age_0 的对象(记住这个对象,后面会用到)

__Block_byref_age_0 结构体内部存在一个 __forwarding 指针,__forwarding 指针类型为 __Block_byref_age_0对象。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
  NSObject *p = __cself->p; // bound by copy
  (age->__forwarding->age) = 20;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_e2457b_mi_0, p);
  }

重点在这一行代码(age->__forwarding->age) = 20; 可以看出age对象通过 __forwarding 指针找到age,再进行赋值,所以__forwarding 指针是一个指向自身的指针

⚠️warning

上图对array对象赋值,所以出现和上面一样的情况

不会报错的原因,是因为block捕获局部变量是值传递,只是使用这个array对象是可以的,并没有对其赋值和修改操作。

面试题:

__block NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1",@"3"]];
__block int i = 10;
void (^block)(void) = ^{
    [array addObject:@"2"];
    NSLog(@"%@,%d",array,i);
};
[array addObject:@"4"];
i = 20;
array = nil;
block();

结果:

test[48738:10976925] (null),20

如果删掉NSMutableArray 之前的__block 结果:

test[48911:10979878] (
    1,
    3,
    4,
    2
),20

可以看出"4"也是能加进去的,而且array = nil,并没有影响block内部的打印
原因猜测,可能不准确: block捕获auto对象类型变量,内部会执行copy函数,array引用计数+1,block执行了copy操作,从栈区copy到堆区,array也从栈区copy到堆区,所以,外部array=nil,只是栈区将array的指针只nil,但是内存地址还存在,不影响block内部。

六. __block内存管理

  1. __block修饰基本数据类型

    • 当block在栈上时,并不会对__block变量产生强引用
    • Block0在栈上,对Block0强引用时,ARC环境下,系统会自动将Block0 copy到堆上,同时__block修饰的变量也会被复制到堆上,此时堆上的Block0对__block修饰的变量时一个强引用的状态。
    • 当Block0被copy到堆上时,会调用block内部的copy函数,copy函数会调用 _Block_object_assign 函数,_Block_object_assign 函数会对__block修饰的变量形成强引用。

    • 当block从堆中移除时
    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的__block变量(release)


  2. __block修饰的对象类型

    • 当__block变量在栈上时,不会对指向的对象产生强引用
    • 当__block变量被copy到堆时
    • 会调用__block变量内部的copy函数
    • copy函数内部会调用 _Block_object_assign 函数
    • _Block_object_assign 函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
    • 如果__block变量从堆上移除
    • 会调用__block变量内部的dispose函数
    • dispose函数内部会调用 _Block_object_dispose 函数
    • _Block_object_dispose 函数会自动释放指向的对象(release)

    eg:

    MJPerson *person = [[MJPerson alloc] init];
    __block __weak MJPerson *weakPerson = person;
    MyBlock block = ^{
        NSLog(@"%p", weakPerson);
    };
    
    __block修饰的对象被block在内部包装成对象的结构体

可以看出,因为我们使用__weak修饰,所以结构体内部对person是__weak弱引用,所以 _Block_object_assign 函数会根据对象的修饰符做出相应的操作,当block调用dispose函数时,_Block_object_dispose 函数会自动释放指向的对象(release),对象也会调用自己结构体内的 __Block_byref_id_object_dispose 函数执行释放操作。
如果之前对象是强指针,会执行(release)操作,引用计数为0的话,就会销毁对象。
如果是__weak 弱引用的话,系统会在其生命周期结束时正常销毁。

七. block循环引用

  1. 循环引用产生的原因


  1. 用__weak、__unsafe_unretained解决

    • __weak : 不会产生强引用,指向的对象销毁时,会自动让指针置为nil
    • __unsafe_unretained: 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,产生悬垂指针,再次使用会造成crash
     __weak typeof(person) weakPerson = person;
    person.block = ^{
        NSLog(@"age is %d", weakPerson.age);
    };
    
    __unsafe_unretained id weakPerson = person;
    person.block = ^{
        NSLog(@"age is %d", weakPerson.age);
    };
    
对对象弱引用
  1. 用__block解决(必须要调用block)

    __block MJPerson *person = [[MJPerson alloc] init];
    person.age = 10;
    person.block = ^{
        NSLog(@"age is %d", person.age);
        person = nil;
    };
    person.block();
    

  1. 保证在整个执行过程self不会死
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) myself = weakSelf;
        
        NSLog(@"age is %d", myself->_age);
    };
  1. 介绍iOS中@strongify和@weakify
    @weakify 将当前对象声明为weak.. 
    这样block内部引用当前对象,就不会造成引用计数+1可以破解循环引用
    @strongify 相当于声明一个局部的strong对象,等于当前对象.
    可以保证block调用的时候,内部的对象不会释放
    
    可以理解为和第4个步骤一样。

    使用时注意,
    只在block外面使用__weak,内部没有__strong这个对象,可能会有问题
    在 block 中先写一个 strong self,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退。

  • 以 AFNetworking 中 AFNetworkReachabilityManager.m 的一段代码举例:
    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
         __strong __typeof(weakSelf)strongSelf = weakSelf;
    
        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
         strongSelf.networkReachabilityStatusBlock(status);
        }
    };
    
    • 只使用用__weak 修饰,会造成坏内存crash
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        weakSelf.test2Block = ^{
            NSLog(@"123");
        };
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3     * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 模拟testBlock执行一半时,再执行test2Block,此时 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
            weakSelf.test2Block();
        });
    };
    
    • 改进方法,添加__strong
    self.testBlock();
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        strongSelf.test2Block = ^{
            NSLog(@"123");
        };
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3     * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            strongSelf.test2Block();
        });
    };
    self.testBlock();
    

如果没有 strongSelf 的那行代码,那么后面的每一行代码执行时,self 都可能被释放掉了,这样很可能造成逻辑异常。
特别是当我们正在执行 strongSelf.networkReachabilityStatusBlock(status); 这个 block 闭包时,如果这个 block 执行到一半时 self 释放,那么多半情况下会 Crash。

不会产生循环引用的情况
  1. block作为Cocoa API中方法名含有usingBlock的方法参数时

    // UIView动画
    [UIView animateWithDuration:0.2 animations:^{
        self.alpha = 1;
    }];
    // 快速枚举
    [self.dataArray enumerateObjectsUsingBlock:^(NSString *str, NSUInteger idx, BOOL * _Nonnull stop) {
        [self dosomething:str];
    }];
    
  2. block作为GCD API的方法参数时

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"不会产生循环引用");
    });
    
  3. 类方法
    类似于第一种,自定义类方法,block作为方法参数时,也不会造成循环引用,因为self无法对一个类强引用。

  4. masonry

    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    block并没有被view引用,block执行完毕就会释放,不会造成循环引用。

  5. 局部变量
    局部变量的block没有被其他对象强引用的时候,在当前作用域结束就会销毁。

  6. AFN
    AFN3.0之前 AFURLConnectionOperation 里的一个请求结束之后,setCompleteBlock会把block设置为nil,来打破循环引用。


    16410253422245.jpg

3.0以后

AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
[manager POST:urlStr parameters:parm progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *  _Nullable responseObject) {    
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {     
}];

当然在项目上一般不会直接使用AFHTTPSessionManager,会封装一层,我们先看[AFHTTPSessionManager manager], 也就是说每次都相当于 [[AFHTTPSessionManager alloc] init], 在函数中,AFHTTPSessionManager * manager是一个局部变量, 随着函数栈的调用结束,这个局部变量也就被回收了. self并没有持有manager对象.

解决循环引用问题 - MRC
  1. 用__unsafe_unretained解决


    __unsafe_unretained解决
  2. 用__block解决


    __block解决

    上面提到过,MRC下在结构体内部__block 不会对对象产生强引用

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

推荐阅读更多精彩内容

  • Block是一个代码块, 类似匿名函数, 是封装了函数及其上下文的OC对象,也可以叫做闭包。 闭包就是能够读取其它...
    小李不木阅读 1,057评论 0 1
  • blcok 本质上也是个 oc 对象,其内部结构也是具有 isa 指针,是封装了函数调用以及函数调用环境的 OC ...
    Hugin阅读 951评论 4 0
  • 前言 block是日常iOS开发高频率使用的闭包,之前也看过不少文章,但是一直疏于总结,今日再次深入研究一下,并记...
    妖精的菩萨阅读 471评论 0 0
  • 1. Block内存管理 OC代码转换成C++代码 _block的内部要调用外边的变量,_block的desc0的...
    switer_iOS阅读 687评论 0 0
  • Block 这一篇我们来研究一下objc的block并回答一下面试中的下列问题: 1.block的内部实现,结构体...
    iOS猿_员阅读 2,621评论 0 6