03-iOS- OC中block底層原理

1. block的本質(zhì)

  • block本質(zhì)上也是一個OC對象,它內(nèi)部也有個isa指針。
  • block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境(block函數(shù)的調(diào)用地址、參數(shù)、變量等信息)的OC對象。
  • block的底層結(jié)構(gòu)代碼如下:
    1. 首先在main函數(shù)中申明一個block
//  首先在main函數(shù)中申明一個block
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 20;
        // 申明一個block
        void (^block)(int, int) =  ^(int a , int b){
            NSLog(@"this is a block! -- %d", age);
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
            NSLog(@"this is a block!");
        };
    }
    return 0;
}

2.將main函數(shù)的oc代碼轉(zhuǎn)成C++代碼,具體看下block的底層實現(xiàn)結(jié)構(gòu)如下:

// 將main函數(shù)的oc代碼轉(zhuǎn)成C++代碼,具體看下block的底層實現(xiàn)結(jié)構(gòu)如下:
// oc中申明的block代碼底層實現(xiàn)是一個__main_block_impl_0的結(jié)構(gòu)體
struct __main_block_impl_0 {
  // impl:是__block_impl類型的結(jié)構(gòu)體,其內(nèi)部有個isa指針,所以block的本質(zhì)是一個oc對象。
  struct __block_impl impl;
  // Desc:是__main_block_desc_0類型的結(jié)構(gòu)體。
  struct __main_block_desc_0* Desc;
  // age:是main函數(shù)中申明的局部變量age
  int age;
  // c++的構(gòu)造方法,類似于oc的init構(gòu)造方法
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// impl的結(jié)構(gòu)體內(nèi)部實現(xiàn):
struct __block_impl {
  void *isa;
  // 默認為0
  int Flags;
  int Reserved;
  // FuncPtr:block內(nèi)部函數(shù)執(zhí)行地址
  void *FuncPtr;
};
// Desc的結(jié)構(gòu)體內(nèi)部實現(xiàn):
static struct __main_block_desc_0 {
  size_t reserved;
  // Block_size:block的內(nèi)存空間大小
  size_t Block_size;
}
  • block的底層結(jié)構(gòu)如右圖所示:
    底層結(jié)構(gòu).png

2. block的變量捕獲(capture)

為了保證block內(nèi)部能夠正常訪問外部的變量,block有個變量捕獲機制。判斷會不會被捕獲的標(biāo)準(zhǔn)是:如果是全局變量不會捕獲,如果是局部變量則會捕獲。
捕獲機制.png

代碼演示如下:

  // auto:自動變量,離開作用域就銷毀(平時申明的變量前面默認auto類型,auto是省略了)
        auto int age = 10;
        static int height = 10;
        void (^block)(void) = ^{
            // age的值捕獲進來(capture)height的地址捕獲進來
            NSLog(@"age is %d, height is %d", age, height);
        };
        // 值傳遞
        age = 20;
        // 指針傳遞
        height = 20;
        // 打印結(jié)果:age is 10, height is 20
        block();

注意:self也是一個局部變量,所以也會被捕獲。所以通過self訪問的變量也都會被捕獲。方法調(diào)用中,c++底部所有的方法調(diào)用都會默認傳遞self和_cmd(方法名)兩個參數(shù),傳遞的參數(shù)就是局部變量。
默認方法入?yún)?png

3. block的類型

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

  • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )存放在數(shù)據(jù)區(qū)域。沒有訪問auto變量時,就是該類型block。
  • __NSStackBlock__ ( _NSConcreteStackBlock )存放在棧段。訪問了auto變量時,就是該類型block。
  • __NSMallocBlock__ ( _NSConcreteMallocBlock )存放在堆段。NSStackBlock類型block調(diào)用了copy函數(shù)時就是該類型。
    存儲位置示意圖:
    存儲位置.png
    PS:各存儲位置存儲內(nèi)容:
  • 程序區(qū)域:程序編譯時,將代碼相關(guān)數(shù)據(jù)存儲在此區(qū)域。無需開發(fā)者管理。
  • 數(shù)據(jù)區(qū)域:程序編譯時,全局變量數(shù)據(jù)存儲在此區(qū)域。無需開發(fā)者管理。
  • 堆:程序運行時,動態(tài)分配內(nèi)存,需要開發(fā)者申請內(nèi)存,也需要開發(fā)者自己管理內(nèi)存。
  • 棧:系統(tǒng)自動分配內(nèi)存,自己銷毀。存放局部變量數(shù)據(jù),離開作用域時內(nèi)存銷毀。

(2) 每一種類型的block調(diào)用copy后的結(jié)果如下所示:
調(diào)用copy結(jié)果.png

(3) block的copy操作:

  • 在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復(fù)制到堆上(block會變成NSMallocBlock類型),比如以下情況:
1. block作為函數(shù)返回值時;
2. 將block賦值給__strong指針時;
3. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時;
4. block作為GCD API的方法參數(shù)時;
  • ARC下block屬性的建議寫法:
    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
  • MRC下block屬性的建議寫法:
    @property (copy, nonatomic) void (^block)(void);

4. block內(nèi)部訪問對象類型的auto變量

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

  • 如果block是在棧上,將不會對auto變量產(chǎn)生強引用。
  • 如果block被拷貝到堆上:1. 會調(diào)用block內(nèi)部的copy函數(shù);2. copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù);3. _Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強引用(做一次retain操作)或者弱引用;
  • 如果block從堆上移除。1. 會調(diào)用block內(nèi)部的dispose函數(shù);2. dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù);3. _Block_object_dispose函數(shù)會斷開對auto變量的引用(做一次release操作);
    函數(shù)調(diào)用時機.png

5. block關(guān)于__block修飾變量

  • __block可以用于解決block內(nèi)部無法修改auto變量值的問題。
  • __block不能修飾全局變量、靜態(tài)變量(static)。
  • 編譯器會將__block修飾符的變量包裝成一個對象。底層掩飾如下
// 申明一個__block修飾符變量
typedef void (^MJBlock)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int age = 10;
        MJBlock block1 = ^{
            age = 20;
            NSLog(@"age is %d", age);
        };
        block1();
    }
    return 0;
}
// 上述oc代碼轉(zhuǎn)成c++底層代碼,__block int age的結(jié)構(gòu)
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  /* __block int age變量被包裝成__Block_byref_age_0類型的結(jié)構(gòu)體,結(jié)構(gòu)體里 
    有isa指針,實質(zhì)是oc對象。
    block修改age的值是通過*age ->forwarding->age來修改的
 */  
  __Block_byref_age_0 *age; 
};
// __Block_byref_age_0結(jié)構(gòu)體:
struct __Block_byref_age_0 {
  void *__isa;
// 存放指向自己的內(nèi)存地址
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
// __block int age變量 age的值
 int age;
};
  • __block修飾變量的內(nèi)存管理

    • 當(dāng)block在棧上時,并不會對__block變量產(chǎn)生強引用

    • 當(dāng)block被copy到堆時:1. 會調(diào)用block內(nèi)部的copy函數(shù);2. copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù);3._Block_object_assign函數(shù)會對__block變量形成強引用(做一次retain操作);

      引用流程圖.png

    • 當(dāng)block從堆中移除時: 1. 會調(diào)用block內(nèi)部的dispose函數(shù);2. dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù);3. _Block_object_dispose函數(shù)會斷開對__block變量的引用(做一次release操作);

      移除流程圖.png

  • __block修飾的對象變量內(nèi)存管理

    • 當(dāng)block在棧上時,并不會對__block變量產(chǎn)生強引用;
    • 當(dāng)block被copy到堆時:1. 會調(diào)用block內(nèi)部的copy函數(shù);2. _Block_object_assign函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃?code>(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強引用(retain)或者弱引用(注意:這里僅限于ARC時會retain,MRC時不會retain);3._Block_object_assign函數(shù)會對__block變量形成強引用(做一次retain操作);
    • 當(dāng)block從堆中移除時: 1. 會調(diào)用block內(nèi)部的dispose函數(shù);2. dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù);3. _Block_object_dispose函數(shù)會斷開對__block對象變量的引用(做一次release操作);
  • __block修飾變量的__forwarding指針。
    上面提到,__block修飾符變量的底層是包裝成一個oc對象,其內(nèi)部有一個指向自己的__forwarding指針,訪問__block變量是通過__forwarding訪問自己內(nèi)部的__block變量。

    這樣做的原因是:如果block在棧上時, __forwarding指針指向是棧上的block, 如果block copy到堆上時, __forwarding指針指向的是堆上的block, 通過__forwarding指針來訪問變量,就可以保證訪問的變量是堆上的變量。流程圖如下:
    訪問流程.png

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

  • 什么是block循環(huán)引
    循環(huán)引用是指對象之間的強引用鏈形成了環(huán)就創(chuàng)造了一個循環(huán)引用。最簡單的情況,兩個對象之間強引用,你引用我,我引用你,導(dǎo)致內(nèi)存無法釋放,就形成了循環(huán)引用。
    block 的循環(huán)引用情況是,block 會捕獲內(nèi)部使用的對象,形成隱式的強引用,一般有以下兩種常見的情況:
    1. 引用 self:直接寫 self:
    self.callback = ^{
      NSLog(@"callback: %@", self);}
    
    2.成員變量:不寫 self,但實際上還是對 self 的強引用:
      self.callback = ^{
      NSLog(@"callback: %@", _name);
      // 等價于
      NSLog(@"callback: %@", self->_name); 
       }
    
  • ARC-解決循環(huán)引用問題
    1. 用__weak解決,不會產(chǎn)生強引用,指向的對象銷毀時,會自動讓指針置為nil。
          MJPerson *person = [[MJPerson alloc] init];
          __weak typeof(person) weakSelf = person;
          person.block = ^{
              NSLog(@"age is %d", weakSelf.age);
          }
    
    1. 用__unsafe_unretained解決,不會產(chǎn)生強引用,不安全,指向的對象銷毀時,指針存儲的地址值不變,所以一般不常用。
         MJPerson *person = [[MJPerson alloc] init];
          __unsafe_unretained typeof(person) weakPerson = person;
          person.block = ^{
              NSLog(@"age is %d", weakPerson.age);
          };
    
    1. 用__block解決(必須要調(diào)用block),缺點:1. 必須要將block強引用的對象置空,且block一定要調(diào)用;2. 一定要等到block執(zhí)行完,對象才能被釋放。如果這個block一直沒有被調(diào)用,對象就一直不會被釋放,就會存在內(nèi)存泄露。
      block解決循環(huán)引用示意圖.png

      代碼演示:

          __block MJPerson *person = [[MJPerson alloc] init];
          person.block = ^{
              person.age = 20;
              NSLog(@"age is %d", person.age);
              person = nil
          };
          person.block();
    
  • MRC-解決循環(huán)引用問題(不支持__weak)
    1. 用__unsafe_unretained解決
      image.png
    2. 用__block解決
      image.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ā)布平臺,僅提供信息存儲服務(wù)。

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

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