熟悉 Objective-C
一、OC的起源
OC的方法(本质上讲是消息发送)在运行时决定。使用函数调用的语言,由编译器决定。如果涉及多态,则用到虚函数表。
二、降低类之间的耦合
在类的头文件里尽量少的的引入其他头文件,或者将引入其他头文件的时机延后(使用@class),@class 也被称为向前声明。这样可以减少编译时间。
如果在各自头文件引入对方的头文件,还对导致循环引用(chicken-and-egg situation)。
使用#import
而非#include
指令虽然不会导致死循环,但是也会导致其中一个类无法正确编译。
有时无法使用向前声明的时候,比如要声明某各类遵循一项协议,尽量把该类遵循某协议的这条声明移动至分类中,或者是把协议单独放在一个头文件中,然后将其引入。
三、多用字面量语法,少用与之等价的方法。
如:使用 NSString *str = @"HHHH"
代替 NSString *str = [[NSString alloc] initWithString: @"str"]
等等。
注意:向数组或者字典中插入 nil
时,如果使用字面量会直接报错,如果是使用初始化方法,则 nil
以后的对象被忽略。
四、多用类型常量,少用#define预处理指令
#define ANIMATION_DURATION 0.3
static const NSTimerInterval kAnimationDuration = 0.3
- 不在头文件里面声明预处理命令,防止被别的文件引用,
static const
也不能用 - 在
.m
文件里面声明的变量,需要加上 static,不然会产生外部符号,重复的外部符号会导致编译错误 - const保证了所声明的变量不能被修改。static则可以用来保证只在编译单元内可见。
- 用
static const
定义只在类内部可见的常量 - 用
extern
定义全局常量,并使用类名作为前缀,但是尽量少使用extern
- 使用
FOUNDATION_EXPORT
比extern
更好
五、用枚举值表示状态、选项、状态码
- 指定枚举变量的类型
1 << 0: 按位左移,即1的二进制向左移动0位,结果还是1
M << N: 将M的二进制左移N位,即M乘以2的N次方
M >> N: 是M除以2的N次方
&: 按位与
|: 按位或
typedef NS_ENUM(NSInteger, Direction) {
left = 1
right = 2
up = 3
down = 4
};
对象、消息、运行期
六、属性相关
属性:是OC的一项特性,用于封装对象中的数据;OC对象通常会把其所需要的数据保存为各种实例变量。
@property
的作用是合成存取方法
@synthesize
可以给属性生成的实例变量改名
@dynamic
不会自动创建实现属性的实例变量,也不会为其创建存取方法
nonatomic
原子性
readwrite/readonly
读写权限
assign/strong/weak/copy/
内存管理语义
getter=<name> / setter=<name>
方法名
七、实例变量和属性
- 在对象内部尽量直接访问实例变量,在写入数据时则应该通过属性写入;
- 在初始化方法和dealloc方法中,总是应该通过实例变量的方式来读写数据;
- 懒加载的时候,需要通过属性来读写数据(
getter
)
Tips:直接访问实例变量会比较快,绕过了OC的method dispatch,也不会触发KVO
八、理解"对象等同性"这一概念 --- isEqual
一般认为:当且仅当NSObject
类其"指针值"完全相等时,这两个对象才相等。
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
对象相等时,这两个等式都成立
如果把一个对象放入到set
之后又修该对象的内容,那么可能破坏set
的一些特性。如:
NSMutableSet *set = [NSMutableSet new];
NSMutableArray *arr01 = [@[@1, @2] mutableCopy];
[set addObject: arr01]; // set {((1, 2))}
NSMutableArray *arr02 = [@[@1] mutableCopy];
[set addObject: arr02]; // set {((1), (1, 2))}
[arr02 addObject: @2]; // set {((1, 2), (1, 2))} set元素的不重复性则被破坏了
NSSet *setB = [set set copy]; // setB {((1, 2))} copy之后setB正常了
所以,要么确保对象的哈希值不依赖内部可变的状态,要么确保依赖的状态不会改变。
九、以"类族模式"隐藏实现细节 类族(class cluster)
也就是工厂模式(Factory pattern)
实现方式如下:
typedef NS_ENUM(NSUInteger, EmployeeType) {
EmployeeTypeDeveloper,
EmployeeTypeDesigner,
EmployeeTypeFinance,
};
@interface Employee: NSObject
@property (copy) NSStirng *name;
@property (assign) NSUInteger salary;
+ (Employee *)employeeWithType:(EmployeeType)type;
- (void)doWork;
@end
@implementation: Employee
+ (Employee *)employeeWithType:(EmployeeType)type {
switch (type) {
case EmployeeTypeDeveloper:
return [EmployeeTypeDeveloper new];
break;
case EmployeeTypeDesigner:
return [EmployeeTypeDesigner new];
break;
case EmployeeTypeFinance:
return [EmployeeTypeFinance new];
break
}
}
- (void)doWork {
// subclass implement
}
@end
@interface EmployeeTypeDeveloper:: Employee
@end
@implementation: EmployeeTypeDeveloper
- (void)doWork {
[self writeCode];
}
@end
- 子类应该继承自类族中的抽象基类
- 子类应该定义自己的数据存储方法
- 子类应该重写
(overwrite)
父类文档中指明要重写的方法 - 类族模式可以把实现细节隐藏在一套简单的公共接口后面
- 系统框架中经常使用类族
十、在既有类中使用关联对象存放自定义数据Associated Object
管理关联对象
1、以给定的键和策略为某对象设置关联对象值
void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
2、根据给定的键从某对象中获取相应的关联对象值
id objc_getAssociatedObject(id object, void *key)
3、移除指定对象的全部关联对象
void objc_removeAssociatedObjects(id object)
eg: 如果在一个类中处理多个警告信息视图:
UIAlertView *alert = [[UIAlertView alloc] initWithTitle: @"title" message: @"message" delegate: self cancelButtonTitle: @"cancel" otherButtonTitles: @"continue", nil];
void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {
} else {
}
};
objc_setAssociatedObject(alert, AlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, AlertViewKey);
block(buttonIndex);
}
- 可以通过关联对象将两个对象连起来
- 定义关联对象时可以指定内存管理语义,用以模仿定义属性时所采用的"拥有关系"与"非拥有关系"
- 尽量少使用关联对象
十一、理解objc_msgSend的作用
消息包括 "name"、"selector"
可以接受参数,而且还可以有返回值。
给对象发消息可以这样写:
id returnValue = [someObject messageName:parameter]
someObject
叫做"接收者(receiver)
"; messageName
是selector
,selector
和参数合起来称为message
。编译器看到此消息后,将其转换为一个标准的C语言函数调用,所调用的函数是消息传递机制中的核心函数,叫做
objc_msgSend:void objc_msgSend(id self, SEL cmd, ...)
第一个参数代表接收者;第二个指的是selector
;然后编译器会把刚才那个例子找那个的消息转换为如下函数:
id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);
objc_msgSend
函数会依据接收者与selector
的类型来调整适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其方法列表(list of methods)
,如果能找到与selector
名称相符的方法,就跳转至其实现代码。如果找不到就会沿着继承继续向上查找。如果最终还是找不到相符的方法,那就执行消息转发(message forwarding)
操作。
- 消息由接收者、
selector
及参数构成 - 给对象发消息就是在该对象上调用方法
- 发给某对象的全部消息都要由"动态消息派发系统"
(dynamic message dispatch system)
来处理,该系统会查出对应的方法,并执行其代码
十二、理解消息转发机制
在编译期向类发送了无法解读的消息并不会报错,因为在运行时我们可以继续向类中添加方法,当对象接收到无法解读的消息之后,就会启动消息转发机制(message forwarding)
,我们可以在此时告诉对象应该如何处理未知消息。
消息转发分为三个阶段:
- 第一个阶段,先征询接收者所属的类,看其能否动态添加方法,以处理当前这个"未知的
selector
",这叫做动态方法解析(dynamic method resolution)
- 第二阶段,当接收者无法增加方法来响应该消息,那么先让接收者看看有没有其他对象能处理这条消息;若有,系统会将消息转给那个对象,消息转发过程结束
- 若没有,则启动完整的消息转发机制,运行系统会把与消息有关的全部细节都封装到
NSInvocation
对象中,在给接收者最后一次机会,另其设法解决当前还未处理的消息。
动态方法解析:
对象在收到无法解读的消息之后,先会调用下面的方法(使用该方法的前提是:相关的方法的实现代码已经写好,只等着运行的时候动态的插在类里面就可以了,此方法常用来@dynamic
属性)
+ (BOOL)resolveInstanceMethod:(SEL)selector
当前接收者还有第二次机会来处理该selector
,系统会让接收者看看有没有其他对象能处理这条消息,下面方法返回的是接收者找到的能处理该消息的对象,有就返回对象,没有就返回nil
- (id)forwardingTargetForSelector:(SEL)selector
若没有能够处理的对象,则创建一个NSInvocation
对象把该消息有关的全部细节都封存。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)invocation;
- 若对象无法响应某个
selector
,则进入消息转发流程 - 通过运行时额动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中
- 对象可以把无法响应的
selector
转交给其他对象来处理 - 如果上述之后,还没办法处理
selector
,那就启动完整的消息转发机制
十三、用"方法调配技术"调试"黑盒方法"
方法调配技术:method swizzling
类的方法列表会把selector
的名称映射到相关的方法实现之上,使得"动态消息派发系统"能够据此找到应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做
IMP:id(* IMP)(id, SEL, ...)
交换两个方法的实现:
void method_exchangeImplementations(Method m1, Method m2)
上述函数中的方法可以通过以下函数获得:
Method class_getInstanceMethod(Class aClass, SEL aSelector)
- 在运行时,可以向类中新增或者替换selector所对应的方法实现
- 使用另一份实现来替换原有的方法实现,叫做方法调配,我们可以使用此技术向原有实现中添加新功能。
- 一般来说,只有调试程序的时候才需要在运行时修改方法实现
十四、理解类对象的用意
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;
};
此结构体存放类的元数据"metadata
"、例如类的实例实现了几个方法,具备多少个实例变量等信息。结构体的第一个变量也是isa
指针,说明了Class
本身就是Object-C
对象。类对象所属的类型是另外一个类,叫做元类metaclass
。用来表述类对象本身所具备的元数据。每一个类就只有一个类对象,每个类对象只有一个元类。
-
-isMemberOfClass
能够判断出对象是否为某个特定类的实例 -
-isKindOfClass
能够判断出对象是否为某类或其派生类的实例
isMemberOfClass/isKindOfClass 具体的分析可见另一篇文章 isKindOf vs isMemberOf
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]]; // NO
[dict isMemberOfClass:[NSMutableDictionary class]]; // YES
[dict isKindOfClass:[NSDictionary class]]; // YES
[dict isKindOfClass:[NSArray class]]; // NO
- 类型信息查询方法:
isMemberOfClass
、isKindOfClass
- 每个实例都有一个指向
Class
对象的指针,用以表明其类型,而这些Class
对象则构成了类的继承体系 - 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知
- 不要直接比较类对象
接口与 API 设计
十五、用前缀避免命名空间冲突
- 全局函数和变量需要注意避免冲突
- 在自己开发的库中,为用到的第三方库添加前缀
十六、提供"全能初始化方法"
一般创建一个类的时候,我们尽可能的会提供比较多的初始化方法,但是最好提供一个全能的初始化方法,让其他初始化方法都调用此方法实现
eg:NSDate
- (id)init;
- (id)initWithString:(NSString *)string;
- (id)initWithTimeIntervalSinceNow:(NSTimeInterval)seconds;
- (id)initWithTimeIntervalSinceReferenceDate:(NSTimeInterval)seconds;
initWithTimeIntervalSinceReferenceDate
就是一个全能的初始化方法,这些初始化方法都会调用这个方法。
十七、实现description方法
实现模型的description
方法,在控制台打印log
的时候才会显示出打印的属性
- 正常
NSLog
需要实现description
- 在控制台里
po
,需要实现debugDescription
十八、尽量使用不可变对象
十九、使用清晰而协调的命名方式
- 如果返回值是新建的,首个词是返回值类型
- 不要使用
str
这种简称,使用string
-
get
前缀仅在由“输出参数”保存返回值的方法中使用
二十、为私有方法名添加前缀
为私有方法名添加前缀,但是不要以一个下划线作为前缀,这是苹果自己预留的方法,将私有方法和共有方法区别用过命名方式开来。
二十一、理解OC的错误模型
- 只有在发生了可以使整个应用程序崩溃的严重错误时,才应该使用异常
- 一般情况下使用
NSError
,比如封装在delegate
方法里,返回给调用者 -
NSError
对象封装了三条信息:
1、Error domain
(错误范围,其类型为字符串);
2、Error code
(错误码,其类型为整数);
3、userInfo
(用户信息,其类型为字典)
二十二、理解NSCopying协议
想要使自己的类支持copy
操作,就要实现NSCopying
协议:----返回一个不可变的copy版本
- (id)copyWithZone:(NSZone *)zone
OC
中使用->语法,是因为修饰的是实例变量而不是属性
NSMutableCopying
协议----返回一个可变的copy版本
- (id)mutableCopyWithZone:(NSZone *)zone
可变copy
和不可变copy
:
[NSMutableArray copy] => NSArray
[NSArray copy] => NSMutableArray
-
deep copy
: 在拷贝对象自身时,将其底层数据也一并复制过去;深copy
之后内容所指向的对象是原始内容中相关对象的一份copy
-
shallow copy
: 浅copy
之后的内容与原始内容均指向相同的对象
一般情况下,大部分执行了NSCopying协议的对象的copy都是执行的浅copy,如果需要执行深copy需要自己来编写相关的方法
。
对象的copy
:
- 深
copy
: 直接copy
整个对象到另一块内存中(内容拷贝) - 浅
copy
: 不复制对象本身,仅仅是copy
指向对象的指针(指针拷贝)
集合的浅复制有非常多种方法。当你进行浅复制时,会向原始的集合发送retain
消息,引用计数加1,同时指针被拷贝到新的集合。
NSArray *shallowCopyArray = [someArray copyWithZone:nil];
NSSet *shallowCopySet = [NSSet mutableCopyWithZone:nil];
NSDictionary *shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:NO];
集合的深复制:
NSDictionary shallowCopyDict = [[NSDictionary alloc] initWithDictionary:someDictionary copyItems:YES];
- 如果你用这种方法深复制,集合里的每个对象都会收到
copyWithZone
: 消息。 - 如果集合里的对象遵循
NSCopying
协议,那么对象就会被深复制到新的集合。 - 如果对象没有遵循
NSCopying
协议,而尝试用这种方法进行深复制,会在运行时出错。
copyWithZone
: 这种拷贝方式只能够提供一层内存拷贝(one-level-deep copy)
,而非真正的深复制。
第二个方法是将集合进行归档(archive
),然后解档(unarchive
),如:
NSArray *trueDeepCopyArray = [NSKeyedUnarchiver unarchiveObjectWithData:[NSKeyedArchiver archivedDataWithRootObject:oldArray]];
系统对象的copy
与mutableCopy
:
-
copy
返回imutable
对象;所以,如果对copy
返回值使用mutable
对象接口就会crash
; -
mutableCopy
返回mutable
对象;
在非集合类对象中:
[immutableObject copy] // 浅复制
[immutableObject mutableCopy] // 深复制
[mutableObject copy] // 深复制
[mutableObject mutableCopy] // 深复制
集合对象中:
[immutableObject copy] // 浅复制
[immutableObject mutableCopy] // 单层深复制
[mutableObject copy] // 单层深复制
[mutableObject mutableCopy] // 单层深复制
注意点:
NSString *str = @"string";
str = @"newString";
上面这段代码,在执行第二行代码后,内存地址发生了变化。乍一看,有点意外。按照 C
语言的经验,初始化一个字符串之后,字符串的首地址就被确定下来,不管之后如何修改字符串内容,这个地址都不会改变。但此处第二行并不是对 str 指向的内存地址重新赋值,因为赋值操作符左边的 str 是一个指针,也就是说此处修改的是内存地址
。
赋值之后地址就会发生变化。
NSString *name = @"name";
NSLog(@"===name====%p==", name);// ===name====0x10f803188==
name = @"HHHH";
NSLog(@"===name====%p==", name);// ===name====0x10f8031c8==
用@property
声明NSString、NSArray、NSDictionary
经常使用copy
关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary
,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
-
strong
修饰的对象发生变化时也发生了变化 -
copy
修饰的对象则不受外界的影响
协议与分类
二十三、通过委托与数据源协议进行对象间的通信
Objective-c
分类(Category)
和扩展(Extension)
:
Category:
typedef struct objc_category *Category;
struct objc_category {
char *category_name // 分类名
char *class_name // 分类所属的类名
struct objc_method_list *instance_methods // 实例方法列表
struct objc_method_list *class_methods // 类方法列表
struct objc_protocol_list *protocols //分类所实现的协议列表
}
这个结构体里面是没有实例变量列表的。分类中可以写@property,但是不会生成setter/getter方法,也不会实现合成成员变量。
分类中也可以通过其他方式添加属性,但是一般不推荐这么做:
(objc_getAssociatedObject/objc_setAssociatedObject)
Extension类扩展:
Tips: class-continuation
中文也翻译扩展
实现方式如下:
@interface Class_Name ()
@end
一般的类扩展是写到.m
文件中,一般的私有属性都是写到.m
文件的类扩展中。
作用:为一个类添加额外的变量、方法和属性。
分类和扩展的区别:
- 分类原则上只能增加方法(添加属性是通过
runtime
操作) - 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是
@private
类型的(范围只能是当前类) - 类扩展中声明的方法没被实现,编译器会报警,但是分类中的方法没被实现,编译器不会报警。因为类扩展是在编译阶段添加到类的,分类是在运行时添加到类中的。
- 如果分类和其所属类有相同的方法时,优先调用分类的方法,然后
super
。 - 扩展没有自己单独的实现部分
(implementation)
,依托所属类的实现来实现
二十四、将类的实现代码分散到便于管理的数个分类中
按照不同的分类和属性进行代码分块,私有方法放入私有的类中。
二十五、总是为第三方类的分类名添加前缀
向第三方类中添加分类时,为分类名称和方法名称添加前缀,防止出现了重写系统方法或者是被其他分类重写了当前分类的方法。
二十六、不要在分类中声明属性
- 分类中无法创建实例变量
- 可以定义存取方法,但是不要定义属性
尽管在分类里也可以声明属性,但是这种做法还是要尽量避免。原因是除了class-continuation
分类之外,其他分类都无法向类中新增实例变量,因此它们无法把实现属性所需的实例变量合成出来。此时需要使用@dynamic
,在分类中为该属性实现存取方法。
二十七、使用class-continuation
隐藏实现实现细节
- 比如我们将不愿意暴露的属性或者方法实现在扩展中
- 比如当一个类需要遵从某一个协议的时候,我们可以在扩展中遵从,在类的实现中实现协议方法
二十八、通过协议提供匿名对象
- 具体的对象类型可以淡化成遵从某协议的
id
类型,协议里规定了对象所应该实现的方法 - 使用匿名对象来隐藏类型名称(或者类名)
内存管理
二十九、理解引用计数
引用计数工作原理:对象有个计数器,用以表示当前有多少个对象想令此对象继续存活下去
。在Objective-C
中叫做保留计数(retain count)
也叫引用计数(reference count)
。
NSObject
协议声明了下面三个方法用于操作计数器:
-
retain
: 递增保留计数 -
release
: 递减保留计数 -
autorelease
: 在清理自动释放池时,再递减保留计数
- 先保留新值然后释放旧值,然后更新实例变量
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活,当保留计数降为0的时候,对象就被销毁了。
- 在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
三十、以ARC简化引用计数
-
__strong
: 默认语义,保留此值 -
__weak
: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空 -
__autoreleasing
: 把对象"按引用传递"给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。 -
__weak
常常用来防止block
的循环引用
ARC
管理对象生命周期的方法基本上就是:在合适的地方插入"retain
"和"release
"操作。在ARC
环境下,变量的内存管理语义可以通过属性的修饰符来致命。
ARC
只负责管理Objective-C
对象的内存。CoreFoundation
对象不归ARC
管理,开发者必须手动调用CFRealse、CFRetain
。
三十一、在dealloc
方法中只释放引用并解除监听
在dealloc
中只用来移除KVO
或NSNotificationCenter
的通知,不要做其他事情
三十二、编写"异常安全代码"时留意内存管理问题
try {
} catch {
}
若使用ARC
且必须捕获异常时,则需要打开编译器的-fobjc-arc-exceptions
标志。如果是手动管理引用计数且需要捕获异常时,则需要设法把对象正确清理干净。
三十三、以弱引用来避免循环引用
weak
unsafe_unretained
三十四、以"自动释放池"降低内存峰值
使用
@autoreleasepool {
}
将会占用大量内存的代码包裹起来,系统就会在代码块的结尾把某些对象回收掉,从而降低内存峰值。
三十五、用僵尸对象(Zombie Object)
调试内存管理问题
退出程序调用abort()
方法
TODO
此处需要扩充
三十六、不要使用retainCount
方法
NSObject
协议定义了下列方法,用来查询当前对象的保留计数:
- (NSUInteger)retainCount;
在ARC
此方法已经废弃了。
Block和GCD
三十七、理解block
block
基础知识:
^{};
void (^someBlock)() = ^{
}
定义block
的时候,其所占的内存区域是分配在栈中的。当对一个block
执行copy
操作时,就会把block
从栈复制到堆上。
三十八、为block
创建typedef
typedef int(^someBlock)(BOOL flag, int value);
该block
的名称为someBlock
;返回值为int
;传入的参数有两个,一个是BOOL
,一个是int
。
三十九、用handler
降低代码分散程度
将方法里的闭包用typedef
抽取出来
typedef void(^NetworkErrorHandler)(NSError *error);
- (void)finishWithCompletionHandler:(NetworkErrorHandler)failure;
- 在创建对象时,可以使用内联的
handler
块将相关的业务逻辑一并声明 - 在设计接口的时候,根据情形判断是需要时候用
handler
、block
还是delegate
。
四十、用块引用所属对象时注意循环引用
当block
捕捉的对象直接或间接的保留了块本身,那就需要注意循环引用。
如:在block
里持有了self
,self
又持有了block
;或者block
里持有了self
,self
持有了一个对象A
,A
又持有block
。
四十一、多用派发队列,少用同步锁
在GCD
出来之前,如果有多个线程要执行同一份代码,通常使用锁来实现某种同步机制。
- 是使用内置的同步块
(synchronization block)
:
@synchronized(self) {
}
此方法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。这种写法会降低代码效率。
-
NSLock、NSRecursiveLock
对象:
NSLock *lock = [[NSLock alloc] init];
[lock lock];
// 将代码写在此处
[lock unlock];
或者使用NSRecursiveLock
这种递归锁(recursive lock)
。线程能够多次持有该锁,而且不会出现死锁deadlock
现象。
缺陷:效率也不是很高。
使用串行同步队列(serial synchronization queue)
可以代替同步块或锁对象。将读取、写入操作都安排在同一个队列里,即可保证数据同步。
_syncQueue = dispatch_queue_create("queueName", NULL);
dispatch_sync(_syncQueue, ^{
});
并发队列(concurrent queue)
:
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
栅栏(barrier)
:
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
- 派发队列可用来表述同步
(synchronization semantic)
,这种做法要比使用@synchronized
或者NSLock
对象更简单,更有效率 - 将同步与异步派发结合起来,可以实现与普通枷锁机制一样的同步行为,而这么做却不会阻塞执行异步派发的线程
- 使用同步队列及栅栏,可以令同步行为更加高效
四十二、多用GCD
,少用performSelector
系列方法
如果想要把任务放在另一个线程上执行,最好不要用performSelectorOnMainThread
,而应该用GCD
的方法实现。因为performSelector
系列方法在内存管理方面容易有疏失。
四十三、掌握GCD
及操作队列的使用时机
- 同步机制、只需执行一次的代码(比如单例)使用
GCD
是最好的选择。 - 但是执行后台任务时,
GCD
并不一定是最好的方法。NSOperationQueue
。开发者可以把操作以NSOperation
子类的形式放在队列中,这些操作也可以并发执行。 -
GCD
是纯C
的API
,操作队列是OC
的对象。
NSOperationQueue类的addOperationWithBlock
使用NSOperation
及NSOperationQueue
的好处如下:
- 取消某个操作。如果使用操作队列,那么想要取消操作是很容易的。运行任务之前,可以在
NSOperation
对象上调用cancel
方法。不过已经启动的任务是无法取消的。 - 指定操作间的依赖关系。一个操作可以依赖其他多个操作。使一个操作必须在另一个操作顺利执行完毕之后方可执行。
- 通过键值观察机制监控
NSOperation
对象的属性。如isCancelled
属性来判断任务是否已经取消,isFinished
属性来判断任务是否已经完成。 - 指定操作的优先级。
GCD
的优先级是针对队列而言,而不是针对每个块。NSOperation
对象也有线程优先级(thread priority
)这决定了运行此操作的线程处在何种优先级上。这个功能要比GCD
方便。 - 重用
NSOperation
对象。系统内置了很多NSOperation
的子类,我们也可以自己创建。 -
NSOperationQueue
: 操作队列 -
GCD
:派发队列
四十四、通过Dispatch Group
机制,根据系统资源状况来执行任务
dispatch group
是GCD
的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。
四十五、用dispatch_once
来执行只需运行一次的线程安全代码
单例模式singleton
:
+ (id)sharedInstance {
static SomeClass *sharedInstance = nil;
@synchronized(self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
}
Tips: 上述方法可能会出现线程安全问题
只执行一次的GCD
的API
:
dispatch_once(dispatch_once_t *token, dispatch_block_t block)
该函数里的block
必定会执行,且只执行一次。
+ (id)sharedInstance {
static SomeClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
-
dispatch_one
可以彻底的保证线程安全。而且更高效。 -
static
表示把该变量定义在static
作用域中,可以保证编译器每次在每次执行sharedInstance
方法时都会复用这个变量,而不会创建新的变量。
四十六、不要使用diapatch_get_current_queue
系统框架
四十七、熟悉系统框架
四十八、多用块枚举,少用for
循环
NSEnumerator:
- (NSArray *)allObjects
- (id)nextObject // 返回nil就表示到达枚举末端了
遍历数组:
NSArray *anArray = /*....*/;
NSEnumerator *enumerator = [anArray objectEnumerator];
id object;
while ((object = [enumerator nextObject]) != nil) {
}
这种写法和For
循环相似,但是代码多了一些,不过是通用的,可以用在所有的collection
中
NSDictionary *aDictionary = /*...*/;
NSEnumerator *enumerator = [aDictionary keyEnumerator];
id key;
while ((key = [enumerator nextObject]) != nil) {
id value = aDictionary[key];
}
for in
快速遍历;
enumerateObjectsUsingBlock
块枚举法具备比较多的优势,提供的信息比较丰富
四十九、对自定义其内存管理语义的collection
使用__bridge
当使用CoreFoundation
框架的时候,(带CF
前缀的),注意使用桥式转换(bridgedcast)
-
__bridge
:ARC
仍然具备这个OC
对象的所有权。 -
__bridge_retained
:ARC
将交出对象的所有权,需要调用CFRelease
-
__bridge_transfer
: 让ARC
获得所有权,把CFArray
转换为NSArray
五十、构建缓存时选用NSCache
而非NSDictionary
NSCache
优点:
- 当系统资源将要耗尽时,可以自动删减缓存
-
NSCache
并不会拷贝键,而是会保留它。 -
NSCache
是线程安全的
五十一、精简initialize
和load
的实现代码
- 尽量不要使用
load
方法 -
initialize
是懒加载的,只有在调用的时候才会执行 - 在加载阶段,如果类实现了
load
方法,那么系统就会调用它。load
方法不支持overwrite
。
网友评论