在开发过程中,我们经常会被要求获取每个设备的唯一标示,以便后台做相应的处理。我们来看看有哪些方法来获取设备的唯一标示,然后再分析下这些方法的利弊。

具体可以分为如下几种:

  1. UDID
  2. IDFA
  3. IDFV
  4. MAC
  5. keychain

下面我们来具体分析下每种获取方法的利弊

1、UDID

什么是UDID

UDID 「Unique Device Identifier Description」是由子母和数字组成的40个字符串的序号,用来区别每一个唯一的iOS设备,包括 iPhones, iPads, 以及 iPod touches,这些编码看起来是随机的,实际上是跟硬件设备特点相联系的,另外你可以到iTunes,pp助手或itools等软件查看你的udid(设备标识)

如下图所示:

image

UDID是用来干什么的?

UDID可以关联其它各种数据到相关设备上。例如,连接到开发者账号,可以允许在发布前让设备安装或测试应用;也可以让开发者获得iOS测试版进行体验。苹果用UDID连接到苹果的ID,这些设备可以自动下载和安装从App Store购买的应用、保存从iTunes购买的音乐、帮助苹果发送推送通知、即时消息。 在iOS 应用早期,UDID被第三方应用开发者和网络广告商用来收集用户数据,可以用来关联地址、记录应用使用习惯……以便推送精准广告。

为什么苹果反对开发人员使用UDID?

iOS 2.0版本以后UIDevice提供一个获取设备唯一标识符的方法uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。 许多开发者把UDID跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输UDID和私人信息。 为了避免集体诉讼,苹果最终决定在iOS 5 的时候,将这一惯例废除,开发者被引导生成一个唯一的标识符,只能检测应用程序,其他的信息不提供。现在应用试图获取UDID已被禁止且不允许上架。

所以这个方法作废

2、IDFA

  • 全名:advertisingIdentifier

  • 获取代码:

  #import <AdSupport/AdSupport.h>NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];
  • 来源:iOS6.0及以后

  • 说明:直译就是广告id, 在同一个设备上的所有App都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的,用户可以在 设置|隐私|广告追踪 里重置此id的值,或限制此id的使用,故此id有可能会取不到值,但好在Apple默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。

  • 注意:由于idfa会出现取不到的情况,故绝不可以作为业务分析的主id,来识别用户。

3、IDFV

  • 全名:identifierForVendor
  • 获取代码:
    NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];
  • 来源:iOS6.0及以后

  • 说明:顾名思义,是给Vendor标识用户用的,每个设备在所属同一个Vender的应用里,都有相同的值。其中的Vender是指应用提供商,但准确点说,是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个idfv的值。和idfa不同的是,idfv的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。

  • 注意:如果用户将属于此Vender的所有App卸载,则idfv的值会被重置,即再重装此Vender的App,idfv的值和之前不同。

4、MAC地址

使用WiFi的mac地址来取代已经废弃了的uniqueIdentifier方法。具体可见:
http://stackoverflow.com/questions/677530/how-can-i-programmatically-get-the-mac-address-of-an-iphone

然而在iOS 7中苹果再一次无情的封杀mac地址,使用之前的方法获取到的mac地址全部都变成了02:00:00:00:00:00。

5、Keychain

Paste_Image.png

我们可以获取到UUID,然后把UUID保存到KeyChain里面。

这样以后即使APP删了再装回来,也可以从KeyChain中读取回来。使用group还可以可以保证同一个开发商的所有程序针对同一台设备能够获取到相同的不变的UDID。

但是刷机或重装系统后uuid还是会改变。

把下面两个类文件放到你的项目中

KeychainItemWrapper.h文件
********************************#import <UIKit/UIKit.h>@interface KeychainItemWrapper : NSObject
{NSMutableDictionary *keychainItemData;        // The actual keychain item data backing store.NSMutableDictionary *genericPasswordQuery;    // A placeholder for the generic keychain item query used to locate the item.
}@property (nonatomic, retain) NSMutableDictionary *keychainItemData;
@property (nonatomic, retain) NSMutableDictionary *genericPasswordQuery;// Designated initializer.
- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
- (void)setObject:(id)inObject forKey:(id)key;
- (id)objectForKey:(id)key;// Initializes and resets the default generic keychain item data.
- (void)resetKeychainItem;@end
KeychainItemWrapper.h文件
********************************#import "KeychainItemWrapper.h"
#import <Security/Security.h>/*These are the default constants and their respective types,
available for the kSecClassGenericPassword Keychain Item class:kSecAttrAccessGroup            -        CFStringRef
kSecAttrCreationDate        -        CFDateRef
kSecAttrModificationDate    -        CFDateRef
kSecAttrDescription            -        CFStringRef
kSecAttrComment                -        CFStringRef
kSecAttrCreator                -        CFNumberRef
kSecAttrType                -        CFNumberRef
kSecAttrLabel                -        CFStringRef
kSecAttrIsInvisible            -        CFBooleanRef
kSecAttrIsNegative            -        CFBooleanRef
kSecAttrAccount                -        CFStringRef
kSecAttrService                -        CFStringRef
kSecAttrGeneric                -        CFDataRefSee the header file Security/SecItem.h for more details.*/@interface KeychainItemWrapper (PrivateMethods)
/*
The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was
to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the
Keychain API expects as a validly constructed container class.
*/
- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;
- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;// Updates the item in the keychain, or adds it if it doesn't exist.
- (void)writeToKeychain;@end@implementation KeychainItemWrapper@synthesize keychainItemData, genericPasswordQuery;- (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;
{if (self = [super init]){NSAssert(account != nil || service != nil, @"Both account and service are nil.  Must specifiy at least one.");// Begin Keychain search setup. The genericPasswordQuery the attributes kSecAttrAccount and// kSecAttrService are used as unique identifiers differentiating keychain items from one anothergenericPasswordQuery = [[NSMutableDictionary alloc] init];[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];[genericPasswordQuery setObject:account forKey:(id)kSecAttrAccount];[genericPasswordQuery setObject:service forKey:(id)kSecAttrService];// The keychain access group attribute determines if this item can be shared// amongst multiple apps whose code signing entitlements contain the same keychain access group.if (accessGroup != nil){
#if TARGET_IPHONE_SIMULATOR// Ignore the access group if running on the iPhone simulator.//// Apps that are built for the simulator aren't signed, so there's no keychain access group// for the simulator to check. This means that all apps can see all keychain items when run// on the simulator.//// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the// simulator will return -25243 (errSecNoAccessForItem).
#else[genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif}// Use the proper search constants, return only the attributes of the first match.[genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];[genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];NSMutableDictionary *outDictionary = nil;if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr){// Stick these default values into keychain item if nothing found.[self resetKeychainItem];//Adding the account and service identifiers to the keychain[keychainItemData setObject:account forKey:(id)kSecAttrAccount];[keychainItemData setObject:service forKey:(id)kSecAttrService];if (accessGroup != nil){
#if TARGET_IPHONE_SIMULATOR// Ignore the access group if running on the iPhone simulator.//// Apps that are built for the simulator aren't signed, so there's no keychain access group// for the simulator to check. This means that all apps can see all keychain items when run// on the simulator.//// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the// simulator will return -25243 (errSecNoAccessForItem).
#else[keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif}}else{// load the saved data from Keychain.self.keychainItemData = [self secItemFormatToDictionary:outDictionary];}[outDictionary release];}return self;
}- (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;
{if (self = [super init]){// Begin Keychain search setup. The genericPasswordQuery leverages the special user// defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain// items which may be included by the same application.genericPasswordQuery = [[NSMutableDictionary alloc] init];[genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];[genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];// The keychain access group attribute determines if this item can be shared// amongst multiple apps whose code signing entitlements contain the same keychain access group.if (accessGroup != nil){
#if TARGET_IPHONE_SIMULATOR// Ignore the access group if running on the iPhone simulator.// // Apps that are built for the simulator aren't signed, so there's no keychain access group// for the simulator to check. This means that all apps can see all keychain items when run// on the simulator.//// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the// simulator will return -25243 (errSecNoAccessForItem).
#else            [genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif}// Use the proper search constants, return only the attributes of the first match.[genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];[genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];NSMutableDictionary *outDictionary = nil;if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr){// Stick these default values into keychain item if nothing found.[self resetKeychainItem];// Add the generic attribute and the keychain access group.[keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];if (accessGroup != nil){
#if TARGET_IPHONE_SIMULATOR// Ignore the access group if running on the iPhone simulator.// // Apps that are built for the simulator aren't signed, so there's no keychain access group// for the simulator to check. This means that all apps can see all keychain items when run// on the simulator.//// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the// simulator will return -25243 (errSecNoAccessForItem).
#else            [keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];
#endif}}else{// load the saved data from Keychain.self.keychainItemData = [self secItemFormatToDictionary:outDictionary];}[outDictionary release];}return self;
}- (void)dealloc
{[keychainItemData release];[genericPasswordQuery release];[super dealloc];
}- (void)setObject:(id)inObject forKey:(id)key
{if (inObject == nil) return;id currentObject = [keychainItemData objectForKey:key];if (![currentObject isEqual:inObject]){[keychainItemData setObject:inObject forKey:key];[self writeToKeychain];}
}- (id)objectForKey:(id)key
{return [keychainItemData objectForKey:key];
}- (void)resetKeychainItem
{OSStatus junk = noErr;if (!keychainItemData) {self.keychainItemData = [[NSMutableDictionary alloc] init];}else if (keychainItemData){NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];junk = SecItemDelete((CFDictionaryRef)tempDictionary);NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );}// Default attributes for keychain item.[keychainItemData setObject:@"" forKey:(id)kSecAttrAccount];[keychainItemData setObject:@"" forKey:(id)kSecAttrLabel];[keychainItemData setObject:@"" forKey:(id)kSecAttrDescription];// Default data for keychain item.[keychainItemData setObject:@"" forKey:(id)kSecValueData];
}- (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert
{// The assumption is that this method will be called with a properly populated dictionary// containing all the right key/value pairs for a SecItem.// Create a dictionary to return populated with the attributes and data.NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];// Add the Generic Password keychain item class attribute.[returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];// Convert the NSString to NSData to meet the requirements for the value type kSecValueData.// This is where to store sensitive data that should be encrypted.NSString *passwordString = [dictionaryToConvert objectForKey:(id)kSecValueData];[returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];return returnDictionary;
}- (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert
{// The assumption is that this method will be called with a properly populated dictionary// containing all the right key/value pairs for the UI element.// Create a dictionary to return populated with the attributes and data.NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];// Add the proper search key and class attribute.[returnDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];[returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];// Acquire the password data from the attributes.NSData *passwordData = NULL;if (SecItemCopyMatching((CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData) == noErr){// Remove the search, class, and identifier key/value, we don't need them anymore.[returnDictionary removeObjectForKey:(id)kSecReturnData];// Add the password to the dictionary, converting from NSData to NSString.NSString *password = [[[NSString alloc] initWithBytes:[passwordData bytes] length:[passwordData length] encoding:NSUTF8StringEncoding] autorelease];[returnDictionary setObject:password forKey:(id)kSecValueData];}else{// Don't do anything if nothing is found.NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");}[passwordData release];return returnDictionary;
}- (void)writeToKeychain
{NSDictionary *attributes = NULL;NSMutableDictionary *updateItem = NULL;OSStatus result;if (SecItemCopyMatching((CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr){// First we need the attributes from the Keychain.updateItem = [NSMutableDictionary dictionaryWithDictionary:attributes];// Second we need to add the appropriate search key/values.[updateItem setObject:[genericPasswordQuery objectForKey:(id)kSecClass] forKey:(id)kSecClass];// Lastly, we need to set up the updated attribute list being careful to remove the class.NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];[tempCheck removeObjectForKey:(id)kSecClass];#if TARGET_IPHONE_SIMULATOR// Remove the access group if running on the iPhone simulator.// // Apps that are built for the simulator aren't signed, so there's no keychain access group// for the simulator to check. This means that all apps can see all keychain items when run// on the simulator.//// If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the// simulator will return -25243 (errSecNoAccessForItem).//// The access group attribute will be included in items returned by SecItemCopyMatching,// which is why we need to remove it before updating the item.[tempCheck removeObjectForKey:(id)kSecAttrAccessGroup];
#endif// An implicit assumption is that you can only update a single item at a time.result = SecItemUpdate((CFDictionaryRef)updateItem, (CFDictionaryRef)tempCheck);NSAssert( result == noErr, @"Couldn't update the Keychain Item." );}else{// No previous item found; add the new one.result = SecItemAdd((CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);NSAssert( result == noErr, @"Couldn't add the Keychain Item." );}
}@end

我们在写一个工具类用来保存UUID到keychain和从keychain中读取UUID.

实现代码

AppUntils.m文件
*********************#import  <Security/Security.h>
#import "KeychainItemWrapper.h"#pragma mark - 保存和读取UUID
+(void)saveUUIDToKeyChain{KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];NSString *string = [keychainItem objectForKey: (__bridge id)kSecAttrGeneric];if([string isEqualToString:@""] || !string){[keychainItem setObject:[self getUUIDString] forKey:(__bridge id)kSecAttrGeneric];}
}+(NSString *)readUUIDFromKeyChain{KeychainItemWrapper *keychainItemm = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];NSString *UUID = [keychainItemm objectForKey: (__bridge id)kSecAttrGeneric];return UUID;
}+ (NSString *)getUUIDString
{CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);CFStringRef strRef = CFUUIDCreateString(kCFAllocatorDefault , uuidRef);NSString *uuidString = [(__bridge NSString*)strRef stringByReplacingOccurrencesOfString:@"-" withString:@""];CFRelease(strRef);CFRelease(uuidRef);return uuidString;
}

写入UUID到keychain

我们最好在程序启动之后把UUID写入到keychain,代码如下:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{[AppUtils saveUUIDToKeyChain];
}

读取UUID

在需要读取的地方直接调用AppUtils的类方法readUUIDFromKeyChain即可。


注意

1.设置非ARC编译环境

因为KeychainItemWrapper.m文件是在非ARC环境下运行的,所以需要设置非arc编译环境,
如图所示:

image

2. 让同一开发商的所有APP在同一台设备上获取到UUID相同

在每个APP的项目里面做如下设置

2.1、设置accessgroup

keychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:@"YOUR_BUNDLE_SEED.com.yourcompany.userinfo"];

此处设置accessGroup为YOUR_BUNDLE_SEED.com.yourcompany.userinfo

2.2、创建plist文件

然后在项目相同的目录下创建KeychainAccessGroups.plist文件。

该文件的结构是一个字典,其中中最顶层的节点必须是一个键为“keychain-access-groups”的Array,并且该Array中每一项都是一个描述分组的NSString。YOUR_BUNDLE_SEED.com.yourcompany.userinfo就是要设置的组名。

如图:

image

2.3、 设置code signing

接着在Target--->Build Settings---->Code Signing栏下的Code Signing Entitlements右侧添加KeychainAccessGroups.plist

如图:

image

这样就可以保证每个app都是从keychain中读取出来同一个UUID


参考文章:

1、http://blog.sheliw.com/blog/2015/02/16/keychain/

2、http://cnbin.github.io/blog/2015/08/18/ios-keychain-ji-chu/

3、http://www.xuebuyuan.com/1963303.html


iOS10注意:

iOS10系统导入上面的两个类运行会崩溃,需要做如下处理:

Paste_Image.png

作者:西木柚子
链接:http://www.jianshu.com/p/2741f0124cd3
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

转载于:https://www.cnblogs.com/baitongtong/p/7339440.html

IOS - UDID IDFA IDFV MAC keychain相关推荐

  1. ios 设备获取idfa_一文读懂 UDID,IDFV,IDFA,OAID等概念

    概要 如果从事移动互联网行业,经常会涉及到用户的标识,UDID,IDFV,IDFA,OAID等概念就是用来标识用户. 场景 移动互联网经常需要统计使用产品的用户量(PV,UV)等.在投放广告的情况下, ...

  2. iOS编程——经过UUID和KeyChain来代替Mac地址实现iOS设备的唯一标示(OC版)

    iOS编程--通过UUID和KeyChain来代替Mac地址实现iOS设备的唯一标示(OC版) 很多的应用都需要用到手机的唯一标示,而且要求这个唯一标示不能因为应用app的卸载或者改变而变化. 在iO ...

  3. 新的 CSS 攻击会导致 iOS 系统重启或 Mac 冻结

    Wire 的安全研究员 Sabri Haddouche 发现了一种新的攻击,只需访问包含某些 CSS 和 HTML 的网页,就会导致 iOS 重新启动以及 macOS 冻结. Windows 和 Li ...

  4. .iOS APP Project or Mac APP Project编译错误提示: My Mac 64-bit is not valid for Running the scheme...

    1. iOS APP Project or  Mac APP Project编译错误提示: "The run destination My Mac 64-bit is not valid f ...

  5. Appium Doctor iOS Could not detect Mac OS X Version from sw_vers output: '10.12 '解决方法

    Appium Doctor  iOS Could not detect Mac OS X Version from sw_vers output: '10.12.3 '   解决方法 本人亲测验证,本 ...

  6. IOS UDID 6种方法在线获取

    IOS UDID: iOS设备的唯一识别码,每台iOS设备都有一个独一无二的编码,这个编码,就称为识别码,也叫做UDID(Unique Device Identifier) 获取方式: 1.蒲公英 l ...

  7. iOS - 广告标识符(IDFA IDFV)

    历史问题 在iOS5之前,获取设备唯一标识都是采用UDID. 12年9月份iOS6发布,IDFA面世,用于给开发者跟踪广告的投放效果. 一直到13年5月份App Store明令禁止获取UDID的应用上 ...

  8. ios如何看idfv_ios获取手机状态 idfa   idfv   网络类型   分辨率   获取运营商

    标签:ios //idfa [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString]; //idfv [[[UI ...

  9. 关于ios的IDFA

    了解IDFA,看我这篇文章就够了 双11剁手后,我静静的限制了广告追踪 今年双11爆了,据统计,全天交易额1207亿,移动端占比82%,在马云的持续教育和移动端的爆发下,用户在移动端消费的习惯已经不可 ...

最新文章

  1. HarmonyOS 2面世!是没有退路还是时机成熟?中国操作系统崛起元年或已到来
  2. 基于opencv的双线性插值的实现(一)
  3. LeetCode Mini Parser(栈操作)
  4. 用于 Outlook 2003 的删除重复邮件的插件(加载项)
  5. java break 在if 中使用_java中使用国密SM4算法详解
  6. SD-WAN三大部署方式 用户现身说法谈优劣势
  7. Protobuf的C++使用笔记
  8. UI框架-JQuery Smart / 淘宝JS库 KISSY UI
  9. 持续数据保护(CDP):Near-CDP vs. real-CDP
  10. java验证码短信发送_java发送短信验证码
  11. 搜狗输入法如何禁用所有快捷键包括系统快键键
  12. 从互联网大厂跳槽到国企后,我发现没有一劳永逸的工作。。。
  13. 类和结构体的内存空间占有问题
  14. 解决 Virtualbox 6.1.34 出现 End kernel panic - not syncing: attempted to kill the idle task
  15. Java进阶(八)Stream、异常体系
  16. 夜曲歌词 拼音_矢野真纪《夜曲》罗马拼音歌词
  17. 计算机谈音乐南山南,马頔:从《南山南》到《是首俗歌》,恰是我们难以言表的流年...
  18. 双流中学高2021年高考成绩查询,2020成都57所高中高考成绩大汇总,哪些学校考得好,家长心知肚明!...
  19. uview组件库表单验证,验证对象中的对象
  20. json常用方法,json如何使用,json是什么,json的作用

热门文章

  1. python打印机打印图片_Python综合应用:教你用字符打印一张怀旧风格的照片
  2. 如何使用MATLAB绘制绕线画?附带颜色渲染
  3. 42 学习工程与管理12h-北大吴峰谢克海老师
  4. linux下qt判断网线是否插入
  5. centos部署微博_linux环境下搭建个人微博
  6. Obsidian 彩色标题
  7. 基于SOFA的空间坐标系变换
  8. fastadmin学习笔记 03 数据查询关联搜索
  9. 智能技术发展迅速,嵌入式系统迎来市场新需
  10. 写给未来孩子的一封信