当前位置: 代码迷 >> Android >> Android数据存储草案
  详细解决方案

Android数据存储草案

热度:87   发布时间:2016-04-27 23:36:34.0
Android数据存储方案

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

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

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

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

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

使用Shared Preferences

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

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

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

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

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

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

以上具体的方法可以参考我之前的文章:http://blog.csdn.net/wangyongge85/article/details/45305717。

注意: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/目录下。使用openRawResource()获取InputStream读取数据,该方法参数为:R.raw.<filename>。但是我们不能向该文件中写内容。

保存缓存文件

若我们想缓存某些数据而不是永久保存的话可以使用Context类中的getCacheDir()方法打开一个文件,该文件代表了一个可以保存临时文件的绝对路径。当内部存储空间不足时,可能会删除这些缓存,然而我们通常需要在程序中限制并清除这些缓存,大小最好不要超过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 ActionBarActivity {    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中讲的很清楚了,有兴趣的可以去看看:http://blog.csdn.net/wangyongge85/article/details/45849515和http://blog.csdn.net/wangyongge85/article/details/47057369。

当然上面的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中介绍。

  相关解决方案