工作文档iOS大牛修炼之路ios

Effective Objective-C 2.0 敲门砖

2016-05-19  本文已影响1398人  Jerry4me
Effective Objective-C 2.0 编写高质量iOS和OS X代码的52个有效方法

前言

这本书和Objective-C高级编程-iOS和OS X多线程和内存管理实在是iOS开发人员必读书. 实在是太经典了. 相信懂的人自然懂~

Objective-C高级编程的读书笔记我已经整理好发布了, 大家感兴趣的话可以去看看, 不感兴趣就直接略过吧~
Objective-C高级编程读书笔记之内存管理
Objective-C高级编程读书笔记之blocks
Objective-C高级编程读书笔记之GCD

这篇文章只是一个敲门砖, 大家不要指望看了这篇文章就不用去看书了, 那是不可能的, 也是远远不够的, 只是希望各位能借助我这篇文章, 留个整体的印象, 然后再带着问题去研读这本书. 那才能达到最好的效果.


目录

第1章 : 熟悉Objective-C
第2章 : 对象, 消息, 运行时
第3章 : 接口与API设计
第4章 : 协议和分类
第5章 : 内存管理
第6章 : 块与大中枢派发(也就是Block与GCD)
第7章 : 系统框架


第1章 : 熟悉Objective-C

1. Objective-C是一门动态语言, 该语言使用的是"消息结构"而非"函数调用".

也就是说
<pre> [person run];</pre>

给person对象发送一条run消息 : 不到程序运行的时候你都不知道他究竟会执行什么代码. 而且, person这个对象究竟是Person类的对象, 还是其他类的对象, 也要到运行时才能确定, 这个过程叫动态绑定.

2. 堆空间

对象所占内存总是分配在堆空间中. 不能在栈中分配Objective-C对象.

<pre>
NSString *anString = @"Jerry";
NSString *anotherString = anString;
</pre>

以上代码的意思是, 在堆空间中创建一个NSString实例对象, 然而栈空间中分配两个指针分别指向该实例. 如图,


堆和栈
在类的头文件中尽量少引入其他文件

在类的头文件中用到某个类, 如果没有涉及到其类的细节, 尽量用@class向前声明该类(等于告诉编译器这是一个类, 其他你先别管)而不导入该类的头文件以避免循环引用和减少编译时间.

多用字面量语法, 少用与之等价的方法

我们知道, 现在我们创建Foundation框架的类时有许多便捷的方法, 如
<pre>
NSString *string = @"Jerry";
NSNumber *number = @10;
NSArray *array = @[obj, obj1, obj2];
NSDictionary *dict = @{
@"key1" : obj1,
@"key2" : obj2,
@"key3" : obj3 };
</pre>

我用们字面量语法替代传统的alloc-init来创建对象的好处 :

局限性 :

关于字面量语法, 有位哥们写得挺清楚, 可以去看看浅谈OC字面量语法.

多用类型常量, 少用#define预处理指令

为什么少用#define预处理指令?

为什么多用类型常量?

针对const#define的优劣, 可参考我之前写过的一篇文章15分钟弄懂 const 和 #define

用枚举来表示状态, 选项, 状态码

相对于魔法数字(Magic Number), 使用枚举的好处不言而喻. 这里只说两个.

  1. 如果枚举类型的多个选项不需要组合使用, 则用NS_ENUM
    <pre>
    typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,
    UIViewAnimationTransitionFlipFromLeft,
    UIViewAnimationTransitionFlipFromRight,
    UIViewAnimationTransitionCurlUp,
    UIViewAnimationTransitionCurlDown,
    };
    </pre>

  2. 如果枚举类型的多个选项可能组合使用, 则用NS_OPTIONS
    <pre>
    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
    };
    </pre>

以上代码为苹果源码.
使用NS_ENUM和NS_OPTIONS来替代C语言的enum的好处

另外, 在处理枚举的switch语句中, 不要使用default分支, 因为以后你加入新枚举之后, 编译器会提示开发者 : switch语句没有处理所有枚举(没使用default的情况下).


第2章 : 对象, 消息, 运行时

上一章我们说到, Objective-C是一门动态语言, 其动态性就由这一章来说明.

理解"属性"这一概念

<pre>
@interface Person : NSObject {
@public
NSString *_firstName;
NSString *_lastName;
@private
NSString *_address;
}
</pre>

编写过Java或C++的人应该比较熟悉这种写法, 但是这种写法问题很大!!!
对象布局在编译器就已经固定了. 只要碰到访问_firstName变量的代码, 编译器就把其替换为"偏移量", 这个偏移量是"硬编码", 表示该变量距离存放对象的内存区域的起始地址有多远.

目前这样看没有问题, 但是只要在_firstName前面再加一个实例变量就能说明问题了.

<pre>
@interface Person : NSObject {
@public
NSDate *_birthday;
NSString *_firstName;
NSString *_lastName;
@private
NSString *_address;
}
</pre>

原来表示_firstName的偏移量现在却指向_birthday了. 如图

在类中新增另一个实例变量前后的数据布局图

有人可能会有疑问, 新增实例变量不是要写代码然后编译运行程序吗? 重新编译后对象布局不就又变正确了吗? 错误! 正是因为Objective-C是动态语言, 他可以在运行时动态添加实例变量, 那时对象布局早就已固定不能再更改了.

那么Objective-C是怎么避免这种情况的呢? 它把实例变量当做一种存储偏移量所用的"特殊变量", 交由"类对象"保管(类对象将会在本章后面说明). 此时, 偏移量会在运行时进行查找, 如果类的定义变了, 那么存储的偏移量也会改变, 这样在运行时无论何时访问实例变量, 都能使用正确的偏移量. 有了这种稳固的ABI(Application Binary Interface), OC就能在运行时给类动态添加实例变量而不会发生访问错误了.

@property, @synthesize, @dynamic

这是本节的重中之重. 我们必须要搞清楚使用@property, @synthesize, @dynamic关键字, 编译器会帮我们做了什么, 才能更好地掌握使用属性.

以上代码编译器会帮我们分解成setter和getter方法声明, 以上代码与以下代码等效
<pre>
@interface Person : NSObject

<pre>
@implementation Person
@synthesize firstName;
@end
</pre>

以上代码相当于给Person类添加一个_firstName的实例变量并为该实例变量生成setter和getter方法的实现(存取方法).

可以利用@synthesize给实例变量取名字(默认为_xxx, 例如@property声明的是name, 则生成的是_name的实例变量)

<pre>
@implementation Person
@synthesize firstName = myFirstName;
@end
</pre>

以上代码就是生成myFirstName的实例变量了. 由于OC的命名规范, 不推荐这么做. 没必要给实例变量取另一个名字.

<pre>
@implementation Person
@dynamic firstName;
@end
</pre>

该代码会告诉编译器 : 不要自动创建实现属性(property)所用的实例变量(_property)和存取方法实现(setter和getter).

也就是说, 实例变量不存在了, 因为编译器不会自动帮你创建了. 而且如果你不手动实现setter和getter, 使用者用点语法或者对象方法调用setter和getter时, 程序会直接崩溃, 崩溃原因很简单 : unrecognized selector sent to instance

上代码
<pre>
// Person.h
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end


// Person.m
@implementation Person
@dynamic name;
@end


// main.m
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
p.name = @"Jerry";
return 0;
}


// 程序崩溃, 控制台输出
-[Person setName:]: unrecognized selector sent to instance
</pre>

原因很简单, 我用@dynamic骗编译器, 你不用帮我生成实例变量跟方法实现啦, 我自己来. 结果运行的时候却发现你丫的根本找不到实现方法, 所以崩溃了呗~

总结下

在现在的编译器下,

  1. @property会为属性生成setter和getter的方法声明, 同时调用@synthesize ivar = _ivar生成_ivar实例变量和存取方法的实现
  2. 手动调用@synthesize可以用来修改实例变量的名称
  3. 手动调用@dynamic可以告诉编译器: 不要自动创建实现属性所用的实例变量, 也不要为其创建实例变量的存取方法声明与实现.
readonly与readwrite

以上文档说明, 就算你没有用@dynamic, 只要你手动实现了setter和getter方法(属性为readwrite情况下)或者手动实现getter方法(属性为readonly情况下), @property关键字也不会自动调用@synthesize来帮你合成实例变量了.

以上特性均可以使用runtime打印类的实例变量列表来印证.

在对象内部尽量直接访问实例变量

为什么呢? 使用点语法不好吗? 这里说说区别

综上, 比较折中的方法就是

对象等同性

比较两个对象是否相同.
我们可以重写isEqual方法自定义对象等同的条件

类族模式

Objective-C的系统框架中普遍使用此模式, 用子类来隐藏"抽象基类"的内部实现细节.
我们肯定使用过UIButton的这个类方法
<pre> + (UIButton *)buttonWithType:(UIButtonType)type;</pre>

这就是UIButton类实现的"工厂方法", 根据传入的枚举创建并返回合乎条件的子类.

Foundation框架中大部分容器类都是类族, 如NSArray与NSMutableArray, NSSet与NSMutableSet, NSDictionary与NSMutableDictionary.

用isKindOfClass方法可以判断对象所属的类是否位于类族之中.

在类族中实现子类时所需遵循的规范一般都会定义于基类的文档之中, 使用前应先看看.

具体类族的使用方法大家请看书~~

在既有类中使用关联对象存放自定义数据

在类的内部利用哈希表映射技术, 关联一个与该类毫无耦合的对象.
使用场景

鉴于书中所说, 容易出现循环引用, 以及关联对象释放和移除不同步等缺陷,
使用关联对象这一解决方案总是不到万不得已都不用的, 所以这里只提供两篇文章, 感兴趣的话大家可以去了解了解.
Associated Objects
Objective-C Associated Objects 的实现原理

消息发送和转发机制

OC的消息发送和转发机制是深入了解OC这门语言的必经之路. 下面我们就来学习学习这个消息发送和转发机制的神奇之处.

objc_msgSend

在解释OC消息发送之前, 最好先理解C语言的函数调用方式. C语言使用"静态绑定", 也就是说在编译器就能决定运行时所应调用的函数. 如下代码所示
<pre>
void run() {
// run
}

void study() {
// study
}

void doSomething(int type) {
if (type == 0) {
run();
} else {
study();
}
}
</pre>

如果不考虑内联, 那么编译器在编译代码的时候就已经知道程序中有run和study这两个函数了, 于是会直接生成调用这些函数的指令. 如果将上述代码改写成这样呢?
<pre>
void run() {
// run
}

void study() {
// study
}

void doSomething(int type) {
void (*func)();
if (type == 0) {
func = run;
} else {
func = study;
}
func();
}
</pre>

这就是"动态绑定".

在OC中, 如果向某对象发送消息, 那就会使用动态绑定机制来决定需要调用的方法. OC的方法在底层都是普通的C语言函数, 所以对象收到消息后究竟要调用什么函数完全由运行时决定, 甚至可以在运行时改变执行的方法.

现在开始来探索OC的消息机制
<pre>
[person read:book];
// 解释
// person : receiver(消息接收者)
// read : selector(选择子)
// 选择子 + 参数 = 消息
</pre>

编译器会将以上代码编译成以下代码
<pre>
objc_msgSend(person, @selector(read:), book);
// 解释
// objc_msgSend方法原型为 void objc_msgSend(id self, SEL cmd, ...)
// self : 接收者
// cmd : 选择子
// ... : 参数, 参数的个数可变
</pre>

objc_msgSend会根据接收者和选择子的类型来调用适当的方法, 流程如下

  1. 查找接收者的所属类的cache列表, 如果没有则下一步
  2. 查找接收者所属类的"方法列表"
  3. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  4. 找不到, 就沿着继承体系继续向上查找
  5. 如果能找到与选择子名称相符的方法, 就跳至其实现代码
  6. 找不到, 执行"消息转发".

那么找到与选择子名称相符的方法, 就跳至其实现代码这一步是怎么实现的呢? 这里又要引出一个函数原型了
<pre>
<return_type> Class_selector(id self, SEL _cmd, ...);
</pre>

真实的函数名可能有些出入, 不过这里志在用该原型解释其过程, 所以也就无所谓了.
每个类里都有一张表格, 其中的指针都会指向这种函数, 而选择子的名称则是查表时所用的key. objc_msgSend函数正是通过这张表格来寻找应该执行的方法并跳至其实现的.

方法底层实现

乍一看觉得调用一个方法原来要这么多步骤, 岂不是很费时间? 不着急~ objc_msgSend会将匹配结果缓存在"快速映射表"里, 每个类都有这样一块缓存, 下次调用相同方法时, 就能很快查找到实现代码了.

消息发送的其他方法

消息转发

上面我们曾说过, 如果到最后都找不到, 则进入消息转发

动态方法解析 :

对象在收到无法解读的消息后, 首先调用其所属类的这个类方法 :

<pre>

假如尚未实现的方法不是实例方法而是类方法, 则会调用另一个方法resolveClassMethod:

备胎 :

动态方法解析失败, 则调用这个方法
<pre>

通过备胎这个方法, 可以用"组合"来模拟出"多重继承".

完整的消息转发 :

备胎也无能为力了, 只能把消息包装成一个对象, 给接收者最后一次机会, 搞不定就不搞了!
<pre>

在这里能做的比较现实的事就是 : 在触发消息前, 先以某种方式改变消息内容, 比如追加另外一个参数, 或是改变选择子等等. 实现此方法时, 如果发现某调用操作不应该由本类处理, 可以调用超类的同名方法. 则继承体系中的每个类都有机会处理该请求, 直到NSObject. 如果NSObject搞不定, 则还会调用doesNotRecognizeSelector:来抛出异常, 此时你就会在控制台看到那熟悉的unrecognized selector sent to instance..

消息转发

尽量在第一步就把消息处理了, 因为越到后面所花代价越大.

Method Swizzling

被称为黑魔法的一个方法, 可以把两个方法的实现互换.
如上文所述, 类的方法列表会把选择子的名称映射到相关的方法实现上, 使得"动态消息派发系统"能够据此找到应该调用的方法. 这些方法均以函数指针的形式来表示, 这种指针叫做IMP,
<pre> id (*IMP)(id, SEL, ...)</pre>

NSString类的选择子映射表

OC运行时系统提供了几个方法能够用来操作这张表, 动态增加, 删除, 改变选择子对应的方法实现, 甚至交换两个选择子所映射到的指针. 如,

经过一些操作后的NSString选择子映射表

如何交换两个已经写好的方法实现?
<pre>
// 取得方法
Method class_getInstanceMethod(Class aClass, SEL aSelector)
// 交换实现
void method_exchangeImplementations(Method m1, Method m2)
</pre>

通过Method Swizzling可以为一些完全不知道其具体实现的黑盒方法增加日志记录功能, 利于我们调试程序. 并且我们可以将某些系统类的具体实现换成我们自己写的方法, 以达到某些目的. (例如, 修改主题, 修改字体等等)

类对象

OC中的类也是对象的一种, 你同意吗?

<pre>
// 对象的结构体
struct objc_object {
Class isa;
};
// 类的结构体
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;
}
</pre>

根据以上源码我们可以知道, 其实类本身也是一个对象, 称之为类对象, 并且类对象是单例, 即在程序运行时, 每个类的Class仅有一个实例.

实例对象的isa指针指向所属类, 那么类对象的isa指向什么呢? 是元类(metaclass)

类与对象的继承层级关系图
isa指针
用类型信息查询方法来检视类继承体系

<pre>
// 判断对象是否为某个特定类的实例

例如, GoodPerson是Person的子类

<pre>
Person *p = [[Person alloc] init];
GoodPerson *gp = [[GoodPerson alloc] init];
// 判断p的类型
[p isMemberOfClass:[Person class]]; // YES
[p isMemberOfClass:[GoodPerson class]]; // NO
[p isKindOfClass:[Person class]]; // YES
[p isKindOfClass:[GoodPerson class]]; // NO
// 判断gp的类型
[gp isMemberOfClass:[Person class]]; // NO
[gp isMemberOfClass:[GoodPerson class]]; // YES
[gp isKindOfClass:[Person class]]; // YES
[gp isKindOfClass:[GoodPerson class]]; // YES
</pre>


第3章 : 接口与API设计

这一章讲的是一些命名规范, 设计API时的一些注意点.

用前缀避免命名空间冲突

OC没有其他语言那种内置的命名空间, 所以只能通过前缀来营造一个假的"命名空间". 这里推荐

提供"全能初始化方法"

何为全能初始化方法?

为什么要提供全能初始化方法?

子类的全能初始化应该调用超类的全能初始化方法. 若超类的初始化方法不适用于子类, 那么应该重写这个超类方法, 并在该方法抛出异常.

实现description方法

我们知道, 利用%@来打印一个对象得到的永远是<类名 : 内存地址>(NSString, NSDictionary, NSArray等对象除外). 如果我们需要输出一些我们想要的内容, 那么重写该方法即可. 应该注意的是不要在description方法输出self, 会引发死循环.

除了description方法, 还有一个dubug专用的debugDescription方法. 该方法只有在开发者在调试器中用LLDC的"po"指令打印对象时才会调用.

尽量使用不可变对象

我们知道, 如果我们暴露一个可变属性出去, 然而别人就可以绕过你的API, 随意地修改该属性, 进行添加, 删除操作. 为了加强程序的鲁棒性, 我们应该对外公布一个不可变属性, 然后提供相应的方法给调用者操作该属性, 而内部修改对象时我们可以使用可变对象进行修改.
<pre>
// Person.h
@interface Person : NSObject
@property (nonatomic, strong, readonly) NSSet *friends;

// Person.m
@interface Person()
{
NSMutableSet *_internalFriends;
}

@end

@implementation Person
// 返回所有朋友

</pre>

这样别人拿到的永远是不可变的NSSet, 而且只能用你给的接口来操作这个set, 你内部依然是使用一个可变的NSMutableSet来做事情, 一举两得!

为了使我们的程序变得更加健壮, 我们应该尽量多声明readonly属性!

使用清晰而协调的命名方式

我们知道, OC的方法名总是很长, 长得跟句子一样, 好处很明显, 那就是一读就知道该方法是干嘛用的, 劣处嘛, 那就是麻烦了. 这里给几个方法命名规范

为私有方法加前缀

因为私有方法只在类内部调用, 不像外部方法, 修改会影响到面向外界的那些API, 对于私有方法来说可以随意修改. 所以为私有方法加前缀可以提醒自己哪些方法可以随意修改, 哪些不应轻易改动.

正确处理错误信息

不是有错就要抛出异常!!!只有在发生了可能致使应用程序崩溃的严重错误, 才使用异常. OC多使用代理和NSError对象来处理错误信息.
NSError对象封装了三条信息 :

理解NSCopying协议

巧了, 之前我写过一篇关于copy的文章, 这里就直接引用, 不在赘述了.
小结iOS中的copy


第4章 : 协议和分类

这一章讲的协议和分类都是两个需要重点掌握的语言特性.

通过委托和数据源协议进行对象间通信

委托(delegate), 我还是比较习惯叫代理, 下文就直接说代理了..

代理和数据源, 我们在哪里看到过? 没错, UITableView, UICollectionView.
无论是什么对象, 只要遵循了代理协议和数据源协议就都能当一个对象的代理和数据源. 苹果这么做完全是为了解耦和复用.

而使用代理的时候, 我们是不是总是写以下这些代码
<pre>
if ( [self.delegate respondsToSelector:@selector(someClassDidSomething:)] ) {
[self.delegate someClassDidSomething:self];
}
</pre>

那大家有没有想过, 如果这个方法调用得很频繁很频繁, 那么每次调用之前都要问问代理能不能响应这个方法, 不是很影响效率吗?

我们可以这样来优化程序效率 -> 把代理能否响应某个方法这一信息缓存起来

这里我们需要用到一个C语言的"位端数据类型". 我们可以把结构体中某个字段所占用的二进制个数设为特定的值
<pre>
struct data {
unsigned int fieldA : 8;
unsigned int fieldB : 4;
unsigned int fieldC : 2;
unsigned int fieldD : 1;
}
</pre>

以上代码表示fieldA只占用8个二进制位, dieldB占用4个, 如此类推. 那么我们可以根据此特性设计一个代理对象是否响应某代理方法的结构体
<pre>
@interface Person() {
struct {
unsigned int didEat : 1;
unsigned int didSleep : 1;
} _delegateFlags;
}
@end
</pre>

这时我们可以拦截setDelegate方法, 在该方法里面一次过把代理是否响应代理方法全部问个遍, 然后对号入座把各自的BOOL值赋值给\\_delegateFalgs结构体的对应变量中. 那么我们下次调用代理的相关方法之前就变得优雅多了, 如下:
<pre>
if ( _delegateFlags.didEat ) {
[self.delegate didEat:self];
}
</pre>

将类的实现代码分散到便于管理的数个分类之中

如果某个类方法太多, 整个类太臃肿了, 可以根据方法的功能用分类的思想跟方法集分个类, 划分成易于管理的小块.

总是为第三方类的分类名称加前缀

这种情况说起来比较抽象, 直接上代码, 例如你想要给NSString添加分类,
<pre>
@interface NSString (HTTP)

我们不应该像以上代码那么做, 因为苹果说不定哪一天会给NSString加上一个HTTP分类呢? 那么你就相当于复写了系统的分类了, 这是不允许的. 对应的方法也是, 我们应该为自己为第三方类的分类和方法名加上自己的专用前缀, 如下 :
<pre>
@interface NSString (JR_HTTP)

不要在分类中声明属性

除了"class-continuation分类"之外, 其他分类都无法向类中新增实例变量, 它们无法将实现属性所需的实例变量合成出来. 所以, 请不要在分类中声明属性.

分类的目的在于扩展类的功能, 而不是封装数据.

使用"class-continuation分类"隐藏实现细节

"class-continuation分类"与其他分类不同

"class-continuation分类"的作用 :

通过协议提供匿名对象

有时候对象的类型并不那么重要, 我们只需要保证他能满足我的需求即可, 不管他是什么类, 这时候可以使用协议来隐藏类的类型, 如下 :
<pre> @property (nonatomic, weak) id<JRDelegate> delegate </pre>

我们使用代理时总是这样, 为什么呢? 只要他遵循了这个协议, 我们甚至不用关心代理是什么, 阿猫阿狗都可以成为我的代理.

而字典中也是说明这一概念. 在字典中, 键的标准内存管理语义是"设置时拷贝", 而值的语义则是"设置时保留".
<pre> - (void)setObject:(id)object forKey:(id<NSCopying>)key </pre>

我们可以使用这一方法来屏蔽代理对象的实现细节, 使用者只需要这种对象实现了代理方法即可, 其他的你不需要管.


第5章 : 内存管理 与 第6章 : Block与GCD

不知不觉也写了差不多8千字了, 终于可以歇会了... 哇你千万不要以为下面的内容不重要. 相反, 他们太重要了, 我花了好多时间去研究内存管理和block, GCD. 还好, 这部分内容我之前已经总结过了, 刚好一一对应.


所以第5章和第6章我会用比较少的笔墨来写, 因为大部分的内容都已经在文章一开头所分享的3篇文章里涵盖了, 这里只把一些漏网之鱼补上.

在dealloc方法中只释放引用并解除监听

在这个方法里只释放指针, 解除KVO监听和NSNotificationCenter通知, 不要做其他耗时操作, 尤其是不要执行异步任务!

用"僵尸对象"调试内存管理问题

在Xcode - Scheme - Run - Diagnostics - 勾选 "Enable Zombie Objects"选项来开启僵尸对象.

开启之后, 系统在即将回收对象时, 会执行一个附加步骤, 把该对象转化为僵尸对象, 而不彻底回收. 这样你对僵尸对象发送消息后, 控制台会打印错误.

僵尸类 : 如果NSZombieEnabled变量已设置, 那么运行时系统会swizzle原来的dealloc方法, 转而执行另一方法, 将该类转换成_NSZombie_OriginalClass, 这里的OriginalClass是原类名.

用handler块降低代码分散程度

以前我们总是用代理来监听一个类内部发生的时. 例如一个下载器类, 下载完毕后通知代理, 下载出错时通知代理, 这个时候我们的代码是这样写的,
<pre>

pragma mark - JRDownloaderDelegate

这种办法没毛病, 也没错, 很好, 但是如果该类中的代理多了起来, 这个类就会变得十分臃肿, 我们可以使用block来写, 代码会更加紧致, 开发者调用起来也为方便.如下所示 :
<pre>

把completion handler块传递给start方法, 当方法调用完毕方法内部就会调用该block把data传进来. 这种办法是不是更加聪明呢~

然而我们再想一下, 终于给我们发现代理模式的一个缺点了! 假设我们要同时开启若干个下载器, 那么在代理方法里面是不是就要对各个下载器进行判断然后执行对应的操作呢? 很麻烦对吧, 一大堆判断, if, else, if, else. 然而handler的优势马上展现出来了.

<pre>

一目了然, 我们根本不需要对哪个下载器进行判断, 再处理响应的数据, 因为在创建下载器的时候已经设定好了.

而且我们还能用handler很easy地处理下载成功和失败的情况! 例如,
<pre>

除了这种设计模式以外, 还有一个就是把成功和失败都放在一个handler中来处理, 例如,
<pre>

这里说说各自的优缺点 :

两种方法都可以, 萝卜青菜各有所爱. 不过综上和结合苹果的API, 我建议用一个block来同时处理成功和失败.

补充 : 使用handler的好处还有, 可以通过传递多一个队列的参数, 指定该block在哪个队列上执行.

用块引用其所属对象时, 注意避免循环引用

<pre>
__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) {
[wself removeObserver: wself forKeyPath:@"dog"];
};
</pre>

这里我就不介绍__weak来避免循环引用了, 要说的是苹果称为"Strong-Weak Dance"的一个技术.

我们知道, 使用__weak确实可以避免循环引用. 但是还有点小瑕疵, 假如block是在子线程中执行, 而对象本身在主线程中被销毁了, 那么block内部的弱引用就会置空(nil). 而这在KVO中会导致崩溃.

Strong-Weak Dance就是针对以上问题的. 使用方法很简单, 只需要加一行代码
<pre>
__weak typeof(self) wself = self;
self.completionHandler = ^(NSInteger result) {
__strong typeof(wself) sself = wself;
[sself removeObserver: sself forKeyPath:@"dog"];
};
</pre>

这样一来, block中sself所指向的对象在block执行完毕之前都不会被释放掉, 因为在ARC下, 只要对象被强引用着, 就不会被释放.

这里推荐一篇文章, 对Strong-Weak Dance分析得很周到
对 Strong-Weak Dance 的思考


第7章 : 系统框架

我们之所以能够编写OS X和iOS的程序, 全是因为有系统框架在背后默默地支持着我们. 系统的框架非常强大, 以置于我们想要实现一些功能的时候, 可以不妨先找找系统有没有已经帮我们实现好的方法, 往往可以事半功倍.

多用块枚举, 少用for循环

for循环

用C语言写到OC, 我们再熟悉不过了
<pre>
NSArray *array = /* ... */
for (int i = 0; i < array.count; i++) {
id obj = array[i];
// do something with 'obj'
}
</pre>

快速遍历

OC 2.0的新特性, 语法简洁, 好用, 唯一的缺点就是没有索引
<pre>
NSArray *array = /* ... */
for (id obj in array) {
// do something with 'obj'
}
</pre>

用OC 1.0 的NSEnumerator来遍历

这种方法已经过时了, 这里不介绍.

基于块的遍历方式

NSArray, NSDictionary, NSSet都有基于block的遍历方式, 例如数组的 :
<pre>
NSArray *array = /* ... */
[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
// do something with 'obj'
if (shouldStop) {
*stop = YES;
}
}];
</pre>

不仅能简单遍历, 还能控制什么时候退出遍历. 还有更高级的块遍历方法能够指定选项, 例如反向遍历, 并行快速遍历等等.
<pre>
// 数组的方法

构建缓存时选用NSCache而非NSDictionary

NSCache是Foundation框架中为缓存而生的类. NSCache对比NSDictionary的优点

NSCache还经常会跟NSPurgeableData(NSMutableData的子类)搭配使用, 当NSPurgeableData对象所占内存被系统所丢弃时, 该对象自身也会从缓存中移除.

该类有2个特别的方法

缓存使用得当, 将会大大提高应用程序的响应速度. 但并不是什么东西都需要缓存, 只有那种"重新计算起来费劲"的数据, 才值得放入缓存中. 例如从网络获取或从磁盘读取的数据.

精简 initialize 与 load 的实现代码

<pre>+ (void)load </pre>

该方法特点如下 :

<pre>+ (void)initialize </pre>

该方法特点如下 :

回到主题, 为什么initialize 与 load的代码要尽量精简呢?

综上, 尽量不要重写load方法, 而initialize方法只应该用来

  1. 设置内部数据, 不应该调用其他方法, 哪怕是本类自己的方法.
  2. 如果单例类在首次使用之前需要做一些操作, ok, 在这里执行吧.

NSTimer会保留其目标对象

这种情况跟block引用self那种情况差不多. 目标对象保留计时器, 计时器反过来又保留对象, 则会导致循环引用.

我们可以利用block或者"Strong-Weak Dance"来解决此问题.


啰哩啰嗦地讲了好多好多, 也算是分享了自己的一点看法, 字有点多, 如果有人能耐心看完的话绝壁是真爱哈哈..其实写文章并不是要写给谁看, 是对自己语言总结能力, 书写能力的一种锻炼, 有句话说得好, 就算没有人看你的博客, 你也要坚持写下去. 共勉!


欢迎大家关注@Jerry4me, 我会不定时更新一些学习心得与文章.

上一篇 下一篇

猜你喜欢

热点阅读