Block代码块的一点讲解

一、概述

block

上图就是一个block简单使用,它包括了block的声明赋值实现调用 三个部分,其中,实现部分可以看作是一种匿名函数;跟函数一样,block也是需要调用才能执行内部代码的;赋值的行为又让block看起来跟数据类型类似

代码块Block是在iOS4开始引入的,是对C语言的扩展,用来实现匿名函数的特性

Block是一种特殊的数据类型,可以像基本数据类型一样定义成变量、作为参数、返回值来使用

Block还可以保存一段代码,在需要的时候调用

在iOS开发中,Block被系统应在很多地方,例如:GCD、UIView动画、排序等,我们开发者也可以应用在各类回调、传值、传消息等


二、Block的声明、赋值实现、调用

Block的声明样式:

返回类型 (^Block名称)(参数列表);

例:

void(^myBlock)(NSString *, NSString *)

Block的返回类型分为有返回类型和无返回类型(void),参数列表也可有也可以没有,具体看需求

// 无返回类型无参数列表
void(^block)();
// 无返回类型有参数列表
void(^block)(int);
// 有返回类型无参数列表
int(^block)();
// 有返回列表有参数列表
int(^block)(int);

Block的简单使用:声明、赋值、调用

Block变量的赋值格式为:

Block变量 = ^(参数列表){函数体}; // 这里的参数列表一定要和声明时的参数列表一致

// 声明
void(^block)();
// 赋值
block = ^(){
    NSLog(@"Hello World");
};
// 调用
block();

也可以在声明时完成赋值

// 声明、赋值
void(^block)() = ^(){
    NSLog(@"Hello World");
};
// 调用
block();

定义Block类型

前面提到过,Block是一种特殊的数据类型,我们可以使用typedef来定义Block类型,这样我们就可以使用该类型来声明很多相同的(返回类型和参数列表一致)Block变量了

typedef 返回类型(^Block名称)(参数列表);

例:

// 声明一个Block类型
typedef void(^Block)();

// 声明
Block myBlock,myNewBlock;
// 赋值
myBlock = ^(){
    NSLog(@"Hello World");
};
myNewBlock = ^(){
    NSLog(@"Hello World, I am lolita0164.");
};
// 调用
myBlock();
myNewBlock();

三、简单应用,ARC模式下

1、作为对象属性实现数据传递

前面说到,Block保存一段代码,在需要的时候调用。我们可以将使用Block的三个步骤拆开,实现消息传递、传值功能

首先我们定义一个Person类

Person.h 文件

#import 
// 定义Block类型
typedef void(^Block)(NSString *);

@interface Person : NSObject
// 声明Block变量
@property (nonatomic,copy) Block myBlock;

-(void)sayHello;
@end

Person.m 文件

#import "Person.h"
@implementation Person
-(void)sayHello{
    // Block调用
    self.myBlock(@"Hello, I am lolita0164");
}
@end

在主函数中

#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person new];
        // Block赋值实现
        p.myBlock = ^(NSString *string) {
            NSLog(@"%@",string);
        };
        [p sayHello];
    }
    return 0;
}

结果

结果

这样,在主程序中,我们就可以在Block的赋值实现部分里拿到p类里的数据了

2、作为函数参数实现数据回调

我们将之前的例子稍加改动

Person.h 文件

#import 
// 定义Block类型
typedef void(^Block)(NSString *);
@interface Person : NSObject
// 将Block作为参数
-(void)sayHelloUseBlock:(Block)myBlock;
@end

Person.m 文件

#import "Person.h"
@implementation Person
-(void)sayHelloUseBlock:(Block)myBlock{
    // Block调用
    myBlock(@"Hello, I am lolita0164");
}
@end

在主程序中

#import 
#import "Person.h"
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *p = [Person new];
        // Block实现
        [p sayHelloUseBlock:^(NSString *string) {
            NSLog(@"%@",string);
        }];
    }
    return 0;
}

结果和属性传递一样的

注:在使用自定义的Block中,要特别注意循环引用而导致内存泄漏的问题;该问题的产生原因以及解决方案会在后面介绍

3、作为返回值实现链式语法

使用过著名的约束框架masonry的人,想必都属性它的语法。masonry是一种典型的基于Block作为返回值的约束框架,通过Block在设置完部分约束之后再将类本身回调出来继续设置其他约束。下面我们通过一个简单的示例来演示Block作为返回值使用。

/// 十六进制颜色
+(UIColor* (^)(NSInteger))colorWithHex{
    return ^UIColor (NSInteger rgbValue) {
        return [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0];
    };
}

上述方法中的 UIColor* (^)(NSInteger) 是一个标准的有返回值UIColor,参数为NSInterger的Block,它被作为来返回值,在该方法体中,最外层的return返回了Block的实现部分,那么此时Block的声明、赋值实现、调用三要素中还差一个调用部分,这部分是由使用者在外部完成的:

UIColor.colorWithHex(0xdddddd);

当你调用colorWithHex时,实际上拿到了返回值Block,这时候你可以直接加上()给其传递需要的参数,完成调用部分,有几个参数就需要传递几个参数并且需要一一对应。

masonry不同,我们这里并没有做完一些操作(如约束)后再次回调自身而是根据需求回调了最终的结果。


四、Block访问局部变量问题

  • 在Block中可以访问局部变量
int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
Block(); // 输出 "global = 100"
  • block会把变量复制为自己私有的const变量,也就是说block会捕获栈上的变量(或指针),将其复制为自己私有的const变量,当变量被修改时,不会影响到block自己私有的const变量
int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block(); // 输出 "global = 100"
  • 在Block中不可以直接修改局部变量
int global = 100;
void (^Block)() = ^(){
    global ++; // 这句报错
    NSLog(@"global = %i", global);
};
Block();

__block 修饰符 修饰后局部变量

__block int global = 100;
void (^Block)() = ^(){
    NSLog(@"global = %i", global);
};
global = 101;
Block();   //输出 "global = 101"
__block int global = 100;
void (^Block)() = ^(){
    global ++; // 这句正确
    NSLog(@"global = %i", global);
};
Block();   //输出 "global = 101"

原因:在局部变量前使用__block修饰,在Block定义时便是将局部变量的指针传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时内部的值也是可以修改的

Block访问全局变量、静态变量问题

全局变量所占用的内存只有一份,供所有函数共同调用,在Block定义时并未将全局变量的值或者指针传给Block变量所指向的结构体,因此在调用Block之前对局部变量进行修改会影响Block内部的值,同时内部的值也是可以修改的

在Block定义时便是将静态变量的指针传给Block变量所指向的结构体,因此在调用Block之前对静态变量进行修改会影响Block内部的值,同时内部的值也是可以修改的


五、Block在ARC下的内存管理

  • 在ARC默认情况下,Block的内存存储在堆中,ARC会自动进行内存管理,我们只需要避免循环引用即可
// 当Block变量出了作用域,Block的内存会被自动释放
void(^myBlock)() = ^{
    NSLog(@"------");
};
myBlock();
  • 在Block的内存存储在堆中,如果在Block中引用了外面的对象,会对所引用的对象进行强引用,但是在Block被释放时会自动去掉对该对象的强引用,因此比并不会造成内存泄漏问题
Person *p = [[Person alloc] init];
void(^myBlock)() = ^{
    NSLog(@"------%@", p);
};
myBlock();
// Person对象在这里可以正常被释放
// 注:这里的Block只是单方面的强引用,所以不会产生循环引用,也不会内存泄漏
  • 如果对象内部有一个Block属性,而在Block内部又访问了该对象,那么会造成循环引用,导致内存泄漏

Person.m 文件

@interface Person : NSObject
@property (nonatomic, copy) void(^myBlock)(); // person类强引用Block
@end
@implementation Person
- (void)dealloc{
    NSLog(@"Person dealloc");
}
@end

Person类在Block内强引用了自己,这时候产生了循环引用,不能被释放

#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *p = [[Person alloc] init];
        p.block = ^{
            NSLog(@"------%@", p);
        };
        p.block();
    }
    return 0;
}

上述问题解决办法

解决循环引用的办法就是使用一个弱引用的指针指向该对象,然后在Block内部使用该弱引用指针来进行操作,这样就避免了BLock对对象进行强引用

#import 
#import "Person.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        Person *p = [[Person alloc] init];
        __weak typeof(p) weakP = p;
        p.block = ^{
            NSLog(@"------%@", weakP);
        };
        p.block();
    }
    return 0;
}

这时候p就可以正常释放了

补充:

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


六、扩展,仿写系统数组block排序

在iOS开发过程中,我们可能会遇到给对象数组排序的问题,系统也提供了以下方法来解决问题

- (NSArray *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr;

其中:NSComparator是一个Block类型

typedef NSComparisonResult (^NSComparator)(id obj1, id obj2);

接下来就使用Block仿写这种排序方式

首先我们来新建NSArray的分类,为此添加一个带有Block参数的排序方法,并仿照系统定义一个枚举,作为Block的返回值,也是我们进行排序的依据

类声明部分

#import 

typedef NS_ENUM(NSInteger, NSComparisonResult_LOLITA) {
    NSOrderedAscending_LOLITA = -1L, // 表示左边对象小于右边的对象 ,不交换
    NSOrderedSame_LOLITA,   // 两者相等,不交换
    NSOrderedDescending_LOLITA, // 左边对象大于右边的对象, 交换
};

@interface NSArray (sortArray)

-(NSArray *)sortedArrayUsingComparator_LOLITA:(NSComparisonResult_LOLITA(^)(id obj1, id obj2))Comparison;// Block的声明部分

@end

类实现部分

#import "NSArray+sortArray.h"

@implementation NSArray (sortArray)

-(NSArray *)sortedArrayUsingComparator_LOLITA:(NSComparisonResult_LOLITA (^)(id, id))Comparison{
    
    NSMutableArray *tmpArray = [NSMutableArray arrayWithArray:self];
    
    for (int i=0; i

注:系统的sortedArrayUsingComparator这个方法本身就是按递增的方式排序,他的枚举型(NSOrderedAscending 不交换,NSOrderedSame 不交换,NSOrderedDescending 交换),所以除了NSOrderedDescending做了交换处理之外,另外两个根本没做处理

这样我们就可以使用我们自定义的排序方法了

给Person类添加一个年龄属性,我们根据这个年龄来排序

#import 
@interface Person : NSObject
@property (assign ,nonatomic) int age;
@end

在主程序中

#import 
#import "Person.h"
#import "NSArray+sortArray.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Person *p1,*p2,*p3,*p4;
        p1 = [Person new];
        p2 = [Person new];
        p3 = [Person new];
        p4 = [Person new];
        p1.age = 10;
        p2.age = 20;
        p3.age = 5;
        p4.age = 12;
        NSArray *array = @[p1,p2,p3,p4];
        array = [array sortedArrayUsingComparator_LOLITA:^NSComparisonResult_LOLITA(id obj1, id obj2) { // block的实现部分
            Person *p1 = obj1;
            Person *p2 = obj2;
            if (p1.age > p2.age) {
                return NSOrderedDescending_LOLITA;
            }
            return NSOrderedSame_LOLITA; // 或者 NSOrderedAscending_LOLITA  都是一样的
            
        }];

        for (Person *p in array) {
            NSLog(@"年龄:%i\n",p.age);
        }
    }
    return 0;
}

运行结果

Block代码块的一点讲解_第1张图片
结果

如果你想将序,只需将Block内部实现改为

if (p1.age < p2.age) {  // 这里更改
    return NSOrderedDescending_LOLITA;
}
return NSOrderedSame_LOLITA; // 或者 NSOrderedAscending_LOLITA  都是一样的

Block代码块的一点讲解_第2张图片
结果

在上述例子中,Block的使用三步骤依旧被我们拆开,我们在排序方法里进行了Block的声明和调用操作,在使用排序方法时,才赋值实现该Block

抛砖引玉,你也可以类似的去仿写一下系统的其他使用到Block的方法,例如GCD、UIView动画块等,快去试试吧


七、补充

1、声明block属性的时候为什么用copy呢?

在说明为什么要用copy前,先思考下block是存储在栈区还是堆区呢?其实block有3种类型:

  1. 全局块(_NSConcreteGlobalBlock)
  2. 栈块(_NSConcreteStackBlock)
  3. 堆块(_NSConcreteMallocBlock)

全局块存储在静态区(也叫全局区),相当于OC中的单例;栈块存储在栈区,超出作用域则马上被销毁。堆块存储在堆区中,是一个带引用计数的对象,需要自行管理其内存

关于内存分配,请看这篇:C语言内存分配

怎么判断一个block所在的存储位置呢?

  1. block不访问外界变量(包括栈中和堆中的变量)
    block既不在栈中也不在堆中,此时就为全局块,ARC和MRC下都是如此
  2. block访问外面变量
    MRC环境下:默认存储在栈区
    ARC环境下:默认存储在堆中,实际上是先放在栈区,在ARC情况下自动又拷贝到堆区,自动释放

因此,使用copy修饰符的作用就是将block从栈区拷贝到堆区

为什么要这么做呢?官方给出的答案是:

复制到堆区的主要目的就是保存block的状态,延长其声明周期。因为block如果在栈上的话,其所属的变量作用域结束,该block就被释放掉了,block中的__block变量也同时被释放掉了,为了解决超出作用域就被释放的问题,我们就需要把block复制到堆中


八、参考文章

  1. 一篇文章看懂iOS代码块Block
  1. iOS进阶(一)block与property

你可能感兴趣的:(Block代码块的一点讲解)