美文网首页学海无涯
Effective Objective-C---阅读笔记

Effective Objective-C---阅读笔记

作者: xxxxxxxx_123 | 来源:发表于2020-01-05 14:42 被阅读0次

熟悉 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_EXPORTextern 更好

五、用枚举值表示状态、选项、状态码

  • 指定枚举变量的类型
    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)"; messageNameselectorselector和参数合起来称为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
  • 类型信息查询方法:isMemberOfClassisKindOfClass
  • 每个实例都有一个指向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]];

系统对象的copymutableCopy:

  • 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文件的类扩展中。
作用:为一个类添加额外的变量、方法和属性。

分类和扩展的区别:

  1. 分类原则上只能增加方法(添加属性是通过runtime操作)
  2. 类扩展不仅可以增加方法,还可以增加实例变量(或者属性),只是该实例变量默认是@private类型的(范围只能是当前类)
  3. 类扩展中声明的方法没被实现,编译器会报警,但是分类中的方法没被实现,编译器不会报警。因为类扩展是在编译阶段添加到类的,分类是在运行时添加到类中的。
  4. 如果分类和其所属类有相同的方法时,优先调用分类的方法,然后super
  5. 扩展没有自己单独的实现部分(implementation),依托所属类的实现来实现

二十四、将类的实现代码分散到便于管理的数个分类中

按照不同的分类和属性进行代码分块,私有方法放入私有的类中。

二十五、总是为第三方类的分类名添加前缀

向第三方类中添加分类时,为分类名称和方法名称添加前缀,防止出现了重写系统方法或者是被其他分类重写了当前分类的方法。

二十六、不要在分类中声明属性

  1. 分类中无法创建实例变量
  2. 可以定义存取方法,但是不要定义属性

 尽管在分类里也可以声明属性,但是这种做法还是要尽量避免。原因是除了class-continuation分类之外,其他分类都无法向类中新增实例变量,因此它们无法把实现属性所需的实例变量合成出来。此时需要使用@dynamic,在分类中为该属性实现存取方法。

二十七、使用class-continuation隐藏实现实现细节

  • 比如我们将不愿意暴露的属性或者方法实现在扩展中
  • 比如当一个类需要遵从某一个协议的时候,我们可以在扩展中遵从,在类的实现中实现协议方法

二十八、通过协议提供匿名对象

  • 具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应该实现的方法
  • 使用匿名对象来隐藏类型名称(或者类名)

内存管理

二十九、理解引用计数

引用计数工作原理:对象有个计数器,用以表示当前有多少个对象想令此对象继续存活下去在Objective-C中叫做保留计数(retain count)也叫引用计数(reference count)
NSObject协议声明了下面三个方法用于操作计数器:

  • retain: 递增保留计数
  • release: 递减保留计数
  • autorelease: 在清理自动释放池时,再递减保留计数
  1. 先保留新值然后释放旧值,然后更新实例变量
  2. 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活,当保留计数降为0的时候,对象就被销毁了。
  3. 在对象生命周期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

三十、以ARC简化引用计数

  • __strong: 默认语义,保留此值
  • __weak: 不保留此值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空
  • __autoreleasing: 把对象"按引用传递"给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放。
  • __weak 常常用来防止block的循环引用

ARC管理对象生命周期的方法基本上就是:在合适的地方插入"retain"和"release"操作。在ARC环境下,变量的内存管理语义可以通过属性的修饰符来致命。

ARC只负责管理Objective-C对象的内存。CoreFoundation对象不归ARC管理,开发者必须手动调用CFRealse、CFRetain

三十一、在dealloc方法中只释放引用并解除监听

dealloc中只用来移除KVONSNotificationCenter的通知,不要做其他事情

三十二、编写"异常安全代码"时留意内存管理问题

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块将相关的业务逻辑一并声明
  • 在设计接口的时候,根据情形判断是需要时候用handlerblock还是delegate

四十、用块引用所属对象时注意循环引用

block捕捉的对象直接或间接的保留了块本身,那就需要注意循环引用。
如:在block里持有了selfself又持有了block;或者block里持有了selfself持有了一个对象AA又持有block

四十一、多用派发队列,少用同步锁

GCD出来之前,如果有多个线程要执行同一份代码,通常使用锁来实现某种同步机制。

  1. 是使用内置的同步块(synchronization block):
@synchronized(self) {

}

此方法会根据给定的对象,自动创建一个锁,并等待块中的代码执行完毕。这种写法会降低代码效率。

  1. 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是纯CAPI,操作队列是OC的对象。
NSOperationQueue类的addOperationWithBlock

使用NSOperationNSOperationQueue的好处如下:

  • 取消某个操作。如果使用操作队列,那么想要取消操作是很容易的。运行任务之前,可以在NSOperation对象上调用cancel方法。不过已经启动的任务是无法取消的。
  • 指定操作间的依赖关系。一个操作可以依赖其他多个操作。使一个操作必须在另一个操作顺利执行完毕之后方可执行。
  • 通过键值观察机制监控NSOperation对象的属性。如isCancelled属性来判断任务是否已经取消,isFinished属性来判断任务是否已经完成。
  • 指定操作的优先级。GCD的优先级是针对队列而言,而不是针对每个块。NSOperation对象也有线程优先级(thread priority)这决定了运行此操作的线程处在何种优先级上。这个功能要比GCD方便。
  • 重用NSOperation对象。系统内置了很多NSOperation的子类,我们也可以自己创建。
  • NSOperationQueue: 操作队列
  • GCD:派发队列

四十四、通过Dispatch Group机制,根据系统资源状况来执行任务

dispatch groupGCD的一项特性,能够把任务分组。调用者可以等待这组任务执行完毕,也可以在提供回调函数之后继续往下执行,这组任务完成时,调用者会得到通知。

四十五、用dispatch_once来执行只需运行一次的线程安全代码

单例模式singleton

+ (id)sharedInstance {
    static SomeClass *sharedInstance = nil;
    @synchronized(self) {
        if (!sharedInstance) {
            sharedInstance = [[self alloc] init];
        }
    }
    return sharedInstance;
}

Tips: 上述方法可能会出现线程安全问题

只执行一次的GCDAPI:

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)

  1. __bridge: ARC仍然具备这个OC对象的所有权。
  2. __bridge_retained: ARC将交出对象的所有权,需要调用CFRelease
  3. __bridge_transfer: 让ARC获得所有权,把CFArray转换为NSArray

五十、构建缓存时选用NSCache而非NSDictionary

NSCache优点:

  1. 当系统资源将要耗尽时,可以自动删减缓存
  2. NSCache并不会拷贝键,而是会保留它。
  3. NSCache是线程安全的

五十一、精简initializeload的实现代码

  1. 尽量不要使用load方法
  2. initialize是懒加载的,只有在调用的时候才会执行
  3. 在加载阶段,如果类实现了load方法,那么系统就会调用它。load方法不支持overwrite

五十二、NSTimer会保留其目标对象 防止出现循环引用

相关文章

网友评论

    本文标题:Effective Objective-C---阅读笔记

    本文链接:https://www.haomeiwen.com/subject/iraqactx.html