block 源碼分析 底層原理

block底層原理是什么?

封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC對象

block

將main.m文件轉(zhuǎn)換成C++文件,當前文件夾下

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp

通過分析main.cpp 我們可以看到編譯后的block 。

block編譯

我們可以看出block進過編譯后生成一個__main_block_impl_0 的結(jié)構(gòu)體,內(nèi)部有一個__block_impl結(jié)構(gòu)體變量,而且__block_impl內(nèi)部有一個isa指針,說明block其實也是屬于OC對象的,而且我們看到block內(nèi)部使用的age變量,也存在于block內(nèi)部,說明當block內(nèi)部使用變量的時候,會將變量也傳入block內(nèi)部進行使用。下面我們就具體來分析下block內(nèi)部的本質(zhì)。

block的調(diào)用流程:

通過編譯文件我們可以看到幾個結(jié)構(gòu)體

__block_impl  :isa指針、FuncPtr 指針:指向的block需要執(zhí)行的代碼地址
__main_block_impl_0 :結(jié)構(gòu)體內(nèi)部存在一個__main_block_impl_0函數(shù),這是屬于C++的構(gòu)造器函數(shù),返回是當前的一個結(jié)構(gòu)體,相當于OC里面的init函數(shù)
__main_block_func_0 : 執(zhí)行block內(nèi)部的需要執(zhí)行的代碼
__main_block_desc_0 : block的描述信,第一個參數(shù)是0,第二個是block的 sizeof 內(nèi)存大小。

block定義

void(*block)(void) = ((void(*)())&__main_block_impl_0((void*)__main_block_func_0, &__main_block_desc_0_DATA, age));
//去除 強制轉(zhuǎn)換類型代碼 偽代碼 :
void(*block)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age))
//我們可以看出 block內(nèi)部 是通過調(diào)用__main_block_impl_0函數(shù) 返回來的結(jié)構(gòu)體,并取其地址,__main_block_func_0 和 &__main_block_desc_0_DATA, age是傳遞的三個參數(shù)

block對變量的捕獲(可以根據(jù)上述方法,查看編譯文件)

類型 是否捕獲 原因
局部變量 出了作用域會銷毀,需要捕獲保留,值傳遞
局部(全局)常量static static創(chuàng)建在程序退出之前始終存在內(nèi)存中,所以采用指針傳遞
全局變量 在當前作用域是不會銷毀的,即使銷毀了,block也會一起銷毀,所以不需要捕獲
self 每個函數(shù)都有隱式參數(shù)(Class *self,SEL _cmd)所以self屬于局部變量,需要捕獲
成員變量 成員變量的本質(zhì)就是 self->name訪問,所以也是需要捕獲self變量來進行訪問,注意捕獲的是self,而并非是_name
函數(shù)調(diào)用 A函數(shù)調(diào)用B函數(shù) ([self b])會進行消息轉(zhuǎn)發(fā)機制 objc_msgSend(self,SEL b,參數(shù)),所以也是捕獲的self
int global = 10;
int main(int argc, const char * argv[]) {
@autoreleasepool {
int age = 10;
static int height = 10;
void (^block)(void) = ^{
NSLog(@"age :----%d",age);
NSLog(@"height :----%d",height);
NSLog(@"global :----%d",global);
};
age = 20;
height =20;
global = 20;
block();
}
return 0;}

#interface Person :NSobject
@property (nomotic,assign)int age;
#end
#implentation Person
- (void)test {      --》隱式參數(shù)- (void)test:(Person * self,SEL _cmd)
}
#end
//從上一部分我們可以看出,block內(nèi)部存在外部的變量,block內(nèi)部會創(chuàng)建相應的變量來接受外部變量,此時block內(nèi)部的變量已經(jīng)不是外部變量了
  區(qū)別是,
  auto屬性(默認屬性) 屬于值傳遞 所以輸出是10
  static 屬性是指針傳遞 輸出是20
  全局變量 不會捕獲到block內(nèi)部,直接調(diào)用 輸出是20
  局部變量因為作用域問題,aoto局部變量出了作用域會自動銷毀,所以block需要及時捕獲值
  static局部變量 是一直儲存在內(nèi)存中的,所以采用指針訪問。
  全局變量,可以直接訪問,所以不需要捕獲也能訪問。
  self是否會捕獲? 隱式參數(shù)(每個函數(shù)都會有2個默認參數(shù) 就是當前 調(diào)用者self,SEL _cmd(方法名)),所以self是屬于局部變量,所以會捕獲
  成員變量(_name),本質(zhì)是調(diào)用self->name   所以也會捕獲.

block分類

block分類以及繼承關(guān)系

不同的block分布在內(nèi)存中的位置不同

類型 內(nèi)存中位置 特點
NSGlobalBlock data段 沒有訪問auto變量,跟全局變量在一塊,由系統(tǒng)管理
NSMallocBlock 需要手動釋放,NSStackBlock 調(diào)用copy生成
NSStackBlock 訪問了auto變量, 系統(tǒng)管理釋放,超過作用域就釋放

block-copy 操作

在ARC環(huán)境下,系統(tǒng)默認會對block進行copy操作的幾種情況:
1.block作為函數(shù)的返回值的時候。
2.將block賦值給__strong指針時。
3.block作為cocoa API中方法名含有usingBlock的方法參數(shù)時。
4.block作為GCD API的方法參數(shù)時。
copy內(nèi)部原理:當block從棧copy到堆上之后,如果存在__block、__weak、__strong修飾的對象,在__main_block_desc_0函數(shù)內(nèi)部會增加copy跟dispose函數(shù),copy函數(shù)內(nèi)部會根據(jù)修飾類型對對象進行強引用還是弱引用,當block釋放之后會進行dispose函數(shù),release掉修飾對象的引用,如果都沒有引用對象,將對象釋放

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};
block-copy操作

block 對象類型的auto變量捕獲

1.如果block在棧上,將不會對auto變量產(chǎn)生強引用。
2.如果block被copy到堆上,會調(diào)用block內(nèi)部的copy函數(shù),copy內(nèi)部函數(shù)會調(diào)用_Block_object_assign 函數(shù),_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong, __weak,__unsafe_unretaineaod)做出相應的操作,類似于tain(形成強引用,弱引用)


對象類型的auto變量

block引用對象類型的auto變量的時候,ARC會對當前對象進行內(nèi)存管理操作,如果用__weak修飾的對象,不會增加其引用計數(shù),出了作用域?qū)ο缶蜁会尫牛斢胈_strong修飾對象,會增加其引用計數(shù),block執(zhí)行之后會進行一次release操作。

__block 詳解

我們知道 __block的修飾變量之后是就可以修改其值了,但是原理是什么呢?我們先看下代碼

typedef void(^JWBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        JWBlock block = ^{
            age = 20;
            NSLog(@"ageage---------%d",age);
        };
        block();
        
    }
    return 0;
}

我們轉(zhuǎn)換成 C++代碼之后(xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc main.m)


block修改值過程

__block修飾對象類型

總結(jié):__block修飾的auto變量,編譯器會包裝成一個__Block_byref_age_0(根據(jù)變量名可變)的對象類型的結(jié)構(gòu)體,將指向自己的指針傳遞給__forwarding指針(這樣做的目的是為了當多個block都使用__block修飾的變量的時候,能夠始終指向堆中的變量),__Block_byref_age_0結(jié)構(gòu)體內(nèi)部存在的變量age才是真正__block修飾的變量,通過__Block_byref_age_0 -->__forwarding-->age改變變量的值。如果變量是NSObject對象,還會處理內(nèi)存管理的問題,如上圖,對象類型會生成__Block_byref_id_object_copy 跟__Block_byref_id_object_dispose這兩個函數(shù),這兩個函數(shù)會對當前對象進行內(nèi)存內(nèi)存管理工作,下面會細講這兩個函數(shù)的作用

block循環(huán)引用問題

Person * person = [[Person alloc]init];
        person.block = ^{
            NSLog(@"%d",person.age);
        };

原因:block內(nèi)部用到了外部的auto對象,block內(nèi)部實現(xiàn)會對person進行強引用,person的block成員變量也會對block進行強引用,當person超出作用域之后,被回收,但是此時block強引用著Person,Person強引用著block 導致無法釋放,造成循環(huán)引用,內(nèi)存泄漏。

循環(huán)引用問題

一般我們希望block跟person的周期是一致的,所以最好將block內(nèi)部引用person的指針換成__weak弱引用是最好的。這樣就不會造成互相引用,導致內(nèi)存無法釋放

      Person * person = [[Person alloc]init];
     __weak Person * weakPerson = person;
//__weak typeof(person) weakPerson = person;
//typeof作用是保持person 跟weakPerson是相同類型的。
//也可以用__unsafe_unretained 來修飾
        person.block = ^{
            NSLog(@"%d",weakPerson.age);
        };

區(qū)別:__weak :當指向的指針沒有強指針指向的時候,會將當前對象置為nil,__unsafe_unretained:當指向的指針沒有強指針指向的時候,會將當前對象內(nèi)存地址不變,容易造成野指針,訪問錯誤的情況,所以不常用。
__block : 也可以解決循環(huán)引用的問題,但是使用__block時候必須執(zhí)行block,并且在block內(nèi)部將對象置為nil。

面試題

  • block本質(zhì)是什么?
    封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC對象
  • __block的作用是什么?
    1.如果__block在棧上,將不會對指向的對象產(chǎn)生強引用。
    2.如果__block被copy到堆上,會調(diào)用block內(nèi)部的copy函數(shù),copy內(nèi)部函數(shù)會調(diào)用_Block_object_assign 函數(shù),_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong, __weak,__unsafe_unretaineaod)做出相應的操作,類似于retain(形成強引用,弱引用)(這里只是針對ARC時會retain,在MRC下不會進行retain操作
    3.如果變量從堆中移除,會調(diào)用block內(nèi)部的dispose函數(shù),dispose內(nèi)部會調(diào)用_Block_object_dispose函數(shù)會自動釋放其指向的函數(shù)
  • block使用修飾詞為什么用copy,注意的細節(jié)
    block如果沒有進行copy操作,就不會在堆上,無法控制block的生命周期,違背了block得初衷。
    應避免循環(huán)引用的問題
  • block在修改NSMutableArray的時候,需要增加__block么?
    不需要,修改可變數(shù)組內(nèi)容,只是對其內(nèi)容的操作,并沒有對指針方面的修改,是對數(shù)組的使用并沒有重新賦值操作。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容