iOS ------ Block的总结

时间:2024-04-22 07:12:45

前面看了Block的基本知识,和一些源码。但对于block怎么用的还不了解,代码中出现block会看不懂,现在来具体看一下Block的用法并做个总结。

1.Block是什么

block对象是一个C语言结构体,可以并入C和OC的代码中,Block本质是一个匿名函数,以及与该函数一起使用的数据,其他语言有时称为闭包或ambda。Block特别适合于回调,或者是在你为了让代码看起来具有更清晰的逻辑进行代码的组合时使用。

上面是苹果官方的解释,告诉我们:首先Block是一个OC中的对象,并且这个对象是一个C语言的结构体,它可以使用在C语言和OC中;同时,Block本质是一个匿名函数和其包含的数据集中

2.为什么用Block

苹果官方的文档的描述

  1. 代替代理和委托方法
  2. 作为回调函数的代替
  3. 一次性编写多次处理的任务
  4. 方便对集合中的所有项执行任务
  5. 与GCD队列一起执行异步任务

以上这些情况,我们都能够使用Block。我感受最深刻的是使用Block回调,很多情况下,我们可能只需要对某个事件进行一个简单的回调,也许就仅仅一次,如果我们使用代理的话,我们需要创建类,编写协议,仅仅对一个小地方的回调成本很高,那么Block的使用就恰到好处。除此之外,Block的特性还可以让代理集中在某处,我们只需要在一个地方就可以完成回调之前和回调时的代码,相比,使用回调函数和代理都没有这个优势。另外,我门可以想到,OC中封装了一些集合分方法,比如数组的排序,这里就使用Block进行回调操作的。

简单讲一下回调的概念

将一段代码和一个特定的事件联系在一起,当特定事件发生后,这段代码才会被执行。
OC的几种回调

  • Targe-action回调
  • delegate方式
  • NOtification方式
  • block方式

OC的的回调几种方式

3.怎么使用Block

我们先创建一个Block,并简单的使用
在这里插入图片描述

运行结果
在这里插入图片描述
这里是为了强调block的回调效果。可以发现,尽管block的代码早就声明了,但是没用立即调用,而是在block的调用的时候,才被执行。到这里应该对Block的回调有一定的理解。

这好像并没有什么用处,下面看一看另外两种Block的使用。前面说block是OC的一个对象,既然是对象,我们可以把它当做一个类分属性,应该也可以更其他属性一样,被当作一个方法的参数吧。这也是block被大家认可的地方。

假设我们有这样一个类:包含一个block属性testBlock,包含一个调用自己的block属性的方法blockDo。

@interface Computer : NSObject
@property (nonatomic, copy) NSString* (^testBlock)(NSString*);//将block作为computer的属性
- (void)blockDo;
@end

#import "Computer.h"
@implementation Computer
- (void)blockDo {
    NSString* testString = @"textData_old";
    if (self.testBlock) {
        NSLog(@"%@", self.testBlock(testString));//调用并打印
    }
}
@end

在其他地方写下这些代码

Computer* computer = [[Computer alloc] init];
        computer.testBlock =  ^(NSString* parStr){
            NSLog(@"%@",parStr);
            parStr = @"testData_New";
            return parStr;
        };
        [computer blockDo]; //执行Block

运行结果
在这里插入图片描述

这里的调用就比前面的复杂了。因为我给Computer添加了一个方法,并且将block的调用交给了Computer,我只是实现了block而已,最后启动调用它的方法。我们在另一个地方对Computer类模拟了一个方法(也就是块,这个方法没有在Computer类中实现,我们甚至可以在任何地方实现它,最后我们可以在其他地方调用。这就是Block的神奇的地方。

再来看一个

@interface Computer2 : NSObject
- (void)doSomthingFeedBakck:(NSString* (^)(NSString*))handle;
@end
#import "Computer2.h"

@implementation Computer2
-(void)doSomthingFeedBakck:(NSString * _Nonnull (^)(NSString * _Nonnull))handle {
    NSString* handleStr = @"Old";
    sleep(3.0);
    NSLog(@"%@", handle(handleStr));
}
@end

其他地方调用

Computer2* computer2 = [[Computer2 alloc] init];
        [computer2 doSomthingFeedBakck:^NSString * _Nonnull(NSString * parStr) {
            NSLog(@"%@", parStr);
            NSString* returnstr = [NSString stringWithFormat:@"add %@", parStr];
            return returnstr;
        }];

运行结果
在这里插入图片描述

这里讲Block作为一个参数放在doSomthingFeedBakck函数面。体现了block的对象本质,相比之下,代码很是简洁。这种实现回调的方法逻辑更加清晰,明朗。

上面三个例子,展示了block的三种不同的使用方式。它们分别是:

  • 将block定义成员变量
  • 将block定义成属性
  • 将block作为参数

4.总结

通过上面的block的额用法发现,block每次回调是通过它的匿名函数进行的,也就是每次最多执行一个回调,在需要进行大批量的回调的时候,就需要写很多不同的block回调,这样就不合适这时使用协议和代理的方式就自然多了。除此之外,block还比较适合线程之间的切换回调,GCD就是采用了多线程结合block来做的。

5.下面我们记录一些block的原理性知识

  • 为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数
  • 全局block,栈block,以及堆区block的区别和他们之间的联系,探究block的内存管理
  • 为什么使用_block就可以使block可以修改外部变量
  • 引起强引用的原因是什么,我们解决的方法和原理是什么

1,为什么说block是一个结构体,也是一个对象,同时还是携带数据的匿名函数

使用Mac终端,cd将图中的第一个文件拖入

在这里插入图片描述

文件写以下内容

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        void (^block)(void) = ^ {
            printf("a block");
        };
        block();
    }
    return 0;
}

查看clang中间文件

clang -rewrite-objc main.m

在这里插入图片描述

回车会在刚拖入的文件中生成.cpp文件

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

//block 结构体
//这一部分在.cpp文件中没有找到
struct __block_impl {
  void *isa;//block 的类型
  int Flags;
  int Reserved;
  void *FuncPtr;// block的执行函数指针,指向__main_block_func_0
};
                        
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("a block");
        }

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; 
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

到这里,我们可以看到,在编译之前,block复原成以上四个结构体。 它们分别是:

  • __main_block_impl_0
  • __block_impl
  • __main_block_desc_0
  • __main_block_func_0

我们暂时不管这些结构体代表的都是什么。但可以说明block是结构体。仔细观察__main_block_impl_0 的机构中,有isa指针一项(黄色标出)。看到这里理解为什么苹果强调block也是一个对象了。再看还有 __main_block_func_0 ,这里其实就是我们对block函数体的实现,它实际上是一个匿名函数,作为block的众多结构体的一部分。

全局block,栈block,以及堆区block的区别和他们之间的联系,探究block的内存管理

在OC中用三种不同的block类型。它们分别是全局block _NSConcretionGlobalBlock, 栈block _NSConcretionStackBlock,以及堆block _NSConcretionMallocBlock.

全局block

假如我们这样创建一个block并使用
在这里插入图片描述

通过clang 命令获得中间编译内容

int GlobalInt = 0;

struct __getGlobalInt_block_impl_0 {
  struct __block_impl impl;
  struct __getGlobalInt_block_desc_0* Desc;
  __getGlobalInt_block_impl_0(void *fp, struct __getGlobalInt_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteGlobalBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static int __getGlobalInt_block_func_0(struct __getGlobalInt_block_impl_0 *__cself) {

    return GlobalInt;
}

static struct __getGlobalInt_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __getGlobalInt_block_desc_0_DATA = { 0, sizeof(struct __getGlobalInt_block_impl_0)};
static __getGlobalInt_block_impl_0 __global_getGlobalInt_block_impl_0((void *)__getGlobalInt_block_func_0, &__getGlobalInt_block_desc_0_DATA);
int (*getGlobalInt)(void) = ((int (*)())&__global_getGlobalInt_block_impl_0);
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_46_qzlmlhgd0xd2xjp590bfb5_00000gn_T_main_14343d_mi_0, ((int (*)(__block_impl *))((__block_impl *)getGlobalInt)->FuncPtr)((__block_impl *)getGlobalInt));
    }
    return 0;
}

_NSConcretionGlobalBlock代表这是一个全局的block。全局block和全局变量一样,可以在整个数据域中使用。 这里的其他的任何retain, copy对它是没有影响的。它存储在静态区域,基本可以理解,在APP运行期间,它是一直存在的。

很明显看到,这个block作为全局变量的形式被创建出来的。还有一种更加隐秘的方式, 像下面这样。

在这里插入图片描述

打印
在这里插入图片描述

同样是一个全局的block。

所以全局block的生成有两种不同的情况,一个是直接将block创建成一个全局变量。这是苹果官方的用法。另一种是创建一个局部变量的block,在block的函数体中,不使用任何外部的全局变量。但这个block和全局变量的block是有所不同的。我们将这个block的cpp中间文件打开。

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("a block");
        }

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; 
        void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_46_qzlmlhgd0xd2xjp590bfb5_00000gn_T_main_c071cc_mi_0, block);
    }
    return 0;
}

竟然是 _NSConcreteStackBlock! 也即是我们所说的栈block! 这是为什么呢? 我个人的理解是,对于CPP文件中的指示,它是告诉用户这个结构体的存储的位置。 而在nslog打印中显示不一样是因为因为没有包含局部变量,所以block本身不需要携带上下文环境,系统在编译的时候,默认Block是全局环境。 这才导致两种展示的方式不一样。

栈block

刚才说了,全局block的其中创建方式是作为一个不包含外部变量的局部变量block。 那如果这个block变量包含了外部变量那又会怎样呢。没错,当包含了外部变量的时候,它是一个栈Blobk。

在这里插入图片描述

clang查看代码生成的中间文件。 发现是存在于栈中的。

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;
  }
};

打印结果,在ARC环境中
在这里插入图片描述

在MRC环境下
在这里插入图片描述

为什么在ARC的环境下的打印结果不同呢?

正常情况下,block的申明都是在栈中的,如果需要将它转到堆上,需要进行block_copy,或者其他发送copy消息。
如果在MRC没有进行copy的话,那么当处于栈中的block的环境被销毁的时候,block也等同被销毁了。
而在ARC中 ,因为系统会自动对block发送copy消息(原因是为了确保在块作为对象时,其生命周期得到正确管理)所以我们打印的时候看到block是mallco类型,即位于堆上的。在没有进行copy 之前,栈上的Block使用retain 等操作都是没有实际作用的。

堆上的block

block的申明的时候都是在栈上的,如果发送了copy消息,那么block才会被复制到上。

当复制到堆上之后,我们使用block就可以像使用普通的属性一样,可以进行retain等。注意,重复发送copy 消息,也只会在堆上保留一份blcok。在block所在的栈中的内容没有被销毁之前,这个栈中的block还依然存在的。但是它多了一条跟堆中block的联系。我们回过头看他的一个结构体:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

printf(“a block”);

}

在这个block的结构体中,结构体本身的类型是__main_block_func_0 。 内部有一个参数指针指向了一个__main_block_impl_0,cself 结构体,这个指针实际上指向的是自己,如果block接受了copy 消息之后,那么这个指针将指向堆上的那份block,而堆上的那份的block的cself 还是指向堆上的blobk结构体,这就是为什么在复制到堆上之后,当栈上的内容被销毁时,block调用不会crash的原因了。

三,为什么使用_block就可以使block可以修改外部变量

我们先看一看当block对外部变量使用__block修饰局部变量前后的clang
使用前
在这里插入图片描述

clang

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf("%d", a);
        }```

使用后
在这里插入图片描述

clang

struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int 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; // bound by ref

            printf("%d", (a->__forwarding->a));
        }

我们可以看到区别:

当使用不加block修饰的局部变量时,直接通过__cself指针找到__main_block_impl_0结构体中自动生成的一个相同类型的age变量(为值传递),所以不能修改捕获的变量。

但使用block修饰的block变量时,通过__cself指针找到__main_block_impl_0结构体中__Block_byref_a_0 *的a结构体(_block修饰的局部变量),再在a中找到__forwarding指针,可以看到它的类型同样为__Block_byref_a_0 *类型,它指向结构体本身(block修饰的局部变量),最后通过_forwarding指针找到局部变量a本身,这样就可以进行修改外部变量。

简单讲,就是一个修改的是值传递过来的外部变量,一个是通过指针找到外部变量本身,修改实际上就是对外部变量的修改

四,引起强引用的原因是什么,我们解决的方法和原理是什么

什么情况下block会造成循环引用

block为了保证代码块内部对象不被提前释放,会对block中的对象进行强引用,就相当于持有了其中的对象,如果此时block中的对象有持有了该block,就会造成循环引用。

在这里插入图片描述

block作为self的一个属性,表明self是持有block的,这样,如果需要释放block,至少需要先把self对它的持有释放,而block是self的属性,要让self不在持有它,只有一种情况,self已经被释放。ARC会自动处理块对self的持有关系。当块被复制到堆上时,会自动将self的引用计数加1,以确保在块的生命周期内,self对象不会被提前释放,也即是持有了self.

那么这样的情况下,如果要释放self,至少要做的一件事情是让block不再持有self,显然,上面的这种情况,要不持有slef,只能等待block被销毁。 这时候,block和slef相互等待着对方先被释放,才能释放自己,一直矛盾着两个对象都得不到释放,就会造成循环引用

解决方法
1,OC提供了weak修饰符
我们会申请一个weak self对象参与block的copy使用。

在这里插入图片描述

这样就不会发生警报。

2.使用弱引用和强引用的结合
当使用__weak修饰后,如果外部对象为空了,那么block的内部对象也将为空,这样有时候并不是我们想要的。AFNetWorking中使用了一个办法解决这一问题。就是在block内部继续使用__Strong来修饰带进来的weakself。
在这里插入图片描述

这样做的好处就是避免了循环引用,同时保证对象在block中的持续存在,而不会因为block外的变量因为被释放掉使block内部的变量也为空了。

3.使用__block修饰符

在这里插入图片描述

当一个对象被__block修饰时,在块内部对该对象的引用会变为一个弱引用。这样,在块内部对该对象的使用不会阻止对象的释放,从而打破了循环引用。

需要注意的是,__block修饰符只在块内部起作用,不会改变对象在块之外的引用语义。因此,即使对象被__block修饰,如果在块外部仍然存在其他强引用,循环引用仍然可能发生。因此,使用__block修饰符时,需要综合考虑对象在块内外的引用关系,以避免潜在的循环引用问题。