尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

小红书Android笔试题卷二深度复盘:核心考点与答题策略解析

小红书Android笔试题卷二深度复盘:核心考点与答题策略解析 套题刷起来才发现小红书这套Android笔试题卷二的含金量比我想象中高。这里我结合自己当时刷题、整理、反复翻看的过程把这套题涉及的核心内容做一个相对完整的复盘。不保证内容一字不差但考点方向、出题偏好和展开方式应该能代表那一年校招Android方向的整体水准。如果你是准备投大厂移动端岗位或者日常开发中想检验自己基础扎不扎实这套题很值得拿出来过一遍。先说我对整张卷子的直观感觉它不考花哨的东西也基本不考特别偏门的新特性更多是把Android开发中最常用的基础能力打散重排然后刻意往里加变体。比如同一个知识点换个问法、换个场景就能淘汰一批“只背结论没做过深入思考”的候选人。题目覆盖面挺广的从Java基础、并发、虚拟机到Android的Handler、Binder、事件分发、绘制流程再到网络、序列化、内存优化几乎每条主线都拉了一两个题出来。量不算特别大但每一道题都需要你真正理解原理才能答得稳。1. 卷面整体版块与出题倾向分析1.1 选择题部分的隐藏分水岭选择题在整个卷子里占比不算低但说实话真正拉开差距的不是“记没记住”而是“能不能排除干扰项”。小红书这套卷子的选择题有个特点干扰项设置得非常接近正确答案经常把“边界条件”或“进程间通信的表现形式”这类容易被忽略的细节拿出来做文章。重点集中在几个区域Java并发synchronized与volatile的区别、锁的粒度、线程池的饱和策略、AQS的基本逻辑。JVM与内存GC Roots、引用类型、对象创建过程、类加载的双亲委派。Android消息机制Handler、Looper、MessageQueue三件套以及IdleHandler、同步屏障这类进阶问题。Binder与IPCBinder一次拷贝原理、AIDL的使用限制、为什么Android选择Binder而不是传统管道或Socket。View体系MeasureSpec的三种模式、requestLayout和invalidate的区别、事件分发的责任链。这些内容表面上都是在考“基础”但如果只靠背八股文很容易在变体题上翻车。比如它不会直接问“Handler的原理是什么”而是换一种方式主线程为什么不会因为Looper.loop()的死循环而卡死这种题就是不问你“是什么”直接问你“为什么”考的是有没有真正把机制串起来。1.2 简答题与编程题的实战导向简答题部分明显偏向“工作场景”与“底层原理”的结合。它不是要你背诵源码而是希望你能从工程角度给出解决问题的完整思路。我记得其中有几道很有代表性的多线程并发下载图片后如何保证UI正确更新如何设计一个“防止短时间内重复点击”的工具一个列表快速滑动时会遇到哪些性能问题你如何定位和解决序列化的方式有哪些具体在什么场景下选择哪一种这些问题看起来简单实际回答起来很容易暴露水平。因为每个问题都能从“标准答案”延伸到“为什么要这么写”的层面。比如“防重复点击”如果停留在“用时间戳判断”的层面那基本只能算及格如果能意识到需要“在触摸事件源头处理”或者“用RxJava的throttleFirst操作符封装”甚至考虑到子线程场景下的同步问题才算真正理解这个需求的本质。编程题那边主要还是围绕数据结构和算法但结合了Android常见的场景比如LRU Cache的实现、链表相关的操作、二叉树的遍历变体等。难度不会到LeetCode Hard但需要能顺手写出无bug的代码这本身就是一种筛选。2. 核心考点逐项拆解与答题思路2.1 Java基础与并发不只考语法考JMM与锁的本质在Android开发里Java并发是最容易被忽视却又最爱被考察的一块。卷子里关于多线程的题目不少而且喜欢往“可见性”“有序性”“原子性”这三个角度上钻。先说volatile。有一类题目会问volatile能不能保证原子性正确答案是不能。volatile只能保证可见性和一定程度的顺序性但a这种读改写操作本身并不是原子操作。这里需要从字节码层面理解a其实对应了多次指令操作不是一次完成的所以多线程下用volatile修饰int做计数最终结果依然可能丢失更新。答的时候如果能把“为什么不能保证”说清楚比单纯说“不能”要加分很多。再看synchronized和Lock的区别。这题基本是必考。需要注意几个层面synchronized隐式获取和释放锁异常时JVM会自动释放在JDK 1.6之后有偏向锁、轻量级锁、重量级锁的升级过程。Lock显式获取释放需要手动在finally中unlock支持非阻塞获取锁、可中断获取锁、超时获取锁功能上更灵活。底层实现synchronized基于监视器MonitorLock基于AQSAbstractQueuedSynchronizer。还有一个高频点线程池。题目会给出几种不同配置的线程池场景让你判断哪个适合哪个任务类型。这里要分清楚newCachedThreadPool弹性伸缩、newFixedThreadPool固定线程数、newScheduledThreadPool延时/周期任务和newSingleThreadExecutor单线程、保证任务顺序的适用场景。另外核心线程数、最大线程数、队列长度的关系也要能随手画出来。2.2 JVM与内存管理从对象创建到GC全集JVM这块卷子重点考的是对象创建过程、类加载机制、GC算法与引用类型。关于对象创建标准流程是类加载检查、分配内存、初始化零值、设置对象头、执行init方法。这里有个容易漏掉的细节内存分配时如何保证线程安全JVM的做法有CAS加失败重试、TLABThread Local Allocation Buffer两种主要手段。如果在答“对象怎么创建”时能把TLAB说出来说明你不只是背过流程而是对并发下的分配方案也理解。引用类型这块属于必考。强引用、软引用SoftReference、弱引用WeakReference、虚引用PhantomReference四者的区别以及它们分别适合用在什么场景要能一口气说清楚。尤其是软引用和弱引用在Android中的应用场景比如图片缓存框架早期的内存缓存用LRU算法配合强引用而不是盲目用软引用再比如有些源码里用WeakReference包裹Context防止内存泄漏这些都是项目经验与JVM知识的结合点。GC算法方面比较重要的是可达性分析。哪些对象可以作为GC Roots通常有这几类虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。考法上喜欢让你判断某个对象“会不会被回收”这时候不能只看“是否还有引用”要盯着“是否还和GC Roots直接或间接相连”。2.3 Android消息机制Handler的底层逻辑闭环Handler算是Android平台的老牌考点这些年热度从未降过。卷子里有一道经典变形题主线程Looper.loop()是一个死循环为什么不会导致App卡死这个问题要答通透至少要说清三层为什么主线程没有因为死循环而退出因为循环一直在处理消息队列没有消息时通过epoll机制进入休眠不会占用CPU空转。为什么不会ANR因为ANR的本质是“输入事件、广播、服务等在一定时间内没有被处理完”并不是主线程死循环导致的。Looper.loop()循环本身就是主线程的“正常存活方式”。Activity启动、点击事件、刷新UI都是通过向MessageQueue发消息来驱动所以loop的存在恰恰是这些机制运行的基础。与Handler配套的还有同步屏障、IdleHandler、消息优先级这些深水区。说到同步屏障它能拦截同步消息只让异步消息通过这一机制在UI绘制、Choreographer中都有应用可以用来保证绘制消息优先处理。IdleHandler则是在消息队列空闲时执行一些非紧急任务比如启动优化中的延迟初始化。2.4 View体系测量、布局、绘制与事件分发自定义View相关题目在卷子里的存在感很高。基本上离不开以下内容MeasureSpec由specSize和specMode组成三种模式分别是UNSPECIFIED、EXACTLY、AT_MOST。要记住父View对子View的MeasureSpec并不是直接复制自己的而是由自身的MeasureSpec和子View的LayoutParams共同决定的。很多刚入门的人在这里会理解偏误以为子View的MeasureSpec就是父View给的同一份。onMeasure、onLayout、onDraw的调用顺序和触发条件onMeasure负责测量尺寸onLayout负责决定子View的位置onDraw负责实际绘制。这三者的调用时机与View的flag有关容易出判断题。requestLayout和invalidate的区别requestLayout会触发整棵View树的measure和layout流程但不会触发drawinvalidate只在UI线程中标记重绘会触发draw但不会重新测量。如果两者分不清性能优化题就很容易翻车。事件分发这块核心就是dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三者的配合逻辑。题目往往会给一个具体的ViewGroup嵌套场景让你判断不同的返回值对应的点击效果。这里建议自己画一张“责任链”流程图把DOWN事件、MOVE事件、UP事件在不同返回值下的流向都理清楚比死记硬背要稳得多。还有一个和事件分发紧密相关的点是滑动冲突。比如ScrollView内部嵌套了横向滑动的ViewPager或者ListView嵌套了可展开的条目这类场景需要理解外部拦截法、内部拦截法的具体实现。小红书这套卷子有没有考滑动冲突我不太确定但大概率是不会回避这种高频场景题的。2.5 网络、序列化与数据持久化题目不难但容易踩坑网络部分重点围绕HTTP/HTTPS、HTTP缓存机制、TCP与UDP的区别来展开。题目不会特别深但很有画面感比如结合“弱网下图片加载失败后应该怎么处理”这样的场景。这里要注意几个容易被问到的点HTTP与HTTPS的区别不仅仅是加密还涉及CA证书的信任机制、非对称加密与对称加密的配合使用。HTTP缓存中有几个关键HeaderCache-Control、Expires、Last-Modified、ETag。要能说清“强缓存”和“协商缓存”的区别以及各自的优先级。TCP的三次握手和四次挥手过程UDP为什么更快但不可靠在音视频实时传输中为什么往往选UDP或基于UDP的QUIC。序列化方面Java原生Serializable与Android的Parcelable的区别是必考项。这里不是简单背“Serializable慢、Parcelable快”就结束要能解释为什么Serializable在序列化时会创建大量临时对象且通过反射获取类描述信息性能开销大Parcelable则是在进程间通信时通过共享内存完成数据打包没有频繁的IO操作专门为IPC场景设计。还要知道Parcelable不能用文件存储或网络传输这时需要自己定义一些转换方案或者直接用JSON格式。数据持久化这块SQLite和Room应该是默认考点。Room的优势在于编译期SQL校验、LiveData与协程的整合、迁移机制明确相比原生SQLiteOpenHelper开发效率会高很多。如果题目问“如何设计一张本地缓存表支持按时间清理”你要能给出建表字段主键、数据、时间戳、过期标识和索引建立的思路。2.6 性能优化与内存泄漏从理论映射到工程实践性能优化类问题在笔试题里通常以“现象→定位→解决”的形式出现。比如App启动慢如何优化要能从冷启动流程入手区分感性启动时间和视觉启动时间。优化手段包括减少Application的onCreate中耗时操作、用启动器优化任务调度、异步初始化非必要模块、提前加载首屏数据、用Splash布局替代白屏等。列表滑动卡顿怎么定位要能想到onBindViewHolder里有没有耗时操作、有没有过度绘制、图片是否做过压缩和缓存、item布局层级是否过深。还可以从工具层面回答用Systrace、CPU Profiler、GPU渲染模式分析来定位瓶颈点。内存泄漏相关的题最经典的场景就是Context泄漏。比如Activity被静态变量持有、Handler延迟消息导致Activity无法回收、单例持有Activity的Context、线程持有Activity引用等。回答这类题时最好能从“为什么会泄漏”和“怎样避免”两个维度分别说明把Static关键字的作用、Handler的弱引用写法、Activity生命周期这几个点串起来。还有一道值得特别注意的题是关于图片内存占用计算的。给你一张1024×1024的ARGB_8888图片放在drawable-xxhdpi目录下在特定分辨率的设备上加载后实际占用的内存是怎么算的这个需要清晰掌握Bitmap内存计算公式像素数×每个像素的字节数同时还要考虑不同drawable目录之间的缩放比例。能够把这里讲清楚以后在OOM问题上就不会再犯低级错误。3. 编程题的思路还原与代码示例3.1 实现一个不依赖SDK的LRU缓存LRU Cache是我印象里Android校招笔试中出现频率最高的一道算法题小红书这套题大概率也有类似的设计题。LRU的核心思想是“最近最少使用”淘汰一套优秀的解决方案是哈希表双向链表。哈希表保证O(1)时间获取数据双向链表保证数据按照访问顺序排列。import java.util.HashMap; import java.util.Map; public class LRUCacheK, V { static class NodeK, V { K key; V value; NodeK, V prev; NodeK, V next; Node(K key, V value) { this.key key; this.value value; } } private final int capacity; private final MapK, NodeK, V map new HashMap(); private final NodeK, V head new Node(null, null); private final NodeK, V tail new Node(null, null); public LRUCache(int capacity) { this.capacity capacity; head.next tail; tail.prev head; } public V get(K key) { NodeK, V node map.get(key); if (node null) { return null; } removeNode(node); insertToHead(node); return node.value; } public void put(K key, V value) { NodeK, V node map.get(key); if (node ! null) { node.value value; removeNode(node); insertToHead(node); } else { if (map.size() capacity) { NodeK, V removed tail.prev; removeNode(removed); map.remove(removed.key); } NodeK, V newNode new Node(key, value); map.put(key, newNode); insertToHead(newNode); } } private void removeNode(NodeK, V node) { node.prev.next node.next; node.next.prev node.prev; } private void insertToHead(NodeK, V node) { node.next head.next; node.prev head; head.next.prev node; head.next node; } }写这道题时有几个容易踩的坑忘记在访问get时更新节点位置。这个如果不做LRU逻辑就不成立变成了普通的淘汰策略。对于已存在的key执行put时需要先更新value再把它移动到链表头部这两个动作缺一不可。删除尾部节点时必须同步从map中移除映射否则容量会越界。如果能顺手写出泛型版本会比直接写Integer版本更显功底。题目如果允许还可以补充一个用LinkedHashMap的极简版但要说明“LinkedHashMap的accessOrder为true时可以实现LRU”这会让面试官觉得你是真的理解而不是只背了一版代码。3.2 多线程下的数据竞争处理与线程池调优编程题里如果有体现并发场景的一般是要求你实现一个“线程安全的计数器”或者“模拟多线程下载任务并汇总进度”。这类题实际上在考察原子操作的理解线程池的创建与使用任务间协作CountDownLatch、FutureTask、CompletableFuture下面是一个比较贴近Android场景的示例模拟并发下载几个任务全部完成后回调结果。import java.util.ArrayList; import java.util.List; import java.util.concurrent.*; public class DownloadTaskSimulator { static class DownloadResult { int index; boolean success; long costTime; DownloadResult(int index, boolean success, long costTime) { this.index index; this.success success; this.costTime costTime; } } public static void main(String[] args) throws InterruptedException { ExecutorService executor new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(16), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy() ); int taskCount 6; CountDownLatch latch new CountDownLatch(taskCount); ListFutureDownloadResult futures new ArrayList(); for (int i 0; i taskCount; i) { final int index i; futures.add(executor.submit(() - { try { Thread.sleep(500); long cost ThreadLocalRandom.current().nextLong(100, 300); Thread.sleep(cost); return new DownloadResult(index, true, cost); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return new DownloadResult(index, false, 0); } finally { latch.countDown(); } })); } latch.await(5, TimeUnit.SECONDS); int successCount 0; for (FutureDownloadResult future : futures) { try { DownloadResult result future.get(); if (result.success) { successCount; } } catch (ExecutionException e) { e.printStackTrace(); } } System.out.println(成功任务数: successCount); executor.shutdown(); } }这里想提醒一个面试里经常被追问的细节为什么不用Executors.newFixedThreadPool()因为newFixedThreadPool的等待队列是无界的如果任务持续积压会造成内存爆炸。手动穿参ThreadPoolExecutor能更精细地控制队列长度和拒绝策略。如果你在笔试现场能把这一点讲清楚会比直接写Executors工厂方法得分更高。3.3 手写一个生命周期可感知的计数器还有一类题会结合Android生命周期来出。比如“请用Kotlin或Java实现一个计数器在页面不可见时停止计数可见时继续计数。”这种题目考察的不只是计时器写法而是组件生命周期感知能力。我当时的回答思路是定义一个Counter类内部持有开始时间和累计时间利用LifecycleObserver监听ON_START与ON_STOP事件在ON_STOP时记录当前累计值在ON_START时重新恢复计算。这样能保证在页面进入后台或从后台返回时数据不会错乱。写这类题时注意避免直接在Activity里用Thread.sleep做循环计数这种方式既容易内存泄漏也会在页面销毁后继续运行。优先选择Handler.postDelayed或协程配合生命周期框架来做。4. 应试策略与答题节奏复盘4.1 时间分配不要在一道题上死磕整套卷子做下来我个人的体感是时间紧张程度中等偏上。选择题和简答题在深度上远超普通背诵题需要现场组织逻辑所以时间消耗会比较大。我的建议是选择题控制在30到40分钟以内遇到犹豫超过2分钟的题先标记跳过等到后面填涂时再回头。简答题按分值分配时间每道题控制在8到12分钟写答案时先列要点再补充细节不要一上来就长篇大论。编程题留足45分钟以上因为要写代码、检查边界条件而且很可能被要求现场改造或追问。编程题如果完全没思路不要空着至少把题目涉及的类结构或伪代码写出来。有些题目阅卷时看重思路大于运行结果写一个能说明逻辑的骨架都会比白卷强。4.2 把“背诵”升级为“讲解”做这套卷子最好的方式不是对着答案背一遍而是把自己想象成老师在给一个实习生讲题。每道题都尝试用“是什么、为什么、怎么用、有什么坑”四个维度去回答。比如被问到Handler时不要只说Handler是用于切换线程消息通信的工具。而是要说Handler由Looper和MessageQueue支撑。子线程向主线程发送消息时通过Handler.enqueueMessage将消息插入到主线程的MessageQueue中主线程的Looper.loop()不断循环取消息再分发给目标Handler的handleMessage方法。主线程之所以不会被这个循环卡死是因为没有消息时队列通过epoll机制阻塞而不是忙等。当你能把一道八股题讲成一个完整的“机制故事”时笔试和后续面试都会变得顺很多。4.3 考题背后的能力模型我复盘完这套题最大的感受是小红书出题人想找的并不是“背题高手”而是“真正理解Android运行机制有工程判断力能在复杂问题下快速给出合理方案”的候选者。很多题目表面上在问“怎么写”实际在问“怎么选”“为什么这么选”“如果条件变了该怎么办”。举个例子序列化那道题表面上是问Parcelable和Serializable的区别实际是在考察你在“进程间通信”“跨进程传输大数据”“网络传输场景”下的技术决策能力。如果你只会说“Parcelable性能好”而没有意识到它无法用于持久化与网络传输那说明你还没有真正把这些技术点放到实际开发环境中去思考。5. 高频考点速查表与自查清单这里把卷子里我印象比较深的知识点整理成一张速查表方便大家在考前做最后一遍自检模块核心考点容易踩的坑回答时需要补充的细节Java并发volatile、synchronized、AQS误以为volatile能保证原子性对变量的读改写操作需要加锁或使用原子类JVM引用类型、GC Roots、类加载混淆强引用与弱引用场景软引用适合实现内存敏感缓存弱引用适合避免泄漏Handler消息机制、同步屏障、IdleHandler只答“子线程发消息给主线程”主线程死循环为什么不死、epoll阻塞等待Binder一次拷贝、AIDL背了概念但说不清Memory-Mapping传统IPC两次拷贝与Binder一次拷贝的对比ViewMeasureSpec、事件分发、invalidate混淆requestLayout与invalidaterequestLayout只触发measure/layout不触发draw网络HTTP缓存、TCP/UDP混淆强缓存与协商缓存Cache-Control、ETag、Last-Modified的具体用法内存优化Bitmap计算、内存泄漏忽略drawable目录密度对内存的影响96×96 BMP在xxhdpi与xxxhdpi下内存差异序列化Serializable与Parcelable只背“Parcelable快”Parcelable不能用于存储或网络传输用这张表自测时如果某一项能不看资料独立讲满三分钟基本说明这个考点过关了如果讲两句就卡壳或者开始“嗯嗯啊啊”那这一块就需要再回去翻翻资料。6. 复盘后的几点体会与扩展建议整套题刷完我最想分享的一个体会是**这套题更像是一面镜子照出来的是日常开发中的积累深度而不是考前突击能填补的鸿沟。**如果你平时写代码时只是“Copy代码、跑通功能、完事”那面对卷子里那些“换个场景还认不认识这个知识点”的考题很可能会卡住但如果你平时有意识地去思考“这个API底层到底怎么实现的”“这个方案在另一个场景下还成立吗”答题时就会顺手很多。基于这套题我给自己列了一个后续知识补充清单这里也分享出来供参考深入读一遍Android消息机制与Choreographer源码搞清楚同步屏障和Vsync信号之间的联系。系统整理一遍Activity、Window、ViewRootImpl三者的创建与关联过程把自己从“只会用”提升到“懂原理”。动手写一个简易版的图片加载框架尝试集成内存缓存、磁盘缓存、网络请求与生命周期感知把所有分散考点汇聚成一个完整项目。把协程、Flow与生命周期组件整合起来对比它们与传统Handler线程模型之间的优劣拓宽并发编程的视野。这些内容不一定马上用得上但笔试中涉及的底层原理在未来的实际开发和晋升面试中大概率会以不同的面貌再次出现。早一点把原理啃下来后面会越走越轻松。
返回列表