Objective-C与Foundation
对象(Object)
对象和结构的不同之处在于:
1.对象还可以包含一组函数,并且这些函数可以使用对象所保存的数据。这类函数称为方法。
2.结构中的数据称为成员,对象中的数据称为实例变量(instance variables)。
类(Classes)
一个类定义了一种对象,同时它也可以创建这种对象。类既是对象的原型,又是生产对象的工厂。
关于对象图(object diagrams)的注意事项:
类一般都使用虚线来画。实例一般使用实线画。
创建对象并使用对象(Creating and using your first object)
#import和#include的区别:
-
import指令导入更有效率。导入之前会让编辑器检查之前是否已经导入过这个文件,或是已经被包含到目标文件中了。
-
include指令只能告诉编译器做简单的复制。
消息的接收方是一个指向接收消息的对象的地址。 方法执行后,类会在堆上给实例声明一部分内存,然后返回新对象的地址。
用最少的代码获取一个类的实例,这种方法称为便利方法(convenience method)。
NSArray
使用快速枚举遍历NSMutableArray时,不能在枚举过程中增加或删除数组中的指针。如果遍历时需要添加或删除指针,则需要使用标准的for循环。
用arrayWithObjects: 方法创建数组时,末尾应添加nil,否则会崩溃
方括号的三种使用情形:
- 发送消息
- 创建NSArray对象:NSArray *dateList = @[now, yesterday, tomorrow];
- 访问某个数组特定索引上的对象:NSDate *firstDate = dateList[0];
有时在代码中,这些不同的使用方法会叠加起来。发生这种情况的时候,使用旧式的数组方法会让代码更容易阅读。
id selectedDog = dogs[[tableView selectedRow]];
旧式:
id selectedDog = [dogs objectAtIndex:[tableView selectedRow]];
类前缀
OC没有命名空间,因此,为了避免名字冲突,苹果公司推荐使用三个或三个以上的字母作为类的前缀,让类的名字独一无二。
继承
继承层次
NSObject虽然有很多方法,但是只有一个实例变量:isa指针。任何一个对象的isa指针都会指向创建该对象的类。(isa == is a “是一个”)
description方法和%@转换说明
格式说明符%@让对象描述自己。处理%@时,程序会先向相应的指针变量所指的对象发送description消息。
description返回该对象的内存地址。
对象实例变量及属性
对象实例变量是指向另一个对象的指针。
一对一关系
指针,指向单个复杂的对象。建议每次都声明属性,这样就不需要自己创建存取方法。一对多关系
指针,指向某个collection类的实例。需要自己创建实例变量,存取方法及从关系中增加或移除对象的方法。
任何一个对象不会在其内部保存其他对象,只会保存相应对象的地址。
**** 指向其他对象的特性会产生两大副作用:
- 单个对象可能会扮演多个角色。
-
导致产生大量独立的对象,耗尽程序的可用内存。
对象所有权与ARC
Objective-C提出了对象所有权(object ownership)概念。任何一个对象都知道自己当前有多少个拥有方。
- 当collection对象加入某个对象时,会保存指向该对象的指针,并成为该对象的拥有方。
- 当collection对象移除某个对象时,会删除指向该对象的指针,并不再是该对象的拥有方。
使用@class编译器不会查看实现细节,所以处理速度更快。
内存泄漏
如果程序有用不到的但又没有释放的对象,就称程序有内存泄漏(memory leak)。通常情况下,内存泄漏会导致程序中不使用的对象越来越多,从而占用越来越多的内存。
类扩展(class extensions)
头文件是类声明它的属性及方法的地方这样其他的对象才能够知道如何和类进行交互。
然而,不是所有的属性或方法都需要在类的头文件中声明。有的属性或方法只是该类或其类实例才需要使用的。设计实现细节的属性或方法最好在类扩展中声明。类扩展是一组私有的声明。只有类和其实例才能使用在类扩展中声明的属性,实例变量,方法。
将声明移到类扩展中有两个相关的影响
- 非本类对象不能再看到这个属性。(无法获取到)
- 使类的头文件声明减少。头文件其实是一个告示牌,它的工作就是告诉其他开发者如何使用这个类。而太多的内容会给阅读和使用增加困难。
隐藏可变属性(hiding mutability)
例如在.h文件中声明@property (nonatomic) NSMutable *array不可变属性。在类扩展中声明可变实例变量:NSMutableArray *_array;
如此以来,非本类对象只知道其为不可变数组。实际上是可变的实例。
头文件与继承(headers and inheritance)
子类无法获取父类的类扩展。
避免内存泄漏(preventing memory leaks)
强引用会保留对象的拥有方,使其不被释放。而弱引用则不会保留。
如果需要明确的将指针声明为弱引用,则可以标注__weak,类似以下代码:
__weak NSString *str;
深入学习:手动引用计数和ARC历史
Retain计数规则
- 如果用来创建对象的方法,其方法名是以alloc或new开头的,或者包含copy,mutableCopy,那么便得到的该对象的所有权(即可以假设新对象的retain计数是1,而且该对象不会在NSAutoreleasePool对象中)。
- 通过任何其他途径创建的对象(例如通过便捷方法),你是没有所有权的(即可以假设新对象的retain计数是1,而且该对象已经在NSAutoreleasePool中。如果没有保留该对象,那么当NSAutoreleasePool对象被“排干”(drain)时,这个对象就会被释放。)
- 如果你不拥有某个对象,但是要确保该对象能继续存在,那么可以通过向其发送retain消息来获得所有权。
- 当你拥有的某个对象不需要使用时,可以向其发送release或者autorelease。
理解内存管理的技巧之一是“从局部的角度来考虑问题,以类为分解”。
Collection类(collection classes)
NSSet对象最大用处是检查某个对象是否存在。完成此类任务NSSet对象的速度比数组快得多。
NSSet对象中的对象是无序的,所以不能通过索引来访问。只能向NSSet对象查询某个对象是否存在。
NSObject类定义了一个名为isEqual:的方法。用来检查两个对象是否相等。
- 相等(equal)和相同(identical)是两个概念。
- 相同的变量一定是相等的,相等的变量不一定相同。
不可修改对象(immutable objects)
使用不可修改的collection可以节约内存提高性能,因为它永远无需拷贝。
使用可修改的对象,则可能发生一种情况:程序中的其他代码可能在你使用这个可修改的对象的时候修改它的内容。为避免这种情况,就需要copy一份。
使用不可修改的collection,例如NSArray的copy方法其时不会做任何额外的工作,仅仅返回指向自身的指针。而NSMutableArray的copy方法会制作一份自己的拷贝,并返回指向新数组对象的指针。
数组排序(sorting arrays)
NSMutableArray 最常用的一个排序方法:
- (void)sortUsingDescriptors:(NSArray *)sortDescriptors;
该方法的实参是一个包含NSSortDescriptor对象的数组对象,排序描述对象包含两个信息,一是数组中对象的属性名,二是升序还是降序。
用于排序的属性可以是任意的实例变量,也可以是实例方法的返回值。
过滤(filtering)
对collection进行过滤的时候,程序将对collection对象和一条逻辑语句进行比较,得到一个合成的collection,这个collection只包含满足这条语句的对象。
- NSPredicate对象
详情参考《Predicate Programming Guide》
事例:
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"过滤条件"];
NSArray *array = [allAssets filteredArrayUsingPredicate:predicate];
C语言基本类型
collection只能保存对象,如果要保存float,int等变量,需要进行封装。
NSNumber封装的值不能用来计算,只能先提取出来,再做计算。
结构体用NSValue封装。
Collection与nil
本章所介绍的collection对象都不能保存nil。如果要将“空洞”加入collection,可以使用NSNull类。例如:
[hotel addObject:lobby];
[hotel addObject:[NSNull null]];
[hotel addObject:pool];
常量(constants)
在Objective-C中,通常可以通过两种途径来定义常量。即#define和全局变量。
预处理指令(Preprocessor directives)
C,C++,OC 代码文件需要经过两步才能完成编译:
- 首先预处理器会读入并处理整个文件。
- 接着,预处理器的输出结果会作为输入交给真正的编译器。预处理指令以#开头。
指定导入文件时,需要为文件名加上双引号或尖括号。如果是双引号,那么编译器会先在项目目录下查找相应的头文件。如果是尖括号,那么编译器会先在预先设定好的标准目录下查找相应的头文件。
为了简化文件导入代码并加快编译速度,Xcode加入了对预编译文件的支持,凡是出现在预编译文件中的头文件,Xcode都会事前编译好并自动导入每个文件。
通过#define,不仅可以替换代码中的某个特定的值,还可以构建类似函数的代码段,后者的这种代码段称为宏(macro)。
全局变量(global variables)
编写Objective-C时,通常不是使用#define来保存常量,而是使用全局变量来完成这项任务。
enum
为某个方法的形参声明某个特定的类型,并告诉编译器,调用方只能向该方法传入枚举类型中的一种。
通常情况下,枚举常量所代表的数字不会有实际的意义,其作用仅仅是为了能够区分不同的常量。
从OS X10.8 和 iOS6系统开始,苹果公司引入了一种新的enum声明语法:
NS_ENUM()。示例如下:
typedef NS_ENUM(int, Speed) {
BlenderS1,
BlenderS2
};
NS_ENUM()实际上是一个预处理器宏,它带有两个实参:数据类型和名字。
苹果公司使用NS_ENUM()做enum声明,最重要的优点是它可以声明整数的数据类型(short,unsigned,long等)。与旧语法相比,可以节省内存。
比较#define与全局变量
- 在某些情况下,使用全局变量的效率更高。例如,如果某个字符串都以一个固定的全局变量的形式出现,那么在进行字符串比较时,就可以使用==,而不是isEqual:(算术运算快于消息发送)。
- 调试程序时,全局变量可以在调试器中查看其值。
通过NSString 和 NSData 将数据写入文件(writing files with NSString and NSData)
在将字符串对象写入文件时,要指定字符串编码。字符串编码的作用是描述字符和代表字符的数字之间的映射关系。
文件的路径可以是绝对的或相对的。绝对路径以“/”开头(/代表文件系统的根目录)。而相对路径则是相对于程序当前的工作目录。编写程序时,通常都会使用绝对路径。
NSData对象“代表”内存中的某块缓冲区。可以保存相应字节数的数据。
回调(callbacks)
回调就是将一段可执行的代码和一个特定的事件绑定起来。当特定的事件发生时,就会执行这段代码。
在OC中,有四种途径可以实现回调:
- 目标-动作对(target-action)
- 辅助对象(helper objects)
- 通知(notifications)
- Block对象(Blocks)
运行循环(the run loop)
事件驱动的程序需要有一个对象,专门负责等待事件的发生。NSRunLoop实例会持续等待着,当特定的事件发生时,就会向相应的对象发送消息。NSRunLoop实例会在特定的事件发生时触发回调。
[[NSRunLoop currentRunLoop] run];
没有使用的变量可以使用__unused 来消除警告。
目前的回调规则如下:
当要向一个对象发送一个回调时,苹果公司会使用目标-动作对。
当要向一个对象发送多个回调时,使用相应协议的辅助对象。
回调与对象所有权
遵守以下规则:
- 通知中心不拥有观察者
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- 对象不拥有委托对象或数据源对象
- (void)dealloc {
[... setDelegate:nil];
[... setDataSource:nil];
}
- 对象不拥有目标
[... setTarget:nil];
选择器的工作机制
当某个对象收到消息时,会对该对象的类进行查询。
方法查询速度非常快。如果使用方法的实际名称进行查询,那么查询速度会很慢。为了提速,编译器会为每个其接触过的方法附一个唯一的数字。运行时,程序使用这个数字进行查找。
以上提到的代表特定方法名的唯一数字称为选择器(selector)。当一个方法需要一个选择器作为实参,它实际上就是需要这个数字。通过编译指令@selector,可以得到与方法名相对应的选择器。
Block对象
与函数指针相比,如果能正确的使用block对象,就可以写出更简介的代码。
Block对象VS其他回调
委托机制和通告机制有一个缺点,即回调的设置代码和回调方法的具体实现无法写在同一段代码中。而且两段代码通常会相隔很远。
深入学习Block对象
在block外的变量称为外部变量,当执行block对象时,为了确保其下的外部变量能够始终存在,相应地block对象会捕获这些变量。
对于基本类型的变量,捕获意味着程序会拷贝变量的值,并用block对象内的局部变量保存。对指针类型的变量,block会使用强引用。
在Block中使用self
myBlock = ^{
NSLog(@"Employee:%@", self);
}
Employee有一个指向Block对象的指针。这个block对象会捕获self,所以它有一个指回Employee的指针。陷入强引用循环。
为打破强引用,应该如此:
__weak Employee *weakSelf = self;
myBlock = ^{
Employee *innerSelf = weakSelf;
NSLog(@"Employee:%@", self);
}
在Block中无意使用self
如果直接在Block对象中使用实例变量,那么block会捕获self。而不会捕获实例变量。
init
每个类都有一个指定初始化方法。如:init是NSObject的指定初始化方法。
指定初始化方法扮演的是单一入口的角色。任何类都有且只有一个指定初始化方法。如果某个类还有其它初始化方法,那么这些方法应该(直接或间接)调用指定初始化方法。
编写指定初始化方法时,应该遵循以下规则:
- 其他的初始化方法都应该直接或间接的调用指定初始化方法。
- 指定初始化方法应该先调用父类的指定初始化方法,然后再对变量进行初始化。
- 如果某个类的指定初始化方法和父类的不同(这里指方法名不同),就必须覆盖父类的指定初始化方法,并调用新的指定初始化方法。
- 如果某个类有多个初始化方法,就应该在相应的头文件中明确的标明哪个方法是指定初始化方法。
禁用init方法
最佳解决方案是覆盖父类的指定初始化方法,然后通过某种途径告之程序员不能调用这个方法,并提供修改建议:
- (instancetype)init {
[NSException raise:@"CZWallSafeInitialization"
format:@"USE initWithSecretCode:, not init"];
}
再谈属性
深复制与浅复制
- copy复制后的对象是不可变的,mutableCopy复制后的对象是可变的。与原始对象是否可变无关。
- 对不可变对象进行copy是浅复制,mutableCopy是深复制。
- 对可变对象进行copy和mutableCopy都是深复制。
针对容器类对象的深复制与浅复制(只探讨复制后容器内元素的变化)
- 容器内对象浅复制: 方法: copy,mutableCopy,initWithArray: withItems:NO
- 容器内对象深复制: 方法: 归档,initWithArray: withItems:YES (如容器内对象也是容器对象,则子类容器对象是使用init方法是浅复制。使用归档方法是完全的深复制)
KVC (Key-value coding)
KVC能够让程序通过名称直接存取属性,因为与KVC有关的方法都是在NSObject中定义的,所以凡是继承自NSObject的类都具备KVC功能。
[a setProductName:@“Washing Machine”];
现在用KVC重写:
[a setValue@“Washing Machine” forKey:@“productName”];
setValue: forKey:方法会查找名为 setProductName:的存方法,如果a对象没有setProductName:方法,就会直接为实例变量赋值。
也可以使用KVC读取实例变量
[a valueForKey:@“productName”];
为什么使用KVC,有什么好处?
- 当苹果公司提供的某个框架需要向你编写的对象写入数据时,会使用setValve:forKey:
- 当苹果公司提供的某个框架需要从你编写的对象读取数据时,会使用valveForKey:
所谓对象封装是指对象的方法可以公开,但是实例变量应该保持私有。KVC是一个例外。
非对象类型
KVC只对对象有效,但是可以将要赋值的int,float等类型转化为NSNumber对象。
Key路径
当遍历一个特别长的属性表的时候可以使用Key path。将想要的key排成一个长串,用逗号分隔。
KVO
键值观察是指当指定的属性被修改时,允许对象接收通知的机制。总的来说就是告诉一个对象,“我要观察你的某个属性,如果他发生了变化,就通知我”。
在KVO中使用context
在代码中将某个对象注册为观察者时,需要传递指针作为context。当接收变化的通知时,context会随通知一起发送。context可以用来回答“这真的是我需要的通知吗”
显式触发通知
如果不使用存取方法设置属性,观察这便不会收到通知。因此应在属性被赋值的前后添加如下代码:
[self willChangeValueForKey:@“某属性”];
...;
[self didChangeValueForKey:@“某属性”];
独立的属性
_lastTime属性的值发生变化时,_lastTimeString也会发生变化时,当你想观察_lastTimeString时会发现观察者并没有正确的收到通知。为了修复这个问题,应在被观察者的.m文件中实现一个类方法来做这项工作。
+ (NSSet *)keyPathsForValuesAffectingLastTimeString {
return [NSSet setWithObject:@“lastTime”];
}
注意这个方法的命名:
它是keyPathsForValuesAffecting加上首字母大写的键的名字。
没有必要在.h中声明这个方法,系统会在运行时找到它。
范畴
应该使用范畴来给已经存在的类增加新方法,而不是在范畴中替换已存在的方法,这种情况下应该创建子类。
在定义方法名时加上前缀(cz_)
位运算
和内存打交道时,无法精确到位,必须以字节为单位。
编写代码时为了方便确认,会在十六进制前加上前缀0x
需要使用按位或的运算
使用整数来保存某个设置的特定状态,因为整数是由一系列的位组成,所以可以使用整数中的某个位来表示设置的某个状态时打开的还是关闭的(只有与状态相对应的那个位是1,其余是0)。
同样的还有按位与,编程时,可以通过按位与运算检查某个整数是否包含指定的标志位。
C字符串和NSString对象的相互转化
char *greeting = "Hello";
NSString *x = [NSString stringWithCString:greeting encoding:NSUTF8StringEncoding];
NSString *greeting = @"Hello";
const char *x = NULL;
if ([greeting canBeConvertedToEncoding:NSUTF8StringEncoding]) {
x = [greeting cStringEncoding:NSUTF8StringEncoding];
}