ios block的全方位解刨(block用什么修饰,block的循环引用,weakSelf 需要配合 strongSelf 使用,操作Block外部的变量)

Block的简介

Block 的官方定义是这样的:Block块是封装工作单元的对象,是可以在任何时间执行的代码段,其本质是可移植的匿名函数,可以作为方法和函数的参数传入,可以从方法和函数中返回。

在iOS4以后,越来越多的系统级的API在使用Block。苹果对于Block的使用主要集中在如下几个方面:


 - 完成处理–Completion Handlers
 - 通知处理–Notification Handlers
 - 错误处理–Error Handlers
 - 枚举–Enumeration
 - 动画与形变–View Animation and Transitions
 - 分类–Sorting
 - 线程管理:GCD/NSOperation

第二部分:操作Block外部的变量
Q:访问Block之外的变量?

int age=10;
void (^Block)(void) = ^{
    NSLog(@"age:%d",age);
};
Block();
age = 20;
Block();

输出值为 age:10
输出值为 age:10
原因:创建block的时候,已经把age的值存储在里面了。
注意此时觉得不能对block里面age的值修改,会报错

Q:下列代码输出值分别为多少?

auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

输出结果为:age:10,num:11
愿意:auto变量block访问方式是值传递,static变量block访问方式是指针传递
源码证明

int age = __cself->age; // bound by copy
int *num = __cself->num; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d2875b_mi_0, age, (*num));

int age = 10;
static int num = 25;

block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, age, &num));

age = 20;
num = 11;

上述代码可查看 static修饰的变量,是根据指针访问的

Q:为什么block对auto和static变量捕获有差异?

auto自动变量可能会销毁的,内存可能会消失,不采用指针访问;static变量一直保存在内存中,指针访问即可

Q:修改block之外的变量

在block中假如需要修改block之外定义的变量时,那么在定义变量时必须加上__block关键字这个比较常用

  self.shadowView.alpha = 0.0f;
   __block typeof(self) bself = self;
[UIView animateWithDuration:0.2f animations:^{
    
        bself.shadowView.alpha = 1.0f;
    
    } completion:^(BOOL finished){
    
    }];

或者

改变 result的结果
__block BOOL result = NO;
[self.dataBaseQueue inDatabase:^(FMDatabase *db){
    
        result = [db executeUpdate:sql];
        
    }];

Q:block在修改NSMutableArray,需不需要添加__block?
不需要。

NSMutableArray *array = [NSMutableArray array];
    void(^block)(void) = ^{
        [array addObject:@123];
    };
    Block();

这里  对 array 只是一个使用,而不是赋值,所以不需要 _ _block 进行修饰


错误的例子
  NSMutableArray *array = nil;
    void(^block)(void) = ^{
            array = [NSMutableArray array];
    };
    Block();

这里就需要在array的声明处添加__block修饰符,不然编译器会报错

总结下,对变量进行赋值的时候,下面这些不需要__block修饰符

Q:block能否修改变量值?

auto修饰变量,block无法修改,因为block使用的时候是内部创建了变量来保存外部的变量的值,block只有修改内部自己变量的权限,无法修改外部变量的权限。
static修饰变量,block可以修改,因为block把外部static修饰变量的指针存入,block直接修改指针指向变量值,即可修改外部变量值。
全局变量值,全局变量无论哪里都可以修改,当然block内部也可以修改。

Q:__block 修饰符作用?

  • __block可以用于解决block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(static)
  • 编译器会将__block变量包装成一个对象
  • __block修改变量:age->__forwarding->age
  • __Block_byref_age_0结构体内部地址和外部变量age是同一地址

在这里插入图片描述

Q:block的属性修饰词为什么是copy?

block一旦没有进行copy操作,就不会在堆上
block在堆上,程序员就可以对block做内存管理等操作,可以控制block的生命周期
@property (copy, nonatomic) CMBCAddressUIBlock finishCompletion;

Block分类**

block的类型,取决于isa指针,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock __ ( _NSConcreteGlobalBlock )
__NSStackBlock __ ( _NSConcreteStackBlock )
__NSMallocBlock __ ( _NSConcreteMallocBlock )

各类型的block在内存中如何分配的?

  • __NSGlobalBlock __ 在数据区
  • __NSMallocBlock __ 在堆区(访问外部变量强引用在堆区)
  • __NSStackBlock __ 在栈区(访问外部变量弱引用在栈区)
  • 堆:动态分配内存,需要程序员自己申请,程序员自己管理
  • 栈: 自动分配内存,自动销毁,先入后出,栈上的内容存在自动销毁的情况
    在这里插入图片描述

对每种类型block调用copy操作后是什么结果?

  • __NSGlobalBlock __ 调用copy操作后,什么也不做
  • __NSStackBlock __ 调用copy操作后,复制效果是:从栈复制到堆;副本存储位置是堆
  • __NSStackBlock __ 调用copy操作后,复制效果是:引用计数增加;副本存储位置是堆

对象类型的auto变量**

Q:当block内部访问了对象类型的auto变量时,是否会强引用?
答案:分情况讨论,分为栈block和堆block
栈block
a) 如果block是在栈上,将不会对auto变量产生强引用
b) 栈上的block随时会被销毁,也没必要去强引用其他对象
堆block
1.如果block被拷贝到堆上:
a) 会调用block内部的copy函数
b) copy函数内部会调用_Block_object_assign函数
c) _Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
2.如果block从堆上移除
a) 会调用block内部的dispose函数
b) dispose函数内部会调用_Block_object_dispose函数
c) _Block_object_dispose函数会自动释放引用的auto变量(release)
正确答案:

如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象
如果block在堆空间,如果外部强引用,block内部也是强引用;如果外部弱引用,block内部也是弱引用

Q:__weak 在使用clang转换OC为C++代码时,可能会遇到以下问
题cannot create __weak reference in file using manual reference

解决方案:支持ARC、指定运行时系统版本,比如
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

Q1:gcd的block中引用 Person对象什么时候销毁?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    Person *person = [[Person alloc] init];
    person.age = 10;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%d",person.age);
    });
    
    NSLog(@"touchesBegan");
}

原因:gcd的block默认会做copy操作,即dispatch_after的block是堆block,block会对Person强引用,block销毁时候Person才会被释放。

Q2:上述代码如果换成__weak,Person什么时候释放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");
}

原因:使用__weak修饰过后的对象,堆block会采用弱引用,无法延时Person的寿命,所以在touchesBegan函数结束后,Person就会被释放,gcd就无法捕捉到Person。

Q4:如果gcd内部先强引用后弱引用,Person什么时候释放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",weakPerson);
        });
        NSLog(@"1-----age:%p",person);
    });

    NSLog(@"touchesBegan");
}

原因:Person会等待强引用执行完毕后释放,只要强引用执行完,就不会等待后执行的弱引用,会直接释放的,所以Person释放时间为4秒。

block循环引用:

循环引用原因

Block的循环引用原理和解决方法大家都比较熟悉,此处将结合上文的介绍,介绍一种不常用的解决Block循环引用的方法和一种借助Block参数解决该问题的方法。
Block循环引用原因:一个对象A有Block类型的属性,从而持有这个Block,如果Block的代码块中使用到这个对象A,或者仅仅是用用到A对象的属性,会使Block也持有A对象,导致两者互相持有,不能在作用域结束后正常释放。

循环引用解决方式

解决原理:对象A照常持有Block,但Block不能强引用持有对象A以打破循环。

第一种方式:__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

第二种方式:__block

__block的作用:能够对外部的变量操作和修改

 __block XXController *blkSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",blkSelf);
    };

注意上述代码仍存在内存泄露,因为:

  • XXController对象持有Block对象blk
  • blk对象持有__block变量blkSelf
  • __block变量blkSelf持有XXController对象
   __block XXController *blkSelf = self;
    self.blk = ^{
        NSLog(@"In Block : %@",blkSelf);
        blkSelf = nil;//不能省略
    };
    
    self.blk();//该block必须执行一次,否则还是内存泄露

在block代码块内,使用完使用完__block变量后将其设为nil,并且该block必须至少执行一次后,不存在内存泄露,因为此时:

  • XXController对象持有Block对象blk
  • blk对象持有__block变量blkSelf(类型为编译器创建的结构体)
  • __block变量blkSelf在执行blk()之后被设置为nil(__block变量结构体的__forwarding指针指向了nil),不再持有XXController对象,打破循环

第二种使用__block打破循环的方法,优点是:

  • 可通过__block变量动态控制持有XXController对象的时间,运行时决定是否将nil或其他变量赋值给__block变量
  • 不能使用__weak的系统中,使用__unsafe_unretained来替代__weak打破循环可能有野指针问题,使用__block则可避免该问题

其缺点也明显:

  • 必须手动保证__block变量最后设置为nil
  • block必须执行一次,否则__block不为nil循环应用仍存在

因此,还是避免使用第二种不常用方式,直接使用__weak打破Block循环引用

第三种方式:__weak

__weak 加入弱引用表示同一个指针地址,weakSelf 是否对引用计数处理

将在Block内要使用到的对象(一般为self对象),以Block参数的形式传入,Block就不会捕获该对象,而将其作为参数使用,其生命周期系统的栈自动管理,不造成内存泄露

    __weak typeof(self) weakSelf = self;
    self.blk = ^{
        __strong typeof(self) strongSelf = weakSelf;
        NSLog(@"Use Property:%@", strongSelf.name);
        //……
    };
    self.blk();

为什么 weakSelf 需要配合 strongSelf 使用

__weak typeof(self) weakSelf = self;
[self doSomeBackgroundJob:^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (strongSelf) {
        ...
    }
}];

在 block 中先写一个 strongSelf,其实是为了避免在 block 的执行过程中,突然出现 self 被释放的尴尬情况。通常情况下,如果不这么做的话,还是很容易出现一些奇怪的逻辑,甚至闪退。

strongSelf 释放时机?

strongSelf 的作用域在block里面,也就是block执行完毕后就会被释放掉。

比如下面这样

__weak __typeof__(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    [weakSelf doSomething];
    [weakSelf doOtherThing];

});

在 doSomething 内,weakSelf 不会被释放.可是在执行完第一个方法后 ,weakSelf可能就已经释放掉,再去执行 doOtherThing,会引起 一些奇怪的逻辑,甚至闪退。
所以需要这么写

__weak __typeof__(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    __strong __typeof(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doOtherThing];
});

在比如

 __weak typeof(self) weakSelf = self;
    [self.collectionView performBatchUpdates:^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            [strongSelf.collectionView deleteItemsAtIndexPaths:@[ previousIndexPath ]];
            [strongSelf.collectionView insertItemsAtIndexPaths:@[ newIndexPath ]];
        }
    } completion:^(BOOL finished) {
        __strong typeof(self) strongSelf = weakSelf;
        if ([strongSelf.dataSource respondsToSelector:@selector(collectionView:itemAtIndexPath:didMoveToIndexPath:)]) {
            [strongSelf.dataSource collectionView:strongSelf.collectionView itemAtIndexPath:previousIndexPath didMoveToIndexPath:newIndexPath];
        }
    }];

优点:

  • 简化了两行代码,更优雅
  • 更明确的API设计:告诉API使用者,该方法的Block直接使用传进来的参数对象,不会造成循环引用,不用调用者再使用weak避免循环
__weak 能够自动置为nil的原因?

依赖于终中介者模式,自己不方便nil就交给别人来处理

第四种方式:self->block->self-通讯

self.block = ^(ViewController *vc) {
        NSLog(@"%@",vc.name)
    };

分析
self 对block 持有,但是vc)没有对block 持有,所以不造成循环引用。它只是作为一种临时变量压栈进去。

第五种方式:proxy

self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(test1) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

-(void)test1{

    NSLog(@"%s",__FUNCTION__);

}

1.timer的timerWithTimeInterval这个方法里面的参数target,接收的是viewController的内存地址,而在该方法内部,会形成一个对viewController 的强引用;
2.而恰巧 timer 是viewController的一个强指针属性,这就造成了强循环引用

因此我们可以设法在timer和viewController之间加入一个中间人。你timer不是谁把地址传给你你就强引用谁吗?好嘛,你本来是要强引用viewController的,现在你不必了,中间人的地址传给你timer,你timer要调用viewController的什么方法,你先告知中间人,中间人因为这种弱引用了viewController,所以可以把你timer发过来的消息转发给viewController。这样,即解决了消息传递问题,又能将强循环引用斩断,一举两得。
请添加图片描述

block 底层编译时

block 的本质(结构体)

如图我简单的声明了一个block查看汇编
请添加图片描述

block 结构体原型

struct __mian_block_impl_0 {
  struct __block_impl impl;
  struct __mian_block_desc_0* Desc;
  __mian_block_impl_0(void *fp, struct __mian_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

总结block 是一个结构体,所以block能够打印“%@”

{}的由来

如图所示

 void(*block)(void) = ((void (*)())&__mian_block_impl_0((void *)__mian_block_func_0, &__mian_block_desc_0_DATA));

这两个方法是有两个参数:
1.__mian_block_func_0
2.&__mian_block_desc_0_DATA

static void __mian_block_func_0(struct __mian_block_impl_0 *__cself) {
  printf("赵苗苗");
    }

在结构体里面传给了 impl.FuncPtr = fp

 __mian_block_impl_0(void *fp, struct __mian_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp; // __mian_block_func_0 这个函数给了fp 手法:函数式:(以函数作为参数传进来)
    Desc = desc;
  }

小知识点:以函数式作为参数就是函数式编程

总结结构体里面有匿名函数:({})

block 为什么需要调用block()

((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

简洁化


// 为什么参数是block 
// block 里面包含了所有参数,隐藏参数
// 函数里面拿不到block,所以把block传进来
 block->FuncPtr(block);

因为函数的声明和具体的实现

block 为什么捕获外部变量要使用__block

不使用__block如图所示

请添加图片描述

static void __mian_block_func_0(struct __mian_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy 
    printf("赵苗苗 --- %d",a);
    }

分析

1.a 在编译时就自动生成了相应的变量
2. int a = __cself->a 是值拷贝
3. 值拷贝:内存地址与原来的内存地址不一样,所以只能读,不能修改

使用__block如图所示

请添加图片描述

分析

 __Block_byref_a_0 a = {
        // a 是外部变量的地址
        (void*)0,(__Block_byref_a_0 *)&a,
        0, sizeof(__Block_byref_a_0),
        // 值
        10
        
    };

1.把原来的地址和值封装成对象传给了结构体

static void __mian_block_func_0(struct __mian_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref // 指针拷贝
    
    // 所以对block 里面的++,就是对外面的++
        (a->__forwarding->a)++;
        printf("赵苗苗 --- %d",(a->__forwarding->a));
    }

__Block_byref_a_0 *a = __cself->a 它是一个指针拷贝
所以对block 里面的++,就是对外面的++

block 底层运行时

block Copy 如何从栈区拷贝到堆区的。

解读过的源码



  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值