iOS 底层原理 04 – Block 专题

发布于 2022-08-17  176 次阅读




Block 原理

Block 本质上也是 OC 的对象,那么也就是一个结构体,它内部也有一个 isa 指针
Block 是封装了函数调用(主要是函数的地址 FunPtr)以及函数调用环境的 OC 对象。
最简单的 block : ^{} 这种是指封装了内容,但是没有执行,相当于做了方法的实现,但是没有调用方法。 而方法的调用是这样 doSomeThing();,所以执行 block 是这样的 ^{}();

Block 底层结构

iOS 底层原理 04 - Block 专题

Block 变量捕获

为了保证 block 内部能够正常访问外部的变量, block 有个变量捕获机制(capture)

iOS 底层原理 04 - Block 专题

局部变量

auto: 离开作用于自动销毁,所以称为自动变量,又叫做局部变量(只能是局部,不能是全局)。在函数被调用时,系统为其分配空间,存储在动态存储区,函数执行后释放存储空间

static:在编译时,系统为其分配存储空间,存放在静态存储区(全局数据区)。

1. static 只能在当前文件中使用,哪怕被修饰了 extern ,外面也是无法访问。外面的其他文件可以拥有同样用 static 修饰的相同名字的变量,互不影响。
2. static 只被初始化一次,程序运行期间不消失,所以下次值是根据上次的值改变的。
3. static 修饰的全局变量和常规的静态全局变量,存储的位置都是全局数据区的静态存储区,但是 static 修饰的全局变量规定了作用于,只能在当前文件使用。常规的静态全局变量,施加了 static 之后会变成静态全局变量。
4. static 修饰的局部变量,相较于没有用 static 修饰的局部变量,只是改变了存储区域,也就是改变了它的生命周期。
5. static 定义的函数也是如此,只能在当前文件中使用。

无论是 auto 还是 static 类型的局部变量,都会被捕获,只不过前者是捕获值,后者是捕获指针。主要原因是因为局部变量的作用域,因为有可能存在跨函数访问,而局部变量只有在当前函数的作用域内有效,所以需要捕获,然后存起来。

全局变量

全局变量(无论是否是 static)并没有 capture 到 block 内部,也就是说没有在 block 结构体内部生成对应的新的变量或者指针变量。
主要是因为全局变量谁都可以访问,所以没必要捕获,直接访问即可。

Block 类型

Block 由于也是OC对象,它底层内部也有一个 isa指针(继承于 NSObject 的),所以它也是有类型的,而它的ISA指针就指向了它的类型。
Block 一共有 3 种类型:
__NSGlobalBlock__(__NSConcreteGlobalBlock)
__NSStackBlock__(__NSConcreteStackBlock)
__NSMallocBlock__(__NSConcreteMallocBlock)

  • 无论哪种类型,它们都继承于 NSBlock 这个基础类型,而NSBlock的父类就是NSObject,所以说它本质上是一个OC对象。可以通过调用 class 方法或者查看 isa 指针指向来看其具体类型。
  • 继承关系: __NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject ,其他几个也类似

应用程序内存分布图

iOS 底层原理 04 - Block 专题

  1. .text 区,也称为代码区,是内存的低地址地区
  2. .data 区,一般存放全局变量
  3. 堆区,动态分配内存,程序员通过 alloc/malloc 这种方式申请的内存,一般在这里。动态分配的特点,需要程序员去申请和销毁的。
  4. 栈区,放的局部变量。 系统会自动分配对应的内存,并且会自动回收。比如在离开大括号这种作用域之后,会自动回收内存。

Block 类型的区别

iOS 底层原理 04 - Block 专题

  • 不同的类型的 Block 调用了 copy 操作之后:

iOS 底层原理 04 - Block 专题

Global 类型的 block

没有访问 auto 类型的变量的 block ,也就是没有访问全局类型的 block 。这种 Block 存放在数据区,也就是全局区。

        //1. 没有访问局部变量的 block 为全局类型的 block :__NSGlobalBlock__
        void(^block1)(void)=^{
            NSLog(@"Block1 ------------- ");
        };

Stack 类型的 block

访问了 auto 类型变量的 block 。 这种 block 会存放在 Stack 栈区,代码执行完会自动销毁(MRC下),ARC下会自动进行 copy 操作,拷贝到 堆区

        //2. 访问了 auto 变量的,为 stack 的类型的 block:__NSStackBlock__ (非ARC下,如果是ARC下,则为 malloc ,这是因为 ARC 帮程序员做了一些 copy 的操作)
        // stack 类型的 block 会放在栈段,在超出作用域外,其实 block 被销毁了,所以调用可能会出现垃圾数据或异常。
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"Block2 ----------- age is %d",age);
        };

Malloc 类型的 block

对 stack 类型的 block 进行 copy 操作,就得到了 Malloc 类型的 block,存放在堆区。

void(^block3)(void) =  [block2 copy];

block 中对象类型的 auto 变量强弱引用

先看示例代码

        bchBlock baiBlock;
        {
            Person * p = [[Person alloc]init];
            p.age = 10;
            bchBlock  baiBlock =^{
                NSLog(@"p.age is %d",p.age);
            };
            
        }
        NSLog(@"------");
  • 在最下面的 NSLog 打印时,person 对象没有被释放。
  • 原因是:p 虽然是person的局部实例对象,但是 block 会捕获这个局部变量,而且捕获的是指针,而且这个 block 是在ARC下的堆空间内,因为大括号外面有个强制真引用,所以是从栈拷贝到了堆,从而没有释放,而它内部又通过捕获指针的方式强持有这个对象,所以 person 对象没有被释放。
  • 栈空间上的 block 没有能力保住外面的对象,但是堆空间上的 block 有能力保住外面的对象。

    MJPerson *p = [[MJPerson alloc] init];
    __weak MJPerson *weakP = p;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"1-------%@", p);
        
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-------%@", weakP);
        });
    });
  • 看 P 的强指针引用在哪个 block 结束,就在什么时候销毁。
  • 如果第一层是weak,但是最后一层是强引用,那么也不会在第一层销毁。

Block 修改变量

修改 block 外部的局部变量的时候,由于捕获机制,(block 内部产生一个一毛一样的变量,然后捕获的是值),所以不能直接修改。
如果要修改,要么改成 static 类型,这样捕获指针,就可以修改,要么改成全局变量(但是这样一直存在在内存中也不好)。
或者通过 __block 的形式

__block 原理及注意点

  • __block 不能修饰全局变量、static 变量,仅能修饰 auto 类型的变量
  • 编译器会将 __block 修饰的变量(基础变量或者是对象变量)包装成一个对象(结构体),实际上用到的这个变量是在这个结构体内部(内部有一个名字一毛一样的名字的变量),我们要改,也是改这个结构体内部的变量。
  • 外面修改的是结构体内部的那变量,打印的话,也是这个内部变量的地址。

iOS 底层原理 04 - Block 专题

  • 这里有个 __forwarding 指针指向这个结构体本身

__block 内存管理

  • copy 到堆上的时候 调用 copy 函数(多个 block 使用一个变量,则该变量只会被 copy 一次)

iOS 底层原理 04 - Block 专题

  • 移除强引用

iOS 底层原理 04 - Block 专题


Block 循环引用的问题

造成循环引用的原因

Block 造成循环引用的话,主要是因为: Block 是封装了环境和函数,其中环境就是捕获的变量(这里主要是指针),这个指针强指向了某个对象A。 而这个对象A里有对block的操作,所以A也强持有了Block,导致了它们之间互相强引用,无法释放。

如果 Block 使用了 self 或者成员变量,也有可能造成循环引用,因为 self 本质上是局部变量,block 捕获的是这个变量的指针,同理,成员变量,本身也是通过 self 调用的,所以也会造成循环引用。

解决方案

  1. __weak__unsafe_unretain 来解决
    让 block 内部强引用外面对象的那个变为弱引用就可以了。 外面的不能变为弱引用,因为外面要持有 block 对象才能使用 block 。
    __weak : 不会产生强引用,对象销毁后,原来的指针会自动变为 nil,所以不会产生野指针.
    __unsafe_unretain:也不会产生强引用。但是对象销毁后,原来的指针不会变为 nil ,还是原来的地址值 ,所以会产生野指针。

  2. __block (block 必须执行,block 内部必须要对对象进行置为nil的操作来打破循环)
    __block 修饰的对象变量会被包装成一个结构体,而 block 捕获的就是这个结构体,这个结构体内部有个 指针指向这个变量。外面看到的这个变量就是指这个结构体内部的指针变量。
    缺点:就是必须要执行 block ,如果没机会执行 block 还是会内存泄露。

MRC 下不支持弱指针的,所以只能通过 __unsafe_unretain 来操作,但这个终究不安全


小结

变量捕获

  • block 外的局部变量(无论是 auto 还是 static),会被自动捕获进 block 结构体内部,(准确的说,应该是 auto 类型的局部变量的值被捕获进去,static 的则是捕获其指针;对象类型的,则是捕获对应的指针,是强指针就捕获强指针,是弱指针就捕获弱指针),block 内部会产生一个一毛一样的变量,将这个值存储起来,block 函数体内用的就是这个自己产生的变量,和外面的没关系。
  • auto 变量由于会自动销毁,所以需要将变量值存起来,不然存地址的话,以后调用,由于auto变量已经消失了,就会导致访问内存的错误。
  • self_cmd 是 OC 的方法底层实际每个都会有的参数:self 是当前对象的指针,_cmd 是当前方法的一个 SEL 指针,所以这2个是局部变量,只要是局部变量都会被 block 捕获,
  • 同上,访问实例变量的时候,比如 _name ,实际上是相当于访问 self->_name,捕获的,通过属性访问也是如此。

copy 操作

  • ARC 下声明的属性,属性的类型是 block ,用的是 copy 关键字修饰,那么编译器会自动帮我们将 block copy 到堆。
  • (MRC 下)copy 到堆上的 block ,要进行 release 操作,防止内存泄露(ARC不需要这样操作)。
  • ARC 的环境下,编译器会根据情况将 stack 区的 block 拷贝至 栈区 :
  • block 作为函数返回值时。
  • 将 block 赋值给 __strong 指针时
  • block 作为 Cocoa API 中方法名含有usingBlock的方法参数时
  • block 作为 GCD API 里的参数的时候

对象类型的 auto 变量

  1. 如果 block 内部访问的对象类型的 auto 变量时:
  • 如果 block 是在栈上,无论 ARC 还是 MRC,都不会对 auto 变量产生强引用。
  • 如果 block 被拷贝到堆上,会调用 block 内部的 copy 函数,copy 函数内部又会调用 _Block_object_assign 函数,而这个函数会根据 auto 变量的修饰符(__strong/__weak/__unsage_unretained)做出对应的操作(类似于 retain,只是计数器 +1),形成强引用或弱引用。
  • 如果 block 从堆上移除,则会调用内部的 dispose 函数,而这个函数会调用_Block_object_dispose函数,而这个函数会自动release(只是计数器-1)之前引用的 auto 变量。变量是否释放,还是根据这个变量的引用计数是否为 0。
  • 只有 block 内部访问的是对象类型,block 底层的结构的 DESC 才会有上面2个函数来对这个对象进行一个内存管理的操作。基础数据类型则不用。

iOS 底层原理 04 - Block 专题

__block 内存管理

  • 当 block 在栈上的时候,对局部变量(基础类型或者对象类型)都不会产生强引用。
  • 当 block 拷贝到堆上的时候,都会调用 block 内部的assign函数,来进行引用(基础数据类型都是强引用。对象类型的变量,则根据外部传来的是weak还是strong进行对应的引用,这是ARC下。如果是 MRC 下,无论怎么写,对象类型,最终结构体内部都是弱引用
  • 当 block 从堆上移除的时候,都会调用 block 内部的 dispose函数,来进行释放,类似于 release 。

__forwarding 指针的作用

当 block 在栈上的时候,auto 变量也是在栈上,所以 __forwarding 指针指向自己没问题。
但是,当 block 被从栈拷贝到堆上的时候,这个 block 引用的auto变量也被拷贝到了堆上。
其实就是 __block 修饰的变量,也就是包装的对象(结构体A)中,有一个实际保存变量的结构体(结构体B),当结构体B在栈上的时候,那么 __forwarding 指针指向的就是它本身,外部可以正常访问里面的变量。
但是当从栈上拷贝到堆上的时候,那么原来 A 结构体中的B中的 __forwarding 指针指向就改成了堆中那份结构体B的地址,而B中 __forwarding 指针指向的是B在堆中的自己。

这个改变栈上 __forwarding 指针指向的操作是在 copy 到堆上的时候做的,也就是通过对这个变量封装的结构体内部的 __Block_object_assign 实现的

iOS 底层原理 04 - Block 专题

Block 解决循环引用

  1. ARC 下使用 __weak__unsafe_unretain__block(但是必须执行 block ,而且 block 内部要将对象置为 nil。不推荐) 来打破循环引用,但是推荐 __weak
  2. MRC 下推荐使用 __block,因为 MRC 下,用__block修饰的、封装的结构体对象内部的变量指针都是弱引用。
  3. __weak 是为了打破循环引用。而在 block 内部,再次用 __strong 修饰weak对象,是防止外面将 weak 对象提前释放掉。用 __strong 就保证了 block 内可以正常持有该对象,然后执行完毕,否则 block 内访问这个被置为 nil 的对象可能会出错。

面试题

block 的原理是怎样的? 本质是什么?

答:
Block 是封装了函数调用及函数调用环境的 OC 对象,也就是结构体。

_block的作用是什么? 有什么使用注意点?

答:

  1. 将所修饰的局部变量对象封装成一个结构体,但是不能修饰全局变量和 static 变量。
  2. 解决 block 内部不能修改变量的问题。
  3. 主要是注意内存管理,有哪几种不同类型的 block ,它们在内存的什么位置,有什么特性。
  4. 在 MRC 下,__block 包装的对象,不会对内部的变量有强引用。ARC 下根据情况。

block 的属性修饰词为什么是 copy ? 使用 block 有哪些使用注意点?

答:
block 一旦没有进行 copy 操作,就不会在堆上。
需要注意循环引用的问题。

block 在修改 NSMutableArray ,需不需要加上 _block ?

答:

不需要
增加或删除,仅仅是调用了这个数组的指针地址,向其发送消息。 本身并没有修改这个指针地址。


迷雾尽散 破晓而生