ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

小满春招Android笔试通关指南:Handler、LRU与启动模式全解析

小满春招Android笔试通关指南:Handler、LRU与启动模式全解析 1. 笔试题型全景与整体策略1.1 小满春招笔试到底考什么我在2023年春季投了小满的Android研发岗第一轮笔试做完之后最大的感受是这套题不像市面上很多大厂笔试那样动不动就甩几道LeetCode Hard压场子而是在“基础是否扎实、工程习惯是否在线、有没有真正做过东西”这三个维度上反复试探。整套笔试分为四块单选、多选、两道编程题、两道简答设计题总时长120分钟。单选题大概12道范围覆盖Java/Kotlin语法、数据结构与算法基础、计算机网络、Android四大组件、Handler机制、进程与线程。多选题6道几乎全部围绕Android生命周期、启动模式、Layout渲染、性能优化展开。编程题两道一道偏数据结构缓存策略一道偏字符串处理。简答题两道一道让写某个系统组件的调用链一道给一个业务场景让你做技术方案设计。这里有个很关键的信息笔试不是纯客观题简答和设计题占了将近一半的分值。这意味着如果你只刷八股文选择题能拿满分但简答题写不出东西总分照样突围不了。我当时在筛选简历的时候也留意到小满的JD里面强调“对技术有热情、有独立解决问题的习惯”这套笔试的简答题基本就是在验证这两点。1.2 120分钟的实战时间分配我的建议是选择题和编程题加起来控制在55分钟以内剩下55分钟全部压在简答题上。为什么这么分因为选择题是机械记忆会就是会不会纠结两分钟和纠结二十分钟结果一样不如快速过。编程题基本十到十五分钟一道只要思路清晰、代码风格干净不会太拖节奏。简答题才是真正拉分的地方你需要在有限时间里把调用链讲全、把设计方案的取舍理由写清楚这非常消耗时间。我实际做题时选择多选只用了25分钟编程题用了30分钟简答题用了将近50分钟剩下15分钟检查。检查阶段重点不是复查选择题而是补简答题里没写完整的边界条件描述。题型题量建议用时备注单选1215分钟不会就跳过别恋战多选610分钟宁可少选不要错选编程题230分钟注意时间复杂度和边界简答设计题255分钟写清调用链和取舍理由检查-10分钟重点补简答题细节这套分配的前提是你选择题基础扎实。如果选择题都需要纠结说明知识点有盲区属于备考没到位不是时间分配能解决的。2. 核心知识点拆解从“背八股”到“能讲清原理”2.1 Handler消息机制必考但不该只背答案Handler机制在笔试里的出题率接近100%小满这套题也不例外。单选和多选各出了一道简答题里的调用链也和它有关。很多人能背出“Handler、Looper、MessageQueue、Message”四个类名但一旦问到底层怎么阻塞、为什么不会导致ANR就卡壳。核心原理一句话主线程的Looper.loop()是一个死循环它不断从MessageQueue里取消息队列为空时通过epoll机制让线程进入休眠等有新消息通过管道唤醒。ANR的发生和这个死循环没有直接关系ANR的原因是“事件处理超时”比如输入事件5秒没处理完或者广播前台10秒没处理完而不是因为loop()在死循环占用了CPU。这个区别必须能说清楚面试官就是靠这种细节判断你是真理解还是背的。MessageQueue的阻塞是通过nativePollOnce实现的底层用epoll监听一个管道文件描述符没有消息时休眠等待写入端唤醒。用这个机制的好处是线程在等待时不占用CPU这也是Android敢让主线程死循环的原因。补充一个容易被忽略的点MessageQueue里有个IdleHandler机制当队列暂时空了会回调空闲任务很多第三方框架的延迟初始化就是用它实现的。再说同步屏障这个是加分项。同步屏障插入后MessageQueue只会取出异步消息执行其他同步消息全部挂起。这解释了为什么Choreographer的VSYNC回调能优先于普通消息执行。写简答题时把这层补充上去分值会明显不一样。2.2 Activity启动模式与任务栈结合场景才好答这套笔试题里多选有一道是这样出的一个App有三个ActivityMainActivity是singleTask跳到DetailActivity之后又从通知栏点击跳回MainActivity问onNewIntent和任务栈的变化。这种题单纯背“singleTask复用栈内实例”是不够的要把任务栈的变化过程画出来。记住几个关键结论。singleTask启动时如果目标Activity已经在栈中存在会把它上面的所有Activity全部出栈然后回调onNewIntent不再走onCreate。singleInstance启动的Activity独占一个任务栈且这个栈里只有它自己适合来电页面这种不希望被其他Activity干扰的场景。standard和singleTop的区别在于singleTop只在栈顶做了判断栈顶是自己才复用否则新建主要用于避免连续点击按钮导致页面重复。taskAffinity这个属性也值得写。它决定Activity被放置在哪个任务栈。默认情况下同一个App的任务栈名字是包名如果你给某个Activity指定了不同的taskAffinity通过Intent启动时会寻找同名任务栈找不到就创建新栈。allowTaskReparenting可以让Activity在所在任务栈退到后台时迁移到相同taskAffinity的任务栈这个在多应用协作的场景下比较实用。简答题里如果考到启动流程我建议按这个顺序写startActivity - Instrumentation.execStartActivity - ActivityTaskManagerService跨进程调度 - ActivityTaskManagerInternal通知ActivityThread - ActivityThread通过H机制发送消息 - handleLaunchActivity创建Activity。把AMS和App进程之间的分工说明白主线程和Binder线程分别干了什么这题就稳了。2.3 性能优化笔试里的“隐性高分项”单选里有一道关于内存泄漏的题多选里有一道关于卡顿定位的题。泄漏题不难就是Handler持有Activity导致泄漏、静态Context引用、匿名内部类持有外部类这几个经典陷阱。卡顿定位题问的是Systrace、Profile、ANR日志的使用场景。我这里的建议是遇到性能优化题的答案不要只写工具名要写工具选型的依据。比如定位卡顿第一优先级是用Systrace抓trace因为它能看到系统级真实调度情况blocked、runnable、running状态一目了然。而Android Studio Profiler更适合看方法耗时和堆内存两者定位的问题层级不一样。内存泄漏的排查思路用LeakCanary可以做初步检测但笔试简答题如果考到不能只说“用LeakCanary”得分点在于“如何从堆转储文件里分析”。MAT或Memory Profiler打开hprof文件后找Dominator Tree看哪个对象持有了本该被回收的Context沿着GC Root引用链找到持有者。这个分析过程写出来才能证明你不是只会接插件。关于ANR要能区分三类输入事件5秒无响应、BroadcastReceiver前台10秒后台60秒、Service前台20秒后台200秒。定位ANR时优先看data/anr/目录下的traces文件重点找主线程卡在哪个锁或哪个系统调用上。2.4 Jetpack与MVVM不只是会用还要能讲清为什么小满这家公司做外贸SaaSApp里面业务表单、联系人列表、消息推送这类页面特别多数据变化频繁对状态管理的稳定性要求高。所以笔试里出现“为什么用MVVM而不是MVC”这种设计题很贴合业务实际。回答这道题的思路不要停留在“ViewModel能保存数据、LiveData能感知生命周期”这种API层面要讲清楚MVC为什么在移动端撑不住Activity同时承担View和Controller的职责业务逻辑和UI更新耦合在一起页面一复杂Activity能到一两千行测试也没法写。MVP用Presenter把逻辑抽出来但Presenter和View还是接口回调页面重建时状态容易丢。MVVM的核心是把“UI观测的数据”和“UI本身”解耦ViewModel不持有View引用数据用LiveData或StateFlow暴露Activity重建后数据还在视图自动重新订阅。另外要强调ViewModel的生命周期范围。ViewModel的onCleared在Activity真正finished时才回调旋转屏幕不会触发。LiveData的粘性特性也要注意新订阅者会立刻收到当前值这在用SingleLiveEvent或Flow的SharedFlow时需要特殊处理避免事件重复消费。这一层讲到面试官才会觉得你真的在实际项目里踩过坑。3. 编程题实战两道典型题从读题到AC的完整推演3.1 手写LRU缓存考察点不只是“会调LinkedHashMap”小满的编程题第一道就是LRU缓存题目要求设计一个支持get和put操作的数据结构容量有限超出容量时淘汰最久未使用的键值对要求get和put的时间复杂度都是O(1)。这题在LeetCode上是146题但笔试考它有一个隐蔽的坑直接new LinkedHashMap然后重写removeEldestEntry五分钟就能写完但想拿高分必须考虑多线程访问。题目没明说是否多线程我在代码注释里主动加了线程安全处理这就是加分项。用LinkedHashMap实现的核心逻辑是构造函数传入accessOrdertrue这样每次访问元素就会把它移动到链表尾部链表头部就是最久未使用的元素。put时重写removeEldestEntry当size() capacity时返回trueLinkedHashMap内部就会自动移除头节点。public class LRUCacheK, V { private final int capacity; private final LinkedHashMapK, V map; public LRUCache(int capacity) { this.capacity capacity; this.map new LinkedHashMapK, V(capacity, 0.75f, true) { Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() capacity; } }; } public synchronized V get(K key) { return map.getOrDefault(key, null); } public synchronized void put(K key, V value) { map.put(key, value); } }如果面试官想考手写程度会让你不用LinkedHashMap自己用HashMap双向链表实现。我笔试时写的是自实现版本因为LinkedHashMap方便但容易被认为是背题。双向链表的思路是HashMap负责O(1)查找链表维护访问顺序每次get先把节点从链表摘除再插到头部put时若容量满就删除尾节点并移除HashMap中的键。两个版本的时间复杂度都是O(1)。但要注意自实现版本里HashMap的扩容可能会影响微秒级的延迟笔试阶段不深究这一点但你可以主动在代码注释里写一句“若需要更稳定的延迟可初始化HashMap容量为capacity/0.75f防止扩容”这会让阅卷人觉得你考虑问题很细。3.2 字符串高频题滑动窗口的边界处理决定调试时间第二道编程题是给定一个字符串找出其中不含有重复字符的最长子串的长度。题目中规中矩但笔试环境里没有IDE提示纯手写容易在边界条件上栽跟头。滑动窗口的思路是用两个指针left和right维护一个窗口用HashMap记录每个字符最近出现的位置。right向右移动时如果当前字符在窗口内已经出现过就把left跳到max(left, 上次出现位置1)然后更新该字符的位置为right计算maxLen max(maxLen, right-left1)。public int lengthOfLongestSubstring(String s) { int[] lastIndex new int[128]; Arrays.fill(lastIndex, -1); int left 0, maxLen 0; for (int right 0; right s.length(); right) { char c s.charAt(right); if (lastIndex[c] left) { left lastIndex[c] 1; } lastIndex[c] right; maxLen Math.max(maxLen, right - left 1); } return maxLen; }这里面有三个易错点。第一判断是否出现重复时必须是lastIndex[c] left如果只判断lastIndex[c] ! -1窗口外的旧位置也会被误判导致left回退。第二更新left时用max(left, lastIndex[c]1)防止left被拉回之前的位置。第三如果用HashMap而不用int数组记得value存的是索引1还是索引这两种写法很容易混。笔试时我把数组从128扩展到256考虑的是扩展ASCII字符严格来说这不算标准答案但能体现对Unicode的思考。如果题目强调Unicode全量字符集那就该用HashMap。我一般会先写HashMap版本保证正确性再用数组版本优化性能这样阅卷人能看到两种取舍。3.3 手写生命周期感知组件一道容易忽略的“隐藏编程题”这两道编程题之外有一道简答题实际上需要你手写代码题目大意是设计一个带生命周期感知的倒计时器Activity销毁时自动停止。这个题考的是“会不会用Lifecycle架构”如果你在项目里用过LifecycleObserver二十分钟能写完如果只用过Handler.postDelayed很容易写出一堆内存泄漏代码。正确思路是定义一个类实现DefaultLifecycleObserver在onStart里启动倒计时onStop里取消。计数从N递减到0每秒回调一次倒计时结束允许外部通过回调通知UI。关键点在实现方式可以用CountDownTimer也可以用Handler.postDelayed配合SystemClock.elapsedRealtime计算剩余时间。CountDownTimer本身会持有Handler如果Activity销毁时没有cancel回调仍会执行。接入Lifecycle后在onDestroy里cancel就干净了。但我更推荐用Handler实现因为CountDownTimer源码内部就是Handler自己实现反而更容易控制恢复场景。倒计时恢复是容易被忽略的点Activity从后台回前台倒计时应该继续而不是重新开始。这时候需要用SystemClock.elapsedRealtime记录结束时间每次onStart时重新计算剩余时间并postDelayed这样即使系统可能触发Doze模式导致延迟时间也是正确的。这个细节写在代码注释里比写一大段设计说明更有说服力。4. 高频考点背后的原理拆解4.1 Binder机制为“性能优化”埋下的基础选择题里有一道关于进程间通信的题问Android为什么选用Binder而不是管道或共享内存。备选答案里还有Socket。我的答题思路要从三个角度写。第一是性能Binder只需一次拷贝传统管道需要两次。为什么能做到一次拷贝因为Binder驱动使用mmap把内核缓冲区映射到接收进程的用户空间发送进程的数据从用户空间拷贝到内核缓冲区后接收进程直接通过映射读取不需要再拷贝一次。这就是“一次拷贝”的本质。第二是安全Binder通信时内核会给每个进程分配UID调用方身份由内核校验不像共享内存那样需要应用层自己管理权限。第三是面向对象设计Binder的交互模型类似于接口调用传输对象而不是裸数据流这让Android四大组件的跨进程调用能保持面向对象的开发方式。如果笔试问到了“为什么不用共享内存”还有一个点要说共享内存虽然零拷贝性能最好但多进程同时读写时没有内核级的同步与安全校验开发复杂度和风险都很高不适合作为Android系统IPC的默认方案。4.2 App冷启动链路从点击图标到第一帧简答题里的调用链题目和启动流程高度相关。冷启动的完整链路可以分成三段来写。第一段是系统阶段Launcher调用startActivity后经过ActivityTaskManagerServiceATMS在system_server进程内完成解析、校验然后通过Socket通知Zygote进程fork出一个新的应用进程。Zygote是系统启动时就存在的进程预加载了所有Java类库和资源所以fork出来的新进程自带大部分运行环境这也是Android应用启动能这么快的原因之一。第二段是App进程阶段新进程入口是ActivityThread.main()它会创建主线程Looper然后调用Application.attach。注意这里是先attach后onCreate这两个方法的区别在于attach阶段ApplicationContext还没有创建完成不能做太多初始化而onCreate才是业务方做初始化的地方。第三段是Activity阶段ActivityThread通过H机制收到LAUNCH_ACTIVITY消息后在主线程上依次调用Activity的onCreate、onStart、onResume。onCreate里执行setContentView和view树的inflateonResume之后Choreographer收到第一帧VSYNC信号才会真正触发measure、layout、draw。所以“第一帧”不是Activity方法走到onResume就结束了而是要等渲染管线消费掉VSYNC信号。写这道题的加分项是提到减少启动耗时的手段Application.attach里避免做耗时代理初始化、用启动器管理异步任务、异步Inflate布局、提前初始化ContentProvider。尤其是ContentProvider它是App启动时最容易被忽略的耗时点系统会串行初始化所有ContentProvider所以很多框架会把初始化逻辑做成懒加载来避免卡启动。4.3 版本适配与构建工具链选择题的隐藏考点这套题的选择题里出现了两三个和构建环境相关的题比如Android 13的预测性返回手势、不同targetSdk对文件访问的影响、FileProvider的使用方式。热词里提到了Android Studio Hedgehog和AGP 8的兼容问题我实际笔试时也遇到了一道类似单选题。先明确一个结论Android Studio Hedgehog代号2023.1.1的Patch 2版本最高能支持AGP 8.2版本默认配套的Gradle是8.4。如果你的项目用的是AGP 8.0以上Gradle必须升到8.0以上且JDK版本必须是17。AGP 8.0开始移除了很多旧API比如不再支持compileSdkVersion直接在library模块里配置的某些写法同时要求所有依赖使用变体感知的元数据。这些如果项目里没踩过坑选择题很容易凭感觉选错。FileProvider那道题考的是content://和file://的区别。Android 7.0开始如果你用file://分享文件给其他应用会直接抛FileUriExposedException必须通过FileProvider生成content://URI并且可以临时授权给目标应用使用。热词里出现了content://com.baidu.searchbox.fileprovider这类字符串实际上就是第三方应用通过FileProvider暴露自己的目录。笔试里如果考到核心是写清getUriForFile如何把File路径映射成content URI以及grantUriPermission和Intent.FLAG_GRANT_READ_URI_PERMISSION配合使用的目的。既然聊到Android 13的适配还有一个小考点Android 13开始支持主题应用图标Themed Icons系统会读取应用提供的单色图标资源来让图标适配不同主题。这个知识点虽然冷门但选择题里出现过一次我用一句话记住在adaptive-icon的drawable里增加一个monochrome图层并且保证用单一颜色绘制不能带透明度渐变。5. 常见失分点与阅卷视角的答题技巧5.1 简单题里最容易丢分的3个细节笔试之后我和几个同批投递的同学对了下答案发现我们丢分的点高度集中在一些特别基础的地方。第一是equals和的区别这个题几乎成了必考但考法变了不是让你选概念而是给一段代码问输出。比如Integer a 127; Integer b 127; 问a b是多少以及Integer x 128; Integer y 128; 问x y是多少。前者是true后者是false因为IntegerCache默认缓存-128到127。这道题考的不只是equals还有装箱缓存的边界。第二是集合的fail-fast机制。HashMap在遍历过程中如果被其他线程修改会抛出ConcurrentModificationException。这个机制是基于modCount的变更检测不是线程安全的保证。笔试里会用一个“遍历时remove失败”的代码片段来考正确做法是使用Iterator的remove或者用ConcurrentHashMap。第三是String相关的内存分配。String s1 abc; String s2 new String(abc); s1 s2是false但s1.equals(s2)是true。因为字面量会进入字符串常量池new出来的在堆上。如果题目里加了intern()情况又不一样s2.intern() s1是true因为intern返回常量池里的引用。这些考点不难但很容易在考场紧张时出错。5.2 阅卷人真正在看什么我后来也帮团队筛过几轮笔试站在阅卷视角看一份答卷第一眼看的不是代码对不对而是代码风格和答题结构。编程题哪怕有少量逻辑问题只要变量命名清晰、有注释说明思路、边界条件处理了分数都不会太低。反之一个解法正确但代码没有缩进、变量名是a/b/c、没有任何注释的答卷阅卷人很难给你高分因为这不像是能进团队一起干活的人写的代码。简答题的回答结构也很重要。我的建议是先用一句话给出核心结论再分点展开原理最后用实际开发中的场景举例。比如Handler问题开篇写“Handler的本质是向MessageQueue投递任务由Looper循环取出并执行主线程通过这种方式处理异步消息”然后展开说从sendMessage到handleMessage经过哪些对象最后补一个“若线程没有Looper需要调用Looper.prepare创建”的坑这就是标准的高分结构。另外阅卷人会留意到你的技术视野。笔试题里有些扩展题比如热词里提到的I2C-tools在Android上的使用、Android OTA升级、PhoneStateListener的应用这些不会作为主要考点但如果选择题里出现答对了会体现出你有底层开发的积累。I2C-tools在Android上通常用于调试传感器、触摸屏等硬件设备需要root权限通过i2cget、i2cset命令直接与设备寄存器交互这在驱动开发和硬件调试领域非常有用。OTA升级相关考点关注的是升级包校验、A/B无缝升级、恢复模式这三件事。PhoneStateListener需要配合READ_PHONE_STATE权限使用用来监听电话状态变化这在通话类应用和车载系统里是常见需求。5.3 笔试中不要触碰的“暗雷”有一个很容易被忽略的雷区手写代码时不要依赖IDE自动补全的类。笔试里有人写HashMap时忘记import还有人写android.util.Log没引入直接用了这在纯手写环境下会丢掉不必要的分数。虽然不是编译运行题但阅卷人会觉得你的工程习惯不好。另一个雷区是用过时的API。比如用getColor(R.color.xxx)而不用ContextCompat.getColor用startPrivateMode而不考虑targetSdk版本。选择题里如果有“哪些写法已经废弃”之类的选项这些基本都是干扰项。我遇到的一个选项是AsyncTaskAndroid 11开始已经不建议使用官方推荐用协程或线程池这个知识点在笔试里出现的概率很高。还有Kotlin和Java混用导致的语法错误。笔试编程题如果可以选择Kotlin我建议用Kotlin写因为代码量更少、空安全处理能减少NPE。但如果你的Kotlin不熟别硬写举一个例子Kotlin里没有new关键字很多人Java写习惯了会顺手写个new这在阅卷时非常扎眼。考前最好把自己最熟悉的语言练到肌肉记忆。6. 2023年春季Android复习路线与时间安排6.1 第一周查漏补缺知识点扫盲备考笔试不要从LeetCode题海开始因为小满这种规模的笔试算法题难度有限真正拉开差距的是基础知识覆盖度。第一周重点是建立一个知识清单逐个排查。语言方面Java的集合、泛型、反射、异常、JVM内存模型Kotlin的协程、空安全、扩展函数。Android基础方面四大组件、Handler、事件分发、RecyclerView复用机制、动画原理。系统原理方面进程与线程、内存管理、Binder、广播的两种注册方式区别。体系结构方面MVP、MVC、MVVM、组件化、插件化。我复习时用的是“口述法”每看一个知识点不看书给自己讲一遍讲不顺的地方就是没理解透的地方。这个办法比刷题效率高很多因为人的大脑在“输出”时记忆效果最好考场上也能更快调出知识。6.2 第二周刷题和项目复盘双线并行第二周开始刷算法重点是LeetCode热题100里的数组、链表、字符串、二叉树、动态规划五大类。不用刷HardMedium足够。每一道题都要总结复杂度笔试简答题可能让你写“你的解法时间复杂度是多少”没有这步你会慌。项目复盘也是这周的重点。小满的笔试简答题里有业务场景设计如果你的项目经验很扎实会非常占优。我复盘项目时是围绕五个问题展开的项目整体架构是什么你在这个项目里负责什么遇到的最复杂的问题是什么你如何定位和解决的如果重新做你会怎么优化把这五个问题写下来基本就能答好90%的开放设计题。如果项目里做过性能优化把优化前后的数据、工具链和排障思路整理出来这比任何八股文都更有说服力。6.3 考前一天的三个动作考前一天不建议再学新知识了我做了三件事。第一把Easy和Medium的解题模板快速过一遍比如二分查找、快排、链表反转、层序遍历的标准写法避免考场手生。第二看一遍Android生命周期和启动模式的完整表格这是最高频也最容易混的知识点。第三把简历里提到的技术点全部列一遍确保被问到细节时能讲出来因为笔试通关后紧接着就是面试笔试答卷中的设计题往往会成为面试官追问的素材。还有一个小技巧提前在本地装好Android Studio用官网新版本配环境跑通一个demo项目。小满笔试用的是在线编程系统不是本地IDE但你平时练手的环境要和真实考试保持一致。热词里很多人问Android Studio怎么汉化、怎么装其实汉化只是习惯问题考试环境就是纯英文提前适应反而能避免考场找按钮浪费时间。我在实际使用中发现笔试前把官方文档里Activity和Fragment的生命周期图打印出来贴在眼前一个月效果比我反复背十遍都强。笔试当天虽然不能翻资料但那种对生命周期的“图形记忆”会让选择题答得非常快。2023年春招整体比往年卷但小满这套笔试并不偏怪考的都是Android开发日常离不开的东西吃透原理、写干净代码、表达清晰突围并没有想象中那么难。
返回列表