iOS底层原理(四)block

887 阅读18分钟

block的本质

1.block的基本用法

// 不带参数无返回值的block
void (^block)(void) = ^{
	NSLog(@"Hello, World!");
};

block();

// 带参数无返回值的block
void (^block)(int, int) =  ^(int a , int b)
	NSLog(@"this is a block!");
};

 block(10, 20);

2.将block代码转换成C++文件后发现,生成了一个__main_block_impl_0类型的结构体,block是指向这个结构体的指针

int age = 20;
   
void (^block)(int, int) =  ^(int a , int b){
  NSLog(@"this is a block! -- %d", age);
  NSLog(@"this is a block!");
  NSLog(@"this is a block!");
  NSLog(@"this is a block!");
};

block(10, 10);

// 转换后的C++代码
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int age = 10;

        // 定义block变量
        // block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age)
        void (*block)(int, int) = ((void (*)(int, int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        // 执行block内部代码
        // __main_block_impl_0可以直接转换为__block_impl类型,是因为两个类型的结构体地址是一样的,而且相当于直接把__block_impl里的值都放到__main_block_impl_0里
        // (block->FuncPtr)(block, 10, 10)
        ((void (*)(__block_impl *, int, int))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 10, 10);
    }
    return 0;
}

3.__main_block_impl_0类型的结构体,里面包含了__block_impl类型的结构体变量impl__main_block_desc_0类型的结构体变量Desc,一个返回值为__main_block_impl_0类型的构造函数,还会生成一个age来存储外面引用的值

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
    
    // 构造函数(类似oc的init)
    // __main_block_func_0的地址传给fp
    //  : age(_age)语法会自动将_ag赋值给age
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock; // isa指向的_NSConcreteStackBlock就是当前block类型
    impl.Flags = flags;
    impl.FuncPtr = fp; // 保存的就是__main_block_func_0的地址,也就是block执行逻辑的函数
    Desc = desc; // 保存的是&__main_block_desc_0_DATA的地址(主要存储的就是__main_block_impl_0的大小)
      
      // 默认返回的就是__main_block_impl_0结构体
  }
};

4.__block_impl类型的结构体里包含isa指针,说明block也是一个OC对象。在__main_block_impl_0的构造函数中isa指向的是_NSConcreteStackBlock类型的地址,侧面说明这个类型也是当前编译时的block的真实类型

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

5.__main_block_func_0是一个封装了block执行逻辑代码的函数,在__main_block_impl_0的构造函数中通过参数void *fp赋值给FuncPtr指针变量,来保存执行代码的地址

// 封装了block执行逻辑的函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
  int age = __cself->age; // bound by copy

    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_0, age);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_1);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_2);
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_3f4c4a_mi_3);
}

6.__main_block_desc_0这个类型的结构体变量__main_block_desc_0_DATA里面的reserved赋值为0,Block_size赋值为sizeof(struct __main_block_impl_0),也就是当前__main_block_impl_0这个结构体的大小。在__main_block_impl_0的构造函数中通过参数desc赋值给Desc

static struct __main_block_desc_0 {
  size_t reserved; // 0
  size_t Block_size; // 计算的就是__main_block_impl_0这个结构体的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

7.__main_block_impl_0的构造函数中 : age(_age)语法会自动将_age赋值给变量int age来保存。而且转换后的调用__main_block_impl_0可以直接转换为__block_impl类型,是因为两个类型的结构体地址是一样的,而且相当于直接把__block_impl里的值都放到__main_block_impl_0

总结
  • block本质上也是一个OC对象,它内部也有个isa指针
  • block是封装了函数调用以及函数调用环境的OC对象

block的本质结构可以概括为下面这张图

block的变量捕获

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制

  • 局部变量默认是被auto修饰的,表示自动变量,离开作用域就销毁。block的捕获该变量是值传递
  • 局部变量被static修饰,会一直在内存中不被释放,block的捕获该变量是指针传递
  • 全局变量因为是一直都在内存中存在的,所以不用捕获

block的类型

  • block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型
    • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )
    • __NSStackBlock__ ( _NSConcreteStackBlock )
    • __NSMallocBlock__ ( _NSConcreteMallocBlock)
  • block的真实类型都是以运行时为准的,通过Clang编译出的C++类型不是最准确的,因为在运行时又会做了一些变动和处理。而且现在LLVM只会生成一种中间文件,和Clang生成的文件有差异

通过下面代码观察block的对应输出

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
       void (^block)(void) = ^{
        NSLog(@"Hello");
	    };
	    
	    NSLog(@"%@", [block class]);
	    NSLog(@"%@", [[block class] superclass]);
	    NSLog(@"%@", [[[block class] superclass] superclass]);
    }
    return 0;
}

// 对应的输出:__NSGlobalBlock__ NSBlock NSObject

不同内存区域对应的block类型不同

  • 数据段对应的是__NSGlobalBlock__类型的block
  • 堆段对应的是__NSMallocBlock__类型的block
  • 栈段对应的是__NSStackBlock__类型的block

不同操作对应的block类型不同

  • 没有访问自动变量的block的类型是__NSGlobalBlock__
  • 访问了自动变量的block的类型是__NSStackBlock__
  • __NSStackBlock__的block调用了copy后类型会变为__NSMallocBlock__

每一种类型的block调用copy后的结果如下所示

修改Xcode的Build Setting->Objective-C Automatic Reference CountingNo,使编译环境为MRC,然后输出下面代码可以查看block对应的类型

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        
        // 堆:动态分配内存,需要程序员申请申请,也需要程序员自己管理内存
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d", age);
        };
        
        NSLog(@"%@ %@ %@", [block1 class], [[block2 copy] class], [^{
            NSLog(@"%d", age);
        } class]);
    }
    return 0;
}

// 对应的输出:__NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

注意:block2在MRC环境下的类型为__NSStackBlock__,是存储在栈段的。只有通过copy修饰才会变成__NSMallocBlock__,存储在堆中。在ARC环境下即使不用copy修饰类型也是__NSMallocBlock__,因为编译器会视情况自动进行copy操作

block的copy操作

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

1.block作为函数返回值

如果不进行copy操作myblock内部的block返回值作用域一结束就会被释放

typedef void (^Block)(void);

Block myblock()
{
    int age = 10;
    return ^{
        NSLog(@"---------%d", age);
    };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
       Block block = myblock();
        block();
        NSLog(@"%@", [block class]); // __NSMallocBlock__
    }
    return 0;
}

2.将block赋值给__strong指针时

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
       
        // 强指针Block block
        Block block = ^{
            NSLog(@"---------%d", age);
        };
        
        NSLog(@"%@", [block class]); // __NSMallocBlock__
    }
    return 0;
}

3.block作为Cocoa API中方法名含有usingBlock的方法参数时

NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
          
}];

4.block作为GCD API的方法参数时

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
  
});
   
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
  
});

不同环境下block属性的写法

1.MRCblock属性的建议写法

@property (copy, nonatomic) void (^block)(void);

2.ARCblock属性的建议写法

// 因为编译器会自动视情况进行copy操作,所以两种写法都没问题,只是为了统一规范建议使用copy来修饰属性
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

对象类型的auto变量

当block内部访问了对象类型的auto变量时

1.如果block是在栈上,将不会对auto变量产生强引用

@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

@implementation Person

- (void)dealloc
{
    [super dealloc];
    NSLog(@"Person - dealloc");
}
@end

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Block block;
        
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            block = ^{
                NSLog(@"---------%d", person.age);
            };
            
            // MRC环境下对应的内存管理
            [person release];
            NSLog(@"------%@", [block class]);
        }
        
        // 在这里打断点,由于MRC环境下block是在栈区间的,所以不会对age进行强引用,person会随着作用域结束而释放
        NSLog(@"------");
    }
    return 0;
}

2.如果block被拷贝到堆上,会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作

@interface Person : NSObject
@property (assign, nonatomic) int age;
@end

@implementation Person

- (void)dealloc
{
    NSLog(@"Person - dealloc");
}
@end

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        Block block;
        
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
           // __strong Person *weakPerson = person;
            __weak Person *weakPerson = person;
            
            block = ^{
                NSLog(@"---------%d", weakPerson);
            };
            
            NSLog(@"------%@", [block class]);
        }
        
        // 在这里打断点,在ARC环境下block会自动拷贝到堆区间,切换修饰符__strong和__weak,person分别会不释放和释放
        NSLog(@"------");
    }
    return 0;
}

将上面代码文件转换成C++文件可以看出,block内部的__main_block_desc_0结构体会调用copy函数copy函数内部会调用_Block_object_assign函数,而_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__strong person;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

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};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->person, (void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

3.如果block从堆上移除,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的auto变量(release)

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->person, 3/*BLOCK_FIELD_IS_OBJECT*/);}

4.block只有引用的是基本数据类型才不会生成copydispose函数

5.如果用static修饰对象类型,那么生成的C++代码如下

Block block;        
{
    static NSString *string = @"haha";
    block = ^{
        NSLog(@"---------%@", string);
    };
}

// 生成的C++代码        
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  
  // string变量的类型是NSString **
  NSString *__strong *string;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__strong *_string, int flags=0) : string(_string) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

注意:代码里有__weak,转换C++文件可能会报错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 源文件

__block修饰符

__block修饰基本数据类型

看下面代码,怎样可以在block内部修改age的值

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        Block block1 = ^{
            // age = 20;
            NSLog(@"age is %d", age);
        };
        
        block1();
        
        NSLog(@"age的内存地址 - %p", &age);
    }
    return 0;
}

1.用static来修饰age属性block内部引用的是age的地址值,可以根据地址去修改age的值。但不好的是age属性会一直存放在内存中不销毁,造成多余的内存占用,而且会改变age属性的性质,不再是一个auto变量

static int age = 10;

2.用__block来修饰属性,底层会生成__Block_byref_age_0类型的结构体对象,里面存储着age的真实值

__block int age = 10;

3.转换成C++文件来查看内部结构,会根据__main_block_impl_0里生成的age对象来修改内部的成员变量age而且在外面打印的age属性的地址值也是__Block_byref_age_0结构体里的成员变量age的地址,目的就是不需要知道内部的真实实现,所看到的就是打印出来的值

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding; // 保存的自己的地址
 int __flags;
 int __size;
 int age;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        // __Block_byref_age_0 age = {0, &age, 0, sizeof(__Block_byref_age_0), 10};  
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};

        Block block1 = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));

        ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);


    }
    return 0;
}
总结:
  • __block可以用于解决block内部无法修改auto变量值的问题
  • 编译器会将__block变量包装成一个对象
  • 其实修改的变量是__block生成的对象里面存储的变量的值,而不是外面的auto变量,但是内部生成的相同的变量的地址和外面的auto变量地址值是一样的,所以修改了内部的变量也会修改了外面的auto变量
  • __block不能修饰全局变量、静态变量(static)
__block的内存管理

1.程序编译时,block__block都是在栈中的,这时并不会对__block变量产生强引用

2.因为__block也会包装成OC对象,所以block底层也会生成copy函数dispose函数

3.当blockcopy到堆时,会调用block内部的copy函数copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会对__block变量形成强引用(retain)

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};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

实际上这时__block修饰的变量因为被包装成了OC对象,所以也会被拷贝到堆上,如果再有block强引用__block,由于__block变量已经拷贝到堆上了,就不会再拷贝了,下图可以很好的表达出关系

3.当block从堆中移除时,会调用block内部的dispose函数dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放引用的__block变量(release)

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);}

如果有多个block同时持有着__block变量,那么只有所有的block都从堆中移除了,__block变量才会被释放

__block和OC对象在block中的区别

看下面的代码,在block中的本质区别是什么

__block int age = 10;
NSObject *obj = [[NSObject alloc] init];
        
Block block1 = ^{
  age = 20;
  
  NSLog(@"age is %d", age);
  NSLog(@"obj is %p", obj);
};

转成C++文件发现,__block生成的对象就是强引用,而NSObject对象会根据修饰符__strong或者__weak来区分是否要进行retain操作

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSObject *__strong obj;
  __Block_byref_age_0 *age; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *__strong _obj, __Block_byref_age_0 *_age, int flags=0) : obj(_obj), age(_age->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);}

注意:__weak不能修饰基本数据类型,编译器会报__weak' only applies to Objective-C object or block pointer types; type here is 'int'警告

__forwarding指针
  • 在栈中,__block中的__forwarding指针指向自己的内存地址
  • 复制到堆中之后,__forwarding指针指向堆中的__block
  • 堆中的__forwarding指向堆中的__block
  • 这样的目的都是为了不论访问的__block是在栈上还是在堆上,都可以通过__forwarding指针找到存储在堆中的auto变量

__block修饰对象类型

1.看下面代码,用__block修饰的对象类型什么时候被释放

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            __block Person *weakPerson = person;

            block = ^{
                NSLog(@"---------%d", weakPerson.age);
            };
            
           
            NSLog(@"------%@", [block class]);
        }
        
        // 在这里打断点观察person是否会被释放
        NSLog(@"------");
    }
    return 0;
}

2.转换成C++文件可以发现,__block底层生成的结构体里面会引用着该对象类型,并且默认是用__strong来修饰,而且内部也会对应的生成copydispose函数

struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__strong weakPerson;
};

3.我们看main函数里会将__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131赋值给__Block_byref_weakPerson_0这个结构体对象

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        Block block;

        {
            Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
            ((void (*)(id, SEL, int))(void *)objc_msgSend)((id)person, sel_registerName("setAge:"), 10);

            // __Block_byref_weakPerson_0 weakPerson = {0, &weakPerson, 33554432, sizeof(__Block_byref_weakPerson_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, person};
            __attribute__((__blocks__(byref))) __Block_byref_weakPerson_0 weakPerson = {(void*)0,(__Block_byref_weakPerson_0 *)&weakPerson, 33554432, sizeof(__Block_byref_weakPerson_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, person};

            block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_weakPerson_0 *)&weakPerson, 570425344));


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_wn_kcs2c07n3mqd02d77tvjgtjr0000gn_T_main_8be7f8_mi_1, ((Class (*)(id, SEL))(void *)objc_msgSend)((id)block, sel_registerName("class")));
        }

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_wn_kcs2c07n3mqd02d77tvjgtjr0000gn_T_main_8be7f8_mi_2);
    }
    return 0;
}

4.找到这两个值能发现也是分别会调用_Block_object_assign_Block_object_dispose这两个函数,而且传的对象就是__Block_byref_weakPerson_0内部的weakPerson这个对象,也就是说这个结构体内部也会对weakPerson这个对象进行着retainrelease的操作

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);

5.我们把第一段代码中的weakPerson加上__weak修饰符,再运行程序会发现,当作用域结束后,person对象也会被释放了

typedef void (^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block;
        
        {
            Person *person = [[Person alloc] init];
            person.age = 10;
            
            __block __weak Person *weakPerson = person;

            block = ^{
                NSLog(@"---------%d", weakPerson.age);
            };
            
           
            NSLog(@"------%@", [block class]);
        }
        
        // 在这里打断点观察person是否会被释放
        NSLog(@"------");
    }
    return 0;
}

6.我们转换成C++文件能发现,__Block_byref_weakPerson_0里面的person对象修饰符变成了__weak

struct __Block_byref_weakPerson_0 {
  void *__isa;
__Block_byref_weakPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 Person *__weak weakPerson;
};
总结:
  • __block修饰的对象类型也会生成一个新的结构体对象,并且只会被block进行强引用,同__block修饰基本数据类型是一样的
  • __block内部也会生成该对象类型的成员变量,而且会根据不同的修饰符__strong__weak来对应着该对象类型是否被强引用
  • __block内部也会生成copydispose函数
    • __block变量被copy到堆时 ,会调用__block变量内部的copy函数copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用
    • 如果__block变量从堆上移除,会调用__block变量内部的dispose函数dispose函数内部会调用_Block_object_dispose函数,_Block_object_dispose函数会自动释放指向的对象(release)

注意:在MRC环境下即使用__block修饰,__block内部只会对auto变量进行弱引用,无论加不加__weak,block还没有释放,__block修饰的变量就已经释放了,这点和在ARC环境下不同

循环引用

block在使用中很容易就会造成循环引用问题,例如下面的代码

typedef void (^Block) (void);

@interface Person : NSObject
@property (copy, nonatomic) Block block;
@property (assign, nonatomic) int age;

- (void)test;
@end

@implementation Person

- (void)test {
	// 内部循环引用
    self.block = ^{
    	NSLog(@"age is %d", self.age);
    };
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        
        // 循环引用
        person.block = ^{
            NSLog(@"age is %d", person.age);
        };
    }
    
    NSLog(@"111111111111");
    return 0;
}

person对象里面的block属性强引用着block对象,而block对象内部也会有一个person的成员变量指向这个Person对象,这样就会造成循环引用,谁也无法释放

解决方法

在ARC环境下

让其中一个指针变成弱引用

1.用__weak解决,不会产生强引用,当指向的对象销毁时,会自动让指针置为nil

@implementation Person

- (void)test {
	__weak typeof(self) weakSelf = self;
   
    self.block = ^{
        NSLog(@"age is %d", weakSelf.age);
    };
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        
        // __weak Person *weakPerson = person;
        __weak typeof(person) weakPerson = person;
        person.block = ^{
            NSLog(@"age is %d", weakPerson.age);
        };
    }
    
    NSLog(@"111111111111");
    return 0;
}

2.用__unsafe_unretained解决,不会产生强引用,但是是不安全的,当指向的对象销毁时,指针存储的地址值不变,仍然是指向着那块已经被回收的内存空间,那么再访问这个这个变量就会造成野指针错误

@implementation Person

- (void)test {
	__unsafe_unretained typeof(self) weakSelf = self;
   
    self.block = ^{
        NSLog(@"age is %d", weakSelf.age);
    };
}
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        
        __unsafe_unretained Person *weakPerson = person;
        person.block = ^{
            NSLog(@"age is %d", weakPerson.age);
        };
    }
    
    NSLog(@"111111111111");
    return 0;
}

3.用__block解决,用__block修饰对象会造成三者相互引用造成循环引用,需要手动调用block

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block Person *person = [[Person alloc] init];
        person.age = 10;
        
        person.block = ^{
            NSLog(@"age is %d", person.age);
            person = nil;
        };
    }
    
    person.block();
    
    NSLog(@"111111111111");
    return 0;
}

block内部也需要手动将person置空,这个person__block内部生成的指向Person对象的变量

在MRC环境下

1.用__unsafe_unretained解决,同ARC环境下一样,只是MRC不支持__weak

Person *person = [[Person alloc] init];
__unsafe_unretained typeof(person) weakPerson = person;

person.block = [^{
  NSLog(@"age is %d", weakPerson.age);
} copy];

[person release];

2.用__block解决,在MRC中,__block对象里是不会对person对象进行强引用的,所以不会造成循环引用

__block Person *person = [[Person alloc] init];        
person.age = 10;

person.block = [^{
  NSLog(@"age is %d", person.age);
} copy];
   
[person release];

面试题

1.看下面代码,分别输入的值是什么

int a = 10;
static int b = 10;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
		auto int age = 10;
		static int height = 10;
		
		void (^block)(void) = ^{
			NSLog(@"age is %d, height is %d", age, height);
			NSLog(@"a is %d, b is %d", a, b);
		};
		
		age = 20;
		height = 20;
		a = 20;
		b = 20;
		
		block();
		    
		// 输出结果为:age=10,height=20,a=20,b=20 
	}
    return 0;
}   

age是自动变量,是值传递

height表示的是指针传递,block捕获的是该变量的地址

a、b都为全局变量,所以block根本不用捕获,需要时直接拿取当前最新的值就可以了

int a = 10;
static int b = 10;

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int age;
  int *height;
  
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int *_height, int flags=0) : age(_age), height(_height) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
    	auto int age = 10;
    	static int height = 10;
    	
    	void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age, &height));
    
    	age = 20;
    	height = 20;
    	a = 20;
		b = 20;
    
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

2.看下面代码,block内部会不会捕获self

@interface Person : NSObject

@property (copy, nonatomic) NSString *name;

- (instancetype)initWithName:(NSString *)name;
@end

@implementation Person

- (void)test
{
    void (^block)(void) = ^{
        NSLog(@"-------%d", [self name]);
    };
    block();
}
 
- (instancetype)initWithName:(NSString *)name
{
    if (self = [super init]) {
        self.name = name;
    }
    return self;
}

@end

会捕获。因为self本质也是一个局部变量,block内部会生成一个变量来保存Person对象的地址

struct __Person__test_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  Person *self;
  __Person__test_block_impl_0(void *fp, struct __Person__test_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 函数都会生成隐式参数self和_cmd
static void _I_Person_test(Person * self, SEL _cmd) {
    void (*block)(void) = ((void (*)())&__Person__test_block_impl_0((void *)__Person__test_block_func_0, &__Person__test_block_desc_0_DATA, self, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}

3.__block的作用是什么?有什么使用注意点

可以将修饰的变量包装成一个对象,解决在block内部无法修改外部变量的问题。

__block内部会进行内存管理,还有在MRC环境下是不会对对象进行强引用

4.block的属性修饰词为什么是copy?使用block有哪些使用注意?

block一旦没有进行copy操作,就不会在堆上。放到堆上的目的是方便我们来控制他的生命周期,可以更有效的进行内存管理。

注意循环引用