1. Activity 生命周期是什么? onCreate:初始化视图、数据恢复 onStart:页面可见 onResume:页面处于前台,可交互 onPause:失去焦点,轻量释放 onStop:完全不可见,重资源释放 onDestroy:销毁前清理,但不保证一定执行 加上一个onrestart 启动模型

跳转时: 旧 Activity 先 onPause,新 Activity 启动后,旧的再 onStop。 切回主界面时: onRestart -> onStart -> onResume。

  1. activity的启动模式 (LaunchMode) 及场景

Standard: 默认模式,每次启动都创建新实例。

SingleTop: 栈顶复用。场景: 通知栏点击进入已打开的页面,避免重复创建。

SingleTask: 栈内复用,会移除其上方的所有 Activity。场景: APP 主页。

SingleInstance: 独占一个任务栈。场景: 闹钟、来电显示。

  1. 屏幕旋转后的 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

  1. Fragment 生命周期与 Activity 的对应

简洁回答: Fragment 依赖 Activity 存在。它多出了 onAttach (关联 Activity) 和 onDetach (解除关联),以及专门管理 View 的 onCreateView 和 onDestroyView。

关键点: Activity 进入 onPause,内部所有 Fragment 强制进入 onPause。

  1. Add vs Replace 的区别

Add: 将 Fragment 叠加到容器中,不销毁之前的 Fragment。流程: 配合 hide/show 使用,不会触发生命周期重走。

Replace: 移除容器内旧的,添加新的。流程: 会导致旧 Fragment 销毁并重新创建。场景: 内存压力大或页面切换频繁时用 Replace,需要保留状态时用 Add + Hide。

Fragment 的 View 生命周期可能短于 Fragment 自身,不能让已经销毁的 View 被继续引用 常见泄漏点:binding、adapter、匿名内部类、observe 生命周期绑错

  1. 为什么有时候在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()

  1. 如何执行耗时操作?

重点: Service 默认运行在主线程。

必须在 Service 内部手动开启子线程(Thread/Executor),或者直接使用 IntentService。或者和 IntentService 内部封装了 HandlerThread,任务执行完会自动 stopSelf(),非常适合处理后台排队任务。

  1. 前台 Service(Foreground Service)是什么?为什么需要它?

问题背景: Android 8.0 (Oreo) 后,系统对后台 Service 限制越来越严格,后台 Service 可能被随时杀死。

前台 Service 特点:

  • 必须显示一个持久的系统通知(用户可感知)
  • 系统不会轻易回收前台 Service
  • 使用 startForeground(notificationId, notification) 启动

场景: 音乐播放、实时导航、文件下载、健康数据采集。

Android 12+ 还需要在 Manifest 中声明 android:foregroundServiceType,如 mediaPlaybacklocation 等。 如果 App 已经在后台(比如用户已经回到了桌面),直接调用普通的 startService() 会直接报错。 这个时候必须使用startForegroundService(),然后调用startForeground()方法,否则会报错。

  1. Service 与线程的区别?

Service 是 Android 的组件,有生命周期,运行在主线程。
Thread 是 Java 线程,没有 Android 生命周期感知。

区别核心:

  • Service 即使 Activity 销毁了仍可运行;Thread 会随 Activity 泄漏。
  • Service 可被系统调度、重启(通过 onStartCommand 返回值控制);Thread 不行。
  • Service 是跨组件通信入口;Thread 是纯执行单元。
  1. AIDL 是什么?什么时候用?

AIDL(Android Interface Definition Language)是 Android 实现 跨进程通信(IPC) 的接口定义语言,底层基于 Binder 机制。

什么时候用:

  • 需要从另一个进程(不同 App 或多进程 App)访问 Service 的功能时。
  • 如果只是同进程内通信,直接用普通 Binder(继承 Binder 类)即可,更简单。

Binder 机制核心:内核层的一次内存拷贝(mmap),比传统 IPC(Socket/管道两次拷贝)更高效。

BroadcastReceiver

  1. 广播的两种注册方式及区别?

静态注册(Manifest 中注册):

  • 即使 App 进程未启动,也能收到广播
  • Android 8.0 后大量隐式广播无法用静态注册接收(安全限制)
  • 适合:开机启动、包安装等系统广播

动态注册(代码中 registerReceiver):

  • 生命周期与注册者绑定,需手动 unregister(否则内存泄漏)
  • 可接收 8.0 后被限制的隐式广播
  • 适合:网络状态变化、屏幕亮灭等与 UI 关联的场景
  1. 有序广播 vs 普通广播?

普通广播(Normal Broadcast):异步,所有 Receiver 同时收到,无法被拦截或修改。

有序广播(Ordered Broadcast):

  • priority 优先级依次传递
  • 高优先级 Receiver 可修改广播数据或调用 abortBroadcast() 终止传递
  • 场景:短信拦截、支付安全校验
  1. LocalBroadcastManager 是什么?为什么被废弃?

LocalBroadcastManager 只在 App 内部传递广播,不跨进程,安全性更高、效率更好。

为何废弃(AndroidX 1.1.0 后):

  • 功能完全可被 LiveData、Flow、EventBus 等替代,且后者有生命周期感知能力,不会内存泄漏。
  • 官方建议迁移到 LiveDataFlow
  1. 广播的常见使用注意点?
  • 不能在 onReceive 中执行耗时操作(有 10 秒 ANR 限制,且运行在主线程)
  • 耗时操作应使用 goAsync() + 子线程,或启动 Service
  • 动态注册一定要在合适时机 unregisterReceiver,通常在 onStoponDestroy

ContentProvider

  1. ContentProvider 是什么?核心作用?

ContentProvider 是 Android 四大组件之一,提供了一套标准的、基于 Uri 的数据共享接口,可以安全地将数据暴露给其他 App 或组件。

核心作用:

  • 跨进程数据共享:其他 App 通过 ContentResolver + Uri 访问数据,无需关心底层实现(SQLite / 文件 / 网络皆可)
  • 权限控制:可以精细化控制读/写权限(readPermission / writePermission
  • 标准化接口:query / insert / update / delete,语义清晰
  1. ContentProvider 的 Uri 是如何构成的?
content://authority/path/id
  • content://:固定 scheme,标识这是一个 ContentProvider 数据
  • authority:Provider 的唯一标识,通常是包名(如 com.example.app.provider
  • path:数据集名称(如 userscontacts
  • id:(可选)特定记录的 ID
  1. ContentProvider 线程安全问题?

ContentProvider 的 CRUD 方法默认在 Binder 线程池中被调用,即天然多线程

注意事项:

  • 如果底层用 SQLite,SQLiteDatabase 本身是线程安全的。
  • 如果底层是其他数据结构,必须自己加锁保证线程安全。
  • onCreate() 在主线程调用,应避免耗时操作。
  1. 如何用 ContentProvider 监听数据变化?

Provider 端修改数据后调用:

context.contentResolver.notifyChange(uri, null)

客户端注册监听:

contentResolver.registerContentObserver(uri, true, myObserver)

MediaStore 等系统 Provider 正是利用这套机制通知图库、音乐等 App 刷新数据。

  1. FileProvider 是什么?为什么 Android 7.0 后必须用它?

FileProvider 是 ContentProvider 的子类,专门用于安全地跨 App 共享文件

Android 7.0 前:可以直接把 file:// URI 传给其他 App(如调用系统相机拍照保存到本地)。

Android 7.0 后:系统强制要求使用 content:// URI 共享文件,直接使用 file:// 会抛出 FileUriExposedException。 FileProvider停止传递真实的文件路径,改用 FileProvider 生成带有临时授权的 content:// URI 使用步骤:

  1. 在 Manifest 声明 FileProvider
  2. res/xml/ 配置文件路径白名单
  3. FileProvider.getUriForFile() 生成 content:// URI,并通过 Intent.addFlags(FLAG_GRANT_READ_URI_PERMISSION) 授予临时访问权

四大组件对比总结

组件核心职责运行线程典型场景
Activity用户界面交互主线程登录页、详情页
Service后台任务/IPC入口主线程(需自开子线程)音乐播放、数据同步
BroadcastReceiver事件广播与接收主线程(≤10s)监听网络状态、开机启动
ContentProvider跨进程数据共享Binder线程池通讯录、MediaStore

高频追问:ANR 触发条件

场景超时阈值
Activity 主线程无响应5 秒
BroadcastReceiver onReceive10 秒(前台广播),60 秒(后台广播)
Service onCreate/onStartCommand20 秒(前台),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 观察者只在 STARTEDRESUMED 状态下分发数据,在 DESTROYED 时自动移除观察者,所以用 observe(viewLifecycleOwner, ...) 永远不会因为 Fragment View 销毁后还回调而导致泄漏或崩溃。


常见内存泄漏场景速查

场景原因修法
匿名 Thread / Runnable隐式持有 Activity 引用lifecycleScope 协程
非静态 Handler持有 Activity 引用 + Message 队列延迟静态内部类 + 弱引用,或 lifecycleScope
动态注册 BroadcastReceiver 未注销Receiver 对象被系统持有onStopunregisterReceiver
Fragment 中 _binding = null 未置空ViewBinding 持有 View 树,View 生命周期短于 FragmentonDestroyView 中置空 binding
单例持有 Context单例生命周期 = 进程,Activity Context 被持有改用 applicationContext
observe 绑错生命周期this(Fragment)比 viewLifecycleOwner 生命周期长始终用 viewLifecycleOwner 观察 LiveData