前言
block可以叫回調(diào)代碼塊,是iOS開發(fā)中至關(guān)重要的形式之一。不同的編程語言都會用到block, 只是體現(xiàn)形式有所不同,例如c/c++
叫函數(shù)指針
,javascript
叫它閉包
。它用簡單的方式幫我們解決了很多復(fù)雜的問題。
block如何將變量傳遞及持有
測試代碼:
傳遞原則:
- 捕獲對象是
基礎(chǔ)類型
變量,如int, double類型時,是值傳遞。 - 捕獲對象是一個object,那么它會被強引用
我們來驗證一下,我們在block賦值之后修改a的值
ViewController.m:
int a = 10;
_foo.testBlock= ^() {
_testView.backgroundColor = nil;//持有了self
NSLog(@"a:%d", a);
};
a+= 10;
_foo.testBlock();
輸出結(jié)果:仍然是10,它不會被外界改變。
但是如果我們用__block來修飾int a,也就是 __block int a = 10
, 最終a的值就是20,它被外界改變了,__block幫我們解決問題。
但是Why? block內(nèi)部是以什么形式存在,并捕獲值的呢?接下來我們要一探究竟。
準(zhǔn)備工作:clang命令
大家可以用clang(或者gcc) -rewrite-objc xxxxx.m
命令來查看轉(zhuǎn)化成的c++代碼來了解內(nèi)幕。如果你引用了UIKIt
庫,這個命令會報錯,那個因為命令里沒有指定sdk的版本,此時用下面的命令完美解決:
clang -x objective-c -rewrite-objc -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk ViewController.m
__block的奧秘
不帶__block的轉(zhuǎn)化cpp代碼:
int a = 10;
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, a, 570425344)));
a+= 10;
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();
帶__block轉(zhuǎn)化代碼:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};//對a的封裝進行初始化
((void (*)(id, SEL, void (*)()))(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("setTestBlock:"), ((void (*)())&__ViewController__viewDidLoad_block_impl_0((void *)__ViewController__viewDidLoad_block_func_0, &__ViewController__viewDidLoad_block_desc_0_DATA, self, (__Block_byref_a_0 *)&a, 570425344)));
(a.__forwarding->a)+= 10;
((void (*(*)(id, SEL))())(void *)objc_msgSend)((id)(*(Foo **)((char *)self + OBJC_IVAR_$_ViewController$_foo)), sel_registerName("testBlock"))();
__Block_byref_a_0的定義如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;//---->它就是傳遞進來的a
};
我們發(fā)現(xiàn)編譯器把int a封裝
成了一個叫__Block_byref_a_0
的結(jié)構(gòu)體,最終用&
符號將它的地址傳遞給了block,所以后面a被修改時,block里面的a也被同時修改。__block的奧秘就是這個!
Block的存在形式
object-c的Block最終以轉(zhuǎn)化成多個了結(jié)構(gòu)體,每個結(jié)構(gòu)體都不同的職責(zé)。
__ViewController__viewDidLoad_block_impl_0 包含了block相關(guān)的所有信息的
基本構(gòu)造是:
- impl
- desc
- 引用變量列表:ivar1, ivar2, ivar...
struct __ViewController__viewDidLoad_block_impl_0 {
struct __block_impl impl;//block的一個簡化類,它里面存儲了函數(shù)指針
struct __ViewController__viewDidLoad_block_desc_0* Desc;//block的描述類
ViewController *self;//被引用的viewcontroller
__Block_byref_a_0 *a; // 被引用的a的封裝
__ViewController__viewDidLoad_block_impl_0(void *fp, struct __ViewController__viewDidLoad_block_desc_0 *desc, ViewController *_self, __Block_byref_a_0 *_a, int flags=0) : self(_self), a(_a->__forwarding) {//構(gòu)造函數(shù)
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block_impl定義如下,很像一個類,里面有isa指針和block的函數(shù)指針,所以我們可以把它當(dāng)作一個類
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
我們可以發(fā)現(xiàn)__ViewController__viewDidLoad_block_impl_0結(jié)構(gòu)體的第一個參數(shù)是__block_impl,所以這兩個結(jié)構(gòu)體實例的地址是相同的。那到底oc的block是誰呢?我們一般會認(rèn)為oc的block就是__block_impl,因為它正好有isa,可以作為一個類,這樣大家都容易理解。
Block的三個子類
1.我們可以打印一個block的類及父類的名字:(這段代碼摘自facebook的FBRetainCycleDetector
)
static Class _BlockClass() {
static dispatch_once_t onceToken;
static Class blockClass;
dispatch_once(&onceToken, ^{
void (^testBlock)() = [^{} copy];
blockClass = [testBlock class];
while(class_getSuperclass(blockClass) && class_getSuperclass(blockClass) != [NSObject class]) {
blockClass = class_getSuperclass(blockClass);
}
[testBlock release];
});
return blockClass;
}
結(jié)果是__NSGlobalBlock -> NSBlock -> NSObject
事實上block有三種形式:
- __NSGlobalBlock 全局 (未捕獲變量)
- __NSStackBlock 棧 捕獲變量
- __NSMallocBlock 堆 捕獲變量
在 ARC 中,捕獲外部了變量的 block 的類會是 NSMallocBlock 或者 NSStackBlock,如果 block 被賦值給了某個變量在這個過程中會執(zhí)行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock;但是如果 block 沒有被賦值給某個變量,那它的類型就是 NSStackBlock;沒有捕獲外部變量的 block 的類會是 NSGlobalBlock 即不在堆上,也不在棧上,它類似 C 語言函數(shù)一樣會在代碼段中。
2.那什么時候在堆上,什么時候在棧上呢?
在ARC有效時,大多數(shù)情況下編譯器會進行判斷,自動生成將Block從棧上復(fù)制到堆上的代碼,以下幾種情況棧上的Block會自動復(fù)制到堆上:
- 調(diào)用Block的copy方法
- 將Block作為函數(shù)返回值時
- 將Block賦值給__strong修改的變量時
- 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時
3.block用strong修飾還是copy修飾呢?
實際上調(diào)用retain方法時, block會調(diào)用copy方法,所以這兩種修飾是相同的。但是為了語義的明確,推薦用copy修飾。
獲取block引用的對象
現(xiàn)在不講源碼,只講實際應(yīng)用。如何知道一個block對象,如何知道它持有的所有對象呢?facebook的FBRetainCycleDetector
檢測循環(huán)引用的庫就實現(xiàn)這樣的功能,非常的巧妙!我們就FBRetainCycleDetector的源碼展出分析。
1.調(diào)用allRetainedObjects來獲取:
- (NSSet *)allRetainedObjects
{
NSMutableArray *results = [[[super allRetainedObjects] allObjects] mutableCopy];
// Grab a strong reference to the object, otherwise it can crash while doing
// nasty stuff on deallocation
__attribute__((objc_precise_lifetime)) id anObject = self.object;//objc_precise_lifetime 翻譯一下就是:精確生命周期,其實是強引用了object對象,防止在運行期間被釋放
void *blockObjectReference = (__bridge void *)anObject;
NSArray *allRetainedReferences = FBGetBlockStrongReferences(blockObjectReference);//獲得所有引用對象,就是下面要講的方法
for (id object in allRetainedReferences) {
FBObjectiveCGraphElement *element = FBWrapObjectGraphElement(self, object, self.configuration);//對每個對象進行包裝成box對象
if (element) {
[results addObject:element];
}
}
return [NSSet setWithArray:results];
}
2.FBGetBlockStrongReferences方法通過調(diào)用_GetBlockStrongLayout(block)方法返回的持有對象的位置Index, 然后通過偏移量來取得對應(yīng)的對象:
NSArray *FBGetBlockStrongReferences(void *block) {
if (!FBObjectIsBlock(block)) {//是否是block類型
return nil;
}
NSMutableArray *results = [NSMutableArray new];
void **blockReference = block;
NSIndexSet *strongLayout = _GetBlockStrongLayout(block);//得到block里強引用的對象
[strongLayout enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
void **reference = &blockReference[idx];//把它當(dāng)作了一個鏈表來取值,它本是block結(jié)構(gòu)體,正好它的align對齊字節(jié)大小是void*類型大小。怪不得blockReference要用void **來修飾,要以把它當(dāng)作(void *)*blockReference,所以blockReference是一個指向(void *)的指針。
if (reference && (*reference)) {
id object = (id)(*reference);
if (object) {
[results addObject:object];
}
}
}];
return [results autorelease];
}
我寫了一個demo, block里引用了viewcontroller, 但是viewController的idx是4,為什么呢?我們打印了blockLiteral結(jié)構(gòu)體及成員變量的地址:
struct BlockLiteral {
void *isa; //0x00000001702493f0 占8個字節(jié)
int flags;//0x00000001702493f8 占4個字節(jié)
int reserved;//0x00000001702493fc 占4個字節(jié)
void (*invoke)(void *, ...);//0x0000000170249400 占8個字節(jié)
struct BlockDescriptor *descriptor;//0x0000000170249408 占8個字節(jié)
// imported variables
};
所有指針類型
全是占8
個字節(jié),int類型
占4
個字節(jié),所以flags+reserved加起來是相當(dāng)于一個指針類型,不管怎么說,BlockLiteral的大小是固定的,它的對齊字節(jié)
是8,它的大小正好是sizeof(void)的整數(shù)倍
。這也解釋了為什么定義了void **blockReference = block;
,它把blockReference定義成了指向(void)的指針!(這里寫的有點啰唆,實際上void*是最長的字節(jié)8,因為Int類型字節(jié)長只有4,所以是以void*作為進行字節(jié)對齊的)
3._GetBlockStrongLayout是整個過程最關(guān)鍵,也是最巧妙的地方:
static NSIndexSet *_GetBlockStrongLayout(void *block) {
struct BlockLiteral *blockLiteral = block;
/**
BLOCK_HAS_CTOR - Block has a C++ constructor/destructor, which gives us a good chance it retains
objects that are not pointer aligned, so omit them.
!BLOCK_HAS_COPY_DISPOSE - Block doesn't have a dispose function, so it does not retain objects and
we are not able to blackbox it.
*/
if ((blockLiteral->flags & BLOCK_HAS_CTOR)
|| !(blockLiteral->flags & BLOCK_HAS_COPY_DISPOSE)) {
return nil;
}
void (*dispose_helper)(void *src) = blockLiteral->descriptor->dispose_helper;
const size_t ptrSize = sizeof(void *);
// Figure out the number of pointers it takes to fill out the object, rounding up.
const size_t elements = (blockLiteral->descriptor->size + ptrSize - 1) / ptrSize;
// Create a fake object of the appropriate length.
void *obj[elements];
void *detectors[elements];
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = [FBBlockStrongRelationDetector new];
obj[i] = detectors[i] = detector;
}
@autoreleasepool {
dispose_helper(obj);
}
// Run through the release detectors and add each one that got released to the object's
// strong ivar layout.
NSMutableIndexSet *layout = [NSMutableIndexSet indexSet];
for (size_t i = 0; i < elements; ++i) {
FBBlockStrongRelationDetector *detector = (FBBlockStrongRelationDetector *)(detectors[i]);
if (detector.isStrong) {
[layout addIndex:i];
}
// Destroy detectors
[detector trueRelease];
}
return layout;
}
重寫了FBBlockStrongRelationDetector類的release方法:
- (oneway void)release
{
_strong = YES;
}
這個過程是:
- 獲取銷毀函數(shù)dispose_helper的函數(shù)指針
- 通過descriptor->size計算出block里成員變量個數(shù)(上面我們說過它把兩個int算作了一個void*, 所以成員變量的個數(shù)實際上少了1個)
- 創(chuàng)建兩個相同個數(shù)的fake數(shù)組obj,detectors,然后通過dispose_helper來只釋放obj數(shù)組,dispose_helper會向調(diào)用每個對象的release方法, 而它又重寫了release方法,在release時作了標(biāo)記,通過這個標(biāo)記就可以判斷是為是引用的對象。它其實是欺騙了dispose_helper函數(shù),因為它只認(rèn)位置Index, 并不關(guān)心數(shù)組存儲的是什么...
不得不贊嘆fb的源碼的深度和創(chuàng)新~
結(jié)語
還是那句話:源碼下面無秘密。
蘋果底層對于block實現(xiàn)真是煞費苦心。我們了解了原理,用起來會更加得深應(yīng)手。