(数据存储)Android系统存储数据

移动设备需要存储数据,处理数据并输出处理后的信息。

(数据存储)Android系统存储数据

主题一:存储键值对

If you have a relatively small collection of key-values that you'd like to save, you should use the SharedPreferences APIs. 使用SharedPreferences存储键值对

A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them. SharedPreferences实例实际上指向的是一个文件。Each SharedPreferences file is managed by the framework and can be private or shared.

Note: The SharedPreferences APIs are only for reading and writing key-value pairs and you should not confuse them with the Preference APIs, which help you build a user interface for your app settings (although they use SharedPreferences as their implementation to save the app settings). SharedPreferences只能用于存储键值对。

获取SharedPreferences实例

You can create a new shared preference file or access an existing one by calling one of two methods:

1. getSharedPreferences() — Use this if you need multiple shared preference files identified by name, which you specify with the first parameter. You can call this from any Context in your app.

2. getPreferences() — Use this from an Activity if you need to use only one shared preference file for the activity. Because this retrieves a default shared preference file that belongs to the activity, you don't need to supply a name.

其一,为SharedPreferences实例指定了文件名;其二,没有指定文件名,但该SharedPreferences属于当前的Activity。

Context context = getActivity();
SharedPreferences sharedPref = context.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE);

获取到以 R.string.preference_file_key 指定的SharedPreferences文件实例,并以Private的方式访问(只能在本App内部访问)。

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);

或者是上述情况:

Caution: If you create a shared preferences file with MODE_WORLD_READABLE or MODE_WORLD_WRITEABLE, then any other apps that know the file identifier can access your data.

若以MODE_WORLD_READABLE或者是MODE_WORLD_WRITEABLE创建文件时,其他App也可以获取到该数据;这样可能会引起安全问题。

向SharedPreferences文件写数据

To write to a shared preferences file, create a SharedPreferences.Editor by calling edit() on your SharedPreferences. Pass the keys and values you want to write with methods such as putInt() and putString(). Then call commit() to save the changes.

SharedPreferences sharedPref = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score), newHighScore);
editor.commit();

从SharedPreferences文件中读取数据

To retrieve values from a shared preferences file, call methods such as getInt() and getString(), providing the key for the value you want, and optionally a default value to return if the key isn't present.

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);

主题二:存储文件

Android uses a file system that's similar to disk-based file systems on other platforms. This lesson describes how to work with the Android file system to read and write files with the File APIs.

A File object issuited(数据存储)Android系统存储数据 to reading or writing large amounts of data in start-to-finish order without skipping around. 流式数据特别使用File的方式存储

存储内部还是存储外部?

All Android devices have two file storage areas: "internal" and "external" storage. 两者的区别如下:

(数据存储)Android系统存储数据

Some devices divide the permanent storage space into "internal" and "external" partitions, so even without a removable storage medium, there are always two storage spaces and the API behavior is the same whether the external storage is removable or not. 所有的Android文件系统都有两个文件存储区域:internal和external,这两个分区来自于早先的Android系统;当时的Android系统内置了不可变的内存internal和一个类似于SD Card的可卸载存储部件external。之后有一些设备将"internal" 与 "external" 都做成了不可卸载的内置存储,虽然如此,但是这一整块还是从逻辑上有被划分为"internal"与"external"的。只是现在不再以是否可卸载进行区分了。

外部存储可以通过物理介质提供(如SD卡),也可以通过将内部存储中的一部分封装而成,设备可以有多个外部存储实例。

Tip: Although apps are installed onto the internal storage by default, you can specify the android:installLocation attribute in your manifest so your app may be installed on external storage. Users appreciate this option when the APK size is very large and they have an external storage space that's larger than the internal storage.

获取外部存储权限

To write to the external storage, you must request the WRITE_EXTERNAL_STORAGE permission in your manifest file.

<manifest ...>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
...
</manifest>

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.

<manifest ...>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
...
</manifest>

对于Internal Storage的读写权限:You don’t need any permissions to save files on the internal storage. Your application always has permission to read and write files in its internal storage directory.

在Internal Storage中存储文件

首先需要获取Internal Storage的目录以及相关的File实例

getFilesDir(): Returns a File representing an internal directory for your app.

getCacheDir(): Returns a File representing an internal directory for your app's temporary cache files. Be sure to delete each file once it is no longer needed and implement a reasonable size limit for the amount of memory you use at any given time, such as 1MB. If the system begins running low on storage, it may delete your cache files without warning.

使用下述方法获取到内部存储的文件实例:上述方法获取到的文件路径--> /data/data/包名...

File file = new File(context.getFilesDir(), filename);

Alternatively, you can call openFileOutput() to get a FileOutputStream that writes to a file in your internal directory.

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();
}

或者是获取到Cached中的文件:

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;
}

对于文件权限的说明:Note: Your app's internal storage directory is specified by your app's package name in a special location of the Android file system. Technically, another app can read your internal files if you set the file mode to be readable. However, the other app would also need to know your app package name and file names. Other apps cannot browse your internal directories and do not have read or write access unless you explicitly set the files to be readable or writable. So as long as you use MODE_PRIVATE for your files on the internal storage, they are never accessible to other apps.

在外部存储中存储文件

You can query the external storage state by calling getExternalStorageState(). If the returned state is equal to MEDIA_MOUNTED, then you can read and write your files. 对于外部存储的可能状态,需要对当前状态进行判断;如果是Unmounted状态,是不能够读写的。

/* 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;
}

对于存放在外部存储设备中的文件,有以下两种情况:

Public files: Files that should be freely available to other apps and to the user. When the user uninstalls your app, these files should remain available to the user. For example, photos captured by your app or other downloaded files.

Private files:Files that rightfully belong to your app and should be deleted when the user uninstalls your app. Although these files are technically accessible by the user and other apps because they are on the external storage, they are files that realistically don't provide value to the user outside your app. When the user uninstalls your app, the system deletes all files in your app's external private directory. For example, additional resources downloaded by your app or temporary media files.

如果想要在外部存储空间中存放public files,use the getExternalStoragePublicDirectory() method to get a File representing the appropriate directory on the external storage. 这个方法会需要带有一个特定的参数来指定这些public的文件类型,以便于与其他public文件进行分类。参数类型包括DIRECTORY_MUSIC 或者 DIRECTORY_PICTURES. getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)--> /storage/sdcard0/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;
}

如果想要在外部存储空间存放private files,you can acquire the appropriate directory by calling getExternalFilesDir() and passing it a name indicating the type of directory you'd like. 每一个以这种方式创建的目录都会被添加到封装我们app目录下的参数文件夹external storage中(如下则是albumName)。这下面的文件会在用户卸载我们的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在external storage下的private的根目录。

Remember that getExternalFilesDir() creates a directory inside a directory that is deleted when the user uninstalls your app. If the files you're saving should remain available after the user uninstalls your app—such as when your app is a camera and the user will want to keep the photos—you should instead use getExternalStoragePublicDirectory().

Regardless of whether you use getExternalStoragePublicDirectory() for files that are shared or getExternalFilesDir() for files that are private to your app, it's important that you use directory names provided by API constants like DIRECTORY_PICTURES. These directory names ensure that the files are treated properly by the system. For instance, files saved in DIRECTORY_RINGTONES are categorized by the system media scanner as ringtones instead of music.

并没有强制要求在写文件之前去检查剩余容量;我们可以尝试先做写的动作,然后通过捕获 IOException。这种做法仅适合于事先并不知道想要写的文件的确切大小。例如,如果在把PNG图片转换成JPEG之前,我们并不知道最终生成的图片大小是多少。

获取存储空间大小

If you know ahead of time how much data you're saving, you can find out whether sufficient space is available without causing an IOException by calling getFreeSpace() or getTotalSpace(). These methods provide the current available space and the total space in the storage volume, respectively. This information is also useful to avoid filling the storage volume above a certain threshold.

删除文件

删除文件方式:

myFile.delete();

如果文件存储在内部存储中:

myContext.deleteFile(fileName);

Note: When the user uninstalls your app, the Android system deletes the following:

All files you saved on internal storage

All files you saved on external storage using getExternalFilesDir().

However, you should manually delete all cached files created with getCacheDir() on a regular basis and also regularly delete other files you no longer need.

主题三:使用SQL Database存储数据

Saving data to a database is ideal for repeating or structured data, such as contact information. The APIs you'll need to use a database on Android are available in the android.database.sqlite package.

建立数据库结构和约定 --> 也就是数据库的“骨架”

Schema是一种DB结构的正式声明,用于表示数据库的组成结构。Schema是从创建DB的SQL语句中生成的。

Contract类用于描述创建的数据库所需要的静态字段信息,指定了Schema样式。Contract Class是一些常量的容器,定义了URIs、表名、列名等。这个Contract类运行在同一个包下与其他类使用同样的常量,并让我们在一个地方修改列明,自动传递到整个全局。

组织Contract类的一种方式是:在类的根层级定义一些全局变量,然后为每一个Table来创建内部类。

下述定义了表名和该表的列名:

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";
...
}
}

Note: By implementing the BaseColumns interface, your inner class can inherit a primary key field called _ID that some Android classes such as cursor adaptors will expect it to have. It's not required, but this can help your database work harmoniously with the Android framework. 通过实现BaseColumns的接口,内部类可以继承一个名为_ID的主键,对于Android里面的一些类似Cursor Adapter类是很有必要的。这样能够使我们的DB与Android的Framework更好的契合。

创建数据库

实现和创建数据库和对应的数据表

下面是一些典型的创建和删除Table的语句:

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;

类似于保存文件到设备的Internal Storage,Android系统会将DB保存到程序的Private空间中,不可被其他程序访问。

当使用SQLiteOpenHelper类中的APIs时,系统会对哪些有可能比较耗时的操作(例如:创建与更新等),在真正需要执行的时候才去运行,而不是在App刚启动的时候就去运行。我们所需要做的仅仅是执行getWritableDatabase()或者getReadableDatabase()。

Note: Because they can be long-running, be sure that you call getWritableDatabase() or getReadableDatabase() in a background thread, such as with AsyncTask or IntentService.

为了使用SQLiteOpenHelper,需要创建一个子类并重写onCreate()、onUpgrade()、onOpen()等回调。

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);
}
}

为了操作数据库,需要获取到OpenHelper实例:

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(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);

读数据

查询数据库,使用query(),并传递需要查询的条件,返回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();
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedReaderContract.FeedEntry._ID)
);

删除数据

和查询数据库一样,删除数据同样需要提供一些删除标准。DB的API提供了一个防止SQL注入的机制来创建查询与删除标准。

该机制把删除和查询语句划分为两部分:选项条件和选项参数。条件定义了查询的列的特征,参数用于测试是否符合前面的条件。

// Define 'where' part of query.
String selection = 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();该方法结合了插入和删除语法。

SQLiteDatabase db = mDbHelper.getReadableDatabase();
// New value for one column
ContentValues values = new ContentValues();
values.put(FeedEntry.COLUMN_NAME_TITLE, title);
// Which row to update, based on the ID
String selection = FeedEntry.COLUMN_NAME_ENTRY_ID + " LIKE ?";
String[] selectionArgs = { String.valueOf(rowId) };
int count = db.update(
FeedReaderDbHelper.FeedEntry.TABLE_NAME,
values,
selection,
selectionArgs);

P.S.

SQL Injection:(随着B/S模式应用开发的发展,使用这种模式编写应用程序的程序员也越来越多。但由于程序员的水平及经验也参差不齐,相当大一部分程序员在编写代码时没有对用户输入数据的合法性进行判断,使应用程序存在安全隐患。用户可以提交一段数据库查询代码,根据程序返回的结果,获得某些他想得知的数据,这就是所谓的SQL Injection,即SQL注入)

上一篇:Android Volley 之自己定义Request


下一篇:Volley自定义Request及使用单例封装RequestQueue