本篇是我阅读《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》的摘要与总结。
一、熟悉Objective-C
1.了解Objective-C语言的起源
Objective-C为C语言添加了面向对象特性,是其超集。Objective-C使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行环境而非编译器来决定。
理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型与指针。
CoreGraphics 中的CGRect 属于结构体,如果用OC对象,性能会受影响
2.在类的头文件中尽量少引入其他头文件
除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明(forward declaring)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation”分类中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
头文件中若想导入其他的类,用@class
3.多用字面量语法,少用与之等价的方法
应该使用literal语法来创建字符串,数值,数组,字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
用literal语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
比如多用NSArray *array = @[@1,@2];少用NSArray *array = [NSArray arrayWithObjects:@1,@2,nil];
一句话,就是代码能写简单就写简单
4.多用类型常量,少用#define预处理指令
不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件中使用static const来定义只在编译单元内可见的常量。由于此类常量不在全局符号表中,所以无需为其名称加前缀。
在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称要加以区隔,通常用与之相关的类名做前缀。
用法:将#define EOCAnimatedViewAnimationDuration 0.3 替换为以下
EOCAnimatedView.h
extern const NSTime IntervalEOCAnimatedViewAnimationDuration;
EOCAnimatedView.m
const NSTime IntervalEOCAnimatedViewAnimationDuration = 0.3;
注意命名
5.用枚举表示状态、选项、状态码
应该用枚举来表示状态机的状态、传递给方法的选项遗迹状态码等值,给这些值起个易懂的名字。
如果把传递给某个方法的选项表示为枚举型,而多个选项又可同时使用,那么就将各选项值定义为2的幂,以便通过按位或者操作将其组合起来。
用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有的枚举。
用法:
typedef NS_Enum(NSUInteger,EOCConnectionState){
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
}
switch(_currentState){
EOCConnectionStateDisconnected:
//Handle disconnected state
break;
EOCConnectionStateConnecting:
//Handle disconnected state
break;
EOCConnectionStateConnected:
//Handle disconnected state
break;
}
1 << 2 = 0b100
0b1111 >> 3 = 0b0001
二、对象、消息、runtime
6.理解“属性”这一概念
可以通过@property语法来定义对象中所封装的数据。
通过“特质”来指定存储数据所需的正确语义
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
开发iOS程序时,应该使用nonatomic属性,因为atomic属性会严重影响性能。
7.在对象内部尽量直接访问实例变量
由于不经过Objective-C 的“方法派发”(method dispatch,见11)步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问保存对象实例变量的那块内存。
直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比方说,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值并释放旧值。
如果直接访问实例变量,那么不回触发“键值观察(Key-Value Observing, KVO)”通知。这样做是否会产生问题,还取决于具体的对象行为。
通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”和/或“设置方法”中新增“断点(breakpoint)”监测该属性的调用者及其访问时机
在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,应该通过属性来写。
在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
有时会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据。
8.理解“对象等同性”这一概念
若想检测对象的等同性,请提供“isEqual:”与hash方法。
相同的对象必须具有相同的hash码,但是两个hash码相同的对象却未必相同。
不要盲目的逐个监测每条属性,而是应该依照具体需求来制定检测方案。
编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
对象是否相同 不能用== 要用isEqual
NSString*foo =@"badger 123";
NSString*bar = [NSStringstringWithFormat:@"badger %i",123];
BOOLequalA = (foo == bar );//< equalA = NO
BOOLequalB = [fooisEqual:bar];//< equalB = YES
BOOLequalC = [fooisEqualToString:bar];//< equalC = YES
9.以“类族模式”隐藏实现细节
子类应该继承自类族中的抽象基类。
子类应该定义自己的数据存储方式。
子类应当复写超类文档中指明需要复写的方法
类族模式可以把实现细节隐藏在一套简单的公共接口后面。
系统框架中经常使用类族。
从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
例如NSArray与NSMutableArray
// YDEmployee.h
#import
typedefNS_ENUM(NSUInteger, YDEmployeeType){
YDEmployeeTypeDeveloper,
YDEmployeeTypeDesigner,
YDEmployeeTypeFinance,
};
@interfaceYDEmployee :NSObject
@property(nonatomic, copy)NSString*name;
@property(nonatomic, assign)NSUIntegersalary;
//1.定义类方法,根据不同type,放回同一父类对象
+(YDEmployee*)employeeWithType:(YDEmployeeType)type;
-(void)doWork;
@end
// YDEmployee.m
#import"YDEmployee.h"
//3.定义各自子类
@interfaceYDEmployeeDeveloper :YDEmployee
@end
@implementationYDEmployeeDeveloper
-(void)doWork
{
[selfwriteCode];
}
-(void)writeCode
{
/////
}
@end
@interfaceYDEmployeeDesigner :YDEmployee
@end
@implementationYDEmployeeDesigner
-(void)doWork
{
[selfdesighApp];
}
-(void)desighApp
{
/////
}
@end
@interfaceYDEmployeeFinance :YDEmployee
@end
@implementationYDEmployeeFinance
-(void)doWork
{
[selfmanageFinance];
}
-(void)manageFinance
{
////
}
@end
@implementationYDEmployee
//2.抽象基类没有特殊标识,一般不定义init方法也不实现抽象函数,各自在子类里实现
+(YDEmployee*)employeeWithType:(YDEmployeeType)type
{
switch(type) {
caseYDEmployeeTypeDeveloper:
return[YDEmployeeDevelopernew];
break;
caseYDEmployeeTypeDesigner:
return[YDEmployeeDesignernew];
break;
caseYDEmployeeTypeFinance:
return[YDEmployeeFinancenew];
break;
default:
break;
}
returnnil;
}
-(void)doWork
{
//2.1抛出一个异常,避免在基类里面实现
NSException*e = [NSException
exceptionWithName:@"exceptionName"
reason:@"必须在子类实现改方法"
userInfo:nil];
@throwe;
}
@end
10.在既有类中,使用关联对象(Associated Object)存放自定义数据
可以通过“关联对象”机制来把两个对象连起来。
定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug。
下列方法可以管理关联对象:
voidobjc_setAssociatedObject(idobject,void*key,idvalue, objc_AssociationPolicy policy)此方法以给定的键和策略为某对象设置关联对象值。
idobjc_getAssociatedObject(idobject,void*key)此方法根据给定的键从某对象中获取相应的关联对象值。
voidobjc_removeAssociatedObjects(idobject)此方法移除指定对象的全部关联对象。
关联对象类型 等效的@property
OBJC_ASSOCIATION_ASSIGN assign
_RETAIN_NONATOMIC nonatomic,retain
...
11.理解objc_msgSend的作用
消息由接受者,selector及参数构成。给某对象“发送消息”也就相当于在该对象上调用方法。
发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
id returnValue = [someObject messageName:parameter];
编译器看到消息后将其转换为标准的C语言函数调用,原型是:
void objc_msgSend(id self,SEL cmd,...)
转换后:
id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);
12.理解消息转发机制
若对象无法响应某个selector,则进入消息转发流程。
通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
对象可以将其无法解读的某些selector转交给其他对象处理。
经过上述两步后,如果还是没办法处理selector,那就启动完整的消息转发机制。
13.用method swizzling调试黑盒方法
在runtime中,可以向类中新增或替换selector所对应的方法实现。
使用另一份实现来替换原有的方法实现,这道工序叫做method swizzling,开发者常用此技术向原有视线中添加功能。
一般来说,只有调试程序的时候才需要在runtime中修改方法实现,这种做法不宜滥用。
//获取方法实现
Method originalMethod =class_getInstanceMethod([NSStringclass],@selector(lowercaseSting));
Method swappedMethod =class_getInstanceMethod([NSStringclass],@selector(uppercaseString));
//交换方法
method_exchangeImplementations(originalMethod, swappedMethod);
14.理解“类对象”的用意
每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
对象都有isa指针,指向的是对象的元类(metaclass)
id 中也包含isa指针,id对象定义
typedef struct objc_object{
Class isa;
} *id;
三、接口与API设计
15.用前缀避免命名空间冲突
选择与你公司、应用程序或者二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀。
若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
Apple宣称保留使用所有两字母前缀的权利,所以自己所选用的前缀最好是三字母的。
比如你的程序中引用了XYZLibrary,想要发布,那么应该都改为YDXYZLibrary,防止直接他人引入XYZLibrary产生冲突
16.提供“全能初始化方法”
在类中提供一个全能初始化方法,并于文档里指明。其它初始化方法均应调用此方法。
若全能初始化方法与超类不同,则需覆写超类中对应方法。
如果超类的初始化方法并不适用于子类,那么应该覆写这个超类方法,并在其中抛出异常。
抛出异常方法:
@throw[NSExceptionexceptionWithName:NSInternalInconsistencyExceptionreason:@"Must use initWithDimension:instead."userInfo:nil];
17.实现description方法
实现description方法返回一个有意义的字符串,用以描述该实例。
若想在调试时打印出更详尽的对象描述信息,则应该实现debugDescription方法。
//name,age 为对象属性
- (NSString*)description{
return [NSString stringWithFormat:@"%@ %d",_name,_age];
}
- (NSString*)debugDescription{
return [NSString stringWithFormat:@"<%@: %p,\"%@ %d\">",[self class],self,_name,_age];
}
18.尽量使用不可变对象
尽量创建不可变的对象。
若某属性仅可于对象内部修改,则在“class-continuation分类”中将其由readonly属性扩展为readwrite属性。
不要把可变的collection作为属性公开,而应提供相关方法,一次修改对象中的可变collection。
扩展例子
.h 中
@property(nonatomic,copy,readonly)NSString*someString;
//提供初始化方法
- (instancetype)initWithSomeString:(NSString*)someString;
.m中class-continuation 中
@property(nonatomic,copy,readwrite)NSString*someString;
@property(nonatomic,strong,readwrite)dispatch_queue_tsyncQueue;
.m中
//派发队列防止发生“竞争条件”
_syncQueue=dispatch_queue_create("com.manqian.syncQueue",NULL);
- (NSString*)someString{
__blockNSString*localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString =_someString;
});
returnlocalSomeString;
}
- (void)setSomeString:(NSString*)someString
{
dispatch_barrier_async(_syncQueue, ^{
_someString= someString;
});
}
19.使用清晰而协调的命名方式
起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
方法名利不要使用缩略后的类型名称。
给方法吗起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
继承至UIView的子类,类名末尾一定是View,同理其他的也需要这样
20.为私有方法名加前缀
给私有方法的名称加上前缀,这样可以很容易的将其通公共方法区分开。
不要单用一个下划线做私有方法的前缀,因为这种做法的预留给苹果公司用的。
建议私有方法用p_ 开头,如
- (void)p_privateMethod {
/* … */
}
21.理解Objective-C错误模型
只有发生了可使整个应用程序崩溃的严重错误时,才使用异常。
在错误不那么严重的情况下,可以指派委托方法来处理错误,也可把错误信息放在NSError对象里,经由输出参数返回给调用者。
NSError ** 会转化为 MSError *__autoreleasing*
意为 指向指针的指针
22.理解NSCopying协议
若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝。
如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
NSCopying协议只有一个方法
- (id) copyWithZone:(NSZone *)zone //zone 区,每个程序只有一个“默认区”
深拷贝 会新建一个对象,原来的对象被销毁了,深拷贝的对象不会被销毁
浅拷贝 不会新建对象,只是新增了一个指针指向原来的对象,原来的对象被销毁,浅拷贝的东西也被销毁
四、协议与分类
23.通过委托与数据源协议进行对象间通信
委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
将委托对象应该支持的接口定义成协议,在协议中把可能需要吃力的事件定义成方法。
当某对象需要从另外一个对象中获取数据时,可使用委托模式。在这种情况下,该模式亦称数据源协议。
若有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存至其中。
数据流向
Data Source —>class—>Delegate
#import <Foundation/Foundation>
@classYDNetworkFetcher;
@protocolYDNetworkFetcherDelegate
@optional
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didFailWithError:(NSError*)error;
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didUpdateProgressTo:(float*)progress;
@end
@interfaceYDNetworkFetcher :NSObject
/***设置代理*/
@property(nonatomic,weak)id delegate;
@end
#import"YDNetworkFetcher.h"
@interfaceYDNetworkFetcher()
{
struct{
unsignedintdidReceiveData :1;
unsignedintdidFailWithError :1;
unsignedintdidUpdateProgressTo :1;
} _delegateFlags;
}
@end
@implementationYDNetworkFetcher
- (void)setDelegate:(id)delegate{
iddelegateObject = delegate;
_delegate= delegate;
//将代理是否执行方法缓存起来
_delegateFlags.didReceiveData= [delegateObjectrespondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailWithError= [delegateObjectrespondsToSelector:@selector(networkFetcher:didFailWithError:)];
_delegateFlags.didUpdateProgressTo= [delegateObjectrespondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didReceiveData:(NSData*)data
{
//不需要每次都去查找是否实现该方法
if(_delegateFlags.didReceiveData) {
[_delegatenetworkFetcher:selfdidReceiveData:data];
}
}
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didFailWithError:(NSError*)error{
if(_delegateFlags.didFailWithError) {
[_delegatenetworkFetcher:selfdidFailWithError:error];
}
}
- (void)networkFetcher:(YDNetworkFetcher*)fetcher didUpdateProgressTo:(float*)progress{
if(_delegateFlags.didUpdateProgressTo) {
[_delegatenetworkFetcher:selfdidUpdateProgressTo:progress];
}
}
@end
24.将类的实现代码分散到便于管理的数个分类之中
使用分类机制把类的实现代码划分成易于管理的小块。
将应该视为私有的方法归入名叫Private的分类中,以隐藏实现细节。
@interfaceYDPerson :NSObject
@property(nonatomic,copy)NSString*firstName;
@property(nonatomic,copy)NSString*lastName;
@property(nonatomic,strong)NSArray*friends;
- (instancetype)initWithFirstName:(NSString*)firstName
LastName:(NSString*)lastName
Friends:(NSArray*)friends;
@end
@interfaceYDPerson (Friendship)
- (void)addFriend:(YDPerson*)person;
- (void)removeFriends:(YDPerson*)person;
- (BOOL)isFriendsWith:(YDPerson*)person;
@end
@interfaceYDPerson (Work)
- (void)performDaysWork;
- (void)takeVacationFromWork;
@end
@interfaceYDPerson (Play)
- (void)goToTheCinema;
- (void)goToSportsGame;
@end
25.总是为第三方类的分类名称加前缀
向第三方类中添加分类时,总应给其名称加上你专用的前缀。
向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
26.勿在分类中声明属性
把封装数据所用的全部属性都定义在主接口里。
在class-continuation分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
27.使用class-continuation分类隐藏实现细节
通过class-continuation分类向类中新增实例变量。
如果某属性在主接口中声明为只读,而类的内部又要用设置方法修改此属性,那么就在class-continuation分类中将其扩展为可读写。
把私有方法的原型声明在class-continuation分类里面。
若想使类遵循的协议不为人所知,则可于class-continuation分类中声明。
28.通过协议提供匿名对象
协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某些一的id类型,协议里规定了对象所应实现的方法。
使用匿名对象来隐藏类型名称或类名。
如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。