基于 《Effective+Objective-C+2.0++编写高质量iOS与OS+X代码的52个有效方法》,借鉴、搬个砖,然后补个墙
对Objective-C 语言的发展史理解
1980 年代初,布莱德·考克斯(Brad Cox)在其公司 Stepstone 发明 Objective-C。Brad Cox 一直专注软件工程,软件重用性,组建化,这也是 ObjC 里面的核心思想,Brad 当时想打造一门流行的、可移植的 C 语言与 优雅的Smalltalk 的结合体,而 Smalltalk 是消息型语言的鼻祖。
ObjC 是根据 C语言 所衍生出来的语言,继承了 C语言 的特性,是扩充 C语言 的面向对象编程语言。
相较于开始于 1982 年的 C++ ,Obj-C 更是古老。Object-C 和 C++ 在成长过程中,吸收新生语言,同时相互借鉴。
所以在理解 OC 语言上,应该从 C语言 入手,而不是 C++,尤其要掌握 C语言 中的内存模型和指针。
//消息型语言
Object *obj = [Object new];
[obj performWith:parameter1 and:parameter2];
//函数型语言
Object *obj = new Object;
obj->preform(parameter1,parameter2);
Objective-C 为 C语言 了添加面向对象特性,是其超集。Objective-C 使用了动态绑定的消息结构,在运行时才会检查对象类,所以也叫运行时动态语言。
对xcode的配置理解
- Architectures(指令集)——设置你想支持的指令集
- Valid architectures : 指即将编译的指令集
- arm指令集和i386、x86_64指令集与真机和模拟器的关系
- Build Active Architecture Only : 是否只编译当前适用的指令集。
- iPhone OS Deployment Target:指的是编译出的程序将在哪个系统版本上运行
- bitcode 理解
- search paths 的理解
- ...
在类的头文件中尽量少引用其他头文件
虽然,由于 OC 中,#import 即使重复添加头,也不会造成编程错误,但重复的头文件,在运行和后期维护中会造成干扰。
@class MGDeviceModel;
@interface MGLocalManager : NSObject
@property (nonatomic,strong) MGDeviceModel *m_deviceModel;
一般来说,应在某个类的头文件中使用向前声明(@class语法)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低类之间的耦合。
有时无法使用向前声明,比如要声明某个类遵循一项协议。这种情况下,尽量把“该类遵循某协议”的这条声明移至“class-continutation分类”中。如果不行的话,就把协议单独放在一个头文件中,然后将其引入。
注:“class-continutation 分类”说的就是“扩展”。
个人理解:少引入无用头文件的作用:
减少程序编辑时间
降低类之间的耦合,使类更清晰,让类的使用者更容易理解
有效避免相互引用的问题
减少类修改维护代价
个人觉得:代码要简洁而精炼
多用字面量语法,少用与之等效的语法
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。
多用类型常量,少用#define预处理指令
//预处理指令
#define ANIMATION_DURATION 0.3
//常量定义
static const NSTimeInterval kAnimationDuration = 0.3;
//全局常量 头文件中 声明
extern NSString *const EOCStringConstant;
//全局常量 实现文件中 定义
NSString *const EOCStringConstant = @"VALUE";
少用预处理指令定义常量。因为这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找和替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这将导致应用程序中的常量值不一致。
在实现文件中使用 static const 来定义“只在编译单元内可见的常量”。由于此类常量不在全局符号表中,所有无须为其名称加前缀。
在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所有其名称要加以区隔,通常用与之相关的类名做前缀。
但要注意的是,#define 还可以用来定义方法,因为 #define 定义的方法或者常量,内存分配在栈上面,另外是在程序运行前,预编译在内存中,所以一定程度上,会加快编译速度,也是用内存空间换时间的一种方式。
用枚举表示状态、选项、状态码
//普通枚举
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
}
应用枚举来表示状态机的状态、传递给方法的选项及状态码等值,给这些值起个易懂的名字 ,这样会让程序易读性增加,统一逻辑,另外方便 switch 语句的调度。应避免用散乱的方式去对这种场景的逻辑处理。
另外,如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用,那么就将各选项的值定义为2的幂,以便通过按位或操作将其组合起来。
swift中,switch 还可以直接匹配字符串,这让这种方式写法变的更有趣。
理解“属性”这一概念
Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中,“获取方法(getter)”用于读取变量值,而“设置方法(setter)”用于写入变量值。
@synthesize语法:编译器会自动创建 get 和 set 方法
@implementation EOCPerson
@synthesize firstName = _myFirstName;
@end
@dynamic关键字:它会告诉编译器,不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。
@implementation EOCPerson
@dynamic firstName,lastName;
@end
原子性:默认atomic属性。可以通过锁定机制来确保 getter 方法操作的原子性。但是并不能保证“线程安全”。由于iOS中使用同步锁开销太大,一般只使用 nonatomic。
读/写权限:readonly 和 readwrite
getter=:指定getter的方法名。
@property (nonatomic, getter=isOn) BOOL on;
可以使用 @property 语法来定义对象中所封装的数据。
通过“特质”来定义存储数据所需的正确语义。
在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。
详细请看:iOS属性关键字
理解“对象等同性”这一概念
若想检测对象的等同性,请提供“isEqual”和hash方法。
相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同。
不要盲目地逐个检测每条属性,而是应该依照具体需求来制定检测方案。
编写 hash 方法时,应该使用计算速度快而且哈希码碰撞概率低的算法。
以“类族方式”隐藏实现细节
类族模式可以把实现细节隐藏在一套简单的公共接口后面。
系统框架中经常使用类族。
从类族的公共抽象基类中继承子类时要当心,若有开发文档,则应首先阅读。
类族,也是面向接口编程的体现,这对一个需要长期维护的大型项目,是非常重要的。
在既有类中使用关联对象存放自定义数据
关联类型 等效的 @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。
理解 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)。
发送给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。
理解消息转发机制
OC消息传递机制和消息转发机制
若对象无法响应某个选择子,则进入消息转发流程。
通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
对象可以把其无法解读的某些选择子转交给其他对象来处理。
经过上面两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制。
用“方法调配”技术调试“黑盒方法”
//方法交换
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),开发者常用此技术向原有实现中添加新功能。
一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。
这种方式,由于所有的类,都会加载 load 方法,所以,可以实现对类默认属性的修改,比如说修改 UILable 的默认字体、UIView 的默认背景等,这个用处还是非常多的,要挖掘。
【iOS 不常用技术解密之 hook】 也有提到这方面的应用
第14条:理解“类对象”的用意
Class定义
- isMemberOfClass:能够判断出对象是否为某个类的实例
- isKindOfClass:能够判断出对象是否为某类或其派生类的对象
每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法探知。
尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。
用前缀避免命名空间冲突
Objective-C 没有其他语言那种内置的命名空间机制。鉴于此,我们在起名时需要避免潜在的命名冲突。
避免此问题的唯一办法是变相实现命名空间:为所有名称都加上适当前缀。
选择与你的公司、应用程序或者二者皆有关联之名称作为类名的前缀,并在所有代码中均使用该前缀。
若自己开发的程序库中用到了第三方库,则应为其中的名称加上前缀。
这个是非常重要的,特别是在做 framework 和 library 的封装时,尤为重要
实现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 方法。
这其实应该属于编程习惯,就像写一个 shell 时,会加一个 help 方法
为私有方法名加前缀
给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开,有助于代码维护。
不要单用一个下划线做私有方法的前缀,因为这种做法是预留给苹果公司用的。
理解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对象里,经由“输出参数”返回给调用者。
理解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 协议。
复制对象时需决定采用浅拷贝还是深拷贝,一般情况下应该尽量 执行浅拷贝。
如果你所写的对象需要深拷贝,那么可考虑新增一个专门执行深拷贝的方法。
通过委托与数据源协议进行对象间通信
对象把应对某个行为的责任委托给另外一个类了。
常规的委托模式:信息从类流向受委托者(delegate)。
数据源模式:信息从数据源(Data Source)流向类。
信息流向
委托模式为对象提供了一套接口,使其可由此将相关事件告知其他对象。
将委托对象应该支持的接口定义为协议,在协议中把可能需要处理的事件定义成方法。
当某对象需要从另外一个对象获取数据时,可以使用委托模式。这种情境下,该模式亦称“数据源协议”。
若有必要,可实现含有位段的结构体,将委托对象是否能相应相关协议方法这一信息缓存至其中。
将类的实现代码分散到便于管理的数个分类之中
使用分类机制把类的实现代码划分多个管理的小块。
将应该视为“私有”的方法归入名叫Private的分类中,以隐藏实现细节。
以一个分类,实现某个功能,这种方式在 OC 的官方代码中,也是非常常用的方式
总是为第三方类名称加前缀
向第三方类中添加分类时,总应给其名称加上你专用的前缀。
向第三方类中添加分类时,总应给其中的方法名加上你专用的前缀。
勿在分类中声明属性
把封装数据所用的全部属性都定义在主接口里。
在“class-continuation分类”之外的其他分类中,可以定义存取方法,但尽量不要定义属性。
若是定义了,会非常不好用,只会自己坑自己;当然也有特殊情况,也是需要这种方式的
使用“扩展分类”隐藏实现细节
这里的“class-continuation分类”其实就是我们平常所说的“扩展”。
通过“class-continuation分类向类中新增实例变量。
如果某属性在主接口
中声明为“只读”,而类的内部又要用设置方法修改此属性
尽可能的暴露少的接口,方便其他人使用你的类
通过协议提供匿名对象
协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所对应实现的方法。
使用匿名对象来隐藏类型名称(或类名)。
如果具体类型不重要,重要的是对象能够相应(定义在协议里的)特性方法,那么可以使用匿名对象来表示。
在dealloc方法中只释放引用并解除监听
dealloc方法绝不能主动调用。
在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”(KVO)或 NSNotificationCenter 等通知,不要做其他事情。
如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和使用者约定:用完资源后必须调用 close 方法。
执行异步任务的方法不应在 dealloc 里调用;只能在正常状态下执行的那些方法也不应在 dealloc 里调用,因为此时对象已处于正在回收的状态了。
编写“异常安全代码”时留意内存管理问题
捕获异常时,一定要注意将 try 块内所创立的对象清理干净。
在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
以弱引用避免保留环
将某些引用设为 weak,可避免出现“保留环”。
weak 引用可以自动清空,也可以不自动清空。自动清空是随着 ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
以“自动释放池块”降低内存峰值
iOS系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行“事件循环”(event loop)时,就会将其清空。
自动释放池的范围:左括号到右括号({自动释放池范围})。在该范围内的对象,将会在末尾处收到release消息。
自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
合理运用自动释放池,可降低应用程序的内存峰值。
ARC 中,使用@autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。
用“僵尸对象”调试内存管理问题
系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量 NSZomebieEnabled 可开启此功能。
系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。
理解“ block ”这一概念
//块的语法结构
return_type (^block_name)(parameters)
块的强大之处是:在声明它的范围里,所有变量都可以为其所捕获。也就是说,那个范围内的全部变量,在块里依然可用。
如果块所捕获的变量是对象类型,那么就会自动保留它。
定义块的时候,其所占的内存区域是分配在栈中的。也就是说,块只在定义它的那个范围内有效。
为解决此问题,可给块对象发送copy消息以拷贝之。这样的话,就可以把块从栈复制到堆了。
全局块;不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。
块是C、C++、Objective-C中的语法闭包。
块可接受参数,也可返回值。
块可以分配在栈或堆上,也可以是全局的。分配在栈上的块可拷贝到堆里,这样的话,就和标准的 Objective-C 对象一样,具备引用计数了。
为常用的块类型创建 typedef
//块类型的语法结构:
return_type (^block_name)(parameters)
//typedef
typedef return_type(^block_name)(parameters);
以 typedef 重新定义块类型,可令块变量用起来更加简单和代码可读性。
定义新类型时应遵从现有的命名习惯,勿使其名称与别的类型向冲突。
不妨为同一个签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应typedef中的块签名即可,无须改动其他 typedef。
用 handler 块降低代码分散程度
委托模式有个缺点:如果累要分别使用多个获取器系在不同数据,那么就得在 delegate 回到方法里根据传入的获取器来切换。
在创建对象时,可以使用内联的 handler 块将相关业务逻辑一并声明。
在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,若改用 handler 块来实现,则可直接将块与相关对象放在一起。
设计 API 时如果用到了 handler 块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。
其实在出现 handler 后, 我基本不再使用 delegate 模式,定义可读性都不好;但如果有多个类,使用到同一个 delegate ,这种情况下,delegate 方式会让程序结构更清晰和完善。
用块引用其所属对象时不要出现保留环
如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。
一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。
内存的管理,应当尽量保证在当前类内,虽然这种方式会造成一定程度上的内存开销和使用,但更安全
多用派发队列,少用同步锁
派发队列可用来表示同步语义,这种做法要比使用 @synchronized块或NSLock对象更简单。
将同步与异步派发结合起来,可以实现与普通加锁机制一样的同步行为,而这么做却不会阻塞队列执行异步派发的线程。
使用同步队列及栅栏块,可以令同步行为更加高效。
多用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的相关方法来实现。
另外,从本身机制上来说,GCD 方式是 纯C 的API,在内存上和调度时间上,都优于 performSelector 的线程调度;所有,在任何时候都应该使用 GCD 实现线程的调度。
掌握GCD及操作队列的使用时机
NSOperationQueue 发者可以把操作以 NSOperation 子类的形式放在队列中,而这些操作也能够并发执行。
GCD是 纯C 的 API,而 NSOperationQueue 则是 Objective-C 的对象。
用 NSOperationQueue 类的 “addOerationWithBlock:” 方法搭配 NSBlockOperation 类来使用操作队列,其语法与纯GCD非常类似。
NSOperationQueue与NSOperation类的优点:
- 取消某个操作
- 指定操作间的依赖关系
- 通过KVO监控NSOperation对象的属性
- 指定操作的优先级
- 重用NSOperation对象
在解决多线程与任务管理问题时,派发队列并非唯一方案。
操作队列提供了一套高层的 Objective-C API,能实现纯 GCD 所具备的绝大部分功能,而且还能完成一些更为复杂的操作,那么操作若改用GCD来实现,则需另外编写代码。
通过 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 函数时,传进去的标记也实现相同的。
不要使用 dispatch_get_current_queue
dispatch_get_current_queue 函数的行为常常与开发者所预期的不同。此函数已经废弃,只应做调试之用。
由于派发队列是按层级来组织的,所以无法单用某个队列对象来描述“当前队列”这一概念。
dispatch_get_current_queue 函数用于解决由不可重入的代码所引发的死锁,然而能用此函数解决的问题,通常也能改用“队列特定数据”来解决。
多用块枚举,少用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 含有何种对象,则应修改块签名,指出对象的具体类型。
构建缓存时选用NSCache而非 NSDictionary 或者其他类
实现缓存时应选用 NSCache 而非 NSDictionary 对象等。因为 NSCache可以提供优雅的自动删减功能,而且是“线程安全的”,此外,它与字典不同,并不会拷贝键。
可以给 NSCache 对象设置上限,用以限制缓存中的对象总个数及“总成本”,而这些尺度则定义了缓存删减其中对象的时机。但是绝对不要把这些尺度当成可靠的“硬限制”,他们仅对 NSCache 起指导作用。
将 NSPurgeableData 与 NSCache 搭配使用,可实现自动清除数据的功能,也就是说,当 NSPurgeableData 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
如果缓存使用得当,那么应用程序的相应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些从网络获取或从磁盘读取的数据。