本文翻译自android官方文档,结合自己测试,整理如下。
Android提供了一些永久保存数据的方法,可以根据具体的需求决定使用哪种方式存储,例如私有数据,外部程序是否可以访问等等。有以下几种方法存储:
当然Android中提供了一种使用content provider可以将私有数据暴露给外部程序使用。有兴趣地可以参考我之前翻译的文章:
下面分别介绍以上四种(Network Connection不介绍)。
SharedPreferences类提供了一个基本的框架,能够使我们保存和检索私有键值对,可以保存的类型有:boolean,float,int,long,String。这些数据将会永久保存。
为了能过获得SharedPreferences对象,我们可以使用以下两种方法中的任何一种:
getSharedPreferences()
getPreferences()
然后,可以通过下面的步骤完成写数据:
edit()
获得SharedPreferences.Editor
对象;putXXX()
方法添加数据;commit()
;为了读取数据,可以使用SharedPreferences中的getXXX()
方法。
以上具体的方法可以参考我之前的文章:android之SharedPreferences。
注意:Shared Preferences方式不是严格意义上的保存用户偏好(user preference),例如保存用户选择的铃声。若想要实现这种功能的话可以继承PreferenceActivity类,该类是Activity框架,但是能够自动永久保存用户偏好(也是使用Shared Preferences)。当然对于其他的控件来说,Android也提供了相应的处理办法。例如:CheckBoxPreference, EditTextPreference, ListPreference, MultiSelectListPreference, PreferenceCategory, PreferenceScreen, SwitchPreference
。这部分会在后续更新,请持续关注我的博客。
我们可以直接将数据保存在内部存储上。默认情况下,保存在内存上的数据是对程序私有的,外部程序无法获取。该存储方式又可称为文件存储,是android中一种比较简单的存储方式,它不对存储内容进行任何处理(怎么读的怎么存),就是利用java中的文件输入输出流来管理数据。
我们可以通过以下方法实现内部存储:
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/目录下。使用Resources的实例方法openRawResource()
获取InputStream读取数据,该方法参数为:R.raw.<filename>
。但是我们不能向该文件中写内容。
若我们想缓存某些数据而不是永久保存的话可以使用Context类中的getCacheDir()
方法打开一个文件,该文件代表了一个可以保存临时文件的绝对路径(即/data/data//cache)。当内部存储空间不足时,可能会删除这些缓存,然而我们通常需要在程序中限制并清除这些缓存,大小最好不要超过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 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中介绍。