http://developer.android.com/training/basics/data-storage/index.html
Saving Data
Most Android apps need to save data, even if only to save information about the app state during onPause() so the user's progress is not lost. Most non-trivial apps also need to save user settings, and some apps must manage large amounts of information in files and databases.
This class introduces you to the principal data storage options in Android, including:
1. Saving key-value pairs of simple data types in a shared preferences file
2. Saving arbitrary files in Android's file system
3. Using databases managed by SQLite
Lessons
1. Saving Key-Value Sets 存贮小型键值对数据集
Learn to use a shared preferences file for storing small amounts of information in key-value pairs.
2. Saving Files 存储文件
Learn to save a basic file, such as to store long sequences of data that are generally read in order.
3. Saving Data in SQL Databases 在数据库存储
Learn to use a SQLite database to read and write structured data.
1. 保存键值对(Saving Key-Value Sets)
如果你要存储存储小型的键值对(key-value)数据集的话,可以使用SharedPreferences API。SharedPreferences对象是一个包含键值对的文件,提供了读和写的方法。每一个SharedPreferences文件都被框架(framework)管理,并且既可以是私有的,也可以使公有的。以下内容主要是:使用SharedPreferences APIs 来存储和检索简单的数值。
注意:SharedPreferences APIs 只用来读和写键值对,不要把它和 Preference APIs混淆了, Preference APIs是给app的设置(settings)建立用户界面的,这个app的设置结果是存储在 SharedPreferences 里的。
Get a Handle to a SharedPreferences
新建一个shared preference文件,或者访问一个已存在的文件,用以下方法:getSharedPreferences() — 如果涉及到多个存储文件,这些文件以名字区分,名字用作函数第一个参数,这个方法可以在app里任意的地方(any Context)调用。
getPreferences() — 如果这个activity只用到一个存储文件,那么在这个activity里调用这个方法就可以,而且不需要指定名字参数,因为检索的存储文件是属于这个activity的默认文件。
以下代码在一个Fragment里执行,第一个指定name的参数从资源文件夹(R.string.preference_file_key) 里找,第二个参数指定文件打开方式:Context.MODE_PRIVATE,所以这个文件只有这个app可以访问。
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
当命名shared preference 时,必须使用一个unique名字。比如 "com.example.myapp.PREFERENCE_FILE_KEY"
或者,如果你的activity只需要一个 shared preference 文件,你可以使用getPreferences()方法。这样的话,就不用name参数了。但要指定文件打开方式。
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
注意:如果创建文件的时候,文件打开方式是 MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE,那么其他app都可以访问里面的数据了。
Write to Shared Preferences
要写入数据,
1.先在SharedPreferences通过调用edit()方法创建一个SharedPreferences.Editor 。
2.通过putInt()和putString()来把键值对写入,
3.然后调用commit()方法来保存。
For example:
//新建一个SharedPreferences文件,设定为private模式
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
//新建一个editor对象,通过SharedPreferences对象的edit()方法。
SharedPreferences.Editor editor = sharedPref.edit();
//调用editor的putInt()方法,参数就是键值对。
editor.putInt(getString(R.string.saved_high_score), newHighScore);
//调用editor的commit()方法,保存。
editor.commit();
Read from Shared Preferences
调用SharedPreferences对象的getInt()方法和getString()方法,读取SharedPreferences文件。参数可以写default,也可以写key。返回值都是value。
//创建一个SharedPreferences对象
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
//参数我default情况
long default = getResources().getInteger(R.string.saved_high_score_default));
//参数为(key,default)情况
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), default);
2.保存文件(saving files)
android系统里对文件进行读写,主要使用了File APIs。
File对象主要用来读写大的数据量。比如图像文件、通过网络交换的文件。
以下内容基于读者已经了解Linux文件系统以及java.io里的标准文件输入输出API。
Choose Internal or External Storage
android设备都有两个文件存储区域:1.内部的(internal) 2.外部的(external)。 这种叫法的历史渊源是:早起android设备都有一个内置存储器,还有一个外置存储器(internal storage),外置存储器一般都是microSD卡(external storage)。
内部存储Internal storage的特点:
1.一直可用
2.是默认程序的默认文件访问位置,其他app访问不了。
3.卸载app时,系统会删除这个app所有文件
Internal storage 可以确保用户和其他app无法访问你的文件。很好的私密性。
外部存储External storage的特点:
1.并非一直可用。可以插在卡槽,但是卸载掉,这时候系统就看不到了,或者用户把sd卡当U盘使。
2.文件存储不安全。sd卡拿出来,里面的信息就泄露了。
3.卸载app时,只有你在路径getExternalFilesDir()保存文件的时候,系统才可以删除这个app的文件。
如果不在这个路径的话,那SD卡上就会有残留文件了。
如果文件没有访问限制,或者你想让文件为其他app所用,或者希望通过电脑查看,外部存储不失为一种好方法。
TIPS:可以设置manifest文件里的android:installLocation属性来让app装在外部存储。
Obtain Permissions for External Storage
要向外部存储写文件,必须在manifest文件里声明WRITE_EXTERNAL_STORAGE权限。
//写入外部存储文件的权限
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
/>
...
注意:目前,所有的app都可以读取外部存储上的文件,不需要特殊的权限。但是以后这个肯定要改的,读取外部存储的文件也是需要权限的。所以也要在manifest文件里声明READ_EXTERNAL_STORAGE权限。
//读取外部存储文件的权限
android:name="android.permission.READ_EXTERNAL_STORAGE"
/>
...
往内部存储写文件是不需要权限的,从内部存储读文件也是不需要权限的。
所以内外存储文件还有权限上的区别:外部的要权限,内部的不需要。
Save a File on Internal Storage
保存一个文件到内部存储的时候,需要获得一个存储路径,方法有二:
1.getFilesDir()--------返回文件File路径
2.getCacheDir()--------返回一个缓存文件路径
临时缓存文件里存放着temporary cache files,确保不用这些文件的时候删除,并有size上的控制,系统在低存储空间的时候,会直接删除这个临时文件夹内容。
在存储路径上创建文件时候,使用File()构造器,参数为路径,和文件名称。
//使用File()构造器,参数1:文件路径 参数2:文件名称
File file = new File(context.getFilesDir(), filename);
或者,可以调用openFileOutput()方法来获取FileOutputStream,来向内部存储路径写文件。
String filename = "myfile";
String string = "Hello world!";
FileOutputStream outputStream;
try {
//调用方法创建流,参数1:文件名参数2:文件类型为私有
putStream = openFileOutput(filename, Context.MODE_PRIVATE);
//调用流的write方法
outputStream.write(string.getBytes());
//关闭流
outputStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
如果要创建一个缓存文件,就要用createTempFile()方法。
以下代码从url提取文件name,然后用那个name在缓存文件夹创建一个文件。
public File getTempFile(Context context, String url) {
File file;
try {
String fileName = Uri.parse(url).getLastPathSegment();
file = File.createTempFile(fileName, null, context.getCacheDir());
catch (IOException e) {
// Error while creating file
}
return file;
}
注意:app的内部存储路径是由你的app的包的名称指定的,在android文件系统的特定位置。技术上看,如果你把文件mode设置为可读,其他app是可以读取你的app的数据的。但是其他app要想读取,必须要知道你的包的名称和文件的名称。除非你明确设定file mode为readable或者writable,否则其他app肯定没法读和写你的internal file。 所以,只要你把文件mode设置为Context.MODE_PRIVATE,其他app是无论如何也没法访问到这个文件的。
Save a File on External Storage
因为外部存储有可能不可用,所以用之前要查询一下外部存储的状态,使用之前肯定要查一下外部存储的可用空间。调用getExternalStorageState()方法,如果返回的状态是MEDIA_MOUNTED的话,外部存储就是可用的,包括读和写。
//检查外部存储是否可以读和写
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;
}
有两个文件夹来存放文件:
1.公共文件夹(Public files)
其他用户和app都可以访问,app卸载以后,文件仍然存在,其他app和用户可以继续使用。
比如:app拍的照片
2.私有文件夹(Private files)
文件私有,其他用户和app无权访问。app卸载以后,文件即被删除。尽管文件存储在外部存储上,其他用户
和app是可以访问的,但是实际上,这些文件是不会向app以外的其他用户提供数据的。、
比如:app下载的内容和临时文件
存储public文件到外部存储:调用getExternalStoragePublicDirectory()方法。这个方法有个参数,来指定你要存储文件的类型,这样,其他公共文件夹就可以将其包括进去。比如DIRECTORY_MUSIC和 DIRECTORY_PICTURES。
public File getAlbumStorageDir(String albumName) {
// 获取用户公共图片文件夹路径
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
存储public文件到外部存储:调用getExternalStoragePublicDirectory()方法。这个方法有个参数,来指定你要存储文件的类型,这样,其他公共文件夹就可以将其包括进去。比如DIRECTORY_MUSIC和 DIRECTORY_PICTURES。
public File getAlbumStorageDir(String albumName) {
// 获取用户公共图片文件夹路径
// 参数1:Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
// 参数2:albumName
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
存储private文件到外部存储:调用getExternalFilesDir()方法,也要传入一个指定文件类型的参数。以下是创建一个私人相册的文件的代码:
public File getAlbumStorageDir(Context context, String albumName) {
// 获取应用的私人相册文件路径
//参数1:context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
//参数2:albumName
File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
也可以调用 getExternalFilesDir()方法,传如null参数,这样就会返回外部存储的根目录。
记住:
1. getExternalFilesDir()方法创建的文件路径,会在用户卸载这个app的时候被删除。
2. 如果想让你的文件路径在app被卸载以后仍然存在,那么getExternalStoragePublicDirectory() 是一个更好的选择。
3.无论是建立可以共享的文件路径,还是私有文件路径,使用API 常量来指定文件目录类型是很重要的。比如,DIRECTORY_PICTURES,这可以确保系统正确的识别目录里的数据。
Query Free Space
如果你事先知道要存储的数据量的大小,你就可以先查询一下可用空间有多少, 这样就可以避免IOException,可用的方法是:getFreeSpace()和getTotalSpace()。
getFreeSpace()---返回剩余空间
getTotalSpace()---返回总空间。
但是返回的可用空间大小,并不说明,你就可以存入这么多的数据。如果空间比你要存入的数据大几M,或者空间被占用率小于90%,都还可以,反之,就未必能存入。
并不需要每次存文件之前都来这么个判断,加个异常捕捉就OK。而且,你也未必知道你要存入的数据有多大,你想判断,也没法判断的,所以能catch这个IOException就行。
Delete a File
删除文件最简单的方法是调用文件本身的方法:delete()
myFile.delete();
如果文件存储在内部存储介质上,可以利用上下文的方法:deleteFile()
myContext.deleteFile(fileName);
注意:当用户卸载app时候,系统会删除以下文件:
1.所有存储在内部介质上的数据
2.使用getExternalFilesDir()创建路径的外部介质上的数据
但是,所有缓存文件夹是需要手动删除的
3.Saving Data in SQL Databases
重复的和结构化的数据最好存储到数据库,比如联系人信息。
本节主要讲SQLite数据库,包android.database.sqlite包含了使用SQLite数据库的所有API
Define a Schema and Contract
SQL数据库主要概念之一就是Schema----一个关于如何组织数据库的定义
Schema反映在你用SQL语句定义数据库的时候。
合同类(抽象类)是一个容器,里面装着定义URI、表、列的常量。
合同类允许在同一个包内不同的类里使用相同的变量。
组织合同类的最好的方法是:把这些用来定义的常量在最上层父类里设置为整个数据库的全局常量。然后把每一个表创建为一个内部类,用这个内部类“包裹”出每一个表,列举出每个表所有的列。
实现BaseColumns接口以后,内部类就可以继承一个主键字段_ID,这也是很多android里的类所需要的,比如游标适配器(cursor adaptor)。这虽然不是必须,但是可以使数据库和android的框架协调工作。
BaseColumns接口有两个常量:1.总行数_count 2.每行的独特的_ID
以下是一个为单表定义表名和列名的代码片段:
//定义一个合同类的子类,实现BaseColumns接口,父类是FeedReaderContract
//这个inner类就代表了数据库很多表的其中之一
public static abstract class FeedEntry implements BaseColumns {
//定义表名
public static final String TABLE_NAME = "entry";
//定义列名
public static final String COLUMN_NAME_ENTRY_ID = "entryid";
public static final String COLUMN_NAME_TITLE = "title";
public static final String COLUMN_NAME_SUBTITLE = "subtitle";
...
}
其中,合同类会定义出所有的表名和所有的列名,分属不同的表,然后用内部类来实现区分。
为了避免有人实例化合同类FeedReaderContract(抽象类),定义了一个空构造器:
private FeedReaderContract() {}
Create a Database Using a SQL Helper
定义好数据库大体的样子以后,你需要实现创建和维护数据库和表的方法,以下是创建表和删除表的典型的语句:
private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + FeedReaderContract.FeedEntry.TABLE_NAME + " (" +
FeedReaderContract.FeedEntry._ID + " INTEGER PRIMARY KEY," +
FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
... // Any other options for the CREATE command
" )";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + TABLE_NAME_ENTRIES;
android系统里,数据库存储在private空间里,很安全,其他app无法访问。
SQLiteOpenHelper类里有很多有用的API,当你使用这个类获取数据库的引用的时候,只有当你需要的时候,系统才有可能进行耗时较长的操作,比如创建和更新数据库。你只要调用getWritableDatabase() 和 getReadableDatabase()方法就可以。
注意,因为有些操作可能会long-running,所以务必在后台进程调用getWritableDatabase() 和 getReadableDatabase()方法。
要使用SQLiteOpenHelper类(抽象类),就需要创建一个子类,并且覆写 onCreate(), onUpgrade() 和 onOpen()这几个回调方法,同时,也最好实现 onDowngrade()方法。
以下是使用以上方法的代码示例:
//创建SQLiteOpenHelper的子类
public class FeedReaderDbHelper extends SQLiteOpenHelper {
// 如果要改变数据库模式,则要升级数据库版本.
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "FeedReader.db";
//子类构造器调用父类SQLiteOpenHelper的构造器
public FeedReaderDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
//覆写方法
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// This database is only a cache for online data, so its upgrade policy is
// to simply to discard the data and start over
//删除数据并重新建库
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
要使用数据库,就必须要实例化SQLiteOpenHelper类的子类:
FeedReaderDbHelper mDbHelper = new FeedReaderDbHelper(getContext());
Put Information into a Database
把 ContentValues对象当参数传给insert()方法,就可以把数据存入数据库。代码如下:
//调用SQLiteOpenHelper的子类的对象的方法
SQLiteDatabase db = mDbHelper.getWritableDatabase();
//创建值的键值对, 列名是key
//新建ContentValues类的对象values
ContentValues values = new ContentValues();
//调用values的put()方法,这个values装了一个row
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_CONTENT, content);
//插入新的一行,返回这行的主键
//insert()方法参数:
参数1:表名table_name
参数2:指定是否可以可空,nullable可插入空数据,null不可插入空数据
参数3:插入的键值对,也就是一个row
long newRowId;
newRowId = db.insert(
FeedReaderContract.FeedEntry.TABLE_NAME,
FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE,
values
);
Read Information from a Database
从数据库读取信息使用 query()方法,查询返回值在一个Cursor对象里。
//获取可读数据库,赋给SQLiteDatabase对象
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// 定义一个projection
String[] projection = {
FeedReaderContract.FeedEntry._ID,
FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE,
FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED,
...
};
//查询结果在cursor里的排序方式
String sortOrder =
FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED + " DESC";
//查询结果赋给Cursor对象
Cursor c = db.query(
FeedReaderContract.FeedEntry.TABLE_NAME, //表名
projection, // The columns to return
selection, // The columns for the WHERE clause
selectionArgs, // The values for the WHERE clause
null, // don't group the rows
null, // don't filter by row groups
sortOrder // 排序方式
);
查看cursor里一个row的方法:在读数值之前,必须要调用move方法,首先调用 moveToFirst()方法,这样游标就到了第一个位置,取值就用get系方法,比如getString()和getLong() ,但是get系方法要传入index作参数,这个参数往往不知道,可以通过getColumnIndex() 和 getColumnIndexOrThrow()方法获取,故只需要输入列名就可以。
//指针置首
cursor.moveToFirst();
//根据给定的列名,取值
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
);
Delete Information from a Database
要是想删除一个表里的行,就要提供筛选标准来确定要删除的行。
数据库API提供了一个机制来创建筛选标准,来防止SQL注入攻击。
// 定义查询的WHERE部分
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
// Specify arguments in placeholder order.
String[] selelectionArgs = { String.valueOf(rowId) };
// 组装SQL语句
//delete()方法中
参数1:表名
参数2:WHERE语句
参数3:要查的字段
db.delete(table_name, selection, selectionArgs);
Update a Database
要修改数据库值的子集时,使用 update()方法。
语法其实是把insert()里的values和delete()里的where子句结合了起来。
SQLiteDatabase db = mDbHelper.getReadableDatabase();
//列的新值
ContentValues values = new ContentValues();
values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title);
//根据ID,确定需要update的列
String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selelectionArgs = { String.valueOf(rowId) };
//执行update
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);