去年帮我一个学弟准备字节面试,应届生,投的初级Android岗。面完出来说了句话:"哥,HashMap问了我快20分钟,差点没出来。"
字节面试跟别家不太一样,特别喜欢从一个点往深了挖。你觉得准备了HashMap,他问红黑树;你觉得准备了Handler,他问epoll。初级岗也不例外,表面看题目基础,每道题都有追问。
下面8道题是我从牛客、脉脉上整理的字节初级Android高频题,答案经过验证,放心食用。
Q1:HashMap底层原理,JDK8之后做了什么优化?
面试官想考的不是"数组+链表"四个字,而是你知不知道什么时候链表变红黑树。
HashMap底层是数组,每个槽位挂链表。hash冲突时新节点追加到链表尾部。JDK8之前用头插法,多线程扩容会出环形链表的死循环bug,JDK8改成了尾插法。
关键优化:当链表长度超过8且数组长度≥64时,链表转红黑树,查询从O(n)降到O(log n)。为什么是8?作者做了泊松分布计算,理想hash下链表长度到8的概率极低。容量始终是2的幂,默认负载因子0.75,16个槽位放12个元素就扩容。
避坑:别说"HashMap线程安全",那是ConcurrentHashMap。
Q2:Handler消息机制的完整工作流程?
字节面试Handler几乎必问,经常追问到nativePollOnce。
四个角色:Message(消息体,有对象池复用机制避免频繁创建)、MessageQueue(按时间排序的单链表)、Looper(循环器,一个线程只能有一个)、Handler(处理器)。Handler发消息时调enqueueMessage按when字段插入队列,Looper.loop()死循环取消息,取到了调Handler的handleMessage处理。
// Looper.loop()核心逻辑
while (true) {
val msg = queue.next() // nativePollOne阻塞等待
if (msg == null) return
msg.target.dispatchMessage(msg) // target就是Handler
}
追问:queue.next()怎么阻塞的?底层调了nativePollOnce,用Linux的epoll等待。有新消息时nativeWake唤醒。所以主线程死循环并不会卡,没消息时线程在休眠。
Q3:Activity生命周期,从A跳到B的回调顺序?
90%的人能背出来,但字节面试官会追加场景。
标准顺序:A.onPause → B.onCreate → B.onStart → B.onResume → A.onStop。A先pause,B走完整生命周期,A才stop。
追问:B是透明主题,A会走到onStop吗?不会。A只走到onPause,因为A仍然可见。很多人不知道这个。
追问2:按Home键回桌面?onPause → onStop。回来后onRestart → onStart → onResume,没有onCreate。
Q4:View的绘制流程,measure、layout、draw分别做什么?
measure确定大小,layout确定位置,draw画内容。
measure阶段处理MeasureSpec,由父容器约束和自身LayoutParams共同决定。三种模式:EXACTLY(精确值/match_parent)、AT_MOST(wrap_content)、UNSPECIFIED(不受限)。自定义View最常见的坑就是wrap_content不生效,看代码:
override fun onMeasure(wSpec: Int, hSpec: Int) {
val w = if (MeasureSpec.getMode(wSpec) == MeasureSpec.AT_MOST) 200
else MeasureSpec.getSize(wSpec)
setMeasuredDimension(w, MeasureSpec.getSize(hSpec))
}
layout阶段父容器给子View设定left/top/right/bottom坐标。draw阶段Canvas绑Paint往Surface上画像素。
Q5:常见内存泄漏场景有哪些?怎么排查?
说白了就是该回收的对象被持有,GC回收不了。最常见的四种:
Handler泄漏:非静态内部类的Handler隐式持有Activity引用,Message又持有Handler,形成引用链。解法用静态内部类+WeakReference。
单例持有Context:单例跟进程一样长,传Activity进去就回收不了。永远传applicationContext。
未注销的监听器:广播接收器、EventBus、回调注册了没反注册,对象一直被持有。
WebView:销毁后仍持有大量native内存,必须removeAllViews再destroy。
排查首推LeakCanary,原理是监控onDestroy,5秒后检查是否被GC,没有就dump内存分析引用链。
Q6:线程池的核心参数有哪些?
ThreadPoolExecutor面试常问前4个:corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(非核心线程存活时间)、workQueue(任务队列)。
任务来了先看核心线程满没满,没满就创建。核心满了丢队列,队列满了创建非核心线程。都满了执行拒绝策略。
// IO密集型线程池示例
val pool = ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors() * 2, // core
Runtime.getRuntime().availableProcessors() * 4, // max
60L, TimeUnit.SECONDS,
LinkedBlockingQueue(128) // 有界队列防OOM
)
经验:CPU密集型核心线程设CPU核数+1,IO密集型设核数*2。队列一定用有界的,无界队列容易OOM。拒绝策略推荐CallerRunsPolicy,让提交线程自己执行,起背压效果。
Q7:RecyclerView的四级缓存?
四层各有分工:Scrap是屏幕内正在显示的ViewHolder,layout期间分离再附加,不重新bind。CachedViews(默认容量2)是刚滑出屏幕的,保留position可直接复用。ViewCacheExtension是自定义缓存,很少用。RecycledViewPool按viewType分组(每组默认5个),需要重新bind数据。
前两层是热缓存直接复用,第四层是冷缓存要重新绑定。这设计保证了滑动时不频繁创建和绑定ViewHolder。
Q8:SharedPreferences有什么缺陷?
SP主要有三个坑。apply()虽然异步写内存,但在Activity的onPause里会通过QueuedWork等文件写入完成,可能ANR(Android 11后修复了)。全量加载问题——第一次读取把整个XML读到内存,文件大就很慢。还有不支持多进程,跨进程读写数据不一致。
替代方案用DataStore,分Preferences和Proto两种。Proto用Protobuf序列化,支持类型安全和schema迁移,是官方推荐方向。从SP迁移过来也简单,Google提供了SharedPreferencesMigration工具类,一行代码搞定旧数据迁移。
面试Tips:字节初级Android面试有几个特点——算法必考(LeetCode中等起步)、Java基础问得深(集合和并发是重点)、Android基础不考太偏但追原理。建议把HashMap、Handler、Activity生命周期吃透,这三个几乎每轮都会问。
下一篇是字节跳动中级Android工程师面经,重点聊RecyclerView源码级分析、Fragment懒加载的正确姿势、自定义View事件分发的坑。中级岗面试官明显更关注源码阅读能力。
面试过字节的同学来评论区聊聊,你觉得哪道题被追问得最狠?
本系列连载中,关注不迷路,下一篇:字节跳动中级Android工程师面试真题
系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。