美文网首页
2023iOS面试题总结

2023iOS面试题总结

作者: iOS_Programmer | 来源:发表于2023-04-02 16:41 被阅读0次

    1.为什么说Objective-C是一门动态的语言?

    Objective-C被称为一门动态的语言,因为它允许在程序运行时进行许多决策和操作,而不需要在编译时进行决策或指定类型。以下是Objective-C被认为是一门动态语言的原因:

    运行时类型确定性:Objective-C使用运行时动态确定对象类型,因此不需要在编译时确定对象的类型,这意味着对象的类型可以在程序运行时动态更改。

    动态绑定:Objective-C的方法调用是在运行时进行绑定的,这意味着编译器不需要在编译时知道对象的实际类型。方法的实现可以在程序运行时根据对象类型进行动态选择。

    动态消息传递:Objective-C的消息传递是动态的,这意味着它允许在程序运行时动态调用对象的方法。

    运行时类型信息:Objective-C运行时系统提供了大量的函数和API,可以查询和操作对象的类型信息,比如类、方法、属性等。

    这些特性使得Objective-C适合于动态类型系统和运行时环境的开发,也让它成为了iOS和MacOS应用程序开发中不可或缺的一部分。

    2.OC中的成员变量和属性是什么区别?

    访问权限不同:成员变量是类的内部数据,只能在类的实现中访问,而属性可以在类的接口中定义,可以被其他类访问。

    封装性不同:属性提供了一种封装成员变量的机制,可以将成员变量隐藏起来,只提供getter和setter方法供外部访问,从而保护成员变量的数据安全性和完整性。

    存储方式不同:成员变量的存储方式可以是实例变量或静态变量,而属性的存储方式可以是实例变量、静态变量或计算属性,具体取决于属性的定义方式。

    实现方式不同:属性的实现通常需要使用@synthesize(xcode4.4之前)指令来自动生成getter和setter方法的实现,而成员变量不需要。

    内存管理方式不同:属性的内存管理可以使用ARC(自动引用计数)或手动管理,而成员变量的内存管理通常需要手动管理。

    总的来说,属性是对成员变量的一种封装,提供了更好的封装性、访问控制和内存管理机制,而成员变量则是属性的基础,是类中数据的实际存储和访问方式。

    3.KVO的基本原理

    KVO(键值观察)是Objective-C语言中一种常用的观察者模式,用于监听对象的属性变化,以便在属性变化时执行相应的操作。KVO的基本原理如下:

    当一个对象的属性被观察时,系统会为该对象动态生成一个子类,子类名为NSKVONotifying_父类名,重写该对象的setter方法,并在setter方法中添加一些监听操作,比如通知观察者属性变化等。

    当观察的属性发生变化时,被观察对象会调用setter方法,该方法被重写过,会在原有的setter方法中添加一些通知操作,通知系统属性发生了变化。

    系统收到属性变化通知后,会向观察者发送通知消息,并在通知消息中包含属性的变化信息,观察者接收到通知后可以执行相应的操作。

    KVO的实现依赖于Objective-C语言的动态特性和Runtime机制。它利用Runtime提供的函数,动态生成子类并重写setter方法,实现了对对象属性的动态监听和通知功能。

    键值观察通知依赖于NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey:;在一个被观察属性发生改变之前, willChangeValueForKey:一定会被调用,这就 会记录旧的值。而当改变发生后,didChangeValueForKey:会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。

    需要注意的是,KVO监听的是属性而非变量,因此只有通过属性访问对象的变量才会触发KVO通知。此外,KVO机制也有一些限制和注意事项,如不支持监听类方法、私有属性和访问器方法等,同时也存在可能导致循环引用和性能问题的情况。因此,在使用KVO时需要注意其适用范围和使用方法,以确保程序的正确性和性能。

    4.isa指针是什么

    在 Objective-C 中,每个对象都有一个指向其类的指针,称为 isa 指针。isa 指针实际上是一个指针,指向对象的类或元类。

    当我们调用一个对象的方法时,Objective-C 运行时会根据该对象的 isa 指针找到该对象的类,然后在该类及其父类中查找相应的方法实现,并执行该方法。如果在该类及其父类中都找不到相应的方法实现,就会执行消息转发流程,尝试动态解析和转发消息。

    因此,isa 指针在 Objective-C 中扮演着非常重要的角色,它决定了对象的类型和行为,并支持了 Objective-C 的动态特性。在对象创建时,isa 指针会被初始化为指向该对象的类,然后在运行时根据需要进行动态调整,以适应各种不同的需求和场景。

    5.OC中的消息转发机制原理?

    Objective-C 中的消息转发机制是指当一个对象接收到一个它无法识别的消息时,它会尝试通过动态解析、备用接收者和完整的消息转发机制等多种方式,让其他对象来处理该消息。


    消息转发机制

    消息转发机制主要包括以下几个步骤:

    动态解析:当一个对象接收到一个无法识别的消息时,Objective-C 运行时会调用该对象的resolveInstanceMethod:或resolveClassMethod:方法,尝试动态解析并添加一个新的方法实现。如果动态解析成功,就会重新执行一次消息发送,让新的方法实现来处理该消息。

    备用接收者:如果动态解析失败,Objective-C 运行时会调用forwardingTargetForSelector:方法,尝试把该消息转发给另一个对象来处理。在该方法中,我们可以返回一个备用接收者对象,让它来接收并处理该消息。如果备用接收者对象也无法处理该消息,就会继续执行下一步。

    完整消息转发:如果备用接收者也不存在或者不合适,Objective-C 运行时会调用methodSignatureForSelector:方法和forwardInvocation:方法,进行完整的消息转发。在methodSignatureForSelector:方法中,我们需要返回一个方法签名对象,来描述该消息的参数和返回值类型等信息。在forwardInvocation:方法中,我们需要手动构造一个 NSInvocation 对象,并将该消息转发给另一个对象来处理。在该方法中,我们可以自由地修改 NSInvocation 对象的参数和返回值等信息,以达到我们想要的目的。

    通过上述三个步骤,我们可以实现非常灵活和强大的消息转发机制,来处理各种不同的消息和异常情况。在实际应用中,我们可以通过重写上述方法,来自定义对象的消息转发逻辑,实现更加高效和安全的代码。

    6.weak的理解

    在 Objective-C 中,对象之间的引用关系是通过指针来实现的。如果一个对象 A 持有另一个对象 B 的强引用,那么 B 对象的引用计数器会加 1,表示有一个强引用指向它;如果 A 对象持有 B 对象的弱引用,那么 B 对象的引用计数器不会增加,因为它只有一个弱引用指向它,不算是真正的引用。

    在实现 weak 属性时,其本质是通过维护一个指向对象的全局 hash 表来实现的。当一个对象被设置为某个对象的 weak 属性时,实际上是将这个对象加入到全局 hash 表中,并将这个属性的指针指向这个对象在 hash 表中的位置。当这个对象被销毁时,会将其从全局 hash 表中删除,并将这个属性的指针设置为 nil。

    因此,当一个对象 A 持有一个对象 B 的弱引用时,即使对象 A 被销毁了,对象 B 的引用计数器不会减少,因为它只有一个弱引用指向它,不算是真正的引用。当对象 B 被销毁时,会将其从全局 hash 表中删除,并将 A 对象的 weak 属性指针设置为 nil。

    7.iOS应用程序启动时,main()方法之前会进行哪些操作?

    加载可执行文件:iOS操作系统会将应用程序的可执行文件加载到内存中。

    运行时连接:iOS操作系统会对应用程序的可执行文件进行运行时连接,以解析符号和符号表之间的引用。

    初始化代码:iOS操作系统会执行可执行文件中的attribute((constructor))修饰的函数,这些函数会在main()方法之前执行,用于初始化代码。

    UIApplicationMain()方法调用:iOS操作系统会调用UIApplicationMain()方法,该方法会创建应用程序对象,并设置应用程序的运行循环。

    加载main.storyboard或MainInterface:如果应用程序使用Interface Builder创建UI,则iOS操作系统会加载应用程序的主要用户界面,即main.storyboard或MainInterface文件。

    应用程序委托的方法调用:在应用程序启动期间,iOS操作系统会调用应用程序委托的方法,以便应用程序可以响应系统事件和应用程序生命周期事件。

    8.为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?

    为什么代理要用weak?
    在使用代理模式时,通常会将代理对象设置为被代理对象的属性。如果代理对象是强引用,那么当被代理对象被释放时,代理对象依然持有对其的引用,导致被代理对象不能正常释放,从而导致内存泄漏。因此,为了避免这种情况的发生,通常将代理对象设置为 weak 引用,使得被代理对象被释放时,代理对象也会被自动释放。
    举个例子,就是控制器和按钮,按钮定义了协议,当使用self.button时候,button被引用,当self.button.delegate= self时候,控制器又被button引用,此时如果用strong修饰delegate,那么就是说控制器是强引用delegate,button也在强引用控制器,控制器销毁时候,引用计数不为0不销毁,就会造成两个互相持有,循环引用.

    代理的 delegate 和 dataSource 有什么区别?
    delegate 和 dataSource 都是代理模式的应用,不同之处在于它们的作用和使用方式。通常,delegate 用于处理代理对象的事件回调,比如 UITableView 的 didSelectRowAt 方法;而 dataSource 则是用于提供数据源,比如 UITableView 的 numberOfRowsInSection 和 cellForRowAt 方法。可以理解为 delegate 处理用户交互,dataSource 处理数据。

    block 和代理的区别?
    block 和代理都是用于解耦的设计模式,它们的作用是将一些逻辑从实现类中剥离出来,让使用者可以自由地定制这些逻辑。不同之处在于,block 是将逻辑封装在一个代码块中,而代理则是将逻辑封装在一个类中,使用者通过实现特定的接口方法来实现这些逻辑。一般来说,使用 block 可以更加灵活和简洁,而代理则更加结构化和易于维护。在实际开发中,可以根据具体情况来选择使用哪种方式。

    9.属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?

    属性(property)是 Objective-C 和 Swift 语言中用于封装对对象成员变量(instance variable)的访问的一种机制。属性的实质是一个方法(method),它封装了对成员变量的访问和操作。

    一个属性通常包括以下几个部分:

    数据类型(type):指定属性的数据类型,例如 int、NSString 等。

    实例变量名(instance variable name):属性所对应的实例变量名。

    存取器(accessors):用于读取和写入属性的值的方法,通常包括 getter 和 setter 方法。

    特性(attributes):用于定义属性的特性,例如原子性、可读写性等。

    属性默认的关键字包括:

    assign:用于指定基本数据类型(如 int、float 等)的属性。

    retain:用于指定对象类型的属性,告诉编译器要对该对象进行 retain 操作。

    copy:用于指定对象类型的属性,告诉编译器要对该对象进行 copy 操作。

    nonatomic:用于指定非原子性,即该属性在多线程环境下不保证线程安全。

    atomic:用于指定原子性,即该属性在多线程环境下保证线程安全。

    @dynamic 关键字表示该属性的存取方法由运行时动态提供,而不是在编译期间生成。通常用于在运行时为一个对象添加属性。

    @synthesize 关键字用于指定一个属性的实现方式。如果一个属性没有手动实现 getter 和 setter 方法,编译器会自动为其生成一个带有默认实现的 getter 和 setter 方法。在这种情况下,可以使用 @synthesize 关键字来指定属性的实现方式,例如实现为某个成员变量。

    10.属性的默认关键字是什么?

    在 Objective-C 中,属性的默认关键字是 atomic 和 readwrite。这意味着属性是原子性的,并且可以被读写。可以通过显式地指定关键字来改变这些默认设置。例如,指定为 nonatomic 可以使属性变为非原子性的。指定为 readonly 可以使属性只读,不能被写入。

    11.(注意:这里没有说用strong就一定不行。使用copy和strong是看情况而定的)

    深拷贝和浅拷贝的区别

    12.iOS 如何令自己所写的对象具有拷贝功能?

    需要实现 NSCopying 协议。NSCopying 协议定义了一个 copyWithZone: 方法,该方法用于复制对象并返回一个新的对象。

    - (id)copyWithZone:(NSZone *)zone {
        MyObject *newObject = [[MyObject allocWithZone:zone] init];
        // 在这里复制对象的属性
        return newObject;
    }
    

    在 copyWithZone: 方法中,需要创建一个新的对象,并将原对象的属性复制到新对象中。需要注意的是,在使用 allocWithZone: 方法时,需要将参数 zone 传递给它。

    完成上述步骤后,就可以在程序中使用 copy 方法来复制该对象。例如:

    MyObject *object1 = [[MyObject alloc] init];
    MyObject *object2 = [object1 copy];
    

    需要注意的是,如果自定义对象中包含了其他对象的引用,那么这些引用也需要被复制。如果这些引用指向的对象也实现了 NSCopying 协议,那么可以使用其 copy 方法来完成复制,否则需要手动复制引用对象的属性。

    13.可变集合类 和 不可变集合类的 copy 和 mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么?

    使用copy时 不可变集合的指针地址以及内存地址都不相同 深复制 可变集合的指针地址不一样但是内存地址一样 属于浅复制(为了节省内存开销)

    使用mutableCopy的时候无论是可变集合还是不可变集合的指针地址和内存地址都不同 都属于深复制

    14.为什么IBOutlet修饰的UIView使用weak关键字?

    因为controller已经强引用拖线进来的view的父视图了,就没必要再强引用这个view了

    15.nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?

    atomic

    是默认的
    对同一对象的set和get的操作是顺序执行的
    速度不快,因为要保证操作整体完成
    线程安全,需要消耗大量系统资源来为属性加锁
    使用atomic并不能保证绝对的线程安全,对于要绝对保证线程安全的操作,还需要使用更高级的方式来处理,比如NSSpinLock、@syncronized等

    nonatomic

    不是默认的
    更快
    如有两个线程访问同一个属性,会出现无法预料的结果
    非线程安全,适合内存较小的移动设备

    在不添加atomic或nonatomic的情况下,默认的是atomic
    setter/getter方法

    平时在自己写setter、getter方法的时候,atomic/nonatomic/retain/assign/copy这些关键字只起提示作用,写不写都一样。
    atomic
    系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。
    nonatomic
    不做保持getter完整性保证,但在运行速度上要比atomic快

    假设有一个 atomic 的属性 "name",如果线程 A 调[self setName:@"A"],线程 B 调[self setName:@"B"],线程 C 调[self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。
    但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
    如果 name 属性是 nonatomic 的,那么上面例子里的所有线程 A、B、C、D 都可以同时执行,可能导致无法预料的结果。如果是 atomic 的,那么 A、B、C 会串行,而 D 还是并行的。

    简单来说,就是 atomic 会加一个锁来基本保障线程安全(但不能保证线程安全),并且引用计数会 +1,来向调用者保证这个对象会一直存在。假如不这样做,如有另一个线程调 setter,可能会出现线程竞态,导致引用计数降到0,原来那个对象就释放掉了。
    @property(nonatomic, retain) UITextField *userName;
    //系统生成的代码如下:

    - (UITextField *) userName {
        return userName;
    }
    
    - (void) setUserName:(UITextField *)userName_ {
        [userName_ retain];
        [userName release];
        userName = userName_;
    }
    复制代码
    @property(retain) UITextField *userName;
    //系统生成的代码如下:
    
    - (UITextField *) userName {
        UITextField *retval = nil;
        @synchronized(self) {
            retval = [[userName retain] autorelease];
        }
        return retval;
    }
    
    - (void) setUserName:(UITextField *)userName_ {
        @synchronized(self) {
          [userName release];
          userName = [userName_ retain];
        }
    }
    

    16.用StoryBoard开发界面有什么弊端?如何避免?

    使用 Storyboard 开发界面的优点在于它能够快速创建用户界面并且方便管理界面的视图层次结构,同时可以使得代码与界面分离,降低代码的耦合度,从而提高代码的可维护性。

    但是,使用 Storyboard 开发界面也存在一些缺点,主要包括以下几个方面:

    文件过大:当 Storyboard 文件中包含大量的视图控制器和视图时,文件会变得非常庞大,导致打开、编辑和保存文件的时间变慢。

    协作困难:由于 Storyboard 文件是二进制格式的文件,不能像代码文件一样进行版本控制,因此在多人协作开发时可能会出现冲突和困难。

    处理复杂场景困难:当需要处理复杂的视图控制器之间的跳转和传递数据时,使用 Storyboard 可能会变得非常困难,需要考虑到界面之间的关系和复杂度。

    为了避免上述问题,可以采取以下几种方法:

    拆分 Storyboard 文件:将 Storyboard 文件拆分成多个小文件,每个文件负责管理一部分视图控制器和视图,这样可以减小单个文件的大小,提高编辑和保存的效率。

    使用 XIB 文件:对于复杂的界面,可以使用 XIB 文件来替代 Storyboard 文件,每个 XIB 文件只负责管理一个视图或一个视图控制器,这样可以使得文件更加轻量化。

    使用代码实现界面:对于特别复杂的场景,可以使用代码来实现界面,这样可以更好地控制界面的复杂度和灵活性。

    做好版本控制:虽然 Storyboard 文件不能像代码文件一样进行版本控制,但可以使用一些工具和技巧来做好版本控制,例如将 Storyboard 文件转换成 XML 格式进行版本控制,或者使用 Git LFS 等工具来管理大型二进制文件。

    注意 Storyboard 文件的命名规范:为了方便管理和维护,需要注意 Storyboard 文件的命名规范,最好使用有意义的命名方式,避免命名重复和混淆。

    17.UICollectionView自定义layout如何实现?

    18.进程和线程的区别?同步异步的区别?并行和并发的区别?

    进程是操作系统资源分配的基本单位,一个进程可以包含多个线程;
    线程是程序执行的最小单位,一个进程中的多个线程共享进程的内存和资源,但每个线程都有自己的栈和局部变量等。
    同步和异步的区别:

    同步是指一个任务完成后才能执行下一个任务,即一个任务执行完之后等待其返回结果再执行下一个任务;
    异步是指多个任务并发执行,不需要等待上一个任务返回结果即可执行下一个任务。
    并行和并发的区别:

    并行是指多个任务同时执行,即在多个 CPU 上同时执行多个任务;
    并发是指多个任务交替执行,即在单个 CPU 上轮流执行多个任务,每个任务只执行一小段时间,然后切换到下一个任务。
    在 iOS 中,通常使用 GCD(Grand Central Dispatch)进行多线程编程,其中使用队列管理任务,队列分为串行队列和并行队列,通过选择不同的队列和执行方式,可以实现同步、异步、并行和并发等不同的执行方式。

    19.iOS线程间如何通信

    GCD(Grand Central Dispatch):GCD 提供了一种简单易用的线程间通信方式。可以使用 dispatch_async 将任务提交到另一个队列中异步执行,并在执行完成后使用 dispatch_async 回到主队列中更新 UI。
    例如,在后台队列中执行耗时操作,然后在主队列中更新 UI:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 耗时操作
        NSString *result = [self doSomeWork];
    
        dispatch_async(dispatch_get_main_queue(), ^{
            // 更新 UI
            self.label.text = result;
        });
    });
    

    NSOperationQueue:NSOperationQueue 也提供了一种简单易用的线程间通信方式。可以使用 addOperationWithBlock 将任务提交到队列中异步执行,并在执行完成后使用 addOperationWithBlock 回到主队列中更新 UI。
    例如,在后台队列中执行耗时操作,然后在主队列中更新 UI:

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperationWithBlock:^{
        // 耗时操作
        NSString *result = [self doSomeWork];
    
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 更新 UI
            self.label.text = result;
        }];
    }];
    

    performSelectorOnMainThread:可以使用 performSelectorOnMainThread 方法将任务提交到主线程中异步执行,并在执行完成后更新 UI。
    例如,在后台线程中执行耗时操作,然后在主线程中更新 UI:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // 耗时操作
        NSString *result = [self doSomeWork];
    
        [self performSelectorOnMainThread:@selector(updateUI:) withObject:result waitUntilDone:NO];
    });
    
    - (void)updateUI:(NSString *)result {
        // 更新 UI
        self.label.text = result;
    }
    

    需要注意的是,在使用这些方式进行线程间通信时,需要避免线程安全问题,例如多个线程同时更新同一 UI 控件等。为了避免这种情况,可以使用同步锁或信号量等机制进行线程同步。

    19.GCD的一些常用的函数?(group,barrier,信号量,线程同步)

    20.如何使用队列来避免资源抢夺?

    当我们使用多线程来访问同一个数据的时候,就有可能造成数据的不准确性。这个时候我么可以使用线程锁的来来绑定。也是可以使用串行队列来完成。如:fmdb就是使用FMDatabaseQueue,来解决多线程抢夺资源。

    21.数据持久化方案?

    plist,存储字典,数组比较好用
    preference:偏好设置,实质也是plist
    NSKeyedArchiver:归档,可以存储对象
    sqlite:数据库,经常使用第三方来操作,也就是[fmdb]
    keychain:钥匙串,内部c语言实现,Keychain 是一种安全的存储机制,用于在 App 之间存储和共享敏感信息,例如密码、证书、私钥等.
    同一开发者的 App 才能共享 keychain 数据。也就是说,不同开发者的 App 是无法访问彼此的 keychain 数据的。
    App 需要使用相同的 App Group Identifier。在 App 的 entitlements 文件中设置 App Group Identifier,可以让多个 App 共享同一个 keychain 数据。
    App 需要具有正确的 keychain 访问权限。在 App 的 entitlements 文件中设置 keychain 访问权限,例如:kSecAttrAccessibleAfterFirstUnlock。

    22.fmdb内部实现原理

    SQLite C语言库:FMDB底层使用了SQLite C语言库作为数据库引擎。SQLite是一种嵌入式数据库,它将数据库引擎嵌入到应用程序中,不需要独立的服务器进程,可以直接访问和管理数据库文件。

    Objective-C封装:FMDB使用Objective-C进行封装,提供了面向对象的API来操作数据库。它将SQLite C语言库的底层操作封装成了Objective-C的方法,使得在iOS和macOS平台上使用起来更加方便和简洁。

    数据库连接与操作:FMDB使用SQLite的连接对象(sqlite3)来连接和操作数据库。连接对象用于与数据库建立连接、执行SQL语句、处理事务等操作。

    数据库操作队列:FMDB使用FMDatabaseQueue类来管理数据库操作队列,实现了多线程并发操作数据库的能力。FMDatabaseQueue使用了GCD(Grand Central Dispatch)来管理多线程操作,保证了数据库的线程安全性。

    SQL语句拼装与执行:FMDB使用字符串拼装的方式来生成SQL语句,并通过sqlite3_exec函数来执行SQL语句。FMDB提供了多种方式来执行SQL语句,包括查询、更新、删除等操作。

    结果集处理:FMDB使用FMResultSet类来处理SQL查询语句的结果集。FMResultSet封装了SQLite C语言库的结果集对象(sqlite3_stmt),提供了方便的方法来遍历查询结果。

    错误处理:FMDB通过NSError类来处理数据库操作中的错误,例如连接失败、SQL语句执行错误等。错误信息可以通过NSError对象的属性来获取和处理。

    23.NSCache是什么

    NSCache是iOS中的一个缓存类,用于在内存中存储临时对象,以便在需要时可以快速访问,从而提高应用程序的性能。

    NSCache提供了一个键值对存储的方式,其中键是一个NSObject类型的对象,而值可以是任何NSObject的子类对象。NSCache会在内存不足时自动进行清理,以便释放内存空间。

    NSCache的特点包括:

    自动内存管理:NSCache使用弱引用来管理存储的对象,这意味着当对象在应用程序的其他地方被释放时,NSCache会自动将其从缓存中删除,从而避免了悬空指针的问题。

    自动清理:NSCache会根据系统内存情况自动清理缓存,以便在内存不足时释放空间,防止应用程序因内存占用过高而崩溃。

    可设置的缓存上限:NSCache提供了一个totalCostLimit属性,可以设置缓存的总成本上限,当缓存的总成本超过该限制时,NSCache会自动进行清理,以便保持缓存的大小在可控范围内。

    线程安全:NSCache是线程安全的,可以在多个线程中同时访问和修改缓存。

    NSCache通常用于存储一些需要临时保存在内存中的数据,例如网络请求的响应数据、解析后的图片数据等。但需要注意的是,由于NSCache是基于内存的缓存,当应用程序退出或被终止时,缓存中的数据会被清空,因此不适合用于持久化存储。如需持久化存储数据,可以考虑其他方式,如NSUserDefaults、文件存储等。同时,NSCache也不适合用于需要频繁读写的大量数据,因为频繁的缓存清理可能会对性能产生影响。在选择缓存方式时,应根据具体的业务需求和数据特点进行选择。

    24.堆和栈区别

    在计算机科学中,堆和栈都是数据结构。堆和栈在内存分配中有不同的用途和区别。

    栈是一种数据结构,它是一种先进后出的数据结构。栈通常用于函数调用,其中每个函数都会创建自己的局部变量,并且这些局部变量会随着函数返回而销毁。栈内存是由操作系统自动分配和释放的,因此不需要程序员手动管理。

    堆是另一种数据结构,用于在程序运行时动态分配内存。堆内存需要程序员手动分配和释放。堆内存的生命周期由程序员控制,这使得堆能够存储更大的数据。

    堆和栈之间的最大区别在于它们的分配方式和管理方式。栈内存由操作系统自动分配和管理,它通常是一个固定大小的内存区域,不需要程序员显式释放。而堆内存则由程序员自己分配和管理,它的大小通常是动态变化的,程序员需要显式地释放内存。

    在iOS中,栈主要用于存储函数和方法的调用栈,而堆则用于存储动态分配的对象、数组、字符串等数据。在Objective-C中,对象通常是在堆上动态分配内存的,因此需要程序员负责管理对象的内存。而对于基本类型和结构体,它们通常存储在栈上,无需手动释放内存。

    25.block的实质是什么?一共有几种block?都是什么情况下生成的?

    block:本质就是一个[object-c对象] block:存储位置,可能分为3个地方:代码去,堆区、栈区(ARC情况下会自动拷贝到堆区,因此ARC下只能有两个地方:代码去、堆区)
    代码区(全局block):不访问栈区的变量(如局部变量),且不访问堆区的变量(alloc创建的对象),此时block存放在代码去。
    堆区:访问了处于栈区的变量,或者堆区的变量,此时block存放在堆区。–需要注意实际是放在栈区,在ARC情况下会自动拷贝到堆区,如果不是ARC则存放在栈区,所在函数执行完毕就回释放,想再外面调用需要用copy指向它,这样就拷贝到了堆区,strong属性不会拷贝、会造成野指针错区。

    26.block为什么要用copy而不是strong来修饰

    在 Objective-C 中,Block 对象会在栈(stack)上创建,而栈上的内存是很容易被回收的。如果我们需要在 Block 对象被创建之后,仍然可以正常访问其内部的变量,就需要使用 Block 的 copy 属性来进行复制,使其在堆(heap)上分配内存。这样就可以保证 Block 对象在复制之后仍然可以被正常访问,不会出现访问已经被回收的内存的问题。因此,对于需要在 Block 对象被复制后仍然需要访问其内部变量的情况,需要使用 copy 属性。

    因为block变量默认是声明为栈变量的,为了能够在block的声明域外使用,所以要把block拷贝(copy)到堆。
    block本质是对象,可以retain,和release。但是,block在创建的时候,它的内存是分配在栈上的,而不是在堆上。他本身的作于域是属于创建时候的作用域,一旦在创建时候的作用域外面调用block将导致程序崩溃。因为栈区的特点就是创建的对象随时可能被销毁,一旦被销毁后续再次调用空对象就可能会造成程序崩溃。在对block进行copy后,block存放在堆区. 使用retain也可以,但是block的retain行为默认是用copy的行为实现的,

    ARC下, 使用copy与strong其实都一样, 因为block的retain就是用copy来实现的, 所以在ARC下 block使用copy 和 strong 都可以.
    为了保证修饰符和block特性的一致性,使用copy修饰符仍然是最为合适的。

    27.block循环引用

    Block的循环引用指的是在Block内部持有了外部对象,并且该外部对象也持有了Block,从而形成了一个循环引用的情况。这种情况下,由于Block持有了外部对象,导致该外部对象不能被释放,从而造成了内存泄漏。

    举个例子来说明:假设有一个ViewController,在其中有一个按钮,当按钮被点击时,会执行一个Block:

    @interface ViewController : UIViewController
    @property (nonatomic, copy) void (^myBlock)(void);
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        __weak typeof(self) weakSelf = self;
        self.myBlock = ^{
            [weakSelf doSomething]; // 访问weakSelf,避免循环引用
        };
    }
    
    - (void)doSomething {
        NSLog(@"do something");
    }
    
    @end
    

    上述代码中,myBlock持有了self,self持有了myBlock,从而形成了一个循环引用。为了解决这个问题,我们使用__weak关键字将self弱引用,然后在Block中访问weakSelf,这样就可以避免循环引用的问题。

    28.为什么一些常用的第三方或者系统的block不会造成循环引用?

    在block中,并不是所有的block都会循造成环引用,比如UIView动画block、Masonry添加约束block、AFN网络请求回调block等。

    UIView动画block不会造成循环引用是因为这是类方法,不可能强引用一个类,所以不会造成循环引用。

    • (void)animateWithDuration:(NSTimeInterval)duration animations:是类方法,当前控制器无法强引用一个类,所以循环引用无法构成。

    Masonry约束block不会造成循环引用是因为self并没有持有block,所以我们使用Masonry的时候不需要担心循环引用。

    AFN请求回调block不会造成循环引用是因为在内部做了处理。

    block先是被AFURLSessionManagerTaskDelegate对象持有。而AFURLSessionManagerTaskDelegate对象被mutableTaskDelegatesKeyedByTaskIdentifier字典持有,在block执行完成后,mutableTaskDelegatesKeyedByTaskIdentifier字典会移除AFURLSessionManagerTaskDelegate对象,这样对象就被释放了,所以不会造成循环引用。

    29.为什么在默认情况下无法修改被block捕获的变量? __block都做了什么?

    在默认情况下,被 block 捕获的变量是以值的形式传递到 block 内部的,而不是以指针的形式传递的。也就是说,block 内部所使用的变量是 block 外部变量的一个副本,对 block 内部变量的修改不会影响到外部变量的值。

    为了能够修改外部变量的值,需要在变量的声明前加上 __block 关键字,表示在 block 内部可以修改该变量的值。在使用 __block 修饰的变量被 block 捕获时,实际上是将该变量包装成了一个对象,block 内部持有该对象的指针,因此可以修改该对象的值。

    在内存管理方面,被 __block 修饰的变量会被捕获到 block 内部的结构体中,该结构体在 block 内外都存在一个指针指向它。在使用 ARC 的情况下,由于该结构体需要在 block 内外都存在,因此需要在 block 内部对该结构体进行拷贝,否则在 block 内部使用该结构体的指针时可能会出现野指针的问题。因此,在 ARC 中默认情况下,被 __block 修饰的变量会被强制转成指向堆上的对象,并且在 block 内部使用时会进行拷贝。为了避免在 block 内外出现不一致的问题,需要使用 __weak 或者 __unsafe_unretained 关键字来修饰被捕获的对象。

    30.objc在向一个对象发送消息时,发生了什么?

    当向一个Objective-C对象发送消息时,发生了以下几个步骤:

    1.编译器首先检查这个对象是否能够接收这个消息。编译器会检查这个对象的类的方法列表和实例变量列表,看这个对象是否实现了该方法。

    2.如果对象没有实现该方法,编译器会在当前类和它的父类中查找方法的实现。如果找到了,则会生成一条消息发送指令,指令包含了要调用的方法的名称以及指向该方法实现的指针。

    3.如果没有找到该方法的实现,则会触发运行时的消息转发机制,Objective-C运行时会调用- (id)forwardingTargetForSelector:(SEL)aSelector方法,尝试将该消息转发给另一个对象进行处理。如果该方法返回了一个非空对象,则消息会被转发给这个对象;否则,Objective-C运行时会继续执行下一步。

    4.运行时会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,获取该方法的参数和返回值类型信息。如果该方法返回了一个非空值,则继续执行下一步;否则,Objective-C运行时会认为该消息无效,并抛出unrecognized selector sent to instance异常。

    5.运行时会调用- (void)forwardInvocation:(NSInvocation *)anInvocation方法,将该消息的参数和方法签名封装在NSInvocation对象中,并将其作为参数传递给该方法。在该方法中,可以将该消息转发给另一个对象进行处理,也可以修改NSInvocation对象中的参数和返回值,以改变消息的处理结果。

    6.如果实现了- (void)doesNotRecognizeSelector:(SEL)aSelector方法,则在以上所有步骤都失败后,Objective-C运行时会调用该方法,抛出unrecognized selector sent to instance异常。

    总之,向一个Objective-C对象发送消息,本质上是在调用该对象的方法,如果对象没有实现该方法,则会通过消息转发机制寻找能够处理该消息的对象。

    当发送消息的时候,我们会根据类里面的methodLists列表去查询我们要动用的SEL,当查询不到的时候,我们会一直沿着父类查询,当最终查询不到的时候我们会报unrecognized selector错误
    当系统查询不到方法的时候,会调用+(BOOL)resolveInstanceMethod:(SEL)sel动态解释的方法来给我一次机会来添加,调用不到的方法。或者我们可以再次使用-(id)forwardingTargetForSelector:(SEL)aSelector重定向的方法来告诉系统,该调用什么方法,一来保证不会崩溃。

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

    1.不能向编译后得到的类增加实例变量
    2.能向运行时创建的类中添加实例变量
    解释:
    1.编译后的类已经注册在runtime中,类结构体中的objc_ivar_list实例变量的链表和instance_size实例变量的内存大小已经确定,runtime会调用class_setvarlayout或class_setWeaklvarLayout来处理strong weak引用.所以不能向存在的类中添加实例变量
    2.运行时创建的类是可以添加实例变量,调用class_addIvar函数.但是的在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上.

    32.runloop是来做什么的?runloop和线程有什么关系?主线程默认开启了runloop么?子线程呢?

    RunLoop是iOS中的一种机制,它是用来处理事件并保持线程不被过早释放的机制。其作用是用来监听输入源(Input Source)和定时源(Timer Source),如触摸事件、UI刷新、网络请求等。当输入源和定时源发生时,会通过RunLoop将其转发到对应的处理函数中去执行。

    RunLoop和线程是紧密相关的,每个线程都有且仅有一个RunLoop对象,当RunLoop被创建时,系统会自动关联一个线程,RunLoop会在这个线程中不断运行。RunLoop也是一个事件循环机制,它在等待处理事件时会进入休眠状态,节约CPU资源。当事件到来时,RunLoop会自动唤醒并处理事件,直到没有事件需要处理后又会进入休眠状态。

    在iOS中,主线程默认开启了RunLoop,而子线程默认是没有开启的,需要手动调用run方法来开启RunLoop。RunLoop的存在能够让主线程一直处于运行状态,可以接收用户的交互事件并进行相应的处理,从而保证应用程序的正常运行。而在子线程中使用RunLoop可以实现长期的异步操作,如网络请求、定时器等,避免了线程过早结束导致资源浪费。

    33.runloop的mode是用来做什么的?有几种mode?

    Runloop的mode是用来描述Runloop运行的模式,即Runloop可以处理哪些事件以及优先级,从而控制Runloop的行为。每个mode都包含一组特定的输入源、定时器和观察者。当Runloop运行时,它只会处理当前mode中的输入源、定时器和观察者。

    iOS中常用的Runloop Mode有以下几种:

    NSDefaultRunLoopMode:默认模式,处理NSConnection、NSURLConnection等事件。
    UITrackingRunLoopMode:处理UIScrollView滑动事件。
    NSRunLoopCommonModes:默认包含NSDefaultRunLoopMode和UITrackingRunLoopMode,可以同时处理这两种事件。
    在使用Runloop时,可以通过调用CFRunLoopAddCommonMode方法将自定义的mode添加到NSRunLoopCommonModes中,从而让其可以同时处理多种事件。例如,可以将自定义的mode添加到NSRunLoopCommonModes中,以便处理网络请求和定时器事件。

    34.苹果是如何实现Autorelease Pool的?

    在 Objective-C 中,Autorelease Pool 用于管理对象的生命周期,防止内存泄漏。在 Cocoa 程序中,每个线程都有一个 Autorelease Pool,当代码在每个 Autorelease Pool 中完成后,所有对象会被释放掉。

    苹果实现 Autorelease Pool 主要依赖了两个 C++ 类:AutoreleasePoolPage 和 AutoreleasePool。在每个 Autorelease Pool 中,都有一个 AutoreleasePoolPage 对象。每当创建一个新的 Autorelease Pool 时,就会创建一个新的 AutoreleasePoolPage 对象,并将它添加到 Autorelease Pool 的栈顶。在 Autorelease Pool 中调用 autorelease 方法时,会将对象加入到 AutoreleasePoolPage 对象中。

    当 AutoreleasePool 的作用域结束时,会调用 pop 方法,将栈顶的 AutoreleasePoolPage 对象弹出并销毁。在销毁 AutoreleasePoolPage 对象时,会对其中所有的对象发送一次 release 消息,释放这些对象的内存。

    总的来说,Autorelease Pool 是通过一个栈来管理 AutoreleasePoolPage 对象的。当栈顶的 Autorelease Pool 退出作用域时,会自动销毁 AutoreleasePoolPage 对象并释放其中所有对象的内存。这个机制使得程序员可以更加方便地管理对象的生命周期,同时也避免了内存泄漏的风险。

    虽然ARC(Automatic Reference Counting,自动引用计数)自动管理内存,但Autorelease Pool在ARC中仍然有它的作用。

    在ARC中,我们不再需要手动添加release或autorelease方法,而是由编译器在编译时自动生成。但是,当我们需要在代码块中手动管理内存时,我们仍然需要使用Autorelease Pool。例如,在循环中创建大量的临时对象时,我们可以在每次循环中创建一个Autorelease Pool,在循环结束后释放临时对象,以便及时释放内存。

    因此,在ARC中,Autorelease Pool仍然是非常有用的。

    在 iOS 开发中,我们通常会使用 autorelease pool 来管理内存。例如,当我们需要在一个循环中创建大量临时对象时,可以使用自动释放池来及时释放这些对象,避免内存占用过大。另外,在某些多线程的场景下,我们也需要使用自动释放池来避免内存泄漏的问题。

    举一个简单的例子:假设我们需要在一个循环中创建 100 个临时对象,如果没有自动释放池,那么这些对象就会一直存在于内存中,占用大量内存。而使用自动释放池,我们可以将这些临时对象添加到自动释放池中,在循环结束时自动释放这些对象,避免内存占用过大的问题。以下是示例代码:

    for (int i = 0; i < 100; i++) {
        @autoreleasepool {
            // 创建临时对象
            NSString *tempString = [NSString stringWithFormat:@"Temp String %d", i];
            // 执行一些操作
            // ...
        }
    }
    

    在这个例子中,我们在循环中使用了自动释放池,每次循环都会创建一个新的自动释放池,并将临时对象添加到自动释放池中。当循环结束时,自动释放池会自动将其中的对象全部释放。

    当你需要在某个循环中创建大量临时对象,但不希望这些对象一直留在内存中时,也可以使用自动释放池。例如,在解析XML文件时,可能需要在循环中创建大量的NSString和NSData对象,但是这些对象在循环结束后就不再需要了。在这种情况下,可以使用自动释放池将这些临时对象放在池中,当循环结束时,自动释放池会将所有对象释放掉,避免了内存泄漏。

    35.介绍一下iOS中的分类,能用分类做什么?内部是如何实现的?它为什么会覆盖掉原来的方法?

    在iOS开发中,分类(Category)是一种在现有类中添加新方法的方式。使用分类可以将一个类分成多个模块,每个模块都可以单独实现和维护。分类通常用于在不改变原始类实现的情况下,给现有类添加额外的功能。

    使用分类可以做很多事情,例如:

    给现有类添加新方法,以便在应用中使用。
    将现有类分成多个模块,每个模块都可以单独实现和维护。
    通过分类来遵循协议,以实现更多的功能。
    在内部实现上,分类会生成一个新的结构体,在运行时会将这个结构体合并到原始类的结构体中。这样,原始类就拥有了分类中定义的新方法。因此,使用分类时需要注意,分类中定义的方法可能会覆盖原始类中的同名方法。

    具体来说,当一个类使用了多个分类,而这些分类中定义了同名的方法时,最终会覆盖掉原来类中的方法。这是因为在运行时,分类会覆盖掉原始类的方法实现,以使得新的方法能够生效。

    需要注意的是,分类并不能添加新的实例变量,因为它只是在原始类的结构体中添加新的方法,而不是添加新的实例变量。因此,如果你需要添加新的实例变量,那么你需要使用类扩展(Class Extension)来实现

    36.UITableview的优化方法(缓存高度,异步绘制,减少层级,hide,避免离屏渲染)

    UITableView是iOS开发中常用的控件之一,用于展示列表数据。由于列表数据可能非常庞大,因此在使用UITableView时需要注意性能优化,以提高应用程序的响应速度和流畅度。

    以下是一些UITableView的优化方法:

    缓存高度:通过实现UITableViewDelegate的heightForRowAtIndexPath方法来计算cell的高度,可以有效提高UITableView的性能。但是如果每次滑动UITableView时都要计算cell的高度,会导致UITableView的滑动卡顿,因此可以通过缓存高度的方式来提高UITableView的性能。可以将计算好的cell高度缓存起来,再次显示cell时直接从缓存中读取高度即可。

    异步绘制(drawRect):对于UITableView的每个cell,可以通过异步绘制来减少主线程的负担,提高UITableView的性能。可以使用dispatch_async函数将cell的绘制放在后台线程中进行,然后在绘制完成后再将cell显示在UITableView上。

    - (void)drawRect:(CGRect)rect {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            // 在后台线程中执行绘制操作
            UIImage *image = [self drawImage];
            
            dispatch_async(dispatch_get_main_queue(), ^{
                // 在主线程中将绘制结果显示到屏幕上
                self.imageView.image = image;
            });
        });
    }
    
    //复用前取消绘制
    - (void)prepareForReuse {
        [super prepareForReuse];
        // 取消之前的绘制操作
        [self.imageView.layer removeAllAnimations];
        self.imageView.image = nil;
    }
    

    减少层级:在UITableView的cell中,尽量减少视图层级的嵌套,因为每个视图层级都需要消耗一定的性能。可以通过使用UIView的layer属性来减少层级,将需要裁剪的视图放在同一个图层中。

    隐藏不需要显示的cell:可以通过UITableView的visibleCells属性获取当前正在显示的cell,然后将不需要显示的cell隐藏起来,以提高UITableView的性能。

    避免离屏渲染:在UITableView中,尽量避免使用需要离屏渲染的功能,比如圆角、阴影等。可以通过设置UIView的layer属性来避免离屏渲染。另外,可以使用异步绘制的方式来避免离屏渲染。

    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:10.0];
    CAShapeLayer *maskLayer = [CAShapeLayer layer];
    maskLayer.path = path.CGPath;
    imageView.layer.mask = maskLayer;
    

    除了上述方法,还有一些其他的优化方法,比如使用重用机制、使用索引、使用局部刷新等。通过对UITableView的性能进行优化,可以提高应用程序的响应速度和流畅度,提升用户体验。

    下面是一个缓存UITableView cell高度的例子:

    首先,在UITableViewDelegate的heightForRowAtIndexPath方法中计算并缓存cell的高度,然后在cellForRowAtIndexPath方法中从缓存中读取高度:

    @interface MyTableViewController ()
    
    @property (nonatomic, strong) NSMutableDictionary *cellHeightCache;
    
    @end
    
    @implementation MyTableViewController
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 判断缓存中是否已经有该cell的高度
        NSNumber *cachedHeight = self.cellHeightCache[indexPath];
        if (cachedHeight != nil) {
            return [cachedHeight floatValue];
        }
        
        // 计算cell的高度
        CGFloat height = [self calculateHeightForRowAtIndexPath:indexPath];
        
        // 将cell的高度缓存起来
        self.cellHeightCache[indexPath] = @(height);
        
        return height;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell" forIndexPath:indexPath];
        
        // 从缓存中读取cell的高度
        NSNumber *cachedHeight = self.cellHeightCache[indexPath];
        if (cachedHeight != nil) {
            CGRect frame = cell.frame;
            frame.size.height = [cachedHeight floatValue];
            cell.frame = frame;
        }
        
        // 设置cell的内容
        [self configureCell:cell forIndexPath:indexPath];
        
        return cell;
    }
    
    - (CGFloat)calculateHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
        // 计算cell的高度
        // ...
    }
    
    - (void)configureCell:(UITableViewCell *)cell forIndexPath:(NSIndexPath *)indexPath {
        // 设置cell的内容
        // ...
    }
    
    @end
    

    在上面的例子中,我们定义了一个cellHeightCache字典来缓存UITableView cell的高度。在heightForRowAtIndexPath方法中,如果缓存中已经有该cell的高度,就直接从缓存中读取;否则,计算cell的高度并将其缓存起来。在cellForRowAtIndexPath方法中,我们首先从缓存中读取cell的高度,如果缓存中已经有该cell的高度,就直接设置cell的frame,否则,计算cell的高度并设置cell的frame。

    37.SDWebImage的缓存策略?

    sd加载一张图片的时候,会先在内存里面查找是否有这张图片,如果没有会根据图片的md5(url)后的名称去沙盒里面去寻找((先查找内存imageCache,如果内存不存在该图片,再查找硬盘;查找硬盘时,以URL的MD5值作为key).),是否有这张图片,如果没有会开辟线程去下载,下载完毕后加载到imageview上面,并md(url)为名称缓存到沙盒里面。

    38.AFN请求过程梳理

    AFN详解

    39.KVC的使用?实现原理?(KVC拿到key以后,是如何赋值的?知不知道集合操作符,能不能访问私有属性,能不能直接访问_ivar)

    相关文章

      网友评论

          本文标题:2023iOS面试题总结

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