iOS中的私有唯一设备标识符

34

我和同事们正在开发一个项目,需要使用许多私有的非官方代码。这个项目不是为AppStore使用。

我们唯一的要求是不要使用越狱(jailbreak)。

首先,UDIDOpenUDID或其他任何解决方案在此处无效,也没有预期会有效。

我们进行了大量的背景研究和测试,从尝试以编程方式获取IMEIICCID、IMSI和序列号开始。以上方法在iOS 7及以上版本没有越狱的情况下都不起作用。

我们还花了几个月时间使用著名的IOKitBrowser和转储整个iOS内部的内容来研究IOKit框架。不幸的是,我们发现iOS 8.3上它停止工作了。

我们在这里讨论的不是获取UDID或任何其他“主流”东西,而是总的来说我们需要一种方法来获取

任何足够唯一以标识设备并且在设备擦除和iOS版本更改后仍然存在的永久硬件标识符。

这个问题与其他问题不重复(例如这里没有找到任何解决方案),并且仅针对私有API

非常感谢任何帮助。


1
可能是UIDevice uniqueIdentifier Deprecated - What To Do Now?的重复问题。 - Paulw11
你能获取唯一的设备标识符的唯一途径是通过MDM路线 - 你需要在设备上安装设备管理配置文件,然后你的服务器就可以访问UDID。 - Paulw11
@dandan78 它到底出了什么问题? - Sergey Grischyov
这个问题听起来像是越狱社区可以很容易地在IRC或Twitter上回答的。无论如何,我很快就会发布一个答案。 - gbuzogany
@gbuzogany 先生,您这样做真的非常有帮助。 - Sergey Grischyov
显示剩余3条评论
8个回答

17

经过一番搜索,我发现所有私有API都使用libMobileGestalt来获取任何硬件标识符,而libMobileGestalt又使用IOKitMobileGestalt会检查当前pid的沙盒规则,并寻找com.apple.private.MobileGestalt.AllowedProtectedKeys授权。

请看下面的代码:

signed int __fastcall sub_2EB8803C(int a1, int a2, int a3, int a4)
{
  int v4; // r5@1
  int v5; // r4@1
  int v6; // r10@1
  int v7; // r2@1
  int v8; // r0@3
  int v9; // r6@3
  int v10; // r11@4
  int v11; // r4@4
  int v12; // r0@5
  signed int v13; // r6@6
  int v14; // r6@7
  char *v15; // r0@7
  int v16; // r1@7
  int v17; // r1@14
  int v18; // r3@16
  int v19; // r5@16
  signed int v20; // r1@17
  int v21; // r0@17
  __CFString *v22; // r2@19
  int v23; // r4@27
  __CFString *v24; // r2@27
  int v26; // [sp+8h] [bp-428h]@1
  char v27; // [sp+10h] [bp-420h]@1
  int v28; // [sp+414h] [bp-1Ch]@1

  v26 = a2;
  v4 = a1;
  v5 = a3;
  v6 = a4;
  v28 = __stack_chk_guard;
  memset(&v27, 0, 0x401u);
  v7 = *(_DWORD *)(dword_32260254 + 260);
  if ( !v7 )
    v7 = sub_2EB8047C(65, 2);
  v8 = ((int (__fastcall *)(int, _DWORD))v7)(v4, "com.apple.private.MobileGestalt.AllowedProtectedKeys");
  v9 = v8;
  if ( !v8 )
    goto LABEL_12;
  v10 = v5;
  v11 = CFGetTypeID(v8);
  if ( v11 != CFArrayGetTypeID() )
  {
    v14 = (int)"/SourceCache/MobileGestalt/MobileGestalt-297.1.14/MobileGestalt.c";
    v15 = rindex("/SourceCache/MobileGestalt/MobileGestalt-297.1.14/MobileGestalt.c", 47);
    v16 = *(_DWORD *)(dword_32260254 + 288);
    if ( v15 )
      v14 = (int)(v15 + 1);
    if ( !v16 )
      v16 = sub_2EB8047C(72, 2);
    ((void (__fastcall *)(int))v16)(v4);
    _MGLog(3, v14);
LABEL_12:
    v13 = 0;
    goto LABEL_13;
  }
  v12 = CFArrayGetCount(v9);
  if ( CFArrayContainsValue(v9, 0, v12, v26) )
    v13 = 1;
  else
    v13 = sub_2EB7F948(v9, v26, v10, "MGCopyAnswer");
LABEL_13:
  if ( !v6 )
    goto LABEL_30;
  v17 = *(_DWORD *)(dword_32260254 + 288);
  if ( !v17 )
    v17 = sub_2EB8047C(72, 2);
  v19 = ((int (__fastcall *)(int))v17)(v4);
  if ( v13 != 1 )
  {
    v21 = *(_DWORD *)v6;
    if ( *(_DWORD *)v6 )
    {
      v22 = CFSTR(" and IS NOT appropriately entitled");
      goto LABEL_22;
    }
    v23 = CFStringCreateMutable(0, 0);
    *(_DWORD *)v6 = v23;
    sub_2EB7F644(v19, &v27);
    v24 = CFSTR("pid %d (%s) IS NOT appropriately entitled to fetch %@");
    goto LABEL_29;
  }
  v20 = MGGetBoolAnswer((int)CFSTR("LBJfwOEzExRxzlAnSuI7eg"));
  v21 = *(_DWORD *)v6;
  if ( v20 == 1 )
  {
    if ( v21 )
    {
      v22 = CFSTR(" but IS appropriately entitled; all is good in the world");
LABEL_22:
      CFStringAppendFormat(v21, 0, v22, v18);
      goto LABEL_30;
    }
    v23 = CFStringCreateMutable(0, 0);
    *(_DWORD *)v6 = v23;
    sub_2EB7F644(v19, &v27);
    v24 = CFSTR("pid %d (%s) IS appropriately entitled to fetch %@; all is good in the world");
LABEL_29:
    CFStringAppendFormat(v23, 0, v24, v19);
    goto LABEL_30;
  }
  if ( v21 )
  {
    CFRelease(v21);
    *(_DWORD *)v6 = 0;
  }
  *(_DWORD *)v6 = 0;
LABEL_30:
  if ( __stack_chk_guard != v28 )
    __stack_chk_fail(__stack_chk_guard - v28);
  return v13;
}

signed int __fastcall sub_2EB88228(int a1, int a2, int a3)
{
  int v3; // r4@1
  int v4; // r10@1
  int v5; // r0@1
  int v6; // r6@1
  int v7; // r5@5
  signed int result; // r0@6
  char v9; // [sp+8h] [bp-420h]@5
  int v10; // [sp+40Ch] [bp-1Ch]@1

  v3 = a1;
  v4 = a3;
  v10 = __stack_chk_guard;
  v5 = sandbox_check();
  v6 = v5;
  if ( v5 )
    v5 = 1;
  if ( v4 && v5 == 1 )
  {
    memset(&v9, 0, 0x401u);
    v7 = CFStringCreateMutable(0, 0);
    *(_DWORD *)v4 = v7;
    sub_2EB7F644(v3, &v9);
    CFStringAppendFormat(v7, 0, CFSTR("pid %d (%s) does not have sandbox access for %@"), v3);
  }
  result = 0;
  if ( !v6 )
    result = 1;
  if ( __stack_chk_guard != v10 )
    __stack_chk_fail(result);
  return result;
}

此处所述,UDID的计算方法如下:

UDID = SHA1(serial + ECID + wifiMac + bluetoothMac)

MobileGestalt通过以下方式从IOKit获取这些值:

CFMutableDictionaryRef service = IOServiceMatching("IOPlatformExpertDevice");
    io_service_t ioservice = IOServiceGetMatchingService(kIOMasterPortDefault, service);
    CFTypeRef entry = IORegistryEntryCreateCFProperty(ioservice, CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);

    const UInt8 * data = CFDataGetBytePtr(entry);
    CFStringRef string = CFStringCreateWithCString(kCFAllocatorDefault, data, kCFStringEncodingUTF8);

如果您试图自己操作,将会失败,因为iOS 8.3中的新沙盒规则非常严格,拒绝访问所有硬件标识符,就像这样:

deny iokit-get-properties IOPlatformSerialNumber

可能的解决方案

看起来获取UDID的唯一方法是:

  1. 在应用程序中启动一个包含两个页面的Web服务器:其中一个页面应返回特殊制作的MobileConfiguration配置文件,另一个页面应收集UDID。更多信息请参见此处此处此处
  2. 您从应用程序内部在移动Safari中打开第一个页面,并重定向到Settings.app请求安装配置文件。安装配置文件后,UDID将发送到第二个Web页面,您可以从应用程序内部访问它。(Settings.app具有所有必要的权限和不同的沙盒规则)。

已确认的解决方案

这里是基于RoutingHTTPServer的示例:

import UIKit
import RoutingHTTPServer

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var bgTask = UIBackgroundTaskInvalid
    let server = HTTPServer()

    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        application.openURL(NSURL(string: "http://localhost:55555")!)
        return true
    }

    func applicationDidEnterBackground(application: UIApplication) {
        bgTask = application.beginBackgroundTaskWithExpirationHandler() {
            dispatch_async(dispatch_get_main_queue()) {[unowned self] in
                application.endBackgroundTask(self.bgTask)
                self.bgTask = UIBackgroundTaskInvalid
            }
        }
    }
}

class HTTPServer: RoutingHTTPServer {
    override init() {
        super.init()
        setPort(55555)
        handleMethod("GET", withPath: "/") {
            $1.setHeader("Content-Type", value: "application/x-apple-aspen-config")
            $1.respondWithData(NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("udid", ofType: "mobileconfig")!)!)
        }
        handleMethod("POST", withPath: "/") {
            let raw = NSString(data:$0.body(), encoding:NSISOLatin1StringEncoding) as! String
            let plistString = raw.substringWithRange(Range(start: raw.rangeOfString("<?xml")!.startIndex,end: raw.rangeOfString("</plist>")!.endIndex))
            let plist = NSPropertyListSerialization.propertyListWithData(plistString.dataUsingEncoding(NSISOLatin1StringEncoding)!, options: .allZeros, format: nil, error: nil) as! [String:String]

            let udid = plist["UDID"]! 
            println(udid) // Here is your UDID!

            $1.statusCode = 200
            $1.respondWithString("see https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/ConfigurationProfileExamples/ConfigurationProfileExamples.html")
        }
        start(nil)
    }
}

以下是 udid.mobileconfig 文件的内容:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>PayloadContent</key>
        <dict>
            <key>URL</key>
            <string>http://localhost:55555</string>
            <key>DeviceAttributes</key>
            <array>
                <string>IMEI</string>
                <string>UDID</string>
                <string>PRODUCT</string>
                <string>VERSION</string>
                <string>SERIAL</string>
            </array>
        </dict>
        <key>PayloadOrganization</key>
        <string>udid</string>
        <key>PayloadDisplayName</key>
        <string>Get Your UDID</string>
        <key>PayloadVersion</key>
        <integer>1</integer>
        <key>PayloadUUID</key>
        <string>9CF421B3-9853-9999-BC8A-982CBD3C907C</string>
        <key>PayloadIdentifier</key>
        <string>udid</string>
        <key>PayloadDescription</key>
        <string>Install this temporary profile to find and display your current device's UDID. It is automatically removed from device right after you get your UDID.</string>
        <key>PayloadType</key>
        <string>Profile Service</string>
    </dict>
</plist>

安装配置文件会失败(我没有实现预期的响应,请参考文档),但是应用程序将获得正确的UDID。您还应该签署mobileconfig文件


1
从未想过可以按照你所描述的方式设置计划。我们会去验证一下,谢谢。 - Sergey Grischyov
1
经过数周的调查,我回来确认所有现有的解决方案都需要运行php脚本来收集数据。我们已经配置了本地iOS服务器和配置文件本身。它可以安装并且甚至可以重定向回我们内置的服务器,但是它要么需要一个php脚本,要么返回一些p7s文件,或者完全需要HTTPS。而且我不确定任何服务器解决方案是否知道如何处理php。如果您能提供帮助,我准备开启一个新的问题,并提供500的赏金。我们看到可能没有人曾经解决过这样的任务,我们需要任何帮助。谢谢。 - Sergey Grischyov
@SergiusGee p7s 文件是一个包含 UDID 的已签名 plist。我已经更新了我的答案,其中包含 UDID 提取技术的代码。 - bzz
这是我在这个网站上看到的最好的答案之一。 - undefined

16

很抱歉地说,从 iOS 8.3 开始,要获取任何唯一标识符,你需要比普通用户更高的访问级别。

没有利用任何东西,仅使用私有框架、库和内核请求,任何对唯一标识符的请求都会返回 null。

举个例子:

尝试使用 IOKit:

void *IOKit = dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", RTLD_NOW);
if (IOKit)
{
    mach_port_t *kIOMasterPortDefault = dlsym(IOKit, "kIOMasterPortDefault");
    CFMutableDictionaryRef (*IOServiceMatching)(const char *name) = dlsym(IOKit, "IOServiceMatching");
    mach_port_t (*IOServiceGetMatchingService)(mach_port_t masterPort, CFDictionaryRef matching) = dlsym(IOKit, "IOServiceGetMatchingService");
    CFTypeRef (*IORegistryEntryCreateCFProperty)(mach_port_t entry, CFStringRef key, CFAllocatorRef allocator, uint32_t options) = dlsym(IOKit, "IORegistryEntryCreateCFProperty");
    kern_return_t (*IOObjectRelease)(mach_port_t object) = dlsym(IOKit, "IOObjectRelease");

    if (kIOMasterPortDefault && IOServiceGetMatchingService && IORegistryEntryCreateCFProperty && IOObjectRelease)
    {
        mach_port_t platformExpertDevice = IOServiceGetMatchingService(*kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
        if (platformExpertDevice)
        {
            CFTypeRef platformSerialNumber = IORegistryEntryCreateCFProperty(platformExpertDevice, CFSTR("IOPlatformSerialNumber"), kCFAllocatorDefault, 0);
            if (platformSerialNumber && CFGetTypeID(platformSerialNumber) == CFStringGetTypeID())
            {
                serialNumber = [NSString stringWithString:(__bridge NSString *)platformSerialNumber];
                CFRelease(platformSerialNumber);
            }
            IOObjectRelease(platformExpertDevice);
        }
    }
    dlclose(IOKit);
}

失败。原因:IOPlatformSerialNumber不可访问。许多其他请求正常工作。

尝试使用 Mach 调用获取网络适配器的硬件 ID:

int         mib[6], len;
char            *buf;
unsigned char       *ptr;
struct if_msghdr    *ifm;
struct sockaddr_dl  *sdl;

mib[0] = CTL_NET;
mib[1] = AF_ROUTE;
mib[2] = 0;
mib[3] = AF_LINK;
mib[4] = NET_RT_IFLIST;
if ((mib[5] = if_nametoindex("en0")) == 0) {
    perror("if_nametoindex error");
    exit(2);
}

if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) {
    perror("sysctl 1 error");
    exit(3);
}

if ((buf = malloc(len)) == NULL) {
    perror("malloc error");
    exit(4);
}

if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {
    perror("sysctl 2 error");
    exit(5);
}

ifm = (struct if_msghdr *)buf;
sdl = (struct sockaddr_dl *)(ifm + 1);
ptr = (unsigned char *)LLADDR(sdl);
printf("%02x:%02x:%02x:%02x:%02x:%02x\n", *ptr, *(ptr+1), *(ptr+2),
       *(ptr+3), *(ptr+4), *(ptr+5));

失败。原因:对于任何网络适配器,返回02:00:00:00:00:00

尝试连接到lockdownd:

void *libHandle = dlopen("/usr/lib/liblockdown.dylib", RTLD_LAZY);
if (libHandle)
{
    lockdown_connect = dlsym(libHandle, "lockdown_connect");
    lockdown_copy_value = dlsym(libHandle, "lockdown_copy_value");

    id connection = lockdown_connect();
    NSString *kLockdownDeviceColorKey
    NSString *color = lockdown_copy_value(connection, nil, kLockdownDeviceColorKey);
    NSLog(@"color = %@", color);
    lockdown_disconnect(connection);

    dlclose(libHandle);
}
else {
    printf("[%s] Unable to open liblockdown.dylib: %s\n",
           __FILE__, dlerror());
}

失败。原因:lockdown_connect() 函数调用失败,返回 null

尝试使用 libMobileGestalt 库:

void *libHandle = dlopen("/usr/lib/libMobileGestalt.dylib", RTLD_LAZY);
if (libHandle)
{
    MGCopyAnswer = dlsym(libHandle, "MGCopyAnswer");

    NSString* value = MGCopyAnswer(CFSTR("SerialNumber"));
    NSLog(@"Value: %@", value);
    CFRelease(value);
}

失败。原因:请求唯一标识符返回null。任何其他请求都可以正常工作。

我的建议是使用某些特权升级技术来获得超级用户访问权限,然后运行此处列出的任何方法来获取属性。

此外,对liblockdown进行扩展研究。如果可以在用户级别上访问它(使用除lockdown_connect之外的其他内容),可能可以读取这些内容。


哇,这确实是一项令人惊叹的研究!不过,我们能否不直接寻找UDID,而是寻找其他硬件ID,主要是任何唯一的硬件ID,比如制造日期 - 那将非常完美。你有什么想法吗? - Sergey Grischyov
我寻找任何唯一标识符,而不仅仅是UDID。显然,他们映射了任何唯一设备标识符并进行了覆盖。iOS 8.3带来了许多 安全更新,所以我认为它是以安全为重点的,因为它没有带来太多其他方面的更新。我寻找网络硬件标识符、序列号、制造日期、蓝牙minor/major ID、IMEI、基带ID等,但这些都被隐藏了起来。 - gbuzogany

3
你可以尝试直接访问libMobileGestalt.dylib,通过lockdownd API
头文件在这里。 访问UDID的基本代码应该是:(你仍然需要加载dylib)
CFStringRef udid = (CFStringRef)MGCopyAnswer(kMGUniqueDeviceID);

这里的内容(稍作修改)取自此处

如需了解有关libMobileGestalt的更多信息,请查看此处

如果这种方法失败了,您仍然可以尝试通过SSL套接字与lockdownd通信(请参见此处),不过我不知道它是如何工作的,但您可能会弄清楚。

正如您可能已经注意到的那样,这上面的所有东西都是几年前的。不过还是值得一试的。


哇,这个真棒。我会在iPad上查看并回复你的。谢谢。 - Sergey Grischyov
尝试使用MobileGestalt,但返回null。使用lockdown_connect()尝试连接到Lockdown也返回null。 - gbuzogany
@gbuzogany,您还需要授权才能使其工作,这将限制您只能在越狱设备上使用。 - Nate

2
实际上,我不知道这个解决方案是否有用。但是在移除UDID支持后,我通过使用供应商ID来管理唯一设备标识。以下是我们所做的:
当应用程序运行时,我会检查特定应用程序的供应商ID是否已经存储在钥匙链中。如果没有存储,则将该供应商ID存储在钥匙链中。因此,第二次当我的应用程序再次检查特定应用程序的供应商ID是否存储在钥匙链中时,如果存储,则从钥匙链中获取它并根据要求执行相同的操作。因此,这里的供应商ID始终是设备的唯一标识。 以下是我们通过供应商ID保持设备唯一性的步骤: 步骤1:将Lockbox集成到您的项目中。这将帮助您在钥匙链中存储/检索供应商ID。 步骤2:以下是执行检查供应商ID从钥匙链中检索供应商ID的代码。
-(NSString*)getidentifierForVendor{
    NSString *aStrExisting = [Lockbox stringForKey:[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleIdentifierKey]];

    if (aStrExisting == Nil) {
        NSString *aVendorID = [[[UIDevice currentDevice]identifierForVendor]UUIDString];
        aStrExisting=aVendorID;
        [Lockbox setString:aVendorID forKey:[[[NSBundle mainBundle] infoDictionary] objectForKey:(NSString*)kCFBundleIdentifierKey]];
        return aVendorID;
    }else{
        return aStrExisting;
    }

通过以上步骤,您始终可以获得设备的唯一性。因为密钥链永远不会被删除,它总是更新的。
希望这能帮助到您...

@SergiusGee,是的,我知道,但我们需要先检查特定“App ID”的供应商ID是否存在于密钥链中。如果存在,则无需使用新的供应商ID,只需从密钥链中获取初始存储的供应商ID即可。这样它就会成为应用程序 - >设备的唯一标识符。 - Jatin Patel - JP
当钥匙串被清除时会发生什么?那将是一个全新的标识符。 - Sergey Grischyov
我认为密钥链永远不会被擦除。它将会被添加或更新。 - Jatin Patel - JP
在完全清除设备、恢复出厂设置的情况下,它不会被擦除吗? - Paul Cezanne
有另一种方法可以使用“设备令牌”来获取唯一性,该令牌用于发送推送通知。您可以按照自己的方式使用它以找到唯一性。 - Jatin Patel - JP
显示剩余3条评论

2
您是否可以要求用户提供其MSISDN(国际电话号码)?就像WhatsApp在用户首次登录时所做的那样,通过向用户的msisdn发送SMS代码进行确认。如果您知道用户的MSISDN,则可以将它们保存在您的服务器数据库中,并仅允许白名单内的msisdn注册和使用您的服务。如果您想更安全地发送短信,则有一种方法可以从应用程序中了解SIM卡更改(我想这适用于T-Mobile和欧洲使用),以便用户无法通过输入不同MSISDN的SMS然后更改到他/她真正的MSISDN sim卡来愚弄您。
MSISDN是全球唯一的,并且由电信运营商保护,因此我认为这个解决方案非常安全。您觉得呢?祝好运。
更新:实际上,再次仔细阅读您的问题后,我认为您不希望用户登录任何信息,对此我感到抱歉给出了错误的答案:/
注意:为什么您不能使用HTML标签?
[[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString]

广告标识符是设备唯一且永久的,我猜测。不过我不知道你能否私下使用它。


2

我不确定你的完整意图,但是否仅仅在应用程序安装时让应用程序生成和存储您自己的唯一ID就足够了?然后,也许让应用程序发送该ID到服务器并存储它来自哪个IP。也许还可以有一些逻辑,定期让应用程序与服务器联系,以便您可以存储其他IP(如果它们发生变化)。显然,这并不是万无一失的,但这可能是解决方法的开端。


好主意,但是你如何保证随机选择的移动网络上的动态IP地址不会偶尔交叉?而且在重新安装应用程序后,应用程序将生成一个新的ID,而我仍然需要之前发送给我的唯一ID。 - Sergey Grischyov
我不能保证它是万无一失的,但根据您的意图,这可能已经足够了。您要防止/跟踪什么? - Rel
如果EIMI是您以前的选项之一,那么可以安全地说您所有的设备都将具备手机/移动功能,而不是iPod Touch。 - Rel

2
这是一个关于iOS设备唯一标识符的链接:https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIDevice_Class/#//apple_ref/occ/instp/UIDevice/identifierForVendor 这个属性返回一个字母数字字符串,它能够唯一地将设备与应用程序的供应商匹配。这个属性只读。
无论是从同一供应商在同一设备上运行的应用程序,还是来自不同供应商或不同设备的应用程序,都会返回不同的值。
自iOS 6以来一直存在,并且在iOS 8中仍然存在。
这满足了您对“任何永久硬件标识符”的要求,该标识符足够独特,可以在设备擦除和不同iOS版本之间持续存在。
据记录,这是每个设备唯一且持久的,无论是从应用商店还是企业交付。
通常情况下,供应商是由App Store提供的数据确定的。如果应用程序未从应用商店安装(例如企业应用程序和仍在开发中的应用程序),则基于应用程序的捆绑ID计算供应商标识符。该捆绑ID假定采用反向DNS格式。

不,它不是持久的。它可以手动重置,并且在设备擦除时会更改。 - Sergey Grischyov

2

不知道您是否感兴趣商业解决方案,可以看一下http://www.appsflyer.com

我与该公司没有关联,但我们在以前的雇主使用过他们的SDK。他们有一种设备指纹技术,确实可以运作。

注意:如果用户重置IDFA,则AppsFlyer将视其为新设备。然而,这已经有一段时间了,我记不清了,我认为您可以使用他们的SDK,不使用AdSupport.framework,然后他们将无法获取IDFA。所以,我猜测他们的设备指纹技术可能会生效。

他们还有竞争对手,可以搜索设备指纹技术。查看Yozio和Branch.io,他们都声称能做到这一点。我没有使用过他们的产品,只是看过他们的网站。


感谢您的建议,我们会仔细研究。 - Sergey Grischyov
如果非IDFA的技巧可行,请告诉我们。如果我记得正确,他们的文档在这方面有点不清楚。 - Paul Cezanne
我曾经参与过Branch.io iOS SDK的开发,并且对该领域中的其他公司也很了解。出于隐私保护的原因,所有人都只使用IDFA。我们不想让我们的应用合作伙伴冒险尝试任何过于冒险的事情。由于滥用IDFA的收集而被苹果随机拒绝的情况已经屡见不鲜,所以请大家小心谨慎。 - Alex Austin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接