main
SunChengyong 2026-05-20 16:36:30 +08:00
parent e174ec54d4
commit 92758032be
3 changed files with 452 additions and 21 deletions

View File

@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using UnityEngine;
public class DeviceIdHandle
@ -18,6 +19,8 @@ public class DeviceIdHandle
public string channel;
public string package_name;
public int platform;
public string identity_key;
public string caid_device_info_str;
public DeviceIdInfo()
{
@ -35,6 +38,7 @@ public class DeviceIdHandle
#else
platform = 0;
#endif
identity_key = SystemInfo.deviceUniqueIdentifier;
}
}
@ -196,4 +200,27 @@ public class DeviceIdHandle
}
#endif
}
/// <summary>
/// 获取 CAID 设备信息 JSONiOS 侧采集,返回所有参数的 JSON 字符串)
/// </summary>
public string GetCAIDDeviceInfoJSON()
{
#if UNITY_IOS && !UNITY_EDITOR
IntPtr ptr = GetCAIDDeviceInfoJSONNative();
if (ptr == IntPtr.Zero)
return string.Empty;
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
return result ?? string.Empty;
#else
// 非 iOS 平台返回空 JSON可用于编辑器调试
return string.Empty;
#endif
}
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal", EntryPoint = "GetCAIDDeviceInfoJSON")]
private static extern IntPtr GetCAIDDeviceInfoJSONNative();
#endif
}

View File

@ -2,6 +2,11 @@
#import <AdSupport/AdSupport.h>
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <UIKit/UIKit.h>
#import <sys/sysctl.h>
#import <sys/stat.h>
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <CoreTelephony/CTCarrier.h>
#import <CommonCrypto/CommonCrypto.h>
#ifdef __cplusplus
extern "C" {
@ -12,15 +17,274 @@ void UnitySendMessage(const char* obj, const char* method, const char* msg);
// 辅助方法:将 NSString 转换为 C 语言字符串
const char* ConvertNSStringToCString(NSString* nsString) {
if (nsString == NULL) {
if (nsString == NULL || nsString == nil) {
return NULL;
}
const char* nsStringUtf8 = [nsString UTF8String];
if (nsStringUtf8 == NULL) {
return NULL;
}
char* cString = (char*)malloc(strlen(nsStringUtf8) + 1);
strcpy(cString, nsStringUtf8);
return cString;
}
// ==========================================
// CAID 设备信息采集(参见 CAID API 接入文档 v1.17
// ==========================================
#pragma mark - 6.1.1 设备启动时间
static time_t bootSecTime() {
struct timeval boottime;
size_t len = sizeof(boottime);
int mib[2] = { CTL_KERN, KERN_BOOTTIME };
if (sysctl(mib, 2, &boottime, &len, NULL, 0) < 0) {
return 0;
}
return boottime.tv_sec;
}
NSString* GetBootTimeInSec() {
return [NSString stringWithFormat:@"%ld", bootSecTime()];
}
#pragma mark - 6.1.2 国家
NSString* GetCountryCode() {
NSLocale *locale = [NSLocale currentLocale];
NSString *countryCode = [locale objectForKey:NSLocaleCountryCode];
return countryCode ?: @"";
}
#pragma mark - 6.1.3 语言
NSString* GetLanguage() {
NSString *language;
NSLocale *locale = [NSLocale currentLocale];
if ([[NSLocale preferredLanguages] count] > 0) {
language = [[NSLocale preferredLanguages] objectAtIndex:0];
} else {
language = [locale objectForKey:NSLocaleLanguageCode];
}
return language ?: @"";
}
#pragma mark - MD5 辅助函数(32位小写)
NSString* MD5HexDigest(NSString *input) {
if (!input || [input length] == 0) return @"";
const char *cStr = [input UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (CC_LONG)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for (int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x", digest[i]];
}
return output;
}
#pragma mark - 6.1.4 设备名称(小写 MD5
NSString* GetCAIDDeviceName() {
NSString *deviceName = [[UIDevice currentDevice] name];
if ([deviceName length] == 0) {
return nil;
}
return MD5HexDigest(deviceName);
}
#pragma mark - 6.1.5 系统版本
NSString* GetSystemVersion() {
return [[UIDevice currentDevice] systemVersion];
}
#pragma mark - 6.1.6 设备 Machine
static NSString* getSystemHardwareByName(const char *typeSpecifier) {
size_t size;
sysctlbyname(typeSpecifier, NULL, &size, NULL, 0);
char *answer = malloc(size);
if (!answer) return @"";
sysctlbyname(typeSpecifier, answer, &size, NULL, 0);
NSString *results = [NSString stringWithUTF8String:answer];
free(answer);
return results ?: @"";
}
NSString* GetMachine() {
NSString *machine = getSystemHardwareByName("hw.machine");
return machine ?: @"";
}
#pragma mark - 6.1.7 运营商
NSString* GetCarrierInfo() {
#if TARGET_IPHONE_SIMULATOR
return @"SIMULATOR";
#else
static dispatch_queue_t _queue;
static dispatch_once_t once;
dispatch_once(&once, ^{
_queue = dispatch_queue_create("com.carr.devicehelper", NULL);
});
__block NSString *carr = nil;
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(_queue, ^{
CTTelephonyNetworkInfo *info = [[CTTelephonyNetworkInfo alloc] init];
CTCarrier *carrier = nil;
if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 12.1) {
if ([info respondsToSelector:@selector(serviceSubscriberCellularProviders)]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
NSArray *carrierKeysArray = [info.serviceSubscriberCellularProviders.allKeys
sortedArrayUsingSelector:@selector(compare:)];
carrier = info.serviceSubscriberCellularProviders[carrierKeysArray.firstObject];
if (!carrier.mobileNetworkCode) {
carrier = info.serviceSubscriberCellularProviders[carrierKeysArray.lastObject];
}
#pragma clang diagnostic pop
}
}
if (!carrier) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
carrier = info.subscriberCellularProvider;
#pragma clang diagnostic pop
}
if (carrier != nil) {
NSString *networkCode = [carrier mobileNetworkCode];
NSString *countryCode = [carrier mobileCountryCode];
if (countryCode && [countryCode isEqualToString:@"460"] && networkCode) {
if ([networkCode isEqualToString:@"00"] ||
[networkCode isEqualToString:@"02"] ||
[networkCode isEqualToString:@"07"] ||
[networkCode isEqualToString:@"08"]) {
carr = @"中国移动";
} else if ([networkCode isEqualToString:@"01"] ||
[networkCode isEqualToString:@"06"] ||
[networkCode isEqualToString:@"09"]) {
carr = @"中国联通";
} else if ([networkCode isEqualToString:@"03"] ||
[networkCode isEqualToString:@"05"] ||
[networkCode isEqualToString:@"11"]) {
carr = @"中国电信";
} else if ([networkCode isEqualToString:@"04"]) {
carr = @"中国卫通";
} else if ([networkCode isEqualToString:@"20"]) {
carr = @"中国铁通";
}
} else {
carr = [carrier.carrierName copy];
}
}
if (carr.length <= 0) {
carr = @"unknown";
}
dispatch_semaphore_signal(semaphore);
});
dispatch_time_t t = dispatch_time(DISPATCH_TIME_NOW, 0.5 * NSEC_PER_SEC);
dispatch_semaphore_wait(semaphore, t);
return [carr copy] ?: @"unknown";
#endif
}
#pragma mark - 6.1.8 物理内存容量
NSString* GetMemory() {
return [NSString stringWithFormat:@"%lld", [NSProcessInfo processInfo].physicalMemory];
}
#pragma mark - 6.1.9 硬盘容量
NSString* GetDisk() {
int64_t space = -1;
NSError *error = nil;
NSDictionary *attrs = [[NSFileManager defaultManager]
attributesOfFileSystemForPath:NSHomeDirectory() error:&error];
if (!error) {
space = [[attrs objectForKey:NSFileSystemSize] longLongValue];
}
return [NSString stringWithFormat:@"%lld", space];
}
#pragma mark - 6.1.10 系统更新时间
NSString* GetSysFileTime() {
NSString *result = nil;
// Base64 解码: /var/mobile/Library/UserConfigurationProfiles/PublicInfo/MCMeta.plist
NSString *information = @"L3Zhci9tb2JpbGUvTGlicmFyeS9Vc2VyQ29uZmlndXJhdGlvblByb2ZpbGVzL1B1YmxpY0luZm8vTUNNZXRhLnBsaXN0";
NSData *data = [[NSData alloc] initWithBase64EncodedString:information options:0];
NSString *dataString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSError *error = nil;
NSDictionary *fileAttributes = [[NSFileManager defaultManager]
attributesOfItemAtPath:dataString error:&error];
if (fileAttributes) {
id singleAttribute = [fileAttributes objectForKey:NSFileCreationDate];
if ([singleAttribute isKindOfClass:[NSDate class]]) {
NSDate *dataDate = (NSDate *)singleAttribute;
result = [NSString stringWithFormat:@"%f", [dataDate timeIntervalSince1970]];
}
}
return result ?: @"";
}
#pragma mark - 6.1.11 设备 Model
NSString* GetModel() {
NSString *model = getSystemHardwareByName("hw.model");
return model ?: @"";
}
#pragma mark - 6.1.12 时区
NSString* GetTimeZone() {
NSInteger offset = [NSTimeZone systemTimeZone].secondsFromGMT;
return [NSString stringWithFormat:@"%ld", (long)offset];
}
#pragma mark - 6.1.13 设备初始化时间
NSString* GetDeviceInitTime() {
struct stat info;
int result = stat("/var/mobile", &info);
if (result != 0) {
return @"";
}
struct timespec time = info.st_birthtimespec;
return [NSString stringWithFormat:@"%ld.%09ld", time.tv_sec, time.tv_nsec];
}
// ==========================================
// Unity C# 统一调用接口:返回 CAID 设备信息的 JSON 字符串
// ==========================================
const char* GetCAIDDeviceInfoJSON() {
NSMutableDictionary *deviceInfo = [NSMutableDictionary dictionary];
deviceInfo[@"bootTimeInSec"] = GetBootTimeInSec() ?: @"";
deviceInfo[@"countryCode"] = GetCountryCode() ?: @"";
deviceInfo[@"language"] = GetLanguage() ?: @"";
deviceInfo[@"deviceName"] = GetCAIDDeviceName() ?: @"";
deviceInfo[@"systemVersion"] = GetSystemVersion() ?: @"";
deviceInfo[@"machine"] = GetMachine() ?: @"";
deviceInfo[@"carrierInfo"] = GetCarrierInfo() ?: @"";
deviceInfo[@"memory"] = GetMemory() ?: @"";
deviceInfo[@"disk"] = GetDisk() ?: @"";
deviceInfo[@"sysFileTime"] = GetSysFileTime() ?: @"";
deviceInfo[@"model"] = GetModel() ?: @"";
deviceInfo[@"timeZone"] = GetTimeZone() ?: @"";
deviceInfo[@"deviceInitTime"] = GetDeviceInitTime() ?: @"";
NSError *error = nil;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:deviceInfo options:0 error:&error];
if (error || !jsonData) {
return ConvertNSStringToCString(@"");
}
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
return ConvertNSStringToCString(jsonString);
}
// ==========================================
// IDFA / IDFV (原有方法保留)
// ==========================================
// Unity 调用:请求/拉取 IDFA (保留此方法供 Unity 安全拉取数据)
void RequestIDFA() {
dispatch_async(dispatch_get_main_queue(), ^{

View File

@ -1,30 +1,170 @@
# DeviceIdentifiers.m
# DeviceHelper_iOS
用于向 iOS (Xcode) 原生层请求获取设备广告标识符 (IDFA) 与供应商标识符 (IDFV) 的 Unity 桥接脚本。
Unity iOS 原生桥接插件,提供以下功能:
1. **IDFA** — 广告标识符(异步获取,含自动弹窗机制)
2. **IDFV** — 供应商标识符(同步获取)
3. **CAID 设备信息采集** — 采集 iOS 设备信息,以 JSON 字符串形式返回给 Unity C#
---
## 文件说明
| 文件 | 路径 |
|------|------|
| 源文件 | `Assets/Plugins/iOS/DeviceIdentifiers.m` |
| C# 桥接 | 项目 `Assets/PhxhSDK/Phxh/Others/DeviceHelper.cs` |
---
## Unity C# 调用方式
### 1. 获取 IDFA异步
```csharp
// IDFA 为异步获取,结果会通过 UnitySendMessage 回调到 GameLauncher.OnIDFAResult
// 主动触发请求:
// (代码中已实现 App 启动时自动弹窗,无需额外调用)
// 若需手动调用:
// (在 iOS 原生侧已暴露 RequestIDFA() 函数,可通过 DllImport 调用)
```
> **回调方式**:结果通过 `UnitySendMessage("GameLauncher", "OnIDFAResult", idfaString)` 发送。
> 确保 Unity 场景中存在名为 **GameLauncher** 的 GameObject并挂载了 `OnIDFAResult(string idfa)` 方法。
### 2. 获取 IDFV同步
```csharp
using System.Runtime.InteropServices;
public static string GetIDFV()
{
#if UNITY_IOS && !UNITY_EDITOR
IntPtr ptr = GetIDFVNative();
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
return result;
#else
return "";
#endif
}
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal", EntryPoint = "GetIDFV")]
private static extern IntPtr GetIDFVNative();
#endif
```
### 3. 获取 CAID 设备信息 JSON同步
```csharp
// 直接调用 DeviceHelper.GetCAIDDeviceInfoJSON()
// 返回 JSON 字符串,包含 CAID API 所需的全部设备参数
string caidJson = DeviceHelper.GetCAIDDeviceInfoJSON();
```
返回的 JSON 示例:
```json
{
"bootTimeInSec": "1595643553",
"countryCode": "CN",
"language": "zh-Hans-CN",
"deviceName": "e910dddb2748c36b47fcde5dd720eec1",
"systemVersion": "14.0",
"machine": "iPhone10,3",
"carrierInfo": "中国移动",
"memory": "3955589120",
"disk": "63900340224",
"sysFileTime": "1595214620.383940",
"model": "D22AP",
"timeZone": "28800",
"deviceInitTime": "1632467920.301150749"
}
```
各字段说明参见 [CAID API 接入文档](https://caid.china-caa.org) 附录 6.1 章节。
#### C# 侧方法定义(已内置在 `DeviceHelper.cs` 中)
```csharp
public static string GetCAIDDeviceInfoJSON()
{
#if UNITY_IOS && !UNITY_EDITOR
IntPtr ptr = GetCAIDDeviceInfoJSONNative();
if (ptr == IntPtr.Zero)
return "{}";
string result = Marshal.PtrToStringAuto(ptr);
Marshal.FreeCoTaskMem(ptr);
return result ?? "{}";
#else
return "{}";
#endif
}
#if UNITY_IOS && !UNITY_EDITOR
[DllImport("__Internal", EntryPoint = "GetCAIDDeviceInfoJSON")]
private static extern IntPtr GetCAIDDeviceInfoJSONNative();
#endif
```
---
## 注意点
- IDFA (广告标识符) —— 异步获取
*授权弹窗:获取 IDFA 必须调起 iOS 系统原生的 AppTrackingTransparency 弹窗,只有在用户主动允许追踪后才可获取真实 IDFA。
* 异步回调:由于需要等待用户交互,此过程为异步操作。
* 回调目标:获取结果会通过 UnitySendMessage 传递给 Unity。请务必确保 Unity 场景中存在名为 GameLauncher 的 GameObject并已挂载接收回调的方法。
### IDFA
- IDFV (供应商标识符) —— 同步获取
* 直接获取IDFV 为设备厂商 ID无需经过用户授权弹窗可直接在调用时同步获取并返回结果。
- **授权弹窗**:获取 IDFA 必须调起 iOS 系统原生的 AppTrackingTransparency 弹窗,只有在用户主动允许追踪后才可获取真实 IDFA。
- **异步回调**:由于需要等待用户交互,此过程为异步操作。
- **回调目标**:获取结果会通过 `UnitySendMessage` 传递给 Unity。请务必确保 Unity 场景中存在名为 `GameLauncher` 的 GameObject并已挂载接收回调的方法。
- 自动触发机制 (App 启动即弹窗)
* 默认行为:代码内实现了 onAppDidBecomeActive 监听。App 每次启动并进入活跃状态时,将自动调起 IDFA 请求弹窗。
* 修改建议:若项目业务逻辑不需要在 App 刚启动时就弹出授权框(例如需要延迟到特定关卡或场景再弹),请在源码中手动删除或注释掉 onAppDidBecomeActive 相关的监听与函数体。
### IDFV
## Xcode 工程配置须知 (重要)
- 添加系统库:
在 Xcode -> Target -> Build Phases -> Link Binary With Libraries 中,添加以下框架:
* AdSupport.framework
* AppTrackingTransparency.framework
- **直接获取**IDFV 为设备厂商 ID无需经过用户授权弹窗可直接在调用时同步获取并返回结果。
- 配置隐私描述:
在 Info.plist 中必须添加跟踪权限的用途描述,否则弹窗无法显示且会导致应用崩溃或拒审:
* Key: Privacy - Tracking Usage Description (NSUserTrackingUsageDescription)
* Value: (请填写向用户解释为何需要追踪的文案)
### CAID 设备信息
- **同步接口**:所有设备参数的采集均在当前线程同步完成,返回 JSON 字符串。
- **注意内存释放**C# 侧在 `Marshal.PtrToStringAuto` 后调用 `Marshal.FreeCoTaskMem` 释放原生侧 `malloc` 分配的内存。
### 自动触发机制IDFA 弹窗)
- **默认行为**:代码内实现了 `onAppDidBecomeActive` 监听。App 每次启动并进入活跃状态时,将自动调起 IDFA 请求弹窗。
- **修改建议**:若项目业务逻辑不需要在 App 刚启动时就弹出授权框(例如需要延迟到特定关卡或场景再弹),请在源码中手动删除或注释掉 `AutoIDFATrigger` 相关的 `+load``onAppDidBecomeActive` 实现。
---
## Xcode 工程配置须知(重要)
- **添加系统库**
在 Xcode -> Target -> Build Phases -> Link Binary With Libraries 中,添加以下框架:
| 框架 | 用途 |
|------|------|
| `AdSupport.framework` | IDFA 获取 |
| `AppTrackingTransparency.framework` | 跟踪授权弹窗 |
| `CoreTelephony.framework` | 运营商信息采集CAID |
| `libz.tbd` | MD5 计算CAID |
- **配置隐私描述**
`Info.plist` 中必须添加跟踪权限的用途描述,否则弹窗无法显示且会导致应用崩溃或拒审:
- **Key**: `Privacy - Tracking Usage Description` (`NSUserTrackingUsageDescription`)
- **Value**: 请填写向用户解释为何需要追踪的文案(如"用于提供个性化广告服务"
- **Unity 构建时自动添加框架**
若使用 Unity 2020.3+,可在 `Player Settings -> iOS -> Other Settings` 中勾选:
- `AdSupport.framework` → **Required**
- `AppTrackingTransparency.framework` → **Required**
- `CoreTelephony.framework` → **Required**
或在项目中添加 `TDS-Info.plist` 等相关配置(`Assets/Plugins/iOS/TDS-Info.plist`)。
---
## 版本历史
| 版本 | 日期 | 说明 |
|------|------|------|
| 1.0.0 | - | 初始版本IDFA / IDFV 获取 + 自动弹窗 + CAID 设备信息采集 |