绝大多数的Android App都需要保存数据,即使仅仅在onPause()方法里保存app状态信息以免user进度信息被丢失。大多数非著名的app也可能要保存用户设置信息。一些应用需要在文件和数据库保存大量的信息。本文将向你讲述Android主要的数据存储方式。包括:
- 在SharedPreferences文件里保存简单数据类型的key-value键值对。
- 在Android文件系统里保存任意类型的文件。
- 使用SQLite数据库
Lessons
Learn to use a shared preferences file for storing small amounts of information in key-value pairs.
Learn to save a basic file, such as to store long sequences of data that are generally read in order
Learn to use a SQLite database to read and write structured data.
Saving Key-Value Sets
如果你有少量的key-value数据需要保存,你应该使用SharePreferences APIs。一个SharedPreferences对象指向一个包含key-value对的文件,并提供了简单的方法读写它们。SDK框架管理每个SharedPreferences文件,你可以指定该文件是私有的还是共享的。
本文告诉你如何使用SharedPreferences的APIs存储和检索简单的值。
注: SharedPreferences
APIs仅仅用于读和写键-值对,你不应该使用Preference APIs混淆它们。Preference是一个帮助你处理你的app设置的用户界面(虽然SharePrerences是保存用户设置的preference界面的一个默认实现类)。更多如何使用Preference APIs的信息,参见Settings 向导。
Get a Handle to a SharedPreferences
你能调用如下两个方法之一产生一个新的shared preferences文件或者访问一个已存在的sharedPreferences文件。
-
getSharedPreferences()
— 如果需要多个用名字标识的sharedpreferences文件使用该方法,你能用第一个参数指定文件名字。你必须从你的app里的任何Context上调用该方法。 -
getPreferences()
— 该方法在Activity上调用,如果在该Activity里你只需要一个sharedPreferences文件可以调用该方法。因为该方法检索一个默认的sharedPreferences。该默认文件属于调用该方法的activity,你不需要提供一个名字。
例如,下面的代码在一个fragment里执行。它访问一个由R.string.preference_file_key字符串资源指定的sharedPreferences文件。该文件已私有模式访问,因此,仅仅你的app能访问该文件。
Context context = getActivity(); SharedPreferences sharedPref = context.getSharedPreferences( getString(R.string.preference_file_key), Context.MODE_PRIVATE);
当命名你的sharedPreferences文件时,你应该使用一个你的app范围内唯一标识的名字。例如“com.example.myapp.PREFERENCE_FILE_KEY”。
或者,你仅仅在某个activity里只需要一个shared preferences文件,你能使用getPreferences()方法:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
注意:如果你用MODE_WORLD_READABLE或者MODE_WORLD_WRITEABLE模式产生一个shared preferences文件,那么任何知道该文件名的app都能访问你的数据。
Write to Shared Preferences
为了写数据到shared preferences文件,调用SharedPreferences的edit()方法产生一个SharedPreferences.Editor对象。
调用例如putInt()或者putString()方法传递要写入的key-value值,然后调用commit()方法保存这些值。例如:
SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPref.edit(); editor.putInt(getString(R.string.saved_high_score), newHighScore); editor.commit();
Read from Shared Preferences
为了从shared preferences文件读取数据,调用getInt()或者getString()等方法,提供你要的数据的key值,如果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);
Saving Files
Android使用的文件系统类似于其他平台的基于磁盘的文件系统。本文描述如何使用Android的文件系统和 File
APIs读和写文件。
File对象适合从头到尾没有调过的读和写大量的数据。例如,图片文件和网络交互的数据。
本文讲述在你的app里如何执行基本的文件相关的操作。本文假设你对Linux文件系统基础和java.io包里的标准文件输入/输出APIs很熟悉。
Choose Internal or External Storage
所有的Android设备有两个文件存储区域:“internal”和“external”存储。这些名字来自于Android早期,当时大多数设备提供内置的非易失的内存(内部存储),同时提供一个可移除存储媒介例如一个micro SD卡(外部存储)。一些设备把永久存储空间划分为“internal” 和 “external”部分,因此,甚至没有一个可移除的存储媒介,也总是存在“internal” 和 “external”两个存储空间,且外部存储不管是不是可移除的,API行为都是相同的。
提示:虽然app默认的安装到内置存储,但你能在manifest文件里指定android:installLocation
属性让你的app安装到外部存储。当APK大小是非常大,同时用户有外置存储,并且外置存储比内置存储空间更大时,需要可能会需要改选项。
Obtain Permissions for External Storge
为了写到外部存储,你必须在manifest file里添加WRITE_EXTERNAL_STORAGE权限:
<manifest ...> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> ... </manifest>
注意:当前,app不用指定专门的权限而去读外部存储。然而,以后发版的android版本将改变这点。如果你的app需要读(但不写)外部存储,那么你将需要声明READ_EXTERNAL_STORAGE权限。为了确保你的应用能正常的运行,你应该总是声明该权限,即使该改变还没有生效。
<manifest ...> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> ... </manifest>
然而,如果你的app使用WRITE_EXTERNAL_STORAGE权限,那么,它隐式的包含有读外部存储的权限。
在内部存储上保存文件你不需要任何权限。你的app始终有读和写内部存储文件的权限。
Save a File on Internal Storage
当保存文件到内部存储时,你能调用下面两个方法里的一个获取合适的文件目录。
返回一个代表你的应用app的内部目录的文件。
getCacheDir()
返回一个内部存储目录的文件作为你的app的临时缓存文件。一定要一旦该缓存文件不再需要时删除该文件,从而实现合理的内存大小限制,例如1M。如果系统开始变的存储紧张,系统将不给你任何警告的情况下删除你的缓存文件
为了在这些目录中产生新的文件,你能使用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里抽取文件名,然后用你的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; }
注:你的app的内部存储目录在android文件系统的一个特定的位置通过app的包名被指定。理论上,如果你设置你的内部文件为可读模式,其他的app能读取你的内部文件。当然,其他的app也将需要知道你的app的包名和文件名。其他的应该不能浏览你的内部存储目录,也没有读写访问权限,除非你显示的设置文件为可读和可写模式。因此一旦你设置你的内存存储中的文件为MODE_PRIVATE模式,其他的app将不能访问。
因为外部存储可能不是总是有效的——例如当用户挂载存储到PC或者移除作为外部存储的SD卡时——你应该在访问外部存储之前总是检查该卷是否有效。你能调用getExternalStorageState()方法插叙外部存储状态。如果返回的状态值等于MEDIA_MOUNTED
,那么你能读写外部存储文件。例如,下面的方法被用于检查存储是否有效:
/* Checks if external storage is available for read and write */ public boolean isExternalStorageWritable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state)) { return true; } return false; } /* Checks if external storage is available 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; }
虽然外部存储能被用户或者其他的app修改,有两类文件你能保存在外部存储上:
Public files
文件时完全的公开的, 对其他的app和用户是完全可见的。当用户卸载你的app,这些文件应该仍 然保留,对用户有效。
例如:用户拍的照片或者下载的文件。
Private files
指仅仅属于你的app,当用户卸载你的app时应该被删除的文件。虽然理论上这些文件也可以被用 户或者其他的app访问,因为它们在外部存储。当用户卸载你的app时,系统应该删除所有的外部 存储里私有目录下的所有文件。
例如,你的app下载的额外资源或者临时媒体文件。
如果你想在外部存储保存public的文件,使用getExternalStoragePublicDirectory()方法获取SD卡上合适的public目录。该方法接受一个指定文件类型的参数,以便于该文件和其他的公共的文件能逻辑地组织,例如DIRECTORY_MUSIC或者DIRECTORY_PICTURES。如下:
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; }
如果你想要保存文件是私有的,你能通过调用getExternalFilesDir()方法取得合适的私有目录,传递一个表明目录类型的名字。这种方式产生的目录被添加到一个包含了你的app所有私有文件的父目录。这些,当用户卸载你的app时系统会删除所有的私有文件。
例如,如下是一个产生个人相片册目录的方法:
public File getAlbumStorageDir(Context context, String albumName) { // Get the directory 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值。这样系统返回你的app私有文件目录的根目录。
记住通过getExternalFileDir()产生的目录,当用户卸载你的app时,该目录下的文件和该目录会被删除。
如果你想保存的文件在app卸载后仍然有效——例如你的app是一个照相机应用,用户想要保存照片——你应该使用getExternalStoragePublicDirectory()
.方法。
不管你是使用getExternalStoragePublicDirectory()方法产生共享文件还是使用getExternalFilesDir()产生私有文件,使用系统提供的API常量例如DIRECTORY_PICTURES作为目录是非常重要的。这些目录名确保文件被系统合理的处理和对待。例如,保存在 DIRECTORY_RINGTONES
里的文件能被系统media scanner分类作为铃声而非音乐。
Query Free Space
如果你提前知道你要保存多少数据,你可能需要知道是否有足够的空间是有效的,而不引起IOException.这可以通过调用getFreeSpace()或者getTotaolSpace()方法实现。该方法分别提供了存储卷当前的剩余空间和总空间大小。其他的情况下可需要知道SD卡的存储大小信息,例如空间信息在避免超出一定的存储阀值避免写入也是有用的。
然而,系统并不能保存写入和getFreeSpace()返回值大小一样的字节值。如果剩余空间比你要保存的数据大小多几MB。或者文件系统剩余空间大于10%,那么保存文件操作是安全的。否则,你可能不适合想存储卷里写。
注:当你不知道你保存的文件大小时,你不应该检查有效空间量,而是试着写文件,然后捕获IOException。例如你将文件从PNG图片格式转换到JPEG格式时,你并不知道转换后的文件大小。
Delete a File
你应该总是记得删除你不再需要的文件。删除文件最直接的方式就是有一个打开文件的应用,调用其上的delete()方法
myFile.delete();
如果该文件保存在内部存储,你也能获取Contex,调用该对象的deleteFile()
方法删除文件:
myContext.deleteFile(fileName);
注:当用户卸载你的app时,Android系统会删除:
- 你保存在内存存储的所有文件
- 你通过调用
getExternalFilesDir()
方法保存在外部存储的文件
然而,你也应该手动的删除调用getCacheDir()
方法产生的缓存文件,也应该删除其他的你不需要的文件。
Saving Data in SQL Databases
对于重复的或者结构化的数据保存数据到数据库是一个理想的方式,例如联系人信息。 本文假设你对SQL 数据库的一般知识是熟悉的,帮助你如何在Adnroid上使用SQLite数据库。Android上操作数据库的API在android.database.sqlite包下。
Defina a Schema and Contract
SQL数据库的主要原则之一是schema:数据库怎么被组织的格式化声明。schema反射到你创建数据库的SQL语句上。schema在产生companion类时是很有用的,例如contract类,其通过系统的和自描述方式显示的指定了你的schema的布局。
contract类是一个定义URI,表和栏的常量的容器。contract类允许你在相同包里的不同类之间使用同样的常量。这使得你修改的你栏名只需要修改一个地方即可。
组织contract类的一种好的方式是把数据库的全局的定义放在该类的根上,然后对每个表产生一个内部类来枚举该表的栏。
注:通过实现BaseColumns接口,你的内部类能继承一个名为_ID的主键属性。Android里一些类例如cursor adaptor期望有该属性。这是不必须的,但是这能使得你的数据库在Android框架上工作和谐。
例如,下面代码片段定义了表名和某个表的栏名:
public final class FeedReaderContract { // To prevent someone from accidentally instantiating the contract class, // give it an empty constructor. public FeedReaderContract() {} /* Inner class that defines the table contents */ 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"; ... } }
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 " + 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 + ... // Any other options for the CREATE command " )"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
像保存在内部存储的文件,Android保存你的数据库在与你的app相关联的私有存储空间。这样你的数据是安全的,默认地,这块区域不能被其他的应用访问。
一些操作数据库的有效API在类SQLiteOpenHelper里 。当你使用该类获取你的数据库引用时,系统可能执行一个耗时操作产生和更新数据库。仅仅在需要时执行而不是应用启动时,所有你需要做的是调用 getWritableDatabase()
或者getReadableDatabase()
.
注:因为 getWritableDatabase()
orgetReadableDatabase()
.是耗时操作,确保在后台线程调用它们,例如AsyncTask
or IntentService。
为了使用SQLiteOpenHelper,产生该类的子类,重写 onCreate()
, onUpgrade()
and 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());
Put Information into a Database
通过传递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(FeedEntry.COLUMN_NAME_ENTRY_ID, id); values.put(FeedEntry.COLUMN_NAME_TITLE, title); values.put(FeedEntry.COLUMN_NAME_CONTENT, content); // Insert the new row, returning the primary key value of the new row long newRowId; newRowId = db.insert( FeedEntry.TABLE_NAME, FeedEntry.COLUMN_NAME_NULLABLE, values);insert()方法的第一个参数是表名。第二参数用于指定可以插入NULL值的栏名,以防ContentValues为空(如果你设置该参数为“null”,没有值将不能被插入)。
Read Inforamtion from a Database
为了从数据库里读取数据,使用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 = { FeedEntry._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_UPDATED, ... }; // How you want the results sorted in the resulting Cursor String sortOrder = FeedEntry.COLUMN_NAME_UPDATED + " DESC"; Cursor c = db.query( 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里的一行,使用cursor的移动方法,在你从cursor读取数据时,你必须总是调用该方法。一般地,开始你应该调用 moveToFirst() 方法,该方法移动游标到结果的开始处。对于每一行,你可以调用Cursor的get方法获取某一个栏的值,例如
getString()
or
getLong()
,对于get方法,你必须传递栏的索引位置给get方法,你能调用
getColumnIndex()
or
getColumnIndexOrThrow()
方法获取栏索引,例如: