闭包与Objective-C block | 青训营笔记

499 阅读7分钟

这是我参与「第四届青训营 」笔记创作活动的的第7天。

今天继续整理了上课的闭包与Objective-C block部分,对于内存管理处还有一些疑问,明日继续研究。

闭包与 Objective-C block

闭包基础

闭包的基本定义

  • 闭包(Closure)词法闭包/函数闭包 一个结构体
  • 支持头等函数的编程语言中实现词法绑定的一种技术——具体到实现就是函数既可以成为函数的入参/返回值,也可以当做变量使用的编程语言。词法绑定,这里可以理解为是将一系列符号与对应值的对应关系绑定到指定函数上。
  • 闭包存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用)(可能没有)。
  • 与函数区别

    捕捉闭包时,自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定。

闭包在各个语言中的表现形式

C++ lambda表达式

Python 内部函数形式

Objective-C block

#import <Foundation/Foundation.h>int main(int argc, const char * argv[]) {
 @autoreleasepool {
   int i = 1024;
   //printBlock捕获了一个定义在block外,非入参的局部变量(自由变量),最终将对应的自由变量值打印。
   void (^printBlock)(void) = ^{
     printf("%d\n", i);
  };
   printBlock();
}
 return 0;
}

Block基础

基础概念

  • block是闭包在Objective-C中的实现
  • block可以接受参数也可以有返回值
  • block可以分配在栈和堆上,也可以是全局的。分配到栈上的块可以拷贝到堆中,同标准的Objective-C对象一样,具备引用计数。

标准格式

  • block声明时:组成部分,返回值,block的名称以及block传入的参数.
  • block赋值时,没有block名称这一部分,在对应的位置用返回值代替。
// block声明
returnType (^blockName)(parameters);
// block赋值
^returnType(parameters) {
 // do something;block真正的实现
};
// 🌰
//sumBlock是闭包名
int (^sumBlock)(int a, int b) = ^int(int a, int b) {
 return a + b;
};
int sum = sumBlock(11);//调用sumBlock闭包

常用简写

// 有返回值有入参
int (^sumBlock)(intint) = ^(int a, int b) {
   return a + b;
};
// 无返回值无入参
void (^printBlock)(void) = ^{
   NSLog(@"Hello World!");
};

block的声明,只可以省略非空入参的变量名。

block的赋值,无论是否有返回值,都可以将其省略,编译器会自动根据左侧声明检查对应的返回值是否满足要求,当函数没有入参时,还可以进一步简化为 ^{} 的形式。

typedef returnType (^blockName)(parameters);
​
typedef int (^SumBlock)(int a, int b);
SumBlock block = ^(int a, int b) {
   return a + b;
};

typedef 给常用block声明定义别名的形式来简化.

相当于声明了一种类型,所以blockName在typedef时的命名一般与类的命名相同,首字母大写。

Block内存管理

基础分类

int main(int argc, char * argv[]) {
   @autoreleasepool {
       // __NSGlobalBlock__
//globalBlock因为没有访问任何自动变量会被存储在.data段 NSGlobalBlock
       void(^globalBlock)(void) = ^{
           NSLog(@"Hello, World!");
      };
       NSLog(@"%@", [globalBlock class]);
​
       // __NSStackBlock__
//stackBlock引用了临时变量age会被存储在栈上 NSStackBlock
       int age = 18;
       void(^stackBlock)(void) = ^{
           NSLog(@"Hello, World! %d", age);
      };
       NSLog(@"%@", [stackBlock class]);
​
       // __NSMallocBlock__
//mallocBlock因为手动调用了copy方法被存储在堆上 __NSMallocBlock__。
       void(^mallocBlock)(void) = [stackBlock copy];
​
       NSLog(@"%@", [mallocBlock class]);
  }
   return 0;
}

注意:引入ARC机制,默认情况下系统会自动调用copy操作 栈区block会变成堆区block

  • block作为函数返回值
  • 将block赋值给strong指针
  • block作为某些系统方法参数

变量捕获

值捕获➕名称引用(添加_block➕捕获对象属性)

一些🌰

- (void)changeValue {
   int value = 1;
​
   void (^oneBlock)(void) = ^{
       NSLog(@"value = %d", value); // value1 = 1
  };
//OC是值引用 只取了value的值 没有取对象 block捕获 脱离使用
   value = 2;
   oneBlock();
   NSLog(@"value = %d", value); // value2 = 2
}

oneBlock在初始化的时候,按值捕获了value,而后,当value更改后,block内捕获的值并没有跟着一起变化;而value2处则是使用的改变后的变量。

- (void)changeValue {
   __block int value = 1;
//_block关键字允许被修饰变量在block内部改变 名称引用
   void (^oneBlock)(void) = ^{
       NSLog(@"value = %d", value); // value1 = 2
       value = 3;
  };
​
   value = 2;
   oneBlock();
   NSLog(@"value = %d", value); // value2 = 3
}

传入的变量前增加了一个关键字__block,这个关键字标志了该变量允许在block中被修改。这里block内部对于value的捕获是引用捕获,也就是说block中的value就是外部的value对象,因此对应值的改变也会同步,内外互相影响。

- (void)changeValue {
   NSString *value = @"1";
   void (^oneBlock)(void) = ^{
       NSLog(@"value = %@", value); // value1 = 1
  }
   value = @"2";
   oneBlock();
   NSLog(@"value = %@", value); // value2 = 2
}

取了之前的指针的“值”,当value被重新赋值时,block捕获的是上一个指针值

@property (nonatomiccopyNSString *name;
​
- (void)changeValue {
   self.name = @"1";
   void (^oneBlock)(void) = ^{
       NSLog(@"value = %@"self.name); // value1 = 2
     //oc是动态语言 捕获self 发送消息取name变量 只捕获了self对象
     //self指针没变,在block向self发消息时,它能正确的找到变化后的值
  }
   self.name = @"2";
   oneBlock();
   NSLog(@"value = %@"self.name); // value2 = 2
}

内存泄漏的产生与修复——Block的循环引用

在ARC下,系统会在某些场景默认对block执行copy操作,使其变为NSMallocBlock,此时block也会有自己的内存引用计数。

此代码中,产生循环引用,VC和Block循环引用,内存泄漏

// ViewController.m#import "ViewController.h"@interface ViewController ()@property (nonatomiccopyNSString *name;
@property (nonatomiccopyvoid (^completionBlock)(void);
​
@end@implementation ViewController
​
- (void)viewDidLoad {
  [super viewDidLoad];
   self.completionBlock = ^{
       NSLog(@"%@"self.name);
  };
}
​
@end

循环引用的常规解决方案就是打破引用环,使用weak。

block内没有直接用weakSelf而是手动strong了一下:

⚠️

// ViewController.m#import "ViewController.h"@interface ViewController ()@property (nonatomiccopyNSString *name;
@property (nonatomiccopyvoid (^completionBlock)(void);
​
@end@implementation ViewController
​
- (void)viewDidLoad {
  [super viewDidLoad];
   __weak typeof(self) weakSelf = self;
   self.completionBlock = ^{
       __strong typeof(weakSelf) strongSelf = weakSelf;
       NSLog(@"%@", strongSelf.name);
  };
}
​
@end

另一种比较隐晦的内存泄漏

// ViewController.m#import "ViewController.h"@interface ViewController ()@property (nonatomiccopyNSString *name;
@property (nonatomiccopyvoid (^completionBlock)(void);
​
@end@implementation ViewController
​
- (void)viewDidLoad {
  [super viewDidLoad];
   self.completionBlock = ^{
       NSLog(@"%@", _name);
  };
}
​
@end

block内虽然没有直接引用self,但是_name表示viewController的变量,要找到它就需要持有self,因此形成了隐式的内存泄漏。

Block的应用

数组遍历

🌰

// block作为入参
[object doSomethingWithBlock:^returnType (varType varName) {
   // do something
}];
// 获取数组中大于2的数字的个数
__block NSInteger count = 0;
NSArray<NSNumber *> *array = @[@0, @1, @2, @3, @4, @5];
NSRange range = NSMakeRange(0, array.count);
[array enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:range] options:NSEnumerationReverseusingBlock:^(NSNumber * _Nonnull number, NSUInteger idx, BOOL * _Nonnull stop) {
   if (number.integerValue >= 2) {
       ++count;
  }
}];
NSLog(@"%ld", count); // count:4

网络请求中数据传递

  • iOS常用网络库AFNetWorking中的一个接口,传入url、网络参数以及进度、成功和失败的回调block,三个block就是主要负责数据传递的。
  • 通过对应接口下载图片的函数,对应代码逻辑集中在了一块,可读性高。如果换用Objective-C中的其它方式,比如delegate,需要调用很多方法
// AFHTTPSessionManager.h
​
- (nullable NSURLSessionDataTask *)GET:(NSString *)URLString
                           parameters:(nullable id)parameters
                             progress:(nullable void (^)(NSProgress *downloadProgress)) downloadProgress
                              success:(nullable void (^)(NSURLSessionDataTask *task, id _Nullable responseObject))success
                              failure:(nullable void (^)(NSURLSessionDataTask * _Nullable task, NSError *error))failure;
AFHTTPSessionManager *manager = AFHTTPSessionManager.manager;
   manager.responseSerializer = [AFHTTPResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"image/jpeg"nil];
​
__weak typeof(self) weakSelf = self;
NSString *url = @"<http://www.pptbz.com/pptpic/UploadFiles_6909/201203/2012031220134655.jpg>";
​
// 开始下载
[manager GET:url parameters:nil progress:^(NSProgress *downloadProgress) {
   NSLog(@"progress:%lld",downloadProgress.completedUnitCount);
} success:^(NSURLSessionDataTask *task, id responseObject) {
   NSLog(@"图片下载完成");
   __strong typeof(weakSelf) strongSelf = weakSelf;
   strongSelf.imgView.image = [UIImage imageWithData:(NSData *)responseObject];
} failure:^(NSURLSessionDataTask *task, NSError *error) {
   if (error) {
       NSLog(@"%@",error.userInfo);
  }
}];

链式调用

为了简化代码,提升可读性。假设想要实现简单的四则运算,下面两种方式,后者可读性明显更高一些。

名称加括号的调用方式非常类似于block的调用,唯一需要考虑的就是如何在调用一个block后能够继续调用下一个。

// 常规调用
float a = 0;
a = a + 2;
a = a - 4;
a = a * 5;
a = a / 3;
// 优化调用
Calculator *calculator = Calculator.new;
calculator.add(2).subtract(4).multiply(5).devideBy(3);
NSLog(@"%f", calculator.result);

add操作(其余操作也是相同的代码逻辑),整体通过声明了一个返回一个对象本身的block来实现,链式调用就是self不停地调用block。

// Calculator.h
@interface Calculator : NSObject@property (nonatomicCGFloat result;
​
- (Calculator *(^)(CGFloat addend))add;
​
@end
// Calculator.m
#import "Calculator.h"@implementation Calculator
​
- (Calculator *(^)(CGFloat addend))add {
   return ^(CGFloat addend) {
       self.result += addend;
       return self;
  };
}
​
@end

多线程中调用

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
   // do something
});

小结

Block有几种 区别是什么

对应存储位置不同

全局 NSGlobalBlock

栈 _NSStackBlock _

堆 _NSMallocBlock _

Block有哪些捕获变量的方式

值捕获 引用捕获

Block内存泄漏什么时候发生 怎么打破

循环引用 弱引用打破环