文章目錄
- 序言
- 四大組件
- Activity
- Activity 生命周期
- onStart() 與 onResume() 區別?
- Activity 啟動模式
- launchMode
- 使用 Intent 標記
- taskAffinity
- 清除回傳堆疊
- allowTaskReparenting
- 使用 Intent 隱式啟動 Activity(IntentFilter 匹配規則)
- Activity 的啟動流程
- 當點擊一個應用圖示以后,都發生了什么,描述一下這個程序?
- 基于 Android 9.0(API 28) 的 Activity 啟動流程分析
- Android 系統啟動流程是什么(提示:init 行程 -> Zygote 行程 -> SystemServer 行程 -> 各種系統服務 -> 應用行程)?
- Service
- 啟動方法
- 生命周期
- Activity 與 Service 通信
- IntentService
- HandlerThread
- IntentService 內部機制
- 為什么在 mServiceHandler 的 handleMessage() 回呼方法中執行完 onHandlerIntent() 方法后要使用帶引數的 stopSelf() 方法?
- 為什么 bindService 可以跟 Activity 生命周期聯動
- 如何保證 Service 不被殺死?
- 提高行程優先級,降低行程被殺死的概率
- 在行程被殺死后,進行拉活
- 依靠第三方
- BroadcastReceiver
- 應用場景
- 注冊方式
- 發送和接收的原理
- 傳輸資料的限制
- ContentProvider、ContentResolver
- 簡介
- ContentProvider 初始化程序
- ContentProvider 對應方法所在的執行緒
- Fragment
- 生命周期
- 當呼叫了 FragmentTransaction#add 方法將一個 Fragment 添加到一個容器中時,Fragment 會按順序回呼如下方法
- 當呼叫了 FragmentTransaction#hide() 方法隱藏一個 Fragment 時
- 當呼叫了 FragmentTransaction#remove 方法將一個 Fragment 移除時,Fragment 會按順序回呼如下方法
- FragmentTransaction#replace()
- FragmentTransaction#addToBackStack 方法
- 回退堆疊常見的方法
- Fragment 的通信
- FragmentPagerAdapter 與 FragmentStatePagerAdapter 的區別
- 遇到過哪些 Fragment 的問題,如何處理的?
- Fragment 視圖重疊
- getActivity() 空指標
- Window
- Window 的型別
- Window 的添加程序
- Window 的洗掉程序
- Window 與 WindowManager
- View 相關
- View 基礎知識
- View 的位置引數
- MotionEvent 和 TouchSlop
- MotionEvent
- getAction() 與 getActionMask()
- 事件
- 常用方法
- TouchSlop
- VelocityTracker、GestureDetector
- VelocityTracker
- GestureDetector
- ScaleGestureDetector
- View 的滑動
- 使用 View#scrollTo、View#scrollBy 函式
- Scroller 彈性滑動
- 使用影片
- View 的事件分發機制
- 一些重要結論
- 事件分發流程
- ACTION_CANCEL 什么時候觸發,觸摸 Button,然后滑動到外部抬起會觸發點擊事件嗎,再滑動回去抬起會嗎?
- 點擊事件被攔截,但是想傳遞到下邊的 View 怎么辦?
- 如何處理滑動沖突?
- 繪制流程
- View 的繪制流程(DecorView 是如何與 WindowManager 關聯到一起的?/setContentView 是如何將 View 添加到視圖上的?)(基于 API 24)
- Measure
- Layout
- layout 的作用
- getMeasuredWidth() 與 getWidth 的區別
- 何時可以獲取真實的寬、高
- onDraw
- 一些問題
- ViewGroup 會呼叫 onDraw 嗎?為什么?
- getWidth 和 getMeasureWidth 方法的區別?
- onMeasure 有時候會執行多次?
- 如何在 Activity 啟動時獲取到 View 寬高?
- 為什么能在 View#post 中獲取到 View 寬高?
- 聊聊對子執行緒不能更新 UI 的看法?
- 如何在子執行緒更新 UI?
- View#post 傳入的 Runnable 一定會被執行嗎?
- requestLayout、invalidate、postInvalidate 的區別與聯系?
- 自定義 View
- 自定義 View 的幾種型別
- Android 的資料存盤
- 檔案存盤(I/O)
- SharedPreferences
- SharedPreferences.Editor 的 commit() 和 apply() 方法區別,如果寫入失敗了會怎樣?
- commit()
- apply()
- SharedPreferences 是否可以跨行程使用?
- SharedPreferences#getXXX() 方法
- SharedPreferences#putXXX() 方法
- SharedPreferences 使用注意事項
- 資料庫存盤
- SQLiteOpenHelper 類
- SQLiteDatabase 類
- ContentProvider 讀取資料
- 影片
- ObjectAnimator
- 使用
- 使用 ObjectAnimator 的要求
- 設定監聽
- AnimatorSet
- Interpolator
- TypeEvaluator
- 硬體加速
- 離屏緩沖
- View#setLayerType()
- Canvas#saveLayer()
- Toast 原理
- 適配
- 螢屏適配
- 螢屏尺寸、解析度、像素密度三者關系
- 使用第三方適配庫
- AndroidAutoSize
- 寬高限定符適配
- smallestWidth 限定符適配適配
- 原理
- 使用
- 缺點
- 跨行程通信
- 為何需要 IPC?
- 直接進行跨行程通信可能出現的問題?
- AIDL
- 關鍵類和方法
- 語法
- 使用
- 使用 CopyOnWriteArrayList
- 使用 AIDL 要注意的
- AIDL 如何讓服務端呼叫客戶端的方法?
- RemoteCallbackList 的作用
- 在 Binder 中使用 List 保存 listener 完成注冊與反注冊是否可行?為什么?
- RemoteCallbackList 的作業原理
- RemoteCallbackList 的特性
- AIDL Binder 意外死亡如何處理
- 在 AIDL 中使用權限驗證
- 其他
- Android 中的 Context
- APK 打包流程
- APK 的安裝流程
- APT(Annotation Processing Tool)
- 如何處理全域例外捕獲(CrashHandler)
- 使用
- 注意
- 記憶體泄露
- Context 相關
- Android 應用里有幾種 Context 物件?
- Android 中行程的優先級?
- 多行程場景遇到過嗎?
- Android 類加載器
- 子執行緒可以彈 Toast 和 Dialog 嗎?正確姿勢是?
- MultiDex
- 使用 MultiDex 解決何事?根本原因在于?
- 主 Dex 檔案放那些東西,跟其他 Dex 呼叫、關聯?
- Odex 的作用?
- 規避 64K 限制
- 組件化、插件化
- 組件化
- 插件化
- 插件化原理
- 插件化的作用
- 常用的插件化框架
- 熱修復
- 熱修復與插件化的區別
- 熱修復的原理
- 通過干預 ClassLoader findClass 的程序實作熱修復
- loadClass() 的類加載程序(Android 中類加載機制)
- 通過 ClassLoader 方式熱修復的具體流程
- 常用的熱更新框架
- 參考文章
序言
博客很久沒更新了,上次更新的時候還沒有畢業 =_=!后來由于作業和自身的原因,一直懶得沒有維護博客,今年年后,自己也是準備跳槽找作業,于是復習整理了一下 Android 相關的基礎知識,都是自己辛苦看過、總結、碼出來的,希望對其他找作業的同孩兒們能有一些幫助 ~

后續也會抽時間陸續整理一下其他的問題,比如 Java 相關的知識、原始碼分析、面試程序中遇到的問題、心得和感受等等,覺得寫得不錯的觀眾老爺記得點贊加收藏哦 ~
四大組件
Activity
Activity 是 Android 中四大組件部分我們接觸的最多,也最常使用的一個組件了,由此大家也應該知道這塊的重要性了吧,相對于其他組件來說,這塊也是問的最多的~

Activity 生命周期

正常流程是:onCreate() -> onStart() -> onResume() -> onPause() -> onStop() -> onDestroy()
當 Activity 被其他的透明 Activity 蓋住時,只會執行其 onPause 方法,因為 Activity 只是被遮住了,失去焦點,但對用戶還可見,但當 Activity 被 Dialog 蓋住時,并不會執行任何生命周期方法!!!
網上大多數博客說的都是錯誤的!!!
onStart() 與 onResume() 區別?
- onStart() 是 Activity 界面被顯示出來的時候執行的,此時還不能與其互動
- onResume() 是 Activity 與用戶能進行互動時執行,用戶可以獲取其焦點,
Activity 啟動模式
這部分是 Activity 組件的重中之重,也是面試中最常問到的問題,可能平時我們都覺得自己掌握的差不多了,但真的問起來,你,確定你答的上嗎?

launchMode

-
standrad:系統默認的啟動模式,多次啟動同一個 Activity,Activity 堆疊中可存在多個實體
-
singleTop:堆疊頂復用,若 A 在堆疊頂,則再次啟動 A 時,會呼叫 A 的 onNewIntent() 方法,而不會再啟動一個 A 入堆疊;若 A 不在堆疊頂,再次啟動 A 時,會重新入堆疊一個 A Activity,
-
singleTask:堆疊內唯一,A 設定了 singleTask,若 A 在堆疊頂,與 singleTop 效果一樣;若 A 不在堆疊頂,則會將 A 之上的所有 Activity 出堆疊(singleTask 默認附帶 CLEAR_TOP 效果),然后回呼 A 的 onNewIntent 方法,
- 啟動一個設定了 singleTask 啟動模式的 Activity,在啟動這個 Activity 時,會尋找這個 Activity 所在的堆疊是否存在,存在則直接入堆疊,否則創建一個新堆疊,將 Activity 入堆疊,若在這個 Activity 中再啟動別的 Activity,則默認情況下,這個 Activity 入的是這個堆疊,不是以前 App 的堆疊,
- 若在 App1 中啟動了 App2 中設定了 singleTask 的 Activity(假設為 A),則 A 會尋找自己所屬的堆疊,壓入其中,若沒有指定 taskAffinity,則 A 會加入 App2 的堆疊,然后將 App2 整個堆疊拿過來,壓在 App1 的堆疊上(有 task 間的切換影片),若一步步回傳的時候,則會基于 App2 的堆疊一步步退出,直到整個 App2 都退出了,會切換回 App1 跳轉 A 之前的頁面(task 間切換的影片),
- task 的疊加只適用于前臺 task,task 由前臺進入后臺,會被拆開,比如:1)當按了 Home;2)按了最近任務鍵(在最近任務串列顯示出來的時候就已經進入后臺,而不是在切換到其他應用之后),再回來,疊加的 task 就會被拆開,
- singleInstance:跟 singleTask 有些像,但是設定了 singleInstance 的 Activity,會獨自占用一個堆疊,
比如有 A、B、C 三個 Activity,B 設定了 singleInstance,則 A 啟動 B 時,B 會 new 一個新堆疊并入堆疊,再從 B 啟動 C 時,C 會壓入 A 的堆疊,

那么問題來了,MainActivity 一般用什么啟動模式呢?
使用 Intent 標記
可以使用 Intent 標記覆寫在 Activity 中設定的 launchMode
- FLAG_ACTIVITY_NEW_TASK:同 singleTask,但是 singleTask 實際上還附帶 FLAG_ACTIVITY_CLEAR_TOP Flag
- FLAG_ACTIVITY_SINGLE_TOP:同 singleTop
- FLAG_ACTIVITY_CLEAR_TOP:如果要啟動的 Activity 已經在當前任務中運行,則不會啟動該 Activity 的新實體,而是會銷毀位于它之上的所有其他 Activity,并通過 onNewIntent() 將此 intent 傳送給它的已恢復實體(現在位于堆疊頂部),
FLAG_ACTIVITY_CLEAR_TOP 最常與 FLAG_ACTIVITY_NEW_TASK 結合使用,達到 singleTask 的效果,
taskAffinity
可翻譯為堆疊親和性,一個 Activity 不設定 taskAffinity,默認與 Application 的 taskAffinity 相同,Application 默認的 taskAffinity 是 packageName,可以通過這個屬性與 singleTask 配合讓 Activity 在一個新堆疊中打開,
一個 task 的 taskAffinity 取自堆疊底 Activity 的 taskAffinity,
正常情況下一個 Application 只會在多任務串列中顯示一個 task,因為 當多個 task 具有相同的 taskAffinity 時,最近任務串列里只會顯示最新展示過的那個,如果為一個 App 的多個 Activity 設定不同的 taskAffinity,則可以在多任務串列下看到這些 Activity 的縮略,如果為這些 Activity 分別配置了 action 為 MAIN、category 為 LAUNCH,以及 label 和 icon,則會在應用程式串列中看到這些 Activity 的快捷打開方式,
清除回傳堆疊
如果用戶離開任務較長時間,系統默認會清除任務中除根 Activity 以外的所有 Activity,當用戶再次回傳到該任務時,只有根 Activity 會恢復,系統之所以采取這種行為方式是因為,經過一段時間后,用戶可能已經放棄了之前執行的操作,現在回傳任務是為了開始某項新的操作,可以使用一些 Activity 屬性改變此行為:
- alwaysRetainTaskState:如果在任務的根 Activity 中將該屬性設為 “true”,則不會發生上述默認行為,即使經過很長一段時間后,任務仍會在其堆疊中保留所有 Activity,
- clearTaskOnLaunch:如果在任務的根 Activity 中將該屬性設為 “true”,那么只要用戶離開任務再回傳,堆疊就會被清除到只剩根 Activity,
- finishOnTaskLaunch:該屬性與 clearTaskOnLaunch 類似,但它只會作用于單個 Activity 而非整個任務,它還可導致任何 Activity 消失,包括根 Activity,如果將該屬性設為 “true”,則 Activity 僅在當前會話中歸屬于任務,如果用戶離開任務再回傳,則該任務將不再存在,
allowTaskReparenting
允許 task 重新回去屬于自己的堆疊,如下:對 Activity 設定 allowTaskReparenting = “true”,為 App2 的 A Activity 設定了這個屬性,在 App1 中啟動 App2 的 A Activity,A 會被壓入到 App1 的堆疊中,回到桌面,再打開 App2 時,會把 A Activity 拉回到 App2 自己的堆疊中,
使用 Intent 隱式啟動 Activity(IntentFilter 匹配規則)
- 實體化 Intent
- 為 Intent 設定 action,若目標 Activity 設定了 action,則此處設定一個匹配任意 action 的 action 過去
- 為 Intent 設定 category,可以不設定,不設定默認為 Intent.CATEGORY_DEFAULT,
- 為 Intent 設定 data,若目標 Activity 設定了 data,則此處設定一個匹配任意 data 的 data 過去
- URI 匹配規則:
<scheme>://<hose>:<port>/[<path>|<pathPrefix>|<pathPattern>] - 如:content://com.example.project:200/folder/etc
- 若未指定 URI,默認值(schema)為 content 和 file
- URI 匹配規則:
- 需要呼叫 Intent#setDataAndType 方法,單獨設定 data 會將 type 清空;單獨設定 type 會將 data 清空,
Activity 的啟動流程
一些關鍵類
- Instrumentation:監控應用與系統相關的互動行為
- AMS:組件管理調度中心
- ActivityStarter:Activity 啟動的控制器,處理 Intent 與 Flag 對 Activity 啟動的影響,
1.尋找符合啟動條件的 Activity,如果有多個,讓用戶選擇;
2.校驗啟動引數合法性;
3.回傳 int 引數,代表 Activity 是否啟動成功- ActivityStack:用來管理任務堆疊里的 Activity
- ActivityStackSupervisior:用來管理任務堆疊,高版本才有的類,用來管理多個 ActivityStack,早期的版本只有一個 ActivityStack 對應著手機螢屏,后來高版本支持多屏之后就有了多個 ActivityStack,于是就引入了 ActivityStackSupervisior 來管理多個 ActivityStack
- ApplicationThread:實際起作用的類,是 ActivityThread 的內部類,Activity、Service、BroadcastReceiver 的啟動、切換、調度等各種操作都在這個類中完成,
- startActivity 最侄訓呼叫到 startActivityForResult 方法,
- startActivityForResult 方法中會呼叫 Instrumentation#execStartActivity 方法,將呼叫委托給 ActivityManagerProxy
- ActivityManagerProxy 會呼叫 system_server 行程中的 AMS 的 startActivity 方法
- AMS 會將呼叫委托給 ApplicationThreadProxy(AMS 最侄訓呼叫 ActivityStackSupervisor 中的 realStartActivityLocked 方法,通過 ApplicationThreadProxy,最終呼叫到 ApplicationThread 的 scheduleLaunchActivity 方法,)
- ApplicationThreadProxy 將發呼叫委托給 ApplicationThread 的 scheduleLaunchActivity 方法,給 ActivityThread 中的 Handler 發送訊息,處理啟動 Activity 的 launch 請求
- 按順序呼叫 Activity 中的 handleLaunchActivity 方法,performResumeActivity() 方法等,完成 Activity 的啟動,

當點擊一個應用圖示以后,都發生了什么,描述一下這個程序?
點擊應用圖示后會去啟動應用的 LauncherActivity,如果 LauncherActivity 所在的行程還沒有創建,會先創建行程,整體的流程就是一個 Activity 的啟動流程,

- 點擊桌面應用圖示,Launcher 行程將啟動 Activity 的請求以 Binder 的形式發送給了 AMS
- AMS 收到請求后,交給 ActivityStarter 處理 Intent 和 Flag 等資訊,然后交給 ActivityStackSupervisior 和 ActivityStack 處理 Activity 進堆疊相關流程,同時請求 Zygote 行程 fork 新行程
- Zygote 接收到新行程創建請求后,fork 出新行程
- 在新行程里創建 ActivityThread 物件,呼叫其 main 方法,創建 Looper,開啟訊息回圈,
- ActivityStackSupervisior 中將啟動 Activity 的指令通過 ApplicationThreadProxy 代理給 ActivityThread 中的 ApplicationThread 中的 scheduleLaunchActivity 方法,
- 通過 Handler 發送訊息給 ActivityThread 中的 Handler,執行 handleLaunchActivity、performResumeActivity 等方法,回呼 Activity 對應的生命周期,
基于 Android 9.0(API 28) 的 Activity 啟動流程分析
ActivityStackSupervisior 負責所有 Activity 堆疊的管理,內部管理了 mHomeStack、mFocusedStack 和 mLastFocusedStack 三個 Activity 堆疊,mHomeStack 管理的是 Launcher 相關的 Activity 堆疊;mFocusedStack 管理的是當前顯示在前臺 Activity 的 Activity 堆疊;mLastFocusedStack 管理的是上一次顯示在前臺 Activity 的 Activity 堆疊,
- startActivity 或 startActivityForResult 向 AMS 發起 startActivity 的啟動請求,
- AMS 收到請求后,最侄訓呼叫到 startActivityAsUser 方法,構建一個 ActivityStarter 物件,呼叫其 execute 方法,最終呼叫到
startActivityUnchecked方法,在 startActivityUnchecked 方法中會處理 Activity 的 Intent 資訊、Flag 資訊,判斷 Activity 啟動模式、是否要呼叫 deliverNewIntent 回呼 onNewIntent 等,然后交給 ActivityStackSupervisior 和 ActivityStack 處理 Activity 進堆疊相關流程(ActivityStarter#startActivityUnChecked -> ActivityStackSupervisor#resumeFocusedStackTopActivityLocked) - ActivityStackSupervisior 中會呼叫 mFocusedStack 的 resumeTopActivityUncheckedLocked 方法,判斷是否有 Activity 處于 Resume 狀態,有的話會先讓這個 Activity 進入 Pausing 程序,然后 ActivityStackSupervisior 再執行 startSpecificActivityLocked 嘗試啟動要啟動的 Activity
(ActivityStackSupervisior#resumeTopActivityInnerLocked -> ActivityStack#startPausingLocked -> ActivityStack#startSpecificActivityLocked) - 執行完 resume 的 Activity 的 pause之后,ActivityStackSupervisior 中會判斷當前行程是否創建
(startSpecificActivityLocked),未創建,則會呼叫 Zygote fork 出一個新行程,并通過反射創建 ActivityThread,執行它的 main 方法完成主執行緒的初始化 - ActivityThread 的 main 方法中會創建 Looper 并開啟 loop 回圈,還會呼叫 attach 方法,通過 AMS 為應用系結 Application(關聯 ApplicationThread) 物件,
(然后添加一個垃圾回收觀察者,每當系統觸發垃圾回收,會在 run 方法中計算應用使用了多少記憶體,如果超過總量的四分之三就會釋放記憶體) - 主執行緒和主行程都初始化完畢后,attachApplication 的最后會呼叫 ActivityStackSupervisor 的 realStartActivityLocked 方法,啟動 Activity(通過 ClientLifecycleManager.scheduleTransaction post 了一個 LaunchActivityItem),
(ActivityStackSupervisor#realStartActivityLocked),最后會執行到 ActivityThread 中啟動 Activity 的相關流程 - 執行之前堆疊頂 Activity 的 onStop 程序
Android 9.0 引入了 ClientLifecycleManager 和 ClientTransactionHandler 輔助管理 Activity 生命周期,ActivityThread 繼承自 ClientTransactionHandler,ClientTransactionHandler 通過呼叫 ActivityThread 的 sendMessage 方法向 ActivityThread 中的 Handler 發送訊息,它會發送 EXECUTE_TRANSACTION 訊息到 ActivityThread.H 里繼續處理,
- ClientLifecycleManager.scheduleTransaction -> ClientTransaction.schedule -> ApplicationThread.scheduleTransaction -> ClientTransactionHandler.scheduleTransaction -> 發送訊息給 ActivityThread.H
Android 系統啟動流程是什么(提示:init 行程 -> Zygote 行程 -> SystemServer 行程 -> 各種系統服務 -> 應用行程)?
- 啟動電源以及系統啟動:當電源按下時引導芯片從預定義的地方(固化在ROM)開始執行,加載引導程式BootLoader到RAM,然后執行,
- 引導程式BootLoader:BootLoader是在Android系統開始運行前的一個小程式,主要用于把系統OS拉起來并運行,
- Linux內核啟動:當內核啟動時,設定快取、被保護存盤器、計劃串列、加載驅動,當其完成系統設定時,會先在系統檔案中尋找init.rc檔案,并啟動init行程,
- init行程啟動:初始化和啟動屬性服務,并且啟動Zygote行程,
- Zygote行程啟動:創建JVM并為其注冊JNI方法,創建服務器端Socket,啟動SystemServer行程,
- SystemServer行程啟動:啟動Binder執行緒池和SystemServiceManager,并且啟動各種系統服務,
- Launcher啟動:被SystemServer行程啟動的AMS會啟動Launcher,Launcher啟動后會將已安裝應用的快捷圖示顯示到系統桌面上,
Service
服務并不是運行在一個單獨的行程中,而是創建于創建服務時所在的應用程式行程,Service 默認不會開啟執行緒,若要處理耗時操作,需要自己開啟作業執行緒,使用時需要先在 AndroidManifest 中注冊服務,然后在組件中開啟服務,
啟動方法
- startService:開啟 Service,與呼叫者生命周期無關,呼叫者退出后仍然運行
- bindService:系結 Service,與呼叫者生命周期相關,呼叫者退出后也隨之退出
生命周期
- 只是呼叫 startService() 啟動服務:onCreate()->onStartCommand()->onDestroy()
- 只是呼叫 bindService() 啟動服務:onCreate()->onBind()->onUnBind()->onDestroy()
- 同時使用 startService() 和 bindService():onCreate()->onStartCommand()->onBind()->onUnBind()->onDestroy()
Activity 與 Service 通信
使用 bindService 系結服務,呼叫 bindService 方法時,需要傳入一個實作了 ServiceConnection 介面的物件,在 onServiceConnected 方法中可以獲取到 Service 中的 onBind 方法回傳的 IBinder 物件,就可以進行 Activity 與 Service 間通信了,
也可以在 Service 中創建一個 Messenger 物件,傳入一個 Handler 作為引數,在 onServiceConnected 中拿到 Messenger 物件,通過 send 方法發送訊息,交給實體化 Messenger 時傳入的 Handler 物件處理,
- 服務只能被創建一次,onCreate 方法只會被回呼一次,之后呼叫 startService 方法只會回呼 onStartCommand 方法
- 若一個 Service 既呼叫了 startService,又呼叫了 bindService 方法,則必須呼叫了 stopService 和 unbindService 兩個方法時才會被 destroy
- onStartCommand 方法回傳值 START_STICKY,表示當服務由于例外被 kill 時,若情況允許系統會自動重啟服務,但是重啟后,傳入的 intent 物件為 null;START_REDELIVER_INTENT 時,系統會重啟服務并傳入 intent,
- BroadcastReceiver 中不能使用 bindService,因為 bindService 生命周期隨著組件,而廣播執行完 onReceive 中代碼就銷毀了
IntentService
HandlerThread
HandlerThread 就是一個封裝了 Looper 的 Thread 類,
- 在重寫的 run 方法中加了鎖,保證執行緒安全的獲取當前執行緒的 Looper 物件,獲取成功之后通過 notifyAll 方法喚醒其他執行緒,
- 在 getLooper 方法獲取當前執行緒 Looper 時加鎖,若 looper 物件為 null 時呼叫 wait 等待,等 run 方法中創建好 looper 物件后再回傳 looper 物件,
IntentService 內部機制
- 使用了 HandlerThread 實作,在 onCreate 方法中會實體化一個 HandlerThread() 物件,并且使用 HandlerThread 物件的 Looper 物件構造一個 Handler 物件 mServiceHandler,
- 通過 mServiceHandler 發送的訊息,都會在 HandlerThread 中執行,
- 每次通過 onStartCommand 方法傳過來的 Intent 物件,都會 通過 handler 的 handleMessage 方法回呼到 onHandleIntent 方法中,同一時刻只傳遞一個 Intent 物件,
- 所有請求都執行完之后,會呼叫 stopSelf 方法自動停止服務,
為什么在 mServiceHandler 的 handleMessage() 回呼方法中執行完 onHandlerIntent() 方法后要使用帶引數的 stopSelf() 方法?
因為 stopSelf() 會立即停止服務,而 stopSelf(int startid) 會等所有訊息都處理完之后才終止服務,一般情況下,帶引數的 stopSelf 方法在嘗試停止服務之前,會判斷最近啟動服務的次數是否和 startid 相等,相等就立刻停止服務,不相等則不停止,
為什么 bindService 可以跟 Activity 生命周期聯動
- bindService 執行時,LoadedApk 會記錄 ServiceConnection 資訊
- Activity 執行 finish 方法時,會通過 LoadedApk 檢查 Activity 是否存在未注銷/解綁的 BroadcastReceiver 和 ServiceConnection,如果有,會通知 AMS 注銷/解綁對應的 BroadcastReceiver 和 Service,并列印例外資訊,通知用戶應主動進行注銷/解綁,
如何保證 Service 不被殺死?
提高行程優先級,降低行程被殺死的概率
- 監控手機鎖屏解鎖事件,在螢屏鎖屏時啟動 1 個像素的 Activity,將當前行程提升為前臺行程,減少被系統殺死的概率,在解鎖時,將 Activity 銷毀
- 啟動前臺 Service(在 onStartCommand 中呼叫 startForeground() 方法把 Service 提升為前臺行程,在 onDestroy 中呼叫 stopForeground() 方法)
- 提升 Service 優先級
在 AndroidManifest.xml 檔案中為 Service 添加 節點,在 intent-filter 中添加 android:priority = "1000"設定最高優先級,1000 是最高值,陣列越小優先級越低,廣播同樣適用
在行程被殺死后,進行拉活
- 注冊高頻的系統事件的廣播接收器,如網路變化、解鎖螢屏、開機等
- 雙行程互相喚起
- 在 Service 的 onDestroy 中發送一個自定義廣播,收到廣播時,重啟 Service
依靠第三方
根據終端不同,MIUI 系統接入小米推送,華為手機接入華為推送等
BroadcastReceiver
應用場景
- 普通廣播:sendBroadcast() 發送,異步執行,廣播發出后,所有注冊的廣播接收器幾憾訓同時受到訊息,沒有先后順序,最常用的廣播
- 有序廣播:sendOrderedBroadcast(),發送出去的廣播會同步執行,廣播接收者按 priority 屬性從大到小排序執行,priority 屬性相同的,動態注冊的廣播優先,廣播接收者還可以在高優先級的廣播接收者中進行截斷和修改廣播,
- 本地廣播:LocalBroadcastManager,本地廣播發送的廣播只能在應用程式內部進行傳遞,并且廣播接收器也只能接受來自本應用程式的廣播,利用 Handler實作,利用了 IntentFilter 的 match 功能,提供訊息的發布與接收功能,效率比較高,
本地廣播不能靜態注冊,
注冊方式
- 靜態注冊:常駐系統,不受組件生命周期影響,即便應用退出,廣播還是可以被接收,耗電,占記憶體,
- 動態注冊:非常駐,跟隨組件生命周期變化,組件結束,廣播結束,在組件結束前,需要先移除廣播,否則容易造成記憶體泄露,
發送和接收的原理
- 繼承 BroadcastReceiver,重寫 onReceive 方法
- 通過 Binder 機制向 AMS 注冊廣播
- 通過 Binder 機制向 AMS 發送廣播
- AMS 查找符合相應條件的廣播,通過 IntentFilter/Permission 匹配,將廣播發送到 BroadcastReceiver 所在的訊息佇列中
- 廣播所在訊息佇列拿到廣播后,回呼它的 onReceiver() 方法
傳輸資料的限制
- 廣播是通過 Intent 攜帶需要傳遞的資料的
- Intent 是通過 Binder 機制實作的
- Binder 對資料的大小有限制,不同 Rom 不一樣,一般為 1M
ContentProvider、ContentResolver
簡介
- ContentProvider:管理資料,提供資料的增刪改查操作,資料源可以是資料庫、檔案、XML 等,ContentProvider 為資料的訪問提供了統一介面,可以用來做行程間資料共享,
- ContentResolver:ContentResolver 可以通過不同的 URI 操作不同的 ContentProvider 中的資料,外部行程可以通過 ContentResolver 與 ContentProvider 進行互動,
- ContentObserver:觀察 ContentProvider 中的資料變化,并將變化通知給外界,
ContentProvider 包括六個抽象方法,分別是:onCreate、query、update、delete、insert 和 getType,onCreate 是指 ContentProvider 的創建,一般初始化的作業在這里做,getType 用來回傳一個 Uri 請求所對應的 MIME 型別,
ContentProvider 初始化程序
ContentProvider 的 onCreate 方法是在 Application 的 attachBaseContext 和 onCreate 之間呼叫的,所以很多第三方庫利用了這個特點,在 ContentProvider 中去完成庫初始化,這樣接入方就不需要顯示的在 Application 中實體化三方庫,
ContentProvider 對應方法所在的執行緒
onCreate 由系統呼叫,并運行在主執行緒里,其它五個方法均由外界回呼并運行在 Binder 執行緒池中,
Fragment
Fragment 的生命周期與 Activity 之間的關系

生命周期
當呼叫了 FragmentTransaction#add 方法將一個 Fragment 添加到一個容器中時,Fragment 會按順序回呼如下方法
- onAttach(Context context):onAttach() 方法會在 Fragment 與 Activity 視窗關聯后立刻呼叫,從該方法開始,就可以通過 getActivity() 方法獲取到與 Fragment 關聯的 Activity 視窗物件了,但此時 Fragment 中的 View 還未初始化,所以不能操縱控制元件,
- onCreate(Bundle savedInstanceState):可以在 Bundle 物件中獲取一些在 Activity 中傳遞過來的資料,通常會在該方法中讀取保存的這狀態,獲取或初始化一些資料,不要進行耗時操作,不然 Activity 視窗不會顯示
- onCreateView(LayoutInfalter inflater,ViewGroup container,Bundle savedInstanceState):很重要的方法,在該方法中,創建 Fragment 顯示的 View,saveInstanceState 可以獲取 Fragment 保存的狀態
- onViewCreated(View view,Bundle savedInstanceState):view 引數是上一步創建好的,在 onCreateView 之后呼叫,在任何 saved state 恢復到 View 之前,子類可以在這里初始化自己的一些 view 相關的內容,
- onActivityCreated(Bundle saveInstanceState):在 Activity 的 onCreate() 方法執行完之后,會立即呼叫這個方法,表示 Activity 已經初始化完成
- onStart():同 Activity
- onResume():同 Activity
當呼叫了 FragmentTransaction#hide() 方法隱藏一個 Fragment 時
不會回呼生命周期方法
當呼叫了 FragmentTransaction#remove 方法將一個 Fragment 移除時,Fragment 會按順序回呼如下方法
- onPause
- onStop
- onDestroyView():onCreateView() 中的 View 將被移除
- onDestroy
- onDetach():Fragment 與 Activity 不再有關聯,
FragmentTransaction#replace()
等效于
- 先呼叫 FragmentTransaction#remove()
- 在呼叫 FragmentTransaction#add()
FragmentTransaction#addToBackStack 方法
addToBackStack() 保存的是一系列針對一個fragmentTransaction的操作記錄,按斬訓退堆疊 add 的 Fragment,按 back 鍵后會一級級回呼回去,
FragmentTransaction#beginTransaction()
.add(R.id.container, fragment1, Fragment1::class.java.simpleName)
.addToBackStack("a")
.commit()
回退堆疊常見的方法
- FragmentTransaction#addToBackStack(String name):將一個剛添加的 Fragment 加入到回退堆疊
- getSupportFragmentManager().getBackStackEntryCount():獲取回退堆疊中的數量
- getSupportFragmentManager().popBackStack():彈出堆疊頂 fragment
- getSupportFragmentManager().popBackStack(String name,int flags):根據 name 立刻彈出堆疊頂 fragment
- getSupportFragmentManager().popBackStack(int id,int flags):根據 id 立刻彈出堆疊頂 fragment
Fragment 的通信
- Fragment 可以通過 getActivity() 拿到 Activity 物件實體
- Activity 中可以通過為 Fragment 設定介面,監聽 fragment,通過介面傳資料給 Activity
- Activity 可以通過 Bundle 傳資料給 Fragment
- 可以使用 ViewModel 共享資料
FragmentPagerAdapter 與 FragmentStatePagerAdapter 的區別
- FragmentPagerAdapter 適用于頁面較少的情況,destroyItem() 方法中只是呼叫了 FragmentTransaction 的 detach 方法,
- FragmentStatePagerAdapter 適用于頁面較多的情況,每次切換 ViewPager 的時候是回收記憶體的,destroyItem 呼叫了 FragmentTransaction 的 remove 方法,
遇到過哪些 Fragment 的問題,如何處理的?
Fragment 視圖重疊
Activity 因為例外被殺死,恢復界面的時候,Fragment 頁面重疊了,
- 在 onSaveInstanceState 方法中,通過 FragmetnManager#putFragment(Bundle outState,String key,Fragment fragment) 方法將當前展示的 Fragment 存盤到 bundle 中,
- 在呼叫 FragmentTransaction#add 方法添加 Fragment 時,添加一個 tag
- 在 onCreate 中,先通過 FragmentManager#findFragmentByTag(String tag) 來找對應 tag 的 Fragment 是否存在,不為空則使用舊的 Fragment
- 然后判斷 saveInstanceState 是否為空,不為空則取出當前應該展示的 Fragment 展示
getActivity() 空指標
一般發生在異步任務里呼叫 getActivity() 方法,而 Fragment 已經 onDetach() 了,此時就會空指標,可以維護一個全域變數 mActivity,在 onAttach() 中賦值,同時也要記得在 Fragment 銷毀時,要停掉所有異步任務,
- 在 Fragment 中與子 Fragment 互動,用 getChildFragmentManager() 方法獲取 FragmentManager
- 在 Fragment 中不要使用 getActivity().startActivityForResult(),會回呼到 Activity 的 onActivityResult 方法中;要直接使用 startActivityForResult() 方法,
Window
Window 是一個抽象類,Activity、Dialog、Toast、PopupWindow 等都是依附于 Window 物件的,Winndow 的實際實作類是 PhoneWindow 類,
Window 的型別
ViewRootImpl 中的 Window 默認使用 WindowManager#LayoutParams(),而 WindowManager#LayoutParams 默認是 MATCH_PARENT,并且 type 是 TYPE_APPLICATION 型別的,Window 有三種型別:
- Application Window
- Sub Window
- System Window
Window 的添加程序
- 需要通過 WindowManager 的 addView 實作,最終 addView 方法會委托給 WindowManagerGlobal 中的 addView,會實體化一個 ViewRootImpl 物件,并呼叫它的 setView 方法,
- WindowManagerGlobal 中有一個 list,存盤了 Window 上的所有 View、所有 ViewRootImpl 和所有布局引數資訊,
- ViewRootImpl 中會呼叫 requestLayout 觸發布局程序,
- Window 的添加是一次 IPC 程序,通過 binder 機制,呼叫 IWindowSession 的 addToDisPlay 方法,將 Window 的添加請求交給 WMS 處理,IWindowSession 的實作類是 Session,且在 Session#addToDisPlay 方法中會對 Window 的 token 和 type 進行檢查,Dialog 使用 Application 的 Context 創建時報錯資訊就是在這里檢查出來報錯的,
Window 的洗掉程序
洗掉程序和添加程序一樣,都是先通過 WindowManagerImpl 后,呼叫委托給 WindowManagerGlobal 物件實作,WindowManagerGlobal#removeView 方法,
- 通過 findViewLocked 查找待洗掉的 View 索引
- 呼叫 removeViewLocked 通過 ViewRootImpl 來做進一步洗掉
- WindowManager 中提供了兩種洗掉介面,removeView 和 removeViewImmediate,異步和同步的洗掉,一般都是使用異步的
- ViewRootImpl#die 方法 -> doDie() 方法
- 垃圾回收相關作業
- 通過 Session 的 remove 方法洗掉 Window(IPC 程序,會呼叫 WMS 的 removeWindow 方法)
- 呼叫 View 的 dispatchDetachedFromWindow 方法,內部會呼叫 View 的 onDetachedFromWindow()
- 當 View 從 Window 中移除時,這個方法會呼叫,可以在這里做一些資源回收作業,如終止影片、停止執行緒
- 呼叫 WindowManagerGlobal 的 doRemoveView 方法重繪資料
Window 與 WindowManager
- Window 通過 Window#setWindowManager 與 WindowManager 關聯到一起
- 通過 WindowManager#addView 將 DecorView 與 WindowManager 關聯到一起
- Activity 的 attach 方法執行時,會創建 PhoneWindow 型別的 Window 物件,并且呼叫 setWindowManager 將 Window 與 WindowManager 關聯起來,在 performResumeActivity 中,通過呼叫 WindowManager#addView,將 Window 物件中的 DecorView 與 wm 關聯起來,
- Dialog 同理,是在建構式中創建 Window 物件并將 Window 與 WindowManager 關聯在一起,在 show 方法時,呼叫 wm#addView,將 decorview 放上去顯示,
View 相關
View 基礎知識
View 的位置引數
- View 的位置主要由它的四個頂點決定,left、top、right、bottom,(left,top) 是左上角頂點,(right,bottom) 是右下角頂點,
- 這些坐標都是相對于 View 的父容器來說的,也就是相對坐標
- width = right - left/height = bottom - top,在 onLayout 之后可以獲得
- getRawX、getRawY 是相對于螢屏左上角的坐標
Android 3.0 之后新增了幾個引數,x、y、translationX 和 translationY,x、y 是 View 的左上角,translationX 和 translationY 是 View 左上角相對于父容器的偏移量,默認是 0,
- View 在平移的程序中,top 和 left 表示的是原始左上角的位置資訊,是不會發生改變的,發生改變的是 x、y、translationX 和 translationY 四個引數,
- x = left + translationX
- y = top + translationY
MotionEvent 和 TouchSlop
MotionEvent
Android 中所有的事件都是針對 MotionEvent 的,通過它可以獲取到各種跟手指相關的互動,
getAction() 與 getActionMask()
- getAction 用于單點觸控,只能獲取到 ACTION_DOWN 等單點觸控事件
- getActionMask 用于多點觸控獲取事件,可以獲取到包括 ACTION_POINTER_DOWN 在內的多點觸控事件
事件
- ACTION_DOWN:第一根手指按下
- ACTION_UP:最后一根手指抬起
- ACTION_POINTER_DOWN:非第一根治按下
- ACTION_POINTER_UP:非最后一根手指抬起
常用方法
- getActionIndex(index):獲取非第一根按下手指或非最后一根抬起手指的 index
- getPointerId(actionIndex):根據 actionIndex 獲取手指的 pointerId,每根手指的 index 可能會變,但只要它未抬起,pointerId 是不會變的
- findPointerIndex(pointerId):根據 pointerId 尋找 index
TouchSlop
TouchSlop 是系統能識別出的被認為是滑動的最小距離,是一個常量,跟設備相關,使用這個常量可以幫助處理滑動,比如當兩次滑動事件的距離小于這個數,就不處理等,
ViewConfiguration.get(context).getScaledTouchSlop()
VelocityTracker、GestureDetector
VelocityTracker
速度追蹤,用于追蹤手指在滑動程序中的速度,包括水平速度和垂直方向的速度,使用如下,先在 onTouchEvent 方法中追蹤當前單擊事件的速度:
VelocityTracker vt = VelocityTracker.obtain();
vt.addMovement(event);
獲取當前速度:
vt.computeCurrentVelocity(1000); // 時間間隔,單位 ms
int xVelocity = (int) vt.getXVelocity();
int yVelocity = (int) vt.getYVelocity();
...
// 不使用了之后要重置并回收
vt.clear();
vt.recycle();
- 使用之前先計算速度,即 getXVelocity/getYVelocity 之前需要先呼叫 computeCurrentVelocity 方法,
- 速度指一段時間內手指一動的像素數,公式為:速度 = (終點位置 - 起點位置)/ 時間段
- 向著坐標系正方向,速度為正,向著坐標系負方向,速度為負,
- 不使用了之后要重置并回收
GestureDetector
手勢檢測,可用于在點擊、長按之外,監聽雙擊、滑動、放大縮小等手勢
- 宣告一個 GestureDetector 類,傳入一個實作了 OnGestureListener 介面的類,在 OnGestureListener 類里完成手勢監聽的處理,一般也可以用 SimpleOnGestureListener 來實作 OnGestureListener,
- 接管目標 View 的 onTouchEvent 方法,在待監聽 View 的 onTouchEvent 方法中回傳 mGestureDetector.onTouchEvent(event)
- OnGestureListener 中幾個重要方法
- onDown:手指輕觸螢屏的瞬間,由一個 ACTION_DOWN 觸發
- onSingleTapUp:回應單擊事件(輕輕觸摸后松開),伴隨一個 ACTION_UP 觸發
- onScroll:滑動事件
- onFling:快速滑動,按下螢屏,快速滑動后松開
- onLongPress:長按事件
- OnDoubleTapListener:雙擊監聽器
- onDoubleTap:雙擊事件,不可能和 onSingleTapConfirmed 共存
- onSingleTapConfirmed:單擊
- onDoubleTapEvent:表示發生了雙擊行為,雙擊期間,ACTION_DOWN、ACTION_MOVE 和 ACTION_UP 都會觸發這個方法
ScaleGestureDetector
監聽放大縮小(捏撐操作)
OnScaleGestureListener:監聽類,在回呼里處理自己的邏輯
View 的滑動
使用 View#scrollTo、View#scrollBy 函式
scrollBy 本質上呼叫的也是 scrollTo 函式,scrollTo 滑動到的是絕對位置,scrollBy 滑動到的是相對于之前的位置的偏移,
- mScrollX 和 mScrollY 單位都是像素
- 往 x、y 軸的正向滑動時,mScrollX 和 mScrollY 為負數
- 往 x、y 軸的負向滑動時,mScrollX 和 mScrollY 為正數
注意,不管是 scrollTo 還是 scrollBy,滑動的都是 View 中的內容,不是 View 容器本身的位置的改變,本質上 scrollTo 方法是對 View 可視區域的滑動,可以理解為 layout 的 l、t、r、b 的區域的改變
Scroller 彈性滑動
彈性滑動物件,用于實作 View 的彈性滑動,當我們使用 View 的 scrollTo 和 scrollBy 分發進行滑動時,程序是瞬間完成的,沒有過渡效果,使用 Scroller 可以使 View 實作影片效果的滑動,
Scroller 本身無法讓 View 彈性滑動,需要配合 View 的 computeScroll 方法使用,共同完成這個功能,
示例代碼:
// XXXView.java
...
private val scroller = Scroller(context)
fun smoothScroll(destX: Int, destY: Int) {
val startX = scrollX
val deltaX = destX - startX
scroller.startScroll(startX, 0, destX, 0, 1000)
invalidate()
}
override fun computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.currX, scroller.currY)
postInvalidate()
}
}
...
- 呼叫 Scroller#startScroller 方法時,只會記錄一些關鍵引數,不會執行其他操作,需要呼叫 invalidate 觸發 View 失效,讓 View 重繪
- View 的 onDraw 方法中會呼叫 computeScroll 方法,專門用于處理滑動的情況,父類中此方法是空實作,需要在自定義的子 View 中重寫該方法
- 在 computeScroll 中,需要呼叫 Scroller#computeScrollOffset 方法,回傳 true,則證明滑動未執行完,否則證明滑動執行完了
- computeScrollOffset 方法中會根據時間完成度算出當前 View 的 currentX 和 currentY 值,呼叫完 computeScrollOffset 之后,可以通過 Scroller#getCurrX() 和 Scroller#getCurrY() 來獲取當前的 x、y 坐標,通過 scrollTo 傳入引數執行滑動
- 若 computeScrollOffset 回傳為 true,別忘了繼續呼叫 invalidate 方法再次觸發重繪
Scroller 只會移動 View 的內容,并非 View 本身位置的改變
使用影片
View 的事件分發機制
View 的事件分發機制主要通過 dispatchTouchEvent()、onInterceptTouchEvent() 和 onTouchEvent() 三個方法完成,是否消費事件,主要取決于 ACTION_DOWN 事件是否回傳 true,
- dispatchTouchEvent():回傳 true,表示事件被當前 View 消費;回傳 super.dispatchTouchEvent() 表示繼續分發該事件,回傳 false 表示交給父 View 的 onTouchEvent 處理
- onInterceptTouchEvent():回傳 true,表示攔截事件,交給自己的 onTouchEvent 處理,回傳 false 不攔截,繼續傳遞給子 View,回傳 super.onInterceptTouchEvent() 分兩種情況
- 該 ViewGroup 存在子 View 并且點擊到了子 View 上,不攔截,繼續分發給子 View 處理,相當于 return false(默認不攔截)
- 該 ViewGroup 不存在子 View,或者存在子 View,但是沒點中,交給自己的 onTouchEvent 處理,相當于 return true
- onTouchEvent():回傳 true,自己處理事件,回傳 false,交給父 View 的 onTouchEvent 處理;回傳 super.onTouchEvent(),兩種情況
- 該 View 是 clickable 或 longclickable 的,會回傳 true,消費事件,自己處理
- 該 View 不是 clickable 或 longclickable 的,回傳 false,向上傳遞事件
一些重要結論
- 事件傳遞優先級,onTouchListener.onTouch > onTouchEvent > onClickListener.onClick
- 一個 View 的 onTouchEvent 回傳 true 攔截了 ACTION_DOWN 事件,那么后續的所有事件都會繼續發送給它,并且不再呼叫父 View 的 onTouchEvent
- 一個 View 呼叫 onInterceptTouchEvent 截獲了某次 ACTION_DOWN 后,那么后續的事件不再經過 onInterceptTouchEvent 方法,因為 dispatchTouchEvent 中判斷了當前不是 ACTION_DOWN,或者當前沒有字 View 處理事件時,intercepted 直接置為 true,不走 onInterceptTouchEvent 方法
- 正常情況下,一個事件序列只能被一個 View 截獲并消費
- 如果 View 不消耗除 ACTION_DOWN 以外的事件,這個點擊事件會消失,父元素的 onTouchEvent 不會被呼叫,并且當前 View 可以收到這個事件序列后續的事件,這些消失的事件都會傳遞給 Activity 處理
- ViewGroup 默認不攔截任何事件
- View 的 enable 不影響 onTouchEvent 的默認回傳值
- requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發程序,但是 ACTION_DOWN 事件除外,因為每次呼叫 dispatchTouchEvent 時,都會清除之前的狀態,其中就包括了 FLAG_DISALLOW_INTERCEPT 的 flag
事件分發流程
Android 中事件分發的流程其實就是個責任鏈模式,都是先從 Activity 開始,分發到頂層 View,頂層 View 再一層層遞回呼叫子 View,這是分發的程序,處理的程序就是從子 View 一層層向上回呼,流程如下所述
事件分發機制偽代碼:
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
- 一個 ACTION_DOWN 事件,先從 Activity 的 dispatchTouchEvent() 分發開始向上傳遞,Activity 的 dispatchTouchEvent 分發中,會呼叫 window 物件的 superDispatchTouchEvent 方法,此處若所有的 View 都不處理事件,則交由 Activity 的 onTouchEvent 方法自己處理,若處理,則因為 window 的實際實作類是 PhoneWindow,從 PhoneWindow 又向上傳遞最侄訓呼叫到頂層 View 也就是 PhoneView 中的 DecorView 中,DecorView 是 FrameLayout 的子類,則最終呼叫的就是 ViewGroup 的 dispatchTouchEvent 方法
- ViewGroup 中的 dispatchTouchEvent 方法,會先判斷是否是一個 ACTION_DOWN 事件,當前是否有子 View 處理事件,
- 若滿足其一(DOWN 事件,或有子 View 處理事件),呼叫 onInterceptTouchEvent 判斷是否攔截事件
- 若不是 ACTION_DOWN 并且沒有子 View 處理事件,則 intercepted 直接置為 true,后續事件不呼叫 onInterceptTouchEvent 方法,ViewGroup 自己處理此事件序列的事件
- ViewGroup 不攔截事件,遍歷 Children,判斷手指點擊落在哪個 Child 上
- 找到了 Child,呼叫它的 dispatchTouchEvent 方法
- Child 自己處理事件,將其賦值給 mFirstTouchTarget,breadk 跳出 for 回圈
- Child 自己不處理事件,dispatchTouchEvent 回傳 false
- 沒找到 Child,繼續往下走(ViewGroup 存在子 View,但是沒點中子 View 的情況,走自己的 onTouchEvent 方法,onInterceptTouchEvent 相當于回傳了 true)
- 找到了 Child,呼叫它的 dispatchTouchEvent 方法
- 判斷 mFirstTouchTarget 若為空,證明沒有 Child 處理事件,ViewGroup 自己處理事件,呼叫 super.dispatchTouchEvent
- mFirstTouchTarget 不為空,判斷當前 target 中是否有處理過事件,也就是上面有 Child 處理了事件的情況下,handle 標記為 true
- 否則走正常事件傳遞流程,呼叫子 View 的 dispatchTouchEvent 方法
ACTION_CANCEL 什么時候觸發,觸摸 Button,然后滑動到外部抬起會觸發點擊事件嗎,再滑動回去抬起會嗎?
- ACTION_CANCEL 和 ACTION_UP 一般都作為 View 的一段事件處理的結束,如果在父 View 中攔截 ACTION_UP 或 ACTION_MOVE,在第一次父 View 攔截訊息的瞬間,父 View 指定了子 View 不接受后續訊息了,同時子 View 會收到 ACTION_CANCEL 事件
- 如果觸摸某個控制元件,但是又不在這個控制元件的區域上抬起(移動出了 View 的范圍),會出現 ACTION_CANCEL
點擊事件被攔截,但是想傳遞到下邊的 View 怎么辦?
呼叫 view.requestDisallowInterceptTouchEvent(true) 請求父 View 不要攔截
如何處理滑動沖突?
滑動沖突,有幾種情況,一種是指容器和子 View 的滑動方向相同時的沖突;一種是容器與子 View 的滑動方向不同時的沖突;更復雜的情況是上述兩種摻雜在一起的時候,不過可以通過分別處理外層與中間層,中間層與內層的關系解決,針對第一種、第二種,典型的有 ScrollView 嵌套 RecyclerView、ViewPager 嵌套 RecyclerView 等,
- 對于滑動方向不一致的滑動沖突,可以通過多種方式界定是否要攔截,如:判斷某個方向的滑動距離大于另一個方向;判斷某個方向的滑動速度大于另一個方向;或者判斷滑動時與水平方向的夾角,
- 對于滑動方向一致的滑動沖突,可以通過業務需求規定何時需要攔截
滑動沖突有兩種解決方式:
- 外部攔截法:比較符合 Android 中事件的分發機制,在父 View 的 onInterceptTouchEvent 中根據業務需求,判斷父容器需要處理此事件,則攔截,否則不攔截,
- 內部攔截法:稍微復雜,當子 View 需要某個事件時,配合呼叫 requestDisallowInterceptTouchEvent 方法請求父容器不要攔截,在子 View 中自行處理,否則交由父容器處理,
繪制流程
View 的繪制流程(DecorView 是如何與 WindowManager 關聯到一起的?/setContentView 是如何將 View 添加到視圖上的?)(基于 API 24)
- ActivityThread 中執行到 preformLaunchActivity 時,會實體化 Activity 物件,之后會執行 Activity 的 attach 方法,在 Activity 的 attach 方法中會實體化 Window 物件、WindowManager 物件,接著會呼叫 Activity 的 onCreate 方法
- Activity 的 onCreate 方法中會呼叫 setContentView 將 View 設定進去,setContentView 實際上呼叫的是 window 物件的 setContentView 方法,而 window 的實際實作類是 PhoneWindow,在 PhoneWindow 的 setContentView 方法中會創建頂層 DecorView 物件,這就是最頂層的 View,PhoneWindow 會根據 Activity 的主題等加載一個布局到 DecorView,一般是包含一個 Title 和一個 Content,content 是一個 FrameLayout,取出這個 FrameLayout,賦值給 mContentParent 物件,最后將我們設定進去的 View inflate 出來 add 到這個 FrameLayout 中,
- 在 ActivityThread 中的 handleResumeActivity 方法中,會取出上一步實體化的 window 物件、windowManager 物件,以及 decorView 物件,然后執行 windowManager 的 addView 方法,WindowManager 的實作類是 WindowManagerImpl,WindowManagerImpl 中又將真正的實作委托給了 WindowManagerGlobal
- WindowManagerGlobal 中的 addView 方法中實體化了一個 ViewRootImpl 物件,并且呼叫了它的 setView 方法,將 View 傳遞進去,WindowManager 與 DecorView 就關聯到一起了
- setView 方法中會 requestLayout,最終執行 View 繪制流程的地方在 performTraversals 方法中,依次執行測量、布局和繪制,
DecorView 的 MeasureSpec 由視窗尺寸和其自身的 LayoutParams 共同決定;普通 View,它的 MeasureSpec 由父 View 的 MeasureSpec 和其自身的 LayoutParams 共同決定
Measure
- 自定義 View 時,如果需要自己測量寬高,需要重寫 onMeasure 方法,如果是 ViewGroup,則在 onMeasure 中完成所有子 View 的測量(如果子 View 是 ViewGroup,則會遞回這個程序),然后結合子 View 寬高、父 View 測量出來的自己的寬高,將最終結果通過 setMeasureDimension 設定進去
- 在 ViewGroup 中,measureChildren(measureChildWithMargins) 方法會遍歷測量 ViewGroup 中所有的 View,當 View 的可見性處于 GONE 時,不會測量
- 測量某個子 View 時,需要傳入父 View 的 MeasureSpec,結合子 View 自身的 LayoutParams 引數可以計算出子 View 的 MeasureSpec,然后呼叫子 View 的 measure 方法,子 View 的 onMeasure 方法,測量自身,將測量結果寫入 mMeasureWidth、mMeasureHeight
- setMeasureDimension 方法用于設定測量出來的寬高,如果 View 沒有重寫 onMeasure 方法,則默認的實作中,會傳入 getDefaultSize 來獲取 View 的寬高
- getDefaultSize 的引數是 getSuggestMinimumWidth 方法,該方法會判斷 View 是否有背景,如沒有,則取 minWidth 屬性,默認為 0,;如果設定了背景,則回傳 minWidth 和背景最小寬度中的最大值
- 直接繼承 View,若不在 onMeasure 中處理 AT_MOST 也就是 WRAP_CONTENT 的情況,則默認 WRAP_CONTENT 與 MATCH_PARENT 的行為一致,因為在 View 的 onMeasure 方法中,getDefaultSize 中,AT_MOST 和 EXACTLY 的行為是一樣的,取父 View 允許使用的最大值,
- 測量子 View 大小時,需要根據父 View 的 MeasureSpec 規格,加上子 View 自己的 LayoutParams 生成一個子 View 的 MeasureSpec,然后呼叫 child.measure() 方法測量子 View,子 View MeasureSpec 的生成可以看 ViewGroup 中的 getChildMeasureSpec() 方法,
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
Layout
- layout 方法確定 View 本身的位置,onLayout 方法確定所有子 View 的位置
- View 的 layout 方法會通過 setFrame 方法來設定 View 的四個頂點的位置(setFrame 方法中還會呼叫 onSizeChanged 回呼),即子 View 在父 View 中的位置(left、top、right、bottom),接著會回呼 onLayout 方法,讓父 View 確定子 View 的位置,如果是 ViewGroup,則需要實作這個方法,實作 ViewGroup 中所有 View 控制元件的布局流程,
layout 的作用
- 我們在 Measure 程序中獲取到子 View 的 measureWidth 和 measureHeight,在 Layout 程序中對子 View 進行擺放
- Measure 程序通過設定 PFLAG_LAYOUT_REQUIRED 標記告訴 View 需要進行 onLayout,而 Layout 程序通過清除 PFLAG_FORCE_LAYOUT 告訴 Measure 程序不需要執行 onMeasure 了
- View 的繪制需要 Canvas,Canvas 是有作用域限制的
- 對于硬體加速繪制來說,通過 Layout 程序中設定的 RenderNode 坐標
protected boolean setFrame(int left, int top, int right, int bottom) {
...
mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
...
}
* 對于軟體繪制來說
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
canvas.translate(mLeft - sx, mTop - sy);
}
getMeasuredWidth() 與 getWidth 的區別
- getMeasuredWidth 在 measure 執行過后,呼叫過 setMeasureDimensions 就可以獲取到,得到的是測量的寬;
- getWidth 需要在 onLayout 之后才能獲取到,是通過 mRight - mLeft 得來,是 View 真實的寬
- getMeasuredWidth 與 getWidth 一般情況下最后都是相等的
何時可以獲取真實的寬、高
- 重寫 View.onSizeChanged() 方法獲取
- 注冊 View.addOnLayoutChangeListener(),在 onLayoutChange 里獲取
- 重寫 onLayout 方法獲取
onDraw
Draw 的基本步驟:
- 繪制 View 背景
- 按需選擇是否 saveLayer
- 繪制 View 內容
- 繪制 View 的子 View,通過 dispatchDraw 傳遞繪制程序
- 按需繪制 View 的 fading 邊緣,并 restoreLayer
- 繪制 View 的裝飾,如滾動條
一些問題
ViewGroup 會呼叫 onDraw 嗎?為什么?
- ViewGroup 默認不會呼叫 onDraw,因為在 ViewGroup 的構造中,呼叫了 setFlags(WILL_NOT_DRAW, DRAW_MASK),等效于呼叫了 setWillNotDraw 方法,將其設定為了不可繪制的,等于將其標志為透明的,因為 ViewGroup 一般不需要繪制,所以系統為了優化,將其標記為不可繪制,在 View#draw 方法中,會判斷當前標記位,是不透明的,才會去呼叫 onDraw,
- 可以通過呼叫 setWillNotDraw(true),讓 ViewGroup 可以執行 onDraw
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
...
}
getWidth 和 getMeasureWidth 方法的區別?
- getWidth 方法需要等 layout 程序結束之后才能拿到結果,是 View 的 mRight - mLeft 的結果
- getMeasureWidth 方法在測量完成就可以拿到結果,是測量階段的寬(View 需要呼叫 setMeasuredDimension 方法)
- 一般情況下最終的 getMeasureWidth(測量程序中可能會經常改變,因為有的 ViewGroup 會執行多次測量流程) 和 getWidth 是相等的
onMeasure 有時候會執行多次?
- 父 View 可以使用 UNSPECIFIED dimensions 來將它的每個子 View 都測量一次來算出它們到底需要多大尺寸,如果所有這些子 View 沒被限制的尺寸(如 WRAP_CONTENT)的和太大或太小,那么它會用精確數值再次呼叫 measure() 方法,(父 View 的 onMeasure 方法中可能多次呼叫 child.measure() 方法)
- 在 Window 根 View 和 Window 自身的測量上,即 ViewRootImpl 的 performTraversals 中,measure 程序也可能被執行多次,
- measureHierarchy 方法中,會根據 DecorView 尺寸和 Window 的尺寸,去計算 RootView 的尺寸,最多計算三次
- 如果與 Window 關聯的 DecorView 寬度是 WRAP_CONTENT,并且傳入的期望寬大于系統預置的 baseSize,則會根據 base 重新測量一次寬
- 如果還是太小擺放不下,則會擴大 baseSize,重新測量
- 還是不能滿足要求,使用期望的寬高進行測量
- measureHieracrchy 后邊還會執行一次 measure 流程
- measureHierarchy 方法中,會根據 DecorView 尺寸和 Window 的尺寸,去計算 RootView 的尺寸,最多計算三次
如何在 Activity 啟動時獲取到 View 寬高?
View 的 measure 程序和 Activity 的生命周期方法不是同步執行的,如果 View 還未測量完,則在 onCreate、onStart、onResume 中都不能獲取到 View 的寬高資訊,可以通過如下幾種方式解決:
- 在 Activity/View 的 onWindowFocusChanged 方法中:此時 View 已經初始化完畢,當 Activity 的視窗獲得焦點和失去焦點均會被呼叫,如果頻繁執行 onResume 和 onPause,此方法會被呼叫多次
- 呼叫 view.post 方法,在 runnable 中獲取 View 的寬高,詳情見下邊解釋
- 獲取 View 的 ViewTreeObserver:view.getViewTreeObserver().addonGlobalLayoutListener:當 View 樹的狀態發生改變,比如 View 可見性改變等,會觸發 onGlobalLayout 方法,會被觸發多次,記著移除
- ViewTreeObserver#dispatchOnGlobalLayout() 是在 ViewRootImpl 的 performTraversals 中執行完 performLayout 之后執行的,所以可以獲取到 View 寬高,但是此時繪制任務還未開始

- 自己呼叫 view.measure() 方法測量:需要分情況處理,且 match_parent 時測不出,
為什么能在 View#post 中獲取到 View 寬高?
- 呼叫 View 的 post 方法時,如果 attachInfo 不為空,則直接呼叫 attachInfo 中的 handler 的 post 方法將訊息發送出去,attachInfo 不為空時,View 的三大流程已經執行完了,這時可以獲取到 View 的寬高,
- 另一種情況是 attachInfo 為空時,比如在 onCreate 中呼叫 View#post 方法,此時 View 的三大流程還未執行完,會將 Runnable 訊息 post 到一個 HandlerActionQueue 中等待執行,當 ViewRootImpl 呼叫 View 的 dispatchAttachedToWindow 方法時,會將 HandlerActionQueue 中的 runnable 訊息真正的 post 出去,放到訊息佇列尾部,當執行到它們時,View 的測量已經完成,所以可以拿到 View 的寬高,
聊聊對子執行緒不能更新 UI 的看法?
子執行緒不能更新 UI,本質上是因為在與 ViewRootImpl 創建時的執行緒不是同一個執行緒的時候,不允許更新 UI,正常情況下 ViewRootImpl 是在 ActivityThread 中的 handleResumeActivity 中呼叫 WM#addView 方法時,在 WindowManagerGlobal 中創建的,此時是主執行緒,當在子執行緒更新 UI 時,會一層層傳遞到最終的 ViewRootImpl 中的 requestLayout 方法里,requestLayout 方法中會對執行緒做檢查,若不等,拋例外,
如何在子執行緒更新 UI?
一種思路是規避掉 ViewRootImpl#requestLayout 中的執行緒檢查機制;一種是創建一個新的 ViewRootImpl 環境,
- 在 ViewRootImpl 未創建成功之前更新,也就是在 onCreate 中更新 UI
- 開啟硬體加速,并為 View 設定固定寬高,此時重繪 View 會跳過 checkThread 流程,走硬體加速的繪制流程
- 在 SurfaceView 中可以拿到一個 canvas 物件,可以直接在其他執行緒更新 UI
- 在子執行緒創建一個 ViewRootImpl 物件(WindowManager#addView 方法中會創建一個 ViewRootImpl),然后更新 UI
View#post 傳入的 Runnable 一定會被執行嗎?
不一定,若在 post 出去的 Runnable 執行之前,呼叫了 remove 方法,將 Runnable 移除調了,那么就不會執行,而且 API 不同版本區別如下:
- API 24 以下不一定,API 24 以下,在 ViewRootImpl 中使用了 ThreadLocal 存盤 HandlerActionQueue,如果在 ViewRootImpl 創建出來之前,在子執行緒 post 出去一個訊息,ViewRootImpl 將永遠拿不到這個訊息
- API 24 以上一定會執行,API 24 以上去掉了 ThreadLocal 存盤 HandlerActionQueue 的設計,在 performTraversals 方法中會呼叫 View 的 dispatchAttachedToWindow 方法,里邊會使用 attachInfo 中的 handler 將訊息都 post 出去執行
requestLayout、invalidate、postInvalidate 的區別與聯系?
- 他們都能起到重繪 UI 的效果
- invalidate 和 postInvalidate 方法只會呼叫 onDraw 方法進行重繪,requestLayout 會重新呼叫 onMeasure、onLayout,以及有可能呼叫 onDraw
- 呼叫了 requestLayout 方法后,會為 View 添加一個 FLAG_FORCE_LAYOUT 標記,這個標記是用來表示在 measure 方法中判斷是否要執行 onMeasure 的,這個標記會在 layout 結束時清除,同時遞回呼叫父 View 的 requestLayout 方法,最終到 ViewRootImpl 的 requestLayout 方法觸發 performTraversal 處理該事件,將 mLayoutRequested 標記為 true,出發 measure 和 layout,如果 layout 程序中發現 l、t、r、b 跟之前不同,就會觸發 invalidate,也就是會觸發 onDraw
- invalidate 和 postinvalidate 相同,只不過 postinvalidate 將訊息 post 到主執行緒執行,invalidate 會為該 View 添加一個標記位,同時遞回呼叫父 View 的 invalidateChildInParent 方法,直到傳遞到 ViewRootImpl 中,最終觸發 performTraversals,由于 mLayoutRequested 為 fasle,所以不會導致 onMeasure 和 onLayout 的呼叫,onDraw 會被呼叫
自定義 View
自定義 View 的幾種型別
- 繼承自 View,重寫 onDraw 方法,一般適用于一些特殊不規則的效果,在 onDraw 函式中自己處理繪制(如:下邊圓形的計時 View)
- 繼承自特定的 ViewGroup,如 FrameLayout,處理組合 View 的情況,比如 XXTitleView,將某幾個 View 組合成一個自定義 View,在里邊處理 View 固有的邏輯,接入開發者不需要關心內部實作,直接接入使用即可,
- 繼承自某個 View,如:繼承自 TextView 等,一般用于擴展某種已有 View 的功能
- 繼承 ViewGroup,重寫 onLayout,一般適用于實作某些特殊的布局,如:下邊的 TagLayout
Android 的資料存盤
Android 中的資料存盤一般包括檔案存盤、SharedPreferences 存盤、資料庫存盤和 ContentProvider 共享資料
檔案存盤(I/O)
檔案存盤分為內部存盤和外部存盤,外部存盤又分為私有外部存盤和公共外部存盤,訪問本 App 內部的資料不需要訪問權限,訪問其他部分的,如 sdcard 中的某個檔案/夾需要讀寫權限,
- 從內部存盤空間訪問:getFilesDir() 或 getCacheDir() 方法,不需要任何權限,檔案目錄為
data/data/package_name/xxx- Context 的方法
- App 洗掉時,內容會被洗掉
- Context#getExternalFilesDir:
- 從 API 19 之后,對此方法回傳的路徑上的檔案不需要讀寫權限,但是只適用于當前呼叫 App 的包名路徑下的檔案
- 訪問別的 package 下的檔案,還是需要
android.Manifest.permission#WRITE_EXTERNAL_STORAGE、android.Manifest.permission#READ_EXTERNAL_STORAGE權限 - App 洗掉時,內容也會洗掉
- App 相關的一些資源可以使用 getExternalFilesDir,放在 Application 對應目錄下
- 檔案目錄為
sdcard/Android/data/package_name/files/xxx,sdcard 對應storage/emulated/0/...
- Environment#getExternalStoragePublicDirectory:外部公共目錄,操作需要讀寫權限,所有 Application 均可訪問
- 檔案目錄為
sdcard/Music(Pictures)...,sdcard 對應storage/emulated/0/... - 不隨 App 洗掉而洗掉
- 檔案目錄為
SharedPreferences
適用于保存少量的資料,比如一些配置資訊等,核心原理是將資料的鍵值對保存到一個 xml 檔案中,Sp 物件本身只能用于獲取資料,需要呼叫 SharedPreferences#edit() 方法獲取一個 Editor 物件用于 commit,
- Sp 的 get 最終都會呼叫到 ContextImpl#getSharedPreferences(File file,int mode) 方法
- ContextImpl 中會先去 ArrayMap 中找對應 file 的 sp 實體,找不到會構建一個 SharedPreferencesImpl 物件,并根據檔案放入 ArrayMap
- sp 的創建程序是執行緒安全的
- SharedPreferencesImpl 的建構式中,會開啟一個執行緒去異步加載磁盤資料,并決議檔案,將 key-value 對保存在 mMap(HashMap) 中,
- 還會保存檔案的修改時間戳(mStatTimestamp)和大小(mStatSize),用于跨行程的情況
- 呼叫 notifyAll() 方法喚醒其他等待執行緒資料加載完畢
- 如果 sp 檔案過大,則會導致卡頓
sp 檔案存盤在手機的 `data/data/package_name/shared_prefs/ 目錄下生成一個 xml 檔案存盤資料,
SharedPreferences.Editor 的 commit() 和 apply() 方法區別,如果寫入失敗了會怎樣?
- commit() 方法是同步的,并發呼叫時會阻塞,方法會回傳一個 boolean 值,新的 value 寫入成功后回傳 ture,失敗回傳 false,
- apply() 方法是異步的,沒有回傳值,如果 commit() 時有正在執行的 apply 方法,則會 block,直到 apply() 執行完成才會執行 commit(),apply() 寫入失敗不會有任何提示,
Editro 的真正實作類是 SharedPreferences.EditorImpl 類,當呼叫 Editor#putXXX 方法時,實際上并不會立刻將修改同步到檔案中,而是會保存在一個 mModified(HashMap) 中,當呼叫 commit() 或 apply() 方法時,才會將 mMap 與 mModified 中的值進行合并處理,并寫入到磁盤檔案中,
commit()
- 將對 editor 的操作記錄(mModified)同步到 mMap 中
- 通過 enqueueDiskWrite 方法,將資料同步寫入磁盤,需要等待寫入完成
- 通知監聽(registerOnSharedPreferenceChangeListener)
- 回傳執行結果
apply()
- 將對 editor 的操作記錄同步到 mMap 中
- 異步將資料寫入磁盤(通過一個 HandlerThread)
- 不需要等待寫入完成,直接回傳,沒有回傳值
SharedPreferences 是否可以跨行程使用?
可以,為 SharedPreferences 設定 MODE_MULTI_PROCESS Flag 就可以,用于一個 App 有多個行程,又想往同一個 Sp 中寫入資料時,但是目前官方已經標記為廢棄,不建議使用了,
SharedPreferences#getXXX() 方法
- getXXX 方法是執行緒安全的,加了 synchronized 關鍵字
- getXXX 方法是直接操作記憶體的,直接從 mMap 中根據傳入的 key 讀取 value
- getXXX 方法有可能阻塞,卡在 awaitLoadedLocked 方法,第一次呼叫 getSharedPreferences 方法時,會創建一個執行緒去異步加載資料,當資料未加載完時呼叫 getXXX 方法,此時 mLoaded 為 false,所以會導致其呼叫 wait() 等待,需要等待資料加載完呼叫 notifyAll() 來喚醒繼續執行
SharedPreferences#putXXX() 方法
- putXXX 方法需要先通過 sp 獲取到一個 Editor 物件,實作類是 EditorImpl
- 不會直接對 mMap 做操作,對鍵值對的修改記錄保存在一個 Map 中,在 commit/apply 時同步到記憶體以及磁盤資料中,
SharedPreferences 使用注意事項
- 第一次構建 SharedPreferences 的時候開啟了一個子執行緒從磁盤獲取資料(后面走快取),不會阻塞 SharedPreferences 的構建,但是會阻塞 getXX/putXX/remove/clear 等呼叫,
- 不要使用 sp 在多行程場景使用,沒有跨行程的鎖,有可能導致資料丟失錯亂,
- 每個 sp 檔案不能過大,sp 的檔案存盤性能與檔案大小相關,不要將毫無關聯的配置保存在同一個檔案中,同時考慮將頻繁修改的條目單獨隔離出來
- 還是每個 sp 檔案不能過大,第一次呼叫 getSharedPreferences() 方法時,會先加載 sp 檔案進記憶體,過大的檔案會導致阻塞甚至 ANR
- 每個 sp 檔案不能過大,每次 apply 或 commit,都會把全部的資料一次性寫入磁盤,sp 檔案過大會影響性能,
- apply 雖然是異步寫,但會將異步任務放在 QueuedWork 中,在 Activity、Service 等組件結束時,會遍歷 QueuedWork 中的任務進行執行,如果 sp 檔案過大,會導致 Activity、Service 生命周期阻塞
- ActivityThread#handlePauseActivity -> QueuedWork.waitToFinish()
資料庫存盤
SQLite 是一個輕量級關系型資料庫,運算快,占用空間小,
SQLiteOpenHelper 類
是 SQLiteDatabase 的幫助類,用于管理資料庫的創建和升級,是抽象類,有兩個重要方法,onCreate 和 onUpgrade,用于創建和升級資料庫
SQLiteDatabase 類
通過 SQLiteOpenHelper#getReadableDatabase 或者 getWriteableDatabase 可以獲取一個 SQLiteDatabase 物件,然后操縱它完成資料的增刪改查,
- SQLiteOpenHelper 有兩個回呼方法,onCreate 和 onUpgrade,建構式中有一個引數是 version,資料庫版本號,構建 SQLiteOpenHelper 時,如果傳遞的版本號大于之前的版本號,會自動回呼 onUpgrade 方法,在這里完成資料庫的升級操作,觸發此方法,會將資料庫老版本和新版本都作為引數傳遞過來
- Android 使用 getWritableDatabase 和 getReadableDatabase 都可以獲取一個用于操縱資料庫的 SQLiteDatabase 實體
- getWriteableDatabase 是以讀寫方式打開,資料庫滿了,資料庫只能讀不能寫,此時會報錯
- getReadableDatabase 是先以讀寫方式打開資料庫,如果磁盤滿了,會打開失敗,繼續以只讀方式嘗試打開資料庫,如果該問題成功解決,只讀資料庫物件關閉,回傳一個可讀寫的資料庫物件
ContentProvider 讀取資料
- ContentProvider 忽略了資料底層實作,支持從 xml、資料庫、檔案等資源的讀取,通過一系列的 Uri 來操縱資料
- 宣告、注冊 ContentProvider
- 其他 App 通過 ContentResolver 匹配對應 Uri 來讀取共享資料的 app 的資料
影片
ObjectAnimator
原理是通過 TypeEvaluator,計算對應影片完成度時,屬性的具體值是什么,通過不停改變屬性的值,然后呼叫 invalidate() 方法使 View 無效,等待下一幀 View 重繪到來時重繪 View,來達到影片的效果,可以對任意 Object 物件做屬性影片,
使用
val animator = ObjectAnimator#ofXXX()
animator.start()
使用 ObjectAnimator 的要求
- 要添加影片效果的物件的屬性必須有 set() 形式的 setter 函式和 get() 形式的函式(采用駝峰式大小寫),
- 如果是自定義的屬性,則還要在其 setter 函式中手動呼叫 invalidate() 函式,使 View 失效,以便在下次重繪的時候使用最新值更新 View
invalidate() 方法是把 View 標記為失效,下一幀到來的時候會重繪 View,
PropertyValuesHolder#setupStartValue(target);
設定監聽
animator.addUpdateListener(object :ValueAnimator.AnimatorUpdateListener{
override fun onAnimationUpdate(animation: ValueAnimator?) {
}
})
AnimatorSet
管理多個 ObjectAnimator,可以讓其按照某個順序執行
val animatorSet = AnimatorSet()
// animatorSet.playSequentially(animator1, animator2) 按順序執行
// animatorSet.playTogether(animator1, animator2) 一起執行
animatorSet.start()
Interpolator
插值器,用于設定時間完成度到影片完成度的計算公式,就是設定影片的速度曲線,比如,LinearInterpolatro(勻速插值器)、AccelerateInterpolator(加速插值器)、AccelerateDecelerateInterpolator(先加速后減速插值器,默認的)等,
TypeEvaluator
設定影片完成度到屬性具體值的轉換計算,比如,影片完成了百分之二十時,具體的屬性值應該是多少,
可以通過仿照 FloatTypeEvaluator 來寫出自定義的 TypeEvaluator,完成影片完成度到屬性 value 值的映射關系,
硬體加速
- API14 之后默認開啟
- 可以配合 ViewPropertyAnimator 使用,提高繪制速度,只有 ViewPropertyAnimator 中提供的屬性支持贏家加速
view
.animate()
.translationX(200f)
.withLayer() // 開啟硬體加速
- 開啟硬體加速會使用 GPU 繪制,提高繪制效率,因為硬體加速會記錄繪制指令,對繪制做優化,
- 硬體加速有兼容性問題,有的繪制不支持硬體加速
離屏緩沖
View#setLayerType()
針對整個 View,不能針對 onDraw() 里某個程序
- setLayerType(LAYER_TYPE_HARDWARE):開啟離屏緩沖,并使用硬體繪制
- setLayerType(LAYER_TYPE_SOFTWARE):開啟離屏緩沖,并使用軟體繪制,一定意義上算是可以關閉硬體加速,因為 Google 沒有顯示的提供一個關閉硬體加速的方法
- setLayerType(LAYER_TYPE_NONE):不使用離屏緩沖
Canvas#saveLayer()
針對 Canvas,針對某一繪制程序,所以可以在使用時用 saveLayer 包住需要離屏緩沖的代碼
Toast 原理
Android 中的所有 View 都是依附于 Window 存在的,Activity、Dialog、Toast、PopupWindow、Menu 等
- Toast 的顯示隱藏其實是一個 Binder 程序
- Toast.makeText() 時會創建 Toast 物件和一個 TN 物件,TN 物件是負責與 NMS(NotificationManagerService 這個 Binder 執行緒互動的物件)
- 呼叫 Toast show 方法時,會取 system_server 中的 NMS,呼叫 NMS 的 enqueueToast 方法,同時將 TN 物件作為 Binder 互動的 callback 傳遞進去
- NMS 中根據包名維護了一個 mToastQueue,本質是一個 ArrayList,ArrayList 中存的是 ToastRecord 物件,
- enqueueToast 方法中會判斷同一個 pkg 下 toast 的數量,超出 25 就不會彈出 toast
- 在 enqueueToast 中構建一個 ToastRecord 物件,將傳入的 TN 物件賦值給 ToastRecord 的 callback 屬性,存到 mToastQueue 中,呼叫 showNextToastLocked 方法,
- showNextToastLocked 會取 mToastQueue 第 0 個元素,呼叫 ToastRecord 物件的 callback 欄位的 show 方法,
- 回到 Toast 的內部類 TN 的 show 方法中,因為 Toast 的顯示隱藏是個 Binder 程序,在 Binder 執行緒中執行,所以需要通過 Handler 將 Toast 訊息發送回創建 Toast 的執行緒,在 mHandler 中處理 Toast 的訊息,
- handleShow 就是 show toast 的流程,呼叫 WindowManager 的 addView
適配
螢屏適配
Android 中的 dp 在渲染前會將 dp 轉為 px,計算公式為:
- density = dpi / 160
- px = density * dp
- px = dp * (dpi / 160)
dpi 是根據螢屏真實解析度和尺寸來計算的,每個設備都可能不一樣(使用系統 Api 可以直接獲取到)
螢屏尺寸、解析度、像素密度三者關系
通常情況下,一部手機的解析度是寬x高,螢屏大小是以寸為單位,那么三者的關系是:

螢屏解析度為:1920*1080,螢屏尺寸為5吋的話,那么dpi為440,

使用第三方適配庫
AndroidAutoSize
寬高限定符適配
窮舉可能用到的 Android 設備的寬高像素值,創建對應的 values-AxB 檔案夾,如:values-480x320,設定一個基準解析度,其他解析度通過這個基準解析度來計算,在不同尺寸檔案夾內創建不同的 dimens 檔案,根據定義好的基準解析度計算各個檔案夾中的值,

比如以480x320為基準解析度
- 寬度為320,將任何解析度的寬度整分為320份,取值為x1-x320
- 高度為480,將任何解析度的高度整分為480份,取值為y1-y480
那么對于800*480的解析度的dimens檔案來說,
x1=(480/320)*1=1.5px
x2=(480/320)*2=3px
APP運行在不同解析度的手機中時,這些系統會根據這些dimens參考去該解析度的檔案夾下面尋找對應的值,
問題:這個方案有個問題,需要精確命中才能適配,比如 1920x1080 的手機必須要找到 1920x1080 的限定符,不然就只能用統一的默認的 dimens 檔案,使用默認尺寸,UI 又很可能變形,容錯機制差,
smallestWidth 限定符適配適配
原理
也叫 sw 限定符適配,指的是 Android 會識別螢屏可用寬高的最小尺寸(其實就是寬),然后根據識別到的結果去資源檔案中尋找對應限定符的檔案夾下的資源檔案,
比如,某手機的 dpi 是 480,橫向像素是 1080px,根據 px = dp(dpi/160),可以算出橫向的 dp 值是 1080/(480/160),也就是 360dp,系統會去尋找是否存在 value-sw360dp 的檔案以及對應的資源檔案,
與寬高限定符方案一樣,都是系統通過特定的規則來選擇對應的檔案,但是 sw 限定符適配有很好的的容錯性,如果沒有 value-swdp 檔案夾,系統會向下尋找,找到一個離目標值最近的檔案夾,比如沒有 value-360dp,則會向下尋找,比如找到了 value-350dp,則用這個檔案夾下邊的檔案,
使用
假設 UI 給定的標準是橫向 360dp,我們設備的寬為 375dp,則把 360dp 等分為 375 份,則在該設備上,1dp = 0.96dp,
缺點
多個 dimens 檔案可能導致 apk 變大問題,
跨行程通信
為何需要 IPC?
- 某些模塊可能因為一些特殊的原因需要運行在單獨的行程,比如單獨放在一個行程的下載模塊、圖片加載模塊等,
- 多個應用之間共享資料
- 為了加大一個應用可使用的記憶體,所以需要通過多行程來獲取多份記憶體控制元件,Android 中對單個應用的最大記憶體做了限制,一般都以主行程進行限制,而多行程可以打破這個限制,
直接進行跨行程通信可能出現的問題?
Android 為每個應用分配了一個虛擬機,或者說為每個行程分配了一個虛擬機,不同的虛擬機在記憶體分配上有不同的地址空間,這會導致在不同的虛擬機中訪問同一個類的物件會產生多份副本,一般會造成如下一些問題:
- 靜態成員和單例模式完全失效:每個行程獨立虛擬機造成的
- 執行緒同步機制完全失效:每個行程獨立虛擬機造成的
- Sp 的可靠性下降,這是因為 Sp 不支持兩個行程并發進行讀寫(系統會對 sp 的讀寫有一定的快取策略),有一定幾率導致資料丟失
- Application 會被創建多次:Android 系統在創建新的行程時,會分配獨立的虛擬機,所以這個程序其實就是啟動一個應用的程序,相當于系統又把這個行程重新啟動了一遍,
可以這樣理解多行程:相當于兩個不同的應用采用了 SharedUID 的模式,
AIDL
關鍵類和方法
- AIDL 介面,繼承 IInterface
- Stub 類:Binder 的實作類,服務端通過這個類來提供服務
- Proxy 類:服務端的本地代理,客戶端通過這個類呼叫服務端方法(本質上是一個 IBinder 物件,在 Service 的 onBind 方法中回傳)
- asInterface():客戶端呼叫,將服務端回傳的 IBinder 物件轉換成客戶端需要的 AIDL 介面型別的物件,如果客戶端和服務端位于同一行程,則直接回傳 Stub 物件本身,否則回傳系統封裝后的 Stub.proxy 物件(asInterface 方法中會判斷要回傳哪個物件)
- asBinder():回傳代理 Proxy 的 Binder 物件
- onTransact():運行在服務端的 Binder 執行緒池中,當客戶端發起跨行程請求時,遠程請求會通過系統底層封裝后交給此方法處理,
- transact():運行在客戶端,客戶端發起遠程請求的同時將當前執行緒掛起,之后呼叫服務端的 onTransact() 方法直到遠程請求回傳,當前執行緒才繼續執行,
語法
AIDL 的語法基本與 Java 保持一致,有以下幾點規則:
- AIDL 檔案以 .aidl 為后綴名
- AIDL 支持的資料型別如下
- 八種基本資料型別:int、short、long、float、double、boolean、char、byte
- String、CharSequence
- 實作了 Parcelable 介面的資料型別
- List 型別,List 承載的資料必須是 AIDL 支持的型別,或者是其它宣告的 AIDL 物件
- Map 型別,Map 承載的資料必須是 AIDL 支持的型別,或者是其它宣告的 AIDL 物件
- AIDL 檔案分兩類
- 一類用來宣告實作了 Parcelable 介面的資料型別,以供其它 AIDL 檔案使用那些非默認支持的資料型別(擴展的實作了 Parcelable 的 Javabean 物件,如下邊的 Book.aidl 檔案)
- 一類用來定義介面方法,宣告要暴露哪些介面給客戶端呼叫,定向 Tag 就是用來標注這些方法的引數值
- 定向 Tag
- 明確導包,在 AIDL 中宣告的介面檔案,需要手動指定 package,并明確標明參考到的資料型別所在的包名,即使兩個檔案處在通個包名下,
使用
- 在 main 目錄下,java 同級創建 aidl 檔案夾
- 在 aidl 檔案夾下,創建應用同包名目錄結構,然后在其中添加 aidl 檔案
- 如要參考 java 包下的類,如 Javabean 等,需要手動在 aidl 檔案中 import 參考,同時 aidl 檔案需要有 package,
// IBookManager.aidl
package com.example.interviewdemo; // 這個是必須的,否則編譯時 aidl 生成 java 檔案會找不到 Book 參考
import com.example.interviewdemo.Book; // 這個也是必須的
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
// Book.aidl
// Declare Book so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Book;
// Book.java 定義在 java 目錄下的工程中的 JavaBean
public class Book implements Parcelable {
public int bookId;
public String bookName;
protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
}
使用 CopyOnWriteArrayList
AIDL 方法是在服務端 Binder 執行緒池中執行的,當多個客戶端同時連接的時候,會存在多個執行緒同時訪問的情況,處理 List 的執行緒同步,可以使用 CopyOnWriteArrayList 進行 List 執行緒同步的管理,
AIDL 中所支持的是抽象的 List,List 是一個介面,因此雖然服務端回傳的是 CopyOnWriteArrayList,但是在 Binder 中會按照 List 的規范去訪問資料,最終生成一個 ArrayList 傳遞給客戶端,類似的還有 ConcurrentHashMap,
使用 AIDL 要注意的
- 客戶端呼叫遠程服務的方法,被呼叫的方法運行在服務端 Binder 執行緒池中,方法呼叫時,客戶端執行緒會被掛起,要注意如果是在 UI 執行緒,不要呼叫服務端耗時方法,
- 客戶端的 onServiceConnected 和 onServiceDisconnected 方法都運行在 UI 執行緒,所以也不可以在這里呼叫服務端耗時方法,
- 服務端呼叫客戶端的 listener 中的方法時,被呼叫的方法運行在客戶端的 Binder 執行緒池中,所以同樣不可以在服務端中呼叫客戶端的耗時方法,否則可能導致服務端無回應,
- 服務端訪問客戶端的 listener 中的方法時,客戶端的具體回呼此時是在客戶端 Binder 執行緒池中的,所以不能在這里訪問 UI 相關的操作,
AIDL 如何讓服務端呼叫客戶端的方法?
- 定義一個 AIDL 介面,向服務端注冊這個介面的回呼,并使用 RemoteCallbackList 管理,
- 在客戶端實作這個介面,同時因為服務端呼叫介面的方法是在 Binder 執行緒池中執行,還需要一個 Handler 將訊息發送到主執行緒中執行(類似 AMS 與 ActivityThread#ApplicationThread 中的互動,通過 H 這個 Handler),
RemoteCallbackList 的作用
RemoteCallbackList 是系統專門提供的用于洗掉跨行程 listener 的介面,支持管理任意的 AIDL 介面,因為所有的 AIDL 介面都是實作了 IInterface 的,
在 Binder 中使用 List 保存 listener 完成注冊與反注冊是否可行?為什么?
如果使用普通的注冊、反注冊介面的方式,也就是直接傳遞介面保存到 List 中的方式,那么反注冊時會找不到要反注冊的 listener,因為 Binder IPC 的程序中是不能直接傳遞物件的,物件的跨行程傳輸本質上都是序列化、反序列化的程序,所以 AIDL 中的物件都要實作 Parcelable 介面,Binder 會把客戶端傳遞過來的物件重新轉化并生成一個新的物件,
RemoteCallbackList 的作業原理
在多次跨行程傳輸中,客戶端的同一個物件雖然會在服務端生成不同的物件,但是這些新生成的物件的 Binder 物件是同一個,RemoteCallbackList 就是利用了這一特性,當客戶端接注冊時,遍歷服務端所有 listener,找到和解注冊的 listener 具有相同的 Binder 物件的服務端 listener 把它洗掉即可,這就是 RemoteCallbackList 做的事,
- RemoteCallbackList 內部維護了一個 Map 結構專門用來保存所有的 AIDL 回呼,這個 Map 的 key 是 iBinder 型別,value 是 Callback 型別
ArrayMap<IBinder,Callback> mCallbacks = new ArrayMap<IBinder,Callback>();
- Callback 中封裝了真正的遠程 listener,當客戶端注冊 listener 的時候,它會把這個 listener 資訊存入 mCallbacks 中,key 和 value 的獲得方式如下:
IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);
- 使用時先呼叫
int n = RemoteCallbackList#beginBroadcast(),獲取元素個數 - 通過遍歷 n,呼叫
RemoteCallbackList#getBroadcastItem(i)獲取某個 listener 物件,然后可以呼叫 listener 物件中的方法 - 最后需要呼叫
RemoteCallbackList#finishBroadcast方法清理 broadcast 的狀態
RemoteCallbackList 的特性
- 客戶端行程終止后,它能自動移除客戶端注冊的 listener
- RemoteCallbackList 內部自動實作了執行緒同步的功能,使用時不需要欄位外的執行緒同步作業
AIDL Binder 意外死亡如何處理
當服務行程意外停止時,需要重新連接服務,
- 給 Binder 設定 DeathRecipient 監聽,當 Binder 死亡時,會收到 binderDied 方法的回呼,在 binderDied 方法中可以重連遠程服務器,(在客戶端 Binder 執行緒池中被回呼)
- 在 onServiceConnectedDisconnected 中重連遠程服務,(在客戶端 UI 執行緒被回呼)
在 AIDL 中使用權限驗證
默認情況下,遠程服務任何人都可以連接,可以加入權限驗證功能,權限驗證失敗則無法呼叫服務器中的方法,
- 在 onBind 中進行驗證,不通過直接回傳 null,
- 在 AndroidManifest 中宣告權限,在 onBind 方法中做權限驗證,若 App 無這個權限,則直接回傳 null,系結也就失敗了,
- 在服務端的 onTransact 方法中進行權限驗證,驗證失敗直接回傳 false,
- 可以采用 permission 驗證
- 可以采用 Uid 和 Pid 驗證,通過 Binder#getCallingUid 和 getCallingPid 可以拿到客戶端所屬的應用的 Uid 和 Pid,通過這個引數可以做一些驗證作業,比如包名,
其他
Android 中的 Context
Context 是抽象類,繼承關系如下所示:

APK 打包流程

- 通過 AATP 工具將資源檔案和 AndroidManifest 等檔案打包生成 R.java 檔案
- AIDL 工具將 aidl 檔案生成對應的 java 檔案
- javac 工具將 .java 檔案生成 .class 檔案
- d8 工具將 .class 檔案打包生成 .dex 檔案
- apkbuilder 將資源檔案、dex 檔案打包成一個未簽名的 Apk
- jarsigner 工具使用簽名檔案對未簽名的 Apk 進行簽名
- zipalign 對簽名后的 Apk 進行對齊處理,對齊的程序就是將 Apk 檔案中所有的資源檔案的起始距離都偏移 4 位元組的整數倍,這樣通過記憶體映射訪問 Apk 檔案的速度會更快,
APK 的安裝流程

- 復制 Apk 到 /data/app 目錄下,解壓并掃描安裝包
- 資源管理器決議 Apk 里的資源檔案
- 決議 AndroidManifest 檔案,并在 /data/data 目錄下創建對應的應用資料目錄
- 對 dex 檔案進行優化,并保存在 dalvik-cache 目錄下
- 將 AndroidManifest 檔案決議出的四大組件資訊注冊到 PackageManagerService 中,
- 安裝完成后,發送廣播
APT(Annotation Processing Tool)
Annotation Processing Tool 注解處理器,是一種注解處理工具,用來在編譯期掃描和處理注解,通過注解生成 Java 檔案,以注解作為橋梁,通過預先定好的代碼生成規則來自動生成 Java 檔案,ButterKnife、Dragger2 中都使用了 APT,
Java API 已經提供了掃描原始碼并決議注解的框架,繼承 AbstractProcessor 類實作自己的注解決議邏輯,APT 的原理就是在注解了某些代碼元素(欄位、函式、類)后,在編譯時編譯器會檢查 AbstractProcessor 的子類,并自動呼叫 process() 方法,然后將添加了指定注解的所有代碼元素作為引數傳遞給該方法,開發者根據注解元素在編譯期輸出對應的 Java 代碼,
- 創建一個工程(Java or Kotlin Library)
- 在主專案的 build.gradle 中使用 annotationProcessor 引入
- 創建一個類 BindingProcessor,繼承 AbstractProcessor
- 在 main 包下創建 resources/META_INF/services 檔案夾
- 在該檔案夾下創建檔案 javax.annotation.processing.Processor
- 在上述檔案中引入 BindingProcessor 的參考
- 重寫 BindingProcessor 中的 process() 方法和 getSupportedAnnotationTypes() 方法
- getSupportedAnnotationTypes() 方法回傳要對哪些注解進行處理,是一個 Set 集合
- 在 process() 方法中使用 Javapoet 生成指定格式的代碼,最后通過 JavaFile 寫入檔案
如何處理全域例外捕獲(CrashHandler)
當執行緒由于未捕獲的例外即將終止時,JVM 會使用 Thread#getUncaughtExceptionHandler 方法查詢執行緒的 UncaughtExceptionHandler,并呼叫它的 uncaughtException(Thread) 方法,開發者可以在這個方法中捕獲例外,列印 log,寫入檔案等操作,
使用
- 定義 CrashHandler 類,實作 Thread.UncaughtExceptionHandler 介面
- 重寫 uncaughtException 方法,可以在里邊做列印例外資訊、寫日志等操作
- 在 Application 中實體化,呼叫
Thread.setDefaultUncaughtExceptionHandler(handler);方法將例外處理物件傳入進去(可以將 CrashHandler 定義為單例,提供一個 init 方法,在其中設定 setDefaultUncaughtExceptionHandler)
public class CrashHandler implements Thread.UncaughtExceptionHandler {
private Thread.UncaughtExceptionHandler mDefaultCrashHandler;
public void init() {
// 獲取系統默認的 UncaughtException 處理器
mDefaultCrashHandler = Thread.getDefaultUncaughtExceptionHandler();
// 設定該 CrashHandler 為程式的默認處理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("enter uncaughtException:" + e);
System.out.println("Crash Thread name:" + t.getName() + " Crash Thread id:" + t.getId());
// 1. 保存檔案到本地......
// 2. 上傳檔案到服務器等操作
// 如果系統提供了默認的例外處理器,則交給系統去結束程式,否則處理自己的邏輯
if (mDefaultCrashHandler != null) {
mDefaultCrashHandler.uncaughtException(t, e);
} else {
// 按照業務需求定義處理操作,可以殺死自己
Process.killProcess(Process.myPid());
}
}
}
// 在 Application 中使用
public class App extends Application{
@Override
public void onCreate(){
new CrashHandler().init();
}
}
注意
- 因為 Thread 中的 defaultUncaughtExceptionHandler 是個靜態成員變數,所以它的作用物件是當前行程的所有執行緒
- 代碼中被 catch 的例外不會交給 CrashHandler 處理,CrashHandler 只能收到那些未捕獲的例外
記憶體泄露
Android 中的記憶體泄露,一般是長生命周期的物件持有了短生命周期的物件,導致短生命周期物件無法釋放
- 單例導致的記憶體泄露:單例物件的生命周期等同于 App,如果一個物件沒有用了,但是單例還持有它的參考,則這個物件不能被正常回收,就泄露了,比如單例持有 Activity 的 Context
- 非靜態內部類:Java 中的匿名內部類隱式的持有外部類的強參考,如果在 Activity 中宣告,則會持有 Activity 的參考,可能導致 Activity 無法正常銷毀,如 Handler、Thread 等,一般這種匿名內部類常見于監聽器,
- 資源使用后未關閉:如 BroadcastReceiver、ContentProvider、File、Cursor、Stream、Bitmap 等,這些資源在讀寫操作時通常都使用了緩沖,如果不及時關閉,這些緩沖物件會一直占用記憶體得不到釋放,導致記憶體泄露
- WebView 造成的記憶體泄露:WebView 在加載網頁后會長期占用記憶體而不能被釋放,需要在 Activity 銷毀時,將其從父容器移除,然后呼叫它的 destroy 方法銷毀它以釋放記憶體
// 先從父控制元件中移除WebView
mWebViewContainer.removeView(mWebView);
mWebView.stopLoading();
mWebView.getSettings().setJavaScriptEnabled(false);
mWebView.clearHistory();
mWebView.removeAllViews();
mWebView.destroy();
- 屬性影片造成記憶體泄露:比如在 Activity 中開啟了屬性影片,但是 Activity 銷毀時沒有呼叫 cancel 方法,影片參考了 View,View 參考了 Activity,所以此時會導致 Activity 無法釋放
- 集合類記憶體泄露:如果將一個物件放入 ArrayList、HashMap 中,這個集合就會持有該物件的參考,當我們不需要這個物件時,如果沒有從集合移除,只要集合還在使用,這個物件就造成了記憶體泄漏,如果一個集合類是靜態的,集合里沒有使用的物件更會造成記憶體泄露了,應該及時將不需要使用的物件從集合 remove,或者 clear 集合,
- 未取消注冊或回呼導致的記憶體泄露:比如 BroadcastReceiver、或者是 MVP 中 P 層對 V 層的參考
- Timer 和 TimerTask:如果 TimerTask 中持有了 Activity 的 Context,如果 Activity 銷毀時,沒有 cancel 掉 Timer 和 TimerTask,Timer 還在繼續等待執行 TimerTask,那么就會導致泄露
查找記憶體泄露可以使用 AS 中自帶的 Android Profiler 工具或引入 LeakCanary 庫,
Context 相關
Android 應用里有幾種 Context 物件?
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-IOkiXxeN-1622169847533)(https://github.com/guoxiaoxing/android-open-source-project-analysis/raw/master/art/app/component/context_uml.png)]
Activity、Service、Application 都間接的繼承自 Context 類
- Dialog 中不能使用 Application context 創建 Dialog,驗證 token 會失敗拋出例外,Dialog 只支持以 Activity 的 token 創建,
- Activity 和 Service 以及 Application 的 Context 不一樣,Activity 繼承自 ContextThemWrapper,其他的繼承自 ContextWrapper
- Activity 和 Service 中使用 getApplication 獲取 Application,BroadcastReceiver 中使用 getApplicationContext 方法
- Context 的數量等于 Activity 的數量 + Service 的數量 + 1(Application)
Android 中行程的優先級?
- 前臺行程
用戶正在互動的 Activity 或 Activity 用到的 Service 等,系統記憶體不足時,前臺行程是最晚被殺死的 - 可見行程
可以是處于暫停狀態(onPause)的 Activity 或系結在其上的 Service,即被用戶可見,但由于失去焦點不能與用戶互動 - 服務行程
正在運行的,使用 startService() 方法啟動的服務,且不屬于上述兩個更高類別行程的行程,比如,在后臺播放音樂或下載資料, - 后臺行程
包含目前對用戶不可見的 Activity 行程(已經呼叫 onStop() 方法),這些進城一般對用戶體驗沒有直接影響,記憶體不足時會首先回收, - 空行程
不包含任何應用程式的行程,這樣的行程一般系統不會讓他存在
多行程場景遇到過嗎?
- 在新的行程中,啟動前臺 Service,播放音樂
- 多行程開發可以一定程度上避免 OOM,因為 Android 對記憶體的限制是針對主行程的,比如當我們要加載大圖的時候可以去新的行程執行,避免主行程 OOM,假如圖片瀏覽行程崩潰,不會影響主行程,
Android 類加載器
Android 平臺上虛擬機運行的是 Dex 位元組碼,是一種對 class 檔案優化后的產物,傳統 Class 檔案是一個 Java 原始碼檔案對應一個 .class 檔案,Android 中是把所有 Class 檔案進行合并,優化,最終生成一個 class.dex 檔案,目的是把不同 class 檔案中重復的東西只保留一份,如果 Android 應用不進行分 dex 處理,最后一個應用的 apk 只會有一個 .dex 檔案,
Android 中常用的類加載器有兩個,DexClassLoader 和 PathClassLoader,他們都繼承于 BaseDexClassLoader,區別在于呼叫父構造器時,DexClassLoader 可以多傳入一個 optimizedDerectory 引數,這個目錄必須是內部存盤路徑,用來快取系統創建的 Dex 檔案,PathClassLoader 該引數為 null,只能加載已經安裝到 Android 系統的 APK 檔案,DexClassLoader 可以加載任意目錄下的 dex、jar、apk、zip 檔案,不過這個引數在 API26 以上廢棄了,Android 中默認的類加載器是 PathClassLoader,
子執行緒可以彈 Toast 和 Dialog 嗎?正確姿勢是?
不可以,本質上 Toast 的實作依賴其中的一個靜態內部類 TN,TN 中創建了一個 Handler 處理 Toast 訊息,也就是 Toast 實際上是依賴于 Handler 的,我們呼叫 Toast.makeText().xx 方法時,內部會呼叫 Toast 的建構式,在建構式中會實體化 TN,傳入了一個 null 的 Looper,在 TN 的建構式中會創建一個 Looper 物件并新建一個 Handler,但是并沒有呼叫 Looper.loop() 方法,所以子執行緒彈 Toast 會報錯,
Dialog 內部也是基于 Handler 發送訊息,子執行緒如果未呼叫 Looper.prepare() 和 Looper.loop() 開啟回圈,也會報錯,
- 可以將 Toast 的彈出 post 到主執行緒
- 在子執行緒開啟 loop 回圈之后再 toast
MultiDex
使用 MultiDex 解決何事?根本原因在于?
Dalvik Executable 規范將可在單個 DEX 檔案內參考的方法總數限制為 65536(底層使用了無符號的 short 陣列),其中包括 Android 框架方法、庫方法以及自己的代碼中的方法,
使用 MultiDex 主要解決方法數 65535 限制的問題,即方法數不能超過 65535 個,
主 Dex 檔案放那些東西,跟其他 Dex 呼叫、關聯?
主 Dex 檔案存放應用啟動就必須加載的類,可以使用 multiDexKeepFile/multiDexKeepProguard 屬性宣告這些類,手動將其指定為 Dex 檔案中的必須類,
Odex 的作用?
Odex 的作用主要在預處理,可以縮短較長的增量構建時間,Odex 可以在構建之間重用 MultiDex 輸出,但只在 Android 5.0(API21)以上支持(ART),
規避 64K 限制
官方檔案
- minSdkVersion 為 21 或更高版本,系統會默認啟用 MultiDex,并且不需要 MultiDex 庫
- minSdkVersion 為 20 或更低版本,必須配置 MultiDex 庫并對專案做修改
- 修改 module 級 build.gradle 檔案以啟用 MultiDex,并將 MultiDex 庫添加到依賴項
android { defaultConfig { ... minSdkVersion 15 targetSdkVersion 28 multiDexEnabled true } ... } dependencies { implementation "androidx.multidex:multidex:2.0.1" }- 若不替換 Application 類,在 Manifest 檔案中配置:
- 如果替換 Application,繼承 MultiDexApplication
- 或者繼承自己定義的 Application,但在 Application 的 attachBaseContext 方法中配置 MultiDex.install(this) 方法
注意:在 MultiDex.install() 完成之前,不要通過反射或 JNI 執行 MultiDex.install() 或其他任何代碼,MultiDex 的多個 dex 檔案之間的跟蹤功能不會追蹤這些呼叫,從而導致出現 ClassNotFoundException,或因 DEX 檔案之間的類磁區錯誤而導致驗證錯誤,
組件化、插件化
組件化
組件化、模塊化是類似的,拆分成多個 module 開發就是組件化,
插件化
App 的部分功能模塊在打包時不以傳統方式打包進 apk 檔案中,可以放在網路上適時下載,在需要的時候動態對這些功能模塊進行加載,就是插件化,
這些單獨二次封裝的功能模塊 apk 就是插件,
插件化原理
插件化的原理是動態加載,通過自定義 ClassLoader 來加載新的 dex 檔案(這個 ClassLoader 持有這個插件的所有 .class 檔案),從而讓程式原本沒有的類可以被使用,就是插件化的原理,
- 構建一個 DexClassLoader/PathClassLoader
- 使用構建出來的 ClassLoader 物件加載外部 apk/dex 檔案(PathClassLoader 只能訪問 app 包下的內容,DexClassLoader 可以訪問任意路徑下的 apk 檔案)
- 通過反射去實體化插件中的類,然后呼叫其中的方法,
在宿主 App 不能加載插件中的 Activity,因為在宿主 App 的 AndroidManifest 中沒有宣告,可以通過 Fragment/View 的形式替代
DexClassLoader classLoader = new DexClassLoader(apk.getPath(), getCacheDir().getPath(), null, null);
try {
Class utilsClass = classLoader.loadClass("com.hencoder.plugin.Utils");
Constructor utilsConstructor = utilsClass.getDeclaredConstructors()[0];
utilsConstructor.setAccessible(true);
Object utils = utilsConstructor.newInstance();
Method shoutMethod = utilsClass.getDeclaredMethod("shout");
shoutMethod.setAccessible(true);
shoutMethod.invoke(utils); // 呼叫 utils 物件的 shoutMethod
Intent intent = new Intent();
intent.setClassName("com.hencoder.plugin", "com.hencoder.plugin.MainActivity");
startActivity(intent);
} catch (...) {
...
}
插件化的作用
- 早期用來解決 dex 65535 問題
- 可以減小安裝包大小
- 實作動態部署,模塊化發布
- bug 熱修復:在特定位置預留好功能點入口,點擊時,加載對應插件
常用的插件化框架
- 滴滴的 VirtualAPK
- 360 的 RePlugin
熱修復
常用于 bug 修復,或者是對軟體進行區域更新,
熱修復與插件化的區別
- 插件化的內容在原 App 中沒有,而熱更新是原 App 中的內容做了改動
- 插件化在代碼中有固定的入口,而熱更新則可能改變任何一個位置的代碼
熱修復的原理
- ClassLoader 的 dex 檔案的替換
- 利用 loadClass 類加載程序
- 直接修改位元組碼
通過干預 ClassLoader findClass 的程序實作熱修復
loadClass() 的類加載程序(Android 中類加載機制)
Android 中的類加載機制,也即是網上常說的雙親委托機制,其實就是一個帶快取的自上而下的加載程序,對于一個 ClassLoader 而言,加載一個類需要呼叫其 loadClass() 方法,loadClass 方法中,會做如下幾件事:
- 會先去自己的快取中找是否有 class
- 快取中沒有,則判斷是否有父 ClassLoader,若有父 ClassLoader,則向父 ClassLoader 要,是同樣的類加載程序
- 父 ClassLoader 中也沒有,則嘗試呼叫自己的 findClass 方法,加載并快取,且只有自己加載的 class 才會進行快取
也即,若父 ClassLoader 中有要尋找的 class,則即便子 ClassLoader 有能力加載,也不會重復加載,
Android 中默認的 ClassLoader 是 PathClassLoader,PathClassLoader 的父類是 BaseDexClassLoader,
類的加載機制沒辦法改變,所以 ClassLoader 這種方案的熱更新就是干預類自己的加載程序(應用類加載器),即 BaseDexClassLoader 的 findClass() 中的邏輯
通過 ClassLoader 方式熱修復的具體流程
- BaseDexClassLoader 的 findClass 中,會呼叫 DexPathList 的 findClass(className) 方法
- DexPathList#findClass() 方法中會遍歷 Element 陣列 dexElements,呼叫它的 findClass() 方法尋找 class
- Element 陣列是 DexPathList 實體化時,根據 path 初始化的,而 DexPathList 的初始化是 BaseDexClassLoader 的建構式中,即 Element 陣列中,存的是 class 檔案的路徑
- 所以熱更新的關鍵就是把補丁 dex 檔案加載放進一個 Element,并且插入到 DexPathList 的 Element 陣列中的最前面(將其他元素后移一位),使其最先加載
熱更新加載完成后,需要先殺死程式,清除掉已經加載過的 ClassLoader 的快取,再次打開 App 才能讓補丁生效,因為老的 class 檔案已經被加載過了,如果不殺死程式,根據雙親委派原理,則新的補丁永遠不會生效
常用的熱更新框架
- 微信的 Tinker
- 支付寶 AndFix
- 美團的 Robust 方案
參考文章
Activity 面試黑洞 - 當按下 Home 鍵再切換回來會發生什么
配置構建
(Android 9.0)Activity啟動流程原始碼分析
為什么 Activity 的 onStop 延遲了 10s 執行
踩坑之路:finish方法執行后居然還有這種操作?
Android行程保活之一個像素保活
Tasks and back stack
Handler 27 問
Handler 的同步屏障
Android IdleHandler 機制
每日一問 聽說過Handler中的IdleHandler嗎?
今日頭條螢屏適配方案
Android 目前穩定高效的UI適配方案
今日頭條螢屏適配方案終極版正式發布!
屬性影片
ViewGroup 為什么不會呼叫 onDraw
小知識又來了!ViewGroup onDraw為什么不呼叫?
直面底層:經常用的ViewTreeObserver 背后的原理
面試高頻題:一眼看穿 SharedPreferences
Android Storage example ExternalStorage.java
再談Android各種Context的前世今生!
ConstraintLayout
MotionLayout samples
MotionLayout Guide
資料系結庫
Android Transition
Android KTX library
面試官:簡歷上最好不要寫Glide,不是問原始碼那么簡單
聊一聊關于Glide在面試中的那些事
直面底層:Window/WindowManager 不可不知之事
直面底層:WindowManager 視圖系結以及體系結構
Android 全域例外處理
使用 Adb shell dumpsys 檢測 Android 的 Activity 任務堆疊
Android 自定義View之Measure程序
Android 自定義View之Layout程序
onMeasure() 為什么會執行多次?
requestLayout 竟然涉及到這么多知識點
Android Resources之assets
Android 混淆、壓縮
Proguard 規則檔案
優先使用 KTX 庫 | MAD Skills
Android常見記憶體泄漏及優化總結
再次回顧 Android View 核心知識與原理
玩轉自定義 View,你必須搞清楚這些:Style,Theme,Attr,Styleable,TypedArray
服務概覽
全面復盤Android開發者容易忽視的Backup功能
Android scrollTo、scrollBy、以及scroller詳解
Android ContentProvider 初始化程序
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/291514.html
標籤:其他
上一篇:Android系統架構
