关于Block一些记录

大概两三周前通过学习《Objective-C高级编程 iOS与OS X多线程和内存管理》中的Block章节,系统深入了解了Block相关原理和内存管理的内容,昨天闲暇时回想起来,感觉有些东西又模糊了,内容记得七七八八,太碎片化了。索性好记性不如烂笔头,把自己的理解整理记录一下。

将Objective-C代码转换为C\C++代码

ClangLLVM编译器)具有转换为我们可读源代码的功能。

//如果需要链接其他框架,使用-framework参数。比如-framework UIKit
xcrun  -sdk  iphoneos  clang  -arch  arm64  -rewrite-objc  OC源文件  -o  输出的cpp文件

设置了sdk的平台和cpu架构,减少转换出来的代码量,方便查阅。

可能会遇到以下问题:

cannot create __weak reference in file using manual reference

解决方案:支持ARC、指定运行时系统版本,比如

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 OC源文件 -o  输出的cpp文件

Block底层结构

Block没有自动捕获变量时:

//Block定义的结构体
struct __main_block_impl_0 {
struct _block_impl impl;
struct __main_block_desc_0 *Desc;
__main_block_impl_0(void *fp,struct __main_block_desc_0 *desc, int flags=0)
{
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
}
};

我们代码中写的Block在底层会被转换成类似上面这样子的结构体类型,struct __main_block_impl_0

struct __main_block_impl_0中包含了两个结构体:struct _block_impl implstruct __main_block_desc_0 *Desc,以及一个构造函数。再看一下两个结构体的定义。

struct _block_impl {
void *isa;  //指向了block所属的类型
int Flags;  
int Reserved;   // 预留
void *FuncPtr;  // 函数指针,指向block中方法实现
};
//存储block的其他信息,大小等
struct __main_block_desc_0 {
unsigned long reserved;   // 预留
unsigned long Block_size; // Block的大小
}

通过上面可以看出Blcok也是包含一个isa指针,因此也是一种OC对象。具体是什么类,因为涉及到Blcok的内存管理,所以后面篇幅再深入讨论。

再看一下给Blcok结构体赋值和调用的代码:

//赋值部分
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0,&__mainBlock_desc_0_DATA);
struct __main_block_impl_0 *blk = &temp;
//调用部分
(*blk->impl.FuncPtr)(blk);

赋值部分就是调用了__main_block_impl_0的构造函数,将方法和__main_block_desc_0类型的结构体作为参数传递进入。
方法调用是通过Blcok的结构体取出其中的函数指针,直接调用该函数,同时将Block自身作为参数传递给方法实现。

先对简单的Block有个印象。

Block变量捕获机制

int c = 30; // 全局变量(数据段,不需要捕获)

- (void)blockTest {
    auto int a = 10;//局部auto变量(栈区,值捕获)
    auto __strong NSObject *object = [[NSObject alloc] init];//局部auto变量(栈区,值捕获)
    static int b = 20;//局部static变量(数据段,指针捕获)
    void (^block)(void) = ^(void) {
        NSLog(@"a:%d b:%d c:%d",a,b,c);
        NSLog(@"object:%@",object);
    };
    block();
}

为了在Block内部可以访问外部的变量,Block有个变量捕获机制。那么什么样的变量才会捕获,什么样的不会捕获呢?

  • 局部变量

    1. auto变量(平时我们在方法中声明的非static局部变量,只是省略了auto关键字),这种情况是值捕获。
    2. static变量和结构体,这种情况是指针捕获。
  • 全局变量:不会捕获,因为不需要捕获就可以访问。

总结就是:只捕获局部变量。

Block捕获变量之后代码什么样子?

将上面的- (void)blockTest转换C看一下:

struct __blockTest_block_impl_0 {
  struct __block_impl impl;
  struct __blockTest_block_desc_0* Desc;
  int a;
  int *b;
  NSObject *object;
  // 省略构造函数...
};

嗯,备注的没有错。变量aobject都是值捕获,而变量b捕获的是*b,是指针的捕获,而c没有捕获。

Block的内存管理

Block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型。

  • NSGlobalBlock ( _NSConcreteGlobalBlock )//全局
  • NSStackBlock ( _NSConcreteStackBlock ) //栈
  • NSMallocBlock ( _NSConcreteMallocBlock )//堆

除了打印,那么怎么判断一个Block的具体类型...?

  1. NSGlobalBlock : 没有访问auto变量。
  2. NSStackBlock : 访问了auto变量。
  3. NSMallocBlock : __NSStackBlock__调用了copy

可能有的同学在这里这样子测试一下,发现上面的判断依据并不对...

001.png

明明Block访问的是auto变量,但是Block的类型是__NSMallocBlock__呐,并不是__NSStackBlock__,你说的不对。不着急,其实这里还涉及到另外一个问题:Block的内存管理。

对一个Blcok进行copy操作后,对三种类型的Blcok产生的影响:

  1. __NSGlobalBlock__ :
    • copy前:Block位于数据段中;
    • copy后:不产生任何影响。
  2. __NSStackBlock__ :
    • copy前:Block位于函数栈中;
    • copy后:从栈中复制一份到堆中
  3. __NSMallocBlock__ :
    • copy前:Block位于堆中;
    • copy后:Block的引用计数增加

在ARC环境下,编译器会根据情况自动将栈上的Block复制到堆上,比如以下情况:

  1. Block作为函数返回值时。
  2. Block赋值给__strong指针时。
  3. Block作为Cocoa API中方法名含有usingBlock的方法参数时。
  4. Block作为GCD API的方法参数时。

在之前的图片(001)中,就是其中的第二种情况,Block被赋值给__strong指针。

这也是为什么我们习惯于用copy关键字,来修饰一个Block。以及将Block当做参数传递时,安全起见,会对Block参数执行copy操作。

Block对象类型变量的强弱引用问题

  1. Block内部访问了对象类型的auto变量时:

    • 如果Block是在栈上,将不会对auto变量产生强引用。就是说栈上的Block不会强引用一个对象
  2. Block被拷贝到堆上时:

    • 会调用Block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据auto变量的修饰符(__strong__weak__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
  3. Block从堆上移除时:

    • 会调用Block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的auto变量release

__block修饰符

__block的作用:

  • __block可以用于解决Block内部无法修改auto变量值的问题。
  • __block不能修饰全局变量、静态变量(static)。

编译器会将__block变量包装成一个对象:

void blockTest() {
    __block int a = 10;
    __block NSObject *object = [[NSObject alloc] init];
    NSLog(@"a:%d",a);
    void (^block)(void) = ^(void) {
        a = 20;
        NSLog(@"object --- %@",object);
    };
    block();
}

将上面的代码转换成C++之后可以看到:

// __block int a 被转换为下面的结构体

struct __Block_byref_a_0 {
 void *__isa;
 __Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};
//  __block NSObject *object 被转换为下面的结构体

struct __Block_byref_object_1 {
  void *__isa;
__Block_byref_object_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *object;
};

__Block_byref_a_0 *__forwarding是一个指向自身的指针。

Snip20180612_3.png

当我们的OC代码中,去访问被__block修饰的变量,在底层中是如何去读取变量呢?
上面的代码中有一段打印的代码:

NSLog(@"a:%d",a);

在C++中被转成了:

NSLog((NSString *)//此处省略,影响阅读//,(a.__forwarding->a));

在Blcok中修改a的值:

a = 20;

在C++中被转成了:

// 从blcok取出blcok捕获的被`__block`修饰的变量
__Block_byref_a_0 *a = __cself->a; //__cself是blcok
(a->__forwarding->a) = 20;

Block外部外部访问变量是通过a.__forwarding->a,访问结构体的__forwarding指针找到值。可能有的同学有疑问:不是多此一举的嘛?结构体->__forwarding->结构体->val,直接结构体->val不就可以了吗?

目前能看出的作用是保持统一的写法,当然还有其他的原因,后面讲解。

总结:

  • 当没有使用__block时,由于是值捕获,所以哪怕在Block内修改,也不能影响到Block外变量的值,因此苹果不允许直接修改。
  • 而当我们在Block去修改被__block修饰的变量时,由于是捕获到__block结构体的指针,这样就可以我们可以修改Block外面的值了。

__block的内存管理

  • 开始时__block结构体是一个在上的结构体,在栈上的内存无所谓强弱引用的关系。而__block结构体包装的对象是强或弱引用,是通过你使用__weak__strong哪个来修饰决定的。
 NSObject *object = [[NSObject alloc] init];
__block __weak typeof(object) weakObject = object;
__block  NSObject *strongObjce = object;
  • Block被拷贝到上时,会自动将捕获的__block结构体也拷贝到堆上。由于__block的结构体也有isa指针,同时还在堆空间中,我们可以将它理解成一个OC的对象,__block对象。
Snip20180612_4.png
  • 于此同时还会将栈上的__block结构体中的__forwarding指针,指向堆空间中的__block对象Block捕获的指针,会从栈上的__block结构体变为堆空间中的__block对象,同时对__block对象强引用。
Snip20180612_6.png
  • Block从堆中移除时:
    Snip20180612_5.png

这样分析一下,除了会将__block结构体从栈移动到堆之外,和普通形式的auto对象内存管理,流程上没有什么差别。当然具体内部调用的函数参数还是有点区别的:

Block拷贝到堆上时,都会通过copy函数来处理它们

__block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

Block从堆上移除时,都会通过dispose函数来释放它们:

__block变量(假设变量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
对象类型的auto变量(假设变量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

能看到,虽然调用的方法相同,但是传递的参数类型不同:3和8。这决定了方法内部如何去处理流程吧。

循环引用

想必读到这里,应该可以理解循环引用是怎么产生的了。

Snip20180612_7.png

注意:self是方法调用时传递的第一个参数,是局部变量。

怎么解决?
不让Block强引用self,断掉一条线,就不会产生循环引用。

我们一般通过__weak来修饰变量,比如这样:

__weak typeof(self) weakSelf = self;

也可以使用__unsafe_unretained修饰变量解决。关于__unsafe_unretained可以看这篇文章

他们的区别:
__weak: 对于__weak,指针的对象在它指向的对象释放的时候回转换为nil,这是一种特别安全的行为。
__unsafe_unretained: 就像他的名字表达那样,__unsafe_unretained会继续指向对象存在的那个内存,即使是在它已经销毁之后。这会导致因为访问那个已释放对象引起的崩溃。

为了更安全的使用,我们经常是这样写:

__weak typeof(self) weakSelf = self; //解决循环应用
self.block = ^{
    __strong typeof(self) strongSelf = weakSelf; 
    //在Block方法内部,即:局部变量内;对weakSelf进行一个强引用,
    //这样可以确保,当self其他的强引用都释放时,仍然保持有一个强引用,
    //这样self不会再block内部突然释放掉,导致后面的代码出现未知的问题。
    //do someThing...//
};
以上就是我个人对Block的一些理解,如有错误的地方,希望各位大侠不吝赐教!!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,230评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,261评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,089评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,542评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,542评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,544评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,922评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,578评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,816评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,576评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,658评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,359评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,920评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,859评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,381评论 2 342

推荐阅读更多精彩内容