Rason的iOS开发进阶专题程序员Rason的iOS知识体系专题

高级编程之Blocks

2018-05-23  本文已影响24人  RasonWu

本文主要写了为什么会这么设计,该怎么去想这个问题。可能没有其它文章那么详细的去写实现细节,但如果知道了为什么会这样设计,用法的东西应该很快就能通的。

为什么会有Block?

首先要理解的是闭包,闭包是引用了自由变量的函数。当然,这样的话,就有挺多问题需要考虑的:

  1. 如果是基础类型数据,并且里面没有修改,那么最简单的就是值拷贝了。
  2. 如果在函数里面,需要修改外部指针变量,这时候就需要传递地址才可以修改指针变量了。
  3. 如果在里面,引用了对象,这里就比较复杂一些了。我们需要在方法里也能跑,正常的话,会想到增加引用计数,这样就能保活往下跑了。然后为什么会出现循环引用,内存泄漏的情况呢?是不是循环引用就会内存泄漏呢?其实不是这样的,如果有一方能解开这个环也是可以的。其实如果block保存在栈上的话,只要出了作用域之后会被释放。所以这种情况基本上是不存在内存泄漏问题。但是我们为什么要拷贝到堆上呢?就要想一下刚才那个问题,出了作用域就会被释放,那么如果我在另外一个方法,才对这个block执行呢?这样的话,就只能拷贝到堆上。
  4. 大概说一下,三种block的情况。第一种:仅引用了全局变量或者static静态变量的,就是存在data区域的_NSConcreteGlobalBlock,这种不需要我们自己释放了,生命周期和应用一样。第二种:除了第一种说的,初始化的时候,都是_NSConcreteStackBlock。第三种:注意我们是无法生成_NSConcreteMallocBlock的,只有在_NSConcreteStackBlock调用__Block_copy时才会被copy到堆上,生成_NSConcreteMallocBlock。
  5. 想想为什么会是上面这样的结果,我们都知道在方法里面定义的变量,出了作用域就销毁。就很符合上面第二种类型。至于第三种类型,其实就是希望这个block在其它地方也能跑,如果对堆栈比较熟悉的话,就会想用堆。
    其实这三种情况,什么时候是怎么样的,大概就清晰了。

Block的内部结构

其实在iOS中,对象归根到底,其实也就是c数据和c方法组成。那么最简单的区别,就是数据和方法了。那么最容易想到的就是存储引用数据实现方法了。由于我们需要从栈拷贝到堆,所以还需要知道block的大小。由于Block里面可能会引用对象,那么还需要考虑内存管理

  1. Block的拷贝:_NSConcreteGlobalBlock直接返回不做处理,_NSConcreteStackBlock会从栈拷贝到堆,_NSConcreteMallocBlock对这种类型再拷贝,引用计数加一。
  2. Block中变量的拷贝方法_Block_object_assign:1. 对象的话,会调用_Block_retain_object方法会被赋值为retain操作(需要注意的是在ARC环境是是什么都不做的,因为ARC环境下有更成熟的内存管理)2. 引用对象是block则进行递归对Block进行copy。3. 如果是__block变量的话,如果是id或者block类型,会进行简单赋值。
  3. 由于我们知道,循环引用会有可能会导致内存泄漏。为了避免循环引用的问题,__block修饰的变量在MRC中可以避免retain操作,这是因为该变量会被打包成Block_byref类型的结构体。所以该变量在被引用的就可以避免调用_Block_retain_object,而是调用了另外一个方法__Block_byref_id_object_copy,仅做了赋值操作。

Block记法

int func(int count){
  return count +1;
}
int (*funcptr)(int) = &func;

所以变量申明赋值就是这样的

int (^blk)(int) = ^(int count){return count + 1};

必须搞清楚的一些东西

[self SendImage:fileName];
//上面转换如下
void (*action)(id, SEL, NSString*) = (void (*)(id, SEL, NSString*))objc_msgSend;
action(self, @selector(SendImage:), fileName);
#include <stdio.h>
struct ClassBook{
    int age;
    int number;
    int lala;
    ~ClassBook(){
        printf("被自动被释放了");
    };
    ClassBook(int _age,int _number):age(_age),number(_number)
    {
    };
};

void hello(){
    ClassBook book = {23,11};
    ClassBook *temp = &book;
    int *test = (int *)temp;
    for (int i=0; i<3; i++) {
        printf("第%d个数是:%d\n",i,*test);
        test++;
    };
    printf("%d\n",sizeof(ClassBook));
    printf("%d\n",temp->age);
}

int main(){
    hello();
    return 0;
}

运行结果

第0个数是:23
第1个数是:11
第2个数是:0
12
23
被自动被释放了
#include <stdio.h>
#include <stdlib.h>
int *pGlobal;//指针pGlobal在全局区(静态区)
void demo1(){
    printf("全局指针现在的位置%p\n",&pGlobal);//全局区
    printf("全局指针存储的内容%p\n",pGlobal);//全局变量都会初始化为0
    pGlobal = (int *)malloc(sizeof(int)*20);//把分配一个堆区的内容的地址赋值给全局区的pGlobal的内容
    printf("全局指针现在的位置%p\n",&pGlobal);//当然还是全局区,编译时就已经决定了
    printf("全局指针存储的内容%p  这个pGlobal存储的地址其实现在是在堆区的地址了\n",pGlobal);//pGlobal保存了一个堆区的地址
    printf("---------------------------------------------------------\n");
};
int a = 3;//变量是全局区(静态区)内容:常量区
void demo2(){
    printf("%p  常量区\n",a);//常量区
    printf("%p  全局区\n",&a);//全局区
    int *p = &a;//3所在的地址
    a = 5;
    printf("%p  既然是3的地址,那么肯定就是全局区啦\n",p);//全局区
    printf("%p  p是局部变量,那么现在存放位置肯定就是栈区啦\n",&p);//全局区
    printf("---------------------------------------------------------\n");
}
void demo3(){
    int temp = 3;//变量和内容都是在栈区
    int temp2 = 44444;//变量和内容都是在栈区
    printf("%p  \n",&temp);//栈区
    printf("%p 栈区\n",&temp2);//栈区
}
int main(){
    //搞清楚全局区和堆区,(指针变量)是所指向的地址,就是保存的地址,(&指针变量)是指针变量所在的地址,(*指针变量)是所指向的地址的内容。这些东西必须记牢。
    demo1();
    demo2();
    demo3();
    return 0;
}

运行结果

全局指针现在的位置0x100001030
全局指针存储的内容0x0
全局指针现在的位置0x100001030
全局指针存储的内容0x102196bc0  这个pGlobal存储的地址其实现在是在堆区的地址了
---------------------------------------------------------
0x3  常量区
0x100001028  全局区
0x100001028  既然是3的地址,那么肯定就是全局区啦
0x7ffeefbff668  p是局部变量,那么现在存放位置肯定就是栈区啦
---------------------------------------------------------
0x7ffeefbff66c  
0x7ffeefbff668 栈区
Program ended with exit code: 0

clang转把OC代码转写成c/c++代码

//block1.c代码
#include <stdio.h>
int main(){
    int localNumber = 4;
    char *format = (char *)"%d";
    void (^blk)(void) = ^{
        printf(format,localNumber);
    };
    blk();
    return 0;
}

其实如果熟悉c语言和c++的话,阅读下面这些东西,应该是无障碍的,当然至少得搞清楚结构体和构造方法。如果不清楚,建议先看完c和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;
  char *format;
  int localNumber;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, char *_format, int _localNumber, int flags=0) : format(_format), localNumber(_localNumber) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  char *format = __cself->format; // bound by copy
  int localNumber = __cself->localNumber; // bound by copy

        printf(format,localNumber);

    }

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 localNumber = 4;
    char *format = (char *)"%d";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, format, localNumber));
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

Block结构体的话,应该没有太多可以说的。就是把变量、大小、实际调用函数,isa所属类,还有一些标识往里塞就是了。方法的调用就是这个结构体里面实际调用的方法,参数就是Block结构体本身。
不过看到isa,应该第一反应就是其实Block本质也是对象。不清楚的话,看看iOS Runtime ---- 元类

struct __block_impl {
  void *isa;
};

我们先来理解一下传值和方法的调用差异

    //假设Block要截获的是如下的数据,并且下面都是局部变量
    int localNumber = 4;
    char *format = (char *)"%d";

上面这种数据最直接的值传递,为什么只能打印却无法修改?其实Block截获这些数据的时候,都是值传递。也就是说,你把4传递了给Block里面的变量,那么你对变量修改的话,就应该是不被允许的,也无法修改到原来的变量(因为你只能修改到Block保存的变量啊)。第二种有点特殊,其实这个是字符串的地址的传递,其实可以想象一下,你把内存的地址赋值给了Block的指针。你能修改原来的指针吗?不行。所以也是不被允许的。
对于全局变量和静态全局变量、静态局部变量来说,却有所不同。对于全局变量和静态全局变量来说,在源代码转换后,仍然为全局变量和静态全局变量。而静态局部变量,则变成int 指针。传递的时候,也是传递静态局部变量的地址。static int static_value = 3;实际传值为&static_value,而方法调用赋值方式为(*static_value )=6;

__block int val = 10;
//编译后,变换为下面
struct __Block_byref_val_0 {
  void *__isa;
__Block_byref_val_0 *__forwarding;
 int __flags;
 int __size;
 int val;
};
//实际上变成了结构体实例
__Block_byref_val_0 val = {0,&val, 0, sizeof(__Block_byref_val_0), 10};

而赋值到Block结构体又是怎么样的呢?

  __Block_byref_val_0 *val = __cself->val; // bound by ref
      (val->__forwarding->val) = 5;

其实val->__forwarding->val就是原自动变量,并提供了__Block_byref_val_0的一对内存管理方法。为什么__Block_byref_val_0会有__forwarding并指向自己?其实是为了从栈上复制到堆上的时候,让栈上的__forwarding指向堆上的,这样就可以访问同一个变量。

为什么我们需要从栈拷贝到堆?

对Block进行copy的所有情况:

为什么我们需要进行额外的内存管理?

  1. 其实我们主要也是考虑从栈复制到堆的情况。当然除了拷贝Block,捕获的变量需要考虑吗?需要的,因为我们拷贝到堆,就是为了出了作用域还能用。如果我们对于捕获的变量也只是值拷贝的话。出了作用域,就会发生悬垂指针了。所以我们也需要把__block的栈变量也拷贝到堆。所以我们需要一个拷贝变量的方法。也就是说捕获的变量为id类型变量和__block修饰的变量,则需要调用Block_descriptor_2->copy()方法(其实就是内存管理的方法,id对象的话,为了出了作用域还能访问,你至少得retain操作对吧?如果是__block对象,也是为了出了作用域还能访问,那么你就得把__block修饰的结构体拷贝到堆上,那么肯定需要管理啦),该方法会调用到_Block_object_assign方法(管理捕获变量的内存)。
  2. 如果调用_Block_object_assign的结构体是个BLOCK_FIELD_IS_BYREF(也就是被__block的变量并且捕捉的对象是个id类型)的类型,那么除了会从栈上拷贝到堆上,还会调用__Block_byref_obj2_1上的__Block_byref_id_object_copy方法。其实上面一部分的原理是一样的,就不再重复了,所以你需要再做一次的内存管理,最终调用的还是_Block_object_assign方法。

除了前面讲的,我们最常见的捕获自由变量的情况分析:

  1. id类型,不带__block
    在MRC下,由于Block保存的也是id,也就是存储的是一个地址,你修改了Block里面的变量的地址,当然影响不了原来的id。但是可以根据这个地址找到对应的对象,从而进行一些操作。简单来说,就是不能修改对象,但是如果你这个对象是个NSArray类型,但是可以做添加元素的操作。有悬垂指针的尴尬。
    在ARC下,和上面的情况差不多,但是执行期间对象不会突然变为悬垂指针,因为ARC下默认就是__strong的修饰符。
  2. id,NSString *类型,带__block
    这种情况,为什么就可以修改了呢?这是因为被__block修饰的变量,其实是个存有id类型的结构体。而我们访问的数据,也是这个结构体里面的id对象,其实我们所捕获的自由变量。那么如果你是把这样结构体的id存储的地址修改了,那么就是相当于修改了外部的。也就是说,如果你在block再去访问这个id对象的时候,实际上就是通过结构体里面的id对象,编译器会自动转换的。
    内存管理,看看能不能考虑一下。
  3. int,int *类型,不带__block
    其实都是简单的值传递,只是指针变量传的是地址,而普通变量传的是数值。这种只是简单的值传递。Block的结构体会保存着他们的值,但是修改结构体里面的数据,并不会影响到外面的数据。
  4. int,int *类型,带__block。
    其实这种的变量,外面包了一层结构体,外部访问也是通过这个结构体访问(编译器会自动处理)。所以我们对结构体的修改(值传递),也会同时影响到外面。

为什么有些需要拷贝到堆,有的不需要?

这个时候,我们知道需要管理捕获变量的内存,并且是Block从栈拷贝到堆的时候,还有Block从堆上释放的时候。当然,只有捕获的变量为id, NSObject, attribute((NSObject)), block, …类型变量和__block修饰的变量才需要这个复制。为什么呢?其实原理都一样,堆上的当然需要管理啦。那为什么普通的指针类型char *test= "nihao";和普通变量int a = 3;不需要内存管理?其实只能说明他们都不需要修改,如果需要修改,那么还是一样,要加__block才行。

核心方法_Block_object_assign用来确定被捕获的变量怎样进行copy。

让我们分析一下为什么简单赋值就好

这时__block变量结构体已经从栈拷贝到堆上了,这个必须先理解的。当我们访问原变量的时候,是怎么访问的?还记得吗?obj2->__forwarding->obj2 = 新对象地址这样解决修改原变量的问题了吗?解决了,所以目的也已经达到了。出了作用域能访问吗?不能,因为__block还避免了MRC下被持有,ARC有自己的一套内存管理机制,所以默认情况下,还是会持有原变量,所以ARC下却可以继续访问。其实这个地方,必须得有取舍,在ARC下,如果是__strong 修饰的原变量,那么原变量将被持有,那么意味着,可能原变量,永远不会被释放。但是如果没有对原变量进行持有的话,那么意味的,需要进行持有,并且在不再需要原变量的时候需要进行释放。

__block修饰的变量,如果是id或者block类型,那么变量就会转换成下面这样的结构体。

struct __Block_byref_obj2_1 {
  void *__isa;
__Block_byref_obj2_1 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
 NSObject *__strong obj2;
};
// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime  
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
};
block类型,默认为0 
// flags/_flags类型 
enum { 
/* See function implementation for a more complete description of these fields and combinations */ 
// 是一个对象 
BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, attribute((NSObject)), block, … */ 
// 是一个block 
BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */ 
// 被__block修饰的变量 
BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */ 
// 被__weak修饰的变量,只能被辅助copy函数使用 
BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */ 
// block辅助函数调用(告诉内部实现不要进行retain或者copy)
BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */ 
};

block辅助函数调用的情况,其实就是__Block_byref_id_object_copy,131就是 BLOCK_FIELD_IS_OBJECT| BLOCK_BYREF_CALLER。

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

以下使用Block不用手动调用copy操作:

参考资料:
《Objective-C 高级编程 iOS与OS X多线程和内存管理》

上一篇下一篇

猜你喜欢

热点阅读