面试题手册

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

服务端阅读 05月30日 00:37

React Query 如何处理错误和重试?哪些错误不该重试?

React Query 的错误处理要分两层:组件里展示用户能看懂的错误,全局里做日志和兜底通知。重试不要一刀切,网络抖动和 5xx 可以重试,401、403、404、表单校验这类 4xx 通常不该重试;否则只是把错误请求重复打到服务端。追问retry 默认是几次?浏览器端查询默认会重试 3 次,并使用退避延迟;服务端渲染场景通常不建议重试太多,否则会拖慢响应。retry 和 refetch 有什么区别?retry 是一次请求失败后的自动补救,refetch 是用户或程序主动重新拉取。错误页里的“再试一次”通常调用 refetch 或 reset error boundary。Mutation 失败要怎么处理?如果做了乐观更新,onMutate 里先保存旧数据,onError 里回滚,再提示用户。不要只弹 toast,却让缓存停在错误状态。全局 onError 适合做什么?适合上报 Sentry、统一 toast、处理登录过期。具体页面文案仍应在业务组件里判断。写段代码useQuery({ queryKey: ['todos'], queryFn: getTodos, retry: (count, error: any) => { if ([400, 401, 403, 404].includes(error.status)) return false; return count < 3; }, retryDelay: i => Math.min(1000 * 2 ** i, 30_000),});
服务端阅读 05月30日 00:37

React Query 如何与 Suspense 集成?错误边界怎么处理?

React Query 接入 Suspense 后,加载状态交给 Suspense fallback,错误交给 Error Boundary。实际项目里优先用 useSuspenseQuery,而不是在每个组件里判断 isLoading。注意:开启 Suspense 不代表不用管错误,网络失败会抛给最近的错误边界;如果要让“重试”按钮生效,还要配 QueryErrorResetBoundary。追问useSuspenseQuery 和 useQuery 有什么区别?useSuspenseQuery 成功返回时 data 一定有值,加载中会挂起组件。你少写了 loading 分支,但必须准备好 Suspense 和错误边界。Error Boundary 应该放在哪里?放在页面块级别通常最合适。太外层会导致整页白掉,太内层又会让错误处理重复。有缓存时还会进入 fallback 吗?已有可用缓存时通常直接渲染;queryKey 改变触发新请求时,可能再次 fallback。交互更新可用 startTransition 降低界面闪烁。SSR 里能直接用吗?要谨慎。Next.js 等场景通常配合预取、dehydrate/hydrate,或使用框架推荐的 Suspense 数据方案。写段代码<QueryErrorResetBoundary> {({ reset }) => ( <ErrorBoundary onReset={reset} fallbackRender={({ resetErrorBoundary }) => ( <button onClick={resetErrorBoundary}>重试</button> )}> <Suspense fallback={<Spinner />}> <UserPanel /> </Suspense> </ErrorBoundary> )}</QueryErrorResetBoundary>
服务端阅读 05月30日 00:37

React Query 项目中如何组织查询和命名 query key?

项目里不要把 useQuery 散落在组件里。更稳的做法是按业务域组织:API 函数只负责请求,自定义 Hook 负责暴露查询,query key 工厂负责命名和失效。query key 用数组,从宽到窄写,比如 ['users', 'detail', id];凡是 queryFn 依赖的参数都必须进 key,否则缓存可能串数据。追问为什么不建议直接写字符串 key?字符串很难表达层级,也不方便批量失效。数组 key 可以让你失效整个用户域、某个详情页,或某个筛选列表。API 函数和 Hook 为什么要分开?API 函数可独立测试,也能被预取、SSR、mutation 复用。Hook 里再放 staleTime、enabled、select 等 React Query 配置。项目结构按 api、hooks 分,还是按 feature 分?小项目都可以;中大型项目更推荐 feature 分组,例如 features/user/api.ts、queries.ts、keys.ts,修改用户模块时不用跨目录找文件。命名上最容易踩什么坑?列表和详情共用一个 key、筛选条件没放进 key、mutation 成功后失效范围太大。这些都会造成脏数据或无意义重请求。写段代码export const userKeys = { all: ['users'] as const, detail: (id: string) => [...userKeys.all, 'detail', id] as const,};export function useUser(id: string) { return useQuery({ queryKey: userKeys.detail(id), queryFn: () => getUser(id), staleTime: 60_000, });}
服务端阅读 05月30日 00:37

什么是 WebRTC?它的核心组成部分有哪些?

WebRTC 是浏览器里的实时音视频和数据通信能力,核心价值是不用插件就能让两个端建立低延迟连接。它主要由三块组成:媒体采集用 getUserMedia 拿摄像头、麦克风;连接与协商用 RTCPeerConnection 处理 SDP、ICE、加密和媒体传输;任意数据传输用 RTCDataChannel。另外要记住:WebRTC 不自带信令服务,房间、呼叫、offer/answer 和 candidate 交换通常由业务用 WebSocket 或 HTTP 自己实现。追问WebRTC 是完全点对点吗?不一定。能直连时媒体可以 P2P;直连失败会走 TURN 中继;多人会议通常还会用 SFU 转发媒体流,所以“WebRTC 等于 P2P”这个说法不准确。RTCPeerConnection 具体负责什么?它负责创建 offer/answer、管理 ICE 候选、建立 DTLS/SRTP 安全传输,并把本地媒体轨道发送给对端。简单说,它是 WebRTC 连接的核心对象。信令为什么不算 WebRTC 标准的一部分?因为不同业务的房间模型、鉴权、重连和消息格式差异很大。标准只规定浏览器如何生成 SDP 和 candidate,至于怎么传给对端,由业务决定。getUserMedia 和 RTCDataChannel 有什么区别?getUserMedia 采集音视频流,适合通话、录制、屏幕共享。RTCDataChannel 传任意数据,适合聊天、白板同步、文件分片或游戏状态同步。写段代码const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true })const pc = new RTCPeerConnection({ iceServers })stream.getTracks().forEach(track => pc.addTrack(track, stream))const channel = pc.createDataChannel('chat')
服务端阅读 05月30日 00:37

WebRTC 如何实现 NAT 穿透?STUN、TURN 和 ICE 分别做什么?

WebRTC 的 NAT 穿透不是靠某一个协议硬打洞,而是由 ICE 统一调度:先收集本地地址,再用 STUN 拿到公网映射地址,最后在直连失败时走 TURN 中继。面试里可以这样答:STUN 负责“我在公网看起来是谁”,TURN 负责“直连不通时替我转发”,ICE 负责“把所有候选路径试一遍,选能通且质量最好的”。生产环境只配 STUN 不够,遇到对称 NAT、企业防火墙或 UDP 被限制时,必须有 TURN 兜底。追问STUN 和 TURN 有什么区别?STUN 只参与建连阶段,帮助发现公网 IP 和端口,媒体流通常仍然点对点传输。TURN 会一直在媒体路径上转发数据,成功率高,但延迟、带宽成本和服务器压力都更大。ICE 候选者有哪些?常见有 host、srflx、relay 三类。host 是本机局域网地址,srflx 是 STUN 得到的公网映射地址,relay 是 TURN 分配的中继地址。为什么有 STUN 还需要 TURN?因为 STUN 依赖 NAT 映射可预测。对称 NAT 或严格防火墙下,对端无法直接打到这个映射端口,TURN 中继就成了最后的可用路径。实际项目里怎么配置?至少配置一个 STUN 和一个带鉴权的 TURN,TURN 建议靠近用户部署。监控里要看 relay 占比、ICE 失败率、连接耗时和 RTT,relay 占比突然升高通常说明网络或 STUN 可用性出问题。写段代码const pc = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'turn:turn.example.com:3478', username: 'u', credential: 'p' } ]})pc.onicecandidate = e => sendToPeer(e.candidate)
服务端阅读 05月30日 00:10

Canvas 如何进行图像处理和像素操作?

Canvas 图像处理就是四步:drawImage 把图片、视频帧或另一个 Canvas 画上去;getImageData 读出像素;修改 data 里的 RGBA;再用 putImageData 写回。data 是 Uint8ClampedArray,每个像素 4 个值,位置从 i * 4 开始。九参数 drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh) 可同时裁剪和缩放,雪碧图截帧、头像裁剪常用。跨域图必须服务端开 CORS,并给图片设置 crossOrigin="anonymous",否则读像素会抛 SecurityError。追问Canvas 橡皮擦怎么做?把 globalCompositeOperation 设成 destination-out,新画的区域会从旧内容里挖掉。蒙版常用 source-in,发光叠加可用 lighter。灰度滤镜为什么不用 RGB 平均值?人眼对绿色更敏感,常用 0.299R + 0.587G + 0.114B 算亮度,视觉效果比简单平均自然。大图逐像素处理很卡怎么办?用 OffscreenCanvas + Worker,把像素循环放到子线程;静态图层先离屏缓存;只改局部时用 putImageData 的脏矩形参数,别全画布回写。处理后的图片怎么导出?推荐 canvas.toBlob(cb, "image/jpeg", 0.92),异步且省内存。toDataURL() 同步生成 Base64,只适合小图预览。写段代码function grayscale(imageData) { const d = imageData.data; for (let i = 0; i < d.length; i += 4) { const gray = d[i] * 0.299 + d[i + 1] * 0.587 + d[i + 2] * 0.114; d[i] = d[i + 1] = d[i + 2] = gray; } return imageData;}
服务端阅读 05月30日 00:10

Shell 中 if 和 case 语句有什么区别?如何使用?

if 适合判断条件真假,比如文件是否存在、变量是否为空、命令是否执行成功;case 适合一个变量匹配多个模式,比如菜单选项、文件后缀、运行环境。写 Shell 条件判断时,变量要加引号,复杂字符串判断优先用 [[ ]],数字计算用 (( ))。追问[ ] 和 [[ ]] 有什么区别?[ ] 是传统 test 写法,兼容性好但限制多;[[ ]] 是 Bash 扩展,支持 &&、||、模式匹配和正则,变量未加引号时也更不容易出错。写 Bash 脚本通常优先用 [[ ]]。if 能直接判断命令结果吗?可以。if grep -q "ERROR" app.log; then ... fi 判断的是命令退出码,0 表示成功,非 0 表示失败。这比手动判断 $? 更清晰。case 适合替代多个 elif 吗?当你围绕同一个变量做多分支匹配时,case 更清楚。它支持通配符,比如 *.log)、start|restart),最后用 *) 兜底。常见坑是什么?[ $name = foo ] 在变量为空时会语法错误,应该写 [ "$name" = "foo" ]。另外 -a、-o 可读性差,建议拆成 [ cond1 ] && [ cond2 ]。写段代码file="$1"if [[ -f "$file" && -r "$file" ]]; then echo "readable file"ficase "$file" in *.sh) echo "shell script" ;; *.log) echo "log file" ;; *) echo "unknown" ;;esac
服务端阅读 05月30日 00:10

grep、sed、awk 和 cut 分别适合处理什么文本问题?

Shell 文本处理常用四件套:grep 负责找行,sed 负责按规则改行,awk 负责按列计算和格式化,cut 负责简单切字段。判断工具时别背命令,先看问题:是搜索、替换、列处理,还是固定分隔符提取。追问grep 适合什么?适合按关键字或正则筛选行。常用参数是 -i 忽略大小写、-n 显示行号、-r 递归目录、-v 反向过滤。sed 和 awk 怎么区分?sed 更像“流式编辑器”,擅长替换、删除、打印某段行;awk 更像“小型报表工具”,擅长按字段取列、条件过滤、求和统计。cut 还有必要学吗?有。字段分隔很固定时,cut -d ':' -f1 比 awk 更直接。但它不擅长复杂条件,也不适合多空格混合的文本。实际排日志会怎么组合?先用 grep 缩小范围,再用 awk 提取字段,最后用 sort | uniq -c 统计。管道里每一步只做一件事,脚本会更好查错。写段代码# 查 ERROR,提取第 5 列 IP,统计出现次数grep "ERROR" app.log | awk '{print $5}' | sort | uniq -c | sort -nr# 批量替换配置sed -i.bak 's/port=8080/port=9090/' app.conf
服务端阅读 05月30日 00:10

Shell 脚本中如何定义和使用数组?

Shell 数组主要分两类:普通数组和关联数组。普通数组用数字下标,适合保存文件名、参数列表;关联数组用字符串 key,适合保存配置。写脚本时最重要的一点是:访问数组尽量用双引号包住 "${arr[@]}",否则遇到空格文件名会翻车。追问普通数组怎么定义和读取?普通数组用空格分隔定义,索引从 0 开始。${arr[0]} 取第一个元素,${arr[@]} 取全部元素,${#arr[@]} 取长度。${arr[@]} 和 ${arr[*]} 有什么区别?加双引号时区别最大:"${arr[@]}" 会保留每个元素的边界,"${arr[*]}" 会把所有元素拼成一个字符串。遍历数组时优先用 "${arr[@]}"。关联数组怎么用?Bash 4+ 支持关联数组,必须先 declare -A map。例如 map[host]=localhost,读取用 ${map[host]},遍历 key 用 ${!map[@]}。删除数组元素会重新编号吗?不会。unset arr[1] 只删除该位置,其他索引不变,所以遍历稀疏数组时用 ${!arr[@]} 更稳。写段代码arr=("a.txt" "b file.txt")arr+=("c.txt")for i in "${!arr[@]}"; do echo "$i => ${arr[$i]}"donedeclare -A conf=([host]="127.0.0.1" [port]="3306")echo "${conf[host]}:${conf[port]}"
服务端阅读 05月30日 00:10

如何在浏览器中正确加载和初始化 OpenCV.js?

OpenCV.js 在浏览器里不能只看 script onload,真正可用要等 WASM 运行时初始化完成。常见方式有三种:CDN 引入、本地静态文件、npm 包。最稳的判断是监听 cv.onRuntimeInitialized,否则很容易出现 cv.imread is not a function 或方法尚未挂载的问题。追问CDN 怎么写才安全?script 的 onload 只代表文件下载完成,不代表 OpenCV 运行时准备好了。应在 onload 里设置 cv.onRuntimeInitialized,初始化完成后再启用按钮或调用图像处理逻辑。npm 包适合什么场景?适合工程化项目,比如 React、Vue。@techstark/opencv-js 用起来方便,但仍要处理异步初始化,不能在模块导入后立刻假设所有 API 都可用。加载慢怎么办?OpenCV.js 体积通常较大,建议使用 CDN 缓存、gzip/br 压缩、按需加载,并把初始化状态做成 Promise,避免多个组件重复加载。浏览器兼容要注意什么?OpenCV.js 依赖 WebAssembly,老旧浏览器支持不好。读取跨域图片时还要配置 CORS,否则 cv.imread 背后的 Canvas 读取会因为安全策略失败。写段代码<script async src="https://docs.opencv.org/4.x/opencv.js" onload="initCv()"></script><script>function initCv() { cv.onRuntimeInitialized = () => { console.log('OpenCV.js ready'); document.querySelector('#run').disabled = false; };}</script>
服务端阅读 05月30日 00:10

OpenCV.js 和原生 OpenCV 有什么区别?

OpenCV.js 是把 OpenCV 编译到 WebAssembly/JavaScript 后在浏览器或 Node.js 里运行;原生 OpenCV 通常指 C++、Python、Java 版本,跑在桌面、服务器或移动端。两者算法体系相近,但运行环境、性能、内存管理和部署方式差异很大:前者胜在纯前端和隐私,后者胜在性能、硬件能力和完整生态。追问API 一样吗?很多函数名接近,比如 cvtColor、GaussianBlur、Canny,但写法不完全一样。OpenCV.js 使用 cv.Mat、cv.MatVector,更像 C++ API 的 JS 绑定,不是普通前端库那种链式风格。性能差多少?OpenCV.js 借助 WebAssembly 已经很快,但仍受浏览器、设备、内存和线程限制。大图批处理、高清视频流、GPU/硬件加速场景,原生 OpenCV 通常更稳。内存管理有什么坑?OpenCV.js 的 cv.Mat 不靠 JS 垃圾回收释放,必须手动 delete()。忘记释放会造成 WASM 内存泄漏,长时间视频处理时尤其明显。浏览器里还有哪些限制?跨域图片会污染 Canvas,导致无法读像素;摄像头需要用户授权;大文件和大分辨率图像会受浏览器内存限制。原生 OpenCV 访问文件、摄像头和硬件更直接。实际怎么选?网页端预览、滤镜、轻量识别、隐私敏感场景选 OpenCV.js;服务器批处理、高性能视频分析、工业视觉、模型训练或硬件加速场景选原生 OpenCV。写段代码const mat = cv.imread(image);try { cv.cvtColor(mat, mat, cv.COLOR_RGBA2GRAY); cv.imshow('canvas', mat);} finally { mat.delete();}
服务端阅读 05月30日 00:10

OpenCV.js 和其他前端图像处理库怎么选?

OpenCV.js 适合做“看懂图像”的任务,比如边缘检测、轮廓识别、模板匹配、视频帧处理;Fabric.js、p5.js、Three.js 更偏“画出来”和交互展示;TensorFlow.js 适合跑模型做分类、检测、分割。选型不要只看库名,先看需求是图像处理、交互编辑、3D 渲染,还是机器学习推理。追问OpenCV.js 相比 Fabric.js 强在哪?OpenCV.js 强在传统计算机视觉算法,能做滤波、形态学、特征点、轮廓等处理。Fabric.js 强在 Canvas 对象模型、拖拽、缩放、文字和图形编辑,适合做海报编辑器,不适合复杂视觉算法。和 p5.js 怎么区分?p5.js 更适合创意编程、教学和视觉实验,API 友好,上手快。OpenCV.js 学习成本高,但处理边缘检测、透视变换、实时视频分析时更专业。和 TensorFlow.js 是竞争关系吗?不完全是。OpenCV.js 负责图像预处理和传统视觉算法,TensorFlow.js 负责模型推理。实际项目里常见组合是:OpenCV.js 裁剪、灰度化、归一化,再交给 TensorFlow.js 做识别。OpenCV.js 最大的坑是什么?包体大、API 偏 C++ 风格、cv.Mat 要手动 delete(),否则浏览器内存会涨。移动端实时视频处理还要控制分辨率,否则帧率很容易掉。项目里怎么选?图像编辑器选 Fabric.js;创意互动选 p5.js;3D 场景选 Three.js;AI 识别选 TensorFlow.js;需要浏览器里做专业图像处理,再选 OpenCV.js。写段代码const src = cv.imread(img);const gray = new cv.Mat();try { cv.cvtColor(src, gray, cv.COLOR_RGBA2GRAY); cv.imshow('canvas', gray);} finally { src.delete(); gray.delete();}
服务端阅读 05月30日 00:10

Spring Boot 的启动流程是怎样的?关键阶段有哪些?

Spring Boot 启动本质上是 SpringApplication.run() 创建并刷新 Spring 容器的过程。主线可以记成:创建 SpringApplication、准备 Environment、创建 ApplicationContext、刷新容器、启动 WebServer、执行 Runner、发布 ready 事件。真正初始化 Bean、自动配置和启动内嵌 Tomcat 的核心都发生在 refreshContext() 里的 ApplicationContext.refresh()。追问SpringApplication 实例化时做了什么?它会推断应用类型:Servlet、Reactive 或普通应用;再加载 ApplicationContextInitializer、ApplicationListener,并推断 main 方法所在主类。这一步还没创建业务 Bean,只是在准备启动所需的元信息。Environment 是什么时候准备的?在创建 ApplicationContext 之前。Spring Boot 会合并命令行参数、系统属性、环境变量、application.yml/properties 和 profile 配置,再发布 ApplicationEnvironmentPreparedEvent。refresh() 为什么最关键?因为它会加载 BeanDefinition、执行 BeanFactoryPostProcessor、注册 BeanPostProcessor、实例化非懒加载单例 Bean。Servlet 应用的内嵌 Tomcat/Jetty 通常也在刷新过程的 onRefresh() 阶段创建并启动。Runner 在什么时候执行?ApplicationRunner 和 CommandLineRunner 在容器刷新完成、ApplicationStartedEvent 发布后执行。它们适合做启动后的初始化任务,但不要放耗时阻塞逻辑,否则应用迟迟到不了 ready 状态。启动失败会发生什么?Spring Boot 会发布 ApplicationFailedEvent,销毁已创建的 Bean,并把异常继续抛出。排查启动问题时,重点看 Environment、自动配置条件和 Bean 创建异常。写段代码@SpringBootApplicationpublic class App { public static void main(String[] args) { SpringApplication.run(App.class, args); }}
服务端阅读 05月30日 00:10

如何在 axios 中实现请求和响应拦截器?

Axios 拦截器就是请求发出前、响应返回后统一插一层处理逻辑。请求拦截器常用来加 token、加请求 ID、处理 loading;响应拦截器常用来拆 data、统一处理业务错误、401 登录失效和网络异常。项目里一般不要直接改全局 axios,而是 axios.create() 建实例,再给实例挂拦截器,避免多个后端服务互相污染配置。追问请求拦截器和响应拦截器分别适合做什么?请求拦截器改 config,比如加 Authorization、baseURL、防缓存参数。响应拦截器处理 response 或 error,比如把 { code, data, message } 统一拆成 data 返回。多个拦截器的执行顺序是什么?请求拦截器后添加的先执行,响应拦截器先添加的先执行。排查问题时要注意顺序,否则 token 还没加上,请求日志就已经打印了旧配置。loading 为什么不能简单请求开始显示、结束隐藏?并发请求会出问题。第一个请求结束就隐藏 loading,但其他请求还没回来。常见做法是维护计数器,请求加一,响应或错误减一,减到 0 再隐藏。什么时候要移除拦截器?临时调试、微前端子应用卸载、某个页面单独加拦截逻辑时要 eject,否则重复注册会导致同一个错误提示弹很多次。写段代码const api = axios.create({ baseURL: '/api', timeout: 10000 });api.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config;});api.interceptors.response.use( res => res.data, err => Promise.reject(err.response?.data || err));
服务端阅读 05月30日 00:10

axios 中如何进行错误处理?有哪些常见错误类型?

Axios 错误处理先看三类:error.response 表示服务端返回了非 2xx 状态码,重点处理 400、401、403、404、5xx;error.request 表示请求发出但没收到响应,多半是网络、超时、CORS;两者都没有通常是请求配置写错。实际项目里建议:业务层只处理当前页面关心的错误,全局拦截器统一做登录失效、错误提示、日志和重试。追问error.response 和 error.request 有什么区别?response 说明后端有响应,只是状态码失败;request 说明请求发出去了但没有拿到响应。前者看 status 和 data.message,后者看 code,比如 ECONNABORTED 或 ERR_NETWORK。401 应该在每个接口里处理吗?不要。401 属于全局认证问题,放响应拦截器里统一清 token、跳登录页或刷新 token。页面里只关心业务错误,比如表单校验失败。超时和 5xx 要不要自动重试?可以,但只重试幂等请求,比如 GET。POST、支付、下单这类接口不能盲目重试,否则可能造成重复提交。实际项目里最容易踩什么坑?拦截器里 return Promise.reject(error) 忘了写,外层 catch 拿不到错误;另一个坑是把所有错误都弹 toast,导致 401 跳转时还弹一堆无意义提示。写段代码axios.interceptors.response.use(r => r.data, error => { if (error.response) { const { status, data } = error.response; if (status === 401) { localStorage.removeItem('token'); location.href = '/login'; } return Promise.reject(new Error(data?.message || `HTTP ${status}`)); } if (error.code === 'ECONNABORTED') { return Promise.reject(new Error('请求超时')); } return Promise.reject(new Error(error.message || '网络错误'));});
服务端阅读 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