iOS基础(六) - 弄懂Object-C中block的实现

banner.jpg

前言:之前写swift进阶(二) - 闭包(Closure),看了大量关于block的文档,寻思着把自己这段时间关于block的理解,整理出来,以后忘了还能找回来。😄

1.基础知识

巩固一下基础知识,大学学的差不多都还给老师了。😂

  • 指针与地址:p VS *p VS &p
    看一段代码:
char *p = "abc";
NSLog(@"&p: %p----p: %p----*p: %c", &p, p, *p);
char **p1 = &p;
NSLog(@"&p1: %p----p1: %p----*p1: %s----**p1: %c", &p1, p1, *p1, **p1);
//&p: 0x7ffeefbff5a8----p: 0x100001cad----*p: a
//&p1: 0x7ffeefbff5a0----p1: 0x7ffeefbff5a8----*p1: abc----**p1: a

&是取址操作,以上面代码为例:p是指向字符数组的指针,&p则是指针p在内存的地址,*p是指针p指向内存地址的存放内容,这里是字符数组的首地址也是第一个字符的内存地址,所以返回的是a,而*p1返回abc,是因为*p1其实就是p所指向内存的内容,所以返回的是abc。下面p1是指针的指针,p1是指向指针p的指针,所以,p,*p和&p的关系如下:


p|*p|&p.png

验证一下:

NSLog(@"p1: %p == &(*p1): %p", p1, &(*p1));
NSLog(@"&(*p1): %p == &p: %p", &(*p1), &p);
//p1: 0x7fff5fbff708 == &(*p1): 0x7fff5fbff708
//&(*p1): 0x7fff5fbff708 == &p: 0x7fff5fbff708
//满足p = &(*p)

注意:引用类型(对象)和值类型(基本数据类型,如int,float等等)的储存不一样,引用类型初始化返回的是指向该类型分配内存地址的指针,而基本数据类型返回的是改类型分配的内存地址。

  • iOS编译的程序占用内存分布的结构
    程序占用内存的分布结构如下图:(图片来源
    内存结构.jpg

    栈区:一般存放函数参数,局部变量等值,由系统自动分配和管理,程序员不必关心。存放里面的数据,遵从先进后出的原则。
    堆区:由程序员申请,管理和内存回收。数据储存的结构是链表。
    全局区/静态区:储存全局变量和静态变量。
    文字常量区:主要储存字符串常量。
    程序代码区:存放程序的二进制代码。
    举个例子:(代码来源
//main.cpp
int a = 0; // 全局初始化区
char *p1; // 全局未初始化区
main {
    int b; // 栈
    char s[] = "abc"; // 栈,字符串常量一般在文字常量区,但是该字符串从文字常量区复制到了栈区
    char *p2; // 栈
    char *p3 = "123456"; // 123456\0在常量区,p3在栈上
    static int c =0; // 全局静态初始化区
    p1 = (char *)malloc(10);
    p2 = (char *)malloc(20); // 分配得来的10和20字节的区域就在堆区
    strcpy(p1, "123456"); // 123456\0在常量区,这个函数的作用是将"123456" 这串字符串复制一份放在p1申请的10个字节的堆区域中。
    // p3指向的"123456"与这里的"123456"可能会被编译器优化成一个地址。
}

内存结构从低地址端到高地址端:


内存结构地址.png

2.block数据结构

block的数据结构定义如下图:(图片来源

block-struct.jpg

下面来验证一下block的数据结构,看一段代码:

NSString *str = @"hello";
void(^block)() = ^{
        NSLog(@"%@", str);
};
block();

用clang命令反编译Object-C文件成C++源文件,结果如下:

//字符串str初始化
NSString *str = (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2cb64e_mi_0;
//block定义
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344));
//block调用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

可以看出,block是一个函数指针,指向_main_block_impl_0的内存地址,并且_main_block_imp_0传入了__main_block_func_0一个空的函数指针,定义了block里的操作;__main_block_desc_0_DATA数据描述;str指针,指向初始化字符串的内存地址,这个后面写block里如何修该外面的值的时候会提到;还有一个标识码
上面提到了__main_block_impl_0__main_block_func_0__main_block_desc_0_DATA,下面一个一个看一下它们是什么。

//__main_block_impl_0
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSString *str;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *_str, int flags=0) : str(_str) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString *str = __cself->str; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_2cb64e_mi_1, str);
        }

//__main_block_desc_0_DATA
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*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

//__block_impl
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

block初始化返回来的是结构体 __main_block_impl_0的内存地址,该结构体的构造函数接收了四个参数,分别是__main_block_func_0,__main_block_desc_0_DATA,str和标识符。因为block里捕获的是该变量指向的内存地址,而不是直接把当前的对象的地址,所以,理论上是两个指向同一个内存地址的变量,所以,修改block里面的变量地址,并不能改变外面的变量,编译器就干脆禁止了。看一下__block_impl这个结构体,是不是和图block-struct中的Block_layout很像,把结构体__block_impl里面的变量接在结构体__main_block_impl_0上,就一模一样了,Block_descriptor结构体就是__main_block_desc_0结构体,主要储存block的大小等信息。再看一下__main_block_func_0,相信大家都看出来了吧,这就是block执行的操作,打印获取的str。

3.block的类型

  1. _NSConcreteGlobalBlock全局静态block,不访问任何外部变量
  2. _NSConcreteStackBlock保存在栈中的block,当函数返回时会被销毁
  3. _NSConcreteMallocBlock保存在堆中的block,当引用计数为0时会被销毁

举例说明:
_NSConcreteGlobalBlock

void(^block)() = ^{
        };
block();
NSLog(@"%@", block);    
//<__NSGlobalBlock__: 0x1000030d0>

上面的block在ARC和非ARC打印出来都是NSGlobalBlock,但是clang一下C++源文件确是_NSConcreteStackBlock,不清楚为什么?有可能编译出的偏差也有可能是苹果系统做了什么,知道的同学请告诉我一声,感激不尽。

_NSConcreteStackBlock/ _NSConcreteMallocBlock

NSString *str = @"hello";
void(^block)() = ^{
       NSLog(@"%@", str);
};
block();
NSLog(@"%@", block);
//ARC:<__NSMallocBlock__: 0x100208f40>
//非ARC:<__NSStackBlock__: 0x7fff5fbff6d8>

ARC环境下会自动把栈里的block复制到堆里,非ARC环境下需要自己调用copy,复制到堆里,例:Block myBlock = [[block copy] autorelease]。

4.block获取外部变量

1.没有__block修饰的外部变量,block里面不能修改
在前面讲block的结构的时候,提到block里面获取的变量并不是外部变量的本身内存地址,而是指向的内存地址,下面代码验证一下:

NSString *str = @"hello";
NSLog(@"before----&str: %p----str: %p", &str, str);
void(^block)() = ^{
        NSLog(@"block----&str: %p----str: %p", &str, str);
};
block();
NSLog(@"after----&str: %p----str: %p", &str, str);
2017-03-27 10:05:33.557243 BasicTest[35056:2385069] before----&str: 0x7fff5fbff708----str: 0x100003100
2017-03-27 10:05:33.557494 BasicTest[35056:2385069] block----&str: 0x7fff5fbff6f8----str: 0x100003100
2017-03-27 10:05:33.557516 BasicTest[35056:2385069] after----&str: 0x7fff5fbff708----str: 0x100003100

block里面的str内存地址和外部变量不一样,但是block里面的str指向的内存地址则和外部str指向的内存地址一样,所以我们修改不了外部变量的内存地址。
2.有__block修饰的外部变量,block里面可以修改外部变量的内存地址
代码验证:

__block NSString *str = @"hello";
NSLog(@"before----&str: %p----str: %p", &str, str);
void(^block)() = ^{
        NSLog(@"block----&str: %p----str: %p", &str, str);
        str = nil;
};
block();
NSLog(@"after----&str: %p----str: %p", &str, str);
2017-03-27 10:10:42.929639 BasicTest[35092:2391935] before----&str: 0x7fff5fbff708----str: 0x100003110
2017-03-27 10:10:42.929831 BasicTest[35092:2391935] block----&str: 0x7fff5fbff708----str: 0x100003110
2017-03-27 10:10:42.929853 BasicTest[35092:2391935] after----&str: 0x7fff5fbff708----str: 0x0

外部变量使用__block修饰之后,block里面访问的变量地址和外部变量的地址是一样的,也就是说,外部变量和block里面的捕获的变量是同一个,所以修改block里面的变量地址,外部的变量地址也会发生变化。这到底是怎么实现的呢?我们反编译一下面的代码:

__block NSMutableString *str = [NSMutableString stringWithString: @"hello"];
__block NSInteger number = 1;
void(^block)() = ^{
       NSLog(@"str: %@----number: %ld", str, number);
};
block();

得到:

//str
__attribute__((__blocks__(byref))) __Block_byref_str_0 str = {(void*)0,(__Block_byref_str_0 *)&str, 33554432, sizeof(__Block_byref_str_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("stringWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_95b039_mi_0)};
//number
__attribute__((__blocks__(byref))) __Block_byref_number_1 number = {(void*)0,(__Block_byref_number_1 *)&number, 0, sizeof(__Block_byref_number_1), 1};
//block
void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_str_0 *)&str, (__Block_byref_number_1 *)&number, 570425344));
//block()
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

__block将字符串str和整形number变成了__Block_byref_str_0和__Block_byref_number_1类型,并且block传进去的是&str和&number,也就是外部变量的内存地址,block里捕获的外部变量就是外部变量本身,所以,block里面可以修改外部变量的地址。我们来看一下__Block_byref_str_0和__Block_byref_number_1是什么类型:

struct __Block_byref_str_0 {
  void *__isa;
__Block_byref_str_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSMutableString *str;
};
struct __Block_byref_number_1 {
  void *__isa;
__Block_byref_number_1 *__forwarding;
 int __flags;
 int __size;
 NSInteger number;
};

它们都是保存了block捕获的外部变量内存地址的结构体,Object-C中,带有isa指针的结构体可以看作对象,所以,它们都是对象。

打印一下__block修饰的变量,编译成C代码,会看到:

//Objective-C
__block NSInteger number = 123;
NSLog(@"number2: %ld", number);

//C
__attribute__((__blocks__(byref))) __Block_byref_number_0 number = {(void*)0,(__Block_byref_number_0 *)&number, 0, sizeof(__Block_byref_number_0), 123};
NSLog((NSString *)&__NSConstantStringImpl__var_folders_pn_xqk2zmnx559bmvnwm35rzb4h0000gn_T_main_7d3b3c_mi_1, (number.__forwarding->number));

可以看到,number并不是直接访问__Block_byref_number_0结构体里面的number,而是通过指向本身的指针__forwarding->number获取对应的值。那么?这个有什么意义?为什么要再绕一圈取值?别急,先来看一组代码:

//ARC
@interface Person : NSObject
@property (nonatomic, assign) void(^block)(void);
@end

@implementation Person
- (void)test {
    self.object = [NSObject new];
    NSLog(@"%p--retainCount1: %ld", &self, CFGetRetainCount((CFTypeRef)self));
    self.block = ^{
        NSLog(@"%p--retainCount2: %ld", &self, CFGetRetainCount((CFTypeRef)self));
    };
    self.block();
    NSLog(@"%p--retainCount3: %ld", &self, CFGetRetainCount((CFTypeRef)self));
}
@end

//输出
2018-04-12 15:00:07.908685+0800 BasicDemo[18910:2888235] 0x7ffeefbff538--retainCount1: 1
2018-04-12 15:00:07.909099+0800 BasicDemo[18910:2888235] 0x7ffeefbff528--retainCount2: 2
2018-04-12 15:00:07.909129+0800 BasicDemo[18910:2888235] 0x7ffeefbff538--retainCount3: 2

将block的修饰关键字换成copy,再编译
//输出
2018-04-12 15:02:42.246133+0800 BasicDemo[18923:2891037] 0x7ffeefbff538--retainCount1: 1
2018-04-12 15:02:42.246526+0800 BasicDemo[18923:2891037] 0x100526c40--retainCount2: 3
2018-04-12 15:02:42.246557+0800 BasicDemo[18923:2891037] 0x7ffeefbff538--retainCount3: 3

看到了什么?嘿嘿,当使用assign修饰block时,该block不会被复制到堆上去,所以,该block捕获的self会放在栈上,所以retain了一下当前对象,retainCount=2;当使用copy修饰block时,该block会被复制到堆上去,所以,该block先捕获的self放在栈上,然后被复制到堆上再次retain当前对象,所以retainCount=3。想一想使用assign修饰的block会不会造成循环引用?下面解释循环引用再回答。

回到上一个问题:为什么用_forwarding -> number?
上面解释了,当用copy/strong修饰block时,block不仅在栈上,还会被复制到堆上,那么会有两个block对象。所以,为了保证访问的变量都是同一个,就会把栈上的_forwarding指向堆block里的对象,保证两个block都是同一个对象。

5.block在ARC和MRC的区别

ARC和MRC最主要的区别就是,ARC是系统自动为对象插入-retain和-release方法,MRC则需要使用者自己插入。先上代码:

NSMutableString *str = [NSMutableString stringWithString: @"hello"];
NSInteger number = 1;
NSLog(@"&str: %p----str: %p----&number: %p", &str, str, &number);
void(^block)() = ^{
        NSLog(@"&str: %p----str: %p----&number: %p", &str, str, &number);
};
block();
//
//MRC下的输出:
2017-03-27 11:28:11.835575 BasicTest[36692:2513331] &str: 0x7fff5fbff708----str: 0x100209080----&number: 0x7fff5fbff700
2017-03-27 11:28:11.835770 BasicTest[36692:2513331] &str: 0x7fff5fbff6e8----str: 0x100209080----&number: 0x7fff5fbff6f0
//block外部:&str存放在栈区,而str则存放在堆区(因为&str内存地址比str要大很多,栈区在高地址,堆区在低地址,并且一般栈区比较小,1~2M左右,这个不用的操作系统分配的内存大小不一样,但是不会差很远)
//block内部:&str地址也在栈区,但是和外部的&str地址不一样,但是str的地址确实一样的,这也验证了外部变量在没有__block关键字修饰的情况下,block里捕获该变量会复制一份,而不是直接引用
//&number也一样
//
//ARC下的输出:
2017-03-27 11:35:26.885352 BasicTest[36731:2523310] &str: 0x7fff5fbff708----str: 0x100302f30----&number: 0x7fff5fbff700
2017-03-27 11:35:26.885547 BasicTest[36731:2523310] &str: 0x100400170----str: 0x100302f30----&number: 0x100400178
//block里面捕获的变量也会复制一份,和MRC环境下一样,但是不同的是,复制的内存地址从栈区跑到了堆区
//&str = 0x100400170. &number = 0x100400178
//因此,该block不会随着函数的调用而被释放掉,而MRC则需要自己把block从栈区copy到堆区

注意:
__block在MRC下不会retain对象,在ARC下会retain对象,MRC可以通过__block来解决循环引用问题。

6.block的循环引用

先看图:


cycle-retain.png

上面最简单的循环引用,A引用B,B引用A或者A对象内部拥有block,block里对A强引用。还有更复杂的情况,就是多个对象引用形成一个环,比如:A->B->C->D->A。循环引用有什么坏处?造成资源浪费,因为双方都不能释放,类似线程的死锁。

  • 为什么会造成循环引用?举个例子:
- (void)test: (NSString *)str {
    self.name = str;
    self.block = ^{
        NSLog(@"name: %@", self.name);
    };
}
//编译器报警告:
Capturing 'self' strongly in this block is likely to lead to a retain cycle

上面的代码在ARC下会报警告,但是在MRC下却不会报警告,为什么呢?
在第四小节block获取外部变量可以知道,__block的区别在于对象的内存地址还是对象指向的内存地址,而ARC和MRC的区别除了把栈区的block自动复制到堆区,还有一个重要区别就是自动插入-retain和-release函数,所以,ARC是强引用,MRC是弱引用,因为MRC并没有自动将block获取的外部变量的引用计数器的值增加。

  • 如何解决循环引用
    1.在执行完操作,将其中一个对象设置为nil
    2.其中一个对象的引用设置为弱引用,比如上面的例子,可以将self设置为弱引用
    3.先设置弱引用,在block里再强引用,防止该引用提前被回收掉

回答:使用assign修饰的block会不会造成循环引用?
答案不会
想想block是在哪分配的内存?是在栈上,不是由程序员管理内存,由系统管理,所以,block出了作用域就会被系统释放掉,所以,不会造成循环引用的问题。

针对第三点,说说个人理解,为什么要先weak,再strong,不会引起循环引用,而且在block使用期间,weak引用不会被提前释放掉。先看图:


block-cycle-retain.png

首先,对象A和block之间是不会形成循环引用的,这点是明确的。在block内部再对对象A强引用,看起来好像是形成了循环引用,其实,别忘记这是在block里面,这里的强引用只是一个局部变量,存储在栈上,也就是说,一旦出了函数域,该强引用就会被系统释放掉,也就不存在循环引用的问题。

总结:

Object-C因为有了block使本身语言更为便利,block及时返回的特性,也让代码的上下文结构更为清晰,某种程度比delegate更适合用在网络请求的功能实现。

参考
谈Objective-C block的实现
对Objective-C中Block的追探
block源码
Ry’s Objective-C Tutorial/blocks
Objective-C中的Block
《iOS 与OS X多线程和内存管理》笔记:Blocks实现(二)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容