美文网首页
面试(全)

面试(全)

作者: 夜雨聲煩_ | 来源:发表于2023-07-26 00:49 被阅读0次
    1. 常见的属性修饰符有哪些?

    strong: 用于 ARC (Automatic Reference Counting) 下的对象属性,表示对该对象的强引用,该对象引用计数会增加。
    weak: 用于 ARC 下的对象属性,表示对该对象的弱引用,该对象引用计数不会增加。当对象被释放时,属性会自动被设置为 nil,避免野指针。
    readwrite: 用于生成 getter 和 setter 方法,属性在初始化后可通过 setter 方法修改。
    readonly: 用于只生成 getter 方法,不生成 setter 方法,属性在初始化后不可更改。
    atomic: 表示属性是原子性的,保证在多线程环境下的安全访问,但会影响一定的性能。
    noatomic: 表示属性是非原子性的,不保证在多线程环境下的安全访问,但具有更高的访问性能。
    copy: 用于 ARC 下的对象属性,表示在设置属性时执行一次拷贝操作,以保证新属性对象的独立性。
    assign: 用于基本数据类型(如 NSInteger,CGFloat 等)和 C 数据类型属性,进行简单的赋值操作,不涉及引用计数管理。
    nullable: 用于指定对象属性可以接受 nil 值。
    nonnull: 用于指定对象属性不能接受 nil 值。
    __kindof: 泛型关键词,用于表明返回值类型为当前类或其子类。
    __autoreleasing: 用于指定在传递对象参数时,不改变其引用计数,通常用于 NSError ** 等情况。

    2.使用copy时应该注意什么?

    1)仅适用于遵循 NSCopying 协议的对象
    copy 属性只适用于遵循 NSCopying 协议的对象。该协议要求对象实现 copyWithZone: 方法,用于返回一个对象的深拷贝(即新对象与原对象内容相同,但是指向不同的内存地址)。如果给一个不符合 NSCopying 协议的对象使用 copy 修饰符,编译器会产生警告,且在运行时会导致崩溃。

    NSString、NSArray、NSDictionary等大多数不可变的 Foundation 类都遵循了 NSCopying 协议,允许进行深拷贝。一些自定义的类也可以选择遵循 NSCopying 协议,使得它们的对象也能够进行深拷贝,实现copyWithZone方法。
    NSMutableString、NSMutableArray、NSMutableDictionary等可变的类通常没有实现 NSCopying 协议,而是实现了NSMutableCopying协议。可以通过 copy 方法得到一个不可变的副本,也可以通过mutableCopy得到可变副本。

    2)深拷贝与浅拷贝
    使用 copy 修饰符会执行一次深拷贝,复制出一个新的对象。这意味着新对象和原对象是完全独立的,修改其中一个对象不会影响另一个对象。相比之下,strong 修饰符是执行一次浅拷贝,新对象和原对象共享相同的内存地址。
    3)注意内存管理
    使用 copy 修饰符会增加内存使用,因为每次设置属性时都会进行一次拷贝操作。确保在需要拷贝对象时使用 copy,而不是不必要地进行拷贝。
    总结:深拷贝增加内存消耗,确定是否必要。
    4)可变对象问题
    使用 copy 修饰符拷贝可变对象时,实际上会得到一个不可变的副本。这是因为 copy 方法返回的对象类型是 id<NSCopying>,而不是具体的可变类型。如果需要得到可变副本,可以使用 mutableCopy 方法。
    同1)。
    5)自定义对象实现 NSCopying 协议
    如果使用自定义对象,并且想要使用 copy 修饰符,需要确保该自定义对象正确实现了 NSCopying 协议。在 copyWithZone: 方法中,应该返回一个深拷贝的对象。

    以下是copyWithZone的标准示例代码:

    - (id)copyWithZone:(nullable NSZone *)zone {
        // 因为我们使用了 ARC(自动引用计数),所以可以直接使用 [self alloc] 来创建一个新的对象。
        Person *copy = [[Person allocWithZone:zone] init];
        
        // 我们对 NSString 属性使用 'copy' 属性修饰符,这将创建字符串的新副本。
        copy.name = [self.name copy];
        copy.address = [self.address copy];
        
        return copy;
    }
    
    3.atomic真的安全么?加的锁是哪种锁?

    atomic 不是绝对安全的:尽管 atomic 修饰符提供了一定程度的线程安全性,但它并不能解决所有多线程并发访问的问题。它只能保证在属性访问期间的线程安全性,但并不能保证整个对象在复杂操作下的线程安全。
    不安全的原因:
    系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。getter 还是能得到一个完好无损的对象(可以保证数据的完整性),但这个对象在多线程的情况下是不能确定的,也就是说:如果有多个线程同时调用setter的话,不会出现某一个线程执行完setter全部语句之前,另一个线程开始执行setter情况,相当于函数头尾加了锁一样,每次只能有一个线程调用对象的setter方法,所以可以保证数据的完整性。但是对于线程A执行的getter而言,不确定拿到的值是原始值,还是线程B执行setter以后的值,还是线程C执行setter以后的值。

    例如,如果定义属性NSInteger i是原子的,对i进行i = i +1;操作就是不安全的,因为原子性只能保证读写安全,而该表达式需要三步操作
    (1)读取i的值存入寄存器
    (2)将i加1
    (3)修改i的值
    如果在第1步完成的时候i被其他线程修改了,那么表达式执行的结果就会和预期的不一样,也就是不安全的。
    

    对于大多数情况,开发者更倾向于使用 nonatomic 属性修饰符,并借助其他手段来确保线程安全性。如有复杂的线程安全需求,可以通过其他同步机制(如 GCD、NSLock、dispatch_semaphore 等)来实现更精确的控制。
    加的锁是自旋锁。os_unfair_lock。

    自旋锁(Spinlock)是一种简单的同步机制,用于在多线程环境下保护共享资源,以防止多个线程同时访问或修改数据。它是一种忙等待(Busy Waiting)的锁,意味着当一个线程想要获取锁时,它会在一个循环中不断检查锁的状态,而不是立即进入等待状态。

    因为atomic只保证读写安全,是靠在getter、setter中加锁来实现的。所以如果一个对象的改变不是直接调用 getter/setter 方法,而是直接对对象内部属性修改、字符串拼接、数组增加和移除元素等操作,就不能保证这个对象是线程安全的。例如:objectAtIndex等方法都不保证线程安全。

    4.iOS中的内存管理是怎么样的

    在 iOS 中,内存管理是通过引用计数(Reference Counting)来实现的。开发者通过 retain、release 和 autorelease 操作来管理对象的引用计数。引入 ARC 技术后,大部分情况下无需手动管理内存,编译器会自动生成合适的引用计数操作。然而,需要注意避免循环引用,使用弱引用和无主引用来处理特定情况。同时,自动释放池用于延迟释放对象,减少内存管理的开销。

    5.自动释放池
    原理:

    iOS 中的自动释放池(Autorelease Pool)是一种用于延迟释放对象的机制,它可以帮助开发者避免在不必要的地方频繁地进行引用计数操作,从而提高内存管理的效率。自动释放池是通过 @autoreleasepool 块来实现的,在每个 run loop 迭代中都会创建一个自动释放池,用于管理在该迭代中创建的临时对象。

    本质:

    自动释放池(Autorelease Pool)的本质是一个数据结构,它是一个栈(Stack)或链表(Linked List),用于管理需要延迟释放的对象。自动释放池的引入是为了实现对象的延迟释放,从而避免频繁的引用计数增减操作,提高内存管理的效率。

    在 iOS 应用程序的运行过程中,系统会在每个 run loop 迭代中创建一个自动释放池。在每个自动释放池的作用域内,所有调用了 autorelease 方法的对象都会被添加到这个自动释放池中。

    当自动释放池的作用域结束时(即作用域结束或 run loop 迭代结束),自动释放池会被释放,这时其中的所有对象会依次调用 release 方法,从而释放对象的内存。

    应用:

    在循环内加入autoreleasepool降低内存峰值。

    for (int i = 0; i < 10; i++) {
        @autoreleasepool {
            // 在这个自动释放池中创建临时对象
            NSString *tempString = [NSString stringWithFormat:@"Object %d", I];
            NSLog(@"%@", tempString); // 使用临时对象
            
            // 当自动释放池的作用域结束时,其中的临时对象会被自动释放
        }
    }
    
    6.常见的内存泄漏有哪些

    循环引用:
    block的循环引用、delegate的循环引用、对象之间的循环引用、iOS9以前的通知没有移除。

    7.线程和runloop之间的关系是怎么样的

    RunLoop 是 iOS 和 macOS 中负责管理事件和消息循环的机制。每个线程都有一个对应的 RunLoop,用于在线程空闲时等待输入事件,当事件到达时处理事件,并执行相应的任务。
    1)RunLoop 和线程是一一对应的关系。每个线程都有一个与之关联的 RunLoop,而且 RunLoop 只能在其对应的线程中运行。
    2)主线程的Runloop是由系统自动创建并启动;其他线程在创建时并没有开启Runloop,需要手动开启,若不开启就一直不会有Runloop。
    3)苹果不提供直接创建Runloop的方法,其他线程Runloop的创建发生在第一次获取的时候[NSRunLoop currentRunLoop] ,类似于懒加载。系统判断当前线程没有Runloop就会自动创建。
    4)当前线程结束,其对应的Runloop也被销毁。

    更多详细内容:https://www.jianshu.com/p/14bcdd55ae3b

    8.GCD中串行并行队列,同步异步的区别

    1)串行队列(Serial Queue): 串行队列中的任务按顺序一个接一个地执行,每次只执行一个任务。当一个任务执行完毕后,才会执行下一个任务。在串行队列中,每个任务必须等待前一个任务完成后才能开始执行。串行队列适用于需要按顺序执行任务的场景,保证任务的有序性。

    2)并行队列(Concurrent Queue): 并行队列中的任务可以同时执行,不必等待前一个任务完成。多个任务可以并发执行,提高了执行效率。并行队列适用于任务之间没有依赖关系,可以并发执行的场景。

    3)同步执行(Synchronous): 同步执行是指在当前线程中执行任务,不会开启新的线程。在同步执行中,任务会立即执行,执行完毕后才会继续执行后面的代码。如果是在串行队列中,同步执行会造成任务的阻塞,直到当前任务执行完毕。

    4)异步执行(Asynchronous): 异步执行是指将任务提交到队列中,然后立即返回,不会等待任务执行完毕。(如果在viewdidload里异步加入一段代码,会等viewdidload执行结束后再执行异步加入的代码。)异步执行会开启新的线程(如果是在并行队列中),从而可以在后台执行任务,不影响当前线程的继续执行。

    9.有遇到过死锁么,怎么产生的

    使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
    示例1:

    - (void)interview01
    {
        // 问题:以下代码是在主线程执行的,会不会产生死锁?会!
        NSLog(@"执行任务1");
        
        dispatch_queue_t queue = dispatch_get_main_queue();
        dispatch_sync(queue, ^{
            NSLog(@"执行任务2");
        });
        
        NSLog(@"执行任务3");
        
        // dispatch_sync立马在当前线程同步执行任务
    }
    

    示例2:

    dispatch_queue_t queue = dispatch_queue_create("com.example.queue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
        // 线程1,同步调度另一个任务
        dispatch_sync(queue, ^{
            // 线程1等待任务2的执行,但任务2又在等待任务1的执行,形成死锁
        });
    }
    
    10.runtime查找方法的过程

    1)首先,Runtime 会查找类的缓存(Cache),在缓存中快速查找方法的实现。如果找到了,就直接调用并结束查找过程。
    2)如果在缓存中没有找到方法,Runtime 会继续查找类的方法列表。方法列表是一个数组,其中包含了类的所有方法。Runtime 会遍历方法列表,逐个检查方法的名称和参数类型是否和调用的方法匹配。如果找到匹配的方法,就调用该方法并结束查找过程。
    3)如果在当前类的方法列表中没有找到方法,Runtime 会继续在父类的方法列表中查找,直到找到匹配的方法或者查找到达 NSObject 类为止。
    4)如果在以上步骤中还是没有找到方法的实现,Runtime 提供了动态方法解析的机制。在这一步中,运行时会调用类方法 +resolveInstanceMethod: 或 +resolveClassMethod:,允许开发者在运行时动态添加方法的实现。如果开发者添加了方法的实现,那么 Runtime 会重新开始方法查找过程。
    5)如果在以上步骤中都没有找到方法的实现,那么 Runtime 会调用方法 +doesNotRecognizeSelector:,默认情况下会抛出异常,表示消息未处理。

    11.runtime是怎么实现weak置nil的

    1)当一个对象被声明为 weak 属性时,该对象会被注册到一个全局的 weak 对象哈希表中。这个哈希表中保存着所有的 weak 对象以及它们对应的弱引用计数器。
    2) 当有一个新的弱引用指向这个对象时,Runtime 会在哈希表中查找这个对象的对应的弱引用计数器。如果找到了,表示该对象已经有其他 weak 引用存在,就直接将新的弱引用添加到弱引用计数器中,不会创建新的弱引用计数器。
    3) 弱引用计数器是一个指向对象地址的指针数组,每个弱引用都会被添加到这个数组中。每个弱引用计数器中的元素都持有一个指向对象的弱引用。多个弱引用可以共享同一个弱引用计数器。
    4)当对象被释放时,Runtime 会将其注册的 weak 对象从全局的 weak 哈希表中移除,并且将所有与之关联的弱引用计数器中的元素置为 nil,这样所有指向对象的弱引用就都变成了 nil。
    5)当某个对象的所有强引用都被释放,且没有任何弱引用指向该对象时,对象会被销毁。此时,所有指向该对象的弱引用计数器中的元素会被置为 nil,从而使所有的 weak 属性自动变成 nil。

    Runtime 对注册的类,会进行内存布局,从一个粗粒度的概念上来讲,这时候weak 对象会放入一个 hash 表中,这是一个全局表,表中是用 weak 指向的对象内存地址作为key,用所有指向该对象的 weak 指针表作为value。当此对象的引用计数为 0 的时候会 dealloc,假如该对象内存地址是 address,那么就会以 address为key,在这个weak 表中搜索,找到所有以 address 为键的weak对象,从而设置为nil。

    12.关联对象是线程安全的么

    关联对象在多线程环境下不是线程安全的。在 Objective-C 中,我们可以使用 Runtime 的关联对象 API 来给已有的对象动态添加关联的属性。这些关联对象是存储在哈希表中的,而哈希表在多线程环境下是非线程安全的。

    当多个线程同时对同一个对象添加、获取、修改或删除关联对象时,会存在竞态条件(Race Condition)的问题。可能会导致关联对象操作的不一致性,例如重复添加、丢失关联对象,或者导致内存管理问题。

    13.isKindOf和isMemberOf区别

    isKindOfClass: 方法用于检查对象是否是指定类或其子类的实例,包括继承关系。
    isMemberOfClass: 方法用于检查对象是否是指定类的实例,不包括其子类,只有完全一致时才返回 YES。

    14.Class结构

    https://www.jianshu.com/p/8842bda36bfd

    15.load和initialize区别

    load 方法是在类加载到内存时调用(import或者NSClassFromString等动态方式),全局唯一,无需手动触发,通常用于方法交换和全局初始化。
    initialize 方法是在类第一次被使用时调用,每个类的 initialize 方法只会调用一次,用于类相关的初始化逻辑。

    16.KVO的实现原理,使用KVO要注意什么,手动触发应该怎么做
    实现原理

    当对象的属性被注册为 KVO 时,Runtime 动态生成一个派生类,并将对象的 isa 指针指向该派生类,从而实现动态子类化。
    在派生类中,会重写被观察属性的 setter 方法,以便在属性发生变化时能够触发通知。
    重写的 setter 方法内部会先调用父类的 setter 方法,然后再发送通知给监听者。

    注意事项

    dealloc时移除。
    使用context区分通知来源
    可变数组如果直接添加数据,是不会调用setter方法,不会触发kvo通知回调。

    手动触发
    - (void)setName:(NSString *)name
    {
        [self willChangeValueForKey:@"name"];   //即将改变
        _name = name;
        [self didChangeValueForKey:@"name"];   //已改变
    }
    
    17.有多个分类实现同一个方法,最后会执行哪个

    最后加载的分类中的方法实现会覆盖之前加载的分类和原类中的同名方法实现。

    18.iOS产生卡顿的原因

    主线程阻塞: 当在主线程上执行耗时的操作,比如网络请求、文件读写、复杂的计算等,会导致主线程阻塞,造成界面无响应,产生卡顿现象。
    大量计算和绘制: 如果应用中存在大量的计算和绘制任务,比如复杂的布局计算、绘制大量图片或动画等,也会导致主线程过载,引起卡顿。
    内存压力: 当应用占用大量内存,且系统内存资源不足时,可能触发系统的内存警告,导致系统对应用进行内存管理,从而引起卡顿。
    当 UI 元素频繁刷新或存在渲染性能问题时,会导致界面绘制延迟,产生卡顿现象。

    19.什么是离屏渲染

    它是指将要渲染的内容绘制到屏幕以外的缓冲区(离屏),然后再将渲染结果合并到屏幕上。由于会导致上下文的频繁切换,所以更耗性能。

    20.沙盒文件目录
    Documents 目录

    用于存放应用生成的持久化数据,例如用户创建的文件、数据库文件等。iCloud 会自动备份该目录中的内容。

    tmp 目录

    用于存放临时文件,应用退出后该目录下的文件会被删除,不会被备份到 iCloud。

    Library 目录

    存放应用的支持文件,包括 Caches 目录和 Preferences 目录。
    Caches用于存放应用运行时产生的缓存文件,例如图片缓存、音频缓存等。不会被备份到 iCloud,系统可能在磁盘空间不足时自动清理该目录下的内容。
    Preferences用于存放应用的偏好设置数据,通常是以 plist 文件形式存储。被 UserDefaults 自动管理。

    21.从点击屏幕开始到某个按钮触发响应链传递机制

    1)用户点击屏幕: 用户在屏幕上点击某个位置,系统会将这个事件封装成一个 UIEvent 对象,并传递给应用的事件队列。
    2)事件传递阶段: 在事件传递阶段,UIKit 将事件沿着视图层次结构进行传递,找到最适合处理该事件的视图。

    • 事件捕获阶段(Event Capture Phase): 事件从应用的 UIWindow 开始,通过响应链向下传递,即从父视图到子视图。在这个阶段,系统会找到离触摸点最近的父视图(通常是最外层的视图),然后继续向下传递。
    • 事件响应阶段(Event Target/Responder Phase): 一旦事件到达目标视图,即事件的最终接收者(一般是按钮或其父视图),事件将在目标视图上触发。系统会从目标视图开始,沿着响应链向上传递,直到找到合适的处理者,比如 UIButton 或其父视图,处理事件的逻辑。
    • 事件冒泡阶段(Event Bubble Phase): 如果事件在目标视图上没有得到处理,它会继续沿着响应链向上传递,即从子视图到父视图。这个阶段通常用于视图之间的通信,但事件的处理已经在前面的阶段完成了。
    • 事件处理: 当事件找到最适合处理的视图(如按钮)时,系统会调用该视图对应的事件处理方法(比如 touchesBegan:withEvent: 等),进行具体的响应和处理逻辑。
    22.如何更改响应范围

    1)一个事件多个视图处理:
    重写touchesBegan方法,在内部处理事件之后,再执行[super touchesBegan:touches withEvent:event];,使其父视图处理。
    2)视图不处理事件
    通过重写 hitTest(:with:) 方法,我们可以实现自定义的事件分发逻辑,比如更改触摸事件的响应范围、修改响应层次等

    23.iOS中常用的锁有哪些,性能怎么样

    NSLock:互斥锁
    性能:性能较好,适用于低级别的互斥操作。但不支持递归锁,不能在同一线程中多次加锁。(连续两次加锁会造成死锁,后续连续两次解锁也无效)。
    NSRecursiveLock:递归锁
    性能较互斥锁略差,适用于同一线程需要多次加锁的情况,支持同一线程多次加锁,不会产生死锁。
    NSReadWriteLock: 读写锁
    NSLock 的扩展,可以允许多个线程同时读取共享资源,但只允许一个线程进行写操作。这样可以提高读操作的并发性能,适用于读多写少的场景。

    NSReadWriteLock *lock = [[NSReadWriteLock alloc] init];
    
    // 读操作
    [lock readLock];
    // 读取共享资源
    [lock unlockRead];
    
    // 写操作
    [lock writeLock];
    // 写入共享资源
    [lock unlockWrite];
    

    synchronized:关键字
    当一个线程执行到 @synchronized 块时,它会尝试获取与 lockObject 相关联的锁。如果当前没有其他线程持有该锁,当前线程会获得锁,并进入临界区执行代码。
    (实际上用的是NSRecursiveLock,系统帮创建使用)
    使用方便,但在性能上相对较慢,因为它需要维护每个对象的锁表,并且只能在 Objective-C 对象之间实现线程同步。

    @synchronized (lockObject) {
        // 临界区的操作
    }
    

    Semaphore:信号量
    性能较好,适用于控制并发线程数的场景。可以通过信号量控制并发访问资源的线程数量,但使用不当可能导致线程饥饿。

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 10 * NSEC_PER_SEC);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, overTime); // 计数大于0立即返回,计数为0一直等待
        NSLog(@"线程1开始执行"); 
        dispatch_semaphore_signal(semaphore); // 释放 增加信号量 0变1
    });
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_semaphore_wait(semaphore, overTime);
        NSLog(@"线程2开始执行");
        dispatch_semaphore_signal(semaphore);
    });
    
    24.xcode从开始编译到App出现一个界面之间进行了哪些工作

    1)编译源代码:
    Xcode 首先会将你的源代码编译成可执行的机器代码。这个过程包括预处理、编译、汇编和链接等步骤。编译器将源代码翻译成机器指令,生成可执行文件(通常是 Mach-O 格式)。
    2)代码签名和打包:
    编译完成后,Xcode 会对可执行文件进行代码签名,以确保应用来源的可信性和安全性。然后,将可执行文件、资源文件和其他必要的文件打包成一个 .app 文件。
    3)启动应用:
    用户点击应用图标启动应用,操作系统会将应用加载到内存中,并执行应用的主函数(entry point)
    4)执行启动代码:
    应用的启动代码包括主线程的初始化、创建应用程序对象、设置应用程序委托和主 run loop 等。
    5)加载 main storyboard 或 NIB 文件:
    应用的主 run loop 开始运行后,根据 Info.plist 中的配置,应用会加载主界面的描述文件,可能是一个 Main Storyboard 或 NIB 文件
    6)创建视图控制器和视图:
    在加载主界面描述文件后,应用会根据其中的视图层次结构创建相应的视图控制器和视图,并完成视图的布局。

    25.说一下OC的反射机制

    Objective-C的反射机制(Reflection Mechanism)是指在运行时检查和操作类、对象以及它们的属性、方法和成员变量的能力。Objective-C为开发者提供了一套强大的运行时库(Runtime Library),使得在程序运行过程中能够动态地获取和修改类的信息、调用方法、访问属性等。

    反射机制的核心是Objective-C的运行时库,该库提供了一系列C函数和数据结构,允许开发者在运行时操作和查询类的结构和行为。

    26.block
    实质:

    Block的实质是一个封装了一段代码的对象,包含了代码块以及在执行过程中所需要的变量和常量的信息。Block可以捕获其周围范围内的变量,形成一个闭包,这意味着它们可以访问在其声明范围内的变量,即使这些变量在Block执行时已经超出了作用域。

    block的种类和区别
    1. 全局Block(Global Block):
      不使用外部变量的block是全局block。(在Block内部直接使用,而不是使用block的参数)
      全局Block在定义时没有捕获任何外部变量,因此可以在任何地方调用。全局Block一般用于表示不依赖于外部状态的代码块,类似于普通函数。
    2. 栈上的Block(Stack Block):
      使用外部变量并且未进行copy操作的block是栈block。
      栈上的Block在定义时会捕获外部变量的值,但只能在其定义范围内使用。当Block超出定义范围后,它所捕获的外部变量会被复制到堆上,以便于继续使用。
      3.堆上的Block(Heap Block):
      对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
      堆上的Block通常是栈上的Block在超出作用域后自动从栈复制到堆上生成的。堆上的Block可以在其定义范围外继续使用,并且可以通过Block的拷贝操作进行传递。

    当block引用到外部变量的时候, 在MRC下是栈block, 而在ARC下是堆block, 这个是编译器为我们做的, 而ARC为什么直接是堆block呢, 其实是编译器自动把栈block事先做一次copy, 放在堆中而已。即创建出来的block本身是栈block,通过=赋值时系统帮做了copy操作,变成了在堆上。

    block捕获变量

    block是值捕获,捕获后修改无效。在使用__block修饰后,可以修改内部的值。原因是使用__block会在Block外部创建一个指向变量的指针,并在Block内部引用这个指针。这就允许在Block内部修改指向的变量的值,而不是只修改捕获的值。

    26. 加密

    哈希算法(Hash Algorithm)是一种将任意长度的输入数据映射为固定长度哈希值(散列值)的算法。哈希算法的核心目标是快速、高效地生成一个固定大小的输出,通常用于数据加密、数据完整性校验、散列表、密码存储等应用。

    三段式加密

    三段式加密,顾名思义,就是数据传输过程中,加密数据由三段(数据摘要 + 对称加密数据 + 数字信封)组成。三段分别采用以上三种类型的加密算法。
    在数据传输前,请求方和服务方会约定一对公私钥,请求方持有公钥(publicKey),服务方持有私钥(privateKey);
    对称密钥(desKey)由请求方随机生成。

    25.iOS音视频开发的简单流程

    1)获取音视频数据:
    音视频开发的第一步是获取音频和视频数据源。音频数据通常来自麦克风或者音频文件,而视频数据通常来自摄像头或者视频文件。
    2)音视频采集:
    对于音频,可以使用 AVCaptureSession 来进行音频采集,获取麦克风的输入。对于视频,可以使用 AVCaptureSession 来进行视频采集,获取摄像头的输入。
    3)音视频编码:
    采集到的原始音视频数据通常会进行编码,将其压缩成特定格式,以便后续传输或存储。iOS 中可以使用 AVAssetWriter 或 AVAssetExportSession 进行音视频编码。
    4)音视频传输:
    编码后的音视频数据可以传输到远程服务器或其他设备,以便实现实时通讯或者视频直播。常用的传输协议有 RTP、RTSP、RTMP 等。
    (RTMP、HLS)
    5)音视频解码:
    接收端收到音视频数据后,需要进行解码还原成原始的音频和视频数据。iOS 中可以使用 AVAssetReader 进行音视频解码。
    6)音视频渲染:
    解码后的音频和视频数据需要进行渲染,即将其播放出来。对于音频,可以使用 AVAudioEngine 或者 AVAudioPlayer 进行音频渲染。对于视频,可以使用 AVPlayerLayer 或者 AVSampleBufferDisplayLayer 进行视频渲染。
    7)音视频处理:
    在音视频渲染的过程中,可能需要进行一些额外的处理,比如音频的音效处理、视频的滤镜效果等。iOS 中可以使用 AVAudioEngine 进行音频处理,使用 Core Image 进行视频滤镜处理。

    28.pcm数据格式是怎么样构成的

    采样位数(Bit Depth):表示每个样本所占的位数,常见的有 8 位、16 位、24 位和 32 位。
    采样率(Sample Rate):表示每秒钟对音频信号进行的采样次数,常见的有 8000 Hz、16000 Hz、44100 Hz 和 48000 Hz 等。

    29.常见的音频压缩方式,优缺点

    1)有损压缩(Lossy Compression):
    有损压缩是通过舍弃一些音频数据以减小文件大小,从而导致音频质量的损失。这种压缩方式在减小文件大小的同时,会牺牲一定的音质。常见的有损压缩格式有 MP3、AAC、OGG 等。

    • 优点:
      文件大小较小,节省存储空间和传输带宽。
      适用于网络传输和音乐在线播放,节省流量和加载时间。
    • 缺点:
      音质有损失,尤其是高比特率压缩后的音质相对原始音频较差。
      不适合对音质要求较高的专业音频应用。

    2)无损压缩(Lossless Compression):
    无损压缩是通过压缩音频数据,但在解压缩后可以完全还原原始音频数据,不会损失任何音质。常见的无损压缩格式有 FLAC、ALAC、WAV 等。

    • 优点:
      完全保留原始音频质量,没有音质损失。
      适用于对音质要求较高的专业音频存储和处理。
    • 缺点:
      文件大小通常较大,占用较多的存储空间和传输带宽。
      不适合网络传输和音乐在线播放,加载时间较长。
    29.算法:链表的反转

    最优解通常是使用迭代法实现。

    // 反转链表函数
    ListNode* reverseLinkedList(ListNode *head) {
        ListNode *prev = nil;
        ListNode *current = head;
        
        while (current != nil) {
            ListNode *nextTemp = current.next;
            current.next = prev;
            prev = current;
            current = nextTemp;
        }
        
        return prev; // 返回新的头节点
    }
    
    30.算法:查找

    1)线性查找:
    线性查找是最简单的查找算法,它逐个遍历数据集合,找到目标元素时返回其位置或值。

    // 线性查找算法实现
    - (NSInteger)linearSearch:(NSArray *)array target:(NSInteger)targetValue {
        for (NSInteger i = 0; i < array.count; i++) {
            if ([array[i] integerValue] == targetValue) {
                return i; // 找到目标元素,返回其位置
            }
        }
        return NSNotFound; // 未找到目标元素,返回 NSNotFound
    }
    

    2)二分查找:
    二分查找是一种高效的查找算法,但要求数据集合已经排好序。它通过反复将查找范围折半,缩小查找范围,最终找到目标元素或判定目标元素不存在。

    // 二分查找算法实现
    - (NSInteger)binarySearch:(NSArray *)sortedArray target:(NSInteger)targetValue {
        NSInteger left = 0;
        NSInteger right = sortedArray.count - 1;
    
        while (left <= right) {
            NSInteger mid = (left + right) / 2;
            NSInteger midValue = [sortedArray[mid] integerValue];
    
            if (midValue == targetValue) {
                return mid; // 找到目标元素,返回其位置
            } else if (midValue < targetValue) {
                left = mid + 1; // 目标值在右半部分,缩小查找范围到右边
            } else {
                right = mid - 1; // 目标值在左半部分,缩小查找范围到左边
            }
        }
    
        return NSNotFound; // 未找到目标元素,返回 NSNotFound
    }
    
    31.算法:排序

    1)快速排序(Quick Sort):
    快速排序是一种高效的排序算法,它采用分治的思想,通过递归地将列表分成较小的子列表并分别排序,然后将子列表合并成最终的有序列表。(最优解,适用数据量大)

    // 快速排序算法实现
    - (NSArray *)quickSort:(NSArray *)unsortedArray {
        if (unsortedArray.count <= 1) {
            return unsortedArray;
        }
    
        NSInteger pivotIndex = unsortedArray.count / 2;
        NSInteger pivotValue = [unsortedArray[pivotIndex] integerValue];
        NSMutableArray *less = [NSMutableArray array];
        NSMutableArray *greater = [NSMutableArray array];
    
        for (NSInteger i = 0; i < unsortedArray.count; i++) {
            if (i == pivotIndex) {
                continue;
            }
    
            NSInteger value = [unsortedArray[i] integerValue];
            if (value < pivotValue) {
                [less addObject:unsortedArray[i]];
            } else {
                [greater addObject:unsortedArray[i]];
            }
        }
    
        NSArray *sortedLess = [self quickSort:less];
        NSArray *sortedGreater = [self quickSort:greater];
    
        NSMutableArray *sortedArray = [NSMutableArray array];
        [sortedArray addObjectsFromArray:sortedLess];
        [sortedArray addObject:@(pivotValue)];
        [sortedArray addObjectsFromArray:sortedGreater];
    
        return [sortedArray copy];
    }
    

    2)插入排序(Insertion Sort):
    插入排序是一种简单直观的排序算法,它将列表分为已排序区和未排序区,逐个将未排序区的元素插入到已排序区的正确位置。(最直观,适用数据量小)

    // 插入排序算法实现
    - (NSArray *)insertionSort:(NSArray *)unsortedArray {
        NSMutableArray *sortedArray = [unsortedArray mutableCopy];
        NSInteger n = sortedArray.count;
    
        for (NSInteger i = 1; i < n; i++) {
            NSInteger current = [sortedArray[i] integerValue];
            NSInteger j = i - 1;
    
            while (j >= 0 && [sortedArray[j] compare:@(current)] == NSOrderedDescending) {
                sortedArray[j + 1] = sortedArray[j];
                j--;
            }
    
            sortedArray[j + 1] = @(current);
        }
    
        return [sortedArray copy];
    }
    

    时间复杂度: 平均情况下,快速排序的时间复杂度为 O(n log n),是一种高效的排序算法。
    空间复杂度: 快速排序是原地排序算法,不需要额外的空间开销,只需要在原始数组上进行元素交换和移动。
    稳定性: 快速排序是一种不稳定的排序算法,即相等元素的相对顺序可能发生改变。

    32.算法:hash算法

    哈希算法(Hash算法)是一种将任意长度的数据映射成固定长度散列值(hash值)的算法。它可以将输入数据转换为固定长度的数字串,通常是一个固定长度的二进制序列。
    iOS中使用的常见哈希算法包括MD5、SHA-1、SHA-256等。然而,由于MD5和SHA-1存在安全性问题,现在更推荐使用更强大的哈希算法,如SHA-256。在iOS开发中,可以使用系统提供的CommonCrypto库或第三方的开源库来实现这些哈希算法。

    33.iOS单元测试的简单流程

    iOS单元测试是用于测试应用程序中独立的、最小的功能单元的一种测试方法。
    可以用于测试算法,测试网络请求,测试ui,测试性能等。

    36.面向对象的六大设计原则

    https://www.jianshu.com/p/a81306e0e4c4
    1)单一职责原则 (Single Responsibility Principle - SRP):
    一个类应该只负责一项功能,这样可以使类的设计更加简单和清晰,也更容易维护。
    2)开放封闭原则 (Open/Closed Principle - OCP):
    软件实体(类、模块、函数等)应该对扩展是开放的,对修改是封闭的。这意味着应该通过扩展现有的实体来实现变化,而不是直接修改它们。
    3)里氏替换原则 (Liskov Substitution Principle - LSP)
    子类对象应该能够替换父类对象而不影响程序的正确性。也就是说,一个基类的对象在任何使用基类的地方都可以被其子类所替代,而不会产生意外或错误的结果。
    4)接口隔离原则 (Interface Segregation Principle - ISP):
    不应该强迫客户端依赖于它们不使用的接口。一个类不应该实现它用不到的接口,而应该将接口细分为更小的粒度,使得客户端只需知道它们需要使用的方法。
    5)依赖倒置原则 (Dependency Inversion Principle - DIP):
    高层模块不应该依赖于低层模块,它们都应该依赖于抽象。抽象不应该依赖于细节,而细节应该依赖于抽象。这样可以减少模块间的耦合,使得系统更加灵活和易于扩展。
    6)迪米特法则 (Law of Demeter / Least Knowledge Principle - LoD):
    一个对象应该对其他对象有尽可能少的了解,只与自己直接的朋友通信。这样可以降低对象之间的依赖关系,减少耦合,提高代码的可维护性和复用性。

    相关文章

      网友评论

          本文标题:面试(全)

          本文链接:https://www.haomeiwen.com/subject/dqahpdtx.html