尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

网易Android校招笔试题解析:并发、Handler、自定义View与优化全覆盖

网易Android校招笔试题解析:并发、Handler、自定义View与优化全覆盖 1. 这套笔试卷到底考什么先说结论网易2018校园招聘Android开发工程师BJ笔试卷基本代表了当年一线互联网公司校招笔试的最高水准。它不是那种“背背八股文就能过”的试卷更像是一场对Android知识体系完整度的全面体检。作为一个在移动开发领域泡了十多年的老Android我拿到这套题的第一感觉是出题人很清楚自己想要什么样的人——不是会调接口的码农而是真正理解系统运行机制、能独立解决复杂问题的工程师。整套试卷的考察范围大致可以分成四块Java基础与并发、Android系统核心机制、自定义View与事件分发、性能优化与网络。这个布局很有意思它基本覆盖了一个Android开发者在实际工作中最常打交道的所有知识域。如果你在工作三五年后回头看这套题会发现它考察的深度和广度恰恰对应了一个中级工程师应该具备的完整知识栈。值得说的是2018年这个时间节点很微妙。那时候Kotlin刚被Google宣布为Android官方语言不久但校招笔试里仍然以Java为主说明企业在实际项目里的技术栈切换是有滞后性的。所以这套卷子里Java相关的题目占比不小尤其是并发和内存模型那块这些内容放在今天依然有很强的参考价值。注意这套题不是用来“刷”的而是用来“照镜子”的。我见过太多人把校招笔试当成高考一样去背题结果面试环节一追问就露馅。真正聪明的做法是把每一道题当成一个知识锚点沿着它把整个相关的知识网络梳理一遍。这篇文章我就按这个思路把这套卷子里最有代表性的考点逐个拆开讲清楚背后的原理、答题的思路以及在真实开发中这些知识是怎么用的。2. 基础考点Java并发与内存模型2.1 线程安全的本质可见性、原子性、有序性笔试卷子里关于Java并发的那几道题核心就围绕一个问题在什么情况下多线程程序会出问题答案无非是三个——可见性、原子性、有序性。这三个概念很多人能背出来但要真正理解需要在代码里踩过坑。先说可见性。Java内存模型规定每个线程有自己的工作内存线程对变量的操作其实是在工作内存里进行的然后再同步回主内存。如果一个线程修改了变量而另一个线程读到的还是旧值这就是可见性问题。经典场景就是那个永远跑不出来的循环// 这段代码在理论上可能永远无法退出 private static boolean flag true; // 线程A while (flag) { // 做点什么 } // 线程B flag false;如果flag没有用volatile修饰线程A可能永远看不到线程B的修改。用volatile修饰后每次读都会强制从主内存读取每次写都会强制刷回主内存。但volatile只能保证可见性和有序性不能保证原子性。再说原子性。i这种操作表面上是一行代码实际上对应了“读取-修改-写入”三步在多线程环境下就会出问题。要保证原子性要么用synchronized要么用AtomicInteger这些原子类。我在实际项目里就遇到过类似的问题统计在线用户数时用了普通的int变量加加结果线上数据总是对不上排查了半天才发现是并发问题。最后是有序性。CPU和编译器为了优化性能会对指令进行重排。在单线程下没问题但多线程下可能导致诡异的现象。经典的DCL单例模式如果instance不加volatile就可能因为指令重排导致拿到一个未初始化完成的对象// 正确的DCL写法 private static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; }那行new Singleton()在字节码层面会经历“分配内存、初始化对象、将引用指向内存”三个步骤第二步和第三步可能被重排。volatile在这里的作用就是禁止重排保证引用指向内存时对象一定初始化完毕。2.2 线程池为什么不用new Thread笔试卷里线程池的题目表面上是问参数实际上是在考察你是否理解线程池存在的意义。直接new Thread的问题在于每次都要创建和销毁线程频繁发生时有性能损耗线程数量不可控多了会耗尽内存缺少统一管理出问题不好排查。线程池的核心构造参数有七个核心线程数、最大线程数、空闲存活时间、时间单位、任务队列、线程工厂、拒绝策略。这里最容易混淆的是核心线程数和最大线程数之间的关系。我给出的记忆方法是核心线程数是“保底值班的人”最大线程数是“紧急情况下最多能叫来的人”任务队列是“排队等待的区域”。任务来了先给核心线程处理核心线程满了进队列队列满了才考虑创建新线程到最大线程数再满了就用拒绝策略。实际开发中IO密集型和CPU密集型任务的线程池配置策略完全不同。CPU密集型任务线程数设置为CPU核心数加一即可因为线程多了反而因为上下文切换降低效率。IO密集型任务线程数可以设置得更大一些因为线程大部分时间在等待IO返回不占CPU。2.3 锁机制synchronized与Lock的取舍关于锁的题目我认为出题人最想看到的是你不仅能说出两者的区别还能结合实际场景说明怎么选。synchronized是JVM层面实现的使用简单锁的获取和释放由虚拟机自动管理JDK 1.6之后引入了偏向锁、轻量级锁等一系列优化性能已经不输Lock。Lock是JDK层面的接口提供了tryLock、lockInterruptibly、公平锁等synchronized不具备的能力。选择synchronized还是Lock我的原则是能用synchronized就尽量用synchronized。因为它在语义上更清晰出问题的概率更低。只有遇到确实需要超时控制、可中断、读写分离这些场景才考虑用Lock。比如一个文件缓存系统的读操作远多于写操作就可以用ReentrantReadWriteLock来提升并发度。3. Android核心机制Handler与Binder3.1 Handler的完整链路从post到回调Handler是Android面试中永远绕不开的知识点这套笔试卷也不例外。往深了问一道Handler的题目就可以覆盖Android的消息机制、线程通信、内存泄漏等多个考点。先把完整链路理一遍当你调用handler.post(runnable)时这个runnable会被包装成一个Message对象然后通过sendMessageDelayed发送出去。Message进入MessageQueue后Looper通过无限循环调用queue.next()去取消息。取到消息后调用msg.target.dispatchMessage(msg)最终在handleMessage或者runnable的run方法中执行。这里有两个细节值得展开。第一MessageQueue.next()并不是一个简单的队列出队操作。当队列中没有消息时next方法会进入阻塞状态让出CPU。当有延迟消息时会调用nativePollOnce进行精确的定时等待。这个过程涉及到Linux的epoll机制这也是为什么Handler能够实现精准定时的原因。第二主线程的Looper是在ActivityThread.main()方法中通过Looper.prepareMainLooper()创建的。它从启动开始就不会退出一直在循环处理消息。这也就是为什么主线程可以一直响应用户操作也是为什么UI操作必须放在主线程——因为View的绘制和事件响应都是通过Handler机制排队执行的。关于Handler内存泄漏的经典问题如果用内部类创建Handler它会持有外部Activity的引用。如果Activity已经销毁但消息队列里还有延迟消息没处理完就会导致Activity无法被GC回收。解决办法是在onDestroy中移除所有消息或者用静态内部类加弱引用。3.2 Binder一次拷贝的奇妙设计Binder是Android系统进程间通信IPC的核心机制也是笔试中的高频考点。很多初学者会问Linux本身就有管道、Socket这些IPC方式为什么Android还要搞一个Binder原因主要有三点性能好、安全性高、调用方便。性能方面的核心优势是“一次拷贝”。传统IPC方式比如管道数据需要从发送进程的用户空间拷贝到内核空间再从内核空间拷贝到接收进程的用户空间总共两次拷贝。而Binder通过mmap技术在接收进程的用户空间和内核空间之间映射同一块物理内存数据只需要从发送进程用户空间拷贝一次到内核空间接收进程就能直接访问省掉了一次拷贝。安全性方面传统IPC方式无法可靠地知道对面的进程身份而Binder本身就有UID/PID信息内核可以据此进行权限校验。这也是为什么Android系统服务大多用Binder通信。在具体实现上Binder通信的基本过程是客户端调用代理对象的transact方法数据被序列化后通过内核的Binder驱动传递到服务端服务端在Binder线程池中找到一个线程来处理请求处理完结果再原路返回。这个过程涉及一个很重要的组件——Binder线程池。默认情况下每个进程的Binder线程池最多支持16个线程如果线程全部繁忙新的Binder请求就会阻塞等待。3.3 Activity启动流程一次完整的系统服务调用Activity的启动流程可以作为理解Android系统架构的典型样例。这个流程在18年的试卷里就已经是常客后来更是衍生出了各种“启动优化”的面试问题。简单的流程是这样的Activity A调用startActivity后这个请求会通过Binder传递给AMSActivityManagerService。AMS经过一系列解析和校验后通过Socket或Binder通知Zygote进程fork出新的应用进程如果目标Activity不在当前进程。Zygote fork出的新进程会创建Application、创建主线程的Looper然后AMS再通过Binder通知新进程的ActivityThread让它去创建Activity对象并执行onCreate。在这个流程中有两个容易被忽略的细节。第一Activity对象的创建是由系统通过反射完成的不是在代码里new出来的。第二虽然Activity的onCreate等生命周期方法是在新进程里执行的但这个进程是应用自己创建的还是系统帮忙创建的取决于当前的进程情况。若Activity本来就运行在现有进程中则不存在进程创建这一步。这套流程被面试官问烂了但我始终觉得能把它讲透的人确实不多。很多人卡在一个点为什么AMS能跨进程调用到应用进程里的ActivityThread答案是ApplicationThread它就是ActivityThread的一个内部Binder对象通过它系统可以反向调用应用里的方法。4. 自定义View与事件分发进阶必考4.1 事件分发机制的完整流程自定义View和事件分发是网易这套笔试卷里技术含量最高的部分。很多人在这一块栽跟头不是因为不了解单个方法的作用而是无法把整个流程串起来。Android触摸事件的分发核心是三个方法dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent。这三者的关系可以用一句口诀概括分发先走拦截看中间处理在最后。流程大致是这样的触摸事件从Activity进入往下传给顶层ViewGroup的dispatchTouchEventViewGroup先判断是否拦截不拦截就传给子View子View的dispatchTouchEvent如果处理不了就把事件回传给父View的onTouchEvent处理。这里最关键也最容易混淆的点是DOWN事件和MOVE/UP事件的处理差异。Android的事件流是以DOWN事件开始、以UP事件结束的一整条序列。DOWN事件决定了事件流的目标一旦某个View在DOWN事件中返回了true那么后续的MOVE和UP事件都会优先传给这个View。如果DOWN事件没有任何View处理那么后续的MOVE和UP也不会有人处理。再往深一层事件分发的设计意图是什么是为了实现灵活的触摸响应体系。ViewGroup可以在DOWN事件时决定是否拦截也可以在MOVE事件时根据滑动距离等条件动态决定是否拦截。经典场景是在ScrollView内部嵌入一个水平滑动的ViewPager如何在垂直滑动和水平滑动之间做出正确的拦截判断这就是对事件分发机制理解程度的一次全面检验。4.2 自定义View的绘制流程measure、layout、draw自定义View题目在笔试卷中的分量很重知识点也很密集。从measure到layout到draw每个阶段的每个方法都有其存在的意义。measure阶段View的measure方法接收父View传入的MeasureSpec包含specMode和specSize通过onMeasure方法计算出自己的宽高。MeasureSpec有三种模式UNSPECIFIED表示父容器不对子View有任何约束EXACTLY表示确定尺寸AT_MOST表示子View不能超过某个尺寸。对于自定义View如果需要支持wrap_content就必须在onMeasure里处理AT_MOST模式——不然wrap_content的效果和match_parent一样这是个非常经典的坑。layout阶段这个阶段的主要任务是根据measure阶段计算出的宽高结合父容器传入的left、top、right、bottom参数确定View在屏幕上的最终位置。对于ViewGroup需要重写onLayout来安排子View的位置。draw阶段draw方法会依次执行几个步骤——绘制背景、调用onDraw绘制自身内容、分发绘制子View、绘制装饰。对于自定义View来说onDraw是主要重写的方法。这里有个性能建议不要在onDraw里做创建对象、分配内存等操作因为onDraw在每次重绘时都会被调用频繁创建对象会触发频繁的GC。4.3 ViewGroup自定义布局与测量的联动自定义ViewGroup比自定义View要复杂一个量级因为涉及到多个子View的测量和布局协调。比较典型的操作是重写onMeasure遍历每个子View给它们分别调用measure再结合自己的大小约束算出最终的宽高在onLayout中遍历子View用计算好的位置逐个调用layout方法。一个经常被问到的点为什么自定义ViewGroup时需要关心子View的MeasureSpec生成规则因为父View在measure子View时会根据父View自己的MeasureSpec和子View的LayoutParams的宽高值生成子View的MeasureSpec。这就是getChildMeasureSpec方法的职责。如果这个规则处理不好子View的尺寸就会和预期不符。我强烈建议初学者用ViewDragHelper实现一个简单的可拖动ViewGroup来练手它能让你一次性接触事件分发、measure、layout和拦截逻辑是极好的学习材料。动手写一遍胜过背十遍八股文。5. 性能优化与网络决定上线质量的分水岭5.1 内存泄漏定位与修复的思路性能优化题在这套笔试卷中有不小的比重因为网易一直很看重应用的质量表现。内存泄漏是其中最重要的考点。常见的内存泄漏场景包括Handler持有Activity引用、静态变量持有Context、非静态内部类创建的实例在生命周期较长的对象中被引用等等。排查内存泄漏的实操路径我建议是先用Android Studio自带的Profiler工具抓取内存快照然后分析Heap Dump文件查看对象引用链。熟练之后可以配合LeakCanary这类工具做持续监控。定位到泄漏点后修复方式通常有几种用静态内部类替代非静态内部类、用弱引用持有外部对象、在合适的时机清理资源。有一个案例值得说一个音乐播放器项目播放页的Activity每次进出都会内存上涨查来查去发现是一个全局的单例Manager持有了播放页的Context。这个Manager需要访问资源文件但直接把Activity的Context塞给了单例。修复方式其实很简单——改用getApplicationContext作为全局Context。这个案例特别经典校招面试时讲到这个细节往往是加分项。5.2 卡顿优化从帧率到帧耗时Android卡顿优化的核心目标是保证16.6ms内完成一帧的渲染。超过这个时间用户就会感知到掉帧。要分析掉帧原因第一工具是Systrace它能精确记录每个系统方法的时间消耗。通过Systrace可以看到是由Layout耗时、Draw耗时还是GPU渲染耗时导致的掉帧再针对性地做优化。接下来是代码层面的优化。常见问题包括主线程做了耗时IO操作、onDraw中进行了复杂计算、布局层级过深导致measure和layout耗时过长等。布局优化建议使用扁平化的布局结构合理使用ConstraintLayout和merge、ViewStub等标签。列表优化方面要注意ViewHolder复用、避免在getView中创建对象、减少ItemView的嵌套层级。5.3 网络从TCP到HTTP网络模块的题目考察内容比较传统但也是必备的TCP三次握手和四次挥手、HTTP与HTTPS的区别、HTTP 2.0的特性等。TCP三次握手的核心目的是确认双方的收发能力正常并协商好初始序列号。HTTP与HTTPS的区别关键点是TLS握手过程和证书验证。这个考点在多益、字节等公司的面试中也是经常出现的。HTTP 2.0相对于1.1的几个关键改进多路复用、头部压缩、服务端推送。多路复用是一个连接可以同时并行处理多个请求解决了1.1时代队头阻塞的问题。我在项目里遇到过的情况是旧版本使用HTTP 1.1时图片资源加载慢切换到HTTP/2后有了明显提升就是因为多个资源可以并行下载。5.4 网络优化实践从OkHttp到底层细节从笔试角度说网络优化最常考的还是OkHttp的原理。一个高质量的答案需要串起这些点OkHttp通过Dispatcher维护请求队列以责任链模式串联多个Interceptor每个Interceptor各司其职——RetryAndFollowUpInterceptor负责重试和重定向BridgeInterceptor负责请求头补充CacheInterceptor处理缓存ConnectInterceptor负责建立连接CallServerInterceptor负责真正的网络请求。缓存策略这块也要会聊。HTTP缓存的核心是Cache-Control头和ETag。客户端发起一个带缓存的请求如果服务器返回的Cache-Control过期时间未到就直接用本地缓存不发网络请求。如果缓存过期会带If-None-Match发起条件请求服务器判断资源未修改则返回304不返回实体内容这样节省了流量和带宽。6. 这套卷子对今天的启发回过头来看这套2018年的网易Android笔试卷有几处今天依然值得借鉴的思路。第一基础数据结构与算法的考察其实很有度。这套题的算法部分更偏向于考察“能用合适的数据结构解决问题”的思路而不是一味地追求解难题。这对应到工作中的实际情况我们每天都在处理列表的增删改查、字符串拼接、去重、排序这些操作只是大多数时候这些操作都封装好了不需要你手写。但如果对底层的数据结构比如HashMap的扩容机制、ArrayList和LinkedList的差异不够理解就很容易写出线上性能不达标的代码。第二很多知识点的本质其实是对操作系统和网络原理的理解。比如Binder涉及到的内存映射、线程池涉及到的资源管理、HTTP缓存涉及的协议语义这些都是计算机基础知识的应用场景。所以这套卷子验证了一个结论好的Android开发工程师一定不是只会写业务代码而是对系统底层有足够的认知。第三如果放到今天再答这套卷子还需要补充一些新东西Jetpack Compose的UI体系、协程的并发模型、AGP的构建流程、Flutter/RN等跨端方案的原理对比。2018年那会儿没有这些概念但底层的考察逻辑是一样的——你是否真正理解了你每天在用的框架和工具背后的设计思想。另外一个观察如今面试里已经越来越少见到纯记忆性的题目更多是场景题——定义一个网络框架你如何设计App启动耗时你如何排查笔试的功能正在从“筛选知识量”转变成“筛选思维框架”。但这套卷子的价值恰在于它是一个很好的知识图谱你能答出多少基本就代表了你在Android生态里的地图有多大。如果你想拿它来准备面试我的建议不是逐题去找答案背而是每道题都走这样一个循环看到题目、先独立思考如何回答、然后查资料补全、最后落地写一段Demo验证。四个步骤走完一遍这个知识点在你的知识体系里才算真正站稳了这才是刷旧题的真正意义。
返回列表