Effective Objective-C读书笔记,记录书中的总结点,加入了一些例子,方便理解和后期回顾。
一、熟悉Objective-C
1、了解OC语言的起源
- oc为c语言添加了面向对象特性,是其超集。oc使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。
- 理解c语言的核心概念有助于写好oc程序。尤其要掌握内存模型与指针。
C语言内存模型
2、在类的头文件中尽量少引入其他头文件
- 除非却有必要,否则不要引入头文件。一般来说,应在某各类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合(coupling),缩短编译时间。
.h
#import <Foundation/Foundation.h>
//向前声明
@class Car;
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, strong) Car *car;
@end
.m
#import "Person.h"
#import "Car.h"
@implementation Person
@end
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continuation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
3、多用字面量语法,少用与之等价的方法
字面量字符串
NSString *str = @"This is string";
字面量数值
NSNumber *intNumber = @1;
NSNumber *floatNumber = @2.5;
NSNumber *doubleNumber = @3.14159;
NSNumber *bollNumber = @YES;
NSNumber *charNumber = @'a';
字面量数组
NSArray *array = @[@"1`",@"2",@"3"];
字面量字典
NSDictionary *dic = @{@"one":@"1",@"two":@2};
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或字典时,若值中有nil,则会抛出异常。因此,务必确保值里不含nil。
4、多用类型常量,少用#define预处理指令
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用 static const 来定义 “只在编译单元内可见的常量(translation-unit-specific constant)” 。由于此类常量不在全局符号表中,所以无须为其名称加前缀。
static const NSString *kObserverName = @"name";
//用const修饰之后,如果修改它,那么编译器就会报错。
//而static修饰符则意味着该变量仅在定义次变量的编译单元可见。
- 在头文件中使用 extern 来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区分,通常用与之相关的类名做前缀。
.h
//声明一个全局常量
extern NSString *const PersonStringConstant;
.m
//定义全局常量
NSString *const PersonStringConstant = @"";
5、用枚举表示状态、选项、状态码
- 应该用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个一栋的名字。
/**
网络请求类型枚举
*/
typedef NS_ENUM(NSInteger, NetworkMethod) {
NetworkMethodGet,
NetworkMethodPost,
NetworkMethodPut,
NetworkMethodDelete,
};
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项定义为2的幂,以便通过按位或操作将其组合起来。
typedef NS_OPTIONS(NSUInteger, UIRemoteNotificationType) {
UIRemoteNotificationTypeNone = 0,
UIRemoteNotificationTypeBadge = 1 << 0,
UIRemoteNotificationTypeSound = 1 << 1,
UIRemoteNotificationTypeAlert = 1 << 2,
UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3,
}
- 用NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。
- 在处理枚举类型的 switch 语句中不要实现 default 分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch 语句并未处理所有枚举。
二、对象、消息、运行时
1、理解“属性”这一概念
- 可以用 @property 语法来定义对象中封装的数据
- 通过 “特质” 来指定存储数据所需的正确语义
- 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义
- copy特质一般用在NSString、NSArray、NSDictionary等不变的变量类型上,这样能保护其封装性。因为传递给设置方法的新值可能指向一个可变的实例(如NSMutableString)。
如一个Person类,有一个name属性,如果用strong特质来进行修饰:
import <Foundation/Foundation.h>
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@end
如果像下面这样,那么这个Person的实例中的name属性值就可能会莫名其妙的被改了:
- ```
Person *p = [[Person alloc] init];
NSMutableString *newName = [[NSMutableString alloc] initWithFormat:@"gaofeng"];
p.name = newName;
NSLog(@"====== %@",p.name);// gaofeng
[newName appendString:@"tan"];
NSLog(@"====== %@",p.name);// gaofengtan
这并不是我们想要的结果。
- 开发iOS程序时应该使用 nonatomic 属性,因为atomic属性开销太大,会严重影响性能,而且 nonatomic 特质也并不能保证线程安全。
列如,一个线程在连续多多次读取某属性值得过程中有别的线程在同时改写该值,那么即便将属性声明为 *atomic* ,也还是会读到不同的属性值。
2、在对象内部尽量直接访问实例变量
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
- 在初始化方法及delloc方法中,总是应该直接通过实例变量来 读写 数据。
- 有时会使用懒加载技术配置某份数据,这种情况下,需要通过属性来读取数据。
列如:
- (Car *)car {
if (!_car) {
_car = [Car new];
}
return _car;
}
若没有调用 “获取方法” 就直接访问实例变量,则会看到尚未设置好的car,所以说,如果使用懒加载技术,那么必须通过存取方法来访问其数据。
3、理解 “对象等同性(equality)” 这一概念
- 若想检测对象的等同性,请提供 “isEqual:” 和 “hash” 方法。
这里提供一篇文章,比较详细的讲解了这两个方法 isEqual & hash - 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞几率低的算法。
4、以 “类族模式” 隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
.h
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, GFEmployeeType) {
GFEmployeeTypeDeveloper,
GFEmployeeTypeDesigner,
GFEmployeeTypeFinance
};
//雇员基类
@interface GFEmployee : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger salary;
+ (instancetype)employeeWithType:(GFEmployeeType)type;
- (void)doADaysWork;
@end
.m
#import "GFEmployee.h"
#import "GFEmployeeDeveloper.h"
#import "GFEmployeeDesign.h"
#import "GFEmployeeFinance.h"
@implementation GFEmployee
+ (instancetype)employeeWithType:(GFEmployeeType)type {
switch (type) {
case GFEmployeeTypeDeveloper:
return [GFEmployeeDeveloper new];
break;
case GFEmployeeTypeDesigner:
return [GFEmployeeDesign new];
break;
case GFEmployeeTypeFinance:
return [GFEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
// subclass implement this
}
@end
使用
GFEmployee *employee = [GFEmployee employeeWithType:GFEmployeeTypeDeveloper];
[employee doADaysWork];
- 系统框架中经常使用类族。
比如UIButton:UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
5、在既有类中使用关联对象(Associated Object)存放自定义数据
- 可以通过 “关联对象” 机制来把两个对象连起来。
//创建一个自定义UIBarButtonItem,点击方法直接通过一个block来回调
#import "UIBarButtonItem+Common.h"
#import <objc/runtime.h>
static const void *BarButtonItemBlockKey = &BarButtonItemBlockKey;
@implementation UIBarButtonItem (Common)
+ (UIBarButtonItem *)itemWithBtnTitle:(NSString *)title clickHandle:(void (^)(void))action {
UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] init];
buttonItem.title = title;
buttonItem.target = buttonItem;
buttonItem.style = UIBarButtonItemStylePlain;
buttonItem.action = @selector(handleClick:);
[buttonItem setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor lightGrayColor]} forState:UIControlStateDisabled];
//给buttonItem添加关联对象
objc_setAssociatedObject(buttonItem, BarButtonItemBlockKey, action, OBJC_ASSOCIATION_COPY_NONATOMIC);
return buttonItem;
}
- (void)handleClick:(UIBarButtonItem *)buttonItem {
//获取buttomItem对应key的关联对象
void (^block)(void) = objc_getAssociatedObject(buttonItem, BarButtonItemBlockKey);
block();
}
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用 “拥有关系” 与 “非拥有关系”。
内存管理语义:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, // assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // nonatomic, retain
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // nonatomic, copy
OBJC_ASSOCIATION_RETAIN = 01401, // retain
OBJC_ASSOCIATION_COPY = 01403 // copy
};
- 关联对象的key是个 “不透明的指针(所指向的数据结构不局限于某种特定类型的指针)” ,在设置关联对象值时,通常使用静态变量做键。
- 只有在其他做法不可行时才应选用关联对象,因为这种做法通常会引入难于查找的bug(循环引用)。
6、理解 objc_msgSend 的作用(消息传递机制)
- 消息由接收者(receiver)、选择子(selector)及参数(parms)构成。给某对象 “发送消息” 也就相当于在该对象上 “调用方法”(call a method)。
// 发送消息最终会转变为调用函数,叫做 objc_msgSend,它的原型如下:
void objc_msgSend(id self, SEL _cmd, ...)
- 每个类里面都有一张表用来存放该类的所有方法,其中选择子的名称则是查找方法时所用的 “键”。
- 发给某对象的全部消息都要由 “动态消息派发系统”(dynamic message dispatch system) 来处理,该系统会查出对应的方法,并执行其代码。
7、理解消息转发机制
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再讲其加入类中。(动态方法解析)
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
- 对象可以把其无法解读的某些选择子转交给其他对象来处理。(备援接收者)
- (id)forwardingTargetForSelector:(SEL)aSelector
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。(完整的消息转发)
- (void)forwardInvocation:(NSInvocation *)anInvocation
-
经过以上消息转发后若消息还是无法得到处理,那就会调用NSObject的方法 “doesNotRecognizeSelector:” 抛出异常(unrecognized selector sent to instance)
8、用 “方法调配技术(method swizzling)” 调试 “黑盒方法”
- 在运行期,可以向类中新增或替换选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做 “方法调配” ,开发者常利用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种方法不宜滥用。
方法调配中常用的方法:
//获取方法
Method method = class_getClassMethod(Class cls, SEL name)
Method method = class_getInstanceMethod(Class cls, SEL name)
//交换方法
method_exchangeImplementations(Method m1, Method m2)
9、理解 “类对象” 的用意
- 每个实例都有一个指向Class对象的指针(isa),用以表明其类型,而这些Class对象则构成了类的继承体系。
-
类对象本质是一个结构体,此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备了多少个实例变量等信息。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。
- 如果对象类型无法再编译期确定,那么就应该使用类型信息查询方法来探知。
// 能够判断出对象是否为某个特定类的实例
isMemberOfClass:
// 能够判断出对象是否为某类或者其派生类的实例
isKindOfClass:
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。