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

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

实战场景

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

  1. 如果没处理横竖屏适配,视频会从头播放(体验差)
  2. 如果在onSaveInstanceState保存了播放进度,恢复时能从断点继续
  3. 如果用了ViewModel,数据自动保存,连进度都不用自己管
kotlin 复制代码
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加入购物车:

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

面试加分点

  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:

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

解决方案:

  1. 用WorkManager替代:后台任务用WorkManager,它会在合适的时机执行
  2. JobIntentService:如果必须用Service,可以用这个,8.0前用普通Service,8.0后自动转为JobService
  3. 申请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)

面试加分点

  1. Service和Thread的关系:Service是运行在主线程的,如果你要干活,还是得开Thread或者用协程
  2. onStartCommand返回值START_STICKY会被系统重启(清空intent),START_NOT_STICKY不会重启,START_REDELIVER_INTENT会重传intent
  3. 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_PICTUREACTION_PACKAGE_REPLACED等。

替代方案:

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

实战场景

监听网络变化:

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

面试加分点

  1. 有序广播(Ordered Broadcast) :可以设置优先级,优先级高的Receiver可以中断广播传播
  2. LocalBroadcastManager已废弃:Google推荐用LiveData或EventBus替代
  3. 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/...      |                     |
     |                           |                     |

简单说:

  1. Provider启动时,在AMS注册,返回一个Binder对象
  2. Client通过ContentResolver访问,Resolver内部通过Binder找到Provider
  3. 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"
)

面试加分点

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

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

面试加分点

  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

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

  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数据 通知更新、推送更新
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)

面试加分点

  1. FLAG_IMMUTABLE(Android 12+) :必须加!否则会崩溃。Android 12开始,出于安全考虑,PendingIntent必须是不可变的
  2. requestCode的重要性:必须唯一,否则不同的Intent会互相覆盖
  3. 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)
}

面试加分点

  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从未离开前台:一直是可见的
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恢复
        }
    }
}

面试加分点

  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值(进程优先级)来决定杀谁。值越大优先级越低。

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

面试加分点

  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的存活能力,但别滥用保活,官方方案最好用
相关推荐
测试开发-学习笔记16 小时前
adb命令
android·adb
HjhIron16 小时前
数组去重:从零开始,写一个靠谱的工具函数
算法·面试
私人珍藏库16 小时前
【Android】Todesk手机远控手机、电脑,无会员无广告!!
android·学习·智能手机·app·工具·软件·多功能
JohnnyDeng9416 小时前
Android 动画体系:属性动画与 Compose 动画对比
android
独隅17 小时前
MySQL主从延迟根因诊断法:全面详解指南
android·mysql·adb
私人珍藏库17 小时前
【Android】图片工具箱-免费开源图片处理软件
android·人工智能·app·工具·软件·多功能
晴天彩虹雨17 小时前
大厂 Flink 面试 100 题
大数据·面试·flink
以身入局17 小时前
android Binder 讲解
android
武当王丶也17 小时前
React Native Turbo Module 实战:从 0 封装一个 PDA 扫码模块
android·前端·react native