2023最新中级难度Objective-C面试题,包含答案。刷题必备!记录一下。
好记性不如烂笔头
问: 请解释一下Objective-C中的属性(property)和实例变量(instance variable)之间的关系。
在Objective-C中,属性(property)和实例变量(instance variable,简称ivar)都是类的组成部分,它们都用来存储对象的状态。然而,它们在使用方式、访问控制以及内存管理等方面有所不同。
实例变量(Instance Variable):
实例变量是类内部定义的私有变量,通常用于存储对象的状态数据。实例变量是在对象被创建时分配内存,并且在整个对象生命周期内保持不变。你可以直接通过点运算符来访问和修改实例变量,但这种访问通常仅限于类的内部实现。
例如:
@interface MyClass : NSObject {
// 实例变量声明
NSString *_name;
}
@end
@implementation MyClass
- (void)setName:(NSString *)name {
_name = name;
}
@end
在这个例子中,_name
是一个实例变量,它用于存储对象的名称。我们可以通过self->_name
或_name
来访问这个实例变量。
属性(Property):
属性是一种更高级别的抽象,它为实例变量提供了额外的功能,如自动合成存取方法、内存管理语义以及访问控制等。当你声明一个属性时,编译器会自动生成相应的实例变量(如果还没有的话),以及getter
和setter
方法。
属性可以在.h
文件中公开声明,这样其他类就可以通过点运算符来访问它们。你还可以指定属性的内存管理语义,比如strong
、weak
、copy
或assign
等。
例如:
@interface MyClass : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation MyClass
// 如果没有提供@synthesize,编译器会自动合成
// @synthesize name = _name;
@end
在这个例子中,name
是一个属性,它具有copy
的内存管理语义。当我们设置name
属性时,实际调用的是set_name:
方法,而获取name
属性时,实际调用的是_name
方法。
关系:
在Objective-C中,属性和实例变量的关系可以总结如下:
- 属性提供了一种更加方便的方式来管理对象的状态,包括访问控制、内存管理语义以及自动合成存取方法。
- 每个属性都会对应一个实例变量。如果你没有显式地声明实例变量,编译器会在幕后为你生成一个以下划线开头的实例变量,如
_name
。 - 你可以选择是否为属性提供自定义的存取方法。如果没有提供自定义的方法,编译器会为你合成默认的方法。
- 在MRC(手动引用计数)模式下,属性还负责处理内存管理问题。而在ARC(自动引用计数)模式下,这通常是自动完成的。
总的来说,实例变量是类的基本构建块,而属性则是对实例变量的封装,提供了更多的功能和灵活性。在大多数情况下,你应该优先使用属性来管理对象的状态。
问: Objective-C中的“copy”、 “strong”、 “weak”和 “assign”等属性修饰符有什么作用?它们之间有何区别?
在Objective-C中,属性修饰符(也称为“内存管理语义”)是用来指定对象如何存储和管理的。主要有四种属性修饰符:copy
、strong
、weak
和assign
。
-
strong:
strong
是默认的属性修饰符,用于保持对对象的强引用。这意味着当一个对象被赋予了strong
属性时,它的引用计数会增加。只要有一个或多个strong
指针指向这个对象,它就不会被释放。如果所有的strong
指针都释放了它们的引用,那么对象的引用计数就会变为0,系统会自动回收该对象所占用的内存。 -
copy:
copy
属性修饰符意味着该属性应该是一个对象的副本,而不是原始对象本身。当你使用copy
修饰符时,系统会调用对象的copyWithZone:
方法来创建一个新的实例,并将新实例赋值给属性。这对于那些希望确保数据安全性和不变性的场景非常有用,比如字符串和字典等不可变对象。 -
weak:
weak
属性修饰符表示对对象的弱引用。弱引用不会增加对象的引用计数,因此即使有弱引用存在,只要所有强引用都释放了,对象也会被销毁。当你不希望保留对象,但仍然需要访问它时,可以使用弱引用。另外,弱引用还可以防止循环引用问题。 -
assign:
assign
属性修饰符通常用于非对象类型,如基本数据类型(如NSInteger、CGFloat等)。它只是简单地执行指针赋值,而不涉及任何内存管理。对于对象类型,使用assign
可能会导致意外的结果,因为不会增加对象的引用计数,可能导致对象提前被释放。
这些属性修饰符之间的主要区别在于它们如何影响对象的生命周期和所有权:
strong
持有对象,延长其生命周期。copy
复制对象,保证属性的安全性。weak
不持有对象,不会阻止对象被释放,但在对象未释放时仍能访问它。assign
适用于非对象类型,或者你不希望持有对象的情况。
选择正确的属性修饰符取决于你的具体需求和应用场景。例如,如果你希望确保对象在属性中始终保持不变,你应该使用copy
;如果你不关心对象的所有权,只希望在对象还活着的时候能够访问它,你应该使用weak
。
问: 如何使用Objective-C中的“Categories”为现有的类添加方法?
在Objective-C中,使用“Categories”为现有类添加方法是一种非常实用的方法,它允许我们在不修改原始类源代码的情况下扩展其功能。以下是如何使用Category为现有的类添加方法的步骤:
- 定义Category头文件:
首先,创建一个新的.h
文件,例如:MyClass+MyCategory.h
。这里的MyClass
是你要扩展的类名,MyCategory
是你自定义的类别名称。
// MyClass+MyCategory.h
#import "MyClass.h"
@interface MyClass (MyCategory)
// 在这里声明要添加的方法
- (void)myCategoryMethod;
@end
- 实现Category方法:
接下来,创建一个对应的.m
文件,例如:MyClass+MyCategory.m
。在这个文件中实现你刚才声明的方法。
// MyClass+MyCategory.m
#import "MyClass+MyCategory.h"
@implementation MyClass (MyCategory)
// 实现category中的方法
- (void)myCategoryMethod {
// 这里编写你的实现代码
}
@end
- 引入并使用Category:
现在可以在其他地方引入这个Category,并调用你新添加的方法了。
// 在需要使用Category的地方导入它
#import "MyClass+MyCategory.h"
// 创建或获取一个MyClass对象
MyClass *instance = [[MyClass alloc] init];
// 调用通过Category添加的新方法
[instance myCategoryMethod];
这样,你就成功地使用Objective-C的Category为现有的类添加了一个新的方法。这种方法特别适用于当你想要扩展系统提供的类(如UIKit中的UIView、UIViewController等)时,因为你不可以直接修改这些系统的源代码。
问: Objective-C中的“Blocks”是什么?如何在项目中使用它们?
Objective-C中的"Blocks"是一种可以封装代码和数据的结构,它允许你定义一个可以在稍后执行的一段代码。在Cocoa Touch开发中,Blocks通常用于回调、异步操作以及GCD(Grand Central Dispatch)等场景。
Block的基本语法如下:
^(parameters) {
// 你的代码块在这里
}
例如,一个简单的Block可以是这样的:
void (^printBlock)(NSString *str) = ^(NSString *str) {
NSLog(@"打印:%@", str);
};
printBlock(@"Hello, Blocks!");
要在项目中使用Blocks,你需要遵循以下步骤:
- 声明Block变量:
首先,你需要声明一个Block类型的数据作为方法的参数或返回值。例如:
typedef void (^PrintBlock)(NSString *str); // 声明一个Block类型
- (void)useBlock:(PrintBlock)block; // 在方法签名中使用Block类型作为参数
- 创建并初始化Block:
创建一个新的Block实例,并为其提供实现代码。这可以通过在赋值时直接定义来完成:
PrintBlock printBlock = ^(NSString *str) {
NSLog(@"打印:%@", str);
};
- 传递和调用Block:
将Block作为参数传递给接受它的方法,并在那里调用它:
[self useBlock:printBlock];
- 在接收方法中使用Block:
接收Block的方法可以像普通函数一样调用它:
- (void)useBlock:(PrintBlock)block {
block(@"从方法内部调用Block");
}
- 访问外部变量:
Block可以访问其作用域内的变量,但需要注意的是,如果Block修改了外部变量,需要使用__block
存储类型修饰符:
__block int counter = 0;
IncrementBlock incrementBlock = ^() {
counter++;
};
incrementBlock(); // counter现在增加到1
- 捕获对象引用:
如果Block捕获了对象引用,则必须注意循环引用的问题。在这种情况下,可以使用weakSelf
和strongSelf
模式来避免循环引用:
__weak typeof(self) weakSelf = self;
MyBlock block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf) {
// 这里使用strongSelf而不是self
}
};
通过这些步骤,你就可以在Objective-C项目中有效地使用Blocks了。
问: 请解释一下Objective-C中的“Core Data”框架的主要组件和工作原理。
Objective-C中的“Core Data”框架是苹果公司为iOS和OS X开发的持久化存储技术。它是一个对象关系映射(ORM)系统,用于在内存中管理应用程序的数据,并将其自动保存到磁盘上。以下是一些主要组件和工作原理:
主要组件:
-
Managed Object Model (MOM):这是一个XML文件,描述了应用程序数据模型的结构,包括实体(Entities)、属性(Attributes)和它们之间的关系。
-
Persistent Store Coordinator (PSC):这是与存储介质(如SQLite数据库或XML文件)交互的组件。它负责将对象模型与实际的物理存储进行映射。
-
Managed Object Context (MOC):这是一个临时的数据缓存区,存储着应用程序当前使用的托管对象。它负责跟踪对托管对象的所有更改,并协调这些更改最终被提交到持久存储。
-
NSManagedObject:这是一个基类,用户定义的实体类型应该继承自这个类。这些类代表模型中的实体实例。
-
Fetch Request:这是用来从上下文中获取符合特定条件的托管对象的请求。
-
Fetched Results Controller:这是一个可选组件,通常用于管理表格视图的数据源。它自动处理对象的排序、过滤以及当底层数据改变时更新视图。
工作原理:
-
创建模型:首先,你需要使用Xcode的数据模型编辑器来设计你的数据模型,包括实体、属性和关系。
-
初始化Core Data堆栈:在应用启动时,你需要设置一个包含上述所有组件的堆栈。这通常是通过一个单例或AppDelegate完成的。
-
创建/读取对象:你可以使用
insertNewObjectForEntityForName:
方法插入新对象,或者使用fetch request查询现有对象。 -
保存更改:当你对上下文中的对象进行修改后,需要调用
save:
方法将更改保存到持久存储。 -
监听对象变化:可以注册观察者来监听对象的改变,以便在其他地方作出相应的反应。
-
销毁堆栈:当不再需要Core Data时,你应该正确地销毁堆栈以释放资源。
简而言之,Core Data是一个强大的数据管理和持久化框架,它允许你以面向对象的方式操作数据,并且提供了许多功能,如撤销/重做、离线存储、对象生命周期管理等,使得应用程序能够更加高效地处理数据。
问: 如何使用Objective-C中的“Core Animation”框架创建基本动画效果?
Objective-C中的“Core Animation”框架(也称为CA)是iOS和Mac OS X平台上用于创建动画效果的强大工具。它允许你使用硬件加速来创建复杂的2D动画,而不会影响你的应用程序的性能。以下是如何使用Core Animation在Objective-C中创建基本动画效果的步骤:
- 导入相关库:
在你的.m
文件中引入Core Animation库。
#import <QuartzCore/QuartzCore.h>
- 获取要动画的目标对象:
通常,这将是一个UIView
或CALayer
对象。
UIView *viewToAnimate = ...; // 获取一个视图实例
- 创建动画对象:
使用CABasicAnimation
类创建一个新的动画对象。你需要指定你要改变哪个属性以及它的初始值和结束值。
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.scale"];
animation.fromValue = @(1.0);
animation.toValue = @(0.5); // 缩小到原来的一半大小
- 设置动画参数:
可以通过修改动画对象的各种属性来定制动画的行为,如持续时间、重复次数、填充模式等。
animation.duration = 0.5; // 动画持续0.5秒
animation.autoreverses = YES; // 动画完成后自动反向播放
animation.repeatCount = MAXFLOAT; // 无限循环
- 添加动画到目标对象:
使用addAnimation:forKey:
方法将动画添加到目标对象的层上。
[viewToAnimate.layer addAnimation:animation forKey:@"scale"];
- 开始动画:
添加动画后,动画会立即开始播放。
如果你想在动画结束后保持最终状态,你可以将图层的模型层(presentation layer)上的相应属性复制回其模型层(model layer)。这样,即使动画结束并被移除,图层仍会保持其最后的状态。
[CATransaction begin];
[CATransaction setCompletionBlock:^{
viewToAnimate.layer.transform = viewToAnimate.layer.presentationLayer.transform;
}];
[viewToAnimate.layer addAnimation:animation forKey:@"scale"];
[CATransaction commit];
以上就是在Objective-C中使用Core Animation创建基本动画效果的步骤。这只是冰山一角,Core Animation还提供了许多其他功能,例如关键帧动画、路径动画、过渡动画等等。
问: Objective-C中的“NSPredicate”和“NSFetchRequest”有什么作用?如何使用它们进行数据查询?
Objective-C中的“NSPredicate”和“NSFetchRequest”是Core Data框架中用于查询数据的核心组件。它们的作用如下:
NSPredicate
NSPredicate
是一个对象,它表示一个布尔值表达式,用来测试属性值是否满足某些条件。你可以使用NSPredicate
来指定筛选或排序规则,以便在数据库中查找特定的记录。
NSFetchRequest
NSFetchRequest
是一个对象,它表示对Managed Object Context(MOC)中的实体集合进行查询的请求。它可以与NSPredicate
结合使用,以根据给定的条件检索数据。
要使用这些类进行数据查询,请遵循以下步骤:
- 初始化NSFetchRequest:
首先,你需要创建一个新的NSFetchRequest
实例,并设置它的实体名称。
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:@"MyEntity"];
- 设置过滤条件(可选):
如果你想根据特定条件过滤结果集,可以创建一个NSPredicate
对象,并将其设置为fetchRequest
的predicate
属性。
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"attributeName == %@", @"someValue"];
[fetchRequest setPredicate:predicate];
- 设置排序规则(可选):
如果你希望按特定属性对结果进行排序,可以创建一个NSSortDescriptor
对象,并将其添加到fetchRequest
的sortDescriptors
数组中。
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"attributeName" ascending:YES];
[fetchRequest setSortDescriptors:@[sortDescriptor]];
- 执行查询:
使用你的ManagedObjectContext
执行查询,获取结果。
NSError *error = nil;
NSArray *results = [context executeFetchRequest:fetchRequest error:&error];
if (error) {
NSLog(@"Error fetching data: %@", error);
}
- 处理结果:
结果将作为一个包含所有匹配项的数组返回。你可以遍历这个数组并访问每个元素的属性。
以上就是如何使用NSPredicate
和NSFetchRequest
在Objective-C中进行数据查询的基本过程。这只是一个基本示例,实际应用中可能会有更复杂的查询需求,如关联查询、分页等。
问: 请解释一下Objective-C中的“NSOperation”和“NSOperationQueue”的使用场景及优劣势。
Objective-C中的NSOperation
和NSOperationQueue
是Apple提供的用于在多线程环境中执行任务的API。它们为异步操作提供了高级抽象,使得开发者可以更容易地管理和协调多个任务。
使用场景:
- 并行执行多个任务:你可以将多个
NSOperation
对象添加到同一个NSOperationQueue
中,系统会自动根据可用资源来调度这些任务。 - 依赖关系管理:
NSOperation
支持设置依赖关系,这意味着一个操作只有在其依赖的操作完成后才会开始执行。 - 取消操作:你可以在任何时间取消正在排队或正在执行的操作。
- 操作优先级:可以通过设置操作的优先级来控制它们在队列中的执行顺序。
- 状态监控:
NSOperation
提供了一些属性(如isExecuting
、isFinished
等)来监控操作的状态。
优劣势:
优势:
- 易于使用:与GCD相比,
NSOperation
和NSOperationQueue
提供了一个更高级别的抽象,使得代码更易读、更易于维护。 - 灵活:
NSOperation
是一个抽象类,你可以通过子类化它来自定义自己的操作行为。 - 更好的错误处理:如果操作过程中发生错误,你可以捕获并处理它。
- 可取消性:由于每个操作都是一个独立的对象,因此可以轻松地取消单个操作。
劣势:
- 性能:虽然
NSOperation
和NSOperationQueue
在大多数情况下已经足够快,但在某些情况下,GCD可能会提供更好的性能。 - 额外的复杂性:相比于直接使用GCD,使用
NSOperation
和NSOperationQueue
可能需要更多的代码和理解。 - 不适用于所有情况:对于一些简单的并发任务,直接使用GCD可能是更好的选择。
综上所述,NSOperation
和NSOperationQueue
在需要更复杂的任务管理和调度的情况下非常有用,而GCD则更适合于简单的一次性任务。在实际开发中,可以根据具体需求选择最合适的工具。
问: Objective-C中的“NSURLConnection”和“NSURLSession”有何异同?如何选择合适的网络请求方式?
Objective-C中的NSURLConnection
和NSURLSession
都是用于从服务器获取数据的网络请求API。虽然它们在功能上有很多相似之处,但也有一些关键的区别。
相同点:
- 基于URL的工作方式:两者都使用一个指定的URL来发送请求,并接收响应。
- 异步处理:两者都支持异步操作,允许应用程序在等待网络响应时继续执行其他任务。
- 代理模式:两者都可以通过代理对象(delegate)来接收请求的结果或错误信息。
不同点:
- 内存管理:
NSURLConnection
需要手动管理内存,而NSURLSession
会自动管理内存。 - 多任务支持:
NSURLSession
提供了更强大的多任务支持,可以同时处理多个请求。 - 缓存策略:
NSURLSession
具有更灵活的缓存策略,可以根据需要选择合适的缓存机制。 - 后台传输:
NSURLSession
支持在应用进入后台后继续进行上传和下载任务,而NSURLConnection
则不支持这一特性。 - 安全性和隐私:
NSURLSession
提供了更多的安全选项,例如HTTP严格传输安全(HSTS)、TLS协议版本控制等。
如何选择:
- 对于简单的单个请求:如果你的应用只需要偶尔发送一个简单的GET或POST请求,那么使用
NSURLConnection
就足够了。 - 对于复杂的多任务需求:如果你的应用需要同时处理多个请求,或者需要在后台继续传输数据,那么
NSURLSession
是一个更好的选择。 - 对于需要更高级特性的场景:如果你需要特定的安全特性、缓存策略或者优先级设置,那么
NSURLSession
将提供更多的灵活性。
总的来说,NSURLSession
通常被认为是更现代、更强大的解决方案,但NSURLConnection
在一些简单的情况下仍然可用。根据你的具体需求和项目的复杂性来决定使用哪种技术。
问: 如何使用Objective-C中的“NSURLSessionDataTask”和“NSURLSessionDownloadTask”进行网络请求和文件下载操作?
在Objective-C中,NSURLSessionDataTask
和NSURLSessionDownloadTask
都是用于从服务器获取数据的API。它们都属于NSURLSession
的一部分,但在处理请求结果的方式上有所不同。
NSURLSessionDataTask
NSURLSessionDataTask
适用于获取较小的数据,如JSON或XML响应,并直接在内存中处理这些数据。以下是如何使用NSURLSessionDataTask
进行网络请求的基本步骤:
- 创建NSURLSession对象:
首先,你需要创建一个NSURLSession
对象。你可以使用默认的配置或者自定义配置来初始化这个对象。
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
- 创建NSURLRequest对象:
创建一个NSURLRequest
对象,指定要访问的URL和HTTP方法(GET、POST等)。
NSURL *url = [NSURL URLWithString:@"http://example.com/api"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];
- 创建NSURLSessionDataTask对象:
使用NSURLSession
对象的dataTaskWithRequest:completionHandler:
方法创建一个新的NSURLSessionDataTask
对象。
NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
// 在这里处理响应数据和错误
}];
- 启动任务:
调用resume
方法开始执行任务。
[task resume];
- 处理响应数据:
在完成处理器闭包中,你可以检查是否有错误,然后处理返回的NSData
对象。通常情况下,你会将其转换为适当的类型,例如解析为JSON或XML。
if (error == nil) {
NSString *responseString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"Response: %@", responseString);
} else {
NSLog(@"Error: %@", error);
}
NSURLSessionDownloadTask
NSURLSessionDownloadTask
主要用于下载大文件,并将文件保存到磁盘上。以下是如何使用NSURLSessionDownloadTask
进行文件下载的基本步骤:
- 创建NSURLSession对象:
同样地,首先需要创建一个NSURLSession
对象。
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
- 创建NSURLRequest对象:
创建一个NSURLRequest
对象,指定要下载的文件的URL。
NSURL *url = [NSURL URLWithString:@"http://example.com/file.zip"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
[request setHTTPMethod:@"GET"];
- 创建NSURLSessionDownloadTask对象:
使用NSURLSession
对象的downloadTaskWithRequest:completionHandler:
方法创建一个新的NSURLSessionDownloadTask
对象。
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
// 从临时位置移动文件到你想要的位置
[[NSFileManager defaultManager] moveItemAtURL:location toURL:finalLocation error:nil];
} else {
NSLog(@"Error: %@", error);
}
}];
- 启动任务:
调用resume
方法开始执行任务。
[task resume];
- 处理下载结果:
在完成处理器闭包中,你可以检查是否有错误,然后处理返回的临时文件路径。通常情况下,你会将文件从临时位置移动到你想要存储的位置。
请注意,NSURLSessionDownloadTask
会自动管理文件的下载和存储过程。当下载完成后,你需要手动将文件从临时位置移动到你的应用程序可以访问的目录中。
通过以上步骤,你可以使用NSURLSessionDataTask
和NSURLSessionDownloadTask
进行网络请求和文件下载操作。