沙盒介绍:
获取路径参数介绍:
/**
NSSearchPathForDirectoriesInDomains 是用来查找文件路径的函数,其返回值是一个数组,这个数组中只有一个NSString类型元素,这个元素保存的就是查找的路径。
参数1: NSDocumentDirectory 表示需要查找的是Document文件夹
参数2:NSUserDomainMask 表示在用户的主目录中查找
参数3:YES 表示返回路径展开
*/
iOS 应用程序在安装时,会创建属于自己的沙盒文件,应用程序不能直接访问其他应用程序的沙盒文件,
当应用程序需要向外部请求或接收数据时,都需要经过权限认证,否则,无法获取到数据。
应用程序中所有的非代码文件都保存在沙盒中,比如图片、声音、属性列表,sqlite数据库和文本文件等
沙盒的的根目录有三个文件夹,分别是 Documents,Library,tmp
Documents:中一般保存应用程序本身产生文件数据,例如游戏进度,绘图软件的绘图等, iTunes备份和
恢复的时候,会包括此目录
注意:在此目录下不要保存从网络上下载的文件,否则app无法上架
Documents:
路径获取有3种方法
1.利用字符串拼接,在home目录后面拼接字符串Documents
//获取家目录
NSString*homeDocumentPath = NSHomeDirectory();
//拼接
NSString*documents = [homeDocumentPath stringByAppendingPathComponent:@"Documents"];
2.NSSearchPathForDirectoriesInDomains方法
NSString*path=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask,
YES).lastObject;
3.NSFileManager
[[[NSFileManager defaultManager] URLsForDirectory: NSDocumentDirectory
inDomains:NSUserDomainMask] lastObject]
Library:目录下有两个子目录:Caches 和 Preferences
Library/Caches/(这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息)
此目录用来保存应用程序运行时生成的需要持久化的数据,这些数据一般存储体积比较大,又不是十分重
要,比如网络请求数据等。这些数据需要用户负责删除。iTunes同步设备时不会备份该目录。
路径同样有三种获取方式:
NSString*homeDocumentPath = NSHomeDirectory();
1.
NSString*cachePath =
[homeDocumentPath stringByAppendingPathComponent:@"Library/Caches"];
2.
NSString*path=
NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask,
YES).lastObject;
3.
[[[NSFileManager defaultManager] URLsForDirectory: NSCachesDirectory
inDomains:NSUserDomainMask] lastObject]
Library/Preferences/(用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息)
此目录保存应用程序的所有偏好设置,iOS的Settings(设置)应用会在该目录中查找应用的设置信息。
iTunes同步设备时会备份该目录
在Preferences/下不能直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的
偏好.
获取路径同上(改一下标红部分即可)
tmp: iTunes不会备份这个目录,用来保存临时数据,应用退出时会清除该目录下的数据
NSString *tmp = NSTemporaryDirectory();
ios 常用的存储方式:
- NSUserDefaults(存在沙盒的Library/Preferences 目录文件夹里)
- 归档(序列化)(存在Documents文件夹下以生成data.archive文件形式存在.缺点:归档的形式来保存数据,只能⼀次性归档保存以及一次性解压。所以只能针对小量数据,而且对数据操作比较 笨拙,即如果想改动数据的某一小部分,还是需要解压整个数据或者归档整个数据)
- plist文件(沙盒的Documents中就创建了一个plist文件,并且写入数据保存)
- 数据库(SQLite3的使用还是比较麻烦的,因为都是些c语言的函数,理解起来有些困难。不过在一般开发过程中,使用的都是第三方开源库 FMDB,封装了这些基本的c语言方法,使得我们在使用时更加容易理解,提高开发效率,同样是存储到沙盒的Documents文件夹中,适合存储大量的内容)
- CoreData
- KeyChain
NSUserDefaults:NSUserDefaults适合存储轻量级的本地数据,比如要保存一个登陆界面的数据,用户名、
密码之类的(支持的数据格式有:NSNumber(Integer、Float、Double),NSString,
NSDate,NSArray,NSDictionary,BOOL类型).
/**
NSUserDefaults*defaults = [NSUserDefaultsstandardUserDefaults];
//保存值(key值同名的时候会覆盖的)
[defaults setObject:@"用户名"forKey:kUsernameKey];
//立即保存
[defaults synchronize];
//取值
NSString*username = [defaults objectForKey:kUsernameKey];
*/
另外的保存方法
//保存NSInteger
[defaults setInteger:(NSInteger) forKey:(nonnullNSString*)];
//保存BOOL
[defaults setBool:(BOOL) forKey:(nonnullNSString*)];
//保存NSURL
[defaults setURL:(nullableNSURL*) forKey:(nonnullNSString*)];
//保存float
[defaults setFloat:(float) forKey:(nonnullNSString*)];
//保存double
[defaults setDouble:(double) forKey:(nonnullNSString*)];
//取值
[defaults integerForKey:(nonnullNSString*)];
[defaults boolForKey:(nonnullNSString*)];
[defaults URLForKey:(nonnullNSString*)];
[defaults floatForKey:(nonnullNSString*)];
[defaults doubleForKey:(nonnullNSString*)];
归档(序列化):一般保存自定义的对象,但是只有遵守NSCoding的类才能使用归档归档保存自定义对象
定义一个Person类,如果想对person进行归档解档,首先要让Person遵守协议
/********Person.h*********/
#import
//遵守NSCoding协议
Person : NSObject
(nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;
//自定义的归档保存数据的方法
+(void)savePerson:(Person *)person;
//自定义的读取沙盒中解档出的数据
+(Person *)getPerson;
@end
NSCoding协议有2个方法:
(void)encodeWithCoder:(NSCoder *)aCoder
归档时调用这个方法,在方法中使用encodeObject:forKey:归档变量。
(instancetype)initWithCoder:(NSCoder *)aDecoder
解档时调用这个方法,在方法中石油decodeObject:forKey读出变量。
/******Person.m*******/
#import "Person.h"
@implementation Person
//归档,Key建议使用宏代替,这里就不使用了
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:self.name forKey:@"name"];
[aCoder encodeInteger:self.age forKey:@"age"];
}
//解档
-(instancetype)initWithCoder:(NSCoder *)aDecoder {
if (self=[super init]) {
self.name = [aDecoder decodeObjectForKey:@"name"];
self.age = [aDecoder decodeIntegerForKey:@"age"];
}
return self;
}
//类方法,运用NSKeyedArchiver归档数据
+(void)savePerson:(Person *)person {
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *path=[docPath stringByAppendingPathComponent:@"Person.plist"];
[NSKeyedArchiver archiveRootObject:person toFile:path];
}
//类方法,使用NSKeyedUnarchiver解档数据
+(Person *)getPerson {
NSString *docPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).lastObject;
NSString *path=[docPath stringByAppendingPathComponent:@"Person.plist"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:path];
return person;
}
@end
下面就可以在需要的地方归档或解档Person对象。
/*******ViewController.m*******/
//创建Person对象
Person *person = [Person new];
person.name = @"jack";
person.age = 17;
//归档保存数据
[Person savePerson:person];
//解档拿到数据
Person *resultPerson = [Person getPerson];
//打印出结果,证明归档解档成功
NSLog(@"name = %@, age = %ld",resultPerson.name,resultPerson.age);
SQLite
第⼀步:需要添加SQLite相关的库以及头文件:在项目文件的Build Phases下,找到Link Binary Library(ies),添加libsqlite3.0.dylib(libsqlite3.dylib与前者的区别暂时不知,两者应该差不多);在项目文件中头文件或者源文件中添加头文件 #import "/usr/include/sqlite3.h"
第二步:开始使用SQLite:
使用前注意:如果不往数据库里面添加任何的表,这个数据库等于没有建立,不会在硬盘上产生任何文件,如果数据库已经 存在,则会打开这个数据库。
NSArray*documentsPaths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask , YES);
NSString *databaseFilePath=[[documentsPaths objectAtIndex:0] stringByAppendingPathComponent:@"mydb"];//上面两句已经比较熟悉了吧!
//打开数据库 if (sqlite3_open([databaseFilePath UTF8String], &database)==SQLITE_OK) {NSLog(@"sqlite dadabase is opened."); }
else{ return;}//打开不成功就返回
在打开了数据库的前提下,如果数据库没有表,那就开始建表了哦!
char *error;
const char *createSql="create table(id integer primary key autoincrement, name text)"; if (sqlite3_exec(database, createSql, NULL, NULL, &error)==SQLITE_OK) {NSLog(@"create table is ok."); }
else {
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下⼀一次使用 }
建表完成之后,就开始插入记录:
const char *insertSql="insert into a person (name) values(‘gg’)";
if (sqlite3_exec(database, insertSql, NULL, NULL, &error)==SQLITE_OK) {NSLog(@"insert operation is ok."); }
else {
NSLog(@"error: %s",error);
sqlite3_free(error);//每次使用完毕清空error字符串,提供给下⼀一次使用 }
下⼀一步,查询记录:
const char *selectSql="select id,name from a person";
sqlite3_stmt *statement; if (sqlite3_prepare_v2(database,selectSql, -1, &statement, nil)==SQLITE_OK) {NSLog(@"select operation is ok."); }
else {
sqlite3_free(error); }
while(sqlite3_step(statement)==SQLITE_ROW) {
int _id=sqlite3_column_int(statement, 0); NSString *name=(char*)sqlite3_column_text(statement, 1); NSLog(@"row>>id %i, name %s",_id,name); } sqlite3_finalize(statement); 最后,关闭数据库: sqlite3_close(database);注意:写入数据库,字符串可以采用char方式,而从数据库中取出char类型,当char类型有表示中文字符 时,会出现乱码。这是因为数据库默认使用ascII(二进制)编码方式。所以要想正确从数据库中取出中文,需要用 NSString来接收从数据库取出的字符串。
iOS本地数据库FMDB的使用
FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便
FMDB优点:
1.使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码 2.对比苹果自带的CoreData框架,更加轻量级和灵活 3.提供多线程安全,有效地防止数据混乱,原来的SQLite不是线程安全FMDB缺点:
因为是OC语言封装的,失去了SQLite原来的跨平台性
我们要使用FMDB需要的步骤:
1.项目中添加libsqlite3库的依赖 2.导入FMDB源码: 3.下载FMDB的源代码,将代码文件拖入工程 4.#import导入FMDB的头文件"FMDatabase.h" 使用FMDB前,需要先了解下3个主要类: 1.FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。 2.FMResultSet :执行查询一个FMDatabase结果集。 3.FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。一般的FMDB数据库操作有:
1.创建数据库 2.打开数据库、关闭数据库 3.执行更新(增加、删除、修改)的SQL语句 4.执行查询的SQL语句一、打开数据库
通过指定SQLite数据库文件路径来创建FMDatabase对象 FMDatabase *db = [FMDatabase databaseWithPath:path]; if (![db open]) { NSLog(@"数据库打开失败!"); } 文件路径有三种情况 (1)具体文件路径 如果不存在会自动创建 (2)空字符串@"" 会在临时目录创建一个空的数据库 当FMDatabase连接关闭时,数据库文件也被删除 (3)nil 会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁 二、执行更新 在FMDB中,除查询以外的所有操作,都称为“更新” create、drop、insert、update、delete等 使用executeUpdate:方法执行更新 - (BOOL)executeUpdate:(NSString*)sql, ... - (BOOL)executeUpdateWithFormat:(NSString*)format, ... - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments 示例 [db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"] 三、执行查询 查询方法 - (FMResultSet *)executeQuery:(NSString*)sql, ... - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments 示例 // 查询数据 FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"]; // 遍历结果集 while ([rs next]) { NSString *name = [rs stringForColumn:@"name"]; int age = [rs intForColumn:@"age"]; double score = [rs doubleForColumn:@"score"]; } 代码示例://MARK:创建本地数据库
- - (void)CreateDatabase
- {
- NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
- NSString *dbPath = [docsPath stringByAppendingPathComponent:@"liantongzhanting.db"];
- FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
- if ([db open]) {
- NSString*createTable=@"create table firstcontentlist_tb (id integer primary key autoincrement,contentName text,fileName text,veision text);"
- "create table contentlist_tb (id integer primary key autoincrement,contentId text,contentName text,contentNo text, fileName text, filePath text,veision text);"
- "create table userinfo_tb(id integer primary key autoincrement, areaId text,email text,password text,status text,tel text,userId text,username text);"
- "create table collect_tb (id integer primary key autoincrement, collectImage text,filePath text,contentID text,collectTitle text,collectDesc text,collectText text,tel text,collectTime text,state text,collectType text);"
- "create table addcollect_tb (id integer primary key autoincrement, imagePath text,url text);"
- "create table loginfo_tb (id integer primary key autoincrement, tel text,resultDetail text,showroomName text,optDetail text,objectId text,resultCode text,optCode text,optTime text,logdel text);"
- "create table orderinfo_tb (id integer primary key autoincrement, orderTime text,customerCompany text,customerIndustry text,customerName text,customerDepartment text,customerJob text,customerPhone text,customerMail text,id text,state text);";
- BOOL result=[db executeStatements:createTable];
- if (result)
- {
- NSLog(@"创表成功");
- }else
- {
- NSLog(@"创表失败");
- }
- }
- [db close];
- }
增加数据:
//MARK:插入下载内容列表
- -(void)insertContentlist:(NSMutableArray*)data
- {
- [queue inDatabase:^(FMDatabase *db) {
- //打开数据库
- if ([db open]) {
- [db executeUpdate:[NSString stringWithFormat:@"delete from contentlist_tb"]];
- }
- }];
- if(data==nil||data.count<=0)
- {
- return;
- }
- NSString*insertContentSql=@"insert into contentlist_tb(contentId ,contentName ,contentNo,fileName,filePath ,veision) values (?,?,?,?,?,?)";
- [queue inDatabase:^(FMDatabase *db) {
- //打开数据库
- if ([db open]) {
- db.beginTransaction;
- for(int i=0;i<data.count;i++)
- {
- NSDictionary* contentData=[data objectAtIndex:i];
- NSString*contentId=[contentData objectForKey:@"contentId"];
- NSString*contentName=[contentData objectForKey:@"contentName"];
- NSString*contentNo=[contentData objectForKey:@"contentNo"];
- NSString*fileName=[contentData objectForKey:@"fileName"];
- NSString*filePath=[contentData objectForKey:@"filePath"];
- NSString*veision=[contentData objectForKey:@"veision"];
- NSArray*contentValues= [[NSArray alloc] initWithObjects:contentId,contentName,contentNo,fileName,filePath,veision,nil];
- [db executeUpdate:insertContentSql withArgumentsInArray:contentValues];
- }
- }
- db.commit;
- }];
- }
//操作收藏数据库,删除
- [queue inDatabase:^(FMDatabase *db) {
- //打开数据库
- if ([db open]) {
- [db executeUpdate:[NSString stringWithFormat:@"delete from collect_tb where id='%@'",idStr]];
- }
- }];
- FMDatabaseQueue* queue=[FMDatabaseQueue getSharedDatabaseQueue];
- [queue inDatabase:^(FMDatabase *db) {
- //打开数据库
- if ([db open]) {
- [db executeUpdate:@"UPDATE loginfo_tb SET logdel = ? WHERE optTime = ?",logdel,optTime];
- NSLog(@"operationtime=======%@",optTime);
- }
- }];
查询数据:
- //根据filename查询version
- arguments = [NSMutableArray array];
- [queue inDatabase:^(FMDatabase *db) {
- if ([db open]) {
- FMResultSet *rs = [db executeQuery:[NSString stringWithFormat:@"SELECT * FROM contentlist_tb where fileName='%@'",filename]];
- while ([rs next]) {
- [arguments addObject:[rs resultDictionary]];
- }
- [rs close];
- }
- }];
[queue inDatabase:^(FMDatabase *db) {
- if ([db open]) {
- FMResultSet *rs = [db executeQuery:@"SELECT * FROM contentlist_tb"];
- arguments = [NSMutableArray array];
- while ([rs next]) {
- [arguments addObject: [rs resultDictionary]];
- }
- [rs close];
- }
- }];
CoreData:
CoreData
是一门功能强大的数据持久化技术,位于SQLite数据库之上,它避免了SQL的复杂性,能让我们以更自然的方式与数据库进行交互。CoreData提供数据--OC对象映射关系来实现数据与对象管理,这样无需任何SQL语句就能操作他们。
CoreData
数据持久化框架是Cocoa API
的一部分,⾸次在iOS5 版本的系统中出现,它允许按照实体
-属性
-值模型
组织数据,并以XML
、⼆进制文件
或者SQLite数据⽂件
的格式持久化数据 1、可视化,且具有undo/redo能力
2、可以实现多种文件格式:
* NSSQLiteStoreType
* NSBinaryStoreType
* NSInMemoryStoreType'
* NSXMLStoreTyp3、
苹果官方API支持,与iOS结合更紧密
CoreData核心类与结构
NSManagedObjectContext
(数据上下文)
- 对象管理上下文,负责数据的实际操作(重要)
- 作用:插入数据,查询数据,删除数据,更新数据
NSPersistentStoreCoordinator
(持久化存储助理)
- 相当于数据库的连接器
- 作用:设置数据存储的名字,位置,存储方式,和存储时机
NSManagedObjectModel
(数据模型)
- 数据库所有表格或数据结构,包含各实体的定义信息
- 作用:添加实体的属性,建立属性之间的关系
- 操作方法:视图编辑器,或代码
NSManagedObject
(被管理的数据记录)
- 数据库中的表格记录
NSEntityDescription
(实体结构)
- 相当于表格结构
NSFetchRequest
(数据请求)
- 相当于查询语句
后缀为.xcdatamodeld
的包
- 里面是.xcdatamodel文件,用数据模型编辑器编辑
- 编译后为.momd或.mom文件
- 关系图:
手动创建CoreData
创建步骤如下:
1.创建模型文件 [相当于一个数据库]
2.添加实体 [一张表]
3.创建实体类 [相当模型--表结构]
4.生成上下文 关联模型文件生成数据库
0、手动创建CoreData数据时,我们创建一个和平常一样的工程,不需要勾选Use Core Data
:
1、创建模型文件
1、进入创建新文件,command+N
或者如下图
2、选择文件类型, 如下图:
3、设置文件名,如下图:
4、模型文件创建成功,会出现以后
2、创建实体
1、利用可视化的方式创建实体,实体的功能就类似于我们的Model类,具体操作如下如:
3、创建实体类
利用可视化创建了实体,但是我们要想获取对应的数据和名称,就必须关联类,因此要创建实体类,创建步骤如下:
1、进入创建新文件
2、选择文件类型, 实体类文件类型选择:NSManagedObject subclass
3、选择模型文件
4、选择实体
5、创建成功
实体文件创建成功后系统自动帮我们生成对应的类和属性
,类名
对应实体名称
, 属性
对应实体中的属性名称
;如果我们的属性是基本数据类型,那么默认会帮我们转换成NSNumber
类型的属性.
* 老版本:只生成一对文件,即类和属性都在一起
* 新版本:生成一对类文件,再生成一对类目,在类目中生成属性
4、生成上下文 关联数据库
在创建上下文和关联数据库之前我们先来看看想对应的关系依赖图:
从上图我们看到,要想生成上下文,需要有数据库助理
和模型
的支持;就像SQLite中一样,想要操作数据库,你必须要有数据库
,并且创建好表
;我们来看实现代码
- (void)viewDidLoad { [superviewDidLoad];
//1、创建模型对象
//获取模型路径
NSURL*modelURL = [[NSBundlemainBundle] URLForResource:@"School"withExtension:@"momd"];
//根据模型文件创建模型对象
NSManagedObjectModel*model = [[NSManagedObjectModelalloc] initWithContentsOfURL:modelURL];
//2、创建持久化助理
//利用模型对象创建助理对象
NSPersistentStoreCoordinator*store = [[NSPersistentStoreCoordinatoralloc] initWithManagedObjectModel:model];
//数据库的名称和路径
NSString*docStr = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString*sqlPath = [docStr stringByAppendingPathComponent:@"mySqlite.sqlite"];
NSLog(@"path = %@", sqlPath);
NSURL*sqlUrl = [NSURLfileURLWithPath:sqlPath];
//设置数据库相关信息
[store addPersistentStoreWithType:NSSQLiteStoreTypeconfiguration:nilURL:sqlUrl options:nilerror:nil];
//3、创建上下文
NSManagedObjectContext*context = [[NSManagedObjectContextalloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
//关联持久化助理
[context setPersistentStoreCoordinator:store];
_context = context;
}
- 这样之后,我们整个CoreData数据库就算创建完成,整个过程都是我们手动,这样对于
原理
能更好的理解; - 运行之后其实数据库就已经创建完成,进入对应路径下,我们能看到已经创建好的数据库文件;
- 利用工具打开后,发现里面对应的表也帮我们创建好了;
- 经过上面的步骤创建后,在后续的工作中我们就不需要进行任何和数据库相关的工作了,所有和数据库打交道的工作就都交给了CoreData来实现。
0.系统创建CoreData
利用系统自带的方式创建数据库和手动创建的方式内部步骤一样的,只是系统将创建模型文件、生成上下文、关联数据库的工作帮我们做了,这些工作我们无需再做;
说这么多,我们一起来看看如何利用系统自带来创建CoreData数据库对于利用系统自带创建的话,只需要两个步骤:
- 创建工程
- 创建实体已经关联实体类
创建工程
1、手动创建CoreData
数据库时,我们创建一个和平常一样的工程, 特别注意:一定要勾选Use Core Data
:
2、工程创建完后,系统会自动帮我们创建一个与工程同名的模型文件
;以及帮我们写好了生成上下文
和关联数据库
的代码
模型文件
生成上下文关联数据库
上面这些做好之后,我们只需要创建实体
,以及关联实体类
就可以
创建实体 关联实体类
创建实体和关联实体类和手动创建数据库
的方式是一样的,参照手动创建2、3
即可
iOS开发中使用keyChain保存用户密码
keyChain保存用户密码
对于一些轻量的数据,可以使用NSUserDefaults将其保存到应用的沙盒中的plist文件中。
那么对于用户密码是否可以如法炮制呢?答案是否定的。根据查询的资料介绍,如果沙盒被破解,或者手机被越狱,plist文件就会被获取,明文的用户密码就暴露了,导致安全问题。
加密后保存到plist文件中
基于第一个方案,自然可以想到是否可以对明文进行加密后保存呢?答案是肯定的。
iOS提供了多种加密算法,对于用户密码,通常采用的是MD5加密,该加密是不可逆的。使用MD5加密算法需要导入头文件:
#import <CommonCrypto/CommonDigest.h>
那么这个方法是最优方案吗?不是的,因为Apple提供了一个更好的机制:
使用keyChain保存用户密码
根据文档介绍,iOS设备中的keyChain是一个安全的存储容器,可以用来为不同应用保存敏感信息(用户名,密码,网络密码等)。同时,keyChain是一个相对独立的空间,当应用替换或删除时并不会删除keyChain的内容,这对用户来说就十分的便利了。
既然keyChain是Apple提供的专门用于保存敏感信息的容器,同时又具有相对的独立性,那么目前看来,使用keyChain来保存用户名和用户密码是最优的解决方案。
更新:经知友指正与查阅资料,证实iPhone越狱后,有技术手段获取keyChain内容。从用户角度来讲,谨慎越狱。从开发者角度来讲,对于这个安全隐患要有清醒认识。
实现keyChain保存用户密码
通过查询资料,可以知道Apple是提供了官方的一些方法的。但是本着不要重复造轮子,个人采取了学习其他开发者的代码的方法,对于功能实现原理做了解,不去深究细节。
首先,需要导入secutity.framework框架:为了进行实践,个人新建了一个keychain项目。而后:
- 在Xcode左侧文件列表中选中项目名称,然后选中TARGETS;
- 中间界面选中Build Phases,在Link Binary With Libraries中点击加号;
- 搜索找到secutity.framework导入即可,如下图所示:
其次,新建KeyChain类,用于实现向keyChain存储、读取和修改用户名密码。代码如下:
KeyChain.h文件
#import <Foundation/Foundation.h>
#import <Security/Security.h>
@interface KeyChain : NSObject
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service;
// save username and password to keychain
+ (void)save:(NSString *)service data:(id)data;
// load username and password from keychain
+ (id)load:(NSString *)service;
// delete username and password from keychain
+ (void)delete:(NSString *)serviece;
@end
KeyChain.m文件
#import "KeyChain.h"
@implementation KeyChain
+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service {
return [NSMutableDictionary dictionaryWithObjectsAndKeys:
(id)kSecClassGenericPassword,(id)kSecClass,
service, (id)kSecAttrService,
service, (id)kSecAttrAccount,
(id)kSecAttrAccessibleAfterFirstUnlock,(id)kSecAttrAccessible,
nil];
}
#pragma mark 写入
+ (void)save:(NSString *)service data:(id)data {
//Get search dictionary
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Delete old item before add new item
SecItemDelete((CFDictionaryRef)keychainQuery);
//Add new object to search dictionary(Attention:the data format)
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
//Add item to keychain with the search dictionary
SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
}
#pragma mark 读取
+ (id)load:(NSString *)service {
id ret = nil;
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
//Configure the search setting
//Since in our simple case we are expecting only a single attribute to be returned (the password) we can set the attribute kSecReturnData to kCFBooleanTrue
[keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
} @catch (NSException *e) {
NSLog(@"Unarchive of %@ failed: %@", service, e);
} @finally {
}
}
if (keyData)
CFRelease(keyData);
return ret;
}
#pragma mark 删除
+ (void)delete:(NSString *)service {
NSMutableDictionary *keychainQuery = [self getKeychainQuery:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
}
@end
为了验证KeyChain类,修改ViewController.m文件代码进行实验。具体如下:
#import "ViewController.h"
#import "KeyChain.h"
@interface ViewController ()
@end
//
NSString * const KEY_USERNAME_PASSWORD = @"com.company.app.usernamepassword";
NSString * const KEY_USERNAME = @"com.company.app.username";
NSString * const KEY_PASSWORD = @"com.company.app.password";
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSMutableDictionary *userNamePasswordKVPairs = [NSMutableDictionary dictionary];
[userNamePasswordKVPairs setObject:@"userName" forKey:KEY_USERNAME];
[userNamePasswordKVPairs setObject:@"password" forKey:KEY_PASSWORD];
NSLog(@"%@", userNamePasswordKVPairs); //有KV值
// A、将用户名和密码写入keychain
[KeyChain save:KEY_USERNAME_PASSWORD data:userNamePasswordKVPairs];
// B、从keychain中读取用户名和密码
NSMutableDictionary *readUsernamePassword = (NSMutableDictionary *)[KeyChain load:KEY_USERNAME_PASSWORD];
NSString *userName = [readUsernamePassword objectForKey:KEY_USERNAME];
NSString *password = [readUsernamePassword objectForKey:KEY_PASSWORD];
NSLog(@"username = %@", userName);
NSLog(@"password = %@", password);
// C、将用户名和密码从keychain中删除
[KeyChain delete:KEY_USERNAME_PASSWORD];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
实际遇到的问题与解决方法
按理说,运行项目,应该得到的结果应该是得到输出:
2016-10-02 04:10:59.463 keychain[16953:1510836] username = userName
2016-10-02 04:10:59.463 keychain[16953:1510836] password = password
然而我在尝试的时候,却出现了两个问题:
第一个问题
在输出中出现了大量个怪异输出如下:
subsystem: com.apple.UIKit, category: HIDEventFiltered, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0
subsystem: com.apple.UIKit, category: HIDEventIncoming, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0
subsystem: com.apple.BaseBoard, category: MachPort, enable_level: 1, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0
subsystem: com.apple.UIKit, category: StatusBar, enable_level: 0, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 1, privacy_setting: 2, enable_private_data: 0
subsystem: com.apple.BackBoardServices.fence, category: App, enable_level: 1, persist_level: 0, default_ttl: 0, info_ttl: 0, debug_ttl: 0, generate_symptoms: 0, enable_oversize: 0, privacy_setting: 0, enable_private_data: 0
通过在Stackoverflow上查询,发现这是Xcode8更新后出现的普遍问题,解决方法是:
- 按顺序选择Product–>Scheme–>Edict Scheme;
- 选择Run中的Arguments;
- 选择Environment Variables;
- 添加OS_ACTIVITY_MODE字段并设置Value值为disable;
- 点击Close即可。
而后运行项目,问题解决。
第二个问题
程序关于用户名和密码的输出实际为:
2016-10-02 04:10:59.463 keychain[16953:1510836] username = (null)
2016-10-02 04:10:59.463 keychain[16953:1510836] password = (null)
通过分步NSLog可以发现,NSMutableDictionary类的对象userNamePasswordKVPairs是有键值对的,由此可以判断问题还是在KeyChain类的方法本身。受第一个问题的启发,猜测可能和Xcode或iOS的更新有关,经过查询Stackoverflow,证实了这一个猜测。解决方法:
- 在Xcode左侧文件列表中选中项目名称,然后选中TARGETS;
- 中间界面选中Capabilities;
- 将Keychain Sharing的开关由off改为on,如下图所示:
而后运行项目,问题解决,得到输出如下
2016-10-02 04:10:59.463 keychain[16953:1510836] username = userName
2016-10-02 04:10:59.463 keychain[16953:1510836] password = password