iOS八股文(十六)关于Block,你在第几层?

466 阅读12分钟

Block在iOS开发中有举足轻重的地位,Block能理解和使用透彻的开发者并不多,借助此次整理八股文机会,透彻的聊一聊Block,看看关于Block,你在第几层。

Block的分类

Block根据其类型可以分为三类:

  • 全局Block(NSGlobalBlock
  • 栈Block(NSMallocBlock
  • 堆Block(NSStackBlock

而其区分的规则为:

如果没有引用局部变量,或者只引用了静态变量和全局变量,则为全局Block,如果内部有使用局部变量,如果有被强指针引用过,就是堆Block,如果没有则为栈Block。

- (void)func2 {
    /**
     — 全局block,没有使用局部变量,或者只使用了静态变量或者只使用了全局变量
     */
    // 没有使用局部变量
    NSLog(@"block0 - %@",^{});
    // 使用了静态变量
    void(^block1)(void) = ^{
        blockInt = 3;
    };
    NSLog(@"block1 - %@",block1);
    
    /**
     - 堆Block 使用局部变量 并且用强指针引用过
     */
    // 即使用局部变量又使用,静态变量
    NSInteger i = 1;
    void(^block2)(void) = ^{
        NSLog(@"block %ld", i);
        NSLog(@"block static %d", blockInt);
    };
    NSLog(@"block2 - %@",block2);
    // 只使用局部变量
    void(^block3)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block3 - %@",block3);
    // 使用强指针引用过,再使用若指针引用
    void(^ __weak block4)(void) = block3;
    NSLog(@"block4 - %@",block4);
    /**
     - 栈Block 没有被强引用过的
     */
    // 没有使用强指针引用过
    void (^__weak block5)(void) = ^{
        NSLog(@"block %ld", i);
    };
    NSLog(@"block5 - %@",block5);
}

分析:
block0 : 没有使用任何变量,属于全局block。
block1 : 只使用了静态变量blockInt,属于全局block。
block2 : 使用了局部变量和静态变量,并且有被strong引用过,属于堆block
block3 : 使用了局部变量i,并且有被strong引用过,属于堆block
block4 : 虽然被weak指针引用的,但其已经被strong引用过,属于堆block
block5 : 没有被strong指针引用过。即使使用了局部变量,属于栈block

打印结果:

image.png

Block本质

可以将block代码通过clang 重写为C++代码,看其底层实现

int object_c_origin_block() {
    NSObject *obj = [[NSObject alloc] init];
    void(^ block)(void) = ^ {
        NSLog(@"%@",obj);
    };
    block();
    return 0;
}

重写命令

xcrun -sdk iphoneos clang -rewrite-objc <OriginFile> -o <CppFile>

得到Block结构的定义:

struct __object_c_origin_block_block_impl_0 {
  struct __block_impl impl;
  struct __object_c_origin_block_block_desc_0* Desc;
  NSObject *obj;
  __object_c_origin_block_block_impl_0(void *fp, struct __object_c_origin_block_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

从重写的结果,我们可以得到Block的以下特点:

  • block出生就是在栈上(isa指针指向_NSConcreteStackBlock)
  • block有捕获变量的能力(__object_c_origin_block_block_impl_0内部有obj变量)
  • block也是个对象(存在isa指针)

Block 循环引用

在使用Block的时候,最容易出现的问题就是循环引用,尤其是在mvvm架构中,Controller引用ViewModel,ViewModel引用Block,有的Block的复制在Controller里面完成,有可能会捕获到Controller,从而造成循环引用。

- (void)bindViewModel {
    self.viewModel.refreshViewCallBack = ^(void) {
        [self.tableView reloadData];
    };
}

而解决循环引用有一下几种办法:

__weak __strong协作

- (void)bindViewModel01 {
    __weak typeof(self) weakSelf = self;
    self.viewModel.refreshViewCallBack = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf.tableView reloadData];
    };
}

block对象,并没有引用self,在执行block的时候strongSelf的生命周期只有在block内部,在block内部,self的引用计数+1,当执行完block,引用计数-1。既没有引起循环引用,又适当延长了self的生命周期,一举双得。

__block

- (void)bindViewModel02 {
    __block Controller *blockSelf = self;
    self.viewModel.refreshViewCallBack = ^{
        [blockSelf.tableView reloadData];
        blockSelf = nil;
    };
}

使用这种方式,同样也可以解决循环引用,但是要注意,block执行完一次,下一次执行之前记得要给blockSelf重新复制,不然会出问题,显然维护这个是非常麻烦的,所以不推荐。

参数传递

- (void)bindViewModel03 {
    self.viewModel.refreshViewBlcok = ^(OSMVVMViewController * _Nonnull vc) {
        [vc.tableView reloadData];
    };
    
    self.viewModel.refreshViewBlcok(self);
}

通过block的参数进行传递,同样可以解决循环引用,但是这样做的意义不大,因为block在执行的地方,一定是需要获取到self的,如果已经获取到self了,就可以直接对self操作了,再使用block有点多余。应用并不多,只做了解。

Block中对象的引用计数

来道经典的面试题:

- (void)func3 {
    NSObject *o = [[NSObject alloc] init];
    NSLog(@"CFGetRetainCount print start");
    NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    //1
    void(^strongBlock)(void) = ^ {
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    strongBlock();
    //3  在栈区引用一次,在堆区又引用一次
    
    void(^ __weak weakBlock)(void) = ^{
        NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)o));
    };
    weakBlock();
    // 4栈区引用一次
    
    void(^copyBlock)(void) = [strongBlock copy];
    copyBlock();
    // 4 本来就在栈上不用+1。
    
    void(^copyBlock1)(void) = [weakBlock copy];
    copyBlock1();
    // 5
    NSLog(@"CFGetRetainCount print end");
}

Block Copy

在Block源码中,有关于BlockCopy的源码:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
// 拷贝 block,
// 如果原来就在堆上,就将引用计数加 1;
// 如果原来在栈上,会拷贝到堆上,引用计数初始化为 1,并且会调用 copy helper 方法(如果存在的话);
// 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
// 参数 arg 就是 Block_layout 对象,
// 返回值是拷贝后的 block 的地址
// 运行?stack -》malloc
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    // 如果 arg 为 NULL,直接返回 NULL

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    // 强转为 Block_layout 类型
    aBlock = (struct Block_layout *)arg;
    const char *signature = _Block_descriptor_3(aBlock)->signature;
    
    // 如果现在已经在堆上
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        // 就只将引用计数加 1
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果 block 在全局区,不用加引用计数,也不用拷贝,直接返回 block 本身
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        // block 现在在栈上,现在需要将其拷贝到堆上
        // 在堆上重新开辟一块和 aBlock 相同大小的内存
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        // 开辟失败,返回 NULL
        if (!result) return NULL;
        // 将 aBlock 内存上的数据全部复制新开辟的 result 上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        // 将 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清为 0
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        // 将 result 标记位在堆上,需要手动释放;并且引用计数初始化为 1
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // copy 方法中会调用做拷贝成员变量的工作
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        // isa 指向 _NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  • copy的调用时机,有强指针第一次指向的时候,会调用一次copy。而这次copy,会把栈区的Block拷贝到堆区。在平时使用 [block copy]的时候,也会调用。
  • copy的时候根据Block的类型采取了不同的操作,如果是堆block,只进行引用计数+1,相当于浅拷贝,如果是全局block,直接返回,相当于不拷贝,如果是栈block,是重新开辟新的内存并创建,并且isa指向_NSConcreteMallocBlock,设置类型为堆block,然后返回。

_Block_copy源码中,从栈区copy到堆区的过程中,_Block_call_copy_helper(result, aBlock)的调用时为了复制栈区的Block里面的成员变量,给堆区的Block。

// 调用 block 的 copy helper 方法,即 Block_descriptor_2 中的 copy 方法
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    // 取得 block 中的 Block_descriptor_2
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    // 如果没有 Block_descriptor_2,就直接返回
    if (!desc) return;

    // 调用 desc 中的 copy 方法,copy 方法中会调用 _Block_object_assign 函数
    (*desc->copy)(result, aBlock); // do fixup
}

其中最最要的代码时找到了一个copy方法然后调用。而这个copy方法是在desc中。需要理解这些代码,需要借助clang重写的c++代码。

在重写的代码中可以看到__object_c_origin_block_hello_block_impl_0的构造函数的调用:

void(* block)(void) = ((void (*)())&__object_c_origin_block_hello_block_impl_0
                           (
                            (void*)__object_c_origin_block_hello_block_func_0,
                            &__object_c_origin_block_hello_block_desc_0_DATA,
                            (__Block_byref_obj_0 *)&obj,
                            570425344)
                           );

可以看第三个参数的初始化:

    __object_c_origin_block_hello_block_desc_0_DATA = {
        0,
        sizeof(struct __object_c_origin_block_hello_block_impl_0),
        __object_c_origin_block_hello_block_copy_0,
        __object_c_origin_block_hello_block_dispose_0
    };

其实这里根据变量名称,就能猜个大概,__object_c_origin_block_hello_block_desc_0_DATA就是源码中的desc,而__object_c_origin_block_hello_block_copy_0即为(*desc->copy)方法。所以我们要看其中拷贝成员变量的过程,需要关注__object_c_origin_block_hello_block_copy_0的实现。

static void __object_c_origin_block_hello_block_copy_0(
     struct __object_c_origin_block_hello_block_impl_0*dst,
     struct __object_c_origin_block_hello_block_impl_0*src)
{
        _Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
}

这里是因为重写前的objcet-c代码里面的block捕获了1个局部变量,只用copy一个成员变量所以只会有一句,如果捕获多个局部变量,就会有多句。例如:

static void __object_c_origin_block_hello_block_copy_0(
    struct __object_c_origin_block_hello_block_impl_0*dst, 
    struct __object_c_origin_block_hello_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);
 }

其内部其实是调用了_Block_object_assign方法,在源码中也可以找到_Block_object_assign的实现:

/*******************************************************
 block 可以引用 4 种不同的类型的对象,当 block 被拷贝到堆上时,需要 help,即帮助拷贝一些东西。
 1)基于 C++ 栈的对象
 2)Objective-C 对象
 3)其他 Block
 4)被 __block 修饰的变量
 
 block 的 helper 函数是编译器合成的(比如编译器写的 __main_block_copy_1() 函数),它们被用在 _Block_copy() 函数和 _Block_release() 函数中。copy helper 对基于 C++ 栈的对象调用调用 C++ 常拷贝构造函数,对其他三种对象调用 _Block_object_assign 函数。 dispose helper 对基于 C++ 栈的对象调用析构函数,对其他的三种调用 _Block_object_dispose 函数。
 
 _Block_object_assign 和 _Block_object_dispose 函数的第三个参数 flags 有可能是:
 1)BLOCK_FIELD_IS_OBJECT(3) 表示是一个对象
 2)BLOCK_FIELD_IS_BLOCK(7) 表示是一个 block
 3)BLOCK_FIELD_IS_BYREF(8) 表示是一个 byref,一个被 __block 修饰的变量;如果 __block 变量还被 __weak 修饰,则还会加上 BLOCK_FIELD_IS_WEAK(16)
 
 所以 block 的 copy/dispose helper 只会传入四种值:3,7,8,24
 
 上述的4种类型的对象都会由编译器合成 copy/dispose helper 函数,和 block 的 helper 函数类似,byref 的 copy helper 将会调用 C++ 的拷贝构造函数(不是常拷贝构造),dispose helper 则会调用析构函数。还一样的是,helpers 将会一样调用进两个支持函数中,对于对象和 block,参数值是一样的,都另外附带上 BLOCK_BYREF_CALLER (128) bit 的信息。#疑问:调用的这两个函数是啥?BLOCK_BYREF_CALLER 里究竟存的是什么??
 
 所以 __block copy/dispose helper 函数生成 flag 的值为:对象是 3,block 是 7,带 __weak 的是 16,并且一直有 128,有下面这么几种组合:
    __block id                   128+3       (0x83)
    __block (^Block)             128+7       (0x87)
    __weak __block id            128+3+16    (0x93)
    __weak __block (^Block)      128+7+16    (0x97)
 
********************************************************/

//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// 当 block 和 byref 要持有对象时,它们的 copy helper 函数会调用这个函数来完成 assignment,
// 参数 destAddr 其实是一个二级指针,指向真正的目标指针
void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/
        // 默认什么都不干,但在 _Block_use_RR() 中会被 Objc runtime 或者 CoreFoundation 设置 retain 函数,
        // 其中,可能会与 runtime 建立联系,操作对象的引用计数什么的
        _Block_retain_object(object);
        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

       // 使 dest 指向的拷贝到堆上object
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
         // 使 dest 指向的拷贝到堆上的byref
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/

        // 使 dest 指向的目标指针指向 object
        *dest = object;
        break;

      default:
        break;
    }
}

这里是对成员变量的类型进行了分类,如果是对象类型的,直接将对象的增加对象的引用计数,如果是Block类型,会对该Block也进行一次_Block_copy操作,如果是__block修饰的,会调用_Block_byref_copy_Block_byref_copy的解析在下面👇。

__block

对于__block修饰的对象,底层会将其多封装一层。

struct __Block_byref_obj_0 {
  void *__isa;
__Block_byref_obj_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *obj;
};

在对block内部操作,其根本是操作他的__forwarding->obj操作:

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_qz_4pv2xnmd3g137rwtb0fpj2rr0000gn_T_OSBlockOriginFile_ce67cf_mi_1,(obj->__forwarding->obj));
    (obj->__forwarding->obj) = __null;

在Block Copy的时候,__block修饰的对象或类型在拷贝的过程中会调用_Block_byref_copy进行拷贝。

// 1. 如果 byref 原来在堆上,就将其拷贝到堆上,拷贝的包括 Block_byref、Block_byref_2、Block_byref_3,
//    被 __weak 修饰的 byref 会被修改 isa 为 _NSConcreteWeakBlockVariable,
//    原来 byref 的 forwarding 也会指向堆上的 byref;
// 2. 如果 byref 已经在堆上,就只增加一个引用计数。
// 参数 dest是一个二级指针,指向了目标指针,最终,目标指针会指向堆上的 byref
static struct Block_byref *_Block_byref_copy(const void *arg) {
    // arg 强转为 Block_byref * 类型
    struct Block_byref *src = (struct Block_byref *)arg;

    // 引用计数等于 0
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        // 为新的 byref 在堆中分配内存
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        // 新 byref 的 flags 中标记了它是在堆上,且引用计数为 2。
        // 为什么是 2 呢?注释说的是 non-GC one for caller, one for stack
        // one for caller 很好理解,那 one for stack 是为什么呢?
        // 看下面的代码中有一行 src->forwarding = copy。src 的 forwarding 也指向了 copy,相当于引用了 copy
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
        // 堆上 byref 的 forwarding 指向自己
        copy->forwarding = copy; // patch heap copy to point to itself
        // 原来栈上的 byref 的 forwarding 现在也指向堆上的 byref
        src->forwarding = copy;  // patch stack to point to heap copy
        // 拷贝 size
        copy->size = src->size;

        // 如果 src 有 copy/dispose helper
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            // 取得 src 和 copy 的 Block_byref_2
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            // copy 的 copy/dispose helper 也与 src 保持一致
            // 因为是函数指针,估计也不是在栈上,所以不用担心被销毁
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            // 如果 src 有扩展布局,也拷贝扩展布局
            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                // 没有将 layout 字符串拷贝到堆上,是因为它是 const 常量,不在栈上
                copy3->layout = src3->layout;
            }
            // 调用 copy helper,因为 src 和 copy 的 copy helper 是一样的,所以用谁的都行,调用的都是同一个函数
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            // 如果 src 没有 copy/dispose helper
            // 将 Block_byref 后面的数据都拷贝到 copy 中,一定包括 Block_byref_3
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    // src 已经在堆上,就只将引用计数加 1
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}

__blockBlock类似,如果在栈区,会重新malloc一份,进行深拷贝操作,但这两个的forwarding都会指向堆区的,如果已经在堆区,只会将其引用计数+1。