iOS 底层 Block 的类型 - AlvinSunny/OC-TheUnderlying GitHub Wiki

block的类型

block 有三种类型,可以通过调用class方法或者isa指针查看具体类型,但最终所有的都是继承自NSBlock。而NSBlock 继承自NSObject !

运行时 -->  __NSGlobalBlock__ ( 编译时 -->   _NSConcreteGlobalBlock ) 

运行时 -->  __NSMallocBlock__ ( 编译时 -->   _NSConcreteMallocBlock )

运行时 -->  __NSStackBlock__  ( 编译时 -->   _NSConcreteStackBlock )

使用上以运行时的类型为准,编译阶段和运行时不同主要是编译时有些数据可能无法确定,所以苹果在设计上做出的区别。众所周知OC的数据类型是动态的嘛 ,只有在运行时才能确定的!另_NSConcreteGlobalBlock这种类型是通过clang编译器生成的,可能和在运行时的有所不同是正常的。

继承关系举例

__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

为什么会有不同的block类型,主要的原因就是block存放的位置不同,分别有数据段、堆段、栈段,下面试MRC环境下的block类型

MRC环境 block类型.png

    int a = 110;
    NSLog(@"block : %@",[^{} class]);                 //__NSGlobalBlock__
    NSLog(@"block1 : %@",[^{a;} class]);            //__NSStackBlock__
    NSLog(@"block2 : %@",[[^{a;} copy] class]);//__NSMallocBlock__

注意:

  1. 在ARC环境对应的情况类型会有变化,因为ARC帮助我们做了内存管理
  2. 没有访问auto变量:其他变量是可以访问的,比如局部static变量 全局变量

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

1. block作为函数的返回值时;//Mansory 框架中经常用到
2. 将block赋值给强指针时;
3. block作为 Cocoa API中方法名,含有usingBlock的方法参数时;
4. block作为 GCD API的方法参数时;
5. block作为属性声明时;

应用程序中 block 的内存分配

QQ20200306-222711@2x.png

NSStackBlock 类型的 block 是存放在栈段的,栈的内存是由系统负责管理的,当一个方法执行结束后就会被销毁;这样是有风险的;比如该block中捕获了外部的局部变量,在该方法执行结束后再次调用block ,这时获取到的变量值可能就不准确了。如下示例:

QQ20200306-225246@2x.png

这时就需要用到 copy ,对block进行一次copy;这样block就会从栈段移动到堆段(堆:动态分配内存,需要开发者申请,也需要自己管理内存);这样block 中存储的变量就不会被提前释放。

QQ20200306-225622@2x.png

注意

__NSGlobalBlock__类型调用copy是没有意义的,因为这种类型的block不访问任何的auto变量;

NSMallocBlock调用copy还是在堆段的只不过引用计数器会加一;

QQ20200306-230238@2x.png

关于block属性copy声明


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

上面的代码在ARC环境下就等价于对一个block在定义后进行一次 copy 操作;

  XYHPerson *p = [[XYHPerson alloc] init];
  p.block = [^{

  } copy];

疑问

下面代码中的FuncPtr指向的方法是放在那里 ?


struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr; //指向将来需要执行的函数的地址
};

答 :是放在代码段。

函数调用栈

函数调用栈是指在调用某个函数时,在栈区分配一块栈空间给这个函数来使用;当函数调用完毕后这片空间就会被回收。这块数据就变垃圾数据,等待再次分配给其他地方使用。

宏定义和全局变量的区别

宏定义可以理解为一种替换,比如把某个数值替换成 age,在编译时又替换成对应数值;且宏定义不是变量

 #define Age 10

全局变量是在方法外部定义的变量,可以在任意地方访问;前提是定义时没有用static关键字修饰。

如何判断某个数据存储在哪里呢 ?

找三个已知的和一个想知道的通过NSLog的方式输出对比可能能够得到答案

内存探索.png

运行时数据存储方式有哪些 ?有什么作用

堆、栈、寄存器

堆:Objective-C对象一般申请内存是在堆空间, 并且堆内存释放ARC环境是由 runtime+LLVM 协作完成

栈: 非OC对象、系统默认创建的对象、指针一般放在栈里面(栈内存会被系统自动回收)

寄存器: CPU中的元件单元,是硬件,arm64之后方法中的隐藏参数self 、_cmd就是放在寄存器中的。