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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...