00
- 没有考证过, 链式语法是什么时候引入到iOS/Mac开发中来的。 个人第一次接触从
blocksKit
开始:
NSArray *myArray = ...;
[[[myArray bk_reject:^BOOL (id obj) {
return .....;
}] bk_map:^id(id obj) {
return ...;
}] bk_each:^(id obj) {
....;
}];
- 然后后来在老郭(@老郭为人民服务)的
BeeFramework
框架中看到用block方式写的链式表达语法:
- 貌似真正让链式语法深入人心的, 还是
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();
}
不用想,大家都知道运行之后肯定是个悲剧:
为什么呢? [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
疗效如何? 事实是检验真理的唯一标准:
注意链式语法开头, 这是一个ALSQLSelectStatement
对象,但他的值为nil。
我们会看到,这个testcase能通过测试, 同时我们也会在console中看到表达式中有nil值的信息:
从log中我们就可以清楚的看到在哪里出现了nil, 还有方法的调用堆栈信息。
SafeBlocksChain 详细代码