iOS-重回block小白之路

时间:2023-03-09 06:05:30
iOS-重回block小白之路

在我刚刚接触iOS开发的时候,是通过MJ老师讲的OC基础入门的,iOS圈的人应该基本都知道MJ大神吧,即便如此大神,讲解完block之后我依然感觉晕晕乎乎的,直到后来真正进公司做项目,依然感觉这是自己的一个弱项,后来通过不断接触,对它可能有了更多的了解,但是不一定够全面够深入,现在准备通过自己看过的几篇觉得还不错的文章,系统的来总结一下block的使用。不多废话,下面开始:

1、我在平时读他人文章的时候对block常见的描述是匿名函数,再多一些描述就是可以在方法内部使用,也可以在方法外部使用,还能做参数使用。下面看一下它的简单定义方式和使用

 int x = ;
- (void)blockTest10 { int (^myBlock)(int) = ^(int b){
x = ;
return x + b;
};
int result = myBlock();
NSLog(@"%d", result); //
} - (void)blockTest9 {
static int a = ;
int (^myBlock)(int) = ^(int b){
a = ;
return a + b;
};
int result = myBlock();
NSLog(@"%d", result); //
} - (void)blockTest8 {
static int a = ;
int (^myBlock)(int) = ^(int b){
return a + b;
};
a = ;
int result = myBlock();
NSLog(@"%d", result); //
}
- (void)blockTest7 {
__block int a = ; //加上__block前缀,就会传址进去
int (^myBlock)(int) = ^(int b){
// a = 2; //编译不再报错
return a + b;
};
a = ;
int result = myBlock();
//上面 a = 2 注释的情况下打印出的是10,解注释的情况下打印出的是5
NSLog(@"%d", result); } - (void)blockTest6 {
NSMutableArray *mutableArray = [NSMutableArray arrayWithObjects:@"one", @"two", @"three", nil];
int result = ^(int a){
[mutableArray removeLastObject]; //这里是传址而不是传值,因此这行代码会移除成功
return a * a;
}(); NSLog(@"array :%@", mutableArray);
NSLog(@"%d",result); //
} - (void)blockTest5 {
//这段代码你会发现打印值依然未变,这是因为block对a的使用只是a的值的使用,而不是地址的引用,它在内部把a的值5作为常量来使用,因此在外部再改变a的值不会对block内部的a值造成任何影响,并且这时在block内部a是不能改变的,它在这里相当于一个常量。
int a = ;
int (^myBlock)(int) = ^(int b){
// a = 2; //编译报错
return a + b;
};
a = ;
int result = myBlock();
//依然打印出8
NSLog(@"%d", result); } - (void)blockTest4 {
int a = ;
int (^myBlock)(int) = ^(int b){
return a + b;
}; int result = myBlock();
//打印出8
NSLog(@"%d", result); } //square参数的类型是int(^)(int)
- (void)blockTest3:(int(^)(int))square{
NSLog(@"%d",square());
} - (void)blockTest2 {
//声明一个名为square的block,并且返回值和参数都是int型
int (^square)(int);
//为square赋实体,类似函数内部的执行模块
square = ^(int a){
return a * a ;
};
//调用block,这里跟C函数调用一模一样
int result = square();
//这里打印值为25
NSLog(@"%d", result);
}
- (void)blockTest1 {
//直接使用一个block实体来进行计算
//前面说过block某些方面跟函数非常相似,所以在这里
//a相当于参数,return的值相当于返回值,5相当于传入的参数
int result = ^(int a){
return a * a;
}();
NSLog(@"%d",result); //打印值为25
//当然,我们几乎不会这样去使用它
}

以上是十种简单的使用情景,具体的说明在注释里,我想应该足够详细了,反正在这里了解一下基本格式和简单使用就好,平时不会直接这样使用,所以不能说不重要,但不属于精髓部分,这些应该是小白都懂的东西。下面是调用:

 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

 //    [self blockTest1];
// [self blockTest2];
//上面是block的一些简单定义和使用的方式,下面看一下block作为参数的简单使用
// [self blockTest3:^int(int a) { //执行这行代码,会打印出9
// return a * a;
// }];
//如果上面的示例不太明白的话,可以继续往下看,后面会介绍block做参数在实际开发工作中的具体使用场景。 //下面看一下block对外部变量的使用
// [self blockTest4];
//可以把4和5对比来看
// [self blockTest5]; //对象引用,传址的情况
// [self blockTest6]; //如果外部变量不像上面一样是一个对象指针,那该怎么处理呢?
// [self blockTest7]; //静态变量全局只有一份,所以不管在哪里访问改变的都是a本身
// [self blockTest8]; // [self blockTest9]; //全局变量也一样
[self blockTest10];
}

好了,简单的东西简单说,如果你感兴趣还可以去我的github把demo弄下来自己运行看看。这是地址:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/1-block%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8

2、好,继续往下进行,再深入一点,下面是较常用的方式了,比较重要,用block在类之间传递信息

  前面已经说过,block某些方面很像函数(但是block是OC对象),所以定义一个block,相当于定义了一个函数,它既可以定义在方法内部也可以定义在方法外部(定义在外部的时候你可以把它看做一个全局变量),而且调用规则和函数一样,调用的时候才会执行block内部的代码;

  个人感觉从这里开始才真正进入主题,因为像之前那种做法基本没人会那么用,在同一个控制器内定义和使用block,如果是这种需求那就没有必要用block了,你用C函数或OC方法岂不是更方便?block最大的用处本人认为是在不同的类之间传递消息,类似代理,但是使用起来比代理更加灵活简便,但是对掌握不熟练地人来说也很容易出错,这也会让很多人望而生畏,之前的我也是如此,哈哈。

好了,不BB那么多了,看到这就直接去看另外两个demo吧,代理传值和block传值,这两个已经在我之前的博客控制器之间传值的方式总结中讲过了,但是我打算在把代理和block两个demo拷过来,这样可以方便对比一下,并针对block再多加几句话。

第一步:在需要对外传值的控制器声明一个block。

 //一般都会用这种方式声明一个block类型(返回值类型为空,参数类型为字符串)
typedef void (^TestBlock) (NSString *str);

第二步:声明block类型属性。

 //一般使用copy策略,因为在ARC环境下已经不再有存储在栈中的block了,而是在堆中。声明一个TestBlock类型的变量
@property (nonatomic, copy) TestBlock testBlock;

第三步:在你认为需要传值出去的时机调用block,把你想要传递出去的信息传递出去。

    //block传值
//自己确定需要传递信息的时机,这里是返回上一页的时候传值
//用if判断一下是为了安全性,只有block确实存在的时候才会调用,否则会出问题
if(self.testBlock) {
//调用block成员变量
self.testBlock(@"绿色");
}

第四步:调用block的时候会执行block实体,这时候就把消息传过来了。

    //这里是block回传的值
//在这里实现block的实体,并接收调用者传递过来的参数,这就实现了控制器之间传递信息
nextVc.testBlock = ^(NSString *str) {
NSLog(@"%@",str);
};

代理这里就不说咯,自己可以把demo搞下来对比一下,github地址:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/2-block%EF%BC%88%E5%86%8D%E6%B7%B1%E5%85%A5%E4%B8%80%E7%82%B9%EF%BC%89

3、下面进行到第三阶段了,这里着重分析一下block的实现原理,虽然之前我在面试题blog中也简单分析过,但是这里打算放一些更详细一点的,我就直接把唐巧大大的博客链接放到这里了,因为过于底层的东西平时开发是很少会接触到的,但是对于爱刨根问底的我们-程序猿来说,还是很有诱惑力的,所以有兴趣的童鞋可以去看看,没兴趣的就继续往下吧。我在这里只摘抄一些我认为大家都应该知道的一些东西。

1)、闭包

闭包是一个函数(或指向函数的指针),再加上该函数执行的外部的上下文变量(有时候也称作*变量)。block 实际上就是 Objective-C 语言对于闭包的实现。

2)、在 Objective-C 语言中,一共有 3 种类型的 block:

  1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁

3)、NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,因为默认它是当一个 block 被 copy 的时候,才会将这个 block 复制到堆中,目标的 block 类型被修改为 _NSConcreteMallocBlock。

4)、在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。

原本的 NSConcreteStackBlock 的 block 会被 NSConcreteMallocBlock 类型的 block 替代。证明方式是以下代码在 XCode 中,会输出<__NSMallocBlock__: 0x100109960>。在苹果的 官方文档 中也提到,当把栈中的 block 返回时,不需要调用 copy 方法了。

引用文章地址:http://blog.devtang.com/2013/07/28/a-look-inside-blocks/

上面这四条都来自于唐巧的博客,但是后面我还打算补充一点更通俗一点的东西。

5)Block在MRC下的内存管理

block在mrc的情况下,默认是存储在栈中的,因此不需要程序员自己对它做内存管理,即使引用了外部的对象,也不会对该对象的引用计数产生任何影响。

但是,一旦对block进行了copy操作,它便会被移到堆中,这时候便需要对它做内存管理,包括释放以及对它引用对象的释放,因为如果它存在堆中的时候,那么被它引用的对象引用计数会+1,所以这个对象需要释放两次。

 void(^myBlock)() = ^{
NSLog(@"------");
};
myBlock(); Block_copy(myBlock); // do something ... Block_release(myBlock);

那么如何避免当block存在于堆,又对其他对象做了强引用,并且这个对象又对block产生了强引用的情况(比如block内部使用了self)。

 void(^myBlock)() = ^{
NSLog(@"------%@",self.view);
};
myBlock(); Block_copy(myBlock); // do something ... Block_release(myBlock);

由于对block进行了copy操作,这时候self对block是强引用的,那么block内部又对self做了强引用,就会造成强引用循环,造成内存泄漏。解决办法便是:

 __block typeof(self) weakSelf = self;
void(^myBlock)() = ^{
NSLog(@"------%@",weakSelf.view);
};
myBlock(); Block_copy(myBlock); // do something ... Block_release(myBlock);

加入了第一行之后,block便不会再对self做强引用了。

6)Block在ARC下的内存管理

  在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,程序员只需要避免循环引用即可.

 __weak typeof(self) weakSelf = self;
void(^myBlock)() = ^{
NSLog(@"------%@",weakSelf.view);
};
myBlock(); Block_copy(myBlock); // do something ... Block_release(myBlock);

ARC环境下,只需要把__block换成__weak就可以了,还有个什么长长的前缀也可以,但是我忘记了,__weak够了。

为什么在ARC环境下__block不行了呢?因为__block在ARC中并不能禁止block对所引用的对象进行强引用,解决办法可以是在Block中将这个强引用对象置空,但是不推荐这么做。

但是在需要修改外部变量的时候,还是需要使用__block对变量进行修饰才能对变量进行修改的。

同时,在block内部定义的变量,会在作用域结束时自动释放,block对其并没有强引用关系,且在ARC中只需要避免循环引用即可,如果只是block单方面地对外部变量进行强引用,并不会造成内存泄漏。

第三阶段就写这么多,即使写到这里我还是觉得远远没到自己感兴趣的那个程度,只掌握到这个程度的话在实际项目应用方面还是比较菜的,所以这里不多废话了,还有两个小demo,没事的话可以clone下来看一下,或许会有点帮助:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/3-%E7%A8%8D%E7%A8%8D%E6%80%BB%E7%BB%93%E4%B8%80%E4%B8%8B

4、实用篇,写一点实际应用场景。

1、之前提到过,也是最常见的,block做参数。

比如AFN中

 [[AFHTTPSessionManager manager] POST:@"" parameters:params progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//do something
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { }];

Masonry中:(请忽略我见不得人的命名)

 [self.nameLaebl makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.productBtn.top);
make.left.equalTo(self.productBtn.right).offset(nameLabelLeftMargin);
make.right.equalTo(self.right).offset(nameLabelRightMargin);
}];

拿AFN来说吧,AFN对上面方法的实现是这样的:

 - (AFHTTPRequestOperation *)POST:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithHTTPMethod:@"POST" URLString:URLString parameters:parameters success:success failure:failure]; [self.operationQueue addOperation:operation]; return operation;
}

继续向下调用:

 - (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{ dispatch_async(http_request_operation_processing_queue(), ^{
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
id responseObject = self.responseObject;
if (self.error) {
if (failure) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
failure(self, self.error);
});
}
} else {
if (success) {
dispatch_group_async(self.completionGroup ?: http_request_operation_completion_group(), self.completionQueue ?: dispatch_get_main_queue(), ^{
success(self, responseObject);
});
} };
}

这里负责把方法中block参数中的参数传递出去,而在我们使用这个方法的时候,便在block中接收它传递出来的结果,最常用的是responseObject,AFN返回的网络数据就存在这个值中。

而在我们使用POST方法的时候,就把success block内实现的代码块和failure block内的代码块层层传递到了AFN内部函数,然后等待网络请求的回应失败或者成功就调用相应的block,最后把获取的结果通过AFN方法中的block参数中的参数传给我们。

总结一下:先在block内部实现一个代码块,然后在合适的时候调用该block并传入参数,就可以实现对该代码块的调用,达到回调的目的。(不过AFN属于提供给别人用的,所以我理解的是这个顺序:你调用它的接口方法的时候,便实现了它block接口的实体,这样它在内部调用这个block的时候便有了实体,便成功调用参数block把block中的参数传递出来,所以block做参数的时候,一般block自己的参数便是传递信息的核心媒介)block就是一个对象,和OC中其他的对象一样,所以可以被当做参数来传递,区别是block是一个匿名函数,所以你可以调用它实现某些功能。

2、block做返回值

这里我写了个demo,直接看demo的代码吧:

先新建一个Person类:

 #import <Foundation/Foundation.h>

 @interface Person : NSObject

 - (NSString * (^)(NSUInteger))speak;

 - (void (^)(NSUInteger))eat;
@end @implementation Person
- (NSString *(^)(NSUInteger))speak {
return ^ NSString * (NSUInteger a) {
return [NSString stringWithFormat:@"my age is %ld", a];
};
} - (void (^)(NSUInteger))eat {
return ^(NSUInteger a) {
NSLog(@"eat 了 %ld 个鸭梨", a);
};
}
@end

定义两个公共方法供外部调用,返回值都是block类型。

接下来看一下怎么在控制器中使用:

 Person *p = [[Person alloc] init];

 //    NSLog(@"%@",[p speak](5));
//
// [p eat](6); //其实上面这种做法是没有必要的,因为如果你真的想实现这种功能的话不需要多走一层block,但是这里是为了使用点语法实现链式调用,所以应该是下面这么用的
NSLog(@"%@",p.speak()); p.eat(); //是不是感觉有点像Masonry中的链式语法,点语法调用函数,就是调用该函数的的getter方法

OK,下面会讲到链式语法。上面代码的demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/4-%E5%AE%9E%E7%94%A8%E7%AF%87/block%E5%81%9A%E8%BF%94%E5%9B%9E%E5%80%BC

3、block链式语法

先看一个场景:

 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSUInteger a = ,b = ,c = ,d = ,e = ;
NSUInteger result = ;
result = [self add:a b:b];
result = [self add:result b:c];
result = [self add:result b:d];
result = [self add:result b:e];
NSLog(@"%ld",result);
} - (NSUInteger)add:(NSUInteger)a b:(NSUInteger)b {
return a + b;
}

假设定义一个简单的计算两个整数相加之和的方法,当我们需要计算多个数之和的时候,便需要不断的调用,但是链式语法便要简单的多:(这里的举例可能不太合适,因为谁都知道计算两数之和没必要单独写个方法,这里只是为了说明链式语法的优点)

 NSUInteger result = [NSObject makeCalculate:^(CalculateManager *mgr) {
mgr.add(a).add(b).add(c).add(d).add(e);
}];

好了,下面就来看一下具体的实现过程是怎么样的:

先新建一个计算管理类:

 @interface ATCalcManager : NSObject
@property (nonatomic, assign) NSUInteger result; - (ATCalcManager *(^)(NSUInteger s))add; @implementation ATCalcManager
- (ATCalcManager *(^)(NSUInteger s))add {
return ^ATCalcManager *(NSUInteger x) {
self.result += x;
return self;
};
}
@end

再新建一个NSObject的分类:

 @class ATCalcManager;
@interface NSObject (ATCalc)
+ (NSUInteger)makeCalc:(void(^)(ATCalcManager *mgr))block;
@end @implementation NSObject (ATCalc)
+ (NSUInteger)makeCalc:(void(^)(ATCalcManager *mgr))block {
ATCalcManager *mgr = [[ATCalcManager alloc] init];
block(mgr); //计算
return mgr.result;
}

在控制器中使用:

  NSUInteger a = ,b = ,c = ,d = ,e = ;
NSUInteger result = ; result = [NSObject makeCalc:^(ATCalcManager *mgr) {
mgr.add(a).add(b).add(c).add(d).add(e);
}];
NSLog(@"%ld",result); //15

总结:1、先看控制器中的这行调用

 result = [NSObject makeCalc:^(ATCalcManager *mgr) {
mgr.add(a).add(b).add(c).add(d).add(e);
}];

这个方法的参数是一个block,我们在这里定义这个block的参数实体,也就是我们想实现的链式语法。

2、Command + 鼠标左键进去看这个函数的具体实现,该方法内部初始化一个ATCalcManager实例对象mgr,然后作为block的参数传入block,然后调用该block,回调了我们上一步实现的block实体。

3、接下来执行的链式调用代码mgr.add(a).add(b).add(c).add(d).add(e);,可以看到add方法返回的是一个block,该block的实现是累加传递进来的值然后赋值给属性result保存下来,然后返回值是self,也就是ATCalcManager实例对象。这样又可以实现点语法继续调用add方法,最后return mgr.result;返回计算结果。

最后,实现链式调用的一个关键点:就是每次调用add方法必须返回自身,然后才可以继续调用,如此一致循环下去,实现这一切都是block的功劳。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/4-%E5%AE%9E%E7%94%A8%E7%AF%87/block%E9%93%BE%E5%BC%8F%E8%AF%AD%E6%B3%95

4、block实现函数式编程

先看个示例:

 [[[[mgr calculate:^NSUInteger(NSUInteger result) {
result += ;
return result;
}] printResult:^(NSUInteger result) {
NSLog(@"第一次计算结果为:%ld",result);
}] calculate:^NSUInteger(NSUInteger result) {
result -= ;
return result;
}] printResult:^(NSUInteger result) {
NSLog(@"第二次计算结果为:%ld",result);
}];

计算和打印循环套用,逻辑过程清晰的连在一起,而且不需要中间变量。

下面看如何实现:

新建一个ATCalcManager类

 @interface ATCalcManager : NSObject
@property (nonatomic, assign) NSUInteger result;
- (instancetype)calculate:(NSUInteger(^)(NSUInteger result))calculateBlock;
-(instancetype)printResult:(void(^)(NSUInteger result))printBlock;
@end @implementation ATCalcManager
- (instancetype)calculate:(NSUInteger (^)(NSUInteger result))calculateBlock
{
_result = calculateBlock(_result);
return self;
} -(instancetype)printResult:(void(^)(NSUInteger result))printBlock{
printBlock(_result);
return self;
}
@end

和链式编程一样,上面两个函数的关键点仍然在于每次都必须返回self,这样才可以继续嵌套调用其他函数。函数的内部实现是做一些内部处理,然后传入参数来调用block。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/4-%E5%AE%9E%E7%94%A8%E7%AF%87/block%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B

5、block保存代码块

首先说一下回调的概念,直接从大神那里拿过来了,简单易懂的例子:你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。

然后看一下具体场景中的应用,一个很常见的需求:

在tableview的每行cell上都有一个按钮,你需要在这个按钮被点击的时候处理这个动作,但是这个动作显然不适合在view中解决,这时就需要借助block回调来传递事件。

1、首先在cell视图中定义一个block属性

 @property(copy, nonatomic) void (^callBack)(ATModel *);

2、在cell的实现文件中监听按钮点击

 [btn addTarget:self action:@selector(btnClick:) forControlEvents:UIControlEventTouchUpInside];

 //...

 - (void)btnClick:(UIButton *)btn {
if (self.callBack) {
self.callBack(self.model);
}
}

3、在cellForRowAtIndexPath方法中为block赋实体

 cell.callBack = ^(ATModel *model) {
NSLog(@"%ld---%@",indexPath.row,model.name);
};

这样你想要的点击rowIndex和对应的model都很容易的获取到了。

如果还不明白,直接运行一下demo,看一下输出就明白了。

demo:https://github.com/alan12138/Classification-of-knowledge-points/tree/master/block%20iOS/4-%E5%AE%9E%E7%94%A8%E7%AF%87/block%E4%BF%9D%E5%AD%98%E4%BB%A3%E7%A0%81%E5%9D%97

总结:

做个总结吧,block有时候理解起来确实不那么顺,老感觉有点别扭有点绕,但是它也不过是个普通的OC对象而已,用习惯了就好了。最后推荐一个计算并缓存cellHeigth的小分类工具代码,很惭愧非原创,是我从大牛哪里学来的,做了一点微不足道的改动自己就用上了,哈哈,不过感觉里面的block使用的很值得看看,毕竟是实实在在在实际项目中应用的,而不是简单的demo简单介绍一下的就能比拟效果。代码在这里,有兴趣可以看看:https://github.com/alan12138/Tools/tree/master/rowHeightTest