当前位置: 首页 > news >正文

iOS设备信息详解

文章目录

    • ID 体系
    • iOS设备信息详解
      • IDFA
        • 介绍
        • 特点
        • IDFA新政
        • 前世今生
        • 获取方式
      • IDFV
        • 介绍
        • 获取方式
      • UUID
        • 介绍
        • 特点
        • 获取方式
      • UDID
        • 介绍
        • 获取方式
      • OpenUDID
        • 介绍
      • Bundle ID
        • 介绍
        • 分类
        • 其他
      • IP地址
        • 介绍
        • 获取方式
      • MAC地址
        • 介绍
        • 获取方式
            • 正常获取MAC地址
            • 获取对应Wi-Fi的MAC地址
      • 系统版本
        • 获取方式
      • 设备型号
        • 获取方式
      • 设备名称
        • 获取方式
      • 磁盘大小
        • 获取方式
      • 磁盘剩余空间
        • 获取方式
      • 电量
        • 获取方式
      • 电池状态
        • 获取方式
      • 屏幕尺寸
        • 获取方式
      • 屏幕亮度
        • 获取方式
      • 音量大小
        • 获取方式
      • Wifi名称
        • 获取方式
      • 网络制式
        • 获取方式
      • 是否越狱
        • 获取方式
      • 是否插入SIM卡
        • 获取方式
      • 是否允许推送
        • 获取方式
      • 剪切板内容
        • 获取方式
      • 是否使用代理
        • 获取方式
    • 附录1—Android主要设备信息
      • DeviceId(设备ID/DID)
        • 介绍
        • 补充知识
          • 国际移动设备识别码(IMEI)
          • 移动设备识别码(MEID )
        • 系统版本迭代带来的影响
        • 获取方式
        • 补充获取方式
      • ANDROID_ID
        • 介绍
        • 获取方式
      • AAID
      • OAID(匿名设备标识符)
    • 附录2—Android 系统名字、版本、API level的对应关系

ID 体系

所谓的ID体系就是在我们想要追踪一个用户就必须先找到用户,在这个过程中,标识符(ID)就像我们的另一张身份证,它们就代表了数字化之后的你和我。

不同 App 可能通过某些唯一标识符对你进行强制跟踪,广告平台则会通过这个唯一标识符对你进行用户画像描绘,进而共享给相关 App 及其后台,一旦你打开了其中的某个 App,那么你就会被识别到(你点了什么、看过什么、可能需要什么),它们比你自己都清楚。

而在智能设备的 ID 体系中存在许许多多不同种类的标识符,本篇文章我们就简单介绍一下这些标识符。

iOS设备信息详解

file

IDFA

介绍

IDFA全称为Identifier for Advertising(广告标识符),在同一个设备上的所有App都会取到相同的值,用于给开发者跟踪广告效果用的,可看作是iPhone 的设备临时身份证,说是临时身份证是因为它允许用户更换。

特点

以下方式都可以改变IDFA

  • 重置系统
  • 设置程序 -> 通用 -> 还原 -> 还原位置与隐私
  • 设置程序-> 通用 -> 关于本机 -> 广告 -> 还原广告标示符 -> 重启。

用户可以在 设置 -> 隐私 -> 广告追踪里重置此id或者限制追踪,就会出现idfa不一致或取不到,故绝不可以作为业务分析的主id,来识别用户。

IDFA新政

根据苹果《用户隐私和数据使用》规定 ,从iOS 14.5,iPadOS 14.5和Apple tvOS 14.5开始,App需要通过App Tracking Transparency框架征得用户许可,然后才能跟踪用户或访问其设备的广告标识符。

新旧政策的主要区别在于,新版本是opt-in(手动选择打开),旧版本是opt-out(手动选择关闭)。iOS 14增加“应用跟踪透明度”(App Tracking Transparency)的提示弹窗。根据苹果《用户隐私和数据使用》QA,App为满足以下用途而接收或共享其中任何标识符(包括但不限于设备的广告标识符、会话ID、指纹ID和设备图形标识符),App都必须使用ATT框架来征得用户同意。而在当前的系统版本里,广告跟踪功能是默认打开状态的,关闭选项需要手动操作。

新版本:
file
file

前世今生

file

获取方式
+ (NSString *)getIDFA
{return [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
}

IDFV

介绍

全名:identifierForVendor ,是给Vendor(开发商)标识用户用的。在同一设备上,同一个Vender的应用的所有app,IDFV的值相同。

判断Vender是否相同是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个idfv的值。和idfa不同的是,idfv的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。

获取方式
+ (NSString *)getIDFV
{return [[UIDevice currentDevice].identifierForVendor UUIDString];
}

UUID

介绍

全称时Universally Unique IDentifier,它是基于iOS设备上面某个单个的应用程序,只要用户没有完全删除应用程序,则这个 UUID 在用户使用该应用程序的时候一直保持不变。

特点

如果用户删除了这个应用程序,然后再重新安装,刷机或重装系统后uuid还是会改变。

我们可以获取到UUID,然后把UUID保存到KeyChain里面,这样以后即使APP删了再装回来,也可以从KeyChain中读取回来。使用group还可以保证同一个开发商的所有程序针对同一台设备能够获取到相同的不变的UDID,以此来实现设备的唯一标示。

获取方式
+ (NSString *)getUUID
{return [[NSUUID UUID] UUIDString];
}

UDID

介绍

UDID全称为Unique Device Identifier Description,是一个40个字符串的序号,用来标示唯一的iOS设备。

iOS 2.0版本以后UIDevice提供一个获取设备唯一标识符的方法uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。 许多开发者把UDID跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输UDID和私人信息。iOS5之后废弃

虽然iOS5之后无法通过API获取UDID

获取方式
[UIDevice currentDevice].identifierForVendor.UUIDString

OpenUDID

介绍

不是苹果官方的,是一个替代 UDID 的第三方解决方案, 缺点是如果你完全删除全部带有 OpenUDID SDK 包的 App(比如恢复系统等),那么 OpenUDID 会重新生成,而且和之前的值会不同,相当于新设备

Bundle ID

介绍

全称Bundle identifier,也叫 App ID 或者应用 ID,一个开发者账号下每一个 iOS应用的唯一标识,就像一个人的身份证号码。

分类
  • Explicit App ID「明确的 App ID」,一般格式是:com.company.appName;这种 id 只能用在一个app上,每一个新应用都要创建并只有一个。
  • Wildcard App ID「通配符 App ID」, 一般格式是:com.domainname.* ;这种 id 可以用在多个应用上,虽然方便,但是使用这种id的应用不能使用通知功能,所以不常用。
其他

iOS 是 bundle id,android 是 packageName,一般情况下都叫 bundle ID 或者 App ID。

我们看一下淘宝同开发者应用的Bundle ID
下图是淘宝同开发者应用列表(只截取了部分数据)
file
来看一下Bundle ID结果:
file
我们在上面IDFV也介绍了,同一个Vendor,前两个字段是相同的。

IP地址

介绍

IP地址(Internet Protocol Address)是指互联网协议地址,又译为网际协议地址。IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

获取方式
/** 获取设备当前网络IP地址*/
- (NSString *)getIPAddress:(BOOL)preferIPv4
{NSArray *searchArray = preferIPv4 ?@[ IOS_VPN @"/" IP_ADDR_IPv4, IOS_VPN @"/" IP_ADDR_IPv6, IOS_WIFI @"/" IP_ADDR_IPv4, IOS_WIFI @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6 ] :@[ IOS_VPN @"/" IP_ADDR_IPv6, IOS_VPN @"/" IP_ADDR_IPv4, IOS_WIFI @"/" IP_ADDR_IPv6, IOS_WIFI @"/" IP_ADDR_IPv4, IOS_CELLULAR @"/" IP_ADDR_IPv6, IOS_CELLULAR @"/" IP_ADDR_IPv4 ] ;NSDictionary *addresses = [self getIPAddr];__block NSString *address;[searchArray enumerateObjectsUsingBlock:^(NSString *key, NSUInteger idx, BOOL * _Nonnull stop) {address = addresses[key];//筛选出IP地址格式if([self isValidatIP:address]) *stop = YES;}];return address ? address : @"0.0.0.0";
}
- (BOOL)isValidatIP:(NSString *)ipAddress {if (ipAddress.length == 0) {return NO;}NSString *urlRegEx = @"^([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.""([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.""([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.""([01]?\\d\\d?|2[0-4]\\d|25[0-5])$";NSError *error;NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:urlRegEx options:0 error:&error];if (regex != nil) {NSTextCheckingResult *firstMatch=[regex firstMatchInString:ipAddress options:0 range:NSMakeRange(0, [ipAddress length])];return firstMatch;}return NO;
}
- (NSDictionary *)getIPAddr
{NSMutableDictionary *addresses = [NSMutableDictionary dictionaryWithCapacity:8];// retrieve the current interfaces - returns 0 on successstruct ifaddrs *interfaces;if(!getifaddrs(&interfaces)) {// Loop through linked list of interfacesstruct ifaddrs *interface;for(interface=interfaces; interface; interface=interface->ifa_next) {if(!(interface->ifa_flags & IFF_UP) /* || (interface->ifa_flags & IFF_LOOPBACK) */ ) {continue; // deeply nested code harder to read}const struct sockaddr_in *addr = (const struct sockaddr_in*)interface->ifa_addr;char addrBuf[ MAX(INET_ADDRSTRLEN, INET6_ADDRSTRLEN) ];if(addr && (addr->sin_family==AF_INET || addr->sin_family==AF_INET6)) {NSString *name = [NSString stringWithUTF8String:interface->ifa_name];NSString *type;if(addr->sin_family == AF_INET) {if(inet_ntop(AF_INET, &addr->sin_addr, addrBuf, INET_ADDRSTRLEN)) {type = IP_ADDR_IPv4;}} else {const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6*)interface->ifa_addr;if(inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf, INET6_ADDRSTRLEN)) {type = IP_ADDR_IPv6;}}if(type) {NSString *key = [NSString stringWithFormat:@"%@/%@", name, type];addresses[key] = [NSString stringWithUTF8String:addrBuf];}}}// Free memoryfreeifaddrs(interfaces);}return [addresses count] ? addresses : nil;
}

MAC地址

介绍

用来定义网络设备的位置。一个主机会有一个 MAC 地址,MAC 地址是网卡决定的,是固定的,为了保护用户隐私苹果已经禁止读取这个标识了。

获取方式
正常获取MAC地址

结果是:020000000000

- (NSString *)getmacaddress2
{int                 mib[6];size_t              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) {printf("Error: if_nametoindex error/n");return NULL;}if (sysctl(mib, 6, NULL, &len, NULL, 0) < 0) {printf("Error: sysctl, take 1/n");return NULL;}if ((buf = malloc(len)) == NULL) {printf("Could not allocate memory. error!/n");return NULL;}if (sysctl(mib, 6, buf, &len, NULL, 0) < 0) {printf("Error: sysctl, take 2");return NULL;}ifm = (struct if_msghdr *)buf;sdl = (struct sockaddr_dl *)(ifm + 1);ptr = (unsigned char *)LLADDR(sdl);NSString *outstring = [NSString stringWithFormat:@"%02x:%02x:%02x:%02x:%02x:%02x", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)];//    NSString *outstring = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x", *ptr, *(ptr+1), *(ptr+2), *(ptr+3), *(ptr+4), *(ptr+5)];NSLog(@"outString:%@", outstring);free(buf);return [outstring uppercaseString];
}
获取对应Wi-Fi的MAC地址

不同的Wi-Fi的MAC地址不同

- (nullable NSString *)getMacAddress3 {res_9_init();int len;//get currnet ip addressNSString *ip = [self currentIPAddressOf:IOS_WIFI];if(ip == nil) {fprintf(stderr, "could not get current IP address of en0\n");return DUMMY_MAC_ADDR;}//end if//set port and destination_res.nsaddr_list[0].sin_family = AF_INET;_res.nsaddr_list[0].sin_port = htons(MDNS_PORT);_res.nsaddr_list[0].sin_addr.s_addr = [self IPv4Pton:ip];_res.nscount = 1;unsigned char response[NS_PACKETSZ];//send mdns queryif((len = res_9_query(QUERY_NAME, ns_c_in, ns_t_ptr, response, sizeof(response))) < 0) {fprintf(stderr, "res_search(): %s\n", hstrerror(h_errno));return DUMMY_MAC_ADDR;}//end if//parse mdns messagens_msg handle;if(ns_initparse(response, len, &handle) < 0) {fprintf(stderr, "ns_initparse(): %s\n", hstrerror(h_errno));return DUMMY_MAC_ADDR;}//end if//get answer lengthlen = ns_msg_count(handle, ns_s_an);if(len < 0) {fprintf(stderr, "ns_msg_count return zero\n");return DUMMY_MAC_ADDR;}//end if//try to get mac address from dataNSString *macAddress = nil;for(int i = 0 ; i < len ; i++) {ns_rr rr;ns_parserr(&handle, ns_s_an, 0, &rr);if(ns_rr_class(rr) == ns_c_in &&ns_rr_type(rr) == ns_t_ptr &&!strcmp(ns_rr_name(rr), QUERY_NAME)) {char *ptr = (char *)(ns_rr_rdata(rr) + 1);int l = (int)strcspn(ptr, "@");char *tmp = calloc(l + 1, sizeof(char));if(!tmp) {perror("calloc()");continue;}//end ifmemcpy(tmp, ptr, l);macAddress = [NSString stringWithUTF8String:tmp];free(tmp);}//end if}//end for eachmacAddress = macAddress ? macAddress : DUMMY_MAC_ADDR;return macAddress;
}//end- (nonnull NSString *)currentIPAddressOf: (nonnull NSString *)device {struct ifaddrs *addrs;NSString *ipAddress = nil;if(getifaddrs(&addrs) != 0) {return nil;}//end if//get ipv4 addressfor(struct ifaddrs *addr = addrs ; addr ; addr = addr->ifa_next) {if(!strcmp(addr->ifa_name, [device UTF8String])) {if(addr->ifa_addr) {struct sockaddr_in *in_addr = (struct sockaddr_in *)addr->ifa_addr;if(in_addr->sin_family == AF_INET) {ipAddress = [self IPv4Ntop:in_addr->sin_addr.s_addr];break;}//end if}//end if}//end if}//end forfreeifaddrs(addrs);return ipAddress;
}//end currentIPAddressOf:- (nullable NSString *)IPv4Ntop: (in_addr_t)addr {char buffer[INET_ADDRSTRLEN] = {0};return inet_ntop(AF_INET, &addr, buffer, sizeof(buffer)) ?[NSString stringWithUTF8String:buffer] : nil;
}//end IPv4Ntop:- (in_addr_t)IPv4Pton: (nonnull NSString *)IPAddr {in_addr_t network = INADDR_NONE;return inet_pton(AF_INET, [IPAddr UTF8String], &network) == 1 ?network : INADDR_NONE;
}//end IPv4Pton

系统版本

获取方式
+ (NSString *)getSystemVersion
{return [[UIDevice currentDevice] systemVersion];
}

设备型号

获取方式
#import <sys/utsname.h>+ (NSString *)getDeviceModel
{struct utsname systemInfo;uname(&systemInfo);NSString *deviceString = [NSString stringWithCString:systemInfo.machine encoding:NSUTF8StringEncoding];if ([deviceString isEqualToString:@"iPhone3,1"])    return @"iPhone 4";if ([deviceString isEqualToString:@"iPhone3,2"])    return @"iPhone 4";if ([deviceString isEqualToString:@"iPhone3,3"])    return @"iPhone 4";if ([deviceString isEqualToString:@"iPhone4,1"])    return @"iPhone 4S";if ([deviceString isEqualToString:@"iPhone5,1"])    return @"iPhone 5";if ([deviceString isEqualToString:@"iPhone5,2"])    return @"iPhone 5 (GSM CDMA)";if ([deviceString isEqualToString:@"iPhone5,3"])    return @"iPhone 5c (GSM)";if ([deviceString isEqualToString:@"iPhone5,4"])    return @"iPhone 5c (GSM CDMA)";if ([deviceString isEqualToString:@"iPhone6,1"])    return @"iPhone 5s (GSM)";if ([deviceString isEqualToString:@"iPhone6,2"])    return @"iPhone 5s (GSM CDMA)";if ([deviceString isEqualToString:@"iPhone7,1"])    return @"iPhone 6 Plus";if ([deviceString isEqualToString:@"iPhone7,2"])    return @"iPhone 6";if ([deviceString isEqualToString:@"iPhone8,1"])    return @"iPhone 6s";if ([deviceString isEqualToString:@"iPhone8,2"])    return @"iPhone 6s Plus";if ([deviceString isEqualToString:@"iPhone8,4"])    return @"iPhone SE";if ([deviceString isEqualToString:@"iPhone9,1"])    return @"iPhone 7";if ([deviceString isEqualToString:@"iPhone9,2"])    return @"iPhone 7 Plus";if ([deviceString isEqualToString:@"iPhone9,3"])    return @"iPhone 7";if ([deviceString isEqualToString:@"iPhone9,4"])    return @"iPhone 7 Plus";if ([deviceString isEqualToString:@"iPhone10,1"])   return @"iPhone 8";if ([deviceString isEqualToString:@"iPhone10,4"])   return @"iPhone 8";if ([deviceString isEqualToString:@"iPhone10,2"])   return @"iPhone 8 Plus";if ([deviceString isEqualToString:@"iPhone10,5"])   return @"iPhone 8 Plus";if ([deviceString isEqualToString:@"iPhone10,3"])   return @"iPhone X";if ([deviceString isEqualToString:@"iPhone10,6"])   return @"iPhone X";if ([deviceString isEqualToString:@"iPhone11,2"])   return @"iPhone XS";if ([deviceString isEqualToString:@"iPhone11,4"])   return @"iPhone XS Max";if ([deviceString isEqualToString:@"iPhone11,6"])   return @"iPhone XS Max";if ([deviceString isEqualToString:@"iPhone11,8"])   return @"iPhone XR";if ([deviceString isEqualToString:@"iPhone12,1"])   return @"iPhone 11";if ([deviceString isEqualToString:@"iPhone12,3"])   return @"iPhone 11 Pro";if ([deviceString isEqualToString:@"iPhone12,5"])   return @"iPhone 11 Pro Max";if ([deviceString isEqualToString:@"iPhone12,8"])   return @"iPhone SE (2nd generation)";if ([deviceString isEqualToString:@"iPhone13,1"])   return @"iPhone 12 mini";if ([deviceString isEqualToString:@"iPhone13,2"])   return @"iPhone 12";if ([deviceString isEqualToString:@"iPhone13,3"])   return @"iPhone 12 Pro";if ([deviceString isEqualToString:@"iPhone13,4"])   return @"iPhone 12 Pro Max";if ([deviceString isEqualToString:@"iPhone14,2"])   return @"iPhone 13 Pro";if ([deviceString isEqualToString:@"iPhone14,2"])   return @"iPhone 13 Pro";if ([deviceString isEqualToString:@"iPhone14,2"])   return @"iPhone 13 Pro";if ([deviceString isEqualToString:@"iPhone14,3"])   return @"iPhone 13 Pro Max";if ([deviceString isEqualToString:@"iPhone14,4"])   return @"iPhone 13 mini";if ([deviceString isEqualToString:@"iPhone14,5"])   return @"iPhone 13";return deviceString;
}

设备名称

获取方式
+ (NSString *)getDeviceName
{return [UIDevice currentDevice].name;
}

磁盘大小

获取方式
+ (long)getDiskTotalSize
{NSDictionary *systemAttributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:nil];NSNumber *diskTotalSize = [systemAttributes objectForKey:NSFileSystemSize];return (long)(diskTotalSize.floatValue / 1024.f / 1024.f);
}

磁盘剩余空间

获取方式
+ (long)getDiskFreeSize
{NSDictionary *systemAttributes = [[NSFileManager defaultManager] attributesOfFileSystemForPath:NSHomeDirectory() error:nil];NSNumber *diskFreeSize = [systemAttributes objectForKey:NSFileSystemFreeSize];return (long)(diskFreeSize.floatValue / 1024.f / 1024.f);
}

电量

获取方式
+ (CGFloat)getBatteryLevel
{[UIDevice currentDevice].batteryMonitoringEnabled = YES;return [[UIDevice currentDevice] batteryLevel];
}

电池状态

获取方式
+ (NSString *)getBatteryState
{[UIDevice currentDevice].batteryMonitoringEnabled = YES;UIDeviceBatteryState batteryState = [UIDevice currentDevice].batteryState;switch (batteryState) {case UIDeviceBatteryStateUnplugged:return @"未充电";case UIDeviceBatteryStateCharging:return @"充电中";case UIDeviceBatteryStateFull:return @"已充满";default:return @"未知";}
}

屏幕尺寸

获取方式
+ (CGSize)getScreenSize
{CGRect screenBounds = [[UIScreen mainScreen] bounds];CGFloat screenScale = [UIScreen mainScreen].scale;CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);return screenSize;
}

屏幕亮度

获取方式
+ (CGFloat)getScreenBrightness
{return [UIScreen mainScreen].brightness;
}

音量大小

获取方式
#import <AVFoundation/AVFoundation.h>+ (CGFloat)getDeviceVolume
{return [[AVAudioSession sharedInstance] outputVolume];
}

Wifi名称

获取方式
#import <SystemConfiguration/CaptiveNetwork.h>+ (NSString *)getWifiSSID
{NSArray *ifs = (__bridge id)CNCopySupportedInterfaces();id info = nil;for (NSString *ifnam in ifs) {info = (__bridge id)CNCopyCurrentNetworkInfo((__bridge CFStringRef)ifnam);if (info && [info count]) {break;}}NSDictionary *dctySSID = (NSDictionary *)info;return [dctySSID objectForKey:@"SSID"];
}

网络制式

获取方式
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <CoreTelephony/CTCarrier.h>+ (NSString *)getNetCarrier
{NSString *mobileCarrier;CTTelephonyNetworkInfo *networkInfo = [[CTTelephonyNetworkInfo alloc] init];CTCarrier *carrier = networkInfo.subscriberCellularProvider;NSString *MCC = carrier.mobileCountryCode;NSString *MNC = carrier.mobileNetworkCode;if (!MCC || !MNC) {mobileCarrier = @"No Sim";} else {if ([MCC isEqualToString:@"460"]) {if ([MNC isEqualToString:@"00"] || [MNC isEqualToString:@"02"] || [MNC isEqualToString:@"07"]) {mobileCarrier = @"China Mobile";} else if ([MNC isEqualToString:@"01"] || [MNC isEqualToString:@"06"]) {mobileCarrier = @"China Unicom";} else if ([MNC isEqualToString:@"03"] || [MNC isEqualToString:@"05"] || [MNC isEqualToString:@"11"]) {mobileCarrier = @"China Telecom";} else if ([MNC isEqualToString:@"20"]) {mobileCarrier = @"China Tietong";} else {mobileCarrier = [NSString stringWithFormat:@"MNC%@", MNC];}} else {mobileCarrier = @"Foreign Carrier";}}return mobileCarrier;
}

是否越狱

获取方式
#import <sys/stat.h>
#import <dlfcn.h>//判断是否越狱
+ (BOOL)isJailBreak
{//以下检测的过程是越往下,越狱越高级//获取越狱文件路径NSString *cydiaPath = @"/Applications/Cydia.app";NSString *aptPath = @"/private/var/lib/apt/";if ([[NSFileManager defaultManager] fileExistsAtPath:cydiaPath]) {return YES;}if ([[NSFileManager defaultManager] fileExistsAtPath:aptPath]) {return YES;}//可能存在hook了NSFileManager方法,此处用底层C stat去检测struct stat stat_info;if (0 == stat("/Library/MobileSubstrate/MobileSubstrate.dylib", &stat_info)) {return YES;}if (0 == stat("/Applications/Cydia.app", &stat_info)) {return YES;}if (0 == stat("/var/lib/cydia/", &stat_info)) {return YES;}if (0 == stat("/var/cache/apt", &stat_info)) {return YES;}//可能存在stat也被hook了,可以看stat是不是出自系统库,有没有被攻击者换掉。这种情况出现的可能性很小int ret;Dl_info dylib_info;int (*func_stat)(const char *,struct stat *) = stat;if ((ret = dladdr(func_stat, &dylib_info))) {//相等为0,不相等,肯定被攻击if (strcmp(dylib_info.dli_fname, "/usr/lib/system/libsystem_kernel.dylib")) {return YES;}}//通常,越狱机的输出结果会包含字符串:Library/MobileSubstrate/MobileSubstrate.dylib。//攻击者给MobileSubstrate改名,原理都是通过DYLD_INSERT_LIBRARIES注入动态库。那么可以检测当前程序运行的环境变量char *env = getenv("DYLD_INSERT_LIBRARIES");if (env != NULL) {return YES;}return NO;
}

是否插入SIM卡

获取方式
#import <CoreTelephony/CTTelephonyNetworkInfo.h>
#import <CoreTelephony/CTCarrier.h>+ (BOOL)isSimInserted
{CTTelephonyNetworkInfo *networkInfo = [[CTTelephonyNetworkInfo alloc] init];CTCarrier *carrier = [networkInfo subscriberCellularProvider];if (!carrier.isoCountryCode) {return NO;}return YES;
}

是否允许推送

获取方式
+ (BOOL)isPushEnabled
{if ([[UIDevice currentDevice].systemVersion floatValue] >= 8.0f) {UIUserNotificationSettings *setting = [[UIApplication sharedApplication] currentUserNotificationSettings];if (UIUserNotificationTypeNone == setting.types) {return NO;} else {return YES;}} else {UIRemoteNotificationType type = [[UIApplication sharedApplication] enabledRemoteNotificationTypes];if(UIRemoteNotificationTypeNone == type){return NO;} else {return YES;}}
}

剪切板内容

获取方式
+ (NSString *)getPasteBoardString
{return [UIPasteboard generalPasteboard].string;
}

是否使用代理

获取方式
+ (BOOL)isViaProxy
{NSDictionary *proxySettings = (__bridge NSDictionary *)(CFNetworkCopySystemProxySettings());NSArray *proxies = (__bridge NSArray *)(CFNetworkCopyProxiesForURL((__bridge CFURLRef _Nonnull)([NSURL URLWithString:@"https://www.baidu.com/"]), (__bridge CFDictionaryRef _Nonnull)(proxySettings)));NSDictionary *settings = proxies[0];if (![[settings objectForKey:(NSString *)kCFProxyTypeKey] isEqualToString:@"kCFProxyTypeNone"]){return YES;}return NO;
}

附录1—Android主要设备信息

file

DeviceId(设备ID/DID)

介绍

DeviceId是用来标识一台Android物理设备的唯一id

Google提供了TelephonyManager.getDeviceId方法来获取Android的DID。该API是获取GSM手机的国际移动设备识别码(IMEI)或者 CDMA手机的移动设备识别码(MEID )。但该API存在一些限制。

补充知识
国际移动设备识别码(IMEI)

**全称“International Mobile Equipment Identity”,*是通常所说的手机序列号、手机“串号”。用于在移动电话网络中识别每一部独立的手机等移动通信设备,相当于移动电话的身份证,序列号共有15~17位数字,通过在手机拨号键盘中输入#06#即可查询。

但存在以下限制:

  • 1、自API23(Android 6.0)开始,获取IMEI需要用户予"android.permission.READ_PHONE_STATE";
  • 2、自API29(Android 10.0)开始,您的应用必须是设备或个人资料所有者应用具有特殊运营商权限或具有 READ_PRIVILEGED_PHONE_STATE 特许权限,才能访问这些标识符。
  • 3、某些小厂商某型号的手机IMEI可能相同。
移动设备识别码(MEID )

全称“Mobile Equipment Identifier”,是CDMA手机的身份识别码,也是每台CDMA手机或通讯平板唯一的识别码。通过这个识别码,网络端可以对该手机进行跟踪和监管。用于CDMA制式的手机。MEID的数字范围是十六进制的,和IMEI的格式类似。

存在的限制同IMEI的限制。

系统版本迭代带来的影响
  • 为了更好保护用户隐私,谷歌对安卓Q系统中所有获取设备识别码的接口都增加了新的权限控制:READ_PRIVILEGED_PHONE_STATE,该权限需要系统签名的应用才能申请。同时,系统默认WiFi Mac地址随机化,当设备连上不同的WiFi网络时随机生成Mac地址;

  • 通过READ_PHONE_STATE权限获取Device ID的应用以及将设备WiFi Mac地址作为设备唯一标志符的应用将受影响

  • 在Android10及以上设备中,对于TargetSdkVersion<Q且没有申请READ_PHONE_STATE权限的应用和TargetSdkVersion>=Q的全部应用,获取Device ID会抛异常SecurityException。

  • 在Android10及以上设备中,对于 TargetSdkVersion<Q且申请了READ_PHONE_STATE权限的应用,通过getDeviceId接口读取的值为Null。

获取方式
public static String getIMEIDeviceId(Context context) {String deviceId;//当APK运行在Android10(API>=29)及以上时,获取到的是AndroidIDif (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);} else {final TelephonyManager mTelephony = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);//当APK运行在Android6.0(API>=23)及以上时,需要check有无READ_PHONE_STATE权限。if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {if (context.checkSelfPermission(Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {return "";}}assert mTelephony != null;//如果TelephonyManager获取到的DeviceId不为nullif (mTelephony.getDeviceId() != null) {//获取GSM手机的国际移动设备识别码(IMEI)或者 CDMA手机的移动设备识别码(MEID).deviceId = mTelephony.getDeviceId();} else {//如果DeviceId为null,我们的DID依然是AndroidID。deviceId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);}}return deviceId;}
补充获取方式

以上获取方式有诸多不足之处,也有很多厂商选择自己用硬件拼出来一个UUID
但是这种方式也不是百分百准确。

public static String getUUID()
{String serial = null;String m_szDevIDShort = "35" +Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +Build.USER.length() % 10; //13 位try {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {serial = android.os.Build.getSerial();} else {serial = Build.SERIAL;}//API>=9 使用serial号return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();} catch (Exception exception) {//serial需要一个初始化serial = "serial"; // 随便一个初始化}//使用硬件信息拼凑出来的15位号码return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}

有一个比较详细的源码,自取地址:https://github.com/57xiaoyu/DeviceIDUtils

ANDROID_ID

介绍

又称SSAID。设备启动时,随机生成一个 64 位数字(表示为十六进制字符串),对于应用签名密钥、用户和设备的每个组合都是唯一的。 ANDROID_ID 的值受签名密钥和用户的限制。

但存在如下限制:

  • 1、如果在设备上执行恢复出厂设置或 APK 签名密钥更改,则该值可能会更改。
  • 2、某些小厂商的Android手机可能为null或相同。

Android 开发者文档中有关于Android 8.0后的隐私性和SSAID的变化说明:
file
从图中不难看出,在 Android 8.0 以后,签名不同的 App 所获取的 Android ID(SSAID)是不一样的,但同一个开发者可以根据自己的数字签名,将所开发的不同 App 进行关联。

获取方式
private String android_id = Secure.getString(getContext().getContentResolver(),Secure.ANDROID_ID); 

AAID

AAID 与 IDFA 作用相同——IDFA 是 iOS 平台内的广告跟踪 ID,AAID 则用于 Android 平台。

它们都是一种非永久、可重置的标识符,专门提供给 App 以进行广告行为,用户随时可以重置该类 ID,或通过系统设置关闭个性化广告跟踪。但 AAID 依托于 Google 服务框架,因此如果手机没有内置该框架、或框架不完整、或无法连接到相关服务,这些情况都有可能导致 AAID 不可用。

OAID(匿名设备标识符)

是指华为、小米、OPPO、VIVO等安卓系统设备提供一串非永久性设备标识符,示例:1fe9a970-efbb-29e0-0bdd-f5dbbf751ab5。

Android 10 之后的替代方案

OAID 的本质其实是一种在国行系统内使用的、应对 Android 10 限制读取 IMEI 的、「拯救」国内移动广告的广告跟踪标识符,其背后是 移动安全联盟(Mobile Security Alliance,简称 MSA)。

附录2—Android 系统名字、版本、API level的对应关系

代号版本API 级别/NDK 版本
Android12L12API 级别 32
Android1212API 级别 31
Android1111API 级别 30
Android1010API 级别 29
Pie9API 级别 28
Oreo8.1.0API 级别 27
Oreo8.0.0API 级别 26
Nougat7.1API 级别 25
Nougat7API 级别 24
Marshmallow6API 级别 23
Lollipop5.1API 级别 22
Lollipop5API 级别 21
KitKat4.4 - 4.4.4API 级别 19
Jelly Bean4.3.xAPI 级别 18
Jelly Bean4.2.xAPI 级别 17
Jelly Bean4.1.xAPI 级别 16
Ice Cream Sandwich4.0.3 - 4.0.4API 级别 15,NDK 8
Ice Cream Sandwich4.0.1 - 4.0.2API 级别 14,NDK 7
Honeycomb3.2.xAPI 级别 13
Honeycomb3.1API 级别 12,NDK 6
Honeycomb3API 级别 11
Gingerbread2.3.3 - 2.3.7API 级别 10
Gingerbread2.3 - 2.3.2API 级别 9,NDK 5
Froyo2.2.xAPI 级别 8,NDK 4
Eclair2.1API 级别 7,NDK 3
Eclair2.0.1API 级别 6
Eclair2API 级别 5
Donut1.6API 级别 4,NDK 2
Cupcake1.5API 级别 3,NDK 1
(无代号)1.1API 级别 2
(无代号)1API 级别 1

参考文章:
1、https://blog.csdn.net/u011774517/article/details/107456052
2、https://baijiahao.baidu.com/s?id=1695811594529218926&wfr=spider&for=pc
3、https://juejin.cn/post/6844904178653855752
4、https://www.helloworld.net/p/5706465559
5、https://www.jb51.net/shouji/674558.html
6、https://blog.csdn.net/weixin_38244174/article/details/120249949
7、https://blog.csdn.net/linxinfa/article/details/102395922
8、https://blog.csdn.net/weixin_42600398/article/details/117984064
9、https://blog.csdn.net/qiluoyiyi/article/details/118418383

相关文章:

iOS设备信息详解

文章目录 ID 体系iOS设备信息详解IDFA介绍特点IDFA新政前世今生获取方式 IDFV介绍获取方式 UUID介绍特点获取方式 UDID介绍获取方式 OpenUDID介绍 Bundle ID介绍分类其他 IP地址介绍获取方式 MAC地址介绍获取方式正常获取MAC地址获取对应Wi-Fi的MAC地址 系统版本获取方式 设备型…...

如何使用支付宝沙箱环境支付并公网调用sdk创建支付单服务

文章目录 1.测试环境2.本地配置2. 内网穿透2.1 下载安装cpolar内网穿透2.2 创建隧道3. 测试公网访问4. 配置固定二级子域名4.1 保留一个二级子域名4.2 配置二级子域名5. 使用固定二级子域名进行访问 1.测试环境 MavenSpring bootJdk 1.8 2.本地配置 获取支付宝支付Java SDK,…...

[EFI]Dell Latitude-7400电脑 Hackintosh 黑苹果efi引导文件

硬件型号驱动情况主板 Dell Latitude-7400 处理器Intel Core i7-8665U已驱动内存16GB DDR4 RAM已驱动硬盘Toshiba KIOXIA 512GB SSD已驱动显卡Intel UHD 620 Graphics已驱动声卡Realtek ALC256已驱动有线网卡 无 无无线网卡蓝牙Intel Wireless-AC 9560已驱动 支持系统版本 maco…...

用芯片SIC8833可开发电子秤方案

SIC8833作为一款高性能的电子秤方案芯片&#xff0c;这款芯片是一个带24bitADC的8位RISC MCU&#xff0c;内置8k16位OTP程序存储器。具体24位双向I/O口的特性&#xff0c;广泛应用于电子衡器和精密测量及控制系统&#xff0c;能满足用户的不同需求和应用场景。 以下是电子秤方案…...

【Qt-QFile-QDir】

Qt编程指南 ■ Stream■ QTextStream■ QDataStream ■ QDial■ QDir■ QFile■■ ■ Stream ■ QTextStream /* 获取文件的路径 */ QString fileName QFileDialog::getOpenFileName(this);/* 指向文件 */ file.setFileName(fileName);/* 判断文件是否存在 */ if (!file.exi…...

设计模式之-单列设计模式,5种单例设计模式使用场景以及它们的优缺点

系列文章目录 设计模式之-6大设计原则简单易懂的理解以及它们的适用场景和代码示列 设计模式之-单列设计模式&#xff0c;5种单例设计模式使用场景以及它们的优缺点 设计模式之-3种常见的工厂模式简单工厂模式、工厂方法模式和抽象工厂模式&#xff0c;每一种模式的概念、使用…...

Android 13 - Media框架(25)- OMXNodeInstance(二)

上一节我们了解了 OMXNodeInstance 的创建过程&#xff0c;以及 IOmx 服务和 OMXNodeInstance、OMX组件之间的联系。接下来我们将一起了解 ACodec 是如何通过 OMXNodeInstance 这个中间层进行端口定义设置&#xff0c;以及端口Buffer分配的。 OMXNodeInstance 的代码还是比较长…...

生物系统学中的进化树构建和分析R工具包V.PhyloMaker2的介绍和详细使用

V.PhyloMaker2是一个R语言的工具包&#xff0c;专门用于构建和分析生物系统学中的进化树&#xff08;也称为系统发育树或phylogenetic tree&#xff09;。以下是对V.PhyloMaker2的一些基本介绍和使用说明&#xff1a; 论文介绍&#xff1a;V.PhyloMaker2: An updated and enla…...

XStream 反序列化漏洞 CVE-2021-39144 已亲自复现

XStream 反序列化漏洞 CVE-2021-39144 已亲自复现 漏洞名称漏洞描述影响版本 漏洞复现环境搭建 修复建议总结 漏洞名称 漏洞描述 在Unmarshalling Time处包含用于重新创建前一对象的类型信息。XStream基于这些类型的信息创建新实例。攻击者可以控制输入流并替换或注入对象&am…...

深入剖析LinkedList:揭秘底层原理

文章目录 一、 概述LinkedList1.1 LinkedList简介1.2 LinkedList的优点和缺点 二、 LinkedList数据结构分析2.1 Node节点结构体解析2.2 LinkedList实现了双向链表的原因2.3 LinkedList如何实现了链表的基本操作&#xff08;增删改查&#xff09;2.4 LinkedList的遍历方式 三、 …...

计算机网络复习-OSI TCP/IP 物理层

我膨胀了&#xff0c;挂我啊~ 作者简介&#xff1a; 每年都吐槽吉师网安奇怪的课程安排、全校正经学网络安全不超20人情景以及割韭菜企业合作的FW&#xff0c;今年是第一年。。 TCP/IP模型 先做两道题&#xff1a; TCP/IP协议模型由高层到低层分为哪几层&#xff1a; 这题…...

虚拟机服务器中了lockbit2.0/3.0勒索病毒怎么处理,数据恢复应对步骤

网络技术的不断发展也为网络威胁带来了安全隐患&#xff0c;近期&#xff0c;对于许多大型企业来说&#xff0c;许多企业的虚拟机服务器系统遭到了lockbit2.0/3.0勒索病毒攻击&#xff0c;导致企业所有计算机系统瘫痪&#xff0c;无法正常工作&#xff0c;严重影响了企业的正常…...

【MATLAB】 RGB和YCbCr互转

前言 在视频、图像处理领域经常会遇到不同色域图像的转换&#xff0c;比如RGB、YUV、YCbCr色域间的转换&#xff0c;这里提供一组转换公式&#xff0c;供大家参考。 色彩模型 RGB RGB色彩模型是一种用于表示数字图像的颜色空间&#xff0c;其中"RGB"代表红色&…...

【线性代数】决定张成空间的最少向量线性无关吗?

答1&#xff1a; 是的&#xff0c;张成空间的最少向量是线性无关的。 在数学中&#xff0c;张成空间&#xff08;span space&#xff09;是一个向量空间&#xff0c;它由一组向量通过线性组合&#xff08;即每个向量乘以一个标量&#xff09;生成。如果这组向量是线性无关的&…...

暴力破解(Pikachu)

基于表单的暴力破解 先随便输入一下&#xff0c;然后抓包&#xff0c;进行字典爆破 验证码绕过(on server) server服务端要输入正确的验证码后进行爆破 之后的操作没什么不一样 验证码绕过(on client) 这个也需要输入验证码&#xff0c;但是后面进行字典爆破的时候&#xf…...

如何使用CMake查看opencv封装好的函数

当我们有时想查看opencv自带的函数的源代码&#xff0c;比如函数cvCreateImage, 此时我们选中cvCreateImage, 点击鼠标右键->转到定义&#xff0c;我们会很惊讶的发现为什么只看到了cvCreateImage的一个简单声明&#xff0c;而没有源代码呢&#xff1f;这是因为openCV将很多…...

微盛·企微管家:用户运营API集成,电商无代码解决方案

连接电商平台的新纪元&#xff1a;微盛企微管家 随着电子商务的蓬勃发展&#xff0c;电商平台的高效运营已经成为企业成功的关键。在这个新纪元里&#xff0c;微盛企微管家以其创新的无代码开发连接方案&#xff0c;成为企业之间连接电商平台的强大工具。它允许企业轻松集成电…...

Hive 部署

一、介绍 Apache Hive是一个分布式、容错的数据仓库系统&#xff0c;支持大规模的分析。Hive Metastore&#xff08;HMS&#xff09;提供了一个中央元数据存储库&#xff0c;可以轻松地进行分析&#xff0c;以做出明智的数据驱动决策&#xff0c;因此它是许多数据湖架构的关键组…...

CopyOnWriteArrayList源码阅读

1、构造方法 无参构造函数 //创建一个空数组&#xff0c;赋值给array引用 public CopyOnWriteArrayList() {setArray(new Object[0]); }//仅通过getArray / setArray访问的数组。 private transient volatile Object[] array;//设置数组 final void setArray(Object[] a) {arra…...

Windows操作系统:共享文件夹,防火墙的设置

1.共享文件夹 1.1 共享文件夹的优点 1.2 共享文件夹的优缺点 1.3 实例操作 ​编辑 2.防火墙设置 2.1 8080端口设置 3.思维导图 1.共享文件夹 1.1 共享文件夹的优点 优点 协作和团队合作&#xff1a;共享文件夹使多个用户能够在同一文件夹中协作和编辑文件。这促进了团…...

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

Qt/C++开发监控GB28181系统/取流协议/同时支持udp/tcp被动/tcp主动

一、前言说明 在2011版本的gb28181协议中&#xff0c;拉取视频流只要求udp方式&#xff0c;从2016开始要求新增支持tcp被动和tcp主动两种方式&#xff0c;udp理论上会丢包的&#xff0c;所以实际使用过程可能会出现画面花屏的情况&#xff0c;而tcp肯定不丢包&#xff0c;起码…...

线程与协程

1. 线程与协程 1.1. “函数调用级别”的切换、上下文切换 1. 函数调用级别的切换 “函数调用级别的切换”是指&#xff1a;像函数调用/返回一样轻量地完成任务切换。 举例说明&#xff1a; 当你在程序中写一个函数调用&#xff1a; funcA() 然后 funcA 执行完后返回&…...

DAY 47

三、通道注意力 3.1 通道注意力的定义 # 新增&#xff1a;通道注意力模块&#xff08;SE模块&#xff09; class ChannelAttention(nn.Module):"""通道注意力模块(Squeeze-and-Excitation)"""def __init__(self, in_channels, reduction_rat…...

镜像里切换为普通用户

如果你登录远程虚拟机默认就是 root 用户&#xff0c;但你不希望用 root 权限运行 ns-3&#xff08;这是对的&#xff0c;ns3 工具会拒绝 root&#xff09;&#xff0c;你可以按以下方法创建一个 非 root 用户账号 并切换到它运行 ns-3。 一次性解决方案&#xff1a;创建非 roo…...

今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存

文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...

10-Oracle 23 ai Vector Search 概述和参数

一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI&#xff0c;使用客户端或是内部自己搭建集成大模型的终端&#xff0c;加速与大型语言模型&#xff08;LLM&#xff09;的结合&#xff0c;同时使用检索增强生成&#xff08;Retrieval Augmented Generation &#…...

2025季度云服务器排行榜

在全球云服务器市场&#xff0c;各厂商的排名和地位并非一成不变&#xff0c;而是由其独特的优势、战略布局和市场适应性共同决定的。以下是根据2025年市场趋势&#xff0c;对主要云服务器厂商在排行榜中占据重要位置的原因和优势进行深度分析&#xff1a; 一、全球“三巨头”…...

短视频矩阵系统文案创作功能开发实践,定制化开发

在短视频行业迅猛发展的当下&#xff0c;企业和个人创作者为了扩大影响力、提升传播效果&#xff0c;纷纷采用短视频矩阵运营策略&#xff0c;同时管理多个平台、多个账号的内容发布。然而&#xff0c;频繁的文案创作需求让运营者疲于应对&#xff0c;如何高效产出高质量文案成…...