Launch and exit of Shence analysis IOS SDK full buried point analysis


1、 Foreword

The last “Shence analysis IOS SDK code embedding point analysis” mainly introduced how to design and implement code embedding points. Specifically, it implements a – Track: interface, which can be called at the right time to record a user’s behavior data. In general, for different apps, valuable behavior data are different, and the timing of calling – Track: interface is naturally different. Developers need to call it manually according to business scenarios.
For apps, some specific and meaningful user behaviors can be collected directly in the SDK. For example: app startup, APP exit, element click, page browsing, etc. In order to distinguish it from the code buried point, we call it full buried point (also known as no buried point, no code buried point, no trace buried point and automatic buried point).
It is not difficult to see that the full buried point mainly faces two difficulties:
1. Timing: how to insert the code to collect the event when the event occurs?
2. Attribute: can I collect other meaningful preset attributes besides the preset attributes collected by default? And how to add custom attributes to these events?
The next full buried Point Analysis series blog is mainly to solve the above two difficulties. This paper mainly discusses the collection of APP startup and exit events.

2、 Application status

Before discussing the collection of APP startup and exit events, we should first understand the meaning of these two events. Here we need to introduce several running states of app:

typedef NS_ENUM(NSInteger, UIApplicationState) { UIApplicationStateActive, UIApplicationStateInactive, UIApplicationStateBackground} API_AVAILABLE(ios(4.0));

Several possible statuses of APP during execution:
1. Active: the program is running in foregroup and is receiving events;
2. Inactive: the program runs in foregroup but does not receive events. This may be caused by the following reasons: interruption (such as incoming phone or SMS message), application is transitioning to the background, and application is transitioning from the background;
3. Background: the program is running in background and executing code.
In addition, app will have two states without executing code:
1. Not running: the program is not running. If the app has not been started for the first time, the app has been killed, and the app has not been run after the mobile phone is restarted, it will be in this state;
2. Suspended: the program runs in the background, but does not execute the code and is in the suspended state. When most applications enter the background, they will be switched to the suspended state by the system in a short time.
These five states are all the running states of the app, as shown in Figure 2-1:

Launch and exit of Shence analysis IOS SDK full buried point analysis
Figure 2-1 app running status (picture from Apple Developer’s official website)
When the running state of the application changes, it will call back the protocol method in uiapplicationdelegate, which is implemented by appdelegate by default, as shown in table 2-1:
Launch and exit of Shence analysis IOS SDK full buried point analysis
Table 2-1 protocol method in uiapplicationdelegate
It should be noted here that not every state change has a corresponding method. For example, there is no corresponding method for the two changes in the red box in Figure 2-1.
For the collection of APP startup and exit events, we should find ideas in these methods and notifications. The following are the common scenarios of running state changes:
1. Cold start, i.e. start after kill app, or start for the first time after app installation (not running – > inactive – > active);
2. App returns to the home screen (active – > inactive – > background – > suspended). If in info If application does not run in background is set to yes in plist, the app will be killed immediately after returning to the main screen (active – > inactive – > background – > suspended – > not running);
3. Enter the app switcher in the app, and then directly return to the app (active – > inactive – > active);
4. Enter the app switcher in the app, and then enter the main screen (active – > inactive – > background – > suspended);
5. Enter app switcher in app, and then kill app (active – > inactive – > background – > suspended – > not running);
6. App runs again in suspended state, i.e. hot start (suspended – > background – > inactive – > active);
7. When the app is suspended, kill the app or directly delete the app (suspended – > not running).

3、 App launch

App startup refers to application startup, including cold startup and hot startup scenarios. Cold start and hot start will involve different app application status methods, so the acquisition methods are also different, which will be discussed separately below.

3.1 cold start

3.1.1 acquisition scheme

Cold start, i.e. start after kill app, or start for the first time after app installation. The acquisition method is as follows:

  • Apptypesensoreventstart {(apptypesensoreventreturn) / (apptypestart: {ignore event) / (enable)// Since a complete application life cycle will only trigger a cold start, add dispatch_ Once to prevent multiple triggers. static dispatch_ once_ t onceToken; dispatch_ Once (^ oncetoken, ^ {/ / whether to start for the first time is recorded in sa_event_property_app_first_start. The tag is saved in nsuserdefaults. Bool isfirststart = no; if (! [[nsuserdefaults standarduserdefaults] boolforkey: sa_has_launched_once]) {isfirststart = yes; [[nsuserdefaults standarduserdefaults] setbool: Yes forkey: sa_has_launched_once];}// Judge whether it is passive start. Passive start will be recorded in app passive start event. App passive start will be discussed in detail later. NSString *eventName = [self isLaunchedPassively] ? SA_ EVENT_ NAME_ APP_ START_ PASSIVELY : SA_ EVENT_ NAME_ APP_ START; // SA_ EVENT_ PROPERTY_ RESUME_ FROM_ Backgroup: whether the app is restored from the background. To distinguish between hot and cold start. NSDictionary *properties = @{SA_EVENT_PROPERTY_RESUME_FROM_BACKGROUND: @(NO), SA_EVENT_PROPERTY_APP_FIRST_START: @(isFirstStart)}; [self track:eventName withProperties:properties withTrackType:SensorsAnalyticsTrackTypeAuto]; // Start the $append event timer [self tracktimerstart: sa_event_name_app_end];});}

The next problem to be solved is the acquisition timing. From the above description of APP application status, we know that during the cold start process, app will go through the process of not running – > inactive – > active, that is, execute the following two methods:

  • application:didFinishLaunchingWithOptions:
  • applicationDidBecomeActive:
    Since – applicationdidbecomeactive: will be called in multiple scenarios, and – Application: didfinishlaunchingwithoptions: will only be called during cold start, select to collect cold start events in – Application: didfinishlaunchingwithoptions: method. The code is as follows:
  • (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { [self autoTrackAppStart]; return YES;}

3.1.2 scheme optimization

Since we design SDK, the code should achieve the goal of high cohesion and low coupling. Therefore, some modifications need to be made to the above code:
1. – autotrackappstart should be the method of SDK, which will depend on the initialization of SDK;
2. SDK has some setting methods that should be set before collecting the first event, such as setting public properties.
Therefore, the code is modified as follows:

  • (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )Launchoptions {/ / initialize SDK [sensoranalysissdk startwithconfigoptions: Nil]; / / initialize public properties [[sensoranalysissdk sharedinstance] registersuperproperties: {@ “key”: @ “value”}]// Acquisition cold start [[sensoranalysissdk sharedinstance] autotrackappstart]; return YES;}

The above code has achieved the primary goal of SDK, but there are still some problems that need to be improved:
1. There are strict execution sequence requirements between codes. For example: – autotrackappstart must be placed behind – registersuperproperties:, which virtually increases the integration difficulty for developers;
2. – autotrackappstart, as a method of collecting cold start events, should not be exposed to the SDK. As a part of the full buried point, it can expose an interface for setting the full buried point;
3. Because the full buried point involves the method of monitoring the system, we hope to take “set the full buried point type” as the initialization parameter of an SDK. It is not recommended to set or modify it after initialization.
For the above reasons, we have added the saconfigoptions class to configure the initialization parameters of the SDK. Set the autotrackeventtype property in saconfigoptions to set the full buried point property, as shown below:

  • (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )Launchoptions {/ / configure the SDK initialization parameters; server_url is the data receiving address saconfiguptions * options = [saconfiguptions. Alloc initwithserverurl: sa_server_url launchoptions: launchoptions]; / / configure the open all buried point: cold start [options setautotrackeventtype: sensoranalysiseventtypeappstart]; / / initialize the SDK [sensoranalysissdk startwithconfigoptions: options] ; // Initialize public properties [[sensoranalysissdk sharedinstance] registersuperproperties: {@ “key”: @ “value”}]; return YES;}

To ensure that the – autotrackappstart method can be executed after – registersuperproperties: and other SDK setting methods, the following scheme is adopted:
1. Listen for uiapplicationdidfinishlunchinchinginotication notification in SDK initialization method + startwithconfigoptions:, which will be sent after – didfinishlunchingwithoptions: execution is completed;
2. After listening to uiapplicationdidfinishlaunchingnotification, call the – autotrackappstart method.
Therefore, the SDK initialization method is designed as follows (the code includes all the contents that need to be initialized by the SDK mentioned):

  • (void)startWithConfigOptions:(SAConfigOptions )Configoptions {nsassert (sensordata_is_same_queue (dispatch_get_main_queue()), @ “Shence IOS SDK must be initialized in the main thread, otherwise unexpected problems will be caused (such as the loss of $appstart event)”; dispatch_ once(&sdkInitializeOnceToken, ^{ sharedInstance = [[SensorsAnalyticsSDK alloc] initWithConfigOptions:configOptions debugMode:SensorsAnalyticsDebugOff]; });}- (instancetype)initWithConfigOptions:(nonnull SAConfigOptions)Configoptions debugmode: (sensorsanalyticsdebugmode) debugmode {@ try {self = [super init]; if (self) {_configoptions = [configoptions copy]; dispatch_block_t mainthreadlock = ^ () {/ / judge passive startup if (uiapplication. Sharedapplication. Applicationstate = = uiapplicationstatebackground) { self->_launchedPassively = YES; } }; sensorsdata_ dispatch_ main_ safe_ sync(mainThreadBlock); // Debug mode_ debugMode = debugMode; // Data receiving address_ network = [[SANetwork alloc] initWithServerURL:[NSURL URLWithString:_configOptions.serverURL]]; // Is it hot start_ appRelaunched = NO; // Prevent duplicate app exit events_ applicationWillResignActive = NO; // Timer_ trackTimer = [[SATrackTimer alloc] init]; // Get the distinguishid, loginid and superproperties [self unarchive] saved during the last process exit// Whether to access if (self. Firstday = = Nil) {nsdateformatter on the first daydateFormatter = [SADateFormatter dateFormatterFromString:@”yyyy-MM-dd”]; self. firstDay = [dateFormatter stringFromDate:[NSDate date]]; [self archiveFirstDay]; } // Collect preset attribute self automaticProperties = [self collectAutomaticProperties]; // [self setuplisteners];}}@ catch(NSExceptionexception) { SAError(@”%@ error: %@”, self, exception); } return self;}- (void) setuplisteners {/ / listen for app start or end events nsnotificationcenternotificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(applicationDidFinishLaunching:) name:UIApplicationDidFinishLaunchingNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; [notificationCenter addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil];}- (void)applicationDidFinishLaunching:(NSNotification )Notification {/ / collect cold start event [self autotrackappstart];}

So far, there is still a problem with this scheme: when integrating the SDK, developers may initialize the SDK after the uiapplicationdidfinishlaunchingnotification notification is sent, so that cold start events will not be collected. For example:
1. Initialize the SDK after – Application: didfinishlaunchingwithoptions:;
2. Initialize SDK asynchronously in – Application: didfinishlaunchingwithoptions:;
3. When the app starts, first request the network data receiving address, and then initialize the SDK.
In order to avoid the loss of cold start events, the following solution can be adopted: during SDK initialization (i.e. in – initwithconfigoptions: debugmode:), asynchronously call the – autotrackappstart method again in the main thread. The scheme has the following advantages:
1. Because there is a dispatch in the – autotrackappstart method_ Once ensures that the code is executed only once, so the cold start event will not be triggered repeatedly;
2. If the app has missed the uiapplicationdidfinishlaunchingnotification notification, the asynchronous method can ensure that the cold start event will still be collected;
3. The asynchronous task of the main thread will not be executed until the SDK related initialization is completed. At this time, the collected cold start event will not lose the public properties.
The code is as follows:

  • (instancetype)initWithConfigOptions:(nonnull SAConfigOptions *)configOptions debugMode:(SensorsAnalyticsDebugMode)debugMode { …… dispatch_async(dispatch_get_main_queue(), ^{ [self autoTrackAppStart]; }); ……..}

However, this scheme also has disadvantages: it is only applicable to initializing the SDK in the main thread and cannot solve the problem of initializing the SDK in the sub thread. Therefore, it is necessary to ensure that the SDK must be initialized in the main thread, which is also the reason for adding the assertion of judging the main thread in the + startwithconfigoptions: method.

3.2 hot start

Hot start, that is, when the app is in the supdependent state, it enters the app from the home screen. The acquisition method is as follows:

  • (void) trackrelaunchppstart {/ / trace appstart event if ([self isautotrackeventtypeignored: sensoranalysiseventtypeappstart] = = no) {[self track: sa_event_name_app_start withproperties: @ {sa_event_property_resume_from_backup: @ (_apprelaunched), sa_event_property_app_first_start: @ (no),} withtracktype: sensoranalysistracktypeauto];}// Start the $append event timer [self tracktimerstart: sa_event_name_app_end];}

According to the previous analysis, the status changes at this time are: suspended – > background – > inactive – > active, that is:
1、- applicationWillEnterForeground:
2、- applicationDidBecomeActive:
For SDK, it is more convenient to listen for notifications than uiapplicationdelegate protocol method. Therefore, the following two notices are actually used:
The analysis is as follows:
1. In many cases, inactive – > active will appear, so uiapplicationdidbecomeactivenotification cannot be used alone;
2. When sending uiapplicationwillenterforegroundnotification, the appstate is still background, and there may be some code execution problems.
Therefore, the above two notifications need to be used in combination. The code is as follows:

  • (void)applicationWillEnterForeground:(NSNotification )Notification {/ / identifier, which tells the SDK to collect the hot start event in the next notification uiapplicationdibecomeactivenotification _apprelaunched = yes; / / hot start, non passive start, self.launchedpassively = no;}- (void)applicationDidBecomeActive:(NSNotification)Notification {/ / enter the foreground from the background, and directly return if (! _apprelaunched) {return;}_ appRelaunched = NO; // Tracking appstart event [self trackrelaunchppstart];}

3.3 passive start

3.3.1 related concepts

After IOS 7, apple added a background application refresh function, which allows the operating system to pull up the application within a certain time interval (this time interval varies according to users’ different operating habits, which may be a few hours or a few days) and let it run in the background at the same time, so that the application can obtain the latest data and update relevant content, This ensures that users can view the latest content at the first time when opening the application. For example, news or social media applications can use this function to obtain the latest data content in the background. When users open the application, it can shorten the waiting time for application startup and content display, and finally improve the user experience of the product.
Background application refresh can shorten the waiting time for users; For products, it can improve the user experience; But for the data collection SDK, it may bring a series of problems. For example, when the system pulls up the application and lets it run in the background at the same time, the first page of the application (uiviewcontroller) will also be loaded, which will trigger a page browsing event. This is obviously unreasonable because the user does not open the application and does not browse the first page. In fact, the whole background application refresh process is completely transparent and imperceptible to users. Therefore, in the actual data collection process, we need to avoid this situation, so as not to affect the normal data analysis.
Here, we call the application triggered by IOS system and automatically running in the background as passive startup, which is usually represented by $appstartpassively event. Background application refresh is one of the most common causes of passive startup, and background application refresh is only one of the background operation modes, and some other background operation modes will also trigger passive startup. We will introduce it in detail below.
3.3.2 Background Modes
To create a new application using Xcode, the background refresh function is turned off by default. We can turn on background modes in the capabilities tab, and then check the required functions, as shown in Figure 3-1:
Launch and exit of Shence analysis IOS SDK full buried point analysis

Figure 3-1 background modes
As can be seen from Figure 3-1, there are several background operation modes, which will also trigger passive startup ($appstartpassively event).
Location updates: in this mode, the application startup is triggered due to the change of geographical location;
Newsstand downloads: this mode is only for newspaper and magazine applications. When a new newspaper can be downloaded, it will trigger the application to start;
External accessory communication: in this mode, some MFI peripherals are connected with IOS devices through Bluetooth or lightning connector, so that the corresponding application can be triggered when the peripherals send messages to the application;
Uses Bluetooth Le accessories: this mode is similar to external accessory communication, except that there is no need to restrict MFI peripherals, but Bluetooth Le devices are required;
Acts as a Bluetooth Le accessory: in this mode, iPhone is connected as a Bluetooth peripheral, which can trigger the application to start;
Background fetch: in this mode, IOS system will trigger application startup within a certain time interval to obtain application data;
Remote notifications: this mode supports silent push. When the application receives this push, there will be no interface prompt, but it will trigger the application to start.

3.3.3 acquisition scheme

After the background application refreshes and pulls up the application, it will first call back the – Application: didfinishlaunchingwithoptions: Method in appdelegate. Therefore, we can collect passive start event information by registering to listen to uiapplicationdidfinishlaunchingnotification local notifications.
However, there is a problem here: uiapplicationdidfinishlaunchingnotification will also be sent for the normal cold start of the application, which will also trigger the $appstartpassively event. So how to solve this problem?
It is still determined by the applicationstate of uiapplication discussed in Section 2:

@property(nonatomic,readonly) UIApplicationState applicationState API_AVAILABLE(ios(4.0));

For normal cold start, the value of applicationstate should be uiapplicationstateinactive; However, in case of passive startup, the value will be uiapplicationstatebackground. Therefore, when the application starts, if the value of the applicationstate property is equal to uiapplicationstatebackground, it means that the application is started passively at this time. This can solve the problem that cold start will also trigger passive start events. The code is as follows:

  • (instancetype) initwithconfigoptions: (nonnull saconfigoptions *) configoptions debugmode: (sensoranalysisdebugmode) debugmode {… Dispatch_block_t mainthreadblock = ^ () {/ / judge passive startup if (uiapplication. Sharedapplication. Applicationstate = = uiapplicationstatebackground) { self->_launchedPassively = YES; } }; ……}

4、 App exit

According to the previous introduction, when an app exits (represented by the $append event), it means that the application enters the “background”, that is, it is in the background state. Therefore, for realizing the full burial point of $append event, we only need to register to listen to uiapplicationdidenterbackgroundnotification notification notification, and then trigger $append event when receiving the notification to achieve the effect of full burial point of $append event.
4.1 acquisition scheme
App exit can be divided into the following situations:
1. Return to the main screen in app;
2. App enters the switcher, and then returns to the main screen;
3. App enters the switcher, and then kill app.
In either case, the state transition of active – > inactive – > background – > suspended will be executed. Therefore, it should be considered to record the event in one of the following two notices:
Similarly, since there are many cases where active – > inactive will appear, uiapplicationwillresignactivnotification cannot be used alone. The code is as follows:

  • (void)applicationDidEnterBackground:(NSNotification )Notification {/ / reset the “passive start” flag self.launchedpassively = No. / / set the background task timeout block. If you receive a notification, end the background task uiapplicationapplication = UIApplication. sharedApplication; __ block UIBackgroundTaskIdentifier backgroundTaskIdentifier = UIBackgroundTaskInvalid; backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{ [application endBackgroundTask:backgroundTaskIdentifier]; backgroundTaskIdentifier = UIBackgroundTaskInvalid; }]; // Tracking $append event if ([self isautotrackeventtypeignored: sensoranalysiseventtypeappend] = = no) {[self track: sa_event_name_app_end withtracktype: sensoranalysistracktypeauto];}}

It should be noted here that the collection method of APP browsing duration attribute attached to app exit event is as follows:
1. No matter when the hot and cold start is triggered, we record the time when the app is started and store its value (the key is $append);
2. When collecting the app exit event, we will use $append as the key to take out the pre stored app start time and subtract it from the current time. The resulting time is the app browsing time, which is stored in $event_ In the duration attribute.

4.2 scheme optimization

There is a problem with the above scheme: in some special cases, the application will send uiapplicationdidenterbackgroundnotification twice in a row. For example, if you lock the screen while returning to the main screen, you will be notified of uiapplicationwillreassignactivenotification once and uiapplicationdidenterbackgroundnotification twice. In this way, redundant exit events will be collected, and the second event will not have app browsing time, which is obviously abnormal.
After analyzing the phenomenon, the solution is ready to come out: before the app enters the background, uiapplicationwillresignactivnotification notification will be sent. At this time, a flag bit is used to indicate that the app has logged off the active state; When the app enters the background, it will receive uiapplicationdidenterbackgroundnotification notification and change this flag to No. Therefore, judge whether this mark is yes or not, and the code is as follows:

  • (void)applicationWillResignActive:(NSNotification )notification { _applicationWillResignActive = YES;}- (void)applicationDidEnterBackground:(NSNotification )notification { if (!_applicationWillResignActive) { return; } _applicationWillResignActive = NO; ……}

5、 Summary

This article is the second in the series of blog “Shence analysis of IOS SDK source code analysis”. It mainly introduces the design scheme of starting and exiting buried points in IOS full buried points, as well as the relevant knowledge of APP application status. I hope it can bring some help to you on the road of learning full buried point technology.

6、 References

Managing Your App’s Life Cycle(…

Source: official account Shence technology community