《Effective Objective-C 2.0编写高质量i
用Objective-C等面对对象语言编程时,“对象”(object)就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。
在对象之间传递数据并执行任务的过程,就叫做“消息传递”。
当应用程序运行起来后,为其提供相关支持的代码叫做“Objective-C运行期环境”(Objective-C runtime)。它提供了一些使得对象之间能够传递消息的重要函数,并且包含创建类实例所用的全部逻辑。
6. 理解“属性”这一概念
//copy
// EOCPerson.h
@interface EOCPerson : NSObject
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
//与下面setter和getter方法等效
//- (NSString *)firstName;
//- (void)setFirstName:(NSString *)firstName;
//- (NSString *)lastName;
//- (void)setLastName:(NSString *)lastName;
- (id)initWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName;
@end
/************************/
// EOCPerson.m
#import "EOCPerson.h"
@implementation EOCPerson
- (id)initWithFirstName:(NSString *)firstName
lastName:(NSString *)lastName {
if ((self = [super init])) {
_firstName = [firstName copy];
_lastName = [lastName copy];
}
return self;
}
@end
属性特质
分为4类:通过这些特质,可以 微调 由编译器所合成的存取方法。
1. 原子性
.1 默认情况下,由编译器所合成的方法会通过**锁定机制**确保其原子性(atomicity)。
.2 若属性具备 nonatomic 特性,则不适用同步锁。
.3 若未标明具备 “atomic”或 “ nonatomic” 特性,那么,仍然是“原子的”的属性特质。
2. 读/写权限
.1 readwrite特质
声明该特质的属性,拥有 getter (获取方法)和 setter (设置方法);
但,当该属性由 @synthesize 实现时,编译器会自动为其合成读取方法。
.2 readonly特质
声明该特质的属性,仅拥有获取方法。
但,当该属性由 @synthesize 实现时,编译器才会为其合成获取方法。
特殊写法:可以利用此特质,可以把某个属性对外公开为只读属性,然后在.m文件中,将其重新定义为读写属性。详见27条
3. 内存管理语义
属性用于封装数据,而数据则要有“具体的所有权语义”。下面的这些特性仅会影响“设置方法”setter。
.1 assign
setter 只会执行针对“纯量类型”的简单赋值操作。如CGFloat、NSInteger等
.2 strong 此特质表明该属性:
定义了一种“拥有关系”(owning relationship)。
为这种方法设置新值时,设置保留新值->释放旧值->设置为新值
.3 weak 此特质表明该属性:
定义了一种“非拥有关系”(nonowning relationship)。
为这种方法设置新值时,设置方法既不保留新值,也不释放旧值。
在属性所指对象遭到摧毁时,属性值也会清空(nil out)。
.4 unsafe_unretained
语义与 assign 比较,设置方法只会针对“对象类型”。
该特质表达了一种“非拥有关系”(unretained),当目标对象遭到摧毁时,属性值不会自动清空(unsafe,不安全),与weak有区别。
.5 copy
设置方法并不保留新值,而是“拷贝”。
当属性类型为NSString *时,经常用此特质保护其封装性。
原因:新值可能是指向一个NSMutableString类的实例,是NSString的子类,那么设置完成后,字符串的值就可能在对象不知情的情况下遭人更改。所以要拷贝一份,确保对象中的字符串值不会无意间变动。
案例:setter方法:_xxxx = [xxxx copy];
4. 方法名
可通过如下特质指定存取方法的方法名:
.1 getter=<name>
指定获取方法的方法名。
当属性为BOOL或Boolean类型时,如果想在获取方法前面加上“is”前缀,就可以使用此方法。
案例:@property (nonatomic, getter=isOn) BOOL on;
.2 setter=<name>
指定设置方法的方法名。
用法不常见。
要点
- 可以用 @property 语法来定义对象中所封装的数据。
- 通过 “特质” 来指定存储数据结构所需的正确语义。
- 在设置属性所对应的实例变量时,一定要遵从该属性声明的语义。
- 开发iOS程序时应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能。
7. 在对象内部尽量直接访问实例变量
使用“点语法” 和 直接访问实例变量 的写法区别
.1 直接访问实例变量的 速度比较快。
这种方式,不经过Objective-C的“方法派发”(method dispatch,见11条),编译器所生成的代码会 直接访问 保存对象实例变量的那块 内存。
.2 直接访问实例变量时,不会调用其“设置方法”。
绕过了为相关属性所定义的“内存管理语义”。
.3 直接访问实例变量,不会触发“键值观测”(KVO)通知。
是否因此产生问题,取决于具体的对象行为。
.4 通过属性来访问,有助于排查与之相关的错误。
因为,可以给“获取方法”或“设置方法”中新增断点(breakpoint),监控该属性的调用者及其访问时机。
要点
- 在 对象内部 读 取数据时,应该直接读取实例变量(_xxxx);而 写 入数据时,通过属性来写(set.xxx)。
- 在 初始化方法 和 dealloc 方法中,总是应该 直接通过实例变量(_xxxx) 来读写数据。
- 有时会使用 惰性初始化技术(懒加载) 配置某份数据,在这种情况下,需要通过属性来读数据。
8.理解概念:“对象等同性”
NSString *elem = @"hello 123";
NSString *elem2 = [NSString stringWithFormat:@"hello %d", 123];
NSLog(@"1 %@", (elem == elem2)?@"YES":@"NO");//NO
NSLog(@"2 %@", [elem isEqual:elem2]?@"YES":@"NO");//YES
NSLog(@"3 %@", [elem isEqualToString:elem2]?@"YES":@"NO");//YES。字符串比较时,用时优于isEqual
NSLog(@"4 %lu_%lu_%@", (unsigned long)[elem hash], (unsigned long)[elem2 hash], ([elem hash] == [elem2 hash])?@"YES":@"NO");//比较哈希值:YES
NSArray *arr1 = @[@"1", @"2"];
NSArray *arr2 = [NSArray arrayWithObjects:@"1", @"2", nil];
if ([arr1 isEqualToArray:arr2]) {
NSLog(@"%@: YES", NSStringFromSelector(_cmd));
}else {
NSLog(@"%@: NO", NSStringFromSelector(_cmd));
}
特定类具有的等同性判定方法:
#pragma mark - 8. 判断等同性
- (BOOL)isEqualToEOCPerson:(EOCPerson *)anotherPerson {
if (self == anotherPerson) {
return YES;//判断两个指针是否相等
}
if (![_firstName isEqualToString:anotherPerson.firstName]) {
return NO;
}
if (![_lastName isEqualToString:anotherPerson.lastName]) {
return NO;
}
if (_age != anotherPerson.age) {
return NO;
}
return YES;
}
- (BOOL)isEqual:(id)object {
if ([[self class] isKindOfClass:[object class]]) {
return [self isEqualToEOCPerson:(EOCPerson *)object];
} else {
return [super isEqual:object];
}
}
容器中可变类的等同性: 向比较对象中插入可变元素,后面的行为将很难预料。
NSMutableSet *setA = [NSMutableSet new];
[setA addObject:arr1]; NSLog(@"setA: %@", setA);//{((1,2))}
[setA addObject:arr2]; NSLog(@"setA: %@", setA);//{((1,2))}
NSMutableArray *arr3 = [[NSMutableArray alloc] initWithObjects:@"1", nil];
[setA addObject:arr3]; NSLog(@"setA: %@", setA);//{((1),(1,2))}
[arr3 addObject:@"2"]; NSLog(@"setA: %@", setA);//{((1,2),(1,2))}
NSSet *setB = [setA copy]; NSLog(@"setB: %@", setB);//{((1,2))}
要点总结:
"=="操作符比较的是两个指针本身,而不是所指的对象
字符串比较时,isEqualToString用时优于isEqual
数组NSArray和字典NSDictionary都有对应的比较方法:isEqualToArray、isEqualToDictionary
“深度等同性判定”,指若对应位置上的对象均相等,那么两个就想等。比如若是从数据库读取数据,其中对象可能包含另外一个属性“唯一标识符”(主键,primary key)。那么,只需要根据它判断等同性。
- 若想检测对象间的等同性,请提供 “isEqual:” 或 hash 方法。
- 相同的对象具有相同的哈希码(hash值),但具有相同哈希码(hash值)的对象不一定相等。
- 不要盲目的逐个检测每条属性,而应该按照具体情况来制定检测方案。如:两个对象是否具有“唯一标识符”。
- 编写hash算法时,应使用 计算速度快 但 哈希值碰撞几率低 的算法。
提问:
-
平时使用等同性判定有什么细节?
-
什么情况下需要使用自定义等同性判定?
-
有哪些等同性判定的方法,如何优化自定义?
.1 isEqualToString等方法,注意OC语言特性,在编译期不做强类型检查(strong type checking) .2 等同判定频繁,且比较的属性(或对持有对象)等比较多 .3 可以判断特征值(如唯一标识符),也可以比较生成hash值
9. 以 “类族模式” 实现隐藏细节
工厂模式 是 创建类族 的方法之一。如下:
1). 抽象父类 abstract class
.h
//
// EOCEmployee.h
// Copyright © 2018 Mr. Wang. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSUInteger, EOCEmployeeType) {
EOCEmployeeTypeDeveloper,
EOCEmployeeTypeDesigner,
EOCEmployeeTypeFinance
};
/** abstract class:抽象子类 */
@interface EOCEmployee : NSObject
@property (copy) NSString *name;
@property (assign) NSUInteger salary;
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)employeeType;
- (void)doADaysWork;
@end
.m
//
// EOCEmployee.m
// Copyright © 2018 Mr. Wang. All rights reserved.
//
#import "EOCEmployee.h"
#import "EOCEmployeeDeveloper.h"
@implementation EOCEmployee
+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)employeeType {
switch (employeeType) {
case EOCEmployeeTypeDeveloper:
return [EOCEmployeeDeveloper new];
break;
case EOCEmployeeTypeDesigner:
return [EOCEmployeeDesigner new];
break;
case EOCEmployeeTypeFinance:
return [EOCEmployeeFinance new];
break;
}
}
- (void)doADaysWork {
//sub method
}
2). 实现子类:concrete class
.h
//
// EOCEmployeeDeveloper.h
// Copyright © 2018 Mr. Wang. All rights reserved.
//
#import "EOCEmployee.h"
/** concrete class:实体子类 */
@interface EOCEmployeeDeveloper : EOCEmployee
@end
.m
//
// EOCEmployeeDeveloper.m
// Copyright © 2018 Mr. Wang. All rights reserved.
//
#import "EOCEmployeeDeveloper.h"
@implementation EOCEmployeeDeveloper
- (void)doADaysWork {
if (self.name && self.salary) {
NSLog(@"%@做了一天的开发者,得到了%lu钱", self.name, (unsigned long)self.salary);
}else {
NSLog(@"我做了一天的开发者");
}
}
@end
3). 调用
/*
实现: 在公司有个搞开发的,叫Samara,工资时200元,输出他一天的工作绩效。
*/
EOCEmployee *aEmployee = [EOCEmployee employeeWithType:EOCEmployeeTypeDeveloper];
aEmployee.name = @"Samara";
aEmployee.salary = 200;
[aEmployee doADaysWork];
为类族增加新的子类。需要遵守:
-
子类应该 继承自类族中的抽象基类。
-
子类应该 定义自己的存储格式。
超类本身只不过是抱在其它隐藏对象外面的壳,仅仅定义了所有类都要具备的一些接口。
-
子类应该 覆写超类文档中指明需要覆写的方法。
具体写法,根据文档
要点总结:
- 类族模式可以把 实现细节隐藏 在一套简单的 公用接口后面。
- 系统架构 中经常使用类族。
- 从类族的公共抽象类中继承子类时要当心,若有开发文档,应先读。
10. 在既有类中使用关联对象存放自定义数据
存储策略
-
对象关联类型
|关联类型|等效的 @property 属性
|:--|:--
|OBJC_ASSOCIATION_ASSIGN| assign
|OBJC_ASSOCIATION_RETAIN_NONATOMIC|nonatomic, retain
|OBJC_ASSOCIATION_COPY_NONATOMIC| nonatomic, copy
|OBJC_ASSOCIATION_RETAIN| retain
|OBJC_ASSOCIATION_COPY|copy -
管理关联对象
- void objc_setAssociatedObject** (id object, void *key, id value, objc_AssociationPolicy policy)
此方法 以给定的键和策略 为某对象设置关联对象值。
- id objc_getAssociatedObject**(id object, void *key)
此方法 根据给定的键从某对象中 获取相应的关联对象值。
- void objc_removeAssociatedObjects**(id object)
此方法 移除指定对象的全部关联对象。
使用场景
在分类中使用runtime动态添加属性
要点
- 可以通过 “关联对象”机制 来把两个对象连起来。
- 定义关联对象时,可指定内存管理语义,用以模仿定义属性是所采用的 “拥有关系” 与 “非拥有关系”。
- 只有在其它做法不可行时,才应选用关联对象。因为这种做法通常会引入难于查找的bug。
11. 理解 objc_msgSend 的作用(对象的消息传递机制)
信息的调用过程
- objc_msgSend
//给对象发送消息 OC中
id returnValue = [someObject messageName:parameter];
//编译器看到此消息后,将其转换为一条标准的C语言函数调用,所用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend,其“原形”(property)如下:
void objc_msgSend(id self, SEL cmd, ...) //参数可变函数
//即:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
信息调用过程的应对其它“边界情况”的,Objective-C运行环境中的一些函数:
- objc_msgSend_stret
若,待发送的消息要返回 结构体,则可交由此函数处理。
- objc_msgSend_fpret
若,带发送的消息要返回 浮点数,则可交由此函数处理。
- objc_msgSendSuper
若,要给超类发送消息,则可交由此函数处理。如 [super message:parameter];
要点
- 消息由接收者、选择子和参数构成。给某对象发送消息(invoke a message) 也就相当于在该对象上“调用方法”(call a method)。
- 发送给某对象的全部消息都由 “动态消息派发系统”(dynamic message dispatch system) 来处理:查出对应的方法,执行其代码。
12. 理解消息转发机制
对象在接受到消息后,可能可以解读,也可能无法解读。
动态方法解析 第一次机会
在无法解决时,调用
+ (BOOL)resolveInstanceMethod:(SEL)sel;//处理实例方法
//或
+ (BOOL)resolveClassMethod:(SEL)sel;//处理类方法
该方法的参数是那个未知的选择子,其返回值是Boolean类型,表示这个类能否新增一个实例方法 以处理此选择子。
备援接收者 第二次机会
- (id)forwardingTargetForSelector:(SEL)selector;
完整的消息转发 第三次机会
- (void)forwardInvocation:(NSInvocation *)invocation;
首先创建NSInvocation对象,把尚未处理的那条消息有关的全部细节都封于其中。此对象包括选择子、目标、及参数。在触发NSInvocation对象是,“消息派发系统”将亲自出马,把消息指派给目标对象。
最后
在实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。由此,继承体系里的每个类都有机会处理此调用请求,直至NSObject。若调用到了NSObject类的方法,该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,表明选择子最终未能得到处理。
消息转发全流程图要点总结
- 若对象无法响应某个选择子,则进入 消息转发流程。
- 通过运行期的动态方法解析功能,方法可以在 用到时再添加 到该类中。
- 对象可以把无法解析的选择子 转交给其它对象处理。
- 若经过上述两步还 未解析成功 选择子,那么 启用完整的消息转发机制。
13. 用“方法调配技术”调试“黑盒方法”
/*
class_getClassMethod: 获取类方法
class_getInstanceMethod: 获取对象方法
*/
//获取要交换的两个目标方法
Method methodA = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method methodB = class_getInstanceMethod([NSString class], @selector(uppercaseString));
//交换
method_exchangeImplementations(methodA, methodB);
/*** 测试 ***/
NSString *string = @"Samara";
NSLog(@"原小写%@_原大写:%@", [string lowercaseString], [string uppercaseString]);
//添加新功能
/** 只要用到了某类,就会调用该类的load 方法 */
+ (void)load {
//获取要交换的两个目标方法
Method methodA = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method methodB = class_getInstanceMethod([NSString class], @selector(eoc_lowercaseString));
//交换
method_exchangeImplementations(methodA, methodB);
}
- (NSString *)eoc_lowercaseString {
NSString *str = [self eoc_lowercaseString];
return [NSString stringWithFormat:@"Ori:%@_rst:%@",self, str];
}
//调用
NSString *string = @"Samara";
NSLog(@"%@", [string lowercaseString]);
//输出结果
2018-11-07 13:39:17.768666+0800 EffectiveOC[7729:423305] Ori:Samara_rst:samara
要点总结:
- 在运行期,可以向类中 新增或替换 选择子所对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做方法调配,开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有在调试程序的时候才需要在运行期修改方法,不宜滥用。
可用来开发微信抢红包、外挂等作弊功能
14. 理解“类对象”的用意
Class 对象在运行期头文件(objc/runtime.h)中的状态:
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
要点总结
- 每个实例都有一个指向 Class 对象的指针,用以表明其类型,而这些 Class 对象则构成了类的继承关系。
- 如果对象类型无法在编译期确定,那么就应该使用 类型信息查询方法 查询。
- 尽量使用 类型信息查询方法 确定对象类型,而不要直接比对对象类型,因为某些对象可能实现了 消息转发 功能。