一种查看Block中引用的所有外部对象的实现方法

5,007 阅读14分钟

在我的前一篇文章:iOS调试Block引用对象无法被释放的一个小技巧 中有介绍一种显示某个block对象的实现函数的方法,以及从Debug Memory Graph中查看某个对象被哪个block所引用的方法,其实有更加简单的两个方法来查看持有某个对象的block的信息:

方法1:

在项目工程中打开Edit Scheme... 在出现的如下界面:

中勾选Malloc Stack。 这样在Debug Memory Graph中就可以看到对象的内存分配调用栈信息,以及某个block的实现函数代码了。

方法2:

在lldb控制台中使用 po [xxx debugDescription] 这里面的xxx就是某个block对象或者block在内存中的地址。


既然从Debug Memory Graph中可以查看某个对象是被哪个具体的block所持有,那么反过来说是否有查看某个block中持有了哪些对象呢?很明显在Debug Memory Graph中是无能为力了。

block内存布局简介

要想实现这个能力,就需要从block对象的内存布局说起,如果你查看开源库 opensource.apple.com/source/libc… 中关于block内部实现的定义就可以看出,在其中的Block_private.h文件中有关于block对象内部布局的定义,每个block其实是一个如下形式的结构体:

//block的描述信息
struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
    //可选的Block_descriptor_2或者Block_descriptor_3
};

//block的描述信息
struct Block_descriptor_2 {
    // requires BLOCK_HAS_COPY_DISPOSE
    BlockCopyFunction copy;
    BlockDisposeFunction dispose;
};

//block的描述信息
struct Block_descriptor_3 {
    // requires BLOCK_HAS_SIGNATURE
    const char *signature;
    const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

//block的内存布局结构。
struct Block_layout {
    void *isa;         //block对象的类型
    volatile int32_t flags; // block对象的一些特性和标志
    int32_t reserved;   //保留未用
    void *invoke;      //block的实现函数地址
    struct Block_descriptor_1 *descriptor;   //block的描述信息
    // imported variables   所引用的外部对象或者变量。
};

之所以一个block的闭包函数能够引用外部的一些对象或者变量,其根本的原因是每一个引用的外部对象或者变量都会在编译运行时添加到上面的imported variables部分作为block布局的扩展成员数据。就比如下面的一个block实例代码:

//假设是在TestViewController这个类的viewDidLoad中使用block对象。
   -(void)viewDidLoad{
     [super viewDidLoad];
     
    id obj = [NSObject new];
    int a = 0;
    void (^blk)() = ^(){
        NSLog("obj = %@ a=%d self = %@",obj, a, self);
        };
    }

当上述的代码被编译运行时,blk对象的内存布局除了基本的Block_layout外还有一些扩展的数据成员其真实的结构如下:

//blk对象的真实内部布局结构。
 struct Block_layout_for_blk
 {
   void *isa;         //block对象的类型
   volatile int32_t flags; // block对象的一些特性和标志
   int32_t reserved;   //保留未用
   void *invoke;      //block的实现函数地址
   struct Block_descriptor_1 *descriptor;   //block的描述信息
   //下面部分就是使用的外部对象信息。扩展布局部分的内存信息。
   id obj;
   TestViewController *self;
   int a;
 }

从上面的结构中你应该已经了解到了一个block内之所有能够访问外部变量的原因了吧!其实没有什么秘密,就是系统在编译block时会把所有访问的外部变量都复制到block对象实例内部而已。

我们知道在普通OC类中有一个ivar_layout数据成员来描述OC对象数据成员的布局信息。对于block而言要想获取到对象的所有扩展的成员数据则需要借助上述的flags数据成员以及descriptor中的信息来获取。针对一个block中的flags可设置值可以是下面值的组合:

// Values for Block_layout->flags to describe block objects
enum {
   BLOCK_DEALLOCATING =      (0x0001),  //runtime  标志当前block是否正在销毁中。这个值会在运行时被修改
   BLOCK_REFCOUNT_MASK =     (0xfffe),  //runtime block引用计数的掩码,flags中可以用来保存block的引用计数值。
   BLOCK_NEEDS_FREE =        (1 << 24), // runtime block需要被销毁
   BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler block有XXX
   BLOCK_HAS_CTOR =          (1 << 26), // compiler block中有C++的代码
   BLOCK_IS_GC =             (1 << 27), // runtime, 新版本中未用。
   BLOCK_IS_GLOBAL =         (1 << 28), // compiler  block是一个GlobalBlock
   BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
   BLOCK_HAS_SIGNATURE  =    (1 << 30), // block的函数有签名信息
   BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // block中有访问外部变量和对象
};

可以看出当一个block中有引用外部对象或变量时,其flags值中就会有BLOCK_HAS_EXTENDED_LAYOUT标志。而当有BLOCK_HAS_EXTENDED_LAYOUT标志时就会在block的Block_layout结构体中的descriptor中会有数据成员来描述所有引用的外部数据成员的扩展描述信息。这个描述结构体就是上面提到的:

struct Block_descriptor_3 {
   // requires BLOCK_HAS_SIGNATURE
   const char *signature;
   const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};

针对layout部分的定义在Block_private.h文件头中有明确描述:

// 扩展布局信息编码
// Extended layout encoding.

// Values for Block_descriptor_3->layout with BLOCK_HAS_EXTENDED_LAYOUT
// and for Block_byref_3->layout with BLOCK_BYREF_LAYOUT_EXTENDED

// If the layout field is less than 0x1000, then it is a compact encoding 
// of the form 0xXYZ: X strong pointers, then Y byref pointers, 
// then Z weak pointers.

// If the layout field is 0x1000 or greater, it points to a 
// string of layout bytes. Each byte is of the form 0xPN.
// Operator P is from the list below. Value N is a parameter for the operator.
// Byte 0x00 terminates the layout; remaining block data is non-pointer bytes.

enum {
    BLOCK_LAYOUT_ESCAPE = 0, // N=0 halt, rest is non-pointer. N!=0 reserved.
    BLOCK_LAYOUT_NON_OBJECT_BYTES = 1,    // N bytes non-objects
    BLOCK_LAYOUT_NON_OBJECT_WORDS = 2,    // N words non-objects
    BLOCK_LAYOUT_STRONG           = 3,    // N words strong pointers
    BLOCK_LAYOUT_BYREF            = 4,    // N words byref pointers
    BLOCK_LAYOUT_WEAK             = 5,    // N words weak pointers
    BLOCK_LAYOUT_UNRETAINED       = 6,    // N words unretained pointers
    BLOCK_LAYOUT_UNKNOWN_WORDS_7  = 7,    // N words, reserved
    BLOCK_LAYOUT_UNKNOWN_WORDS_8  = 8,    // N words, reserved
    BLOCK_LAYOUT_UNKNOWN_WORDS_9  = 9,    // N words, reserved
    BLOCK_LAYOUT_UNKNOWN_WORDS_A  = 0xA,  // N words, reserved
    BLOCK_LAYOUT_UNUSED_B         = 0xB,  // unspecified, reserved
    BLOCK_LAYOUT_UNUSED_C         = 0xC,  // unspecified, reserved
    BLOCK_LAYOUT_UNUSED_D         = 0xD,  // unspecified, reserved
    BLOCK_LAYOUT_UNUSED_E         = 0xE,  // unspecified, reserved
    BLOCK_LAYOUT_UNUSED_F         = 0xF,  // unspecified, reserved
};

上面文档的解释就是当layout的值小于0x1000时,则是一个压缩的扩展布局描述,其格式是0xXYZ, 其中的X的值表示的是block中引用的外部被声明为strong类型的对象数量,Y值则是block中引用的外部被声明为__block 类型的变量数量,而Z值则是block中引用的外部被声明为__weak类型的对象数量。

如果当layout的值大于等于0x1000时则是一个以0结束的字节串指针,字节串的每个字节的格式是0xPN,也就是每个字节中的高4位bit表示的是引用外部对象的类型,而低4位bit则是这种类型的数量。

上面的信息只是记录了一个block对象引用了外部对象的布局信息描述,对于普通的数据类型则不会记录。并且系统总是会把引用的对象排列在前面,而引用的普通数据类型则排列在后面。

打印一个block中引用的所有外部对象

通过对上述的介绍后,你是否了解到了一个block是如何持有和描述引用的外部对象的,那么回到本文主题,我们又如何去访问或者查看这些引用的外部对象呢?我们可以根据上面对block对象的内存布局描述来并下面的代码来实现打印出一个block对象所引用的所有外部对象:

/*
 * Copyright (c) 欧阳大哥2013. All rights reserved.
 * github地址:https://github.com/youngsoft
 */

void showBlockExtendedLayout(id block)
{
    static int32_t BLOCK_HAS_COPY_DISPOSE =  (1 << 25); // compiler
    static int32_t BLOCK_HAS_EXTENDED_LAYOUT  =  (1 << 31); // compiler
    
    struct Block_descriptor_1 {
        uintptr_t reserved;
        uintptr_t size;
    };

    struct Block_descriptor_2 {
        // requires BLOCK_HAS_COPY_DISPOSE
        void *copy;
        void *dispose;
    };

    struct Block_descriptor_3 {
        // requires BLOCK_HAS_SIGNATURE
        const char *signature;
        const char *layout;     // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
    };
    
    struct Block_layout {
        void *isa;
        volatile int32_t flags; // contains ref count
        int32_t reserved;
        void *invoke;
        struct Block_descriptor_1 *descriptor;
        // imported variables
    };

    //将一个block对象转化为blockLayout结构体指针
    struct Block_layout *blockLayout = (__bridge struct Block_layout*)(block);
    //如果没有引用外部对象也就是没有扩展布局标志的话则直接返回。
    if (! (blockLayout->flags & BLOCK_HAS_EXTENDED_LAYOUT)) return;
    
    //得到描述信息,如果有BLOCK_HAS_COPY_DISPOSE则表示描述信息中有Block_descriptor_2中的内容,因此需要加上这部分信息的偏移。这里有BLOCK_HAS_COPY_DISPOSE的原因是因为当block持有了外部对象时,需要负责对外部对象的声明周期的管理,也就是当对block进行赋值拷贝以及销毁时都需要将引用的外部对象的引用计数进行添加或者减少处理。
    uint8_t *desc = (uint8_t *)blockLayout->descriptor;
    desc += sizeof(struct Block_descriptor_1);
    if (blockLayout->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }
    
    //最终转化为Block_descriptor_3中的结构指针。并且当布局值为0时表明没有引用外部对象。
    struct Block_descriptor_3 *desc3 = (struct Block_descriptor_3 *)desc;
    if (desc3->layout == 0)
        return;
    
    
    //所支持的外部对象的类型。
    static unsigned char BLOCK_LAYOUT_STRONG           = 3;    // N words strong pointers
    static unsigned char BLOCK_LAYOUT_BYREF            = 4;    // N words byref pointers
    static unsigned char BLOCK_LAYOUT_WEAK             = 5;    // N words weak pointers
    static unsigned char BLOCK_LAYOUT_UNRETAINED       = 6;    // N words unretained pointers
    
    const char *extlayoutstr = desc3->layout;
    //处理压缩布局描述的情况。
    if (extlayoutstr < (const char*)0x1000)
    {
        //当扩展布局的值小于0x1000时则是压缩的布局描述,这里分别取出xyz部分的内容进行重新编码。
        char compactEncoding[4] = {0};
        unsigned short xyz = (unsigned short)(extlayoutstr);
        unsigned char x = (xyz >> 8) & 0xF;
        unsigned char y = (xyz >> 4) & 0xF;
        unsigned char z = (xyz >> 0) & 0xF;
        
        int idx = 0;
        if (x != 0)
        {
            x--;
            compactEncoding[idx++] = (BLOCK_LAYOUT_STRONG<<4) | x;
        }
        if (y != 0)
        {
            y--;
            compactEncoding[idx++] = (BLOCK_LAYOUT_BYREF<<4) | y;
        }
        if (z != 0)
        {
            z--;
            compactEncoding[idx++] = (BLOCK_LAYOUT_WEAK<<4) | z;
        }
        compactEncoding[idx++] = 0;
        extlayoutstr = compactEncoding;
    }
    
    unsigned char *blockmemoryAddr = (__bridge void*)block;
    int refObjOffset = sizeof(struct Block_layout);  //得到外部引用对象的开始偏移位置。
    for (int i = 0; i < strlen(extlayoutstr); i++)
    {
        //取出字节中所表示的类型和数量。
        unsigned char PN = extlayoutstr[i];
        int P = (PN >> 4) & 0xF;   //P是高4位描述引用的类型。
        int N = (PN & 0xF) + 1;    //N是低4位描述对应类型的数量,这里要加1是因为格式的数量是从0个开始计算,也就是当N为0时其实是代表有1个此类型的数量。
        
       
        //这里只对类型为3,4,5,6四种类型进行处理。
        if (P >= BLOCK_LAYOUT_STRONG && P <= BLOCK_LAYOUT_UNRETAINED)
        {
            for (int j = 0; j < N; j++)
            {
                //因为引用外部的__block类型不是一个OC对象,因此这里跳过BLOCK_LAYOUT_BYREF,
                //当然如果你只想打印引用外部的BLOCK_LAYOUT_STRONG则可以修改具体的条件。
                if (P != BLOCK_LAYOUT_BYREF)
                {
                    //根据偏移得到引用外部对象的地址。并转化为OC对象。
                    void *refObjAddr = *(void**)(blockmemoryAddr + refObjOffset);
                    id refObj =  (__bridge id) refObjAddr;
                    //打印对象
                    NSLog(@"the refObj is:%@  type is:%d",refObj, P);
                }
                //因为布局中保存的是对象的指针,所以偏移要加上一个指针的大小继续获取下一个偏移。
                refObjOffset += sizeof(void*);
            }
        }
    }
}

通过上述的代码我们就可以将一个block中所持有的所有外部OC对象都打印出来了。在实践中我们可以将这部分代码通过方法交换的形式来作为block对象的日志输出,比如:

//description方法的实现
NSString *block_description(id obj, SEL _cmd)
{
    showBlockExtendedLayout(obj);
    return @"";
}

////////////////////
//针对NSBlock类型添加一个自定义的描述信息输出函数。
 Class blkcls = NSClassFromString(@"NSBlock");
 BOOL bok = class_addMethod(blkcls, @selector(description), block_description, "@@:");
  

这样我们就可以在控制台 通过 po [xxx description] 的形式来展示一个block所持有的对象信息了。

结尾

既然我们可以通过Xcode 的Debug Memory Graph来查看某个对象被哪个block所引用,而又可以通过文本介绍的方法来查看某个block对象引用了哪些对象。两个方法双管齐下,就可以更加愉快的调试block和内存泄漏以及内存引用的相关问题了。

两个有趣的点

  1. 在笔者完成这篇文章时,特意在网络上搜索了一下是否有同类型或者已经实现了的方法,果然有几篇介绍block持有对象的文章,内心一阵慌乱。点进去看后其实都是在介绍Facebook的FBRetainCycleDetector 是如何实现block强持有对象检测的。看了看源代码,发现实现的思路和本文完全不同,这才放下心来。 总的来Facebook那套是用了一些巧劲来实现检测的,而本文则算是比较官方的实现,而且可检测的持有对象类型更加宽泛和通用。

  2. 在知道block有BLOCK_BYREF_LAYOUT_EXTENDED这么一个标志前,我的一个老的实现方法是通过分析block描述中的copy函数的指令来判断和获取扩展对象的偏移量的。因为如果某个block持有了外部对象时就必然会实现一个copy函数来对所有外部对象进行引用计数管理。我当时的方法就是通过分析copy函数的机器指令特征,然后通过解析特征指令中的常数部分来获取对象的偏移量的。下面就是实现的代码, 有兴趣的读者可以阅读一下(需要注意的是下面的代码只能在真机上运行通过):

/*
 * Copyright (c) 欧阳大哥2013. All rights reserved.
 * github地址:https://github.com/youngsoft
 */

struct Block_descriptor_1 {
    uintptr_t reserved;
    uintptr_t size;
    // requires BLOCK_HAS_COPY_DISPOSE
    void* copy;
    void* dispose;
};

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    void* invoke;
    struct Block_descriptor_1 *descriptor;
    // imported variables
};

//定义ldr指令结构
struct arm64_ldr_immediate_unsignedoffset
{
    uint32_t Rt:5;      //目标寄存器
    uint32_t Rn:5;      //源寄存编号
    uint32_t imm12:12;  //偏移 = imm12 << size;
    uint32_t opc:8;   //11100101
    uint32_t size:2;  //11
};

boolean_t is_arm64_ldr_immediate_unsignedoffset(uint32_t *ins)
{
    struct arm64_ldr_immediate_unsignedoffset *vins = (struct arm64_ldr_immediate_unsignedoffset*)ins;
    return  vins->size == 0b11 && vins->opc == 0b11100101;
}

//定义add立即数指令结构
struct arm64_add_immediate
{
    uint32_t Rd:5;  //目标
    uint32_t Rn:5;
    uint32_t imm12:12;
    uint32_t shift:2;  //00
    uint32_t opS:7; //0010001
    uint32_t sf:1;  //1
};

boolean_t is_arm64_add_immediate(uint32_t *ins)
{
    struct arm64_add_immediate *vins = (struct arm64_add_immediate*)ins;
    return vins->sf == 0b1 && vins->opS == 0b0010001 && vins->shift == 0b00;
}

//定义mov寄存器指令结构
struct arm64_mov_register
{
    uint32_t Rd:5;    //目标
    uint32_t Rn:5;    //11111
    uint32_t imm6:6;  //000000
    uint32_t Rm:5;    //源
    uint32_t opc:10; //0101010000
    uint32_t sf:1;  //1
};

boolean_t is_arm64_mov_register(uint32_t *ins)
{
    struct arm64_mov_register *vins = (struct arm64_mov_register*)ins;
    return vins->sf == 0b1 && vins->opc == 0b0101010000 && vins->imm6 == 0b000000 && vins->Rn == 0b11111;
}

//定义函数调用指令
struct arm64_bl
{
    uint32_t imm26:26;
    uint32_t op:6; //100101
};

boolean_t is_arm64_bl(uint32_t *ins)
{
    struct arm64_bl *vins = (struct arm64_bl*)ins;
    return vins->op == 0b100101;
}

//定义跳转指令
struct arm64_b
{
    uint32_t imm26:26;
    uint32_t op:6; //000101
};

boolean_t is_arm64_b(uint32_t *ins)
{
    struct arm64_b *vins = (struct arm64_b*)ins;
    return vins->op == 0b000101;
}

//定义函数返回指令。
struct arm64_ret
{
    uint32_t op:32; //0xd65f03c0
};

boolean_t is_arm64_ret(uint32_t *ins)
{
    struct arm64_ret *vins = (struct arm64_ret*)ins;
    return vins->op == 0xd65f03c0;
}


//寄存器编号信息
typedef enum : unsigned char {
    REG_X0,
    REG_X1,
    REG_X2,
    REG_X3,
    REG_X4,
    REG_X5,
    REG_X6,
    REG_X7,
    REG_X8,
    REG_X9,
    REG_X10,
    REG_X11,
    REG_X12,
    REG_X13,
    REG_X14,
    REG_X15,
    REG_X16,
    REG_X17,
    REG_X18,
    REG_X19,
    REG_X20,
    REG_X21,
    REG_X22,
    REG_X23,
    REG_X24,
    REG_X25,
    REG_X26,
    REG_X27,
    REG_X28,
    REG_X29,
    REG_X30,
    REG_SP
} ARM64_REG;

void showBlockExtendedLayout(id block)
{
    static int32_t BLOCK_HAS_COPY_DISPOSE =  (1 << 25); // compiler
    
    struct Block_layout *blockLayout = (__bridge struct Block_layout*)(block);
    
    //如果没有持有附加的对象则没有BLOCK_HAS_COPY_DISPOSE这个特性
    if ((blockLayout->flags & BLOCK_HAS_COPY_DISPOSE) != BLOCK_HAS_COPY_DISPOSE)
        return;
    
    //定义引用的外部对象的偏移位置和block的尺寸
    //所有外部引用对象的偏移位置必须>=firstRefObjOffset 并且 < blockSize
    int firstRefObjOffset = sizeof(struct Block_layout);
    int blockSize = (int)blockLayout->descriptor->size;
    
    
    
    //得到block的copy函数的地址,并读取函数指令内容。
    uint32_t *copyfuncAddr = blockLayout->descriptor->copy;
    if (copyfuncAddr == NULL)
        return;
    
    //读取地址的内容。
    int validateRefObjOffsets[40];
    int validateRefObjCount = 0;
    int validateRefObjOffset = 0;
    //定义一个映射表。。。key是寄存器,value是偏移。记录可能候选的偏移量
    NSMutableDictionary *regoffsetMap = [NSMutableDictionary new];
    
    unsigned char *pcopyfuncAddr = copyfuncAddr;
    while (true)
    {
        //这里读取数据。然后解析。
        if (is_arm64_ldr_immediate_unsignedoffset(pcopyfuncAddr))
        {
            //目标可以不是x0,这个要和mov指令结合。
            struct arm64_ldr_immediate_unsignedoffset *vins = (struct arm64_ldr_immediate_unsignedoffset*)pcopyfuncAddr;
            
            int immediate = vins->imm12 << vins->size;
            //必须是范围内,并且源不是sp寄存器。
            if (immediate >= firstRefObjOffset && immediate < blockSize && vins->Rn != REG_SP)
            {
                if (vins->Rt == REG_X0)
                {
                    validateRefObjOffset = immediate;
                }
                else
                {
                     regoffsetMap[@(vins->Rt)] = @(immediate);
                }
            }
        }
        else if (is_arm64_add_immediate(pcopyfuncAddr))
        {
            //确保目标寄存器是x0
            struct arm64_add_immediate *vins = (struct arm64_add_immediate*)pcopyfuncAddr;
            int immediate = vins->imm12;
            if (immediate >= firstRefObjOffset && immediate < blockSize && vins->Rn != REG_SP)
            {
                if (vins->Rd == REG_X0)
                {
                    validateRefObjOffset = immediate;
                }
                else
                {
                    regoffsetMap[@(vins->Rd)] = @(immediate);
                }
            }
        }
        else if (is_arm64_mov_register(pcopyfuncAddr))
        {
            //确保目标寄存器是x0
            struct arm64_mov_register *vins = (struct arm64_mov_register*)pcopyfuncAddr;
            if (vins->Rd == REG_X0)
            {
                //确保源寄存器必须是上面ldr的目标寄存器。
                NSNumber *num = regoffsetMap[@(vins->Rm)];
                if (num != nil)
                {
                    validateRefObjOffset = num.intValue;
                }
            }
        }
        else if (is_arm64_bl(pcopyfuncAddr))
        {
            if (validateRefObjOffset != 0)
            {
                validateRefObjOffsets[validateRefObjCount++] = validateRefObjOffset;
            }
            
            validateRefObjOffset  = 0;
            [regoffsetMap removeAllObjects];
        }
        else if (is_arm64_b(pcopyfuncAddr))
        {
            if (validateRefObjOffset != 0)
            {
                validateRefObjOffsets[validateRefObjCount++] = validateRefObjOffset;
            }
            
            validateRefObjOffset = 0;
            [regoffsetMap removeAllObjects];
            
            //当末尾是b指令时也认为是函数结束
            break;
        }
        else if (is_arm64_ret(pcopyfuncAddr))
        {
            //函数结束,停止遍历。
            break;
        }
        
        pcopyfuncAddr += 4;
    }
    
    if (validateRefObjCount > 0)
    {
        //分别打印每个对象。
        for (int i = 0; i < validateRefObjCount; i++)
        {
            unsigned char *blockmemoryAddr = (__bridge void*)block;
            void *refObjAddr = *(void**)(blockmemAddr + validateRefObjOffsets[i]);
            id refObj =  (__bridge id) refObjAddr;
            NSLog(@"refObj is:%@ offset:%d",refObj, validateRefObjOffsets[i]);
        }
    }
}