美文网首页
关于Block一些记录

关于Block一些记录

作者: FGNeverMore | 来源:发表于2018-06-12 16:56 被阅读11次

    大概两三周前通过学习《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的一些理解,如有错误的地方,希望各位大侠不吝赐教!!

    相关文章

      网友评论

          本文标题:关于Block一些记录

          本文链接:https://www.haomeiwen.com/subject/mqrreftx.html