2020金山办公Android校招笔试题解析:原理与实战经验

2020金山办公Android校招笔试题解析:原理与实战经验 2020年金山办公校招Android笔试题一那批题当时在圈子里传出来之后很多准备春招和秋招的同学都在讨论。我完整看了几遍一个直观的感受是这套题很能代表国内工具类App厂商的出题风格——不搞偏题怪题不炫技扎扎实实地看你对Android基础机制有没有真正吃透有没有在真实项目里踩过坑。这篇文章我就从这套题里几个核心考察方向入手把考点背后的原理、答题思路以及我这些年面试别人和带新人时积累的一些经验一起梳理一遍。不管你是准备校招还是想找个机会系统回顾一遍Android基础知识这篇内容都会对你有用。先说清楚我不会去逐题贴原题和标准答案因为笔试题更多考的是思路和原理把思路捋顺了题目怎么变都不慌。1. 整体设计思路校招笔试到底在筛什么人1.1 考点地图从这套题看Android校招的能力模型金山办公的这套笔试题整体可以划分成几个方向Java基础与并发、Android四大组件与系统机制、内存优化与性能治理、数据持久化方案、还有一部分设计思维和代码题。这个分布其实不是随便排的它对应的是校招Android工程师的三个核心能力层次。第一个层次是基础扎实度。Java集合、并发、JVM、Android生命周期、启动模式、消息机制这些是地基。校招候选人普遍没有太多真实项目经验笔试能考察的最可靠指标就是基础是否牢固。我面试时见过不少简历上写着“精通Android”的同学一问Handler的Looper在哪里创建的就卡住了。所以这类题不是刁难人是替后面的面试提前做一个筛选。第二个层次是实战敏感度。内存泄漏、卡顿、ANR、线程管理这些在真实项目里天天都会遇到。工具类App尤其吃这一套因为用户打开一个超大文档或者一个复杂表格内存和CPU的消耗是立竿见影的。笔试里出现这些方向说明出题人希望你不仅会写代码还要知道代码在真实设备上是如何运行的。第三个层次是设计思维。对MVVM这样的架构模式的理解、对模块化的思考、对代码可维护性的认知。这一部分往往没有绝对的标准答案但能拉开差距。我见过一些候选人基础题答得不错一聊到“如果让你设计一个公共组件你会考虑哪些问题”就完全没思路这就是平时只写业务代码不思考的结果。1.2 为什么办公软件团队会这么出题金山办公的核心产品是WPS这类办公软件有几个非常鲜明的技术特点处理大文件、长文档、复杂排版渲染和内存压力大用户会在碎片时间频繁切换文档对启动速度和恢复速度要求高草稿要自动保存多端要同步本地存储和跨进程交互非常频繁。这几个特点直接决定了团队对工程师的偏好。笔试里为什么会反复出现内存、存储、线程相关的内容因为WPS这种体量的App线上崩溃率里很大一部分来自内存问题而文档的异步加载、后台保存、格式解析全部依赖多线程和存储方案的设计。所以这不是一套“通用Android题”而是一套贴着业务场景设计的题。理解这一点你就能明白为什么有些你背过的“八股文”在这套题里可能不怎么出现而一些你平时觉得“工作里也用不到”的底层原理反而是重点。2. 高频考点深度拆解四大组件与系统机制2.1 Activity启动模式别只会背四种模式的名字Activity启动模式是校招笔试题里的常青树基本属于必考。standard、singleTop、singleTask、singleInstance这四个名字大部分人都能写出来但题目一换场景很多人就懵了。比如题目问“如果一个页面只允许存在一个实例再次启动时还要把最新数据传给它应该怎么选”很多人会直接回答singleTask但如果追问“数据从哪里取、onNewIntent里面怎么处理”就露馅了。正确的答题思路应该分三步先说结论再说原理最后落到代码。用singleTask没问题但要解释清楚为什么不用standard或者singleTop——standard会不断创建新实例栈里会堆积一摞相同的页面用户体验差singleTop只处理栈顶复用如果目标Activity不在栈顶依然会创建新实例。然后还要提到taskAffinity因为singleTask默认会去寻找或者创建同一个task如果你设置了不同的taskAffinity行为会变得很微妙这一点是很多人忽略的。最后要落到onNewIntent。很多人在笔试题里只写了“配置android:launchModesingleTask”却漏了onNewIntent里的setIntent逻辑。实际上系统只会调用onNewIntent并把新的Intent传进来你在onCreate里取的intent还是旧的如果不重新setIntent后续在onResume或者某个点击事件里再getIntent拿到的就是过期数据。这种细节没在真机调试里踩过坑的人很难答得完整。答这类题的时候还有一个技巧尽量把场景讲清楚。比如可以补一句“在应用内的文档编辑页打开本地文件时如果该页面已经在栈顶直接复用并刷新内容如果不在栈顶就把它上面的页面清掉并把它提到栈顶”。这样回答面试官能立刻感知到你是真的用过这个知识点而不是背的。2.2 ContentProvider与FileProvider跨进程数据共享的底层逻辑ContentProvider在很多人眼里就是用Android Studio的模板一键生成一个类然后写几个query、insert、update、delete方法就完了。但笔试题如果要考就不会只停在API层面。它真正想考的是ContentProvider到底是怎么跨进程的为什么用ContentProvider可以直接拿到其他App的数据底层核心是Binder。客户端通过ContentResolver发起调用经过Binder驱动把请求转发到Provider所在的进程由Provider完成数据操作后再把结果传回去。整个过程对调用方是透明的但底层涉及Binder线程池的调度、匿名共享内存传递大数据等机制。如果题目里出现“ContentProvider的onCreate和Application的onCreate谁先执行”这类问题考察的就是你是否有过比较深的源码阅读经历以及是否清楚ContentProvider是Android启动流程中一个独立初始化阶段。热搜词里有大量content://开头的路径比如content://com.baidu.searchbox.fileprovider这种格式。实际上这是FileProvider的Uri映射也就是把file://转换成content://。笔试如果涉及这个点核心考察方向有两个一个是你知不知道为什么要做这个转换另一个是你能不能手写一个FileProvider的配置。为什么不能用file://从Android 7.0开始应用之间共享文件如果还使用file://直接会抛FileUriExposedException。因为file://暴露了文件的绝对路径而content://可以通过授权机制只授予临时访问权限更安全。配置的时候需要在AndroidManifest里声明provider然后在res/xml里写file_paths用path映射到具体的目录。这里有个很坑的细节如果目录写错了运行时就报“Failed to find configured root”而且这个错误只在真机运行时才出现Android Studio的lint不会帮你查出来。2.3 Handler消息机制主线程为什么不会因为消息循环卡死Handler几乎是安卓面试必考题笔试题里出现的概率极高而且往往考得很细。最常见的问法是“主线程的Looper.loop()是个死循环为什么不会导致App卡死”。很多候选人张口就答“因为循环里会阻塞”但这不算完整答案需要从Linux的epoll机制说起。主线程的loop()会不断从MessageQueue里取消息队列空了就调用nativePollOnce进入休眠这时候主线程确实被挂起了但系统通过epoll监听文件描述符当有消息写入队列比如触摸事件、binder唤醒时epoll会立刻通过pipe唤醒主线程继续处理。这套机制最大的好处是主线程在没有消息的时候不占CPU但有了消息又能及时响应这就是Android能把“事件循环”和“UI响应”融合在一起的原因。和Handler配套考的知识点还有ThreadLocal。Looper是存在ThreadLocal里的一个线程只能有一个Looper所以你在子线程里调Looper.loop()之前必须先Looper.prepare()而在主线程里系统已经在启动阶段帮我们准备好了。笔试题经常在这里设一个陷阱new Handler()到底是什么时候绑定Looper的答案是在构造方法里它会去取当前线程的Looper如果没有就会抛异常“Cant create handler inside thread that has not called Looper.prepare()”。如果你在主线程里创建Handler当然没问题但在子线程里直接new Handler()就会崩。再往下如果题目考到内存泄漏就会把Handler和Activity的销毁场景结合起来Handler持有Activity的引用消息还在队列里没处理完Activity已经销毁了就会泄漏。标准解法是静态内部类 弱引用然后在onDestroy里removeCallbacksAndMessages(null)。这块内容我建议动手写一遍代码量不大但比背答案有用得多。3. 高频考点深度拆解内存、并发与存储3.1 内存泄漏一道题就能看出你有没有真正排查过线上问题内存泄漏在笔试题里通常是简答或者案例分析出题方式很灵活。比如给一段代码让你指出存在什么问题、怎么修复。这类题考察的已经不是记忆能力而是实打实的工程经验。常见的泄漏场景就那么几类静态变量持有Activity或View的引用、Handler和内部类导致的外部类泄漏、没有反注册的BroadcastReceiver或者EventBus、关闭不及时的流或者Cursor、单例里传入了Activity的Context。如果笔试题给了一个单例的代码让你找问题你一眼就要看出“这个单例持有的是Activity的Context应该用Application的Context”。但光答出这些还远远不够优秀的回答应该再补一层怎么发现和验证这类问题。我会提到LeakCanary的使用但更建议你说一下Android Studio自带的Memory Profiler因为不是所有公司都会在项目里集成LeakCanary而Memory Profiler是官方标配。操作流程是打开Profiler操作App进入可疑页面反复进出几次后手动GC然后Dump Java Heap用Analyzer Tasks让工具自动检测泄漏。分析结果里会给出泄漏链路从引用链的根节点一路往下你能清楚地看到是哪个对象持有导致的。这套流程答出来面试官就能确定你是真的做过性能优化的。还有个很常见的考察点是Context的泄漏。Activity的Context和Application的Context生命周期不一样如果一个生命周期很长的对象比如单例、全局集合持有的是Activity的Context当Activity销毁后这个引用依然存在Activity就无法被GC回收。笔试题会直接问“Activity的Context和Application的Context有什么区别分别应该在什么场景使用”展开答的时候记得补充牵涉到UI操作和启动Activity必须用Activity的Context但Toast、Dialog、访问系统服务这些可以用Application的Context因为它们的生命周期跟当前组件无关。3.2 线程池与并发控制为什么不能随便new Thread并发是笔试题里分量很重的一块。Java并发基础、线程池参数、同步机制、死锁等等都会涉及。Android场景下最常见的问题无非两种一种是你怎么在子线程里更新UI另一种是你怎么管理多个并发任务。很多人张口就是“用runOnUiThread”但笔试考的一定是更底层的线程池和HandlerThread这些机制。先说线程池。ThreadPoolExecutor的七大参数在笔试题里几乎每年都出现尤其是核心线程数、最大线程数、任务队列、拒绝策略。但光背参数没有用你得能讲清楚每个参数在什么场景下怎么调。比如一个IO密集型任务核心线程数可以设置成CPU核心数的两倍甚至更多因为IO操作会让线程阻塞阻塞期间CPU可以调度其他线程但如果是CPU密集型任务核心线程数设置成CPU核心数加一就比较合理线程太多反而会增加上下文切换的开销。拒绝策略也是一个容易被忽略的点——默认的AbortPolicy会直接抛RejectedExecutionException如果你的代码没有捕获很可能崩溃。我实际项目里比较常用的是CallerRunsPolicy任务满了就让提交任务的线程自己去执行这样等于天然实现了背压不会丢失任务也不会无限堆积。笔试如果问“你的任务提交频率很高怎么保证不丢任务”这两种策略的选择就可以作为答题重点。Android开发里还被经常问到的就是IntentService。虽然现在有WorkManager和协程但IntentService在源码层面的意义依然是教学级的它把HandlerThread和Service结合保证后台任务串行执行执行完自动stopSelf。笔试题如果问“多个耗时任务顺序执行、且结束后要释放资源你会怎么做”你用IntentService作答并解释它的内部原理分数就不会低。3.3 数据存储方案选型办公App为什么对持久化这么敏感数据持久化在通用Android笔试题里常考的点集中在SharedPreferences、SQLite、Room这几块。但如果套上金山办公的背景你会发现存储方案的考察还会延伸到MMKV、文件目录、断点续传、草稿保存这些更实际的内容。先说SharedPreferences。很多笔试题会问“SP有什么缺点”。最标准的回答是SP适合存储轻量级的键值对但不适合存大数据因为每次commit或者apply都是全量写入而且SP在初始化的时候会把整个文件加载到内存文件越大加载越慢还会占内存。在WPS这种动辄处理几十兆文档的场景里如果用SP去存文档相关的元数据性能会很难看。然后是SQLite。SQLite在笔试题里的考法通常集中在事务和索引上。比如“批量插入10000条数据怎么优化”标准答案是用事务把插入包在beginTransaction和setTransactionSuccessful之间否则每一条插入都是一个独立事务磁盘IO开销巨大。如果没有加分说明“预编译SQL语句”、“关闭自动生成索引”等细节回答会显得很单薄。Room是Google官方推荐的ORM框架对它的考察往往停留在注解和编译期生成代码这个层面。但如果你能在回答里提到“Room通过编译期校验SQL的正确性直接规避了手写SQL时的拼写错误”就会比单纯喊一句“Room好用”更有说服力。至于MMKV这类第三方组件笔试考的概率不高但如果题目开放性比较强比如“如果让你做一个键值对存储要求高性能、多进程访问你会怎么做”能把MMKV基于mmap的原理讲出来就是一个大加分项。4. 实操向从笔试题到工程落地4.1 一道典型编程题实现一个可以自动回收的LRU缓存笔试编程题里LRULeast Recently Used缓存几乎是最高频的题目之一。Android面试考它一方面是因为Glide、OkHttp这类第三方库底层都用到了LRU思想另一方面是它的实现复杂度适中既能考察数据结构功底又能考察编码习惯。最简单的实现是用LinkedHashMap。关键点在构造函数里传入accessOrdertrue让链表顺序按照访问顺序调整再重写removeEldestEntry方法当大小超过容量时就移除最久未使用的条目。Java的LinkedHashMap底层是一个双向链表加哈希表访问一次就把节点移到链表尾部头部的节点就是最久没被访问的移除头部节点就是一次O(1)操作。核心代码如下。public class LRUCacheK, V extends LinkedHashMapK, V { private final int maxSize; public LRUCache(int maxSize) { super(16, 0.75f, true); this.maxSize maxSize; } Override protected boolean removeEldestEntry(Map.EntryK, V eldest) { return size() maxSize; } }这段代码能跑但在真正的工程里还有个问题线程安全。如果多个线程同时读写这个缓存需要加锁或者用Collections.synchronizedMap包装一层而更优雅的做法是直接使用ConcurrentHashMap加一个双向链表自己实现。如果笔试时间允许我建议你写一个自实现的LRU版本用ConcurrentHashMap作为存储节点之间用双向链表维护访问顺序然后在moveToHead、addNode、removeNode这些关键操作里用锁保护。这样写出来复杂度高一些但能体现出你对并发场景的思考分数上限也更高。还有个容易忽略的细节缓存的容量设置要结合具体的业务场景。图片缓存通常按内存大小设置比如App最大堆的八分之一数据缓存则按条目数量设置更为方便。你可以展开聊聊在Glide里LruCache是按bitmap的字节数计算的而OkHttp的Cache策略又不一样。面试官听到这种细节通常都会觉得这个候选人是真的研究过源码而不是死记硬背。4.2 网络层设计笔试题从来不只考“怎么发一个请求”网络相关的笔试题在2020年那会儿已经很成熟了通常不会直接让你写一个HttpURLConnection发GET请求而是问“你的App网络层是怎么设计的”。这道题目的是考察你对封装、解耦、扩展性的理解而不是单纯考API。一条清晰的回答路径是基于Retrofit OkHttp 协程或者RxJava来搭建网络层Retrofit把接口定义和请求实现解耦OkHttp承接底层协议和连接复用拦截器负责打日志、加公共参数、统一处理token刷新和错误码。如果题目延伸出了“离线缓存怎么做”就要把OkHttp的CacheInterceptor拿出来讲——get请求可以按响应头里的Cache-Control做缓存而POST请求默认不缓存这一点很多人会答错。网络层设计还有一个常见考察点是HTTP和HTTPS的区别以及TLS握手的过程。笔试中如果出现相关概念题不需要你深入去抠加密算法的每一个细节但至少要能讲清楚对称加密和非对称加密是怎么配合的握手阶段用非对称加密交换密钥数据传输阶段用对称加密保证效率。在Android面试里能把这个闭环讲清楚的人比例其实不高。4.3 从笔试到二面这一题怎么往下追问笔试题的作答内容往往就是二面面试官追问你的素材。比如题目里考了Handler面试官大概率会继续问你在项目里有没有遇到过消息队列阻塞的问题怎么定位的遇到这种追问如果你只回答“用Looper.getMainLooper().getQueue()判断isIdle”显得很单薄更完整的思路是用Systrace抓trace或者用BlockCanary监控主线程卡顿根据堆栈定位到具体是哪个消息耗时。回答这类问题时把“问题现象—定位过程—解决方案—验证方式”这条链路完整说出来比给一个完美结论有用得多。同理如果你在笔试题里写了“我用LeakCanary查过内存泄漏”那二面大概率会追问“如果线上用户没有集成LeakCanary你怎么发现内存泄漏”。这个问题考的是线上监控和运营思维一般的回答是“通过线上打点上报内存快照、关注OOM率、用hprof分析线上堆转储文件”。如果你再补一句“OOM率下降了多少、具体怎么验证的”就会更有说服力。5. 常见问题与排查技巧实录5.1 笔试中反复出现的五个理解偏差这段时间我看了不少求职者的笔试复盘发现有几个坑是大家普遍会踩的。第一个是生命周期相关的问题比如“onSaveInstanceState在什么时机调用”很多人只写了“Activity被销毁前”但没有提到“它只在非用户主动销毁的情况下触发如系统回收、屏幕旋转而用户按返回键不会触发”。第二个是Binder的传输大小限制——一次Binder事务缓冲区默认是1MB超过这个大小即使没写错也会崩很多人在回答跨进程传大图时根本没往这个方向想。第三个是进程优先级与保活的混淆网上很多文章把“进程不被系统杀死”等同于“App永不死亡”这本身就是错误的预期笔试题只要考到“进程被系统回收时应该怎么恢复状态”就是在考察你是否理解onSaveInstanceState和持久化存储才是正确可靠的方案而不是去研究各种所谓的保活手段。第四个偏差是对View的绘制流程理解不全。Measure、Layout、Draw三个步骤背得很熟但一追问“wrap_content和match_parent在Measure时有什么区别”就说不清楚了。说到底View的宽高测量结果受父容器传递的MeasureSpec影响这是一个递归传递的过程不是View自己说了算。第五个偏差是把ANR和OOM混为一谈。ANR的本质是输入事件或广播处理超时而OOM是内存分配失败它们的发生机制和排查手段完全不同。5.2 笔试题的时间分配和读题策略笔试题的时间一般不算宽裕尤其是选择题和简答题混在一起的卷子。我见过很多候选人把大量时间花在了一道不确定的选择题上结果后面的编程题没时间写。这里分享一个我自己的做题策略先花一到两分钟把整张卷子浏览一遍按分值和对自己的难易度排序先把有把握的题目拿下再回头啃硬骨头。编程题通常分值最高我会留出充足的整块时间不要在简答题上反复纠结措辞。读题的时候一定要圈出题目里的限制条件。比如“用你熟悉的语言实现”“不能使用第三方库”“只考虑内存缓存不考虑磁盘缓存”这些都是决定答案走向的关键约束漏看一个可能整个代码结构就是错的。答题时如果要写伪代码请务必备注清楚关键步骤的意图因为阅卷人看的就是你思考的路径。关于编程题再提醒一点一定注意代码的缩进和变量命名。笔试题的阅卷很多时候是人工看的一份命名规范、结构清晰的代码即使最后没有完全跑通分数也不会低。相反代码乱成一团就算逻辑是对的也很容易被扣分。5.3 我的个人建议刷题之外更重要的事我对校招笔试的看法是这样的它是一道门槛但不是终点。刷题能帮你过这一关但如果只刷题不思考二面三面很难撑住。真正有效的准备方式是把每一道笔试题当作一个知识锚点顺着它往下挖掘底层原理。比如考了Handler就去找Handler、Looper、MessageQueue的源码看一遍考了内存泄漏就在自己的项目里真实解决一次泄漏并记录下排查过程。这个过程积累下来的认知才是面试时你能流畅表达的素材。另外建议准备校招的你把过去做过的项目好好梳理一遍哪怕很小的项目也要能讲出“这里为什么这么设计”“遇到了什么问题是怎么解决的”。面试官想看到的不是一个“API调用熟练工”而是一个遇到问题能独立分析定位解决的工程师。这个能力在笔试里可能只能通过原理题间接体现但在面试里一定会被反复检验。如果非要给一个优先级我会说原理源码 项目复盘 刷题数量。基础扎实、原理清晰、项目真实这三样都有了无论是金山办公还是其他大厂的Android校招笔试面试你都会从容很多。