iOS底层分析-block(一)

791 阅读7分钟

这是我参与8月更文挑战的第20天,活动详情查看: 8月更文挑战

block的分类

iOSblock分为三类:

  • NSGlobalBlock:全局block
    • 位于全局区;
    • block内部不使用外部变量,或者只是用静态变量和全局变量;
  • NSMallocBlock:堆block
    • 位于堆区;
    • block内部使用局部变量或者OC属性,并且赋值给强引用;
  • NSStackBlock:栈block
    • 位于栈区;
    • NSMallocBlock一样,可以在内部使用局部变量或者OC属性。但是不能赋值给强引用或者Copy修饰的变量;

block的运行

  • NSGlobalBlock:全局block,内部不使用外部变量
  • NSMallocBlock:堆block,内部使用变量a
  • NSStackBlock:栈block,使用__weak进行弱引用(默认为强引用)

需要注意的是:block持有的是^{}的内存空间;

block操作引用计数

我们在使用block的过程中,经常会出现循环引用的问题,继而导致出现内存泄漏,视图无法释放dealloc不执行的问题;所以我们要熟练掌握不同的block对变量的持有情况;

解析:

  • 1、[NSObject new]之后,其引用计数为1;相信这一步打印大家应该是没有异议的;
  • 2、strongBlock打印3应该是出乎大家意料的,这是为什么呢?
    • 首先:strongBlock会捕获objc,从而导致objc的引用计数+1
    • 但是因为strongBlock内部使用了objc,并且是默认的强引用,所以strongBlock是个堆区的block;由于strongBlock是一个非全局的block,那么进行操作时会进行block内存的开辟一个result,然后会将原始的block也就是ablocksizeinvokeflagsisa等成员变量都会赋值给result一份,相当于将aBlock也就是原始的block拷贝了一份给result这个新开辟了内存空间的block;那么此时a因为被strongBlock强持有,会把的内存拷贝了一份到上去,那么其引用计数也要再+1,所以最终是3;这部分分析,可在block的底层源码中看到:
  • 3、weakBlock因为使用了__weak进行修饰,那么他就是个栈block栈block不进行内存拷贝的操作;其引用技术只能+14
  • 4、weakBlock拷贝了一份放在名为mallocBlock堆block上,所以引用计数再次+15;

此处,其实第二步操作可以看作是第三步和第四步操作合体;第二步分解之后就是第三步和第四步操作;

block的内存拷贝

为了便于操作block的成员变量,我们按照系统block来重写了一个block的结构;那么我们就可以对系统的block进行强转,然后操作成员变量;

我们来看一段代码:

  • _LGBlock是我们重写的block的结构
  • struct _LGBlock *blc = (__bridge struct _LGBlock *)weakBlock;将系统的block强制转换为_LGBlock;
  • blc->invoke = nil;这里我们想要论证一个点:我们强转之后的blcweakBlock是不是同一片的内存空间?如果是,那么对blcinvoke进行=nil操作,那么weakBlock也会被修改,将无法调用;
  • void(^strongBlock1)(void) = strongBlock;因为我们把strongBlock 声明为了id,此处这么写是为了使strongBlock具有block的特性,可以使用strongBlock()进行调用;其实可以直接写成void(^strongBlock1)(void) __strong strongBlock = weakBlock

我们来看一下代码的执行结果:

代码崩溃,表明invoke置空之后导致block找不到了;

我们对代码进行调整之后,再次运行:

进行copy操作之后,strongBlock的内存将会从上拷贝到上,那么strongBlockweakBlock已经不是同一片内存了;

分析到这里,我们应该对栈block堆block有一定的认知了;

block堆栈的释放

我们接着看一段代码的执行:

  • 因为strongBlock使用了局部变量,但是使用了__weak进行弱引用,所以strongBlock是个栈block,然后将strongBlock赋值给了weakBlock,所以此时weakBlock指向了栈block的内存空间,而strongBlock在所在代码块执行完毕之后就被释放;但是weakBlock的生命周期是在blockDemo3的整个代码区;所以虽然strongBlock最已经释放,但是由于weakBlock指向了其内存空间,最后依然能够正常打印出1 0;

代码执行如下:

我们将代码稍作修改,去掉strongBlock__weak,查看运行结果:

直接崩溃!!!Why???

解析:

  • 去掉了__weakstrongBlock成为了堆block,其生命周期为所在的代码块区域;
  • strongBlock赋值给了weakBlock,而weakBlock是一个堆block,所以会将strongBlock的指针拷贝给了weakBlock,他们两个的指针地址是一样的;当出了strongBlock的作用域之后,strongBlock就会释放,那么weakBlock所指向的内存空间将会不存在,所以会崩溃;

如图所示:

block的循环引用

  • 可以正常释放

  • 循环引用无法释放

接下来我们来看段代码:

UIView持有了blockblock捕获的self,所以不会造成循环引用;

self持有了blockblock又捕获了self,然后导致了retain cycle,造成循环引用;

那么我们怎么解决这个循环引用呢?

我们可以通过__weak typeof(self) weakSelf = self;来解决循环引用;

那么是不是通过__weak typeof(self) weakSelf = self;就能一劳永逸呢?我们再来看一段代码:

block中有一个延迟2秒执行的任务,我们在当前界面停留超过2秒在进行pop返回上级界面时,一切正常;

那么如果我们在2秒内就返回呢?

可以看到,如果我们快速的返回上级界面,dealloc执行之后,name值就获取不到了;因为weakSelf已经释放了,nil发送消息还是nil

那么如何解决呢?我们继续修改代码如下:

此时,即使我们快速的返回上级界面,dealloc执行,name也能够正常打印;__weak typeof(self) weakSelf = self;__strong typeof(weakSelf) strongSelf = weakSelf;结合使用可以避免数据的丢失;

strongSelf是一个临时变量,在作用域执行完毕之后就会释放;weakSelf可以自动置为nil

那么除此之外,还有没有其他方式可以避免循环引用呢?

我们修改代码如下:

此时,即使我们快速的返回上级界面,dealloc执行,name也能够正常打印;

解析:

  • vc是一个临时变量,对self进行持有;其生命周期只在NSLog之前有意义,然后进行=nil
  • 如果不进行=nil操作;那么self持有block,block持有vc,vc又持有self,最终导致循环引用;

除此之外,我们还可以通过参数的方式来解决循环引用;

我们修改block的定义,把typedef void(^KCBlock)(void);修改为typedef void(^KCBlock)(ViewController *);,代码修改如下:

正常运行,可以释放;

  • 因为vc是以参数的形式传递的,所以并不会被block捕获;

block循环引用的面试题

面试题一

static ViewController *staticSelf_;

- (void)blockWeak_static {
    __weak typeof(self) weakSelf = self;
    staticSelf_ = weakSelf;
}

上面方法是否会发生循环引用?答案是会发生循环引用!!!

weakSelf是个弱引用,那么为什么还会发生内存泄漏导致无法释放呢?

解析:

weakSelfself是同一片内存空间,是映射关系;可以通过打印进行验证:

那么staticSelf持有的实际上是self,而staticSelf是一个全局的静态变量,所以self无法释放;

面试题二

@property (nonatomic, copy) KCBlock doWork;
@property (nonatomic, copy) KCBlock doStudent;

- (void)block_weak_strong {
    __weak typeof(self) weakSelf = self;
    self.doWork = ^{
        __strong typeof(self) strongSelf = weakSelf;
        weakSelf.doStudent = ^{
            NSLog(@"%@", strongSelf);
        };
       weakSelf.doStudent();
    };
   self.doWork();
}

这个方法依然会造成内存泄漏

解析:

这个方法的问题出在

weakSelf.doStudent = ^{
    NSLog(@"doStudent:%@", strongSelf);
};
weakSelf.doStudent();

strongSelfdoStudent进行捕获持有,那么strongSelf的引用计数就会+1;所以在当前代码块执行完毕,strongSelf引用计数-1之后,其引用计数依然不为0;所以无法释放;