整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          JS拖拽專題(五)-「玩出花兒來」移動端滑動事件的封

          JS拖拽專題(五)-「玩出花兒來」移動端滑動事件的封裝

          迎來到我的JS拖拽專題系列文章,本章節將是拖拽系列的最后一篇。感謝大家的支持^_^

          上一章節我們說到了拖拽讓圖片相互之間交換位置,相對來說是一個比較綜合的示例,涉及到了矩形的碰撞檢測,勾股定理計算兩點間的距離以及最小距離的獲取。

          本章目標:

          1. jquery插件的擴展原理
          2. 滑動事件swipe的介紹和封裝

          在移動端,我們經常會通過手指左滑,右滑,上滑,下滑去觸發一些操作,這種手指滑動操作我們稱之為swipe相關的事件。

          先來看下今天要實現的效果吧!

          一個簡單的滑動事件的示例

          然后不巧的是,這些事件并不是原生就提供給我們使用的。而我們能夠使用之,是因為有人造好了輪子。

          那么接下來我們也一起去造一下這個輪子吧,看看它和我們的拖拽有著怎樣千絲萬縷的聯系吧~

          本次swipe相關事件是基于 偉大的jquery來實現的。所以我們先來了解一下jquery的插件擴展原理吧

          jquery插件的擴展原理

          熟悉jqeuery的特性的都知道,它是基于面向對象,插件的擴展內部原理其實就是在類為原型上添加自定義的方法。

          $.fn.pluginName=function(){ 
           ... do something
          }
          

          what?不是插件是在原型上擴展的嗎???

          OK,為了驗證我的做法,老規矩,找源碼去。

          作者在類下直接掛載了一個fn屬性,這個屬性和jQuery的原型對象相等,我們知道在jquery中,$===jQuery的。所以,我們可以$.fn.pluginName=function(){}進行擴展

          滑動事件swipe的介紹和封裝

          分析一下,滑動的動作,手指按下,手指移動,手指抬起,實質是三個事件的合體,剛好和我們的拖拽三大事件不謀而合。

          問題1:如何定義滑動的方向?

          假如圓心為我們的手指的起始點,那么手指抬起的時候位置落在的區域如圖所示,我們就能輕松判斷出用戶的手指的滑動方向。

          問題2:觸發最終滑動事件的條件是什么?比如向上滑動的判斷條件是什么?

          1. 手指的終點在的pageY要大于手指按下時的pageY
          2. 手指的y方向的運動距離要大于X軸的運動距離。
          3. 從手指按下到抬起的時間差在某一個范圍內。

          接下來我們用代碼實現一下

          我們定義擴展的插件名為swipe,于是就有了:

          $.fn.swipe=function(type,fn){}
          

          其中type為我們要滑動的方向,比如:up, down ,left,top

          依據上面的分析:在手指按下的時候,記錄手指的起始X和Y的坐標和起始時間

          在手指抬起的時候,再次獲取當前的X,Y和時間

          再分別計算出差值。

          條件判斷

          執行我們的fn函數

          完整的代碼請私信我

          這是我們封裝向上滑動的插件內部實現。同理,其它的方向的滑動只需要做細小的變動即可。這里不再贅述。

          說明:插件的擴展還有一些默認的參數的配置,這些在本章節中不是主要,就不再細分下去了。有興趣的大家可以閱讀其它的swipe插件的封裝。比我這邊要復雜一些,但是原理和我分享的差不了。建議各位項目中還是用成熟的插件哦。

          我這里只是想告訴大家這個輪子大概是怎么造出來的

          插件的調用

          var ul=$('#container ul');
          
          var iNow=0;
          
          ul.swipe('up',()=>{
          	console.log('up');
          	iNow++;
          	iNow=Math.min(5,iNow)
          	ul.css({transform:'translateY('+-iNow * 100 +'vh)'});
          }).swipe('down',e=>{
          	iNow--;
          	iNow=Math.max(0,iNow)
          	ul.css({transform:'translateY('+-iNow * 100 +'vh)'});
          })
          

          這里的swipe就是我們自己擴展的插件的方法。up是我們定義滑動的方向fn是滑動后的回調方法。

          為什么我們定義的插件后,可以直接用ul.swipe方法呢?這個我們在文章的最前端已經說到,jquery 是基于面向對象的,var ul=$('#container ul');就相當于new jQuery(),也就是說ul是jQuery類的一個實例,而我們的插件是基于類的原型擴展出來的方法。所以我們可以直接通過ul.swipe來調用。


          這里是【暢哥聊技術】JS拖拽專題系列技術文章的最后一個章節,在 web中,有關于拖拽的變形應用還有很多,但是萬變不離其蹤,掌握原理,其它都是浮云。我的系列文章只是記錄了我在工作中經常用到的案例。希望從中能幫助到大家。

          最后感謝各位的支持,下一個系列專題分享還在準備中...

          敬請期待!

          (全文完)

          者:edworldwang,騰訊PCG客戶端開發工程師

          本文分享的是筆者遇到的一個Android端滑動事件異常,從業務層排查到深入源碼,從Input系統的framework native到base逐層進行分析。在翻閱git history逐個對比差異的過程中,定位到Android 11版本上一處有貓膩的提交,再經過一番死磕,最后真相大白,問題得解。并針對Android 11的提交進行修復,往AOSP(Android開源社區)上進行commit,得到google developer對此問題的回復。寫這篇文章的目的除了讀者大致了解下Input系統,更重要的是為讀者提供一種思路,面對系統級的疑難雜癥該如何一步一步定位源碼,找到深層次的原因。

          前言

          在View中調用getHandler().removeCallbacks系列方法是很常見的一種退出保護方法。然而在Android 11的系統上,這將有可能導致界面的觸摸事件異常!

          背景

          近幾個月來收到了多起在Android手機上,拖拽界面時無法滑動的問題反饋。 表現為在異常的界面上按住屏幕進行滑動沒有任何響應,但又可以進行點擊。而除了這個界面,其他界面一切正常

          復現場景

          在B界面(個人主頁)發送事件(取消關注某個作者),界面A(列表界面)收到事件,進行RemoveData(移除對應作者的作品), 然后調用RecyclerView.Adapter#notifyDataSetChange操作通知刷新。再返回到A界面,此時的A界面變變得無法滑動,但可以點擊。再點擊進入其他界面C,C界面都可正常滑動。

          被合并的Move事件

          大部分的滑動問題都是因為存在著嵌套滑動沖突。為了驗證是否是嵌套的問題,我們需要在不同層級的View中打印接收到的MotionEvent. 很快,我們就排除了嵌套滑動的因素。因為當我們在Activity#dispatchTouchEvent的時候對MotionEvent進行打印,驚奇的發現MotionEvent在分發到Activity的時候就已經“不同尋常”。 1. 手指在按壓滑動過程中不會收到任何Move事件。Move事件在手指抬起后,跟隨Up事件一并發送,并且有僅只有一個Move事件。 2. 通過查看這個“唯一”的Move事件,發現其MotionEvent#getHistorySize()竟然達到幾十上百,存放著Move過程中的所有軌跡點。

          前期問題定位

          結合復現的場景,這里我們列出了問題相關的幾個“嫌疑人”

          1. VideoView。因業務涉及到視頻播放,是否存在視頻進行播放切換的時候,內部存在一些“操作”,例如SurfaceView的動態添加移除。這些操作在界面stop狀態下存在異常?

          在移除了視頻播放相關的業務邏輯之后依舊復現此問題。排除

          2. RecyclerView。RecyclerView的版本是從v7升級到androidx,會不會是RecyclerView的問題?

          在將RecyclerView的版本降回到v7的版本也依舊可以復現這個問題。排除

          3. 會不會是硬件層的觸摸事件采集出現了問題?

          結合異常情況出現時,是能同時存在正常界面的。底層的觸摸事件采集跟業務的界面屬于不同結構層級,業務的一些狀態管理問題應該不會反作用于硬件層的觸摸采集,因此這個問題與硬件層的關系不是很大。排除

          Android 11有貓膩

          在排查了多個因素無果之后,我們將焦點放到反饋問題的手機上。出現問題的手機有一個共同點是支持高刷新頻率(90HZ,120HZ...)。而一般手機的刷新頻率是60HZ。難道是高刷新頻率機制在某些場景下導致了觸摸事件的異常? 此外,高刷機型的聚集也側面反映了這些反饋問題的都是比較新款的手機,另一個共同點是對應的版本都是Android11。因此對刷新頻率和Android版本這兩個變量進行交叉組合驗證

          1. 60HZ(默認),90HZ和120HZ
          2. Android 10和Android 11

          經過測試:

          • 出現問題Android 11的手機的刷新頻率從120HZ設置為60HZ,依舊出現滑不動的問題
          • Android 10的手機即便設置了高刷新頻率,也不會出現滑不動的問題(華為Mate40Pro)

          這意味著滑動問題與Android 11存在著緊密的聯系,而Android 10是不存在這個問題的。那么要想徹底探究清楚這個問題,就必須深入了解Android 10和Android 11這兩版本在Input系統的事件處理上的差異,源碼分析勢在必行

          Framework源碼閱讀

          本文許多地方引用到了Android Framework中native,base這兩部分的源碼,這里提供源碼的閱讀的一些鏈接。

          1. https://cs.android.com/android/platform/superproject 推薦,優點是可以進行搜索,速度也挺快的
          2. https://android.googlesource.com/ 推薦,AOSP開源代碼倉庫,優點是可以查看最新的代碼和提交記錄
          3. http://androidxref.com 不推薦,已經很久不更新了,只有Android 9的源碼,只適合考古**

          由于對Input事件的處理涉及到Android框架的多個結構層次,從native到base層,且為了探究Android 11與之前的版本差異,更需要用到翻看git history對比差異。這里我是同步整個開源倉庫的代碼,學有余力的同學可以參考下這個Android 開源項目指南 Wiki

          Android Input系統

          Input系統結構

          這里先放一張結構草圖,讓大家對Input系統結構層次有個粗略的印象。(PS:這里的流程是片面的)

          源碼中核心類及文件路徑:

          c++:

          • NativeInputEventReceiver /frameworks/base/core/jni/android_view_InputEventReceiver.cpp
          • InputReader /frameworks/native/services/inputflinger/reader/InputReader.cpp
          • InputDispatcher /frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
          • InputConsumer,InputChannel /frameworks/native/libs/input/InputTransport.cpp

          java:

          • ViewRootImpl /frameworks/base/core/java/android/view/ViewRootImpl.java
          • Choreographer /frameworks/base/core/java/android/view/Choreographer.java
          • Handler /frameworks/base/core/java/android/os/Handler.java

          Input系統基本單位 Window

          Android Input系統中Window是接收用戶Input事件的基本單位, 它可以是一個Activity,也可以是個Dialog,Toast,StatusBar,NavigationBar等等 ,每個Window都會對應一個ViewRootImpl. 前面分析的問題來說:界面A可以簡單理解為Window A,界面B為Window B

          Socket跨進程通信

          Android Input事件的讀取和分發是進行在一個System Server進程中的,因此從System Server進程中發送觸摸事件到我們App主進程是需要進行跨進稱通信,這里選用的通信方式就是socket Activity初始化的時候, 每一個Activity實例都會創建一個用于接收事件的socket通訊通道, 通過對Windows的管理, 找到當前需要接收事件的Windows, 通過socket直接將事件數據發送給對應的Windows, Window內以RootViewImpl為起點, 對事件進行分發處理。

          NativeInputEventReceiver

          NativeInputEventReceiver運行在主進程,承擔著socket cilent端的通信。其本質是一個LooperCallback,LooperCallback定義在system/core/include/utils/Looper.h中,作為Looper::addFd的回調 NativeInputEventReceiver的構造函數會接收Java層傳遞的Main Looper的MessageQueue指針, 初始化過程中, 調用Main Looper的addFd將該ViewRootImpl的InputChannel的接收端的fd添加到Main Looper的輪循中,同時將NativeInputEventReceiver注冊為回調。每次receiver端的socket中的事件到達的時候就會觸發到NativeInputEventReceiver的函數handleEvent調用。

          ViewRootImpl 萬View之祖

          ViewRootImpl顧名思義,是所有View的根結點,也是我們的DecorView的parent。事件分發到ViewRootImpl之后,會調用其內部的dispatchInputEvent分發,也就是我們老生常談的View事件分發。

          每一個ViewRootImpl都有一個WindowInputEventReceiver對象,其繼承自InputEventReceiver,WindowInputEventReceiver在ViewRootImpl#setView時, 對InputEventReceiver進行構造,在構造時調用nativeInit,創建NativeInputEventReceiver,將自己的指針傳給NativeInputEventReceiver,同時保留NativeInputEventReceiver的指針。可以理解為WindowInputEventReceiver是NativeInputEventReceiver在java層的“代言人”。 所以,每一個ViewRootImpl對應一個NativeInputEventReceiver。ViewRootImpl中的WindowInputEventReceiver#onInputEvent , onBatchedInputEventPending會在NativeInputEventReceiver#handleEvent中被調用。

          尋找消失的MotionEvent

          InputReader和InputDispatcher

          InputReaderInputDispatcher 是跑在System Server進程中的里面的兩個 Native 線程,負責讀取和分發 Input 事件。要想分析input事件的流向,需要從這里開始入手。

          • InputReader: 負責從 EventHub 里面把 Input 事件讀取出來,然后交給 InputDispatcher 進行事件分發
          • InputDispatcher: 在拿到 InputReader 獲取的事件之后,對事件進行包裝和分發 (也就是發給對應的Window) Connection: 與每個Window建立的通信鏈接對象,持有InputChannel(用于接收事件)OutboundQueue: 里面放的是即將要被派發給對應 Connection 的事件(每個Connection持有一個)WaitQueue: 里面記錄的是已經派發給Connection,但是還沒有得到App處理回應的事件(每個Connection持有一個)

          工作流程

          從InputReader和InputDispatcher這兩個線程的角度,我們可以將整個input事件的處理流程簡單歸納如下:

          1. InputReader 讀取 Input 事件
          2. InputReader 將讀取的 Input 事件放到 InboundQueue 中
          3. InputDispatcher 從 InboundQueue 中取出 Input 事件派發到目標 Connection 的 OutBoundQueue(即發送給哪個Window是由InputDispatcher決定的)
          4. 同時將事件記錄到各個 Connection 的 WaitQueue
          5. App 接收到 Input 事件,同時記錄到 PendingInputEventQueue ,然后對事件進行分發處理
          6. App 處理完成后,回調 InputManagerService 將負責監聽的 WaitQueue 中對應的 Input 移除

          InputDispatcher內部維護了一個mConnectionsByFd,根據File Descriptor存放了所有的Connection(與每個Window都有一個),Connection持有InputChannel用于發送Intput Message

          // All registered connections mapped by channel file descriptor.
          std::unordered_map<int, sp<Connection>> mConnectionsByFd GUARDED_BY(mLock); 
          

          Android系統中,Dispatch線程與眾多APP密切聯系,當我們創建一個APP時候,便于Dispatch線程產生聯系,這些Connection由窗口管理器(WindowManager)創建的。故Dispatch線程便可通過這些Connection將輸入事件發送給對應的APP。

          了解了一些Input機制后,我們該怎么對InputReaderInputDispatcher這兩個Native線程進行Native調試呢?

          InputReader

          這里我們使用的是sdk中自帶的工具systrace.py. 我們對異常界面進行了Systrace(在native分析方面比AS更強大)

          cd ${AndroidHome}/platform-tools/systrace python systrace.py --time=10 -o trace.html

          將生成html,拖入chrome://tracing/中進行分析。 可以看到InputReader在488ms,496ms,504ms有明顯的函數調用棧,即此時進行了input數據的采集,間隔約為8ms,符合當前120HZ的屏幕刷新頻率(1s/120HZ)。如果是60HZ的刷新頻率,則是約16ms進行input事件采集

          可以看到InputReader采集事件之后有喚醒InputDispatcher進行事件分發。EventHub及InputReader只負責將讀取到的事件分發給InputDispatcher,并不會關心到具體是那個界面,如果這里出了問題,那么應該是所有的界面都會出現同樣的問題。因此所以問題不會出現在InputReader

          那么懷疑點便來到了InputDispatcher,回到我們Move Event被合并的問題: Q1: 會不會是在InputReader線程發送的事件到Dispatcher的OutboundQueue中進行了合并處理? Q2: 會不會在InputDispatcher進行分發給Connection的時候做了合并的操作?

          InputDispatcher

          源碼核心類必能dump,源碼核心類必能dump,源碼核心類必能dump. 涉及到framework的核心類,在源碼的實現上都可以看到dump方法的實現,dump方法會打印該類的一些內部信息,借助這個dump方法,我們可以獲取framework類的大部分關鍵運行時信息

          系統服務調試指令 adb shell dumpsys adb shell dumpsys+ service name可以對系統框架服務進行當前的一些狀態信息,例如adb shell dumpsys activity用于打印當前的所有activity信息等。具體的service name可以通過adb shell dumpsysadb shell service list方法獲取。點擊了解更多dumpsys的使用

          我們這里使用的是adb shell dumpsys input,可以看到

          我們對出現問題的界面進行滑動,同時手指保持再屏幕上,不進行抬起,進行是adb shell dumpsys input 可以看到OutboundQueue中是沒有任何東西的,而WaitQueue中堆積了大量的MotionEvent(action=MOVE),此時也并沒有被合并成一個。

          與此同時,我們打開一個新的界面,在正常的界面上進行同樣的操作,發現正常的界面的WaitQueue并不會堆積如此之多的MotionEvent。 WaitQueue 依賴主線程消費 Input 事件后進行反饋,那么當 Input 事件沒有及時被消耗,就會在 WaitQueue 這里的length上體現出來。當 Input 事件長時間沒有被消費的話,我們常見的ANR Exception就是這里拋出的,最最常見的原因就是主線程的耗時操作,進而引發卡頓。

          但我們這里的問題與主線程耗時卡頓有本質區別。如果是主線程做了耗時的操作,也不應該出現WaitQueue里的Move事件一直持續增加。

          這里我們再放出系統結構圖,前面我們已經通過systraceadb shell dumpsys input,分析出1,2,3這流程是正常的,4這個步驟是用socket的一個發送input message,對數據無感的一個流程,而且我們在問題界面也能夠收到Down和Up事件。那么4這個步驟就是正常的。

          NativeInputEventReceiver

          這里需要對源碼逐步分析,當InputEvent到來的時候,調用的是NativeInputEventReceiver::handleEvent,其內部又調用了NativeInputEventReceiver::consumeEvents,核心對inputEvent的處理再InputConsumer:consume中。

          //以下代碼經過裁剪,去除了一些日志打印和非關鍵路徑的代碼
          int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
              if (events & ALOOPER_EVENT_INPUT) {
                  JNIEnv* env = AndroidRuntime::getJNIEnv();
                  status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr);
                  mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
                  return status == OK || status == NO_MEMORY ? 1 : 0;
              }
              if (events & LOOPER_EVENT_OUTPUT) {
                  return 1;
              }
              return 1;
          }
          

          在consumeEvents中可以看到正常的流程是會走native調用java方法InputEventReceiver#dispatchInputEvent.這里我們要留意的是其他分支情況,可以看到在status==WOULD_BLOCK,我們是會走到里面的分支,從native調用java方法InputEventReceiver#onBatchedInputEventPending,往下進行分析怎么場景會走到這里。 因為源碼邏輯比較復雜,我們的注意力要放在對ACTION_MOVE這類關鍵字上,看哪些這類事件進行了特殊操作

          //以下代碼經過裁剪,去除了一些日志打印和非關鍵路徑的代碼
          status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
                  bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
              if (consumeBatches) {
                  mBatchedInputEventPending = false;
              }
              if (outConsumedBatch) {
                  *outConsumedBatch = false;
              }
              ScopedLocalRef<jobject> receiverObj(env, nullptr);
              bool skipCallbacks = false;
              for (;;) {
                  uint32_t seq;
                  InputEvent* inputEvent;
                  status_t status = mInputConsumer.consume(&mInputEventFactory,
                          consumeBatches, frameTime, &seq, &inputEvent);
                  if (status == WOULD_BLOCK) {
            //收到socket傳來的input event時,以下條件為true
                      if (!skipCallbacks && !mBatchedInputEventPending && mInputConsumer.hasPendingBatch()) {
                          // There is a pending batch.  Come back later.
                          if (!receiverObj.get()) {
                              receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));
                          }
                          mBatchedInputEventPending = true;
                          env->CallVoidMethod(receiverObj.get(),
                                              gInputEventReceiverClassInfo.onBatchedInputEventPending,
                                              mInputConsumer.getPendingBatchSource());
                      }
                      return OK;
                  }
                  if (!skipCallbacks) {
                      jobject inputEventObj;
                      switch (inputEvent->getType()) {
                      case AINPUT_EVENT_TYPE_MOTION: {
                          MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
                          if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
                              *outConsumedBatch = true;
                          }
                          inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
                          break;
                      }
                      if (inputEventObj) {
                          env->CallVoidMethod(receiverObj.get(),
                                  gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
                      } else {
                          skipCallbacks = true;
                      }
                  }
          
                  if (skipCallbacks) {
                      mInputConsumer.sendFinishedSignal(seq, false);
                  }
              }
          }
          

          InputConsumer#consume的方法中,可以看到一處AMOTION_EVENT_ACTION_MOVE, 果不其然,在該方法中,對是否是input事件進行了判斷,如果是move類型的事件,會進行一個batch操作,然后直接返回,此時的*outEvent=nullptr.而當 事件為非move類型事件,會走到*outEvent=motionEvent;.最終在外頭會走到InputEventReceiver#dispatchInputEvent. 也就是MOVE類型的事件并沒有像Down和Up事件一樣走dispatchInputEvent方法分發到上層,而是走了另外一個onBatchedInputEventPending方法

          //以下代碼經過裁剪,去除了一些日志打印和非關鍵路徑的代碼
          status_t InputConsumer::consume(InputEventFactoryInterface* factory, bool consumeBatches,
                                          nsecs_t frameTime, uint32_t* outSeq, InputEvent** outEvent) {
              *outSeq = 0;
              *outEvent = nullptr;
              // Fetch the next input message.
              // Loop until an event can be returned or no additional events are received.
              while (!*outEvent) {
                  if (mMsgDeferred) {
                      // mMsg contains a valid input message from the previous call to consume
                      // that has not yet been processed.
                      mMsgDeferred = false;
                  } else {
                      // Receive a fresh message.
                      status_t result = mChannel->receiveMessage(&mMsg);
                      if (result) {
                          // Consume the next batched event unless batches are being held for later.
                          if (consumeBatches || result != WOULD_BLOCK) {
                              result = consumeBatch(factory, frameTime, outSeq, outEvent);
                              if (*outEvent) {
                                  break;
                              }
                          }
                          return result;
                      }
                  }
          
                  switch (mMsg.header.type) {
                      ...
                      case InputMessage::Type::MOTION: {
                          ssize_t batchIndex = findBatch(mMsg.body.motion.deviceId, mMsg.body.motion.source);
                          if (batchIndex >= 0) {
                              Batch& batch = mBatches.editItemAt(batchIndex);
                              if (canAddSample(batch, &mMsg)) {
                                  batch.samples.push(mMsg);
                               break;
                              } else {
                                   ...
                                  break;
                              }
                          }
          
                          // Start a new batch if needed.
                          if (mMsg.body.motion.action == AMOTION_EVENT_ACTION_MOVE ||
                              mMsg.body.motion.action == AMOTION_EVENT_ACTION_HOVER_MOVE) {
                              mBatches.push();
                              Batch& batch = mBatches.editTop();
                              batch.samples.push(mMsg);
                              break;
                          }
                          
                          //如果是ACTION_DOWN,ACTION_UP等其他事件最終會走到這里
                          updateTouchState(mMsg);
                          initializeMotionEvent(motionEvent, &mMsg);
                          *outSeq = mMsg.body.motion.seq;
                          *outEvent = motionEvent;
                          break;
                      }
                      ...
                  }
              }
              return OK;
          }
          
          

          輪循還是通知

          前面我們深入分析了源碼,最終發現在分發的路徑上,Move類型的事件并沒有跟Down和Up事件一樣走dispatchInputEvent直接分發到上層。之前的系統結構圖是不完整的!!! 有些同學會認為,觸摸事件的處理是由框架層每隔一定的周期(一幀)去調用某個native方法來觸發input事件上傳消費(輪循),或者是底層接收到觸摸事件之后,native調用java主動通知上層進行消費(通知).源碼分析到這里,可以發現在input事件分發消費機制中“輪循”和“通知”是并存的

          Batched Consumption機制

          首先需要了解下Batched Consumption機制。一般應用只在每個VSYNC的周期下進行一次繪制。因此,在每一幀的時候應用只能對一次input事件進行響應反饋。如果在一個VSYNC周期中出現了多個input事件,每次input事件到來的時候都立即分發到應用層是比較浪費資源的。為了避免浪費,就有了Batched Consumption機制,input事件會被進行批處理,然后在每個Frame渲染時發送一個batched input事件給到應用層。

          對于批量的Move事件,事件從分發到消費對的鏈路如下:

          1. InputDispatcher 分發事件到app層
          2. app層的Looper 收到事件通知
          3. 執行handleEvent方法. 從fd中讀取Event
          4. 當存在batched event時,InputConsumer::hasPendingBatch 將會返回true. 這個時候并不會發送event到我們的app上.
          5. native層會調用InputEventReceiver#onBatchedInputEventPending告知app,有batched event可供消費。這時候就會通過Choreographerschedules一個ConsumeBatchedInputRunnable在下一幀之前來進行input event的消費
          6. ConsumeBatchedInputRunnable在執行的時候不只是進行batched input的消費,會盡可能將socket中所有的input event都進行消費
          7. native調用到InputEventReceiver#onInputEvent,將所有傳入的事件都發送到app層。

          對于Down和Up事件來說,并沒有batched event的概念,因此鏈路為1,2,3,7之前的系統圖只適合描述Down和Up事件

          最接近真相的猜想

          將我們的異常現象的表現結合Batched Consumption機制,有了以下的猜想:

          在一次觸摸屏幕開始之后,Down事件由底層向上層正常進行分發,Move事件也到來了,但是沒有立即分發給上層,此時只是在native進行batch,并通知上層來進行讀取消費。而上層在此時調用底層進行讀取Move事件的鏈路上出現了異常!導致Move事件在WaitQueue里面進行堆積,一直沒有被消費。而手指抬起的時候,產生了Up事件,觸發了向上層分發Up事件,順帶將隊列前面的沒有被消費的所有Move事件一并向上發送。(這里是個傳遞指針操作)

          兩種事件分發模式,最后都走到了native調用java方法,dispatchInputEventonBatchedInputEventPending,這些方法運行在主進程。我們可以查看java堆棧來查看不同場景下Down,Up和Move事件的分發過程中的Java調用鏈

          使用AndroidStudio Profile查看Java調用棧 使用AndroidStudio Profile工具,選擇CPU,觸摸界面并進行record,dump文件之后,可以看到java層的代碼調用。(AS也可以進行native調用棧的查看)

          那么我們來check下不同場景下,consumeBatchInput的調用情況。 這里羅列幾個AS的trace圖,可以更直觀的看到系統對Down,Up和Move事件的不同處理過程。

          實驗手機是oppo find x2 pro (Android 11)

          Down和Up

          Down和Up事件走dispatchInputEvent分發到上層

          正常情況Consume Batched MoveEvent

          異常情況Consume Batched MoveEvent

          百花齊放的ROM

          細心的讀者可能會發現,上面正常情況的圖中里面并沒有出現onBatchedInputEventPending調用,而是由ViewRootImpl每隔一幀的時間觸發一次消費consumeBatchedInput.并不是照Android 11源碼上的,只有當move事件到來的時候,觸發onBatchedInputEventPending,再下一幀繪制的時候觸發一次consumeBatchedInput 探究后,發現這手機(Oppo find x2 pro)雖然是Android 11的版本,但在input事件的處理上存在著諸多Android 9的代碼調用,Android 9在消費Move事件上是輪循的機制,而Android 11在消費Move事件上是通知的機制

          ViewRootImpl

          從前面的java堆棧圖中,我們可以看到java層是主動調用了一個doConsumeBatchedInput來進行input事件消費的。而這個doConsumeBatchedInput與兩個Runnable有關ConsumeBatchedInputRunnableConsumeBatchedInputImmediatelyRunnable

          ConsumeBatchedInputRunnable 和 ConsumeBatchedInputImmediatelyRunnable

          ConsumeBatchedInputRunnableConsumeBatchedInputImmediatelyRunnable這兩個是ViewRootImpl中定義的Runnable,他們都會調用到native方法nativeConsumeBatchedInputEvents讀取inputChannel中的input event,前者是等到下一個Frame繪制的時候再執行input事件消費。后者如其名稱immediately,是立即進行input事件的消費,常用于一些異常場景下的事件清零操作。 與此對應的有mConsumeBatchInputScheduledmConsumeBatchInputImmediatelyScheduled這兩個變量,來標識是否已經將對應的Runnable添加到MessageQueue里面,避免加入重復的Runnable。在對應Runnable的內部執行中又會把這個變量置為false。

          Lastest Change

          現在壓力傳遞到了ViewRootImpl,Android 11是去年年底發布的,有可能是最近的提交引入了這個問題。老規矩,甩鍋常規操作,點開git history查看源碼最近一段時間的改動提交

          改動點1: ViewRootImpl#scheduleConsumeBatchedInput

          這里對ConsumeBatchedInputRunnable的添加新增了一個開關變量mConsumeBatchedImmediatelyScheduled,使得“延時消費input”和“立即消費input”變成兩個互斥的操作。

          改動點2: ViewRootImpl#setWindowStopped

          可以看到在去年的六月,google developer A在setWindowStopped中新增調用一次scheduleConsumeBatchedInputImmdiately()。目的是在window切換為stopped狀態后為了避免ANR,調用scheduleConsumeBatchedInputImmdiately()立即進行一次input事件消費 也就是在這里mConsumeBatchedInputImmediatelyScheduled這個變量被置為true,從結果上來說,這個Runnable并沒有被執行!

          基于改動的猜想

          針對這兩次的修改,我們大膽猜測mConsumeBatchInputImmediatelyScheduled這個在置為true之后,出現了某種異常,對應的ConsumeBatchedInputImmediatelyRunnable并沒有被執行,該變量并沒有被置為false,導致另外一個ConsumeBatchedInputRunnable不滿足執行條件,進而引發事件消費異常。Move Event沒有被應用消費,導致界面無法滑動。那么我們如何進行驗證呢?

          雖然說ViewRootImpl是框架層的類,代碼層沒法直接引用到,但畢竟是萬view之祖,我們可以拿到DecorView,再拿到DecorView的父View來得到ViewRootImpl,進而探訪這個ViewRootImpl對象。 斷點之下,一覽無余!

          可以看到出問題的界面上的ViewRootImpl對象的mConsumeBatchedImmediatelyScheduledtrue,與我們的猜想一致。那問題來到了這個mConsumeBatchedInputImmediatelyRunnable為什么沒有被執行!

          Runnable沒有被執行?是不是從消息隊列中被remove了?

          我們在ViewRootImpl中翻看,并沒有看到有將ConsumeBatchedInputImmediatelyRunnable進行reomve的操作。

          臨時修復方案

          滑不動的直接原因找到了,那么我們就可以先“對癥下藥”,出了個臨時的修復方案,我們針對Android 11的機型,在界面onResume的時候,取到ViewRootImpl對象(可以通過DecorView#getParent取到),運用反射,對mConsumeBatchedImmediatelyScheduled這個變量進行了檢測,如果是true則需要進行修復,修改值為false,并調用一次scheduleConsumeBatchedInput觸發原有的input消費流程。經過驗證,界面恢復正常了!

          意料之外的調用

          再仔細閱讀下setWindowStopped,這個函數是有個參數bool stopped,即在Stopped狀態下的參數是true,但參數為false的時候也同樣調用了scheduleConsumeBatchedInputImmediately

          追溯下setWindowStopped的調用,發現在Activity#performStart的時候也會調用到這里。而這次的調用顯然是不符合預期的(預期只在Window stopped下進行調用,用于避免ANR,所以說Window start的時候的調用就屬于意料之外)我們之前的操作場景下B界面回到A界面時,就會觸發A界面的performStart進而調用到scheduleConsumeBatchedInputImmediately

          這個Runnable并沒有設置任何延時,應該是要被立馬執行的。 在回到setWindowStopped下閱讀,看下不同參數下的執行路徑,當stoppedfalse時,是先執行了scheduleTraversals,之后便調用了scheduleConsumeBatchedInputImmediately

          進入scheduleTraversals,發現方法內部調用了mHandler.getLooper().getQueue().postSyncBarrier()對MessageQueue直接進行了操作,這個操作很可能是ConsumeBatchedInputImmediatelyRunnable沒有運行的關鍵所在。

          //ViewRootImpl.java
          void scheduleTraversals() {
              if (!mTraversalScheduled) {
                  mTraversalScheduled = true;
                  mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();//??這里對MessageQueue做了一個postSyncBarrier的操作
                  mChoreographer.postCallback(
                          Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                  notifyRendererOfFramePending();
                  pokeDrawLockIfNeeded();
              }
          }
          

          Handler之同步屏障

          scheduleTraversals中的postSyncBarrier就是往MessageQueue中插入一個同步屏障消息。 MessageQueue中的消息可以分為三種:普通消息(同步消息)屏障消息(同步屏障)和異步消息。我們通常使用的都是普通消息,而屏障消息就是在消息隊列中插入一個屏障,在屏障之后的所有普通消息都會被擋著,不能被處理。不過異步消息卻例外,屏障不會擋住異步消息,因此可以這樣認為:屏障消息就是為了確保異步消息的優先級,設置了屏障后,只能處理其后的異步消息,同步消息會被擋住,除非撤銷屏障。

          屏障消息

          對于一個普通消息來說,它都是存在target,而屏障信息跟同步消息最大的區別就是沒有target,因為屏障消息不需要被執行。

          //MessageQueue.java
          public int postSyncBarrier() {
              return postSyncBarrier(SystemClock.uptimeMillis());
          }
          //可以看到下面生成屏障消息的時候并沒有設置 target
          private int postSyncBarrier(long when) {
              // Enqueue a new sync barrier token.
              // We don't need to wake the queue because the purpose of a barrier is to stall it.
              synchronized (this) {
                  final int token = mNextBarrierToken++;
                  final Message msg = Message.obtain();
                  msg.markInUse();
                  msg.when = when;
                  msg.arg1 = token;
                  Message prev = null;
                  Message p = mMessages;
                  if (when != 0) {
                      while (p != null && p.when <= when) {
                          prev = p;
                          p = p.next;
                      }
                  }
                  if (prev != null) { // invariant: p == prev.next
                      msg.next = p;
                      prev.next = msg;
                  } else {
                      msg.next = p;
                      mMessages = msg;
                  }
                  return token;
              }
          }
          

          ViewRootImpl中的同步屏障

          ViewRootImpl#scheduleTraversals方法就使用了同步屏障,以此阻塞其他的同步消息,保證UI繪制優先執行。之后再移除這屏障,讓同步消息執行起來。(這也是AOSP源碼中唯一一處使用到同步屏障機制的地方)

          mTraversalBarrier是用于存放同步屏障的token的變量

          //繪制UI之前設置同步屏障,保存 token 到 mTraversalBarrier
          void scheduleTraversals() {
              if (!mTraversalScheduled) {
                  mTraversalScheduled = true;
                  mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                  mChoreographer.postCallback(
                          Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                  notifyRendererOfFramePending();
                  pokeDrawLockIfNeeded();
              }
          }
          //在performTraversals進行繪制,此時可以根據 mTraversalBarrier 移除同步屏障
          //這里需要知道的是View繪制三大流程measure,Layou,Draw。就發生在performTraversals中,不做展開。
          void doTraversal() {
              if (mTraversalScheduled) {
                  mTraversalScheduled = false;
                  mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
          
                  if (mProfile) {
                      Debug.startMethodTracing("ViewAncestor");
                  }
          
                  performTraversals();
          
                  if (mProfile) {
                      Debug.stopMethodTracing();
                      mProfile = false;
                  }
              }
          }
          

          被遺忘的Runnable

          結合前面提到同步屏障的機制,可以發現當Activity#performStart的時候會觸發一次ViewRootImpl#scheduleTraversals,與此同時設置了一個同步屏障,并緊隨其后添加了ConsumeBatchedInputImmediatelyRunnable這個同步消息。這個同步消息因同步屏障的存在并不會立即被執行,而是被阻塞住直到UI繪制完成。

          到這里我們猜想是因為ViewRootImpl中同步屏障出現了問題,設置了多個屏障,但是只移除了一個屏障,仍有屏障沒有被移除,導致了后續的ConsumeBatchedInputImmediatelyRunnable沒有執行。

          那么怎么驗證呢? 將消息隊列中所有的消息打印出來!看是否存在barrier消息和被阻塞的ConsumeBatchedInputImmediatelyRunnable 前面說過AOSP中大多數的核心類都提供了dump方法用于調試,LooperMessageQueue中也有,Looper中的是public可以被調用到

          Looper.java
          public void dump(@NonNull Printer pw, @NonNull String prefix) {
              pw.println(prefix + toString());
              mQueue.dump(pw, prefix + "  ", null);
          }
          
          MessageQueue.java
          void dump(Printer pw, String prefix, Handler h) {
                  synchronized (this) {
                      long now = SystemClock.uptimeMillis();
                      int n = 0;
                      for (Message msg = mMessages; msg != null; msg = msg.next) {
                          if (h == null || h == msg.target) {
                              pw.println(prefix + "Message " + n + ": " + msg.toString(now));
                          }
                          n++;
                      }
                      pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked()
                              + ", quitting=" + mQuitting + ")");
                  }
              }
          

          ViewRootImpl中的mHandler的Looper即主線程的Looper,我們可以調用以下的方法進行打印調試

          Looper.getMainLooper().dump(new LogPrinter(int priority,String tag),String prefix);
          

          我們在異常的界面上打印MainLooper的MesageQueue中的所有Message對象 但在打印面板上并沒有發現Barrier Message和ConsumeBatchedInputImmediatelyRunnable Message的蹤影,也就是說ConsumeBatchedInputImmediatelyRunnable并沒有被阻塞在MessageQueue中,也沒有被運行,那我們的Runnable哪去了? 前面我們提及了在ViewRootImpl中并沒有找到對mHandler進行remove runnable的操作。

          在正常的業務場景中,我們也會創建內部的handler對象,并在銷毀等退出時機下,對該handler對象進行消息對象的移除,來避免內存泄漏問題。

          因此,我們將排查的目標擴散到了我們的業務類,對所有涉及到Handler的remove操作的方法removeCallbacks,removeMessage,removeCallbacksAndMessages等等進行排查。 果不其然,我們定位到了一個類A,其在內部onDetachedFromWindow的時候調用的是View#getHandler,并不是業務內創建的handler對象。

          public class A extends View {
           ...
           @Override
           protected void onDetachedFromWindow() {
              super.onDetachedFromWindow();
              Handler handler = getHandler();//這里調用的是View#getHandler()
              if (handler != null) {
                 handler.removeCallbacksAndMessages(null);
              }
           }
          }
          

          View#getHandler

          前面我們提到過ViewRootImpl萬view之祖,這里拿到的getHandler取到對象就是ViewRootImpl$ViewRootHandler,與添加ConsumeBatchedInputImmediatelyRunnable的Handler是同一個,對此handler調用handler.removeCallbacksAndMessages(null);就會將同時處于MessageQueue中的ConsumeBatchedInputImmediatelyRunnable移除,從而造成連鎖反應,進而導致我們這個滑動問題!

          View#mAttachInfo

          View中的getHandler()為什么會是ViewRootImpl$ViewRootHandler?先看下源碼中View中是怎么取到handler的。

          //View.java
          public Handler getHandler() {
              final AttachInfo attachInfo = mAttachInfo;
              if (attachInfo != null) {
                  return attachInfo.mHandler;
              }
              return null;
          }
          

          View是通過在一個mAttachInfo對象取到handler,而View中的mAttachInfo來自于父ViewGroup,ViewGroup在addView和dispatchAttachedToWindow中會將自己的mAttachInfo分發給子view,而ViewGroup的mAttachInfo正是來自于ViewRootImpl,ViewRootImpl在與DecorView的綁定中將mAttachInfo傳遞給DecorView,進而傳遞到每一個子View上。詳細的可以自行翻看下源碼。

          總結

          在我們將業務內getHandler().removeCallbacksAndMessage的錯誤調用去除后,應用就恢復了正。

          總結下滑動問題的鏈路流程:

          1.我們業務對一個Stop的界面A進行了列表數據的remove

          2.回到界面A,觸發onStart,在Framework的ViewRootImpl會在此時,觸發一次scheduleTraversals準備下一幀的界面重繪,在Android 11的版本上,還會額外調用一個ConsumeBatchedInputImmediatelyRunnable,因為scheduleTraversals會觸發同步屏障,這個ConsumeBatchedInputImmediatelyRunnable并不會被立即運行,必須等到下一幀開始繪制后才可以運行

          3.繪制開始performTraversal中會調用到onMeasure,onLayout和onDraw等流程,由于我們進行了RecyclerView數據的移除,會觸發到RecyclerView#onLayout,然后觸發部分ItemView的onDetachedFromWindow

          4.在這個onDetachedFromWindow中我們調用了getHandler().removeCallbacksAndMessages(null),將target同為ViewRootImpl$ViewRootHandler的ConsumeBatchedInputImmediatelyRunnable從消息隊列中移除。

          5.渲染結束,但是ConsumeBatchedInputImmediatelyRunnable并沒有被執行,mConsumeBatchInputImmediatelyScheduled卻已經被置為true,沒有被重置為false

          6.觸摸屏幕,底層Down事件分發正常

          7.當底層Input事件中的Move事件到來,觸發了onBatchedInputEventPending,觸發到scheduleConsumeBatchedInput,因為Android 11版本新增了對mConsumeBatchInputImmediatelyScheduled開關變量檢測,沒有往下觸發流程,導致move事件沒有被消費。

          8.底層Up事件正常分發,順帶將前面被阻塞的Batched Move事件上傳

          向AOSP發一個小小的commit

          前面分析過,ViewRootImpl#setWindowStoppedActivtity#performStart階段存在對scheduleConsumeBatchedInputImmediately不合理的調用,加上我們不合理的Handler#removeCallbacksAndMessage導致問題悲劇的發生,這里提一個小的commit到AOSP上來移除這個不合理的調用,并invite了當時對這里修改的Google developer前來code review. 這是當時的commit message https://android-review.googlesource.com/c/platform/frameworks/base/+/1722623

          Commit Message

          Google developer's reply

          不久后也收到Google developer的回復。Google內部早已經revert這一次有問題的提交(was invalid),此外還給出了另外一個解法,并熱心的貼出一個內部的patch和文檔來解釋ComsumeBatch的機制。感興趣的同學可以通過commit鏈接進行查看。短時間內Android 11依舊會保持現狀,我們需要持續注意這個問題。

          應對方案

          這個滑動問題,造成的因素有Android 11框架層的一個冗余調用,也有業務側對View#getHandler().removeCallbacks(null)系列方法的不規范調用。我們業務已經對內部存量的View#getHandler().removeCallbacks(null)調用進行替換和移除。考慮到Android 11框架層這個冗余調用會在短期內一直存在,同時也很難保證所有開發和第三方庫在此系列方法上的規范調用,我們會維持臨時修復方案。

          引用參考

          Android Systrace 基礎知識 - Input 解讀(https://androidperformance.com/2019/11/04/Android-Systrace-Input/)

          十分鐘了解Android觸摸事件原理(https://www.jianshu.com/p/f05d6b05ba17)

          招賢納士

          手Q招聘Android開發工程師,感興趣可前往此頁面投遞:

          https://careers.tencent.com/jobdesc.html?postId=1404731576830402560

          或將簡歷發送至:erainzhong@tencent.com

          # 網頁文本禁止復制粘貼?一分鐘學會8種方法輕松突破限制

          段子手168


          方法一:代碼破解法

          打開你需要復制內容的網頁,在瀏覽器地址欄輸入“javascript:void($={});”這串代碼,

          然后按下回車鍵,這時候就允許你復制文本了。

          方法 二:打印網頁法

          我們還可以利用打印網頁的時候,在預覽頁面將文本復制下來。按下快捷鍵【Ctrl+P】,

          將會進入打印界面,直接在右側的預覽界面,選中文本進行復制。

          方法三:后臺控制端

          打開網頁后,按下功能鍵【F12】,進入網頁后臺找到【Console】,

          在下面輸入這串符號“$=0”,再2按下回車鍵,

          網頁文字就能自由復制了。

          方法四:查看源代碼

          你還可以在網頁空白處,右擊選擇【查看頁面源代碼】,然后一直向下滑動,找到密密麻麻的文本,

          選中直接復制提取出來。

          方法五:保存本地網頁

          打開網頁鼠標右擊,選擇【網頁另存為】,然后在彈出的窗口中,

          將保存類型改為【網頁,僅HTML】,接著點擊【保存】。

          關閉當前網頁,回到桌面找到剛剛保存的本地網頁文件,雙擊打開后,就可以隨意復制啦。

          方法六:截圖識別文字

          此外,我們還可以利用OCR文字識別技術,將網頁文字識別出來。

          需要借助掌上識別王工具,找到【文字識別】-【快速截圖識別】功能。

          方法七:

          網址最前面加上 read: (用 Microsoft Edge 瀏覽器打開)

          方法八:

          1)按 F12 打開調試框,點擊右上角【設置】。

          2)往下拉,找到 【Debugger】

          3)勾選 【Disable JavaScript】

          4)返回頁面,按 F5 刷新一下頁面,這樣網頁文字就可以復制了。


          主站蜘蛛池模板: 国产精品区AV一区二区| 国产成人精品一区二区A片带套| 福利电影一区二区| 麻豆精品人妻一区二区三区蜜桃| 久久久久女教师免费一区| 日本一区二区三区在线看| 亚洲日韩中文字幕一区| 久久无码人妻一区二区三区| 国产精品美女一区二区| 国产一区二区在线观看| 日韩毛片基地一区二区三区| 高清一区二区三区日本久| 无码人妻精品一区二区蜜桃AV| 亚洲一区影音先锋色资源| 熟女少妇精品一区二区| 亚洲免费一区二区| 冲田杏梨高清无一区二区| 国产精品成人免费一区二区 | 国产精品成人99一区无码| 91精品乱码一区二区三区| 91午夜精品亚洲一区二区三区| 久久99热狠狠色精品一区| 亚洲综合av永久无码精品一区二区| jazzjazz国产精品一区二区| 日本在线视频一区| 国产成人片视频一区二区| 91麻豆精品国产自产在线观看一区 | 午夜DV内射一区区| 精品无码国产AV一区二区三区| 国产乱码精品一区二区三区麻豆| 日本一道高清一区二区三区| 影院成人区精品一区二区婷婷丽春院影视| 精品少妇ay一区二区三区| 国产精品电影一区| 内射一区二区精品视频在线观看| 无码精品久久一区二区三区 | 无码人妻精品一区二区三区99性| 亚洲AV无码一区二区三区在线| 亚洲视频在线一区二区三区| 国产A∨国片精品一区二区| 午夜影院一区二区|