关于Block一些记录

2018-06-12  本文已影响11人  FGNeverMore

大概两三周前通过学习《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有个变量捕获机制。那么什么样的变量才会捕获,什么样的不会捕获呢?

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

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类型。

除了打印,那么怎么判断一个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变量包装成一个对象:

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的内存管理

 NSObject *object = [[NSObject alloc] init];
__block __weak typeof(object) weakObject = object;
__block  NSObject *strongObjce = object;
Snip20180612_4.png Snip20180612_6.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的一些理解,如有错误的地方,希望各位大侠不吝赐教!!
上一篇下一篇

猜你喜欢

热点阅读