はじめに
サービスを成長させていく中で、機能がどんどん増えていってアプリでできることが多すぎる!なんてことはありませんか?
でも、一度追加した機能はアプリ自体のコードだけでなく、ユーザにも関わっていて、無暗に削除することは難しいですよね。
また、機能は開発側の意図しない使われ方をすることも多くあると思います。
そこで、数値ベースで機能の使われ方を調べるために、ユーザの行動トラッキングで良い方法を模索しています。
UIイベントにおいて、10月末にversion 1.0.0
がリリースされたalibaba製のライブラリViewTracker-Androidがサンプルを動かしてみた所、良さげでした。
しかし、まだリリースされてから日が浅い + 数も多くありません。
ただ、star数が少ないからと言って信頼性がないというのは、なんとも悲しいことなので、ライブラリのコードはどうなっているのか読んでみることにしました。
※ 中国のECアプリ T-mall(天猫)のプロダクション環境で、2016年3月から利用されているようです。
公式サンプル
Viewに対して、view.setTag(TrackerConstants.VIEW_TAG_UNIQUE_NAME, viewName);
のように、TrackerConstants.VIEW_TAG_UNIQUE_NAME
というタグにユニークな文字列を設定するだけで、
クリック,Viewが露出されていた時間を検出し、自動的にログを採ることができます。
また、ページ or View単位で、任意の値をHashMap
で設定することができます。
ライブラリの処理を読み解く
利用する側から見る
ライブラリを利用するためにApplicationクラス内でトラッキング用クラスのTrackerManager
を初期化します。
TrackerManager.getInstance().setCommit(new DemoDataCommitImpl());
TrackerManager.getInstance().init(this, true, true, true);
まず、DemoDataCommitImpl
のインスタンスを生成しセットしています。
DemoDataCommitImpl
はinterface IDataCommit
を実装したクラスで、クリック,Viewが露出されていた時間を検出した時の値を受け取り、サーバへログを送信するなど任意の処理を行うことができます。
どのようにしてトラッキングしているのか
トラッキングするためのクラスとして、主に4つのクラスが登場します。
- トラッキング処理の全体を管理、ライブラリ利用側からのインターフェースとなる
TrackerManager
- 主なトラッキング処理の起点となる
TrackerFrameLayout
- クリックトラッキングを管理する
ClickManager
- Viewの露出トラッキングを管理する
ExposureManager
TrackerManager
TrackerManager.getInstance().init(...)
が呼ばれると、Applicationクラスに対してActivityLifecycleCallbacks
を実装したActivityLifecycleForTracker
が登録されます。
ActivityLifecycleForTracker
はActivityがonResume
,onPause
される時に、TrackerFrameLayout
のattach
,detach
処理を行います。
TrackerFrameLayout
attach
- ActivityのルートとなるView(id:
android.R.id.content
)を取得する。 - ActivityをContextとして
TrackerFrameLayout
を生成する。 - View(id:
android.R.id.content
)の子Viewを、取得・削除しながら、全ての子Viewを 2. で生成したTrackerFrameLayout
へ移し替える。 -
TrackerFrameLayout
をルートのViewとして、View(id:android.R.id.content
)へ追加する。
つまり、既存のViewを全てトラッキング用のViewの子Viewへと移行して、トラッキングを行っています。
detach
- attach時に追加した
TrackerFrameLayout
を削除する。
のみです。Activityのdestroy
時に呼び出されるため、移し替えたViewを戻す必要はありません。
ClickManager(TrackerFrameLayoutでのクリックトラッキング)
-
View.dispatchTouchEvent
のイベントがMotionEvent.ACTION_DOWN
の時に処理が行われる。 -
ClickManager.getInstance().eventAspect(...)
によって、
タップした座標に存在するかつtag
にTrackerConstants.VIEW_TAG_UNIQUE_NAME
が設定されている一番子供のViewに、View.AccessibilityDelegate
を継承したViewDelegate
を登録する。 - ユーザのアクションにより、登録した
ViewDelegate
のsendAccessibilityEvent
が呼び出される。 - 発生したイベントが
AccessibilityEvent.TYPE_VIEW_CLICKED
だった時に
DataProcess.processClickParams(...)
を経由し、初期化時にTrackerManager.getInstance().setCommit(...)
したIDataCommit.commitClickEvent
が呼び出される。
ExposureManager(TrackerFrameLayoutでのView露出トラッキング)
- ClickManagerの時と違い、Viewに変更が入る様々なタイミングで処理が行われる。
-
View.dispatchTouchEvent
のイベントがMotionEvent.ACTION_DOWN
-
GestureDetector.OnGestureListener.onFling
(フリック処理が呼び出された1秒後) View.dispatchWindowFocusChanged
View.dispatchVisibilityChanged
-
ExposureManager.getInstance().triggerViewCalculate(...)
によって、
ExposureManager.traverseViewTree(...)
でtag
にTrackerConstants.VIEW_TAG_UNIQUE_NAME
が設定されている自身と子要素全てを、現在表示されているViewの一覧としてHashMap
に登録する。 - 前回表示されていたViewの一覧の中で現在表示されているViewの一覧に存在しないViewだった時に、
new HandlerThread("ViewTracker_exposure")
で動作しているHandlerを介して、
DataProcess.commitExposureParams(...)
を経由し、初期化時にTrackerManager.getInstance().setCommit(...)
したIDataCommit.commitExposureEvent
が呼び出される。
また、ViewPager
は他のViewに比べて特殊な構造をしているため、View.onLayout
時にExposureManager.getInstance().traverseViewTree(...)
によって、ReuseLayoutHook
というViewPager用のトラッキング処理を発火させるためのインスタンスを設定しています。
さいごに
クラスの継承なども必要なく、他との依存が殆どないので、良さげだと思いました。
View.AccessibilityDelegate
を利用することで、既存の処理に影響を与えないようにしていて、全く自分の考えになかったので驚きました。
デメリットとしては、必ずViewの階層が1つ深くなる
、Viewのクリック・表示非表示の判定は、View自身がそのようなメソッドを持っていないので自力で計算している
ということです。
UIトラッキングライブラリなのに、公式ページにライブラリを仕様した際のFPS比較が載っている理由が、コードを読んでみてわかりました。
FPS比較の結果は、影響がないと同等のようです。
画面遷移のトラッキングはできないですが、イベントのトラッキングは十分に機能があって
既にプロダクション環境で利用されていることもあり、導入しても問題なさそうだと感じています。