Objective-C 基础入门

Objective-C 基础入门

七月 20, 2021

前言:

写React Native原生和Canvas底层代码写的头疼,所以打算先简单的学一下Objective- C的语法。


Objective-C:C的超集

Objective-C是C语言的严格超集,任何C语言程序不经修改就可以直接通过Objective-C编译器,在Objective- C中使用C语言代码是完全合法的。Objective- C的原意就是在C语言的主体上加入面向对象的特性。

文件扩展名与导入

扩展名 内容类型
.h 头文件:包含类,类型,函数和常数的声明
.m 源代码文件:可以包含Objective- C和C代码
.mm 源代码文件:除了可以包含Objective- C和C代码外还可以包含C++代码

当需要在源代码文件中包含头文件的时候,你可以使用标准的#include编译选项,但是Objective- C提供了更好的方法。#import#include完全相同,但是#import可以在确保相同的文件只会被包含一次,所以在Objective- C中,更倾向于使用#import

语法

Objective- C的面向对象语法源于Smalltalk消息传递风格。在所有其他非面向对象的语法上,包括变量类型,预处理器(preprocessing),流程控制,函数声明与调用皆与C语言完全一致。第一个Objective- C程序:

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

int main(int argc, char *argv[]) {
@autoreleasepool {
NSLog(@"Hello World!");
}

return 0;
}

消息传递

Objective- C最大的特色是承自于Smalltalk的消息传递模型(Message passing),此机制与今日C++式的主流风格差异很大。在Objective- C中,与其说对象互相调用方法,不如说对象之间互相传递消息。C++里类别与方法的关系严格清楚,一个方法必定属于一个类别,而且在编译时就已经绑定,不可能调用一个不存在类别里的方法。但在Objective-C中,类别与消息的关系比较松散,调用方法被视为对对象发送消息,所有方法都被视为对消息的回应。也就是说,一个类别不保证一定会回应收到的消息,如果类别收到了一个无法处理的消息,程序只会抛出异常,并不会出错崩溃。

C++调用方法的形式:

1
obj.method(argument);

Objective-C则写成:

1
[obj method: argument];

Objective- C天生既具备鸭子类型的动态绑定能力,因为运行期间才处理消息,允许发送未知消息给对象。可以发送消息给整个对象集合而不需要一一检查每个对象的类型,也具备消息转送机制。同时空对象nil接收消息后默认不做事,所以发消息给nil也不用担心程序崩溃。

基本数据类型

在阐述基本数据类型之前,首先要简单了解一下Cocoa框架,Cocoa框架是iOS应用程序的基础,是OS X和iOS操作系统的程序的运行环境,其包含了两个最重要的基本框架Foundation和UIKit,分别负责与界面无关的部分和界面相关的部分。

int 与 NSInteger:

C语言中的int类型,Objective- C同样支持,但是并不建议使用,推荐使用Cocoa框架中的NSInteger。NSInteger在64位内核中位long类型,在32位内核中位int类型,因此使用NSInteger,就无需特意考虑内核位宽的问题。

bool 与 BOOL:

C语言标准中没有布尔类型变量,C++中的bool类型,为true和false,这与大多数类C语言是相同的。在Objective- C中,你可以使用bool类型,但是更建议使用Objective-C专用的BOOL类型,这个布尔类型的值为YES和NO。

float 与 CGFloat:

CGFloat不是Foundation框架的基础变量,而是定义在UIKit框架中,CG代表CoreGraphi(核心绘图框架)。CGFloat根NSInteger一样,根据系统内核的位宽不同,类型也不同。在64位中CGFloat是double类型,在32位中是float类型。

NSString:

在Objective中char和string类型均可使用,但基本不用。NSString严格来说是类,并不是基本类型。但是Cocoa框架高度优化了NSString类,让其在实例化操作时就像基本类型一样,例如:

1
2
3
4
5
6
// 这里@表明后续字符串是一个 NNString类实例
NSString* textA = @"123";
NSString* textB = textA;
textA = @"456";
NSLog(@"%@", textA); // 456
NSLog(@"%@", textB); // 123

按照传统的引用类型分析,textB的指针指向textA后,当textA发生改变时,textB也应该发生改变。但是对于NSS tring类型来说,等号赋值,实际上是执行深拷贝。在text = @"456"时,textA的指针已经改变,同理textB = textA,textB的指针也已经改变了。所以为什么说NSString在操作时与基本类型十分相似。

此外NSString类,支持与众多基本类型之间的相互转换。例如NSString转换为数字:

1
2
3
NSString* number = @"1111111";
NSInteger inValue = [number integerValue];
CFFloat floatValue = [number doubleVlue];

NSValue:

NSValue是个可以和各种基本数据类型互相转换的类。包裹CGPpoint, CGRect,CGSize等等。例如

1
2
[NSValue valueWithCGSize:CGSizeMake(100, 1000)];
[NSValue valueWithRand:NSMakeRange(0, 10)];

NSNumber:

NSNumber不属于基本类型,而是对象。NSNumber继承自NSValue,而NSValue继承自NSObject。NSNumber支持和基本数据类型的相互转换,并且与NSString一样支持@简写。

1
2
3
4
5
6
7
NSNumber* number = @(123);
NSNumber* number1 = @(3.1415);
NSNumber* number2 = @(YES);

NSInteger intValue = [number integerValue];
CGFloat floatValue = [number1 doubleValue];
BOOL boolValue = [number2 boolValue];

容器

Objective- C提供三种容器:数组,字典和集合,三个容器中存储的都是对象类型。而可变容器类对象是不可变容器类对象的子类,在继承父类功能的基础上,扩充了对原有对象的增删改操作。

数组与可变数组

数组(NSArray):

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
// 初始化,注意initWithObject有无S(单个赋值,多个赋值)
NSArray *names = [[NSArray alloc] initWithObjects:@"科比", @"麦迪"];

// 数组复制
NSArray *namecopy [NSArray arrayWithArray: names];

// 便利构造器(快速构造)
NSArray *name = [NSArray arrayWithObjects:@"科比",@"麦迪"];

// 字面量构建(语法糖)
NSArray *name = @[@"科比", @"麦迪"];

// 数组数量(下标)
[name count];

// 数组读取
[name objectAtIndex: 2];
name[2];

// 包含判断
- (BOOL)containsObject:(id)anObject;
[name containsObject:@"科比"];

// 数组索引
- (NSUInteger)indexOfOject:(id)anObject;
[name indexOfObject:@"科比"];

// 数组分割
- (NSArray *)componentsSeparatedByString:(NSString *)separator
NSString *str = @"Imsh.memeda.piapiaia";
NSArray *cut = [str componentsSeparatedByString:@"."];

// 数组拼接
- (NSString *)componentsJoinedByString:(NSString *)component
NSString *string = [cut componentsJoinedByString:"&"];

可变数组(NSMutableArray):

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
// 初始化, initWithCapacity后的数字为预估数组长度,实际可超出
NSMutableArray *name1 = [[NSMutableArray alloc] initWithCapacity: 0];

// 便利构造器
NSMutableArray *name2 = [NSMutableArray initWithCapacity:0];

// 字面量
NSMutableArray *name3 = [@[@"frank", @"duck"] mutableCopy];

// 添加对象
[name1 addObject:@"cow"];

// 数组合并
[name2 arrayByAddingObjectsFromArray:name1];

// 在指定下标处插入对象
[name2 insertObjet:@"kG" atIndex:1];

// 移除指定对象
[name1 removeObject:@"KG"];

// 移除数组最后一个对象
[name1 removeLastObject];

// 移除所有对象
[name1 removeAllObject];

// 移除指定位置对象
[name1 removeObjectAtIndex:1];

// 替换指定位置对象
[name1 replaceObjectAtIndex:1 withObject:@"IMCOOL"];

// 交换对应下标对象的位置
[name exchangeObjectAtIndex:0 withObjectAtIndex:1];

字典与可变字典

字典(NSDictionary):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/* 初始化,先value后key,字典的键值只能是对象。但检测到value或key,系统判断字典插入完毕,所以需要注意作为值的对象是否可能为nil,导致字典插入提前结束 */
NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndkeys:@"value1", @'key1', @"value2", @"key2"];

// 便利构造器
NSDictionary *dic2 = [NSDictionary dictionaryWithObjectsAndKeys:@"value1", @"key1", @"value2", @"key2"];

// 字面量
NSDictionary *dic3 = @{@"key1": @"value1", @"key2": @"value2"};

// 获得所有的Key和Value
NSArray *keyarray = [dic allKeys];
NSArray *valuearry = [dic allValues];

// 取值
NSLog(@"%@", [dic objectForKey:@"key1"]);

可变字典(NSMutableDictionary):

1
2
3
4
5
6
NSMutableDictionary *test = [NSMutableDiction dictionary];
[test setObject:@"value1" forKey: @"key1"];

// 移除
[test removeObjectForKey: @"key1"];
[test removeAllObjects];

集合与可变集合

集合(NSSet):

1
2
3
4
5
6
7
8
9
10
11
12
13
// 集合经常用来处理重用问题,
NSSet *set = [[NSSet alloc] initWithObjects:@"value1", @"value2", nil];

// 便利构造器,区分s
NSSet *set2 = [NSSet setWithObjects:@"value1", @"value2", nil];

// 随机取值,没什么作用
[set2 anyObject];

// 判断集合中是否包含指定对象
[set containsObject:@"value1"];

// 注意:不可变集合一旦创建,集合中的对象无法修改,只能从集合中读取对象,并且没有快速创建集合对象的字面量。

可变集合(NSMutableSet):

1
2
3
4
5
6
NSMutableSet *muset = [[NSMutableSet alloc] initWithCapacity: 0];
NSMutableSet *muset2 = [NSMutableSet setWithObjects:0];

[muset addObject:@"value1"];
[muset removeObject:@"value1"];
[muset removeAllObjects];

如同所有其他的面向对象语言,类是Objective-C用来封装数据,以及操作数据行为的基础结构。对象就是类子啊运行期间的实例,它包含了类声明实例变量的内存拷贝,以及类成员的指针。Objective-C的类规格说明包含两个部分:定义(interface)与实现(implementation)。定义部分包含了类声明和实例变量的定义,以及类相关的方法。实现部分包含对实例变量的补充和类方法的实际代码。

Interface

定义部分,清楚的定义了类的名称,所继承的父类,数据成员和方法。以关键字@interface 开始,@end作为结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MyObject: NSObject {
@private // 作用范围自身类
NSInteger age;
@protected // 默认,作用范围自身类和子类
id job; /* id类型可以转换为任何数据类型,实际上为一个指向对象的指针,可以指向任何数据类型的对象,当无法明 确数据类型时,可以使用id类型 */
@public // 全部范围
NSString* name;
}
+(return type) class_method; // 类方法,及[NSSet alloc],alloc即为NSSet类的类方法

- (return type) instance_method1; // 实例方法
- (return type) instance_method2: (int)p1 andPar:(int)p2;

@end

Implementation

实现区块包含了公开方法的实现,以及定义私有变量。以关键字@implementation作为区块起头,@end结尾

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
#import "Header.h"

@implementation Human {
//在Implementation区块也可以定义实体变量作为补充,但是这里定义的实体变量默认为private
NSInger member;
}

// 重写NSObject类的init方法,自定义初始化过程,设置实体变量默认值
-(id)init {
if(self = [super init]) { // 防止父类初始化失败,失败后会返回nil
age=20;
name=@"holy";
}
return self;
}

// 自定义可以接受参数进行初始化的函数
-(id)initWithArguments:(int)a Name:(NSString*)n {
if(self = [super init]) {
age = a
[name release]; /* 这步操作设计OC的内存管理,我简单理解为释放name内存,使得下一步name可以指向一个新 的内存空间 */
name = [n copy]; // 重新赋值
}
return self;
}

-(NSSInteger)age {
return age;
}

@end

创建对象

Objective- C创建对象需要通过alloc以及init两个消息。alloc的作用时分配内存,init则是初始化对象。init和alloc都定义在NSObject里。

1
2
3
4
5
Human* man = [[Human alloc] init]; //这里将会调用我们重写的init方法,进行初始化
Human* woman = [[Human alloc] initWithArguments:1 Name:@"Amy"];

// 在Objective-C 2.0中,若创建对象不需要参数,可以直接使用new,效果与调用init一致,只是为类语法上的精简
Human* my = [Human new];

方法

Objective-C中的类可以声明两种类型的方法:实例方法和类方法。方法的声明包含方法类型标识符、返回类型、一个或多个方法标识关键字、参数类型和形参。

其中insertObject和atIndex都是方法标识关键字,多个关键字构成一个方法。

为了避免声明过多的本地变量保存临时结果,Objective-C允许使用嵌套消息。每个嵌套消息的返回值可以作为其他消息的参数或者目标

1
[[myObject getArray] insertObject:[myObject getObjectToInsert] atindex: 0];

属性

属性是用来代替声明存取方法的便捷方式。属性仅仅是快速定义访问实例变量的方式。通过使用属性,可以不再声明实例变量的getter和setter方法。当属性所对应的实例变量不存在时,会自动创建一个下划线+属性名的实例变量。

属性声明在类定义的方法声明处进行。基本的使用为适应@property编译选项,紧跟着属性特性、属性类型和属性的名字。

特性有三种类型,最多可以写6个特性,每种类型都有默认特性值,如果不写特性系统就会采用默认值

特性的三种类型:

  • Atomicity(原子性):
    • atomic(默认值):保证线程安全,给getter和setter加锁,当有线程访问setter时,其他线程会阻塞等待。但是同时访问并不能确保访问的值获取准确,可能会被其他线程修改。
    • nonatomic:可以减少性能消耗,尤其在atomic无法保证正确获值的情况下,基本使用nonatomic
  • Access(存取特性):
    • readwrite(默认值)
    • readonly
  • Storage(内存管理特性):
    • strong(对象类型属性的默认值):表示持有这个对象,负责保持这个对象的生命周期
    • week:保持对对象的引用,但是不主张所有权,当对象被销毁时,所有用week引用的属性都会被置为nil
    • copy:深拷贝对象,不可以用copy声明可变类型的属性
    • assign:与weak类似,但是当所指对象被销毁时,这些指针不会被设置为nil,而是没有指向,当再被访问时,程序将会出现crash

下面是属性声明的例子:

1
2
3
4
5
6
7
8
9
@interface Person: NSObject {
@public NSString* name;
@private int age;
}

@property(copy) NSString* name;
@property(readonly) int age;

@end

属性可以使用消息传递,点表达式、valueForKey和->的形式来访问

1
2
3
4
5
6
7
8
Person* aPerson = [[Person alloc] init];
aPerson.name = @"Steve";
[aPerson setValue:@"Steve" forKey:@"name"];

[aPerson name];
aPerson.name;
[aPerson valueForKey:@"name"];
aPerson->name;

类或协议的属性可以被动态获得。

快速枚举

比起利用NSEnumerator对象在集合中依次枚举,Objective- C 2.0提供了快速枚举的语法。在Objective- C 2.0中,以下循环的功能是相等的,但性能特性不同。

1
2
3
4
5
6
7
// 使用NSEnumerator
NSEnumerator *enumerator = [thePeople objectEnumerator];
Person *p;

while((p = [enumerator nextObject]) != nil) {
NSLog(@"%@ is %i yers old.", [p name], [p age]);
}
1
2
3
4
5
// 使用依次枚举
for(int i=0; i < [thePeople count]; i++){
Person *p = [thePeople objectAtIndex:i];
NSLog(@"%@ is %i years old.", [p name], [p age]);
}

协议

协议是一组没有实现的方法列表,任何的类均可采纳协议并具体实现这组方法。协议的声明不提供实现,它只是简单的表明匹配该协议的类实现了该协议的方法,保证调用端可以安全调用方法。

协议以关键字@protocol作为区块起始,@end结束,中间为方法列表。

1
2
3
4
5
6
7
8
@protocol Locking
- (void)lock;
- (void)unlock;
@end

// 采用协议
@interface SomeClass: SomeSuperClass <Locking>
@end

动态类型

Objective- C具备动态类型:即消息可以发送给任何对象实体,无论该对象实体的公开接口中有没有对应的方法。一个消息收得到消息后,它有三种处理消息的可能手段,第一是回应消息并运行方法。若无法回应,则可以转发消息给其他对象。若以上两种均无,就要处理无法回应导致的意外情况。

虽然Objective-C具备动态类型的能力,但编译期间的静态类型检查依旧可以应用到变量上。以下三种声明运行时的结果是完全相同的,但是三种声明提供了一个比一个更明显的类型信息。附加的类型信息让编译器在编译时可以检查变量类型,并对类型不符的变量提出警告。下面三个方法,差异仅在于参数的形态:

1
2
3
4
5
6
7
8
// foo可以时任何类的实例
- setMyValue:(id) foo;

// foo可以是任何类,但必须采用aProtocol协议
- setMyValue:(id <aProtocol>) foo;

// foo必须是NSNumber的实例
- setMyValue:(NSNumber* ) foo;