iOS底層-Block底層原理

Block函數(shù)有三種:

第一種:全局block

void (^block)(void) = ^{
        NSLog(@"block!");
};
NSLog(@"%@",block);

打印結(jié)果:<__NSGlobalBlock__: 0x10d94f088>

第二種:堆區(qū)block

int a = 10;
void (^block)(void) = ^{
        NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
打印結(jié)果:<__NSMallocBlock__: 0x6000020eb0c0>

第三種:棧區(qū)block,棧區(qū)block在iOS14后,越來越少,因此需要使用__weak使其不在強持有。

int a = 10;
void (^__weak block)(void) = ^{
        NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
<__NSStackBlock__: 0x7ffeeba41478>

全局訪問外界變量強引用變成堆區(qū),弱引用變成棧區(qū)。

既然是block,那就存在循環(huán)引用問題,那就先要了解循環(huán)引用的概念,按照正常的流程來說,例如A持有B,B的引用計數(shù)加1,而當A發(fā)送dealloc信號之后,B的引用計數(shù)需要減1變?yōu)?,那么dealloc才會正常被調(diào)用;而循環(huán)引用就是A持有B,B也持有A,構(gòu)成了相互持有,那么在釋放的時候,誰也釋放不了對方,就造成了循環(huán)引用問題。

那么如何解決循環(huán)引用問題呢?

來看一段代碼:

typedef void(^WXBlock)(void);
@interface ViewController ()
@property (nonatomic, copy) WXBlock block;
@property (nonatomic, copy) NSString *name;


@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 循環(huán)引用
    self.name = @"Block";
    
    self.block = ^(void) {
        NSLog(@"%@",self.name);
    };
    self.block();
}

在上面一段代碼中,肯定是會造成循環(huán)引用的,因為self引用了block,而bloc也引用了self;類似于self -> block ->self;

那么解決循環(huán)引用,相信很多人都知道是用__weak;它加入了一張弱引用表,增加__weak typeof(self) weakSelf = self;這一行實現(xiàn)弱引用,就類似于self -> block ->weakSelf -> self;

那么weakSelf持有強引用對象self,引用計數(shù)是不會增加的,因此weakSelf持有的self在weakSelf生命周期結(jié)束之后,也就進行釋放了。
下面是執(zhí)行的結(jié)果:

iShot2020-11-15 11.41.51.png

那么這種方式來解決循環(huán)引用是會存在某些問題的,例如修改部分代碼,異步延遲兩秒執(zhí)行:

self.block = ^(void) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",weakSelf.name);
        });
    };

那么在延遲兩秒執(zhí)行后,還沒來得及調(diào)用,self就被釋放了,因此self的生命周期是不足以得到保證的。

iShot2020-11-15 11.42.18.png

那么我們又可以在block函數(shù)內(nèi)部對weakSelf進行強引用,就可以解決這個問題。
增加代碼__strong typeof(self) strongSelf = weakSelf;
打印結(jié)果為:

iShot2020-11-15 12.02.53.png

這樣的強引用對象是在block函數(shù)調(diào)用結(jié)束之后,就會進行釋放。
那么使用__weak解決循環(huán)引用就需要weakstrong結(jié)合使用。
完整代碼:

__weak typeof(self) weakSelf  = self;
    self.block = ^(void) {
        __strong typeof(self) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",strongSelf.name);
        });
        
    };
    self.block();

__weak只是解決循環(huán)引用的方式之一,他是自動釋放,下面介紹第二種解決方式,手動釋放,看代碼:

__block ViewController *vc = self;
    self.block = ^(void) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
            vc = nil;
        });
        
    };
    self.block();

使用__block對ViewController賦值self,通過在輸出之后,手動將vc置為nil。類似于self->block-> vc=nil ->self;vc被block捕獲,無法自動釋放,那么手動釋放,就解決了釋放這一問題。

接下來介紹第三種解決循環(huán)引用問題,那就是通過參數(shù)來解決問題:
看代碼

typedef void(^WXBlock)(ViewController *);

self.block = ^(ViewController *vc) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@",vc.name);
        });
    };
    self.block(self);

在了解了block的使用之后,下面來看一下block的底層原理,首先通過xcrun來看一下block的cpp是如何實現(xiàn)的:

#include "stdio.h"
int main(){
    void(^block)(void) = ^{
        printf("Block - ");
    };
     block();
    return 0;
}

上面的c代碼通過xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c轉(zhuǎn)換為:


int main(){
    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;
}

__main_block_impl_0是一個結(jié)構(gòu)體:

iShot2020-11-15 12.22.38.png

iShot2020-11-15 12.31.06.png

也就是說,block的本質(zhì)就是對象結(jié)構(gòu)體,以函數(shù)作為參數(shù)傳入進來;
impl.FuncPtr = fp;可以知道block是需要具體函數(shù)實現(xiàn)的;
*__cself作為匿名參數(shù),因此可以獲取block內(nèi)部的代碼,并執(zhí)行。

那么如果有外界參數(shù)時,block又是如何實現(xiàn)的呢?
通過轉(zhuǎn)換之后得到了下面的代碼:

 int a = 11;
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
iShot2020-11-15 12.40.52.png

可以看到,a的值在編譯時,就自動生成了相應的變量,而在__main_block_func_0的方法中,就通過了一種賦值拷貝的方式賦值給a,但是里面的a和外面的a是不一樣的。

那么對里面的a進行加加,在轉(zhuǎn)換后為:

__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
    void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));

     ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    return 0;
iShot2020-11-15 12.48.32.png

可以看到a是進行了指針拷貝,也就是說兩個變量a同時指向同一片內(nèi)存空間。

總結(jié):
block本質(zhì)一個對象結(jié)構(gòu)體,匿名函數(shù),block自動捕獲外界變量生成同一個屬性來保存,block調(diào)用block()是因為函數(shù)申明,需要有具體的函數(shù)實現(xiàn);而__block的原理就是生成相應的結(jié)構(gòu)體,保存原始變量進行指針拷貝,傳遞指針地址給block。

那到現(xiàn)在為止,其實還沒有探索到一些核心的底層原理,還不清楚,__NSGlobalBlock__,__NSStackBlock____NSMallocBlock__在內(nèi)存地址中是如何變化的,以及關于block調(diào)用的問題。
下面我們探索一下運行時的block。

首先創(chuàng)建工程,代碼很簡單:

iShot2020-11-16 15.53.53.png

利用真機進行調(diào)試,打開匯編模式;
就出現(xiàn)了如下圖所示的代碼,這邊它執(zhí)行了一個objc_retainBlock的跳轉(zhuǎn);

iShot2020-11-16 15.59.43.png

接下來我們手動添加objc_retainBlock的斷點:

iShot2020-11-16 16.05.11.png

執(zhí)行下一步之后,就會進入到objc_retainBlock的匯編,按住control+step into,就進入到了一個_Block_copy的匯編當中:

iShot2020-11-16 16.06.47.png

到這里,就可以清楚的知道block所在的動態(tài)庫在libsystem_blocks.dylib,因此可以在蘋果官網(wǎng)下載所需要的源碼。

在上面試過轉(zhuǎn)化的cpp文件存在Block_layout,在libsystem_blocks.dylib就有這個結(jié)構(gòu),它是一個結(jié)構(gòu)體,里面還有一個isa,block在底層真正的類型就是Block_layout,在源碼中,很多方法的參數(shù)都有Block_layout

iShot2020-11-16 16.09.39.png

下面來研究一下block的全局,堆區(qū)和棧區(qū)地址的變化,將除了26行的斷點留下,其他斷點去掉,重新執(zhí)行程序,通過控制臺讀取寄存器信息:
下圖是__NSGlobalBlock__內(nèi)存信息:

iShot2020-11-16 16.19.39.png

下面嘗試一下捕獲外界變量,聲明一個a,在block中打印出來,重新執(zhí)行程序:

在執(zhí)行程序之后,它并沒有跳轉(zhuǎn)到objc_retainBlock中來,打印的x0信息不對,在objc_retainBlock出打下斷點,這時候讀x0信息,就是棧區(qū)block了,__NSStackBlock__

iShot2020-11-16 16.22.54.png

__NSMallocBlock__是從__NSStackBlock__拷貝過去的,那么意味著預編譯的時候是__NSStackBlock__,然后讓block copy操作,當進入了_Block_copy匯編代碼中,在最后一行有一個ret的返回操作,在此處打斷點:
如下圖所示,在經(jīng)過_Block_copy返回之后,block的內(nèi)存地址發(fā)生了變化,從0x000000016f837728變到0x0000000282e036c0,而__NSStackBlock__也變成了__NSMallocBlock__。

iShot2020-11-16 16.32.40.png

下圖是_Block_copy的底層源碼實現(xiàn),內(nèi)部實現(xiàn)了為什么從__NSStackBlock__轉(zhuǎn)換成__NSMallocBlock__

iShot2020-11-16 17.16.00.png

總結(jié):在block捕獲外界變量時,會從__NSStackBlock__經(jīng)過_Block_copy處理變成__NSMallocBlock__。

下面來看一下block的簽名,在block_layout的結(jié)構(gòu)體當中,有很多屬性,其中就存在Block_descriptor_1類型的descriptor,而Block_descriptor_2Block_descriptor_3都是可選類型,表示不是所有block都存在它們的一些屬性;

iShot2020-11-16 16.48.36.png

而在它們是如何辨別是否需要屬性呢?


iShot2020-11-16 17.02.22.png

看上圖,主要是通過枚舉值類型和進行地址平移來獲得所需要的屬性:
看下圖的Block_descriptor的源碼實現(xiàn):


iShot2020-11-16 16.49.48.png

那現(xiàn)在去獲取block的簽名:

執(zhí)行程序,將程序卡在_Block_copy執(zhí)行完之后,讀取寄存器x0的信息:
最終獲取的__NSMallocBlock__的地址是0x0000000281e7c4e0,而在查看block_layout結(jié)構(gòu)之后,通過x/4gx獲取它的信息,其中第一個是isa的值,而第4個就是descriptor

iShot2020-11-16 16.55.43.png

那我們清楚,descriptor的類型有1,2,3,其中2和3都是可選類型的,并不清楚它們是否存在,因此需要一個一個去嘗試,首先打印第四個地址的內(nèi)存情況,通過上面給的枚舉值屬性左移的位數(shù),來查看地址是否有值,經(jīng)過一翻查詢,Block_descriptor_2是沒有的,而Block_descriptor_3就存在值,在打印第三個地址之后,得到了它的簽名:

iShot2020-11-16 17.06.42.png

打印簽名信息:

iShot2020-11-16 17.11.34.png
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容