iOS底層第八、九、十天 -- Block

引導語:

  • Block原理是怎樣的?本質是什么?
  • _block的作用是什么?使用需要注意什么?
  • block的屬性修飾詞為什么是copy?使用block有哪些使用注意?
  • block在修改NSMutableArray時需不需要添加_block?

我們通常代碼中使用的block是什么:為什么能夠保存一段代碼,而控制block內部代碼塊執行時機呢?


Q:Block本質是什么?
block是封裝了函數調用、以及函數調用環境的OC對象。
函數調用:block 的{}代碼塊。
函數調用環境:block傳入的參數、捕獲的auto變量。
相關圖:

Block本質.png

查看block源碼:

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;
  // 構造函數(類似于OC的init方法),返回結構體對象
  __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;
  }
};

// 封裝了block執行邏輯的函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_c60393_mi_0);
        }

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;
        // 定義block變量
        void (*block)(void) = &__main_block_impl_0(
                                                   __main_block_func_0,
                                                   &__main_block_desc_0_DATA
                                                   );

        // 執行block內部的代碼
        block->FuncPtr(block);
    }
    return 0;
}
Block代碼本質.png
  • FuncPtr:指向調用函數的地址
  • __main_block_desc_0 :block描述信息
  • Block_size:block的大小

C語言中不允許結構體內有方法函數
C++允許結構體內些方法函數(構造函數),構造函數類似OC中的init


第二部分:Block捕獲變量

Q:下述代碼輸出值為多少?

int age=10;
void (^Block)(void) = ^{
    NSLog(@"age:%d",age);
};
age = 20;
Block();

輸出值為 age:10
原因:創建block的時候,已經把age的值存儲在里面了。

Q:下列代碼輸出值分別為多少?

auto int age = 10;
static int num = 25;
void (^Block)(void) = ^{
    NSLog(@"age:%d,num:%d",age,num);
};
age = 20;
num = 11;
Block();

輸出結果為:age:10,num:11
原因:auto變量block訪問方式是值傳遞,static變量block訪問方式是指針傳遞
源碼證明:

int age = __cself->age; // bound by copy
int *num = __cself->num; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_d2875b_mi_0, age, (*num));

int age = 10;
static int num = 25;

block = ((void (*)())&__test_block_impl_0((void *)__test_block_func_0, &__test_block_desc_0_DATA, age, &num));

age = 20;
num = 11;

上述代碼可查看 static修飾的變量,是根據指針訪問的

Q:為什么block對auto和static變量捕獲有差異?
auto:自動變量,離開作用域銷毀,不采用指針訪問;
static:變量一直保存在內存中,可以訪問內存,指針訪問即可。

Q:block對全局變量的捕獲方式是?
block不需要對全局變量捕獲,都是直接采用取值的

Q:為什么局部變量需要捕獲?
考慮作用域的問題,需要跨函數訪問,就需要捕獲

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

變量.png

Q:block內訪問self是否會捕獲(capture)
會,self是當調用block函數的參數,參數是局部變量,self指向調用者

Q:block里訪問成員變量是否會捕獲?
會,成員變量_xxx的訪問其實是self->xxx,先捕獲self,再通過self訪問里面的成員變量.


第三部分 Block類型

Q:block有哪幾種類型?
block的類型,取決于isa指針,可以通過調用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型

  • __NSGlobalBlock __ ( _NSConcreteGlobalBlock )
  • __NSStackBlock __ ( _NSConcreteStackBlock )
  • __NSMallocBlock __ ( _NSConcreteMallocBlock )

代碼示例:

void (^block1)(void) = ^{
    NSLog(@"block1");
};
NSLog(@"%@",[block1 class]);
NSLog(@"%@",[[block1 class] superclass]);
NSLog(@"%@",[[[block1 class] superclass] superclass]);
NSLog(@"%@",[[[[block1 class] superclass] superclass] superclass]);
NSLog(@"%@",[[[[[block1 class] superclass] superclass] superclass] superclass]);

輸出結果:
NSGlobalBlock
__NSGlobalBlock
NSBlock
NSObject
null

上述代碼輸出了block1的類型,也證實了block是對象,最終繼承NSObject.

如果我們在lldb的過程中觀看block的類型時,發現編譯階段block都是stack形式,但是打印出來會顯示不同類型。這是因為OC位動態語言,在這個過程中會對其進行處理。所以一切以runtime運行時的結果為準。

Q:各類型的block在內存中如何分配的?

  • __NSGlobalBlock __ 在數據區
  • __NSMallocBlock __ 在堆區
  • __NSStackBlock __ 在棧區

堆:動態分配內存,需要程序員自己申請,程序員自己管理
棧:自動分配內存,自動銷毀,先入后出,棧上的內容存在自動銷毀的情況
編譯階段:此階段會把代碼 編入代碼區、data區內

block內存分配.png

Q:如何判斷block是哪種類型?

Block類型 環境
_NSGlobalBlock 沒有訪問auto變量
_NSStackBlock 訪問了auto變量
_NSMallocBlock _NSStackBlock 調用了copy

Q:對每種類型block調用copy操作后是什么結果?

Block類型 原配置存儲 復制效果
_NSGlobalBlock 數據區 什么也不會做
_NSStackBlock 棧區 從棧復制到堆上
_NSMallocBlock 堆區 引用計數增加

總結:棧上block用完,其內部的數據變成垃圾數據,說不準是啥。所以copy棧數據到堆上使用才可以。

Q:在ARC環境下,編譯器什么情況下會自動將棧上的block復制到堆上?

  1. block作為函數返回值時
  2. 將block賦值給__strong指針時
  3. block作為Cocoa API中方法名含有usingBlock的方法參數時
  4. block作為GCD API的方法參數時

ARC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);

MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);


第四部分:對象類型的auto變量

Q:ARC下述代碼中Person對象是否會釋放?
示例代碼:

typedef void(^XBTBlock)(void);
XBTBlock block;
{
    Person *p = [[Person alloc] init];
    p.age = 10;
    
    block = ^{
        NSLog(@"======= %d",p.age);
    };
}

Person.m
- (void)dealloc{
    NSLog(@"Person - dealloc");
}

輸出結果:不會打印Person - dealloc

轉化C++代碼后:

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, MJPerson *__strong _person, int flags=0) : person(_person) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

上述結果:
block為堆block,block里面有一個Person指針,Person指針指向Person對象。只要block還在,Person就還在。block強引用了Person對象。

疑問:此處的isa指針明明指向生成的stackBlock呀,為什么最后還是強引用person對象呢?是不是什么地方有錯誤?
推論:此情況只是編譯時候產生的,我們在runtime的時候對這個類型進行了改變,我們通過打印class 進行block類型查看即可。所以可以引導出以下結論。
結論:ARC情況下會自動將stackBlock拷貝到堆上生成mallocBlock,從而強引用里面person對象

Q:上述代碼改換成MRC,Person對象會釋放么?
會的!
堆空間的block會對Person對象retain操作,擁有一次Person對象。

Q:下列代碼中Person是否會被釋放?

@autoreleasepool {
        XBTBlock block;
        
        {
            Person *p = [[Person alloc] init];
            p.age = 10;
            __weak Person *weakPersn = p;
            block = ^{
                NSLog(@"======= %d",weakPersn.age);
            };
        }
        NSLog(@"--------------");
}

答案:會釋放
原因: __weak修飾對象后,我們block捕獲的就是一個弱引用的對象,所以block不會強引用person對象,在person出了作用域后就可以釋放了。

小結

無論MRC還是ARC,棧空間上的block,不會持有對象;堆空間的block,會持有對象。

Q:當block內部訪問了對象類型的auto變量時,是否會強引用?
答案:分情況討論,分為棧block和堆block

棧block: 如果block是在棧上,將不會對auto變量產生強引用(棧上的block隨時會被銷毀,自身都難保,也沒必要去強引用其他對象)

堆block:
1.如果block被拷貝到堆上:
a) 會調用block內部的copy函數
b) copy函數內部會調用_Block_object_assign函數
c) _Block_object_assign函數會根據auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用

2.如果block從堆上移除
a) 會調用block內部的dispose函數
b) dispose函數內部會調用_Block_object_dispose函數
c) _Block_object_dispose函數會自動釋放引用的auto變量(release

正確答案:

  • 如果block在棧空間,不管外部變量是強引用還是弱引用,block都會弱引用訪問對象
  • 如果block在堆空間,如果外部強引用,block內部也是強引用;如果外部弱引用,block內部也是弱引用

Q1:gcd的block中引用 Person對象什么時候銷毀?
代碼:

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    Person *person = [[Person alloc] init];
    person.age = 10;
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%d",person.age);
    });
    
    NSLog(@"touchesBegan");
}

輸出結果:
14:36:03.395120+0800 test[1032:330314] touchesBegan
14:36:05.395237+0800 test[1032:330314] age:10
14:36:05.395487+0800 test[1032:330314] Person-dealloc

原因:gcd的block默認會做copy操作,即dispatch_after的block是堆block,block會對Person強引用,block銷毀時候Person才會被釋放。

Q2:上述代碼如果換成__weak,Person什么時候釋放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");
}

輸出結果:
14:38:42.996990+0800 test[1104:347260] touchesBegan
14:38:42.997481+0800 test[1104:347260] Person-dealloc
14:38:44.997136+0800 test[1104:347260] age:0x0

原因:使用__weak修飾過后的對象,堆block會采用弱引用,無法延時Person的壽命,所以在touchesBegan函數結束后,Person就會被釋放,gcd就無法捕捉到Person。

Q3:如果gcd內包含gcd,Person會什么時候釋放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",person);
        });
        NSLog(@"1-----age:%p",weakPerson);
    });

    NSLog(@"touchesBegan");
}

輸出結果:
14:48:01.293818+0800 test[1199:403589] touchesBegan
14:48:05.294127+0800 test[1199:403589] 1-----age:0x604000015eb0
14:48:08.582807+0800 test[1199:403589] 2-----age:0x604000015eb0
14:48:08.583129+0800 test[1199:403589] Person-dealloc

原因:gcd內部只要有強引用Person,Person就會等待執行完再銷毀!所以Person銷毀時間為7秒。

Q4:如果gcd內部先強引用后弱引用,Person什么時候釋放?

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    Person *person = [[Person alloc] init];
    person.age = 10;
    
    __weak Person *weakPerson = person;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC)),
                   dispatch_get_main_queue(), ^{
                       
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"2-----age:%p",weakPerson);
        });
        NSLog(@"1-----age:%p",person);
    });

    NSLog(@"touchesBegan");
}

輸出結果
14:52:29.036878+0800 test[1249:431302] touchesBegan
14:52:33.417862+0800 test[1249:431302] 1-----age:0x6000000178d0
14:52:33.418178+0800 test[1249:431302] Person-dealloc
14:52:36.418204+0800 test[1249:431302] 2-----age:0x0

原因:Person會等待強引用執行完畢后釋放,只要強引用執行完,就不會等待后執行的弱引用,會直接釋放的,所以Person釋放時間為4秒。_weak修飾person后,則block不會強引用它,所以出了其作用域就會被銷毀。


第五部分:__block修飾符

Q:block能否修改變量值?
auto修飾變量:block無法修改,因為block使用的時候是內部創建了變量來保存外部的變量的值,block只有修改內部自己變量的權限,無法修改外部變量的權限。
static修飾變量:block可以修改,因為block把外部static修飾變量的指針存入,block直接修改指針指向變量值,即可修改外部變量值。
全局變量值:全局變量無論哪里都可以修改,當然block內部也可以修改。

Q:__block int age = 10,系統做了哪些?
答案:編譯器會將__block變量包裝成一個對象
查看c++源碼:

struct __Block_byref_age_0 {
  void *__isa;
__Block_byref_age_0 *__forwarding;//age的地址
 int __flags;
 int __size;
 int age;//age 的值
};

Q:__block 修飾符作用?

  • _block可以用于解決block內部無法修改auto變量值的問題
  • __block不能修飾全局變量、靜態變量(static)
  • 編譯器會將__block變量包裝成一個對象
  • __block修改變量:age->__forwarding->age
  • __Block_byref_age_0結構體內部地址和外部變量age是同一地址


    __block的forwarding指針指向.png

Q:__block 能修改auto變量的本質?

  • 編譯器會將__block變量包裝成一個對象(在內存單獨有空間保存數據)
  • __block修改變量:age->__forwarding->age

說明:block內部訪問的&age地址是__block對象結構體內部的age地址。

Q:block在修改NSMutableArray,需不需要添加__block?
不需要。block內部可以拿到數組的&p地址,通過指針對數組進行加減等操作是可以的。

Q:block可以向NSMutableArray添加元素么?

NSMutableArray *arr = [NSMutableArray array];

Block block = ^{
    [arr addObject:@"123"];
    [arr addObject:@"2345"];
};

答案:可以,因為是addObject是使用NSMutableArray變量,而不是通過指針改變NSMutableArray。但是如果是arr = nil,這就是改變了NSMutableArray變量,會報錯,這時需要用__block修飾才行。

5.1 __block的內存管理

當block在棧上時,并不會對__block變量產生強引用。
block用到某個對象時候,會對這個對象進行內存管理。

Q:block的屬性修飾詞為什么是copy?
block一旦沒有進行copy操作,就不會在堆上
block在堆上,程序員就可以對block做內存管理等操作,可以控制block的生命周期。在棧上出來作用域數據可能變成臟數據

為什么block不能在棧上.png

Q:當block被copy到堆時,對__block修飾的變量做了什么?

  • 會調用block內部的copy函數
  • copy函數內部會調用_Block_object_assign函數
  • _Block_object_assign函數會對__block變量形成強引用(retain)
  • 對于__block 修飾的變量 assign函數對其強引用;對于外部對象 assign函數根據外部如何引用而引用


    block0復制到堆上.png
block1復制到堆上.png

Q:當block從堆中移除時,對__block修飾的變量做了什么?

  • 會調用block內部的dispose函數
  • dispose函數內部會調用_Block_object_dispose函數
  • _Block_object_dispose函數會自動釋放引用的__block變量(release)


    block被廢棄.png
block1被廢棄.png

Q:block對象類型的auto變量、__block變量的區別?
從幾方面回答:

1.當block在棧上時,對它們都不會產生強引用
2.當block拷貝到堆上時

  • 都會通過copy函數來處理它們
  • 對于__block 修飾的變量 assign函數對其強引用;對于外部對象 assign函數根據外部如何引用而引用。注意第三個參數值是不一樣的
    __block變量(假設變量名叫做a)
    _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    對象類型的auto變量(假設變量名叫做p)
    _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

3.當block從堆上移除時

  • 都會通過dispose函數來釋放它們
    __block變量(假設變量名叫做a)
    _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
    對象類型的auto變量(假設變量名叫做p)
    _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

4.__block的__forwarding指針

  • 棧上__block的__forwarding指向本身
  • 棧上__block復制到堆上后,棧上block的__forwarding指向堆上的block,堆上block的__forwarding指向本身


    forwarding指向.png

Q:被__block修飾的對象類型在block上如何操作的?

分幾方面回答:

1.當__block變量在棧上時,不會對指向的對象產生強引用
2.當__block變量被copy到堆時

  • 會調用__block變量內部的copy函數
  • copy函數內部會調用_Block_object_assign函數
  • _Block_object_assign函數會根據所指向對象的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用(注意:這里僅限于ARC時會retain,MRC時不會retain
  • MRC環境下不會根據對象的修飾符引用,都是弱引用(注意是在有__block修飾的前提下)

3.如果__block變量從堆上移除

  • 會調用__block變量內部的dispose函數
  • dispose函數內部會調用_Block_object_dispose函數
  • _Block_object_dispose函數會自動釋放指向的對象(release)

__block修飾對象、auto修飾對象內存指向圖有什么區別?
auto修飾:block內的person指針,指向的MJPerson類對象內存。
__block修飾:block內的strongPerson指針,指向__block類對象 結構體struct_block_byref_person。struct_block_byref_person內的weakPerson根據屬性強弱指向MJPerson的類對象。

__block內存流程.jpg

無__block內存流向.jpg


第六部分 block循環引用

block中如何形成的循環?
1.block強持有對象person,對象person強持有block。
2.block強持有__block對象,__block強持有person,person強持有block

block循環.jpg

block循環圖1.png

block2循環與破解圖.png

Q:ARC下如何解決block循環引用的問題?

三種方式:__weak、__unsafe_unretained、__block

1.第一種方式:__weak

Person *person = [[Person alloc] init];
//        __weak Person *weakPerson = person;
__weak typeof(person) weakPerson = person;

person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

2.第二種方式:__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

3.第三種方式:__block

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
    person = nil;
};
person.block();

4.三種方法比較

  • __weak:不會產生強引用,指向的對象銷毀時,會自動讓指針置為nil。
  • __unsafe_unretained:不會產生強引用,不安全,指向的對象銷毀時,指針存儲的地址值不變。
  • __block:必須把引用對象置位nil,并且要調用該block();

Q:MRC下如何解決block循環引用的問題?
MRC環境下不支持弱指針,也就是不支持__weak。
所以兩種方式:__unsafe_unretained、__block

1.第一種方式:__unsafe_unretained

__unsafe_unretained Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", weakPerson.age);
};

2.第二種方式:__block

__block Person *person = [[Person alloc] init];
person.block = ^{
    NSLog(@"age is %d", person.age);
};

說明:本文參照李明杰老師課程、《iOS-Block本質》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容