1. id 和 instanceType 有什么区别?
在iOS开发中,id和instancetype是Objective-C中的两种类型,它们在类型安全和使用场景上有一些重要的区别。以下是对这两种类型的详细解释:
1. id
定义:
id是一个通用的对象类型,表示任何类型的对象。它是Objective-C中的一种动态类型,可以指向任何类的实例。-
特点:
-
灵活性:
id可以用来表示任何对象类型,因此在编写通用代码时非常有用。 -
类型安全:使用
id时,编译器不会进行类型检查,这意味着在编译时无法确保对象的类型。这可能导致运行时错误。 - 使用场景:常用于需要接受多种类型对象的方法参数或返回值。
-
灵活性:
-
示例:
id myObject = [[NSString alloc] initWithString:@"Hello"]; myObject = [[NSNumber alloc] initWithInt:42]; // 可以重新赋值为其他类型
2. instancetype
定义:
instancetype是一个特殊的类型,表示当前类的实例类型。它用于方法的返回类型,指示返回值是调用该方法的类的实例。-
特点:
-
类型安全:
instancetype提供了更好的类型安全性,因为它会根据调用该方法的类来推断返回值的类型。 - 使用场景:常用于工厂方法或初始化方法,确保返回的对象类型与调用者一致。
-
类型安全:
-
示例:
@interface MyClass : NSObject + (instancetype)createInstance; // 返回当前类的实例 @end @implementation MyClass + (instancetype)createInstance { return [[self alloc] init]; // 返回当前类的实例 } @end MyClass *myInstance = [MyClass createInstance]; // myInstance 的类型是 MyClass *
总结
-
id:- 表示任何类型的对象,灵活但缺乏类型安全。
- 适用于需要接受多种类型的场景。
-
instancetype:- 表示当前类的实例类型,提供更好的类型安全。
- 适用于工厂方法和初始化方法,确保返回值的类型与调用者一致。
相同点
instancetype 和 id 都是万能指针,指向对象。
不同点:
1.id 在编译的时候不能判断对象的真实类型,instancetype 在编译的时候可以判断对象的真实类型。
2.id 可以用来定义变量,可以作为返回值类型,可以作为形参类型;instancetype 只能作为返回值类型。
在实际开发中,优先使用instancetype来提高代码的类型安全性,尤其是在涉及对象创建和返回的场景中。使用id时要小心,确保在使用对象之前进行适当的类型检查。
2. self和super的区别
在iOS开发中,self和super是Objective-C和Swift中用于引用对象和其父类的方法的关键字。它们在语义和使用场景上有明显的区别。以下是对这两个关键字的详细解释:
1. self
定义:
self是一个指向当前对象实例的指针。在类的方法中,self用于引用当前对象的属性和方法。-
特点:
-
当前实例:
self始终指向当前正在执行方法的对象实例。 -
用于方法调用:可以使用
self来调用当前对象的方法或访问属性。 -
在初始化方法中:在初始化方法中,
self可以用于返回当前对象的实例。
-
当前实例:
-
示例:
@interface MyClass : NSObject @property (nonatomic, strong) NSString *name; - (void)printName; @end @implementation MyClass - (void)printName { NSLog(@"My name is %@", self.name); // 使用 self 访问属性 } @end
2. super
定义:
super是一个指向父类的指针,用于调用父类的方法和访问父类的属性。-
特点:
-
父类实例:
super用于引用当前对象的父类,允许访问父类的方法和属性。 -
用于方法重写:在子类中重写父类的方法时,可以使用
super调用父类的实现,以确保父类的逻辑得以执行。 -
避免循环调用:使用
super可以避免在子类中直接调用自身的方法,从而避免循环调用。
-
父类实例:
-
示例:
@interface MyBaseClass : NSObject - (void)doSomething; @end @implementation MyBaseClass - (void)doSomething { NSLog(@"Doing something in MyBaseClass"); } @end @interface MyClass : MyBaseClass - (void)doSomething; // 重写父类方法 @end @implementation MyClass - (void)doSomething { [super doSomething]; // 调用父类的方法 NSLog(@"Doing something in MyClass"); } @end
总结
-
self:- 指向当前对象实例,表示当前正在执行方法的对象。
- 用于访问当前对象的属性和方法。
-
super:- 指向当前对象的父类,允许访问父类的方法和属性。
- 用于在子类中调用父类的方法,确保父类的逻辑得以执行。
在实际开发中,self和super的使用场景各有不同,理解它们的区别有助于更好地管理对象的行为和继承关系。
3. GCD执行原理
在iOS开发中,GCD(Grand Central Dispatch) 是一个强大的并发编程工具,旨在简化多线程编程。GCD的执行原理涉及任务的调度、线程管理和资源优化。以下是GCD的执行原理的详细解释:
1. 任务和队列
- 任务(Task):GCD中的任务是指需要执行的代码块,通常是一个闭包(closure)或函数。任务可以是同步的或异步的。
-
队列(Queue):GCD使用队列来管理任务的执行。队列可以分为两种类型:
- 串行队列(Serial Queue):一次只执行一个任务,任务按添加顺序执行。
- 并发队列(Concurrent Queue):可以同时执行多个任务,任务的执行顺序不确定。
2. 主队列和全局队列
- 主队列(Main Queue):是一个特殊的串行队列,所有任务都在主线程上执行。适用于更新UI的任务。
- 全局队列(Global Queue):是系统提供的并发队列,具有不同的优先级(如高、中、低)。适用于执行后台任务。
3. 任务调度
-
异步执行:使用
dispatch_async将任务添加到队列,GCD会在适当的线程上执行任务,而不会阻塞当前线程。 -
同步执行:使用
dispatch_sync将任务添加到队列,当前线程会等待任务完成后再继续执行。这种方式会阻塞当前线程。
4. 线程管理
- 线程池:GCD内部维护一个线程池,动态管理线程的创建和销毁。GCD会根据系统的负载和可用资源自动调整线程的数量。
- 任务分配:GCD会根据任务的优先级和队列的类型,将任务分配给可用的线程执行。它会尽量利用系统资源,提高任务的执行效率。
5. 资源优化
-
延迟执行:GCD支持延迟执行任务,可以使用
dispatch_after来指定任务的延迟时间。 -
组(Dispatch Group):GCD允许将多个任务组合在一起,使用
DispatchGroup可以在所有任务完成后执行某个操作。 - 信号量(Dispatch Semaphore):GCD提供信号量机制,用于控制对共享资源的访问,避免竞争条件。
6. 例子
以下是一个简单的GCD使用示例,展示如何在并发队列中执行任务:
import Foundation
let queue = DispatchQueue.global(qos: .userInitiated)
queue.async {
// 执行耗时操作
for i in 1...5 {
print("Task 1 - \(i)")
sleep(1) // 模拟耗时操作
}
}
queue.async {
// 执行另一个耗时操作
for i in 1...5 {
print("Task 2 - \(i)")
sleep(1) // 模拟耗时操作
}
}
// 保持主线程活着,等待任务完成
RunLoop.main.run()
总结
GCD的执行原理基于任务和队列的管理,通过动态调度和线程池优化资源使用。它提供了简单而强大的API,使得多线程编程变得更加容易和高效。通过理解GCD的执行原理,开发者可以更好地利用其功能来实现高效的并发编程。
4. 什么情况使用 weak 关键字,相比 assign 有什么不同?
在 iOS 开发中,weak 关键字和 assign 关键字都用于定义对象的引用,但它们的使用场景和内存管理方式有所不同。
使用 weak 的情况:
-
避免循环引用:当两个对象互相持有对方时,会导致循环引用,造成内存泄漏。使用
weak可以打破这种循环,因为weak引用不会增加对象的引用计数。 -
代理模式:在代理模式中,通常会将代理对象声明为
weak,以避免被代理对象强引用,从而导致内存泄漏。 -
可选引用:
weak引用可以为nil,当被引用的对象被释放时,weak引用会自动变为nil,这在处理可选对象时非常有用。
weak 和 assign 的不同:
-
内存管理:
-
weak:使用 ARC(自动引用计数)时,weak引用不会增加对象的引用计数。当对象被释放时,weak引用会自动设置为nil。 -
assign:assign是一个简单的指针赋值,不会管理对象的内存。它适用于基本数据类型(如int、float等),但不适合对象引用,因为它不会处理对象的生命周期。
-
-
使用场景:
-
weak主要用于对象引用,尤其是在需要避免循环引用的情况下。 -
assign适用于基本数据类型或不需要内存管理的场景。
-
总结:
在 iOS 开发中,通常推荐使用 weak 来管理对象引用,特别是在涉及到对象之间的关系时,以确保内存的有效管理和避免内存泄漏。
5. block为什么要用 copy 关键字?
在 iOS 开发中,使用 copy 关键字来修饰 block 是因为 block 在 Objective-C 中是对象,且它们的内存管理方式与普通的对象不同。
原因如下:
内存管理:当你创建一个 block 时,它最初是存储在栈上的。如果你将这个 block 赋值给一个属性而不使用
copy,那么当这个 block 超出其作用域时,它将被销毁,导致你可能会访问到一个已经被释放的内存区域。对象的复制:使用
copy关键字时,block 会被复制到堆上,这样即使原始的栈内存被释放,堆上的 block 仍然存在,确保了 block 的有效性。避免潜在的错误:如果不使用
copy,可能会导致在 block 被调用时出现未定义的行为,尤其是在异步操作中。
示例:
@property (nonatomic, copy) void (^myBlock)(void);
在这个例子中,myBlock 属性使用了 copy 关键字,确保 block 在被赋值后会被复制到堆上,从而避免了潜在的内存问题。
总结
block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区。
在 ARC 中写不写都行,在 ARC 环境下,编译器会根据情況自动将栈上的 block 复制到堆上。对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。
6. 这个写法会出什么问题: @property (copy) NSMutableArray *array;
在 Objective-C 中,使用 @property (copy) 来声明一个 NSMutableArray 类型的属性会导致一些问题。具体来说,copy 属性修饰符会在设置属性时创建一个对象的副本,而 NSMutableArray 是一个可变数组。使用 copy 时,实际上会创建一个不可变的 NSArray 副本,而不是一个可变的 NSMutableArray。
这意味着:
- 当你尝试对
array进行修改(例如添加或删除元素)时,会导致运行时错误,因为你实际上在操作一个不可变的数组。 - 如果你希望
array是一个可变数组,应该使用strong修饰符。
正确的写法应该是:
@property (strong) NSMutableArray *array;
这样可以确保 array 是一个可变数组,并且可以正常进行修改。
7. @synthesize和@dynamic分别有什么作用?
在iOS开发中,@synthesize和@dynamic是Objective-C中用于属性的关键字,它们的作用如下:
@synthesize
-
@synthesize用于自动生成属性的getter和setter方法。它会根据属性的名称生成相应的实例变量(ivar),并提供默认的实现。 - 例如,如果你有一个属性
@property (nonatomic, strong) NSString *name;,使用@synthesize name = _name;会生成- (NSString *)name和- (void)setName:(NSString *)name方法,并将_name作为实例变量。
@dynamic
-
@dynamic告诉编译器,属性的getter和setter方法是动态实现的,编译器不会为这些属性生成默认的getter和setter。 - 这通常用于那些需要手动实现getter和setter的情况,比如从数据库或网络获取数据,或者使用KVC(键值编码)来动态访问属性。
总结
- 使用
@synthesize时,编译器会自动生成方法和实例变量。 - 使用
@dynamic时,开发者需要自己实现这些方法,编译器不会生成。
希望这能帮助你理解这两个关键字的作用!如果你有更多问题,请随时问我。
8. 消息转发机制
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
1. Method resolution(动态方法解析)
objc运行时会调用+resolveInstanceMethod:或者 +resolveClassMethod:,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。
2. Fast forwarding(快速消息转发)
如果目标对象实现了 -forwardingTargetForSelector:,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
3. Normal forwarding(标准消息转发)
这一步是Runtime最后一次给你挽救的机会。首先它会发送 -methodSignatureForSelector:消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回nil,Runtime则会发出 -doesNotRecognizeSelector: 消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送 -forwardInvocation: 消息给目标对象。
9. 下面的代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:
都输出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
这个题目主要是考察关于 Objective-C 中对 self 和 super 的理解。
super关键字,有以下几点需要注意:
-
receiver还是当前类对象,而不是父类对象; -
super这里的含义就是优先去父类的方法列表中去查实现,很多问题都是父类中其实也没有实现,还是去根类里 去找实现,这种情况下时,其实跟直接调用self的效果是一致的。
下面做详细介绍:
我们都知道:self 是类的隐藏参数,指向当前调用方法的这个类的实例。那 super 呢?
很多人会想当然的认为“super 和 self 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self是指向的同一个消息接受者!他们两个的不同点在于:super 会告诉编译器,调用 class 这个方法时,要去父类的方法,而不是本类里的。
上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。
当使用 self调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用 super时,则从父类的方法列表中开始找。然后调用父类的这个方法。
10. 能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?怎么添加?
在iOS开发中,向编译后得到的类中添加实例变量是不可行的,因为编译后的类的结构是固定的,实例变量的内存布局在编译时就已经确定。
然而,向运行时创建的类中添加实例变量是可以的。Objective-C的运行时特性允许你在运行时动态地添加属性和实例变量。你可以使用class_addIvar函数来实现这一点。
如何添加实例变量
以下是一个简单的示例,展示如何在运行时向一个类中添加实例变量:
#import <objc/runtime.h>
@interface MyClass : NSObject
@end
@implementation MyClass
@end
void addInstanceVariable() {
// 获取类
Class myClass = [MyClass class];
// 添加实例变量
const char *ivarName = "newVariable"; // 实例变量名
size_t size = sizeof(int); // 实例变量类型大小
uint32_t alignment = alignof(int); // 实例变量对齐
class_addIvar(myClass, ivarName, size, alignment, @encode(int));
// 创建实例并设置新变量的值
id instance = [[myClass alloc] init];
[instance setValue:@(42) forKey:@"newVariable"];
// 获取并打印新变量的值
NSLog(@"New variable value: %@", [instance valueForKey:@"newVariable"]);
}
解释
-
获取类:使用
[MyClass class]获取目标类。 -
添加实例变量:使用
class_addIvar函数添加新的实例变量。需要提供变量名、大小、对齐和类型编码。 - 使用新变量:通过KVC(键值编码)设置和获取新变量的值。
注意事项
- 动态添加实例变量可能会导致代码的可读性和可维护性降低,因此应谨慎使用。
- 这种方法主要适用于Objective-C,Swift不支持在运行时动态添加实例变量。
11. RunLoop和线程有什么关系?
在iOS开发中,RunLoop和线程之间有着密切的关系。以下是它们之间的主要联系:
线程的运行机制:每个线程都有一个RunLoop。RunLoop是一个循环,它使线程能够处理事件和消息。没有RunLoop的线程在执行完任务后会立即退出,而有了RunLoop,线程可以在等待事件的同时保持活跃状态。
事件处理:RunLoop负责管理输入源(如用户输入、网络请求等)和定时器。它会在有事件到达时唤醒线程,处理这些事件,然后继续等待下一个事件。
主线程的RunLoop:在iOS中,主线程(UI线程)有一个默认的RunLoop,它会处理所有的UI事件和用户交互。开发者通常不需要手动管理主线程的RunLoop,因为UIKit会自动处理。
自定义线程的RunLoop:对于自定义线程,开发者需要手动创建和管理RunLoop,以确保线程能够处理事件。可以通过
CFRunLoopRun()函数启动RunLoop。性能优化:使用RunLoop可以提高应用的性能,因为它允许线程在空闲时进入休眠状态,减少CPU的使用。
总结来说,RunLoop是线程的一个重要组成部分,它使得线程能够有效地处理事件和消息,从而提高应用的响应性和性能。
12. setNeedsDisplay 和 layoutIfNeeded 两者是什么关系?
setNeedsDisplay 和 layoutIfNeeded 是 iOS 开发中与视图更新相关的两个方法,但它们的作用和使用场景有所不同。
setNeedsDisplay
-
作用:调用
setNeedsDisplay方法会标记视图为“需要重绘”。这意味着在下一个绘制周期中,系统会调用视图的drawRect:方法来重新绘制视图。 - 使用场景:当你需要更新视图的内容(例如,改变图形、文本等)时,可以调用这个方法。它不会立即重绘视图,而是将重绘请求放入队列,等待系统在合适的时机处理。
layoutIfNeeded
-
作用:调用
layoutIfNeeded方法会强制视图立即布局其子视图。如果视图的布局需要更新(例如,子视图的大小或位置发生变化),这个方法会立即执行布局更新。 - 使用场景:当你需要确保视图的布局是最新的,特别是在需要立即获取子视图的框架或位置时,可以使用这个方法。
关系
-
setNeedsDisplay和layoutIfNeeded都是用于更新视图的,但它们关注的方面不同:前者关注的是视图的绘制,后者关注的是视图的布局。 - 在某些情况下,你可能会同时使用这两个方法。例如,当你改变了视图的内容并且需要重新布局子视图时,可以先调用
setNeedsDisplay来标记需要重绘,然后调用layoutIfNeeded来确保子视图的布局是最新的。
总结来说,setNeedsDisplay 是关于绘制的,而 layoutIfNeeded 是关于布局的。
12. ViewController的生命周期
在iOS开发中,UIViewController 的生命周期是通过一系列的方法来管理的,这些方法在视图控制器的生命周期中的不同阶段被调用。了解这些方法是非常重要的,因为它们允许你在正确的时间执行适当的操作,比如加载数据、更新用户界面等。下面是主要的生命周期方法:
loadView()
当视图控制器需要其视图时被调用,用于创建视图。你很少需要重写这个方法,因为大多数视图是在Storyboard或Interface Builder中创建的。viewDidLoad()
在视图控制器的视图被加载进内存后调用。这是进行初始化操作的好地方,比如设置默认值、初始化网络请求等。viewWillAppear(_:)
在视图控制器的视图即将加入视图层级时调用。用于对视图做一些显示前的最终更改,比如更新界面元素、方向、状态栏样式等。viewWillLayoutSubviews()
在视图控制器的视图将要布局子视图之前调用。如果你需要调整布局,这是一个调整子视图位置的好时机。viewDidLayoutSubviews()
在视图控制器的视图已经布局子视图后调用。如果你需要知道子视图的大小和位置,这是一个好时机。viewDidAppear(_:)
在视图控制器的视图已经加入到视图层级并且动画结束时调用。可以在这里开始动画、启动定时器、响应显示视图的事件。viewWillDisappear(_:)
在视图控制器的视图即将从视图层级中移除时调用。用于执行一些清理工作,比如取消网络请求、保存编辑内容、隐藏键盘等。viewDidDisappear(_:)
在视图控制器的视图已经从视图层级中移除后调用。可以在这里停止相关的动画或是清理资源。
理解这些方法及其调用顺序可以帮助你更好地管理视图的加载、显示和卸载。
13. int和nsinteger的区别
在iOS开发中,int 和 NSInteger 之间的主要区别在于它们的平台依赖性和类型安全性。
-
平台依赖性:
-
int是一个标准的 C 语言类型,其大小(通常是32位或64位)取决于编译它的系统的架构。在32位系统上,int通常是32位,在64位系统上也通常是32位。 -
NSInteger是一个在 Foundation 框架中定义的类型,它在不同的平台上有不同的大小。在32位系统上,NSInteger是32位的,在64位系统上,NSInteger是64位的。这使得NSInteger在不同设备上更加灵活和适应性强。
-
-
类型安全性:
- 使用
NSInteger可以提高代码的类型安全性,因为它是与 Objective-C 和 Swift 的其他部分更好地集成的 Cocoa 类型。这意味着在使用 Cocoa 或 Cocoa Touch API 时,使用NSInteger可以减少类型转换错误和兼容性问题。
- 使用
-
使用场景:
- 在涉及到 Cocoa API 的 iOS 应用开发中,推荐使用
NSInteger。这是因为大多数 Foundation 框架的函数和方法都使用NSInteger类型。 - 对于与硬件或底层 C 语言库直接交互的操作,可能更倾向于使用
int,因为这些库可能会指定使用标凈的 C 类型。
- 在涉及到 Cocoa API 的 iOS 应用开发中,推荐使用
总结来说,如果你的开发工作主要涉及到使用 Cocoa 框架,那么使用 NSInteger 是更好的选择。如果你需要确保与某些旧的 C 语言库的兼容性,或者在非 Cocoa 环境中工作,使用 int 可能更合适。
14. 类和实例的区别
1. 类(Class)
- 类是一个抽象的模板或蓝图
- 定义了一类对象的属性和方法
- 使用关键字
@interface和@implementation来声明和实现 - 类本身不占用内存空间(除了类的元数据)
示例:
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, assign) NSInteger age;
- (void)sayHello;
@end
2. 实例(Instance)
- 是类的具体对象
- 根据类的定义创建,占用实际的内存空间
- 包含实际的数据
- 使用
alloc和init方法创建
示例:
// 创建实例
Person *person1 = [[Person alloc] init];
person1.name = @"张三";
person1.age = 25;
// 另一个实例
Person *person2 = [[Person alloc] init];
person2.name = @"李四";
person2.age = 30;
3. 主要区别
-
内存占用
- 类:不占用数据内存空间
- 实例:每个实例都会占用独立的内存空间
-
使用方式
- 类方法:使用
+号声明,通过类名调用 - 实例方法:使用
-号声明,通过实例调用
- 类方法:使用
-
数据存储
- 类:定义属性和方法的模板
- 实例:存储具体的属性值
-
多个实例
- 一个类可以创建多个不同的实例
- 每个实例都有自己独立的属性值
示例代码:类方法和实例方法的区别
@interface Person : NSObject
// 类方法
+ (void)classMethod;
// 实例方法
- (void)instanceMethod;
@end
// 调用方式
[Person classMethod]; // 类方法调用
Person *person = [[Person alloc] init];
[person instanceMethod]; // 实例方法调用
总结
- 类是抽象的模板,定义了对象的结构和行为
- 实例是类的具体化,包含实际的数据
- 一个类可以创建多个实例,每个实例都是独立的
- 类方法属于类本身,实例方法属于具体的实例
15. 通知是同步还是异步
在 iOS 中,通知(NSNotification)的发送可以是同步的,也可以是异步的,这取决于你使用的方法:
同步通知
// 同步发送通知
[[NSNotificationCenter defaultCenter] postNotification:notification];
// 或
[[NSNotificationCenter defaultCenter] postNotificationName:@"NotificationName" object:nil];
同步通知特点:
- 通知发送后会阻塞当前线程
- 等待所有观察者都处理完通知后才会继续执行
- 观察者的处理方法在发送通知的同一线程中执行
异步通知
// 异步发送通知
[[NSNotificationQueue defaultQueue] enqueueNotification:notification
postingStyle:NSPostASAP];
异步通知特点:
- 通知发送后不会阻塞当前线程
- 通知会被加入到通知队列中
- 系统会在当前运行循环的合适时机发送通知
使用建议
- 大多数情况下使用同步通知就足够了
- 如果通知处理耗时较长,建议在观察者的处理方法中使用异步处理
- 如果确实需要异步通知,可以使用 NSNotificationQueue
- iOS 10 之后,也可以使用
NotificationCenter.default.post(name:object:userInfo:)配合 DispatchQueue 来实现异步通知
// Swift 异步发送通知示例
DispatchQueue.global().async {
NotificationCenter.default.post(name: NSNotification.Name("NotificationName"),
object: nil)
}
需要注意的是,无论是同步还是异步通知,都要注意避免循环引用和内存泄漏问题,记得在适当的时机移除观察者。
16. block为什么要用copy修饰
在 iOS 中使用 copy 修饰 block 的主要原因如下:
- 内存管理安全性:
- block 在创建时默认是在栈上分配的
- 当 block 所在的作用域结束时,栈上的 block 就会被释放
- 使用
copy修饰符可以将 block 从栈复制到堆上,确保 block 在作用域结束后仍然可用
- 防止野指针:
- 如果使用
assign或weak,当 block 从栈上释放后,指针就会变成野指针 - 使用
copy可以保证 block 的生命周期与对象同步
举例说明:
@interface MyClass : NSObject
@property (nonatomic, copy) void (^myBlock)(void);
@end
@implementation MyClass
- (void)someMethod {
// 正确的做法:使用 copy
self.myBlock = ^{
NSLog(@"This is safe");
};
// 如果用 assign,当方法结束后可能导致崩溃
// self.myBlock = ^{
// NSLog(@"This might crash");
// };
}
@end
- ARC 的处理:
- 在 ARC 环境下,编译器会自动将 block 从栈复制到堆
- 但使用
copy修饰符仍然是最佳实践,因为:- 可以明确表达代码意图
- 保证在 MRC 环境下也能正常工作
- 与苹果的编程规范保持一致
需要注意的是,在 ARC 环境下,以下情况编译器会自动将 block 复制到堆上:
- block 作为方法的返回值
- block 被赋值给
__strong修饰的变量 - block 作为 Cocoa API 中方法名含有
using、with、perform等的参数 - block 作为 GCD API 的参数
总之,使用 copy 修饰 block 是一种安全和标准的做法,可以避免很多潜在的内存问题。
17. 代理用什么关键字修饰,为什么,底层实现原理
代理属性修饰符
通常我们用 weak 来修饰代理属性:
@property (nonatomic, weak) id<MyProtocol> delegate;
为什么要用 weak?
-
避免循环引用
- 代理模式中,对象 A 持有代理属性指向对象 B,而对象 B 往往也会持有对象 A
- 如果使用 strong,会造成循环引用(retain cycle),导致内存泄漏
- weak 不会增加引用计数,可以避免这个问题
-
符合代理设计模式的思想
- 代理对象不应该被 delegate 属性所持有
- delegate 更像是一个临时的协作者,而非从属关系
底层实现原理
-
weak 的实现机制:
- 系统维护了一个 weak 表(hash 表)
- key 是所指对象的地址,value 是 weak 指针的地址数组
- 当对象被释放时,会通过对象地址在 weak 表中找到所有指向它的 weak 指针
- 将这些 weak 指针自动置为 nil
-
具体流程:
{ id obj = [[NSObject alloc] init]; id __weak weakPtr = obj; // 1. 系统将 weakPtr 存入 weak 表 // 2. weak 表的 key 是 obj 地址 // 3. value 是所有指向 obj 的 weak 指针地址 } // 当 obj 被释放时: // 1. 对象的 dealloc 被调用 // 2. 在 weak 表中查找所有指向 obj 的 weak 指针 // 3. 将所有这些 weak 指针置为 nil // 4. 从 weak 表中清除 obj 的记录
特殊情况
-
NSProxy 的代理可以用 strong
- NSProxy 是一个抽象类,用于转发消息
- 它不会造成循环引用问题
-
单例对象的代理可以用 strong
- 单例的生命周期与应用程序相同
- 不会有内存管理问题
注意事项
-
使用
weak修饰的代理可能在使用时已经被释放- 使用前最好先判断是否为 nil
if ([self.delegate respondsToSelector:@selector(someMethod)]) { [self.delegate someMethod]; } -
不要使用
assign- assign 用于基本数据类型
- 对于对象类型,被释放后会产生野指针
18. 分类底层为什么不能添加成员变量,实现原理
1. 分类的本质
分类(Category)是 Objective-C 运行时的一个特性,它的本质是一个 category_t 的结构体:
struct category_t {
const char *name; // 分类名
classref_t cls; // 分类所属的类
struct method_list_t *instanceMethods; // 实例方法列表
struct method_list_t *classMethods; // 类方法列表
struct protocol_list_t *protocols; // 协议列表
struct property_list_t *instanceProperties; // 属性列表
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
};
2. 为什么不能添加成员变量
主要有以下几个原因:
-
内存布局问题
- 对象的内存布局是在编译期就确定的
- 成员变量的内存偏移量也是编译期确定
- 运行时添加成员变量会破坏原有的内存布局
-
结构体限制
- 从上面的
category_t结构体可以看出,分类中根本没有存储成员变量的字段 - 只有方法,属性,协议等字段
- 从上面的
-
运行时加载机制
- 分类是在运行时才被加载的
- 类的内存布局在运行前就已经确定,包括:
- 实例变量的大小
- 实例变量的布局
- 实例变量的偏移量
3. 解决方案
虽然不能直接添加成员变量,但可以通过以下方式变通:
- 使用关联对象(Associated Object)
#import <objc/runtime.h>
@interface NSObject (MyCategory)
@property (nonatomic, strong) NSString *name;
@end
@implementation NSObject (MyCategory)
- (void)setName:(NSString *)name {
objc_setAssociatedObject(self, @selector(name), name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
return objc_getAssociatedObject(self, @selector(name));
}
@end
- 使用静态变量配合字典
@implementation NSObject (MyCategory)
static NSMutableDictionary *_nameMap = nil;
- (void)setName:(NSString *)name {
if (!_nameMap) {
_nameMap = [NSMutableDictionary dictionary];
}
[_nameMap setObject:name forKey:@(self.hash)];
}
- (NSString *)name {
return [_nameMap objectForKey:@(self.hash)];
}
@end
4. 总结
- 分类不能添加成员变量是由于编译期和运行时的内存布局限制
- 可以使用关联对象或其他方式来模拟成员变量的功能
- 关联对象是最常用的解决方案,它利用了运行时的特性
- 使用关联对象时要注意内存管理问题