自我介绍
你好,我是欧阳林,有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和原生通信方式有哪些?
- MethodChannel(最常用:异步调用)
这是最常见的“逻辑触发”方式,类似于 RPC(远程过程调用)。
机制:双向通信。Flutter 可以调用原生的方法,原生也可以主动调用 Flutter。
特点:
异步执行:调用后会返回一个 Future,不会阻塞 UI 线程。
数据传递:依赖二进制序列化(StandardMessageCodec),支持基础数据类型、List 和 Map。
场景:调用系统摄像头、获取电量、发起原生支付、调用第三方 SDK 接口。- EventChannel(数据流:持续监听)
用于从原生向 Flutter 发送持续不断的数据流。
机制:观察者模式。Flutter 端监听,原生端发送。
特点:
单向流:通常是由原生主动推送给 Flutter。
长连接:一旦建立监听,原生可以持续发送数据,直到取消。
场景:手机传感器数据(加速度计、陀螺仪)、网络状态变化监听、下载进度实时回传。- BasicMessageChannel(基础通信:频繁交换)
用于传递简单的、频繁的数据或字符串。
机制:类似于低级的消息传递,可以自定义编解码器(Codec)。
特点:
支持同步和异步两种模式。
比 MethodChannel 更轻量,适合不需要明确方法名的纯数据交换。
场景:实时同步某些简单的状态参数,或者作为插件底层的通信协议。- Pigeon(现代首选:类型安全方案)
这是 Google 官方推荐的进阶版 MethodChannel,旨在解决原生通信中最头疼的“字符串硬编码”和“类型不安全”问题。
机制:基于 MethodChannel,但引入了代码生成器。
核心优势:
类型安全:不需要再手写 methodName == "getUserInfo" 这种易错的字符串。
自动生成:你只需定义一个接口协议(Dart),Pigeon 会自动生成对应的 iOS (Swift/ObjC) 和 Android (Java/Kotlin) 代码。
场景:中大型项目,接口繁多且对代码质量要求高的场景。- 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];
// 重要:自动munmap20. 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. 音视频流程
- 采集(摄像头/麦克风获取原始YUV/PCM数据)
- 前处理(视频降噪/缩放、音频AEC/ANS处理)
- 编码(H.264/H.265压缩视频,AAC/Opus压缩音频)
- 传输(通过RTMP/RTP/HTTP协议网络传输或存储为MP4/MKV文件)
- 解复用(分离音视频流)
- 解码(还原为YUV/PCM原始帧)
- 同步(基于PTS时间戳以音频为基准调整视频播放节奏)
- 渲染(视频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. 苹果审核常见的问题
- App 完整性与测试问题 (Guideline 2.1 - App Completeness) 这是最常见的被拒原因。苹果要求提交审核的 App 必须是正式产品,不能是半成品。
崩溃与 Bug: 审核人员在测试核心流程时出现闪退、UI 错乱或网络请求无响应。
缺乏测试资源: 如果 App 需要登录才能使用体验,却未在 App Store Connect 的“审核备注”中提供有效的测试账号、密码,或者后端服务在审核期间宕机。
包含占位符内容: 界面上存在“测试”、“Beta”、“Lorem Ipsum”假文本,或者有未开发完的空页面(如“敬请期待”)。
IPv6 兼容性问题: App 必须在仅支持 IPv6 的网络环境下正常运行。
- 重复或垃圾应用 (Guideline 4.3 - Spam) 苹果严厉打击“换壳”应用和同质化严重的 App。如果你是接外包或者公司做矩阵产品,很容易触发这条。
马甲包 / 模板应用: 同一个开发者账号提交了多个功能和 UI 极其相似的 App,或者直接使用了市面上泛滥的模板生成代码。
过度细分类目: 例如本可以整合在一个 App 中的功能,硬被拆分成 10 个不同的 App 提交。
应对建议: 如果触发 4.3,通常需要从底层修改 UI 交互、增加核心差异化功能,或在申诉中详细阐述产品的独特性和具体的受众群体。
- 隐私与数据安全 (Guideline 5.1 - Privacy) 近年来苹果对隐私的管控极其严格,这类问题极易触发审核红线。
权限请求说明不明确 (5.1.1): Info.plist 中的权限提示语不能太宽泛。必须具体说明为什么需要以及怎么用(例如:“需要访问相册以便您能选择并上传个人头像”,而不是敷衍的“需要访问相册”)。
缺失账号注销功能 (5.1.1): 极其重要。 只要你的 App 支持创建账号,就必须在 App 内提供明显且能完全删除账号及关联数据的功能,不能仅提供“退出登录”。
强制收集无关数据 (5.1.1): App 不能强制要求与当前核心业务无关的权限才能进入主界面(例如单机工具类 App 强制要求定位权限)。
- App 内购买与变现 (Guideline 3.1.1 - In-App Purchase) 涉及资金链路,苹果通常采用零容忍态度。
绕过苹果支付: 如果你的 App 售卖的是数字商品或虚拟服务(如 VIP 会员、虚拟币、线上课程),必须使用苹果的 IAP 系统,且不能在 App 内任何地方(包括描述、公告)出现第三方支付的图标、链接或引导字眼。
缺乏“恢复购买”按钮: 只要包含非消耗型项目或自动续期订阅的 App,必须在购买界面提供显著的“恢复购买(Restore Purchases)”选项。
- 准确的元数据 (Guideline 2.3 - Accurate Metadata) “元数据”指的是 App 的名称、副标题、描述、截图和分类。
截图与实际不符 (2.3.3): 商店截图展示了某些功能,但审核人员在 App 实际运行中找不到;或者使用了不对应尺寸机型的截图拉伸充数。
隐藏违规功能 (2.3.1): 描述中隐藏了通过热更新(如 JSPatch、某些 Flutter 动态化方案)下发的非合规业务逻辑代码,一旦被苹果扫出,可能面临封号风险。
提及第三方平台名称 (2.3.7): 在文案、截图或 UI 中出现了竞品名称,或是包含了“Android”、“安卓版”等字眼。
- 最低功能要求 (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 的核心区别有四点:
- API 风格: 从 delegate+回调 变成 async/await
- 验证方式: 从 receipt 变成 JWS 签名
- 订阅管理: 客户端可以直接获取订阅状态
- 测试体验: 支持 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 的防掉单是行业标准三件套:
- Keychain/本地持久化: 先存后买,记录订单状态
- 服务端确认: receipt 发服务端验证,确认后才 finishTransaction
- 启动恢复: 检查未完成订单 + 和服务端同步权益
V2 的防掉单思路一样,但实现更简单:
- 本地持久化: 仍然需要,记录 transactionId 和状态
- 服务端确认: 改成发 JWS 给服务端验证
- Transaction.unfinished: 替代 V1 的 Observer 重推机制,更可靠
V2 没有解决的: 服务端确认和 finish 之间崩溃的场景, 仍然需要本地记录 + 启动重试。防掉单的核心逻辑没变, V2 只是简化了 API,减少了样板代码。
第五点:V1 到 V2 的迁移(加分项,主动提)
如果 App 要支持 iOS 13+,两套需要共存:
用 PurchaseService 协议做统一接口层 内部用 if #available(iOS 15.0, *) 分发到 V1 或 V2 实现
用户从 iOS 14 升级到 iOS 15:
- V1 未 finish 的交易会出现在 V2 的 Transaction.unfinished 里
- Apple 保证了 transactionIdentifier 是一致的
- V2 代码可以读取 V1 写入的 Keychain/数据库(只要 key 一致)
用户关联:
- V1 用 applicationUserName 传 userId
- V2 用 appAccountToken (UUID),存在 Apple 服务器上,不会丢
- 迁移期用多层级匹配: appAccountToken → 本地DB → 服务端 → 当前用户
服务端需要同时支持 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 渲染管线:顶点数据输入 -> 顶点着色器 (处理位置/矩阵变换) -> 图元装配 -> 光栅化 (转为像素) -> 片段着色器 (计算颜色/纹理采样) -> 帧缓冲区。
这版是不是够直接了?哪个点看着眼生或者拿不准,随时说。