ios - block原理解读(一)

作者: tigerAndBull | 来源:发表于2019-02-17 20:03 被阅读104次

前言

block在网络上的文章也比较多,
本文将开发中block使用细节和block实现原理结合起来,
加上个人的理解,
帮助大家更好地理解block和使用block。

问题

  • block的意义
  • block为什么不能修改外部变量?这里的外部变量又指的是什么?
  • block为什么要用copy
  • 被block引用的对象,引用计数为何+=2?
  • __block 又是什么原理?
  • 循环引用究竟是为何引起的?

等等

block产生的意义

程序始终都要遵循逐行执行的原则,
而block 可以理解为 逻辑触发执行
定时器 可以理解为 时间触发执行

举个有点意思的例子:

背景:我现在身处异世界,我有很多酷炫的技能
我现在有这样一个技能,发动这个技能,我可以在当前这个地方留一个分身并安排好任务,然后我可以继续做我自己的事情了,等我再次使用这个技能时,我的分身将开始处理这项任务,处理完成后,我的分身将会消失。

隐含的问题:给分身安排具体任务的时候,这个任务在未来是否能够完成是未知的,因为我们不知道未来会发生什么,
代码亦是如此

进一步理解:程序是严密而又真实的,所以并不存在什么高科技呀,黑魔法呀~
block其实就是用程序实现了代码缓存和对象缓存,相应的对象缓存在block对象中,
执行block,就是把缓存的代码执行一遍,而相应的对象的状态,可能会因为执行完缓存下来的代码而发生变化。

到此,希望你对block会产生了那么一点点的兴趣~

依旧还是要从源码说起


强调:以下讲的变量a 默认指的是 基础类型的变量,不是对象类型


#import <UIKit/UIKit.h>

int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        int a = 0;
        void (^block)(void) = ^{
            NSLog(@"%d",a);
        };
        
       block();

        return 0;
    }
}

将其翻译成底层c++文件, 一点一点看
main函数里的代码

int a = 10;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

这是一大串什么?
不要着急,我们从上到下,从左往右进行说明。
首先,block初始化这一行

// 原代码
void (^block)(void) = ^{
       NSLog(@"%d",a);
 };
// 翻译成c++代码
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 简化后
block =  &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA,a);
  1. void (*block)(void) 函数指针,参数void,返回值void

  2. ((void (*)()) 上面函数指针的类型,作用是强转

  3. &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)) 分解来看

分解第一步:__main_block_impl_0 组成

__main_block_impl_0是一个结构体,包含一个构造函数和三个变量
一个普通的结构体类型,一个结构体指针类型,
还有一个和外部的变量a,名称一样,类型一样。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int 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;
  }
};
分解第二步:__main_block_impl_0 的 构造函数

第一个参数void *fp,传入的是 &__main_block_func_0

__main_block_func_0是一个静态函数,
它的参数又是__main_block_impl_0这个结构体指针

即 FuncPtr 存储的是 __main_block_func_0 静态函数地址

impl.FuncPtr = fp;
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_lb_tby1gwds2fnb89dzkf4cq3xh0000gn_T_main_9bc6d9_mi_0,a);
}

是不是快要绕晕了呢?

在静态函数__main_block_func_0内,
先是获取__main_block_impl_0结构体内的变量a,然后打印出来。

从这一点可以看出 静态函数的作用就是写在block内部的代码的容器和入口。

而__main_block_impl_0结构体内的变量a的值来自外部,是在结构体的构造函数内进行了赋值操作。

得到如下结论:

获取到的基础变量的值在block初始化的时候已经确定了,
block外部的变量a在后续无论做什么操作,都不会影响block内部保存的变量a
这就是在block内部直接修改基本变量会报错的原因,
如果这里直接允许修改了,在目前条件下,也仅仅能做到block内部的变量a进行重新赋值操作,和外部变量a没有关系,产生歧义。

示例图.png
当然,使用__block修饰的基本变量可以进行修改,那么又是什么原理呢?我们先继续解读当前的源码

第二个参数 &__main_block_desc_0
__main_block_desc_0静态结构体存储的是block的基础信息

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

第三个参数为我们用到的外部的变量a,赋值给结构体内的变量a

第四个参数没有传,默认为0 (辅助变量,可以忽略,不会影响block的理解)

分解第三步:__main_block_impl_0 中的 __block_impl

存储的block的信息,相应的参数在构造函数内进行了赋值操作

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

最后 block初始化代码和执行代码放在一起看

void (^block)(void) = ^{
       NSLog(@"%d",a);
 };
// c++代码
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
// 简化
block =  &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a)

block();
// c++代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 简化
block->FuncPtr(block);

block->FuncPtr 指的就是 __main_block_func_0这个函数的地址,参数为block本身

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

串起来再看block构造

block初始化:block 通过 __main_block_impl_0结构体构造函数进行初始化,同时生成__main_block_func_0静态函数,并将其地址以及其他相关信息储存在__block_impl这个结构体成员变量中。
其中,__block_impl这个结构体成员变量是__main_block_impl_0的首地址。

block调用:block指针指向的是__main_block_impl_0 的首地址,即__block_impl的地址,所以可以强转为(__block_impl *)类型,并访问其成员FuncPtr,指向的是静态函数地址,并传入参数__main_block_impl_0,也就是block自己。

名称 类型 是否随block内容改变 生成顺序 说明
__block_impl 结构体 NO 1 底层结构体,属于__main_block_impl_0成员
__main_block_impl_0 结构体 YES 2 缓存变量/对象,主结构体
__main_block_func_0 静态函数 YES 2 缓存代码,地址存放在__block_impl中

该缓存代码指的是:

缓存代码一词,代码的意思.png

总结:
将外部变量/对象的信息缓存在__main_block_impl_0中,
将代码缓存在静态函数中,
静态函数在缓存代码的时候需要用到外部变量/对象的信息
执行block就是执行了该静态函数


如果到此有不理解的地方可能c++基础较薄弱,百度一下辅助查看


最后

本文说明了block的用意,揭开了黑魔法的初级面纱,细讲了block基础源码,解释了基础类型的变量为何不能在block内部直接修改

后续会借此基础之上,继续解读目录中的问题

相关文章

网友评论

    本文标题:ios - block原理解读(一)

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