Activity 生命周期
应用内 Activity 跳转流程(A → B)
从 Activity A 打开新的 Activity B(如点击按钮跳转详情页)
- A.onCreate() → A.onStart() → A.onResume() (A 已在前台)
- 点击跳转按钮 → A.onPause() (A 暂停但仍可见)
- B.onCreate() → B.onStart() → B.onResume() (B 进入前台)
- A.onStop() (A 完全不可见,但未被销毁)
返回键关闭当前 Activity(B → A)
在 Activity B 中按返回键,回到 Activity A
- 按返回键 → B.onPause()
- A.onRestart() → A.onStart() → A.onResume() (A 重新可见)
- B.onStop() → B.onDestroy() (B 被销毁)
Home 键切到后台(应用存活)
在 Activity A 运行时按 Home 键回到桌面
- 按 Home 键 → A.onPause() → A.onStop()
(注意:此时 A 未被销毁,进程存活)
切换到其他应用(如从微信跳转到支付宝)
从当前应用 Activity A 打开另一个应用(如点击链接跳转支付宝)
- 点击跳转 → A.onPause()
- 支付宝冷/温启动 → 支付宝页面显示
- A.onStop() (A 完全不可见,但进程存活)
后台被系统回收后恢复(温启动场景)
应用在后台时,因内存不足被系统回收 Activity(非杀进程),用户再次点击图标进入
- 系统回收 Activity → 调用
A.onSaveInstanceState()
保存数据 - 用户点击图标 → 重建 Activity A:
A.onCreate(savedInstanceState)
→A.onStart()
→A.onResume()
返回键退出应用(销毁所有 Activity)
在首页 Activity A 按返回键退出应用
- 按返回键 → A.onPause() → A.onStop() → A.onDestroy()
- 进程仍存活(系统缓存),但任务栈清空
任务(Task)和返回栈(Back Stack)
一、核心概念
-
任务(Task):
- 本质:用户为完成特定目标(如“写邮件”、“购物”)而交互的 Activity 集合。
- 表现形式:一个按打开顺序排列的 Activity 栈(即返回栈)。
- 系统级标识:每个任务有独立 任务 ID,系统通过它管理任务切换。
- 用户视角:在“最近任务列表”(Recents Screen)中显示为独立卡片。
-
返回栈(Back Stack):
- 本质:属于同一任务的 Activity 实例的有序栈(后进先出)。
- 关键规则:用户按返回键时,栈顶 Activity 出栈并销毁,前一个 Activity 恢复显示。
- 跨进程支持:栈内 Activity 可来自不同应用(如从浏览器打开地图应用)。
二、底层工作原理
1. Activity 启动与入栈
- 默认行为:新启动的 Activity 被压入当前任务的栈顶(
standard
启动模式)。 - 任务亲和性(Task Affinity):
- 每个 Activity 通过
android:taskAffinity
属性声明“归属偏好”。 - 默认亲和性 = 应用包名(同一应用 Activity 通常属于同一任务)。
- 每个 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。
- 当从应用 A 启动应用 B 的 Activity 时:
- 后台任务回收:
- 系统内存不足时,按 LRU 规则销毁后台任务栈(保留状态 Bundle 以便重建)。
进程间通信规则
核心思想: 应用运行在独立的进程(沙盒)中,无法直接访问彼此的内存。IPC 提供一种安全的“邮递”机制,让应用可以发送请求(消息、数据、方法调用)并接收响应。
底层核心机制:Binder
- 建立邮箱(Binder 驱动): 操作系统内核提供了一个中央“邮局”(Binder 驱动)。所有需要通信的应用(进程)都向这个邮局注册自己的“邮箱地址”(Binder 引用)。
- 写信(序列化): 发送方应用(客户端)将想要传递的数据或方法调用请求(包括方法名、参数)序列化成一个线性格式(通常使用
Parcel
)。想象成把信息写在纸上。 - 投递到邮局(系统调用): 客户端通过系统调用(
ioctl
)将打包好的Parcel
发送给 Binder 驱动。这个调用会指定目标“邮箱地址”(目标服务的 Binder 引用)。 - 邮局分拣(内核处理): Binder 驱动在内核空间接收到数据包。它根据目标引用找到接收方应用(服务端)对应的进程和线程信息。
- 派送信件(唤醒目标线程): Binder 驱动将数据包放入接收方进程的一个专属接收队列中,并唤醒服务端进程中负责处理 IPC 的线程(通常是主线程或 Binder 线程池中的一个线程)。
- 拆信(反序列化): 服务端线程被唤醒,从队列中取出
Parcel
,将数据反序列化回原始格式(方法名、参数)。 - 处理请求(执行方法): 服务端根据方法名找到对应的实现代码,使用反序列化得到的参数执行该方法。
- 写回信(序列化结果): 服务端将方法执行的结果(或异常)再次序列化成
Parcel
。 - 回信投递(系统调用): 服务端通过另一个系统调用将结果
Parcel
发送回 Binder 驱动。 - 邮局送回(内核处理): Binder 驱动将结果包放入客户端进程的接收队列,并唤醒等待结果的客户端线程。
- 客户端收信(反序列化结果): 客户端线程被唤醒,取出结果
Parcel
,反序列化得到最终结果或异常。 - 客户端处理结果: 客户端继续执行,使用收到的结果。
隐式/显式 Intent
-
显式 Intent (点名道姓):
- 明确知道要启动哪个“人”(组件)干活。
- 直接告诉系统:“启动 包名 com.example.app 里 类名 com.example.app.MyActivity 这个 Activity!”
- 用在: 启动自己 App 内部的界面 (Activity)、服务 (Service) 等,或者明确知道另一个 App 里具体哪个组件(需要知道包名和类名)。
- 优点: 精准、高效。
- 缺点: 必须知道具体目标,跨 App 启动需要对方暴露组件信息(有时不推荐)。
-
隐式 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 的构建基石
-
树形结构:
- 所有 UI 元素 (
Button
,TextView
,ImageView
, 甚至LinearLayout
,RelativeLayout
) 都是View
或其子类 (ViewGroup
)。 ViewGroup
是特殊的View
,可以包含其他View
(子 View) 或ViewGroup
(子 ViewGroup)。- 整个界面是一棵由
View
和ViewGroup
组成的树状结构,最顶层通常是DecorView
(包含状态栏、标题栏、内容区域),根部是Activity
的Window
。
- 所有 UI 元素 (
-
核心流程:
- 测量 (
Measure
): 父 View (ViewGroup
) 询问每个子 View:“你需要多大空间?” (考虑自身尺寸要求wrap_content
/match_parent
/固定值 和父 View 的约束)。这是一个递归过程,从根 View 开始向下遍历整棵树。 - 布局 (
Layout
): 父 View (ViewGroup
) 根据测量结果,告诉每个子 View:“你被放在哪里 (左上右下坐标)”。这也是递归过程。 - 绘制 (
Draw
): 每个 View 负责绘制自己到屏幕上指定的矩形区域。流程是从根 View 开始,先绘制背景,再绘制自己内容 (onDraw
),然后递归绘制它的所有子 View。遵循顺序:父 View 在底层 -> 子 View 在上层。
- 测量 (
-
关键角色:
View
: UI 基本单元,负责自身绘制和响应触摸事件。ViewGroup
: 特殊的View
,核心职责是容纳和管理子 View:- 测量子 View (询问大小)。
- 摆放子 View (决定位置)。
- 管理事件分发 (决定哪个子 View 能处理触摸事件)。
二、 事件分发机制:触摸事件的旅程
-
事件源头: 用户触摸屏幕产生一个
MotionEvent
对象 (包含触摸坐标、动作类型如ACTION_DOWN
/MOVE
/UP
等)。 -
分发目标: 事件需要找到能“消费” (处理) 它的
View
。 -
传递路径: 事件从根 View (通常是
DecorView
) 开始,沿着 View 树自上而下传递。- 事件首先到达最顶层的
ViewGroup
(Activity 的根布局)。 - 然后层层向下传递到可能的子
ViewGroup
或最终的子View
。
- 事件首先到达最顶层的
-
核心方法 (决策点): 事件在
View
和ViewGroup
之间传递时,关键由三个方法决定去向:-
dispatchTouchEvent(MotionEvent event)
: 事件分发入口。View
/ViewGroup
收到事件后首先调用此方法。View
: 检查自身是否可点击/可处理事件,是则尝试onTouchEvent
。ViewGroup
: 核心逻辑所在地! 它决定:- 是否拦截 (
onInterceptTouchEvent
) 事件,不让子 View 处理。 - 如果不拦截,则遍历子 View (通常按 Z 序或添加顺序反向遍历,后添加/上层 View 优先),询问子 View 是否愿意处理 (
dispatchTouchEvent
)。
- 是否拦截 (
-
onInterceptTouchEvent(MotionEvent event)
:ViewGroup
独有! 在dispatchTouchEvent
内部调用。用于判断当前ViewGroup
是否要“截胡” 这个事件序列 (从DOWN
到UP
/CANCEL
)。如果返回true
,后续事件不再分发给子 View,直接交给自身的onTouchEvent
处理。默认返回false
(不拦截)。 -
onTouchEvent(MotionEvent event)
: 事件处理终点。View
或拦截了事件的ViewGroup
在这里真正尝试消费 (处理) 事件。如果成功处理 (如点击了按钮),返回true
;如果处理不了或不关心,返回false
,事件会向上回溯给父 View 的onTouchEvent
尝试处理。
-
-
分发逻辑 (核心流程):
- 事件从根
ViewGroup
的dispatchTouchEvent
开始。 - 根
ViewGroup
先调用自己的onInterceptTouchEvent
看是否拦截。 - 如果不拦截:
- 遍历子 View (通常从最上层的子 View 开始)。
- 判断触摸点是否落在子 View 区域内且子 View 能接收事件。
- 如果满足,调用子 View 的
dispatchTouchEvent
(递归开始)。
- 如果拦截或所有子 View 都不处理:
- 调用自身的
onTouchEvent
尝试处理。
- 调用自身的
- 如果自身的
onTouchEvent
也不处理,事件回传给父 ViewGroup 的onTouchEvent
(向上回溯)。 - 如果某个 View 的
onTouchEvent
在ACTION_DOWN
时返回true
,表示它消费了这个事件序列,后续的MOVE
/UP
等事件会直接分发给它 (不再询问onInterceptTouchEvent
,可能跳过中间 ViewGroup 的dispatch
部分逻辑,但流程更高效),直到序列结束 (UP
/CANCEL
)。
- 事件从根
资源管理与适配机制
核心目标: 让同一份 App 代码能优雅地适配不同设备(屏幕尺寸、分辨率、语言、系统版本、横竖屏、夜间模式等)和用户配置(字体大小)。
一、 资源管理:组织与访问
-
资源是什么?
- App 中非代码的一切:图片 (
drawable
)、布局 (layout
)、字符串 (string
)、颜色 (color
)、尺寸 (dimen
)、样式 (style
)、菜单 (menu
)、动画 (anim
)、原始文件 (raw
)、XML 等。 - 目的: 将 UI 内容、文本、样式等与 Java/Kotlin 代码逻辑分离,便于修改、复用和适配。
- App 中非代码的一切:图片 (
-
资源存放 (
res/
目录):- 按类型分目录:
res/drawable/
,res/layout/
,res/values/
,res/menu/
等。这是基本组织方式。 - 关键:资源限定符 (Qualifiers): 核心适配机制!
- 在目录名后添加后缀来指定资源适用的特定条件。
- 格式:
资源类型-限定符1-限定符2-...
(例如:drawable-hdpi
,layout-sw600dp-land
,values-en-rUS
)。 - 系统自动选择: 运行时,Android 系统根据设备的当前配置(语言、屏幕尺寸、横竖屏、夜间模式等),自动选择最匹配限定符目录下的资源。如果没有完全匹配,会寻找最接近的或默认目录 (
drawable/
,values/
等) 的资源。 - 优先级: 系统按预定义规则评估多个限定符的优先级(如屏幕尺寸优先级高于语言)。
- 按类型分目录:
-
资源编译与访问:
- 编译:
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
)。
- 编译:
二、 适配机制:应对多样性
-
屏幕适配:
- 核心理念:密度无关 (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
竖屏) 为不同屏幕尺寸/方向提供不同的布局文件。 - 响应式布局设计: 使用
ConstraintLayout
、LinearLayout
(权重weight
)、RelativeLayout
等构建能弹性伸缩和重新排列的布局。优先考虑match_parent
,wrap_content
和约束关系。 - 使用
dimens.xml
: 为不同屏幕尺寸定义不同的尺寸值 (使用限定符目录)。
- 限定符: 使用
- 核心理念:密度无关 (Density-Independent)
-
语言/区域适配:
- 限定符: 使用语言代码 (
en
,zh
)、区域代码 (rUS
,rCN
) 创建不同的values-<qualifier>
目录 (如values-en/
,values-zh-rCN/
)。 - 存放内容: 在对应的
values-<qualifier>/strings.xml
等文件中放置翻译好的字符串、本地化的图片引用、日期/货币格式等。 - 自动切换: 系统根据用户设备的语言/区域设置,自动加载匹配的字符串资源。
- 限定符: 使用语言代码 (
-
夜间模式/主题适配:
- 限定符: 使用
night
(values-night/
,drawable-night/
)。 - 主题属性: 在
styles.xml
中定义主题,使用主题属性 (?attr/colorPrimary
) 引用颜色等资源,而非硬编码。在日间/夜间主题中为同一属性指定不同的颜色值。 - 动态切换:
AppCompatDelegate.setDefaultNightMode()
允许 App 内动态切换日/夜模式。
- 限定符: 使用
-
API 版本适配:
- 限定符: 使用
v<N>
(如drawable-v21/
) 提供只在特定 API 级别及以上可用的资源(如 Vector Drawables, 特定主题属性)。 - 代码检查: 在 Java/Kotlin 代码中使用
Build.VERSION.SDK_INT
判断系统版本,决定是否使用新 API 或提供兼容方案。
- 限定符: 使用
权限机制
核心目标: 保护用户隐私和设备安全,防止 App 随意访问敏感数据(如位置、通讯录、短信)或执行危险操作(如打电话、录音、访问外部存储)。
核心原则: 最小权限原则 - App 只能获取其明确声明且用户明确授权的权限。
一、 权限分类(按获取时机与方式):
-
安装时权限 (Install-Time Permissions / Normal Permissions):
- 特点: 涉及低风险操作,对用户隐私或设备操作影响极小。
- 获取方式: 在 App 安装时,系统自动授予(用户无需额外操作)。用户无法在安装后单独撤销这些权限。
- 例子: 设置时区 (
android.permission.SET_TIME_ZONE
)、访问网络 (android.permission.INTERNET
)、蓝牙 (android.permission.BLUETOOTH
)、振动 (android.permission.VIBRATE
)。
-
运行时权限 (Runtime Permissions / Dangerous Permissions):
- 特点: 涉及高风险操作,直接访问用户隐私数据或影响设备安全/其他 App 操作。这是权限机制的核心和重点!
- 获取方式 (关键流程):
- 声明: 在
AndroidManifest.xml
中声明需要的权限 (如<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
)。 - 检查: 在代码中执行需要该权限的操作之前,使用
ContextCompat.checkSelfPermission(Context, permissionString)
检查该权限是否已被授予。 - 请求:
- 如果未授予,调用
ActivityCompat.requestPermissions(Activity, new String[]{permissionString}, requestCode)
向用户弹出系统对话框请求授权。 - 用户可以选择
允许
或拒绝
。
- 如果未授予,调用
- 处理结果: 在 Activity/Fragment 中重写
onRequestPermissionsResult(requestCode, permissions[], grantResults[])
方法,处理用户的授权选择结果。
- 声明: 在
- 关键点:
- 用户控制: 用户可以在系统
设置
>应用
>权限
中随时授予或撤销这些权限。 - 临时拒绝 (Ask Every Time): 用户首次拒绝时,系统可能会提供“仅此一次”或“使用时允许”的选项(取决于权限类型和系统版本)。如果用户选择了
拒绝
并且 勾选了不再询问
(或等效选项),后续请求将直接失败。 - 权限组: 运行时权限被分组管理(如
位置
组包含ACCESS_FINE_LOCATION
和ACCESS_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 限制)。
-
特殊权限 (Special Permissions):
- 特点: 权限行为非常特殊,不在标准运行时权限流程内。通常涉及系统级设置或深度集成。
- 获取方式: 无法通过
requestPermissions()
获取! 需要引导用户跳转到特定的系统设置页面 (Settings.ACTION_APPLICATION_DETAILS_SETTINGS
或其他特定ACTION_..._SETTINGS
) 去手动开启。 - 例子: 悬浮窗 (
SYSTEM_ALERT_WINDOW
)、修改系统设置 (WRITE_SETTINGS
)、精确闹钟 (SCHEDULE_EXACT_ALARM
- Android 12+)、电池优化忽略 (REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
)。
-
签名权限 (Signature Permissions):
- 特点: 主要用于系统 App 或由同一开发者签名的 App 之间进行受保护的交互。
- 获取方式: 如果 App 的签名证书与声明该权限的 App/系统的签名证书匹配,则系统会在安装时自动授予。
- 开发者控制: 普通开发者一般无法定义或使用新的签名权限,主要用于平台或预装应用。
二、 关键机制与最佳实践:
AndroidManifest.xml
声明是必须的: 任何权限(尤其是运行时权限)都必须先在清单文件中声明,否则系统不会授予(即使代码请求了)。- 按需请求: 只在真正需要执行相关操作时才请求权限。避免在启动时请求一堆权限(“权限轰炸”),这会让用户反感并卸载 App。
- 解释为什么需要权限: 在请求权限前(尤其是用户可能不理解为什么需要时),使用
ActivityCompat.shouldShowRequestPermissionRationale(Activity, permissionString)
检查是否需要向用户解释。如果需要,先弹出自定义对话框解释清楚、简洁的原因,解释完后再调用requestPermissions()
。 - 优雅处理拒绝:
- 如果用户拒绝(未勾选“不再询问”),可以在后续合适时机再次请求(并附带解释)。
- 如果用户永久拒绝(勾选“不再询问”),应引导用户到 App 的设置页面 (
Settings.ACTION_APPLICATION_DETAILS_SETTINGS
) 手动开启权限,并禁用依赖该权限的功能(而不是崩溃或反复弹窗)。
- 权限组意识: 了解权限分组,但不要依赖自动授予行为作为不请求权限的理由。始终请求你需要的具体权限。
- 适配新版本: 关注新 Android 版本(如 11, 12, 13, 14)对权限模型的更新(如后台位置访问限制、照片选择器、邻近 Wi-Fi 权限、通知权限等),及时调整 App 逻辑。
- 存储权限 (
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
)。
- Android 10 (API 29) 引入 Scoped Storage: 限制 App 随意访问外部存储其他 App 的私有文件。强调使用
存储机制
核心目标: 在保护用户隐私和数据安全的前提下,为 App 提供可靠的文件存储能力,并实现不同 App 之间的安全数据共享。
核心挑战: 平衡 App 功能需求与用户数据安全/隐私,尤其在设备文件系统日益复杂和恶意软件威胁下。
一、 关键演变:从自由到严格(Scoped Storage 为核心)
-
Android 10 (API 29) 之前:相对自由
WRITE_EXTERNAL_STORAGE
权限 = 万能钥匙: 一旦用户授予,App 几乎可以读写整个外部存储(SD卡和内置存储的公共部分)的任何文件,包括其他 App 的私有文件。隐私泄露风险高!
-
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)
(系统文件选择器)。
- 默认作用域: App 默认只能无需权限访问:
-
Android 11 (API 30) 及以后:强化与完善
- 进一步限制:
READ_EXTERNAL_STORAGE
权限也受到更严格限制。 MANAGE_EXTERNAL_STORAGE
成为特殊权限: 提供给文件管理器、备份恢复等需要广泛文件访问的特定类型 App。普通 App 强烈不建议申请,上架应用商店审核严格且用户授权率极低。需要引导用户跳转到系统设置手动开启。- 文件访问意图更明确:
- 媒体文件: 优先且主要使用
MediaStore
。 - 文档/其他文件: 优先使用
Storage Access Framework (SAF)
。 - App 自身文件: 使用 App 专属目录。
- 媒体文件: 优先且主要使用
- 进一步限制:
通知机制
📣 核心流程(简单版)
- APP想通知你: 某个应用(比如微信、邮箱、游戏)发生了需要你注意的事情(新消息、下载完成、系统提醒)。
- APP打包“通知”: APP按照安卓系统的规定,创建一个通知对象 (Notification)。这个对象包含:
- 小图标 (Small Icon): 在状态栏显示的小图(必须)。
- 标题 (Title): 通知的主题(比如“新消息”、“下载完成”)。
- 内容文本 (Content Text): 通知的详细内容(比如“张三:晚上吃饭吗?”)。
- 大图标 (Large Icon - 可选): 展开通知后显示的大图(比如发信人头像)。
- 优先级 (Priority): 告诉系统这个通知有多紧急(高、中、低等,影响显示位置和是否响铃)。
- 点击动作 (PendingIntent): 最关键!你点击通知后要做什么?(比如打开聊天窗口、跳转到邮件详情、播放音乐)。
- 渠道 (Channel - Android 8.0+ 必须): 通知的分类(比如微信可以有“新消息”、“群通知”、“公众号更新”等不同渠道)。用户可以根据渠道单独设置开关和提醒方式!
- 其他花活 (可选): 进度条、按钮(快速回复、标记已读)、图片、媒体控制等。
- APP把通知“递”给系统: APP调用
NotificationManager.notify(id, notification)
方法,把这个打包好的通知对象交给安卓系统的 通知管理器 (Notification Manager)。 - 系统“展示”通知:
- 状态栏图标: 通知的小图标会出现在屏幕顶部的状态栏。
- 通知抽屉: 下拉状态栏,你会看到通知的详细列表(标题、内容、图标等)。
- 提醒方式 (根据用户设置):
- 声音 (Sound): 播放提示音。
- 震动 (Vibrate): 手机震动。
- 呼吸灯 (Lights - 如果手机有): 闪烁指示灯。
- 浮动通知/弹窗 (Heads-up - 高优先级): 在屏幕顶部短暂弹出(不影响当前操作)。
- 锁屏显示 (根据用户设置): 通知内容可能显示在锁屏上(注意隐私)。
🔑 关键机制和规则
-
通知渠道 (Android 8.0 Oreo 引入):
- 核心思想: 让用户精细控制通知! 不再是“整个APP的通知要么全开要么全关”。
- APP的责任: APP必须为不同类型的通知创建不同的渠道 (Channel) (比如“交易提醒”、“营销推送”、“聊天消息”)。
- 用户的权力: 用户可以单独为每个渠道设置:
- 开关: 是否允许显示。
- 提醒方式: 是否响铃、震动、浮动显示、在锁屏显示。
- 重要性 (Importance Level): 决定通知的干扰程度(紧急、高、中、低)。
- 好处: 用户能屏蔽烦人的广告推送,但保留重要的聊天消息提醒。
-
通知权限:
- Android 13 (Tiramisu) 之前: APP安装后默认可以发通知。
- Android 13 及以后: 新增运行时权限
POST_NOTIFICATIONS
!- 当APP第一次尝试发通知时,系统会弹窗询问用户**“是否允许 [APP名称] 发送通知?”**。
- 用户可以选择 “允许” 或 “不允许”。
- 开发者注意: 必须适配!用户拒绝后,调用
notify()
会失效。
-
勿扰模式 (Do Not Disturb):
- 用户可以开启“勿扰模式”(手动或按计划)。
- 在该模式下,只有被用户标记为“允许打扰” 的APP或联系人的通知(通常是最高优先级或特殊渠道)才会发出声音/震动,其他通知会静默进入通知抽屉。
-
后台限制 (省电优化):
- 安卓系统(尤其国产定制系统)对APP在后台运行有严格限制,防止耗电。
- 影响: 如果APP被系统“杀掉”或在后台被严格限制,它可能无法及时触发后台服务来发送通知。
- 解决方案 (给开发者):
- 使用
WorkManager
安排可靠的后台任务(系统会找合适时机运行)。 - 使用厂商推送服务 (如小米推送、华为推送、FCM) 替代APP自己维持长连接(更省电,推送更可靠)。
- 引导用户将APP加入“电池优化白名单”或“允许后台运行”(效果因厂商而异)。
- 使用
-
通知分组和摘要 (Android 7.0+):
- 分组 (Grouping): 同一个APP的多个通知(比如多封未读邮件)可以被折叠成一个“组”显示,点击组再展开详情。避免通知栏被刷屏。
- 摘要 (Bundling/Summary): 可以为分组提供一个摘要通知(比如“5条新消息”)。
-
长连接与推送服务:
- 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想再启动新服务?门都没有!
- 前台服务 (Foreground Service): 如果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的唤醒请求集中到某个时间点执行,减少频繁唤醒。
- 结果: 用户省电效果可能更好,但开发者适配极其痛苦,后台任务可靠性严重依赖用户手动设置白名单。