16、iOS底層探索-Block

語言: CN / TW / HK

highlight: hybrid

1、Block型別

  • 全域性block
    • 如果沒有使用外部變數,或者只使用全域性變數或靜態變數,則是全域性block
  • 棧block
    • 如果使用了外部變數,賦值弱引用,則是棧block
  • 堆block
    • 如果使用了外部變數,賦值強引用,則是堆block

2、底層探索

小測驗

```swift - (void)viewDidLoad {     [super viewDidLoad];

NSObject *obj = [NSObject new];     NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));

void (^block1)(void) = ^{         NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));     };     block1();

void (^__weak block2)(void) = ^{         NSLog(@"---%ld",CFGetRetainCount((__bridge CFTypeRef)(obj)));     };     block2(); }

// 列印結果 134 ``` - 在第一次列印時obj引用計數為1

  • block1是一個堆block捕獲obj時會先將obj捕獲到棧上,再copy到堆上,因此引用計數會被兩次+1,所以block1列印為3

  • 弱引用的 block2是一個棧block,所以obj被捕獲到棧上,不再向堆上copy,引用計數+1,列印為4

2.1、cpp檔案分析block

image.png 轉譯成.cpp檔案 image.png - 可以看到先建立為 棧block,並捕獲了外部obj變數 - .cpp中,block會被轉換成_block_impl的結構體 swift struct __block_impl { void *isa; // 指標 int Flags; int Reserved; void *FuncPtr; // 儲存程式碼塊 }; - block本質是結構體

2.2、彙編探索

  1. 我們在block1處打上斷點執行,並進入彙編除錯 image.png
  2. 在彙編中找callq看到,block1中呼叫了objc_retainBlock符號(用__weak修飾後不會呼叫該符號),並且 此時是一個_NSConcreteStackBlock,也就是 棧block,那麼我們可以嘗試對 objc_retainBlock 下一個符號斷點: image.png
  3. 可以看到其中使用了一個_Block_copy image.png
  4. 我們繼續對 _Block_copy 下符號斷點 image.png
  5. _Block_copy 中,看callq指令我們知道了,系統呼叫了malloc方法,而最終生成了__NSMallocBlock__也就是 堆block

2.3、原始碼探索

借用上邊彙編探索的部分內容,我們知道了block是在 _Block_copy由棧block變為了堆block,那麼我們在Block原始碼中搜索 _Block_copy: ```swift // Copy, or bump refcount, of a block.  If really copying, call the copy helper if present. // 拷貝 block, // 如果原來就在堆上,就將引用計數加 1; // 如果原來在棧上,會拷貝到堆上,引用計數初始化為 1,並且會呼叫 copy helper 方法(如果存在的話); // 如果 block 在全域性區,不用加引用計數,也不用拷貝,直接返回 block 本身 // 引數 arg 就是 Block_layout 物件, // 返回值是拷貝後的 block 的地址 // 執行?stack -> malloc void _Block_copy(const void arg) {     struct Block_layout *aBlock;

// 如果 arg 為 NULL,直接返回 NULL

if (!arg) return NULL;

// The following would be better done as a switch statement     // 強轉為 Block_layout 型別     aBlock = (struct Block_layout )arg;     const char signature = _Block_descriptor_3(aBlock)->signature;

// 如果現在已經在堆上     if (aBlock->flags & BLOCK_NEEDS_FREE) {         // latches on high         // 就只將引用計數加 1         latching_incr_int(&aBlock->flags);         return aBlock;     }     // 如果 block 在全域性區,不用加引用計數,也不用拷貝,直接返回 block 本身     else if (aBlock->flags & BLOCK_IS_GLOBAL) {         return aBlock;     }     else {         // Its a stack block.  Make a copy.         // block 現在在棧上,現在需要將其拷貝到堆上         // 在堆上重新開闢一塊和 aBlock 相同大小的記憶體         struct Block_layout result =             (struct Block_layout )malloc(aBlock->descriptor->size);         // 開闢失敗,返回 NULL         if (!result) return NULL;         // 將 aBlock 記憶體上的資料全部複製新開闢的 result 上         memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

if __has_feature(ptrauth_calls)

// Resign the invoke pointer as it uses address authentication.         result->invoke = aBlock->invoke;

endif

// reset refcount         // 將 flags 中的 BLOCK_REFCOUNT_MASK 和 BLOCK_DEALLOCATING 部分的位全部清為 0         result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed         // 將 result 標記位在堆上,需要手動釋放;並且引用計數初始化為 1         result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1         // copy 方法中會呼叫做拷貝成員變數的工作         _Block_call_copy_helper(result, aBlock);         // Set isa last so memory analysis tools see a fully-initialized object.         // isa 指向 _NSConcreteMallocBlock         result->isa = _NSConcreteMallocBlock;         return result;     } } - **block在全域性**:直接返回block本身 - **block在堆上**:引用計數+1 - **block在棧上**:將棧上的所有資料(**包括block捕獲後成為其成員變數的外部變數**)全部copy到新開闢的`Block_layout`型別的 result 上swift struct Block_layout { void isa; volatile int32_t flags; // contains ref count int32_t reserved; // libffi -> BlockInvokeFunction invoke; struct Block_descriptor_1 descriptor; // imported variables }; ```

3、迴圈引用

準備一份迴圈引用程式碼 ```swift

import "LZViewController.h"

typedef void(^LZBlock)(void);

@interface LZViewController ()

@property (nonatomic, copy) LZBlock block; @property (nonatomic, copy) NSString *name;

@end

@implementation LZViewController

  • (void)viewDidLoad {     [super viewDidLoad];     self.view.backgroundColor = [UIColor greenColor];

self.name = @"lz";     self.block = ^{         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{             NSLog(@"%@",self.name);         });     };     self.block(); }

  • (void)touchesBegan:(NSSet )touches withEvent:(UIEvent )event {     [self dismissViewControllerAnimated:YES completion:nil]; }

  • (void)dealloc {     NSLog(@"%s",func); } ```

3.1、weak打破迴圈

```swift - (void)viewDidLoad {     [super viewDidLoad];     self.view.backgroundColor = [UIColor greenColor];

self.name = @"lz";

// weak打破迴圈

__weak typeof(self)weakSelf = self;     self.block = ^{ // strong保證block中程式碼執行後才會被釋放         __strong typeof(weakSelf)strongSelf = weakSelf;         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{             NSLog(@"%@",strongSelf.name);         });     };     self.block(); } ```

3.2、手動釋放

```swift - (void)viewDidLoad {     [super viewDidLoad];     self.view.backgroundColor = [UIColor greenColor];

self.name = @"lz"; // 宣告變數vc指向self __block LZViewController *vc = self;     self.block = ^{         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{             NSLog(@"%@",strongSelf.name); // 使用後手動釋放vc vc = nil;         });     };     self.block(); } ```

3.3、改為block引數

block不會捕獲自身的引數,引數是棧自動管理的 ```swift

import "LZViewController.h"

typedef void(^LZBlock)(void); // 在block中增加引數 typedef void(^LZBlock1)(LZViewController *);

@interface LZViewController ()

@property (nonatomic, copy) LZBlock block; @property (nonatomic, copy) LZBlock1 block1; @property (nonatomic, copy) NSString *name;

@end

@implementation LZViewController

  • (void)viewDidLoad {     [super viewDidLoad];     self.view.backgroundColor = [UIColor greenColor];

self.name = @"lz";

self.block1 = ^(LZViewController *vc) {         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{             NSLog(@"%@",vc.name);         });     };     self.block1(self); } ```

4、需要呼叫block()的原因

swift int main(int argc, const char * argv[]) {     @autoreleasepool {         // Setup code that might create autoreleased objects goes here.     }     int a = 10;     void (^block)(void) = ^{         NSLog(@"%d",a);     };     block();     return NSApplicationMain(argc, argv); }

轉譯.cpp檔案: swift struct __block_impl {   void *isa;   int Flags;   int Reserved;   void *FuncPtr; }; image.png - 在.cpp中,block是一個__block_impl型別的結構體 impl - block執行__main_block_impl_0函式進行構造初始化 - __main_block_impl_0 中第一個引數__main_block_func_0是程式碼塊內容執行構造方法時被儲存在 impl 的 FuncPtr 中 - 當呼叫block()時,系統會調取 impl 的 FuncPtr 執行其中的程式碼塊內容

5、__block

swift int main(int argc, const char * argv[]) {     @autoreleasepool {         // Setup code that might create autoreleased objects goes here.     } // 當想在block內部改變外部變數值時,需要新增__block修飾變數     __block int num = 10;     void (^block)(void) = ^{         NSLog(@"%d",--num);     };     // 呼叫block     block();     return NSApplicationMain(argc, argv); } 轉譯.cpp檔案: image.png - 對比於不加__block關鍵字,被捕獲的外部變數變成了__Block_byref_num_0結構體 swift struct __Block_byref_num_0 { void *__isa; // 預設指向結構體本身 __Block_byref_num_0 *__forwarding; int __flags; int __size; int num; }; - __main_block_func_0中程式碼塊,列印--num的地方變成了--(num->__forwarding->num),也就是說我們需要知道num結構體 中的__forwarding指向

  • __main_block_copy_0中的_Block_object_assign會改變__forwarding指向

_Block_object_assign

image.png - switch case 的值: image.png 看註釋可以知道 __block 修飾時會走 BLOCK_FIELD_IS_BYREF 分支

  • __Block_byref_copy image.png

小結

  • 當外部變數num使用__block修飾時,底層cpp中變數num會被變成 __Block_byref_num_0結構體num,其內部有一個__forwarding,預設先指向棧上的結構體num自身,程式碼塊中使用這個num的地方也會變成使用num->__forwarding

  • __main_block_copy_0 中的 _Block_object_assign 方法 在block由棧copy到堆上的過程中,也將指向結構體num自身的__forwarding改為指向了堆上新copy的內容

  • 因此如果修改已被copy到堆上的block外部變數的值,也會影響指向這個堆的原本棧上的值

6、block底層結構

本文 2.3 中我們檢視 _Block_copy 時我們看到: image.png

Block_layout

swift struct Block_layout { void *isa; // 指向堆、棧、全域性 volatile int32_t flags; // 附加資訊,類似指標的MASK掩碼,和不同的引數取 與操作能得到不同的值 int32_t reserved; // 保留變數 // libffi -> BlockInvokeFunction invoke; //實現的函式指標 struct Block_descriptor_1 *descriptor; // 存放copy、dispose、大小、簽名等資訊 }; - block的本質是 Block_layout - 對比.cpp檔案中__block_impl型別的結構體 impl,可以發現內容很相似 - flags:附加資訊,類似指標的MASK掩碼,和不同的引數取 與操作 能得到不同的內容(在Block_descriptor_1中就使用了flags獲取copy、dispose與簽名等內容swift // Values for Block_layout->flags to describe block objects enum { BLOCK_DEALLOCATING =      (0x0001),  // runtime BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime BLOCK_NEEDS_FREE =        (1 << 24), // runtime BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code BLOCK_IS_GC =             (1 << 27), // runtime BLOCK_IS_GLOBAL =         (1 << 28), // compiler BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler };

Block_descriptor_1

image.png 其中部分內容存放在 Block_descriptor_2Block_descriptor_3 中,它們的空間連續,使用時 通過記憶體平移調取

image.png

  • block的簽名是@?