iOS面试题回忆

552 阅读13分钟
一、 项目包体积优化

影响安装包体积大小的三个因素:

  • Xcode 配置
  • 资源文件
  • 代码层面
资源文件瘦身
  1. 移除未使用资源(图片,音频,gif等资源)
  2. 删除x1图片
  3. 压缩图片等资源文件
  4. 删除重复文件
  5. 部分大资源文件通过运行下载
  6. 图片资源放入.xcassets
代码瘦身
  1. 删除未使用代码
  2. 删除版本遗留代码
  3. 精简重复代码
Xcode编译选项配置
  1. Valid Architectures

设置编译生成的ipa包所支持的架构

  1. Strip Link Product 和 Deployment Postprocessing

Strip Linked Product 默认为 YesDeployment Postprocessing 默认为 NoStrip Linked ProductDeployment Postprocessing 设置为 YES 的时候才生效。当Strip Linked Product设为YES的时候,ipa会去除掉symbol符号,运行 App 断点不会中断,在程序中打印[NSThread callStackSymbols]也无法看到类名和方法名。而在程序崩溃时,终端的函数调用栈中也无法看到类名和方法名。但是不会影响正常的崩溃日志生成和解析,依然可以通过符号表来解析崩溃日志,适合线上使用,建议在 release 下都设置为 Yes

  1. Generate Debug Symbols

默认为 Yes,当设置为 Yes 时,编译生成的 .o文件会更大,包含了断点信息和符号化的调试信息,方便开发阶段调试,建议在 release 下设置为 No,线上需要获取崩溃信息时搭配编译生成的 dSYM 文件解析符号。

  1. Enable C++ ExceptionsEnable Objective-C Exceptions

默认都为 Yes,用于捕获 C++OC 的异常,如果项目中使用了 try catch, 可考虑去掉并在 release 下设置为 No,配合在 Other C Flags 添加 -fno-exceptions-fno-rtt ,会有比较明显的体积减小。

其它
Framework 静态库瘦身
  • 只保留需要的指令集
  • 精简Framework代码,移除未使用代码
第三方库引入
  • 可根据需要,保留部分第三方库代码,制作供项目使用的二方库

二、 组件化怎么做的


解耦分层
  • 基础层:不常改动的基础类
  • 通用层:工具类,数据管理,第三方库等
  • 网络层:网络库,pingback投递等
  • 业务层:首页,设置,详情等
  • 路由:中间件

各个模块采用cocoapods管理, 库采用的是制作成Framework方式

路由实现方案

采用注册制,维护一个类的字符串的plist文件,动态解析类名生成控制器,类似于蘑菇街的URL注册方案

三、 https原理及charles抓包https原理


https三要素:
  1. 加密:通过对称算法加密
  2. 认证:通过数字签名验证,因为私钥只有合法的发送方持有,其他人伪造的数字签名无法通过验证
  3. 报文完整性:通过数字签名实现,数字签名中包含了消息摘要,其他人篡改的消息无法通过验证
https三阶段:
  1. CA证书校验:客户端验证证书合法性,从而验证公钥合法性
  2. 秘钥协商:客户端生成随机数,通过非对称加密,用公钥和私钥进行加密解密传输随机数
  3. 数据传输:使用第二步得到的随机数,通过对称加密进行数据传输
https原理:

https原理.png

证书验证
  1. 客户端发起https请求
  2. 服务端返回https证书
  3. 客户端验证证书是否合法
数据传输
  1. 证书合法后,客户端在本地生成随机数
  2. 客户端用从服务器收到的证书中的公钥进行加密随机数,并把加密后的随机数传递到服务端
  3. 服务端通过私钥解密随机数
  4. 服务端通过客户端传递的随机数,用对称加密算法加密数据,传递给客户端
加密选择

https 使用的是对称加密和非对称加密混合,非对称加密是传输随机数用的,对称加密是对数据进行加密传输,因为非对称加密性能低

Charles抓包原理概括为: Charles抓包HTTPS的原理是通过拦截设备或模拟器上的网络流量,并使用自签名的SSL证书来解密和查看HTTPS请求和响应的内容。这样,开发者可以方便地分析和调试应用程序与服务器之间的加密通信。

具体有以下几个步骤:

  1. 代理设置:为了拦截HTTPS流量,Charles需要在设备或模拟器上设置代理。通常,你需要在设备或模拟器的网络设置中配置代理服务器地址和端口,将其指向运行Charles的机器。
  2. SSL证书生成:当设备或模拟器连接到Charles代理时,Charles会生成一个自签名的SSL证书。该证书用于代表Charles来与应用程序建立安全连接。
  3. 安装根证书:为了让设备或模拟器信任Charles生成的证书,你需要在设备或模拟器上安装Charles的根证书。这样,设备或模拟器就会将Charles的自签名证书视为受信任的证书。
  4. HTTPS代理:当应用程序发起HTTPS请求时,Charles会拦截该请求并使用自己的SSL证书与应用程序建立安全连接。此时,Charles充当了应用程序与服务器之间的中间人。
  5. 解密和查看流量:使用自己的SSL证书,Charles可以解密经过它的HTTPS流量。这允许Charles查看请求和响应的内容,包括URL、请求头、请求体、响应头和响应体等信息。
中间人攻击原理
  1. 本地请求被劫持(如DNS劫持等),所有请求均发送到中间人的服务器
  2. 中间人服务器返回中间人自己的证书
  3. 客户端创建随机数,通过中间人证书的公钥对随机数加密后传送给中间人,然后凭随机数构造对称加密对传输内容进行加密传输
  4. 中间人因为拥有客户端的随机数,可以通过对称加密算法进行内容解密
  5. 中间人以客户端的请求内容再向正规网站发起请求
  6. 因为中间人与服务器的通信过程是合法的,正规网站通过建立的安全通道返回加密后的数据
  7. 中间人凭借与正规网站建立的对称加密算法对内容进行解密
  8. 中间人通过与客户端建立的对称加密算法对正规内容返回的数据进行加密传输
  9. 客户端通过与中间人建立的对称加密算法对返回结果数据进行解密

由于缺少对证书的验证,所以客户端虽然发起的是 HTTPS 请求,但客户端完全不知道自己的网络已被拦截,传输内容被中间人全部窃取。

参考资料: HTTPS原理和防范中间人攻击

四、 自动释放池原理(销毁的原理)


1. 基本
  • 场景:

    有些函数或者方法需要返回一个对象,而系统可能在返回对象之前,就已经销毁了对象。

    如果要保证能正常返回对象,就需要让对象延迟销毁,自动释放池就可以满足这种情况。

  • 概念:

    自动释放池是一个存放对象的容器,它会保证延迟销毁池中的所有对象。

  • autorelease

    该方法不会改变对象的引用计数,只是将该对象添加到自动释放池中,它会返回调用该方法的对象本身。

    当该自动释放池释放时,它里面的所有对象都会执行release方法

  • autoreleasepool是由多个autoreleasepoolpage以双向链表的形式连接起来

  • autoreleasepool的基本原理:

    在每个自动释放池创建的时候,会在当前的autoreleasepoolpage中设置一个标记位,当有对象调用autorelease时,会把对象添加到autoreleasepoolpage中,若当前页添加满了,会初始化一个新页,然后用双向链表连接起来,并把初始化的新页作为hotpage,自动释放池释放时,会从最下面依次往上pop,调用每一个对象的release方法,直到遇到标志位.

五、 App冷启动做了什么


启动时,App的进程不在系统里,需要开启新的进程

冷启动需要分为三个阶段:

  • main()函数执行前(pre-main阶段)
  • main()函数执行后(从main()函数执行后到设置完rootViewController)
  • 首屏渲染完成后(从rootViewController设置完成到didFinishLaunchWithOptions方法作用域结束)
  1. main()函数执行前
  • 加载可执行文件(App中所有的.o文件)
  • 加载动态链接库,进行rebase指针调整和bind符号绑定
  • ObjCruntime初始化:包括ObjC相关classcategory注册和selector唯一性检查
  • 初始化:包括执行+load()方法,attribute(constructor)修饰的函数的调用,创建c++静态全局变量等

总结:

1. 首先app启动后,main()函数调用前,系统内核(kernel)会创建一个进程

其次,加载可执行文件

然后,加载dyld,主要分为4

  1. load dylibs:这一阶段会分析应用依赖的dylib,找到mach-o文件,打开和读取文件并验证其有效性,接着找到代码签名注册到内核,最后对dylib的每一个segment进行mmap()调用
  2. 进行rebase指针调整和bind符号绑定
  3. ObjCsetup():包括ObjC相关classcategory注册和selector唯一性检查
  4. initializers: 包括执行+load()方法,attribute(constructor)修饰的函数的调用,创建c++静态全局变量等
2. main()函数执行后
  • 首屏初始化所需配置文件的读写操作
  • 首屏列表大数据的读取
  • 首屏渲染的计算
3. 首屏渲染完成后
  • 初始化首屏渲染不需要的功能
  • 优化主线程,先处理会卡主主线程的方法

六、flutter在setstate之后做了什么


@protected
void setState(VoidCallback fn) {
  ...
  ...
  final Object? result = fn() as dynamic;
  ...
  ...
  _element!.markNeedsBuild();
}

源码发现,在调用setState之后,最终会到当前_element元素调用markNeedsBuild方法

element是一个抽象类

abstract class Element extends DiagnosticableTree implements BuildContext
void markNeedsBuild() {
  assert(_lifecycleState != _ElementLifecycle.defunct);
  assert(owner != null);
  assert(_lifecycleState == _ElementLifecycle.active);
  ...
  if (dirty) {
    return;
  }
  _dirty = true;
  owner!.scheduleBuildFor(this);
}

markNeedsBuild主要做了这几件事:

  • 标记当前elementdirty
  • BuildOwner拿到当前的element,调用scheduleBuildFor方法
/// Adds an element to the dirty elements list so that it will be rebuilt
/// when [WidgetsBinding.drawFrame] calls [buildScope].
void scheduleBuildFor(Element element) {
  assert(element.owner == this);
  if (element._inDirtyList) {
    _dirtyElementsNeedsResorting = true;
    return;
  }
  if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
    _scheduledFlushDirtyElements = true;
    onBuildScheduled!();
  }
  _dirtyElements.add(element);
  element._inDirtyList = true;
  assert(() {
    if (debugPrintScheduleBuildForStacks) {
      debugPrint('...dirty list is now: $_dirtyElements');
    }
    return true;
  }());
}

将被标记dirtyelement添加到_dirtyElements链表

然后调用scheduleFrame来注册Vsync回调。 当下一次vsync信号的到来时会执行handleBeginFrame()handleDrawFrame()来更新UI

简单总结如下: 在Flutter中,setState()方法是用于更新与当前Widget相关的状态并触发重新构建的方法。当调用setState()时,Flutter会执行以下步骤:

  1. 标记状态变化:Flutter会标记当前Widget的状态已经发生变化,这意味着需要进行重新构建。
  2. 调用build方法:Flutter会调用当前Widget的build()方法,该方法返回一个新的Widget树。
  3. 对比前后Widget树:Flutter会将新的Widget树与之前的Widget树进行对比,找出差异。
  4. 更新差异部分:Flutter会将差异部分更新到底层的渲染引擎中,以更新屏幕上的UI。

基本上,setState()方法的目的是通知Flutter框架当前Widget的状态已经改变,并触发重新构建,以便更新UI。这种机制使得Flutter能够高效地根据状态变化来更新UI,而不需要重建整个Widget树。

七、 链表反转


在遍历列表时,将当前节点的 next 指针改为指向前一个元素。由于节点没有引用其上一个节点,因此必须事先存储其前一个元素。在更改引用之前,还需要另一个指针来存储下一个节点,在最后返回新的头引用! 代码如下:

public ListNode reverseList(ListNode head) { 
  ListNode prev = null; 
  ListNode curr = head; 
  while (curr != null) { 
    ListNode nextTemp = curr.next; 
    curr.next = prev; 
    prev = curr; 
    curr = nextTemp; 
 } 
  return prev; 
}

八、 快排时间复杂度


O(nlogn)~ O(n^2)

九、 flutter热重载,热启动区别


热重载

Flutter 的热重载功能可帮助你在无需重新启动应用程序的情况下快速、轻松地测试、构建用户界面、添加功能以及修复错误。通过将更新的源代码文件注入到正在运行的 Dart 虚拟机(VM) 来实现热重载。在虚拟机使用新的字段和函数更新类之后, Flutter 框架会自动重新构建 widget 树,以便你可以快速查看更改的效果。

JIT 和 AOT

JIT(Just In Time):指的是即时编译或运行时编译,在 Debug 模式中使用,可以动态下发和执行代码,启动速度快,但执行性能受运行时编译影响;

AOT(Ahead Of Time):指的是提前编译或运行前编译,在 Release 模式中使用,可以为特定的平台生成稳定的二进制代码,执行性能好、运行速度快,但每次执行均需提前编译,开发调试效率低。

这两种编译模式,AOT 是静态编译,最终产物编译成可直接执行的机器码,而 JIT 则是动态编译,dart 代码编译成的是中间代码,在程序运行的时候通过 Dart VM 解释运行。

热重载步骤
  • 工程改动:热重载Server会逐一扫描工程中的文件,检查是否有新增、删除或者改动,直到找到在上次编译之后,发生变化的 Dart 代码。

  • 增量编译:热重载模块会将发生变化的 Dart 代码,通过编译转化为增量的 Dart Kernel 文件。

  • 推送更新:热重载Server将增量的 Dart Kernel 文件通过 RPC 协议,发送给正在手机上运行的 Dart VM

  • 代码合并:Dart VM 会将收到的增量 Dart Kernel 文件,与原有的 Dart Kernel 文件进行合并,然后重新加载新的 Dart Kernel 文件。

  • Widget增量渲染:在确认 Dart VM 资源加载成功后,Flutter 会将其 UI 线程重置,通知 flutter.framework 重建 Widget

不支持热重载的场景
  • main 方法里的更改
  • initState 方法里的更改
  • 代码出现编译错误
  • 全局变量和静态属性的更改
  • Widget 状态无法兼容
  • 枚举和泛类型更改

这些情况只能通过热启动或者冷启动来处理

热重载只执行 build方法,重新构建widget树,不会执行 initStatemain方法

参考资料: 热重载原理解析

十、 flutter与原生交互的方式


交互方式用途交互方向返回值
BasicMessageChannel传递字符串和半结构化信息双向交互
MethodChannel传递方法双向交互
EventChannel数据流通信原生发送到Flutter

三者本质上都是传递的数据

具体的使用方法可以参照我的如下文章: