Android 开发-----数据存储

数据一般有以下保存方式实现:

  • SharedPreferences 
  • 采用java.io.* 库所提供的I/O 接口,读写文件。
  • SQLite 数据库
  • ContentProvider 

一.SharedPreferences 

  SharedPreferences 是一种轻量级的数据保存方式,比较类似于我们常用的ini文件,用来保存运用程序的一些属性设置,较简单的参数设置。SharedPreferences将NVP(Name/Value Pair,键值对)保存在Android的文件系统,为XML文件。SharedPreferences 将对文件系统的操作过程封装起来,开发人员仅通过SharedPreferences 的API 即可完成读写过程。

1.三种数据访问模式

  • 私有(MODE_PRIVATE):仅创建程序可读写。
  • 全局读(MODE_WORLD_READABLE): 创建程序可读写,其他程序可读不可写。
  • 全局写(MODE_WORLD_WRITEABLE):创建程序可读写,其他程序都可写不可读。

注意:根据官网文档,后两种全局模式在API 17 之后已被弃用(deprecated),强烈不推荐使用SharedPreferences 来实现数据共享。开发者应该选择ContentProvider, 广播和服务等数据共享方式。Android N 中使用这两种模式会抛出异常。

2.SharedPreferences的使用

a.获取SharedPreferences 

  • getSharedPreferences() — 如果需要多个通过名称参数来区分的shared preference文件, 名称可以通过第一个参数来指定。可在app中通过任何一个Context 执行该方法。
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
        "SharedPreferences_file_name", Context.MODE_PRIVATE);
  • getPreferences() — 当activity仅需要一个shared preference文件时。因为该方法会检索activity下默认的shared preference文件,并不需要提供文件名称。
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

b.写SharedPreferences 

  执行写操作需要同edit()创建一个SharedPreferences.Editor,然后通过类似putInt和putString的方法传递key-value,接着调用Editor 对象的commit() 或apply() 方法保存修改内容。两者的区别在于:commit() 函数会立即将修改结果同步地写入文件中。而apply() 则采用异步的方式写入文件。在主线程(UI线程)中,建议使用apply() 而非commit() 以防阻塞。

  支持数据类型:boolean, float, int, long, String 和Set<String>.

SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putBoolean("RunInBackGround", true);
editor.putFloat("Height", 163.5f);
editor.putInt("Age", 20);
editor.putLong("TimeStamp", 1476931843L);
editor.putString("Name", "Tom");
Set<String> tags = new HashSet<>();
tags.add("Android");
editor.putStringSet("Tags", tags);
editor.apply();

c.读SharedPreferences 

 为了从shared preference中读取数据,可以通过类似于 getInt() 及 getString()等方法来读取。在那些方法里面传递我们想要获取的value对应的key,并提供一个默认的value作为查找的key不存在时函数的返回值。如下:

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int defaultValue = getResources().getInteger(R.string.saved_high_score_default);
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), default);  

二.文件存储

  所有的Android设备均有两个文件存储区域:"internal" 与 "external" 。 这两个名称来自于早先的Android系统,当时大多设备都内置了不可变的内存(internal storage)及一个类似于SD card(external storage)这样的可卸载的存储部件。之后有一些设备将"internal" 与 "external" 都做成了不可卸载的内置存储,虽然如此,但是这一整块还是从逻辑上有被划为"internal"与"external"的。只是现在不再以是否可卸载进行区分了。 下面列出了两者的区别:

  • Internal storage:

    • 总是可用的
    • 这里的文件默认只能被我们的app所访问。
    • 当用户卸载app的时候,系统会把internal内该app相关的文件都清除干净。
    • Internal是我们在想确保不被用户与其他app所访问的最佳存储区域。
  • External storage:

    • 并不总是可用的,因为用户有时会通过USB存储模式挂载外部存储器,当取下挂载的这部分后,就无法对其进行访问了。
    • 是大家都可以访问的,因此保存在这里的文件可能被其他程序访问。
    • 当用户卸载我们的app时,系统仅仅会删除external根目录(getExternalFilesDir())下的相关文件。
    • External是在不需要严格的访问权限并且希望这些文件能够被其他app所共享或者是允许用户通过电脑访问时的最佳存储区域。
    • 无论是否支持SD 卡等外置存储设备,所有Android 手机都将存储空间划分为「内部存储」与「外部存储」两部分。

  一般默认安装到internal storage的,可以通过在manifest文件中声明android:installLocation来指定安装到external storage中。 

1.Internal storage:

  Android 系统允许应用程序创建仅能够自身访问的私有文件,文件保存在设备的内部存储器上,路径为:/data/data/<package name>/files/。

Android系统支持四种文件操作模式:

  • MODE_PRIVATE私有模式,文件仅能够被文件创建程序访问,或具有相同UID的程序访问。
  • MODE_APPEND追加模式,如果文件已经存在,则在文件的结尾处添加新数据。
  • MODE_WORLD_READABLE全局读模式,允许任何程序读取私有文件。
  • MODE_WORLD_WRITEABLE全局写模式,允许任何程序写入私有文件

获取目录:

  • getFilesDir() : 返回一个File,代表了我们app的internal目录。
  • getCacheDir() : 返回一个File,代表了我们app的internal缓存目录。请确保这个目录下的文件能够在一旦不再需要的时候马上被删除,并对其大小进行合理限制,例如1MB 。系统的内部存储空间不够时,会自行选择删除缓存文件。

可以使用File构造器在目录下创建新的文件,如下:

File file = new File(context.getFilesDir(), filename);

获取/新建文件:

openFileOutput():

  用于写入数据,如果指定文件不存在,则创建一个新的文件 

  函数签名:public FileOutputStreamopenFileOutput(String name, intmode),返回FileOuputStream对象

Stringfilename ="myfile";
Stringstring="Helloworld!";
FileOutputStreamoutputStream;
    try{
        outputStream=openFileOutput(filename,Context.MODE_PRIVATE);
        outputStream.write(string.getBytes());
        outputStream.close();
    }catch(Exceptione){
        e.printStackTrace();
    }            

openFileInput():

  用于打开一个与应用程序关联的私有文件输入流

  函数签名:public FileInputStreamopenFileInput(String name)

  当指定文件名对应的文件不存在时,会抛出FileNotFound异常

FileInputStreamfileInputStream;
final String FILE_NAME = "file.txt",
try {
  fileInputStream= openFileInput(FILE_NAME);
  byte[] contents = new byte[fileInputStream.available()];
fileInputStream.read(contents);
} catch (IOExceptionex) {
  Log.e("TAG", "Fail to read file.");
}

 如果需要缓存一些文件,可以使用createTempFile()。例如:下面的方法从URL中抽取了一个文件名,然后再在程序的internal缓存目录下创建了一个以这个文件名命名的文件。 

 public File getTempFile(Context context, String url) {
    File file;
    try {
        String fileName = Uri.parse(url).getLastPathSegment();
        file = File.createTempFile(fileName, null, context.getCacheDir());
    catch (IOException e) {
        // Error while creating file
    }
    return file;
}  

2.external storage:

获取External存储的权限:

要往外部存储控件中写入文件,必须在AndroidManifest.xml 中声明权限。

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

检查External存储是否可用:

  因为外部存储可能是不可用的,比如遇到SD卡被拔出等情况时。因此在访问之前应对其可用性进行检查。我们可以通过执行 getExternalStorageState()来查询external storage的状态。若返回状态为MEDIA_MOUNTED, 则为可用。

 /* 检查外部存储可读写*/
public boolean isExternalStorageWritable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state)) {
        return true;
    }
    return false;
}

/* 检查外部存储至少可读*/
public boolean isExternalStorageReadable() {
    String state = Environment.getExternalStorageState();
    if (Environment.MEDIA_MOUNTED.equals(state) ||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
        return true;
    }
    return false;
}

类型: 

  • Public files :这些文件对与用户与其他app来说是public的,当用户卸载我们的app时,这些文件应该保留。例如,那些被我们的app拍摄的图片或者下载的文件。
  • Private files: 这些文件完全被我们的app所私有,它们应该在app被卸载时删除。尽管由于存储在external storage,那些文件从技术上而言可以被用户与其他app所访问,但实际上那些文件对于其他app没有任何意义。因此,当用户卸载我们的app时,系统会删除其下的private目录。例如,那些被我们的app下载的缓存文件。

  想文件已public形式保存在外部存储中,getExternalStoragePublicDirectory(String))来获取目录,其中的String 参数指的是目录类型,可以为DIRECTORY_MUSIC, DIRECTORY_PICTURES 等。若无指定类型,可以传入null,函数则会返回files 目录本身。

public File getAlbumStorageDir(String albumName) {
    // Get the directory for the user's public pictures directory.
    File file = new File(Environment.getExternalStoragePublicDirectory(
            Environment.DIRECTORY_PICTURES), albumName);
    if (!file.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created");
    }
    return file;
}

  想文件已private形式保存在外部存储中,getExternalFilesDir()来获取目录。

检查剩余空间:

  可以通过执行getFreeSpace() or getTotalSpace() 来判断是否有足够的空间来保存文件,从而避免发生异常。

删除文件:

myFile.delete();

  如果文件是保存在internal storage,我们可以通过Context来访问并通过执行deleteFile()进行删除

myContext.deleteFile(fileName);

Note:

当用户卸载我们的app时,android系统会删除以下文件:

  • 所有保存到internal storage的文件。
  • 所有使用getExternalFilesDir()方式保存在external storage的文件。

通常来说,我们应该手动删除所有通过 getCacheDir() 方式创建的缓存文件,以及那些不会再用到的文件。

3.资源文件:

如何读取原始格式文件: 

  • 首先调用getResource() 函数获得资源对象
  • 然后通过调用资源对象的openRawResource() 函数,以二进制流的形式打开文件
  • 在读取文件结束后,调用close()函数关闭文件流 
InputStreaminput = this.getResources().openRawResource(R.raw.filename);
byte[] reader = new byte[inputStream.available()];
while (inputStream.read(reader) != -1) {}
txt_text.setText(new String(reader,“utf-8”));//获得Context资源
input.close();//关闭输入流

读取XML格式文件:  

首先通过调用资源对象的getXml() 函数,获取到XML解析器XmlPullParser。

XmlPullParser parser = resources.getXml(R.xml.toys);
//通过资源对象的getXml()函数获取到XML解析器
while (parser.next() != XmlPullParser.END_DOCUMENT) {
String toys = parser.getName(); // getName()函数获得元素的名称
......
} 

获取元素个数:

int count = parser.getAttributeCount();

获得属性名和属性值:

String attrName= parser.getAttributeName(i); / /获得属性名
String attrValue= parser.getAttributeValue(i); // 获得属性值

XmlPullParser的XML事件类型:

  • START_TAG:读取到标签开始标志
  • TEXT:读取文本内容
  • END_TAG:读取到标签结束标志
  • END_DOCUMENT:文档末尾

  读取文件过程中遇到END_DOCUMENT 时停止分析

三.SQLite数据库

定义Schema与Contract:

  SQL中一个重要的概念是schema:一种DB结构的正式声明,用于表示database的组成结构。schema是从创建DB的SQL语句中生成的。我们会发现创建一个伴随类(companion class)是很有益的,这个类称为合约类(contract class),它用一种系统化并且自动生成文档的方式,显示指定了schema样式。  

  Contract Clsss是一些常量的容器。它定义了例如URIs,表名,列名等。这个contract类允许在同一个包下与其他类使用同样的常量。 它让我们只需要在一个地方修改列名,然后这个列名就可以自动传递给整个code。

  组织contract类的一个好方法是在类的根层级定义一些全局变量,然后为每一个table来创建内部类。

Note:通过实现 BaseColumns 的接口,内部类可以继承到一个名为_ID的主键,这个对于Android里面的一些类似cursor adaptor类是很有必要的。这么做不是必须的,但这样能够使得我们的DB与Android的framework能够很好的相容。

public final class FeedReaderContract {
    // To prevent someone from accidentally instantiating the contract class,
    // give it an empty constructor.
    public FeedReaderContract() {}

    /* Inner class that defines the table contents */
    public static abstract class FeedEntry implements BaseColumns {
        public static final String TABLE_NAME = "entry";
        public static final String COLUMN_NAME_ENTRY_ID = "entryid";
        public static final String COLUMN_NAME_TITLE = "title";
        public static final String COLUMN_NAME_SUBTITLE = "subtitle";
        ...
    }
}

  更具定义好的结构,然后定义对应的创建,增删改查等语句。

private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
    "CREATE TABLE " + FeedReaderContract.FeedEntry.TABLE_NAME + " (" +
    FeedReaderContract.FeedEntry._ID + " INTEGER PRIMARY KEY," +
    FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
    FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
    ... // Any other options for the CREATE command
    " )";

private static final String SQL_DELETE_ENTRIES =
    "DROP TABLE IF EXISTS " + TABLE_NAME_ENTRIES; 

使用SQL Helper创建DB:

  在SQLiteOpenHelper 类中有一些很有用的APIs。当使用这个类来做一些与db有关的操作时,系统会对那些有可能比较耗时的操作(例如创建与更新等)在真正需要的时候才去执行,而不是在app刚启动的时候就去做那些动作。我们所需要做的仅仅是执行getWritableDatabase()或者getReadableDatabase().

public class FeedReaderDbHelper extends SQLiteOpenHelper {
    // If you change the database schema, you must increment the database version.
    public static final int DATABASE_VERSION = 1;
    public static final String DATABASE_NAME = "FeedReader.db";

    public FeedReaderDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(SQL_CREATE_ENTRIES);
    }
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // This database is only a cache for online data, so its upgrade policy is
        // to simply to discard the data and start over
        db.execSQL(SQL_DELETE_ENTRIES);
        onCreate(db);
    }
    public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        onUpgrade(db, oldVersion, newVersion);
    }
}

实例化自定义的SQLiteOpenHelper的子类:

FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext());

然后通过getWritableDatabase()或getReadableDatabase()来获取DB对象:

SQLiteDatabase db = mDbHelper.getWritableDatabase();

添加数据:

  我们利用ContentValues对象来添加数据:

  首先构造一个ContentValues对象,然后调用ContentValues对象的put()方法,将每个属性的值写入到ContentValues对象中,最后使用SQLiteDatabase对象的insert()函数,将ContentValues对象中的数据写入指定的数据库表中。

SQLiteDatabase db = mDbHelper.getWritableDatabase();

// Create a new map of values, where column names are the keys
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_CONTENT, content);

// Insert the new row, returning the primary key value of the new row
long newRowId;//insert()函数的返回值是新数据插入的位置,即ID值。
newRowId = db.insert(
         FeedReaderContract.FeedEntry.TABLE_NAME,
         FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE,
         values);

  put方法的参数是一个键值对,第1个参数是名称(列名称),第2个参数是值.

  insert()函数中,第1个参数是数据表的名称,第2个参数是在NULL时的替换数据,第3个参数是需要向数据库添加的数据。

修改数据:

  更新数据使用到 update() 方法,其也是通过ContentValues对象实现。

  update(String table,ContentValuesvalues,String whereClause,String[] whereArgs);

  • table是表名;
  • values是要更新的数据;
  • whereClause:满足whereClause子句的记录将会被更新;

whereArgs:为whereClause子句传入的参数

删除数据:

  使用delete方法delete(String table,String whereClause,String[] whereArgs);

  • table是表名
  • whereClause指的是删除的条件
  • whereArgs用于为whereClause子句传入参数

查询数据:

  从DB中查询数据使用到query()方法,查询结构返回一个 Cursor 对象。

  query()方法:Cursor android.database.sqlite.SQLiteDatabase.query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)

  

   Cursor函数说明:

  

例如:  

Cursor cursor = mSQLiteDatabase.query(TABLE_NAME,
new String[] { TABLE_ID, TABLE_NUM, TABLE_DATA },
null, null, null, null, null);

  要查询在cursor中的行,使用cursor的其中一个move方法,但必须在读取值之前调用。一般来说应该先调用moveToFirst()函数,将读取位置置于结果集最开始的位置。对每一行,我们可以使用cursor的其中一个get方法如getString()getLong()获取列的值。对于每一个get方法必须传递想要获取的列的索引位置(index position),索引位置可以通过调用getColumnIndex()getColumnIndexOrThrow()获得。

cursor.moveToFirst();
long itemId = cursor.getLong(
    cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
); 
posted @ 2017-08-18 19:52  sloth_ccc  阅读(298)  评论(0编辑  收藏  举报