iOS 常用技术基础

基于 《Effective+Objective-C+2.0++编写高质量iOS与OS+X代码的52个有效方法》,借鉴、搬个砖,然后补个墙

coffee-heart-i.jpg

对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)。
发送给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。


理解消息转发机制

1818095-e7a25f32f80550ba.png

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条:理解“类对象”的用意

1818095-16f3c333126d16f0.png

Class定义

1818095-08172175a478dd08.png
  • 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 对象所占内存为系统所丢弃时,该对象自身也会从缓存中移除。
如果缓存使用得当,那么应用程序的相应速度就能提高。只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些从网络获取或从磁盘读取的数据。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,445评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,889评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,047评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,760评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,745评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,638评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,011评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,669评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,923评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,655评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,740评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,406评论 4 320
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,995评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,961评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,023评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,483评论 2 342

推荐阅读更多精彩内容