SimpleTouch:一個庫徹底搞懂事件分發流程

Alex_MaHao發表於2018-12-03

SimpleTouch:一個庫徹底搞懂事件分發流程

該庫已經開源到github,地址github.com/AlexMahao/S…

目標

一個用於監聽android事件分發流程的庫,兩行程式碼即可在執行時期監聽事件的分發流程。

在編寫一些複雜的佈局時,常常由於事件分發到底是哪個view處理產生困擾,做法通常需要經過以下步驟:

  • 自定義一個View,重寫disaptchTouchEvent等方法。
  • 新增log日誌。
  • 然後替換佈局檔案。
  • 編譯,通過控制檯檢視事件分發流程。
  • 繼續自定義View .... 如果沒有發現問題,無線迴圈...
  • 問題解決,刪除之前定義的View,還原佈局檔案。

對於如上的流程,需要多次的修改程式碼,編譯等,而且還有還原錯誤的風險。

那麼有沒有一種方式,能夠在儘可能的少編寫程式碼而實現上述流程,減少對於事件分發列印的困擾呢。

簡介

SimpleTouch為了解決如上問題而誕生,該庫可以在執行時期列印完整的事件分發流程。

  • 監聽ViewdispatchTouchEventonTouchEventonInterceptTouchEvent
  • 執行時期動態列印事件分發流程。
  • 每一次完整的事件分發記錄以json的形式寫入檔案。
  • 去重功能,對相同的move事件會自動過濾。
  • 提供no-op版本,使用時可區分debugrelease
  • 提供不同模式顯示

對於一次完整的手指點選,控制檯列印日誌如下:

SimpleTouch:一個庫徹底搞懂事件分發流程
同時提供以json的格式寫入到磁碟,便於細緻分析。 (由於暫時沒找到合適的流程圖軟體,暫時以json代替)

SimpleTouch:一個庫徹底搞懂事件分發流程

該展示效果來源於bejson的檢視展示功能。

使用

新增依賴

在專案的app下的build.gradle中新增依賴

debugApi 'com.spearbothy:simple-touch:1.0.5'
releaseApi 'com.spearbothy:simple-touch-no-op:1.0.5'
複製程式碼

初始化

在專案的ApplicationonCreate()中呼叫初始化方法Touch.inject(this);

Touch.init(this, new Config().setSimple(false));

複製程式碼

Config物件提供一些配置選項

public class Config {

    // 輸出的日誌以極簡模式輸出
    private boolean isSimple = true;
    // 是否延遲列印日誌,延遲列印日誌會在觸控事件結束之後列印,並且具有去重功能
    private boolean isDelay = true;
    // 是否保留重複的,預設不保留
    private boolean isRepeat = false;
    // 是否寫入到檔案
    private boolean isPrint2File = true;
    // 是否處理,不處理則不會監聽任何方法,任何功能都無法生效
    private boolean isProcess = true;
}

複製程式碼

注入代理類(用於監聽事件分發)

ActivityonCreate()super.onCreate(savedInstanceState);之前呼叫.

  @Override
    protected void onCreate(Bundle savedInstanceState) {
        Touch.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mRootView = (LinearLayout) findViewById(R.id.root);
    }
複製程式碼

使用

編譯完成之後,開啟app,開始觸控吧!!! 每一次手指離開到觸控請間隔大於1s,目的是對於每次觸控加以區分,暫時沒想到合適的判斷條件。

備註

  • 提供了no-op版本,該版本中包含有初始化和注入方法的空實現,以達到debugrelease使用不同的版本,使release不包含任何注入和初始化邏輯。
  • 在注入的時候有點耗時,如果頁面過於複雜,會有種頁面卡頓的感覺.

思考

對於該庫,其實核心就是怎麼能夠監聽onTouchEvent()等事件分發方法。

從實現的角度,核心問題在於兩個:

  • 如何生成代理類,該代理類中包含對view中事件的hook
  • 如何將View替換為生成的代理類物件。

生成代理類

生成代理類有以下幾種方式:

  • 靜態方式:預先編寫一些基本view的代理類,而對於自定義view,可以在編譯期通過Processor生成。
  • 動態方式:在apk執行時期,動態的生成代理類,該方式參考java的動態代理機制。

替換代理類物件

  • 靜態方式:在程式編譯時期,監聽xml的打包流程,動態的修改佈局檔案替換為代理類物件。類似於程式碼注入。
  • 動態方式:在執行時期,構造view物件的時候,替換為構造代理類。

根據以上的兩種方式,最終全部選擇動態的方式,及執行時期動態的生成代理類以及動態的替換view物件。

實現

生成代理類

java本身提供了動態代理的機制,但是由於動態代理的物件必須是介面的方法,而view的事件分發方法都不是某一個介面的方法,那麼java本身的動態代理機制是不行的。

cglibjava的一個動態代理庫,可以代理類方法。但是因為android中是以dex方式儲存程式碼,所以無法應用於android

dexmaker是應用於android的動態生成程式碼的庫。可以用該庫實現動態生成代理類。

動態生成代理類的關鍵點在於ViewProxyBuilder類,通過該類可以生成代理類物件。

生成的方式如下:

 private static View proxy(final View view, AttributeSet attrs) {
        try {
            return ViewProxyBuilder.forClass(view.getClass())
                    .handler(new TouchHandler())
                    .dexCache(view.getContext().getDir(Constants.DEX_CACHE_DIR, Context.MODE_PRIVATE))
                    .constructorArgTypes(Context.class, AttributeSet.class)
                    .constructorArgValues(view.getContext(), attrs)
                    .addProxyMethod(Arrays.asList(Constants.PROXY_METHODS))
                    .build();
        } catch (IOException e) {
            return null;
        }
    }

複製程式碼

其中handler為代理方法處理類。

public class TouchHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        TouchMessageManager.getInstance().printBefore(proxy, method, args);
        Object result = ViewProxyBuilder.callSuper(proxy, method, args);
        TouchMessageManager.getInstance().printAfter(proxy, method, args ,result);
        return result;
    }
}

複製程式碼

該類實際上只是在方法前和方法後都列印日誌

動態替換物件

整合生成了代理物件,還有一個問題就是如何將生成的代理物件和原view`物件替換。

該思路來源於support-v7,對於繼承AppCompatActivity的頁面,其中的TextView等執行時期都會被替換為AppCompatTextView。核心便是LayoutInfalter類,該類用於生成所有的佈局物件,同時該類提供生成佈局物件的hook方法,可以新增一下自定義操作。

核心就是呼叫LayoutInflater.setFactory(),關鍵程式碼如下:

public static void inject(Context context) {
        if (sConfig == null || !sConfig.isProcess()) {
            return;
        }
        LayoutInflater inflater;
        if (context instanceof Activity) {
            inflater = ((Activity) context).getLayoutInflater();
        } else {
            inflater = LayoutInflater.from(context);
        }
        ViewFactory factory = new ViewFactory();
        if (context instanceof AppCompatActivity) {
            final AppCompatDelegate delegate = ((AppCompatActivity) context).getDelegate();
            factory.setInterceptFactory(new LayoutInflater.Factory2() {
                @Override
                public View onCreateView(String name, Context context, AttributeSet attrs) {
                    return delegate.createView(null, name, context, attrs);
                }

                @Override
                public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
                    return delegate.createView(parent, name, context, attrs);
                }
            });
        }
        // 設定hook
        inflater.setFactory2(factory);
    }

複製程式碼

引用或借鑑的三方庫

  • com.android.support:appcompat-v7
  • com.google.dexmaker:dexmaker
  • com.alibaba:fastjson
  • com.noober.background:core

關於

有任何疑問可以通過issue或者以郵件的形式傳送到zziamahao@163.com

相關文章