解读Android之数据存储方案

本文翻译自android官方文档,结合自己测试,整理如下。

Android提供了一些永久保存数据的方法,可以根据具体的需求决定使用哪种方式存储,例如私有数据,外部程序是否可以访问等等。有以下几种方法存储:

  • Shared Preferences
    使用键值对存储私有数据类型
  • Internal Storage(或称为文件存储)
    使用内部存储保存私有数据
  • External Storage
    使用外部存储保存公共数据
  • SQLite Databases
    使用私有数据库保存结构化数据
  • Network Connection
    保存在网络服务器中

当然Android中提供了一种使用content provider可以将私有数据暴露给外部程序使用。有兴趣地可以参考我之前翻译的文章:

  • 解读Android之ContentProvider(1)CRUD操作
  • 解读Android之ContentProvider(2)创建自己的Provider

下面分别介绍以上四种(Network Connection不介绍)。

使用Shared Preferences

SharedPreferences类提供了一个基本的框架,能够使我们保存和检索私有键值对,可以保存的类型有:boolean,float,int,long,String。这些数据将会永久保存。

为了能过获得SharedPreferences对象,我们可以使用以下两种方法中的任何一种:

  • getSharedPreferences()
    如果需要使用多个通过名字识别的存储文件,使用该方法。该方法需要指定文件名。
  • getPreferences()
    如果在activity中只需要一个存储文件的话,使用该方法。由于该方法只能创建一个文件,因此不需要提供文件名。

然后,可以通过下面的步骤完成写数据:

  1. 调用edit()获得SharedPreferences.Editor对象;
  2. 通过putXXX()方法添加数据;
  3. 完成添加数据时调用commit()

为了读取数据,可以使用SharedPreferences中的getXXX()方法。

以上具体的方法可以参考我之前的文章:android之SharedPreferences。

注意:Shared Preferences方式不是严格意义上的保存用户偏好(user preference),例如保存用户选择的铃声。若想要实现这种功能的话可以继承PreferenceActivity类,该类是Activity框架,但是能够自动永久保存用户偏好(也是使用Shared Preferences)。当然对于其他的控件来说,Android也提供了相应的处理办法。例如:CheckBoxPreference, EditTextPreference, ListPreference, MultiSelectListPreference, PreferenceCategory, PreferenceScreen, SwitchPreference。这部分会在后续更新,请持续关注我的博客。

使用内部存储

我们可以直接将数据保存在内部存储上。默认情况下,保存在内存上的数据是对程序私有的,外部程序无法获取。该存储方式又可称为文件存储,是android中一种比较简单的存储方式,它不对存储内容进行任何处理(怎么读的怎么存),就是利用java中的文件输入输出流来管理数据。

我们可以通过以下方法实现内部存储:

  1. 调用Context类中的openFileOutput()方法,该方法返回FileOutputStream
  2. 调用write()写数据;
  3. 调用close()关闭流。

例如:


String FILENAME = "hello_file";
String string = "hello world!";
// FILENAME可能是存在或不存在的文件名,若存在则替换现有的
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();

openFileOutput()接收两个参数:文件名和操作模式。文件名不能包括路径,这是因为所有的文件都有默认的位置:/data/data//files/。操作模式有:MODE_PRIVATE(默认情况,程序私有,若文件存在覆盖原有),MODE_APPEND(若文件存在,则在内容后面添加而不是替换,若不存在直接创建),MODE_WORLD_READABLE(API17后已弃用),MODE_WORLD_WRITEABLE(API17后已弃用)。

通过上面的例子可以看到和java输入输出流一样。

通过下列方法可以读取数据:

  1. 调用openFileInput()方法,该方法返回FileInputStream;
  2. 调用read()方法读取数据;
  3. 调用close()方法关闭流。

openFileInput()方法只接受一个文件名参数,系统会自动在/data/data//files/目录下查找,之后调用java流进行读取数据。

注意:若想在编译时保存静态文件,该文件保存在项目的res/raw/目录下。使用Resources的实例方法openRawResource()获取InputStream读取数据,该方法参数为:R.raw.<filename>。但是我们不能向该文件中写内容。

保存缓存文件

若我们想缓存某些数据而不是永久保存的话可以使用Context类中的getCacheDir()方法打开一个文件,该文件代表了一个可以保存临时文件的绝对路径(即/data/data//cache)。当内部存储空间不足时,可能会删除这些缓存,然而我们通常需要在程序中限制并清除这些缓存,大小最好不要超过1MB。当用户把我们的程序卸载时应该删除这些缓存。

在获得以上目录文件后就可以根据java输入输出流对文件进行读写。

其它方法

Context中还提供了一下方法方便我们处理文件存储:

  • getFilesDir()
    获得内部文件保存的绝对路径。
  • getDir()
    打开或创建一个内部存储空间中的目录。
  • deleteFile()
    删除一个文件
  • fileList()
    返回文件列表。

使用外部存储

每一个android兼容的设备都支持共享的外部存储,我们可以保存文件数据。这些设备可以是可拆卸的(例如SD卡)或者是内部的。保存在外部存储上的数据外部程序是可以获取的,并能够通过USB传到电脑上进行修改。

注意:如果用户连接到计算机或删除外部设备上的媒体文件,外部存储可能会变得不可用,并没有安全强制执行保存到外部存储的文件。所有应用程序都可以读取和写入存放在外部存储上的文件,用户也可以移除它们。

获取许可

想要读取或写入外部设备上的文件,我们的程序必须获得READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE系统许可。例如:


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

若想同时获得读写许可的话, 只需要另一个许可即可(写许可隐式包含了读许可)。

检查媒体文件的可用性

在使用外部设备时,我们应该首先调用getExternalStorageState()来检查媒体文件是否可用。例如下面的方法:


/* 检查外部设备是否可以读写 */
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;
}

getExternalStorageState()也可以返回其它状态,例如是否可以共享,是否被移除等。

保存可以和其他程序共享的文件

一般情况,新文件应该放在一个公共的地方以便其它程序可以访问,并且方便拷贝。例如用一个共享的公共目录,Music/,Pictures/,Ringtones/。

为了获得一个合适的公共目录文件,可以调用getExternalStoragePublicDirectory(),目录类型可以有:DIRECTORY_MUSICDIRECTORY_PICTURESDIRECTORY_RINGTONES等。按照目录类型建立文件并存放相应类型的内容以便系统方便寻找。例如保存媒体类型的文件在相应的目录中时,系统媒体扫描仪能够对文件进行合适的分类(for instance, ringtones appear in system settings as ringtones, not as music)。

例如下面一个方法用于创建一个名为album文件夹用于存放图片, 该文件夹在公共的图片目录下:


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;
}

注意:为了回避媒体扫描仪的扫描,我们可以创建一个以.nomedia为名字的空文件。该文件能够禁止扫描仪读取媒体文件。但是若我们的文件是程序私有的,应该在私有的目录下保存它们。

保存私有文件

若想保存程序私有文件,则需要私有存储目录保存文件,可以调用getExternalFilesDir()。该方法接收一个类型参数,能够指定子目录的类型(例如DIRECTORY_MOVIES)。若不需要指定媒体目录,可以需要传递null,来接收私有目录的根目录。

从Android4.4之后,读写私有目录下的文件不需要许可READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE。我们可以声明使用权限的最大版本号,如下:


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

注意:当程序被卸载后,这些文件目录将都会删除。系统媒体扫描仪不能够读取这些目录的文件。因此我们不能使用这些目录保存属于用户的媒体,例如用户下载的图片等,这些文件应该保存在公共的目录下,以防在卸载程序后删除这些文件。

有时候,一种设备,该设备分配一个内存作为外部存储,也可以提供一个SD卡插槽。那么该设备运行在4.3及以下系统上时,getExternalFilesDir()只能获得内部存储文件,我们的app不能读写到SD卡上。4.4开始以上两个位置都可以获取,通过getExternalFilesDirs()方法,该方法返回文件数组。若想在低版本中使用,则可以使用兼容库中的静态方法ContextCompat.getExternalFilesDirs()。虽然仍返回一个文件数组,但通常只有一个元素。

注意尽管通过getExternalFilesDir()getExternalFilesDirs()获得的目录不能通过MediaStore content provider获取,但是其他拥有READ_EXTERNAL_STORAGE许可的程序能够获取所有外部存储上的文件。若要做到严格限制的话,需要使用内部存储。

保存缓存文件

通过调用getExternalCacheDir()可以用于保存缓存文件。当用户卸载程序时,这些文件自动删除。通过调用ContextCompat.getExternalCacheDirs()可以将缓存文件保存在第二个存储设备上。

注意:为了充分利用文件空间并且提高程序性能,因此管理好缓存文件非常重要,并且在不需要它们的时候移除它们。

在获得以上目录文件后就可以根据java输入输出流对文件进行读写。

使用数据库

android完全支持SQLite数据库,在程序内的任何类都可以访问我们创建的数据库,其它外部程序则不能直接访问。

SQLite是一款轻量级的关系型数据库,它的运算速度非常快,占用资源非常小,通常只需要几百KB的内存就能够满足,因此特别适合移动设备。

创建SQLite数据库的一个好的方法是创建一个抽象类SQLiteOpenHelper的子类,该抽象类是一个帮助类,可以方便的对数据库进行创建和升级。在SQLiteOpenHelper类中有两个重要的抽象方法:onCreate()onUpgrade(),我们必须实现这两个方法,前者用于创建数据库,后者用于升级数据库。并且这两个方法无须我们调用,系统会在合适的地方调用(下面有讲到)。

SQLiteOpenHelper类中还有两个重要的方法:getReadableDatabase()getWritableDatabase(),两者都可以打开(若没有则创建)现有的数据库,并返回一个SQLiteDabase对象,然后使用该对象就可以对数据库进行对数的操作。。两者的不同点在于:前者在数据库不可写入(如空间已满)时,返回的对象只能以只读的方式打开数据库;而后者将会抛出异常。数据库文件会存放在/data/data//databases/目录下。

由于SQLiteOpenHelper类没有无参构造器,因此在继承SQLiteOpenHelper类时,必须要调用父类构造器,而通常来说,我们可以调用参数较少的一个构造器。

下面我们来看一个具体的示例:


/** * SQLiteOpenHelper练习 * SQLiteOpenHelper是一个管理SQLite数据库的帮助抽象类 * Created by sywyg on 2015/5/20. */
public class MyDatabaseHelper extends SQLiteOpenHelper{

    private Context mContext;
    /** * SQL语句, * SQLite中支持的数据类型包括:null, integer整型,text文本类型,real浮点类型, * blob二进制类型(应该是任意输入的数值) * primary key 表示设置主键,autoincrement表示id自动增长 */
    public static final String CREATE_BOOK = "create table book(" +
            "id integer primary key autoincrement," +
            "author text," +
            " price real," +
            " state blob)";
    public static final String CREATE_CATEGORY = "create table category(" +
            "id integer primary key autoincrement," +
            "name text," +
            " state blob)";
    /** * java语法:若父类没有无参构造器的话,则子类必须调用父类构造器,否则在实例化子类的时候无法调用父类构造器。 * 因此这个构造器(或另一个参数多的构造器)是必须的 * @param context 当前访问数据库的组件 * @param name 数据库名称 * @param factory 自定义Cursor ,一般为null * @param version 数据库版本号,可用于对数据库升级操作。 */
    public MyDatabaseHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version){
        super(context,name,factory,version);
        mContext = context;

    }

    /** * 新建数据库时会执行,在这里一般处理创建表的逻辑 * @param db */
    @Override
    public void onCreate(SQLiteDatabase db) {
        //执行SQL语句,创建两个表,可以封装在一个方法中,方便在onUpgrade()中调用
        db.execSQL(CREATE_BOOK);
        db.execSQL(CREATE_CATEGORY);
        Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_LONG).show();
    }

    /** * 用于对数据库进行升级,当在实例化该类时传入的version大于之前的值就会执行该方法 * @param db * @param oldVersion * @param newVersion */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        // 这种采用直接删除的办法会导致数据的丢失,其实是不合理的
        // 可以通过switch条件判断:若产品升级的话,现有的数据表保存
        // 而不是删除表,且case块中,不需要break语句。
        // 这样能够保证无论多少次更新,数据库总是最新的。
        // 以上内容参考郭霖第一行。
        db.execSQL("drop table if exists book");
        db.execSQL("drop table if exists category");
        onCreate(db);
    }

    /** * 可以创建或打开一个现有的数据库(如果数据库已经存在则打开,若不存在则新建) * 返回一个可对数据库读写操作的对象 * 但数据库不可写入(如磁盘空间已满)时,返回的对象只能以只读的方式打开数据库 * @return */
    @Override
    public SQLiteDatabase getReadableDatabase() {
        return super.getReadableDatabase();
    }

    /** * 可以创建或打开一个现有的数据库(如果数据库已经存在则打开,若不存在则新建) * 返回一个可对数据库读写操作的对象 * 但数据库不可写入时,将出现异常 * @return */
    @Override
    public SQLiteDatabase getWritableDatabase() {
        return super.getWritableDatabase();
    }
}

为了读写数据,我们可以调用getWritableDatabase()getReadableDatabase()获取SQLiteDabase对象,然后使用该类的方法就可以对数据库进行对数的操作。我们可以使用query()方法查询数据库,若要执行更为复杂的查询语句,则可以使用SQLiteQueryBuilder。

每一个SQLite查询都会返回Cursor对象,使用该对象对查询结果进行处理。

我们来看一下如何对数据库进行处理:


/** * SQLite数据库练习 * @author sywyg * @since 2015.5.20 */

public class MainActivity extends Activity {
    private MyDatabaseHelper helper;
    private SQLiteDatabase sqLiteDatabase;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 创建数据库帮助类
        helper = new MyDatabaseHelper(this,"BookStore.db",null,1);
    }
    /** * 按钮点击事件 * @param view */
    public void onButtonClick(View view) {
        switch (view.getId()) {
            /** * 获得数据库 */
            case R.id.btn_create:
                sqLiteDatabase = helper.getReadableDatabase();
                break;
            /** * 插入数据(也可以直接执行SQL语句)。 * SqLiteDatabase类中的实例方法insert()方法 * insert()方法接受三个参数分别为: * 表名,不指定列的默认值null,ContentValues对象。 * ContentValues类实现了Parcelable接口,提供一系列的put()方法。 * 用于添加数据,put()方法参数为:列名,值。 */
            case R.id.btn_add:
                sqLiteDatabase =  helper.getWritableDatabase();
                ContentValues values = new ContentValues();
                values.put("author","sywyg");
                values.put("price", 25);
                values.put("state", 0);
                sqLiteDatabase.insert("book", null, values);
                //清除values中的值
                values.clear();
                //插入第二条数据
                values.put("author","sywyg2");
                values.put("price", 15);
                //values.put("state",1);
                sqLiteDatabase.insert("book",null,values);
                Toast.makeText(this, "add succeeded", Toast.LENGTH_LONG).show();
                break;
            /** * 删除数据(也可以直接执行SQL语句) * delete()方法参数分别为:表名,第二和第三个为约束条件 */
            case R.id.btn_delete:
                sqLiteDatabase =  helper.getReadableDatabase();
                //问号表示占位符,由第三个参数中的字符串数组指定相应的内容
                sqLiteDatabase.delete("book","author = ?",new String[]{"sywyg2"});
                Toast.makeText(this, "delete succeeded", Toast.LENGTH_LONG).show();
                break;
            /** * 更新数据(也可以直接执行SQL语句) * update()方法参数分别为:表名,ContentValues对象,第三和第四个为约束条件 */
            case R.id.btn_update:
                sqLiteDatabase =  helper.getWritableDatabase();
                ContentValues values1 = new ContentValues();
                values1.put("author","wygsy");
                values1.put("price", 100);
                //更新author为sywyg且price为15的数据
                sqLiteDatabase.update("book", values1, "author = ? and price = ?", new String[]{"sywyg", "15"});
                Toast.makeText(this, "update succeeded", Toast.LENGTH_LONG).show();
                break;
            /** * 查询数据(也可以直接执行SQL语句) * query()方法参数分别为: */
            case R.id.btn_select:
                sqLiteDatabase =  helper.getReadableDatabase();
                Cursor cursor = sqLiteDatabase.query("book", null, null, null, null, null, null);
                while (cursor.moveToNext()) {
                    int id = cursor.getInt(cursor.getColumnIndex("id"));
                    String author = cursor.getString(cursor.getColumnIndex("author"));
                    Toast.makeText(this, "id:" + id + ",author:" + author, Toast.LENGTH_LONG).show();
                }
                break;
        }
    }
}

代码中已经解释的很清楚,不再多说。需要多说的是关于数据库的增删改查(CRUD)操作,我已在contentprovider中讲的很清楚了,有兴趣的可以去看看:解读Android之ContentProvider(1)CRUD操作和解读Android之ContentProvider(2)创建自己的Provider。
当然上面的CRUD也直接可以使用SQL语句处理,使用SQLiteDatabase对象的execSQL()

若要实现插入数据的唯一性可以使用insertWithOnConflict()同时需要在创建表时指定个不允许重复的字段设为主键PrimaryKey或者唯一性索引UNIQUE。

Android没有增加任何超出SQLite语句的限制。我们推荐使用一个自动增加的主键,但是这个不是必须的。对于content provider来说,这个主键(BaseColumns._ID)是必须的。

使用事务

这部分内容来自郭霖第一行。

SQLite数据库是支持事务的,事务的特性是保证某一系列操作要么都执行,要么都不执行。那么如何使用事务呢?

首先调用SQLiteDatabase对象的beginTransaction()开启事务,然后在一个异常捕获块中去执行数据库操作,当所有的操作完成后调用setTransactionSuccessful(),表示事务完成,最后在finally中调用endTransaction()关闭事务。

以上操作能够保证一次事务的执行,若执行不到setTransactionSuccessful(),则所有数据库操作都将无效。

数据库调试

Android SDK中的adb调试工具中包括sqlite3命令,这些命令可以进行相关数据库操作。这一部分将在Android Debug Bridge中介绍。

你可能感兴趣的:(android,数据存储)