OC底层探索25-深入浅出Block

block几乎天天都在使用,也是面试题高发区。可是原理还是有点晦涩的,现在就静下心来听我慢慢道来!

1、循环引用的解决

1.1 循环引用的造成

  1. 正常情况:


  2. 循环引用:


  • 两个对象相互引用,导致两个对象即使使用完成后也不能正常进行dealloc,这就是循环引用
  1. Block的循环引用:
self.block = ^{
    [self sayHello];
};
  • Block是极其容易造成循环引用的,例如下方这段代码。self和block相关引用

1.2 循环引用的解决

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

  1. __weak + __strong + dance,利用中介者模式;
  2. __block修饰引用对象,同样是利用中介者模式但是需要手动释放引用对象;
  3. self作为参数传入
  4. 使用NSProxy;
1.2.1 __weak + __strong + dance
@property(nonatomic, copy) void(^block)(void);

__weak typeof(self) weakSelf = self;
self.block = ^(void){
    __strong  typeof(weakSelf) strongSelf = weakSelf;
     NSLog(@"%@",strongSelf.name);
};
1.2.2 __block修饰引用对象
@property(nonatomic, copy) void(^block)(void);

__block UIViewController *vc = self;
self.block = ^(void){
    NSLog(@"%@",vc.name);
    vc = nil;   //手动释放
};
  • 在Block中对引用参数进行修改必须使用__block修饰,第四部分会分析;
  • 需要注意的是:block必须调用,不调用依旧会造成循环引用;
1.2.3 self作为参数传入
@property(nonatomic, copy) void(^block)(UIViewController *);

self.block = ^(UIViewController *vc){
    NSLog(@"%@",vc.name);
};

//调用
self.block(self);
  • 这种写法在ReactCocoa中还挺常见的;
1.2.4 NSProxy 模板类

NSProxy 和 NSObject是同级的一个类,也可以说是一个虚拟类,只是实现了NSObject的协议;

@interface HRTestProxy : NSProxy
@property (nonatomic, strong, nullable) NSObject *target;
@end

@implementation HRTestProxy
// NSProxy实现逻辑和方法慢速转发一致,通过模板类对详细进行转发
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end
-(void)demo{
    HRTestProxy *myProxy = [HRTestProxy alloc];
    myProxy.target = self;
    self.block = ^(void){
        [myProxy performSelector:@selector(say)];
    };
    //调用
    self.block();
}

-(void)say{
    NSLog(@"%@",self.name);
}
  • 通过利用虚基类NSProxy的特性将self -> block -> self,改为self -> block -> NSProxy的结构,从而完成循环引用的打破;

2、Block结构分析

2.1 block最基本形态

分析结构一定是离不开clang,

clang命令:
clang -rewrite-objc main.m -o main.cpp

void (^mallocBlock)(void) = ^void {
    NSLog(@"HR_Block");
};

将上方代码进行编译;


  • block在编译之后是一个结构体/对象isa:_NSConcreteStackBlockobjc_object是一致的;
  • 初始化:block结构体的构造函数:&__main_block_impl_0(方法, &参数),将isa、大小参数、执行函数等信息赋值;
  • 调用:从结构体__main_block_impl_0里获取FuncPtr函数指针,也就是__main_block_func_0(block自己),并将block自己传入;这个位置可以理解为OC方法中的隐藏参数: (id self,Sel cmd)
2.1 补充 block三种类型

__NSGlobalBlock__全局block,存储在全局区

void(^block)(void) = ^{
    NSLog(@"HR");
};
  • 此时的block无参也无返回值,属于全局block

__NSMallocBlock__堆区block

int a = 10;
void(^block)(void) = ^{
    NSLog(@"HR - %d", a);
};
  • 此时的block会访问外界变量,即底层拷贝a,所以是堆区block

__NSStackBlock__栈区block

int a = 10;
void(^__weak block)(void) = ^{
    NSLog(@"HR - %d", a);
};
  • 通过__weak不进行强持有,block就还是栈区block;

2.2 捕获外界变量(值拷贝)

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

mallocBlock();
  • block内部使用到了外部变量之后,会导致block结构体有什么变化呢?;


  • 初始化:发现__main_block_impl_0结构体内多了一个int a,也就是说block会把捕获的外界变量进行copy到自身结构里,当然这些操作都是编译器帮我们完成了;

  • 调用没有变化,只是在体内多了一个a的取值.通过观察在__main_block_func_0里参数int a = __cself->a;进行了值拷贝

2.1.1 值拷贝验证
  • block外部a的地址;
  • block内部的a的地址,两个地址完全是不同.验证了之前值拷贝的说法;

2.3 __block声明变量(指针拷贝

在2.2中出现了值拷贝,而且在block中也不允许使用修改捕获参数,想过要block内部修改就需要__block声明变量。

__block int a = 10;
void (^mallocBlock)(void) = ^void {
    NSLog(@"HR_Block - %d",a++);
};

编译之后block结构体的变化很大;


  • __block修改的被捕获变量被声明成__Block_byref_a_0这样的一个结构。把指针的地址和值进行保存,所以才可以在block块内完成值的修改;
  • 调用:__Block_byref_a_0 *a = __cself->a;中的赋值和直接捕获是不一样的,这里是引用了__Block_byref_a_0的地址。在a++操作的时候(a->__forwarding->a)++);因为在初始化的时候保存的就是_a->__forwarding;
  • _Block_object_assign,_Block_object_dispose就是对外界捕获参数的操作,在block的三层拷贝的时候会着重分析;这里就需要简单知道:block需要对外界变量进行持有;因为这里都是c++的操作,无法操作ARC完成引用计数的操作;
  • 如果__block 修饰的是一个对象的话:__Block_byref_id_object_copy_xx会出现这样一个方法.

3、block模板类型

源码位置:
在工程中增加符号断点_Block_copy

libclosure源码下载想要继续深入就需要分析源码;

3.1 Block_layout

Block真正的结构类型,之前看到__main_block_impl_0都是以它为模板生成的,所以Block_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
    // 所有捕捉的外部函数声明
};

#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;  // 保存block的方法签名
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
3.1 Block_layout结构图:
Block_layout

3.2 __block修饰后Block_byref模板

struct Block_byref {
    void *isa;
    struct Block_byref *forwarding;
    volatile int32_t flags; // contains ref count
    uint32_t size;
};

//__Block 修饰的结构体 byref_keep 和 byref_destroy 函数 - 来处理里面持有对象的保持和销毁
struct Block_byref_2 {
    // requires BLOCK_BYREF_HAS_COPY_DISPOSE
    BlockByrefKeepFunction byref_keep;
    BlockByrefDestroyFunction byref_destroy;
};

struct Block_byref_3 {
    // requires BLOCK_BYREF_LAYOUT_EXTENDED
    const char *layout;
};
  • 在前面出现的__Block_byref_a_0就是以Block_byref为模板进行创建的。
  • 如果__block引用的是一个对象则会出现Block_byref_2中的这部分结构。

3.3 Block的方法签名

利用Block_layout结构获取Block方法签名篇幅有点大,就新起了一篇;代码很有意思欢迎阅读!!

4、Block的多次拷贝

编译器是不可以直接在堆区进行创建的需要进行malloc的内存申请,可是__NSMallocBlock__这个类型的Block就是存储在堆区的,那么就一定进行过malloc操作;

4.1 _Block_copy-lldb调试

还记得之前提到过的_Block_copy这个符号断点吗?

  • 通过lldb获取寄存器rax(模拟器)的值,可以看到当前是一个__NSStackBlock__

等到方法执行完之后,在查看寄存器rax(模拟器)的值

  • 进过_Block_copy函数之后,Block从__NSStackBlock__变成了一个__NSMallocBlock__,这个函数就很关键了。

4.2 _Block_copy-源码 第一次拷贝

void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;
    if (!arg) return NULL;
    // 这就是block第一次copy    
    aBlock = (struct Block_layout *)arg;
    // 如果Block需要释放则直接释放
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    // 如果Block是一个全局Block,不做任何操作
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // 从堆区申请内存空间,并将栈区内容移动到堆区
        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)
        // 完成赋值、标示符的修改
        result->invoke = aBlock->invoke;
#endif
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        // 将Block的类型转换为_NSConcreteMallocBlock
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}
  • 代码很清晰,在堆区申请空间,并将栈区所有数据移动到堆区;
  • 最后将isa指向_NSConcreteMallocBlock,完成类型的修改;

4.3 _Block_object_assign-源码 第二次拷贝

Block 捕获外界变量的操作

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没有做任何处理
        _Block_retain_object(object);
        // Block对捕获的外界参数进行了一次强引用,这就是导致循环引用的本质
        *dest = object;
        break;
      case BLOCK_FIELD_IS_BLOCK:
        // 如果外界参数是一个Block类型,那么再进行一次_Block_copy
        *dest = _Block_copy(object);
        break;
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        // 外界参数被__block修饰
        *dest = _Block_byref_copy(object);
        break;
    //还有些其他类型,这里就省略了
    }
}

4.4 _Block_byref_copy-源码 第三次拷贝

__block 捕获外界变量的操作 内存拷贝 以及常规处理

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

    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // 申请了一片堆区控件
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        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) {
            //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_131
            (*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修饰之后会进入进行第三次拷贝,如果本次拷贝的变量又支持copy那么将进行更深层次的_Block_object_assign;

4.5 多层拷贝流程图

如果需要可编译的libclosure源码可以私信我。

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

推荐阅读更多精彩内容