这是Objective-C系列的第4篇。
- Objective-C语言(一)熟悉Objective-C
- Objective-C语言(二)对象、消息、运行期
- Objective-C语言(三)接口与API设计
- Objective-C语言(四)协议与分类
- Objective-C语言(五)系统框架
- Objective-C语言(六)Block与GCD
一、最佳实践
- 在开发中合理巧妙的使用位段
-
将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小模块;
- 将应该视为“私有”的方法归入名叫
Private
的分类中,以隐藏实现细节。
-
总是为第三方类的分类名称添加前缀
- 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
- 向第三方类中添加分类时,总应该给其中的方法加上你专用的前缀。
-
勿在分类中声明属性
-
把封装数据所用的全部属性都定义在主接口里;
-
在“class-continuation”分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
编者按:在很多第三方开源库中,使用“关联对象”来在分类中定义属性是很常见的手段。
-
-
使用“class-continuation”分类隐藏实现细节
- 通过“class-continuation”分类向类中新增实例变量;
- 如果某属性在主接口总声明为“readonly(只读)”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation”中将其扩展为“readwrite(可读写)”;
- 把私有方法的原型声明声明在“class-continuation”里面;
- 若想使类所遵循的协议不为人所知,则可与“class-continuation”中声明。
-
通过协议提供匿名对象
- 协议可在某某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某些一的id类型,协议里规定了对所应事先的方法。
- 使用匿名对象来隐藏类型名称(或类名)。
- 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。
二、实践详解
2.1 位段
在委托代理中,如果要频繁检查该代理是否响应某个方法,那么将代理相应能力缓存起来达到优化。而优化的最佳途径就使用“位段”。“位段”是一个C语言数据类型。
关于位段,简要做个说明:
2.1.1 定义
struct bs{
int a:1;
int :2; //无位段名,它只用来作填充或调整位置
int b:3;
int :0; //空域
int c:5; //从下一单元开始存放
};
struct bs data;
或者:
struct bs{
int a:1;
int :2;
int b:3;
int :0;
int c:5;
} data;
2.1.2 示例
#include <stdio.h>
int main(){
struct{
unsigned a:1;
unsigned b:3;
unsigned c:4;
} bit, *pbit;
bit.a=1;
bit.b=7;
bit.c=15;
printf("%d, %d, %d\n", bit.a, bit.b, bit.c);
pbit=&bit;
pbit->a=0;
pbit->b&=3;
pbit->c|=1;
printf("%d, %d, %d\n", pbit->a, pbit->b, pbit->c);
return 0;
}
- 位段的类型只能是int,unsigned int,signed int三种类型,不能是char型或者浮点型;
- 位段占的二进制位数不能超过该基本类型所能表示的最大位数,比如在VC中int是占4个字节,那么最多只能是32位;
- 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的;
- 若位段占的二进制位数为0,则这个位段必须是无名位段,下一个位段从下一个位段存储单元开始存放;
2.1.3 位段在委托代理模式中的应用
@class HONetworkFetcher;
@protocol NetworkFetcherDelegate <NSObject>
@optional
- (void)networkFetcher:(HONetworkFetcher*)fetcher didReceiveData:(NSData*)data;
- (void)networkFetcher:(HONetworkFetcher*)fetcher didFailerWithError:(NSError *)error;
- (void)networkFetcher:(HONetworkFetcher*)fetcher didUpdateProgerssTo:(float)progress;
@end
@interface HONetworkFetcher : NSObject
@property (nonatomic ,weak) id<HONetworkFetcherDelegate> delegate;
@end
#import "HONetworkFetcher.h"
@interface HONetworkFetcher()
{
struct {
unsigned int didReceiveData :1;
unsigned int didFailedWithError :1;
unsigned int didUpdateProgressTo :1;
} _delegateFlags;
}
@end
@implementation HONetworkFetcher
/**
* 在设置代理的时候检查方法可达性,并缓存起来
*/
- (void)setDelegate:(id<HONetworkFetcherDelegate>)delegate
{
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didFailedWithError = [delegate respondsToSelector:@selector(networkFetcher:didFailerWithError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgerssTo:)];
}
@end
2.2 将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小模块;
- 将应该视为“私有”的方法归入名叫
Private
的分类中,以隐藏实现细节。
2.3 总是为第三方类的分类名称添加前缀
分类为现有类添加新功能,假如多个分类都为该类添加了同一个方法名的某一个方法,那么在运行时,会造成该方法名多次覆盖,以最后一次覆盖为主。假如遇到这种情况的bug,很难追溯源头,因为你不知道,其他人也重写了该方法。所以为了避免这种情况的发生,就需要为分类加上前缀,作为一个“命名空间”,比如:
@interface NSString (HOG_HTTP)
- (NSString*)hog_urlEncodedString;
@end
即便加了前缀,也难保其他分类不会覆盖你所写的放方法。但是降低了概率。
- 向第三方类中添加分类时,总应该给其名称加上你专用的前缀;
- 向第三方类中添加分类时,总应该给其中的方法加上你专用的前缀。
2.4 勿在分类中声明属性
属性是封装数据的方式。在技术上,分类也可以声明属性,但是要避免这种做法。
声明文件:
@interface HOPerson (Friends)
@property (nonatomic ,strong)NSSet *friends;
@end
实现文件:
@implementation HOPerson(Friends)
@end
这时会发出警告:
HOPerson+Friends.m:11:17: Property 'friends' requires method 'friends' to be defined - use @dynamic or provide a method implementation in this category
HOPerson+Friends.m:11:17: Property 'friends' requires method 'setFriends:' to be defined - use @dynamic or provide a method implementation in this category
要消除警告,要么添加@dynamic
,要么添加对应的setter/getter方法。
下面是在实现文件里添加setter/getter方法:
#import "HOPerson+Friends.h"
#import <objc/runtime.h>
static const char *kFriendPropertyKey = "kFriendPropertyKey";
@implementation HOPerson(Friends)
//@dynamic friends;
- (NSSet *)friends
{
return objc_getAssociatedObject(self, kFriendPropertyKey);
}
- (void)setFriends:(NSSet *)friends
{
objc_setAssociatedObject(self,
kFriendPropertyKey,
friends,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
在本例中,正确的做法是将所有的属性都定义在主接口里。主接口是唯一能定义成员变量(数据)的地方。而属性只是定义实例变量及相关存取方法所用的“语法糖”,所以也应遵循同实例变量一样的规则。至于分类机制应将其理解为一种手段,目标在于扩展类的功能,而非封装数据。
- 把封装数据所用的全部属性都定义在主接口里;
- 在“class-continuation”分类之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
2.5 使用“class-continuation”分类隐藏实现细节
class-continuation和其他分类不同,它必须定义在其所接续的那个类的实现文件里。其重要之处在于,这是唯一能声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应定义在类的主实现文件里。
可参考本文上段中“位段在委托代理模式中的应用”中定义的位段,即实例变量。
在class-continuation中定义实例变量,主要是为了将细节隐藏起来。
另外,在class-continuation中声明只有在类的实现代码中的私有方法也是较为可取的。在编写类的实现代码之前,先在class-continuation中将需要实现的方法原型声明,然后逐一实现。比如:
@interface HOPerson()
{
NSMutableSet *_internalFriends;
}
- (void)hog_findFriends;
@end
@implementation HOPerson
@end
最后,还有一种情况,就是对象所遵循的协议只应视为私有的话,那么最好也在class-continuation中声明。比如:
@interface HOPerson()<NSCopying,NSCoding>
{
NSMutableSet *_internalFriends;
}
- (void)hog_findFriends;
@end
@implementation HOPerson
@end
- 通过“class-continuation”分类向类中新增实例变量;
- 如果某属性在主接口总声明为“readonly(只读)”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation”中将其扩展为“readwrite(可读写)”;
- 把私有方法的原型声明声明在“class-continuation”里面;
- 若想使类所遵循的协议不为人所知,则可与“class-continuation”中声明。
2.5 通过协议提供匿名对象
- 协议可在某某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某些一的id类型,协议里规定了对所应事先的方法。
- 使用匿名对象来隐藏类型名称(或类名)。
- 如果具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。