【iOS底層分析】Swift閉包&OC閉包
基礎
- Block是⼀個自包含的(捕獲了上下⽂的常量或者是變數的)函式程式碼塊,可以在程式碼中被傳遞和使用。
- 全域性和巢狀函式實際上也是特殊的閉包,閉包採用如下三種形式之一:
- 全域性函式是一個有名字但不會捕獲任何值的閉包
- 巢狀函式是一個有名字並可以捕獲其封閉函式域內值的閉包
- 閉包表示式是一個利用輕量級語法所寫的可以捕獲其上下文中變數或常量值的匿名閉包
OC-Block
分類
NSGlobalBlock
- 位於全域性區
- 在Block內部不使用外部變數,或者只使用靜態變數和全域性變數
NSMallocBlock
- 位於堆區
- 被強持有
- 在Block內部使用區域性變數或OC屬性,可以賦值給強引用/copy修飾的變數
NSStackBlock
- 位於棧區
- 沒有被強持有
- 在Block內部使用區域性變數或OC屬性,不能賦值給強引用/copy修飾的變數
如下簡單demo code所示
```objectivec int a = 10; // 區域性變數
void(^Global)(void) = ^{ NSLog(@"Global"); };
void(^Malloc)(void) = ^{ NSLog(@"Malloc,%d",a); };
void(^__weak Stack)(void) = ^{ NSLog(@"Stack,%d",a); };
NSLog(@"%@",Global); // <NSGlobalBlock: 0x101aa80b0> NSLog(@"%@",Malloc); // <NSMallocBlock: 0x600003187900> NSLog(@"%@",Stack); // <NSStackBlock: 0x7ff7b12c22f0> ```
下面重點介紹堆Block。
NSMallocBlock
Block拷貝到堆Block的時機:
- 手動copy
- Block作為返回值
- 被強引用/copy修飾
- 系統API包含using Block
所以總結一下堆Block判斷依據:
- Block內部有沒有使用外部變數
- 使用的變數型別?區域性變數/OC屬性/全域性變數/靜態變數
- 有沒有被強引用/copy修飾
原始碼探究
我們建立一個捕獲了局部變數的block
```objectivec
import
void test() { int a = 10;
void(^Malloc)(void) = ^{
NSLog(@"%d",a);
};
} ```
執行clang -rewrite-objc main.m -o main.cpp
命令,檢視main.cpp檔案可以看到Malloc閉包的結構如下。
```cpp struct __test_block_impl_0 { struct __block_impl impl; struct __test_block_desc_0* Desc;
// 內部儲存了變數a
int a;
/// 初始化函式。包含三個引數
// - Parameters:
/// - fp: 函式指標 /// - desc: 描述 /// - _a: flag
__test_block_impl_0(void fp, struct __test_block_desc_0 desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
// 建立Malloc閉包,傳入引數如下 // fp: (void )__test_block_func_0 // desc: &__test_block_desc_0_DATA // _a: 變數a的值(值拷貝) void(Malloc)(void) = ((void ()())&__test_block_impl_0((void )__test_block_func_0, &__test_block_desc_0_DATA, a));
// __test_block_func_0實現如下 static void __test_block_func_0(struct __test_block_impl_0 *__cself) { int a = __cself->a; // bound by copy NSLog(···); } ```
開啟llvm可以看到,該block原本是在棧上,呼叫了objc_retainBlock
方法,而在該方法中實際呼叫了_Block_copy
方法。
在Block.h的原始碼中可以找到_Block_copy
方法,其官方註釋是“建立一個基於堆的Block副本,或者簡單地新增一個對現有Block的引用。”,從而將這個棧block拷貝到了堆上,下面我們根據該方法的原始碼來探究一下堆Block的原理。(只擷取重點程式碼)
```cpp void _Block_copy(const void arg) { return _Block_copy_internal(arg, true); }
static void _Block_copy_internal(const void arg, const bool wantsOne) { struct Block_layout *aBlock;
···
// 型別強轉為Block_layout
aBlock = (struct Block_layout *)arg;
···
// Its a stack block. Make a copy.
// 分配記憶體
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
// isa重新標記為Malloc Block
result->isa = _NSConcreteMallocBlock;
_Block_call_copy_helper(result, aBlock);
return result;
} ```
Block底層結構為Block_layout
struct Block_layout {
void *isa; // isa指標
volatile int32_t flags; // contains ref count
int32_t reserved; // 保留位
void (*invoke)(void *, ...); // call out funtion
struct Block_descriptor_1 *descriptor;
};
總結:
Block在執行時才會被copy,在堆上開闢記憶體空間。
迴圈引用
解決方案
-
__weak
+__strong
思路: 在block裡短暫持有self的生命週期。(
weak
自動置空)```objectivec self.name = @"YK";
__weak typeof(self) weakSelf = self; self.block = ^{ __strong typeof(self) strongSelf = weakSelf; strongSelf.callFunc(); }; ```
-
__block
思路: 值拷貝。(手動置空)
我們有如下程式碼,生成cpp檔案看一下
```objectivec
import
void test() { __block int a = 10;
void(^Malloc)(void) = ^{
a++;
NSLog(@"%d",a);
};
Malloc();
} ```
```cpp // 可以看到傳入的第三個引數,是__Block_byref_a_0結構體型別的a變數地址,而不是上面講過的直接儲存int型別 void(Malloc)(void) = ((void ()())&__test_block_impl_0((void )__test_block_func_0, &__test_block_desc_0_DATA, (__Block_byref_a_0 )&a, 570425344));
// __test_block_impl_0結構體中儲存的變數也是__Block_byref_a_0型別 struct __test_block_impl_0 { struct __block_impl impl; struct __test_block_desc_0 Desc; __Block_byref_a_0 a; // by ref __test_block_impl_0(void fp, struct __test_block_desc_0 desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
// 初始化__Block_byref_a_0如下 attribute((blocks(byref))) __Block_byref_a_0 a = {(void)0, (__Block_byref_a_0 )&a, 0, sizeof(__Block_byref_a_0), 10};
// __Block_byref_a_0結構體 struct __Block_byref_a_0 { void __isa; __Block_byref_a_0 __forwarding; // 指標指向原始值 int __flags; int __size; int a; // 值拷貝儲存 }; ```
總結 __block
原理
- 建立
__Block_byref_a_0
結構體 - 傳給block指標地址
- block內修改的是與原始值同一片的記憶體空間
注意點
根據上述分析我們可以得出結論,如果在OC的block中捕獲了沒有加__block
的外部變數,在編譯時就會將變數值傳入(值拷貝),如果捕獲了加__block
的外部變數,則會獲取到變數指標對應的記憶體空間的地址。程式碼驗證如下
```cpp int a = 1; __block int b = 2;
void(^Malloc)(void) = ^{ NSLog(@"a,%d",a); NSLog(@"b,%d",b); };
a = 3; b = 4; Malloc();
// 輸出結果如下 // a,1 // b,4 ```
Swift-Closure
- Swift 的閉包表示式擁有簡潔的風格,並鼓勵在常見場景中進行語法優化,主要優化如下:
- 利用上下文推斷引數型別和返回值型別
- 隱式返回單表示式閉包(單表示式閉包可以省略
return
關鍵字) - 引數名稱縮寫,可以用$0,$1表示
- 尾隨閉包語法:如果函式的最後一個引數是閉包,則閉包可以寫在形參小括號的外面。為了增強函式的可讀性。
- Swift 的閉包是一個引用型別,驗證如下。我們知道Swift的引用型別在建立時都會呼叫
swift_allocObject
方法
```swift // 未呼叫swift_allocObject let closure1 = { () -> () in print("closure1") }
// 呼叫swift_allocObject let a = 10 let closure2 = { () -> () in print("closure2 (a)") } ```
捕獲值
- 在閉包中如果通過
[variable1, variabla2]
的形式捕獲外部變數,捕獲到的變數為let
型別,即不可變 - 在閉包中如果直接捕獲外部變數,獲取的是指標,也就是說在閉包內修改變數值的話,原始變數也會被改變。
- 如果捕獲的是指標型別(
Class
),無論是否用[],在閉包內對該變數進行修改,都會影響到原始變數
簡單驗證如下:
```swift var variable = 10 let closure = { () -> () in variable += 1 print("closure (variable)") }
closure() // closure 11 print(variable) // 11 ```
可見直接獲取變數的話,會修改到原始值。
如果改成下面這樣會編譯報錯”可變運算子的左側不可變”
```swift var variable = 10 let closure = { [variable] () -> () in variable += 1 print("closure (variable)") }
closure() print(variable) ```
捕獲指標型別驗證
```swift class YKClass { var name = "old" }
let demoS = YKStruct() let demoC = YKClass()
let closure1 = { [demoC] () -> () in demoC.name = "new" print("closure1 (demoC.name)") }
closure1() // closure1 new print(demoC.name) // new
let closure2 = { () -> () in demoC.name = "new2" print("closure2 (demoC.name)") }
closure2() // closure2 new2 print(demoC.name) // new2 ```