《Effective Objective-C》重读校验自己的知识
读后感先放在前边
现在详细来看这本书应该也不晚吧,iOS 开发之类的书籍其实网上的总结还是蛮多的 有很多文章写得都是挺不错的, 但是终归是别人的的读后感总结,看着别人的总结终归不能完全吸收为自己的,所以近期抽空把 iOS 相关书籍看一遍 来对自己的知识体系做一个校验
书中举得很多例子都是挺好的 此文章也总结了此书的大纲,只有一些本人比较生疏的知识点才会展开详细描述,书中有很多细节并不是我们日常开发中能注意到的但是很重要的一些知识点, 此篇文章写得耗费时间还是挺久的
第一章 熟悉 Objective-C
1 了解 Objective-C 语言的起源
OC 语言使用了"消息结构" 而非是"函数调用"
消息结构与函数调用区别关键在于:
一 使用消息结构的语言,其运行时所应执行的代码有运行环境来决定
二 使用函数调用的语言,则有编译器决定的
OC 语言使用动态绑定的消息结构,也就是说在在运行时才会检查对象类型,接受一条消息之后,究竟应执行何种代码, 有运行期环境而非编译器来决定
下图来看一下 OC 对象的内存分配
WechatIMG86.jpeg
此图布局演示了一个分配在对堆中的 NSString 实例, 有两个分配在栈上的指针指向改实例
OC 系统框架中也有很多使用结构体的, 比如 CGRect, 因为如果改用 OC 对象来做的话, 性能就会受影响
2 在类的头文件中尽量少引用其他头文件
- 我们如非必要, 就不要引入头文件, 一般来说, 应在某个类的头文件中使用向前声明(向前声明的意思就是用
@Class Person
来表明 Person 是一个类)来提及别的类, 并在实现文件中引入那些类的 头文件, 这样做尽量降低类之间 的耦合 - 有时无法使用向前声明,比如要声明某个类遵循一项协议,这种情况下,尽量吧"该类遵循某协议"的这条声明一直"Class-Continuation 分类"中,如果不行的话, 就把协议单独放在一个头文件中.然后将其引入
3 多用字面量语法 少用与之等价的方法
推荐使用字面量语法:
NSString * someString = @"奥卡姆剃须刀";
NSNumber *number = @18;
NSArray *arr = @[@"123",@"456];
NSDictionary *dict = @{
@"key":@"value"
};
对应的非字面量语法
NSString *str = [NSString stringWithString:@"奥卡姆"];
NSNumber *number = [NSNumber numberWithInt:18];
NSArray *arr = [NSArray arrayWithObject:@"123",@"456"];
4 多用类型常量,少用 #define 预处理指令
-
不要使用预处理指令定义常量, 这样定义出来的常量不含类型,编译器只会在编译前据此执行查找与替换操作, 即使有人重新定义了常量值, 编译器也不会产生警告信息, 这将导致应用程序中的常量值不一致
-
在实现文件中使用 static const 来定义"只在编译单元内可见的常量",由于此类常量不在全局符号表中, 所以无须为其名称加前缀
举例说明
不合适的写法
//动画时间
#define ANIMATION_DUATION 0.3
正确的写法
视图修改 const修饰的变量则会报错
static const NSTimeInterval KAnimationDuration = 0.3
- 在头文件中使用 extern 来声明全局变量,并在相关实现文件中定义其值.这种常量要出现在全局符号表中, 所以其名称应该加以区隔,通常用与之相关的类名做前缀.
// EOCAnimatedView.h
extern const NSTiemInterval EOCAnimatedViewANmationDuration
// EOCAnimatedView.m
const NSTiemInterval EOCAnimatedViewANmationDuration = 0.3
这样定义的常量要优于# Define 预处理指令, 因为编译器会确保常量不变, 而且外部也可以使用
5 用枚举表示状态,选项, 状态码
- 如果把传递给某个方法的选项表示为枚举类型,而对个选项又可以同事使用, 那么就将各选项值定义为2的幂, 以便通过按位或操作器组合起来
- 在处理枚举类型的 switch 语句中不要实现 default 分支, 这样的话, 加入新枚举之后,编译器就会提示开发者, switch 语句并未处理所有枚举
按位或操作符枚举
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
第二章 对象,消息,运行期
6 理解"属性"这一概念
- 可以用@ property 语法来定义对象中所封装的数据
- 通过"特性"来指定存储数据所需的正确语义
- 在设置属性所对应的实例变量时, 一定要遵守该遵守该属性所声明的语义
- 开发 IOS 程序是应该使用 nonatomic 属性,因为 atomic 属性会严重影响性能
7 在对象内部尽量直接访问实例变量
- 在对象内部读取数据是, 应该直接通过实例变量来读,而写入数据是,则应该通过属性来写
- 在初始化方法及 dealloc 中,总是应该直接通过实例变量来读写数据
- 有时会使用惰性初始化技术(高大上的说法,其实就是懒加载)配置某份数据,这种情况下,需要通过属性来读取数据
8 理解"对象等同性"这一概念
- 若想检测对象的等同性. 请提供
isEqual
与hash
方法 - 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
- 不要盲目的逐个检测每条属性,而是应该依照具体需求来制定检测方案
- 编写hash 方法是,应该使用计算速度快而且哈希码碰撞几率低的算法
9 以类族模式隐藏实现细节
- 类族模式可以吧实现细节隐藏在一套简单的公共接口后面,
- 系统框架中经常使用类族
- 从类族的公共抽象基类中继承自雷是要当心,若有开发文档,则应实现阅读
此小节比较抽象,用文中的规则来总结一下 大概如下
- 1 子类应该继承自类族中的抽象基类
若想编写 NSArray 类族的子类,则需令其继承自不可变数组的基类或可变数组的基类 - 2 子类应该定义自己的数据存储方式
开发者编写 NSArray 子类时, 经常在这个问题上受阻, 子类必须用一个实例变量来存放数组中的对象, 这似乎与大家预想的不同, 我们以为 NSArray 自己肯定会保存那些对象,所以子类中就无需在存一份了, 但是大家要记住, NSArray 本身只不过是包在其他隐藏对象外面的壳, 他仅仅定义了所有数组都需具备的一些接口,对于这个自定义的数组子类来说, 可以用 NSArray 来保存其实例 - 3 子类应该复写超类文档中指明需要复写的方法
在每个抽象基类中, 都有一些子类必须腹泻的方法, 比如说,想要编写 NSArray 的子类, 就需要实现 count 及 objectAtIndex 方法,像 lastObject 这种方法则无需事先,因为基类可以根据前两个方法实现出这个方法
10 在既有类中使用关联对象存放自定义数据
- 可以通过"关联对象" 机制来吧两个对象连起来
- 定义关联对象时,可指定内存管理语义,用以模仿定义属性时所采用的"拥有关系"和"非拥有关系"
- 只有在其他做法不可行是,才应选用关联对象,因为这种做法通常会引入难于查找的 bug
这种方法我在分类中经常使用,而且屡试不爽 以下是本人在项目中的用法
static void *callBackKey = "callBackKey";
@implementation UIView (category)
- (void)addTapWithBlock:(callBack)callback{
objc_setAssociatedObject(self, callBackKey, callback, OBJC_ASSOCIATION_COPY);
self.userInteractionEnabled = YES;
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(tapClick)];
[self addGestureRecognizer:tap];
}
- (void)tapClick{
callBack block = objc_getAssociatedObject(self, callBackKey);
if (block) {
block();
}
}
11 理解 objc_msgSend 的作用
objc_msgSend 函数会依据接受者与选择子的类型来调用适当的方法,为了完成此操作, 该方法需要在接受者所属的类中搜寻其"方法列表" ,如果能找到与选择名称相符的方法,就跳至其实现代码, 若是找不到 就沿着继承体系继续向上查找, 等找到合适的方法在挑战, 如果还是找不到相符的方法,那就执行"消息转发"操作 这个会在12条来讲
- 消息有接受者,选择子及参数构成, 给某对象"发送消息"也就相当于在该对象上"调用方法"
- 发给某对象的全部消息都要有"动态消息派发系统"来处理, 该系统会查出对应的方法,并执行其代码
12 理解 消息转发机制 重点再次复习一遍
消息转发分为两大阶段,第一阶段先征询接受者,所属的类, 看其是否能动态添加方法,以处理当前这个"未知的的选择子" 这叫做"动态方法解析",第二阶段涉及完整的消息转发机制. 如果运行期系统已经把第一阶段执行完了, 那么接受者自己就无法再以动态新增方法的手段来响应包含盖选择子的消息了, 此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用, 这又细分两小步. 首先请接受者看看有没有其他对象能处理这条消息,若有 则运行期系统会吧消息转给那个对象,于是消息转发过程结束,一切如常, 若没有背援的接受者,则启动完整的消息转发机制,运行期系统会吧消息有关的全部细节都封装在 NSInvocation 对象中, 在给接受者最后一次机会, 令其设法解决当前还没处理的这条消息
动态方法解析
+ (Bool) resolveInstanceMethod:(SEL)selector
该方法的参数就是那个未知的选择子,其返回值为 Boolean 类型,表示这个类是否能新增一个实例方法用以处理此选择子.在继续往下执行转发机制之前, 本类有机会新增一个处理此选择子的方法,假如尚未实现的方法不是实例方法而是类方法, 那么运行期系统就会调用另外一个方法 和当前方法类似 resolveClassMethod
备援接受者
当前接受者还有第二次机会能处理未知的选择子,在这一步,运行期系统会问它: 能不能把这条消息转给其他接受者来处理. 与该步骤对应的处理方法如下:
- (id)forwardingTargetForSelestor:(SEL)selector
方法参数代表未知的选择子, 若当前接受者能找到备援对象,则将其返回,若找不到,就返回 nil
完整的消息转发
如果转发算法已经到这一步的话,俺那么唯一能做的就是启用完整的消息转发机制了.首先创建 NSInvocation 对象, 把与尚未处理的那条消息有关的全部细节, 都封装于其中,此对象包含选择子、目标,及参数, 在触发 NSInvocation 对象时, "消息派发系统"将亲自出马,把消息指派给目标对象 此步骤会调用下列方法来转发消息
- (void)forwardInvocation:(NSInvocation * )invocation
再触发消息前, 先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等
实现此方法是,若发现某调用不应有本类处理,择婿调用超类的同名方法, 这样的话,继承体系中的每个类都有机会处理此调用请求,直至 NSObject, 如果最后调用了 NSOBject 方法,那么该方法还会继而调用doesNotRecognizeSelector
以抛出异常,此异常表明选择子最终未能得到处理
消息转发全流程
消息转发全流程.jpg- 若对象无法响应某个选择子,则进入消息转发流程
- 通过运城期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把其中无法解读的某些选择子转交给其他对象来处理
- 经过上述两步之后, 如果还是没办法处理选择子,那就启动完整的消息转发机制
13 用"方法调配技术"调试"黑盒方法"
通俗讲 其实就是利用 runtime 实现方法交换 这个就不再详细解说了
- 在运行器,可以向类中新增或替换选择子所对应的方法实现
- 使用另一份实现来替换原有的方法实现, 这道工序叫做"方法调配", 开发者常用此技术向原有实现中添加功能
- 一般来说, 只有调试程序的时候,才需要在运行期修改方法实现, 这种做法不易滥用
14 理解"类对象"的用意
每个 Objective-C 对象实例都是指向某块内存数据的指针,描述 Objective-C对象所用的数据结构定义在运行期程序库的头文件里, id 类型本身也是定义在这里
typedef struct objc_object {
Class isa;
} * id
由此可见,每个对象结构体的首个成员是 Class 类的变量. 该变量定义了对象所属的类,通常称为 isa 指针
Class 对象也定义在运行期程序库的头文件中中:
typedef struct objc_class *Class;
struct objc_class{
Class isa;
Class super_class;
const char *name;
long version;
long info;
long instance_size;
struct objc_ivar_list *ivars;
struct objc_method_list **methodLists;
struct objc_cache *cache;
struct objc_protocol_list *protocols;
}
此结构体存放类的元数据,此结构体的首个变量也是 isa 指针, 这说明, Class 本身也是 Objective-C 对象,结构体中的 super_class 它定义了本类的超类, 类对象所属的类型(也就是 isa 指针所指向的类型)是另外一个类, 叫做元类,用来表述类对象本身所具备的元数据.每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类
假设有一个 someClass 的子类从 NSObject 中继承而来,则它的继承体系可由下图表示
在类继承体系中查询类型信息
可以用类型信息查询方法来检视类继承体系,isMemberOfClass
能够判断出对象是否是特定类的实例 而isKindOfClass
则能够判断出对象是否为某类或某派生派类的实例
- 每个实例都一个指向 Class 对象的指针, 用以表明其类型,而这些 Class 对象则构成了类的继承体系
- 如果对象类型无法在编译期确定,那么应该使用类型信息查询方法来探知
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能
第三章 接口与 API 设计
15 用前缀避免命名空间冲突
- 选择与你的公司,应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中均使用这一前缀.
- 若自己所开发的程序库中使用到第三方库, 则应为其中的名称加上前缀
16 提供"全能初始化方法"
UITableViewCell 初始化该类对象的时候,需要指明其样式及标识符, 标识符能够区分不同类型的单元格, 由于这种对象的创建成本比较高, 所以绘制表格时 可依照标识符来服用,提升程序执行效率,这种可以为对象提供必要信息以便其能完成工作的初始化方法叫做"全能初始化方法"
- 在类中提供一个全能初始化方法,并于文档中指明, 其他初始化方法均应调用此方法
- 若全能初始化方法与超类不同, 则需覆盖超类中的对应方法
- 如果超类的初始化方法不适用于子类, 那么应该复写这个超类方法,并在其中排除异常
这一点写开源框架的时候十分的受用
17 实现 description 方法
这个就不多说了 实际开发中经常用
- 实现 description 方法 返回一个有意义的字符串,用以描述该实例
- 若想在调试时打印出更详尽的对象描述信息,则应实现 debugDescription
18 尽量使用不可变对象
- 尽量创建不可变对象
- 若某属性仅可在对象内部修改,则在
class-continuation分类
中将其有 readonly 属相扩展为 readwrite 属性 - 不要把可变的 collection 作为属性公开,而应提供相关方法, 以此修改对象中的可变 collection
LLPerson.h
@interface LLPerson : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSInteger age;
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age;
@end
LLPerson.m
@interface LLPerson()
@property (nonatomic, copy, readwrite) NSString *name;
@property (nonatomic, assign, readwrite) NSInteger age;
@end
@implementation LLPerson
- (instancetype)initWithName:(NSString *)name age:(NSInteger)age{
if (self = [super init]) {
self.name = name;
self.age = age;
}
return self;
}
19 使用清晰而协调的命名方式
方法命名的几条规则
- 如果方法的返回值是新创建的, 那么方法名的首个词应是返回值得类型,除非前面还有修饰语,例如 localizedString 属性的存取方法不遵循这种命名方式,因为一般以为这些方法不会创建对象,即便有时返回内部对象的一份拷贝, 我们也认为那相当于原有的对象,这些存取方法应该按照其所对应的属性来命名
- 应该把表示参数类型的名词放在参数前面
- 如果方法要在当前对象执行操作,那么久应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词
- 不要使用 str 这种简称,应该用 string 这样的全称
- Boolean 属性应加上 is 前缀,如果方法返回非属性的 Boolean 值, 那么应该根据其功能 选用 has 或 is 当前缀
- 将 get 这个前缀留给那些借由"输出参数"来保存返回值的方法, 比如说,把返回值填充到"C语言数组"里的那张方法就可以使用这个词做前缀
类与协议的命名 - 起名时应遵从标准的 objective-C 命名规范,这样创建出来的接口更容易为开发者所理解
- 方法名要言简意赅,从左至右读起来要像个日常用于中的句子才好
- 方法名不要使用缩略后的类型名称
- 给方法起名时的第一要务 就是确保其风格与你自己的代码或所要集成的框架相符
20 为私有方法名加前缀
- 给私有方法的名称加上前缀, 这样可以很容易的将其同公共方法区分
- 不要单用一个下划线做私有方法的前缀, 因为这种做法是预留给苹果公司用的
21 理解 OBjective -C 错误类型
- 只有发生了可使整个应用程序崩溃的严重错误时, 才应使用异常
- 在错误不那么严重的情况下, 可以指派"委托方法"来处理错误,也可以把错误信息放在 NSError 对象里, 经由"输出参数"返回给调用者
// 比如 有一个抽象基类, 他的正确用法是先从中继承一个类,然后使用这个子类, 在这种情况下,如果有人直接使用了一个抽象基类,那么久抛出异常
- (void)mustOverrideMethod{
NSString *reason = [NSString stringWithFormat:@"%@m must be overridden",
NSStringFromSelector(_cmd)];
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:reason
userInfo:nil];
}
22 理解 NSCopying 协议
- 若想另自己所写的对象具有拷贝功能, 则需实现 NSCopying 协议
- 如果自定义的对象分为可变版本与不可变版本, 那么就要同时实现 NSCoping与 NSMutableCopying 协议
- 赋值对象是需决定采用浅拷贝还是深拷贝,一般情况下应该尽量执行浅拷贝
- 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法
第四章 协议与分类
23 通过委托与数据源协议进行对象间通信
这个就是常规我们使用的代理了 但是书中讲了一个新的知识点 我倒是从前从没有见过的 可以一起来看一下
- 如果有必要,可实现含有位段的结构体, 将委托对象是否能相应相关协议方法这一信息缓存至其中
这个知识点比较有价值
// 定义一个结构体
@interface LLNetWorkFetcher(){
struct {
unsigned int didReceiveData : 1;
unsigned int didDailWIthError : 1;
unsigned int didUpdateProgressTo : 1;
} _delegateFlags;
// 在外界设置代理的时候 重写 delegate 的 set 方法 对此结构体进行赋值
- (void)setDelegate:(id<LLNetworkFetcherDelegate>)delegate{
_delegate = delegate;
_delegateFlags.didReceiveData = [delegate respondsToSelector:@selector(networkFetcher:didReceiveData:)];
_delegateFlags.didDailWIthError = [delegate respondsToSelector:@selector(networkFetcher:didDailWIthError:)];
_delegateFlags.didUpdateProgressTo = [delegate respondsToSelector:@selector(networkFetcher:didUpdateProgressTo:)];
}
// 这样在调用的时候只需判断 结构体里边的标志就可以了 不需要一直调用 respondsToSelector 这个方法
if (_delegateFlags.didUpdateProgressTo) {
[_delegate networkFetcher:self didUpdateProgressTo:currentProgress];
}
}
24 将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分成易于管理的小块
- 将应该视为私有的方法归入名叫 Private 的分类中, 以隐藏实现细节
25 总是为第三方类的分类名称加前缀
分类的方法加入到类中这一操作是在运行期系统加载分类是完成的.运行期系统会把分类中所实现的每个方法都加入到类的方法列表中,如果类中本来就有此方法,而分类又实现了一次, 那么分类中的方法会覆盖原来那一份实现代码, 实际上可能会发生多次覆盖, 多次覆盖的结果一最后一个分类为准
- 向第三方类中添加分类时, 总应给其名称加上你专用的前缀
- 向第三方类中添加分类是,总应给其中的方法加上你专用的前缀
26 勿在分类中声明属性
这个老生常谈了
- 把封装数据 所用的全部属性都定义在主接口里
- 在"Class-continuation分类"之外的其他分类中,可以定义存取方法,但尽量不要定义属性
27 使用"class - continuation分类" 隐藏实现细节
class - continuation分类 通俗点来讲其实就是我们平时所说的延展
- 通过"class - continuation分类"向类中新增实例变量
- 如果某属性在主接口中声明为"只读" 而在类的内部又要用设置方法修改此属性,那么就在"class - continuation分类" 将其扩展为"可读写"
- 把私有方法的原型声明在"class - continuation分类里面
- 若想是类所遵循的协议不为人所知, 则可于"class - continuation分类中声明
28 通过协议提供匿名对象
- 协议可在某种程度上提供匿名类型, 具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法
- 使用匿名对象来隐藏类型名称(或类名)
- 如果具体类型不重要,重要的是对象能够响应(定义在协议里)特定方法,那么可以使用匿名对象来表示
第五章 内存管理
29 理解引用计数器
这一点也不多说了 不过有一个概念确实是之前没想过的
UIApplication 对象是 跟对象
- 引用计数机制通过可以递增递减的计数器来管理内存, 对象创建好之后, 其保留计数至少为1 , 若保留计数为正,则对象继续存活, 当保留计数降为0时,对象就被销毁了
- 在对象生命期中, 其余对象通过引用来保留或释放此对象, 保留与释放操作分别会递增及递减保留计数
30 以 ARC 简化引用计数
- 有了 ARC 之后, 程序员就无需担心内存管理问题了, 使用 ARC 来编程,可省去类中许多"样板代码"
- ARC 管理对象生命期的办法基本就是:在合适的地方插入"保留"及释放操作, 在 ARC 环境下, 变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行"保留"及"释放" 操作
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现, ARC 将此确定为开发者必须遵守的规则
- ARC 只负责管理 OBjectice-C 对象的内存, 尤其注意: CoreFounfation 对象不归 ARC 管理,开发者必须适时使用 CFRetain/CFRelease
31 在 dealloc 方法中只释放引用并解除监听
- 在 dealloc 方法里, 应该做的事情就是释放指向其他对象的引用, 并取消原来订阅的"键值观测"(KVO) 或 NSNotificationCenter 等通知, 不要做其他事情
- 如果对象持有文件描述符等系统资源, 那么应该专门编写一个方法来释放此种资源. 这样的类要和其使用者约定,用完资源后必须调用 close
- 执行异步任务的方法不应该放在 dealloc 里调用;只能在正常状态下,执行的那些方法也不应在 dealloc 里调用,因此此时对象已处于正在回收的状态了
32 编写"异常安全代码"时留意内存管理问题
- 在捕获异常时, 一定要注意将 Try 块内所创立的对象清理干净
- 在默认情况下, ARC 不生成安全处理代异常所需的清理代码,开启编译器标志后, 可以生成这种代码,不过会导致应用程序变大, 而且会降低运行效率
如下边代码
若在 ARC 且必须捕获异常时, 则需要打开-fobjc-arc-exceptions
标志
NSObject *object;
@try {
object = [NSObject new];
[object doSomeThingThatMayThrow];
}
@catch(...){
}
@finally{
}
33 以弱引用避免保留环
- 将某些引用设为 weak 可避免出现"保留环"
- weak 引用可以自动清空,也可以不自动清空.自动清空(autonilling)是随着 ARC 而引入的新特性,由运行期系统来实现.在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收的对象
34 以"自动释放池块"降低内存峰值
- 自动释放池排布在栈中, 对象收到 autorelease 消息后, 系统将其放入最顶端的池里
- 要合理运用自动释放池, 可降低应用程序的内存封值
- @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池
常见的例子就是 下边的 加上@autoreleasepool
应用程序在执行循环的时候内存峰值就会降低
NSArray *dataArr = [NSArray array];
NSMutableArray *personArrM = [NSMutableArray array];
for (NSDictionary *recode in dataArr) {
@autoreleasepool{
LLPerson *person = [[LLPerson alloc]initWithRecode:recode];
[personArrM addObject:person];
}
}
35 用"僵尸对象"调试内存管理问题
- 系统在回收对象时,可以不将其真的回收, 而是把它转化为僵尸对象,通过环境变量 NSZombieEnabled 可开启此功能
- 系统会修改对象的 isa 指针,令其指向特殊的僵尸类, 从而使改对象变为僵尸对象.僵尸类能够相应所有的选择子, 相应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序
36 不要使用retainCount
- 对象的保留计数看似有用, 实则不然,因为任何给定时间点上的"绝对保留计数"都无法反应对象生命期的全貌
- 引入 ARC 之后, retainCount 方式就正式废止了,在 ARC 下调用该方法会导致编译器报错
第六章 块与大中枢派发
37 块的内部结构
块对象内部结构.jpeg块本身也是对象,在存放块对象内存区域中, 首个变量是指向 Class 对象的指针,该指针叫做 isa, 其余内存里含有块对象正常运转所需的各种信息, 在内存布局中,最重要的就是 invoke 变量,这就是函数指针,指向块的实现代码, 函数原型只要要接受一个 void* 型的参数, 此参数代表块.刚才说过,块其实就是一种代替函数指针的语法结构, 原来使用函数指针是,需要用不透明的 void 指针来传递状态 而改用块之后, 则可以把原来用标准 C 语言特性所编写的代码封装成简明且易用的接口.
descriptor 变量是指向结构体的指针, 每个块里都包含此结构体,其中声明了块对象的总体大小,还声明了 copy 和 dispose 这两个辅助函数所对象的函数指针, 辅助函数在拷贝及丢弃块对象时运行, 其中会执行一些操作, 比方说 前者要保留捕获的对象, 而后者则将之释放
块还会把它所捕获的所有变量都拷贝一份, 这些拷贝放在 descriptor 变量后边,捕获了多少变量,就要占据多少内存空间, 请注意, 拷贝的并不是对象变量,而是指向这些对象的指针变量, invoke 函数为何需要把块对象作为参数传进来呢? 原因就在于,执行块的时候 要从内存中把这些捕获到的变量读出来
38 为常用的块类型创建 typedef
- 以 typedef 重新定义块类型, 可令块变量用起来更加简单
- 定义新类型时应遵从现有的命名习惯,无使其名称与别的类型相冲突
- 不妨为同一个块签名定义多个类型别名, 如果要重构的代码 使用了块类型的某个别名, 那么只需要就该相应的 typedef 中的块签名即可, 无序改动气的 typedef
39 用 Handel 块降低代码分散程度 其实也就是我们所说的 block 回调
- 在创建对象时, 可以使用内联的 handle 块将相关业务逻辑一并声明
- 在有多个实例需要监控时, 如果采用委托模式, 那么经常需要根据传入的对象来切换, 而若改用 handle 块来实现, 则可直接将块与相关对象放在一起
- 设计 API 是如果用到了 handle 块 那么可以增加一个参数, 使调用者可通过参数来决定把块安排在哪个队列上执行
40 用块引用其所属对象时不要出现保留环
- 如果块所捕获的对象直接或间接的保留了块本身, 那么就得当心保留环问题了
- 一定要找个适当的时机解除保留环, 而不能把责任推给 API 的调用者
41 多用派发队列,少用同步锁
这一点就详细说说吧
在 OC 中多线程要执行同一份代码,那么有时可能会出问题, 这种情况下,通常要使用锁来实现某种同步机制.
在 GCD 出现之前, 有两种方法:
- 1 第一种采用内置的"同步块"
- (void)synchronizedMethod{
@synchronized(self){
// safe
}
}
- 2 直接使用 NSLock 对象
_lock = [[NSLock alloc]init];
- (void)synchronizedMethod{
[_lock lock];
// safe
[_lock unlock];
}
这两种方法都很好不过也都有缺陷 比如说,在极端情况下,同步块会导致死锁, 另外 其效率也不见得高, 而如果直接使用锁对象的话,一旦遇到死锁, 就会非常麻烦
GCD 的到来它能以更简单更高效的形式为代码加锁
我们都知道属性就是开发者经常需要同步的地方,这种属性需要做成"原子的", 用 atomic 即可实现这一点, 但如果我们自己实现的话就可以用 GCD 来实现
- 优化1
使用"串行同步队列, 将读取操作及写入操作都安排在同一个队列里,即可保证数据同步" 如一下代码
_syncQueue = dispatch_queue_create("aokamu.syncQueue", NULL);
- (NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue, ^{
_someString = someString;
})
}
上述代码: 把设置操作与获取操作都安排在序列化的队列里执行了, 这样的话, 所有针对属性的访问操作就都同步了, 全部加锁任务都在 GCD 中处理, 而 GCD 是相当深的底层来实现的,于是能够做许多优化
- 优化2 设置方法不一定非得是同步的
- (void)setSomeString:(NSString *)someString{
dispatch_async(_syncQueue, ^{
_someString = someString;
})
}
这个吧同步派发改成异步派发,可以提升设置方法的执行速度, 而读取操作与写入操作依然会按顺序执行, 不过这样写昂写还是有一个弊端. :如果你测一下程序性能,那么可能会发现这种写法比原来慢, 因为执行异步派发时需要拷贝块.
- 优化3 终极优化 不用串行队列, 而改用并发队列 并且使用 栅栏(barrier)
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- (NSString *)someString{
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
})
}
在队列中 栅栏块必须单独执行, 不能与其他块并行, 这只对并发队列有意义, 因为串行队列中的块总是按顺序逐个来执行的, 并发队列如果发现接下来要处理的块是个栅栏块,那么久一直要等当前所有并发块都执行完毕,才会单独执行这个栅栏块 待栅栏块执行过后 再按正常方式向下处理 如下图
Snip20171031_1.png- 派发队列可用来表述同步语义,这种做法要比使用
@synchronized
块或者NSLock
对象更简单 - 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步下行为,而这么做却不会阻塞执行异步派发的线程
- 使用同步队列及栅栏块.可以令同步行为更加高效
42 多用 GCD 少用 performSelector 系列方法
这个现在已经没有人去用performSelector 系列方法了
- performSelector 系列方法在内存管理方面容易有疏失,他无法确定将要执行的选择子具体是什么, 因而 ARC 编译器也就无法插入适当的内存管理方法
- performSelector 系列方法所能处理的选择子太过于局限了,选择子的返回值类型及发送给方法的参数个数都受到限制
- 如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里, 然后调用大中枢派发机制的相关方法来实现
43 掌握 GCD 及操作队列的使用时机
在来简单总结一下操作队列(NSOPeration)的几种使用方法
① 取消某个操作
运行任务前可以调用 cancel 方法 ,该方法会设置对象内的标志位,用以表明此任务不需要执行, 不过已经启动的任务无法取消了,
②指定操作间的依赖关系
一个操作可以依赖其他多个操作
③ 通过键值观测机制监控 NSOperation 对象的属性.
NSOPeration 对象有许多属性都适合通过键值观测机制来监听
④指定操作的优先级
NSOperation 对象也有"线程优先级",这决定了运行此操作的线程处在何种优先级上
- 在解决多线程与任务管理问题时,派发队列并非唯一方案
- 操作队列提供了一套高层的 Objective-CAPI, 能实现纯 GCD 所具备的绝大部分功能,而且还能完成一些更为复杂的操作, 那些操作弱改用 GCD 来实现, 则需另外编写代码
44 通过 Dispatch Group 机制, 根据系统资源状况来执行任务
这个也简单记录一下把
Dispatch Group 俗称 GCD 任务组,我们 用伪代码来看一下 Dispatch Group的用法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t dispatchGroup = dispatch_group_create();
for (id object in collectin) {
dispatch_group_async(dispatchGroup,
queue,
^{
[object performTask];
})
}
dispatch_group_notify(dispatchGroup,
dispatch_get_main_queue(),
^{
[self updateUI];
})
notify回调的队列完全可以自己来定 可以用自定义的串行队列或全局并发队列
这里还有 GCD 的另一个函数平时比较少用的 那就是dispatch_apply
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(array.count,
queue,
^(size_t i) {
id object = array[i];
[object performTask];
})
dispatch_apply所使用的队列可以使并发队列, 也可以是串行队列, 加入把块派给了当前队列(或体系中高于当前队列的某个串行队列),这将会导致死锁,
- 一系列任务可以归入一个 dispatch group 之中,开发者可以在这组任务执行完毕是获得通知
- 通过 dispatch Group, 可以在并发式派发队列里同时执行多项任务, 此时 GCD 会根据系统资源状况来调度这些并发执行的任务, 开发者若自己来实现此功能,则需编写大量代码
45 使用 dispatch_once 来执行只需要运行一次的线程安全代码
这个就是老生常谈的单例了 也就不多说了
46 不要使用 dispatch_get_current_queue
这个函数已经废弃了 此处就不多说了
第七章 系统框架
47 熟悉系统框架
我们开发者经常碰到的就是 Foundation 框架 像NSobject,NSArray,NSDictionary 等类 都在其中,
还有一个与Foundation相伴的框架是 CoreFoundation,CoreFoundation 不是 OC 框架,但是确定编写 OC 应用程序时所应熟悉的重要框架,Foundation框架中的许多功能都可以在此框架中找到对应的 C 语言 API
除了 Foundation和CoreFoundation还有以下系统库:
-
CFNetwork 此框架提供了 C 语言级别的网络通信, 它将"BSD 套接字"抽象成易于使用的网络接口
-
CoreAudio 该框架所提供的 C语言 API 可用来操作设备上的音频硬件, 这个框架属于比较难用的那种, 因为音频处理本身就很复杂,所幸由这套 API 可以抽象出另外一个 OC 的 API, 用后者来处理音频问题会简单些
-
AVFoundation 此框架所提供的 OC 对象可用来回放并录制音频及视频,比如 能够在 UI 视图类播放视频
-
CoreData 此框架中所提供的 OC 接口可将对象放入到数据库中,便于持久保存
-
CoreText 此框架提供的 C语言接口可以高效执行文字排版及渲染操作
-
请记住 用纯 C 语言写的框架与用 OC 写成的一样重要, 若想成为优秀的 OC 开发者, 应该掌握 C 语言的核心概念
48 多用块枚举 少用 for 循环
- 块枚举法 本身就能通过 GCD 来并发执行遍历操作,无须另行编写代码,而采用其他遍历方式则无法轻易实现这一点
- 若提前知道待遍历的 collection 含有何种对象,则应修改块签名, 指出对象的具体内容
NSArray<LLPerson *> *dataArr = [NSArray array];
[dataArr enumerateObjectsUsingBlock:^(LLPerson * _Nonnull obj,
NSUInteger idx,
BOOL * _Nonnull stop) {
}];
49 对自定义其内存管理语义的 collection 使用无缝桥接
- 通过无缝桥接技术, 可以在 Foundation 框架中的 OC 对象与 CoreFoundation 框架中的 C语言数据结构之间来回转换
- 在CoreFoundation 层面创建collection 时,可以执行许多回调函数, 这些函数表示此 collection 应如何处理其元素, 然后可运用无缝桥接技术, 将其转换成具备特殊内存管理语义的 OC collection
NSArray *anNSArray = @[@1,@2,@3,@4,@5];
CFArrayRef aCFArray = (__bridge CFArrayRef)(anNSArray);
NSLog(@"count = %li",CFArrayGetCount(aCFArray));
// Output: count = 5 ;
50 构建缓存时选用 NSCache 而非 NSDIctionary
- 实现缓存时选用 NSCache 而非 NSDictionary 对象,因为 NSCache 可以提供优雅的自动删减功能,而且是线程安全的, 此外 他与字典不同,并不会拷贝键
- 可以给 NSCache 对象设置上限, 用以限制缓存中的对象总个数及"总成本".而这些初度则定义了缓存删减其中对象的时机, 但是绝对不要把这些尺度当成可靠地"硬限制"他们仅仅对 NSCache 起指导作用
- 将 NSPurgeableData 与 NSCache 搭配使用.可实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除
- 如果缓存使用得当, 那么应用程序的响应速度就能提高,只有那种"重新计算起来很费事的"数据才值得放入缓存,比如那些需要从网络获取或者从磁盘读取的数据
来看下边伪代码
typedef void(^LLNetWorkFetcherCompleteHandler)(NSData *data);
@interface LLNetWorkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(LLNetWorkFetcherCompleteHandler)handler;
@end
#import "LLClass.h"
#import "LLNetWorkFetcher.h"
@implementation LLClass{
NSCache *_cache;
}
- (instancetype)init{
if (self = [super init]) {
_cache = [NSCache new];
_cache.countLimit = 100;
_cache.totalCostLimit = 5 * 1024 * 1024;
}
return self;
}
- (void)downLoadDataForURL:(NSURL *)url{
NSData *cacheData = [_cache objectForKey:url];
if (cacheData) {
[self useData:cacheData];
}else{
LLNetWorkFetcher *fetcher = [[LLNetWorkFetcher alloc]initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
[_cache setObject:data forKey:url cost:data.length];
[self useData:cacheData];
}];
}
}
51 精简 initialize 与 load 的实现代码
-
+ (void) load
对于加入运行期系统的每个类及分类来说,必定会调用此方法而且仅调用一次,当包含类或者分类的程序库载入系统时, 就会执行此方法
如果分类和其所属的类都定义了 load 方法, 则先调用类里边的 在调用分类里边的
load 方法的问题在于执行该方法时,运行期系统是"脆弱状态",在执行子类的 load 方法之前,必定会先执行所有超类的 load 方法, 如果代码还依赖了其他程序库,那么程序库里相关类的 load 方法也必定会先执行, 根据某个给定的程序库,却无法判断出其中各个类的载入顺序, 因此 在 load 方法中使用其他类是不安全的.
load 方法不像其他普通方法一样, 他不遵从那套继承规则, 如果某个类本身没有实现 load 方法,那么不管其各级超类是否实现此方法, 系统都不会调用. -
+ (void)initialize
对于每个类来说 该方法会在程序首次使用该类之前调用, 且只调用一次,他是有运行期系统来调用的,绝不应该通过代码直接调用 他与 load 方法有一定的区别的
首先 他是惰性调用的, 也就是说只有当程序用到了相关的类是,才会调用 如果某个类一直都没有使用, 那么其 initialize 方法就一直不会运行
其次, 运行期系统在执行该方法时,是处于正常状态的, 因此 从运行期系统完整度来讲, 此时可以安全使用并调用任意类中的任意方法 而且运行期系统也能确保initialize 方法一定会在"线程安全的环境"中执行,也就是说 只有执行initialize的那个线程 可以操作类与类实例,
最后, initialize 方法与其他消息一样,如果某个类未实现它, 而其超类实现了,俺那么就会运行超类的实现代码
- 在加载阶段 如果类实现了 load 方法,那么系统就会调用它.分类里也可以定义此方法,类的 load 方法要比分类中先调用,其他方法不同, load 方法不参与复写机制
- 首次使用某个类之前,系统会向其发送initialize 消息,由于此方法遵从普通的复写规则,所以通常应该在里边判断当前要初始化的是哪一个类
- load 和initialize 方法都应该实现的精简一些, 这有助于保持应用程序的相应能力 也能减少引入"依赖环"的几率
- 无法在编译期设定的全局变量,可以放在initialize 方法里初始化
52 别忘了 NSTimer 会保留其目标对象
计时器是一种很方便也很有用的对象,但是 由于计时器会保留其目标对象, 所以反复执行任务通常会导致应用程序出问题,也就是很容易引入"保留环"
来看下列代码
@interface LLClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end
@implementation LLClass{
NSTimer *_pollTimer;
}
- (void)startPolling{
_pollTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(p_doPoll)
userInfo:nil
repeats:YES];
}
- (void)stopPolling{
[_pollTimer invalidate];
_pollTimer = nil;
}
- (void)p_doPoll{
}
- (void)dealloc{
[_pollTimer invalidate];
}
计时器的目标对象是 self, 然后计时器使用实例变量来存放的, 所以实例变量也保存李计时器, 于是就产生了保留环
本书中提供了一个用"块"来解决的方案 虽然计时器当前并不直接支持块,但是可以用下面这段代码添加功能
@implementation NSTimer (LLBlocksSupport)
+ (NSTimer *)ll_schedeledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)())block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(ll_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)ll_blockInvoke:(NSTimer *)timer{
void (^block)() = timer.userInfo;
if (block) {
block();
}
}
上边的代码是在 NSTimer 分类中添加的代码 来看一下具体的使用
- (void)startPolling{
__weak LLClass *weakSelf = self;
_pollTimer = [NSTimer ll_schedeledTimerWithTimeInterval:5.0
block:^{
LLClass *strongSelf = weakSelf;
[strongSelf p_doPoll];
}
repeats:YES];
先定义弱引用,然后用block捕获这个引用,但是在用之前在立刻生成 strong 引用.保证实例在执行期间持续存活
- NSTimer 对象会保留其目标, 直到计时器本身失效为止,调用 invalidate 方法可令计时器失效, 另外 一次性的计时器, 在触发任务之后,也会失效,
- 反复执行任务的计时器,很容易引入保留环, 如果这种计时器的目标对象有保留了计时器本事,那么肯定会导致保留环,这种环保留,可能直接发生,也可能是通过对象图里的其他对象间接发生
- 可以扩充 NSTimer 的功能,用"块"来打破保留环,不过 除非 NSTimer 将来在公共接口里提供此功能, 否则必须创建分类,将相关实现代码加入其中