更新于 
苹果手机app开发 Learned by 且听风吟
更新于 

objective-c 基础学习

对C的扩展

Objective-C 程序

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

int main (int argc, char *argv[]) {
NSLog (@"Hello, Objective-C!");
return 0;
} // main

main.m文件,Xcode通过.m来表明文件是Objective-C类型,从而交给Objective-C编译器执行。.m代表的是message,这是Objective-C的一个重要特性。

解构 Objective-C 程序

#import语句

保证头文件只会被引用一次。

NSLog()@“字符串”

字符串打印函数,与C语言中的printf类似,但是在此基础之上添加了一些新的特性:

  • 时间戳
  • 日期戳
  • 自动附加换行符('\n')

NS前缀只是为了避免名称冲突。
@表示引号内的字符串会被作为CocoaNSString元素来处理。

布尔类型

  • 不是仅可以存储YES或NO值的真正布尔类型
  • 是对signed char的类型重定义
  • 一个字节,使用#define将YES定义为1,NO定义为0

面向对象编程

小知识

  1. id是一种泛型,可以引用任意类型的对象
  2. [对象 方法]表示调用该对象的方法

相关术语

  1. 类:表示对象类型的结构体
  2. 对象:包含值和指向其类的隐藏指针的结构体
  3. 实例:对象
  4. 消息:对象可以执行的动作
  5. 方法:响应消息而运行的代码
  6. 方法调度:推测执行什么方法以响应某个特定的消息

类的声明

@interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface Cicle : NSObject
{ // 实例变量,对象一旦被创建,都会默认包含所有的实例变量,并且赋予默认值。如果没有实例变量,可以直接省略花括号
ShapeColor fillColor;
ShapeRect bounds;
}
// 方法
- (void) setFillColor: (ShapeColor) fillColor;

- (void) setBounds: (ShapeRect) bounds;

- (void) draw;

// 方法调用 中缀符 [对象 方法[:,冒号有无取决于后面有无参数] [参数说明: 要传入的实际参数] [...]]
// 栗子:[textThing setStringValue: @"helllo there" color: blue]

@end // Cicle

@implementation

类方法的真正实现

1
2
3
4
5
6
@implementation Cicle
- (void) setFillColor: (ShapeColor) c
{
fillColor = c;
} // setFillColor
......
  1. @implementation 是编译器指令,用于说明为某个类提供执行的代码
  2. 可以定义没有在 @interface 中声明的方法,相当于该类的私有方法。不过Objective-C中并不存在真正的私有方法。
  3. @interface@implementation 中的方法参数可以不同,但是,@implementation 中的方法参数不应与实例变量名相同,防止出现覆盖的情况

继承

语法格式

1
2
@interface 子类 : 父类
@end
  1. 不支持多继承
  2. 需要重写的方法仍然需要在 @implementation 中定义,什么都不写或者返回一个虚值(dummy)

方法调度

对象拥有一个指向类的指针,类拥有一个指向其执行代码的指针和一个指向其父类的指针。当对象去调用一个方法时,首先会通过它的指向类的指针去它父类的代码里面去寻找是否包含该方法,如果不包含,则转去它的父类的父类里面去寻找,以此类推,直到找到对应的方法。如果在顶层父类中仍旧没有找到该方法,则会报错。

实例变量

  1. 每个方法调用都会传入一个名 self 的隐藏参数用来指向对象本身
  2. 每一个对象都有一个isa指针指向其类对象,每一个类对象也有一个 isa 指针指向其父类对象。顶层父类 NSObject只有一个实例变量— isa 指针,用来指向本身
  3. 对象在访问其实例变量的时候,会使用 self 指针从继承链实例变量顶层一次往下寻找,即:从 NSObjectisa开始往下找

super 关键字

调用父类的方法

复合

通过将实例对象的指针作为当前对象的实例变量来实现

准确来说,是一种对象间的组合

栗子

1
2
3
4
5
6
@interface Car : NSObject
{
Tire *tires[4]; // 此处便是复合
Engine *engine;
}
@end // Car

此时,Car对象会给实例对象指针分配内存空间,但是并不会给响应的对象分配内存空间

使用 new 创建对象的过程

  1. 分配内存空间
  2. 调用类的init方法进行初始化

init 方法的简要讲解

代码

1
2
3
4
5
6
7
@implementation Car
- (id) init {
if (self = [super init]) {
....
}
}
@end

说明

  1. [super init] 让父类的初始化工作完成
  2. self = [super init] 将返回的已经初始化的对象指针赋予给当前的 self,防止出现父类初始化返回的对象与一开始创建的对象不一致的问题

存取方法

用来存取或者改变对象属性的方法

  1. setter 方法的命名格式是在其所要修改的变量名称前面加 set
  2. getter 方法的命名格式是直接将所要读取的变量作为方法名

所有对象间的交互都是通过指针来实现的

复合还是继承

  1. 继承: is a
  2. 复合: has a

源文件组织

拆分接口与实现

  1. .h 头文件:存放类的 @interface 指令、公共 struct 定义、enum 常量、#defineextern 全局变量等
  2. .m 文件:存放所有的实现内容

.mm 文件可以同时使用 C++Objective-C 编程

  • .m 文件中使用 @import 导入创建的 .h 文件,若是系统文件,使用尖括号,若是项目中自定义的文件,使用双引号

@class 关键字

前向引用。告诉编译器这是一个类,以后会知道这个类具体的内容。

可以避免 .h 文件中的循环引用问题

注意

如果一个类继承另外一个类,那么,父类就不可以使用 @class 来进行前向引用。因为,编译器要编译子类的时候,需要预先知道其父类的详细信息,从而在其父类的基础之上对子类进行编译。而且,@class 实际上是通过告诉编译器这是一个使用指针来指向对象的变量,所以只需要申请对应的内存空间即可,但是子类继承父类则不是这么回事。故而,我们不可以使用上述方法来引入父类。

Xcode

按键描述
Control + I选中代码,点击自动缩进
Command + [将选中的代码左移
Command + ]将选中的代码右移
Command + shift + Oopen quickly
选中 + option + 点击打开开发者文档
Tab键接受代码自动完成提示
Control + .循环浏览代码提示
shift + Control + .反向循环浏览代码提示
Command + Comtrol + S创建快照
Control + F前移光标
Control + B后移光标
Control + P移动光标到上一行
Control + N移动光标到下一行
Control + A移动光标到本行行首
Control + E移动光标带本行行位
Control + T交换光标左右两边的字符
Control + D删除光标右边的字符
Control + K删除本行
Control + L将光标置于窗口正中央
Command + Control + 向上方向键打开配套的文件
Command + Y激活/禁用断点
Command + Control + Y继续运行(在调试器中有效)
F6跳过
F7跳入
F8跳出
command+option + ⬅️局部折叠
command+option + shift + ⬅️全部折叠

Foundation Kit

struct 结构体

NSRange 范围

1
2
3
4
5
typedef struct _NSRange
{
unsigned int location;
unsigned int length;
} NSRange;
  1. 表示事物的范围。location 表示范围的起始位置,length 表示范围的长度。注意:location 还可以使用 NSNotFound 来表示没有范围

  2. 通常用于表示字符串或者数组的范围

创建 NSRange 方式

1
2
3
4
5
6
7
8
9
10
11
// way 1 
NSRange range;
range.location = 10;
range.length = 10;

// way 2
NSRange range = {10, 10};

// way 3 该方式可以作为函数参数直接传递
NSRange range = NSMakeRange(10, 10);

几何数据类型

Core Graphics 框架(由C语言所写)提供,用于进行2D渲染。带有前缀 CG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 坐标
struct CGPoint
{
float x;
float y;
};
// 存储长度和宽度
struct CGSize
{
float width;
float height;
};
// 矩形
struct CGRect
{
CGPoint origin;
CGSize size;
};
// 创建这些数据类型的函数:CGPointMake()、CGSizeMake()、CGRectMake()

使用结构体的原因

在 Objective-C 中,对象都是动态分配的,而动态分配会消耗大量的时间。而在程序中,尤其是GUI程序中,存在着大量的临时的坐标、大小和矩形区域的创建和使用。如果将结构体换成对象的话,会大大增加系统的开销。

字符串 NSString

创建字符串

1
2
3
4
5
6
7
8
// + (id) stringWithFormat: (NSString *) format, ...;

NSString *height;
height = [NSString stringWithFormat: @"Your height is %d feet, %d inches", 5, 11];

// 结果
// Your height is 5 feet,11 inches

stringWidthFormat 方法中的参数列表中的最后一个参数是 ... ,这说明这个方法可以接受多个以逗号隔开的其他参数

类方法

  1. Objective-C 在生成一个类的时候,会创建一个代表该类的类对象(class object)。其包含了指向父类、类名和类方法列表的指针,还包含一个 long 类型的数据,用来给新创建的对象指定大小
  2. 在声明方法的时候添加了加号,那么,就声明了一个类方法,该方法属于类对象。这个方法通常会用来创建新的类的实例对象,这种方法被称为工厂方法。

关于大小 length

返回字符串的字符个数

- (NSUInteger) length;

1
NSUInteger length = [height length];

相比于C语言中的 strlen 方法,该方法可以精确地计算各种语言的字符串的字符个数。因为有些字符的字节数可能会大于一个字节,而 strlen 函数只是单纯地返回字节数而导致计算。

字符串比较

1
- (BOOL) isEqualToString: (NSString *) aString;

返回一个BOOL值来表示两个字符串是否相同。如果要比较两个字符串是否是同一个,需要使用 == 来直接比较对象指针的地址

1
- (NSComparisonResult) compare: (NSString *) aString;

将当前的对象与传递的字符串中的字符进行逐个比较,返回一个 NSComparisonResult (一个 enum 型枚举)来表示最终的比较结果。
区分大小写

1
2
3
4
5
6
7
8
enum
{
NSOrderedAscending = -1, // 左边字符串更小
NSOrderedSame, // 两字符串相同
NSOrderedDescending // 右边字符串相同
};
typedef NSInteger NSComparisonResult;

栗子
1
2
3
4
5
6
7
NSString *thing1 = @"hello 5";
NSString *thing2 = [NSString stringWithFormat: @"hello %d", 5];

if ([thing1 isEqualToString: thing2]) {
NSLog(@"They are same!");
}

不区分大小写的比较

1
- (NSComparisonResult) compare: (NSString *) aString options: (NSStringCompareOptions) mask;

options: 参数是一个掩位码,可以使用位或 | 来添加选项标记。可用的标记如下:

  • NSCaseInsensitiveSearch: 不区分大小写
  • NSLiteralSearch: 区分大小写
  • NSNumericSearch: 比较字符串的个数而不是字符串的值
栗子
1
2
3
4
if ([thing1 compare:options: NSCaseInsesitiveSearch | NSNumericSearch] == NSOrderedSame) {
NSLog(@"They match!");
}

字符串内是否还包含别的字符串

  1. 判断字符串是否以另一个字符串开头
    1
    - (BOOl) hasPrefix: (NSString *) aString;
  2. 判断字符串是否以另一个字符串结尾
    1
    - (BOOl) hasSuffix: (NSString *) aString;
  3. 判断字符串内部是否包含另一个字符串
    1
    - (NSRange) rangeOfString: (NSString *) aString;
    返回一个 NSRange 结构体,说明字符串匹配的部分在哪里以及能够匹配上的字符个数。如果没有匹配,那么结构体里面的 location 值为 NSNotFound

可变性

NSString 是不可变的。不能以删除字符或者添加字符的方式来改变字符串。

注意NSString 的子类 NSMutableString 可变

创建 NSMutableString
1
2
3
4
- (id) stringWithCapacity: (NSUInteger) capacity;

NSMutableString *string = [NSMutableString stringWithCapacity: 47];

这里的容量只是一个建议值, 可以超过这个大小。

  1. 添加字符串
  • appendString
1
- (void) appendString: (NSString *) aString;

将要添加的字符串添加到当前字符串的结尾

  • appendFormat
1
- (void) appendFormat: (NSString *) aString, ...;

并不会创建新的字符串,而是将格式化后的字符串添加到当前字符串的结尾

  • 栗子
1
2
3
NSMutableString *string = [NSMutableString stringWithCapacity: 47];
[string appendString: @"Hello there "];
[string appendFormat: @"human %d", 39];
  1. 删除字符串
  • deleteCharactersInRange
1
- (void) deleteCharactersInRange: (NSRange *) aRange;

集合大家族

NSArray

是一个 Cocoa 类,用来存储任意类型对象的有序列表

  1. 只能存储 Objective-C 对象
  2. 不能存储 nil
  3. 不可变数组
创建
1
2
3
4
// 方式 1
NSArray *array = [NSArray arrayWithObjects:@"one", @"two", @"three", nil];
// 方式 2 字面量语法格式 无需添加 nil 来表示数组结束
NSArray *array2 = @[@"one", @"two", @"three"];
  1. 发送一个以逗号分隔的对象列表
  2. 列表末尾添加 nil 代表结束,这也是数组无法存储 nil 的原因
内置方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取元素数量
- (void) count;

// 获取某个特定索引处的元素
- (id) objectAtIndex: (NSUInteger) index;

// 通过字面量获取元素
id *myObject = array[1];

// 栗子
for (NSInteger i = 0; i < [array count]; ++i) {
NSLog(@"index %d has %@.", i, [array objectAtIndex:i]);
NSLog(@"index %d has %@.", i, array[i]);
}

异常(exception) 是 Cocoa 说明“我不知道该如何处理”的方式

1
2
3
4
5
6
// 切分数组
NSString *string = @"oop:ack:bork:greeble:ponies";
NSArray *chunks = [string componentsSeparatedByString:@":"]; // 根据":"来切割字符串并返回一个数组

string = [chunks componentsJoinedByString:@"-"]; // 根据"-"来连接数据中的字符串并返回最终合成的字符串

可变数组:NSMutableArray

创建
1
2
3
4
5
6
+ (id) arrayWithCapacity: (NSUInteger) numItems;

// 栗子

NSMutableArray *array = [NSMutableArray arrayWithCapacity:17];

  1. 容量只是最终容量的一个参考
  2. 没有用来创建 NSMutableArray 的字面量语法
  3. 添加元素或删除元素的时候会动态地调整容量大小
添加对象
1
2
3
4
5
6
- (void) addObject: (id) anObject;

// 栗子
for (NSIngeter i = 0; i < 4; ++i) {
[array addObject: @"123"];
}
  1. 在数组末尾添加元素
特定位置删除元素
1
2
3
4
- (void) removeObejectAtIndex: (NSUIngeter) index;

// 栗子
[array removeObjectAtIndex: 0]; // 删除位置为0的数组元素
  1. 删除元素之后,数组后面的所有元素自动向前移动

枚举

NSEnumerator,Cocoa 用它来表示集合中迭代出的对象。需要提前获取数组的枚举器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (NSEnumerator *)objectEnumerator;  // 从前往后的迭代器

// 栗子
NSEnumerator *enumerator = [array objectEnumerator];

- (NSEnumerator *) reverseObjectEnumerator; // 从后往前的迭代器


// 使用枚举器获取下一个对象

- (id) nextObject; // 如果返回 nil 表示迭代结束 这也是数组中不能存储 nil 的另外的一个原因

// 栗子
NSEnumerator *enumerator = [array objectEnumerator];
while (id = thingie = [enumerator nextObject]) {
NSLog(@"I found %@", thingie);
}

注意:不能在使用迭代器的过程中添加或者删除数组中的元素,否则会导致迭代器混乱,获得未定义的结果

快速枚举

类似于foreach

1
2
3
4
for (NSString *string in array) {
NSLog(@"I found %@", string);
}

  1. 无法在旧的机器上运行

NSDictionary

不可变

创建

1
2
3
4
5
6
// 方式 1 字面量方式
@{key:value,...}

// 方式 2 函数方式
+ (id) dictionaryWithObjectsAndKeys: (id) firstObject,...;

  1. dictionaryWithObjectsAndKeys 后面的参数先是value,然后才是key
  2. dictionaryWithObjectsAndKeysnil 值作为终止符号,所以,NSDictionary 不能存储 nil
  3. 字面量语法不需要以 nil 结尾
1
2
3
4
5
6
7
8
9
Tire *t1 = [Tire new];
Tire *t2 = [Tire new];
Tire *t3 = [Tire new];
Tire *t4 = [Tire new];

NSDictionary *tires = [NSDictionary dictionaryWithObjectsAndKeys: t1, @"font-left", t2, @"font-right", t3, @"back-left", t4, @"back-right", nil]; // 先value后key 以nil作为结尾

NSDictionary *tires = @{@"font-left":t1, @"font-right":t2, @"back-right":t3, @"back-right":t4};

访问

1
2
3
4
5
6
// 方式 1 使用函数
- (id) objectForKey: (id) aKey;

// 方式 2 字面量语法
tires[key];

可变字典 NSMutableDictionary

1
2
3
4
5
6
7
8
9
10
11
12
// 创建
+ (id) dictionaryWithCapacity: (NSUInteger) numItems;

// 直接给 NSMutableDictionary 发送 dictionary 信息
[NSMutableDictionary dictionary];

// 添加元素 如果存在相同的key 那么会直接覆盖原有的value
- (void) setObject: (id) anObject forKey: (id) aKey;

// 删除元素
- (void) removeObjectForKey: (id) aKey;

不要乱来

不要尝试创建 NSStringNSArray、或 NSDictionary 的子类。因为在 Cocoa 中,许多的类都是以类簇的方式来实现的,他们是一群隐藏在通用接口之下的与实际实现相关的类。我们在创建 NSString 对象的时候,实际上可能得到是 NSLiteralString 或者其他类型的类。

其他数值

NSNumber

1
2
3
4
5
6
// 函数方法

+ (NSNumber *) numberWithChar: (char) value;
+ (NSNumber *) numberWithInt: (int) value;
+ (NSNumber *) numberWithFloat: (float) value;
+ (NSNumber *) numberWithBool: (BOOL) value;
1
2
3
4
5
6
7
8
9
10
// 字面量方法

NSNumber *number;
number = @'X'; // 字符型
number = @12345; // 整型
number = @12345ul; // 无符号长整数
number = @12345ll; // long long
number = @123.45f; // 浮点型
number = @123.45; // 双浮点型
number = @YES; // 布尔值
1
2
3
4
5
6
7
// NSNumber --> 基本数据类型

- (char) charValue;
- (int) intValue;
- (float) floatValue;
- (BOOL) boolValue;
- (NSString *) stringValue;

Objective-C 不支持自动装箱功能。

NSValue

可以封装任意值

1
2
// 函数方法
+ (NSValue *) valueWithBytes: (const void *) value objcType: (const char *) type;
  1. 传递的参数是要封装的数值的地址
  2. objcType 指的是一个描述数据类型的字符串,通常使用 @encode(数据类型) 来获取该字符串
1
2
3
4
5
6
7
8
9
10
// 获取数值
- (void) getValue: (void *)buffer;

// 栗子
NSRect rect = NSMakeRect (1, 2, 30, 40);
NSValue *value = [NSValue valueWithBytes:&rect objcType:@encode(NSRect)];
[array addObject:value];

value = [array objectAtIndex:0];
[value getValue:&rect];
  1. getValue 中传入的参数是要存储数据所要存储的空间的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
// 常用的将struct转换成NSValue的方法
+ (NSValue *)valueWithPoint: (NSPoint) aPoint;
+ (NSValue *)valueWithSize: (NSSize) size;
+ (NSValue *)valueWithRect: (NSRect) rect;
- (NSPoint) pointValue;
- (NSSize) sizeValue;
- (NSRect) rectValue;

// 栗子
value = [NSValue valueWithRect: rect];
[array addObject: value];
...
NSRect anotherRect = [value rectValue];

NSNull

代表什么都没有,用于消除 nil 带来的歧义,并可以作为对象存储到容器中

1
2
3
4
5
6
7
8
+ (NSNull *) null;  // 总是返回相同的数值 可以使用 == 来与其他的值进行比较

// 栗子
[contact setObject: [NSNull null] forKey: @"home fax machine"];
id homefax = [contact objectForKey: @"home fax machine"];
if (homefax == [NSNull null]) {
// ...
}

内存管理

对象生命周期

诞生(alloc、new) --》 生存(接收消息并执行操作) --》 交友(复合、想方法传递参数) --》 死去

引用计数

  1. 每一个对象都会有一个引用计数器与之关联,当其被访问时,引用计数器会加1,访问结束后,引用计数器会减1

  2. 当使用 alloc new 或者 copy 创建一个对象时,对象的引用计数器会被设置成1

  3. 给对象发送 retain 消息,引用计数器会加1;给对象发送 release 消息,引用计数器会减一

  4. 对象的引用计数器减为0时,编译器会自动调用对象的 dealloc 方法来释放资源。我们也可以重写 dealloc 方法

1
2
3
- (id) retain;
- (oneway void) release;
- (NEUInteger) retainCount;
  1. retain 会返回一个 id 类型的值,从而可以实现对象在引用计数器加1的同时还可以完成其他的动作

自动释放池和销毁时间

创建

  1. @autoreleasepool 关键字

    1
    2
    3
    @autoreleasepool {

    }
  2. NSAutoreleasePool 对象

    1
    2
    3
    4
    NSAutoreleasePool *pool;
    pool = [NSAutoreleasePool new];
    ...
    [pool release];

优先使用关键字语法

  1. 自动释放池的分配和销毁的代价很小
  2. 自动释放池以栈的形式实现

垃圾回收

自动内存管理机制

启用垃圾回收机制后,平常的内存管理指令就不起作用了

对象初始化

分配对象

向某个类发送 alloc 消息,可以为该类分配一块足够大的内存空间,用来存放该类的全部实例变量。
同时,alloc 方法还会将这块内存空间全部初始化为0

初始化对象

1
2
3
4
// 正确写法
Car *car = [[Car alloc] init];
// 错误写法
Car *car = [Car alloc]; [Car init];

如果不使用嵌套,那么会导致初始化方法返回的对象和分配的对象不一致

编写初始化方法

1
2
3
4
5
6
7
8
9
10
(id) init {
if (self = [super init]) {
engine = [Engine new];
tires[0] = [Tire new];
tires[1] = [Tire new];
tires[2] = [Tire new];
tires[3] = [Tire new];
}
return (self);
} // init

if (self = [super init]) 的解释:先运行 [super init] 以完成父类自身的初始化操作。这个赋值操作只影响 init 方法中的 self 值,不会影响该方法之外的任何内容

  1. 如果在初始化对象时出现问题,那么 init 方法可能会返回 nil,表示没有成功初始化对象

惰性求值:即使目前没有设置自定义属性的值,也应该等到调用者需要时再创建对象

构造初始化方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@interface Car: NSObject
{
...
}
- (id) initWithPresure: (float) p treadDepth: (float) td
...
@end

@implementation Car

- (id) initWithPresure: (float) p treadDepth: (float) td {
if (self = [super init]) {
presure = p;
treadDepth = td;
}
return self;
}
...
@end

// 使用
Car *car = [[Car alloc] initWithPresue:20 treadDepth:33];

属性

语法

1
@property 类型 属性名
  1. 可以通过调用 -set属性名 来设置属性值
  2. 可以通过调用 -属性名 来获取属性值
  3. @property 的作用是让编译器自动添加 settergetter 方法
1
@synthesize 属性名
  1. 创建了该属性的访问代码
  2. Xcode4.5 以后的版本不需要再使用该语法了

变量的声明位置的区别

  1. 实例变量如果在 .h 文件中声明,那么,其子类也可以直接通过属性来访问实例变量
  2. 实例变量如果在 .m 文件中声明,那么,其子类不可以直接通过属性来访问实例变量
  3. 如果我们并没有声明实例变量,那么编译器会自动帮我们创建实例变量

点表达式的妙用

1
2
对象.属性名 = 属性值;  // 修改
对象.属性名 // 访问

属性扩展

1
2
@property (copy) 属性名;  // 属性会被复制
@property (assign) 属性名; // 默认是弱引用
  1. 如果没有手动指定属性的特性,那么会默认使用 assignnonatomic
  2. nonatomic 属性如果不在多线程中使用,可以提高访问方法的调用速度
  3. 自己定义了 settergetter 方法,就不可以使用 atomic 特性

名称的使用

属性的名称和实例变量的名称可以不相同

语法

1
2
3
4
5
6
7
8
9
10
11
12
13
@synthesize 属性名 = 实例变量名

// 栗子

@interface Car : NSObject
{
NSString *appellation;
...
}

@property (copy) NSString *name;

@synthesize name = appellation;

编译器仍旧会创建 -setName-name 方法,只不过里面操作的实例变量变为了 appellation

只读属性

1
@property (readonly) 属性类型 属性名;

@dynamic

告诉编译器不要生成任何代码或者创建相应的实例变量

1
@dynamic 属性名称;

类别

一种为现有的类添加新方法的方式

  1. 类别代码通常单独放在一个文件中,并且按照“类名+类别名”命名文件

声明

1
2
3
4
5
6
7
8
9
10
@interface NSString (NumberConvenience)
- (NSNumber *) lenghtAsNumber;
@end

@implementation NSString (NumberConvenience)
- (NSString *) lengthAsNumber {
NSUInteger length = [self length]; // 获取字符串的长度
return ([NSNumber numberWithUnsignedInt:length]);
}
@end
  1. 类名后面的括号里面的新名称就是类别的名称
  2. 只要保证类别名称唯一,你可以向一个类添加任意数量的类别
  3. 可以添加方法但是不能添加实例变量
  4. 可以添加属性,但是属性必须是 @dynamic 类型的。好处是你可以通过点来访问 settergetter 方法

**注意:**任何 NSString 对象都可以响应 lengthAsNumber 消息,包括字面量字符串、description 方法返回的字符串、可变字符串、其他工具集部分字符串、文件中加载的字符串、从因特网海量内容中提取的字符串等

类别的局限性

  1. 无法添加新的实例变量
  2. 名称冲突,类别的优先级高于类中方法的优先级。当类别的方法和类中的方法重名的时候,类别的方法会取代类中的方法,导致类中的方法不再可用。

类别的优势

  1. 将类的实现代码分散到不同的文件中
  2. 创建对私有方法的前向引用
  3. 向对象添加非正式协议

类扩展 特殊的类别

  1. 无需类别名
  2. 可以在自己的类中使用
  3. 可以添加实例变量
  4. 可以将只读的权限改为可读可写的权限
  5. 创建的数量不限
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface Things : NSObject
@property (assign) NSInteger things;
@property (readonly, assign) NSInteger things;
...
- (void) resetAllValues;
@end

@interface Things ()
{
NSInteger thing4;
}
@property (readwrite, assign) NSInteger thing2;
@property (assign) NSInteger thing3;
@end
  1. 所添加的都是私有的属性和方法
  2. 类别可以访问继承的类的实例变量

Cocoa 并没有真正的私有方法,我们可以直接使用对象支持的却没有在 @interface 中声明的方法,不过会导致编译器警告

通过在类别中声明和实现私有方法来解决

非正式协议和委托类别

委托(delegate)是一种对象,由另一个类请求执行某些工作。

  1. 被发送给委托对象的方法可以声明为一个 NSObject 的类别
  2. 也就是说,只要实现了委托方法,任何类的对象都可以成为委托对象
  3. 创建一个 NSObjct 的类别被称为“创建一个非正式协议”

响应选择器

选择器只是一个方法名称,但是它会以 Objc 运行时使用的特殊方式编码,以快速查询。可以使用 @selector() 圆括号中的方法名称来指定选择器

1
2
3
4
5
6
7
8
@selector(setTire:atIndex)

// NSObject 使用 respondsToSelector: 来确定是否可以响应某个特定的消息

if ([car respondsToSelector:@selector(setEngine)]) {
NSLog(@"sdfk");
}

协议

正式协议

包含了方法和属性的有名称列表

使用协议的方式是在类的 @interface 声明中列出协议的名称。采用协议就意味着你必须实现该协议中的所有方法。

声明协议

1
2
3
@protocol NSCopying
- (id) copyWithZone: (NSZone *) zone;
@end
  1. 协议名称必须唯一
  2. 可以继承父协议,后面使用尖括号包裹父协议
1
2
3
@protocol MySuperDuberProtocol <MyPerentProtocol>

@end
  1. 协议中不会引入新的实例变量
  2. 采用了协议的类必须实现协议中的所有的方法,包括协议继承父协议的方法

采用协议

在类的声明中使用尖括号包裹协议

1
2
3
4
5
6
@interface Car : NSObject <NSCopying, NSCoding>
{

}
...
@end
  1. 一个类只能继承一个直接父类
  2. 一个类可以继承多个协议

复制

copy 消息会告诉对象创建一个全新的与接收 copy 消息的对象完全一样的对象

1
2
3
4
5
6
7
8
9
10
11
12
@interface Engine : NSObject <NSCopying>
@end // Engine

@implementation Engine
- (id) copyWithZone: (NSZone *) Zone
{
Engine *engineCopy;
engineCopy = [[[self class] allocWithZone: zone] init];
return (engineCopy);
}
@end

  1. [self class] 获取 self 所属的类,这里的 self 指的是正在接收 copy 消息的对象
  2. allocWithZone 是一个类方法,根据类来创建对象的内存空间
  3. Engine 的子类对象也可以被复制

协议和数据类型

可以在使用的数据类型中为实例变量和方法参数指定协议名称。

1
- (void) setobjectValue: (id<NSCopying>) object;
  1. 传入的参数类型必须遵循 NSCopying 协议

objc 2.0 的新特性

1
2
3
4
5
6
7
8
9
10
@protocol BaseballPlayer
- (void) drawHugeSallary; // 必须实现的方法
@optional // 可选择实现的方法
- (void) slideHome;
- (void) catchBall;
- (void) throwBall;
@required // 必须实现的方法
- (void) swingBat;
@end

委托方法

委托就是一个对象指定另外一个对象处理某些特定任务的设计模式

代码块和并发性

代码块(闭包)

对c语言函数的扩展

由c语言实现

两种绑定类型

  1. 自动绑定
    • 使用栈中的内存
  2. 托管绑定
    • 使用堆区内存

代码块和函数指针

代码块的特征

  1. 返回类型可以手动声明也可以由编译器推导
  2. 具有指定类型的参数列表
  3. 拥有名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义 
<returntype> (^blockname) (list of arguments) = ^(arguments) {body;};
void (^my_block)(void); // 无返回值 无参数 有名

// 栗子
int (^square_block) (int number) = ^(int number) {
return number * number;
};
int result = square_block(5);
...

// 如果没有参数,可以省略 `^(arguments)` 括号内容和括号本身
void (^theBlock)() = ^{...};

  1. 代码块声明成了变量,可以像函数一样使用
  2. 代码块可以替换原先的函数:代码块可以访问与它相同的有效范围内声明的变量,也就是说代码块可以访问与它同时创建的变量
1
2
3
4
5
6
int value = 6;
int (^multiply_block)(int number) = ^(int number) {
return value * number; // 直接使用外部的与代码块在同一话括号内的 value
};
int result = multiply_block(7);
...

作为参数直接使用代码块

1
2
3
4
5
NSArray *array = [NSArray arrayWithObjects: @"Amir", @"Mishal", @"Irrum", nil];
NSLog(@"Unsorted Array %@", array);
NSArray *sortedArray = [array sortedArrayUsingComparator:^(NSString *object1, NSString *object2){
return [object1 compare: object2];
}];

使用 typedef 关键字

1
2
3
4
5
6
typedef double (^MKSampleMutiply2BlockRef) (double c, double d);

MKSampleMutiply2BlockRef mutiply2 = ^(double c, double d) {
return c * d;
};

代码块和变量

代码块被声明后会捕捉创建点时的状态。代码块可以访问函数用到的标准类型的变量:

  1. 全局变量
  2. 全局函数
  3. 封闭范围内的参数
  4. 函数级别的 __block 变量。
    • 用于修饰在代码块中可被修改的变量
    • 不可被声明为 __block 的类型
      • 长度不可变的数组
      • 不包含长度可变数组的结构体
  5. 封闭范围内的非静态变量会被捕获为常量
  6. objc的实例变量
  7. 代码块内部的本地变量

并发性

能够在同一时间执行多项任务的程序称为并发程序

1
2
3
4
@synchronized (theObject)
{

}
  1. 可以确保不同的线程会连续地访问临界区的代码
  2. 如果属性没有指定 nonatomic 特性,那么生成的 gettersetter 是彼此互斥的

选择性能

让一些代码在后台执行

创建在线程执行的方法

1
2
- (void) myMethod;
- (void) myMethod:(id)myObject;
  1. 不能有返回值
  2. 要么没有参数,要么只有一个参数
  3. 需要在方法中创建自动释放池
1
2
3
4
5
6
7
8
- (void) myBackgroundMethod {
@autoreleasepool {
NSLog(@"My Background Method");
}
}

// 执行
[self performSelectorInBackground:@selector(myBackgroundMethod) withObject:nil];
  1. 当方法执行结束之后,objc 运行时会特定地清理并弃掉线程

调度队列

  1. 连续队列:根据指派的顺序执行任务
  2. 并发队列:并发执行一个或者多个任务,也是按照指派的顺序来执行任务
  3. 主队列:应用程序中有效的主队列,执行的是应用程序的主线程任务
连续队列
1
2
dispatch_queue_t my_serial_queue;
my_serial_queue = dispatch_queue_create("com.apress.MySerialQueue1", NULL);
  1. 第一个参数是队列名称,第二个参数是负责提供给队列的特性
并发队列
  1. 运行可以并行执行的任务
  2. 一次所运行的任务数是不确定的
  3. 三种并发队列
    • 高优先级
    • 默认优先级
    • 低优先级
1
2
dispatch_queue_t myQueue;
myQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 默认优先级
主队列
1
dispatch_queue_t main_queue = dispatch_get_current_queue(void);
  1. 通常要以同步方式使用该队列,防止出现阻塞主应用程序运行

队列也要内存管理

  1. 调度队列也是引用计数对象,可以使用 dispatch_remaindispatch_release 来修改引用计数器的值
  2. 只能对自己创建的队列使用上述方法,不能用自全局调度队列上

队列的上下文

  1. 可以给调度对象指派全局数据上下文
  2. 可以在数据上下文中指派任意类型的数据
  3. 上下文的内存管理需要自己做
1
2
3
4
NSMutableDictionary *myContext = [[NSMutableDictionary alloc] initWithCapacity:5];
[myContent setObject:@"My Context" forKey:@"title"];
[myContent setObject:[NSNumber numberWithInt:0] forKey:@"value"];
dispatch_set_context(_serial_queue, (__bridge_retained void*)myContext);
  1. 使用字典来存储上下文
  2. 需要保证对象是有效的,所以使用 __bridge_retained 来给 myContext 的引用计数器加1
清理函数

对象在弃用之前调用的函数

1
2
3
4
5
6
7
8
9
// 格式
void function_name (void *context);

// 栗子
void myFinalizerFunction(void *contect) {
NSLog(@"myFinalizerFunction");
NSMutableDictionary *theData = (__bridge_transfer NSMutableDictionary*)contex;
[theData removeAllObjects];
}
  1. __bridge_transfer 把对象的内存管理由全局释放池变成函数
获取上下文
1
NSMutableDictionary *myContext = (__bridge NSMutableDictionary *) dispatch_get_context(dispatch_get_current_queue());
  1. 添加 __bridge 是为了告诉 ARC 我们自己不想管理内存,交由系统来进行管理
添加任务
  1. 同步:一直等待前面的任务结束
  2. 异步:不必等待任务,会立即返回。优先使用这种方式

调度程序

最简单的方法是通过代码块

  1. 代码块必须是 dispatch_block_t 类型
  2. 没有返回值
  3. 没有参数
1
typedef void (^dispatch_block_t) (void)

使用代码块对象

1
2
3
4
dispatch_block_t myBlcok = ^{
NSLog(@"My Predfriend block");
};
dispatch_async(_serial_queue, myBlock);

使用函数

1
2
3
4
void myDispatchFunction(void *argument) {
...
}
dispatch_async_f(_serial_queue, (__bridge void *)[NSNumber numberWithInt:3], (dispatch_function_t)myDispatchFunction);

其他

1
2
dispatch_suspend(_serial_queue);  // 暂停
dispatch_resume(_serial_queue); // 唤醒

操作队列

创建调用操作

NSInvocationOperation 会给执行任务的类调用选择器

1
2
3
4
5
6
7
8
@implementation MyCustomClass
- (NSOperation *) operationWithData:(id) data {
return [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(myWorkerMethod:) object:data];
}
- (void) myWorkerMethod: (id)data {
NSLog(@"My Worker Method %@", data);
}
@end

创建代码块操作

1
2
3
4
5
6
NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
// work
}];
[blockOperation addExecutionBlock:^{
// work
}];

向队列中添加操作

NSOperationQueue 一般会并发执行。并且具有相关性,如果此操作是在别的操作的基础之上进行的,那么,这两个操作会相应地执行

  1. 可以通过设置最大并发数为1来确保操作是连续执行的
1
2
3
4
5
6
7
8
9
NSOperationQueue *currentQueue = [NSOperationQueue currentQueue];  // 当前队列
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; // 主队列
NSOperationQueue *_operationQueue = [[NSOperationQueue alloc] init]; // 自建队列

[theQueue addOperation:blockOperation]; // 添加操作
[theQueue addOperationWithBlock:^{
NSLog(@"My Block");
}];

文件加载与保存

属性列表

用来放置一些 Cocoa 可以处理(主要是存储到文件和从文件中加载)的对象

属性列表类

  • NSArray
  • NSDictionary
  • NSString
  • NSNumber
  • NSDate
  • NSData
  • 上述类的可修改形态(Mutable)

NSDate

用于处理时间和日期的基础类

1
2
NSDate *date = [NSdate date];  // 获取当前的时间
NSLog(@"today is %@", date);
1
2
3
// 与当前时间相隔一段时差的日期
NSDate *yesterday = [NSDate dateWithTimeIntervalSinceNow: -(24 * 60 * 60)];
NSLog(@"yesterday is %@", yesterday);
  1. +dateWithTimeIntervalSinceNow 接收一个 NSTimeInterval 类型的参数
  2. 参数是一个双精度值
  3. 以秒为单位
  4. 正数表示未来,负数表示过去

NSData

  1. 包含大量的字节
  2. 可以获取数据的长度
  3. 可以获取指向字节起始位置的指针
  4. 不可变更。如果需要变更,使用 NSMutableData 即可
1
2
3
4
const char *string = "Hi there, this is a C string!";
NSData *data = [NSData dataWithBytes:string length:strlen(string) + 1];
NSLog(@"data is %@", data);
NSLog(@"%d byte string is '%s'", [data length], [data bytes]);
  1. length 之所以加1是为了将c语言末尾的 ‘/0’ 包含进去
  2. -length 方法返回字节数量
  3. -bytes 返回字符串起始位置的指针

写入和读取属性列表

  1. 集合型属性列表类( NSArray 和 NSDictionary )具有一个 -writeToFile:atomically 方法
    • 将属性列表的内容写入文件
  2. NSString 和 NSData 也具有该方法,但是只能写出字符串或数据块
1
2
[[NSArray arrayWithObjects:@"I", @"seem", @"to", @"be", @"a", @"verb", nil] writeToFile:@"./tmp/verbiage.txt" atomically:YES];
NSLog(@"%@", [NSArray arrayWithContentsOfFile:@"./tmp/verbiage.txt"]);
  1. atomically 表示是否将文件内容暂存到临时文件中。如果在保存过程中出现意外,那么不会破坏源文件。但是代价是会使用双倍的磁盘空间
  2. 缺点是不会返回任何错误信息

修改对象类型

  1. 遍历集合并修改对象类型保存
  2. 使用 NSPropertyListSerialization
1
2
3
// 预设了很多行为选项
propertyListFromData:mutabilityOption:format:errorDescription:
// 返回 plist 并且在出现异常的时候会提供错误信息

编码对象

将对象转换成某种格式并保存到磁盘中的机制

  1. 对象可以将它们的实例变量和其他数据编码为数据块,然后保存到磁盘中
  2. 这些数据块以后还可以重新读回内存,并且基于保存的数据创建新对象
  3. 过程叫做编码和解码,或者序列化和反序列化
1
2
3
4
@protocol NSCoding
- (void) encodeWithCoder: (NSCoder *) encoder;
- (id) initWithCoder: (NSCoder *) decoder;
@end
  1. 通过 encodeWithCoder 序列化自身
  2. 通过 initWithCoder 加载自身
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void) encoderWithCoder: (NSCoder *) coder {
[coder encodeObject: name forKey: @"name"];
[coder encodeInt: magicNumber forKey: @"magicNumber"];
[coder encodeFloat: shoeSize forKey: @"shoeSize"];
[coder encodeObject: subThingies forKey: @"subThingies"];
}

- (id) initWithCoder: (NSCoder *) decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey: @"name"];
self.magicNumber = [decoder decodeIntForKey: @"magicNumber"];
self.shoeSize = [decoder decodeFloatForKey: @"shoeSize"];
self.subThingies = [decoder decodeObjectForKey: @"subThingies"];
}
}

  1. 是否使用 super init 取决于父类是否采用了 NSCoding 协议。
    • 没有:super init
    • 有:super initWithCoder
  2. 数组被编码时,里面的所有对象均会被编码
  3. 会智能地解决循环问题——数组中包含指向该数组的指针变量

键/值编码

一种间接更改对象状态的方式

KVC 简介

  1. valueForKey: 方法
    • 给对象发送消息并传递属性名称参数,获取其属性
    • NSString *name = [car valueForKey:@"name"];
    • 先去寻找与参数名称相同的 getter 方法,没找到会接着寻找与参数名称相同的实例变量
    • 通过元数据打开对象进入其中寻找所需要的信息,无需通过对象指针来访问实例变量
    • 会自动装箱和拆箱
  2. setValue:forKey: 方法
    • 设置对象实例变量的值
    • 第一个参数必须是对象指针
    • 其余与 valueForKey: 中的说明相同

键路径

键/值编码还支持指定路径,就像文件系统路径一样,可以遵循一系列关系来指定该路径

  1. 假设类 Engine 的实例变量 horsepower 没有设置为属性,也没有提供 getter 方法 和 setter 方法
1
2
[car setValue: [NSNumber numberWithInt:10] forKey: @"engine.horsepower"];
NSLog(@"horsepower is %@", [car valueforKey: @"engine.horsepower"]);

整体操作

如果使用某个键值来访问一个 NSSrray 数组,它实际上会查询响应数组中的每一个对象,然后将查询结果打包到另一个数组中返回

1
2
3
4
NSArray *presures = [car valueForKey:@"tires.presure"];
NSLog(@"presures %@", presures);

// 返回到是一个 presure 变量封装称 NSNumber 对象所构成的数组
  1. 不能在键路径索引数组,比如:tires[0].presure

快速运算

可以引用一些运算符来进行一些运算

1
2
NSNumber *count;
count = [garage valueForKeyPath: @"cars.@count"];
  1. cars 用来获取 garage 对象的属性
  2. @count 用来告诉 KVC 机制计算键路径上的左侧的对象总数
  3. @sum 计算总和
  4. @avg 计算平均值
  5. @min 计算最小值
  6. @max 计算最大值
  7. @distinctUnionOfObjects 获取左侧集合对应右侧属性的集合,这个集合中没有重复的元素

不要滥用 KVC

  1. KVC 需要解析字符串,速度比较慢
  2. 无法进行错误检查,可能会出现键路径错误

批处理

dictionaryWithValuesForKeys:

传入的字符串数组中的每一个字符串作为 key ,对象中每一个属性名为 key 的 属性名的值作为 value ,然后凭此生成一个字典

1
2
3
car = [[garage valueForKeyPath:@"cars"] lastObject];
NSArray *keys = [NSArray arrayWithObjects: @"make", @"model", @"modelYear", nil];
NSDictionary *carValues = [car dictionaryWithValuesForKeys: keys];

setValuesForKeysWithDictionary:

获取字典值并根据 key 设置对象与 key 对应的的属性的值

1
2
3
4
5
6
7
8
NSDictionary *newValues = [
NSDictionary dictionaryWithObjectsAndKeys:
@"Chevy", @"make",
@"Nova", @"model",
[NSNumber numberWithInt: 1964], @"modelYear",
mil
];
[car setValuesForKeysWithDictionary: newValues];

nil 仍然可用

重写 setNilValueForKey: 方法以提供逻辑上有意义的任何值

1
2
3
4
5
6
7
- (void) setNilValueForKey: (NSString *) key {
if ([key isEqualToString: @"mileage"]) {
mileage = 0;
} else {
[super setNilValueForKey: key];
}
}

处理未定义的键

  1. 通过重写 valueForUndefinedKey: 方法来处理未定义的键
  2. 如果要更改未知键的值,可以使用 setValue:forUndefinedKey: 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@interface Garage: NSObject
{
NSString *name;
NSMutableArray *cars;
NSMutableDictionary *stuff;
}

- (void) setValue: (id) value forUndefinedKey: (NSString *) key {
if (stuff == nil) {
stuff = [[NSMutableDictionary alloc] init];
}
[stuff setValue:value forKey:key];
}

- (id) valueForUndefinedKey: (NSString *) key {
id value = [stuff valueForKey: key];
return value;
}

使用静态分析器

无需运行程序便可以从逻辑上检测代码的工具

可以分析出的错误

  1. 安全问题,内存泄漏、缓冲区溢出等
  2. 并发性问题
  3. 逻辑问题

缺点

  1. 需要耗费时间进行分析,会拖慢构建程序设计的过程
  2. 有时会误报错误
  3. 改变了熟悉的工作流程

NSPredicate

用于指定过滤器的条件

创建谓词

  1. 创建许多对象,并将其组合起来
  2. 查询代码中的字符串
1
NSPredicate *predicate = [NSPredicate predicateWithFormat: @"name == 'Herbie'"];
  1. 谓词字符串左侧的 name 是键路径
  2. 谓词字符串右侧的 'Herbie' 是字符串字面量

计算谓词

1
2
BOOL match = [predicate evaluateWithObject: car];
NSLog(@"%s", (match) ? "YES" : "NO");
  1. evaluateWithObjectt: 计算对象是否符合谓词

数组过滤器

1
2
NSArray *array = [cars filteredArrayUsingPredicate: predicate];
NSLog(@"%@", array);
  1. filteredArrayUsingPredicate: 是 NSArray 数组中的一种类别方法,会循环过滤数组内容,根据谓词计算每一个对象的值,并将值为 YES 的对象累计到将被返回的数组中
  2. NSMutableArray 可以使用 filteredUsingPredicate 方法

格式说明符

方式一

1
2
predicate = [NSPredicate predicateWithFormat: @"engine.horsepower > %d", 50];
predicate = [NSPredicate predicateWithFormat: @"%K == %@", @"name", @"Herbie"];

方式二

1
2
3
NSPredicate *predicateTemplate = [NSPredicate predicateWithFormat: @"name == $NAME"];
NSDictionary *varDict = [NSDictionary dictionaryWithObjctsAndKeys: @"Herbie", @"name", nil];
NSPredicate *predicate = [predicateTemplate predicateWithSubstitutionVariables: varDict];
  1. 不能使用 $变量名 来作为键路径
  2. 谓词机制不会进行静态类型检查

运算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 比较和逻辑运算符
> >= < <= !=
and or not // 不区分大小写
&& || !
// 数组运算符
between {}
in {}
// self 表示用于响应谓词计算的对象
self.属性名
self
// 字符串运算法
// 接收 [cd] 修饰符 表示 是否区分大小写和发音符
beginswith // 以。。。开头
endswith // 以。。。结尾
contains // 包含。。。
// like
like ‘正则表达式’
曲终