在個人前一篇文章:iOS調試Block引用對象沒法被釋放的一個小技巧 中有介紹一種顯示某個block對象的實現函數的方法,以及從Debug Memory Graph中查看某個對象被哪一個block所引用的方法,其實有更加簡單的兩個方法來查看持有某個對象的block的信息:git
在項目工程中打開Edit Scheme... 在出現的以下界面:github
中勾選Malloc Stack。 這樣在Debug Memory Graph中就能夠看到對象的內存分配調用棧信息,以及某個block的實現函數代碼了。bash
在lldb控制檯中使用 po [xxx debugDescription] 這裏面的xxx就是某個block對象或者block在內存中的地址。網絡
既然從Debug Memory Graph中能夠查看某個對象是被哪一個具體的block所持有,那麼反過來講是否有查看某個block中持有了哪些對象呢?很明顯在Debug Memory Graph中是無能爲力了。閉包
要想實現這個能力,就須要從block對象的內存佈局提及,若是你查看開源庫 opensource.apple.com/source/libc… 中關於block內部實現的定義就能夠看出,在其中的Block_private.h文件中有關於block對象內部佈局的定義,每一個block實際上是一個以下形式的結構體:app
//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實例代碼:less
//假設是在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可設置值能夠是下面值的組合:post
// 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對象所引用的全部外部對象:
/*
* 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和內存泄漏以及內存引用的相關問題了。
在筆者完成這篇文章時,特地在網絡上搜索了一下是否有同類型或者已經實現了的方法,果真有幾篇介紹block持有對象的文章,心裏一陣慌亂。點進去看後其實都是在介紹Facebook的FBRetainCycleDetector 是如何實現block強持有對象檢測的。看了看源代碼,發現實現的思路和本文徹底不一樣,這才放下心來。 總的來Facebook那套是用了一些巧勁來實現檢測的,而本文則算是比較官方的實現,並且可檢測的持有對象類型更加寬泛和通用。
在知道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]);
}
}
}
複製代碼