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/45849515http://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中介绍。

时间: 2024-08-09 19:09:25

Android数据存储方案的相关文章

Android Learning:数据存储方案归纳与总结

前言 最近在学习<第一行android代码>和<疯狂android讲义>,我的感触是Android应用的本质其实就是数据的处理,包括数据的接收,存储,处理以及显示,我想针对这几环分别写一篇博客,记得我的学习心得,也希望跟各位新手同学相互努力促进.今天这篇博客,我想介绍一下数据的存储,因为数据的接收,存储,处理以及显示这几环环环相扣,而数据的存储直接关系到数据的处理和显示,所以显得尤为重要. 所以本文针对数据存储的常见方案和其使用进行了归纳.分为程序内存储和程序间数据访问,程序内存储

Android——数据存储(四种方式之一)SharedPrefereces

Android--数据存储(四种方式) 1.SharedPrefereces   轻量级.XML  存储文件名,数据保存在data/data/basepackage/shared_prefs/myopt.xml中   实例-收藏-记住密码自动登录 //一种轻量级的数据存储方式//通过KEY 存入数据--putxxxx(key,value) 取出数据--getxxxx(key  default)   2.读写SD卡  SD的根目录  适用于数据流读写 3.SQLite  轻量级.dp文件多用于手机

Android数据存储(2):Internal Storage

Android数据存储的第二种形式是Internal Storage,即内部存储. 内存存储的特点是存储的是私有数据,其存储位置是在手机内存,一种是普通的文件存储,另一种是文本或图片在内存的缓存. 内部缓存的存放位置/data/data/包名/files 或 /data/data/包名/cache 1.create and write a private file to the internal storage: 步骤: 1)调用 openFileOutput()方法,返回的是一个FileOut

数据存储方案评估标准RDBMS or KV

作者:zhanhailiang 日期:2014-12-11 本文主要介绍常见的数据存储方案及相应选型的评估标准的介绍. Guideline:针对不同应用场景,针对性选择存储方式. 1. 数据存储方案 SQL: MySQL 5.5/5.6/MariaDB(对于Dev绝大多数场景下透明): Oracle|MS SQL暂不考虑: NoSQL: Memcached 1.4.21: Redis 2.8: MongoDB 2.6.6: Hbase 0.96/0.98: 2. 评估标准 RDBMS:(MySQ

【Android数据存储】- File

个人学习整理,如有不足之处,请不吝指教.转载请注明:@CSU-Max 读写本应用程序数据文件夹中的文件 此种方法读写的文件在/data/data/<应用程序包名>中 android 中提供了 openFileInput(String fileName) 和 openFileOutput(String fileName, int mode) 两个方法来读取本应用程序数据文件夹中的文件和向其写入. openFileInput(String fileName)    打开文件输入流 openFile

Android 数据存储方式分为哪几种?

10道题值得思考 第一道题 1.Android 数据存储方式分为哪几种? SharedPreferences存储数据 2. CotentProvider内容提供者 3. 文件存储 4. 数据库存储(Sqlite) 5.网络存储 2. NDK是什么? 1. 一系列工具类的集合 2. Arm指令集 3. NDK提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so和java应用一起打包成apk.这些工具对开发者的帮助是巨大的. NDK集成了交叉编译器,并提供了相应的mk文件隔离

Android数据存储技术五大方式总结

本文介绍Android平台进行数据存储的五大方式,分别如下: 1 使用SharedPreferences存储数据 2 文件存储数据 3 SQLite数据库存储数据 4 使用ContentProvider存储数据 5 网络存储数据 下面详细讲解这五种方式的特点 第一种: 使用SharedPreferences存储数据     适用范围:保存少量的数据,且这些数据的格式非常简单:字符串型.基本类型的值.比如应用程序的各种配置信息(如是否打开音效.是否使用震动效果.小游戏的玩家积分等),解锁口令密码等

Android——数据存储(四种方式之二)读写SD卡

Android--数据存储(四种方式) 1.SharedPrefereces 只能保存一些简单的数轻量级.XML  存储文件名, 数据保存在data/data/basepackage/shared_prefs/myopt.xml中    实例-收藏-记住密码自动登录 //一种轻量级的数据存储方式//通过KEY 存入数据--putxxxx(key,value) 取出数据--getxxxx(key  default) 2.读写SD卡  SD的根目录  适用于数据流读写 实现步骤:加入读写SD卡权限

Android数据存储之GreenDao 3.0 详解(一)

前言: 今天一大早收到GreenDao 3.0 正式发布的消息,自从2014年接触GreenDao至今,项目中一直使用GreenDao框架处理数据库操作,本人使用数据库路线 Sqlite---->OrmLite---->GreenDao.今天白天一直在忙着公司的项目需求,只能晚上加班来学习最新的GreenDao 3.0使用方式了. GreenDao 介绍: greenDAO是一个对象关系映射(ORM)的框架,能够提供一个接口通过操作对象的方式去操作关系型数据库,它能够让你操作数据库时更简单.更