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_PICTURE、ACTION_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的onResume和onPause是基于用户可见性的,不是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在执行生命周期方法 :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()
    )

面试加分点

  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的存活能力,但别滥用保活,官方方案最好用
相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
晨米酱5 天前
AGENTS.md:Agent 的上下文策略层
面试·架构·agent
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
彧azz5 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
ttyyttemo5 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077005 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
ai2work5 天前
ch23 综合复刻:从零做一个最小可用版本(capstone)
kotlin
其实防守也摸鱼5 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透