
拿到贝壳找房2023届校招移动端类试卷的时候我第一反应是它比想象中均衡得多。移动端这几年技术栈越来越杂Android、iOS、小程序、H5 都在抢人一张卷子想兼顾所有方向很容易考成四不像。但贝壳这套卷子从选择题到简答题再到编程题基本把“基础功底 平台特性 实战排查”三个层次都覆盖到了认真做下来甚至能反推出这家公司的移动端团队平时在关注什么。这不是单纯背八股就能拿高分的卷子。比如有一类题表面在考 Java 的 HashMap实际想让你答出扩容时链表转红黑树的阈值以及为什么偏偏是 8再比如 Android 的启动模式会结合房源详情页的跳转场景来出题只看过概念没在项目里踩过坑的人很容易选错。我把这套卷子里反复出现的考点连同解题思路和延伸学习路线一起整理出来给准备移动端校招的同学做个参考也聊聊贝壳这类重业务、重稳定性的 App 团队在校招笔试里到底想筛选什么样的人。1. 试卷整体设计与考点分布1.1 这套卷子的真实结构贝壳找房的移动端校招试卷通常不是一份纯选择题的题库而是几类题目组合的综合卷。常见结构是单选题 多选题 简答题 一道到两道编程题个别批次还会加一道系统设计类问题比如“如果让你设计一个房源列表页的加载状态你会怎么考虑”。整套卷子的核心目标是快速筛掉三部分人基础不牢的、只会写页面不懂原理的、代码手感生疏的。单选题和多选题主要覆盖 Java/Kotlin 基础、数据结构、操作系统、计算机网络、Android 或 iOS 平台机制。简答题则偏向“解释某个机制的原理”或者“线上出现某个问题你会怎么排查”。编程题一般不会太难中等偏下难度重点考察边界处理和代码完整性而不是让你在笔试里写一个红黑树出来。从我接触到的历届考生反馈来看这套卷子的题量控制得比较合理正常 90 分钟到 120 分钟能做完。但如果前面基础题犹豫太久后面编程题就会很赶。所以第一个建议是拿到卷子先花两分钟扫一遍全卷把编程题留够至少 30 分钟。1.2 考点权重与复习优先级根据近两年移动端校招笔试的题目形态我大致梳理了这张卷子的考点权重考点方向大概占比常见出题形式Java/Kotlin 语言基础15%集合类、关键字、异常、协程数据结构与算法20%链表、二叉树、哈希、动态规划操作系统10%进程线程、内存、死锁计算机网络15%TCP/UDP、HTTP/HTTPS、DNSAndroid 专项15%生命周期、Handler、启动模式、内存泄漏iOS 专项10%ARC、内存管理、Runloop、Block跨端/H5/小程序10%跨端方案对比、H5 调试、小程序生命周期其他5%设计模式、代码输出题这里有个很关键的点Android 和 iOS 是分开招聘的但很多学校里的“移动开发”课程是混着教的导致一部分同学两边都懂一点两边都不深。贝壳的卷子在这个环节筛选度很高——它不要求你 iOS 和 Android 都会但要求你在自己投的那个方向上答得足够专业。如果你投的是 Android那 iOS 的题可以战略性放弃把时间留给 Android 专项反之亦然。最怕的是在非目标方向的题目上花太多时间最后自己的主战场反而没答好。2. 基础题解析移动端笔试的高频底座2.1 Java与数据结构HashMap只是起点贝壳移动端试卷里Java 集合类是选择题的常客尤其是 HashMap。我见过不止一道类似这样的题“HashMap 在什么条件下链表会转为红黑树”答案是两个条件同时满足链表长度达到 8并且数组长度达到 64。如果数组长度不到 64即使链表超过 8 也只会扩容不会转红黑树。为什么阈值选 8这里有一个统计学背景HashMap 的源码注释里提到在随机哈希码的情况下链表节点数量遵循泊松分布当负载因子是 0.75 时链表中节点数达到 8 的概率约为千万分之六。也就是说正常情况下几乎不可能出现链表长度到 8 的情况真出现了说明哈希函数有问题或者发生了严重的哈希碰撞这时候用红黑树来缓解查询退化才是合理的。面试官问“为什么是 8”其实就是在看你有没有真的读过源码注释而不只是背了一个答案。如果题目再延伸一点还会考 HashMap 在扩容时的并发问题。JDK 1.7 是头插法并发扩容时可能形成环形链表导致 get 死循环JDK 1.8 改成尾插法解决了一部分问题但并发下仍然可能丢数据。所以真正的结论是并发场景不要用 HashMap用 ConcurrentHashMap。在移动端开发里ConcurrentHashMap 也经常用于缓存管理的底层结构。还有一道和移动端强相关的题就是 LruCache 的实现原理。LruCache 内部用的是 LinkedHashMap并且 accessOrder 设置为 true这样每次 get 一个元素该元素就会移动到链表尾部当缓存满时移除链表头部的元素也就是最久没被访问的。这道题在贝壳这类业务里特别实用因为图片加载库、列表缓存、网络缓存都会用到 LRU 策略。答题时建议顺带说一句“我会在项目里把 LruCache 封装一层加上线程安全控制”这会让面试官觉得你不是只会背原理。2.2 操作系统与网络移动开发绕不开的两座山操作系统考点不多但几乎每年都会出现。最常见的是进程和线程的区别、死锁产生的四个必要条件、用户态和内核态的概念。移动端场景下进程线程常和 Binder 机制结合考Android 里每个 App 默认是一个进程线程是 CPU 调度的最小单位跨进程通信靠 Binder而不是传统的共享内存或管道因为 Binder 只需要一次拷贝性能更好同时自带身份校验更安全。网络部分TCP 三次握手和四次挥手是必考题。这里建议不要只背状态流转要能解释“为什么不能两次握手”。因为两次握手只能确认客户端发送能力正常无法确认客户端是否真的收到了自己上次的请求。最典型的场景是客户端发送了一个连接请求因为网络延迟超时后重发如果旧请求后来先到了服务器服务器返回确认两次握手下连接就建成了但客户端其实已经不需要这个连接了服务器就会白白维持一个无效连接。三次握手可以把这种情况挡在外面因为客户端收到服务端的确认后会判断这个确认是否是自己想要的那个不是就发 RST 重置。HTTP 与 HTTPS 的题也是高频。注意 HTTPS 的连接过程不只是“证书 对称加密”要能说清证书校验、非对称密钥交换、对称加密数据传输三个阶段。很多同学会漏掉“证书链校验”这一层也就是客户端如何确认服务端证书是可信的。简单说是由系统内置的根证书逐级向上验证如果证书不被信任客户端会提示安全警告。移动端 App 里如果出现 HTTPS 证书校验失败常见原因是测试环境用了自签名证书但没有在 App 里配置信任这种实际问题也经常作为简答题出现。3. 平台专项题Android、iOS与跨端方案3.1 Android生命周期与启动模式业务场景才是标尺Android 生命周期题在学校里都会被背得滚瓜烂熟但贝壳的卷子不是直接问顺序而是给场景。比如“App 在后台被系统回收后用户再次点击任务栏恢复此时 Activity 经历了怎样的生命周期”答案是onRestart - onStart - onResume而不是 onCreate - onStart - onResume。因为 Activity 实例还在只是走了 onStop系统并没有销毁它。更常见的一类题是结合 onSaveInstanceState 考状态保存。比如旋转屏幕时Activity 默认会销毁重建onSaveInstanceState 在 onStop 之前调用应该把编辑框内容、列表滚动位置存进去。这里有个容易答错的点如果用户主动按返回键退出系统不会调用 onSaveInstanceState因为系统认为用户明确要关闭页面不需要恢复状态。搞清楚“被动回收会保存主动退出不保存”这道题基本就稳了。启动模式也是高频场景题。贝壳的业务场景里从推送通知点击进入房源详情页再返回时应该回到之前的列表页而不是重新创建一个详情页或者栈底混乱。这个场景最合适的选择是 singleTask因为它会复用已有的 Task 中的实例并把其上的 Activity 全部出栈。但如果从搜索结果页连续点击多个房源希望每次都能形成独立的返回栈那可能要配合 intent flags 灵活处理。回答这类题时不要只报四个启动模式的名字要说出每种模式在真实业务中的应用场景以及选择它的理由。3.2 线程通信与Handler机制Handler 机制是 Android 校招必考点没有之一。试卷上的典型考法是“子线程能不能直接更新 UI为什么Handler 机制的工作原理是什么”子线程不能直接更新 UI因为 UI 操作不是线程安全的如果允许任意线程修改界面会出现绘制错乱和状态不一致。Android 的解决方案是UI 操作只能在主线程执行子线程通过 Handler 把消息发到主线程的消息队列由主线程的 Looper 取出并执行。Handler 工作的四个核心对象是 Handler、Message、MessageQueue、Looper。主线程在 ActivityThread 的 main 方法里调用 Looper.prepareMainLooper 和 Looper.loop开启无限循环从 MessageQueue 里取消息。子线程里如果要用 Handler必须先 Looper.prepare 给当前线程创建 Looper再 Looper.loop 启动消息循环否则会直接崩错误信息就是 “Cant create handler inside thread that has not called Looper.prepare()”。我在给学弟学妹们做模拟面的时候会额外提醒一句话Handler 也是内存泄漏的重灾区。非静态内部类隐式持有外部 Activity 的引用如果消息延迟处理Activity 已经销毁了但消息队列里还持有着这个 Handler导致 Activity 无法被回收。标准解法是写成静态内部类用 WeakReference 持有外部 Activity。这道题如果能从“原理”答到“内存泄漏”再答到“解决方案”基本可以拿满分。3.3 iOS内存管理与跨端技术对比投 iOS 方向的同学要重点准备 ARC 机制、Block 循环引用、Runloop 这几个点。ARC 是编译器在编译期自动插入 retain/release但它只能解决编译期能确定引用关系的问题解决不了循环引用。最常见的循环引用是对象 A 持有 BlockBlock 内部又使用了 A这样 A 和 Block 互相持有谁都释放不了。解法是把 Block 里用到的对象声明为 __weak也就是 weakSelf。Runloop 经常和 autolreleasepool 结合考。iOS 主线程 Runloop 在每次事件循环结束后会自动释放 autorelease 对象所以大量创建临时对象时不用手动插 autoreleasepool。但在循环里大量创建临时对象或者处理大图片时可以手动加 autoreleasepool 提前释放内存这个优化点写进简历里会很加分。跨端题在贝壳的试卷里也会有因为它既有 App也有微信小程序还有 H5 页面。常见考法是一张对比表问你 Flutter、React Native、小程序、H5 各自的优缺点。我推荐这样答H5 和 JS 生态最灵活、发布最快但性能和体验受限于 WebView小程序基于 WebView 但补充了原生能力张小龙当年定位是“用完即走”强在轻量弱在复杂交互React Native 通过 JS 桥接调用原生组件体验比 H5 好但桥接性能有损耗Flutter 直接用 Skia 引擎自绘 UI几乎不依赖原生控件性能最接近原生代价是包体积大、Dart 语言有学习成本。如果对方再追问“你会怎么选”就说核心交易流程用原生或 Flutter营销类页面用 H5 或小程序动态下发能力要求高的用 H5。4. 移动端性能优化与线上调试实战4.1 冷启动与首屏渲染优化性能优化题几乎每年都有只是形式不同。贝壳这类 App 启动速度直接影响用户第一印象所以冷启动优化是一个很好的考点。冷启动指进程从零开始创建要经过系统分配进程、Application 创建、Activity 创建和首帧绘制。试卷上如果问“App 启动太慢你会怎么排查”正确的回答思路是先用 adb 命令或 Profiler 看启动耗时的分布区分是 Application 初始化耗时还是首帧绘制耗时再针对性优化。Application 初始化阶段常见的坑就是在 onCreate 里做大量无关紧要的操作比如初始化推送 SDK、地图 SDK、埋点 SDK而且全都放在主线程同步执行。优化方法有三个异步初始化把不依赖上下文的 SDK 放到子线程懒加载真正用到某个模块时才初始化延迟初始化利用 IdleHandler 在主线程空闲时再执行。这里有一个经验值冷启动时 Application.onCreate 里超过 100ms 的工作都要被质疑超过 300ms 基本就是启动慢的元凶。首屏绘制优化核心是减少布局层级、避免过度绘制、把耗时操作从主线程挪走。如果是 H5 首屏还要考虑静态资源体积、接口请求时机、图片懒加载。试卷里如果给了一段代码让你找问题一般就是这种套路JSON.parse 大对象在主线程、图片没有压缩、View 层级嵌套太多。答题时按“主线程耗时、内存占用、渲染次数”三个维度去拆逻辑会很清晰。4.2 vConsole任意移动端页面的即时调试贝壳的业务里移动端 H5 页面占了很大比例所以试卷里偶尔会出一道“线上 H5 页面出问题你怎么调试”的题目。标准工具就是 vConsole。vConsole 是腾讯开源的一个移动端 H5 调试面板能查看 console 日志、网络请求、Cookie、LocalStorage还能手动执行 JS。它最爽的一点是不需要任何构建工具往页面里插一个 script 标签就能用。如果你说“线上页面我不能改代码怎么办”其实还有办法。vConsole 的脚本可以动态注入到任意已打开的页面。在移动端调试场景里常见做法是用 Charles 或 Fiddler 这类抓包工具做响应重写在 HTML 返回内容里自动插入下面这段脚本script srchttps://cdn.jsdelivr.net/npm/vconsole/script script var vConsole new VConsole(); /script这样所有经过代理的页面都会自动挂上 vConsole 面板不用改业务代码。如果在 WebView 里调试也可以在 WebView 加载 URL 时通过 loadUrl 注入一段 JS道理一样。这个技巧在校招笔试的简答题里很加分因为它说明你不仅会用工具还理解工具的原理和边界。4.3 ECharts移动端tooltip显示问题贝壳的业务里有大量的数据可视化场景房价走势、成交统计、房源对比都会用到图表库。校园招聘试卷里不太会直接出 ECharts 源码题但可能会给一个场景“移动端折线图渲染完成后怎么默认显示最后一个点的 tooltip”这道题据我了解在很多公司移动端前端笔试里都出现过贝壳的试卷里也有类似倾向。ECharts 的 tooltip 默认是鼠标悬浮才触发但移动端没有鼠标通常是触摸才显示。如果想让渲染完成后自动显示最后一个点的 tooltip有两个方案。第一个是 dispatchAction 方案在 setOption 完成之后主动派发 showTip 事件const chart echarts.init(document.getElementById(chart)); chart.setOption(option); setTimeout(() { const lastIndex option.xAxis.data.length - 1; chart.dispatchAction({ type: showTip, seriesIndex: 0, dataIndex: lastIndex }); }, 300);为什么要 setTimeout因为 setOption 是异步渲染的需要等渲染完成后再派发事件不然可能在图表还没有绑定事件时触发导致不生效。第二个方案是监听全局鼠标事件在 mousemove 事件里判断如果靠近最后一个点就主动显示 tooltip。这个方案更灵活但代码量更大。还有一个常用的配套设置把 tooltip 的 trigger 设为 axis配合 axisPointer 的 type 设为 line 或 cross这样用户能清楚看到当前数据点在坐标轴上的位置。移动端图表还有一个优化点就是 tooltip 的内容不要一次展示太多数据横屏显示、字号适当放大、加上单位这些细节虽然不在试卷里直接考但在项目实战里很重要。5. 编程题与手写代码的解题套路5.1 手写JS四件套贝壳移动端试卷的编程题如果投的是 H5 或跨端方向很可能会要求手写防抖、节流、深拷贝、Promise 这类工具函数。这几个题看起来简单但真要在线写很多人会栽在细节上。比如防抖核心是每次触发都清除上一次的定时器function debounce(fn, delay 300) { let timer null; return function(...args) { if (timer) clearTimeout(timer); timer setTimeout(() { fn.apply(this, args); }, delay); }; }注意点在于返回的函数里 this 要能正确绑定所以用 fn.apply(this, args) 而不是直接 fn(...args)。节流也是同样套路但是用时间戳或者定时器来控制执行频率。深拷贝的问题更多很多人递归拷贝遇到循环引用就直接爆栈正确做法是用 WeakMap 记录已经拷贝过的对象function deepClone(obj, map new WeakMap()) { if (obj null || typeof obj ! object) return obj; if (map.has(obj)) return map.get(obj); const clone Array.isArray(obj) ? [] : {}; map.set(obj, clone); for (const key of Object.keys(obj)) { clone[key] deepClone(obj[key], map); } return clone; }为什么用 WeakMap 而不是 Map因为 WeakMap 的键是弱引用不影响垃圾回收当原对象不再被使用后WeakMap 里的键值对可以被自动回收。这个点如果能在代码注释或面试中讲出来会是一个明显的加分项。5.2 算法题的边界意识与复杂度分析算法题方面贝壳的卷子难度定位是“让人能写出来但不细心会错”。出现过比较多的类型包括两数之和、最大子序和、二叉树层序遍历、链表反转、最长回文子串。这些题在 LeetCode 上都是简单或中等难度但笔试现场的氛围不同很容易因为边界条件丢分。我举一个最常被忽视的例子二叉树层序遍历。很多人知道用队列做 BFS但在输出格式上会踩坑。LeetCode 要求返回的是二维数组每一层一个子数组。如果只是单纯把节点值塞进一个一维数组那就不符合要求。正确写法是每次循环前先记录当前队列长度这个长度就是当前层的节点数然后循环处理这一层function levelOrder(root) { if (!root) return []; const result []; const queue [root]; while (queue.length) { const levelSize queue.length; const level []; for (let i 0; i levelSize; i) { const node queue.shift(); level.push(node.val); if (node.left) queue.push(node.left); if (node.right) queue.push(node.right); } result.push(level); } return result; }关键就在const levelSize queue.length这一行。如果不缓存它直接用queue.length作为循环次数队列会不断变长循环次数就会失控把后面的节点也吞进当前层。再比如两数之和最容易出错的不是哈希表解法而是处理重复元素。如果数组是 [3, 3]目标值是 6用if (map.has(target - nums[i]))判断时要把当前元素先放进 map 还是先判断正确顺序是先检查 map 里有没有差值然后把当前元素加入 map。也就是“先查后存”。如果把顺序搞反[3, 3] 这个用例就会返回 [0, 1] 变成 [0, 0] 之类错误答案。笔试的时候一定要想清楚哈希表存的是“已经扫描过的元素”不是“当前元素”。写完算法题之后建议再花 30 秒写上时间和空间复杂度。这不是加分项而是失分项——很多评分标准里明确写了“未分析复杂度扣分”。时间复杂度用大 O 表示说明最坏情况空间复杂度要说明额外开了多少空间。比如哈希表解法时间复杂度 O(n)空间复杂度 O(n)因为最坏情况下所有元素都放进哈希表里了。6. 从试卷反推的备战清单与避坑经验6.1 岗位分方向复习备考贝壳移动端校招最忌讳的就是“什么都学什么都不深”。我的建议是先确定方向再按方向分配时间。投 Android重点放在 Java/Kotlin、四大组件、Handler、启动模式、性能优化和 Jetpack 常用库上iOS 部分了解即可投 iOS重点放在 Swift/OC、ARC、Block、Runloop、UIKit 和内存管理上投 H5/小程序方向重点放在 JS 基础、浏览器渲染机制、跨端框架、性能优化、手写代码这几块。这里有个容易忽略的点就是网络题不管哪个方向都会考。TCP、HTTP、HTTPS 这些属于“移动开发者的公共底座”没有方向区分。每次校招前我都会提醒基础三件套Java/数据结构、网络、操作系统一定要先过完再去看平台专项否则很容易出现“Android 原理背得很熟但三握四挥答不上来”的尴尬局面。6.2 一份可落地的冲刺计划如果按一个月时间准备我的建议是把时间切成三段。前两周刷基础每天上午看网络和操作系统下午刷 LeetCode 2~3 道中等题晚上背平台必考点。中间一周做专项突破把你投的方向的常考题目整理成题单比如 Android 方向的 Handler、启动模式、内存泄漏每个考点找 3~5 道真题练手并尝试自己写成文字答案。最后一周做模拟严格按考试时间做一套完整试卷计时两小时做完后逐题复盘重点看是哪类题占用了过多时间。这里有个小技巧笔试前 3 天不要做新题只看错题和笔记。新题会带来焦虑而且边际收益很低。你需要的是稳定输出不是临时抱佛脚学会一个新知识点。把每个基础考点的答案用自己的话说一遍说到能不看笔记复述出来为止。6.3 我踩过的几个坑我自己当年校招时在移动端笔试上栽过几次跟头复盘下来最有价值的几个教训这里说给后来人。第一个坑是“只刷题不总结”。刷 LeetCode 100 道但如果每道题只是过了测试点就没再回头看考场上遇到变形题依然会懵。正确做法是每道题做完后写半行注释这题考什么、用了什么数据结构、边界在哪里。第二个坑是“手写代码不运行”。笔试毕竟是手写平时在 IDE 里按 tab 补全很爽到线上编辑器里没有提示连 forEach 的参数顺序都能写反。建议平时练习时用记事本或者白板写代码写完再复制到编辑器里跑。第三个坑是“前面选择题恋战”。有一道多选题拿不准反复琢磨了 10 分钟最后编程题来不及写这是最亏的。我的原则是不定项选择题如果在一道题上卡了 2 分钟还没思路直接凭第一感觉选完就走先保证编程题的完成度。以我个人这两年和校招同学打交道的体会贝壳找房这套移动端试卷其实是在用一套题完成三层筛选第一层筛基础扎实程度第二层筛平台理解深度第三层筛代码落地能力。能同时过这三关的人往往不是刷题最多的而是平时就习惯把每个机制的原理弄清楚、每段代码的边界想明白的人。如果你能把这份解析里的每个考点都吃透再配合真题练习我相信上岸的概率会大很多。最后再分享一个小偏方考前把每个核心考点写在一张 A4 纸上只写关键词比如“Handler - 内存泄漏 - WeakRef”考试当天空闲时间扫一遍进考场前再看一眼比抱着厚厚一本笔记翻有效得多。