iOS - __block 修飾符底層探索

Block技術(shù)合集

iOS - Block變量截獲
Block的寫(xiě)法及使用

閱讀本文前祝蝠,請(qǐng)先思考如下問(wèn)題

  • 為什么Block可以截獲變量
  • 為什么Block外定義的基本數(shù)據(jù)類(lèi)型犀暑,在Block內(nèi)部不能修改
  • 為什么用__block修飾后擎鸠,在Block內(nèi)部可以修改
    本文將對(duì)Block底層探索并解答如上三個(gè)問(wèn)題

什么是Block

帶有自動(dòng)變量值的匿名函數(shù)

Block截獲變量

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        void(^blk)(void) = ^{
            printf("Block\n");
        };
        blk();
    }
    return 0;
}

編譯成成cpp代碼, 代碼非常多细办,我們精簡(jiǎn)如下

//1. 結(jié)構(gòu)體
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

//2. 
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __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;
  }
};

//3. 
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            printf("Block\n");
        }

//4. 
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

//5. main函數(shù)代碼塊
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

總共4個(gè)結(jié)構(gòu)體和一個(gè)main函數(shù)代碼塊
查看5. main函數(shù)代碼塊 可見(jiàn)哆档,block對(duì)象被編譯成了__main_block_impl_0類(lèi)型的結(jié)構(gòu)體, 這個(gè)結(jié)構(gòu)體由兩個(gè)成員結(jié)構(gòu)體和一個(gè)構(gòu)造函數(shù)組成燃辖,兩個(gè)結(jié)構(gòu)體分別是__block_impl__main_block_desc_0類(lèi)型的锯玛,其中__block_impl結(jié)構(gòu)體中有一個(gè)函數(shù)指針接奈, 指針指向__main_block_func_0類(lèi)型的結(jié)構(gòu)體踢涌,總結(jié)關(guān)系圖如下:

Block在定義的時(shí)候:

((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))

Block在調(diào)用的時(shí)候:

((__block_impl *)blk)->FuncPtr

Block內(nèi)部的函數(shù)打印,很顯然放在了__main_block_func_0序宦,那么block內(nèi)部截獲的數(shù)據(jù)存放在哪呢睁壁?同樣 我們對(duì)如下代碼進(jìn)行編譯

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        void(^blk)(void) = ^{
            printf(" Block\n a = %d\n", a);
        };
        blk();
    }
    return 0;
}

編譯成cpp

//1. 
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf(" Block\n a = %d\n", a);
        }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

很顯然的是,__main_block_impl_0結(jié)構(gòu)體增加了成員變量int a;并且在結(jié)構(gòu)體的構(gòu)造函數(shù)__main_block_func_0中對(duì)變量進(jìn)行賦值int a = __cself->a,而這一賦值操作,在Block定義的時(shí)候就已完成(并非在Block調(diào)用的時(shí)候)互捌,這也是Block截獲變量的原理(文章開(kāi)頭問(wèn)題1:為什么Block可以截獲變量)潘明。Block對(duì)不同數(shù)據(jù)類(lèi)型截獲方式請(qǐng)查看我之前寫(xiě)的iOS - Block變量截獲

為什么Block中不能修改變量值

我們先把代碼做微小的修改,即對(duì) block外定義的變量'int a = 10'秕噪, 分別在block定義前后及block內(nèi)部打印其地址

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10;
        printf("before block &a = %p \n", &a);
        void(^blk)(void) = ^{
            printf(" Block\n a = %d\n in block &a = %p \n ", a, &a);
        };
        printf("after  block &a = %p \n\n", &a);
        blk();
    }
    return 0;
}

打印如下:

before block &a = 0x7ffeefbff4ec 
after  block &a = 0x7ffeefbff4ec 

 Block
 a = 10
 in block &a = 0x1004385f0 

很明顯的是钳降,外block外部打印的int a地址一致,但在block內(nèi)部卻不一樣了,即block內(nèi)部的a并不是我們外部定義的int a(此時(shí)作者想起了一首歌:你說(shuō)的黑不是黑腌巾,你說(shuō)的白是神魔TM的白...)

這里問(wèn)題二的答案已經(jīng)很明顯了遂填,為什么block內(nèi)部無(wú)法修改外部的變量,因?yàn)榫筒皇峭粋€(gè)變量啊澈蝙,只是長(zhǎng)的一樣而已
有人就問(wèn)了吓坚,那block內(nèi)部的那個(gè)a究竟是誰(shuí)從哪里來(lái)?請(qǐng)看前邊編譯的cpp代碼中block方法的結(jié)構(gòu)體__main_block_func_0

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

            printf(" Block\n a = %d\n", a);
        }

此時(shí)你應(yīng)該恍然大悟灯荧,這個(gè)a是block內(nèi)部重新定義的a礁击,取值自block外部定義的int a = 10,至此,block內(nèi)部無(wú)法修改外部變量的問(wèn)題顯而易見(jiàn):

為什么無(wú)法修改:因?yàn)椴皇峭粋€(gè)值哆窿,地址不一樣
內(nèi)部的a變量哪來(lái)的:block底層重新定義的链烈,取值自外部(相當(dāng)于副本)

為什么用__block修飾后,在Block內(nèi)部可以修改

先附上__block修飾前編譯的main函數(shù)源碼(用于下文做比較)

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        printf("before block &a = %p \n", &a);
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        printf("after  block &a = %p \n\n", &a);
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

不廢話挚躯,改代碼加__block修飾测垛,先打印看看

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block int a = 10;
        printf("before block &a = %p \n", &a);
        void(^blk)(void) = ^{
            printf(" Block\n a = %d\n in block &a = %p \n ", a, &a);
        };
        printf("after  block &a = %p \n\n", &a);
        blk();
    }
    return 0;
}
before block &a = 0x7ffeefbff4e8 
after  block &a = 0x103009f98 
 Block
 a = 10
 in block &a = 0x103009f98 

根據(jù)打印,很明顯的能看到秧均,a在__block修飾定義時(shí)的地址食侮,與block內(nèi)部及block定義后的地址不一致,此處大膽猜測(cè)目胡,__block修飾的變量锯七,在block定義時(shí),會(huì)生成新的對(duì)象(下文得知是結(jié)構(gòu)體)誉己,在block外部獲取眉尸、更改該變量時(shí),獲取的是這個(gè)新生成的對(duì)象

我們編譯一下

//1. 
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

//2. 
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

//3. 
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref

            printf(" Block\n a = %d\n in block &a = %p \n ", (a->__forwarding->a), &(a->__forwarding->a));
        }

//4. 
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
        printf("before block &a = %p \n", &(a.__forwarding->a));
        void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        printf("after  block &a = %p \n\n", &(a.__forwarding->a));
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

編譯后源碼先找不同

  • 定義int a = 10變成了__Block_byref_a_0 a = 10(精簡(jiǎn))
  • 多了各結(jié)構(gòu)體__Block_byref_a_0,而此結(jié)構(gòu)體內(nèi)部有int a
  • __main_block_impl_0 結(jié)構(gòu)體中的int a不見(jiàn)了巨双,多了個(gè)__Block_byref_a_0 *a
  • __main_block_func_0結(jié)構(gòu)體中的int a = __cself->a變成了__Block_byref_a_0 *a = __cself->a
  • block外部的printf("after block &a = %p \n\n", &a)變成了printf("after block &a = %p \n\n", &(a.__forwarding->a))

上文不同翻譯總結(jié)一下就是答案:
變量添加__block修飾后噪猾,變量會(huì)被封裝稱(chēng)結(jié)構(gòu)體,結(jié)構(gòu)體內(nèi)部包含變量筑累,
在block內(nèi)部修改變量時(shí)袱蜡,修改的是結(jié)構(gòu)體__Block_byref_a_0內(nèi)部的變量數(shù)據(jù)(a->__forwarding->a)(所以可以修改)
出了block作用域后,修改數(shù)據(jù)修改的仍然是__Block_byref_a_0內(nèi)部的變量數(shù)據(jù)(a.__forwarding->a)

疑問(wèn):

printf("before block &a = %p \n", &(a.__forwarding->a));
printf("after  block &a = %p \n\n", &(a.__forwarding->a));
before block &a = 0x7ffeefbff4e8 
after  block &a = 0x100474798 `

查看編譯后底層代碼慢宗,打印地址查找都是&(a.__forwarding->a))坪蚁,為什么打印出來(lái)的地址不同

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市镜沽,隨后出現(xiàn)的幾起案子敏晤,更是在濱河造成了極大的恐慌,老刑警劉巖缅茉,帶你破解...
    沈念sama閱讀 221,576評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件嘴脾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡蔬墩,警方通過(guò)查閱死者的電腦和手機(jī)译打,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,515評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)筹我,“玉大人扶平,你說(shuō)我怎么就攤上這事∈呷铮” “怎么了结澄?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,017評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵哥谷,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我麻献,道長(zhǎng)们妥,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,626評(píng)論 1 296
  • 正文 為了忘掉前任勉吻,我火速辦了婚禮监婶,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘齿桃。我一直安慰自己惑惶,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,625評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布短纵。 她就那樣靜靜地躺著带污,像睡著了一般。 火紅的嫁衣襯著肌膚如雪香到。 梳的紋絲不亂的頭發(fā)上鱼冀,一...
    開(kāi)封第一講書(shū)人閱讀 52,255評(píng)論 1 308
  • 那天,我揣著相機(jī)與錄音悠就,去河邊找鬼千绪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛梗脾,可吹牛的內(nèi)容都是我干的荸型。 我是一名探鬼主播,決...
    沈念sama閱讀 40,825評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼藐唠,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼帆疟!你這毒婦竟也來(lái)了鹉究?” 一聲冷哼從身側(cè)響起宇立,我...
    開(kāi)封第一講書(shū)人閱讀 39,729評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎自赔,沒(méi)想到半個(gè)月后妈嘹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,271評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡绍妨,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,363評(píng)論 3 340
  • 正文 我和宋清朗相戀三年润脸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片他去。...
    茶點(diǎn)故事閱讀 40,498評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡毙驯,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出灾测,到底是詐尸還是另有隱情爆价,我是刑警寧澤,帶...
    沈念sama閱讀 36,183評(píng)論 5 350
  • 正文 年R本政府宣布,位于F島的核電站铭段,受9級(jí)特大地震影響骤宣,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜序愚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,867評(píng)論 3 333
  • 文/蒙蒙 一憔披、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爸吮,春花似錦芬膝、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,338評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至埂软,卻和暖如春锈遥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背勘畔。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,458評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工所灸, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人炫七。 一個(gè)月前我還...
    沈念sama閱讀 48,906評(píng)論 3 376
  • 正文 我出身青樓爬立,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親万哪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子侠驯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,507評(píng)論 2 359

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