欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

那些关于iOS Blocks的坑(一)

程序员文章站 2022-03-09 21:38:45
...

  • 很多时候,我们都只关心怎么把一些语法用得非常熟练,记住所有的坑点,比如说Blocks,什么循环引用的坑,属性声明用copy...等等。但是我们总是懒于去深究,为什么会有这些问题。源码面前,没有秘密。今天就一起来分析下关于Blocks的底层。

这个专题主要介绍Blocks的底层实现,分为以下几个部分,将通过多篇博文一一阐述。
1.Blocks的本质
2.Blocks为什么截取自动变量值
3.__block说明符
4.关于Blocks的存储
5.关于__block变量的存储
6.Blocks的循环引用问题


Blocks的本质

  • Blocks究竟是什么?我们先用clang(LLVM编译器)将我们的OC代码转化C++源代码,所谓源码面前,了无秘密。
//block.m
int main() {
    int count = 10;
    void (^blk)(void) = ^{
        printf("%d\n", count);
    };
    blk();
    return 0;
}

<strong>clang -rewrite-objc 源代码文件名</strong>

#define BLOCK_IMPL
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 count;

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    printf("%d\\n", count);
}

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

以上便是那份OC代码block.m文件编译出来摘取的关键代码。几行代码瞬间变成了几十行代码。看着很吓人,其实不难分析。

先来看main函数中的调用声明和调用block的代码,转化成了什么。
int main() {
    int count = 10;
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, count));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

  • int count = 10; 局部变量count,赋值为10;
  • void (*blk)(void), 这个东西有C语言函数指针基础的童鞋肯定一眼就认出来。其实void (^blk)(void) 就是转化为指针名字为blk的函数指针。我们对block赋值,实则是对该函数指针赋值。
  • 其次我们对void (*blk)(void)函数指针进行赋值,赋值对象为结构体struct __main_block_impl_0,<strong>通过它的构造方法对相应的参数进行赋值。</strong>
    • impl.isa = &_NSConcreteStackBlock;isa指针其实是指向其父类class_t的地址,后面会有讨论。
    • impl.FuncPtr = fp; 这一句很关键,该FuncPtr是指向调用方法的指针,也就是我们执行block表达式时,去调用的方法,这里传的参数是方法__main_block_func_0;
  • 最后一句代码就是通过调用__block_impl指针中的FuncPtr指向的方法去执行block;
__cself参数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int count = __cself->count; // bound by copy
    printf("%d\\n", count);
}
  • 执行blk()调用的函数中,参数__cself类型便是上面我们分析的这个结构体,大家肯定一下子就明白了,这是参数相当于self, 在C++中相当于this指针。即是当前类的对象。
  • <strong>__main_block_impl_0</strong>结构体中包含以下这些成员变量。
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int count;
}
  • <strong>struct __block_impl</strong>

结构体中的第一个参数所对应的结构体 struct __block_impl,根据名称可以联想到某些标志,今后版本升级所需的区域,以及函数指针。

struct __block_impl {
    void *isa;  
    int Flags;        //标志
    int Reserved;     //今后版本升级所需的区域
    void *FuncPtr;    //函数指针
};
  • <strong>struct __main_block_desc_0* Desc</strong>
static struct __main_block_desc_0 {
    size_t reserved;      //今后版本升级所需的区域
    size_t Block_size;    //Block的大小
} 
  • <strong>__main_block_impl_0结构体中最后一个成员变量count</strong>
    不难发现,这是个类成员变量,是存在于block内部的。这也是为什么block会截取局部变量的原因,它是从外部定义的变量count中,通过值传递拷贝到block内部的。所以也就导致,在外部修改了count值,是没办法在block中也对应修改的原因。接下来会详细介绍。
__main_block_func_0函数
int count = __cself->count; // bound by copy
printf("%d\\n", count);
  • 到这里大家应该豁然开朗了,这里打印的count值,是通过__cself指针获取到block中的那个类成员变量count。以上便是这样一个block的底层代码分析。

Blocks为什么截取自动变量值

  • 从上面的分析中,我们知道,count变量在block定义的时候,便作为形式参数,通过__main_block_impl_0构造函数进行了值拷贝。
何为值拷贝,何为地址拷贝?
  • 所谓值拷贝,<strong>就是重新开辟一块内存将值拷贝存进这块新的内存中。它和被拷贝的值所在的地址是不同的。</strong>
  • 所谓地址拷贝,<strong>就是开辟一个指针的内存,将指针的指向赋值为被拷贝的值所在的那块内存。</strong>
  • 通过值拷贝,无法通过修改被拷贝的值进而修改拷贝的那个值,这也造成了所谓的<strong>Blocks截取自动变量值</strong>的效果。而通过地址拷贝可以做到,这也是接下来__block将要做的事情。

__block关键字

  • 前面一个例子中,如果我们在Block中进行局部变量的修改,那么编译器就不乐意了。
//block.m
int main() {
    int count = 10;
    void (^blk)(void) = ^{
        count = 11;//编译错误
    };
    blk();
    return 0;
}
  • 产生以下编译错误
error: variable is not assignable (missing __block type specifier)
        count = 11;
  • 显然,编译器是拒绝这么做的,并告诉我们 count变量缺少 <__block类型说明符 >(missing __block type specifier)
解决方案
  • 对于这个问题,有两种解决方案。
    • 第一种就是编译器提示我们的,对count加入<strong>__block声明</strong>
    • 第二种则是将count声明为<strong>静态变量,静态全局变量,或者全局变量。</strong>

  • 直接看__block声明之后,编译器为我们做了些什么。
//block.m
int main() {
   __block int count = 10; 
   void (^blk)(void) = ^{ 
      count = 11;
   };
   blk();
   return 0;
}

clang -rewrite-obj 编译文件

struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_count_0 *count; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_count_0 *_count, int flags=0) : count(_count->__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_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 11;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->count, (void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->count, 8/*BLOCK_FIELD_IS_BYREF*/);}

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

int main() {

    __attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
        (void*)0,
        (__Block_byref_count_0 *)&count, 
        0, 
        sizeof(__Block_byref_count_0), 
        10
    };
    
    void (*blk)(void) = ((void (*)())&__main_block_impl_0(
    (void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_count_0 *)&count, 570425344));
    
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

    return 0;
}
  • 每次编译出来是不是都感觉压力山大,哈哈。慢慢剖析一下通过__block说明符编译出来的源码。
  • 先来看看__block变量count是怎么转化过来的。
__attribute__((__blocks__(byref))) __Block_byref_count_0 count = {
        (void*)0,
        (__Block_byref_count_0 *)&count, 
        0, 
        sizeof(__Block_byref_count_0), 
        10
    };

  • 通过前面的介绍,我们可以看到,这个过程又转化成了我们相对比较熟悉的结构体了。__block变量如同Blocks一样变成了__Block_byref_count_0结构体类型的自动变量,即在栈上生成的__Block_byref_count_0的结构体实例。
__Block_byref_count_0
struct __Block_byref_count_0 {
  void *__isa;
__Block_byref_count_0 *__forwarding;
 int __flags;
 int __size;
 int count;
};

相比起我们一开始编译出来没有使用__block说明符声明的代码中,多出了我们不太熟悉的一个指针

__forwarding    
//持有指向该实例自身的指针。而我们之所以能在block中修改外部变量的原因就在于此。
//原理就是,通过修改成员变量__forwarding访问成员变量count。(成员变量count是该实例自身持有的变量,它相当于block中的原自动变量)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_count_0 *count = __cself->count; // bound by ref

        (count->__forwarding->count) = 11;
}
// 通过以上这段代码,大家就知道了,__block说明符就是通过修改__forwarding指针中的count变量从而达到修改外部变量的效果,多数时候我们可以推理出,能够修改一个地方达到修改其他地方的这种场景,无异于对同一块内存上的内容进行了修改,而能够达到这种效果的,便是指针的操作。

至于静态变量,静态全局变量和全局变量也能达到这个效果的办法,请大家自己编译源码查看下,其实原理也是通过修改指针来达到这个效果的。而这些变量的值是存在于__main_block_impl_0结构体中的

关于Blocks的存储

  • 从上面编译出来的代码中,我们前面提到的isa指针,在初始化Block中,impl.isa = &_NSConcreteStackBlock;所谓结构体类型的自动变量,即栈上生成的该结构体的实例。

  • 以上的Block的类转化为_NSConcreteStackBlock,虽然并没有出现转化过后对应的源代码,但还有几个与之类似的类.

    • _NSConcreteStackBlock //将该类的对象Block设置在栈上
    • _NSConcreteGlobalBlock //与全局变量一样,设置在数据区域(.data区)中
    • _NSConcreteMallocBlock //设置在malloc函数分配的内存块中(即堆)
分析如何创建对应存储区的Block
  • _NSConcreteStackBlock : 通常情况下,在方法内部手动声明定义的Block,均为栈上分配的。
  • _NSConcreteGlobalBlock : 将Block声明在函数外部,作为全局变量的Block则是分配在数据区域。
  • _NSConcreteMallocBlock :