iOS Block學(xué)習(xí)與使用

Block

一.什么是block

block是將函數(shù)調(diào)用及其上下文封裝的OC對象,內(nèi)部也有isa指針

二.block有幾種類型

  • block有3種類型,可以通過調(diào)用class方法或者isa指針查看具體類型,最終都是繼承自NSBlock類型
  • 注:auto變量(自動變量):離開當(dāng)前作用域就銷毀,默認(rèn)省略
block類型

在ARC環(huán)境下,編譯器會根據(jù)情況自動將棧上的block復(fù)制到堆上,比如以下情況

  1. block作為函數(shù)返回值時
  2. 將block賦值給__strong指針時
  3. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時
  4. block作為GCD API的方法參數(shù)時
  • 每一種類型的block調(diào)用copy后的結(jié)果如下所示


    block的復(fù)制效果

三.截獲變量

  1. 對于基本數(shù)據(jù)類型的局部變量截獲其值
  2. 對于對象類型的局部變量連同所有權(quán)修飾符一起截獲
  3. 靜態(tài)局部變量截取到內(nèi)部為指針傳遞,外部修改其值后,block內(nèi)部通過指針訪問時獲取最新值
  4. 全局變量和靜態(tài)全局變量不捕獲,直接訪問


    block截獲變量

eg:

   // 局部變量
   auto int a = 5;
   int(^block)(int) = ^int(int num){
       return a * num;
   };
   a = 7;
   NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 20
   // 靜態(tài)局部變量
   static int a = 5;
   int(^block)(int) = ^int(int num){
       return a * num;
   };
   a = 7;
   NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 28
    // __block修飾符
   __block int a = 5;
    int(^block)(int) = ^int(int num){
        return a * num;
    };
    a = 7;
    NSLog(@"block 結(jié)果 = %d", block(4)); // 結(jié)果 = 28

局部變量需要捕獲的原因是,block內(nèi)部需要跨函數(shù)訪問,所以需要先將局部變量存起來。全局變量不需要捕獲,因?yàn)橐恢痹趦?nèi)存中,可以直接訪問。

問題:

  1. 下面這種情況block會捕獲 self 嗎?
- (void)test {
   void(^block)(void) = ^{
       NSLog(@"-------%p", self);
   }
}

答案是會捕獲,因?yàn)樵谵D(zhuǎn)換底層c語言會默認(rèn)傳遞id self 和 SEL _cmd 兩個參數(shù),所以參數(shù)作為局部變量會被捕獲。

  1. 下面這種情況block會捕獲 _name 嗎?
- (void)test {
  void(^block)(void) = ^{
      NSLog(@"-------%@", _name);
  }
}

其實(shí)block內(nèi)部是訪問了self里的_name, 所以是先對self進(jìn)行捕獲,再訪問self中取_name(self->_name);所以并不是捕獲_name。

截獲對象類型的auto變量

  1. 如果block在棧上, 因?yàn)殡S時都有可能被銷毀,所以無論是MRC還是ARC環(huán)境下,block內(nèi)部訪問對象類型變量時,不會產(chǎn)生循環(huán)引用

  2. 如果block被拷貝到堆上,會調(diào)用block內(nèi)部的copy函數(shù)
    copy函數(shù)內(nèi)部會調(diào)用 _Block_object_assign 函數(shù)

    _Block_object_assign 函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用

  3. block從堆上移除,會調(diào)用dispose函數(shù),dispose函數(shù)內(nèi)部會調(diào)用 _Block_object_dispose 函數(shù)
    _Block_object_dispose 函數(shù)會自動釋放引用的auto變量(相當(dāng)于release)

  • 注意: 記住上面這兩個函數(shù),后面block的內(nèi)存管理會提到這個兩個函數(shù)
函數(shù)調(diào)用時機(jī)

四. block的屬性修飾詞

  1. MRC中建議使用copy

    @property (copy, nonatomic) void (^block)(void);
    
  2. ARC中建議使用strong和copy

    @property (strong, nonatomic) void (^block)(void);
    @property (copy, nonatomic) void (^block)(void);
    

注意:block作為oc對象,如果用assign修飾,因?yàn)閍ssign一般對基本數(shù)據(jù)類型修飾,基本數(shù)據(jù)類型存在棧區(qū),有系統(tǒng)自己處理生命周期,如果assign作用在對象身上,只是單純的指針賦值,當(dāng)block對象釋放后,指針沒有被置nil,造成懸垂指針,再對其發(fā)送消息會造成崩潰。assign是指針賦值。
block一旦沒有進(jìn)行copy操作,就不會在堆上

如果向一個nil對象發(fā)消息不會crash的話,那么unrecognized selector sent to instance的錯誤是怎么回事?

這是因?yàn)檫@個對象已經(jīng)被釋放了(引用計數(shù)為0了),那么這個時候再去調(diào)用方法肯定是會Crash的,因?yàn)檫@個時候這個對象就是一個懸垂指針(指向僵尸對象(對象的引用計數(shù)為0,指針指向的內(nèi)存已經(jīng)不可用)的指針)了,安全的做法是釋放后將對象重新置為nil,使它成為一個空指針,大家可以在關(guān)閉ARC后手動release對象驗(yàn)證一下。

OC中向nil發(fā)消息,程序是不會崩潰的。

因?yàn)镺C的函數(shù)都是通過objc_msgSend進(jìn)行消息發(fā)送來實(shí)現(xiàn)的,相對于C和C++來說,對于空指針的操作會引起crash問題,而objc_msgSend會通過判斷self來決定是否發(fā)送消息,如果self為nil,那么selector也會為空,直接返回,不會出現(xiàn)問題。視方法返回值,向nil發(fā)消息可能會返回nil(返回值為對象),0(返回值為一些基礎(chǔ)數(shù)據(jù))或0X0(返回值為id)等。但對于[NSNull null]對象發(fā)送消息時,是會crash的,因?yàn)镹SNull類只有一個null方法。

重點(diǎn):這里要區(qū)別的是空指針、野指針和懸垂指針的區(qū)別!

空指針:

1> 沒有存儲任何內(nèi)存地址的指針就稱為空指針(NULL指針)
2> 空指針就是被賦值為0的指針,在沒有被具體初始化之前,其值為0。

野指針:

野指針的產(chǎn)生是由于在首次使用之前沒有進(jìn)行必要的初始化。因此,嚴(yán)格地說,在編程語言中的所有為初始化的指針都是野指針。

懸垂指針

在許多編程語言中(比如C),顯示地從內(nèi)存中刪除一個對象或者返回時通過銷毀棧幀,并不會改變相關(guān)的指針的值。該指針仍舊指向內(nèi)存中相同的位置,即使引用已經(jīng)被刪除,現(xiàn)在可能已經(jīng)挪作他用。

參考assign修飾object類型會怎樣?

@property (nonatomic ,assign) TestObject *property1;

- (void)test {
    TestObject *obj = [TestObject new];
    self.property1 = obj;
    self.property1.age = 1;
}
test方法執(zhí)行完畢以后,該臨時變量就會被釋放,此時self.property1將變?yōu)閼掖怪羔槨?

五. __block 修飾符

使用block直接修改auto變量時出現(xiàn)下面這樣的問題

是因?yàn)閎lock截獲auto變量時是值傳遞,不能訪問到auto變量的指針地址,所以無法修改
有兩種方案解決

// 第一種
__block int num = 10;
void(^block)(void) = ^{
    num = 20;
};
// 第二種
static int num = 10;
void(^block)(void) = ^{
    num = 20;
};
  1. static 修飾的auto變量被block捕獲為指針捕獲,所以可以在內(nèi)部通過指針地址修改其值
  2. __block修飾的auto變量,在block內(nèi)部被包裝成一個對象
    __block不能修飾全局變量、靜態(tài)變量(static)

eg: block內(nèi)部調(diào)用age變量


下圖可以看出在block內(nèi)部被包裝成一個 __Block_byref_age_0 的對象(記住這個對象,后面會用到)

__Block_byref_age_0 結(jié)構(gòu)體內(nèi)部存在一個 __forwarding 指針,__forwarding 指針類型為 __Block_byref_age_0對象。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_age_0 *age = __cself->age; // bound by ref
  NSObject *p = __cself->p; // bound by copy
  (age->__forwarding->age) = 20;
  NSLog((NSString *)&__NSConstantStringImpl__var_folders_2r__m13fp2x2n9dvlr8d68yry500000gn_T_main_e2457b_mi_0, p);
  }

重點(diǎn)在這一行代碼(age->__forwarding->age) = 20; 可以看出age對象通過 __forwarding 指針找到age,再進(jìn)行賦值,所以__forwarding 指針是一個指向自身的指針

??warning

上圖對array對象賦值,所以出現(xiàn)和上面一樣的情況

不會報錯的原因,是因?yàn)閎lock捕獲局部變量是值傳遞,只是使用這個array對象是可以的,并沒有對其賦值和修改操作。

面試題:

__block NSMutableArray *array = [NSMutableArray arrayWithArray:@[@"1",@"3"]];
__block int i = 10;
void (^block)(void) = ^{
    [array addObject:@"2"];
    NSLog(@"%@,%d",array,i);
};
[array addObject:@"4"];
i = 20;
array = nil;
block();

結(jié)果:

test[48738:10976925] (null),20

如果刪掉NSMutableArray 之前的__block 結(jié)果:

test[48911:10979878] (
    1,
    3,
    4,
    2
),20

可以看出"4"也是能加進(jìn)去的,而且array = nil,并沒有影響block內(nèi)部的打印
原因猜測,可能不準(zhǔn)確: block捕獲auto對象類型變量,內(nèi)部會執(zhí)行copy函數(shù),array引用計數(shù)+1,block執(zhí)行了copy操作,從棧區(qū)copy到堆區(qū),array也從棧區(qū)copy到堆區(qū),所以,外部array=nil,只是棧區(qū)將array的指針只nil,但是內(nèi)存地址還存在,不影響block內(nèi)部。

六. __block內(nèi)存管理

  1. __block修飾基本數(shù)據(jù)類型

    • 當(dāng)block在棧上時,并不會對__block變量產(chǎn)生強(qiáng)引用
    • Block0在棧上,對Block0強(qiáng)引用時,ARC環(huán)境下,系統(tǒng)會自動將Block0 copy到堆上,同時__block修飾的變量也會被復(fù)制到堆上,此時堆上的Block0對__block修飾的變量時一個強(qiáng)引用的狀態(tài)。
    • 當(dāng)Block0被copy到堆上時,會調(diào)用block內(nèi)部的copy函數(shù),copy函數(shù)會調(diào)用 _Block_object_assign 函數(shù),_Block_object_assign 函數(shù)會對__block修飾的變量形成強(qiáng)引用。

    • 當(dāng)block從堆中移除時
    • 會調(diào)用block內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會自動釋放引用的__block變量(release)


  2. __block修飾的對象類型

    • 當(dāng)__block變量在棧上時,不會對指向的對象產(chǎn)生強(qiáng)引用
    • 當(dāng)__block變量被copy到堆時
    • 會調(diào)用__block變量內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會調(diào)用 _Block_object_assign 函數(shù)
    • _Block_object_assign 函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃╛_strong、__weak、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(注意:這里僅限于ARC時會retain,MRC時不會retain
    • 如果__block變量從堆上移除
    • 會調(diào)用__block變量內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會調(diào)用 _Block_object_dispose 函數(shù)
    • _Block_object_dispose 函數(shù)會自動釋放指向的對象(release)

    eg:

    MJPerson *person = [[MJPerson alloc] init];
    __block __weak MJPerson *weakPerson = person;
    MyBlock block = ^{
        NSLog(@"%p", weakPerson);
    };
    
    __block修飾的對象被block在內(nèi)部包裝成對象的結(jié)構(gòu)體

可以看出,因?yàn)槲覀兪褂胈_weak修飾,所以結(jié)構(gòu)體內(nèi)部對person是__weak弱引用,所以 _Block_object_assign 函數(shù)會根據(jù)對象的修飾符做出相應(yīng)的操作,當(dāng)block調(diào)用dispose函數(shù)時,_Block_object_dispose 函數(shù)會自動釋放指向的對象(release),對象也會調(diào)用自己結(jié)構(gòu)體內(nèi)的 __Block_byref_id_object_dispose 函數(shù)執(zhí)行釋放操作。
如果之前對象是強(qiáng)指針,會執(zhí)行(release)操作,引用計數(shù)為0的話,就會銷毀對象。
如果是__weak 弱引用的話,系統(tǒng)會在其生命周期結(jié)束時正常銷毀。

七. block循環(huán)引用

  1. 循環(huán)引用產(chǎn)生的原因


  1. 用__weak、__unsafe_unretained解決

    • __weak : 不會產(chǎn)生強(qiáng)引用,指向的對象銷毀時,會自動讓指針置為nil
    • __unsafe_unretained: 不會產(chǎn)生強(qiáng)引用,不安全,指向的對象銷毀時,指針存儲的地址值不變,產(chǎn)生懸垂指針,再次使用會造成crash
     __weak typeof(person) weakPerson = person;
    person.block = ^{
        NSLog(@"age is %d", weakPerson.age);
    };
    
    __unsafe_unretained id weakPerson = person;
    person.block = ^{
        NSLog(@"age is %d", weakPerson.age);
    };
    
對對象弱引用
  1. 用__block解決(必須要調(diào)用block)

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

  1. 保證在整個執(zhí)行過程self不會死
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) myself = weakSelf;
        
        NSLog(@"age is %d", myself->_age);
    };
  1. 介紹iOS中@strongify和@weakify
    @weakify 將當(dāng)前對象聲明為weak.. 
    這樣block內(nèi)部引用當(dāng)前對象,就不會造成引用計數(shù)+1可以破解循環(huán)引用
    @strongify 相當(dāng)于聲明一個局部的strong對象,等于當(dāng)前對象.
    可以保證block調(diào)用的時候,內(nèi)部的對象不會釋放
    
    可以理解為和第4個步驟一樣。

    使用時注意,
    只在block外面使用__weak,內(nèi)部沒有__strong這個對象,可能會有問題
    在 block 中先寫一個 strong self,其實(shí)是為了避免在 block 的執(zhí)行過程中,突然出現(xiàn) self 被釋放的尷尬情況。通常情況下,如果不這么做的話,還是很容易出現(xiàn)一些奇怪的邏輯,甚至閃退。

  • 以 AFNetworking 中 AFNetworkReachabilityManager.m 的一段代碼舉例:
    __weak __typeof(self)weakSelf = self;
    AFNetworkReachabilityStatusBlock callback = ^(AFNetworkReachabilityStatus status) {
         __strong __typeof(weakSelf)strongSelf = weakSelf;
    
        strongSelf.networkReachabilityStatus = status;
        if (strongSelf.networkReachabilityStatusBlock) {
         strongSelf.networkReachabilityStatusBlock(status);
        }
    };
    
    • 只使用用__weak 修飾,會造成壞內(nèi)存crash
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        weakSelf.test2Block = ^{
            NSLog(@"123");
        };
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3     * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 模擬testBlock執(zhí)行一半時,再執(zhí)行test2Block,此時 Thread 1: EXC_BAD_ACCESS (code=1, address=0x10)
            weakSelf.test2Block();
        });
    };
    
    • 改進(jìn)方法,添加__strong
    self.testBlock();
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        strongSelf.test2Block = ^{
            NSLog(@"123");
        };
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3     * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            strongSelf.test2Block();
        });
    };
    self.testBlock();
    

如果沒有 strongSelf 的那行代碼,那么后面的每一行代碼執(zhí)行時,self 都可能被釋放掉了,這樣很可能造成邏輯異常。
特別是當(dāng)我們正在執(zhí)行 strongSelf.networkReachabilityStatusBlock(status); 這個 block 閉包時,如果這個 block 執(zhí)行到一半時 self 釋放,那么多半情況下會 Crash。

不會產(chǎn)生循環(huán)引用的情況
  1. block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時

    // UIView動畫
    [UIView animateWithDuration:0.2 animations:^{
        self.alpha = 1;
    }];
    // 快速枚舉
    [self.dataArray enumerateObjectsUsingBlock:^(NSString *str, NSUInteger idx, BOOL * _Nonnull stop) {
        [self dosomething:str];
    }];
    
  2. block作為GCD API的方法參數(shù)時

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"不會產(chǎn)生循環(huán)引用");
    });
    
  3. 類方法
    類似于第一種,自定義類方法,block作為方法參數(shù)時,也不會造成循環(huán)引用,因?yàn)閟elf無法對一個類強(qiáng)引用。

  4. masonry

    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    block并沒有被view引用,block執(zhí)行完畢就會釋放,不會造成循環(huán)引用。

  5. 局部變量
    局部變量的block沒有被其他對象強(qiáng)引用的時候,在當(dāng)前作用域結(jié)束就會銷毀。

  6. AFN
    AFN3.0之前 AFURLConnectionOperation 里的一個請求結(jié)束之后,setCompleteBlock會把block設(shè)置為nil,來打破循環(huán)引用。


    16410253422245.jpg

3.0以后

AFHTTPSessionManager * manager = [AFHTTPSessionManager manager];
[manager POST:urlStr parameters:parm progress:nil success:^(NSURLSessionDataTask * _Nonnull task, NSDictionary *  _Nullable responseObject) {    
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {     
}];

當(dāng)然在項(xiàng)目上一般不會直接使用AFHTTPSessionManager,會封裝一層,我們先看[AFHTTPSessionManager manager], 也就是說每次都相當(dāng)于 [[AFHTTPSessionManager alloc] init], 在函數(shù)中,AFHTTPSessionManager * manager是一個局部變量, 隨著函數(shù)棧的調(diào)用結(jié)束,這個局部變量也就被回收了. self并沒有持有manager對象.

解決循環(huán)引用問題 - MRC
  1. 用__unsafe_unretained解決


    __unsafe_unretained解決
  2. 用__block解決


    __block解決

    上面提到過,MRC下在結(jié)構(gòu)體內(nèi)部__block 不會對對象產(chǎn)生強(qiáng)引用

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Block是一個代碼塊, 類似匿名函數(shù), 是封裝了函數(shù)及其上下文的OC對象,也可以叫做閉包。 閉包就是能夠讀取其它...
    小李不木閱讀 1,403評論 0 1
  • blcok 本質(zhì)上也是個 oc 對象,其內(nèi)部結(jié)構(gòu)也是具有 isa 指針,是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的 OC ...
    Hugin閱讀 1,018評論 4 0
  • 前言 block是日常iOS開發(fā)高頻率使用的閉包,之前也看過不少文章,但是一直疏于總結(jié),今日再次深入研究一下,并記...
    妖精的菩薩閱讀 555評論 0 0
  • 1. Block內(nèi)存管理 OC代碼轉(zhuǎn)換成C++代碼 _block的內(nèi)部要調(diào)用外邊的變量,_block的desc0的...
    switer_iOS閱讀 844評論 0 0
  • Block 這一篇我們來研究一下objc的block并回答一下面試中的下列問題: 1.block的內(nèi)部實(shí)現(xiàn),結(jié)構(gòu)體...
    iOS猿_員閱讀 2,788評論 0 6

友情鏈接更多精彩內(nèi)容