Android四大组件面试题,看完这篇就够了
四大组件是Android的基础中的基础,但面试的时候很多人只停留在"会用"的层面。比如Service,很多人知道startService和bindService,但问到它们区别、什么时候用哪个、怎么保活,就卡壳了。
1. Activity生命周期:从打开到销毁,系统都干了啥?
核心回答
完整流程是这样的:
makefile
启动Activity: onCreate → onStart → onResume
Activity可见: onRestart → onStart → onResume
退出Activity: onPause → onStop → onDestroy
正常情况很简单,但面试官爱问的是异常情况。
内存不足时:
系统会按优先级杀进程,Activity跟着遭殃。流程是 onPause → onStop → onSaveInstanceState,注意不一定会调onDestroy 。等你再回来,系统会调 onCreate(savedInstanceState) → onRestoreInstanceState。
kotlin
// Activity被系统杀死后恢复
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) {
// 从bundle恢复数据
}
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
// 或者在这里恢复,更安全,因为super之后数据已就绪
}
配置变更时(屏幕旋转):
Activity会被销毁重建,生命周期和内存不足一样:onPause → onStop → onSaveInstanceState → onDestroy
然后 onCreate(savedInstanceState) 重新创建。
实战场景
你做个视频播放页面,屏幕旋转了,这时候:
- 如果没处理横竖屏适配,视频会从头播放(体验差)
- 如果在
onSaveInstanceState保存了播放进度,恢复时能从断点继续 - 如果用了ViewModel,数据自动保存,连进度都不用自己管
kotlin
class VideoViewModel : ViewModel() {
var currentPosition: Long = 0 // 配置变更后自动保留
}
面试加分点
- onStart和onResume的区别:onStart是Activity可见但不可交互,onResume是可见且可交互。比如弹 Dialog 时,Activity只是不可交互但仍可见,所以只调onPause不调onStop。
- onSaveInstanceState的调用时机:不一定是销毁前,系统觉得需要保存状态时就会调。
- 现代方案:Jetpack ViewModel + SavedStateHandle 处理配置变更,比手动存bundle优雅得多。
2. Activity启动模式:四种模式到底怎么选?
核心回答
先搞清楚两个概念:
- 任务栈(Task Stack) :存放Activity的栈,默认一个应用一个任务栈
- 返回栈(Back Stack) :用户按返回键时Activity的弹出顺序
四种模式:
| 模式 | 特点 | 典型场景 |
|---|---|---|
| standard | 默认,每次启动都创建新实例 | 普通页面 |
| singleTop | 栈顶复用,如果目标Activity已在栈顶,不创建 | 通知栏跳详情、搜索页 |
| singleTask | 整个任务栈只有一个实例,清除上面的Activity | 首页、App入口 |
| singleInstance | 独占一个任务栈,其他App可直接调用 | 闹钟、电话、Launcher |
实战场景
电商App加入购物车:
ini
// 从商品详情加入购物车,用singleTop避免重复创建购物车页
// AndroidManifest.xml
<activity
android:name=".CartActivity"
android:launchMode="singleTop" />
// 或者代码指定
val intent = Intent(this, CartActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
startActivity(intent)
微信聊天页跳转账页:
arduino
// 支付宝这种,需要singleTask确保只有一个支付任务栈
// 避免用户在其他地方付款后,App内支付页混乱
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_CLEAR_TASK
面试加分点
- Flags的坑 :代码里用
intent.flags和xml里用launchMode效果不完全一样,Flags优先级更高 - 任务亲和性(affinity) :可以自定义任务栈,配合
taskAffinity属性 - 跨应用启动 :用
Intent.FLAG_ACTIVITY_NEW_TASK可以从其他App启动自己的Activity到新任务栈
3. Service:start和bind到底怎么选?
核心回答
Started Service(started service):
- 调用
startService()启动,独立运行在主线程 - 组件销毁后Service继续运行,直到自己调用
stopSelf()或外部调用stopService() - 单向通信,组件无法获取Service的返回值
Bound Service(binded service):
- 调用
bindService()绑定,组件和Service绑定在一起 - 组件销毁时Service也跟着销毁(除非用了
START_STICKY) - 双向通信,可以通过IBinder获取Service的引用,调用其方法
前台Service vs 后台Service:
kotlin
// 前台Service,必须显示通知,否则用户以为App偷跑
val notification = NotificationCompat.Builder(this, channelId)
.setContentTitle("正在下载")
.setContentText("xxx.apk")
.build()
val serviceIntent = Intent(this, DownloadService::class.java)
startForegroundService(serviceIntent) // Android 8.0+必须用这个
// 然后在Service里
class DownloadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
startForeground(NOTIFICATION_ID, notification)
return START_NOT_STICKY
}
}
Android 8.0+后台启动限制
后台App不能启动前台Service ,会抛IllegalStateException。
解决方案:
- 用WorkManager替代:后台任务用WorkManager,它会在合适的时机执行
- JobIntentService:如果必须用Service,可以用这个,8.0前用普通Service,8.0后自动转为JobService
- 申请FOREGROUND_SERVICE权限 :Manifest里声明
android:foregroundServiceType
实战场景
音乐播放器:
kotlin
// 必须用前台Service,否则后台播放会被杀
class MusicService : Service() {
private val binder = MusicBinder()
inner class MusicBinder : Binder() {
fun getService(): MusicService = this@MusicService
}
override fun onBind(intent: Intent?): IBinder = binder
}
// Activity绑定
val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
musicService = (service as MusicBinder).getService()
}
}
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
面试加分点
- Service和Thread的关系:Service是运行在主线程的,如果你要干活,还是得开Thread或者用协程
- onStartCommand返回值 :
START_STICKY会被系统重启(清空intent),START_NOT_STICKY不会重启,START_REDELIVER_INTENT会重传intent - JobScheduler:比Service更省电,系统会批量执行和优化调度
4. BroadcastReceiver:静态注册和动态注册怎么选?
核心回答
静态注册(在Manifest里声明):
xml
<!-- Manifest -->
<receiver android:name=".BootCompleteReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
特点:
- App未运行时也能接收
- 系统会创建Receiver实例并回调
- Android 8.0+大量静态广播被禁用
动态注册:
kotlin
class MainActivity : AppCompatActivity() {
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// 处理
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(receiver) // 必须解注册!
}
}
特点:
- 跟组件生命周期绑定,更可控
- 不能接收隐式广播(Android 8.0+)
本地广播 vs 全局广播
本地广播(LocalBroadcastManager):
- 只在App内部传递,不涉及IPC
- 更安全,不会被其他App接收
- 效率更高
kotlin
// 发送本地广播
val intent = Intent("com.example.MY_ACTION")
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)
全局广播:
- 跨进程,可以被其他App接收
- 需要考虑安全问题,避免被恶意App拦截
Android 8.0+隐式广播限制
大部分隐式广播不能用静态注册了 。比如ACTION_NEW_PICTURE、ACTION_PACKAGE_REPLACED等。
替代方案:
- 用动态注册:在需要的时候注册,生命周期结束解注册
- 用JobScheduler/WorkManager轮询:比如想知道App更新,可以用版本检查
- 保留的白名单广播:系统会维护一个白名单,可以查官方文档
实战场景
监听网络变化:
kotlin
// 正确做法:动态注册
class NetworkActivity : Activity {
private val networkReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val network = connectivityManager.activeNetwork
val capabilities = connectivityManager.getNetworkCapabilities(network)
if (capabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true) {
// 有网
} else {
// 没网
}
}
}
override fun onResume() {
super.onResume()
registerReceiver(networkReceiver, IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION))
}
override fun onPause() {
super.onPause()
unregisterReceiver(networkReceiver)
}
}
面试加分点
- 有序广播(Ordered Broadcast) :可以设置优先级,优先级高的Receiver可以中断广播传播
- LocalBroadcastManager已废弃:Google推荐用LiveData或EventBus替代
- Receiver的耗时限制:onReceive里不能做耗时操作(10秒),否则ANR
5. ContentProvider:跨进程数据共享的原理是什么?
核心回答
ContentProvider是Android的进程间通信(IPC) 机制之一,底层用的是Binder。
流程是这样的:
sql
App A(Provider) Binder驱动 App B(Client)
| | |
| ContentProvider.onCreate()| |
|<---- publish ---------> | |
| | |
| |<---- getContentResolver()
| | |
|<--------- IPC ----------->| |
| query/update/... | |
| | |
简单说:
- Provider启动时,在AMS注册,返回一个Binder对象
- Client通过ContentResolver访问,Resolver内部通过Binder找到Provider
- Provider在主线程执行数据库/文件操作(所以别在主线程干重活)
生命周期和启动时机
kotlin
class MyProvider : ContentProvider() {
override fun onCreate(): Boolean {
// Provider创建时调用,只调用一次
// 可以在这里初始化数据库、打开文件等
return true
}
override fun query(uri: Uri, projection: Array<String>?,
selection: String?, selectionArgs: Array<String>?,
sortOrder: String?): Cursor? {
// 真正的数据查询在这里
return null
}
}
启动时机:
- 按需启动:ContentResolver首次访问时,AMS才启动Provider
- 预启动 :可以通过
android:initOrder控制启动顺序
实战场景
实现一个图片Provider:
kotlin
class ImageProvider : ContentProvider() {
private lateinit var dbHelper: ImageDbHelper
companion object {
const val AUTHORITY = "com.example.provider.images"
val CONTENT_URI: Uri = Uri.parse("content://$AUTHORITY/images")
}
override fun onCreate(): Boolean {
dbHelper = ImageDbHelper(context)
return true
}
override fun query(uri: Uri, projection: Array<String>?,
selection: String?, selectionArgs: Array<String>?,
sortOrder: String?): Cursor? {
val db = dbHelper.readableDatabase
return db.query("images", projection, selection,
selectionArgs, null, null, sortOrder)
}
}
其他App访问:
less
// ContentResolver自动处理Binder通信
val cursor = contentResolver.query(
ImageProvider.CONTENT_URI,
arrayOf("_id", "path", "size"),
"size > ?",
arrayOf("1024"),
"size DESC"
)
面试加分点
- ContentUris工具类 :
withAppendedId()添加ID,parseId()解析ID - uriMatcher:用UriMatcher匹配不同的uri来做路由
- 权限控制 :
android:grantUriPermissions="true"配合Intent.FLAG_GRANT_READ_URI_PERMISSION - 和Binder的关系:ContentProvider底层是Binder,但封装了更高级的接口,比AIDL更方便
6. Fragment:生命周期怎么配合Activity?事务怎么提交才不出bug?
核心回答
Fragment和Activity的关系:
Fragment生命周期嵌套在Activity里:
makefile
Activity: onCreate ─────────────────────────────────────────────────
│
Fragment: onAttach → onCreate → onCreateView → onActivityCreated ──
│ │
↓ ↓
Activity: onStart ──────────────────────────────────────────────── onStart
│ │
Fragment: onStart ─────────────────────────────────────────── onStart
│ │
Activity: onResume ────────────────────────────────────────── onResume
│ │
Fragment: onResume ──────────────────────────────────────── onResume
简单记:Activity的每个生命周期,Fragment都会跟着经历一次(除了onCreate有对应的onCreate但子步骤不同)。
add/show/hide vs replace
| 操作 | 特点 | 适用场景 |
|---|---|---|
| add | 添加Fragment,回调onCreateView,不销毁视图 | 需要快速切换、保留状态 |
| show | 显示已add的Fragment | 切换Tab时保留状态 |
| hide | 隐藏Fragment | 配合show用 |
| replace | 先remove旧的,再add新的 | 确实要销毁重建 |
scss
// add + show/hide:保留状态,切换快
supportFragmentManager.beginTransaction()
.add(R.id.container, fragment1, "f1")
.add(R.id.container, fragment2, "f2")
.hide(fragment2)
.commit()
// replace:会销毁重建
supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment3)
.commit()
事务提交的最佳实践
必须用commitAllowingStateLoss()!
scss
// ❌ 可能会崩溃
transaction.commit()
// ✅ 安全版
transaction.commitAllowingStateLoss()
为什么?
当你按了Home键,Activity正在onSaveInstanceState保存状态,这时候commit()会抛异常:IllegalStateException: Can not perform this action after onSaveInstanceState
但commitAllowingStateLoss()允许在状态保存后提交,不会崩溃(可能丢失最后一次操作)。
scss
// Fragment的坑:如果View已经销毁了,不能commit
if (isAdded) { // 先检查Fragment是否attached
supportFragmentManager.beginTransaction()
.replace(R.id.container, newFragment)
.commitAllowingStateLoss()
}
实战场景
ViewPager + Fragment配合:
kotlin
class MyPagerAdapter : FragmentStateAdapter {
override fun createFragment(position: Int): Fragment {
return when (position) {
0 -> HomeFragment()
1 -> DiscoveryFragment()
2 -> MineFragment()
else -> throw IllegalArgumentException()
}
}
override fun getItemCount(): Int = 3
}
// ViewPager2用这个
viewPager2.adapter = MyPagerAdapter(this)
Fragment懒加载(ViewPager2 + Fragment):
kotlin
abstract class LazyFragment : Fragment() {
private var isDataLoaded = false
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (userVisibleHint && !isDataLoaded) {
loadData()
isDataLoaded = true
}
}
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
super.setUserVisibleHint(isVisibleToUser)
if (isVisibleToUser && !isDataLoaded) {
loadData()
isDataLoaded = true
}
}
abstract fun loadData()
}
面试加分点
- Fragment的坑 :Fragment的
onResume和onPause是基于用户可见性的,不是Activity那种精确区分 - FragmentFactory :AndroidX 1.2.0+引入,解决
no empty constructor的经典问题 - FragmentResult API:Fragment之间通信的新方式,比setArguments/onActivityCreated更清晰
7. Intent和PendingIntent:PendingIntent为什么能跨进程?
核心回答
Intent:消息对象,告诉系统"要做什么"
PendingIntent :Intent的包装器,让别人在未来的某个时刻替我执行Intent
kotlin
// Intent:立刻执行
val intent = Intent(this, TargetActivity::class.java)
startActivity(intent)
// PendingIntent:稍后执行(可能是其他进程)
val pendingIntent = PendingIntent.getActivity(
this,
requestCode, // 唯一标识
intent, // 要执行的Intent
flags // 行为标志
)
// 典型场景:Notification
val notification = NotificationCompat.Builder(this, channelId)
.setContentIntent(pendingIntent) // 点击通知时打开Activity
.build()
为什么能跨进程?
PendingIntent本质上是Binder Token。
当你创建PendingIntent时,系统会:
- 分配一个Binder token(唯一标识这个PendingIntent)
- 把你的Intent包装起来
- 把PendingIntent保存到系统服务(NotificationManagerService等)
- 返回给你一个引用
当其他进程使用这个PendingIntent时:
- 系统通过Binder token找到原始Intent
- 在原App的进程里执行Intent
- 结果返回给调用方
所以PendingIntent不是"把你的Intent发到其他进程",而是"让其他进程能回调到你的进程"。
PendingIntent Flags怎么选?
| Flag | 效果 | 适用场景 |
|---|---|---|
| FLAG_ONE_SHOT | 只用一次,用完就删 | 验证码通知、一次性操作 |
| FLAG_NO_CREATE | 不存在就返回null,不创建 | 判断PendingIntent是否已存在 |
| FLAG_CANCEL_CURRENT | 存在就删除旧创建新的 | 确保拿到最新数据 |
| FLAG_UPDATE_CURRENT | 存在就更新extra数据 | 通知更新、推送更新 |
kotlin
// 场景1:推送一条通知,需要多次更新内容
// 用FLAG_UPDATE_CURRENT,这样每次更新都是同一个PendingIntent
val pendingIntent = PendingIntent.getActivity(
this,
1001,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 场景2:验证码通知,只能点一次
// 用FLAG_ONE_SHOT,防止重复点击
val pendingIntent = PendingIntent.getActivity(
this,
1002,
intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
实战场景
通知系统:
kotlin
// 创建 PendingIntent
val intent = Intent(this, ChatActivity::class.java).apply {
putExtra("chat_id", chatId)
putExtra("from_notification", true)
}
val pendingIntent = PendingIntent.getActivity(
this,
chatId.hashCode(), // requestCode要唯一,否则会覆盖
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
// 构建通知
val notification = NotificationCompat.Builder(this, chatChannelId)
.setSmallIcon(R.drawable.ic_chat)
.setContentTitle("新消息")
.setContentText(lastMessage)
.setContentIntent(pendingIntent) // 点击打开聊天页
.setAutoCancel(true) // 点击后消失
.build()
notificationManager.notify(chatId, notification)
面试加分点
- FLAG_IMMUTABLE(Android 12+) :必须加!否则会崩溃。Android 12开始,出于安全考虑,PendingIntent必须是不可变的
- requestCode的重要性:必须唯一,否则不同的Intent会互相覆盖
- PendingIntent的"回拨"机制: 不是把Intent发出去,而是让目标进程回调原进程
8. 权限管理:运行时权限怎么申请才优雅?
核心回答
Android 6.0之前:安装时一次性授予,无法撤回
Android 6.0之后(API 23+) :危险权限必须运行时申请
权限分组:
makefile
Calendar: READ_CALENDAR, WRITE_CALENDAR
Camera: CAMERA
Contacts: READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS
Location: ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
Microphone: RECORD_AUDIO
Phone: READ_PHONE_STATE, READ_PHONE_NUMBERS, CALL_PHONE, READ_CALL_LOG...
SMS: SEND_SMS, RECEIVE_SMS, READ_SMS...
Storage: READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE(Android 10+Scoped Storage)
申请流程
kotlin
class PermissionActivity : AppCompatActivity() {
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
// resultMap: { "permission_name": true/false }
val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
val locationGranted = permissions[Manifest.permission.ACCESS_FINE_LOCATION] ?: false
when {
cameraGranted && locationGranted -> {
// 全部授权,执行操作
openCameraWithLocation()
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
// 用户拒绝过,需要解释
showRationaleDialog()
}
else -> {
// 用户拒绝且不展示 rationale,可能是永久拒绝
showSettingDialog()
}
}
}
fun requestPermissions() {
when {
// 检查权限
ContextCompat.checkSelfPermission(
this,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// 已授权
openCamera()
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
// 需要解释为什么必须用相机
AlertDialog.Builder(this)
.setMessage("没有相机权限无法扫码,请授权")
.setPositiveButton("去授权") { _, _ ->
permissionLauncher.launch(
arrayOf(Manifest.permission.CAMERA)
)
}
.show()
}
else -> {
// 第一次请求或永久拒绝
permissionLauncher.launch(
arrayOf(Manifest.permission.CAMERA)
)
}
}
}
}
Android 13+新权限
通知权限(Android 13+):
ini
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
scss
// Android 13+必须申请
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), 1)
}
}
媒体权限细化:
| 权限 | 范围 |
|---|---|
| READ_MEDIA_IMAGES | 图片 |
| READ_MEDIA_VIDEO | 视频 |
| READ_MEDIA_AUDIO | 音频 |
| READ_EXTERNAL_STORAGE(已废弃) | 全部 |
传感器权限:
| 权限 | 用途 |
|---|---|
| BODY_SENSORS | 身体传感器(心率等) |
| BODY_SENSORS_BACKGROUND | 后台访问传感器 |
| ACTIVITY_RECOGNITION | 计步器、运动检测 |
自定义权限
xml
<!-- Provider A(提供方)定义权限 -->
<permission
android:name="com.example.PROTECTED_DATA"
android:protectionLevel="signature" /> <!-- 只允许签名一致的App访问 -->
<!-- Provider B(使用方)声明权限 -->
<uses-permission android:name="com.example.PROTECTED_DATA" />
实战场景
扫码场景(典型多权限组合) :
scss
val permissions = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.FLASHLIGHT // 如果要闪光灯
)
permissionLauncher.launch(permissions)
拍照保存到相册:
scss
// Android 10+需要MANAGE_EXTERNAL_STORAGE或用MediaStore
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// 用MediaStore,不需要WRITE_EXTERNAL_STORAGE
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/AppName")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
contentResolver.openOutputStream(uri)?.use { os ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, os)
}
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(uri, contentValues, null, null)
}
面试加分点
- 权限不可继承:App更新不会重新请求权限,用户可以在设置里随时撤销
- 权限和UI线程:checkSelfPermission是轻量的,但requestPermissions会触发系统弹窗
- 权限兼容库PermissionDispatcher:简化权限申请逻辑,自动处理shouldShowRationale等逻辑
9. onSaveInstanceState和onRestoreInstanceState:什么时候触发?
核心回答
触发时机:
- Home键/切到后台:Activity被遮盖
- 屏幕旋转:配置变更
- 长按Home显示任务列表
- 系统杀死进程前
- 弹出对话框:Dialog不会,但BottomSheetDialog会触发
不触发时机:
- 按返回键:用户主动关闭,不需要保存
- Activity从未离开前台:一直是可见的
kotlin
class MyActivity : AppCompatActivity() {
// 保存数据
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("name", name)
outState.putInt("count", count)
// outState有大小限制(约1MB),不能存大对象
}
// 恢复数据(onCreate也可以恢复,但这个更规范)
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
name = savedInstanceState.getString("name")
count = savedInstanceState.getInt("count")
}
}
和ViewModel的关系
这是重点!
| 特性 | onSaveInstanceState | ViewModel |
|---|---|---|
| 配置变更保留 | ✅ | ✅ |
| 进程被杀保留 | ✅ | ❌ |
| 存储容量 | 小(~1MB Bundle) | 无限制(内存) |
| 适用场景 | 用户数据、UI状态 | 数据、业务逻辑 |
kotlin
kotlin
// ViewModel:配置变更自动保留,但进程被杀就丢了
class MyViewModel : ViewModel() {
var userData: User? = null // 配置变更不丢失
}
// SavedStateHandle:ViewModel + 进程级别保留
class MyViewModel2(private val savedStateHandle: SavedStateHandle) : ViewModel() {
private var name: String?
get() = savedStateHandle["name"]
set(value) = savedStateHandle.set("name", value)
// 进程被杀也能恢复
}
实战场景
表单页面 :
kotlin
// ❌ 低效:每次onCreate都从网络加载
class BadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
loadDataFromNetwork() // 每次都请求
}
}
// ✅ 正确:用ViewModel,只加载一次
class GoodActivity : AppCompatActivity() {
private val viewModel: FormViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 正常情况用ViewModel里的数据
// 只有savedInstanceState != null时,才从bundle恢复(比如进程被杀)
if (savedInstanceState != null) {
// 从bundle恢复
}
}
}
面试加分点
- onRestoreInstanceState的时机:在onStart之后,onPostCreate之前。适合做一些初始化完UI之后的恢复操作
- 配合ViewModel的最佳实践:ViewModel处理业务数据,Bundle只存轻量UI状态
- onBackPressed回调:按返回键时不会触发onSaveInstanceState,如果需要保存,考虑OnBackPressedDispatcher
10. 进程优先级:系统什么时候杀进程?
核心回答
Android将进程分为5个优先级(从高到低) :
| 优先级 | 说明 | 被杀可能性 |
|---|---|---|
| 前台进程(Foreground) | 正在交互的Activity、正在执行的BroadcastReceiver、正在运行的Service | 几乎不会 |
| 可见进程(Visible) | Activity可见但不是前台、Service被前台Activity绑定 | 极低 |
| 服务进程(Service) | 后台Service(startService启动) | 较低 |
| 后台进程(Background) | 不可见的Activity(stopped状态) | 较高 |
| 空进程(Empty) | 没有活跃组件,只有缓存 | 最后被杀 |
什么情况算前台进程?
- Activity正在和用户交互 :
onResume执行中 - BroadcastReceiver正在执行 :
onReceive执行中 - Service在执行生命周期方法 :
onCreate、onStart、onDestroy等
系统什么时候杀进程?
内存不足时,按优先级从低到高杀。
杀进程的是Low Memory Killer(Linux内核),它根据oom_adj值(进程优先级)来决定杀谁。值越大优先级越低。
ini
oom_adj:
0 = 前台进程
1 = 可见进程
2 = 服务进程
3 = 后台进程
4 = 服务进程
5 = 空进程
实战场景
容易被杀的情况:
kotlin
// ❌ 进程优先级低,容易被杀
class BadService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 启动后台线程处理任务
thread {
// 处理中...这时候Service已经返回START_STICKY
// 如果线程没跑完,进程可能被杀
}
return START_NOT_STICKY
}
}
// ✅ 提高进程优先级
class GoodService : Service() {
override fun onCreate() {
super.onCreate()
// 申请前台Service,提升优先级
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
}
}
保活方案的合理性分析
| 方案 | 合理性 | 评价 |
|---|---|---|
| 前台Service | ⭐⭐⭐⭐⭐ | 官方认可,必要时必须用 |
| JobScheduler | ⭐⭐⭐⭐ | 系统级优化,推荐 |
| WorkManager | ⭐⭐⭐⭐ | 封装了JobScheduler,API更友好 |
| startForeground | ⭐⭐⭐⭐⭐ | 唯一合法保活方式(除非要偷跑) |
| 双Service互相守护 | ⭐⭐⭐ | 可用,但容易被系统针对 |
| native保活 | ⭐ | 基本没用了 |
| 1像素Activity | ⭐ | 已被堵死 |
kotlin
// WorkManager - 推荐的后台任务方案
class MyWorker(appContext: Context, workerParams: WorkerParameters)
: CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// 这里处理后台任务
return Result.success()
}
}
// 调度
WorkManager.getInstance(context)
.enqueueOneTimeWorkRequest(
OneTimeWorkRequestBuilder<MyWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
)
面试加分点
- 绑定Service的优先级:绑定到前台Activity的Service优先级更高
- 进程优先级的叠加:一个进程可能有多个组件,优先级取最高的那个
- Low Memory Killer的配置 :在
/sys/module/lowmemorykiller/parameters/下,可以调整阈值(root设备) - 国产Rom的特殊性:华为、小米、OPPO等有自己的后台管理策略,比原生Android更激进,需要单独适配
总结
四大组件是Android的基石,面试时不仅要知道"是什么",更要知道"为什么"和"怎么用"。
几个关键点:
- Activity的异常生命周期处理是必考点,配合ViewModel使用是加分项
- Service选型很重要:需要双向通信用bind,只需要执行任务用start,Android 8.0后前台Service是趋势
- BroadcastReceiver现在基本用动态注册+本地广播了
- ContentProvider底层是Binder,跨进程通信绕不开
- Fragment 事务用
commitAllowingStateLoss(),生命周期要清楚嵌套关系 - PendingIntent是回调机制,FLAG_IMMUTABLE必须加
- 权限现在已经是运行时申请为主了,Android 13+还有新权限要处理
- 进程优先级决定了App的存活能力,但别滥用保活,官方方案最好用