前言

因为遇到了一个问题,在Swift项目中引用Objective-C编写的第三方库,由于这个库没有做好Nullability的处理,默认的返回的对象以及对象属性都是隐式拆包的,在某些情况下返回nil时,会导致程序崩溃,主要出现在使用getter,setter返回的对象,或者用strong修饰的属性

对于自己的编写的程序,我们大概清楚到底什么时候返回nil,什么时候不返回,而对于一些开放源码的第三方库,不清楚的情况下也能通过看代码解决问题,但如果是静态库,那就很蛋疼了,比如微信支付SDK。Apple引入nullability原本就是为了确保Objective-C向Swift的过渡,奈何两年多过去了,许多开发者依旧没有采用

处理方式

对于这种情况,在选择第三方库时,避免没有使用nullability的,如果已经采用了,那么在使用时,需要检查返回值,比如微信支付的回调处理中

1
2
3
4
var info:String = "支付失败"
if let err:String = resp.errStr {
    info += ": " + err
}

返回的PayResp的errStr属性默认是隐式拆包的,出现返回值为空时,就会Crash,另外一些针对UIKit对象的Category也有一样的问题,这个更容易被忽略

Nullability and Objective-C

这里对Apple文档的做一些记录,原链接在这里:Nullability and Objective-C

在Xcode 7及之后,Nullability相关的类型标记有四个:_Nullable 、_Nonnull、nullable、nonnull

区别用一段代码就可以表示:

1
2
3
4
5
- (AAPLListItem * _Nullable)itemWithName:(NSString * _Nonnull)name;
@property (copy, readonly) NSArray * _Nonnull allItems;

- (nullable AAPLListItem *)itemWithName:(nonnull NSString *)name;
@property (copy, readonly, nonnull) NSArray *allItems;

_Nullable与_Nunnull位于指针后,而nullable与nonnull放在类型之前,也可以存放在property中

当然如果觉得添加大量的nonnull很麻烦的话,可以采用标记头文件的部分区域,该区域内的普通指针默认标记为nonnull,而手动标记为nullable的不受影响,就像最初引入自动引用计数时一样,另外如果属性标记为weak,会有提示添加_Nullable或nullable标记。

示例代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<strong>NS_ASSUME_NONNULL_BEGIN</strong>
@interface AAPLList : NSObject <NSCoding, NSCopying>
// ...
- (nullable AAPLListItem *)itemWithName:(NSString *)name;
- (NSInteger)indexOfItem:(AAPLListItem *)item;

@property (copy, nullable) NSString *name;
@property (copy, readonly) NSArray *allItems;
// ...
@end
<strong>NS_ASSUME_NONNULL_END</strong>

// --------------

self.list.name = nil;   // okay

AAPLListItem *matchingItem = [self.list itemWithName:nil];  // warning!

采用nullability后,如果给一个标记为nonnull的参数传递nil会收到警告

另外标记区域对一些特殊的类型无效,包括:

  1. typedef 类型,它们受上下文影响可以为空也可以不为空,所以不能推断为nonnull
  2. 复杂的指针类型如:id *,需要准确的标记,例如:指向一个非空指针的,但可以为空的对象引用,_Nullable id * _Nonnull,id本身就是一个指针,可以为空,而它指向的指针不能为空
  3. 特殊的类型 **NSError ****,它是一个可以为nil的指针,而它指向的NSError对象引用也可以为nil

最后是两个例子:

Fabirc

使用NS_ASSUME_NONNULL_BEGIN与NS_ASSUME_NONNULL_END标记头文件,然后对可以为空的参数标记nullable,另外它也针对不支持nullability的环境做了兼容,重定义了相关了关键字

SDWebImage

默认使用小写,不带下划线的标记,对于block类型中的参数和const修饰的变量,使用大写带下划线的标记

1
2
3
4
@property (strong, nonatomic, <strong>nonnull</strong>) NSOperationQueue *downloadQueue;
@property (assign, nonatomic, <strong>nullable</strong>) Class operationClass;
extern NSString * <strong>_Nonnull</strong> const SDWebImageDownloadStartNotification;
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage * <strong>_Nullable</strong> image, NSData * <strong>_Nullable</strong> data, NSError * <strong>_Nullable</strong> error, BOOL finished);