Block 原来你是这样的(一)

984 阅读9分钟

最近抽空把 iOS与OSX多线程和内存管理 这本书看完了,个人感觉这本书还是值得去看看的。

第一部分详细说明了内存管理,包括 ARC 下和 MRC 下的引用计数、__strong__weak__autorelease 的一些用法和使用过程中的问题;第二部分详细的介绍了Block的原理,包括平时开发过程中所遇到的 Block 循环应用问题的原因;第三部分为 GCD API 的使用方式。

本文是对第二部分 Blocks 的整理,但是还是建议去看下书。

1、什么是 Blocks

Blocks 是 C 语言的扩充功能。用一句话来表示 Blocks 的扩充功能;带有自动变量(局部变量)值的匿名函数。

所谓匿名函数就是不带名称的函数。C语言的标准不允许存在这样的函数。一般声明和调用 C 函数如下:

/// 声明
int func(int count);

///调用
int result = func(10);

如果用下方函数指针代替直接调用函数,看起来似乎可以不用知道函数名也能调用,事实上使用函数指针也需要函数名称,如果不使用函数名称就不能获取该函数的地址,因为在程序中函数名即为指针地址。

int func(int count) {
    return count + 1;
}

int (*funcptr)(int) = &func;

int result = *(funcptr)(10);

如果一些页面传值回调的代码大量使用上述方式,就会使代码变得非常冗余而且难以阅读,所以 iOS 4.0 引入了 Block ,其实和指针调用的写法差不多,对应上方。

int (^blk)(int) = ^(int count) { retrun count + 1; };
int result = blk(10);

由此,再看下方的 Block 的范式就不难明白了。

image.png

Block 可以隐藏多个参数,比如:可以隐藏返回值,如果表达式中存在 return 语句,Block 自己可以推断类型,如下图:

image.png

如果不使用参数,参数也可以省略,如下图:

image.png

2、理解 Block

1、带有自动变量值

通过上方 Blocks 的介绍说明了 "带有自动变量(局部变量)值的匿名函数" 中的 "匿名函数",而 "带有自动变量值" 是什么意思呢?我们看下方这个代码:

int main()
{
    int a = 10;
    void(^blk)(void) = ^{ printf("%d",a); };
    a = 11;
    blk();
}

上述代码中 print 的值是 10 并不是 11 , 因此 a 的值被保存(即被截获),从而在执行 block 时使用,这是就是自动变量值的截获。

既然 block 会截获 block 中所使用的值,那么我们开发中的 self 同样也会被截获,一旦被截获就会形成循环引用,这是我们共知的。所以留个小问题:self 是怎么被截获的?

2、__block 说明符

int main()
{
    int a = 10;
    void(^blk)(void) = ^{ a = 11; };
    blk();
}

如果在 Block 中 修改 a 的值,编译器是会报错的,所以自动变量值截获是保存执行 Block 语法瞬间的值,保存后就不能改写改值。如果想改就需要将 Block 中被截获的值添加 __block 说明符。

int main()
{
    // 这里添加了 __block
    __block int a = 10;
    void(^blk)(void) = ^{ a = 11; };
    blk();
}

3、Block 本质探索

1、未截获自动变量值的 Block

先来一段代码,然后打开终端,在这个 源代码文件 目录下,输入 clang -rewrite-objc 源代码文件名

int main(int argc, const char * argv[]) {
    void(^blk)(void) = ^{ printf("Hello, World!\n"); };
    blk();
    return 0;
}

Clang 后代码如下:

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

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;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    printf("Hello, World!\n"); }

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[]) {
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

为了方便阅读和理解,去掉一些类型转换后代码如下:

int main(int argc, const char * argv[]) {

     void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    
    // 上方的代码等价于下方代码
    struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
    struct __main_block_impl_0 *blk = &tmp;
    (*blk->impl.FuncPtr)(blk);
    
    return 0;
}

再看上述代码你就会发现,Block 的本质也就是C语言函数指针的调用。那么开始逐行分析:

 struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);

__main_block_impl_0 结构体的构造器有4个参数,初始化传入了2个:

  • isa:表示当前结构体类型,分为__NSConcreteStackBlock(栈),__NSConcreteMallocBlock(堆),__NSConcreteGlobalBlock(数据区);

  • flags:初始化默认给了0;flags 相关枚举如下:

enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};
  • __main_block_func_0:这个函数是 Block 代码块中的方法在底层经过编译器生成的函数,将方法实现放在函数体中;

  • &__main_block_desc_0_DATA__main_block_desc_0_DATA 的结构体指针,用来表示 Block 的大小。

另外 __main_block_func_0 函数将 struct __main_block_impl_0 * 类型的 __cself 作为参数传递,这个参数是为了如果 Block 中捕获了自动变量,就会使用 __cself 取值,下方截获自动变量值的 Block会说明。

struct __main_block_impl_0 *blk = &tmp;

将初始化的结构体取地址给 blk 结构体指针。

(*blk->impl.FuncPtr)(blk);

blk 结构体指针指向的值取出方法指针地址执行 Block 块中的方法。

2、截获自动变量值的 Block

稍微改动一下未截获自动变量值的 Block 的代码,让 Block 中使用自动变量 int aClang 一下再看代码。

int main(int argc, const char * argv[]) {
    int a = 10;
    void(^blk)(void) = ^{ printf("Hello, World! = %d\n",a); };
    blk();
    return 0;
}

Clang 后的代码如下:

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    /// 这里多了一个成员变量
    int a;
    
    /// 这里入参多了一个 a 
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

/// 这里使用了 __cself
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    /// 使用__cself 获取 a 的值
    int a = __cself->a; // bound by copy
    printf("Hello, World! = %d\n",a); }

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[]) {
    int a = 10;
    
    /// 构造函数将 a 的值传入
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

这段代码与前面的代码稍有差异,使用的 int a 自动变量被作为成员变量追加到了 __main_block_impl_0 结构体中,并且该结构体的构造方法多了 int a 的传参(没有在 Block 中使用的不会截获),被截获的自动变量,传递给结构体进行保存。

被截获的自动变量的值是不能在 Block 内修改的,比如将上方代码 void(^blk)(void) = ^{ printf("Hello, World! = %d\n",a); }; 修改成 void(^blk)(void) = ^{ a = a + 1; }; 编译器就会报错。

然而有一个情况比较特殊,用于 Block 回调后将回调对象添加到数组中,代码如下:

int main(int argc, const char * argv[]) {
    NSMutableArray *a = [NSMutableArray array];
    
    void(^blk)(void) = ^{
        [a addObject:@1];
    };
    
    blk();
    return 0;
}

虽然 NSMutableArray *a 被截获了,但是依然能对 a 数组进行添加数据操作,这是因为 Block 截获的只是 a 对象本身,不对可操作数据内部进行截获,也就是说:对于自动变量对象只要对象指针地址不发生改变,便能正常使用;对于自动变量为值类型的只要值不发生改变,便能正常使用。

由下方编译器报错示例可以推断:

int main(int argc, const char * argv[]) {
    NSMutableArray *a = [NSMutableArray array];
    
    void(^blk)(void) = ^{
        /// 编译器报错
        a = [NSMutableArray array];
    };
    
    blk();
    return 0;
}

3、修改 Block 中变量值的方法

开发中经常遇到需要改动 Block 代码块中存在的值,如何实现呢?经过上述研究知道了以下两点:

  • Block 仅仅截获自动变量;
  • Block 截获的自动变量的值不允许修改。

所以首先尝试不使用自动变量,改用全局变量、全局静态变量和静态变量实现。代码如下:

int global_var = 1;
static int static_global_var = 2;

int main(int argc, const char * argv[]) {
    static int static_var = 3;
    
    void(^blk)(void) = ^{
        global_var *= 3;
        static_global_var *= 3;
        static_var *= 3;
    };
    blk();
    return 0;
}

Builde 一下,编译正常,然后 Clang 一下看看和截获自动变量有什么区别:

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

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    /// 这里变成了指针
    int *static_var;
    
    /// 仅仅入参了静态变量
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int flags=0) : static_var(_static_var) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

    /// 静态变量从 __cself 取出来,全局变量和全局静态变量直接使用了
    int *static_var = __cself->static_var; // bound by copy
    
    global_var *= 3;
    static_global_var *= 3;
    (*static_var) *= 3;
}

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[]) {
    static int static_var = 3;
    
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_var));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

分析后发现,全局变量和全局静态变量直接使用了,而静态变量被截获了,截获的是静态变量的地址,通过对地址指向的值改变从而达到改变值的目的。

看起来静态变量的这种被 Block 截获指针后改值的方法似乎也适用于声明一个自动变量的指针然后在 Block 中更改值。但是为什么我们没有这么做,转而使用 __block 说明符呢?

本菜鸡想了半天,想了一个办法展示这个错误,代码如下:

int main(int argc, const char * argv[]) {
    blk_t blk = func();
    blk();
    return 0;
}

blk_t func() {
    int *_var;
    {
        int a = 3;
        _var = &a;
    }
    
    blk_t blk = ^{
        *_var *= 3;
        printf("static_var = %d\n",*_var);
    };
    return blk;
}

错误结果如下:

image.png

因为如果 int a 被释放掉了,那么 int *_var; 取值就会错误,所以自动变量不能像静态变量一样使用。

关于 __block 的分析,下一篇 Block 原来你是这样的(二)说明。

4、结语

到这里,应该对 Block 的内部有一些了解了吧,其实 Block 就是我们 OC 中的对象,所以 Block 的第一个参数为 void *isa,下一篇 Block 原来你是这样的(二)__block 说明符修饰的变量进行分析,以及__NSConcreteStackBlock__NSConcreteMallocBlock__NSConcreteGlobalBlock三种类型Block的作用域分析。

有问题欢迎指出,共同学习,一起进步。