block的本質

block的本質

block本質上也是一個oc對象,他內部也有一個isa指針。block是封裝了函數調用以及函數調用環境的OC對象。

1.png

block其實也是NSObject的子類

block的類型

一共有三種類型的block分別是:全局的,棧上的b,堆上的

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

通過代碼查看一下block在什么情況下其類型會各不相同


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 內部沒有調用外部變量的block
        void (^block1)(void) = ^{
            NSLog(@"Hello");
        };
        // 2. 內部調用外部變量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };
       // 3. 直接調用的block的class
        NSLog(@"%@ %@ %@", [block1 class], [block2 class], [^{
            NSLog(@"%d",a);
        } class]);
    }
    return 0;
}


打印結果為:

2018-08-29 11:39:27.969734+0800 block的本質[40624:68783097] __NSGlobalBlock__ __NSMallocBlock__ __NSStackBlock__

__NSGlobalBlock__直到程序結束才會被回收
__NSStackBlock__類型的block存放在棧中,我們知道棧中的內存由系統自動分配和釋放,作用域執行完畢之后就會被立即釋放
__NSMallocBlock__是在平時編碼過程中最常使用到的。存放在堆中需要我們自己進行內存管理。

block是如何定義其類型

2.png

block的實現

寫一個簡單的block

int main(int argc, const char * argv[]) {
    @autoreleasepool {

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

        blk();
    }
    return 0;
}

使用命令行將代碼轉化為c++查看其內部結構,與OC代碼進行比較
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m

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_sr_m_cfkwyx2h56vh4_kf65_vw40000gn_T_main_55e532_mi_0,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 = 5;
        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;
}

首先我們看一下__block_impl第一個成員就是__block_impl結構體

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

我們可以發現__block_impl結構體內部就有一個isa指針。因此可以證明block本質上就是一個oc對象

接著通過上面對__main_block_impl_0結構體構造函數三個參數的分析我們可以得出結論:

  1. __block_impl結構體中isa指針存儲著&_NSConcreteStackBlock地址,可以暫時理解為其類對象地址,block就是_NSConcreteStackBlock類型的。
  2. block代碼塊中的代碼被封裝成__main_block_func_0函數,FuncPtr則存儲著__main_block_func_0函數的地址。
  3. Desc指向__main_block_desc_0結構體對象,其中存儲__main_block_impl_0結構體所占用的內存。

block的變量捕獲

為了保證block內部能夠正常訪問外部的變量,block有一個變量捕獲機制

局部變量

  • auto變量 - 值傳遞
  • static變量 - 指針傳遞

全局變量

  • 直接訪問

block對對象變量的捕獲

void(^blk)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        {
        Person *person = [[Person alloc] init];
        person.age = 10;
        blk = ^{
             NSLog(@"------block內部%ld",person.age);
        };
        }

        NSLog(@"-----");

    }
    return 0;
}

大括號執行完畢之后,person依然不會被釋放。person為aotu變量,即block有一個強引用引用person,所以block不被銷毀的話,peroson也不會銷毀。

查看源代碼確實如此

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

__weak

__weak添加之后,person在作用域執行完畢之后就被銷毀了

void(^blk)(void);
int main(int argc, const char * argv[]) {
    @autoreleasepool {

        {
        Person *person = [[Person alloc] init];
        person.age = 10;

        __weak Person *weakPerson = person;
        blk = ^{
             NSLog(@"------block內部%ld",weakPerson.age);
        };
        }

        NSLog(@"-----");

    }
    return 0;
}

__weak修飾變量,需要告知編譯器使用ARC環境及版本號否則會報錯,添加說明:

xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m

__weak修飾的變量,在生成的__main_block_impl_0中也是使用__weak修飾

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  Person *__weak weakPerson;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
__main_block_copy_0 和 __main_block_dispose_0

當block中捕獲對象類型的變量時,我們發現block結構體__main_block_impl_0的描述結構體__main_block_desc_0中多了兩個參數copy和dispose函數,查看源碼:

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->weakPerson, (void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->weakPerson, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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};

  • copy本質就是__main_block_copy_0函數,__main_block_copy_0函數內部調用_Block_object_assign函數
    當block進行copy操作的時候就會自動調用__main_block_desc_0內部的__main_block_copy_0函數
    __main_block_copy_0函數內部會調用_Block_object_assign函數。

_Block_object_assign函數會自動根據__main_block_impl_0結構體內部的person是什么類型的指針,對person對象產生強引用或者弱引用。可以理解為_Block_object_assign函數內部會對person進行引用計數器的操作,如果__main_block_impl_0結構體內person指針是__strong類型,則為強引用,引用計數+1,如果__main_block_impl_0結構體內person指針是__weak類型,則為弱引用,引用計數不變。

  • 當block從堆中移除時就會自動調用__main_block_desc_0中的__main_block_dispose_0函數,__main_block_dispose_0函數內部會調用_Block_object_dispose函數。

_Block_object_dispose會對person對象做釋放操作,類似于release,也就是斷開對person對象的引用,而person究竟是否被釋放還是取決于person對象自己的引用計數。

總結

  1. 一旦block中捕獲的變量為對象類型,block結構體中的__main_block_desc_0會出兩個參數copy和dispose。因為訪問的是個對象,block希望擁有這個對象,就需要對對象進行引用,也就是進行內存管理的操作。比如說對對象進行retarn操作,因此一旦block捕獲的變量是對象類型就會會自動生成copy和dispose來對內部引用的對象進行內存管理。

  2. 當block內部訪問了對象類型的auto變量時,如果block是在棧上,block內部不會對person產生強引用。不論block結構體內部的變量是__strong修飾還是__weak修飾,都不會對變量產生強引用。

  3. 如果block被拷貝到堆上。copy函數會調用_Block_object_assign函數,根據auto變量的修飾符(__strong,__weak,unsafe_unretained)做出相應的操作,形成強引用或者弱引用

  4. 如果block從堆中移除,dispose函數會調用_Block_object_dispose函數,自動釋放引用的auto變量。

block內修改變量的值

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };

    }
    return 0;
}

block不能修改外部的局部變量

age是在main函數內部聲明的,說明age的內存存在于main函數的棧空間內部,但是block內部的代碼在__main_block_func_0函數內部。__main_block_func_0函數內部無法訪問age變量的內存空間,兩個函數的棧空間不一樣,__main_block_func_0內部拿到的age是block結構體內部的age,因此無法在__main_block_func_0函數內部去修改main函數內部的變量。

方式一:age使用static修飾。

static修飾的age變量傳遞到block內部的是指針,在__main_block_func_0函數內部就可以拿到age變量的內存地址,因此就可以在block內部修改age的值。

方式二:__block

__block用于解決block內部不能修改auto變量值的問題,__block不能修飾靜態變量(static) 和全局變量

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        __block int a = 5;
        void(^blk)(void) = ^{
            a = 10;
        };

    }
    return 0;
}

查看源碼:


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->__forwarding->a) = 10;
}

首先被__block修飾的a變量聲明變為名為age的__Block_byref_a_0結構體,也就是說加上__block修飾的話捕獲到的block內的變量為__Block_byref_a_0類型的結構體。

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


__isa指針 :__Block_byref_age_0中也有isa指針也就是說__Block_byref_age_0本質也一個對象。

__forwarding :__forwarding是__Block_byref_age_0結構體類型的,并且__forwarding存儲的值為(__Block_byref_age_0 *)&age,即結構體自己的內存地址。

__flags :0

__size :sizeof(__Block_byref_age_0)即__Block_byref_age_0所占用的內存空間。

a :真正存儲變量的地方,這里存儲局部變量10。


__block將變量包裝成對象,然后在把age封裝在結構體里面,block內部存儲的變量為結構體指針,也就可以通過指針找到內存地址進而修改變量的值。

循環引用

情景一:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Person *person = [[Person alloc] init];
        person.age = 10;
        person.block = ^{
            NSLog(@"%d",person.age);
        };
    }
    NSLog(@"大括號結束啦");
    return 0;
}

可以發現大括號結束之后,person依然沒有被釋放,產生了循環引用

通過一張圖看一下他們之間的內存結構

3.png

上圖中可以發現,Person對象和block對象相互之間產生了強引用,導致雙方都不會被釋放,進而造成內存泄漏

情景二:

#import "Person.h"

@implementation Person


- (instancetype)init
{
    self = [super init];
    if (self) {
        self.block = ^{
            NSLog(@"%@",[self class]);
        };
    }
    return self;
}

這是開發中經常遇到的一個場景。之前我們說過block會捕獲局部,上面的OC函數調用轉化為runtime代碼為
objc_msgSend(self,@selector(init)) 在OC的方法中 有2個隱藏參數 self和_cmd 這2個參數作為函數的形參
在方法作用域中屬于局部變量 , 所以在block中使用self就滿足之前提到的 block會捕獲局部變量,查看源碼為:

struct __Person__init_block_impl_0 {
  struct __block_impl impl;
  struct __Person__init_block_desc_0* Desc;
  Person *self;
  __Person__init_block_impl_0(void *fp, struct __Person__init_block_desc_0 *desc, Person *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __Person__init_block_func_0(struct __Person__init_block_impl_0 *__cself) {
  Person *self = __cself->self; // bound by copy

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_sr_m_cfkwyx2h56vh4_kf65_vw40000gn_T_Person_3f840b_mi_0,((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class")));
}

這里可以看到 __Person__init_block_impl_0結構體中 創建了一個Person *self的強指針 指向init方法中self
指針所指向的person對象,使person引用計數+1 而person對block也有一個強引用。這里就造成了循環引用。

  • 解決循環引用問題

    首先為了能隨時執行block,我們肯定希望person對block對強引用,而block內部對person的引用為弱引用最好。
    使用__weak__unsafe_unretained修飾符可以解決循環引用的問題

    • __weak__unsafe_unretained的區別。

      __weak不會產生強引用,指向的對象銷毀時,會自動將指針置為nil。因此一般通過__weak來解決問題。

      __unsafe_unretained不會產生強引用,不安全,指向的對象銷毀時,指針存儲的地址值不變。會造成野指針問題

情景三

在block中調用super也會造成循環引用 :

#import "Person.h"

@implementation Person


- (instancetype)init
{
    self = [super init];
    if (self) {
        self.block = ^{
            [super init];
        };
    }
    return self;
}

查看源碼為:

static void __Person__init_block_func_0(struct __Person__init_block_impl_0 *__cself) {

            ((Person *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Person"))}, sel_registerName("init"));
        }

當使用[self class]時,會調用objc_msgSend函數,第一個參數receiver就是self,而第二個參數,要先找到self所在的這個class的方法列表

當使用[super class]時,會調用objc_msgSendSuper函數,此時會先構造一個__rw_objc_super的結構體作為objc_msgSendSuper的第一個參數。 該結構體第一個成員變量receiver仍然是self,而第二個成員變量super_class即是所在類的父類

struct __rw_objc_super {
    struct objc_object *object;
    struct objc_object *superClass;
    __rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};

runtime對外暴露的類型為:

struct objc_super {
    __unsafe_unretained _Nonnull id receiver;

    __unsafe_unretained _Nonnull Class super_class;
};

結構體第一個成員receiver 就代表方法的接受者  第二個成員代表方法接受者的父類

所以

self.block = ^{
    [super init];
};

轉換為源碼是:


self.block = ^{
        struct objc_super  superInfo = {
            .receiver = self,
            .super_class = class_getSuperclass(objc_getClass("Person")),
        };

        ((Class(*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo,@selector(init));
    };

可以很明顯的看到問題,block強引用了self,而self也強持有了這個block

  • 解決方法

    正確的調用姿勢跟平常我們切斷block的循環引用的姿勢一模一樣:

__weak __typeof(self) weakSelf = self;
self.block = ^{
    struct objc_super superInfo = {
            .receiver = weakSelf,
            .super_class = class_getSuperclass(NSClassFromString(@"ViewController")),
        };
        ((Class(*)(struct objc_super *, SEL))objc_msgSendSuper)(&superInfo,@selector(class));
};
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容