Block是在iOS4引入的新特性,是一種特殊的數據類型,今天我們就從源碼層面探索一下Block具體是一種什麼類型,並探尋下Block的內存管理方式。
一、Block類型
對於Block是什麼類型,其實網上已經給出了答案,那就是Block實例也是一種對象。這個觀點是完全正確的,我們可以從以下兩個方面進行驗證:
1. 源碼
目前關於Block的源碼是公開的,具體下載位置爲地址。
對於Block,本質上是一個結構體,其內容如下:
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
#define BLOCK_DESCRIPTOR_3 1
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)(void *, ...);
struct Block_descriptor_1 *descriptor;
// imported variables
};
從Block_layout
的isa
指針可以得出以上結論。
2. clang轉換後的代碼
我們可以簡單寫一個Block:
void(^block1)(void) = ^(void) {
NSLog(@"block1");
};
通過clang -rewrite-objc main.m
得到轉換後的代碼,其中與該Block有關的內容如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_hb_tnc4751s73b8_zwpzmxttpvm0000gn_T_main_f377bb_mi_0);
}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
void(*block1)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
可以看到該Block本身就是一個__main_block_impl_0
的結構體,而該結構體中第一個成員便是struct __block_impl
:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
可以看到,雖然轉換出來的代碼最終是__block_impl
結構體,但他與源碼的結構體內容完全一致,且第一個成員爲isa指針,同樣可以證明Block變量就是一個對象。
二、Block引用計數
既然Block也是一種對象,那它是否也遵循對象的引用計數方式呢?
我們從runtime的SideTable源碼並沒有找到任何關於Block引用計數的相關代碼,但這並不表示Block和引用計數沒有關聯,Block與引用計數的關聯其實很簡單,就是保持在struct Block_layout
結構體本身中:
volatile int32_t flags; // contains ref count
flags
成員的註釋中表明瞭它保持了引用計數的內容,那麼他是如何保存的呢?
在struct Block_layout
聲明的代碼上部就有一些關於flags
標誌的定義:
// 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
};
結合這些標誌,我們可以得出flags
成員的含義:
flags二進制下標 | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | 23 ... 1 | 0 |
代表含義 | 是否有擴展布局 | 是否有簽名 | 返回值是否在棧上 | 是否是全局Block | 是否採用垃圾回收機制 | helper是否有C++代碼 | 是否有copy/dispose方法 | 是否需要釋放,即存在與堆上 | 引用計數 | 是否正在釋放 |
由以上源碼可知,Block的引用計數保存在32位flags
的第1-23位上,同時flags
也指定了Block是否需要釋放
等信息。
既然有是否需要釋放
這個信息,那麼肯定就存在Block
需要釋放與不需要釋放兩種情況,那麼這兩種情況是如何出現的,我們需要從Block的分類開始說起。
三、Block分類
Block既然有isa指針,那麼Block屬於哪種對象呢?
從源碼中我們可以得到以下分類:
void * _NSConcreteStackBlock[32] = { 0 };
void * _NSConcreteMallocBlock[32] = { 0 };
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 };
Block共有以上5中類型,其中可以分爲以下幾種情況:
_NSConcreteGlobalBlock & _NSConcreteStackBlock
在官方文檔中,有以下一段描述:
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
文檔表明Block在創建時,只能創建出_NSConcreteStackBlock與_NSConcreteGlobalBlock兩種類型的Block,那麼如何創建這兩種類型的Block,網上已經有不少文章指出了,關鍵點就在Block是否需要引用外部變量,若未引用,則爲_NSConcreteGlobalBlock,若引用,則爲_NSConcreteStackBlock。
_NSConcreteMallocBlock
在源碼中,搜索_NSConcreteMallocBlock,發現只有一處代碼有isa = _NSConcreteMallocBlock,且代碼位於_Block_copy中:
//libclosure-74
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
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
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
由源碼可知,當_NSConcreteStackBlock調用copy方法的時候,纔會變爲_NSConcreteMallocBlock。且這是創建_NSConcreteMallocBlock的唯一途徑。
同時可以看出,在創建_NSConcreteMallocBlock類型的Block時,是通過malloc方法申請的堆內存,說明該類型的Block位於堆上。
_NSConcreteAutoBlock & _NSConcreteFinalizingBlock
這兩種類型的Block創建方式在源碼中同樣只有一處,在_Block_copy_internal中:
//libclosure-63
static void *_Block_copy_internal(const void *arg, const bool wantsOne) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GC) {
// GC refcounting is expensive so do most refcounting here.
if (wantsOne && ((latching_incr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK) == 2)) {
// Tell collector to hang on this - it will bump the GC refcount version
_Block_setHasRefcount(aBlock, true);
}
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// Its a stack block. Make a copy.
if (!isGC) {
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
}
else {
// Under GC want allocation with refcount 1 so we ask for "true" if wantsOne
// This allows the copy helper routines to make non-refcounted block copies under GC
int32_t flags = aBlock->flags;
bool hasCTOR = (flags & BLOCK_HAS_CTOR) != 0;
struct Block_layout *result = _Block_allocator(aBlock->descriptor->size, wantsOne, hasCTOR || _Block_has_layout(aBlock));
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// reset refcount
// if we copy a malloc block to a GC block then we need to clear NEEDS_FREE.
flags &= ~(BLOCK_NEEDS_FREE|BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
if (wantsOne)
flags |= BLOCK_IS_GC | 2;
else
flags |= BLOCK_IS_GC;
result->flags = flags;
_Block_call_copy_helper(result, aBlock);
if (hasCTOR) {
result->isa = _NSConcreteFinalizingBlock;
}
else {
result->isa = _NSConcreteAutoBlock;
}
return result;
}
}
由源碼可知,在使用垃圾回收機制時纔會創建這兩種類型的Block,且當涉及C++代碼的helper會生成_NSConcreteFinalizingBlock類型的Block,其他情況下生成_NSConcreteAutoBlock類型的Block。
這兩種類型的Block我們可以不用關注了,在iOS上並沒有垃圾回收機制,同時在最新的libclosure代碼中,這兩種類型的Block已經沒有創建路徑了,即不可能創建出這兩種類型的Block了。
四、內存管理
爲了能夠直觀的觀察Block引用計數的變化,我們模擬構造與源碼佈局一致的結構體,這樣便於直接觀察flags
的變化:
enum {
MBlock_REFCOUNT_MASK = (0xfffe),
MBlock_HAS_COPY_DISPOSE = (1 << 25),
MBlock_HAS_CTOR = (1 << 26),
MBlock_IS_GLOBAL = (1 << 28),
MBlock_HAS_STRET = (1 << 29),
MBlock_HAS_SIGNATURE = (1 << 30),
};
struct MBlockLiteral {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct MBlockDecriptor1 *descriptor;
};
struct MBlockDescriptor1 {
uintptr_t reserved;
uintptr_t size;
};
同時我們知道Block本質上是一個對象,那麼涉及到內存管理的方法就只有retain
、release
以及copy
三個方法了,我們接下來就從這三個方法進行研究。
從源碼中,我們可以找尋到這三個方法對應的原始方法,分別爲:bool _Block_tryRetain(const void *arg)
、void _Block_release(const void *arg)
以及void *_Block_copy(const void *arg)
。
爲了方便觀察三個方法的調用情況,我們使用fishhook對這三個方法進行hook:
static bool hook_Block_tryRetain(const void *arg);
bool (*origin_Block_tryRetain)(const void *arg);
static void * hook_Block_copy(const void *arg);
void *(*origin_Block_copy)(const void *arg);
static void hook_Block_release(const void *arg);
void (*origin_Block_release)(const void *arg);
static bool hook_Block_tryRetain(const void *arg) {
NSLog(@"Block_tryRetain %@", (__bridge id)arg);
return origin_Block_tryRetain(arg);
}
static void * hook_Block_copy(const void *arg) {
NSLog(@"Block_copy %@", (__bridge id)arg);
return origin_Block_copy(arg);
}
static void hook_Block_release(const void *arg) {
NSLog(@"Block_release %@", (__bridge id)arg);
origin_Block_release(arg);
}
struct rebinding rebindBlock_tryRetain;
rebindBlock_tryRetain.name = "_Block_tryRetain";
rebindBlock_tryRetain.replacement = hook_Block_tryRetain;
rebindBlock_tryRetain.replaced = (void *)&origin_Block_tryRetain;
struct rebinding rebindBlock_copy;
rebindBlock_copy.name = "_Block_copy";
rebindBlock_copy.replacement = hook_Block_copy;
rebindBlock_copy.replaced = (void *)&origin_Block_copy;
struct rebinding rebindBlock_release;
rebindBlock_release.name = "_Block_release";
rebindBlock_release.replacement = hook_Block_release;
rebindBlock_release.replaced = (void *)&origin_Block_release;
struct rebinding rebs[3] = {rebindBlock_tryRetain, rebindBlock_copy, rebindBlock_release};
rebind_symbols(rebs, 3);
_NSConcreteGlobalBlock
我們創建一個全局Block,分別調用三個內存管理方法,然後觀察輸入日誌:
void(^block1)(void) = ^(void) {
NSLog(@"block1");
};
struct MBlockLiteral *block1Ref = (__bridge struct MBlockLiteral *)block1;
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 retain];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 copy];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
[block1 release];
NSLog(@"block1---%@ %lu", block1, (unsigned long)block1Ref->flags & MBlock_REFCOUNT_MASK);
輸出爲:
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
block1---<__NSGlobalBlock__: 0x106132050> 0
可以看出,對於全局Block來說,在調用三個內存管理方法時,並未觸發相關方法,即對於全局Block來說,調用retain
、release
、copy
來說,是無效的。
同時可以看出,全局Block的引用計數一直保持在0。
_NSConcreteStackBlock
我們創建一個棧Block,分別調用retain
和release
(由於copy方法會產生堆Block,我們將copy放在堆Block中討論)。
int a = 0;
void(^block2)(void) = ^(void) {
NSLog(@"block2 %d", a);
};
struct MBlockLiteral *block2Ref = (__bridge struct MBlockLiteral *)block2;
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
[block2 retain];
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
[block2 release];
NSLog(@"block2---%@ %lu", block2, (unsigned long)block2Ref->flags & MBlock_REFCOUNT_MASK);
輸出爲:
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
block2---<__NSStackBlock__: 0x7ffeef0de0a0> 0
可以看出,對於棧Block來說,調用retain
和release
是,也未觸發相關方法,即對於棧Block來說,調用retain
和release
來說,是無效的。
同時可以看出,此Block的地址較高,位於棧區,引用指數也一直保持在0,釋放時機由系統控制。
_NSConcreteMallocBlock
我們由棧Block通過copy方法創建一個堆Block,並分別調用retain
、release
和copy
。
void(^block3)(void) = ^(void) {
NSLog(@"block3 %d", a);
};
block3 = [block3 copy];
struct MBlockLiteral *block3Ref = (__bridge struct MBlockLiteral *)block3;
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 retain];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 copy];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 release];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
[block3 release];
NSLog(@"block3---%@ %lu", block3, (unsigned long)block3Ref->flags & MBlock_REFCOUNT_MASK);
輸出爲:
Block_copy <__NSStackBlock__: 0x7ffee4514068>
2020-06-17 16:20:28.750074+0800 TheTestTest[26212:315120] block3---<__NSStackBlock__: 0x7ffee4514068> 0
2020-06-17 16:20:28.750142+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 2
2020-06-17 16:20:28.750201+0800 TheTestTest[26212:315120] Block_copy <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750453+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 4
2020-06-17 16:20:28.750522+0800 TheTestTest[26212:315120] Block_copy <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750585+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 6
2020-06-17 16:20:28.750648+0800 TheTestTest[26212:315120] Block_release <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750713+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 4
2020-06-17 16:20:28.750778+0800 TheTestTest[26212:315120] Block_release <__NSMallocBlock__: 0x600001122340>
2020-06-17 16:20:28.750838+0800 TheTestTest[26212:315120] block4---<__NSMallocBlock__: 0x600001122340> 2
由以上輸出可知:
- 將棧Block通過copy方法複製到堆上時,調用的是_Block_copy方法,在copy之後,棧Block的引用計數還是0,堆Block的引用計數變爲2;
- 爲堆Block調用retain和copy方法,調用的均是_Block_copy方法,調用後,堆Block的引用計數增加2;
- 爲堆Block調用release方法,調用的是_Block_release方法,調用後,堆Block的引用計數減少2;
通過以上結論,我們可知,堆Block的內存管理集中在_Block_copy
和_Block_release
兩個方法中,我們來看一下兩個方法的實現,其中的邏輯我已做出註釋:
//libclosure-74
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 若傳入空,直接返回
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
// 若Block的BLOCK_NEEDS_FREE標誌爲1,表示Block爲堆Block,則調用latching_incr_int方法
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 若Block是全局Block,直接返回
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// 若Block是棧Block,則拷貝至堆區
// 在堆區申請空間
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// 拷貝內容
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// 設置回調指針
result->invoke = aBlock->invoke;
#endif
// 將flag的1-23位引用計數標誌位以及第0位是否正在釋放位重置
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
// 設置flag的BLOCK_NEEDS_FREE位置爲1,表示此時處於堆區;同時與2,將第1位置爲1,表示引用計數此時爲1
result->flags |= BLOCK_NEEDS_FREE | 2;
// 調用copy helper
_Block_call_copy_helper(result, aBlock);
// 設置Block的isa指針爲堆Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 若flags的第1-23位全部爲1(即引用計數到達最大值)時,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
// 將flags的值增加2,即從第1位開始增加1,表示引用計數加1
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
//libclosure-74
void _Block_release(const void *arg) {
struct Block_layout *aBlock = (struct Block_layout *)arg;
// 如果傳入Block爲空,直接返回
if (!aBlock) return;
// 如果傳入的Block爲全局Block,直接返回
if (aBlock->flags & BLOCK_IS_GLOBAL) return;
// 如果傳入的Block不是堆Block,即是棧Block,直接返回
if (! (aBlock->flags & BLOCK_NEEDS_FREE)) return;
// 調用latching_decr_int_should_deallocate減少引用計數
if (latching_decr_int_should_deallocate(&aBlock->flags)) {
// 調用dispose helper
_Block_call_dispose_helper(aBlock);
// 釋放堆區內存
_Block_destructInstance(aBlock);
free(aBlock);
}
}
static bool latching_decr_int_should_deallocate(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
// 若flags的第1-23位全部爲1(即引用計數到達最大值)時,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return false;
}
// 若flags的第1-23位全部爲0(即引用計數爲0)時,不做操作,直接返回
if ((old_value & BLOCK_REFCOUNT_MASK) == 0) {
return false;
}
// 將flags的值減少2,即從第1位開始減少1,表示引用計數減1
int32_t new_value = old_value - 2;
bool result = false;
// 若此時引用計數剛好是2,即計數爲1,將flags減爲1,此時第1位爲0,第0位置爲1,表示引用計數爲0並正在釋放
if ((old_value & (BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING)) == 2) {
new_value = old_value - 1;
result = true;
}
if (OSAtomicCompareAndSwapInt(old_value, new_value, where)) {
return result;
}
}
}
源碼邏輯並不複雜,在_Block_copy
中,有對全局Block的空處理,在_Block_release
中,有對全局Block和棧Block的空處理,從之前的分析我們可知,這些處理其實永遠不用執行,但源碼同樣做了異常保護。
在每次增加/減少引用計數時,是將flags增加/減少2,這是由於flags的第1-23位保存的是引用計數,需要對第0位進行偏移。
以上就是Block引用計數機制及各個分類Block內存管理的實現原理,底部實現其實非常有設計性,使用一個32位的flags可以記錄Block的各種信息,進而減少內存的使用。
如有興趣,可以去研究一下Object的引用計數機制,它同樣也是採用一個flag記錄多個信息,其中引用計數是從第2位開始的,所以每次都是以4爲單位進行增減的,但不一樣的是,它的引用計數並沒有保存在自身結構體中,而是保存在SideTable中的。