那些关于iOS Blocks的坑(一)


  • 很多时候,我们都只关心怎么把一些语法用得非常熟练,记住所有的坑点,比如说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 :
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容