This paper mainly discusses the traceless operation of the traceless buried point, which does not involve burying point storage, uploading and other problems. It is more suitable for the project using Umeng and similar partners, project address.
Requirements and solution analysis
When making a project, under normal circumstances, there will be a need to bury the user behavior, most companies will use Umeng or similar schemes to bury the code, call relevant methods to record where the burying point is needed, and report it when appropriate. The specific record and report are determined by SDK. We just need to worry about putting code of the following type in place:
public static void onEvent(Context context, String eventID);
public static void onEvent(Context context, String eventID, String label);
Copy the code
The simple way is to manually add the embedded point operation everywhere in the code, manual embedded point although more flexible and simple, but there will be the following problems:
- The buried code and the business code are so tightly coupled that you may need to add buried points everywhere you click, requiring a lot of repetition and not being elegant.
- Burying points are error-prone and difficult to maintain. There may be burying points for adding, deleting or changing in each version. Generally, we have to face an Excel table.
- Buried points cannot be added or modified once they are online.
For the above problems, we are looking for solutions one by one:
-
Code coupling issues:
- By setting the AccessibilityDelegate or directly reflect replace click events.
- By means of AOP, for events, methods, etc., which need burying point, plug in the code related to burying point.
- Replace the control directly with our own control, burying points inside the control
-
Maintenance issues:
We generate a unique identifier for the controls that need to be buried, such as: Activity+ layer layout ClassName+ ID /index. A set of mapping relationship is established between ID and buried point content. When burying point needs to be triggered, the content is obtained according to the identification and buried point is carried out.
-
Dynamic modification:
This is relatively simple. In the scheme of 2, the mapping relationship can be directly sent to the APP, and the concept of version can be added. After all, the burying point itself will not be changed frequently.
A concrete implementation of code coupling problems
-
View.AccessibilityDelegate
The Android system provides the AccessibilityDelegate to help vision, physical disabilities, such as the user to use Android devices.
** view.performClick ()**
public boolean performClick(a) { final boolean result; final ListenerInfo li = mListenerInfo; if(li ! =null&& li.mOnClickListener ! =null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; } Copy the code
After clicking, sendAccessibilityEvent(Accessibilityevent.type_view_clicked) is called;
public void sendAccessibilityEvent(int eventType) { if(mAccessibilityDelegate ! =null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else{ sendAccessibilityEventInternal(eventType); }}Copy the code
Finally invokes AccessibilityDelegate. SendAccessibilityEvent (View host, int eventType) method. We customize the AccessibilityDelegate, embedding points in the related methods.
We can in the Application of the onCreate () method, through the Application. The registerActivityLifecycleCallback to monitor all of the Activity lifecycle, iterate through all the controls, Set AccessibilityDelegate and give RootView registered ViewTreeObserver. OnGlobalLayoutListener listener, in the layout change, reset AccessibilityDelegate traverse control.
This scheme has two drawbacks: 1) it cannot bury Dialog and PopupWindow, and 2) it requires multiple traversal of the control, which affects performance
-
reflection
The ListenerInfo class is used to store all events in the ListenerInfo class. We can write a view. OnClickListener proxy class. View.hasonclicklisteners () determine whether there are clicklisteners and replace them directly. The specific code is as follows:
Public void hookListener(View View) {// 1. Reflection calls the View's getListenerInfo method (API>=14) and gets the mListenerInfo object Class viewClazz = class.forName ("android.view.View"); Method getListenerInfoMethod = viewClazz.getDeclaredMethod("getListenerInfo"); if(! getListenerInfoMethod.isAccessible()) { getListenerInfoMethod.setAccessible(true); } Object mListenerInfo = listenerInfoMethod.invoke(view); Class listenerInfoClazz = class.forname ("android.view.View$ListenerInfo"); Field onClickListenerField = listenerInfoClazz.getDeclaredField("mOnClickListener"); if(! onClickListenerField.isAccessible()) { onClickListenerField.setAccessible(true); } View.OnClickListener mOnClickListener = (View.OnClickListener) onClickListenerField.get(mListenerInfo); OnClickListenerProxy = new OnClickListenerProxy(mOnClickListener); OnClickListenerProxy onClickListenerField.set(mListenerInfo, mOnClickListener) mOnClickListenerProxy); View. SetOnClickListener (mOnClickListenerProxy); }Copy the code
This approach also has obvious disadvantages, first still need to loop all controls, second, when to replace, there is such a situation: Third, there are controls that trigger the click event through performClick() instead of OnClickListener, such as TabLayout.
-
AOP through the way to insert buried code, here mainly introduces Aspectj in Android use
The first thing you need to do is configure, which is a lot of configuration to use AspectJ originally. There is also an easy way to use AspectJX directly from Hujiang. See the project description for details.
For AspectJ usage, check out some resources on the web. This article is well written.
Aspectj has a lot of sophisticated ways to write, but it’s all about writing and experimenting. Here I’ve put a buried point for the click event
@aspect Public Class ViewCore extends BaseCore {/** * this is the Pointcut of a custom annotation. If {@link Event} is added to the method, it is a Pointcut."execution(@com.warm.someaop.annotation.Event * *(..) )") public void method() { } /** * {@link android.view.View.OnClickListenerTangent point # onClick (View)}@param view / @pointcut (value =)"(execution(* android.view.View.OnClickListener.onClick(android.view.View))&&args(view))||(execution(void *.. lambda*(android.view.View))&&args(view))"Public void onClick(View View) {} public void onClick(View View) {} * @param joinPoint * @Param View * @param obj * @throws Throwable */ @after ("onClick(view)&&! method()&&this(obj)") public void injectOnClick(JoinPoint joinPoint, View view,Object obj) throws Throwable { Trace trace = Data.getEvent(getName(joinPoint, view)); if(trace ! = null) { track(trace.getId(), trace.getValue()); } } private String getName(JoinPoint joinPoint, View view) { StringBuilder sb = new StringBuilder(); sb.append(getViewName(view)) .append("$") .append(getClassName(joinPoint.getTarget().getClass())) .append("$") .append(getClassName(view.getContext().getClass())); String md5 = Utils.toMD5(sb.toString()); if (BuildConfig.DEBUG) { Log.d(TAG, "getName: " + sb.toString() + ",MD5: " + md5); } returnmd5; }}Copy the code
We define a Data management class. Data in this project simply simulates Data storage and acquisition. All buried points are stored in the Application class, which should be directly delivered by the network in the actual project. When we intercept the click event, we get the buried information from the data management class. This scheme also has some problems: it is mainly for some lambda expressions and does not intercept well, for example: This ::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick: this::onClick:
@Pointcut("call(* android.view.View.setOnClickListener(android.view.View.OnClickListener))&&args(clickListener)") public void setOnClickListener(View.OnClickListener clickListener) {}@Around("setOnClickListener(clickListener)") public Object injectSetOnClickListener(ProceedingJoinPoint joinPoint, View.OnClickListener clickListener) throws Throwable { return joinPoint.proceed(new Object[]{new OnClickListenerProxy(clickListener)}); } Copy the code
-
Using the Gradle plugin, we can change the parent class of the control directly to our own control
The idea of this scheme comes from a blog of Meituan, and I have practiced and supplemented it. This is a relatively perfect scheme in my opinion.
We can customize some common controls, all controls are directly inherited from the custom controls, in our own controls for buried operations, so that more events can be intercepted, stable and reliable, such as the following TTextView:
public class TTextView extends TextView { public TTextView(Context context) { super(context); } public TTextView(Context context, AttributeSet attrs) { super(context, attrs); } public TTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override public boolean performClick(a) { boolean click = super.performClick(); Track.getTrack().getViewTracker().performClick(this); returnclick; }}Copy the code
We buried the dot in performClick(), and any click event must go to this method, so we don’t have to worry about OnClickListener
The current development basically adds Support packages and inherits AppCompatActivity. Controls we use in XML can automatically be converted to Appcompat** controls. The V7 package can convert to AppCompatViewInflater. We can look at the AppCompatDelegateImpl#createView method:
@Override public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) { if (mAppCompatViewInflater == null) { TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme); String viewInflaterClassName = a.getString(R.styleable.AppCompatTheme_viewInflaterClass); if ((viewInflaterClassName == null) || AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) { // Either default class name or set explicitly to null. In both cases // create the base inflater (no reflection) mAppCompatViewInflater = new AppCompatViewInflater(); } else { try { Class viewInflaterClass = Class.forName(viewInflaterClassName); mAppCompatViewInflater = (AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor() .newInstance(); } catch (Throwable t) { Log.i(TAG, "Failed to instantiate custom view inflater " + viewInflaterClassName + ". Falling back to default.", t); mAppCompatViewInflater = new AppCompatViewInflater(); } } } boolean inheritContext = false; if (IS_PRE_LOLLIPOP) { inheritContext = (attrs instanceof XmlPullParser) // If we have a XmlPullParser, we can detect where we are in the layout ? ((XmlPullParser) attrs).getDepth() > 1 // Otherwise we have to use the old heuristic : shouldInheritContext((ViewParent) parent); } return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */ true, /* Read read app:theme as a fallback at all times for legacy reasons */ VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */ ); } Copy the code
We know that we can set appappviewinflater in the Theme and we can customize TAppCompatViewInflater and replace it with our own controls. But the controls in the third party library, our own custom controls, will be invalidated, so we need to write a plug-in that, at compile time, will print the converted jar package path,
Change the parent class of the control to our custom control. We can view the modified code, the generated path is: app/build/intermediates/transforms/TrackTransform/debug, use the JD – GUI to open, we can see:
I wrote an extension for the plugin to exclude packages that do not need to be converted:
track { excludes = [ "retrofit2"."com/umeng/"]}Copy the code
This way, you can convert all the controls into custom controls, but there are still some drawbacks. For example, we often need to add controls dynamically to our code. We can call the addView() method to pass in the controls we want to add. We might exist new LinearLayout () or new Button () this kind of situation, we still can’t, but we can combine the first solution, in ViewGroup. When setting the addView AccessibilityDelegate, This covers all the cases
@Override public void onViewAdded(View child) { super.onViewAdded(child); AccessibilityDelegateHelper.onViewAdded(child); } Copy the code
A concrete implementation of a unique identity and mapping relationship
The rules of unique identification are basically to design a ViewPath, such as Activity+ layer layout ID, you can see the article 51 credit card, netease HubbleData, the article is also very clear, mainly id and idEntryName selection, The optimization of index and the optimization of special controls, the online articles are basically the same, everyone’s plan is similar, this project is the same. However, in the generation of ViewPath this problem, we need to generate according to the actual situation of the project, in line with the project ViewPath, the following two common situations are introduced:
-
For the use of buried point upload mechanism similar to friends (the main case for this article), the generated ViewPath needs to be configured in the background and delivered to the APP. In view of this situation, layout index is generally do not need to be added to the ViewPath, because the scheme is for fixed points, those need to dynamically add view or ListView, RecyclerView point, the specific item is how many, actually do not know the background. We can’t configure 100 and 1000 buried points. For the parent layout, we need to add index to the child control and manually add a tag or create a set of mappings to map the parent layout to distinguish between them. Of course, we can also do special processing for special controls, depending on the actual situation of the project.
-
Buried there are some points to upload solution is to upload all controls click events background, from the background to get effective burial site, the plan to join the index is necessary, after all the background can control which needs to be the index, which don’t need to APP to upload the information of point line, and also is the point of the buried solution, such solution faults mainly waste flow, Not very targeted.
No matter what kind of solution, the traceless burial point needs more maintenance points, every time the page is modified, it may need to maintain the burial point in the background, such work can be assigned to the product, test, operation of them.
The last
No matter how “no trace”, for the user are simple fixed events, such events may account for up to 60% or 70%, and serious business coupling buried points, still need to carry out manual code buried points. In the selection of embedding technology, it is not necessary to be traceless, but to pay more attention to the actual situation of the project and choose the appropriate scheme.