第一章 熟悉Objective-C
1.OC的起源
oc使用了消息结构而非函数调用。使用消息结构的语言,其运行时所执行的代码由运行环境决定,而使用函数调用的语言,则由编译器决定。
OC对象所占内存总是分配在“堆空间”,而不会分配在“栈”上。分配在堆中的内存必须直接管理,而分配栈上的用于保存变量的内存 则会在其栈帧弹出时自动清理。
当我们看到一个变量类型是已知的,就分配在栈里面,比如,int、double等。其他未知的类型,比如自定义的类型,因为系统不知道需要多大,所以程序自己申请,这样就分配在堆里面百度文库
2.在类的头文件中尽量少引入其他头文件
- 除非有必要,否则不要引入头文件。一般来说,应在某个类的头文件里使用向前声明来提及别的类,并在实现文件中引入哪些类的头文件。这样做可以尽量降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation”分类中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
3.多用字面量语法,少用与之等价的方法
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。(即使用NSStringstr = @"string"*这种方式来创建)
- 应该通过取下标操作来访问数组下标或字典中的键值所对应的元素。(即使用array[1]这种方式)
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
4.多用类型常量,少用#define预处理指令
应该使用
static constant NSTimeInterval kAnimationDuration = 0.3;
而不要使用
#define Animation_duration 0.3
如果想要在该常量的编译单元之外使用,那么使用这种形式
//In the header file
extern NSString *constant EOCStringConstant;//这里用UIKIT_EXTERN会比较好
//In the implementation file
NSString *const EOCStringConstant = @"Value";`
常量定义应从右至左解读,EOCStringConstant就是一个常量,而这个常量是指针,指向NSString对象。(大概意思应该是,const修饰的是常量,那么EOCStringConstant是一个常量,而*是一个指针,最后,它的类型是NSString。)
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即时有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const来定义“只有在编译单元内可见的常量”。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。
5.用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态,传递给方法的选项以及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各个选项值为2的幂,以便通过按位或操作将其组合起来。
- 用NS_ENUM与NS_OPTIONS宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的switch语句中不要实现default分支。这样的话,加入新枚举之后,编译器就会产生还有未处理的枚举。(xcode7.3实践,反正不加default会有警告。。。这可能是版本问题?)
第二章 对象、消息、运行期
6.理解“属性“这一概念
- 可以使用@property语法来定义对象中所封装的数据
- 通过“特性”来指定存储数据所需的正确语义(即copy,atomic等)
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义(比如,NSString使用copy修饰的时候,那么在set方法内部应该使用[string copy])
- 开发iOS程序时应该使用nonatomic属性,因为atomic属性会严重影响性能
7.在对象内部尽量直接范文实例变量
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。(直接访问实例变量比较快,但是使用属性会遵循属性的内存管理语义,会触发键值观察,出错的时候通过断点比较好排查)
- 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据。
- 有时会使用懒加载的初始化方法来配置某份数据,这种情况下,需要通过属性来读取数据
8.理解“对象等同性”这一概念
- 若想检测对象的等同性,请提供“isEqual”与hash方法
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来指定检测方案
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法
9. 以“类簇模式”隐藏实现细节
- 子类应该继承自类簇中的抽象基类
若要编写NSArray类簇的子类,则需令其继承自不可变数组的基类或可变数组的基类。 - 子类应该定义自己的数据存储方式。
开发编写NSArray子类时,经常在这个问题上受阻。子类必须用一个实例变量来存放数组中的对象。这似乎与大家预想的不同,我们以为NSArray自己肯定会保存那些对象,所以在子类中就无须再存一份了。但是大家要记住,NSArray本身只不过是包在其他隐藏对象外面的壳,它仅仅定义了所有数组都需具备的一些接口。对于这个自定义的数组子类来说,可以用NSArray来保存其实例 - 子类应该覆写超类文档中指明需要覆写的方法。
在每个抽象基类中,都有一些子类必须覆写的方法。比如说,想要编写NSArray的子类,就需要实现count及其“objectAtIndex:”方法。像lastObject这种方法则无须实现,因为基类可以根据前两个方法实现出这个方法。
在类簇中实现子类所需遵循的规范一般都会定义于基类的文档之中,编码前应该先看看。 - 类簇模式可以把实现细节隐藏在一套简单的公共接口后面(有点像协议)
- 系统框架中经常使用类簇
- 从类簇的公共抽象基类中继承子类时要小心,若有开发文档,则应先阅读
10. 在既有类中使用关联对象存放自定义数据
- 可以通过“关联对象”机制来把两个对象连起来(就是runtime里面的objc_getAssociateObject与objc_getAssociatedObject关联的参考)
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非拥有关系”。
- 只有在其他做法不可行时才应选用非关联对象,因为这种做法通常会引入难以查找的bug。
11. 理解objc_msgSend的作用
C语言的函数调用方式:C语言使用“静态绑定”,也就是说,在编译期就能决定运行时所应调用的函数。编译器在编译代码的时候就已经知道程序中有哪些函数了。于是会直接生成调用这些函数的命令。
动态绑定:在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有的方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法则取决于运行期决定,甚至可以在程序运行时改变,这些特性使得OC成为一门真正的动态语言,类似如下
void printHello(){
printf("Hello, world!");
}
void printGoodbye(){
print("goodbye,world!\n");
}
void doTheThing(int type){
void (*fnc)();
if(type == 0){
fnc = printHello;
}else{
fnc = printGoodby;
}
}
一般的方法调用:
id returnValue = [someObject messageName:parameter]
本例中,somObject叫做“接收者”(receiver),messageName叫做“选取器”(selector)。选取器与参数合起来称为“消息”(message)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,“原型”如下
void objc_msgSend(id self, SEL cmd,...)
这是个“参数个数可变的函数,能接受两个或两个以上的参数。第一个参数代表接收者,第二个代码选取器,后续参数就是消息中的那些参数,其顺序不变。选取器指的就是方法名。“选取器”与“方法”这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
objc_msgSend函数会一句接收者与选取器的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果能找到与选取器名称相符的方法,就调至其实现代码。没有就沿着继承体系向上查找。如果最终找不到,就执行“消息转发”操作。
还有另外一些边界情况,将交由oc运行环境中的另一些函数来处理:
objc_msgSend_stret:如果待发送消息要返回结构体,那么可交由此函数处理。
objc_msgSend_fpret:如果消息返回的是浮点数,那么可交由此函数处理。
objc_msgSendSuper:如果需要给超类发消息,例如[super message:parameter],那么就交由此函数处理。
消息接收者、选取器及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”
发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码
12. 理解消息转发机制
消息转发分为两大阶段,第一阶段先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选取器”,这叫做“动态方法解析"。第二阶段涉及“完整的消息转发机制”,如果runtime已经把第一阶段执行完了,那么接收者自己无法再以动态添加的方法来相应包含该选取器的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两部,首先,看有没其他对象处理。有就给它。即“备援的接收者”。没有,则启动“完整的消息转发机制”。runtime会把与消息有关的全部细节都封装到NSInvocation对象,再给接收者最后一次机会。
动态方法解析:
对象在收到无法解读的消息后,首先将调用其所属类的下列表方法
+ (BOOL)resolveInstanceMethod:(SEL)selecotor
该方法的参数就是那个未知的选取器,其返回值表示这个类能否新新增一个实例方法用以处理这个选取器。在继续往下执行转发机制之前,本类有机会新增一个处理此选取器的方法。加入尚未实现的方法不是实例方法,而是类方法,那么会调用另一个叫resolveClassMethod:
的方法
使用这种方法的前提是,相关方法的实现代码已经写好,只等着运行时,动态插在类里就可以了。此方法常用来实现@dynamic属性。
备援接收者
当前接收者还有第二次机会能处理未知的选取器,在这一步中,runtime系统会问它,能不能把这条消息转给其他接收者来处理。其对应处理方法:
- (id)forwardingTargetForSelector:(SEL)selector
方法参数代表未知的选取器,若当前接收者能找到备援对象,则将其返回,找不到,就返回nil。通过此方案,我们可以用“组合”来模拟出“多重集成”的某些特性。在一个对象内部,可以还有其他对象,改对象经由此方法将能够处理某选取器的相关内部对象返回,这样的话,看起来就是该对象处理的。
注意,我们无法操作经由这一步所转发的消息,如果想在发送给备援接收者之前修改消息内容,那就得通过完整的消息转发机制来做。
完整的消息转发
如果转发算法已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制。首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节全部封装与其中。此对象包含选取器,目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。
此步骤会调用下列方法来转发消息:
- (void)forwardInvocation:(NSInvocation *)invocation
这个方法可以实现的很简单:只需改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效。比较有用的实现方法:在触发消息前,先以某种方式改变消息内容,比如追加另一个参数,或者改换选取器等。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类方法,那么该方法还会继而调用doesNotRecognizeSelector:
以抛出异常,此异常表明选取器最终未能得到处理。
- 若对象无法响应某个选取器,则进入消息转发流程
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其无法解读的某些选取器交给其他对象来处理
- 经过上述两部之后,如果还是没办法处理,那就只能启动完整的消息转发机制(目前能看出的用处:1. 理解了错误的时候抛出异常的地方,2. 用于实现类似多继承的效果)
13. 用"方法转换“调试”黑盒方法“
使用** 方法转换 **可以让我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于腹写了相关方法的那些子类实例。
类的方法列表会把选取器的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫IMP,原型如下
id (*IMP)(id,SEL, ...)
NSString类可以响应uppercaseString等选取器。
#emsp;想交换方法实现,可用下列函数:
void method_exchangeImplementations(Method m1, Method m2)
此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:
Method class_getInstanceMethod(Class aClass, SEL aSelector)
此函数根据给定的选择从类中去除与之相关的方法。执行下列代码,即可交换比如lowercaseString和uppercaseString方法实现:
Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
如果想要在调用lowercaseString时记录某些信息,这时可以通过交换方法来实现
新方法可以添加到NSString的一个category钟:
@interface NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString;
@end
上述新方法将与原有的lowercaseString方法呼唤
新方法的实现可以这样写:
@implementation NSString (EOCMyAdditions)
- (NSString *)eoc_myLowercaseString{
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ = > %@", self, lowercase);
return lowercase;
}
@end
这段代码看上去好像会是死循环,不过要记住,此方法是准备和lowercaseString方法呼唤的。所以,在运行期,eoc_myLowercaseString选取器实际上对应于原有的lowercaseString方法实现。最后通过以下来交换实现:
Method originalMethod = class_getInstanceMethod([NSStringclass], @selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass], @selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
- 在运行期,可以向类中新增或替换选取器所对应的方法实现
- 使用另一份实现来替换原有的方法实现,这道工序叫做** 方法转换 **,开发者常用词技术向原有实现中添加新功能
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用
14. 理解类对象的用意
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系
- 如果对象类型无法再编译期确定,那么就应该使用类型信息来查询方法探知(isEquelto)
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类的对象,因为某些对象可能实现了消息转发功能