小米初级Android面经:反转链表(迭代+递归)、Activity生命周期、ConcurrentHashMap在JDK

23 阅读5分钟

小米初级Android面试:小米Android开发覆盖MIUI系统定制、IoT设备互联。技术栈偏底层,面试官喜欢问系统级原理和性能数据。技术方向偏向MIUI定制、IoT互联。特色专项包括MIUI系统定制与IoT设备开发。今天8道题覆盖小米初级Android面试核心考点。

Q1:反转链表(迭代+递归)?

迭代:pre=null, cur=head,遍历时记下next=cur.next,然后cur.next指pre,pre和cur后移。时间复杂度O(n),空间O(1)。递归:递到最后一个节点作为新head,归的时候head.next.next=head, head.next=null。

fun reverse(head: ListNode?): ListNode? { var pre: ListNode? = null; var cur = head
    while (cur != null) { val next = cur.next; cur.next = pre; pre = cur; cur = next }
    return pre }

深度解析:从复杂度看,迭代三指针(pre/cur/next)原地反转,时间 O(n) 空间 O(1)。。常见变体与延伸包括:K 个一组反转(穿针引线)、反转部分区间。。实战要特别注意边界情况:空/单节点、k 大于长度、k=1 不变。。面试官大概率会接着追问:反转后原头节点的 next 指向哪里?会不会成环?。

追问:递归版本的空间复杂度?O(n)——递归调用栈深度等于链表长度。

Q2:Activity生命周期,A启动B的完整回调顺序?

标准全屏场景:A.onPause→B.onCreate→B.onStart→B.onResume→A.onStop→(可能A.onSaveInstanceState)。关键点:B透明主题或Dialog样式时A只走到onPause不会onStop——因为A仍然部分可见。onSaveInstanceState在onStop之前调用,仅在Activity可能被系统意外回收时触发。

深度解析:Activity 有典型生命周期:onCreate→onStart→onResume(可见可交互)→onPause→onStop→onDestroy,外加 onRestart。启动模式 standard/singleTop/singleTask/singleInstance 决定栈行为。容易踩的坑是:onPause 里别做耗时,否则影响下一个 Activity 的启动;onSaveInstanceState 只存轻量状态。;面试官常追问:追问:横竖屏切换生命周期怎么走?默认会重建,可用 configChanges 拦截。。

追问:onSaveInstanceState和onRestoreInstanceState的配对?onSaveInstanceState存,onCreate(Bundle)或onRestoreInstanceState(Bundle)恢复。

Q3:ConcurrentHashMap在JDK8中的实现?

JDK8放弃分段锁,改用CAS+synchronized。每个桶位独立加锁,锁粒度从Segment降到Node级别。put时空桶CAS写入,冲突则synchronized锁链表头节点。size计数用LongAdder分槽累加——比AtomicLong竞争小得多。

if ((f = tabAt(tab, i)) == null)
    casTabAt(tab, i, null, new Node<>(hash, key, val));
else synchronized (f) { /* 写入该桶 */ }

深度解析:HashMap 在 Java 8 是数组+链表/红黑树,默认容量 16、负载因子 0.75,扩容翻倍并 rehash。冲突用拉链法,链表长度超 8 转红黑树。Android 的 ArrayMap/SparseArray 针对小数据更省内存。容易踩的坑是:HashMap 非线程安全,并发用 ConcurrentHashMap(1.8 用 CAS+synchronized 桶头)。;面试官常追问:追问:为什么容量是 2 的幂?位运算取模更快。。

追问:size()为什么不精确?分槽计数有延迟,迭代器是弱一致性的——允许遍历过程中其他线程修改。

Q4:协程的底层原理?

Kotlin协程本质是编译器生成的状态机。每个suspend函数编译后会多一个Continuation参数,每个挂起点对应状态机的一个case分支。调度器CoroutineDispatcher决定协程在哪个线程执行。协程是M:N调度——一个线程可以跑成千上万个协程。

suspend fun fetchData(): String {
    val data = api.getData() // 挂起点1
    return process(data) // 挂起点2
}

追问:协程比线程轻在哪?线程默认1MB栈空间,协程的Continuation对象只有几百字节。

Q5:Flow和RxJava的对比?

Flow是冷流——有collector才开始发射数据。SharedFlow/StateFlow是热流。Flow的背压是天然的(suspend函数自动阻塞生产者)。RxJava操作符更多更丰富,但上手成本高。StateFlow带初始值且配置变化不重置,SharedFlow可通过replay控制缓存。

val shared = flow { emit(1); emit(2) }
    .shareIn(scope, SharingStarted.WhileSubscribed(), replay = 1)

追问:StateFlow和LiveData的核心区别?StateFlow必须提供初始值,不会因为生命周期而停止——需要用repeatOnLifecycle控制。

Q6:HashMap底层原理,JDK8做了什么优化?

底层是数组+链表/红黑树。hash冲突时尾插法——JDK8修复了头插法多线程扩容时环形链表的经典bug。链表长度>8且数组>=64时转红黑树,查询从O(n)降到O(logn)。容量始终2的幂,负载因子0.75。

int threshold = capacity * loadFactor;

追问:为什么选8做树化阈值?根据泊松分布计算,理想hash下链表长度到8的概率极低——约千万分之六。

Q7:Fragment生命周期和ViewPager2懒加载?

完整链:onAttach→onCreate→onCreateView→onViewCreated→onStart→onResume→onPause→onStop→onDestroyView→onDestroy→onDetach。ViewPager2的懒加载靠setMaxLifecycle控制——预加载的Fragment最大生命周期到STARTED或RESUMED,真正可见时才到RESUMED触发数据加载。

fragment.setMaxLifecycle(FragmentA.LifecycleState.STARTED)

追问:ViewPager2相比ViewPager的最大变化?基于RecyclerView实现,天然支持垂直滑动和RTL布局。

Q8:LRU缓存实现?

LinkedHashMap最简单——构造函数第三个参数accessOrder=true,重写removeEldestEntry。手动实现:HashMap+双向链表,get时把节点移到链表头(最近使用),put满时删链表尾节点(最久未使用)。

class LRUCache(capacity: Int) : LinkedHashMap<Int, Int>(capacity, 0.75f, true) {
    override fun removeEldestEntry(eldest: Map.Entry<Int, Int>?) = size > capacity
}

追问:LinkedHashMap的accessOrder原理?true时每次get/put把访问节点移到链表尾,head就是LRU。

下一篇进入小米中级Android——ViewModel的生命周期、Android内存优化、Compose的状态管理。

觉得有用的同学扣1,你面试被追问最狠的是哪道题?

本系列连载中,关注不迷路,下一篇:小米中级Android面经:ViewModel的生命周期、Android内存优化、Compose的状态管理

系列简介:Android大厂面经连载,覆盖字节跳动、腾讯、阿里、美团等40+企业,从初级到架构师全岗位覆盖。每篇文章包含真实面试题+详细答案+代码示例,帮你拿到大厂Offer。