iOS底层小记 二:Block的真相

741 阅读5分钟

1、block的底层结构

  • 先给出结论: block的本质是结构体,block内执行的方法是这个结构体里成员变量-匿名函数函数指针
  • 验证一下:
// main.m
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"i'm a NSGlobalBlock block");
        };
        
        block1();
        NSLog(@"%@", block1); 
        
    }
    
    return 0;
}

编译main.m拿到c++源码:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

拿到源码main.cpp


/// 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内执行的方法,此刻是一个函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6f_2snw6yxn2sz630d3r61gc0700000gn_T_main_a2088f_mi_0);
        }

/// block内的实现结构体
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

/// block的信息
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};



int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void (*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

        ((void (*)(__block_impl *))((__block_impl *)block1)->FuncPtr)((__block_impl *)block1);
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_6f_2snw6yxn2sz630d3r61gc0700000gn_T_main_a2088f_mi_1, block1);
        
        
        /// 简化一下
        /// 构造block结构体
        block1 = __main_block_impl_0(__main_block_func_0, __main_block_desc_0_DATA);
        /// 调用匿名函数
        block1->funcPtr();

    }

    return 0;
}

由此可见,block的本质就是一个结构体,内部有两个结构体成员:第一个存储着结构体的isa指针匿名函数指针,第二个存储着block的基本信息(size)。

因此我们得出block结构体基本上如下:

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

struct __block_info {
  size_t reserved;
  size_t Block_size;
};

typedef struct _SanjiBlock {
     struct __block_impl impl;
     struct __block_info info;
} *sanjiBlock;

我们可以用_SanjiBlock来强转以上的block

// 直接调用block
block1();

// 通过拿到函数指针来调用
sanjiBlock sanjiBlock = (__bridge void *)block1;
void (*p)(void) = sanjiBlock->impl.FuncPtr;
p();

//打印结果一样:
2020-12-09 14:35:15.207888+0800 BlockDetail[39554:1284881] i'm a NSGlobalBlock block
2020-12-09 14:35:15.207928+0800 BlockDetail[39554:1284881] i'm a NSGlobalBlock block

2、捕获(capture)、以及 __block

  • 对局部变量的捕获(auto,static
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        /// auto可省略
        auto int age1 = 10;
        void (^block1)(void) = ^{
            NSLog(@"age1 is %d", age1);
        };
        age1 += 10;
        block1();
        
        static int age2 = 10;
        void (^block2)(void) = ^{
            NSLog(@"age2 is %d", age2);
        };
        
        age2 += 10;
        block2();
        
    }
    
    return 0;
}
/// 打印结果
age1 is 10
age2 is 20

仍然编译main.m:

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

struct __main_block_impl_1 {
  struct __block_impl impl;
  struct __main_block_desc_1* Desc;
  int *age2;
  __main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int *_age2, int flags=0) : age2(_age2) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看出,block1捕获了age1的值,block2捕获了age2的地址,原因很简单: age1auto局部变量,离开作用域就销毁了,所以block1只能捕获它的值(赶在销毁之前),而static修饰的age2是一只在内存中的,block2捕获它的地址,随时可以访问到最新的值。

  • Block会捕获全局变量么? 不会,因为全局变量一直在内存中,可以直接访问拿到最新的值。

  • 所以,得出以下结论:

变量类型是否会被block捕获访问方式
auto局部变量值传递
static局部变量指针传递
全局变量不会直接访问

3、block的类型

block类型环境
NSGlobalBlock没有访问auto变量
NSStackBlock访问了auto变量
NSMallocBlockNSStackBlock调用copy

上代码:(在ARC环境下)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void (^block1)(void) = ^{
            NSLog(@"i'm block1");
        };
        
        int age = 10;
        void (^block2)(void) = ^{
            NSLog(@"age is %d", age);
        };
        
        void (^block3)(void) = [block2 copy];

        NSLog(@"\nblock1 is %@\nblock2 is %@\nblock3 is %@\n", [block1 class], [block2 class], [block3 class]);
    }
    
    return 0;
}

/// 打印结果
block1 is __NSGlobalBlock__
block2 is __NSMallocBlock__
block3 is __NSMallocBlock__

那为什么block访问了auto变量,居然是__NSMallocBlock__, 那是ARC的作用,ARC把block从栈区拷贝到了堆区, 同时引用计数器+1如果block在栈区,那么离开作用域以后就会被释放,这个时候再去调用block就会产生不可预知的结果,在堆区则靠程序员管理内存。

修改环境为MRC

block1 is __NSGlobalBlock__
block2 is __NSStackBlock__
block3 is __NSMallocBlock__

另外哪些情况下,block会被自动拷贝到堆区?

  • block当成方法返回值被返回的时候
  • block赋值给__strong指针的时候
  • block作为cocoa API中含有usingBlock的参数时

4、__Block

  • __Block用于修改block内部无法修改的auto变量
  • __Block修饰的变量会被包装成一个对象

4、block的内存管理

先从两段代码说开:ARC环境下

@interface OnePerson()
@property(nonatomic, assign) int age;
@end

@implementation OnePerson

- (void)dealloc
{
    NSLog(@"OnePerson --- dealloc");
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        OnePerson *person = [[OnePerson alloc] init];
        void (^block)(void) = ^{
           person.age = 10;
         };
        
        NSLog(@"%@", block);
        NSLog(@"block即将销毁"); 
    }
    
    return 0;
}

/// 打印结果
2020-12-09 20:09:12.955196+0800 BlockDetail[55739:1517434] <__NSMallocBlock__: 0x10040c800>
2020-12-09 20:09:12.955546+0800 BlockDetail[55739:1517434] block即将销毁
2020-12-09 20:09:12.955596+0800 BlockDetail[55739:1517434] OnePerson --- dealloc

block捕获了auto变量person, 因此应该是NSStackBlock,但由于ARC环境下自动copy到了堆区, person对象延迟释放了,直到block被释放person对象才被释放,为什么呢?

再次窥探底层源码:

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

person对象被捕获进去,是被__strong修饰符引用的,可见,堆区block对捕获的对象是强引用的!!!!

栈空间的block不会强引用捕获的对象,堆空间的block会强引用捕获的对象。

为啥栈区block不会强引用捕获变量???栈区的block自身难保,朝不保夕,随时要被释放,怎么还会去强引用捕获的变量呢。也就是说栈区的block不会retain捕获的外部变量