YYModel源代码阅读 - 基础知识

YYModel堪称神级大作,虽然不免一些小bug,但是完全不影响膝盖碎一地。文章主要分享在阅读YYModel源代码时,所引发的知识点的补充与思考。

前言

这段时间因为工作需要,阅读了YYModel这个开源框架,至于它能做什么,最直白的讲述就是JSON与Model之间的相互转化。

源代码在Github,大家可以自行git clone或者download。

接下来,笔者主要分析阅读源代码而引出的各种问题与知识点,不足之处请大家指正。

NS_ASSUME_NONNULL_BEGIN & NS_ASSUME_NONNULL_END

这组宏是成对使用的,不得不说我们自己写代码的时候使用的很少,以至于遗漏这个知识点,现在我们就来看看这两个宏会引出什么问题。

这组宏会引出几个关于Objective-C新特性的知识点:

  • Nullability Annotations

  • Lightweight Generics

  • __kindof

Nullability Annotations

我们都知道在swift中,可以使用!和?来表示一个对象是optional的还是non-optional,如view?和view!。而在 Objective-C中则没有这一区分,view既可表示这个对象是optional,也可表示是non-optioanl。这样就会造成一个问题:在 Swift与Objective-C混编时,Swift编译器并不知道一个Objective-C对象到底是optional还是non-optional,因此这种情况下编译器会隐式地将Objective-C的对象当成是non-optional。

为了解决这个问题,苹果在Xcode 6.3引入了一个Objective-C的新特性:nullability annotations。这一新特性的核心是两个新的类型注释:** __nullable** 和 __nonnull 。从字面上我们可以猜到,__nullable表示对象可以是NULL或nil,而__nonnull表示对象不应该为空。当我们不遵循这一规则时,编译器就会给出警告。

我们来看看以下的实例,

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(NSString * __nonnull)name;
@end
@implementation TestNullabilityClass
...
- (void)testNullability {
[self itemWithName:nil]; // 编译器警告:Null passed to a callee that requires a non-null argument
}
- (id)itemWithName:(NSString * __nonnull)name {
return nil;
}
@end

不过这只是一个警告,程序还是能编译通过并运行。

事实上,在任何可以使用const关键字的地方都可以使用__nullable__nonnull,不过这两个关键字仅限于使用在指针类型上。而在方法的声明中,我们还可以使用不带下划线的nullablenonnull,如下所示:

1
2
3
4
5
6
7
8
- (nullable id)itemWithName:(NSString * nonnull)name
在属性声明中,也增加了两个相应的特性,因此上例中的items属性可以如下声明:

@property (nonatomic, copy, nonnull) NSArray * items;
当然也可以用以下这种方式:

@property (nonatomic, copy) NSArray * __nonnull items;
推荐使用nonnull这种方式,这样可以让属性声明看起来更清晰。

Nonnull区域设置(Audited Regions)

如果需要每个属性或每个方法都去指定nonnullnullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END。在这两个宏之间的代码,所有简单指针对象都被假定为 nonnull,因此我们只需要去指定那些nullable的指针。如下代码所示:

1
2
3
4
5
6
NS_ASSUME_NONNULL_BEGIN
@interface TestNullabilityClass ()
@property (nonatomic, copy) NSArray * items;
- (id)itemWithName:(nullable NSString *)name;
@end
NS_ASSUME_NONNULL_END

在上面的代码中,items属性默认是nonnull的,itemWithName:方法的返回值也是nonnull,而参数是指定为nullable的。

不过,为了安全起见,苹果还制定了几条规则:

  • typedef定义的类型的nullability特性通常依赖于上下文,即使是在Audited Regions中,也不能假定它为nonnull。

  • 复杂的指针类型(如id *)必须显示去指定是nonnull还是nullable。例如,指定一个指向nullable对象的nonnull指针,可以使用”__nullable id * __nonnull”。

  • 我们经常使用的NSError **通常是被假定为一个指向nullable NSError对象的nullable指针。

兼容性

因为Nullability Annotations是Xcode 6.3新加入的,所以我们需要考虑之前的老代码。实际上,苹果已以帮我们处理好了这种兼容问题,我们可以安全地使用它们:

  • 老代码仍然能正常工作,即使对nonnull对象使用了nil也没有问题。

  • 老代码在需要和swift混编时,在新的swift编译器下会给出一个警告。

  • nonnull不会影响性能。事实上,我们仍然可以在运行时去判断我们的对象是否为nil。

事实上,我们可以将nonnull/nullable与我们的断言和异常一起看待,其需要处理的问题都是同一个:违反约定是一个程序员的错误。特别是,返回值是我们可控的东西,如果返回值是nonnull的,则我们不应该返回nil,除非是为了向后兼容。

Lightweight Generics

Lightweight Generics 轻量级泛型,轻量是因为这是个纯编译器的语法支持(LLVM 7.0),和 Nullability 一样,没有借助任何 objc runtime 的升级,也就是说,这个新语法在 Xcode 7 上可以使用且完全向下兼容(更低的 iOS 版本)

带泛型的容器

这无疑是本次最重大的改进,有了泛型后终于可以指定容器类中对象的类型了:

1
2
NSArray<NSString *> *strings = @[@"sun", @"yuan"];
NSDictionary<NSString *, NSNumber *> *mapping = @{@"a": @1, @"b": @2};

返回值的 id 被替换成具体的类型后,令人感动的代码提示也出来了。

假如向泛型容器中加入错误的对象,编译器会不开心的。

系统中常用的一系列容器类型都增加了泛型支持,甚至连 NSEnumerator 都支持了,这是非常 Nice 的改进。和 Nullability 一样,我认为最大的意义还是丰富了接口描述信息,对比下面两种写法:

1
2
@property (readonly) NSArray *imageURLs;
@property (readonly) NSArray<NSURL *> *imageURLs;

不用多想就清楚下面的数组中存的是什么,避免了 NSStringNSURL 的混乱。

自定义泛型类

比起使用系统的泛型容器,更好玩的是自定义一个泛型类,目前这里还没什么文档,但拦不住我们写测试代码,假设我们要自定义一个 Stack 容器类:

1
2
3
4
5
@interface Stack<ObjectType> : NSObject
- (void)pushObject:(ObjectType)object;
- (ObjectType)popObject;
@property (nonatomic, readonly) NSArray<ObjectType> *allObjects;
@end

这个 ObjectType 是传入类型的 placeholder,它只能在 @interface 上定义(类声明、类扩展、Category),如果你喜欢用 T 表示也 OK,这个类型在 @interface@end 区间的作用域有效,可以把它作为入参、出参、甚至内部 NSArray 属性的泛型类型,应该说一切都是符合预期的。我们还可以给 ObjectType 增加类型限制,比如:

1
2
3
4
// 只接受 NSNumber * 的泛型
@interface Stack<ObjectType: NSNumber *> : NSObject
// 只接受满足 NSCopying 协议的泛型
@interface Stack<ObjectType: id<NSCopying>> : NSObject

若什么都不加,表示接受任意类型 ( id );当类型不满足时编译器将产生 error。
实例化一个 Stack,一切工作正常:

对于多参数的泛型,用逗号隔开,其他都一样,可以参考 NSDictionary 的头文件。

协变性和逆变性

当类支持泛型后,它们的 Type 发生了变化,比如下面三个对象看上去都是 Stack,但实际上属于三个 Type:

1
2
3
Stack *stack; // Stack *
Stack<NSString *> *stringStack; // Stack<NSString *>
Stack<NSMutableString *> *mutableStringStack; // Stack<NSMutableString *>

当其中两种类型做类型转化时,编译器需要知道哪些转化是允许的,哪些是禁止的,比如,默认情况下:

1
2
3
4
5
6
7
8
9
10
Stack *stack;
Stack<NSString *> *stringStack;
Stack<NSMutableString *> *mutableStringStack;

stack = stringStack;
stack = mutableStringStack;
stringStack = stack;
stringStack = mutableStringStack;
mutableStringStack = stack;
mutableStringStack = stringStack

在Xcode中我们可以看到,不指定泛型类型的 Stack 可以和任意泛型类型转化,但指定了泛型类型后,两个不同类型间是不可以强转的,假如你希望主动控制转化关系,就需要使用泛型的协变性和逆变性修饰符了:

1
2
__covariant - 协变性,子类型可以强转到父类型(里氏替换原则)
__contravariant - 逆变性,父类型可以强转到子类型(WTF)

协变

1
@interface Stack<__covariant ObjectType> : NSObject

逆变

1
@interface Stack<__contravariant ObjectType> : NSObject

协变是非常好理解的,像 NSArray 的泛型就用了协变的修饰符。

__kindof

__kindof 这修饰符还是很实用的,解决了一个长期以来的小痛点,拿原来的 UITableView 的这个方法来说:

1
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier;

使用时前面基本会使用 UITableViewCell 子类型的指针来接收返回值,所以这个 API 为了让开发者不必每次都蛋疼的写显式强转,把返回值定义成了 id 类型,而这个 API 实际上的意思是返回一个 UITableViewCellUITableViewCell 子类的实例,于是新的 __kindof 关键字解决了这个问题:

1
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

既明确表明了返回值,又让使用者不必写强转。再举个带泛型的例子,UIView 的 subviews 属性被修改成了:

1
@property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews;

这样,写下面的代码时就没有任何警告了:

1
UIButton *button = view.subviews.lastObject;

NS_ENUM & NS_OPTIONS

枚举是指将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。

枚举本质上是一个整数,枚举的作用是把值限定在指定的范围内,并且增加代码的可读性。 枚举的成员如果没有显示指定值,那么第一个成员的值总是0,后面成员的值依次递增。枚举可以直接用于比较。

一般我们声明枚举:

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>

// 声明枚举类型
enum Direction {up, down, left = 10, right};

int main(int argc, const char * argv[]){
...
}
其中up = 0, down = 1, left = 10, right = 11。

我们会发现枚举中一些不可自定义的部分,例如,枚举名。

NS_ENUM 和 NS_OPTIONS 都不算太古老的宏,在iOS 6 / OS X Mountain Lion才开始有,它们都是代替 enum 的更好的办法。

NS_ENUM

如果要在早期的iOS或OS X系统中使用这两个宏,简单定义一下就好

1
2
3
#ifndef NS_ENUM
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#endif

在OS X 10.4 中的原始定义如下:

1
2
#define NS_ENUM(_type, _name) CF_ENUM(_type, _name)
#define NS_OPTIONS(_type, _name) CF_OPTIONS(_type, _name)

在之前枚举可以这么定义:

1
2
3
4
5
6
typedef enum {
UITableViewCellStyleDefault,
UITableViewCellStyleValue1,
UITableViewCellStyleValue2,
UITableViewCellStyleSubtitle
};

或者

1
typedef NSInteger UITableViewCellStyle;

现在,有了统一的风格

1
2
3
4
5
6
typedef NS_ENUM(NSInteger, UITableViewCellSelectionStyle) {
UITableViewCellSelectionStyleNone,
UITableViewCellSelectionStyleBlue,
UITableViewCellSelectionStyleGray,
UITableViewCellSelectionStyleDefault
};

NS_ENUM 的第一个参数是用于存储的新类型的类型。在64位环境下,UITableViewCellStyleNSInteger 一样有8 bytes长。你要保证你给出的所有值能被该类型容纳,否则就会产生错误。第二个参数是新类型的名字。大括号里面和以前一样,是你要定义的各种值。

NS_OPTIONS

语法和 NS_ENUM 完全相同,但这个宏提示编译器值是如何通过位掩码 | 组合在一起的。

1
2
3
4
5
6
typedef NS_OPTIONS(NSUInteger, AMGResizing) {
AMGResizingNone = 0,
AMGResizingFlexibleWidth = 1 << 0,
AMGResizingFlexibleHeight = 1 << 1,
AMGResizingFlexibleUnicorn = 1 << 2
};

attribute((always_inline))

我们知道一般的函数调用都会通过call的方式来调用,这样让攻击很容易对一个函数做手脚,如果是以inline的方式编译的会,会把该函数的code拷贝到每次调用该函数的地方。而static会让生成的二进制文件中没有清晰的符号表,让逆向的人很难弄清楚逻辑。

__attribute__((always_inline)) 的意思是强制内联,所有加了__attribute__((always_inline)) 的函数再被调用时不会被编译成函数调用而是直接扩展到调用函数体内,比如定义了函数
__attribute__((always_inline)) void a()

1
2
3
void b(){  
a();

b 调用 a 函数的汇编代码不会是跳转到a执行,而是 a 函数的代码直接在 b 内成为 b 的一部分。
#define __inline __attribute__((always_inline)) 的意思就是用
__inline 代替__attribute__((always_inline))
内声明a的时候可以直接写成__inline void a() 这样比较方便因为__attribute__((always_inline)) 字多。

undef

这是预编译指令,和#define搭配使用,意思是取消之前的宏定义。

1
2
3
4
5
6
7
8
9
10
11
12
#define PROC_ADD 
void main(void)
{
#ifdef PROC_ADD
// Do this code here then undefined it to run the code in the else
// processing work
#undef PROC_ADD
#else
// now that PROC_ADD has been undefined run this code
// processing work
#endif
}

__unsafe_unretained

__unsafe_unretained是对对象的非zeroing的weak reference,意思是当对象所指向的内存被销毁了,对象还存在,称为“野指针”。

在iOS引入了Automatic Reference Count(ARC)之后,编译器可以在编译时对obj-c对象进行内存管理。大致规则如下:

1
2
3
4
5
alloc的要release;
retain/copy的要release;
NSAutoreleasePool在ARC中被禁止使用,替换成@autoreleasepool 函数体;
使用@ autoreleasepool,在函数入口的时候,autorelease pool入栈,正常退出时,autorelease pool出栈,从而释放变量.
注意:@ autoreleasepool在非ARC模式下,也能使用,并据说使用@autoreleasepool比使用NSAutoreleasePool速度能快6倍, 明显提升程序性能.

@package

为了强制一个对象隐藏其数据,编译器限制实例变量范围以限制其在程序中的可见性,但是为了提供灵活性,苹果也让开发者显式设置范围。

以下是这些关键字的使用范围:

  • @private

The instance variable is accessible only within the class that declares it.

实例变量只能被声明它的类访问.

  • @protected

The instance variable is accessible within the class that declares it and within classes that inherit it. All instance variables without an explicit scope directive have @protected scope.

实例变量能被声明它的类和子类访问,所有没有显式制定范围的实例变量都是.

  • @public

The instance variable is accessible everywhere.

实例变量可以被在任何地方访问.

  • @package

Using the modern runtime, an @package instance variable has @public scope inside the executable image that implements the class, but acts like @private outside.使用modern运行时,一个@package实例变量在实现这个类的可执行文件镜像中实际上是@public的,但是在外面就是@private【runtime需要再看一下苹果文档Runtime Programming Guide】

The @package scope for Objective-C instance variables is analogous to private_extern for C variables and functions. Any code outside the class implementation’s image that tries to use the instance variable gets a link error.

Objective-C中的@package与C语言中变量和函数的private_extern类似。任何在实现类的镜像之外的代码想使用这个实例变量都会引发link error

This scope is most useful for instance variables in framework classes, where @private may be too restrictive but @protected or @public too permissive.

这个类型最常用于框架类的实例变量,使用@private太限制,使用@protected或者@public又太开放. |