Android面试题(一)
记录 Android 面试题, 有时间过来翻翻。
博主博客
目录
- 一、什么是 ANR 如何避免它?
- 二、Activity和Fragment生命周期有哪些?
- 三、Fragment生命周期与Activity有何不同?
- 四、横竖屏切换时Activity的生命周期如何变化?
- 五、Android进程优先级从高到低有哪些?
- 六、Bundle为何需要序列化?Serializable和Parcelable的区别?
- 七、Android动画有哪些类型?有何区别?
- 八、Activity、Service、Application 的 Context 有什么区别?
- 九、从 Android 5.0 到 Android 14,有哪些主要新特性?
- 十、JSON 是什么?在 Android 中如何解析?
一、什么是 ANR 如何避免它?
(一)什么是ANR?
ANR(Application Not Responding,应用程序无响应)是Android系统的一种保护机制。当应用程序的主线程(UI线程)在特定时间内未能响应用户操作或完成特定任务时,系统会向用户显示ANR对话框,提示用户应用无响应,用户可以选择等待或强制关闭应用。
触发ANR的三种主要场景及超时时间:
- 输入事件超时(最常见):Activity在5秒内未处理完用户的输入事件(如按键、触摸)。
- 广播超时:BroadcastReceiver的
onReceive()方法在前台广播10秒内未执行完成。 - 服务超时:Service在前台服务20秒内未完成启动(
onCreate()和onStartCommand())。
(二)ANR产生的主要原因
主线程被阻塞的常见情况:
- 主线程执行耗时I/O操作
- 网络请求(注意:Android 4.0+已禁止在主线程进行网络I/O)
- 文件读写操作(数据库、SharedPreferences等)
- 图片/视频等媒体文件处理
- 主线程进行复杂计算
- 大量数据处理或复杂算法运算
- 布局测量/绘制过于复杂
- 主线程被同步机制阻塞
- 错误的线程同步(在主线程调用
wait()、sleep()) - 主线程等待子线程锁释放(死锁)
- Binder调用阻塞(跨进程通信等待过久)
- 错误的线程同步(在主线程调用
- 错误的生命周期方法实现
- 在
Activity.onCreate()、onResume()中执行耗时操作 BroadcastReceiver.onReceive()中执行耗时任务
- 在
(三)如何调试ANR?
1. 获取ANR日志
-
开发期间:通过
adb命令获取# 获取ANR traces文件 adb pull /data/anr/traces.txt . # Android 11及以上可能使用新格式 adb shell ls /data/anr/ # 查看ANR文件列表 -
Logcat查看:在Android Studio中过滤关键字
ANR in -
线上监控:通过Firebase Performance Monitoring、腾讯Bugly等APM工具收集线上ANR数据
2. 分析ANR日志
- 重点查看主线程(main thread):检查主线程的堆栈信息,找到阻塞点
- 检查锁状态:查看是否有死锁或锁竞争
- 关注Binder调用:检查主线程是否在等待Binder调用返回
- 分析CPU使用情况:检查当时CPU是否被其他进程或线程占用
3. 使用调试工具
-
StrictMode:在开发阶段检测主线程中的意外I/O操作
// 在Application或Activity中启用 StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() .penaltyLog() .build()) -
Systrace:分析应用性能,找出卡顿原因
-
CPU Profiler:分析主线程执行时间
(四)如何避免ANR?
1. 基本原则:保持主线程轻量
- 主线程只负责UI更新和轻量级操作
- 所有耗时操作都应在后台线程执行
2. 异步处理最佳实践(已更新)
过时方案(已废弃):
- ❌
AsyncTask:已在Android 11中废弃,不应在新项目中使用
推荐方案(现代Android开发):
方案一:Kotlin协程(官方首选)
// ViewModel中执行
viewModelScope.launch {
// IO操作使用IO调度器
val data = withContext(Dispatchers.IO) {
repository.fetchData()
}
// 主线程更新UI
_uiState.value = data
}
方案二:RxJava/Coroutines + 架构组件
// 结合Room、Retrofit等(自动提供协程支持)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
suspend fun getUsers(): List<User> // Room支持挂起函数
}
// ViewModel中使用
val users: LiveData<List<User>> = userDao.getUsers().asLiveData()
方案三:ExecutorService + Handler/ViewModel
// 使用线程池处理后台任务
private val executor = Executors.newFixedThreadPool(4)
fun loadData() {
executor.execute {
val data = performLongRunningTask()
// 通过主线程Handler或runOnUiThread更新UI
mainHandler.post { updateUI(data) }
}
}
3. 特定组件的优化策略
Activity生命周期优化:
- 避免在
onCreate()、onResume()中执行耗时初始化 - 使用懒加载或异步初始化
- 复杂的布局考虑使用
ViewStub延迟加载
BroadcastReceiver优化:
onReceive()中应快速返回,10秒内必须完成- 需要长时间处理时,应启动Service或WorkManager
- 使用
goAsync()延长处理时间(但仍有时间限制)
Service优化:
- 避免在Service生命周期方法中执行耗时操作
- IntentService已废弃,推荐使用
WorkManager或带协程的Service - 长时间运行的任务考虑使用
Foreground Service并显示通知
4. 线程优先级管理
// 后台线程设置较低优先级,避免影响主线程
Thread {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
// 执行后台任务
}.start()
5. 避免同步问题
- 避免在主线程使用
synchronized等待锁 - 使用并发安全的数据结构(如
ConcurrentHashMap) - 考虑使用
Atomic变量或Mutex(协程中的锁)
6. 性能优化
- 优化布局层次,减少测量/绘制时间
- 使用
RecyclerView代替ListView - 图片加载使用Glide/Picasso等库(自动后台加载)
- 数据库操作使用Room的协程支持
7. 现代Android架构推荐
// 使用MVVM + Repository + Coroutines
class MyViewModel(private val repository: MyRepository) : ViewModel() {
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repository.fetchData() // 在IO线程执行
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
}
(五)线上监控与预防
1. ANR监控方案
- 集成APM工具:监控线上ANR率、卡顿率
- 自定义监控:使用
ANRWatchDog等库检测接近ANR的情况 - 异常上报:收集ANR时的堆栈信息、设备信息、用户操作路径
2. 定期性能检查
- 使用Android Studio的Profiler定期分析应用性能
- 进行Monkey测试,检查随机操作下是否会出现ANR
- 在不同设备上测试性能表现
二、Activity和Fragment生命周期有哪些?
(一)Activity生命周期核心方法
Activity生命周期包含7个核心方法,描述了从创建到销毁的完整过程。
1. onCreate()
- 调用时机:Activity首次创建时调用(冷启动或配置更改后重建)
- 主要职责:
- 执行一次性初始化操作
- 调用
setContentView()设置布局 - 初始化静态数据
- 绑定数据到列表等组件
- 参数:接收
savedInstanceState参数,用于恢复之前保存的状态
2. onStart()
- 调用时机:Activity即将变为可见(进入前台)
- 主要职责:
- 开始执行与界面显示相关的初始化
- 注册广播接收器、监听器等
- 准备资源使其对用户可见
3. onResume()
- 调用时机:Activity已进入前台并开始与用户交互
- 主要职责:
- 启动需要持续运行的功能(如动画、传感器监听)
- 恢复在
onPause()中暂停的操作
- 状态:此时Activity处于活动状态,位于返回栈顶部
4. onPause()
- 调用时机:Activity即将失去焦点(部分可见)
- 主要职责:
- 暂停或释放独占资源(如相机、传感器)
- 提交未保存的更改(轻量级数据持久化)
- 停止动画或其他消耗CPU的操作
- 重要限制:必须快速执行(否则会阻塞下一个Activity的启动)
5. onStop()
- 调用时机:Activity完全不可见
- 主要职责:
- 释放/取消所有不需要在后台运行的资源
- 注销在
onStart()中注册的监听器 - 保存数据到持久存储(如果需要)
6. onRestart()
- 调用时机:Activity从
onStop()状态重新回到前台 - 主要职责:
- 重新初始化在
onStop()中释放的资源 - 通常不需要做太多工作,除非有特殊需求
- 重新初始化在
7. onDestroy()
- 调用时机:Activity即将被销毁
- 主要职责:
- 执行最终的资源清理
- 终止后台线程
- 注销全局监听器
- 注意:不能保证一定被调用(如进程被强行终止时)
(二)状态保存与恢复方法
1. onSaveInstanceState(Bundle outState)
- 调用时机:在Activity可能被销毁并重建时调用(如配置更改、内存不足)
- 调用顺序:在
onStop()之前调用,但不保证在onPause()之前或之后 - 主要职责:
- 保存临时UI状态(文本框内容、滚动位置等)
- Bundle数据是序列化的,应仅保存轻量级数据
- 注意:不应保存持久化数据(使用SharedPreferences或数据库)
2. onRestoreInstanceState(Bundle savedInstanceState)
- 调用时机:在
onStart()之后、onResume()之前调用 - 触发条件:仅当有保存的状态需要恢复时调用
- 优势:比在
onCreate()中恢复状态更清晰,因为savedInstanceState一定不为null
(三)Activity生命周期流程图
graph TD
A[启动Activity] --> B[onCreate]
B --> C[onStart]
C --> D[onResume]
D --> E[运行状态 <br/> 用户可交互]
E -->|失去焦点 <br/> 如对话框弹出| F[onPause]
F -->|重新获得焦点| D
F -->|完全不可见| G[onStop]
G -->|用户返回| H[onRestart]
H --> C
G -->|被销毁| I[onDestroy]
I --> J[Activity销毁]
subgraph "状态保存与恢复"
K[配置更改/内存不足] --> L[onSaveInstanceState]
M[Activity重建] --> N[onCreate或onRestoreInstanceState]
end
(四)现代Android开发的补充与更新
1. 新增的重要生命周期回调
(1)onBackPressed()(已弃用)
- 弃用时间:从Android 13 (API 33)开始标记为弃用
- 替代方案:使用
OnBackPressedDispatcher - 现代实现:
// 在onCreate中设置 onBackPressedDispatcher.addCallback(this) { // 处理返回逻辑 if (shouldInterceptBackPress) { // 自定义处理 } else { isEnabled = false onBackPressed() } }
(2)Activity Result API(替代startActivityForResult)
- 弃用:
startActivityForResult()和onActivityResult()已弃用 - 新API:使用
ActivityResultContracts和registerForActivityResult()// 注册结果回调 private val resultLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> if (result.resultCode == RESULT_OK) { // 处理结果 } } // 启动Activity resultLauncher.launch(intent)
2. 生命周期感知组件(Jetpack)
(1)LifecycleOwner 与 LifecycleObserver
- Activity本身是
LifecycleOwner - 可以通过
lifecycle.addObserver()注册观察者class MyObserver : LifecycleObserver { @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) fun connectListener() { // 连接监听器 } @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) fun disconnectListener() { // 断开监听器 } } // 在Activity中使用 lifecycle.addObserver(MyObserver())
(2)ViewModel 生命周期
- ViewModel的生命周期与Activity不同
- 在配置更改(如屏幕旋转)时,ViewModel不会销毁
- 在Activity真正结束时(调用finish()),ViewModel会调用
onCleared()
3. 进程生命周期与Activity生命周期的关系
重要概念:当应用进入后台,进程可能因内存不足被系统终止(进程死亡)
恢复流程:
- 系统重新创建进程
- 重新创建Activity
- 调用
onCreate()并传入保存的Bundle - 如果使用了ViewModel + SavedState,状态会自动恢复
4. 处理配置更改的现代方式
(1)传统方式的问题
- 手动保存大量状态到Bundle
- 重建时恢复状态复杂
- 可能丢失非序列化对象
(2)现代解决方案
方案一:使用ViewModel
class MyViewModel : ViewModel() {
val liveData = MutableLiveData<String>()
// ViewModel在配置更改时保持存活
}
方案二:使用SavedStateHandle(保存简单状态)
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
val selectedItem: MutableLiveData<String> =
savedStateHandle.getLiveData("selectedItem", "")
fun saveSelection(item: String) {
savedStateHandle["selectedItem"] = item
}
}
方案三:禁用配置更改(谨慎使用)
<!-- AndroidManifest.xml -->
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
三、Fragment生命周期与Activity有何不同?
(一)核心问题:Fragment生命周期与Activity的主要区别是什么?
两者的生命周期紧密关联但职责不同,最主要的区别在于 Fragment拥有独立的视图(View)生命周期,这使其比Activity多出一些与视图创建和销毁相关的回调方法。
一个关键的体现是:当Fragment被添加到回退栈(Back Stack)时,其视图会被销毁,但Fragment实例本身会保留。这意味着它会经历 onDestroyView(),但不会走到 onDestroy() 和 onDetach()。
(二)详细阐述:Fragment独有的核心生命周期方法有哪些?
1. onAttach(Context context)
- 作用:当Fragment首次与其关联的Activity(或其上下文)建立连接时调用。这是Fragment可以使用
getActivity()等方法获取宿主引用的起点。 - 现代补充:在Android API 23+,也提供了
onAttach(Context),但传统onAttach(Activity)已弃用。通常在此处初始化与Activity的通信接口。
2. onCreateView()
- 作用:创建并返回与Fragment关联的视图层次结构。这是Fragment与Activity在视图创建上的核心区别。如果Fragment没有UI(如一个作为工作线程的Fragment),可返回
null。 - 最佳实践:在此处调用
LayoutInflater.inflate()来膨胀布局。应避免在此方法中直接进行视图操作(如findViewById),因为视图可能还未完全初始化。
3. onViewCreated(View view, Bundle savedInstanceState)
- 作用:在
onCreateView()返回后立即调用,此时视图已完全初始化。这是进行视图绑定、设置监听器和初始化UI组件的最佳位置。 - 补充说明:参数中传递的
view就是onCreateView()返回的根视图。
4. onDestroyView()
- 作用:当与Fragment关联的视图层次结构被移除时调用。这是Fragment生命周期中最独特且重要的一环。
- 关键原因:当Fragment被替换或移除并添加到回退栈时,视图会被销毁以释放内存,但Fragment实例本身仍然存活。因此,必须在此方法中释放所有对视图的引用(例如,将
RecyclerView的适配器置为null,取消对View的绑定),以防止内存泄漏。实例变量和ViewModel中的数据会保留。
5. onDetach()
- 作用:当Fragment与Activity解除关联时调用。此后,
getActivity()将返回null。应在此清理所有对Activity或Context的引用。
(三)生命周期流程对比图与核心关联
核心流程对比表
| 阶段 | Activity 生命周期 | Fragment 生命周期 (关键差异) |
|---|---|---|
| 创建与启动 | onCreate() → onStart() → onResume() |
onAttach() → onCreate() → onCreateView() → onViewCreated() → onStart() → onResume() |
| 运行中 | Resumed (活跃状态) | Resumed (活跃状态) |
| 进入后台 | onPause() → onStop() |
onPause() → onStop() |
| 视图销毁 (实例保留) |
无对应阶段 | → onDestroyView() ⭐ (Fragment独有) |
| 完全销毁 | onDestroy() |
→ onDestroy() → onDetach() |
重要联动回调(已过时与替代方案)
onActivityCreated(Bundle):此方法现已过时(Deprecated)。它原本在宿主Activity的onCreate()完成、且Fragment的视图已创建时调用。现代开发中,所有应在此时进行的初始化操作(如观察Activity的LiveData)都应迁移到onViewCreated()中进行。
四、横竖屏切换时Activity的生命周期如何变化?
(一)核心问题:横竖屏切换时,Activity的默认生命周期是如何变化的?
默认情况下,横竖屏切换会被系统视为一次运行时配置更改,它会触发当前Activity的销毁与重建,其目的是为了自动加载新的屏幕方向所对应的布局资源(例如从layout-port切换到layout-land)。
完整的生命周期回调顺序如下:
- 当前Activity暂停并停止:
onPause()→onStop()→onDestroy() - 新的Activity实例被创建:
onCreate()→onStart()→onResume() - 状态保留:在销毁前,系统会调用
onSaveInstanceState(Bundle)保存临时数据,并在新的onCreate()或onRestoreInstanceState()中恢复。
(二)高级追问:我们能否避免重建?如何实现?
可以。通过在AndroidManifest.xml中为该Activity声明android:configChanges属性,并自行处理配置变更,即可避免重建。
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize|keyboardHidden|smallestScreenSize" />
关键点解释:
orientation:处理屏幕方向改变(竖屏、横屏)。screenSize:(API 13及以上必须添加) 当屏幕尺寸改变时(例如横竖屏切换导致的可用尺寸变化)。keyboardHidden:处理键盘可用性改变。smallestScreenSize:屏幕最小尺寸改变时。
配置后的行为变化:
- Activity不再销毁重建。
- 系统将调用该Activity的
onConfigurationChanged(Configuration newConfig)方法。 - 开发者责任:在此回调中手动更新UI,例如重新加载布局、调整视图尺寸等。
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
// 手动处理布局变更
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 加载或调整为横屏布局
} else {
// 加载或调整为竖屏布局
}
}
(三)深度追问:为什么默认要销毁重建?这样做有什么好处和坏处?
设计初衷(好处):
- 资源自动匹配:系统可以根据新的配置(如横屏、语言、字体大小)自动加载最合适的资源(布局、图片、字符串),无需开发者手动判断。
- 简化开发:开发者只需为不同配置提供备用资源,系统自动处理切换逻辑。
带来的问题(坏处):
- 状态丢失:如果未妥善保存,Activity成员变量、UI状态会丢失。
- 性能开销:完整的销毁与重建过程可能较慢,影响用户体验。
- 复杂状态管理:需要额外处理
Bundle的保存与恢复。
(四)现代最佳实践:如何优雅地处理横竖屏切换?
在现代化架构中,我们优先考虑保持数据与UI状态,而非简单阻止重建。推荐组合使用以下方案:
1. 使用 ViewModel(核心方案)
- 目的:将界面数据与Activity生命周期分离。
- 效果:ViewModel在配置更改(如横竖屏切换)时不会被销毁,数据得以保留。
class MyViewModel : ViewModel() {
val userData: MutableLiveData<User> = MutableLiveData()
}
// 在Activity中
private val viewModel: MyViewModel by viewModels()
2. 使用 onSaveInstanceState + ViewModel 与 SavedStateHandle
- 目的:保存和恢复少量易失的UI状态(如文本框临时内容)。
- 机制:将Bundle的保存与恢复委托给ViewModel。
class MyViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
var inputText: String?
get() = savedStateHandle["key_input"]
set(value) = savedStateHandle.set("key_input", value)
}
3. 使用 View Binding/Data Binding 避免空指针
- 在
onDestroyView中释放绑定,在onCreateView中重新绑定,配合Fragment时尤为重要。
4. 决策树:何时配置 configChanges?
通常不建议随意使用configChanges来阻止重建,除非:
- 应用是游戏或视频播放器,需要完全自己控制渲染。
- 重建成本极高且无法通过ViewModel优化。
- 你已准备好手动在
onConfigurationChanged中处理所有UI更新。
(五)面试总结与对比
| 处理方式 | 生命周期 | 数据保持 | 开发者责任 | 推荐场景 |
|---|---|---|---|---|
| 默认(重建) | 完整销毁→重建 | 需手动保存(Bundle)或依靠ViewModel | 提供多套资源,处理状态保存 | 大多数常规应用 |
| 配置configChanges | 不重建,只调用onConfigurationChanged |
全部保持(成员变量等) | 手动响应配置变更,更新UI | 游戏、相机、视频播放等特殊应用 |
| ViewModel架构 | 重建,但ViewModel存活 | 自动保持ViewModel内数据 | 无额外成本,现代标准做法 | 所有现代应用的首选 |
最终建议:
在面试中,可以这样总结:“对于横竖屏切换,现代Android开发的最佳实践是拥抱系统重建机制,但通过ViewModel和SavedStateHandle来无感地保持数据和状态,这样既能享受系统自动资源切换的便利,又能避免状态丢失的问题。仅在确有特殊性能需求的场景下,才考虑使用configChanges并手动处理。”
五、Android进程优先级从高到低有哪些?
(一)核心问题:Android系统的进程优先级是如何划分的?请从高到低说明。
Android系统根据进程对用户体验的重要性,将进程分为五个优先级层次。当系统内存不足时,会按照从低到高的顺序终止进程以释放资源。这五个优先级从高到低依次是:
- 前台进程(Foreground Process)
- 可见进程(Visible Process)
- 服务进程(Service Process)
- 后台进程(Background Process)
- 空进程(Empty Process)
(二)深度追问:请详细说明每个优先级进程的特点和构成。
1. 前台进程(最高优先级)
- 定义:用户当前正在与之交互的进程,对用户体验影响最直接。
- 典型场景:
- 拥有一个正在与用户交互的Activity(其
onResume()方法已被调用) - 拥有一个绑定到用户正在交互Activity的Service
- 拥有一个正在"前台"运行的Service(调用了
startForeground()) - 拥有一个正在执行生命周期回调的Service(如
onCreate()、onStartCommand()、onDestroy()) - 拥有一个正在执行
onReceive()方法的BroadcastReceiver
- 拥有一个正在与用户交互的Activity(其
- 系统策略:系统会不惜一切代价避免终止前台进程。只有在内存极度不足,无法维持任何进程运行时才会考虑终止前台进程。
2. 可见进程(次高优先级)
- 定义:用户虽未直接交互但依然可见的进程。
- 典型场景:
- 拥有一个不在前台但仍对用户可见的Activity(其
onPause()方法已被调用) - 拥有一个绑定到可见(或前台)Activity的Service
- 例如:一个Activity上显示了对话框或弹窗,导致后面的Activity部分可见
- 拥有一个不在前台但仍对用户可见的Activity(其
- 系统策略:除非为了维持所有前台进程运行,否则系统不会终止可见进程。
3. 服务进程(中等优先级)
- 定义:通过
startService()方法启动,正在执行用户可感知操作的进程。 - 典型场景:
- 后台播放音乐的Service
- 执行文件下载、数据同步的Service
- 网络聊天应用保持连接的后台Service
- 系统策略:系统通常会让服务进程保持运行,除非内存不足以维持所有前台和可见进程。注意:从Android 8.0(API 26)开始,对后台Service有严格限制,建议使用JobScheduler或WorkManager替代。
4. 后台进程(较低优先级)
- 定义:包含当前对用户不可见的Activity的进程(Activity的
onStop()方法已被调用)。 - 典型场景:
- 用户按Home键或Back键离开应用后的进程
- 包含已停止但尚未被销毁的Activity的进程
- 系统策略:系统可能随时终止后台进程以回收内存。后台进程被保存在一个LRU(最近最少使用)列表中,确保最近使用的进程最后被终止。
5. 空进程(最低优先级)
- 定义:不包含任何活跃应用组件的进程。
- 典型场景:
- 应用的所有Activity、Service、BroadcastReceiver等组件都已销毁
- 进程仅作为缓存保留,不执行任何代码
- 系统策略:系统会优先终止空进程以回收内存。保留空进程的主要目的是为了加快下次启动速度。
(三)现代Android开发的补充与更新
1. Android对后台进程的限制日益严格
- Android 8.0(API 26)及以上:应用在后台时,对后台Service的使用受到严格限制。如果应用针对Android 8.0或更高版本,当应用进入后台后,有几分钟的时间窗口可以创建和使用Service,之后系统会停止这些Service。
- 替代方案:对于需要长时间运行的后台任务,应使用以下替代方案:
- JobScheduler:安排任务在特定条件下执行
- WorkManager:Jetpack组件,兼容API 14+,推荐用于可延迟的后台任务
- 前台Service:必须显示通知,让用户知道应用正在运行
2. 应用待机分组(Android 9+)
Android 9(API 28)引入了应用待机分组功能,进一步优化后台资源分配:
| 分组 | 限制级别 | 典型场景 |
|---|---|---|
| 活跃(Active) | 无限制 | 应用正在被使用或最近被使用 |
| 工作集(Working Set) | 中等限制 | 应用经常但不是每天使用 |
| 频繁(Frequent) | 严格限制 | 应用定期使用但不是每天 |
| 罕见(Rare) | 非常严格限制 | 应用很少使用 |
| 受限(Restricted) | 最多限制 | 应用表现不佳,消耗过多资源 |
系统根据应用使用模式将其分配到不同分组,并相应限制后台活动、作业和闹钟。
3. 现代进程管理最佳实践
避免进程被过早终止的策略:
- 合理使用前台服务:对于用户明确知道且需要的后台任务(如音乐播放、导航),使用前台服务并显示通知
- 正确使用WorkManager:处理可延迟的后台任务
- 优化内存使用:避免内存泄漏,及时释放不再使用的资源
- 实现状态保存与恢复:正确处理
onSaveInstanceState()和ViewModel,确保进程被终止后能恢复状态
应避免的反模式:
- ❌ 使用
android:persistent="true"(仅系统应用可用) - ❌ 相互唤醒的保活策略(违反Android设计原则)
- ❌ 不必要的常驻后台服务(导致电量消耗,可能被系统限制)
六、Bundle为何需要序列化?Serializable和Parcelable的区别?
(一)核心问题:为什么在Bundle中传递自定义对象需要序列化?
这主要是由Android的进程间通信(IPC)机制决定的。当我们在Activity、Fragment等组件之间通过Intent传递Bundle,或进行跨进程通信时,数据需要被序列化(转换为字节流),以便在不同的进程内存空间中进行传输。
根本原因:
- 跨进程内存隔离:每个Android应用运行在独立的进程中,拥有各自的内存空间。Binder机制作为Android的IPC核心,需要在进程间传递数据时,将对象扁平化(flatten) 为可以在内核层拷贝的字节流。
- Bundle与Parcel:Bundle底层使用Parcel进行数据的封装和传输。Parcel要求其中存储的对象必须是基本数据类型(如int、String)或实现了特定序列化接口(如Parcelable、Serializable)的对象。
因此,自定义对象必须经过序列化,才能放入Bundle中进行跨组件或跨进程传递。
(二)深度对比:Serializable与Parcelable的主要区别
| 特性 | Serializable(Java原生) | Parcelable(Android专用) |
|---|---|---|
| 所属平台 | Java标准接口,跨平台通用 | Android SDK专用接口 |
| 实现原理 | 通过Java反射机制自动序列化与反序列化,产生大量临时对象,开销大 | 手动实现序列化过程,将对象分解为基本数据类型,直接写入Parcel,效率极高 |
| 使用方式 | 实现Serializable接口(标记接口),无需其他方法,简单 |
实现Parcelable接口,需重写:1. writeToParcel()(序列化)2. describeContents()3. 提供静态 CREATOR字段(反序列化),较复杂 |
| 性能 | 较低(反射耗时,内存占用大) | 非常高(直接内存读写,无反射) 测试表明,Parcelable比Serializable快10倍以上 |
| 适用场景 | 1. 对象需要存储到本地文件或网络传输 2. 结构复杂但对性能不敏感的场景 |
Android组件间数据传递的首选,如: 1. Activity/Fragment间通过Intent/Bundle传递 2. Service/Binder跨进程通信 |
| 文件大小 | 序列化后的字节流较大(包含大量类信息) | 字节流精简,体积小 |
| 版本兼容 | 通过serialVersionUID控制,不匹配会反序列化失败,需手动处理 |
无内置版本控制,字段增减需手动适配,易出错 |
性能关键点补充:
- Serializable:序列化过程使用大量反射,会遍历整个对象图(包括父类和关联对象),生成大量临时对象,容易触发GC。
- Parcelable:开发者手动指定需要写入Parcel的字段,避免了反射和递归遍历,直接操作内存,是Android为高性能IPC设计的方案。
(三)实现示例与注意事项
1. Serializable 实现示例与要点
// 1. 实现Serializable接口
class UserSerializable(val name: String, val age: Int) : Serializable {
// 2. 【关键】显式声明 serialVersionUID,保证版本兼容
companion object {
private const val serialVersionUID: Long = 1L
}
// 3. 敏感字段应标记为transient,避免被序列化
@Transient
val sensitiveData: String = "This won't be serialized"
}
注意事项:
- 务必显式定义
serialVersionUID,否则类结构改变(如增减字段)时,默认生成的UID会变化,导致反序列化失败。 - 使用
transient关键字修饰不需要或不能序列化的字段(如Bitmap、Context等)。
2. Parcelable 传统实现示例
class UserParcelable(val name: String, val age: Int) : Parcelable {
// 1. 从Parcel中读取数据的构造函数
constructor(parcel: Parcel) : this(
parcel.readString() ?: "", // 顺序必须与writeToParcel一致
parcel.readInt()
)
// 2. 描述内容类型(通常返回0)
override fun describeContents(): Int = 0
// 3. 序列化:将对象字段写入Parcel
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeInt(age)
}
// 4. 静态CREATOR对象,负责反序列化
companion object CREATOR : Parcelable.Creator<UserParcelable> {
override fun createFromParcel(parcel: Parcel): UserParcelable {
return UserParcelable(parcel)
}
override fun newArray(size: Int): Array<UserParcelable?> {
return arrayOfNulls(size)
}
}
}
传统实现的缺点:样板代码多,容易因字段顺序不一致导致反序列化错误。
(四)现代Android开发的最佳实践
1. 使用 @Parcelize 注解(官方推荐)
Kotlin的kotlin-parcelize插件可自动生成Parcelable代码,彻底简化实现。
步骤1:在模块级build.gradle.kts中启用插件
plugins {
id("kotlin-parcelize")
}
步骤2:使用注解声明数据类
@Parcelize
data class User(
val name: String,
val age: Int,
@IgnoredOnParcel // 标记不参与序列化的字段
val transientField: Bitmap? = null
) : Parcelable
优势:
- 零样板代码,编译器自动生成所有Parcelable方法
- 安全:字段顺序由编译器保证,避免人为错误
- 支持大部分常见数据类型(包括List、Map等集合)
2. 使用第三方注解处理器(如AutoValue、Parceler)
对于Java项目或需要更复杂序列化逻辑的场景,可考虑:
- AutoValue with Parcelable Extension:生成不可变值对象并自动实现Parcelable。
- Parceler:通过注解配置序列化方式。
3. 高级场景:自定义Parcelable逻辑
当自动生成的序列化逻辑不满足需求时(如复杂对象转换),可手动实现Parcelable或使用@TypeParceler注解。
@Parcelize
@TypeParceler<Date, DateParceler>()
data class Event(
val name: String,
val date: Date // 自定义Date的序列化方式
) : Parcelable
object DateParceler : Parceler<Date> {
override fun create(parcel: Parcel): Date {
return Date(parcel.readLong())
}
override fun Date.write(parcel: Parcel, flags: Int) {
parcel.writeLong(time)
}
}
(五)面试总结与选择策略
1. 决策流程图:
是否需要跨Android组件/进程传递?
├── 否 → 考虑Serializable(存储/网络)
└── 是 → 使用Parcelable
├── 项目使用Kotlin → 使用@Parcelize(首选)
├── 项目使用Java且结构简单 → 手动实现Parcelable
└── 复杂Java对象 → 考虑AutoValue + Parcelable扩展
2. 关键原则:
- 性能至上:在Android平台内部传递数据,无脑选Parcelable。
- 开发效率:使用
@Parcelize注解,兼顾性能与编码效率。 - 版本兼容:若使用Serializable,务必定义
serialVersionUID。 - 避免误区:不要将Context、View等与UI相关的对象放入Bundle传递(易导致内存泄漏或崩溃)。
3. 扩展问题准备:
-
Q:为什么Parcelable不能用于网络传输或本地存储?
A:Parcelable的设计与Android运行时紧密耦合,其序列化格式可能随Android版本变化,且只保证在同一设备上的进程间有效。网络传输和持久化存储需要稳定、跨平台的格式,应使用Serializable或更专业的方案(如Protocol Buffers、JSON)。 -
Q:在ViewModel或SavedStateHandle中传递数据,推荐用什么?
A:推荐使用Parcelable(配合@Parcelize)。虽然SavedStateHandle内部使用Bundle存储,但现代架构中,复杂对象应通过ID传递,在ViewModel中通过Repository重新从数据库/网络加载,而不是整个对象塞进Bundle。
七、Android动画有哪些类型?有何区别?
(一)核心问题:Android动画主要有哪些类型?它们之间最本质的区别是什么?
Android动画体系主要包含三种传统类型:View动画(补间动画)、帧动画和属性动画。它们最本质的区别在于是否真正改变对象的属性值。
- View动画和帧动画:只改变绘制效果,对象的真实属性(如位置、大小)并未改变,这可能导致点击事件响应区域与实际显示位置不匹配。
- 属性动画:通过动态修改对象的属性值来实现动画,是真正意义上的“属性改变”,因此功能更强大、效果更真实。
此外,现代Android开发中还涌现了过渡动画(Transition)、物理动画(DynamicAnimation) 和矢量动画(Vector Drawable/AnimatedVectorDrawable) 等更高级的动画方案。
(二)详细解析:三种传统动画的深度对比
1. View动画(补间动画 Tween Animation)
原理:通过对View的绘制过程施加变换(平移、旋转、缩放、透明度),生成连续的中间帧,但View的实际属性从未改变。
| 特性 | 说明 |
|---|---|
| 实现方式 | XML定义或代码创建(AlphaAnimation, RotateAnimation, ScaleAnimation, TranslateAnimation) |
| 作用对象 | 仅限View |
| 是否改变属性 | 否。例如,View平移后,其getLeft()、getX()等值不变,点击事件仍在原位置响应 |
| 优点 | API简单,兼容性好(API 1+),性能开销较小 |
| 缺点 | 功能有限,交互可能出错,无法应用于非View对象 |
| 代码示例 | xml <alpha android:fromAlpha="1.0" android:toAlpha="0.5"/> |
| 现状 | 已过时(Deprecated),仅用于维护旧项目,新项目应使用属性动画替代 |
2. 帧动画(Frame Animation / Drawable Animation)
原理:按顺序播放一系列静态图片(Drawable),模拟动画效果,类似于GIF。
| 特性 | 说明 |
|---|---|
| 实现方式 | XML定义<animation-list>,代码中调用start() |
| 作用对象 | ImageView或作为背景的View |
| 是否改变属性 | 否,仅是图片切换 |
| 优点 | 实现简单,适合小体积、帧数少的动画(如加载进度) |
| 致命缺点 | 极易导致OOM,尤其在高分辨率设备上;资源占用大;缺乏灵活性 |
| 代码示例 | xml <animation-list android:oneshot="false"> <item android:drawable="@drawable/frame1" android:duration="100"/> </animation-list> |
| 现状 | 强烈不推荐。可用Lottie或GIF库替代,或转换为WebP/AnimatedVectorDrawable |
3. 属性动画(Property Animation)
原理:系统通过不断计算并直接修改目标对象的属性值,生成平滑的动画效果。
| 特性 | 说明 |
|---|---|
| 核心类 | ValueAnimator, ObjectAnimator, AnimatorSet |
| 作用对象 | 任何对象(包括自定义对象),不限于View |
| 是否改变属性 | 是。真实修改属性,因此交互与显示一致 |
| 优点 | 功能强大、灵活,支持自定义属性、动画组合、监听器 |
| 缺点 | API稍复杂,需注意内存泄漏(如引用Activity的View) |
| 核心机制 | 插值器(TimeInterpolator) + 估值器(TypeEvaluator) |
关键概念解释:
- 插值器(Interpolator):控制动画变化速率(如加速、减速、反弹)。系统内置多种,如
AccelerateDecelerateInterpolator。 - 估值器(Evaluator):根据当前动画进度(0f~1f) 计算出属性的具体当前值。例如,从0到100的动画,在进度50%时,线性估值器会返回值50。
// 属性动画示例:在2秒内将view的x属性从0平滑过渡到500像素
ObjectAnimator.ofFloat(view, "translationX", 0f, 500f)
.setDuration(2000)
.start();
八、Activity、Service、Application 的 Context 有什么区别?
(一)核心问题:Activity、Service和Application的Context有什么区别?
这三者虽然都是Context的子类,但在来源、主题、生命周期和使用场景上存在本质区别。简单来说:
- Activity的Context:拥有界面主题,与用户界面紧密相关,生命周期最短。
- Service的Context:无界面主题,用于后台运行,生命周期较长。
- Application的Context:全局单例,贯穿整个应用生命周期,无界面主题。
它们之间的错误混用(如用Application Context启动Activity)可能导致功能异常或崩溃。
(二)详细对比:三者的本质差异
1. 来源与继承关系不同
// 继承链示意
Context
├── ContextWrapper
│ ├── Application // 应用级别的Context
│ └── Service // 服务级别的Context
└── ContextThemeWrapper
└── Activity // 拥有主题的Context
- Activity:继承自
ContextThemeWrapper,因此拥有主题(Theme)资源,可以用于构建UI界面。 - Service和Application:继承自
ContextWrapper,没有主题,不能用于构建需要主题的UI组件。
2. 实例与生命周期不同
| 类型 | 实例数量 | 生命周期 | 销毁时机 |
|---|---|---|---|
| Application | 1个(单例) | 最长(应用进程存活期间) | 进程终止时 |
| Activity | 多个(每Activity一个) | 较短(用户交互期间) | 调用finish()或配置销毁时 |
| Service | 多个(每Service一个) | 较长(后台运行期间) | 调用stopSelf()或stopService()时 |
关键点:每个Activity、Service以及Application都对应一个独立的ContextImpl对象(Context的真正实现者),因此它们的内存地址不同。
3. 主题(Theme)与UI能力不同
- Activity Context:唯一能用于创建与主题相关UI组件的Context。例如:
- 显示Dialog(
AlertDialog、DialogFragment) - 启动一个具有界面元素的Window(如
PopupWindow) - 调用
LayoutInflater.from(context)并传入非Activity的Context可能导致主题缺失
- 显示Dialog(
- Service/Application Context:无法用于创建需要主题的UI组件,否则会抛出
WindowManager.BadTokenException异常。
// ❌ 错误示例:使用Application Context显示对话框
val dialog = AlertDialog.Builder(applicationContext).create() // 运行时报错
dialog.show() // 抛出异常:Unable to add window -- token null is not valid
// ✅ 正确示例:必须使用Activity Context
val dialog = AlertDialog.Builder(activityContext).create()
dialog.show()
4. 资源访问的细微差别
理论上,三者都可以访问应用的资源(字符串、颜色、尺寸等)。但涉及主题相关资源时,只有Activity Context能正确获取当前主题下的资源值。
// 可能得到不同的颜色值
val colorFromActivity = ContextCompat.getColor(activityContext, R.color.primary) // 使用Activity主题
val colorFromApp = ContextCompat.getColor(applicationContext, R.color.primary) // 使用默认主题
(三)如何正确获取Application Context?
候选人答: 有两种主要方式,但适用场景不同:
| 方法 | 调用位置 | 返回类型 | 说明 |
|---|---|---|---|
getApplication() |
仅限Activity或Service内部 | Application |
返回当前组件所属的Application实例(具体类型) |
getApplicationContext() |
任何Context对象均可调用 | Context(实际为Application) |
返回Application的Context引用(Context类型) |
关键区别:
getApplication()返回的是具体的Application子类(如MyApplication),便于类型转换后访问自定义方法。getApplicationContext()返回的是Context类型,但运行时就是Application实例。
现代最佳实践:
- 在非UI组件(如Repository、ViewModel)中需要Context时,应通过依赖注入传递
Application的Context(而非Activity的),以避免内存泄漏。 - 使用
Application作为单例Context源。
// 推荐:在自定义Application中提供全局Context
class MyApp : Application() {
companion object {
lateinit var instance: MyApp
private set
}
override fun onCreate() {
super.onCreate()
instance = this
}
}
// 使用时
val appContext = MyApp.instance.applicationContext
(四)使用场景与注意事项
1. 必须使用Activity Context的场景
- 显示任何类型的对话框(Dialog、AlertDialog、DialogFragment)
- 启动
PopupWindow - 使用
LayoutInflater.inflate()加载布局并希望应用当前Activity主题时 - 调用
startActivityForResult()(已废弃,但相关) - 调用
registerForActivityResult()注册Activity结果回调
2. 可以使用Application Context的场景
- 启动Service(但注意Android 8.0+的限制)
- 发送广播(Broadcast)
- 获取系统服务(如
getSystemService()) - 访问全局资源(如图片加载、数据库初始化)
- 创建静态工具类时(需注意内存泄漏)
3. 常见错误与内存泄漏
错误示例:在单例或静态变量中持有Activity Context。
object AppManager {
// ❌ 危险:静态变量持有Activity引用,导致Activity无法被回收
var context: Context? = null
}
// 在Activity中
AppManager.context = this // 内存泄漏!
解决方案:
- 使用
WeakReference弱引用(不推荐,易出错) - 最佳实践:传递Application Context,或使用依赖注入框架(如Hilt)管理Context依赖
(五)数量关系与进阶理解
1. Context数量公式(简化版)
Context总数 = Activity实例数 + Service实例数 + 1(Application)
注意:实际上还有:
- 静态广播接收器(
BroadcastReceiver)的Context - ContentProvider的Context
- 其他系统创建的Context
但在日常开发中,我们主要关注Activity、Service和Application这三类。
2. 关于getBaseContext()
- 这是
ContextWrapper类的方法,返回其包装的ContextImpl对象。 - 在Activity或Service中,
getBaseContext()返回的是同一个ContextImpl。 - 不建议使用:直接使用
this作为Context即可,除非在自定义ContextWrapper中。
3. 现代架构中的Context管理
在MVVM或MVI架构中,应尽量避免在ViewModel或UseCase中直接持有Context。推荐做法:
方案一:使用AndroidViewModel(谨慎)
class MyViewModel(application: Application) : AndroidViewModel(application) {
// 可以访问Application Context
val context: Context get() = getApplication<Application>().applicationContext
}
// 注意:ViewModel应避免持有Activity Context
方案二:依赖注入(推荐)
// 使用Hilt注入Application Context
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideAppContext(@ApplicationContext context: Context): Context = context
}
// 在Repository中使用
class UserRepository @Inject constructor(
@ApplicationContext private val context: Context
) { /* ... */ }
方案三:使用资源包装类
// 将Context依赖封装为资源提供器
interface ResourceProvider {
fun getString(@StringRes resId: Int): String
}
class AndroidResourceProvider @Inject constructor(
@ApplicationContext private val context: Context
) : ResourceProvider {
override fun getString(resId: Int): String = context.getString(resId)
}
(六)面试总结:快速对比表
| 特性 | Application Context | Activity Context | Service Context |
|---|---|---|---|
| 继承类 | ContextWrapper | ContextThemeWrapper | ContextWrapper |
| 主题 | 无 | 有 | 无 |
| 生命周期 | 应用级别(最长) | Activity级别(短) | Service级别(较长) |
| UI能力 | 不能创建UI组件 | 能创建所有UI组件 | 不能创建UI组件 |
| 对话框 | ❌ 导致异常 | ✅ 正确方式 | ❌ 导致异常 |
| 启动Activity | 需加FLAG_ACTIVITY_NEW_TASK |
直接启动 | 需加FLAG_ACTIVITY_NEW_TASK |
| 内存泄漏风险 | 低(全局单例) | 高(易被长生命周期对象引用) | 中 |
| 推荐使用场景 | 全局工具类、资源访问 | UI相关操作、对话框 | 后台服务操作 |
一句话总结:
在Android中,Application Context用于全局操作且生命周期长,Activity Context用于UI相关操作但需警惕内存泄漏,Service Context用于后台服务。选择Context的原则是:能用Application的就不用Activity的,必须用Activity的绝不滥用。
九、从 Android 5.0 到 Android 14,有哪些主要新特性?
(一)核心问答:请简述从Android 5.0到14的主要演进脉络。
Android在这十年间的更新,主线非常清晰,主要围绕设计语言、性能与流畅度、隐私安全、以及用户体验四大方向展开。
- 设计革新(5.x - 12):从Material Design的引入,到Material You动态主题,系统视觉和交互范式发生了根本性变化。
- 性能与能效(5.x - 14):虚拟机从ART(AOT) 演进到JIT/AOT混合编译;同时通过Doze模式、应用待机、后台限制等一系列措施持续优化续航。
- 隐私安全(6.0 - 14):这是最深刻的变革。权限管理从安装时静态授权变为运行时动态申请,并不断细化,衍生出一次性权限、近似位置、自动重置、照片选择器、通知权限等更精细的控制。
- 交互与体验(7.0 - 14):分屏、画中画、手势导航、边缘到边缘等特性,让应用能更好地适应大屏、折叠屏等多样化的设备形态。
下面的表格可以帮你快速回顾每个版本最核心的特性:
| 版本 | 核心特性(开发者视角) | 补充说明与修正 |
|---|---|---|
| Android 5.x | 1. Material Design:全新设计语言。 2. ART虚拟机(AOT):提升应用运行效率。 3. 改进通知:锁屏通知。 |
奠定了现代Android的视觉基础。 |
| Android 6.0 | 1. 运行时权限:核心变革,需动态申请。 2. Doze与App Standby:引入后台省电机制。 3. 指纹支持。 |
必须掌握checkSelfPermission()和requestPermissions()的适配。 |
| Android 7.0 | 1. 多窗口模式:分屏显示。 2. JIT编译器回归:与AOT结合,加快安装和运行速度。 3. V2应用签名。 |
通知功能增强,支持捆绑通知和直接回复。 |
| Android 8.0 | 1. 通知渠道:用户可精细管理每类通知。 2. 画中画模式:视频播放等场景。 3. 后台执行限制:限制后台服务和位置更新。 4. 自适应图标。 |
必须适配通知渠道,否则通知无法在8.0及以上系统显示。 |
| Android 9 | 1. 刘海屏适配。 2. 统一生物识别对话框。 3. 隐私加强:限制后台访问传感器、Wi-Fi。 |
建议使用DisplayCutout API进行凹口适配。 |
| Android 10 | 1. 全系统黑暗主题。 2. 手势导航:引入全屏手势。 3. 隐私增强:仅限前台访问位置(重大变更)、存储分区(Scoped Storage)。 |
Scoped Storage是适配重点,改变了应用访问外部存储的方式。 |
| Android 11 | 1. 一次性权限(位置、麦克风、相机)。 2. 自动重置未使用应用权限。 3. 会话权限:如前台服务位置访问。 |
强调权限授予的临时性和用户可控性。 |
| Android 12 | 1. Material You:动态配色引擎。 2. 更安全的隐私指示器:相机、麦克风调用时状态栏有提示。 3. 近似位置权限。 |
设计上鼓励应用跟随系统动态取色。 |
| Android 13 | 1. 更精细的媒体权限:独立选择照片/视频,而非整个存储。 2. 通知运行时权限:新增 POST_NOTIFICATIONS权限。3. 主题应用图标。 |
必须适配新的通知权限,否则应用通知默认关闭。 |
| Android 14 | 1. 前台服务类型细化:更严格的类型声明和权限要求。 2. 隐式广播限制加强。 3. 照片选择器(系统级)等隐私增强。 4. 语法性别API等。 |
进一步收紧后台行为,推动使用JobScheduler等现代API进行后台任务调度。 |
(二)如何让回答更出彩:延伸与关联
在回答时,如果能将特性关联到具体的开发适配经验和架构演进,会大大加分。
可以这样延伸:
“以后台处理为例,可以清晰地看到一条演进路径:在早期,我们可能会直接使用
Service进行长时间后台工作。但从Android 6.0的Doze模式、8.0的后台执行限制,到10以后的严格限制,系统一直在引导开发者使用更高效的方案,如JobScheduler、WorkManager。在Android 14中,对前台服务类型的细化,正是这条路径的最新体现。这要求我们在架构设计时,就必须将后台任务的可推迟性、省电性纳入考量。”
面试官可能会追问:
- “你提到运行时权限是Android 6.0的核心,如果要在不支持此特性的旧系统上保证兼容,该怎么设计?”
- 答:使用
ContextCompat.checkSelfPermission()和ActivityCompat.requestPermissions()(来自AndroidX支持库),它们内部会自动判断版本,在旧系统上直接返回授权成功或忽略请求。这是典型的向后兼容设计。
- 答:使用
- “Android 10的Scoped Storage(分区存储)主要解决了什么问题?如何适配?”
- 答:主要解决应用随意访问用户全部外部存储文件带来的隐私混乱和文件管理混乱问题。适配分两种情况:
- 访问自己的文件:使用
Context.getExternalFilesDir(),无需权限。 - 访问共享媒体文件:使用
MediaStoreAPI。如需访问其他类型文件,应使用系统文件选择器(ACTION_OPEN_DOCUMENT)。
- 访问自己的文件:使用
- 答:主要解决应用随意访问用户全部外部存储文件带来的隐私混乱和文件管理混乱问题。适配分两种情况:
- “从开发者角度看,你认为哪个版本的适配挑战最大?为什么?”
- 答:(这是一个开放性问题,没有标准答案,考察思考和总结能力)
- 挑战最大:Android 6.0或Android 10。6.0因为运行时权限彻底改变了应用与用户的权限交互模型,涉及大量代码重构和逻辑重写。10的Scoped Storage则彻底改变了文件访问范式,影响深远。
- 最具前瞻性:Android 12的Material You。它要求应用在设计时不仅要考虑自身品牌色,还要思考如何与系统和谐共生,代表了未来UI动态化、个性化的方向。
- 答:(这是一个开放性问题,没有标准答案,考察思考和总结能力)
十、JSON 是什么?在 Android 中如何解析?
(一)核心问题:什么是JSON?在Android中如何解析它?
JSON是一种轻量级的、基于文本的数据交换格式。它采用完全独立于编程语言的文本格式,使用键:值对的方式来组织数据,结构清晰,易于人阅读和编写,也易于机器解析和生成。在Android开发中,它已成为网络接口数据传输的事实标准。
Android中解析JSON的核心思路是:将JSON格式的字符串,通过特定的解析库,转换(反序列化) 为程序中的对象(如Kotlin数据类),以便进行操作。
(二)主流JSON解析方案对比与选择
当前主要有以下几种解析方案,下表对比了它们的特点:
| 方案 | 优点 | 缺点 / 注意事项 | 适用场景 |
|---|---|---|---|
| GSON | 1. Google出品,流行度极高,社区资源丰富。 2. API极为简单,一行代码完成对象转换。 3. 功能全面(支持泛型、复杂对象图)。 |
1. 大量使用反射,在性能敏感或包体积敏感的场景下可能不是最优选择。 2. 默认配置下容错性强,但可能导致意料之外的数据映射。 |
快速原型开发、对性能要求不苛刻的中小型应用、遗留项目。 |
| Moshi | 1. Square公司出品,性能优于GSON(使用代码生成而非反射)。 2. 对Kotlin支持极好(支持空安全、默认参数)。 3. 设计更现代化、更严格。 |
1. 功能不如GSON丰富(例如,默认不支持多态解析)。 2. 社区生态和熟悉度略逊于GSON。 |
追求性能和新特性的现代Kotlin项目,尤其是与OkHttp/Retrofit同属Square生态,集成流畅。 |
| kotlinx.serialization | 1. JetBrains官方出品,Kotlin原生序列化框架。 2. 编译时生成代码,无反射,性能优异。 3. 与Kotlin语言特性(如协程、多平台项目)深度集成。 |
1. 需要额外的编译器插件(Kotlin Plugin)支持。 2. 相对较新,在处理某些极端复杂的JSON结构时可能需更多配置。 |
Kotlin多平台项目(KMM)、新启动的纯Kotlin项目、追求极致Kotlin体验的开发。 |
| org.json (Android内置) | 1. 无需引入第三方库,无额外依赖。 2. 提供基础的 JSONObject和JSONArray进行手动解析。 |
1. 手动解析,代码繁琐且易出错。 2. 不支持对象直接映射。 |
仅解析极简单的JSON结构,或对应用包体积有极端要求的场景(现已罕见)。 |
现代选择建议:
- 新项目首选:kotlinx.serialization(官方未来)或 Moshi(生态成熟)。
- 维护旧项目:继续使用 GSON。
- 避免使用:手动解析
org.json,效率低下。
(三)实现示例:三种主流方案的用法
假设我们有如下JSON字符串和对应的Kotlin数据类:
// JSON 字符串
val jsonString = """
{
"name": "张三",
"age": 25,
"email": "[email protected]"
}
"""
// 对应的Kotlin数据类
data class User(
val name: String,
val age: Int,
val email: String? // 可空字段,应对JSON中可能缺失的情况
)
1. 使用 GSON 解析
// 1. 添加依赖:implementation `com.google.code.gson:gson:2.10.1`
// 2. 创建Gson实例(建议全局复用)
val gson = Gson()
// 解析为单个对象
val user: User = gson.fromJson(jsonString, User::class.java)
// 解析为列表(注意处理泛型擦除)
val jsonArrayString = """[ ... ]"""
val typeToken = object : TypeToken<List<User>>() {}.type
val userList: List<User> = gson.fromJson(jsonArrayString, typeToken)
2. 使用 Moshi 解析
// 1. 添加依赖:
// implementation `com.squareup.moshi:moshi:1.15.0`
// kapt `com.squareup.moshi:moshi-kotlin-codegen:1.15.0` // 代码生成
// 或使用 reflection(包体积更大):implementation `com.squareup.moshi:moshi-kotlin:1.15.0`
// 2. 创建Moshi实例并构建解析器
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory()) // 如果使用反射适配器则需要这行
.build()
val jsonAdapter: JsonAdapter<User> = moshi.adapter(User::class.java)
// 3. 解析
val user: User? = jsonAdapter.fromJson(jsonString) // 返回可空类型
val json = jsonAdapter.toJson(user) // 序列化
3. 使用 kotlinx.serialization 解析
// 1. 项目级 build.gradle.kts: plugins { kotlin("plugin.serialization") version "1.9.0" }
// 2. 模块级 build.gradle.kts: implementation `org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0`
// 2. 为数据类添加 @Serializable 注解
import kotlinx.serialization.Serializable
@Serializable
data class User(
val name: String,
val age: Int,
val email: String? = null // 可空且有默认值
)
// 3. 解析
import kotlinx.serialization.json.Json
val user: User = Json.decodeFromString<User>(jsonString)
val userList: List<User> = Json.decodeFromString<List<User>>(jsonArrayString) // 原生支持集合
(四)高级话题与常见问题处理
1. 处理JSON字段与Kotlin属性名不一致的情况
- GSON: 使用
@SerializedName注解。 - Moshi: 使用
@Json注解。 - kotlinx.serialization: 使用
@SerialName注解。
// 以 kotlinx.serialization 为例
@Serializable
data class User(
@SerialName("user_name") // JSON中的字段名为 user_name
val name: String,
@SerialName("user_age")
val age: Int
)
2. 解析策略:严格模式 vs 宽松模式
- 严格模式:要求JSON格式与数据类定义完全匹配,否则抛出异常。这有助于在开发早期发现问题。
- 宽松模式:忽略未知字段、允许数值转换为字符串等,兼容性更好。
// 在 kotlinx.serialization 中配置宽松模式
val lenientJson = Json {
ignoreUnknownKeys = true // 忽略未知键
isLenient = true // 允许JSON格式稍不严格(如尾逗号)
}
val user = lenientJson.decodeFromString<User>(someJson)
3. 性能与内存优化
- 对于超大JSON(如超过1MB),考虑使用流式解析(如Moshi的
JsonReader或GSON的JsonReader),避免一次性将整个字符串读入内存。 - 复用解析器实例(如
Gson、Moshi、Json对象),避免重复创建的开销。
(五)面试总结与要点回顾
1. 一句话总结:
JSON是一种轻量级数据交换格式。在Android中,我们不再手动解析,而是使用如GSON、Moshi或kotlinx.serialization等库,将JSON字符串自动反序列化为Kotlin/Java对象。
2. 选择建议:
- 快速开发/旧项目:GSON。
- 现代Kotlin项目:Moshi 或 kotlinx.serialization。
- Kotlin多平台(KMM):kotlinx.serialization。
3. 必备知识点:
- 能说明反射解析(GSON) 与代码生成解析(Moshi/kotlinx) 在原理和性能上的区别。
- 能处理字段名映射、空安全和默认值问题。
- 了解严格模式与宽松模式的区别及配置。
4. 关联问题准备:
- Q: 除了JSON,还了解哪些数据交换格式(如Protocol Buffers)?与JSON相比有何优劣?
- A: Protocol Buffers(Protobuf)是二进制格式,体积更小、序列化/反序列化速度更快,但可读性差,需要预定义
.proto文件,常用于对性能要求极高的内部微服务通信。JSON则因其可读性好、通用性广,更常用于对外的Web API。
- A: Protocol Buffers(Protobuf)是二进制格式,体积更小、序列化/反序列化速度更快,但可读性差,需要预定义
- Q: 在解析网络返回的JSON时,如何设计一个健壮的数据层?
- A: 通常会定义统一的响应包装类(如
BaseResponse<T>),包含code、message、data字段。使用sealed class(密封类)或Result类来封装成功/失败状态,结合协程的异常处理,在Repository或ViewModel层进行统一处理。
- A: 通常会定义统一的响应包装类(如
在面试中,清晰地说出不同方案的权衡取舍以及你选择它们的理由,比单纯罗列API更能体现你的思考深度。
参考文献
Android面试题(一)
https://blog.uso6.com/archives/androidmian-shi-ti-yi
评论