首页 OC面试题整理
文章
取消

OC面试题整理

Objective-C中中strong,weak ,retain,copy,assign的特性及用途,以及它们的相似和区别。

OC 中的 strong, weak, retain, copy, 和 assign 是属性(property)修饰符,用于管理内存和对象引用。下面是它们的详细解释和比较:

  1. strong
    • 用途: 用于ARC(自动引用计数)环境。当你将一个对象赋值给一个strong属性时,这个属性会”拥有”这个对象。这意味着对象的引用计数会增加,保证对象不会在其所有者还需要它时被销毁。
    • 特点: 它是ARC下默认的属性修饰符。主要用于保持对象的生命周期,直到其所有者被销毁。
  2. weak
    • 用途: 也用于ARC。weak属性用于创建一个不拥有对象的引用。当对象的引用计数达到0时,对象会被销毁,而weak属性会自动变为nil,避免悬挂指针。
    • 特点: 常用于防止循环引用,例如在父子关系中的子对象。
  3. retain
    • 用途: 用于非ARC(手动引用计数)环境。当你将一个对象赋值给一个retain属性时,这个属性会增加对象的引用计数。
    • 特点: 类似于strong,但用于非ARC环境。保持对象活跃,直到所有者释放它。
  4. copy
    • 用途: 当对象赋值给copy属性时,会创建该对象的副本。对于字符串和集合类这类可变对象尤其重要,以防止原始对象被修改时影响到属性值。
    • 特点: 常用于保持属性的不可变性,特别是在处理类似NSStringNSArray这样的不可变对象时。
  5. assign
    • 用途: 一般用于基本数据类型(如NSInteger, CGFloat)的属性,因为这些类型不是对象,所以不需要引用计数。
    • 特点: 在ARC环境下,通常不用于对象,因为它不会增加对象的引用计数,可能导致使用悬挂指针。

相似点与区别:

  • 相似点: strong, retain, 和 copy 都是为了在某种程度上”拥有”对象。它们都增加了对象的引用计数(除了copy是对对象的副本进行计数)。
  • 区别: strongretain 在处理对象的所有权时有所不同,主要体现在ARC和非ARC环境的使用上。weakassign 都不增加对象的引用计数,但weak用于对象,而assign通常用于基本数据类型。copy创建对象的副本,与其他修饰符管理同一个对象不同。

__weak与weak的区别?

__weak 是一个类型限定符,用于在变量声明中指明一个弱引用,而 weak 是一个属性关键字,用于在属性声明中指明一个弱引用。在实际使用中,__weak 主要用于局部变量或者实例变量,而 weak 主要用于属性。

  1. __weak 这是一个类型限定符,用于修饰变量的类型。当你声明一个变量为弱引用时,你会在其类型前使用 __weak。这告诉编译器该变量是一个弱引用。弱引用不会增加对象的引用计数,并且当所引用的对象被销毁时,弱引用会自动被设置为 nil。这有助于防止循环引用和内存泄漏。

    1
    
    __weak MyClass *weakObject = someObject;
    
  2. weak 在属性声明中,weak 是一个属性关键字,用于指定属性的存储语义。它表明该属性是一个弱引用。当使用 weak 声明一个属性时,相应的实例变量也会被自动设置为弱引用。

    例如:

    1
    
    @property (weak) MyClass *weakProperty;
    

如何理解OC中的深拷贝和浅拷贝?

因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。

1. 对非集合类对象的copy操作:

在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。

1
2
3
4
[immutableObject copy] // 浅拷贝
[immutableObject mutableCopy] //深拷贝
[mutableObject copy] //深拷贝
[mutableObject mutableCopy] //深拷贝

根据上面的结论,我们也可以总结出规律:

对于非集合类对象而言,从不可变转换到另一个不可变,因为没必要创建一个新对象出来, 所以是浅拷贝。 而不可变与可变对象的互相转换过程中、从一个可变到另一个可变, 为了不影响可变对象的可变特性,必须要创建一个新对象出来,所以是深拷贝。

2、集合类对象的copy与mutableCopy

从集合内的元素的角度而言,对任何集合对象(可变和不可变集合)进行的 copy 与 mutableCopy 操作都可以称之为浅拷贝。在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。

1
2
3
4
[immutableCollectionObject copy] // 浅拷贝
[immutableCollectionObject mutableCopy] //浅拷贝,也可以称之为“单层深拷贝”。
[mutableCollectionObject copy] //浅拷贝,也可以称之为“单层深拷贝”。
[mutableCollectionObject mutableCopy] //浅拷贝,也可以称之为“单层深拷贝”。

OC中的typedef

在 OC 中,typedef 是一个关键字,用于为现有类型创建一个新的别名。这是一个来自 C 语言的特性,Objective-C 作为 C 的一个超集,同样支持这个功能。使用 typedef 可以增加代码的可读性和维护性。以下是一些关于 typedef 使用的要点和例子:

基本用法

  • 定义新类型名typedef 允许你为复杂的类型声明创建一个简单的别名。例如,你可以为一个特定的结构体类型定义一个新的名称,而不需要每次都写完整的结构体定义。

    1
    2
    3
    4
    5
    6
    
    //别名为Vector3D
    typedef struct {
        float x, y, z;
    } Vector3D;
      
    Vector3D vector;
    
  • 简化函数指针:对于函数指针的类型声明,typedef 可以使用这个别名来声明函数指针,而不需要写出完整的函数指针类型。

    1
    2
    3
    4
    
    //别名为MyFunctionPointer
    typedef void (*MyFunctionPointer)(int, float);
      
    MyFunctionPointer funcPtr;
    
  • 增加代码可移植性typedef 也常用于隐藏平台或实现细节,提高代码的可移植性。

在 Objective-C 中的应用

  • Block 类型定义:在 Objective-C 中,typedef 常用于定义 block 类型。这让你能够在声明方法或属性时使用别名,而不是每次都写出完整的 block 类型声明。

    1
    2
    3
    4
    
    //别名为CompletionBlock
    typedef void (^CompletionBlock)(BOOL success, NSError *error);
      
    - (void)performAsyncOperationWithCompletion:(CompletionBlock)completion;
    
  • 自定义类型:为自定义类或者结构体创建类型别名,可以使得代码在引用这些类型时更加简洁。

    1
    2
    3
    4
    
    //别名为StringArray,这个数组专门用于存储 NSString 对象
    typedef NSArray<NSString *> StringArray;
      
    StringArray *array = @[@"Hello", @"World"];
    

OC中直接使用常量和使用const有何区别?

使用const提供了更好的类型安全、作用域控制和编译时检查,这通常使得代码更健壮、可维护。然而,在某些情况下,直接常量可能更适合,例如定义预处理器指令或条件编译。

  1. 作用域:
    • 直接常量(例如#define CONSTANT 5)是通过预处理器定义的,它们在编译前就被替换为其值。这意味着它们没有类型信息,也没有作用域。它们在整个程序中都是可见的,只要包含了定义它们的头文件。
    • 使用const(例如const int Constant = 5;)定义的常量是在编译时创建的。它们有明确的类型和作用域。const常量的作用域限制在声明它们的文件、函数或块内,除非它们被声明为extern
  2. 类型安全:
    • 直接常量没有类型信息,所以不提供类型安全。如果使用它们的方式不正确(例如将整数常量用作指针),编译器可能不会发出警告。
    • 使用const声明的常量有具体的类型,这提供了类型安全。如果尝试错误地使用这些常量,编译器将发出警告或错误。
  3. 编译时检查:
    • 直接常量在预处理阶段处理,编译器不会对它们的使用进行检查。这可能导致一些意外的错误,特别是在复杂的宏替换中。
    • const常量在编译时处理,编译器将对它们进行类型检查和其他错误检查。这有助于发现潜在的问题。

@property 的本质是什么?

在 OC 中,@property 是一个非常重要的特性,用于声明类的属性。它的本质可以从几个方面来理解:

  1. 语法糖: @property 本质上是一种语法糖。它简化了 getter 和 setter 方法的声明和实现。在没有 @property 的时代,你需要手动编写 getter 和 setter 方法来访问和修改实例变量。而 @property 自动为你生成这些方法。
  2. 存取器方法: 当你声明一个 @property,编译器会自动为你生成相应的存取器方法,即 getter 和 setter 方法(除非你指定了 readonly,这种情况下只生成 getter 方法)。这些方法允许你以安全和一致的方式访问和修改属性的值。
  3. 内存管理: 在 ARC 之前,@property 也用于指定内存管理语义,如 assign, retain, copy。在 ARC 环境下,这些关键词被 strong, weak, copy 替代,它们帮助编译器理解如何自动管理对象的内存。
  4. 线程安全和其他特性: @property 还可以指定其他特性,如 nonatomic。默认情况下,生成的存取器方法是线程安全的,但这可能会导致性能损失。使用 nonatomic 可以生成非线程安全的版本,以提高性能。
  5. 合成实例变量(Synthesized Instance Variables): 当使用 @property 时,编译器还会自动为你合成(或自动生成)相应的实例变量(通常是以 _ 开头的变量,例如对于一个名为 name 的属性,合成的实例变量通常是 _name)。这意味着你不需要手动声明实例变量。
  6. KVO 和 KVC 兼容性: 通过使用 @property 自动生成的 getter 和 setter 方法,你的类自动支持键值观察(KVO)和键值编码(KVC)。

@property 后面可以有哪些修饰符?

在 Objective-C 中,@property 声明可以跟随一系列的修饰符,这些修饰符影响着属性的内存管理、线程安全性、存取方式等方面。以下是一些常见的修饰符:

  1. 内存管理修饰符:
    • strong(或在 MRC 下的 retain): 表示属性保持对对象的强引用。
    • weak: 表示属性是一个弱引用,当所引用的对象被释放时,属性自动置为 nil
    • assign: 用于基本数据类型,如整数和浮点数,以及不支持自动引用计数的对象。
    • copy: 当对象赋值给属性时,赋值的是对象的副本。
  2. 读写修饰符:
    • readonly: 只生成 getter 方法,不生成 setter 方法。
    • readwrite: (默认)生成 getter 和 setter 方法。
  3. 原子性修饰符:
    • atomic: (默认)确保属性的存取是线程安全的,但可能影响性能。
    • nonatomic: 表示存取器不保证线程安全,但性能更优。
  4. 存取器名修饰符:
    • getter=<name>: 指定自定义的 getter 方法名称。
    • setter=<name>: 指定自定义的 setter 方法名称。
  5. 其他修饰符:
    • nullable: 表示属性可以是 nil
    • nonnull: 表示属性不能是 nil
    • class: 用于声明类属性,而非实例属性。

例如,一个属性声明可能看起来像这样:

1
@property (nonatomic, strong, getter=theName, nullable) NSString *name;

这声明了一个名为 name 的属性,具有 nonatomicstrong 修饰符,自定义的 getter 方法名为 theName,并且这个属性可以被赋予 nil 值。

@protocol 和 category 中如何使用 @property?

在协议中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的是希望遵守我协议的对象能实现该属性。

1
2
3
@protocol MyProtocol <NSObject> 
@property (nonatomic, strong) NSString *requiredString; 
@end

在类别中使用 @property 有一定的限制。虽然你可以在类别中声明属性,但类别不能合成属性的存取器方法,也不能合成实例变量。这意味着你需要用运行时函数 objc_setAssociatedObjectobjc_getAssociatedObject 手动实现存取器方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface MyClass (MyCategory)
@property (nonatomic, strong) NSString *categoryString;
@end

@implementation MyClass (MyCategory)

- (NSString *)categoryString {
    return objc_getAssociatedObject(self, @selector(categoryString));
}

- (void)setCategoryString:(NSString *)categoryString {
    objc_setAssociatedObject(self, @selector(categoryString), categoryString, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

@synthesize和@dynamic分别有什么作用?什么情况下不会autosynthesis(自动合成)?

  • @synthesize 主要用于指示编译器自动合成属性的存取器方法和实例变量。

    在早期的 Objective-C 版本中,如果你声明了一个 @property,你需要显式地使用 @synthesize 来指示编译器为该属性自动生成 getter 和 setter 方法。在较新的 Objective-C 版本中(特别是在引入自动引用计数(ARC)之后),如果你不提供 @synthesize@dynamic,编译器会默认为 @property 自动合成存取器方法和实例变量,因此 @synthesize 的使用变得较少。

  • @dynamic 用于告诉编译器属性的存取器方法将在其他地方实现或在运行时动态提供。

    @dynamic 告诉编译器,属性的存取器方法将由用户在别处提供,或者在运行时动态提供,编译器不应自动合成它们。使用 @dynamic 时,你负责在类的实现中提供或确保这些方法的存在。这通常用于动态类型的场景,比如 Core Data 的属性,或者在使用 Objective-C 的运行时特性来动态处理方法时。

    1
    2
    3
    4
    5
    
    //用法
    @property (nonatomic, strong) NSString *name;
    @synthesize name;
    //或者
    @dynamic name;
    
    以下情况不会自动合成:
    1. 同时重写了 setter 和 getter 时
    2. 重写了只读属性的 getter 时
    3. 使用了 @dynamic 时
    4. 在 @protocol 中定义的所有属性
    5. 在 category 中定义的所有属性
    6. 重写(overridden)的属性

    一个objc对象如何进行内存布局?

    OC 对象的内存布局受其运行时和面向对象特性影响。在 OC 中,每个对象都是一个结构体,其内存布局通常包括以下部分:

    1. isa指针
      • 每个 Objective-C 对象的最开始部分是一个称为 isa 的指针。这个指针指向对象的类,它告诉 Objective-C 运行时系统这个对象是什么类型。
      • isa 指针使得运行时能够在对象上执行对象相关的操作,如方法调用、对象复制等。
    2. 对象实例变量
      • isa 指针之后,对象的内存布局包含了其实例变量。
      • 这些变量的顺序和大小由对象的类定义中的属性和变量声明确定。
      • 子类的实例变量会跟在父类的实例变量之后,形成一个连续的内存块。
    3. 对齐和填充
      • 为了提高访问效率,编译器可能会在实例变量之间插入填充(padding)来确保对齐。
      • 对齐意味着实例变量的地址符合特定的倍数,这是由平台的硬件架构决定的。
    4. 属性
      • 虽然属性在语法上是类的一部分,但从内存布局的角度来看,它们实际上是通过实例变量实现的。
      • 因此,定义的属性最终会反映为对象内存布局中的实例变量。
    5. 关联对象
      • Objective-C 允许通过关联对象机制向现有类动态添加数据。
      • 这些数据实际上不存储在对象本身的内存布局中,而是存储在一个全局的、由运行时管理的表中。
    6. 方法列表、协议列表和其他结构
      • 这些不是存储在每个对象的内存中的,而是存储在类对象(Class object)中。
      • 类对象包含了方法列表、协议列表和其他与类相关的信息。

一个objc对象的isa的指针指向什么?有什么作用?

在 OC 中每个对象都有一个 isa 指针。isa 指针的作用和它所指向的内容如下:

  1. 指向内容
    • 对于实例对象,isa 指针指向该对象的类。
    • 对于类对象(即类本身),isa 指针指向元类(metaclass)。同一个类的不同对象,他们的 isa 指针是一样的。
    • 对于元类对象,isa 指针通常指向根元类(root metaclass)。
  2. 作用
    • 类型标识isa 指针允许运行时系统知道对象属于哪个类。这对于执行诸如方法派发等操作至关重要,因为它决定了应该调用哪些方法实现。
    • 方法派发:当给对象发送消息(调用方法)时,OC 运行时使用 isa 指针来查找对象的类,进而查找该类中的方法实现。
    • 继承支持:由于类对象也包含自己的 isa 指针,指向其元类,这支持了 OC 中类与继承体系的实现。元类的概念允许类本身拥有类方法。
    • 运行时动态性isa 指针是 OC 动态特性的基础之一。它允许在运行时改变对象的类或者行为(虽然这种做法并不常见)。
  3. 动态修改
    • 在某些高级用法中,开发者可以动态修改 isa 指针,从而改变对象的类。这可以用于实现诸如安全检查、调试工具或者其他高级功能。
  4. 内存管理
    • 在某些 OC 运行时实现中,isa 指针也用于存储关于对象内存管理的信息,如引用计数的标记位等。

下面的代码输出什么?

1
2
3
4
5
6
7
8
9
10
11
@implementation Son : Father
   - (id)init
   {
       self = [super init];
       if (self) {
           NSLog(@"%@", NSStringFromClass([self class]));
           NSLog(@"%@", NSStringFromClass([super class]));
       }
       return self;
   }
   @end

都输出 Son

在 OC 中,[self class][super class] 实际上是在调用相同的方法。这是因为 super 关键字在 OC 中并不会改变接收消息的对象(即 self),它只是改变了消息派发的方式,使得运行时会跳过当前类的方法实现,转而寻找父类的方法实现。

runtime如何通过selector找到对应的IMP地址?

简单来说,当你在 Objective-C 中调用一个方法时,系统会用这个方法的名字去类的方法列表里找对应的代码。如果找不到,就会在父类中继续找,一直找到或者确定这个方法不存在。

  1. 选择器(Selector):这就是方法的名字。当你调用一个方法时,这个名字被用来找到相应的代码。
  2. 类的结构:每个类都有一个包含其所有方法的列表。这个列表有方法的名字和相应的代码地址(称为方法实现,或 IMP)。
  3. 查找过程
    • 当你调用一个方法时,系统首先查看这个对象的类,看看这个方法名(选择器)是否有对应的代码。
    • 如果没找到,系统会查看这个类的父类,依次向上,直到找到相应的方法或者查找完所有的父类。
  4. 缓存:为了加快速度,一旦一个方法被调用,它的信息就会被存储起来。下次再调用相同的方法时,系统可以直接从缓存中获取,不用再去查找。
  5. 如果找不到:如果系统在类及其父类中都找不到这个方法,还有一些特殊的机制可以让程序员在运行时添加这个方法或者把这个调用转给其他对象处理。

OC中的类方法和实例方法有什么不同之处和相同之处?

不同之处

  1. 定义和调用方式
    • 类方法:定义在类本身上。它们使用加号 + 定义,并且可以通过类名直接调用,而无需创建类的实例。
    • 实例方法:定义在类的实例上。它们使用减号 - 定义,并且必须通过类的实例来调用。
  2. 访问实例变量
    • 类方法不能访问实例变量或调用实例方法,因为它们不是在类的具体实例上下文中执行的。
    • 实例方法可以访问实例变量和调用其他实例方法,因为它们是在类的具体实例上下文中执行的。
  3. 使用场景
    • 类方法通常用于实现那些与特定实例无关的功能,例如创建类的新实例或执行与类相关的通用任务。
    • 实例方法用于执行与特定对象实例相关的操作,如更改对象的状态或响应特定的事件。
  4. 内存管理
    • 类方法不涉及实例的创建和销毁,因此不像实例方法那样直接关联到对象的生命周期和内存管理。

相同之处

  1. 语法和编程风格
    • 无论是类方法还是实例方法,它们在语法上都有相同的结构,包括返回类型、方法名、参数等。
  2. 继承
    • 类方法和实例方法都遵循继承机制。子类可以继承父类的方法,并有能力重写这些方法。
  3. 多态性
    • 类方法和实例方法都支持多态性,即在子类中重写父类方法的能力,这使得您可以在子类中定制或改进方法的行为。

_objc_msgForward函数是做什么的,直接调用它将会发生什么?

在 Objective-C 中,_objc_msgForward 函数是消息转发机制的一部分,用于处理无法正常响应的方法调用。当一个对象收到无法识别的消息(即没有匹配的方法实现)时,_objc_msgForward 可以被用来拦截这个消息并进行自定义处理。这是 Objective-C 动态特性的一个重要方面。

_objc_msgForward 的作用

  1. 消息转发:当一个对象的某个方法被调用,但该对象并未实现这个方法时,_objc_msgForward 用于启动消息转发过程。这允许程序以另一种方式响应这个方法调用,而不是简单地抛出异常。
  2. 自定义处理:通过消息转发,你可以动态地决定如何处理这个未知的消息。例如,你可以转发这个消息给另一个对象处理,或者动态地为这个方法提供一个实现。
  3. 调试和日志记录:在某些情况下,_objc_msgForward 可以用于调试目的,比如记录所有未处理的方法调用。

直接调用 _objc_msgForward

如果你直接调用 _objc_msgForward,而不是让它作为消息转发机制的一部分被自动触发,它将尝试对一个方法调用进行消息转发。但是,由于直接调用并不是响应一个实际的方法调用,这通常会导致程序行为异常或崩溃。

使用场景

通常,你不需要直接操作 _objc_msgForward。它主要用于内部机制,由 OC 运行时自动处理。当你需要自定义方法的响应方式时,通常是通过重写 forwardInvocation:methodSignatureForSelector: 方法来实现消息转发机制的。

runtime如何实现weak变量的自动置nil?

OC 中的 weak 变量是通过 Runtime 的自动引用计数(ARC)机制来管理的。Runtime 实现 weak 变量自动置 nil 的主要机制如下:

  1. 注册 weak 变量
    • 当一个对象被赋值给 weak 属性时,这个属性的地址被注册到一个hash表中,用 weak 指向的对象内存地址作为 key,这个hash表跟踪所有指向该对象的 weak 指针。
    • 这个hash表由 Runtime 管理,确保每个对象都有一个对应的 weak 指针列表。
  2. 对象销毁时的处理
    • 当对象的引用计数降至零并准备被销毁时,Runtime 会查找跟踪这个对象的 weak 指针表。
    • 然后,Runtime 会遍历这个表中的所有 weak 指针地址。
  3. 自动置 nil
    • 对于表中的每个 weak 指针,Runtime 将其自动置为 nil
    • 这样,当对象被销毁后,所有的 weak 引用都会安全地变成 nil,避免了悬挂指针的问题。

能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

  1. 向编译后的类中添加实例变量
    • 不可能:一旦类在编译时被定义后,它的内存布局就固定了,包括实例变量的数量和类型。这是因为编译器需要在编译时确定每个对象的大小,以便在运行时正确地分配和管理内存。
    • 向已经编译的类中添加实例变量会打乱这个固定的内存布局,可能导致内存访问错误、数据损坏或其他未定义的行为。
  2. 向运行时创建的类中添加实例变量
    • 可能,但有限制:在运行时创建的类(使用如 objc_allocateClassPair 创建的类)可以在注册类(objc_registerClassPair)之前添加实例变量。
    • 一旦类被注册后,其内存布局也被固定,此后就不能再添加实例变量了。
    • 这种灵活性允许在动态创建类时定义其属性,但一旦类完成注册,就无法再进行修改,以保持内存布局的一致性。

runloop和线程有什么关系?

  1. 基本关系
    • 每个线程都可以有自己的 RunLoop。
    • RunLoop 是用来管理线程的事件循环的,它使得线程可以在有工作做时忙碌,没有工作做时休眠。
  2. 线程生命周期
    • RunLoop 和线程的生命周期是紧密相连的。当线程被创建时,它并没有一个自动关联的 RunLoop,RunLoop 需要被显式地创建并运行。
    • 一旦 RunLoop 被启动,它会处理输入事件(如用户输入、定时器事件、网络事件等),直到被停止。
    • 如果线程的 RunLoop 没有运行,线程就会退出。
  3. 主线程特殊性
    • 主线程的 RunLoop 会自动被创建和运行,这是因为主线程负责处理 UI 更新、用户输入等关键任务。
    • 对于后台线程,RunLoop 默认不会启动,需要手动管理。
  4. 事件处理
    • RunLoop 负责监视各种事件源,并在事件发生时将控制权交给适当的处理程序。
    • 这意味着线程可以在没有事件处理时处于休眠状态,这有助于节省 CPU 资源和电池寿命。
  5. 定时器和异步任务
    • RunLoop 通常用于安排和管理定时器(NSTimer)或进行异步任务的调度。
    • 如果线程中需要定期执行任务或响应异步事件,那么这个线程就需要一个运行中的 RunLoop。

runloop的mode作用是什么?

RunLoop 的模式(mode)主要是用来指定事件在运行循环中的优先级的。常见的 RunLoop 模式:

  • 默认模式(NSDefaultRunLoopMode):这是大多数情况下使用的模式,用于处理大部分事件。
  • 跟踪模式(UITrackingRunLoopMode):这个模式通常在用户正在进行交互操作(如滚动屏幕)时使用,以确保界面顺畅。
  • 公共模式(NSRunLoopCommonModes):这是一组模式的集合,可以让定时器和其他输入源在多个模式下都能接受。

以+ scheduledTimerWithTimeInterval…的方式触发的timer,在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?

在使用 +scheduledTimerWithTimeInterval... 方法创建的 NSTimer 在滑动页面上的列表(如 UIScrollView 或其子类,比如 UITableViewUICollectionView)时暂停回调,原因在于 NSTimer 被添加到了当前 RunLoop 的默认模式(NSDefaultRunLoopMode)中。当滑动列表时,RunLoop 会切换到 UITrackingRunLoopMode 模式,以保证用户界面滑动的流畅性。在这种模式下,不会处理默认模式下的定时器事件。

如何解决

为了使 NSTimer 在列表滑动时也能继续工作,您可以将定时器添加到 NSRunLoopCommonModes 中。这样,定时器就可以在默认模式和跟踪模式下都运行。以下是如何实现这一点的代码示例:

1
2
3
4
5
6
7
objectiveCopy code
NSTimer *timer = [NSTimer timerWithTimeInterval:interval
                                         target:self
                                       selector:@selector(timerFired:)
                                       userInfo:nil
                                        repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

在这个例子中,timerWithTimeInterval:target:selector:userInfo:repeats: 方法用于创建一个不会自动加入 RunLoop 的定时器。随后,使用 addTimer:forMode: 明确地将定时器添加到 NSRunLoopCommonModes 中。这种方式确保了无论用户是否正在滑动列表,定时器都会持续触发。

猜想runloop内部是如何实现的?

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

1
2
3
4
5
6
7
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

什么情况下会出现循环引用,如何解决?

当两个或多个对象互相持有对方的强引用,导致它们都无法被释放时就会出现循环引用。

发生情况

典型的引用循环发生在以下情况:

  1. 两个对象相互引用: 最简单的循环引用是两个对象通过强引用相互持有对方。
  2. 对象与闭包(Blocks): 当一个对象持有一个闭包,而这个闭包又捕获了这个对象,就会发生循环引用。
  3. 委托模式: 如果委托(delegate)对象和被委托对象互相持有强引用,也会产生循环引用。
  4. 观察者模式: 类似地,如果在观察者模式中,观察者和被观察的对象互相持有强引用。
  5. 集合对象: 如果集合对象(如数组、字典)包含对象的强引用,而这些对象又持有集合对象的强引用,也可能导致循环引用。

解决方案

解决循环引用的关键是打破强引用链。在 OC 中,这通常通过以下方式实现:

  1. 使用弱引用(weak): 对于不需要拥有对象的情况(如委托模式中的委托对象),可以将引用声明为弱引用。弱引用不会增加对象的引用计数,并且当对象被销毁时,弱引用会自动置为 nil。

  2. 使用无主引用(__unsafe_unretained): 与弱引用类似,无主引用不增加对象的引用计数,但当对象被销毁时,无主引用不会自动置为 nil,可能会成为野指针。

  3. 在闭包中使用弱引用或无主引用: 当闭包捕获 self 时,可以通过捕获一个弱引用或无主引用的版本来避免循环引用。

    例如,使用弱引用:

    1
    2
    3
    4
    5
    
    __weak typeof(self) weakSelf = self;
    self.myBlock = ^{
        [weakSelf doSomething];
    };
       
    
  4. 手动打破循环: 在某些情况下,可能需要手动解除对象间的强引用,比如在适当的时机将引用设置为 nil。

在block内如何修改block外部变量?

使用 __block 存储类型修饰符来声明这个外部变量。__block 修饰符允许 Block 内部的代码修改外部变量的值。

1
2
3
4
5
6
7
8
9
10
11
__block int outsideVariable = 10;

void (^myBlock)(void) = ^{
    outsideVariable = 20; // 修改外部变量的值
    NSLog(@"Inside the block: %d", outsideVariable);
};

myBlock(); // 调用 Block

NSLog(@"Outside the block: %d", outsideVariable); 

GCD的队列(dispatch_queue_t)有哪些类型?

主要有三种:

  1. 串行队列(Serial Queue):这种队列一次只执行一个任务。完成一个任务后,它才会开始执行下一个任务。串行队列通常用于同步访问特定资源或数据。
  2. 并行队列(Concurrent Queue):并行队列可以同时执行多个任务。不过任务的开始执行时间还是按照它们被添加到队列的顺序来的,但它们的完成顺序并不固定,因为任务可以并行运行。
  3. 主队列(Main Queue):这是一个特殊的串行队列,用于在主线程上执行任务。这对于所有涉及用户界面的更新特别重要,因为所有的用户界面更新都必须在主线程上执行。

如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

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
// 假设 urls 是包含图片 URL 的 NSArray
NSArray *urls = ...;

// 创建一个调度组
dispatch_group_t group = dispatch_group_create();

// 创建一个用于加载图片的并行队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

// 用于存储加载的图片
NSMutableArray *images = [NSMutableArray array];

for (NSURL *url in urls) {
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        // 加载图片
        UIImage *image = [self loadImageFromURL:url];
        if (image) {
            // 添加图片到数组,注意要同步访问 images 数组
            @synchronized(images) {
                [images addObject:image];
            }
        }
        dispatch_group_leave(group);
    });
}

// 所有图片加载完成后的操作
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 在这里合成图片
    UIImage *combinedImage = [self combineImages:images];
    // 更新UI等操作
    // ...
});

dispatch_barrier_async的作用是什么?

dispatch_barrier_async 函数是 GCD 中的一个功能,它在并发队列(concurrent queue)中创建了一个“屏障”。这个屏障的作用是确保在屏障块之前提交到队列中的任务都完成后,屏障块才开始执行。屏障块执行完毕后,队列才会继续执行后面的任务。

dispatch_barrier_async 的主要作用包括:

  1. 同步读写操作:当你有多个线程同时读写某个共享资源(比如共享数据结构)时,dispatch_barrier_async 可以用来保证在进行写操作时不会有其他读或写操作在执行。这样就可以防止数据竞争和数据不一致的问题。
  2. 性能优化:与简单地在串行队列上执行所有操作相比,使用 dispatch_barrier_async 可以允许多个读操作同时进行,只在必要时才限制并发。这通常可以提高多线程应用程序的性能。

需要注意的是,dispatch_barrier_async 只在自定义的并发队列上 dispatch_queue_t 有效。如果你在全局并发队列或串行队列上使用它,它会像普通的异步任务一样执行,不会提供屏障的作用。

以下代码运行结果如何?

1
2
3
4
5
6
7
8
   - (void)viewDidLoad {
       [super viewDidLoad];
       NSLog(@"1");
       dispatch_sync(dispatch_get_main_queue(), ^{
           NSLog(@"2");
       });
       NSLog(@"3");
   }

这段代码在 viewDidLoad 方法中执行,并且可能导致应用程序死锁。具体来说,这是由于使用 dispatch_sync 向主队列发送同步任务所导致的。

下面是代码的执行流程:

  1. viewDidLoad 被调用时,它首先在主线程上执行,打印出 "1"
  2. 然后,代码尝试使用 dispatch_sync 向主队列派发一个闭包。这个闭包包含打印 "2" 的命令。
  3. 但是,dispatch_sync 函数会等待派发的闭包执行完毕才继续执行。因为主队列是一个串行队列,它会按照顺序执行任务。在这种情况下,主队列正在执行 viewDidLoad,它需要完成才能执行后续的闭包。
  4. 这导致了一个死锁:dispatch_sync 正在等待闭包在主队列上执行,而主队列又正在等待 viewDidLoad 完成以便执行闭包。因此,这两者都在相互等待对方,导致应用无法继续执行。
  5. 结果,NSLog(@"2")NSLog(@"3") 都不会被执行,应用程序在此处卡住。

要避免这种情况,不应在主队列上同步地派发任务。如果需要在主线程上执行任务,可以使用 dispatch_async,或者确保不在主线程上同步调用主队列。

addObserver:forKeyPath:options:context:各个参数的作用分别是什么,observer中需要实现哪个方法才能获得KVO回调?

addObserver:forKeyPath:options:context: 方法是用于注册键值观察(KVO)的。这个方法允许一个对象观察另一个对象的属性变化。

  1. observer: 这是观察者对象,它将会接收到有关键值变化的通知。这个对象必须实现观察响应的方法。
  2. forKeyPath: 这是要观察的属性的键路径。键路径是一个字符串,指定了被观察对象的哪个属性或属性的属性被观察。
  3. options: 这是一个枚举值,指定了观察者接收通知时,通知中应包含哪些信息。常用的选项包括 NSKeyValueObservingOptionNew(提供改变后的新值)、NSKeyValueObservingOptionOld(提供改变前的旧值)等。
  4. context: 这是一个可选的上下文对象,用于在回调方法中传递额外的信息。这可以用来区分不同的观察者或是同一个观察者的多个不同观察操作。

在注册观察者之后,观察者对象需要实现 observeValueForKeyPath:ofObject:change:context: 方法来接收通知。这个方法会在被观察的属性值发生变化时被调用。

1
2
3
4
5
6
7
8
9
10
11
- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context
{
    if ([keyPath isEqualToString:@"yourKeyPath"]) {
        // 处理变化
    }
}
//可以通过检查 keyPath 参数来确定哪个属性发生了变化,并通过 change 字典获取新值和旧值等信息。context 参数是在注册观察者时提供的同一个上下文对象。
//在观察者不再需要观察时,应该调用 removeObserver:forKeyPath: 方法来注销观察者,以避免内存泄漏或其他问题。

如何手动触发一个value的KVO?

要手动触发一个属性的键值观察(KVO),你需要显式地通知观察者属性值的改变。这通常是通过在属性的改变之前和之后调用 willChangeValueForKey:didChangeValueForKey: 方法来完成的。

以下是手动触发KVO的步骤:

  1. 在改变值之前调用 willChangeValueForKey:: 这个方法通知KVO机制一个指定的属性即将改变。这里的键(key)就是你想要观察的属性名。
  2. 改变属性的值: 在 willChangeValueForKey:didChangeValueForKey: 之间,你可以改变属性的值。
  3. 在改变值之后调用 didChangeValueForKey:: 在属性值改变后,你需要调用这个方法来通知KVO机制属性已经改变。

例如,假设有一个名为 myValue 的属性,你可以这样手动触发它的KVO:

1
2
3
4
[self willChangeValueForKey:@"myValue"];
// 更新 myValue 的代码
self.myValue = newValue;
[self didChangeValueForKey:@"myValue"];

任何观察 myValue 属性的观察者都会在属性改变时收到通知。

重要的是要确保 willChangeValueForKey:didChangeValueForKey: 方法成对出现,以确保KVO的正确性和一致性。如果忘记调用这些方法中的任何一个,KVO可能不会正确地通知观察者,或者可能导致程序运行错误。

KVC的keyPath中的集合运算符如何使用?

  1. @avg (平均值): 计算集合中对象的某个属性的平均值。 例如,[employees valueForKeyPath:@"@avg.salary"] 计算 employees 集合中所有对象的 salary 属性的平均值。
  2. @count (计数): 返回集合中对象的数量。 例如,[employees valueForKeyPath:@"@count"] 返回 employees 集合中对象的数量。
  3. @max (最大值)@min (最小值): 返回集合中对象的某个属性的最大值和最小值。 例如,[employees valueForKeyPath:@"@max.age"] 返回 employeesage 属性的最大值。
  4. @sum (求和): 计算集合中对象的某个属性的总和。 例如,[employees valueForKeyPath:@"@sum.salary"] 计算 employeessalary 属性的总和。
  5. @distinctUnionOfObjects (去重聚合): 返回集合中对象的某个属性的所有不同值。 例如,[employees valueForKeyPath:@"@distinctUnionOfObjects.department"] 返回 employees 中所有不同的 department 值。
  6. @unionOfObjects (聚合): 返回集合中对象的某个属性的所有值(包括重复值)。 例如,[employees valueForKeyPath:@"@unionOfObjects.department"] 返回 employees 中所有 department 值。

如何关闭默认的KVO的默认实现,并进入自定义的KVO实现?

可以通过重写类的 +automaticallyNotifiesObserversForKey: 方法来关闭 Key-Value Observing (KVO) 的默认实现,并进入自定义的 KVO 实现。这样做可以让你完全控制属性值变化的通知过程,比如在某些条件下不发送通知,或者在通知前后执行额外的逻辑。

要关闭默认的 KVO 实现,并使用自定义实现,你需要遵循以下步骤:

  1. 重写 +automaticallyNotifiesObserversForKey:: 重写这个类方法,对于你想要手动处理的键返回 NO。这会关闭该键的自动通知。

    1
    2
    3
    4
    5
    6
    
    + (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
        if ([key isEqualToString:@"yourCustomKey"]) {
            return NO;  // 对于 yourCustomKey 关闭自动通知
        }
        return [super automaticallyNotifiesObserversForKey:key];
    }
    
  2. 手动管理通知: 在属性值改变之前和之后,分别调用 willChangeValueForKey:didChangeValueForKey: 来手动发送通知。

    1
    2
    3
    4
    5
    6
    
    - (void)setYourCustomKey:(YourType)newVal {
        [self willChangeValueForKey:@"yourCustomKey"];
        // 设置属性值
        _yourCustomKey = newVal;
        [self didChangeValueForKey:@"yourCustomKey"];
    }
    

    setYourCustomKey: 方法中,你先调用 willChangeValueForKey:,然后更新属性值,最后调用 didChangeValueForKey:。这样可以确保观察者能够接收到属性值变化的通知。

IBOutlet连出来的视图属性为什么可以被设置成weak?

使用storyboard(xib不行)创建的VC,会有一个叫 _topLevelObjectsToKeepAliveFromStoryboard 的私有数组强引用所有 top level 的对象。

当一个视图(UIView/NSView)被添加到另一个视图上时,父视图会自动持有一个对子视图的强引用。这意味着只要视图被其父视图持有,它就不会被释放。如果 IBOutlet 属性被设置为 strong,并且它的父视图也被设置为 strong,这可能会导致循环引用,进而引起内存泄漏。

interface builder中User Defined Runtime Attributes如何使用?

它能够通过KVC的方式配置一些你在 interface builder 中不能配置的属性。当你希望在IB中作尽可能多得事情,这个特性能够帮助你编写更加轻量级的VC。

例如,如果你想为一个 UIButton 设置圆角,你可以在 User Defined Runtime Attributes 中添加一个属性,键路径设置为 layer.cornerRadius,类型选择 Number,然后为其指定一个合适的值。

本文由作者按照 CC BY 4.0 进行授权

Stable Diffusion Prompt

iOS开发中的堆和栈