- Activity 生命周期是什么? onCreate:初始化视图、数据恢复 onStart:页面可见 onResume:页面处于前台,可交互 onPause:失去焦点,轻量释放 onStop:完全不可见,重资源释放 onDestroy:销毁前清理,但不保证一定执行 加上一个onrestart 启动模型
跳转时: 旧 Activity 先 onPause,新 Activity 启动后,旧的再 onStop。 切回主界面时: onRestart -> onStart -> onResume。
- activity的启动模式 (LaunchMode) 及场景
Standard: 默认模式,每次启动都创建新实例。
SingleTop: 栈顶复用。场景: 通知栏点击进入已打开的页面,避免重复创建。
SingleTask: 栈内复用,会移除其上方的所有 Activity。场景: APP 主页。
SingleInstance: 独占一个任务栈。场景: 闹钟、来电显示。
- 屏幕旋转后的 Activity 重建
原理: 默认会执行销毁并重建流程。
处理方案:
持久化: 在 onSaveInstanceState 中保存 Bundle 数据,在 onCreate 或 onRestoreInstanceState 中恢复。
ViewModel: 利用 ViewModel 生命周期长于 Activity 的特性存储 UI 数据。可以详细了解Viewmodel 中的过程。
配置拦截: 在 Manifest 设置 android:configChanges=“orientation|screenSize”,拦截后在 onConfigurationChanged 中手动处理。
onSaveInstanceState 保存临时 UI 状态,例如Android 默认会保存带有 id 的 View 的状态(比如 EditText 的文字、Checkbox 的选中状态)。如果没给 View 设置 ID,状态就不会被自动保存。
activity 与 task 的关系
默认情况是的,所有 Activity 都在同一个以包名命名的 Task 中。但可以通过在 Manifest 中配置 taskAffinity 属性,或者在 Intent 中使用 FLAG_ACTIVITY_NEW_TASK 标志位,来让特定的 Activity 运行在独立的 Task 中。” 不同 App 的 Activity 能进同一个 Task 吗? 可以的,这也是 Android 跨应用协作的精髓。比如, App 调用系统相机拍照,此时相机的 Activity 其实是被压入了 App 当前的 Task 栈顶。这样用户拍完照按返回键,能极其自然地退回我的 App,体验是无缝的。
Fragment
- Fragment 生命周期与 Activity 的对应
简洁回答: Fragment 依赖 Activity 存在。它多出了 onAttach (关联 Activity) 和 onDetach (解除关联),以及专门管理 View 的 onCreateView 和 onDestroyView。
关键点: Activity 进入 onPause,内部所有 Fragment 强制进入 onPause。
- Add vs Replace 的区别
Add: 将 Fragment 叠加到容器中,不销毁之前的 Fragment。流程: 配合 hide/show 使用,不会触发生命周期重走。
Replace: 移除容器内旧的,添加新的。流程: 会导致旧 Fragment 销毁并重新创建。场景: 内存压力大或页面切换频繁时用 Replace,需要保留状态时用 Add + Hide。
Fragment 的 View 生命周期可能短于 Fragment 自身,不能让已经销毁的 View 被继续引用 常见泄漏点:binding、adapter、匿名内部类、observe 生命周期绑错
- 为什么有时候在Fragment 中会有 getActivity() 为什么有时会为空(返回 null)? 可能最多的原因:Fragment 的生命周期已经走到了 onDetach(),脱离了 Activity,但后台任务才刚刚执行完。 场景:Fragment 中发起了一个网络请求或者启动了一个延迟动画。在请求还没有返回时,用户按了返回键或者切换了页面,导致这个 Fragment 被销毁并从 Activity 上解绑。几秒后,网络请求成功,回调执行。回调代码中尝试调用 getActivity().runOnUiThread(…) 或者用 getActivity() 去获取资源(Resource)。因为此时 Fragment 已经没有宿主了,getActivity() 就会返回 null,应用直接崩溃。
所以这时候的方法:1 使用前判空 如果为null就直接return 2 使用viewmodel 和 livedata 绑定生命周期,可以自动直接取消观察者的回调 3 使用lifecyclescope感知生命周期,协程会自动取消。
Service
Service 的生命周期 startService vs bindService
startService: 启动后与启动者无关。生命周期:onCreate -> onStartCommand -> onDestroy。除非手动调用 stopService,否则一直运行。
bindService: 与启动者绑定,生命周期随之销毁。通过 onBind 返回的 IBinder 实现进程内/间通信。onCreate() -> onBind() -> onUnbind() -> onDestroy()
- 如何执行耗时操作?
重点: Service 默认运行在主线程。
必须在 Service 内部手动开启子线程(Thread/Executor),或者直接使用 IntentService。或者和 IntentService 内部封装了 HandlerThread,任务执行完会自动 stopSelf(),非常适合处理后台排队任务。
- 前台 Service(Foreground Service)是什么?为什么需要它?
问题背景: Android 8.0 (Oreo) 后,系统对后台 Service 限制越来越严格,后台 Service 可能被随时杀死。
前台 Service 特点:
- 必须显示一个持久的系统通知(用户可感知)
- 系统不会轻易回收前台 Service
- 使用
startForeground(notificationId, notification)启动
场景: 音乐播放、实时导航、文件下载、健康数据采集。
Android 12+ 还需要在 Manifest 中声明 android:foregroundServiceType,如 mediaPlayback、location 等。
如果 App 已经在后台(比如用户已经回到了桌面),直接调用普通的 startService() 会直接报错。
这个时候必须使用startForegroundService(),然后调用startForeground()方法,否则会报错。
- Service 与线程的区别?
Service 是 Android 的组件,有生命周期,运行在主线程。
Thread 是 Java 线程,没有 Android 生命周期感知。
区别核心:
- Service 即使 Activity 销毁了仍可运行;Thread 会随 Activity 泄漏。
- Service 可被系统调度、重启(通过
onStartCommand返回值控制);Thread 不行。 - Service 是跨组件通信入口;Thread 是纯执行单元。
- AIDL 是什么?什么时候用?
AIDL(Android Interface Definition Language)是 Android 实现 跨进程通信(IPC) 的接口定义语言,底层基于 Binder 机制。
什么时候用:
- 需要从另一个进程(不同 App 或多进程 App)访问 Service 的功能时。
- 如果只是同进程内通信,直接用普通 Binder(继承 Binder 类)即可,更简单。
Binder 机制核心:内核层的一次内存拷贝(mmap),比传统 IPC(Socket/管道两次拷贝)更高效。
BroadcastReceiver
- 广播的两种注册方式及区别?
静态注册(Manifest 中注册):
- 即使 App 进程未启动,也能收到广播
- Android 8.0 后大量隐式广播无法用静态注册接收(安全限制)
- 适合:开机启动、包安装等系统广播
动态注册(代码中 registerReceiver):
- 生命周期与注册者绑定,需手动 unregister(否则内存泄漏)
- 可接收 8.0 后被限制的隐式广播
- 适合:网络状态变化、屏幕亮灭等与 UI 关联的场景
- 有序广播 vs 普通广播?
普通广播(Normal Broadcast):异步,所有 Receiver 同时收到,无法被拦截或修改。
有序广播(Ordered Broadcast):
- 按
priority优先级依次传递 - 高优先级 Receiver 可修改广播数据或调用
abortBroadcast()终止传递 - 场景:短信拦截、支付安全校验
- LocalBroadcastManager 是什么?为什么被废弃?
LocalBroadcastManager 只在 App 内部传递广播,不跨进程,安全性更高、效率更好。
为何废弃(AndroidX 1.1.0 后):
- 功能完全可被 LiveData、Flow、EventBus 等替代,且后者有生命周期感知能力,不会内存泄漏。
- 官方建议迁移到
LiveData或Flow。
- 广播的常见使用注意点?
- 不能在
onReceive中执行耗时操作(有 10 秒 ANR 限制,且运行在主线程) - 耗时操作应使用
goAsync()+ 子线程,或启动 Service - 动态注册一定要在合适时机
unregisterReceiver,通常在onStop或onDestroy
ContentProvider
- ContentProvider 是什么?核心作用?
ContentProvider 是 Android 四大组件之一,提供了一套标准的、基于 Uri 的数据共享接口,可以安全地将数据暴露给其他 App 或组件。
核心作用:
- 跨进程数据共享:其他 App 通过
ContentResolver+ Uri 访问数据,无需关心底层实现(SQLite / 文件 / 网络皆可) - 权限控制:可以精细化控制读/写权限(
readPermission/writePermission) - 标准化接口:query / insert / update / delete,语义清晰
- ContentProvider 的 Uri 是如何构成的?
content://authority/path/id
content://:固定 scheme,标识这是一个 ContentProvider 数据authority:Provider 的唯一标识,通常是包名(如com.example.app.provider)path:数据集名称(如users、contacts)id:(可选)特定记录的 ID
- ContentProvider 线程安全问题?
ContentProvider 的 CRUD 方法默认在 Binder 线程池中被调用,即天然多线程。
注意事项:
- 如果底层用 SQLite,
SQLiteDatabase本身是线程安全的。 - 如果底层是其他数据结构,必须自己加锁保证线程安全。
onCreate()在主线程调用,应避免耗时操作。
- 如何用 ContentProvider 监听数据变化?
Provider 端修改数据后调用:
context.contentResolver.notifyChange(uri, null)
客户端注册监听:
contentResolver.registerContentObserver(uri, true, myObserver)
MediaStore 等系统 Provider 正是利用这套机制通知图库、音乐等 App 刷新数据。
- FileProvider 是什么?为什么 Android 7.0 后必须用它?
FileProvider 是 ContentProvider 的子类,专门用于安全地跨 App 共享文件。
Android 7.0 前:可以直接把 file:// URI 传给其他 App(如调用系统相机拍照保存到本地)。
Android 7.0 后:系统强制要求使用 content:// URI 共享文件,直接使用 file:// 会抛出 FileUriExposedException。
FileProvider停止传递真实的文件路径,改用 FileProvider 生成带有临时授权的 content:// URI
使用步骤:
- 在 Manifest 声明 FileProvider
- 在
res/xml/配置文件路径白名单 - 用
FileProvider.getUriForFile()生成content://URI,并通过Intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION)授予临时访问权
四大组件对比总结
| 组件 | 核心职责 | 运行线程 | 典型场景 |
|---|---|---|---|
| Activity | 用户界面交互 | 主线程 | 登录页、详情页 |
| Service | 后台任务/IPC入口 | 主线程(需自开子线程) | 音乐播放、数据同步 |
| BroadcastReceiver | 事件广播与接收 | 主线程(≤10s) | 监听网络状态、开机启动 |
| ContentProvider | 跨进程数据共享 | Binder线程池 | 通讯录、MediaStore |
高频追问:ANR 触发条件
| 场景 | 超时阈值 |
|---|---|
| Activity 主线程无响应 | 5 秒 |
| BroadcastReceiver onReceive | 10 秒(前台广播),60 秒(后台广播) |
| Service onCreate/onStartCommand | 20 秒(前台),200 秒(后台) |
| ContentProvider 请求超时 | 10 秒 |
根本原因:主线程被阻塞(IO、锁竞争、死循环)。排查工具:ANR traces 文件(/data/anr/traces.txt)、Android Vitals。
内存泄漏
核心原理:生命周期长的对象,持有了生命周期短的对象的引用
这是 Android 内存泄漏的唯一根本原因。
GC 的回收条件是:一个对象不再被任何 GC Root 引用时,才能被回收。 如果一个长生命周期的对象持有了短生命周期对象的强引用,即使短生命周期对象"该死了",GC 也无法回收它,内存就泄漏了。
长生命周期对象 ──持有引用──► 短生命周期对象(本该被回收)
|
还活着,是 GC Root 的可达节点
↓
短生命周期对象永远无法被 GC 回收 → 内存泄漏
经典案例:Thread 的生命周期长于 Activity
场景还原:
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 匿名内部类 Runnable,隐式持有外部 MyActivity 的引用
Thread {
Thread.sleep(30_000) // 模拟耗时操作,如网络请求
runOnUiThread {
textView.text = "完成" // 直接访问 Activity 的 View
}
}.start()
}
}
泄漏链路分析:
用户在线程跑完之前按了返回键,Activity 执行 onDestroy(),本该被回收。
但此时引用链是:
Thread(仍在运行,是 GC Root)
└─► 匿名 Runnable 对象
└─► 隐式持有 MyActivity.this(外部类引用)
└─► MyActivity 的所有 View、Bitmap、Context...
Activity 无法被 GC 回收,它持有的界面资源(View 树、Bitmap 等)全部被阻止回收,严重时几十 MB 就这样泄漏了。
为什么匿名内部类会隐式持有外部类的引用?
这是 Java 编译器的设计:非静态内部类(包括匿名类、Lambda 捕获了外部变量时)在编译后会自动加入一个指向外部类实例的字段 this$0。这就是泄漏的根源。
解决方案的演进
方案一:弱引用(WeakReference)
用 WeakReference 包裹 Activity,让 GC 可以在内存紧张时回收它:
class MyTask(activity: MyActivity) : Runnable {
// 弱引用:不阻止 GC 回收 Activity
private val activityRef = WeakReference(activity)
override fun run() {
Thread.sleep(30_000)
val activity = activityRef.get() ?: return // Activity 已销毁则直接退出
activity.runOnUiThread {
activity.textView.text = "完成"
}
}
}
缺点:需要手动判空,代码繁琐;并且线程本身还在跑,浪费 CPU,只是不再泄漏 Activity 而已。
方案二:静态内部类 + 弱引用
// 静态内部类:不持有外部类引用
private class MyHandler(activity: MyActivity) : Handler(Looper.getMainLooper()) {
private val ref = WeakReference(activity)
override fun handleMessage(msg: Message) {
ref.get()?.updateUI()
}
}
这是 Handler 内存泄漏的经典修法,Handler 和 Thread 的原理相同。
方案三:主动取消任务
真正的修法是:Activity 销毁时,主动取消掉它相关的所有异步任务。
class MyActivity : AppCompatActivity() {
private var job: Job? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
job = lifecycleScope.launch { // ✅ 与 Activity 生命周期绑定的协程作用域
val result = withContext(Dispatchers.IO) {
fetchData() // 耗时操作在 IO 线程执行
}
textView.text = result // 自动切回主线程更新 UI
}
}
// lifecycleScope 在 onDestroy 时自动取消,不需要手动 job?.cancel()
}
lifecycleScope 是 AndroidX Lifecycle 库提供的协程作用域,它与 Activity 的生命周期绑定,Activity 销毁时自动 cancel 所有协程,从根本上解决了这类问题。
Lifecycle 系统:系统性解决生命周期感知问题
上面 Thread 的例子揭示了一个更普遍的问题:很多对象(网络库、定位 SDK、传感器监听)都有自己的生命周期,它们需要感知 Activity/Fragment 的状态,在合适的将这个任务取消。
手动在 onStart/onStop 里写启停逻辑,代码分散且容易遗忘。Jetpack 的 Lifecycle 是google 给出的系统性感知生命周期的方案:
核心角色:
| 角色 | 说明 |
|---|---|
LifecycleOwner | 持有生命周期的组件(Activity、Fragment 已内置实现) |
LifecycleObserver | 想感知生命周期的观察者,实现这个接口 |
Lifecycle | 生命周期状态机,维护 CREATED / STARTED / RESUMED / DESTROYED 等状态 |
实现方式:让自定义组件感知生命周期
class LocationTracker(private val lifecycle: Lifecycle) : DefaultLifecycleObserver {
override fun onStart(owner: LifecycleOwner) {
startListening() // Activity onStart 时自动开始定位
}
override fun onStop(owner: LifecycleOwner) {
stopListening() // Activity onStop 时自动停止,不泄漏
}
}
// 在 Activity 中只需一行接入:
lifecycle.addObserver(LocationTracker(lifecycle))
这样 LocationTracker 内部不再需要持有 Activity 引用,Activity 的生命周期事件会主动推送给它,彻底解耦。
LiveData 是 Lifecycle 的最佳实践之一:
LiveData 观察者只在 STARTED 或 RESUMED 状态下分发数据,在 DESTROYED 时自动移除观察者,所以用 observe(viewLifecycleOwner, ...) 永远不会因为 Fragment View 销毁后还回调而导致泄漏或崩溃。
常见内存泄漏场景速查
| 场景 | 原因 | 修法 |
|---|---|---|
| 匿名 Thread / Runnable | 隐式持有 Activity 引用 | lifecycleScope 协程 |
| 非静态 Handler | 持有 Activity 引用 + Message 队列延迟 | 静态内部类 + 弱引用,或 lifecycleScope |
| 动态注册 BroadcastReceiver 未注销 | Receiver 对象被系统持有 | onStop 中 unregisterReceiver |
Fragment 中 _binding = null 未置空 | ViewBinding 持有 View 树,View 生命周期短于 Fragment | onDestroyView 中置空 binding |
| 单例持有 Context | 单例生命周期 = 进程,Activity Context 被持有 | 改用 applicationContext |
observe 绑错生命周期 | this(Fragment)比 viewLifecycleOwner 生命周期长 | 始终用 viewLifecycleOwner 观察 LiveData |