一種查看Block中引用的所有外部對象的實現方法

  • 2019 年 10 月 5 日
  • 筆記

在我的前一篇文章: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對象的記憶體布局說起,如果你查看開源庫 https://opensource.apple.com/source/libclosure/libclosure-73/ 中關於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]);          }      }  }