那年的秋招,我在宿舍抱着笔记本刷开爱奇艺校园招聘的笔试链接,页面左上角倒计时跳出来的时候,手心里全是汗。考完Android方向笔试题(B)之后,我对着屏幕愣了很久——这套卷子的风格,跟我之前刷过的很多题库完全不是一个路子。它不怎么直接问“Activity有几种启动模式”,而是把启动模式塞进一个“通知栏点视频、正在播放页要不要重建”的场景里,让你在做题过程中,顺便模拟了一遍真实开发中的取舍。
现在回头看,这套B卷几乎可以当成一份Android校招知识点的浓缩体检报告:基础知识覆盖面广,但真正拉开差距的是那些需要“想一层为什么”的题。这篇复盘我拖了很久才写,就是因为每道题要讲清楚背后的原理,都得成体系地展开。今天我把几道印象最深、也最典型的题目整理出来,配上完整的解析思路和踩坑记录,希望能给正在准备Android校招的同学一点参考。
1. 先把这套B卷拆开看:题量、时间与筛选逻辑
1.1 卷面构成与90分钟的时间分配
我拿到的B卷,整体分为四个部分:单选题/多选题、简答题、编程题和一道综合设计题。总的考试时间是90分钟。单选题大概占20道左右,覆盖面很广——Java、Android、网络、操作系统、数据库都会沾一点,其中Android和Java的权重最高,网络和多媒体也有不小的比例。简答题一般有3到4道,需要你用一段话把一个机制讲清楚,比如“一次完整的触摸事件分发流程”“主线程Looper为什么不会卡死”。编程题通常是两道,一道偏算法,一道偏并发或多线程。综合设计题放在最后,给一个业务场景,让你给出技术方案,比如“播放页滑动卡顿怎么排查”。
我自己的失败经验是:在单选题上耗了太多时间。有几道选项看起来每个都对,反复看了一两分钟还是拿不准,结果后面编程题时间紧到差点没写完。笔试不是让你每道题都满分,而是让你在有限时间内拿到尽可能高的总分。比较合理的分配是:选择题控制在30分钟内,拿不准的先标出来,不要恋战;简答题留30分钟,每道题按“先说结论,再讲原理,最后补细节”的方式答;编程题留20分钟,剩下10分钟给综合设计题和整体检查。尤其是编程题,哪怕只写出核心思路、跑不出完整代码,也比空着强,阅卷会看思路。
1.2 视频类App的笔试风格:知识点都泡在业务里
爱奇艺是长视频平台,技术栈和业务特点决定了它的笔试风格和其他公司有明显区别。视频业务对三个方面的能力要求特别高:一是多媒体,包括播放器、音视频解码、硬解软解、HLS/DASH这类知识点;二是网络,视频是流量大户,HTTP协议、CDN、缓存策略肯定绕不开;三是性能优化,用户对播放流畅度和启动速度极其敏感,所以Bitmap内存、启动优化、卡顿排查这些题的比例明显高于一般综合类公司的卷子。
这套B卷给我最大的感受是:知识点还是那些知识点,但考法更“场景化”。比如网络题不会问你“TCP和UDP有什么区别”,而是问“视频播放过程中,你觉得哪些场景适合TCP,哪些场景适合UDP,为什么”;数据库题也不会只考SQL语法,而是结合“视频观看历史记录怎么设计表结构”。如果你只是把概念背得滚瓜烂熟,却不知道怎么结合实际业务,很容易在“为什么”环节露馅。我后来复盘时意识到,刷这套题最好的方式不是死记答案,而是把每个知识点都往自己的项目或常见App场景上靠一遍。
2. Java基础题遍地是坑:HashMap、锁与线程池的答法
2.1 HashMap:1.7到1.8,树化阈值8背后有讲究
这套卷子的Java部分有一道非常经典的题:JDK 1.8的HashMap相比1.7,结构上最大的变化是什么?为什么引入红黑树?为什么链表长度达到8才转红黑树?这道题我印象很深,因为选项里给出的几个阈值特别容易记混。
先讲结构变化。1.7的HashMap底层是“数组+链表”,哈希碰撞严重时链表会变得很长,查询要一步步遍历,最坏情况下复杂度是O(n)。1.8改成了“数组+链表+红黑树”:当链表的长度超过阈值8,并且数组容量达到64时,这个链表会转成一棵红黑树,查询复杂度从O(n)降为O(log n)。这里有个隐藏考点是,不是链表一超过8就一定树化,数组长度必须同时不小于64才行。如果数组还没扩容到64,更倾向于先resize扩容,把元素重新分散,而不是直接树化。
至于为什么阈值偏偏是8,源码注释里写得很明白:在负载因子0.75的前提下,单个哈希桶内的元素个数服从泊松分布,链表长度达到8的概率大约是亿分之六。也就是说,正常情况下几乎不可能出现链表长度超过8的情况,树化更多是为了防御恶意构造的哈希碰撞,防止系统被拖垮。这个概率推导过程,面试官比较喜欢听,写答案时能补一句“这是泊松分布下的概率结论”会很加分。
还有一个高频考点是扩容机制。1.7扩容时采用的是头插法,多线程并发扩容会产生链表环,导致下一次get操作死循环。1.8改成尾插法,避免了这个问题。另外,1.8扩容后旧元素的位置要么保持在原来的索引,要么移动到“原索引+旧容量”的新位置,判断依据是元素hash值新增的那一位是0还是1,这个设计让rehash的过程非常高效。最关键的是一点:HashMap本身是线程不安全的,并发场景要用ConcurrentHashMap,它的实现里用了CAS和synchronized锁桶头节点,性能比Hashtable好很多。
2.2 synchronized、Lock与线程池:简答题想拿满分要这样答
简答题里有一道多线程的题,问的是synchronized和ReentrantLock的区别,并说“在什么场景下你会优先选择ReentrantLock”。这道题看起来基础,但想答全其实不容易。
两者核心区别可以整理成一张表:
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 加锁方式 | 隐式,进入同步代码块自动获取 | 显式,需要lock()和unlock() |
| 释放锁 | 代码块执行完或异常自动释放 | 必须手动在finally中释放 |
| 可中断 | 不支持 | 支持lockInterruptibly() |
| 公平锁 | 只能是非公平 | 可设置公平/非公平 |
| 条件变量 | 用wait/notify,只能有一个等待队列 | 用Condition,支持多个等待队列 |
| 锁粒度 | 块结构 | 可以跨方法跨代码块(不推荐) |
回答时一定要提到ReentrantLock的“非块结构加锁”能力,也就是可以在一个方法里加锁、在另一个方法里解锁,虽然日常不推荐这么写,但这是它的特性。更实用的是Condition支持多个条件队列,经典的生产者消费者模型里,一个锁可以分别维护“缓冲区满”和“缓冲区空”两个等待队列,比wait/notify的单一等待队列清晰得多。
线程池也是一道必修题。笔试和面试都爱问ThreadPoolExecutor的核心参数:核心线程数、最大线程数、keepAliveTime、工作队列、线程工厂、拒绝策略。一定要能说清楚任务提交后线程池的执行顺序:先判断核心线程是否已满,没满就创建核心线程执行;满了就进工作队列;队列满了再判断是否达到最大线程数;还不够就触发拒绝策略。很多人会把顺序记反,其实口诀是“核心线程 -> 工作队列 -> 最大线程 -> 拒绝”。另外强调一点,不要用Executors提供的快捷方法,像newFixedThreadPool和newCachedThreadPool的队列要么无界、要么最大线程数是Integer.MAX_VALUE,生产环境容易把资源耗尽,还是手动new ThreadPoolExecutor更稳妥。
2.3 编程题方向:链表、多线程与边界条件
编程题这块,我印象里B卷有一道链表相关的题,还有一道并发题。链表的题是“判断一个单向链表是否有环”。这个题最经典的解法是快慢指针,一个每次走一步,一个每次走两步,如果链表有环,两个指针最终会相遇;如果无环,快指针会先走到null。
public class ListNode { int val; ListNode next; ListNode(int x) { val = x; } } public boolean hasCycle(ListNode head) { if (head == null || head.next == null) { return false; } ListNode slow = head; ListNode fast = head; while (fast != null && fast.next != null) { slow = slow.next; fast = fast.next.next; if (slow == fast) { return true; } } return false; }写这道题时最容易被忽略的是边界条件:空链表、只有一个节点、只有两个节点且成环,这三类情况要分别跑到。笔试里这种边界条件很值钱,跑通核心逻辑但边界漏了至少扣一半分。
并发题方向,我印象中考了生产者消费者模型。这类题的要点是:在wait()之后一定要用while循环重新检查条件,而不是用if。原因是线程可能被虚假唤醒(spurious wakeup),也就是没有其他线程调用notify/notifyAll,线程自己就从wait状态恢复了。如果用的是if,唤醒后不会重新判断条件,直接往下执行就可能出问题;用while就能在唤醒后重新检查等待条件,避免消费到不存在的“新数据”。我当时就是在这里丢的分,后来记了一辈子:凡是用wait,必须配while。
还有一个实用的编程题准备方向,是“两个线程交替打印数字”,这道题很能考察对synchronized和Lock的掌握程度,也容易扩展到“三个线程轮流打印ABC”这类变体。笔试编程题不会出特别偏门的算法,链表、字符串处理、二叉树遍历、滑动窗口、两个线程协作,基本就在这个范围内打转。
3. Android机制题的核心考点:启动模式、Handler、Binder
3.1 播放页启动模式:一道送分却容易失分的场景题
B卷场景题里有一道印象很深的:用户正在从首页进入播放页观看视频,然后又跳到了详情页。这时候用户从通知栏点击播放通知,希望直接回到刚才那个播放页,并且不要重新创建Activity,同时把详情页关掉。问选择哪种启动模式。
答案是singleTask。它的特性是在栈内复用,当某个Activity已经是singleTask模式,系统发现栈里已存在该Activity实例时,会把它上面的所有Activity全部出栈,并回调该实例的onNewIntent(),让这个Activity处理新的intent。这就正好满足“播放页复用、详情页被清除”的需求。如果选singleTop,只有当播放页已经位于栈顶时才会复用;现在栈顶是详情页,singleTop会再创建一个新的播放页,结果就是播放页有多个实例。如果选singleInstance,则整个系统内只有一个实例,且它独占一个任务栈,用在这里语义不对,也不适合普通的页面跳转逻辑。
这个题还延伸出一个考点:onNewIntent()里如何拿到新的intent数据。很多人知道有这个方法,但不知道要调用setIntent(newIntent)去替换旧的Intent,否则下一次再触发onNewIntent时,还是拿到旧的intent,在“点击不同通知展示不同视频”的场景下就会出bug。我后来在项目里踩过这个坑,所以看到这道题格外有感触。
启动模式这块,还要注意和Intent.FLAG的关系。比如你想用代码动态控制栈的行为,可以用Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP,效果和singleTask在某些场景下很接近。difference在于:FLAG_CLEAR_TOP会把目标Activity上面的所有Activity都清掉,然后默认是销毁重建目标Activity;如果同时加上FLAG_SINGLE_TOP,才会复用已有实例,回调onNewIntent。笔试如果出这种对比题,要能把“静态配置在Manifest”和“动态设置Flag”两条路径都讲清楚。
3.2 Handler消息循环:主线程为什么不会被死循环拖垮
Handler这道题,可以说是Android机制题的常青树。B卷的简答考法是:Looper.loop()是一个死循环,主线程运行在这个死循环里,为什么不会导致ANR,也不会让CPU占用率飙高?
答案的关键在MessageQueue.next()。主线程的Looper会不断调用next()取消息,当队列里没有消息时,next()不会空转,而是调用nativePollOnce()进入阻塞休眠。这个底层机制依赖Linux的epoll,当没有消息需要处理时,线程会真正挂起,不占CPU;等有消息或者有Binder驱动事件进来,再被唤醒。所以虽然看起来是“死循环”,但它的等待机制是高效的,不会空转消耗CPU。
另外要提到几点:主线程的Looper是在ActivityThread.main()里通过Looper.prepareMainLooper()创建的,然后调用Looper.loop()进入消息循环。每个线程如果想要使用Handler,需要自己调用Looper.prepare()创建Looper,再调用Looper.loop()开启循环,而一个线程只能创建一个Looper,ThreadLocal就是用来保证这一点的。子线程用Handler时,记得用完之后调用Looper.quitSafely()退出循环,否则子线程会一直挂着。
如果想答得更深,可以说一说“同步屏障”。Android为了保证UI的绘制优先执行,Choreographer在VSync信号到来时会往MessageQueue里插入一个同步屏障postSyncBarrier(),屏蔽所有同步消息,只允许异步消息通过,这样界面渲染消息就能插队执行。这个点不是所有面试者都能答上来的,写进答案里是明显的加分项。
3.3 Binder:为什么Android跨进程通信偏偏选它
Binder这道题,大概率以选择题或简答题的形式出现,核心是:Android为什么用Binder作为跨进程通信的主要方式,而不是传统的管道、消息队列、共享内存或Socket。
从技术的角度,Binder基于mmap内存映射,数据只需要从用户空间拷贝到内核空间一次,再由内核空间映射到接收进程的用户空间,相当于同一块物理内存被两个进程映射,整体只发生一次拷贝。而传统的管道、Socket等方式,数据从发送方拷贝到内核,再从内核拷贝到接收方,至少两次拷贝。在高频跨进程调用的场景下,这个性能差距非常明显。Binder的另一个优势是安全性:Binder驱动会为每个调用附带UID/PID,内核层就能校验调用方身份,而传统IPC机制没有这个能力,需要应用层自己校验,容易遗漏。稳定性方面,Binder设计为一次调用一次返回,整体模型更可预测,这也是Android选择它的综合考虑。
如果要把一次完整调用讲清楚,可以按这个顺序说:进程A调用某个接口的代理对象(Proxy),这个代理把参数打包成Parcel,通过Binder驱动传给进程B;Binder驱动为调用方和目标进程建立数据通道,目标进程的Binder线程池取出请求,调用对应的Stub实现类,也就是真正的服务端逻辑;结果再按原路返回。这里注意是异步还是同步,默认是同步,双向等待;Binder也支持oneway异步调用,比如系统的一些通知类操作,不需要等待返回结果。
Binder线程池也是一个常考点:每个进程默认有16个Binder线程,数量耗尽时新请求会等待,所以大量耗时操作不要直接塞进Binder线程里做,会拖垮其他跨进程调用。这套卷子里的Binder题不算特别深,但如果你能在答案里自然带出“Binder驱动、内存映射、线程池限制”这几个关键词,阅卷人的好感度会立刻不一样。
4. 性能优化题是重头戏:Bitmap计算、内存泄漏与ANR排查
4.1 Bitmap内存计算题:一张图差点让App OOM
性能优化几乎是这套B卷的重点,第一道重头题就是Bitmap内存计算。题目大概是:一张1920x1080像素的图片,以ARGB_8888格式加载,放在xhdpi资源目录里,在xxhdpi设备上显示,请问它在内存中占用多大空间?如果改成RGB_565,又占多大?
先理清基础:ARGB_8888表示每个像素用4字节存储,RGB_565是每个像素2字节。所以不经过任何缩放,一张1920x1080的图以ARGB_8888加载,内存占用是1920×1080×4≈8.29MB。但问题还没结束,它还涉及资源目录的密度缩放。Android加载res中的图片时,会根据设备density和目标资源目录的density进行缩放,缩放系数是targetDensity除以资源目录的density。xxhdpi对应480dpi,xhdpi对应320dpi,所以系数是1.5。这个缩放会同时作用于宽和高,图片解码后的实际尺寸变成1920×1.5和1080×1.5,内存占用变成原本的2.25倍,约18.66MB。
这个计算出来的一瞬间,我是有点冒冷汗的——平时开发里随随便便放一张图进drawable-xhdpi,在高端机上就要占接近20MB内存,一旦图片数量多了,OOM根本不用等到用户手机老化。这个计算题考察的不只是公式,更是开发者对图片放置目录和内存敏感度的理解。
那怎么优化?老生常谈的套路就是:先用BitmapFactory.Options的inJustDecodeBounds属性只读图片宽高,然后计算合适的inSampleSize采样率,再用压缩解码。inSampleSize必须是2的幂,这样系统内部可以用位移运算提高效率。如果只看手机屏幕大小,很多时候不需要加载整张原图,比如一张3000x4000的图放到屏幕是1080x1920,采样率按4来算,内存直接降到原来的1/16。如果有更精细的需求,可以用inDensity和inTargetDensity配合,让图片解码到目标尺寸,配合Glide的override等方法。回答这类题,最怕只背优化名词不背计算过程,一定要能算得出数,考官才相信你是真懂。
4.2 内存泄漏场景与LeakCanary检测原理
内存泄漏那道题,考法很典型:请列举Android中常见的内存泄漏场景,并说明为什么会导致Activity无法被回收。我当时的答案是列举一堆场景,比如静态变量引用Activity、非静态内部类隐式持有外部类、Handler未移除消息、BroadcastReceiver未注销、资源流未关闭。这些都是“你肯定听过”的。但B卷在这个基础上追加了一问:LeakCanary检测内存泄漏的原理是什么?
这个问题把很多人卡住了,因为它考的不是背场景,而是背机制。LeakCanary的核心是利用WeakReference和ReferenceQueue。当Activity执行onDestroy()后,LeakCanary会通过ActivityLifecycleCallbacks监听到这个生命周期,然后创建一个指向该Activity的WeakReference,同时关联一个ReferenceQueue。在触发几次GC之后,LeakCanary检查这个WeakReference是否被放进了ReferenceQueue——如果被放进了,说明对象已经被回收,没有泄漏;如果迟迟没有进队,说明该Activity仍然被某个强引用持有着,存在内存泄漏。接下来它会通过Debug.dumpHprofData()导出当前进程的堆快照,再使用HAHA等库解析这个hprof文件,找到从GC Roots到该Activity的完整引用链,最后把这条引用链展示出来。
这道题给我们的启示是:平时用LeakCanary只看到了“泄漏提示”,却不知道它背后是怎么工作的。笔试不会问“你用过什么工具”,而是问“工具的原理是什么”,这就逼着我们把工具的使用经验上升到机制层面。如果提前准备过,这道题就是送分题;没准备过,光靠经验随便写几句,很难拿分。
4.3 ANR超时类型与trace文件定位思路
ANR也是Android性能题里的常客。B卷考了一道资料分析类的简答题:当用户反馈“播放页点击没反应,几秒后弹出ANR对话框”,你需要怎么定位?请尽量描述排查过程。
首先要明确ANR有几种类型和超时时间,这是基本功:
| ANR类型 | 触发条件 | 超时时间 |
|---|---|---|
| InputDispatching Timeout | 输入事件分发超时,比如按键、触屏事件5秒内未处理完成 | 5秒 |
| BroadcastReceiver Timeout | 前台广播执行超时10秒,后台广播执行超时60秒 | 10秒/60秒 |
| Service Timeout | 前台Service生命周期超时20秒,后台Service超时200秒 | 20秒/200秒 |
| ContentProvider Timeout | ContentProvider操作超时 | 10秒 |
定位ANR的完整链路是:先看logcat日志里有没有“ANR in”关键行,或者过滤关键字“am_anr”,这一步能确认ANR类型和发生进程。然后去拉取/data/anr/目录下的traces.txt文件,这个文件里记录了ANR发生时所有线程的堆栈,重点看main线程在哪个方法卡住,是被什么锁阻塞,还是在执行一个耗时的循环。常见的根因大概有这么几类:主线程做了耗时IO操作、主线程在等一个子线程释放锁、主线程在等Binder响应、文件读取或数据库操作过于缓慢、以及频繁GC导致主线程长时间停顿。拿到这个方向后,再去Memory Profiler或CPU Profiler里做二次确认,最后给出优化方案,比如IO操作移到子线程、锁粒度缩小、减少主线程等待等。
我当时答这道题时思路不够清晰,只写了“看traces文件,分析主线程”一句话,太单薄了。正确的答法是把“日志确认类型 -> 拉取traces -> 定位主线程栈 -> 分析根因 -> 优化验证”这条链路完整铺开,让阅卷人看出你是有排查经验的。还要补充一句,adb shell kill -3 <pid>可以手动触发一次ANR trace dump,查看当前线程运行情况,这个冷门但是实用的技能会让你和只会纸上谈兵的候选人拉开差距。
5. 网络与多媒体题:视频业务逼你多想想缓存和协议
5.1 TCP与HTTP:用视频请求场景把协议串起来
网络部分的选择题比较多,但有一道简答题很能体现爱奇艺的业务特色:用户在Wi-Fi下点开一个视频,从点击播放到第一帧画面出现,这个过程中网络层到底发生了什么?请结合TCP和HTTP来回答。
我的建议是,回答这类题不要只背概念,要把它串成一个流程。用户点击播放后,App先向CDN节点发起HTTP请求,这一步通常会先经过DNS解析,拿到IP;然后客户端和服务器通过TCP三次握手建立连接;之后客户端发起HTTP GET请求,请求视频的关键数据(比如MPD文件或m3u8索引);服务器返回数据后,客户端解析出媒体分片地址,再请求具体分片;播放器拿到第一个分片后解码渲染,画面就出来了。这样答,至少比干巴巴背“TCP三次握手是SYN、SYN-ACK、ACK”要有说服力得多。
既然提到HTTP,就肯定会考HTTP/1.1和HTTP/2的区别。HTTP/1.1时期避免不了队头阻塞问题,一个TCP连接上同时只能处理一个请求,虽然Keep-Alive可以复用连接,但Pipeline模式要求响应必须按请求顺序返回,队头阻塞依然严重。HTTP/2通过多路复用、二进制分帧、头部压缩HPACK和服务端推送这几个特性解决了HTTP层的队头阻塞,可以在同一个TCP连接上并行传输多个二进制帧。但这个题的进阶考点是:HTTP/2只是解决了HTTP层的队头阻塞,TCP层的队头阻塞依然存在,因为TCP要求数据按序到达,一个包丢了后续包都要等着,因此后来的HTTP/3选择基于UDP实现QUIC协议,减少连接建立和丢包重传的延迟。把这个逻辑链条理清楚,网络题基本不慌。
OkHttp相关的题也建议准备一下,视频App客户端网络请求几乎都靠它。OkHttp的拦截器链是很好的考点:自定义应用拦截器、重试和重定向拦截器、桥接拦截器、缓存拦截器、连接拦截器、网络拦截器,一层层把请求发出去。连接池它会复用TCP连接,减少频繁三次握手的开销,这在点播、循环播放场景下收益非常明显。如果问到网络优化,可以说OkHttp连接复用、HTTP/2多路复用、Gzip压缩、CDN就近接入、预连接等方案。
5.2 视频缓存与播放器设计:爱奇艺笔试的“亲儿子”考点
多媒体题是这份卷子的最大特色。有一道题我记得特别清楚:请你设计一个视频播放页的缓存策略,要求做到“第二次点开秒开、弱网情况也能流畅播放”。这道题既考技术认知,也考对视频业务的理解,很有区分度。
我的答题思路是这样的:播放缓存不能只做一层,而是要做多层。最上层是内存缓存,可以把最近播放过的视频分片数据存在内存里,但内存缓存容量有限,比如20MB到50MB,适合放当前正在播放和下一集的分片;第二层是本地磁盘缓存,用LRU或者按文件大小限定的策略,把播放过的视频分片保存在App私有目录里,几GB级别的空间可以存很多集,这样二次点开时可以绕过网络直接读磁盘;再往外是CDN分发,App播放时优先请求CDN节点,而不是源站,大幅降低首帧时间和网络拥塞概率。
缓存策略还要结合“预下载”逻辑。在Wi-Fi环境下,播放器可以预下载下一集的前几个分片,或者把当前集后续几分钟的分片提前拉到本地,这样即使网络抖动,播放器还能继续播一段时间。弱网下可以动态降低清晰度,在用户无感知的前提下切到更小的分辨率,减少缓冲等待。秒开这块有个细节:播放器接收到播放指令后,优先去拉取最近的I帧(关键帧),因为只有从I帧开始才能独立解码渲染,不用等之前的依赖帧。很多播放器的“秒开”优化,本质就是在尽量短的时间里拿到第一个包含I帧的分片并完成解码。
音视频基础题也出现过:硬解和软解的区别。硬解依赖专门的硬件解码芯片,比如MediaCodec,解码效率高、耗电低,但兼容性受设备硬件和驱动影响;软解用CPU计算,通用性强、基本不会出现解码器不支持的情况,但费电、发热大,高分辨率视频容易卡顿。商用播放器的做法通常是优先硬解,如果出现解码失败或黑屏,再降级到软解,同时上报相关信息。这个“优先硬解、保底软解”的思路,比单纯说“硬解快”“软解兼容”要有工程感得多。
5.3 SQLite事务与ContentProvider:容易被忽略的数据库题
数据库在Android笔试题里占比不算高,但如果出现在B卷里,考的往往不是简单建表,而是事务和跨进程数据共享。有一道题是:向SQLite里批量插入1000条观看历史记录,怎么保证速度和一致性?这道题的考点就是事务。
SQLite每执行一条写操作,都会触发一次磁盘同步,如果1000条记录逐条insert,耗时和磁盘IO开销都很大。解决办法是用事务包裹整批操作:beginTransaction(),循环里装execSQL或insert,最后setTransactionSuccessful()并endTransaction()。事务提交时底层只会做一次同步写盘,整体速度提升非常明显。这个知识点很基础,但性价比很高,一道题就能区分出有没有真实写过数据库代码的人。要注意的是,setTransactionSuccessful()之后一定要调用endTransaction(),而且不要把这个成功标记放在finally里面,否则事务永远不会提交。
ContentProvider的相关考点是:它解决了什么问题?为什么要用它?ContentProvider的核心意义在于跨进程数据共享,它把数据访问接口封装成一套稳定的URI规范,配合Binder机制,让不同App之间可以安全地读取和写入数据。它也可以在同一个App内部作为数据访问的抽象层,隔离数据源。笔试题如果问到“跨进程共享数据有哪些方式”,一定要先提ContentProvider,再补一句Binder是它的底层通信机制。如果复习得再深一点,可以提Room框架,它基于SQLite封装,在编译期校验SQL语句,配合LiveData和Flow还能监听数据库变化,可以体现你对现代Android架构的掌握。
6. 综合设计题的答题框架与校招备考建议
6.1 综合设计题怎么答:分层定位而不是零散堆点
综合设计题通常放在卷子最后,分值不低。B卷的题目大概是:某视频App用户反馈首页热门信息流滑动越来越卡,帧率下降明显,请分析可能原因并给出排查和优化方案。这种题没有标准答案,但很考验候选人能不能把零散的知识点组织成完整的排查链路。
我整理了一套百搭的答题框架:先给问题定性,再分层排查,最后给出可执行的优化方案。问题定性很重要,卡顿本质是掉帧,也就是UI线程在16.6ms内没完成一帧的绘制,可以从UI线程耗时、渲染过重、内存抖动三个大方向去排查。分层排查可以先看主线程:信息流页面是否存在过度嵌套布局,导致measure/layout耗时过长;列表item里是不是有主线程IO,比如直接从磁盘读图片;有没有锁竞争,主线程在等子线程的锁。再看渲染:是否存在过度绘制Overdraw,可以用GPU调试工具检查;复杂动画、阴影效果、高频刷新会影响RenderThread负载。然后看内存:滑动时如果频繁创建对象,GC就会变多,导致掉帧。
优化方案也要按优先级展开。第一优先是主线程耗时,引入线程池执行IO和耗时任务,布局用ConstraintLayout优化层级,RecyclerView的item通过复用和DiffUtil减少刷新;第二是图片加载,统一用Glide,加载前计算采样率,列表滑动时暂停图片请求,停止滑动再恢复;第三是内存和GC,减少短生命周期对象,字符串用StringBuilder,避免在onBindViewHolder里创建大量临时对象;第四是预加载和懒加载,首屏只加载可见item,滚动到靠近底部时预取下一页数据。最后一定要提验证方式:用Systrace看卡顿函数、用CPU Profiler看线程负载、用Memory Profiler看对象分配,灰度发布后对比卡顿率和帧率数据。这个框架几乎能覆盖所有综合调优题,关键是它能体现你的分层思维。
6.2 校招备考的一点个人体会与错题整理方法
考完这套B卷之后,我做的最有价值的一件事,不是去对答案,而是把错题单独整理成了一张表。表分三列:第一列是知识点,比如“HashMap树化阈值”“Binder原理”“Bitmap密度缩放”;第二列是混淆点,也就是我当时为什么会选错或者漏答,比如把singleTop和singleTask的作用范围搞混;第三列是编码方法,也就是以后遇到同类型题,我该怎么快速判断。这样做的好处是,到后续面试阶段,我一看到类似的题就能条件反射地按“是什么、为什么、怎么做”三层结构来回应,信息密度和表达效率比单纯背答案高很多。
如果你也在准备Android校招,我的建议是:不要只刷选择题和名词解释,试着把每一道题都当成简答题来准备,多追问自己一个“为什么”。为什么HashMap要引入红黑树,为什么Binder要基于mmap,为什么图片采样率必须是2的幂,为什么ANR超时时间要分前台和后台。这些为什么背后,是面试官真正想考察的深度。笔试只是一个入口,把这张卷子的每一道题背后的知识点吃透,等于给自己铺了一条很扎实的复习主线。后面无论是遇到字节的源码追问,