Objective-C基础之四(深刻理解Block)

什么是Block

Block其实就是一个代码块,一般被称为“闭包”,它封装了函数调用以及函数调用环境,以便在合适的时机进行调用,在OC中,Block其实就是一个OC对象,它能够当作参数传递。ios

Block的结构以下:面试

Block的本质

无外部变量访问时Block的底层结构

  • 首先,建立一个Demo,在main.m中加入以下代码:
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void(^test)(void) = ^{
            NSLog(@"Block");
        };
        test();
    }
    return 0;
}
复制代码
  • 而后经过xcrun指令将main.m文件转换成C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
复制代码
  • 查看生成的main.cpp文件,首先看main函数,转换成C++以后,结构以下,此处去除了多余的强转操做,方便阅读
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        //Block的定义
        void(*test)(void) = &__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA
                                                  );
        //block的调用
        test->FuncPtr(test);
    }
    return 0;
}
复制代码
  • Block在编译完成以后,转换成了__main_block_impl_0类型的结构体,它的内部结构以下
struct __main_block_impl_0 {
  //存放了block的一些基本信息,包括isa,函数地址等等
  struct __block_impl impl; 
  //存放block的一些描述信息
  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_impl是直接存放在__main_block_impl_0结构体的内部,因此__main_block_impl_0结构体也能够转换成以下形式bash

struct __block_impl {
  void *isa;    //isa指针,能够看出Block其实就是一个OC对象
  int Flags;    //标识,默认为0
  int Reserved; //保留字段
  void *FuncPtr;//函数内存地址
};

struct __main_block_impl_0 {
  void *isa;    
  int Flags;    
  int Reserved; 
  void *FuncPtr;
  struct __main_block_desc_0* Desc;
};
复制代码

block将咱们所要调用的代码封装成了函数__main_block_func_0,而且将函数__main_block_func_0的内存地址保存在到void *FuncPtr中,具体函数以下闭包

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //此处就是调用的NSLog
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_4f0065_mi_0);
}
复制代码

结构体__main_block_desc_0中则保存了block所占用内存大小等描述信息iphone

static struct __main_block_desc_0 {
  size_t reserved;      //保留字段
  size_t Block_size;    //__main_block_impl_0结构体所占内存大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
复制代码
  • 由此能够看出block底层其实就是一个OC对象,由于它内部拥有isa指针。同时block将内部所要执行的代码封装成了函数,而且将函数内存地址保存到结构体当中,以便在合适的时机进行调用

访问外部变量时Block的底层结构

咱们在使用Block的过程当中,能够在Block内部访问外部的变量,包含局部变量、静态变量(至关于私有的全局变量)、全局变量等等。如今就经过一个Demo来看一下block底层是如何访问外部变量的。async

  • 首先建立Demo,在main.m文件中添加以下代码
//定义全局变量c
int c = 30;
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //局部变量a
        int a = 10;
        //静态变量b
        static int b = 20;
        void(^test)(void) = ^{
            NSLog(@"Block - %d, %d, %d", a, b, c);
        };
        test();
    }
    return 0;
}
复制代码
  • 将main.m转换成C++代码后,再次查看main函数,结果以下
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;
        static int b = 20;
        void(*test)(void) = (&__main_block_impl_0(
                                                  __main_block_func_0,
                                                  &__main_block_desc_0_DATA,
                                                  a,
                                                  &b));
        test->FuncPtr(test);
    }
    return 0;
}
复制代码

能够看出,此时__main_block_impl_0结构体中多了两个参数,分别是局部变量a的值,静态变量b的指针,也就是它的内存地址。函数

  • 查看__main_block_impl_0结构体的内存结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  int *b;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
复制代码

发现,在__main_block_impl_0结构体中多了两个成员变量,一个是int a,一个是int *b学习

  • 当经过test->FuncPtr(test)执行block时,会经过结构体中的FuncPtr找到函数__main_block_func_0的地址进行调用,查看__main_block_func_0函数以下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy
  int *b = __cself->b; // bound by copy

  NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_064cd6_mi_0, a, (*b), c);
}
复制代码

__main_block_func_0函数中,访问局部变量a和静态变量b时都是经过传递过来的__main_block_impl_0结构体拿到对应的成员变量进行访问,可是全局变量c并无存放在结构体中,而是直接进行访问。测试

  • 由此咱们就能够得出结论,block中有变量捕获的机制
    • 当访问局部变量的时候,会将局部变量的值捕获到block中,存放在一个同名的成员变量中。
    • 当访问静态变量时,会将静态变量的地址捕获到block中,存放在一个同名的成员变量中。
    • 当访问全局变量时,由于全局变量是一直存在,不会销毁,因此在block中直接访问全局变量,不须要进行捕获

此处须要注意的是,其实在OC中有个默认的关键字auto,在咱们建立局部变量的时候,会默认在局部变量前加上auto关键字进行修饰,例如上文中的int a,其实就至关于auto int a。auto关键字的含义就是它所修饰的变量会自动释放,也表示着它所修饰的变量会存放到栈空间,系统会自动对其进行释放。ui

block总结

block底层结构总结

block在编译完成以后会转换成结构体进行保存,结构体中的成员变量以下,其中在成员变量descriptor指向的结构体中,多了两个函数指针分别为copydispose,这两个函数和block内部对象的内存管理有关,后面会具体说明。

block变量捕获总结

block使用变量捕获机制来保证在block内部可以正常的访问外部变量。

  • 当block访问的是auto类型的局部变量时,会将局部变量捕获到block内部的结构体中,而且是直接捕获变量的值。
  • 当block访问的是static类型的静态变量时,会将静态变量捕获到block内部的结构体中,而且捕获的是静态变量的地址。
  • 当block访问的是全局变量时,不会进行捕获,直接进行访问。

block的类型

block的三种类型

在OC当中block其实拥有三种类型,能够经过class或者isa指针来查看block具体的类型

  • 首先在main.m中添加如下示例代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //第一种类型NSGlobalBlock
        NSLog(@"%@",[^{
            NSLog(@"NSGlobalBlock");
        } class]);
        
        //第二种类型NSStackBlock
        int a = 10;
        NSLog(@"%@",[^{
            NSLog(@"%d", a);
        } class]);
        
        //第三种类型NSMallocBlock - 1
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
        NSLog(@"%@",[test2 class]);
        
        //第三种类型NSMallocBlock - 2
        NSLog(@"%@",[[^{
            NSLog(@"%d", a);
        } copy] class]);
    }
    return 0;
}
复制代码

运行结果以下:

  • 运行后能够发现,block能够有三种类型,分别是NSGlobalBlockNSStackBlockNSMallocBlock,这三种类型分别存放在.data区、栈区堆区。对应的结构图以下

图中的block类型和上文中打印出来的block类型对应关系以下

class方法返回类型 isa指向类型
NSGlobalBlock _NSConcreteGlobalBlock
NSStackBlock _NSConcreteStackBlock
NSMallocBlock _NSConcreteMallocBlock

可是无论它是哪一种block类型,最终都是继承自NSBlock类型,而NSBlock继承自NSObject,因此这也说明了block自己就是一个对象。

如何区分block类型

在上述示例中,提到了四种生成不一样类型的block的方法,分别以下:

  1. 没有访问局部变量的block,而且没有强指针指向block,则此block为NSGlobalBlock
  2. 访问了局部变量的block,可是没有强指针指向block,则此block为NSStackBlock
  3. 访问了局部变量的block,而且有强指针指向block,则此block为NSMallocBlock
  4. NSStackBlock类型的block,执行了copy操做以后,生成的block为NSMallocBlock

其实第三点和第四点生成的都是NSMallocBlock,由此咱们就能够获得下面的结论

block的类型 block执行的操做
NSGlobalBlock 没有访问auto类型的变量
NSStackBlock 访问了auto类型的变量
NSMallocBlock __NSStackBlock__类型的block执行了copy操做

block的copy操做

block执行copy操做后的内存变化

NSGlobalBlockNSStackBlockNSMallocBlock三种类型的block分别存放在了数据区、栈区和堆区。将三种类型的block分别进行copy操做以后,产生的结果以下:

  • NSGlobalBlock的block进行copy操做,什么也不会发生,生成的仍是NSGlobalBlock类型的block
  • NSStackBlock类型的block进行操做,会将block从栈上复制一份到堆中,生成NSMallocBlock类型的block
  • NSMallocBlock类型的block进行copy操做,此block的引用计数会加1

结构图以下

ARC环境下哪些操做会自动进行copy操做?

在上述示例中,NSStackBlock类型的block,执行了copy操做以后,生成的block为NSMallocBlock,其实不止这一种方式生成NSMallocBlock,如下是OC中在ARC环境下自动触发copy操做的几种状况:

  1. block做为返回值时,会自动进行copy
typedef void(^block)(void);
block test(){
    return ^{
        NSLog(@"NSMallocBlock");
    };
}
复制代码
  1. 使用__strong类型的指针指向block时,会执行copy操做
void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %d", a);
        };
复制代码
  1. block做为Cocoa API中含有usingBlock的方法的参数时,会执行copy操做
NSArray *arr = @[@"1",@"2"];
[arr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
     NSLog(@"NSMallocBlock");
}];
复制代码
  1. block做为GCD方法的参数时会执行copy操做
dispatch_async(dispatch_get_main_queue(), ^{
     NSLog(@"NSMallocBlock");
});
复制代码

咱们日常在使用block做为属性的时候,都会使用copy修饰符来修饰,其实内部就是对block进行了一次copy操做,将block拷贝到堆上,以便咱们手动管理block的内存

block访问对象类型

访问对象类型的auto变量时,block的底层结构

上文中Block访问的外部变量都是基本数据类型,因此不涉及到内存管理,若是在block中访问外部对象时,block内部又是什么样的结构呢?

  • 首先在main.m中加入如下示例代码
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //默认对象
        NSObject *obj1 = [[NSObject alloc] init];
        void(^test1)(void) = ^{
            NSLog(@"NSMallocBlock - %@", obj1);
        };
        test1();
        
        //使用__weak指针修饰对象
        NSObject *obj2 = [[NSObject alloc] init];
        __weak typeof(obj2) weakObj = obj2;
        void(^test2)(void) = ^{
            NSLog(@"NSMallocBlock - %@", weakObj);
        };
        test2();
    }
    return 0;
}
复制代码
  • 使用以下指令将main.m文件转换成C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
复制代码

此处因为使用了__weak关键字来修饰对象,涉及到runtime,全部须要指定runtime的版本。

  • 转换成main.cpp文件后,查看block的底层结构为
//直接访问外部对象的block内部结构
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //生成strong类型的指针
  NSObject *__strong obj;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
//访问__weak修饰符修饰的外部对象的block内部结构
struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  //自动生成weak类型的指针
  NSObject *__weak weakObj;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, NSObject *__weak _weakObj, int flags=0) : weakObj(_weakObj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

复制代码

这时发现,若是直接在block中访问外部的auto类型的对象,默认是在block结构体中生成一个strong类型的指针指向外部对象,如结构体__main_block_impl_0。若是在block中访问了__weak修饰符修饰的外部对象,那么在它的内部会生成一个weak类型的指针指向外部对象,如结构体__main_block_impl_1

__main_block_impl_0的构造函数中,obj(_obj)就表明着,之后构造函数传过来的_obj参数会自动赋值给结构体中的成员变量obj。

  • 因为__main_block_desc_0__main_block_desc_1结构相同,因此如下只以__main_block_desc_0为例,查看__main_block_desc_0结构体,会发现它内部新增长了两个函数指针,以下
static struct __main_block_desc_0 {
  size_t reserved;  //保留字段
  size_t Block_size; //整个block所占内存空间
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);   //copy函数
  void (*dispose)(struct __main_block_impl_0*); //dispose函数
} __main_block_desc_0_DATA = { 0,
                               sizeof(struct __main_block_impl_0),
                               __main_block_copy_0,
                               __main_block_dispose_0};
复制代码

新增长了copy和dispose两个函数指针,对应着函数__main_block_copy_0__main_block_dispose_0,以下

//copy指针指向的函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
//dispose指针指向的函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

复制代码

以前说过,block封装了函数调用和函数调用环境,这也就意味这若是它引用了外部的对象,就须要对外部对象进行内存管理操做。__main_block_copy_0函数内部会调用_Block_object_assign函数,它的主要做用是根据外部引用的对象的修饰符来进行相应的操做,若是外部对象是使用__strong来修饰,那么_Block_object_assign函数会对此对象进行一次相似retain的操做,使得外部对象的引用计数+1。

__main_block_dispose_0函数内部会调用_Block_object_dispose函数,它的做用就是在block内部函数执行完成以后对block内部引用的外部对象进行一次release操做。

总结

Block在栈上

若是block在栈上,那么在block中访问对象类型的auto变量时,是不会对auto变量产生强引用的。这个须要在MRC状况下进行测试,将Xcode中Build Settings下的Automatic Reference Counting设置成NO,代表当前使用MRC环境。

  • 首先建立XLPerson类,重写dealloc方法,方便测试
@implementation XLPerson

- (void)dealloc{
    [super dealloc];
    NSLog(@"%s", __func__);
}

@end
复制代码
  • 在main.m中增长以下测试代码
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //建立block
        TestBlock block;
        {
            XLPerson *person = [[XLPerson alloc] init];
            block = ^{
                NSLog(@"block --- %p", &person);
            };
            NSLog(@"%@", [block class]);
            [person release];
        }
        NSLog(@"block执行前");
        block();
        [block release];
        NSLog(@"block执行后");
        
    }
    return 0;
}
复制代码
  • 运行程序,获得以下的打印信息

能够发现,在MRC环境下,即便是有强指针指向block,系统也不会对block进行默认的copy操做,因此当前的block类型依旧为NSStackBlock类型。并且,在block执行以前,XLPerson就已经释放了,说明在栈上的block并无对person对象进行强引用。

block被copy堆上

  • 首先,若是block被copy到了堆上,在访问auto修饰的对象变量时,内部会自动调用copy函数,它内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)作出相应的处理,若是是__strong修饰,则内部对外部的对象造成强引用,若是是__weak或者__unsafe_unretained,则会造成弱引用
  • 若是block执行完成,被系统从堆中移除时,会调用dispose函数,它内部调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量,也就是对引用的auto变量进行一次release操做。
  • copy和dispose函数调用时机以下
函数 调用时机
copy 栈上的block被复制到堆上
dispose 堆上的block被释放时

__block

__block的做用

使用block时,若是block中访问到了外部被auto修饰的变量,咱们常用到__block来修饰外部变量,它的主要做用就是可以让咱们在block内部来修改外部变量的值,固然,block只能用来修饰auto变量,不能用来修饰全局变量和静态变量。

  • 首先来建立Demo,查看源码
typedef void(^TestBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block XLPerson *person = [[XLPerson alloc] init];
        __block int a = 10;
        TestBlock block = ^{
            person = nil;
            a = 20;
            NSLog(@"block -- a:%d, person:%@",a,person);
        };
        block();
        NSLog(@"block调用后,a:%d, person:%@",a,person);
    }
    return 0;
}
复制代码
  • 经过如下指令转换成C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
复制代码
  • 首先查看block结构体的源码,发现block内部多了两个指针,__Block_byref_person_0类型的指针person和__Block_byref_a_1类型的指针a
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
复制代码
  • 再查看main函数中,局部变量和block的建立方式
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        //封装person对象
       __Block_byref_person_0 person = {
            0,      //
            &person,
            33554432,
            sizeof(__Block_byref_person_0),
            __Block_byref_id_object_copy_131,
            __Block_byref_id_object_dispose_131,
            objc_msgSend(objc_msgSend(objc_getClass("XLPerson"), sel_registerName("alloc")), sel_registerName("init"))};
            
        //封装变量a
        __Block_byref_a_1 a = {
            0,
            (__Block_byref_a_1 *)&a,
            0,
            sizeof(__Block_byref_a_1),
            10
        };
        
        //建立block
        TestBlock block = (&__main_block_impl_0(
                                                __main_block_func_0,
                                                &__main_block_desc_0_DATA,
                                                &person,
                                                &a,
                                                570425344));
        block->FuncPtr(block);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_115560_mi_1,(a.__forwarding->a),(person.__forwarding->person));
    }
    return 0;
}
复制代码

__block修饰对象类型auto变量

经过__block修饰的person对象在编译后被封装成了__Block_byref_person_0类型的结构体,内部有多个成员变量,以下

#将person对象封装成结构体__Block_byref_person_0
struct __Block_byref_person_0 {
  void *__isa;                                      //isa指针
__Block_byref_person_0 *__forwarding;               //forwarding指针
 int __flags;                                       //标识位
 int __size;                                        //结构体所占内存大小
 void (*__Block_byref_id_object_copy)(void*, void*);//函数指针指向copy函数
 void (*__Block_byref_id_object_dispose)(void*);    //函数指针指向dispose函数
 XLPerson *__strong person;                         //强引用XLPerson的实例对象
};


//__Block_byref_person_0结构体的建立与赋值
__Block_byref_person_0 person = {
    0,                                  //对应isa指针,传0
    &person,                            //对应forwarding指针,将结构体自身的地址传给了forwarding指针
    33554432,                           //对应flags
    sizeof(__Block_byref_person_0),     //当前结构体所需内存大小
    __Block_byref_id_object_copy_131,   //copy函数
    __Block_byref_id_object_dispose_131,//dispose函数
    objc_msgSend(objc_msgSend(objc_getClass("XLPerson"),    
                              sel_registerName("alloc")), 
                              sel_registerName("init"))     //经过objc_msgSend建立XLPerson对象,而且将对象的指针传入结构体中
};
复制代码

能够明显看出,在结构体__Block_byref_person_0中,存在以下成员变量

  • isa指针,此处赋值为0,同时也能说明此结构体也是一个OC对象
  • forwarding指针,指向结构体自身的内存地址
  • flags,标志位,此处传33554432
  • size,结构体大小,经过sizeof(__Block_byref_person_0)得到,此处能够简单计算出结构体所需内存大小为48个字节
  • __Block_byref_id_object_copy,copy函数,由于在结构体中引用到了person对象,因此调用此方法来根据person指针的引用类型决定是否对person对象进行retain操做,此处person对象是使用__strong来修饰,因此copy函数的做用就是对person对象进行一次retain操做,引用计数+1。
  • __Block_byref_id_object_dispose_131,dispose函数,在结构体从内存中移除的时候,会调用dispose函数,对person对象进行一次release操做,引用计数-1
  • person指针,由于咱们外部建立的是XLPerson的实例对象,因此结构体内部直接保存了person指针来指向咱们建立的XLPerson对象。

前文提到过,由于block封装了函数调用环境,因此一旦它内部引用了外部的auto对象,就须要对外部对象的内存进行管理,因此才有了copy函数和dispose函数。此处也同样,由于使用__block修饰的XLPerson对象的指针存放在告终构体内部,因此须要使用copy函数和dispose函数来管理对象的内存。

__block修饰基本数据类型auto变量

若是使用__block来修饰基本数据类型的auto变量,就会将变量封装成__Block_byref_a_1类型的结构体,内部结构以下

#将变量a封装成结构体__Block_byref_a_1
struct __Block_byref_a_1 {
  void *__isa;                  //isa指针
__Block_byref_a_1 *__forwarding;//forwarding指针
 int __flags;                   //标识位
 int __size;                    //结构体大小
 int a;                         //变量a
};

//封装变量a
__Block_byref_a_1 a = {
    0,                          //isa,传0
    (__Block_byref_a_1 *)&a,    //传入当前结构体a的地址
    0,                          //flags
    sizeof(__Block_byref_a_1),  //结构体的大小
    10                          //外部变量a的值
};
复制代码

相对于__block修饰auto对象,若是修饰基本数据类型,则结构体中少了copy函数和dispose函数,由于基本数据类型不须要进行内存管理,因此不须要调用这两个函数。

  • isa指针,此处传0
  • forwarding指针,指向结构体自身的内存地址
  • flags,此处传0
  • size,结构体的大小,使用sizeof(__Block_byref_a_1)来获取,此处结构体大小为28个字节
  • a,保存了外部变量a的值,此处为10

总结

  • __block能够用来解决在block内部没法修改auto变量值的问题。
  • __block只能用来修饰auto类型变量,没法用来修饰全局变量、静态变量等等
  • 使用__block修饰的auto变量,编译器会将此变量封装成一个结构体(其实也是一个对象),结构体内部有如下几个成员变量
    • isa指针
    • forwarding指针,指向自身内存地址
    • flags
    • size,结构体的大小
    • val(使用的外部变量,若是是基本数据类型,就是变量的值,若是是对象类型,就是指向对象的指针)
  • __block修饰基本数据类型的auto变量,例如__block int a,那么封装的结构体内部成员变量如上,若是是修饰对象类型的auto变量,如__block XLPerson *person,那么生成的结构体中会多出copy和dispose两个函数,用来管理person对象的内存。

__block的内存管理

当block访问外部__block修饰的auto变量时,会将变量封装成结构体,而且将结构体的地址值存放在block内部

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_person_0 *person; // by ref
  __Block_byref_a_1 *a; // by ref
};
复制代码

其中persona就是指向两个__block结构体的指针,正由于在block中有引用到__Block_byref_person_0__Block_byref_a_1,那么block就必须对这两个结构体的内存进行管理,因此相应的在__main_block_desc_0中就生成了两个函数copy和dispose,专门用来管理persona所指向的结构体(也是对象)的内存。以下

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*);  //copy函数
  void (*dispose)(struct __main_block_impl_0*);                            //dispose函数
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,            //copy函数
    __main_block_dispose_0          //dispose函数
};
复制代码

相应的copy函数和dispose函数以下

//copy函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->person,src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//dispose函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->person, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

这里和上文中说到的block中访问外部对象的内存管理相同

  • 当block在栈上的时候,block内部并不会对__block变量产生强引用,在上文中已经使用demo验证过
  • 当block被copy到堆上时,首先会调用copy函数,在copy函数内部会调用_Block_object_assign函数来对__block变量造成强引用。这里和以前说到的block访问外部auto对象有点不一样,若是block访问外部对象,_Block_object_assign会根据外部对象的修饰符是不是__strong仍是__weak来决定是否对对象造成强引用,可是若是是访问__block变量,block就必定会对__block变量造成强引用。

当图中的Block0被赋值到堆上时,会将他所引用的__block变量一块儿赋值到堆上,而且对堆上的__block变量产生强引用

当图中的Block1被复制到堆上时,由于以前__block变量已经被复制到了堆上,因此Block1只是对堆上的__block变量产生强引用。

  • 当block从堆中移除时,会调用block内部的dispose函数,dispose函数内部又会调用_Block_object_dispose函数来自动释放引用的__block变量,至关于对__block变量执行一次release操做。

当Block0和Block1都被废弃时,Block0和Block1对__block变量的引用会被释放,因此__block变量最终由于没有持有者而被废弃

__block中的__forwarding指针

__block修饰的auto变量所对应的结构体以下

在结构体中有一个__forwarding指针指向本身,在后续访问__block变量的时候也是经过__forwarding指针来进行访问

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_1 *a = __cself->a; // bound by ref
  __Block_byref_person_0 *person = __cself->person; // bound by ref

  (a->__forwarding->a) = 20;        //经过__forwarding指针来拿到a进行修改
  (person->__forwarding->person) = __null; //经过__forwarding指针来拿到person进行修改
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_f3_lg91hwts5rjdlzjph0sn82m80000gp_T_main_fbc4b7_mi_0,(a->__forwarding->a),(person->__forwarding->person));
}
复制代码

当block在栈上时,__block变量也存放在栈上,它内部的__forwarding指针指向它自己

当block被复制到堆上以后,block所引用的__block变量也会被复制到堆上,这样在栈上和堆上各存在一份__block变量,此时将栈上__block变量中的__forwarding指针指向堆上__block变量的地址,同时,堆上的__block变量中的__forwarding指针指向它自己,那么此时,无论咱们是访问栈上__block变量中的属性值仍是堆上__block变量中的属性值,都是经过__forwarding指针访问到堆上的__block变量。

__block修饰的对象类型内存管理总结

  • 当__block变量存放在栈上时,他内部不会对指向的对象产生强引用
  • 当block被copy到堆上时,它访问的__block变量也会被copy到堆上
    • 会首先调用__block变量内部的copy函数,copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会更加所指向对象的修饰符(__strong、__weak、__unsafe_unretained)来作出相应的操做,若是是__strong修饰的对象,则会对它进行强引用
  • 若是block从堆上移除
    • 会调用__block变量内部的dispose函数,dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会对__block变量内部引用的对象进行释放操做,至关于执行一次release。

block访问对象类型的auto变量和__block变量对比

相同点

当block存放在栈上是,对对象类型的auto变量和__block变量都不会产生强引用

不一样点

当block被copy到堆上时

  • 访问对象类型的auto变量时,block内部会调用copy函数,根据对象的修饰符(__strong、__weak、__unsafe_unretained)来决定是否对对象进行强引用,若是是__strong修饰的对象,则进行强引用。copy函数以下
//copy函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
    
}
复制代码
  • 访问__block变量时,block内部会直接调用copy函数,对__block变量进行强引用,copy函数以下
//copy函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign(&dst->a, src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

当block从堆中移除

当block从堆中移除时,都会调用dispose函数来对引用的对象进行释放

  • 引用对象类型的auto变量时调用的dispose函数
//dispose函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

复制代码
  • 引用__block变量时调用的dispose函数
//dispose函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
复制代码

虽然调用的都是copy函数,可是传递的参数类型不一样,访问对象类型的auto变量时,传递的参数为3(BLOCK_FIELD_IS_OBJECT),访问__block变量时,传递的参数为8(BLOCK_FIELD_IS_BYREF)

补充

block循环引用的问题

在使用block时,若是block做为一个对象的属性,而且在block中也使用到了这个对象,则会产生循环引用,致使block和对象相互引用,没法释放。Demo以下

typedef void(^TestBlock)(void);
@interface XLPerson : NSObject

@property(nonatomic, copy)NSString *name;
@property(nonatomic, copy)TestBlock block;

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        XLPerson *person = [[XLPerson alloc] init];
        person.name = @"张三";
        person.block = ^{
            NSLog(@"%@",person.name);
        };
        person.block();
    }
    return 0;
}
复制代码

解决方式有两种(此处主要讲解ARC的状况下):

  • 使用__weak来修饰对象
__weak typeof(person) weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
复制代码
  • 使用__unsafe_unretained来修饰对象
__unsafe_unretained XLPerson *weakPerson = person;
person.block = ^{
    NSLog(@"%@",weakPerson.name);
};
复制代码

__weak和__unsafe_unretained的区别

__weak和__unsafe_unretained最终的效果都是能shi使block不对外部访问的对象造成强引用,而是造成弱引用。也就是说外部对象的引用计数不会增长。可是__weak和__unsafe_unretained也有区别,__weak在对象被销毁后会自动将weak指针置为nil,而__weak和__unsafe_unretained修饰的对象在被销毁后,指针是不会被清空的,若是后续访问到了这个指针,会报野指针的错误,所以在遇到循环引用的时候,优先使用__weak来解决。更多的关于__weak的内容会在后续文章中进行学习。

面试题

一、block的本质是什么?

block其实就是封装了函数调用与调用环境的OC对象,它的底层实际上是一个结构体。

二、__block的做用是什么?

在block中若是想要修改外部访问的auto变量,就须要使用__block来修饰auto变量,它会将修饰的变量封装成一个结构体,结构体内部存放着变量的值。若是__block修饰的是对象类型,那么在结构体中会保存着存储对象内存地址的指针,同时在结构体中还多出两个函数指针copy和dispose,用来管理对象的内存。

三、block做为属性时为何要用copy来修饰?

在ARC中,block若是使用copy来修饰,会将block从栈上复制到堆上,方便咱们手动管理block的内存,若是不用copy来修饰的话,那么block就会存在栈上,由系统自动释放内存。

四、使用block会遇到什么问题?怎么解决?

在使用block过程当中,会遇到循环引用的问题,解决方式就是使用__weak或者__unsafa_unretain来修饰外部引用的对象。优先使用__weak。

结束语

以上内容纯属我的理解,若是有什么不对的地方欢迎留言指正。

一块儿学习,一块儿进步~~~

相关文章
相关标签/搜索