目录(?)[+]
大多数Android应用程序需要保存数据,即使只为了不丢失用户的进度,在onPause()中保存应用程序的状态信息。大部分不平凡的应用程序也需要保存用户设置,而一些应用程序必须在文件和数据库中管理大量信息。本课程介绍你在Android的主要数据存储选项,包括:
· 在一个共享的首选项文件中保存键-值对简单数据类型
· 在Android的文件系统中保存任意文件
· 使用SQLite进行数据库管理
保存键-值集
了解使用共享首选项文件存储少量的信息键-值对的。
保存文件
了解保存基本文件,如要存储的数据一般都是按顺序阅读的长序列。
将数据保存在SQL数据库
了解如何使用SQLite数据库读取和写入结构化的数据。
如果你有一个相对较小的键-值的集合想要保存,你应该使用SharedPreferencesAPI。SharedPreferences对象指向一个包含键-值对文件,并提供简单的方法来读取和写入它们。每个 SharedPreferences文件由框架管理,可以设为私有或共享。
本课程向您展示如何使用SharedPreferences API来存储和检索简单的值。
注:SharedPreferences API只能读取和写入键-值对,你不应该将它与Prefernce的API混淆,Prefernce帮助你为你的应用程序设置建立一个用户界面(虽然它们保存应用程序设置是通过SharedPreferences来实现
)。对于使用的Preference API的信息,请参阅“ 设置”指南。
您可以通过调用以下两种方法之一来创建一个新的或访问一个现有的共享偏好文件:
· getSharedPreferences() -如果你需要使用多个不同文件名的共享偏好文件,可以使用这个方法,用第一个参数指定文件名。你可以在您的应用程序的任何 上下文(
Context
)中
调用这个方法。
· getPreferences() -如果你只需要为活动(Activity)使用一个共享偏好文件,可以在活动使用此方法。因为它检索的默认共享偏好文件从属于活动,所以你不需要提供一个文件名。
例如,下面的代码在一个碎片(Fragment
)里
执行。它访问由资源字符串R.string.preference_file_key
所标识的共享文件,并使用私有的模式,所以只有您的应用程序才能访问该文件。
Context context = getActivity(); SharedPreferences sharedPref = context.getSharedPreferences( getString(R.string.preference_file_key), Context.MODE_PRIVATE);
(在新版的API中,已经不需要通过getActivity()方法得到context对象,可直接调用getSharedPreferences()方法,译者注)
为共享偏好文件命名时,你应该使用一个应用程序能唯一识别的名称,如“com.example.myapp.PREFERENCE_FILE_KEY”
另外,如果你的活动只需要一个共享的偏好设置文件,您可以使用 getPreferences()方法:
SharedPreferences sharedPref= getActivity().getPreferences(Context.MODE_PRIVATE);
注意:如果您以MODE_WORLD_READABLE或MODE_WORLD_WRITEABLE方式
创建了一个共享的偏好文件,任何其它知道该文件标识符的应用程序都可以访问您的数据。
要写入一个共享的喜好文件,对您的SharedPreferences调用edit()方法创建一个SharedPreferences.Editor。
通过putInt()和putString()等
方法传递你想要写的键和值。然后调用commit()方法来保存更改。例如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPref.edit(); editor.putInt(getString(R.string.saved_high_score), newHighScore); editor.commit();
从一个共享的偏好文件检索值,可以调用调用getInt()的getString()方法,提供你想要的值的键,和一个当键不存在时默认返回值(可选的)。例如:
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);
跟Google学习Android开发-起始篇-保存数据(2)
分类: 精彩江湖 2013-05-22 17:22 307人阅读 评论(0) 收藏 举报翻译 教程 Google Android 官方目录(?)[+]
5.2 保存文件
Android使用的文件系统,这是在其他平台上的基于磁盘的文件系统类似。这节课介绍了如何使用Android文件系统的File API 来读取和写入文件。
一个File对象适合以从头到尾非跳跃的方式读取或写入大量的数据。例如,它适合图像文件或任何在网络上交换的东西。
这节课在您的应用程序显示了如何执行基本的文件相关的任务。这节课假定您是熟悉Linux文件系统的基础知识和java.io中的标准文件输入/输出API 。
选择内部或外部存储
所有的Android设备有两个文件存储区:“内部”和“外部”存储。这些名称来自Android的早期,那时大多数设备提供内置的非易失性存储器(内部存储器),加上一个可移动存储介质如微型SD卡(外部存储)。某些设备把永久存储空间划分为“内部”和“外部”分区,所以,即使没有可移动存储介质,都会有两个存储空间,并且不管外部存储是否可移动,API的行为都是相同的。下面的列表总结了每个存储空间的事实。
内部存储:
· 它总是可用的。
· 默认情况下,这里保存的文件只有您的应用程序能访问。
· 当用户卸载你的应用程序,系统将从内部存储删除你的应用程序的所有文件。
当你要确信无论是用户还是其他的应用程序都不可以访问您的文件的时候,内部存储是最好的选择。
外部存储:
· 它并不总是可用的,因为用户可以用USB存储设备作为外部存储,并在某些情况下,把它从装置中取出。
· 这是全局可读,所以保存在这里的文件可能会在你的控制之外被读取。
· 当用户卸载您的应用程序时,只有当你之前是用getExternalFilesDir()方法将你的应用程序的文件保存在目录,系统才会从这里删除它们。
不需要访问限制的文件和要与其它应用程序共享的文件,或者允许用户用电脑访问时,外部存储是最好的地方。
提示:虽然默认情况下应用程序安装到内部存储的,你可以在你的清单文件指定
android
:
installLocation
属性,让您的应用程序可以安装在外部存储。当APK大小是非常大的并且用户有一个比内部存储大得多的外部存储时,他们偏向于这个选项。欲了解更多信息,请参阅应用程序安装位置。
获取外部存储的权限
要写入到外部存储,您必须在你的manifest文件中请求WRITE_EXTERNAL_STORAGE权限:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>注意: 目前,所有的应用程序都无需特殊权限就能够读取外部存储。然而,这将在未来的版本中改变。如果你的应用程序需要读取外部存储(但不需要写入),那么您将需要声明READ_EXTERNAL_STORAGE权限。要确保你的应用继续按预期方式工作,你应该在更改生效前现在就声明此权限。
<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
...
</manifest>但是,如果你的应用程序使用WRITE_EXTERNAL_STORAGE 权限,那么它同时隐含有权限读取外部存储。
你不需要任何权限在内部存储上保存文件。你的应用程序总是有权限在其内部存储目录读取和写入文件。
在内部存储上保存文件
保存一个文件到内部存储时,通过以下两种方法之一,你可以获取相应的目录作为 File对象:
getFilesDir()
返回一个表示您的应用程序的内部目录的File对象。
getCacheDir()
返回一个你的应用程序的临时缓存文件的内部目录File对象。确保每个文件一旦不再需要时删除它们,并在任何给定的时间内对使用的内存施加一个合理的大小限制,如1MB。如果系统开始在低存储情况下运行,它可能会在没有警告的情况下删除您的缓存文件。
要在这些目录中创建一个新的文件,你可以使用File()构造函数,传递文件由上述指定你的内部存储目录的方法之一提供的File对象。例如:
File file= new File(context.getFilesDir(), filename);
或者,您可以调用openFileOutput()得到一个FileOutputStream 写入到内部目录的文件中。例如,这里是写一些文字到一个文件的方法:
String filename= "myfile";
String string= "Hello world!";
FileOutputStream outputStream;
try {
outputStream = openFileOutput(filename,Context.MODE_PRIVATE);
outputStream.write(string.getBytes());
outputStream.close();
} catch (Exception e){
e.printStackTrace();
}或者,如果你需要缓存一些文件,你应该使用createTempFile()代替。例如,下面的方法从URL中提取文件名,并用该名字在您的应用程序的内部缓存目录中创建一个文件:
publicFile 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;
}注: 您的应用程序的内部存储目录由您的应用程序的包名指定在Android文件系统中一个特殊的位置。从技术上说,另一个应用程序可以读取你的内部文件,如果你设置了文件模式是可读的。然而,其他应用程序还需要知道你的应用程序包名和文件名。其他应用程序不能浏览您的内部目录也没有读或写访问权限,除非你明确地设置文件可以读写。所以只要你对内部存储中的文件使用MODE_PRIVATE,它们对其他的应用程序就从来不可用。
在外部存储上保存文件
由于外部存储可能不可用,例如,当用户安装存储到PC上或移除了提供外部存储的SD卡,你一定要在访问它前确认它可用。您可以调用getExternalStorageState()查询外部存储状态。如果返回的状态等于MEDIA_MOUNTED,那么你可以阅读和写入文件。例如,可以用下列方法来确定存储可用:
/* Checks if external storage is available for read andwrite */
public boolean isExternalStorageWritable(){
String state =Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)){
return true;
}
return false;
}
/* Checks if external storage isavailable to at least read */
public boolean isExternalStorageReadable(){
String state =Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)||
Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)){
return true;
}
return false;
}虽然外部存储是对用户和其他应用程序是可修改的,有两类文件你可能会保存在这里:
公共文件
自由提供给其他应用程序和用户的文件。当用户卸载你的应用程序,这些文件应该对用户仍然可用。
例如,由你的应用程序拍摄的照片或其他下载的文件。
私有文件
原本属于您的应用程序并应该在用户卸载您的应用程序时删除的文件。由于它们是在外部存储中,尽管这些文件在技术上是可由用户和其他应用程序来访问,但它们实际上是在您的应用程序外不提供用户价值的文件。当用户卸载你的应用程序时,系统会删除你的应用程序的外部私有目录中的所有文件。
例如,您的应用程序下载的额外资源或临时的媒体文件。
如果你想在外部存储保存公共文件,使用 getExternalStoragePublicDirectory()方法来获得一个File对象表示外部存储中的相应目录。该方法需要一个参数指定你要保存的文件类型,使它们可以与其它公共文件在逻辑上组织好,比如DIRECTORY_MUSIC或DIRECTORY_PICTURES。例如:
publicFile getAlbumStorageDir(String albumName){
// Get thedirectory 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;
}如果你想保存对您的应用程序是私有的文件,你可以通过调用getExternalFilesDir(),并传递给它一个名字,表示你想要的目录类型,来获得相应的目录。这种方式创建每个目录都会添加到一个父目录,该父目录封装了你的应用程序的所有外部存储文件,当用户卸载您的应用程序时系统会删除这件文件。
例如,这里有一个方法,你可以使用它来创建一个个人相册目录:
publicFile getAlbumStorageDir(Context context,String albumName){
// Get thedirectory for the app's private pictures directory.
File file =new File(context.getExternalFilesDir(
Environment.DIRECTORY_PICTURES), albumName);
if (!file.mkdirs()){
Log.e(LOG_TAG,"Directory not created");
}
return file;
}如果没有预先定义的子目录名称匹配你的文件,你可以调用getExternalFilesDir(),并传递null。它会返回外部存储上您的应用程序的私有目录的根目录。
请记住,getExternalFilesDir() 在这样的目录下创建一个目录:当用户卸载您的应用程序时该目录会被删除。如果你保存的文件,要在用户卸载后您的应用程序还能保留,例如,你的应用程序是一个摄像头而用户将要保留照片,你应该使用getExternalStoragePublicDirectory()代替。
无论你对要共享的文件使用getExternalStoragePublicDirectory()还是对您的应用程序是私有的文件使用getExternalFilesDir(),使用像DIRECTORY_PICTURES这样的API常量作为目录名,对你都是相关重要的 。这些目录的名称,确保这些文件由系统妥善处理。例如,保存在DIRECTORY_RINGTONES的文件被系统媒体扫描仪归类为铃声,而不是音乐。
查询空闲空间
如果你提前知道你要保存多少数据,调用getFreeSpace()或getTotalSpace()你可以知道是否有足够的空间可用,而不是引起一个IOException。这些方法分别提供了当前可用空间和存储容量总空间。此信息也有助于避免填充的存储量超过某个临界值。
然而,系统并不能保证你可以完全写入getFreeSpace()表示的字节数。如果返回的数字比您想要保存的数据的大小超过几MB,或者如果文件系统还不到90%,那么它进行保存可能是安全的。否则,你可能不应该写入到存储中。
注:在您保存你的文件之前,你并不需要检查可用空间。相反,你可以马上尝试写入文件,然后捕获一个IOException,如果它发生了。您可能需要这样做,如果你不知道你需要的空间究竟有多大。例如,如果你把文件转换为PNG图片或JPEG来保存之前,改变它的编码,那么你不会事先知道文件的大小。
删除文件
你应该总是删除您不再需要的文件。删除一个文件,最简单的方法是对打开的文件的引用本身调用delete()方法。
myFile.delete();
如果该文件被保存在内部存储,你还可以使用Context调用DeleteFile() 定位并删除一个文件:
myContext.deleteFile(fileName);
注:当用户卸载你的应用程序,Android系统将删除以下内容:
· 你保存在内部存储上的所有文件
· 你使用getExternalFilesDir()保存在外部存储上的所有文件。
然而,你应该手动删除所有定期用getCacheDir()创建的缓存文件 ,还定期删除不再需要的其他文件。
5.3将数据保存在SQL数据库
对于重复或结构化的数据,如联系人信息,将它们保存到数据库是理想选择。这节课假定您熟悉一般的SQL数据库,并帮助您开始在Android上使用SQLite数据库。在Android上,你需要使用数据库的API都在 android.database.sqlite包提供。
定义一个模式(Schema)和合同(Contract)
SQL数据库的主要原则之一是模式:数据库是如何组织的正式声明。该模式体现在你用来创建数据库的SQL语句中。您可能会发现它有利于创造一个同伴(companion)类,作为合同类为人所知,后者用系统和自文档(self-documentint)的方式明确指定您的模式的布局。
合同类是定义URI、表和列名等常量的一个容器。合同类,允许您在同一个包中的其它所有类里使用相同的常量。这可以让你在一个地方改变某个列名,它就传遍你的代码。
组织合同类的一个好方法就是把你的整个数据库的全局定义放到类的根层次。然后为每个枚举列的表创建一个内部类。
注:通过实现BaseColumns接口,内部类可以继承一个称为
_ID
的
主键字段,某些Android类,如光标适配器,会期望你的内部类有这个字段。它不是必需的,但这个可以帮你的数据库与Android框架和谐地工作。例如,这个片段定义为一个单一的表的表名和列名:
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"; ... }
为了防止有人意外地实例化合同类,给它一个空的构造方法。
// Prevents the FeedReaderContract class from beinginstantiated. private FeedReaderContract() {}
使用SQL Helper创建数据库
一旦你定义完你的数据库的样子,你应该实现创建和维护数据库和表的方法。下面是一些典型的创建和删除一个表的语句:
private static final String TEXT_TYPE = " TEXT"; private static final String COMMA_SEP = ","; private static final String SQL_CREATE_ENTRIES = "CREATETABLE " + 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 = "DROPTABLE IF EXISTS " + TABLE_NAME_ENTRIES;
就像您保存在设备内部存储的文件那样,Android的存储你的数据库在与应用程序关联的私有磁盘空间。您的数据是安全的,因为默认情况下,这个区域对其他应用程序是不可访问的。
一组有用的API,可在SQLiteOpenHelper类中找到。当你使用这个类来获取到你的数据库的引用时,系统执行潜在的长时间运行的创建和更新数据库的操作,只会在需要的时候,而不是在应用程序启动时。你所要做的全部就是调用getWritableDatabase()或 getReadableDatabase() 。
注:因为他们可以长时间运行,确保您是在后台线程调用getWritableDatabase()或getReadableDatabase(),比如用AsyncTask或IntentService。
要使用 SQLiteOpenHelper,创建一个子类重写 onCreate(), onUpgrade() 和 onOpen()回调方法。您可能还想要实现onDowngrade()方法,但它不是必需的。
例如,这里是SQLiteOpenHelper使用上面展示的命令的一个实现:
public class FeedReaderDbHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. 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) { 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());
将信息存入数据库
通过传递一个ContentValues 对象到 insert() 方法,可以把数据插入到数据库中:
// Gets the data repository in write mode SQLiteDatabase db = mDbHelper.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); 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 the new row, returning the primary key value of the new row long newRowId; newRowId = db.insert( FeedReaderContract.FeedEntry.TABLE_NAME, FeedReaderContract.FeedEntry.COLUMN_NAME_NULLABLE, values);insert()方法的第一个参数很简单就是表名。第二个参数提供列名,如果 ContentValues为空(empty)框架会为该列插入NULL值 (如果你把列名设为
"null"
, 那么当它没有值时框架将不会插入一行).
从数据库中读取信息
使用query() 方法从数据库中读取,传递你的选择子句和所需的列。该方法结合了insert() 和update()的元素,除了那些列是定义你想获取的数据,而不是要插入的数据。查询结果在一个Cursor对象中返回给你。
SQLiteDatabase db = mDbHelper.getReadableDatabase(); // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { FeedReaderContract.FeedEntry._ID, FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED, ... }; // How you want the results sorted in the resulting Cursor String sortOrder = FeedReaderContract.FeedEntry.COLUMN_NAME_UPDATED + " DESC"; Cursor c = db.query( FeedReaderContract.FeedEntry.TABLE_NAME, // The table to query 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 // The sort order );为了看光标中的一行,使用其中一个Cursor 的移动方法,在开始读取值之前你必须始终调用这些方法。一般来说,你应该从调用moveToFirst()开始,让“读取位置”定位到结果中的第一项。对于每一行,你可以通过调用一个Cursor 的get方法来读取列的值,如 getString()或getLong()。对于每一个get方法,你必须传递你想要的列的索引位置,你可以通过调用getColumnIndex() 或getColumnIndexOrThrow()得到列的索引。例如:
cursor.moveToFirst(); long itemId = cursor.getLong( cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID) );
从数据库中删除信息
要删除表中的行,你需要提供的选择条件来确定哪些行。数据库API提供了一种机制来创建可防止SQL注入的选择条件。该机制将选择规范分解成一个选择子句和选择参数。子句定义列,也允许你组合列来试验。参数是试验绑定到子句的值。(原文:The clausedefines the columns to look at, and also allows you to combine column tests.The arguments are values to test against that are bound into the clause.)因为结果与一个常规的SQL语句处理不一样,它对SQL注入是免疫的。
// Define 'where' part of query. String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?"; // Specify arguments in placeholder order. String[] selectionArgs = { String.valueOf(rowId) }; // Issue SQL statement. db.delete(table_name, selection, selectionArgs);
更新数据库
当你需要修改你的数据库数值的一个子集时,使用 update() 方法。更新表内容结合使用insert() 方法的语法和带where的delete()方法的语法。(原文:Updating the table combines thecontent values syntax of insert()with the where syntax of delete())
SQLiteDatabase db = mDbHelper.getReadableDatabase(); // New value for one column ContentValues values = new ContentValues(); values.put(FeedReaderContract.FeedEntry.COLUMN_NAME_TITLE, title); // Which row to update, based on the ID String selection = FeedReaderContract.FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?"; String[] selectionArgs = { String.valueOf(rowId) }; int count = db.update( FeedReaderDbHelper.FeedEntry.TABLE_NAME, values, selection, selectionArgs);