?1. Activity 啟動模式問題?
?面試官?:
“我看你項目里用了 SingleTask 模式,能具體說說為什么用它嗎?如果從 Activity A(SingleTask)跳轉到 B(Standard),再返回 A,任務棧會怎么變化?”
?候選人?:
(技巧?:先明確問題,再分場景解釋)
“好的。在我們的項目里,主頁 Activity 被設置為 SingleTask 模式,主要是為了防止重復創建主頁。比如用戶從通知欄跳轉到主頁時,如果已經有主頁實例在后臺,就直接復用,而不是新建一個,這樣可以避免用戶按返回鍵時多次退出。
您提到的從 A 跳轉到 B 再返回 A 的情況,假設 A 是 SingleTask,B 是 Standard:
- 當第一次啟動 A 時,系統會為 A 創建一個獨立的任務棧,假設叫 Task1。
- 從 A 跳轉 B,因為 B 是 Standard 模式,B 會被壓入 Task1 的棧頂。
- 當從 B 返回 A 時,由于 A 是 SingleTask,系統會先檢查任務棧。發現 Task1 中已經存在 A 實例,于是會把 A 之上的所有 Activity(也就是 B)彈出棧,讓 A 回到棧頂。這時候用戶再按返回鍵,就直接退出應用了。”
?加分話術?:
“這里有個實際踩過的坑:如果 A 的啟動模式設置不當,可能會導致返回棧混亂。我們當時用 adb shell dumpsys activity
命令查看任務棧,驗證邏輯是否符合預期。”
?2. Service 保活與 ANR 問題?
?面試官?:
“Service 里做耗時操作為什么會 ANR?你們項目里怎么解決的?”
?候選人?:
(技巧?:承認問題 + 解決方案 + 實際案例)
“是的,Service 默認運行在主線程,如果直接在里面做網絡請求或大量計算,肯定會阻塞主線程導致 ANR。我們在項目里是這樣處理的:
- ?異步處理?:比如用 IntentService,它內部自己開了工作線程,處理完自動停止。
- ?結合 HandlerThread?:如果是長期運行的后臺任務,會創建一個 HandlerThread,再通過 Handler 發送任務到子線程執行。
- ?前臺服務保活?:像音樂播放功能,我們用了前臺服務,顯示通知欄避免被系統回收。不過保活很難完全做到,Android 8.0 之后限制更多,我們最終改用 JobScheduler 在合適時機重啟服務。”
?加分話術?:
“其實現在更推薦用 WorkManager 處理后臺任務,它能根據系統版本自動選擇底層實現,比如用 JobScheduler 或 AlarmManager,這樣兼容性更好。”
Service 擴展?
?場景一:Service 混合啟動模式?
?面試官?:
“假設我先用 startService()
啟動了一個 Service,接著又用 bindService()
綁定它。這時候 Service 的生命周期會怎么走?銷毀時要注意什么?”
?候選人?:
(自然思考狀)
“嗯,這個問題其實涉及到 Service 的兩種啟動方式混合使用的情況。我舉個例子吧:比如我們做一個音樂播放器,先用 startService()
啟動播放服務,保證音樂在后臺持續播放;然后在 Activity 里調用 bindService()
綁定它,用來調節音量或者切歌。這時候 Service 的生命周期大概是這樣的——”
- ?第一步?:
startService()
觸發后,Service 會先走onCreate()
,然后onStartCommand()
,這時候服務就算正式啟動了。 - ?第二步?:再調用
bindService()
,這時候不會重新創建 Service,而是直接調用onBind()
,返回一個 IBinder 對象給客戶端。 - ?銷毀時?:得同時解綁和停止服務。比如用戶退出 Activity 時調用
unbindService()
,然后還得主動調用stopService()
,否則 Service 會一直活著,直到系統資源不足被干掉。”
加分話術?:
“在源碼中,Service 的生命周期由?ActivityManagerService
?管理。每次?startService()
?會增加一個 ‘started’ 狀態計數,而?bindService()
?會增加一個 ‘bound’ 計數。只有當兩個計數都歸零時,才會調用?onDestroy()
。我們可以通過?adb shell dumpsys activity services
?查看當前 Service 的狀態。”?
(補充踩坑經驗)
“對了,我之前遇到過這種情況:只調了 unbindService()
沒調 stopService()
,結果發現通知欄的音樂控件還在,用戶點了之后又恢復播放。后來用 adb shell dumpsys activity services
一查,發現 Service 的 ‘started’ 狀態還沒歸零,這才恍然大悟。”
?場景二:IntentService 的線程模型?
?面試官?:
“IntentService 是怎么在子線程處理任務的?如果我連續發 10 個 Intent,它們是排隊一個一個執行,還是能并發?”
?候選人?:
????????這個問題我當初也好奇,還特意寫代碼驗證過。IntentService 內部其實藏了個 HandlerThread
,這個類特別有意思,它自己帶了一個 Looper,專門用來在子線程處理消息隊列。
? ? ? ? 當您調用 startService()
發送 Intent 時,IntentService 會把每個 Intent 包裝成一個 Message,扔進 HandlerThread 的消息隊列里。這個隊列是先進先出的,所以哪怕同時發 10 個 Intent,也得老老實實排隊。比如第一個任務是下載文件,耗時 5 秒,后面 9 個任務都得等它完了才能執行。
(舉反例)
“不過有一次,我手賤在 onHandleIntent()
里開了個新線程,結果任務全并行跑起來了,日志亂成一團。這才明白:IntentService 的串行特性全靠 HandlerThread 的消息隊列,如果自己開線程反而會打破這個機制。”
?場景三:Service 的 ANR 避坑?
?面試官?:
“聽說在 Service 里直接做網絡請求會 ANR?你們項目里是怎么處理的?”
?候選人?:
“這事兒我們還真踩過坑!早期圖省事,在 onStartCommand()
里直接寫了個網絡請求,結果線上 ANR 率飆升。后來復盤發現,Service 默認在主線程跑,一個慢請求就能卡死整個 APP。”
(分步驟解釋)
“我們的解決方案分了三步走:
- ?緊急修復?:先在
onStartCommand()
里手動new Thread()
,把請求扔到子線程。雖然土,但能快速止血。 - ?中期優化?:換成 IntentService,但很快就發現它只能串行執行,下個版本需求要支持多文件同時上傳,只好再改。
- ?最終方案?:上線程池!用
Executors.newFixedThreadPool(3)
控制并發數,搭配 LiveData 把結果拋回主線程更新 UI。代碼大概是這樣的——”
(模擬寫代碼)
public class UploadService extends Service {private ExecutorService pool = Executors.newFixedThreadPool(3);@Overridepublic int onStartCommand(Intent intent, int flags, int startId) {pool.execute(() -> {String url = intent.getStringExtra("url");boolean success = doUpload(url); // 模擬上傳LiveDataBus.getInstance().post(new UploadEvent(success));});return START_NOT_STICKY;}
}
?場景四:IntentService 的淘汰原因?
?面試官?:
“聽說 IntentService 過時了?你們現在用什么替代?”
?候選人?:
“確實,Android 8.0 之后 IntentService 有點力不從心了。比如我們之前用它做日志上報,結果在后臺經常被系統干掉,查文檔才發現:8.0 開始限制后臺 Service,startService()
得配合前臺通知才能用。”
(對比分析)
“后來我們全面轉向 WorkManager,這玩意兒聰明在哪呢?它底層自動適配系統版本——在 Android 6.0 用 AlarmManager,7.0 用 JobScheduler,甚至還能在國產 ROM 上兼容。比如上傳失敗的任務會自動重試,還能設置網絡條件,比 IntentService 手動重試省心多了。”
(舉例說明)
“比如一個夜間日志上傳的需求,用 WorkManager 可以這么搞:”
// 定義任務
class LogUploadWorker(context: Context, params: WorkerParameters) : Worker(context, params) {override fun doWork(): Result {return if (uploadLogs()) Result.success() else Result.retry()}
}// 設置約束:充電狀態 + 夜間時段
val constraints = Constraints.Builder().setRequiresCharging(true).build()val request = OneTimeWorkRequestBuilder<LogUploadWorker>().setConstraints(constraints).setInitialDelay(6, TimeUnit.HOURS) // 延遲到凌晨.build()WorkManager.getInstance(context).enqueue(request)
“現在除非兼容老系統,否則新項目基本不用 IntentService 了。不過它的設計思想——子線程干活、干完自毀,在 WorkManager 的 Worker 里還能看到影子。”
?3. BroadcastReceiver 使用場景?
?面試官?:
“你提到用廣播實現登錄狀態同步,能具體說說嗎?靜態注冊和動態注冊怎么選?”
?候選人?:
(技巧?:場景化描述 + 安全提醒)
“比如用戶登錄成功后,我們需要更新多個頁面的 UI。這時候發送一個自定義廣播,各個頁面注冊 Receiver 監聽。登錄頁發送廣播后,主頁、個人中心頁收到通知,主動刷新數據。
關于注冊方式的選擇:
- ?靜態注冊?:適合監聽系統廣播,比如開機啟動。但要注意 Android 8.0 之后對靜態廣播的限制。
- ?動態注冊?:靈活且優先級高,但要在 Activity 或 Fragment 的 onResume 注冊,onPause 注銷,防止內存泄漏。
不過現在更推薦用 LiveData 或 EventBus 替代廣播,減少系統開銷。只有跨進程通信時才會用 ContentProvider 或廣播。”
?加分話術?:
“我們之前遇到過一個坑:動態注冊的 Receiver 沒及時注銷,導致 Activity 泄漏。后來用 LeakCanary 檢測出來,加了個 try-catch
確保 unregisterReceiver
一定執行。”
?4. ContentProvider 安全與多進程?
?面試官?:
“如果想讓其他應用訪問我們的數據,用 ContentProvider 怎么保證安全?”
?候選人?:
(技巧?:分層回答 + 權限細節)
“我們分了三層防護:
- ?權限聲明?:在 AndroidManifest.xml 里定義讀寫權限,比如
com.example.READ_DATA
,并設置protectionLevel="signature"
,只允許相同簽名的應用訪問。 - ?Uri 權限控制?:在 Provider 的 query 方法里校驗調用方的包名,通過
Binder.getCallingUid()
獲取 UID,對比白名單。 - ?數據加密?:敏感數據(如用戶手機號)在存儲時用 AES 加密,即使被惡意讀取也無法解密。”
?加分話術?:
“其實 Android 的 FileProvider 也是類似的思路,通過 grantUriPermission
動態授權臨時權限,避免長期暴露數據。”
基礎知識講解
?Activity 啟動模式(LaunchMode)及場景?
- ?Standard?:默認模式,每次啟動創建新實例(可能產生重復登錄頁面的問題)。
- ?SingleTop?:棧頂復用,若已在棧頂則不創建新實例(適用于通知跳轉頁)。
- ?SingleTask?:棧內復用,在任務棧中只存在一個實例(主頁面常用)。
- ?SingleInstance?:獨立任務棧,全局唯一(如系統來電頁面)。
?真題?:
?Q?:從 Activity A(SingleTask)跳轉到 B(Standard),再從 B 跳轉回 A,描述任務棧變化。
?A?:A 啟動時創建新任務棧,B 在默認棧中。當 B 跳轉回 A 時,由于 A 的 SingleTask 特性,系統會清空 A 所在棧頂之上的所有 Activity,直接復用 A 實例。
Q1:Activity A 跳轉 B,B 跳轉 C,按返回鍵時的生命周期回調順序? (假設 A, B, C 都是 Standard 模式)
A: (這是一個經典問題,關鍵在于理解 Activity 棧和生命周期)
-
當前狀態: 棧頂是 C,其下是 B,再下是 A。
[A, B, C]
-
在 C 按返回鍵:
- C:
onPause()
- B:
onRestart()
(如果 B 之前onStop()
了) ->onStart()
->onResume()
(B 變為可見并獲得焦點) - C:
onStop()
->onDestroy()
(C 被銷毀并出棧) - 此時棧:
[A, B]
- C:
-
在 B 按返回鍵:
- B:
onPause()
- A:
onRestart()
(如果 A 之前onStop()
了) ->onStart()
->onResume()
- B:
onStop()
->onDestroy()
- 此時棧:
[A]
- B:
?Activity 與 Fragment 通信方式?
- ?Bundle + setArguments?:傳遞初始參數。
- ?接口回調?:Fragment 定義接口,Activity 實現。
- ?ViewModel?:通過共享 ViewModel 實現數據監聽。
- ?EventBus?:事件總線解耦通信(需注意內存泄漏)。
?真題?:
?Q?:Fragment 如何回傳數據給 Activity?
?A?:在 Fragment 中定義接口,在?onAttach()
?中綁定 Activity 實例,調用接口方法傳值。代碼示例:
// Fragment 中
public interface OnDataCallback {void onDataReceived(String data);
}@Override
public void onAttach(Context context) {super.onAttach(context);if (context instanceof OnDataCallback) {callback = (OnDataCallback) context;}
}// 傳值時調用
callback.onDataReceived("data");
Q1?:Activity A 跳轉 B,B 跳轉 C,按返回鍵的生命周期回調順序?
?A?:
- C →?
onPause()
?→ B →?onRestart()
?→?onStart()
?→?onResume()
?→ C →?onStop()
?→?onDestroy()
。
?Q2?:Service 中執行耗時操作為何可能引發 ANR?如何解決?
?A?:Service 默認運行在主線程,直接執行耗時操作會阻塞 UI 線程。應使用?IntentService
?或?HandlerThread
?在子線程處理。
?Q3?:BroadcastReceiver 的 onReceive() 中能否啟動彈窗?
?A?:可以,但需通過?Intent
?跳轉 Activity 并添加?FLAG_ACTIVITY_NEW_TASK
(因為 Receiver 無任務棧)。