60

重新回味·Block·

 5 years ago
source link: http://www.cocoachina.com/ios/20190318/26583.html?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

zYFf2mf.png!web 说起Block,我想大家再熟悉不过了。每天的开发过程中都在与他打着交道,简洁高效的使用给我们的工作带来了便利。

正是因为Block对于我们而言不可或缺,我更觉得应该抽出时间好好认识一下Block,了解一些他的优缺点、过往经历,让我们对它的理解更深一层,进而开发出更加高效的程序。

对Block的认识

Block与C函数的区别和联系

Block是C语言的扩充,常被称为带有局部变量的匿名函数;

看一下正规的C语言函数定义:

int test(int count);
int result = test(10);

以上声明了函数名为test函数;

使用函数指针调用函数:

int (*testFuncPtr)(int) = &test;
int result = (* testFuncPtr)(10);

对比Block,Block就可以定义为不带名称的函数,说的直白一点,就是匿名函数,即不用显式的定义函数名称,但是匿名函数都有固定的一个表达式。

比如:

^(int count){
  printf("%d", count);
}

等价于:
void test(int count){
}

Block语法定义格式如下:

^ 返回值类型 参数列表 表达式

省略之后:

^ 表达式

通过上面的例子可以看出两点不同:

  • 没有函数名称;

  • 带有“^”符号;

Block的基本使用

可以通过定义Block类型的变量对Block进行调用,与函数指针有相似之处,具体如下:

int (^block)(int) = ^(int count){
      return count + 1;
}
block(5);

7FrqaaZ.png!web

Block与Block的变量

Block类型变量用途:局部变量、参数、静态变量、全局变量以及静态全局变量等等。

Block 可以作为参数进行传递,也可以作为返回值进行传递,具体如下:

- (void)testFuncWithBlock:(void (^)(int count))block{
    if (block) {
        block(5);
    }
}

// 将Block作为返回值,代码摘自·Masonry·
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

具体以Masonry使用为例讲解一下,Block调用:

// Masonry 在常用布局时用到方法如下:
make.top.equalTo(self.titleLabel.mas_bottom).offset(15);

// 有些同学可能对上述的链式调用不是很清楚,其实上述链式调用就是使用Block实现的,分解如下:
MASConstraint *constraint = make.top;
MASConstraint *(^equalBlk)(id) = constraint.equalTo;
constraint = equalBlk(self.titleLabel.mas_bottom);
MASConstraint *(^offsetBlk)(CGFloat) = constraint.offset;
constraint = offsetBlk(15);

可使用关键字typedef,进行Block类型变量的定义

typedef int(^Blk)(int);

// 调用
Blk blk = ^int (int count){
        return count ++;
    };
    int result = blk(5);

Block 作为属性变量的用法:

@property (nonatomic, copy) void (^successCompletion)(int);
@property (nonatomic, copy) Blk failCompletion;

通过上面一小节了解到:

  • 函数与Block区别与联系;

  • Block变量和Block表达式的联系

Block的实质

在前面小节中,介绍了Block的基本用法。那么问题来了,Block究竟是什么呢?现在公认的说法是带有自动变量的匿名函数,动手一探究竟。

使用Clang -rewrite-objc命令对下面代码进行编译:

void testBlock(){
    
    int (^blk)(int) = ^(int count){
        return count ++;
    };
    blk(5);
}

执行完命令会在同级目录下生成XX.cpp文件,这个文件就是编译好的文件。

由于编译后文件包含很多C函数声明,在此就不一一解释了,主要关注上述Block方法实现:

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

static struct __testBlock_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __testBlock_block_desc_0_DATA = { 0, sizeof(struct __testBlock_block_impl_0)};
struct __testBlock_block_impl_0 {
    struct __block_impl impl;
    struct __testBlock_block_desc_0* Desc;
    
    __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;�
        Desc = desc;
    }
};

static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
    
    return count ++;
}

函数具体的实现如下:

void testBlock(){
    
    int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);
}

虽然编译后Block方法很长,但是并不复杂,Block调用就像普通的C函数的使用。

  • 首先,看一下源代码中Block方法体实现:

int (^blk)(int) = ^(int count){
        return count ++;
    };
static int __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself, int count) {
    
    return count ++;
}

可以发现两者在语法格式上非常相近,实际上Block所使用的匿名函数就是被作为简单的C函数处理的。

在代码中:

__testBlock_block_impl_0 *__cself

其中,参数__cself相当于C++中指向实例自身变量的this或者Objective-C中的self。参数__cself在这里是__testBlock_block_impl_0结构体的指针。

然后,看一下__testBlock_block_impl_0结构体的组成:

struct __testBlock_block_impl_0 {
         struct __block_impl impl;
         struct __testBlock_block_desc_0* Desc;
};

去掉代码中加入的构造函数,感觉结构上清晰多了。第一个成员变量是impl,__block_impl结构体的声明如下所示:

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

其中,有关对象isa指针的理解可以参考之前写过的文章 Objective-C Runtime:深入理解类与对象

第二个成员变量Desc,主要保存block所在内存的区域以及Block的大小。

  • 关于__testBlock_block_impl_0结构体构造函数如下:

__testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;�
        Desc = desc;
    }

isa指针指向_NSConcreteStackBlock, __testBlock_block_impl_0结构体相当于objc_object结构体的Objective-C类对象的结构体。

isa = &_NSConcreteStackBlock;

上述代码表明_NSConcreteStackBlock相当于objc_class结构体的实例。从而得知,Block就是Objective-C的对象了。

具体关于Objective-C类与对象的知识可以参考另一篇文章 深入理解类与对象

对于上述构造函数调用部分如下:

int (*blk)(int) = ((int (*)(int))&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA));
    ((int (*)(__block_impl *, int))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk, 5);

看着有些繁琐,对其去掉相关转换的部分:

struct __testBlock_block_impl_0 temp = __testBlock_block_impl_0(__testBlock_block_func_0, &__testBlock_block_desc_0_DATA);
struct __testBlock_block_impl_0 *blk = & temp;
(*blk->impl.FuncPtr)(blk, 5);

上述代码中,将__testBlock_block_impl_0结构体实例的指针赋值给变量blk,从而知道源代码中的Block表达式就是__testBlock_block_impl_0结构体类型的变量,同时也是该结构体在栈上生成的实例。

从函数指针调用得知,由Block语法转换的__testBlock_block_func_0函数指针被赋值到__testBlock_block_impl_0的成员变量FuncPtr,同时说明了__testBlock_block_func_0中的__cself指向Block值。

截获局部变量

在开发过程中,我们经常遇到在Block中截获局部变量或者改变局部变量,如下所示:

- (void)testBlock{
    
    int value = 1;
    void (^block) (void) = ^{
        NSLog(@"value = %d", value);
    };
    block();
}

看到这里,可能就有疑问了,Block内部是如何获取局部变量的,带着疑问重新Clang了一下源文件,似乎找到了答案,摘取了关键代码如下所示:

static void _I_TestBlock_testBlock(TestBlock * self, SEL _cmd) {

    int value = 1;
    void (*block) (void) = ((void (*)())&__TestBlock__testBlock_block_impl_0((void *)__TestBlock__testBlock_block_func_0, &__TestBlock__testBlock_block_desc_0_DATA, value));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    
}

//Block内部实现:
static void __TestBlock__testBlock_block_func_0(struct __TestBlock__testBlock_block_impl_0 *__cself) {
  int value = __cself->value; // bound by copy
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_pf_nyb_8kl12yq6v2sxzwdr5v8w0000gn_T_TestBlock_516a43_mi_0, value);
}

// Block结构体
struct __TestBlock__testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __TestBlock__testBlock_block_desc_0* Desc;
 // 将局部变量追加成成员变量 
 int value;
 
  __TestBlock__testBlock_block_impl_0(void *fp, struct __TestBlock__testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

通过上述Block语法表达式中使用的局部变量作为成员变量被加到了__TestBlock__testBlock_block_impl_0结构体中,同时,加到结构体中的成员变量的类型与局部变量的类型保持一致。当然了,如果Block表达式中没有使用局部变量,则不会加入到Block的结构体中。

通过初始化Block实例的构造方法和调用也可以看到局部变量value作为参数传递,具体如下所示:

//Block 初始化方法
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, int _value, int flags=0) : value(_value)

// 初始化方法的调用:
void (*block) (void) = ((void (*)())&__testBlock_block_impl_0((void *)__testBlock_block_func_0, &__testBlock_block_desc_0_DATA, value));

到这里,大致明白了Block如何截获局部变量value的,小结如下:

  • 1、系统编译时,根据Block是否需要截获外部变量来判断是都将局部变量作为成员变量保存到Block的结构体__testBlock_block_impl_0中;

  • 2、若需要截获局部变量,需要在初始化结构体函数中添加与局部变量相同类型的参数;

  • 3、局部变量就通过初始化构造函数传递到Block结构体__testBlock_block_impl_0,同时赋值给Block中的成员变量;

  • 4、在执行Block时,局部变量value会初始化__testBlock_block_impl_0结构体实例, 如下所示;

    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = 0;
    impl.FuncPtr = __testBlock_block_func_0;
    Desc = &__testBlock_block_desc_0_DATA;
    value = 1;

总的来说,截获局部变量就是将要使用的局部变量保存到Block结构体实例中。

Block中使用C语言数组

  • 只使用C语言的字符串数组,也没有向捕获的局部变量赋值,然而下面的代码再编译时就会产生错误,如下所示:

int testBlock1(){
    
    const char country[] = "China";
    void (^block) (void) = ^{
        printf("text=%c",  country[1]);
    };
    block();
    
    return 0;
}

编译的时候会出现错误:

RFrU3eF.png!web错误信息

看到这,你会感到疑惑,上面刚讲到Block是如何截获局部变量的,为什么到这里就失效了呢?

这是因为,Block中,没有实现对C语言数组的截获。通过上述截获局部变量的理论,C语言数组会作为成员变量保存到Block实例的结构体中,在初始化Block时,由成员变量赋值给局部变量,猜测代码如下:

void func(char a[]){
    char b[] = a;
}

从而看出端倪,将C语言数组类型变量赋值给C语言数据类型变量,这个在C语言下是无法编译,C语言不允许这样编译的。

如何解决呢?

目前,可以改成C语言数组的形式就可以解决上述问题了。

int testBlock1(){
    
    const char *country = "China";
    void (^block) (void) = ^{
        printf("text=%c", country[1]);
    };
    block();
    
    return 0;
}

__block关键字的作用

前面的小节中,我们仅仅讲解了在Block中截获局部变量,那么,在Block中修改截获的局部变量又会带来什么样的问题呢?

IFfYNvu.png!web

修改截获的局部变量

通过上面的代码可以看出,虽然在Block中不能对局部变量进行修改,但是在全局变量、静态全局变量、静态变量都是可以在Block中进行修改的,具体如下:

int globalValue = 10;
static int staticGlobalValue = 5;
- (void)testBlockFunc {
    static int value = 5;
    void (^block)(void) = ^{
        
        globalValue = 11;
        staticGlobalValue = 12;
        value = 3;
        printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, value);
    };
    
    block();
}

打印的结果如下:

2018-12-02 21:56:47.323727+0800 TestBlock[38880:1182883] 
globalValue = 11 
staticGlobalValue = 12 
value = 3

再次使用clang编译了一下,发现静态全局变量和全局变量转换前后没有任何变换,大家可能疑问静态局部变量是如何转换的呢?

__TestBlockMemory__testBlockFunc_block_impl_0 *__cself) {
  
    int *value = __cself->value; // bound by copy
        globalValue = 11;
        staticGlobalValue = 12;
        (*value) = 3;
        printf("globalValue = %d \n staticGlobalValue = %d \n value = %d \n", globalValue, staticGlobalValue, (*value));
    }

与截获局部变量的转换很相似,唯一的区别是Int类型的变量转换成了指针,从而正确的改变了原有的值。

除了上述的几种方式可以在Block中修改变量值外,OC专门提供了一个说明符——__block。

int testBlock(){
    __block int number = 1;
    void (^block) (void) = ^{
        number = 2;
    };
    block();
    return 0;
}

将上述代码进行Clang编译,还原成以下代码:

struct __Block_byref_number_0 {
  void *__isa;
__Block_byref_number_0 *__forwarding;
 int __flags;
 int __size;
 int number;
};

struct __testBlock_block_impl_0 {
  struct __block_impl impl;
  struct __testBlock_block_desc_0* Desc;
  __Block_byref_number_0 *number; // by ref
    
  __testBlock_block_impl_0(void *fp, struct __testBlock_block_desc_0 *desc, __Block_byref_number_0 *_number, int flags=0) : number(_number->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __testBlock_block_func_0(struct __testBlock_block_impl_0 *__cself) {
  __Block_byref_number_0 *number = __cself->number; // bound by ref
        
        (number->__forwarding->number) = 2;
}

惊奇的发现,局部变量number变成了结构体__Block_byref_number_0,Block的__testBlock_block_impl_0的结构体实例持有__block变量的__Block_byref_number_0结构体实例的指针。__testBlock_block_impl_0实例的成员变量__forwarding指向实例自身,并通过__forwarding变量访问成员变量** number**。

__Block_byref_number_0 *number = __cself->number; // bound by ref
 (number->__forwarding->number) = 2;

值得注意一点是,__Block_byref_number_0结构体并没有在__testBlock_block_impl_0结构体中,这样主要是为了在多个Block中共用该__block变量。

Block的储存

Block分为三类:** _NSConcreteStackBlock**

种类 存储域 Block变量类型 复制后的存储域 _NSConcreteStackBlock 栈 局部/自动变量 由栈复制到堆上 _NSConcreteGlobalBlock 程序的数据区域(.data区) 全局变量 保持在数据区 _NSConcreteMallocBlock 堆 局部变量 增加引用计数

定义在全局的Block在变量作用域之外通过指针访问使用,定义在栈上的__block局部变量和Block在超出作用域时将被废弃。那Block超出作用域是如何存在的呢?

这里就用到Block的Copy操作,即将分配在栈上的Block 和__block变量复制到堆上,从而延长了Block和__block变量的生命周期。

那么,Block被复制到堆上的操作是何时进行的呢?

首先,比较明确的一点是:将Block作为返回值返回时,编译器将会自动将Block由栈上复制到堆上,其他情况下,需要手动执行Copy操作;在ARC有效的情况下,Block是否被复制到堆上,编译器会进行相关的判断;

举个栗子:

typedef int (^BlockCase)(int);

BlockCase getBlockCallBack(int value) {
    return ^(int count) {
        return count + value;
    };
}

在ARC有效的情况下,作为返回值的Block会被复制到堆上。当然了,系统中存在的一些API是无需手动复制的:Cocoa框架方法中带有usingBlock方法、GCD相关的API等等。

__block变量的存储域

当在Block中使用__block变量时,当在Block从栈复制到堆上同时,被使用过的__block变量也会从栈复制到堆上。如下图所示:

__block变量所分配的存储域 Block从栈复制到堆上后的影响 栈 由栈复制到堆上并被Block持有 堆 被Block持有

__fawarding指针存在的意义

前边介绍了Block和__block变量从栈复制到堆上的情景,但都与__fawarding指针脱不开关系。

  • 由栈复制到堆之前,__fawarding指针指向自身;

  • 复制之后,栈上的__fawarding指针指向复制到堆上的__block变量,堆上的__fawarding指针指向自身。

    从而,解释了无论__block变量配置在栈上还是配置在堆上时都能正确的访问到__block变量;

Block截获对象

通过了解上节的内容,很好理解以下内容:

  • __block修饰的变量从栈复制到堆上,赋值给该__block变量的对象也被从栈复制到堆上,当__block变量从堆上释放时,该对象才能得到释放;

  • 当使用__weak修饰的__block变量在赋值的时候,由于赋值对象的作用域问题而释放,从而导致__block变量不能强持有该对象。

Block循环引用

形成原因:

  • 对象与Block相互持有;

  • 形成强持有环;

    解决方式:

  • 设置weak弱持有关系;

  • 手动置为nil,梳理好持有关系,破坏环状结构。

结束

关于Block就大致说到这吧,从Block的使用到Block的原理剖析,希望能让我们更客观的认识Block,写出更高质量的代码。

好久没有更新文章了,想必老铁们也都等着急了吧。主要是从2018年3月底更新后最后一篇文章,就一直在忙,关于那段时间的事情会在另一篇文章中详述。

谢谢大家的支持!

作者:Jack_lin

链接:https://www.jianshu.com/p/ad0a41981017


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK