Objective-C内存管理:Block

栏目: Objective-C · 发布时间: 5年前

内容简介:以下Block在ARC环境下能正常运行吗?若能分别打印什么值?Objective-C中的Block中文名闭包,是C语言的扩充功能,是一个匿名函数并且可以截获(保存)局部变量。通过三个小节来解释这个概念。因为Block是在模仿C语言函数指针的写法:
  1. Block作为属性声明时为什么都声明为Copy?

  2. Block为什么能保存外部变量?

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

  4. Block有几种类型?

  5. 什么时候栈上的Block会复制到堆?

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

  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语言 的扩充功能,是一个匿名函数并且可以截获(保存)局部变量。通过三个小节来解释这个概念。

其他语言中的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高级编程》中的插图:

Objective-C内存管理:Block

__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

一、哪些情况下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

参考

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

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

重构

重构

Martin Fowler / 熊节 / 中国电力出版社 / 2003-8-1 / 68.00元

Martin Fowler和《重构:改善既有代码的设计》(中文版)另几位作者清楚揭示了重构过程,他们为面向对象软件开发所做的贡献,难以衡量。《重构:改善既有代码的设计》(中文版)解释重构的原理(principles)和最佳实践方式(best practices),并指出何时何地你应该开始挖掘你的代码以求改善。《重构:改善既有代码的设计》(中文版)的核心是一份完整的重构名录(catalog of r......一起来看看 《重构》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具