BlocksKit的使用详解

开源地址:https://github.com/zwaldowski/BlocksKit


Block比传统回调函数有2点优势:

  1. 允许在调用点上下文书写执行逻辑,不用分离函数
  2. Block可以使用local variables.

基于以上种种优点Cocoa Touch越发支持Block式编程,这点从UIView的各种动画效果可用Block实现就可见一斑。而BlocksKit是对Cocoa Touch Block编程更进一步的支持,它简化了Block编程,发挥Block的相关优势,让更多UIKit类支持Block式编程。BlocksKit是一个block的大杂烩,它给Fundation和UIKit框架里很多的类都做了扩展,可以通过调用相关类的扩展的方法简单的实现一下几个功能:

  • 1.通过block传入事件处理函数
  • 2.创建动态代理,传入block给想要实现的方法。
  • 3.在很多基础的类上增加额外的方法

导入工程:

导入工程有两种方式:

  • 第一种 
    按照官方文档描述,编译成静态库,添加到自己的文件工程中。

  • 第二种 
    把文件BlocksKit添加到自己工程文件中,然后修改部分.h文件; 
    修改规则如下:

    #import <BlocksKit/BlocksKit.h>都换成#import <BlocksKit.h>
       


    核心功能简介:

    • 1、容器相关(NSArray、NSDictionary、NSSet、NSIndexSet、NSMutableArray、NSMutableDictionary、NSMutableSet、NSMutableIndexSet)
    • 2、关联对象相关
    • 3、逻辑执行相关
    • 4、KVO相关
    • 5、定时器相关

    具体使用:

    使用的时候要导入相应的包//Foundation框架:
    #import "BlocksKit.h"
    //UIKit框架:
    #import "BlocksKit+UIKit.h"
    //QuickLook框架:
    #import "BlocksKit+QuickLook.h"
    //MessageUI框架:
    #import "BlocksKit+MessageUI.h"
     
      2.使用例子

      • 部分不可变容器的BlocksKit声明:
      //串行遍历容器中所有元素
      - (void)bk_each:(void (^)(id obj))block; 
      //并发遍历容器中所有元素(不要求容器中元素顺次遍历的时候可以使用此种遍历方式来提高遍历速度)
      - (void)bk_apply:(void (^)(id obj))block;
      //返回第一个符合block条件(让block返回YES)的对象
      - (id)bk_match:(BOOL (^)(id obj))block;
      //返回所有符合block条件(让block返回YES)的对象
      - (NSArray *)bk_select:(BOOL (^)(id obj))block;
      //返回所有!!!不符合block条件(让block返回YES)的对象
      - (NSArray *)bk_reject:(BOOL (^)(id obj))block;
      //返回对象的block映射数组
      - (NSArray *)bk_map:(id (^)(id obj))block;
      
      //查看容器是否有符合block条件的对象
      //判断是否容器中至少有一个元素符合block条件
      - (BOOL)bk_any:(BOOL (^)(id obj))block; 
      //判断是否容器中所有元素都!!!不符合block条件
      - (BOOL)bk_none:(BOOL (^)(id obj))block;
      //判断是否容器中所有元素都符合block条件
      - (BOOL)bk_all:(BOOL (^)(id obj))block;


      使用

      NSString* str = [arr bk_match:^BOOL(id  _Nonnull obj) {
          return ((NSString *)obj).length == 1;
      }];
      NSArray* arr_01 = [arr bk_select:^BOOL(id  _Nonnull obj) {
          return ((NSString *)obj).length == 1;
      }];
      NSArray* arr_02 = [arr bk_reject:^BOOL(id  _Nonnull obj) {
          return ((NSString *)obj).length == 1;
      }];
      NSLog(@"str = %@",str);
      NSLog(@"arr_01 = %@",arr_01);
      NSLog(@"arr_02 = %@",arr_02);
       

        • 部分可变容器的BlocksKit声明
        /** Filters a mutable array to the objects matching the block.
        
         @param block A single-argument, BOOL-returning code block.
         @see <NSArray(BlocksKit)>bk_reject:
         */
        //删除容器中!!!不符合block条件的对象,即只保留符合block条件的对象
        - (void)bk_performSelect:(BOOL (^)(id obj))block;
        
        //删除容器中符合block条件的对象
        - (void)bk_performReject:(BOOL (^)(id obj))block;
        
        //容器中的对象变换为自己的block映射对象
        - (void)bk_performMap:(id (^)(id obj))block;

        (2)、关联对象相关的BlocksKit

        关联对象的作用如下: 
        在类的定义之外为类增加额外的存储空间。使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(ARC下也不会导致资源不可回收)。 
        关联对象的例子,在我们的实际项目中的常见用法一般有category中用关联对象定义property,或者使用关联对象绑定一个block。 
        关联对象相关的BlocksKit是对objc_setAssociatedObject、objc_getAssociatedObject、objc_removeAssociatedObjects这几个原生关联对象函数的封装。主要是封装其其内存管理语义。

        部分函数声明如下

        //@interface NSObject (BKAssociatedObjects)
        
        //以OBJC_ASSOCIATION_RETAIN_NONATOMIC方式绑定关联对象
        - (void)bk_associateValue:(id)value withKey:(const void *)key;
        //以OBJC_ASSOCIATION_COPY_NONATOMIC方式绑定关联对象
        - (void)bk_associateCopyOfValue:(id)value withKey:(const void *)key;
        //以OBJC_ASSOCIATION_RETAIN方式绑定关联对象
        - (void)bk_atomicallyAssociateValue:(id)value withKey:(const void *)key;
        //以OBJC_ASSOCIATION_COPY方式绑定关联对象
        - (void)bk_atomicallyAssociateCopyOfValue:(id)value withKey:(const void *)key;
        //弱绑定
        - (void)bk_weaklyAssociateValue:(__autoreleasing id)value withKey:(const void *)key;
        //删除所有绑定的关联对象
        - (void)bk_removeAllAssociatedObjects;
         
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14

        (3)、逻辑执行相关的BlocksKit

        所谓逻辑执行,就是Block块执行。逻辑执行相关的BlocksKit是对dispatch_after函数的封装。使其更加符合语义。

        主要函数如下

        //@interface NSObject (BKBlockExecution)
        
        //主线程执行block方法,延迟时间可选
        - (BKCancellationToken)bk_performAfterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
        //后台线程执行block方法,延迟时间可选
        - (BKCancellationToken)bk_performInBackgroundAfterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
        //所有执行block相关的方法都是此方法的简化版,该函数在指定的block队列上以指定的时间延迟执行block
        - (BKCancellationToken)bk_performOnQueue:(dispatch_queue_t)queue afterDelay:(NSTimeInterval)delay usingBlock:(void (^)(id obj))block;
        //取消block,非常牛逼!!!一般来说一个block加到block queue上是没法取消的,此方法实现了block的取消操作(必须是用BlocksKit投放的block)
        + (void)bk_cancelBlock:(id <NSObject, NSCopying>)block;
         
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        static id <NSObject, NSCopying> BKDispatchCancellableBlock(dispatch_queue_t queue, NSTimeInterval delay, void(^block)(void)) {
            dispatch_time_t time = BKTimeDelay(delay);
        
        #if DISPATCH_CANCELLATION_SUPPORTED
            if (BKSupportsDispatchCancellation()) {
                dispatch_block_t ret = dispatch_block_create(0, block);
                dispatch_after(time, queue, ret);
                return ret;
            }
        #endif
            //cancelled是个__block变量,使得该block在加入queue后能够逻辑上取消。注意,仅仅是逻辑上取消,不能把block从queue中剔除。
            __block BOOL cancelled = NO;
            //在外部block之上加一层能够逻辑取消的代码,使其变为一个wrapper block
            //当调用wrapper(YES)的时候就让__block BOOL cancelled = YES,使得以后每次block主体都被跳过。
            void (^wrapper)(BOOL) = ^(BOOL cancel) {
            //cancel参数是为了在外部能够控制cancelled _block变量
                if (cancel) {
                    cancelled = YES;
                    return;
                }
                if (!cancelled) block();
            };
            //每个投入queue中的block实际上是wraper版的block
            dispatch_after(time, queue, ^{
            //把cancel设置为NO,block能够逻辑执行
                wrapper(NO);
            });
            //返回wraper block,以便bk_cancelBlock的时候使用
            return wrapper;
        }
        + (void)bk_cancelBlock:(id <NSObject, NSCopying>)block
        {
            NSParameterAssert(block != nil);
        
        #if DISPATCH_CANCELLATION_SUPPORTED
            if (BKSupportsDispatchCancellation()) {
                dispatch_block_cancel((dispatch_block_t)block);
                return;
            }
        #endif
            //把cancel设置为YES,修改block中_block cancelled变量,如果此时block未执行则,block在执行的时候其逻辑主体会被跳过
            void (^wrapper)(BOOL) = (void(^)(BOOL))block;
            wrapper(YES);
        }
         
        • 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
        • 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

        (4)、KVO相关BlocksKit

        KVO主要涉及两类对象,即“被观察对象“和“观察者“。 
        与“被观察对象”相关的函数主要有如下两个:

        //添加观察者
        - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context;
        //删除观察者
        - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context;
        //与“观察者“相关的函数如下:
        //观察到对象发生变化后的回调函数(观察回调)
        - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
         
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7

        通常的KVO做法是先对“被观察对象”添加“观察者”,同时在“观察者”中实现观察回调。这样每当“被观察对象”的指定property改变时,“观察者”就会调用观察回调。 
        KVO相关BlocksKit弱化了“观察者”这种对象,使得每当“被观察对象”的指定property改变时,就会调起一个block。具体实现方式是定义一个_BKObserver类,让该类实现观察回调、对被观察对象添加观察者和删除观察者。

        _BKObserver类定义如下:

        @interface _BKObserver : NSObject {
            BOOL _isObserving;
        }
        
        //存储“被观察的对象”
        @property (nonatomic, readonly, unsafe_unretained) id observee;
        @property (nonatomic, readonly) NSMutableArray *keyPaths;
        //存储回调block
        @property (nonatomic, readonly) id task;
        @property (nonatomic, readonly) BKObserverContext context;
        
        - (id)initWithObservee:(id)observee keyPaths:(NSArray *)keyPaths context:(BKObserverContext)context task:(id)task;
        
        @end
         
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        static void *BKObserverBlocksKey = &BKObserverBlocksKey;
        static void *BKBlockObservationContext = &BKBlockObservationContext;
        
        @implementation _BKObserver
        
        - (id)initWithObservee:(id)observee keyPaths:(NSArray *)keyPaths context:(BKObserverContext)context task:(id)task
        {
            if ((self = [super init])) {
                _observee = observee;
                _keyPaths = [keyPaths mutableCopy];
                _context = context;
                _task = [task copy];
            }
            return self;
        }
        
        - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
        {
            //观察者回调,在KV改变的时候调用相关block
            if (context != BKBlockObservationContext) return;
        
            @synchronized(self) {
                switch (self.context) {
                    case BKObserverContextKey: {
                        void (^task)(id) = self.task;
                        task(object);
                        break;
                    }
                    case BKObserverContextKeyWithChange: {
                        void (^task)(id, NSDictionary *) = self.task;
                        task(object, change);
                        break;
                    }
                    case BKObserverContextManyKeys: {
                        void (^task)(id, NSString *) = self.task;
                        task(object, keyPath);
                        break;
                    }
                    case BKObserverContextManyKeysWithChange: {
                        void (^task)(id, NSString *, NSDictionary *) = self.task;
                        task(object, keyPath, change);
                        break;
                    }
                }
            }
        }
        
        //开启KV观察
        - (void)startObservingWithOptions:(NSKeyValueObservingOptions)options
        {
            @synchronized(self) {
                if (_isObserving) return;
        
                [self.keyPaths bk_each:^(NSString *keyPath) {
                //observee的被观察对象,observer是自己,
                    [self.observee addObserver:self forKeyPath:keyPath options:options context:BKBlockObservationContext];
                }];
        
                _isObserving = YES;
            }
        }
        
        //停止KV观察
        - (void)stopObservingKeyPath:(NSString *)keyPath
        {
            NSParameterAssert(keyPath);
        
            @synchronized (self) {
                if (!_isObserving) return;
                if (![self.keyPaths containsObject:keyPath]) return;
        
                NSObject *observee = self.observee;
                if (!observee) return;
        
                [self.keyPaths removeObject: keyPath];
                keyPath = [keyPath copy];
        
                if (!self.keyPaths.count) {
                    _task = nil;
                    _observee = nil;
                    _keyPaths = nil;
                }
        
                [observee removeObserver:self forKeyPath:keyPath context:BKBlockObservationContext];
            }
        }
        @end
         
        • 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
        • 45
        • 46
        • 47
        • 48
        • 49
        • 50
        • 51
        • 52
        • 53
        • 54
        • 55
        • 56
        • 57
        • 58
        • 59
        • 60
        • 61
        • 62
        • 63
        • 64
        • 65
        • 66
        • 67
        • 68
        • 69
        • 70
        • 71
        • 72
        • 73
        • 74
        • 75
        • 76
        • 77
        • 78
        • 79
        • 80
        • 81
        • 82
        • 83
        • 84
        • 85
        • 86
        • 87
        • 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
        • 45
        • 46
        • 47
        • 48
        • 49
        • 50
        • 51
        • 52
        • 53
        • 54
        • 55
        • 56
        • 57
        • 58
        • 59
        • 60
        • 61
        • 62
        • 63
        • 64
        • 65
        • 66
        • 67
        • 68
        • 69
        • 70
        • 71
        • 72
        • 73
        • 74
        • 75
        • 76
        • 77
        • 78
        • 79
        • 80
        • 81
        • 82
        • 83
        • 84
        • 85
        • 86
        • 87

        使用BlocksKit

        - (void)bk_addObserverForKeyPaths:(NSArray *)keyPaths identifier:(NSString *)token options:(NSKeyValueObservingOptions)options task:(void (^)(id obj, NSString *keyPath, NSDictionary *change))task;
        
        - (void)bk_removeObserverForKeyPath:(NSString *)keyPath identifier:(NSString *)token;
         
        • 1
        • 2
        • 3
        • 1
        • 2
        • 3

        (5)、定时器相关BlocksKit

        NSTimer有个比较恶心的特性,它会持有它的target。比如在一个controller中使用了timer,并且timer的target设置为该controller本身,那么想在controller的dealloc中fire掉timer是做不到的,必须要在其他的地方fire。这会让编码很难受。具体参考《Effective Objective C》的最后一条。 BlocksKit解除这种恶心,其方式是把timer的target设置为timer 的class对象。把要执行的block保存在timer的userInfo中执行。因为timer 的class对象一直存在,所以是否被持有其实无所谓。

        实现代码如下:

        //"Replaced with -bk_performAfterDelay:usingBlock:"
        + (id)bk_scheduledTimerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
        {
            NSParameterAssert(block != nil);
            return [self scheduledTimerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
        }
        
        + (id)bk_timerWithTimeInterval:(NSTimeInterval)inTimeInterval block:(void (^)(NSTimer *timer))block repeats:(BOOL)inRepeats
        {
            NSParameterAssert(block != nil);
            return [self timerWithTimeInterval:inTimeInterval target:self selector:@selector(bk_executeBlockFromTimer:) userInfo:[block copy] repeats:inRepeats];
        }
        
        + (void)bk_executeBlockFromTimer:(NSTimer *)aTimer {
            void (^block)(NSTimer *) = [aTimer userInfo];
            if (block) block(aTimer);
        }
         
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17
        • 1
        • 2
        • 3
        • 4
        • 5
        • 6
        • 7
        • 8
        • 9
        • 10
        • 11
        • 12
        • 13
        • 14
        • 15
        • 16
        • 17

        2、动态代理样例

        代理是objective c里常用的模式,主要用来做逻辑切分,一个类做一类事情,让代码的耦合度减少。但他不方便的地方在于,要创建一个代理,就要定义一个类,声明这个类遵循那些接口,然后实现这些接口对应的函数。动态代理(Dynamic delegate)则让我们能够在code里,on the fly的创建这样一个代理,通过block定义要实现的方法。 
        例如:

        - (void)annoyUser
        {
            // 创建一个alert view
            UIAlertView *alertView = [[UIAlertView alloc]
                                      initWithTitle:@"Hello World!"
                                      message:@"This alert's delegate is implemented using blocks. That's so cool!"
                                      delegate:nil
                                      cancelButtonTitle:@"Meh."
                                      otherButtonTitles:@"Woo!", nil];
            // 获取该alert view的动态代理对象(什么是动态代理对象稍后会说)
            A2DynamicDelegate *dd = alertView.bk_dynamicDelegate;
        
            // 调用动态代理对象的 - (void)implementMethod:(SEL)selector withBlock:(id)block;方法,使得SEL映射一个block对象(假设叫做block1)
            [dd implementMethod:@selector(alertViewShouldEnableFirstOtherButton:) withBlock:^(UIAlertView *alertView) {
                NSLog(@"Message: %@", alertView.message);
                return YES;
            }];
        
            // 同上,让映射-alertView:willDismissWithButtonIndex:的SEL到另外一个block对象(假设叫做block2)
            [dd implementMethod:@selector(alertView:willDismissWithButtonIndex:) withBlock:^(UIAlertView *alertView, NSInteger buttonIndex) {
                NSLog(@"You pushed button #%d (%@)", buttonIndex, [alertView buttonTitleAtIndex:buttonIndex]);
            }];
            // 把alertView的delegate设置为动态代理
            alertView.delegate = dd;
            [alertView show];
        }
        // 那么,alert view在显示的时候收到alertViewShouldEnableFirstOtherButton:消息调用block1;alert view在消失的时候收到alertView:willDismissWithButtonIndex:消息,调用block2
         
        • 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
        • 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

        从上面的代码我们可以直观地看到:dd(动态代理对象)直接被设置为alert view的delegate对象,那么该alert view的UIAlertViewDelegate消息直接传递向给了dd。然后dd又通过某种方式把对应的SEL调用转为对应的block调用。我们又可以作出如下猜测:

        • 1、dd内部可能有个dic一样的数据结构,key可能是SEL,value可能是与之对应的block,通过implementMethod:withBlock:这个方法把SEL和block以键值对的形式建立起dic映射
        • 2、Host对象(本例是alertView)向dd发delegate消息的时候传递了SEL,dd在内部的dic数据结构查找对应的block,找到后,调用该block。

        3、UIKit相关的Block

        拿UIControl打比方,要想处理一个事件:

        • 以前:
        - (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents;
         
        • 1
        • 1

        需要通过上述方法将某一个对象的某一个selector传入,一般的做法是在viewcontroller里定义一个方法专门处理某一个按钮的点击事件。

        • 现在:
        - (void)bk_addEventHandler:(void (^)(id sender))handler forControlEvents:(UIControlEvents)controlEvents;
         
        • 1
        • 1

        通过上述方法将一个block注册上去,不需要单独定义方法。

        例如:

        [btn bk_addEventHandler:^(id  _Nonnull sender) {
            NSLog(@"111");
        } forControlEvents:UIControlEventTouchUpInside]

        UIControl

            [button bk_addEventHandler:^(id sender) {
                // do something
            } forControlEvents:UIControlEventTouchUpInside];

        UITextField

            [testField setBk_shouldReturnBlock:^BOOL(UITextField *field) {
                // do something like
                [self.view endEditing:YES];
                return YES;
            }];
        // 类似的还有其他几个代理

        UIView

            [view bk_whenTapped:^{
                // do something // 直接添加手势,不需要额外代码
            }];
        // 类似还有双击等

        UIWebView

            [webView bk_setDidFinishWithErrorBlock:^(UIWebView *webView, NSError *error) {
                // do something with error
            }];
        // 类似还有其他代理
        评论
        添加红包

        请填写红包祝福语或标题

        红包个数最小为10个

        红包金额最低5元

        当前余额3.43前往充值 >
        需支付:10.00
        成就一亿技术人!
        领取后你会自动成为博主和红包主的粉丝 规则
        hope_wisdom
        发出的红包
        实付
        使用余额支付
        点击重新获取
        扫码支付
        钱包余额 0

        抵扣说明:

        1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
        2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

        余额充值