zoukankan      html  css  js  c++  java
  • iOS开发-由浅至深学习block

    关于block

    在iOS 4.0之后,block横空出世,它本身封装了一段代码并将这段代码当做变量,通过block()的方式进行回调。这不免让我们想到在C函数中,我们可以定义一个指向函数的指针并且调用:

    1
    2
    3
    4
    5
    bool executeSomeTask(void) {
        //do something and return if success or not
    }
    bool (*taskPoint)(void);
    taskPoint = something;

    上面的函数指针可以直接通过(*taskPoint)()的方式调用executeSomeTask这个函数,这样对比block跟似乎C语言的函数指针是一样的,但是两者仍然存在以下区别:

    • block的代码是内联的,效率高于函数调用

    • block对于外部变量默认是只读属性

    • block被Objective-C看成是对象处理

    对于block的底层实现在网上已经有很多资料了,其源码更是可以在opensource.apple.com上下载,因此,本文更着重于对于block的应用

    block特性

    认识block

    先从一个简单的需求来说:传入两个数,并且计算这两个数的和,为此创建了这样一个block:

    1
    2
    3
    int (^sumOfNumbers)(int a, int b) = ^(int a, int b) {
        return a + b;
    };

    这段代码等号左侧声明一个名为sumOfNumbers的代码块,名称前用^符号表示后面的字符串是block的名称。最左侧的int表示这个block的返回值,括号中间表示这个block的参数列表,这里接收两个int类型的参数。 而在等号右侧表示这个block的定义,其中返回值是可以省略的,编译器会根据上下文自动补充返回值类型。使用^符号衔接着一个参数列表,使用括号包起来,告诉编译器这是一个block,然后使用大括号将block的代码封装起来。

    783864-3ad5d92333756aa7500.jpg

    block代码结构

    捕获外界变量

    block还可以访问外界的局部变量,在我的从UIView动画说起中有这么一段代码,其中block内部使用到了外部的局部变量:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CGPoint center = cell.center;
    CGPoint startCenter = center;
    startCenter.y += LXD_SCREEN_HEIGHT;
    cell.center = startCenter;
    [UIView animateWithDuration: 0.5 delay: 0.35 * indexPath.item usingSpringWithDamping: 0.6 initialSpringVelocity: 0 options: UIViewAnimationOptionCurveLinear animations: ^{
        cell.center = center;
    } completion: ^(BOOL finished) {
        NSLog("animation %@ finished", finished? @"is", @"isn't");
    }];

    这里面就用到了void(^animations)(void)跟void(^completion)(BOOL finished)两个block,系统会在动画开始以及动画结束的时候分别调用者两个block。在实现动画的block内部,代码访问了上文中的center属性——在动画开始的时候这个动画函数的生命周期早已结束,而block会捕获代码外的局部变量,当然这只局限于只读操作。如果我们在block中修改外部变量,编译器将会报错:

    783864-b470894efa49f5de.jpg

    block中修改外界局部变量

    对于希望在block中修改的外界局部对象,我们可以给这些变量加上__block关键字修饰,这样就能在block中修改这些变量。在捕获变量特性中,还有一个有趣的小机制,我们把上面的代码改成这样:

    1
    2
    3
    4
    5
    6
    CGPoint center = CGPointZero;
        CGPoint (^pointAddHandler)(CGPoint addPoint) = ^(CGPoint addPoint) {
        return CGPointMake(center.x + addPoint.x, center.y + addPoint.y);
    }
    center = CGPointMake(100, 100);
    NSLog(@"%@", pointAddHandler(CGPointMake(10, 10)));    //输出{10,10}

    block在捕获变量的时候只会保存变量被捕获时的状态(对象变量除外),之后即便变量再次改变,block中的值也不会发生改变。所以上面的代码在计算新的坐标值时center的值依旧等于CGPointZero

    循环引用

    开头说过,block在iOS开发中被视作是对象,因此其生命周期会一直等到持有者的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @implementation LXDObject
    {
        void (^_cycleReferenceBlock)(void);
    }
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        _cycleReferenceBlock = ^{
            NSLog(@"%@", self);   //引发循环引用
        };
    }
    @end

    遇到这种代码编译器只会告诉你存在警告,很多时候我们都是忽略警告的,这最后会导致内存泄露,两者都无法释放。跟普通变量存在__block关键字一样的,系统提供给我们__weak的关键字用来修饰对象变量,声明这是一个弱引用的对象,从而解决了循环引用的问题:

    1
    2
    3
    4
    __weak typeof(*&self) weakSelf = self;
    _cycleReferenceBlock = ^{
        NSLog(@"%@", weakSelf);   //弱指针引用,不会造成循环引用
    };

    对于block这种有趣的特性,在唐巧的谈Objective-C block的实现有详细介绍block的底层实现代码,我在这里就不多说了

    使用block

    在block出现之前,开发者实现回调基本都是通过代理的方式进行的。比如负责网络请求的原生类NSURLConnection类,通过多个协议方法实现请求中的事件处理。而在最新的环境下,使用的NSURLSession已经采用block的方式处理任务请求了。各种第三方网络请求框架也都在使用block进行回调处理。这种转变很大一部分原因在于block使用简单,逻辑清晰,灵活等原因。接下来我会完成一次网络请求,然后通过block进行回调处理。这些回调包括请求完成、下载进度

    按照returnValue(^blockName)(parameters)的方式进行block的声明未免麻烦了些,我们可以通过关键字typedef来为block起类型名称,然后直接通过类型名进行block的创建:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    @interface LXDDownloadManager: NSObject< NSURLSessionDownloadDelegate >
     
    //block重命名
    typedef void(^LXDDownloadHandler)(NSData * receiveData, NSError * error);
    typedef void(^LXDDownloadProgressHandler)(CGFloat progress);
     
    - (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (LXDDownloadHandler)handler progress: (LXDDownloadProgressHandler)progress;
     
    @end
     
    @implementation LXDDownloadManager
    {
        LXDDownloadProgressHandler _progress;
    }
     
    - (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (LXDDownloadHandler)handler progress: (LXDDownloadProgressHandler)progress
    {
        //创建请求对象
        NSURLRequest * request = [self postRequestWithURL: URL params: parameters]; 
        NSURLSession * session = [NSURLSession sharedSession];
     
        //执行请求任务
        NSURLSessionDataTask * task = [session dataTaskWithRequest: request completionHandler: ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            if (handler) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    handler(data, error);
                }); 
            }
        }];
        [task resume];
    }
     
    //进度协议方法
    - (void)URLSession:(NSURLSession *)session
         downloadTask:(NSURLSessionDownloadTask *)downloadTask 
        didWriteData:(int64_t)bytesWritten // 每次写入的data字节数  
       totalBytesWritten:(int64_t)totalBytesWritten // 当前一共写入的data字节数  
      totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite // 期望收到的所有data字节数  
    {   
        double downloadProgress = totalBytesWritten / (double)totalBytesExpectedToWrite;  
        if (_progress) { _progress(downloadProgress); }
    }  
     
    @end

    上面通过封装NSURLSession的请求,传入一个处理请求结果的block对象,就会自动将请求任务放到工作线程中执行实现,我们在网络请求逻辑的代码中调用如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    [[LXDDownloadManager alloc] downloadWithURL: QQMUSICURL parameters: nil handler ^(NSData * receiveData, NSError * error) {
        if (error) { NSLog(@"下载失败:%@", error) }
        else {
            //处理下载数据
        }
    } progress: ^(CGFloat progress) {
        NSLog(@"下载进度%lu%%", progress*100);
    }];

    仿swift高阶函数

    用过swift的开发者都知道swift的函数调用很好的体现了链式编程的思想,即将多个操作通过.连接起来,使得可读性更强,比如ocString.stringByAppendingFormat("abc").stringByAppendingFormat("edf")就是连续调用了追加字符串的方法。这种编程方式的条件之一是每次函数调用必须有返回值。虽然在使用Objective-C开发的过程中,方法的调用是通过[target action]的方式完成的,但是block本身的调用方式也是通过blockName(parameters)的方式执行的,与这种链式函数有异曲同工之妙。

    在swift中提供了包括map、filter、reduce等十分简洁优秀的高阶函数供我们对数组数据进行操作,同样情况下,遍历一个数组并求和在使用oc(不使用kvc)和swift的环境下的代码是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #pragma mark - OC code
    NSArray numbers = @[@10, @15, @99, @66, @25];
    NSInteger totalNumber = 0;
    for (NSNumber number in numbers) {
        totalNumber += number.integerValue;
    }
    #pragma mark - swift code
    let numbers = [10, 15, 99, 66, 25];
    let totalNumber = numbers.reduce(0, { $0+$1 })

    无论是代码量还是简洁性,此时的oc都比不上swift。那么接下来就要通过神奇的block来为oc添加这些高阶函数的实现。为此我们需要新建一个NSArray的分类扩展,命名为NSArray+LXDExtension

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #import /// 数组元素转换
    typedef id(^LXDItemMap)(id item);
    typedef NSArray *(^LXDArrayMap)(LXDItemMap itemMap);
    /// 数组元素筛选
    typedef BOOL(^LXDItemFilter)(id item);
    typedef NSArray *(^LXDArrayFilter)(LXDItemFilter itemFilter);
    /**
    *  扩展数组高级方法仿swift调用
    */
    @interface NSArray (LXDExtension)
    @property (nonatomic, copy, readonly) LXDArrayMap map;
    @property (nonatomic, copy, readonly) LXDArrayFilter filter;
    @end

    前面说了为了实现链式编程,函数调用的前提是具有返回对象。因此我使用了typedef声明了几个不同类型的block。虽然本质上LXDArrayMap和LXDArrayFilter两个block是一样的,但是为了区分它们的功能,还是建议这么做。其实现文件如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    typedef void(^LXDEnumerateHandler)(id item);
    @implementation NSArray (LXDTopMethod)
    - (LXDArrayMap)map
    {
        LXDArrayMap map = (LXDArrayMap)^(LXDItemMap itemMap) {
            NSMutableArray * items = @[].mutableCopy;
            for (id item in self) {
            [items addObject: itemMap(item)];
        }
        return items;
        };
        return map;
    }
    - (LXDArrayFilter)filter
    {
        LXDArrayFilter filter = (LXDArrayFilter)^(LXDItemFilter itemFilter) {
        NSMutableArray * items = @[].mutableCopy;
            for (id item in self) {
                if (itemFilter(item)) { [items addObject: item]; }
            }
            return items;
        };
        return filter;
    }
    - (void)setFilter:(LXDArrayFilter)filter {}
    - (void)setMap:(LXDArrayMap)map {}
    @end

    我们通过重写setter方法保证block不会被外部修改实现,并且在getter中遍历数组的元素并调用传入的执行代码来实现map和filter等功能。对于这两个功能的实现也很简单,下面举出两个调用高阶函数的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #pragma mark - 筛选数组中大于20的数值并转换成字符串
    NSArray * numbers = @[@10, @15, @99, @66, @25, @28.1, @7.5, @11.2, @66.2];
    NSArray * result = numbers.filter((LXDArrayFilter)^(NSNumber * item) {
        return item.doubleValue > 20
    }).map((LXDArrayMap)^(NSNumber * item) {
        return [NSString stringWithFormat: @"string %g", item.doubleValue];
    });
     
    #pragma mark - 将数组中的字典转换成对应的数据模型
    NSArray * jsons = @[@{ ... }, @{ ... }, @{ ... }];
    NSArray * models = jsons.map((LXDArrayMap)^(id item) {
        return [[LXDModel alloc] initWithJSON: item];
    })

    由于语法上的限制,虽然这样的调用跟swift原生的调用对比起来还是复杂了,但通过block让oc实现了函数链式调用的代码看起来也清爽了很多。

    总结

    block捕获变量、代码传递、代码内联等特性赋予了它多于代理机制的功能和灵活性,尽管它也存在循环引用、不易调试追溯等缺陷,但无可置疑它的优点深受码农们的喜爱。如何更加灵活的使用block需要我们对它不断的使用、探究了解才能完成。

  • 相关阅读:
    C#listbox使用方法
    poj 3894 System Engineer (二分图最大匹配--匈牙利算法)
    Java实现 蓝桥杯VIP 算法训练 连接字符串
    Java实现 蓝桥杯VIP 算法训练 连接字符串
    Java实现 蓝桥杯VIP 算法训练 比较字符串
    Java实现 蓝桥杯VIP 算法训练 比较字符串
    Java实现 蓝桥杯VIP 算法训练 比较字符串
    Java实现 蓝桥杯VIP 算法训练 比较字符串
    Java实现 蓝桥杯VIP 算法训练 比较字符串
    Java实现 蓝桥杯VIP 算法训练 黑白无常
  • 原文地址:https://www.cnblogs.com/luoxiaofu/p/5445327.html
Copyright © 2011-2022 走看看