《iOS知识点梳理-Block》

知识点总结中,如有幸被您观看,更有可能看到不足,期待指出交流

前言

最近放了很多假期,感觉身体得到了很好的休息.

Block底层原理实现

首先我们看四个函数

void test1()
{
    int a = 10;
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block(); // 10
}
void test2()
{
    __block int a = 10;
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block(); // 20
}
void test3()
{
    static int a = 10;
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block(); // 20
}
int a = 10;
void test4()
{
    void (^block)() = ^{
        NSLog(@"a is %d", a);
    };
    a = 20;
    block();//20
}

分析:造成这样的原因是:传值和传址.为什么说会有传值和传址,把.m编译成c++代码.得到.cpp文件,我们来到文件的最后,看到如下代码

struct __test1_block_impl_0 {
    struct __block_impl impl;
    struct __test1_block_desc_0* Desc;
    int a;
    __test1_block_impl_0(void *fp,struct __test1_block_desc_0*
                         Desc,int _a,int flag=0): a(_a){
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};
static void __test1_block_func_0(struct __test1_block_imp_0
                                 *__cself)
{
    int a = __cself->a;
    NSLog(a);//a }
void test1()
{
int a = 10;
void (*block)() = (void (*)())&__test1_block_impl_0((void
*))__test1_block_func_0,&__test1_block_desc_0_DATA,a);
a = 20;
((void (*)(__block_impl *))((__block_ipml *)block)->FuncPtr)
((_block_impl *)block);
}
int main(int argc, const char * argv[])
{
/* @autoreleasepool */ { __AtAutoreleasePool
__autoreleasepool;
test1(); }
return 0; }
 48 
static struct IMAGE_INFO { unsigned version; unsigned flag; }
_OBJC_IMAGE_INFO = { 0, 2 };
  • 我们看到void test1()中, void(*block)()右边最后面,把a出拉进去了,也就是把10这个值传进去了
  • 而对void(block)()简化分析,void(block)()=&_test_block_impl_0();所以block就是指向结构体的指针.
  • 对void test1 ()中最下面的函数进行简化分析,得到(block)-.FuncPtr(block),我们再回到刚才test1_block_impl_0 这个结构体重, impl.FuncPtr = fp; 而fp又是传入结构体的第一个参数,而在void(*block)()中,传入结构体的第一个参数为test1_block_func_0,也就是说(block)->FuncPtr(block)=>_test1_block_func_0(block)
  • 上一步相当于调用test1_block_func_0()这个函数,我们来看这个函数,有这样一段代码, int a = cself ->a; 访问block中的a值,传递给a;所以是10,这种就是传值

现在我们来看看test2(),添加了__block会发生什么变化呢

void test2()
{
    __attribute__((_blocks__(byref))) __Block_byref_a_0 a =
    {(void*)0,(__Block_byref_a_0 *)&a,0,sizeof(__Block_byref_a_0),10};
    void(*block)() =  (void (*)())&__test2_block_impl_0((void
                                                         *))__test2_block_func_0,&__test2_block_desc_0_DATA,
    (__Block_byref_a_0 *)&a,570425344);
    (a.__forwarding->a) = 20;
    ((void (*)(__block_impl *))((__block_ipml *)block)->FuncPtr)
    ((_block_impl *)block);
}
int main(int argc, const char * argv[])
{
    /* @autoreleasepool */ { __AtAutoreleasePool
        __autoreleasepool;
        test2(); }
 49 
return 0; }
static struct IMAGE_INFO { unsigned version; unsigned flag; }
_OBJC_IMAGE_INFO = { 0, 2 };
  • 代码虽然很多看着很复杂, 但是我们只需要看我们想要知道的,睁大你的眼睛,看到void(*block)()这个函数的最后面,有个&a,天啊,这里传的是a的地址.从test2到test4,都是传址,所以a的值发生变化,block打印出来的是a的最终值.
  • 总结: 只有普通局部变量是传资,其他的情况都是传址
block 定义
//  无参数无返回值
void(^block)();
//  无参数有返回值
int(^block1)();
//  有参数有返回值
int(^block1)(int number);

也可以直接打入inline来自动生成block格式

<#returnType#>(^<#blockName#>)(<#parameterTypes#>) =
^(<#parameters#>) {
    <#statements#>
};
block的内存管理
  • 无论当前环境是ARC还是MRC,只要block没有访问外部变量,block始终在全局区
  • MRC情况下
    -- block如果访问外部变量, block 在栈里
    -- 不能对block使用retain,否则不能保存在堆里
    -- 只用使用copy, 才能放在堆里
  • ARC情况下
    -- block如果访问外部变量,block在堆里
    -- block 可以使用copy 和 strong, 并且block是一个对象
block的循环引用
  • 如果要在block中直接使用外部强指针会发生错误,使用以下代码在block外部实现可以解决
__weak typeof(self) weakSelf = self;
  • 但是如果在block内部使用延时操作还使用弱指针的话会取不到该弱指针,需要在block内部再将弱指针强引用以下
__strong typeof(self) strongSelf = weakSelf;
描述一个你遇到过的retain cycle例子

block中的循环引用:一个viewController

blockviewController
@property (nonatomic,strong)HttpRequestHandler * handler;
@property (nonatomic,strong)NSData  *data;
 _handler = [httpRequestHandler sharedManager];
  [ downloadData:^(id responseData){
_data = responseData;
}];

self 拥有 _handler, _handler 拥有 block, block 拥有self(因为使用了self的 _data属性, block会copy 一份self)
解决方法

__weak typedof(self)weakSelf = self
 [ downloadData:^(id responseData){
weakSelf.data = responseData;
block中的weak self, 是在任何时候都需要添加吗
  • 不是任何时候都需要添加,不过任何时候都添加总是好的,主要出现像self->block->self.proerty/self->_ivar这样的结构链时,才会出现循环引用问题.
通过block来传值
  • 在控制器间传值可以使用代理或者block,使用block相对来说简洁
  • 在前一个控制器的touchesBegan:方法内实现如下代码
  ModalViewController *modalVc = [[ModalViewController alloc]
init];
  modalVc.valueBlcok = ^(NSString *str){
NSLog(@"ViewController%@",str); };
 [self presentViewController:modalVc animated:YES completion:nil];
  • 在ModalViewController控制器的.h文件中声明一个block属性
 @property (nonatomic ,strong) void(^valueBlcok)(NSString *str); 
  • 并在.m文件中实现方法
 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:
(UIEvent *)event
{
// 传值:调用block
        if (_valueBlcok) {
        _valueBlcok(@"123");
        }
}
  • 这样在ModalViewController回到上一个控制器的时候,上一个控制器的label就能显示ModalViewController传过来的字符串
block作为一个参数使用
  • 新建一个类,在.h文件中声明一个方法
- (void)calculator:(int(^)(int result))block; 

  • 并在.m文件中实现该方法
-(void)calculator:(int (^)(int))block
  {
  self.result = block(self.result);
  }
  • 在其他类中调用该方法
CalculatorManager *mgr = [[CalculatorManager alloc] init];
[mgr calculator:^(int result){
    result += 5;
    return result;
}];
block最为返回值使用
  • 在masonry框剪中我们可以看到如下用法
make.top.equalTo(superview.mas_top).with.offset(padding.top);

这个方法实现就是将block最为返回值来使用

  • 来分析一下这段代码:其实可以将这段代码看成
make.top,make.equalTo,make.with,make.offset,

所以可以得出一个结论make.top返回了一个make,才能实现make.top.equalTo

  • 那来模仿一下这种功能的实现
  • 新建一个类,在.h文件中声明一个方法
- (CalculatorManager *(^)(int a))add;
  • 在.m文件中实现方法
-(CalculatorManager * (^)(int a))add
 {
     return ^(int a){
         _result += a;
     return self;
 };
}
  • 这样就可以在别的类中实现上面代码的用法
mgr.add(1).add(2).add(3)
block的变量传递
  • 如果block访问的外部变量是局部变量,那么就是值传递,外界改了,不会影响里面
  • 如果block访问的外部变量是__block或者static修饰,或者是全局变量,那么就是指针传递,block里面的值和外界同一个变量,外界改变,里面也会改变
  • 验证一下是不是这样
  • 通过clang来将main.m文件编译为C++
  • 在终端输入如下命令clang-rewrite-objc main.m
void(*block)() = ((void (*)())&__main_block_impl_0((void
                                                    *)__main_block_func_0,         &__main_block_desc_0_DATA,
                                                   (__Block_byref_a_0 *)&a, 570425344));
void(*block)() = ((void (*)())&__main_block_impl_0((void
                                                    *)__main_block_func_0,         &__main_block_desc_0_DATA, a));
  • 可以看到在编译后的代码最后可以被发现被__block修饰过的变量使用是&a,而局部变量是a
block的注意点
  • 在block内部使用外部指针且会造成循环引用的情况下,需要用__weak修饰外部指针__weak typeof(self) weakSelf = self;
  • 在block内部如果调用了延时函数还是用弱指针会取不到该指针,因为已经被销毁了,需要在block内部在将弱指针重新引用一下
__strong typeof(self) strongSelf = weakSelf;
  • 如果需要在block内部改变外部变量的话,需要在用__block修饰外部变量
使用block有什么好处,使用NSTimer写出一个使用block显示(在UILabel)秒表上面的代码

说到block的好处,最直接的就是代码紧凑,传值,回调都很方便,省去了写代理的很多代码,
对于这里根本没有必要使用block来刷新UILabel显示,因为都是直接赋值.当然,笔者觉得这是在考验应聘者如何将一个通用的block版本

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                    repeats:YES
                                   callback:^() {
  weakSelf.secondsLabel.text = ...
}
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSRunLoopCommonModes];
block跟函数很像
  • 可以保存代码
  • 有返回值
  • 有形参
  • 调用方式一样
使用系统的某些block api (如UIView 的block版本写动画时),是否也考虑应用循环问题

系统的某些block api中,UIView 的block版本写动画时不需要考虑,但也有一些api需要考虑.所谓"引用循环"是指双向的强引用
所以那些"单向的强引用"(block强引用self)没有问题,比如这些:

[UIView animateWithDuration:duration animations:^{ [self.superview
layoutIfNeeded]; }];
[[NSOperationQueue mainQueue]
addOperationWithBlock:^{ self.someProperty = xyz; }];
[[NSNotificationCenter defaultCenter]
addObserverForName:@"someNotification"
                           object:nil
                           queue:[NSOperationQueue mainQueue]
notification) {
usingBlock:^(NSNotification *
self.someProperty = xyz;
                         }];

这些情况不需要考虑"循环引用"
但是如果你使用一些参数中可能含有成员变量的系统api,如GCD,NSNotificationCenter就要小心一点.比如GCD内部如果引用了self,而且GCD的其他参数是成员变量,则要考虑到循环引用:

__weak __typeof(self) weakSelf = self;
dispatch_group_async(_operationsGroup, _operationsQueue, ^{
    __typeof__(self) strongSelf = weakSelf;
    [strongSelf doSomething];
    [strongSelf doSomethingElse];
});

类似的:

__weak __typeof(self) weakSelf = self;
_observer = [[NSNotificationCenter defaultCenter]
addObserverForName:@"testKey"
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
  __typeof__(self) strongSelf = weakSelf;
  [strongSelf dismissModalViewControllerAnimated:YES];
}];
self –> _observer –> block –> self 
谈谈对Block的理解.并写出一个使用Block执行UIView动画
  • block是可以获取其他函数局部变量的匿名函数,其不但方便开发,并且可以大幅提高应用的执行效率(多核心CPU可直接处理Block指令)
[UIView transitionWithView:self.view duration:0.2
ptions:UIViewAnimationOptionTransitionFlipFromLeft
animations:^{ [[blueViewController view] removeFromSuperview];
[[self view] insertSubview:yellowViewController.view atIndex:0]; }
                                       completion:NULL];
写出上面代码的Block的定义
  • typedef void(^animations) (void);
  • typedef void(^completion) (BOOL finished);
什么是block
  • 对于闭包(block),有很多定义,其中闭包就是获取其他函数局部变量的匿名函数,这个定义即接近本质又比较好理解
  • 对于刚接触block的同学,会觉得有些绕,因为我们习惯写这样的程勋main(){funA();funB();funC();}funB(){.....};就是函数main调用函数A,函数A调用函数B...函数们一次顺序执行,但现实中不全是这样的,列如项目经理M,手下有3个程序员ABC,当他给程序员a安排实现F1功能时,他并不等着A完成之后,再去安排B去实现F2,而是给安排A功能F1,B功能F2,C功能F3,然后去写技术文档,而当A遇到问题时,他就会找项目经理M,当B做完时,会通知M,这就是一个异步执行的例子
  • 在这种情形下,block便可大显身手,因为在项目经理M,给A安排工作时,同事会告诉A如果遇到困难,如何能找到他报告问题(列如打他的手机号),这就是项目经理M给A的一个回调接口,要回调的操作,比如接到电话,百度查询后,返回网页内容给A,这就是一个block,在M交代工作时,已经定义好,并且取得了F1的任务号(局部变量),当A遇到问题是,才调用执行,夸函数在项目经理M查询百度,获得结果后回调block
block实现原理
  • Objective-C是对C语言的扩展,block的实现是基于指针的函数指针.
  • 从计算机语言的发展,最早的goto,高级语言的指针,到面向对象语言的block,从机器的思维,一步步接近人的思维,以方便开发人员为高效,直接的描述出想死的逻辑
  • 使用实例:cocoaTouch框架下动画效果的block调用
    使用typed声明block
typedef void(^didFinishBlock) (NSObject *ob);

这就声明了一个didFinishBlock类型的block;
然后便可以用

@property (nonatomic,copy) didFinishBlock finishBlock;

声明一个block对象,注意对象属性设置为copy,接到block参数时,便会自动复制一份
__block是一个特殊类型
使用这个关键字声明局部变量,可以被block所改变,并且其在原函数中的值会被改变

关于block
使用block和使用delegate完成委托模式有什么优点

首先要了解什么是委托模式,委托模式在iOS中大量应用,其在设计模式中是适配器模式中的对象适配器,Objective-C使用id类型指向一切对象,使用委托模式更为简洁.了解委托模式的细节
iOS设计模式-委托模式
使用block实现委托模式,其优点是回调的block代码块定义在委托对象函数内部,使用代码更为紧凑;
适配对象不再需要实现具体的某个protocol,代码更加简洁.

多线程和block

GCD与block
使用dispatch_async系列方法,可以以指定的方式执行block
GCD变成实例
dispatch_async的完整定义

void dispatch_async(
dispatch_queue_t queue,
dispatch_block_t block);

功能:在指定的队列里提交一个异步执行的block ,不阻塞当前线程
通过queue来控制block执行的线程.主线程执行前文定义的finishBlock对象

dispatch_async(dispatch_get_main_queue(),^(void){finishBlock();});

解释以下代码的内存泄漏原因
@implementation HJTestViewController

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    HJTestCell *cell = [tableView
dequeueReusableCellWithIdentifier:@"TestCell"
forIndexPath:indexPath];
[cell setTouchBlock:^(HJTestCell *cell) {
        [self refreshData];
    }];
   return cell;
}

原因

[cell setTouchBlock:^(HJTestCell *cell) {
    [self refreshData];
}];

产生内存泄漏的原因是因为循环引用
在给cell设置的touchBlock中,使用了__strong修饰的self,,由于Block的原理,当touchBlock从栈复制到堆中时,self会一同复制到堆中,retain一次,被touchBlock持有,而touchBlcok又是呗cell持有的,cell又被tableView持有,tableView又被self持有,因此形成了循环引用,self间接持有touchBlock,touchBlock持有self
一旦尝试了循环引用,由于俩个object都被强引用,所以retainCount始终不能为0,无法释放,产生内存泄漏
解决办法:
使用weakSelf解除touchBlock对self的强引用


}];
__weak __typeof__(self) weakSelf = self;
[cell setTouchBlock:^(HJTestCell *cell) {
    [weakSelf refreshData];
}];

推荐阅读更多精彩内容

  • 1 Block机制 (Very Good) Block技巧与底层解析 http://www.jianshu.com...
    Kevin_Junbaozi阅读 2,697评论 3 45
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 6,234评论 1 20
  • 【原创博文,转载请注明出处!】 一、block的本质是什么? 接下来通过一个简单的demo,开启我们探索block...
    RephontilZhou阅读 1,204评论 4 25
  • 轻轻地透过那扇门 偷偷去看另一世界的人 声音像芬芳一样 传递到我的肉身荡涤我的灵魂 指尖的婉转打破清晨 再也没有一...
    望成恨水阅读 28评论 0 0
  • 原 则【小小说】 王圣礼 吴局长是个很有原则的人。刚履新几个月,吴局长就给大家留下了这样的印象。 他刚上任,局里需...
    穷且益坚不坠青云之志阅读 44评论 0 1