第一章:熟悉 Objective-C
- OC 语言是使用“消息转发机制”而不是“函数调用机制”。主要区别就是:消息结构语言的代码都是在运行环境决定,而函数调用是由编译器决定。
- OC 创建的引用对象保存在
“堆空间(heap space)”
中,其变量是存在“栈”(stack)
中。栈中的每个变量在32位计算机上是4字节;在64位计算机上是8字节,其内存的管理由系统自动清理,而堆上的内存管理可以由编程人员自己管理也可以由"引用计数"
管理。
第 2 条:在类的头文件中尽量少引入其他头文件
- 当我们在某个类中需要使用到另外一个类的时候,我们尽量避免直接 #import 导入头文件,而是使用
“前象引用声明(forward declaring)”
,这样可以优化编译器的编辑时间,同时也可以避免循环引用头文件以及降低类之间的耦合。 - 如果遇到无法使用前向引用声明的时候,比如类的协议,这时可以考虑将部分将协议的声明移到“分类”或者一个新文件中去,然后引入“分类”文件或者新文件,这样可以避免引入整个类文件。
第 3 条:多用字面量语法,少用与之等价的方法
- 字面量使得代码看上起更加的简洁、易懂。
- 同样我们应该使用取下标的方式访问和修改数组与字典的元素。
第4条:多用类型常量,少用 #define 预处理命令
- 预处理指令定义常量不包含类型信息,并且是在编译前执行
操作和替换
操作,编译耗时,且容易出错。 - 使用
const
来定义常量,如果只在类中使用,则使用static
声明,并且在实现文件中定义,不要在头文件中定义。 - 常量的命名,参考系统命名,最好使用类名做前缀,比如:UIApplicationDidBecomeActiveNotification 等。
第 5 条:用枚举表示状态、选项、状态码。
- 如果枚举的值可以组合使用,则可以用移位操作定义为2的幂,以便通过位运算(位与、位或)进行组合。
- 如果使用 switch 语法判断枚举,则尽量不使用 default 操作,这样的话,如果加入新的枚举,编辑器就会提示。
-
NSEnum
和NS_Options
定义枚举类型可以指明底层的数据类型,确保不是编译器给出来的数据类型,它们2个本质上是一样的,只是字面上来讲,NS_Options
表示可位移操作的枚举。
第6条:理解“属性”这一概念
- 属性(@Property)声明之后,会自动生成一个带下划线的实例变量,以及根据设置的读写属性,生成对应的“存取方法(getter/setter)”。
- 使用属性的点语法获取属性时,会调用 getter 方法,所以如果我们在
对象内部
,尽量直接使用实例变量。 - @synthesize 关键字可以指明,属性所对应的
实例变量
名称;@dynamic 关键字告诉编译器,不要自动生成 getter/setter 方法。 - 属性声明的
copy
语义,在 setter 方法中,会拷贝属性值,这样做可以避免,属性值在其它地方被修改。同时需要注意,如果是可变类型的属性(mutable),比如 @property(nonatomic, copy) NSMutableArray *array;按这种方式声明,使用 copy 之后,属性会变成一个不可变类型,所以最好重写 setter 方法,使用 mutableCopy 进行设置。 -
nonatomic
和atomic
声明属性是否具备“原子性”,一般我们在 iOS 程序上使用 nonatomic。因为 atomic 严重影响性能,而且还不能完全保证原子性。因为其原理只是在 setter 方法时加上同步锁,但是如果我们在一个线程中连续请求这个属性,另外一个线程修改这个属性,那么还是会出现获取的值不是修改的值,所以还是不能保证“线程安全”,如果需要实现线程安全,还需要进一步写代码;如果在 Mac OS X 上使用 atomic 则就没有性能瓶颈。 -
weak
和unsafe_unretained
这2个关键字都表示不会 retain 增加自动引用计数。但是区别就是,当对象被销毁时,weak 会自动指向 nil,而 unsafe_unretained 则还是会指向之前的内存地址,所以很容易变成野指针。
第7条:在对象内部尽量直接访问实例变量
- 不经过 getter / setter 方法,访问速度更快。
- 直接访问实例变量,不会触发 KVO。(参考:https://www.jianshu.com/p/83b3ee289152)
- 直接改变实例变量的值,不会按照属性设置的 weak/strong/copy 关键字进行设置,所以这个要根据具体情况进行判断使用哪种方式。
- 推荐写法:对象内部读取数据的时候,使用实例变量;而写入数据的时候,则通过属性来写。
- 在初始化方法和 delloc 方法中,总是应该用实例变量来存取数据。
- 在使用了懒加载的方式来初始化时,则需要通过属性来读取数据。
第8条:理解“对象同等性”这一概念
-
"=="
只能判断值类型
数据的值是否相等。如果用于判断引用类型
,则只能判断2个引用类型的指针所指的内存地址是否一样。 - NSString 的
isEqualToSting
比isEqual
方法更快,因为” isEqualToSting“直接比较字符串相等,而 "isEqual" 方法还要判断其它,如下:
- (BOOL)isEqual:(id)object {
if(self == object) {
// 首先判断 指针地址是否相等
return YES;
}
// 再判断 object 是不是 NSString 类型
if(![object isKindOfClass:[NSString class]]) {
return NO;
}
// 最后再调用 isEqualToString:
return [self isEqualToString: object];
}
- 如果是其它的 NSObject 类,”isEqual“ 类默认只判断了
”指针值“(指针指向的地址)
是否相等,所以如果要判断2个对象是否相等,就需要我们自己去重写 "isEqual"
方法。 - 如果使用集合类 (NSSet) 时,当把一个对象加入集合时,集合类会首先判断对象的
hash
值是否一致,如果一致,会调用"isEqual"
方法判断对象是否相等,如果相等则不会再加入相同的对象。 - 所以为了在使用集合类时,能够减少碰撞的次数。所以我们在重写对象的
- (NSUInteger)hash
方法时,尽量避免返回相同的值,推荐使用所有属性的 hash 值进行^ 异或
处理。 - 重写
- (NSUInteger)hash
方法时,尽量不要使用对象的可变对象去计算 hash 值。因为如果对象的属性可变,那么当属性改变之后,hash 值也会改变,这样就会影响其在集合类的后续操作。 - NSArray 等同性判定策略:首先判断数组的个数是否相等;然后在数组中每个对象再执行 isEqual,这也叫做“深度等同性判断”。
第9章:以“类族模式”隐藏实现细节
- 类族的应用:当我们有很多相识的子类时,我们可以将很多子类合并起来,并提取出一个抽象类(本质上讲 OC 没有抽象类)。然后所有子类我们都继承抽象类,作为开发我们只需要记住抽象类即可,不需要关心所有其它子类的具体实现(类族的核心就是通过公共接口隐藏子类的具体实现),只需要通过抽象类的方法生成对应的子类。(系统框架经常使用类族比如:UIButton、NSArray、NSNumber)。
- 类族的实现:可以通过“工厂模式”实现类族。
- 理解 isKindOfClass 和 isMemberOfClass 的用法。
第10章:在即有类中使用关联对象存放自定义数据
- objc_setAssociatedObject(id objct,void * key, id value, objc_associationPolicy 关联类型 )
OBJC_ASSOCIATION_ASSGIN:asign
OBJC_ASSOCIATION_RETAIN_NONATOMIC:strong, nonatomic
OBJC_ASSOCIATION_COPY_NONATOMIC:copy, nonatomic
OBJC_ASSOCIATION_RETAIN:strong,atomic
OBJC_ASSOCIATION_COPY:copy, atomic
- objc_getAssociatedObjects(id objct, void *key) 获取某个对象的关联对象
- 所以关联对象也是传值的一种方式,但是尽量少的使用关联对象,只有在其它做法不可用的时候再使用。
第11条:理解objc_msgSend 的作用
-
void objc)magSend(id self, SEL cmd, ...)
: 根据函数体就可以得知,为什么我们在某个类里面,可以使用 self 获取到本类的实例。
第12条:理解消息转发机制
- 如果某个对象以及其父类都无法响应某个消息(没有实现方法),此时就会进入消息转发机制。
- 消息转发的流程:
(1)对象或则类如果在收到无法解读的消息后
如果是对象会调用:
+ (BOOL)resolveInstanceMethod:(SEL)sel {}
如果是类方法则会调用:
+ (BOOL)resolveClassMethod:(SEL)sel {}
此时我们就可以在这个方法中,
使用 class_addMethod() 动态添加方法到对象或者类中。
(2)如果在(1)中没有发现有动态方法添加其中,则会进入“备援接受者”阶段,即看能不能转给其它接受者来处理:
在 - (id)forwardingTargetForSelector:(SEL)aSelector {} 方法中
返回一个对象,然后交由该对象执行该消息。
(3)如果再第(2)不也还是没有找到接受者,则消息派发系统
通过 NSInvocation 对象进入完成的消息转发阶段:
// 进入该阶段之后,我们首先需要获取方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if(aSelector == @selector(count)) {
// Type Encodings:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1
return [NSMethodSignature signatureWithObjCTypes:"i@:"];
// 也可以自动获取 Type Encodings
// Method method = class_getInstanceMethod(Model.class, aSelector);
// const char *des = method_getTypeEncoding(method);
}
return nil;
}
// 然后通过 NSInvocation 转发给其它对象去实现(该阶段和第(2)阶段的区别就是我们可以转发给多个对象去实现)
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if(anInvocation.selector == @selector(count)) {
[anInvocation invokeWithTarget:[Model1 new]];
[anInvocation invokeWithTarget:[Model2 new]];
// 虽然此处是分发给多个对象执行的,但是返回值是最后一个 invokeWithTarget 的对象对应方法的返回值。
}
}
(4)如果在第(3)步还找不到消息接受者,则直接调用doesNotRecognizeSelector
抛出异常。
第13条:用“方法调配技术”调试“黑盒方法”
- 方法调配(Method Swizzling):指在运行期间,通过运行时方法改变方法的执行(method_exchangeImplementations(Method m1,Method m2)),而不用去覆写原方法。
- 应用:可以实现对黑盒方法新增日志功能。
- 不宜滥用该方式。
第14条:理解类对象的用意
- 每个对象都有一个指向 Class 对象的指针,用以表明其类型(isa)
- 尽量使用“类型信息查询方法”来确定对象类型,再做后续操作。
第15条:用前缀避免命名空间冲突
- 在项目中使用公司、项目名等作为类的前缀,通过这种方式避免“重名符号错误(duplicate symbol error)”
- Apple 宣称其保留使用所有“两字母前缀(two-letter prefix)”,所以我们尽量用3字母作为前缀。
第16条:提供“全能初始化方法”
- 在初始化对象时,对象需要完成一些必要工作(比如初始化一些必须的属性或者生成本地文件、创建数据库)。这时我们需要提供一个完成这些工作的初始化方法,这个方法就是“全能初始化方法”。(系统框架中很多这类方法,比如 UITableViewCell)。
- 为了保证对象能够正常工作,需要类的所有初始化方法都要调用这个“全能初始化”方法。这样做不但能够保证对象能够正常工作,而且也便于后期如果需要调整类的必要信息时,也只需要修改这个“全能初始化方法”。
- 如果子类的全能初始化方法和超类不同,则需要覆写超类的对应方法。
- 如果超类的全能初始化方法不适用于子类,那么超类方法时需要抛出异常。
第17条:实现 description 方法
- 我们在类中可以通过实现 description ,返回类的一些必要信息,这样在使用 NSLog 方法打印对象时,就可以看到这个对象的必要信息。
- debugDescriotion 的区别是在调试器中打印的信息。
第18条:尽量使用不可变对象
- 当我们确定某些属性是不应该被修改的时候,我们应该在公共接口中声明为 readonly:
@property (nonatomic, strong, readonly) NSString *name;
- 如果我们想在本类中修改这个属性,有以下2种方式。
- 直接访问实例变量:_name
- 在类的扩展(Extension 匿名分类)中,重新声明属性为可写。
@interface Model ()
@property (nonatomic, strong, readwrite) NSString *name;
@end
- 不要把可变的 colletion (NSMutableArray、NSMutableSet、NSMutableDictionary)作为属性,而是应该提供对象的添加、删除方法对其进行操作。
- 可以降低代码的耦合度
- 保证数据的一致性(比如在绑定 TableView 时,同时又再操作数据,如果用可变对象就会出现数据不一致的情况)
第19条:使用清晰而协调的命名方式
- 起名时应遵从标准的 OC 命名规范,这样让其它开发者更加好理解
- 不要使用缩略后的类型名称
- 文件、类名称采用大驼峰命名,方法名和属性名采用小驼峰命名
第20条:为私有方法名加前缀
- 给私有方法加上前缀,用于区别公共方法:
- (void)p_privateMethod {
}
- 根据苹果官方文档的说明,由于用一个单一的下划线作为前缀是苹果常用的方式,所以我们尽量避免使用这种方法。
第21条:理解 Objective-C 的错误模型
- 只有发生可使整个应用程序崩溃的严重错误时,才应该使用异常(NSException)
- 如果错误不严重,可以使用NSError通过委托、输出参数返回给调用者。
第22条:理解 NSCopying 协议
- 如果想令类支持拷贝操作,则需要实现 NSCopying 协议,并实现以下方法:
- (id)copyWithZone:(NSZone *)zone {
}
- NSZone 是因为以前开发程序时,会讲内存分成不同的区(Zone),但是现在每个程序只有一个“默认区(default zone)”。
- 理解 mutableCopy:此方法来自 NSMutableCopying 协议,只是放回的是一个可变的对象。
- 深拷贝、浅拷贝、copy、mutableCopy 之间的联系:
- 首先 copy 不等于浅拷贝;mutbleCopy 不等于深拷贝
- 对于非容器类对象:
- 如果是不可变对象,使用 copy 则是浅拷贝,如果是使用 mutableCopy 则是深拷贝
- 如果是可变对象,则使用 copy 和 mutableCopy 都是深拷贝
- 对于容器类对象:
- 如果是不可变对象,使用 copy 是浅拷贝,使用 mutableCopy 时,开辟了新的内存存放数据,但是对于数组的里的元素还是浅拷贝。
- 如果是可变对象,copy和mutableCopy都会开辟新的内存存放数据,但是对于数组的里的元素还是浅拷贝。
第23条:通过委托与数据源协议进行对象通信
- 对委托模式进行优化:
# 常规情况:
if([delegate respondsToSelector: SEL]) {
// 判断委托对象是否执行了该方法:
...
}
缺点:这种方式,除了第一次检测的结果有用之外,后续的检测都是多余的。
# 优化:
通过 C 语音的位段方式将检测结果缓存起来。
(1) 设置位段
struct {
// 通过一个位段来表示 delegate 是否可以调用
unsigned int delegateVia : 1;
} flag;
(2) 在 setDelegate 里面判断位段值
- (void)setDelegate:(id)delegate {
_delegate = delegate;
_flag.delegateVia = [delegate respondsToSelector: SEL];
}
(3) 判断是否可以调用
if(_flag.delegateVia) {
[_delegate 调用方法];
}
第24条:将类的实现代码分散到便于管理的数个分类中
- 如果我们在写一个类的时候将所有代码都写在一个文件里面,将会变得难于管理。
- 可以通过分类,将所有的私有方法放在一起,将公开方法放在一起。
第25条:总是给第三方类的分类名称加前缀
- 为了避免和其它的第三方类的分类重名,我们需要在我们添加的分类方法里面添加前缀,这种方式在其它第三方库中也是很常见的,比如 mas_、sd_等
第26条:勿在分类中声明属性
- 除了“class-continuation 分类”(扩展/匿名分类)外,在其它分类中声明属性,都没有办法向类中新增实例变量,也无法合成出 getter 和 setter 方法。
- 如果非要在分类中实现属性,有以下3中方式:
1. 将属性声明为只读属性,同时实现其读取方法:
@property (nonatomic, strong, readonly) NSString *name;
- (NSString *)name {
return @"....";
}
如果是这种方式还是建议直接将属性变成一个方法,而不是直接声明为属性。
2. 将属性声明为 @dynamic 在运行期提供存取方法
static NSString *_name;
@dynamic name;
- (NSString *)name {
return _name;
}
- (void)setName:(NSString *)name {
_name = name;
}
3. 使用关联对象实现存取方法
- (NSString *)phone {
return objc_getAssociatedObject(self, "name");
}
- (void)setPhone:(NSString *)phone {
objc_setAssociatedObject(self, "name", phone, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- 总之,虽然可以通过上面的方式在分类中实现属性,但是我们在变成的时候尽量不要在分类中声明属性,而是在主文件中声明。
第27条:使用“class-continuation 分类”
- 在类中我们使用到的所有实例变量、属性、方法、协议,如果只是在本类中使用的话,都应该定义在匿名分类中,而不要定义在头文件中。
- 匿名分类还可以帮助类隐藏其具体的实现(让外部不知道内部使用的具体协议,属性,变量以及语言)。
第28条:通过协议提供匿名对象
- 当我们在定义属性、函数参数、函数返回值的时候,如果允许多个类型时,并且这些类型之间都有必须要实现的方法或者属性。这时我们就可以定义一个协议,声明这些必须实现的方法和属性,然后通过
id <protocolname>
这种方式来声明,这种方式就叫做匿名对象。
例如字典:
[dic setObject:<#(nonnull id)#> forKey:(nonnull id<NSCopying>)]
字典中由于 key 在底层会调用 copy 方法,所以在调用这个方法的时候,
就将 key 声明为了一个匿名对象,这个对象只要遵循 NSCopying 协议即可。
第29条:理解引用计数
- retain: 引用计数加1;
- release:引用计数减1;
- autorelease:稍后清理“自动释放池”的时候,再减少引用计数。
第30条:以 ARC 简化引用计数
- 注意: CoreFoundation 和 malloc 的内存,ARC 无法自动管理。
第31条:在 dealloc 方法中只释放引用并解除监听
- dealloc 方法中就只做移除引用计数(ARC 下不需要)和解除监听(NSNotification、KVO)
- 如果使用了开销较大和稀缺的资源,比如文件描述符、套接字(Socket)、大块内存等。则不应该在 dealloc 里面进行释放,而应该在使用介绍之后就移除掉。
- 不能在 dealloc 里面执行异步方法也不能执行只能在主线程里面执行的方法,因为此时对象已经处于正在回收状态并且很可能不在主线程中。
第32条:编写“异常安全代码”时,留意内存管理问题
- ARC 是不会在 @try 自动清理内存,所以如果要自动清理,需要将文件标志为 -fobjc-arc-exceptions
第33条:以弱引用避免保留环
- 注意 weak 和 unsafe_unreatained 使用
第34条:以“自动释放池块”降低内存峰值
- 根据需要,我们可以在程序中使用 @autoreleasepool ,释放某些占用内存比较高的操作,尤其是在使用 for 循环时使用很多临时对象时,就可以通过这种方式来降低内存的峰值。
第35条:用“僵尸对象”调试内存管理问题
- 调试时如果有时候向已回收的对象发送消息时,可能会出现崩溃的情况,这是因为如果已经回收的对象被其它对象复用了,这是就会崩溃。
- 为了能够调试这种崩溃,我们可以设置调试时 NSZombieEnable 为 YES。这样的话被回收的对象就不会被复用,就有助于我们调试应用程序。
- 僵尸对象的原理就是在对象在被回收时,通过运行时的特性,将该对象生成为一个 _NSZombie_ClassName开头再加上原类名的对象,同时也不执行 free 操作。所以当我们程序如果还在访问这个变成僵尸对象的时候,就会在控制到中打印出:[ClassName method]: message sent to deallocated instance 0x7ff93c080e0
第36条:不要使用 retainCount
- ARC 已弃用
第37条:理解 Block
-
Block 的结构
block 的结构.png
- Block 的分类
1. _NSConcreteStackBlock 保存在栈中的 block,我们创建 blcok 时默认就是在栈中。
但是在 ARC 下就不存在这种模式,但如果在 MRC 下如果我们创建好 block 之后,没有移到堆中,
很可能我们在使用的时候回崩溃(如下)。
- (NSArray *)blockArray {
int num = 123;
return @[^{ NSLog(@"this is block 0"); },
^{ NSLog(@"this is block 1:%i", num); },
^{ NSLog(@"this is block 2:%i", num); }];
}
- (void)test {
NSArray *array = [self blockArray];
void(^someblock)(void) = array.lastObject;
someblock();
}
如果我们在 MRC 调用 test() 方法时,就会崩溃。
因为栈是由系统管理的,所以在我们调用 block 时,已经被销毁了。
2. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。
(1)如果是在 ARC 下,如果 block 捕获了外部的局部变量则也会保存在堆中;
(2)在 MRC 和 ARC 下如果当在栈中的 block 执行了 `_Block_copy` 函数也会保存在堆中
以下方式会被拷贝:
a. 显示调用 copy 方法
b. 赋值给一个用 copy 修饰的 blcok 属性时:
关于这一点也就说明我们为什么在类里面声明一个 blcok 时要使用 @property (nonatomic, copy)。因为如果不使用 copy ,block 还保存在栈上的,这是很可能被系统回收了,但是在 ARC 下已经不存在,使用 @property (nonatomic, strong) 也是一样的效果。
c. 在ARC下,向函数或者方法传递Block时(MRC下需要手动copy)
d. 调用Coaca框架中方法名中含有usingBlock的方法时
e. 调用GCD的API时
3. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量
(1)在 MRC 和 ARC 下一个定义在外部的 block 就是一个全局的 block
(2)在 ARC 下如果是一个没有捕获任何外部变量的 block 也是一个全局 block
-
__block 关键字的使用:
(1)不使用关键字的时候,block 是复制外部的变量进行访问的
block-capture-1.jpg
(2)使用关键字的时候,block 只是复制其引用地址来进行访问的

- 参考链接:
http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
https://www.cnblogs.com/xitang/p/4039314.html
https://www.jianshu.com/p/997d144001f9
第38条:为常用的块类型创建 typedef
- 由于 block 定义语法看上去比较复杂,所以我们可以使用 typedef 来简化 block 的定义
typedef return_type (^block_name)(parameters);
- 使用 typedef 的另外一个优点是,当我们修改了 block 的定义时,那么所以使用了这个 block 的都会编译不过,便于我们更改。
第39条:用 handle 块降低代码的分散程度
- 与委托模式相比,使用 block 可以使代码更加的紧致。
第40条:使用block时,不要出现保留环
- 除了弱引用的方式解除循环应用;也可以在 block 中主动释放持有的对象,来解除循环引用。
书签:154页
网友评论