Block原理解析

作者: JimmyOu | 来源:发表于2017-11-27 17:57 被阅读337次

    Block语法简介

    Block:可以理解为带有自动变量值的匿名函数。

    Blocks提供了类似由C++和Objective-C类生成实例变量或对象来保持变量值的方法。

    Block语法定义
    ^返回值 类型参数列表 表达式

    ^void (int event) { NSLog(@"。。。");}

    Block类型变量定义
    int (^blk)(int); 可以认为是匿名函数的地址,但是实际上它是是被看成对象来操作的,有自己的isa指针。

    简单的Block原理分析

    我们来分析最简单的block:我们定了一个变量名称为blk的Block变量,在定义部分省略了返回值和类型参数列表,然后在下面调用它,打出一串”Block”;

    
    void (^blk)(void) = ^{printf("Block\n");};
    blk();
    
    

    源码通过clang,去掉一些类型转换我们可以得到以下代码

    
    struct _block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
    };
    //Block定义的结构体
    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.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
    };
    //我们的Block里面的函数
    static void __main_block_func_0(struct __main_block_impl_0 *_cself)
    {
    printf("Block\n");
    }
    //存储block的其他信息,大小等
    struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
    } __mainBlock_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0)
    };
    /*以下是我们的代码部分*/
    //赋值部分,
    struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
    struct __main_block_impl_0 *blk = &temp;
    //调用部分
    (*blk->impl.FuncPtr)(blk);
    /*以下是我们的代码部分*/
    
    

    看起来好像很麻烦?居然两句代码变出了这么多代码。慢慢分析起来其实也不难理解

    C++中,struct 约等于 class,唯一差别是struct中的默认成员属性是public的。class中的默认成员属性是private的。所以struct也可以拥有变量和函数。

    首先系统自动给我们生成了三个结构体。

    
    //block的结构体定义
    struct __main_block_impl_0 {
    struct _block_impl impl;//Block isa ,函数地址等定义
    struct __main_block_desc_0 *Desc;//Block size等信息定义
    };
    struct _block_impl {
    void *isa;//所属的类
    int Flags;
    int Reserved;
    void *FuncPtr;//函数地址
    };
    struct __main_block_desc_0 {
    unsigned long reserved;
    unsigned long Block_size;
    } 
    
    

    生成了两个函数

    
    //Block信息初始化的函数
    __main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
    {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
    //我们的Block里面的函数,_cself就是调用这个函数的调用者的指针
    static void __main_block_func_0(struct __main_block_impl_0 *_cself)
    {
    printf("Block\n");
    }
    
    

    我们定义Block的代码如下

    
    void (^blk)(void) = ^{printf("Block\n");};
    
    

    转化成

    
    /*
    初始化
    __main_block_func_0:函数地址,
    __mainBlock_desc_0_DATA:block的size信息
    */
    struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
    struct __main_block_impl_0 *blk = &temp;
    
    

    定义了一个main_block_impl_0的block,初始化函数为main_block_impl_0,传入函数指针和block的大小等信息

    
    //Block信息初始化的函数
    __main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
    {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
    }
    
    

    我们看到这个block的class类型为_NSConcreteStackBlock,这个下面会详细解释。函数地址为FuncPtr。所以iOS里的Block是被当成一个类来看待的,有自己的存储空间。可以理解为带有自动变量值的匿名函数

    我们调用block的代码如下

    
    //调用部分,
    blk();
    
    

    转化成

    
    //调用部分
    (*blk->impl.FuncPtr)(blk);
    
    

    拿到上面定义的Block变量blk,找到函数地址,调用函数,并把调用者也就是blk传递进去。

    Block会截获自动变量

    
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt,val);};
    val = 2;
    fmt = "these values were changed. val = %d\n";
    blk();
    
    

    输出为输出

    val = 10

    而不是

    these values were changed. val = 2

    说明自动变量截获只能保存执行block语法瞬间的值

    但我们知道加上__block,是可以在Block内部对变量进行修改的。详细讲__block(__block storage-class-specifier)为存储类型说明符,

    c语言有以下说明符:

    • tydedef
    • extern
    • static:表示静态变量存储在数据区
    • auto:表示自动变量存储在栈
    • register:应将其保存在CPU的寄存器中(而不是栈或堆)

    __block类似于后三种,表示将变量值设置到哪个存储区
    如果我们加上__block

    
    __block int val = 10;
    void (^blk)(void) = ^{val=1;};
    
    

    进行编译后,并剔除和以上通过clang一样的部分,我们看到以下不同

    
    struct __Block_byref_val_0 {
    void *_isa;
    __Block_byref_val_0 *_forwarding;
    int __flags;
    int __size;
    int __val;
    };
    struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    __Block_byref_val_0 *val;
    }   
    
    

    我们发现val变量居然变成了结构体实例__Block_byref_val_0,既在栈上生成了__Block_byref_val_0结构体实例,且初始化为10

    ^{val=1;}赋值过程变成什么样子了呢

    
    static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_val_0 *val = __cself->val;
    (val->__forwarding->val) = 1;
    }
    
    

    找到Block下面的val变量,拿出val变量的__forwarding指向的val变量,拿出val变量下的val值赋值。如果Block此时在栈区,那么__forwarding指向val变量自己。

    image

    copy到堆后:__forwarding指向堆区的val变量

    image

    Block的类型

    • _NSConcreteStackBlock: 存储在栈区,需要截取变量
    • _NSConcreteGlobalBlock: 1.存储在程序数据区域,2.不需要截取变量
    • _NSConcreteMallocBlock: 存储在堆区

    根据之前的分析,我们看到block的isa指针为_NSConcreteStackBlock,里面有个Stack,可以猜到,这个为存储在栈区的Block。

    我们在记述全局变量的地方使用Block语法时候,生成的Block为_NSConcreteGlobalBlock,因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量的截获。

    
    void (^blk)(void) = ^{printf("global Block");};
    int main(){
    }
    
    

    那如何使得栈上的Block到堆上呢?

    ARC 条件下编译器会适当判断,自动生成将block从栈上复制到堆上的代码。

    
    //比如
    typedef int (^blk_t)(int);
    blk_t func(int rate) {
    return ^(int count){return rate * count;};
    }
    
    

    该代码返回设置在栈上的Block函数。但函数作用域结束,栈上的Block被废弃。但编译器自动会加上copy

    什么情况下编译器不能进行判断要不要加copy,而需要手动执行copy?

    1. 向方法或者函数参数中传递block;
    2. 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

    例:
    在用 如NSArray 的 enumerateObjectsUsingBlock 的实例方法和 dispatch_async函数前,不用手动copy。

    在用如NSArray的 initWithObjects 前,需要手动copy。

    
    typedef void (^blk_t)(void);
    NSArray *blocks = [self getBlockArray];
    blk_t blk = (blk_t)[blocks objectAtIndex:0];
    blk();
    - (NSArray *)getBlockArray {
    int val = 0;
    return [NSArray arrayWithObjects: ^{NSLog(@"blk0:%d",val);},
    ^{NSLog(@"blk0:%d",val);}, nil];
    }
    //会发生崩溃。因为NSArray 的initWithObjects因为系统不确定加入的是不是block,不会自动执行copy操作,如果我们也不执行,在作用域外调用就会发生崩溃。
    
    

    也许你会想,那么任何时候都用copy就好啦。但是从栈上的block copy到堆上很耗CPU。所以最好自己判断需不需要把Blockcopy到栈上

    综上:我们要想把栈上的Block复制到堆上,只有执行copy方法,有些情况下,系统会自动帮我们执行,但也有些情况我们需要手动执行copy。

    栈上的Block被复制到堆的情况

    • 手动调用Block的copy实例方法
    • Block作为函数返回值返回
    • 将block赋值给附有__strong修饰符id类型的类或Block类型的成员变量。
    • 如果在函数或者方法中已经copy了传递过来的参数(Cocoa框架的方法且方法名中含有usingBlock,GCD的API)

    注: __weak, __strong 用来修饰变量,此外还有 __unsafe_unretained, __autoreleasing 都是用来修饰变量的。
    __strong 是缺省的关键词。
    __weak 声明了一个可以自动 nil 化的弱引用。
    __unsafe_unretained 声明一个弱应用,但是不会自动nil化,也就是说,如果所指向的内存区域被释放了,这个指针就是一个野指针了。
    __autoreleasing 用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。

    各种类型的Block调用copy后

    Block类型 存储区域 赋值效果
    _NSConcreteStackBlock 栈-》堆
    _NSConcreteGlobalBlock 程序数据区域 什么也不做
    _NSConcreteMallocBlock 引用计数+1

    所以不管任何时候copy方法复制都不会出错。但是多次调用copy会不会引起内存释放问题呢?

    
    //多次调用copy
    blk = [[[[blk copy] copy] copy] copy];
    //代码解释
    {
    /*
    将配置在栈上的Block赋值给blk变量。
    */
    blk_t temp = [blk copy];
    /*
    将配置在堆上的block赋值给tmp变量,temp强持有Block
    */
    blk = temp;
    /*
    将变量tmp的Block赋值为变量blk,blk强持有Block
    此时block的持有者为变量temp和blk;
    */
    }
    /*
    由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
    由于Block的此时还被blk持有,所以没有废弃。
    */
    {
    /*
    配置在堆上的Block被赋值给blk;同时变量blk持有强制引用的Block
    */
    blk_t temp = [blk copy];
    /*
    将配置在堆上的block赋值给tmp变量,temp强持有Block
    */
    blk = temp;
    /*
    将变量tmp的Block赋值为变量blk,blk强持有Block
    此时block的持有者为变量temp和blk;
    */
    }
    /*
    由于变量作用域结束,所以变量temp被废弃,其强引用失效并释放所持有的Block
    由于Block的此时还被blk持有,所以没有废弃。
    */
    /*下面重复*/
    
    

    答案是 :多次调用copy完全不会有任何问题

    一个含有__block变量的block被copy

    __block变量的配置存储域 Block从栈赋值到堆时候的影响
    从栈赋值到堆并被Block持有
    被Block持有

    我们看看以下代码,一个在栈上的Block

    __block int val = 0;
    void (^blk)(void) = ^{val = 1; printf("val = %d\n",val);};
    blk();
    printf("val = %d\n",val);
    
    

    同样的如果Block在堆上两个输出也一样:

    说明

    无论在Block语法中,Block语法外使用__block变量,还是__block变量配置在栈上或者堆上,都可以顺利访问同一个__block

    说到Block不得不谈循环引用问题,但是比较简单,网上一大堆,这里也不分析了。

    小结

    本文探索了Block的底层实现机制,我们发现Block在iOS中是作为对象来管理的。现在再看看这句话
    Block:可以理解为带有自动变量值的匿名函数。是不是形容的很贴切。

    相关文章

      网友评论

        本文标题:Block原理解析

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