本文翻译自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中只需要一个存储文件的话,使用该方法。由于该方法只能创建一个文件,因此不需要提供文件名。
然后,可以通过下面的步骤完成写数据:
- 调用
edit()
获得SharedPreferences.Editor
对象; - 通过
putXXX()
方法添加数据; - 完成添加数据时调用
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中的文件输入输出流来管理数据。
我们可以通过以下方法实现内部存储:
- 调用Context类中的
openFileOutput()
方法,该方法返回FileOutputStream
; - 调用
write()
写数据; - 调用
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输入输出流一样。
通过下列方法可以读取数据:
- 调用
openFileInput()
方法,该方法返回FileInputStream; - 调用
read()
方法读取数据; - 调用
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_STORAGE
或WRITE_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_MUSIC
,DIRECTORY_PICTURES
,DIRECTORY_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_STORAGE
或WRITE_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中介绍。