Block详细解读

众所周知,block可以封装一个匿名函数为对象,并捕获上下文所需的数据,并传给目标对象在适当的时候回调。正因为将抽象的函数具体化为一个可以存储管理的对象,block可以很容易被建立,管理,回调,销毁,也能很好的管理其执行所需要的数据,再加上即用即走和对代码逻辑上下文完整等优点,被大多数开发者广泛使用。虽然使用者很多,但还是有不少人对其实现和编译器背后如何支持还有一些疑惑,通过阅读本文相信你对block将会有一个比较清晰的认知。在解决一些棘手的内存问题的时候将会更加得心应手。

block的本质

首先写一个简单的block

int main(int argc, char * argv[]) {
    void(^blockA)(void) = ^{};
    blockA();
    return 0;
}

使用简单的clang main.m -rewrite-objc得到C++的main.cpp文件
关注我们感兴趣的部分,还是做个注释吧(64bit),熟悉这些偏移量比较重要,对分析问题很有帮助,block基础大小是32byte。

extern "C" _declspec(dllexport) void *_NSConcreteGlobalBlock[32];
extern "C" _declspec(dllexport) void *_NSConcreteStackBlock[32];
struct __block_impl {
  void *isa; //8byte,isa指针,很重要的标志,意味着block很可能是个OC的类
  int Flags;//4byte,包含的引用个数
  int Reserved;//4byte
  void *FuncPtr;//8byte,回调函数指针
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;//8byte,block的描述,辅助工具
  //如果有捕获外部变量的话会定义在这里
  ...
  __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) {
//自定义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)};//这里Block_size=32byte

int main(int argc, char * argv[]) {
    //定义函数指针,然后赋值上面静态函数,具体的代码实现被移到了上面的函数中
     void(*blockA)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
    //调用和传参数
     ((void (*)(__block_impl *))((__block_impl *)blockA)->FuncPtr)((__block_impl *)blockA);
     return 0;
}

我们可以拷贝这些代码来运行,但有几点需要注意的不要引入OC头文件,否则_NSConcreteStackBlock会重复定义,我们只需要定义同样的全局的变量来替代它或者删掉就可以了;main中第一句代码会报错taking the address of a temporary object of type '__main_block_impl_0', 这是因为这里调用了构造函数 _main_block_impl_0,这会生成一个临时返回值,在c++ 11语法里面这个返回值是个右值引用,是无法进行取地址运算的。所以这里改写一下就可以运行了,代码运行起来其调用过程就比较好办了,这里就不具体细说了。

    __main_block_impl_0 block_struct = __main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
    void (*blockA)() = (void (*)())&block_struct;
    
    void (*block_func)(struct main_block_impl_0 *__cself) = (void (*)(struct __main_block_impl_0 *))((__block_impl *)blockA)->FuncPtr;
    block_func((struct __main_block_impl_0 *)(*blockA));
       

注意:我这里改写的这个代码是存在一些问题的,因为block_struct是栈上的,所以一旦赋值给强引用时会copy一份放到堆上(GlobalBlock除外),调用block的时候可能已经超出block_struct的生命周期了。

接着看代码

在代码中我们看到了OC类的标志isa指针,并且指向_NSConcreteStackBlock,但具体是不是那么回事还是需要证明一下。毕竟编译和运行时还是有些不一样。

在bockA();这句加断点;在编译器debug窗口左侧有当前调用栈帧可见变量,找到blockA,发现 __isa=__NSGlobalBlock__和上面改写代码中的impl.isa = &_NSConcreteStackBlock还是有出入的,我们选择相信运行时。

选中__isa右键菜单View Memery of "__isa"可以浏览当前内存的值,我这里isa指向的地址是0x1003b8048,然后可以看到这里值是70 94 59 0b 01 00 00 00(8byte),小端机器,实际的数据是010B599470,观察代码void *_NSConcreteGlobalBlock[32]发现这是一个地址,对应的地址就是0x10B599470,管它是不是OC对象,我们在lldb下po 0x10B599470输出一下是__NSGlobalBlock__,呵,可能有谱,再追踪这个地址

Block详细解读_第1张图片
屏幕快照 2018-05-03 18.16.44.png

可以看到以下内存数据前8个byte是010B5994F0,顺便输出一下也打印了__NSGlobalBlock__,再看发现这个地址就在附近,里面记录的第一个数据是010780DE58,我们知道OC对象第一个数据就是isa指针,将其构建成地址0x10780de58,输出一下,打印了NSObject,这里可以理出关系blockA.isa->__NSGlobalBlock__.isa->NSObject,也就是说__NSGlobalBlock__的元类是NSObject,这基本可以证明__NSGlobalBlock__应该是个OC类型。

但我们希望得到最直接的证明就是一直找superclass直至找到NSObject

找到objc_class的定义,发现其继承自objc_object

struct objc_class : objc_object {
    Class superclass;//Class就是objc_class *
    ...
}
struct objc_object {
private:
    isa_t isa;
}

objc_object只有一个isa_t的数据

union isa_t {
    Class cls;
    uintptr_t bits;//是个unsigned long
    ...//带位域的struct,这里不关注
}

由此可见objc_class的前8byte是isa指针,第二个8byte是superclass指针。

我这里一次是0x010a6112a0(__NSGlobalBlock)0x10a611110(NSBlock)0x10780dea8(NSObject)

对照上面的NSObject,其地址0x10780de58,有点蒙,到底哪个对?其实都算对。

这里我定义了一个NSObject的对象,利用其运行时isa和superclass的数据,做了一个两者的关系图。

Block详细解读_第2张图片
屏幕快照 2018-05-03 18.28.27.png

还记得这个继承体系图,和上面结果一致。

Block详细解读_第3张图片
isa_superclass.jpg

从相关资料可以了解,OC中除了NSProxy外,其他的类都是NSObject的子类,包括元类,这个NSObject就是下图中的0x10780dea8。

OK,至此证明block是个OC对象,其继承自NSBlock,NSObject。

上面的环也可以解释一些经典的问题,比如(写本文的时候查资料时刚好遇到,就贴上来了):

    Class cls1 = [NSObject class];//0x10780dea8
    id cls2 = (id)[NSObject class];//0x10780dea8
    BOOL r1 = [cls2 isKindOfClass:cls1];//isa找到0x10780de58,再找到0x10780dea8比较
    BOOL r2 = [cls2 isMemberOfClass:cls1];//isa找到0x10780de58比较

  //User不存在这个环,也就不会出现这个现象
    Class cls3 = [User class];
    id cls4 = (id)[User class];
    BOOL r3 = [cls4 isKindOfClass:cls3];
    BOOL r4 = [cls4 isMemberOfClass:cls3];

    NSLog(@"%d  %d  %d  %d",r1, r2, r3, r4);//结果是 1 0 0 0

之所以费力的证明Block是个OC对象,是因为这可以更好的认知Block,得到很多的信息和用法。我们或许可以像用普通的OC对象一样使用Block,可以方便得被ARC管理,不用担心内存泄露或者非法访问。weak,autorelease等等也都可以使用,还可以放在集合里面,可以被别的对象持有,当然也可以持有别的对象,了解到这一点对于我们分析block的相关的内存管理和循环引用意义重大。

但在重写的C++的代码中我们看不到编译器帮我们插入的release,retain这样的代码,所以我们不得不用别的办法来了解Block具体是否被ARC管理的。

Block本身的内存管理

首先要明确一件事:Block本身内存的管理和Block捕获的对象的内存管理是两个问题。这里我们先讨论前者。

前面遗留了一个问题就是,代码里面isa指针明明指向了_NSConcreteStackBlock,怎么到了运行时的时候就变成了__NSGlobalBlock__

我们再做一个实验,将代码改为

void afunc() {
    __unsafe_unretained void(^blockA)(void) = ^{};
    blockA();
}

int main(int argc, char * argv[]) {
    afunc();
    return 0;
}

在blockA()处下个断点,查看debug数据,发现isa指针确实指向的是__NSGlobalBlock__,也就是说在这之前就被更新了,目前我还没有找到这个更新时机。

我们发现调用blockA()可以成功,没有crash,我尝试取了一下retainCount发现是1,去掉__unsafe_unretained也一样。

注意:通过kvc可以获取对象的引用计数,如果一个函数来打印对象的引用计数,这函数的参数声明是有讲究的

void printRetainCount(__unsafe_unretained id o) {
    void *p = (__bridge void *)o;
    NSLog(@"%p:%d",p,[o valueForKey:@"retainCount"]);
}

参数需要用__unsafe_unretained来修饰,最好不要用强引用,这会导致引用计数器+1,更不能用weak,这会导致每次使用weak对象的时候,retainCount都会增加,这个坑一不小心就会忽略,导致获取的数据可能不准确,关于这个问题具体情况以后有机会再讨论。

修改代码增加全局__weak void(^blockB)(void) = nil;,并在afunc()对其赋值,在main()中调用blockB(),发现也可以调用成功,并没有crash。通过__NSGlobalBlock__这个名字大概可以猜测出这个是一个全局的block,其生命周期全局有效,即使主动调用copy,也不会入堆,似乎不受ARC控制。对照源码可知,globalblock其实并不依赖外部数据,只要有代码入口就可以使用,甚至不需要知道block,只有有函数入口地址就可以直接调用,而另外两种都需要通过block去调用,而不能直接调用block内函数指针(当然要是自己准备各种参数也是可以的)。

将代码修改为:

void afunc() {
    int a = 100;
    __unsafe_unretained void(^blockA)(void) = ^{
        int b = a;
    };
    blockA();
}

int main(int argc, char * argv[]) {
    afunc();
    return 0;
}

在blockA()处下个断点,查看debug数据,发现isa指针指向的是__NSStackBlock__,去掉 __unsafe_unretained后isa指针变成了__NSMallocBlock__

我们发现调用blockA()可以成功,没有crash,我尝试取了一下retainCount发现是1,去掉__unsafe_unretained也一样,,跟一般的OC对象不太一样?

再次修改代码和上面一下增加__weak void(^blockB)(void) = nil,在main中调用blockB(),发现其crash了,证明blockB已经无效了。再次将__unsafe_unretained修改为__autoreleasing,发现其可以调用成功,所以证明block此时被autoreleasepool接管了,看上去ARC还是有作用的。

那么在ARC下,如果增加一个强引用指向block会不会导致retainCount增加呢?通过实验发现不会,依旧是1,这一点又和普通的对象不太一样。

难道这就是真相,no,这无法解释之前观察到的各种现象。我多次运行,多次调用并打印block的地址,发现其地址都一样。

Printing description of *(blockA).__FuncPtr:
(void (*)(NSString *, int)) __FuncPtr = 0x0000000100f66570 (Block`__afunc_block_invoke at main.m:58)

仔细一看,发现其打印的地址和__FuncPtr地址一样,那么同理取的block的retainCount也就可有能不正确,去objc源码中搜索了一下发现其实现为

-(NSUInteger)retainCount {
    return (_rc_ivar + 2) >> 1;                                             
}     

也就是说,只要对象没有被释放,那么其retainCount至少是1。换句话说,如果某个对象没有_rc_ivar,或者_rc_ivar=0,那么其结果都是1,所以这里通过KVC取retainCount在block这里并不可靠,因为ARC机制下并不允许访问retainCount,所以其可靠性在有些情况还是会受到质疑的,不足以作为判断标准。但是我们发现一个问题,就是分配在栈上的block出了作用域已经无效了,那也就是说block应该在一定程度上受到ARC机制的约束,这需要进一步求证。

还记isa_a定义么,接下来我们去撸一下完整源码:

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
}

我们这次只关注其中shiftcls和extra_rc,前者是存放isa指针存储数据的真正空间,后者是存放对象额外引用计数的,如果这里19 bits还不够的话,就使用sidetable来记录。也就是说,绝大部分情况下,引用计数器是存在对象的isa里面的,所以我们只需要去查看isa的内存值,解析最后19bits的值就可以得到引用计数。

打个断点,在这里选择debug窗口左侧的variables view窗口,选中某个block指针,右键 view memery of "*blockA",可能不能浏览,所以view memery of "blockA",我这里是内存地址0x16ee9f8f8存储了以下数据

90 54 44 C0 01 00 00 00

将其构建出地址0x01c0555490,在Address中输入该值,跳转到新地址,我这里结果如下:

88 FF A7 B3 01 00 00 00    02 00 00 C3 00 00 00 00    70 65 F6 00 01 00 00 00

看到最后一个8个byte,有点眼熟,就是之前打印的__FuncPtr = 0x0000000100f66570。那前俩byte存的是啥呢?第一个byte明显是一个指针,打印一下,就是__NSMallocBlock__,那剩下的8byte呢?从block的数据结构了解到其对应的就是

  int Flags;//4byte,包含的引用个数
  int Reserved;//4byte

后者是0,前者是有值而且会变化,我尝试再给block一个强引用,发现02 00 00 C3变成了04 00 00 C3,再赋值就变成了06 00 00 C3,所以这个06应该就是引用计数器,而且也符合retainCount的运算逻辑,从内存布局上看,19bits的存储位置应该在一个8-byte的末尾,也就是包含02这段空间,但只是不太了解为啥isa被分成了两个64bits存储。

同理我尝试了仅仅在stack上的block,其数据位00 00 00 C2,计数器为0。

同理我尝试了global的block数据位00 00 00 50,计数器为0。

结果符合预期,除了进入堆上block会受ARC约束,其他的block都不需要ARC参与就可以完成内存管理。

小结
  1. Block如果不捕获外界变量,就没有上下文依赖,编译器会将其标记为global类型(当然可能编译器标记为stack,运行时优化glabal常量);否则编译器会在创建时将其标记为stack,当运行时对象被强引用时或者主动调用copy会被标记为malloc类型。
  2. global和stack的block都不需要ARC参与内存管理。malloc的block将受到ARC管理,包括autorelease和weak。

Block参数传递

前面的小节研究了Block的本质和其本身的内存管理,我们几乎可以把他当做普通对象来使用,同时其拥有唯一的成员函数,其执行所要依赖的数据来源有两个,一个是当前上下文环境的各种变量,另外就是调用方的传参。block传参和函数传参并没有什么不同,这里就不做具体讨论。

Block如何捕获外界变量

之前为了重写简单我并没有引入OC基础框架,而要将一般的OC代码转成C++,比如以下代码就引用了NSString:

typedef void (^ABlock)(void);

ABlock afunc() {
    NSString *a = @"this is a demo";
    void(^blockA)(void) = ^{
         NSString *b = a;
    };
    return blockA;
}

int main(int argc, char *argv[]) {
    ABlock aBlock = afunc();
    aBlock();
    return 0;
}

对于这类引用了OC类型的代码,需要使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.10 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk main.m可以将OC代码转成C++代码。

这里需要指定编译的sdk库(我使用的是iPhoneSimulator.sdk),否则会出现“UIKit/UIKit.h” file not found,还需要指定-fobjc-arc开启ARC的编译开关和-fobjc-runtime=macosx-10.10,否则会出现“cannot create __weak reference in file using manual reference counting”类似的错误。

编译真机的话需要指定支持的CPU架构和库等(折腾了挺久才试出这些参数,)clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-10.0 -arch arm64 -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk main.m

编译器会根据捕获的原始变量的不同情况,定义不同类型的变量来存储这些数据。

根据变量定义类型,这里我分成以下几类:

  1. 以下是捕获一个基本类型临时变量i的c++代码
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  //如果有捕获外部变量的话会定义在这里
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到,编译器定义了一个int i来对应外界的int i,同时接收了外界i的值来初始化。

  1. 同理如果是局部指针这里将定义为对应的指针,例如这里是 int *ip。
  2. 如果需要捕获的是一个局部OC对象,其实和2中情况一致,不同之处在于ARC会介这个对象的管理。
  3. 对于全局变量,因为访问是开放的,所以编译器不需要做处理,直接使用该变量就行。

根据变量定义的附加修饰特性:

  1. 对于局部static变量,因为访问不开放,所以会被编译器升级为指针,例如:static int staticInt = 100,会定义一个int *staticInt来捕获staticInt的地址,以便以后修改。
  2. 对于__weak修饰的对象引用,这个重点说明。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  NSString *__weak weakSelf;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *__weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

可以看到对于__weak修饰的引用,编译器也在Block中定义一个一模一样的引用,然后通过构造函数通过参数传入初始化结构体(c++中struct和class绝大部分情况是等效的),这是意味着什么呢?我们知道所有函数参数传递就一种方式——copy,这里的参数捕获间接套用了该性质。一句话来说,对象还是那个对象,但引用已经不是那个引用了。

Block详细解读_第4张图片
屏幕快照 2018-05-14 17.21.39.png

这里我画了一个简图,实际上每个weakSelf都是不一样的,只是其指示的内容是同样的。

  1. __block修饰的对象,这里改写为c++代码出错,我没能解决这个问题。所以就只有推测了,其做法应该和局部的static变量捕获差不多,都会定义一个同类型的指针或者引用,以便可以在block中访问该变量修改变量。
小结
  1. 参数捕获和参数传递,前者发生在block定义初始化的时候,是对当前现场的一种保存,后者发生在调用的时候传参,其存储上的区别是前者是成员变量持续存储,后者是临时变量。相同之处就是获取方式完全一致,都是函数参数传递。
  2. 编译器会对待不同类型的参数捕获处理方式都一样,全部浅拷贝;对于不同修饰参数则不太一样,会根据不同的情况来决定是否升级为指针捕获;OC对象将会引入ARC机制去管理。

Block循环引用及解决办法

如果能明确认识到block就是个对象,那么造成循环引用的原因就不难理解了,block可以持有对象也可以被对象持有,如果两者直接或者间接包含同一对象时就成了环,实际上就是object->block(->…)->object。

那么为什么用weak strong dance就可以解决这个问题呢?看下面这个典型例子。

__weak typeof(self) weakSelf = self;
void (^block)(void) = ^{
  __strong typeof(weakSelf) strongSelf = weakSelf;  
};

通过前面的C++的代码分析,答案已经很清晰了,这里就再解释一次:

我们知道block外部定义了一个weakSelf(为了方便说明,可以认为是weakSelf1),而在block内部并没有直接使用这个weakSelf1(就是没有使用这个weakSelf1这个硬编码或者说其对应的地址),而是另外定义了一个对应的构造函数参数__weak weakSelf(weakSelf2),通过指针copy传参的方式,weakSelf2指向了weakSelf1指向的内容,同时block内部的成员变量 __weak weakSelf(weakSelf3)通过weakSelf赋值也指向了weakSelf1指向的内容。所以从始至终这些weakSelf都不是同一个东西。至于strongSelf就简单了,对象赋值给强引用会导致retainCount+1,还记我之前文章里面的观点么,ARC是用栈管理引用,用引用生命周期管理对象,所有strongSelf生命周期结束,自然retainCount-1。
所以在block还没有执行的时候,self的生命周期不受block影响,如果执行的时候self已经被释放, weakSelf3=nil,也不会导致问题,但是如果weakSelf3还有值,strongSelf就会导致retainCount+1。有很多人认为,无论如何必须等到block执行完或者销毁self才会释放是不正确的。仔细对照block和delegate就会发现两者在这方面其实本质是一样的的,如果delegate不使用weak也一样可能循环引用。还是那句话,内存中通信就一个招,拿到地址,所以无论是直接的delegate,block,target-action,还是间接的Notification,或者其他的玩法都一样。

注意:__strong不能省略。

当然并不是说见到block就需要weak strong dance,对于以下情况就可以不使用(从调用方和回调方分析)

  1. 如果能确定block的调用方并不会长期持有block,比如传给B一个A持有的block,B并不存储,而是立刻回调,常见的就是把block当函数参数传递。
  2. 如果确定block调用方会在必要的时候去除强引用,比如:dispatch_async,其虽然会被队列强引用,但在block回调的时候,_dispatch_call_block_and_release会在执行完release,这也不会导致循环引用。
  3. block创建方不会直接或间接强引用block。
  4. 对于绝不可能持有block的对象,可以放心捕获,比如NSString,NSDate,NSURL等等,但对于一些可能存储block需要小心,比如:NSArray,NSDictionary,自定义的对象(self)。

如果你是创建方,不想去分析也不知道调用方干了什么,建议就无脑weak strong dance,几乎可以就可以解决问题了。如果你是调用方,会麻烦一些,需要具体问题具体分析。

Block捕获对象的内存管理

这分成三个方面,如果只是基本类型,那就不需要操心;如果是C指针,那指向对象的生命周期需要开发者手动管理;如果是个OC对象,内存管理由ARC代劳,只需要注意一些特殊情况就好。前两者不做讨论,研究一下后者。

typedef void(^ABlock)();
void pc(__unsafe_unretained id o) {
    void *p = (__bridge void *)o;
    NSLog(@"%@ %p:%@",o, p, [o valueForKey:@"retainCount"]);
}
@interface BlockDemo : NSObject
@end

@implementation BlockDemo
static int global = 1000;

- (ABlock)afunc:(NSString *)string {
    pc(self);
    pc(string);
    
    ABlock b;
    ABlock c;
    
    __weak typeof(self) weakSelf = self;
    b = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        pc(strongSelf);
        pc(string);
    };
    
    c = b;
    
    b();
    pc(self);
    pc(string);
    return b;
}
@end


int main(int argc, char * argv[]) {
    NSString *string = [[NSString alloc] initWithUTF8String:"this is a demo"];
    pc(string);
    BlockDemo *block = [BlockDemo new];
    ABlock a = [block afunc:string];
    a();    
}

此时输出日志如下:

2018-05-15 11:44:46.626942+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:1

2018-05-15 11:44:46.627326+0800 Demo2[3175:1513369]  0x1c400e690:1
2018-05-15 11:44:46.627515+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2

2018-05-15 11:44:46.627588+0800 Demo2[3175:1513369]  0x1c400e690:2
2018-05-15 11:44:46.627719+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4

2018-05-15 11:44:46.627786+0800 Demo2[3175:1513369]  0x1c400e690:1
2018-05-15 11:44:46.627810+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:4

2018-05-15 11:44:46.627995+0800 Demo2[3175:1513369]  0x1c400e690:2
2018-05-15 11:44:46.628072+0800 Demo2[3175:1513369] this is a demo 0x1c403cc40:2

可以看到BlockDemo只在main中被持有+1,然后调用时被strongSelf持有+1,weakSelf并没导致引用计数器增加,与输出日志相符。

进入afunc:时,string引用计数为2,一个发生在main,一个发生在afunc:的参数中声明中。调用b()时,retainCount=4,这是因为这是存在一个StackBlock和一个MallocBlock,这两个都会有一个引用指向string。在afunc:函数末尾打印string是4也是同样的原因;当afunc:执行完后,StackBlock已经释放,返回block给main中的a,此时调用a(),输出2,其中引用来自于MallocBlock,符合预期。

c=b这句赋值,只引起block计数器增加,而不会导致捕获OC对象引用计数器增加,符合预期。

我们在lldb设置俩符号断点:

breakpoint set -n _Block_copy
breakpoint set -n _Block_release

可以发现_Block_copy和普通对象retain时机调用类似。

需要注意的问题

  1. block捕获变量防止循环引用容易漏掉一些情况,在捕获时需要多注意,举个例子,直接捕获成员变量。

    假设在一个对象方法里面,比如ViewController
    void (^block)(void) = ^{
        //这里是等效于self->_name,编译器编码为self+offset(_name),依然会导致强引用
         NSString *name = _name; 
    };
    
  1. 直接修改捕获的变量不能成功,因为里外的两个array不是一个array,需要加上__block,变量捕获通过函数传参的方式实现,而传参全是copy。

    NSArray *array = @[@1,@2];
    void (^block)(void) = ^{
        array = @[];
    };
    

附加内容

之前从宏观层面了解了block和其捕获对象的生命周期,但具体是怎样还是不太清晰,有兴趣的话可以看下面一段内容,具体了解block是怎么玩的,汇编较长,看起来也比较绕,没兴趣的话可以忽略,看看其中的一些要点。
源码:

typedef void (^ABlock)(void);

ABlock afunc() {
    NSString *a = @"demo";
    void(^blockA)(void) = ^{
         NSString *b = a;
    };
    return blockA;
}

int main(int argc, char *argv[]) {
    ABlock aBlock = afunc();
    aBlock();
    return 0;
}

汇编:

    .section    __TEXT,__text,regular,pure_instructions
    .ios_version_min 11, 0
    .file   1 "/Users/Wei/File/program/Block" "/Users/Wei/File/program/Block/Block/main.m"
    .globl  _afunc                  ; -- Begin function afunc
    .p2align    2
_afunc:                                 ; @afunc
Lfunc_begin0:
    .loc    1 33 0                  ; /Users/Wei/File/program/Block/Block/main.m:33:0
    .cfi_startproc
; BB#0:
    sub sp, sp, #112            ; =112
    stp x29, x30, [sp, #96]     ; 8-byte Folded Spill
    add x29, sp, #96            ; =96
Lcfi0:
    .cfi_def_cfa w29, 16
Lcfi1:
    .cfi_offset w30, -8
Lcfi2:
    .cfi_offset w29, -16
Ltmp0:
    .loc    1 34 15 prologue_end    ; /Users/Wei/File/program/Block/Block/main.m:34:15
    adrp    x0, l__unnamed_cfstring_@PAGE
    add x0, x0, l__unnamed_cfstring_@PAGEOFF
    bl  _objc_retain
    stur    x0, [x29, #-8]
    add x0, sp, #40             ; =40
    .loc    1 36 11                 ; /Users/Wei/File/program/Block/Block/main.m:36:11
    add x30, x0, #32            ; =32
    .loc    1 36 27 is_stmt 0       ; /Users/Wei/File/program/Block/Block/main.m:36:27
    adrp    x8, __NSConcreteStackBlock@GOTPAGE
    ldr x8, [x8, __NSConcreteStackBlock@GOTPAGEOFF]
    str x8, [sp, #40]
    mov w9, #-1040187392
    str w9, [sp, #48]
    mov w9, #0
    str w9, [sp, #52]
    adrp    x8, ___afunc_block_invoke@PAGE
    add x8, x8, ___afunc_block_invoke@PAGEOFF
    str x8, [sp, #56]
    adrp    x8, ___block_descriptor_tmp@PAGE
    add x8, x8, ___block_descriptor_tmp@PAGEOFF
    str x8, [sp, #64]
    ldur    x8, [x29, #-8]
    str x0, [sp, #32]           ; 8-byte Folded Spill
    mov  x0, x8
    str x30, [sp, #24]          ; 8-byte Folded Spill
    bl  _objc_retain
    str x0, [sp, #72]
    .loc    1 36 11                 ; /Users/Wei/File/program/Block/Block/main.m:36:11
    ldr x0, [sp, #32]           ; 8-byte Folded Reload
    bl  _objc_retainBlock
    stur    x0, [x29, #-16]
    .loc    1 44 12 is_stmt 1       ; /Users/Wei/File/program/Block/Block/main.m:44:12
    ldur    x0, [x29, #-16]
    bl  _objc_retainBlock
    sub x8, x29, #16            ; =16
    mov x30, #0
    .loc    1 45 1                  ; /Users/Wei/File/program/Block/Block/main.m:45:1
    str x0, [sp, #16]           ; 8-byte Folded Spill
    mov  x0, x8
    mov  x1, x30
    str x30, [sp, #8]           ; 8-byte Folded Spill
    bl  _objc_storeStrong
    ldr x0, [sp, #24]           ; 8-byte Folded Reload
    ldr x1, [sp, #8]            ; 8-byte Folded Reload
    bl  _objc_storeStrong
    sub x0, x29, #8             ; =8
    ldr x1, [sp, #8]            ; 8-byte Folded Reload
    bl  _objc_storeStrong
    ldr x0, [sp, #16]           ; 8-byte Folded Reload
    ldp x29, x30, [sp, #96]     ; 8-byte Folded Reload
    add sp, sp, #112            ; =112
    b   _objc_autoreleaseReturnValue
Ltmp1:
Lfunc_end0:
    .cfi_endproc
                                        ; -- End function
    .p2align    2               ; -- Begin function __afunc_block_invoke
___afunc_block_invoke:                  ; @__afunc_block_invoke
Lfunc_begin1:
    .loc    1 36 0                  ; /Users/Wei/File/program/Block/Block/main.m:36:0
    .cfi_startproc
; BB#0:
    sub sp, sp, #48             ; =48
    stp x29, x30, [sp, #32]     ; 8-byte Folded Spill
    add x29, sp, #32            ; =32
Lcfi3:
    .cfi_def_cfa w29, 16
Lcfi4:
    .cfi_offset w30, -8
Lcfi5:
    .cfi_offset w29, -16
    stur    x0, [x29, #-8]//sp+24的位置
Ltmp2:
    .loc    1 36 28 prologue_end    ; /Users/Wei/File/program/Block/Block/main.m:36:28
    mov  x8, x0
    str x8, [sp, #16]
Ltmp3:
    .loc    1 37 20                 ; /Users/Wei/File/program/Block/Block/main.m:37:20
    ldr x8, [x0, #32] //x0是block首地址,x0+32是捕获的第一个变量位置,就是NSString
    mov  x0, x8
    bl  _objc_retain
    mov x8, #0
    add x30, sp, #8             ; =8
    str x0, [sp, #8]  //将其存在了栈上sp+8的位置,就是b变量
Ltmp4:
    .loc    1 38 5                  ; /Users/Wei/File/program/Block/Block/main.m:38:5
    mov  x0, x30
    mov  x1, x8
    bl  _objc_storeStrong
    ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
    add sp, sp, #48             ; =48
    ret
Ltmp5:
Lfunc_end1:
    .cfi_endproc
                                        ; -- End function
    .p2align    2               ; -- Begin function __copy_helper_block_
___copy_helper_block_:                  ; @__copy_helper_block_
Lfunc_begin2:
    .loc    1 38 0                  ; /Users/Wei/File/program/Block/Block/main.m:38:0
    .cfi_startproc
; BB#0:
    sub sp, sp, #48             ; =48
    stp x29, x30, [sp, #32]     ; 8-byte Folded Spill
    add x29, sp, #32            ; =32
Lcfi6:
    .cfi_def_cfa w29, 16
Lcfi7:
    .cfi_offset w30, -8
Lcfi8:
    .cfi_offset w29, -16
    mov x8, #0
    stur    x0, [x29, #-8] //目标地址
    str x1, [sp, #16]  //block
Ltmp6:
    .loc    1 36 27 prologue_end    ; /Users/Wei/File/program/Block/Block/main.m:36:27
    ldr x0, [sp, #16]//block
    ldur    x1, [x29, #-8]//目标地址
    mov  x9, x1
    add x9, x9, #32    //目标地址         ; =32
    ldr x0, [x0, #32]  //对象a
    str x8, [x1, #32]
    str x0, [sp, #8]            ; 8-byte Folded Spill
    mov  x0, x9
    ldr x1, [sp, #8] //对象a           ; 8-byte Folded Reload
    bl  _objc_storeStrong
    ldp x29, x30, [sp, #32]     ; 8-byte Folded Reload
    add sp, sp, #48             ; =48
    ret
Ltmp7:
Lfunc_end2:
    .cfi_endproc
                                        ; -- End function
    .p2align    2               ; -- Begin function __destroy_helper_block_
___destroy_helper_block_:               ; @__destroy_helper_block_
Lfunc_begin3:
    .loc    1 36 0                  ; /Users/Wei/File/program/Block/Block/main.m:36:0
    .cfi_startproc
; BB#0:
    sub sp, sp, #32             ; =32
    stp x29, x30, [sp, #16]     ; 8-byte Folded Spill
    add x29, sp, #16            ; =16
Lcfi9:
    .cfi_def_cfa w29, 16
Lcfi10:
    .cfi_offset w30, -8
Lcfi11:
    .cfi_offset w29, -16
    mov x8, #0
    str x0, [sp, #8]
Ltmp8:
    .loc    1 36 27 prologue_end    ; /Users/Wei/File/program/Block/Block/main.m:36:27
    ldr x0, [sp, #8]
    add x0, x0, #32             ; =32
    mov  x1, x8
    bl  _objc_storeStrong
    ldp x29, x30, [sp, #16]     ; 8-byte Folded Reload
    add sp, sp, #32             ; =32
    ret
Ltmp9:
Lfunc_end3:
    .cfi_endproc
                                        ; -- End function
    .globl  _main                   ; -- Begin function main
    .p2align    2
_main:                                  ; @main
Lfunc_begin4:
    .loc    1 47 0                  ; /Users/Wei/File/program/Block/Block/main.m:47:0
    .cfi_startproc
; BB#0:
    sub sp, sp, #64             ; =64
    stp x29, x30, [sp, #48]     ; 8-byte Folded Spill
    add x29, sp, #48            ; =48
Lcfi12:
    .cfi_def_cfa w29, 16
Lcfi13:
    .cfi_offset w30, -8
Lcfi14:
    .cfi_offset w29, -16
    stur    wzr, [x29, #-4]
    stur    w0, [x29, #-8]
    stur    x1, [x29, #-16]
Ltmp10:
    .loc    1 50 16 prologue_end    ; /Users/Wei/File/program/Block/Block/main.m:50:16
    bl  _afunc
    .loc    1 50 12 is_stmt 0       ; /Users/Wei/File/program/Block/Block/main.m:50:12
    ; InlineAsm Start
    mov  x29, x29   ; marker for objc_retainAutoreleaseReturnValue
    ; InlineAsm End
    bl  _objc_retainAutoreleasedReturnValue
    str x0, [sp, #24]
    .loc    1 51 5 is_stmt 1        ; /Users/Wei/File/program/Block/Block/main.m:51:5
    ldr x0, [sp, #24]
    mov  x1, x0
    ldr x0, [x0, #16]
    str x0, [sp, #16]           ; 8-byte Folded Spill
    mov  x0, x1
    ldr x1, [sp, #16]           ; 8-byte Folded Reload
    blr x1
    mov x0, #0
    add x1, sp, #24             ; =24
    .loc    1 62 5                  ; /Users/Wei/File/program/Block/Block/main.m:62:5
    stur    wzr, [x29, #-4]
    .loc    1 63 1                  ; /Users/Wei/File/program/Block/Block/main.m:63:1
    str x0, [sp, #8]            ; 8-byte Folded Spill
    mov  x0, x1
    ldr x1, [sp, #8]            ; 8-byte Folded Reload
    bl  _objc_storeStrong
    ldur    w0, [x29, #-4]
    ldp x29, x30, [sp, #48]     ; 8-byte Folded Reload
    add sp, sp, #64             ; =64
    ret
Ltmp11:
Lfunc_end4:
    .cfi_endproc

我们需要从上层函数入手,只有了解了传入的参数具体分析,才比较容易了解代码功能,不然就头疼了,这里就是先分析main函数。

main:

  1. bl _afunc,没有参数直接跳转,从源码可知返回了一个block对象在x0中,bl _objc_retainAutoreleasedReturnValue表明其被autoreleasepool管理。
  2. 接下将x0存在了sp+24这里,再下一句没有啥意义。
  3. 将x0赋值给x1,挪出空间,加载x0+16的值到x0,找到最开始struct __block_impl的内存布局,发现这个地址存放的是回调函数的指针。
  4. 接下来通过[sp, #16]中转,将x1和x0内容互换,至此x0是block首地址,x1是回调函数地址;blr x1跳转到x1;2,3,4步加一起就是源码里面的aBlock()。
  5. 最后那段就是在release之前的block对象。

_afunc:

最前面栈增长了112个byte,这里局部变量较多所以,栈分配的较大。

  1. 直接看Ltmp0:,前面一个_objc_retain是引用了字符串"demo"

. stur x0, [x29, #-8],将x0("demo")存在了x29-8这个位置,也就是sp+88的位置。

  1. 然后x0=sp+40,x30=sp+72

  2. 接下来两句加载"__NSConcreteStackBlock"符号对应的地址,然后将其存在sp+40这个地址,而x0目前是指向这个地址的。

    struct __block_impl {
      void *isa; //8byte,isa指针,很重要的标志,意味着block很可能是个OC的类
      int Flags;//4byte,包含的引用个数
      int Reserved;//4byte
      void *FuncPtr;//8byte,回调函数指针
    };
    
    struct __main_block_impl_0 {
      struct __block_impl impl;
      struct __main_block_desc_0* Desc;//8byte,block的描述
      //如果有捕获外部变量的话会定义在这里
      id a;
    };
    

    这里贴一个内存布局,sp+40就是__main_block_impl_0首地址,也是isa地址,这里就是“StackBlock”类似符号。

  3. 接下来就简单了,依次存储了Flags(sp+48),Reserved(sp+52)各4个byte。

  4. 存储FuncPtr指针,就是汇编符号___afunc_block_invoke对应的地址,sp+56的8个byte。arm64指针占8个byte。

  5. 存储Desc,这也是个指针,存储的是___block_descriptor_tmp对应的地址,其记录了___copy_helper_block___destory_helper_block和method signature,block size等信息,对应sp+64的8个byte。

  6. 接下来加载x29-8的内容到x8就是"demo"字符串。然后将x0暂存在sp+32,同时将x0=x8,然后存储x30到sp+24

  7. 调用_objc_retain,参数x0,所以结果是retain了“demo”一次。之后将x0存储在了sp+72这里,就是struct __main_block_impl_0 中的id aid a是我随意写的,但实际定义也应该差不多的。需要注意的是,这里的调用是因为创建了一个StackBlock,其也是要使用"demo"这个数据的,所以retain了一次。但MallocBlock的引用计数则由___copy_helper_block来管理。)

  8. 然后将sp+32的内容加载到x0,也就是sp+40即__main_block_impl_0的首地址。

  9. 调用_objc_retainBlock,去苹果源码中找一下:

id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}

其调用了_Block_copy,这个函数在Block.h中声明的,我没有找到相关的实现源码,不过可以证明的是对于block来说retain和copy效果一致。

  1. 再次调用了_objc_retainBlock

    这里的两次调用一次是赋值给blockA,一次是return blockA造成的。

  2. 后面有三个_objc_storeStrong,x1=0,都是在做release操作,具体过程比较繁琐,我就直接给结论了,其中第一个release一次blockA,第二release一次“demo”字符串(提示:sp+24存的是x30,x30=sp+40+32,这里存的是“demo”),第三个也是release一次"demo"(sub x0, x29, #8,提示一下,x29=sp+112-96,所以这句也是x0=sp+24)

    通过这里的分析,对于Block捕获对象,ARC怎么作用的,相应的数据结构:block retain会被copy,捕获OC类型参数的时候会retain参数,参数的传递方式——拷贝。

___afunc_block_invoke:

  1. ldr x8, [x0, #32],x0是block的首地址,x0+32就是变量a的地址,mov x0,x8,bl _objc_retain,retain了a这个对象,和源码功能一致。
  2. 后面就是在release这个对象,源码里面也确实没有别的操作了。

___copy_helper_block:

这个这里找不到调用方,所以传递的参数就无法知道。先尝试分析一下:看它使用了x0,x1俩寄存器,应该有俩参数。所以顺序分析可能就不太好使了,但我们发现这里就只调用了_objc_storeStrong,这个函数就比较熟悉了,第一个参数是id *,第二个参数是id,那我们就倒着分析。x0=x9,x9=x9+32,x9=x1,x1=(x29-8)=x0,所以x0=x0+32,再看x1=(sp+8)=x0=[x0+32]=(sp+16)=x1,所以x1=[x1+32](其中小括号是内存暂存,方括号是加载该内存地址的数据)。见到“+32”偏移量就熟悉了,这就是之前a的地址,整个函数的功能就是retain并且store一下block中捕获的变量a,如果有多个引用将会有多次这种操作,但不适用于基本数据类型。

理论分析确实很麻烦,但这里提供另外一种办法就是运行下断点breakpoint set -n __copy_helper_block_,打印x0,x1

可以看到在_Block_copy中调用这个函数,打印一下

(lldb) po $x0
<__NSStackBlock__: 0x1c0250680>

(lldb) po $x1
<__NSStackBlock__: 0x16afeb8f8>

这里也可以通过直接浏览的方式

Block详细解读_第5张图片
view_reg.png

_Block_copy调用时其还是在栈block,不同的是虽然x0(目标地址)的isa还是指向StackBlock,但实际内容已经是MallocBlock,比如x0已经产生引用计数了。(我研究了一下_Block_copy汇编代码,其会malloc一段新的内存,将数据填充过去,同时修改Flags的值,Reserved字段全被赋值为了0,x0是新地址,x1是旧地址,然后跳转__copy_helper_block_,做OC参数的retain操作)

___destroy_helper_block:

这个调用_objc_storeStrong,x1=x8=0,很明显是在做release。

总结一下:
  1. 每一个block背后都有一个struct做数据支撑,与一般的对象的结构组织和行为模式基本一致。一般的对象是一份数据结构可以对应多个方法,而block却是一个方法对应多个数据,导致其占用资源较多。
  2. Block的OC类型参数捕获时,如果只是栈Block,则直接插入retain和release解决对象引用的问题。如果Block对象被拷贝到堆上,则需要通过调用_Block_copy通过对应的的___copy_helper_block___destroy_helper_block函数来支撑捕获对象的生命周期管理。
  3. __main_block_desc_0还会同时保存的方法签名(这里是v8@?0),还有block的大小,捕获参数个数会造成这个大小的改变。

最后说一下Block零碎的东西

  1. 在Block_private.h文件中发现,除了我们熟知的三种block意外还有三种运行时的block

    BLOCK_EXPORT void * _NSConcreteAutoBlock[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32]
        __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2);
    

    目前还不知道具体用途。

  2. BLOCK_EXPORT size_t Block_size(void *aBlock);
    
    BLOCK_EXPORT const char * _Block_signature(void *aBlock)
        __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
        
    BLOCK_EXPORT const char * _Block_layout(void *aBlock)
        __OSX_AVAILABLE_STARTING(__MAC_10_7, __IPHONE_4_3);
    

    这三个函数我比较感兴趣,但却是在Block_private.h中,但不碍事,知道名字了,就好办了。

    我们知道.h头文件的一个重要作用就是编译指示,啥意思呢?简单来说就是告诉编译器当前环境的其他编译模块有某些符号,到时候编译器自己去寻找并链接。所以我们只需要在使用前做以下声明就行

    extern const char *_Block_signature(id block);
    extern size_t Block_size(id block);
    extern const char* _Block_layout(id ablock);
    

    我调用了一下,发现_Block_layout输出是个null,不清楚有啥作用。

    Block_size调用结果是40,因为捕获了一个NSString*(8byte)+ Block基础的32byte,正好。

    _Block_signature输出 v20@?0@"NSString"8i16,我的Block原型是typedef void (^ABlock)(NSString *s, int i);

    其中v是void,20是总共需要内存大小,@?是Block的encode,0是第一个默认参数block结构体从偏移量0开始,@"NSString"8,则指NSString从偏移量8开始,最后是i从偏移量16开始占4位。

    有了这么详细的签名,动态调用或者动态替换实现就方便了,也许用它还能搞一波事情。

为什么要花这么多时间去分析汇编,了解的那么详细。其实一般的情况下是用不上的,但是如果遇到了线上crash,棘手的内存问题,要debug,这时候这些知识就会很有用了,你了解的越多,你解决问题的方式就越多,就更容易解决问题。当然也不是说什么都需要去仔细了解,也不是了解的越多越好,这个就需要根据自己的兴趣和需求去决定了。但是对于基础知识,确实可以多时间和精力去完善之,有了这些才能高屋建瓴得心应手。

感谢阅读。

你可能感兴趣的:(Block详细解读)