十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

2017美图客户端笔试题全解析:从Android到算法设计

2017美图客户端笔试题全解析:从Android到算法设计 1. 试卷整体观感这一份题到底在筛选什么样的人2017年美图秋招的客户端工程师笔试试卷放到今天回头看依然值得细嚼。那会儿美图系产品正处于用户量高速增长期美图秀秀、美颜相机、美拍在各自赛道都有很强存在感客户端团队对候选人的要求也相当明确基础扎实、思路清晰、能上手干活的工程师。整张卷子给我的第一印象是——不偏不怪但覆盖面很广没有那种“面试官刚在某个开源项目里看到的新奇概念”也没有特别偏门的底层八股绝大部分题目都在考察一个客户端工程师日常开发中躲不开的知识点。我当时拿到这套题的第一反应是出题人很清楚客户端岗位的日常是什么样。Android的四大组件、Handler消息机制、内存泄漏、多线程并发iOS的内存管理、Block、RunLoop再叠加上操作系统、网络、数据结构与算法基本把客户端工程师的“基本功底盘”全部扫了一遍。题型上分客观题、简答题、编程题和一道开放设计题题量不少正常发挥的话整场笔试需要比较紧凑的时间分配。如果你正准备客户端岗位的校招笔试这套卷子的参考价值不在于“背答案”而在于它清晰地划出了客户端工程师的知识边界——哪些是必须熟练掌握的哪些是了解即可的哪些是可以通过开放题看出工程思维的。这篇文章我按自己的理解把整张卷子拆开讲一遍包含每类题型的考察意图、典型题目的解题切入点以及我复盘之后觉得值得注意的细节。2. Android与iOS基础题看似送分实则拉开差距2.1 Activity启动模式不只是背四种模式的名字选择题里出现Activity启动模式的概率极高这套卷子也不例外。standard、singleTop、singleTask、singleInstance四种模式大部分人都能背出定义但题目往往会加一层场景包装。我记得有一道题大概是问某个社交类App从通知栏点击跳转到某个页面再返回到主界面这个过程中启动模式应该如何设置才能避免创建多个MainActivity实例。这道题真正想考察的不是你能不能默写四种模式的适用场景而是你有没有遇到过“通知栏多次点击导致页面栈混乱”这种实际问题。答案其实落在singleTask上因为singleTask模式会保证任务栈中只有一个实例并且当实例已存在时会清空它之上的所有Activity正好符合“从任意入口回到主界面只保留一个实例”的需求。但很多人会误选singleTop理由是“主界面已经在栈顶”实际场景里主界面通常不在栈顶而是被其他页面压在下面singleTop只在栈顶才生效所以它并不能解决多实例的问题。类似的坑在任务栈相关的题目里还会出现。你至少要把onNewIntent的触发时机和intent复用逻辑搞清楚。singleTask模式下如果实例已存在系统会调用onNewIntent把新的intent传进来而不是重新走onCreate很多人在重写onNewIntent时没有调用setIntent导致getIntent拿到的还是旧数据——这种细节才是笔试真正能筛选出有经验候选人的地方。2.2 Handler消息机制图能画出来题就赢了一半MessageQueue、Looper、Handler三者之间的关系是Android客户端笔试的保留项目。这套卷子里有一道简答题要求简述Handler消息机制的运行原理并且说明为什么主线程的Looper不需要我们手动创建。这类题想拿满分光写“Handler发送消息到MessageQueueLooper循环取出消息分发”是不够的。建议按这个顺序组织答案Looper.prepare()负责创建Looper对象内部会初始化一个MessageQueueLooper.loop()进入死循环不断从MessageQueue中取消息取到就分发给目标Handler的handleMessage方法Handler在发送消息时通过enqueueMessage把消息插入MessageQueue并持有目标Looper的引用主线程的Looper在ActivityThread的main方法中已经被prepareMainLooper和loop准备好了所以App启动后主线程就自动处于消息循环中还有一个隐蔽考点主线程的Looper不能退出所以如果主线程消息队列里有一个耗时消息会导致后续消息全部卡住这就是ANR的本质。答题时如果能主动提到MessageQueue的核心数据结构是单链表、按消息的when字段排序插入会显得你确实看过源码而不是背了面经。2.3 iOS内存管理与Block捕获绕不开的ARC细节美图这套卷子对Android和iOS是分开命题的iOS方向一定会有内存管理和Block相关的题目。2017年Swift已经出现但普及度还不算高所以笔试主体还是以Objective-C为主。内存管理部分主要考察ARC下对象的生命周期、循环引用的成因和解决方案。有一道典型题是关于Block捕获外部变量的。很多人知道在Block内部修改外部局部变量需要加__block修饰符但不够理解为什么。原因是Block在捕获外部局部变量时默认是值捕获也就是在Block创建时把这个变量的值拷贝了一份你在Block内部修改的其实是拷贝后的副本外部变量不会变化。而加了__block之后编译器会把变量包装成一个结构体Block捕获的是这个结构体的指针这样内外操作的就是同一个内存地址。如果能在答案里点出“__block变量在block被拷贝到堆上时也会跟着迁移”这个细节说明你对iOS内存管理不是停留在表面。这道题和Android的Handler内存泄漏本质上是同一个问题——长生命周期对象持有短生命周期对象的引用只是具体表现不同。能把两种平台的内存管理问题放在一起思考在笔试的开放题部分会很有优势。3. 数据结构与算法题笔试中的硬骨头和破题思路3.1 链表类题目双指针是万金油算法题在美图这套卷子里占比不算低而且风格偏向基础但需要临场思考的类型。链表相关的题目基本属于必考范围我印象比较深的一道是“判断一个单向链表是否有环如果有环找出环的入口节点”。这道题的标准解法是快慢指针快指针每次走两步慢指针每次走一步如果链表有环两个指针必然在环内相遇。问题在于第二步——如何找环的入口。这里有个不容易记住的结论相遇点到环入口的距离等于链表头到环入口的距离。所以当两个指针第一次相遇后把一个指针移回链表头然后两个指针每次都走一步再次相遇的位置就是环入口。这个结论推导起来不复杂但如果你现场推容易在紧张状态下出错。我的建议是提前推导并理解而不是死记结论假设链表头到环入口的距离是a环入口到相遇点的距离是b相遇点继续绕环到环入口的距离是c那么慢指针走了ab快指针走了abcb因为快指针速度是慢指针的两倍所以ab cb也就是a c。这个推导过程理解了你不仅能在笔试中写出来面试聊到的时候也能解释得清楚印象分会高不少。3.2 动态规划状态定义比状态转移重要还有一道动态规划题类似经典的最长上升子序列。题目描述是给定一个无序数组求最长的递增子序列长度。这类题目在客户端笔试中出现频率不如后端高但一旦出现往往是区分度最高的题目。动态规划的核心在于定义状态。对这道题来说dp[i]表示以第i个元素结尾的最长上升子序列长度那么状态转移就是初始化dp[i] 1因为单个元素本身就是一个长度为1的上升子序列转移对每个i遍历它之前的所有j如果nums[j] nums[i]那么dp[i] max(dp[i], dp[j] 1)最终结果所有dp[i]中的最大值时间复杂度O(n^2)空间复杂度O(n)。如果你能进一步想到用贪心二分把时间复杂度优化到O(nlogn)在笔试中会是很亮眼的加分项。优化的思路是维护一个递增的tails数组tails[k]表示长度为k1的上升子序列的最小末尾元素遍历数组时通过二分查找找到第一个大于等于当前元素的位置并替换这样保证了tails数组始终有序且尽可能小。3.3 字符串处理边界条件永远是丢分重灾区字符串相关的题目在客户端笔试里也很常见因为实际开发中对用户输入的处理需求特别多。这套卷子里的字符串题比较直白比如判断括号是否匹配、反转字符串中的单词等。这类题难点不在算法思路而在边界条件。以反转字符串中的单词为例the sky is blue反转后应该是blue is sky the但实际做题时会遇到几种边界情况字符串首尾有空格 hello world 反转后应该是world hello单词之间有多个空格a good example反转后应该变成example good a单个单词的字符串反转后保持不变空字符串和只有空格的字符串返回空很多人在LeetCode上写过这道题逻辑并不复杂先把整个字符串反转再把每个单词反转回来但在笔试这种手写代码的环境里最容易忽略的就是空格处理的细节。我在实际批改类似的卷子时发现能一次性处理好所有空格情况的人不到三成大部分人能想出思路但代码跑边界用例的时候就暴露问题。笔试中算法题的边界条件本质上是考察你写代码时是否考虑全面这是工程习惯的体现而不仅仅是算法能力。4. 操作系统、网络与并发客户端工程师的另一块拼图4.1 进程与线程概念题的必考姿势这套卷子对操作系统基础知识的考察不算深但一定会有进程和线程相关的题目。常见问法包括进程和线程的区别、进程间通信方式、线程同步方式等。答这类题的时候不要只背概念最好用一个类比把两者的关系说清楚。进程是操作系统资源分配的基本单位包含独立的地址空间、文件描述符、信号处理器等资源线程是CPU调度的基本单位同一进程内的线程共享进程的地址空间和资源。如果说进程是一个公司线程就是公司里的员工公司有独立的办公场所和财务独立地址空间和资源员工共享公司的会议室和打印机共享内存和文件描述符但每个员工有自己的工位和电脑独立的栈和寄存器。IPC方式这部分需要答完整管道含匿名管道和命名管道、消息队列、共享内存、信号、信号量、套接字。客户端开发中常用的是Binder机制Android特有的IPC方式如果能在写通用答案之后补充一句“Android开发中还会使用Binder作为主要的IPC手段它的设计避免了传统IPC的多余拷贝”会显得你对操作系统和平台结合有思考。4.2 TCP与HTTP网络题不能只背三次握手网络部分的选择题和简答题分布比较平均。TCP三次握手、四次挥手基本是必考的但出题角度通常不是“叙述三次握手的过程”而是给出一些变形。比如可能问TCP连接建立过程中SYN泛洪攻击发生在哪个阶段服务端收到SYN后进入SYN_RCVD状态如果迟迟收不到ACK会怎么样这种题目需要你不仅知道三次握手是“SYN - SYNACK - ACK”还要理解每一步的服务端状态变化和超时重传机制。HTTP相关的考察点则更贴近实际开发。比如HTTP和HTTPS的区别、HTTPS的握手流程、HTTP方法GET/POST/PUT/DELETE的语义差异等。其中有一类题很有意思短连接和长连接的区别以及客户端开发中为什么更倾向使用长连接。原因是短连接每次请求都要经历TCP三次握手和四次挥手网络开销大延迟高长连接复用已有的TCP连接减少了握手开销适合频繁请求的场景。但长连接也有问题比如服务端需要维护连接状态、空闲连接占用资源所以实际开发中一般会引入心跳机制来保活同时设置空闲超时来回收资源。4.3 线程池与并发比“用什么”更重要的是“怎么配”并发相关的简答题多数人会答“用线程池”但很少有人能说清楚线程池的核心参数怎么配、为什么这么配。这套卷子里有一道关于线程池的题目问的是ThreadPoolExecutor的核心线程数、最大线程数、阻塞队列、拒绝策略分别怎么影响任务的执行。这道题的回答思路应该是核心线程数决定了正常情况下能同时执行的任务数当核心线程都忙碌且阻塞队列未满时新任务会进入队列等待当队列也满了才会创建新的线程直到达到最大线程数如果最大线程数也满了就执行拒绝策略。更深入的问题是核心线程数怎么设置。如果是CPU密集型任务核心线程数一般设置为CPU核数1如果是IO密集型任务核心线程数可以设置得大一些比如CPU核数*2。原因是CPU密集型任务主要消耗CPU线程太多会导致频繁的上下文切换IO密集型任务大部分时间在等待IO线程多一些能更充分利用CPU。这种答案会让人觉得你有实际项目的并发处理经验而不只是背了面试题。5. 编程题与开放设计题笔试中最能看出一名工程师成色的部分5.1 手写LRU缓存最常见的“客户端感”算法题这套卷子的编程题里有一道LRU缓存设计题目要求实现一个LRU最近最少使用缓存支持get和put操作get返回缓存值put写入缓存并在容量不足时淘汰最久未使用的数据要求在O(1)时间复杂度内完成这两个操作。这道题非常贴近客户端实际场景因为图片缓存、网络缓存、数据缓存到处都用得到LRU策略。O(1)复杂度的要求决定了你必须用哈希表双向链表的组合哈希表实现O(1)的查找双向链表实现O(1)的节点移动和删除。具体逻辑是get时如果key存在把对应节点移到链表头部并返回value如果key不存在返回-1put时如果key已存在更新value并移到链表头部如果key不存在创建一个新节点加入链表头部然后检查容量是否超出超出则删除链表尾部节点并同步删除哈希表中对应的key我见过不少人在笔试时用Java的LinkedHashMap直接实现这没问题但一定要说明你理解LinkedHashMap的accessOrder参数的作用。accessOrder为true时get操作会把节点移到链表尾部这样尾部就是最近使用的淘汰时删除头部即可。如果你能写出这个细节面试官会认为你不仅会调API还理解内部机制。不过更稳妥的做法是手动实现双向链表虽然代码量多一点但能完整展示你对数据结构的掌握程度。注意几个细节链表节点要同时存储key和value因为删除尾部节点时需要知道对应的key才能在哈希表中同步删除为了方便操作建议使用虚拟头节点和虚拟尾节点避免处理链表为空的边界情况。5.2 二叉树的层序遍历要不要考层次感另一道编程题是二叉树的层序遍历顺便要求按层输出每一层的节点集合。这道题属于BFS广度优先搜索的基础应用用队列实现。但要注意“按层输出”和“按序遍历”的区别如果只是把节点按层序打出来一个队列直接遍历就行如果要按层分组输出需要在每一层开始时记录当前队列的大小然后一次性取出该层的所有节点。这个“记录队列大小”的技巧看起来很简单但现场写代码很容易漏掉。漏掉的后果是每层节点混在一起输出结果不对。我建议在练习时就养成这种思维模式凡是需要区分层次或批次的BFS问题都通过“在循环开始时记录当前队列长度”来实现分组。二叉树相关的算法题在客户端笔试中经常出现因为二叉树的遍历和递归思维跟UI视图树的渲染、布局计算有相通之处。Android的视图层级就是一棵树很多布局优化问题本质上都可以抽象成树的遍历问题。如果你在答题后能主动提到这一点会给面试官留下“这个候选人能把算法联系到实际业务”的印象。5.3 开放设计题如何设计一个图片加载框架这套卷子里有一道开放题的大意是如果让你设计一个图片加载库你会考虑哪些方面请给出你的设计思路。这道题没有标准答案考察的是工程思维和系统设计能力但可以从几个维度展开这也是后来很多面经里总结过的经典思路第一是缓存策略。图片加载库必须有多级缓存最常见的分层是内存缓存、磁盘缓存和网络缓存。内存缓存用LRU策略磁盘缓存可以用DiskLruCache网络层负责从远端拉取。加载图片时依次检查内存、磁盘、网络命中哪一层就在哪一层返回。这一部分如果能把Glide和Picasso的缓存设计差异点出来会显得你确实研究过主流库的实现。第二是生命周期管理。图片请求必须跟视图的生命周期绑定。比如在ListView或RecyclerView中item滑出屏幕时应该取消正在加载的图片请求否则会有图片错位和内存浪费的问题。在Android上可以注册Activity和Fragment的生命周期回调来管理请求的发起和取消。Glide会监听Activity的生命周期自动暂停和恢复请求这个设计细节很值得借鉴。第三是内存优化。大图片必须做采样压缩不能直接加载原图而是根据ImageView的实际尺寸计算出合适的inSampleSize同时要考虑Bitmap的复用避免频繁分配大内存导致GC压力。第四是加载策略。支持占位图、错误图、淡入动画等这个属于体验层面的设计能体现你对用户感受的重视。如果你能把以上几点写完整并且按照“缓存分层 - 生命周期 - 内存 - 体验”的逻辑组织答案这道开放题基本就稳了。最怕的是只写一句“我会用LRU做缓存”这跟没答一样。5.4 开放题的另一面如何优化App启动速度美图这套卷子还有一个和App启动速度优化相关的开放题我印象很深。题目大意是App启动时如果白屏时间过长你会从哪些方面分析和优化。这道题我觉得有必要单独拿出来说因为客户端工程师日常工作中最容易碰到启动性能问题而且这也是面试官想听到“你有真实分析思路”的高频场景。答题时可以从主线程耗时、布局加载、初始化任务三个方向展开。首先App启动后主线程要从onCreate一直执行到onResume任何阻塞主线程的操作都会延后首帧渲染时间。所以要排查Application和Activity的onCreate里有没有做磁盘IO、网络请求、大对象初始化这些耗时操作。答案是可以把非必要的初始化任务放到子线程或者使用懒加载而不是一股脑全在启动阶段执行。其次布局加载也是启动耗时的重灾区。尤其是冷启动时加载的首页布局如果层级嵌套过深、使用了过多的weight权重计算会导致measure和layout阶段耗时增加。优化手段包括使用ConstraintLayout减少布局层级、把不需要立即显示的View用ViewStub延迟加载、减少过度的wrap_content使用等。第三启动阶段如果有同步的数据库操作、SharedPreferences读写、SDK初始化都会直接拖慢启动速度。常见的做法是把这些操作改造成异步执行或者把SDK初始化放到启动后空闲时再执行。比如使用IdleHandler在主线程空闲后才执行不重要任务这是2017年前后比较流行的面试讨论点。这道题最重要的是展现出“定位问题—分析原因—分步解决—验证效果”的完整思路。如果只是说“用Systrace看一下卡顿点”说明你连基本的性能分析工具都没用过往往会扣分。更好的回答方式是说明白先通过工具找到启动阶段的主线程耗时分布再针对耗时点逐一优化最后通过启动耗时指标对比验证效果。6. 从这份笔试卷看客户端工程师的知识底牌与备考策略6.1 底层逻辑为什么考这些而不是考那些复盘完整套卷子后我觉得有两条主线非常清晰。第一条是平台基础能力Android和iOS方向的题目都紧扣日常开发中最常用、最核心的机制Android四大组件、消息循环、内存管理、渲染基础iOS的ARC、Block、RunLoop。这些内容不是面试官随手从网上找的题而是客户端工程师每天编写代码时都会在脑海里调用的底层知识。第二条是算法与数据结构基本功链表、树、动态规划、LRU缓存设计这些题目的共同特点是——它们几乎都能在客户端开发中找到对应的实际场景而不是纯为考察算法而考察。这也说明客户端笔试的高分策略不是背题库而是把知识点串成网络。比如Handler机制看似是Android专属问题但背后其实是操作系统的消息循环模型和iOS的RunLoop、事件循环机制是同一个思想在不同平台的具体实现。你能抓住“消息循环”这条线就能同时理解两个平台的核心机制。再比如LRU缓存设计数据结构的基础是哈希表和链表它跟操作系统的页面置换算法、数据库的缓冲池管理都是相通的。6.2 应试技巧时间分配和答题顺序的建议按照这套卷子的题量和难度建议先把客观题快速过一遍不会的可以先标记跳过不要在一道选择题上耗太久。我个人实际做过一次限时模拟完整做完加检查大约需要80到90分钟如果某道题卡了10分钟以上大概率是方向偏了先放一放回头再看反而容易想通。编程题建议先写主逻辑再补边界条件。很多人在笔试时犯的最大错误是一开始就想把代码写得很完美结果写到一半发现思路不对整段推倒重来。正确做法是先在白纸上把核心思路和数据结构确定下来然后快速写出主代码最后再检查特殊输入。这样即使时间不够也能拿到大部分分数。如果系统支持编译运行一定要把示例输入和边界输入都测一遍哪怕逻辑看起来没问题。简答题和开放题注意分点作答。阅卷人一天要看很多份卷子清晰的结构能帮对方快速找到你的答案要点。比如问“如何设计一个图片加载库”不要写一大段流水账而是分成“缓存策略”“生命周期管理”“内存优化”“加载策略”几个小标题来写每个小标题下面再补充两到三句话解释。这样既显得条理清楚也能保证踩分点都在。6.3 考后复盘我自己做题时踩过的坑和心得最后说几个我实际的体会。第一不要小看客观题。很多人觉得选择题简单但我在限时模拟时发现选择题里面有不少“最”“一定”“必须”之类的绝对化表述稍不留神就会被带偏。比如有的题问“以下哪种方式一定不会导致内存泄漏”如果对每个选项的知识点掌握不深很容易把“使用单例”误认为安全其实单例如果持有Activity的引用反而比局部变量持有更危险。第二手写代码的规范和风格很重要。阅卷人看到一份变量命名清晰、分支处理完整、有简单注释的代码和看到一份变量全是a、b、c、d且没有空行的代码印象是完全不同的。笔试不是只会运行算法是否正确还会观察你的工程素养。平时提交代码养成好习惯笔试中的代码质量自然会高。第三开放题是最值得花时间打磨的部分。客观题和编程题考察的是“你会不会”开放题考察的是“你懂不懂”。我见过很多候选人在开放题上写得很潦草两三行就结束其实这部分才是最能展现你项目经验和思考深度的机会。如果你做过一些项目把项目里真实遇到的难点和解决方案写进去会比空谈概念更有说服力。第四学有余力一定要研究一下主流开源库的源码。美图这份试卷的开放题没有直接考Glide和OkHttp但如果你能在回答中适当引用你读过源码的实现细节比如Glide的缓存设计、OkHttp的拦截器链会让面试官觉得你是一个主动钻研的人而不是只完成课堂作业的学生。这在校招候选人里是非常稀缺的特质能直接拉开和其他人的差距。我自己的经验是笔试这种事情刷题只能保证下限真正决定上限的是你对知识点的理解深度。手上这套2017年的美图卷子虽然年份不新了但它考察的知识结构和思维方式直到现在都适用。认真过一遍你会发现自己哪些地方薄弱哪些地方还需要补课这个过程本身就是备考最大的价值。
返回列表