iOS-玩转Block(从入门到底层原理)

  • 还记得当初刚接触Block的时候,第一感觉就是觉得语法怪异,只知道就这么写就对了,然后稀里糊涂地用了一段时间,之后发现在iOS里,Block频繁使用,比如官方的API大量用到Block来回调做事情。经过一段漫长岁月的使用和研究才明白Block这个东西远远没有这么简单。
  • 所以在这里总结一下我所学的关于Block的所有知识点,毕竟好记性不如烂笔头,写下来记忆会更加深刻而且写的过程会有更多的思考。

我将会从以下方面来讲解Block

  • Block的定义
  • Block的基本使用
  • Block的底层数据结构
  • Block的类型
  • Block捕获变量机制
  • __Block修饰符究竟做了什么?
  • Block内存管理
  • Block循环引用
  • Block交换实现
  • Block相关面试题
  • ...

Block的定义

Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。
顾名思义,所谓匿名函数就是不带有名称的函数。
—— 引用自《iOS与OS X多线程和内存管理》

也就是说,Blocks类似于某些语言中的闭包函数,以下是block的语法声明

返回值类型 (^变量名)(参数列表) = ^ 返回值类型 (参数列表) 表达式

用代码来表示就是

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

其中右边的返回值类型和参数类型为空的时候可以省略不写

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

当然,我们也可以利用typedef的特性来定义一个Block

typedef void (^block)(void);

这样使用起来更方便
比如第三方网络框架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可以作为属性、参数、返回值等形式使用

  • 一、当block作为属性时
@property(nonatomic, copy)  void (^NormalBlock)(void);

或者

typedef void (^NormalBlock)(void);

@property(nonatomic, copy)  NormalBlock block;

这种用法最常见的就是平时我们在cell中的响应事件的处理,有时使用block来回调到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作为参数时
    有时候我们需要从一个方法中返回一个值时,但刚好需要经过GCD延时处理后赋值才返回,这种场景用return时不行的,因为GCD中的block返回值类型为空,那么这时候可以用block来回调返回值。
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作为返回值时
    我们经常使用的Masonry框架内部实现就大量用到block返回值来实现链式调用的语法
[_iconImg mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.left.bottom.right.mas_equalTo(0);
    }];

在这里简单说一下Masonry链式调用的实现原理(想要看完整源码解析的可以看这篇iOS开发之Masonry框架源码解析,个人觉得写得非常不错)

mas_makeConstraints这个方法的实现如下,可以看到我们平时写的约束代码都是通过Block传参的方式来对MASConstraintMaker进行所有的约束设置,然后再调用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);这一句链式调用内部是这么操作的

  • 通过封装好各种约束方法的工厂类MASConstraintMaker,首先调用top
- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
  • 然后在调用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;//设为代理
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;//这里返回MASCompositeConstraint类型
    }
    if (!constraint) {
        newConstraint.delegate = self;//设为代理
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;//这里返回MASViewConstraint类型
}
  • 接着再次调用left(这次是MASConstraint里的方法)
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
  • MASConstraint通过把MASConstraintMaker设为代理从而使调用MASConstraintleft方法传递到MASConstraintMaker实现的代理方法里面,然后代理方法又返回约束类MASConstraint本身,这样就可以连续设置多个约束,而且最终都会调用到最上层工厂类MASConstraintMaker里的方法
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    //调用代理方法
    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里的方法,所以设置完约束后返回的MASConstraint类可以直接调用。
可以看到这两个方法都返回了一个(返回值为MASConstraint类型的Block),所以mas_equalTo(0)相当于(MASConstraint * (^)(id))(0)MASConstraint * (^)(id)看作一个整体Block的话就相当于Block(0),这不就是我们平时调用Block的方法么!然后调用Block后返回MASConstraint类型,从而可以继续调用下一个方法,这就是Block作为返回值实现链式调用的用法所在。

正所谓光说(看)不练假功夫,那么现在我们亲自实现一个链式调用的例子!!
创建一个Student
.h文件

#import 

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(@"我是一名学生,我的名字是%@,身高%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] 我是一名学生,我的名字是小强,身高173,年龄18,尺寸{180, 80}
(lldb) 

这就是一个简单的链式语法调用的实现,简单太优美了有木有!比oc那繁琐的对象.调用简洁太多了。
顺便打个小广告^-^
iOS-CJJTimer 高性能倒计时工具(短信、商品秒杀
Github地址
我封装的一个倒计时工具,里面也用到了链式语法调用,有兴趣的可以看看。


Block的底层数据结构

Block本质上是一个OC对象,因为它继承自NSBlock,而NSBlock又继承自NSObject,所以Block内部是有一个isa指针的。
并且,Block是一个封装了函数调用以及函数调用环境的OC对象。

  • 函数调用
void (^block)(void) = ^{
    NSLog(@"%d",a);
};

通过窥探底层,我们会发现

NSLog(@"%d",a);

这一句代码会直接存在于Block中,在Block的初始化方法中,传递了一个参数*fp(最后把函数的地址传给了block->impl->FuncPtr),这就意味着直接把整段代码块传递进Block里面存着了(封装了函数的地址,属于引用传递)

  • 函数调用环境

Block里面会封装(存储)外面传进来的自动变量

具体的实现流程接下来会讲到:
通过翻看苹果官方源码或者直接把oc代码编译成底层语言C++代码,就可以找到以下源码

  • block的底层结构如下图所示
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可以简化成以下结构

struct __main_block_impl_0{
    //struct __block_impl impl;  //block的底层信息
    void *isa;//说明block是一个oc对象
    int Flags;
    int Reserved;
    void *FuncPtr;//所封装的函数的地址
    //struct __main_block_desc_0* Desc;  //block的描述信息
    size_t reserved;
    size_t Block_size;//block的大小
};

可以看到,Block的底层数据结构就是一个结构体,其简化后所包含的成员变量如下

  • 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函数,具体后面会讲到

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

以上代码来自苹果官方源码libclosure-74

  • int Reserved //版本升级所需的区域大小
  • void *FuncPtr //所封装的函数的地址
  • size_t reserved //版本升级所需的区域大小
  • size_t Block_size //block的大小

以及初始化函数

__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个参数,一个是函数对象的地址impl.FuncPtr = fpfp就是函数指针(void *)__main_block_func_0),另一个是描述对象的地址Desc = desc(desc就是描述信息的地址&__main_block_desc_0_DATA)


Block的类型

Block有3种类型,可以通过调用class方法查看其类型以及继承链

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

为什么Block会有三种类型的呢?
这个是由存储它的内存位置决定的,下图展示了在应用程序的内存中,三种Block所存在的区域,也就是说要判断一个Block是什么类型,就是看它存在于内存的哪个区域。

block类型及存储

那么如何区分三种 Block,它们之间有什么异同点?
以下就是这三种Block的对比

  • NSGlobalBlock
    存储的位置:程序的数据区域(全局区)
    环境:没有访问auto变量
    copy后的效果:什么也不做

  • NSStackBlock
    存储的位置:栈
    环境:访问了auto变量
    copy后的效果:从栈赋值到堆

  • NSMallocBlock
    存储的位置:堆
    环境:NSStackBlock调用了copy
    copy后的效果:引用计数增加

举例

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内部能够正常访问外部的变量,Block有一个捕获变量的机制。

Block捕获变量后相当于往Block结构体里增加一个成员变量。
首先变量可以分为两种局部变量全局变量
局部变量分为局部(自动)变量局部静态变量(static
全局变量分为全局变量全局静态变量(static
以下是它们的区别

  • 局部变量
    • 1.自动变量(意思是,离开作用范围就会自动销毁,所以叫做自动变量,被Block捕获时是值传递(捕获的是具体存储的值))
    {
        auto int a = 0;
    }
    
    • 2.局部静态变量(会在内存中一直存在,被Block捕获时是引用传递(捕获的是变量的地址))
    {
       static int a = 0;
     }
    
  • 全局变量(会在内存中一直存在,不会被Block捕获)
    • 全局变量
    int a = 0;
    
    • 全局静态变量,只能在本文件访问,不能在外部extern
    static int b = 0;
    

总结:只有局部变量才会被Block捕获,全局变量不会被捕获

为什么全局变量不用捕获?

因为随时可以访问

为什么局部变量需要捕获?

作用域的问题,在Block里面使用Block外声明的局部变量,相当于跨函数使用这个局部变量,如果不存一份到Block里面,是无法使用的,会造成访问无效内存,因为外面的局部变量有可能过了作用域就会自动被销毁
例如

typedef void (^Block)(void);

@property(nonatomic, copy) Block block;

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

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

以上这段代码,当点击self.view时会响应touchBegin,然后调用testtest里面创建了一个局部自动变量a,然后初始化了self.block变量,里面使用了a,但是调用完test后,a就会销毁,然后才调用Block,这时候Block里面再使用a,如果不事先捕获(存一份),就会崩溃,访问无效内存,这就是为什么局部变量需要捕获,而全局变量不需要捕获的根本原因。

还有一个特殊情况,self会被捕获吗?
- (void)test{
    self.block = ^{
        NSLog(@"%p",self);
    };
}

会,因为self也是局部变量,我们来回想一下,在OC里调用方法实际上会传递self指针的参数,而且捕获的是指针,所以属于引用传递。

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修饰为全局变量或者静态变量,而更好的办法是用__block修饰符修饰

__block修饰符

  • __block可以用于解决Block内部无法修改auto变量值的问题
  • __block不能修饰全局变量、静态变量(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结构体实例持有指向__block变量的__Block_byref_val_0结构体实例的指针。

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

会发现里面也有一个val,其实这里的val才是Block捕获进来的那个val
还有一个成员变量__forwarding
而且__main_block_impl_0里的__Block_byref_val_0变量并不是存在于Block结构体里面,Block只是保存了一个引用了__Block_byref_val_0变量地址的指针,这样就可以在多个不同的Block里面访问同一个__block变量了。

访问__block变量

看着这个图可能会有疑问了。
为什么不能直接在Block结构体里面存储val,而要搞这么麻烦,生成一个val结构体,然后把val变量存放到里面呢?

  • 我的理解是,因为直接在Block中存储val变量的话,是在栈上存储的,等变量作用域过去之后变量就会销毁,这样就无法在Block里访问该变量了;而通过把其包装成一个__Block_byref_val_0类型的对象,把该变量保存在对象里,当Block从栈copy到堆上的时候,相当于__block变量也从栈copy到堆里存了一份,这样作用域过了之后,Block仍然可以访问val变量,而在copy的过程中,栈上的__block变量中的__forwarding指针会变为指向堆上的__block变量的结构体实例的地址,而通过这种方式,无论是在Block语法中、Block语法外使用__block变量,还是__block变量配置在栈上或堆上,都可以顺利地访问同一个__block变量。
    赋值__block变量

Block内存管理

如果Block捕获了对象类型的auto变量会怎么样?

实际上只是多了内存管理方面的操作。
Block经过copy之后会在desc里生成的2个函数

  • copy函数
    调用时机 栈上的Block复制到堆时
  • dispose函数
    调用时机 堆上的Block被废弃时

Block内部访问了带有__block修饰符的对象类型的auto变量时

  • block在栈上时,并不会对__block变量产生强引用

  • blockcopy到堆时

    • 会调用block内部的copy函数
    • copy函数内部会调用_Block_object_assign函数
    • _Block_object_assign函数会根据所指向对象的修饰符(__strong, __weak, __unsafe_unretained)做出相应的操作,形成强引用(retain)或者弱引用(注意:这里仅限于ARC时会retain,MRC时不会retain
      __block持有对象
  • block从堆中移除时

    • 会调用block内部的dispose函数
    • dispose函数内部会调用_Block_object_dispose函数
    • _Block_object_dispose函数会自动释放引用的__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在栈上时,对它们都不会产生强引用
  • block拷贝到堆上时,都会通过copy函数来处理它们
//传8和3来区别这两种变量
//__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*/);
  • 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*/);

Block循环引用

有一个对象A,一个Block
A强引用了BlockBlock也强引用了A,这种情况就是循环引用,造成内存泄漏。
用代码表示就是

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

@implementation

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

@end

如上,self持有block属性,然后block里持有self,互相强引用,造成谁也释放不了,这只是最简单的一种情况,实际上平时遇到得有可能比这种复杂得多,有自引用循环(A->A),双向引用循环(A->B->A),多引用循环(A->B->C->A)等等,但是只要我们清楚了引用循环的本质,这些情况其实都很容易发现并解决,我们只要切断引用链中随意一方的强引用就可以解决引用循环的问题。

解决方案
ARCMRC下解决循环引用的方式各有不同。
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内部强引用外部的变量
//所以需要调用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下用__weakMRC下用__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执行过程中就释放了,也就是weakself指针置为nil,一旦释放再使用nil指针去访问成员变量拿到的值也为nil

问题二:而address却可以直接用weakSelf访问weakSelf.address?
答2:因为weakSelf.address是调用addressgetter方法,而不是直接访问成员变量,即使weakself释放了,也不一定会影响使用,因为nil访问getter方法无效,给空对象发消息是不会生效的。

另外

在平时开发中,我发现有一些同事看到只要有block的地方就使用weakself,即使是工作了三四年的,也有这种问题,实际上就是没有搞懂引用循环的本质,下面举几个block里使用self不需要弱引用的例子

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

@interface A : NSObject

@end

@implementation

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

@end

分析:self不持有block,block持有self,不构成双向的循环引用,所以不需要weakself

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

@interface A : NSObject

@end

@implementation

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

}

@end

分析:同例子一

例子三:AFNetworking的请求方法的回调block(不需要weakself)

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

分析:首先大多数封装了AFN的都是使用单例,正常情况下,如果单例持有了self,是会造成释放不了self的,因为除非人为释放,否则单例会在内存中一直存在,那么这里的AFNblock引用了self为什么不需要weakself呢,是因为AFN内部已经做了处理,在请求结束之后移除了对block的引用,所以在这种情况下是不需要使用weakself的。

strongself

我们经常会使用(weakself + strongself)搭配使用

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

作用就是防止在block执行过程中使用了self,但是self已经销毁的情况
比如

@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 *)touches withEvent:(UIEvent *)event {
    self.testBlock();
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dataCollect {
    NSLog(@"发送埋点");
}

@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的时候,调用了block的同时触发了dismiss,这时候由于没有地方对self有强引用,所以就会走dealloc方法,等5s之后再触发GCD里面的代码时,weakself已经为nil,所以无法调用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 *)touches withEvent:(UIEvent *)event {
    self.testBlock();
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)dataCollect {
    NSLog(@"发送埋点");
}

@end
2022-04-13 11:16:13.391062+0800 Test[38821:1055142] 
2022-04-13 11:16:13.391154+0800 Test[38821:1055142] 发送埋点
2022-04-13 11:16:13.391199+0800 Test[38821:1055142] TestViewController销毁

当然,只要你结合上下文分析出不会出现以上这种情况,只使用weakself也没问题。


Block交换实现

由于这一主题内容太多,所以另开一篇来谈谈
如何去hook一个block的实现?
传送门->iOS-玩转Block(Hook Block 交换block的实现)


Block相关面试题

  • 一、Block的原理是怎样的?本质是什么?
    封装了函数调用以及调用环境的OC对象。(有待补充,结合实际面试情况自由发挥)

  • 二、__block的作用是什么?有什么使用注意点?
    本质:把变量包装成一个对象
    作用:可以解决Block内部无法修改auto变量值的问题
    使用注意:内存管理问题,在MRC__block修饰内部不会对对象产生强引用(retain);ARC下会,需要避免循环引用。

  • 三、Block的属性修饰词为什么是copy?使用Block有哪些使用注意?
    原因:Block一旦没有进行copy操作,就不会在堆上,所以通过copy到堆上我们可以对Block进行内存管理
    使用注意:循环引用问题
    另外:ARC下用StrongCopy是一样的,都会把Block copy到堆里面,MRC下只能用Copy,所以结合两种情况,用Copy是最好的

  • 四、Block在修改NSMutableArray,需不需要添加__block?
    不需要,因为只是对数组操作内容,并不是修改他的内存地址

你可能感兴趣的:(iOS-玩转Block(从入门到底层原理))