Getting Started-----Saving Data

        绝大多数的Android App都需要保存数据,即使仅仅在onPause()方法里保存app状态信息以免user进度信息被丢失。大多数非著名的app也可能要保存用户设置信息。一些应用需要在文件和数据库保存大量的信息。本文将向你讲述Android主要的数据存储方式。包括:

  • 在SharedPreferences文件里保存简单数据类型的key-value键值对。
  •  在Android文件系统里保存任意类型的文件。
  • 使用SQLite数据库

        Lessons

          Saving Key-Value Sets

              Learn to use a shared preferences file for storing small amounts of information in key-value pairs.

          Saving Files 

               Learn to save a basic file, such as to store long sequences of data that are generally read in                        order

           Saving Data in SQL Databases 

              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

        当保存文件到内部存储时,你能调用下面两个方法里的一个获取合适的文件目录。

       getFilesDir()

        返回一个代表你的应用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() 方法获取栏索引,例如:         
cursor.moveToFirst();
long itemId = cursor.getLong(
    cursor.getColumnIndexOrThrow(FeedEntry._ID)
);
                  Delete Information from a Database          为了删除表里的某行,你需要提供表示行的选择标准。数据库API提供了产生选择标准的机制,该机制防止SQL注入。该机制将选择条件分成了选择条款和选择参数。条款定义了查看的栏。参数是绑定到选择从句的值。因为结果不是被规则的SQL语句处理,最小的减少了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 a Database         当你需要修正你的数据库的部分字段的值时,使用 update()   方法。          Updating the table combines the content 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(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);
 

        

猜你喜欢

转载自xhmj12.iteye.com/blog/2083096