美文网首页
Block原理分析(1)

Block原理分析(1)

作者: Jacob6666 | 来源:发表于2020-10-16 19:19 被阅读0次

前情提要

1.闭包、Block是一个带有自动变量值(可以截获自动变量值)的匿名函数。截获的含义是保存该自动变量的瞬间值。
2.OC中如果要改变Block截获的外部自动变量的值,需要在该变量前加上__block修饰符。Swift不用,系统会自动处理这些问题。延伸到对象,对于Swift来说系统也会处理但OC不同。OC下,当从外部截获一个对象(NSMutableArray* )mArray时,在Block内调用NSMutableArray的方法(addObject:)没问题,因为Block捕获的是NSMutableArray类对象用的结构体实例指针。但若对其进行操作,比如赋值,就会产生编译错误,要在变量前加上__block。Swift下直接用就行了。
3.从C代码的角度分析closure/block。OC可以直接用clang -rewrite-objc 原文件名来分析,Swift下的clang ..我没有找到对应的命令,所以只看OC的。在分析之前,先介绍两个小概念。
--1.OC&Swift中的self
OC Code:

- (void) method:(int)arg {
    NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = [[MyObject alloc] init];
[obj method: 10];

转换成C Code:

void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg) {
    NSLog(@"%p %d\n", self, arg);
}
MyObject *obj = objc_msgSend(objc_getClass("MyObject"), sel_registerName("alloc"));
obj = objc_msgSend(obj, sel_registerName("init"));
objc_msgSend(obj, sel_registerName("method:"), 10);

最后一条执行语句,也就是[obj method: 10]转换成objc_msgSend(obj, sel_registerName("method:"), 10)objc_msgSend(obj, sel_registerName("method:"), 10)函数根据指定的对象和函数名,从对象持有类(MyObject)的结构体中检索[XX method:10]对应的函数-->void _I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的指针并调用。此时,objc_msgSend函数的第一个参数obj作为_I_MyObject_method_(struct MyObject *self, SEL _cmd, int arg)的第一个参数self进行传递。即self就是Myobject类的对象-obj自己。

--2.OC中的id

*id为objc_object结构体的结构体指针。
id在C语言中的声明:

typedef struct objc_object {
    Class isa;
} *id;

*Class为objc_class结构体的结构体指针。
Class在C语言中的声明:

typedef struct objc_class {
    Class isa;
} *Class;

这与objc_object结构体相同。然而,objc_object结构体和objc_class结构体归根结底是在各个对象(id)和类(Class)的实现中使用的最基本的结构体。

例:

类,MyObject

@interface MyObject : NSObject {
    int val0;
    int val1;
}
@end

使用MyObject创建的对象newObject = [MyObject new],就是基于objc_object创建了一个该类(MyObject)的结构体实例。通过isa保持该类(MyObject)的结构体实例指针。

struct newObject {
    Class isa;
    int val0;
    int val1;
    ...;
}

而各类的结构体是就是基于objc_class的 class_t结构体。

struct class_t {
    struct class_t *isa;
    struct class_t *superclass;
    Cache cache;
    IMP *vtable;
    uintptr_t data_NEVER_USE;
    ...;
};

class_t中持有,声明的成员变量,方法的名称,方法的实现(函数指针),属性以及父类的指针,并被OC运行时库所使用的。

对象中的isa指针和类中的isa指针的区别在于,对象的是Class类型,只负责指向他的所属类。类的isa指针是class_t类型,包含了class_t结构体中的所有内容(比如方法,父class等等),指向它自己的元类(metaclass)。他的元类中就包含了各种方法,变量等等。而他的metaclass的isa指向了NSObject,superclass指向它父类的元类。以ViewController举例:

static void OBJC_CLASS_SETUP_$_ViewController(void ) {
    OBJC_METACLASS_$_ViewController.isa = &OBJC_METACLASS_$_NSObject;
    OBJC_METACLASS_$_ViewController.superclass = &OBJC_METACLASS_$_UIViewController;
    OBJC_METACLASS_$_ViewController.cache = &_objc_empty_cache;
    OBJC_CLASS_$_ViewController.isa = &OBJC_METACLASS_$_ViewController;
    OBJC_CLASS_$_ViewController.superclass = &OBJC_CLASS_$_UIViewController;
    OBJC_CLASS_$_ViewController.cache = &_objc_empty_cache;
}

给ViewController添加了一个实例方法(instanceFunc)一个类方法(classFunc)。
Metaclass方法列表的赋值:(const struct _method_list_t *)&_OBJC_$_CLASS_METHODS_ViewController;--->OBJC_$_CLASS_METHODS_ViewController--->:

_OBJC_$_CLASS_METHODS_ViewController __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_objc_method),
    1,
    {{(struct objc_selector *)"classFunc", "v16@0:8", (void *)_C_ViewController_classFunc}}
};

实例方法被保存在了_class_ro_t _OBJC_CLASS_RO_$_ViewController这里他会赋值给_class_t OBJC_CLASS_$_ViewController也就是本类。

即元类保存了类方法。

本类保存了实例方法。

MyObject类的实例变量val0,val1被直接声明为对象的结构体成员。OC中由类生成对象就意味着,生成一个基于该类的结构体实例,这些结构体实例(对象)通过isa保持该类的结构体实例指针。举个例子就是:有3个类,C动物->C狗->C柯基,基于柯基生成的一个对象叫做KK,那么KK这个结构体就会有一个isa指向C柯基,而这个C柯基的isa指向自己的元类。当KK发送了一个消息(狼嚎时)因为C柯基本类的class_t结构体中的cache和vtable都没有这个方法,便逐级会向上传递,找不到再performslector等等。


介绍完两个小概念,开始从C代码的角度分析Block,举两个例子,一个是不截获自动变量值的另一个是截获自动变量值的:

不截获自动变量值:

.m文件下的代码:

int main() {
    void(^blk)(void) = ^{printf("Block\n");};
    blk();
    return0;
}

clang -rewrite-objc 项目文件名.m之后,摘取有用的内容:

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("Block\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() {
    void(*blk)(void) = ((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);
    return0;
}

源码中的^{printf("Block\n");};转换后的代码是

static void__main_block_func_0(struct __main_block_impl_0 *__cself) {printf("Block\n");}

通过转换后的代码可知,通过Block使用的匿名函数实际上被作为简单的C语言函数来处理。另外根据Block语法所属的函数名(此处为main)和该Block语法在该函数出现的顺序值(此处为0)来给经clang变换的函数命名(void__main_block_func_0)。该函数的参数*__cself就是指向Block值的变量,__main_block_impl_0结构体的指针。

那我们来看看__main_block_impl_0结构体,去掉构造函数后:

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

第一个成员变量是impl,他的结构体声明是:

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

impl是虚函数列表,该结构体内存放了一个标志,包括今后版本升级所需的区域及函数指针。

第二个成员变量是Desc指针,__main_block_desc_0结构体的声明是:

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
};

这些也如同其成员名称所示,其结构为今后版本升级所需的区域和Block大小。
这两个成员变量看完之后,再来看一下初始化含有这些结构体的__main_block_impl_0结构体的构造函数,就是我们刚才忽略的那一部分代码,这个代码执行完成意味着_cself初始化完成:

    __main_block_impl_0(void *fp,struct__main_block_desc_0 *desc,intflags=0) {
      impl.isa = &_NSConcreteStackBlock;
      impl.Flags = flags;
      impl.FuncPtr = fp;
      Desc = desc;
  }

在分析这个结构体之前,先看一下这个构造函数的调用(在 int main() {...}中):

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

去掉转换部分 简化成两行代码:

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;

该源代码将__main_block_impl_0结构体类型的自动变量(栈上生成的__main_block_impl_0结构体实例的指针),赋值给__main_block_impl_0结构体指针类型的变量blk,对应未转换前的代码就是这个赋值操作:

void(^blk)(void) = ^{printf("Block\n");};

即,将Block语法生成的Block赋给Block类型变量blk。该源代码中的Block就是__main_block_impl_0结构体类型的自动变量,即栈上生成的__main_block_impl_0结构体实例。然后指针赋值。

接下来看看__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);结构体实例构造参数。__main_block_func_0&__main_block_desc_0_DATA

__main_block_func_0是一个由Block语法转换的C语言函数指针,对应的函数就是:static void __main_block_func_0(struct__main_block_impl_0 *__cself) {printf("Block\n");}

&__main_block_desc_0_DATA是作为静态全局变量初始化的__main_block_desc_0结构体实例指针。其初始化代码如下:

__main_block_desc_0_DATA= {0,sizeof(struct__main_block_impl_0)};

由此可知,该源代码使用Block,即__main_block_impl_0结构体实例的大小,进行初始化。


下面综合来看一下,栈上的Block即__main_block_impl_0结构体实例的初始化到底是怎样完成的(相关参数展开):

struct __main_block_impl_0 {
    void *isa;
    Int Flags;
    int Reserved;
    void &FuncPtr;
    struct __main_block_desc_0* Desc;
}

该结构体会根据构造函数进行如下的初始化:

isa = &_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;
FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;

即,向内存申请空间,并将__main_block_func_0函数指针赋值给成员变量FunPtr,为后续调用做好准备。


源码中调用blk的部分blk()对应转换后的代码是:

((void(*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);

去掉转换部分:

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

这就是简单地使用函数指针调用函数。

阶段总结

由Block语法转换的__main_block_func_0函数的指针被赋值给栈上生成的__main_block_impl_0结构体实例的FuncPtr中,再将栈上生成的__main_block_impl_0结构体实例赋值给Block类型的变量blk,之后调用blk()
通过观察调用函数,__main_block_func_0函数的参数__cself指向Block值blk,调用:(*blk->impl.FuncPtr),参数:(blk)。由此看出Block正是作为参数进行了传递。


截获自动变量值的:

.m文件下的代码:

#include <stdio.h>
int main() {
    int unCapturedVariable = 100;
    int capturedVariable = 60;
    const char *fmt = "capturedVariable = %d/n";
    void(^blk)(void) = ^{printf(fmt, capturedVariable);};
    blk();
    return 0;
}

clang -rewrite-objc 项目文件名.m之后,摘取有用的内容:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int capturedVariable;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _capturedVariable, int flags=0) : fmt(_fmt), capturedVariable(_capturedVariable) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int capturedVariable = __cself->capturedVariable; // bound by copy
printf(fmt, capturedVariable);}

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 unCapturedVariable = 100;
    int capturedVariable = 60;
    const char *fmt = "capturedVariable = %d/n";
    void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, capturedVariable));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

这与前面转换的源代码稍有差异。下面来看看不同之处。首先我们注意到,Block语法表达式中使用的自动变量(capturedVariablefmt)被作为成员变量追加到了__main_block_impl_0中。并且类型完全相同,而Block语法表达式中没有使用的自动变量(unCapturedVariable)并没有被追加进去。

因此,Block的自动变量截获只针对Block中使用的自动变量。

再来看看初始化__main_block_impl_0结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _capturedVariable, int flags=0) : fmt(_fmt), capturedVariable(_capturedVariable) {...},有什么不同。
在初始化结构实例时,根据传递给构造函数的参数对成员变量(capturedVariable和fmt)进行初始化。通过以下代码:
void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, capturedVariable));。使用执行Block语法时的自动变量capturedVariablefmt来初始化__main_block_impl_0结构体实例(系统生成的Block,或者叫右边的Block可能好理解一些)。即在该源代码中,__main_block_impl_0结构体实例的初始化方法如下(相关参数展开):

struct __main_block_impl_0 {
    void *isa;
    Int Flags;
    void &FuncPtr;
    struct __main_block_desc_0* Desc;
    char *fmt;
    int capturedVariable;
}

赋值:

impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &__main_block_desc_0_DATA;
fmt = "capturedVariable = %d/n";
capturedVariable = 60;

由此可知在__main_block_impl_0结构体实例中,成员变量(fmtcapturedVariable)被外部自动变量值赋值。

下面再来看一下使用Block的匿名函数的实现。转换前的代码片段是:printf(fmt, capturedVariable);
转换后的是:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  const char *fmt = __cself->fmt; // bound by copy
  int capturedVariable = __cself->capturedVariable; // bound by copy
printf(fmt, capturedVariable);}

在转换后的源代码中,截获到__main_block_impl_0结构体实例的成员变量的自动变量(__cself->fmt__cself->capturedVariable), bound by copy!这些变量(fmtcapturedVariable)在Block语法表达式执行之前(没初始化__main_block_impl_0之前)就已经被声明定义在__main_block_impl_0结构体里。

总的来说,所谓“截获自动变量值”意味着在执行__main_block_func_0时,Block语法表达式所使用的自动变量值已经被保存到Block的结构体实例(__main_block_impl_0)中了。在函数中可以直接截获到它们,并使用。


相关文章

  • Block原理分析(1)

    前情提要 1.闭包、Block是一个带有自动变量值(可以截获自动变量值)的匿名函数。截获的含义是保存该自动变量的瞬...

  • block分析(下)

    block通过clang分析 带着下面的疑问,我们去探索block原理 探索block底层源码 block在底层是...

  • Block原理分析(2)完结

    前情提要 基于Block原理分析(1)[https://www.jianshu.com/p/afd031effac...

  • 深入研究Block用weakSelf、strongSelf、@w

    前言 在上篇中,仔细分析了一下Block的实现原理以及__block捕获外部变量的原理。然而实际使用Block过程...

  • Block原理分析

    block类型 我们都知道block有三种类型:NSGlobalBlock、NSStackBlock、NSMall...

  • 基础的block实现原理

    理解定义 BLock官方的定义 分析实现原理 从最简单的block开始 将下面的 simple_test.m 代码...

  • iOS:Block(二)

    推荐阅读:Block原理分析(一) 目录一,修改变量二,基本数据类型的__block变量三,__forwardin...

  • Block 原理面试(1)

    玖:Block 原理面试(1) block的原理是怎样的?本质是什么?答:Block 的本质是一个封装了函数及其调...

  • Block原理分析详解

    1 基本说明 Block一直是OC的一个重点、难点、黑科技。Block在日常项目中经常使用,他的实现方式和一般的o...

  • ios block原理分析

    Block本质上也是一个OC对象,它内部也有个isa指针,它是封装了函数调用以及函数调用环境的OC对象。闭包 = ...

网友评论

      本文标题:Block原理分析(1)

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