Android重学笔记|Activity必问十连击

360 阅读15分钟

零:前言

在Android的世界里,四大组件(Activity、Service、BroadcastReceiver、ContentProvider)  如同搭建应用骨架的「四根支柱」,它们的协作定义了应用的基础行为模式。但许多开发者(甚至有一定经验的人)对它们的理解往往停留在表面:

  • 你以为懂了Activity:背熟了生命周期图,却在屏幕旋转时丢数据;
  • 你以为懂了Service:知道startServicebindService的区别,却说不清「前台服务」与「JobScheduler」的取舍;
  • 你以为懂了BroadcastReceiver:用过动态注册,却对「有序广播」和「粘性广播」的坑视而不见;
  • 你以为懂了ContentProvider:用它跨进程共享数据,却从未深究过Binder的底层通信机制...

接下来本系列将以「高频面试题」为线索,重新解构四大组件。作为本系列的第一篇博客将会先搞定四大组件之首 - Activity

壹、Activity

问题1:Activity的生命周期有哪些?请描述从启动到销毁的完整流程。

(一)正常生命周期流程

  1. 启动阶段

    1. onCreate():首次创建时调用,用于初始化视图和数据。
    2. onStart():Activity可见但未进入前台。
    3. onResume():Activity进入前台,可接收用户输入。
  2. 暂停与停止

    1. onPause():Activity失去焦点(如弹窗覆盖),需释放占用资源(如相机)。
    2. onStop():Activity完全不可见(如跳转到其他Activity)。
  3. 重新回到前台

    1. onRestart():Activity从停止状态重新启动。
    2. onStart() → onResume():再次可见并进入前台。
  4. 销毁阶段

    1. onDestroy():Activity被销毁(用户主动退出或系统回收)。

完整流程图

onCreate() → onStart() → onResume() → [运行中]onPause() → onStop() → onDestroy()

(二)异常生命周期流程

  1. 配置变更(如屏幕旋转) Activity会被销毁并重建,触发onSaveInstanceState()保存临时数据,重建后通过onRestoreInstanceState()恢复。
  2. 内存不足回收 后台Activity可能被系统回收,需通过onSaveInstanceState()保存关键数据。

关键方法

  • onSaveInstanceState(Bundle outState):保存临时数据(如文本框内容)
  • onRestoreInstanceState(Bundle savedInstanceState):恢复数据

(三)常见问题

  1. onResume()onPause()的注意事项

    1. onResume():适合启动动画、传感器监听等需要实时交互的操作
    2. onPause():必须轻量化(耗时操作会阻塞下一个Activity启动)
  2. 避免在onCreate()中执行耗时任务 若初始化耗时(如加载数据库),应使用异步任务或ViewModel延迟加载。

  3. 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 的核心优势:

  1. 轻量高效:适合高频次、局部 UI 更新(如 Navigation 切换、ViewPager2 滑动)。
  2. 模块化与复用性:便于组合复杂界面,支持跨 Activity 复用。
  3. 生命周期可控性:与 Jetpack 组件深度集成,简化状态管理。
  4. 现代化架构支持:单 Activity + 多 Fragment 是 Google 推荐的最佳实践。

问题4:如果使用 Navigation 时需要在 Fragment 之间传递大量数据(如 Bitmap),如何优化性能?

传递Bitmap这类大数据的问题在于,如果直接通过Bundle传递,会导致序列化和反序列化的开销,可能引发TransactionTooLargeException异常。所以应该避免直接传递Bitmap对象。

  1. 序列化开销:Bitmap 需要序列化为字节流,消耗 CPU 和内存。
  2. TransactionTooLargeException:Android 的 Binder 事务缓冲区限制为 1MB,传递大对象易崩溃。
  3. 内存泄漏风险:未释放的 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仅在目标位于栈顶时复用实例(否则新建),复用时会调用onNewIntentonNewIntent防重复点击场景(如支付按钮跳转结果页、推送通知快速点击)
singleTask在任务栈中查找匹配实例(根据taskAffinity),若存在则清空其上方所有实例并复用onNewIntent应用主页(要求全局唯一)、深层链接入口(如从浏览器跳转回App主流程)
singleInstance独占一个新任务栈,且该栈中只能存在该ActivityonNewIntent独立功能模块(如系统相机、第三方登录页)
  1. taskAffinity的作用

    1. 默认与包名一致,可通过android:taskAffinity自定义。
    2. singleTask会优先匹配相同taskAffinity的任务栈,若无匹配栈则新建。
    3. 示例:浏览器应用通过不同taskAffinity管理多个网页栈。
  2. singleInstance的特殊性

    1. 完全独占任务栈,其他Activity无法进入该栈。
    2. singleInstance启动其他Activity时,会跳转到其他任务栈(可能触发系统任务管理器的多窗口效果)
  3. allowTaskReparenting的联动

    1. allowTaskReparenting="true"时,Activity可根据taskAffinity动态迁移到前台任务栈(常见于跨应用跳转后返回原应用)
(2)通知栏跳转场景的优化设计

需求分析

  • 核心目标:无论用户从通知栏点击多少次,目标Activity(如消息详情页)始终保持唯一实例,且不重复创建。

  • 潜在问题

    • 用户可能从不同入口(如主页、其他页面)触发通知跳转。
    • 需要处理返回栈逻辑,避免回退时出现空白页面。

实现方案

  • launchMode="singleTask":确保全局唯一性,复用实例时清除上方页面。
  • taskAffinity:隔离消息相关Activity到独立任务栈,避免影响主流程。
  • FLAG_ACTIVITY_NEW_TASK:强制在新任务栈中启动(配合taskAffinity)。
  • FLAG_ACTIVITY_CLEAR_TOP:若目标Activity已存在,清除其上方所有实例。
<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状态陈旧。

override fun onNewIntent(intent: Intent?) {
    super.onNewIntent(intent)
    val messageId = intent?.getStringExtra("message_id")
    // 根据新的messageId刷新UI
    loadMessageDetails(messageId)
}

问题6:taskAffinity有什么作用,可以在哪些场景用到它?

  1. 定义
  • taskAffinity 是 Activity 的一个属性(可通过 AndroidManifest.xml 或代码设置),用于指定该 Activity  “倾向于”归属于哪个任务栈
  • 默认情况下,所有 Activity 的 taskAffinity 继承自应用的包名(<manifest package>)。例如,包名为 com.example.app,则默认 taskAffinity="com.example.app"
  1. 核心规则
  • 任务栈匹配优先级:当启动一个 Activity 时,系统会优先寻找与其 taskAffinity 相同的任务栈。
  • 与启动模式联动singleTask 和 singleInstance 启动模式的行为高度依赖 taskAffinity
  1. taskAffinity 的实践案例

案例1:浏览器应用的多标签页管理

  • 需求:每个浏览器标签页独立运行,且在最近任务列表中显示为独立卡片。
  • 实现:taskAffinity + singleTask
<!-- 每个 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匹配的前台栈。

<!-- App A 的订单详情页 -->
<activity
    android:name=".OrderDetailActivity"
    android:taskAffinity=".OrderTask"
    android:allowTaskReparenting="true" /
// 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()之后
onPause() → onSaveInstanceState() → onStop() → onDestroy()。

2.  onRestoreInstanceState(Bundle savedInstanceState):

用于从Bundle中恢复之前保存的临时数据,通常在Activity被系统重建后调用。

触发条件:

Activity被系统重建:例如屏幕旋转后恢复。

调用时机:在onStart()之后、onResume()之前调用。

onCreate() → onStart() → onRestoreInstanceState() → onResume()

3.与 onCreate() 的数据恢复对比

方法Bundle参数是否可能为null适用场景
onCreate(Bundle)是(首次创建时为null)通用初始化,需检查Bundle是否为null。
onRestoreInstanceState()否(Bundle一定非null)专用恢复数据,无需判空,代码更简洁。

问题8:如何通过Intent在Activity之间传递数据?Bundle的大小限制是多少?

1.通过 Intent putExtra() 方法直接传递基本类型数据

// 在源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 接口以支持序列化。

// 定义数据类
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共享数据

// 在源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之间的通信?

  1. 定义通信接口
  • 实现方法: 在Fragment中定义通信接口,由Activity实现该接口
  • 优点:解耦,Fragment不依赖具体Activity。
  • 缺点:需手动管理接口绑定与解绑。
  1. ViewModel共享数据(推荐用于数据驱动场景)
  • 实现方法: 创建共享ViewModel,Activity与Fragment中访问ViewModel
  • 优点:生命周期安全,数据持久化,支持多Fragment共享。
  • 缺点:需引入AndroidX依赖。
  1. 直接引用Activity(简单但高耦合)
  • 实现方法: 在Fragment中获取Activity引用
  • 优点:实现简单。
  • 缺点:高耦合,Fragment无法复用。
  1. EventBus(第三方库,慎用)
  • 实现方法: 添加依赖, Fragment发送事件,Activity订阅事件
  • 优点:跨组件通信灵活。
  • 缺点:需引入第三方库,易导致内存泄漏和代码混乱。
  1. Fragment Result API(AndroidX推荐方式)
  • 优点:官方推荐,生命周期安全。
  • 缺点:仅支持单向通信,适合简单场景。
  1. 最佳实践
  • 简单数据传递:优先使用 Fragment Result API
  • 数据共享与状态管理:选择 ViewModel

问题10:如何避免Activity的内存泄漏

常见内存泄漏场景与解决方案

(1)静态变量持有Activity引用

场景:单例类或静态变量直接或间接持有Activity的Context

object Singleton {var context: Context? = null // 错误:可能持有Activity的引用}

解决方案

  • 使用Application Context代替Activity Context
  • 若必须使用Activity引用,使用弱引用(WeakReference):
class Singleton {
    private var activityRef: WeakReference<Activity>? = null
    
    fun setActivity(activity: Activity) {
        activityRef = WeakReference(activity)
    }
}

(2)非静态内部类或匿名内部类

场景:Handler、Runnable等内部类隐式持有Activity引用。

class MyActivity : Activity() {
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // 隐式持有外部Activity引用
        }
    }
}

解决方案:静态内部类 + 弱引用

private class SafeHandler(activity: MyActivity) : Handler(Looper.getMainLooper()) {
    private val activityRef = WeakReference(activity)
    
    override fun handleMessage(msg: Message) {
        val activity = activityRef.get() ?: return
        // 处理消息
    }
}

onDestroy()中移除回调

override fun onDestroy() {
    handler.removeCallbacksAndMessages(null)
    super.onDestroy()
}

(3) 未正确注销监听器或回调

场景:注册广播、事件总线、监听器后未及时注销

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    LocalBroadcastManager.getInstance(this).registerReceiver(receiver, intentFilter)
}

解决方案:onDestroy()中反注册:

override fun onDestroy() {
    LocalBroadcastManager.getInstance(this).unregisterReceiver(receiver)
    super.onDestroy()
}

(4)异步任务未取消

场景:AsyncTask、RxJava、Coroutine等异步任务未随Activity销毁终止。

private var job: Job? = null

fun startTask() {
    job = CoroutineScope(Dispatchers.IO).launch {
        // 耗时操作
        withContext(Dispatchers.Main) {
            updateUI() // 若Activity已销毁,此处可能崩溃
        }
    }
}

解决方案: 使用生命周期感知的协程(lifecycleScope

lifecycleScope.launch {
    // 自动在onDestroy时取消
}

// 取消任务
override fun onDestroy() {
    job?.cancel()
    super.onDestroy()
}

(5) 资源未释放

场景:文件流、数据库连接、传感器服务等未关闭。

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()中释放资源:

override fun onDestroy() {
    sensorManager.unregisterListener(sensorListener)
    super.onDestroy()
}
避免内存泄漏的黄金法则
  1. 及时释放资源:在onDestroy()中取消任务、反注册监听器、关闭资源。
  2. 避免强引用:优先使用弱引用或生命周期感知的组件。
  3. 最小化Context使用:尽量使用Application Context代替Activity Context
  4. 工具辅助:定期使用LeakCanary和Profiler检测潜在问题。

问题11: 解释Activity的任务栈(Task)和返回栈(Back Stack)管理机制。

  1. 任务栈(Task)

    1. 定义:一组Activity的集合,按启动顺序排列,形成一个任务栈。

    2. 特点

      • 每个任务栈有一个唯一的ID(Task ID)。
      • 任务栈可以跨应用(如从应用A跳转到应用B的某个Activity)。
      • 任务栈中的Activity可以来自不同应用(如浏览器打开多个标签页)。
  2. 返回栈(Back Stack)

    1. 定义:用户通过返回键(Back)导航的Activity栈,是任务栈的子集。

    2. 特点

      • 返回栈遵循“后进先出”(LIFO)原则。
      • 用户按返回键时,当前Activity出栈,前一个Activity恢复显示。
  3. 任务栈与返回栈的关系

  • 任务栈是物理概念,返回栈是逻辑概念。
  • 返回栈是任务栈中用户可见的部分。
  • 一个应用可以有多个任务栈(如通过FLAG_ACTIVITY_NEW_TASK启动新栈)。
  • 每个任务栈有独立的返回栈。
  1. 任务栈的管理机制

(1)创建新任务栈: 通过Intent.FLAG_ACTIVITY_NEW_TASK启动Activity时,若目标Activity的taskAffinity与当前任务栈不同,则创建新任务栈。

val intent = Intent(this, TargetActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
  • 切换任务栈: 通过ActivityManagermoveTaskToFront()方法将任务栈切换到前台。

(2)任务栈的销毁

  • 用户主动退出:按返回键直到栈中所有Activity出栈。
  • 系统回收:内存不足时,后台任务栈可能被系统销毁。

(3)返回栈的入栈与出栈

  • 入栈:启动新Activity时,当前Activity入栈。
  • 出栈:按返回键时,当前Activity出栈,前一个Activity恢复。

(4)返回栈的调整

  • FLAG_ACTIVITY_CLEAR_TOP:若目标Activity已在栈中,则清除其上方所有Activity并复用。
  • FLAG_ACTIVITY_REORDER_TO_FRONT:若目标Activity已在栈中,则将其移到栈顶
val intent = Intent(this, TargetActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 或者 intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP 
startActivity(intent)