Objective-C Programming 阅读笔记

2020-03-09
iOS

实例变量,属性

成员变量

  • 成员变量的默认修饰是 @protected
  • 成员变量不会自动生成 setget 方法,需要自己手动实现。
  • 成员变量不能用点语法调用,因为没有 setget 方法,只能使用->调用。

按照惯例,实例变量的名字是以下划线”_”开始的。使用下划线的前缀,可以很容易分辨实例变量和局部变量。下划线没有特别的语法意义,它只是实例变量名字的第一个字符。

属性的特征

属性有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。

  • 属性的默认修饰是 @protected
  • 属性会自动生成set和get方法。
  • 属性用点语法调用,点语法实际上调用的是 setget 方法。

属性修饰:

  • nonatomic / atomic -> 原子性
  • readonly / readwrite -> 只读 读写属性
  • copy -> 可拷贝

@synthesize

1
@synthesize mushroom = _mushroom;

@synthesize 指令告诉编译器有一个叫做 _mushroom 的实例变量,它是 mushroom 以及 setMushroom 的实例变量。如果它不存在,就要将它创建出来。

但如果只写一个 @synthesize 指令,编译器就会警告说 _mushroom 是未经定义的。

isa 指针

NSObject 虽然拥有很多方法,但是只有一个实例变量:isa 指针。任何一个对象的 isa 指针都会指向创建该对象的类。

给对象发送消息的时候,对象就会查询是否有该消息名的方法。搜索会通过 isa 指针找到该对象的类并查询 “是否有名为消息名的实例方法?” 如果没有,就会继续查询它的父类。以此类推,对象会沿着继承链向上查询,直到找到名为消息名的方法,或到达继承链的顶端(NSObject)为止。

@class XXXX

这行代码的作用是告诉编译器,程序有一个名为 BNRAsset 的类。 当使用 @class 而不是 #import 时,编译器不会查看文件的全部内容,因此处理速度会更快。之所以可以在头文件中使用 @class,是因为编译器不需要知道实现细节就能处理文件中的所有声明。

类扩展

不是所有的属性或方法都需要在类的头文件中声明。有的属性或方法只是该类或其实例才需要使用的。设计实现细节的属性或方法最好在类扩展中声明。类扩展是一组私有的声明。只有类和其类实例才能使用在类扩展中声明的属性,实例变量或方法。

通常,类扩展是添加在类实现文件中,实现方法的 @implementation 之上的。

1
2
3
4
5
6
7
8
@interface BNREmployee ()

@property (nonatomic) unsigned int officeAlarmCode;

@end

@implementation BNREmployee
...

ARC

  • 如果用来创建对象的方法,其方法名是以 allocnew 开头的,或者包含 copy,那么你已经得到了该对象的所有权(即可以假设新对象的 retain 计数是1,而且该对象不在 NSAutoreleasePool 对象中)。你要负责在不需要使用该对象的时候释放。以下是部分常见的,会 “传输” 所有权的方法:alloc(后面总会跟一个 init 方法),copymutableCopy
  • 通过任何其他途径创建的对象(例如通过便捷方法),你是没有什么所有权的(即可以假设新对象的 retain 计数是 1,而且假设对象已经在 NSAutoreleasePool 对象中。如果没有保留该对象,那么当 NSAutoreleasePool 对象被 “排干” 时,这个对象就会被释放。
  • 如果你不拥有某个对象,但是要确保该对象能够继续存在,那么可以通过向其发送 retain 消息来获得所有权(这会使 retain 计数加 1)
  • 当你拥有某个对象并且不再需要该对象的时候,可以向其发送 release 消息或 autorelease 消息(release 消息会使 retain 计数立刻减 1. autorelease 会导致: 当 NSAutoreleasePool 对象被 “排干” 时,再向相应的对象发送 release 消息)
  • 只要对象还有至少一个拥有方,该对象就会继续存在下去(当该对象的 retain 计数到达 0 时,就会收到 dealloc 消息)。

== 与 isEqual

== 比较两个对象的内存地址

isEqualNSObject 的方法,

  • 首先会判断指针是否相等
  • 再判断是否是同类对象或非空
  • 再依次判断对象对应的属性是否相等

hashisEqual: 方法都在 NSObject 协议中声明,且彼此关系紧密。实现 hash 方法必须返回一个整型数(NSInterger),作为哈希表结构中的表地址。

两个对象相等(isEqual: 方法判断的结果)意味着它们有相同的哈希值。如果哈希值相同,两个对象不一定相等。

isEqualToStringNSString 的方法

不可修改对象

使用不可修改的 collection 可以节约内存提高性能,因为它永远无须拷贝。而对于可修改对象,这可能发生这么一种情况:程序中的其他代码可能在你使用这个可修改对象的时候修改它的内容。为了避免这种情况发生,就需要复制一份私有的拷贝。而每个程序的代码都可能做一份私有的拷贝,这样就可能会有多份一样的对象拷贝。

全局变量

头文件中:

1
extern NSString const *NSLocaleCurrencyCode;

const 的意思是在程序的整个运行过程中, NSLocaleCurrencyCode 指针的值不会发生变化。
extern 的意思是 NSLocaleCurrencyCode 是存在的,但是会在另一个文件中定义。

实现文件:

1
NSString const *NSLocaleCurrencyCode;

enum

1
2
3
4
5
typedef enum {
BlenderSpeedStir = 1,
BlenderSpeedChop = 2,
....
} BlenderSpeed;

从 OS X 10.8 和 iOS 6 系统开始,Apple 引入了一种新的 enum 声明语法: NS_ENUM()

1
2
3
4
5
typedef NS_ENUM(int, BlenderSpeed) {
BlenderSpeedStir,
BlenderSpeedChop,
...
}

NS_ENUM() 实际上是一个预处理宏,它带有两个实参:数据类型和名字。 NS_ENUM() 最重要的优点是它可以声明整数数据类型(short, unsigned, long等)。

如果使用旧的语法,编辑器会为 enum 选择合适的数据类型,通常是 int 类型。如果你的 enum 只需枚举四个常量,那么它们的值是什么都无所谓,就不需要四个字节来存储它。一个字节就可以存储到 255 的整数。 使用 NS_ENUM 声明一个节省内存的 enum:

1
2
3
4
typedef NS_ENUM(char, BlenderSpeed) {
BlenderSpeedStir,
...
}

selector

当某个对象收到消息时,会向该对象的类进行查询,检查是否有与消息名称相匹配的方法。该查询过程会沿着继承层次结构向上,直到某个类回应”我有与消息名称相匹配的方法”。

方法的查询非常快速。如果使用方法的实际名称(可能会很长)进行查询,那么查询速度会很慢。为了提速,编译器会为每个其接触过的方法附上一个唯一的数字。运行时,程序使用的是这个数字,而不是方法名。

以上提到的代表特定方法名的唯一数字称为选择器(selector)。当一个方法需要一个选择器作为实参,它实际就是需要这个数字。通过编译指令 @selector 可以得到与方法名相应的选择器。

Block 对象

Block 对象是一段代码。

1
2
3
^{
NSLog(@"This is an instruction within a block.")
}

看上去和 C 函数类似,都是在一个花括号内的一套指令。但是他没有函数名,相应的位置只有一个 ^ 符号。^ 表示这段代码是一个 Block 对象。

1
void (^devowekuzer)(id, NSUInteger, BOOL*)

Block 变量的名字(如 devowelizer)是写在括号中,跟在 ^ 字符后面的。Block 的声明需要包括 Block 的返回类型(void)以及它的实参的类型(id, NSUInteger, BOOL*), 这点类似函数的声明。

那么 Block 变量是什么类型的呢? 它不是一个简单的 “block”。它的类型是一个有着三个参数(一个对象指针,一个整数和一个BOOL指针),并且没有返回值的 Block 对象。

通过 typdef 可以将某个 Block 对象类型定义为一个新类型,以方便使用。需要注意的是,不能在方法的实现代码中使用 typedef 也就是说,应该在实现文件的顶部,或者头文件内使用 typdef

1
typedef void (^ArrayEnumerationBlock)(id, NSUInteger, BOOL *);

捕获值

Block 对象通常会(在其代码中)使用外部创建的其他变量(基本类型的变量,或者是指向其他对象的指针)。这些外部创建的变量叫做外部变量(external variables)。当值行 Block 对象时,为了确保当下的外部变量能够始终存在,相应的 Block 对象会捕获这些变量。

对基本类型的变量,捕获意味着程序会拷贝变量的值,并用 Block 对象内的局部变量保存。对指针类型的变量,Block 对象会使用强引用。这意味着凡是 Block 对象用到的对象,都会被保留。所以在相应的 Block 对象被释放前,这些对象一定不会被释放。

如果需要写一个使用 self 的 Block 对象,就必须要多做几步工作来避免造成强引用循环。考虑一下这个例子,BNREmployee 实例创建了一个 Block 对象,每次执行的时候就会打印出这个 BNREmployee 实例:

1
2
3
myBlock = ^{
NSLog(@"Employee: %@", self);
};

BNREmployee 有一个指向 Block 对象的指针。这个 Block 对象会捕获 self,所以它有一个指向 BNREmployee 实例的指针。现在就陷入强引用循环了。

为了打破这个强引用循环,可以先在 Block 对象外声明一个 __weak 指针;然后将这个指针指向 Block 对象使用的 self;最后在 Block 对象中使用这个新的指针。

1
2
3
4
__weak BNREmployee *weakSelf = self;
myBlock = ^{
NSLog(@"Employee: %@", weakSelf);
};

现在这个 Block 对象对 BNREmployee 实例是弱引用,强引用循环打破了。

然而,由于是弱引用,所以 self 指向的对象在 Block 执行的时候可能会被释放。

为了避免这种情况的发生,可以在 Block 对象创建一个对 self 的局部强引用:

1
2
3
4
5
__weak BNREmployee *weakSelf = self;
myBlock = ^{
BNREmployee *innerSelf = weakSelf;
NSLog(@"Employee: %@", innerSelf);
};

通过创建 innerSelf 强引用,就可以在 Block 和 BNREmployee 实例中再次创建一个强引用循环。但是,由于 innerSelf 引用是针对 Block 内部的,所以只有在 Block 执行的时候他才会执行,而 Block 结束之后就会自动消失。

修改外部变量

在 Block 对象中,被捕获的变量是常数,程序无法修改变量所保存的值。
如果需要在 Block 对象内修改某个外部变量,则可以在声明相应的外部变量时,在前面加上 __block 关键字。

生命周期类型

assign 是默认的也是最简单的:存方法会将传入的值直接赋给实例变量。可以使用 assign 特性来保存非对象类型的实例变量。对非对象类型的实例变量来说,因为是默认使用 assign 特性的,所以不需要在属性声明中显式添加 assign

strong 特性,要求保留传入的对象,并放弃原有对象(如果原有对象不再有其他拥有方,就会被释放)。凡是指向对象的实例变量,通常都应该使用 strong 特性。

weak 特性要求不保留传入的对象。如果该对象被释放,那么相应的实例变量会被自动赋为 nil。这么做可以避免产生悬空指针。悬空指针指向的是不再存在的对象。向悬空指针发送消息通常会导致程序崩溃。相应的存方法会将传入的对象直接赋给实例变量。

unsafe_unretained 特性和 weak 特性类似,要求不保留传入的对象。然而,如果该对象被释放,那么相应的实例变量不会被自动赋为 nil

copy 特性要求拷贝传入的对象,并将新对象赋给实例变量。

copymutableCopy

在OC语法中,提供了Copy语法(Copy/MutableCopy)用于对象的拷贝。其中很容易混淆的是浅拷贝和深拷贝。所谓浅拷贝,即单纯的地址拷贝,并不产生新的对象,而是对原对象的引用计数值加1;而深拷贝,即是对象拷贝,产生新的对象副本,拥有独立的内存空间,引用计数器为1。

参考: iOS中的Copy和mutableCopy

Category

通过使用 Category,程序员可以为任何已有的类添加方法。

打开 NSString+BNRVowelCounting.h, 为 Category 声明一个方法。该方法会加入 NSString 类,代码如下:

1
2
3
4
5
6
#import <Foundation/Foundation.h>

@interface NSString (BNRVowelCounting)
- (int)bnr_vowelCount;

@end

在 NSString+BNRVowelCounting.m 中实现相应的方法,代码如下:

1
2
3
4
5
6
7
8
#import "NSString+BNRVowelCounting.h"

@implementation NSString (BNRVowelCounting)
- (int)bnr_vowelCount {
....
}

@end

注意,如果在类中已经存在名称相同的另一个方法,这个使用了 Category 的方法就会替换掉之前存在的方法。

应该使用 Category 来给已存在的类增加新方法,而不要在 Category 中替换已存在的方法:这种情况下应该创建该类的子类。

=====

类扩展的一般用法:

类扩展即类的 .m 文件中 @implementation 之前开始的部分,所谓的类的 continuous 区域:

1
2
3
@interface class name ()
// ...
@end

类扩展的作用本来是用于私有函数的前向声明,但最新编译器无需声明也有相同的效果,因此私有方法可在.m文件中任意位置直接写实现而无需在此处进行前向声明,如果在此处声明函数那么一定要在后面进行实现,否则编译器会给出警告。现在类扩展区域的作用主要是快速定义类的私有属性,即将暴露给外部的属性变量定义在头文件中,而不想暴露给外部的属性则直接定义在类扩展区域。【注意这里的私有属性和私有方法并不是绝对私有的,OC中没有绝对的私有方法和私有变量,因为即使它们隐藏在.m实现文件里不暴露在头文件中,开发者仍然可以利用runtime运行时机制对其暴力访问,只是一般情况可以达到私有的效果】

Category与扩展(Extension) 的比较

Category 和 Extension 明显的不同在于后者可以添加属性。另外后者添加的方法是必须要实现的。Extension 可以认为是一个私有的匿名的 Category,因为 Extension 定义在 .m 文件头部,添加的属性和方法都没有暴露在头文件,在不考虑运行时特性的前提下这些扩展属性和方法只能类内部使用,一定程度上可以说是私有的。

Objective-C 运行时

内省

内省(introspection) 是 Objective-C 运行时的一个特性:它能够让对象在程序运行时的时候回答关于自身的问题。 例如,这里有一个 NSObject 方法叫做 respondsToSelector::

1
- (BOOL)respondsToSelector:(SEL)aSelector;

其中的一个实参是一个选择器(一个方法的名字)。如果对象实现了该选择器的名字的方法,就会返回 YES;如果没有实现,则返回 NO。使用 respondsToSelector: 则是内省的例子。

动态查找并执行方法

一个运行中的 Objective-C 程序包含了大量的对象,对象直接彼此发送消息。对象发送消息的时候,它会开始搜索要执行的方法,通常会从接收者的 isa 指针指向的类开始进行搜索,然后根据继承层级搜索,直到找到需要的方法。

动态查找并执行方法构成了 Objective-C 消息发送机制的基础,它也是 Objective-C 运行时的另一大特性。

查找并执行方法是 C 函数 objc_msgSend() 的工作。这个函数的实参是接受消息的对象,被执行的方法的选择器,以及这个方法的所有实参。

类以及继承层级的管理

Objective-C 运行时不仅负责记录正在使用哪些类,还负责记录那些包含到程序中的库以及框架使用的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 声明一个变量保存注册类的数量
unsigned int classCount = 0;

// 创建一个指针指向应用当前加载的所有类的列表
// 通过引用返回注册类的数量
Class *classList = objc_copyClassList(&classCount);

for (int i = 0; i < classCount; i++) {
Class currentClass = classList[i];
NSString *className = NSStringFromClass(currentClass);
NSLog(@"%@", className);
}
free(classList);
}
return 0;
}

objc_copyClassList() 函数会返回一个由指向类对象的指针组成的 C 数组。按照惯例,调用名字中包含 “copy” 或 “create” 的函数时所采用的内存,例如 objc_copyClassList 函数,如果不再需要,就必须释放。这种情况我们称之为创建规则(create rule)。与之类似,如果调用名字中包含 “get” 的函数时所采用的内存不归你所有,就不必释放。这种情况我们称之为获取规则(get rule)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NSArray *BNRHierarchyForClass(Class cls) {
NSMutableArray *classHierarchy = [NSMutableArray array];

for (Class c = cls; c != Nil; c = class_getSuperclass(c)) {
NSString *className = NSStringFromClass(c);
[classHierarchy insertObject:className atIndex:0];
}
return classHierarchy;
}

NSArray *BNRMethodsForClass(Class cls) {
unsigned int methodCount = 0;
Method *methodList = class_copyMethodList(cls, &methodCount);
NSMutableArray *methodArray = [NSMutableArray array];

for (int m = 0; m < methodCount; m++) {
Method currentMethod = methodList[m];
SEL methodSelector = method_getName(currentMethod);
[methodArray addObject:NSStringFromSelector(methodSelector)];
}
return methodArray;
}

这里有一个之前没有见过的类型:方法。在这段代码中,方法是一类结构的名字,这类结构的成员包括方法的选择器(SEL 类型的变量)以及一个函数指针(function pointer)—————— 指向执行程序中内存数据段的一大块代码。这个函数指针是 IMP 类型的变量。

修改后:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#import <Foundation/Foundation.h>
#import <objc/runtime.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 创建一个字典数组,每个字典都会保存类的名称,层级以及给定类的方法列表
NSMutableArray *runtimeClassesInfo = [NSMutableArray array];

unsigned int classCount = 0;

Class *classList = objc_copyClassList(&classCount);

for (int i = 0; i < classCount; i++) {
Class currentClass = classList[i];
NSString *className = NSStringFromClass(currentClass);
// NSLog(@"%@", className);
NSArray *hierarchy = BNRHierarchyForClass(currentClass);
NSArray *methods = BNRMethodsForClass(currentClass);

NSDictionary *classInfoDict = @{ @"className" : className,
@"hierarchy" : hierarchy,
@"methods" : methods };
[runtimeClassesInfo addObject:classInfoDict];
}
free(classList);

NSSortDescriptor *alphaAsc = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
NSArray *sortedArray = [runtimeClassesInfo sortedArrayUsingDescriptors:@[alphaAsc]];
NSLog(@"There are %ld classes registered with this program's Runtime.", sortedArray.count);
NSLog(@"%@", sortedArray);
}
return 0;
}

KVO的工作原理

运行时,如果向某个对象发送 addObserver:forKeyPath:options:context: 消息,那么这个方法可以:

  • 决定被观察对象的类,并使用 objc_allocateClassPair() 函数给这个类定义一个新的子类。
  • 改变对象的 isa 指针,让它指向新的子类(高效改变对象的类型)。
  • 覆盖被观察对象的存取器,发生 KVO 消息。

例如,一个类的 location 属性的存方法代码如下:

1
2
3
4
- (void)setLocation:(NSPoint)location 
{
_location = location;
}

在新的子类中,存取器会被覆盖如下:

1
2
3
4
5
6
- (void)setLocation:(NSPoint)location
{
[self willChangeValueForKey:@"location"];
[super setLocation:location];
[self didChangeValueForKey:@"location"];
}

子类的存取器实现会调用原始类的实现,然后将它们用简明的 KVO 通知消息封装起来。这些新的类以及方法都会在运行时使用 Objective-C 运行时函数定义。