深入理解 Objective-C ☞ Block

栏目: Objective-C · 发布时间: 4年前

内容简介:日常开发中经常会用到 Block,也熟悉各种注意事项,不过对于它的底层实现并没有特别深入地挖掘过,还不能算是真正掌握了,本篇就来探究一下 Block 的底层实现原理。先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。

深入理解 Objective-C ☞ Block

日常开发中经常会用到 Block,也熟悉各种注意事项,不过对于它的底层实现并没有特别深入地挖掘过,还不能算是真正掌握了,本篇就来探究一下 Block 的底层实现原理。

1.举个 :chestnut:

先来看一个例子,下边是一种简单的 block 使用场景: 无参数、无返回值的 block。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 30;
        // 创建
        MyBlock blk = ^{
            NSLog(@"My age is %d .", age);
        };
        // 执行
        blk();
    }
    return 0;
}

2.Block的实质

为了探究 Block 的本质,我们需要借助 clang 将含有 Block 语法的源代码转换成 C++ 代码。

2.1 Block 的底层结构

终端执行 $ clang -rewrite-objc main.m 命令,就可以将 main.m 文件编译生成 main.cpp 文件,这里截取了 main.cpp 文件中与 block 相关的代码,并添加了部分注释:

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

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

// Block 的结构体
struct __main_block_impl_0 {

    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;

    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// block 的 { } 里边的代码构成的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

// main() 函数
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;

        int age = 30;

        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

下面开始一步步讨论源码。从 main() 函数开始,关于自动释放池的代码不在此处讨论,先看一下 block 的创建过程:

MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 简化后的代码:
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

我们注意到这里实际上有两个函数: __main_block_impl_0()__main_block_func_0()

先来看后者,具体代码如下,实际是 block 的 { } 里边的代码构成的函数。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

然后搜索前一个函数的函数名 __main_block_impl_0 ,发现它位于下边这个结构体里边:

struct __main_block_impl_0 {

    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;    

    // 构造函数
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;  // 指明该 block 的类型(此处是栈上的 block)。
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

而这个结构体就是 block 经编译后得到的结构,很明显 __main_block_impl_0() 是它的构造函数,我们发现,这个构造函数里边都是在给 block 的前 3 个元素 (2 个结构体和一个 int age) 赋值。

第一个元素 impl ,它的组成是这样的:

struct __block_impl {
    void *isa;       // 用于说明 block 的类型
    int Flags;       // 标识位
    int Reserved;    // 保留字段
    void *FuncPtr;   // 指针
};
  • FuncPtr 是一个指针,根据名字推断应该是一个函数指针,结合 main() 函数中执行构造函数创建 block 的过程可以看出, FuncPtr 指向的是 block 的 { } 里边的代码构成的函数。

  • isa 指明了block 的类型,构造函数中给它赋的值是 &_NSConcreteStackBlock ,说明他是栈上的 block。关于 block 的类型,下一小节就会讲到。

第二个元素 Desc__main_block_desc_0 类型的结构体,如下所示:

static struct __main_block_desc_0 {
    size_t reserved;    // 保留字段
    size_t Block_size;  // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

其中 Block_size 从名字推断应该是结构体的大小,紧随其后定义了一个 __main_block_desc_0 类型的变量 main_block_desc_0_DATA,它的第二个元素值就是当前 block 的大小 `sizeof(struct main_block_impl_0) ,从 main() 函数中执行 block 构造函数的语句可以看出,__main_block_desc_0_DATA 最终赋值给了 block 中的 Desc`,进一步验证了 Block_size 中存放的是 block 的大小。

第三个元素 int age 是 block 捕获的一个 auto 变量,关于捕获变量的机制,后面会详细讨论。

最后回到 main() 函数的最后一行代码:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 简化后:
blk->FuncPtr(blk);

很明显这是执行 blk 里边的指针 FuncPtr 指向的函数,而且将 blk 自己传了进去,这样,就可以在函数内部访问到 block 捕获的变量,如前文提到的 int age。

至此,本文开头的 block 的底层结构基本介绍完了,看起来比较零散,这里绘制了一张总图做个简单小结:

深入理解 Objective-C ☞ Block

2.2 Block 的类型

在此,简单说明一下 block 的类型,block 的 3 种类型及其内存分布如下:

深入理解 Objective-C ☞ Block

那么这 3 种类型的 Block 有什么区别呢,为了搞清楚这个问题,我们需要先回顾一下 4 种常见的变量类型及其代码示例:

  • 自动变量(auto 变量)
  • 静态局部变量
  • 全局变量
  • 静态全局变量
// *** 4 中变量的代码示例:

// 全局变量
int global_var = 10;
// 静态全局变量
static int static_global_var = 20;

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

        // 自动变量(局部变量)
        int local_var = 30; // <==> auto int local_var = 30;
        // 静态局部变量
        static int local_static_var = 40;

        MyBlock blk = ^{
            NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var);
        };
        blk();
    }
    return 0;
}

关于各种 Block 的区别,可以简单汇总成下边的图表:

深入理解 Objective-C ☞ Block

也就是说:

  • 如果 Block 里边访问了 auto 变量,那么他就是栈上的 Block;
  • 如果没有访问 auto 变量,就是全局的 Block;
  • 如果对栈上的 Block 执行了 copy 操作,就变成了堆上的 Block。

2.3 Block 的 copy

上文提到了对栈上 Block 的 copy 操作,那么为什么需要 copy 呢?原因是:设置在栈上的 Block 如果其所属的作用域结束,该 Block 就会被废弃,为了延长它的生命周期,就需要将其复制到堆上。

既然栈上的 block 经 copy 后会从栈上复制到堆上,那么另外两种 Block 执行 copy 操作又会发生什么呢? 每一种 Block 被 copy 后的结果如下:

深入理解 Objective-C ☞ Block

ARC 环境下,编译器会根据情况自动将栈上的 block 复制到堆上,比如满足一下条件之一时:

  • block 作为函数返回值时;
  • 将 block 赋值给 __strong 指针时;
  • block 作为 Cocoa API 中方法名含有 usingBlock 的方法参数时;
  • block 作为 GCD API 的方法参数时。

MRC 环境下,需要手动调用 block 的 copy 操作,才能将栈上的 block 复制到堆上。

3.变量捕获

为了保证 Block 内部能够正常访问外部的变量,block有个变量捕获机制,我们以前边介绍常见变量类型的代码为例,看看 Block 是怎么捕获变量的。

执行 clang -rewrite-objc main.m 之后,转换的 block 的源码如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  // 捕获的变量
  int local_var;
  int *local_static_var;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

从上边的源码可以看出来,对于这 4 种不同的变量,实际只捕获了 自动变量 local_var静态局部标量 local_static_var ,而且自动变量是捕获了值,静态局部变量捕获的是变量地址。至于为什么这么设计,推测可能的原因如下:

深入理解 Objective-C ☞ Block

3.1 捕获自动变量

实际开发中,block 捕获到的变量基本都是自动变量(局部变量),理由是:对于全局变量,任何地方都可以访问它,不安全;对于静态局部变量,它会一直存在于内存中,对内存是一种浪费。

对于基本数据类型的自动变量,前边已经讲过了,就是简单的值捕获,接下来我们重点讨论一下对象类型 auto 变量的捕获。

下边是 block 访问外部对象类型 auto 变量的简单实例。

typedef void (^MyBlock)(void);

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

        NSObject *obj = [[NSObject alloc] init];

        MyBlock blk = ^{
            NSLog(@"%@", obj);
        };

        blk();
    }
    return 0;
}

执行 clang -rewrite-objc main.m 后,生成的源码中有这 2 点不同:

  • 捕获了 NSObject *obj;

    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;
    
      NSObject *obj;
    
      __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    
  • main_block_desc_0 中增加了两个指针 copydispose ,整个文件里新增了 2 个函数 ` main_block_copy_0() __main_block_dispose_0() ,结合上下问可以知道,这两个函数地址最终传给了 block 里 Desc 中的 copy dispose` 这 2 个指针。

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

这两个函数调用时机和作用如下:

  • 如果 block 从栈上拷贝到堆上

    会调用 block 内部的 copy() 函数,此函数内部会调用 _Block_object_assign() 函数,它会根据 auto 变量的修饰符( strong、 weak、__unsafe_unretained)做出相应的操作,形成 强引用 或 弱引用。

  • 如果 block 从堆上移除

    会调用 block 内部的 dispose() 函数,此函数内部会调用 _Block_object_dispose() 函数,它会自动释放引用的 auto 变量(即 release)。

另外,如果 block 一直是在栈上,将不会对 auto 变量产生强引用。

3.2 捕获 __block 变量

前边我们只是在 block 内部 使用 变量,事实上,如果直接 修改 变量的话,比如下边这个例子,就会报错:此变量不可赋值 (错误信息见注释)。

typedef void (^MyBlock)(void);

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

        int age = 10;

        MyBlock blk = ^{
            age = 20; // Error: Variable is not assignable (missing __block type specifier)
            NSLog(@"%d", age);
        };

        blk();
    }
    return 0;
}

按照错误信息的提示,如果给 int age = 10; 前边加上 __block ,就可以解决 block 内部无法修改 auto 变量的问题,实际操作后,发现果然可以正常输出 age 的新值 20。

3.2.1 __block 变量能够被 block 修改的原因

现在来看看 __block 修饰符到底做了什么,先将上边的代码转成 C++ 源码,下边截取了其中部分关键代码:

// 新出现的结构体
struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;

    __Block_byref_age_0 *age; // by ref

    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref

    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

先看 main() 函数, __block int age 变成了 __Block_byref_age_0 类型的 age ,也就是说编译器将 __block 修饰的变量包装成了一个新的结构:

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

__Block_byref_age_0 这个结构体里边有一个 int age ,用于存储 age 的值 (10)。还有一个重要成员 __Block_byref_age_0 *__forwarding; ,结合 block 的构造函数,我们知道 __forwarding 指针实际指向了它所在的结构体。

深入理解 Objective-C ☞ Block

之所以这么做是为了当 block 被拷贝到堆上以后,无论访问栈上的 block 还是 堆上的 block,最终都是访问的堆上的同一个 block(拷贝后,堆上的 forwarding 指向自己所在的 block 变量,栈上的 forwarding 指向堆上的 block 变量),如下图所示。

深入理解 Objective-C ☞ Block

接下来,看看 block 的结构 __main_block_impl_0 ,里边多了一个变量 __Block_byref_age_0 *age; ,即 block 捕获了这个新的结构体 __Block_byref_age_0 的地址,所以 block 里边就可以通过地址访问这个结构体,进而修改里边 int age 的值。

__block 修饰的对象类型的 auto 变量与此类似,差别仅在于新生成的结构体:

深入理解 Objective-C ☞ Block

从上图可知,__block 修饰的对象类型转换后的结构体里边多了两个函数指针,他们分别指向下面 2 个函数,负责内存管理的相关操作,下边就会讲到。

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

3.2.2 被 __block 修饰的对象类型的内存管理

对于 被 __block 修饰的对象类型,内存管理分以下 3 种情况:

  • 1.当 __block 变量 在栈上时,不会对指向的对象产生强引用。

  • 2.当 __block 变量 被 copy 到堆时,分两种情况:

    • ARC 环境下,会调用 __block 变量内部 的 copy 函数,它会调用 _Block_object_assign() 函数,此函数会根据所指向对象的修饰符( strong、 weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。

深入理解 Objective-C ☞ Block

  • MRC 环境下,不会形成强引用(retain)。
typedef void (^MyBlock)(void);

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

        __block NSObject *obj = [[NSObject alloc] init];

        MyBlock block = [^{
            NSLog(@"%p", obj);
        } copy];

        block();

        [obj release];
    }
    return 0;
}

如上所示,在 MRC 环境下,对象前加了 __block,不会对 block 形成强引用, 即当执行完 [obj release]; 之后,person 就被释放了。

深入理解 Objective-C ☞ Block

  • 3.如果 block 变量从堆上移除,会调用 ` block 变量内部 dispose() 函数,它会调用 _Block_object_dispose()` 函数,此函数会自动释放指向的对象(release)

3.2.3 对象类型的auto变量 和 __block 变量

  • 当block在栈上时,对它们都不会产生强引用

  • 当 block 拷贝到堆上时,都会通过 copy 函数来处理它们

    • 对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 对于对象类型的 auto 变量(假设变量名叫做p),最终会执行 _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

      以上两者最终调用的方法是相同的,只不过最后一个参数有差别,前者是 8(表示引用类型),后者是 3 (表示对象),下面对 _Block_object_dispose() 函数的调用与之类似。

  • 当 block 从堆上移除时,都会通过 dispose 函数来释放它们

    • 对于 __block变量(假设变量名叫做a),最终会执行 _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 对于对象类型的auto变量(假设变量名叫做p),最终会在执行 _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

4.循环引用

关于使用 block 可能遇到的循环引用问题,我们分 ARC 和 MRC 两种情况进行讨论。

ARC 环境

在 ARC 环境下,目前大概有 3 种常见的解决循环引用的方式:

  • __weak

此方式是最常见也是推荐使用的一种方式,基本原理是,self 对 block 的引用维持强引用,不过将 block 对 self 的引用改成了弱引用。

__weak typeof(self) weakSelf = self;
self.block = ^{
    printf("%p", weakSelf);
};
  • __unsafe_unreturned

这种方式与上边的方式类似,不过当 weakSelf 指向的对象销毁后,指针已然指向那块已经被回收的内存,可能发生野指针错误,所以是不安全的。

__unsafe_unretained typeof(self) weakSelf = self;
self.block = ^{
    printf("%p", weakSelf);
};
  • __block
__block typeof(self) weakSelf = self;
self.block = ^{
    printf("%p", weakSelf);
    weakSelf = nil;
};
self.block();

我们知道,当在变量前边加了 block 之后就多了一个 blcok 变量,于是里边的引用关系就变成了:

深入理解 Objective-C ☞ Block

为了打破这个循环引用的关系,需要在 block 里边将对象置为 nil,而且必须执行 block 才能断开 __block 变量对对象的强引用。

深入理解 Objective-C ☞ Block

MRC 环境

MRC 环境下解决循环引用的方式与 ARC 环境类似,只是由于 MRC 环境下不可以使用 weak,所以只有 __unsafe_unreturned__block 2 种解决方式。对于 __block 的方式,在MRC中, __block 变量 不会对 weakSelf 产生强引用,也就不需要将其置为 nil 并执行 block 了。

__block typeof(self) weakSelf = self;
self.block = ^{
    printf("%p", weakSelf);
};

以上所述就是小编给大家介绍的《深入理解 Objective-C ☞ Block》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Algorithms on Strings, Trees and Sequences

Algorithms on Strings, Trees and Sequences

Dan Gusfield / Cambridge University Press / 1997-5-28 / USD 99.99

String algorithms are a traditional area of study in computer science. In recent years their importance has grown dramatically with the huge increase of electronically stored text and of molecular seq......一起来看看 《Algorithms on Strings, Trees and Sequences》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具