Android 底层实现基础

Activity 生命周期

应用内 Activity 跳转流程(A → B)

从 Activity A 打开新的 Activity B(如点击按钮跳转详情页)

  1. A.onCreate()A.onStart()A.onResume() (A 已在前台)
  2. 点击跳转按钮 → A.onPause() (A 暂停但仍可见)
  3. B.onCreate()B.onStart()B.onResume() (B 进入前台)
  4. A.onStop() (A 完全不可见,但未被销毁)

返回键关闭当前 Activity(B → A)

在 Activity B 中按返回键,回到 Activity A

  1. 按返回键 → B.onPause()
  2. A.onRestart()A.onStart()A.onResume() (A 重新可见)
  3. B.onStop()B.onDestroy() (B 被销毁)

Home 键切到后台(应用存活)

在 Activity A 运行时按 Home 键回到桌面

  1. 按 Home 键 → A.onPause()A.onStop()
    注意:此时 A 未被销毁,进程存活)

切换到其他应用(如从微信跳转到支付宝)

从当前应用 Activity A 打开另一个应用(如点击链接跳转支付宝)

  1. 点击跳转 → A.onPause()
  2. 支付宝冷/温启动 → 支付宝页面显示
  3. A.onStop() (A 完全不可见,但进程存活)

后台被系统回收后恢复(温启动场景)

应用在后台时,因内存不足被系统回收 Activity(非杀进程),用户再次点击图标进入

  1. 系统回收 Activity → 调用 A.onSaveInstanceState() 保存数据
  2. 用户点击图标 → 重建 Activity A
    A.onCreate(savedInstanceState)A.onStart()A.onResume()

返回键退出应用(销毁所有 Activity)

在首页 Activity A 按返回键退出应用

  1. 按返回键 → A.onPause()A.onStop()A.onDestroy()
  2. 进程仍存活(系统缓存),但任务栈清空

任务(Task)和返回栈(Back Stack)

一、核心概念

  1. 任务(Task)

    • 本质 :用户为完成特定目标(如"写邮件"、"购物")而交互的 Activity 集合
    • 表现形式 :一个按打开顺序排列的 Activity 栈(即返回栈)
    • 系统级标识 :每个任务有独立 任务 ID,系统通过它管理任务切换。
    • 用户视角:在"最近任务列表"(Recents Screen)中显示为独立卡片。
  2. 返回栈(Back Stack)

    • 本质 :属于同一任务的 Activity 实例的有序栈(后进先出)。
    • 关键规则:用户按返回键时,栈顶 Activity 出栈并销毁,前一个 Activity 恢复显示。
    • 跨进程支持:栈内 Activity 可来自不同应用(如从浏览器打开地图应用)。

二、底层工作原理

1. Activity 启动与入栈
  • 默认行为 :新启动的 Activity 被压入当前任务的栈顶(standard 启动模式)。
  • 任务亲和性(Task Affinity)
    • 每个 Activity 通过 android:taskAffinity 属性声明"归属偏好"。
    • 默认亲和性 = 应用包名(同一应用 Activity 通常属于同一任务)。
  • Intent Flags 控制栈行为 (代码动态控制):
    • FLAG_ACTIVITY_NEW_TASK:在新任务中启动 Activity(若任务不存在则创建)。
    • FLAG_ACTIVITY_CLEAR_TOP:若目标 Activity 已在栈中,则清除其上的所有 Activity。
    • FLAG_ACTIVITY_SINGLE_TOP:若目标 Activity 已在栈顶,则复用实例(触发 onNewIntent())。
2. 启动模式(Launch Modes)
模式 行为描述 测试关注点
standard (默认) 每次启动创建新实例,压入当前栈。 多实例场景下的状态一致性(如填写表单)。
singleTop 若目标 Activity 在栈顶,则复用实例(触发 onNewIntent());否则创建新实例。 通知栏点击打开已存在的页面时是否刷新数据。
singleTask 系统创建新任务或将 Activity 移至现有任务根部。同一任务只存在一个实例。 多任务边界、深度链接跳转后的返回路径是否异常。
singleInstance 独占整个任务,该任务仅容纳此一个 Activity。 与其他应用的交互(如相机调用),返回栈隔离性。
3. 任务管理机制
  • 最近任务列表(Recents)
    • 系统维护任务快照(缩略图 + 描述)。
    • 移除任务卡片会清除整个返回栈(所有 Activity 销毁)。
  • 任务重用(Re-parenting)
    • 当从应用 A 启动应用 B 的 Activity 时:
      • 若 B 已有任务在后台,该 Activity 会移入 B 的任务栈
      • 返回键会先回退到 B 的前一个 Activity,而非回到 A。
  • 后台任务回收
    • 系统内存不足时,按 LRU 规则销毁后台任务栈(保留状态 Bundle 以便重建)。

进程间通信规则

核心思想: 应用运行在独立的进程(沙盒)中,无法直接访问彼此的内存。IPC 提供一种安全的"邮递"机制,让应用可以发送请求(消息、数据、方法调用)并接收响应。

底层核心机制:Binder

  1. 建立邮箱(Binder 驱动): 操作系统内核提供了一个中央"邮局"(Binder 驱动)。所有需要通信的应用(进程)都向这个邮局注册自己的"邮箱地址"(Binder 引用)。
  2. 写信(序列化): 发送方应用(客户端)将想要传递的数据或方法调用请求(包括方法名、参数)序列化 成一个线性格式(通常使用 Parcel)。想象成把信息写在纸上。
  3. 投递到邮局(系统调用): 客户端通过系统调用(ioctl)将打包好的 Parcel 发送给 Binder 驱动。这个调用会指定目标"邮箱地址"(目标服务的 Binder 引用)。
  4. 邮局分拣(内核处理): Binder 驱动在内核空间接收到数据包。它根据目标引用找到接收方应用(服务端)对应的进程和线程信息。
  5. 派送信件(唤醒目标线程): Binder 驱动将数据包放入接收方进程的一个专属接收队列中,并唤醒服务端进程中负责处理 IPC 的线程(通常是主线程或 Binder 线程池中的一个线程)。
  6. 拆信(反序列化): 服务端线程被唤醒,从队列中取出 Parcel,将数据反序列化回原始格式(方法名、参数)。
  7. 处理请求(执行方法): 服务端根据方法名找到对应的实现代码,使用反序列化得到的参数执行该方法。
  8. 写回信(序列化结果): 服务端将方法执行的结果(或异常)再次序列化Parcel
  9. 回信投递(系统调用): 服务端通过另一个系统调用将结果 Parcel 发送回 Binder 驱动
  10. 邮局送回(内核处理): Binder 驱动将结果包放入客户端进程的接收队列,并唤醒等待结果的客户端线程。
  11. 客户端收信(反序列化结果): 客户端线程被唤醒,取出结果 Parcel反序列化得到最终结果或异常。
  12. 客户端处理结果: 客户端继续执行,使用收到的结果。

隐式/显式 Intent

  1. 显式 Intent (点名道姓):

    • 明确知道要启动哪个"人"(组件)干活。
    • 直接告诉系统:"启动 包名 com.example.app 里 类名 com.example.app.MyActivity 这个 Activity!"
    • 用在: 启动自己 App 内部的界面 (Activity)、服务 (Service) 等,或者明确知道另一个 App 里具体哪个组件(需要知道包名和类名)。
    • 优点: 精准、高效。
    • 缺点: 必须知道具体目标,跨 App 启动需要对方暴露组件信息(有时不推荐)。
  2. 隐式 Intent (发广播招人):

    • 只知道要干什么"活"(操作),但不知道谁干。
    • 告诉系统:"我要 查看一张图片 (Action=VIEW, Data=图片URI, Type=image/*)!" 或者 "我要 发送一封邮件 (Action=SEND, Type=text/plain)!"
    • 系统怎么做: 系统拿着你的"招聘要求"(Action, Data, Type, Category等),去查所有 App 的"简历"(在 AndroidManifest.xml 中声明的 <intent-filter>)。找到所有符合条件的组件。
    • 结果:
      • 如果只有一个组件符合:直接启动它。
      • 如果有多个符合:弹出选择器 (Chooser) 让用户选一个。
      • 如果没找到:启动失败。
    • 用在: 启动系统功能(拍照、打电话、选择联系人)、分享内容、打开特定类型文件、让其他 App 提供特定服务等。跨 App 协作的主要方式。
    • 优点: 灵活、解耦。你的 App 不需要知道具体谁来处理。
    • 缺点: 控制权较低(用户可能选错 App),性能略低(需要系统匹配)。
特征 显式 Intent (Explicit Intent) 隐式 Intent (Implicit Intent)
目标指定 点名道姓! setComponent(), setClass()new Intent(Context, Class) 明确指定要启动哪个 App 的哪个 Activity/Service 等。 只提要求! 通过 action (动作,如打电话、发邮件、查看)、data (数据,如网址、电话号码) 和 category (类别) 描述你想做什么
定位方式 精准定位。 就像你知道朋友的具体门牌号去找他。 广播找人。 就像你在广场喊"谁会修电脑?",会修的人(组件)自己响应。
作用范围 通常用于启动自己 App 内部的组件。 因为你知道组件的具体名字。 用于启动自己 App 内部或其他 App 的组件。 是实现不同 App 之间协作的关键。
系统处理 系统直接启动你指定的那个组件。 系统查找所有 声明了能处理该 Intent 要求的 (action + data + category) 的组件,如果有多个,会让用户选择(选择器)。
典型用途 App 内部页面跳转、启动自己 App 的后台 Service。 打开网页、打电话、发邮件、分享内容、选择图片、使用地图等跨 App 或系统级功能
关键优势 精准、高效、安全(不易被劫持)。 灵活、解耦、支持跨应用。
关键风险 只能启动已知组件,灵活性差。 可能找不到匹配组件导致崩溃 (需用 resolveActivity() 检查),或有多个匹配时用户需要选择

一句话总结:

  • 显式 Intent: "张三,你去把这事办了!" (指定具体组件)
  • 隐式 Intent: "谁能办这事? 来个人把它办了!" (声明需求,系统找匹配者)

关键底层点简化:

  • 显式 Intent 直接调用目标组件,不经过系统匹配。
  • 隐式 Intent 依赖系统在安装时收集所有 App 的 <intent-filter> 信息(存储在 PackageManager 数据库里)。启动时,系统根据 Intent 里的信息(主要是 Action + Data/Type)去数据库里快速查找匹配的组件。

View系统与事件分发机制

一、 View 系统:UI 的构建基石

  1. 树形结构:

    • 所有 UI 元素 (Button, TextView, ImageView, 甚至 LinearLayout, RelativeLayout) 都是 View 或其子类 (ViewGroup)。
    • ViewGroup 是特殊的 View,可以包含其他 View (子 View) 或 ViewGroup (子 ViewGroup)。
    • 整个界面是一棵由 ViewViewGroup 组成的树状结构 ,最顶层通常是 DecorView (包含状态栏、标题栏、内容区域),根部是 ActivityWindow
  2. 核心流程:

    • 测量 (Measure): 父 View (ViewGroup) 询问每个子 View:"你需要多大空间? " (考虑自身尺寸要求 wrap_content/match_parent/固定值 和父 View 的约束)。这是一个递归过程,从根 View 开始向下遍历整棵树。
    • 布局 (Layout): 父 View (ViewGroup) 根据测量结果,告诉每个子 View:"你被放在哪里 (左上右下坐标)"。这也是递归过程。
    • 绘制 (Draw): 每个 View 负责绘制自己 到屏幕上指定的矩形区域。流程是从根 View 开始,先绘制背景,再绘制自己内容 (onDraw),然后递归绘制它的所有子 View。遵循顺序:父 View 在底层 -> 子 View 在上层
  3. 关键角色:

    • View UI 基本单元,负责自身绘制和响应触摸事件
    • ViewGroup 特殊的 View,核心职责是容纳和管理子 View
      • 测量子 View (询问大小)。
      • 摆放子 View (决定位置)。
      • 管理事件分发 (决定哪个子 View 能处理触摸事件)。

二、 事件分发机制:触摸事件的旅程

  1. 事件源头: 用户触摸屏幕产生一个 MotionEvent 对象 (包含触摸坐标、动作类型如 ACTION_DOWN/MOVE/UP 等)。

  2. 分发目标: 事件需要找到能"消费 " (处理) 它的 View

  3. 传递路径: 事件从根 View (通常是 DecorView) 开始,沿着 View 树自上而下传递。

    • 事件首先到达最顶层的 ViewGroup (Activity 的根布局)。
    • 然后层层向下传递到可能的子 ViewGroup 或最终的子 View
  4. 核心方法 (决策点): 事件在 ViewViewGroup 之间传递时,关键由三个方法决定去向:

    • dispatchTouchEvent(MotionEvent event) 事件分发入口View/ViewGroup 收到事件后首先调用此方法。

      • View: 检查自身是否可点击/可处理事件,是则尝试 onTouchEvent
      • ViewGroup: 核心逻辑所在地! 它决定:
        • 是否拦截 (onInterceptTouchEvent) 事件,不让子 View 处理。
        • 如果不拦截,则遍历子 View (通常按 Z 序或添加顺序反向遍历,后添加/上层 View 优先),询问子 View 是否愿意处理 (dispatchTouchEvent)。
    • onInterceptTouchEvent(MotionEvent event) ViewGroup 独有!dispatchTouchEvent 内部调用。用于判断当前 ViewGroup 是否要"截胡" 这个事件序列 (从 DOWNUP/CANCEL)。如果返回 true,后续事件不再分发给子 View,直接交给自身的 onTouchEvent 处理。默认返回 false (不拦截)

    • onTouchEvent(MotionEvent event) 事件处理终点View 或拦截了事件的 ViewGroup 在这里真正尝试消费 (处理) 事件 。如果成功处理 (如点击了按钮),返回 true;如果处理不了或不关心,返回 false,事件会向上回溯 给父 View 的 onTouchEvent 尝试处理。

  5. 分发逻辑 (核心流程):

    1. 事件从根 ViewGroupdispatchTouchEvent 开始。
    2. ViewGroup 先调用自己的 onInterceptTouchEvent 看是否拦截。
    3. 如果不拦截
      • 遍历子 View (通常从最上层的子 View 开始)。
      • 判断触摸点是否落在子 View 区域内且子 View 能接收事件。
      • 如果满足,调用子 View 的 dispatchTouchEvent (递归开始)。
    4. 如果拦截所有子 View 都不处理
      • 调用自身的 onTouchEvent 尝试处理。
    5. 如果自身的 onTouchEvent 也不处理 ,事件回传给父 ViewGrouponTouchEvent (向上回溯)。
    6. 如果某个 View 的 onTouchEventACTION_DOWN 时返回 true ,表示它消费了这个事件序列 ,后续的 MOVE/UP 等事件会直接分发给它 (不再询问 onInterceptTouchEvent,可能跳过中间 ViewGroup 的 dispatch 部分逻辑,但流程更高效),直到序列结束 (UP/CANCEL)。

资源管理与适配机制

核心目标: 让同一份 App 代码能优雅地适配不同设备(屏幕尺寸、分辨率、语言、系统版本、横竖屏、夜间模式等)和用户配置(字体大小)。

一、 资源管理:组织与访问

  1. 资源是什么?

    • App 中非代码 的一切:图片 (drawable)、布局 (layout)、字符串 (string)、颜色 (color)、尺寸 (dimen)、样式 (style)、菜单 (menu)、动画 (anim)、原始文件 (raw)、XML 等。
    • 目的: 将 UI 内容、文本、样式等与 Java/Kotlin 代码逻辑分离,便于修改、复用和适配。
  2. 资源存放 (res/ 目录):

    • 按类型分目录: res/drawable/, res/layout/, res/values/, res/menu/ 等。这是基本组织方式。
    • 关键:资源限定符 (Qualifiers): 核心适配机制!
      • 在目录名后添加后缀 来指定资源适用的特定条件
      • 格式: 资源类型-限定符1-限定符2-... (例如:drawable-hdpi, layout-sw600dp-land, values-en-rUS)。
      • 系统自动选择: 运行时,Android 系统根据设备的当前配置 (语言、屏幕尺寸、横竖屏、夜间模式等),自动选择 最匹配限定符目录下的资源。如果没有完全匹配,会寻找最接近的或默认目录 (drawable/, values/ 等) 的资源。
      • 优先级: 系统按预定义规则评估多个限定符的优先级(如屏幕尺寸优先级高于语言)。
  3. 资源编译与访问:

    • 编译: aapt2 (Android Asset Packaging Tool) 将 res/ 下资源编译打包进 APK,并生成 R.java (或 R.kt) 文件。
    • 访问 (代码中): 通过自动生成的 R 类访问资源 (如 R.drawable.icon, R.string.app_name, R.layout.activity_main)。
    • 访问 (XML 中): 使用 @ 符号引用 (如 @drawable/icon, @string/hello, @dimen/padding_medium)。

二、 适配机制:应对多样性

  1. 屏幕适配:

    • 核心理念:密度无关 (Density-Independent)
      • dp (Density-independent Pixels): 长度/尺寸单位。 1dp 在屏幕密度为 160dpi (基准密度) 的设备上等于 1px。系统会根据实际屏幕密度自动缩放。应始终用于指定 View 尺寸和边距!
      • sp (Scale-independent Pixels): 字体大小单位。 类似 dp,但会额外尊重用户系统的字体大小设置应始终用于字体大小!
      • 避免 px (Pixels): 直接对应屏幕物理像素,在不同密度屏幕上显示大小不一致。
    • 布局适配:
      • 限定符: 使用 smallestWidth (sw<N>dp,如 sw600dp 用于 7 寸平板)、screen size (small, normal, large, xlarge - 已弃用,推荐 sw)、screen orientation (land 横屏, port 竖屏) 为不同屏幕尺寸/方向提供不同的布局文件。
      • 响应式布局设计: 使用 ConstraintLayoutLinearLayout (权重 weight)、RelativeLayout 等构建能弹性伸缩和重新排列 的布局。优先考虑 match_parent, wrap_content 和约束关系。
      • 使用 dimens.xml 为不同屏幕尺寸定义不同的尺寸值 (使用限定符目录)。
  2. 语言/区域适配:

    • 限定符: 使用语言代码 (en, zh)、区域代码 (rUS, rCN) 创建不同的 values-<qualifier> 目录 (如 values-en/, values-zh-rCN/)。
    • 存放内容: 在对应的 values-<qualifier>/strings.xml 等文件中放置翻译好的字符串、本地化的图片引用、日期/货币格式等。
    • 自动切换: 系统根据用户设备的语言/区域设置,自动加载匹配的字符串资源。
  3. 夜间模式/主题适配:

    • 限定符: 使用 night (values-night/, drawable-night/)。
    • 主题属性:styles.xml 中定义主题,使用主题属性 (?attr/colorPrimary) 引用颜色等资源,而非硬编码。在日间/夜间主题中为同一属性指定不同的颜色值。
    • 动态切换: AppCompatDelegate.setDefaultNightMode() 允许 App 内动态切换日/夜模式。
  4. API 版本适配:

    • 限定符: 使用 v<N> (如 drawable-v21/) 提供只在特定 API 级别及以上可用的资源(如 Vector Drawables, 特定主题属性)。
    • 代码检查: 在 Java/Kotlin 代码中使用 Build.VERSION.SDK_INT 判断系统版本,决定是否使用新 API 或提供兼容方案。

权限机制

核心目标: 保护用户隐私和设备安全,防止 App 随意访问敏感数据(如位置、通讯录、短信)或执行危险操作(如打电话、录音、访问外部存储)。

核心原则: 最小权限原则 - App 只能获取其明确声明且用户明确授权的权限。

一、 权限分类(按获取时机与方式):

  1. 安装时权限 (Install-Time Permissions / Normal Permissions):

    • 特点: 涉及低风险操作,对用户隐私或设备操作影响极小。
    • 获取方式: 在 App 安装时,系统自动授予(用户无需额外操作)。用户无法在安装后单独撤销这些权限。
    • 例子: 设置时区 (android.permission.SET_TIME_ZONE)、访问网络 (android.permission.INTERNET)、蓝牙 (android.permission.BLUETOOTH)、振动 (android.permission.VIBRATE)。
  2. 运行时权限 (Runtime Permissions / Dangerous Permissions):

    • 特点: 涉及高风险 操作,直接访问用户隐私数据或影响设备安全/其他 App 操作这是权限机制的核心和重点!
    • 获取方式 (关键流程):
      1. 声明:AndroidManifest.xml 中声明需要的权限 (如 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>)。
      2. 检查: 在代码中执行需要该权限的操作之前 ,使用 ContextCompat.checkSelfPermission(Context, permissionString) 检查该权限是否已被授予。
      3. 请求:
        • 如果未授予 ,调用 ActivityCompat.requestPermissions(Activity, new String[]{permissionString}, requestCode) 向用户弹出系统对话框请求授权
        • 用户可以选择 允许拒绝
      4. 处理结果: 在 Activity/Fragment 中重写 onRequestPermissionsResult(requestCode, permissions[], grantResults[]) 方法,处理用户的授权选择结果。
    • 关键点:
      • 用户控制: 用户可以在系统 设置 > 应用 > 权限 中随时授予或撤销这些权限。
      • 临时拒绝 (Ask Every Time): 用户首次拒绝时,系统可能会提供"仅此一次"或"使用时允许"的选项(取决于权限类型和系统版本)。如果用户选择了 拒绝 并且 勾选了 不再询问 (或等效选项),后续请求将直接失败。
      • 权限组: 运行时权限被分组管理(如 位置 组包含 ACCESS_FINE_LOCATIONACCESS_COARSE_LOCATION)。一旦用户授予了组内某个权限,再次请求组内其他权限时系统会自动授予(不会弹窗)。 但最佳实践仍是显式请求所需的所有权限。
    • 例子: 相机 (CAMERA)、位置 (ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION)、通讯录 (READ_CONTACTS)、麦克风 (RECORD_AUDIO)、短信 (SEND_SMS)、日历 (READ_CALENDAR)、存储 (READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE - 注意 Scoped Storage 限制)。
  3. 特殊权限 (Special Permissions):

    • 特点: 权限行为非常特殊,不在标准运行时权限流程内 。通常涉及系统级设置或深度集成
    • 获取方式: 无法通过 requestPermissions() 获取! 需要引导用户跳转到特定的系统设置页面 (Settings.ACTION_APPLICATION_DETAILS_SETTINGS 或其他特定 ACTION_..._SETTINGS) 去手动开启。
    • 例子: 悬浮窗 (SYSTEM_ALERT_WINDOW)、修改系统设置 (WRITE_SETTINGS)、精确闹钟 (SCHEDULE_EXACT_ALARM - Android 12+)、电池优化忽略 (REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)。
  4. 签名权限 (Signature Permissions):

    • 特点: 主要用于系统 App 或由同一开发者签名的 App 之间进行受保护的交互。
    • 获取方式: 如果 App 的签名证书与声明该权限的 App/系统的签名证书匹配,则系统会在安装时自动授予。
    • 开发者控制: 普通开发者一般无法定义或使用新的签名权限,主要用于平台或预装应用。

二、 关键机制与最佳实践:

  1. AndroidManifest.xml 声明是必须的: 任何权限(尤其是运行时权限)都必须先在清单文件中声明,否则系统不会授予(即使代码请求了)。
  2. 按需请求: 只在真正需要执行相关操作时才请求权限。避免在启动时请求一堆权限("权限轰炸"),这会让用户反感并卸载 App。
  3. 解释为什么需要权限: 在请求权限前(尤其是用户可能不理解为什么需要时),使用 ActivityCompat.shouldShowRequestPermissionRationale(Activity, permissionString) 检查是否需要向用户解释 。如果需要,先弹出自定义对话框解释清楚、简洁 的原因,解释完后再调用 requestPermissions()
  4. 优雅处理拒绝:
    • 如果用户拒绝 (未勾选"不再询问"),可以在后续合适时机再次请求(并附带解释)。
    • 如果用户永久拒绝 (勾选"不再询问"),应引导用户到 App 的设置页面 (Settings.ACTION_APPLICATION_DETAILS_SETTINGS) 手动开启权限,并禁用依赖该权限的功能(而不是崩溃或反复弹窗)。
  5. 权限组意识: 了解权限分组,但不要依赖自动授予行为作为不请求权限的理由。始终请求你需要的具体权限。
  6. 适配新版本: 关注新 Android 版本(如 11, 12, 13, 14)对权限模型的更新(如后台位置访问限制、照片选择器、邻近 Wi-Fi 权限、通知权限等),及时调整 App 逻辑。
  7. 存储权限 (READ/WRITE_EXTERNAL_STORAGE) 的演变:
    • Android 10 (API 29) 引入 Scoped Storage: 限制 App 随意访问外部存储其他 App 的私有文件。强调使用 MediaStore API 访问媒体文件和 SAF (Storage Access Framework) 访问特定文档/目录。
    • Android 11 (API 30) 及以后: 进一步收紧,MANAGE_EXTERNAL_STORAGE 成为特殊权限(需跳转设置),普通 App 应尽量避免使用。优先使用 App 专属目录 (Context.getExternalFilesDir()) 和共享存储 API (MediaStore, SAF)。

存储机制

核心目标: 在保护用户隐私和数据安全的前提下,为 App 提供可靠的文件存储能力,并实现不同 App 之间的安全数据共享。

核心挑战: 平衡 App 功能需求与用户数据安全/隐私,尤其在设备文件系统日益复杂和恶意软件威胁下。

一、 关键演变:从自由到严格(Scoped Storage 为核心)

  1. Android 10 (API 29) 之前:相对自由

    • WRITE_EXTERNAL_STORAGE 权限 = 万能钥匙: 一旦用户授予,App 几乎可以读写整个外部存储(SD卡和内置存储的公共部分)的任何文件 ,包括其他 App 的私有文件。隐私泄露风险高!
  2. Android 10 (API 29) 引入 Scoped Storage (分区存储):重大变革!

    • 核心理念: 限制 App 随意扫描整个存储空间,保护用户隐私和其他 App 的数据。
    • 关键变化:
      • 默认作用域: App 默认只能无需权限 访问:
        • 自身专属的外部存储目录 (Context.getExternalFilesDir(), Context.getExternalCacheDir()) :存放 App 私有文件,卸载时会被删除。这是首选存放位置。
        • 特定类型的媒体文件 (图片、视频、音频) :但必须通过 MediaStore API 访问(需要运行时权限 READ_EXTERNAL_STORAGE 来读取其他 App 创建的媒体文件)。
      • WRITE_EXTERNAL_STORAGE 权限作用大幅缩减: 在 Android 10 上,它主要允许写入 MediaStore不再能随意写任何地方!
      • 访问其他 App 的私有目录或非媒体文件: 必须使用 Storage Access Framework (SAF) (系统文件选择器)。
  3. Android 11 (API 30) 及以后:强化与完善

    • 进一步限制: READ_EXTERNAL_STORAGE 权限也受到更严格限制。
    • MANAGE_EXTERNAL_STORAGE 成为特殊权限: 提供给文件管理器、备份恢复等需要广泛文件访问的特定类型 App 。普通 App 强烈不建议申请,上架应用商店审核严格且用户授权率极低。需要引导用户跳转到系统设置手动开启。
    • 文件访问意图更明确:
      • 媒体文件: 优先且主要使用 MediaStore
      • 文档/其他文件: 优先使用 Storage Access Framework (SAF)
      • App 自身文件: 使用 App 专属目录

通知机制

📣 核心流程(简单版)

  1. APP想通知你: 某个应用(比如微信、邮箱、游戏)发生了需要你注意的事情(新消息、下载完成、系统提醒)。
  2. APP打包"通知": APP按照安卓系统的规定,创建一个通知对象 (Notification) 。这个对象包含:
    • 小图标 (Small Icon): 在状态栏显示的小图(必须)。
    • 标题 (Title): 通知的主题(比如"新消息"、"下载完成")。
    • 内容文本 (Content Text): 通知的详细内容(比如"张三:晚上吃饭吗?")。
    • 大图标 (Large Icon - 可选): 展开通知后显示的大图(比如发信人头像)。
    • 优先级 (Priority): 告诉系统这个通知有多紧急(高、中、低等,影响显示位置和是否响铃)。
    • 点击动作 (PendingIntent): 最关键!你点击通知后要做什么?(比如打开聊天窗口、跳转到邮件详情、播放音乐)。
    • 渠道 (Channel - Android 8.0+ 必须): 通知的分类 (比如微信可以有"新消息"、"群通知"、"公众号更新"等不同渠道)。用户可以根据渠道单独设置开关和提醒方式!
    • 其他花活 (可选): 进度条、按钮(快速回复、标记已读)、图片、媒体控制等。
  3. APP把通知"递"给系统: APP调用 NotificationManager.notify(id, notification) 方法,把这个打包好的通知对象交给安卓系统的 通知管理器 (Notification Manager)
  4. 系统"展示"通知:
    • 状态栏图标: 通知的小图标会出现在屏幕顶部的状态栏。
    • 通知抽屉: 下拉状态栏,你会看到通知的详细列表(标题、内容、图标等)。
    • 提醒方式 (根据用户设置):
      • 声音 (Sound): 播放提示音。
      • 震动 (Vibrate): 手机震动。
      • 呼吸灯 (Lights - 如果手机有): 闪烁指示灯。
      • 浮动通知/弹窗 (Heads-up - 高优先级): 在屏幕顶部短暂弹出(不影响当前操作)。
    • 锁屏显示 (根据用户设置): 通知内容可能显示在锁屏上(注意隐私)。

🔑 关键机制和规则

  1. 通知渠道 (Android 8.0 Oreo 引入):

    • 核心思想: 让用户精细控制通知! 不再是"整个APP的通知要么全开要么全关"。
    • APP的责任: APP必须 为不同类型的通知创建不同的渠道 (Channel) (比如"交易提醒"、"营销推送"、"聊天消息")。
    • 用户的权力: 用户可以单独 为每个渠道设置:
      • 开关: 是否允许显示。
      • 提醒方式: 是否响铃、震动、浮动显示、在锁屏显示。
      • 重要性 (Importance Level): 决定通知的干扰程度(紧急、高、中、低)。
    • 好处: 用户能屏蔽烦人的广告推送,但保留重要的聊天消息提醒。
  2. 通知权限:

    • Android 13 (Tiramisu) 之前: APP安装后默认可以发通知。
    • Android 13 及以后: 新增运行时权限 POST_NOTIFICATIONS
      • 当APP第一次尝试发通知时,系统会弹窗询问用户**"是否允许 [APP名称] 发送通知?"**。
      • 用户可以选择 "允许""不允许"
      • 开发者注意: 必须适配!用户拒绝后,调用 notify() 会失效。
  3. 勿扰模式 (Do Not Disturb):

    • 用户可以开启"勿扰模式"(手动或按计划)。
    • 在该模式下,只有被用户标记为"允许打扰" 的APP或联系人的通知(通常是最高优先级或特殊渠道)才会发出声音/震动,其他通知会静默进入通知抽屉。
  4. 后台限制 (省电优化):

    • 安卓系统(尤其国产定制系统)对APP在后台运行有严格限制,防止耗电。
    • 影响: 如果APP被系统"杀掉"或在后台被严格限制,它可能无法及时触发后台服务来发送通知
    • 解决方案 (给开发者):
      • 使用 WorkManager 安排可靠的后台任务(系统会找合适时机运行)。
      • 使用厂商推送服务 (如小米推送、华为推送、FCM) 替代APP自己维持长连接(更省电,推送更可靠)。
      • 引导用户将APP加入"电池优化白名单"或"允许后台运行"(效果因厂商而异)。
  5. 通知分组和摘要 (Android 7.0+):

    • 分组 (Grouping): 同一个APP的多个通知(比如多封未读邮件)可以被折叠成一个"组"显示,点击组再展开详情。避免通知栏被刷屏。
    • 摘要 (Bundling/Summary): 可以为分组提供一个摘要通知(比如"5条新消息")。
  6. 长连接与推送服务:

    • APP主动拉取 (Polling): APP定期去服务器检查新消息(耗电、不实时)。
    • 长连接 (Persistent Connection): APP在后台和服务器保持一个连接,服务器有新消息可以立刻推给APP,APP再发通知(更实时,但APP需后台保活,可能被系统限制)。
    • 统一推送服务 (FCM/厂商推送): 最佳实践!
      • APP不需要自己维持长连接。
      • 服务器把通知消息发给 Google 的 Firebase Cloud Messaging (FCM)手机厂商的推送服务器 (如小米推送、华为推送)
      • FCM/厂商服务器利用系统级的、更省电的长连接通道,将消息推送到用户设备
      • 设备系统收到后,直接唤醒目标APP或代表APP弹出通知(无需APP后台运行)。
      • 好处: 省电、推送可靠、及时。

后台执行限制

核心就是 "系统如何管住APP在后台偷偷搞事情" 的规则,目的是 省电、省流量、保流畅、护隐私


🛑 核心目标:限制APP在后台干啥?

系统想阻止APP在你不用它的时候:

  • 狂耗电: 后台不断联网、定位、计算。
  • 偷跑流量: 后台疯狂上传下载。
  • 拖慢手机: 后台占用CPU和内存,让你用前台APP时卡顿。
  • 偷偷收集数据: 后台扫描位置、读取文件、监听传感器。

🔒 主要限制手段(不同安卓版本不断加码)

1. 后台服务限制 (Android 8.0 Oreo 起关键变化)
  • 以前: APP可以轻松在后台启动一个Service(服务)长期运行(比如放音乐、下载文件、定时同步)。
  • 现在 (Android 8.0+):
    • 前台服务 (Foreground Service): 如果APP需要在后台做用户可感知需要持续运行 的任务(如音乐播放、导航、文件下载),必须启动一个前台服务!
      • 特点: 必须在状态栏显示一个常驻通知(告诉用户"我正在后台工作呢!")。
      • 好处: 用户知道谁在耗电,也能手动划掉通知停止它。
    • 后台服务 (Background Service):
      • APP在前台或刚退到后台: 可以正常启动和使用后台服务(有短暂宽限期)。
      • APP在后台一段时间后: 系统会强制停止APP的所有后台服务! APP想再启动新服务?门都没有!
  • 开发者应对: 需要长时间后台任务?用前台服务(配通知)!或者用更智能的调度方式(如WorkManager)。
2. 广播接收器限制 (Android 8.0+)
  • 广播 (Broadcast): 系统或APP发出的全局事件(比如开机完成、网络变化、充电中)。
  • 以前: APP可以注册监听很多广播(即使没在运行),一收到广播就能被唤醒干活。
  • 现在 (Android 8.0+):
    • 显式广播 (Explicit Broadcast): 发给特定APP的广播,基本不受限。
    • 隐式广播 (Implicit Broadcast): 发给所有APP的全局广播(如 ACTION_BOOT_COMPLETED 开机完成、CONNECTIVITY_CHANGE 网络变化)受到严格限制
      • 静态注册 (Manifest 里声明): 大部分隐式广播收不到了!只有少数系统白名单广播例外(如开机完成,但应用首次启动后也收不到了)。
      • 动态注册 (代码里注册): APP在前台时能收到,退到后台后就收不到了
  • 目的: 防止一堆APP被无关紧要的全局广播频繁唤醒。
  • 开发者应对: 避免依赖隐式广播唤醒后台任务。用JobScheduler/WorkManager替代。
3. 后台位置访问限制 (Android 10+ 大幅收紧)
  • 以前: APP在后台可以相对容易地获取用户位置。
  • 现在 (Android 10+):
    • 新增权限: ACCESS_BACKGROUND_LOCATION (后台位置权限)。
    • 用户授权更严格: 用户必须在设置页里单独授予这个权限(不像前台位置权限那样在运行时弹窗就能给)。
    • 前台服务要求: 即使有后台位置权限,APP在后台持续 获取位置信息时,也必须启动一个前台服务(并显示通知告知用户)。
  • 目的: 防止APP在后台偷偷追踪用户位置,严重侵犯隐私。
  • 开发者应对: 非导航/运动类APP,强烈建议避免在后台获取位置。如必须,请求后台权限并配前台服务+通知。
4. 后台网络访问限制 (Android 7.0+ Doze & App Standby)
  • Doze 模式 (打盹模式 - Android 6.0+):
    • 触发: 手机灭屏、静置、未充电一段时间后。
    • 限制:
      • 暂停所有后台网络访问(WiFi和移动数据)。
      • 延迟所有后台JobScheduler任务、SyncAdapter同步、AlarmManager闹钟(非精确闹钟)。
      • 禁止后台服务启动。
    • 维护窗口 (Maintenance Window): 系统会周期性地短暂退出Doze(例如每小时一次),让被延迟的任务有机会执行。执行完又进入Doze。
  • App Standby (应用待机桶 - Android 6.0+):
    • 触发: 用户长时间没用某个APP。
    • 限制: 将该APP放入限制桶 (Restricted Bucket)
      • 大幅限制后台网络访问
      • 延迟后台任务(JobScheduler/SyncAdapter)。
      • 禁止后台服务启动。
    • 用户唤醒: 只要用户手动启动了该APP,它立刻跳出限制桶,恢复所有能力。
  • 目的: 限制不常用APP在后台偷跑网络和资源。
  • 开发者应对: 使用WorkManager调度网络任务(它知道如何应对Doze和待机桶)。避免在后台做不必要的网络请求。
5. 厂商定制系统的"魔改" (尤其国内 ROM)
  • 更激进! 小米、华为、OPPO、vivo 等国产手机的系统,后台限制往往比原生安卓更狠
  • 常见手段:
    • 自动启动管理: 默认禁止APP开机自启、被其他APP唤醒(链式启动)。
    • 后台运行管理: 锁屏后几分钟就清理后台APP进程和服务(即使你设置了前台服务通知也可能被清!)。
    • 省电优化/电池管理: 用户必须手动将APP加入"白名单"、"允许后台运行"、"允许关联启动"、"忽略电池优化",否则后台任务几乎无法运行。
    • 对齐唤醒: 强制所有APP的唤醒请求集中到某个时间点执行,减少频繁唤醒。
  • 结果: 用户省电效果可能更好,但开发者适配极其痛苦 ,后台任务可靠性严重依赖用户手动设置白名单
相关推荐
fatiaozhang95272 小时前
创维智能融合终端SK-M424_S905L3芯片_2+8G_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
来来走走3 小时前
Flutter开发 了解Scaffold
android·开发语言·flutter
闻道且行之5 小时前
Android Studio下载及安装配置
android·ide·android studio
alexhilton5 小时前
初探Compose中的着色器RuntimeShader
android·kotlin·android jetpack
小墙程序员5 小时前
kotlin元编程(二)使用 Kotlin 来生成源代码
android·kotlin·android studio
小墙程序员5 小时前
kotlin元编程(一)一文理解 Kotlin 反射
android·kotlin·android studio
fatiaozhang95276 小时前
创维智能融合终端DT741_移动版_S905L3芯片_安卓9_线刷固件包
android·电视盒子·刷机固件·机顶盒刷机
小林学Android8 小时前
Android四大组件之Activity详解
android
搬砖不得颈椎病9 小时前
Jetpack DataStore vs SharedPreferences:现代Android数据存储方案对比
android