Client burying point is the most basic means of data collection, but due to the rapid business iteration, manual burying point scheme although flexible, but greatly increased the workload of client developers. Developing and completing business functions takes a lot of effort to deal with buried points, and as the number of buried points increases with iterations, maintenance of these old buried points also takes a lot of effort. In addition, the correctness of manual burying points is also an extreme test of the patience and seriousness of developers, it is inevitable that there will be such and such problems. So, being able to develop a functional SDK that can be embedded in different business scenarios with little or no developer involvement is definitely a valuable thing for improving version iteration speed and developer happiness. Android technology advanced QQ group 271165123

Even more valuable is that, without the intervention of developers, operational or user research students can adjust the data collection scheme dynamically at any time.

Throughout the current mature no-buried point scheme, there are the following problems:

Problem 1: It is theoretically feasible to use XPath to locate controls, but practice shows that the complexity of this solution is very high, especially for handling controls like GridView, ListView, and RecyclerView. Not only that, but the process of xpath generation itself is a very performance-intensive activity, traversing the View tree and storing a lot of path information on the view.

Question 2: corresponding data access control is through the data path way, each time adding a new point, if need to report the data, then researchers need and developers to use one by one to confirm the control of data path, which greatly limits the freedom of client development, even simple refactorings can make configuration buried some information before failure.

In view of the above problems, we have concluded a more flexible and reasonable no-buried point scheme after deeply digging the internal logical relationship and comparing the advantages and disadvantages. The following three parts introduce the implementation considerations and internal mechanism one by one.

1. Locate the target control that interacts with the user

We also considered xpath’s approach to locating interactive controls, but abandoned it due to the complexity, inflexibility, and potential problems of its implementation. By repeatedly reading the source code related to View touch event handling, we finally found a better way to solve the problem.

The ViewGroup has a variable of type TouchTarget, mFirstTouchTarget, which represents the list of controls that consume the current touch event. For example, if you click a button on the screen, the mFirstTouchTarget variable in the ViewGroup of the button points to that button. When a ViewGroup dispatches a touch event, it first determines whether the variable mFirstTouchTarget exists. If it does, it loops through the TouchTarget list elements, finds a View that can handle the event, and sends a MotionEvent to that View. If no TouchTarget exists, the ViewGroup loops through all child Views until it finds one that can handle the event and assigns that view to the mFirstTouchTarget as a First touch Target.

When the user fires a Down event, the following logic is executed to find the TouchTarget consuming the current event.

if(actionMasked == motionEvent.action_down){// If it is a down event, traverse the Child and find TouchTarget.. . final View[] children = mChildren;for(int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndexchildrenCount, i, customOrder); final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex); . .if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign) {// Child consumes the touch event.. . NewTouchTarget = addTouchTarget(Child, idBitsToAssign); . .break; }}Copy the code

When a Down event is emitted and the TouchTarget is found, or a non-Down event is emitted, the following processing logic is performed.

if (mFirstTouchTarget == null) {
    // No touch targets so treat this as an ordinary view.
    handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);
} else{// Find TouchTarget when Down events occur, or directly execute the following logic for non-Down events // Send events to the View represented by TouchTarget with TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget;while(target ! = null) { final TouchTarget next = target.next;if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted;

            if(dispatchTransformedTouchEvent(ev, CancelChild, target. The child, target. PointerIdBits)) {/ / specified TouchTarget corresponding View the event handled = correct consumptiontrue; }.. . }.. . }}Copy the code

Tip: Since there can be multiple controls consuming touch events, you need to traverse the TouchTarget linked list. To quote the official text: This behavior is enabled by default for applications that target an SDK version of 11 (Honeycomb) or newer. On earlier platform versions this feature was not supported and this method is a no-op.

MotionEvents may be split and dispatched to different child views depending on where each pointer initially went down. This allows for user interactions such as scrolling two panes of content independently, chording of buttons, and performing independent gestures on different pieces of content.

Using the event handling mechanism of the ViewGroup, we take over the window’s event dispatch by calling window.setcallback () on the Activity’s window, Add the analyzeMotionEvent() method to the dispatchTouchEvent handler. If an UP event is received, the processing logic is executed to find the target control for this interaction through the ViewGroup TouchTarget linked list. After getting the control, we can determine the unique identity of the target control by using the Activity class name + the layout name of the control where the control is located + the resource name corresponding to the control ID.

DispatchTouchEvent source code:

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if(! AutoPointer.isAutoPointEnable()) {return super.dispatchTouchEvent(ev);
        }

        int actionMasked = ev.getActionMasked();

        if(actionMasked ! = MotionEvent.ACTION_UP) {returnsuper.dispatchTouchEvent(ev); } long t = System.currentTimeMillis(); analyzeMotionEvent(); // For the offline version, print the execution timeif(! AutoPointer.isOnlineEnv()) { long time = System.currentTimeMillis() - t; DDLogger.d(TAG, String.format(Locale.CHINA,"Processing time :%d ms", time));
        }

        return super.dispatchTouchEvent(ev);
    }Copy the code

AnalyzeMotionEvent source code is as follows:

/** * analyze user click behavior */ private voidanalyzeMotionEvent() {
        if (mViewRef == null || mViewRef.get() == null) {
            DDLogger.e(TAG, "window is null");
            return;
        }

        ViewGroup decorView = (ViewGroup) mViewRef.get();
        int content_id = android.R.id.content;
        ViewGroup content = (ViewGroup) decorView.findViewById(content_id);
        if(content == null) { content = decorView; } Pair<View, Object> targets = findActionTargets(content);if (targets == null) {
            DDLogger.e(TAG, "has no action targets!!!");
            return; } // Send task in single thread pool int hashCode = target.first.hashcode ();if (mIgnoreViews.contains(hashcode)) return;

        PointerExecutor.getHandler().post(PointPostAction.create(targets.first, targets.second));
    }Copy the code

Obtain the business data corresponding to the target control

To obtain control data, we configure multiple data acquisition strategies in the system in order to maximize the acquisition speed. If the target control is the AbsListView or RecyclerView Child View and the ChID of the Child View, we can retrieve the desired data by the location of the Child View in the Adapter. This approach handles fetching data for most page controls. The system can configure policies as follows:

    private static Map<String, DataStrategy> mStrategies = new HashMap<>();

    static {
        //configure RecyclerView and subclass's search strategy DataStrategy recyclerViewStrategy = new RecyclerViewStrategy(); mStrategies.put("RecyclerView", recyclerViewStrategy); mStrategies.put("DDCollectionView", recyclerViewStrategy); //ExpandableListView DataStrategy EListViewStrategy = new ExpandableListViewStrategy(); mStrategies.put("ExpandableListView", EListViewStrategy); mStrategies.put("DDExpandableListView", EListViewStrategy); DataStrategy adapterViewStrategy = new AdapterViewStrategy(); //ListView mStrategies.put("ListView", adapterViewStrategy); mStrategies.put("DDListView", adapterViewStrategy); mStrategies.put("ListViewCompat", adapterViewStrategy); //GridView mStrategies.put("GridView", adapterViewStrategy); mStrategies.put("DDGridView", adapterViewStrategy); //ViewPager DataStrategy viewPagerStrategy = new ViewPagerStrategy(); mStrategies.put("ViewPager", viewPagerStrategy); //TabLayout DataStrategy tabLayoutStrategy = new TabLayoutStrategy(); mStrategies.put("TabLayout", tabLayoutStrategy); }Copy the code

For pages with completely custom layout drawing, such as personal-center pages, business developers need to establish a mapping relationship between the control tree and data through the framework API, so that when the framework needs to obtain data, it is very easy to obtain the data through this relationship.

/** * Configates the data binding relationship of the custom layout. When any * control in the custom layout clicks, the sent buried point will carry the changed data ** @param ID * @param object * @return
     */
    @NonNull
    @Override
    public DataConfigureImp configLayoutData(@IdRes int id, @NonNull Object object) {
        Preconditions.checkNotNull(object);

        mDataLayout.put(id, object);
        return this;
    }Copy the code

According to the TouchTarget to find the data acquisition strategy or data mapping relationship, we can obtain the bound data very simply, and the data acquisition algorithm is as follows:

        if(strategyView ! = null) { Object data = strategy.fetchTargetData(strategyView);return Pair.create(touchTarget, data);
        }

        if(configDataView ! = null) {returnPair.create(touchTarget, mDataLayout.get(configId)); } // Resolve data binding issues for custom layoutsif(dataAdapter ! = null) {return Pair.create(touchTarget, dataAdapter.getData());
        }Copy the code

Three, realize the dynamic configuration of buried points

In the test environment, user researchers will obtain the unique ID and data information of the control reported by the SDK through manual simulation click. After confirming the correctness of ID and data, they need to manually configure the corresponding relationship between ID and buried point events, as well as the reported data fields, and store them in the configuration warehouse. In the online environment, when the user starts the app, the configuration information is pulled and loaded into memory. In this way, when a user triggers a click, the system queries the configuration based on the ID obtained in the first step. If the corresponding entry is found in the configuration, the system reports the corresponding event and data to the server.

To process configuration drop-down failure unable to send the condition of the buried, we need to put additional qualifications to the configuration of the same assets directory, each app launched change request configuration interface to determine whether a configuration information, if the configuration does not change, direct use of assets in the configuration file, otherwise, the drop-down latest configuration, using the latest buried point configuration information.

4. Constraints of traceless burying point scheme on existing projects

There are certain development specifications that need to be followed to use the unburied SDK. See the Project README for specific development specifications. To ensure that the project code is standardized, we have developed a series of Lint checking rules to help find errors. Lint engineering code github.com/jessie345/C… Integrated Lint functionality github.com/jessie345/C…

Currently, integrating this no-buried solution has some usage constraints and requires the addition of some specific configuration functions in the main project. The next step is decoupling. Migrate all constraints as far as possible to dynamic technology guarantees via javasist technology, rather than via the Lint specification to keep them as invasive as possible.

At this point, the core operating mechanism of the no-buried SDK has been completely sorted out.