はじめに
サービスを成長させていく中で、機能がどんどん増えていってアプリでできることが多すぎる!なんてことはありませんか?
でも、一度追加した機能はアプリ自体のコードだけでなく、ユーザにも関わっていて、無暗に削除することは難しいですよね。
また、機能は開発側の意図しない使われ方をすることも多くあると思います。
そこで、数値ベースで機能の使われ方を調べるために、ユーザの行動トラッキングで良い方法を模索しています。
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.dispatchWindowFocusChangedView.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比較の結果は、影響がないと同等のようです。
画面遷移のトラッキングはできないですが、イベントのトラッキングは十分に機能があって
既にプロダクション環境で利用されていることもあり、導入しても問題なさそうだと感じています。![]()
