0%

序言

看《Effective Objective-C 2.0》这本书发现“尾调用”这个词汇,之前没接触过,记录下来。

什么是尾调用

“尾调用”是指一个函数的最后一项操作是调用另一个函数,即被调用函数的返回值就是当前函数的返回值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (int)func0:(int)i {
// do anything
return [self func1:i];
}

- (int)func0:(int)i {
// do anything
if (i == 0) {
return [self func2:i];
} else {
return [self func1:i];
}
}

下面的例子不属于尾调用

1
2
3
4
5
6
7
8
9
10
- (int)func0:(int)i {
// do anything
return [self func1:i] + 1;
}

- (int)func0:(int)i {
// do anything
int value = [self func1:i]
return value;
}

尾递归

如果函数在尾部调用的是自身,那么就叫做“尾递归”。

1
2
3
4
- (int)func0:(int)i {
// do anything
return [self func0:i];
}

尾调用优化

当一个函数是尾调用时,那么当前函数开辟的栈空间就已经不需要再使用了。被调用函数不需要开辟新的栈空间,而是直接使用当前函数的栈空间(更新原有栈),再把被调用函数的返回地址替换成当前函数的返回地址,这就是“尾调用优化”。使用“尾调用优化”技术,可以避免栈溢出。

尾递归优化(例子)

求n!

没有使用尾调用时代码是这样写的:

1
2
3
4
5
- (int)factorial0:(int)n {
if (n < 1) return 0;
if (n == 1) return 1;
return n * [self factorial0:n - 1];
}

程序第一次进入 factorial0 函数时需要在栈中分配内存用来保存 n 值。然后在每一次递归调用 factorial0 时都需要再分配新的内存来保存新的变量 n(这里的每一个 n 值都是不一样的,内存也是不一样的),空间复杂度O(n)。这样栈就会一直叠加,最后可能造成栈溢出。

使用了尾调用时代码是这样写的:

1
2
3
4
5
6
7
8
9
- (int)factorial1:(int)n {
if (n < 1) return 0;
return [self factorial1:n total:1];
}

- (int)factorial1:(int)n total:(int)total {
if (n == 1) return total;
return [self factorial1:n - 1 total:total * n];
}

factorial1 只执行一次,不影响,空间复杂度O(1)。
程序第一次进入 factorial1:count 函数时需要在栈中分配内存用来保存 n 值。当第二次调用 factorial1:count 时,由于是尾调用,此时第一次分配的栈空间已经不需要再用了,所以第二次调用的时候直接使用原有栈,不需要分配额外的内存。空间复杂度O(1)。

第二章:对象、消息、运行期(2)

第9条:以 “类族模式” 模式隐藏实现细节

类族模式可以把实现细节隐藏在一套简单的公共接口后面。Objective-C 的系统框架普遍使用此模式。例如:UIButton NSArray NSNumber 等等。

自定义 “类族模式”

定义一个 Person 基类以及三个子类 PersonA, PersonB, PersonC 。三个子类分别实现自己的 doWork 任务。

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
// Person
@interface Person : NSObject
+ (instancetype)personWithType:(PersonType)personType;
- (void)doWork;
@end

@implementation Person
+ (instancetype)personWithType:(PersonType)personType {
switch (personType) {
case PersonTypeA:
return [PersonA new];
break;
case PersonTypeB:
return [PersonB new];
break;
case PersonTypeC:
return [PersonC new];
break;
}
}

- (void)doWork {
//SubClasses implement this
}
@end

//
// Subclass PersonA
@interface PersonA : Person

@end

@implementation PersonA

- (void)doWork {
NSLog(@"do PersonA Work");
}

//
// Subclass PersonB
@interface PersonB : Person

@end

@implementation PersonB

- (void)doWork {
NSLog(@"do PersonB Work");
}

//
// Subclass PersonC
@interface PersonC : Person

@end

@implementation PersonC

- (void)doWork {
NSLog(@"do PersonC Work");
}

@end

接口调用如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Person *personA = [Person personWithType:PersonTypeA];
Person *personB = [Person personWithType:PersonTypeB];
Person *personC = [Person personWithType:PersonTypeC];

NSLog(@"%@",[personA class]);
NSLog(@"%@",[personB class]);
NSLog(@"%@",[personC class]);

[personA doWork];
[personB doWork];
[personC doWork];

// 输出
// PersonA
// PersonB
// PersonC
// do PersonA Work
// do PersonB Work
// do PersonC Work

这样就只需要传入不同的 Type 就可以实现不同的任务。这种实现模式就叫做“类族模式”。

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

可以通过“关联对象”这项特性,给某个类关联多个对象,这些对象可以通过 key 区分。在关联对象的时候需要指明对象的“存储策略”,用来维护相应的“内存管理语义”。“存储策略”由 objc_AssociationPolicy 这个枚举维护。下面给出 objc_AssociationPolicy 枚举的取值以及等效的 @property 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Policies related to associative references.
* These are options to objc_setAssociatedObject()
*/
typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
OBJC_ASSOCIATION_ASSIGN = 0, /**< Specifies a weak reference to the associated object. */
OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
* The association is not made atomically. */
OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /**< Specifies that the associated object is copied.
* The association is not made atomically. */
OBJC_ASSOCIATION_RETAIN = 01401, /**< Specifies a strong reference to the associated object.
* The association is made atomically. */
OBJC_ASSOCIATION_COPY = 01403 /**< Specifies that the associated object is copied.
* The association is made atomically. */
};


对应的3个方法为:

1
2
3
4
5
6
// 设置关联对象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy);
// 获取关联对象
id objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
// 移除关联对象
void objc_removeAssociatedObjects(id _Nonnull object)

系统没有给出移除单个关联对象的接口,如果要移除某个关联对象,可以通过给该关联对象的 key 设置一个空值来实现。
void objc_setAssociatedObject(object, key, nil, policy);

示例

当我们需要使用 UIAlertView 时,一般会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)showAlert {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"title" message:@"message" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"Confirm", nil];
[alertView show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
if (buttonIndex == 0) {

} else if (buttonIndex == 1) {

}
}

当存在多个 UIAlertView 时,委托方法里面就需要对 alertView 进行判断。使用关联对象可以简化这里的逻辑

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

static const void *kAlertKey = @"kAlertKey";
- (void)showAlert {
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"title" message:@"message" delegate:self cancelButtonTitle:@"Cancel" otherButtonTitles:@"confirm", nil];

void (^block)(NSInteger) = ^(NSInteger buttonIndex) {
if (buttonIndex == 0) {

} else if (buttonIndex == 1) {

}
};
objc_setAssociatedObject(alertView, kAlertKey, block, OBJC_ASSOCIATION_COPY);
[alertView show];
}

- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex {
void (^block)(NSInteger) = objc_getAssociatedObject(alertView, kAlertKey);
block(buttonIndex);
}

注意

当关联对象需要捕获了其他变量,可能会造成引用循环。使用关联对象会降低代码的可读性,增加调试的难度。应谨慎使用。

第11条:理解 objc_msgSend 的作用

给对象发消息

1
id returnValue = [someObject msgName:parameter];

编译器会转换为

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

objc_msgSend 会在接受者类中搜寻“方法列表”,如果找到对应的方法,则转跳实现代码。如果没找到就沿着继承类向上找。如果最终还是找不到该方法,则进行“消息转发”。同时 objc_msgSend 还会将找到的方法缓存在“快速映射表”,如果下次还需要执行该方法,就会先从“快速映射表”中查找,这样执行起来会快很多。
每个类都会有一张类似于字典一样的表格,方法名是 Key ,对应的 Value 则保存着函数指针。objc_msgSend 就是通过这个表格来寻找应该执行的方法并跳转其实现的。这些工作由“动态消息派发系统”来处理。

尾调用优化

“尾调用”是指一个函数最后一项操作是调用另一个函数,即被调用的函数的返回值就是当前函数的返回值。如果函数在尾部调用的是自身,那么就叫做“尾递归”。
尾调用优化是指不需要在当前调用栈上开辟新的栈空间,而是更新原有栈(原有栈的数据已经不需要了),再把调用函数的返回地址替换成当前函数的返回地址。
使用“尾调用优化”技术,很大程度上可以避免了栈溢出。

第二章:对象、消息、运行期(1)

“对象”是基本构造单元,可以通过对象来存储数据和传递数据。对象之间传递数据并执行任务的过程就叫做“消息传递”。

第6条:理解 “属性” 这一概念

“属性” 是 Objective-C 的一项特性,用来封装对象中的数据。属性最终是通过实例变量来实现的,属性只是提供了一种简洁的抽象机制。

对象布局

对象布局在编译期就已经确定了,当代码需要访问实例变量的时候,编译器会把其替换成偏移量,这个偏移量是“硬编码”,表示该变量距离对象内存起始地址有多远。
当类增加了实例变量时,原来的偏移量就已经不再适用,所以这时候需要重新编译。偏移量保存在类对象中,会在运行时查找。

应用程序二进制接口(Application Binary Interface,ABI)

应用程序二进制接口描述了应用程序和操作系统之间,一个应用和它的库之间,或者应用的组成部分之间的低层接口。ABI不同于应用程序接口(API),API定义了源代码和库之间的接口,因此同样的代码可以在支持这个API的任何系统中编译,然而ABI允许编译好的目标代码在使用兼容ABI的系统中无需改动就能运行。(百度百科)

ABI定义了许多内容(标准),其中一项就是生成代码时所应遵循的规范,有了这种规范,我们就可以在分类和实现文件定义实例变量,可以将实例变量从接口文件中移开,以便保护和类实现相关的内部信息。

@synthesize & @dynamic

1
2
3
4
@implementation
@synthesize firstName = _myFirstName;
@dynamic firstName;
@end

@synthesize 用来指定实例变量的名称。
@dynamic 告诉编译器不要自动生成实例变量,也不要生成 setter 和 getter 方法。这时编译器不会报错,而是在运行时查找。

属性特质

原子性,读写权限,内存管理(assign、strong、weak、unsafe_unretained、copy),方法名

原子性

iOS 开发的时候应该尽量使用 nonatomic,使用 atomic 会严重影响性能。

读写权限

readwrite 同时生成setter 和 getter 方法。
readonly 只生成 getter 方法。

copy

当属性类型为 NSString 时,一定要用 copy 修饰,防止当传递过来的值是 NSMutableString 类型,从而可能会在不知情的情况下更改属性的值。

第7条:在对象内部尽量直接访问实例变量(感觉有歧义)

在对象外面,应该通过属性访问实例变量。在对象内部,除了几种特殊的情况下,读取实例变量应该采用直接访问的形式,设置实例变量则采用属性来设置。

对象内部不要直接设置实例(有歧义)

这样做不会调用 setter 方法,也就绕过了相关属性定义的“内存管理语义”,比如使用了 copy 特质,直接访问不会拷贝该属性,只会保留新值并释放旧值。此外当设置了KVO时,直接设置实例也不会触发KVO。

初始化时应该直接访问实例

如果父类初始化使用 setter 方法设置属性,而子类又重写了这个 setter 方法,那么子类初始化时,父类也会初始化,这时父类将会调用子类的 setter 方法。
例外:如果待初始化的实例变量申明在父类中,而子类无法直接访问此实例变量,这时就需要调用 setter 方法了。

dealloc 方法中也应该直接读写实例变量

懒加载

如果某个属性使用了懒加载,那就必须使用 getter 方法了。

第8条:理解 “对象同等性” 这一概念

“对象同等性” 可以理解为某种意义上两个对象相等,这个“相等”是我们自定义的。官方给我们定义了一些判断两个对象是否“相等”的方法

1
2
3
4
5
6
7
8
// NSString
- (BOOL)isEqualToString:(NSString *)aString;

// NSData
- (BOOL)isEqualToData:(NSData *)other;

// NSDictionary
- (BOOL)isEqualToDictionary:(NSDictionary<KeyType, ObjectType> *)otherDictionary;

对象完全相等

用 “==” 判断两个对象是否是同一个对象,这里判断的是指针。

自定义 “相等”

通过 NSObject 协议中的两个方法自定义 “相等”。

1
2
- (BOOL)isEqual:(id)object;  
@property (readonly) NSUInteger hash;

自定义一个 Person 类,包含一个 email 属性。

1
2
3
@interface Person()
@property (nonatomic, copy) NSString *email;
@end

假定对象的 email 属性值相同,就认为这两个类“相同”,那么自定义方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (BOOL)isEqualToPerson:(Person *)otherPerson {
if (nil == otherPerson) return NO;
if (self == otherPerson) return YES;

if ([_email isEqualToString:otherPerson.email]) return YES;

return NO;
}

- (BOOL)isEqual:(id)object {
if ([self class] != [object class]) return NO;

[self isEqualToPerson:object];

return NO;
}

// 如果两个对象相等,则其哈希码一定相同。反之,如果哈希码相同,这两个对象不一定相同。
// 考虑到性能问题,hash 方法要保证高效率
- (NSUInteger)hash {
// 此处逻辑可以自定义
return [_email hash];
}

典型应用

1
2
3
4
// NSArray
- (BOOL)containsObject:(ObjectType)anObject;
// NSSet
- (BOOL)containsObject:(ObjectType)anObject;

使用 NSArray 调用 containsObject 这个方法,会直接调用 isEqual 方法判断两个对象是否相等。测试发现这里并没有调用 hash 方法,原因不明,例子如下:

1
2
3
4
5
6
7
8
9
10
NSMutableArray *array = [NSMutableArray array];
Person *aPerson = nil;
for (int i = 0; i < 5; i++) {
Person *p = [[Person alloc] initWithEmail:[NSString stringWithFormat:@"%zd",i]];
[array addObject:p];
aPerson = p;
}
if ([array containsObject:aPerson]) {
NSLog(@"array has 'aPerson'");
}

再使用 NSSet 看看是怎么执行的。

1
2
3
4
5
6
7
8
9
10
NSMutableSet *sets = [NSMutableSet set];
Person *aPerson = nil;
for (int i = 0; i < 5; i++) {
Person *p = [[Person alloc] initWithEmail:[NSString stringWithFormat:@"%zd",i]];
[sets addObject:p];
aPerson = p;
}
if ([sets containsObject:aPerson]) {
NSLog(@"array has 'aPerson'");
}

NSSet 在 addObject 和 containsObject 方法中都会调用 hash 方法。再 addObject 方法中会调用 isEqual 方法,而 containsObject 方法中则不再调用。NSArray 则是在 containsObject 方法中调用 isEqual 方法。

不同的集合会使用不同的逻辑判断是否“相等”。

注意

在 NSSet 中, hash 方法是判断的第一步,应该保证此方法的高效性,同时也要考虑 哈希碰撞 发生的概率。

第一章:熟悉 Objective-C 语言

第1条:了解 Objective-C 语言的起源

消息结构

Objective-C 使用的是“消息结构”(messaging structure)而非“函数调用”(function calling)。
使用消息结构的语言,其运行时所执行的代码由运行环境决定。而使用函数调用的语言,则由编译器决定。

在C/C++中,如果使用的函数是多态,那么运行时会根据“虚方法表”(virtual table)来查找应该执行哪个函数实现。而采用消息结构的语言则都是在运行的时候才查找要执行的方法。

运行期组件(runtime component)

Objective-C 中重要工作都由运行期组件完成,而非编译器。里面包含了面向对象所需的全部数据结构及函数。其本质是与开发者所编写的代码相链接的动态库。

对象内存分配

对象所占有的内存总是分配到堆空间(Head)中,而指向对象的指针则是分配到栈(stack)中。分配到堆中的内存必须进行管理,分配到栈上用于保存对象地址的内存,则会在栈帧弹出时自动处理。
当遇到非指针类型变量的时候,变量可能会分配到栈空间,比如:结构体。

第2条:在类的头文件中尽量少引用其他头文件

向前声明(forward declaring)

如果只需要知道有那么一个类名,则不需要引用该类名的头文件(不需要知道其他细节),这时可以向前声明该类,既使用:

@class className;

然后在实现文件中引入该头文件。这样可以降低类与类之间的耦合。
引入头文件的时机应该尽量延后,只有当确定要引用该头文件的时候才引用。将大量的头文件引入到头文件中,会增加文件之间的依赖性,从而增加编译时间。

循环引用

向前申明可以解决两个类之间的循环引用。文章说道:

使用 #import 虽然不会导致引用循环,但却意味着两个类有一个不能被正确编译。

不过,这句话我。。。。无法理解!!!

头文件需要引用协议

如果要使用某个协议,则不能使用向前声明,为了不引用整个头文件,可以将协议放到“class-continuation 分类”中,或者单独放到一个文件中,然后使用 #import 引用头文件,这样就不会出现上面说的问题。

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

使用字面量语法可以缩减代码长度,提高代码可读性。也要确保创建对象的时候不能为nil。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
NSString *string0 = [[NSString alloc] initWithString:@"123"];
NSString *string1 = @"123";

NSNumber *number0 = [NSNumber numberWithInt:1];
NSNumber *number1 = @1;

NSArray *array0 = [NSArray arrayWithObjects:@"cat", @"dog", @"fish", nil];
NSString *cat0 = [array0 objectAtIndex:0];
NSArray *array1 = @[@"cat", @"dog", @"fish"];
NSString *cat1 = array1[0];

NSDictionary *dictionary0 = [NSDictionary dictionaryWithObjectsAndKeys:@"key0", @"value0", @"key1", @"value1", nil];
NSString *value0 = [dictionary0 objectForKey:@"key0"];

NSDictionary *dictionary1 = @{@"key0":@"value0", @"key1":@"value1"};
NSString *value1 = dictionary1[@"key1"];

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

使用 #define 无法确定类型信息

比如下面的代码用 #define 无法预知 kAnimationDuration 的数据类型,不利于编写开发文档。

1
2
3
#define kAnimationDuration 0.1
static const NSTimeInterval kAnimationDuration = 0.1;
static const float kAnimationDuration = 0.1;

static const 修饰

如果一个变量用 static const 修饰,那么编译器不会创建符号,而是会像 #define 预处理指令一样,在编译的时候将所有的变量替换成常值。

extern 声明全局变量

使用 static const 修饰的变量只能在本文件内使用,但有时候需要对外公布这个变量,比如该变量作为“通知”的key的时候,此时可以稍微改一下。

1
2
3
4
// .h文件 声明一个变量
extern NSString *const VCHLoginNotification;
// .m文件 定义一个变量
NSString *const VCHLoginNotification = @"kLoginNotification";

这种变量会保存在“全局符号表”中。为了避免命名冲突,这种变量应该加上类名前缀。

判断 const 修饰的是对象还是指针(自己理解)

const 修饰的是右边的第一个字符

1
2
3
4
5
6
7
float const valueFloat0 = 0.1; //[1]
const float valueFloat1 = 0.1; //[2]
NSString const * string0 = @"abc"; //[3]
NSString * const string1 = @"abc"; //[4]
const NSString * string2 = @"abc"; //[5]
const NSString * const string3 = @"abc"; //[6]
const NSString const * string4 = @"abc"; //[7]

[1] const 右边第一个字符是 valueFloat0,表示 valueFloat0 里面的值是不变的。valueFloat0 不能是左值。
[2] const 右边第一个字符是 float,而 float 指的就是 valueFloat1,所以 valueFloat1 的值是不变的。valueFloat1 不能是左值。
[3] const 右边第一个字符是 string0,string0 是一个指针,所以 string0 指向的地址是不变的。string0 不能是左值。
[4] const 右边第一个字符是 string1(指针),所以 string1 指向的地址是不变的。string1 不能是左值。
[5] const 右边第一个字符是 NSString,表示的是 @”abc” 这个对象,所以 @”abc 是不可变对象。不可以通过 string2 这个指针来修改它指向的对象的内容。(这里刚好 @”abc” 是不能修改的,就算指向的对象是可以被修改的,也不能通过 const 修饰的指针去修改)
[6] 第一个 const 右边第一个字符是 NSString, 等同于 [5]。第二个 const 等同于 [4]。
[7] 等同于 [6]

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

枚举可以提高代码可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 状态、状态码
typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
UIViewAnimationTransitionNone,
UIViewAnimationTransitionFlipFromLeft,
UIViewAnimationTransitionFlipFromRight,
UIViewAnimationTransitionCurlUp,
UIViewAnimationTransitionCurlDown,
};

// 可组合选项
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
};

enum 用来表示状态,options 用来表示可组合的选项。

注意

1、用枚举处理 switch 的时候不要实现 default 分支。这样加入新的分支后,编译器就会提示开发者。

序言

从开始学iOS就看到这样的一个例子,也是面试题,但我一直都觉得这题目没什么意义,最初设计这题目的人目的是什么?想让人了解什么是编译时什么是运行时?不解!不管了,以后应该会了解的。

题目

string在编译时和运行时分别时什么类型的对象?
NSString *string = [[NSDate alloc] init]; // 原题是 NSData,我改为 NSDate,方便我后面发消息

直接这样写编译器会报警告!

Incompatible pointer types initializing ‘NSString *‘ with an expression of type ‘NSDate *‘

可以使用类型转换消除这个警告

NSString *string = (NSString *)[[NSDate alloc] init];

此时编译器已经没有任何警告了,然后我们给 “string” 这个对象发消息看看会出现什么情况

直接调用方法

1
2
3
4
5
6
NSString *string = (NSString *)[[NSDate alloc] init];
//调用 NSString 的方法,编译通过
[string stringByAppendingString:@"abc"];
//调用 NSDate 的方法,编译不通过
//提示:No visible @interface for 'NSString' declares the selector 'isEqualToDate:'
[string isEqualToDate:[NSDate date]];

从上面提示的注释信息可以看出,编译器认为 “string” 就是 NSString 类型的对象。调用 NSDate 的方法编译不通过。
然后,运行Demo,结果程序直接崩溃,提示:

-[_NSZeroData stringByAppendingString:]: unrecognized selector sent to instance 0x1010d12e0

显然程序是找不到 “stringByAppendingString” 方法才崩溃的。

通过 performSelector 调用方法

1
2
3
4
5
NSString *string = (NSString *)[[NSDate alloc] init];
//编译通过,运行崩溃
[string performSelector:@selector(stringByAppendingString:) withObject:@"abc" afterDelay:0];
//编译通过,运行通过
[string performSelector:@selector(isEqualToDate:) withObject:[NSDate date] afterDelay:0];

通过 performSelector 给 “string” 对象发 “stringByAppendingString” 消息,依然是崩溃。发 “isEqualToDate” 消息,成功。

结论

在编译的时候 “string” 是 NSString 类型的对象,所以给 “string” 对象发送 NSString 特有的消息可以通过编译。
而在运行的时候 “string” 是 NSDate 类型的对象,这就是为什么给 “string” 对象发送 “stringByAppendingString” 消息会崩溃,而 发送 “isEqualToDate” 消息则成功的原因。
从下图也可以看出运行的时候的类型为 NSDate

编译时 & 运行时

编译时类型由声明该变量时使用的类型决定,运行时类型由实际赋给该变量的对象决定。
编译时会对语言进行语法检测,并编译成机器语言,由于OC的动态性,代码能否执行成功还得到运行时才能检测出来。
运行时会把编译好的代码装到内存中运行,这时候会对对象的类型、方法等进行检测,如果检测失败可能会直接崩溃。

load

load 会在类装载的时候调用(main函数执行之前)。load 不会被覆盖,如果父类、父类分类、子类、子类分类,同时实现了 load 方法,那么这些方法都会执行。

调用顺序

1、先调用父类的 load 方法,再调用子类的 load 方法。
2、先调用本类的 load 方法,再调用分类的 load 方法。

使用场景

1、Method Swizzle 。

initialize

initialize 会在第一次给该类发送消息之前调用,有些文章说是在实例化、初始化之前调用是错的。看看文档是怎么说的

Initializes the class before it receives its first message.

调用顺序

父类优先于子类调用 initialize 方法。
如果子类以及子类的分类没有实现 initialize 方法,那么当第一次给子类发消息的时候,会先给父类的 initialize 发消息。也就是父类的 initialize 方法可能会被调用多次。
如果本类以及其分类都实现 initialize 方法,那么只会调用 分类的 initialize 方法。

线程安全

initialize 是线程安全的,自带锁,当第一个线程给这个类发了 initialize 消息,其他想给这个类发 initialize 消息的线程会被阻塞,直到第一个线程发送完 initialize 消息。

问题

1、如果本类以及本类的多个分类都实现了 initialize 方法,怎么调用?
答:会调用 Compile Source 中的本类最后一个分类的 initialize 方法,如下图:

我在代码中实现三个 initialize 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Person
+ (void)initialize {
NSLog(@"Person initialize.......");
}

// Person (Special)
+ (void)initialize {
NSLog(@"Person (Special) initialize.......");
}

// Person (Special1)
+ (void)initialize {
NSLog(@"Person (Special1) initialize.......");
}

结果打印出”Person (Special1) initialize…….“。
当我在 Compile Source 中调换了本类两个分类的先后顺序后,则打印出”Person (Special) initialize…….“。
从而得出结论上面的结论,Compile Source 中本类最后一个分类的 initialize 方法将会被调用。

2、当子类及其分类没有实现 initialize 方法,在父类的 initialize 方法中怎么区分这个消息是通过本类发来的还是子类发来的?
这里我定义了两个类,Person 为父类,Student 为子类。Student 类中没有实现 initialize 方法。那么给 Student 发消息后,结果如下:

1
2
3
4
5
6
7
+ (void)initialize {
if (self == [Person self]) {
NSLog(@"本类发来的 initialize 消息。%@ - %@",self,[Person self]);
} else {
NSLog(@"子类发来的 initialize 消息。%@ - %@",self,[Person self]);
}
}

打印结果:
本类发来的 initialize 消息。Person - Person
子类发来的 initialize 消息。Student - Person

既:如果是本类发送的 initialize 消息,那么 self == Person。如果是子类发送过来的消息,那么 self == Student。

使用场景

1、初始化静态变量。

注意

1、由于 initialize 使用的是阻塞的调用方式,当一个类 initialize 依赖 另一个类的 initialize 的时候,容易造成死锁,所以应该尽量避免在 initialize 中完成一些复杂的初始化工作。
2、应该避免问题1这样的情况出现。如果需要在本类以及分类中同时实现初始化,可以考虑用 load 方法。

要求

给定已排序数组,找出和为特定数值的两个数。假定数组中的数值都不相同。

方法一:穷举法

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)fineTwoNumberWithArray0:(int *)nums count:(int)count target:(int)target {
int leftNum,rightNum;

for (int i = 0; i < count - 1; i++) {
leftNum = nums[i];
for (int j = i + 1; j < count; j++) {
rightNum = nums[j];
if ((rightNum + leftNum) == target) {
NSLog(@"%d + %d = %d",leftNum, rightNum, target);
}
}
}
}

T(n) = (n - 1 + 1) * n / 2 = (n^2) / 2 = O(n^2),即时间复杂度:O(n^2)。

方法二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)fineTwoNumberWithArray1:(int *)nums count:(int)count target:(int)target {
int left = 0;
int right = count - 1;
int sum = 0;
while(left <= right) {
sum = nums[left] + nums[right];
if (sum < target) {
left += 1;
} else if (sum > target) {
right -= 1;
} else {
NSLog(@"%d + %d = %d",nums[left], nums[right], target);
left += 1;
right -= 1;
}
}
}

T(n) <= n - 1 = O(n) 时间复杂度:O(n)。

时间复杂度

一个算法需要执行的次数我们记为T(n),其中n为算法的规模。现在引入某个辅助函数f(n),当n趋近于无穷大时,T(n)/f(n) = C (C ≠ 0)。则f(n)和T(n)是同量级函数,记为T(n) = O(f(n)),我们称这个为时间复杂度。

##每种时间复杂度表示的意思

T(n) = O(1)

1
2
3
- (void)aFunction0:(int)n {
NSLog(@"%zd",n); // 执行 1 次
}

不管输入的n是多少,执行次数都是常数。执行次数和输入n值没有任何关系。
T(n) = 1 = O(1)

T(n) = O(n)

1
2
3
4
5
- (void)aFunction1:(int)n {
for (int i = 0; i < n; i++) { // n + 1 次
NSLog(@"%zd",i); // n 次
}
}

执行次数和输入的n值成线性关系。
T(n) = n + 1 + n = 2n + 1 = O(n)

T(n) = O(n^2)

1
2
3
4
5
6
7
- (void)aFunction2:(int)n {
for (int i = 0; i < n; i++) { // n + 1
for (int j = 0; j < n; j++) { // n + 1
NSLog(@"%zd",i); // n
}
}
}

执行次数和输入的n值成线性关系。
T(n) = (n + 1) * (n + 1 + n) = 2n^2 + 3n + 1 = O(n^2)

T(n) = O(log(n))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (int)aFunction3:(int *)nums count:(int)count target:(int)target {
int left = 0;
int right = count - 1;
int mid = 0;
while(left <= right) {
mid = (left + right) >> 1;
if (nums[mid] < target) {
left = mid + 1;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
return mid;
}
}
return -1;
}

第1次查找,找到的概率为 1/n
第2次查找,找到的概率为 2/n
第3次查找,找到的概率为 4/n
第m次查找,找到的概率为 2^(m - 1)/n
假设最多需要查找m次,那么存在:1/n + 2/n + 4/n + … + 2^(m - 1)/n = 1,可以推导出 m <= lg(n + 1)
即 T(n) = O(log(n))

T(n) = O(nlog(n))

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
34
35
// 快速排序
- (void)quickSort:(int *)nums count:(int)count {
int start = 0;
int end = count - 1;
int value = nums[start];

if (count < 1) return;

while (start < end) {
//从数组右边往左查找一个小于value的元素
while (start < end) {
if (nums[end] < value) {
nums[start] = nums[end];
start++;
break;
} else {
end--;
}
}
//从数组左边往右查找一个大于value的元素
while (start < end) {
if (nums[start] > value) {
nums[end] = nums[start];
end--;
break;
} else {
start++;
}
}
}
nums[start] = value;

[self quickSort:nums count:start];
[self quickSort:nums + start + 1 count:count - start - 1];
}

第1次递归:T[n] = 2T[n/2] + n
第2次递归:T[n] = 2{ 2T[n/4] + (n/2) } + n = 2^2 T[n/(2^2)] + 2n
第m次递归:T[n] = 2^m T[n/(2^m)] + mn
假设最多需要m次递归完,那么:T[n/(2^m)] = T(1) ==> m = log2(n)
得到:T[n] = 2^m T[1] + mn = 2^(log2(n))T[1] + (log2(n))n = nT[1] + (log2(n))n
当n趋近于无穷大的时候 T[n] = nT[1] + (log2(n))n = (log2(n))n = O(nlogn)
即:T(n) = O(nlog(n))