This is the 19th day of my participation in the August More Text Challenge
Why the burial point
In our usual App development process, with the continuous increase of business needs, in order to analyze user behavior, we need to carry out burying points in App. Burying points can solve two kinds of problems:
- Understanding user usage
App
Behavior; - Reduce the difficulty of analyzing online problems.
Classification of buried sites
At present, the common buried points in iOS development mainly include code buried points, visual buried points and no buried points these three.
- Code buried: mainly by means of writing code to pay, can be very precise in points need to be buried at point code plus the code, you can easily record the current environment variable values, convenient debugging, and tracking point content, but there exists large development effort, and buried the point code is everywhere, in the late problems such as difficult to maintain.
- Visualization of buried points: The work of adding and modifying buried points is visualized, improving the experience of adding and maintaining buried points.
- No buried points: Not that there are no buried points, but rather “full buried points”, and buried points do not appear in business code and are easy to manage and maintain. Its disadvantages lie in that the cost of burying the point is high, the later analysis is also more complex, plus
view_path
The uncertainty of. Therefore, this solution can not solve all buried point requirements, but for a large number of common buried point requirements, can save a lot of development and maintenance costs.
Among the three schemes, visual buried point and no-buried point are non-intrusive buried point schemes. Because none of them require burying code in the project code. Therefore, adopting such non-invasive burying point scheme can not only achieve unified maintenance of burying point, but also realize decoupling from engineering code.
Hook buried point
Our most common buried point is the number of page entry, page stay time, page click events buried; So in order to achieve non-intrusive burial point, we can use the Runtime technology;
We create a class PVHook for method exchange, + (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector :(SEL)toSelector :(SEL)toSelector:
#import "PVHook.h"
#import <objc/runtime.h>
@implementation PVHook
+ (void)hookClass:(Class)classObject fromSelector:(SEL)fromSelector toSelector:(SEL)toSelector {
Class class = classObject;
// Get the instance method of the replaced class
Method fromMethod = class_getInstanceMethod(class, fromSelector);
// Get the instance method of the replacement class
Method toMethod = class_getInstanceMethod(class, toSelector);
// class_addMethod returns success to indicate that the replaced method is not implemented, and is implemented first through the class_addMethod method. Return failure means that the method has been replaced, can be directly IMP pointer exchange
if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
// Replace the method
class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
} else {
// Swap IMP Pointersmethod_exchangeImplementations(fromMethod, toMethod); }}@end
Copy the code
It uses the runtime mechanism to exchange the implementation of the method. When the original method is called, it will be hook, so as to execute the method after the exchange.
UIViewController life cycle buried point
We use page entry times and dwell time as examples to illustrate burying points. To achieve these two burying points, we need to bury the lifecycle of UIViewController;
We can create a UIViewController class to operate on to isolate business code:
@implementation UIViewController (Log)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ // Make sure to swap only once
/ / get replaced by @ the selector and substitution method of SEL, as PVHook: hookClass: fromeSelector: toSelector parameters passed in
SEL fromSelectorAppear = @selector(viewWillAppear:);
SEL toSelectorAppear = @selector(hook_viewWillAppear:);
[PVHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
SEL fromSelectorDisappear = @selector(viewWillDisappear:);
SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
[PVHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
});
}
- (void)hook_viewWillAppear:(BOOL)animated {
// Execute the insert code first, then the original viewWillAppear method
[self insertToViewWillAppear];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
// Execute the insert code and then execute the viewWillDisappear method
[self insertToViewWillDisappear];
[self hook_viewWillDisappear:animated];
}
- (void)insertToViewWillAppear {
// The burial point for logging in ViewWillAppear
}
- (void)insertToViewWillDisappear {
// buried point to log when ViewWillDisappear
}
@end
Copy the code
- In classification, we are in
load
Method.PVHook
Method swap, inserted in the swapped methodinsertToViewWillAppear
Buried point in the method; In that case, every single one of themUIViewController
Life cycle toviewWillAppear
Will be executedinsertToViewWillAppear
Methods The buried point operation was carried out. - It can be passed at the burial point
NSStringFromClass([self class])
Method to get the name of the class that distinguishes the currentUIViewController
;
Residence time can be calculated from buried point time of two life cycle methods.
UIButton’s click event burial point
The most important thing is to find the sendAction:to:forEvent: method and then replace it in the load method. This code looks like this:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/ / get replaced by @ the selector and substitution method of SEL, as PVHook: hookClass: fromeSelector: toSelector parameters passed in
SEL fromSelector = @selector(sendAction:to:forEvent:);
SEL toSelector = @selector(hook_sendAction:to:forEvent:);
[PVHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
});
}
- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self insertToSendAction:action to:target forEvent:event];
[self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
// Log
if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
// Get the action selector name
NSString *actionString = NSStringFromSelector(action);
// Get target, view name
NSString *targetName = NSStringFromClass([target class]);
// Log}}Copy the code
- and
UIViewController
The life cycle of buried point operation is different from oursUIButton
More than one may have been created in the viewUIButton
May exist in the current viewUIButton
The differentA derived class
We need to distinguish these, so we need to go throughactionString
andtargetName
Combined together into a unique identifier, to distinguish buried sites;
With the exception of UIViewController and UIButton controls, which we discussed above, most of the other controls in the Cocoa framework work this way. For example, with the UITableView control we often use, we can hook the setter method of its delegate, the setDelegate method, and then swap methods to implement buried points; Gesture events can be buried via the hook method initWithTarget: Action.
Unique identifier of a buried event
By swapping methods at runtime, we can hook into almost any Objective-C method that, under normal circumstances, meets our daily burying needs.
However, the granularity of this solution is not high enough, and in some cases it still does not meet the requirements; For example, the same UIButton instances button1 and Button2 in a view are indistinct just by the way the selector name and the view class name form a unique identifier, as described earlier in actionString and targetName. So at this point, we need a real event unique identifier to distinguish;
So if I make this unique identifier? The first thing we might think of is the view tree hierarchy to solve this problem, because every interface has a view tree structure, and we can restore the view tree of each interface through properties such as superView and subViews.
The top layer of the view tree is UIWindow, and every subsequent view is on the child node of the tree. The structure is as follows:
The child nodes of a view may be different instances of the same view. For example, the two UIButtons under the UIView view node in the figure above are different instances of the same class. Therefore, the unique identification of a view cannot be determined only by the path of the view tree.
Each child view has its own index in the parent view, so if we add this index, we can determine the unique identity of each view? Would a view-level path with a unique identifier determined by an index in the parent view cover all cases?
Unique identifier of UITableViewCell
Of course, we can’t cover every case, but we also need to think about UITableViewCell, which is reusable, which is reusable as you scroll across the page, so indexing is basically invalid in this case;
So at this point in the analysis, is there no solution to the problem? UITableViewCell requires indexPath, which contains both section and row values, so we can use indexPath to determine the uniqueness of each Cell;
UIAlertController unique identifier
In addition, UIAlertController is something special, and what makes it special is that the view level is not fixed; Because we can pop up UIAlertController anywhere, it could be on any interface in our App. But UIAlertController’s function is usually distinguished by the contents of the popover, so we can determine its unique identity by the contents;
Besides, may we need to have a lot of other special case, but we can pass some other methods to identify their unique identifier to, general idea is: by identifying elements between different factors, and then are combined, finally can form a can distinguish it from other elements of the logo;
Unsolvable buried spot situation
In addition to the special case of concentration mentioned above, there is another case we need to pay special attention to; If the hierarchy of a view changes at runtime, such as insertSubView:atIndex: or removeFromSuperview at runtime, it becomes difficult to get an accurate unique identity;
Even though the hierarchy of the view is not modified at runtime, the interface will be updated frequently with the continuous iteration of requirements, and the unique identity of the view also needs to be updated and maintained synchronously. In this case, since the accuracy of the unique identification of the event is difficult to be guaranteed, it is difficult to choose the appropriate solution; This is why non-intrusive burial point solutions through run-time substitution are difficult to cover across companies; Although the non-intrusive burying point cannot solve all problems and still faces many challenges, its solution can solve the purchase requirements in most cases and save a lot of labor costs to a certain extent.
conclusion
The scheme of non-intrusive burial site is difficult to be fully adopted because of the difficulty in maintaining the unique identification of events and ensuring its accuracy. For the most part, we still prefer manual intrusive burials where functionality and views are stable; However, we can greatly reduce the intrusion range of buried sites by combining non-intrusive burial sites with intrusive burial sites.