为了编写更高质量OC的代码,这段时间读了这本书,真的很赞,特此记录下。
熟悉Objective-C
第1条:了解Objective-C语言的起源
Objective-C为C语言添加了面向对象特性,是其“超集”。
OC使用动态绑定的消息结构,也就是说,在运行的时候才会去检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行环境而非编译器来决定。
分配在堆中的的内存必须直接管理,分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。OC将堆内存管理抽象出来,不需要用malloc
及free
来分配或释放对象所占的内存。OC运行期环境把这部分工作抽象为一套内存管理架构,名叫引用计数。
当然有时,OC中遇到定义里不含*
的变量,它们可能不适用“栈空间”,这些变量保存的不是OC对象,例如CGRect
。
CGRect
是C的结构体,其定义是:
struct CGRect {
CGPoint origin;
CGSize size;
};
因为有时不使用这些结构体改用OC对象来做的话,性能会受影响。
理解C语言的核心概念有助于写好OC程序。尤其掌握内存模型与指针。
第2条:在类的头文件中尽量少引入其他头文件
除非确实有必要,否则不要引入头文件。一般来说,在某个类的头文件中使用向前声明(
@ classs "some.h";
)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。
第3条:多用字面量语法,少用与之等价的方法
NSNumber * someNumber = [NSNumber numberWithInt:1];
//字面量令代码中更为整洁
NSNumber * someNumber = @1;
NSArray * someArray = @[@"1",@"2"];
NSDictionary * someDictionary = @{@"one":@1,@"two":@2};
// 取值
NSString * one = someArray[0];
NSNumber * oneNumber = someDictionary[@"one"];
- 使用字面量语法,更加简单明了;
- 用字面量语法创建数组或字典的时候,若值中有
nil
,则会抛出异常,因此确保值不含有nil
,当然这样有利于查错。
第4条:多用类型常量,少用#define
预处理指令
编译单元之内
#define YANG_ANIMATION_DURATION 0.5
// 转换为
static const NSTimeInterval yangAnimationDuration = 0.3;
当然我们用到更多的是在编译单元之外的"全局符号表":
// .m 文件中
NSString * const LoginMethod = @"loginRequestHandler";
// .h 文件中
UIKIT_EXTERN NSString * const LoginMethod;
- 不要用预处理指令定义常量。 这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作,即使有人重新定义了常量值,编译器也不会产生⚠️,导致应用程序中的常量值不一致。
- 在实现文件中使用
static const
来定义“只在编译单元内可见的常量”; - 头文件中使用
UIKIT_EXTERN
来声明全局常量,并在相关实现文件中定义其值。
第5条:用枚举表示状态、选项、状态码
当使用 enum
的时候,建议使用新的固定的基础类型定义,因它有更强大的的类型检查和代码补全。 SDK 现在有一个 宏来鼓励和促进使用固定类型定义 - NS_ENUM()
typedef NS_ENUM(NSUInteger, YangMachineState) {
YangMachineStateNone,
YangMachineStateIdle,
YangMachineStateRunning,
YangMachineStatePaused
};
在处理枚举类型的switch 语句中不要实现default分支。这样的话,假如新枚举之后编译器就会提示我们开发者:switch 语句并未处理所有枚举。
对象、消息、运行期
第6条:理解"属性"这一概念
6-1、@property
:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。
@interface TestPeople : NSObject
@property NSString * name;
@property NSString * age;
@end
等同于
@interface TestPeople : NSObject
- (NSString *)name;
- (void)setName:(NSString *)name;
- (NSString *)age;
- (void)setAge:(NSString *)age;
@end
使用属性了的话,编译器不仅会自动编写访问这些属性所需要的方法,也会自动向类中添加适当类型的实例变量,并且在属性名称名前加下滑线,默认是_name
,_age
,同时在类的实现代码里通过@synthesize
语法来指定实例变量的名字
@implementation TestPeople
@synthesize name = _myName;
@synthesize age = _myAge;
@end
但是一般命名一般用默认的就OK,现在一般都不用@synthesize
,而@dynamic
,阻止自动合成存取方法,更不会用啦
6-2、属性特征
readwrite
:拥有“获取方法”和“设置方法”,可读可写。readonly
:仅仅拥有“获取方法”,只可读。assign
:“设置方法”只会执行针对“纯量类型”(CGFloat
,NSINteger
)的简单赋值操作。strong
:为这种属性设置新值的时候,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。weak
:为这种属性设置新值的时候,设置方法不保留新值,也不释放旧值,但是在属性所指的对象遭到销毁的时候,属性值会清空。copy
:设置方法并不保留新值,而是将其拷贝。特别是像NSString *
,因为其传递给设置的新值可能指向一个NSMutableString
类的实例,当值变化后,拷贝了的情况会保证字符串值不会无意间变动。-
getter=<name>
:指定“获取方法”的方法名@property (nonatomic, getter=isOn) BOOL on;
setter=<name>
:指定“设置方法”的方法名,这个不太常用。
另外注意的是:atomic
,nonatomic
,简单的说,前者是加了锁定机制来确保其操作的原子性,安全性更高,但是我们的属性一般都是声明nonatomic
。因为使用同步锁的开销较大,这会带来性能问题;一般情况下并不要求属性必须是原子性的,若需要则需要采用更深层次的锁定机制。
-
atomic
:默认是有该属性的,这个属性是为了保证程序在多线程情况下,编译器会自动生成一些互斥加锁代码,避免该变量的读写不同步问题。 -
nonatomic
:如果该对象无需考虑多线程的情况,请加入这个属性,这样会让编译器少生成一些互斥加锁代码,可以提高效率。
可以常用
@property
语法来定义对象中所封装的数据。
第7条:在对象内部尽量直接访问实例变量
遵循的规则是,在读取实例变量的时候采用直接访问的形式,而在设置实例变量的时候通过属性来做。
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据的时候,则应通过属性来
- 在初始化及
dealloc
,总是应该直接通过实例变量来读写数据。
第8条:理解“对象同等性”这一概念
NSObject
中两个用于判断的
- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
平常我们掌握第一种方法就好啦,像字符串这块- (BOOL)isEqualToString:(NSString *)aString;
是我们常用的方法。
相同的对象必须具有相同的哈希码,但是两个相同的对象却未必相同。因为可以这样理解哈希码计算的时候只是针对对象内存的部分而不是全部。
第9条:以“类族模式”隐藏实现细节。
谈到类族,自然涉及到新增子类的时候,但是需要遵守以下几条规则:
子类应该继承自类族中的抽象基类。
若要编写NSArray
类族的子类,则需令其继承自不可变数组的基类或可变数组的基类。子类应该定义自己的数据存储方式。
例如在编写NSArray
子类时,子类必须用一个实例变量来存放数组中的对象。因为NSArray
本身只不过是包在其他隐藏对象外面的壳,仅仅定义了数组中都需要具备的一些接口。-
子类应当覆写超类文档中指明需要覆写的方法。
在每个抽象类中,都有一些子类必须覆写的方法。比如说,编写NSArray
的子类的时候,就需要实现count
及objectAtIndex:
方法
在类族中实现所需隐藏的规范一般会都会在定义于基类的文档中,编码前应该先看看。类族模式可以把实现细节隐藏在一套简单的公共接口后面
+ (instancetype)buttonWithType:(UIButtonType)buttonType;
该方法所返回的对象,其类型取决于传入的按钮类型(buttonType
),然而不管返回哪种类型,都是继承自同一个类:UIButton
。这就是这种模式啦,可以仔细想想这种方式的优点。
第10条:在既有类中使用关联对象存放自定义数据
想到要如何为所有的对象增加实例变量吗?我们知道,使用Category
可以很方便地为现有的类增加方法,但却无法直接增加实例变量。不过从Mac OS X v10.6
开始,系统提供了Associative References
,这个问题就很容易解决了。
简单的说,现在你准备用一个系统的类,但是系统的类并不能满足你的需求,你需要额外添加一个属性。这种情况的一般解决办法就是继承。但是,只增加一个属性,就去继承一个类,总是觉得太麻烦类。
于是关联(association)
就出现了,先来看下列几个方法
// 先导入这个头文件
#import <objc/runtime.h>
// 此方法以给定的键和策略为某对象设置关联对象值
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
// 此方法根据给定的键从某对象获取相应的关联对象值
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
// 此方法移除指定对象的全部关联对象
OBJC_EXPORT void objc_removeAssociatedObjects(id object)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
objc_setAssociatedObject(self, @selector(property), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject
的意思就是 给当前这个类(self
)添加一个 叫value
的 关联属性,而且属性的唯一Id
叫 @selector(property)
。关联策略是 retain_nonatomic
....
-
id object
给谁设置关联对象。 -
const void *key
关联对象唯一的key
,获取时会用到的主键。 -
id value
关联对象。 -
objc_AssociationPolicy
关联策略,有以下几种策略:
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0,
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,
OBJC_ASSOCIATION_COPY_NONATOMIC = 3,
OBJC_ASSOCIATION_RETAIN = 01401,
OBJC_ASSOCIATION_COPY = 01403
};
详细可以看这个链接,分析的不错
http://www.cocoachina.com/ios/20150629/12299.html。
第11条:理解objc_msgSend
的作用
在OC中,如果向某个对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。
给对象发送消息可以这样来写:
id returnValue = [someObject messageName:parameter];
someObject
叫做“接收者”(receiver
),messageName
叫做“选择子”(selector
)。选择子与参数合起来称为“消息”(message
)。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做objc_msgSend
,其“原型”(prototype
)如下:
void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者
,第二个参数代表选择子
(SEL
是选择子
的类型),后续参数就是消息中的那些参数,其顺序不变。选择子
指的就是方法的名字。选择子
与方法
这两个词经常交替使用。编译器会把刚才那个例子中的消息转换为如下函数:
id returnValue = objc_msgSend(someObject,
@selector(messageName:),
parameter);
objc_msgSend
函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”(list of methods
),如果能找到与选择子名称相符的方法,就跳至其实现代码。若是找不到,那就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。
另外objc_msgSend_stret
(待发送的消息是结构体),objc_msgSend_fpret
(消息返回的是浮点数),objc_msgSendSuper
(给超类发消息),这是几个“边界情况”需要用来这些函数处理的。这些都是我们通常用的objc_msgSend
这种情况不太合适的,了解下就好了。
- 消息有接收者、选择子(SEL cmd)及参数构成。给某个对象“发送消息”,也就是相当于在该对象上“调用方法”。
- 发给某个对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
这个11点可以看看,这位朋友写的objc_msgSend消息传递学习笔记 ,很详细。
第12条:理解消息转发机制
第11条讲解了对象的消息传递机制,并强调了其重要性。第12条则要讲解另外一个重要的问题,就是对象在收到无法解读的消息之后会发生什么情况。
当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding
)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector
),这叫做“动态方法解析”(dynamic method resolution
)。第二阶段涉及“完整的消息转发机制”(full forwarding mechanism
)。如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又细分为两小步。首先,请接收者看看有没有其他对象能处理这条消息。若有,则运行期系统会把消息转给那个对象,于是消息转发过程结束,一切如常。若没有“备援的接收者”(replacement receiver
),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation
对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
创建NSInvocation
对象,把与尚未处理的那条消息有关的全部细节都封于其中。此对象包含选择子、目标(target
)及参数。在触发NSInvocation
对象时,“消息派发系统”(message-dispatch system
)将亲自出马,把消息指派给目标对象。
// 方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子
+ (BOOL)resolveInstanceMethod:(SEL)selector ;
//方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil。
- (id)forwardingTargetForSelector:(SEL)selector
// 完整的消息转发
- (void)forwardInvocation:(NSInvocation*)invocation
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在某个需要用到的某个方法事,再将其加入到类中。
- 对象可以把其无法解除某些选择子转交给其他对象来处理。
- 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
第13条:用“方法调配技术”调试“黑盒方法”
OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。此时你可以想:与给定的选择子名称相对应的方法是不是也可以在运行期改变呢?没错,就是这样。若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。此方案经常称为“方法调配”(method swizzling
)。
先来了解下:如何互换两个方法实现
void method_exchangeImplementations(Method m1, Method m2)
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 调换大小写
Method originalMethod =
class_getInstanceMethod([NSStringclass],
@selector(lowercaseString));
Method swappedMethod =
class_getInstanceMethod([NSStringclass],
@selector(uppercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
但实际中,这两个功能已经OK,没必要替换。但是,可以通过这一手段来为既有的方法实现增添新功能。比方说,想要在调用lowercaseString时记录某些信息,这时就可以通过交换方法实现来达成此目标。
新方法可以添加至NSString的一个“分类”(category)中:
// .h
@interface NSString (TestAdditions)
- (NSString*)eoc_myLowercaseString;
@end
// .m
@implementation NSString (TestAdditions)
- (NSString*)eoc_myLowercaseString {
NSString *lowercase = [self eoc_myLowercaseString];
NSLog(@"%@ => %@", self, lowercase);
return lowercase;
}
@end
这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后,通过下列代码来交换这两个方法实现:
Method originalMethod =
class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod =
class_getInstanceMethod([NSString class],
@selector(eoc_myLowercaseString));
method_exchangeImplementations(originalMethod, swappedMethod);
执行完上述代码之后,只要在NSString实例上调用lowercaseString方法,就会输出一行记录消息:
NSString *string = @"I aM BDd Man";
NSString *lowercaseString = [string lowercaseString];
//打印: I aM BDd Man => i am bad man
通过此方案,开发者可以为那些“完全不知道其具体实现的”(completely opaque
,“完全不透明的”)黑盒方法增加日志记录功能,这非常有助于程序调试。然而,此做法只在调试程序时有用。很少有人在调试程序之外的场合用上述“方法调配技术”来永久改动某个类的功能。不能仅仅因为OC语言里有这个特性就一定要用它。若是滥用,反而会令代码变得不易读懂且难于维护。
- 在运行期,可以向类中新增或替换选择子对应的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,可以用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
第14条:理解"类对象"的用意
Objective-C实际上是一门极其动态的语言。第11条讲解了运行期系统如何查找并调用某方法的实现代码,第12条则讲述了消息转发的原理:如果类无法立即响应某个选择子,那么就会启动消息转发流程。然而,消息的接收者究竟是何物?是对象本身吗?运行期系统如何知道某个对象的类型呢?对象类型并非在编译期就绑定好了,而是要在运行期查找。而且,还有个特殊的类型叫做id
,它能指代任意的Objective-C对象类型。一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息。而类型为id的对象则不然,编译器假定它能响应所有消息。
“在运行期检视对象类型”这一操作也叫做“类型信息查询”(introspection
,“内省”),这个强大而有用的特性内置于Foundation
框架的NSObject
协议里,凡是由公共根类(common root class
,即NSObject
与NSProxy
)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。
先说一下,类是一个对象是Class
类型的对象简称“类对象”
// 一个任意的类型,表示一个Objective-C类
typedef struct objc_class *Class;
类型查询方法:
可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”
能够判断出对象是否为某个特定类的实例,而“isKindOfClass:”
则能够判断出对象是否为某类或其派生类的实例。例如:
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; ///< NO
[dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES
[dict isKindOfClass:[NSDictionary class]]; ///< YES
[dict isKindOfClass:[NSArray class]]; ///< NO
比较类对象是否等同的办法来判断,使用==
操作符,而不要使用比较Objective-C对象时常用的“isEqual:
”方法。原因在于,类对象是“单例”(singleton
),在应用程序范围内,每个类的Class
仅有一个实例。
这个理解就好,注意下面几个要点就好啦。
- 每个实例都有一个指向
Class
对象的指针,用以表明其类型,而这些Class
对象则构成了类的继承体系。
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
持续笔记中·····