美文网首页
iOS底层-- block

iOS底层-- block

作者: Engandend | 来源:发表于2020-09-02 15:49 被阅读0次

手动目录

  • 循环引用
  • block的类
  • Block的相关信息
    block本质
    block如何捕获外界变量?
    __block修饰的本质
    block 捕获外部变量的补充
  • block 堆/栈 转移过程
    汇编分析
    源码分析
  • block 签名
  • block的三层拷贝
    _Block_copy (第一层拷贝)
    block_assign (第二层拷贝)
    __Block_byref_id_object_copy_131 (第三层拷贝)
    _main_block_dispose_0 释放
  • Block总结
  • GCD的Block 是否需要weak?

循环引用

一般来说 对于 不会自动release的block 为了避免循环引用,一般采用中介者模式
比如 __weak typeof(self) weakSelf = self

中介者模式原理:
用weak之前:
self -> block -> self
用weak之后
weakSelf -> self -> block -> weakSelf
但是 weakSelf 由弱引用表来维护,不会进行计数器加减。在dealloc的时候,weakSelf被释放, 就打破了循环引用

所有的block 都可以用weak来修饰吗? 答案是否定的:可能会造成 self被提前释放

@property (nonatomic, strong) NSString *name;   

- (void)viewDidLoad {
    _name = @"asdasd";
    [super viewDidLoad];
   __weak typeof(self) weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;        // 这一行加不加 都会造成self被提前释放
        NSLog(@"%@",strongSelf.name);
    });
}  
- (void)dealloc {
    NSLog(@"dealloc");
}

操作:
跳转到这个界面之后,2s内进行返回。
返回的时候会进行正常的dealloc 打印,在2s后 打印 NSLog(@"%@",weakSelf.name); 的时候就出现了问题: 打印(null)。

这是为什么呢:
weakSelf是弱引用,因为dispatch_after 是到时才将block加入队列, 在block执行之前,并没有进行strongSelf对其进行持有,所有weakSelf会在dealloc的时候被释放, weakSelf、self、_name 都会被释放,所以打印就会出现null。
所以:weak并不是什么地方都可以使用的

在看另外一个例子:

typedef void(^HandleBlock)(void);
@property (nonatomic, copy) HandleBlock block;

//    self -> block -> self (这个self 虽然没有被after copy,但是被 self.block copy)
    self.block = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",self.name);
        });
    };
    self.block();


//   strong ->  weakSelf -> self -> block -> strong (strong 是临时变量,block任务执行完毕,strong就被释放)
__weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
    };
    self.block();

使用strong-weak-dance 解决引用对象可能被提前释放的问题。

block的类

block 有几种? - 6 种
源码 libclosure-74 中列举出6种block

void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
void * _NSConcreteWeakBlockVariable[32] = { 0 };

其中 上层常用的3种:
__NSGlobalBlock__(全局)、__NSMallocBlock__(堆)、__NSStackBlock__(栈)
另外三种一般是系统级别去使用。

我们用代码来打印常用的三种:

种类一:全局block(block内部不捕获外部变量)            <__NSGlobalBlock__: 0x10696c450>  
void(^ block)(void) = ^{
    NSLog@(@"a");
};
NSLog(@"block = %@",block);  

id globalBlock = ^(NSString *name,NSInteger a){        //这个也是全局   因为他没有捕获外部变量
        NSLog(@"globalBlock : name = %@",name);
};

种类二:堆block (block内部捕获临时变量)                <__NSMallocBlock__: 0x600001dba220>
int a = 0;
void(^ block)(void) = ^{
    NSLog(@"a = %@",@(a));
};
NSLog(@"block = %@",block);

种类三:栈block (捕获外部变量,并在copy之前)            <__NSStackBlock__: 0x7ffeed693978>
int a = 0;
NSLog(@"block = %@",^{
    NSLog(@"%@",@(a));
});

这里有需要注意的点: 上面的int a = 0; a是临时变量
1、对于种类二:如果a 是全局变量、全局静态变量、局部静态变量,二的类型是 __NSGlobalBlock__
2、在捕获临时变量a的时候,本身是栈block,
因为 临时变量在栈上,超过作用域 会被销毁,为了保证数据安全,系统是 将其自动进行copy操作, 会将其拷贝到堆上。这个时候,这个block变成了mallocBlock。

简单点说:
block默认是全局变量,但是在捕获了栈上 的变量(临时变量),那么它是栈block,但是栈block为了数据安全,会自动进行copy操作,将其变成堆block。

Block的相关信息

block本质

用代码进行clang (.m 文件也可以,但是clang出来的内容太长,我们用.c文件 操作)

// 创建一个 C File 文件  BlockTest.c
#include <stdio.h>
int main() {
    void(^block)(void) = ^{
        printf("a");
    };
    block();
}

使用clang命令:clang -rewrite-objc BlockTest.c 得到以下信息

int main() {
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

// 简化我们需要的信息
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));        // 构造函数
((void (*)))(block)->FuncPtr)((__block_impl *)block);        // 调用函数

// 在编译后的文件中可以找到由以下信息
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;
  }
};

所以block 的本质 可以说是对象,深一层 可以说是结构体 (上面已经打印了block的类名)

block如何捕获外界变量?

还是用代码进行clang

int main() {
    int a = 10
    void(^block)(void) = ^{
        printf("a = %d",a);
    };
    block();
}
// 查看相关信息
int main() {
    int a = 10;
 //与上面相比 __main_block_impl_0  多了一个参数  a
    void(*block)(void) = &__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));             
    block)->FuncPtr)((__block_impl *)block);
}

//       结构体中, 多了一个参数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;
  }
};

// 在执行代码中,将a进行了copy操作
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy            // ------ 在这里 前面的a 和后面的a 地址不同。
        printf("a = %d",a);
 }

在这里考虑一个问题: a没有进行__block 修饰的情况下,进行a ++ 会是什么结果?
假设能编译通过的情况下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
        a ++;                        //  在这里进行a++
        printf("a = %d",a);
 }

那么a++ 是对 int a 中的a 进行操作。和外部的a 完全没有任何关系。a++中的a 是内部的a ,在外界没有被定义(不能访问内部的a),所以是不允许的。

结论:

  • 如何捕获变量
    在block的结构体中,生成了一个新的成员变量去保存外部变量。

  • 在没有__block修饰的情况下,为何不能进行修改外部变量?
    如果在block中进行修改属性,实质上是对内部成员变量的修改,但是在外部,是无法当问结构体中生成的变量。所以无法修改。

__block修饰的本质

还是用代码进行clang

int main() {
        __block int a = 10;
    void(^block)(void) = ^{
        printf("a = %d",a);
    };
    block();
   }
// 简化之后
int main() {
// 结构体的初始化
    __Block_byref_a_0 a = {(void*)0,
                            (__Block_byref_a_0 *)&a,
                            0,
                            sizeof(__Block_byref_a_0),
                            10};            
// 注意第三个参数传递的是一个指针地址。
    void(*block)(void) = (&__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));        
    block)->FuncPtr)((__block_impl *)block);
}

// 生成了一个 关于 a 的 __Block_byref_a_0 的结构体
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

// __block修饰后 block的结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__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_a_0 *a = __cself->a; // bound by ref
        printf("a = %d",(a->__forwarding->a));
    }

结论:
--block 是生成了一个 block_byref_ 的结构体 用来保存原有变量的指针 和 值
传递给block的是一个指针。

block 捕获外部变量的补充

这部分内容来源于另一片文章
其中有一部分内容讲解了 自动变量/静态变量/静态全局变量/全局变量 多种变量在block中捕获的情况

说明:
自动变量 --- 临时变量(方法内部定义的变量)
静态变量 --- 方法内部定义的static 变量
静态全局变量 --- 方法外部定义的static变量
全局变量 ---- 类的成员变量

int a = 10;
static int  b = 20;

int main(int argc, char * argv[]) {
    static int  c = 30;
    int         d = 40;
    __block int e = 50;
    void(^block)(void) = ^{
        a++;    b++;    c++;    e++;
//        d++;   内部无法操作
        printf("\na = %d  b = %d c = %d d = %d e = %d",a,b,c,d,e);
    };
    a++;    b++;    c++;    d++;    e++;
    printf("a = %d  b = %d c = %d d = %d e = %d",a,b,c,d,e);
    
    block();
    retrun 0;
}

这里的打印结果
外部 a = 11 b = 21 c = 31 d = 41 e = 51
内部 a = 12 b = 22 c = 32 d = 40 e = 52

因为d是在外部进行++之前就进行了值拷贝,其他的是进行指针访问/直接 访问,所以都有产生了相应的变化。

clang 转换之后的关键信息

// 简化处理
int main() {
    static int c = 30;
    int d = 40;
    __Block_byref_e_0 e = {(void*)0,
                            (__Block_byref_e_0 *)&e,
                            0,
                            sizeof(__Block_byref_e_0),
                            50};
    void(*block)(void) = (&__main_block_impl_0(__main_block_func_0,
                                               &__main_block_desc_0_DATA,
                                               &c,
                                               d,
                                               (__Block_byref_e_0 *)&e,
                                               570425344));
    a++; b++; c++; d++; (e.__forwarding->e)++;
    printf("a = %d  b = %d c = %d d = %d e = %d",a,b,c,d,(e.__forwarding->e));
    
    (block)->FuncPtr)((__block_impl *)block);
    
}

// block 新结构         🌟新增了 c(int *)、d(int)、e(__Block_byref_ *)🌟
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *c;
  int d;
  __Block_byref_e_0 *e; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_c, int _d, __Block_byref_e_0 *_e, int flags=0) : c(_c), d(_d), e(_e->__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_e_0 *e = __cself->e; // bound by ref
  int *c = __cself->c; // bound by copy
  int d = __cself->d; // bound by copy

        a++; b++; (*c)++; (e->__forwarding->e)++;

        printf("\na = %d  b = %d c = %d d = %d e = %d",a,b,(*c),d,(e->__forwarding->e));
    }

从上面的结果中可以知道以下信息

  • 1、全局变量(静态/非静态) 都不参与block内部处理。 而是直接访问外部变量
    block结构体中没有新增a、b的成员变量
    static void __main_block_func_0 中也没有对a、b的处理,只有对c、d、e的相应处理

  • 2、静态变量(c)、临时变量(d)、__block修饰的临时变量(e) 都会被block 编译进结构体,并进行间接访问 -- 指针地址访问/值拷贝

  • 3、变化类型
    静态变量 内部 是处理成相应的指针 c => int *c
    临时变量 只是做一个值存储 d => int d
    __block修饰的内部变量处理成指针 e => __Block_byref_e_0 *e

  • 4、指针地址拷贝/值拷贝
    静态变量是指针地址拷贝 ----------------内部 能对外部进行修改 int *c = __cself->c; // bound by copy
    __block 是指针地址拷贝 ----------------内部 能对外部进行修改 __Block_byref_e_0 *e = __cself->e; // bound by ref
    临时变量是值拷贝 ------------------------内部不能对外部进行修改 int d = __cself->d; // bound by copy

再换一个情况 ,block内部使用指针

NSMutableArray *arr = [NSMutableArray new];
    NSString *a = @"a";
    void(^block)(void) = ^{
        [arr addObject:@"1"];
//        arr = [NSMutableArray new];    这一行不被允许  
    };
    block();

用clang转换后的结构 类似于上面的 静态变量

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSMutableArray *arr;                                      //    ⬅️ 重点
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, NSString *_str, int flags=0) : arr(_arr), str(_str) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSMutableArray *arr = __cself->arr; // bound by copy              //  ⬅️这里也是指针拷贝
......
}

block 捕获变量总结

  • 1 、全局变量 不捕获,直接访问
  • 2、非全局变量,对捕获的内容进行拷贝
    ===》捕获的指针,对指针进行拷贝,可以修改指针所致 的内容,不能改变指针指向(---静态变量传递给Block是内存地址值---) ----- 传入可变数组,可以改变数组内容,不能对数组重指向。
    ===》捕获的值,对值进行拷贝,不能修改这个值 ----- 传入 int a,内部只能使用,不能修改
  • 3、_block 修饰的变量。生成了一个 block_byref 的结构体 用来保存原有变量的指针 和 值
    传递给block的是一个指针。
    block 捕获的时机: 并不是在执行block的时候。

以下代码是用来说明 捕获时机的

    NSMutableArray *arr = [NSMutableArray new];             // 1
//    __block NSMutableArray *arr = [NSMutableArray new];   // 1
    void(^block)(void) = ^{
        [arr addObject:@"1"];                               // 2
    };
    arr = [NSMutableArray new];                             // 3
    block();
    NSLog(@"arr = %@",arr);                                 // 4

// 无__block修饰的情况
在2位置, arr的地址:2 捕获了 1 的地址    也就是说  外部修改了arr的地址,内部捕获到的地址并没有随之改变

// 有__block修饰的情况
2 先捕获了1 的地址, 执行 3的时候, 2的地址随之改变。 2 其实是对 3的arr进行操作。   4打印的是 3的地址。

block 堆/栈 转移过程

汇编分析

  • 无外部变量引用的block

    void(^block)(void) = ^{      // ⬅️ 断点在这
    };
      block();
    

    到了断点之后,下符号断点_Block_copy

    libsystem_blocks.dylib`_Block_copy:
    ->  0x1867f48c0 <+0>:   stp    x22, x21, [sp, #-0x30]!            // 步骤一: 断点在这行 
        0x1867f48c4 <+4>:   stp    x20, x19, [sp, #0x10]
    ........
        0x1867f49a4 <+228>: ldp    x22, x21, [sp], #0x30
        0x1867f49a8 <+232>: ret                                      // 步骤二: 断点在这行 
    

    步骤一:此时读寄存器x0 register read x0po打印
    步骤二:断点在步骤二的位置,在进行一次打印

    // 步骤一 打印结果
    (lldb) register read x0
          x0 = 0x00000001002e8080  
    (lldb) po 0x00000001002e8080
    <__NSGlobalBlock__: 0x1002e8080>
    
    // 步骤二  打印结果   和步骤一  一致
    (lldb) register read x0
          x0 = 0x00000001002e8080 
    (lldb) po 0x00000001002e8080
    <__NSGlobalBlock__: 0x1002e8080>
    

    最后结果都是GlobalBLock

  • 有外部变量引用的情况
    换一个有引用外部变量的block在进行上述操作

    int a = 10;
      void(^block)(void) = ^{
          NSLog(@"%d",a);
      };
      block();
    

    寄存器打印结果

    // 步骤一
    (lldb) register read x0
          x0 = 0x000000016f75bdc8
    (lldb) po 0x000000016f75bdc8
    <__NSStackBlock__: 0x16f75bdc8>
    
    // 步骤二
    (lldb) register read x0
          x0 = 0x0000000283a3c630
    (lldb) po 0x0000000283a3c630
    <__NSMallocBlock__: 0x283a3c630>
    

    打印结果 从 StackBlock 变成了 MallocBlock

源码分析

源码 libclosure-74 中来找相关信息
先看block定义的结构

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE          // 依赖于   BLOCK_HAS_COPY_DISPOSE  (有BLOCK_HAS_COPY_DISPOSE 才会有这些 信息)
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE    // 依赖于   BLOCK_HAS_SIGNATURE  (有BLOCK_HAS_SIGNATURE 才会有这些 信息)
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    BlockInvokeFunction invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

基本结构为Block_layout 有没有Block_descriptor_2、Block_descriptor_3,取决于 flags。
看flags结构

// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime                             正在释放,释放标记,一般常用BLOCK_NEEDS_FREE 做 位与 操作 一同传入flags 告知该block可释放
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime                             存储引用计数的值,是一个可选用参数
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime                             是否有效的标志,程序根据他来决定是否增加或减少引用计数的值
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler                            是否拥有拷贝辅助函数(a copy helper function),决定Block_descriptor_2
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code     是否拥有block C++的析构函数
    BLOCK_IS_GC =             (1 << 27), // runtime                             标志是否有垃圾回收  ---- OS X
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler                            标志是否是全局block
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE         与BLOCK_HAS_SIGNATURE相对,判断当前block是否有签名 用于runtime时动态调用
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler                            是否有签名
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler                            是否有扩展  决定Block_descriptor_3
    
    /**         block 捕获外部变量的类型
     BLOCK_FIELD_IS_OBJECT  = 3, 对象
     BLOCK_FIELD_IS_BLOCK   = 7, 是一个block变量
     BLOCK_FIELD_IS_BYREF   = 8, __block 修饰的结构体
     BLOCK_FIELD_IS_WEAK    = 16,  __weak 修饰的变量
     BLOCK_BYREF_CALLER     = 128 处理Block_byref 内部对象内存的时候会加一个往外的标记,配合上面的枚举提起使用
     */
};

在源码中看 block_copy 做了那些事情

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {                            // 如果内存需要自己管理   那么引用计数相应增加    最开始是0
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {          //  如果是全局  不做任何操作
        return aBlock;
    }
    else {                          //     如果是栈block  进行一次拷贝
        // Its a stack block.  Make a copy.
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

在Block_copy 中 主要有3个判断 ( 操作的内容 ,后面加了备注 ) ,其中重点在else 里面

做了2件事:

  • 1、 malloc 申请内存(堆区)struct Block_layout *result = (struct Block_layout *)malloc(aBlock->descriptor->size);
  • 2、将原来的block 进行memmove (从栈区 移动到堆区) 并将结构的相关信息进行更新
    result->flags |= BLOCK_NEEDS_FREE | 2; // 标记为 needs__free 并逻辑计数器为1
    result->isa = _NSConcreteMallocBlock; // 从栈block 编程了 堆block

block 签名

在 方法中有签名 =====iOS 底层--Class探索和方法执行过程 ---> 3、方法 中有关于方法签名的相关内容
在block源码中也有签名信息, - 其签名 信息在BLOCK_DESCRIPTOR_3中,依赖于 flags 中的BLOCK_HAS_SIGNATURE。 直接lldb打印比较麻烦,需要计算地址偏移。
这篇文章中找到简便方法---- 借助 aspects
aspects中 签名信息的方法是私有,稍作修改,去掉staic 并在。h中进行申明,这样才可以外部访问。

NSString *getMethodSignatureTypeEncoding(NSMethodSignature *methodSignature){
    NSMutableString *str = @"".mutableCopy;
    const char *rtvType = methodSignature.methodReturnType;
    [str appendString:[NSString stringWithUTF8String:rtvType]];
    
    for (int i = 0; i < methodSignature.numberOfArguments; i ++) {
        const char *type = [methodSignature getArgumentTypeAtIndex:i];
        [str appendString:[NSString stringWithUTF8String:type]];
    }
    return [str copy];
}

void task() {
    id globalBlock1 = ^(NSString *str ,NSInteger a,NSArray *arr){
        NSLog(@"globalBlock1 : name = %@",str);
    };
    
    NSMethodSignature *signature1 = aspect_blockMethodSignature(globalBlock1, NULL);
    NSLog(@"%@",getMethodSignatureTypeEncoding(signature1));
}
// 打印结果  v@?@"NSString"q@"NSArray"

block 签名信息
v@?@"NSString"q@"NSArray" -- 中间省略了参数占位长度
v - 返回值类型 void - 无
@? - block 的签名
@"NSString" 第一个参数 NSString 类型 -- 表示 是一个NSString 对象
q 第二个参数 NSInteger
@"NSArray" 第三 个参数 NSArray 类型 -- 表示 是一个NSArray 对象

回顾方法签名信息
v 返回值
@ 第一个参数 一个对象
: 表示 方法(SEL)

block和方法签名有点区别
方法签名 一般不指明是什么类型 只表示是一个对象 比如 v@:
block的签名 参数中 不仅指明是一个对象,还指出对象是什么类型 比如 @"NSString"

block的三层拷贝

_Block_copy (第一层拷贝)

这个在上面栈/堆转移过程已经说过了

block_assign (第二层拷贝)

在上面转换的cpp文件中 还有一个方法需要注意
__main_block_copy_0
这里调用到 _Block_object_assign ,我们在源码中看看究竟干了什么。

void _Block_object_assign(void *destArg, const void *object, const int flags) {
  const void **dest = (const void **)destArg;
  switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {

    case BLOCK_FIELD_IS_OBJECT:
      _Block_retain_object(object);
      *dest = object;
      break;

    case BLOCK_FIELD_IS_BLOCK:
      *dest = _Block_copy(object);
      break;
  
    case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
    case BLOCK_FIELD_IS_BYREF:
      *dest = _Block_byref_copy(object);
      break;
      
    case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
    case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      *dest = object;
      break;

    case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
    case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
      *dest = object;
      break;

    default:
      break;
  }
}

根据捕获的对对象类型分别做不同的操作。其中关键的一个 BLOCK_FIELD_IS_BYREF 类型---- __block修饰的类型

_Block_byref_copy(object);

static struct Block_byref *_Block_byref_copy1(const void *arg) {
  struct Block_byref *src = (struct Block_byref *)arg;

  if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
      // src points to stack
      struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
      copy->isa = NULL;
      // byref value 4 is logical refcount of 2: one for caller, one for stack
      copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
      copy->forwarding = copy; // patch heap copy to point to itself
      src->forwarding = copy;  // patch stack to point to heap copy
      copy->size = src->size;

      if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
          ........
      }
      else {
          // Bitwise copy.
          // This copy includes Block_byref_3, if any.
          memmove(copy+1, src+1, src->size - sizeof(*src));
      }
  }
  ........
  return src->forwarding;
}

关键部分

  • 1、在堆区申请一块和原来block_byref 相同大小的空间 (struct Block_byref *)malloc(src->size);
  • 2、堆区的指针指向 copy copy->forwarding = copy;
  • 3、原来的 block_byref(栈区) 也指向copy src->forwarding = copy;
    所以 __block 修饰的变量才有修改的能力。

__Block_byref_id_object_copy_131 (第三层拷贝)

这个拷贝是对__block修饰的对象进行拷贝。拷贝到 Block_byref_2 -> BlockByrefKeepFunction byref_keep

最终是对这个对象进行了 _Block_object_assign 操作。

struct __Block_byref_str_0 {
  void *__isa;
__Block_byref_str_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *str;                    // ⬅️
};

也就是对 结构体中的 NSString *str 进行memmove(拷贝操作)。

__main_block_dispose_0 释放

void _Block_object_dispose(const void *object, const int flags) {
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK:
        _Block_release(object);
        break;
      case BLOCK_FIELD_IS_OBJECT:
        _Block_release_object(object);
        break;
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        break;
      default:
        break;
    }
}

结构很清晰,就是对不同拷贝的类型 进行release操作。

Block总结

  • 1、 __block 做了什么
    在编译时,对__block修饰的对象转换成一个 block_byref 的结构体,这个结构体里面保存了捕获对象的指针和值
  • 2、为什么__block 修饰的对象具有修改的能力?
    简单点说:__block修饰的对象被从栈区拷贝到堆区。堆区是可以有程序员自己去控制、修改。
    具体的过程为: block进行了三次memmove(copy),
    1、block 本身的copy,将 block本身拷贝的堆区 _block_copy.
    2、捕获的对象的结构体进行memmove(copy),在这个过程中,在堆区申请内存,然后原来栈区的block_byref 指向这块内存,其本身也指向这块内存,都指向同一块内存空间,所以就有了值/地址修改的能力。
    3、保存的指针进行memmove(copy),同时是按照步骤二进行操作。目的是使对象可以进行值的修改。

GCD的Block 是否需要weak?

有些需要,有些不需要

  • 不需要:
    调度组(dispatch_group_async / dispatch_group_notify)
    栅栏(dispatch_barrier_async / dispatch_barrier_sync)
    dispatch_async
    dispatch_sync
    dispatch_after

同步和异步的是有区别的:
异步是将任务进行包装,在包装的过程中,进行(copy)、引用(invoke)、释放(call_and_release)。
同步是任务不进行copy,对于调用者, 只是 “borrows”(借用),而不是对调用者进行持有。

  • 需要:
    dispatch_source_timer(计时器)。

源码分析

  • dispatch_async分析

    dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
    {
    dispatch_continuation_t dc = _dispatch_continuation_alloc();
    uintptr_t dc_flags = DC_FLAG_CONSUME;
    dispatch_qos_t qos;
    
    qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
    _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
    }
    
    ------------------------------
    ❌关键在这个方法  _dispatch_continuation_init❌
    DISPATCH_ALWAYS_INLINE
    static inline dispatch_qos_t
    _dispatch_continuation_init(dispatch_continuation_t dc,
        dispatch_queue_class_t dqu, dispatch_block_t work,
        dispatch_block_flags_t flags, uintptr_t dc_flags)
    {
    void *ctxt = _dispatch_Block_copy(work);                    // 对任务进行copy
    
    dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
    if (unlikely(_dispatch_block_has_private_data(work))) {
        dc->dc_flags = dc_flags;
        dc->dc_ctxt = ctxt;
        return _dispatch_continuation_init_slow(dc, dqu, flags);
    }
    
    dispatch_function_t func = _dispatch_Block_invoke(work)     // 引用这个 任务
    if (dc_flags & DC_FLAG_CONSUME) {                   
        func = _dispatch_call_block_and_release;                // 释放
    }
    return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
    }
    
    ------------------------------
    void
    _dispatch_call_block_and_release(void *block)
    {
    void (^b)(void) = block;
    b();
    Block_release(b);
    }
    

    源码分析 async 调用步骤:
    1、对block 进行 copy _dispatch_Block_copy(work);
    2、对任务进行引用(invoke) _dispatch_Block_invoke(work)
    3、释放这个任务 _dispatch_call_block_and_release
    4、将block包装成 dispatch_continuation_t

    释放的条件分析
    dc_flags 入参 //DC_FLAG_CONSUME = 0x004
    dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
    ====> 拆解: dc_flags = 0x004 | (0x010 | 0x100); = 1001 0100 (276)
    dc_flags & DC_FLAG_CONSUME
    ====> 拆解: 1001 0100 & 0000 0100 = 0x004
    条件成立,会调用 _dispatch_call_block_and_release

  • dispatch_after 分析
    同样通过源码

    static inline void
    _dispatch_after(dispatch_time_t when, dispatch_queue_t dq,
          void *ctxt, void *handler, bool block)
    {
      ....
    
      dispatch_continuation_t dc = _dispatch_continuation_alloc();
      if (block) {
          _dispatch_continuation_init(dc, dq, handler, 0, 0);
      } else {
          _dispatch_continuation_init_f(dc, dq, ctxt, handler, 0, 0);
      }
      .....
    }
    

    源码分析:
    又回到了 _dispatch_continuation_init 的调用,还是和上面一样,不过传入的参数 dc_flags 参数是0 ,调用 _dispatch_call_block_and_release 条件不成立,这只能说明:此时的block是不能被释放的,但是延时执行是在指定时间后,将block添加的相应的线程去执行。所以:dispatch_after 的block 不是当时释放,而是指定时间后再去释放。(这个地方源码没有分析出来)

    通过API说明 能找到相应信息:
    The block to submit. This function performs a Block_copy and Block_release on behalf of the caller.

  • dispatch_sync 分析

    static inline void
    _dispatch_sync_f_inline(dispatch_queue_t dq, void *ctxt,
          dispatch_function_t func, uintptr_t dc_flags)
    {
      if (likely(dq->dq_width == 1)) {
          return _dispatch_barrier_sync_f(dq, ctxt, func, dc_flags);
      }
      ....
      _dispatch_introspection_sync_begin(dl);
      _dispatch_sync_invoke_and_complete(dl, ctxt, func DISPATCH_TRACE_ARG(
              _dispatch_trace_item_sync_push_pop(dq, ctxt, func, dc_flags)));
    }
    

    源码全部看下来,没有定义block进行copy的操作。

    在官方文档中看到这样一段描述:

    Unlike with `dispatch_async`, no retain is performed on the target queue. 
    Because calls to this function are synchronous, it "borrows" the reference of the caller.
    Moreover, no `Block_copy` is performed on the block.
    
    大概意思就是:
    与dispatch_async不同,不在目标队列上执行retain。因为对这个函数的调用是同步的,所以它“借用”了调用者的引用。此外,不在该块上执行块复制。
    

    因为block 不对调用者进行持有,它只是“借用”,所以不会造成循环引用。

  • dispatch_barrier_async(栅栏) 分析

    void
    dispatch_barrier_async(dispatch_queue_t dq, dispatch_block_t work)
    {
      dispatch_continuation_t dc = _dispatch_continuation_alloc();
      uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
      dispatch_qos_t qos;
    
      qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
      _dispatch_continuation_async(dq, dc, qos, dc_flags);
    }
    

    dispatch_barrier_async 类似于 dispatch_async , 将block包装起来。
    dc_flags = DC_FLAG_CONSUME | DC_FLAG_BARRIER;
    条件成立,会调用 _dispatch_call_block_and_release

  • dispatch_group (调度组) 分析
    调度组分2个:组里面的每个小任务 和 组任务完成之后的notify
    void
    dispatch_group_async_f(dispatch_group_t dg, dispatch_queue_t dq, void *ctxt,
          dispatch_function_t func)
    {
      dispatch_continuation_t dc = _dispatch_continuation_alloc();
      uintptr_t dc_flags = DC_FLAG_CONSUME | DC_FLAG_GROUP_ASYNC;
      dispatch_qos_t qos;
    
      qos = _dispatch_continuation_init_f(dc, dq, ctxt, func, 0, dc_flags);
      _dispatch_continuation_group_async(dg, dq, dc, qos);
    }
    
    void
    dispatch_group_notify(dispatch_group_t dg, dispatch_queue_t dq,
          dispatch_block_t db)
    {
      dispatch_continuation_t dsn = _dispatch_continuation_alloc();
      _dispatch_continuation_init(dsn, dq, db, 0, DC_FLAG_CONSUME);
      _dispatch_group_notify(dg, dq, dsn);
    }
    

    dispatch_group_async 和 dispatch_group_notify 都是回归到 _dispatch_continuation_init
    根据传入的参数 dc_flags
    最后的结果都是回归调用 _dispatch_call_block_and_release

block 变量捕获 补充 2020.09.19
- (void)test {
    void (^block)(void) = ^{
        NSLog(@"self = %@",self);
    };
    block();
}

思考:这个block中的self 是否会被捕获?

答案: 会。 因为在test方法中,默认是有2个参数:(self、_cmd), 所以在这里的self 其实是一个临时变量。(具体编译后的结构 可通过clang命令验证)。

那再看这段代码:

@property (nonatomic, copy) NSString *name;   /// 类的一个属性

- (void)test {
    void (^block)(void) = ^{
        NSLog(@"name = %@",_name);
    };
    block();
}

在这段代码中, _name 不会被捕获,但是 self会被捕获。 使用 _name 实际上是 self->_name; 所以self被捕获 了。

相关文章

网友评论

      本文标题:iOS底层-- block

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