简单的打印、神奇的本质之 Block

本来想把标题命名为 【OCBlock 的本质】。
废话不多说,直接往下看。

一、Block 捕获变量的地址变了

有几句简单的代码,望君记下:

NSObject* obj;
printf("1 = %p\n", &obj);
void (^block)(void) = ^{
    printf("2 = %p\n", &obj);
};
printf("3 = %p\n", &obj);
block();
printf("4 = %p\n", &obj);

接下来,为了简单方便,直接使用图片了。

场景一

简单的打印、神奇的本质之 Block_第1张图片
image.png

除了红框框中的打印,其它的都一样。换句话说,在 block 中的 obj 的地址变了,同一个东西,地址尽然还变了,这是什么个情况???同时也要注意地址变化的位置,貌似相隔甚远呐。

场景二

简单的打印、神奇的本质之 Block_第2张图片
image.png

是的、没有看错,相比于场景一,就多了一个 __block 修饰符。除了 block 定义之前的都变了,尤其是第3个,block 还没被执行呢,还跟着凑什么热闹,尽然也变了。同样,也看一下变化的地址。

场景三

简单的打印、神奇的本质之 Block_第3张图片
image.png

是的、你依然没有看错,仅仅是变了一个修饰符 static。这一次就更加的厉害了,都没有变。但是,不要忘记了看看这一次的地址,与场景一、二的有什么不同。答案是长度不同,对的、可以这么回答。

说吧

看完了上面的三张图片,你就没有什么要跟我说的吗?好吧、不说的话,就直接看最后的总结吧。

本节小节

image.png
你知道这到底是为什么吗?看完了下面的内容、你应该能明白。

二、Block 的内部结构

涛声依旧,简单代码、望君记下:

// block 测试代码
- (void)testBlock {
    void (^hgBlock)(void) = ^{
        NSLog(@"block 中就一句打印");
    };
}

很简单的代码,定义了一个 block,啥也没干,也没有调用。

接下来看看这段代码的背会到底隐藏着什么,通过如下指令转成 cpp 代码:

mxcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 HGObject.m -o HGObject.cpp

你在 cpp 文件中,应该能找到以下这些代码。
代码一:

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

代码二:

static struct __HGObject__testBlock_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __HGObject__testBlock_block_desc_0_DATA = { 0, sizeof(struct __HGObject__testBlock_block_impl_0)};

关键的代码三:

struct __HGObject__testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __HGObject__testBlock_block_desc_0* Desc;
  __HGObject__testBlock_block_impl_0(void *fp, struct __HGObject__testBlock_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

总的来说,上面定义的一个 hgBlock 主要生成了上面核心的代码。其中 __HGObject__testBlock_block_impl_0 就是 hgBlock 的完整结构。通过其命名也能看出来:impl 主要是 block 调用时候用的,可以自行实验。Desc 就是这个 Block 的描述信息。所以每次研究 Block 的结构,主要就是看 implDesc 的变化,这两个地方是会改变的,尤其是当 Block 使用到 Block 以外的非全部变量的时候。

其实现在就可以将第一小节的各种场景都试一下,相信你能看出其中的 奥妙

三、强/弱引用

很多时候总是会说,Block 强引用了某个指针变量、或者说 Block 弱引用了某个指针变量。这是咋回事?这个问题是不是很简单?其实不简单!
通常在 Block 中使用了一个强指针,我们就会说这个 Block 强引用了这个指针,弱指针则同理。
要想策底的说明这个问题,其实就是要验证在 Block 中使用了一个强指针之后到底发生了什么事情。凭什么说在 Block 中使用了,就有引用关系了。直接看下面的图片吧。

场景一:强引用

// block 测试代码
- (void)testBlock {
    void (^hgBlock)(void) = ^{
        self;
    };
}

是的在 hgBlock 中仅仅是使用了一下 self 而已。

简单的打印、神奇的本质之 Block_第4张图片
image.png

注意观察 红框框,在 impl 中多了这么一句:

HGObject *const __strong self;

一定要注意这个是 自动 生成的,换句话来说只要是在 Block 中使用了一个 强指针,那么在这个地方就会有一个对应类型的指针生成。也可以自行试一下第一小节的情况。
如果你试过了第一小节,你会发现是没有上图中(__HGObject__testBlock_block_desc_0)第二个 红框框 中的内容的。这个也很容易理解,这两个函数的出现主要是处理内存相关的。接下来简单的介绍一下关于 Block 中的内存问题。
Block 会出现在三个地方:栈区、全局区域堆区。具体的规律是:没有访问 auto 变量的都在全局区、访问了 aotu 变量的在栈区,当在栈区的 Block 调用了 copy 之后就会变成了堆区。

当你按照上面的规律试验的时候,你会发现根本不对,不会出现在栈区的情况。你可能使用的是 ARC环境了,你换成 MRC 环境试试,因为 ARC 环境会自动的将栈区的 Block 变量执行以下 copy 函数,所以就直接在堆区了。注意:我说的是变量,而不是说直接在栈区就 copy 了。可以不使用变量接收,而直接使用 NSLog 打印就知道了。

在上面也说到了一个 copy 函数,其实说的就是 Desc 中的那个 copy 了。当然了还有一个 dispose 这个也很理解,这个是在这个 Block 销毁的时候对引用的这个对象做 release 操作的。

说了这么多,别忘了,这个场景的主题是 强引用

场景二:弱引用

// block 测试代码
- (void)testBlock {
    __weak typeof(self) weakSelf = self;
    void (^hgBlock)(void) = ^{
        weakSelf;
    };
}

是的、这是一个和常规的 weakSelf 的声明方式。看一下转成 cpp 后的结果:

简单的打印、神奇的本质之 Block_第5张图片
image.png

果真不负众望,出现了这个:

HGObject *const __weak weakSelf;

仔细想想、其实也对。

本节小节

注意:这个小节主要介绍的是 Block 中的强/弱引用、其实我也没有打算介绍多么深奥的东西。

四、__block 隐藏在背后的秘密

这是一个很神秘的变量,具体的作用,应该没有不清楚的。具体的看代码吧:

// block 测试代码
- (void)testBlock {
    __block int a = 10;
    
    void (^hgBlock1)(void) = ^{
        a = 18;
        NSLog(@"hgBlock1 = %p", &a);
    };
    
    void (^hgBlock2)(void) = ^{
        NSLog(@"hgBlock2 = %p", &a);
    };
    
    hgBlock1();
    hgBlock2();
}

这个就有点复杂了,两个 Block 同时使用了一个 __block 变量。
转成 cpp 如下:

简单的打印、神奇的本质之 Block_第6张图片
image.png

上图中仅仅是其中一个 Block 的结构,另一个也类似。但是新出来一个东西:__Block_byref_a_0,你仔细的话,还会发现另一个 Block 是共用__Block_byref_a_0 的。当你执行上面的代码、你还会惊奇的发现,里面的 &a 的值都是一样的。是的,他们不仅共用同一个数据结构,还共用同一个 __Block_byref_a_0 对象。在 __Block_byref_a_0 中还有一个比较有意思的指针 __forwarding, 这个指针就是当前 __Block_byref_a_0 对象的地址,可以通过转成 cpp 代码中寻找具体的逻辑。这样设计的目的是考虑到到当一个栈区的 Block 调用 copy 之后,在栈区与堆区共用同一个变量。具体可以查看下面的图:

简单的打印、神奇的本质之 Block_第7张图片
image.png

到这里,__block 关键字背后隐藏的那些密码、也差不多介绍了 30%了,剩余的 70% 可以自行慢慢的体会。

五、typeof 引发的思考

其实在上面 弱引用 的介绍中、已经使用到 typeof 关键字了,通过上面对 强/弱引用 的介绍。请看下面的代码:

// block 测试代码
- (void)testBlock {
    __weak typeof(self) weakSelf = self;
    void (^hgBlock1)(void) = ^{
        __strong typeof(self) self = weakSelf;
    };
}

厉害了,这不是在 hgBlock 强引用 self 了么?理由是这一个 typeof(self)。确实是引用了 self 变量,但是 type 有两个特性:类型替换,并且是发生在编译期,然而 Block 对指针的引用决定于运行期,当生成 .......impl 结构体的时候,self 已经做完了类型替换了。

可以看一下 cpp 后的结果:

简单的打印、神奇的本质之 Block_第8张图片
image.png

只有对 weakSelf 的弱引用,没有对 self 的强引用。

但是通常我是这么写的:

__strong typeof(weakSelf) self = weakSelf;

所以第一次看到这里写成 typeof(self),还是惊讶的,但是其实是没有影响的。

六、一个小实验

有这样的一个结构体:

// 结构体
struct HGStruct {
    void *isa;
    int a;
    void (*func)(void);
};

再有这样的一个测试代码:

// 结构体测试
- (void)structTest {
    struct HGStruct *s = nil;
    s->func();
}

运行代码:


简单的打印、神奇的本质之 Block_第9张图片
image.png

话不多说,直接看 Block 解开多年来的误解,这个问题我已经在很多的场合提到了。这个问题是我第一次离职、第一次面试的第二轮遇到的面试题。记忆深刻啊、经历过后才发现 找工作 不是想的那么简单。

但是仔细观察,你会发现上面的定义的这个 HGStruct 结构体与 __block_impl还相差一个成员变量,为什么 Crash 的值是一样的呢。这是我故意的,因为这里关系到一个地址对齐的技术点。

其次在上面的代码中,我是故意的将 s 设置成的 nil。这个应该是编译器的初始化问题,这种 C 结构体的指针的默认值并不是 nil,这一点不像 OC 的对象。


接下来给一到练习题吧。

练习题、也是思考题(注意区分 ARC 与 MRC)

今天无意间有发现了这样的问题,代码如下:

- (NSArray *)createBlockArray {
    NSInteger i = 1;
    NSInteger j = 10;
    return [NSArray arrayWithObjects:^{
        NSLog(@"%ld", i);
    }, ^{
        NSLog(@"%ld", j);
    }, nil];
}

MRC 环境的运行效果是这样的:

简单的打印、神奇的本质之 Block_第10张图片
MRC 环境

ARC 环境的运行效果是这样的:

简单的打印、神奇的本质之 Block_第11张图片
ARC 环境

是的、ARC 环境也 Crash 了,但是 Block 正常执行了,最后也 Crash 了,并且地址的值 都是 0x20

那么问题来了:

1、请分析上面导致的原因。
2、将 createBlockArray 中的数组换成 @[。。。。] 的方式,再分别在 A/MRC 环境试试会有什么不一样。

提醒一下:Block 所在哪个数据区(栈、堆与全局)?

这里也得出一个结论:不是所有的 Block 闪退都是 0x10

七、总结

虽然也就是简单的 Block 相关介绍,但是通过上面的一些操作还是依旧可以再学习到更多的东西、更多的信息还得自行慢慢品味。

你可能感兴趣的:(简单的打印、神奇的本质之 Block)