Androidのalibaba製UIイベントトラッキングライブラリを読む

# はじめに

サービスを成長させていく中で、機能がどんどん増えていってアプリでできることが多すぎる！なんてことはありませんか？
でも、一度追加した機能はアプリ自体のコードだけでなく、ユーザにも**関**わっていて、無暗に削除することは難しいですよね。
また、機能は開発側の意図しない使われ方をすることも多くあると思います。

そこで、数値ベースで機能の使われ方を調べるために、ユーザの行動トラッキングで良い方法を模索しています。
UIイベントにおいて、10月末に`version 1.0.0`がリリースされたalibaba製のライブラリ[ViewTracker-Android](https://github.com/alibaba/android_viewtracker)がサンプルを動かしてみた所、良さげでした。
しかし、まだリリースされてから日が浅い + :star:数も多くありません。
ただ、**star数が少ないからと言って信頼性がないというのは、なんとも悲しいこと**なので、ライブラリのコードはどうなっているのか読んでみることにしました。
※ **中国のECアプリ T-mall(天猫)のプロダクション環境**で、2016年3月から利用されているようです。

## 公式サンプル

![demo.gif](https://qiita-image-store.s3.amazonaws.com/0/42327/023545dd-6267-b6f0-8139-3ba81857ab53.gif)

Viewに対して、`view.setTag(TrackerConstants.VIEW_TAG_UNIQUE_NAME, viewName);`のように、`TrackerConstants.VIEW_TAG_UNIQUE_NAME`というタグにユニークな文字列を設定するだけで、 
**クリック**,**Viewが露出されていた時間**を検出し、自動的にログを採ることができます。
また、ページ or View単位で、任意の値を`HashMap`で設定することができます。

# ライブラリの処理を読み解く

## 利用する側から見る :eyes: 

ライブラリを利用するためにApplicationクラス内でトラッキング用クラスの`TrackerManager`を初期化します。

```java
TrackerManager.getInstance().setCommit(new DemoDataCommitImpl());
TrackerManager.getInstance().init(this, true, true, true);
```

まず、`DemoDataCommitImpl`のインスタンスを生成しセットしています。
`DemoDataCommitImpl`は`interface IDataCommit`を実装したクラスで、**クリック**,**Viewが露出されていた時間**を検出した時の値を受け取り、サーバへログを送信するなど任意の処理を行うことができます。

## どのようにしてトラッキングしているのか :thinking:

トラッキングするためのクラスとして、主に4つのクラスが登場します。

- トラッキング処理の全体を管理、ライブラリ利用側からのインターフェースとなる`TrackerManager`
- 主なトラッキング処理の起点となる`TrackerFrameLayout`
- クリックトラッキングを管理する`ClickManager`
- Viewの露出トラッキングを管理する`ExposureManager`

### TrackerManager

`TrackerManager.getInstance().init(...)`が呼ばれると、Applicationクラスに対して`ActivityLifecycleCallbacks`を実装した`ActivityLifecycleForTracker`が登録されます。

`ActivityLifecycleForTracker`はActivityが`onResume`,`onPause`される時に、`TrackerFrameLayout`の`attach`,`detach`処理を行います。

### TrackerFrameLayout

#### attach

1. ActivityのルートとなるView(id: `android.R.id.content`)を取得する。
2. ActivityをContextとして`TrackerFrameLayout`を生成する。
3. View(id: `android.R.id.content`)の子Viewを、取得・削除しながら、全ての子Viewを 2. で生成した`TrackerFrameLayout`へ移し替える。
4. `TrackerFrameLayout`をルートのViewとして、View(id: `android.R.id.content`)へ追加する。

つまり、既存のViewを全てトラッキング用のViewの子Viewへと移行して、トラッキングを行っています。

#### detach

1. attach時に追加した`TrackerFrameLayout`を削除する。

のみです。Activityの`destroy`時に呼び出されるため、移し替えたViewを戻す必要はありません。

### ClickManager（TrackerFrameLayoutでのクリックトラッキング）

1. `View.dispatchTouchEvent`のイベントが`MotionEvent.ACTION_DOWN`の時に処理が行われる。
2. `ClickManager.getInstance().eventAspect(...)`によって、
タップした座標に存在するかつ`tag`に`TrackerConstants.VIEW_TAG_UNIQUE_NAME`が設定されている**一番子供のView**に、`View.AccessibilityDelegate`を継承した`ViewDelegate`を登録する。
3. ユーザのアクションにより、登録した`ViewDelegate`の`sendAccessibilityEvent`が呼び出される。
4. 発生したイベントが`AccessibilityEvent.TYPE_VIEW_CLICKED`だった時に  
`DataProcess.processClickParams(...)`を経由し、初期化時に`TrackerManager.getInstance().setCommit(...)`した`IDataCommit.commitClickEvent`が呼び出される。

### ExposureManager（TrackerFrameLayoutでのView露出トラッキング）

1. ClickManagerの時と違い、Viewに変更が入る様々なタイミングで処理が行われる。
  - `View.dispatchTouchEvent`のイベントが`MotionEvent.ACTION_DOWN`
  - `GestureDetector.OnGestureListener.onFling`（フリック処理が呼び出された１秒後）
  - `View.dispatchWindowFocusChanged`
  - `View.dispatchVisibilityChanged`
2. `ExposureManager.getInstance().triggerViewCalculate(...)`によって、
`ExposureManager.traverseViewTree(...)`で`tag`に`TrackerConstants.VIEW_TAG_UNIQUE_NAME`が設定されている**自身と子要素全て**を、現在表示されているViewの一覧として`HashMap`に登録する。
3. 前回表示されていたViewの一覧の中で現在表示されているViewの一覧に存在しないViewだった時に、  
`new HandlerThread("ViewTracker_exposure")`で動作しているHandlerを介して、  
`DataProcess.commitExposureParams(...)`を経由し、初期化時に`TrackerManager.getInstance().setCommit(...)`した`IDataCommit.commitExposureEvent`が呼び出される。

また、`ViewPager`は他のViewに比べて特殊な構造をしているため、`View.onLayout`時に`ExposureManager.getInstance().traverseViewTree(...)`によって、`ReuseLayoutHook`というViewPager用のトラッキング処理を発火させるためのインスタンスを設定しています。


# さいごに

クラスの継承なども必要なく、他との依存が殆どないので、良さげだと思いました。 :+1:
`View.AccessibilityDelegate`を利用することで、既存の処理に影響を与えないようにしていて、全く自分の考えになかったので驚きました。

デメリットとしては、必ず`Viewの階層が１つ深くなる`、`Viewのクリック・表示非表示の判定は、View自身がそのようなメソッドを持っていないので自力で計算している`ということです。
UIトラッキングライブラリなのに、公式ページにライブラリを仕様した際のFPS比較が載っている理由が、コードを読んでみてわかりました。
FPS比較の結果は、影響がないと同等のようです。

画面遷移のトラッキングはできないですが、イベントのトラッキングは十分に機能があって
既にプロダクション環境で利用されていることもあり、導入しても問題なさそうだと感じています。:ok_woman: