主頁 > 移動端開發 > iOS全埋點解決方案-用戶標識

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

2022-04-30 10:04:27 移動端開發

前言

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

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

一、登陸之前

業界一般使用 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 = https://www.cnblogs.com/r360/archive/2022/04/27/[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"
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/468099.html

標籤:其他

上一篇:iOS全埋點解決方案-用戶標識

下一篇:【直播回顧】OpenHarmony知識賦能第五期第二課——如何成為社區貢獻達人

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more