Skip to content

自我介绍

你好,我是欧阳林,有7年iOS开发经验和3年 Flutter 跨端开发经验。上一份工作是在广州轻阅网络科技有限公司任职,我深度参与了菠萝包轻小说、点鸭互动小说等几个App迭代维护以及技术攻坚,我主导了互动小说阅读引擎从0到1的研发,基于Flutter实现了iOS/Android双端集成,并支持Web端作者后台预览。

同时我也独立负责了出海项目 LoloBun 从技术选型到合规上架,积累了完整的出海应用经验。在技术深度上擅长性能优化和架构设计,我能在技术追求与商业交付间找到平衡点,日常深度使用AI工具提升研发效率。

基础部分

  • Swift 的 结构体和类的区别?
    • 结构体是值类型、不可继承、默认分配在栈上、深拷贝传递、没有析构函数。
  • OC的Property关键词有哪些?
    • class(类属性)、nonatomic、atomic(单次读写加锁,不保证线程安全)、readwrite、readonly、assign、weak、strong、__unsafe_unretained、nullable、nonnull、null_resettable、null_unspecified
  • Block的本质是啥?
    • Block 本质是"编译器生成的、包含函数指针的、OC 对象格式的闭包结构,

1. OC 运行机制本质是啥?

OC 本质:OC是基于C语言超集,通过Runtime(C和汇编实现的动态库)增加面向对象能力。核心理念是动态性,很多决定推迟到 运行时,而不是编译/链接时。

实例方法调用: [实例 method] -isa-> 实例类对象 -superclass-> 父类类对象 -- .. --> NSObject 类对象 --> 消息动态转发机制

类方法调用: [类 method] -isa-> 元类对象 -superclass -> 父类元类对象 -- .. --> NSObject 元类对象 --> NSObject 类对象 --> 消息动态转发机制

struct objc_object{
    Class isa;  -->指向类对象
    实例变量...
}
struct objc_class : objc_object {
    // 类特有的数据
    Class superclass;                      // 父类
    cache_t cache;                         // 方法缓存
    class_data_bits_t bits;                // 类信息位字段
    
    // 通过bits获取实际数据
    class_rw_t *data() const {
        return bits.read();
    }
};

2. OC的Runtime有哪些运用?

消息派发机制、交换方法、AOP无侵入埋点、KVO、KVC、分类添加属性、归档与解档、json转模型、安全防护、动态添加类等

Swift运行机制是啥?

机制主要体现在三个方面:方法派发机制、内存分配、极简的Swift Runtime。

  • 方法派发机制:
    • 直接派发:值类型(struct、enum);被final修饰的类和方法;extension(不包含协议拓展)中定义的方法。
    • 函数表派发:普通的类及其方法、协议。类在内存中维护一张 虚拟函数表(V-Table),协议会生成一张 协议见证表(Protocol-Witness-Table PWT),运行时会先在内存中寻找到表找到函数指针再跳转执行。
    • 消息派发:@objc dynamic 之类的,会使用 OC Runtime走消息动态派发流程。
  • 内存分配:值类型(结构体、枚举、元祖)优先分配在栈上,不需要ARC介入;引用类型(class、闭包)需要通过 swift_retain/swift_release 来维护引用计数。
  • Runtime: 代码的 as? is 类型检查和转换;泛型实例化;反射(Mirror)

3. Swift虚拟函数(V-Table)表怎么工作的?

“V-Table 是一组连续的函数指针数组。在 Swift 中,它并不是一个独立存在的数据结构,而是被直接内嵌在静态数据区(Data Segment)的 类元数据 (ClassMetadata) 结构体的尾部。”

这也解释了为什么定义在 Extension 中的普通方法无法被子类重写。因为 V-Table 的内存布局在编译期就已经固定,无法在扩展中动态改变表格大小和偏移量。因此,扩展中的方法默认采用的是静态的直接派发。

建表再查表。理清楚流程就知道为啥Swift的继承与多态,以及Extension无法重载(Extension方法)

  • 建表(拓扑排序,先扫描所有的类,然后构建依赖树,先建立没有继承其他class或继承外部(NSObject)等之类的的表,放在classmetadata 最下面按顺序):
    • 继承拷贝:子类会无条件完整拷贝父类的 V-Table。
    • 重写覆盖(Override):如果子类重写了父类的方法,就会在自己这张表里,把对应位置的指针替换成自己新的函数地址。
    • 新增追加:如果子类新增了方法,新方法的指针只能紧挨着表尾依次追加,绝不能插队。
  • 读表
    • 读类型:去 myPet 指向的堆内存中,读取这个对象真实的类型元数据(发现它其实是个 Dog)。
    • 找表:顺着类型元数据,找到 Dog 类的 V-Table。
    • 跳偏移:因为编译器知道 makeSound 永远在索引 1 的位置,所以 CPU 直接去表中提取索引 1 的函数指针,并跳转执行。

4. Swift 协议目击表(Protocol-Witness-Table)?

Swift 的强大在于支持面向协议编程(POP),让原本没有继承关系的值类型(Struct/Enum)也能实现多态。但由于不同类型占用内存大小不同,系统无法统一处理,因此 Swift 引入了 Existential Container(存在容器) 这一底层结构来统一内存布局。

在 64 位系统下,存在容器占用固定的 5 个 Word(40 字节) 的连续内存。它的内部结构被划分为三个部分:

  • Value Buffer (前 3 个 Word):用于内联存储小于 24 字节的数据;如果实际数据超大,则在堆区开辟内存,并将指针存在这里。

  • VWT (Value Witness Table, 第 4 个 Word):存储用于管理内存生命周期的函数指针(分配、拷贝、销毁),因为协议本身不知道具体类型的内存管理规则。

  • PWT (Protocol Witness Table, 第 5 个 Word):协议见证表。它同样是一个函数指针数组,内部存储了该具体类型实现协议方法的真实内存地址。

PWT 的工作原理 当我们将一个具体的 Struct 赋值给协议类型时,编译器会进行封箱(Boxing)操作,生成这个 40 字节的容器。运行期调用协议方法时,系统会越过容器前段,直接读取第 5 个 Word 拿到 PWT 指针,再通过偏移量取出函数地址并执行,从而实现了跨越值类型的动态派发。

5. 反射(Mirror)主要用来做啥?

用来查看属性和值,主要用来做日志打印(可以打印出所有属性以及值)、本地数据库ORM框架映射、模型转字典(早期用,现在都用Codable协议)、依赖注入。

6. 什么是Runloop以及作用?

本质它就是一个死循环(do-while 循环),加一套事件驱动和休眠唤醒机制。

Runloop 的三大核心作用:

  • 保持程序持续运行(保活):保证主线程永远不会退出,让 App 能一直活着。

  • 处理 App 中的各种事件:你的每一次屏幕滑动、点击(Touch)、每一个定时器(NSTimer)、每一次网络请求的回调、甚至界面 UI 的刷新(PerformSelector),全部都是 Runloop 在排队处理的。

  • 节省 CPU 资源,提高程序性能:这是它最伟大的设计。它让线程在有事做的时候全力以赴,在没事做的时候彻底休眠(挂起),把 CPU 资源让给其他线程,绝不空转浪费电量。

Runloop 肚子里的“四大金刚”:

Runloop 不是瞎转的,它内部有极其严密的结构。一个 Runloop 包含多个 Mode(模式),每个 Mode 里又包含了以下几个核心部件:

  • Source(事件源):分为 Source0 和 Source1。

  • Source0:非基于 Port 的,也就是App 内部事件(比如你点击了一个按钮,或者执行了 performSelector)。它需要手动标记为待处理,并手动唤醒 Runloop。

  • Source1:基于 Mach Port 的,也就是系统底层和硬件事件(比如屏幕捕捉到一次触摸,或者网络端口收到数据)。它能主动唤醒休眠中的 Runloop。

  • Timer(定时器):你平时写的 NSTimer 本质上就是注册到了 Runloop 里。Runloop 跑圈时顺便看看时间到了没,到了就触发。

  • Observer(观察者):它是 Runloop 内部状态的“监视器”。Runloop 在跑圈时,每进入一个新阶段(比如刚唤醒、准备处理 Timer、准备休眠),都会广播一个通知。系统底层的 UI 自动刷新(AutoreleasePool 的释放等),就是靠监听这些状态来触发的。

为什么滑动 ScrollView 时,NSTimer 会停止跳动?

答案就在 Mode 里:正常情况下主线程处于 DefaultMode,你的 Timer 注册在这里。当你滑动屏幕时,系统为了保证滑动的绝对流畅,会把主线程的 Runloop 强行切换到 UITrackingRunLoopMode。因为两个 Mode 是相互隔离的,所以 DefaultMode 里的 Timer 就被暂停了。解决办法就是把 Timer 注册到 NSRunLoopCommonModes 里。

界面的 UI 是什么时候真正渲染到屏幕上的?

答案在 Observer 里:你用代码改了 view.frame,界面不会立刻变化。系统只是做了个标记。等当前代码执行完,Runloop 跑到 BeforeWaiting(准备休眠) 状态时,Observer 会触发一个回调,把所有标记了需要刷新的 UI 集中起来,打包提交给 GPU 渲染。

7. 内存管理?

iOS 内存管理的核心是 ARC(自动引用计数)。它的本质是编译器在编译时,自动在合适的位置帮我们插入 retain、release 和 autorelease 代码。 在底层,ARC 依靠 SideTable(散列表) 来管理引用计数,并利用 weak_table 来实现弱引用。这也是为什么对象销毁时,weak 指针能自动置为 nil 的原因。

另外,它还配合 Runloop 的 AutoreleasePool 来处理延迟释放。每圈 Runloop 准备休眠前,都会清空释放池里的对象。 我们在开发中主要关注循环引用,通常用 weak 指针来打破引用闭环。排查工具主要用 Xcode 的 Leaks 和 Memory Graph。

维度一:物理内存分布 (Stack vs Heap)

“在 iOS 中,内存被划分为五大区(TEXT、DATA、BSS、堆区、栈区)。我们开发者最需要关心的就是堆(Heap)和栈(Stack)。

栈区 (Stack):是一块连续的内存,速度极快,由 CPU 硬件指针自动管理。在 Swift 中,所有的**值类型(Struct、Enum)**默认都分配在栈上,用完即毁,完全没有 ARC 的性能开销。

堆区 (Heap):是不连续的内存,速度较慢。所有的 OC 对象、Swift 的引用类型(Class)和闭包都分配在堆上。这块区域极其容易产生内存泄漏(碎片化),因此才需要引入 ARC 来进行精准的生命周期管理。”

维度二:ARC 的底层核心原理与 SideTable

“ARC 的表面是强引用(Strong)和弱引用(Weak),但其底层依赖的是 SideTable(散列表/辅助表) 数据结构。

引用计数的存储:在 64 位系统下,对象的引用计数优先存储在对象自身 isa 指针的扩展位段(Non-pointer isa)中。如果计数值爆表存不下了,就会被转移到全局的 SideTable 的 RefcountMap 中。

Weak 指针的魔法(底层亮点):为什么弱引用对象销毁时,指针会自动置为 nil?因为系统在全局维护了一个叫 weak_table_t 的哈希表。以对象的内存地址为 Key,以所有指向它的 weak 指针数组为 Value。当对象的引用计数归零触发 dealloc 时,系统会通过对象的地址去这个表里把所有的 weak 指针拿出来,统一赋值为 nil,从而绝对杜绝了**野指针(Wild Pointer)**异常。”

维度三:延迟释放机制与 Runloop (AutoreleasePool)

“有些对象我们不能立刻释放(比如函数内部的局部变量作为返回值返回时),这时就需要用到 AutoreleasePool(自动释放池)。 它的底层是一个双向链表构成的 AutoreleasePoolPage 栈。 它和 Runloop 的关系极其紧密(高分必答):主线程的 Runloop 默认注册了两个 Observer。

第一个 Observer 监听 Runloop 即将进入(Entry),此时会创建一个最新的释放池。

第二个 Observer 监听 Runloop 准备休眠(BeforeWaiting),此时会触发一次释放池的 Drain(清空释放内部所有对象),并立刻创建一个新的池子。

所以,自动释放池里的对象,最迟也会在当前这圈 Runloop 跑完、即将休眠前被统一销毁。”

8. 那你平时是怎么排查和解决内存泄漏的?”

绝大多数内存泄漏都是由于**循环引用(Retain Cycle)**造成的,也就是对象 A 强持有 B,B 也强持有 A,导致它们的引用计数永远降不到 0。 常见的场景有三种:Block/闭包的循环捕捉、Delegate 未使用 weak 修饰、NSTimer 未及时 invalidate。 解决的核心思想只有一个:打破闭环,将其中一环改为 weak 或 unowned(Swift)。

9. Swift 内存管理 Cow机制

写时复制(Copy-on-Write, 简称 CoW)。

“Swift 里的 Array、Dictionary、String 等集合类型虽然是值类型(Struct),但如果数据量庞大,每次赋值都拷贝全量内存会极其消耗性能。

为此,Swift 在底层引入了 CoW 机制:当你把数组 A 赋值给数组 B 时,它们在内存中其实共享同一块数据缓存。只有当其中任意一个数组**尝试修改(Write)**数据时,系统才会立刻分配一块新的内存将数据拷贝过去,然后再进行修改。 这是一种极度聪明的‘懒惰’机制,完美兼顾了值类型的安全性和引用类型的性能。”

10. 多线程?

对于 OC,我倾向于按需选择。GCD 是首选,它基于 C 语言的派发队列,处理简单的线程切换和异步派发效率最高。 但涉及复杂业务流时,我会选择 NSOperation。它通过封装对象提供了任务依赖和状态控制,更重要的是,它能通过 maxConcurrentOperationCount 精准控制并发频率,这在处理类似‘下载管理器’或‘大规模图片处理’时,是防止线程爆炸(Thread Explosion)和系统 OOM 的关键。

“Swift 的并发编程则进入了‘结构化’和‘内存安全’的维度。 Actor 模型:通过内置的串行队列和邮箱机制(Mailbox),确保同一时间只有一个 Task 能访问其内部状态。它利用编译器检查配合 Sendable 协议,从根源上解决了多线程中最头疼的数据竞争,替代了过去繁琐的加解锁操作。

Task 任务树:引入了结构化并发(Structured Concurrency)。Task 之间存在明确的父子关系,这种‘任务树’机制让取消信号可以自动向下传递,极大简化了异步任务的资源管理。

async/await 协作式调度:这是性能飞跃的核心。编译器会将代码切分为多个 Continuations(延续性片段)。当遇到 await 时,任务会挂起并释放当前线程,让 CPU 去处理更紧急的任务。等后台操作完成后,再从线程池中恢复执行。这种协作式调度避免了传统线程阻塞带来的上下文切换开销,是目前最高效的并发处理方案。”

11. iOS底层渲染机制

iOS 的渲染不是一个框架完成的,而是由 Core Animation 统筹同作业。

一、 三大角色分工
UIKit / SwiftUI:处于最上层,负责处理交互事件和视图布局,但不直接参与渲染。

Core Animation (Render Server):渲染的中枢。它负责将视图(CALayer)的属性打包,并交给底层的 GPU 处理。

Core Graphics / Metal:
    Core Graphics:负责 CPU 软件绘图(如 drawRect:),慢但灵活。
    Metal (OpenGL 继承者):负责 GPU 硬件加速渲染,快且高效。
二、渲染流水线:从代码到像素的 5 个步骤
这是一个典型的 双进程协同 模式。App 进程负责“下单”,Render Server 进程负责“出货”。

Commit Phase (App 进程):

Layout:计算视图层级、Frame、Autolayout。

Display:如果是图片或自定义绘图(drawRect:),CPU 会在这里将位图解码或绘制出来。

Prepare:图片解码和格式转换。

Commit:将图层树(Layer Tree)打包发送给 Render Server 进程。

Render Server (系统渲染进程):

解析 App 传过来的包,解压成 渲染树(Render Tree)。

GPU 绘制 (GPU 硬件):

顶点着色器 (Vertex Shader):计算三角形顶点。

形状装配。

片元着色器 (Fragment Shader):计算每个像素点的颜色。

Display (屏幕显示):

将计算好的像素存入 帧缓冲区 (Frame Buffer),等待屏幕读取。
三、 屏幕刷新机制:VSync 与双缓冲
为了保证画面不撕裂、不卡顿,iOS 采用了 垂直同步 (VSync) 和 双缓冲 (Double Buffering) 机制。

VSync (垂直同步信号):屏幕每秒刷新 60 次(ProMotion 机型 120 次),每当刷新完成,硬件会发出 VSync 信号。

Runloop 协同:Runloop 监听到 VSync 信号后,会唤醒 App 开始执行下一帧的 Commit Phase。

双缓冲:GPU 有两个缓冲区(Front Buffer 和 Back Buffer)。屏幕读取 Front Buffer 的数据,GPU 则在 Back Buffer 悄悄绘制。等下一次 VSync 到来,两个缓冲区交换身份。

四、 核心性能痛点:为什么会掉帧?
如果面试官追问“为什么会卡顿?”,你可以点出这两个关键瓶颈:

CPU 瓶颈:在 Commit Phase 做太多的图片解码、复杂的布局计算或频繁的 drawRect:,导致在 VSync 到来前没能完成打包。

GPU 瓶颈:

离屏渲染 (Off-screen Rendering):当设置圆角+裁剪(masksToBounds)、阴影或光栅化时,GPU 无法一次性在当前帧缓冲区画完,必须开辟一块离屏缓冲区先画好再合成。

代价:这涉及到了上下文切换(Context Switch),极度消耗 GPU 资源。

12. Flutter渲染机制

渲染引擎在 iOS 上为 Impeller,安卓上绝大部分为 Impeller,少数为 Skia,在Web上使用Skia、Wasm、HTML。 “Flutter 的渲染原理可以概括为:‘数据驱动、三棵树协同、底层直绘’。

它通过 Widget、Element、RenderObject 三棵树的解耦,实现了高效的 Diff 算法,避免了昂贵的底层对象频繁销毁。

与原生 iOS 不同的是,Flutter 跨过了 UIKit 这一层,直接利用 Engine 层的渲染引擎(如 Impeller)将图层指令提交给 GPU。其布局逻辑遵循‘向下传递约束,向上回传尺寸’的原则,通过层级合成(Compositing)和光栅化(Rasterization)最终完成渲染。这种机制让 Flutter 摆脱了原生控件的动态性限制,达到了接近游戏的渲染性能。”

因为 Flutter 采用了 ‘线性布局模型’。它不像 UIKit 可能需要多次计算(Pass)才能确定 Frame,Flutter 的单次遍历(Single Pass)布局机制配合其高效的 三树 Diff 算法,确保了在复杂层级下依然能稳定在 60 或 120 FPS。

1. 核心本质:自绘引擎
“Flutter 的本质是**‘去原生化’的自绘渲染**。它不依赖系统原生控件,而是通过底层的 Impeller (或 Skia) 引擎,直接调用 GPU 接口将 UI 画在屏幕上,类似于游戏的渲染逻辑。”
2. 内存模型:三树协同 (核心逻辑)
“为了兼顾开发效率与性能,内存中维护了三棵树:
Widget 树:不可变的配置快照。它极其轻量,销毁和重建的开销很小。

Element 树:逻辑骨架。它负责执行 Diff 算法。当调用 setState 时,Element 会比对新旧 Widget 的 Type 和 Key。如果匹配,则说明底层节点可以复用,仅需同步属性。

RenderObject 树:真正的渲染实体。它负责执行昂贵的布局(Layout)和绘制(Paint)操作。”
3. 属性同步:updateRenderObject (物理更新)
“当 Element 发现类型和 Key 没变时,它会触发 updateRenderObject。此时,Widget 里的参数(如 padding 宽、color 色)会被同步给 RenderObject:

如果是颜色、透明度等外观属性变化,RenderObject 会标记 markNeedsPaint,下一帧只触发重绘。

如果是尺寸、约束等位置属性变化,则标记 markNeedsLayout,触发重新测量。这种增量更新极大降低了性能损耗。”
4. 渲染循环:流水线 (Pipeline)
“渲染是一个由 VSync 信号驱动的循环。其核心布局逻辑是:‘向下传递约束(Constraints),向上回传尺寸(Sizes)’。
流程简述为:Build (重建快照) -> Layout (确定位置大小) -> Paint (记录绘制指令) -> Composition (图层合成) -> Rasterize (光栅化转为像素)。”

13. Flutter和原生通信方式有哪些?

  1. MethodChannel(最常用:异步调用)
这是最常见的“逻辑触发”方式,类似于 RPC(远程过程调用)。
机制:双向通信。Flutter 可以调用原生的方法,原生也可以主动调用 Flutter。

特点:
异步执行:调用后会返回一个 Future,不会阻塞 UI 线程。
数据传递:依赖二进制序列化(StandardMessageCodec),支持基础数据类型、List 和 Map。
场景:调用系统摄像头、获取电量、发起原生支付、调用第三方 SDK 接口。
  1. EventChannel(数据流:持续监听)
用于从原生向 Flutter 发送持续不断的数据流。
机制:观察者模式。Flutter 端监听,原生端发送。

特点:
单向流:通常是由原生主动推送给 Flutter。
长连接:一旦建立监听,原生可以持续发送数据,直到取消。
场景:手机传感器数据(加速度计、陀螺仪)、网络状态变化监听、下载进度实时回传。
  1. BasicMessageChannel(基础通信:频繁交换)
用于传递简单的、频繁的数据或字符串。
机制:类似于低级的消息传递,可以自定义编解码器(Codec)。

特点:
支持同步和异步两种模式。
比 MethodChannel 更轻量,适合不需要明确方法名的纯数据交换。
场景:实时同步某些简单的状态参数,或者作为插件底层的通信协议。
  1. Pigeon(现代首选:类型安全方案)
这是 Google 官方推荐的进阶版 MethodChannel,旨在解决原生通信中最头疼的“字符串硬编码”和“类型不安全”问题。
机制:基于 MethodChannel,但引入了代码生成器。

核心优势:
类型安全:不需要再手写 methodName == "getUserInfo" 这种易错的字符串。
自动生成:你只需定义一个接口协议(Dart),Pigeon 会自动生成对应的 iOS (Swift/ObjC) 和 Android (Java/Kotlin) 代码。
场景:中大型项目,接口繁多且对代码质量要求高的场景。
  1. FFI (Foreign Function Interface) & JNI
这属于高性能/底层通信方案,绕过了 Flutter 的 Channel 机制。
机制:Dart 直接调用 C/C++ 代码,或者通过 Dart 3.x 以后的 dart:ffi 直接与 ObjC/Swift 通信。

特点:
极高性能:没有 Channel 的序列化和跨进程开销,直接操作内存。
复杂性高:需要处理内存指针和 C 语言层级的逻辑。
场景:高性能图像处理、音视频解码库、自研的 C++ 引擎。

14. 三端同构机制是指?

1. 移动端:iOS 与 Android (真正的“硬”核自绘)
在这两个移动端平台上,Flutter 展现了它最原始、最强悍的自绘能力。它会把你的 Dart 代码直接编译成底层硬件能看懂的 AOT(提前编译)ARM 机器码。

iOS 平台:
引擎 (Engine):目前全面采用 Impeller。它利用苹果底层的 Metal API 进行硬件加速渲染。
外壳 (Embedder):Flutter 会被包裹在一个标准的 FlutterViewController 中。从 iOS 系统的视角来看,Flutter App 就像是一个全屏播放的高清视频,系统完全不知道里面有各种按钮,只负责刷新这块画布。

Android 平台:
引擎 (Engine):逐渐普及 Impeller,兼容老设备时回退到 Skia。底层调用的是 Vulkan 或 OpenGL ES。
外壳 (Embedder):包裹在 FlutterActivity 或 FlutterFragment 中,向 Android 系统申请一块 Surface 来作画。
2. Web 端:完全不同的物种
当 Flutter 运行在 Web 上时,它不再包含那个庞大的 C++ 渲染引擎(Engine),而是利用浏览器的标准能力。由于 Dart 可以被编译成 JavaScript 或 WebAssembly (Wasm),Flutter 为 Web 提供了两种完全不同的渲染模式:

HTML 渲染模式 (重 DOM,轻负载):
机制:Flutter 把你的 Widget 翻译成了标准的 HTML 标签(<p>, <div>)、CSS 和少量的 Canvas。
特点:包体积极小(因为不需要下载渲染引擎),首屏加载快,对浏览器的兼容性极好。但复杂的动画会卡顿。

CanvasKit 渲染模式 (重画布,高保真):
机制:Flutter 把底层的 Skia 引擎通过 WebAssembly 编译成了能在浏览器里跑的代码(CanvasKit)。它用 WebGL 直接操控浏览器里的 <canvas> 标签进行像素级绘制。
特点:UI 表现和移动端 100% 完美一致,滚动极其丝滑。缺点是用户第一次打开网页时,必须先下载几兆的 WebAssembly 引擎包,首屏加载有白屏时间。
如果在面试中,面试官问:“既然 Flutter 在 Web 上可以转成 HTML,那它和 Vue/React 有什么区别?”

这是绝佳的加分机会,你可以这样一剑封喉:

“Vue 和 React 的核心是操作 DOM,它们的上限完全由浏览器的排版引擎决定。

而 Flutter 即使在 Web 端使用 HTML 模式,它的本质依然是**‘接管布局’**。Flutter 并没有把复杂的排版逻辑交给浏览器去算,而是自己在 Dart 层算好了每一个元素的绝对坐标,然后强行用 CSS 的绝对定位(Absolute Positioning)把它们钉在网页上。

所以,Flutter Web 本质上是一个充满绝对定位标签的画板,或者是一个巨大的 WebGL 画布 (CanvasKit),这就保证了它无论在移动端还是 Web 端,UI 绝对不会因为平台差异而跑版。”

15.App出海经验

1. 支付与订阅的深水区 (Monetization & Billing)

  • 支付边缘情况
  • 建立 S2S (Server-to-Server) 强同步机制。 配置 Apple Server Notifications 和 Google RTDN (Real-Time Developer Notifications)。当用户在系统设置里发生退款或续费时,苹果服务器会直接 webhook 通知你的服务器,你的服务器立刻修改数据库里的 VIP 状态,从而彻底解决“跨端 VIP 状态不一致”和“恶意退款白嫖”的问题。

2. 本地化与多语言的“隐形地雷” (Localization)

🤬 实战痛点:
 很多人以为本地化就是建个 en.json 和 zh.json 把字翻译了就行。但上线后发现:

 UI 撑爆:德语、俄语的单词长度往往是英语的 1.5 倍,中文的 2 倍,直接把按钮挤爆或者换行错乱。

 布局反转:中东土豪市场(阿拉伯语)是从右向左读的(RTL)。你的返回箭头、输入光标、排版全部需要镜像对称。

 复数规则:英语只有 1 个和多个,俄语/阿拉伯语的复数规则极其复杂(1个,2个,3-4个,5个以上词缀都不同)。
   UI 布局重构:抛弃任何硬编码的宽/高(绝对不能写死 width: 100)。全面拥抱 Flex 弹性布局、滚动视图(ScrollView)或约束布局。

   原生 RTL 支持测试:

   在 iOS 中使用 leading/trailing 约束,而不是 left/right。

   在 Flutter 中,根节点注入 Directionality(textDirection: TextDirection.rtl) 进行极端测试,检查图标是否需要翻转(Transform(alignment: Alignment.center, transform: Matrix4.rotationY(math.pi)))。

   使用国际化标准工具:文本不要自己拼接,使用 ICU 格式(例如 Intl 库)来处理复数和性别,不要硬写 if (count == 1) 这种低级代码。

3. 隐私合规与上架审核红线 (Privacy & Compliance)

    引入 Google UMP SDK:这是出海的标配。谷歌官方提供的 SDK,一键解决欧洲 GDPR 和美国加州 CCPA 的弹窗问题,用户同意了才初始化后续的广告和埋点 SDK。

    设计“预授权 (Pre-ATT)”弹窗:不要一打开 App 就冷冰冰地弹系统 ATT 框,用户 99% 会点拒绝。先弹一个你自定义的精美全屏页,告诉用户:“为了给您推荐更精准的内容,并且保持 App 免费,请在下一步允许跟踪”,然后再触发系统弹窗。

    彻底的 Account Deletion:在设置里加上醒目的“Delete Account”。注意,苹果要求这是硬删除(Hard Delete)。你的服务器不仅要删账号,还要删掉与其关联的用户生成内容(UGC)。

4. 网络加速与时区

1. 域名隔离
动态 API 和静态资源不放在一个域名下

2. CF 的“503/403 防火墙拦截
CF 有很强的抗 Ddos 和反爬虫机制(WAF)。有时候某个国家的网络节点被 CF 判定为高风险,CF 会直接拦截你的 API 请求,并返回一个 503 状态码和一段含有 Captcha(人机验证)的 HTML 网页。

暗号机制 (Custom Header Bypass):
客户端每次发请求,必须带上一个“App 专属暗号”(比如硬编码的 Secret Key 或者基于时间戳的动态 Token)。后端在 CF 的 WAF 规则里配置:只要 Header 里有这个暗号,直接放行,绝不弹人机验证。

容错拦截器 (Interceptor):
客户端必须在网络层兜底,识别出 CF 的拦截特征。

3. 边缘缓存控制 (Edge Cache Control)
4. 全球网络的重试与超时策略 (Retry & Timeout)
5. HTTPDNS:绕过本地运营商劫持
6. 时区只使用UTC0时区,客户端不传任何时区,只有显示时才计算时间。

5. 广告归因和数据黑盒

16. 网络层优化

整体思路:链路层优化 -> 传输层优化 -> 弱网情况 -> 用户感受方面

  • 链路层使用HTTPDNS防止劫持和解析慢的问题
  • 传输层图片使用CDN动态裁剪参数防止小ImageView解析高清大图
  • 弱网下使用动态超时、退避重试机制、备用域名和IP
  • 预加载和Websocket下发、智能缓存机制、防重发

iOS15启用TLS 1.3, TLS 1.3 0-RTT重放攻击只需要运维和后端确保非幂等性操作如 POST、PUT 写入操作返回425,然后走 1-RTT 握手连接。

17. HTTP/HTTPS 区别

HTTPS 的本质就是在 HTTP 和底层 TCP 之间,加塞了一层 TLS 加密层。端口由 80 升级到了 443。

它的核心是利用混合加密机制:在握手阶段用‘非对称加密’安全交换密钥,在传输阶段用‘对称加密’高速发送数据,保证了机密和防篡改。

但对于我们移动端来说,HTTPS 最大的代价是 ‘握手延迟’。通过升级 TLS 1.3 把建连时间从 2-RTT 压榨到 1-RTT 甚至 0-RTT,来对抗弱网卡顿。

TLS1.2: TCP 1 RTT + 2-RTT = 3次 RTT TLS1.3: TCP 1 RTT + 1/0-RTT = 2/1 次 RTT

18. 如何做内存优化?

日常开发测试中集成了MLeaksFinder 会自动检查一些页面时,发现一些如闭包循环引用这种低级内存泄露时就会立即修复,如果没那么直观会使用 Xcode Memory Graph 来定位问题;一些新增功能和核心模块更改后会使用 Instruments 通过打标记方式 以及 Allocations 来定义隐性内存泄露的问题并修复;临时创建大量对象会使用 autoreleasepool 包裹来消除内存峰值;原生收到内存警告时会同步到flutter端侧释放一些图片资源。

19. 怎么看待内存管理?

“结合我的经验,我对内存管理的理解主要分为三层:语言底层、操作系统层面和跨端架构层面。

语言底层: iOS 中 ARC的底层实现依赖于isa指针和SideTable协同工作,isa内联存储大部分强引用计数(19位),弱引用和溢出的强引用放在SideTable。为优化并发,全局SideTable采用分片设计——iOS设备8个分片,模拟器64个分片,通过地址哈希分散对象,每个分片独立锁,大幅降低竞争。 而 Flutter 侧则是依赖 Dart 的分代垃圾回收(GC),高频 UI 走新生代,长驻内存走老年代。

OS 层面: 核心痛点是 iOS 没有内存交换(Swap)机制。像解码后的图片这种 Dirty Memory 一旦激增,触碰了苹果的 Jetsam 水位线,就会被系统无声无息地强杀(OOM)。

跨端架构(核心难点): 也是我在互动引擎里踩过最大的坑。原生和 Flutter 混编时,生命周期是割裂的。原生页面被 ARC 释放了,Dart 的 GC 并不知情。所以我必须在架构层打通双端,在原生页面退出时,主动通过 Channel 强制清理底层的 ImageCache 和纹理内存。 mach_task_basic_info 的 resident_size 只是常驻内存。 task_vm_info 的 phys_footprint 大于水准线才是触发OOM真正的原因。可以监听 DISPATCH_SOURCE_TYPE_MEMORYPRESSURE ,在Jetsam机制 OOM 前一秒做最后的挣扎,清空极其耗内存的图片缓存、释放非当前展示的跨端引擎。

读取超大文件进内存,不能使用 NSData contentofFile: 应该使用 mmap 虚拟映射,直接把磁盘文件映射到虚拟内存空间,0拷贝,不占用物理内存。

void *mappedData = mmap(
        NULL,                  // 让系统选择地址
        fileSize,              // 映射大小
        PROT_READ,             // 只读权限
        MAP_PRIVATE,           // 私有映射(写时复制)
        fd,                    // 文件描述符
        0                      // 偏移量
    );

    close(fd); // 映射后可以关闭文件描述符
    
    if (mappedData == MAP_FAILED) {
        NSLog(@"mmap failed: %s", strerror(errno));
        return nil;
    }
    
    // 4. 创建 NSData (不复制内存)
    NSData *data = [NSData dataWithBytesNoCopy:mappedData
                                      length:fileSize
                                freeWhenDone:YES]; 
    // 重要:自动munmap

20. UI渲染防卡顿怎么做?

主要从这几方面:

  • 极度复杂列表不使用约束来布局
  • 异步渲染
  • 复杂计算、解码、I/O等耗时操作都放在异步线程,主线程更新
  • 高频刷新的view使用对象复用池
  • 减少图层混合
  • 减少离屏渲染:
    • 切圆角
    • 阴影无路径
    • 图层遮罩
    • 高斯模糊
    • 光栅化(本意是开启方便复用,如果频繁变化的cell开启反而因为缓存经常失效重复绘制加剧卡顿)
  • Flutter 跨端专属的防卡顿
    • 隔离重绘边界(RepaintBoundary)
    • 阻断 Widget 树重建(精准局部刷新)
    • 规避底层的 SaveLayer 操作(等同于iOS离屏渲染)
  • 线上与线下卡顿监控闭环:
    • 线下排查: 依赖 Xcode Core Animation(看黄色离屏渲染、红色图层混合)和 Flutter DevTools 的 Raster 线程 耗时。
    • 线上捕获: 利用 CADisplayLink 监控丢帧率,或者通过监听主线程 Runloop 的状态切换耗时(如 BeforeSources 到 AfterWaiting 之间如果超过阈值),抓取卡死瞬间的堆栈上报。

21. 能耗优化怎么做?

主要从几个方面:

  • 系统层面:
    • 合理使用后台任务
    • 适配低电量模式
    • 定位精度控制以及推迟位置更新
  • 应用层优化:
    • 多线程QoS分级
    • 网络层优化:合并请求、合理使用缓存、使用HTTP/2
    • UI渲染优化
    • 内存优化
  • 工具层监控(展示工程化思维) "建立完整的监控闭环:
    • 开发阶段:Xcode Instruments的Energy Log + Time Profiler
    • 测试阶段:使用XCTest的XCTMeasureMetrics进行能耗基准测试
    • 线上监控:集成MetricKit收集真实用户能耗数据,设置阈值告警"

22. OpenGLES 渲染管线

首先顶点着色器对每个顶点进行坐标变换和属性计算;接着图元装配阶段将顶点组装成点、线或三角形,并进行视锥裁剪;然后光栅化阶段将图元转换为屏幕上的片段(像素),并对顶点属性进行插值;随后片段着色器计算每个片段的最终颜色,包括光照、纹理采样等;最后逐片段操作阶段执行深度测试、模板测试和混合等操作,通过测试的片段才会写入帧缓冲区形成最终图像。

23. 音视频流程

  1. 采集(摄像头/麦克风获取原始YUV/PCM数据)
  2. 前处理(视频降噪/缩放、音频AEC/ANS处理)
  3. 编码(H.264/H.265压缩视频,AAC/Opus压缩音频)
  4. 传输(通过RTMP/RTP/HTTP协议网络传输或存储为MP4/MKV文件)
  5. 解复用(分离音视频流)
  6. 解码(还原为YUV/PCM原始帧)
  7. 同步(基于PTS时间戳以音频为基准调整视频播放节奏)
  8. 渲染(视频YUV转RGB显示,音频PCM输出到声卡)。

24. 一秒100条弹幕怎么处理?

普通文本表情弹幕直接通过Core Graphic 渲染合成一张图使用复用池的layer来显示 + CADisplayLink + 弹幕追尾算法 来更新弹幕位置 渐变文本+表情也是如此,只不过多一步混合使用 SourceIn模式来让字体为渐变色。 如果是动态图,通过Core Text 提前计算图片位置,然后留透明图层,然后最终使用复用池子的layer + CADisplayLink 来控制显示动图展示,表情动态图共享解码,不能一张图解多次。 如果还是太多可以任务切片,监听Runloop的BeforeWaiting状态,每次处理一些。

25. OT/CRDT 多人并发编辑算法?

  • OT算法的核心:

    • 收敛性:无论操作顺序如何,所有副本最终状态一致,通过transform、compose数学表达。
    • 因果顺序:如果A在逻辑上早于B,所有副本都应该先执行A在执行B。通过版本向量、时钟向量来控制。
    • 意图保留:转换后的操作应当保留原始的用户操作的意图。
  • CRDT: 每个客户端可以独立更新本地数据,通过设计良好的数据结构和合并规则,确保所有副本最终一致,无需集中服务器做 transform。

    • 交换律:merge(A, B) = merge(B, A)
    • 结合律:merge(merge(A, B), C) = merge(A, merge(B, C))
    • 幂等律:merge(A, A) = A

26. 苹果审核常见的问题

  1. App 完整性与测试问题 (Guideline 2.1 - App Completeness) 这是最常见的被拒原因。苹果要求提交审核的 App 必须是正式产品,不能是半成品。

崩溃与 Bug: 审核人员在测试核心流程时出现闪退、UI 错乱或网络请求无响应。

缺乏测试资源: 如果 App 需要登录才能使用体验,却未在 App Store Connect 的“审核备注”中提供有效的测试账号、密码,或者后端服务在审核期间宕机。

包含占位符内容: 界面上存在“测试”、“Beta”、“Lorem Ipsum”假文本,或者有未开发完的空页面(如“敬请期待”)。

IPv6 兼容性问题: App 必须在仅支持 IPv6 的网络环境下正常运行。

  1. 重复或垃圾应用 (Guideline 4.3 - Spam) 苹果严厉打击“换壳”应用和同质化严重的 App。如果你是接外包或者公司做矩阵产品,很容易触发这条。

马甲包 / 模板应用: 同一个开发者账号提交了多个功能和 UI 极其相似的 App,或者直接使用了市面上泛滥的模板生成代码。

过度细分类目: 例如本可以整合在一个 App 中的功能,硬被拆分成 10 个不同的 App 提交。

应对建议: 如果触发 4.3,通常需要从底层修改 UI 交互、增加核心差异化功能,或在申诉中详细阐述产品的独特性和具体的受众群体。

  1. 隐私与数据安全 (Guideline 5.1 - Privacy) 近年来苹果对隐私的管控极其严格,这类问题极易触发审核红线。

权限请求说明不明确 (5.1.1): Info.plist 中的权限提示语不能太宽泛。必须具体说明为什么需要以及怎么用(例如:“需要访问相册以便您能选择并上传个人头像”,而不是敷衍的“需要访问相册”)。

缺失账号注销功能 (5.1.1): 极其重要。 只要你的 App 支持创建账号,就必须在 App 内提供明显且能完全删除账号及关联数据的功能,不能仅提供“退出登录”。

强制收集无关数据 (5.1.1): App 不能强制要求与当前核心业务无关的权限才能进入主界面(例如单机工具类 App 强制要求定位权限)。

  1. App 内购买与变现 (Guideline 3.1.1 - In-App Purchase) 涉及资金链路,苹果通常采用零容忍态度。

绕过苹果支付: 如果你的 App 售卖的是数字商品或虚拟服务(如 VIP 会员、虚拟币、线上课程),必须使用苹果的 IAP 系统,且不能在 App 内任何地方(包括描述、公告)出现第三方支付的图标、链接或引导字眼。

缺乏“恢复购买”按钮: 只要包含非消耗型项目或自动续期订阅的 App,必须在购买界面提供显著的“恢复购买(Restore Purchases)”选项。

  1. 准确的元数据 (Guideline 2.3 - Accurate Metadata) “元数据”指的是 App 的名称、副标题、描述、截图和分类。

截图与实际不符 (2.3.3): 商店截图展示了某些功能,但审核人员在 App 实际运行中找不到;或者使用了不对应尺寸机型的截图拉伸充数。

隐藏违规功能 (2.3.1): 描述中隐藏了通过热更新(如 JSPatch、某些 Flutter 动态化方案)下发的非合规业务逻辑代码,一旦被苹果扫出,可能面临封号风险。

提及第三方平台名称 (2.3.7): 在文案、截图或 UI 中出现了竞品名称,或是包含了“Android”、“安卓版”等字眼。

  1. 最低功能要求 (Guideline 4.2 - Minimum Functionality) 苹果希望你的产品具备原生体验的价值。

功能过于单薄: 如果你的 App 只是一个简单的 H5 网页套壳(Web App)、几个纯静态资讯页面的组合,苹果会认为它不具备作为一个 App 的存在意义,并建议你做成网页版。

应对建议: 为应用增加一些原生特性,例如消息推送、本地相册交互、Widget 小组件或 Core Data 本地数据持久化等。

27. 工程化是啥?

软件工程化就是把个人化的、手工式的开发方式,转变为可规模化、可协作、可维护、可复制的标准化流程和体系。

28. CI/CD流中的问题

1. SwiftLint/OCLint 介入阶段

SwiftLint 确实主要基于 AST,它调用 SourceKit(Swift 编译器的前端接口)把源码解析成 AST,然后匹配规则。


OCLint 是基于 LLVM IR + 控制流图(CFG)+ 数据流分析的,它拿到的是经过类型检查、语义分析之后的中间表示,能理解代码的实际执行逻辑。
- 源代码
↓ Clang 前端 → AST
↓ 语义分析
↓ 生成 LLVM IR  ← OCLint 在这一层工作

29. StoreKit V1 和 V2 区别?

StoreKit V2 是 Apple 在 WWDC21 (iOS 15) 推出的重构版本, 和 V1 的核心区别有四点:

  1. API 风格: 从 delegate+回调 变成 async/await
  2. 验证方式: 从 receipt 变成 JWS 签名
  3. 订阅管理: 客户端可以直接获取订阅状态
  4. 测试体验: 支持 Xcode 本地测试,不需要 Sandbox 账号

第一点:API 风格(必讲)

V1 是命令式的,基于 SKPaymentTransactionObserver 回调。 购买流程散落在多个回调方法里,代码碎片化,维护成本高。

V2 是声明式的,一行代码完成购买: let result = try await product.purchase()

不需要注册 Observer,不需要管理回调时机, 整个流程是线性的,可读性大幅提升。

面试官追问细节你就展开:

V1 购买流程: addTransactionObserver (必须在 didFinishLaunching) → add(payment) → paymentQueue:updatedTransactions: 回调 → 在回调里判断 .purchased/.failed/.restored → 获取 receipt 验证 → finishTransaction

V2 购买流程: Product.products(for: ids) → product.purchase() → switch result { case .success(let verification): ... } → transaction.finish()

代码量大概减少 40-50%。


第二点:签名验证(重点讲,体现安全意识)

V1 用 receipt (PKCS#7 格式) 验证:

  • receipt 是整个 App 的购买记录打包在一个文件里
  • 要么本地用 OpenSSL 验证(复杂且容易被破解)
  • 要么发到服务端调 Apple 的 verifyReceipt 接口
  • sandbox 和 production 两套环境,容易搞混
  • receipt 文件可能丢失、过期、格式变更

V2 用 JWS (JSON Web Signature) 验证:

  • 每笔交易是独立的签名 token
  • 客户端用 Apple 根证书直接验证,不需要 receipt 了
  • VerificationResult 枚举,.verified 说明安全,.unverified 说明被篡改
  • 标准格式,服务端任何 JWT 库都能解析

从安全角度看,V2 的 JWS 比 V1 的 receipt 更难伪造, 因为每笔交易独立签名,不像 receipt 是一个大包。

面试官问"JWS 是什么":

JWS 是 JSON Web Signature,格式是 Header.Payload.Signature, 三部分用点号连接,都是 Base64URL 编码。

Header 里声明签名算法和 Apple 的证书链, Payload 里是交易数据(transactionId、productId 等), Signature 是 Apple 用私钥对前两部分的签名。

验证方用 Apple 公开根证书验证签名,通过就说明数据没被篡改。


第三点:交易监听(体现架构能力)

V1 用 Observer 模式:

  • 必须在 App 启动时注册 SKPaymentTransactionObserver
  • 注册晚了可能漏掉交易
  • 回调里要自己管理 transaction 的 finish 时机

V2 用 AsyncSequence:

  • Transaction.updates 是一个异步序列
  • 随时可以开始监听,不依赖注册时机
  • Transaction.unfinished 能获取所有未完成的交易
  • 包括 App 崩溃前没来得及 finish 的交易

第四点:防掉单(核心考察点,一定要主动讲)

V1 的防掉单是行业标准三件套:

  1. Keychain/本地持久化: 先存后买,记录订单状态
  2. 服务端确认: receipt 发服务端验证,确认后才 finishTransaction
  3. 启动恢复: 检查未完成订单 + 和服务端同步权益

V2 的防掉单思路一样,但实现更简单:

  1. 本地持久化: 仍然需要,记录 transactionId 和状态
  2. 服务端确认: 改成发 JWS 给服务端验证
  3. Transaction.unfinished: 替代 V1 的 Observer 重推机制,更可靠

V2 没有解决的: 服务端确认和 finish 之间崩溃的场景, 仍然需要本地记录 + 启动重试。防掉单的核心逻辑没变, V2 只是简化了 API,减少了样板代码。


第五点:V1 到 V2 的迁移(加分项,主动提)

如果 App 要支持 iOS 13+,两套需要共存:

  1. 用 PurchaseService 协议做统一接口层 内部用 if #available(iOS 15.0, *) 分发到 V1 或 V2 实现

  2. 用户从 iOS 14 升级到 iOS 15:

    • V1 未 finish 的交易会出现在 V2 的 Transaction.unfinished 里
    • Apple 保证了 transactionIdentifier 是一致的
    • V2 代码可以读取 V1 写入的 Keychain/数据库(只要 key 一致)
  3. 用户关联:

    • V1 用 applicationUserName 传 userId
    • V2 用 appAccountToken (UUID),存在 Apple 服务器上,不会丢
    • 迁移期用多层级匹配: appAccountToken → 本地DB → 服务端 → 当前用户
  4. 服务端需要同时支持 receipt 验证和 JWS 验证


收尾(30秒总结)

总结来说:

  • V2 用现代 Swift 特性重写了整个购买流程
  • async/await 让代码更清晰
  • JWS 签名让验证更安全
  • 本地测试让开发更高效
  • 但防掉单的架构思路和 V1 一致,该做的不能省

如果是新项目,minTarget 直接 iOS 15+,只用 V2。 如果要兼容老版本,协议层分发 + 共享存储 + 服务端双接口验证。

30. 设计原则、设计模式

设计原则(SOLID)原则:

  • S 单一职责。一个类只有一个理由去改它
  • O 开闭原则。加功能用新增,不用修改
  • L 里氏替换。子类不能比父类"差"
  • I 接口隔离。接口要瘦不要胖
  • D 依赖倒置。面向协议编程
  • DRY 不要重复
  • KISS 能简单别复杂

设计模式(Design Pattern)← 微观,解决具体编码问题(GoF 23种)

  • 创建型:
    • 单例 全局唯一实例。例子:UIApplication/NSNotificationCenter/URLSession
    • 工厂 子类决定创建哪个实例。例子:Image/cell复用/NSData/NSString
    • 抽象工厂 创建一整套相关对象。例子:主题
    • 建造者 分步构建复杂对象。例子:创建请求/级联
    • 原型
  • 结构型:
    • 适配器 旧API适应新接口
    • 装饰器 给UIView添加一些圆角、边框等方法
    • 代理 控制对象访问,UITableView 代理和数据源
    • 桥接
    • 组合 UIView层级天然就是这种模式
    • 外观 调用极简,简化复杂子系统,控制器内实例化拍照子系统,外部只管调用
    • 享元 共享尽可能多的对象 UITableViewCell复用池
  • 行为型:
    • 观察者 一对多通知。例子:通知/KVO/订阅
    • 策略 算法可互换。 例子:支付策略/排序策略/登录策略
    • 命令 请求封装成对象。例子:系统的 UndoManager、NSOperationQueue下载
    • 状态 状态变化改变行为。例子:网络请求状态、根据状态显示view
    • 模板方法 骨架固定,子类重写步骤。例子:UIViewController
    • 迭代器 顺序访问集合。例子:for-in、for-each
    • 中介者 对象不直接通信。例子:组件化
    • 备忘录 保存恢复状态。例子: UIKit 的 UITextView, 撤销 NSUndoManager 保存每次编辑的状态快照
    • 责任链 请求沿链传递。例子:UIKit点击事件传递。
    • 访问者

单例模式优缺点: 优点:全局访问、只创建一次、共享状态 缺点:隐式依赖、难以测试、生命周期不受控、并发问题、滥用导致状态混乱

30. 算法

时间复杂度:代码执行次数随数据量增长的变化趋势。不是算具体跑了多少秒,而是算"数据量翻倍,执行次数翻几倍"

空间复杂度:代码占用额外内存随数据量增长的变化趋势。不是算具体用了多少字节,而是算"数据量翻倍,内存翻几倍"

比较:O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)

O(1) — 常数级(最快,不随数据量增长)

  不管数据多大,执行时间固定

  例子:
    取数组第 i 个元素:array[5]
    存一个变量:x = 10
    Hash表查找:map["key"]
    push/pop 栈顶

  O(log n) — 对数级(非常快)

  数据量翻倍,执行次数只加1

  n = 10        → 约 3 次
  n = 100       → 约 7 次
  n = 1,000     → 约 10 次
  n = 1,000,000 → 约 20 次

  例子:
    二分查找:100万个数最多查 20 次
    平衡二叉树查找

  O(n) — 线性级(和数据量成正比)

  数据量翻倍,时间翻倍

  n = 100    → 100 次
  n = 1000   → 1000 次
  n = 10000  → 10000 次

  例子:
    遍历数组找最大值
    for 循环一遍
    线性查找

  O(n log n) — 线性对数级(比较快的排序)

  比 O(n) 慢一点,但比 O(n²) 快很多

  n = 100    → 约 700 次
  n = 1000   → 约 10000 次
  n = 10000  → 约 130000 次

  例子:
    快速排序(平均)
    归并排序
    堆排序

  O(n²) — 平方级(慢了)

  数据量翻倍,时间翻4倍

  n = 100    → 10,000 次
  n = 1000   → 1,000,000 次
  n = 10000  → 100,000,000 次(1亿次)

  例子:
    冒泡排序、选择排序、插入排序
    双重 for 循环

  O(2ⁿ) — 指数级(很慢,数据稍大就爆炸)

  每多一个数据,时间翻倍

  n = 10  → 1,024
  n = 20  → 1,048,576
  n = 30  → 1,073,741,824(10亿次)
  n = 50  → 基本算不完

  例子:
    暴力破解密码(每位两种可能)
    递归求斐波那契(不记忆化的版本)
    子集枚举

  O(n!) — 阶乘级(最慢)

  n = 5   → 120
  n = 10  → 3,628,800
  n = 15  → 1,307,674,368,000
  n = 20  → 地球上所有计算机一起算也算不完

  例子:
    暴力旅行商问题(试所有路线)
    全排列

其他复习点

一、 iOS 原生底层 (OC/Swift/运行时)

  • [ ] ARC 机制本质:LLVM 编译器自动插入 retain/release,配合 Runtime 运行时的弱引用表(SideTable)管理内存。
  • [ ] weak 底层实现:Runtime 维护了一个 Hash 表(SideTable),以对象内存地址为 Key,指向 weak 指针数组为 Value。对象销毁时清空对应的指针数组并置为 nil。
  • [ ] AutoreleasePool 结构:以双向链表组成的以 AutoreleasePoolPage 为结点的栈结构。
  • [ ] Runloop Mode 切换机制:一个 Runloop 包含多个 Mode,每次只能运行在一个 Mode 下,切换 Mode 必须退出当前 Loop 重新进入。主要用于分离主线程的滚动渲染(TrackingMode)和日常定时器/网络请求(DefaultMode)。
  • [ ] 离屏渲染 (Offscreen Rendering) 成因:由于图层组合(如 cornerRadius+maskToBounds、阴影、遮罩),GPU 无法在当前屏幕缓冲区(On-Screen)一次性渲染完成,必须开辟一块新的独立缓冲区(Off-Screen)进行分步渲染组合。
  • [ ] 多线程数据竞争防御:常规读写使用 GCD 并发队列 + dispatch_barrier_async(读写锁);高频极短操作使用 os_unfair_lock。不推荐 @synchronized(性能差)。
  • [ ] 消息传递与转发流程objc_msgSend 缓存查找 -> 方法列表查找。找不到则走转发链:动态方法解析 (Dynamic Method Resolution) -> 备用接收者 (Fast Forwarding) -> 完整消息转发 (Normal Forwarding)。

二、 Flutter 与 Dart 跨端底层

  • [ ] Dart Event Loop 机制:单线程模型,包含两个队列:Microtask Queue(微任务,优先级高,如 Future.then)和 Event Queue(事件,如 UI、I/O)。
  • [ ] Flutter 三棵树:Widget(不可变的配置信息) -> Element(连接 Widget 和 RenderObject 的实例,管理生命周期) -> RenderObject(负责真正的布局测算和绘制)。
  • [ ] RepaintBoundary 的作用:切断渲染树的重绘链条。当节点频繁刷新时,加这个属性可以让它与其父节点的绘制分离,自身生成独立的 Layer 缓存,避免整棵树重绘。
  • [ ] Riverpod vs GetX 的核心区别:Riverpod 是编译期安全的(Provider 不是全局字符串查找),且强调不可变状态(Immutable);GetX 靠全局单例依赖注入,极其灵活但也容易导致状态追踪困难。
  • [ ] Flutter Boost 混合栈生命周期:原生层维护真实的 ViewControllers/Activities 栈结构,通过 MethodChannel 通知 Flutter 侧的 Navigator 进行压栈出栈,复用同一个 Flutter Engine 以控制内存。

三、 性能治理与工程化

  • [ ] App 冷启动耗时拆解 (Pre-main):dyld 加载 -> Mach-O 依赖库加载 -> ObjC Runtime 注册(类加载、Category 合并) -> +load 方法执行。
  • [ ] 包体积瘦身手段:可执行文件(LinkMap 分析剔除无用类/方法、编译优化设置) + 资源文件(图片转 WebP、剔除冗余多语言和 @2x 资源)。
  • [ ] OOM (Out Of Memory) 核心监控:通过 Jetsam 机制了解 iOS 杀进程原理。监控手段包括获取当前物理内存 Footprint,配合强引用链抓取工具(如 FBRetainCycleDetector)。
  • [ ] 静态代码扫描原理 (OCLint):拦截 xcodebuild 编译命令生成 compile_commands.json,基于 Clang 生成的抽象语法树(AST)进行自定义规则匹配(如圈复杂度、空指针)。

四、 架构、网络与音视频基建

  • [ ] IAP 支付防掉单核心逻辑:交易状态(SKPaymentTransactionState)必须持久化记录;拿到 Receipt 票据后不可立即 finishTransaction,必须等待服务器 S2S(Server-to-Server)校验成功后再 Finish。
  • [ ] MVC 向 MVVM 演进的本质:抽出 ViewController 中庞杂的网络请求、数据处理和格式化逻辑到 ViewModel 中,ViewController 只负责视图绑定和路由跳转,解决 Controller 臃肿问题。
  • [ ] WebSocket vs HTTP:WebSocket 是基于 TCP 的全双工长连接,服务端可主动推送,适用于弹幕/直播;HTTP 是单向请求-响应,需要靠轮询(Polling)模拟实时性。
  • [ ] AVFoundation 核心层级AVAsset (解析媒体源) -> AVPlayerItem (控制播放状态) -> AVPlayer (负责调度器) -> AVPlayerLayer (渲染视频画面)。
  • [ ] OpenGL ES 渲染管线:顶点数据输入 -> 顶点着色器 (处理位置/矩阵变换) -> 图元装配 -> 光栅化 (转为像素) -> 片段着色器 (计算颜色/纹理采样) -> 帧缓冲区。

这版是不是够直接了?哪个点看着眼生或者拿不准,随时说。

基于 VitePress 构建 · 部署于 Cloudflare Pages