block底層原理是什么?
封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC對象
將main.m文件轉(zhuǎn)換成C++文件,當前文件夾下
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
通過分析main.cpp 我們可以看到編譯后的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分布在內(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 對象類型的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(形成強引用,弱引用)
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)
總結(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)存泄漏。
一般我們希望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ù)組的使用并沒有重新賦值操作。