第14条:理解 “类对象” 的用意
对象类型并不是在编译期就绑定好了,而是要在运行期查找。在运行期检视对象类型的操作,叫做 “类型信息查询(内省)”
元类
在运行期程序库的头文件中,id
类型的定义:
typedef struct objc_object {
Class isa;
} *id;
每个对象结构体是首个成员是 Class
类的变量 isa
,该变量定义了对象所属的类。
在运行期程序库的头文件中,Class
类型的定义:
typedef struct objc_class *Class;
struct objc_class {
Class isa;
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
指针所指向的类。
作用:用来表述对象本身所具备的元数据。“类方法” 就定义在这里。
在类继承体系中查询类型信息
isMemberOfClass:
判断出对象是否为某个特定类的实例。isKindOfClass:
判断出对象是否为某类或者其派生类的实例。
说明代码:
NSMutableDictionary *dict = [NSMutableDictionary dictionary]; // 底层对象为:__NSDictionaryM
NSLog(@"%d", [dict isMemberOfClass:[NSDictionary class]]); // NO
NSLog(@"%d", [dict isMemberOfClass:[NSMutableDictionary class]]); // NO
NSLog(@"%d", [dict isMemberOfClass:NSClassFromString(@"__NSDictionaryM")]); // YES
NSLog(@"%d", [dict isKindOfClass:[NSDictionary class]]); // YES
NSLog(@"%d", [dict isKindOfClass:[NSArray class]]); // NO
像这样的类型信息查询方法使用
isa
指针获取对象所属的类,然后通过super_class
指针在继承体系中游走。
系统对象创建后,其元类并未创建时使用的类,系统将其转为一些底层类,如上面的__NSDictionaryM
。
自定义对象(继承NSObject),还是创建时的对象。
类对象是单例,在用于程序范围内,每个类的 Class 仅有一个实例。
自定义对象,通过 == 操作符也可判断出对象是否为某类的实例。
id object = [EOCSomeClass new];
if ([object class] == [EOCSomeClass class]) {
...
}
代理: 对象可能会把其受到的所有选择子转发给另一个对象,这个对象就是代理。这种对象均以 NSProxy
为根类。
代理对象上调用 class
方法返回的是代理对象本身,而非接受代理的对象所属的类。
总结: 尽量使用类型查询方法来确定对象类型,而不是直接比较对象,因为某些对象可能实现了消息转发功能。
第三章:接口与 API
设计
第15条:用前缀避免命名空间冲突
问题:Objective-C
没有其他语言那种内置的命名空间机制,容易产生命名冲突。
苹果宣称其保留使用所有 “两字母前缀” 的权利,所以我们在开发中最好使用三个字母的。
若自己所开关的程序库中用到了第三方库,则应为其中的名称加上前缀。
第16条:提供 “全能初始化方法”
这种可为对象提供必要信息以便其能完成工作的初始化方法叫做 “全能初始化方法”。
在类中提供一个全能初始化方法。其他初始化方法均应调用此方法。
若全能初始化方法与超类不同,则需要覆写超类中的对应方法。
第17条:实现 description 方法
NSLog(@"object = %@", object)
在构建需要打印到日志的字符串时,object
对象会收到 description
消息,该方法所返回的描述信息将取代 ”格式字符串“ 里的 ”%@“。
自定义对象在打印时,只能打印出对象的类名及地址,这些内容一般没有什么用。
覆写 description 方法,可以打印自己所需要的内容。
- (NSString *)description {
return [NSString stringWithFormat:@"<%@: %p, %@>",
[self class],
self,
@{@"参数1" : 参数1,
@"参数2" : 参数2}
];
}
debugDescription
用于debug
模式下po
的打印。
第18条:尽量使用不可变(对外只读)对象
不可变:只读(read-only)
可变:即可读又可写(read-write)
尽量把对外公布出来的属性设置为只读,而且只在必要的时候才将属性对外公布。
第19条:使用清晰而协调的命名方式
起名时应遵从标准的 Objective-C 命名规范,这样创建出来的接口更容易为开发者所理解。
第20条:为私有方法名加前缀
好处:
- 便于区分 公共方法 和 私有方法
- 便于修改 方法名 或 方法签名
建议:
不要单用一个 _ 做私有方法的前缀,苹果公司就是这么用的,可能会覆写系统私有方法。建议使用 p_
作为私有方法的前缀
第21条:理解 Objective-C
错误模型
第22条:理解 NSCopying
协议
-
copy
:返回的拷贝对象与当前一致 -
immutableCopy
:返回不可变的拷贝对象 -
mutableCopy
:返回可变的拷贝对象
第23条:通过委托与数据源协议进行对象间通信
缓存方法响应能力缓存的最佳途径是使用 ”位段“ 数据类型。
位段:
struct data {
unsigned int fieldA : 8; // 位段,占8个二进制位
unsigned int fieldB : 4; // 位段,占4个二进制位
unsigned int fieldC : 2; // 位段,占2个二进制位
unsigned int fieldD : 1; // 位段,占1个二进制位
};
代理缓存:
// 用于缓存委托对象是否能响应特地的选择子
struct {
unsigned int delegateMethdo1 : 1;
unsigned int delegateMethdo2 : 1;
unsigned int delegateMethdo3 : 1;
} _delegateFlags;
...
// 实现缓存功能所有的代码可以写在 delegate 属性所对应的设置方法里面
- (void)setDelegate:(id<delegate类名>)delegate {
_delegate = delegate;
_delegateFlags.delegateMethdo1 = [delegate respondsToSelector:@selector(delegateMethdo1:)];
_delegateFlags.delegateMethdo2 = [delegate respondsToSelector:@selector(delegateMethdo2:)];
_delegateFlags.delegateMethdo3 = [delegate respondsToSelector:@selector(delegateMethdo3:)];
}
在以后调用代理方法的时候,直接使用结构体里面的标志进行判断即可。
第24条:将类的实现代码分散到便于管理的数个分类之中
第25条:总是为第三方类的分类名称加前缀
在运行期系统加载分类时,会将分类中所实现的方法都加入到类的方法列表中,就好比这个类的固有方法。如果类中本来就有此方法,而分类中有实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,多次覆盖的结果以最后一个分类为准。
为防止覆盖,将分类的名称和其中的方法名称加上前缀