第一章 熟悉Objective-C
1. 了解Objective-C语言的起源
Objective-C(以下简称OC)是C语言的超集,相比C语言多了面向对象的特性。OC使用动态绑定的消息结构,换句话说,只有在运行时才会检查对象类型,发送一条消息,只有在运行时才决定真正要执行哪段代码。
2. 在类的头文件中尽量少引用其他头文件
尽量在类头文件中少引用其他头文件,而采用前向声明的方式,即@class语法,并在类的实现文件中引用需要的头文件。这样做的原因是避免引入过多的不必要的类,这也会增加代码编译时间。例如A.h引用了B.h,C.h,那么当在代码中引用A.h时,B.h和C.h也会被全部引入,这其实是不必要的。
3. 多用字面量语法,少用与之等价的方法
字面量语法,可以称之为“语法糖”,它的使用很简洁,效果和普通的方法相同。比如NSString,NSNumber,NSArray,NSDictionary等数据类型均有对应的字面量语法
NSString *str = @"this is a string";
NSNumber *num = @1;
NSArray *array = @[@"one", @"two", @"three", nil];
NSDictionary *dict = @{@"key1":@"value1",
@"key2":@"value2",
@"key3":@"value3"};
需要指出的是,使用字面量创建数组时,倘若其中有一个对象为nil,程序则会crash,同样地,使用字面量创建字典时,倘若有一个键值为空,也会导致crash,使用时应当注意。另外,使用此法创建的数组和字典均为不可改变的对象,如果需要可以改变的对象,可以调用mutablecopy方法。
4. 多用类型常量,少用#define预处理指令
使用define宏定义的方式不包含类型信息,编译器并不对类型进行检查,当需要定义常量时,如果仅供类内部使用推荐使用static const关键字进行定义(通常定义在类实现文件中,表明该常量并不需要对外公开),static表示常量只在其所在编译单元有效,const则使得常量不允许被更改。如果该常量需要在类外部使用时,可以在类头文件中使用extern关键字声明,在类实现文件中使用const关键字修饰并赋值。这样定义出来的常量具有类型信息,代码更容易理解。
5. 用枚举表示状态,选项和状态码
枚举非常适合用在表示对象经历的一系列状态,传给方法的选项或状态码等情形,我们可以给这些值起一些具有含义容易理解的名字,这样代码的可读性则会增加。Foundation框架中定义了两个宏,NS_ENUM和NS_OPTIONS,我们可以采用这两个宏来定义枚举。值得一提的是,当需要表示一些可以组合的选项时,我们可以指定枚举值为2的幂,这样我们可以对多个枚举值进行或运算来确定启用了哪些枚举选项,例如苹果在UIView中有如下枚举定义:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
我们可以这样来组合多个选项:
UIViewAutoresizing autoresizing = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleBottomMargin;
通常,在定义可组合枚举时我们应当使用NS_OPTIONS,而在其他情况下,我们使用NS_ENUM。
第二章 对象,消息,运行时
6. 理解“属性”这一概念
OC中类的相关数据信息会通过实例变量来保存,而属性则是对实例变量的一种封装,它包含了一组存取方法,用于对实例变量进行读取和写入操作。属性的声明使用@property关键字,同时我们可以在类的实现文件里使用@synthesize关键字来自动生成存取方法。事实上,我们在使用@synthesize的时候除了自动生成存取方法,还会生成一个与属性相对应的实例变量,该实例变量以下划线开头,后面与属性同名。属性在声明的时候可以指定一些属性“特性”,常见的有如下几种:
- 表示原子性。atomic表示属性是原子的,不允许有多个线程同时对属性进行读写操作。nonatomic则是非原子的。
- 表示读写权限。例如readwrite表示可读可写,readonly表示只读等。
- 表示内存管理语义。这个相对比较复杂,常见的几种有assin,strong,week,unsafe_unretained,copy等。strong和copy都会持有对象的强引用,不同是copy会先将对象拷贝一份。assin,week和unsafe_unretained则不持有对象,assin是单纯赋值,用于int,float,bool等简单类型,unsafe_unretained用于对象类型,week和unsafe_unretained的区别是在对象被释放后前者自动变为nil,后者则成为了野指针。
7. 在对象内部尽量直接访问实例变量
在类外部访问实例变量时,我们应当使用属性来访问。但是在类的内部,书籍作者建议读取使用实例变量,写入使用属性。这么做的原因是直接读取实例变量绕过了OC的消息发送机制,访问会更快一点。不过笔者认为,多数情况下应用的性能瓶颈主要并不在这些存取操作上,直接访问实例变量带来的性能提升是有限的,所以倒不如无论何时一律采用属性来访问实例变量来的简单,正像书籍作者所说,有些情况下,比如lazy initialization,在类的内部我们依旧需要使用属性来访问实例变量,直接访问实例变量是不可行的。
8. 理解“对象等同性”这一概念
通常,在OC中判断两个对象是否等同并不会用==,因为==实际上比较的是两个指针本身,而非它们指向的对象,instead,我们应该使用isEqual方法来比较两个对象的等同。如果我们希望对自定义的两个对象进行比较,那么是需要重写NSObject的isEqual方法和hash方法的,重写需要注意的是如何判定两个对象的相等,比如有时候我们并不需要比较对象的所有属性,只需要比较某些属性就可判断对象的相等。
9. 以“类簇模式”隐藏实现细节
类簇模式工作的过程是这样的,首先有一个“抽象类”提供一套公共的接口,在其背后会有多个子类继承于它,在使用者看来,他们操作的是“抽象类”的实例,事实上真正创建出来的是背后的某个子类实例。对使用者来说,他们并不需要关心类背后是如何实现的,这便是隐藏了实现细节。在苹果的cocoa框架中,有很多这样的例子,比如NSArray就是一个类簇,当我们创建NSArray对象时,实际上我们拿到的是某一种array的实例。尽管每一种类型的array是不同的,但他们均实现了NSArray类的一套公共接口,对使用者来说,他们是无差异的。类簇模式体现了封装性的思想。
10. 在既有类中使用关联对象存放自定义数据
有时候我们希望在某个类里保存一些额外的信息,通常的做法是创建一个继承于此类的子类,在子类中增加属性,来保存我们的额外信息。关联对象提供了另外一种方式来实现这个需求。关联对象是Runtime的一项技术。我们可以在运行时给一个对象关联一些其他的对象,关联的过程是通过key-value的形式,在需要的时候,我们同样通过key-value的方式从对象那里取到与其关联的对象。我们可以理解为给对象关联了一个NSDictionary,可以通过key获取对应的value值。关联对象相关的C语言API如下所示:
//设置关联对象,四个参数分别表示被关联对象,关联的key,关联的value(关联对象),关联的策略
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
//获取关联对象,两个参数分别为被关联对象,关联的key,返回值为与key对应的关联对象
id objc_getAssociatedObject(id object, const void *key);
//移除与某对象关联的所有对象,参数为被关联对象
void objc_removeAssociatedObjects(id object);
上面提到的关联策略表示的是内存管理的策略,是一种枚举类型,通常有如下几种,其所对应的内存管理语义也一并在表格中展示。
书中举了一个应用的例子:我们在使用UIAlertView时通常需要实现其委托协议,然后在协议实现里增加用户点击按钮所发生的操作,当有多个alertview时,为了避免混乱,通常我们需要设置每个alertview的tag,在点击按钮的协议方法里根据tag判断是哪个alertview,然后增加对应的处理代码。我们可以通过给alertview关联一个block来实现同样的效果,block里面写点击按钮执行的代码,这样的好处是调用alertview的代码和点击按钮执行的代码可以放在一起,更容易阅读。具体代码就不再详述了。
11. 理解Objc_msgSend的作用
Objective-C中方法的调用实际上是通过消息发送机制来完成的。不像C语言的静态绑定,在编译期函数实现地址就已经硬编码在指令当中,编译期就确定了调用一个方法时会执行什么样的代码,OC是一门动态的语言,给对象发送一条消息,会执行什么样的代码,只有在运行时才能确定。
一条完整的消息有消息名称和参数构成
[object sendmessage:param];
object为消息接受者,sendmessage为消息名称,param为参数。当编译器看到这样一条消息时,会将其转换成标准C函数调用,函数名为Objc_msgSend,该方法“原型”长这个样子:
void objc_msgSend(id self, SEL cmd, ...);
该方法是可变参数的,参数依次为消息接收者,选择子,消息参数(可能是多个)。该方法会负责进行方法的调用,其具体过程是这样的:
首先OC代码在编译期每个类都会对应生成一张方法映射表,键值为方法名,value为对应的方法实现地址,当对象收到一条消息时,会从方法映射表中查找是否存在可以响应的方法,如果有,则跳转到方法地址,如果没有则按照继承体系向上回溯,直到NSObject,如果此时依旧无法响应此消息,则会启动消息转发(消息转发将在下一条详述)。
这种在运行时动态查找的机制相比静态绑定速度会稍慢一些,因为它还有一个查找的过程,为此OC也做了一些优化,比如OC会生成一张方法缓存表,保存前面已经查找过的可以响应的方法,当再次调用这些方法时,就不再需要去方法映射表中查询,而是直接通过缓存表调用方法实现,速度上提升了一些。当然,即便如此它依旧没有硬编码方法的地址这种静态绑定来得快,但并没有关系,因为应用性能的瓶颈通常并不在这里。事物都是有两面性的,这种动态绑定的机制也带来了一些强大的特性。比如我们可以在运行时向类中添加方法,甚至交换两个方法的实现等,这些黑魔法在后面会详述。
12. 理解消息转发机制
当给对象发送一条其无法响应的消息时,此时并不会直接crash,而是启动消息转发。在消息转发过程中,开发者仍然有三次机会来处理此消息以避免应用crash。这三次机会对应了消息转发的三个阶段,倘若开发者在这三个阶段都没有正确处理此消息,那么应用也就crash了。接下来我们详细了解下消息转发的这三个阶段:
第一阶段是消息动态解析。当对象无法响应消息时,运行时会首先尝试是否可以通过动态增加方法的方式来处理此消息。这一阶段有两个相关的API,+resolveInstanceMethod: 和+resolveClassMethod: ,开发者可以去实现这两个方法(前者是用来处理实例方法的,后者是用来处理类方法的),并在方法里增加无法响应的消息(方法)的实现,这可以通过运行时函数class_addMethod来做。这样,运行时就可以通过上述两个API来帮开发者动态向类中添加方法,从而处理之前无法响应的消息,消息转发流程就结束了。但是,倘若开发者并未做上述的工作,消息转发就会进入第二阶段。
第二阶段是寻找备援接收者。到了这一阶段,运行时会尝试寻找是否有其他的对象可以处理此消息。与这一阶段相关的API是-forwardingTargetForSelector:,开发者可以去重载这个方法,并在这个方法里将消息转给另一个可以处理此消息的接收者。这样运行时就找到了备援接收者来处理无法响应的消息,消息转发流程结束。这一阶段处理的方式是将消息转给其他对象来处理,从表象来看,效果类似于实现了“多重继承”,因为原始的消息接受者似乎“继承”了多个类的特性,但实则是该对象将消息转给了其他类的对象来处理。同样地,倘若开发者并未重载上述API,运行时就无法找到备援的接收者,消息转发就会进入第三阶段。
第三阶段是启动完整消息转发机制。到了这一阶段,运行时会创建NSInvocation对象,将消息的所有细节封装到里面,包括选择子,参数等。与此阶段相关的API是-forwardInvocation:和-methodSignatureForSelector:,在调用forwardInvocation之前,运行时会调用methodSignatureForSelector方法,该方法为选择子生成一个方法签名,系统以此创建NSInvocation对象,因此两个方法开发者必须同时实现。在forwardInvocation方法中,开发者可以修改消息的接收者,这样做和第二阶段效果相同,但能在第二阶段处理最好不要等到第三阶段,因为越往后消息处理的代价就越大。开发者还可以修改消息内容,比如增加参数,更换选择子等等。
13. 用“方法调配技术”来调试“黑盒方法”
给对象发送一条消息只有在运行时才能确定执行什么样的代码,当然我们也就有办法在运行时修改方法的实现,来给方法增加额外的功能,不需要使用继承或者category。我们知道,每个类都有一张方法映射表,键对应方法名,值对应方法实现的地址(即IMP指针),我们可以操作运行时的API来改变键值的对应关系,从而交换两个方法的实现,这便是“方法调配”技术。
运行时给我们提供的API使我们可以在运行时为类增加方法,交换方法实现,改变已有方法的功能等。
交换两个方法的实现是通过method_exchangeImplementations方法来完成的,该方法有两个Method类型的参数,它们分别可以通过class_getInstanceMethod或者class_getClassMethod来从选择子中获取。比如我们可能想给NSString的lowercaseString方法在转换完字符串后打印一下字符串,这样或许方便调试,那么我们可以写一个自己的my_lowercaseString方法,调用系统的lowercaseString方法后再打印一下字符串,之后我们利用方法调配技术将我们自己实现的方法和系统的方法进行交换,那么当我们调用系统的lowercaseString方法时,就增加了自动打印字符串的功能,实现代码示例如下:
@implementation NSString (Custom)
- (NSString *)my_lowercaseString
{
//这里看似陷入了递归调用的死循环,事实上在运行时会交换方法实现,真正调用的是系统方法
NSString *lowercase = [self my_lowercaseString];
NSLog(@"transform before: %@, transform after: %@", self, lowercase);
return lowercase;
}
@end
然后我们使用如下代码交换方法实现
Method originMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(my_lowercaseString));
method_exchangeImplementations(originMethod, swappedMethod);
14. 理解“类对象”的用意
OC是一门完全动态的语言,给一个对象发送一条消息,在运行时系统会首先查询消息接收者所属的class,然后在该class的方法映射表中查询对应的选择子。OC判断对象属于哪个类,是通过isa指针得到的,isa指针指向了对象所属的类。描述OC对象的数据结构我们可以在运行时库的头文件中查到:
typedef struct objc_object {
Class isa;
}*id;
可以看出,每个对象结构体的首个字段就是isa,它描述了对象属于哪个类。同样我们可以看到类的定义:
struct objc_class {
struct objc_class *isa;
struct objc_class *super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
};
这个结构体里描述了类的详细信息,比如变量列表,方法列表等。可以看到,首个字段也是isa指针,事实上,类本身也是一个对象,称为“类对象”,即class object,和普通实例对象(instance object)的isa一样,class object的isa指针指向了class object所属的类,这个类称为meta class。每个class都有一个与之唯一对应的meta class,class是meta class的实例,更准确地说,class object是一个单例。meta class和class一样,实际上也是一个对象,也有isa指针,所有的meta class的isa指针都指向基类的meta class,基类的isa指向基类的meta class,而基类的meta class的isa指向它自己。下面这张图清晰地描述了class,meta class,基类,基类的meta class之间的关系:
我们可以通过类型信息查询方法来查询对象属于什么class类型,相关的方法有isMemberOfClass和isKindOfClass,他们会通过isa指针得到对象所属的class类型。因为class object是单例的,也就是说,我们也可以通过比较两个class object是否相同来判断对象是否属于某个class,例如:
id object = /*···*/;
if ([object class] == [NSString class]){ //object为NSString的实例 }
但我们最好不要这样做,而是依旧使用上面的类型查询方法,因为使用类型查询方法可以正确处理使用了消息转发机制的对象。比如我们通过forwardInvocation实现了消息转发,对象会将收到的消息转发给另一个对象,这种对象称为代理,它的基类是NSProxy。这种情况使用class方法会返回代理对象本身的class object,而使用类型信息查询方法时,代理对象会将其转发给真正的接受对象,从而得到的是真正的接受对象的class object,这两者是有区别的。
第三章 接口与API设计
15. 用前缀避免命名空间冲突
OC不像其他语言有命名空间的概念,所以这就有可能造成类之间命名的冲突,为了避免这种情况,通常我们可以给类名添加前缀。苹果宣称其保留使用两个字母的前缀,所以在使用前缀时我们可以采用三个字母的前缀。
16. 提供“全能初始化方法”
有时候,一个类可能具有多个初始化方法,比如苹果很多的控件的初始化就是这样,这种情况下一种比较好的处理方式是提供一个全能初始化方法(designated initializer),其他的初始化方法均通过调用此方法来完成初始化。另外在继承体系中,如果父类的全能初始化方法不适用于子类,子类需要重写父类的全能初始化方法。
17. 实现description方法
description是NSObejct协议中的方法。当我们调用NSLog打印某个对象的信息时,比如像下面这样:
NSLog(@"array is: %@", arrayObject);
实际上则是调用了NSArray的description方法。但是,如果是我们自定义的类,默认是无法使用该方法打印出对象的信息的,如果我们希望像系统class一样打印出来,我们可以在自己的类中实现description方法,在里面打印出我们需要的信息。
18. 尽量使用不可变对象
我们通常习惯使用属性来对类的成员变量进行封装,属性默认是可读可写的,当某些属性只读时,我们应尽量使用readonly来修饰,或者某些属性对外界只读,在类内部是可读写的,我们也可以在类头文件中声明为readonly,然后在实现文件中重新将属性声明为readwrite。总之这样做是当对象不需要被外界改变时,我们无须担心外界在使用中去修改了它。
19. 使用清晰而协调的命名方式
OC的方法命名通常比较长,而且很多时候还会带些介词in,with等,这种命名方式使得OC的代码很容易阅读,如果方法命名遵循了OC的规范,那么代码读起来就像是读故事一样,非常通顺流畅。通常方法命名上我们会采用驼峰的方式,在团队项目开发中,命名的风格尽量和整体框架保持一致。
20. 为私有方法名加前缀
事实上,在OC语言中,并不存在私有方法的概念,OC也没有途径声明某个方法是私有方法,并禁止对象调用该方法。因为OC的方法调用是在运行时动态查找的,即便是类头文件中没有声明的方法,只要在类实现文件中有实现,那么我们依旧可以在运行时调用该方法。即便如此,我们也应该在类的头文件中只公开类使用者需要的方法,与其无关的方法应该作为私有方法放到类实现文件中去实现,尽管我们无法阻止类使用者使用某些方式强行调用我们的“私有方法”,但我们通过这种编码方法告诉使用者不应该调用头文件中没有声明的方法。书籍的作者习惯在私有方法前加p_前缀,笔者则是习惯在类实现文件中将公开方法实现在前面,将私有方法实现在后面,中间使用“#pragma mark -- 以下为私有方法”标记的方式将它们隔开,这个就看个人习惯了。
21. 理解Objective-C错误模型
其他语言的开发者可能经常习惯在错误异常语句中处理各种错误,但OC的思考方式不是这样的。对于OC的异常而言,除非是遇到了比较严重的错误(比如错误导致需要退出应用),否则不要轻易使用OC的异常语句。在ARC下,使用OC的异常语句很可能导致内存泄漏问题。这是因为程序在执行时突然抛出异常,可能会中断后面的内存释放语句,导致对象无法释放的问题。虽然我们可以通过添加-fobjc-arc-exceptions标志来让编译器针对这种情况生成内存安全的代码,但这会大大降低运行效率,所以默认情况下编译器是不会做这种处理的,因为按照OC的思考逻辑,使用异常意味着已经遇到了比较严重的错误,多数情况下需要退出应用了,所以这种情况就没必要再去做内存释放操作了,应用一旦退出所有资源都会被系统回收的,退出前做释放操作意义并不大。
对于OC的错误处理,通常有两种方式。一种是使用委托协议。比如在网络下载中可能会出现下载失败的情况,我们可以使用委托协议,向委托对象发送成功或失败的消息,开发者可以在相应的消息中做不同处理。另一种方式是使用NSError。用法是这样的:
- (BOOL)doSomething:(NSError **)error;
该方法传入的是一个指向NSError的指针,这样我们就可以通过doSomething方法将错误信息写入到error中,并回传给调用者。对调用者而言,使用的方式是这样的:
NSError *error = nil;
BOOL result = [self doSomething:&error];
if (!result) {
//出错
NSLog(@"error is :%@", error);
}
这样的方法通常会返回一个bool值用以表示操作成功还是失败了。当使用者并不关心具体的错误时,可以只判断返回的bool值就好,必要的时候也可以使用error来获取具体的错误信息。
22. 理解NSCopying协议
对于系统的class而言,我们可以通过copy方法对某个对象进行拷贝操作,但如果是我们自定义的类,想实现同样的拷贝自定义类的对象的操作,那么我们需要让类实现NSCopying协议,并实现其中的copyWithZone方法。其定义如下:
- (id)copyWithZone:(nullable NSZone *)zone;
zone是早期内存管理中的概念,现在已经忽略zone这个东西了,我们无需关心。copy是NSObject实现的,但它仅仅是调用了copyWithZone并传入了一个默认区。
我们在使用copy方法时拷贝得到的是不可变的对象,如果需要可变对象,那么我们应该调用mutableCopy方法。因此如果我们的类需要 拷贝得到可变的对象,那么我们需要实现NSMutableCopying协议,并实现其中的mutableCopyWithZone方法。其定义如下:
- (id)mutableCopyWithZone:(nullable NSZone *)zone;
如果我们的类支持可变与不可变两个版本,那么需要同时实现这两个协议。需要注意的是不管什么情况,实现copyWithZone方法时我们应该返回不可变对象,实现mutableCopyWithZone方法时我们应该返回可变对象。另外在实现的时候应该注意深拷贝与浅拷贝的问题。例如Foundation框架中collection类拷贝都是浅拷贝,只会拷贝容器本身,并不会去拷贝容器里的对象。我们在实现拷贝方法时应该考虑使用浅拷贝还是深拷贝策略,通常可以和系统框架保持一致,使用浅拷贝,但也可以增加一个深拷贝方法。
第四章 协议与分类
23. 通过委托与数据源协议进行对象间通信
OC对象间通信的方式有很多,使用委托协议是一种比较常用的方法。这种方式的工作过程是,首先定义一组协议,并将实现了此协议的对象定义为对象A的delegate,然后任何实现了此协议的对象都可以成为对象A的委托,并接收来自对象A的一系列事件(这些事件定义为了协议里的方法)。例如UITableView就有两个协议,一个是数据源协议,一个是委托协议。通常在UITableViewController中我们只需要处理与UI展示相关的逻辑,而列表中数据的填充以及数据要经过何种运算等这些逻辑可以解耦到其他的class处理,我们只需要将class实现UITableViewController的数据源协议就可以接收到来自UITableViewController的数据处理相关事件方法。同样地,我们可以使用另外一个class来实现UITableViewController的委托协议,从而成为UITableViewController的委托,那么就可以接收到来自UITableViewController的与UI交互相关的事件方法。
24. 将类的实现代码分散到便于管理的数个分类之中
这里的分类指的是category。category是OC语言的一项特性。当我们想给某个类增加一些功能时,我们可以采用继承的方法,在子类中添加方法来实现我们需要的功能,OC提供了我们另外一种方式来实现这样的效果,那就是category。我们可以为某个类添加category,编译器在编译时会将原始的类以及类的category合并为一个类,也就是说,我们实现了给原始类添加我们自定义的方法的效果,当我们创建该类对象的,可以调用我们在category中定义的方法,这样讲是不是很酷呢?category的声明方式如下:
@interface NSString (UNUserNotificationCenterSupport)
// Use -[NSString localizedUserNotificationStringForKey:arguments:] to provide a string that will be localized at the time that the notification is presented.
+ (NSString *)localizedUserNotificationStringForKey:(NSString *)key arguments:(nullable NSArray *)arguments __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __TVOS_PROHIBITED;
@end
这里举了一个UserNotifications框架中的category的声明。它是给NSString添加了一个category,category的名称为UNUserNotificationCenterSupport,category中增加了方法localizedUserNotificationStringForKey:arguments,值得一提的是,当category中定义的方法在原始的类中已存在的时候,会覆盖掉原始类中的方法。
当一个类内容过于繁杂,涉及的模块有很多时,全部写在一个实现文件里会显得代码很长,那么我们可以把这个类分散到各个category中,每个category实现其中一个模块,这样某种程度上使得代码更方便管理,但category也不可滥用的,过度使用有时候反而造成不易管理。
25. 总是为第三方类的分类名称加前缀
如果我们想为一些第三方类添加category,那么最好为category名称以及category中的方法添加前缀,这样当别人使用了我们修改后的代码,也想为同样的类添加category时,就最大程度上避免了名称冲突的问题。
26. 勿在分类中声明属性
虽然从技术上来讲我们可以在category中声明属性,但这会带来问题。在OC的规范中,我们不应该在category中声明属性,category里只能添加方法。
27. 使用“class-continuation分类隐藏实现细节”
class-continuation分类特性是在类实现文件中使用的,其用法是这样的:
@interface SomeClass ()
{
NSString *string;
}
@end
@implement SomeClass
...
@end
像上面一样,我们可以在class-continuation中声明一些不需要在类头文件中公开的实例变量,也可以声明一些不需要公开的方法,还有前面提到的,有的属性对外界是只读的,但又需要在类内部可读写,我们可以在头文件中声明属性readonly,但在class-continuation中重新声明属性为readwrite。另外,如果类实现了某个协议,我们并不想让外界看到这个信息,我们也可以在class-continuation中声明类实现了某协议。这些处理的策略能够很好地隐藏实现细节。
28. 通过协议提供匿名对象
这里的匿名对象并不同于其他语言里的匿名对象,我们拿UITableView举例,UITableView有数据源协议和委托协议,在UITableView这个class中,定义了实现了这两个协议的两个属性,如下所示:
@property (nonatomic, weak, nullable) id <UITableViewDelegate> delegate;
该对象是id类型,即任何class对象都可以成为UITableView的delegate,只要它实现了UITableViewDelegate协议,从这个意义上讲,这个delegate就是“匿名”的。这种方式隐藏了对象的类型,当某些情况下我们并不关心对象的类型,只需要对象能响应某些方法,那么我们可以采用匿名对象。