zoukankan      html  css  js  c++  java
  • Objective C block背后的黑魔法

    前言

    block在Objective C开发中应用非常广泛,我们知道block会捕获外部对象,也知道使用block要防止循环引用。

    “知其然而不知其所以然”是一件非常痛苦的事情,那么block这套机制在OC中是怎样实现的呢?本文通过从C/C++到汇编层面分析block的实现原理。


    Clang

    clang是XCode的编译器前端。编译器前端负责语法分析,语义分析,生成中间代码(intermediate representation )。

    比方当你在XCode中进行build一个.m文件的时候。实际的编译命令例如以下

    clang -x objective-c -arch x86_64
     -fmessage-length=0 
     -fobjc-arc... 
     -Wno-missing-field-initializers ... 
     -DDEBUG=1 ... 
     -isysroot iPhoneSimulator10.1.sdk 
     -fasm-blocks ... 
     -I headers.hmap 
     -F 所须要的Framework  
     -iquote 所须要的Framework  ... 
     -c ViewController.m 
     -o ViewController.o
    

    Objective C也能够用GCC来编译,只是那超出了本文的范畴,不做解说。

    Clang除了能够进行编译之外,还有其它一些使用方法。

    比方本文分析代码的核心命令就是这个:

    clang -rewrite-objc 文件.m

    通过这个命令。我们能够把Objective C的代码用C++来表示。

    对于想深入理解Clang命令的同学。能够用命令忙自带的工具来查看帮助文档

    man clang

    或者阅读官方文档:文档地址


    查看汇编代码

    在XCode中。对于一个源文件,我们能够通过例如以下方式查看其汇编代码。这对我们分析代码深层次的实现原理非常实用,这个在后面也会遇到。


    Objective C对象内存模型

    为了本文解说的更清楚。我们首先来看看一个Objective C对象的内存模型。我们首先新建一个类。内容例如以下

    DemoClass.h

    @interface DemoClass : NSObject
    @property (nonatomic, copy) NSString * value;
    @end

    DemoClass.m

    @implementation DemoClass
    - (void)demoFunction{
        DemoClass * obj = [[DemoClass alloc] init];
    }
    @end

    然后。我们用上文提到的Clang命令将DemoClass.m转成C++的表示。

    clang -rewrite-objc DemoClass.m

    转换完成后当前文件夹会多一个DemoClass.cpp文件,这个文件非常大,接近十万行。

    我们先搜索这种方法名称demoFunction,以方法作为切入

    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        DemoClass * obj = ((DemoClass *(*)(id, SEL))(void *)objc_msgSend)((id)((DemoClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
    }

    能够看到。转换成C++后。一个实例方法转换为一个静态方法,这种方法的内容看起来非常乱,由于有各种的类型强制转换,去掉后就比較清楚了。

    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        DemoClass * obj = objc_msgSend(objc_msgSend(objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
    }

    能够看到:

    • 转换后添加了两个參数:self_cmd
    • 方法的调用转换成了objc_msgSend。这是一个C函数,两个參数各自是ClassSEL

    关于objc_msgSend内发生的事情,參见我之前的一篇博客:

    到这里。我们知道了一个OC的实例方法详细是怎么实现的了。

    那么,一个OC对象在内存中是怎样存储的呢?我们在刚刚的方法的上下能够找到这个类的完整实现,

    //类相应的结构体
    struct DemoClass_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        NSString *_value;
    };
    //demoFunction方法
    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        DemoClass * obj = objc_msgSend(objc_msgSend(objc_getClass("DemoClass"), sel_registerName("alloc")), sel_registerName("init"));
    }
    //属性value的getter方法
    static NSString * _I_DemoClass_value(DemoClass * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_DemoClass$_value)); }
    extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);
    
    //属性value的setter方法
    static void _I_DemoClass_setValue_(DemoClass * self, SEL _cmd, NSString *value) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct DemoClass, _value), (id)value, 0, 1); }
    

    我们側重来看看类相应的结构体

    struct DemoClass_IMPL {
        struct NSObject_IMPL NSObject_IVARS;
        NSString *_value;
    };
    //我们依次查找不清楚的定义
    struct NSObject_IMPL {
        Class isa;
    };
    typedef struct objc_class *Class;
    struct objc_class {
        Class isa ;
    };

    能够看到,OC类实际是依照下面方式来存储对象的

    • isa指针。指向objc_class类型的结构体。这个结构体中存储了方法的列表等类相关的信息,由于objc_msgSend中。发给对象的实际是一个字符串。运行时就是通过isa找到类对象。然后通过字符串找到方法的实际运行的。
    • ivar。属性背后的存储对象,到这里也能看出来一个普通的属性就是ivar+getter+setter.

    也就是说。仅仅要有isa指针。指向一个类对象,那么这个结构就能处理OC的消息机制。也就能当成OC的对象来用。


    Block的本质

    我们改动DemoClass.m中的内容例如以下

    typedef void(^VoidBlock)(void);
    @implementation DemoClass
    
    - (void)demoFunction{
        NSInteger variable = 10;
        VoidBlock temp = ^{
            NSLog(@"%ld",variable);
        };
        temp();
    }
    @end

    然后。又一次用clang转换为C++代码。有关这段代码的内容例如以下:

    struct __block_impl {
      void *isa;
      int Flags;
      int Reserved;
      void *FuncPtr;
    };
    struct __DemoClass__demoFunction_block_impl_0 {
      struct __block_impl impl;
      struct __DemoClass__demoFunction_block_desc_0* Desc;
      NSInteger variable;
      __DemoClass__demoFunction_block_impl_0(void *fp, struct __DemoClass__demoFunction_block_desc_0 *desc, NSInteger _variable, int flags=0) : variable(_variable) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };
    static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
      NSInteger variable = __cself->variable; // bound by copy
    
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_Test_c7592d_mi_0,variable);
    }
    
    static struct __DemoClass__demoFunction_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __DemoClass__demoFunction_block_desc_0_DATA = { 0, sizeof(struct __DemoClass__demoFunction_block_impl_0)};
    
    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        NSInteger variable = 10;
        VoidBlock temp = ((void (*)())&__DemoClass__demoFunction_block_impl_0((void *)__DemoClass__demoFunction_block_func_0, &__DemoClass__demoFunction_block_desc_0_DATA, variable));
        ((void (*)(__block_impl *))((__block_impl *)temp)->FuncPtr)((__block_impl *) temp);
    }
    

    我们还是以方法作为切入点,看俺详细是怎么实现的。

    _I_DemoClass_demoFunctionDemoFunction转换后的方法。我们去掉一些强制转化代码,这样看起来更清楚

    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        NSInteger variable = 10;
        VoidBlock temp = &__DemoClass__demoFunction_block_impl_0(__DemoClass__demoFunction_block_func_0, &__DemoClass__demoFunction_block_desc_0_DATA, variable));
        (temp->FuncPtr)(temp);
    }

    从上至下,三行的左右依次是

    • 初始化一个variable(也就是block捕获的变量)
    • 调用结构体__DemoClass__demoFunction_block_impl_0的构造函数来新建一个结构体,而且把地址赋值给temp变量(也就是初始化一个block)
    • 通过调用temp变量内的函数指针(C的函数指针)来运行实际的函数。

    通过这些分析,我们知道了Block的大致实现

    block背后的内存模型实际上是一个结构体。这个结构体会存储一个函数指针来指向block的实际运行代码。

    接着,我们来深入的研究下block背后的结构体。也就是这个结构体__DemoClass__demoFunction_block_impl_0:

    struct __block_impl {
      void *isa; //和上文提到的OC对象isa一样,指向的类对象,用来找到方法的实现
      int Flags; //标识位
      int Reserved; //保留
      void *FuncPtr; //Block相应的函数指针
    };
    
    struct __DemoClass__demoFunction_block_impl_0 {
      //结构体的通用存储结构
      struct __block_impl impl;
      //本结构体的描写叙述信息
      struct __DemoClass__demoFunction_block_desc_0* Desc;
      //捕获的外部变量
      NSInteger variable;
      //构造函数(也就是初始化函数,用来在创建结构体实例的时候。进行必要的初始化工作)
      struct __DemoClass__demoFunction_block_impl_0 {
      struct __block_impl impl;
      struct __DemoClass__demoFunction_block_desc_0* Desc;
      NSInteger variable;
      __DemoClass__demoFunction_block_impl_0(void *fp,
                                             struct __DemoClass__demoFunction_block_desc_0 *desc,
                                             NSInteger _variable,
                                             int flags=0) : variable(_variable) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    };

    我们在回头看看block初始化那句代码

    //OC
    VoidBlock temp = ^{
            NSLog(@"%ld",variable);
    };
    //C++
    VoidBlock temp = &__DemoClass__demoFunction_block_impl_0(__DemoClass__demoFunction_block_func_0, 
    &__DemoClass__demoFunction_block_desc_0_DATA, 
    variable));
    

    在相应之前代码块的构造函数,我们能够清楚的看到,在初始化的时候三个參数依次是

    • 函数指针__DemoClass__demoFunction_block_func_0
    • block的描写叙述结构体(全局静态结构体)__DemoClass__demoFunction_block_desc_0_DATA
    • 捕获的变量variable

    接着。我们来看看block背后的C函数__DemoClass__demoFunction_block_func_0

    static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
      NSInteger variable = __cself->variable; // bound by copy
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_DemoClass_c7592d_mi_0,variable);
    }

    Tips:

    内存中存储区域可分为下面几个区域:

    • TEXT 代码区
    • DATA 数据区
    • Stack 栈区
    • HEAP 堆区

    上文的字符串@”%ld”。相应C++代码是)&__NSConstantStringImpl__var_folders_hj_392p68d55td2kdxrbd9h15g40000gn_T_DemoClass_c7592d_mi_0,是存储在数据区的。

    这样即使程序中有多个@”%ld”,也不会创建多个实例。

    能够看到。这个C函数的參数是__DemoClass__demoFunction_block_impl_0,也就是一个block类型。然后在方法体内部,使用这个block类型的參数。

    最后,我们分析下block的描写叙述信息。也就是这段代码

    static struct __DemoClass__demoFunction_block_desc_0 {
      size_t reserved;
      size_t Block_size;
    } __DemoClass__demoFunction_block_desc_0_DATA = { 0, sizeof(struct __DemoClass__demoFunction_block_impl_0)};

    这段代码不难理解,就是声明一个描写叙述信息的结构体,然后初始化这个结构体类型的全局静态变量。

    分析到这里,上面代码的大多数内容我们都理解了,可是有一点我们还没有搞清楚。就是isa指向的内容_NSConcreteStackBlock

    impl.isa = &_NSConcreteStackBlock;

    可是,到这里我们知道了为什么Block能够当作OC对象来用的原因:就是这个指向类对象的isa指针。


    Block的类型

    上文提到了_NSConcreteStackBlock是Block一种。block一共同拥有三种类型

    • NSConcreteStackBlock 栈上分配,作用域结束后自己主动释放
    • NSConcreteGlobalBlock 全局分配,相似全局变量。存储在数据段。内存中仅仅有一份
    • NSConcreteHeapBlock 堆上分配

    我们仍然尝试用Clang转换的方式,来验证我们的理论。

    将DemoClass.m内容修改动为

    #import "DemoClass.h"
    
    typedef void(^VoidBlock)(void);
    
    @interface DemoClass()
    @property (copy, nonatomic) VoidBlock heapBlock;
    
    @end
    VoidBlock globalBlock = ^{};
    
    @implementation DemoClass
    
    - (void)demoFunction{
        VoidBlock stackBlock = ^{};
        stackBlock();
        _heapBlock = ^{};
    }
    
    @end

    然后。转成C++后,分别相应例如以下

    全局globalBlock

    impl.isa = &_NSConcreteGlobalBlock;

    栈上stackBlock

    impl.isa = &_NSConcreteStackBlock;

    属性Block

    impl.isa = &_NSConcreteStackBlock;

    What the fuck! 怎么属性的block是栈类型的。难道不该是堆类型的吗?

    到这里。C/C++层面的代码已经无法满足我们的需求了。我们试着把代码转成汇编。一探到底:

    方便分析属性block到底是怎么实现的。我们改动.m文件

    #import "DemoClass.h"
    typedef void(^VoidBlock)(void);
    @interface DemoClass()
    @property (copy, nonatomic) VoidBlock heapBlock;
    @end
    @implementation DemoClass
    - (void)demoFunction{
        _heapBlock = ^{};
    }
    @end

    转换成汇编后。在方法demoFunction部分,我们能看到相似汇编代码

    bl  _objc_retainBlock
        adrp    x8, _OBJC_IVAR_$_DemoClass._heapBlock@PAGE
        add x8, x8, _OBJC_IVAR_$_DemoClass._heapBlock@PAGEOFF
        .loc    1 0 0                   ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:0:0
        ldr x1, [sp, #8]
        .loc    1 21 5                  ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:5
        ldrsw       x8, [x8]
        add     x8, x1, x8
        .loc    1 21 16 is_stmt 0       ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:16
        ldr     x1, [x8]
        str     x0, [x8]
        .loc    1 21 16 discriminator 1 ; /Users/hl/Desktop/OCTest/OCTest/DemoClass.m:21:16
        mov  x0, x1
        bl  _objc_release

    也就是说,在方法返回之前,依次调用了

    _objc_retainBlock
    _objc_release

    那么,_objc_retainBlock就是block从栈到堆的黑魔法。

    我们通过Runtime的源代码来分析这种方法的实现:

    id objc_retainBlock(id x) {
        return (id)_Block_copy(x);
    }
    
    // Create a heap based copy of a Block or simply add a reference to an existing one.
    // This must be paired with Block_release to recover memory, even when running
    // under Objective-C Garbage Collection.
    BLOCK_EXPORT void *_Block_copy(const void *aBlock)
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);

    到这里我们就清楚了,编译器为我们自己主动插入了_objc_retainBlock,而这个函数会把栈上的block复制到堆上。

    Tips: 通常在写属性的时候,block都会声明为copy。这是显式的表示,即使block是栈上的,也会复制到堆上。事实上在赋值的时候,编译器已经自己主动帮我们做了这些,所以事实上使用strong也能够。

    那么,一个暂时变量的block会被复制到堆上么?

    改动demoFunction:

    - (void)demoFunction{
        VoidBlock stackBlock = ^{};
    }

    继续查看汇编:

    Ltmp7:
        .loc    1 23 15 prologue_end    ; /Users/hl/Desktop/OCTest/OCTest/Test.m:23:15
        mov  x0, x8
        bl  _objc_retainBlock
        mov x8, #0
        add x1, sp, #8              ; =8
        str x0, [sp, #8]
        .loc    1 24 1                  ; /Users/hl/Desktop/OCTest/OCTest/Test.m:24:1
        mov  x0, x1
        mov  x1, x8
        bl  _objc_storeStrong
        ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
        add sp, sp, #48             ; =48
        ret

    我们仍然看到了_objc_retainBlock,也就是说即使是一个在函数中的block,在ARC开启的情况下,仍然会复制到堆上。


    __block

    通过之前的解说,我们知道了block怎样捕获外部变量,也知道了block的几种类型。那么block怎样改动外部变量呢?

    block是不能够直接改动外部变量的,比方

    NSInteger variable = 0;
    _heapBlock = ^{
        variable = 1;
    };

    直接这么写,编译器是不会通过的,想想也非常easy。由于变量可能在block运行之前就被释放掉了,直接这么赋值会导致野指针。

    在OC层面。我们能够通过添加__block关键字。那么加了这个关键字后,实际的C++层面代码是什么样的呢?

    - (void)demoFunction{
        __block NSInteger variable = 0;
        VoidBlock stackBlock = ^{
            variable = 1;
        };
    }
    

    在转换成C++代码后,例如以下:

    static void _I_DemoClass_demoFunction(DemoClass * self, SEL _cmd) {
        __Block_byref_variable_0 variable = {0,&variable, 0, sizeof(__Block_byref_variable_0), 0};
        VoidBlock stackBlock = &__DemoClass__demoFunction_block_impl_0(( __DemoClass__demoFunction_block_func_0,
                                                                        &__DemoClass__demoFunction_block_desc_0_DATA,
                                                                        (__Block_byref_variable_0 *)&variable,
                                                                        570425344);
    }

    能够看到。__block NSInteger variable = 0转换成了一个结构体

    __Block_byref_variable_0 variable = {0,&variable, 0, sizeof(__Block_byref_variable_0), 0};

    这个结构体定义例如以下:

    struct __Block_byref_variable_0 {
      void *__isa;
    __Block_byref_variable_0 *__forwarding;
      int __flags;
      int __size;
      NSInteger variable; //这个是要改动的变量
    };

    通过初始化我们能够看到

    • __isa指向0
    • __forwarding 指向__Block_byref_variable_0自身
    • __flags为0
    • __size就是结构题的大小
    • variable是我们定义的原始值0

    到这里。我们有一点疑惑

    • 为什么要存在一个__forwarding来指向自身呢?

    我们来看看block的方法体。也就是这部分

    ^{
       variable = 1;
     }

    转换成C++后:

    static void __DemoClass__demoFunction_block_func_0(struct __DemoClass__demoFunction_block_impl_0 *__cself) {
      __Block_byref_variable_0 *variable = __cself->variable; // bound by ref
        variable->__forwarding->variable) = 1;
    }

    也就是说__forwarding存在的意义就是通过它来訪问到变量的地址,假设这个指针一直指向自身。那么它也就没有存在的意义,也就是在将来的某一个时间点,它一定会指向另外一个数据结构。

    我们在上文中讲到,ARC开启的时候,栈上的block会被复制到堆上。

    在没有复制之前:

    复制之后

    这样,我们就清楚原因了:

    即使发生了复制,仅仅要改动__forwarding的指向。我们就能够保证栈上和堆上的block都訪问同一个对象。


    Block对对象的捕获

    到这里,我们分析的block都是捕获一个外部值。并非对象。值和对象最大的差别就是对象有生命周期,对象我们须要考虑引用计数。

    改动DemoFunction

    - (void)demoFunction{
        NSObject * obj = [[NSObject alloc] init];
        VoidBlock stackBlock = ^{
            [obj description];
        };
        stackBlock();
    }

    再转换成C++后,我们对照之前捕获NSInteger,发现多了两个生命周期管理函数

    static void __DemoClass__demoFunction_block_copy_0(struct __DemoClass__demoFunction_block_impl_0*dst, struct __DemoClass__demoFunction_block_impl_0*src)
    {
        _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }
    
    static void __DemoClass__demoFunction_block_dispose_0(struct __DemoClass__demoFunction_block_impl_0*src)
    {
        _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    }

    我们再查看下Block_object_assignBlock_object_dispose的定义

    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int);
    // Used by the compiler. Do not call this function yourself.
    BLOCK_EXPORT void _Block_object_dispose(const void *, const int);

    也就是说,编译器通过这两个函数来管理Block捕获对象的生命周期。当中

    • _Block_object_assign相当于ARC中的reatain,在block从栈上复制到堆上的时候调用
    • _Block_object_dispose相当于ARC中的release,在block堆上废弃的时候调用

    总结

    • block在C语言层面就是结构体,结构体存储了函数指针和捕获的变量列表
    • block分为全局,栈上。堆上三种。ARC开启的时候。会自己主动把栈上的block复制到堆上
    • __block变量在C语言层面也是一个结构体
    • block捕获对象的时候会添加对象的引用计数。

    本文的Github地址:LeoMobileDeveloper,如有问题欢迎issue。也能够在我的CSDN博客下评论,我会及时更正。

  • 相关阅读:
    数据库字段太多,批量快速建立实体类方法(适合大量字段建立实体类)
    SQL service 中的 ”输入SQL命令窗口“ 打开了 “属性界面” 回到 ”输入SQL命令窗口“
    计算机软件编程英语词汇集锦
    编程常用英语词汇
    svn上传和下载项目
    当启动tomcat时出现tomcat setting should be set in tomcat preference page
    Implicit super constructor Object() is undefined for default constructor. Must define an explicit constructor
    eclipse中选中一个单词 其他相同的也被选中 怎么设置
    Spring Boot的@SpringBootApplication无法引入的问题
    最全的SpringCloud视频教程
  • 原文地址:https://www.cnblogs.com/yjbjingcha/p/7360395.html
Copyright © 2011-2022 走看看