【编程语言】从0到1:Objective-C学习之旅开启

目录

一、Objective-C 是什么?

二、学习前的准备

三、基础语法详解

(一)数据类型

(二)变量与常量

(三)运算符

(四)流程控制语句

四、面向对象编程

(一)类与对象

(二)属性与方法

(三)继承与多态

五、内存管理

(一)ARC 机制

(二)手动内存管理(了解)

六、进阶特性

(一)分类(Category)

(二)协议(Protocol)

(三)块(Block)

七、学习资源推荐

八、总结与展望


一、Objective-C 是什么?

Objective-C 是一种高级编程语言,它以 C 语言为基础,是 C 语言的超集 ,并加入了 Smalltalk 式的消息传递机制,从而具备面向对象的特性。这种独特的设计让 Objective-C 既有 C 语言的高效和灵活性,能够直接访问内存,进行底层操作;又拥有面向对象编程的强大功能,比如封装、继承和多态,使代码的组织和维护更加容易。

在编程语言的发展历程中,Objective-C 诞生于 20 世纪 80 年代,由 Brad Cox 和 Tom Love 开发。它的出现,为软件开发带来了新的思路和方法,特别是在苹果生态系统中,Objective-C 扮演着举足轻重的角色。

在 iOS 和 Mac 开发领域,Objective-C 堪称中流砥柱。在 iPhone 应用开发的早期阶段,Objective-C 是开发者的唯一选择,众多知名的 iOS 应用,如早期版本的微信、支付宝等,都是用 Objective-C 编写而成。这些应用充分利用了 Objective-C 与苹果操作系统的紧密集成,能够调用系统提供的各种 API,实现丰富的功能和流畅的用户体验。例如,通过 Objective-C 可以轻松访问 iOS 设备的相机、相册、GPS 等硬件功能,为用户带来便捷的使用感受。

除了 iOS 应用开发,在 Mac 应用开发中,Objective-C 同样发挥着重要作用。许多 Mac 系统上的专业软件,如 Adobe 系列软件的 Mac 版本,在开发过程中都大量使用了 Objective-C。它能够与 Mac 操作系统的 Cocoa 框架完美配合,打造出界面精美、功能强大的桌面应用程序。

二、学习前的准备

在开启 Objective-C 学习之旅前,我们需要准备好开发环境。对于 Objective-C 开发来说,最主要的开发工具就是 Xcode,它是苹果公司开发的一款集成开发环境(IDE),集代码编辑、编译、调试和界面设计等功能于一体,是 iOS 和 macOS 开发的首选工具 。

安装 Xcode 很简单,你可以在 Mac App Store 中搜索 “Xcode”,然后点击下载安装即可。由于 Xcode 的安装包较大,下载和安装过程可能需要一些时间,请耐心等待。

安装完成后,首次打开 Xcode 时,系统可能会提示你安装一些额外的组件,如命令行工具等,按照提示进行安装就好。安装完成后,我们还需要对 Xcode 进行一些基本设置,以适应我们的编程习惯。

比如设置代码缩进,打开 Xcode,点击菜单栏中的 “Xcode”,选择 “Preferences”,在弹出的窗口中选择 “Text Editing” 选项卡,在 “Indentation” 部分,你可以选择使用制表符(Tab)或者空格进行缩进,还能设置缩进的宽度 。一般来说,为了代码的可读性和一致性,建议使用 4 个空格作为缩进单位。

再比如设置字体,同样在 “Preferences” 窗口中,选择 “Fonts & Colors” 选项卡,在这里你可以选择喜欢的字体和字号,让代码看起来更加舒适。Xcode 默认的字体是 Menlo,你也可以根据自己的喜好换成 Consolas、Monaco 等其他等宽字体。

三、基础语法详解

(一)数据类型

Objective-C 的数据类型丰富多样,包含了基本数据类型、对象类型和集合类型等。其中,基本数据类型与 C 语言有相似之处,但也存在一些差异 。

NSInteger 和 NSUInteger 是 Objective-C 中常用的整数类型,它们会根据系统架构自动适配为 32 位或 64 位。在 32 位系统中,NSInteger 等价于 int,而在 64 位系统中,它等价于 long。这种自动适配的特性,使得代码在不同的硬件平台上都能高效运行 。比如在计算数组的索引时,就可以使用 NSInteger 来确保代码的兼容性:


NSArray *array = @[@"apple", @"banana", @"cherry"];

NSInteger index = 1;

NSString *fruit = array[index];

CGFloat 则是用于表示浮点数的类型,同样会根据系统架构调整精度。在 iOS 开发中,常常使用 CGFloat 来处理坐标、尺寸等与图形相关的数据。例如,设置视图的宽度和高度时:


UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200.0f, 100.0f)];

CGFloat width = view.frame.size.width;

CGFloat height = view.frame.size.height;

BOOL 是 Objective-C 中的布尔类型,取值只有 YES 和 NO,分别对应 C 语言中的 1 和 0。在条件判断中,BOOL 类型非常常用,比如判断用户是否登录:


BOOL isLoggedIn = YES;

if (isLoggedIn) {

NSLog(@"用户已登录");

} else {

NSLog(@"用户未登录");

}

与 C 语言相比,Objective-C 还引入了强大的对象类型和集合类型 。NSString 用于表示字符串,它是不可变的,一旦创建,其内容就不能被修改。而 NSMutableString 是 NSString 的可变版本,可以方便地进行字符串的拼接、插入和删除等操作。比如:


NSString *str1 = @"Hello";

NSMutableString *str2 = [NSMutableString stringWithString:@"World"];

[str2 insertString:@" " atIndex:0];

[str2 appendString:str1];

NSArray 和 NSMutableArray 分别表示不可变数组和可变数组。不可变数组在创建后,其元素不能被修改、添加或删除;可变数组则可以动态地改变其内容。例如:


NSArray *immutableArray = @[@1, @2, @3];

NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:immutableArray];

[mutableArray addObject:@4];

NSDictionary 和 NSMutableDictionary 用于存储键值对,同样分为不可变和可变版本。在实际开发中,常常使用字典来存储和传递结构化的数据,比如用户信息:


NSDictionary *userInfo = @{@"name": @"John", @"age": @30, @"email": @"john@example.com"};

NSMutableDictionary *mutableUserInfo = [NSMutableDictionary dictionaryWithDictionary:userInfo];

[mutableUserInfo setObject:@"newemail@example.com" forKey:@"email"];

(二)变量与常量

在 Objective-C 中,变量的声明方式与 C 语言类似,但需要注意数据类型的选择。例如,声明一个整数变量:


NSInteger number = 10;

变量的赋值可以在声明时进行,也可以后续再进行。比如:


NSInteger anotherNumber;

anotherNumber = 20;

变量的作用域决定了它在程序中的可见性和生命周期 。局部变量在方法或代码块内部声明,其作用域仅限于该方法或代码块。当方法或代码块执行结束时,局部变量就会被销毁。例如:


- (void)exampleMethod {

NSInteger localVar = 5;

// 在这里可以访问localVar

{

NSInteger innerVar = 10;

// 在这里可以访问innerVar和localVar

}

// 这里无法访问innerVar,因为它的作用域已结束

}

全局变量在所有方法和类定义之外声明,其作用域是整个程序。在 Objective-C 中,通常使用 extern 关键字来声明全局变量。例如,在一个源文件中定义全局变量:


// 在文件顶部声明全局变量

NSInteger globalNumber = 100;

- (void)anotherMethod {

// 在这里可以访问globalNumber

NSLog(@"全局变量的值: %ld", (long)globalNumber);

}

常量是在程序运行过程中值不能被改变的量。在 Objective-C 中,可以使用 const 关键字来定义常量,也可以使用 #define 预处理指令来定义常量宏 。例如:


// 使用const定义常量

const CGFloat pi = 3.1415926;

// 使用#define定义常量宏

#define kMaxCount 100

常量的命名通常采用大写字母和下划线的组合,以提高代码的可读性和可维护性。比如 kMaxCount 这样的命名方式,能够清晰地表达常量的含义。

(三)运算符

Objective-C 中的运算符涵盖了算术运算符、比较运算符、逻辑运算符等多种类型,这些运算符的基本用法与 C 语言相似,但在具体的应用场景中,又有其独特之处 。

算术运算符用于执行基本的数学运算,如加法(+)、减法(-)、乘法(*)、除法(/)和取余(%) 。在进行整数除法时,需要注意结果会自动取整。例如:


NSInteger result = 5 / 2; // result的值为2

比较运算符用于比较两个值的大小或相等性,包括大于(>)、小于(<)、大于等于(>=)、小于等于(<=)、等于(==)和不等于(!=) 。比较运算符的结果是一个 BOOL 类型的值。例如:


BOOL isGreater = 10 > 5; // isGreater的值为YES

逻辑运算符用于组合多个条件,包括逻辑与(&&)、逻辑或(||)和逻辑非(!) 。逻辑运算符常用于条件判断语句中,以实现复杂的逻辑控制。例如:


BOOL condition1 = 5 > 3;

BOOL condition2 = 2 < 4;

BOOL combinedCondition = condition1 && condition2; // combinedCondition的值为YES

除了这些基本运算符外,Objective-C 还有一些特殊的运算符,如点运算符(.)和消息传递运算符([]) 。点运算符在 Objective-C 中主要用于访问对象的属性,它实际上是一种语法糖,编译器会将其转换为对应的存取方法调用。例如:


UIView *view = [[UIView alloc] init];

view.backgroundColor = [UIColor redColor]; // 等价于[view setBackgroundColor:[UIColor redColor]];

消息传递运算符([])则是 Objective-C 的核心特性之一,用于向对象发送消息,即调用对象的方法。消息传递是 Objective-C 实现面向对象编程的基础,它使得程序在运行时能够根据对象的实际类型来决定执行哪个方法,从而实现多态性 。例如:


NSString *str = @"Hello, Objective-C!";

NSInteger length = [str length]; // 调用NSString的length方法获取字符串长度

(四)流程控制语句

流程控制语句是编程语言的重要组成部分,它允许我们根据不同的条件和需求,控制程序的执行流程。Objective-C 提供了丰富的流程控制语句,包括 if – else、switch、for、while 等,这些语句在 iOS 和 macOS 开发中广泛应用,帮助我们实现各种复杂的业务逻辑 。

if – else 语句用于条件判断,根据条件的真假来决定执行不同的代码块。它的基本语法如下:


if (condition) {

// 条件为真时执行的代码

} else {

// 条件为假时执行的代码

}

在实际应用中,if – else 语句常常用于处理各种业务逻辑。比如,根据用户的登录状态显示不同的界面:


BOOL isLoggedIn = YES;

if (isLoggedIn) {

// 显示用户界面

NSLog(@"欢迎用户,显示用户界面");

} else {

// 显示登录界面

NSLog(@"请先登录,显示登录界面");

}

switch 语句用于多分支选择,根据一个表达式的值来选择执行不同的分支。它的基本语法如下:


switch (expression) {

case value1:

// 当expression等于value1时执行的代码

break;

case value2:

// 当expression等于value2时执行的代码

break;

default:

// 当expression不等于任何case值时执行的代码

break;

}

在开发中,switch 语句常用于处理枚举类型的值。比如,根据不同的星期枚举值,输出对应的星期名称:


typedef NS_ENUM(NSInteger, Weekday) {

WeekdayMonday = 1,

WeekdayTuesday,

WeekdayWednesday,

WeekdayThursday,

WeekdayFriday,

WeekdaySaturday,

WeekdaySunday

};

Weekday today = WeekdayWednesday;

switch (today) {

case WeekdayMonday:

NSLog(@"今天是星期一");

break;

case WeekdayTuesday:

NSLog(@"今天是星期二");

break;

case WeekdayWednesday:

NSLog(@"今天是星期三");

break;

case WeekdayThursday:

NSLog(@"今天是星期四");

break;

case WeekdayFriday:

NSLog(@"今天是星期五");

break;

case WeekdaySaturday:

NSLog(@"今天是星期六");

break;

case WeekdaySunday:

NSLog(@"今天是星期日");

break;

default:

break;

}

for 循环用于重复执行一段代码,它有明确的循环次数。for 循环的基本语法如下:


for (initialization; condition; increment) {

// 循环体代码

}

在 iOS 开发中,for 循环常用于遍历数组。比如,遍历一个存储学生成绩的数组,计算总分:


NSArray *scores = @[@85, @90, @78, @95];

NSInteger totalScore = 0;

for (NSUInteger i = 0; i < scores.count; i++) {

NSNumber *score = scores[i];

totalScore += [score integerValue];

}

NSLog(@"总分为: %ld", (long)totalScore);

while 循环也用于重复执行代码,但它是根据条件来判断是否继续循环,没有明确的循环次数 。while 循环的基本语法如下:


while (condition) {

// 循环体代码

}

在一些需要持续监测某个条件的场景中,while 循环非常有用。比如,监测网络连接状态,直到网络连接成功:


BOOL isConnected = NO;

while (!isConnected) {

// 尝试连接网络的代码

// 假设连接成功后将isConnected设置为YES

// 模拟网络连接成功

isConnected = YES;

}

NSLog(@"网络已连接");

do – while 循环与 while 循环类似,不同之处在于它会先执行一次循环体,然后再判断条件 。do – while 循环的基本语法如下:


do {

// 循环体代码

} while (condition);

比如,在一个游戏中,需要玩家至少进行一次操作,然后根据操作结果决定是否继续游戏,就可以使用 do – while 循环:


BOOL gameOver = NO;

do {

// 玩家进行操作的代码

// 根据操作结果决定是否结束游戏,假设操作失败时将gameOver设置为YES

// 模拟操作失败

gameOver = YES;

} while (!gameOver);

NSLog(@"游戏结束");

四、面向对象编程

(一)类与对象

在 Objective-C 中,类是对象的模板,它定义了对象的属性和行为。类的定义包括接口部分和实现部分 。

接口部分使用 @interface 关键字声明,用于定义类的属性和方法。例如,定义一个名为 Person 的类,包含姓名和年龄两个属性,以及一个打招呼的方法:


#import <Foundation/Foundation.h>

@interface Person : NSObject {

NSString *_name; // 实例变量,用于存储姓名,以下划线开头是一种常见的命名约定

NSInteger _age; // 实例变量,用于存储年龄

}

@property (nonatomic, copy) NSString *name; // 使用@property声明属性,nonatomic表示非原子性,copy表示在设置属性时进行拷贝操作

@property (nonatomic, assign) NSInteger age; // assign表示直接赋值,通常用于基本数据类型

- (void)sayHello; // 声明一个实例方法,用于打招呼

@end

在这个接口声明中,我们首先导入了 Foundation 框架的头文件,这是 Objective-C 开发中常用的基础框架,包含了许多基本的类和函数 。然后,使用 @interface 关键字声明了 Person 类,它继承自 NSObject 类,NSObject 是 Objective-C 中所有类的根类,提供了一些基本的方法和行为 。

在类的大括号内,我们声明了两个实例变量_name 和_age,用于存储对象的内部状态 。接着,使用 @property 声明了两个属性 name 和 age,这实际上是一种语法糖,编译器会自动生成对应的存取方法(getter 和 setter) 。最后,声明了一个实例方法 sayHello,用于对象执行打招呼的行为。

实现部分使用 @implementation 关键字,用于实现类的方法。Person 类的实现如下:


#import "Person.h"

@implementation Person

// 实现name属性的getter方法

- (NSString *)name {

return _name;

}

// 实现name属性的setter方法

- (void)setName:(NSString *)name {

if (_name != name) {

[_name release];

_name = [name copy];

}

}

// 实现age属性的getter方法

- (NSInteger)age {

return _age;

}

// 实现age属性的setter方法

- (void)setAge:(NSInteger)age {

_age = age;

}

// 实现sayHello方法

- (void)sayHello {

NSLog(@"大家好,我叫%@,今年%ld岁。", _name, (long)_age);

}

// 重写dealloc方法,用于释放对象占用的资源

- (void)dealloc {

[_name release];

[super dealloc];

}

@end

在实现文件中,我们首先导入了 Person 类的头文件,确保编译器能够识别类的声明 。然后,使用 @implementation 关键字实现 Person 类的方法。对于属性的存取方法,我们手动实现了它们,以展示其内部的实现机制 。在 setName 方法中,我们首先检查新值和旧值是否相同,如果不同,则先释放旧值,再拷贝新值,以确保内存管理的正确性 。在 dealloc 方法中,我们释放了_name 实例变量,避免内存泄漏 。最后,实现了 sayHello 方法,用于输出对象的信息。

对象的创建和使用非常简单。在需要使用 Person 类的地方,我们可以创建 Person 对象,并调用其方法和访问其属性:


#import <Foundation/Foundation.h>

#import "Person.h"

int main(int argc, const char * argv[]) {

@autoreleasepool {

Person *person = [[Person alloc] init]; // 创建Person对象

person.name = @"张三"; // 设置name属性

person.age = 25; // 设置age属性

[person sayHello]; // 调用sayHello方法

[person release]; // 释放对象,避免内存泄漏

}

return 0;

}

在 main 函数中,我们首先导入了 Foundation 框架和 Person 类的头文件 。然后,使用 [[Person alloc] init] 创建了一个 Person 对象,并将其赋值给 person 变量 。接着,通过点语法设置了 person 对象的 name 和 age 属性,这实际上是调用了对应的 setter 方法 。然后,调用 person 对象的 sayHello 方法,输出对象的信息 。最后,使用 [person release] 释放 person 对象,将其占用的内存归还给系统,避免内存泄漏 。在现代的 Objective-C 开发中,ARC(自动引用计数)机制已经广泛应用,它可以自动管理对象的内存,开发者无需手动调用 release 方法,但了解手动内存管理的原理仍然非常重要 。

(二)属性与方法

在 Objective-C 中,属性是类中用于存储数据的成员,它通过 @property 和 @synthesize 关键字来实现。@property 用于声明属性,告诉编译器自动生成属性的存取方法(getter 和 setter) 。例如:


@interface Car : NSObject {

NSString *_brand;

NSInteger _year;

}

@property (nonatomic, copy) NSString *brand;

@property (nonatomic, assign) NSInteger year;

@end

在这个例子中,我们声明了一个 Car 类,包含 brand 和 year 两个属性 。属性声明中的参数具有重要意义 。nonatomic 表示非原子性,在多线程环境下,它不会对属性的读写操作进行加锁,性能更高,但可能会出现数据竞争问题;如果不写这个参数,默认是 atomic,即原子性,会保证属性的读写操作是线程安全的,但会有一定的性能开销 。copy 表示在设置属性值时,会对传入的值进行拷贝,这样可以确保属性值的独立性,避免外部对属性值的修改影响到对象内部;对于不可变对象(如 NSString),通常使用 copy 来防止意外修改 。assign 用于基本数据类型(如 NSInteger),它直接将值赋给属性,简单高效 。

@synthesize 用于合成属性的存取方法的实现。在 Xcode 4.5 及以后的版本中,如果没有特殊需求,@synthesize 可以省略,编译器会自动为我们合成 。例如:


@implementation Car

@synthesize brand = _brand;

@synthesize year = _year;

@end

当我们省略 @synthesize 时,编译器会默认生成与属性同名的实例变量,并合成存取方法 。比如对于 brand 属性,编译器会生成类似这样的存取方法:


- (NSString *)brand {

return _brand;

}

- (void)setBrand:(NSString *)brand {

if (_brand != brand) {

[_brand release];

_brand = [brand copy];

}

}

对于 year 属性,生成的存取方法如下:


- (NSInteger)year {

return _year;

}

- (void)setYear:(NSInteger)year {

_year = year;

}

实例方法是通过对象调用的方法,用于实现对象的具体行为 。例如,在 Car 类中添加一个启动方法:


@interface Car : NSObject {

//...

}

//...

- (void)startEngine;

@end

@implementation Car

//...

- (void)startEngine {

NSLog(@"汽车引擎启动了");

}

@end

在使用时,我们可以创建 Car 对象并调用 startEngine 方法:


Car *myCar = [[Car alloc] init];

myCar.brand = @"宝马";

myCar.year = 2023;

[myCar startEngine];

[myCar release];

类方法是通过类名调用的方法,通常用于执行与类相关的操作,而不是与特定对象相关的操作 。在 Car 类中添加一个类方法,用于获取当前汽车的品牌数量:


@interface Car : NSObject {

//...

static NSInteger _brandCount;

}

//...

+ (NSInteger)brandCount;

@end

@implementation Car

//...

+ (NSInteger)brandCount {

return _brandCount;

}

@end

在使用时,我们可以直接通过类名调用 brandCount 方法:


NSInteger count = [Car brandCount];

方法的参数传递分为值传递和引用传递 。对于基本数据类型,通常是值传递,即传递的是参数的副本,方法内部对参数的修改不会影响到外部变量 。例如:


- (void)incrementNumber:(NSInteger)number {

number++;

NSLog(@"方法内部的number: %ld", (long)number);

}

调用这个方法时:


NSInteger num = 10;

[myCar incrementNumber:num];

NSLog(@"方法外部的num: %ld", (long)num);

输出结果会是方法内部的 number: 11,方法外部的 num: 10 。

对于对象类型,传递的是对象的指针,本质上还是值传递,但通过指针可以修改对象的内部状态 。例如:


- (void)changeCarBrand:(Car *)car newBrand:(NSString *)newBrand {

car.brand = newBrand;

NSLog(@"方法内部的car品牌: %@", car.brand);

}

调用这个方法时:


Car *myCar = [[Car alloc] init];

myCar.brand = @"奔驰";

[myCar changeCarBrand:myCar newBrand:@"奥迪"];

NSLog(@"方法外部的myCar品牌: %@", myCar.brand);

输出结果会是方法内部的 car 品牌:奥迪,方法外部的 myCar 品牌:奥迪 ,因为通过对象指针修改了对象的内部属性 。

(三)继承与多态

继承是面向对象编程的重要特性之一,它允许一个子类继承父类的属性和方法,并可以在此基础上进行扩展和修改 。在 Objective-C 中,继承通过在子类声明时指定父类来实现 。例如,定义一个父类 Animal,包含一个叫声的方法:


#import <Foundation/Foundation.h>

@interface Animal : NSObject

- (void)makeSound;

@end

@implementation Animal

- (void)makeSound {

NSLog(@"动物发出声音");

}

@end

然后,定义一个子类 Dog,继承自 Animal,并扩展自己的属性和方法:


#import "Animal.h"

@interface Dog : Animal {

NSString *_name;

}

@property (nonatomic, copy) NSString *name;

- (void)run;

@end

@implementation Dog

@synthesize name = _name;

- (void)makeSound {

NSLog(@"汪汪汪");

}

- (void)run {

NSLog(@"%@在奔跑", _name);

}

@end

在这个例子中,Dog 类继承自 Animal 类,它自动拥有了 Animal 类的 makeSound 方法 。同时,Dog 类还定义了自己的属性_name 和方法 run 。并且,Dog 类重写了父类 Animal 的 makeSound 方法,提供了自己的实现,这体现了多态性 。

多态是指同一个行为具有不同的表现形式 。在 Objective-C 中,多态通过继承和方法重写来实现 。当我们使用父类指针指向子类对象时,调用相同的方法会根据对象的实际类型执行不同的实现 。例如:


#import <Foundation/Foundation.h>

#import "Animal.h"

#import "Dog.h"

int main(int argc, const char * argv[]) {

@autoreleasepool {

Animal *animal1 = [[Animal alloc] init];

Animal *animal2 = [[Dog alloc] init];

[animal1 makeSound];

[animal2 makeSound];

[animal1 release];

[animal2 release];

}

return 0;

}

在这个例子中,animal1 是 Animal 类的对象,animal2 是 Dog 类的对象,但它们都被声明为 Animal 类型的指针 。当调用 makeSound 方法时,animal1 会执行 Animal 类中的 makeSound 方法,输出 “动物发出声音”;animal2 会执行 Dog 类中重写后的 makeSound 方法,输出 “汪汪汪” 。这就是多态的体现,同一个方法调用,根据对象的实际类型产生不同的行为 。

多态在实际开发中有着广泛的应用场景 。比如在一个游戏开发中,可能有各种不同的角色类,它们都继承自一个基类 Character 。基类中定义了一些通用的方法,如移动、攻击等 。不同的子类(如战士、法师、刺客等)可以重写这些方法,实现各自独特的移动和攻击方式 。在游戏的主循环中,我们可以使用一个 Character 类型的数组来存储所有的角色对象,然后通过遍历数组调用每个对象的移动和攻击方法 。这样,根据每个对象的实际类型,会执行不同的移动和攻击逻辑,大大提高了代码的灵活性和可扩展性 。

五、内存管理

(一)ARC 机制

在 Objective-C 的内存管理领域,ARC(自动引用计数)机制堪称一场重大变革。它于 iOS 5 和 Mac OS X 10.7 版本引入,旨在将开发者从繁琐的手动内存管理工作中解放出来 。

ARC 的核心原理基于引用计数系统。每个对象都有一个引用计数,用于记录指向该对象的强引用数量 。当对象被创建时,其引用计数初始化为 1。每当有新的强引用指向该对象时,引用计数就会增加 1;反之,当一个强引用不再指向该对象时,引用计数就会减少 1 。当对象的引用计数降为 0 时,表明没有任何强引用指向它,此时对象所占用的内存就会被自动释放 。

举例来说,假设有一个 NSString 对象:


NSString *str = [[NSString alloc] initWithString:@"Hello, ARC!"];

在这行代码中,通过 alloc 和 init 方法创建了一个 NSString 对象,此时该对象的引用计数为 1,str 是指向这个对象的强引用 。

如果再将这个对象赋值给另一个变量:


NSString *anotherStr = str;

这时,anotherStr 也成为了指向该 NSString 对象的强引用,对象的引用计数增加到 2 。

当不再需要这个对象时,比如将 str 和 anotherStr 都设置为 nil:


str = nil;

anotherStr = nil;

每一次将强引用设置为 nil,都会使对象的引用计数减 1。当两个强引用都变为 nil 后,对象的引用计数降为 0,对象所占用的内存就会被自动释放 。

ARC 的优势显著。首先,它大大降低了内存管理的复杂性和出错的概率 。在手动内存管理时代,开发者需要时刻已关注对象的创建和释放,稍有不慎就会导致内存泄漏或野指针等问题 。而 ARC 通过编译器自动插入 retain 和 release 等内存管理代码,让开发者无需手动编写这些繁琐的操作,从而将更多的精力集中在业务逻辑的实现上 。

其次,ARC 提高了代码的可读性和可维护性 。没有了大量的内存管理代码,代码结构更加清晰,易于理解和修改 。同时,由于减少了人为错误,代码的稳定性和可靠性也得到了提升 。

不过,在使用 ARC 时,也有一些注意事项 。虽然开发者不能直接调用 retain、release 和 autorelease 等方法,但仍需理解内存管理的基本原理,以便更好地调试和优化代码 。此外,在使用 ARC 时,需要注意避免循环引用的问题 。循环引用是指两个或多个对象相互持有对方的强引用,导致它们的引用计数永远不会降为 0,从而造成内存泄漏 。例如,在一个视图控制器中,如果视图控制器持有一个子视图,而子视图又持有该视图控制器的引用,就会形成循环引用 。为了避免这种情况,可以使用弱引用(__weak)来打破循环引用 。例如:


__weak typeof(self) weakSelf = self;

self.subView.block = ^{

[weakSelf doSomething];

};

在这个例子中,通过__weak 关键字创建了一个弱引用 weakSelf,在子视图的 block 中使用 weakSelf 来引用视图控制器,从而避免了循环引用 。

(二)手动内存管理(了解)

在 ARC 出现之前,手动内存管理是 Objective-C 开发中必不可少的技能 。虽然现在 ARC 已经成为主流,但了解手动内存管理的原理和方法,对于深入理解 Objective-C 的内存管理机制仍然非常有帮助 。

在手动内存管理中,主要使用 retain、release 和 autorelease 等方法来控制对象的生命周期 。

retain 方法用于增加对象的引用计数 。当调用 retain 方法时,对象的引用计数会加 1,表示有新的持有者增加 。例如:


NSString *str = [[NSString alloc] initWithString:@"Manual Memory Management"];

[str retain];

在这行代码中,创建了一个 NSString 对象,然后调用 retain 方法,此时对象的引用计数变为 2 。

release 方法用于减少对象的引用计数 。当调用 release 方法时,对象的引用计数会减 1,表示持有者减少 。当引用计数减为 0 时,对象所占用的内存会被释放 。例如:


[str release];

[str release]; // 此时对象的引用计数为0,对象被释放

在这行代码中,连续两次调用 release 方法,当第二次调用时,对象的引用计数降为 0,对象被释放 。需要注意的是,不要对已经释放的对象再次调用 release 方法,否则会导致程序崩溃 。

autorelease 方法则是将对象添加到自动释放池中 。自动释放池是一个对象的集合,当自动释放池被销毁时,会向池中的所有对象发送 release 消息 。使用 autorelease 方法可以延迟对象的释放时间,适用于一些临时对象的管理 。例如:


NSString *tempStr = [[[NSString alloc] initWithString:@"Temporary String"] autorelease];

在这行代码中,创建了一个 NSString 对象,并调用 autorelease 方法将其添加到自动释放池中 。当自动释放池被销毁时,tempStr 对象会收到 release 消息,引用计数减 1 。

手动内存管理和 ARC 有明显的差异 。手动内存管理需要开发者手动调用 retain、release 和 autorelease 等方法,对开发者的要求较高,容易出现内存泄漏和野指针等问题 。而 ARC 则由编译器自动管理内存,大大简化了内存管理的工作,降低了出错的概率 。不过,了解手动内存管理的原理,有助于在使用 ARC 时更好地理解内存管理的过程,以及在处理一些特殊情况时,能够更准确地进行调试和优化 。

六、进阶特性

(一)分类(Category)

分类是 Objective-C 中一个非常实用的特性,它允许我们在不修改原有类的源代码的情况下,为已有类添加新的方法 。这一特性在实际开发中有着广泛的应用,大大提高了代码的可维护性和可扩展性 。

分类的作用主要体现在以下几个方面 。首先,它可以将一个庞大的类分解成多个较小的部分,每个部分专注于实现一类功能,从而使代码的组织结构更加清晰 。比如,一个包含众多功能的视图控制器类,我们可以通过分类将界面布局相关的方法放在一个分类中,数据处理相关的方法放在另一个分类中 。其次,分类为系统类的扩展提供了便利 。我们可以为系统类(如 NSString、NSArray 等)添加自定义的方法,以满足项目特定的需求 。

在实际应用中,分类的使用场景十分丰富 。例如,在处理字符串时,我们经常需要进行一些特定的格式转换或校验操作 。通过为 NSString 类添加分类,我们可以将这些常用的操作封装成方法,方便在项目中反复调用 。假设我们需要判断一个字符串是否为有效的邮箱地址,就可以在 NSString 的分类中实现这样的方法:


@interface NSString (EmailValidation)

- (BOOL)isValidEmail;

@end

@implementation NSString (EmailValidation)

- (BOOL)isValidEmail {

NSString *emailRegex = @"[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,6}";

NSPredicate *emailTest = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", emailRegex];

return [emailTest evaluateWithObject:self];

}

@end

使用时,只需要调用:


NSString *email = @"test@example.com";

BOOL isValid = [email isValidEmail];

这样,我们就为 NSString 类增加了一个判断邮箱地址有效性的方法,而且不需要修改 NSString 类的原始代码 。

再比如,在开发一个电商应用时,可能需要对 NSArray 进行扩展,以实现一些与商品列表相关的特殊操作 。我们可以创建一个 NSArray 的分类,添加一个方法来计算商品列表的总价格:


@interface NSArray (ProductPriceCalculation)

- (CGFloat)totalPriceOfProducts;

@end

@implementation NSArray (ProductPriceCalculation)

- (CGFloat)totalPriceOfProducts {

CGFloat totalPrice = 0.0f;

for (id product in self) {

if ([product respondsToSelector:@selector(price)]) {

totalPrice += [product price];

}

}

return totalPrice;

}

@end

假设数组中的每个元素都是一个具有 price 属性的商品对象,通过这个分类方法,就可以方便地计算出商品列表的总价格 。

需要注意的是,分类虽然强大,但也有一定的局限性 。分类只能添加方法,不能添加成员变量 。这是因为在运行时,对象的内存布局已经确定,如果添加实例变量会破坏类的内部布局,这对于编译型语言来说是灾难性的 。此外,如果分类中定义的方法与原有类中的方法同名,那么在运行时,分类中的方法会覆盖原有类的方法 。因此,在使用分类时,要特别注意方法命名,避免出现命名冲突 。

(二)协议(Protocol)

协议是 Objective-C 中另一个重要的特性,它定义了一组方法的声明,但不包含方法的实现 。协议的主要作用是实现对象之间的通信和行为规范,使得不同的类可以遵循相同的协议,从而实现特定的功能 。

协议的定义非常简单,使用 @protocol 关键字来声明 。例如,定义一个名为 MyProtocol 的协议,其中包含两个方法:


@protocol MyProtocol <NSObject>

- (void)doSomething;

- (NSString *)getString;

@end

在这个协议中,表示 MyProtocol 协议继承自 NSObject 协议,NSObject 协议是 Objective-C 中所有类都遵循的基本协议,它定义了一些基础的方法,如 alloc、init、release 等 。继承 NSObject 协议可以确保 MyProtocol 协议具有这些基础功能 。

一个类要遵守某个协议,只需要在类的声明中指定即可 。例如,定义一个 MyClass 类,让它遵守 MyProtocol 协议:


@interface MyClass : NSObject <MyProtocol>

@end

当一个类遵守某个协议后,就必须实现协议中定义的所有 @required 方法(如果协议中没有使用 @optional 关键字标记方法,那么所有方法都被视为 @required) 。对于 @optional 方法,类可以选择实现或不实现 。例如,实现 MyClass 类:


@implementation MyClass

- (void)doSomething {

NSLog(@"执行doSomething方法");

}

- (NSString *)getString {

return @"这是从getString方法返回的字符串";

}

@end

协议在实现代理模式中发挥着至关重要的作用 。代理模式是一种常用的设计模式,它允许一个对象(代理对象)代表另一个对象(委托对象)来处理某些任务 。在 Objective-C 中,通过协议和代理对象,我们可以实现对象之间的解耦,提高代码的可维护性和可扩展性 。

以一个简单的按钮点击事件处理为例 。假设我们有一个视图控制器 ViewController,其中包含一个按钮 Button 。当按钮被点击时,我们希望视图控制器能够收到通知并执行相应的操作 。我们可以通过协议和代理模式来实现这一功能 。

首先,定义一个协议 ButtonDelegateProtocol,用于声明按钮点击时的回调方法:


@protocol ButtonDelegateProtocol <NSObject>

- (void)buttonDidClick:(id)sender;

@end

然后,在 Button 类中定义一个代理属性 delegate,并在按钮点击方法中调用代理的方法:


@interface Button : NSObject

@property (nonatomic, weak) id<ButtonDelegateProtocol> delegate;

- (void)simulateClick;

@end

@implementation Button

- (void)simulateClick {

if ([self.delegate respondsToSelector:@selector(buttonDidClick:)]) {

[self.delegate buttonDidClick:self];

}

}

@end

在 ViewController 中,让它遵守 ButtonDelegateProtocol 协议,并设置按钮的代理为自身:


@interface ViewController : UIViewController <ButtonDelegateProtocol>

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

Button *button = [[Button alloc] init];

button.delegate = self;

// 模拟按钮点击

[button simulateClick];

}

- (void)buttonDidClick:(id)sender {

NSLog(@"按钮被点击了,sender: %@", sender);

}

@end

在这个例子中,Button 类通过代理和协议,将按钮点击事件的处理委托给了 ViewController 。当按钮被点击时,Button 会检查代理是否实现了 buttonDidClick: 方法,如果实现了,就调用该方法,从而实现了对象之间的通信和协作 。通过这种方式,Button 类和 ViewController 类之间的耦合度降低,代码更加灵活和可维护 。

(三)块(Block)

块(Block)是 Objective-C 中的一个强大特性,它封装了一段代码,可以在需要的时候执行 。块可以作为函数参数传递,也可以作为函数的返回值,并且可以捕获其定义时所在作用域的变量 。这种特性使得块在多线程编程、集合遍历、动画转场等场景中得到了广泛的应用 。

块的语法具有独特的结构 。定义一个块变量时,使用 ^ 符号来表示块的开始 。例如,定义一个简单的块,它接受两个整数参数并返回它们的和:


int (^sumBlock)(int, int) = ^(int a, int b) {

return a + b;

};

在这个例子中,int (^sumBlock)(int, int) 定义了一个名为 sumBlock 的块变量,它接受两个 int 类型的参数,返回值也是 int 类型 。^ 后面的部分是块的实现,它接受参数 a 和 b,并返回它们的和 。

块在多线程编程中发挥着重要作用 。通过 GCD(Grand Central Dispatch)框架,我们可以方便地使用块来实现异步任务 。例如,在后台线程中执行一个耗时操作,完成后在主线程中更新 UI:


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

// 异步执行耗时操作,如网络请求或数据处理

NSURL *url = [NSURL URLWithString:@"https://example.com"];

NSData *data = [NSData dataWithContentsOfURL:url];

dispatch_async(dispatch_get_main_queue(), ^{

// 更新UI的操作需要在主线程中执行

UIImage *image = [UIImage imageWithData:data];

self.imageView.image = image;

});

});

在这个例子中,dispatch_async 函数用于将任务提交到指定的队列中执行 。第一个参数是一个队列,这里使用 dispatch_get_global_queue 获取了一个全局的并行队列 。在第一个块中,我们执行了一个网络请求,获取图片数据 。由于网络请求是耗时操作,放在后台线程中执行可以避免阻塞主线程,保证应用的响应性 。当网络请求完成后,通过 dispatch_async 将更新 UI 的操作提交到主线程队列中执行,因为 UI 的更新必须在主线程中进行 。

在集合遍历中,块也提供了简洁高效的方式 。例如,遍历一个数组并打印每个元素:


NSArray *array = @[@"apple", @"banana", @"cherry"];

[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

NSLog(@"第%lu个元素: %@", (unsigned long)idx, obj);

}];

在这个例子中,enumerateObjectsUsingBlock 方法接受一个块作为参数 。块中的三个参数分别表示当前遍历到的元素 obj、元素的索引 idx 以及一个 BOOL 指针 stop 。通过这个块,我们可以方便地对数组中的每个元素进行操作 。如果在块中设置 * stop = YES,就可以停止遍历 。

块还可以作为函数的参数和返回值 。例如,定义一个函数,它接受一个块作为参数,并在内部调用这个块:


- (void)executeBlock:(void (^)(void))block {

if (block) {

block();

}

}

使用时,可以这样调用:


[self executeBlock:^{

NSLog(@"这是在块中执行的代码");

}];

再比如,定义一个函数,它返回一个块:


- (int (^)(int, int))createSumBlock {

return ^(int a, int b) {

return a + b;

};

}

使用时:


int (^sumBlock) (int, int) = [self createSumBlock];

int result = sumBlock(3, 5);

NSLog(@"结果: %d", result);

在这个例子中,createSumBlock 函数返回了一个块,这个块可以在后续的代码中被调用,实现了代码的灵活复用 。

七、学习资源推荐

学习 Objective-C,选择合适的学习资源至关重要,它能让我们的学习之路更加顺畅。

书籍是系统学习的好帮手。《Objective-C 基础教程》是一本非常适合初学者的书籍,它以通俗易懂的语言和丰富的实例,全面介绍了 Objective-C 的基本语法、面向对象编程概念以及内存管理等基础知识 。通过阅读这本书,你可以打下坚实的基础,逐步掌握 Objective-C 的核心要点 。例如,书中对于类和对象的讲解,通过生动的示例代码,让读者能够轻松理解类的定义、对象的创建以及它们之间的关系 。

《Effective Objective-C 2.0:编写高质量 iOS 与 OS X 代码的 52 个有效方法》则更适合有一定基础的开发者,它深入探讨了 Objective-C 的高级特性和最佳实践,通过 52 个具体的方法,帮助开发者写出更高效、更优雅的代码 。比如在讲解内存管理时,书中详细阐述了 ARC 机制下的内存管理技巧,以及如何避免常见的内存问题 。

除了书籍,网络上也有许多优质的学习资源 。苹果官方文档是最权威的学习资料,它涵盖了 Objective-C 的各个方面,包括语言参考、开发指南和 API 文档等 。在开发过程中遇到问题时,查阅官方文档往往能找到最准确的答案 。例如,在使用某个系统框架时,通过官方文档可以了解到该框架的类、方法以及使用示例 。

Stack Overflow 是一个知名的技术问答社区,在这里,你可以搜索到各种关于 Objective-C 的问题和解答 。无论你遇到什么难题,都有可能在这个社区中找到解决方案 。同时,你也可以积极参与社区讨论,与其他开发者交流经验,共同进步 。比如,当你在使用块(Block)时遇到语法错误或逻辑问题,在 Stack Overflow 上搜索相关问题,往往能得到详细的解答和建议 。

CocoaChina 是国内最大的苹果开发技术社区,提供了丰富的 Objective-C 学习资料、开源项目和技术文章 。在这里,你可以与国内的开发者们交流学习心得,分享自己的开发经验 。社区中的论坛板块是一个很好的交流平台,你可以在这里提出问题,也可以帮助其他开发者解决问题 。此外,社区还会定期举办技术讲座和线下活动,为开发者们提供了学习和交流的机会 。

八、总结与展望

学习 Objective-C 是一段充满挑战与收获的旅程。通过掌握它的基础语法、面向对象编程特性、内存管理以及进阶特性,你已经具备了开发 iOS 和 macOS 应用的坚实基础 。在学习过程中,我们了解到 Objective-C 以其独特的消息传递机制和与苹果生态系统的紧密集成,成为了 iOS 和 macOS 开发的重要语言 。从简单的数据类型和变量操作,到复杂的类继承、多态以及内存管理,每一个知识点都是构建强大应用的基石 。

但学习编程绝非一蹴而就,持续的实践和学习至关重要 。建议你在日常学习中,多参与实际项目开发,无论是小型的个人项目,还是参与开源项目,都能让你在实践中不断巩固所学知识,提升编程能力 。同时,积极与其他开发者交流分享,参加技术社区的讨论和线下的技术交流活动,能够拓宽你的技术视野,学习到更多的编程技巧和经验 。

在学习 Objective-C 的过程中,你可能会遇到各种问题和困难,这是很正常的 。关键是要保持积极的学习态度,善于利用各种学习资源,如书籍、官方文档、技术论坛等,去解决问题 。每一次解决问题的过程,都是你成长的机会 。

希望你能在 Objective-C 的学习道路上坚持不懈,不断探索,将所学知识运用到实际项目中,创造出更多优秀的 iOS 和 macOS 应用 。如果你在学习过程中有任何心得体会,欢迎在评论区留言分享,让我们一起共同进步 。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容