保存key-value对——SharedPreferences
如果有比较小的数据是key-value的关系,这些数据需要储存,那么就可以用到ShardPreferences。一个SharedPreferences对象指向一个包含key-value对的文件,它提供一些简单的方法读和写。每个SharedPreferences文件,可以是私有的也可以是共享的。
注意: SharedPreferences 仅仅可以读和写key-value对,不要跟Preference搞混了。
对 SharedPreferences 的一些操作
可以通过调用下面的两个方法中的一个,来创建一个新的SharedPreferences或者获取一个已经存在的sharedPreferences:
getSharedPreferences() — 如果想让多个activity可以分享这些SharedPreferences,就可以调用这个方法。需要在第一个参数传入一个名字来作为唯一标识符(也就是key)。可以在app的任何一个Context 中调用它。
getPreferences() — 如果只是想当前这个activity可以分享到一个sharedpreferences的数据,就调用getPreferences()。因为这样会获得一个默认的属于这个activity的sharedpreferencse,所以就不用传入一个名字来标识了。
例如,在一个Fragment里面,通过资源字符串 R.string.preference_file_key来获得唯一标识的sharedpreference,然后使用private模式,所以只有该app才可以获得它。
Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);
当对一个SharedPerference命名的时候,应该用一个可以唯一标识的名字来作为标识符,比如:
“com.example.myapp.PREFERENCE_FILE_KEY”,使用包名做前缀,这样就不会和其他的app搞混了。
或者,如果只需要在一个activity中分享,就可以使用 getPreferences()方法了。
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
注意 : 如果在创建一个SharedPreference的时候,使用MODE_WORLD_READABLE 或者 MODE_WORLD_WRITEABLE,那么其他任何一个知道标识符的app也都可以获取里面的数据了。
写入数据到 SharedPreferences中
通过SharedPreferences对象调用edit(),创建一个SharedPreferences.Editor,然后写入到SharedPreferences中。使用 putInt() 或者 putString(),传递想要写入的keys和values。最后调用commit()方法来提交修改。
例如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int newHighScore = getNewHighScore(); //getNewHighScore()返回一个int类型的值
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();
从SharedPreferences中读出数据
从SharedPreferences中读出数据,需要调用 getInt()或者 getString()等。通过传入的标识名字(也就是key)来获得想要的数据(value)。 如果key不存在,没有找到值,就用默认值来代替。例如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
int defaultValue = getResources().getInteger(R.string.saved_high_score_default);
long highScore = sharedPref.getInt(getString(R.string.saved_high_score), defaultValue);
如果getString(R.string.saved_high_score)没有找到值,就选择defaultValue来代替返回值。比如字符串返回默认值为“ ”,boolean返回一个默认值false等等。
保存数据到文件
Android 使用一个文件系统,有点类似其它平台基于磁盘的文件系统。
一个File对象适用于读写较大的数据。比如对于图片文件或者一些跟网络交换的数据。
选择内部或者外部存储。
所有的Android设备都有两个文件储存区域:外部和内部存储。
内部存储,大部分数据放置在不容易丢失的内存中
外部存储,可移动的存储媒介,比如SD卡
以下是在存储空间中的两种存储总结:
内部存储:
.它总是可用可访问的。
.默认情况下只有你的应用可以访问此处保存的文件。
.当用户卸载你的应用时,系统会从内部存储中删除您的应用的所有文件。
.当您希望确保用户或其他应用均无法访问您的文件时,内部存储是最佳选择。
外部存储:
.它并非始终可用,因为用户可采用 USB 存储的形式装载外部存储,并在某些情况下会从设备中将其删除。
.它是全局可读的,因此此处保存的文件可能被其他应用读取。
.当用户卸载你的应用时,只有在你通过 getExternalFilesDir() 将你的应用的文件保存在目录中时,系统才会从此处删除您的应用的文件。
.对于无需访问限制以及你希望与其他应用共享或允许用户使用电脑访问的文件,外部存储是最佳位置。
尽管app默认安装在内部存储中,但可以在manifest文件中指定 android:installLocation 的属性,这样app便可安装在在外部存储中。当 APK 非常大且它们的外部存储空间大于内部存储时,用户更青睐这个选择
获得外部存储的权限
想要写入到外部存储,需要在manifest文件中请求WRITE_EXTERNAL_STORAG权限:
<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>
如果在app中使用WRITE_EXTERNAL_STORAGE,那么这意味着也拥有了读取的权限。注意:如果只需要读取外部存储的数据而不需要写入,要在在manifest文件中请求READ_EXTERNAL_STORAGE权限。
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>
但是,官方说到,这种请求WRITE_EXTERNAL_STORAGE也默认可以读取的方式以后会改变。
以下官方原文:
Caution: Currently, all apps have the ability to read the external storage without a special permission. However, this will change in a future release. If your app needs to read the external storage (but not write to it), then you will need to declare the READ_EXTERNAL_STORAGE permission. To ensure that your app continues to work as expected, you should declare this permission now, before the change takes effect.
如果想保存文件到内部存储中,就不需要请求任何的权限了,app可以直接向内部存储写入和读取。
保存文件到内部存储
当保存文件到内部存储时,通过调用下面两个方法中的其中一个,就可以获得适合的目录作为文件储存的地方:
getFilesDir()
返回一个File值,表示内部目录
getCacheDir()
返回一个File值,表示内部目录下临时缓存。
确保一旦这个缓存文件不再需要,记得要及时删掉它,确保有足够的内存。如果系统在低内存下运行,系统可能会自动删除掉缓存文件,并且不会有任何警告提示。
我们可以使用File()构造器来创建一个新的文件,传入上面两个方法其中一个的返回值(也就是文件目录)作为唯一标识内部存储的路径。
例如:
//创建一个文件
String filename = "myfile";
File file = new File(context.getFilesDir(), filename);
然后,可以调用 openFileOutput()来获得一个FileOutputStream,再通过FileOutputStream调用write()写入数据到内部存储的目录中。
例如:
//创建一个文件
String filename = "myfile";
File file = new File(context.getFilesDir(), filename);
String message = "Hello world!";
FileOutputStream outputStream;
//写入文件
try {
outputStream = openFileOutput(filename, Context.MODE_PRIVATE);
outputStream.write(message.getBytes());
outputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
app的内部存储目录是按照app的包名作为目录存储在Android文件系统下。如果设置文件的模式是Readable,其他app就可以读取你的内部文件,当然,其他app也要知道你的app的包名和文件名才可以。所以,只要在内部存储中使用MODE_PRIVATE模式,其他app就不可访问里面的内容了。
或者,如果需要缓存一些文件,那么应该用createTempFile()代替File()来创建一个缓存的文件。下面的例子,从URL中提取信息作为文件的名字,存储在app的内部缓存目录下。
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;
}
保存文件到外部存储
因为外存储存并非始终可用,用户可采用 USB 或者SD卡存储的形式装载外部存储,并在某些情况下会从设备中将其删除。所以在获取它的时候,需要确认是否可用。我们可以通过调用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;
}
尽管外部存储的数据可以被用户或者其他app更改,但是有两类文件要保存在这里:
公共文件
这类文件对于用户或者其他app应该是自由可用的,当用户卸载app时,该文件仍然会被保留。
比如,通过app拍下来的照片或者下载的数据。
私人文件
这类文件属于app,并且当用户卸载app时文件也会跟着被删除。虽然用户或者其他app实际上还是可以进入这个文件,因为它放在外部存储下面。这类文件不会给其他app提供数据。
比如,用户通过app缓存的一些视频文件。
如果想存储一些公共文件在外部存储中,使用 getExternalStoragePublicDirectory() 方法,会得到一个File值,表示一个合适的外部存储目录。这个方法可以传入参数来指定想要保存文件的类型,就可以与其他公共文件逻辑地组织在一起,比如 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;
}
如果想存储私人文件,则需要调用getExternalFilesDir() 。这样创建的每个目录,当用户卸载app,会被系统删除。
public File getAlbumStorageDir(Context context, String albumName) {
File file = new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
如果没有事先已经定义名字的文件,那就调用getExternalFilesDir(),不用传入参数。这样就会返回一个外部存储的根目录。
File file = new File(context.getExternalFilesDir());
查询剩余可用空间
如果提前知道了要保存多少数据,想要找足够的可用空间来存储不会抛出IO异常,那么就可以调用getFreeSpace()或者 getTotalSpace()来看看目前可用的空间和总的空间。
如果不知道确切需要多少空间,可以在保存数据之前不需要来检查可用空间的数量。我们可以尝试写入,然后捕捉IO异常。比如把png格式的图片转换成jpeg格式,我们事先并不知道所需要存储的大小。
删除文件
删除不再需要的文件。最直接的方法就是让该文件调用delete()。
file.delete();
如果文件保存在内部存储,也可以通过本地的Context来调用deleteFile(),传入文件名。
myContext.deleteFile(fileName);
注意: 当用户卸载app时,Android系统会删除的文件:
1、所有保存在内部存储的文件。
2、所有使用getExternalFilesDir()保存在外部存储的文件。
保存数据到SQL数据库中
如果数据是重复的有结构的,那么保存在数据库里面最合适了。比如说手机联系人的信息。
接下来,看看怎么把数据保持到SQL数据库中
定义一个Contract类
SQL databases主要的原则之一就是模式(即架构):告诉我们数据库是怎么组织的。这个模式就体现在我们创建数据所用到的SQL语句. 对于创建一个Contract类,模式是非常有用的。
那什么是Contract类呢?Contract类可以看做一个容器,里面定义了URI、表和其他的方法等。里面的每一个内部类对应一个table,会枚举出每一个属性。
public final class FeedReaderContract {
// 防止有人意外地实例化这个类,所以给它一个空的构造器
public FeedReaderContract() {}
...
/* 内部类定义表的内容 */
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";
...
}
...
}
注意: 通过实现 BaseColumn这个接口,内部类就可以继承一个域 _ID作为主键。一些类比如Cursor类会需要用的数据库中的主键。这不是必需的,但是能够帮助数据库和Android框架协调地工作。
创建一个数据库
就像保存在内部存储的文件一样,Android存储数据库在一个私有的空间里。数据是安全的,因为这片区域是不允许其他应用访问的。我们可以使用SQLiteOpenHelper 类,获得对数据库的引用。需要使用数据库的时候,调用getWritableDatabase() 或者 getReadableDatabase()。
注意:因为会长时间运行,要确保getWritableDatabase()或者getReadableDatabase()这两个方法在后台线程中调用,比如AsyncTask或者 IntentService。
创建一个子类继承SQLiteOpenHelper,覆盖onCreate(),onUpgrade()和onOpen()这些回调方法。还有一个 onDowngrade(),但这不是必需的。
public class FeedReaderDbHelper extends SQLiteOpenHelper {
// 如果改变了数据库的模式,那么必须升级数据库版本,在版本数上加1
public static final int DATABASE_VERSION = 1;
public static final String DATABASE_NAME = "FeedReader.db";
public FeedReaderDbHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
public void onCreate(SQLiteDatabase db) {
private static final String TEXT_TYPE = " TEXT";
private static final String COMMA_SEP = ",";
private static final String SQL_CREATE_ENTRIES =
"CREATE TABLE " + FeedEntry.TABLE_NAME + " (" +
FeedEntry._ID + " INTEGER PRIMARY KEY," +
FeedEntry.COLUMN_NAME_ENTRY_ID + TEXT_TYPE + COMMA_SEP +
FeedEntry.COLUMN_NAME_TITLE + TEXT_TYPE + COMMA_SEP +
...
" )";
private static final String SQL_DELETE_ENTRIES =
"DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
db.execSQL(SQL_CREATE_ENTRIES);
}
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 数据库只对在线数据进行缓存,所以它的升级方法就是简单地丢弃并重新开始
db.execSQL(SQL_DELETE_ENTRIES);
onCreate(db);
}
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onUpgrade(db, oldVersion, newVersion);
}
}
把信息存进数据库里面
为了可以进入数据库,要先实例化一个SQLiteOpenHelper子类——刚才创建的FeedReaderDbHelper。调用getWritableDatabase()可以向数据库写入数据。用一个ContentValues对象按照key-value的方式装入数据,然后通过传递一个ContentValues对象到insert()方法中,来插入数据:
FeedReaderDbHelper mDbHelper = new FeedWritableDbHelper(getContext());
SQLiteDatabase db = mDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_ENTRY_ID, id);
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
values.put(FeedEntry.COLUMN_NAME_CONTENT, content);
...
long newRowId;
newRowId = db.insert(
FeedEntry.TABLE_NAME,
null,
values);
这里insert()有三个参数:
第一个参数是表名。
第二个参数是当ContentValues为空情况下,给某些可为空的属性自动赋值 为NULL,一般用不到这
个功能,直接传入 null 。
第三个参数是一个 ContentValues 对象。
从数据库中读出数据
调用 query() 方法从数据库中读取信息。查询的结果会返回一个Cursor对象。
SQLiteDatabase db = mDbHelper.getReadableDatabase();
String[] projection = {
FeedEntry._ID,
FeedEntry.COLUMN_NAME_TITLE,
FeedEntry.COLUMN_NAME_UPDATED,
...
};
String selection = "where "+FeedEntry.COLUMN_NAME_TITLE+" = ?";
String selectionArgs = new String[]{"title_1"};
String sortOrder =
FeedEntry.COLUMN_NAME_UPDATED + " DESC";
Cursor cursor = db.query(
FeedEntry.TABLE_NAME,
projection,
selection,
selectionArgs,
null,
null,
sortOrder
);
cursor.moveToFirst()
long itemId = cursor.getLong(
cursor.getColumnIndex(FeedEntry._ID));
String title = cursor.getString(
cursor.getColumnIndex(FeedEntry.COLUMN_NAME_TITLE);
...
query这里第一个参数当然是表名了。
第二个参数,返回的每一列
第三参数是where语句
第四个参数是where语句指定的值
第五个参数是Group by语句
第六个参数是对Group by语句之后的过滤
第七个就是数据的排序了。
查询完返回一个Cursor对象,使用对象先调用 moveToFirst(),它将读取第一项的结果。对于每一行,通过调用getString()或者getLong()来读取每一列的值。每个方法再调用getColumnIndex() 或者 getColumnIndexOrThrow()来传递那一列的位置。想读取下一项的数据,就调用moveToNext()
删除数据库
delete()传入三个参数
第一个参数是表名
第二个参数是where语句
第三个参数是where语句指定的值
String selection = FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };
db.delete(table_name, selection, selectionArgs);
更新数据库
update()结合了insert()和delete()的一些参数,分别是tableName,value,selection,selectionArgs
SQLiteDatabase db = mDbHelper.getReadableDatabase();
// 新的值
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
String selection = FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);
不过也有人更加青睐于直接使用 SQL 来操作数据库
比如:
查询:
db.execSQL("insert into ComputerBook (name, author, pages, price) values(?, ?, ?, ?)",
new String[] { "Java Programing", "Dan Brown", "310", "49" });
更新
db.execSQL("update ComputerBook set price = ? where name = ?", new String[] { "50","Java Programing" });
删除
db.execSQL("delete from ComputerBook where pages > ?", new String[] { "300" });
查询
db.rawQuery("select * from ComputerBook", null);