【iOS面试题六】知识要点

55 阅读41分钟

GitHub移动端APP:支持代码浏览、Git事件和AI一键翻译等

 一、Darwin内核

1、什么是iOS的运行时(Runtime)?

(1) iOS运行时是iOS实现消息传递的方案。它是OC语言本身的特性,OC是一种动态语言,不像C/C++这些静态语言,它们的程序在编译和链接的时候,就已经确定了程序的执行流程,在调用一个函数时,是直接执行函数所在的一个内存地址。而OC在调用类方法或实例方法时,则是通过发消息的方式来去由类或对象本身以及一直向上的父类中去找方法的,找到了就以响应消息的方式来执行方法,找不到,就执行运行时指定的一个方法:resolveClassMethod/resolveInstanceMethod

(2) 除此之外,OC类的底层实现是一个C结构体,它包含了类的信息、成员列表、方法列表、协议列表、指向类本身的指针和指向父类的指针等。OC提供了一些运行时方法来依据这个C结构体来执行相关操作,如:

① objc_setAssociatedObject:向系统类添加属性

② class_copyPropertyList:获取属性的名称列表

③ class_addMethod:添加方法

④ method_exchangeImplementations:交换方法实现

2、什么是iOS的运行循环(RunLoop)?

iOS运行循环是iOS实现事件处理的方案。

(1) 其目的在于让App能一直流畅的运行。我们知道一个App就是一个进程,一个进程至少有一个线程,负责UI渲染和事件监听的线程就是主线程。线程就是一个CPU指令序列,执行完后就会自动退出。所有线程都退出后,进程也就随之结束了。为了让App一直运行,就得保证主线程不能退出。因此,苹果设计并实现了运行循环来解决这个问题。在这个方案中,运行循环可以让线程不结束,在有工作的时候进行处理,没有工作的时候只是释放CPU资源而不退出,不退出能保证App一直运行、释放资源能保证App流畅。运行循环和线程是一一对应的,也就是一个线程有运行循环的话,只能有一个。主线程默认开启运行循环,而子线程需要手动开启。

(2) 运行循环是在内核层中实现的。iOS的系统架构从上往下分为应用层、应用框架层、核心框架层和Darwin内核。在内核实现上,RunLoop是一个函数,在应用框架层是通过类对象来封装提供出来。内核的RunLoop函数是在一个端口上通过mach_msg函数来进行休眠的,当收到向这个端口发来的消息,就会唤醒,并执行相应的任务。RunLoop要开启成功,必须加上RunLoop所要监听的源,源主要有三种,Source(源):用于监听事件,Timer(定时器):用于监听时间触发器,Observer(观察者):用于状态的观察。

(3) 举两个例子:

① App启动时会有一个自动释放池,主线程的RunLoop注册两个Observer,一个用于进入RunLoop的时候创建自动释放池,一个用于现场休眠或者退出App的时候自动释放资源。

② 在子线程中使用定时器,如果不主动开启RunLoop、那么定时器是没有效果,线程会走完所有代码就退出了。需要主动开启RunLoop,并添加Timer(定时器)源,才能在子线程中监听到定时器的触发事件。

3、RunLoop有哪五个模式?

默认模式、跟踪运行模式、初始运行模式、系统事件接收模式和通用模式

4、Runloop的运行流程

(1) 通知Observer即将进入Runloop

(2) 通知Observer即将处理Timer

(3) 处理block

(4) 通知Observer即将处理Source0

(5) 处理Source0

(6) 处理block

(7) 是否存在Source1

是:

① 处理Source1

② 如果是Timer唤醒就处理Timer;如果是main_dispatch唤醒就处理main_dispatch;如果都不是就处理Source1

否:

① 休眠等待

② 如果出现Timer、dispatch、Source0、Source1或者手动唤醒等消息,就唤醒

5、Runloop包含哪些信息?

(1) 每个Runloop包含一个Mode(运行循环模式),每个模式又包含Timer/Source/Observer

(2) 切换模式的时候,必须先退出当前模式,然后再重新进入,以保证不同模式下的Timer/Source/Observer互不影响。

(3) 系统默认提供DefaultMode、TrackingMode和CommonMode(前两个的组合模式)等五种模式

(4) 默认模式是Runloop的默认模式,例如进入iOS程序默认不做任何操作就处于这种Mode中,此时滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作。

6、Runloop模式中包含的哪些事件源?

(1) Source/Timer/Observer

(2) Source0和自定义Source属于输入事件源,它们都是手动触发的。Source0负责App内部事件(如UITouch事件),自定义Source由其他线程手动发出。

(3) Source1和Timer属于端口事件源,不同的是所有的Timer都共用一个端口“Mode Timer Port”,而每个Source1都有不同的对应端口。

(4) Source和Timer都包含对应的回调指针,用于触发后执行。Source1除了包含回调指针外包含一个mach port,和Source0需要手动触发不同,Source1可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop。

(5) Observer用于随时通知外部当前RunLoop的运行状态(进入、即将处理Timer、即将处理Source、休眠、唤醒和退出)

7、Runloop是如何实现进入休眠状态的?

是通过iOS系统内核实现的,即Darwin的Mach组件实现的。该组件以消息的方式进行进程和线程之间的通信,消息的发送和接收都是使用mach_msg()函数进行的,当该函数在等待接收消息的时候,会让程序处于休眠状态。

8、Runloop与线程之间的关系

(1) 默认情况下,除了主线程,线程不会创建Runloop。

(2) 一个线程有Runloop的话,最多只能有一个,线程和Runloop是一一对应的,并保存在一个全局字典中。

(3) 如果线程没有开启Runloop,当第一次调用getRunloop时会进行创建。

9、Runloop与自动释放池之间的关系

iOS通过运行循环的机制来创建和释放自动释放池。运行循环会注册两个Observer,分别用于监视运行循环的进入和休眠或退出状态,以便在Observer的回调中进行自动释放池的创建和释放。因为主线程默认开启了运行循环,所以iOS APP就能随时有一个最新自动释放池来使用。

10、子线程默认不会开启Runloop,那出现Autorelease对象如何处理?不手动处理会内存泄漏吗?

不会出现内存泄露。在子线程你创建了Pool的话,产生的Autorelease对象就会交给pool 去管理。如果你没有创建 Pool,但是产生了Autorelease对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个hotpage(当前使用的AutoreleasePoolPage),并调用 page->add(obj)将对象添加到AutoreleasePoolPage的栈中,并由这个hotpage处理后续的对象释放。

11、iOS的编译分哪两步?

(1) Clang编译器前端:语法分析、类型检查,生成中间代码。

(2) LLVM编译器后端:BitCode、链接期优化,生成不同CPU架构对应的机器码

二、核心框架层

1、iOS的事件传递是如何实现的?

(1) 【传递】触摸事件的传递是从父控件传递到子控件,如果父控件不能接收触摸事件,那么子控件就不能接收到触摸事件

(2) 【方法】查找处理触摸事件的视图是通过hitTest和pointInside方法来处理的

(3) 【路径】事件的大体传递过程为:UIApplicaiton->UIWindow->父控件->子控件

(4) 【过程】具体过程为:触摸事件是发生在最顶层的父控件上的,当发生触摸事件的时候,父控件会调用hitTest方法来进行测试,首先判断父控件是否能处理触摸事件,也就是父控件不是隐藏的、userInteractionEnabled等于真;其次再调用pointInside方法来判断触摸的点在该控件上;然后遍历控件的所有子控件、调用convertPoint方法把父控件上触摸点的位置转换为子控件的相应位置;最后递归调用子控件的hitTest方法找到最终响应该次触摸的子控件,如果没有找到就用该父控件进行相应处理。

2、iOS中的事件有哪些?

iOS中的事件主要分为触摸事件、加速计事件和远程控制事件。iOS中只有继承自UIResponder的对象才能响应事件,比如UIApplication、UIViewController和UIView。UIResponder提供三类方法来处理事件:

(1) touchesBegan/Moved/Ended/Cancelled

(2) motionBegan/Ended/Cancelled

(3) remoteControlReceivedWithEvent

3、什么是元类?

(1) 任何OC类在编译的时候都会生成一个描述类定义的对象,这个对象叫类对象,它是单例的,用于在运行的时候生成该类的实例对象的依据。这个类对象的类叫元类,其中的isa成员就是元类对象。

(2) 所以任何继承至NSObject的类的实例对象中都包含了一个指向类对象的指针,它是Class类型的isa结构体,这个结构体中还包含了另外一个isa,这个isa就是元类对象。

(3) 类对象(class object)和元类对象(metaclass object)的定义都是objc_class结构,其不同仅仅是在用途上,比如其中的方法列表在类对象(instance object)中保存的是实例方法(instance method),而在元类对象(metaclass object)中则保存的是类方法(class method)。

(4) 总结:

① 实例对象是类对象的实例,实例对象的isa指向类对象,用于查找实例方法。

② 类对象是元类对象的实例,类对象的isa指向元类对象,用于查找类方法。

③ 元类对象是根元类的实例,元类对象的isa指向根元类对象。

④ 根元类是指向自身的实例,根元类的父类是根类对象。

4、类对象与元类对象的区别

(1) OC在编译时,会给每个类生产唯一一个类对象,用于描述怎么去定义实例对象。

(2) 实例对象中的isa指向类对象,类对象的isa指向元类对象。

(3) 类对象是元类对象的一个实例

(4) 类对象包含实例方法列表、用于实例对象调用实例方法;元类对象包含了类方法列表、用于类对象调用类方法。

(5) 元类对象是根元类的一个实例,根元类是其自身的实例(isa指向其自身)。

(6) 类的super_class指向其父类,而元类的super_class则指向父类的元类

(7) 类对象存的是关于实例对象的信息(实例变量,实例方法等),而元类对象(metaclass object)中存储的是关于类的信息(类的版本,名字,类方法等)

5、什么是消息转发?

(1) 消息转发是指iOS在消息传递时找不到响应消息的对象时触发的机制,用于作为挽救程序crash的最后机会。

(2) 当找不到响应对象时,会调用resolveInstanceMethod或resolveClassMethod方法,我们在该方法中进行消息转发。

(3) 消息转发有3种方案:

① 动态添加:调用class_addMethod来动态添加响应方法,返回[super resolveInstanceMethod:sel]来再触发一次消息传递。

② 快速转发:调用forwardingTargetForSelector来将消息直接转发给另外一个对象来处理。

③ 标准转发:分别调用methodSignatureForSelector和forwardInvocation来进行标准的转发处理。

三、应用框架层

1、NSTimer如何正确销毁?

(1) 因为NSTimer通常采用Target/Action设计模式来添加回调方法。这会造成在ViewController中使用Timer的时候,ViewController和Timer之间会产生循环引用,而造成ViewController的dealloc方法得不到运行,从而造成内存泄漏。

(2) 所以,可以采用两种方法来避免在ViewController中直接使用Timer。

① 自定义一个封装Timer的类,由这个类来管理Timer的生命周期。

② iOS10开始,苹果为Timer提供了Block回调方式的方法,避免使用Target/Action方式。

(3) 除此之外,Timer要生效,需要将其加入到当前线程的运行循环中,所以运行循环也会引用一次Timer,在销毁Timer需调用invalidate方法来解除运行循环对Timer的引用。

2、NSTimer如何正确使用?

(1) NSTimer的定时触发是基于Runloop实现的,所以Timer要生效、需将其加入到当前线程的运行循环中。

(2) NSTimer的scheduledTimerWithTimeInterval方法在创建Timer时会自动加入到运行循环中,而timerWithTimeInterval和initWithFireDate方法仅仅只会创建Timer、需要手动调用addTimer:forMode方法来加入到运行循环中。

(3) addTimer:forMode的mode自动采用默认模式(NSDefaultRunLoopModeMode),如果在UIScrollView中进行滚动时,运行模式自动切换为跟踪运行模式而造成Timer无法正常工作,通过将Timer的模式修改为组合模式(NSRunLoopCommonModes)可以解决这个问题。

(4) 使用Timer很容易造成循环引用问题,可以通过封装Timer中间类或使用Block回调来避免这个问题。

3、介绍一下AsyncDisplayKit

AsyncDisplayKit (ASDK) 是一个由Facebook开发的用于 iOS 应用程序的第三方库,旨在简化构建复杂用户界面的过程,并提高性能。它的主要特点是异步加载和渲染视图,这意味着可以在后台线程上处理视图的布局和绘制,从而避免了主线程阻塞,提高了界面的响应性能。它提供了一套与Cocoa相匹配的类库,比如ASTableNode替代UITableView,相关的属性也同名的。

4、KVO的实现原理

当通过keypath的方式来观察某个对象 的属性 时,KVO 机制动态创建一个该对象当前类的子类,并为这个新的子类重写了被观察属性 keyPath 的 setter 方法(willChangeValueForKey和didChangevlueForKey)。setter 方法随后负责通知观察对象属性的改变状况。且重写观察属性的 setter 方法这种继承方式的注入是在运行时而不是编译时实现的。

5、什么是KVC?

(1) Key-Value-Coding(键值编码),涉及value:forKey、value:forKeyPath、setValue:forKey和setValue:forKeyPath四个方法。

(2) 对象采用这种机制来提供对其属性的间接访问,可以通过字符串来访问一个对象的成员变量或其关联的存取方法(getter or setter)

(3) 根据KVC搜索规则,当没有搜索到对应的 key 或者 keyPath 相关方法或者变量时,会调用对应的异常方法 valueForUndefinedKey: 或 setValue:forUndefinedKey:,这两个方法的默认实现是抛出异常NSUnknownKeyException,并导致程序Crash。

四、应用层(UIKit)

1、什么是Block和闭包?

都是指一个概念(可理解为任务代码块),只是在OC和Swift中采用不同的术语。

OC:叫Block,被捕获的数据叫自动变量。

Swift:叫闭包,被捕获的数据叫捕获列表。

2、OC和Swift分别采用什么解决循环引用?

OC:__weak和__block,后者需要手动将变量置为nil

swift:weak和unowned,后者需要手动将变量置为nil

3、OC和Swift的区别

最重要的区别在于Swift是强类型的静态语言,OC是弱类型的动态语言,前者更多的是面向协议编程、注重值类型,后者是面向对象编程、注重引用类型。另外:

(1) Swift同时支持静态派发和动态派发,OC仅支持动态派发方式。

(2) Swift中不仅类可以实现协议,枚举和结构体也能实现协议。

(3) Swift支持命名空间、泛型、元祖类型和运算符重载,比OC代码更简洁。

4、GCD如何实现珊栏操作?

在执行多组操作的时候,每个操作包含一个或多个任务,比如在操作1完成之后才能执行操作2中的任务。这个时候就需要在操作1与操作2直接添加一个栅栏,在GCD中是通过dispatch_barrier_asyc方法来实现的,该方法后的任务必须等方法前的任务(dispatch_async)完成之后才能执行,所有的任务都必须在同一个队列中。

5、什么是离屏渲染?

在使用圆角、阴影和遮罩等视图功能的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中回执,所以就需要在屏幕外的上下文中渲染,这就是离屏渲染。创建缓冲区和切换上下文最消耗性能,所以会造成卡顿。解决方案有:

(1) 设计圆角图

(2) 利用mask来遮罩出圆角

(3) 绘制圆角

(4) 第三方界面库:AsyncDisplayKit

注:在UIImageView包含png图标时通过clipsToBounds/cornerRadius方法来设置圆角时,iOS9之前会产生离屏渲染,iOS9之后不会。

6、如何进行列表的性能优化?

(1) cell的重用

(2) 缓存cell高度

(3) 避免离屏渲染

(4) 异步展示(AsyncDisplayKit)

7、copy和mutableCopy如何进行拷贝?

copy和mutableCopy遵循一个原则,就是源对象或者新对象的修改不会相互影响。比如NSArray执行mutableCopy,拷贝出来的是一个可变数组,这个可变数组的内容是可以修改的,为了不影响源对象,因此执行的是深拷贝、也就是另外开辟一块内存空间来存放新对象,两个对象的值、也就是指针是不一样的。当NSArray执行copy的时候,拷贝出来的也是NSArray,因为他们都不能被修改,所以执行的浅拷贝,只是是源对象的指针地址拷贝给新对象,两个对象的指针值是相同的。反过来也是一样,比如NSMutableArray执行copy或者mutableCopy,都是深拷贝。另外,数组的内容默认情况下执行的是浅拷贝。

8、block为什么要用copy来修饰?

因为block的执行时间是不确定的,如果block里使用了外界的对象,而这个对象在block执行的时候已经提前被释放了,就会出现野指针类型的crash。使用copy直接复制一份block的代码,里面对外界对象的使用,会让对象的引用计数+1,从而不会出现野指针问题。即,使用copy会把Block拷贝到堆上去,捕获的变量也会,从而不会出现野指针问题。

9、什么是野指针?

一个对象在使用的时候已经被提前释放了,但是没有将其置为nil(空指针),其指向的内存地址是不确定的,对其执行操作的后果是不可预计的,所以叫野指针。因为iOS里的空指针操作是安全的,不会出现crash,所以在释放对象的时候,应该将其置为nil。

10、通常如何做屏幕适配?

通常采用390作为设计尺寸,以宽度为标准来比缩放尺寸。

11、为什么IBOutlet修饰的UIView也适用weak关键字?

IBOutlet修饰的UIView表明该View位于nib或故事板,而nib或故事板实际上是一个控制器的实例,也就是控制器已经引用了视图,不再需要再引用一次视图上的子视图,所以采用weak弱引用。

12、nonatomic和atomic的区别?

(1) nonatomic:生成的getter/setter是不支持原子操作的,所以不安全,但是效率高。

(2) atomic:生成的getter/setter是支持原子操作的,所以安全,但效率不高。

注:atomic不是绝对的线程安全。因为原子操作只支持到getter和setter,多个线程操作setter是安全的,因为这个方法是支持原子操作,但是如果有另外一个线程执行getter的时候,获取的数据仍然是不确定的,因为getter和setter整体不是原子的。所以应该通过加一个线程锁来实现,进入其中一个方法时关闭锁、离开后再打开。

13、线程通信如何实现?

使用dispatch_async或者performSelector:onThread:withObject

14、如何在指定的次数时,及时释放自动释放池中的临时变量?

NSAutoreleasePool类有相应的初始化方法,用于创建在指定次数时调用其drain方法进行倾倒池中现存的临时变量的自动释放池。

15、如何查看变量的变化?

控制台的lldb调试命令: watchpoint set v XXX

16、为什么有了ARC还要自动释放池?

(1) ARC用于自己生成并持有的对象,即alloc/new/copy/mutalbeCopy方法创建的对象;自动释放池用于非自己生成并持有的对象,即非alloc/new/copy/mutalbeCopy方法创建的对象、如NSArray的array方法创建的对象。

(2) ARC由编译器实现自动内存管理,开发者无法干预;自动释放池可以由开发者使用@autoreleasepool来指定一个作用域、用于在作用域结束时及时清理由ARC标记为autorelease的对象,这可应用于小段代码占用大量内存、却得不到及时清理的场景。

(3) 大多数自动内存回收的实现机制是采用定时集中式回收,这使得内存的回收有时达不到及时性,从而使得APP出现卡顿、不流畅的情况。iOS提供的自动释放池依据运行循环来管理其生命周期,使得自动释放的对象能够非常快的及时清理,从而让APP非常流畅。

17、如何IOS工程中混合使用ARC与MRC?

为源文件添加编译选项:-fobjc-arc(ARC模式)或-fno-objc-arc(MRC模式)来实现

18、描述一下autoreleasepool

自动释放池是NSAutoreleasePool的实例,其中包含了收到autorelease消息的对象(也就是管理能够发送autorelease消息的对象)。当一个自动释放池自身被销毁(dealloc)时,它会给池中每一个对象发送一个release消息。

19、autoreleasepool如何创建和销毁?

App启动后,系统在主线程runLoop里注册两个Observser,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 优先级最低,保证其释放池子发生在其他所有回调之后。在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被runLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool。可见开发过程中我们没有创建autoreleasepool,系统也会帮我们创建。这就解释了,为什么开发中没有创建autoreleasepool也没有内存泄露的原因了。

20、autoreleasepool的底层结构是什么?

自动释放池的底层结构是一个双向链表,它由一系列的AutoreleasePoolPage组成。自动释放池的创建和释放,分别由底层的AutoreleasePoolPage调用push和pop方法来实现。

21、__strong的使用场景

Block在捕获外部变量的时候,通过__weak来修饰可以避免出现循环引用问题。但是在Block的执行过程中,弱引用的外部变量可能已经被释放了,为了在Block中能有效地使用它,我们可以在Block中__strong一次这个弱引用。这既能避免循环引用、又能保证block中持有外部变量。

22、__weak和__block的区别

它们都可以用于解决循环引用,区别在于:

(1) 【引用计数】__weak 本身是可以避免循环引用的问题的,但是其会导致外部对象释放了之后,block 内部也访问不到这个对象的问题,我们可以通过在 block 内部声明一个 __strong 的变量来指向 weakObj,使外部对象既能在 block 内部保持住,又能避免循环引用的问题。

(2) 【内存区域】__block 其实提升了变量的作用域,__block 本身无法避免循环引用的问题,但是我们可以通过在 block 内部手动把 blockObj 赋值为nil的方式来避免循环引用的问题。另外一点就是 __block 修饰的变量在 block 内外都是唯一的,要注意这个特性可能带来的隐患。

但是__block有一点:这只是限制在ARC环境下。在非arc下,__block是可以避免引用循环的

23、__block的作用是什么?

Block所捕获的自动变量的值是瞬间值,它默认是只读的。如果需要变为可写,可以使用__block来进行修饰。被__block修饰的外部变量会从栈拷贝到堆中去,从而使得在Block内部也可以修改外部变量的值。

注:栈的内存地址以0x7开头,堆的内存地址以0x1开头。

24、SDWebImage的实现原理

内存->硬盘->下载->保存->返回

25、tableView会造成卡顿的原因大致有哪些?

(1) 【重用】最常用的就是cell的重用, 注册重用标识符

(2) 【隐藏】cell一次布局,不要动态布局,对于风格不一样的cell,通过hide来隐藏或显示对于的view。

(3) 【透明】不要使用透明颜色和透明度为0的视图(即,clearColor或alpha等于0),它们会消耗较长渲染时间。

(4)【高度】缓存cell高度,以便更快显示cell。tableview代理是先定cell高度,在创建cell。固定高度可以使用estimatedHeightForRow方法代替heightForRow。

(5) 【渲染】避免离屏渲染

26、NSDictionary的实现原理

哈希表/哈希函数/负载因子/自动扩容等概念

27、block和函数指针的区别

(1) 相同:回调,变量,函数参数

(2) 不同:函数指针是预定义的代码块,在编译时已经确定好的;block是OC对象,是在运行时在栈/堆/全局数据区上动态创建的。前者只能访问全局变量;后者拥有自己的栈内存,并且可以通过访问堆变量,以及访问__block修饰的局部变量。

28、iOS中的线程锁有哪些?

(1) pthread_mutex(互斥锁,C语言)

(2) NSLock(对象锁)/NSRecursiveLock(递归锁)/NSConditionLock(条件锁)

(3) dispatch_semaphore(信号量,GCD)

(4) @synchronized关键字

29、线程锁有哪些实现方式?

(1) 互斥锁(【持有】具有持有者的概念,互斥量拥有队列、用于存放等待线程)

(2) 信号量(【条件】简单灵活,可能会出现优先级反转的问题)

(3) 自旋锁(适用于短时任务,可能会出现忙等待问题)

30、iOS数组的容错处理

(1) 数组内容,如果其中某个对象未nil,会失败。用RACTuple替代。

(2) 数组越界,采用安全方法,如果index越界直接返回nil

(3) 类型判断,以防后台返回的数据结构与约定的不一致。

31、iOS如何实现设备唯一标识?

(1) UDID:iOS5中被废弃,现在应用试图获取UDID已被禁止且不允许上架。

(2) IDFA:广告ID,用户可以在“设置|隐私|广告追踪”里重置此id的值

(3) IDFV:开发商ID,如果用户将属于此Vender(应用开发商)的所有App卸载,则idfv的值会被重置,即再重装此Vender的App,idfv的值和之前不同。

(4) MAC地址:被苹果禁止

(5) UUID:每次程序运行都会不同

总结:无法获取到,可通过KeyChain来保存/查询来达到目标。

32、iOS APP间如何传递数据?

(1) 剪切板

(2) 钥匙链

(3) iCloud

(4) Action Extension

(5) App Groups

33、NIB与Storyboard的根本区别

当你加载一个XIB的时候所有内容都被放在了内存里,包括任何图片。如果有一个不会即刻用到的view,你这就是在浪费宝贵的内存资源了。Storyboards就是另一码事儿了,storyboard仅在需要时实例化一个view controller。

34、iOS中常用的设计模式有哪些?

(1) 代理模式(UITableView)

(2) 观察者模式(KVO)

(3) MVC/MVVM

(4) 单例模式(UIApplication)

(5) 策略模式(sortedArrayUsingSelector)

(6) 工厂模式

(7) Target/Action

35、推送通知的过程

(1) 注册:为应用程序申请消息推送服务。此时你的设备会向APNs服务器发送注册请求。

(2) APNs服务器接受请求,并将deviceToken返给你设备上的应用程序

(3) 客户端应用程序将deviceToken发送给后台服务器程序,后台接收并储存。

(4) 后台服务器向APNs服务器发送推送消息

(5) APNs服务器将消息发给deviceToken对应设备上的应用程序

36、讲一下iOS的多线程

iOS有3种多线程的实现方案:pthread、NSThread和GCD。其中,pthread是C语言级别的操作系统实现;NSThread是Cocoa Fundation框架提供的;GCD,一种以极简写法的多线程实现。另外,NSObject还提供了2个分别在主线程和后台线程执行任务的类方法(performSelectOnMainThread/BackgroundThread)。非常简单的多线程任务可以借助NSObject提供的这两个方法来完成;其他大多数时候,都采用GCD来实现。GCD不仅在语法的书写上很简单,而且还是一套多线程管理的解决方案。它采用调度队列(Dispatch Queue)的方式来管理任务,以Block的方式来封装任务。GCD提供了两类调度任务:串行的和并行的。串行的任务,依次执行;并行的任务,同时执行。除此之外,系统默认提供了5个调度队列:主线程调度队列、高优先级调度队列、默认调度队列、低优先级调度队列和后台调度队列,除了主线程调度队列是串行队列外,其他都是并行调度队列。另外,GCD还有些高级用法,调度组和调度源。调度组,调度组用于监控一个调度队列里所有任务的执行状态,比如多个串行调度队列或者一个并行调度队列,里面的所有任务都执行完的时间是不确定的,所以就可以通过将这些队列加入到组,调用组的notify方法就可以在所有任务完成后得到通知,还有wait方法可以等待多少秒看里面的任务是否都完成。而调度源,是对底层多线程事件的直接暴露,像GCD提供的用于管理线程的调度队列在执行的过程中是不能取消的,只能暂停和继续执行,而调度源可以实现队列的取消。另外,还有些常用的API:dispatch_after、dispatch_once、dispatch_apply和dispatch_semaphore等。

37、讲一下iOS的核心动画

iOS的核心动画就是Core Animation里提供的动画效果,它是基于CALayer来实现。因为CALayer的大部分属性都是支持隐式动画的,当你给这些属性值添加变化的时间和变化量,它就会以动画的方法进行变化。比如hidden属性是不支持的。而CALayer提供了多种子类来实现不同的动画效果,比如CAShapeLayer,CAGrandientLayer和CATransformLayer等等。核心动画的缺陷是代码量过大和不容易理解,比如要实现链式动画的时候,你需要completion里再执行另外一个动画,完了之后再在complition里执行另外一个动画。Github上有热门的链式动画库,它是基于Core Animation来实现,并且还封装了一些UIBerzierPath和自动布局的属性动画。除此之外,CALayer的CASpringLayer实现的精灵动画(弹跳效果)是iOS9才推出的,而iOS9之前的版本不支持,可以通过facebook的pop库来实现。

38、UIView与CALayer的区别

(1) UIView支持触摸事件,而CALayer不支持。

(2) layer的frame是有anchorPoint, position, bounds和transform共同决定的。

(3) UIView负责显示内容的管理,CALayer负责显示内容的绘制。

(4) 每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个anchorPoint

(5) 在View显示的时候,UIView 作为 Layer 的 CALayerDelegate,View的显示内容由内部的CALayer display

(6) CALayer是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)

(7) layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer

(8) 两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以

39、核心动画有哪些?

核心动画是由速度、时间和时间线确定的。有如下一些分类:

(1) CABasicAnimation:起点和终点

(2) CAPropertyAnimation:属性动画

(3) CAKeyFrameAnimation:帧动画,一组关键帧的值组成动画

(4) CATransition:过渡动画,Fade/MoveIn/Push/Reveal等

(5) CASpringAnimation:精灵动画,实现弹跳效果。

(6) CATransaction:动画事务管理,用于管理一组layer属性同时进行动画。

40、UIView包含了哪两部分?

CALayer和Response两部分,并且CALayer只是暴露一部分给UIView。

41、CALayer有哪些功能没有暴露给UIView?

(1) 边框、圆角和阴影

(2) 3D变换

(3) 非矩形范围

(4) 多级非线性动画

42、如何适配灵动岛?

(1) iOS16的iPhone 14系列才有灵动岛

(2) 灵动岛的UI有紧凑、展开和最小化三种

(3) 灵动岛的背景色无法修改,只能更改字体色和素材色等

(4) 灵动岛的适配框架ActivityKit(采用WidgetKit和SwiftUI进行UI开发)

(5) 灵动岛的实时信息要有明确的开始和结束时间点

(6) 当一个实时信息持续超过8小时,系统会从灵动岛移除这个App的信息

(7) 当一个实时活动结束时,灵动岛上的展示信息也会立即被系统移除

(8) 避免在灵动岛显示广告,毕竟引起用户反感可以被直接关闭

(9) App要能够响应灵动岛的点击信息,跳转到App中正确的子页面,而不是停留在App的首页

43、什么是XCTest?

(1) 功能开发的驱动方式:行为驱动(根据用户需求)和测试驱动(根据测试目标)

(2) XCTest是Xcode自带的单元测试框架,我们可以使用该框架做功能性代码的白盒单元测试,以自测并增强代码健壮性。

(3) 创建项目的时候可以选择支持单元测试,也可以手动新增一个单元测试的target

(4) 测试失败红色正方形+一个叉,测试通过绿色正方形+一个勾

44、从数据的角度,如何避免列表的卡顿?

列表的cell是否需要更新,是根据其关联的数据是否有变化来决定的,所以在比较新model和旧model时,提高比较的效率(比如只比较id、或者只比较updateDate),可以提高列表刷新的流畅度。

45、什么是多态?

比如,定义一个父类的引用,当这个引用引用的是父类,调用方法A的时候,执行的就是父类的A;当这个引用引用的是子类,调用的方法A就是子类的A。从方法的角度看,类的多态性是指方法的重写。

46、Swift的类,如何实现多继承?

Swift不支持类的多继承,可以通过组合或者协议的方式来变通实现。组合是指通过包含所需功能的其他类的实例作为类的属性;使用协议来定义一组方法和属性,让类来遵循实现。

47、选择结构体还是类作为Model的策略是什么?

(1) 语义

类:类是引用类型,它们通过引用来传递和共享数据。当你需要在不同部分之间共享数据或者需要传递数据的引用时,通常使用类。

结构体:结构体是值类型,它们在被传递时会被复制。如果你想要简单的值语义,避免共享和变异的复杂性,通常使用结构体。

(2) 性能

类:由于类是引用类型,当你需要频繁地传递大型数据结构时,由于要复制引用而不是数据本身,所以只会产生较小的性能开销。

结构体:结构体是值类型,它们在传递时会被复制,这可能会导致性能问题,尤其是对于大型数据结构。

(3) 可变性

类:类的实例可以是可变的(var),这意味着你可以在运行时更改它们的属性。如果你的模型需要频繁地修改,使用类可能更合适。

结构体:结构体的实例默认是不可变的(let),但你可以显式地将它们声明为可变的(var)。如果你的模型是不可变的或者需要避免意外的状态变化,结构体可能更适合。

(4) 继承

类:类支持继承,你可以创建一个类的子类,并通过子类来扩展和重写父类的行为。

结构体:结构体不支持继承,它们的行为是通过协议来实现的。

(5) 内存管理

类:类的实例是通过引用计数来管理内存的。当没有引用指向一个类的实例时,它会被自动释放。

结构体:结构体的实例是在栈上分配的,它们的生命周期是由作用域管理的。

总结:采用结构体会占用较多内存、但由于结构体的实例是在栈上分配的,其生命周期由作用域管理、从而再不在使用时能得到立即回收,使用效率会更高。

五、应用层(SwiftUI)

1、什么是actor?

actor修饰的类型确保其在并发环境中数据一致性和线程安全。它通过串行化对其内部状态的访问,来避免竞争条件和数据竞争。涉及到3个关键点:串行访问、数据隔离和并发安全。

2、SwiftUI的布局三大步

(1) 父视图会提供一个建议的大小

(2) 视图根据自身的特点再结合它的子视图计算出一个大小

(3) 使用该大小在父视图中布局

3、什么是@Observable?

(1) 用于简化状态管理和UI更新,在SwiftUI视图中自动跟踪和响应模型状态的变化。

(2) 在早期的SwiftUI中,通常使用@Published和@ObservedObject来实现类似的功能。@Observable是对这些概念的进一步简化,它允许你更直观地定义可观察的对象,而不需要在每个属性上单独使用@Published。

4、什么是@resultBuilder?

(1) 用于构建视图的层次结构(DSL)和处理复杂的条件逻辑(if或for等),从而帮助开发者以更加优雅的方式构建用户界面。

(2) 它通过定义一系列静态方法来组合输入并生成结果

① buildBlock:处理多个子视图或元素的组合

② buildOptional:处理可选值的组合,通常用于if语句

③ buildEither:处理选择性的分支,通常用于if-else或switch语句

5、什么是@ViewBuilder?

(1) @ViewBuilder是Swift的@resultBuilder的一个特定实现,专门用于构建SwiftUI视图,使得声明性UI编程变得直观和灵活。

(2) 通过@ViewBuilder,你可以轻松地组合、条件渲染和循环创建视图,简化了复杂UI的构建过程。它在处理视图层次结构和条件控制方面提供了极大的便利,使得SwiftUI代码更加简洁和易于维护。

6、什么是@MainActor?

(1) 用于在Swift并发编程中确保代码在主线程上执行。对于构建用户界面的开发者来说,@MainActor是非常重要的,因为所有的UI更新都必须在主线程上进行。

(2) 使用它标记某个类、结构体、函数或属性,使其在主线程上执行。

(3) Swift的并发编程(如async/await和Task)与@MainActor可以很好地配合使用。(如MainActor.run方法)

7、什么是@Binding?

(1) 一种双向绑定,它允许子视图读取和更新父视图中的数据。

(2) 可以通过在属性前加上$符号,将@State、@ObservedObject、@EnvironmentObject等属性包装成Binding传递给子视图。

(3) 创建自定义的Binding可以处理更复杂的数据绑定场景。

8、什么是@dynamicMemberLookup?

它可以让你通过点语法访问某些属性,而不需要在类型中显式定义这些属性,它广泛用于Binding类型

9、什么是@StateObject?

@StateObject是SwiftUI中引入的属性包装器,用于在视图中管理和持有一个自定义的ObservableObject实例。它与@ObservedObject类似,但有一个关键区别:@StateObject负责初始化和持有该对象,并确保在视图生命周期中保持该对象的持久性,而@ObservedObject不负责对象的初始化,它仅用于监听外部传递的对象。

10、什么是@ObservedObject?

@ObservedObject是SwiftUI中用于在视图中监听和响应外部对象状态变化的属性包装器。与@StateObject不同,@ObservedObject不负责创建和持有对象,而是用于观察由外部传递的ObservableObject对象。它通常用于在子视图中接收并监听父视图传递的可观察对象。

11、什么是AnyView?

AnyView是SwiftUI提供的用于类型擦除的视图类型包装器。便于将不同的视图类型包裹为一种统一的视图类型,缺点是会带来性能开销。可以通过opacity/hidden视图修饰符或switch条件控制方式来替代,从而提高性能。

12、什么是@EnvironmentObject?

@EnvironmentObject(属性包装器)用于在视图层级之间共享数据,而无需进行逐层传递。

13、如何配置List为普通样式?

List { 
    HStack {
    } 
    .listRowInsets(.zero) 
    .listRowSeparator(.hidden, edges: .top) 
} 
.environment(.defaultMinListRowHeight, 0) 
.listStyle(.plain) 
.background(Color.surface)

14、常用的三种异步编程方案是哪些?它们之间如何转换?

传统回调方式、async/await语言级方式和Combine函数响应式三种

(1) 传统回调方式 -> async/await语言级方式

采用withCheckedContinuation/withCheckedThrowingContinuation方法来转换

func fetchDataAsync() async throws -> Int { 
    try await withCheckedThrowingContinuation { continuation in 
        fetchDataBlock { number, error in 
            if let error = error { 
                continuation.resume(throwing: error) 
            } else { 
                continuation.resume(returning: number) 
            } 
        } 
    } 
}

(2) async/await语言级方式 -> Combine函数响应式

采用Future/Task代码块来转换

func fetchDataPublisher() -> AnyPublisher<Int, Error> { 
    Future { promise in 
        Task { 
            do { 
                let result = try await fetchDataAsync() 
                promise(.success(result)) 
            } catch { 
                promise(.failure(error)) 
            } 
        } 
    } .eraseToAnyPublisher() 
}

(3) Combine函数响应式 -> async/await语言级方式

① 采用withCheckedThrowingContinuation方式

func fetchDataAsync() async throws -> Int {
    try await withCheckedThrowingContinuation { continuation in
        var finishedWithoutValue = true
        var cancellable: AnyCancellable?
        cancellable = fetchDataPublisher
            .sink { completion in
                cancellable?.cancel()
                switch completion {
                case .finished:
                    if finishedWithoutValue {
                        continuation.resume(throwing: HiError.unknown)
                    }
                case let .failure(error):
                    continuation.resume(throwing: error)
                }
            } receiveValue: { value in
                cancellable?.cancel()
                finishedWithoutValue = false
                continuation.resume(returning: value)
            }
        cancellable?.store(in: &disposeBag)
    }
}

② 采用values方法来转换

func fetchDataAsync() async throws -> String {
    do {
        for try await value in fetchDataPublisher.values {
            return value
        }
    } catch {
        throw error
    }
}

15、常用的两种返回错误方案是哪个?它们之间如何转换?

抛出函数和返回Result两种

(1) 抛出函数 -> 返回Result

func fetchDataResult() async -> Result<String, Error> {
    do {
        let data = try await fetchDataAsync()
        return .success(data)
    } catch {
        return .failure(error)
    }
}

(2) 返回Result -> 抛出函数

func fetchDataAsync() async throws -> String {
    let result = fetchDataResult()
    switch result {
    case .success(let value):
        return value
    case .failure(let error):
        throw error
    }
}

16、Result类型的作用?

用于将可抛出错误的函数在可能抛出错误时,将其错误存储在Result中,从而可以将可抛出错误的函数再次封装而编程普通函数。

17、什么是Combine?

Combine是iOS13推出的一个处理异步事件流和响应式编程的框架,它采用声明式的编程来处理异步操作、使代码更加简洁、易读和便于维护。

18、Combine的三大核心是什么?

(1) Publisher(发布者):发布者是数据流的源头,它定义了如何产生和传递数据或事件(完成或失败事件)。

(2) Subscriber(订阅者):订阅者是接收和处理发布者发出的数据或事件的对象。当订阅者订阅发布者时,会建立一个数据流的订阅关系。Combine提供了两个内置的订阅者,分别是Sink Subscriber(接收式订阅者)和Assign Subscriber(分派式订阅者)。

(3) Operator(操作符):操作符是用于转换、处理和组合发布者数据流的方法。常见的操作符包括map、filter、combineLatest等。

六、扩展

1、SwiftUI和UIKit的区别

(1) SwiftUI是声明式的、UIKit是指令式的

(2) SwiftUI的View是结构体、UIKit的View是类

(3) SwiftUI的运行速度高于UIKit

(4) SwiftUI的开发难度低于UIKit

2、什么是MVVM?

(1) view引用viewModel,viewModel引用model

(2) view和viewController视为一个组件

3、什么是敏捷开发?

敏捷开发是一种以人为核心、迭代、循序渐进的开发方法。在敏捷开发中,软件项目的构建被切分成多个子项目,各个子项目的成果都经过测试,具备集成和可运行的特征。

4、什么是散列表?

散列表是一种比较简单的数据结构,它通过将关键字Key映射到数组中的某个位置来实现快速的数据查找和存储。实现这种映射的函数被称为哈希函数。

5、什么是强连通分量?

在有向图中,一组两两可达的顶点集合称为该有向图的强连通分量。所谓可达是指两个顶点之间至少存在一条路径。

6、TCP和UDP的区别

(1) TCP面向连接,提供字节流,服务可靠;UDP无连接,提供报文(不可分割、最小单位),不保证交付成功。

(2) TCP是一对一的;UDP是一对一、一对多、多对一或多对多的。

(3) TCP头占20字节;UDP头占8字节,开销小一些。

(4) UDP适合传输小数据的网络应用,如DNS。

7、七层网络模型

应用层(HTTP/FTP)、表示层(SSL/TLS)、会话层(RPC)、传输层(TCP/UDP)、网络层(IP/ICMP)、数据链路层和物理层