易观方舟

模块设计

graph TB
采集策略-->|控制触发|采集
采集-->自动采集-->全埋点监听touch-->|onGlobalLayout|hookDecorViewClick-->|mOnTouchListener|HookTouchListener-->trackEvent
采集-->手动采集-->trackEvent

上报策略-->|控制触发|上报
/** com/analysys/process/HeatMap.java
 * 反射给View注册监听
 */
private void hookViewClick(View view) throws Exception {

ans-sdk/analysys_core/src/main/java/com/analysys/AutomaticAcquisition.java

AutomaticAcquisition

onActivityCreated

@Override
public void onActivityCreated(final Activity activity, Bundle savedInstanceState) {
    AnalysysConfig config = AgentProcess.getInstance().getConfig();

    if (config.isAutoTrackClick()) {
        AnalysysUtil.onActivityCreated(activity);
    }

    if (config.isAutoHeatMap()) {
        initHeatMap(new WeakReference<>(activity));
    }
}

initHeatMap

private void initHeatMap(final WeakReference<Activity> wa) {
    layoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            if (wa == null) {
                return;
            }
            final Activity activity = wa.get();
            if (activity != null) {
                activity.getWindow().getDecorView().post(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            HeatMap.getInstance().hookDecorViewClick(activity.getWindow().getDecorView());
                        } catch (Throwable ignore) {
                            ExceptionUtil.exceptionThrow(ignore);
                        }
                    }
                });
            }
        }
    };
}

onActivityResumed

@Override
public void onActivityResumed(Activity activity) {
    if (AgentProcess.getInstance().getConfig().isAutoHeatMap()) {
        checkLayoutListener(new WeakReference<>(activity), true);
    }
}

checkLayoutListener

private void checkLayoutListener(WeakReference<Activity> wr, boolean isResume) {
    if (wr != null) {
        Activity activity = wr.get();
        if (layoutListener != null) {
            View rootView = activity.findViewById(android.R.id.content);
            if (isResume) {
                rootView.getViewTreeObserver().addOnGlobalLayoutListener(layoutListener);
            } else {
                if (Build.VERSION.SDK_INT > 15) {
                    rootView.getViewTreeObserver().removeOnGlobalLayoutListener(layoutListener);
                }
            }
        }
    }
}

ans-sdk/analysys_core/src/main/java/com/analysys/process/HeatMap.java

HeatMap

hookDecorViewClick

/***
 * 递归调用解析view
 * @param decorView 根节点view
 */
public void hookDecorViewClick(View decorView) throws Exception {
    if (decorView instanceof ViewGroup) {
        hookViewClick(decorView);
        int count = ((ViewGroup) decorView).getChildCount();
        for (int i = 0; i < count; i++) {
            if (((ViewGroup) decorView).getChildAt(i) instanceof ViewGroup) {
                hookDecorViewClick(((ViewGroup) decorView).getChildAt(i));
            } else {
                hookViewClick(((ViewGroup) decorView).getChildAt(i));
            }
        }
    } else {
        hookViewClick(decorView);
    }
}

hookViewClick

    /**
     * 反射给View注册监听
     */
    private void hookViewClick(View view) throws Exception {
        int visibility = view.getVisibility();
        if (visibility == 4 || visibility == 8) {
            return;
        }
        if (!view.getGlobalVisibleRect(new Rect())) {
            return;
        }
        Class viewClass = Class.forName("android.view.View");
        Method getListenerInfoMethod = viewClass.getDeclaredMethod("getListenerInfo");
        if (!getListenerInfoMethod.isAccessible()) {
            getListenerInfoMethod.setAccessible(true);
        }
        Object listenerInfoObject = getListenerInfoMethod.invoke(view);
        Class mListenerInfoClass = Class.forName("android.view.View$ListenerInfo");
        Field mOnClickListenerField = mListenerInfoClass.getDeclaredField("mOnTouchListener");

//        Log.d("sanbo", view.hashCode() + "-----" + HeatMap.HookTouchListener.class.getName() +
//        " <-------> " + mOnClickListenerField.getType().getName());

        mOnClickListenerField.setAccessible(true);
        Object touchListenerObj = mOnClickListenerField.get(listenerInfoObject);
        if (!(touchListenerObj instanceof HookTouchListener)) {
//            printLog(view, touchListenerObj);
            HookTouchListener touchListenerProxy =
                    new HookTouchListener((View.OnTouchListener) touchListenerObj);
            mOnClickListenerField.set(listenerInfoObject, touchListenerProxy);
        }

    }

HookTouchListener.onTouch

    private class HookTouchListener implements View.OnTouchListener {
        private View.OnTouchListener onTouchListener;

        private HookTouchListener(View.OnTouchListener onTouchListener) {
            this.onTouchListener = onTouchListener;
        }

        @Override
        public boolean onTouch(final View v, final MotionEvent event) {
            try {
                if (event.getAction() == MotionEvent.ACTION_DOWN) {
                        // 黑白名单判断
                        if (isTackHeatMap(v)) {
                            setCoordinate(v, event);
                        }
                    } catch (Throwable ignore) {
                        ExceptionUtil.exceptionThrow(ignore);
                    }
                }
                    return onTouchListener.onTouch(v, event);
            return false;
        }
    }

setCoordinate

private void setCoordinate(final View v, final MotionEvent event) {
    if (isTouch(rawX, rawY)) {
        AThreadPool.asyncLowPriorityExecutor(new Runnable() {
            @Override
            public void run() {
                    final String path = PathGeneral.getInstance().general(v);
                    boolean isAddPath = setPath(path);
                    if (isAddPath) {
                        rx = rawX;
                        ry = rawY;
                        setClickCoordinate();
                        setClickContent(v);
                        clickInfo.putAll(pageInfo);
                        AgentProcess.getInstance().pageTouchInfo(clickInfo,currentTime);
                    }
            }
        });
    }
}

pagetouchinfo

com/analysys/allgro/plugin/ASMProbeHelp.java

ASMProbeHelp.java

trackViewOnClick

    public void trackViewOnClick(final View v, final boolean hasTrackClickAnn) {
        try {
            final long currentTime = System.currentTimeMillis();
            AThreadPool.asyncLowPriorityExecutor(new Runnable() {
                @Override
                public void run() {
                    try {
                        if (isInLimitTime(v)) {
                            return;
                        }
                        for (ASMHookAdapter observer : mObservers) {
                            observer.trackViewOnClick(v, hasTrackClickAnn,currentTime);
                        }
                    } catch (Throwable ignore) {
                        ExceptionUtil.exceptionThrow(ignore);
                    }
                }
            });
        }catch (Throwable ignore){
            ExceptionUtil.exceptionThrow(ignore);
        }
    }

com/analysys/allgro/plugin/ViewClickProbe.java

ViewClickProbe extends ASMHookAdapter

trackViewOnClick

@Override
public void trackViewOnClick(View v, boolean hasTrackClickAnn,long currentTime) {
    try {
        Map<String, Object> viewInfo = new HashMap<>();
        String[] viewTypeAndText = AllegroUtils.getViewTypeAndText(v);
        viewInfo.put(Constants.ELEMENT_TYPE, viewTypeAndText[0]);
        viewInfo.put(Constants.ELEMENT_CONTENT, viewTypeAndText[1]);
        String path = PathGeneral.getInstance().general(v);
        viewInfo.put(Constants.ELEMENT_PATH,path);

        String idName = AllegroUtils.getViewIdResourceName(v);
        if (!TextUtils.isEmpty(idName)) {
            viewInfo.put(Constants.ELEMENT_ID, idName);
        }

        autoTrackClick(pageObj, viewInfo, hasTrackClickAnn,currentTime);
    } catch (Throwable ignore) {
        ExceptionUtil.exceptionThrow(ignore);
    }
}

autoTrackClick

    private void autoTrackClick(Object pageObj, Map<String, Object> elementInfo, boolean hasTrackClickAnn,long currentTime) throws Throwable {
        if (pageObj != null) {
            // 获取页面相关信息
            elementInfo.putAll(AllegroUtils.getPageInfo(pageObj,true));

//            if(elementInfo!=null&&elementInfo.containsKey(Constants.PARENT_URL)){
                // 去除此字段
//                elementInfo.remove(Constants.PARENT_URL);
//            }

        }
        AgentProcess.getInstance().autoTrackViewClick(elementInfo,currentTime);
    }

com/analysys/process/AgentProcess.java

AgentProcess.java

pageTouchInfo

/**
 * Touch事件处理
 */
void pageTouchInfo(final Map<String, Object> screenDetail,final long currentTime) {
    try {
        Context context = AnalysysUtil.getContext();
        Map<String, Object> screenInfo = CommonUtils.deepCopy(screenDetail);
        if (context != null) {
            JSONObject eventData = DataAssemble.getInstance(context).getEventData(
                    currentTime,Constants.API_APP_CLICK, Constants.APP_CLICK, null, screenInfo);
            trackEvent(context, Constants.API_APP_CLICK, Constants.APP_CLICK, eventData);
        }
    } catch (Throwable ignore) {
        ExceptionUtil.exceptionThrow(ignore);
    }
}

autoTrackViewClick

/**
 * 点击自动上报
 *
 * @param clickInfo 点击上报信息
 */
public void autoTrackViewClick(final Map<String, Object> clickInfo,final long currentTime) throws Throwable {
    Context context = AnalysysUtil.getContext();
    if (context != null) {
        JSONObject eventData = DataAssemble.getInstance(context).getEventData(currentTime,Constants.API_USER_CLICK, Constants.USER_CLICK, null, clickInfo);
        trackEvent(context, Constants.API_USER_CLICK, Constants.USER_CLICK, eventData);
    }
}

trackEvent

/**
 * 接口数据处理
 */
private void trackEvent(Context context, String apiName,
                        String eventName, JSONObject eventData) {
    if (!CommonUtils.isEmpty(eventName) && checkoutEvent(eventData)) {
        // 此处重置重传传次数,解决处于重传状态时,触发新事件重传次数不够三次
        //SharedUtil.remove(mContext, Constants.SP_FAILURE_COUNT);
        if (LogBean.getCode() == Constants.CODE_SUCCESS) {
            LogPrompt.showLog(apiName, true);
        }
        EasytouchProcess.getInstance().setEventMessage(eventData.toString());
        if (mEventObserver != null) {
            mEventObserver.onEvent(eventName, eventData);
        }
        UploadManager.getInstance(context).sendManager(eventName, eventData);
    }
}

com/analysys/network/UploadManager.java

UploadManager.java

sendManager

/**
 * 判断 发送数据
 */
public void sendManager(String type, JSONObject sendData) {
    if (CommonUtils.isEmpty(sendData)) {
        return;
    }
    dbCacheCheck();
    TableAllInfo.getInstance(mContext).insert(sendData.toString(), type);
    if (CommonUtils.isMainProcess(mContext)) {
        BaseSendStatus sendStatus = PolicyManager.getPolicyType(mContext);
        if (sendStatus.isSend(mContext)) {
            sendUploadMessage();
        }
    } else {
        LogPrompt.processFailed();
    }
}

sendUploadMessage

/**
 * 发送实时消息
 */
private void sendUploadMessage() {
    if (mHandler.hasMessages(delayUploadData)) {
        mHandler.removeMessages(uploadData);
    }
    Message msg = Message.obtain();
    msg.what = uploadData;
    mHandler.sendMessage(msg);

}

SendHandler.handleMessage

/**
 * 处理数据压缩,上传和返回值解析
 */
private class SendHandler extends Handler {

    private SendHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        try {
            String url = CommonUtils.getUrl(mContext);
            if (!CommonUtils.isEmpty(url)) {
                int what = msg.what;
                if (what == uploadData || what == delayUploadData) {
                    uploadData(url);
                } else if (what == updateTime) {
                    calibrationTime(url);
                }
            } else {
                LogPrompt.showErrLog(LogPrompt.URL_ERR);
            }
        } catch (Throwable ignore) {
            ExceptionUtil.exceptionThrow(ignore);
        }
    }
}

uploadData

/**
 * 数据上传
 */
private void uploadData(String url) throws IOException, JSONException {
  
          if (NetworkUtils.isNetworkAvailable(mContext)) {
            JSONArray eventArray = TableAllInfo.getInstance(mContext).select();
            // 上传数据检查校验
            eventArray = checkUploadData(eventArray);
            if (!CommonUtils.isEmpty(eventArray)) {
                LogPrompt.showSendMessage(url, eventArray);
                encryptData(url, String.valueOf(eventArray));
            } else {
                TableAllInfo.getInstance(mContext).deleteData();
            }
        } else {
            LogPrompt.networkErr();
        }
}

encryptData

/**
 * 数据加密
 */
private void encryptData(String url, String value) throws IOException {
    if (CommonUtils.isEmpty(spv)) {
        spv = CommonUtils.getSpvInfo(mContext);
    }
    Map<String, String> headInfo = null;
    String encryptData;
    if (Constants.encryptType != 0) {
        encryptData = encrypt(value, Constants.encryptType);
        if (!TextUtils.isEmpty(encryptData)) {
            headInfo = getHeadInfo();
            if (!CommonUtils.isEmpty(headInfo)) {
                LogPrompt.encryptLog(true);
            } else {
                encryptData = value;
            }
        } else {
            encryptData = value;
        }
    } else {
        encryptData = value;
    }
    String zipData = CommonUtils.messageZip(encryptData);
    sendRequest(url, zipData, headInfo);
}

sendRequest

/**
 * 发送数据
 */
private void sendRequest(String url, String dataInfo, Map<String, String> headInfo) {
    try {
        String returnInfo;
        if (url.startsWith(Constants.HTTP)) {
            returnInfo = RequestUtils.postRequest(url, dataInfo, spv, headInfo);
        } else {
            returnInfo = RequestUtils.postRequestHttps(mContext, url, dataInfo, spv, headInfo);
        }
        policyAnalysys(analysysStrategy(returnInfo));
    } catch (Throwable ignore) {
        ExceptionUtil.exceptionThrow(ignore);
    }
}

参考

https://docs.analysys.cn/integration/sdk/android/andoird-quick

https://github.com/analysys/ans-android-sdk