iOS Block底层探索

【原创博文,转载请注明出处!】

一、block的本质是什么?

接下来通过一个简单的demo,开启我们探索block之门。
定义一个简单的block并调用:

#import 

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        ^(){
            NSLog(@"Hello world, I'm block!");
          
        }();
        
    }
    return 0;
}

通过平台指令xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp,将OC代码转换成C++代码 。
关于怎么将OC代码转换成C++代码,可以参见之前的博客谈谈我对OC本质的理解,里面详细解释了xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp这段指令的含义。
转换后的C++代码如下:

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

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);

        }

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 (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
        
    }
    return 0;
}

对比之下,原来OC下的block相关代码
 ^(){
        NSLog(@"Hello world, I'm block!");
    }();

对应的C++代码就是:
   ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();

通过C++代码发现,block的调用实际上就是__main_block_impl_0这个结构体,结构体实现如下:

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

__main_block_impl_0结构体内部有一个与结构体同名的__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0)函数,这是C++结构体的写法,该函数为结构体的构造函数,相当于OC类中的- (instancetype)init;方法。__main_block_impl_0函数携带三个参数,最后一个参数为可选的,默认值为0。
再看结构体__main_block_impl_0,发现其第一个成员imp也是个结构体,结构体类型为__block_impl

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

通过结构体__block_impl实现代码中的isa指针,显而易见这是个对象,因此可以准确地说block的本质是一个OC对象。(为什么是OC对象?因为OC的基本数据类型没有isa指针这个概念!博主前面的博客中有讲到,可以翻翻)。
结构体的第二个成员仍然是个__main_block_desc_0类型的结构体

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} 

该结构体两个成员一个是系统的保留值reserved = 0,另一个Block_size则代表了该block的大小。
接下来回到block的调用函数__main_block_impl_0,
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))();
该函数就是结构体__main_block_impl_0的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) ,它有两个必传的参数,一个是函数指针fp ,一个是结构体指针desc,关于结构体指针所指向的结构体就是上面分析到的__main_block_desc_0,那么第一个参数函数指针fp到底是什么?
在这个demo的C++实现代码中,fp指向的函数为__main_block_func_0__main_block_func_0的函数实现代码如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) 
{
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);
}

由于在OC代码中,我们block体内打印了一个字符串,与这个__main_block_func_0函数内的代码完全一致。研究发现__main_block_func_0这个函数的作用就是将block体内的代码封装成一个函数,也就是说block体内的所有OC代码被封装成__main_block_func_0这个函数。与我们OC中的代码NSLog(@"Hello world, I'm block!");相对应的就是NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_a2e15e_mi_0);没毛病。
通过上面的分析,可以肯定的是block本质上也是一个OC对象,它内部也有一个isa指针,block还是一个封装了函数的调用的OC对象。

二、block的变量捕获(capture)

A.局部变量之auto变量

什么是auto变量?局部变量有哪几种?
所谓的auto变量就是非静态的局部变量,离开作用于就会销毁。例如下面这个函数:

- (void)example{
   int a = 5;  //等价于auto int a = 5;
   NSString *name = @"Rephontil.Zhou"; //等价于 auto NSString *name = @"Rephontil.Zhou";
   static int b = 10;  //这个b就不是auto变量
}

常识小结:通常情况下我们定义的局部非static变量都是auto变量,系统会默认在前面加上auto关键字的;但是静态局部变量就不会有auto前缀,加了也会由于报错而编译不通过。

为了保证block内部能够正常访问外部的变量,block有个变量捕获机制,这个变量捕获机制又是怎么样呢?我们一点点来探索:

#import 

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };
        
        a = 20;
        block();
//        2018-08-28 21:34:33.276996+0800 Interview03-block[99340:9151961] 你好世界!a = : 10 ;
        
    }
    return 0;
}

上面demo,block内部访问局部变量a的值,后面在调用block之前修改了a的值,但是打印出来的a的结果仍然为修改之前的值,这与我们的开发经验相符合,但是这究竟是啥原因呢?继续看C++代码的实现:

typedef void(*Block)(void);


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
        }

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; 

        int a = 10;

        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

对比之前没有访问任何变量的block结构体,此时的block所对应的结构体__main_block_impl_0里面多了一个成员int a,并且结构体的构造函数__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a)多出了一个参数_a,(知识点:后面的: a(_a)为C++的语法,意为将参数_a赋值给成员a)。
在实现block的时候,对应的C++代码为Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));可见,系统将a作为函数__main_block_impl_0的参数传递进去,所以block所对应的结构体中int a;这个成员所对应的值a = 10;后面我们修改了a的值为20,并使用block();调用block 打印a的值,这个时候调用了函数__main_block_func_0(struct __main_block_impl_0 *__cself),实现如下:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_09a2a6_mi_0,a);
        }

其内部访问变量a的方式为:int a = __cself->a;__cself为block所对应的结构体对象,所以这个a也就是之前结构体__main_block_impl_0中保存的成员变量a的值,即为10,而不是后面修改的20。针对这个问题,我的看法是block在调用的时候,其实此时main()函数中的a变量相对于block来说是个外部的变量,因为block对应的结构体内部有自己的变量a,外面怎么修改不会影响到block结构体内部成员a的值。

B.局部变量之static变量

根据demoA,我们在demoB中中block内部增加访问静态的局部变量static int b以及修改a、b变量的值后,调用block打印的结果:

#import 

typedef void(^Block)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        auto NSString *name = @"周勇";
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
        };
        
        a = 20;
        b = 20;
        block();
//        2018-08-28 23:16:53.244791+0800 Interview03-block[861:9731638] 你好世界!a = : 10 ;
//        2018-08-28 23:16:53.245153+0800 Interview03-block[861:9731638] 你好世界!b = : 20 ;
        
    }
    return 0;
}

发现局部静态变量b修改之后,block内部打印的结果也变了Σ(⊙⊙"a!
局部变量a的访问过程demoA已经分析过了,接下来仍旧通过C++代码研究局部静态变量b的捕获过程:

typedef void(*Block)(void);

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;
  }
};
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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_0b7d13_mi_1,(*b));
        }

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; 

        int a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

通过C++代码发现,局部自动变量a与静态变量b的捕获方式不同,block结构体中,a为int变量,b为int *变量,也就是指针。在定义block的时候, Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b)),传递的也是b变量的指针,调用block的时候,__main_block_func_0中获取b也是通过block的结构体__main_block_impl_0 访问内部成员变量b,与结构体外部变量b指向的是同一块内存地址,所以只要有地方修改b,结构体内部也会跟随变化,这样就解释了为啥“同样修改了局部auto变量与局部static变量,block访问的结果不同”。
总而言之:在block内部访问的auto变量为值传递,局部静态变量为引用传递(也就是传递变量的指针)。

C.全局变量
#import 

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
            NSLog(@"你好世界!b = : %d ;",b);
            NSLog(@"你好世界!age_ = : %d ;",age_);
        };
        
        a = 20;
        b = 20;
        age_ = 26;
        block();
//        2018-08-29 00:54:13.318712+0800 Interview03-block[2155:10283110] 你好世界!a = : 10 ;
//        2018-08-29 00:54:13.319099+0800 Interview03-block[2155:10283110] 你好世界!b = : 20 ;
//        2018-08-29 00:54:13.319130+0800 Interview03-block[2155:10283110] 你好世界!age_ = : 26 ;

    }
    return 0;
}

block内部访问全局变量age_,其变化同静态局部变量一样。同样转换成C++代码分析:

typedef void(*Block)(void);

int age_ = 25;

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;
  }
};
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__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_0,a);
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_1,(*b));
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_d8c19f_mi_2,age_);
        }

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; 

        int a = 10;
        static int b = 10;
        Block block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, &b));

        a = 20;
        b = 20;
        age_ = 26;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

较demoA、demoB不同的是,block结构体内部没有定义age_变量,block内部访问age_变量的时候,传入的也是全局的age_,因此在任何地方改变这个全局变量,block访问的时候都是这个全局变量的最新值。
通过demoA\B\C,可以肯定对于局部auto变量、static变量、全局变量,block的变量捕获情况如下:

iOS Block底层探索_第1张图片
block变量捕获机制.png

分析了block对自动变量,static变量与全局变量的捕获方式的不同,我认为合理的解释是:自动变量,内存可能会销毁,将来执行block的时候,访问变量的内存,可能会因为不存在引发坏内存访问。
静态局部变量:static变量内存一直会保存在内存中,所以可以取它的最新值,也就是通过指针去取。

三、block的类型有哪几种?

block有3种类型,可以通过调用class方法或者isa指针查看具体类型如下:
NSGlobalBlock ( _NSConcreteGlobalBlock )
NSStackBlock ( _NSConcreteStackBlock )
NSMallocBlock ( _NSConcreteMallocBlock )
这三种类型最终都是继承自NSBlock类型。
通过关键字可以知道这三种类型block分别存放在内存的全局区、栈区、堆区,在内存中对应的区域图示如下:

iOS Block底层探索_第2张图片
三种block在应用程序内存分配情况.png
  • 程序区域存放的就是我们写的代码,比如一个Person类里面的代码。
  • 数据区也就全局区,存放着程序中使用到的全局变量。
  • 堆存放的就是我们新建的对象。如[[Person alloc] init]出来的,这部分内存需要我们手动释放。
  • 栈区存放的就是自动变量,一般在函数调用之后,这些自动变量所占用内存也就被系统回收了。

由于在ARC环境下,编译器为我们做了很多额外的工作,比如将栈区的block copy到堆区,我们在ARC下也就不容易捕获到block初始状态的位置。所以暂时将开发环境切换至MRC下:

iOS Block底层探索_第3张图片
切换环境至MRC.jpg

在MRC下,定义两个block,一个访问auto变量,一个不访问auto变量,最后对访问auto变量的block调用copy方法,依次查看三种情况下block所对应的类型如下:

#import 

typedef void(^Block)(void);

int age_ = 25;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int a = 10;
        static int b = 10;
        Block block = ^(){
            NSLog(@"你好世界!a = : %d ;",a);
        };
        
        Block block1 = ^(){
            NSLog(@"你好世界");
        };
        NSLog(@"%@  %@  %@",[block class],[block1 class],[[block copy] class]);
        
//         __NSStackBlock__  __NSGlobalBlock__  __NSMallocBlock__

    }
    return 0;
}

访问了auto变量的block在栈区,不访问auto变量的block在全局区。对栈区的block调用copy方法,block居然移到了堆区!后面我们对全局区的block调用copy,发现全局区域的block仍旧在全局区。

iOS Block底层探索_第4张图片
三种类型block产生的原因

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

iOS Block底层探索_第5张图片
对各种block进行copy后的block内存区域变化

四、block的copy

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

  • block作为函数返回值时;
  • 将block赋值给__strong指针时;
  • block作为Cocoa API中方法名含有usingBlock的方法参数时;
  • block作为GCD API的方法参数时。

MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);

ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);

为什么要对block进行copy操作?
假如在MRC环境下,在某个函数内定义了一个block变量,并在block中访问了局部变量,但是并没有立即调用该block。后面等到调用该函数的时候,再调用block,看下面的demo:

iOS Block底层探索_第6张图片
图片发自App

调用block后,block内部访问的局部变量打印的结果很糟糕,程序倒是没奔溃,但是结果不如人所愿。
出现这种情况的原因很好理解:由于这个block访问了auto变量,因此是一个NSStackBlock类型的block,该block对应的结构体分配在栈内存上,等到test()函数调用完毕,栈内存会被回收,所以block被调用的时候,访问block结构体内部的变量a,a所对应的内存区域随时可能被系统回收,其内存上的数据也是不确定的。
这种情况该如何保证我们调用block的时候,还能正常访问局部变量呢?正如前面列出的,调用copy方法将block从栈区copy到堆区,事情就解决了。【当然,换成ARC环境,我们通常在声明block属性的时候,使用copy 或 strong关键词修饰,系统也会自动帮我们将block从栈区拷贝到堆区。也就无需我们动手调用block的copy方法了。但是系统底层还是帮我们对block做了copy操作】。

iOS Block底层探索_第7张图片
图片发自App

"copy"这个操作在ARC下是没有必要的。由于我们的block赋值给了void(^block)(void),这个变量默认是__strong修饰的,满足编译器会根据情况自动将栈上的block复制到堆上条件2,即"将block赋值给__strong指针时"

block访问对象类型的auto变量

前面demo中,block访问的都是基本类型的变量。现在我们换对象类型变量看看有啥不同(⊙_⊙)?

iOS Block底层探索_第8张图片
block访问对象类型auto变量.png

访问对象类型auto变量,转换后的C++代码如下:

void(*block)(void);


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  TestClass *__strong testClass;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, TestClass *__strong _testClass, int flags=0) : testClass(_testClass) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  TestClass *__strong testClass = __cself->testClass; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_c36270_mi_0,(long)((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)testClass, sel_registerName("age")));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->testClass, (void*)src->testClass, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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

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};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        TestClass *testClass = ((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)((TestClass *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("TestClass"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)testClass, sel_registerName("setAge:"), (NSInteger)20);

        block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, testClass, 570425344));
        ((void (*)(id, SEL, NSInteger))(void *)objc_msgSend)((id)testClass, sel_registerName("setAge:"), (NSInteger)25);
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

    }
    return 0;
}

对比访问基本数据类型:
相同点:block结构体内部也存在一个与被访问的变量同名的成员变量,本demo中,也就是TestClass *__strong testClass;即testClass实例。
不同点__main_block_desc_0结构体的实现,发现其内部增加了两个函数:
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);

关于copy函数与dispose函数的调用机制:

如果block被拷贝到堆上,会调用block内部的copy函数。copy函数内部会调用_Block_object_assign函数,_Block_object_assign函数会根据auto变量的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用。

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

所谓的弱引用就是在变量前面加上“__weak”修饰符,默认是"__strong"修饰的,即默认对变量为强引用。创建testClass实例加上“__weak”前缀,__weak __weak TestClass *weakTestClass = testClass;对应C++代码中,block结构体内部的成员testClass就声明成 TestClass *__weak weakTestClass;

在使用clang将OC代码转换为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 main.m

MRC下没有"强引用"的概念,只有retain、release概念。栈空间block不会保住引用对象的命,即不会对引用对象进行强引用。但是堆空间block通过强引用被访问对象,从而延长被引用对象的生命周期。
所以可以对栈内存的block进行copy操作,变成堆内存上的block,这样被block引用的对象生命周期就会保住,等到block销毁的时候,被引用的对象才会销毁。
不管ARC、MRC,栈空间block都不会持有对象,如果是堆空间block,有能力保住被引用对象的命,换成ARC下的说法就是“强引用”,MRC下没有“强引用”

在ARC下,我们通过demoA、demoB看看以往开发中,在堆上block内访问oc对象使用不同修饰符造成的结果:

demoA:

iOS Block底层探索_第9张图片
demoA - block弱引用OC对象.png

demoB:

iOS Block底层探索_第10张图片
demoB - block强引用OC对象..png

demoB中,testClass对象默认被__strong修饰符修饰,block会对其强引用,转换为C++环境后block结构体内容如下:

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

block结构体内成员变量testClass就是就是OC中alloc出来的testClass对象,由于该block在堆上,在当前情况下,只要该block没有被销毁,block的内存就一直存在,其内部成员变量testClass由于被"__strong"强引用修饰,也会一直存在,故而TestClass类也一直存在于内存中,这在很多情况下会成为App内存泄露的根源。

五、__block修饰符

在block函数体里面修改变量在日常开发中常见,我们可以轻松在block体内部修改static变量或全局变量,但是却无法修改auto变量。尝试在block中修改auto变量,编译器错误如下:

iOS Block底层探索_第11张图片
block内修改auto变量编译不通过.png

想必入门级的iOS开发者都知道怎么解决这个问题。这里我们起码有三种解决方案:
①、将需要修改的变量设置为全局的;
②、将需要修改的变量设置为static类型;
③、在需要修改的变量前加上“__block”修饰符。
前两种方案一般技术开发都不会采用☹️。
针对方案①,全局变量定义太多影响代码阅读,全局变量生命周期长,占内存;
针对方案②,static类型变量的生命周期同APP一样长,一直存在于内存中,所以舍弃。
方案③是完美方案,“__block”能解决开发中99%以上的问题,1%是例外,后面讲(^^)。

通过demo,我们看看“__block”做了啥(⊙_⊙)?

iOS Block底层探索_第12张图片
__block修改变量.png

OC转换为C++代码:

void(*block)(void);

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_3fd458_mi_0,(age->__forwarding->age));
        }
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*/);}

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

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};
int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

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

        block = ((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 *)block)->FuncPtr)((__block_impl *)block);
        return UIApplicationMain(argc, argv, __null, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class"))));
    }
}

w(゚Д゚)w,变了。。。
__Block_byref_age_0这种类型的结构体本文第一次出现,根据其内部定义:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
 int age;
};

内部包含isa指针,毫无疑问是一个OC对象;
成员__forwarding指向结构体本身,这个操作也很骚,苹果套路深;
成员__size保存了结构体__Block_byref_age_0本身的内存大小;
居然有个成员int age,并且由之前老巢__main_block_impl_0迁移到此;
再看看__main_block_impl_0结构体:

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

内部也有一个age成员,不过是结构体__Block_byref_age_0类型指针,所以block内部访问的auto变量就是该结构体age指针指向的内容。经过“__block”修饰之后,基本类型的“age”变量被包装成“ __Block_byref_age_0”结构体对象。

在main()函数中,我们看到"__block int age = 10;"被定义成:

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

同时block被定义成:

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

也就是将"__Block_byref_age_0 "类型的“age”传入到block的构造函数__main_block_impl_0中,因此__main_block_impl_0中的age被赋值这样的结构体:

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

即:__Block_byref_age_0中最后一个成员“int age;”被赋值10,__forwarding被赋予__Block_byref_age_0型age的地址,这里就是结构体本身,与前面讲述的一致。

再看看block内部的代码块:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref

            (age->__forwarding->age) = 20;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_main_3fd458_mi_0,(age->__forwarding->age));
        }

OC代码age = 20;对应的C++代码(age->__forwarding->age) = 20;,C++代码中有两个不同类型的age变量,前一个age是__Block_byref_age_0类型的结构体指针,后一个age是__Block_byref_age_0结构体中的int型age变量。中间的__forwarding指向的还是__Block_byref_age_0类型的结构体指针age,即第一个age,具体为什么这么设计,后文会讲到。
兜了这么一圈,终于明白了,原来"__block"将局部变量包装成一个__Block_byref_age_0结构体对象,结构体中有与局部变量同名同类型的变量。在block体内修改"__block"变量,通过一系列指针指向关系,最终指向了__Block_byref_age_0结构体内与局部变量同名同类型的那个成员,并成功修改变量值。

六、关于__Block_byref_age_0中的__forwarding指针

iOS Block底层探索_第13张图片
6-1.png
iOS Block底层探索_第14张图片
6-2.png

用"__block"修饰auto变量xxx的时候,系统会将这个auto变量xxx转换成一个__Block_byref_xxx_0结构体类型,结构体中有个成员__forwarding。当block在栈区的时候,__forwarding指向栈区的__Block_byref_xxx_0结构体本身内存地址;当block被copy到堆区的时候,栈上block变量内的__forwarding将会指向堆上的block变量,从而进一步访问block变量内部的成员。这样,前文中访问age的时候通过" (age->__forwarding->age) = 20;"这种做法也就明白了。

七、关于block循环引用及解决方案

iOS Block底层探索_第15张图片
常见循环引用示例.png

demo中,TestClass有一个block实例对象,self对block的关系为强持有。block实现中,也引用了当前实例self,并且也为强引用。这样一来,self持有block,block持有self,所以两者都无法释放,就造成内存泄露。将该.m文件转换为C++实现,看看block结构体__TestClass__test_block_impl_0和block代码块函数__TestClass__test_block_func_0:

struct __TestClass__test_block_impl_0 {
  struct __block_impl impl;
  struct __TestClass__test_block_desc_0* Desc;
  TestClass *const __strong self;
  __TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, TestClass *const __strong _self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __TestClass__test_block_func_0(struct __TestClass__test_block_impl_0 *__cself) {
  TestClass *const __strong self = __cself->self; // bound by copy


        NSLog((NSString *)&__NSConstantStringImpl__var_folders__6_s9x0n6313d99yqk5pltzp6ym0000gn_T_TestClass_e9b143_mi_0, ((NSInteger (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("age")));
    }

正如上面分析的一样:由"TestClass *const __strong self;"可见block结构体中成员变量self为当前类实例的强指针;并且block的代码块中TestClass *const __strong self = __cself->self; // bound by copy也强引用着当前类TestClass的实例,block与self的关系可以用一张引用自《Objective-C高级编程iOS与OS X多线程和内存管理》中经典图示意:

iOS Block底层探索_第16张图片
block与self的互相持有关系.png

如何打破block与持有对象间的强引用关系?
在ARC环境下有以下三种解决方案:

  • ① 使用"__weak";
  • ② 使用"__unsafe_unretained";
  • ③ 使用"__block"(必须要调用block)。
    对于方案①,几乎所有的开发者都熟悉并应用于项目中,这也是最推荐的方案。那么方案②、③又是什么呢❓其实在我没有研究block底层结构、没有去查阅资料的时候,我也仅仅知道方案①☹️(当然事实也证明②、③方案也不推荐使用)
    方案②:“__unsafe_unretained”字面理解就是不安全的、不会导致引用计数增加。简单说就是:不安全的弱引用。
    “__weak”与“ __unsafe_unretained”对比:
    "__weak":不会产生强引用,当指向的对象销毁时,会自动让指针置为nil;
    “ __unsafe_unretained”:不会产生强引用,不安全。当指向的对象销毁时,指针存储的地址值不变,这个时候指向的是一块已经被系统回收的内存,这个时候继续访问会引发"野指针异常"
    对于方案③demo:
#import 
#import "TestClass.h"

@implementation TestClass

- (void)test{
    
    self.age = 20;
    
//    __unsafe_unretained TestClass *weakself = self;
//    __weak TestClass *weakself = self;
    
    __block TestClass* weakSelf = self;
    
    self.block = ^{
        
        NSLog(@"%ld", weakSelf.age);
        weakSelf = nil;
    };
    self.block();

}

- (void)dealloc{
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

当testClass实例销毁的时候,block也释放了,不会循环引用。
我们分析一下转换后的C++代码:

struct __TestClass__test_block_impl_0 {
  struct __block_impl impl;
  struct __TestClass__test_block_desc_0* Desc;
  __Block_byref_weakSelf_0 *weakSelf; // by ref
  __TestClass__test_block_impl_0(void *fp, struct __TestClass__test_block_desc_0 *desc, __Block_byref_weakSelf_0 *_weakSelf, int flags=0) : weakSelf(_weakSelf->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};


struct __Block_byref_weakSelf_0 {
  void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestClass *__strong weakSelf;
};

首先block结构体内持有__block类型的"__Block_byref_weakSelf_0"对象;
其次“__block”类型对象中“TestClass *__strong weakSelf;”,即持有TestClass实例。
由于self持有了block,所以当前对象self、block已经__block变量三者的关系为:


iOS Block底层探索_第17张图片
相互持有状态.png

如此一来:又是一个循环引用问题,我们尝试在block代码块内部去掉"weakSelf = nil",实际结果是TestClass实例不会释放掉。针对这种状况,打破三者之间的循环链即可消除循环引用,解释如下:

首先a. 对象(也就是持有block的对象)对block的持有关系肯定是强持有;
其次b. block对__block变量也是强持有的关系,这两条线无法改动!如果突破__block变量持有对象这条线,就可以了,这样就可以通过调用block后,手动设置__block对象为nil。

在本demo中__block变量定义如下:

struct __Block_byref_weakSelf_0 {
  void *__isa;
__Block_byref_weakSelf_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 TestClass *__strong weakSelf;
};

也就是将结构体__Block_byref_weakSelf_0中的成员变量"TestClass *__strong weakSelf"置为nil,即weakSelf = nil,这样__block就不会持有当前类的实例了,所以循环被打破。打破后三者关系见下图:

iOS Block底层探索_第18张图片
打破相互持有状态.png

由此针对方案③:该方案唯一的缺点就是需要执行block。这么麻烦的关键在于:执行完block之后,在block体内设置引用对象为nil,从而达到手动将__block变量内部的关键成员置为nil,这样就可以打破循环关系,豁然开朗(^^)。

同时MRC环境下开发,方案有两种:

  • ① 用__unsafe_unretained解决;
  • ② 用__block解决(block可以不调用)。

“ __unsafe_unretained”同ARC一致;
在使用"_block"时,我们先总结一下block被copy到堆上时,底层做了啥(⊙⊙)?

  • 当__block变量被copy到堆时,会调用__block变量内部的copy函数。copy函数内部会调用_Block_object_assign函数。_Block_object_assign函数会根据所指向对象的修饰符(__strong、__weak、__unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(**注意:这里仅限于ARC时会retain,MRC时不会retain**)。

  • 如果__block变量从堆上移除,会调用__block变量内部的dispose函数。dispose函数内部会调用_Block_object_dispose函数
    _Block_object_dispose函数会自动释放指向的对象(release)。

正是因为MRC环境下,__block变量对所引用的对象为弱引用关系,所以“对象”、“block”与"block变量"三种之间处于开环状态,也就不存在循环引用问题,因此在MRC下用__block修饰被引用对象,block可以不调用。正如下面demo:

// ATTENTION:MRC环境
#import 
#import "TestClass.h"

@implementation TestClass

- (void)test{
    
    self.age = 20;
    
    __block TestClass* weakSelf = self;
    
    self.block = ^{
        
        NSLog(@"%ld", weakSelf.age);
    };
}

- (void)dealloc{
    [super dealloc];
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

@end


int main(int argc, char * argv[]) {
    @autoreleasepool {
        
        {
            TestClass *testClass = [[TestClass alloc] init];
           
            [testClass test];
            [testClass release];
            
        }
        NSLog(@"---------");
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}


//2018-08-31 11:25:27.800865+0800 Block[74957:7622148] TestClass - dealloc
//2018-08-31  11:25:27.803116+0800 Block[74957:7622148] ---------

结果是TestClass实例被销毁的时候,block也一起销毁了。

八、__weak搭配__strong使用

 __weak TestClass* weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        NSLog(@"%ld", strongSelf.age);
    };

在本demo中,在block内部重新使用__strong修饰weakSelf(被引用)变量是为了在block内部有一个强指针指向weakSelf(弱引用)避免在block调用的时候weakSelf已经被销毁。有些时候block内部访问的对象并不是当前类的实例,考虑到block可能很久才会销毁,因此被block引用的对象应该是弱引用,否则可能造成被引用对象毫无意义地存在于内存中。既然是弱引用,一旦该对象在其他地方被销毁,则block内部的弱引用对象也就销毁了,继续访问也就会返回null,还是用demo说话吧:


// 1. 新建一个Dog类,并实现dealloc方法;
@interface Dog : NSObject

@property (nonatomic, copy) NSString *name;

@end

@implementation Dog

- (void)dealloc{
  
    NSLog(@"Dog - %@",NSStringFromSelector(_cmd));
}


// 2. 在另一个类的block函数体类访问dog的成员属性name;
#import 
#import "TestClass.h"
#import "Dog.h"

@implementation TestClass

- (void)test{
    
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黑";

    __weak Dog *weakDog = dog;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始执行block");
//        __strong typeof(weakDog) strongDog = weakDog;
        sleep(2);
        NSLog(@"狗的名字:%@",weakDog.name);
    });
  
    sleep(1);
    NSLog(@"模拟weakDog被释放");
    dog = nil;
    
    /*
    2018-09-04 14:43:32.966624+0800 Block[80253:7808973] 开始执行block
    2018-09-04 14:43:33.956807+0800 Block[80253:7808911] 模拟weakDog被释放
    2018-09-04 14:43:33.957256+0800 Block[80253:7808911] Dog - dealloc
    2018-09-04 14:43:34.972230+0800 Block[80253:7808973] 狗的名字:(null)
    */
}

- (void)dealloc{
    NSLog(@"TestClass - %@",NSStringFromSelector(_cmd));
}

@end

模拟block内部访问的对象在外部被提前释放的情况,我在调用block的过程中特意将dog设置为nil,访问的结果是:“Block[76269:7674002] 狗的名字:(null)”,项目中block调用的时机是不确定的,被访问的对象何时候释放也是不确定的,故而这种情况下仅仅使用__weak修饰被访问对象肯定存在问题,为了更好解决这样的问题,我们用“__strong”修饰符在block内部搭配外部的"__weak"修饰被访问对象,针对上面demo,正确的做法如下:

- (void)test{
    
    Dog *dog = [[Dog alloc] init];
    dog.name = @"小黑";

    __weak Dog *weakDog = dog;
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"开始执行block");
        __strong typeof(weakDog) strongDog = weakDog;
        sleep(2);
        NSLog(@"狗的名字:%@",strongDog.name);
    });
  
    sleep(1);
    NSLog(@"模拟weakDog被释放");
    dog = nil;
    /*
     2018-09-04 14:46:32.969188+0800 Block[80345:7811829] 开始执行block
     2018-09-04 14:46:33.961744+0800 Block[80345:7811757] 模拟weakDog被释放
     2018-09-04 14:46:33.962013+0800 Block[80345:7811757] ---------
     2018-09-04 14:46:34.974592+0800 Block[80345:7811829] 狗的名字:小黑
     2018-09-04 14:46:34.974973+0800 Block[80345:7811829] Dog - dealloc
    */
}

在block内将weakDog对象强引用为strongDog,执行block过程中将dog设置为nil,结果仍能继续访问。


iOS Block底层探索_第19张图片
图片发自App

你可能感兴趣的:(iOS Block底层探索)