iOS全埋點解決方案-用戶標識

来源:https://www.cnblogs.com/r360/archive/2022/04/27/16200011.html
-Advertisement-
Play Games

前言 ​ 分析用戶行為,需要標識用戶。選擇合適的用戶標識,可以提高用戶行為分析的準確性,尤其是是漏洞留存分析等,這些和用戶分析相關的功能。對於唯一標識一個用戶,我們需要考慮兩種場景。 用戶登陸之前如何標識 用戶登陸之後如何標識 一、登陸之前 業界一般使用 iOS 設備的某個特定屬性或者某幾個特定屬性 ...


前言

​ 分析用戶行為,需要標識用戶。選擇合適的用戶標識,可以提高用戶行為分析的準確性,尤其是是漏洞留存分析等,這些和用戶分析相關的功能。對於唯一標識一個用戶,我們需要考慮兩種場景。

  • 用戶登陸之前如何標識
  • 用戶登陸之後如何標識

一、登陸之前

業界一般使用 iOS 設備的某個特定屬性或者某幾個特定屬性組合方式,來唯一標識一臺 iOS 設備。此時的用戶 ID 一般稱為設備 ID 或者匿名 ID。蘋果公司為了維護整個生態系統的健康發展,也會極力阻止個人或者組織去唯一標識一臺 iOS 設備。因此我們唯一能做的,就是在現有的條件及政策下,努力尋找一種最優的解決方案。

1.1 UDID

​ UDID (Unique Device Identifier,設備唯一標識符)是和設備相關且只和設備相關的。他是一個由 40 位 16 進位組成的序列。在 iOS 5 之前,我們可以通過下麵的代碼段獲取當前設備的UDID。

// uudi = 00008020-000A4D260104003A
NSString *udid = [[UIDevice currentDevice] uniqueIdentifier];

但從 iOS 5 開始,蘋果公司為了保護用戶的隱私,就不在支持通過上面的方法獲取 UDID。

不過我們任然可以通過下麵 2 種方式獲取 iOS 設備的 UDID。

(1) Xcode

​ 把手機連接上電腦,啟動 Xcode,依次點擊 window->Device and Simulators, 然後就可以看到你連接到電腦上的 iOS 設備,其中,Identifier 就是設備的 UDID。

image-20220406143708678

(2)蒲公英

​ 蒲公英提供了一個可以用來獲取 iOS 設備 UDID 。地址:https://www.pgyer.com/tools/udid,或者掃下方二維碼,按照蒲公英提示,然後就可以獲取設備 UDID。

image-20220406144257621

結論: 由於從 iOS 5 開始,蘋果公司不容許 iOS 應用程式通過代碼獲取 UDID,因此,UDID 不適合作為 iOS 設備的 ID 。

1.2 UUID

​ UUID含義是通用唯一識別碼 (Universally Unique Identifier),是由一個 32 為 16 進位組成的序列,使用短橫線來連接。格式為:8-4-4-4-12(數字代表位數,加上4個端橫線,一共 36 位)。

C1F88522-DE2A-4315-B5BA-F557DFFAE3B9

​ UUID 能在任何時刻、不藉助任何伺服器的情況下生成,且在某一特定的時空下是全球唯一的。

​ 從 iOS 6 開始,iOS 應用程式可以通過 NSUUID 類來獲取 UUID:

// NSUUID 獲取
NSString *uuid = [NSUUID UUID].UUIDString;

// CFUUIDRef 獲取
CFUUIDRef cfuuidRef = CFUUIDCreate(kCFAllocatorDefault);
NSString *uuid = (NSString *)CFBridgingRelease(CFUUIDCreateString(kCFAllocatorDefault, cfuuidRef));

生成的 UUID,系統不會做持久化存儲,,因此每次調用的時候都會是獲得到一個全新的 UUID,如果用戶刪除應用程式並再次安裝,將無法做到唯一標識 iOS 設備,因此 UUID 也不適合作為 iOS 設備 ID。

1.3 MAC 地址

​ MAC 地址是用來標識互聯網上的每一個站點,它是由一個 12 為的十六進位組成的序列。

C2:B3:01:60:6D:4E

​ 我們可以通過真機,點擊設置->通用->關於本機->無線區域網地址,查看 iOS 設備的 MAC 地址。

image-20220406152649791

​ 凡是接入網路的設備都會有一個 MAC 地址,用來區分每個設備的唯一性。一個 iOS 設備可能會有多個 MAC 地址,這是因為它可能會有多個設備接入網路,比如 WiFi、SIM 卡等。一般的情況下,只獲取 WiFi 的 MAC 地址即可,即 en0 的地址。

​ 從 iOS 7 之前,可以通過代碼獲取到 WiFi 的 MAC 地址。但 iOS 7 開始,蘋果公司禁止 iOS 應用程式獲取 MAC 地址,因為 MAC 地址也不適合作為 iOS 設備 ID。

1.4 IDFA

​ IDFA(identifier For Advertising,廣告標識符),主要用於廣告推廣,還量等跨應用的設備追蹤。它也是一個由 32 為十六進位組成的序列,格式與 UUID 一致。在同一個 iOS 設備上,同一時刻,所有的應用程式獲取到的 IDFA 都是相同的。

​ 從 iOS 6 開始,我們可以利用 AdSupport.framework 庫提供的方法來獲取 IDFA,代碼片段如下:

#import <AdSupport/AdSupport.h>

NSString *idfa = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
模擬器運行:80DCBD13-4209-4344-848A-1F16BE48B897

但是,IDFA 的值並不是固定不變的。

目前,一下操作均會改變 IDFA 的值

  • 通過設置->通用->還原->抹掉所有內容和設置。
  • 通過 ITunes 還原設備
  • 通過設置->隱私->廣告->限制廣告追蹤

一旦用戶限制了廣告追蹤,我們獲取到的 IDFA 將是一個固定的 IDFA,即一連串零:00000000-0000-0000-0000-000000000000。因此,在獲取 IDFA 之前,我們可以利用 AdSupport.framework 庫提供的介面來判斷用戶是否限制了廣告追蹤。

BOOL isLimitAdTracking = [[ASIdentifierManager sharedManager] isAdvertisingTrackingEnabled];

​ 通過設置->隱私->廣告->限制廣告追蹤,用戶一旦還原了廣告標識符,系統將會生成一個全新的 IDFA。

結論:IDFA 的使用有一些限制條件,但對於上述操作,只有在特定的情況下才會發生,或者只有專業人士才有可能執行這些操作。同時,IDFA 能解決應用程式卸載重裝唯一標識設備問題。因此,IDFA 目前來說比較合適作為 iOS 設備 ID 屬性。

1.5 IDFV

​ IDFV(identifier For Vendor,應用開發商標識符)是為了應用開發商標識用戶,適用於分析用戶在應用內的行為等,它也是一個由 32 為十六進位組成的序列,格式與 UUID 一致。

​ 每一個 iOS 設備在所屬同一個 Vendor 的應用里,獲取到的 IDFV 是相同的。Vendor 是通過翻轉後的 BundleID 的前兩部分進行匹配的,如果相同就屬於通一個 Vendor。和 IDFA 相比, IDFV 不會出現獲取不到的場景。

​ 但 IDFV 也有一個很大的缺點:如果用戶將屬於此 Vendor 的所有的應用程式都卸載, IDFV 的值會被系統重置。即使重裝該 Vendor 的應用程式,獲取到的也是一個全新的 IDFV。一下的操作都會重置 IDFV。

  • 通過設置->通用->還原->抹掉所有內容和設置
  • 通過 iTunes 還原設備
  • 卸載設備上某個開發者賬號下的所有的應用程式

​ 在 iOS 應用程式內,可以通過 UIDevice 類來獲取 IDFV,代碼片段如下:

NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

獲取到的 IDFV : 80DCBD13-4209-4344-848A-1F16BE48B897

結論:和 IDFA 相比,特別是在解決應用程式卸載重裝的問題上,IDFV 不太適合作為 iOS 設備 ID。

1.6 IMEI

​ IMEI(International Mobile Equipment Identity,國際移動設備身份碼)是由 15 未純數字組成的串,並且是全球唯一的。任何一部手機,在其生成並組裝完成智慧,都會被寫入一個全球唯一的 IMEI。我們可以通過設置->通用->關於本機,查看本機的 IMEI。

image-20220406161744827

結論:從 iOS 2 開始,蘋果公司提供了相應的介面來獲取 IMEI。但後來為了保護用戶隱私,從 iOS 5 開始,蘋果公司就不在容許應用程式獲取 IMEI。因此,IMEI 也不合適作為 iOS 設備 ID。

1.7 最佳實踐

​ 通過上面的介紹,他們各有優缺點,但都不是非常完美的方案。總體來說,有兩個問題。1. 無法保證唯一性;2.受到相關政策的限制。關於設備 ID ,到底有沒有一種完美的方案呢?很遺憾,目前看起來沒有,我們只能在有限的條件和限制下,尋找一種比較完美的方案。

(1)方案一

​ 按照優先順序順序獲取:IDFA->IDFV->UUID

第一步:在 SensorsAnalyticsSDK 類中,新增一個屬性,用於保存 anonymousId,

然後在 SensorsAnalyticsSDK.m 文件中新增 anonymousId 聲明

@interface SensorsAnalyticsSDK : NSObject
/// 設備 ID (匿名 ID)
@property (nonatomic, copy) NSString *anonymousId;
@end
@implementation SensorsAnalyticsSDK {
    NSString *_anonymousId;
}
@end

第二步:新增 - saveAnonymousId:方法,用於保存 anonymousId

- (void)saveAnonymousId:(NSString *)anonymousId {
    // 保存設備ID
    [[NSUserDefaults standardUserDefaults] setObject:anonymousId forKey:SensorsAnalyticsAnonymousId];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

第三步:重寫 anonymousId set 方法

- (void)setAnonymousID:(NSString *)anonymousId {
    _anonymousId = anonymousId;
    // 保存設備ID
    [self saveAnonymousId:anonymousId];
}

第四步:重寫 anonymousId get 方法

- (NSString *)anonymousId {
    if (_anonymousId) {
        return _anonymousId;
    }
    
    // 從 NSUserDefaults 讀取
    _anonymousId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsAnonymousId];
    if (_anonymousId) {
        return _anonymousId;
    }
    
    // 獲取 IDFA
    Class cls = NSClassFromString(@"ASIdentifierManager");
    if (cls) {
       // 獲取 ASIdentifierManager 單例對象
        id manager = [cls performSelector:@selector(sharedManager)];
        SEL selector = NSSelectorFromString(@"isAdvertisingTrackingEnabled");
        BOOL (*isAdvertisingTrackingEnabled)(id, SEL) = (BOOL (*)(id, SEL))[manager methodForSelector:selector];
        if (isAdvertisingTrackingEnabled(manager, selector)) {
            // 使用 IDFA 作為設備 ID
            _anonymousId = [(NSUUID *) [manager performSelector:@selector(advertisingIdentifier)] UUIDString];
        }
    }
    
    // 使用 IDFV 作為設備 ID
    if (!_anonymousId) {
        _anonymousId = UIDevice.currentDevice.identifierForVendor.UUIDString;
    }
    // 使用 UUID 作為設備 ID
    if (!_anonymousId) {
        _anonymousId = NSUUID.UUID.UUIDString;
    }
    
    [self saveAnonymousId:_anonymousId];
    return _anonymousId;
}

第五步:修改 - track: properties: 方法,新增 distinct_id 欄位

- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
    NSMutableDictionary *event = [NSMutableDictionary dictionary];
    // 設置事件 distinct_id 欄位,用於唯一標識一個用戶
    event[@"distinct_id"] = self.anonymousId;
    // 設置事件名稱
    event[@"event"] = eventName;
    // 事件發生的時間戳,單位毫秒
    event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
    
    NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
    // 添加預置屬性
    [eventProperties addEntriesFromDictionary:self.automaticProperties];
    // 添加自定義屬性
    [eventProperties addEntriesFromDictionary:properties];
    // 判斷是否是被動啟動狀態
    if (self.isLaunchedPassively) {
        eventProperties[@"$app_state"] = @"background";
    }
    // 設置事件屬性
    event[@"propeerties"] = eventProperties;
    
    // 列印
    [self printEvent:event];
}

第六步:測試驗證

{
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  },
  "event" : "$AppStart",
  "time" : 1649238293741,
  "distinct_id" : "80DCBD13-4209-4344-848A-1F16BE48B897"
}
(2)方案二

​ 使用 Keychain 工具, Keychain 是 OS X 和 iOS 都提供了一種安全存儲敏感信息工具。 Keychain 的安全機制是從系統層面保證了存儲的敏感新信息不會被非法讀取或竊取。

1c9e8103-fae2-45f4-832c-c528d2e0c2f6

Keychain 特點:

  • 保存在 Keychain 中的數據,即使應用程式被卸載,數據仍然存在;重寫安裝應用程式,我們也可以從 Keychain 中讀取這些數據。
  • Keychain 中的數據可以通過 Group 的方式實現應用程式之間共用,只要應用程式具有相同的 TeamID 即可。
  • 保存在 Keychain 中的數據都是經過加密的,因此非常安全。

使用 Keychain 對方案一進行優化:

第一步:新增 SensorsAnalyticsKeychainItem 工具類,用於在 keychain 中保存,讀取及刪除數據。

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface SensorsAnalyticsKeychainItem : NSObject

- (instancetype)init NS_UNAVAILABLE;

- (instancetype)initWithService:(NSString *)service key:(NSString *)key;

- (instancetype)initWithService:service accessGroup:(nullable NSString *)accessGroup key:(NSString *)key NS_DESIGNATED_INITIALIZER;

- (nullable NSString *)value;

- (void)update:(NSString *)value;

- (void)remove;

@end

NS_ASSUME_NONNULL_END

#import "SensorsAnalyticsKeychainItem.h"

#import <Security/Security.h>

@interface SensorsAnalyticsKeychainItem()

@property (nonatomic, strong) NSString *service;
@property (nonatomic, strong) NSString *accessGroup;
@property (nonatomic, strong) NSString *key;

@end

@implementation SensorsAnalyticsKeychainItem

- (instancetype)initWithService:(NSString *)service key:(NSString *)key {
    return [self initWithService:service accessGroup:nil key:key];
}

- (instancetype)initWithService:(id)service accessGroup:(NSString *)accessGroup key:(NSString *)key {
    self = [super init];
    if (self) {
        _service = service;
        _key = key;
        _accessGroup = accessGroup;
    }
    return self;
}

- (nullable NSString *)value {
    NSMutableDictionary *query = [SensorsAnalyticsKeychainItem keychainQueryWithService:self.service accessGroup:self.accessGroup key:self.key];
    query[(NSString *) kSecMatchLimit] = (id)kSecMatchLimitOne;
    query[(NSString *) kSecReturnAttributes] = (id)kCFBooleanTrue;
    query[(NSString *) kSecReturnData] = (id)kCFBooleanTrue;
    CFTypeRef queryResult;
    OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &queryResult);
    
    if (status == errSecItemNotFound) {
        return nil;
    }
    if (status != noErr) {
        NSLog(@"Get item value error %d", (int)status);
    }
    
    NSData *data = [(__bridge_transfer NSDictionary *)queryResult objectForKey:(NSString *)kSecValueData];
    if (!data) {
        return nil;
    }
    NSString *value = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    NSLog(@"Get item value %@", value);
    return value;
}

- (void)update:(NSString *)value {
    NSData *encodedValue = [value dataUsingEncoding:NSUTF8StringEncoding];
    
    NSMutableDictionary *query = [SensorsAnalyticsKeychainItem keychainQueryWithService:self.service accessGroup:self.accessGroup key:self.key];
    
    NSString *originalValue = [self value];
    
    if (originalValue) {
        NSMutableDictionary *arrtibutesToUpdate = [[NSMutableDictionary alloc] init];
        arrtibutesToUpdate[(NSString *)kSecValueData] = encodedValue;
        
        OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)arrtibutesToUpdate);
        if (status == noErr) {
            NSLog(@"update item ok");
        } else {
            NSLog(@"update item error %d", (int)status);
        }
    } else {
        [query setObject:encodedValue forKey:(id)kSecValueData];
        OSStatus status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
        if (status == noErr) {
            NSLog(@"add item ok");
        } else {
            NSLog(@"add item error %d", (int)status);
        }
    }
    
}

- (void)remove {
    NSMutableDictionary *query = [SensorsAnalyticsKeychainItem keychainQueryWithService:self.service accessGroup:self.accessGroup key:self.key];
    OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
    
    if (status != noErr && status != errSecItemNotFound) {
        NSLog(@"remove item %d", (int)status);
    }
}

#pragma mark - private
+ (NSMutableDictionary *)keychainQueryWithService:(NSString *)service accessGroup:(nullable NSString *)accessGroup key:(NSString *)key {
    NSMutableDictionary *query = [[NSMutableDictionary alloc] init];
    query[(NSString *)kSecClass] = (NSString *)kSecClassGenericPassword;
    query[(NSString *)kSecAttrService] = (NSString *)service;
    query[(NSString *)kSecAttrAccount] = key;
    query[(NSString *)kSecAttrAccessGroup] = accessGroup;
    return query;
}

@end

第二步:在 SensorsAnalyticsSDK.m 文件中 ,- saveAnonymousId: 方法中使用 keychain 進行保存

#import "SensorsAnalyticsKeychainItem.h"

static NSString *const SensorsAnalyticsKeychainService = @"cn.sensorsdata.SensorsAnalytics.id";

- (void)saveAnonymousId:(NSString *)anonymousId {
    // 保存設備ID
    [[NSUserDefaults standardUserDefaults] setObject:anonymousId forKey:SensorsAnalyticsAnonymousId];
    [[NSUserDefaults standardUserDefaults] synchronize];
    
    //
    SensorsAnalyticsKeychainItem *item = [[SensorsAnalyticsKeychainItem alloc] initWithService:SensorsAnalyticsKeychainService key:SensorsAnalyticsAnonymousId];
    if (anonymousId) {
        // 當設備ID 不為空時,將其保存到 keychain 中
        [item update:anonymousId];
    } else {
        [item remove];
    }
}

第三步:修改 SensorsAnalyticsSDK.m 文件中 - anonymousId get 方法

- (NSString *)anonymousId {
    if (_anonymousId) {
        return _anonymousId;
    }
    
    // 從 keychain 獲取
    SensorsAnalyticsKeychainItem *item = [[SensorsAnalyticsKeychainItem alloc] initWithService:SensorsAnalyticsKeychainService key:SensorsAnalyticsAnonymousId];
    _anonymousId = item.value;
    if (_anonymousId) {
        return _anonymousId;
    }
    // 從 NSUserDefaults 讀取
    _anonymousId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsAnonymousId];
    if (_anonymousId) {
        return _anonymousId;
    }
    
    // 獲取 IDFA
    Class cls = NSClassFromString(@"ASIdentifierManager");
    if (cls) {
       // 獲取 ASIdentifierManager 單例對象
        id manager = [cls performSelector:@selector(sharedManager)];
        SEL selector = NSSelectorFromString(@"isAdvertisingTrackingEnabled");
        BOOL (*isAdvertisingTrackingEnabled)(id, SEL) = (BOOL (*)(id, SEL))[manager methodForSelector:selector];
        if (isAdvertisingTrackingEnabled(manager, selector)) {
            // 使用 IDFA 作為設備 ID
            _anonymousId = [(NSUUID *) [manager performSelector:@selector(advertisingIdentifier)] UUIDString];
        }
    }
    
    // 使用 IDFV 作為設備 ID
    if (!_anonymousId) {
        _anonymousId = UIDevice.currentDevice.identifierForVendor.UUIDString;
    }
    // 使用 UUID 作為設備 ID
    if (!_anonymousId) {
        _anonymousId = NSUUID.UUID.UUIDString;
    }
    
    [self saveAnonymousId:_anonymousId];
    return _anonymousId;
}

第四步:測試驗證,卸載 APP 重新安裝時 distinct_id 的值不變

{
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$title" : "標題2",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$screen_name" : "ViewController",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  },
  "event" : "$AppViewScreen",
  "time" : 1649302123586,
  "distinct_id" : "68120246-9BBC-45F4-B37C-4299BA12674A"
}

二、登陸之後

​ 用戶一旦註冊或者登陸用於程式,那麼其在用戶系統里肯定是唯一的。我們可以提供 - login:方法。當應用程式獲取用戶登陸的 ID 智慧,通過調用 - login:方法把登陸的 ID 傳給 SDK,之後用戶觸發事件,可使用登陸 ID 來標識。

第一步:在 SensorsAnalyticsSDK.h 文件中聲明 -login:方法:併在.m文件中實現

@interface SensorsAnalyticsSDK : NSObject

/// 用戶登陸設置登陸 ID
/// @param loginId 用戶的登陸 ID
- (void)login:(NSString *)loginId;

@end
static NSString *const SensorsAnalyticsLoginId = @"cn.sensorsdata.login_id";

/// 登陸 ID
@property (nonatomic, copy) NSString *loginId;

- (instancetype)init {
    self = [super init];
    if (self) {
        _automaticProperties = [self collectAutomaticProperties];

        // 設置是否需是被動啟動標記
        _launchedPassively = UIApplication.sharedApplication.backgroundTimeRemaining != UIApplicationBackgroundFetchIntervalNever;
        
        _loginId = [[NSUserDefaults standardUserDefaults] objectForKey:SensorsAnalyticsLoginId];
        
        // 添加應用程式狀態監聽
        [self setupListeners];
    }
    return self;
}

#pragma mark - login
- (void)login:(NSString *)loginId {
    self.loginId = loginId;
    
    [[NSUserDefaults standardUserDefaults] setObject:self.loginId forKey:SensorsAnalyticsLoginId];
    [[NSUserDefaults standardUserDefaults] synchronize];
}

第二步:修改 SensorsAnalyticsSDK 類別 track 中的 - track: properties: 方法,給欄位 distinct_id 賦值

- (void)track:(NSString *)eventName properties:(nullable NSDictionary<NSString *, id> *)properties {
    NSMutableDictionary *event = [NSMutableDictionary dictionary];
    // 設置事件 distinct_id 欄位,用於唯一標識一個用戶
    event[@"distinct_id"] = self.loginId ?: self.anonymousId;
    // 設置事件名稱
    event[@"event"] = eventName;
    // 事件發生的時間戳,單位毫秒
    event[@"time"] = [NSNumber numberWithLong:NSDate.date.timeIntervalSince1970 *1000];
    
    NSMutableDictionary *eventProperties = [NSMutableDictionary dictionary];
    // 添加預置屬性
    [eventProperties addEntriesFromDictionary:self.automaticProperties];
    // 添加自定義屬性
    [eventProperties addEntriesFromDictionary:properties];
    // 判斷是否是被動啟動狀態
    if (self.isLaunchedPassively) {
        eventProperties[@"$app_state"] = @"background";
    }
    // 設置事件屬性
    event[@"propeerties"] = eventProperties;
    
    // 列印
    [self printEvent:event];
}

第三步:測試驗證,在應用程式內調用 -login 方法

 {
  "propeerties" : {
    "$model" : "x86_64",
    "$manufacturer" : "Apple",
    "$element_type" : "UIImageView",
    "$lib_version" : "1.0.0",
    "$os" : "iOS",
    "$app_version" : "1.0",
    "$screen_name" : "ViewController",
    "$os_version" : "15.2",
    "$lib" : "iOS"
  },
  "event" : "$AppClick",
  "time" : 1649311931458,
  "distinct_id" : "1234567"
}

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ** linux 下需要知道你系統的Linux內核版本,然後選擇相應的版本下載,版本查詢命令:** uname -a 在高版本20.04和18.04等版本,不需要執行下麵這條命令來編譯makefile文件,因為系統自身存在串口驅動cp210x.o make 於是只需要在[/lib/modules/5 ...
  • 最近需要使用mysql8.0版本,但是原本的mysql5.7版本已經被多個服務依賴,於是想想能不能同一臺伺服器裝多個版本的mysql,一查確實可行,這裡做一個記錄方便自己後期回憶 閱讀本文前請註意!!! 本文是幫助您建立在mysql5.7版本已經安裝完成併在運行中,另外安裝配置mysql8.0版本 ...
  • 事務概述 當多個用戶訪問同一份數據時,一個用戶在更改數據的過程中,可能有其他用戶同時發起更改請求,為保證資料庫記錄的更新從一個一致性狀態變為另外一個一致性狀態,使用事務處理是非常必要的,事務具有以下四個特性: 原子性(Atomicity):事務中所有操作視為一個原子單位,即對事務所進行的數據修改等操 ...
  • Hadoop是什麼 大白話,Hadoop是個存儲數據,計算數據的分散式框架。核心組件是HDFS、MapReduce、Yarn。 HDFS:分散式存儲 MapReduce:分散式計算 Yarn:調度MapReduce 現在為止我們知道了HDFS、MapReduce、Yarn是幹啥的,下麵通過一張圖再來 ...
  • Mysql 連續數據分組 思路是使用變數 逐行將上行和當前行進行對比 條件滿足則生成分組的編號,再根據分組條件和分組編號分組就可以。 ...
  • 主從複製 這是《Redis設計與實現》系列的文章,系列導航:Redis設計與實現筆記 SLAVEOF 新舊複製功能 舊版複製功能 舊版複製功能的實現為 同步 和 命令傳播: 當剛連上Master時,要做一次全同步: sequenceDiagram participant Slave particip ...
  • 移動互聯網的發展給人們的社交和娛樂方式帶來了很大的改變,以vlog、短視頻等為代表的新興文化樣態正受到越來越多人的青睞。同時,隨著AI智能、美顏修圖等功能在圖像視頻編輯App中的應用,促使視頻編輯效率和視頻效果得到了很大的提升,也讓視頻應用場景更加豐富。 當前剪輯產品功能多樣、素材豐富,但是開發周期 ...
  • 4月28日晚上19點,知識賦能第五期第二節課《如何成為OpenHarmony社區貢獻達人?》,在OpenHarmony開發者成長計劃社群內成功舉行。 ...
一周排行
    -Advertisement-
    Play Games
  • 概述:在C#中,++i和i++都是自增運算符,其中++i先增加值再返回,而i++先返回值再增加。應用場景根據需求選擇,首碼適合先增後用,尾碼適合先用後增。詳細示例提供清晰的代碼演示這兩者的操作時機和實際應用。 在C#中,++i 和 i++ 都是自增運算符,但它們在操作上有細微的差異,主要體現在操作的 ...
  • 上次發佈了:Taurus.MVC 性能壓力測試(ap 壓測 和 linux 下wrk 壓測):.NET Core 版本,今天計劃準備壓測一下 .NET 版本,來測試並記錄一下 Taurus.MVC 框架在 .NET 版本的性能,以便後續持續優化改進。 為了方便對比,本文章的電腦環境和測試思路,儘量和... ...
  • .NET WebAPI作為一種構建RESTful服務的強大工具,為開發者提供了便捷的方式來定義、處理HTTP請求並返迴響應。在設計API介面時,正確地接收和解析客戶端發送的數據至關重要。.NET WebAPI提供了一系列特性,如[FromRoute]、[FromQuery]和[FromBody],用 ...
  • 原因:我之所以想做這個項目,是因為在之前查找關於C#/WPF相關資料時,我發現講解圖像濾鏡的資源非常稀缺。此外,我註意到許多現有的開源庫主要基於CPU進行圖像渲染。這種方式在處理大量圖像時,會導致CPU的渲染負擔過重。因此,我將在下文中介紹如何通過GPU渲染來有效實現圖像的各種濾鏡效果。 生成的效果 ...
  • 引言 上一章我們介紹了在xUnit單元測試中用xUnit.DependencyInject來使用依賴註入,上一章我們的Sample.Repository倉儲層有一個批量註入的介面沒有做單元測試,今天用這個示例來演示一下如何用Bogus創建模擬數據 ,和 EFCore 的種子數據生成 Bogus 的優 ...
  • 一、前言 在自己的項目中,涉及到實時心率曲線的繪製,項目上的曲線繪製,一般很難找到能直接用的第三方庫,而且有些還是定製化的功能,所以還是自己繪製比較方便。很多人一聽到自己畫就害怕,感覺很難,今天就分享一個完整的實時心率數據繪製心率曲線圖的例子;之前的博客也分享給DrawingVisual繪製曲線的方 ...
  • 如果你在自定義的 Main 方法中直接使用 App 類並啟動應用程式,但發現 App.xaml 中定義的資源沒有被正確載入,那麼問題可能在於如何正確配置 App.xaml 與你的 App 類的交互。 確保 App.xaml 文件中的 x:Class 屬性正確指向你的 App 類。這樣,當你創建 Ap ...
  • 一:背景 1. 講故事 上個月有個朋友在微信上找到我,說他們的軟體在客戶那邊隔幾天就要崩潰一次,一直都沒有找到原因,讓我幫忙看下怎麼回事,確實工控類的軟體環境複雜難搞,朋友手上有一個崩潰的dump,剛好丟給我來分析一下。 二:WinDbg分析 1. 程式為什麼會崩潰 windbg 有一個厲害之處在於 ...
  • 前言 .NET生態中有許多依賴註入容器。在大多數情況下,微軟提供的內置容器在易用性和性能方面都非常優秀。外加ASP.NET Core預設使用內置容器,使用很方便。 但是筆者在使用中一直有一個頭疼的問題:服務工廠無法提供請求的服務類型相關的信息。這在一般情況下並沒有影響,但是內置容器支持註冊開放泛型服 ...
  • 一、前言 在項目開發過程中,DataGrid是經常使用到的一個數據展示控制項,而通常表格的最後一列是作為操作列存在,比如會有編輯、刪除等功能按鈕。但WPF的原始DataGrid中,預設只支持固定左側列,這跟大家習慣性操作列放最後不符,今天就來介紹一種簡單的方式實現固定右側列。(這裡的實現方式參考的大佬 ...