Block 底層原理(一)

block的本質是對象函數(shù)徙鱼、結構體

一针姿、block 定義

  • block:帶有自動變量的匿名函數(shù)袱吆。
  • 匿名函數(shù):沒有函數(shù)名的函數(shù),一對{}包裹的內容是匿名函數(shù)的作用域距淫。
  • 自動變量:棧上聲明一個變量既不是靜態(tài)變量绞绒,又不是全局變量,是不可以在棧內聲明的匿名函數(shù)中使用的溉愁。但是在block中卻可以处铛,雖然使用block不用聲明類,但是block提供了類似OC的類一樣拐揭,可以通過成員變量來保護作用域外變量值的方法撤蟆,那些再block的一對{}里面使用到,但卻是在{}作用域外聲明的變量堂污,就是block接貨的自動變量家肯。

block表達式語法

^ 返回值類型(參數(shù)列表){表達式}

1、無參數(shù)盟猖,無返回值

// 1讨衣、無參數(shù)换棚,無返回值
void(^block)(void) = ^ {
    NSLog(@"block");
};
block();

2、有參數(shù)反镇,無返回值

// 2固蚤、有參數(shù),無返回值
void(^block)(int, NSString*) = ^(int a, NSString *name){
    NSLog(@"%@ -- %d", name, a);
};
block(10, @"block");

3歹茶、有參數(shù)夕玩,有返回值

// 3、有參數(shù)惊豺,有返回值
int(^block)(int, int) = ^(int a, int b){
    NSLog(@"block -- %d", a+b);
    return a+b;
};
block(10, 20);

4燎孟、無參數(shù),有返回值

// 4尸昧、無參數(shù)揩页,有返回值
int(^block)(void) = ^{
    NSLog(@"block -- 40");
    return 40;
};
block();

5、在開發(fā)中我們還可以用typedef定義block

typedef int(^block)(int, NSString*);

可以這樣使用:

block Myblock = ^(int a, NSString *name){
    NSLog(@"%@ -- %d", name, a);
    return 40;
};
NSLog(@"%d", Myblock(10, @"block"));

也可以定義成屬性:

@property (nonatomic, copy) block Myblock;

二烹俗、block的分類

  • block主要分為三類:
    ① 全局block:_NSConcreteGlobalBlock爆侣;存儲在全局內存中,相當于單例幢妄。
    ② 棧block:_NSConcreteStackBlock累提;存儲在棧內存中,超出其作用域則馬上被銷毀磁浇。
    ③ 堆block:_NSConcreteMallocBlock;存儲在堆內存中朽褪,是一個帶引用計數(shù)的對象置吓,需要自行管理其內存。
    這三種block各自的存儲區(qū)域如下圖:

    image.png

    簡而言之缔赠,存儲在棧中的block就是棧塊衍锚,存儲在堆區(qū)的就是堆塊,既不在棧區(qū)也不在堆區(qū)的就是全局塊

  • 當我們遇到一個block嗤堰,怎么去判定這個block的存儲位置呢戴质?

(1)block不訪問外部變量(包括棧和堆中的變量)
此時block既不在棧中,也不在堆中踢匣,在代碼段中告匠。ARCMRC下都是如此。
此時為全局block_NSConcreteGlobalBlock

void(^block)(void) = ^{
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSGlobalBlock__: 0x100004030>

(2)block訪問外部變量
MRC環(huán)境下:訪問外部變量的block默認是存儲在中的离唬。
ARC環(huán)境下:訪問外部變量的block默認是存儲在中的(實際是放在區(qū)后专,然后ARC情況下又自動拷貝到區(qū)),自動釋放输莺。

  • MRC 環(huán)境:
int a = 10;
void(^block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSStackBlock__: 0x7ffeefbff3e8>
  • ARC 環(huán)境下
int a = 10;
void(^block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSMallocBlock__: 0x1040508b0>
  • 在ARC環(huán)境下我們怎么獲取棧block呢戚哎?
    我們可以這樣做:
int a = 10;
void(^ __weak block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);
/*輸出結果為*/
<__NSStackBlock__: 0x7ffeefbff3e8>

此時我們通過__weak不進行強持有裸诽,block就還是棧區(qū)的block

  • ARC環(huán)境下型凳,訪問外部變量的block為什么要自動從棧區(qū)拷貝到堆區(qū)呢丈冬?
    因為:棧上的block,如果其所屬的變量作用域結束甘畅,該block就會被廢棄埂蕊,如同一般的自動變量。當然橄浓,block中的__block變量也同時會被廢棄粒梦。
    image.png

    為了解決棧塊在其變量作用域結束之后被廢棄(釋放)的問題,我們需要把block復制到堆中荸实,延長其生命周期匀们。開啟ARC時,大多數(shù)情況下編譯器會恰當?shù)倪M行判斷是否有必要將block從棧復制到堆准给,如果有泄朴,自動生成將block從棧復制到堆的代碼。block的復制操作執(zhí)行的是Copy實例方法露氮。block只要調用了Copy方法祖灰,棧塊就會變成堆塊。
    image.png

    eg:
typedef int(^myblock)(int);

myblock func(int a) {
    return ^(int b) {
        return a * b;
    };
}

上面的代碼中畔规,函數(shù)返回的block是配置在棧上的局扶,所以返回返回函數(shù)調用方法時,block變量作用域就被釋放了叁扫,block也會被釋放三妈。但是,在ARC環(huán)境下是有效的莫绣,這種情況編譯器會自動完成復制畴蒲。

在非ARC情況下則需要開發(fā)者調用Copy方法手動復制。

將block從棧區(qū)復制到堆區(qū)非常想好CPU对室,所以當block設置在棧上也能使用時色徘,就不要復制了代赁,因為此時的復制只是在浪費CPU資源辣辫。
block的復制操作蝌箍,執(zhí)行的是Copy實例方法。不同類型的block使用的Copy方法的效果如下:

block的類型 副本源的配置存儲區(qū)域 復制效果
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域 什么也不做
_NSConcreteStackBlock 棧區(qū) 從棧區(qū)復制到堆區(qū)
_NSConcreteMallocBlock 堆區(qū) 引用計數(shù)增加

根據(jù)表格我們知道锭亏,block在堆區(qū)Copy會造成引用計數(shù)增加纠吴,這與其它OC對象是一樣的。雖然block在棧中也是以對象的身份存在慧瘤,但是棧區(qū)沒有引用計數(shù)戴已,因為不需要固该,我們都知道棧區(qū)的內存由編譯器自動分配釋放。

三糖儡、block 底層分析

int a = 10;
void(^ block)(void) = ^{
    NSLog(@"%d", a);
};
NSLog(@"%@", block);

使用 clang將OC代碼轉換成C++文件伐坏,查看block的方法。

  • 在命令行輸入下面的指令(XXX.m就是要編譯的文件握联,需在當前文件夾下面執(zhí)行)
clang -rewrite-objc XXX.m
  • 執(zhí)行網上面的指令之后桦沉,當前文件夾中會多一個XXX.cpp的文件。此時在命令行輸入open XXX.cpp 或者 直接打開文件

  • 打開XXX.cpp文件金闽,在文件底部我們可以看到main函數(shù)被編譯之后的樣式:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int a = 10;
        void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
        NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_c75271_mi_1, block);
    }
    return 0;
}

我們從main函數(shù)中提取一下block

void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));

/*簡化一下纯露,去除強制轉換*/
void(* block)(void) = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); ///構造函數(shù)

可以看到構造函數(shù)名為__main_block_impl_0
下面我們再尋找一下__main_block_impl_0

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;
  }
};
  • 可以看到__main_block_impl_0是一個結構體。同時我們也可以說block是一個__main_block_impl_0類型的對象代芜,這也是為什么block能夠%@打印的原因

1埠褪、block自動捕獲外部變量

  • block自動捕獲的外部變量,在block的函數(shù)體內是不允許被修改的挤庇。
    ① 通過上面的代碼我們可以看到__main_block_impl_0函數(shù)的定義:
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 值拷貝 

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_ab7cb4_mi_0, a);
        }

可以看到钞速,編譯器會自動生成一個同名的變量
__main_block_func_0a是值拷貝嫡秕。
因此渴语,在block內存會生成一個,內容一樣的同名變量昆咽,此時如果在函數(shù)體內進行a++的操作驾凶,則編譯器就不清楚該去修改哪個變量。所以block自動捕獲的變量掷酗,在函數(shù)體內部是不允許修改的狭郑。

  • 那么我們要修改外部變量要怎么辦呢?
    1汇在、__block修飾外部變量。
    2脏答、將變量定義成全局變量
    3糕殉、將變量用參數(shù)的形式,傳入block里面殖告。
    第2種和第3種方式阿蝶,想必大家都非常的熟悉,在這里就不再贅述黄绩。下面我們來看一下第1種方式羡洁,底層究竟做了些什么。

__block 原理

  • 現(xiàn)在我們對a進行__block編譯爽丹,之后我們就可以在block內部對a進行修改筑煮。
__block int a = 10;
        void(^ block)(void) = ^{
            a++;
            NSLog(@"%d", a);
        };
        block();

下面我們再通過clang來觀察一下辛蚊,底層代碼有了什么變化。

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

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;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_a_0 *a = __cself->a; // bound by ref 指針拷貝
            /// 等同于外界的 a++
            (a->__forwarding->a)++;
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_w__pbdfr2t16xx3pkwg63c2y5m80000gn_T_main_b21337_mi_0, (a->__forwarding->a));
        }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);}

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};
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};
        void(* block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
  • 首先我們看到真仲,在main函數(shù)里面袋马,此時a變成了成了一個__Block_byref_a_0類型的對象。
  • 同時在__main_block_func_0函數(shù)中秸应,由之前的值拷貝(bound by copy) 變成了現(xiàn)在的指針拷貝(bound by ref)
    *在main函數(shù)中傳入的a是一個對象虑凛,同時在__main_block_func_0函數(shù)內部,對a進行指針拷貝软啼;則此時創(chuàng)建的對象a和傳入的對象a 指向同一片內存空間桑谍。

總結:__block修飾外界變量的時候:
1、外界變量會生成__Block_byref_a_0結構體
2祸挪、結構體用來保存原始變量的指針(可以在上面編譯后的代碼中找到)
3锣披、將變量生成的結構體對象指針地址傳遞給block,然后在block內部就可以對外界變量進行修改了。

接下來匕积,在給大家看一個東西:

  • 在上面的C++代碼中盈罐,__main_block_func_0函數(shù)中,大家會注意到執(zhí)行a++的是這段代碼(a->__forwarding->a)++;闪唆,那么這個__forwarding又是什么呢盅粪?
    接下來我們先看一下__Block_byref_a_0結構體長什么樣子:
struct __Block_byref_a_0 {
  void *__isa;
__Block_byref_a_0 *__forwarding;
 int __flags;
 int __size;
 int a;
};

可以看到,__forwarding是一個指向自己本身的指針(自身為結構體)悄蕾。
那么票顾,在Copy操作之后,既然__block變量也被Copy到堆區(qū)上了帆调,那么訪問該變量是訪問上的還是上的呢奠骄?這個時候我們就要來看一下,在Copy過程中__forwarding的變化了:

image.png

可以看到番刊,通過__forwarding含鳞,無論是在block中,還是block外訪問__block變量芹务,也不管該變量是在上或是在上蝉绷,都能順利的訪問同一個__block變量。
注意:這里與上面的結論并不矛盾枣抱。大家要主要到局部變量a__block修飾之后熔吗,會變成__Block_byref_a_0結構體對象。所以無論是在棧區(qū)還是在堆區(qū)佳晶,只要__forwarding指向的地址一樣桅狠,那么就可以在block內部修改外界變量。這里大家要仔細觀察一下__Block_byref_a_0結構體


例題:

int a = 10;
int *p = &a;
NSLog(@"開始 a == %d,  s == %p", a, &a);
NSLog(@"開始 p == %d,  s == %p", *p, p);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
     (*p) = 100;
     NSLog(@"異步 a == %d,  s == %p", a, &a);
     NSLog(@"異步 p == %d,  s == %p", *p, p);
});

///打印結果
開始 a == 10,  s == 0x7ffeebb2a0ac
開始 p == 10,  s == 0x7ffeebb2a0ac
異步 a == 10,  s == 0x600003a26c68
異步 p == 0,  s == 0x7ffeebb2a0ac
image
  • 這里有一點要注意
    我們上面用的是異步,所以最后的*p打印的不是100中跌,而是0咨堤。
    這就是因為異步,不用等待晒他,viewDidLoad直接執(zhí)行完畢吱型,*p對應的內存空間被釋放。
    如果在dispatch_async后面寫上其他的一些函數(shù)陨仅,輸出的*p可能是任意值:
    image.png

當然這也是一個釋放時機的問題津滞,如果有很多業(yè)務要處理,可能打印*p的時候灼伤,對應的內存地址還沒有釋放触徐。

  • a的最后輸出值是10這里就不多說,block捕獲局部變量的拷貝上面有講狐赡。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末撞鹉,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子颖侄,更是在濱河造成了極大的恐慌鸟雏,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件览祖,死亡現(xiàn)場離奇詭異孝鹊,居然都是意外死亡,警方通過查閱死者的電腦和手機展蒂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進店門又活,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人锰悼,你說我怎么就攤上這事柳骄。” “怎么了箕般?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵耐薯,是天一觀的道長。 經常有香客問我丝里,道長可柿,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任丙者,我火速辦了婚禮,結果婚禮上营密,老公的妹妹穿的比我還像新娘械媒。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布纷捞。 她就那樣靜靜地躺著痢虹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪主儡。 梳的紋絲不亂的頭發(fā)上奖唯,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機與錄音糜值,去河邊找鬼丰捷。 笑死,一個胖子當著我的面吹牛寂汇,可吹牛的內容都是我干的病往。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼骄瓣,長吁一口氣:“原來是場噩夢啊……” “哼停巷!你這毒婦竟也來了?” 一聲冷哼從身側響起榕栏,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤畔勤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扒磁,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體庆揪,經...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年渗磅,在試婚紗的時候發(fā)現(xiàn)自己被綠了嚷硫。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,646評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡始鱼,死狀恐怖仔掸,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情医清,我是刑警寧澤起暮,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站会烙,受9級特大地震影響负懦,放射性物質發(fā)生泄漏。R本人自食惡果不足惜柏腻,卻給世界環(huán)境...
    茶點故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一纸厉、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧五嫂,春花似錦颗品、人聲如沸肯尺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽则吟。三九已至,卻和暖如春锄蹂,著一層夾襖步出監(jiān)牢的瞬間氓仲,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工得糜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留敬扛,地道東北人。 一個月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓掀亩,卻偏偏與公主長得像舔哪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子槽棍,可洞房花燭夜當晚...
    茶點故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內容