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的喚醒請求集中到某個時間點執行,減少頻繁喚醒。
- 結果: 用戶省電效果可能更好,但開發者適配極其痛苦,后臺任務可靠性嚴重依賴用戶手動設置白名單。