iOS开发面试题(三)

1. id 和 instanceType 有什么区别?

在iOS开发中,idinstancetype是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开发中,selfsuper是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

    • 指向当前对象的父类,允许访问父类的方法和属性。
    • 用于在子类中调用父类的方法,确保父类的逻辑得以执行。

在实际开发中,selfsuper的使用场景各有不同,理解它们的区别有助于更好地管理对象的行为和继承关系。

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 的情况:

  1. 避免循环引用:当两个对象互相持有对方时,会导致循环引用,造成内存泄漏。使用 weak 可以打破这种循环,因为 weak 引用不会增加对象的引用计数。
  2. 代理模式:在代理模式中,通常会将代理对象声明为 weak,以避免被代理对象强引用,从而导致内存泄漏。
  3. 可选引用weak 引用可以为 nil,当被引用的对象被释放时,weak 引用会自动变为 nil,这在处理可选对象时非常有用。

weakassign 的不同:

  • 内存管理

    • weak:使用 ARC(自动引用计数)时,weak 引用不会增加对象的引用计数。当对象被释放时,weak 引用会自动设置为 nil
    • assignassign 是一个简单的指针赋值,不会管理对象的内存。它适用于基本数据类型(如 intfloat 等),但不适合对象引用,因为它不会处理对象的生命周期。
  • 使用场景

    • weak 主要用于对象引用,尤其是在需要避免循环引用的情况下。
    • assign 适用于基本数据类型或不需要内存管理的场景。

总结:

在 iOS 开发中,通常推荐使用 weak 来管理对象引用,特别是在涉及到对象之间的关系时,以确保内存的有效管理和避免内存泄漏。

5. block为什么要用 copy 关键字?

在 iOS 开发中,使用 copy 关键字来修饰 block 是因为 block 在 Objective-C 中是对象,且它们的内存管理方式与普通的对象不同。

原因如下:

  1. 内存管理:当你创建一个 block 时,它最初是存储在栈上的。如果你将这个 block 赋值给一个属性而不使用 copy,那么当这个 block 超出其作用域时,它将被销毁,导致你可能会访问到一个已经被释放的内存区域。

  2. 对象的复制:使用 copy 关键字时,block 会被复制到堆上,这样即使原始的栈内存被释放,堆上的 block 仍然存在,确保了 block 的有效性。

  3. 避免潜在的错误:如果不使用 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

这意味着:

  1. 当你尝试对 array 进行修改(例如添加或删除元素)时,会导致运行时错误,因为你实际上在操作一个不可变的数组。
  2. 如果你希望 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 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nilself,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。

3. Normal forwarding(标准消息转发)

这一步是Runtime最后一次给你挽救的机会。首先它会发送 -methodSignatureForSelector:消息获得函数的参数和返回值类型。如果 -methodSignatureForSelector: 返回nilRuntime则会发出 -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 中对 selfsuper 的理解。

super关键字,有以下几点需要注意:

  • receiver还是当前类对象,而不是父类对象;
  • super这里的含义就是优先去父类的方法列表中去查实现,很多问题都是父类中其实也没有实现,还是去根类里 去找实现,这种情况下时,其实跟直接调用self的效果是一致的。

下面做详细介绍:

我们都知道:self 是类的隐藏参数,指向当前调用方法的这个类的实例。那 super 呢?

很多人会想当然的认为“superself 类似,应该是指向父类的指针吧!”。这是很普遍的一个误区。其实 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"]);
}

解释

  1. 获取类:使用[MyClass class]获取目标类。
  2. 添加实例变量:使用class_addIvar函数添加新的实例变量。需要提供变量名、大小、对齐和类型编码。
  3. 使用新变量:通过KVC(键值编码)设置和获取新变量的值。

注意事项

  • 动态添加实例变量可能会导致代码的可读性和可维护性降低,因此应谨慎使用。
  • 这种方法主要适用于Objective-C,Swift不支持在运行时动态添加实例变量。

11. RunLoop和线程有什么关系?

在iOS开发中,RunLoop和线程之间有着密切的关系。以下是它们之间的主要联系:

  1. 线程的运行机制:每个线程都有一个RunLoop。RunLoop是一个循环,它使线程能够处理事件和消息。没有RunLoop的线程在执行完任务后会立即退出,而有了RunLoop,线程可以在等待事件的同时保持活跃状态。

  2. 事件处理:RunLoop负责管理输入源(如用户输入、网络请求等)和定时器。它会在有事件到达时唤醒线程,处理这些事件,然后继续等待下一个事件。

  3. 主线程的RunLoop:在iOS中,主线程(UI线程)有一个默认的RunLoop,它会处理所有的UI事件和用户交互。开发者通常不需要手动管理主线程的RunLoop,因为UIKit会自动处理。

  4. 自定义线程的RunLoop:对于自定义线程,开发者需要手动创建和管理RunLoop,以确保线程能够处理事件。可以通过CFRunLoopRun()函数启动RunLoop。

  5. 性能优化:使用RunLoop可以提高应用的性能,因为它允许线程在空闲时进入休眠状态,减少CPU的使用。

总结来说,RunLoop是线程的一个重要组成部分,它使得线程能够有效地处理事件和消息,从而提高应用的响应性和性能。

12. setNeedsDisplay 和 layoutIfNeeded 两者是什么关系?

setNeedsDisplaylayoutIfNeeded 是 iOS 开发中与视图更新相关的两个方法,但它们的作用和使用场景有所不同。

setNeedsDisplay

  • 作用:调用 setNeedsDisplay 方法会标记视图为“需要重绘”。这意味着在下一个绘制周期中,系统会调用视图的 drawRect: 方法来重新绘制视图。
  • 使用场景:当你需要更新视图的内容(例如,改变图形、文本等)时,可以调用这个方法。它不会立即重绘视图,而是将重绘请求放入队列,等待系统在合适的时机处理。

layoutIfNeeded

  • 作用:调用 layoutIfNeeded 方法会强制视图立即布局其子视图。如果视图的布局需要更新(例如,子视图的大小或位置发生变化),这个方法会立即执行布局更新。
  • 使用场景:当你需要确保视图的布局是最新的,特别是在需要立即获取子视图的框架或位置时,可以使用这个方法。

关系

  • setNeedsDisplaylayoutIfNeeded 都是用于更新视图的,但它们关注的方面不同:前者关注的是视图的绘制,后者关注的是视图的布局。
  • 在某些情况下,你可能会同时使用这两个方法。例如,当你改变了视图的内容并且需要重新布局子视图时,可以先调用 setNeedsDisplay 来标记需要重绘,然后调用 layoutIfNeeded 来确保子视图的布局是最新的。

总结来说,setNeedsDisplay 是关于绘制的,而 layoutIfNeeded 是关于布局的。

12. ViewController的生命周期

在iOS开发中,UIViewController 的生命周期是通过一系列的方法来管理的,这些方法在视图控制器的生命周期中的不同阶段被调用。了解这些方法是非常重要的,因为它们允许你在正确的时间执行适当的操作,比如加载数据、更新用户界面等。下面是主要的生命周期方法:

  1. loadView()
    当视图控制器需要其视图时被调用,用于创建视图。你很少需要重写这个方法,因为大多数视图是在Storyboard或Interface Builder中创建的。

  2. viewDidLoad()
    在视图控制器的视图被加载进内存后调用。这是进行初始化操作的好地方,比如设置默认值、初始化网络请求等。

  3. viewWillAppear(_:)
    在视图控制器的视图即将加入视图层级时调用。用于对视图做一些显示前的最终更改,比如更新界面元素、方向、状态栏样式等。

  4. viewWillLayoutSubviews()
    在视图控制器的视图将要布局子视图之前调用。如果你需要调整布局,这是一个调整子视图位置的好时机。

  5. viewDidLayoutSubviews()
    在视图控制器的视图已经布局子视图后调用。如果你需要知道子视图的大小和位置,这是一个好时机。

  6. viewDidAppear(_:)
    在视图控制器的视图已经加入到视图层级并且动画结束时调用。可以在这里开始动画、启动定时器、响应显示视图的事件。

  7. viewWillDisappear(_:)
    在视图控制器的视图即将从视图层级中移除时调用。用于执行一些清理工作,比如取消网络请求、保存编辑内容、隐藏键盘等。

  8. viewDidDisappear(_:)
    在视图控制器的视图已经从视图层级中移除后调用。可以在这里停止相关的动画或是清理资源。

理解这些方法及其调用顺序可以帮助你更好地管理视图的加载、显示和卸载。

13. int和nsinteger的区别

在iOS开发中,intNSInteger 之间的主要区别在于它们的平台依赖性和类型安全性。

  1. 平台依赖性:

    • int 是一个标准的 C 语言类型,其大小(通常是32位或64位)取决于编译它的系统的架构。在32位系统上,int 通常是32位,在64位系统上也通常是32位。
    • NSInteger 是一个在 Foundation 框架中定义的类型,它在不同的平台上有不同的大小。在32位系统上,NSInteger 是32位的,在64位系统上,NSInteger 是64位的。这使得NSInteger 在不同设备上更加灵活和适应性强。
  2. 类型安全性:

    • 使用 NSInteger 可以提高代码的类型安全性,因为它是与 Objective-C 和 Swift 的其他部分更好地集成的 Cocoa 类型。这意味着在使用 Cocoa 或 Cocoa Touch API 时,使用 NSInteger 可以减少类型转换错误和兼容性问题。
  3. 使用场景:

    • 在涉及到 Cocoa API 的 iOS 应用开发中,推荐使用 NSInteger。这是因为大多数 Foundation 框架的函数和方法都使用 NSInteger 类型。
    • 对于与硬件或底层 C 语言库直接交互的操作,可能更倾向于使用 int,因为这些库可能会指定使用标凈的 C 类型。

总结来说,如果你的开发工作主要涉及到使用 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)

  • 是类的具体对象
  • 根据类的定义创建,占用实际的内存空间
  • 包含实际的数据
  • 使用 allocinit 方法创建

示例:

// 创建实例
Person *person1 = [[Person alloc] init];
person1.name = @"张三";
person1.age = 25;

// 另一个实例
Person *person2 = [[Person alloc] init];
person2.name = @"李四";
person2.age = 30;

3. 主要区别

  1. 内存占用

    • 类:不占用数据内存空间
    • 实例:每个实例都会占用独立的内存空间
  2. 使用方式

    • 类方法:使用 + 号声明,通过类名调用
    • 实例方法:使用 - 号声明,通过实例调用
  3. 数据存储

    • 类:定义属性和方法的模板
    • 实例:存储具体的属性值
  4. 多个实例

    • 一个类可以创建多个不同的实例
    • 每个实例都有自己独立的属性值

示例代码:类方法和实例方法的区别

@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];

异步通知特点:

  • 通知发送后不会阻塞当前线程
  • 通知会被加入到通知队列中
  • 系统会在当前运行循环的合适时机发送通知

使用建议

  1. 大多数情况下使用同步通知就足够了
  2. 如果通知处理耗时较长,建议在观察者的处理方法中使用异步处理
  3. 如果确实需要异步通知,可以使用 NSNotificationQueue
  4. 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 的主要原因如下:

  1. 内存管理安全性
  • block 在创建时默认是在栈上分配的
  • 当 block 所在的作用域结束时,栈上的 block 就会被释放
  • 使用 copy 修饰符可以将 block 从栈复制到堆上,确保 block 在作用域结束后仍然可用
  1. 防止野指针
  • 如果使用 assignweak,当 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
  1. ARC 的处理
  • 在 ARC 环境下,编译器会自动将 block 从栈复制到堆
  • 但使用 copy 修饰符仍然是最佳实践,因为:
    • 可以明确表达代码意图
    • 保证在 MRC 环境下也能正常工作
    • 与苹果的编程规范保持一致

需要注意的是,在 ARC 环境下,以下情况编译器会自动将 block 复制到堆上:

  • block 作为方法的返回值
  • block 被赋值给 __strong 修饰的变量
  • block 作为 Cocoa API 中方法名含有 usingwithperform 等的参数
  • block 作为 GCD API 的参数

总之,使用 copy 修饰 block 是一种安全和标准的做法,可以避免很多潜在的内存问题。

17. 代理用什么关键字修饰,为什么,底层实现原理

代理属性修饰符

通常我们用 weak 来修饰代理属性:

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

为什么要用 weak?

  1. 避免循环引用

    • 代理模式中,对象 A 持有代理属性指向对象 B,而对象 B 往往也会持有对象 A
    • 如果使用 strong,会造成循环引用(retain cycle),导致内存泄漏
    • weak 不会增加引用计数,可以避免这个问题
  2. 符合代理设计模式的思想

    • 代理对象不应该被 delegate 属性所持有
    • delegate 更像是一个临时的协作者,而非从属关系

底层实现原理

  1. weak 的实现机制

    • 系统维护了一个 weak 表(hash 表)
    • key 是所指对象的地址,value 是 weak 指针的地址数组
    • 当对象被释放时,会通过对象地址在 weak 表中找到所有指向它的 weak 指针
    • 将这些 weak 指针自动置为 nil
  2. 具体流程

    {
        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 的记录
    

特殊情况

  1. NSProxy 的代理可以用 strong

    • NSProxy 是一个抽象类,用于转发消息
    • 它不会造成循环引用问题
  2. 单例对象的代理可以用 strong

    • 单例的生命周期与应用程序相同
    • 不会有内存管理问题

注意事项

  1. 使用 weak 修饰的代理可能在使用时已经被释放

    • 使用前最好先判断是否为 nil
    if ([self.delegate respondsToSelector:@selector(someMethod)]) {
        [self.delegate someMethod];
    }
    
  2. 不要使用 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. 为什么不能添加成员变量

主要有以下几个原因:

  1. 内存布局问题

    • 对象的内存布局是在编译期就确定的
    • 成员变量的内存偏移量也是编译期确定
    • 运行时添加成员变量会破坏原有的内存布局
  2. 结构体限制

    • 从上面的 category_t 结构体可以看出,分类中根本没有存储成员变量的字段
    • 只有方法,属性,协议等字段
  3. 运行时加载机制

    • 分类是在运行时才被加载的
    • 类的内存布局在运行前就已经确定,包括:
      • 实例变量的大小
      • 实例变量的布局
      • 实例变量的偏移量

3. 解决方案

虽然不能直接添加成员变量,但可以通过以下方式变通:

  1. 使用关联对象(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
  1. 使用静态变量配合字典
@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. 总结

  • 分类不能添加成员变量是由于编译期和运行时的内存布局限制
  • 可以使用关联对象或其他方式来模拟成员变量的功能
  • 关联对象是最常用的解决方案,它利用了运行时的特性
  • 使用关联对象时要注意内存管理问题
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容