Android四大组件面试题,看完这篇就够了

6 阅读17分钟

Android四大组件面试题,看完这篇就够了

四大组件是Android的基础中的基础,但面试的时候很多人只停留在"会用"的层面。比如Service,很多人知道startService和bindService,但问到它们区别、什么时候用哪个、怎么保活,就卡壳了。

1. Activity生命周期:从打开到销毁,系统都干了啥?

核心回答

完整流程是这样的:

启动Activity: onCreate → onStart → onResume
Activity可见: onRestart → onStart → onResume
退出Activity: onPause → onStop → onDestroy

正常情况很简单,但面试官爱问的是异常情况

内存不足时:

系统会按优先级杀进程,Activity跟着遭殃。流程是 onPause → onStop → onSaveInstanceState,注意不一定会调onDestroy。等你再回来,系统会调 onCreate(savedInstanceState)onRestoreInstanceState

// 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) 重新创建。

实战场景

你做个视频播放页面,屏幕旋转了,这时候:

  1. 如果没处理横竖屏适配,视频会从头播放(体验差)
  2. 如果在onSaveInstanceState保存了播放进度,恢复时能从断点继续
  3. 如果用了ViewModel,数据自动保存,连进度都不用自己管
class VideoViewModel : ViewModel() {
    var currentPosition: Long = 0  // 配置变更后自动保留
}

面试加分点

  1. onStart和onResume的区别:onStart是Activity可见但不可交互,onResume是可见且可交互。比如弹 Dialog 时,Activity只是不可交互但仍可见,所以只调onPause不调onStop。
  2. onSaveInstanceState的调用时机:不一定是销毁前,系统觉得需要保存状态时就会调。
  3. 现代方案:Jetpack ViewModel + SavedStateHandle 处理配置变更,比手动存bundle优雅得多。

2. Activity启动模式:四种模式到底怎么选?

核心回答

先搞清楚两个概念:

  • 任务栈(Task Stack) :存放Activity的栈,默认一个应用一个任务栈
  • 返回栈(Back Stack) :用户按返回键时Activity的弹出顺序

四种模式:

模式特点典型场景
standard默认,每次启动都创建新实例普通页面
singleTop栈顶复用,如果目标Activity已在栈顶,不创建通知栏跳详情、搜索页
singleTask整个任务栈只有一个实例,清除上面的Activity首页、App入口
singleInstance独占一个任务栈,其他App可直接调用闹钟、电话、Launcher

实战场景

电商App加入购物车:

// 从商品详情加入购物车,用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)

微信聊天页跳转账页:

// 支付宝这种,需要singleTask确保只有一个支付任务栈
// 避免用户在其他地方付款后,App内支付页混乱
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or 
               Intent.FLAG_ACTIVITY_CLEAR_TASK

面试加分点

  1. Flags的坑:代码里用intent.flags和xml里用launchMode效果不完全一样,Flags优先级更高
  2. 任务亲和性(affinity) :可以自定义任务栈,配合taskAffinity属性
  3. 跨应用启动:用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:

// 前台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

解决方案:

  1. 用WorkManager替代:后台任务用WorkManager,它会在合适的时机执行
  2. JobIntentService:如果必须用Service,可以用这个,8.0前用普通Service,8.0后自动转为JobService
  3. 申请FOREGROUND_SERVICE权限:Manifest里声明android:foregroundServiceType

实战场景

音乐播放器:

// 必须用前台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)

面试加分点

  1. Service和Thread的关系:Service是运行在主线程的,如果你要干活,还是得开Thread或者用协程
  2. onStartCommand返回值START_STICKY会被系统重启(清空intent),START_NOT_STICKY不会重启,START_REDELIVER_INTENT会重传intent
  3. JobScheduler:比Service更省电,系统会批量执行和优化调度

4. BroadcastReceiver:静态注册和动态注册怎么选?

核心回答

静态注册(在Manifest里声明):

<!-- Manifest -->
<receiver android:name=".BootCompleteReceiver">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED" />
    </intent-filter>
</receiver>

特点:

  • App未运行时也能接收
  • 系统会创建Receiver实例并回调
  • Android 8.0+大量静态广播被禁用

动态注册:

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接收
  • 效率更高
// 发送本地广播
val intent = Intent("com.example.MY_ACTION")
LocalBroadcastManager.getInstance(this).sendBroadcast(intent)

全局广播:

  • 跨进程,可以被其他App接收
  • 需要考虑安全问题,避免被恶意App拦截

Android 8.0+隐式广播限制

大部分隐式广播不能用静态注册了。比如ACTION_NEW_PICTUREACTION_PACKAGE_REPLACED等。

替代方案:

  1. 用动态注册:在需要的时候注册,生命周期结束解注册
  2. 用JobScheduler/WorkManager轮询:比如想知道App更新,可以用版本检查
  3. 保留的白名单广播:系统会维护一个白名单,可以查官方文档

实战场景

监听网络变化:

// 正确做法:动态注册
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)
    }
}

面试加分点

  1. 有序广播(Ordered Broadcast) :可以设置优先级,优先级高的Receiver可以中断广播传播
  2. LocalBroadcastManager已废弃:Google推荐用LiveData或EventBus替代
  3. Receiver的耗时限制:onReceive里不能做耗时操作(10秒),否则ANR

5. ContentProvider:跨进程数据共享的原理是什么?

核心回答

ContentProvider是Android的进程间通信(IPC) 机制之一,底层用的是Binder

流程是这样的:

App A(Provider)          Binder驱动           App B(Client)
     |                           |                     |
     | ContentProvider.onCreate()|                     |
     |<---- publish --------->  |                     |
     |                           |                     |
     |                           |<---- getContentResolver()
     |                           |                     |
     |<--------- IPC ----------->|                     |
     |     query/update/...      |                     |
     |                           |                     |

简单说:

  1. Provider启动时,在AMS注册,返回一个Binder对象
  2. Client通过ContentResolver访问,Resolver内部通过Binder找到Provider
  3. Provider在主线程执行数据库/文件操作(所以别在主线程干重活)

生命周期和启动时机

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:

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访问:

// ContentResolver自动处理Binder通信
val cursor = contentResolver.query(
    ImageProvider.CONTENT_URI,
    arrayOf("_id", "path", "size"),
    "size > ?",
    arrayOf("1024"),
    "size DESC"
)

面试加分点

  1. ContentUris工具类withAppendedId()添加ID,parseId()解析ID
  2. uriMatcher:用UriMatcher匹配不同的uri来做路由
  3. 权限控制android:grantUriPermissions="true"配合Intent.FLAG_GRANT_READ_URI_PERMISSION
  4. 和Binder的关系:ContentProvider底层是Binder,但封装了更高级的接口,比AIDL更方便

6. Fragment:生命周期怎么配合Activity?事务怎么提交才不出bug?

核心回答

Fragment和Activity的关系:

Fragment生命周期嵌套在Activity里:

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新的确实要销毁重建
// 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()!

// ❌ 可能会崩溃
transaction.commit()

// ✅ 安全版
transaction.commitAllowingStateLoss()

为什么?

当你按了Home键,Activity正在onSaveInstanceState保存状态,这时候commit()会抛异常:IllegalStateException: Can not perform this action after onSaveInstanceState

commitAllowingStateLoss()允许在状态保存后提交,不会崩溃(可能丢失最后一次操作)。

// Fragment的坑:如果View已经销毁了,不能commit
if (isAdded) {  // 先检查Fragment是否attached
    supportFragmentManager.beginTransaction()
        .replace(R.id.container, newFragment)
        .commitAllowingStateLoss()
}

实战场景

ViewPager + Fragment配合:

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):

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()
}

面试加分点

  1. Fragment的坑:Fragment的onResumeonPause是基于用户可见性的,不是Activity那种精确区分
  2. FragmentFactory:AndroidX 1.2.0+引入,解决no empty constructor的经典问题
  3. FragmentResult API:Fragment之间通信的新方式,比setArguments/onActivityCreated更清晰

7. Intent和PendingIntent:PendingIntent为什么能跨进程?

核心回答

Intent:消息对象,告诉系统"要做什么"

PendingIntent:Intent的包装器,让别人在未来的某个时刻替我执行Intent

// 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时,系统会:

  1. 分配一个Binder token(唯一标识这个PendingIntent)
  2. 把你的Intent包装起来
  3. 把PendingIntent保存到系统服务(NotificationManagerService等)
  4. 返回给你一个引用

当其他进程使用这个PendingIntent时:

  1. 系统通过Binder token找到原始Intent
  2. 原App的进程里执行Intent
  3. 结果返回给调用方

所以PendingIntent不是"把你的Intent发到其他进程",而是"让其他进程能回调到你的进程"。

PendingIntent Flags怎么选?

Flag效果适用场景
FLAG_ONE_SHOT只用一次,用完就删验证码通知、一次性操作
FLAG_NO_CREATE不存在就返回null,不创建判断PendingIntent是否已存在
FLAG_CANCEL_CURRENT存在就删除旧创建新的确保拿到最新数据
FLAG_UPDATE_CURRENT存在就更新extra数据通知更新、推送更新
// 场景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
)

实战场景

通知系统:

// 创建 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)

面试加分点

  1. FLAG_IMMUTABLE(Android 12+) :必须加!否则会崩溃。Android 12开始,出于安全考虑,PendingIntent必须是不可变的
  2. requestCode的重要性:必须唯一,否则不同的Intent会互相覆盖
  3. PendingIntent的"回拨"机制: 不是把Intent发出去,而是让目标进程回调原进程

8. 权限管理:运行时权限怎么申请才优雅?

核心回答

Android 6.0之前:安装时一次性授予,无法撤回

Android 6.0之后(API 23+) :危险权限必须运行时申请

权限分组

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)

申请流程

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+):

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
// 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计步器、运动检测

自定义权限

<!-- Provider A(提供方)定义权限 -->
<permission 
    android:name="com.example.PROTECTED_DATA"
    android:protectionLevel="signature" />  <!-- 只允许签名一致的App访问 -->

<!-- Provider B(使用方)声明权限 -->
<uses-permission android:name="com.example.PROTECTED_DATA" />

实战场景

扫码场景(典型多权限组合)

val permissions = arrayOf(
    Manifest.permission.CAMERA,
    Manifest.permission.FLASHLIGHT  // 如果要闪光灯
)

permissionLauncher.launch(permissions)

拍照保存到相册

// 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)
}

面试加分点

  1. 权限不可继承:App更新不会重新请求权限,用户可以在设置里随时撤销
  2. 权限和UI线程:checkSelfPermission是轻量的,但requestPermissions会触发系统弹窗
  3. 权限兼容库PermissionDispatcher:简化权限申请逻辑,自动处理shouldShowRationale等逻辑

9. onSaveInstanceState和onRestoreInstanceState:什么时候触发?

核心回答

触发时机

  1. Home键/切到后台:Activity被遮盖
  2. 屏幕旋转:配置变更
  3. 长按Home显示任务列表
  4. 系统杀死进程前
  5. 弹出对话框:Dialog不会,但BottomSheetDialog会触发

不触发时机

  1. 按返回键:用户主动关闭,不需要保存
  2. Activity从未离开前台:一直是可见的
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的关系

这是重点!

特性onSaveInstanceStateViewModel
配置变更保留
进程被杀保留
存储容量小(~1MB Bundle)无限制(内存)
适用场景用户数据、UI状态数据、业务逻辑

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)
    
    // 进程被杀也能恢复
}

实战场景

表单页面

// ❌ 低效:每次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恢复
        }
    }
}

面试加分点

  1. onRestoreInstanceState的时机:在onStart之后,onPostCreate之前。适合做一些初始化完UI之后的恢复操作
  2. 配合ViewModel的最佳实践:ViewModel处理业务数据,Bundle只存轻量UI状态
  3. onBackPressed回调:按返回键时不会触发onSaveInstanceState,如果需要保存,考虑OnBackPressedDispatcher

10. 进程优先级:系统什么时候杀进程?

核心回答

Android将进程分为5个优先级(从高到低)

优先级说明被杀可能性
前台进程(Foreground)正在交互的Activity、正在执行的BroadcastReceiver、正在运行的Service几乎不会
可见进程(Visible)Activity可见但不是前台、Service被前台Activity绑定极低
服务进程(Service)后台Service(startService启动)较低
后台进程(Background)不可见的Activity(stopped状态)较高
空进程(Empty)没有活跃组件,只有缓存最后被杀

什么情况算前台进程?

  1. Activity正在和用户交互onResume执行中
  2. BroadcastReceiver正在执行onReceive执行中
  3. Service在执行生命周期方法onCreateonStartonDestroy

系统什么时候杀进程?

内存不足时,按优先级从低到高杀。

杀进程的是Low Memory Killer(Linux内核),它根据oom_adj值(进程优先级)来决定杀谁。值越大优先级越低。

oom_adj:
0 = 前台进程
1 = 可见进程  
2 = 服务进程
3 = 后台进程
4 = 服务进程
5 = 空进程

实战场景

容易被杀的情况

// ❌ 进程优先级低,容易被杀
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已被堵死
// 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()
    )

面试加分点

  1. 绑定Service的优先级:绑定到前台Activity的Service优先级更高
  2. 进程优先级的叠加:一个进程可能有多个组件,优先级取最高的那个
  3. Low Memory Killer的配置:在/sys/module/lowmemorykiller/parameters/下,可以调整阈值(root设备)
  4. 国产Rom的特殊性:华为、小米、OPPO等有自己的后台管理策略,比原生Android更激进,需要单独适配

总结

四大组件是Android的基石,面试时不仅要知道"是什么",更要知道"为什么"和"怎么用"。

几个关键点:

  1. Activity的异常生命周期处理是必考点,配合ViewModel使用是加分项
  2. Service选型很重要:需要双向通信用bind,只需要执行任务用start,Android 8.0后前台Service是趋势
  3. BroadcastReceiver现在基本用动态注册+本地广播了
  4. ContentProvider底层是Binder,跨进程通信绕不开
  5. Fragment事务用commitAllowingStateLoss(),生命周期要清楚嵌套关系
  6. PendingIntent是回调机制,FLAG_IMMUTABLE必须加
  7. 权限现在已经是运行时申请为主了,Android 13+还有新权限要处理
  8. 进程优先级决定了App的存活能力,但别滥用保活,官方方案最好用