第1章 熟悉Objective—C
第1条:了解Objective—C语言的起源
| 1 | NSString *s1 = @"Hello"; | 
- s1 和s2 的内存分配在栈上
- @”Hello”的内存分配在堆上
- s1 和 s2 指向同一块内存
第2条:在类的头文件中尽量少引入其他头文件
第3条:多用字面量语法,少用与之等价的方法
使用字面量语法,它是一种语法糖:
| 1 | NSString *str = @"Hello"; | 
这个语法糖更容易暴露隐藏的问题
| 1 | NSArray *array1 = [NSArray arrayWithObjects: obj1, obj2, obj3, nil]; | 
如果 obj1 和 obj3 非空,而 obj2 是 nil
那么 array1 只有一个对象,不会出错;而 array2 在插入的时候会抛出异常
使用字面量语法创建的对象是不可变的,若想要创建一个可变的对象,需要复制一份:
| 1 | NSMuatableArray *mArray = [@[@1, @2] mutableCopy]; | 
第4条:多用类型常量,少用#define预处理指令
第5条:用枚举表示状态、选项、状态码
使用 NS_ENUM 和 NS_OPTIONS 来表示状态机,
| 1 | //NS_ENUM,定义状态等普通枚举 | 
如果一个枚举变量可以同时表示一个或多个选项的集合,那么应当使用 NS_OPTIONS,而且各个选项的值应定义为2的 N 次幂,如上代码,这样就可以用或操作将其组合起来进行表示
相比较 C 语言中的枚举,使用 NS_ENUM 和 NS_OPTIONS 的好处是,可以确保实现枚举值的数据类型是开发者所指定的,而不会默认采用编译器所选的类型
| 1 | typedef enum _TTGState { | 
处理枚举类型的 switch 分支中,不要实现 default 分支。这样的话,加入新的枚举值之后,编译器就会给出提示:switch 语句并未处理所有枚举
参考链接:Enum-枚举的正确使用-Effective-Objective-C-读书笔记-Item-5
第2章 对象、消息、运行期
第6条:理解“属性”这一概念
理解好属性和实例变量的区别
属性 = 实例变量 + setter + getter
如果声明属性
@property (nonatomic, copy) NSString *str;
则编译器会默认实现
| 1 | //.h | 
其中 _str 就是实例变量
使用点语法访问属性 = 调用 setter/getter 方法
Property 的4种 attribute
- 原子性(atomic, nonatomic)
- 读写权限(readonly, readwrite)
- 内存管理(strong, weak, unsafe_unretained, retain, assign, copy)
- 存取方法(getter, setter)
非 ARC 下,没有 weak
ARC下,修饰指针的内存修饰符
- __weak:不retain,如果对象被回收,该指针会被置nil
- __strong:默认,如果对象被回收,需要手动将指针置为nil?
- __unsafe__unretained:不retain,如果对象被回收,该指针不会被置nil(为了在ARC刚发布时兼容iOS 4以及版本,现可废弃)
- __autoreleasing:实现把对象”按引用传递”给方法,变量在方法返回时自动释放
编译器在为一个 property 合成实例变量的时候,也会使用相应的修饰符来修饰这个实例变量
常见数据类型的内存修饰符(待补充)
| 数据类型 | 内存修饰符 | 
|---|---|
| 基本数据类型(int, NSInteger) | assign | 
| block | copy | 
| NSString | copy | 
| NSMutableString | strong | 
| NSArray | copy | 
| NSMutableArray | strong | 
NSArray 用 strong 还是 copy 修饰
| 1 | //.h | 
可以看到使用 strong 修饰 NSArray 非常不安全,数组元素被外部修改了。原因是执行其 setter 操作的时候,假如将一个可变数组赋值给 NSArray,那么 NSArray 的指针会直接指向一个可变对象,然后就可以通过这个可变对象来修改 NSArray。而使用 copy 就不会有这个问题。所以 NSArray 建议使用 copy 修饰,而 NSMutableArray 没有这个问题,可以用 strong 修饰。
第7条:在对象内部尽量直接访问实例变量
类内使用 self.xxx 和 _xxx 的区别
- 访问 _xxx 不经过 setter/getter 方法,速度更快
- 访问 _xxx 不经过 setter 方法,绕过了 property 定义的内存管理逻辑。比如 ARC 下直接访问一个声明为 copy 的属性的实例变量,那赋值过程中,并没有 copy 操作
- 访问 _xxx 不经过 setter/getter 方法,无法触发 KVO
- 访问 _xxx 不经过 setter/getter 方法,无法断点
什么时候使用 _xxx
- 折中方案,读的时候使用 _xxx,写的时候使用 self.xxx
- 父类的 init 和 dealloc 尽量使用 _xxx 来访问,因为如果子类覆盖了 setter 方法并做了某些非空检查,那么父类初始化的时候会调用子类的 setter 方法,由于是在 init/dealloc,参数可能都是空的,此时报错
- 如果实例变量在父类中声明,那么子类只能使用 self.xxx 来访问属性
- 使用 lazy initialization 的情况下,必须通过 self.xxx 来访问属性,否则初始化失败1 
 2
 3
 4
 5
 6- (NSString *)str 
 {
 if (!_str)
 _str = [[NSString alloc] init];
 return _str;
 }
第8条:理解“对象等同性”这一概念
第9条:以“类族模式”隐藏实现细节
第10条:在既有类中使用关联对象存放自定义数据
“关联对象”(Associated Object)是用来为对象关联其他对象的,比如不定义子类的前提下为 UIAlertView 添加一个 Block 属性;比如为一些无法更改其属性(比如工作中的协议文件)的类添加属性
语法
| 1 | void objc_setAssociatedObject (id object, void *key, id value, objc_AssociationPolicy policy); | 
其中 objc_AssociationPolicy 是关联对象的属性,如下
| 1 | OBJC_ASSOCIATION_ASSIGN --- assign | 
与 NSDictionary 的区别
设置关联对象值时,若想令两个健匹配到相同的一个值,则二者必须是完全相同的指针才行。
所以 key 值(一般为 NSString)最好定义为一个全局静态变量,而不能每次都用 @”xxx”
例子1
假如一个页面有2个弹窗,那么代码可能是这样写
| 1 | - (void)askUserAQuestion | 
缺点是alertView的处理逻辑和初始化逻辑分离,不易阅读。有一种解决方法是为 UIAlertView 添加一个 block 属性
| 1 | 
 | 
优点就是处理逻辑和初始化逻辑不再分离,但是使用 block 一不小心可能会引起保留环。一种更好的方法是弄个子类,比如 SIAlertView
例子2 为协议文件添加属性
.h
| 1 | 
 | 
.m
| 1 | 
 | 
第11条:理解objc_msgSend的作用
第12条:理解消息转发机制
第13条:用“方法调配技术”调试“黑盒方法”
创建自己的方法
| 1 | 
 | 
替换
| 1 | Method originalMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString)); | 
第14条:理解“类对象”的用意
我们所说的 Objective-C 对象究竟是什么
| 1 | typedef struct objc_object{ | 
结论:Objective-C 对象 = id = objc_object
那么 Class 是什么
| 1 | typedef struct objc_class *Class; | 
那么 objc_class 又是什么
| 1 | struct objc_class { | 
类的继承体系

| 1 | NSString *str = @"Hello"; | 
str 是一个对象,is a NSString
NSString 是类,is a NSString metaclass
NSString metaclass 是元类,类方法就定义在这里
第3章 接口与API设计
done 第15条:用前缀避免命名空间冲突
done 第16条:提供“全能初始化方法”
done 第17条:实现description方法
done 第18条:尽量使用不可变对象
- 如果某个属性只是内部可修改,则在 .h 中应该声明为 readonly,然后再在扩展里面声明为 readwrite
- 不要把可变的 Collection 对象(NSMutableSet/NSMutableDictionary/NSMutableArray 等)作为属性公开,应该提供 readonly 版本以及读写方法
done 第19条:使用清晰而协调的命名方式:
如果一个方法返回了某个变量,该方法命名不要使用 getXXX,直接使用 XXX 就行了
对于 BOOL 类型,可以在属性声明的时候,指定其 getter 为 isXXX
@property (nonatomic, assign, getter = isOn) on;
done 第20条:为私有方法名加前缀
done 第21条:理解Objective—C错误模型
done 第22条:理解NSCopying协议
第4章 协议与分类
第23条:通过委托与数据源协议进行对象间通信
| 1 | if([_delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)]) | 
如果上面的代码写了很多次,则可以考虑以下优化:
| 1 | // 在扩展中定义结构体 | 
done 第24条:将类的实现代码分散到便于管理的数个分类之中
第25条:总是为第三方类的分类名称加前缀
- 为第三方类添加分类时,总应给其名称加上你专用的前缀
- 为第三方类添加分类时,总应给方法名加上你专用的前缀
| 1 | @interface NSString (ABC_HTTP) | 
第26条:勿在分类中声明属性
属性应该在主类中声明
如果分类中声明属性需要自己重写 setter 和 getter
方法如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static const char *kFriendsPropertyKey = "kFriendsPropertyKey";
@implementation Person(Friendship)
@dynamic friends;
-(NSArray*)friends 
{
  return objc_getAssociatedObject(self, kFriendsPropertyKey);
}
-(void)setFriends:(NSArray*)friends 
{
  objc_setAssociaedObject(self, kFriendsPropertyKey, friends, OBJC_ASSOCIATION_NONATOMIC);
}
@end
缺点如下
- 相似的代码要写很多遍
- 极易忽略属性定义的内存管理语义,且不好维护
第27条:使用“class—continuation分类”隐藏实现细节
声明私有实例变量的3种方法
- 方法1:对外暴露,声明为 private(暴露了细节,不建议) - .h - 1 
 2
 3
 4
 5
 6- @interface ABC:NSObject 
 {
 @private
 XYZ *_xyz;
 }
 @end- 把私有变量放在头文件,暴露了细节,不好
- 假如该实例变量是 objective-c++ 类,则所有引入该头文件的类都要编译为 objective-c++,即使使用 @class 也无法解决这个问题
- 所以既然是私有变量,干嘛不放在 .m 中,偏偏要放到 .h 中作死呢?
 
- 方法2:不对外暴露 - .m - 1 
 2
 3
 4
 5
 6- @interface ABC() 
 {
 XYZ *_xyz;
 }
 @property (nonatomic, strong) XYZ *xyz2;
 @end
- 方法3:对外只读,对内读写 - .h - 1 - @property (nonatomic, readonly) XYZ *xyz; - .m - 1 
 2
 3- @interface ABC() 
 @property (nonatomic, readwrite) XYZ *xyz;
 @end
done 第28条:通过协议提供匿名对象
第5章 内存管理
第29条:理解引用计数
悬浮指针
| 1 | NSNumber *number = [[NSNumber alloc] initWithInt:1]; | 
autorelease
| 1 | - (NSString *)stringValue | 
这种情况下,str 如果在方法内部 release,则调用者得到的一定是一个空对象;所以只能由调用者来负责释放
但是,这是十分不合理的,因为从方法名上看(不含alloc/new/copy/mutableCopy),调用者并不知道它需要负责释放该对象
所以此时,autorelease 就应运而生了
| 1 | - (NSString *)stringValue | 
str 对象会在其所在的释放池释放的时候被释放
如果外部需要 retain 该返回值,则需要这样做
| 1 | NSString *str = [[self stringValue] retain]; | 
autorelease 能延长对象生命周期,使对象在方法结束后依然存活一段时间
第30条:以ARC简化引用计数
ARC 的本质是自动添加 release/retian/autorelease 等
| 1 | + (XYZ *)newXYZ | 
扩展阅读:iOS开发ARC内存管理技术要点
done 第31条:在dealloc方法中只释放引用并解除监听
done 第32条:编写“异常安全代码”时留意内存管理问题
| 1 | @try { | 
假如在执行 doSomethingThatMayThrow 方法中抛出异常,则 release 方法不会执行,会发生内存泄漏
解决方法:
| 1 | EOCSomeClass *object; | 
同理,ARC 下也会发生这个问题
| 1 | @try { | 
可通过打开 -fobjc-arc-exceptions 标记来解决这个问题,不过这个标记会带来性能问题
总结:
- 当捕获到异常,应该注意确保@try中创建的对象被清理完成.
- ARC在默认情况下不会清理抛出异常时的代码,但是可以通过打开一个编译器标记来完成.不过会产生大量的代码和运行时的成本.
done 第33条:以弱引用避免保留环
done 第34条:以“自动释放池块”降低内存峰值
ARC下,可以使用 @autoreleasepool 来降低内存峰值
| 1 | for (int i = 0; i < 9999; ++i) | 
a 是临时对象,handle 方法中也可能创建一些临时对象,ARC 下,这些临时对象可能没有及时 release 而是放到自动释放池里,那么此时使用 @autoreleasepool 就能及时回收这些临时对象,从而降低内存峰值
使用 enumerateObjectsUsingBlock 时,内部会自动添加一个 AutoreleasePool,而普通for循环和for in循环中没有1
2
3[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部@autoreleasepool包围着
}];
另外,@autoreleasepool 跟是否 ARC 无关,MRC 下也可以使用
另外,关于降低内存峰值的之前也有学习过,见Objective-C 内存管理
done 第35条:用“僵尸对象”调试内存管理问题
僵尸对象是调试内存管理问题的最佳方式
被回收对象的内存可能会被系统回收,也可能不会,这样调试起来就很困难,此时可以使用僵尸对象来调试。
打开僵尸对象的方法:
Xcode -> Run -> Diagnostics -> Enable Zombie Objects
僵尸对象的原理:
替换 dealloc 方法,创建一个僵尸对象替换回收对象,从而达到不释放回收对象的内存
原理代码:
| 1 | // Obtain the class of the object being deallocated | 
done 第36条:不要使用retainCount
第6章 块与大中枢派发
done 第37条:理解“块”这一概念
在Objective-C语言中,一共有3种类型的block:
_NSConcreteGlobalBlock 全局的静态block,不会访问任何外部变量。
_NSConcreteStackBlock 保存在栈中的block,当函数返回时会被销毁。
_NSConcreteMallocBlock 保存在堆中的block,当引用计数为0时会被销毁。
- 全局 Block:_NSConcreteGlobalBlock - 定义在函数外面的 block 是全局静态的,没有访问任何外部变量
- 定义在函数内部的 block,但是没有捕获任何自动变量,那么它也是全局的 - 问题:那么定义在函数外部的,捕获变量的,是 global 吗? - 1 
 2
 3
 4- void f() 
 {
 ^{ printf("Hello, World!\n"); } ();
 }
 
- 栈 Block:_NSConcreteStackBlock - 1 
 2
 3
 4
 5- void f() 
 {
 char a = 'A';
 ^{ printf("%c\n",a); } ();
 }
- 堆 Block:_NSConcreteMallocBlock - NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,当一个栈 block 被 copy 的时候,才会将这个 block 复制到堆中 - 1 
 2
 3
 4
 5- void f() 
 {
 char a = 'A';
 void (^block)() = [^{ printf("%c\n",a); } copy];
 }- 对全局 Block 进行 copy 后,什么事也不会发生 对栈 Block 进行 copy 后,会得到一个堆 Block 对堆 Block 进行 copy 后,其引用计数会加1
- 例子 - 1 
 2
 3
 4
 5
 6
 7
 8- void (^blcok)(); 
 if (1)
 {
 block = ^{
 NSLog(@"Hello");
 }
 }
 block();- block执行时,其内存可能已经被释放,因为它是一个栈 block,if 体结束时可能会被释放 - 正确做法是 - 1 
 2
 3
 4
 5
 6
 7
 8- void (^blcok)(); 
 if (1)
 {
 block = [^{
 NSLog(@"Hello");
 } copy];
 }
 block();
done 第38条:为常用的块类型创建typedef
done 第39条:用handler块降低代码分散程度
第40条:用块引用其所属对象时不要出现保留环
例子1
| 1 | // EOCNetworkFetcher.h | 
| 1 | // EOCNetworkFetcher.m | 
| 1 | @implementation EOCClass | 
EoCClass -> networkFetcher -> block -> self(通过_fetchedData)
例子2
将 networkFetcher 变为局部变量,修改如下:
| 1 | - (void)downloadData { | 
networkFetcher -> block -> networkFetcher(通过url)
第41条:多用派发队列,少用同步锁
第42条:多用GCD,少用performSelector系列方法
如何延迟执行一个方法
| 1 | // 方法1:使用 performSelector | 
使用 dispatch_after 比使用 performSelector 更好,因为 performSelector 可能引起内存问题
当然,如果需要取消定时任务,则只能使用 performSelector,dispatch_after 无法取消
如何让一个方法在主线程执行
| 1 | // 方法1:使用 performSelector | 
使用 dispatch_after 比使用 performSelector 更好,因为 performSelector 可能引起内存问题
第43条:掌握GCD及操作队列的使用时机
要知道有个东西叫做 NSOperationQueue 就行了
第44条:通过Dispatch Group机制,根据系统资源状况来执行任务
第45条:使用dispatch_once来执行只需运行一次的线程安全代码
以后只要遇到“只需要执行一次的(线程安全)代码”,就应该想到 dispatch_once
比如单例的书写方式1
2
3
4
5
6
7
8
9+ (instancetype)sharedInstance
{
    static id sharedInstance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstance = [[self alloc] init];
    });
    return sharedInstance;
}
第46条:不要使用dispatch_get_current_queue
尽量别用,该接口已废弃
第7章 系统框架
第47条:熟悉系统框架
- CFNetWork:网络接口,Foundation 框架将其部分内容封装为 Objective-C 接口(C语言)
- CoreAudio:音频处理 API(C语言)
- AVFoundation:视频处理接口(Objective-C)
- CoreData:数据库接口(Objective-C)
- CoreText:文字渲染排版接口(C语言)
done 第48条:多用块枚举,少用for循环
第49条:对自定义其内存管理语义的collection使用无缝桥接
使用无缝桥接技术,转换 Foundation 框架的 Objective-C 对象和 CoreFoundation 框架的 C 语言数据结构
| 1 | NSArray *array = @[@1, @2, @3]; | 
- NSArray 在 CoreFoundation 框架对应的数据结构是 CFArray,但是只能通过 CFArrayRef 指针来操纵 CFArray
- CFArrayGetCount 是 CoreFoundation 框架里获取数组大小的函数
桥式转换
- __bridge:只做类型转换,不修改对象(内存)管理权;
- __bridge_retained:将 Objective-C 的对象转换为 CoreFoundation 的对象,同时 ARC 交出对象(内存)的管理权,后续需要使用 CFRelease 或者相关方法来释放对象;
- __bridge_transfer:将 CoreFoundation 的对象转换为Objective-C的对象,同时将对象(内存)的管理权交给 ARC
| 1 | NSArray *array = @[@1, @2, @3]; | 
| 1 | NSArray *array = @[@1, @2, @3]; | 
使用无缝桥接修改 Collection 的内存管理语义
NSMutableDictionary 加入键值对的时候,字典会自动“拷贝”键并“保留”值,如果键的对象不支持拷贝操作(没有实现 NSCopying 协议)呢?就会出现 Runtime Error
关于拷贝协议可以查看:浅析Objective-C的copy
无缝桥接可以从 CoreFoundation 层创建一个不拷贝键的字典
创建函数
| 1 | CFMutableDictionaryRef CFDictionaryCreateMutable( | 
键值回调
| 1 | typedef struct { | 
创建“保留”键,“保留”值的 NSDictionary
| 1 | const void* EOCRetainCallback (CFAllocatorRef allocator , const void *value) | 
第50条:构建缓存时选用NSCache而非NSDictionary
- 实现缓存时应选用 NSCache 而非 NSDictionary
- 可以给 NSCache 设置缓存数量上限 countLimit 或缓存总和 totalCostLimit(单位 bytes),超过限制的时候系统会自动剔除部分缓存数据
- NSCache 收到系统低内存警告的时候会被系统自动删除,且是线程安全的(多线程环境下不需要对 NSCache 加锁)
- NSCache 不会像 NSDictionary 那样,拷贝对象(只会 retain,不会新建一个)
- 使用 NSPurgeableData 作为 NSCache 的缓存时,系统收到低内存警告的时候,NSPurgeableData 对象所在内存会被系统释放,此时 NSCache 也会将其自动移除
扩展阅读:利用NSCache提升效率
 
        