iOS 底层(深入理解blcok)

2020/7/15 23:10:14

本文主要是介绍iOS 底层(深入理解blcok),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

什么是block

带有自动变量(局部变量)的匿名函数。

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

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类型的结构体,它的内部结构如下
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);
}

复制代码
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结构体也可以转换成如下形式

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所占用内存大小等描述信息

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的底层结构

我们在使用Block的过程中,可以在Block内部访问外部的变量,包含局部变量、静态变量(相当于私有的全局变量)、全局变量等等。现在就通过一个Demo来看一下block底层是如何访问外部变量的。

  • 首先创建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中有变量捕获的机制

  1. 当访问局部变量的时候,会将局部变量的值捕获到block中,存放在一个同名的成员变量中。
  2. 当访问静态变量时,会将静态变量的地址捕获到block中,存放在一个同名的成员变量中
  3. 当访问全局变量时,因为全局变量是一直存在,不会销毁,所以在block中直接访问全局变量,不需要进行捕获
### auto 默认的关键字

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

block总结

block底层结构总结

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

block底层结构总结

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

block变量捕获总结

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

block的类型

在OC当中block其实拥有三种类型(共6种,另外3中系统生成不常见),可以通过class或者isa指针来查看block具体的类型。

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可以有三种类型,分别是NSGlobalBlock、NSStackBlock和NSMallocBlock,这三种类型分别存放在.data区、栈区和堆区。对应的结构图如下

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

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

但是不管它是哪种block类型,最终都是继承自NSBlock类型,而NSBlock继承自NSObject,所以这也说明了block本身就是一个对象。

如何区分block类型

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

  • 没有访问局部变量的block,并且没有强指针指向block,则此block为NSGlobalBlock
  • 访问了局部变量的block,但是没有强指针指向block,则此block为NSStackBlock
  • 访问了局部变量的block,并且有强指针指向block,则此block为NSMallocBlock
  • NSStackBlock类型的block,执行了copy操作之后,生成的block为NSMallocBlock

其实第三点和第四点生成的都是NSMallocBlock,由此我们就可以得到下面的结论

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

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

NSGlobalBlock、NSStackBlock和NSMallocBlock三种类型的block分别存放在了数据区、栈区和堆区。将三种类型的block分别进行copy操作之后,产生的结果如下:

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 访问对象类型

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

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 的本质

带有自动变量(局部变量)的匿名函数。 会捕获访问的变量

blok的变量捕获机制(auto 是默认的关键字和static对应)

由此我们就可以得出结论,block中有变量捕获的机制

  1. 当访问局部变量的时候,会将局部变量的值捕获到block中,存放在一个同名的成员变量中。
  2. 当访问静态变量时,会将静态变量的地址捕获到block中,存放在一个同名的成员变量中
  3. 当访问全局变量时,因为全局变量是一直存在,不会销毁,所以在block中直接访问全局变量,不需要进行捕获
  4. block的类型

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

    block的copy 效果

    什么情况下会对block进行copy

    1. block作为返回值时,会自动进行copy

    2. 使用__strong类型的指针指向block时,会执行copy操作

    3. block作为Cocoa API中含有usingBlock的方法的参数时,会执行copy操作

    4. block作为GCD方法的参数时会执行copy操作

    block 访问对象类型

    block 访问了对象,就会自动捕获。需要对其进行内存管理。 block内部会根据对象的修饰词,进行内部修饰。

    __main_block_desc_0结构体 内新增了2个函数copy和dispose两个函数指针,控制引用计数



这篇关于iOS 底层(深入理解blcok)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程