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后,越來(lái)越少,因此需要使用__weak使其不在強(qiáng)持有。

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

全局訪問(wèn)外界變量強(qiáng)引用變成堆區(qū),弱引用變成棧區(qū)。

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

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

來(lái)看一段代碼:

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ì)造成循環(huán)引用的,因?yàn)閟elf引用了block,而bloc也引用了self;類似于self -> block ->self;

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

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

iShot2020-11-15 11.41.51.png

那么這種方式來(lái)解決循環(huán)引用是會(huì)存在某些問(wè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í)行后,還沒來(lái)得及調(diào)用,self就被釋放了,因此self的生命周期是不足以得到保證的。

iShot2020-11-15 11.42.18.png

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

iShot2020-11-15 12.02.53.png

這樣的強(qiáng)引用對(duì)象是在block函數(shù)調(diào)用結(jié)束之后,就會(huì)進(jìn)行釋放。
那么使用__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)引用的方式之一,他是自動(dòng)釋放,下面介紹第二種解決方式,手動(dòng)釋放,看代碼:

__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對(duì)ViewController賦值self,通過(guò)在輸出之后,手動(dòng)將vc置為nil。類似于self->block-> vc=nil ->self;vc被block捕獲,無(wú)法自動(dòng)釋放,那么手動(dòng)釋放,就解決了釋放這一問(wèn)題。

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

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的使用之后,下面來(lái)看一下block的底層原理,首先通過(guò)xcrun來(lái)看一下block的cpp是如何實(shí)現(xiàn)的:

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

上面的c代碼通過(guò)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是一個(gè)結(jié)構(gòu)體:

iShot2020-11-15 12.22.38.png

iShot2020-11-15 12.31.06.png

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

那么如果有外界參數(shù)時(shí),block又是如何實(shí)現(xiàn)的呢?
通過(guò)轉(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的值在編譯時(shí),就自動(dòng)生成了相應(yīng)的變量,而在__main_block_func_0的方法中,就通過(guò)了一種賦值拷貝的方式賦值給a,但是里面的a和外面的a是不一樣的。

那么對(duì)里面的a進(jìn)行加加,在轉(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是進(jìn)行了指針拷貝,也就是說(shuō)兩個(gè)變量a同時(shí)指向同一片內(nèi)存空間。

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

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

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

iShot2020-11-16 15.53.53.png

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

iShot2020-11-16 15.59.43.png

接下來(lái)我們手動(dòng)添加objc_retainBlock的斷點(diǎn):

iShot2020-11-16 16.05.11.png

執(zhí)行下一步之后,就會(huì)進(jìn)入到objc_retainBlock的匯編,按住control+step into,就進(jìn)入到了一個(gè)_Block_copy的匯編當(dāng)中:

iShot2020-11-16 16.06.47.png

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

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

iShot2020-11-16 16.09.39.png

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

iShot2020-11-16 16.19.39.png

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

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

iShot2020-11-16 16.22.54.png

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

iShot2020-11-16 16.32.40.png

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

iShot2020-11-16 17.16.00.png

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

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

iShot2020-11-16 16.48.36.png

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


iShot2020-11-16 17.02.22.png

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


iShot2020-11-16 16.49.48.png

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

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

iShot2020-11-16 16.55.43.png

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

iShot2020-11-16 17.06.42.png

打印簽名信息:

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

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

  • iOS 底層原理 文章匯總[http://www.itdecent.cn/p/412b20d9a0f6] 本文主...
    Style_月月閱讀 6,488評(píng)論 5 26
  • 前言 block的類型 從一段代碼開始 看下打印信息: 首先我們可以看到,block有3中類型,分別為:NSGlo...
    澤澤伐木類閱讀 452評(píng)論 0 0
  • 手動(dòng)目錄循環(huán)引用block的類Block的相關(guān)信息block本質(zhì)block如何捕獲外界變量?__block修飾的本...
    Engandend閱讀 473評(píng)論 0 1
  • 1.Block的類型 全局Block(NSGlobalBlock) block 內(nèi)部沒有引用外部變量的 Block...
    MonKey_Money閱讀 439評(píng)論 0 4
  • 眾所周知,block可以封裝一個(gè)匿名函數(shù)為對(duì)象,并捕獲上下文所需的數(shù)據(jù),并傳給目標(biāo)對(duì)象在適當(dāng)?shù)臅r(shí)候回調(diào)。正因?yàn)閷⒊?..
    吸血鬼de晚餐閱讀 3,011評(píng)論 0 1

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