iOS面试题

Monday, February 25, 2019

Swift基础

类和结构体有什么区别

在 Swift 中,类是引用类型,结构体是值类型。值类型在传递和赋值时将进行复制,而引用类型则只会使用引用对象的一个"指向”。所以他们两者之间的区别就是两个类型的区别。

加分:

class有几个功能是struct没有的
  • class可以继承
  • 一个类可以被多次引用
  • 类型转换可以在runtime的时候检查和解释一个实例的类型
  • 可以用deinit来释放资源
struct的优势
  • 结构较小,适用于复制操作,相比一个class的实例被多次引用更加安全
  • 无须担心内存memory leak 或者多线程冲突

Swift是面向对象还是函数式的编程语言

Swift 既是面向对象的,又是函数式的编程语言。

说 Swift 是面向对象的语言,是因为 Swift 支持类的封装、继承、和多态,从这点上来看与 Java 这类纯面向对象的语言几乎毫无差别。

说 Swift 是函数式编程语言,是因为 Swift 支持 map, reduce, filter, flatmap 这类去除中间状态、数学函数式的方法,更加强调运算结果而不是中间过程。

在Swift中,什么是可选型(optional)

在 Swift 中,可选型是为了表达当一个变量值为空的情况。当一个值为空时,它就是 nil。Swift 中无论是引用类型或是值类型的变量,都可以是可选型变量。

比较以下关键词:Open, Public, Internal, File-private, Private

Swift 有五个级别的访问控制权限,从高到底依次为比如 Open, Public, Internal, File-private, Private。

  • Open 具备最高的访问权限。其修饰的类和方法可以在任意 Module 中被访问和重写;
  • Public 的权限仅次于 Open。与 Open 唯一的区别在于它修饰的对象可以在任意 Module 中被访问,但不能重写。
  • Internal 是默认的权限。它表示只能在当前定义的 Module 中访问和重写,它可以被一个 Module 中的多个文件访问,但不可以被其他的 Module 中被访问。
  • 其被修饰的对象只能在当前文件中被使用。例如它可以被一个文件中的不同 class,extension,struct 共同使用。
  • Private 是最低的访问权限。它的对象只能在定义的作用域内及其对应的扩展内使用。离开了这个对象,即使是同一个文件中的对象,也无法访问。

比较以下关键词:strong, weak, unowned

Swift 的内存管理机制与 Objective-C一样为 ARC。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。

  • strong 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。
  • weak 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不会崩溃。
  • unowned 与弱引用本质上一样。唯一不同的是,对象在释放后,依然有一个无效的引用指向对象,它不是 Optional 也不指向 nil。如果继续访问该对象,程序就会崩溃。

Objective-C基础

什么是ARC

ARC 是 Objective-C 的内存管理机制。简单地来说,就是代码中自动加入了 retain/release,原先需要手动添加的用来处理内存管理的引用计数的代码可以自动地由编译器完成了。

ARC 的使用是为了解决对象 retain 和 release 匹配的问题。以前手动管理造成内存泄漏或者重复释放的问题将不复存在。

什么情况下会出现循环引用

循环引用是指 2 个或以上对象互相强引用,导致所有对象无法释放的现象。这是内存泄漏的一种情况

请说明并比较以下关键词:atomic, nonatomic

  • atomic 修饰的对象会保证 setter 和 getter 的完整性,任何线程对其访问都可以得到一个完整的初始化后的对象。因为要保证操作完成,所以速度慢。它比 nonatomic 安全,但也并不是绝对的线程安全,例如多个线程同时调用 set 和 get 就会导致获得的对象值不一样。绝对的线程安全就要用 @synchronized。
  • nonatomic 修饰的对象不保证 setter 和 getter 的完整性,所以多个线程对它进行访问,它可能会返回未初始化的对象。正因为如此,它比 atomic 快,但也是线程不安全的。

runloop和线程有什么关系

runloop 是每一个线程一直运行的一个对象,它主要用来负责响应需要处理的各种事件和消息。每一个线程都有且仅有一个 runloop 与其对应,没有线程,就没有 runloop。

其中所有线程中,只有主线程的 runloop 是默认启动的,main 函数会设置一个 NSRunLoop 对象。其他线程,runloop 默认是没有启动的,我们可以通过 [NSRunLoop currentRunLoop] 来获得。

请说明并比较以下关键词:__weak,__block

__weak 与 weak 基本相同。前者用于修饰变量(variable),后者用于修饰属性(property)。__weak 主要用于防止 block 中的循环引用。 __block 也用于修饰变量。它是引用修饰,所以其修饰的值是动态变化的,即可以被重新赋值的。__block用于修饰某些 block 内部将要修改的外部变量。

Swift && Objective-C

在 Swift 和 Objective-C 的混编项目中,如何在 Swift 文件中调用 Objective-C 文件中已经定义的方法?如何在 Objective-C 文件中调用 Swift 文件中定义的方法?

  • 在 Swift 中,若要使用 Objective-C 代码,可以在 ProjectName-Bridging-Header.h 里添加 Objective-C 的头文件名称,这样在 Swift 文件中即可调用相应的 Objective-C 代码。一般情况 Xcode 会在 Swift 项目中第一次创建 Objective-C 文件时自动创建 ProjectName-Bridging-Header.h 文件。
  • Objective-C 中若要调用 Swift 代码,可以导入 Swift 生成的头函数 ProjectName-Swift.h 来实现。

Xcode

如何用 Xcode 检测代码中的循环引用?

有两种方法可以检测。

其一是使用 Xcode 中的 Memory Debug Graph。点击下图所示的调试工具栏中的按钮,Xcode 会自动检测内存相关的 memory runtime issue。点击相关问题处 Xcode 就会给出详细的循环引用示意图。

另一种解决方法是用 Instruments 里面的 Leak 选项——这是一个专门检测内存泄漏的工具。进入页面后发现 Leak Checks 中出现内存泄漏时,我们可以将导航栏切换到 call tree 模式下,强烈建议在 Display Settings 中勾选 Separate by Thread 和 Hide System Libraries 两个选项,这样可以隐藏掉系统和应用本身的调用路径,帮助我们更方便的找出 retain cycle 位置。

该怎样解决 EXC_BAD_ACCESS?

EXC_BAD_ACCESS 主要原因是访问了某些已经释放的对象,或者访问了它们已经释放的成员变量或方法。解决方法主要有以下几种:

  • 设置全局断点快速定位 bug 所在,这种方法效果一般;
  • 重写 object 的 respondsToSelector 方法,这种方法效果一般且要在每个 class 上进行定点排查,不推荐;
  • 使用 Zombie 和 Address Sanitizer,可以在绝大多数情况下定位问题代码

系统框架

Auto Layout 和 Frame 在 UI 布局和渲染上有什么区别?

  • Auto Layout 是针对多尺寸屏幕的设计。其本质是通过线性不等式对 UI 控件的相对位置进行设定,从而适配多种 iPhone/iPad 屏幕的尺寸。
  • Frame 是基于 xy 坐标轴系统的布局机制。它从数学上限定了 UI 控件的具体位置,是 iOS 开发中最底层、最基本的界面布局机制。
  • Auto Layout 的性能比 Frame 差很多。Auto Layout 的布局过程首先求解线性不等式,然后再转化为 Frame 去进行布局。其中求解的计算量非常大,通常 Auto Layout 的性能损耗是 Frame 布局的 10 倍左右。

UIView 和 CALayer 有什么区别?

  • UIView 和 CALayer 都是 UI 操作的对象。两者都是 NSObject 的子类,发生在 UIView 上的操作本质上也发生在对应的 CALayer 上。
  • UIView 是 CALayer 用于交互的抽象。UIView 是 UIResponder 的子类( UIResponder 是 NSObject 的子类),提供了很多 CALayer 所没有的交互上的接口,主要负责处理用户触发的种种操作。
  • CALayer 在图像和动画渲染上性能更好。这是因为 UIView 有冗余的交互接口,而且相比 CALayer 还有层级之分。CALayer 在无需处理交互时进行渲染可以节省大量时间。

iOS 中实现动画的方式有几种

  • UIView Animation 可以实现基于 UIView 的简单动画。它是 CALayer Animation 的封装,主要可以实现移动、旋转、缩放、变色等基本操作。
  • CALayer Animation 是更在底层 CALayer 上的动画接口。除了 UIView Animation 可以实现的效果。它可以修改更多的属性以实现各种复杂的动画效果。
  • UIViewPropertyAnimator 是 iOS 10 引进的处理交互式动画的接口。它也是基于 UIView 实现,可以实现所有的 UIView Animation 效果。它最大的优点在于 timing function 以及与手势配合的交互式动画设置相比 CALayer Animation 十分简便,可以说是为交互而生的动画接口。

请说明并比较以下关键词:contentView,contentInset,contentSize,contentOffset。

  • UIScrollView 上显示内容的区域被称为 contentView。一般情况下我们对 UIScrollView 的操作,例如 addSubview 这样的操作都是在 contentView 上进行。
  • contentInset 是指 contentView 与 UIScrollView 的边界。与网页开发的 padding 类似,分别指 contentView 的四条边到 UIScrollView 的对应边的距离,分别为 top,bottom,left,right。
  • contentSize 是指 contentView 的大小。它一般超过屏幕大小,是整个 UIScrollView 实际内容的大小。比如一张图片有四个屏幕之大,我们在缩放的时候只能看到其 1/4 的内容,那么它的 contentSize 就是四个屏幕合起来的尺寸大小。
  • contentOffset 是当前 contentView 浏览位置左上角点的坐标。它是相对于整个 UIScrollView 左上角为左边原点而言。默认为 CGPointZero。

一个列表视图滑动很慢,该怎样优化?

列表视图滑动很慢,肯定是 UI 或是数据上出了问题,它们可能是:

  • 列表渲染时间较长。可能原因是某些 UI 控件比较复杂,或者图层过多。
  • 界面渲染延后。可能原因是大量的操作或耗时的计算阻塞主线程。
  • 数据源问题。可能原因是网络请求太慢,不能及时得到相应数据;也有可能是需要更新的数据太多,主线程一时处理不过来。

然后我们针对三个问题,分别去进行优化。

  • 第一个问题。首先检查 UITableViewCell 是否进行了复用。对于复杂视图的创建,可以采用惰性加载来推迟创建时间。尽量减少视图层级也是很好的优化方法。Facebook 推出的 ComponentKit 就是很好的解决方案。
  • 第二个问题。可以用 GCD 多线程操作将复杂的计算放到后端线程,并进行缓存。例如布局计算或是非 UI 对象的创建和调整就可以如此操作。Linkedin 推出的 LayoutKit 就是很好的例子。
  • 第三个问题。建议将网络端数据缓存并存储在手机端,将取得部分数据根据优先级进行顺序渲染,还可以优化服务器端的实现来优化网络请求。
  • 另外对于界面渲染和优化其实 Facebook 和 Pinterest 维护的 ASDK 是目前为止功能最全、效果最好、使用最广的第三方解决方案。

请说明并比较以下类:URLSessionTask,URLSessionDataTask,URLSessionUploadTask,URLSessionDownloadTask

  • URLSessionTask 是个抽象类。通过实现它可以实例化任意网络传输任务,诸如请求、上传、下载任务。它的暂停(cancel)、继续(resume)、终止(suspend)方法有默认实现
  • URLSessionDataTask 负责 HTTP GET 请求。它是 URLSessionTask 的具体实现。一般用于从服务器端获取数据,并存放在内存中。
  • URLSessionUploadTask 负责 HTTP Post/Put 请求。它继承了 URLSessionDataTask。一般用于上传数据。
  • URLSessionDownloadTask 负责下载数据。它是 URLSessionTask 的具体实现。它一般将下载的数据保存在一个临时的文件中;在 cancel 后可将数据保存,并之后继续下载。

谈谈 iOS 开发中数据持久化的方案

  • plist。它是一个 XML 文件,会将某些固定类型的数据存放于其中,读写分别通过 contentsOfFile 和 writeToFile 来完成。一般用于保存 App 的基本参数。
  • Preference。它通过 UserDefaults 来完成 key-value 配对保存。如果需要立刻保存,需要调用 synchronize 方法。它会将相关数据保存在同一个 plist 文件下,同样是用于保存 App 的基本参数信息。
  • NSKeyedArchiver。遵循 NSCoding 协议的对象就就可以实现序列化。NSCoding 有两个必须要实现的方法,即父类的归档 initWithCoder 和解档 encodeWithCoder 方法。存储数据通过 NSKeyedArchiver 的工厂方法 archiveRootObject:toFile: 来实现;读取数据通过 NSKeyedUnarchiver 的工厂方法 unarchiveObjectwithFile:来实现。相比于前两者, NSKeyedArchiver 可以任意指定存储的位置和文件名。
  • CoreData。前面几种方法,都是覆盖存储。修改数据要读取整个文件,修改后再覆盖写入,十分不适合大量数据存储。CoreData 就是苹果官方推出的大规模数据持久化的方案。它的基本逻辑类似于 SQL 数据库,每个表为 Entity,然后我们可以添加、读取、修改、删除对象实例。它可以像 SQL 一样提供模糊搜索、过滤搜索、表关联等各种复杂操作。尽管功能强大,它的缺点是学习曲线高,操作复杂。
  • 以上几种方法是 iOS 开发中最为常见的数据持久化方案。除了这些以外,针对大规模数据持久化,我们还可以用 SQLite3、FMDB、Realm 等方法。相比于 CoreData 和其他方案,Realm 以其简便的操作和丰富的功能广受很多开发者青睐。

iOS开发中并发操作有哪几种方式

  • NSThread 可以最大限度的掌控每一个线程的生命周期。但是同时也需要开发者手动管理所有的线程活动,比如创建、同步、暂停、取消等等,其中手动加锁操作挑战性很高。总体使用场景很小,基本是造轮子或是测试时使用。
  • GCD是 Apple 推荐的方式,它将线程管理推给了系统,用的是名为 dispatch queue 的队列。开发者只要定义每个线程需要执行的工作即可。所有的工作都是先进先出,每一个 block 运转速度极快(纳秒级别)。使用场景主要是为了追求高效处理大量并发数据,如图片异步加载、网络请求等。
  • Operations 与 GCD 类似。虽然是 OperationQueue 队列实现,但是它并不局限于先进先出的队列操作。它提供了多个接口可以实现暂停、继续、终止、优先顺序、依赖等复杂操作,比 GCD 更加灵活。应用场景最广,效率上每个 Operation 处理速度较快(毫秒级别),几乎所有的基本线程操作都可以实现。

试比较以下关键词:Serial, Concurrent, Sync, Async

  • Serial/Concurrent 声明队列的属性是串行还是并发。串行队列(Serial Queue)指队列中同一时间只能执行一个任务,当前任务执行完后才能执行下一个任务,在串行队列中只有一个线程。并发队列(Concurrent Queue)允许多个任务在同一个时间同时进行,在并发队列中有多个线程。串行队列的任务一定是按开始的顺序结束,而并发队列的任务并不一定会按照开始的顺序而结束。
  • Sync/Async 表明任务是同步还是异步执行。同步(Sync)会把当前的任务加入到队列中,除非等到任务执行完成,线程才会返回继续运行,也就是说同步会阻塞线程。异步(Async)也会把当前的任务加入到队列中,但它会立刻返回,无需等任务执行完成,也就是说异步不会阻塞线程。
  • 无论是串行还是并发队列都可以执行执行同步或异步操作。注意在串行队列上执行同步操作容易造成死锁,在并发队列上则不用担心。异步操作无论实在串行队列还是并发队列上都可能出现竞态条件的问题;同时异步操作经常与逃逸闭包一起出现在 API 的设计当中。

iOS并发编程中的三大问题

在并发编程中,一般会面对这样的三个问题:竞态条件、优先倒置、死锁问题。针对 iOS 开发,它们的具体定义为:

  • 竞态条件(Race Condition)。指两个或两个以上线程对共享的数据进行读写操作时,最终的数据结果不确定的情况。
  • 优先倒置(Priority Inverstion)。指低优先级的任务会因为各种原因先于高优先级任务执行。
  • 死锁问题(Dead Lock)。指两个或两个以上的线程,它们之间互相等待彼此停止执行,以获得某种资源,但是没有一方会提前退出的情况。iOS 中有个经典的例子就是两个 Operation 互相依赖:

试比较以下 GCD 中的方法 dispatch_async, dispatch_after, dispatch_once, dispatch_group

  • dispatch_async 用于对某个线程进行异步操作。异步操作可以让我们在不阻塞线程的情况下充分利用不同线程和队列来处理任务。例如我们需要从网络端下载图片,然后将图片赋予某个 UIImageView,就可以用到 dispatch_async:
  • dispatch_after 一般用于主线程的延时操作。例如要将一个页面的导航标题由“等待”之后 2 秒改为“完成”,可以用 dispatch_after 来实现:
  • dispatch_once 用于确保单例的线程安全。它表示修饰的区域只会访问一次,这样多线程情况下类也只会初始化一次,确保了 Objective-C 中单例的原子化。
  • dispatch_group 一般用于多个任务同步。一般用法是当多个任务关联到同一个群组(group)后,所有的任务在执行完后我们执行一个统一的后续工作。注意 dispatch_group_wait 是个同步操作,它会阻塞线程。

说说苹果官方的 MVC 架构的优缺点?

MVC 的优点有 2 个:

  • 代码总量少。基本上大量的逻辑和视图代码都集中在 ViewController 里,View 和 Model 也严格区分,代码分配遵循一定规则。
  • 简单易懂。新人可以快速上手;修改和增加新的功能也没有明显障碍;即使是没有经验的开发者也可以很好维护。 缺点主要由视图层 和控制器层高度耦合造成,其负面影响主要为:
  • 代码过于集中。ViewController 因为将两部分高度耦合,它将处理交互、视图更新、布局、Model 数据获取和修改、导航等几乎所有操作。
  • 难以进行测试。由于高度耦合,使得用于检测功能为主的单元测试需要配合特定视图才能进行,测试难度陡增。所以经常在 MVC 架构中,开发者一般只对 Model 进行测试。
  • 难以扩展。在 ViewController 里添加新功能需要格外小心,高度耦合的逻辑结构增加了出错的风险;同时由于 View 和 Controller 部分由于互相依赖,增加新功能不仅可能需要大量修改原有代码,也会使 ViewController 愈发笨重。
  • Model 层过于简单。相比于 ViewController 的庞大代码,Model 层只是定义几个属性。在 Objective-C 的 “.m” 实现文件中,更是几乎看不到代码。
  • 网络请求逻辑无从安放。网络层放在 Model 中,其异步调用的 API 请求会使得整个 Model 层变得复杂。若是将网络层 放在 ViewController 中,则耦合进一步加剧,以上缺点更加放大。
iOS

链表(上)

部署Flask服务器流程