编写OC高质量的代码的有效方法

跨越美利坚面试创业技术培训 2018-01-13

1. 写这个只是为了自己记忆,有相关pdf文件,如需要留下邮箱。。

2. 在类的头文件中尽量少引入其他头文件

  • 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类(使用@class),并在实现文件中引入那些类的头文件,这样做可以尽量降低类之间的耦合。
  • 如果要声明某个类遵循某个协议,应该把这个协议放到分类中,或者把协议单独放在一个头文件中,然后将其引入。

3. 多用字面量语法,少用与之等价的方法

下面是两种方式的对比:

// 使用字面量语法的例子
NSArray *array1 = @[@"1",,@"2"];

NSNumber *number1 = @1;

NSDictionary *dictionary1 = @{@"key":@"value"};

// 使用与之对应的方法
NSArray *array2 = [NSArray arrayWithObjects:@"1",@"2",nil];

NSNumber *number2 = [NSNumber numberWithInt:2];

NSDictionary *dictionary2 = [NSDictionary dictionaryWithWithObjectsAndKeys:@"value":@"key"];
  • 使用字面量语法来创建字符串、数值、数组、字典。与常规方法相比,更加简明扼要
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素
  • 使用字面量语法创建数组或字典时,若值中有nil,则会抛出异常,因此,需确保值里面不含nil

4. 多用类型常量,少用#define预处理指令

定义一个常量的方法:

// 第一种:预处理指令
#define ANIMATION_DURATION 0.3

// 第二种:定义静态常量
static const NSTimeInterval kAnimationDuration = 0.3

我们一般推荐使用第二种,这个方式定义的常量包含类型信息,有助于代码阅读。

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

如我们需要对外公布某个常量,我们可以写成下面的代码:

// Test.h
#import <Foundation/Foundation.h>

extern NSString *const TestDidChangeNotification;

@interface Test : NSObject

@end

// Test.m

#import "Test.h"

NSString *const TestDidChangeNotification = @"TestDidChangeNotification";

@implementation Test
  • 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前根据此执行查找和替换。即使有人重新定义了常量值,编译器也不会有警告,这将导致应用程序中的常量值不一致
  • 在.m文件中使用 static const 来定义“编译单元内可见常量”,无需加类名前缀,加k
  • 在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值,这种常量要加类名前缀。

5. 用枚举来表示状态、选项、状态码

  • 使用枚举来表示状态机的状态、传递给方法的选项以及状态码等值,给这些值起个易懂的名字
  • 用NS_ENUM 与 NS_OPTIONS 宏来定义枚举类型,并指明其底层数据类型。
  • 在处理枚举类型的switch语句中不要事先default分支,这样的话,加入新枚举之后,编译器就会提示开发者:switch语句并未处理所有枚举

6. 理解“属性”这一概念

  • 使用@property语法来定义对象中所封装的数据
  • 通过“特质”属性关键字来指定存储数据所需的正确语义
  • 在设置属性所对应的实例变量时,一定要遵从该属性所声明的语义。

7. 在对象内部尽量直接访问实例变量

比如,Person类有个name属性,我们在这个类的内部想获取这个name属性的数据的时候,一种是通过 self.name,一种是 _name.

这两种的区别:

  • 直接访问实例变量的速度比较快,编译器所生成的代码会直接访问保存对象实例变量的那块内存
  • 直接访问实例变量,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”,比如,在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会保留新值,释放旧值
  • 如果直接访问实例变量,那么不会触发“KVO”,这样做是否会产生问题,取决于具体的对象行为。
  • 通过属性来访问有助于排查与之相关的错误,因为可以给“获取方法”或“设置方法”中新增“断点”,监控该属性的调用者及其访问时机。

注意点:

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写
  • 在初始化方法及dealloc方法中,总是应该直接通过实例变量来读写数据
  • 有时候会使用惰性初始化技术配置某份数据,这种情况下,需要通过属性来读取数据

8. 理解“对象等同性”这一概念

  • 若想检测对象的等同性,请提供“isEqual:”与hash方法
  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同
  • 不要盲目的逐个检测每条属性,而是根据具体需求来指定方案

9. “以类族模式”隐藏实现细节

“类族”是一种很有种的模式,可以隐藏“抽象基类”背后的实现细节。OC的系统框架中普遍使用此模式,比如有一个处理雇员的类,每个雇员都有“名字”和“薪水”这两个属性,管理者可以命令其执行日常工作,但是各种雇员的工作内容却不同,经理在带领雇员做项目时,无需关系每个人如何完成其具体工作,仅需指示其开工就行。我们重构多个子类,把每个人完成具体工作的方法,在子类实现。

首先定义一个抽象基类:

typedef NS_ENUM(NSUInteger, EOCEmployeeType){
    EOCEmployeeTypeDeveloper,
    EOCEmployeeTypeDesigner,
    EOCEmployeeTypeFinance     
}

@interface EOCEmployee : NSObject
@property (copy, nonatomic) NSString *name;
@property (assign, nonatomic) NSInteger salary;

// 创建一个雇员对象
+(EOCEmployee*)employeeWithType:(EOCEmployeeType)type;

// 让雇员工作
- (void)doADaysWork;

@implementation EOCEmployee

+ (EOCEmployee *)employeeWithType:(EOCEmployeeType)type{
    switch (type){
          case EOCEmployeeTypeDeveloper:
                  return [EOCEmployeeTypeDeveloper new];
                  break;
          case EOCEmployeeTypeDeveloper:
                  return [EOCEmployeeTypeDesigner new];
                  break;
           case EOCEmployeeTypeDeveloper:
                  return [EOCEmployeeTypeFinance new];
                  break;
    }  
}

- (void)doADayWork{
  // 子类去实现
}

@end

然后,每个“实体子类”都从基类继承而来,例如:

@interface EOCEmployeeDeveloper : EOCEmployee

@end

@implementation EOCEmployeeDeveloper

- (void)doADaysWork{
   [self wirteCode];
}

@end

在本例中,基类实现了一个“类方法”,该方法根据待创建的雇员类别分配好对应的雇员类实例,这种“工厂模式”是创建类族的办法之一。

如果对象所属的类位于某个类族中,你可能觉得自己创建了某个类的实例,然而实际上创建的却是其子类的实例。

OC中的NSNumber、NSArray等都是类族。

  • 类族模式可以把实现细节隐藏在一套简单的公共接口后面。
  • 系统框架中经常使用类族
  • 从类族的公共抽象基类中集成子类时要当心,若有开发文档,应先阅读。

10. 在既有类中使用关联对象存放自定义数据

有时候需要在对象中存放相关信息,这时候我们通常都会从对象所属类中继承一个子类,然后改用这个子类对象,但是有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的子类实例。OC中有一项强大的特性可以解决,就是“关联对象”。

基于runtime来实现,此处就不多说。

  • 可以通过“关联对象”机制来把两个对象连起来。
  • 定义关联对象时可指定内存管理语义,用以模仿定义属性时所采用的“拥有关系”与“非用有关系”
  • 只有在其他做法不可行时才应该选用关联对象,这种做法通常会引入难于查找的bug

11. 理解objc_msgSend的作用

在对象上调用方法是OC中经常使用的功能。专业术语叫做:“传递消息”。消息有“名称”或“选择子”,可以接受参数,而且可能还有返回值。

C语言使用“静态绑定”,在编译期就能决定运行时所应调用的函数。

OC中使用“动态绑定”,对象接收到消息之后,究竟该调用哪个方法则完全于运行期决定,甚至可以在程序运行时改变。

这里就不多解释objc_msgSend的使用,如有需要可以看runtime的使用。

objc_msgSend 函数会根据接收者和选择子的类型来调用适当的方法,为了完成此操作,该方法需要在接收者所属的类中找到其“方法列表”,如果能找到与选择子名称相符的方法,就跳至其实现代码,如果找不到,那就沿着继承体系向上查找,如果最终没找到,则执行“消息转发”操作。每个类都会有一块缓存,用来缓存方法,若是稍后还向该类发送与选择子相同的消息,那么执行起来就会很快了。

  • 消息由接收者,选择子及参数构成。给某对象“发送消息”,也就相当于在该对象上“调用方法”
  • 发给某对象的全部消息都要由“动态消息派发系统”来处理,该系统会查出对应的方法,并执行其代码。

12. 理解消息转发机制

当对象接收到无法解读的消息后,就会启动“消息转发”机制,程序员可经由此过程告诉对象应该如何处理未知消息。

如果在控制台中看到 unrecognized selector sent to instance 0x87 就说明你曾向某个对象发送过一条其无法解读的消息,从而启动了消息转发机制,然后以程序崩溃而告终。

消息转发分为两个阶段:

  1. 征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子(unknown selector)”,这叫做“动态方法解析”
  2. 第二阶段,涉及“完整的消息转发机制”,如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这又分为两小步:
    1. 首先,看接收者看看有没有其他对象能否处理这条消息
    2. 如果有,则运行期系统会把消息转给那个对象,于是转发郭恒结束,如果没有“备用的接收者”,则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。

动态方法解析:

对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:

// 如果该类调用了一个没有实现的实例方法,会调用此方法<br />+ (BOOL)resolveInstanceMethod:(SEL)selector<br />// 如果该类调用了一个没有实现的类方法,会调用此方法<br />+ (BOOL)resolveClassMethod;

该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。在继续往下执行转发机制之前,我们可以使用runtime动态的增加这个方法。

使用这种办法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。

备用接收者:

当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来处理:

// 方法参数代表未知的选择子,若当前接收者能够找到备援对象,则将其返回,如果找不到就返回nil。
- (id)forwardingTargetForSelector:(SEL)selector;

我们可以用“组合”来模拟出“多重继承”的某些特性,在一个对象内部,可能还有其它一系列对象,该对象可经由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像是由该对象亲自处理的。

完整的消息转发:

如果转发已经来到这一步的话,那么唯一能做的就是启用完整的消息转发机制了,系统会创建NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封装于其中,此对象包含选择子、目标(target)及参数,在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。

此步骤会调用下列方法来转发消息:

// 该方法可以实现的很简单,只需要改变调用目标,是消息在新目标上得以调用即可,然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方法,比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是改换选择子等等。
- (void)forwardInvocation:(NSInvocation *)invocation;

实现此方法时,若发现某调用操作不应由本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直到NSObject,如果最后调用了NSObject类的方法,那么该方法还会继续调用“doesNotRecognizeSelector”,以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程:

编写OC高质量的代码的有效方法编写OC高质量的代码的有效方法

接收者在每一步中均有机会处理消息,步骤越往后,处理消息的代价就越大,最好能在第一步处理完,这样的话,运行期系统就可以将此方法缓存起来了,如果这个类的实例稍后还收到同名选择子,那么根本无需启动消息转发流程。如果想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二部执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。

  • 若对象无法响应某个选择子,则进入消息转发流程。
  • 通过运行期的动态方法解析功能,我们可以在需要用到某个方法时再将其加入类中。
  • 对象可以把其无法解读的某些选择子转交给其它对象来处理
  • 经过上述两步之后,如果还是没有办法处理选择子,那就启动完整的消息转发机制。

http://www.cocoachina.com/ios/20150604/12013.html 相关的例子

13. 用“方法调配技术”调试“黑盒方法”

主要就是runtime的方法交换,runtime具体可见OC类目中关于runtime的介绍。

我们在这里简单的分析下:

类的方法列表会把选择子的名称映射到相关的方法实现直上,使得“动态消息派发系统”能够据此找到应该调用的方法,这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:

id (*IMP)(id,SEL,...)

比如,NSString 类可以相应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上:

编写OC高质量的代码的有效方法

OC运行期系统提供的几个方法都能够用来操作这张表,开发者可以向其中新增选择子,也可以改变某选择子所对应的方法实现,还可以交换两个选择子所映射到的指针,比如我们交换 lowercaseString 和 uppercaseString 的方法实现,类的方法表就会变成以下这个样子:

编写OC高质量的代码的有效方法

在新的映射表中,我们可以看到交换了lowercaseString 和 uppercaseString 的方法实现,并且多了一个名为newSelector的选择子,上述修改均无需编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。

通过此方案,开发者可以为那些“完全不知道其具体实现”的黑盒方法增加日志记录功能,这有助于程序调试。

  • 在运行期,可以向类中新增或替换选择子所对应的方法实现。
  • 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,也就是方法交换,开发者常用此技术向原有实现中添加新功能。
  • 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

14. 理解“类对象”的用意

对象类型并非在编译期就绑定好了,而是要在运行期查找。而且,还有个特殊的类叫做id,它能指代任意的OC对象类型,一般情况下,应该指明消息接收者的具体类型,这样的话,如果向其发送了无法解读的消息,那么编译器就会产生警告信息,而类型为id的对象则不然,编译器嘉定它能够响应所有的消息。

“在运行期检视对象类型”,这个操作也叫做“类型信息查询”(内省),这个强大而有用的特性内置于Foundation框架的NSObject协议里,凡是由公共根类(common root class)继承而来的对象都要遵从此协议。在程序中不要直接比较对象所属的类,明智的做法是调用“类型信息查询方法”。

在此之前,我们看下OC对象的本质是什么?

每个OC对象实例都是指向某块内存数据的指针,所以在声明变量时,类型后面要跟一个“*”字符,如下:

// pointerVariable可以理解成存放内存地址的变量,而NSString 自身的数据就存储于那个地址中,因此可以说,该变量”指向“NSString 实例。所有OC对象都是如此,
NSString *pointerVariable = @"Some string";

描述OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也定义在这里:

typedef struct objc_object{
    Class isa;
}*id;

每个对象,结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为“is a”指针,例如,刚才的例子中所有的对象“是一个”(is a)NSString,所以其“is a”指针就指向NSString。Class对象也定义在运行期程序库的头文件中:

typedef stuct 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 *methodList;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
}

此结构体存放类的“元数据”,例如类的实例实现了几个方法,具备多少个实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量为super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型),是另外一个类,叫做“元类”,用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个“类对象”,而每个“类对象”仅有一个与之相关的“元类”。

super_class 指针确立了继承关系,而isa指针描述了实例所属的类。

  • 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Class对象则构成了类的继承体系。
  • 如果对象类型无法再编译期确定,那么就应该使用类型信息查询方法来弹指
  • 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。

15. 用前缀避免命名空间冲突

应该为所有的名称都加上适当的前缀,比如,你所在的公司焦作Effective Widgets,那么就可以在公共部分代码中使用EWS做前缀,如果有些代码只用于Effective Browser的浏览器项目中,可以使用EWB作前缀。

前缀最好是三个字母的,因为Apple宣称其保留使用所有“两字母前缀”。

  • 选择与你的公司,应用程序或二者皆有关联之名称作为类名的前缀,并在所有代码中使用这一前缀
  • 若自己所开发的程序库中用到了第三方库,则应为其中的名称加上前缀。

16. 提供“全能初始化方法”

UITableViewCell,初始化该类对象时,需要指明其样式及标示符,标示符能够区分不同类型的单元格,由于这种对象的创建成本较高,所以绘制表格时可依照标示符来复用,以提升程序效率,我们把这种可为对象提供必要信息以便其能完成工作的初始化方法叫做“全能初始化方法”。

// 比如创建一个NSDate
- (id)init;
- (id)initWithString:(NSString *)string;
- (id)initWithTimeIntervalSinceNow:(NSTimeInterval)seconds;
- (id)initWIthTimeIntervalSinceRefrenceDate:(NSTimeInterval)seconds;

第四个方法是全能初始化方法,也就是说其余的初始化方法都要调用它,当底层数据存储机制改变时,只需修改此方法的代码。

  • 在类中提供一个全能初始化方法,并在文档里指明。其它初始化方法均应调用此方法
  • 若全能初始化方法与超类不同,则需要复写超类中的对应方法。
  • 如果超类的初始化方法不适用子类,那么应该复写这个超类方法,并在其中抛出异常。

17. 实现description方法

调试程序的时候,经常需要打印并查看对象信息,我们可以重写该对象的description方法,如下:

编写OC高质量的代码的有效方法
  • 实现description方法返回一个有意义的字符串,用以描述该实例
  • 若想在调试时打印出更详尽的对象描述信息,则应实现debugDescription方法

18. 尽量使用不可变对象

设计类的时候,应充分运用属性来封装数据,尽量把对外公布出来的属性设为只读,而且只在确有必要时才将属性对外公布。

  • 尽量创建不可变的对象
  • 若某属性仅可于对象内部修改,则在.m文件中,则将其由readonly变成readwrite属性。
  • 不要把可变的collection作为属性公开,而应提供相关方法,以此修改对象中的collection

19. 使用清晰而协调的命名方式

给方法命名时注意事项:

  • 如果方法的返回值是新创建的,那么方法名的某个词应该是返回值的类型,除非还有修饰语,如:localizedString。属性的存取方法不遵循这种命名方式。
  • 应该把表示参数类型的名词放在参数前面。
  • 如果方法要在当前对象上执行操作,那么应该包含动词。
  • 不要使用str这种简称,使用全程。
  • Boolean属性应加is前缀。如果某方法返回非属性的Boolean值,那么应该根据其功能,选用has或is当前缀。
  • 将get这个前缀留给那些借由”输出参数“来保存返回值的方法。

总结:

  • 起名时应遵从标准的OC命名规范,这样创建出来的接口更容易为开发者所理解。
  • 方法名要言简意赅
  • 方法名不要使用缩略后的类型名称
  • 给方法起名时的第一要务就是确保其风格与你自己的代码或所要继承的框架相符。

20. 为私有方法名加前缀

一个类所做的事情通常都要比从外面看到的更多,编写类的实现代码时,经常要写一些在内部使用的方法。应该为这种方法的名称加上某些前缀,这有助于调试,因为据此很容易就能把公共方法和私有方法区别开。

具体使用何种前缀,可根据个人喜好来定,其中最好包含下划线和字母p,比如p_method。不要使用 _method,因为Apple公司喜欢单用一个下划线做私有方法的前缀,可能会引起冲突。

  • 给私有方法的名称加上前缀,这样可以很容易地将其同公共方法区分开
  • 不要单用一个下划线做私有方法的前缀,因为这种做法是留给苹果公司用的。

21. 理解OC错误模型

  • 只有发生了可使整个应用程序崩溃的严重错误时,才使用异常。
  • 在错误不严重的情况下,使用NSError

22. 理解NSCopying协议

  • 若想让自己所写的对象具有拷贝功能,则需要实现NSCopying协议
  • 如果自定义的对象分为可变和不可变,那么就要同时实现NSCopying和NSMutableCopying协议
  • 复制对象时需决定采用浅拷贝还是深拷贝,一般情况下执行浅拷贝

23. 通过委托与数据源协议进行对象间通信

委托模式:定义一套接口,某对象若想接受另一个对象的委托,则需要实现这个接口,以便成为其"委托对象",而这”另一个对象“则可以给其委托对象回传一些信息,也可以在发生相关事件时通知委托对象。

  • 委托模式为对象提供了一套接口,使其可由此将相关事件告知其它对象
  • 将委托对象应该支持的接口定义成协议,在协议中把可能需要处理的事件定义成方法
  • 当某对象需要从另外一个对象中获取数据时,可以使用委托模式,比如 tableView的dataSource
  • 如果有必要,可实现含有位段的结构体,将委托对象是否能响应相关协议方法这一信息缓存下来,比如,声明一个属性,记录是否实现了某个方法。

24. 将类的实现代码分散到便于管理的数个分类之中

  • 使用分类机制把类的实现代码划分成易于管理的小块
  • 将应该视为”私有“的方法归入名叫Private的分类中,隐藏实现细节。

25. 总是为第三方类的分类名称加前缀

比如你想给系统类添加个方法,如果你没有添加前缀的话,可能会覆盖其方法。

  • 向第三方类中添加分类时,总应给其名称加上你专用的前缀。
  • 给其中的方法名加上你专用的前缀。

26. 不要再分类中声明属性

  • 把封装数据所用的全部属性都定义在主接口里
  • 在分类中,可以定义存取方法,但尽量不要定义属性。

27. 使用 "class-continuation分类"隐藏实现细节

"class-continuation分类"和普通的分类不同,它必须定义在其所接续的那个累的实现文件里。其重要之处在于,这是唯一能够声明实例变量的分类,而且此分类没有特定的实现文件,其中的方法都应该定义在类的主实现文件里。而且,和其它分类不同,它没有名字,比如:

@interface Person ()
// Methods here
@end
  • 通过“class-continuation分类”向类中新增实例变量
  • 如果某属性在主接口中声明为只读,而类的内部又要用设置方法修改此属性,那么就在“class-continuation分类”中将其扩展为“可读写”
  • 把私有方法的原型声明在“class-continuation分类”里面
  • 若想让类所遵循的协议不为人所知,则可于“class-continuation分类”中声明。

28. 通过协议提供匿名对象

如下面的代码:

@property (nonatomic, weak) id <WCEDelegate> delegate;

由于该属性的类型id<EOCDelegate>,所以实际上任何类的对象都能充当这一属性,对于具备此属性的类来说,delegate就是”匿名的“。

  • 协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵从某协议的id类型,协议里规定了对象所应实现的方法
  • 使用匿名对象来隐藏类型名称
  • 如过具体类型不重要,重要的是对象能够响应(定义在协议里的)特定方法,那么可使用匿名对象来表示。

29. 理解引用计数

  • 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1.若保留计数为正,则对象继续存活,当保留计数将为0时,对象就销毁了
  • 在对象声明期中,其余对象通过引用来保留或释放此对象,保留和释放操作分别会递增及递减保留计数

30. ARC注意事项

  • 在ARC之后,程序员就无需担心内存管理问题了
  • 不要手动管理
  • CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease.

31. 在dealloc方法中只释放引用并解除监听

对象在经历其生命周期后,最终会为系统所回收,这时就要执行dealloc方法,在每个对象的生命周期内,此方法仅执行一次,也就是当保留计数为0的时候,然而具体何时执行,则无法保证。

在dealloc方法中,一般都是移除观测行为,注销通知。

  • 在dealloc方法里,应该做的事情就是释放指向其它对象的引用,并取消原来订阅的”kvo“或通知中心的等通知,不要做其它事情
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。
  • 执行异步任务的方法不应再dealloc里,只能在正常状态执行的哪些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。

32. 以弱引用避免循环引用

如果两个对象,相互引用,那么这两个对象都无法被释放,产生内存泄露。

unsafe_unretained 和 weak的区别:

当指向某个实例的引用移除后,unsafe_unretained属性仍指向那个已经回收的实例,而weak属性则指向nil。weak比unsafe_unretained应用可以令代码更安全。

  • 当某些引用设为weak,可避免出现循环引用
  • weak引用可以自动清空,也可以不自动清空。

33. 自动释放池

  • 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里
  • 合理运用自动释放池,可降低应用程序的内存峰值
  • 使用@autoreleasepool

34. 为常用的block类型创建typedef

比如:

typedef void(^WCECompletionHander)(NSData *data);
  • 用typedef重新定义块类型,可让块变量用起来更加简单
  • 定义新类型时,应遵循命名规则

35. 使用block降低代码分散程度

  • 在创建对象时,可以使用内联的handler代码块将相关业务逻辑声明
  • 比如网络请求一般使用代码块来回调数据

相关推荐