高效的OC代码
前言
要啥前言,直接就是撸。
1、OC与C
OC是C的超集,所有C语言的特性,在OC上都可以使用。同时掌握这两门语言,对于提升OC的代码效率非常重要。尤其是理解C语言的内存模型,有助于理解OC的“引用计数”机制的工作原理,不过现在ARC已经很普遍了,很多刚上道的OCer 基本不太理解引用计数,根本不用嘛。Swift的出现更加让OC尴尬,总而言之,还是看自己的理解吧。OC使用动态绑定的消息结构,也就是说只有在运行时,才会检查对象,收到消息,执行哪段代码由运行期决定而非编译器。这点与JAVA完全不同。
2、头文件引入
这个是老梗了,相信老的OCer都应该了解。我再复述一遍吧。
当在a.h头文件中需要用到b类文件时,偷懒的童鞋会直接#import "b.h",这样做就有可能引起某些问题了。
编译时间增加,当编译a文件的时候,会将b文件全部编译。其实在编译a时不需要知道b文件的全部细节,只要知道一个类名就好。
避免头文件的循环引用,当a的头文件#import "b.h",同时在b的头文件#import "a.h",这样就产生了两个类的循环引用,当然使用#import 而非#include指令不会导致死循环,但却意味着两个类里有一个无法正确编译。
针对以上两个问题,产生了一个名词“向前声明”。即通过在a的头文件中使用@class b;就可以在a文件中使用b类。在a.m文件中再#import "b.h"。这样就可以减少编译的时间,也可以防止了头文件的循环引用。
还有一种情况需要在头文件中,必须要#import,即a类继承自b类或者遵循某类协议,那么该类需要完整定义,且不能使用向前声明。根据这种情况,可以考虑将某些代码放入分类(class continuation)中,比如是否遵循某个协议(或者讲协议的定义放入单独的头文件)
3、多用字面量语法,少用与之等价的方法
写OC时,经常会用到几个类,他们属于Foundation,NSString、NSNumber、NSArray、NSDictionary,针对这几个类,推荐多用字面量语法进行定义,减少alloc,init方式初始化。例如
NSString *demoString = @"Demo";
NSNumber *two = @2;
NSArray *cityArray = @[@"北京",@"天津"];
NSDictionary *cityDic= @{@"name":@"北京",@"code":@"010"};
NSString *capital = cityArray[0];
NSString *capitalName= cityDic[@"name"];
字面量语法实际上是一种“语法糖”,可另程序更易读,减少出错率。但也有他的局限性。在用NSArray时,需要先确保值中没有nil,如果有,则会抛出异常。
4、多用类型变量,少用#define预处理
定义一个动画执行时间为0.3秒,也许有人会这么做:
#define ANIMATION_DURATION 0.3
这么写首先没有定义出这个常量的类型,duration应该是时间,但是没有明确指出。另外如果其他类引用了这个类,那么所有的ANIMATION_DURATION都会替换成了0.3,有可能会产生小问题。
解决这个问题可以定义一个类型为NSTimeInterval的常量:
static const NSTimeInterval kAnimationDuration 0.3
这样可以更清晰标明这个属性的类型,也容易让其他人明白其作用。需要注意的是命名规范,弱常量局限于实现文件之内,则在前面加字幕k,如果供外部类使用,需要加类名前缀。
5、用枚举表示状态、选项、状态码
每一个状态都用一个便于理解的值来表示,写出来的代码更容易理解。
专门说一下,如果选项是可组合的,更需要用枚举了。各选项之间通过“按位或操作符”来组合,使用方式如下:
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
这样,每个选项都可以启用或禁用,在做判断的时候,用“按位与运算符”判断是否启用:
enum UIViewAutoresizing resize = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (resize & UIViewAutoresizingFlexibleHeight) {
//处理height
}
6、理解属性的概念
属性特质
原子性:如果属性具有nonatomic,标明属性不使用同步锁,未声明atomic,标明属性是原子的。区别在于atomic特质的获取方法会通过锁定机制确保其操作的原子性,即多个线程读同一属性,无论何时总能看到有效值。如果不加锁的情况,当某一线程正在改写值时,另外一个线程把未修改的属性值取出来,结果可能不对。iOS的同步锁开销较大会带来性能问题,尽量少用。
读写权限:具有readwrite权限的属性拥有getter和setter方法,如果该属性由@synthesize实现,则编译器会自动生成这两个方法。
具有readonly权限的属性只拥有getter方法,
内存管理语义:
assign:“设置方法”只会针对“纯量类型”的简单赋值操作。
strong:此特质表明属性定义了一种“拥有关系”,为这种属性设置新值时,会先保留新值,并释放旧值,然后再将新值赋值上去。
weak : 此特质表明属性定义了一种“非拥有关系”,为这种属性设置新值时,既不保留新值,也不释放旧值,同assign类似,然而在属性所指的对象被销毁时,属性值也会清空
unsafe_unretained: 此特质和assign相同,但是只适用于对象类型,表达一种“非拥有关系”,当目标对象被销毁时,属性值不会被清空,同weak不同。
copy: 此特质与strong类似,然而设置方法并不保留新值,而是将其copy,当属性为NSString类型时,经常用此特质,保护其封装性。因为传递的值很可能是NSMutableString,即NSString的子类,若是不拷贝字符串,那么设置完后,可能会在不知情的情况下遭人篡改,所以这时就要拷贝一份“不可变的”字符串。
方法名:getter = 指定获取方法的名称,如果某属性是BOOL类型,你想为其getter方法添加is前缀,就可以通过这个指定。
setter = 指定设置属性的名称,不常用。
7、对象内部尽量直接访问实例变量
属性访问和直接访问的区别:
直接访问不需要经过OC的方法派发,所以访问实例变量的速度比较快
直接访问不会调用“设置方法”,会绕过属性定义的内存管理语义,比如一个属性定义为copy,如果直接访问,并不会拷贝属性,而是保留新值,释放旧值。
如果直接访问实例变量,不会触发KVO。
通过属性访问有助于排查与之相关的错误。
8、理解“对象等同性”的概念
提到“等同性”,可能第一个想法就是"==",但是"=="比较出来的结果未必是我们想要的结果,因为他比较的是两个指针本身,而不是所指的对象。一般来说,两个不同类型的对象总是不相等的,如果已经知道两个受测对象是相同的,那么可以看下如下代码:
NSString *foo = @"ZRC123";
NSString *foo1 = [NSString stringWithFormat:@"ZRC %@",@"123"];
BOOL equalA = foo == foo1; //equalA = NO
BOOL equalB = [foo isEqual:foo1]; // equalB = YES
BOOL equalC = [foo isEqualToString:foo1]; //equalC = YES
从上述代码不难看出“==”与“isEqual”之间的区别,NSString实现了自己独有的等同性判断方法,名叫"isEqualToString",传递的参数必须是NSString,否则为undefined。该方法要比“isEqual”快,后者要执行其他方法。
NSObject协议中有两个用于判断等同性的关键方法:
-(BOOL)isEqual:(id)object;
-(NSUInteger)hash;
如果“isEqual”判定两个对象相同,则hash值返回的值必定相同,但如果hash值返回相同,那么“isEqual”未必会认为两个对象相同。
如果经常需要判断等同性,很可能需要自己去实现一个判断方法,无需检测参数类型,可以大大提升检测速度。在编写判定方法时,也应一并复写“isEqual”方法。常用的复写“isEqual”,是判断受测参数与接受该消息的参数是否属于同类,如果属于,调用自己编写的判定方法,如果不同,则调用超类。
等同性执行深度
创建等同性判断方法时,需要决定是根据整个对象来判断,还是根据其中几个字段来判断。NSArray判断是先根据数组所含个数是否相同,若相同,则在每个位置的的两个对象调用isEqual,如果每个位置的对象均相等,这就叫做“深度等同性判断”。
容器中可变类的等同性
还有一种情况需要注意,就是在容器中放入可变类对象,就不应该再改变其哈希码了,举个例子:
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arrayA = [@[@1,@2] mutableCopy];
[set addObject:arrayA];
NSLog(@"set is %@",set);
//output set is {((1,2))}
现在set中含有一个数组对象,数组中有两个元素,再向set中添加另外一个数组,此数组与前一个数组对象相同,顺序也相同:
NSMutableArray *arrayB = [@[@1,@2] mutableCopy];
[set addObject:arrayB];
NSLog(@"set is %@",set);
//output set is {((1,2))}
此时set中仍然只有一个对象,因为已有的数组与新添加的数组相同,所以set不会改变。这次我们添加一个和set中已有的数组不同的数组:
NSMutableArray *arrayC = [@[@1] mutableCopy];
[set addObject:arrayC];
NSLog(@"set is %@",set);
//output set is {((1),(1,2))}
如我们所料,set中现在又两个数组了。此时我们改变arrayC,另其和最早的数组相同
[arrayC addObject:@2];
NSLog(@"set is %@",set);
//output set is {((1,2),(1,2))}
set中居然可以包含两个相同的数组,根据set语义是不允许这样的。此时如果我们copy此set,那就更糟了:
NSSet *setB = [set copy];
NSLog(@"setB is %@",setB);
//output setB is {((1,2))}
复制过来的set中又只剩一个对象了,此set看上去好像由一个空set开始,通过逐个向其中添加新对象创造出来的。并不是不可以这么做,只是如果这么做,就要小心其隐患,并用相应的代码处理可能发生的问题。
9、理解objc_msgSend的作用
对象调用方法是Objective-C经常使用的,用OC的术语来说,这叫做“消息传递”,消息有名称和选择器(selector),可以接受参数而且可能有返回值。在OC中,如果向某对象传递消息,会使用动态绑定机制来决定需要调用哪个方法。对象收到消息后,调用哪个方法是运行时决定的,甚至可以再运行时改变。给对象发送消息可以这样来写:
id retureValue = [someObject messageName:param];
在上边这个例子中,someObject作为接收者(receiver),messageName作为选择器和param合起来称为消息。编译器看到此消息后,转换为消息传递机制的核心函数,叫做“objc_msgSend”,方法原型如下:
void objc_msgSend(id self, SEL cmd,...)
这是个“函数参数可变的函数”,能接受两个及以上的参数,第一个参数代表接收者,第二个参数代表选择器(SEL是选择器的类型),后续参数就是消息的实际参数。编译器会把之前的例子转换成:
id returnValue = objc_msgSend(someObject , @selector(messageName),param);
为了完成此操作,消息中心会在接收者的类中搜寻方法(list of method),如果能找到实现的代码,则跳至代码的实现,若是找不到,则沿着继承体系继续往上找,找到合适的方法再跳转。如果最后还是没找到,那就执行“消息转发”操作。前面描述的只是部分调用过程,比如方法缓存之类的都没有详细说明,后续会进行详细说明。其他的边界情况,则需要交由OC运行环境中的另外的函数进行处理。每个类里都有一张表格,选择器的名称作为查表的key,objc_msgSend正是通过表格来寻找应该执行的方法并跳转的。
10、了解消息转发机制
当对象在收到无法解读的消息之后会发生什么?
我们会遇到消息转发流程处理的消息,只是未加留意过,如果在控制台中显示以下信息,说明你曾向某个对象发送了一条无法解读的消息,从而启动了消息转发机制,并转发给了NSObject默认的实现方式。
-[NSCFNumber lowercaseString]: unrecognized selector sent to instance 0x87
上面这段信息是由NSObject的“doesNotRecognizeSelector”方法抛出的,此异常表明,消息接受者的类型时NSCFNumber,而该接受者无法理解名为“lowercaseString”的选择器。这个例子以程序崩溃告终,不过在编写自己的类时,可在转发过程中挂钩,用以执行预定的逻辑,不让程序崩溃。
消息转发分为两个阶段。第一阶段是询问接收者所属的类,是否能动态添加方法,来处理当前未知的选择器,叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”。如果运行期已经把第一阶段执行完了,那么接受者就无法再用动态新增方法的手段来响应包含该选择器的消息了。此时系统会请求接受者以其他手段来处理与消息相关的方法调用。这又细分为两小步:首先,请接受者看看有没有其他对象能处理这条消息,如果有,则转发此消息给这个对象;如果没有“备援的接受者”,则启动完整的消息转发机制,运行期系统把完整的消息封装成NSInvocation中,再给接受者最后一次机会。
动态方法解析
对象在收到无法解析的消息时,首先会调用以下方法:
+ (BOOL)resolveInstanceMethod:(SEL)selector
该方法的参数就是未知的选择器,返回值为BOOL,表示是否能新增一个实例方法来处理这个选择器。假如未实现的方法不是实例方法而是类方法,那么运行期系统会调用“resolveClassMethod”。使用这种方法的前提是,相关方法的实现代码已经写好,只需要在运行时动态插在里面就可以了,此方案常用来处理@dynamic属性。下面的代码演示了如何使用“resolveInstanceMethod”来实现:
void testResolveInstanceMethod(id self, SEL _cmd, NSString *name);
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSString *methodString = NSStringFromSelector(sel);
if ([methodString hasPrefix:@"test"]) {
class_addMethod(self, sel, (IMP)testResolveInstanceMethod, "B@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
备援接收者
当前接收者还有第二次机会处理未知的选择器,在这一步中,运行期系统会询问他,能否将这条消息转发给其他接收者处理,对应的处理方法如下:
-(id)forwardingTargetForSelector:(SEL)selector
若当前的接收者能找到备援对象,则将其返回,若找不到,返回nil。通过此方案,我们可以用“组合”来模拟出“多重继承”的某些特性。在一个对象内部,可能含有一系列的对象,该对象可经由此方法将能够处理某选择器的对象返回,这样在外界看来,好像是由该对象亲自处理的。需要注意的是,我们无法操作经由这一步所转发的消息。如果想在发送给备援接收者之前修改消息,那就只能通过完整的消息转发机制来做了。
完整的消息转发
如果转发算法到达这一步,那么只能通过完整的消息转发机制了。首先创建NSInvocation对象,把尚未处理的消息全部封装到里面,包含选择器、目标和参数,在触发NSInvocation对象时,“消息派发系统”将亲自触发,把消息派发给对象。调用如下方法:
- (void)forwardInvocation:(NSInvocation*)invocation;
这个方法实现比较简单,只需要改变调用目标,使消息在新目标可以被调用即可。通过这种方式和备援接收者方式相同,比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容。
实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法,这样的话,继承体系中的每个类都有机会处理此请求,直至NSObject。如果到了NSObject还未处理,那么该方法会调用“doNotRecognizeSelector”以抛出异常,表明选择器最终未被处理。
11、“方法调配技术”–swizzling
在上一节中,我们介绍了,对象收到消息之后,具体调用什么方法需要在运行期执行。类的方法列表会把选择器的名称映射到相关的方法实现之上,使得动态消息派发系统能够找到具体实现的方法。这些函数均以函数指针的方式来表示,这种指针叫做IMP,原型如下
id (*IMP) (id, SEL , ...)
NSString类可以响应lowercaseString,uppercaseString等选择器,映射表中的每个选择器都对应到不同的IMP上,如下所示:
持续更新中