Block原理详解(二)

2021-04-16 16:36:15 浏览数 (2)

Block的拷贝

通过上篇文章Block原理详解(一)我们知道,block是在block_copy函数中执行拷贝操作的,所以我就从这个函数开始研究。

_Block_copy函数的源码如下:

代码语言:javascript复制
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;

    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        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
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

分析如下:

  1. 参数arg实际上就是传入的block对象
  2. 当传入的对象不存的时候,就会直接返回NULL
  3. 当传入的block对象存在的时候,将其强转成Block_layout类型,然后对其引用计数进行处理
  4. 如果当前Block是GlobalBlock,那么直接返回
  5. 如果当前Block是StackBlock,那么就开始拷贝 5.1 首先使用函数在堆区开辟一块与原来一样大小的内存 5.2 然后将原block中的内容迁移到新创建的block里面 5.3 最后将isa指针指向_NSConcreteMallocBlock,这样,栈Block就拷贝成了堆Block。

是不是很简单?其实,简单只是表象,我们接下来往更深层次挖。

我在main.m中写了如下代码:

代码语言:javascript复制
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);

        __block NSString *name = [NSString stringWithFormat:@"Lavie"];
        void(^block)(void) = ^{
            name = @"Norman";
        };
        block();

    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

然后clang编译成C ,如下:

我们看到,外界的局部变量name会被转换成一个__Block_byref_name_0类型,它是一个结构体,其结构如下:

代码语言:javascript复制
struct __Block_byref_name_0 {
  void *__isa;
__Block_byref_name_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *name;
};

再接着看源码,会发现两个很明显的函数__main_block_copy_0和__main_block_dispose_0:

我们前面分析中已经提到,Block_descriptor_2中有两个变量copy和dispose:

代码语言:javascript复制
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

这里C 代码中的传入__main_block_desc_0中的两个函数函数__main_block_copy_0和__main_block_dispose_0映衬的就是Block_descriptor_2结构体中的这两个变量copy和dispose。

顾名思义,__main_block_copy_0的作用就是拷贝block,__main_block_dispose_0的作用就是析构。

我们就来看一下__main_block_copy_0函数的实现:

代码语言:javascript复制
static void __main_block_copy_0(struct __main_block_impl_0*dst,
                                struct __main_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->name,
                         (void*)src->name,
                         8/*BLOCK_FIELD_IS_BYREF*/);
}

可以看到,__main_block_copy_0里面调用了_Block_object_assign函数,需要注意的是,传入_Block_object_assign函数的前两个参数都是__Block_byref_name_0类型。接下来我们看一下_Block_object_assign函数的实现:

代码语言:javascript复制
//
// When Blocks or Block_byrefs hold objects then their copy routine helpers use this entry point
// to do the assignment.
// hold objects - 自动捕获到变量
// name
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_retain_object(object);
        *dest = object;
        break;

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

        *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 = _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;
        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;
        break;

      default:
        break;
    }
}
  1. _Block_object_assign函数中传递的第二个参数object就是block捕获的对象,这个对象是将原始字符串对象封装成__Block_byref_name_0类型之后的对象。
  2. 在_Block_object_assign函数内部会根据BLOCK_ALL_COPY_DISPOSE_FLAGS标识来进行不同的处理,这些标识的含义如下:

首先,block捕获的name是一个字符串对象,因此符合BLOCK_FIELD_IS_OBJECT,所以会走进去。

进去之后做的第一件事就是调用_Block_retain_object函数:

代码语言:javascript复制
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;

static void _Block_retain_object_default(const void *ptr __unused) { }

我发现_Block_retain_object_default里面什么也没有做。为什么这里什么也没有做呢?按道理不是应该引用计数加1吗?实际上这里确实是有引用计数 1,不过这一步是有ARC自动帮我们完成的,就不需要手动在这里执行retain操作了。

所以此时只需要将原对象的指针传递过来即可(*dest = object;)

我们的name还加了__block,因此符合BLOCK_FIELD_IS_BYREF,所以会走进去,执行_Block_byref_copy函数:

代码语言:javascript复制
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        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
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;

        // 问题 - __block 修饰变量 block具有修改能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy

        copy->size = src->size;

        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
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src 1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy 1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            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);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy 1, src 1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }

    return src->forwarding;
}

1,一定要注意,这里的入参arg也是__Block_byref_name_0类型哦!记住一句话,在block底层源码中,加了__block的局部变量,捕获到之后都会转成对应的结构体类型,不会以原始的类型进行传递的

2,注意下面标红的地方:

用__block修饰的局部变量,被block捕获之后,其内部处理最终会来到_Block_byref_copy函数,该函数的入参arg就是被捕获的局部变量经过封装之后的对应的结构体类型(本例中name对应的是__Block_byref_name_0)的实例。

上图中的copy是复制之后的__Block_byref_name_0类型的实例,src是最一开始封装的__Block_byref_name_0类型的实例。

上图的标红部分的意思是,最初的__Block_byref_name_0类型的实例src中的forwarding指向的是它自己,而对src进行拷贝生成新的__Block_byref_name_0类型的实例copy之后,将src的forwarding指向拷贝生成的copy;而拷贝生成的copy中的forwarding指向的也是copy。

也就是说,当block捕获__block修饰的局部变量时,当对该局部变量所对应的结构体的实例进行拷贝之后,初始的那个结构体实例中的forwarding和拷贝后的forwarding指向的都是同一块内存空间。而这块内存空间中记录了初始局部变量的指针指向,进而找到的就是最原始的局部变量的内存地址:

3,在源码中,判断BLOCK_BYREF_HAS_COPY_DISPOSE符合之后还会进行byref_keep和byref_destroy的迁移:

byref_keep和byref_destroy是定义在Block_byref_2结构体中的:

不难看出,Block_byref、Block_byref_2和Block_byref_3跟我们之前谈到的Block_descriptor、Block_descriptor_2和Block_descriptor_3很像。

所以说,byref_keep就是类型的实例对象中的第5个参数(依次为isa、forwarding、flags、size、byref_keep...)。

现在回到汇编源码,查看原始局部变量被捕获之后转成__Block_byref_name_0的代码:

byref_keep对应的就是__Block_byref_id_object_copy_131。

接下来我就看看__Block_byref_id_object_copy_131函数的实现:

代码语言:javascript复制
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst   40, *(void * *) ((char*)src   40), 131);
}

可以看到,__Block_byref_id_object_copy_131里面调用了_Block_object_assign。

此时不禁会有疑问,之前不是已经分析过_Block_object_assign了吗?怎么又回来了?

莫急,看里面的参数。第一个参数是(char*)dst 40,它表示的是什么呢?

dst的类型是__Block_byref_name_0

所以(char*)dst 40表示的就是name变量,也就是说,最原始的外界__block局部变量会在这一步进行指针的拷贝并与对应的结构体变量进行关联

要注意哦,这一步再进入_Block_object_assign里面,其作用是将最原始的外界局部变量进行指针拷贝,并与对应的结构体变量关联起来。

4,三层拷贝

(1)首先会通过__main_block_copy_0函数进行block的拷贝,这是第一层拷贝

(2)在__main_block_copy_0中调用_Block_object_assign,进而在__block修饰的情况下调用_Block_byref_copy函数,来对捕获到的__block局部变量对应的__Block_byref_变量名_0类型的结构体实例进行拷贝,这是第二层拷贝

(3)在_Block_byref_copy函数中又会通过byref_keep来找到对应的__Block_byref_id_object_copy_131函数,进而调用_Block_object_assign函数。这一步是对外界的__block局部变量进行浅拷贝(即指针拷贝)


至此,关于Block的底层原理就全部讲完了。但是还遗留下一个点,这个点实际上与接下来要聊的内容有一个连接作用,这个点就是Block的Hook。

Hook思想大家都明白,但是可能关于Block的Hook大家平时接触的不多,我在这篇文章的篇幅里面就不做过多介绍了,这里只是做一个引子。

接下来我会抽一两篇文章的篇幅来详细的聊一聊AOP切面编程以及无侵入埋点方法,这个内容将会强烈依赖于Block的Hook。

0 人点赞