Effective Objective-C 2.0 读书笔记

Effective Objective-C 2.0 读书笔记_第1张图片

    • 第 1 章 熟悉 Objective-C
    • 第 2 章 对象消息运行时
    • 第 3 章 接口和 API 设计
    • 第 4 章 协议与分类
    • 第 5 章 内存管理
      • 循环引用
        • 普通的两个变量互相引用
        • Block 循环引用
        • NSTimer
        • 悬挂指针
      • 持有释放不匹配
        • performselector
        • CoreFoundation - Foundation
        • try catch
      • 其他
        • autoreleasepool block 降低内存峰值
        • 需自己负责释放方法命名规则
    • 第 6 章 块与大中枢派发
    • 第 7 章 系统框架

第 1 章 熟悉 Objective-C

0.若是用过另一种面向对象语言(C++ 或 Java),那么就能理解 Objective-C 所用的许多范式与模板了。然而语法上也许会显得陌生,因为该语言使用“消息结构”(messaging structure)而非“函数调用”(function calling)。Objective-C 语言由 Smalltalk 演化而来,后者是消息型语言的鼻祖。

1.关键区别在于:使用消息结构的语言,其运行时所应执行的代码由运行环境来决定;而使用函数调用的语言,则由编译器决定

2.对 1 注解:如果范例代码中调用的函数是多态的,那么在运行时就要按照“虚方法表”(virtual table)来查出到底应该执行哪个函数实现。而采用消息结构的语言,不论是否多态,总是在运行时才会去查找索要执行的方法。实际上,编译器甚至不关心接收消息的对象是何种类型。接收消息的对象也要在运行时处理,其过程叫做“动态绑定”(dynamic binding)。

3.Objective-C 的重要工作都由“运行期组件”(runtime component)而非编译器来完成。使用 Objective-C 的面向对象特性所需的全部数据结构及函数都在运行期组件里面。举例来说,运行期组件中含有全部内存管理方法。运行期组件本质上就是一种与开发者所编写代码相连接的“动态库”(dynamic library),其代码能把开发者编写的所有程序粘合起来。这样的话,只需更新运行期组件,即可提升应用程序性能。而那种许多工作都在“编译期”(compile time)完成的语言,若想获得类似的性能提升,则要重新编译应用程序代码。

4.初始化一个字符串:

NSString *someString = @"The string";

这种语法基本上是照搬 C 语言的。它声明了一个名为 someString 的变量,其类型是 NSString*。也就是说,此变量为指向 NSString 的 指针。所有 Objective-C 语言的对象都必须这样声明,因为对象所占内存总是分配在”堆空间”(heap space)中,而绝不会分配在“栈”(stack)上。

5.分配在堆中的内存必须直接管理,而分配在栈上用于保存变量的内存则会在其栈帧弹出时自动清理。

6.Objective-C 将堆内存管理抽象出来了。不需要用 mallocfree 来分配或释放对象所占内存。Objective-C 运行期环境就把这部分工作抽象为一套内存管理架构,名叫“引用计数”。

7.在 Objective-C 代码中,有时会遇到定义里不含 * 的变量,它们可能会使用“栈空间”(stack space)。这些变量所保存的不是 Objective-C 对象。比如 CoreGraphic 框架中的 CGRect 是 C 结构体。整个系统框架都在用这种结构体,因为如果改用 Objective-C 对象来做的话。

8.Objective-C 为 C 语言添加了面向对象特性,是其超集。Objective-C 使用动态绑定的消息结构,也就是说,在运行时才会检查对象类型。接收一条消息之后,究竟应执行何种代码,由运行期环境而非编译器来决定。

9.除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明(如果提前声明Person类,使用:@class Person;)来提及别的类,并在实现文件中引入那些类的头文件。这样做可以尽量降低类之间的耦合。若引入许多根本用不到的内容,会增加编译时间

10.看下面这个例子(仅列出.h文件):

//YMPerson.h
#import 
#import "YMEmployer.h"

@interface YMPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;
@property (nonatomic, strong) YMEmployer *employer;

@end

//YMEmployer.h
#import 
#import "YMPerson.h"

@interface YMEmployer : NSObject

- (void) addEmployee:(YMPerson *) person;

@end

这样会出现问题,虽然使用 #import 而非 #include 指令虽然不会导致死循环,但这却意味着两个类有一个无法被正确编译。而使用向前声明却可以解决这个问题。

11.有时候必须要在头文件中引入其他头文件:

  • 如果你写的类继承自某个超类,则必须引入定义那个超类的头文件。
  • 如果要声明你写的类遵从某个协议,那么该协议必须有完整定义,且不能使用向前声明。向前声明只能高度编译器有某个协议,而此时编译器却要知道该协议中定义的方法。

12.引入协议头文件,最好把协议单独放在一个头文件中。然而有些协议,例如“委托协议”,就不用单独写一个头文件了,在这种情况下,协议只有与接收协议委托的类放在一起定义才有意义。

13.每次在头文件中引入其他头文件之前,都要先问问自己这样做是否确有必要。如果可以用向前声明取代引入,那么就不要引入。若因为要实现属性。实例变量或者要遵循协议而必须引入头文件,则应尽量将其一直“ class-continuation 分类”。这样不仅可以所见编译时间,而且还能降低彼此依赖程度。若是依赖关系过于复杂,则会给维护带来麻烦,而且,如果只要把代码的某个部分开放为公共API的话,太复杂的依赖关系也会出现问题。

14.应该使用字面量语法来创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,这么做更加简明扼要。

15.用字面量语法创建数组时要注意,若数组元素对象中有 nil,则会抛出异常,因为字面量语法实际上指示一种语法糖( syntactic sugar ),其效果等于是先创建了一个数组,然后把方括号内的所有对象都加到这个数组中。这样会让我们提前知道错误,而使用最原始的方法虽不会抛出异常使我们不好找到异常。

16.字典中的对象和键必须都是 Objective-C 对象,所以不能把整数值(如 10 )直接放进去,而是将其封装在NSNumber实例中( @10 )才行。

17.使用字面量语法创建出来的字符串、数组、字典对象都是不可变的(immutable)。若想要可变版本的对象,则需复制一份:

NSMutableArray *mutable = [[@1, @2, @3] mutableCopy];

18.字面量语法有个小小的限制,就是除了字符串意外,所差出来对象必须属于 Foundation 框架才行。

19.在编写代码时经常要定义常量。掌握了 Objective-C 与其 C 语言基础的人,也许会用这种方法来做:

#define ANIMATION_DURATION 0.3

上述预处理命令会把源代码中的 ANIMATION_DURATION 字符串替换为 0.3 。这可能是你想要的效果,不过这样定义出来的常量没有类型信息

20.要想解决上面的问题,应该设法利用编译器的某些特性才对。有个办法比用预处理指令来定义常量更好。比方说,下面这行代码就定义了一个类型为 NSTimeInteval 的常量:

static const NSTimeInterval kAnimationDuration = 0.3;

这样定义的常量包含类型信息,其好处是清楚地描述了常量的含义。由此可知该常量类型为 NSTimeInterval,这有助于为其编写开发文档。如果要定义许多变量,那么这种方式能令稍后阅读代码的人更易理解其意图。

21.若常量局限于某“编译单元”(也就是“实现文件“)之内,则在前面加字母 k;若常量在类之外可见,则通常以类名为前缀

22.若不打算公开某个常量,则应将其定义在使用该常量的实现文件里。变量一定要同时用 staticconst 来声明。

  • 若视图修改由 const 修饰符所声明的变量,那么编译器就会报错。
  • static 修饰符则意味着该变量仅在定义此变量的编译单元中可见。编译器每收到一个便一单元,就会输出一份”目标文件”。在 Objective-C 的语境下,“编译单元”一词通常指每个类的实现文件(以 .m 为后缀名)。假如声明此变量时不加 static,则编译器会为它创建一个“外部符号”(external symbol)。

23.有时候需要对外公开某个常量。比方说,可能要在类代码中调用NSNotificationCenter来通知他人。用一个对象来派发通知,令其他欲接收通知的对象向该对象注册,这样就能实现此功能了。派发通知时,需要使用字符串来表示此项通知的名称,而这个名字就可以声明为一个外界可见的长值变量(constant variable)。这样的话,注册者无须知道实际字符串值,只需以常值变量来注册自己想要接收的通知即可。应该这样定义:

//在 .h 文件中
extern NSString *const YMStringConstant;

//在 .m 文件中
NSString *const YMStringConstant = @"Value";

这个常量在头文件中“声明”,且在实现文件中“定义”。注意 const 修饰符在常量类型中的位置。

24.注意常量的名字。为避免名称冲突,最好是用与之相关的类名做前缀。系统框架中一般都这样做。例如 UIKit 就按照这种方式来声明用作通知名称的全局常量。其中有类似 UIApplicationDidEnterBackgroundNotificationUIApplicationWillEnterForegroundNotification 这样的常量名。

25.不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。即使有人重新定义了常量值,编译器也不会产生警告信息,这样导致应用程序中的常量值不一致。

26.在实现文件中使用 static const 来定义”只在编译单元内可见的常量“。由于此类常量不在全局符号表中,所以无需为其名称加前缀。

27.在头文件中使用 extern 来声明全局变量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名称做前缀。

28.应该用枚举来表示状态机的状态、传递给方法的选项以及状态吗等值,给这些值起个易懂的名字。

29.如果把传递给某个方法的选项表示为枚举类型,而多个选项又可同时使用。那么就讲各选项值定义为 2 的幂,以便通过按位或操作将其组合起来。

30.用 NS_ENUMNS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。这样做可以确保枚举是用开发者所选的底层数据类型实现出来的,而不会采用编译器所选的类型。

31.我们总习惯在 Switch 语句中加上 default 分支。然而,若是用枚举来定义状态机,则最好不要有 default 分支。这样的话,如果稍后又加了一种状态,那么编译器就会发出警告信息,提示新加入的状态并未在 switch 分支中处理。假如写上了 default 分支,那么它就会处理这个新状态,从而导致编译器不发警告信息。

32.枚举分为:普通枚举(NS_ENUM) 和 选项枚举(NS_OPTIONS),其中选项枚举,如系统中的 UIViewAutoresizing,这么定义:

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

选项枚举的原理,见下图:

Effective Objective-C 2.0 读书笔记_第2张图片

第 2 章 对象、消息、运行时

0.用 Objective-C 等面向对象语言编程时,“对象”就是“基本构造单元”(building block),开发者可以通过对象来存储并传递数据。在对象之间传递数据并执行任务的过程就叫做“消息传递”(Messaging)。

1.当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C 运行期环境”(Objective-C Runtime),它提供了一些使得对象之间能够提供消息的重要函数,并且包含创建类实例所用的全部逻辑。在理解了运行期环境中各个部分协同工作的原理之后,你的开发水平将会进一步提升。

2.属性(property)是 Objective-C 的一项特性,用于封装对象中的数据。Objective-C对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。这个概念已经定型,并且经由“属性”这一特性而成为 Objective-C 2.0 的一部分,开发者可以令编译器自动编写与属性相关的存取方法。此特性引入了一种新的“点语法”(dot syntax),使开发者可以更为容易地依照类对象来访问存放于其中的数据。

3.要访问属性,可以使用“点语法“,在纯 C 中,如果想访问分配在栈上的 struct 结构体里面的成员,也需使用类似语法。编译器会吧”点语法“转换为对存取方法的调用,使用”点语法“的效果与直接调用存取方法相同。因此,使用”点语法“和直接调用存取方法之间没有丝毫差别。

4.属性还有更多优势。如果使用了属性的话,那么编译器就会自动编写访问这些属性所需的方法,此过程叫做”自动合成“。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些”合成方法“的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加上下划线,以此作为实例变量的名字。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。

5.一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。

6.若不想令编译器自动合成存取方法,则可以自己实现。如果你只实现了其中一个存取方法,那么另外一个还是会由编译器来合成。还有一种办法能阻止编译器自动合成存取方法,就是使用 @dynamic 关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。而且,在编译访问属性的代码时,即使编译器发现没有定义存取方法,也不会报错,它相信这些方法能在运行期找到。

7.使用属性时还有一个问题要注意,就是其各种特质(attribute)设定也会影响编译器所生成的存取方法。属性可以拥有的特质分为 4 类:

  • 原子性
  • 读/写权限
  • 内存管理语义
  • 方法名

8.在属性不添加任何特质时,MRC 默认情况是 atomicassignreadwrite。ARC 默认情况是 atomicstrongreadwrite

@property BOOL checked;

http://stackoverflow.com/questions/5802511/what-are-the-defaults-values-for-property-in-ios

http://www.devtalking.com/articles/you-should-to-know-property/

9.原子性:在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。

10.具备 readwrite(读写)特质的属性拥有“获取方法”(getter)与“设置方法”(setter)。

11.具备 readonly(只读)特质的属性仅拥有获取方法。你可以使用此特质把某个属性对外公开为只读属性,然后在“class-continuation 分类”中将其重新定义为读写属性。

12.assign 设置方法 只会执行针对 “纯量类型”(scalar type,例如 CGFloatNSInteger 等)的简单赋值操作。

13.strong 此特质表明该属性定义了一种“拥有关系”(owning relationship)。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。

14.weak 此特质表明该属性定义了一种“非拥有关系”(nonowning relationship)。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然而在属性所指的对象遭到销毁时,属性值也会清空(nil out)。

15.unsafe_unretained 此特质的语义和 assign 相同,但是它适用于 对象类型,该特质表达一种 “非拥有关系”(“不保留”,unretained),当目标对象遭到销毁时,属性值不会自动清空(“不安全”,unsafe),这一点与 weak 有区别。

16.copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其拷贝(copy)。

17.当属性类型为 NSString* 时,经常用此特质来保护其封装性。因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可以修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变”(immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的”(mutable),就应该在设置新属性值时拷贝一份。NSDictionaryNSArray 也应如此。

18.可通过如下特质来制定存取方法的方法名:

@property(nonatomic, getter=isOn) BOOL on;

19.setter= 指定“设置方法”的方法名。

20.通过上述特质,可以微调由编译器所合成的存取方法。不过需要注意:若是自己来实现这些存取方法,那么应该保证其具备相关属性所声明的特质。比方说,如果将某个属性声明为 copy,那么就应该在”设置方法“中拷贝相关对象,否则会误导该属性的使用者,而且,若是不遵从这一约定,还会令程序产生 bug。

21.由于是只读属性,所以编译器不会为其创建对应的“设置方法”,即便如此,我们还是要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。要是不写明语义的话。该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法之前自行拷贝属性值。这种操作是多余而且低效的。

22.atomicnonatomic 的区别

具备 atomic 特质的获取方法会通过锁定机制来保证其原子性。这也就是说,如果两个线程读写同一属性,那么不论何时,总能看到有效的属性值。若是不加锁的话(或者说使用 nonatomic 语义),那么当其中一个线程正在改写某属性值,另一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发生这种情况时,线程读到的属性值可能不对。

23.atomic 一定是线程安全的吗?

如果开发过 iOS 程序,你就会发现,其中所有属性都声明为 nonatomic。这样做的历史原因是:在 iOS 中使用同步锁的开销较大,这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”(thread safety),若要实现“线程安全 ”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。因此,开发 iOS 程序时一般都会使用 nonatomic 属性。但是在开发Mac OS X 程序时,使用 atomic 属性通常都不会有性能瓶颈。

24.消息转发

http://www.jianshu.com/p/1bde36ad9938

25.Runtime(貌似满大街都是的文章了)

http://www.jianshu.com/p/e071206103a4

第 3 章 接口和 API 设计

0.Objective-C 没有其他语言那种内置的命名空间(namespace)机制。鉴于此,我们在起名时要设法避免潜在的命名冲突,否则很容易就重名了。如果发生命名冲突,那么应用程序的链接过程就会出错,因为其中出现了重复符号,duplicate symbol错误。

1.避免此问题的唯一办法就是变相实现命名空间:为所有名称都加上适当前缀。使用 Cocoa 创建应用程序时一定要注意,Apple 宣称其保留使用所有“两字母前缀”的权利,所以自己选用的前缀应该是三个字母的。

2.不仅是类名,应用程序中的所有名称都应该加上前缀。如果要既有类新增 category,那么一定要给 “分类” 及分类中方法加上前缀。

3.在类中提供一个全能初始化方法,并于文档里指明。其他初始化方法均应调用此方法,若全能初始化方法与超类不同,则需复写超类中的对应方法(所谓的全能初始化方法就是,其他初始化方法都要调用它,如 NSDate 中的 initWithTimeIntervalSinceReferenceDate: 就是全能初始化方法)。

4.NSObject 协议中还有个方法要注意,那就是 debugDescription,此方法的用意与 description 非常相似。二者区别在于,debugDescription 方法是开发者在调试器(debugger)中以控制台命令打印对象时才调用的。在 NSObject 类的默认视线中,此方法知识直接调用了 description

5.实现 description 方法返回一个有意义的字符串,用以描述该实例。若想在调试时打印出更详尽的对象描述信息,则应实现 debugDescription 方法.

6.默认情况下,属性是“既可读又可写的”,这样设计出来的类都是“可变的”。一般情况下我们要建模的数据未必需要改变。具体到编程实践中,则应该尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。再将属性在对象内部重新声明为 readwrite。

7.给私有化方法名称添加前缀(p_method,p 表示私有)。苹果公司喜欢单用一个下划线作为私有方法的前缀。你也许也想照着苹果公司的办法指哪一个下划线作前缀,这样做可能会惹来大麻烦:如果苹果公司提供的某个类中继承了一个子类,那么你在子类里可能会无意间复写了父类的同名方法,鉴于此,苹果公司在文档中说,开发者不应该单用一个下划线做前缀。

8.若想令自己缩写的对象具有拷贝功能,则需要实现 NSCoping 协议

9.如果自订一个对象分为可变版本与不可变版本。那么就要同事实现 NSCopingNSMutableCopying 协议。

10.在可变对象上调用 copy 方法会返回另外一个不可变类的实例。

[NSMutableArray copy] => NSArray
[NSArray mutableCopy] => NSMutableArray

第 4 章 协议与分类

0.利用分类机制,我们无须继承子类即可直接为当前类添加方法,而在其他编程语言中,则需通过集成子类来实现。由于 Objective-C 运行期系统是高度动态的,所以才支持这一特性。

1.使用分类机制把类的实现代码划分成易于管理的小块

2.将应该视为“私有”的方法归入名叫 Private 的分类中,以隐藏实现细节。

3.分类中的方法是直接添加在类里面的,他们就好比这个类中的固有方法。将分类方法加入类中这一操作是在运行期系统加载分类时完成的。运行期系统会把分类中所实现的每个方法都加入类的方法列表中。如果类中本来就有此方法,而分类又实现了一次,那么分类中的方法会覆盖原来那一份实现代码。实际上可能会发生很多次覆盖,比如某个分类中的方法覆盖了“主实现”中的相关方法,而另外一个分类中的方法又覆盖了这个分类中的方法。多次覆盖的结果以最后一个分类为准。

4.要解决上述问题,一般的做法是:以命名空间来区别各个分类的名称与其中所定义的方法。想在 Objective-C 中实现命名空间功能,只有一个办法,就是给相关名称都加上某个公用的前缀。

5.除了 class-continuation 分类 之外,其他分类都无法向勒种新增实例变量,因此,他们无法把实现属性所需的实例变量合成出来。

6.通过类的匿名分类向类中新增实例变量,如果某属性在主接口中声明为“只读”,而类的内部又要用设置方法修改此属性,那么就在匿名分类中将其扩展为可读写

7.若想使类所遵循的协议不为人所知,则可以在匿名分类中声明

8.协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的 id 类型,协议里规定了对象所应实现的方法。

9.使用匿名对象来隐藏类型名称(或类名)。

第 5 章 内存管理

下图列出了一些 ARC 下的内存问题,就各个问题一一描述下:
Effective Objective-C 2.0 读书笔记_第3张图片

循环引用

普通的两个变量互相引用

上代码:

self.object1 = [[YMNormalCircularReferenceObject alloc] init];
self.object2 = [[YMNormalCircularReferenceObject alloc] init];

self.object1.data = self.object2;
self.object2.data = self.object1;

两个对象互相引用,解决办法一个使用 weak 标识属性。

Block 循环引用

#import "YMBlockCircularReferenceViewController.h"

typedef void(^YMDownloaderCompleteBlock)(id data);

@interface YMDownloader ()

@property (nonatomic, copy) YMDownloaderCompleteBlock downloaderCompleteBlock;

@end

@implementation YMDownloader


- (void)downloadDataWithURL:(NSURL *)url completeBlock:(YMDownloaderCompleteBlock)block {
    self.downloaderCompleteBlock = block;

    [self downloadDataWithURL:url];
}

- (void)downloadDataWithURL:(NSURL *)url {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);

        self.downloaderCompleteBlock(url);
    });
}

@end


@interface YMBlockCircularReferenceViewController ()

@property (nonatomic, strong) YMDownloader *downloader;
@property (nonatomic, strong) id data;

@end

@implementation YMBlockCircularReferenceViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.downloader = [[YMDownloader alloc] init];
    [self.downloader downloadDataWithURL:[NSURL URLWithString:@""] completeBlock:^(id data) {
        self.data = data;
    }];
}

- (void)dealloc {
    NSLog(@"YMBlockCircularReferenceViewController dealloc 方法");
}

@end

self 保留了 downloaderdownloader 拷贝了块,块里保留了 self,解决办法:

- (void)downloadDataWithURL:(NSURL *)url {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(2);

        self.downloaderCompleteBlock(url);
        self.downloaderCompleteBlock = nil;
    });
}

NSTimer

NSTimer 会保留其目标对象

继续上代码:

#import "YMTimerCircularReferenceViewController.h"

@interface YMTimerCircularReferenceViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation YMTimerCircularReferenceViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerSelector) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timerSelector {
    NSLog(@"%@",[NSDate date]);
}

- (void)dealloc {
    NSLog(@"YMTimerCircularReferenceViewController dealloc");
}

YMTimerCircularReferenceViewController pop 时,上面的 dealloc方法不被调用。

解决办法,加一个 NSTimer 类别:

#import "NSTimer+YMBlock.h"

@implementation NSTimer (YMBlock)

+ (NSTimer * _Nonnull)ym_timerWithTimeInterval:(NSTimeInterval)ti block:(nullable void (^)())block userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    return [self timerWithTimeInterval:ti target:self selector:@selector(ym_blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
}

+ (void)ym_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;

    if (block) {
        block();
    }
}

@end

注:此处虽然依然有保留换,self 引用 self,因为是类对象,无须回收。

CADisplayLink 类似。

悬挂指针

在 Scheme 中开启即可,如下图:

持有、释放不匹配

performselector

使用 performSelector,编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC 采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

CoreFoundation - Foundation

上代码:

- (IBAction)coreFoundationToFoundation:(id)sender {
   CFStringRef coreFoundationStr = CFStringCreateWithCString(NULL, "Hello World!", kCFStringEncodingUnicode);

    NSString *foundationStr = (__bridge NSString *)(coreFoundationStr);
    // NSString *foundationStr = CFBridgingRelease(coreFoundationStr);

    NSLog(@"foundationStr:%@",foundationStr);
}

- (IBAction)foundationToCoreFoundation:(id)sender {
    NSString *foundationStr = [[NSString alloc] initWithFormat:@"Hello World!"];

    CFStringRef coreFoundationStr = CFBridgingRetain(foundationStr);
    NSLog(@"coreFoundationStr:%@",coreFoundationStr);
}

- (IBAction)noMemoryManagement:(id)sender {
    // NSString *foundationStr = @"Hello World!";
    // CFStringRef coreFoundationStr = (__bridge CFStringRef)(foundationStr);


    // CFStringRef coreFoundationStr = CFStringCreateWithCString(NULL, "Hello World!", kCFStringEncodingUnicode);
    // NSString *foundationStr = (__bridge NSString *)(coreFoundationStr);
}

Core Foundation 与 Foundation 内存问题

__bridge 什么也不做,仅仅是转换。此种情况下:

  1. 从 Cocoa 转换到 Core,需要人工 CFRetain,否则,Cocoa 指针释放后, 传出去的指针则无效。
  2. 从 Core 转换到 Cocoa,需要人工 CFRelease,否则,Cocoa 指针释放后,对象引用计数仍为1,不会被销毁。

__bridge_retained 转换后自动调用 CFRetain,即帮助自动解决上述 1 的情形。

__bridge_transfer 转换后自动调用 CFRelease,即帮助自动解决上述 2 的情形。

  • __bridge 用法
NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge CFStringRef)string;

只是单纯地执行了类型转换,没有进行所有权的转移,也就是说,当 string 对象被释放的时候,cfstring 也不能被使用了。

  • __bridge_retained 用法
NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge_retained CFStringRef)string;
...
CFRelease(cfString); // 由于Core Foundation的对象不属于ARC的管理范畴,所以需要自己release

使用 __bridge_retained 可以通过转换目标处( cfString )的 retain 处理,来使所有权转移。即使 string 变量被释放,cfString 还是可以使用具体的对象。只是有一点,由于 Core Foundation 的对象不属于 ARC 的管理范畴,所以需要自己 release

可以用 CFBridgingRetain 替代 __bridge_retained 关键字:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = CFBridgingRetain(string);
...
CFRelease(cfString); // 由于Core Foundation不在ARC管理范围内,所以需要主动release。
  • __bridge_transfer
CFStringRef cfString = CFStringCreate...();
NSString *string = (__bridge_transfer NSString *)cfString;

// CFRelease(cfString); 因为已经用 __bridge_transfer 转移了对象的所有权,所以不需要调用 release

所有权被转移的同时,被转换变量将失去对象的所有权。当 Core Foundation 对象类型向Objective-C 对象类型转换的时候,会经常用到 __bridge_transfer 关键字。

同样,我们可以使用 CFBridgingRelease() 来代替 __bridge_transfer 关键字。

CFStringRef cfString = CFStringCreate...();
NSString *string = CFBridgingRelease(cfString);

— 华丽分割线 —

其实看完上面的解释,就不用介绍上面代码出现的问题了。简要说下:

0.- (IBAction)coreFoundationToFoundation:(id)sender 方法创建了 CFStringRef 对象,并未使用,需要使用下面代码释放:

NSString *foundationStr = CFBridgingRelease(coreFoundationStr);

1.- (IBAction)foundationToCoreFoundation:(id)sender 方法里接手并retain 了 Objective-C 对象的字符串,但是没有做到释放。

@try … @catch

再上代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    @try {
        NSArray *array = @[@"a", @"b", @"c"];
        [array objectAtIndex:3];
    } @catch (NSException *exception) {
        // 处理异常
        NSLog(@"throw an exception: %@", exception.reason);
    } @finally {
        NSLog(@"正常执行");
    }
}

当执行到 [array objectAtIndex:3]; 发生崩溃,这时 array 并未释放,解决办法是开启编译器标志 -fobjc-arc-exceptions

其他

@autoreleasepool block 降低内存峰值

for (id object in array) {
    @@autoreleasepool {
        ...
    }
}

合理运用自动释放池,可降低应用程序的内存峰值。

是否应该用 @autoreleasepool { } 来优化效率,完全取决于具体的应用程序。首先得监控内存用量,判断其中有没有需要解决的问题,如果没完成这一步,那就别急着优化。尽管@autoreleasepool { } 的开销不太大,但毕竟还是有的,所以尽量不要建立额外的自动释放池。

需自己负责释放方法命名规则

在一开始的图中的几个方法,需要在 MRC 下需要对象自己手动释放。

第 6 章 块与大中枢派发

0.如果块所捕获的变量是对象类型,那么就会自动保留它。系统在释放这个块的时候,也会将其一并释放。

1.如果将块定义在 Objective-C 类的实例方法中,那么除了可以访问类的所有实例变量之外,还可以使用 self 变量。块总能修改实例变量。所以在声明时无须加 __block。不过,如果通过读取或写入操作捕获了实例变量,那么也会自动把 self 变量一并捕获了,因为实例变量是与 self 所只带的实例关联在一起的。

2.在 block 中 直接访问实例变量和通过 self 来访问是等效的。

3.一定要记住:self 也是个对象,因而快在捕获它时也会将其保留。如果 self 所指点的那个对象同时也保留了块,那么这种情况通常就会导致“保留环”。

4.除了 “栈块” 和 “堆块”之外,还有一类块叫做 “全局块”(global block)。这种块不会捕捉任何状态(比如外围的变量等),运行时也无须有状态来参与。块所使用的整个内存区域,在编译期已经完全确定了,因此,全局块可以生命在全局内存里,而不需要在每次用到的时候于栈中创建。另外,全局块的拷贝操作是个空操作,因为全局块决不可能为系统所回收。这种块实际上相当于单例。

下面两种方式都是全局块(global block):

void (^myFirstBlock)() = ^{
    NSLog(@"123");
};

void (^mySecondBlock)(int a,int b) = ^(int a,int b){
    NSLog(@"a + b = %@",@(a + b));
};

5.以 typedef 重新定义块类型,可令块变量用起来更加简单。

6.不妨为同一个块签名定义多个类型别名。如果要重构的代码使用了块类型的某个别名,那么只需修改相应 typedef 中的块签名即可,无须改动其他 typedef

7.与使用委托模式的代码相比,用块写出来的代码更为整洁。委托模式有个缺点:如果类要分别使用多个获取器下载不同数据,那么就得在 delegate 回调方法里根据传入的获取器参数来切换。

8.建议使用同一个块来处理成功与失败情况,苹果公司似乎也是这样设计 API 的。

9.如果块所捕获的对象直接或间接地保留了块本身,那么就得当心保留环问题。一定要找个适当的时机解除保留环,而不能把责任推给 API 的调用者。

10.在 Objective-C 中,如果有多个线程要执行同一份代码,那么有时可能会出现问题。这种情况下,通常要使用锁来实现某种同步机制。在 GCD 出现之前,有两种办法:

  • 内置的 @synchronize
  • 使用 NSLock 对象

11.滥用 @synchronized(self) 则会降低代码效率,因为共用同一个锁的那些同步块,都必须按顺序执行。若是在 self 对象上频繁枷锁,那么程序可能要等另一段于此无关的代码执行完毕,才能继续执行当前代码,这样做其实并没有必要。在极端情况下,@synchronize 块会导致死锁,另外,其效率也不见得很高。

12.派发队列可用来表述同步寓意,这样做法要比使用 @synchronized 块或 NSLock 对象更简单。

13.有种简单高效的办法可以代替同步块或锁对象,那就是使用“串行同步队列”。

14.如果将串行同步队列改为串行异步队列,貌似看起来效率更高些,但这么改动有个坏处:如果你测一下程序性能,那么可能会发现这种写法比原来慢,因为执行异步派发时,需要拷贝块。若拷贝块所用的时间明显超过执行块所花的实现,则这种做法将比原来更慢。

15.多个获取方法可以并发执行,而获取方法与设置方法之间不能并发执行,利用这个特点,还能写出更快一些的代码块。利用dispatch_brarrier_async,我们可以使用并行队列。并发队列如果发现接下来要处理的块是个栅栏块。待栅栏块执行过后,再按正常方式继续向下处理。

16.多用 GCD,少用 performSelector

17.使用 performSelector,编译器并不知道将要调用的选择子是什么,因此,也就不了解其方法签名及返回值,甚至连是否有返回值都不清楚。而且,由于编译器不知道方法名,所以就没办法运用 ARC 的内存管理规则来判定返回值是不是应该释放。鉴于此,ARC 采用了比较谨慎的做法,就是不添加释放操作。然而这么做可能导致内存泄漏,因为方法在返回对象时可能已经将其保留了。

18.延后执行可以用 dispatch_after 来实现。

19.GCD VS NSOperation

GCD 的优点:

  • GCD 提供的 dispatch_after 支持调度下一个操作的开始时间而不是直接进入睡眠。
    NSOperation 中没有类似
  • dispatch_source_t,dispatch_io,dispatch_data_t,dispatch_semaphore_t 等操作。

NSOperation 的优点:

  • GCD 没有操作依赖。我们可以让一个 Operation 依赖于另一个 Operation,这样的话尽管两个 Operation 处于同一个并行队列中,但前者会直到后者执行完毕后再执行;
    GCD 没有操作优先级(GCD 有队列优先级),能够使同一个并行队列中的任务区分先后地执行,而在 GCD 中,我们只能区分不同任务队列的优先级,如果要区分block任务的优先级,也需要大量的复杂代码;
  • GCD 没有 KVO。NSOperation 可以监听一个 Operation 是否完成或取消,这样能比GCD 更加有效地掌控我们执行的后台任务
  • 在NSOperationQueue 中,我们可以随时取消已经设定要准备执行的任务(当然,已经开始的任务就无法阻止了),而 GCD 没法停止已经加入 queue 的 Block(其实是有的,但需要许多复杂的代码)
  • 我们能够对 NSOperation 进行继承,在这之上添加成员变量与成员方法,提高整个代码的复用度,这比简单地将 block 任务排入执行队列更有自由度,能够在其之上添加更多自定制的功能。

20.使用 Dispatch Group,并行执行 A、B 任务,最后执行 C 任务:

dispatch_queue_t concurrentQueue = dispatch_queue_create("me.iYiming.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_t group = dispatch_group_create();

dispatch_group_async(group, concurrentQueue, ^{
   // A 任务
});

dispatch_group_async(group, concurrentQueue, ^{
   // B 任务
});

dispatch_group_notify(group, concurrentQueue, ^{
   // C 任务
});

21.使用 dispatch_once 创建单例:

单例:保证只分配一次内存

  • 调用 alloc 方法的时候,内部会调用 allocWithZone 方法,所以控制好 allocWithZone 方法的内存开辟操作就能控制 alloc
  • copymutableCopy 同样要控制,直接返回调用者就好(因为 copymutableCopy 是对象方法,所以如果第一次内存分配控制好了,这里直接返回 self

  • MRC 下 retainreleaseretainCount 处理

  //保存单例对象的静态全局变量
  static id _instance;
  + (instancetype)sharedTools {
      return [[self alloc]init];
  }
  //在调用alloc方法之后,最终会调用allocWithZone方法
  + (instancetype)allocWithZone:(struct _NSZone *)zone {
      //保证分配内存的代码只执行一次
      static dispatch_once_t onceToken;
      dispatch_once(&onceToken, ^{
          _instance = [super allocWithZone:zone];
      });
      return _instance;
  }
  //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
  - (id)copyWithZone:(NSZone *)zone {
      return self;
  }
  //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
  - (id)mutableCopyWithZone:(NSZone *)zone {
      return self;
  }
  #if __has_feature(objc_arc)
  //如果是ARC环境
  #else
  //如果不是ARC环境

  //既然是单例对象,总不能被人给销毁了吧,一旦销毁了,分配内存的代码已经执行过了,就再也不能创建对象了。所以覆盖掉release操作
  - (oneway void)release {
  }
  //这是个对象方法,既然有对象而且是单例,那么调用者就是这个单例对象了,那就返回调用的对象就行
  - (instancetype)retain {
      return self;
  }
  //为了便于识别,这里返回 MAXFLOAT ,别的程序员看到这个数据,就能意识到这是单例。
  - (NSUInteger)retainCount {
      return MAXFLOAT;
  }
  #endif

22.dispatch_sync 执行 Block 所在的 Queue 如果和当前 Queue 是同一个 Queue,那么会造成死锁。比如:

- (void)viewDidLoad {
  [super viewDidLoad];

  dispatch_sync(dispatch_get_main_queue(),^{
    NSLog(@"Hello !");
  });

  NSLog(@"结束");
}

为什么?因为 dispatch_sync 是同步的,又在 viewDidLoad 里,所以主线程等待它执行完才能打印“结束“,然而 dispatch_sync 块需要在 dispatch_get_main_queue() 主线程里添加 Block ^{
NSLog(@"Hello !");
})
,因为主线程现在在等待中,所以 Block 永远无法添加进主线程队列中去,互相等待,从而造成死锁。

第 7 章 系统框架

0.CoreFoundation 与 Foundation 不仅名字相似,而且还有更为紧密的联系。Foundation 框架中的许多功能,都可以在此框架中找到对应的 C 语言 API。有个功能佳作“无缝桥接”(tollfree bridging),可以把 CoreFoundation 中的 C 语言数据结构平滑转换为 Foundation 中的 Objective-C 对象,也可以反向旋转。

1.ARC 下只考虑 Objective-C 对象的内存,对于非 Objective-C 对象,比如 CoreFoundation 中的需要自己考虑内存管理问题。

2.Objective-C 编程的一项重要特点,那就是:经常需要使用底层的 C 语言级 API。用 C 语言来实现 API 的好处是,可以绕过 Objective-C 的运行期系统,从而提升执行速度。

3.多用块枚举,少用 for 循环。

[array enumerateObjectsUsingBlock:^(NSString *  _Nonnull name, NSUInteger idx, BOOL * _Nonnull stop) {

    ...

    if ([name isEqualToString:@"Tom"]) {
        stop = YES;
    }
}];

4.块枚举优点:

  • 遍历时可以直接从块里获取更多信息。在遍历数组时,可以知道当前所针对的下标。
  • 能够修改块的方法签名,以免进行类型转换操作。从效果上讲,相当于把本来需要执行的类型转换操作交给方法签名来做。

5.Core Foundation 与 Foundation 内存问题

__bridge 什么也不做,仅仅是转换。此种情况下:

  1. 从 Cocoa 转换到 Core,需要人工 CFRetain,否则,Cocoa 指针释放后, 传出去的指针则无效。
  2. 从 Core 转换到 Cocoa,需要人工 CFRelease,否则,Cocoa 指针释放后,对象引用计数仍为1,不会被销毁。

__bridge_retained 转换后自动调用 CFRetain,即帮助自动解决上述 1 的情形。

__bridge_transfer 转换后自动调用 CFRelease,即帮助自动解决上述 2 的情形。

  • __bridge 用法
NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge CFStringRef)string;

只是单纯地执行了类型转换,没有进行所有权的转移,也就是说,当 string 对象被释放的时候,cfstring 也不能被使用了。

  • __bridge_retained 用法
NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = (__bridge_retained CFStringRef)string;
...
CFRelease(cfString); // 由于Core Foundation的对象不属于ARC的管理范畴,所以需要自己release

使用 __bridge_retained 可以通过转换目标处( cfString )的 retain 处理,来使所有权转移。即使 string 变量被释放,cfString 还是可以使用具体的对象。只是有一点,由于 Core Foundation 的对象不属于 ARC 的管理范畴,所以需要自己 release

可以用 CFBridgingRetain 替代 __bridge_retained 关键字:

NSString *string = [NSString stringWithFormat:...];
CFStringRef cfString = CFBridgingRetain(string);
...
CFRelease(cfString); // 由于Core Foundation不在ARC管理范围内,所以需要主动release。
  • __bridge_transfer
CFStringRef cfString = CFStringCreate...();
NSString *string = (__bridge_transfer NSString *)cfString;

// CFRelease(cfString); 因为已经用 __bridge_transfer 转移了对象的所有权,所以不需要调用 release

所有权被转移的同时,被转换变量将失去对象的所有权。当 Core Foundation 对象类型向Objective-C 对象类型转换的时候,会经常用到 __bridge_transfer 关键字。

同样,我们可以使用 CFBridgingRelease() 来代替 __bridge_transfer 关键字。

CFStringRef cfString = CFStringCreate...();
NSString *string = CFBridgingRelease(cfString);

6.NSCache VS NSDictionary

  • NSCache 胜过 NSDictionary 之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”通知时手工删减缓存。
  • NSCache还会先行删减 “最久未使用”对象。
  • NSCache并不会拷贝键,而是会保留它。
  • NSCache 是线程安全的,而NSDictionary“ 则绝对不具备此优势。

7.只有那种“重新计算起来很费事的”数据,才值得放入缓存,比如那些需要从网络获取或从磁盘获取的数据。

8.+ (void)load 方法

  • + (void)load,对于加入运行期系统的每个类以及分类来说,必定会调用此方法,而且仅调用一次。当包含类或分类的程序库载入系统时,就会执行此方法,而这通常就是指应用程序启动的时候,若程序是为 iOS 平台设计的,则肯定会在此时执行。
  • load 方法中使用其他类是不安全的。
  • load 方法并不像普通的方法那样,它并不遵从那套继承规则。分类和所属的类里,都可能出现 load 方法。此时两种实现代码都会调用,类的实现要比分类的实现先执行。
  • load 方法务必实现得精简些,也就是要尽量减少其所执行的操作,因为整个应用程序在执行 load 方法时都会阻塞。

9.+ (void)initialize 方法

  • 对于每个类来说,该方法会在程序首次用该类之前调用,且只调用一次。它是惰性调用的,只有程序用到了相关的类时,才会调用。
  • 此方法与 + (void)load方法不同的是,运行期系统在执行该方法时,是处于正常状态的,因此,从运行期系统完整度上来讲,此时可以完全使用并调用任意类中的任意方法。
  • + (void) initialize 方法与其他消息一样,如果某个类未实现它,而其超类实现了,那么就会运行超类实现的代码。

10.NSTimer 会保留其目标对象。如下:

#import "YMTimerCircularReferenceViewController.h"

@interface YMTimerCircularReferenceViewController ()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation YMTimerCircularReferenceViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(timerSelector) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)timerSelector {
    NSLog(@"%@",[NSDate date]);
}

- (void)dealloc {
    NSLog(@"YMTimerCircularReferenceViewController dealloc");
}

YMTimerCircularReferenceViewController pop 时,上面的 dealloc方法不被调用。

解决办法,加一个 NSTimer 类别:

#import "NSTimer+YMBlock.h"

@implementation NSTimer (YMBlock)

+ (NSTimer * _Nonnull)ym_timerWithTimeInterval:(NSTimeInterval)ti block:(nullable void (^)())block userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
    return [self timerWithTimeInterval:ti target:self selector:@selector(ym_blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
}

+ (void)ym_blockInvoke:(NSTimer *)timer {
    void (^block)() = timer.userInfo;

    if (block) {
        block();
    }
}

@end

注:此处虽然依然有保留换,self 引用 self,因为是类对象,无须回收。

你可能感兴趣的:(ios)