Android数据持久化(存储)
1.SharedPreferences
SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在同步不互斥问题
基本使用,haredPreferences的实现:
存入
mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = mSharedPreferences.edit();
editor.putString(key, value);
editor.apply();
获取
mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
mSharedPreferences .getString(key, defaultValue);
与SharePreferences对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下,后缀一定是.xml,数据存储样式如下:
<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
<string name="client.no">2</string>
<string name="login.info">{"result":{"account":"wxx","hospitalId":4,"hospitalInfo":{"address":"湖南省株洲市天元区001街道new","code":"430211020100","contacts":"修改哈哈!new","email":"ylhkaixin@163.com","id":4,"jzrRemark":"接种点备注支付2.0测试","lat":27.841850,"lng":113.136993,"name":"小豆苗预防接种门诊new","openId":"3YddL5KzTMijwVcbsM6fkpp7p3AV9jqF9pGGkJnmRRLmjSS2tMivEhdZVGaPny","page":0,"pageSize":0,"postCode":"522817","qq":"3453241321","regionId":430211,"remark":"","setting":{"hospitalId":4,"id":4,"isAppoint":1,"isBeforeRemind":0,"isDigital":1,"isPayment":2,"isPurchaseInAdvance":1,"isSelectedVacc":1,"isSwitch":1,"isVccInventory":1,"opDigitalclinic":1,"opInfoConsent":1},"shortName":"门诊通知到了23","telephone":"0731-221691778"},"id":19697,"realName":"wxx"},"error":0}</string>
<string name="client.manager.host.ip">192.168.0.104</string>
<string name="login.name">wxx</string>
<long name="login.last.update.time" value="1594888957260" />
<int name="client.type" value="1" />
</map>
Editor是一个接口,这里的实现是一个EditorImpl对象,它首先批量预处理更新操作,之后再提交更新,在提交事务的时候有两种方式,一种是apply,另一种commit,两者的区别在于:何时将数据持久化到xml文件,前者是异步的,后者是同步的。Google推荐使用前一种,因为,就单进程而言,只要保证内存缓存正确就能保证运行时数据的正确性,而持久化,不必太及时
SharedPreferences存储的特点
1、sharedPerferences是一种轻量级的存储方式。
2、只支持JAVA基本数据类型,不支持自定义的数据类型。
3、应用内数据可以共享。
4、使用简单,方便。
getSharedPreferences()源码分析
public SharedPreferences getSharedPreferences(String name, int mode) {
Class var4 = ContextImpl.class;
SharedPreferencesImpl sp;
synchronized(ContextImpl.class) {
//一个全局的静态对象,定义如下:private static ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>> sSharedPrefs;
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap();
}
//根据应用程序的包名在程序的执行环境中取到该应用程序对应的SharedPreferences文件组
String packageName = this.getPackageName();
ArrayMap<String, SharedPreferencesImpl> packagePrefs = (ArrayMap)sSharedPrefs.get(packageName);
//如果不存在就创建,并缓存到全局的ArrayMap(sSharedPrefs)中
if (packagePrefs == null) {
packagePrefs = new ArrayMap();
sSharedPrefs.put(packageName, packagePrefs);
}
if (this.mPackageInfo.getApplicationInfo().targetSdkVersion < 19 && name == null) {
name = "null";
}
//根据文件的Name去拿到对应的SharedPreferencesImpl对象,如果没有就创建,并放入到自己的上一级缓存packagePrefs中
sp = (SharedPreferencesImpl)packagePrefs.get(name);
if (sp == null) {
File prefsFile = this.getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & 4) != 0 || this.getApplicationInfo().targetSdkVersion < 11) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
总结起来就是:由于ContextImpl是应用程序的执行环境,每个应用程序里面可以包含有多个SharedPreference文件。因此,为了更好的定位SharedPreference文件,首先根据应用程序进行筛选,得到ArrayMap 然后再通过SharedPreference文件名进行筛选,得到SharedPreferencesImpl。可以看到,SharedPreferencesImpl只会被创建一次,之后会被保存在缓存中,后续的获取操作都是从缓存中获取SharedPreferencesImpl实例对象。
2.file
Internal Storage VS External Storage
在使用File存储App数据时,我们需要了解Android系统的存储系统。Android的存储分为内部存储和外部存储。
所有的Android设备都有两块存储区域:Internal Storage和External Storage。它们的名称来源于早期的Android系统,那时候大家的手机都内置(Permanent)一块较小存储板(即Internal Storage),并配上一个的外置的(Removable)储存卡(即External Storage)。后来部分手机开始将最初定义的“Internal Storage”,即内置存储,分成Internal和External两部分。这样一来就算没有外置储存,手机也有Internal和External两块存储区域。
Internal Storage
- /data/data/package name/files
- /data/data/package name/cache
- /data/data/package name/databases
- /data/data/package name/shared_prefs
External Storage
由于Android系统的厂商比较多,对于外部存储目录的定义有所不同,可能在根目录下的mnt,sdcard和storage下。以storage为例,打开emulated/0目录,外部存储目录就出现了。虽然可以通过多种路径打开外部存储文件,但是最终他们的路径是相同的:
对比下来External有以下几点优点:
通过USB可以将数据传到电脑上
可以与其他App共享数据
在Android/data/package name/路径以外的数据不会因为程序卸载而被删除
缺点:
外置存储有时不可用
在非root情况下,数据无法私有化
在Android/data/package name/路径以外存储数据需要申请写入权限
Internal Storage存储数据
Android数据存储之File方法getFilesDir()和openFileOutput(String name)返回的路径相同
Android数据存储之SharedPreferences
Databases(数据库)
Content Provider
External Storage存储数据
首先我们要获取外部存储目标文件的路径:
目标目录 获取方法
公有目录九大文件 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
公有根目录 Environment.getExternalStorageDirectory()
私有目录file Context.getExternalFilesDir(Environment.DIRECTORY_MUSIC)
私有目录cache Context.getExternalCacheDir()
然后确定自己需要创建的文件名,结合上面的到的路径,创建一个File对象:
private void saveExternal(String str) {
FileOutputStream outputStream = null;
DataOutputStream out = null;
try{
try{
outputStream = new FileOutputStream(
new File(Environment.getExternalStorageDirectory(),
"data_external.dat"));
out = new DataOutputStream(new BufferedOutputStream(outputStream));
out.writeUTF(str);
}finally {
out.close();
}
}catch (IOException e) {
e.printStackTrace();
}
}
上述表格中的方法,有两个方法需要传入一个String类型的参数,这个参数我们使用了Environment中的常量,参数的意思是我们要访问这个路径下的哪个文件夹
App数据清理
Android系统默认数据清理的路径是,内部存储目录中相应的cache文件夹中的文件和外部存储中相应的cache文件夹中的文件。
细节
内部存储
你的app的internal storage 目录是以你的app的包名作为标识存放在Android文件系统的特定目录下[data/data/com.example.xx]。 从技术上讲,如果你设置文件为可读的,那么其他app就可以读取你的internal文件。然而,其他app需要知道你的包名与文件名。若是你没有设置为可读或者可写,其他app是没有办法读写的。因此只要你使用MODE_PRIVATE ,那么这些文件就不可能被其他app所访问。
内部存储在你的APP卸载的时候,会一块被删除,因此,我们可以在cache目录里面放置我们的图片缓存,而且cache与files的差别在于,如果手机的内部存储空间不够了,会自行选择cache目录进行删除,因此,不要把重要的文件放在cache文件里面,可以放置在files里面,因为这个文件只有在APP被卸载的时候才会被删除。还有要注意的一点是,如果应用程序是更新操作,内部存储不会被删除,区别于被用户手动卸载。
外部存储
不管是使用 getExternalStoragePublicDirectory() 来存储可以共享的文件,还是使用 getExternalFilesDir() 来储存那些对于app来说是私有的文件,有一点很重要,那就是要使用那些类似DIRECTORY_PICTURES 的API的常量。那些目录类型参数可以确保那些文件被系统正确的对待。例如,那些以DIRECTORY_RINGTONES 类型保存的文件就会被系统的media scanner认为是ringtone而不是音乐。
清除数据、清除缓存的区别
清除数据主要是清除用户配置,比如SharedPreferences、数据库等等,这些数据都是在程序运行过程中保存的用户配置信息,清除数据后,下次进入程序就和第一次进入程序时一样
缓存是程序运行时的临时存储空间,它可以存放从网络下载的临时图片,从用户的角度出发清除缓存对用户并没有太大的影响,但是清除缓存后用户再次使用该APP时,由于本地缓存已经被清理,所有的数据需要重新从网络上获取。为了在清除缓存的时候能够正常清除与应用相关的缓存,请将缓存文件存放在getCacheDir()或者 getExternalCacheDir()路径下
3.SQLite存储数据
SQLite是轻量级嵌入式数据库引擎,它支持 SQL 语言,并且只利用很少的内存就有很好的性能。现在的主流移动设备像Android、iPhone等都使用SQLite作为复杂数据的存储引擎,在我们为移动设备开发应用程序时,也许就要使用到SQLite来存储我们大量的数据,所以我们就需要掌握移动设备上的SQLite开发技巧
SQLiteDatabase类为我们提供了很多种方法,上面的代码中基本上囊括了大部分的数据库操作;对于添加、更新和删除来说,我们都可以使用
1 db.executeSQL(String sql);
2 db.executeSQL(String sql, Object[] bindArgs);//sql语句中使用占位符,然后第二个参数是实际的参数集
除了统一的形式之外,他们还有各自的操作方法:
1 db.insert(String table, String nullColumnHack, ContentValues values);
2 db.update(String table, Contentvalues values, String whereClause, String whereArgs);
3 db.delete(String table, String whereClause, String whereArgs);
以上三个方法的第一个参数都是表示要操作的表名;insert中的第二个参数表示如果插入的数据每一列都为空的话,需要指定此行中某一列的名称,系统将此列设置为NULL,不至于出现错误;insert中的第三个参数是ContentValues类型的变量,是键值对组成的Map,key代表列名,value代表该列要插入的值;update的第二个参数也很类似,只不过它是更新该字段key为最新的value值,第三个参数whereClause表示WHERE表达式,比如“age > ? and age < ?”等,最后的whereArgs参数是占位符的实际参数值;delete方法的参数也是一样
数据的添加
1.使用insert方法
ContentValues cv = new ContentValues();//实例化一个ContentValues用来装载待插入的数据
cv.put("title","you are beautiful");//添加title
cv.put("weather","sun"); //添加weather
cv.put("context","xxxx"); //添加context
String publish = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
.format(new Date());
cv.put("publish ",publish); //添加publish
db.insert("diary",null,cv);//执行插入操作
2.使用execSQL方式来实现
String sql = "insert into user(username,password) values (‘Jack Johnson’,‘iLovePopMuisc’);//插入操作的SQL语句
db.execSQL(sql);//执行SQL语句
数据的删除
同样有2种方式可以实现
String whereClause = "username=?";//删除的条件
String[] whereArgs = {"Jack Johnson"};//删除的条件参数
db.delete("user",whereClause,whereArgs);//执行删除
使用execSQL方式的实现
String sql = "delete from user where username='Jack Johnson'";//删除操作的SQL语句
db.execSQL(sql);//执行删除操作
数据修改
同上,仍是2种方式
ContentValues cv = new ContentValues();//实例化ContentValues
cv.put("password","iHatePopMusic");//添加要更改的字段及内容
String whereClause = "username=?";//修改条件
String[] whereArgs = {"Jack Johnson"};//修改条件的参数
db.update("user",cv,whereClause,whereArgs);//执行修改
使用execSQL方式的实现
String sql = "update user set password = 'iHatePopMusic' where username='JackJohnson'";//修改的SQL语句
db.execSQL(sql);//执行修改
数据查询
db.rawQuery(String sql, String[] selectionArgs);
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy);
db.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
db.query(String distinct, String table, String[] columns, String selection,
String[] selectionArgs, String groupBy, String having, String orderBy, String limit);
上面几种都是常用的查询方法,第一种最为简单,将所有的SQL语句都组织到一个字符串中,使用占位符代替实际参数,selectionArgs就是占位符实际参数集;
各参数说明:
table:表名称
colums:表示要查询的列所有名称集
selection:表示WHERE之后的条件语句,可以使用占位符
selectionArgs:条件语句的参数数组
groupBy:指定分组的列名
having:指定分组条件,配合groupBy使用
orderBy:y指定排序的列名
limit:指定分页参数
distinct:指定“true”或“false”表示要不要过滤重复值
Cursor:返回值,相当于结果集ResultSet
最后,他们同时返回一个Cursor对象,代表数据集的游标,有点类似于JavaSE中的ResultSet。下面是Cursor对象的常用方法:
c.move(int offset); //以当前位置为参考,移动到指定行
c.moveToFirst(); //移动到第一行
c.moveToLast(); //移动到最后一行
c.moveToPosition(int position); //移动到指定行
c.moveToPrevious(); //移动到前一行
c.moveToNext(); //移动到下一行
c.isFirst(); //是否指向第一条
c.isLast(); //是否指向最后一条
c.isBeforeFirst(); //是否指向第一条之前
c.isAfterLast(); //是否指向最后一条之后
c.isNull(int columnIndex); //指定列是否为空(列基数为0)
c.isClosed(); //游标是否已关闭
c.getCount(); //总数据项数
c.getPosition(); //返回当前游标所指向的行数
c.getColumnIndex(String columnName);//返回某列名对应的列索引值
c.getString(int columnIndex); //返回当前行指定列的值
实现代码
String[] params = {12345,123456};
Cursor cursor = db.query("user",columns,"ID=?",params,null,null,null);//查询并获得游标
if(cursor.moveToFirst()){//判断游标是否为空
for(int i=0;i<cursor.getCount();i++){
cursor.move(i);//移动到指定记录
String username = cursor.getString(cursor.getColumnIndex("username");
String password = cursor.getString(cursor.getColumnIndex("password"));
}
}
通过rawQuery实现的带参数查询
Cursor result=db.rawQuery("SELECT ID, name, inventory FROM mytable");
//Cursor c = db.rawQuery("s name, inventory FROM mytable where ID=?",new Stirng[]{"123456"});
result.moveToFirst();
while (!result.isAfterLast()) {
int id=result.getInt(0);
String name=result.getString(1);
int inventory=result.getInt(2);
// do something useful with these
result.moveToNext();
}
result.close();
我们完成了对数据库的操作后,记得调用SQLiteDatabase的close()方法释放数据库连接,否则容易出现SQLiteException。