2025Android开发面试题
在面试中,协程(Coroutines)是一个高频考点,尤其在Android开发领域。它是一种轻量级的并发编程方案,能够简化异步操作的代码逻辑,解决传统回调地狱(Callback Hell)问题。以下从核心概念、优势、关键API及实际场景等方面进行梳理,帮助面试应答:
一、协程的核心概念
协程本质是可以暂停和恢复的函数,运行在指定的线程上下文中(如主线程、IO线程),但本身并不直接对应线程,而是由协程调度器(Dispatcher) 管理线程分配。
- 暂停(Suspend):函数执行到某个节点时暂停,释放线程资源(线程可去执行其他任务)。
- 恢复(Resume):当异步操作完成(如网络请求返回、数据库查询结束),函数从暂停处继续执行。
二、协程的优势(对比传统方案)
-
代码简洁,可读性高
异步操作用同步代码的线性逻辑编写,避免多层嵌套回调(如AsyncTask
的onPostExecute
或RxJava的链式调用)。
例:网络请求+数据库存储的异步流程,用协程可写成连续的函数调用,而非嵌套回调。 -
轻量高效
一个进程中可以创建数万甚至数十万协程(内存占用仅几KB),而线程创建数量有限(通常数百个,每个线程占几MB内存)。
适合高并发场景(如批量网络请求、大量数据处理)。 -
结构化并发(Structured Concurrency)
协程具有明确的生命周期作用域(CoroutineScope
),父协程可管理子协程的生命周期(如取消父协程时,自动取消所有子协程),避免内存泄漏。 -
线程切换灵活
通过调度器(Dispatcher)可轻松指定协程运行的线程(如主线程更新UI,IO线程执行耗时操作),无需手动调用runOnUiThread
或Handler
。
三、关键API与核心组件
-
CoroutineScope
(协程作用域)
协程必须在CoroutineScope
中启动,用于管理协程的生命周期(如Activity销毁时取消协程)。
常用作用域:GlobalScope
:全局作用域,生命周期与应用一致(不推荐,易导致内存泄漏)。- 自定义作用域:结合
Job
和Dispatcher
,如val scope = CoroutineScope(Dispatchers.Main + Job())
,可通过scope.cancel()
取消所有子协程。
-
suspend
关键字
标记可暂停的函数,仅能在协程或其他suspend
函数中调用。
例:suspend fun fetchData(): String { ... }
(内部可能包含网络请求等异步操作)。 -
调度器(
Dispatcher
)
决定协程运行的线程:Dispatchers.Main
:Android主线程(用于更新UI,需依赖androidx.lifecycle:lifecycle-viewmodel-ktx
等库)。Dispatchers.IO
:IO线程池(适合网络请求、数据库操作等耗时操作)。Dispatchers.Default
:默认线程池(适合CPU密集型任务,如数据计算)。Dispatchers.Unconfined
:不限制线程(跟随当前线程执行,较少使用)。
-
启动协程的方式
launch
:启动一个不返回结果的协程(用于执行耗时操作后更新UI),返回Job
对象(可用于取消协程)。async/await
:启动一个返回结果的协程,async
返回Deferred
对象,调用await()
获取结果(会暂停当前协程,直到结果返回)。
四、实际场景示例(Android)
场景1:简单的异步任务(IO操作+UI更新)
// 自定义协程作用域(与Activity生命周期绑定)
class MyActivity : AppCompatActivity() {private val scope = CoroutineScope(Dispatchers.Main + Job())override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 启动协程scope.launch {// 1. 切换到IO线程执行网络请求(suspend函数内部通过withContext指定Dispatcher)val data = fetchNetworkData() // 调用suspend函数// 2. 自动切换回主线程更新UI(因为scope的Dispatcher是Main)updateUI(data)}}// 定义suspend函数(内部处理IO操作)private suspend fun fetchNetworkData(): String {// withContext切换到IO线程,执行后自动返回原线程(此处原线程是Main)return withContext(Dispatchers.IO) {// 模拟网络请求URL("https://example.com/data").readText()}}private fun updateUI(data: String) {findViewById<TextView>(R.id.textView).text = data}// 页面销毁时取消协程,避免内存泄漏override fun onDestroy() {super.onDestroy()scope.cancel()}
}
场景2:并行任务(多个异步操作同时执行)
scope.launch {// 启动两个并行的异步任务val deferred1 = async(Dispatchers.IO) { fetchData1() }val deferred2 = async(Dispatchers.IO) { fetchData2() }// 等待两个任务都完成后,合并结果(await()会暂停当前协程)val result1 = deferred1.await()val result2 = deferred2.await()val combinedResult = "$result1 + $result2"// 更新UIupdateUI(combinedResult)
}
五、协程的取消与异常处理
-
取消机制
- 协程通过
Job
对象取消:job.cancel()
,取消后协程会抛出CancellationException
,需在suspend
函数中正确响应(如定期检查isActive
状态)。 - 结构化取消:父协程取消时,所有子协程自动取消(如
scope.cancel()
会取消其启动的所有协程)。
- 协程通过
-
异常处理
launch
中通过try-catch
捕获异常,或使用CoroutineExceptionHandler
统一处理。async
中异常需在await()
时捕获(因为异常会被封装在Deferred
中)。
六、总结(面试核心点)
- 协程是轻量级的并发方案,通过暂停/恢复机制简化异步代码。
- 核心优势:代码简洁、轻量高效、结构化并发、线程切换灵活。
- 关键组件:
CoroutineScope
(作用域)、suspend
(暂停函数)、Dispatcher
(调度器)、launch/async
(启动方式)。 - 实际应用:替代
AsyncTask
、RxJava等,处理网络请求、数据库操作等异步场景,需注意生命周期管理(避免内存泄漏)。
掌握这些要点,能清晰阐述协程的设计思想和实际价值,应对面试中的相关问题。
Socket 是网络编程中的核心概念,面试中常围绕其基本原理、通信流程、阻塞/非阻塞模式、TCP/UDP 区别等展开。以下是高频考点梳理,结合原理和实践问题:
一、Socket 基本概念
- 定义:Socket(套接字)是操作系统提供的网络编程接口,用于实现不同主机(或同一主机不同进程)之间的双向通信。它封装了 TCP/IP 协议的底层细节,提供简单的编程接口(如
read
/write
/connect
等)。 - 核心作用:标识网络中的一个通信端点,由 IP 地址 + 端口号 唯一确定(如
192.168.1.1:8080
)。
二、TCP 与 UDP 的区别(必问)
Socket 基于 TCP 或 UDP 协议工作,二者差异是面试重点:
特性 | TCP(传输控制协议) | UDP(用户数据报协议) |
---|---|---|
连接性 | 面向连接(三次握手建立连接) | 无连接(直接发送,不确认对方是否接收) |
可靠性 | 可靠(重传机制、流量控制、拥塞控制) | 不可靠(丢包不重传,无序) |
数据边界 | 无(流式数据,需应用层处理边界) | 有(每个数据报独立,边界清晰) |
速度 | 较慢(因确认机制) | 较快(无额外开销) |
适用场景 | 文件传输、HTTP/HTTPS、登录等 | 实时通信(视频/语音)、广播、DNS 等 |
三、TCP Socket 通信流程(经典面试题)
TCP 是面向连接的协议,通信需经过“建立连接→数据传输→断开连接”三个阶段,以 C/S 架构为例:
1. 服务器端流程
// 1. 创建 ServerSocket,绑定端口(监听端口)
ServerSocket serverSocket = new ServerSocket(8080);// 2. 阻塞等待客户端连接(accept() 方法)
Socket clientSocket = serverSocket.accept(); // 返回与客户端通信的 Socket// 3. 获取输入流(读客户端数据)和输出流(向客户端写数据)
InputStream in = clientSocket.getInputStream();
OutputStream out = clientSocket.getOutputStream();// 4. 读写数据(业务逻辑)
// ...// 5. 关闭资源(先关客户端 Socket,再关 ServerSocket)
clientSocket.close();
serverSocket.close