iOS 收藏篇傲视苍穹iOS《Objective-C》VIP专题夯实基础

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

2020-09-01  本文已影响0人  JimmyCJJ

前方极其烧脑,建议->点赞再看



我将会从以下方面来讲解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可以作为属性、参数、返回值等形式使用

@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
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);
    });
}
[_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);这一句链式调用内部是这么操作的

- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (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类型
}
- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (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];
}
- (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 <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(@"我是一名学生,我的名字是%@,身高%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++代码,就可以找到以下源码

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的底层数据结构就是一个结构体,其简化后所包含的成员变量如下

// 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

以及初始化函数

__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方法查看其类型以及继承链

(__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject)
(__NSStackBlock__ : __NSStackBlock : NSBlock : NSObject)
(__NSMallocBlock__ : __NSMallocBlock : NSBlock : NSObject)

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

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

举例

type 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
以下是它们的区别

总结:只有局部变量才会被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<UITouch *> *)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 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内存管理

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

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

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

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

//传8和3来区别这两种变量
//__block变量(假设变量名叫做a)
_Block_object_assign((void*)&dst->a, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
//对象类型的auto变量(假设变量名叫做p)
_Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);
//__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交换实现

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


Block相关面试题

上一篇下一篇

猜你喜欢

热点阅读