Objective-C内存管理:Block

总述

如下环境都在ARC环境下,常规设置,使用XCode10测试。html

这篇文章会解决如下几个问题:

  1. Block做为属性声明时为何都声明为Copy?git

  2. Block为何能保存外部变量?程序员

  3. Block中__block关键字为什么能同步Block外部和内部的值?github

  4. Block有几种类型?objective-c

  5. 何时栈上的Block会复制到堆?编程

  6. Block的循环引用应该如何处理?api

  7. Block外部__weak typeof(self) weakSelf = self; Block 内部 typeof(weakSelf) strongSelf = weakSelf;,为何须要这样操做?数组

Block测试:

如下Block在ARC环境下能正常运行吗?若能分别打印什么值?闭包

void exampleA_addBlockToArray(NSMutableArray*array) {
    char b = 'A';
 
    [array addObject:^{
        printf("%c\n", b);
    }];
}

void exampleA() {
    NSLog(@"---------- exampleA ---------- \n");
    
    NSMutableArray *array = [NSMutableArray array];
    exampleA_addBlockToArray(array);
    void(^block)(void) = [array objectAtIndex:0];
    block(); 
}
复制代码
void exampleB_addBlockToArray(NSMutableArray *array) {
    [array addObject:^{
        printf("B\n");
    }];
}

void exampleB() {
    NSLog(@"---------- exampleB ---------- \n");
    
    NSMutableArray *array = [NSMutableArray array];
    exampleB_addBlockToArray(array);
    void(^block)(void) = [array objectAtIndex:0];
    block(); 
}
复制代码
typedef void(^cBlock)(void);
cBlock exampleC_getBlock() {
    char d = 'C';
    return^{
        printf("%c\n", d);
    };
}

void exampleC() {
    NSLog(@"---------- exampleC ---------- \n");
    
    cBlock blk_c = exampleC_getBlock();
    blk_c();  
}
复制代码
NSArray* exampleD_getBlockArray() {
    int val = 10;
    
    return [[NSArray alloc] initWithObjects:^{NSLog(@"blk1:%d",val);}, ^{NSLog(@"blk0:%d",val);}, ^{NSLog(@"blk0:%d",val);}, nil];
}

void exampleD() {
    NSLog(@"---------- exampleD ---------- \n");
    
    typedef void (^blk_t)(void);
    NSArray *array = exampleD_getBlockArray();
    
    NSLog(@"array count = %ld", [array count]);
    blk_t blk = (blk_t)[array objectAtIndex:1];
    
    blk();  
}
复制代码
NSArray* exampleE_getBlockArray() {
    int val = 10;
 
    NSMutableArray *mutableArray = [NSMutableArray new];
    [mutableArray addObject:^{NSLog(@"blk0:%d",val);}];
    [mutableArray addObject:^{NSLog(@"blk1:%d",val);}];
    [mutableArray addObject:^{NSLog(@"blk2:%d",val);}];
    
    return mutableArray;
}

void exampleE() {
    NSLog(@"---------- exampleE ---------- \n");
    
    typedef void (^blk_t)(void);
    NSArray *array = exampleE_getBlockArray();
    NSLog(@"array count = %ld", [array count]);
    
    blk_t blk = (blk_t)[array objectAtIndex:1];
    blk(); 
}
复制代码
void exampleF() {
    NSLog(@"---------- exampleF ---------- \n");
    
    typedef void (^blk_f)(id obj);
   
    __unsafe_unretained blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);   
}
复制代码
void exampleG() {
    NSLog(@"---------- exampleG ---------- \n");
    
    typedef void (^blk_f)(id obj);
    blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        
        blk = ^(id obj) {
            [array addObject:obj];
            NSLog(@"array count = %ld", [array count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);   
}
复制代码
void exampleH() {
    NSLog(@"---------- exampleH ---------- \n");
    
    typedef void (^blk_f)(id obj);
    blk_f blk;
    {
        id array = [[NSMutableArray alloc] init];
        id __weak weakArray = array;
        
        blk = ^(id obj) {
            [weakArray addObject:obj];
            NSLog(@"array count = %ld", [weakArray count]);
        };
    }
    
    blk([[NSObject alloc] init]);   
    blk([[NSObject alloc] init]);  
}
复制代码
void exampleI() {
    NSLog(@"---------- exampleI ---------- \n");
    
    typedef void (^blk_g)(id obj);
    blk_g blk;
    {
        id array = [[NSMutableArray alloc] init];
        __block id __weak blockWeakArray = array;
        
        blk = [^(id obj) {
            [blockWeakArray addObject:obj];
            NSLog(@"array count = %ld", [blockWeakArray count]);
        } copy];
    }
    
    blk([[NSObject alloc] init]);  
    blk([[NSObject alloc] init]); 
}
复制代码

什么是Block

Objective-C中的Block中文名闭包,是C语言的扩充功能,是一个匿名函数而且能够截获(保存)局部变量。经过三个小节来解释这个概念。app

其余语言中的Block概念

程序语言 Block的名称
Swift Closures
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
Javascript Anonymous function

为何Block的写法很别扭?

由于Block是在模仿C语言函数指针的写法:

int func(int count) {
    return count + 1;
}
// int (^tmpBlock)(int i) = ...
int (*funcptr)(int) = &func;
复制代码

可是Block的写法依旧很是难记,国外的朋友更是专门写了一个叫fuckingblock网页提供Block的各类写法。

截获局部变量(或叫自动变量)

// 演示截取局部变量
    int tmpVal = 10;
    void (^blk)(void) = ^{
        printf("val = %d", tmpVal); // val = 10
    };
    
    tmpVal = 2;
    blk();
复制代码

这里依旧显示val = 10,Block会截取当前状态下val的值。至于为何能截获局部变量的值,咱们下一节中讨论。

Block实现原理

Block结构

经过clang -rewrite-objc main.m将上面的示例代码翻译成C,关键代码以下:

// Block基础结构
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
复制代码

Block如何截取局部变量

// 根据示例中blk的实现,生成不一样的 __main_block_impl_0 结构体。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tmpVal;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int flags=0) : tmpVal(_tmpVal) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

根据上面的代码能解决咱们3个疑惑:

  1. __block_impl中有isa指针,那么Block也是一个对象。
  2. 生成不一样的__main_block_impl_0,这里结构里面包含int tmpVal就是咱们局部变量,而__main_block_impl_0的构造函数中是值传递。因此block内部截获的变量不受外部影响。
  3. __main_block_impl_0构造函数中有个void *fp函数指针指向的就是block实现。

咱们向上面示例代码再添加多一些变量类型:

static int outTmpVal = 30; // 静态全局变量

int main(int argc, char * argv[]) {
    int tmpVal = 10;				// 局部变量 
    static int localTmpVal = 20;	// 局部静态变量
    NSMutableArray *localMutArray = [NSMutableArray new];	// 局部OC对象
    
    void (^blk)(void) = ^{
        printf("val = %d\n", tmpVal); // val = 10
        printf("localTmpVal = %d\n", localTmpVal); // localTmpVal = 21
        printf("outTmpVal = %d\n", outTmpVal); // outTmpVal = 31
        
        [localMutArray addObject:@"newObj"];
        printf("localMutArray.count = %d\n", (int)localMutArray.count); // localMutArray.count = 2
    };
    
    tmpVal = 2;
    localTmpVal = 21;
    outTmpVal = 31;
    [localMutArray addObject:@"startObj"];
    blk();
}

复制代码

对应输出结果为:
val = 10
localTmpVal = 21
outTmpVal = 31
localMutArray.count = 2
clang -rewrite-objc main.m后关键代码以下:

static int outTmpVal = 30;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int tmpVal;
  int *localTmpVal;
  NSMutableArray *localMutArray;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _tmpVal, int *_localTmpVal, NSMutableArray *_localMutArray, int flags=0) : tmpVal(_tmpVal), localTmpVal(_localTmpVal), localMutArray(_localMutArray) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

由于涉及到OC对象,这里还会有2个新的方法,这2个方法会放到后面讲:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src){
    _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码
  1. static int outTmpVal = 30;储存在内存中的.data段,static限制了做用域,该文件做用域内可修改。
  2. static int localTmpVal = 20;int main(int argc, char * argv[]) { }做用域可修改,注意__main_block_impl_0构造函数中是传递的*_localTmpVal指针,因此外部修改Block内部一样有效,由于是static因此,Block内部也能够修改localTmpVal的值。
  3. NSMutableArray *localMutArray__main_block_impl_0传递的是指向的地址,因此localMutArray内部操做对于block内一样有效。
  1. 静态变量的这种方式一样也能够做用到局部变量上,传递一个指针到block内,经过指针来读取指向的值,通知也能够修改。可是这种方式在block离开局部变量所在做用域后再调用就会出现问题,由于局部变量已经被释放。
  2. static int localTmpVal = 20;能经过指针的方式修改值,NSMutableArray *localMutArray修改指向的值为何不能够? 这是clang对于Block内修改指针的一个保护措施。

总结下:

  1. 静态变量静态全局变量全局变量均可以访问,修改,保持同一份值。
  2. OC对象,能够进行内部操做。但不能修改OC对象的值(指向的内存地址)。

__block关键字如何实现?

一样的方式,咱们先看__block用C是怎么实现的,下面是一段使用__block的代码:

int main(int argc, char * argv[]) {
    __block int val = 10;
    void (^blk)(void) = ^{
        val = 1;
        printf("val = %d", val);
    };
    
    blk();
}
复制代码

翻译成C,只保留关键代码:

struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
复制代码

这就是__block对应C中的新结构体:

  1. *__forwarding是一个与本身同类型的指针。
  2. int val;这个变量就是为了保存本来__block int val = 10;的值。
  3. 而且__block int val = 10;对应的结构体__Block_byref_val_0也是和以前同样建立在栈上的。

接下来继续看,blk的结构:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_val_0 *val; // by ref
    
  __main_block_impl_0.... // 和以前的__block_impl构造方式一致
};
复制代码

blk结构内部新增了__Block_byref_val_0 *val做为成员变量,和以前原理一致。

blk的实现val = 1;

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_val_0 *val = __cself->val; // bound by ref

        (val->__forwarding->val) = 1;
        printf("val = %d", (val->__forwarding->val));
    }
复制代码

(val->__forwarding->val) = 1;这句很是重要,不是直接经过val->val进行赋值操做,而是通过__forwarding指针进行赋值,这带来很是大的灵活性,如今是blk__block int val都是在栈上,__forwarding也都指向了栈上的__Block_byref_val_0 。以上代码解决了在Block内修改外部局部变量的值。

__block新增了2个方法:__main_block_copy_0__main_block_dispose_0

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
    
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

经过方法命名和参数,能够大体猜出是对Block的拷贝和释放。

Block和__block的储存区域

经过以上clange的编译,Block和__block都是有isa指针的,二者都应该是Objective-C的对象。isa指向的就是它的类对象。在ARC下大体有如下几种,根据名字能够知道对应储存空间:

  • _NSConcreteStackBlock 栈上
  • _NSConcreteGlobalBlock 全局 对应的是.data段
  • _NSConcreteMallocBlock 堆上

clang转出的结果和运行代码时 Block 实际显示的isa类型是不同的,在实际的编译过程当中已经不会通过clang翻译成C再编译。

_NSConcreteGloalBlock有两种状况下能够生成:

  • 声明的是全局变量Block。
  • 在做用域内可是不截获外部变量。

_NSConcreteStackBlock由于在栈上,在函数做用域内声明的Block。

_NSConcreteMallocBlock正由于_NSConcreteStackBlock的做用域在栈上,超出做用域后想要继续使用Block,这就得复制到堆上。那些状况会触发这种复制:

  • ARC下大多数状况会自动复制。好比,栈上block赋值给Strong修饰的属性时。Block做为一个返回值时(超出做用域还能使用,autorelease处理对象生命周期)。
  • 须要手动copy。向方法或函数的参数中传递Block时,编译器没法判断是什么样的状况,由于从Block从栈上复制到堆上很消耗cpu。因此编译器并无帮忙copy
  • Cocoa框架的方法且方法名中含有usingBlock等时,不用外部copy。内部已经进行copy。
  • GCD的Api,也不用外部copy

这里有个比较经典的例子(摘自《Objective-C高级编程》):

- (id)getBlockArray {
	int val = 10;
	return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},
            ^{NSLog(@"blk1:%d",val);}, nil];
}

{
	id obj = [self getBlockArray];
	typedef void (^blk_t)(void);
	blk_t blk = (blk_t)[obj objectAtIndex:0]; 

	blk(); 
}
// crash

复制代码

在ARC状况下,NSArray 数组类会有2个元素,第一个在堆上,第二个栈上。在超出getBlockArray做用域后,第二栈上的block会变成野指针。在全部做用域结束时,Array会释放数组内全部元素。野指针对象执行销毁时会触发崩溃。 正常状况下NSArray应该持有数组内全部元素。但使用initWithObjects:方法时,发现只有第一个元素进行了持有操做,第二个Block依旧在栈上。当我使用NSMutableArrayaddObject:方法时,每一个Block都会进行持有赋值到堆上。我怀疑应该是initWithObjects:方法中多参形式比较特殊。

反复提到Block就是OC的对象,对于对象Copy会带来哪些变化:

Block类 原来储存域 复制产生的影响
_NSConcreteStackBlock 从栈复制到堆
_NSConcreteGlobalBlock .data 无变化
_NSConcreteMallocBlock 引用计数增长

__block的储存区域

Block是一个OC对象,因此涉及到从栈到堆,引用计数的变动等,常见OC对象内存管理的问题。同时Block在堆上时又会对__block进行持有,那么对于 __block一样也是OC对象,内存管理有什么区别呢?

Block从栈复制到堆时对__block变量产生的影响:

__block 存储域 Block 从栈复制到堆时对__block的影响
从栈复制到堆并被Block持有
被Block持有
  • __block从栈上复制到堆上后,本来栈上的__block依旧会存在,被复制到堆上的__block会被Block持有__block的引用计数会增长,栈上的__block会由于做用域结束而释放,堆上的__block会在引用计数归零后释放。
  • 堆上的__block的内存管理就是OC对象的引用计数管理方式,没有被其余Block持有时引用计数归0后释放。

上面提到当__block从栈上复制到堆上,会有两个__block产生,一个栈上的一个堆上的。这两个不一样储存区域的__block是如何实现数据同步的?

这就利用__block关键字如何实现?中提到的指向本身的*__forwarding,当持有__block的Block没有从栈上拷贝到堆上时:*__forwarding指向栈上的__block, 当持有__block的Block拷贝到堆上时后,栈上的__block->__forwarding->堆上的__block,堆上的__block->__forwarding->堆上的__block。读起来有点绕,借用《Objective-C高级编程》中的插图:

__block 和 OC对象从栈上复制到堆上?

上面讲了Block__block在从栈上复制到堆上时的一些变化。为了解决__blockOC对象Block结构体内的生命周期问题,新增了一下几个方法:

  1. __main_block_desc_0中新加2个成员方法:copydispose,这是两个函数指针,指向的分别就是__main_block_copy_0__main_block_dispose_0
  2. Block中使用OC对象__block关键字时新增的2个方法:__main_block_copy_0__main_block_dispose_0 ,这两个方法用于在Blockcopy到堆上时,管理__blockOC对象的生命周期。

Block

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
}
复制代码

OC对象

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->localMutArray, (void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->localMutArray, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
复制代码

__block

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

捕获OC对象和使用__block变量时在参数上会不一样:

OC对象 BLOCK_FIELD_IS_OBJECT
__block BLOCK_FIELD_IS_BYREF

_Block_object_assign就至关于retain方法,_Block_object_dispose就至关于release方法,可是咱们在clang翻译的C语言中并无发现 __main_block_copy_0__main_block_dispose_0的调用。只有在如下时机copydispose方法才会调用:

copy函数 栈上的Block复制到堆时
dispose函数 堆上的Block被废弃时(引用计数为0)

何时栈上的Block会复制到堆?

  1. 调用Blockcopy实例方法。
  2. Block做为函数返回值返回时。(autorelease 对象延长生命周期)
  3. Block赋值给附有__strong修饰符的id类型的类或Block类型成员变量(赋值给Strong修饰的Block类型属性时,编译器会帮忙复制到堆)。
  4. 在方法名中含有usingBlockCocoa框架方法GCD的api中传递Block时。

Block tips

1、哪些状况下Block内self为nil时会引发崩溃?这个时候须要使用Weak-Strong-Dance。

  1. 使用self.blockxxx()时,使用clang转换成C时,能够看到Bblock的调用实际是调用Block`内的函数指针与OC对象调用发消息的形式不同。

  2. 其余业务场景,好比使用self的成员变量作NSAarryNSDictionary 作增长操做时。

    不要无脑使用,更加清晰的理解Weak-Strong-DanceBlock内部strong selfBlock会继续持有self,有些场景并不须要。

解答

  1. 声明成StrongCopy效果都同样。在ARC环境下编译会自动将做为属性的Block从栈Copy到堆,这里Apple建议继续使用Copy防止程序员忘记编译器有Copy动做。
  2. Block内部能截获外部变量。Block结构体中会有建立一个成员变量与截获的变量类型一直,这个值与截获时的值一致,这是一个值传递,保存的是一个瞬时值。
  3. __block关键字的实现是一个结构体,结构体中有个本身同类型的*_farwarding指针,当Block在栈上,__block也是在栈上时:*_farwarding指向栈上的本身。当Block拷贝到堆,堆中建立的__block*_farwarding指向本身,同时将栈上的*_farwarding指向堆中__block
  4. 三种。栈上,堆上,全局。
  5. 1 手动copy。2 做为返回值返回。3 将Block赋值给__strong修饰的id类型Block类型成员变量。4 方面名中含有usingBlockcocoa框架方法GCD
  6. 使用__weak弱引用,或者手动断开强引用。
  7. Block内的weakSelf可能会出现nil的状况,nil可能会形成奔溃或是其余意外结果。因此在Block内做用域内声明一个Strong类型的局部变量,在做用域结束后会自动释放不会形成循环引用。

编程题目答案,请参考Github上的repo:TestBlock

历史文章

Objective-内存管理:对象

参考

  1. 《Objective-C高级编程》
  2. 浅谈 block - 截获变量方式
  3. Blocks Programming Topics
  4. Working with Blocks
  5. fuckingblock

相关文章
相关标签/搜索