美文网首页iOS
Block随记<一>

Block随记<一>

作者: b993bf901411 | 来源:发表于2019-05-05 16:05 被阅读2次

    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 *)&copy->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随记<一>

        本文链接:https://www.haomeiwen.com/subject/ybkhoqtx.html