Mixpanel唯一的开源的商业可视化埋点实现,提供可视化埋点以及代码埋点方式。
首先在Mixpanel官网注册,然后创建一个项目,有一个Token,在初始化的时候需要用到。
接下里我们分析一下源码:
入口MixpanelAPI,提供初始化方法
mMixpanel = MixpanelAPI.getInstance(this, projectToken);
Mixpanel支持代码埋点和可视化埋点,先看一下代码埋点:
1 代码埋点
MixpanelAPI提供track方法埋点
/**
* Track an event.
*
* <p>Every call to track eventually results in a data point sent to Mixpanel. These data points
* are what are measured, counted, and broken down to create your Mixpanel reports. Events
* have a string name, and an optional set of name/value pairs that describe the properties of
* that event.
*
* @param eventName The name of the event to send
* @param properties A JSONObject containing the key value pairs of the properties to include in this event.
* Pass null if no extra properties exist.
*/
// DO NOT DOCUMENT, but track() must be thread safe since it is used to track events in
// notifications from the UI thread, which might not be our MixpanelAPI "home" thread.
// This MAY CHANGE IN FUTURE RELEASES, so minimize code that assumes thread safety
// (and perhaps document that code here).
public void track(String eventName, JSONObject properties) {
if (hasOptedOutTracking()) return;
track(eventName, properties, false);
}
/**
* Equivalent to {@link #track(String, JSONObject)} with a null argument for properties.
* Consider adding properties to your tracking to get the best insights and experience from Mixpanel.
* @param eventName the name of the event to send
*/
public void track(String eventName) {
if (hasOptedOutTracking()) return;
track(eventName, null);
}
例如:
// Now we send an event to Mixpanel. We want to send a new
// "App Resumed" event every time we are resumed, and
// we want to send a current value of "hour of the day" for every event.
// As usual,all of the user's super properties will be appended onto this event.
try {
final JSONObject properties = new JSONObject();
properties.put("hour of the day", hourOfTheDay);
mMixpanel.track("App Resumed", properties);
} catch (final JSONException e) {
throw new RuntimeException("Could not encode hour of the day in JSON");
}
主要是在 activiyt 的 onPause 之后上传数据,同时也提供了方法让你立即上传时间:
public void flush() {
mMessages.postToServer(new AnalyticsMessages.FlushDescription(mToken));
}
2可视化埋点
讲到可视化埋点,那必须讲到服务器和客户端的通信,服务端下发消息客户端在哪里接收?
Mixpanel可视化埋点通信是基于Socket,并且可以找到WebSocketImpl这个类的decode方法,那么这个方法在哪里调用的呢,其实是在WebSocketClient的run方法中,读取到服务器的流然后decode解析,然后调用decodeFrames方法,里面调用
try {
wsl.onWebsocketMessage( this, Charsetfunctions.stringUtf8( f.getPayloadData() ) );
} catch ( RuntimeException e ) {
wsl.onWebsocketError( this, e );
}
最终实现在EditorConnection类
@Override
public void onMessage(String message) {
MPLog.v(LOGTAG, "Received message from editor:\n" + message);
try {
final JSONObject messageJson = new JSONObject(message);
final String type = messageJson.getString("type");
if (type.equals("device_info_request")) {//发送设备信息
mService.sendDeviceInfo();
} else if (type.equals("snapshot_request")) {//发送截图和控件坐标
mService.sendSnapshot(messageJson);
} else if (type.equals("change_request")) {//改变上报事件
mService.performEdit(messageJson);
} else if (type.equals("event_binding_request")) {//后台选中按钮下发绑定事件
mService.bindEvents(messageJson);
} else if (type.equals("clear_request")) {//清除请求
mService.clearEdits(messageJson);
} else if (type.equals("tweak_request")) {//上报事件
mService.setTweaks(messageJson);
}
} catch (final JSONException e) {
MPLog.e(LOGTAG, "Bad JSON received:" + message, e);
}
}
根据后台发送type来区分是要干什么。
主要讲一下snapshot_request(发送截图和控件坐标)和event_binding_request(后台选中按钮下发绑定事件)这两种类型。
1 snapshot_request(发送截图和控件坐标)
首先mService.sendSnapshot(messageJson);实现方法在ViewCrawler类,这个类比较重要,mService其实是ViewCrawler的一个内部内Editor
private class Editor implements EditorConnection.Editor {
@Override
public void sendSnapshot(JSONObject message) {
final Message msg = mMessageThreadHandler.obtainMessage(ViewCrawler.MESSAGE_SEND_STATE_FOR_EDITING);
msg.obj = message;
mMessageThreadHandler.sendMessage(msg);
}
.....
}
ViewCrawler里面有一个ViewCrawlerHandler,顾名思义这其实是继承自Handler, 这里发送了一个ViewCrawler.MESSAGE_SEND_STATE_FOR_EDITING消息,那么对应的我们在handleMessage方法应该要处理这个消息
@Override
public void handleMessage(Message msg) {
mStartLock.lock();
try {
final int what = msg.what;
switch (what) {
/*case MESSAGE_INITIALIZE_CHANGES:
loadKnownChanges();
break;*/
case MESSAGE_CONNECT_TO_EDITOR:
connectToEditor();
break;
case MESSAGE_SEND_DEVICE_INFO:
sendDeviceInfo();
break;
case MESSAGE_SEND_STATE_FOR_EDITING:
sendSnapshot((JSONObject) msg.obj);
break;
case MESSAGE_SEND_EVENT_TRACKED:
sendReportTrackToEditor((String) msg.obj);
break;
case MESSAGE_SEND_LAYOUT_ERROR:
sendLayoutError((ViewVisitor.LayoutErrorMessage) msg.obj);
break;
case MESSAGE_VARIANTS_RECEIVED:
handleVariantsReceived((JSONArray) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CHANGES_RECEIVED:
handleEditorChangeReceived((JSONObject) msg.obj);
break;
case MESSAGE_EVENT_BINDINGS_RECEIVED:
handleEventBindingsReceived((JSONArray) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED:
handleEditorBindingsReceived((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CHANGES_CLEARED:
handleEditorBindingsCleared((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_TWEAKS_RECEIVED:
handleEditorTweaksReceived((JSONObject) msg.obj);
break;
case MESSAGE_HANDLE_EDITOR_CLOSED:
handleEditorClosed();
break;
case MESSAGE_PERSIST_VARIANTS_RECEIVED:
persistVariants((JSONArray) msg.obj);
break;
}
} finally {
mStartLock.unlock();
}
}
当然要走到这里肯定要先连接到后台,其实就connectToEditor()方法(这个不详细介绍)。 回到刚刚我们需要接受到后台下发的消息之后上传我们当前界面的截图和控件的详细信息给到后台,也就是消息处理的sendSnapshot((JSONObject) msg.obj);跟进去看会拼接一个json
try {
writer.write("{");
writer.write("\"type\": \"snapshot\",");
writer.write("\"payload\": {");
{
writer.write("\"activities\":");
writer.flush();
mSnapshot.snapshots(mEditState, out);
}
final long snapshotTime = System.currentTimeMillis() - startSnapshot;
writer.write(",\"snapshot_time_millis\": ");
writer.write(Long.toString(snapshotTime));
writer.write("}"); // } payload
writer.write("}"); // } whole message
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't write snapshot request to server", e);
} finally {
try {
writer.close();
} catch (final IOException e) {
MPLog.e(LOGTAG, "Can't close writer.", e);
}
}
重要的是在mSnapshot.snapshots(mEditState, out)方法实现在ViewSnapshot类的snapshots方法,这个方法实现了对当前屏幕的截屏已经遍历当前界面rootView的遍历获取各个控件的左边等一些详细的信息。
/**
* Take a snapshot of each activity in liveActivities. The given UIThreadSet will be accessed
* on the main UI thread, and should contain a set with elements for every activity to be
* snapshotted. Given stream out will be written on the calling thread.
*/
public void snapshots(UIThreadSet<Activity> liveActivities, OutputStream out) throws IOException {
mRootViewFinder.findInActivities(liveActivities);
final FutureTask<List<RootViewInfo>> infoFuture = new FutureTask<List<RootViewInfo>>(mRootViewFinder);
mMainThreadHandler.post(infoFuture);
final OutputStreamWriter writer = new OutputStreamWriter(out);
List<RootViewInfo> infoList = Collections.<RootViewInfo>emptyList();
writer.write("[");
try {
infoList = infoFuture.get(1, TimeUnit.SECONDS);
} catch (final InterruptedException e) {
MPLog.d(LOGTAG, "Screenshot interrupted, no screenshot will be sent.", e);
} catch (final TimeoutException e) {
MPLog.i(LOGTAG, "Screenshot took more than 1 second to be scheduled and executed. No screenshot will be sent.", e);
} catch (final ExecutionException e) {
MPLog.e(LOGTAG, "Exception thrown during screenshot attempt", e);
}
final int infoCount = infoList.size();
for (int i = 0; i < infoCount; i++) {
if (i > 0) {
writer.write(",");
}
final RootViewInfo info = infoList.get(i);
writer.write("{");
writer.write("\"activity\":");
writer.write(JSONObject.quote(info.activityName));
writer.write(",");
writer.write("\"scale\":");
writer.write(String.format("%s", info.scale));
writer.write(",");
writer.write("\"serialized_objects\":");
{
final JsonWriter j = new JsonWriter(writer);
j.beginObject();
j.name("rootObject").value(info.rootView.hashCode());
j.name("objects");
snapshotViewHierarchy(j, info.rootView);
j.endObject();
j.flush();
}
writer.write(",");
writer.write("\"screenshot\":");
writer.flush();
info.screenshot.writeBitmapJSON(Bitmap.CompressFormat.PNG, 100, out);
writer.write("}");
}
writer.write("]");
writer.flush();
}
我们先看怎么截屏:
执行了一个Future,Future传入了一个从activity中得到的mRootViewFinder,mMainThreadHandler.post(infoFuture)
执行Callable的call()方法,实现为
@Override
public List<RootViewInfo> call() throws Exception {
mRootViews.clear();
final Set<Activity> liveActivities = mLiveActivities.getAll();
for (final Activity a : liveActivities) {
final String activityName = a.getClass().getCanonicalName();
final View rootView = a.getWindow().getDecorView().getRootView();
a.getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
final RootViewInfo info = new RootViewInfo(activityName, rootView);
mRootViews.add(info);
}
final int viewCount = mRootViews.size();
for (int i = 0; i < viewCount; i++) {
final RootViewInfo info = mRootViews.get(i);
takeScreenshot(info);
}
return mRootViews;
}
takeScreenshot(info)截取当前屏幕照片,这个方法实际是根据View反射拿到createSnapshot方法,获取当前屏幕的截图
private void takeScreenshot(final RootViewInfo info) {
final View rootView = info.rootView;
Bitmap rawBitmap = null;
try {
final Method createSnapshot = View.class.getDeclaredMethod("createSnapshot", Bitmap.Config.class, Integer.TYPE, Boolean.TYPE);
createSnapshot.setAccessible(true);
rawBitmap = (Bitmap) createSnapshot.invoke(rootView, Bitmap.Config.RGB_565, Color.WHITE, false);
} catch (final NoSuchMethodException e) {
MPLog.v(LOGTAG, "Can't call createSnapshot, will use drawCache", e);
} catch (final IllegalArgumentException e) {
MPLog.d(LOGTAG, "Can't call createSnapshot with arguments", e);
} catch (final InvocationTargetException e) {
MPLog.e(LOGTAG, "Exception when calling createSnapshot", e);
} catch (final IllegalAccessException e) {
MPLog.e(LOGTAG, "Can't access createSnapshot, using drawCache", e);
} catch (final ClassCastException e) {
MPLog.e(LOGTAG, "createSnapshot didn't return a bitmap?", e);
}
Boolean originalCacheState = null;
try {
if (null == rawBitmap) {
originalCacheState = rootView.isDrawingCacheEnabled();
rootView.setDrawingCacheEnabled(true);
rootView.buildDrawingCache(true);
rawBitmap = rootView.getDrawingCache();
}
} catch (final RuntimeException e) {
MPLog.v(LOGTAG, "Can't take a bitmap snapshot of view " + rootView + ", skipping for now.", e);
}
float scale = 1.0f;
if (null != rawBitmap) {
final int rawDensity = rawBitmap.getDensity();
if (rawDensity != Bitmap.DENSITY_NONE) {
scale = ((float) mClientDensity) / rawDensity;
}
final int rawWidth = rawBitmap.getWidth();
final int rawHeight = rawBitmap.getHeight();
final int destWidth = (int) ((rawBitmap.getWidth() * scale) + 0.5);
final int destHeight = (int) ((rawBitmap.getHeight() * scale) + 0.5);
if (rawWidth > 0 && rawHeight > 0 && destWidth > 0 && destHeight > 0) {
mCachedBitmap.recreate(destWidth, destHeight, mClientDensity, rawBitmap);
}
}
if (null != originalCacheState && !originalCacheState) {
rootView.setDrawingCacheEnabled(false);
}
info.scale = scale;
info.screenshot = mCachedBitmap;
}
如果createSnapshot方法拿到的是null的话还会根据view的getDrawingCache去拿到view绘制的一个缓存图像。
并且根据当前手机屏幕的分辨率来缩放一次。保证跟手机的分辨率一致。再上传给服务器,到这里屏幕截图就完成了。
接下来就是遍历rootView,并获取子view的详细信息了,
void snapshotViewHierarchy(JsonWriter j, View rootView)
throws IOException {
j.beginArray();
snapshotView(j, rootView);
j.endArray();
}
snapshotView(j, rootView)先把当前rootview信息添加到json中,然后
if (view instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) view;
final int childCount = group.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = group.getChildAt(i);
// child can be null when views are getting disposed.
if (null != child) {
j.value(child.hashCode());
}
}
}
j.endArray();
j.endObject();
if (view instanceof ViewGroup) {
final ViewGroup group = (ViewGroup) view;
final int childCount = group.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = group.getChildAt(i);
// child can be null when views are getting disposed.
if (null != child) {
snapshotView(j, child);
}
}
}
然后遍历调用snapshotView将所有子view的信息都添加到json中。
最终给后台的json格式如下:
{
"type": "snapshot",
"payload": {
"activities": [
{
"activity": "com.pingan.pamagicpaneldemo.MainActivity",
"pageid": "SYZX",
"pagename": "SYZX",
"scale": 0.33333334,
"serialized_objects": {
"rootObject": 65838376,
"objects": [
{
"hashCode": 65838376,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 1920,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"com.android.internal.policy.DecorView",
"android.widget.FrameLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": false,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.ColorDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 1920
},
"color": -328966
},
"subviews": [
252438848,
10748025
]
},
{
"hashCode": 252438848,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 1920,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.widget.LinearLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
107370860,
240855861
]
},
{
"hashCode": 107370860,
"id": 16909322,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 0,
"height": 0,
"scrollX": 0,
"scrollY": 0,
"visibility": 8,
"translationX": 0,
"translationY": 0,
"classes": [
"android.view.ViewStub",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 8,
"subviews": [ ]
},
{
"hashCode": 240855861,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 60,
"left": 0,
"width": 1080,
"height": 1860,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.widget.FrameLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
169189464
]
},
{
"hashCode": 169189464,
"id": 2131230770,
"mp_id_name": "decor_content_parent",
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 1860,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.ActionBarOverlayLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
14666390,
84354583
]
},
{
"hashCode": 14666390,
"id": 16908290,
"mp_id_name": "android:content",
"contentDescription": null,
"tag": null,
"top": 168,
"left": 0,
"width": 1080,
"height": 1692,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.ContentFrameLayout",
"android.widget.FrameLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
155356141
]
},
{
"hashCode": 155356141,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 1692,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.widget.LinearLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
239467042,
239996083,
76292720,
80280041,
149788270,
135446799,
124319132
]
},
{
"hashCode": 239467042,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 225,
"height": 57,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatTextView",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": false,
"alpha": 1,
"hidden": 0,
"text": "Hello World!",
"textColor": -1979711488,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 239996083,
"id": 2131230745,
"mp_id_name": "btn1",
"contentDescription": null,
"tag": null,
"top": 57,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击1",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 76292720,
"id": 2131230746,
"mp_id_name": "btn2",
"contentDescription": null,
"tag": null,
"top": 201,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击2",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 80280041,
"id": 2131230747,
"mp_id_name": "btn3",
"contentDescription": null,
"tag": null,
"top": 345,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击3",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 149788270,
"id": 2131230748,
"mp_id_name": "btn4",
"contentDescription": null,
"tag": null,
"top": 489,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击4",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 135446799,
"id": 2131230749,
"mp_id_name": "btn5",
"contentDescription": null,
"tag": null,
"top": 633,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击5",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 124319132,
"id": 2131230750,
"mp_id_name": "btn6",
"contentDescription": null,
"tag": null,
"top": 777,
"left": 0,
"width": 1080,
"height": 144,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.AppCompatButton",
"android.widget.Button",
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": true,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.RippleDrawable",
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 144
}
},
"text": "点击6",
"textColor": -570425344,
"fontSize": 42,
"subviews": [ ]
},
{
"hashCode": 84354583,
"id": 2131230723,
"mp_id_name": "action_bar_container",
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 168,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.ActionBarContainer",
"android.widget.FrameLayout",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.support.v7.widget.ActionBarBackgroundDrawableV21",
"android.support.v7.widget.ActionBarBackgroundDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 168
}
},
"subviews": [
146670918,
145212167
]
},
{
"hashCode": 146670918,
"id": 2131230721,
"mp_id_name": "action_bar",
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 168,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.Toolbar",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [
74140253,
54865874
]
},
{
"hashCode": 74140253,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 43,
"left": 48,
"width": 100,
"height": 81,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.widget.TextView",
"android.view.View"
],
"importantForAccessibility": true,
"clickable": false,
"alpha": 1,
"hidden": 0,
"text": "app",
"textColor": -1,
"fontSize": 60,
"subviews": [ ]
},
{
"hashCode": 54865874,
"id": -1,
"mp_id_name": null,
"contentDescription": null,
"tag": null,
"top": 0,
"left": 1080,
"width": 0,
"height": 168,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.ActionMenuView",
"android.support.v7.widget.LinearLayoutCompat",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"subviews": [ ]
},
{
"hashCode": 145212167,
"id": 2131230728,
"mp_id_name": "action_context_bar",
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 0,
"height": 0,
"scrollX": 0,
"scrollY": 0,
"visibility": 8,
"translationX": 0,
"translationY": 0,
"classes": [
"android.support.v7.widget.ActionBarContextView",
"android.support.v7.widget.AbsActionBarView",
"android.view.ViewGroup",
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 8,
"background": {
"classes": [
"android.graphics.drawable.LayerDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 0,
"top": 0,
"bottom": 0
}
},
"subviews": [ ]
},
{
"hashCode": 10748025,
"id": 16908335,
"mp_id_name": "android:statusBarBackground",
"contentDescription": null,
"tag": null,
"top": 0,
"left": 0,
"width": 1080,
"height": 60,
"scrollX": 0,
"scrollY": 0,
"visibility": 0,
"translationX": 0,
"translationY": 0,
"classes": [
"android.view.View"
],
"importantForAccessibility": false,
"clickable": false,
"alpha": 1,
"hidden": 0,
"background": {
"classes": [
"android.graphics.drawable.ColorDrawable",
"android.graphics.drawable.Drawable"
],
"dimensions": {
"left": 0,
"right": 1080,
"top": 0,
"bottom": 60
},
"color": -13615201
},
"subviews": [ ]
}
]
},
"screenshot": "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mHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQJhxAmHAAYcIBhAkHECYcQJhwAGHCAYQJBxAmHECYcABhwgGECQcQ9u9j1NyNowBRZQAAAABJRU5ErkJg"
}
],
"snapshot_time_millis": 172
}
}
2 event_binding_request(后台选中按钮下发绑定事件)
后台埋点下发事件最终也到handleMessage中处理对应的消息为:
case MESSAGE_HANDLE_EDITOR_BINDINGS_RECEIVED:
handleEditorBindingsReceived((JSONObject) msg.obj);
break;
在处理消息方法中调用applyVariantsAndEventBindings()方法,大致我们可以推断是在这个方法里面对下发的控件进行拦截,然后插入自己的监听事件。进去发现
{
if (mEditorEventBindings.size() == 0 && mOriginalEventBindings.size() == 0) {
for (MPPair<String, JSONObject> changeInfo : mPersistentEventBindings) {
try {
final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
newVisitors.add(new MPPair<String, ViewVisitor>(changeInfo.first, visitor));
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad persistent event binding cannot be applied.", e);
}
}
}
}
{
for (MPPair<String, JSONObject> changeInfo : mEditorEventBindings.values()) {
try {
final ViewVisitor visitor = mProtocol.readEventBinding(changeInfo.second, mDynamicEventTracker);
newVisitors.add(new MPPair<String, ViewVisitor>(changeInfo.first, visitor));
} catch (final EditProtocol.InapplicableInstructionsException e) {
MPLog.i(LOGTAG, e.getMessage());
} catch (final EditProtocol.BadInstructionsException e) {
MPLog.e(LOGTAG, "Bad editor event binding cannot be applied.", e);
}
}
}
final Map<String, List<ViewVisitor>> editMap = new HashMap<String, List<ViewVisitor>>();
final int totalEdits = newVisitors.size();
for (int i = 0; i < totalEdits; i++) {
final MPPair<String, ViewVisitor> next = newVisitors.get(i);
final List<ViewVisitor> mapElement;
if (editMap.containsKey(next.first)) {
mapElement = editMap.get(next.first);
} else {
mapElement = new ArrayList<ViewVisitor>();
editMap.put(next.first, mapElement);
}
mapElement.add(next.second);
}
mEditState.setEdits(editMap);
将下发的事件转化为ViewVisitor对象实现了一个Accumulator接口,这个接口实现方法就是反射拿到view的getAccessibilityDelegate然后替换成自己的AccessibilityDelegate,从而在用户触发控件的时候使用自己的AccessibilityDelegate获取信息上报给服务器,最终实现埋点的效果(谷歌提供的 Accessibility 相关 api ,具体可以详细的了解一下)。
接着看怎么实现的,最重要的是mEditState.setEdits(editMap)方法,我们再进去看一下,
/**
* Sets the entire set of edits to be applied to the application.
*
* Edits are represented by ViewVisitors, batched in a map by the String name of the activity
* they should be applied to. Edits to apply to all views should be in a list associated with
* the key {@code null} (Not the string "null", the actual null value!)
*
* The given edits will completely replace any existing edits.
*
* setEdits can be called from any thread, although the changes will occur (eventually) on the
* UI thread of the application, and may not appear immediately.
*
* @param newEdits A Map from activity name to a list of edits to apply
*/
// Must be thread-safe
public void setEdits(Map<String, List<ViewVisitor>> newEdits) {
// Delete images that are no longer needed
synchronized (mCurrentEdits) {
for (final EditBinding stale : mCurrentEdits) {
stale.kill();
}
mCurrentEdits.clear();
}
synchronized(mIntendedEdits) {
mIntendedEdits.clear();
mIntendedEdits.putAll(newEdits);
}
applyEditsOnUiThread();
}
遍历并调用stale.kill()方法,里面执行了一个handle.post(this)方法,然后我们找到EditState的run方法
@Override
public void run() {
if (!mAlive) {
return;
}
final View viewRoot = mViewRoot.get();
if (null == viewRoot || mDying) {
cleanUp();
return;
}
// ELSE View is alive and we are alive
mEdit.visit(viewRoot);
mHandler.removeCallbacks(this);
mHandler.postDelayed(this, 1000);
}
里面调用mEdit.visit(viewRoot),最终调用Pathfinder类的findTargetsInMatchedView方法里面就会实现上面说到的接口Accumulator的accumulate方法
if (remainingPath.isEmpty()) {
// Nothing left to match- we're found!
accumulator.accumulate(alreadyMatched);
return;
}
那么accumulate里面又是怎么实现的呢,接下去看
@Override
public void accumulate(View found) {
final View.AccessibilityDelegate realDelegate = getOldDelegate(found);//先获取到view的AccessibilityDelegate
if (realDelegate instanceof TrackingAccessibilityDelegate) {
final TrackingAccessibilityDelegate currentTracker = (TrackingAccessibilityDelegate) realDelegate;
if (currentTracker.willFireEvent(getEventName())) {
return; // Don't double track
}
}
// We aren't already in the tracking call chain of the view
final TrackingAccessibilityDelegate newDelegate = new TrackingAccessibilityDelegate(realDelegate);
found.setAccessibilityDelegate(newDelegate);//替换成自己的AccessibilityDelegate
mWatching.put(found, newDelegate);
}
@Override
protected String name() {
return getEventName() + " event when (" + mEventType + ")";
}
//反射拿到view的AccessibilityDelegate
private View.AccessibilityDelegate getOldDelegate(View v) {
View.AccessibilityDelegate ret = null;
try {
Class<?> klass = v.getClass();
Method m = klass.getMethod("getAccessibilityDelegate");
ret = (View.AccessibilityDelegate) m.invoke(v);
} catch (NoSuchMethodException e) {
// In this case, we just overwrite the original.
} catch (IllegalAccessException e) {
// In this case, we just overwrite the original.
} catch (InvocationTargetException e) {
MPLog.w(LOGTAG, "getAccessibilityDelegate threw an exception when called.", e);
}
return ret;
}
private class TrackingAccessibilityDelegate extends View.AccessibilityDelegate {
public TrackingAccessibilityDelegate(View.AccessibilityDelegate realDelegate) {
mRealDelegate = realDelegate;
}
public View.AccessibilityDelegate getRealDelegate() {
return mRealDelegate;
}
public boolean willFireEvent(final String eventName) {
if (getEventName() == eventName) {
return true;
} else if (mRealDelegate instanceof TrackingAccessibilityDelegate) {
return ((TrackingAccessibilityDelegate) mRealDelegate).willFireEvent(eventName);
} else {
return false;
}
}
public void removeFromDelegateChain(final TrackingAccessibilityDelegate other) {
if (mRealDelegate == other) {
mRealDelegate = other.getRealDelegate();
} else if (mRealDelegate instanceof TrackingAccessibilityDelegate) {
final TrackingAccessibilityDelegate child = (TrackingAccessibilityDelegate) mRealDelegate;
child.removeFromDelegateChain(other);
} else {
// We can't see any further down the chain, just return.
}
}
@Override
public void sendAccessibilityEvent(View host, int eventType) {
if (eventType == mEventType) {
fireEvent(host);//事件上传
}
if (null != mRealDelegate) {
mRealDelegate.sendAccessibilityEvent(host, eventType);
}
}
private View.AccessibilityDelegate mRealDelegate;
}
fireEvent处理我们的事件,里面实现了event的OnEventListener的OnEvent方法,最终实现在
DynamicEventTracker类
@Override
public void OnEvent(View v, String eventName, boolean debounce) {
// Will be called on the UI thread
final long moment = System.currentTimeMillis();
final JSONObject properties = new JSONObject();
try {
final String text = textPropertyFromView(v);
properties.put("$text", text);
properties.put("$from_binding", true);
// We may call track much later, but we'll be tracking something
// that happened right at moment.
properties.put("time", moment / 1000);
} catch (JSONException e) {
MPLog.e(LOGTAG, "Can't format properties from view due to JSON issue", e);
}
if (debounce) {
final Signature eventSignature = new Signature(v, eventName);
final UnsentEvent event = new UnsentEvent(eventName, properties, moment);
// No scheduling mTask without holding a lock on mDebouncedEvents,
// so that we don't have a rogue thread spinning away when no events
// are coming in.
synchronized (mDebouncedEvents) {
final boolean needsRestart = mDebouncedEvents.isEmpty();
mDebouncedEvents.put(eventSignature, event);
if (needsRestart) {
mHandler.postDelayed(mTask, DEBOUNCE_TIME_MILLIS);
}
}
} else {
mMixpanel.track(eventName, properties);//实现埋点
}
}
我们看到最终的埋点 mMixpanel.track(eventName, properties)