Block Internal

Đã bao giờ các bạn thắc mắc, block được implement như thế nào, nó hoạt động ra sao, tại sao lại có thể bị memleak khi dùng block trong Objc chưa. Mình có một sự tò mò suốt 1 thời gian dài, đó chính là block đã retain các biến của mình khi nào, và khi nào thì release chúng. Bài viết hôm nay sẽ giải đáp một phần thắc mắc đó, chúng ta sẽ đi tìm hiểu xem block có cấu trúc như thế nào, nó capture và retain các object của chúng ta ra sao.

Cấu trúc của block

Chúng ta xét chương trình sau:

@interface Demo : NSObject

@end

@implementation Demo

- (void)start {
    __block NSInteger count = 0;
    void (^helloBlock)(void) = ^{
        count++;
        [self hello];
    };
    
    helloBlock();
}

- (void)hello {
    
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Demo *demo = [[Demo alloc] init];
        [demo start];
    }
    return 0;
}

Hãy đặt breakpoint tại dòng 10 của chương trình và chạy chương trình.

Tại cửa sổ debug lldb, hãy nhập lệnh:

frame variable

Lệnh này sẽ hiển thị cho ta danh sách các biến trong frame này. Kết quả ta có

(lldb) frame variable
 (__block_literal_1 *) .block_descriptor = 0x0000000100603910
 (Demo *) self = 0x0000000100603640

Aha, chúng ta để ý thấy có một biến có kiểu là __block_literal_1. Đây chính là struct quy định cấu trúc của block helloBlock của chúng ta trong chương trình. Tiếp theo chúng ta dùng lệnh sau để xem cấu trúc của nó nhé:

image lookup -t __block_literal_1

Ta thu được kết quả:

(lldb) image lookup -t __block_literal_1
 Best match found in /Users/thanhvu/Library/Developer/Xcode/DerivedData/BlockCMD-cepsftgdsnrskhfiqassdssqvixu/Build/Products/Debug/BlockCMD:
 id = {0x100000299}, name = "__block_literal_1", byte-size = 48, decl = main.m:19, compiler_type = "struct __block_literal_1 {
     void *__isa;
     int __flags;
     int __reserved;
     void (*__FuncPtr)();
     __block_descriptor_withcopydispose *__descriptor;
     Demo *const self;
     (anonymous struct) *count;
 }"

Từ kết quả được in ra chắc cũng dễ hiểu rằng block trong objective-c được định nghĩa bằng một struct, bên trong nó có chứa những thông tin như isa, flag, function pointer, descriptor, và các biến được capture (ở đây là biến self và count).

Khi block được tạo ra, đoạn code bên trong block sẽ được tạo thành một hàm C tới tên có phần subfix là _block_invoke (khi debug trong block các bạn sẽ thấy tên này). Và field __FuncPtr là một function pointer, sẽ chứa địa chỉ của hàm đó.

Khi chúng ta gọi helloBlock(). Chương trình sẽ tìm tới field __FuncPtr và thực hiện gọi nó với tham số là địa chỉ của biến helloBlock.

Block capture biến để làm gì

Block định nghĩa một đoạn code được sử dụng gọi về sau. Khi bạn gọi một biến được định nghĩa bên ngoài block, thì block cần lưu lại biến đó để sau này khi được gọi thì sẽ lấy ra để sử dụng.

Vậy nên trong trường hợp trên, nhìn vào code thì chúng ta thấy block này được khai báo mà không cần đưa ra thông tin gì liên quan đến biến self. Tuy nhiên trình biên dịch sẽ phân tích và thấy biến self được gọi trong block thì nó sẽ capture (lưu lại) để sử dụng cho sau này.

Block sẽ thực hiện capture lại các biến cần thiết khi nó được tạo ra, và sẽ release các biến đó khi nó huỷ. Theo mình tìm hiểu, thì nó sẽ tạo ra block trên stack, sau đó mới copy vào vùng heap và retain các biến đã capture bằng hàm objc_retainBlock.

Lưu ý khi dùng block

Do block capture và retain lại các biến gọi trong nó, nên có thể gây ra hiện strong reference cycle dẫn tới memory leak. Do đó trong trường hợp có thể tạo thành strong reference cycle thì ta nên dùng pointer weak của self để tránh bị mem leak. Khi ấy chương trình sẽ như sau.

- (void)start {
    __weak Demo *weakSelf = self;
    __block NSInteger count = 0;
    void (^helloBlock)(void) = ^{
        count++;
        [weakSelf hello];
    };
    
    helloBlock();
}

Block vs con trỏ hàm

Nếu các bạn để ý kỹ thì block và pointer khi khai báo sẽ chỉ khác nhau về dấu ^ và *. ?

Tuy nhiên block được xem là nâng cấp của con trỏ hàm. Nó cho phép chúng ta capture lại biến để gọi về sau, thay vì phải trực tiếp truyền tham số cho hàm.

Bật mí thêm là bên C++ có tính năng tên là lambda cũng có chức năng tương tự như block ?. (Không giống hệt nha ?)

Tham khảo

https://clang.llvm.org/docs/BlockLanguageSpec.html

Leave a Reply