第5条:用枚举表示状态、选项、状态码。
1. 凡是需要使用按位或操作的组合的枚举都应使用NS_OPTIONS定义。若是不需要互相组合,则使用NS_ENUM来定义
typedef NS_ENUM (NSUInteger, EOCConnectionState) {
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
typedef NS_OPTIONS (NSUInteger, EOCPermittedDirection) {
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
}
==NS_ENUM可以用来实现 “类族模式” 隐藏实现细节。==
第6条:理解 “属性” 这一概念
1. Objecitve-C 的做法是,把实例变量当做一种存储偏移量所用的 “特殊变量”, 交由 “类对象” 保管。偏移量会在运行期查找,如果类的定义变了,那么存储的偏移量也就变了。这样的话,无论何时访问实例变量,总能使用正确的偏移量。==甚至可以在运行期向类中新增实例变量。这就是稳固的应用程序二进制接口(Application Binary Interface, ABI)。有了这种稳固的ABI, 我们就可以在 “class-continuation分类” 或实现文件中定义实例变量了。== 所以说,不一定要在接口中把全部实例变量都声明好,可以将某些实例变量从接口的public区段移走,以便保护与类实现有关的内部信息。
==注意:C++ 不能在实现文件中定义实例变量,而 Objective-C 则可以在实现文件中定义实例变量。靠的就是上面所说的这种机制。==
2. 使用@dynamic 关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性代码时,即使编译器发现没有定义存储方法,也不会报错,它相信这些方法能在运行期找到。比如说:从CoreData框架中的NSManagedObject类里继承了一个子类,那么就需要在运行期动态创建存取方法。继承NSManagedObject时之所以要这样做,是因为子类的某些属性不是实例变量,其数据来自后端的数据库中。
第七条:在对象内部尽量直接访问实例变量
1. 在对象内部,在读取实例变量时采用直接访问的形式,而在设置实例变量的时候可以通过属性(setter方法)来做。
2. 直接访问实例变量和通过setter、getter方法访问实例变量的区别:
i. 由于不经过 Objective-C 的 “方法派发” 步骤,所以直接访问实例变量的速度当然比较快。在这种情况下,编译器所生成的代码会直接访问对象实例变量的那块内存。
ii. 直接访问实例变量时,不会调用其 “setter方法”, 这就绕过了相关属性所定义的 “内存管理语义”。比如说:如果在ARC下直接访问了一个声明为copy 的属性,那么并不会拷贝该属性。只会保留新值,并释放旧值。(默认为strong语义)
iii. 如果直接访问实例变量,那么就不会触发 “键值观察” (Key-Value Observing, KVO)通知。
iv. 通过属性来访问有助于排查与之相关的错误,因为可以给"setter方法" 或 "getter方法"中新增断点,监控该属性的调用者及其访问时机。
==注意:在初始化方法中,尽量不要使用setter方法、getter方法访问。这种情况下,总是应该直接访问实例变量。除非:1. 如果待初始化的实例变量声明在超类中,而我们又无法在子类中直接访问此实例变量。那么就需要调用“setter方法 ” 或 “getter方法” 2. 惰性初始化方式,必须通过“setter方法” 或 “getter方法” 来访问属性。否则,实例变量永远不会初始化。==
第9条:以 “类族模式” 隐藏实现细节
//
// EOCEmployee.h
// EOCPersonAndEocEmployer
//
// Created by Jeremy on 2016/10/13.
// Copyright © 2016年 Jeremy. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesiger,
EOCEmployeeTypeFinance,
};
@interface EOCEmployee : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type;
- (void)doADaysWork;
- (instancetype)initWithName:(NSString *)name salary:(NSUInteger)salary;
@end
#import "EOCEmployee.h"
#import "EOCEmployeeDeveloper.h"
#import "EOCEmployeeDesiger.h"
#import "EOCEmployeeFinance.h"
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type {
switch (type) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesiger:
return [EOCEmployeeDesiger new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeFinance new];
break;
}
}
- (instancetype)initWithName:(NSString *)name salary:(NSUInteger)salary{
if (self = [super init]) {
_name = [name copy];
_salary = salary;
}
return self;
}
@end
第11条:理解 objc_msgSend 的作用
1.==消息派发(message dispatch)== 中消息传递机制中的核心函数,叫做objc_msgSend,其原型如下:
void objc_msgSend(id self, SEL cmd, ...)
它将Objevitve-C 的消息调用转换为C语言函数调用。
objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其 “方法列表” (list of method),如果能找到与选择子名称相符合的方法,就调转到其实现代码。如果找到不,那就沿着继承体系向上查找,等找到合适的方法之后再跳转。如果最终还是找不到相符的方法,那就执行 ==“消息转发” (message forwarding)== 操作。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。
其他的一些==消息派发(message dispatch)== 函数
objc_msgSend_stret
objc_msgSend_fpret
objc_msgSendSuper
==注意:obj_msgSend 用到尾调用优化技术。从而避免了“栈溢出”(stack overflow)的现象==
第12条:理解消息转发(message forwarding)机制
1.消息转发(message forwarding)分为三个阶段:
i. Method resolution
objc运行时会调用
+ (BOOL)resolveInstanceMethod:(SEL)selector; 或
+ (BOOL)resolveClassMethod:(SEL)selector;
让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。使用这种办法的前提是:相关方法的实现代码已经写好,只是等着运行的时候动态的插入在类里面就可以了。此方案常用来实现@dynamic属性。
ii. Fast forwarding
如果目标对象实现了
- (id)forwardingTargetForSelector:(SEL)aselector;
Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个==NSInvocation对象== ,所以相对更快点。
iii.Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送
- (void)forwardingInvocation:(NSInvocation *)invocation;
消息给目标对象。
第14条:理解 “类对象” 用意
谁都知道,所有的对象都是由其对应的类实例化而来,殊不知类本身也是一种对象。其实在Objective-C中任何的类定义都是对象。即在程序启动的时候任何类定义都对应于一块内存。在编译的时候,编译器会给每一个类生成一个且只生成一个”描述其定义的对象”,也就是苹果公司说的类对象(class object),他是一个单例(singleton), 而我们在C++等语言中所谓的对象,叫做实例对象(instance object)。对于实例对象我们不难理解,但类对象(class object)是干什么吃的呢?我们知道Objective-C是门很动态的语言,因此程序里的所有实例对象(instace object)都是在运行时由Objective-C的运行时库生成的,而这个类对象(class object)就是运行时库用来创建实例对象(instance object)的依据。
typedef struct objc_class *Class
//类似链表的结构
struct obj_class {
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivas;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list protocols;
}
typedef struct objc_class *Class
struct obj_object {
Class isa;
}
objc对象如何进行内存布局?(考虑有父类的情况)
该objc对象的所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.
每一个对象内部都有一个isa指针,指向他的==类对象(class object)== , ==类对象(class object)== 中存放着本对象的
i. 对象方法列表(struct objc_method_list **methodLists, 也即对象能够接收的消息列表,保存在它所对应的类对象中)
ii. 成员变量的列表(struct objc_ivar_list *ivars)
iii. 遵循的协议(objc_protocol_list protocols)
类对象(class object)内部也有一个isa指针指向元类(meta class), ==元类内部存放的是类方法列表(class mothed list),== 类对象内部还有一个super_class的指针,指向他的父类对象。
每个 Objective-C 对象都有相同的结构,如下图所示:
Objective-C 对象的结构图|
---|---
isa指针 |
倒数第二层父类的实例变量 |
... |
父类的实例变量|
类的实例变量|
根的类对象就是NSObject,它的superclass指针指向nil
类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表(class method list),根元类的isa指针指向自己,superclass指针指向NSObject类。
如图: