玖:Block 原理面試(1)
- block的原理是怎樣的?本質是什么?
答:Block 的本質是一個封裝了函數及其調用環境的 Objective-C 對象。原理詳細見「Block 使用及結構」
- block的屬性修飾詞為什么是copy?使用block有哪些使用注意?
答: MRC 下 block 如果沒有 copy 到堆上,值捕獲不會對外部變量引用。 雖然 ARC 環境 strong 也可以修飾 Block,那是因為編譯器會對 strong 修飾的 block 也會進行一次 copy 操作。為什么用 copy 修飾算是歷史習慣問題,推薦不管 ARC、MRC 使用 copy 修飾 。使用注意:循環引用問題
Tip:本文中以下代碼均為 ARC 環境,除非特別注明 MRC。
Block 使用及結構
來看一段簡單的 Block 的代碼:
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"hello world");
};
block();
}
return 0;
}
然后通過 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
查看編譯后的 C++ 代碼。
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
可以看到 block 在編譯之后轉換成了__main_block_impl_0
結構體,結構體的包含的成員如下:
struct __main_block_impl_0 {
// 相當于copy 了整個struct __block_impl impl
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
// 相當于copy 了整個struct __block_impl impl
// Des 指針(描述 block 的大小 )
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;
}
};
[圖片上傳失敗...(image-f96567-1582901096408)]
__main_block_impl_0
結構體和對象結構類似,首個成員是 isa 指針,指向類對象,由此可以推斷 block 可能也是 OC 對象(在下文「Block 類型」中詳細說明)。
此外 __main_block_impl_0
的 FuncPtr
函數指針指向了封裝 block 代碼塊的函數 __main_block_func_0
:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_23bff8_mi_0);
}
一切就緒之后在main
函數中開始執行block。
int main(int argc, const char * argv[]) {
// __AtAutoreleasePool 后面的文章在做講解
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 去除強制轉換后簡化的代碼
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA));
block->FuncPtr(block);
}
return 0;
}
block 結構體小結:
[圖片上傳失敗...(image-a24220-1582901096408)]
其中copy
和 dispose
兩個函數下文「對象類型的值捕獲」會提到。
Block 值捕獲(基本數據類型)
簡單的帶參數 Block (不會進行值捕獲)
void(^block)(int,int) = ^(int a, int b){
NSLog(@"a = %d, b = %d",a,b);
};
block(20,20);
帶參數的 block, 在編譯之后__main_block_impl_0
、__main_block_desc_0
結構并未發生變化。只有__main_block_func_0
在定義和使用中新增了連個 a, b 參數。這種 block 并不涉及到值捕獲。
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int a, int b) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_aec4c2_mi_0,a,b);
}
void(*block)(void) = &__main_block_impl_0(__main_block_func_0,&__main_block_desc_0_DATA));
block->FuncPtr(block,20,20);
局部變量捕獲
捕獲auto變量
簡單的 auto 變量地址捕獲:
// 局部變量默認 auto 修飾
int age = 10; // 相當于 auto int age = 10;
void(^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
// 輸出
age is 10
如果在 block 中訪問了 auto 變量, block 的結構體會發生什么變化呢:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
在上面的__main_block_impl_0
結構體中新增加一個 int age;
成員。__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0): age(_age)
構造方法也有了一個 _age
參數 函數將 _age
賦值給了結構體的 age 成員屬于值傳遞。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_232207_mi_0,age);
}
在執行 block 中的代碼塊函數時,__main_block_impl_0
中的 age 是值傳遞與局部變量 age 無關,所以即使外部的 age 變量修改了值。也是不會影響 block 中早已捕獲的 age。
捕獲static變量
block 捕獲靜態變量
static int age = 10;
void(^block)(void) = ^{
NSLog(@"age is %d",age);
};
age = 20;
block();
// 輸出
age is 20
如果 block 捕獲的是靜態變量, block 的結構體又會發生什么變化?經過 clang 編譯之后:
static int age = 10;
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &age));
age = 20;
block->FuncPtr(block);
和之前 auto 變量比較,static 傳遞的參數是 age
的地址屬于地址傳遞,__main_block_impl_0
的成員 int *age
存放的是 age 的地址,訪問的是同一塊內存,所以 age 在外部更改之后,block 中的 age 指向的值也會變動。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *age;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
局部變量捕獲 auto 和 static 的區別
- auto 變量會在作用域之后銷毀,所以 block 會將 age 進行值傳遞,并存放
__main_block_impl_0
成員 age 中,用于以后可以隨時訪問。 - static 的變量在初始化后會一直存放內存中,所以我們可以通過地址直接訪問,不用擔心變量作用域的問題,block 結構體的構造方法傳遞的是靜態變量 age 的地址。
全局變量
static int age = 10;
int height = 30;
int main(int argc, const char * argv[]) {
@autoreleasepool {
void(^block)(void) = ^{
NSLog(@"age is %d,height is %d",age,height);
};
age = 20;
height = 40;
block();
}
return 0;
}
經過 clang 編譯之后:
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;
}
};
發現 __main_block_impl_0
結構體中沒有任何的值捕獲的成員變量,是因為當 block 中的代碼塊需要訪問全局變量時,可以直接訪問, block 沒有必要在進行值捕獲。
// 直接訪問全局變量 和 全局靜態變量
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_s5_1zc18cl97tn280nn_vywbl1m0000gp_T_main_d25360_mi_0,age,height);
}
Block 類型
在前面的 Block 結構體中都存在一個 isa 指針,且在構造函數的時候賦值 &_NSConcreteStackBlock
。所以可以猜測認為 block 其實也是對象的一種,
嘗試對 block 調用 class 方法來看看會有什么輸出:
Class cls = [block class];
while (cls) {
NSLog(@"%@",cls);
cls = [cls superclass];
}
// 依次輸出:
__NSGlobalBlock__
__NSGlobalBlock
NSBlock
NSObject
可以看出來 block 確實是對象且主要的 block 類型(都是繼承自NSBlock
)有以下三種:
-
__NSGlobalBlock__
( _NSConcreteGlobalBlock )存放在 數據段 中 -
__NSStackBlock__
( _NSConcreteStackBlock ) 存放在 棧 中 -
__NSMallocBlock__
( _NSConcreteMallocBlock )存放在 堆 中
block 是屬于哪一種類型總結下來可以用下面的圖片表示:
[圖片上傳失敗...(image-5e405e-1582901096408)]
// ARC 下賦值給 __Strong(默認)的 變量時會自動調用 copy方法,將 block copy到堆上,無法準確查看 block 類型
// 下面代碼為 MRC 環境
// __NSGlobalBlock__
void(^block1)(void) = ^{
NSLog(@"hello world");
};
// __NSStackBlock__
void(^block2)(void) = ^{
NSLog(@"hello age:%d",age);
};
// __NSMallocBlock__
void(^block3)(void) = [block2 copy];
NSLog(@"block1:%@,block2:%@,block3:%@",block1,block2,block3);
// release 省略下...
// 輸出:
block1:<__NSGlobalBlock__: 0x1000010a8>,
block2:<__NSStackBlock__: 0x7ffeefbff480>,
block3:<__NSMallocBlock__: 0x100638080>
補充: ARC 環境下下列操作會自動 block 進行 copy 操作:
- block 作為方法的返回值
- 將 block 賦值給 __strong 指針時
- block 作為Cocoa API中方法名含有usingBlock的方法參數時
- block 作為GCD API的方法參數時
Block 值捕獲(對象類型)
前面提到的值捕獲都是基本數據類型,如果在 block 捕獲的值是對象類型的話, block的結構體又會發生什么變化呢?
Person *p = [Person new];
p.name = @"hello block!";
void(^block)(void) = ^{
NSLog(@"--- %@",p.name);
};
block();
將上面的代碼 clang
編譯之后:
[圖片上傳失敗...(image-cc8f86-1582901096408)]
對比之前捕獲的普通 auto 變量,可以在圖中看到 block 捕獲的對象變量 Person *p
時在 desc
中新增了兩個函數的指針:
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
在 block 執行構造函數時,會對賦值兩個函數的地址。
_Block_object_assign
函數會在 block 進行一次 copy 操作的時候被調用。
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
_Block_object_assign
函數會根據 auto 變量的修飾符(__strong(默認)
、__weak
、__unsafe_unretained
)做出相應的操作,block 結構體中的 Person *p
對外部的 auto 變量形成強引用(strong)或者弱引用(weak)。
如果block從堆上移除時,會調用 block 內部的_Block_object_dispose
函數。
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
_Block_object_dispose
函數會對結構體中的 Person *p
進行 release 操作。
enum {
/* See function implementation for a more complete description of these fields and combinations */
BLOCK_FIELD_IS_OBJECT = 3, /* id, NSObject, __attribute__((NSObject)), block, ... */
BLOCK_FIELD_IS_BLOCK = 7, /* a block variable */
BLOCK_FIELD_IS_BYREF = 8, /* the on stack structure holding the __block variable */
BLOCK_FIELD_IS_WEAK = 16, /* declared __weak, only used in byref copy helpers */
BLOCK_BYREF_CALLER = 128 /* called from __block (byref) copy/dispose support routines. */
};
補充:
如果 block 如果在棧上,自身的生命周期都不確定,所以無法對外部變量進行引用。當 block 是
__NSStackBlock__
類型是不會對 auto 變量進行強引用。__weak
的作用:
__weak Person *weakPerson = p;
void(^block)(void) = ^{
NSLog(@"--- %@",weakPerson.name);
};
block();
使用xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 main.m
clang 編譯后__main_block_impl_0
區別在于 weakPerson是弱引用:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
}
- block 的屬性修飾
在 MRC 環境下:
@property (copy, nonatomic) void (^block)(void);
在 ARC 環境下block屬性的可以用 strong、copy 修飾,ARC 環境下會默認給賦值 strong 的block進行一次 copy 操作。但一般推薦使用 copy 修飾。算是代碼習慣。
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
文章首發:由面試題來了解iOS底層原理