解析演算法源碼
A. XXL admin 源碼解析
xxl-job 的 admin 服務是 xxl-job 的調度中心,負責管理和調度注冊的 job,關於 xxl-job 的使用,可以閱讀 「參考閱讀」 中的《XXL-JOB分布式調度框架全面詳解》,這里主要是介紹 admin 中的源碼。
admin 服務除了管理頁面上的一些介面外,還有一些核心功能,比如:
1、根據 job 的配置,自動調度 job;
2、接收 executor 實例的請求,實現注冊和下線;
3、監視失敗的 job,進行重試;
4、結束一些異常的 job;
5、清理和統計日誌;
這些功能都是在 admin 服務啟動後,在後台自動運行的,下面將詳細介紹 admin 服務這些功能敏罩的實現。
XxlJobAdminConfig 是 admin 服務的配置類,在 admin 服務啟動時,它除了配置 admin 服務的一些參數外,還會啟動 admin 服務的所有後台線程。
該類的屬性主要分為5類:
1、配置文件中的參數,比如 accessToken;
2、DAO 層各個數據表的 mapper;
3、Spring 容器中的一些 Bean,比如 JobAlarmer、DataSource 等;
4、私有變數 XxlJobScheler 對象;
5、私有靜態變數 adminConfig,指向實例自身。
該類有兩個重要方法,分別實現自介面 InitializingBean、DisposableBean,作用如下:
這兩個方法分別調用了 XxlJobScheler 對象的 init 、 destroy 方法,源碼如下:
XxlJobAdminConfig 作為 admin 服務的配置類,作用就是在 Spring 容器啟動時,調用 XxlJobScheler 的初始化方法,來初始化和啟動 admin 服務的功能。
XxlJobScheler 的作用就是調用各個輔助類(xxxHelper)來啟動和結束不同的線程和功能,初始化方法 init 的代碼如下:
下面我們主要介紹 init 中各個類及其作用,最後再簡單一下介紹 destroy 的作用纖拿森。
當 admin 服務向 executor 實例發出一個調度請求來執行 job 時,會調用 XxlJobTrigger.trigger() 方法把要傳輸的參數(比如 job_id、jobHandler、job_log_id、阻塞策略等,包裝成 TriggerParam 對象)傳給 ExecutorBiz 對象來執行一次調度。
xxl-job 對調度過程做了兩個優化:
JobTriggerPoolHelper 在 toStart 方法中初始化了它的兩個線程池屬性,代碼如下:
每次有調度請求時,就會在這兩個線程池中創建線程,創建線程的邏輯在 addTrigger 方法中。
不同 job 存在執行時長的差異,為了避免不同耗時 job 之間相互阻塞,xxl-job 根據 job 的響應時間,對 job 進行了區分,主要體現在:
如果快 job 與調用頻繁的慢 job 在同一個線程池中創建線程,慢 job 會佔用大量的線程,導致快 job 線程不能及時運行,降低了線程池和線程的利用率。xxl-job 通過快慢隔離,避免了這個問題。
不能,因為慢 job 還是會佔用大量線程,搶佔了快 job 的線程資源;增加線程池中的線程數不但沒有提升利用率,還會導致大量線程看空閑,利用率反而降低了。最好的方法還是用兩個線程池把兩者隔離,可以合理地使用各自線程池的資源。
為了記錄慢 job 的超時次毀畝數,代碼中使用一個 map(變數 jobTimeoutCountMap )來記錄一分鍾內 job 超時次數,key 值是 job_id,value 是超時次數。在調用 XxlJobTrigger.trigger() 方法之前,會先判斷 map 中,該 job_id 的超時次數是否大於 10,如果大於10,就是使用 slowTriggerPool,代碼如下:
調用 XxlJobTrigger.trigger() 方法後,根據兩個值來更新 jobTimeoutCountMap 的值:
和上面的代碼相結合,一個 job 在一分鍾內有10次調用超過 500 毫秒,就認為該 job 是一個 頻繁調度且耗時的 job。
代碼如下:
在該類中,屬性變數 minTim 和 jobTimeoutCountMap 都使用 volatile 來修飾,保證了並發調用 addTrigger 時數據的一致性和可見性。
admin 服務發起 job 調度請求時,是在靜態方法 public static void trigger() 中調用靜態變數 private static JobTriggerPoolHelper helper 的 addTrigger 方法來發起請求的。minTim 和 jobTimeoutCountMap 雖然不是 static 修飾的,但可以看做是全局唯一的(因為持有它們的對象是全局唯一的),因此這兩個參數維護的是 admin 服務全局的調度時間和超時次數,為了避免記錄的數據量過大,需要每分鍾清空一次數據的操作。
admin 服務提供了介面給 executor 來注冊和下線,另外,當 executor 長時間(90秒)沒有發心跳時,要把 executor 自動下線。前一個功能通過暴露一個介面來接收請求,後一個功能需要開啟一個線程,定時更新過期 executor 的狀態。
xxl-job 為了提升 admin 服務的性能,在前一個功能的介面接收到 executor 的請求時,不是同步執行,而是在線程池中開啟一個線程,非同步執行 executor 的注冊和下線請求。
JobRegistryHelper 類就負責管理這個線程池和定時線程的。
線程池的定義和初始化代碼如下:
executor 實例在發起注冊和下線請求時,會調用 AdminBizImpl 類的對應方法,該類的方法如下:
可以看到,AdminBizImpl 類的兩個方法都是調用了 JobRegistryHelper 方法來實現,其中 JobRegistryHelper.registry 方法代碼如下(registryRemove 代碼與之相似):
這兩個方法是通過在線程池 registryOrRemoveThreadPool 中創建線程來非同步執行請求,然後把數據更新或新建到數據表 xxl_job_registry 中。
當 executor 注冊到 admin 服務後(數據入庫到 xxl_job_registry 表),是不會在頁面上顯示的,需要要用戶手動添加 job_group 數據(添加到 xxl_job_group 表),admin 服務會自動把用戶添加的 job_group 數據與 xxl_job_registry 數據關聯。這就需要 admin 定時從 xxl_job_group 表讀取數據,關聯 xxl_job_registry 表和 xxl_job_group 表的數據。
這個功能是與 「executor 自動下線」 功能在同一個線程中實現,該線程的主要邏輯是:
相關代碼如下:
從這里可以看出,如果是對外介面(接收請求等)的功能,使用線程池和非同步線程來實現;如果是一些自動任務,則是通過一個線程來定時執行。
如果一個 Job 調度後,沒有響應返回,需要定時重試。作為一種「自動執行」的任務,很顯然可以像前面 JobRegistryHelper 一樣,使用一個線程定時重試。
在這個類中,定義了一個監視線程,以每10 秒一次的頻率運行,對失敗的 job 進行重試。如果 job 剩餘的重試次數大於0,就會 job 進行重試,並把發送告警信息。線程的定義如下:
在這個線程中,它利用 「資料庫執行 UPDATE 語句時會加上互斥鎖」 的特性,使用了 「基於資料庫的分布式鎖」,代碼如下所示:
在這個語句中,會把 jobLog 的狀態設置為 -1,這是一個無效狀態值,當其他線程通過有效狀態值來搜索失敗記錄時,會略過該記錄,這樣該記錄就不會被其他線程重試,達到的分布式鎖的功能(這個鎖是一個行鎖)。或者說,-1狀態類似於 java 中的對象頭的鎖標志位,表明該記錄已經被加鎖了,其他線程會「忽略」該記錄。
在 try 代碼塊中加鎖和解鎖,如果加鎖後重試時拋出異常,會導致該記錄永遠無法解鎖。所以,應該在 finnally 塊中執行解鎖操作,或者使用 redis 給鎖加一個過期時間來實現分布式鎖。
從失敗的日誌中取出 jobId,查詢出對應的 jobInfo 數據,如果日誌中的剩餘重試次數大於 0,就執行重試。代碼如下:
調度任務使用的就是前面介紹的 JobTriggerPoolHelper.trigger 方法,最後更新 jobLog 的 alarm_status 值,有兩個作用:
這個類與 JobRegistryHelper 類似,都有一個線程池、一個線程,通過前面 JobRegistryHelper 的學習,可以大膽猜測:
實際上,該類中線程池和線程的作用就是用來 「完成」 一個 job。
當 executor 接收到 admin 的調度請求後,會非同步執行 job,並立刻返回一個回調。
admin 接受到回調後,和前面的 「注冊、下線」 一樣,在線程池中創建線程來處理回調,主要是更新 job 和日誌。
當有回調請求時, public callback 方法(該方法被 AdminBizImpl 調用)會在線程池中創建一個線程,遍歷回調請求的參數列表,依次處理回調參數,代碼如下:
從代碼可以看出,最後調用 XxlJobCompleter.updateHandleInfoAndFinish 方法完成回調邏輯。
如果一個 job 較長時間前被調度,但是一直處於 「運行中」 且它所屬的 executor 已經超過 90 秒沒有心跳了,那麼可以認為該 job 已經丟失了,需要把該 job 結束掉。這個就是線程 monitorThread 的主要功能。
monitorThread 會以 60秒 一次的頻率,從 xxl_job_log 表中找出 10分鍾前調度、仍處於」運行中「狀態、executor 已經下線 的 job,然後調用 XxlJobCompleter.updateHandleInfoAndFinish 來更新 handler 的信息和結束 job,代碼如下:
從代碼可以看出,上面的兩個功能最後都調用了 XxlJobCompleter.updateHandleInfoAndFinish 方法,關於該方法的介紹,可以看後面 XxlJobCompleter 部分的介紹,這里不詳細展開。
如果去看 XxlJobTrigger.triger 方法,會發現每次調度 job 時,都會先新增一個 jobLog 記錄,這也是為什麼 JobFailMonitorHelper 中的線程在重試時,先查詢 jobLog 的原因。
JobLog 作為 job 的調度記錄,還可以用來統計一段時間內 job 的調度次數、成功數等;另外,會清理超出有效期(配置的參數 logretentiondays )的日誌,避免日誌數據過大。很顯然,這又是一個 」自動任務「,可以使用一個線程定時完成。
該類持有一個線程變數,線程以 每分鍾一次的頻率,執行兩個操作:
在線程 run 方法的前半部分,線程會統計 3 天內,每天的調度次數、運行次數、成功運行數、失敗次數;然後更新或新增 xxl_job_log_report 表的數據。
在線程 run 方法的後半部分,線程按天對日誌進行清理,如果當前時間與上次清理的時間相隔超過一天,就會清理日誌記錄,代碼如下:
如果不使用參數 lastCleanLogTime 來記錄上次清理的時間,只是清理一天前創建的數據記錄。那麼該線程每分鍾執行一次時,都會刪除前天當前時刻的數據,導致前一年的數不完整。
使用參數 lastCleanLogTime 來記錄上次清理的時間,並且與當前時間相差超過一天時才清理,能保證前一天的日誌是完整的。
不明白為什麼清理日誌時,不是一次性刪除全部的過期日誌,而是每次刪除 1000條。按理說,這些舊的日誌數據應該已經不在 buffer pool 中了,trigger_time 欄位又是普通索引,那麼 DELETE 操作會先更新到 change buffer 中,之後再合並。現在先查詢再刪除,相當於多了一次 IO 且沒有使用到 change buffer。
admin 服務是用來管理和調度 job 的,用戶也可以在它的管理後台新建一個 job,配置 CRON 和 JobHandler,然後 admin 服務就會按照配置的參數來調度 job。很顯然,這種「自動化工作」也是由線程定時執行的。
1、如果使用線程調度 Job,存在的第一個問題是:如果某個 Job 在調度時比較耗時,就可能阻塞後續的 Job,導致後續 job 的執行有延遲,怎麼解決這個問題?
在前面 JobTriggerPoolHelper 我們已經知道,admin 在調度 job 時是 」使用線程池、線程「 非同步執行調度任務,避免了主線程的阻塞。
2、使用線程定時調度 job,存在的第二個問題是:怎麼保證 job 在指定的時間執行,而不會出現大量延遲?
admin 使用 」預讀「 的方式,提前讀取在未來一段時間內要執行的 job,提前取到內存中,並使用 「時間輪演算法」 按時間分組 job,把未來要執行的 job 下一個時間段執行。
3、還隱藏第三個問題:admin 服務是可以多實例部署的,在這種情況下該怎麼避免一個 job 被多個實例重復調度?
admin 把一張數據表作為 「分布式鎖」 來保證只有一個 admin 實例能執行 job 調度,又通過隨機 sleep 線程一段時間,來降低線程之間的競爭。
下面我們就通過代碼來了解 xxl-job 是怎麼解決上述問題的。
在該類中,定義了一個調度線程,用來調度要執行的 job 和已經過期一段時間的 job,定義代碼如下:
該線程會預讀出 「下次執行時間 <= now + 5000 毫秒內」 的部分 job,根據它們下一次執行時間劃分成三段,執行三種不同的邏輯。
1、下次執行時間在 (- , now - 5000) 范圍內
說明過期時間已經大於 5000 毫秒,這時如果過期策略要求調度,就調度一次。代碼如下:
2、下次執行時間在 [now - 5000, now) 范圍內
說明過期時間小於5000毫秒,只能算是延遲不能算是過期,直接調度一次,代碼如下:
如果 job 的下一次執行時間在 5000 毫秒以內,為了省下下次預讀的 IO 耗時,這里會記錄下 job id,等待後面的調度。
3、下次執行時間在 [now, now + 5000) 范圍內
說明還沒到執行時間,先記錄下 job id, 等待後面的調度 ,代碼如下:
上面的3個步驟結束後,會更新 jobInfo 的 trigger_last_time、trigger_next_time、trigger_status 欄位:
可以看到,通過預讀,一方面會把過期一小段時間的 job 執行一遍,另一方面會把未來一小段時間內要執行的 job 取出,保存進一個 map 對象 ringData 中,等待另一個線程調度。這樣就避免了某些 job 到了時間還沒執行。
因為 admin 是可以多實例部署的,所以在調度 job 時,需要考慮怎麼避免 job 被多次調度。
xxl-job 在前面 JobFailMonitorHelper 中遍歷失敗的 job 時,會對每個 job 設置一個無效的狀態作為 」分布式行鎖「,如果設置失敗就跳過。而在這里,如果還使用該方法,有可能出現,一個 job 被設置為無效狀態後,線程就崩潰了,導致該 job 永遠無法被調度。因此,要盡量避免對 job 狀態的修改。
在這里,admin 服務使用一張表 xxl_job_lock 作為分布式鎖,每個 admin 實例都要先嘗試獲取該表的鎖,獲取成功才能繼續執行;同時,為了降低不同實例之間的競爭,會在線程開始執勤隨機 sleep 一段時間。
如何獲取分布式鎖?
在線程中會開啟一個事務,設置為手動提交,然後對表 xxl_job_lock 執行 FOR UPDATE 查詢。如果該線程執行語句成功,其他實例的線程就會排隊等待該表的鎖,實現了分布式鎖功能。代碼如下:
怎麼降低鎖的競爭?
為了降低鎖競爭,在線程開始前會先 sleep 4000 5000 毫秒的隨機值(不能大於 5000 毫秒,5000 毫秒是預讀的時間范圍);在線程結束當前循環時,會根據耗時和是否有預讀數據,選擇不同的 sleep 策略:
代碼如下:
在前面的線程中,對即將要開始的 job,不是立刻調度,而是按照執行的時刻(秒),把 job id 保存進一個 map 中,然後由 ringThread 線程按時刻進行調度,這只典型的「時間輪演算法」。代碼如下:
每次輪詢調度時,只取出當前時刻(秒)、前一秒內的 job,不會去調度與現在相隔太久的 job。
在執行輪詢調度前,有一個時間在 0 1000 毫秒范圍內的 sleep。如果沒有這個 sleep,該線程會一直執行,而 ringData 中當前時刻(秒)的數據可能已經為空,會導致大量無效的操作;增加了這個 sleep 之後,可以避免這種無效的操作。之所以 sleep 時間在 1000 毫秒以內,是因為調度時刻最小精確到秒,一秒的 sleep 可以避免 job 的延遲。
因為在前面的 scheleThread 線程中,最後一個操作是把 job 的 next_trigger_time 值更新為大於 now + 5000 毫秒,其他 admin 實例 scheleThread 線程的查詢條件是:next_trigger_time < now + 5000,不會查詢出這里調度的 job,所以不需要加分布式鎖。
至此,XxlJobScheler-init 方法的作用我們介紹完畢,下面我們簡單介紹一下 XxlJobScheler-destroy 方法
destroy 方法很簡單,就是銷毀前面初始化的線程池和線程,它銷毀的順序與前面啟動的順序相反。
代碼如下:
因為各個 toStop 方法都很相似,所以我們只介紹 JobScheleHelper 的 toStop 方法。
該方法的步驟如下:
1、設置停止標志位為 true;
2、sleep 一段時間,讓出 CPU 時間片給線程執行任務;
3、如果線程不是終止狀態(線程正在 sleep),中斷它;
4、線程執行 join 方法,直到線程結束,執行最後一次。
代碼如下:
至此,JobScheleHelper 的主要功能就介紹完了,可以看出, admin 服務在啟動時,啟動了多個線程池和線程,非同步執行任務和非同步響應 executor 的請求。
下面,我們介紹前面涉及到的 XxlJobTrigger 和 XxlJobCompleter。
XxlJobTrigger 是調度 job 時的封裝類,它主要工作就是接受傳入的 jobId、調度參數等,查詢對應的 jobGroup、jobInfo,然後調用 ExecutorBiz 對象來執行調度(run 方法)。
該類中三個核心方法及其調用關系如下: trigger -> processTrigger -> runExecutor ,
該方法的功能比較簡單,就是根據傳入的參數查詢 jobGroup 和 jobInfo 對象,設置相關的欄位值,然後調用 processTrigger 方法。
該方法的主要工作分為以下幾步:
1、保存一條調度日誌;
2、從 jobInfo、jobGroup 中取出欄位值,構造 TriggerParam 對象;
3、根據 jobInfo 的路由策略,從 jobGroup 中取出要調度的 executor 地址;
4、調用 runExecutor 方法執行調度;
5、保存調度參數、設置調度信息、更新日誌。
這里不會修改 jobInfo、jobGroup 對象的欄位值,只取出欄位值來使用,對這兩個對象欄位的修改,是在前一步 trigger 方法中進行的。
該方法會執行調度,並返回調度結果,它的核心代碼如下:
這里使用 XxlJobScheler 類取出 ExecutorBiz 對象,以 「懶載入」 的方式給每個 address 創建一個 ExecutorBiz 對象,代碼如下:
可以看出,該類中的三個方法其實可以歸類為:pre -> execute -> post,在執行前、執行時、執行後做一些前置和收尾工作。
該類在前面 JobCompleteHelper 中被使用,最終 job 的完成就是在該類中執行的,該類有兩個主要方法:
下面主要介紹 finishJob 方法。
finishJob 的主要功能是:如果當前任務執行成功了,就調度它的所有子任務,最後把子任務的調度消息添加到當前 job 的日誌中。代碼如下:
需要注意的是:
1、這里依賴於 JobTriggerPoolHelper 來調度 job,所以在 JobCompleteHelper 的監視線程開始時,有一個 50 秒的等待,就是等待 JobTriggerPoolHelper 啟動完成;
2、在 finishJob 方法中,調度子任務的時候,默認子任務的調度結果是成功,注意,這里是指 「調度」 這個行為是成功的,而不是指子任務執行是成功的。
1、XxlJobAdminConfig 作為 admin 服務的啟動入口,要盡可能保持簡潔,作用類似於一個倉庫,來管理和持有所有的類和對象,並不會去啟動具體的線程,它只需要「按下啟動器的按鈕」就可以了;
2、XxlJobScheler 是 admin 服務的啟動器類,它會調用各個輔助類(xxxHelper)來啟動對應的線程;
3、對外的介面,比如調度 job、接收注冊或下線等,都是使用線程池 + 線程 的非同步方式實現,避免 job 對主線程的阻塞;
4、對「自動任務「類的功能,都是使用線程定時執行;
XXL-JOB分布式調度框架全面詳解: https://juejin.cn/post/6948397386926391333
時間輪演算法:https://spongecaptain.cool/post/widget/timingwheel
一個開源的時間輪演算法介紹:https://spongecaptain.cool/post/widget/timingwheel2
B. Android TV開發焦點移動源碼分析
點可以理解為選中態,在Android TV上起很重要的作用。一個視圖控制項只有在獲得焦點的狀態下,才能響應按鍵的Click事件。
相對於手機上用手指點擊屏幕產生的Click事件, 在TV中通過點擊遙控器的方向鍵來控制焦點的移動。當焦點移動到目標控制項上之後,按下遙控器的確定鍵,才會觸發一個Click事件,進而去做下一步的處理
在處理焦點的時候,有一些基礎的用法需要知道。
首先,一個控制項isFocusable()需要為true才有資格可以獲取到焦點。如果想要在觸摸模式下獲取焦點,需要通過setFocusableInTouchMode(boolean)來設置。也可以直接在xml布局文件中指定:
keyEvent 分發過程:
而當按下遙控器的按鍵時,會產生一個按鍵事件,就是KeyEvent,包含「上」,「下」,「左」,「右」,「返回」,「確定」等指令。焦點的處理就在KeyEvent的分發當中完成。
首先,KeyEvent會流轉到ViewRootImpl中開始進行處理,具體方法是內部類 ViewPostImeInputStage 中的 processKeyEvent :
接下來先看一下KeyEvent在view框架中的分發:
這里也是可以做焦點控制,最好是在 event.getAction() == KeyEvent.ACTION_DOWN 進行.
因為android 的 ViewRootlmpl 的 processKeyEvent 焦點搜索與請求的地方 進行了判斷if (event.getAction() == KeyEvent.ACTION_DOWN)
• 首先ViewGroup會一層一層往上執行父類的dispatchKeyEvent方法,如果返回true那麼父類的dispatchKeyEvent方法就會返回true,也就代表父類消費了該焦點事件,那麼焦點事件自然就不會往下進行分發。
• 然後ViewGroup會悔李鍵判斷mFocused這個view是否為空,如果為空就會return false,焦點繼續往下傳遞;如果不為空,那就會return mFocused的dispatchKeyEvent方法返回的結果。這個mFocused其實是擾殲ViewGroup中當前獲取焦點的子View
發現執行了onKeyListener中的onKey方法,如果onKey方法返回true,那麼dispatchKeyEvent方法也會返回true
如果想要修改ViewGroup焦點事件的分發
• 重寫view的dispatchKeyEvent方法
• 給某個子view設置onKeyListener監聽
下面再來看一下如果一個頁面第一次進入,系統是如何確定焦點是定位在哪個view上的
由於DecorView繼承自FrameLayout,這里調用的是ViewGroup的requestFocus
descendantFocusability:
• FOCUS_AFTER_DESCENDANTS:先分發給Child View進行處理,如果所有的Child View都沒有處理,則自己再處理
• FOCUS_BEFORE_DESCENDANTS:ViewGroup先對焦碧巧點進行處理,如果沒有處理則分發給child View進行處理
• FOCUS_BLOCK_DESCENDANTS:ViewGroup本身進行處理,不管是否處理成功,都不會分發給ChildView進行處理
因為 PhoneWindow 給 DecoreView 初始化時設置 了 setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS),所以這里默認是FOCUS_AFTER_DESCENDANTS
到此第一次請求焦點的過程基本告一個段落
焦點移動的時候,默認的情況下,會按照一種演算法去找在指定移動方向上最近的鄰居。在一些情況下,焦點的移動可能跟開發者的意圖不符,這時開發者可以在布局文件中使用下面這些XML屬性來指定下一個焦點對象:
在KeyEvent分發中已經知道如果分發過程中event沒有被消耗,就會根據方向搜索以及請求焦點View
流程一:查找用戶指定的下一個焦點
流程二:獲取搜索方向上所有可以獲取焦點的view,使用演算法查找下一個view
addFocusables() 獲取搜索方向上可獲得焦點的view
descendantFocusability屬性決定了ViewGroup和其子view的聚焦優先順序
• FOCUS_BLOCK_DESCENDANTS:viewgroup會覆蓋子類控制項而直接獲得焦點
• FOCUS_BEFORE_DESCENDANTS:viewgroup會覆蓋子類控制項而直接獲得焦點
• FOCUS_AFTER_DESCENDANTS:viewgroup只有當其子類控制項不需要獲取焦點時才獲取焦點
addFocusables 的第一個參數views是由root決定的。在ViewGroup的focusSearch方法中傳進來的root是DecorView,也可以主動調用FocusFinder的findNextFocus方法,在指定的ViewGroup中查找焦點。
FocusFinder.findNextFocus 查找焦點
C. 如何用java計算一個圓的面積和周長
一、數學公式:
圓周長=2*π*半徑
面積=π*半徑²
二、演算法分析:
周長和面積都依賴半徑,所以要先輸入半徑值,然後套用公式,計算周長和面積。 最終輸出結果即可。
三、參考代碼:
代碼如下
#include"stdio.h"
#definePi3.14
voidmain()
{
floatr,c,area;
printf("請輸入圓的半徑:");
scanf("%f",&r);
c=2*Pi*r;
area=Pi*r*r;
printf("該圓的周長是%.2f,面積是%.2f ",c,area);
}
D. Android TV 焦點原理源碼解析
相信很多剛接觸AndroidTV開發的開發者,都會被各種焦點問題給折磨的不行。不管是學技術還是學習其他知識,都要學習和理解其中原理,碰到問題我們才能得心應手。下面就來探一探Android的焦點分發的過程。
Android焦點事件的分發是從ViewRootImpl的processKeyEvent開始的,源碼如下:
源碼比較長,下面我就慢慢來講解一下具體的每一個細節。
dispatchKeyEvent方法返回true代表焦點事件被消費了。
ViewGroup的dispatchKeyEvent()方法的源碼如下:
(2)ViewGroup的dispatchKeyEvent執行流程
(3)下面再來瞧瞧view的dispatchKeyEvent方法的具體的執行過程
驚奇的發現執行了onKeyListener中的onKey方法,如果onKey方法返回true,那麼dispatchKeyEvent方法也會返回true
可以得出結論:如果想要修改ViewGroup焦點事件的分發,可以這么干:
注意:實際開發中,理論上所有焦點問題都可以通過給dispatchKeyEvent方法增加監聽來來攔截來控制。
(1)dispatchKeyEvent方法返回false後,先得到按鍵的方向direction值,這個值是一個int類型參數。這個direction值是後面來進行焦點查找的。
(2)接著會調用DecorView的findFocus()方法一層一層往下查找已經獲取焦點的子View。
ViewGroup的findFocus方法如下:
View的findFocus方法
說明:判斷view是否獲取焦點的isFocused()方法, (mPrivateFlags & PFLAG_FOCUSED) != 0 和view 的isFocused()方法是一致的。
其中isFocused()方法的作用是判斷view是否已經獲取焦點,如果viewGroup已經獲取到了焦點,那麼返回本身即可,否則通過mFocused的findFocus()方法來找焦點。mFocused其實就是ViewGroup中獲取焦點的子view,如果mView不是ViewGourp的話,findFocus其實就是判斷本身是否已經獲取焦點,如果已經獲取焦點了,返回本身。
(3)回到processKeyEvent方法中,如果findFocus方法返回的mFocused不為空,說明找到了當前獲取焦點的view(mFocused),接著focusSearch會把direction(遙控器按鍵按下的方向)作為參數,找到特定方向下一個將要獲取焦點的view,最後如果該view不為空,那麼就讓該view獲取焦點。
(4)focusSearch方法的具體實現。
focusSearch方法的源碼如下:
可以看出focusSearch其實是一層一層地網上調用父View的focusSearch方法,直到當前view是根布局(isRootNamespace()方法),通過注釋可以知道focusSearch最終會調用DecorView的focusSearch方法。而DecorView的focusSearch方法找到的焦點view是通過FocusFinder來找到的。
(5)FocusFinder是什麼?
它其實是一個實現 根據給定的按鍵方向,通過當前的獲取焦點的View,查找下一個獲取焦點的view這樣演算法的類。焦點沒有被攔截的情況下,Android框架焦點的查找最終都是通過FocusFinder類來實現的。
(6)FocusFinder是如何通過findNextFocus方法尋找焦點的。
下面就來看看FocusFinder類是如何通過findNextFocus來找焦點的。一層一層往下看,後面會執行findNextUserSpecifiedFocus()方法,這個方法會執行focused(即當前獲取焦點的View)的findUserSetNextFocus方法,如果該方法返回的View不為空,且isFocusable = true && isInTouchMode() = true的話,FocusFinder找到的焦點就是findNextUserSpecifiedFocus()返回的View。
(7)findNextFocus會優先根據XML里設置的下一個將獲取焦點的View ID值來尋找將要獲取焦點的View。
看看View的findUserSetNextFocus方法內部都幹了些什麼,OMG不就是通過我們xml布局裡設置的nextFocusLeft,nextFocusRight的viewId來找焦點嗎,如果按下Left鍵,那麼便會通過nextFocusLeft值里的View Id值去找下一個獲取焦點的View。
可以得出以下結論:
1. 如果一個View在XML布局中設置了focusable = true && isInTouchMode = true,那麼這個View會優先獲取焦點。
2. 通過設置nextFocusLeft,nextFocusRight,nextFocusUp,nextFocusDown值可以控制View的下一個焦點。
Android焦點的原理實現就這些。總結一下:
為了方便同志們學習,我這做了張導圖,方便大家理解~
E. c語言的兩種排序
1、選擇排序法
要求輸入10個整數,從大到小排序輸出
輸入:2 0 3 -4 8 9 5 1 7 6
輸出:9 8 7 6 5 3 2 1 0 -4
代碼:
#include<stdio.h>
int main(int argc,const char*argv[]){
int num[10],i,j,k,l,temp;
//用一個數組保存輸入的數據
for(i=0;i<=9;i++)
{
scanf("%d",&num<i>);
}
//用兩個for嵌套循環來進行數據大小比較進行排序
for(j=0;j<9;j++)
{
for(k=j+1;k<=9;k++)
{
if(num[j]<num[k])//num[j]<num[k]
{
temp=num[j];
num[j]=num[k];
num[k]=temp;
}
}
}
//用一個for循環來輸出數組中排序好的數據
for(l=0;l<=9;l++)
{
printf("%d",num[l]);
}
return 0;
}
2、冒泡排序法
要求輸入10個整數,從大到小排序輸出
輸入:2 0 3-4 8 9 5 1 7 6
輸出:9 8 7 6 5 3 2 1 0-4
代碼:
#include<stdio.h>
int main(int argc,const char*argv[]){
//用一個數組來存數據
int num[10],i,j,k,l,temp;
//用for來把數據一個一個讀取進來
for(i=0;i<=9;i++)
{
scanf("%d",&num<i>);
}
//用兩次層for循環來比較數據,進行冒泡
for(j=0;j<9;j++)
{
for(k=0;k<9-j;k++)
{
if(num[k]<num[k+1])//num[k]<num[k+1]
{
temp=num[k];
num[k]=num[k+1];
num[k+1]=temp;
}
}
}
//用一個for循環來輸出數組中排序好的數據
for(l=0;l<=9;l++)
{
printf("%d",num[l]);
}
return 0;
}
(5)解析演算法源碼擴展閱讀:
return 0代表程序正常退出。return是C++預定義的語句,它提供了終止函數執行的一種方式。當return語句提供了一個值時,這個值就成為函數的返回值。
return語句用來結束循環,或返回一個函數的值。
1、return 0,說明程序正常退出,返回到主程序繼續往下執行。
2、return 1,說明程序異常退出,返回主調函數來處理,繼續往下執行。return 0或return 1對程序執行的順序沒有影響,只是大家習慣於使用return(0)退出子程序而已。
F. Android socket源碼解析(三)socket的connect源碼解析
上一篇文章著重的聊了socket服務端的bind,listen,accpet的邏輯。本文來著重聊聊connect都做了什麼?
如果遇到什麼問題,可以來本文 https://www.jianshu.com/p/da6089fdcfe1 下討論
當服務端一切都准備好了。客戶端就會嘗試的通過 connect 系統調用,嘗試的和服務端建立遠程連接。
首先校驗當前socket中是否有正確的目標地址。然後獲取IP地址和埠調用 connectToAddress 。
在這個方法中,能看到有一個 NetHooks 跟蹤socket的調用,也能看到 BlockGuard 跟蹤了socket的connect調用。因此可以hook這兩個地方跟蹤socket,不過很少用就是了。
核心方法是 socketConnect 方法,這個方法就是調用 IoBridge.connect 方法。同理也會調用到jni中。
能看到也是調用了 connect 系統調用。
文件:/ net / ipv4 / af_inet.c
在這個方法中做的事情如下:
注意 sk_prot 所指向的方法是, tcp_prot 中 connect 所指向的方法,也就是指 tcp_v4_connect .
文件:/ net / ipv4 / tcp_ipv4.c
本質上核心任務有三件:
想要能夠理解下文內容,先要明白什麼是路由表。
路由表分為兩大類:
每個路由器都有一個路由表(RIB)和轉發表 (fib表),路由表用於決策路由,轉發表決策轉發分組。下文會接觸到這兩種表。
這兩個表有什麼區別呢?
網上雖然給了如下的定義:
但實際上在Linux 3.8.1中並沒有明確的區分。整個路由相關的邏輯都是使用了fib轉發表承擔的。
先來看看幾個和FIB轉發表相關的核心結構體:
熟悉Linux命令朋友一定就能認出這裡面大部分的欄位都可以通過route命令查找到。
命令執行結果如下:
在這route命令結果的欄位實際上都對應上了結構體中的欄位含義:
知道路由表的的內容後。再來FIB轉發表的內容。實際上從下面的源碼其實可以得知,路由表的獲取,實際上是先從fib轉發表的路由字典樹獲取到後在同感加工獲得路由表對象。
轉發表的內容就更加簡單
還記得在之前總結的ip地址的結構嗎?
需要進行一次tcp的通信,意味著需要把ip報文准備好。因此需要決定源ip地址和目標IP地址。目標ip地址在之前通過netd查詢到了,此時需要得到本地發送的源ip地址。
然而在實際情況下,往往是面對如下這么情況:公網一個對外的ip地址,而內網會被映射成多個不同內網的ip地址。而這個過程就是通過DDNS動態的在內存中進行更新。
因此 ip_route_connect 實際上就是選擇一個緩存好的,通過DDNS設置好的內網ip地址並找到作為結果返回,將會在之後發送包的時候填入這些存在結果信息。而查詢內網ip地址的過程,可以成為RTNetLink。
在Linux中有一個常用的命令 ifconfig 也可以實現類似增加一個內網ip地址的功能:
比如說為網卡eth0增加一個IPV6的地址。而這個過程實際上就是調用了devinet內核模塊設定好的添加新ip地址方式,並在回調中把該ip地址刷新到內存中。
注意 devinet 和 RTNetLink 嚴格來說不是一個存在同一個模塊。雖然都是使用 rtnl_register 注冊方法到rtnl模塊中:
文件:/ net / ipv4 / devinet.c
文件:/ net / ipv4 / route.c
實際上整個route模塊,是跟著ipv4 內核模塊一起初始化好的。能看到其中就根據不同的rtnl操作符號注冊了對應不同的方法。
整個DDNS的工作流程大體如下:
當然,在tcp三次握手執行之前,需要得到當前的源地址,那麼就需要通過rtnl進行查詢內存中分配的ip。
文件:/ include / net / route.h
這個方法核心就是 __ip_route_output_key .當目的地址或者源地址有其一為空,則會調用 __ip_route_output_key 填充ip地址。目的地址為空說明可能是在回環鏈路中通信,如果源地址為空,那個說明可能往目的地址通信需要填充本地被DDNS分配好的內網地址。
在這個方法中核心還是調用了 flowi4_init_output 進行flowi4結構體的初始化。
文件:/ include / net / flow.h
能看到這個過程把數據中的源地址,目的地址,源地址埠和目的地址埠,協議類型等數據給記錄下來,之後內網ip地址的查詢與更新就會頻繁的和這個結構體進行交互。
能看到實際上 flowi4 是一個用於承載數據的臨時結構體,包含了本次路由操作需要的數據。
執行的事務如下:
想要弄清楚ip路由表的核心邏輯,必須明白路由表的幾個核心的數據結構。當然網上搜索到的和本文很可能大為不同。本文是基於LInux 內核3.1.8.之後的設計幾乎都沿用這一套。
而內核將路由表進行大規模的重新設計,很大一部分的原因是網路環境日益龐大且復雜。需要全新的方式進行優化管理系統中的路由表。
下面是fib_table 路由表所涉及的數據結構:
依次從最外層的結構體介紹:
能看到路由表的存儲實際上通過字典樹的數據結構壓縮實現的。但是和常見的字典樹有點區別,這種特殊的字典樹稱為LC-trie 快速路由查找演算法。
這一篇文章對於快速路由查找演算法的理解寫的很不錯: https://blog.csdn.net/dog250/article/details/6596046
首先理解字典樹:字典樹簡單的來說,就是把一串數據化為二進制格式,根據左0,右1的方式構成的。
如圖下所示:
這個過程用圖來展示,就是沿著字典樹路徑不斷向下讀,比如依次讀取abd節點就能得到00這個數字。依次讀取abeh就能得到010這個數字。
說到底這種方式只是存儲數據的一種方式。而使用數的好處就能很輕易的找到公共前綴,在字典樹中找到公共最大子樹,也就找到了公共前綴。
而LC-trie 則是在這之上做了壓縮優化處理,想要理解這個演算法,必須要明白在 tnode 中存在兩個十分核心的數據:
這負責什麼事情呢?下面就簡單說說整個lc-trie的演算法就能明白了。
當然先來看看方法 __ip_dev_find 是如何查找
文件:/ net / ipv4 / fib_trie.c
整個方法就是通過 tkey_extract_bits 生成tnode中對應的葉子節點所在index,從而通過 tnode_get_child_rcu 拿到tnode節點中index所對應的數組中獲取葉下一級別的tnode或者葉子結點。
其中查找index最為核心方法如上,這個過程,先通過key左移動pos個位,再向右邊移動(32 - bits)演算法找到對應index。
在這里能對路由壓縮演算法有一定的理解即可,本文重點不在這里。當從路由樹中找到了結果就返回 fib_result 結構體。
查詢的結果最為核心的就是 fib_table 路由表,存儲了真正的路由轉發信息
文件:/ net / ipv4 / route.c
這個方法做的事情很簡單,本質上就是想要找到這個路由的下一跳是哪裡?
在這裡面有一個核心的結構體名為 fib_nh_exception 。這個是指fib表中去往目的地址情況下最理想的下一跳的地址。
而這個結構體在上一個方法通過 find_exception 獲得.遍歷從 fib_result 獲取到 fib_nh 結構體中的 nh_exceptions 鏈表。從這鏈表中找到一模一樣的目的地址並返回得到的。
文件:/ net / ipv4 / tcp_output.c
G. 阿里sentinel源碼解析
sentinel是阿里巴巴開源的流量整形(限流、熔斷)框架,目前在github擁有15k+的star,sentinel以流量為切入點,從流量控制、熔斷降級、系統負載保護等多個維度保護服務的穩定性。
我們以sentinel的主流程入手,分析sentinel是怎麼搜集流量指標,完成流量整形的。
首先我們先看一個sentinel的簡單使用demo,只需要調用SphU.entry獲取到entry,然後在完成業務方法之後調用entry.exit即可。
SphU.entry會調用Env.sph.entry,將name和流量流向封裝成StringResourceWrapper,然後繼續調用entry處理。
進入CtSph的entry方法,最終來到entryWithPriority,調用InternalContextUtil.internalEnter初始化ThreadLocal的物鋒Context,然後調用lookProcessChain初始化責任鏈,最終調用chain.entry進入責任鏈進行處理。
InternalContextUtil.internalEnter會調用trueEnter方法,主要是生成DefaultNode到contextNameNodeMap,然後生成Context設置到contextHolder的過程。
lookProcessChain已經做過優化,支持spi載入自定義的責任鏈bulider,如果沒有定義則使用默認的DefaultSlotChainBuilder進行載入。默認載入的slot和順序可見鎮樓圖,不再細說。
最後來到重頭戲chain.entry進入責任鏈進行處理,下面會按照順序分別對每個處理器進行分析。
首先來到NodeSelectorSlot,主要是獲取到name對應的DefaultNode並緩存起來,設置為context的當前節點,然後通知下一個節點。
下一個節點是ClusterBuilderSlot,繼續對DefaultNode設置ClusterNode與OriginNode,然頌咐後通知下一節點。
下一個節點是LogSlot,只是單純的列印日誌,不再細說。
下一個節點是StatisticSlot,是一個後置節點,先通知下一個節點處理完後,
1.如果沒有報錯,則對node、clusterNode、originNode、ENTRY_NODE的線程數、通過請求數進行增加。
2.如果報錯是PriorityWaitException,則只對線程數進行增加。
3.如果報錯是BlockException,設置報錯到node,然後對阻擋請求數進行增加。
4.如果是其他報錯,設置報錯到node即可。
下一個節點是FlowSlot,這個節點就是重要的限流處理節點,進入此節點是調用checker.checkFlow進行限流處理。
來到FlowRuleChecker的checkFlow方法,調用ruleProvider.apply獲取到資源對應的FlowRule列表,然後遍歷FlowRule調用canPassCheck校驗限流規則。
canPassCheck會根據rule的限流模式,選擇集群限流或者本地限流,這里分別作出分析。
passLocalCheck是本地限流的入口,首先會調用選出限流的node,然後調用canPass進行校驗。罩櫻晌
會根據以下規則選中node。
1.strategy是STRATEGY_DIRECT。
1.1.limitApp不是other和default,並且等於orgin時,選擇originNode。
1.2.limitApp是other,選擇originNode。
1.3.limitApp是default,選擇clusterNode。
2.strategy是STRATEGY_RELATE,選擇clusterNode。
3.strategy是STRATEGY_CHAIN,選擇node。
選擇好對應的node後就是調用canPass校驗限流規則,目前sentinel有三種本地限流規則:普通限流、勻速限流、冷啟動限流。
普通限流的實現是DefaultController,就是統計當前的線程數或者qps加上需要通過的數量有沒有大於限定值,小於等於則直接通過,否則阻擋。
勻速限流的實現是RateLimiterController,使用了AtomicLong保證了latestPassedTime的原子增長,因此停頓的時間是根據latestPassedTime-currentTime計算出來,得到一個勻速的睡眠時間。
冷啟動限流的實現是WarmUpController,是sentinel中最難懂的限流方式,其實不太需要關注這些復雜公式的計算,也可以得出冷啟動的限流思路:
1.當qps已經達到溫熱狀態時,按照正常的添加令牌消耗令牌即可。
2.當qps處於過冷狀態時,會添加令牌使得演算法繼續降溫。
3.當qps逐漸回升,大於過冷的邊界qps值時,不再添加令牌,慢慢消耗令牌使得逐漸增大單位時間可通過的請求數,讓演算法繼續回溫。
總結出一點,可通過的請求數跟令牌桶剩餘令牌數量成反比,以達到冷啟動的作用。
接下來是集群限流,passClusterCheck是集群限流的入口,會根據flowId調用clusterSerivce獲取指定數量的token,然後根據其結果判斷是否通過、睡眠、降級到本地限流、阻擋。
接下來看一下ClusterService的處理,會根據ruleId獲取到對應的FlowRule,然後調用ClusterFlowChecker.acquireClusterToken獲取結果返回。ClusterFlowChecker.acquireClusterToken的處理方式跟普通限流是一樣的,只是會將集群的請求都集中在一個service中處理,來達到集群限流的效果,不再細說。
FlowSlot的下一個節點是DegradeSlot,是熔斷處理器,進入時會調用performChecking,進而獲取到CircuitBreaker列表,然後調用其tryPass校驗是否熔斷。
來到AbstractCircuitBreaker的tryPass方法,主要是判斷熔斷器狀態,如果是close直接放行,如果是open則會校驗是否到達開啟halfopen的時間,如果成功將狀態cas成halfopen則繼續放行,其他情況都是阻攔。
那怎麼將熔斷器的狀態從close變成open呢?怎麼將halfopen變成close或者open呢?sentinel由兩種熔斷器:錯誤數熔斷器ExceptionCircuitBreaker、響應時間熔斷器ResponseTimeCircuitBreaker,都分析一遍。
當業務方法報錯時會調用Tracer.traceEntry將報錯設置到entry上。
當調用entry.exit時,會隨著責任鏈來到DegradeSlot的exit方法,會遍歷熔斷器列表調用其onRequestComplete方法。
ExceptionCircuitBreaker的onRequestComplete會記錄錯誤數和總請求數,然後調用繼續處理。
1.當前狀態是open時,不應該由熔斷器底層去轉換狀態,直接退出。
2.當前狀態是halfopen時,如果沒有報錯,則將halfopen變成close,否則將halfopen變成open。
3.當前狀態時close時,則根據是否總請求達到了最低請求數,如果達到了話再比較錯誤數/錯誤比例是否大於限定值,如果大於則直接轉換成open。
ExceptionCircuitBreaker的onRequestComplete會記錄慢響應數和總請求數,然後調用繼續處理。
1.當前狀態是open時,不應該由熔斷器底層去轉換狀態,直接退出。
2.當前狀態是halfopen時,如果當前響應時間小於限定值,則將halfopen變成close,否則將halfopen變成open。
3.當前狀態時close時,則根據是否總請求達到了最低請求數,如果達到了話再比較慢請求數/慢請求比例是否大於限定值,如果大於則直接轉換成open。
下一個節點是AuthoritySlot,許可權控制器,這個控制器就是看當前origin是否被允許進入請求,不允許則報錯,不再細說。
終於來到最後一個節點SystemSlot了,此節點是自適應處理器,主要是根據系統自身負載(qps、最大線程數、最高響應時間、cpu使用率、系統bbr)來判斷請求是否能夠通過,保證系統處於一個能穩定處理請求的安全狀態。
尤其值得一提的是bbr演算法,作者參考了tcp bbr的設計,通過最大的qps和最小的響應時間動態計算出可進入的線程數,而不是一個粗暴的固定可進入的線程數,為什麼能通過這兩個值就能計算出可進入的線程數?可以網上搜索一下tcp bbr演算法的解析,十分巧妙,不再細說。
H. 面試中的網紅Vue源碼解析之虛擬DOM,你知多少呢深入解讀diff演算法
眾所周知,在前端的面試中,面試官非常愛考dom和diff演算法。比如,可能會出現在以下場景
滴滴滴,面試官發來一個面試邀請。接受邀請📞
我們都知道, key 的作用在前端的面試是一道很普遍的題目,但是呢,很多時候我們都只浮於知識的表面,而沒有去深挖其原理所在,這個時候我們的競爭力就在這被拉下了。所以呢,深入學習原理對於提升自身的核心競爭力是一個必不可少的過程。
在接下來的這篇文章中,我們將講解面試中很愛考的虛擬DOM以及其背後的diff演算法。 請認真閱讀本文~文末有學習資源免費共享!!!
虛擬DOM是用JavaScript對象描述DOM的層次結構。DOM中的一切屬性都在虛擬DOM中有對應的屬性。本質上是JS 和 DOM 之間的一個映射緩存。
要點:虛擬 DOM 是 JS 對象;虛擬 DOM 是對真實 DOM 的描述。
diff發生在虛擬DOM上。diff演算法是在新虛擬DOM和老虛擬DOM進行diff(精細化比對),實現最小量更新,最後反映到真正的DOM上。
我們前面知道diff演算法發生在虛擬DOM上,而虛擬DOM是如何實現的呢?實際上虛擬DOM是有一個個虛擬節點組成。
h函數用來產生虛擬節點(vnode)。虛擬節點有如下的屬性:
1)sel: 標簽類型,例如 p、div;
2)data: 標簽上的數據,例如 style、class、data-*;
3)children :子節點;
4) text: 文本內容;
5)elm:虛擬節點綁定的真實 DOM 節點;
通過h函數的嵌套,從而得到虛擬DOM樹。
我們編寫了一個低配版的h函數,必須傳入3個參數,重載較弱。
形態1:h('div', {}, '文字')
形態2:h('div', {}, [])
形態3:h('div', {}, h())
首先定義vnode節點,實際上就是把傳入的參數合成對象返回。
[圖片上傳失敗...(image-7a9966-1624019394657)]
然後編寫h函數,根據第三個參數的不同進行不同的響應。
當我們進行比較的過程中,我們採用的4種命中查找策略:
1)新前與舊前:命中則指針同時往後移動。
2)新後與舊後:命中則指針同時往前移動。
3)新後與舊前:命中則涉及節點移動,那麼新後指向的節點,移到 舊後之後 。
4)新前與舊後:命中則涉及節點移動,那麼新前指向的節點,移到 舊前之前 。
命中上述4種一種就不在命中判斷了,如果沒有命中,就需要循環來尋找,移動到舊前之前。直到while(新前<=新後&&舊前<=就後)不成立則完成。
如果是新節點先循環完畢,如果老節點中還有剩餘節點(舊前和舊後指針中間的節點),說明他們是要被刪除的節點。
如果是舊節點先循環完畢,說明新節點中有要插入的節點。
1.什麼是Virtual DOM 和Snabbdom
2.手寫底層源碼h函數
3.感受Vue核心演算法之diff演算法
4.snabbdom之核心h函數的工作原理
1、零基礎入門或者有一定基礎的同學、大中院校學生
2、在職從事相關工作1-2年以及打算轉行前端的朋友
3、對前端開發有興趣人群