iOS底層原理-Block

Block

Block定義及本質(zhì)

block本質(zhì)上也是一個(gè)OC對象,它內(nèi)部有個(gè)isa指針(有isa指針就可以認(rèn)為是OC對象)
block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對象

函數(shù)調(diào)用環(huán)境:函數(shù)調(diào)用所需要的參數(shù)及外部參數(shù)等

//block表達(dá)式:
 ^ 返回值類型 (參數(shù)列表) {表達(dá)式}
 ^ int (int count) {
        return count + 1;
    };

//聲明Block類型變量語法:
返回值類型 (^變量名)(參數(shù)列表) = Block表達(dá)式

int age = 20;
void (^Block)(void) = ^{
    NSLog(@"age = %d",age);
}

Block的內(nèi)部結(jié)構(gòu)

//以上block的內(nèi)部結(jié)構(gòu)
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
};

//impl內(nèi)部結(jié)構(gòu)
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
}

//Desc內(nèi)部結(jié)構(gòu)
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
}

通過將oc代碼轉(zhuǎn)換為c++代碼可以看出


int main(int argc, const char * argv[]) {
    // 定義block變量
    // 調(diào)用__main_block_impl_0函數(shù),返回結(jié)構(gòu)體變量地址
    void (*block)(void) = &__main_block_impl_0(
                                               __main_block_func_0,
                                               &__main_block_desc_0_DATA
                                               );
    
    // 執(zhí)行block內(nèi)部的代碼
    // 此時(shí),若block有參數(shù)時(shí),會(huì)將參數(shù)一起傳入
    block->FuncPtr(block);
    //之所以block能直接調(diào)用FuncPtr是因?yàn)榇鎯?chǔ)FuncPtr的impl結(jié)構(gòu)體位于結(jié)構(gòu)體的第一位,所以其地址與結(jié)構(gòu)體的地址是一樣的
    //從另一方面看,由于impl直接是結(jié)構(gòu)體對象,相當(dāng)于可以直接將__block_impl結(jié)構(gòu)體的東西賦值過去到__main_block_impl_0中,故從這方面看也是可以直接調(diào)用的
}


struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    // 構(gòu)造函數(shù)(類似于OC的init方法),返回結(jié)構(gòu)體對象
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// 封裝了block執(zhí)行邏輯的函數(shù)
// 若block中含有參數(shù),則參數(shù)會(huì)在這個(gè)函數(shù)中被當(dāng)做參數(shù)傳遞進(jìn)來
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    //定義block時(shí),block內(nèi)部執(zhí)行的函數(shù)
}

Snip20180716_11.png

Block的變量捕獲

當(dāng)在block內(nèi)部使用外部局部變量時(shí),block的結(jié)構(gòu)體__main_block_impl_0中也會(huì)定義一個(gè)同名的成員變量,并在構(gòu)造函數(shù)中,將外部局部變量的值賦值給內(nèi)部的變量,在block定義中,若有使用到局部變量,會(huì)將外部的局部變量的值存儲(chǔ)到結(jié)構(gòu)體內(nèi)部的成員變量中,所以外邊的變量怎么改變,都不會(huì)修改函數(shù)定義中的變量值

變量捕獲 : 專門新建一個(gè)成員變量來保存外部的變量,稱為捕獲
auto變量 : 自動(dòng)變量,離開作用域就銷毀

Snip20180716_8.png
  • 當(dāng)使用static修飾的局部變量,在變量捕獲時(shí),會(huì)將變量的地址值傳入,在結(jié)構(gòu)體內(nèi)部定義的,就是一個(gè)對應(yīng)的指針類型

  • auto變量值傳遞,static變量指針傳遞是因?yàn)閍uto變量什么時(shí)候被釋放是不確定的,而static修飾的局部變量會(huì)始終在內(nèi)存中

  • 全局變量并沒有捕獲,是因?yàn)楹瘮?shù)在哪里都是可以直接使用全局變量的,故不需要捕獲,而局部變量需要捕獲,是因?yàn)榫植孔兞慷x是在一個(gè)函數(shù),使用又是在另一個(gè)函數(shù),是跨函數(shù)使用的,故需要捕獲

注:所有的方法都會(huì)附帶兩個(gè)參數(shù),一個(gè)是self(方法調(diào)用者),一個(gè)是SEL _cmd(方法名),而參數(shù)都是局部變量,故在方法中的block使用self,也是會(huì)捕獲的

  • 今后凡是涉及到會(huì)不會(huì)捕獲,只需要判斷是否為局部變量即可

  • 在方法中的block直接使用成員變量,即_name時(shí),也是捕獲方法調(diào)用者self,再通過self去取_name的值

Block的類型

block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型

  • __NSGlobalBlock__(_NSConcreteGlobalBlock)
  • __NSStackBlock__(_NSConcreteStackBlock)
  • __NSMallocBlock__(_NSConcreteMallocBlock)

block的類型一切以運(yùn)行時(shí)的結(jié)果為準(zhǔn)
同時(shí)通過clang轉(zhuǎn)成的代碼(C++)并不是真正的底層代碼
clang是屬于LLVM中的一部分

Snip20180716_12.png

堆:動(dòng)態(tài)分配內(nèi)存,需要程序員申請,也需要程序員自己管理內(nèi)存

Snip20180716_15.png
  • 只要沒有訪問auto變量就是global類型,哪怕有訪問static變量或者局部變量
  • 在棧上的block由于處于棧區(qū),受作用域影響,在作用域之外,有可能被銷毀,導(dǎo)致數(shù)據(jù)錯(cuò)亂
  • globalBlock調(diào)用copy方法,依然為global類型

Block的copy操作

Snip20180716_17.png

在ARC環(huán)境下,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的block復(fù)制到堆上,比如以下情況:

  • block作為函數(shù)的返回值
  • 將block賦值給__strong指針時(shí)
  • block作為cocoaAPI中方法含有usingBlock的方法參數(shù)時(shí)
  • block作為GCD方法的參數(shù)時(shí)

MRC環(huán)境下block屬性建議寫法:使用copy關(guān)鍵字

MRC環(huán)境下若使用retain,只會(huì)令block的引用計(jì)數(shù)+1,并不會(huì)將block復(fù)制到棧區(qū)

ARC環(huán)境下,copy和strong都是可以的,因?yàn)樵贏RC環(huán)境下,對block強(qiáng)引用也會(huì)對block進(jìn)行一次copy操作,但還是建議統(tǒng)一寫copy

Block的訪問外部對象類型auto變量

  • ??臻g上的block,是不會(huì)持有外部對象的
  • 堆空間上的block,則會(huì)持有外部對象,當(dāng)block使用到外部變量時(shí),會(huì)對外部變量進(jìn)行一次retain操作,在block自己銷毀時(shí),也會(huì)對內(nèi)部用到的外部變量做一次release操作

當(dāng)block內(nèi)部訪問了對象類型的auto變量時(shí):

  • 如果block是在棧上:
    無論block內(nèi)部對對象類型的auto變量是強(qiáng)引用還是弱引用,都不會(huì)持有該對象,即不會(huì)對auto變量產(chǎn)生強(qiáng)引用

  • 如果block被拷貝到堆上:
    會(huì)調(diào)用block內(nèi)部的Desc中的copy函數(shù)
    copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù)
    _Block_object_assign函數(shù)會(huì)根據(jù)auto變量的修飾符(__strong,__weak,__unsafe_unretained)做出相應(yīng)的操作,類似于retain(形成強(qiáng)引用、弱引用)

  • 當(dāng)block從堆上移除:
    會(huì)調(diào)用block內(nèi)部的Desc中的dispose函數(shù)
    dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù)
    _Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的auto變量,類似于release

copy函數(shù)和dispose函數(shù)都是做內(nèi)存管理用的,只要見到這兩個(gè)函數(shù),就大概知道與對象有關(guān)

Snip20180717_19.png

當(dāng)訪問的是個(gè)對象類型對象,會(huì)自動(dòng)生成_Block_object_assign函數(shù)和_Block_object_dispose函數(shù)對該對象進(jìn)行內(nèi)存管理

看強(qiáng)引用什么時(shí)候被釋放,則該對象就什么時(shí)候釋放,與弱引用無關(guān)

Block的修改外部auto變量

默認(rèn)情況下,在block中是無法修改外部的變量的
從本質(zhì)上看,定義變量是在一個(gè)函數(shù),而block調(diào)用的函數(shù)又是另一個(gè)函數(shù),二者作用域不同,故肯定是無法修改(代碼)

要想修改外部變量,做法:

  • 將變量變?yōu)閟tatic或者為全局變量
  • __block關(guān)鍵字

__block可以用于解決block內(nèi)部無法修改auto變量值的問題
__block不能修飾全局變量,靜態(tài)變量(static)

編譯器會(huì)將__block包裝成一個(gè)對象,故其也有copy與dispose函數(shù)

//包裝的對象結(jié)構(gòu)體為:
struct __Block_byref_age_0 {
    void *__isa;
    ___Block_byref_age_0 *__forwarding;//該指針指向自己
    int __flags;
    int __size;
    int age;
}

在block中定義了一個(gè)指針,指向上面的這個(gè)結(jié)構(gòu)體,當(dāng)需要修改值時(shí),通過指針找到對應(yīng)的__forwarding,在找到對應(yīng)的age,從而能夠成功地修改和讀取值了

此時(shí)在外邊打印age的地址值,實(shí)際上就是__block_byref_age_0內(nèi)部的age的地址,即實(shí)際上外部的age就是結(jié)構(gòu)體內(nèi)部的age

注意點(diǎn)1:若block外部定義了一個(gè)可變數(shù)組array,在block內(nèi)部使用方法[array addObject:@“111”],是可以的,因?yàn)槠浔举|(zhì)是將array用來使用,而不是直接修改array的指針的東西,例如array = nil,(該語句就會(huì)報(bào)錯(cuò) )
注意點(diǎn)2:若對象類型外部在使用__block修飾,則可以修改對象類型的指針變量,即可以令array = nil,其內(nèi)部也會(huì)生成一個(gè)__Block_byref_XXX的結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部還會(huì)存在一個(gè)copy函數(shù)和dispose函數(shù)(因?yàn)閎lock外部需要管理結(jié)構(gòu)體對象,故需要一個(gè)copy和dispose,而結(jié)構(gòu)體內(nèi)部需要管理真正的對象類型,故還是需要一個(gè)copy和dispose函數(shù))

__block的內(nèi)存管理

  • 當(dāng)block在棧上時(shí),并不會(huì)對__block變量產(chǎn)生強(qiáng)引用
  • 當(dāng)block被copy到堆上的時(shí)候,會(huì)調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會(huì)對__block變量形成強(qiáng)引用(retain)(一般都是強(qiáng)引用)
Snip20180717_22.png

當(dāng)block從棧copy到堆上,會(huì)將內(nèi)部用到的__block變量也一起拷貝到堆上,并對其進(jìn)行強(qiáng)引用,當(dāng)時(shí)__block修飾的對象類型,在__block的變量結(jié)構(gòu)體被拷貝到堆上時(shí),會(huì)調(diào)用結(jié)構(gòu)體內(nèi)部的copy函數(shù)對結(jié)構(gòu)體里邊的對象變量進(jìn)行管理
若另外一個(gè)block進(jìn)行同樣的操作,也使用到一樣的__block變量,則該block拷貝到堆上的同時(shí),對該變量進(jìn)行強(qiáng)引用,由于之前__block已經(jīng)在堆上了,故直接強(qiáng)引用就好了,不需要再次復(fù)制

  • 當(dāng)block從堆中移除時(shí),會(huì)調(diào)用block內(nèi)部的dispose函數(shù),dispose函數(shù)內(nèi)部會(huì)調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會(huì)自動(dòng)釋放引用的__block變量(release)
Snip20180717_23.png

block與對象類型的auto對象變量,__block變量內(nèi)存管理總結(jié)

  • 當(dāng)block在棧上時(shí),對他們都不會(huì)產(chǎn)生強(qiáng)引用
  • 當(dāng)block拷貝到堆上時(shí),都會(huì)通過copy函數(shù)來處理它們
  • 當(dāng)block從堆中移除時(shí),都會(huì)調(diào)用dispose函數(shù)來釋放

使用__block修飾基本數(shù)據(jù)類型和直接傳入OC對象的不同是:
使用__block修飾的對象,在block內(nèi)部都是對其有著強(qiáng)引用的
使用OC對象傳入會(huì)根據(jù)外部是強(qiáng)指針(__strong)還是弱引用(__weak)來決定內(nèi)部對該對象是強(qiáng)引用還是弱引用

注:不存在__block __weak int age這樣的寫法,因?yàn)?code>__weak只能用來修飾普通OC對象

__forwarding指針

  • 當(dāng)__block修飾的變量在棧上時(shí),其__forwarding指針指向自己
  • 當(dāng)__block修飾的變量被拷貝到堆上時(shí),棧上的對象中的__forwarding指針指向堆上的對象,堆上對象的__forwarding指針指向自己
Snip20180717_25.png

這樣的好處是,無論訪問堆上還是棧上的__block對象,都能確保讀取的對象一定是堆中的__block對象

__block修飾的對象類型

  • block內(nèi)部指向被__block包裝的對象結(jié)構(gòu)體的指針一定是強(qiáng)指針,而結(jié)構(gòu)體內(nèi)部的指向外部對象的指針會(huì)根據(jù)外部是強(qiáng)指針還是弱指針,對應(yīng)的是強(qiáng)引用還是弱引用
  • 當(dāng)棧上的block調(diào)用了copy操作,會(huì)調(diào)用block中Desc函數(shù)內(nèi)部的copy函數(shù)將修飾的變量中的結(jié)構(gòu)體也復(fù)制一份到堆上
  • 在MRC環(huán)境中,使用__block修飾的對象變量中結(jié)構(gòu)體指向外部對象變量的指針始終都是弱引用(即不會(huì)進(jìn)行retain操作),只有__block對象才會(huì)這樣.比較特殊

循環(huán)引用

解決方法:

  • ARC:
    1.使用__weak__unsafe_unretained指針
    __weak:不會(huì)產(chǎn)生強(qiáng)引用,當(dāng)指向的對象被銷毀,會(huì)自動(dòng)將指針置為nil
    __unsafe_unretained :不會(huì)產(chǎn)生強(qiáng)引用,不安全,指向的對象銷毀時(shí),指針存儲(chǔ)的地址值不變(有可能產(chǎn)生野指針錯(cuò)誤)
    2.使用__block,在block內(nèi)部將__block修飾的指針置為空,且必須要執(zhí)行block
__block MXPerson *person = [[MXPerson alloc] init];
person.age = 10;
person.block = ^{
    NSLog(@"%d",person.age);
    person = nil;
}

person.block();
  • MRC
    1.因?yàn)镸RC是不支持__weak的,所以在MRC環(huán)境中,可以使用__unsafe_unretained
    2.可以使用__block
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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