1. 程式人生 > 其它 >iOS底層原理探索-Block本質(二)

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;操作