2021-07-27

Objective-c——block

Blocks是C语言的扩充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了这个新功能“Blocks”。从那开始,Block就出现在iOS和Mac系统各个API中,并被大家广泛使用。一句话来形容Blocks,带有自动变量(局部变量)的匿名函数。

Block基础部分

1.Block的声明

Block的定义和函数的声明差不多,就是把函数名改成(^blockName)即可。下面是block声明的代码。

(1) 有返回值的

int (^sumBlock) (int, int);

(2) 无返回值的

void (^myBlock)(int, int);

2.给block块赋值

给声明好的block,赋值。block的值就是个函数体,给block块赋值有两种方式,一个在声明的时候赋值,一个是先声明在赋值。

(1) 先声明再赋值

//代码块的声明
void (^myBlock)(int, int);
 
//给代码块赋值
myBlock = ^(int a, int b)
{
    //test ++;  //报错
    NSLog(@"main_test = %d", test);
 
    //blockVar++不报错;
    blockVar ++;
    NSLog(@"blockVar = %d", blockVar);
 
    int sum = a + b;
    NSLog(@"a + b = %d", sum);
};

(2) 在声明的时候赋值

int (^sumBlock) (int, int) = ^(int a, int b)
{
    int sum = a + b;
    return sum;
};

3.调用block

block的使用和普通函数的使用相同,调用方法如下

//调用代码块并接收返回值
int sum = sumBlock(20, 30);

4.把block当做参数传入函数

//把代码块作为函数参数
void blockFunction(int (^myBlock)(int, int))
{
    int sum = myBlock(10,20);
    NSLog(@"fun_sum = %d", sum);
}

5.在代码块中使用局部变量和全局变量

在block中可以和对全局变量进行访问和修改,但对局部变量只可以访问,若想修改的话,我们可以在声明局部变量的时候加上关键字__block

代码如下:

__block int blockVar = 0;

block 在objective-c中的实现:

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables. */
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

Block的捕获外部变量的特性以及__block的实现原理

c语言中变量分为哪几种:
自动变量、函数参数、静态变量、静态全局变量、全局变量

自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量

根据官方文档我们可以了解到,苹果要求我们在自动变量前加入 __block关键字(__block storage-class-specifier存储域类说明符),就可以在Block里面改变外部自动变量的值了。

总结一下在Block中改变变量值有2种方式,一是传递内存地址指针到Block中,二是改变存储区方式(__block)

block本质上是一个OC对象,它内部也有isa指针,这个对象封装了函数调用地址以及函数调用环境(函数参数、返回值、捕获的外部变量等)。
当定义了一个block,并在block里面访问量block外面的变量age,它底层存储结构如下图所示,block底层就是一个结构体__main_block_impl_0。

在这里插入图片描述

  • impl->isa:就是isa指针,可见它就是一个OC对象。
  • impl->FuncPtr:是一个函数指针,也就是底层将block中要执行的代码封装成了一个函数,然后用这个指针指向那个函数。
  • Desc->Block_size:block占用的内存大小。
  • age:捕获的外部变量age,可见block会捕获外部变量并将其存储在block的底层结构体中

当我们调用block时(block()),实际上就是通过函数指针FuncPtr找到封装的函数并将block的地址作为参数传给这个函数进行执行,把block传给函数是因为函数执行中需要用到的某些数据是存在block的结构体中的(比如捕获的外部变量)。如果定义的是带参数的block,调用block时是将block地址和block的参数一起传给封装好的函数。

block变量的捕获机制:

block外部的变量是可以被block捕获的,这样就可以在block内部使用外部的变量了。不同类型的变量的捕获机制是不一样的。看一个示例:

int c = 1000; // 全局变量
static int d = 10000; // 静态全局变量

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

        int a = 10; // 局部变量
        static int b = 100; // 静态局部变量
        void (^block)(void) = ^{
             NSLog(@"a = %d",a);
             NSLog(@"b = %d",b);
             NSLog(@"c = %d",c);
             NSLog(@"d = %d",d);
         };
         a = 20;
         b = 200;
         c = 2000;
         d = 20000;
         block();
    }
    return 0;
}

// ***************打印结果***************
2020-01-07 15:08:37.541840+0800 CommandLine[70672:7611766] a = 10
2020-01-07 15:08:37.542168+0800 CommandLine[70672:7611766] b = 200
2020-01-07 15:08:37.542201+0800 CommandLine[70672:7611766] c = 2000
2020-01-07 15:08:37.542222+0800 CommandLine[70672:7611766] d = 20000

定义的这个block的在编译后底层存储结构

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
};

定义了4个变量,结果只有2个局部变量被捕获了,而且2个局部变量的捕获方式还不一样

全局变量的捕获

不管是普通全局变量还是静态全局变量,block都不会捕获。因为全局变量在哪里都可以访问,所以block内部不捕获也是可以直接访问全局变量的,所以外部更改全局变量的值时,block内部打印的就是最新更改的值。

静态局部变量的捕获

我们发现定义的静态局部变量b被block捕获后,在block结构体里面是以int *b;的形式来存储的,也就是说block其实是捕获的变量b的地址,block内部是通过b的地址去获取或修改b的值,所以block外部更改b的值会影响block里面获取的b的值,block里面更改b的值也会影响block外面b的值。所以上面会打印b = 200。

普通局部变量(自动变量)的捕获

所谓的普通局部变量就是在一个函数或代码块中定义的类似int a = 10;的变量,它其实是省略了auto关键字,等价于auto int a = 10,所以也叫auto变量。和静态局部变量不同的是,普通局部变量被block捕获后再block底层结构体中是以int a;的形式存储,也就是说block捕获的其实是a的值(也就是10),并且在block内部重新定义了一个变量来存储这个值,这个时候block外部和里面的a其实是2个不同的变量,所以外面更改a的值不会影响block里面的a。所以打印的结果是a = 10。

为什么普通局部变量要捕获值,跟静态局部变量一样捕获地址不行吗?是的,不行。因为普通局部变量a在出了大括号后就会被释放掉了,这个时候如果我们在大括号外面调用这个block,block内部通过a的指针去访问a的值就会抛出异常,因为a已经被释放了。而静态局部变量的生命周期是和整个程序的生命周期是一样的,也就是说在整个程序运行过程中都不会释放b,所以不会出现这种情况。

既然静态局部变量一直都不会被释放,那block为什么还要捕获它,直接拿来用不就可以了吗?这是因为静态局部变量作用域只限制在这个大括号类,出了这个大括号,虽然它还存在,但是外面无法访问它。而前面已经介绍过,block里面的代码在底层是被封装成了一个函数,那这个函数肯定是在b所在的大括号外面,所以这个函数是无法直接访问到b的,所以block必须将其捕获。

block捕获变量小结

  • 全局变量–不会捕获,是直接访问。
  • 静态局部变量–是捕获变量地址。
  • 普通局部变量–是捕获变量的值。

block 的类有三种:NSGlobalBlockNSStackBlockNSMallocBlock

1、NSGlobalBlock
如果一个block里面没有访问普通局部变量(也就是说block里面没有访问任何外部变量或者访问的是静态局部变量或者访问的是全局变量),那这个block就是__NSGlobalBlock__。__NSGlobalBlock__类型的block在内存中是存在数据区的(也叫全局区或静态区,全局变量和静态变量是存在这个区域的)。__NSGlobalBlock__类型的block调用copy方法的话什么都不会做
_NSGlobalBlock__的继承链为:NSGlobalBlock : __NSGlobalBlock : NSBlock : NSObject。

2、NSStackBlock

如果一个block里面访问了普通的局部变量,那它就是一个__NSStackBlock__,它在内存中存储在栈区,栈区的特点就是其释放不受开发者控制,都是由系统管理释放操作的,所以在调用__NSStackBlock__类型block时要注意,一定要确保它还没被释放。如果对一个__NSStackBlock__类型block做copy操作,那会将这个block从栈复制到堆上。
__NSStackBlock__的继承链是:NSStackBlock : __NSStackBlock : NSBlock : NSObject。

3、NSMallocBlock

一个__NSStackBlock__类型block做调用copy,那会将这个block从栈复制到堆上,堆上的这个block类型就是__NSMallocBlock__,所以__NSMallocBlock__类型的block是存储在堆区。如果对一个__NSMallocBlock__类型block做copy操作,那这个block的引用计数+1。
__NSMallocBlock__的继承链是:NSMallocBlock : __NSMallocBlock : NSBlock : NSObject

在ARC环境下,编译器会根据情况,自动将栈上的block复制到堆上。有一下4种情况会将栈block复制到堆上:

  1. block作为函数返回值时:
typedef void (^MyBlock)(void);

 1. (MyBlock)createBlock{
    int a = 10;
    return ^{
        NSLog(@"******%d",a);
    };
}
  1. 将block复制给强指针时:

将定义的栈上的block赋值给强指针myBlock,就变成了堆block。

  1. 当block作为参数传给Cocoa API时:
  2. block作为GCD的API的参数时:

另外在MRC环境下,定义block属性建议使用copy关键字,这样会将栈区的block复制到堆区

@property (copy,nonatomic) void(^block)void;

在ARC环境下,定义block属性用copy或strong关键字都会将栈区block复制到堆上,所以这两种写法都可以。

@property (strong, nonatomic) void (^block)(void); 
@property (copy, nonatomic) void (^block)(void);

block对对象型的局部变量的捕获

block对对象类型和对基本数据类型变量的捕获是不一样的,对象类型的变量涉及到强引用和弱引用的问题,强引用和弱引用在block底层是怎么处理的呢?
如果block是在栈上,不管捕获的对象时强指针还是弱指针,block内部都不会对这个对象产生强引用。所以主要来看下block在堆上的情况。

首先来看下强引用的对象被block捕获后在底层结构体中是如何存储的。这里用下面这条命令来将OC代码转成c/c++代码

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

// OC代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        void (^block)(void) = ^{
            NSLog(@"age--- %ld",person.age);
         };
        block();

    }
    return 0;
}

// 底层结构体
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
};

可以看到和基本数据类型不同的是,person对象被block捕获后,在结构体中多了一个修饰关键字__strong。

弱引用对象被捕获后是什么样的:

// OC代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        Person *person = [[Person alloc] init];
        person.age = 20;

        __weak Person *weakPerson = person;
        void (^block)(void) = ^{
            NSLog(@"age--- %ld",weakPerson.age);
         };
        block();

    }
    return 0;
}

// 底层block
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
};

可见此时block中weakPerson的关键字变成了__weak。

在block中修饰被捕获的对象类型变量的关键字除了__strong、__weak外还有一个__unsafe_unretained。那这结果关键字起什么作用呢?
当block被拷贝到堆上时是调用的copy函数,copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数就会根据这3个关键字来进行操作。

如果关键字是__strong,那block内部就会对这个对象进行一次retain操作,引用计数+1,也就是block会强引用这个对象。也正是这个原因,导致在使用block时很容易造成循环引用。
如果关键字是__weak或__unsafe_unretained,那block对这个对象是弱引用,不会造成循环引用。所以我们通常在block外面定义一个__weak或__unsafe_unretained修饰的弱指针指向对象,然后在block内部使用这个弱指针来解决循环引用的问题。

block从堆上移除时,则会调用block内部的dispose函数,dispose函数内部调用_Block_object_dispose函数会自动释放强引用的变量。

block修饰符的作用:

- (void)test{
    int age = 10;
    void (^block)(void) = ^{
        age = 20;
    };
}

上面代码编译器会直接报错,在block中不可以修改这个age的值。

因为age是一个局部变量,它的作用域和生命周期就仅限在是test方法里面,而前面也介绍过了,block底层会将大括号中的代码封装成一个函数,也就相当于现在是要在另外一个函数中访问test方法中的局部变量,这样肯定是不行的,所以会报错。
如果我想在block里面更改age的值要怎么做呢?我们可以将age定义成静态局部变量static int age = 10;。虽然静态局部变量的作用域也是在test方法里面,但是它的生命周期是和程序一样的,而且block捕获静态局部变量实际是捕获的age的地址,所以block里面也是通过age的地址去更改age的值,所以是没有问题的。
但我们并不推荐这样做,因为静态局部变量在程序运行过程中是不会被释放的,所以还是要尽量少用。那还有什么别的方法来实现这个需求呢?这就__block关键字。

当我们用__block关键字修饰后,底层到底做了什么让我们能在block里面访问age呢

 struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
};

struct __Block_byref_age_0 {
  void *__isa; // isa指针
__Block_byref_age_0 *__forwarding; // 如果这block是在堆上那么这个指针就是指向它自己,如果这个block是在栈上,那这个指针是指向它拷贝到堆上后的那个block
 int __flags;
 int __size; // 结构体大小
 int age; // 真正捕获到的age
};

我们可以看到,age用__block修饰后,在block的结构体中变成了__Block_byref_age_0 *age;,而__Block_byref_age_0是个结构体,里面有个成员int age;,这个才是真正捕获到的外部变量age,实际上外部的age的地址也是指向这里的,所以不管是外面还是block里面修改age时其实都是通过地址找到这里来修改的。

所以age用__block修饰后它就不再是一个test1方法内部的局部变量了,而是被包装成了一个对象,age就被存储在这个对象中。之所以说是包装成一个对象,是因为__Block_byref_age_0这个结构体的第一个成员就是isa指针。

__block修饰变量的内存管理:

__block不管是修饰基础数据类型还是修饰对象数据类型,底层都是将它包装成一个对象(我这里取个名字叫__blockObj),然后block结构体中有个指针指向__blockObj。既然是一个对象,那block内部如何对它进行内存管理呢?
当block在栈上时,block内部并不会对__blockObj产生强引用。
当block调用copy函数从栈拷贝到堆中时,它同时会将__blockObj也拷贝到堆上,并对__blockObj产生强引用。
当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部又会调用_Block_object_dispose函数来释放__blockObj。

block的循环引用:

解决循环引用:

  1. weak-strong-dance(最常用的方法)
  2. __block修饰对象,同时置nil
  3. 传递对象self作为block的参数,提供给block内部使用
  4. 使用NSProxy
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值