面试
收录各技术方向的实用面经,涵盖前端、移动端、后端、AI/Agent 等领域,帮助你高效备战技术面试。
技术方向
- React 面经
- Vue 面经
- Android 面经
- iOS 面经
- 小程序面经
- 后端开发面经
- Agent / AI 面经
Vue 面经
核心考点
1. 响应式原理
- Vue 2:Object.defineProperty 劫持属性 getter/setter;无法检测属性新增/删除,需用 Vue.set
- Vue 3:Proxy 代理整个对象,天然支持新增/删除属性,性能更好
2. Composition API vs Options API
| 对比项 | Options API | Composition API |
|---|---|---|
| 逻辑组织 | 按选项分散 | 按功能聚合 |
| 逻辑复用 | Mixins(易冲突) | Composables(清晰) |
| TypeScript | 较弱 | 完整支持 |
| 适合场景 | 简单组件 | 复杂/大型项目 |
3. Vue 3 重要新特性
- Proxy 响应式:性能提升,支持 Map/Set/Array
- Teleport:将组件渲染到 DOM 指定位置(如 Modal 挂到 body)
- Fragments:组件可有多个根节点
- setup() / script setup:更简洁的组合式写法
- Suspense:异步组件加载的内置支持
4. 生命周期对比
Vue 2 Vue 3 (Composition)
beforeCreate → setup()
created → setup()
beforeMount → onBeforeMount
mounted → onMounted
beforeUpdate → onBeforeUpdate
updated → onUpdated
beforeDestroy → onBeforeUnmount
destroyed → onUnmounted
5. computed vs watch vs watchEffect
- computed:依赖缓存,有返回值,用于派生状态
- watch:明确监听某个源,可获取新旧值,支持异步操作
- watchEffect:自动收集依赖,立即执行,适合副作用同步
6. 高频面试题
- v-model 原理:语法糖,绑定
:modelValue+@update:modelValue - nextTick 原理:异步队列(Promise → MutationObserver → setTimeout),DOM 更新后回调
- keep-alive:缓存组件实例,触发
activated/deactivated生命周期 - diff 算法:Vue3 采用最长递增子序列优化移动操作
- 为什么 data 是函数:防止组件实例间共享同一引用
💡 推荐阅读 Vue 3 官方文档 深入理解设计思路
Vue 3 响应式源码深入
响应式系统源码解析
1. 核心数据结构
// 依赖收集的核心映射
type Dep = Set<ReactiveEffect> // 订阅该属性的 effect 集合
const targetMap = new WeakMap<object, Map<string, Dep>>()
// WeakMap<target, Map<key, Set<effect>>>
// WeakMap 保证 target 被 GC 时自动清理
2. reactive / ref 区别
| reactive | ref | |
|---|---|---|
| 接受类型 | 对象/数组 | 任意值 |
| 访问方式 | 直接访问属性 | .value |
| 实现 | Proxy | getter/setter(对象内部用 reactive) |
| 解构 | 会丢失响应式 | toRefs() 保持响应式 |
3. track / trigger 流程
【读取属性】
get trap → track(target, key)
→ 找到当前 activeEffect
→ targetMap[target][key].add(activeEffect)
【修改属性】
set trap → trigger(target, key)
→ 取出 targetMap[target][key]
→ 遍历执行所有 effect
4. effect / computed / watch 关系
ReactiveEffect是核心类,computed/watch 都基于它- computed:懒执行,有
_dirty标记,依赖变化时只标记脏,访问时才重算 - watch:立即执行 effect,对比新旧值后调用回调
- watchEffect:直接执行 effect,自动收集内部响应式依赖
5. 响应式边界问题
// ❌ 解构丢失响应式
const { count } = reactive({ count: 0 })
// ✅ 用 toRefs
const state = reactive({ count: 0 })
const { count } = toRefs(state)
count.value++ // 仍然响应式
// ❌ 将响应式对象赋值给普通变量
let user = reactive({ name: 'A' })
user = { name: 'B' } // 丢失响应式!
// ✅ 修改属性而非替换对象
user.name = 'B'
6. shallowReactive / readonly / shallowReadonly
- shallowReactive:只代理第一层,深层不响应
- readonly:深层只读,set 时警告
- markRaw:标记对象不被转为响应式(如第三方实例)
7. 高频面试题
- Vue 3 为什么用 Proxy 替换 Object.defineProperty?
- 支持新增/删除属性、数组下标修改
- 懒代理嵌套对象(访问时才代理),性能更好
- 支持 Map/Set/WeakMap/WeakSet
- ref 内部为什么对对象用 reactive? 保持深层响应式
- computed 为什么是懒计算? 避免不被读取时白白计算
- watchEffect 和 watch 的 flush 时机? 默认
pre(DOM 更新前),post在 DOM 更新后
Vue 生态(Pinia / Vue Router / Nuxt)
Vue 生态核心面经
1. Pinia(替代 Vuex)
核心概念
export const useUserStore = defineStore('user', () => {
const name = ref('Alice')
const double = computed(() => name.value.repeat(2))
async function fetchUser(id: string) {
name.value = await api.getUser(id)
}
return { name, double, fetchUser }
})
| 对比 | Vuex 4 | Pinia |
|---|---|---|
| TypeScript | 较弱 | 完整支持 |
| 模块化 | namespace | 每个 store 独立 |
| Mutations | 必须 | 不需要,直接修改 |
| DevTools | 支持 | 支持(更好) |
- $patch:批量修改状态,触发一次订阅
- $subscribe:监听 store 变化(类似 vuex 的 subscribe)
- 插件系统:可扩展 store(如持久化插件 pinia-plugin-persistedstate)
2. Vue Router 4
导航守卫执行顺序
触发导航
→ 全局 beforeEach
→ 路由独享 beforeEnter
→ 组件 beforeRouteUpdate(复用组件)
→ 组件 beforeRouteEnter
→ 全局 afterEach
→ beforeRouteEnter 的 next(vm => ...) 回调
动态路由
router.addRoute({ path: '/admin', component: Admin })
router.removeRoute('admin') // 通过 name 删除
- 路由懒加载:
component: () => import('./views/Home.vue') - 滚动行为:
scrollBehavior(to, from, savedPosition)自定义 - 路由元信息:
meta: { requiresAuth: true }配合守卫做权限控制
3. Nuxt 3 核心
目录结构约定
pages/ → 自动生成路由
components/ → 自动导入组件
composables/ → 自动导入 composables
server/api/ → 服务端 API 路由
plugins/ → 客户端/服务端插件
middleware/ → 路由中间件
数据获取
// useFetch:SSR + CSR 通用,自动序列化
const { data } = await useFetch('/api/users')
// useAsyncData:更灵活的异步数据
const { data } = await useAsyncData('users', () => $fetch('/api/users'))
- Nitro 引擎:Nuxt 3 服务端,支持多部署目标(Node/Vercel/CF Workers)
- Island Architecture:
<NuxtIsland>服务端渲染的交互组件 - SEO:
useHead/useSeoMeta管理 meta 标签
4. VueUse
常用 composables:
useStorage— localStorage/sessionStorage 响应式绑定useFetch— 简单请求封装useIntersectionObserver— 懒加载/埋点useEventListener— 自动清理的事件绑定useVModel— 简化 v-model 实现
5. 高频面试题
- Pinia 如何实现跨 store 通信? 在一个 store 中直接导入并使用另一个 store
- Vue Router history 模式需要服务器配置什么? 所有路径返回
index.html,避免 404 - Nuxt SSR 如何避免服务端/客户端数据不一致(hydration mismatch)? 避免在服务端使用
window/document,用onMounted隔离客户端逻辑 - Vue 项目如何做权限控制? 路由 meta + 全局守卫 + 动态添加路由 + 指令(v-permission)
Vue 大型项目架构实战
Vue 大型项目设计
1. 目录结构设计
src/
api/ # API 层:请求封装、类型定义
modules/
user.ts
product.ts
request.ts # Axios 封装、拦截器
types.ts # 响应类型
assets/ # 静态资源
components/ # 通用组件
base/ # 基础组件(Button、Input、Modal)
business/ # 业务组件
composables/ # 可复用的 composition functions
directives/ # 自定义指令
layouts/ # 布局组件
router/ # 路由配置
modules/ # 按模块分割路由
guards.ts # 导航守卫
stores/ # Pinia stores
modules/
utils/ # 工具函数
views/ # 页面组件(对应路由)
2. 请求层设计
// request.ts:统一 Axios 封装
import axios from 'axios'
import type { AxiosRequestConfig, AxiosResponse } from 'axios'
class Request {
private instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000
})
constructor() {
this.setupInterceptors()
}
private setupInterceptors() {
// 请求拦截:注入 token
this.instance.interceptors.request.use(config => {
const token = useAuthStore().token
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// 响应拦截:统一错误处理
this.instance.interceptors.response.use(
(res: AxiosResponse<ApiResponse>) => {
if (res.data.code !== 0) {
showError(res.data.message)
return Promise.reject(res.data)
}
return res.data.data
},
async error => {
if (error.response?.status === 401) {
await useAuthStore().refreshToken()
return this.instance(error.config) // 重试
}
return Promise.reject(error)
}
)
}
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.instance.get(url, config)
}
post<T>(url: string, data?: unknown): Promise<T> {
return this.instance.post(url, data)
}
}
export const request = new Request()
3. 权限系统设计
// 基于 RBAC 的权限体系
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const permissions = ref<string[]>([])
const roles = ref<string[]>([])
function hasPermission(perm: string) {
return permissions.value.includes(perm)
}
function hasRole(role: string) {
return roles.value.includes(role)
}
return { permissions, roles, hasPermission, hasRole }
})
// composables/usePermission.ts
export function usePermission(permission: string) {
const store = useAuthStore()
return computed(() => store.hasPermission(permission))
}
// 自定义指令 v-permission
app.directive('permission', {
mounted(el, binding) {
const store = useAuthStore()
if (!store.hasPermission(binding.value)) {
el.parentNode?.removeChild(el)
}
}
})
// 路由守卫
router.beforeEach(async (to, from) => {
const store = useAuthStore()
if (to.meta.requiresAuth && !store.isLoggedIn) {
return { name: 'login', query: { redirect: to.fullPath } }
}
if (to.meta.permission && !store.hasPermission(to.meta.permission)) {
return { name: '403' }
}
})
4. 动态路由(按权限加载)
// 登录后根据权限动态注册路由
async function setupDynamicRoutes(permissions: string[]) {
const allRoutes = import.meta.glob('./modules/*.ts', { eager: true })
const accessibleRoutes = filterRoutesByPermission(allRoutes, permissions)
accessibleRoutes.forEach(route => router.addRoute(route))
}
// 刷新页面后路由恢复(动态路由会在刷新后丢失)
router.beforeEach(async (to) => {
const store = useAuthStore()
if (store.isLoggedIn && !store.routesLoaded) {
await store.loadUserRoutes() // 重新加载路由
return to.fullPath // 重新导航到目标路由
}
})
5. 组件通信规范
父子通信:props + emits(类型安全)
跨层级:provide / inject + readonly 包裹
兄弟/跨页:Pinia store
复杂异步:组合 composables
// 类型安全的 provide/inject
const FormContextKey = Symbol() as InjectionKey<FormContext>
// 父组件
provide(FormContextKey, readonly(formContext))
// 子组件
const form = inject(FormContextKey)
if (!form) throw new Error('FormContext not provided')
6. 性能优化实践
// 1. 路由懒加载(分包)
const Dashboard = () => import('./views/Dashboard.vue')
// 2. 组件异步加载(大型业务组件)
const HeavyTable = defineAsyncComponent({
loader: () => import('./HeavyTable.vue'),
loadingComponent: Skeleton,
errorComponent: ErrorView,
delay: 200,
timeout: 3000
})
// 3. 长列表虚拟滚动
import { useVirtualList } from '@vueuse/core'
const { list, containerProps, wrapperProps } = useVirtualList(
items,
{ itemHeight: 50, overscan: 10 }
)
// 4. 计算属性缓存敏感数据
const expensiveData = computed(() => {
return heavyCalculation(rawData.value)
})
// 5. shallowRef 减少深层响应式
const tableData = shallowRef<Row[]>([]) // 不对数组元素建立深层响应
7. 资深面试题
- Vue 大型项目如何解决 TypeScript 类型推导问题?
- Volar 插件提供组件 props 类型推导
defineProps<{ title: string }>()泛型语法直接推导类型- API 响应类型用 Zod 做运行时校验 + 类型推导
- Pinia 如何在 SSR 中避免状态污染?
- 每次请求创建新的 Pinia 实例,不共享服务端状态
useStore(pinia)传入请求绑定的 pinia 实例
- 如何做 Vue 项目的国际化?
- vue-i18n 管理翻译文件,按语言懒加载
- 日期、数字用 Intl API 处理本地化格式
- 配合 CI/CD 自动提取新增翻译 key
Vue 编译器原理深入
模板编译全流程
1. 编译三阶段
模板字符串
-> Parse(词法+语法分析)-> AST(抽象语法树)
-> Transform(优化转换)-> 带 PatchFlag 的 JavaScript AST
-> Codegen(代码生成)-> render 函数字符串
2. Parse 阶段
- 有限状态机解析 HTML:Data -> Tag Open -> Tag Name -> Attribute
- 生成元素节点、文本节点、插值节点
// <div :class="cls">{{ msg }}</div> 解析后
{
type: NodeTypes.ELEMENT,
tag: 'div',
props: [{ name: 'class', exp: { content: 'cls' } }],
children: [{
type: NodeTypes.INTERPOLATION,
content: { type: NodeTypes.SIMPLE_EXPRESSION, content: 'msg' }
}]
}
3. Transform 阶段(核心优化)
静态提升(Static Hoisting)
// 模板:
// <ul>
// <li class="static">固定内容</li>
// <li>{{ item }}</li>
// </ul>
// 静态节点提升到 render 函数外
const _hoisted_1 = createVNode('li', { class: 'static' }, '固定内容')
function render() {
return createVNode('ul', null, [
_hoisted_1, // 直接复用,不重新创建
createVNode('li', null, ctx.item, PatchFlags.TEXT)
])
}
PatchFlag 靶向更新
// 标记节点哪部分是动态的
createVNode('div',
{ class: dynamicClass },
null,
PatchFlags.CLASS // 只有 class 是动态的,diff 时只比较 class
)
常见 PatchFlag 值:
TEXT = 1:动态文本CLASS = 2:动态 classSTYLE = 4:动态 stylePROPS = 8:动态非 class/style 属性CACHED = -1:静态节点,跳过所有 diff
Block Tree
- 每个含动态节点的模板区域是一个 Block
- Block 记录其内部所有动态子节点(dynamicChildren)
- Diff 时只遍历 dynamicChildren,跳过静态节点
v-if/v-for会创建新 Block,因为结构会变化
4. v-if / v-for 编译
// v-if 编译为三元表达式
// <div v-if="show">A</div><div v-else>B</div>
_ctx.show ? createVNode('div', null, 'A') : createVNode('div', null, 'B')
// v-for 编译为 renderList
// <li v-for="item in list" :key="item.id">{{ item.name }}</li>
renderList(_ctx.list, (item) =>
createVNode('li', { key: item.id }, toDisplayString(item.name), 1)
)
// v-model 编译为双向绑定
// <input v-model="val">
createVNode('input', {
value: _ctx.val,
onInput: $event => { _ctx.val = $event.target.value }
})
5. 事件缓存
// <button @click="handleClick">按钮</button>
// 编译后:handler 被缓存,避免每次渲染创建新函数
createVNode('button', {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.handleClick(...args))
})
// 内联 handler 会被包裹后缓存
6. 编译时 vs 运行时
| 版本 | 说明 | Bundle 大小 |
|---|---|---|
vue.runtime.esm | 仅运行时,无编译器 | 小(推荐,Vite/Webpack 用) |
vue.esm-bundler | 运行时 + 编译器 | 大(CDN 直接使用时) |
- 使用构建工具时:模板在构建阶段编译,运行时不需要编译器
- 直接
<script src>时:需要运行时编译器
7. 资深面试题
- Vue 3 编译优化为什么比 Vue 2 快?
- 静态提升:不变的节点不重复创建
- PatchFlag:diff 只比较变化的部分
- Block Tree:O(全树) -> O(动态节点数)
- 事件缓存:handler 不每次重新创建
- 为什么 v-if 和 v-for 不能同时使用?
- Vue 3 中 v-if 优先级高于 v-for,v-if 内无法访问 v-for 的变量
- 解决:外层包一个
<template v-for>或用 computed 过滤数据
- 自定义指令如何在编译层面优化?
- 通过
directiveTransforms在 Transform 阶段处理 - 可以将部分指令逻辑转为静态属性,避免运行时指令调用开销
- 通过
- 模板中
key的作用是什么?编译层面如何处理?- 用于标识节点身份,相同 key 的节点会尝试复用
- 编译时 key 被提取为 VNode 的第二个参数
v-for中动态 key 会生成 KEYED_FRAGMENT,告知 diff 算法使用带 key 的策略
Vue 性能优化深度实战
Vue 性能优化完整指南
1. 组件渲染优化
v-once 与 v-memo
<!-- v-once:只渲染一次,永不更新 -->
<ExpensiveComponent v-once />
<!-- v-memo:依赖的值不变则跳过重渲染 -->
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
{{ item.name }}
</div>
<!-- 组合使用:大列表时非常有效 -->
<RecycleScroller v-memo="[item.id, item.selected]">
<ExpensiveListItem :item="item" />
</RecycleScroller>
计算属性 vs 方法
<script setup>
// computed: 只在依赖变化时重计算,结果被缓存
const sortedList = computed(() =>
[...props.list].sort((a, b) => a.name.localeCompare(b.name))
)
// method: 每次渲染都执行
const getSorted = () => [...props.list].sort(...)
</script>
shallowRef / shallowReactive
// 大列表用 shallowRef 避免深层响应式的开销
const bigList = shallowRef([])
// 更新时需触发更新
bigList.value = [...bigList.value, newItem]
// 威尔用 markRaw 标记不需要响应式的对象
const map = markRaw(new Map())
2. 列表渲染优化
虚拟滚动(vue-virtual-scroller)
<template>
<RecycleScroller
class="scroller"
:items="list"
:item-size="60"
key-field="id"
>
<template #default="{ item }">
<ListItem :item="item" />
</template>
</RecycleScroller>
</template>
首屏渲染优化
<!-- 分批渲染大列表,首屏先展示 -->
<script setup>
const visibleCount = ref(50)
const visibleList = computed(() => list.value.slice(0, visibleCount.value))
onMounted(() => {
// 首屏渲染完成后再加载其余
nextTick(() => {
setTimeout(() => {
visibleCount.value = list.value.length
}, 300)
})
})
</script>
3. 路由与分包优化
// 路由懒加载 + 马鼓向加载
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue'), // 分包
children: [
{
path: 'analytics',
component: () => import('./views/Analytics.vue')
}
]
}
]
// 预加载:导航预获取数据
router.beforeResolve(async to => {
if (to.meta.prefetch) {
await prefetchData(to.meta.prefetch)
}
})
分包命名
// webpackChunkName 命名 chunk
const UserProfile = () =>
import(/* webpackChunkName: "user" */ './views/UserProfile.vue')
// Vite 自动分包
// 所有动态导入自动分包
4. 缓存策略
keep-alive 深入
<template>
<!-- 路由级缓存 -->
<router-view v-slot="{ Component, route }">
<transition name="fade">
<keep-alive :include="cachedViews" :max="10">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</transition>
</router-view>
</template>
<script setup>
// 按需加入缓存列表
const cachedViews = ref(['Dashboard', 'UserList'])
// 切换路由时动态添加
router.afterEach((to) => {
if (to.meta.keepAlive) {
cachedViews.value.push(to.name)
}
})
</script>
keep-alive 的生命周期鋈子
<script setup>
// 组件被激活(从缓存中自展示)
onActivated(() => {
fetchLatestData() // 重新获取数据
})
// 组件被缓存
onDeactivated(() => {
cleanupResources() // 清理定时器、套接字等
})
</script>
5. 网络请求优化
// 并行请求多个接口
const [user, config, permissions] = await Promise.all([
fetchUser(),
fetchConfig(),
fetchPermissions()
])
// 请求去重:将相同请求合并
const pendingRequests = new Map()
function deduplicatedFetch(url: string) {
if (pendingRequests.has(url)) {
return pendingRequests.get(url)
}
const promise = fetch(url).finally(() => {
pendingRequests.delete(url)
})
pendingRequests.set(url, promise)
return promise
}
// 请求预加载:Intersection Observer 视口内就加载
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
prefetchNextPage()
}
})
})
observer.observe(bottomRef.value)
6. 资深面试题
- Vue 3 为什么比 Vue 2 快?
- 编译器优化:静态提升 + PatchFlag + Block Tree
- Proxy 响应式:懒代理,访问时才建立响应式
- Tree-shaking 友好:只包含用到的模块
- v-for 的 key 如何影响性能?
- key 不变:全量更新,运行全量 diff
- key 稳定:精确匹配节点,只更新变化的部分
- key = index:列表尾部操作时没有差异,中间插入错误
- nextTick 的使用场景?
- 修改数据后,需访问更新后的 DOM
- 测如分页切换后的滚动位置重置
- 在 created 中操作 DOM(DOM 还未挂载)
- 如何定位 Vue 应用中的内存泄漏?
- 监听器在 beforeUnmount 中取消
- 定时器、EventBus、WebSocket 在生命周期结束时清理
- Vue DevTools 测量组件数量变化
- 浏览器 Memory Profiler 截图对比
Android 面经
核心考点
1. Activity & Fragment 生命周期
Activity: onCreate → onStart → onResume → [运行] → onPause → onStop → onDestroy
Fragment: onAttach → onCreate → onCreateView → onViewCreated → onStart → onResume
→ onPause → onStop → onDestroyView → onDestroy → onDetach
- A 跳 B:A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop
- 按 Back 返回:B.onPause → A.onRestart → A.onStart → A.onResume → B.onStop → B.onDestroy
2. 内存泄漏常见场景
- 静态变量持有 Activity 引用
- 非静态内部类(Handler/AsyncTask)持有外部类引用
- 未注销的监听器、广播、回调
- 资源(Cursor、Stream、Bitmap)未及时释放
- 解决:使用 WeakReference、在 onDestroy 中解绑
3. Handler / Looper / MessageQueue
- Looper 在线程中维护 MessageQueue,loop() 死循环取消息
- Handler 发送/处理消息,绑定到创建它的线程 Looper
- 主线程 Looper 由系统启动时初始化(ActivityThread.main)
4. 协程 (Kotlin Coroutines)
- 轻量级线程,挂起不阻塞线程
Dispatchers.Main / IO / Default:分别用于 UI、IO 密集、CPU 密集viewModelScope / lifecycleScope:自动跟随生命周期取消Flow:冷流,适合异步数据流;StateFlow/SharedFlow:热流
5. Jetpack 核心组件
| 组件 | 作用 |
|---|---|
| ViewModel | 跨配置变化保留数据 |
| LiveData | 生命周期感知的可观察数据 |
| Room | SQLite ORM |
| Navigation | Fragment 导航管理 |
| WorkManager | 后台任务调度 |
| Compose | 声明式 UI 框架 |
6. 性能优化
- ANR 触发:主线程 5s 无响应(耗时操作放子线程)
- 过度绘制:减少层级,避免不必要背景,开启 GPU 调试
- 启动优化:懒加载、异步初始化、减少冷启动耗时
- 内存优化:Bitmap 压缩复用,避免内存抖动
7. 高频面试题
- 进程间通信 (IPC):AIDL、Messenger、ContentProvider、BroadcastReceiver
- 四大组件:Activity、Service、BroadcastReceiver、ContentProvider
- View 绘制流程:measure → layout → draw
- RecyclerView vs ListView:RecyclerView 解耦更彻底,支持多种布局、动画、局部刷新
Android 性能优化专题
性能优化全攻略
1. 渲染性能
过度绘制(Overdraw)
- 开启:开发者选项 → 调试 GPU 过度绘制
- 蓝色=1x,绿色=2x,粉色=3x,红色=4x+(红色区域需优化)
- 方案:移除不必要的背景色,减少布局层级,使用
clipRect限制绘制区域
布局优化
- 使用
ConstraintLayout替代嵌套 LinearLayout,减少层级 include/merge/ViewStub复用和延迟加载布局- 使用
Hierarchy Viewer/Layout Inspector分析布局层级
列表优化
- RecyclerView:合理设置
ViewHolder复用 DiffUtil.Callback精准局部刷新,避免notifyDataSetChangedsetHasFixedSize(true):item 大小固定时跳过 requestLayout- 预取:
RecyclerView.setItemViewCacheSize+RecycledViewPool
2. 启动优化
启动类型
- 冷启动:进程从零开始(最慢,重点优化)
- 温启动:进程存在,Activity 需重建
- 热启动:Activity 在后台,直接前台
冷启动优化
- Application
onCreate中异步初始化三方库(WorkManager / 协程) - 减少主线程耗时操作(SP 改 MMKV,IO 操作移至子线程)
- 使用 SplashScreen API(Android 12+)
- 启动页
windowBackground设为占位图,消除白屏
测量工具
adb shell am start -W com.example/.MainActivity
# ThisTime: 此次 Activity 启动时间
# TotalTime: 整个启动过程时间
3. 内存优化
- Bitmap 优化:
inSampleSize压缩,RGB_565替代ARGB_8888(省一半内存) - 内存泄漏检测:LeakCanary 自动检测,
adb shell dumpsys meminfo分析 - 内存抖动:避免在
onDraw/循环中创建对象,使用对象池 - LruCache:内存缓存图片,基于最近最少使用淘汰
4. 电量与网络优化
- Doze 模式:系统限制后台网络/CPU,使用 WorkManager 适配
- 批量网络请求:合并请求减少无线电激活次数
- 图片格式:WebP 替代 PNG/JPEG,体积减少约 30%
- HTTP/2:多路复用减少连接建立开销
- OkHttp 缓存:离线可用 + 减少重复请求
5. ANR 排查
# 查看 ANR 日志
adb pull /data/anr/traces.txt
# 常见原因
# 1. 主线程 IO(网络/磁盘)
# 2. 主线程等待锁
# 3. Binder 调用耗时(跨进程)
# 4. 主线程处理大量 Message
- StrictMode 开发期检测主线程 IO
- Systrace / Perfetto 分析线程状态和方法耗时
6. 高频面试题
- 如何检测内存泄漏? LeakCanary + MAT(Memory Analyzer Tool)分析 heap dump
- 卡顿原理是什么? 主线程一帧(16.6ms)内未完成 measure/layout/draw
- 为什么不能在子线程更新 UI? ViewRootImpl 检查线程,invalidate 触发检查
- Bitmap 如何高效加载大图? BitmapFactory.Options.inSampleSize 按比例采样
Android 架构模式(MVC / MVP / MVVM / MVI)
Android 架构演进
1. MVC
Model ← Controller → View
↑ ↓
└───────────────┘
- Android 中 Activity 既是 Controller 又是 View,导致职责混乱
- 随业务增长 Activity 代码膨胀,难以测试
2. MVP
View (Activity/Fragment)
↕ 接口
Presenter (业务逻辑)
↕
Model (数据层)
- View 和 Presenter 通过接口解耦,便于单元测试
- 缺点:接口类爆炸,Presenter 持有 View 引用需注意内存泄漏
3. MVVM(Google 官方推荐)
View (Activity/Fragment)
↓ 观察
ViewModel (持有 LiveData/StateFlow)
↕
Repository
↕
Remote DataSource / Local DataSource
ViewModel 核心优势
- 生命周期感知:屏幕旋转不丢失数据
- 不持有 View 引用,无内存泄漏风险
SavedStateHandle:进程被杀后恢复状态
class UserViewModel(private val repo: UserRepository) : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadUser(id: String) {
viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
val user = repo.getUser(id)
_uiState.update { it.copy(user = user, loading = false) }
}
}
}
4. MVI(单向数据流)
User Action (Intent)
↓
ViewModel 处理 → 新 State
↓
View 渲染
- State:UI 的完整快照,单一数据源
- Intent:用户操作,密封类描述所有可能事件
- 单向数据流:状态不可变,每次更新生成新 State
- 优点:状态可预测、易于调试和测试
- 代表库:Orbit MVI、MVI Kotlin
sealed class UserIntent {
data class LoadUser(val id: String) : UserIntent()
object Logout : UserIntent()
}
data class UserState(
val user: User? = null,
val loading: Boolean = false,
val error: String? = null
)
5. Clean Architecture 分层
Presentation Layer (ViewModel + UI)
↓
Domain Layer (UseCase + Entity) ← 纯 Kotlin,无 Android 依赖
↓
Data Layer (Repository + DataSource)
- UseCase(交互器):封装单一业务规则,可单独测试
- Repository:抽象数据来源,协调本地缓存和远端数据
- 依赖规则:内层不依赖外层
6. Hilt 依赖注入
@HiltAndroidApp class App : Application()
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModel: UserViewModel
}
@Module @InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun provideRetrofit(): Retrofit = Retrofit.Builder()...
}
7. 高频面试题
- ViewModel 为什么能在屏幕旋转后存活? 存储在
ViewModelStore中,与 Activity 生命周期解绑 - LiveData 和 StateFlow 如何选择? 新项目推荐 StateFlow(更强大,支持初始值,协程原生)
- Repository 的作用? 单一数据源,屏蔽本地/远端差异,统一数据格式
- UseCase 一定需要吗? 简单场景可省略,复杂业务规则建议抽取便于测试
Jetpack Compose 深入面经
Jetpack Compose 核心知识
1. 重组原理
什么时候会重组?
- 状态变化时,依赖该状态的 Composable 会重组
- Compose 通过水平比较(Structural Equality)决定是否跳过重组
如何避免不必要的重组
// 1. @Stable / @Immutable 标记类,告知 Compose 安全跳过
@Stable // 所有 public property 变化时都通知 Compose
data class User(val name: String, val age: Int)
// 2. remember 缓存计算结果
val sortedList = remember(list) { list.sorted() }
// 3. derivedStateOf 对 state 派生新 state
val isScrolled = remember { derivedStateOf { scrollState.value > 0 } }
// 4. key() 给列表项设置稳定标识
Column {
items.forEach { item ->
key(item.id) { // 避免内容相同时不必要的重组
ItemComposable(item)
}
}
}
2. 状态管理
状态提升(State Hoisting)
// 非受控组件(自己持有状态)
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) { Text("$count") }
}
// 受控组件(状态提升到调用者)
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) { Text("$count") }
}
// 调用者
@Composable
fun ParentScreen() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
ViewModel 与 Compose 集成
@Composable
fun OrderScreen(viewModel: OrderViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is Event.NavigateToSuccess -> navController.navigate("success")
is Event.ShowError -> showSnackbar(event.msg)
}
}
}
when (uiState) {
is Loading -> CircularProgressIndicator()
is Success -> OrderContent((uiState as Success).order)
is Error -> ErrorView((uiState as Error).message)
}
}
3. 布局与修饰器
// Modifier 顺序很重要!
Box(
Modifier
.size(100.dp) // 先设大小
.background(Color.Blue) // 再设背景(背景在内容区域内)
.padding(8.dp) // 再设内边距
.clickable { } // 点击区域包含 padding
)
// Modifier 和 UI 组件顺序的弱和强规则
// padding -> background 让内边距内没有背景
Text(
text = "Hello",
modifier = Modifier
.background(Color.Red) // 背景包含 padding 内容
.padding(16.dp)
)
自定义 Layout
@Composable
fun MyCustomLayout(
modifier: Modifier = Modifier,
content: @Composable () -> Unit
) {
Layout(content = content, modifier = modifier) { measurables, constraints ->
val placeables = measurables.map { it.measure(constraints) }
val width = placeables.maxOf { it.width }
val height = placeables.sumOf { it.height }
layout(width, height) {
var y = 0
placeables.forEach {
it.placeRelative(0, y)
y += it.height
}
}
}
}
4. 动画系统
// 状态动画
val alpha by animateFloatAsState(
targetValue = if (visible) 1f else 0f,
animationSpec = tween(durationMillis = 300)
)
// 可见性动画
AnimatedVisibility(
visible = visible,
enter = fadeIn() + slideInVertically(),
exit = fadeOut() + slideOutVertically()
) {
MyContent()
}
// 转场动画
AnimatedContent(
targetState = currentPage,
transitionSpec = { fadeIn() togetherWith fadeOut() }
) { page ->
PageContent(page)
}
// 共享元素过渡
// Screen A
Box(Modifier.sharedElement(rememberSharedContentState("image"), ...) ) {
Image(...)
}
// Screen B 中同样的 sharedElement key 就会自动过渡
5. 性能优化
// 1. 长列表用 LazyColumn
LazyColumn {
items(items = list, key = { it.id }) { item -> // key 必须提供
ItemCard(item)
}
}
// 2. 避免在 composable 中读取 MutableList
// bad: list.forEach { ... } -> 无法追踪变化
// good: val items by rememberUpdatedState(newItems)
// 3. 使用 Baseline Profiles 提升启动性能
// 在 macrobenchmark 中定义核心路径,编译器预编译这些类
// 4. 不要在 Composable 中做老不必要的异步工作
// bad: 在 composable 中直接 launch
// good: 使用 LaunchedEffect(key) { ... }
// 5. 提副本加载验证
CompositionLocal
6. Compose 中的副作用
// LaunchedEffect:进入居和成立时执行,key 变化时重新执行
LaunchedEffect(userId) {
viewModel.loadUser(userId) // 副作用都在协程中执行
}
// SideEffect:每次重组成功后执行,不可以挂起
SideEffect {
analytics.reportScreen("OrderScreen") // 同步,安全访问外部系统
}
// DisposableEffect:组件离开时需清理
DisposableEffect(sensor) {
sensor.start()
onDispose { sensor.stop() } // 离开成居时执行
}
// rememberCoroutineScope:获取协程作用域,用于事件回调中
val scope = rememberCoroutineScope()
Button(onClick = { scope.launch { doSomething() } }) { ... }
7. 资深面试题
- Compose 与传统 View 的格局算法比较?
- 传统 View:Measure(36项 spec) -> Layout -> Draw,可多次 Measure
- Compose:单次 Measure(展开 Composable) -> 分配大小 -> 放置 -> 绘制
- Compose 由设计上保证单次 Measure,性能更可预测
- remember 在配置变化后是否会清除?
- 默认会清除,用
rememberSaveable写入 Bundle 保持状态 - 复杂对象用
Saver接口自定义序列化
- 默认会清除,用
- derivedStateOf 和 remember { ... } 的区别?
remember缓存结果,依赖的 key 变化时重新计算derivedStateOf用于导出 state,内部 state 变化时才重算,避免不必要重组derivedStateOf应包在remember { }里
- 如何在 Compose 中使用传统 View?
AndroidView { context -> CustomView(context).apply { ... } }AndroidViewBinding使用 ViewBindingsetViewCompositionStrategy管理成居策略
Kotlin 协程与 Flow 深入面经
Kotlin 协程深水区
1. 协程底层原理
CPS 变换(Continuation Passing Style)
- Kotlin 编译器将 suspend 函数变换为状态机
- 每个 suspend 点都是一个状态,函数被切分成多段
// 原始代码
suspend fun fetchUser(id: String): User {
val token = getToken() // suspend 点 1
val user = api.get(id) // suspend 点 2
return user
}
// 编译后(简化伪代码)
fun fetchUser(id: String, continuation: Continuation<User>) {
when (continuation.label) {
0 -> {
continuation.label = 1
getToken(continuation) // 挂起等待
}
1 -> {
val token = continuation.result as String
continuation.label = 2
api.get(id, continuation)
}
2 -> {
continuation.resume(continuation.result as User)
}
}
}
挂起与恢复
- suspend 函数调用后,当前线程不阻塞,返回
COROUTINE_SUSPENDED标记 - IO 完成后,通过
continuation.resume(result)恢复执行 - 恢复时可能在不同线程(取决于 Dispatcher)
2. Flow 深入
冷流 vs 热流
// 冷流:collect 时才执行,每个 collector 独立
val coldFlow = flow {
println("开始生产")
emit(1)
emit(2)
}
// 没有 collect 时,内部代码不执行
// StateFlow:热流,始终有最新值
val stateFlow = MutableStateFlow(0)
// SharedFlow:热流,replay 控制新订阅者能收到的历史消息数
val sharedFlow = MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 64)
背压处理
// buffer:生产者和消费者并发运行
flow { emit(heavyData()) }
.buffer(Channel.UNLIMITED)
.collect { process(it) }
// conflate:只保留最新值,丢弃中间值(适合 UI 状态)
stateFlow.conflate().collect { render(it) }
// collectLatest:新值到来时取消旧的处理
stateFlow.collectLatest { slowRender(it) } // 只完成最新一次
channelFlow 并发发射
// 允许在不同协程中 emit
val result = channelFlow {
launch { send(fetchFromNetwork()) } // 并发
launch { send(fetchFromCache()) } // 并发
}
3. 异常处理
// SupervisorJob:子协程独立失败
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
scope.launch { failingTask() } // 失败不影响其他子协程
scope.launch { normalTask() } // 继续正常运行
// CoroutineExceptionHandler
val handler = CoroutineExceptionHandler { _, e ->
Log.e("TAG", "Uncaught exception", e)
}
scope.launch(handler) { riskyWork() }
// async 的异常在 await 时抛出
val deferred = async { riskyTask() }
try {
val result = deferred.await()
} catch (e: Exception) {
// 处理异常
}
// Flow 的异常处理
flow { emit(riskyOperation()) }
.catch { e -> emit(defaultValue) } // 只捕获 catch 上游异常
.collect { }
4. Dispatcher 选择
| Dispatcher | 线程池 | 适用场景 |
|---|---|---|
| Main | 主线程 | UI 操作、轻量任务 |
| IO | 64 个线程 | 网络、文件读写 |
| Default | CPU 核数 | 计算密集 |
| Unconfined | 不指定 | 测试用 |
// limitedParallelism 限制并发度
val limitedIO = Dispatchers.IO.limitedParallelism(4)
withContext(limitedIO) { /* 最多 4 个线程并发执行 */ }
5. 结构化并发
viewModelScope.launch {
// 并发执行两个请求
val userDeferred = async { fetchUser() }
val postsDeferred = async { fetchPosts() }
// 任一失败,另一个自动取消
val user = userDeferred.await()
val posts = postsDeferred.await()
_uiState.value = Success(user, posts)
}
// coroutineScope:子协程全部完成才返回
suspend fun loadData() = coroutineScope {
val a = async { taskA() }
val b = async { taskB() }
Pair(a.await(), b.await()) // 任一失败都会取消其他
}
6. ViewModel 中的实践
class OrderViewModel(
private val repo: OrderRepository,
savedState: SavedStateHandle
) : ViewModel() {
private val orderId = savedState.get<String>("orderId")!!
// StateFlow 替代 LiveData
private val _uiState = MutableStateFlow<UiState>(Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
// 一次性事件用 SharedFlow
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
init {
loadOrder()
}
private fun loadOrder() = viewModelScope.launch {
_uiState.value = Loading
try {
val order = repo.getOrder(orderId)
_uiState.value = Success(order)
} catch (e: Exception) {
_uiState.value = Error(e.message)
}
}
fun submitOrder() = viewModelScope.launch {
try {
repo.submit(orderId)
_events.emit(Event.NavigateToSuccess)
} catch (e: Exception) {
_events.emit(Event.ShowError(e.message))
}
}
}
7. 资深面试题
- suspend 函数为什么不能在普通函数中调用?
- 编译后带 Continuation 参数,需要协程上下文提供该对象
- 景默认解决:用
runBlocking桥接(但会阻塞线程,仅用于测试)
- StateFlow vs LiveData 如何选择?
- 新项目一律推荐 StateFlow:要求初始值、协程原生、功能更强完整
- LiveData 主要优势是自动处理生命周期,StateFlow 必须配合
repeatOnLifecycle
- Flow 的 catch 操作符为什么只捕上游异常?
- Flow 是流式数据,cat操作符在管道中,只能捕获它上游的异常
- 下游(collect 中)的异常不会被捕获,需单独处理
- 协程和线程的核心区别?
- 协程是语言级的轻量调度单元,运行在线程之上
- 一个线程可运行数千个协程
- 协程切换无需内核介入,开销远小于线程切换
后端开发面经
核心考点
1. 数据库
索引
- B+ 树索引:叶节点存数据,范围查询高效
- 联合索引遵循「最左前缀」原则
- 覆盖索引:查询字段全在索引中,避免回表
- 避免索引失效:函数操作、隐式类型转换、
%开头的 LIKE
事务 ACID
- Atomicity(原子性)、Consistency(一致性)、Isolation(隔离性)、Durability(持久性)
- 隔离级别:读未提交 → 读已提交 → 可重复读(MySQL 默认)→ 串行化
- MVCC:通过 undo log + Read View 实现无锁并发读
2. Redis
- 数据类型:String、Hash、List、Set、ZSet(跳表)、Stream
- 持久化:RDB(快照,恢复快)vs AOF(追加日志,数据更完整)
- 缓存问题:
- 穿透:查不存在的 key → 布隆过滤器 / 缓存空值
- 击穿:热点 key 过期 → 互斥锁 / 永不过期
- 雪崩:大量 key 同时过期 → 随机 TTL / 熔断
- 分布式锁:SET key value NX EX 或 Redlock
3. 分布式系统
- CAP 定理:一致性、可用性、分区容忍性,三选二
- BASE 理论:基本可用、软状态、最终一致性
- 分布式事务:2PC、TCC、Saga、消息最终一致性
- 限流算法:令牌桶、漏桶、固定窗口、滑动窗口
4. 消息队列
| 对比 | Kafka | RabbitMQ | RocketMQ |
|---|---|---|---|
| 吞吐量 | 极高 | 中等 | 高 |
| 顺序消息 | 分区内有序 | 不保证 | 支持 |
| 延迟消息 | 不支持 | 插件支持 | 原生支持 |
| 适合场景 | 日志、大数据 | 异步解耦 | 电商、金融 |
5. 微服务
- 服务发现:Consul、Nacos、Eureka
- API 网关:认证、限流、路由、聚合(Kong、APISIX)
- 熔断降级:Hystrix / Sentinel,防止级联故障
- 链路追踪:Jaeger、Zipkin、SkyWalking
6. 高频面试题
- TCP 三次握手 / 四次挥手:建立/关闭连接,TIME_WAIT 的作用
- HTTP vs HTTPS:HTTPS = HTTP + TLS,证书验证、对称/非对称加密
- JWT 原理:Header.Payload.Signature,无状态认证,注意续期和吊销
- 数据库分库分表:垂直拆分(按业务)vs 水平拆分(按数据量),ShardingSphere
- 接口幂等性:唯一请求ID、数据库唯一约束、状态机
MySQL 深入面经
MySQL 核心知识
1. 存储引擎
| 特性 | InnoDB | MyISAM |
|---|---|---|
| 事务 | ✅ | ❌ |
| 外键 | ✅ | ❌ |
| 行级锁 | ✅ | ❌(表锁) |
| 崩溃恢复 | ✅ | ❌ |
| 全文索引 | ✅(5.6+) | ✅ |
| 适用场景 | OLTP | 读多写少 |
2. 索引深入
B+ 树结构
- 非叶子节点只存 key,叶子节点存 key + data
- 叶子节点通过链表相连,范围查询只需遍历链表
- 树高度通常 2-4 层,亿级数据 IO 次数少
聚簇索引 vs 二级索引
- 聚簇索引(主键):叶子节点直接存完整行数据
- 二级索引:叶子节点存主键值 → 需回表查完整数据
- 覆盖索引:查询字段全在索引中,无需回表
索引失效场景
-- ❌ 函数操作
WHERE YEAR(create_time) = 2024
-- ✅ 改为范围查询
WHERE create_time BETWEEN '2024-01-01' AND '2024-12-31'
-- ❌ 隐式类型转换(字段为 varchar,值为数字)
WHERE phone = 13800138000
-- ✅
WHERE phone = '13800138000'
-- ❌ 联合索引 (a, b, c),跳过了 b
WHERE a = 1 AND c = 3
3. 事务与锁
MVCC 实现细节
- 每行数据有隐藏字段:
trx_id(最后修改的事务ID)、roll_pointer(指向 undo log) - Read View:包含活跃事务列表,决定当前事务能看哪个版本
- 可重复读:事务开始时创建 Read View,整个事务期间复用
- 读已提交:每次 SELECT 创建新 Read View
锁类型
共享锁(S):SELECT ... LOCK IN SHARE MODE
排他锁(X):SELECT ... FOR UPDATE / INSERT / UPDATE / DELETE
意向锁(IS/IX):表级,InnoDB 自动加,表示行锁存在
间隙锁(Gap Lock):锁定索引间隙,防止幻读(可重复读级别)
临键锁(Next-Key Lock)= 行锁 + 间隙锁
死锁
- 两个事务互相等待对方持有的锁
- InnoDB 自动检测,回滚代价小的事务
- 预防:固定加锁顺序、减少锁范围、缩短事务
4. EXPLAIN 分析
EXPLAIN SELECT * FROM orders WHERE user_id = 1 AND status = 'paid';
| 字段 | 关注点 |
|---|---|
| type | system > const > ref > range > index > ALL(ALL 是全表扫描,需优化) |
| key | 实际使用的索引 |
| rows | 预计扫描行数(越小越好) |
| Extra | Using filesort(文件排序,需优化)/ Using temporary / Using index(覆盖索引,好) |
5. 主从复制与高可用
复制原理
Master binlog → Slave IO Thread → relay log → Slave SQL Thread → 执行
- binlog 格式:STATEMENT(可能不一致)/ ROW(准确)/ MIXED
- 半同步复制:至少一个从库收到 binlog 再提交,保证数据不丢
- MGR(MySQL Group Replication):多主复制,Paxos 协议保证一致性
6. 高频面试题
- 为什么推荐自增主键? 顺序写入 B+ 树,避免页分裂,减少碎片
- 大表如何 DDL 变更? pt-online-schema-change / gh-ost,在线变更不锁表
- 慢查询如何定位? 开启 slow_query_log + EXPLAIN 分析 + 优化索引
- count(*) 和 count(1) 有区别吗? InnoDB 中几乎没有区别,都走最小索引全扫描
- 为什么 NOT IN 会导致索引失效? 含 NULL 值时 NOT IN 结果不确定,可改用 NOT EXISTS
分布式系统设计面经
分布式核心知识
1. 分布式 ID 生成
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库自增 | 简单有序 | 单点,性能差 |
| UUID | 无需协调 | 无序,存储空间大 |
| 雪花算法(Snowflake) | 高性能、趋势递增 | 依赖时钟,时钟回拨有风险 |
| 号段模式(Leaf) | DB 友好 | 需 DB,有号段浪费 |
| Redis INCR | 简单 | Redis 持久化有风险 |
雪花算法结构(64 bit)
1 bit (符号位=0) | 41 bit (时间戳ms) | 10 bit (机器ID) | 12 bit (序列号)
- 可用约 69 年,单机每毫秒 4096 个 ID
2. 分布式锁
Redis 实现
-- 加锁
SET lock_key unique_value NX EX 30
-- 释放锁(Lua 脚本保证原子性)
if redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1])
end
return 0
- 锁续期(看门狗):Redisson 默认 30s 过期,持锁期间每 10s 续期
- Redlock:向 N 个独立 Redis 实例加锁,超过半数成功则加锁成功
ZooKeeper 实现
- 创建临时有序节点,序号最小的获得锁
- 监听前一个节点删除事件,避免羊群效应
3. 分布式事务
2PC(两阶段提交)
阶段1:协调者发 Prepare → 各参与者执行事务(不提交)并返回 Yes/No
阶段2:全部 Yes → Commit;任一 No → Rollback
- 问题:协调者单点,阻塞协议,网络分区时数据不一致
TCC(Try-Confirm-Cancel)
- Try:预占资源(冻结库存)
- Confirm:确认提交(扣减库存)
- Cancel:取消回滚(解冻库存)
- 业务侵入性强,需实现三个接口
Saga
- 长事务拆成多个本地事务,每步有对应补偿事务
- 编排模式(Choreography)vs 指挥模式(Orchestration)
消息最终一致性
本地事务 + 发消息 → MQ → 消费者执行 → 失败则重试
- 使用事务消息(RocketMQ)保证发消息和本地事务原子性
4. 一致性哈希
- 将节点和数据 key 都映射到 0~2³² 的环上
- key 顺时针找到第一个节点
- 增减节点只影响相邻节点的数据迁移
- 虚拟节点:每个物理节点映射多个虚拟节点,负载更均衡
5. 分布式缓存架构
多级缓存
请求 → 本地缓存(Caffeine)→ 分布式缓存(Redis)→ 数据库
缓存与 DB 一致性策略
- Cache Aside:读:先缓存后 DB;写:先更新 DB,再删缓存
- Write Through:写 DB 和缓存同步进行(强一致但慢)
- 延迟双删:更新 DB → 删缓存 → 延迟 500ms 再删一次
6. 服务治理
熔断器状态机
Closed(正常)→ [失败率超阈值] → Open(熔断)
↓ [等待超时]
Half-Open(试探)
↓ [成功]
Closed
限流算法对比
| 算法 | 特点 |
|---|---|
| 固定窗口 | 简单,临界点突刺问题 |
| 滑动窗口 | 解决突刺,内存稍多 |
| 漏桶 | 平滑输出,不允许突发 |
| 令牌桶 | 允许一定突发,更常用 |
7. 高频面试题
- BASE 和 ACID 的关系? BASE 是 CAP 中 AP 的实践,用最终一致性换可用性
- 如何保证消息不丢失? 生产者确认 + MQ 持久化 + 消费者手动 ack
- 如何保证消息幂等? 消费端去重(唯一 ID + 数据库唯一约束 / Redis SET NX)
- Raft 和 Paxos 的区别? Raft 更易理解,强 Leader,leader 处理所有请求;Paxos 更通用复杂
Redis 深入面经
Redis 深水区知识
1. 数据结构底层实现
| 类型 | 底层结构 | 编码 |
|---|---|---|
| String | SDS(简单动态字符串) | int/embstr/raw |
| List | quicklist(小节点用 ziplist,大节点用 listpack) | - |
| Hash | ziplist(小)/ hashtable(大) | - |
| Set | intset(小整数)/ hashtable | - |
| ZSet | ziplist(小)/ skiplist+hashtable | - |
SDS vs C 字符串
- 获取长度 O(1)(存储 len)vs O(n)
- 二进制安全,可存储任意数据
- 空间预分配 + 惰性释放,减少内存重分配
跳表(skiplist)
- 多层有序链表,平均查询 O(log n)
- 第一层包含所有节点,每层节点数约为下层的 1/2
- 为什么不用红黑树:实现简单、支持范围查询更自然
2. 持久化深入
RDB 快照
Save 条件:
- save 900 1:900秒内至少 1 个键变化
- save 300 10:300秒内至少 10 个键变化
枯写过程:
fork() -> 子进程生成 RDB -> 枯写完成 -> 替换旧文件
COW(写时复制):
- 子进程共享应用程序内存页
- 主进程写入时才复制页,避免锁内存展层
AOF 日志
Append-Only File:记录每个写操作命令
fsync 策略:
- always:每次写操作后刷盘(最安全,最慢)
- everysec:每秒刷盘(默认,最多丢失 1s 数据)
- no:由操作系统决定(最快,首机可能丢失较多)
AOF 重写:
- 多个操作合并为最终状态(如 INCR 100 次 -> SET key 100)
- bgrewriteaof 命令或自动触发
RDB + AOF 混合持久化(Redis 4.0+)
- AOF 文件前半序是 RDB 快照,后半是增量 AOF 日志
- 兼具两者优点:加载快 + 数据完整
3. 集群模式
主从复制
Master通过 replication backlog 同步数据到 Slave
全量同步:Slave 首次连接,Master bgsave + 发送 RDB
部分同步:断线重连后,通过 offset 发送市机进山的命令
哨兵(Sentinel)
- 监控主从状态,Master 下线后自动故障转移
- 需超过半数 Sentinel 同意才进行故障转移(防脑裂)
- 客户端连接 Sentinel,由 Sentinel 告知当前 Master 地址
Redis Cluster
16384 个哈希槽(slot)分配到各节点
计算:crc16(key) % 16384
常见分配:3 Master + 3 Slave
Master1: slot 0-5461
Master2: slot 5462-10922
Master3: slot 10923-16383
客户端请求错误节点时接收 MOVED 指引到正确节点
4. 缓存三大问题深度分析
缓存击空庄(Penetration)
问题:不存在的 key 每次请求都到达 DB
解决:
1. 布隆过滤器(布隆过滤器如不存在则拦截)
2. 缓存空对象(设置较短的 TTL)
3. 请求参数校验居前,非法参数直接拒绝
缓存击空空(Breakdown)
问题:热点 key 失效,大量并发请求唠向 DB
解决:
1. 互斥锁:缓存失效时只有一个线程去查 DB并回写缓存
2. 预热:后台定时探测前主动刷新缓存
3. 逻辑过期:缓存反回旧数据,后台异步更新
缓存雪崩(Avalanche)
问题:大量 key 同时失效,或 Redis 整体崩溃
解决:
1. TTL 加随机偶尔少,防止同时失效
2. 数据预加载:应用启动时预先加载热点数据
3. 熔断降级: Redis 携带时返回默认值而非打援数据库
4. 集群化插件:防止单点故障
5. 常用模式与实现
分布式锁
-- 加锁:SET key value NX EX timeout
local result = redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])
if result then return 1 else return 0 end
-- 释放锁:比较 value 后删除
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
end
return 0
限流实现
-- 滑动窗口限流
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
-- 删除窗口外的请求
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 统计窗口内请求数
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now)
redis.call('EXPIRE', key, window)
return 1 -- 允许
end
return 0 -- 限流
排行榜(ZSet)
ZADD leaderboard <score> <userId>
ZRANGEBYSCORE leaderboard -inf +inf WITHSCORES -- 按分数查询
ZRANK leaderboard <userId> -- 获取排名
6. 资深面试题
- Redis 为什么首选跳表而不用平衡树?
- 平均 O(log n) 查询和平衡树相同,但实现简单、内存占用小
- 跳表有天然的范围查询优势,ZRANGEBYSCORE 非常高效
- 无需旋转回平衡,写入性能更好
- Redis 单线程执行为什么还这么快?
- 除 IO 外都是内存操作,没有磁盘寻址
- 非阻塞 IO 多路复用,一个线程处理所有请求
- 单线程避免了锁竭争和上下文切换开销
- RESP 协议是什么?
- Redis 客户端服务器通信协议。类型标志:+成功 -错误 $字符串 *数组 :整数
- 为什么选文本協议:易于调试、应用程序层天然支持、解析简单
- Pipeline 和 Lua 脚本的区别?
- Pipeline:客户端批量发送命令,减少网络往返;我不保证原子性
- Lua 脚本:在 Redis 服务端执行,多个命令完全原子
- 需要原子性用 Lua,只是减少网络往返用 Pipeline
Go 语言核心面经
Go 语言面试重点
1. Goroutine 与调度器
GMP 模型
G (Goroutine):Go 协程,轻量线程,初始堆 2KB
M (Machine):操作系统线程
P (Processor):调度器,可运行的 goroutine 队列
每个 P 有本地队列 + 全局队列
M 必须持有 P 才能执行 G
当 G 发生系统调用阻塞时,M 和 P 分离,其他 M 接管 P
工作窃取:P 的本地队列空时从其他 P 的队列尾部窃取
Goroutine 泄漏
// 常见泄漏场景
// 1. channel 没有接收者
ch := make(chan int)
go func() { ch <- 1 }() // 永远阻塞
// 2. goroutine 等待永远不会满足的条件
go func() {
select {
case <- neverClosedChannel:
}
}()
// 解决:始终将 context 传入
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok { return }
process(v)
case <-ctx.Done(): // 外部取消
return
}
}
}
2. Channel 深入
// channel 类型
// 无缓冲:发送和接收必须同时准备
ch1 := make(chan int)
// 有缓冲:发送在缓冲满之前不阻塞
ch2 := make(chan int, 10)
// select 实现多路合并
func fanIn(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range cs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
// 超时控制
select {
case result := <-ch:
process(result)
case <-time.After(3 * time.Second):
fmt.Println("超时")
}
3. 内存管理与 GC
逃逸分析
// 编译器分析变量是分配在栈还是堆上
// 堆:生命周期超过定义作用域的变量
// 栈:局部变量,不逆逸
func bad() *int { // bad: n 逆逸到堆
func bad() *int {
n := 42 // n 逆逸到堆
return &n
}
func good() int { // good: 返回値
n := 42
return n // n 在栈上
}
// 查看逆逸
go build -gcflags='-m' ./...
GC 三色标记清除
白色:待扫描,可能被回收
N 色:正在扫描
N 色:已扫描,确认存活
STW1 -> 标记根节点 ->
并发标记(与业务 goroutine 并行)->
STW2(保添操作)-> 清除白色
写屏障:并发标记期间,对黑色对象的指针修改会标记
4. 接口与多态
// Go 接口是隐式实现的
type Reader interface {
Read(p []byte) (n int, err error)
}
// 任何有 Read 方法的类型都实现了 Reader
type MyReader struct{}
func (r MyReader) Read(p []byte) (n int, err error) { ... }
// 空接口和类型断言
var i interface{} = "hello"
s, ok := i.(string) // 类型断言
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
}
// 接口的内部表示:(type, value) 元组
// 注意 nil 接口 != 具有 nil 值的接口
var p *int = nil
var i interface{} = p
fmt.Println(i == nil) // false!(type 不为 nil)
5. 并发模式
// Worker Pool
func workerPool(jobs <-chan Job, results chan<- Result, n int) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() { wg.Wait(); close(results) }()
}
// sync.Once 实现单例
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() { instance = &Singleton{} })
return instance
}
// sync.Pool 对象池
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
// 使用 buf
}
6. 资深面试题
- Go 的 GC 会 STW 多久?
- Go 1.14+ STW 最大几百微秒,大部分工作并发执行
- 调控 GOGC 环境变量控制 GC 触发阈值(默认 100%)
- make 和 new 的区别?
new(T)分配 T 类型的内存并返回指针,内存为零值make(T, ...)仅用于 slice/map/channel,返回 T 类型(非指针)
- 为什么 slice 并发不安全?
- slice 扩容时会重新分配底层数组,并发修改可能操作舍弃的旧内存
- 采用
sync.Mutex或 channel 保护
- defer 的执行顺序和性能问题?
- LIFO 顺序执行
- defer 在函数返回时执行,可修改命名返回值
- 每个 defer 有一小撑开销,循环中大量使用时用匿名函数包裹
Node.js 核心面经
Node.js 深水区知识
1. 事件循环深度
6 个阶段
timers -> setTimeout / setInterval
pending I/O -> 上一转延迟的 I/O
idle, prepare -> 内部使用
poll -> 检索新 I/O,执行 I/O 回调
check -> setImmediate
close callbacks -> socket.destroy() 等
每个阶段之前,清空 microtask 队列:
process.nextTick 队列 -> Promise 微任务队列
经典面试题:输出顺序
setTimeout(() => console.log('setTimeout'), 0)
setImmediate(() => console.log('setImmediate'))
process.nextTick(() => console.log('nextTick'))
Promise.resolve().then(() => console.log('Promise'))
console.log('sync')
// 输出顺序:
// sync
// nextTick
// Promise
// setTimeout (在 I/O 外面,timeout 和 immediate 顺序不定)
// setImmediate
poll 阶段详解
- 如果 timer 队列非空,进入 timers 阶段
- 如果进入 poll 时没有 timer,直接执行 poll 回调
- 如果 poll 为空且有 setImmediate,进入 check 阶段
2. 模块系统
CommonJS vs ESM
// CommonJS:运行时加载,同步
const fs = require('fs') // 运行时才执行
exports.greet = name => `Hello ${name}`
// ESM:静态分析,异步
import fs from 'fs' // 编译时分析
export const greet = name => `Hello ${name}`
// 主要区别
// 1. CommonJS 是值导出,ESM 是实时绑定
// 2. CommonJS require 可在中途调用,import 必须在顶层
// 3. ESM 有利于 tree-shaking
const value = require('./module').value // CommonJS: value 是拷贝
import { value } from './module' // ESM: value 是引用
模块寻址顺序
1. 核心模块(Buffer、path...)
2. 路径模块(./、../、/开头)
3. node_modules 查找(逐级向上)
3. 性能优化
集群模式
// cluster 模块利用多核 CPU
import cluster from 'cluster'
import { cpus } from 'os'
if (cluster.isPrimary) {
const numCPUs = cpus().length
for (let i = 0; i < numCPUs; i++) {
cluster.fork() // 每个 CPU 核开一个 Worker
}
cluster.on('exit', (worker) => {
cluster.fork() // Worker 崩溃自动重启
})
} else {
// Worker 进程运行 HTTP 服务
app.listen(3000)
}
Worker Threads 处理 CPU 密集任务
import { Worker, isMainThread, parentPort } from 'worker_threads'
if (isMainThread) {
const worker = new Worker(__filename)
worker.on('message', result => console.log('Result:', result))
worker.postMessage({ data: heavyData })
} else {
parentPort.on('message', ({ data }) => {
const result = heavyComputation(data) // CPU 密集运算
parentPort.postMessage(result)
})
}
内存泄漏排查
# 开启内存分析
node --inspect app.js
# 姳片分析
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>
# 常见泄漏场景:
# 1. 全局变量累积
# 2. 闭包持有外部引用未释放
# 3. 事件监听器未移除
4. 流处理与管餅
// 流序列化:管餅
readStream
.pipe(gunzip()) // 解压
.pipe(csvParser()) // 解析
.pipe(transform()) // 转换
.pipe(writeStream) // 写入
// 现代写法:pipeline
const { pipeline } = require('stream/promises')
await pipeline(
fs.createReadStream('input.gz'),
zlib.createGunzip(),
fs.createWriteStream('output')
)
// 自动处理错误和清理
// 备压处理
readStream.on('data', chunk => {
if (!writeStream.write(chunk)) {
readStream.pause() // 写入缓冲达到高水位,暂停读取
writeStream.once('drain', () => readStream.resume())
}
})
5. 资深面试题
- 如何处理运算密集任务而不阻塞事件循环?
- Worker Threads 处理 CPU 密集
- child_process.fork() 开子进程
- 将任务切片,用 setImmediate/setTimeout 分批执行
- require 模块是否每次都重新执行?
- 不是,模块有缓存机制(require.cache)
- 第一次 require 执行并缓存,后续直接返回缓存
- 删除缓存:delete require.cache[require.resolve('./module')]
- 如何优化 Node.js 应用的内存占用?
- 调整 --max-old-space-size 设置堆内存上限
- 大文件用 Stream 处理,避免全部读入内存
- 缓存设置合理的 TTL,防止无限增长
- process.nextTick 和 setImmediate 的应用场景?
- nextTick:当前操作完成后立即执行,高于其他微任务
- setImmediate:在当前 poll 阶段完成后执行
- nextTick 如果递归调用可能阻塞事件循环,要注意
Java / Spring Boot 核心面经
Java 运行时 & Spring Boot 深度
1. JVM 内存结构
Heap(堆)
Young Generation
Eden Space <- 新对象分配在这里
Survivor S0/S1 <- 经历多次 GC 的对象
Old Generation <- 长期存活的对象
Non-Heap
Metaspace <- 类元数据(JDK 8+ 替换 PermGen)
Code Cache <- JIT 编译的机器码
Stack(每个线程一个)
方法调用栈帧:局部变量、返回地址、操数栈
2. GC 算法
| 收集器 | 算法 | 适用场景 |
|---|---|---|
| Serial GC | 单线程标记-清除 | 单核/内存小 |
| Parallel GC | 多线程吸层 | 吸层优先 |
| CMS | 并发标记-清除 | 低延迟(已废弃) |
| G1 GC | 分区 Region | JDK 9+ 默认 |
| ZGC | 全并发、每次 < 1ms STW | 超大堆/低延迟 |
G1 GC 工作原理
- 将堆划分为大小相等的 Region(2MB)
- 优先回收垃圾最多的 Region(Garbage First)
- 可预测停顿时间,默认目标 200ms
3. Java 并发深度
volatile 关键字
private volatile boolean flag = false;
// 保证可见性:一个线程修改后其他线程立即可见
// 保证有序性:Happens-Before 规则
// 不保证原子性:count++ 不是原子操作
synchronized vs ReentrantLock
// synchronized:简单、JVM 自动管理
synchronized (lock) {
// 临界区
}
// ReentrantLock:可中断、公平锁、多条件
ReentrantLock lock = new ReentrantLock(true); // 公平锁
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) {
// 临界区
}
} finally {
lock.unlock(); // 必须手动释放
}
// 多条件锁实现生产者消费者
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
ConcurrentHashMap 实现(JDK 8+)
- 数组 + 链表 + 红黑树
- 不再使用分段锁,转而用 CAS + synchronized
- 每个桶用 synchronized 魔谁头节点
- 读操作岆负极小,岅不锁
4. Spring Boot 核心
Bean 生命周期
容器启动
-> 扫描 @Component/@Service/@Repository
-> BeanDefinition 注册
-> 实例化(构造器注入)
-> 属性注入(@Autowired)
-> Aware 接口回调
-> BeanPostProcessor#beforeInit
-> @PostConstruct
-> InitializingBean#afterPropertiesSet
-> BeanPostProcessor#afterInit’s
-> [Bean 处于就绪状态]
-> @PreDestroy -> DisposableBean#destroy
Spring AOP 实现
@Aspect
@Component
public class TimingAspect {
@Around("@annotation(Timed)")
public Object timeMethod(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
long elapsed = System.currentTimeMillis() - start;
log.info("{} took {}ms", pjp.getSignature(), elapsed);
}
}
}
// 实现数据库事务也是通过 AOP 注入事务逻辑
@Transactional // 内部用 CGLIB 实现
@Transactional 常见坏呓
// 1. 同一类内方法自调失效(代理类内部调用)
@Service
public class OrderService {
@Transactional
public void createOrder() {
this.processPayment(); // 事务失效!
}
@Transactional
public void processPayment() { ... }
}
// 解决:注入自身代理
ApplicationContext.getBean(OrderService.class).processPayment();
// 2. 异常类型错误导致不回滚
@Transactional // 默认只回滚 RuntimeException
public void method() throws Exception {
// Exception 不会回滚
}
// 解决
@Transactional(rollbackFor = Exception.class)
5. Spring Cloud 微服务
服务注册: Nacos / Eureka
负载均衡: Spring Cloud LoadBalancer
API 网关: Spring Cloud Gateway
服务调用: OpenFeign
熔断: Resilience4j
配置中心: Nacos Config
链路追踪: SkyWalking / Micrometer Tracing
OpenFeign 示例
@FeignClient(name = "user-service", fallback = UserServiceFallback.class)
public interface UserServiceClient {
@GetMapping("/users/{id}")
UserDTO getUser(@PathVariable String id);
}
@Component
class UserServiceFallback implements UserServiceClient {
@Override
public UserDTO getUser(String id) {
return UserDTO.empty(); // 降级处理
}
}
6. 性能调优
# JVM 参数调优
java -Xms2g -Xmx2g # 初始和最大堆内存
java -XX:+UseG1GC # 使用 G1 GC
java -XX:MaxGCPauseMillis=200 # GC 停顿目标
# 性能分析工具
arthas # 阿里巴巴开源,线上诊断
jvisualvm / JFR # JVM 性能图谱
# 情境问题快速定位
jstack <pid> # 查看线程堵堵
jmap -heap <pid> # 堆内存分布
7. 资深面试题
- HashMap 为什么容量是 2 的幂次?
- 取模可用位运算代替:
hash & (n-1)效率高于hash % n - 2 的幂次时
n-1的二进制全是 1,哈希分布更均匀
- 取模可用位运算代替:
- Spring 的三级缓存如何解决循环依赖?
- 一级缓存:完整对象
- 二级缓存:早期暴露的对象(尚未设置属性)
- 三级缓存:ObjectFactory(创建对象的工厂)
- 构造器注入的循环依赖不能解决
- @Transactional 与 ThreadLocal 的关系?
- Spring 事务的连接和事务状态绑定在 ThreadLocal 中
- 异步线程中事务无法传播,需特殊处理
- JVM 调优第一步看什么?
- 先确认是 CPU、内存还是 GC 问题
- GC 日志:分析停顿时间和频率
- Arthas:在线分析热点方法和内存异常
Agent / AI 面经
核心考点
1. LLM 基础
- Transformer 架构:Encoder-Decoder,注意力机制(Attention)是核心
- Self-Attention:Q、K、V 矩阵,计算 token 间相关性
- 位置编码:解决序列顺序问题(绝对/相对/RoPE)
- Temperature & Top-p:控制输出随机性;Temperature 越低越确定
- 上下文窗口:模型一次能处理的最大 token 数
2. Prompt 工程
- Zero-shot:直接提问,无示例
- Few-shot:提供少量示例引导输出格式
- Chain-of-Thought (CoT):让模型逐步推理,提升复杂任务准确性
- System Prompt:设定角色和行为约束
- Prompt 注入攻击:用户输入覆盖系统指令,需做输入过滤
3. RAG(检索增强生成)
用户问题 → 向量化 → 向量数据库检索 → 相关文档 → 拼入 Prompt → LLM 生成答案
- 向量数据库:Pinecone、Qdrant、Chroma、Milvus
- Embedding 模型:将文本转为高维向量(如 text-embedding-3-small)
- Chunk 策略:固定大小 / 语义分割;重叠窗口避免信息截断
- Re-ranking:检索后用精排模型提升相关性
4. Agent 设计模式
- ReAct:Reasoning + Acting,思考→动作→观察循环
- Plan-and-Execute:先规划子任务,再逐步执行
- Multi-Agent:多个 Agent 协作,各司其职(Orchestrator + Worker)
- Tool Use / Function Calling:LLM 决定调用哪个工具及参数
5. MCP(Model Context Protocol)
- Anthropic 提出的开放协议,统一 AI 与外部工具/数据源的接口
- 架构:MCP Client(LLM 应用)↔ MCP Server(工具提供方)
- 传输方式:stdio(本地)、HTTP+SSE(远程)
- 核心能力:Tools(工具调用)、Resources(数据读取)、Prompts(提示模板)
- 意义:避免每个应用重复实现工具集成,生态统一
6. 微调 vs RAG vs Prompt
| 方法 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|
| Prompt Engineering | 零成本,快速迭代 | 受上下文长度限制 | 通用任务 |
| RAG | 知识可实时更新 | 检索质量影响结果 | 知识库问答 |
| Fine-tuning | 深度定制风格/能力 | 成本高,数据要求高 | 垂直领域专业化 |
7. 高频面试题
- Hallucination(幻觉)怎么缓解:RAG 提供事实依据、CoT 推理、输出校验
- 如何评估 LLM 效果:BLEU/ROUGE(文本相似度)、人工评估、自动化 LLM-as-Judge
- Token 数怎么优化:压缩 Prompt、摘要历史对话、动态检索
- 流式输出原理:SSE(Server-Sent Events)逐 token 推送
- LangChain vs 原生调用:LangChain 封装链路和工具,快速原型;生产环境可考虑轻量替代(LlamaIndex、自研)
RAG 系统设计与优化
RAG 深入解析
1. RAG 完整架构
【离线阶段 - 知识库构建】
原始文档 → 预处理(清洗/去重)→ 分块(Chunking)
→ Embedding 模型向量化 → 存入向量数据库
【在线阶段 - 检索生成】
用户问题 → Query 改写/增强 → 向量化
→ 向量检索(Top-K)→ 可选:Re-ranking
→ 拼接 Prompt → LLM 生成 → 后处理 → 输出
2. Chunking 策略
| 策略 | 特点 | 适用场景 |
|---|---|---|
| 固定大小 | 简单,可能截断语义 | 通用场景 |
| 递归字符分割 | 按段落/句子/词分割 | 结构化文本 |
| 语义分割 | 按语义边界分块 | 质量要求高 |
| 文档结构感知 | 利用 Markdown/HTML 标题 | 有结构的文档 |
| Small-to-Big | 存小块,检索后返回大块 | 平衡精度和上下文 |
关键参数
chunk_size:通常 512-1024 tokenschunk_overlap:通常 50-200 tokens,避免边界信息丢失
3. 检索优化
混合检索
稠密检索(向量相似度)
+
稀疏检索(BM25 关键词)
→ RRF(倒数排名融合)合并结果
Query 增强
- HyDE(假设文档嵌入):让 LLM 先生成假设答案,用假设答案检索
- Multi-Query:生成多个角度的查询并集检索
- Step-Back Prompting:将具体问题泛化为更高层次的问题
Re-ranking
- Cross-Encoder 模型(如 BGE-Reranker)对 query+doc 联合编码评分
- 比双塔 Embedding 更准确,但速度慢(仅用于 Top-K 精排)
4. 向量数据库选型
| 数据库 | 特点 | 适用场景 |
|---|---|---|
| Pinecone | 全托管,简单 | 快速原型 |
| Qdrant | 开源,Rust 实现,高性能 | 自托管生产 |
| Weaviate | 内置混合检索 | 需混合搜索 |
| Chroma | 轻量,本地优先 | 开发调试 |
| Milvus | 大规模,功能全 | 企业级 |
| pgvector | PostgreSQL 扩展 | 已有 PG 的项目 |
5. 评估体系
RAGAS 评估框架:
- Faithfulness(忠实性):答案是否基于检索到的上下文
- Answer Relevancy(相关性):答案是否回答了问题
- Context Recall(上下文召回):检索的文档是否包含答案所需信息
- Context Precision(上下文精确):检索的文档是否都有用
6. 常见问题与优化
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 检索不到相关内容 | Chunk 太大/太小,Embedding 弱 | 调整分块,换更好的 Embedding 模型 |
| 检索到但答错 | LLM 忽略上下文 | 优化 Prompt,强调「只根据以下内容回答」 |
| 多跳推理失败 | 信息分散在多个文档 | Graph RAG,建立实体关系图 |
| 长文档理解差 | 超出上下文窗口 | 层次化索引(先摘要,再详细) |
7. 高频面试题
- RAG 和 Fine-tuning 如何选择? 知识需要实时更新用 RAG;需要改变模型风格/能力用 Fine-tuning;两者可结合
- 如何提升 RAG 的准确率? 改善 Chunking 策略、升级 Embedding 模型、加 Re-ranking、优化 Prompt
- 向量相似度搜索原理? 计算 Query 向量与文档向量的余弦相似度或内积,用 HNSW/IVF 近似最近邻加速
- Graph RAG 是什么? 将文档解析为知识图谱,结合图遍历和向量检索,适合复杂推理
AI Agent 工程化实践
Agent 工程化全景
1. Function Calling 深入
tools = [{
"type": "function",
"function": {
"name": "search_web",
"description": "搜索互联网获取实时信息",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"}
},
"required": ["query"]
}
}
}]
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
tools=tools,
tool_choice="auto" # auto / none / required
)
# 若模型决定调用工具
if response.choices[0].message.tool_calls:
tool_call = response.choices[0].message.tool_calls[0]
result = execute_tool(tool_call.function.name, tool_call.function.arguments)
# 将结果追加到 messages 继续对话
2. ReAct 模式实现
System Prompt 示例:
你是一个能调用工具的 AI 助手。每次思考请遵循以下格式:
Thought: 分析当前状况,决定下一步
Action: 调用工具名称
Action Input: 工具参数
Observation: (工具返回结果)
... (循环直到有答案)
Final Answer: 最终答案
终止条件
- 得到足够信息生成最终答案
- 达到最大迭代次数(防止死循环)
- 工具调用失败超过阈值
3. 多 Agent 系统设计
层级架构
Orchestrator Agent(任务分解和协调)
├── Research Agent(信息检索)
├── Code Agent(代码生成执行)
├── Writer Agent(内容生成)
└── Critic Agent(质量评估)
通信方式
- 共享消息队列
- Blackboard 模式(共享状态空间)
- 直接函数调用(同步)
- 事件驱动(异步)
代表框架
| 框架 | 特点 |
|---|---|
| LangGraph | 图结构工作流,状态机,支持循环 |
| AutoGen | 对话式多 Agent,微软出品 |
| CrewAI | 角色化 Agent 团队,简单易用 |
| OpenAI Swarm | 轻量级 handoff 模式 |
4. 记忆系统设计
短期记忆(In-context)
→ 当前对话历史,直接放入 context window
→ 超长时摘要压缩
长期记忆(外部存储)
→ 向量存储(语义检索历史对话)
→ 结构化存储(用户偏好、任务状态)
→ 知识图谱(实体关系)
工作记忆(执行状态)
→ 当前任务的中间结果
→ 工具调用历史
5. 可靠性与安全
幂等性
- 工具调用可能因网络重试多次执行
- 设计工具时保证幂等(查询类天然幂等,写入类需去重)
沙箱执行
- 代码执行 Agent 必须在隔离环境(Docker/E2B)
- 限制文件系统访问、网络访问、执行时间
Prompt 注入防御
- 严格区分系统指令和用户输入
- 对工具输出做内容过滤
- 敏感操作需要用户二次确认
可观测性
- 记录每次 LLM 调用(输入/输出/token 数/延迟)
- 工具调用日志(工具名/参数/结果/耗时)
- 使用 LangSmith / Langfuse / Phoenix 做 tracing
6. 成本与延迟优化
- 缓存:相同输入的 LLM 调用结果缓存(Prompt Cache)
- 模型路由:简单任务用小模型(GPT-4o-mini),复杂任务用大模型
- 流式输出:SSE 降低首字节时间,提升用户体验
- 并行工具调用:无依赖的工具并发执行
- Context 压缩:历史对话摘要,减少 token 消耗
7. 高频面试题
- Agent 如何处理工具调用失败? 重试(指数退避)+ fallback 工具 + 告知 LLM 错误信息让其调整策略
- 如何防止 Agent 无限循环? 最大迭代次数 + 检测重复 action + 超时熔断
- 多 Agent 如何保证任务不重复? 任务队列 + 状态锁 + 幂等性设计
- 如何评估 Agent 质量? 任务完成率、工具调用准确率、token 效率、延迟 P99
大模型微调与部署面经
大模型工程化
1. 微调方法对比
| 方法 | 参数量 | 显存需求 | 效果 | 适用场景 |
|---|---|---|---|---|
| Full Fine-tuning | 全量 | 极高(需多卡) | 最好 | 资源充足 |
| LoRA | 极少(0.1-1%) | 低 | 较好 | 最常用 |
| QLoRA | 极少 | 极低(量化基座) | 接近 LoRA | 消费级 GPU |
| Prefix Tuning | 少 | 低 | 一般 | 特定任务 |
| Prompt Tuning | 极少 | 极低 | 较差 | 资源极少 |
LoRA 原理
原始权重 W(冻结)
低秩矩阵 ΔW = B × A(可训练,rank << d)
实际前向:h = Wx + BAx
- rank 通常取 4-64,越大效果越好但参数越多
- 推理时可将 BA 合并回 W,无额外推理开销
2. 训练数据准备
数据格式(Instruction Tuning)
{
"instruction": "将以下英文翻译成中文",
"input": "Hello, world!",
"output": "你好,世界!"
}
数据质量胜过数量
- 1000 条高质量 > 10000 条低质量
- 去重(MinHash)、过滤(困惑度/规则)、多样性保证
- Alpaca / ShareGPT / LIMA 格式是常见开源数据集格式
RLHF 流程
1. SFT(监督微调):在高质量数据上微调
2. Reward Model 训练:让人类对输出排序,训练奖励模型
3. PPO 强化学习:用奖励模型优化策略,对齐人类偏好
DPO(Direct Preference Optimization):绕过 RM 直接从偏好数据优化,更简单稳定
3. 模型量化
| 量化方式 | 精度损失 | 速度提升 | 内存节省 |
|---|---|---|---|
| FP16 / BF16 | 极小 | 2x | 2x |
| INT8(LLM.int8) | 小 | 1.5-2x | 4x |
| INT4(GPTQ/AWQ) | 中 | 2-4x | 8x |
| GGUF(llama.cpp) | 可选 | CPU 可用 | 8-16x |
- GPTQ:后训练量化,逐层校准,精度好
- AWQ:激活感知量化,保留重要权重精度
- GGUF:llama.cpp 格式,支持 CPU 推理和混合量化
4. 推理优化
KV Cache
- 缓存已计算的 Key/Value,避免重复计算历史 token
- 内存瓶颈:长上下文时 KV Cache 可达数十 GB
- 优化:PagedAttention(vLLM)、滑动窗口、GQA(分组查询注意力)
推理框架
| 框架 | 特点 |
|---|---|
| vLLM | PagedAttention,高吞吐,连续批处理 |
| TGI(HuggingFace) | 生产就绪,流式输出 |
| Ollama | 本地部署,易用 |
| llama.cpp | CPU 推理,量化支持好 |
| TensorRT-LLM | NVIDIA 优化,极致性能 |
5. 部署架构
用户请求
↓
API Gateway(认证/限流)
↓
Load Balancer
↓
推理服务集群(vLLM / TGI)
↓
GPU 实例(A100 / H100 / L40S)
+ Prompt Cache 层(Redis)
+ 模型版本管理(MLflow)
+ 监控(Prometheus + Grafana)
6. 高频面试题
- LoRA 的 rank 怎么选? 从小到大实验,数据少 rank 小(4-8),数据多任务复杂 rank 大(32-64)
- 微调后模型为什么会遗忘原有能力? 灾难性遗忘,解决:混合原始数据、降低学习率、使用 LoRA
- 如何估算微调所需显存? 模型参数 × 精度(FP16=2字节)× 3(模型+梯度+优化器状态)+ 激活值
- vLLM 为什么快? PagedAttention 消除 KV Cache 碎片化 + 连续批处理(Continuous Batching)提高 GPU 利用率
- 如何做模型 A/B 测试? 流量分配 + 统一评估指标 + 足够样本量保证统计显著性
LLM 底层原理深入
Transformer 与大模型核心原理
1. Attention 机制
Self-Attention 计算流程
输入嵌入 X (seq_len x d_model)
|
Q = X * W_Q, K = X * W_K, V = X * W_V
|
Attention(Q,K,V) = softmax(Q * K^T / sqrt(d_k)) * V
缩放因子 sqrt(d_k):防止点积过大导致 softmax 梯度消失
|
Multi-Head: 并行多个 attention 头,关注不同子空间
Multi-Head Attention
MultiHead(Q,K,V) = Concat(head_1,...,head_h) * W_O
其中 head_i = Attention(Q*W_Q_i, K*W_K_i, V*W_V_i)
典型配置:GPT-3 96 个头,每个头 d_k = d_model/h = 128
KV Cache 优化
自回归生成时,每个新 token 都需计算所有历史 token 的 K/V
-> 缓存所有历史 K/V,新 token 只需计算自己的 K/V
-> 生成速度从 O(n^2) 降为 O(n)
2. 位置编码
绝对位置编码(Sinusoidal)
PE(pos, 2i) = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))
优点:无参数,可注意到超过训练长度的位置
缺点:强行建模窗口长度限制
RoPE(旋转位置编码)
将位置信息编码到 Q、K 的旋转操作中
内积 q^T * k 天然地只依赖两个 token 的相对位置
限度外推能力更强,被 LLaMA、Qwen 等模型广泛采用
3. 主流架构对比
GPT 系列(Decoder-Only)
仅使用 Transformer Decoder
因果式注意力(Causal Attention):每个 token 只能关注左边
适合文本生成任务
BERT 系列(Encoder-Only)
双向注意力:每个 token 可关注所有其他 token
适合理解任务:分类、NER、问答匹配
T5 系列(Encoder-Decoder)
Encoder 理解输入,Decoder 自回归生成输出
适合 Seq2Seq:翻译、摘要、问答
4. 模型训练
预训练目标
Causal LM(GPT):预测下一个 token
loss = CrossEntropy(predict[i], actual[i+1])
Masked LM(BERT):预测被遮罩的 token
随机遮罩 15% token,预测原始 token
常用优化器
Adam:自适应学习率,LLM 训练最常用
AdamW:Adam + 权重衰减,防止过拟合
Cosine LR Schedule:
- 预热期(warmup):学习率从 0 增到最大値
- 䆮减期:cos 曲线减小到最小学习率
混合精度训练(Mixed Precision)
forward + backward:FP16(快,内存少)
权重更新:FP32(防止上溢)
损失缩放:防止 FP16 下溢(梯度过小表示为 0)
5. 模型评估
自动化指标
Perplexity(困惑度):衡量模型对测试集的预测能力
PPL = exp(mean(-log P(token_i)))
PPL 越低越好
BLEU:机器翻译评估,n-gram 重合度
ROUGE:摘要评估,召回率为主
LLM 专属评测
MMLU:世界知识理解,57 个学科选择题
HumanEval:代码生成能力(编写 Python 函数)
GSM8K:小学数学题推理
MTBench:多轮对话质量,GPT-4 作裁判
6. 高效 Attention 变体
Flash Attention
- 将 Attention 计算分块,减少 HBM(显存)访问次数
- IO 复杂度:O(n^2/B) vs 标准 O(n^2),实际可加速 2-4x
- 然而输出与标准 Attention 完全相同(数学等价)
GQA(分组查询注意力)
MHA: 每个 Q 头都有独立的 K/V 头
MQA: 所有 Q 头共享 1 个 K/V 头(KV Cache 最小)
GQA: G 组 Q 头共享 1 个 K/V 头(平衡)
LLaMA 3 / Mistral 采用 GQA,大幅减小 KV Cache 占用
7. 资深面试题
- 为什么缩放系数 sqrt(d_k) 很重要?
- 向量维度 d_k 增大时,点积的方差增大,导致 softmax 梯度极小
- 除以 sqrt(d_k) 能把方差拉回到合理范围
- 为什么 GPT 不用 Encoder?
- 文本生成天然地是自回归的,只需 Decoder
- 去掉 Encoder-Decoder 交叉注意力,模型更简单、规模更容易扯展
- 长文本处理的振戰是什么?
- 标准 Attention 是 O(n^2) 空间和时间复杂度
- 解决:Flash Attention(效率)、Sliding Window(法拉)、RoPE 线性外推
- 当 context 超过训练长度时模型表现如何?
- Sinusoidal 位置编码:超长位置没被训练过,推理性能显著下降
- RoPE:基于相对位置,外推能力更强,才有 YaRN、LongRoPE 等扩展方法
Prompt Engineering 进阶技巧
资深 Prompt 工程全指南
1. 高质量 Prompt 的完整结构
角色设定(Role):你是谁,有什么能力和经验
任务说明(Task):需要做什么,目标是什么
背景信息(Context):相关上下文和限制
输入数据(Input):具体的内容
输出格式(Format):JSON / Markdown / 列表 / 表格
约束条件(Constraints):哪些要做,哪些不要做
示例(Examples):few-shot 样例
高质量 System Prompt 示例
你是一个资深的 React 代码审查尓,有超过 10 年的上线项目经验。
你的职责:
- 密切关注代码质量、性能和安全性
- 只评论最重要的 2-3 个问题,每个问题用 100 字内阐明
- 对每个问题提供具体的修改建议和代码示例
- 评论用 Markdown 格式输出
严重问题:内存泄漏、XSS、并发风险
中等问题:不必要重渲染、过度设计、缺少错误处理
轻微问题:风格不一致、重复代码
2. Chain-of-Thought 及其变体
Zero-Shot CoT
Q: 23 个員工每人工资 8500 元,每月总工资是多少?让我们一步一步思考。
Few-Shot CoT(提供推理示例)
示例:
Q: 5 个苹果,吃了3个,还剩几个?
A: 初始有 5 个苹果。吃掉了3个,5 - 3 = 2。所以还剩 2 个。
现在解决:
Q: 23 个員工每人工资 8500 元,每月总工资是多少?
A: 总工资 = 23 x 8500 = ...
Tree-of-Thoughts(多路探索)
请展开 3 个不同的解决思路,然后对每个思路进行优劣分析,最后选择最佳方案并详细展开。
考虑维度:可行性、实施成本、预期效果。
3. 达到编垆的输出控制
JSON 输出控制
以 JSON 格式输出结果,不要添加任何其他文字:
{
"intent": "用户意图",
"entities": ["实体列表"],
"sentiment": "positive|negative|neutral",
"confidence": 0.95
}
结构化输出模板
请按如下格式回复,远局一行也不要输出:
## 分析结果
**核心问题:**[1-2句]
**影响范围:**[1-2句]
## 解决方案
1. [100字内]
2. [100字内]
## 推荐方案
[50字内,说明选择理由]
4. 上下文管理技巧
对话历史压缩
[历史对话摘要:
- 用户希望完成一个 React 在线商城设计
- 已确定技术栈:React + TypeScript + TailwindCSS
- 已设计完成首页和商品详情页
- 待完成:购物车和结账页面]
请继续帮助设计购物车页面。
分片加载分析
文档总共 50 页,每次只分析第 X-Y 页的内容。
当上一笔分析结束后,输入「继续」就会分析下一笔。
5. 提升模型输出质量的技巧
角色扮演法
模拟一位拥有 20 年 Kubernetes 运维经验的亓家。
以专家的身份回答:直接指出最常见的坑和额外注意事项。
对比法
以对比表格的形式比较 React 18 和 React 17 的差异。
包含:功能点、性能影响、迁移难度、适用场景。
递进式详细
先用 2 句概述核心关键点。
然后递进展开:
- 实现原理
- 代码示例
- 常见坏呓
- 最佳实践
6. 语境干预技巧
限制模型行为
严格要求:
1. 只使用我提供的代码库和文档中的 API,不要化鱼其他 API
2. 如果不确定就说「我不确定」,而不是编造答案
3. 代码示例必须是可运行的完整代码,不要使用占位符
强制指定格式
回答这个问题时务必遵循下列规则,否则回答无效:
- 第一行必须是 TLDR:用一句话概括
- 第二段展开详细解释
- 最后一行是行动建议
7. 测试与优化 Prompt 的方法
系统性评估
构建测试集:
1. 正常案例:10 个标准输入
2. 边界案例:5 个极端情况
3. 对抗性输入:3 个尝试误导的输入
评指标:输出准确性、格式合规性、违禁率
A/B 测试
对比两种 Prompt:
- 版本 A:直接说任务
- 版本 B:加上角色设定和示例
各运行 20 次,对比平均输出质量
8. 高级技巧
综合运用
步骤 1(角色设定):你是一个 SQL 优化专家
步骤 2(任务分解):分析这个查询的性能问题
- 先识别常见问题(全表扫描、缺少索引等)
- 再给出修改建议
- 最后提供优化后的 SQL
步骤 3(示例):提供一个类似的优化案例
自我评估准则
输出之前,对照以下标准自我检查:
1. 是否直接回答了用户的问题?
2. 是否提供了具体的代码示例?
3. 是否提到了可能的常见坏呓?
4. 格式是否符合要求?
如果以上任一项不满足,重新生成。
9. 高频资深面试题
- Prompt 注入攻击如何防御?
- 将系统指令和用户输入明确分隔
- 输入内容进行转义和过滤
- 配置输出可解释性,异常输出可监测
- 敏感操作需二次确认机制
- Few-shot 和 Fine-tuning 怎么选择?
- 数据量少且任务模式短期内频繁变化 -> Few-shot
- 需要专业风格、领域抓词、特定输出格式 -> Fine-tuning
- 两者并不互斥,微调后的模型用 few-shot 进一步提升
- 如何评估 Prompt 的质量?
- 准确性:输出是否正确回答了问题
- 一致性:相同输入输出是否稳定
- 完整性:输出是否覆盖了关键信息
- 格式合规性:输出是否符合指定格式
- 为什么 Temperature 高时输出更润着但可能不准确?
- Temperature 控制 softmax 后的概率分布晌散程度
- 高 Temperature:概率分布更平坦,小概率的 token 也有机会被选
- 低 Temperature:分布更集中,和确定性如 0 时总是选最高概率 token
小程序面经
核心考点
1. 双线程架构
- 渲染层(WebView):负责 WXML/WXSS 渲染,每个页面一个 WebView
- 逻辑层(JS Engine / V8):执行业务逻辑,独立线程,无法直接操作 DOM
- 通信:通过 JSBridge 与 Native 通信,数据经序列化传递
- 优点:安全(逻辑层无法直接操作 UI);缺点:通信有延迟,setData 数据量大性能差
2. 生命周期
App: onLaunch → onShow → onHide → onError
Page: onLoad → onShow → onReady → onHide → onUnload
- onLoad:只执行一次,接收页面参数
- onReady:页面初次渲染完成
- onShow / onHide:前后台切换时触发
3. 性能优化
- setData 优化:只传变化的数据,避免大对象;使用路径更新
'list[0].name' - 列表渲染:
wx:key必须设置,避免重复渲染 - 图片懒加载:
lazy-load属性 - 分包加载:主包 < 2MB,总包 < 20MB,按需加载分包
- 骨架屏:提升首屏体验
4. 与 H5 / App 的区别
| 对比 | 小程序 | H5 | App |
|---|---|---|---|
| 运行环境 | 微信/支付宝等宿主 | 浏览器 | 原生系统 |
| 发布 | 需审核 | 随时 | 需审核 |
| 能力 | 受宿主限制 | 受浏览器限制 | 几乎无限制 |
| 体验 | 接近原生 | 较差 | 最好 |
5. 跨端框架对比
| 框架 | 原理 | 优缺点 |
|---|---|---|
| Taro | 编译时转换 | React 语法,生态好,编译有差异 |
| uni-app | 运行时 + 编译 | Vue 语法,覆盖平台多 |
| mpx | 增强编译 | 贴近原生写法,性能好 |
6. 高频面试题
- 小程序登录流程:wx.login 获取 code → 后端换取 openid + session_key → 返回自定义 token
- WXS 是什么:在渲染层运行的脚本,可减少逻辑层通信,提升性能
- 自定义组件通信:父→子 properties;子→父 triggerEvent;兄弟通过父或 EventBus
- 为什么不能用 window/document:逻辑层没有 DOM/BOM 环境
- 微信支付流程:后端下单 → 返回支付参数 → 前端 wx.requestPayment → 回调处理
微信小程序实战面经
实战高频考点
1. 登录与鉴权完整流程
前端 wx.login() → 获取 code
↓
发送 code 到自己的后端
↓
后端调用微信接口:code2Session
→ 返回 openid + session_key + unionid
↓
后端生成自定义 token,存 Redis
↓
前端存 token 到 storage,后续请求携带
常见问题
session_key有有效期,需定期调用checkSession验证wx.getUserInfo已废弃,用wx.getUserProfile(需用户触发)unionid需满足:已关联公众号/小程序,或绑定开放平台
2. 支付流程
1. 前端发起支付请求到自己后端
2. 后端调用微信统一下单接口 → 返回 prepay_id
3. 后端用 prepay_id 签名生成支付参数
4. 前端调用 wx.requestPayment({ timeStamp, nonceStr, package, signType, paySign })
5. 用户支付完成 → 微信回调后端 → 后端更新订单状态
6. 前端 success 回调只做提示,不以此为支付成功依据(以后端通知为准)
3. 自定义组件高级用法
behaviors(类似 mixins)
const loginBehavior = Behavior({
data: { isLogin: false },
methods: {
checkLogin() { /* ... */ }
},
lifetimes: { attached() { this.checkLogin() } }
})
Component({
behaviors: [loginBehavior],
// 自动拥有 loginBehavior 的数据和方法
})
抽象节点(类似 slot 的高阶用法)
{ "componentGenerics": { "selectable": { "default": "./default-item" } } }
4. 小程序云开发
- 云函数:Node.js 环境,免鉴权调用微信 API,自动获取 openid
- 云数据库:文档型数据库,支持实时监听
- 云存储:文件上传下载,
wx.cloud.uploadFile - 云调用:云函数中调用微信开放能力(发送订阅消息等)
// 云函数端
exports.main = async (event, context) => {
const { openid } = context.OPENID // 自动获取,无需手动传
return db.collection('users').where({ _openid: openid }).get()
}
5. 分包策略详解
{
"subpackages": [
{ "root": "packageA", "pages": ["pages/cat", "pages/dog"] },
{ "root": "packageB", "pages": ["pages/apple"] }
],
"preloadRule": {
"pages/index": { "network": "all", "packages": ["packageA"] }
}
}
- 独立分包:可不依赖主包运行(分享页、广告页)
- 分包预加载:用户在某页面时提前加载指定分包
6. 调试与发布
- vconsole:小程序内置调试面板,
wx.setEnableDebug({ enableDebug: true }) - 抓包:Charles/Whistle 配合代理,需在微信开发者工具开启
- 体验版/预览版:真机测试不经过审核
- 灰度发布:后台配置按比例放量
7. 高频面试题
- 小程序如何做埋点? 封装统一 Page/Component,重写 onShow/tap 等生命周期
- 如何实现全局错误监控?
App.onError+wx.onError捕获未处理异常 - 小程序能访问 cookie 吗? 不能,用
wx.setStorageSync替代 - 如何做长连接?
wx.connectSocketWebSocket API - 页面间传参的方式? URL 参数、globalData、EventBus、本地存储
跨端框架深度对比(Taro / uni-app / Flutter)
跨端技术选型
1. Taro 深入
技术架构
开发语言:React / Vue
|
Taro 编译器
/ | \
微信 支付宝 H5 RN
小程序 小程序 Web 原生应用
Taro 3.x 运行时架构(重要变化)
- 老版本:编译时转换,将 JSX 转成小程序模板语言
- Taro 3.x:运行时适配,小程序运行 React/Vue 全量,通过运行时模拟 DOM API
- 优点:支持 React 全部特性;缺点:包体积大、性能差一些
Taro 特性
// 内置组件:跨端封装
const App = () => (
<View className="container"> {/* 转为 div / wx:view */}
<Text>Hello</Text> {/* 转为 span / wx:text */}
<Image src={img} /> {/* 转为 img / wx:image */}
<ScrollView scrollY> {/* 转为 各平台滚动组件 */}
{list.map(item => (
<View key={item.id}>{item.name}</View>
))}
</ScrollView>
</View>
)
// 平台判断
import Taro from '@tarojs/taro'
if (Taro.getEnv() === Taro.ENV_TYPE.WEAPP) {
// 微信小程序独有逻辑
}
性能优化
- 采用
@tarojs/plugin-html可直接使用 HTML 标签 - 长列表用
VirtualList组件 - 采用分包加载减小主包体积
2. uni-app 深入
技术架构
开发语言:Vue 3
|
uni-app 编译器
/ | | \
微信 支付宝 H5 App
| |
各家小程序 iOS/Android
<!-- 页面组件 -->
<template>
<view class="container">
<!-- 条件编译:不同平台显示不同内容 -->
<!-- #ifdef MP-WEIXIN -->
<text>微信小程序独有内容</text>
<!-- #endif -->
<!-- #ifdef H5 -->
<text>H5独有内容</text>
<!-- #endif -->
<text>所有平台共有</text>
</view>
</template>
uni-app 的 App 端
- H5+(HTML5 Plus)运行时:WebView 内嵌小程序逻辑 + 原生渲染
- nvue:基于 Weex 的原生渲染,性能较好
- uts(TypeScript 超集):编写原生插件,能调用原生 API
3. Flutter
核心架构
Dart 代码
|
Flutter 引擎
/ \
IOS Android
原生 原生
实现 实现
Flutter vs 小程序跨端
- Flutter 不依赖小程序容器,更适合面向 App 开发
- 小程序跨端(Taro/uni-app)主要面向微信、支付宝等平台
- Flutter 性能更接近原生,小程序跨端更快上手
4. 三种方案综合对比
| 对比项 | Taro 3.x | uni-app | Flutter |
|---|---|---|---|
| 开发语言 | React/Vue | Vue | Dart |
| 跨小程序 | 5家+ | 10家+ | 不支持 |
| 跨原生 App | RN | Weex | 完整支持 |
| 性能 | 中 | 中 | 高 |
| 包体积 | 较大 | 中 | 大 |
| 社区 | 活跃 | 活跃 | 活跃 |
| 适用场景 | 前端团队 | 小程序为主 | App 为主 |
5. 常见坏呓与解决
Taro 常见问题
1. 样式差异:小程序不支持部分 CSS 选择器,用 Taro 内置样式集或注意兼容性
2. 事件处理差异:小程序事件对象属性与 H5 不同,用内置组件
3. 包体积过大:按需加载、分包加载
4. Redux/Zustand 在微信小程序端需配置特殊适配
uni-app 常见问题
1. 条件编译容易遗忘:/* #ifdef MP-WEIXIN */ 由编译器处理
2. nvue 与 Vue 的样式差异: nvue 采用 Flex 布局,不支持所有 CSS
3. App 端原生能力需要通过 uts 或和 Native 插件实现
6. 资深面试题
- Taro 3.x 的运行时适配分导是什么思路?
- 小程序没有真实 DOM,运行时实现了一套虚拟 DOM API
- React/Vue 操作虚拟 DOM, Taro 将虚拟 DOM 映射为 setData 调用
- 比编译时转换灵活,但性能有损耗
- 如何选择跨端框架?
- 已有 React 团队且面向小程序 -> Taro
- 已有 Vue 团队且小程序为主 -> uni-app
- 面向 iOS/Android App,小程序不是主要 -> Flutter
- 需要高性能原生 App -> 纯原生开发
- 跨端框架如何处理平台差异?
- 条件编译(Taro: process.env.TARO_ENV,uni-app: #ifdef)
- 抓象平台能力到中间层,应用层不写平台相关代码
小程序性能深度优化实战
小程序性能优化深度
1. setData 性能优化
为什么 setData 影响性能?
- 逃逸层线程不能直接操作 DOM
- setData 数据经过序列化 -> Native 通信 -> 渲染层更新
- 数据量大、调用频繁就会成为瓶颈
优化实践
// 1. 只传必要的字段
const { list, count } = this.data
// bad
this.setData({ list: newList }) // 传辘1000条数据
// good
this.setData({ 'list[5].status': 'done' }) // 只传变化的字段
// 2. 减少 setData 次数(批量合并)
// bad
this.setData({ count: 1 })
this.setData({ loading: false })
// good
this.setData({ count: 1, loading: false })
// 3. 防抖高频调用(scroll 事件)
let timer
onScroll(e) {
clearTimeout(timer)
timer = setTimeout(() => {
this.setData({ scrollTop: e.detail.scrollTop })
}, 100)
}
// 4. 分屏渲染(分批设置大列表)
async function renderBatch(list) {
const BATCH = 20
for (let i = 0; i < list.length; i += BATCH) {
this.setData({ [`list`]: list.slice(0, i + BATCH) })
await new Promise(r => setTimeout(r, 16)) // 一帧的时间
}
}
2. 页面渲染优化
骨架屏
<!-- 骨架屏:首屏删叨剩留作平衡 -->
<view wx:if="{{loaded}}">
<real-content />
</view>
<view wx:else class="skeleton">
<view class="skeleton-avatar" />
<view class="skeleton-line" />
<view class="skeleton-line short" />
</view>
图片懒加载
<!-- 小程序内置支持 -->
<image lazy-load="{{true}}" src="{{item.url}}" />
<!-- 街道模式:Intersection Observer -->
// IntersectionObserver 实现懒加载
Page({
onReady() {
const observer = this.createIntersectionObserver()
observer.relativeToViewport({ bottom: 100 })
observer.observe('.load-more', (res) => {
if (res.intersectionRatio > 0) {
this.loadMore()
}
})
}
})
3. 分包策略深度
主包优化策略
{
"pages": [
"pages/index/index",
"pages/home/home"
],
"subpackages": [
{
"root": "packageUser",
"name": "user",
"pages": ["pages/profile", "pages/settings"]
},
{
"root": "packageOrder",
"name": "order",
"pages": ["pages/list", "pages/detail"]
}
],
"preloadRule": {
"pages/index/index": {
"network": "wifi",
"packages": ["user"]
}
}
}
分包最佳实践
- 主包只放起始页和公用组件、工具库
- 按功能模块划分分包
preloadRule配置分包预加载- 独立分包用于分享页、广告部置
4. 内存与渲染性能
内存优化
// 1. 图片内存管理——避免重复创建
const imageCache = new Map()
function getImage(url) {
if (!imageCache.has(url)) {
imageCache.set(url, wx.createImage ? wx.createImage() : null)
}
return imageCache.get(url)
}
// 2. 组件销毁时清理引用
Component({
lifetimes: {
detached() {
this._timer && clearInterval(this._timer)
this._observer && this._observer.disconnect()
}
}
})
// 3. 大数据分页加载,不要一次性读取全量
Page({
data: { list: [], page: 1, hasMore: true },
async loadMore() {
if (!this.data.hasMore) return
const res = await api.getList({ page: this.data.page })
this.setData({
list: [...this.data.list, ...res.data],
page: this.data.page + 1,
hasMore: res.hasMore
})
}
})
5. 小程序性能分析工具
1. 微信开发者工具 Audits 面板
- 体验评分:启动性能、页面渲染、用户体验
- 结合常见优化建se建议
2. Timeline / Performance 面板
- 录制页面操作过程
- 分析帧率、Script 耗时、Rendering 耗时
3. 真机调试
- 阴影模式测试(模拟低配置设备)
- 强制缓慢网络测试
6. 资深面试题
- 小程序首屏渲染优化思路?
- 减少主包体积,提卸加载到分包
- 首屏数据在 onLoad 中请求并打开下一页齐平行发起
- 马鼓属屏 + 骨架屏 + 首屏内容尽早请求
- 首屏外的图片延迟加载
- setData 渲染 1000 条数据如何优化?
- 虚拟列表组件(只渲染可视区域)
- 分屏渲染,每帧只 setData 一小批
- 尸体数据尽量小,只存 key 字段而非全量对象
- 小程序页面切换数据如何保存?
- 小程序没有路由層缓存,页面切换后重新创建
- 解决:app.globalData / Storage / 页面层参数传递
- Taro/uni-app 可配置页面缓存 (keepAlive)
React 面经
核心考点
1. Hooks 原理
- useState:基于链表结构存储状态,每次渲染按调用顺序取值,不能在条件/循环中使用
- useEffect:在 commit 阶段后异步执行;依赖数组为空时只在 mount 时执行;返回清理函数在下次 effect 执行前或 unmount 时调用
- useCallback / useMemo:依赖不变时跳过重新计算;useCallback 缓存函数引用,useMemo 缓存计算结果
- useRef:返回 mutable 对象,不触发重新渲染;常用于存访问 DOM 或保存跨渲染的值
2. Fiber 架构
- React 16 引入,将渲染拆成可中断的「工作单元」
- 两个阶段:Reconcile(可中断,生成 effect list)→ Commit(同步,不可中断,操作 DOM)
- 支持时间分片(Time Slicing)和并发特性(Concurrent Mode)
3. Virtual DOM & Diff 算法
- Diff 策略:同层对比 + key 复用
- key 相同类型相同 → 复用节点;key 不同 → 销毁重建
- 为什么不直接操作 DOM?批量更新 + 跨平台抽象
4. 状态管理对比
| 库 | 特点 |
|---|---|
| Redux | 单向数据流,中间件丰富,适合大型项目 |
| Zustand | 轻量,基于 hooks,无样板代码 |
| Jotai | 原子化状态,按需订阅,细粒度更新 |
| Recoil | Facebook 出品,selector 依赖图 |
5. 性能优化
React.memo包裹组件,跳过 props 未变化的渲染useMemo/useCallback缓存昂贵计算和回调- 列表渲染加稳定
key,避免不必要的重建 - 懒加载:
React.lazy+Suspense - 使用
useTransition标记非紧急更新
6. 高频面试题
- 合成事件 vs 原生事件:React 对原生事件封装,统一冒泡到 root 上处理
- 批量更新:React 18 默认开启 Automatic Batching,多次 setState 合并为一次渲染
- 为什么不能在循环中用 Hooks:Hooks 靠调用顺序与 fiber 节点一一对应
- 受控 vs 非受控组件:受控组件状态由 React 管理;非受控用 ref 读取 DOM 值
💡 建议结合源码阅读 React 官方文档 react.dev 加深理解
React 性能优化实战
性能优化全景
1. 渲染优化
避免不必要的重渲染
React.memo(Component, areEqual)— 对比 props,默认浅比较- 父组件回调用
useCallback稳定引用,防止子组件因回调引用变化重渲染 - 将「变化的状态」下沉到最小作用域的子组件,减少影响范围
拆分组件粒度
// ❌ 状态变化导致整个列表重渲染
function Page() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<HeavyList /> {/* 每次都重渲染 */}
</>
)
}
// ✅ 将 count 状态隔离
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}
function Page() {
return (<><Counter /><HeavyList /></>)
}
2. 列表虚拟化
- 数据量 > 500 条时使用
react-window或react-virtual - 只渲染可视区域内的元素,降低 DOM 节点数
- 配合
overscanCount预渲染边缘元素,避免滚动白屏
3. 代码分割与懒加载
// 路由级分割
const Dashboard = React.lazy(() => import('./Dashboard'))
function App() {
return (
<Suspense fallback={<Skeleton />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
)
}
import()动态导入 +React.lazy— 路由/弹窗级切割webpackmagic comment:/* webpackChunkName: "xxx" */命名 chunk- 预加载:鼠标 hover 时触发
import()
4. 并发特性(React 18)
| API | 作用 |
|---|---|
useTransition | 将状态更新标记为「非紧急」,保持 UI 响应 |
useDeferredValue | 延迟某个值的更新,类似防抖 |
startTransition | 在事件处理器外触发低优先级更新 |
const [isPending, startTransition] = useTransition()
startTransition(() => {
setSearchQuery(value) // 低优先级,不阻塞输入
})
5. 状态管理性能
- Context 拆分:避免把所有状态放一个 Context,按功能拆分降低订阅范围
- Context + useMemo:value 对象需 memoize,否则每次渲染都是新引用
- Zustand selector:
useStore(state => state.count)只订阅用到的切片 - Jotai 原子化:精确更新,只有依赖该 atom 的组件重渲染
6. 工具与度量
- React DevTools Profiler — 录制渲染时间,找到耗时组件
- Why Did You Render — 检测不必要的重渲染并输出原因
- Lighthouse — 页面整体性能分
- Web Vitals — LCP / FID / CLS 核心指标监控
7. 高频面试题
- memo 一定有收益吗? 浅比较本身有成本,仅对渲染耗时高的组件使用
- 如何定位慢渲染? Profiler 找 commit 时长 > 16ms 的组件,再缩小范围
- Suspense 与错误边界的关系? Suspense 处理加载态,ErrorBoundary 处理错误态,两者搭配
- 并发渲染会破坏 useEffect 顺序吗? 不会,effect 仍在 commit 后同步运行
React 源码解析(Fiber & Reconciler)
源码核心模块
1. Fiber 数据结构
type Fiber = {
tag: WorkTag // 节点类型(FunctionComponent/ClassComponent/HostComponent...)
key: null | string
type: any // 对应 createElement 的第一个参数
stateNode: any // DOM 节点 或 class 实例
return: Fiber | null // 父节点
child: Fiber | null // 第一个子节点
sibling: Fiber | null // 下一个兄弟节点
pendingProps: any // 本次渲染的 props
memoizedProps: any // 上次渲染的 props
memoizedState: any // hooks 链表的头节点
flags: Flags // 副作用标记(Placement/Update/Deletion)
subtreeFlags: Flags
updateQueue: any // 状态更新队列
}
2. 双缓冲机制
- React 同时维护两棵 Fiber 树:current tree(当前屏幕)和 workInProgress tree(构建中)
- 渲染完成后
root.current = workInProgress,两棵树交替复用 - 好处:构建过程可中断,不影响当前显示内容
3. Reconcile 阶段(beginWork & completeWork)
深度优先遍历 Fiber 树
↓
beginWork:根据 type 处理节点(diff children,打 flags)
↓
若有子节点 → 递归处理子节点
若无子节点 → completeWork:创建/更新 DOM,收集 subtreeFlags
↓
返回父节点继续 completeWork
Diff 策略
- 单节点 Diff:key 和 type 都相同 → 复用;否则销毁重建
- 多节点 Diff:两轮遍历
- 第一轮:按顺序对比,遇到 key 不同则停止
- 第二轮:将剩余旧节点放入 Map,用 key 查找可复用节点
- 最长递增子序列优化移动操作(React 18)
4. Commit 阶段(三个子阶段)
| 子阶段 | 工作内容 |
|---|---|
| BeforeMutation | 读取 DOM 快照,调用 getSnapshotBeforeUpdate |
| Mutation | 操作真实 DOM(增/改/删),调用 useLayoutEffect 清理 |
| Layout | 调用 useLayoutEffect 回调,更新 ref |
useEffect在 commit 后异步调度,不阻塞浏览器绘制useLayoutEffect在 Layout 阶段同步执行,会阻塞绘制
5. Hooks 实现原理
Fiber.memoizedState → Hook₁ → Hook₂ → Hook₃ (链表)
↓
{ memoizedState, queue, next }
- mount 阶段:每个
useState在链表末尾插入新 Hook 节点 - update 阶段:按顺序取链表节点,计算新状态
- 为什么不能条件渲染 Hooks:顺序错位会取到错误的 Hook 节点
6. 调度器(Scheduler)
- 基于
MessageChannel实现任务切片(非 requestAnimationFrame) - 每帧预算 5ms,超出则中断,让出主线程
- 优先级队列:ImmediatePriority > UserBlockingPriority > NormalPriority > IdlePriority
lane模型(React 18):用位运算表示优先级,支持批量处理多个优先级
7. 高频面试题
- 为什么要用 MessageChannel 而非 setTimeout? setTimeout 最小延迟 ~4ms,MessageChannel 延迟更低
- useEffect 清理函数什么时候执行? 下次 effect 执行前 或 组件 unmount 时
- Concurrent Mode 如何保证状态一致性? 同一优先级的更新批量处理,低优先级更新被高优先级打断后重新计算
React 生态与工程化
React 生态全景
1. 路由:React Router v6
- 嵌套路由:
<Outlet />渲染子路由 - loader / action:数据加载和表单提交(Data Router 模式)
- 懒加载路由:
lazy: () => import('./Page')配合<Suspense> - useNavigate / useParams / useSearchParams — 常用 hooks
2. 服务端渲染(SSR)与 Next.js
| 渲染模式 | 说明 | 适用场景 |
|---|---|---|
| SSG(静态生成) | 构建时生成 HTML | 博客、文档 |
| SSR(服务端渲染) | 请求时生成 HTML | 动态内容 |
| ISR(增量静态再生) | 后台定时更新静态页 | 电商、新闻 |
| CSR(客户端渲染) | 浏览器渲染 | 后台管理系统 |
Next.js App Router 核心概念
- Server Components(默认):零 JS 发送到客户端
- Client Components:
'use client'标记,保留交互能力 - Server Actions:表单/变更直接调用服务端函数
3. 数据请求:React Query / SWR
// React Query 示例
const { data, isLoading, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
staleTime: 5 * 60 * 1000, // 5 分钟内认为数据新鲜
})
- 自动缓存与去重:相同 queryKey 的请求只发一次
- 后台刷新:窗口重新聚焦时自动 refetch
- 乐观更新:
onMutate先更新 UI,失败后回滚 - 无限滚动:
useInfiniteQuery+fetchNextPage
4. 样式方案对比
| 方案 | 特点 | 代表库 |
|---|---|---|
| CSS Modules | 局部作用域,零运行时 | 内置支持 |
| CSS-in-JS | 动态样式,有运行时开销 | styled-components, Emotion |
| Atomic CSS | 极致复用,HTML 类名多 | Tailwind CSS |
| Zero-runtime | 编译期提取,无运行时 | Linaria, vanilla-extract |
5. 表单方案:React Hook Form
const { register, handleSubmit, formState: { errors } } = useForm()
<input {...register('email', { required: true, pattern: /^\S+@\S+$/ })} />
{errors.email && <span>邮箱格式错误</span>}
- 非受控模式 + ref 收集值,避免每次输入触发重渲染
- 配合
zod/yup做 schema 验证
6. 测试工具链
- Vitest / Jest — 单元测试
- React Testing Library — 组件集成测试(关注行为而非实现)
- Playwright / Cypress — E2E 测试
// RTL 示例
test('点击按钮增加计数', async () => {
render(<Counter />)
await userEvent.click(screen.getByRole('button', { name: /增加/ }))
expect(screen.getByText('1')).toBeInTheDocument()
})
7. 高频面试题
- Server Component 与 Client Component 如何混用? SC 可以导入 CC,但 CC 不能导入 SC
- React Query 与 Redux 的区别? RQ 专注服务端状态(异步/缓存),Redux 管理客户端状态
- Next.js 如何做 SEO?
generateMetadata动态 meta,SSG/SSR 保证爬虫可读内容 - monorepo 下 React 多版本共存的问题? 用
peerDependencies+ 包管理器 alias 解决
React 并发渲染与 Streaming SSR 深入
并发模式核心机制
1. Time Slicing 时间切片
- Scheduler 将渲染任务切成 5ms 小片,每片执行完检查是否有高优先级任务
- 利用 MessageChannel 而非 setTimeout,避免 4ms 最小延迟
- 有更高优先级任务(如用户输入)则中断当前渲染,先处理紧急任务
2. Lane 优先级模型(React 18)
// 简化版 lane 定义(用二进制位表示优先级)
const SyncLane = 0b0000001 // 最高:同步
const InputContinuousLane = 0b000100 // 用户连续输入(scroll/drag)
const DefaultLane = 0b010000 // 普通更新
const TransitionLane = 0b001000000 // transition 标记的更新
const IdleLane = 0b100000000000000 // 最低:空闲
// 多个优先级合并
const pendingLanes = SyncLane | TransitionLane
// 取最高优先级
const nextLanes = getHighestPriorityLanes(pendingLanes)
3. Automatic Batching(自动批量更新)
// React 17 之前:setTimeout 中不批量
setTimeout(() => {
setCount(c => c + 1) // 触发一次渲染
setFlag(f => !f) // 再触发一次渲染
}, 1000)
// React 18:所有场景自动批量
setTimeout(() => {
setCount(c => c + 1)
setFlag(f => !f)
// 只触发一次渲染!
}, 1000)
// 需要强制非批量时
import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1)) // 立即渲染
4. useTransition 与 useDeferredValue
// useTransition:标记低优先级状态更新
const [isPending, startTransition] = useTransition()
function handleSearch(value: string) {
setInputValue(value) // 高优先级:立即更新输入框
startTransition(() => {
setSearchQuery(value) // 低优先级:可被中断
})
}
// isPending 可用于显示过渡状态
return (
<>
<input value={inputValue} onChange={e => handleSearch(e.target.value)} />
{isPending ? <Spinner /> : <SearchResults query={searchQuery} />}
</>
)
// useDeferredValue:延迟某个值的更新,类似防抖但不固定时间
function SearchPage() {
const [query, setQuery] = useState('')
const deferredQuery = useDeferredValue(query)
const isStale = query !== deferredQuery
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
<Suspense fallback={<Loading />}>
<Results query={deferredQuery} />
</Suspense>
</div>
)
}
5. Streaming SSR
传统 SSR vs Streaming SSR
传统 SSR(All-or-Nothing):
服务端获取所有数据 -> 渲染完整 HTML -> 发送 -> 客户端 Hydration
瓶颈:必须等最慢的数据请求完成才能发送第一个字节
Streaming SSR(React 18):
服务端流式发送 HTML -> 客户端逐步 Hydration
优势:TTFB 更低,用户更快看到内容
// 服务端使用 renderToPipeableStream
import { renderToPipeableStream } from 'react-dom/server'
const { pipe, abort } = renderToPipeableStream(
<App />,
{
bootstrapScripts: ['/static/js/main.js'],
onShellReady() {
// Shell 就绪(不含 Suspense 边界内容)立即发送
response.statusCode = 200
response.setHeader('Content-type', 'text/html')
pipe(response)
},
onError(error) {
response.statusCode = 500
console.error(error)
}
}
)
// 客户端组件使用 Suspense 分割边界
function App() {
return (
<Layout> {/* Shell:立即渲染 */}
<Suspense fallback={<ArticleSkeleton />}>
<Article /> {/* 数据就绪后流式注入 */}
</Suspense>
<Suspense fallback={<CommentsSkeleton />}>
<Comments /> {/* 独立加载,不阻塞其他 */}
</Suspense>
</Layout>
)
}
6. Selective Hydration(选择性水合)
- 用户与某个还未 hydrate 的组件交互时,React 优先 hydrate 该组件
- 其他组件继续排队,不会阻塞用户交互
- 需要配合
React.lazy+Suspense使用
7. Server Components 数据流
1. 请求到达服务端
2. Server Components 执行(可直接访问 DB)
3. 生成 RSC Payload(特殊序列化格式,非 HTML)
4. Client Components 占位符插入 Payload
5. Payload 流式传输到客户端
6. React 在客户端重建组件树并 Hydrate Client Components
RSC Payload 格式示例:
0:["$","div",null,{"children":["$","$L1","1",{}]}]
1:I{"id":"./ClientComponent","chunks":["client"]}
8. 资深面试题
- useSyncExternalStore 解决了什么问题?
- 并发渲染中,一次渲染可能被中断后重新执行,期间外部 store 可能变化导致 UI 撕裂(tearing)
useSyncExternalStore强制同步读取,确保一次渲染内数据一致
- startTransition 如何实现「可中断」?
- 更新被标记为 TransitionLane(低优先级)
- 执行过程中如有高优先级更新(SyncLane)到来,当前渲染丢弃重新来过
- 因此 transition 内的更新可能执行多次,需保证纯函数
- Streaming SSR 中如何处理 SEO?
- Shell(包含关键内容)立即发送,搜索引擎可读取
- 关键 meta 信息放在 Shell 中,非关键内容放在 Suspense 边界内
- Google 支持 JavaScript 渲染,但 Streaming 方式更友好
React 状态管理深度对比
状态管理方案全面对比
1. Redux Toolkit(RTK)
// slice 定义
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
export const fetchUser = createAsyncThunk(
'user/fetchById',
async (userId: string, { rejectWithValue }) => {
try {
return await api.getUser(userId)
} catch (err) {
return rejectWithValue(err.message)
}
}
)
const userSlice = createSlice({
name: 'user',
initialState: { data: null, loading: false, error: null } as UserState,
reducers: {
clearUser: state => { state.data = null } // Immer 支持「可变」写法
},
extraReducers: builder => {
builder
.addCase(fetchUser.pending, state => { state.loading = true })
.addCase(fetchUser.fulfilled, (state, action) => {
state.loading = false
state.data = action.payload
})
.addCase(fetchUser.rejected, (state, action) => {
state.loading = false
state.error = action.payload as string
})
}
})
RTK Query(数据获取层)
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['User', 'Post'],
endpoints: builder => ({
getUser: builder.query<User, string>({
query: id => `/users/${id}`,
providesTags: (result, err, id) => [{ type: 'User', id }]
}),
updateUser: builder.mutation<User, Partial<User>>({
query: ({ id, ...body }) => ({ url: `/users/${id}`, method: 'PUT', body }),
invalidatesTags: (result, err, { id }) => [{ type: 'User', id }]
})
})
})
// 组件中使用
const { data: user, isLoading } = useGetUserQuery(userId)
const [updateUser, { isLoading: isUpdating }] = useUpdateUserMutation()
2. Zustand
// 轻量,基于 hooks,无需 Provider
import { create } from 'zustand'
import { devtools, persist, immer } from 'zustand/middleware'
const useUserStore = create<UserStore>()(
devtools(
persist(
immer(set => ({
user: null,
loading: false,
fetchUser: async (id: string) => {
set(state => { state.loading = true })
const user = await api.getUser(id)
set(state => { state.user = user; state.loading = false })
},
clearUser: () => set({ user: null })
})),
{ name: 'user-storage' } // 自动持久化到 localStorage
),
{ name: 'UserStore' } // DevTools 中显示的名称
)
)
// 精确订阅(避免不必要的重渲染)
const user = useUserStore(state => state.user)
const loading = useUserStore(state => state.loading)
// 不要:const { user, loading } = useUserStore() 会订阅整个 store
Zustand 的切片模式(大型应用)
// 将 store 拆成多个切片组合
const useUserSlice = (set, get) => ({
user: null,
setUser: (user) => set({ user })
})
const useCartSlice = (set, get) => ({
items: [],
addItem: (item) => set(state => ({ items: [...state.items, item] }))
})
const useBoundStore = create((...a) => ({
...useUserSlice(...a),
...useCartSlice(...a)
}))
3. Jotai(原子化状态)
import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai'
import { atomWithQuery } from 'jotai-tanstack-query'
// 基础 atom
const countAtom = atom(0)
const doubleAtom = atom(get => get(countAtom) * 2) // 派生 atom
// 异步 atom
const userAtom = atomWithQuery(get => ({
queryKey: ['user', get(userIdAtom)],
queryFn: () => fetchUser(get(userIdAtom))
}))
// 只订阅需要的 atom,精确更新
function Counter() {
const [count, setCount] = useAtom(countAtom)
const double = useAtomValue(doubleAtom) // 只读
// ...
}
4. 方案选型指南
| 场景 | 推荐方案 |
|---|---|
| 大型企业应用,需要 DevTools 调试 | Redux Toolkit |
| 中型应用,追求简洁 | Zustand |
| 细粒度更新,服务端状态 | Jotai + React Query |
| 纯服务端状态管理 | React Query / SWR |
| 简单全局状态 | Context + useReducer |
5. Context 性能优化
// 问题:Context 值变化导致所有消费者重渲染
const ThemeContext = createContext(null)
// 方案1:拆分 Context(读写分离)
const ThemeValueContext = createContext(null)
const ThemeDispatchContext = createContext(null)
function ThemeProvider({ children }) {
const [theme, dispatch] = useReducer(reducer, initialState)
return (
<ThemeValueContext.Provider value={theme}>
<ThemeDispatchContext.Provider value={dispatch}>
{children}
</ThemeDispatchContext.Provider>
</ThemeValueContext.Provider>
)
}
// 方案2:useMemo 稳定 value 引用
function Provider({ children }) {
const [user, setUser] = useState(null)
const value = useMemo(() => ({ user, setUser }), [user])
return <Ctx.Provider value={value}>{children}</Ctx.Provider>
}
// 方案3:使用 use-context-selector 精确订阅
import { createContext, useContextSelector } from 'use-context-selector'
const user = useContextSelector(AppContext, ctx => ctx.user)
6. 资深面试题
- Redux 与 Zustand 的根本区别?
- Redux:单一 store,严格的单向数据流,action -> reducer -> state
- Zustand:多 store,直接更新 state,无 action 约束,更灵活
- Redux 更适合需要 time-travel 调试、复杂中间件的场景
- 为什么说 React Query 不是状态管理工具?
- React Query 管理的是服务端状态的缓存和同步
- 状态管理工具管理的是纯客户端状态
- 两者职责不同,可以共存使用
- Jotai 如何解决 Context 的性能问题?
- 每个 atom 是独立的 Context,消费者只订阅用到的 atom
- 使用 WeakMap 存储 atom 值,不同组件订阅不同 atom 互不影响
- 派生 atom 只在依赖变化时重新计算
React 大型应用架构设计
大型 React 应用设计原则
1. Feature-Sliced Design 架构
src/
app/ # 应用层:路由、Provider、全局样式
pages/ # 页面层:组合 widgets 组成页面
widgets/ # 组件层:独立的页面区块(Header、Sidebar)
features/ # 功能层:用户操作(点赞、搜索、评论)
entities/ # 实体层:业务对象(User、Article、Order)
shared/ # 共享层:UI 组件库、工具函数、API 客户端
依赖规则:上层可引用下层,同层之间不能随意引用。
模块边界:每个 slice 只通过 index.ts 暴露公开 API。
2. 状态分层管理
服务端状态(Server State)
-> 来自 API,有缓存、失效、同步问题
-> React Query / SWR / RTK Query
客户端状态(Client State)
-> UI 状态(弹窗开关、选中项)
-> useState / useReducer / Zustand
表单状态(Form State)
-> 表单值、验证、提交
-> React Hook Form + Zod
URL 状态(URL State)
-> 搜索参数、分页、筛选条件
-> useSearchParams / nuqs 库
关键原则:不要把服务端状态放进 Redux/Zustand,React Query 专门处理异步状态。
3. 代码分割策略
// 路由级分割(必做)
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Analytics = lazy(() => import('./pages/Analytics'))
// 功能级分割(大功能模块)
const RichEditor = lazy(() => import('./features/editor/RichEditor'))
// 预加载策略(hover 时触发)
const prefetchDashboard = () => import('./pages/Dashboard')
<Link
to="/dashboard"
onMouseEnter={prefetchDashboard} // 鼠标悬停时预加载
>
Dashboard
</Link>
4. 组件设计模式
Compound Components(复合组件)
// 用于构建高度可定制的组件
const Select = ({ children, value, onChange }) => {
return (
<SelectContext.Provider value={{ value, onChange }}>
<div className="select">{children}</div>
</SelectContext.Provider>
)
}
Select.Option = ({ value, children }) => {
const { value: selected, onChange } = useSelectContext()
return (
<div
className={selected === value ? 'selected' : ''}
onClick={() => onChange(value)}
>
{children}
</div>
)
}
// 使用
<Select value={val} onChange={setVal}>
<Select.Option value="a">Option A</Select.Option>
<Select.Option value="b">Option B</Select.Option>
</Select>
Render Props / Children as Function
// 将渲染逻辑控制权交给使用者
function DataProvider({ render }) {
const [data] = useFetch('/api/data')
return render(data)
}
// 使用:自定义渲染
<DataProvider render={data => <CustomChart data={data} />} />
useImperativeHandle 暴露命令式接口
const Dialog = forwardRef((props, ref) => {
const [open, setOpen] = useState(false)
useImperativeHandle(ref, () => ({
open: () => setOpen(true),
close: () => setOpen(false),
}))
return open ? <div>{props.children}</div> : null
})
// 使用
const dialogRef = useRef(null)
<Dialog ref={dialogRef} />
<button onClick={() => dialogRef.current.open()}>打开</button>
5. Monorepo 组织
packages/
ui/ # 共享 UI 组件库(Design System)
utils/ # 工具函数
types/ # 共享 TypeScript 类型
api-client/ # API 客户端(自动生成)
config/ # 共享配置(ESLint、TS、Tailwind)
apps/
web/ # 主应用
admin/ # 管理后台
mobile/ # React Native 应用
工具链:Turborepo(增量构建缓存)+ pnpm workspaces
版本管理:内部包用 workspace:*,公共包用 Changesets
6. 测试体系
// 单元测试:纯函数、hooks、工具
test('useCounter', () => {
const { result } = renderHook(() => useCounter(0))
act(() => result.current.increment())
expect(result.current.count).toBe(1)
})
// 集成测试:组件 + API Mock(MSW)
test('登录成功后跳转首页', async () => {
server.use(
http.post('/api/login', () => HttpResponse.json({ token: 'abc' }))
)
render(<LoginPage />, { wrapper: Providers })
await userEvent.click(screen.getByRole('button', { name: '登录' }))
await expect(screen.findByText('欢迎回来')).resolves.toBeInTheDocument()
})
// E2E(Playwright):核心用户旅程
test('完整购买流程', async ({ page }) => {
await page.goto('/products')
await page.click('[data-testid=add-to-cart]')
await page.goto('/checkout')
await page.fill('[name=card]', '4242 4242 4242 4242')
await page.click('button[type=submit]')
await expect(page.getByText('订单已确认')).toBeVisible()
})
7. 资深面试题
- 如何设计一个跨团队复用的组件库?
- 严格的语义化版本 + Changesets 管理
- Storybook 文档 + 视觉回归测试(Chromatic)
- Compound Component 模式提供灵活性
- 组件 API 向后兼容,breaking change 走大版本
- 微前端架构中 React 应用如何共享状态?
- CustomEvent / window.postMessage(松耦合)
- Module Federation 共享单例 store
- URL 作为通信载体(筛选条件、路由参数)
- 大型 React 应用如何做权限控制?
- 路由级:
<PrivateRoute>包裹需鉴权路由 - 组件级:
usePermission('edit:post')hook - 数据级:服务端返回时过滤,前端只做展示判断
- Feature Flag:LaunchDarkly 实现灰度和 A/B 测试
- 路由级:
前端工程化与性能监控面经
前端工程化核心知识
1. Webpack 深度
核心概念
Entry -> Loader 处理 -> Plugin 阁 -> Bundle 输出
Loader:转换单个文件(babel-loader / css-loader / file-loader)
Plugin:串联整个构建生命周期(HtmlWebpackPlugin / MiniCssExtractPlugin)
Resolver:模块寻址解析
Tree Shaking 原理
- 基于 ESM 静态分析模块导入导出
- Webpack 标记未使用的导出为 unused exports
- Terser 在压缩时删除未使用代码
sideEffects: false可帮助 Webpack 更激进地删除
Code Splitting
// 1. 入口分割
entry: { main: './src/index.js', vendor: './src/vendor.js' }
// 2. 动态导入
const LazyComp = () => import('./HeavyComponent')
// 3. SplitChunksPlugin 公共依赖提取
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /node_modules/,
name: 'vendors',
chunks: 'all'
}
}
}
}
Webpack vs Vite 对比
| 对比项 | Webpack | Vite |
|---|---|---|
| 开发启动 | 打包所有模块 | ESM 按需加载 |
| HMR 速度 | 所有模块出发 | 只有变化的模块 |
| 生产构建 | Webpack | Rollup |
| 配置复杂度 | 高 | 低 |
| 适用场景 | 复杂大型 | 中小型,新项目 |
2. Vite 深度
开发模式原理
1. 浏览器请求模块
2. Vite Dev Server 收到请求
3. 对模块应用转换(如 TypeScript 编译)
4. 返回转换后的模块
-> 无需打包,启动速度极快
预构建依赖
- 第一次启动时用 esbuild 预构建 node_modules
- esbuild 是 Go 写的,比 Babel 快 10-100x
- 预构建结果缓存在 node_modules/.vite 中
自定义插件
// vite.config.ts
export default {
plugins: [
{
name: 'my-transform',
transform(code, id) {
if (id.endsWith('.svg')) {
return `export default ${JSON.stringify(code)}`
}
}
}
]
}
3. CI/CD 流水线
# GitHub Actions 示例
name: Deploy
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm lint
- name: Test
run: pnpm test --coverage
- name: Build
run: pnpm build
env:
VITE_API_BASE: ${{ secrets.API_BASE }}
- name: Deploy to CDN
run: pnpm deploy
4. Web 性能监控
Core Web Vitals
| 指标 | 全称 | 良好阈值 | 优化策略 |
|---|---|---|---|
| LCP | 最大内容绘制 | < 2.5s | SSR、图片优先级、CDN |
| INP | 交互到下一帧 | < 200ms | 少 JS、Web Worker |
| CLS | 累积布局偏移 | < 0.1 | 图片设宽高、字体预加载 |
Performance API
// 页面性能采样
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.entryType === 'largest-contentful-paint') {
console.log('LCP:', entry.startTime)
}
if (entry.entryType === 'layout-shift') {
console.log('CLS score:', entry.value)
}
})
})
observer.observe({ type: 'largest-contentful-paint', buffered: true })
observer.observe({ type: 'layout-shift', buffered: true })
// Navigation Timing
const timing = performance.timing
const pageLoad = timing.loadEventEnd - timing.navigationStart
const domReady = timing.domContentLoadedEventEnd - timing.navigationStart
const ttfb = timing.responseStart - timing.requestStart
前端监控体系
// 错误监控
window.addEventListener('error', (event) => {
sendToSentry({
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error?.stack
})
})
window.addEventListener('unhandledrejection', (event) => {
sendToSentry({ message: event.reason?.message })
})
// 资源监控
const resourceObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (entry.duration > 1000) {
// 资源加载过慢
reportSlowResource(entry)
}
})
})
resourceObserver.observe({ type: 'resource', buffered: true })
5. 包体积分析
# Webpack Bundle Analyzer
npx webpack-bundle-analyzer stats.json
# Vite 分析
npx vite-bundle-visualizer
# 常见优化手段
1. 添加 lodash 用 lodash-es 替代
2. moment.js 用 dayjs 替代(体积约 80% 小)
3. echarts 按需导入组件,不要全量导入
4. 图片用 WebP + 懒加载
5. 富文本编辑器就答动态加载
6. 资深面试题
- Webpack 的 HMR 原理?
- Dev Server 与浏览器建立 WebSocket
- 文件变化时,重新编译对应模块
- 向浏览器推送更新后的模块 hash
- 浏览器下载新模块,通过
module.hot.accept回调操作
- Vite 为什么开发时很快但构建时还用 Rollup?
- 开发时利用浏览器原生 ESM,无需打包
- 生产构建需要 Tree Shaking、代码分割、压缩等 Rollup 的能力
- 未来 Rolldown(Rust 实现)将统一开发和生产
- 如何实现前端性能监控平台?
- Performance API 采频 Core Web Vitals
- 错误监控捕获未处理异常
- User Timing API 自定义性能埋点
- 上报到分析平台(自建 + 各种 SaaS)
- Webpack 构建慢如何优化?
cache: { type: 'filesystem' }文件系统缓存thread-loader并行处理 Loaderbabel-loader配置include: /src/缩小范围- 将稳定依赖定义为 外部扩展 externals + CDN
iOS 面经
核心考点
1. 内存管理 (ARC)
- ARC(Automatic Reference Counting):编译器自动插入 retain/release
- 强引用循环:两个对象互相强引用导致无法释放
- 解决:使用
weak(不增加引用计数,置 nil)或unowned(不置 nil,对象必须比引用者活得长) - 闭包捕获列表:
[weak self]或[unowned self]避免循环
2. RunLoop
- 事件循环机制,有任务则处理,无任务则休眠
- Mode:主线程 RunLoop 有 Default、UITrackingRunLoopMode、Common Modes
- 应用场景:Timer、常驻线程、滚动期间暂停图片加载
3. 多线程
| 方式 | 特点 |
|---|---|
| GCD | 轻量,推荐,任务+队列模型 |
| OperationQueue | 面向对象,支持依赖关系和取消 |
| Thread | 底层,需手动管理 |
- 串行队列:任务顺序执行;并发队列:任务并发执行
- 主队列(串行):UI 操作必须在主线程
- 死锁:主线程同步派发任务到主队列
4. Swift 特性
- Optional:显式处理 nil,
?可选链,!强解包,guard let/if let安全解包 - Protocol + Extension:面向协议编程,替代继承实现代码复用
- Value Types vs Reference Types:struct/enum 是值类型(栈),class 是引用类型(堆)
- Codable:自动序列化/反序列化 JSON
- async/await:Swift 5.5 引入,结构化并发
5. UIKit vs SwiftUI
| 对比 | UIKit | SwiftUI |
|---|---|---|
| 范式 | 命令式 | 声明式 |
| 最低支持 | iOS 2+ | iOS 13+ |
| 生态 | 成熟完整 | 快速发展 |
| 调试 | 较复杂 | Preview 实时预览 |
6. 高频面试题
- Category vs Extension:Category 运行时添加方法(不能加存储属性);Extension 编译期扩展(可加存储属性,仅限当前模块)
- KVO / KVC:键值观察/编码,底层通过 isa-swizzling 实现
- App 生命周期:Not Running → Inactive → Active → Background → Suspended
- Instruments 性能分析:Time Profiler(CPU)、Allocations(内存)、Leaks
- Autolayout 原理:Cassowary 约束求解算法
iOS 性能优化专题
iOS 性能优化全攻略
1. 渲染性能
离屏渲染
- 触发条件:圆角(masksToBounds)、阴影(shadow)、遮罩(mask)、光栅化
- 代价:GPU 需额外开辟缓冲区,切换上下文耗性能
- 优化:
- 圆角 → 用图片遮罩或 Core Graphics 画圆角背景
- 阴影 → 设置
shadowPath避免实时计算 - 静态内容 →
shouldRasterize = true(开启缓存,但内存增加)
卡顿检测
- CADisplayLink 监听帧率
- Instruments → Core Animation 查看 FPS 和离屏渲染
- RunLoop 观察者监测主线程超时(> 16ms 则记录堆栈)
2. 列表优化(UITableView / UICollectionView)
- Cell 复用:必须使用
dequeueReusableCell - 高度缓存:计算后缓存
heightForRow,避免重复计算 - 异步渲染:图片/富文本在子线程渲染,主线程只做展示(Texture/AsyncDisplayKit)
- 预计算:
prefetchDataSource在 cell 滚动到可视区前提前加载数据 - 图片下载:SDWebImage / Kingfisher 自带磁盘+内存缓存
3. 启动优化
冷启动阶段
pre-main 阶段 (dyld)
→ 加载动态库(减少动态库数量,合并/静态化)
→ Rebase / Binding(减少 ObjC 元数据:类/方法/分类)
→ Initializer(+load 方法,改用 +initialize 或 dispatch_once)
main() 之后
→ didFinishLaunching(延迟/异步初始化三方 SDK)
→ 首屏渲染
- 用
DYLD_PRINT_STATISTICS测量 pre-main 耗时 - 减少
+load方法(每个都在启动时执行) - 二进制重排(Clang 插桩 + 收集 order 文件)减少 Page Fault
4. 内存优化
- 图片内存:大图按需缩放,
UIImage(named:)会缓存,UIImage(contentsOfFile:)不缓存 - 循环引用排查:Instruments → Leaks / Allocations
- autoreleasepool:循环处理大量对象时手动加
@autoreleasepool {} - NSCache:系统内存紧张时自动清理,比 NSMutableDictionary 更合适
5. 网络优化
- HTTP/2:URLSession 默认支持,多路复用
- 请求合并:批量接口减少请求次数
- 图片格式:WebP / HEIF 替代 JPEG
- 离线缓存:URLCache + ETag / Last-Modified 协商缓存
- 压缩:Gzip / Brotli 压缩响应体
6. 包体积优化
- 资源优化:Asset Catalog 压缩,移除未使用资源(LSUnusedResources)
- 代码优化:
-dead_strip删除未引用代码 - 动态库转静态库:减少包体积和启动时间
- On-Demand Resources:按需下载资源,减少初始包大小
- App Thinning:Slicing(按设备分包)+ Bitcode
7. 高频面试题
- 界面卡顿的本质? 主线程 RunLoop 一次循环超过 16.7ms,帧率低于 60fps
- 为什么图片圆角会触发离屏渲染? 需要裁剪内容,必须在离屏缓冲区合成
- Instruments 如何定位内存泄漏? Leaks 工具 → 查看泄漏对象 → Allocation 追踪引用链
- 二进制重排原理? 通过收集启动期函数调用顺序写入 link map,让相关代码页相邻,减少 Page Fault
SwiftUI 深入面经
SwiftUI 核心知识
1. 数据流与状态管理
| 属性包装器 | 用途 | 作用域 |
|---|---|---|
@State | 视图私有状态 | 当前视图 |
@Binding | 双向绑定父视图状态 | 父→子 |
@StateObject | 创建并持有 ObservableObject | 当前视图拥有 |
@ObservedObject | 观察外部 ObservableObject | 外部传入 |
@EnvironmentObject | 环境中注入的共享对象 | 整棵子树 |
@Environment | 读取系统环境值(colorScheme 等) | 整棵子树 |
class UserStore: ObservableObject {
@Published var name = "Alice"
@Published var isLoggedIn = false
}
struct ContentView: View {
@StateObject private var store = UserStore()
var body: some View {
NavigationView {
ProfileView()
}
.environmentObject(store)
}
}
2. View 更新机制
- SwiftUI 通过比较
View的值(struct)来决定是否重新渲染 @State变化 → 视图重新body计算 → diff → 更新必要部分- EquatableView:手动实现
Equatable,跳过不必要的重计算 - identity:通过
id(...)或条件视图的位置确定视图标识
3. Swift Data & Core Data 集成
// Swift Data(iOS 17+)
@Model class User {
var name: String
var age: Int
init(name: String, age: Int) { ... }
}
struct UserList: View {
@Query var users: [User]
var body: some View {
List(users) { user in Text(user.name) }
}
}
4. 动画系统
// 隐式动画
withAnimation(.spring(response: 0.3)) {
isExpanded.toggle()
}
// 显式动画(精确控制哪个值动画)
Text("Hello")
.scaleEffect(scale)
.animation(.easeInOut, value: scale)
// matchedGeometryEffect(英雄动画)
@Namespace var hero
Image(...).matchedGeometryEffect(id: "avatar", in: hero)
5. 性能优化
- 避免大 body:拆分子视图,SwiftUI 可精准更新子树
- lazy 容器:
LazyVStack/LazyVGrid— 按需加载 - task / onAppear 加载数据:
.task {}自动绑定到视图生命周期 - Instruments → SwiftUI:查看 body 调用次数和渲染时间
6. UIKit 互操作
// SwiftUI 使用 UIKit 组件
struct MapView: UIViewRepresentable {
func makeUIView(context: Context) -> MKMapView { MKMapView() }
func updateUIView(_ view: MKMapView, context: Context) { ... }
}
// UIKit 使用 SwiftUI
let vc = UIHostingController(rootView: ProfileView())
7. 高频面试题
- @StateObject 和 @ObservedObject 的区别?
@StateObject:视图创建并拥有对象,视图刷新不重建@ObservedObject:外部传入,视图刷新可能重建(慎用)
- SwiftUI 如何做列表性能优化?
LazyVStack+ForEach配合id参数精准更新 - 如何处理复杂的视图层级性能? 拆分子视图、使用
EquatableView、避免频繁读取环境值 - NavigationStack vs NavigationView? NavigationStack(iOS 16+)支持编程式路由和深链接,更推荐
iOS 网络、安全与编译优化面经
iOS 网络与安全深度
1. URLSession 深入
三种任务类型
let session = URLSession(configuration: .default)
// DataTask:内存中传输(小数据)
let (data, response) = try await session.data(for: request)
// DownloadTask:内容写入文件(大文件下载)
let (fileURL, _) = try await session.download(for: request)
// UploadTask:上传文件
let (data, _) = try await session.upload(for: request, from: fileData)
// WebSocket
let task = session.webSocketTask(with: URL(string: "wss://echo.test")!)
task.resume()
try await task.send(.string("Hello"))
let message = try await task.receive()
URLSession 配置
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 30
config.timeoutIntervalForResource = 300
config.requestCachePolicy = .returnCacheDataElseLoad
config.httpMaximumConnectionsPerHost = 6
config.waitsForConnectivity = true // 无网络时等待而非立即失败
let session = URLSession(configuration: config, delegate: myDelegate, delegateQueue: nil)
缓存策略
// URLCache 配置
URLCache.shared = URLCache(
memoryCapacity: 20 * 1024 * 1024, // 20MB 内存
diskCapacity: 100 * 1024 * 1024, // 100MB 磁盘
directory: nil
)
// 请求级缓存控制
var request = URLRequest(url: url)
request.cachePolicy = .returnCacheDataElseLoad // 有缓存就用缓存
2. App Transport Security (ATS)
<!-- Info.plist 常见配置 -->
<key>NSAppTransportSecurity</key>
<dict>
<!-- 允许特定域名 HTTP -->
<key>NSExceptionDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
3. 证书锁定(SSL Pinning)
// URLSession delegate 实现证书锁定
class PinningDelegate: NSObject, URLSessionDelegate {
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// 比较公鑰
let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0)!
let serverKey = SecCertificateCopyKey(serverCert)!
let serverKeyData = SecKeyCopyExternalRepresentation(serverKey, nil)! as Data
if serverKeyData == expectedPublicKey {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
4. 安全存储
// Keychain 存储敬感信息
func save(token: String, for account: String) throws {
let data = token.data(using: .utf8)!
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecValueData: data,
kSecAttrAccessible: kSecAttrAccessibleWhenUnlocked // 解锁后才可访问
]
SecItemDelete(query as CFDictionary) // 先删除旧值
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else { throw KeychainError.saveFailed }
}
func read(for account: String) throws -> String {
let query: [CFString: Any] = [
kSecClass: kSecClassGenericPassword,
kSecAttrAccount: account,
kSecReturnData: true,
kSecMatchLimit: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else { throw KeychainError.readFailed }
return String(data: data, encoding: .utf8)!
}
5. 安全威胁与防御
逆向工程防御
// 1. 判断是否运行在模拟器上
func isRunningOnSimulator() -> Bool {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}
// 2. 判断是否进行了 Jailbreak
func isJailbroken() -> Bool {
let paths = ["/Applications/Cydia.app", "/usr/sbin/sshd", "/etc/apt"]
return paths.contains { FileManager.default.fileExists(atPath: $0) }
}
// 3. 代码混淆:使用 Swift/C 加密关键逻辑
// 4. 字符串加密:避免明文存在于二进制
数据保护
// CryptoKit 加解密
import CryptoKit
func encrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
}
func decrypt(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: data)
return try AES.GCM.open(sealedBox, using: key)
}
// 安全生成密鑰
let key = SymmetricKey(size: .bits256) // 256位 AES 密鑰
6. 编译优化技巧
// 1. 全模块优化(WMO)
// Build Settings -> Whole Module Optimization -> Yes
// 编译器看到全模块代码,可内联跨文件函数
// 2. @inline(__always) 强制内联
@inline(__always)
func criticalPath() -> Int { /* ... */ }
// 3. final 关键字避免动态分发
final class UserService { /* ... */ }
// 4. private/fileprivate 提供优化提示
// 局部小的方法更容易被内联
// 5. Existential 类型 vs Generic 类型
// 下面的写法会有装箱开销
// bad: func process(animal: any Animal) { ... }
// good: func process<T: Animal>(animal: T) { ... }
7. 资深面试题
- HTTPS 中证书屏蔽和证书锁定的区别?
- 证书屏蔽:浏览器内置的屏蔽已到期或不受信任证书的机制
- 证书锁定:应用主动将公鑰打入到应用中,与服务器公鑰对比
- 锁定更严格、主动防证书替换攻击
- Keychain 在 App 删除后是否也删除?
- 默认不删除,重新安装后仍可访问
- 要清理需要在 App 首次启动时主动判断和清除
- App 如何防止投屏时泏露敲感信息?
UITextField.isSecureTextEntry = true- App 进入后台时主动覆盖或模糊番幕
- 什么是 App Attest?
- Apple 提供的设备证明服务,验证请求来自真实未篡改的 App
- 防止服务器上的 API 被未授权的客户端调用
iOS Clean Architecture 与架构模式
iOS 架构设计深度
1. Clean Architecture 分层
Presentation Layer
- ViewController / SwiftUI View
- ViewModel (MVVM 里) / Presenter (MVP 里)
Domain Layer <-- 纯 Swift, 无任何框架依赖
- Use Case(一个 Use Case 对应一个业务操作)
- Entity(核心业务模型)
- Repository Protocol(数据访问抽象)
Data Layer
- Repository Implementation
- Data Source(Remote API + Local DB)
- Data Model Mapper
依赖规则:层之间的依赖只能向内,Domain 层对外无依赖。
2. MVVM 实现
// Domain Layer
struct User {
let id: String
let name: String
let email: String
}
protocol UserRepository {
func fetchUser(id: String) async throws -> User
}
final class FetchUserUseCase {
private let repo: UserRepository
init(repo: UserRepository) { self.repo = repo }
func execute(id: String) async throws -> User {
try await repo.fetchUser(id: id)
}
}
// Presentation Layer
@MainActor
final class UserViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
private let useCase: FetchUserUseCase
init(useCase: FetchUserUseCase) { self.useCase = useCase }
func loadUser(id: String) async {
state = .loading
do {
let user = try await useCase.execute(id: id)
state = .success(user)
} catch {
state = .error(error.localizedDescription)
}
}
}
enum ViewState {
case idle, loading
case success(User)
case error(String)
}
3. 依赖注入
// 构建器注入而非全局单例
struct AppDependencies {
let userRepository: UserRepository
let authService: AuthService
static func makeProd() -> AppDependencies {
AppDependencies(
userRepository: RemoteUserRepository(apiClient: URLSessionAPIClient()),
authService: AuthServiceImpl()
)
}
static func makeTest() -> AppDependencies {
AppDependencies(
userRepository: MockUserRepository(),
authService: MockAuthService()
)
}
}
// Factory 模式创建 ViewModel
final class UserViewModelFactory {
private let deps: AppDependencies
init(deps: AppDependencies) { self.deps = deps }
func make(userId: String) -> UserViewModel {
UserViewModel(
useCase: FetchUserUseCase(repo: deps.userRepository),
userId: userId
)
}
}
4. 导航架构设计
// Coordinator 模式解耦导航逻辑
protocol Coordinator: AnyObject {
var navigationController: UINavigationController { get }
func start()
}
final class AppCoordinator: Coordinator {
let navigationController: UINavigationController
private let deps: AppDependencies
private var childCoordinators: [Coordinator] = []
init(nav: UINavigationController, deps: AppDependencies) {
self.navigationController = nav
self.deps = deps
}
func start() {
showHome()
}
private func showHome() {
let vc = HomeViewController(
viewModel: HomeViewModel(deps: deps),
onUserTapped: { [weak self] userId in
self?.showUserDetail(userId: userId)
}
)
navigationController.setViewControllers([vc], animated: false)
}
private func showUserDetail(userId: String) {
let factory = UserViewModelFactory(deps: deps)
let vc = UserDetailViewController(viewModel: factory.make(userId: userId))
navigationController.pushViewController(vc, animated: true)
}
}
5. 错误处理设计
// 领域错误类型
enum UserError: LocalizedError {
case notFound
case unauthorized
case networkError(underlying: Error)
var errorDescription: String? {
switch self {
case .notFound: return "用户不存在"
case .unauthorized: return "没有权限"
case .networkError(let e): return "网络错误: \(e.localizedDescription)"
}
}
}
// Repository 层映射错误
extension RemoteUserRepository {
private func mapError(_ error: Error) -> UserError {
guard let apiError = error as? APIError else {
return .networkError(underlying: error)
}
switch apiError.statusCode {
case 404: return .notFound
case 401: return .unauthorized
default: return .networkError(underlying: apiError)
}
}
}
6. 单元测试
// Domain 层单独测试,无需模拟框架
final class FetchUserUseCaseTests: XCTestCase {
var sut: FetchUserUseCase!
var mockRepo: MockUserRepository!
override func setUp() {
mockRepo = MockUserRepository()
sut = FetchUserUseCase(repo: mockRepo)
}
func test_execute_whenUserExists_returnsUser() async throws {
mockRepo.stubbedUser = expected
let result = try await sut.execute(id: "1")
XCTAssertEqual(result.id, expected.id)
}
func test_execute_whenUserNotFound_throwsError() async {
mockRepo.stubbedError = UserError.notFound
await XCTAssertThrowsErrorAsync(try await sut.execute(id: "999")) { error in
XCTAssertEqual(error as? UserError, .notFound)
}
}
}
7. 资深面试题
- MVVM 和 MVP 的核心区别?
- MVP: Presenter 知道 View,通过接口调用;不依赖 UI 框架,库测试更简单
- MVVM:ViewModel 不知道 View,通过数据绑定展示;更适合 SwiftUI
- Coordinator 模式解决什么问题?
- ViewController 之间不再互相了解,导航逻辑集中在 Coordinator
- 导航逻辑集中易于测试和复用
- 如何解决 ViewModel 测试中的异步问题?
- 使用
async/await+XCTestExpectation或XCTAssertThrowsErrorAsync - 将依赖的 Clock 抽象,测试时注入可控时间
- 使用
- Use Case 是否一定需要?
- 简单 CRUD 可以省略,直接调用 Repository
- 包含业务规则、多步骤或需单独测试的操作建议抽取