Effective Obj-C <一>

其实很久以前就看过这本书了,在实践的过程中也不断的遵循着这些tips,不过随着时间的推移,苹果又加入了一些新元素,所以记录下来,不断的更新,完善这些iOS开发中需要注意的点。

熟悉的Obj-C

这是一门很古老的语言了,古老到苹果已经推出新的语言来代替了,然而这也不能阻止我依然喜欢这门语言,虽然它还有很多缺陷,但也能看出苹果在不断的完善中。

了解Obj-C的起源

和普通面向对象语言最大的区别是,消息发送和函数调用的区别,前者依托于运行时环境,而后者在编译期已经确定。

类的头文件越简洁越好

可以使用@class来取代import,然后在真正需要的地方使用import,还可以使用@import来引入模块,减少编译时间和引用泛滥。

协议尽量单独文件以减少不必要的引入,如果是类似于delegate的协议则可以放在同一个头文件,如果是实现这类协议可以放到类扩展中。

便捷的字面量

字面量能够极大的简化,而且可读性还增加了。

NSNumber *intNumber = @1;
NSNumber *floatNumber = @1.1;
NSNumber *boolNumber = @YES;
NSNumber *charNumber = @'a';
NSInteger number = 1;
NSNumber *expressionNumber = @(number);

包括NSString,NSArray,NSDictionary都有这样的便捷写法,然而除了NSString可以用这种字面量来申明成静态变量,其他都不行,会报错

initializer element is not a compile-time constant

原因是这只是个让程序员方便的语法糖而已,能让我们创建一个 autoreleased的对象。

慎用#define

原因很简单,没有编译器检查,值可以更改而不提示。在实现文件中可以用static const,在头文件则可以使用extern。

需要注意的是,在头文件中的会注册到全局符号表,最好加上特殊前缀,避免重复。

用枚举,而不是Bool

记得曾经看着代码中一个个Bool变量,千万只草泥马飞奔而过,完全不知道该如何去理清逻辑,而枚举值可以比较清晰的表明不同状态。

现代Obj-C中尽量用NS_ENUM和NS_OPTIONS取代enum,前者可以方便的指定类型,后者可以用在一些条件是组合类型的情况。

typedef NS_ENUM(NSUInteger, MyEnum) {
    <#MyEnumValueA#>,
    <#MyEnumValueB#>,
    <#MyEnumValueC#>,
};

typedef NS_OPTIONS(NSUInteger, <#MyEnum#>) {
    <#MyEnumValueA#> = 1 << 0,
    <#MyEnumValueB#> = 1 << 1,
    <#MyEnumValueC#> = 1 << 2,
};

最后一点就是和switch搭配使用的时候,不要加default分支。

New in iOS7

1.instancetype
这个基本在现代话Obj-C中都改过来了,之前都是返回id,然而编译器并不能识别,不能够很好的提示,所以建议以后的初始化方法返回都用这个。

2.Modules

这个也是个好东西,可能你还没有使用,但其实编译器已经帮你优化了。PCH是曾经用来解决多出引用相同的模块,然而太容易被滥用了,最终的结果就是所有的文件引用都塞进去。

所以引入了module,在编译的时候加入一个Modules列表。如果在编译的文件中引用到某个Modules的话,先在这个列表内找,找到就用,如果没找到,则把该文件加入到这个表中。

New in iOS9

1.Nullability

我觉得主要用在规范上,和下面说的范型是一样一样的,可以用来保证使用方按照我的节奏返回非空数值。

2.范型

Obj-C中两个让我头疼的点终于解决了一个,范型也是用在protocol中比较方便,约定好了数据,再也不用担心使用者乱来。

3.__kindof

还是用在协议中比较好,再也不需要用一个id来表示一类返回值了。

对象,消息和运行时

这一节个人感觉是iOS开发必须要掌握的,深入的理解Obj-C的对象和消息,掌握runtime才能写出高效的代码。正是有了运行时,让Obj-C和其他编译型语言有了很大的区别,当然也带了一些弊端(高效的内联函数,强类型检查等),不过总体来说给开发者带来了多元化的选择。

属性究竟是什么鬼

很长一段时间,其实我也没搞清楚属性究竟是干嘛的,直到有一天,我在protocol中定义了属性后,终于发现其实属性只是一种便捷方式。便捷的合成set,get方法,便捷的生产实例变量。

外部总应该通过属性来访问内部的数据,起到了一层过滤,保护的作用。同时属性还能附带很多特性(原子性,读写权等),熟练的使用属性能够方便的操作数据。

属性Or实例变量

我个人是偏好使用属性的,但是在最近的使用过程中发现,有时候使用属性是有可能产生问题的,比如在继承关系中,比如在accessor方法中还有一些特殊的操作。

现在,在更多的内部使用中,我倾向于直接用实例变量,可以省去一步消息发送,减少内存管理时带来的负担(如果需要KVO或者懒加载则还是使用属性)。

坑爹的对象比较

由于Obj-C中不能操作符重载,所以对象的==比较其实比较的是指针值,经常出现的情况就是同样的NSString,然而比较下来并不相同。

所以Obj-C提供了一个方法来解决这个问题,isEqual。需要注意的是isEqual判定相等的两个对象hash值也必须相等,但是反过来却不是必要的。

class cluster

这个在Obj-C中很常见,大体上可以用工厂来实现,主要用于隐藏具体的细节,而又不想暴露不同的子类。在类别判断的时候尤其要小心,建议用category来扩展系统的class cluster,而不是继承。

category中的“属性”

有时候想给一个私有类添加属性并不一定要通过继承来实现,基于Obj-C的runtime机制,我们可以动态的给类添加“属性”。

static char String;
- (void)setString:(NSString *)string{
    objc_setAssociatedObject(self, &String, string, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)string{
    return objc_getAssociatedObject(self, &String);
}

关联对象很方便,但是却不推荐用,关联对象的清理等问题需要有一定的底层知识才能够理解的,而且不利于问题排查。

消息发送和转发

objc_msgSend这个函数虽然不常用,但其实无时不刻不在使用中,整个Obj-C的方法调用都是基于这个函数,区别于C语言的函数静态绑定,Obj-C可以推迟到运行时才决定方法调用(消息发送)。

当对象无法响应该消息时,就要用到消息转发机制了,分为两步:

1.动态添加方法

+(BOOL)resolveInstanceMethod:(SEL)sel
+(BOOL)resolveClassMethod:(SEL)sel

这两个方法会在第一时间被调用,在里面可以做很多小动作,比如说动态的添加方法。可以实现类似coredata的dynamic属性的accessor方法。

到这一步如果还不能响应,那么这个对象就会找备胎了。

-(id)forwardingTargetForSelector:(SEL)aSelector

通过这个返回值可以把消息转发给相应的对象,可以实现模拟多继承。

2.完整的消息转发

如果前面这些步骤都没能解决,那最后Obj-C就要寄出大招了,完整的消息转发

-(void)forwardInvocation:(NSInvocation *)anInvocation;

最后实在没辙就只能发给这个,系统默认是会抛出异常。

-(void)doesNotRecognizeSelector:(SEL)aSelector;

method swizzling

这个技术其实很有用,不仅仅是在动态改变系统方法的实现,还可以完成AOP编程。

具体使用无非就是靠method_exchangeImplementaions来起到方法的实现互换。用在调试上比较方便,省去了在每个地方都写NSLog。

类对象

首先需要明确的一点就是Obj-C是一门动态语言,其次它也是一门依托于对象的语言。所以在Obj-C中类也是对象。

每个对象都有一个isa指针指向所属的类,为了形成完整的闭环,NSObject的metaclass的isa指向的是自己。

如果对象类型无法在编译器确定(id类型),那么可以通过内省来严格判断,并且尽量使用这样的方式而不是用==。