目标
最近在看一些iOS的进阶书籍,做一些简单的笔记来加深印象。
这次读的是《Effective+Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》。
注:20170719对文章做了一些修正
第1条:了解Objective-C语言的起源
Objective-C语言由Smalltalk语言演化而来,而Smalltalk是消息型语言的鼻祖。
//消息型语言
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
//函数型语言
Object *obj = new Object;
obj->preform(parameter1,parameter2);
- Objective-C为C语言了添加面向对象特性,是其超集。Objective-C使用了动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器决定。
- 理解C语言的核心概念有助于写好Objective-C程序。尤其要掌握内存模型和指针。
第2条:在类的头文件中尽量少引用其他头文件
- 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明(@class语法)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合。
- 有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continutation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
注:“class-continutation分类”说的就是“扩展”。
个人理解:少引入无用头文件的作用:
1)减少程序编辑时间
2)降低类之间的耦合,使类更清晰,让类的使用者更容易理解
3)有效避免相互引用的问题
第3条:多用字面量语法,少用与之等效的语法
NSNumber *someNumber = @(1);
NSArray *animals = @[@"dog",@"cat",@"mouse",@"badger"];
//取下标操作
NSString *dog = animal[1];
NSDictionary *personData = @{@"firstName":@"shi",@"lastName":@"xueqian",age:@(26)};
NSString *lastName = personData[@"lastName"];
//可变数组和字典
[mutableArray replaceObjectAtIndex:1 withObject:@"dog"];
[mutableDictionary setObject:@"xueqian" forKey:@"lastName"];
//可用字面量语法来替换
mutableArray[1] = @"dog";
mutableDictonary[@"lastName"] = @"xueqian";
- 应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。
- 应用通过取下标操作来访问数组下标或字典中的键所对应的元素。
- 用字面量语法创建数组或者字典时,若值中有nil,则会抛出异常。务必确保值里不含nil。
第4条:多用类型常量,少用#define预处理指令
//预处理指令
#define ANIMATION_DURATION 0.3
//常量定义
static const NSTimeInterval kAnimationDuration = 0.3;
//全局常量 头文件中 声明
extern NSString *const EOCStringConstant;
//全局常量 实现文件中 定义
NSString *const EOCStringConstant = @"VALUE";
- 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找和替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
- 在实现文件中使用static const 来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所有无须为其名称加前缀。
- 在头文件中使用extern来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所有其名称要加以区隔,通常用与之相关的类名做前缀。
第5条:用枚举表示状态、选项、状态码
//普通枚举
typedef NS_ENUM(NSUInteger, EOCConnectionState){
EOCConnectionStateDisconnected,
EOCConnectionStateConnecting,
EOCConnectionStateConnected,
};
//使用枚举作为参数时,switch语句最好不要加defalut分支
switch(_currentState) {
case:EOCConnectionStateDisconnected:
//干活
break;
case:EOCConnectionStateConnecting:
//干活
break;
case:EOCConnectionStateConnected:
//干活
break;
}
//二进制枚举
typedef NS_OPTIONS(NSUInteger, EOCPermittedDirection){
EOCPermittedDirectionUp = 1 << 0,
EOCPermittedDirectionDown = 1 << 1,
EOCPermittedDirectionLeft = 1 << 2,
EOCPermittedDirectionRight = 1 << 3,
}
//二进制枚举使用
EOCPermittedDirection direction = EOCPermittedDirectionUp | EOCPermittedDirectionDown;
if (direction & EOCPermittedDirectionUp){
//有设置 EOCPermittedDirectionUp
}
- 应用枚举来表示状态机的状态、传递给方法的选项及状态码等值,给这些值起个易懂的名字。
- 如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项的值定义为2的幂,以便通过按位或操作将其组合起来。
- 在处理枚类型的switch语句中不要出现default分支。这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举。
第6条:理解“属性”这一概念
Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,“获取方法(getter)”用于读取变量值,而“设置方法”(setter)用于写入变量值。
@synthesize语法:指定实例变量的名字(较少用):
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@end
@dynamic关键字:它会告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。
@implementation EOCPerson
@dynamic firstName,lastName;
@end
原子性:默认atomic属性。可以通过锁定机制来确保getter方法操作的原子性。但是并不能保证“线程安全”。由于iOS中使用同步锁开销太大,一般只使用nonatomic。
读/写权限:readonly和readwrite
getter=<name>:指定getter的方法名。
@property (nonatomic, getter=isOn) BOOL on;
- 可以使用@property语法来定义对象中所封装的数据。
2.通过“特质”来定义存储数据所需的正确语义。 - 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
- 开发iOS程序时应使用nonatomic属性,因为atomic属性会严重影响性能。
第7条:在对象内部尽量直接访问实例变量
- 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应使用属性来写。
- 在初始化方法和dealloc方法中,总是应该使用实例变量来读写数据。
- 有时会使用懒加载技术配置某份数据,这种情况下,需要使用属性来读取数据。
第8条:理解“对象等同性”这一概念
- 若想检测对象的等同性,请提供“isEqual”和hash方法。
- 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
- 不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
- 编写hash方法时,应该使用计算速度快而且哈希码碰撞概率低的算法。
第9条:以“类族方式”隐藏实现细节
- 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
- 系统框架中经常使用类族。
- 从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
第10条:在既有类中使用关联对象存放自定义数据
关联类型 | 等效的@property属性 |
---|---|
OBJC_ASSOCIATION_ASSIGN | assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC | nonatomic retain |
OBJC_ASSOCIATION_COPY_NONATOMIC | nonatomic copy |
OBJC_ASSOCIATION_RETAIN | retain |
OBJC_ASSOCIATION_COPY | copy |
//此方法以给定的键和策略为某对象设置关联对象值
void objc_setAssociatedObject(id object, void *key, id value, objc_associationPolicy policy)
//此方法通过给定的键从某对象中获取关联对象的值
void objc_getAssociatedObject(id object, void *key)
//此方法移除某对象的全部关联对象
void objc_removeAssociatedObject(id object)
- 可以通过“关联对象”机制把两个对象连起来。
- 定义关联对象时可指定内存管理语义,用以模仿定义属性时所有采用的“拥有关系”和“非拥有关系”。
- 只有在其他方法不可行时才应选用关联对象,因为这种用法通常会引入难以查找的bug。
第11条:理解objc_msgSend的作用
- C语言:C语言使用“静态绑定”,也就是说,在编译期就能决定运行时所调用的函数。
- Objective-C:所要调用的函数直到运行期才能确定。在底层,所有方法都是普通的C语言函数,然而对象收到消息之后,究竟该调用哪个方法完全由运行期决定,甚至可以在程序运行时改变,这些特性使得Objective-C成为一门真正的动态语言。
//给对象发送消息
id returnValue = [someObject messageName:parameter];
//objc_msgSend原型
void objc_msgSend(id self, SEL _cmd, ...)
//给对象发送消息底层
id returnValue = objc_msgSend(some Object, @selector(messageName:),parameter);
- 消息由接受者、选择子和参数组成。给某个对象“发送消息”(invoke a message)也就相当于在该对象上“调用方法”(call a method)。
- 发送给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
第12条:理解消息转发机制
- 若对象无法响应某个选择子,则进入消息转发流程。
- 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
- 对象可以把其无法解读的某些选择子转交给其他对象来处理。
- 经过上面两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
第13条:用“方法调配”技术调试“黑盒方法”
//方法交换
void method_exchangeImplementations(Method m1, Method m2)
//方法实现
Method class_getInstanceMethod(Class aClass, SEL aSelector)
//demo,交换lowercaseString和uppercaseString方法
Method originalMethod = class_getInstanceMethod([NSString class],
@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSString class], @selector(uppercaseString));
method_exchangeImplementations(originalMethod,swappedMethod);
- 在运行期,可以向类中新增或替换选择子所应用的方法实现。
- 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”(method swizzing),开发者常用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
第14条:理解“类对象”的用意
isMemberOfClass:能够判断出对象是否为某个类的实例
isKindOfClass:能够判断出对象是否为某类或其派生类的对象
- 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
- 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法探知。
- 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
第15条:用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间机制。鉴于此,我们在起名时需要避免潜在的命名冲突。
避免此问题的唯一办法是变相实现命名空间:为所有名称都加上适当前缀。
- 选择与你的公司、应用程序或者二者皆有关联之名称作为类名的前缀,并在所有代码中均使用该前缀。
- 若自己开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
第16条:提供“全能初始化方法”
- 在类中提供一个全能初始化方法,并与文档中指明。其他初始化方法均应调用此方法。
- 若全能初始化方法此超类不同,则需覆写超类中的方法。
- 如果超类的初始化方法不适用于子类,那么应覆写这个超类方法,并在其中抛出异常。
第17条:实现description方法
- (NSString *)description {
return [NSString stringWithFormat:@"<%@:%p %@",[self class], self, @{@"firstName":_firstName, @"lastName":_lastName}];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}
- (NSString *)debugDescription {
return [NSStrig stringWithFormat:@"< %@ ,%p %@ ,%@",[self class], self, _firstName, _lastName];
}
- 实现description方法返回一个有意义的字符串,用以描述该实例。
- 若想在调试时打印出更详尽的对象描述信息,则应实现debugDesription方法。
第18条:尽量使用不可变对象
这里指的可变是“readwrite”属性对象
不可变是“readonly”属性对象
- 尽量创建不可变的对象
- 若某属性仅可用于内部修改,则在“扩展”中将其由readonly属性扩展为readwrite属性。
- 不要把可变的collection作为属性公开,而应提供相关的方法,以此修改对象中的可变collection。
第19条:使用清晰而协调的命名方式
给方法命名时的注意事项:
- 如果方法的返回值是新创建的,那么方法名的首个词应该是返回值的类型,除非前面还有修饰语,例如localizedString。属性的存取方法不遵循这种命名方式,因为一般认为这些方法不会创建新对象,即便有时返回内部对象的一份拷贝,我们也认为那相当于原有的对象。这些存取方法应该按照其所对应的属性来命名。
- 应该把表示参数类型的名词放在参数前面。
- 如果方法要在当前对象上执行操作,那么就应该包含动词;若执行操作时还需要参数,则应该在动词后面加上一个或多个名词。
- 不要使用str这种简称,应该用string这样的全称。
- Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
- 将get这个前缀留给那些借由“输出参数”来保存返回值的方法,比如说,把返回值填充到“C语言式数组”里的那种方法就可以使用这个词做前缀。
- 起名时应遵从标准的Objective-C命名规范,这样创建出来的接口更容易为开发者所理解。
- 方法名要言简意赅,从左至右读起来要像个日常用语中的句子才好。
- 方法名里不要使用缩略后的类型名称。
- 给方法起名时的第一要务就是确保其风格与你自己的代码或所要集成的框架相符。
第20条:为私有方法名加前缀
- 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开。
- 不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
第21条:理解Objective-C错误模型
NSError对象里封装了三条信息:
- Error domain:错误发生的范围,其类型为字符串,通常用一个特有的全局变量来定义。
- Error code:独有的错误代码,其类型为整数。用以指明在某个范围内具体发生了何种错误,通常采用enum来定义。
- User info:用户信息,其类型为字典。有关此错误的额外信息,其中或许包含一段“本地化描述”。
- (BOOL)doSomething:(NSError **)error {
if (/* there was an error */) {
if (error) {
*error = [NSError errorWithDomain:domain code:code userInfo:userInfo];
}
return NO;
else {
return YES;
}
}
NSError *error = nil;
BOOL ret = [object doSomethig:&error];
if (error) {
}
- 只有发生了可使整个应用程序崩溃的严重错误时,才应使用异常。
- 在错误不那么严重的情况下,可以指派“委托方法”来处理错误,也可以把错误信息放在NSError对象里,经由“输出参数”返回给调用者。
第22条:理解NSCopying协议
//一个类支持拷贝功能需要实现 NSCopying协议只有这一个方法。其中NSZone目前只有一个默认值,可不管。
- (id)copyWithZone:(NSZone *)zone
- (id)copyWithZone:(NSZone *)zone {
EOCPerson *copy = [[[self class] allocWithZone:zone] initWithFirstName:_firstName andLastName:_lastName];
return copy;
}
- 若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。
- 如果自定义的对象分为可变版本与不可变版本,那么就要同时实现NSCopying与NSMutableCopying协议。
- 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量 执行浅拷贝。
- 如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
第23条:通过委托与数据源协议进行对象间通信
对象把应对某个行为的责任委托给另外一个类了。
常规的委托模式:信息从类流向受委托者(delegate)。
数据源模式:信息从数据源(Data Source)流向类。
- 委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
- 将委托对象应该支持的接口定义为协议,在协议中把可能需要处理的事件定义成方法。
- 当某对象需要从另外一个对象获取数据时,可以使用委托模式。这种情境下,该模式亦称“数据源协议”。
- 若有必要,可实现含有位段的结构体,将委托对象是否能相应相关协议方法这一信息缓存至其中。
第24条:将类的实现代码分散到便于管理的数个分类之中
- 使用分类机制把类的实现代码划分到便于管理的小块。
- 将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
第25条:总是为第三方类名称加前缀
- 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
- 向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
第26条:勿在分类中声明属性
- 把封装数据所用的全部属性都定义在主接口里。
- 在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
第27条:使用“class-continuation分类”隐藏实现细节
注:这里的“class-continuation分类”其实就是我们平常所说的“扩展”。
- 通过“class-continuation分类”向类中新增实例变量。
- 如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”。
- 把私有方法的原型声明在“class-continuation分类”里面。
- 若想使类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。
第28条:通过协议提供匿名对象
- 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所对应实现的方法。
- 使用匿名对象来隐藏类型名称(或类名)。
- 如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特性方法,那么可以使用匿名对象来表示。
第29条:理解引用计数
从Mac OS X 10.8开始,“垃圾收集器”已经正式废弃了,以Objective-C代码编写Mac OS X程序时不应再使用它,而iOS则从未支持过垃圾收集。
- Retain 递增保留计数
- release 递减保留计数
- autorelease 待稍后清理“自动释放池”时,再递减保留计数。
调用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”时递减。
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
第30条:以ARC简化引用计数
不能在ARC模式下调用
retain
,release
,autorelease,
dealloc方法 以
alloc,
new,
copy,
mutableCopy``开头的方法,返回的对象归调用者所有(返回的对象保留计数会+1)。
- 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
- ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放“操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
- ARC只负责管理Objective-C对象的内存。尤其要注意:CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRealease。
第31条:在dealloc方法中只释放引用并解除监听
dealloc方法绝不能主动调用。
- 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或NSNotificationCenter等通知,不要做其他事情。
- 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和使用者约定:用完资源后必须调用close方法。
- 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。
第32条:编写“异常安全代码”时留意内存管理问题
- 捕获异常时,一定要注意将try块内所创立的对象清理干净。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
第33条:以弱引用避免保留环
- 将某些引用设为weak,可避免出现“保留环”。
- weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
第34条:以“自动释放池块”降低内存峰值
iOS系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。
自动释放池的范围:左括号到右括号({自动释放池范围})。在该范围内的对象,将会在末尾处收到release消息。
- 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。
第35条:用“僵尸对象”调试内存管理问题
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZomebieEnabled可开启此功能。
- 系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。
第36条:不要使用retainCount
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌。
- 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。
第37条:理解“块”这一概念
//块的声明语法结构
return_type (^block_name)(parameters)
//块的定义语法结构:
^ return_type (parameters){函数体},其中``return_type``和``parameters``都可以省略。即^{}
块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围内的全部变量,在块里依然可用。
如果块所捕获的变量是对象类型,那么就会自动保留它。
定义块的时候,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内有效。
为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。
全局块;不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。
- 块是C、C++、Objective-C中的语法闭包。
- 块可接受参数,也可返回值。
- 块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的Objective-C对象一样,具备引用计数了。
第38条:为常用的块类型创建typedef
//块类型的语法结构:
return_type (^block_name)(parameters)
//typedef
typedef return_type(^block_name)(parameters);
- 以typedef重新定义块类型,可令块变量用起来更加简单。
- 定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型相冲突。
- 不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他typedef。
第39条:用handler块降低代码分散程度
委托模式有个缺点:如果类要分别使用多个获取器下载不同数据,那么就得在delegate回到方法里根据传入的获取器来切换。
- 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
- 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用handler块来实现,则可直接将块与相关对象放在一起。
- 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
第40条:用块引用其所属对象时不要出现保留环
- 如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
- 一定要找个适当的时机解除保留环,而不能把责任推给API的调用者。
第41条:多用派发队列,少用同步锁
- 派发队列可用来表示同步语义,这种做法要比使用@synchronized块或NSLock对象更简单。
- 将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞队列执行异步派发的线程。
- 使用同步队列及栅栏块,可以令同步行为更加高效。
第42条:多用GCD,少用performSelector系列方法
//可以在运行时调用方法
- (id)performSelector:(SEL)selector
//可带一个参数
- (id)performSelector:(SEL)selector withObject:(id)object
//可带两个参数
- (id)performSelector:(SEL)selector withObject:(id)object withObject:(id)object
//可延时执行方法
- (void)performSelector:(SEL)selector withObject:(id)argument afterDelay:(NSTimeInterval)delay
//可放到另一个线程中执行
- (void)performSelector:(SEL)selector onThread:(NSThread *)thread withObject:(id)argument waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)selector withObject:(id)argument waitUntilDone:(BOOL)wait
//延后执行方法的两种实现方式:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:5.0];
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^(void){
[self doSomething];
});
//把任务放在主线程执行的两种方式
[self performSlectorOnMainThread:@selector(doSomething) withObject:nil waitUntilDone:NO];
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
- performSelector系列方法在内存管理方面容易有疏失。它无法确定将要执行的选择子具体是什么,因而ARC编译器也就无法插入适当的内存管理方法。
- performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都受到限制。
- 如果想把任务放在另一个线程上执行,那么最好不要用performSelector系列方法,而是应该把任务封装到块里,然后调用GCD的相关方法来实现。
第43条:掌握GCD及操作队列的使用时机
NSOperationQueue,开发者可以把操作以NSOperation子类的形式放在队列中,而这些操作也能够并发执行。
GCD是纯C的API,而NSOperationQueue则是Objective-C的对象。
用NSOperationQueue类的“addOerationWithBlock:”方法搭配NSBlockOperation类来使用操作队列,其语法与纯GCD非常类似。
NSOperationQueue与NSOperation类的好处如下:
- 取消某个操作。
- 指定操作间的依赖关系。
- 通过KVO监控NSOperation对象的属性。
- 指定操作的优先级。
- 重用NSOperation对象。
- 在解决多线程与任务管理问题时,派发队列并非唯一方案。
- 操作队列提供了一套高层的Objective-C API,能实现纯GCD所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那么操作若改用GCD来实现,则需另外编写代码。
第44条:通过Dispatch Group机制,根据系统资源状况来执行任务
dispatch group是GCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。
//创建dispatch group
dispatch_group_t dispatch_group_create();
//把任务编组(普通dispatch_async的变体)
void dispatch_group_asunc(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
//指定任务所属的dispatch_group
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
//等待dispatch_group执行完毕(timeout可以取常量DISPATCH_TIME_FOREVER,表示函数一致等待dispatch_group执行完,而不会超时)
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
//等待dispatch_group执行完毕之后执行block
void dispatch_group_notify(dispatch_group_t group, dispatch_queue_t queue, dispatch_block_t block);
- 一系列任务可归入一个dispatch group之中。开发者可以在这组任务执行完毕时获得通知。
- 通过dispatch group,可以在并发派发队列里同时执行多项任务。此时GCD会根据系统资源状况来调度这些并发执行的任务,开发者若自己来实现此功能,则需要编写大量代码。
第45条:使用dispatch_once来执行只需运行一次的线程安全代码
void dispatch_once (dispatch_once_t *token, dispatch_block_t block);
+ (id)sharedInstance {
static EOCClass *sharedInstance = nil;
static icdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
使用dispatch_once可以简化代码并且彻底保证线程安全,开发者根本无需担心加锁或同步。
由于每次调用时都必须使用完全相同的标记,所以标记要声明成static。
把该变量定义在static作用域中,可以保证编译器在每次执行sharedInstance方法时都会复用这个变量,而不会创建新变量。
- 经常需要编写“只需执行一次的线程安全代码”。通过GCD所提供的dispatch_once函数,很容易就能实现此功能。
- 标记应该声明在static或global作用域中,这样的话,把只需执行一次的块传给dispatch_once函数时,传进去的标记也实现相同的。
第46条:不要使用dispatch_get_current_queue
- dispatch_get_current_queue函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
2.由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。 - dispatch_get_current_queue函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
第47条:熟悉系统框架
将一系列代码封装为动态库,并在其中放入描述其接口的头文件,这样做出来的东西就叫框架。有时为iOS平台构建的第三方框架所使用的是静态库,这是因为iOS应用程序不允许在其中包含动态库。这些东西严格来讲并不是真正的框架,然而也经常视为框架。不过,所有iOS平台的系统框架仍然使用动态库。
在为Mac OS X或iOS系统开发“带图形界面的应用程序”时,会用到名为Cocoa的框架,在iOS上成为Cocoa Touch。其实Cocoa本身并不是框架,但是里面集成了一批创建应用程序时经常会用到的框架。
- 许多系统框架都可以直接使用。其中最重要的是Fundation与CoreFoundation,这两个框架提供了构架应用程序所需的许多核心功能。
- 很多常见任务都能用框架来做。例如音频与视频处理、网络通信、数据管理等。
- 请记住:用纯C写成的框架与用Objective-C写成的一样重要,若想成为优秀的Objective-C开发者,应该掌握C语言的核心概念。
第48条:多用块枚举,少用for循环
//for循环遍历
NSArray *arr1 = @[@1,@2,@3,@4,@5];
for (int i = 0; i < arr1.count; ++i) {
NSLog(@"arr1[i]=%@",arr1[i]);
}
//for循环反向遍历
for (NSInteger i = arr1.count-1; i >= 0; --i) {
NSLog(@"arr1[i]=%@",arr1[i]);
}
//NSEnumerator遍历法
NSArray *arr1 = @[@1,@2,@3,@4,@5];
NSEnumerator *enumerator = [arr1 objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
NSLog(@"object=%@",object);
}
//NSEnumerator遍历法反向遍历
NSEnumerator *reverseenu = [arr1 reverseObjectEnumerator];
id object1;
while ((object1 = [reverseenu nextObject]) != nil) {
NSLog(@"object1=%@",object1);
}
//快速遍历法
NSArray *arr1 = @[@1,@2,@3,@4,@5];
for (NSObject *obj in arr1) {
NSLog(@"obj=%@",obj);
}
//快速遍历法反向遍历
for (NSObject *obj1 in [arr1 reverseObjectEnumerator]) {
NSLog(@"obj1=%@",obj1);
}
//块枚举法
NSArray *arr1 = @[@1,@2,@3,@4,@5];
[arr1 enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"idx=%zd,obj=%@",idx,obj);
}];
//块枚举法反向遍历
[arr1 enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"idx=%zd,obj=%@",idx,obj);
}];
- 遍历collection有四种方式。最基本的办法是for循环,其次是NSEnumerator遍历法及快速遍历法,最新、最先进的方式则是“块枚举法”。
- “块枚举法”本身就能通过GCD来并发执行遍历操作,无须另行编写代码。而采用其他遍历方式则无法轻易实现这一点。
- 若提前知道待遍历的collection含有何种对象,则应修改块签名,指出对象的具体类型。
第49条:对自定义其内存管理语义的collection使用无缝桥接
Foundation框架中的Objective-C类所具备的某些功能,是CoreFoundation框架中的C语言数据接口所不具备的,反之亦然。
__bridge:ARC仍然具备这个Objective对象的所有权。
__bridge_retained:ARC将交出对象的所有权。
__bridge_transfer:C转化为OC
- 通过无缝桥接技术,可以在Foundation框架中的Objective-C对象与CoreFoundation框架中的C语言数据结构之间来回转换。
- 在CoreFoundation层面创建collection时,可以指定许多回调函数,这些函数表示此colection应如何处理其元素。然后,可运用无缝桥接技术,将其转换成具备特殊内存管理语义的Objective-C collection。
第50条:构建缓存时选用NSCache而非NSDictionary
- 实现缓存时应选用NSCache而非NSDictionary对象。因为NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
- 可以给NSCache对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”,他们仅对NSCache起指导作用。
- 将NSPurgeableData与NSCache搭配使用,可实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
- 如果缓存使用得当,那么应用程序的相应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。
第51条:精简initialize与load的实现代码
当程序启动的时候,类和分类,必定会调动且仅调用一次load方法。
先调用类的load方法,再调用分类的load方法。
先调用超类的load方法,再调用子类的load方法。
无法判断出各个类的载入顺序。
load方法需要实现得精简一些,因为整个应用程序会在执行load方法时都会阻塞。
initialize方法会在程序首次用该类之前调用,且只调用一次。
initialize是“懒加载”的,如果某个类一直都没有使用,就不会执行该类的initialize方法。
initialize方法可以安全使用并调用任意类中的任意方法。
initialize方法只应该用来设置内部数据,不应该在其中调用其他方法。
- 在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类中的先调用。与其他方法不同,load方法不参与覆写机制。
- 首次使用某个类之前,系统会向其发送initialize消息。由于此方法遵从普通的覆写规则,所以通常应该在里面判断当前要初始化的是哪个类。
- 无法在编译器设定的全局变量,可以放在initialize方法里初始化。
第52条:别忘了NSTimer会保留其目标对象
NSTimer对象会保留其目标,也就是说, NSTimer对象会对目标对象进行强引用。
一次性计时器:只执行一次任务,之后自动失效
重复执行模式:重复执行任务,必须自己调用invalidate方法,才能令其停止。
只有把计时器放在运行循环里,它才能正常触发任务。
- NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计时器在触发完成任务之后也会失效。
- 反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环。这种环保留关系,可能是直接发生的,也可能是通过对象图里的其他对象发生的。
- 可以扩充NSTimer的功能,用“块”来打破保留环。不过,除非NSTimer将来在公共接口里提供此功能,否则必须创建分类,将相关实现代码加入其中。