小红书2020校招Android笔试题二卷考点深度拆解
最近帮一个学弟整理校招复习资料翻出了当年小红书2020校招Android方向笔试题卷二这份卷子覆盖面很扎实既有Java基础又有Android系统机制还有框架源码和开放题可以说把校招的高频考点一网打尽了。虽然试卷叫“卷二”但题目本身的含金量并不低很多知识点直到现在依然是面试官喜欢追问的方向非常值得认真做一遍。这篇文章我就以这套题为主线把每类题目的考点、答题思路、容易忽略的细节全部拆开聊一遍。不管你现在是在准备校招还是已经工作一段时间想查漏补缺都可以把这份复盘当作一份线索图顺着它去梳理自己的知识体系。1. 卷面全景2020年校招Android笔试题二卷到底考什么1.1 题型分布与分值逻辑先看整体结构。这份卷子的题型大致分为四类选择题、简答题、代码题和开放题。从我接触到的回忆版来看题目数量不算特别多但单题分值高尤其是简答题和代码题基本决定了你能不能拿到面试机会。我根据自己的记忆和当时其他同学反馈的版本整理了一个大致的题型分布表题型大致题量分值占比考察重点选择题8-10题20%左右Java基础、Android常识、网络协议简答题4-5题35%左右Handler机制、Activity启动模式、事件分发、内存优化代码题2-3题30%左右单例模式、算法题、自定义View测量开放题1-2题15%左右App启动优化、稳定性治理、架构设计这个分值分布透露了一个重要信息校招笔试不是单纯考你“背了多少概念”而是看你能不能把原理讲清楚、把代码写对、把方案落地。尤其是简答题和代码题占了大头纯靠刷选择题库很难拿高分。1.2 考点地图从Java基础到框架源码的高频区域把整张卷子的考点摊开看大概是四个梯度每个梯度的复习优先级和投入产出比都不一样第一梯度是Java基础主要包括JVM内存区域、垃圾回收、集合源码、并发编程。这一块的考查方式以选择题和简答题为主难度中等但出现频率极高几乎可以说是送分题前提是你真的理解而不是死记。第二梯度是Android系统机制重点集中在Handler消息机制、Activity启动模式、Service生命周期、BroadcastReceiver和ContentProvider这类四大组件相关的题目。这一块是区分度最大的区域基础好的同学能答得比较完整基础不牢的同学会卡在“底层原理”层面。第三梯度是UI与性能优化包括自定义View的测量流程、事件分发机制、内存泄漏、卡顿优化、ANR产生原因等。这个方向的技术栈和小红书这类内容型App的实际业务贴合度很高——图片列表、瀑布流、大图加载都是高频使用场景。第四梯度是三方框架源码和开放题比如Glide的缓存机制、OkHttp的拦截器链、EventBus的原理以及“如何优化冷启动速度”这类方案题。这部分能看出一个人到底有没有真正读过源码、有没有项目经验也是最容易拉开分数的地方。2. 基础题拆解Java与Android底层逻辑2.1 JVM内存划分与GC回收题这套卷子的选择题里有一类高频题目是“关于JVM内存区域下列说法正确的是”选项通常会混入“方法区是堆的一部分”“栈内存是线程共享的”“Java堆可以细分为新生代和老年代”“程序计数器可能抛出OutOfMemoryError”这类说法稍微对概念模糊就会选错。这道题背后的考察点其实很清晰校招生刚入职公司不会指望你马上写业务代码但希望你具备排查线上OOM问题的能力而这一切的基础就是理解JVM内存模型。答题时要抓住几点程序计数器是线程私有的且是唯一不会抛出OutOfMemoryError的区域。Java虚拟机栈也是线程私有的方法执行时创建的栈帧在这里栈深度不够时抛出StackOverflowError动态扩展时可能抛出OutOfMemoryError。Java堆是线程共享的对象实例和数组在这里分配也是GC管理的主要区域可以细分为新生代Eden、From Survivor、To Survivor和老年代。方法区在JDK 8之后被元空间取代不再是JVM堆的一部分存放类元信息、常量、静态变量等。关于GC回收题目会让判断“可达性分析算法中可以作为GC Roots的对象有哪些”。常见的GC Roots包括虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、JNI中引用的对象。很多人只知道堆和栈忽略方法区里的静态引用这一题的区分度就在这里。答题的时候不能只给概念最好补充一句自己的理解。比如我会加上GC Roots本质上是“当前肯定存活的对象集合”垃圾回收器从这些根出发做可达性分析扫不到的对象就可以回收。这比干巴巴背定义要生动得多。2.2 并发编程与集合源码题卷二里有一类代码题大纲外的隐藏考点synchronized和volatile的区别。它可能以选择题出现也可能在简答题里让你结合单例模式说明volatile的必要性。这两个关键字的区别是校招高频题标准答法是这样几条volatile保证可见性和有序性但不保证原子性synchronized可以保证原子性、可见性和有序性。volatile只能修饰变量synchronized可以修饰方法和代码块。volatile不会阻塞线程synchronized会导致线程阻塞和上下文切换。volatile本质是告诉JVM这个变量在所有线程的缓存中是不安全的每次使用都要到主内存读取synchronized是通过锁机制让多个线程互斥访问共享资源。还需要补充一点volatile还可以禁止指令重排序这在单例双重检查锁中非常关键。我见过很多候选人在这一步翻车他们知道DCL单例要加volatile但说不清为什么。原因是instance new Singleton()不是一个原子操作它会经历“分配内存、初始化对象、把引用指向内存”三个步骤如果指令重排序导致引用先指向了未初始化的内存另一个线程就可能拿到一个半初始化的对象volatile就是为了禁止这种重排序。集合源码里HashMap是必考项。2020年面试时的标准答案应该是JDK 7的HashMap使用数组加链表JDK 8之后改成了数组加链表加红黑树当链表长度超过8且数组长度大于等于64时链表会转成红黑树扩容时JDK 7是头插法可能产生循环链表JDK 8改成了尾插法。这里我建议答题时多说一层HashMap为什么线程不安全。因为多线程同时put可能触发数据覆盖多线程同时扩容可能形成环形链表导致死循环。实际业务中很少直接用HashMap做并发容器一般用ConcurrentHashMap它的核心优化是分段锁或CAS加synchronizedJDK 8里锁的粒度已经细化到了桶级别。3. Android高频考点逐题分析3.1 Handler消息机制为什么这道题是“必考题”在所有Android校招面试题里Handler绝对是出场率最高的一道没有之一。这份卷二也毫不意外地出了一道简答题请描述Handler消息机制的工作原理并说明Handler.post和View.post有什么区别。这道题乍看简单但想拿高分关键在于从“使用层”深入到“原理层”。基本回答要覆盖五个角色Looper负责消息循环每个线程只能有一个Looper通过Looper.prepare创建通过Looper.loop启动循环从消息队列中不断取出消息交给Handler处理。MessageQueue是消息队列内部用单链表数据结构维护消息按时间先后排序。它的enqueueMessage方法和next方法配合实现消息的插入和取出。Message是消息载体创建时尽量用Message.obtain来复用对象避免频繁创建导致内存抖动。Handler是消息的发送者和处理者发送消息时调用sendMessage或post最终都会进入enqueueMessage把Message放入MessageQueue然后再由Looper取出来回调handleMessage。ThreadLocal用于保证每个线程都有自己的Looper实例Looper.myLooper就是通过ThreadLocal获取当前线程绑定的Looper。除了链条本身面试官还喜欢追问主线程的Looper是怎么创建的。答案是应用启动时在ActivityThread的main方法里调用Looper.prepareMainLooper和Looper.loop所以主线程不需要手动创建Looper。如果让你在子线程创建Handler就必须先调用Looper.prepare否则会抛“Cant create handler inside thread that has not called Looper.prepare”的异常。追问再深一点MessageQueue没有消息时主线程会卡住吗不会因为next方法会调用nativePollOnce进入阻塞状态此时主线程不会消耗CPU当有新消息入队时通过nativeWake唤醒。底层机制是通过epoll监听事件存在没有消息就进入休眠的机制这也是Android能实现高性能消息调度的核心原因。进阶部分还可以提到同步屏障、IdleHandler、Handler.post的延迟任务准时性问题。尤其是同步屏障在系统源码里用得很多比如UI绘制消息优先级高于普通消息就是通过同步屏障实现的。这一块如果答出来基本就是加分项了。至于Handler.post和View.post的区别核心在“View.post会判断当前是否在主线程”如果当前是主线程直接执行如果不在主线程会通过内部Handler把任务post到主线程并且保证在View被attach到窗口时执行。它的一个隐藏特性是可以在onCreate里通过View.post拿到View的宽高因为此时消息已经在布局流程之后执行了。3.2 Activity启动模式与任务栈两种常考变形题Activity的启动模式是简答题里的另一道常客。这道题的常规答法是把四种模式背一遍standard标准模式每次启动都会创建新的Activity实例压入启动者的任务栈。singleTop栈顶复用如果Activity已经在栈顶不会创建新实例会回调onNewIntent。singleTask栈内复用如果栈中已存在该Activity实例会把其上面的Activity全部出栈并回调onNewIntent。singleInstance单实例模式Activity所在的任务栈只允许存在这一个Activity常用于来电界面等系统级页面。光背定义还不够我建议每答一种模式就补一个使用场景这样答案会丰满很多singleTop适合接收推送通知跳转的页面防止多次点击产生重复页面。singleTask适合App的主页或WebView容器页面保证只存在一个实例。singleInstance适合需要与主界面完全独立的页面比如来电、闹钟。这道题还有一个常见变形A启动BB的launchMode是singleTask那么A和B各在哪个任务栈这个问题比较绕答案取决于是否设置了taskAffinity。如果B设置了taskAffinity为其他任务栈名称那么系统会先查找对应任务栈是否存在存在则复用它不存在则创建新栈如果没有设置taskAffinity默认使用包名任务栈可能直接复用当前栈。很多候选人在这里卡住因为只背了四种模式没有理解taskAffinity和任务栈的关系。我自己的答题思路是先讲清楚“任务栈”是什么再讲四种模式最后补充onNewIntent、taskAffinity、Intent的FLAG_ACTIVITY_NEW_TASK和FLAG_ACTIVITY_CLEAR_TOP这几个相关的知识点。这样既有层次又能表现你确实理解了这个机制而不是背了一堆名词。3.3 自定义View测量与事件分发一道题串起两大知识块自定义View的题目几乎在校招笔试里不会缺席。这套卷子里的考法是给一个场景让你自定义一个类似小红书信息流图片比例的View要求宽高比固定为3:4手写onMeasure的关键代码。这道题表面考代码实际上考的是你有没有真正理解MeasureSpec。MeasureSpec是测量规格由size和mode组成mode有三种EXACTLY父View给定了精确尺寸对应match_parent和精确dp此时View的测量结果就是MeasureSpec的size。AT_MOST父View提供了最大尺寸对应wrap_content子View需要自己根据内容计算期望尺寸但不能超过这个最大值。UNSPECIFIED父View没有限制子View想多大就多大一般出现在ScrollView、RecyclerView的测量流程中。要写固定宽高比的View核心就是在onMeasure里修改测量结果当宽度确定后高度根据宽度的3/4重新计算然后调用setMeasuredDimension设置最终测量尺寸。代码大致是这样Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthSize MeasureSpec.getSize(widthMeasureSpec); int widthMode MeasureSpec.getMode(widthMeasureSpec); int heightSize MeasureSpec.getSize(heightMeasureSpec); int heightMode MeasureSpec.getMode(heightMeasureSpec); if (widthMode MeasureSpec.EXACTLY) { // 父View已经给出了精确宽度高度按比例计算 int height (int) (widthSize * 3f / 4f); setMeasuredDimension(widthSize, height); } else if (heightMode MeasureSpec.EXACTLY) { // 父View给出了精确高度宽度按比例计算 int width (int) (heightSize * 4f / 3f); setMeasuredDimension(width, heightSize); } else { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } }一个容易忽略的坑是如果宽高都设置了wrap_contentmode是AT_MOST此时不能直接按widthMode或heightMode的EXACTLY分支处理否则测量结果就是父容器的最大尺寸View会变形。我一般会加一个默认尺寸兜底再按比例计算。事件分发题目则完全不需要写代码更多是让你描述事件流的传递机制。标准答法是三大方法加一条主线dispatchTouchEvent负责分发是事件处理的入口。onInterceptTouchEvent只存在于ViewGroup负责拦截返回true表示拦截。onTouchEvent负责处理返回true表示消费事件。事件从Activity的dispatchTouchEvent开始依次向下传递给ViewGroup的dispatchTouchEvent、View的dispatchTouchEvent这就是“自上而下”的分发阶段如果没有任何View消费事件事件会沿原路“自下而上”返回最终由Activity处理。点击事件的传递顺序本质是Activity - Window - ViewGroup - View处理顺序反过来View - ViewGroup - Window - Activity。另一个高频追问是如何解决滑动冲突。常见的场景是垂直滑动的ScrollView里嵌套了一个水平滑动的RecyclerView或者一个ViewPager里嵌套了横向ListView。解决思路有外部拦截法和内部拦截法我推荐先掌握外部拦截法在父View的onInterceptTouchEvent中通过“移动方向来判断是否拦截”如果父View是垂直滚动子View是水平滚动那么当水平位移大于垂直位移时父View不拦截把事件交给子View反之父View拦截。判断位移时注意使用Math.abs计算绝对值不要直接用坐标差否则方向判断会出错。答题时如果能把下面这句话带上会显得很有经验不要在onTouchEvent里处理滑动冲突真正的拦截决策点应该放在onInterceptTouchEvent因为子View有机会通过requestDisallowInterceptTouchEvent在分发前阻止父View拦截。4. 代码题与开放题的答题思路4.1 单例模式代码题手写之外还要答什么卷二里有一道几乎所有Android校招都会出现的代码题手写单例模式。这道题表面上是送分题但很多同学只写了懒汉式而面试官真正想看到的是你掌握了哪几种写法、能不能说清每种写法的优缺点。我当时在答题时写的是双重检查锁这也是网上最推荐的一种。代码大概这样public class Singleton { private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; } }除了代码本身还要解释三个细节为什么要加volatile防止指令重排序导致拿到半初始化对象。为什么要双重判断外层判断避免每次都进入同步代码块提升性能内层判断保证实例只创建一次。为什么锁Singleton.class静态方法中不能用this锁所以要用类的Class对象当锁。如果面试官继续追问“单例怎么防止反序列化破坏”可以补充枚举单例。枚举单例是Effective Java里推荐的写法它天然支持序列化而且是线程安全的public enum SingletonEnum { INSTANCE; public void doSomething() {} }这种写法在Android项目里其实很少用因为枚举在旧版本上会增加包体积但在笔试里写出来能展示你对不同方案的理解深度。如果怕枚举有争议也可以提静态内部类单例它是利用类加载机制保证线程安全同时实现了懒加载public class SingletonHolder { private SingletonHolder() {} private static class Holder { private static final SingletonHolder instance new SingletonHolder(); } public static SingletonHolder getInstance() { return Holder.instance; } }关于静态内部类有一个容易出错的理解它并不是“在getInstance时才创建实例”而是在“第一次访问Holder类时”才触发类初始化过程所以能保证懒加载且线程安全。这一点答题时可以主动讲清楚面试官很吃这种细节。4.2 启动优化开放题从“背答案”到“讲方案”开放题是这套卷子比较有区分度的地方。2020年的题目问法是你负责的App冷启动速度偏慢请给出定位方法和优化方案。这道题没有标准答案但答得好不好一眼就能看出你有没有真正做过性能优化。我建议把答案分成三步定位、优化、验证。定位部分要讲清楚冷启动流程。冷启动指的是进程不存在系统从零开始创建进程并启动App的过程。时间消耗主要包括Application的attachBaseContext和onCreate、ContentProvider的初始化、Activity的onCreate和onResume、首帧绘制。定位时可以分阶段打点用adb命令看一下启动时间再用Android Studio自带的Profiler或自定义的埋点统计每个阶段的耗时找到瓶颈到底在Application、首屏Activity还是竞品SDK。优化部分可以分几个方向主线程做了什么耗时任务、有没有不必要的SDK初始化、有没有重复的View层级。常用的手段有把不需要立即执行的初始化任务放到子线程、延迟到空闲时再执行、通过启动器框架来实现异步化和依赖管理、使用启动红黑树或者启动器拓扑排序来控制任务顺序。另外一个很容易忽略的点是SharedPreferences的初始化尽量放到主线程懒加载避免首次读取时阻塞。验证部分必须带上数据意识不要说“感觉快了很多”而是要给出优化前后具体的启动时间对比。我用adb命令举例adb shell am start -W -n com.example.app/.MainActivity这个命令会输出TotalTime和WaitTime可以用来量化冷启动耗时。优化前可能是1200ms优化后可能是780ms这个数字比任何空泛的描述都有说服力。这道题的加分项是提醒模块化按需加载和启动器方案。比如常见思路是通过Jetpack Startup库或者自研的启动框架把任务分为“必须主线程同步”“可以主线程异步”“必须子线程”三种并且支持任务之间的等待关系。如果你在笔试里能把这些方案说清楚同时强调不能为了速度牺牲稳定性那这道题基本稳了。5. 备考策略与时间分配5.1 针对这套真题的复习节奏建议如果你现在距离校招笔试还有两到三周我的建议是不要眉毛胡子一把抓而是要围绕类似这套题的高频考点做三遍式复习。第一遍用一周时间把基础概念全部过一遍JVM内存模型、Java集合、并发、Android四大组件、Handler、自定义View、事件分发。这个阶段不需要刷太多题重点是建立完整的知识地图知道自己哪里薄弱。第二遍用一周时间专题突破。把每类题目的“为什么”挖透尤其是Handler、启动模式、事件分发这三块。方法很简单对着题目不看资料用给自己的话把答案讲出来能连续讲满三分钟不出错基本就过关了如果卡壳了说明这个点还没真正掌握。第三遍用剩下时间全真模拟。找一套完整的真题按考试时间计时做一遍重点练手写代码的规范性和答题的速度。代码题一定要手写在纸上或白板上不要在IDE里敲因为笔试往往不允许用编译器提示平时用IDE写代码习惯了真上手写很容易漏掉import、漏掉分号。我还整理了一张优先级表给参考优先级知识点建议耗时P0Handler、Activity启动模式、事件分发、单例模式5天P1JVM内存与GC、自定义View、启动优化3天P2HashMap/ConcurrentHashMap、synchronized/volatile2天P3三方框架源码Glide、OkHttp、EventBus、Binder2天5.2 面试官视角什么样的答案能拿高分我在几次模拟评审里发现候选人的答案可以分为三个档次分差的来源不是知识点本身而是组织方式。低分答案的特点是概念堆砌但缺乏逻辑。比如问“请解释Handler机制”回答是“Looper是一个消息循环MessageQueue是消息队列Handler用来发送和处理消息”每个名词都对但每个名词之间没有建立联系面试官无法判断你是真的理解了还是背了一堆术语。中等答案的特点是能完整描述主链路但缺少边界条件和异常情况。比如Handler机制能说到Looper-MessageQueue-Handler的关系但没有提到ThreadLocal、epoll机制没有回答“主线程为什么不会因消息队列为空而退出”。高分答案的特点是结论先行、结构清晰、有边界条件、有使用场景。我的答题套路是四句话先给出结论Handler的核心是“通过消息队列实现线程间通信”主线程维护一个消息循环子线程通过Handler向主线程发消息主线程在loop中取出消息并处理。再展开细节按Looper、MessageQueue、Message、Handler四个角色逐一说明每个角色一句话功能加一句话实现原理。再补边界说明“一个线程只能有一个Looper”、MessageQueue没有消息时通过nativePollOnce阻塞、postDelayed消息不是定时发送而是到时间才取出。最后延展场景实际项目中可以用Handler做子线程切换到主线程更新UI也可以用HandlerThread做串行任务队列。这种答题方式既保证了知识点的完整度又让面试官很容易跟着你的思路走不会觉得你在背课文。6. 常见刷题误区与复盘技巧6.1 三个高频踩坑点第一只背概念不追源码。很多同学能说出来“Handler是线程间通信机制”但你问他ContextImpl和Activity是什么时候关联的他就答不上来。校招笔试越来越看重源码理解尤其是Handler、ActivityThread、View这些核心类。我的建议是不要求全读源码但至少要把主流程的关键方法看一遍比如ActivityThread的main方法、ActivityThread的handleLaunchActivity、Handler的enqueueMessage。第二只看答案不做题。真题的数量本身不多很多同学习惯一边看答案一边点头感觉自己都会了等到合上文档再写才发现自己连单例模式的volatile都忘了写。这个方法我强烈不推荐。做题最大的价值在于暴露你“以为自己懂但实际不懂”的地方不要浪费这个发现问题的机会。第三忽略手写代码的规范性。校招笔试的手写代码题不要求你写出能一次运行通过的无瑕疵代码但基本规范还是要守住类名要首字母大写方法名要驼峰变量名要有意义。我用一个常见问题举例写单例模式时有人把方法名写成getinstance把静态字段写成INSTANCE这类细节都容易被扣分。平时练习时就要认真对待养成肌肉记忆。6.2 真题复盘的正确打开方式最后聊一聊复盘。做完一套真题千万不要对完答案就扔到一边那样做十套题也不会有明显提升。我自己的复盘流程是三步第一步把做错的题按知识点分类找出自己的薄弱区域。比如这套卷子里如果你错的是JVM内存题和GC题那就说明Java基础要补而不是只把错题记住。第二步每道错题至少做三件延伸问自己为什么错、正确的思路是什么、这一类题还有什么变形。我用Handler那道题举例如果你只是记住了MessageQueue的数据结构那你还能回答“MessageQueue如何保证按时间排序”“同步屏障是什么”“IdleHandler是什么时候执行的”吗。能延伸答出这些错误才真正变成了收获。第三步建立自己的错题本。错题本不需要太长一张表格就够了题目考点我的错误答案正确答案要点失分原因复盘结论Handler机制能说出主链路没提epoll需补充主线程阻塞与唤醒机制底层原理不熟阅读MessageQueue源码next方法事件分发能说出三大方法说不清拦截时机需要结合DOWN事件和MOVE事件分析缺少场景推导实际写Demo验证分发流程单例模式没写volatile需解释指令重排序只背代码不理解原因补充JMM内存模型知识这套方法看上去很慢但在实际备考过程中它的效率远高于快速刷完十套题却什么都没有留下的做法。回过头来看这份2020年的小红书校招Android笔试题卷二虽然已经过去几年了其中很多题目的技术栈也发生了一些变化但底层考察的几大能力——Java基础、Android系统机制、UI渲染、性能优化——至今仍然是Android面试的核心主线。与其焦虑题目难不难不如从这份卷子里提取出可以迁移的考点把整个知识体系补扎实。技术栈会变但底层的原理和思维方式不会过时。