iOS底层系列21 -- Block

  • Block又称为代码块,匿名函数,函数指针,下面来详细介绍Block的相关内容;

Block的声明定义

【第一种:在property属性中定义block】
  • 定义的是具有特定参数与返回值类型的block变量,属于定义block变量;
@interface ViewController ()

//定义了一个block变量testOne,类型为void(^)(void)
@property(nonatomic,copy)void(^testOne)(void);
//定义了一个block变量testTwo,类型为void(^)(int)
@property(nonatomic,copy)void(^testTwo)(int a);
//定义了一个block变量testThree,类型为NSString *(^)(int,int)
@property(nonatomic,copy)NSString *(^testThree)(int a,int b);

@end
【第二种:使用typedef定义block类型】
  • 定义的是具有特定参数与返回值类型的block,属于定义block类型;
//定义了一个YYBlock类型
typedef void(^YYBlock)(void);
//定义了一个XXBlock类型
typedef void(^XXBlock)(int a);
//定义了一个ZZBlock类型
typedef NSString *(^ZZBlock)(int a,int b);
【第三种:在函数内部定义局部block变量并初始化其代码块的实现】
  • 定义的是block变量且初始化了代码块;
- (void)viewDidLoad {
    [super viewDidLoad];
    
    //定义了一个block变量add,类型为void(^)(void)
    void(^add)(void) = ^{
        NSLog(@"add");
    };
    
    //定义了一个block变量minus,类型为void(^)(int)
    void(^minus)(int) = ^(int a){
        NSLog(@"%d",a);
    };
    
    //定义了一个block变量multiply,类型为int(^)(int,int)
    int(^multiply)(int,int) = ^(int a,int b){
        int c = a * b;
        NSLog(@"%d",c);
        return c;
    };
    //block代码块的调用
    add();
    minus(10);
    multiply(10,5);
}
  • 等号左边是block变量名+block类型;
  • 等号右边是block代码块初始化实现;
  • 等号左边在定义block类型时,形参的参数名可以省略,只保留形参的类型,从这里可以看出block与函数指针十分相似;
【第四种:block作为函数方法的参数】
  • 这里引用AFNetworking中发送网络请求的函数定义;
- (NSURLSessionDataTask *)GET:(NSString *)URLString
                   parameters:(id)parameters
                      success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
                      failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;
  • successfailure是block变量名,其作为函数参数;success的block类型为
    (void (^)(NSURLSessionDataTask *task, id responseObject))
  • 外界业务层调用此网络层函数,业务层会初始化block变量success的代码块,网络层在发送网络请求获取网络数据之后,会调用success代码块,并传入task与responseObject两个参数;
  • block作为函数参数时,block中的参数名通常要写上,便于理解参数的含义;

Block类型

  • block通常有三种类型,分别如下:

    • 【第一种:__NSGlobalBlock__】:全局区block,存储在全局区;
    • 【第二种:__NSMallocBlock__】:堆区block,因为block既是函数,也是对象;
    • 【第三种:__NSStackBlock__】:栈区block,存储在栈区;
  • 新建C语言工程,配置成MRC环境:设置target --> Build Setting --> 搜索gar --> Objective-C Automatic Reference Counting - NO,LLDB调试如下:

Snip20210317_162.png
Snip20210317_163.png
Snip20210317_164.png
  • 在MRC环境下的总结如下:
    • 全局block变量 访问外界变量 NSGlobalBlock
    • 全局block变量 未访问外界变量 NSGlobalBlock
    • 局部block变量 未访问外界变量 NSGlobalBlock
    • 局部block变量 访问外界局部变量 NSStackBlock
    • 局部block变量 访问外界全局变量 NSGlobalBlock
  • 将C语言工程设置成ARC环境,LLDB调试如下:
Snip20210317_165.png
Snip20210317_166.png
Snip20210317_168.png
  • 在ARC环境下的总结如下:

    • 全局block变量 访问外界变量 NSGlobalBlock
    • 全局block变量 未访问外界变量 NSGlobalBlock
    • 局部block变量 未访问外界变量 NSGlobalBlock
    • 局部block变量 访问外界局部变量 NSMallocBlock
    • 局部block变量 访问外界全局变量 NSGlobalBlock
  • 比较MRC与ARC两种环境,可以看出当局部 block变量在访问外界局部变量时block变量会从栈区拷贝(copy)到堆区__NSStackBlock__ --> __NSMallocBlock__

  • 我们知道一个引用在默认情况下是强引用即strong,若在ARC环境下用弱引用weak修饰block变量时,其访问外界局部变量,依然是栈区block,并没有拷贝到堆区;

Snip20210317_170.png
  • 最终总结针对ARC环境,局部block变量(最常见):

    • 创建的block为空实现时,默认存储在全局区,即全局区block;
    • 如果block访问外界局部变量时:
      • 如果此时的block是强引用(默认强引用),则block存储在堆区,即堆区block;
      • 如果此时的block通过__weak修饰即弱引用,则block存储在栈区,即栈区block;
  • 现看一案例代码,在MRC情况下:

#import <Foundation/Foundation.h>

void(^block)(void);

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

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}
  • 在test函数中定义全局的block变量,且block访问了外界局部变量a,由于是在MRC环境下,所以此block是在栈区分配内存;
  • 当test函数执行完,block就会被回收,当下面再调用block时,会出现数据错乱的问题;
  • 若在ARC环境下,系统默认会将访问了外界局部变量的block从栈区拷贝到堆区,那么再调用block时,就不会出现数据错乱的问题;

Block的copy操作

  • 不同类型block的copy操作如下:
Snip20210703_38.png
  • 在ARC环境下,系统会对MRC环境下的栈区block做默认的copy操作;
  • 在ARC环境下,用强指针指向栈区block,会将其copy到堆区;
  • 在ARC环境下,栈区block作为返回值时,会将其copy到堆区;
  • 在ARC环境下,栈区block作为方法名含有usingBlock的方法参数时,会将其copy到堆区;
  • 在ARC环境下,栈区block作为GCD函数的参数时,会将其copy到堆区;

Block循环引用

  • 废话不多说先上案例如下所示:
Snip20210317_171.png
  • 第一个例子:控制器NextVC持有block,对block强引用,block内部出现self,即对NextVC强引用,NextVC与block彼此之间强引用,形成强引用环即循环引用,两者都不能释放,造成内存泄漏;

  • 第二个例子:animation的block对self(NextVC)强引用,只是单方面的强引用,没有形成循环引用,不会造成内存泄漏;

  • 解决循环引用的常见方案有以下几种方式:

    • 【方式一】使用弱引用__weak修饰符;
    • 【方式二】__block修饰对象(需要注意的是在block内部需要置空对象,而且block必须调用);
    • 【方式三】传递对象self作为block的参数,提供给block内部使用;
    • 【方式四】使用NSProxy;
  • 下面来详细介绍着几种方式:

方式一:使用弱引用__weak修饰符
  • 当block内部为嵌套block时,直接使用__weak修饰符即可;
Snip20210317_174.png
  • self与weakSelf指向的是同一块内存,即两者是同一个对象;
  • block对weakSelf不会强引用,从而打破了强引用环,不会造成内存泄漏;
  • 当block内部嵌套block,需要同时使用__weak 和 __strong;
Snip20210317_175.png
  • self.block内部一定不能出现self,出现就会对self强引用,造成循环引用;
  • strongSelf是一个临时变量,在self.block的作用域内,即内部block执行完就释放strongSelf;
方式二:__block修饰变量
Snip20210317_176.png
  • 注意的是这里的block必须调用,如果不调用block,vc就不会置空,那么依旧是循环引用,self和block都不会被释放;
方式三:传递对象self作为block的参数
Snip20210317_177.png
  • 将对象self作为block参数,提供给block内部使用,不会有引用计数问题;
方式四:使用NSProxy(这里只做简单介绍)
  • 首先来介绍一下NSProxy类;
    • NSProxy 和 NSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议;
    • NSProxy 其实是一个消息重定向封装的一个抽象类,类似一个代理人,中间件,可以通过继承它,并重写下面两个方法来实现消息转发到另一个实例;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel
NSProxy的使用场景
  • 实现多继承功能;

  • 解决了NSTimer&CADisplayLink创建时对self强引用问题,参考YYKit的YYWeakProxy;

  • 下面通过代码实例来实现上述的两种场景:

  • 定义三个类YYProxyYYStudentYYTeacher代码实现如下:

#import <Foundation/Foundation.h>

@interface YYProxy : NSProxy

- (id)transformObjc:(NSObject *)objc;

+ (instancetype)proxyWithObjc:(id)objc;

@end
#import "YYProxy.h"

@interface YYProxy ()

@property(nonatomic,weak,readonly)NSObject *objc;

@end

@implementation YYProxy

+ (instancetype)proxyWithObjc:(id)objc{
    return  [[self alloc] transformObjc:objc];
}

- (id)transformObjc:(NSObject *)objc{
   _objc = objc;
    return self;
}

//1.查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    NSMethodSignature *signature;
    if (self.objc) {
        signature = [self.objc methodSignatureForSelector:sel];
    }else{
        signature = [super methodSignatureForSelector:sel];
    }
    return signature;
}

//2.有了方法签名之后就会调用方法实现
- (void)forwardInvocation:(NSInvocation *)invocation{
    SEL sel = [invocation selector];
    if ([self.objc respondsToSelector:sel]) {
        [invocation invokeWithTarget:self.objc];
    }
}

- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.objc respondsToSelector:aSelector];
}

@end
#import <Foundation/Foundation.h>

@interface YYStudent : NSObject

- (void)study;

@end
#import "YYStudent.h"

@implementation YYStudent

- (void)study{
    NSLog(@"%s",__func__);
}

@end
#import <Foundation/Foundation.h>

@interface YYTeacher : NSObject

- (void)edcuate;

@end
#import "YYTeacher.h"

@implementation YYTeacher

- (void)edcuate{
    NSLog(@"%s",__func__);
}

@end
  • 通过YYProxy实现多继承功能;
Snip20210317_178.png
  • YYProxy类获取了YYStudent与YYTeacher类中的功能方法,主要是通过消息的慢速转发实现的,原理在 iOS底层系列14 -- 消息流程的动态方法决议与转发这篇文章中有详细阐述,YYProxy由于没有目标方法实现,将其转发给YYStudent与YYTeacher类;

  • 通过YYProxy解决定时器中self的强引用问题;

Snip20210317_179.png
  • 将定时器的target指定为YYProxy对象,然后YYProxy对象再将定时器的回调消息函数转发给当前控制器;
  • 在当前控制器销毁时,一定要销毁定时器,从而释放YYProxy对象;
Block导致循环引用的总结
  • 循环应用的解决方式从根本上来说就两种,以self -> block -> self为例:
  • 打断self 对 block的强引用,block属性修饰符使用weak,但是这样会导致block还未创建完就释放了,所以从这里打断强引用行不通;
  • 打断block对self的强引用,主要就是self的作用域和block作用域的通讯,通讯有代理、传值、通知、传参等几种方式,用于解决循环,常见的解决方式如下:
    • weak-strong-修饰符;
    • __block(block内对象置空,且调用block);
    • 将对象self作为block的参数;
    • 通过NSProxy的子类代替self;

Block的底层结构

  • 在C语言工程的main.文件中,定义一个block变量,且调用block;
Snip20210317_180.png
  • cd 到指定文件夹 输入 clang -rewrite-objc main.m -o main.cpp即将main.m文件编译成main.cpp文件,底层C++中的block的定义与调用如下所示:
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 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;
  }
};

//block中代码块 封装在当前函数中
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //bound by copy
    int a = __cself->a;
    printf("YY a = %d", a);
}

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 argc, const char * argv[]) {
    /* @autoreleasepool */
    {
        __AtAutoreleasePool __autoreleasepool;

        int a = 10;
        //block的定义
        void(*TestTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
       //block的调用
        ((void (*)(__block_impl *))((__block_impl *)TestTwo)->FuncPtr)((__block_impl *)TestTwo);
    }
    return 0;
}
Block底层结构.png
  • block中定义的代码块在底层是封装在__main_block_func_0C语言函数中;

  • 可以看到block本质是__main_block_impl_0结构体;

  • 定义block时,底层是调用__main_block_impl_0结构体的同名函数__main_block_impl_0(),第一个参数传入是__main_block_func_0函数指针,也就是block中代码块实现,然后将参数的函数指针赋值给成员变量implFuncPtr成员;

  • block的调用,最终也是通过__main_block_impl_0结构体的成员implFuncPtr函数指针来调用block代码块;

  • 由于impl__main_block_impl_0结构体的第一个成员,那么impl的内存地址就是__main_block_impl_0结构体的内存地址,所以在调用block时,可以将__main_block_impl_0结构体强转成impl然后调用FuncPtr函数指针指向的函数,也就是block中的代码块;

  • block捕获外界局部变量时,从底层我们看到在__main_block_impl_0结构体中有一个int类型成员变量a,其就是用来接收捕获的外界变量值的,在__main_block_func_0函数中完成接受赋值操作,也就是说block在捕获外界变量时,在其底层结构体内部会自动生成同名成员变量来保存;

  • 上述捕获的外界局部变量,在block底层结构体中会自动生成一个同名成员变量保存其值,属于值传递,也就是说block内部捕获的变量与外界的变量是两块不同的内存地址

  • 所以在外界更改变量的值是不会影响到block内部捕获到的变量的值的;

  • 其次在block内部是不能更改捕获变量的值的,因为内外变量的内存地址不同;

  • 下面尝试捕获全局变量,静态变量,看看底层结构体如何操作:

Snip20210318_190.png
Snip20210318_193.png
  • block访问全局变量,其底层结构体并没有自动生成同名的成员变量,表明block不会捕获全局变量,至于block代码块中出现全局变量的赋值,是因为全局变量可以在当前文件中如何地方都可访问到;

  • block访问静态局部变量时,其底层结构体会自动生成一个同名指针的成员,属于指针传递,所以在block内部可以更改捕获的外界变量的值;

  • 总结:

    • block能捕获外界局部变量,block底层结构体会自动生成同名的成员变量来接收外界局部变量的值,但在block内部不能修改外界局部变量的值,因为是值传递,内外内存地址不同;
    • block能捕获外界静态局部变量,其底层结构体会自动生成同名的成员变量来接收,且在block内部可以修改外界局部变量的值,因为是地址的传递,内外内存地址相同;
    • block不能捕获外界全局变量,block底层结构体不会自动生成同名的成员变量来接收,但在block内部可以修改全局变量的值,因为全局变量是全局的可以直接访问;
  • 由上面的内容我们知道,在block内部是不能修改局部变量的,但是可以修改静态局部变量,那么如何修改局部变量呢?接下来就要引出__block修饰符;通过__block修饰符我们可以在block内部修改局部变量;

__block修饰符的底层实现
  • 先上代码如下所示:
Snip20210318_194.png
  • 编译之后的底层代码:
struct __Block_byref_m_0 {
  void *__isa;
__Block_byref_m_0 *__forwarding;
 int __flags;
 int __size;
 int m;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_m_0 *m; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_m_0 *_m, int flags=0) : m(_m->__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_m_0 *m = __cself->m;
    (m->__forwarding->m) = 20;
    printf("YY m = %d",(m->__forwarding->m));
}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->m, (void*)src->m, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->m, 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(int argc, const char * argv[]) {
    { __AtAutoreleasePool __autoreleasepool;
        __attribute__((__blocks__(byref))) __Block_byref_m_0 m = {(void*)0,(__Block_byref_m_0 *)&m, 0, sizeof(__Block_byref_m_0), 10};
        void(*TestTwo)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_m_0 *)&m, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)TestTwo)->FuncPtr)((__block_impl *)TestTwo);
    }
    return 0;
}
  • __block修饰的外界局部变量,在底层结构中会生成__Block_byref_m_0结构体,此结构体会保存局部变量的值和指针,且其内部还有一个__forwarding指针指向它自己;
  • 在block内部捕获的不是__block变量的值,而是捕获__block变量的地址,通过地址访问内存修改其值;
  • 打印出__block修饰的局部变量m与内部捕获的变量m的地址,如下所示:
Snip20210318_196.png
  • 可以看出__block修饰的局部变量存储在栈区,而内部捕获到的局部变量存储在堆区;
  • block访问__block修饰局部变量,其底层结构体之间的关系如下:
Snip20210704_40.png
  • __block修饰的局部变量,在底层会被包装成一个__Block_byref_age_0结构体,此结构体有一个__forwarding成员是指向自己的,其分析如下:
Snip20210704_41.png
  • 当block还在栈区时,__forwarding指向的是栈区的__block变量的底层结构体;
  • 当block拷贝到堆区时,栈区的__forwarding指向堆区的__block变量的底层结构体,堆区的__forwarding也是指向__block变量的底层结构体,保证通过__forwarding指针访问的一定是堆区的__block变量;
Snip20210704_42.png
block底层源码实现
  • 在定义block处加上断点,然后进入汇编页面看到如下:
Snip20210318_197.png
  • 在创建block时会调用objc_retainBlock函数;
  • 在工程中配置符号断点 objc_retainBlock,LLDB调试如下:
Snip20210318_198.png
  • 再在工程中配置符号断点 _Block_copy,LLDB调试如下:
Snip20210318_199.png
  • 可以看到底层执行的是libsystem_blocks.dylib中的_Block_copy函数;
  • 到苹果开源网站下载 libclosure-78 源码,全局搜索_Block_copy,结果如下所示:
Snip20210318_200.png
  • 看到Block_layout结构体,它才是block的真正类型,Block_layout结构如下:
// Block 结构体
struct Block_layout {
    //指向表明block类型的类
    void *isa;//8字节
    //用来作标识符的,类似于isa中的位域,按bit位表示一些block的附加信息
    volatile int32_t flags; // contains ref count 4字节
    //保留信息,可以理解预留位置,用于存储block内部变量信息
    int32_t reserved;//4字节
    //函数指针,指向具体的block实现的调用地址
    BlockInvokeFunction invoke;
    //block的附加信息
    struct Block_descriptor_1 *descriptor;
    // imported variables
};
  • isa:表明block是一种class类;
  • flags:标识符,按bit位表示一些block的附加信息,类似于isa中的位域,其中flags的种类有以下几种,主要重点关注BLOCK_HAS_COPY_DISPOSE 和 BLOCK_HAS_SIGNATURE, BLOCK_HAS_COPY_DISPOSE 决定是否有 Block_descriptor_2,BLOCK_HAS_SIGNATURE 决定是否有 Block_descriptor_3;
    • 第1 位 - BLOCK_DEALLOCATING,释放标记,-般常用 BLOCK_NEEDS_FREE 做 位与 操作,一同传入 Flags , 告知该 block 可释放。
    • 第16位 - BLOCK_REFCOUNT_MASK,存储引用计数的值,是一个可选用参数;
    • 第24位 - BLOCK_NEEDS_FREE,低16是否有效的标志,程序根据它来决定是否增加或是减少引用计数位的 值;
    • 第25位 - BLOCK_HAS_COPY_DISPOSE,是否拥有拷贝辅助函数(a copy helper function);
    • 第26位 - BLOCK_IS_GC,是否拥有 block 析构函数;
    • 第27位,标志是否有垃圾回收;//OS X
    • 第28位 - BLOCK_IS_GLOBAL,标志是否是全局block;
    • 第30位 - BLOCK_HAS_SIGNATURE,与 BLOCK_USE_STRET 相对,判断当前 block 是否拥有一个签名。用于 runtime 时动态调用。
// flags 标识
// Values for Block_layout->flags to describe block objects
enum {
    //释放标记,一般常用于BLOCK_BYREF_NEEDS_FREE做位与运算,一同传入flags,告知该block可释放
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    //存储引用引用计数的 值,是一个可选用参数
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    //低16位是否有效的标志,程序根据它来决定是否增加或者减少引用计数位的值
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    //是否拥有拷贝辅助函数,(a copy helper function)决定block_description_2
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    //是否拥有block C++析构函数
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    //标志是否有垃圾回收,OSX
    BLOCK_IS_GC =             (1 << 27), // runtime
    //标志是否是全局block
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    //与BLOCK_HAS_SIGNATURE相对,判断是否当前block拥有一个签名,用于runtime时动态调用
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    //是否有签名
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    //使用有拓展,决定block_description_3
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};
  • reserved:保留信息,可以理解预留位置,猜测是用于存储block内部变量信息;
  • invoke:是一个函数指针,指向block的执行代码;
  • descriptor:block的附加信息,比如保留变量数、block的大小、进行copy或dispose的辅助函数指针,有三类如下所示:
    • Block_descriptor_1是必选的;
    • Block_descriptor_2 和 Block_descriptor_3都是可选的;
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
    uintptr_t reserved;//保留信息
    uintptr_t size;//block大小
};

#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;//拷贝函数指针
    BlockDisposeFunction dispose;
};

#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;//签名
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT 布局
};
  • 以上关于descriptor的可以从其构造函数中体现,其中Block_descriptor_2和Block_descriptor_3都是通过Block_descriptor_1的地址,经过内存平移得到的;
static struct Block_descriptor_1 * _Block_descriptor_1(struct Block_layout *aBlock)
{
    return aBlock->descriptor;//默认打印
}
#endif

// CJL注释:Block 的描述 : copy 和 dispose 函数
static struct Block_descriptor_2 * _Block_descriptor_2(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;//descriptor_1的地址
    desc += sizeof(struct Block_descriptor_1);//通过内存平移获取
    return (struct Block_descriptor_2 *)desc;
}

//Block 的描述 : 签名相关
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
    if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
    uint8_t *desc = (uint8_t *)aBlock->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    return (struct Block_descriptor_3 *)desc;
}
  • 下面来探索block在创建的时候,其存储域的变化过程:
  • 初始化创建一个局部block变量add,内部未访问外界局部变量,在创建处打下断点:
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.button];
    //定义了一个block变量add,类型为void(^)(void)
    __block int m = 10;
    void(^add)(void) = ^{
        
    };
    add();
}
  • 进入汇编界面,执行到objc_retainBlock断点停住,LLDB调试如下:
Snip20210318_202.png
  • 看到此时的block类型为全局block,即__NSGlobalBlock__
  • 若block内部捕获外界局部变量时,代码如下:
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.button];
    //定义了一个block变量add,类型为void(^)(void)
    __block int m = 10;
    void(^add)(void) = ^{
        m = 20;
        NSLog(@" m = %d",m);
    };
    add();
}
  • 进入汇编界面,执行到objc_retainBlock断点停住,LLDB调试如下:
Snip20210318_203.png
  • 看到此时的block类型为栈区block,即__NSStackBlock__
  • 添加一个符号断点_Block_copy,当执行到此函数汇编指令的ret指令时,停住断点,LLDB调试结果如下:
Snip20210318_204.png
  • 可以看到底层源码在调用_Block_copy函数之后,block类型变成了堆区block,即__NSMallocBlock__完成了block从栈区到堆区的拷贝

  • _Block_copy的函数源码解析:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
//这里是核心重点 block的拷贝操作: 栈Block -> 堆Block
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;//强转为Block_layout类型对象,防止对外界造成影响
    if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要释放
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {//如果是全局block,直接返回
        return aBlock;
    }
    else {//为栈block 或者 堆block,由于堆区需要申请内存,所以只可能是栈区
        // Its a stack block.  Make a copy. 它是一个栈区block,执行拷贝
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);//申请空间并接收
        if (!result) return NULL;
        //通过memmove内存拷贝,将 aBlock 拷贝至result
        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;//可以直接调起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;//设置block对象类型为堆区block
        return result;
    }
}
  • 首先判断block是否需要free,需要free就直接释放;

  • 然后判断block是否为全局block,如果是则不需要copy,直接返回;

  • 最后专门时针对栈区block的逻辑操作,将栈区block拷贝到堆区;

    • 通过malloc申请内存空间用于接收block;
    • 通过memmove将block拷贝至新申请的内存中;
    • 设置block对象的类型为堆区block,即result->isa = _NSConcreteMallocBlock
  • _Block_object_assign函数是在底层编译代码中,当block从栈区拷贝到堆区时,外部变量的拷贝(栈区-->堆区)调用的方法就是它

  • 外部变量的类型如下:

// Block 捕获的外界变量的种类
// Runtime support functions used by compiler when generating copy/dispose helpers

// Values for _Block_object_assign() and _Block_object_dispose() parameters
enum {
    // see function implementation for a more complete description of these fields and combinations
    //普通对象,即没有其他的引用类型
    BLOCK_FIELD_IS_OBJECT   =  3,  // id, NSObject, __attribute__((NSObject)), block, ...
    //block类型作为变量
    BLOCK_FIELD_IS_BLOCK    =  7,  // a block variable
    //经过__block修饰的变量
    BLOCK_FIELD_IS_BYREF    =  8,  // the on stack structure holding the __block variable
    //weak 弱引用变量
    BLOCK_FIELD_IS_WEAK     = 16,  // declared __weak, only used in byref copy helpers
    //返回的调用对象 - 处理block_byref内部对象内存会加的一个额外标记,配合flags一起使用
    BLOCK_BYREF_CALLER      = 128, // called from __block (byref) copy/dispose support routines.
};
  • 其中最常用的是BLOCK_FIELD_IS_BYREFBLOCK_FIELD_IS_OBJECT

  • _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:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/

        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/

        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/

        *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:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
        *dest = object;
        break;

      default:
        break;
    }
}
  • 针对普通对象(BLOCK_FIELD_IS_OBJECT),执行_Block_retain_object(),引用计数+1;

  • 针对block变量(BLOCK_FIELD_IS_BLOCK),执行_Block_copy()即拷贝;

  • 针对__block修饰的变量(BLOCK_FIELD_IS_BYREF),执行_Block_byref_copy();

  • _Block_byref_copy的源码实现如下:

static struct Block_byref *_Block_byref_copy(const void *arg) {
    //强转为Block_byref结构体类型,保存一份
    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;
        //block内部持有的Block_byref 和 外界的Block_byref 所持有的对象是同一个,这也是为什么__block修饰的变量具有修改能力
        //copy 和 scr 的地址指针达到了完美的同一份拷贝,目前只有持有能力
        copy->forwarding = copy; // patch heap copy to point to itself
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
        //如果有copy能力
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            //Block_byref_2是结构体,__block修饰的可能是对象,对象通过byref_keep保存,在合适的时机进行调用
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }
            //等价于 __Block_byref_id_object_copy
            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
        latching_incr_int(&src->forwarding->flags);
    }
    return src->forwarding;
}
  • 首先将传入的对象,强转成Block_byref结构体类型对象;

  • 判断传入的对象是否已经拷贝到堆区:

    • 已经拷贝到堆区,处理直接返回;
    • 未拷贝到堆区,首先申请内存,然后进行拷贝,再将copy(堆区)与src(栈区)的forwarding指针都指向堆区的内存地址;
  • 代码调试:

int main(int argc, const char * argv[])
{
    @autoreleasepool{
        __block NSString *a = [NSString stringWithFormat:@"li"];
        void(^TestBlock)(void) = ^{
            NSLog(@" a = %@",a);
        };
        NSLog(@" TestBlock = %@",TestBlock);
    }
    return 0;
}
  • clang编译成C++语言如下:
struct __Block_byref_a_0 {
 void *__isa;
 __Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSString *a;
};

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;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_1,(a->__forwarding->a));
}
    
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 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(int argc, const char * argv[])
{
    /* @autoreleasepool */{ __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 33554432, sizeof(__Block_byref_a_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSString * _Nonnull (*)(id, SEL, NSString * _Nonnull, ...))(void *)objc_msgSend)((id)objc_getClass("NSString"), sel_registerName("stringWithFormat:"), (NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_0)};
        void(*TestBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_c5_l8bnxw0d2w92f4439t_r8qjc0000gn_T_main_5b35fd_mii_2,TestBlock);
    }
    return 0;
}
  • 看到__block修饰OC对象时,__Block_byref结构体会多生成出两个函数分别为__Block_byref_id_object_copy__Block_byref_id_object_dispose

  • 调试底层源码libclosure,首先底层会执行_Block_copy函数,实现将栈区block拷贝到堆区;

Snip20210321_12.png
  • 当执行到_Block_call_copy_helper函数时,接下来会调用_Block_object_assign函数,传入的参数是__block修饰的局部变量被封装成Block_byref结构体对象;
Snip20210321_14.png
  • 接下来进入_Block_byref_copy函数内部,实现将栈区的Block_byref结构体对象拷贝到堆区;
Snip20210321_15.png
  • 上面已经提到如果__block修饰的是OC对象,在__Block_byref结构体会多生成出两个函数分别为__Block_byref_id_object_copy__Block_byref_id_object_dispose
//block自身拷贝(_Block_copy) 
// __block bref结构体拷贝(_Block_object_assign) //_Block_object_assign中对外部变量(存储在bref)拷贝一份到内存
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
  • if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) 这个判断条件就与是否生成COPY_DISPOSE两个函数有关,如果生成了就会执行这两个函数的相关逻辑;
  • 从COPY_DISPOSE两个函数实现我们看到,dst是__Block_byref结构体,根据结构体成员布局(dst + 40)就是外界局部变量a;所以COPY_DISPOSE这两个函数执行的是外界局部变量a的拷贝与释放;
总结:

__block修饰OC对象存在三层copy:

  • block对象的copy,调用_Block_copy函数实现将栈区block拷贝到堆区;
  • 外界局部变量生成C++结构体Block_byref对象的copy,调用_Block_object_assign-->_Block_byref_copy实现将Block_byref结构体对象从栈区拷贝到堆区;
  • 只针对OC对象类,即在结构体Block_byref中生成COPY_DISPOSE函数,将外界局部变量从栈区拷贝到堆区;
_Block_object_dispose
  • 同一般的retain和release一样,_Block_object_assign其本质主要是retain,所以对应的还有一个release,即_Block_object_dispose方法,其源码实现如下,也是通过区分block种类,进行不同释放操作;
// When Blocks or Block_byrefs hold objects their destroy helper routines call this entry point
// to help dispose of the contents 当Blocks或Block_byrefs持有对象时,其销毁助手例程将调用此入口点以帮助处置内容
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://__block修饰的变量,即bref类型的
        // get rid of the __block data structure held in a Block
        _Block_byref_release(object);
        break;
      case BLOCK_FIELD_IS_BLOCK://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;
    }
}
  • 进入_Block_byref_release源码,主要就是对象、变量的释放销毁;
static void _Block_byref_release(const void *arg) {
    //对象强转为Block_byref类型结构体
    struct Block_byref *byref = (struct Block_byref *)arg;

    // dereference the forwarding pointer since the compiler isn't doing this anymore (ever?)
    byref = byref->forwarding;//取消指针引用
    
    if (byref->flags & BLOCK_BYREF_NEEDS_FREE) {
        int32_t refcount = byref->flags & BLOCK_REFCOUNT_MASK;
        os_assert(refcount);
        if (latching_decr_int_should_deallocate(&byref->flags)) {
            if (byref->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {//是否有拷贝辅助函数
                struct Block_byref_2 *byref2 = (struct Block_byref_2 *)(byref+1);
                (*byref2->byref_destroy)(byref);//销毁拷贝对象
            }
            free(byref);//释放
        }
    }
}

Block的内存管理

  • 栈区block,访问外界局部变量,不会对局部变量产生强引用;
  • 堆区block,会调用block内部的copy函数,copy函数内部会调用_Block_object_assign函数,此函数会根据局部变量的修饰符(__strong,__weak,__unsafe_unretained)做出相应的操作,
  • 堆区block从堆区移除时,会调用block内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数,其会自动释放引用的局部变量,相当于release局部变量;
Snip20210704_39.png
  • 栈区block拷贝到堆区时,__block修饰的变量也会从栈区复制到堆区,并且堆区block会对__block变量进行引用;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容