关于Block的几个知识点分析说明

2018-05-13  本文已影响0人  Moclin

前言

最近复习一下Block的知识,发现一些书籍和网上的文章对Block的某些知识点的解释存在不清晰甚至存在错误。所以决定写下这篇,对其中的一些知识点进行分析。

PS:这里没有对Block的详细介绍,阅读需要先对Block的整体知识有所了解。

关于Block的几个知识点分析说明

1. 在Block中不能对截获的自动变量赋值

我们都知道,在Block中不能截获的自动变量进行修改。我在网上看到很多文章都没有对这个点进行说明,甚至有些文章还有错误理解。实际上这里的修改应该表述为赋值更加准确,也就是说不能修改变量的值内容。对于数值变量来说,是不能修改的其数值;对于指针变量来说,是不能修改其指针的值,也就是指针指向的内存。

明白上面所说的,下面来看一下,为什么在Block中不能对截获的自动变量赋值。

首先,使用clang -rewrite-objc 文件名命令把下面代码转换

int main()
{
    NSObject *obj = [NSObject new];
    int val = 2;
    void (^blk1)(void) = ^{
        NSLog(@"%@ - %d\n", obj, val);
    };
    blk1();
    
    return 0;
}

转换后的关键代码如下

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *obj;
  int val;
  //构造函数
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int _val, int flags=0) : obj(_obj), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main()
{
    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("new"));
    int val = 2;
    void (*blk1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, obj, val, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)blk1)->FuncPtr)((__block_impl *)blk1);

    return 0;
}

可以看到,__main_block_impl_0的构造函数objval参数都是通过值传递的,也就是说改变结构体内objval并不能改变Block外部的原变量,所以在Block内改变截获后的自动变量对于原来的变量就没有意义了。
这个问题的关键是C语言的参数传递,我在另一篇文章已经对C语言的参数传递进行了分析:C语言参数传递

上面已经可以回答为什么在Block内不能对截获的自动变量进行赋值了,那么使用__block修饰的自动变量有为什么可以赋值呢?其实同样是参数传递原因。
首先,使用__block修饰的自动变量通过clang转换后,会变成一个结构体。而Block结构体的构造函数是接收了这个结构体的地址,将结构体的地址赋值给其成员变量(使用__block修饰的自动变量的结构体被Block结构体以成员变量的方式持有),所以使用__block修饰的自动变量在Block中是可以重新赋值的。

2. 不同存储域的Block之间的关系

根据存储域,Block可分为以下三种:

_NSConcreteGlobalBlock  //全局
_NSConcreteStackBlock   //栈
_NSConcreteMallocBlock  //堆

Block是通过Block结构体的isa成员变量来指明Block是属于那种Block的。那么什么样的Block,或者什么时候,Block会分配在不同的存储域呢?

通过断点可以Block的类型,如下图:


pic1.png

这里不贴具体代码和图片,通过断点可以知道:全局Block以及没有截获自动变量的Block是属于_NSConcreteGlobalBlock类的,除此之外,不论在何时何创建的Block应该都是_NSConcreteStackBlock。那么_NSConcreteMallocBlock呢?

实际上,通过断点看到的除了_NSConcreteGlobalBlock都是,其他都是_NSConcreteMallocBlock,这又是为什么呢?跟我前面所说的不一样啊。其实是这样的:在ARC中,把Block赋值给使用strong修饰的变量时,编译器会自动插入copy操作(会把栈上的block复制到堆上)。所以我们会只看到_NSConcreteMallocBlock,验证这一点可以通过把Block赋值给使用__weak修饰的变量,再进行断点查看。

最后,_NSConcreteMallocBlock。只有在_NSConcreteStackBlock复制到堆时,Block才会变成_NSConcreteMallocBlock,而对_NSConcreteGlobalBlock进行``copy```操作是不会改变改变Block的类型的。

3. Block在ARC有效和无效的情况的不同

关于ARC有效和无效的情况下,Block的一些不同,感觉所有书籍和文章提及得比较少,毕竟现在都用ARC了。在这里只是做一下整理,不作详细探究。

首先,是2中提到的,ARC无效的时候,是没有自动copy的,所以ARC无效时,如果手动copy,那么就不会看到_NSConcreteMallocBlock。另外一点是,ARC无效时,在Block中使用使用了__block修饰的自动变量是不会被retain的,所以在ARC有效和无效的不同情况下,__block的使用有着很大区别,无效时相当于__weak,可用于避免循环引用。

上一篇下一篇

猜你喜欢

热点阅读