iOS-玩轉(zhuǎn)Block(從入門到底層原理)

  • 還記得當初剛接觸Block的時候觅闽,第一感覺就是覺得語法怪異,只知道就這么寫就對了聋伦,然后稀里糊涂地用了一段時間夫偶,之后發(fā)現(xiàn)在iOS里,Block頻繁使用觉增,比如官方的API大量用到Block來回調(diào)做事情兵拢。經(jīng)過一段漫長歲月的使用和研究才明白Block這個東西遠遠沒有這么簡單。
  • 所以在這里總結(jié)一下我所學(xué)的關(guān)于Block的所有知識點逾礁,畢竟好記性不如爛筆頭说铃,寫下來記憶會更加深刻而且寫的過程會有更多的思考。

我將會從以下方面來講解Block

  • Block的定義
  • Block的基本使用
  • Block的底層數(shù)據(jù)結(jié)構(gòu)
  • Block的類型
  • Block捕獲變量機制
  • __Block修飾符究竟做了什么嘹履?
  • Block內(nèi)存管理
  • Block循環(huán)引用
  • Block交換實現(xiàn)
  • Block相關(guān)面試題
  • ...

Block的定義

Blocks是C語言的擴充功能腻扇。可以用一句話來表示Blocks的擴充功能:帶有自動變量(局部變量)的匿名函數(shù)砾嫉。
顧名思義幼苛,所謂匿名函數(shù)就是不帶有名稱的函數(shù)。
—— 引用自《iOS與OS X多線程和內(nèi)存管理》

也就是說焕刮,Blocks類似于某些語言中的閉包函數(shù)舶沿,以下是block的語法聲明

返回值類型 (^變量名)(參數(shù)列表) = ^ 返回值類型 (參數(shù)列表) 表達式

用代碼來表示就是

void (^block)(void) = ^void (void){};

其中右邊的返回值類型和參數(shù)類型為空的時候可以省略不寫

void (^block)(void) = ^{};

當然,我們也可以利用typedef的特性來定義一個Block

typedef void (^block)(void);

這樣使用起來更方便
比如第三方網(wǎng)絡(luò)框架AFNetworking就通過這種定義方式大量使用Block

typedef void (^AFURLSessionDidBecomeInvalidBlock)(NSURLSession *session, NSError *error);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef NSURLRequest * (^AFURLSessionTaskWillPerformHTTPRedirectionBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLResponse *response, NSURLRequest *request);
typedef NSURLSessionAuthChallengeDisposition (^AFURLSessionTaskDidReceiveAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, NSURLCredential * __autoreleasing *credential);
typedef id (^AFURLSessionTaskAuthenticationChallengeBlock)(NSURLSession *session, NSURLSessionTask *task, NSURLAuthenticationChallenge *challenge, void (^completionHandler)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential));

以上摘自AFNetworking中的AFURLSessionManager


Block的基本使用

block可以作為屬性配并、參數(shù)括荡、返回值等形式使用

  • 一、當block作為屬性時
@property(nonatomic, copy)  void (^NormalBlock)(void);

或者

typedef void (^NormalBlock)(void);

@property(nonatomic, copy)  NormalBlock block;

這種用法最常見的就是平時我們在cell中的響應(yīng)事件的處理荐绝,有時使用block來回調(diào)到VC去處理會更加方便

@interface Cell : UITableViewCell
@property(nonatomic, copy)  void (^clickBlock)(void);
@end

@implementation Cell

- (void)clickAction{
    if(self. clickBlock){
        self. clickBlock();
    }  
}

@end

@interface VC : UIViewController

@end

@implementation VC

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    Cell *cell = [ProGoldRiceRankCell makeCellWithTableView:tableView];
    cell. clickBlock = ^{
    //do anything
    };
    return cell;
}

@end
  • 二一汽、當block作為參數(shù)時
    有時候我們需要從一個方法中返回一個值時,但剛好需要經(jīng)過GCD延時處理后賦值才返回低滩,這種場景用return時不行的召夹,因為GCD中的block返回值類型為空,那么這時候可以用block來回調(diào)返回值恕沫。
typedef void (^NormalBlock)(NSString *value);

- (void)test{
    [self doSomeThingWithBlock:^(NSString *value) {
        NSLog(@"%@",value);
    }];
}

- (void)doSomeThingWithBlock:(NormalBlock)block{
    NSString *value = @"1";
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        value = @"2";
        block(value);
    });
}
  • 三监憎、當block作為返回值時
    我們經(jīng)常使用的Masonry框架內(nèi)部實現(xiàn)就大量用到block返回值來實現(xiàn)鏈式調(diào)用的語法
[_iconImg mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.bottom.right.mas_equalTo(0);
    }];

在這里簡單說一下Masonry鏈式調(diào)用的實現(xiàn)原理(想要看完整源碼解析的可以看這篇iOS開發(fā)之Masonry框架源碼解析,個人覺得寫得非常不錯)

mas_makeConstraints這個方法的實現(xiàn)如下婶溯,可以看到我們平時寫的約束代碼都是通過Block傳參的方式來對MASConstraintMaker進行所有的約束設(shè)置鲸阔,然后再調(diào)用install方法安裝所有約束

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

make.top.left.bottom.right.mas_equalTo(0);這一句鏈式調(diào)用內(nèi)部是這么操作的

  • 通過封裝好各種約束方法的工廠類MASConstraintMaker偷霉,首先調(diào)用top
- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
  • 然后在調(diào)用top后會返回約束抽象類MASConstraint(實際上返回的是MASConstraint的子類MASViewConstraint或者MASCompositeConstraint)
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;//設(shè)為代理
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;//這里返回MASCompositeConstraint類型
    }
    if (!constraint) {
        newConstraint.delegate = self;//設(shè)為代理
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;//這里返回MASViewConstraint類型
}
  • 接著再次調(diào)用left(這次是MASConstraint里的方法)
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
  • MASConstraint通過把MASConstraintMaker設(shè)為代理從而使調(diào)用MASConstraintleft方法傳遞到MASConstraintMaker實現(xiàn)的代理方法里面,然后代理方法又返回約束類MASConstraint本身褐筛,這樣就可以連續(xù)設(shè)置多個約束类少,而且最終都會調(diào)用到最上層工廠類MASConstraintMaker里的方法
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    //調(diào)用代理方法
    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
  • 我們來看mas_equalTooffset
- (MASConstraint * (^)(id))mas_equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

這兩個方法都是MASConstraint里的方法,所以設(shè)置完約束后返回的MASConstraint類可以直接調(diào)用渔扎。
可以看到這兩個方法都返回了一個(返回值為MASConstraint類型的Block)硫狞,所以mas_equalTo(0)相當于(MASConstraint * (^)(id))(0)MASConstraint * (^)(id)看作一個整體Block的話就相當于Block(0)晃痴,這不就是我們平時調(diào)用Block的方法么残吩!然后調(diào)用Block后返回MASConstraint類型,從而可以繼續(xù)調(diào)用下一個方法倘核,這就是Block作為返回值實現(xiàn)鏈式調(diào)用的用法所在泣侮。

正所謂光說(看)不練假功夫,那么現(xiàn)在我們親自實現(xiàn)一個鏈式調(diào)用的例子=舫活尊!
創(chuàng)建一個Student
.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@class Student;

@interface Student : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger tall;
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGSize size;

- (Student * (^)(NSString *))per_name;
- (Student * (^)(int))per_tall;
- (Student * (^)(int))per_age;
- (Student * (^)(CGSize))per_size;
- (Student * (^)(void))run;

@end

NS_ASSUME_NONNULL_END

.m文件

#import "Student.h"

@interface Student ()

@end

@implementation Student


- (Student * (^)(NSString *))per_name{
    return ^ Student * (NSString *name){
        self.name = name;
        return self;
    };
}

- (Student * (^)(int))per_tall{
    return ^ Student * (int tall){
        self.tall = tall;
        return self;
    };
}

- (Student * (^)(int))per_age{
    return ^ Student * (int age){
        self.age = age;
        return self;
    };
}

- (Student * (^)(CGSize))per_size{
    return ^ Student * (CGSize size){
        self.size = size;
        return self;
    };
}

- (Student * (^)(void))run{
    return ^ Student * (void){
        NSLog(@"我在跑步");
        return self;
    };
}

@end

TestVC里使用

- (void)test{
    Student *s = [Student new];
    s.per_name(@"小強")
    .per_tall(173)
    .per_age(18)
    .per_size(CGSizeMake(180, 80))
    .run();
    
    NSLog(@"我是一名學(xué)生,我的名字是%@琼蚯,身高%ld,年齡%ld,尺寸%@",s.name,s.tall,s.age,NSStringFromCGSize(s.size));
}

打印

2020-08-18 12:02:19.315271+0800 CJJFramework[3846:74527] 我在跑步
2020-08-18 12:02:21.422766+0800 CJJFramework[3846:74527] 我是一名學(xué)生酬凳,我的名字是小強,身高173,年齡18,尺寸{180, 80}
(lldb) 

這就是一個簡單的鏈式語法調(diào)用的實現(xiàn)遭庶,簡單太優(yōu)美了有木有宁仔!比oc那繁瑣的對象.調(diào)用簡潔太多了。
順便打個小廣告^-^
iOS-CJJTimer 高性能倒計時工具(短信峦睡、商品秒殺
Github地址
我封裝的一個倒計時工具翎苫,里面也用到了鏈式語法調(diào)用,有興趣的可以看看榨了。


Block的底層數(shù)據(jù)結(jié)構(gòu)

Block本質(zhì)上是一個OC對象煎谍,因為它繼承自NSBlock,而NSBlock又繼承自NSObject龙屉,所以Block內(nèi)部是有一個isa指針的呐粘。
并且,Block是一個封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對象转捕。

  • 函數(shù)調(diào)用
void (^block)(void) = ^{
    NSLog(@"%d",a);
};

通過窺探底層作岖,我們會發(fā)現(xiàn)

NSLog(@"%d",a);

這一句代碼會直接存在于Block中,在Block的初始化方法中五芝,傳遞了一個參數(shù)*fp(最后把函數(shù)的地址傳給了block->impl->FuncPtr)痘儡,這就意味著直接把整段代碼塊傳遞進Block里面存著了(封裝了函數(shù)的地址,屬于引用傳遞)

  • 函數(shù)調(diào)用環(huán)境

Block里面會封裝(存儲)外面?zhèn)鬟M來的自動變量

具體的實現(xiàn)流程接下來會講到:
通過翻看蘋果官方源碼或者直接把oc代碼編譯成底層語言C++代碼枢步,就可以找到以下源碼

  • block的底層結(jié)構(gòu)如下圖所示
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;
  }
};

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

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

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
      NSLog((NSString *)&__NSConstantStringImpl__var_folders_5l_0xn052bn6dgb9z7pfk8bbg740000gn_T_main_88f00d_mi_0);
}

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

從來沒讀過源碼或者不熟悉C++的可能會覺得一臉懵沉删,其實Block可以簡化成以下結(jié)構(gòu)

struct __main_block_impl_0{
    //struct __block_impl impl;  //block的底層信息
    void *isa;//說明block是一個oc對象
    int Flags;
    int Reserved;
    void *FuncPtr;//所封裝的函數(shù)的地址
    //struct __main_block_desc_0* Desc;  //block的描述信息
    size_t reserved;
    size_t Block_size;//block的大小
};

可以看到渐尿,Block的底層數(shù)據(jù)結(jié)構(gòu)就是一個結(jié)構(gòu)體,其簡化后所包含的成員變量如下

  • void *isa //說明block是一個oc對象
  • int Flags // 某些標志矾瑰,蘋果用這個flags與上以下的枚舉值來判斷一些東西
// Values for Block_layout->flags to describe block objects
enum {
    BLOCK_DEALLOCATING =      (0x0001),  // runtime
    BLOCK_REFCOUNT_MASK =     (0xfffe),  // runtime
    BLOCK_NEEDS_FREE =        (1 << 24), // runtime
    BLOCK_HAS_COPY_DISPOSE =  (1 << 25), // compiler
    BLOCK_HAS_CTOR =          (1 << 26), // compiler: helpers have C++ code
    BLOCK_IS_GC =             (1 << 27), // runtime
    BLOCK_IS_GLOBAL =         (1 << 28), // compiler
    BLOCK_USE_STRET =         (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
    BLOCK_HAS_SIGNATURE  =    (1 << 30), // compiler
    BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31)  // compiler
};

比如通過判斷flags & BLOCK_HAS_COPY_DISPOSE來確定是否存在copydispose函數(shù)砖茸,具體后面會講到

if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) {
        desc += sizeof(struct Block_descriptor_2);
    }

以上代碼來自蘋果官方源碼libclosure-74

  • int Reserved //版本升級所需的區(qū)域大小
  • void *FuncPtr //所封裝的函數(shù)的地址
  • size_t reserved //版本升級所需的區(qū)域大小
  • size_t Block_size //block的大小

以及初始化函數(shù)

__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時傳了2個參數(shù),一個是函數(shù)對象的地址impl.FuncPtr = fpfp就是函數(shù)指針(void *)__main_block_func_0)脯倚,另一個是描述對象的地址Desc = desc(desc就是描述信息的地址&__main_block_desc_0_DATA)


Block的類型

Block有3種類型渔彰,可以通過調(diào)用class方法查看其類型以及繼承鏈

  • 1.全局Block(_NSConcreteGlobalBlock)
(__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject)
  • 2.棧Block(_NSConcreteStackBlock)
(__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject)
  • 3.堆Block(_NSConcreteMallocBlock)
(__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject)

為什么Block會有三種類型的呢嵌屎?
這個是由存儲它的內(nèi)存位置決定的推正,下圖展示了在應(yīng)用程序的內(nèi)存中,三種Block所存在的區(qū)域宝惰,也就是說要判斷一個Block是什么類型植榕,就是看它存在于內(nèi)存的哪個區(qū)域。

block類型及存儲

那么如何區(qū)分三種 Block尼夺,它們之間有什么異同點尊残?
以下就是這三種Block的對比

  • NSGlobalBlock
    存儲的位置:程序的數(shù)據(jù)區(qū)域(全局區(qū))
    環(huán)境:沒有訪問auto變量
    copy后的效果:什么也不做

  • NSStackBlock
    存儲的位置:棧
    環(huán)境:訪問了auto變量
    copy后的效果:從棧賦值到堆

  • NSMallocBlock
    存儲的位置:堆
    環(huán)境:NSStackBlock調(diào)用了copy
    copy后的效果:引用計數(shù)增加

舉例

typedef void (^block0)(void)
int val1 = 10;

- (void)test{
    //NSGlobalBlock
    block0 = ^{

    };

    //NSGlobalBlock
    void (^block)(void) = ^{

    };

    //NSGlobalBlock
    void (^block1)(void) = ^{
        NSLog(@"%d",val1);
    };

    //MRC下為NSStackBlock,ARC下為NSMallocBlock(ARC下賦值給會把此Block從棧Copy到堆里)
    int val2 = 20;
    void (^block2)(void) = ^{
        NSLog(@"%d",val2);
    };

    //NSMallocBlock
    __block int val3 = 20;
    void (^block3)(void) = ^{
        NSLog(@"%d",val3);
    };
}

Block捕獲變量機制

眾所周知淤堵,為了保證Block內(nèi)部能夠正常訪問外部的變量寝衫,Block有一個捕獲變量的機制。

Block捕獲變量后相當于往Block結(jié)構(gòu)體里增加一個成員變量拐邪。
首先變量可以分為兩種慰毅,局部變量全局變量
局部變量分為局部(自動)變量局部靜態(tài)變量(static
全局變量分為全局變量全局靜態(tài)變量(static
以下是它們的區(qū)別

  • 局部變量
    • 1.自動變量(意思是扎阶,離開作用范圍就會自動銷毀汹胃,所以叫做自動變量,被Block捕獲時是值傳遞(捕獲的是具體存儲的值))
    {
        auto int a = 0;
    }
    
    • 2.局部靜態(tài)變量(會在內(nèi)存中一直存在东臀,被Block捕獲時是引用傳遞(捕獲的是變量的地址))
    {
       static int a = 0;
     }
    
  • 全局變量(會在內(nèi)存中一直存在着饥,不會被Block捕獲)
    • 全局變量
    int a = 0;
    
    • 全局靜態(tài)變量,只能在本文件訪問惰赋,不能在外部extern
    static int b = 0;
    

總結(jié):只有局部變量才會被Block捕獲宰掉,全局變量不會被捕獲

為什么全局變量不用捕獲?

因為隨時可以訪問

為什么局部變量需要捕獲赁濒?

作用域的問題轨奄,在Block里面使用Block外聲明的局部變量,相當于跨函數(shù)使用這個局部變量流部,如果不存一份到Block里面戚绕,是無法使用的,會造成訪問無效內(nèi)存枝冀,因為外面的局部變量有可能過了作用域就會自動被銷毀
例如

typedef void (^Block)(void);

@property(nonatomic, copy) Block block;

- (void)test{
    int a = 0;
    self.block = ^{
        NSLog(@"%d",a);
    };
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self test];
    self.block();
}

以上這段代碼舞丛,當點擊self.view時會響應(yīng)touchBegin耘子,然后調(diào)用test焕议,test里面創(chuàng)建了一個局部自動變量a佛猛,然后初始化了self.block變量,里面使用了a番舆,但是調(diào)用完test后吨凑,a就會銷毀捍歪,然后才調(diào)用Block,這時候Block里面再使用a鸵钝,如果不事先捕獲(存一份)糙臼,就會崩潰,訪問無效內(nèi)存恩商,這就是為什么局部變量需要捕獲变逃,而全局變量不需要捕獲的根本原因。

還有一個特殊情況怠堪,self會被捕獲嗎揽乱?
- (void)test{
    self.block = ^{
        NSLog(@"%p",self);
    };
}

會,因為self也是局部變量粟矿,我們來回想一下凰棉,在OC里調(diào)用方法實際上會傳遞self指針的參數(shù),而且捕獲的是指針,所以屬于引用傳遞陌粹。

objc_msgSend(id self, SEL _cmd, ...)

所以我們之所以能在每一個方法中使用self撒犀,就是因為默認傳入self變量

另一個特殊情況,成員變量會被捕獲嗎申屹?
@property(nonatomic, copy) NSString *name;

- (void)test{
    self.block = ^{
        NSLog(@"%@",_name);
    };
}

會绘证,因為這里訪問的成員變量也是局部變量,相當于

- (void)test{
    self.block = ^{
        NSLog(@"%@",self->_name);
    };
}

__Block修飾符究竟做了什么哗讥?

我們來看下面這一段代碼

int val = 10;
void (^block)(void) = ^{
    val = 20;//這個是錯誤的嚷那,不能通過編譯的,因為val是自動局部變量杆煞,過了作用域就銷毀
//而這里是在另一個椢嚎恚空間,不能訪問val
};

那么如何使得變量val可以更改呢决乎?
有幾種辦法
可以把變量val修飾為全局變量或者靜態(tài)變量队询,而更好的辦法是用__block修飾符修飾

__block修飾符

  • __block可以用于解決Block內(nèi)部無法修改auto變量值的問題
  • __block不能修飾全局變量、靜態(tài)變量(static
  • 編譯器會將__block變量包裝成一個對象(__Block_byref_age_0類型)

比如說這一段

__block int val = 1;
int (^block)(CGFloat num) = ^ int (CGFloat num){
    NSLog(@"這是一個Block");
    val = 2;
    return val;
};

編譯成C++代碼如下构诚,我整理了一下格式方便查看

__attribute__((__blocks__(byref))) __Block_byref_ val_0 val =
{
  (void*)0,//void *__isa
  (__Block_byref_ val_0 *)& val,//__Block_byref_val_0 *__forwarding
  0,//int __flags
  sizeof(__Block_byref_val_0),//int __size
  1 //int val
};
int (*block)(CGFloat num) = (
  (int (*)(CGFloat))
  &__main_block_impl_0(
    (void *)__main_block_func_0, //
    &__main_block_desc_0_DATA, 
    (__Block_byref_val_0 *)& val, 
    570425344
  )
);

自動變量val__block修飾后會包裝成__Block_byref_val_0對象蚌斩,也就是說Block__main_block_impl_0結(jié)構(gòu)體實例持有指向__block變量的__Block_byref_val_0結(jié)構(gòu)體實例的指針。

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

struct __Block_byref_val_0{
    void *__isa;
    __Block_byref_age_0 *__forwarding;//這個指針指向該對象自身的地址
    int __flags;
    int __size;
    int val;
};

會發(fā)現(xiàn)里面也有一個val范嘱,其實這里的val才是Block捕獲進來的那個val送膳。
還有一個成員變量__forwarding
而且__main_block_impl_0里的__Block_byref_val_0變量并不是存在于Block結(jié)構(gòu)體里面员魏,Block只是保存了一個引用了__Block_byref_val_0變量地址的指針,這樣就可以在多個不同的Block里面訪問同一個__block變量了叠聋。

訪問__block變量

看著這個圖可能會有疑問了撕阎。
為什么不能直接在Block結(jié)構(gòu)體里面存儲val,而要搞這么麻煩碌补,生成一個val結(jié)構(gòu)體虏束,然后把val變量存放到里面呢?

  • 我的理解是厦章,因為直接在Block中存儲val變量的話镇匀,是在棧上存儲的,等變量作用域過去之后變量就會銷毀闷袒,這樣就無法在Block里訪問該變量了坑律;而通過把其包裝成一個__Block_byref_val_0類型的對象,把該變量保存在對象里囊骤,當Block從棧copy到堆上的時候,相當于__block變量也從棧copy到堆里存了一份冀值,這樣作用域過了之后也物,Block仍然可以訪問val變量,而在copy的過程中列疗,棧上的__block變量中的__forwarding指針會變?yōu)橹赶蚨焉系?code>__block變量的結(jié)構(gòu)體實例的地址滑蚯,而通過這種方式,無論是在Block語法中抵栈、Block語法外使用__block變量告材,還是__block變量配置在棧上或堆上,都可以順利地訪問同一個__block變量古劲。
    賦值__block變量

Block內(nèi)存管理

如果Block捕獲了對象類型的auto變量會怎么樣斥赋?

實際上只是多了內(nèi)存管理方面的操作。
Block經(jīng)過copy之后會在desc里生成的2個函數(shù)

  • copy函數(shù)
    調(diào)用時機 棧上的Block復(fù)制到堆時
  • dispose函數(shù)
    調(diào)用時機 堆上的Block被廢棄時

Block內(nèi)部訪問了帶有__block修飾符的對象類型的auto變量時

  • block在棧上時产艾,并不會對__block變量產(chǎn)生強引用

  • blockcopy到堆時

    • 會調(diào)用block內(nèi)部的copy函數(shù)
    • copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)
    • _Block_object_assign函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃?code>__strong, __weak, __unsafe_unretained)做出相應(yīng)的操作疤剑,形成強引用(retain)或者弱引用(注意:這里僅限于ARC時會retain,MRC時不會retain
      __block持有對象
  • block從堆中移除時

    • 會調(diào)用block內(nèi)部的dispose函數(shù)
    • dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
    • _Block_object_dispose函數(shù)會自動釋放引用的__block變量(release)
block移除對象

對象類型的auto變量闷堡、__block變量

//auto
{
    (auto) Person *person = [Person new];
    void (^block)(void) = ^{
        NSLog(@"%@",person);
    };
}

//__block
{
    __block Person *person = [Person new];
    void (^block)(void) = ^{
        NSLog(@"%@",person);
    };
}

  • block在棧上時隘膘,對它們都不會產(chǎn)生強引用
  • block拷貝到堆上時,都會通過copy函數(shù)來處理它們
//傳8和3來區(qū)別這兩種變量
//__block變量(假設(shè)變量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//對象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
  • block從堆上移除時杠览,都會通過dispose函數(shù)來釋放它們
//__block變量(假設(shè)變量名叫做a)
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
//對象類型的auto變量(假設(shè)變量名叫做p)
_Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

Block循環(huán)引用

有一個對象A弯菊,一個Block
A強引用了BlockBlock也強引用了A踱阿,這種情況就是循環(huán)引用管钳,造成內(nèi)存泄漏吨悍。
用代碼表示就是

@interface A : NSObject
@property(nonatomic, copy) void (^block)(void);
@end

@implementation

- (void)viewDidLoad{
    [super viewDidLoad];
    self.block = ^{
        NSLog(@"%@",self);
    };
}

@end

如上,self持有block屬性蹋嵌,然后block里持有self育瓜,互相強引用,造成誰也釋放不了栽烂,這只是最簡單的一種情況躏仇,實際上平時遇到得有可能比這種復(fù)雜得多,有自引用循環(huán)(A->A)腺办,雙向引用循環(huán)(A->B->A)焰手,多引用循環(huán)(A->B->C->A)等等,但是只要我們清楚了引用循環(huán)的本質(zhì)怀喉,這些情況其實都很容易發(fā)現(xiàn)并解決书妻,我們只要切斷引用鏈中隨意一方的強引用就可以解決引用循環(huán)的問題。

解決方案
ARCMRC下解決循環(huán)引用的方式各有不同躬拢。
ARC下躲履,可以使用__weak__unsafe_unretained聊闯、__block三種方式解決

//__weak
__weak typeof(self) weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

//__block
//因為ARC下__block會使得Block內(nèi)部強引用外部的變量
//所以需要調(diào)用Block并且手動把變量置空(nil)
__block id weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
    weakSelf = nil;
};
self.block();

MRC下工猜,可以使用__unsafe_unretained__block解決

//__unsafe_unretained
__unsafe_unretained id weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

//__block
__block id weakSelf = self;
self.block = ^{
    NSLog(@"%p",weakSelf);
};

綜上菱蔬,最好的方法是ARC下用__weak篷帅,MRC下用__unsafe_unretained

  • 還有一種情況
    如果要在block里面訪問成員變量的話
@interface A : NSObject
{
    NSString *name;
}
@property (nonatomic, copy) void (^block)(void);
@property (nonatomic, copy) NSString *address;
@end

@implementation A

- (void)testMethod {
    name = @"名字";
    
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        //一定要加上這一句才能訪問name拴泌,不然weakSelf->name會報錯Dereferencing a __weak pointer is not allowed due to possible null value caused by race condition, assign it to strong variable first
        __strong __typeof(weakSelf)strongSelf = weakSelf; 
        NSLog(@"%@-%@", strongSelf->name, weakSelf.address);
    };
    
    self.block();
}

@end

分析:
問題一:為什么成員變量name要加上__strong修飾一下才能訪問呢魏身?
答1:因為weakself有可能在block執(zhí)行過程中就釋放了,也就是weakself指針置為nil蚪腐,一旦釋放再使用nil指針去訪問成員變量拿到的值也為nil

問題二:而address卻可以直接用weakSelf訪問weakSelf.address?
答2:因為weakSelf.address是調(diào)用addressgetter方法箭昵,而不是直接訪問成員變量,即使weakself釋放了削茁,也不一定會影響使用宙枷,因為nil訪問getter方法無效,給空對象發(fā)消息是不會生效的茧跋。

另外

在平時開發(fā)中慰丛,我發(fā)現(xiàn)有一些同事看到只要有block的地方就使用weakself,即使是工作了三四年的瘾杭,也有這種問題诅病,實際上就是沒有搞懂引用循環(huán)的本質(zhì),下面舉幾個block里使用self不需要弱引用的例子

例子一:控制器沒有強引用block,block強引用self(不需要weakself)

@interface A : NSObject

@end

@implementation

- (void)viewDidLoad{
    [super viewDidLoad];
    id block = ^{
        NSLog(@"%@",self);
    };
}

@end

分析:self不持有block贤笆,block持有self蝇棉,不構(gòu)成雙向的循環(huán)引用,所以不需要weakself

例子二:類方法的block(不需要weakself)

@interface A : NSObject

@end

@implementation

- (void)viewDidLoad{
    [super viewDidLoad];
    [UIView animateWithDuration:duration animations:^{
            NSLog(@"%@",self);
     }];

}

@end

分析:同例子一

例子三:AFNetworking的請求方法的回調(diào)block(不需要weakself)

[[AFNetWorkManager sharedManager] requestWithUserMethod:POST Url:url parameters:paramsDic success:^(NetWorkResultModel * _Nonnull resultModel) {
    NSLog(@"%@",self);
    } failure:^(NSError * _Nonnull error) {
}];

分析:首先大多數(shù)封裝了AFN的都是使用單例芥永,正常情況下篡殷,如果單例持有了self,是會造成釋放不了self的埋涧,因為除非人為釋放板辽,否則單例會在內(nèi)存中一直存在,那么這里的AFNblock引用了self為什么不需要weakself呢棘催,是因為AFN內(nèi)部已經(jīng)做了處理劲弦,在請求結(jié)束之后移除了對block的引用,所以在這種情況下是不需要使用weakself的醇坝。

strongself

我們經(jīng)常會使用(weakself + strongself)搭配使用

__weak __typeof(self) weakself = self;
self.block = ^{
      __strong __typeof(self) strongSelf = weakself;
};

作用就是防止在block執(zhí)行過程中使用了self邑跪,但是self已經(jīng)銷毀的情況
比如

@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", weakSelf);
            [weakSelf dataCollect];
        });
    };
}

- (void)dealloc {
    NSLog(@"TestViewController銷毀");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.testBlock();
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dataCollect {
    NSLog(@"發(fā)送埋點");
}

@end
2022-04-13 11:13:55.398826+0800 Test[38751:1052715] TestViewController銷毀
2022-04-13 11:14:00.395086+0800 Test[38751:1052715] (null)

當我們點擊view的時候,調(diào)用了block的同時觸發(fā)了dismiss呼猪,這時候由于沒有地方對self有強引用画畅,所以就會走dealloc方法,等5s之后再觸發(fā)GCD里面的代碼時郑叠,weakself已經(jīng)為nil夜赵,所以無法調(diào)用dataCollect
但如果我們在block里面使用strongself乡革,重新強引用self對象,那么就可以延長self的生命周期

@interface TestViewController ()
@property (nonatomic, copy) void (^testBlock)(void);
@end

@implementation TestViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.testBlock = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@", strongSelf);
            [strongSelf dataCollect];
        });
    };
}

- (void)dealloc {
    NSLog(@"TestViewController銷毀");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    self.testBlock();
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dataCollect {
    NSLog(@"發(fā)送埋點");
}

@end
2022-04-13 11:16:13.391062+0800 Test[38821:1055142] <TestViewController: 0x14d627d70>
2022-04-13 11:16:13.391154+0800 Test[38821:1055142] 發(fā)送埋點
2022-04-13 11:16:13.391199+0800 Test[38821:1055142] TestViewController銷毀

當然摊腋,只要你結(jié)合上下文分析出不會出現(xiàn)以上這種情況沸版,只使用weakself也沒問題。


Block交換實現(xiàn)

由于這一主題內(nèi)容太多兴蒸,所以另開一篇來談?wù)?br> 如何去hook一個block的實現(xiàn)视粮?
傳送門->iOS-玩轉(zhuǎn)Block(Hook Block 交換block的實現(xiàn))


Block相關(guān)面試題

  • 一、Block的原理是怎樣的橙凳?本質(zhì)是什么蕾殴?
    封裝了函數(shù)調(diào)用以及調(diào)用環(huán)境的OC對象。(有待補充岛啸,結(jié)合實際面試情況自由發(fā)揮)

  • 二钓觉、__block的作用是什么?有什么使用注意點坚踩?
    本質(zhì):把變量包裝成一個對象
    作用:可以解決Block內(nèi)部無法修改auto變量值的問題
    使用注意:內(nèi)存管理問題荡灾,在MRC__block修飾內(nèi)部不會對對象產(chǎn)生強引用(retain);ARC下會,需要避免循環(huán)引用批幌。

  • 三础锐、Block的屬性修飾詞為什么是copy?使用Block有哪些使用注意荧缘?
    原因:Block一旦沒有進行copy操作皆警,就不會在堆上,所以通過copy到堆上我們可以對Block進行內(nèi)存管理
    使用注意:循環(huán)引用問題
    另外:ARC下用StrongCopy是一樣的截粗,都會把Block copy到堆里面信姓,MRC下只能用Copy,所以結(jié)合兩種情況桐愉,用Copy是最好的

  • 四财破、Block在修改NSMutableArray,需不需要添加__block?
    不需要从诲,因為只是對數(shù)組操作內(nèi)容左痢,并不是修改他的內(nèi)存地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市系洛,隨后出現(xiàn)的幾起案子俊性,更是在濱河造成了極大的恐慌,老刑警劉巖描扯,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件定页,死亡現(xiàn)場離奇詭異,居然都是意外死亡绽诚,警方通過查閱死者的電腦和手機典徊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恩够,“玉大人卒落,你說我怎么就攤上這事》渫埃” “怎么了儡毕?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長扑媚。 經(jīng)常有香客問我腰湾,道長,這世上最難降的妖魔是什么疆股? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任费坊,我火速辦了婚禮,結(jié)果婚禮上押桃,老公的妹妹穿的比我還像新娘葵萎。我一直安慰自己导犹,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布羡忘。 她就那樣靜靜地躺著谎痢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卷雕。 梳的紋絲不亂的頭發(fā)上节猿,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音漫雕,去河邊找鬼滨嘱。 笑死,一個胖子當著我的面吹牛浸间,可吹牛的內(nèi)容都是我干的太雨。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼魁蒜,長吁一口氣:“原來是場噩夢啊……” “哼囊扳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起兜看,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤锥咸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后细移,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體搏予,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年弧轧,在試婚紗的時候發(fā)現(xiàn)自己被綠了雪侥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡精绎,死狀恐怖校镐,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捺典,我是刑警寧澤,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布从祝,位于F島的核電站襟己,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏牍陌。R本人自食惡果不足惜擎浴,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望毒涧。 院中可真熱鬧贮预,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至唤冈,卻和暖如春峡迷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背你虹。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工绘搞, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人傅物。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓夯辖,卻偏偏與公主長得像,于是被迫代替她去往敵國和親董饰。 傳聞我的和親對象是個殘疾皇子蒿褂,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354