链式语法空指针的坑...

00

  1. 没有考证过, 链式语法是什么时候引入到iOS/Mac开发中来的。 个人第一次接触从blocksKit开始:
NSArray *myArray = ...;
[[[myArray bk_reject:^BOOL (id obj) {
     return .....;
}] bk_map:^id(id obj) {
    return ...;
}] bk_each:^(id obj) {
    ....;
}];
  1. 然后后来在老郭(@老郭为人民服务)的BeeFramework框架中看到用block方式写的链式表达语法:
链式语法空指针的坑..._第1张图片
blocks_chain_beeframework.png
  1. 貌似真正让链式语法深入人心的, 还是Masonry(忽略了一点: RAC其实也有链式语法, 也是链式语法变得流行的一个原因):
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

然后, 就出现了N多通过分析masnory代码来说明如何写链式语法的教程。 不过这些文章都有意无意的忽略了一个问题:

链式表达式里边出现空指针怎么办???

01

回头来看看, OC里头链式语法的几种实现方式:

  • 传统方法调用blocksKit, RAC为代表, 实现也比较简单。 由于OC可以向nil对象发送任意消息, 这种实现也不会导致空指针的问题, 比较安全。唯一的缺点就是, 链式表达式的开头会出现一大堆方括号『[』(参见RAC),比较别扭。
  • 通过block实现 这种实现看起来比较优雅(再也没有开头那一堆 [ 了), 不过也在代码中埋了空指针这个雷。

02

为什么会有空指针问题?

我们看看下边这段代码:

#include 
typedef void (^block)(void);

int main() {
    block b = NULL;
    b();
}

不用想,大家都知道运行之后肯定是个悲剧:

blocks_chain_null_block.png

为什么呢? [nil doSomething]nil()有什么区别呢?

我们把上边的代码用clang改写成c++的形式(clang -rewrite-objc xxx.c):

typedef void (*block)(void);

int main() {
    block b = __null;
    ((void (*)(__block_impl *))((__block_impl *)b)->FuncPtr)((__block_impl *)b);
}

注意这个: ((__block_impl *)b)->FuncPtr, 实际上就是 NULL->FuncPtr, 所以, 毫无疑问, 肯定是一个空指针异常。

回到我们用block实现链式语法的一般形式:

@interface Foo:NSObject
@property(readonly, copy) Foo *(^MY_BLOCK)();
//...
@end

@implementation Foo

- (Foo (^)()) MY_BLOCK {
    return ^Foo *() {
        //...
        return self;
    };
}

@end

使用方式:

Foo *obj = // get foo instance value here  <- 1
obj.MY_BLOCK()...

如果第一行返回的是正常的Foo实例, 大家都开心。 但是,你知道的, OC是一个动态语言,而且由于nil可以接收任何消息, 所以大家都不会去注意这里的坑:万一 obj = nil呢?BOOM!!!

03

这可是一个令人沮丧的问题, 谁也不想在自己的项目中埋一堆雷! 可是就只能放弃么? 河豚美味有剧毒, 但是吃死的人很少, 关键是怎么制作!

一线曙光:LinkBlock
这个库基本上能做到放心的使用链式block语法了,可是(有可是说明还是有坑),仔细看了源码, 这里的实现是, 所有的block都是做为NSObject属性通过NSObject的category来实现的。。。。心里边冷了一半:(哦, 不对, 毕竟我已经踩在作者的肩膀上, 看到远处的曙光了)。

下一步怎么办呢???

我们的目的很简单:

  • 返回值不能为nil
  • 返回类型必须是方法返回值类型(有点绕口?想一下OC是动态语言)
  • 能标记这个返回值实际上代表的是nil

不得不说, OC Runtime真是个好东西(吐槽一下, 为啥swift就放弃为动态语言特性呢。。。我其实一直盼望swift会是一个强类型动态语言)。

我们可以借鉴KVO的实现机制!

我们只要动态生成一个返回值类型的子类实例就可以了!

Talk is cheap, show me the code!

//
//  SafeBlocksChain.h
//  patchwork
//
//  Created by Alex Lee on 7/18/16.
//  Copyright © 2016 Alex Lee. All rights reserved.
//

NS_ASSUME_NONNULL_BEGIN

extern id _Nullable instanceOfFakeBlocksChainClass(Class srcClass, NSString *file, NSInteger line, NSString *funcName,
                                                      NSArray *stack);

#define SafeBlocksChainObj(obj, CLASS)  ((CLASS *)((obj) ?: \
    instanceOfFakeBlocksChainClass([CLASS class],           \
    (__bridge NSString *)CFSTR(__FILE__),                   \
    __LINE__,                                               \
    [NSString stringWithUTF8String:__PRETTY_FUNCTION__],    \
    backtraceStack(10)) ))

#define ObjIsValidBlocksChainObject(obj)                \
    ({                                                  \
        BOOL ret = [(obj) isValidBlocksChainObject];    \
        if (!ret) {                                     \
            ALLogError(@"%@", (obj));                   \
        }                                               \
        ret;                                            \
    })


@interface NSObject (SafeBlocksChain)

- (BOOL)isValidBlocksChainObject;

@end

NS_ASSUME_NONNULL_END
//
//  SafeBlocksChain.m
//  patchwork
//
//  Created by Alex Lee on 7/18/16.
//  Copyright © 2016 Alex Lee. All rights reserved.
//

#import "SafeBlocksChain.h"
#import "UtilitiesHeader.h"
#import "ALOCRuntime.h"
#import "NSString+Helper.h"
#import "ALLogger.h"
#import "ALLock.h"


static NSString * const kFakeChainingObjectProtocolName = @"__AL_BlocksChainFakeObjectProtocol";

static Protocol *fakeBlocksChainProtocol() {
    const char *protochlCName = [kFakeChainingObjectProtocolName UTF8String];
    
    __block Protocol *protocol = nil;
    static_gcd_semaphore(sem, 1);
    with_gcd_semaphore(sem, DISPATCH_TIME_FOREVER, ^{
        if ((protocol = objc_getProtocol(protochlCName)) != nil) {
            return;
        }
        protocol = objc_allocateProtocol(protochlCName);
        if (protocol) {
            objc_registerProtocol(protocol);
        }
    });
    
    return protocol;
}

static Class fakeBlocksChainClass(Class forClass) {
    if (forClass == nil) {
        return Nil;
    }

    Protocol *fakeProtocol = fakeBlocksChainProtocol();
    if (class_conformsToProtocol(forClass, fakeProtocol)) {
        return forClass;
    }

    const char *classname =
        [[NSStringFromClass(forClass) stringByAppendingString:@"_ALBlocksChainFakeClass"] UTF8String];

    __block Class fakeclass = Nil;
    static_gcd_semaphore(sem, 1);
    with_gcd_semaphore(sem, DISPATCH_TIME_FOREVER, ^{
        if ((fakeclass = objc_getClass(classname)) != nil) {
            return;
        }

        fakeclass = objc_allocateClassPair(forClass, classname, 0);
        if (fakeclass != Nil) {
            class_addProtocol(fakeclass, fakeProtocol);
            objc_registerClassPair(fakeclass);
        }
    });
    return fakeclass;
}

id instanceOfFakeBlocksChainClass(Class srcClass, NSString *file, NSInteger line, NSString *funcName,
                                  NSArray *stack) {
    Class cls = fakeBlocksChainClass(srcClass);
    if (cls != Nil) {
        id fakeObj = [[cls alloc] init];

        IMP descIMP = imp_implementationWithBlock(^NSString *(__unsafe_unretained id obj) {
            NSMutableString *desc = [NSMutableString string];
            [desc appendFormat:
                      @"*** Found nil object (expected type: %@) in blocks-chain expression, first occurred in:\n",
                      srcClass];
            [desc appendFormat:@"    %@ (%@:%ld)\n", funcName, [stringValue(file) lastPathComponent], (long) line];

            [desc appendString:@"*** Backtrace:\n{\n"];
            for (NSString *frame in stack) {
                [desc appendFormat:@"    %@\n", frame];
            }
            [desc appendString:@"}"];

            return desc;
        });

        Method descMethod = class_getInstanceMethod(srcClass, @selector(description));
        method_setImplementation(descMethod, descIMP);
        return fakeObj;
    }
    return nil;
}

@implementation NSObject(SafeBlocksChain)

- (BOOL)isValidBlocksChainObject {
    return ![self conformsToProtocol:fakeBlocksChainProtocol()];
}

@end

疗效如何? 事实是检验真理的唯一标准:

链式语法空指针的坑..._第2张图片
blocks_chain_testcase.png

注意链式语法开头, 这是一个ALSQLSelectStatement对象,但他的值为nil
我们会看到,这个testcase能通过测试, 同时我们也会在console中看到表达式中有nil值的信息:

链式语法空指针的坑..._第3张图片
blocks_chain_testcase_log.png

从log中我们就可以清楚的看到在哪里出现了nil, 还有方法的调用堆栈信息。

SafeBlocksChain 详细代码

你可能感兴趣的:(链式语法空指针的坑...)