該庫已經開源到github,地址github.com/AlexMahao/S…
目標
一個用於監聽android事件分發流程的庫,兩行程式碼即可在執行時期監聽事件的分發流程。
在編寫一些複雜的佈局時,常常由於事件分發到底是哪個view
處理產生困擾,做法通常需要經過以下步驟:
- 自定義一個
View
,重寫disaptchTouchEvent
等方法。 - 新增
log
日誌。 - 然後替換佈局檔案。
- 編譯,通過控制檯檢視事件分發流程。
- 繼續自定義
View
.... 如果沒有發現問題,無線迴圈... - 問題解決,刪除之前定義的
View
,還原佈局檔案。
對於如上的流程,需要多次的修改程式碼,編譯等,而且還有還原錯誤的風險。
那麼有沒有一種方式,能夠在儘可能的少編寫程式碼而實現上述流程,減少對於事件分發列印的困擾呢。
簡介
SimpleTouch
為了解決如上問題而誕生,該庫可以在執行時期列印完整的事件分發流程。
- 監聽
View
的dispatchTouchEvent
,onTouchEvent
,onInterceptTouchEvent
。 - 執行時期動態列印事件分發流程。
- 每一次完整的事件分發記錄以
json
的形式寫入檔案。 - 去重功能,對相同的
move
事件會自動過濾。 - 提供
no-op
版本,使用時可區分debug
和release
。 - 提供不同模式顯示
對於一次完整的手指點選,控制檯列印日誌如下:
同時提供以json
的格式寫入到磁碟,便於細緻分析。 (由於暫時沒找到合適的流程圖軟體,暫時以json代替)
該展示效果來源於bejson
的檢視展示功能。
使用
新增依賴
在專案的app
下的build.gradle
中新增依賴
debugApi 'com.spearbothy:simple-touch:1.0.5'
releaseApi 'com.spearbothy:simple-touch-no-op:1.0.5'
複製程式碼
初始化
在專案的Application
的onCreate()
中呼叫初始化方法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;
}
複製程式碼
注入代理類(用於監聽事件分發)
在Activity
的onCreate()
的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
版本,該版本中包含有初始化和注入方法的空實現,以達到debug
和release
使用不同的版本,使release
不包含任何注入和初始化邏輯。 - 在注入的時候有點耗時,如果頁面過於複雜,會有種頁面卡頓的感覺.
思考
對於該庫,其實核心就是怎麼能夠監聽onTouchEvent()
等事件分發方法。
從實現的角度,核心問題在於兩個:
- 如何生成代理類,該代理類中包含對
view
中事件的hook
。 - 如何將
View
替換為生成的代理類物件。
生成代理類
生成代理類有以下幾種方式:
- 靜態方式:預先編寫一些基本
view
的代理類,而對於自定義view
,可以在編譯期通過Processor
生成。 - 動態方式:在apk執行時期,動態的生成代理類,該方式參考
java
的動態代理機制。
替換代理類物件
- 靜態方式:在程式編譯時期,監聽xml的打包流程,動態的修改佈局檔案替換為代理類物件。類似於程式碼注入。
- 動態方式:在執行時期,構造
view
物件的時候,替換為構造代理類。
根據以上的兩種方式,最終全部選擇動態的方式,及執行時期動態的生成代理類以及動態的替換view
物件。
實現
生成代理類
java
本身提供了動態代理的機制,但是由於動態代理的物件必須是介面的方法,而view
的事件分發方法都不是某一個介面的方法,那麼java
本身的動態代理機制是不行的。
cglib
是java
的一個動態代理庫,可以代理類方法。但是因為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