面试题手册

梳理高频技术问题,帮助你按主题复习和查漏补缺。

服务端阅读 05月29日 23:47

Android 内存泄漏有哪些常见场景?如何检测和避免?

Android 内存泄漏本质是对象生命周期结束了,却还被 GC Roots 间接引用,导致无法回收。高频场景有:静态变量持有 Activity、非静态 Handler 或匿名内部类持有外部类、监听器/广播未注销、线程或网络请求未取消、Cursor/IO 流未关闭、集合缓存长期保存 View 或 Context。检测优先用 LeakCanary 看引用链,复杂问题再用 Android Studio Memory Profiler 抓 Heap Dump。避免原则很简单:谁注册谁注销,谁启动谁取消,长生命周期对象不要持有短生命周期 Context。追问为什么静态变量持有 Activity 会泄漏?静态变量生命周期接近进程,Activity 销毁后如果仍被它引用,GC 无法回收整个 Activity 以及关联 View 树。需要 Context 时优先传 applicationContext。Handler 为什么容易泄漏?非静态内部 Handler 会隐式持有 Activity,消息队列里的 Message 又持有 Handler。Activity 销毁后消息没执行完,就会延长 Activity 生命周期。LeakCanary 主要看什么?重点看泄漏对象到 GC Root 的引用链,找到第一个不该长期持有它的对象。不要只看类名,要结合页面生命周期判断是不是误报。项目里怎么避免监听器泄漏?在 onStart/onStop 或 onCreate/onDestroy 成对注册和注销。使用 LifecycleObserver、协程 lifecycleScope、Flow repeatOnLifecycle 可以减少手动清理遗漏。写段代码static class SafeHandler extends Handler { private final WeakReference<Activity> ref; SafeHandler(Activity activity) { ref = new WeakReference<>(activity); } @Override public void handleMessage(Message msg) { Activity a = ref.get(); if (a == null || a.isFinishing()) return; }}@Override protected void onDestroy() { handler.removeCallbacksAndMessages(null); super.onDestroy();}
服务端阅读 05月29日 23:47

Android View 的绘制流程是怎样的?

View 的绘制流程一句话就是:从 ViewRootImpl 发起,依次执行 measure、layout、draw。measure 负责算宽高,核心是父 View 传下来的 MeasureSpec;layout 负责确定 left、top、right、bottom;draw 负责真正画到 Canvas 上。自定义 View 里最常见的问题是只重写 onDraw,却忘了在 onMeasure 处理 wrap_content。尺寸或位置变化用 requestLayout,内容变化用 invalidate,别混着用。追问MeasureSpec 有哪几种模式?EXACTLY 表示确定尺寸,常见于固定 dp 或 matchparent;ATMOST 表示最大不能超过某个值,常见于 wrap_content;UNSPECIFIED 表示父容器不限制,ScrollView 测子 View 时可能出现。ViewGroup 和普通 View 的绘制有什么区别?ViewGroup 在 measure 阶段要测量子 View,在 layout 阶段摆放子 View,在 draw 阶段通过 dispatchDraw 绘制子 View。普通 View 通常只关心自己的测量和 onDraw。requestLayout 和 invalidate 有什么区别?requestLayout 会重新走 measure、layout,必要时再 draw;invalidate 只触发重绘。改文字大小、宽高、位置用 requestLayout,改颜色、进度、选中态通常用 invalidate。实际项目里容易踩什么坑?自定义 View 如果 wrap_content 不设置默认尺寸,可能测出来是 0 或不符合预期。另一个坑是在 onDraw 里频繁 new Paint、Path、Rect,会造成 GC 抖动和掉帧。写段代码@Overrideprotected void onMeasure(int widthSpec, int heightSpec) { int defaultW = dp(120); int defaultH = dp(48); int w = resolveSize(defaultW, widthSpec); int h = resolveSize(defaultH, heightSpec); setMeasuredDimension(w, h);}@Overrideprotected void onDraw(Canvas canvas) { super.onDraw(canvas); canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, radius, paint);}
服务端阅读 05月29日 23:47

Android 热修复原理是什么?主流方案怎么选?

Android 热修复的本质是在不发新版 APK 的情况下,让应用优先执行修复后的代码。常见路线有三类:类加载方案把补丁 dex 插到 dexElements 前面,重启后优先生效;底层替换方案改 ArtMethod 入口,可即时生效但兼容性压力大;插桩方案在编译期埋跳转逻辑,运行时分发到补丁实现。追问Tinker 为什么通常需要重启?Tinker 走类加载和差分合成路线,把补丁 dex、资源或 so 合并后,在下次启动时让 ClassLoader 加载新内容。它稳定、覆盖面广,但不能保证所有改动立即生效。Sophix、AndFix 这类方案为什么兼容性难?它们涉及 ART 虚拟机内部结构,比如方法入口、ArtMethod 布局。不同 Android 版本和厂商 ROM 实现差异大,越底层越容易踩兼容坑。热修复不能修什么?通常不适合修 Manifest 变更、新增四大组件、资源 ID 大变动、启动早期就崩的代码。补丁也要做签名校验、版本校验和灰度发布,否则有安全风险。线上怎么选方案?如果追求稳定和覆盖范围,选 Tinker 类方案;如果业务强依赖即时修复,才考虑商业方案或插桩方案。无论哪种,都要有回滚、灰度和补丁监控。写段代码// 关键思路:patchElements 放到原 dexElements 前面Object[] combined = concat(patchElements, dexElements);setField(pathList, "dexElements", combined);
服务端阅读 05月29日 23:47

Android 性能优化怎么做?常用工具有哪些?

Android 性能优化先看指标,再动代码。常见方向是启动、卡顿、内存、网络、电量和包体积;常用工具是 Android Profiler、Perfetto/Systrace、Layout Inspector、GPU Overdraw、LeakCanary、StrictMode、Battery Historian。不要凭感觉优化,先抓 trace、heap dump 或线上监控数据,定位瓶颈后再改。追问卡顿一般怎么排查?先看主线程是否超过 16ms,抓 Perfetto 或 System Trace,重点看 UI Thread、RenderThread、Choreographer、GC 和锁等待。常见原因是主线程 IO、布局太深、频繁创建对象、RecyclerView 绑定太重。内存优化主要看什么?看泄漏和峰值。LeakCanary 适合开发期发现 Activity、Fragment、View 泄漏;Android Profiler 和 heap dump 用来查大对象、Bitmap、集合缓存是否失控。图片要按需采样,缓存要有上限。启动优化怎么做?区分冷启动、温启动、热启动。Application 里只放必要初始化,非关键 SDK 延迟或异步;首屏数据和布局尽量轻,启动耗时用 Startup Timing、Perfetto 或线上 APM 看。网络和电量怎么优化?网络上减少请求次数、启用缓存和压缩、合并接口;电量上少用常驻后台服务,周期任务交给 WorkManager,并设置网络、电量等约束。写段代码val request = PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() ).build()
服务端阅读 05月29日 23:47

pnpm 在 CI/CD 中如何加速安装和构建?

pnpm 在 CI/CD 里提速,核心是三件事:固定依赖、缓存 store、只构建必要包。pnpm install --frozen-lockfile 保证流水线不重新解析依赖;缓存 pnpm store 可以避免每次从网络下载;Monorepo 里用 --filter 只跑受影响的包,比全量构建更省时间。追问为什么优先缓存 pnpm store,而不是只缓存 node_modules?pnpm 的依赖真实内容放在 store,项目里的 node_modules 主要是链接。缓存 store 命中率更稳定,也更适合不同 job 复用。node_modules 可以缓存,但跨系统、跨 Node 版本时更容易出问题。GitHub Actions 里怎么配?用 actions/setup-node 的 cache: pnpm,再配合 pnpm/action-setup。如果要手动缓存,key 必须包含 pnpm-lock.yaml 的 hash,避免依赖变了还复用旧缓存。Docker 构建怎么提速?先复制 package.json 和 pnpm-lock.yaml,安装依赖后再复制源码。这样源码改动不会让依赖层失效。BuildKit 环境还可以挂载 pnpm store 缓存。Monorepo 怎么避免全量构建?用 pnpm -r --filter "...[origin/main]" build 只构建受影响包;需要并行时加 --workspace-concurrency,不要盲目开满 CPU,容易把 CI 机器打爆。写段代码- uses: pnpm/action-setup@v4 with: version: 9- uses: actions/setup-node@v4 with: node-version: 20 cache: pnpm- run: pnpm install --frozen-lockfile --prefer-offline- run: pnpm -r --filter "...[origin/main]" build
服务端阅读 05月29日 23:47

如何优化 Babel 编译性能?哪些配置最有效?

优化 Babel 编译性能,优先做三件事:少编译、用缓存、少插件。少编译就是用 include 精准限定源码目录,不要把整个 node_modules 丢给 Babel;用缓存就是开启 babel-loader 的 cacheDirectory 和配置层的 api.cache(true);少插件就是只保留必要转换,能交给 esbuild/SWC 的纯语法转换就别强行走 Babel。再往下才是 targets、polyfill 和并行。@babel/preset-env 的 targets 越准确,Babel 做的无用转换越少;useBuiltIns: 'usage' 可以减少 polyfill 体积;大型 Webpack 项目可配合 thread-loader,但小项目并行成本可能比收益还高。追问为什么 include 往往比 exclude 更稳?exclude 容易漏掉特殊目录,include 是白名单,只编译明确需要处理的源码和少数第三方包,性能和可控性都更好。cacheDirectory 能解决什么问题?它会缓存 babel-loader 的转换结果,二次构建只处理变更文件。开发环境收益最大,常能把重复编译时间降很多。targets 配错会怎样?目标浏览器写得太旧,Babel 会做大量降级和 polyfill;写得太新,旧环境可能跑不起来。性能优化不能牺牲兼容性。什么时候用 SWC 或 esbuild 替代 Babel?如果只是 JSX、TS 或 ES 语法转换,SWC/esbuild 更快。如果依赖 Babel 插件生态,比如自定义 AST 插件,仍然要用 Babel。写段代码module.exports = api => { api.cache(true); return { presets: [['@babel/preset-env', { targets: '>0.5%, not dead', modules: false }]] };};
服务端阅读 05月29日 23:47

Babel 如何接入 Webpack、Vite 和 Rollup?

Babel 接入构建工具的核心思路是:让构建工具负责文件扫描、依赖图和打包,Babel 只负责把指定源码转换成目标语法。Webpack 通常用 babel-loader,Vite 默认走 esbuild,只有需要 Babel 插件时才通过 React 插件或 Babel 插件接入,Rollup 用 @rollup/plugin-babel,库开发还要特别处理 helpers。关键不是“能不能接”,而是“什么时候该接”。如果只是语法降级,Vite/esbuild 或 SWC 往往更快;如果要 decorators、宏、React 特定转换、自定义 AST 插件,才值得引入 Babel。追问Webpack 里 Babel 放在哪一层?放在 module rules 的 loader 阶段。Webpack 匹配 js/jsx/ts/tsx 文件后交给 babel-loader,Babel 根据 preset 和 plugin 输出新代码。Vite 为什么默认不依赖 Babel?因为 Vite 开发阶段追求速度,默认用 esbuild 做转译。只有遇到 Babel 生态里的插件能力,比如 legacy decorators,才需要补 Babel。Rollup 里 babelHelpers 怎么选?应用打包可用 bundled,helpers 直接打进产物。库开发更推荐 runtime,并把 @babel/runtime 设为 external,避免每个包重复注入 helpers。实际项目里容易踩什么坑?最常见是把整个 node_modules 都交给 Babel,构建变慢。更稳的做法是用 include 精准指定 src 和少数需要转译的第三方包。写段代码// webpack.config.jsmodule.exports = { module: { rules: [{ test: /\.[jt]sx?$/, include: /src/, use: { loader: 'babel-loader', options: { cacheDirectory: true } } }] }};
服务端阅读 05月29日 23:47

什么是 Babel AST?如何用它写自定义插件?

Babel AST 是源码解析后的抽象语法树,Babel 插件本质上就是“遍历这棵树,找到目标节点,再改掉它”。完整流程是:parser 把代码转成 AST,traverse 按 visitor 访问节点,插件通过 path.remove、replaceWith、insertBefore 等方法修改节点,最后 generator 再生成代码。写插件时不要直接乱改 node,优先用 path,因为 path 带着父节点、作用域、替换/删除能力。比如删掉 console.log,要访问 CallExpression,判断 callee 是否是 console.log,然后 remove。追问AST 和普通字符串替换有什么区别?字符串替换只看文本,容易误伤注释、变量名或字符串。AST 能理解语法结构,比如只删除真正的函数调用,不会碰到字符串里的 console.log。visitor 为什么按节点类型写?因为 Babel 遍历 AST 时会按节点类型触发回调。你关心函数调用就写 CallExpression,关心变量名就写 Identifier。path.scope 有什么用?它用于处理作用域绑定,比如判断变量是否在当前作用域声明、重命名变量、生成不冲突的临时变量。写复杂插件时这是避免变量污染的关键。项目里最容易踩什么坑?最常见的是替换节点后又被继续遍历,导致重复处理。可以在必要时用 path.skip(),或者给新节点加标记避免二次转换。写段代码module.exports = ({ types: t }) => ({ name: 'remove-console-log', visitor: { CallExpression(path) { const c = path.node.callee; if ( t.isMemberExpression(c) && t.isIdentifier(c.object, { name: 'console' }) && t.isIdentifier(c.property, { name: 'log' }) ) path.remove(); } }});
服务端阅读 05月29日 23:47

什么是 cURL?它在 Web 开发中有什么作用?

cURL 是一个命令行数据传输工具,也是一套库,常用来通过 URL 发起 HTTP/HTTPS 请求。Web 开发里它最常见的作用是调试 API:不用写页面、不用打开 Postman,直接在终端复现 GET、POST、请求头、Cookie、鉴权和上传下载问题。追问cURL 和 Postman 有什么区别?Postman 更适合可视化调试和团队管理接口;cURL 更轻、更容易复制到终端、脚本、CI/CD 或线上机器里排查问题。很多接口文档也会直接给 cURL 示例,因为它可执行、可复现。cURL 在项目里通常怎么用?前后端联调时,用它验证接口是否真的可访问;线上排障时,用它检查状态码、响应头、重定向和 TLS;自动化脚本里,用它做健康检查或触发 Webhook。它只支持 HTTP 吗?不是。cURL 支持 HTTP、HTTPS、FTP、SFTP、SMTP 等多种协议,只是 Web 开发中最常用的是 HTTP/HTTPS。面试里怎么回答更像真用过?可以说自己用 curl -i 看响应头,用 -H 带 token,用 -d 复现 POST 请求,用 -v 排查 DNS、TLS 或连接失败。这样比只背“命令行工具”更具体。写段代码# 查看接口是否可访问curl -i https://api.example.com/users# 带 Token 调 APIcurl -H "Authorization: Bearer token" \ https://api.example.com/profile# 提交 JSONcurl https://api.example.com/users \ -H "Content-Type: application/json" \ -d '{"name":"John"}'
服务端阅读 05月29日 23:47

如何用 cURL 发送 GET 和 POST 请求?

GET 用来从服务端取数据,参数通常放在 URL 查询串里;POST 用来提交数据,数据通常放在请求体里。用 cURL 时,GET 最常见的是直接请求 URL,POST 常用 -d 或 --data 传请求体。注意:curl -d 默认会发 POST,不一定非要写 -X POST。追问GET 和 POST 最大区别是什么?GET 的参数暴露在 URL 中,适合查询;POST 的数据放在 body 中,适合提交表单、JSON 或文件。GET 通常可缓存、幂等;POST 通常不缓存,也不保证幂等。-d 和 -G 一起用是什么意思?-d 默认把数据放进请求体并触发 POST;加 -G 后,cURL 会把这些参数拼到 URL 后面,仍然发 GET。实际调接口时最常用哪些参数?常用 -H 加请求头,-d 传 JSON 或表单,-i 看响应头和响应体,-I 只看响应头,-v 排查连接、TLS、重定向等问题。发送 JSON 时容易踩什么坑?最常见是忘了加 Content-Type: application/json,或者 JSON 字符串引号没转义好。另一个坑是误以为 -X POST 必须写,其实有 -d 时 cURL 已经会用 POST。写段代码# GET:查询用户curl "https://api.example.com/users?page=1&limit=10"# GET:带请求头curl -H "Authorization: Bearer token" \ https://api.example.com/users# POST:提交 JSONcurl https://api.example.com/users \ -H "Content-Type: application/json" \ -d '{"name":"张三","email":"zhangsan@example.com"}'# POST:上传文件curl https://api.example.com/upload \ -F "file=@/path/to/file.pdf"