iOS底層原理探索-Block本質(二)
面試題:
問:在24行打斷點,person物件是否被釋放?
按說,person的作用域是其附近的兩個{},過了兩個{}後,person物件應該被釋放,而實際上,在24行斷點處,person物件並沒有消失。
問:為什麼呢?
首先我們將程式執行,可以看到其執行過程:
24行列印block學習[2478:134123] ---------
25行列印block學習[2478:134123] 呼叫了block---10
26行結束列印block學習[2478:134123] YZPerson消失了
將main.m轉化為底層程式碼後,我們進行分析,可以看到block的構成
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; YZPerson *person; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *_person, int flags=0) : person(_person) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
可以看到,block內部捕獲了局部變數YZPerson的值,相當於block內部有一個指標對person物件進行了強引用,從而保證了person物件沒有消失。 在26行過後,block物件消失,因此,person物件也消失。
由於在ARC環境下,系統會幫我們做很多事情,我們需要具體看一下里面的一些細節,因此,我們切換到MRC環境下。
在MRC環境下:
我們發現一個有趣的現象:
在MRC環境下:
沒有對block進行copy操作,person會被釋放。
對block進行copy操作,person不會被釋放。
首先,上述例子中,block是區域性變數,而我們知道:區域性變數是儲存在棧區
^{ NSLog(@"呼叫了block---%d", person.age); };
由於訪問量auto變數person,因此,其實儲存型別是NSStackBlock
型別。
[^{
NSLog(@"呼叫了block---%d", person.age);
} copy];
由於訪問量auto變數person,其實儲存型別是NSStackBlock型別,又因為呼叫了copy,最終其儲存型別是NSMallocBlock型別
當block是NSStackBlock型別時,不能擁有其內部的變數。
這是因為,其本身就是儲存在棧區,是不穩定的。
而當執行copy操作後,其儲存在堆區,可以擁有其內部的變數。
在ARC下,由於block指向物件是有強指標引用的,因此會預設對其進行copy操作,將block指向的物件存放在堆區,因此是可以擁有其內部的變數person。
在ARC環境下:
注意,23行呼叫的是person.age,而不是weakPerson.age
注意,23行呼叫的是weakPerson.age
一個現象:在block內部引用使用__weak修飾的auto變數weakPerson,在26行YZPerson消失了。這又是為什麼呢?
當block進行了copy操作,其內部又經歷了哪些方法和操作呢?
我們再次探究原始碼:
#import "YZPerson.h"
typedef void(^YZBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
YZBlock block;
{
YZPerson *person = [[YZPerson alloc] init];
person.age = 10;
block = ^{
NSLog(@"呼叫了block---%d", person.age);
};
}
NSLog(@"---------");
block();
}
return 0;
}
使用命令列指令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m
其中:
-fobjc-arc表明是arc環境
-fobjc-runtime=ios-12.0.0需要用到執行時,版本12.0.0
轉換為底層程式碼後,block裡面的內容為
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
YZPerson *__strong person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__strong _person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出person是__strong修飾的。
#import "YZPerson.h"
typedef void(^YZBlock)(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {
YZBlock block;
{
YZPerson *person = [[YZPerson alloc] init];
person.age = 10;
__weak YZPerson *weakPerson = person;
block = ^{
NSLog(@"呼叫了block---%d", weakPerson.age);
};
}
NSLog(@"---------");
block();
}
return 0;
}
檢視底層程式碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
YZPerson *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, YZPerson *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出person是__weak修飾的。
當block內部沒有引用外部區域性變數的時候
block = ^{};
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)};
當block內部引用外部區域性變數的時候
block = ^{
NSLog(@"呼叫了block---%d", weakPerson.age);
};
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0
};
兩個函式的實現:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
可以發現,相比block內部沒有引用外部區域性變數,__main_block_desc_0裡面多了兩個函式指標,copy和dispose
當block從棧拷貝到堆裡面的時候,會自動呼叫 __main_block_copy_0 函式,在裡面實現_Block_object_assign,在這個裡面有呼叫外部引用的weakPerson,該呼叫是強指標或者弱指標是根據block定義裡面的weakPerson型別做判斷,追溯到上面,其實是程式碼中__weak YZPerson *weakPerson = person;__weak修飾起的作用。在我們的例子這,該呼叫是一個__weak弱指標呼叫。
同樣,有建立就有消除,當堆上的block將移除的時候,會自動呼叫__main_block_dispose_0函式,在裡面實現_Block_object_dispose,在這個裡面同樣會呼叫外部引用的weakPerson。
如果block是在棧上是NSStackBlock型別時,將不會對auto變數產生強引用
參考:當block是NSStackBlock型別時,不能擁有其內部的變數,因為,其本身就是儲存在棧區,是不穩定的。而當執行copy操作後,其儲存在堆區,可以擁有其內部的變數
當block內部訪問了物件型別為auto的變數時候
如果block被拷貝到堆上
會呼叫block內部的copy函式
copy函式內部會呼叫_Block_object_assign函式
_Block_object_assign函式會根據auto變數的修飾符( __strong、 __weak、__unsafe_unretain )作出相應的操作,形成強引用或者弱引用。
如果block從堆上移除
會呼叫block內部的dispose函式
dispose函式內部會呼叫_Block_object_dispose函式
_Block_object_dispose函式會自動釋放引用的auto變數
blcok內部引用__weak修飾的auto區域性變數,在26行結束後,YZPerson被銷燬的原因是因為,block內部對其進行的是__weak弱引用。
需要注意的是,如果block內部訪問的區域性變數為非物件型別,是不會生成copy和dispose函式的。
幾個測試題b
block作為GCD引數的時候,會將block複製到堆上,而裡面又引用了person區域性變數的物件,因此會對block裡面的person物件變數進行類似強引用功能,從而保證person在{}消失的時候不會消失。在3秒過後,GCD釋放,從而person物件也釋放。
block作為GCD引數的時候,會將block複製到堆上,而裡面又引用了person區域性變數的物件,但是,前面是__weak修飾的,因此會對block裡面的person物件變數進行類似弱引用功能。因此,在{}執行完畢後,person就被銷燬。
block作為GCD引數的時候,會將block複製到堆上,第二次使用block做GCD引數,會將block的引用做類似+1操作。第二個block裡面引用了person區域性變數的物件,因此會對第二個block裡面的person物件變數進行類似強引用功能。因此,在第二個block執行完畢後,person才被銷燬。
block作為GCD引數的時候,會將block複製到堆上,第二次使用block做GCD引數,會將block的引用做類似+1操作。第二個block裡面引用了person區域性變數的物件,但是,前面是__weak修飾的,因此會對block裡面的person物件變數進行類似弱引用功能。因此,在{}執行完畢後,person就被銷燬。
block作為GCD引數的時候,會將block複製到堆上。block裡面引用了person區域性變數的物件,因此會對第一個block裡面的person物件變數進行類似強引用功能。
第二次使用block做GCD引數,會將block的引用做類似+1操作。第二個block裡面引用了person區域性變數的物件,但是是__weak型別的,因此會對第二個block裡面的person物件變數進行類似弱引用功能。
因此,在第一個block執行完畢後,person就被銷燬。
block作為GCD引數的時候,會將block複製到堆上。block裡面引用了person區域性變數的物件,但是是__weak型別的,因此會對第一個block裡面的person物件變數進行類似弱引用功能。
第二次使用block做GCD引數,會將block的引用做類似+1操作。第二個block裡面引用了person區域性變數的物件,因此會對第二個block裡面的person物件變數進行類似強引用功能。
因此,在第二個block執行完畢後,person才銷燬。一個簡單的栗子:
問:下面的列印結果是什麼?
int age = 10;
YZBlock block = ^{
NSLog(@"---%d---", age);
};
age = 20;
block();
列印結果是:---10---
原因很簡單,是因為block將變數age捕獲到block內部,並且由於是auto變數,捕獲的是值。
雖然age=20,但是在編譯的時候,block已經將age=10的值捕獲進去。因此,列印的是10。
問:為何不能直接在block內部修改外部區域性變數呢?
int age是在main函式裡面建立的;
block內部的age是定義在__main_block_func_0裡面的;
不能通過修改__main_block_func_0裡面的age從而去反向改變main裡面的age值。
另外,捕獲其實是新建一個同名變數,因此,block裡面的age是一個新建的age,其值是10。
從下面的例子可以看出,block內部的變數跟block外部的變數,不是同一個變數。
可以看出:
16行、22行的age地址相同,也就是block外部的age是同一個
19行、20行的age地址相同,也就是block內部的age是同一個
而19-20行的age地址跟16、22行的age地址不同,說明block內部的age變數與block外部的age變數不是同一個。
既然block內外age變數不是同一個,就不能通過修改block內部的age變數,去修改block外部的age變數。
至於為什麼內部的age變數也不能修改,是因為 block內部的捕獲新建是隱式的,在外部看來並沒有新建一個age,block內外的age就是同一個age。為了避免使用者想去通過修改block內部的age而去修改外部的age值,蘋果直接將block內部的age做了限制,只能使用,禁止賦值。
類似的有區域性變數NSMuttableArray *array,在block內部只能使用,不能賦值。
也就是,只能做[array addObject:];等操作
不能做,array = nil;或者 array = [NSMuttableArray array];等操作
使用static修飾的區域性變數就可以進行修改。
這是因為,使用static修飾的區域性變數,block內部捕獲的是指標,因此,可以通過指標修改外部age的值,這我們前面講過。
當然,全域性變數是可以修改的,這個就不用說了。
現在我們還可以通過另外一種方法,進行修改外面佈局變數的值,這就是__block。
問:為什麼使用__block修飾區域性變數就可以修改age的值呢?
__block只能修飾auto區域性變數,不能修飾 全域性變數 和 static修飾的靜態變數。
通過底層原始碼可以看到:
首先,我們看下__block int age = 10;
轉換為:
__attribute__((__blocks__(byref))) __Block_byref_age_0 age =
{
(void*)0,
(__Block_byref_age_0 *)&age,
0,
sizeof(__Block_byref_age_0),
10
};
簡化後
__Block_byref_age_0 age = {
0,
&age,
0,
sizeof(__Block_byref_age_0),
10
};
可以看出age最後被轉換為__Block_byref_age_0型別的age結構體。那麼__Block_byref_age_0這是個什麼型別的東西呢?
從原始碼可以看出__Block_byref_age_0的定義是
struct __Block_byref_age_0 {
void *__isa;//isa指標,代表該型別是一個物件
__Block_byref_age_0 *__forwarding;//接收自己的地址
int __flags;
int __size;//改型別值的大小
int age;//__block修飾的變數age10
};
那麼兩個結合到一起,可以看到那些值分別代表的意義。
YZBlock block = ^{
age = 30;
NSLog(@"---%d---", age);
};
被轉換為:
YZBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
簡化後
YZBlock block = &__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&age,
570425344);
__main_block_impl_0的定義是
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
可以看出,block裡面並沒有直接捕獲age值,而是新建立了一個__Block_byref_age_0型別的age物件。
通過其建構函式可以看到其賦值過程
__main_block_impl_0(
__main_block_func_0,
&__main_block_desc_0_DATA,
&age,
570425344);
__main_block_impl_0(
void *fp,
struct __main_block_desc_0 *desc,
__Block_byref_age_0 *_age,
int flags=0)
那,裡面的age=30;是如何修改的呢?
首先,block會將裡面的內容封裝到__main_block_func_0函式裡面,然後通過(age.__forwarding->age) = 30;根據__Block_byref_age_0型別的age裡面自己的__forwarding指標,獲取裡面的age,修改為30;
這裡,由於block定義裡面是拿的型別為__Block_byref_age_0變數名為age的指標,由於是指標,因此,我們可以拿到裡面的值,也可以修改裡面的值。
問:下面的程式碼為何會報錯?
static修飾變數的時候報錯,錯誤提示是:
Initializer element is not a compile-time constant
這是因為,被static修飾的區域性變數,會延長生命週期,使得週期與整個應用程式有關; 只會分配一次記憶體;程式一執行,就會分配記憶體,而不是執行到那才分配。
而[[YZPerson alloc] init];是在執行到此處的時候才會分配記憶體。會有衝突,因此,不能這麼寫。
可以看到,使用static修飾的區域性變數,捕獲的是指標YZPerson **person;
上圖表明,person、array等指標型別,由於auto型別,捕獲進去的是值,因此,在block裡面捕獲的是指標。
指標指向的內容可以修改,也就是person.age, [array addObject:@“4”];都可以修改。
但是,person和array指標本身是不可以修改的。因此,person = nil; array = nil;是不可以執行的。
這個試驗引出了一個常問的面試題:
Block與陣列的關係
從上面的結果來看:
auto型別的array,在block內部可以進行新增刪除元素操作,但不可以進行array = nil;操作
static型別的array,捕獲的是指標,也可以進行新增刪除元素操作,可以進行array = nil;操作
__block,也是對指標操作,也可以進行新增刪除元素操作,可以進行array = nil;操作