零:前言
在Android的世界里,四大组件(Activity、Service、BroadcastReceiver、ContentProvider) 如同搭建应用骨架的「四根支柱」,它们的协作定义了应用的基础行为模式。但许多开发者(甚至有一定经验的人)对它们的理解往往停留在表面:
- 你以为懂了Activity:背熟了生命周期图,却在屏幕旋转时丢数据;
- 你以为懂了Service :知道
startService
和bindService
的区别,却说不清「前台服务」与「JobScheduler」的取舍; - 你以为懂了BroadcastReceiver:用过动态注册,却对「有序广播」和「粘性广播」的坑视而不见;
- 你以为懂了ContentProvider :用它跨进程共享数据,却从未深究过
Binder
的底层通信机制...
接下来本系列将以「高频面试题」为线索,重新解构四大组件。作为本系列的第一篇博客将会先搞定四大组件之首 - Activity
壹、Activity
问题1:Activity的生命周期有哪些?请描述从启动到销毁的完整流程。
(一)正常生命周期流程
-
启动阶段:
onCreate()
:首次创建时调用,用于初始化视图和数据。onStart()
:Activity可见但未进入前台。onResume()
:Activity进入前台,可接收用户输入。
-
暂停与停止:
onPause()
:Activity失去焦点(如弹窗覆盖),需释放占用资源(如相机)。onStop()
:Activity完全不可见(如跳转到其他Activity)。
-
重新回到前台:
onRestart()
:Activity从停止状态重新启动。onStart()
→onResume()
:再次可见并进入前台。
-
销毁阶段:
onDestroy()
:Activity被销毁(用户主动退出或系统回收)。
完整流程图:
scss
onCreate() → onStart() → onResume() → [运行中] → onPause() → onStop() → onDestroy()
(二)异常生命周期流程
- 配置变更(如屏幕旋转) Activity会被销毁并重建,触发
onSaveInstanceState()
保存临时数据,重建后通过onRestoreInstanceState()
恢复。 - 内存不足回收 后台Activity可能被系统回收,需通过
onSaveInstanceState()
保存关键数据。
关键方法:
onSaveInstanceState(Bundle outState)
:保存临时数据(如文本框内容)onRestoreInstanceState(Bundle savedInstanceState)
:恢复数据
(三)常见问题
-
onResume()
与onPause()
的注意事项onResume()
:适合启动动画、传感器监听等需要实时交互的操作onPause()
:必须轻量化(耗时操作会阻塞下一个Activity启动)
-
避免在
onCreate()
中执行耗时任务 若初始化耗时(如加载数据库),应使用异步任务或ViewModel
延迟加载。 -
onDestroy()
的不确定性 不能依赖onDestroy()
释放资源(系统可能直接终止进程),应在onStop()
中释放。
问题2: Activity和Fragment的生命周期有哪些关键区别?
- onAttach(Context context): 当 Fragment 与 Activity 关联时调用(此时可通过
getActivity()
获取宿主 Activity)。通常用于获取 Activity 传递的依赖(如接口回调)。 - onCreateView(): 创建 Fragment 的视图层次结构时调用,通过
LayoutInflater
解析布局文件 - onViewCreated(): 在
onCreateView()
返回的视图创建完成后调用,适合初始化视图组件(如findViewById
)、设置 RecyclerView 适配器等 - onActivityCreated() (已废弃):在 AndroidX 中,此方法已被废弃,推荐在
onViewCreated()
中结合ViewLifecycleOwner
监听生命周期。 - onDestroyView() :当 Fragment 的视图被销毁时调用(如 Fragment 被移除或替换)。用途:清理与视图相关的资源(如取消异步任务、解除观察者绑定)。
- onDetach() :当 Fragment 与 Activity 解除关联时调用。释放对宿主 Activity 的引用,避免内存泄漏。
问题3:在什么场景下你会选择使用Fragment而不是直接使用多个Activity?
Fragment相较于Activity来说是轻量化组建,Fragment是可以依附在Activity的生命周期
(1)何时选择 Fragment?
- 界面复用的时候:在不同 Activity 中复用同一界面
- 底部导航栏和侧边抽屉的这种导航组件时,这种场景需要频繁切换,可以使用Fragment来实现这种轻量化的切换。
- Fragment 的生命周期与宿主 Activity 绑定,适合管理局部 UI 逻辑
(2)何时选择 Activity?
- 需要完全独立的界面,不同模块功能的入口
- 需要处理全局配置(如系统级别的 Intent 过滤)
Fragment 的核心优势:
- 轻量高效:适合高频次、局部 UI 更新(如 Navigation 切换、ViewPager2 滑动)。
- 模块化与复用性:便于组合复杂界面,支持跨 Activity 复用。
- 生命周期可控性:与 Jetpack 组件深度集成,简化状态管理。
- 现代化架构支持:单 Activity + 多 Fragment 是 Google 推荐的最佳实践。
问题4:如果使用 Navigation 时需要在 Fragment 之间传递大量数据(如 Bitmap),如何优化性能?
传递Bitmap这类大数据的问题在于,如果直接通过Bundle传递,会导致序列化和反序列化的开销,可能引发TransactionTooLargeException异常。所以应该避免直接传递Bitmap对象。
- 序列化开销:Bitmap 需要序列化为字节流,消耗 CPU 和内存。
- TransactionTooLargeException:Android 的 Binder 事务缓冲区限制为 1MB,传递大对象易崩溃。
- 内存泄漏风险:未释放的 Bitmap 可能长期占用内存。
(1)首先想到的是ViewModel,因为ViewModel可以在Fragment之间共享数据,避免直接在导航参数中传递。例如,使用activity范围的ViewModel,这样多个Fragment可以访问同一个ViewModel实例,从而共享Bitmap数据。
(2)使用内存缓存,比如LruCache,这样可以临时存储Bitmap,避免重复加载。同时,需要处理缓存的生命周期,确保不会引起内存泄漏。
(3)使用单例或依赖注入框架(如Hilt)来管理共享的数据,这样数据独立于组件的生命周期,但需要注意内存管理和线程安全。
(4)图片加载库如Glide或Picasso可以处理Bitmap的加载和缓存,避免手动管理
注意:考虑内存泄漏的问题,比如在ViewModel中持有Bitmap的引用,当不再需要时应及时释放
问题5:请解释 Activity 的四种启动模式(Launch Mode)及其使用场景。如果在实际开发中需要实现"点击通知栏消息后跳转到某个特定 Activity,并确保该 Activity 的实例唯一且不重复创建",你会如何设计?
(1)Activity启动模式详解
启动模式 | 行为特性 | 生命周期回调 | 典型应用场景 |
---|---|---|---|
standard | 默认模式,每次启动创建新实例,允许多个实例共存于同一任务栈 | 完整生命周期(onCreate) | 常规页面(如新闻列表页、普通表单页) |
singleTop | 仅在目标位于栈顶时复用实例(否则新建),复用时会调用onNewIntent | onNewIntent | 防重复点击场景(如支付按钮跳转结果页、推送通知快速点击) |
singleTask | 在任务栈中查找匹配实例(根据taskAffinity),若存在则清空其上方所有实例并复用 | onNewIntent | 应用主页(要求全局唯一)、深层链接入口(如从浏览器跳转回App主流程) |
singleInstance | 独占一个新任务栈,且该栈中只能存在该Activity | onNewIntent | 独立功能模块(如系统相机、第三方登录页) |
-
taskAffinity
的作用- 默认与包名一致,可通过
android:taskAffinity
自定义。 singleTask
会优先匹配相同taskAffinity
的任务栈,若无匹配栈则新建。- 示例:浏览器应用通过不同
taskAffinity
管理多个网页栈。
- 默认与包名一致,可通过
-
singleInstance
的特殊性- 完全独占任务栈,其他Activity无法进入该栈。
- 从
singleInstance
启动其他Activity时,会跳转到其他任务栈(可能触发系统任务管理器的多窗口效果)
-
allowTaskReparenting
的联动- 当
allowTaskReparenting="true"
时,Activity可根据taskAffinity
动态迁移到前台任务栈(常见于跨应用跳转后返回原应用)
- 当
(2)通知栏跳转场景的优化设计
需求分析
-
核心目标:无论用户从通知栏点击多少次,目标Activity(如消息详情页)始终保持唯一实例,且不重复创建。
-
潜在问题:
- 用户可能从不同入口(如主页、其他页面)触发通知跳转。
- 需要处理返回栈逻辑,避免回退时出现空白页面。
实现方案
launchMode="singleTask"
:确保全局唯一性,复用实例时清除上方页面。taskAffinity
:隔离消息相关Activity到独立任务栈,避免影响主流程。FLAG_ACTIVITY_NEW_TASK
:强制在新任务栈中启动(配合taskAffinity
)。FLAG_ACTIVITY_CLEAR_TOP
:若目标Activity已存在,清除其上方所有实例。
ini
<activity
android:name=".MessageDetailActivity"
android:launchMode="singleTask"
android:taskAffinity=".MessageTask" />
val intent = Intent(context, MessageDetailActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
putExtra("message_id", messageId)}
val pendingIntent = PendingIntent.getActivity(
context,
requestCode,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
在目标Activity中处理数据更新
必须重写 onNewIntent
:处理复用实例时的数据更新,避免UI状态陈旧。
kotlin
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
val messageId = intent?.getStringExtra("message_id")
// 根据新的messageId刷新UI
loadMessageDetails(messageId)
}
问题6:taskAffinity
有什么作用,可以在哪些场景用到它?
- 定义
taskAffinity
是 Activity 的一个属性(可通过AndroidManifest.xml
或代码设置),用于指定该 Activity "倾向于"归属于哪个任务栈。- 默认情况下,所有 Activity 的
taskAffinity
继承自应用的包名(<manifest package>
)。例如,包名为com.example.app
,则默认taskAffinity="com.example.app"
。
- 核心规则
- 任务栈匹配优先级 :当启动一个 Activity 时,系统会优先寻找与其
taskAffinity
相同的任务栈。 - 与启动模式联动 :
singleTask
和singleInstance
启动模式的行为高度依赖taskAffinity
。
taskAffinity
的实践案例
案例1:浏览器应用的多标签页管理
- 需求:每个浏览器标签页独立运行,且在最近任务列表中显示为独立卡片。
- 实现:taskAffinity + singleTask
ini
<!-- 每个 WebActivity 设置不同的 taskAffinity -->
<activity
android:name=".WebActivity"
android:taskAffinity=".WebTask_${uniqueId}" <!-- 动态生成唯一ID -->
android:launchMode="singleTask" />
// 启动新标签页时指定唯一 taskAffinity
val intent = Intent(this, WebActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra("task_affinity", "com.example.app.WebTask_${System.currentTimeMillis()}")
}
startActivity(intent)
-
效果:
-
每个 WebActivity 在独立任务栈中运行。
-
用户通过最近任务列表可快速切换不同标签页。
-
案例2:跨应用跳转后返回原任务栈
- 需求:从 App A 跳转到 App B 的支付页,支付完成后返回 App A 的订单详情页,而非 App B 的主页。
- 实现 :taskAffinity + allowTaskReparenting
allowTaskReparenting="true"
:当某个任务栈进入前台时,Activity 会从后台任务栈迁移到与其 taskAffinity
匹配的前台栈。
xml
<!-- App A 的订单详情页 -->
<activity
android:name=".OrderDetailActivity"
android:taskAffinity=".OrderTask"
android:allowTaskReparenting="true" /
kotlin
// App B 的支付页完成支付后
val intent = Intent(this, OrderDetailActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(intent)
效果:
allowTaskReparenting="true"
使得OrderDetailActivity
会从 App B 的任务栈迁移到 App A 的.OrderTask
栈。- 用户返回时直接回到 App A 的订单详情页
问题7:谈谈Activity的onSaveInstanceState()和onRestoreInstanceState()的作用及调用时机
1. onSaveInstanceState(Bundle outState)
用于保存Activity的临时状态数据(如用户输入、滚动位置),以便在Activity被异常销毁后重建时恢复这些数据。
触发条件:
- 系统主动回收:当Activity内存不足或者配置发生变更(比如旋转屏幕、语言切换)被销毁时。
- 在
onStop()
之前调用,但不一定在onPause()
之后。
scss
onPause() → onSaveInstanceState() → onStop() → onDestroy()。
2. onRestoreInstanceState(Bundle savedInstanceState)
:
用于从Bundle
中恢复之前保存的临时数据,通常在Activity被系统重建后调用。
触发条件:
Activity被系统重建:例如屏幕旋转后恢复。
调用时机 :在onStart()
之后、onResume()
之前调用。
scss
onCreate() → onStart() → onRestoreInstanceState() → onResume()
3.与 onCreate()
的数据恢复对比
方法 | Bundle参数是否可能为null | 适用场景 |
---|---|---|
onCreate(Bundle) | 是(首次创建时为null) | 通用初始化,需检查Bundle是否为null。 |
onRestoreInstanceState() | 否(Bundle一定非null) | 专用恢复数据,无需判空,代码更简洁。 |
问题8:如何通过Intent在Activity之间传递数据?Bundle的大小限制是多少?
1.通过 Intent
的 putExtra()
方法直接传递基本类型数据
kotlin
// 在源Activity中
val intent = Intent(this, TargetActivity::class.java)
intent.putExtra("key_string", "Hello, World!")
intent.putExtra("key_int", 123)
intent.putExtra("key_boolean", true)
startActivity(intent)
// 在目标Activity的onCreate()中
val stringValue = intent.getStringExtra("key_string")
val intValue = intent.getIntExtra("key_int", 0) // 第二个参数为默认值
val booleanValue = intent.getBooleanExtra("key_boolean", false)
2.复杂对象需实现 Parcelable
或 Serializable
接口以支持序列化。
kotlin
// 定义数据类
data class User(val name: String, val age: Int) : Parcelable
// 发送数据
val user = User("Alice", 30)
intent.putExtra("key_user", user)
// 接收数据
val receivedUser = intent.getParcelableExtra<User>("key_user")
3.Bundle的大小限制
- Binder事务缓冲区限制 :Android通过Binder机制传输数据,其事务缓冲区大小约为1MB(不同版本可能略有差异)。
- 实际可用空间:由于系统占用部分缓冲区,实际可传递数据应远小于1MB
- 超过限制:会 抛出
TransactionTooLargeException
异常,导致应用崩溃。 - 避免传递大数据:如图片、长文本等,可以改用以下方式:
(1)全局单例或ViewModel:在内存中共享数据。
(2)持久化存储:将数据保存到数据库或文件,传递标识符(如文件路径)。
(3)ContentProvider:跨进程大数据共享。
(4)EventBus或LiveData:组件间通信。
4.最佳实践
(1)传递最小必要数据: 传递用户ID而非整个用户对象,目标Activity再通过ID查询数据。
(2)使用ViewModel共享数据
csharp
// 在源Activity中
val viewModel: SharedViewModel by viewModels()
viewModel.setUser(user)
// 在目标Activity中
val viewModel: SharedViewModel by viewModels()
val user = viewModel.getUser()
(3)处理配置变更: 使用onSaveInstanceState()
保存临时数据,结合ViewModel
保留复杂状态。
问题9:如何实现Activity与Fragment之间的通信?
- 定义通信接口
- 实现方法: 在Fragment中定义通信接口,由Activity实现该接口
- 优点:解耦,Fragment不依赖具体Activity。
- 缺点:需手动管理接口绑定与解绑。
- ViewModel共享数据(推荐用于数据驱动场景)
- 实现方法: 创建共享ViewModel,Activity与Fragment中访问ViewModel
- 优点:生命周期安全,数据持久化,支持多Fragment共享。
- 缺点:需引入AndroidX依赖。
- 直接引用Activity(简单但高耦合)
- 实现方法: 在Fragment中获取Activity引用
- 优点:实现简单。
- 缺点:高耦合,Fragment无法复用。
- EventBus(第三方库,慎用)
- 实现方法: 添加依赖, Fragment发送事件,Activity订阅事件
- 优点:跨组件通信灵活。
- 缺点:需引入第三方库,易导致内存泄漏和代码混乱。
- Fragment Result API(AndroidX推荐方式)
- 优点:官方推荐,生命周期安全。
- 缺点:仅支持单向通信,适合简单场景。
- 最佳实践
- 简单数据传递 :优先使用 Fragment Result API。
- 数据共享与状态管理:选择 ViewModel
问题10:如何避免Activity的内存泄漏
常见内存泄漏场景与解决方案
(1)静态变量持有Activity引用
场景:单例类或静态变量直接或间接持有Activity的Context
csharp
object Singleton {var context: Context? = null // 错误:可能持有Activity的引用}
解决方案:
- 使用
Application Context
代替Activity Context
。 - 若必须使用Activity引用,使用弱引用(
WeakReference
):
kotlin
class Singleton {
private var activityRef: WeakReference<Activity>? = null
fun setActivity(activity: Activity) {
activityRef = WeakReference(activity)
}
}
(2)非静态内部类或匿名内部类
场景:Handler、Runnable等内部类隐式持有Activity引用。
kotlin
class MyActivity : Activity() {
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
// 隐式持有外部Activity引用
}
}
}
解决方案:静态内部类 + 弱引用
kotlin
private class SafeHandler(activity: MyActivity) : Handler(Looper.getMainLooper()) {
private val activityRef = WeakReference(activity)
override fun handleMessage(msg: Message) {
val activity = activityRef.get() ?: return
// 处理消息
}
}
在 onDestroy()
中移除回调:
kotlin
override fun onDestroy() {
handler.removeCallbacksAndMessages(null)
super.onDestroy()
}
(3) 未正确注销监听器或回调
场景:注册广播、事件总线、监听器后未及时注销
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
LocalBroadcastManager.getInstance(this).registerReceiver(receiver, intentFilter)
}
解决方案: 在onDestroy()
中反注册:
kotlin
override fun onDestroy() {
LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
super.onDestroy()
}
(4)异步任务未取消
场景:AsyncTask、RxJava、Coroutine等异步任务未随Activity销毁终止。
kotlin
private var job: Job? = null
fun startTask() {
job = CoroutineScope(Dispatchers.IO).launch {
// 耗时操作
withContext(Dispatchers.Main) {
updateUI() // 若Activity已销毁,此处可能崩溃
}
}
}
解决方案: 使用生命周期感知的协程(lifecycleScope
)
kotlin
lifecycleScope.launch {
// 自动在onDestroy时取消
}
// 取消任务
override fun onDestroy() {
job?.cancel()
super.onDestroy()
}
(5) 资源未释放
场景:文件流、数据库连接、传感器服务等未关闭。
kotlin
private lateinit var sensorManager: SensorManager
private lateinit var sensorListener: SensorEventListener
override fun onCreate(savedInstanceState: Bundle?) {
sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
sensorManager.registerListener(sensorListener, ...)
}
解决方法 :在onDestroy()
中释放资源:
kotlin
override fun onDestroy() {
sensorManager.unregisterListener(sensorListener)
super.onDestroy()
}
避免内存泄漏的黄金法则
- 及时释放资源 :在
onDestroy()
中取消任务、反注册监听器、关闭资源。 - 避免强引用:优先使用弱引用或生命周期感知的组件。
- 最小化Context使用 :尽量使用
Application Context
代替Activity Context
。 - 工具辅助:定期使用LeakCanary和Profiler检测潜在问题。
问题11: 解释Activity的任务栈(Task)和返回栈(Back Stack)管理机制。
-
任务栈(Task)
-
定义:一组Activity的集合,按启动顺序排列,形成一个任务栈。
-
特点:
- 每个任务栈有一个唯一的ID(Task ID)。
- 任务栈可以跨应用(如从应用A跳转到应用B的某个Activity)。
- 任务栈中的Activity可以来自不同应用(如浏览器打开多个标签页)。
-
-
返回栈(Back Stack)
-
定义:用户通过返回键(Back)导航的Activity栈,是任务栈的子集。
-
特点:
- 返回栈遵循"后进先出"(LIFO)原则。
- 用户按返回键时,当前Activity出栈,前一个Activity恢复显示。
-
-
任务栈与返回栈的关系
- 任务栈是物理概念,返回栈是逻辑概念。
- 返回栈是任务栈中用户可见的部分。
- 一个应用可以有多个任务栈(如通过
FLAG_ACTIVITY_NEW_TASK
启动新栈)。 - 每个任务栈有独立的返回栈。
- 任务栈的管理机制
(1)创建新任务栈 : 通过Intent.FLAG_ACTIVITY_NEW_TASK
启动Activity时,若目标Activity的taskAffinity
与当前任务栈不同,则创建新任务栈。
ini
val intent = Intent(this, TargetActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
- 切换任务栈 : 通过
ActivityManager
的moveTaskToFront()
方法将任务栈切换到前台。
(2)任务栈的销毁
- 用户主动退出:按返回键直到栈中所有Activity出栈。
- 系统回收:内存不足时,后台任务栈可能被系统销毁。
(3)返回栈的入栈与出栈
- 入栈:启动新Activity时,当前Activity入栈。
- 出栈:按返回键时,当前Activity出栈,前一个Activity恢复。
(4)返回栈的调整
FLAG_ACTIVITY_CLEAR_TOP
:若目标Activity已在栈中,则清除其上方所有Activity并复用。FLAG_ACTIVITY_REORDER_TO_FRONT
:若目标Activity已在栈中,则将其移到栈顶
ini
val intent = Intent(this, TargetActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 或者 intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
startActivity(intent)