iOS开发面试题(一)

一、值类型和引用类型的区别

在iOS开发中,值类型和引用类型的区别主要体现在以下几个方面:

  1. 存储方式

    • 值类型:每个值类型的实例都有自己的独立数据副本。当你将值类型的实例赋值给另一个变量或常量时,会创建一个新的副本。常见的值类型包括 structenumIntFloatBool 等基本数据类型。
    • 引用类型:引用类型的实例在内存中只有一个副本,多个变量或常量可以引用同一个实例。当你将引用类型的实例赋值给另一个变量或常量时,实际上是复制了对该实例的引用。常见的引用类型包括 classfunction
  2. 内存管理

    • 值类型:由于每个实例都有自己的副本,值类型的内存管理相对简单,通常在栈上分配内存。
    • 引用类型:引用类型的实例通常在堆上分配内存,使用引用计数(ARC)来管理内存,确保在没有引用时释放内存。
  3. 性能

    • 值类型:在某些情况下,值类型可能会更快,因为它们在栈上分配内存,且不需要进行引用计数。
    • 引用类型:引用类型可能会引入额外的性能开销,尤其是在频繁创建和销毁对象时。
  4. 使用场景

    • 值类型:适合用于表示简单的数据结构,或者当你希望每个实例都有独立的数据时。
    • 引用类型:适合用于表示复杂的对象,或者当你希望多个变量共享同一个实例时。

总结来说,选择值类型还是引用类型取决于具体的使用场景和需求。在Swift中,通常推荐使用值类型(如结构体)来实现数据模型,除非有明确的理由使用引用类型(如类)。

二、说一说存储属性和计算属性

在iOS开发中,存储属性和计算属性是Swift中用于定义类和结构体属性的两种不同类型。它们的主要区别如下:

存储属性 (Stored Properties)

  • 定义:存储属性是用于存储常量或变量的值。它们可以是类、结构体或枚举的属性。
  • 类型:存储属性可以是变量(var)或常量(let)。
  • 初始化:存储属性在实例化时被初始化,可以在构造函数中设置初始值。
  • 示例
    class Person {
        var name: String // 存储属性
        let age: Int // 存储属性
    
        init(name: String, age: Int) {
            self.name = name
            self.age = age
        }
    }
    

计算属性 (Computed Properties)

  • 定义:计算属性并不直接存储值,而是通过一个 getter 和可选的 setter 来计算和返回值。它们可以是类、结构体或枚举的属性。
  • 只读和读写:计算属性可以是只读的(只有 getter)或可读写的(同时有 getter 和 setter)。
  • 示例
    struct Rectangle {
        var width: Double
        var height: Double
    
        var area: Double { // 计算属性
            return width * height
        }
    
        var perimeter: Double { // 计算属性
            get {
                return 2 * (width + height)
            }
            set {
                // 这里可以实现自定义的 setter 逻辑
                width = newValue / 2
                height = newValue / 2
            }
        }
    }
    

总结

  • 存储属性用于存储数据,而计算属性用于动态计算和返回值。
  • 存储属性在内存中占用空间,而计算属性则在访问时计算值,不占用额外的存储空间。
  • 在设计数据模型时,可以根据需要选择使用存储属性或计算属性,以实现更灵活和高效的代码。

三、谈一谈消息转发机制

在iOS开发中,消息转发机制是Objective-C语言中的一个重要特性,它允许对象在接收到消息时,能够动态地决定如何处理该消息。消息转发机制主要涉及以下几个方面:

1. 消息发送过程

在Objective-C中,当你向一个对象发送消息时,系统会首先检查该对象是否能够响应该消息。这个过程分为两个主要步骤:

  • 方法查找:当你调用一个方法时,Objective-C运行时会首先检查该对象的类及其父类,看看是否实现了该方法。
  • 消息转发:如果该对象没有实现该方法,运行时会触发消息转发机制。

2. 消息转发的步骤

消息转发机制主要分为三个步骤:

  1. 动态方法解析:在这个阶段,运行时会调用 +resolveInstanceMethod:+resolveClassMethod: 方法,允许类动态地添加方法实现。如果返回 YES,则消息会被处理;如果返回 NO,则继续下一步。

  2. 消息转发:如果动态方法解析失败,运行时会调用 -forwardInvocation: 方法。此时,你可以在这个方法中处理未实现的方法,或者将消息转发给其他对象。你还可以使用 NSInvocation 来创建一个调用对象,并在需要时执行它。

  3. 重定向:如果 -forwardInvocation: 也没有处理该消息,运行时会调用 -doesNotRecognizeSelector: 方法,抛出一个异常,表示该消息无法被处理。

3. 示例代码

以下是一个简单的示例,展示了如何使用消息转发机制:

@interface MyClass : NSObject
@end

@implementation MyClass

// 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(dynamicMethod)) {
        class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 动态方法实现
void dynamicMethodIMP(id self, SEL _cmd) {
    NSLog(@"Dynamic method called!");
}

// 消息转发
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([anInvocation selector] == @selector(anotherMethod)) {
        // 转发消息到另一个对象
        [anInvocation invokeWithTarget:anotherObject];
    } else {
        [super forwardInvocation:anInvocation];
    }
}

// 处理未识别的选择器
- (void)doesNotRecognizeSelector:(SEL)aSelector {
    NSLog(@"Unrecognized selector: %@", NSStringFromSelector(aSelector));
}

@end

4. 总结

  • 消息转发机制使得Objective-C具有高度的灵活性和动态性,允许开发者在运行时决定如何处理消息。
  • 通过动态方法解析和消息转发,开发者可以实现更复杂的行为,例如代理模式、事件处理等。
  • 了解消息转发机制对于深入掌握Objective-C和iOS开发是非常重要的。

四、如何实现 GCD 请求合并

在iOS开发中,使用GCD(Grand Central Dispatch)进行请求合并可以有效地减少网络请求的数量,避免重复请求,提高应用的性能。以下是实现GCD请求合并的基本思路和示例代码。

实现思路

  1. 请求合并:当多个请求同时发起时,可以将它们合并为一个请求,等待所有请求完成后再处理结果。
  2. 使用Dispatch Group:利用GCD的DispatchGroup来管理多个异步请求的完成状态。
  3. 使用队列:可以使用串行队列来确保请求的顺序执行,或者使用并发队列来提高性能。

示例代码

以下是一个简单的示例,展示如何使用GCD实现请求合并:

import Foundation

class NetworkManager {
    private var requests: [String] = [] // 存储请求的标识符
    private let queue = DispatchQueue(label: "com.example.networkQueue") // 串行队列
    private let group = DispatchGroup() // 请求组

    func fetchData(for requestID: String) {
        queue.async {
            self.requests.append(requestID) // 添加请求标识符
            self.group.enter() // 进入请求组

            // 模拟网络请求
            DispatchQueue.global().async {
                // 模拟网络延迟
                sleep(1)
                print("Fetched data for request: \(requestID)")
                self.group.leave() // 离开请求组
            }
        }
    }

    func executeRequests() {
        queue.async {
            // 等待所有请求完成
            self.group.notify(queue: DispatchQueue.main) {
                print("All requests completed: \(self.requests)")
                self.requests.removeAll() // 清空请求标识符
            }
        }
    }
}

// 使用示例
let networkManager = NetworkManager()
networkManager.fetchData(for: "Request1")
networkManager.fetchData(for: "Request2")
networkManager.fetchData(for: "Request3")

// 执行请求合并
networkManager.executeRequests()

代码说明

  1. NetworkManager:定义了一个网络管理类,包含请求标识符数组、串行队列和请求组。
  2. fetchData(for:):模拟发起网络请求的方法,将请求标识符添加到数组中,并在请求完成后调用leave()
  3. executeRequests():使用notify方法在所有请求完成后执行特定操作(如更新UI或处理结果)。
  4. 使用示例:创建NetworkManager实例,发起多个请求,并执行请求合并。

总结

  • 使用GCD的DispatchGroup可以有效地管理多个异步请求,确保在所有请求完成后进行后续处理。
  • 通过合并请求,可以减少网络负担,提高应用的性能和用户体验。
  • 根据具体需求,可以进一步扩展和优化请求合并的逻辑,例如处理请求的优先级、错误处理等。

五、存储结构有哪些?如何让存储结构安全添加取值?

在iOS开发中,常见的存储结构主要包括以下几种:

1. 常见存储结构

  • 数组 (Array):有序集合,可以存储多个相同类型的元素。使用Array类型。
  • 字典 (Dictionary):无序集合,存储键值对。使用Dictionary类型。
  • 集合 (Set):无序集合,存储唯一元素。使用Set类型。
  • 链表 (Linked List):通过节点连接的线性数据结构,适合频繁插入和删除操作。
  • 栈 (Stack):后进先出(LIFO)的数据结构,适合处理递归和回溯问题。
  • 队列 (Queue):先进先出(FIFO)的数据结构,适合处理任务调度。

2. 安全添加和取值

为了确保在使用这些存储结构时的安全性,可以采取以下措施:

2.1 使用类型安全

Swift是类型安全的语言,确保在编译时检查类型。使用ArrayDictionarySet时,确保元素类型一致。

var numbers: [Int] = [] // 只允许存储Int类型

2.2 使用可选类型

在取值时,可以使用可选类型来处理可能的nil值,避免运行时错误。

var dictionary: [String: Int] = [:]
let value: Int? = dictionary["key"] // 取值时使用可选类型
if let unwrappedValue = value {
    print("Value: \(unwrappedValue)")
} else {
    print("Key not found")
}

2.3 使用线程安全的集合

在多线程环境中,使用DispatchQueueNSLock来确保对存储结构的安全访问。

class ThreadSafeArray<T> {
    private var array: [T] = []
    private let queue = DispatchQueue(label: "com.example.threadSafeArray")

    func append(_ element: T) {
        queue.async {
            self.array.append(element)
        }
    }

    func get(at index: Int) -> T? {
        return queue.sync {
            guard index >= 0 && index < self.array.count else { return nil }
            return self.array[index]
        }
    }
}

2.4 使用错误处理

在进行取值操作时,可以使用do-catch语句来处理可能的错误。

enum StorageError: Error {
    case keyNotFound
}

func getValue(forKey key: String) throws -> Int {
    guard let value = dictionary[key] else {
        throw StorageError.keyNotFound
    }
    return value
}

do {
    let value = try getValue(forKey: "key")
    print("Value: \(value)")
} catch StorageError.keyNotFound {
    print("Key not found")
} catch {
    print("An unexpected error occurred: \(error)")
}

总结

  • 常见的存储结构包括数组、字典、集合等,选择合适的存储结构可以提高代码的效率和可读性。
  • 为了确保存储结构的安全添加和取值,可以使用类型安全、可选类型、线程安全的集合和错误处理等方法。
  • 通过这些措施,可以有效地避免运行时错误和数据竞争,提高应用的稳定性和安全性。

六、模块化和组件化的区别?以及各自优缺点

在iOS开发中,模块化和组件化是两种常用的架构设计方法,它们有不同的侧重点和实现方式。以下是它们的区别以及各自的优缺点。

模块化 (Modularization)

定义:模块化是将应用程序分解为多个独立的模块,每个模块负责特定的功能或业务逻辑。模块之间通过明确的接口进行交互。

特点

  • 每个模块可以独立开发、测试和维护。
  • 模块之间的依赖关系较少,降低了耦合度。

优点

  1. 可维护性:模块化使得代码结构清晰,便于维护和更新。
  2. 重用性:可以在不同项目中重用模块,减少重复开发。
  3. 团队协作:不同团队可以并行开发不同模块,提高开发效率。

缺点

  1. 初始成本:模块化设计需要在初期投入更多的时间和精力进行架构设计。
  2. 复杂性:管理多个模块的依赖关系和版本可能会增加系统的复杂性。

组件化 (Componentization)

定义:组件化是将应用程序分解为多个组件,每个组件通常是一个功能单元,可能包含多个模块。组件可以是UI组件、网络组件等,通常是更细粒度的划分。

特点

  • 组件可以是可重用的UI元素或功能单元,通常具有更高的封装性。
  • 组件之间的交互通常通过事件或回调机制实现。

优点

  1. 灵活性:组件化允许开发者根据需要灵活组合和替换组件。
  2. 可重用性:组件可以在多个项目中复用,尤其是UI组件。
  3. 快速开发:可以快速构建应用程序,因为可以直接使用现成的组件。

缺点

  1. 依赖管理:组件之间的依赖关系可能会导致版本冲突和管理困难。
  2. 性能开销:过多的组件可能会导致性能开销,尤其是在UI渲染方面。

总结

  • 模块化更关注于将应用程序分解为独立的功能模块,强调代码的可维护性和团队协作。
  • 组件化则更关注于构建可重用的功能单元,强调灵活性和快速开发。

在实际开发中,模块化和组件化可以结合使用,以实现更高效的开发流程和更好的代码结构。选择哪种方法取决于项目的规模、团队的结构以及具体的业务需求。

七、如何更新 UI,为什么子线程不能更新 UI?

在iOS开发中,更新UI是一个非常重要的任务。以下是如何更新UI以及为什么子线程不能直接更新UI的详细说明。

如何更新UI

在iOS中,所有的UI更新必须在主线程(也称为UI线程)上进行。可以使用以下几种方法来确保在主线程上更新UI:

1. 使用 DispatchQueue.main.async

这是最常用的方法,可以将UI更新的代码放入主线程的异步队列中执行。

DispatchQueue.main.async {
    // 在这里更新UI
    self.label.text = "Hello, World!"
}

2. 使用 performSelector(onMainThread:with:waitUntilDone:)

这个方法可以将选择器(方法)在主线程上执行。

self.performSelector(onMainThread: #selector(updateUI), with: nil, waitUntilDone: false)

@objc func updateUI() {
    // 在这里更新UI
    self.label.text = "Hello, World!"
}

3. 使用 OperationQueue

可以创建一个操作队列,并将UI更新的操作添加到主队列中。

OperationQueue.main.addOperation {
    // 在这里更新UI
    self.label.text = "Hello, World!"
}

为什么子线程不能更新UI

  1. 线程安全:UIKit不是线程安全的,所有UI操作都必须在主线程上执行。直接在子线程中更新UI可能导致数据竞争和不一致的状态,从而引发崩溃或未定义的行为。

  2. 主线程的职责:主线程负责处理用户交互和UI更新。如果在子线程中进行UI更新,可能会导致主线程的任务被阻塞,从而影响用户体验,导致界面卡顿或无响应。

  3. 崩溃风险:如果在子线程中尝试更新UI,可能会导致应用程序崩溃,通常会抛出[<NSObject> setValue:forUndefinedKey:]或类似的异常。

总结

在iOS开发中,所有UI更新必须在主线程上进行,以确保线程安全和良好的用户体验。可以使用DispatchQueue.main.asyncperformSelector(onMainThread:with:waitUntilDone:)OperationQueue等方法来确保在主线程上更新UI。直接在子线程中更新UI是不可行的,可能导致应用崩溃和不稳定。

八、iOS开发,字典、数组、集合(数据结构)的底层实现原理和逻辑?

在iOS开发中,字典(NSDictionary和NSMutableDictionary)、数组(NSArray和NSMutableArray)和集合(NSSet和NSMutableSet)的底层实现原理和逻辑各有不同。以下是这三种数据结构的简要概述:

1. 字典(Dictionary)

  • 底层实现:字典使用哈希表(Hash Table)作为其底层数据结构。
  • 哈希函数:通过哈希函数将键(key)映射到哈希值(hash value),并根据哈希值存储和检索值(value)。
  • 冲突解决:使用链式法或开放地址法来处理哈希冲突。
  • 动态扩展:当元素数量超过一定阈值时,字典会扩展哈希表的大小,并重新计算现有键的哈希值。
  • 时间复杂度:理想情况下,插入、删除和查找操作的时间复杂度为O(1)。

2. 数组(Array)

  • 底层实现:数组使用动态数组(Dynamic Array)作为其底层数据结构。
  • 初始容量:创建数组时分配一个初始容量,决定可以存储的元素数量。
  • 扩展机制:当元素数量超过当前容量时,数组会创建一个更大的数组(通常是当前容量的两倍),并将现有元素复制到新数组中。
  • 元素访问:支持通过索引快速访问元素,时间复杂度为O(1)。
  • 插入和删除操作:在末尾插入元素通常是O(1),但在中间插入或删除元素时,时间复杂度为O(n)。
  • 不可变与可变数组:NSArray是不可变的,而NSMutableArray是可变的。

3. 集合(Set)

  • 底层实现:集合通常使用哈希表(Hash Table)或平衡树(如红黑树)作为其底层数据结构。
  • 唯一性:集合中的元素是唯一的,不能重复。
  • 哈希函数:与字典类似,集合使用哈希函数来存储和检索元素。
  • 动态扩展:当元素数量超过一定阈值时,集合会扩展其底层存储结构。
  • 时间复杂度:在理想情况下,插入、删除和查找操作的时间复杂度为O(1)(使用哈希表)或O(log n)(使用平衡树)。

总结

  • 字典:基于哈希表,支持键值对存储,快速查找。
  • 数组:基于动态数组,支持按索引访问,适合顺序存储。
  • 集合:基于哈希表或平衡树,支持唯一元素存储,适合无序集合。

这三种数据结构各自有其适用场景,开发者可以根据需求选择合适的数据结构来优化性能和内存使用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容