Block随记<一>

Block本质是什么

Block的本质从两方面体现:

  1. Block本质也是一个OC对象,因为它的内部也有isa指针;
  2. Block是封装了函数调用以及函数调用环境的OC对象。

本质一:Block也是一个OC对象

简单的命令行程序:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^alBlock)(void) = ^{
            printf("I'm a Block!");
        };
        alBlock();
    }
    return 0;
}

在终端cdmain.m所在的文件夹,执行clang -rewrite-objc main.m即可得到编译后的main.cpp。去除一些类型转换,并把代码结构微调:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  /// 结构体构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;/// 成员变量impl就存储着isa指针,表明Block所属的类信息
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("I'm a Block!");
}
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /// 调用结构体的构造函数初始化alBlock
    void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
    (alBlock->FuncPtr)(alBlock);
    return 0;
}

alBlock指向__main_block_impl_0类型的结构体实例的首地址,其中(alBlock->impl).isa就表明了alBlock所属的类型。

本质二:封装了函数调用以及函数调用环境

1.封装了函数调用

(alBlock->impl).FuncPtr就是一个函数指针,它指向Block代码块的首地址,在上面的例子中输出I'm a Block!的代码块儿就对应于__main_block_func_0这个静态函数。执行alBlock时通过(alBlock->impl).FuncPtr调用对应的函数即可。

2.封装了函数调用环境

有了函数要执行的操作,剩下的就是操作要是使用的数据了。函数调用环境(上下文)通常指Block操作的数据,这里的数据包含:1. Block的入参2. Block捕获的外部变量

其中Block的入参很好理解就是函数的参数,比如:

void (^sayHelloTo)(NSString *) = ^(NSString *someone) {
    NSLog(@"Hello %@", someone);
};

其中someone就是Block的形参,调用时sayHelloTo(@"World!")中的@"World!"就是入参。

而Block捕获外部变量的方式分为值捕获引用捕获

Block的值捕获特性

修改命令行程序:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger autoIns = 14;
        void (^alBlock)(void) = ^{
            printf("%d", (int)autoIns);
        };
        autoIns += 2;
        alBlock();/// 控制台输出14并不是16
    }
    return 0;
}

再次执行clang -rewrite-objc main.m,得到:

/// Block被定义成_NSConcreteStackBlock类的对象
struct __main_block_impl_0 {
    struct __block_impl impl;         /// Block的部分信息--> isa:所属类型、FuncPtr:函数指针、Flags:标志位...
    struct __main_block_desc_0* Desc; /// Block其它部分的(描述)信息
    NSInteger autoIns;                /// Block捕获的值(定义Block时外部变量autoIns的瞬时值)
    
    /// 构造函数,此次是使用入参NSInteger类型的_autoIns直接初始化结构体成员变量autoIns
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSInteger _autoIns, int flags=0) : autoIns(_autoIns)
    {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
/// Block对应的C函数,默认的一个参数为Block本身
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    NSInteger autoIns = __cself->autoIns; // bound by copy ///Block捕获的值
    printf("%d", (int)autoIns); /// 对捕获的值执行的操作
}
/// Block其它部分的(描述)信息
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size; /// block大小
}
static struct __main_block_desc_0 __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    NSInteger autoIns = 14;
    void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, autoIns);/// 传递值
    autoIns += 2;
    (alBlock->FuncPtr)(alBlock);
    return 0;
}

在内存中大致是这样的:


捕获一个值

初始化alBlockautoIns是值传递,在alBlock内部一直都是14并和外部的autoIns脱离了联系,这就是无论在调用alBlock之前如何改变autoIns的值,控制台始终输出14的原因。

Block的引用捕获特性

再次修改命令行程序:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block NSInteger autoIns = 14;
        void (^alBlock)(void) = ^{
            auto += 4;
            printf("%d", (int)autoIns);
        };
        autoIns += 2;
        alBlock();/// 控制台输出20
    }
    return 0;
}

执行clang -rewrite-objc main.m,得到:

/**
 * NSInteger -> __Block_byref_autoIns_0
 * __block修饰的变量将被重新定义为一个结构体变量,这是结构体的定义
 */
struct __Block_byref_autoIns_0 {
    void *__isa;      /// 初始为0
    __Block_byref_autoIns_0 *__forwarding; ///指向同类型结构体的指针
    int __flags;
    int __size;       /// 结构体的大小
    NSInteger autoIns;/// 被重新定义为一个结构体之前的值
};
/**
 * Block被定义成_NSConcreteStackBlock类的对象
 */
struct __main_block_impl_0 {
    struct __block_impl impl;         /// Block的部分信息--> impl.isa:所属的类型、impl.FuncPtr:函数指针、impl.Flags:标志位...
    struct __main_block_desc_0* Desc; /// Block其它部分的(描述)信息--> Desc->Block_size:Block的大小、Desc->copy:Block的辅助函数1、Desc->dispose:Block的辅助函数2
    __Block_byref_autoIns_0 *autoIns; // by ref /// Block捕获的值的指针
    
    /// 结构体构造函数,参数--> fp:Block对应的函数、desc:描述信息、_autoIns:指针指向__Block_byref_autoIns_0结构体、flags:默认等于0。
    /// autoIns(_autoIns->__forwarding)是成员变量初始化列表,之所以叫列表是因为可以有很多项,以逗号分隔。此处使用入参_autoIns的__forwarding成员初始化自身的autoIns。
    /// 成员变量初始化列表多用于初始化常量成员,因为常量不能赋值只能初始化
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_autoIns_0 *_autoIns, int flags=0) : autoIns(_autoIns->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
/**
 * Block的部分信息对应结构体,此部分定义在main.cpp 62左右,其余的末尾。
 */
struct __block_impl {
    void *isa;     /// isa指针表明Block身份
    int Flags;
    int Reserved;
    void *FuncPtr; /// 使用Block时调用的函数的指针
};
/**
 * Block对应的C函数,入参为Block本身
 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
    __Block_byref_autoIns_0 *autoIns = __cself->autoIns; // bound by ref /// 要操作的值,从入参Block中取
    (autoIns->__forwarding->autoIns) += 4; /// 执行操作
    printf("%d", (int)autoIns);            /// 执行操作
}
/**
 * Blcok从栈上拷贝到堆上时调用该方法将捕获的外部变量也拷贝到堆上
 */
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
    _Block_object_assign((void*)&dst->autoIns, (void*)src->autoIns, 8/*BLOCK_FIELD_IS_BYREF*/);
}
/**
 * 释放Block时调用该方法释放结构体捕获的值所占用的空间
 */
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->autoIns, 8/*BLOCK_FIELD_IS_BYREF*/);
}
/**
 * Block其它部分的(描述)信息
 */
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;  /// Block的大小
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    void (*dispose)(struct __main_block_impl_0*);
};
/// __main_block_desc_0_DATA 初始化结构体变量的时候用到。
/// 此处源码是和结构体的定义连着写的,这里把定义和初始化一个静态变量分开:
static struct __main_block_desc_0 __main_block_desc_0_DATA = {
    0, 
    sizeof(struct __main_block_impl_0), 
    __main_block_copy_0, 
    __main_block_dispose_0
};

int main(int argc, const char * argv[]) {
    /// 初始化一个结构体变量autoIns用于包装之前的整型变量autoIns
    __Block_byref_autoIns_0 autoIns = {
        (void*)0,
        (__Block_byref_autoIns_0 *)&autoIns,
        0,
        sizeof(__Block_byref_autoIns_0),
        14
    };
    /// 调用结构体的构造函数初始化一个Block结构体变量alBlock
    void (*alBlock)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &autoIns, 570425344);
    /// 通过类型转换拿到FuncPtr并调用,完整的写法应该是(alBlock->impl).FuncPtr,因为存放函数指针的impl成员变量在结构体的起始位置,所以可以直接转换
    (((__block_impl *)alBlock)->FuncPtr)(alBlock);
    /// 以后即使在block外部也使用包装后的结构体变量autoIns
    (autoIns.__forwarding->autoIns) += 2;
    printf("%d", (int)(autoIns.__forwarding->autoIns));
}

先上图:

捕获引用

定义Block的结构体__main_block_impl_0大致由3部分组成:

  1. 红色部分:__block_impl Block通用部分的信息:*isa``*FuncPtr...
  2. 橙色部分:__main_block_desc_0 描述Block的其它信息:Block_size``copy``dispose...
  3. 浅黄色部分:__Block_byref_autoIns_0Block捕获的变量。

上一节初始化alBlock时传递的是autoIns的值,这次则是autoIns的地址(当然,此时autoIns已封装成一个结构体变量)。若在alBlock外对autoIns进行修改,alBlock内部通过指针访问到的也是修改过的值,而且也可以通过指针实现赋值操作。这就像C语言中swap函数的形式参数是指针一样:swap(int *a, int *b)

Block捕获不同种类变量的方式

作用域 static/auto 捕获方法
局部变量 {auto int i;} 捕获值,加__block的话先封装成对象然后捕获其引用
局部静态变量 { static int i;} 捕获引用
全局变量 int i 无需捕获直接访问
全局静态变量 static int i 无需捕获直接访问

Blcok的拷贝

上两节中的Block的isa指针都指向了_NSConcreteStackBlockimpl.isa = &_NSConcreteStackBlock;但当在main函数中打断点可知__isa = (Class) __NSMallocBlock__

ARC自动拷贝Block

这是开启ARC的缘故,运行时ARC自动为我们做[alBlock copy]的操作,将栈上的Block拷贝到了堆上。MAC下是这样的:
MRC下运行时是__NSStackBlock__

Block拷贝时函数的调用链如下:
(1) [alBlock copy]
(2) _Block_copy(alBlock)
(3) _Block_copy_internal(alBlock, 570425344)
_Block_copy_internal中,针对不同类型的Block它的操作也不尽相同,源码如下:

static void *_Block_copy_internal(const void *arg, const int flags) {
    struct Block_layout *aBlock;
    const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
    if (!arg) return NULL;
    
    aBlock = (struct Block_layout *)arg;
    /// mallocBlock增加引用计数后返回
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    /// GlobalBlock直接返回 
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
     /// StackBlock复制到堆上
    // 1
    struct Block_layout *result = malloc(aBlock->descriptor->size);
    if (!result) return (void *)0;

    // 2
    memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

    // 3
    result->flags &= ~(BLOCK_REFCOUNT_MASK);    // XXX not needed
    result->flags |= BLOCK_NEEDS_FREE | 1;

    // 4
    result->isa = _NSConcreteMallocBlock;

    // 5
    if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
        (*aBlock->descriptor->copy)(result, aBlock); // do fixup
    }

    return result;
}

[stackBlock copy]

做的事情大致会做如下操作:

  1. 在堆heap上申请同大小的内存空间;
  2. 将栈数据复制过去;
  3. 修改新Block的引用计数和标志位,BLOCK_NEEDS_FREE表明Block需要释放,在release以及再次拷贝时会用到;
  4. 修改isa的指向;
  5. 若Block中有copy函数,那么就调用copy函数来拷贝Block捕获的外部变量。

[mallocBlock copy]

如果Block的flags中有BLOCK_NEEDS_FREE标志,则表明这个Block已经在堆上了(从栈中拷贝到堆时添加的标志),就执行latching_incr_int操作,其功能就是让Block的引用计数加1。所以堆中Block的拷贝只是单纯地改变了引用计数然后返回它。

[globalBlock copy]

对于全局Block,函数没有做任何操作,直接返回了传入的Block。

ARC下Blcok会被拷贝的场景

  1. 当Block调用copy方法时,如果Block在栈上,会被拷贝到堆上;
  2. 当Block作为函数返回值返回时,编译器自动将Block作为_Block_copy函数,效果等同于Block直接调用copy方法;
  3. 当Block被赋值给__strong id类型的对象或Block的成员变量时,编译器自动将Block作为_Block_copy函数,效果等同于Block直接调用copy方法;
  4. 当Block作为参数被传入方法名带有usingBlock的Cocoa Framework方法或GCD的API时。这些方法会在内部对传递进来的Block调用copy或_Block_copy进行拷贝.

Blcok拷贝到堆上对捕获的外部变量的影响

  1. 若Block捕获的是值,则没有影响;(内外的变量已脱离联系)
  2. 若Block捕获的是引用,则引用的结构体(对外部变量的封装)也会被拷贝到堆上。

autoIns从栈上到堆上

autoIns的地址由栈上的高地址0x7ffeefbff500变成了堆上的低地址0x10056af78。
被捕获的“对象”的拷贝过程的调用链如下:
(1) _Block_copy(alBlock)
(2) _Block_copy_internal(alBlock, 570425344)
(3) __main_block_copy_0(copiedBlock, alBlock)
(4) _Block_object_assign(&(copiedBlock->autoIns), aBlock->autoIns, 8)
(5) _Block_byref_assign_copy(&(copiedBlock->autoIns), aBlock->autoIns, 8)

最后一步_Block_byref_assign_copy()函数的源码如下:

static void _Block_byref_assign_copy(void *dest, const void *arg, const int flags) {
     /// Block_byref和__Block_byref_autoIns_0的前4个成员的类型都是一样的,内存空间排列一致。多的向少的转换
     /// 堆中Block的autoIns指针的指针,因为要改变autoIns指针的指向所以要使用二级指针
    struct Block_byref **destp = (struct Block_byref **)dest;
    /// 源数据,栈中Block的autoIns指针,指向栈中被捕获的对象
    struct Block_byref *src = (struct Block_byref *)arg;
        
    //printf("_Block_byref_assign_copy called, byref destp %p, src %p, flags %x\n", destp, src, flags);
    //printf("src dump: %s\n", _Block_byref_dump(src));
    if (src->forwarding->flags & BLOCK_IS_GC) {
        ;   // don't need to do any more work
    }
    else if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        //printf("making copy\n");
        // src points to stack
        bool isWeak = ((flags & (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK)) == (BLOCK_FIELD_IS_BYREF|BLOCK_FIELD_IS_WEAK));
        // if its weak ask for an object (only matters under GC)
        struct Block_byref *copy = (struct Block_byref *)_Block_allocator(src->size, false, isWeak);
        copy->flags = src->flags | _Byref_flag_initial_value; // non-GC one for caller, one for stack
        /// 堆中拷贝的forwarding指向它自己
        copy->forwarding = copy; // patch heap copy to point to itself (skip write-barrier)
        /// 栈中的forwarding指向堆中的拷贝
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        if (isWeak) {
            copy->isa = &_NSConcreteWeakBlockVariable;  // mark isa field so it gets weak scanning
        }
        /// 如果被捕获的对象也定义有copy和dispose函数则调用,
        /// 注意和_Block_copy_internal中类似的判断做区分,
        /// _Block_copy_internal:result->flags中的result指Block本身
        /// 此处src->flags中的src指上述Block捕获的对象
        if (src->flags & BLOCK_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
            copy->byref_keep = src->byref_keep;
            copy->byref_destroy = src->byref_destroy;
            (*src->byref_keep)(copy, src);
        }
        else {
            // just bits.  Blast 'em using _Block_memmove in case they're __strong
            _Block_memmove(
                (void *)©->byref_keep,
                (void *)&src->byref_keep,
                src->size - sizeof(struct Block_byref_header));
        }
    }
    // already copied to heap /// 如果src->forwarding已经指向堆区那么增加堆拷贝对象的引用计数。
    else if ((src->forwarding->flags & BLOCK_NEEDS_FREE) == BLOCK_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    // assign byref data block pointer into new Block
    /// 此时src->forwarding指向堆区,把堆区对象的首地址复制给destp,也即修改堆中Block的autoIns指针的指向,使其指向堆中被捕获的对象。
    /// *destp = src->forwarding
    _Block_assign(src->forwarding, (void **)destp);
}

这个函数做的事情可以归纳为:

  1. 使用_Block_allocator在堆上创建新对象;
  2. 对新对象前四个成员isaflagsforwardingsize赋值;对源对象的forwarding成员重新赋值,指向新对象;
  3. 根据情况调用_Block_memmove()(*src->byref_keep)()出处理剩余的成员;
  4. 给堆上Block的autoIns指针重新赋值,指向新对象。


    autoIns赋值到堆上

总结

示例代码中Block捕获的都是基本数据类型。

  • 值捕获说明Block内仅使用它的值不会做修改,那么Block被定义是就在内部存储一个一摸一样的值,这个值和外部的值完全脱离的联系;即使Block被拷贝的堆上也不会对之前外部的值有任何影响。
  • 引用捕获说明Block内部试图修改它的值,而外部也要能修改它的值,那么这个值就会被封装成一个结构体,Block内部通过结构体的地址访问它,外部直接使用这个结构体;Block被拷贝到堆上时,结构体也会被拷贝到堆上,此时autoIns.forward->autoIns就是堆空间的那个整型值了。

其它

封装成结构体只是达到Block内外访问一致的一种方法,也可以通过直接在Block内存一份变量的地址的方式达到这种效果,就像局部静态变量那样:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSInteger autoIns = 14;
        NSInteger *address_autoIns = &autoIns;
        void(^alBlock)(void) = ^{
            NSInteger *pInt = address_autoIns;
            *pInt += 4;
            printf("%ld", *pInt);
        };
        autoIns += 2;
        alBlock();
    }
    return 0;
}

同样输出是20,但这种方式无法处理Block的被拷贝到堆上的情况,Block的生命周期长于autoIns,当autoIns在栈上被销毁时再通过地址访问它就会出现异常。局部静态变量一经创建就一直存在所以不会有这个问题,和全局静态变量的区别就是作用域有区别,在作用域外只能通过地址访问,而全局静态变量可以直接访问。

你可能感兴趣的:(Block随记<一>)