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
  • 什麼是工廠模式 工廠模式是最常用的設計模式之一,屬於創建型模式。 有點: 解耦,可以把對象的創建和過程分開 減少代碼量,易於維護 什麼時候用? 當一個抽象類有多個實現的時候,需要多次實例化的時候,就要考慮使用工廠模式。 比如:登錄的抽象類ILoginBusiness,它有2個實現,一個用用戶名密碼登 ...
  • 這次iNeuOS升級主要升級圖形渲染引擎和增加豐富的圖元信息,可以很快的方案應用。總共增加41個通用和行業領域的圖元應用,增加2154個圖元信息,現在iNeuOS視圖建模功能模塊總共包括5894個行業圖元信息。現在完全支持製作高保真的工藝流程和大屏展示效果。 ...
  • 效果圖先附上: 首先 這是我是參考 教程:使用 SignalR 2 和 MVC 5 實時聊天 | Microsoft Docs 先附上教程: 在“添加新項 - SignalRChat”中,選擇 InstalledVisual> C#>WebSignalR>,然後選擇 SignalR Hub 類 (v ...
  • 一、前言 項目中之前涉及到胎兒心率圖曲線的繪製,最近項目中還需要添加心電曲線和血樣曲線的繪製功能。今天就來分享一下心電曲線的繪製方式; 二、正文 1、胎兒心率曲線的繪製是通過DrawingVisual來實現的,這裡的心電曲線我也是採用差不多相同的方式來實現的,只是兩者曲線的數據有所區別。心電圖的數據 ...
  • 安裝 Redis # 首先安裝依賴gcc, 後面需要使用make編譯redis yum install gcc -y # 進入 /usr/local/src 目錄, 把源碼下載到這裡 cd /usr/local/src # 下載 redis 7.0.2 的源碼,github被牆,可以使用國內的地址 ...
  • Redis 的定義? 百度百科: Redis(Remote Dictionary Server ),即遠程字典服務,是一個開源的使用ANSI C語言編寫、支持網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API。 中文官網: Redis是一個開源(BSD許可),記憶體存 ...
  • 事情的起因是收到了一位網友的請求,他的java課設需要設計實現迷宮相關的程式——如標題概括。 我這邊不方便透露相關信息,就只把任務要求寫出來。 演示視頻指路👉: 基於JavaFX圖形界面的迷宮程式演示_嗶哩嗶哩_bilibili 完整代碼鏈接🔎: 網盤:https://pan.baidu.com ...
  • Python中的字典 Python中的字典是另一種可變容器模型,且可存儲任意類型對象。鍵值使用冒號分割,你可以看成是一串json。 常用方法 獲取字典中的值 dict[key] 如果key不存在會報錯,建議使用dict.get(key),不存在返回None 修改和新建字典值 dict[key]=va ...
  • 迎面走來了你的面試官,身穿格子衫,挺著啤酒肚,髮際線嚴重後移的中年男子。 手拿泡著枸杞的保溫杯,胳膊夾著MacBook,MacBook上還貼著公司標語:“加班使我快樂”。 面試官: 看你簡歷上用過MySQL,問你幾個簡單的問題吧。什麼是聚簇索引和非聚簇索引? 這個問題難不住我啊。來之前我看一下一燈M ...
  • tunm二進位協議在python上的實現 tunm是一種對標JSON的二進位協議, 支持JSON的所有類型的動態組合 支持的數據類型 基本支持的類型 "u8", "i8", "u16", "i16", "u32", "i32", "u64", "i64", "varint", "float", "s ...