前言

相较于搜狗输入法那场面试的面试官,这次的面试官明显就对我感兴趣很多了,和面试官互动得很深入。

依旧是深度定制DIY面试,无八股盛宴

正文

手撕环节

无自我介绍,上来直接开始手撕代码

题目是二维矩阵最优寻找target数(240. 搜索二维矩阵 II

虽然leetcode做过,但是测试程序依旧是不支持Kotlin,面试官说可以用Java,然鹅我Java都忘得差不多了😂

没办法,最后面试官让我用JavaScript写,不得不说,JavaScript和Kotlin在基础语法上真的就大差不差,直接救我老命😂

虽然基础语法上大差不差,但是细节上坑死我了,把console.print写成print,把数组长度的length写成size和size(),在那里测试半天,最后还是面试官提醒我该怎么写🥹

最后代码出来了,但是边界条件没做好,又卡半天,面试官又善良的提醒了我一下while的边界条件,才把题目A掉。(面试官太善良了🥹)

本来面试官打算出第二道题的,看时间有点不够了,便结束了手撕环节

项目盘点环节

先是让我介绍一下简历里的两个项目,挑重点讲。于是我便快速的过了一遍项目重点和难点。

然后问了下简历中写的项目优化是怎么优化的

接着问了下项目用户受众情况

询问了下项目缓存的实现逻辑以及缓存过期逻辑

询问了下如何处理宽屏分辨率问题(答得不是很好,因为这块代码是AI写的(doge))

Kotlin相关知识

Kotlin中的suspend是怎么挂起的,它是线程安全的吗?

这个前半段有点问道我盲点了,后面问了下Gemini,Gemini是这样说的:

Gemini回答

简单来说:suspend 的挂起是通过“状态机(State Machine)”和“续体传递风格(CPS 变换)”实现的;而 suspend 关键字本身绝对不是线程安全的。

一、 suspend 是怎么挂起的?(底层原理)

suspend 实际上是一个编译器魔法。当你在 Kotlin 中写下一个 suspend 函数时,编译器会在后台对代码进行大幅度的重写。它并不会真正去“阻塞”或“休眠”底层的操作系统线程,而是将函数切分成了多个片段。

具体来说,编译器做了以下三件事:

  1. CPS 变换 (Continuation-Passing Style)

编译器会在你的 suspend 函数签名中,默默追加一个隐藏的参数:Continuation(续体)。
Continuation 可以理解为“后续的代码”或“一个书签”,它保存了函数恢复执行时需要的所有上下文。

  • 你的代码:
1
2
suspend fun getUserInfo(id: String): User

  • 编译器眼里的代码(伪代码):
1
2
Object getUserInfo(String id, Continuation<User> cont)

(注:返回值变成了 Object,因为它可能返回真实结果,也可能返回一个特殊的挂起标志。)

  1. 生成状态机 (State Machine)

编译器会把你函数内部的代码,以其他的 suspend 函数调用为“分界点”,切分成多个状态(label)。

假设你有这样一段代码:

1
2
3
4
5
6
7
suspend fun fetchData() {
println("Start")
val token = requestToken() // 挂起点 1
val data = requestData(token) // 挂起点 2
println("Result: $data")
}

编译器会把它变成类似下面这样的状态机(伪代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Object fetchData(Continuation cont) {
// 提取状态机和局部变量
StateMachine sm = cont;

switch (sm.label) {
case 0:
println("Start");
sm.label = 1; // 准备进入下一个状态
// 调用挂起函数,如果它真挂起了,立刻 return 释放当前线程
if (requestToken(sm) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;

case 1:
// 恢复执行时,提取上一步的结果
String token = (String) sm.result;
sm.label = 2;
if (requestData(token, sm) == COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED;

case 2:
String data = (String) sm.result;
println("Result: " + data);
return Unit;
}
}

  1. 挂起与恢复 (Suspend & Resume)
  • 挂起(Suspend): 当代码执行到 requestToken() 并真正需要等待(比如网络 IO)时,它会向上一层返回一个特殊的标志 COROUTINE_SUSPENDED。这个标志会一路 return 到最外层,从而把当前的 CPU 线程释放出来,让线程可以去干别的事情(比如更新 UI 或处理其他协程)。
  • 恢复(Resume): 当网络请求完成时,底层系统(比如回调机制)会调用 Continuation.resumeWith(result)。这会重新触发 fetchData 函数,由于 sm.label 已经变成了 1,代码会直接 switchcase 1 继续执行,并带入了网络请求的结果。

生活中的比喻:
就像你看书时遇到了一个不懂的词(挂起点),你夹入一个书签(保存 Continuation 状态),然后合上书去查字典。在这个期间,你的大脑(线程)可以去喝水、听歌(执行其他任务)。查到意思后,你翻开书签那一页(Resume 恢复),继续往下读。

后半段我有点没懂面试官意思,以为面试官是说协程本身是不是线程安全的。

这就让我有点懵,这就好比你问线程本身是不是线程安全的这种问题

后面我知道了面试官是问用协程去操作一个变量是不是线程安全的,那当然是非线程安全的啦,这和线程并发出现竞争是一个道理。不过感觉答得不是很好

然后就是问Kotlin中和UI实现状态的同步是怎么做的。

我的回答就是用Kotlin的StateFlow,Compose会collect ViewModel中的StateFlow,根据StateFlow传来的变化而变化之类的,感觉答的不是很好。

接着就是问是否有用过传统Android XML UI。答:有用过,不过后来转Kotlin+Compose了。

AI相关知识

根据我的项目提问,询问在使用AI编程的时候如何节省Token。

我回答是让AI编程工具生成AGENT.md,GEMINI.md等文件,提前读取项目整个架构,避免AI无意义的搜索整个项目,然后每次给出清晰的指令,避免给出模糊的指令,以及模块化编程,避免AI大范围搜索和改动。

然后面试官询问Skill和MCP哪个用的token较少。

这当然是Skill啦,Skill当初提出就是为了解决MCP占用上下文过多的问题

最后问我用过哪些Agent,Agent是什么。

我目前接触的Agent就Copilot、Codex里面这些自带的Plan、Build这些Agent,没自己做过Agent,所以后半段答得不是很好。

反问环节

依旧是询问团队是用Java用的比较多还是Kotlin比较多

回答不出意外,已有代码用Java,新代码用Kotlin(咳特灵还是太好用了,有效治疗Java带来的偏头痛,以至于我完全不想用Java(逃))

面试小插曲

因为实验室刚搬迁,网络设施还没弄好,于是只能用手机热点给电脑共享网络。结果面试到一半时突然网络卡顿,立马查了用腾讯会议自带的网络检查功能检查,发现网络有问题,看了下手机,结果是逆天中信(为什么逆天之后说)打来的电话,把我网络顶掉了(有电话时移动数据会被顶掉),想都没想直接挂掉😡