天天看点

Android数据持久化

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">{&quot;result&quot;:{&quot;account&quot;:&quot;wxx&quot;,&quot;hospitalId&quot;:4,&quot;hospitalInfo&quot;:{&quot;address&quot;:&quot;湖南省株洲市天元区001街道new&quot;,&quot;code&quot;:&quot;430211020100&quot;,&quot;contacts&quot;:&quot;修改哈哈!new&quot;,&quot;email&quot;:&quot;[email protected]&quot;,&quot;id&quot;:4,&quot;jzrRemark&quot;:&quot;接种点备注支付2.0测试&quot;,&quot;lat&quot;:27.841850,&quot;lng&quot;:113.136993,&quot;name&quot;:&quot;小豆苗预防接种门诊new&quot;,&quot;openId&quot;:&quot;3YddL5KzTMijwVcbsM6fkpp7p3AV9jqF9pGGkJnmRRLmjSS2tMivEhdZVGaPny&quot;,&quot;page&quot;:0,&quot;pageSize&quot;:0,&quot;postCode&quot;:&quot;522817&quot;,&quot;qq&quot;:&quot;3453241321&quot;,&quot;regionId&quot;:430211,&quot;remark&quot;:&quot;&quot;,&quot;setting&quot;:{&quot;hospitalId&quot;:4,&quot;id&quot;:4,&quot;isAppoint&quot;:1,&quot;isBeforeRemind&quot;:0,&quot;isDigital&quot;:1,&quot;isPayment&quot;:2,&quot;isPurchaseInAdvance&quot;:1,&quot;isSelectedVacc&quot;:1,&quot;isSwitch&quot;:1,&quot;isVccInventory&quot;:1,&quot;opDigitalclinic&quot;:1,&quot;opInfoConsent&quot;:1},&quot;shortName&quot;:&quot;门诊通知到了23&quot;,&quot;telephone&quot;:&quot;0731-221691778&quot;},&quot;id&quot;:19697,&quot;realName&quot;:&quot;wxx&quot;},&quot;error&quot;: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目录,外部存储目录就出现了。虽然可以通过多种路径打开外部存储文件,但是最终他们的路径是相同的:

Android数据持久化
Android数据持久化

对比下来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。