编写高质量iOS与OS X代码的52个有效方法 - 学习笔记 1、2

672 阅读16分钟



第一章   熟悉 Objective-C

1、了解 Objective-C 语言的起源

  • OC是“消息结构”(message structure)而非“函数调用”(function caling)。
  • OC在运行时才会检查对象类型,接收一条消息之后,执行的代码由运行期环境来决定。
  • 掌握C语言的内存模型与指针很重要。对象所占内存总是分配在“堆空间”中。


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

将引入头文件的时机尽量延后。
  • 向前声明:@class SomeClass;  
  • 除非确有必要,否则不要引入头文件。一般来说,应在某个类的头文件中使用向前声明来提及别的类,并在实现文件中引入那些类的头文件。这样做可以降低耦合。
  • 有时无法使用向前声明,如要声明某个类遵循某协议。此时尽量把“该类遵循某协议”的这条声明移至分类中。如果不行,就把协议单独放在一个头文件中再引入。


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

  • 应用字面量语法创建字符串、数值、数组、字典。与创建此类对象的常规方法相比,更简明扼要。
  • 应该通过取下标操作来访问数组下标或字典中的键所对应的元素。
  • 用字面量语法创建数组或字典时,务必确保值不含nil。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSNumber *someNumber1 = [NSNumber numberWithInt:1];
    NSNumber *someNumber2 = @1;
    
    NSArray *animals1 = [NSArray arrayWithObjects:@"cat", @"dog", @"mouse", nil];
    NSArray *animals2 = @[@"cat", @"dog", @"mouse"];
    /*
     用字面两语法创建数组时,若数组元素对象中有nil,则会抛出异常。
     “arrayWithObjects:”方法会一次处理各个参数,直到发现nil为止。
     
     使用字面量语法更安全。抛出异常比创建数组之后发现元素个数少了要好。
     */
    
    NSDictionary *dictA = [NSDictionary dictionaryWithObjectsAndKeys:
                           @"value1", @"key1",
                           @"value2", @"key2",
                           @"value3", @"key3", nil];
    
    NSDictionary *dictB = @{@"key1" : @"value1",
                            @"key2" : @"value2",
                            @"key3" : @"value3"};
    /*
     字典中的 Key 跟 Value 都必须是OC对象
     字典跟数组一样,存在nil的问题
    */
    
}


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

总之,勿使用预处理指令定义常量,应借助编译器来确保常量正确
  • 不要用预处理指令定义常量。这样定义出来的常量不含类型信息,编译器只是会在编译前据此执行查找与替换操作。
  • 在实现文件(.m文件)中使用 static const 来定义“只在编译单元内可见的常量”。此类常量不在全局符号表中,所以无须为其名称加前缀。
  • 在头文件中使用 extern 来声明全局常量,并在相关实现文件中定义其值。这种常量要出现在全局符号表中,所以其名称应加以区隔,通常用与之相关的类名做前缀。 

#define ANIMATION_DURATION 0.3

static const NSTimeInterval kAnimationDuration = 0.3;

//In the .h
extern NSString *const EOCStringConstant;
//In the .m
NSString *const EOCStringConstant = @"VALUE";


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

  • 应用枚举来表示状态、传递给方法的选项以及状态码等值。
  • 如果传递给某个方法的选项为枚举类型,且此选项可多个同时使用。那么将个选项值定以为2的幂,以便通过按位或操作将其组合起来。
  • 在处理枚举类型的 switch 语句中不要实现 default 分支


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

在对象之间传递数据并执行任务的过程叫做“消息传递”
当应用程序运行起来以后,为其提供相关支持的代码叫做“Objective-C 运行时环境”

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

“属性”(property)是OC的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”来访问。其中 getter方法 用于读取变量值,而 setter方法 用于写入变量值。

6.1、@property

property = ivar(实例变量) + getter + setter (存取方法)

在正规的OC编码风格中,存取方法有着严格的命名规范。因此,OC这门语言才能根据名称自动创建出存取方法。

在对象接口的定义中,可以使用属性,这是一种标准的写法,能够访问封装在对象里的数据。因此,也可以把属性理解为:编译器会自动写出一套存取方法,用以访问给定类型中具有给定名称的变量。如下的这个类:

@interface Person : NSObject

@property NSString *firstName;
@property NSString *lastName;

@end

上述代码写出来的类与下面这种写法等效:

@interface Person : NSObject

- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;

- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;

@end

要访问属性,可以使用“点语法“。编译器会把”点语法“转换为对存取方法的调用


6.2、autosynthesis(自动合成)

如果使用了属性,那么编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字:

@implementation Person

@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;

@end

若不想令编译器自动合成存取方法,则可以自己实现。但你不能同时自己实现 getter 和 setter。还有一种办法能阻止编译器自动合成存取方法,就是使用 @dynamic 关键字,它会告诉编译器:不要自动创建实现属性所用的实例变量,也不要为其创建存取方法。

//In the .h
@interface ViewController : UIViewController

@property NSString *firstName;
@property NSString *lastName;

@end

//In the .m
@implementation ViewController

@dynamic firstName, lastName;

@end

/*
假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候
没问题,但程序运行到instance.var = someVar, 或者 somevar = var时,
由于缺少 setter 和 getter,程序会崩溃。
编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
*/


6.3、属性特质

使用属性时需注意:各种特质设定也会影响编译器所生成的存取方法。

属性可以拥有的特质分为四类:

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

6.3.1、原子性

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

6.3.2、读 / 写权限

具备 readwrite(读写)特质的属性拥有 getter 和 setter。

具备 readonly(只读)特质的属性仅有获取方法。

6.3.3、内存管理语义

属性用于封装数据,而数据则要有“具体的所有权语义”。  

下面这一组特质仅会影响 setter方法:

  • assign。setter方法只会执行针对“纯量类型”(如 CGFloat 或 NSInteger等)的简单赋值操作。
  • strong。此特质表明该属性定义了一种“拥有关系”,为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • weak。此特质表明该属性定义了一种“非拥有关系”,为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同 assign 类似,然而在属性所指的对象遭到摧毁时,属性值会置空(nil out)。
  • unsafe_unretained。此特质的语义和 assign 相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。
  • copy。此特质所表达的所属关系与 strong 类似,然而设置方法并不保留新值,而是将其 copy。当属性类型为 NSString* 时,用此特质来保护其封装性。

6.3.5、方法名                                      

1、getter=<name> 指定“获取方法”的方法名。 

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

2、setter=<name> 指定“设置方法”的方法名,这种用法不太常见


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

直接访问实例变量 跟 通过属性来访问 的区别:

  • 由于不经过OC的“方法派发”,所以直接访问实例变量的速度更快。
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。
  • 如果直接访问实例变量,那么不会触发 KVO
  • 通过属性来访问有助于排查与之相关的错误。

要点:

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应通过属性来写。
  • 在初始户方法及 dealloc 方法中,总是应该直接通过实例变量来读写数据。
  • 有时会使用懒加载技术配置某份数据,此时需要通过属性来读取数据。


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

使用 NSObject 协议中声明的 “isEqual:” 方法来判断两个对象的等同性


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

“类族”可以隐藏“抽象基类”背后的实现细节。OC的系统框架中普遍使用此模式,如 UIKit 中的UIButton类,想创建按钮,只需调用下面这个“类方法”。使用者无需关心创建的按钮属于哪个类,也不用考虑按钮的实现细节。

+ (UIButton *)buttonWithType:(UIButtonType)type;


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

有时候类的实例可能是由某种机制所创建的,而开发者无法令这种机制创建出自己所写的自类实例。OC通过 关联对象 这一特性解决此问题。

可以给某对象关联许多其他对象,这些对象通过“键”来区分。存储对象值的时候,可以指明“存储策略”,用以维护相应的“内存管理语义”。

10.1、存储策略由名为 objc_AssociationPolicy 的枚举所定义


10.2、管理关联对象的方法

//此方法以给定的键和策略为某对象设置关联对象值
objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy)
    
//此方法根据给定的键从某对象中获取相应的关联对象值
objc_getAssociatedObject(id object, void *key)
    
//此方法移除指定对象的全部关联对象
objc_removeAssociatedObjects(id object)

关联对象保存在一个全局的AssociationsManager里边,原理在此


11、理解 objc_msgSend 的作用

在对象上调用方法是OC经常使用的功能,用哪个OC的术语来说,这叫做“传递消息”。消息有 “名称” 和 “选择子”,可以接受参数,而且可能还有返回值。
C语言的函数调用方式:C语言使用“静态绑定”,就是在编译期就能决定运行时所应调用的函数。

给对象发送消息可以这样写:

id returnValue = [someObject messageName:parameter];

本例中,someObject 叫做“接收者”,messageName 叫做“选择子”。选择子与参数合起来称为“消息”。编译器看到此消息后,将其转换为标准的C语言函数调用,所调用的函数乃是消息传递机制中的核心函数,叫做 objc_msgSend,其“原型”如下:

//这是个“参数个数可变的函数”,能接受两个或两个以上的参数
void objc_msgSend(id self, SEL cmd, ...)

编译器会把本例中的消息转换为如下函数:

id returnValue = objc_msgSend(someObject, @selector(messageName:), parameter);

  • objc_msgSend 函数会根据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜寻其“方法列表”,如果找到了与选择子名称相符的方法,就跳转至其实现代码。
  • 若找不到,就沿着继承体系继续向上查找,等找到合适的方法之后再跳转。
  • 如果最终还是找不到相符的方法,那就执行”消息转发“操作。
  • objc_message 会将匹配结果缓存在“快速映射表”里面,每个类都有这样一块缓存。


12、理解消息转发机制

对象在收到无法解读的消息之后会发生什么情况?-- 会启动“消息转发”机制

消息转发过程,分为两大阶段:

第一阶段:先征询接收者所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”,这叫做“动态方法解析”。

如果第一阶段执行完,那么接收者自己就无法再以动态新增方法的手段来响应包含该选择子的消息。此时来到第二阶段,这里涉及“完整的消息转发机制”。

第二阶段:会先让接收者看看有没有其他对象能处理这条消息,若有,则运行时系统会把消息转发给那个对象,于是消息转发过程结束,一切正常。

第三阶段:若没有“被援的接收者”,则启动完整的消息转发机制,运行时系统会把与消息有关的全部细节都封装到 NSInvocation 对象中,再给接收者最后一次机会。

12.1、动态方法解析

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

+ (BOOL)resolveInstanceMethod:(SEL)selector

该方法的参数就是那个未知的选择子,其返回值是布尔类型,表示这个类是否能新增一个实例方法用一处理此选择子。

在继续往下执行转发机制之前,奔雷有机会新增一个处理此选择子的方法。

假设尚未实现的方法不是实例方法而是类方法,那么运行时系统会调用另外一个方法,该方法是:resovleClassMethod:

调用 class_addMethod: 来新增一个实例方法

12.2、备援接收者

当前接收者还有第二次机会能处理未知的选择子。在此,运行时系统会问它:能不能把这条消息转给其他接收者来处理。会调用下列方法:

- (id)forwardingTargetForSelector:(SEL)selector

该方法的参数就是那个未知的选择子,若当前接收者能找到被援对象,则将其返回。若找不到,就返回 nil。

我们无法操作经由这一步所转发的消息。若想在发送给被援接收者之前先修改消息内容,那就的通过完整的消息转发机制来做了。

12.3、完整的消息转发

首先创建 NSInvocation 对象,把与尚未处理的那条消息有关的全部细节都封于其中。在触发 NSInvocation 对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。会调用下列方法:

- (void)forwardInvocation:(NSInvocation *)invocation

实现此方法时,若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至 NSObject。

如果最后调用了 NSObject 类的方法,那么该方法会调用 “doesNotRecognizeSelector:”  以抛出异常,此异常表明选择子最终未能得到处理。

消息转发全流程:



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

给定的选择子名称相对应的方法可以在运行期改变,此方案成为“方法调配”。

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

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

想交换方法的实现,可用下列函数:

void method_exchangeImplementaions(Method m1, Method m2)

此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass, SEL aSelector)

举个例子:


假设在调用 lowercaseString 时记录某些信息,这时就可以通过交换方法实现来达成此目标。我们新编写一个方法,在此方法中实现所需的附加功能,并调用原有实现。

新增的方法添加至NSString的一个分类中:

//In the .h
@interface NSString (Category)

- (NSString *)eoc_myLowercaseString;

@end

//In the .m
@implementcation NSString (Category)

- (NSString *)eoc_myLowercaseString {
    NSString *lowercase = [self eoc_myLowercaseString];
    NSLog(@"%@ => %@", self, lowercase);
    return lowercase;
}

/*
这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,
此方法是准备和 lowercaseString 方法互换的,所以不会有问题
*/

@end

最后通过下列代码交换这两个方法实现:

Method oMethod = class_getClassMethod([NSString class], @selector(lowercaseString));
    
Method sMethod = class_getClassMethod([NSString class], @selector(eoc_myLowercaseString));
    
method_exchangeImplementations(oMethod, sMethod);

通过此方案,开发者可以为那些黑盒方法增加日志记录功能。


15、理解 “类对象” 的用意

  • 对象类型并非在编译期就绑定的,是在运行期去查找。
  • 有一个特殊的类型叫做 id,它能指代任意的 Objective-C 对象类型。
  • 每个OC对象实例都是指向某块内存数据的指针。

15.1、id 类型定义,它是 objc_object 结构体

typedef struct objc_object {
    Class isa;
} *id;

  • 每个对象结构体的首个成员是 Class 类的变量。该变量定义了对象所属的类,通常成为“isa”指针。
  • 举个例子:NSString *pointerVariable = @"Some string"; 这个例子中,所用的对象“是一个” (isa)NSString,所以其 “isa” 指针就指向 NSString

15.2、Class 类型定义,它是 objc_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 obje_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};

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


15.3、再累继承体系中查询类型信息

  • “isMemberOfClass:” 能够判断出对象是否为某个特定类的实例。
  • "isKindOfClass:" 能够判断出对象是否为某类或其派生类的实例。