03-iOS- OC中block底層原理

1. block的本質

  • block本質上也是一個OC對象,它內部也有個isa指針。
  • block是封裝了函數調用以及函數調用環(huán)境(block函數的調用地址杠人、參數、變量等信息)的OC對象宋下。
  • block的底層結構代碼如下:
    1. 首先在main函數中申明一個block
//  首先在main函數中申明一個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函數的oc代碼轉成C++代碼嗡善,具體看下block的底層實現結構如下:

// 將main函數的oc代碼轉成C++代碼,具體看下block的底層實現結構如下:
// oc中申明的block代碼底層實現是一個__main_block_impl_0的結構體
struct __main_block_impl_0 {
  // impl:是__block_impl類型的結構體学歧,其內部有個isa指針罩引,所以block的本質是一個oc對象。
  struct __block_impl impl;
  // Desc:是__main_block_desc_0類型的結構體枝笨。
  struct __main_block_desc_0* Desc;
  // age:是main函數中申明的局部變量age
  int age;
  // c++的構造方法蜒程,類似于oc的init構造方法
  __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的結構體內部實現:
struct __block_impl {
  void *isa;
  // 默認為0
  int Flags;
  int Reserved;
  // FuncPtr:block內部函數執(zhí)行地址
  void *FuncPtr;
};
// Desc的結構體內部實現:
static struct __main_block_desc_0 {
  size_t reserved;
  // Block_size:block的內存空間大小
  size_t Block_size;
}
  • block的底層結構如右圖所示:
    底層結構.png

2. block的變量捕獲(capture)

為了保證block內部能夠正常訪問外部的變量,block有個變量捕獲機制伺帘。判斷會不會被捕獲的標準是:如果是全局變量不會捕獲昭躺,如果是局部變量則會捕獲。
捕獲機制.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;
        // 打印結果:age is 10, height is 20
        block();

注意:self也是一個局部變量,所以也會被捕獲张咳。所以通過self訪問的變量也都會被捕獲帝洪。方法調用中,c++底部所有的方法調用都會默認傳遞self和_cmd(方法名)兩個參數脚猾,傳遞的參數就是局部變量葱峡。
默認方法入參.png

3. block的類型

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

  • __NSGlobalBlock__ ( _NSConcreteGlobalBlock )存放在數據區(qū)域。沒有訪問auto變量時提鸟,就是該類型block军援。
  • __NSStackBlock__ ( _NSConcreteStackBlock )存放在棧段。訪問了auto變量時称勋,就是該類型block胸哥。
  • __NSMallocBlock__ ( _NSConcreteMallocBlock )存放在堆段。NSStackBlock類型block調用了copy函數時就是該類型赡鲜。
    存儲位置示意圖:
    存儲位置.png
    PS:各存儲位置存儲內容:
  • 程序區(qū)域:程序編譯時空厌,將代碼相關數據存儲在此區(qū)域庐船。無需開發(fā)者管理。
  • 數據區(qū)域:程序編譯時嘲更,全局變量數據存儲在此區(qū)域醉鳖。無需開發(fā)者管理。
  • 堆:程序運行時哮内,動態(tài)分配內存,需要開發(fā)者申請內存盗棵,也需要開發(fā)者自己管理內存。
  • 棧:系統(tǒng)自動分配內存北发,自己銷毀纹因。存放局部變量數據,離開作用域時內存銷毀琳拨。

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

(3) block的copy操作:

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

4. block內部訪問對象類型的auto變量

當block內部訪問了對象類型的auto變量時:

  • 如果block是在棧上颜启,將不會對auto變量產生強引用。
  • 如果block被拷貝到堆上:1. 會調用block內部的copy函數浪讳;2. copy函數內部會調用_Block_object_assign函數缰盏;3. _Block_object_assign函數會根據auto變量的修飾符(__strong、__weak淹遵、__unsafe_unretained)做出相應的操作口猜,形成強引用(做一次retain操作)或者弱引用;
  • 如果block從堆上移除透揣。1. 會調用block內部的dispose函數济炎;2. dispose函數內部會調用_Block_object_dispose函數;3. _Block_object_dispose函數會斷開對auto變量的引用(做一次release操作)辐真;
    函數調用時機.png

5. block關于__block修飾變量

  • __block可以用于解決block內部無法修改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代碼轉成c++底層代碼倘感,__block int age的結構
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  /* __block int age變量被包裝成__Block_byref_age_0類型的結構體放坏,結構體里 
    有isa指針,實質是oc對象老玛。
    block修改age的值是通過*age ->forwarding->age來修改的
 */  
  __Block_byref_age_0 *age; 
};
// __Block_byref_age_0結構體:
struct __Block_byref_age_0 {
  void *__isa;
// 存放指向自己的內存地址
__Block_byref_age_0 *__forwarding;
 int __flags;
 int __size;
// __block int age變量 age的值
 int age;
};
  • __block修飾變量的內存管理

    • 當block在棧上時淤年,并不會對__block變量產生強引用

    • 當block被copy到堆時:1. 會調用block內部的copy函數钧敞;2. copy函數內部會調用_Block_object_assign函數;3._Block_object_assign函數會對__block變量形成強引用(做一次retain操作);

      引用流程圖.png

    • 當block從堆中移除時: 1. 會調用block內部的dispose函數麸粮;2. dispose函數內部會調用_Block_object_dispose函數溉苛;3. _Block_object_dispose函數會斷開對__block變量的引用(做一次release操作);

      移除流程圖.png

  • __block修飾的對象變量內存管理

    • 當block在棧上時弄诲,并不會對__block變量產生強引用愚战;
    • 當block被copy到堆時:1. 會調用block內部的copy函數;2. _Block_object_assign函數會根據所指向對象的修飾符(__strong齐遵、__weak寂玲、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用(注意:這里僅限于ARC時會retain梗摇,MRC時不會retain)拓哟;3._Block_object_assign函數會對__block變量形成強引用(做一次retain操作);
    • 當block從堆中移除時: 1. 會調用block內部的dispose函數伶授;2. dispose函數內部會調用_Block_object_dispose函數断序;3. _Block_object_dispose函數會斷開對__block對象變量的引用(做一次release操作);
  • __block修飾變量的__forwarding指針糜烹。
    上面提到违诗,__block修飾符變量的底層是包裝成一個oc對象,其內部有一個指向自己的__forwarding指針疮蹦,訪問__block變量是通過__forwarding訪問自己內部的__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)引用。最簡單的情況笛粘,兩個對象之間強引用趁怔,你引用我,我引用你薪前,導致內存無法釋放润努,就形成了循環(huán)引用。
    block 的循環(huán)引用情況是示括,block 會捕獲內部使用的對象铺浇,形成隱式的強引用,一般有以下兩種常見的情況:
    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解決,不會產生強引用鳍侣,指向的對象銷毀時丁稀,會自動讓指針置為nil。
          MJPerson *person = [[MJPerson alloc] init];
          __weak typeof(person) weakSelf = person;
          person.block = ^{
              NSLog(@"age is %d", weakSelf.age);
          }
    
    1. 用__unsafe_unretained解決,不會產生強引用倚聚,不安全线衫,指向的對象銷毀時,指針存儲的地址值不變惑折,所以一般不常用授账。
         MJPerson *person = [[MJPerson alloc] init];
          __unsafe_unretained typeof(person) weakPerson = person;
          person.block = ^{
              NSLog(@"age is %d", weakPerson.age);
          };
    
    1. 用__block解決(必須要調用block),缺點:1. 必須要將block強引用的對象置空,且block一定要調用惨驶;2. 一定要等到block執(zhí)行完矗积,對象才能被釋放。如果這個block一直沒有被調用敞咧,對象就一直不會被釋放棘捣,就會存在內存泄露。
      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
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末休建,一起剝皮案震驚了整個濱河市乍恐,隨后出現的幾起案子,更是在濱河造成了極大的恐慌测砂,老刑警劉巖茵烈,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異砌些,居然都是意外死亡呜投,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門存璃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來仑荐,“玉大人,你說我怎么就攤上這事纵东≌痴校” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵偎球,是天一觀的道長洒扎。 經常有香客問我,道長衰絮,這世上最難降的妖魔是什么袍冷? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮猫牡,結果婚禮上胡诗,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好乃戈,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布褂痰。 她就那樣靜靜地躺著亩进,像睡著了一般症虑。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上归薛,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天谍憔,我揣著相機與錄音,去河邊找鬼主籍。 笑死习贫,一個胖子當著我的面吹牛,可吹牛的內容都是我干的千元。 我是一名探鬼主播苫昌,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼幸海!你這毒婦竟也來了祟身?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤物独,失蹤者是張志新(化名)和其女友劉穎袜硫,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體挡篓,經...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡婉陷,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了官研。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片秽澳。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖戏羽,靈堂內的尸體忽然破棺而出肝集,到底是詐尸還是另有隱情,我是刑警寧澤蛛壳,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布杏瞻,位于F島的核電站,受9級特大地震影響衙荐,放射性物質發(fā)生泄漏捞挥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一忧吟、第九天 我趴在偏房一處隱蔽的房頂上張望砌函。 院中可真熱鬧,春花似錦、人聲如沸讹俊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽犀被。三九已至昏鹃,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間贩疙,已是汗流浹背讹弯。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留这溅,地道東北人组民。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像悲靴,于是被迫代替她去往敵國和親臭胜。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內容