Android存儲方式

Android系統提供4種基本的數據存儲方式,分別是SharedPreferences存儲方式,文件存儲方式,SQLite數據庫存儲方式和ContentProvider存儲方式。

  • SharedPreferences:一個輕量級數據存儲類,適用于保存軟件配置參數。使用SharedPreferences保存數據,最終使用xml格式文件存放數據,文件存放在/data/data/<package name>/shared_prefs目錄下。

  • SQLite:一個輕量級數據庫,支持基本SQL語法,是常被常用的一種數據存儲方式。Android為此數據庫提供了一個名為SQLiteDatabase的類,封裝了一些操作數據庫的API。

  • File:通用的文件(I/O)存儲方法,常用于存儲大數量的數據,缺點是更新數據比較麻煩。

  • ContentProvider:Android系統中所有應用實現數據共享的一種數據存儲方式,由于數據通常在各應用間的是相對私密的,為了數據安全,應用相互之間不能直接實現共享,特別是音頻,視頻,圖片或者通信錄,一般采用這種方式存儲。每個ContentProvider都會對外提供一個公共的URI(包裝為URI對象),如果應用程序有數據需要共享,就需要使用ContentProvider為這些數據定義一個URI,然后其他應用程序就可以通過ContentProvider傳入的這個URI來對數據進行操作。

SharedPreferences的使用

將數據存儲到SharedPreferences中:

a. 要想使用SharedPreferences存儲數據,首先需要獲取到SharedPreferences對象:

  • Context類中的getSharedPreferences方法:
/**
 * 參數一用于指定SharedPreferences文件的名稱
 * 參數二指定操作,MODE_APPEND為默認操作,其余操作已經在Android6之后廢棄
 */
SharedPreferences sharedPreferences = getSharedPreferences("data",MODE_APPEND);
  • Activity類的getPreferences方法:
//傳入一個默認的指定操作
//這里不需要指定文件名,因為它默認將當前活動的類名作為文件名
SharedPreferences sharedPreferences = getPreferences(MODE_APPEND);
  • PreferenceManager類中的getDefaultSharedPreferences方法:
//這是一個靜態方法,它接收一個Context參數,并自動使用當前應用程序的包名作為文件名
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);

建議:如果文件為整個應用程序所共用,則使用getSharedPreferences方法或getDefaultSharedPreferences方法,若文件只是針對某個活動使用,則可以使用getPreferences方法。

b. 獲取到SharedPreferences對象后,再通過下面三步實現:

//獲取Editor對象
SharedPreferences.Editor editor = sharedPreferences.edit();
//通過Editor對象添加數據
editor.putString("name","Tom");
editor.putInt("age",18);
editor.putBoolean("married",false);
//通過Editor提交數據
editor.apply();

讀取SharedPreferences中的數據

//獲取SharedPreferences對象
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
//通過get方法和key獲取到對應的value
String name = sharedPreferences.getString("name","");
int age = sharedPreferences.getInt("age",0);
boolean isMarried = sharedPreferences.getBoolean("married",false);
SharedPreferences的優化

SharedPreferences實際上是對一個XML文件存儲key-value鍵值對,每一次的commit和apply操作都是一次I/O寫操作。我們知道I/O操作時最慢的操作之一,在主線程中操作會導致主線程緩慢。SharedPreferences性能優化主要是兩個方面:

  • IO性能
  • 同步鎖問題

IO性能:SharedPreferences上的IO分別為讀取數據到內存和數據寫入磁盤

  • 當SharedPreferences文件還沒被加載到內存時,調用SharedPreferences方法會初始化文件并讀入內存,這容易導致耗時更長。
  • Editor的commit或apply方法每次執行時,寫入磁盤時會耗時。

建議:

  1. 在application初始化時,聲明SharedPreferences為全局變量并對其進行初始化操作。
  2. 對于提交數據時,若不需要返回結果,則使用apply方法,apply為異步寫入,commit為同步寫入,因此apply方法可以提高性能。

同步鎖:SharedPreferences類中的commitToMemory方法會鎖定SharedPreference對象,put和getEditor方法會鎖定Editor對象,在寫入磁盤時會鎖定一個寫入鎖。

建議:最好的辦法是避免頻繁讀寫SharedPreferences,減少無謂的調用。對于SharedPreferences的批量操作,最好先獲取一個Editor,進行批量操作,然后調用apply方法。

文件存儲

文件存儲是Android中最基本的一種數據存儲方式,它不對存儲的內容進行任何形式的格式化處理,所有數據都是原封不動地保存到文件中,因此它比較適合用于存儲一些簡單的文本數據或二進制數據。

將數據存儲到文件中

Context類提供了一個openFileOutput方法,可以用于將數據存儲到指定的文件中。該方法接收兩個參數,第一個參數為文件名,注意這里指定的文件是不包含路徑的,因為所有的文件都默認存儲到/data/data/<packagename>/files/目錄下。第二個參數為文件的操作模式:MODE_PRIVATE和MODE_APPEND。MODE_PRIVATE表示如果該文件已經存在,則所寫入的內容會覆蓋原文件的內容;MODE_APPEND表示如果該文件存在,則將內容追加到文件末尾。

public void save(String inputText){
    FileOutputStream fos = null;
    BufferedWriter writer = null;
    try{
        fos = openFileOutput("data",MODE_PRIVATE);
        writer = new BufferedWriter(new OutputStreamWriter(fos));
        writer.write(inputText);
    } catch (IOException e){
        e.printStackTrace();
    } finally {
        if (writer != null){
            try {
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

如上,我們通過BufferedWritter來寫入數據,但要注意的一點是在最后需要關閉寫入流。

讀取文件數據

Context類提供了一個openFileInput方法用來從文件中讀取數據。它只接收一個參數,即要讀取的文件名稱。然后系統會從默認路徑/data/data/<packagename>/files/下去查找,若查找成功則加載該文件。

public void load(){
        FileInputStream fis = null;
        BufferedReader reader = null;
        StringBuilder content = new StringBuilder();
        try{
            fis = openFileInput("data");
            reader = new BufferedReader(new InputStreamReader(fis));
            String line = "";
            while ((line = reader.readLine()) != null) 
                content.append(line);
        }catch (IOException e){
            e.printStackTrace();
        } finally {
            if (reader != null){
                try {
                    reader.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

如上,我們通過BufferedReader來實現讀取,同樣在最后需要關閉文件流。

存儲方式:內部存儲和外部存儲

內部存儲:

  • 總是可用的
  • 內部存儲空間的文件默認只有你的App可以訪問
  • 當用戶卸載了你的App,系統從內部存儲空間中移除所有你的App相關的文件。

建議:當你希望用戶和其他App都不能訪問你的文件時,內部存儲是最好的選擇

外部存儲

  • 并非總是可用的,比如外部存儲空間連接到PC或SD卡被移除時,所以最好在訪問它的時候檢查它的可用性。
  • 外部存儲是公用的,因此存儲在這里的文件可以被其他應用程序訪問。
  • 當用戶卸載你的App時,系統僅僅會移除存儲在通過getExternalFilesDir()獲取到的路徑中的該App相關的文件。
  • 使用前,需要在Manifest中獲取到其相關權限。

建議:當你的文件不需要訪問限制,或者你想把文件分享給其他的App,或者允許用戶通過電腦來訪問它,那么外部存儲是最好的選擇。

保存文件到內部存儲空間

File file = new File(getFilesDir(),"data");
File file1 = new File(getCacheDir(),"data");
FileOutputStream fos = openFileOutput("data",MODE_PRIVATE);

如上,有三種存儲方式:

  1. getFilesDir獲取到一個合適的目錄作為File的目錄。
  2. getCacheDir返回一個代表內部臨時緩存文件目錄的File對象。需要對該文件目錄設置一個合理的大小。如果系統存儲空間不足時,可能會在沒有警告的情況下刪除緩存的文件。

保存文件到外部存儲空間

  • 檢查是否可讀寫:
//Check if external storage is available for read and write
public boolean isExternalStorage(){
    return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
}

//Check if external storage is available for read
public boolean isExternalStorageReadable(){
    String state = Environment.getExternalStorageState();
    return Environment.MEDIA_MOUNTED.equals(state)
    || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state);
}
  • 獲取到外部存儲空間文件:
public File getPublicStorageFile(String fileName){
    File file = new File(Environment.getExternalStoragePublicDirectory(
        Environment.DIRECTORY_PICTURES), fileName);
    if (!file.mkdirs()){
        Log.e(LOG_TAG,"Directory not created");
    }
    return file;
}

public File getPrivateStorageFile(String fileName){
    File file = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES),fileName);
    if (!file.mkdirs()){
        Log.e(LOG_TAG,"Directory not created");
    }
    return file;
}

如上,getExternalStoragePublicDirectory方法獲取一個代表外部存儲合適目錄的File對象,用于保存公有文件。而getExternalFilesDir所創建的文件目錄將被添加到封裝了該App所有外部存儲文件的目錄下,并且會在用戶卸載App時被系統刪除。

SQLite數據庫存儲

SQLite數據庫是一款輕量級的關系型數據庫,它的運算速度非???,占用資源少,通常只需要幾百kb即可,因而特別適用于移動設備中。

SQLite的使用
  • 創建數據庫:通過在onCreate方法中執行數據庫創建操作
public class MyDatabaseHelper extends SQLiteOpenHelper {
    public static final String CREATE_BOOK = "create table Book(" + "id integer primary key autoincrement,"
            + "author text," + "price real," + "pages integer," + "name text)";
    private Context mContext;
    
    public MyDatabaseHelper(Context context, String name, 
                            SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

使用:
public class MainActivity extends AppCompatActivity {
    private MyDatabaseHelper dbHelper;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,1);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dbHelper.getWritableDatabase();
            }
        });
    }
}
  • 升級數據庫:通過增加版本號,來調用onUpgrade方法來實現升級。

a. 實現為表Book添加一個出版社字段:

@Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        if (oldVersion < 2){
            db.execSQL("alter table Book add column press text");
        }
    }

b. 實現增加表Category來記錄圖書的分類:

public static final String CREATE_CATEGORY = "create table Category(" 
            + "id integer primary key autoincrement," + "category_name text," 
            + "category_code integer)";
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
     if (oldVersion < 2){
         db.execSQL(CREATE_CATEGORY);
     }
}

c. 實現刪除表中的某個字段:例如刪除表Book中的pages字段(通過建立一個臨時表來傳遞數據,以保留原來的數據)

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    if (oldVersion < 2){
        db.beginTransaction();
        try{
            db.execSQL("create temporary table tmp_book(id,author,price,name);");
            db.execSQL("insert into tmp_book select id,author,price,name from Book;");
            db.execSQL("drop table Book;");
            db.execSQL("create table Book(id integer primary key autoincrement,author text," +
                "price real, name text);");
            db.execSQL("insert into Book select * from tmp_book;");
            db.execSQL("drop table tmp_book;");
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }
}

d. 調用方法:升級版本號即可。

dbHelper = new MyDatabaseHelper(this,"BookStore.db",null,2);
  • 插入數據:使用ContentValues+insert方法或使用execSQL方法
SQLiteDatabase db = dbHelper.getWritableDatabase();
try{
    db.beginTransaction();
    ContentValues values = new ContentValues();
    values.put("name","The Da Vinci Code");
    values.put("author","Dan Brown");
    values.put("pages",454);
    values.put("prices",16.96);
    db.insert("Book",null,values);
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

try{
    db.beginTransaction();
    db.execSQL("insert into Book(name,author,pages,prices) values(" + "'The Da Vinci Code',"
        + "'Dan Brown',454,16.96);");
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
  • 更新數據:例如我們將價格修改為11.99
SQLiteDatabase db = dbHelper.getWritableDatabase();
try{
    db.beginTransaction();
    ContentValues values = new ContentValues();
    values.put("prices",11.99);
    //參數一為表名
    //參數二為更新的數據
    //參數三為判斷的條件
    //參數四為所需加入的條件
    db.update("Book",values,"name = ?",new String[]{"The Da Vinci Code"});
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

try{
    db.beginTransaction();
    db.execSQL("update Book set prices = 11.99 where name = 'The Da Vinci Code';");
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
  • 刪除數據:
SQLiteDatabase db = dbHelper.getWritableDatabase();
try{
    db.beginTransaction();
    db.delete("Book","pages > ?",new String[]{"500"});
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}

try{
    db.beginTransaction();
    db.execSQL("delete from Book where pages > 500;");
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
  • 查詢數據:
SQLiteDatabase db = dbHelper.getWritableDatabase();
Cursor cursor = null;
try{
    db.beginTransaction();
    //參數一:table(from table_name) 指定查詢的表名
    //參數二:column (select from column1,column2) 指定查詢的列名
    //參數三:selection (where column = value) 指定where的約束條件
    //參數四:selectionArgs 為where中的占位符提供具體的值
    //參數五:groupBy (group by column) 指定需要group by的列
    //參數六:having (having column = value) 對gourp by 后的結果進一步約束
    //參數七:orderBy (order by column1,column2) 指定查詢結果的排序方式
    cursor = db.query("Book",new String[]{"name","prices"},"pages > ?",
        new String[]{"500"},"author","'Dan Brown'","id");
    if (cursor.moveToFirst()){
        do {
            String name = cursor.getString(cursor.getColumnIndex("name"));
            String prices = cursor.getString(cursor.getColumnIndex("prices"));
            Log.d("tag",name);
            Log.d("tag",prices);
        }while (cursor.moveToNext());
    }
    db.setTransactionSuccessful();
} finally {
    if (cursor != null) {
        cursor.close();
    }
    db.endTransaction();
}

try{
    db.beginTransaction();
    db.execSQL("select name,prices from Book where pages > 500 group by author having author = 'Dan Brown' orderBy id;");
    db.setTransactionSuccessful();
} finally {
    db.endTransaction();
}
數據庫的優化
  • 我們通過一個簡單聯系人管理模塊來實現數據庫的優化,如下為各個類的功能:

ContactInfo類:描述聯系人的信息

public class ContactInfo {
    private long contactId = -1;
    private String contactName, contactNum;

    public ContactInfo(long contactId, String contactName, String contactNum) {
        this.contactId = contactId;
        this.contactName = contactName;
        this.contactNum = contactNum;
    }
}

BaseTable類:所有表的基類

public abstract class BaseTable {
    private Context mContext;

    public BaseTable(Context context){
        mContext = context;
    }

    public Context getContext(){
        return mContext;
    }

    public void setContext(Context context){
        mContext = context;
    }

    public ContentResolver getContentResolver(){
        return (mContext == null) ? null : mContext.getContentResolver();
    }

    public SQLiteDatabase getSqliteDB(){
        return DBManager.getWriteDB(mContext.getApplicationContext());
    }

    public SQLiteDatabase getSqliteReadDB(){
        return DBManager.getReadDB(mContext.getApplicationContext());
    }

    public abstract SQLiteStatement getSQLiteStatement();

    public abstract String getTableName();

    public abstract String[] getAllKey();

    public int getTotalCount(){
        Cursor cursor = null;
        try{
            SQLiteDatabase sqLiteDb = getSqliteDB();
            if (sqLiteDb != null){
                cursor = sqLiteDb.rawQuery("select count(*) from " + getTableName(),null);
                if (cursor != null && cursor.moveToFirst()){
                    return cursor.getInt(0);
                }
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (cursor != null){
                cursor.close();
            }
        }
        return -1;
    }

    protected static String kv(String key,String value){
        return key + "=" + value;
    }

    protected static String kv(String key, long value){
        return key + "=" + value;
    }

    protected static String kv(String key, int value){
        return key + "=" + value;
    }

}

DBConfig接口:描述表字段

public interface DBConfig {
    public static final String DATABASE_FILE = "data";
    public static final int DB_VER = 1;
}

DBManager:數據庫管理類,提供應用唯一的數據庫SQLiteOpenHelper實例。

public class DBManager  implements DBConfig{
    private static final String TAG = "DBManager";
    private static SQLiteDatabase mDB = null;
    private static DataBaseHelper mDatabaseHelper = null;
    public static void close(){
        if (mDB != null){
            mDB.close();
        }
        mDB = null;
    }

    private static DataBaseHelper getDatabaseHelper(Context AppContext){
        if (mDatabaseHelper == null){
            mDatabaseHelper = new DataBaseHelper(AppContext);
        }
        return mDatabaseHelper;
    }

    public static synchronized void InitDB(Context AppContext){
        getWriteDB(AppContext);
        getReadDB(AppContext);
    }

    public static synchronized SQLiteDatabase getWriteDB(Context AppContext){
        if (mDB == null || !mDB.isOpen()){
            mDB = getDatabaseHelper(AppContext).getWritableDatabase();
        }
        return mDB;
    }

    public static synchronized SQLiteDatabase getReadDB(Context AppContext){
        if (mDB == null || !mDB.isOpen()){
            mDB = getDatabaseHelper(AppContext).getReadableDatabase();
        }
        return mDB;
    }

    private static class DataBaseHelper extends SQLiteOpenHelper{
        private Context mContext;

        public DataBaseHelper(Context context) {
            super(context, DATABASE_FILE, null, DB_VER);
            mContext = context;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            createTable(db);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        }

        private void createTable(SQLiteDatabase db){
            db.execSQL("DROP TABLE IF EXISTS " + ContactInfoTable.CONTACT_INFO_TABLE);
            db.execSQL(ContactInfoTable.TABLE_CREATE);
        }
    }
}

ContactManager類:具體業務,為一個單例,實現相關操作的接口方法。

public class ContactInfoTable extends BaseTable {
    private static String TAG = "ContactInfoTable";
    public static final String CONTACT_INFO_TABLE = "ContactInfo_table";
    private static final String KEY_CONTACT_ID = "user_num";
    private static final String KEY_CONTACT_NAME = "user_name";
    private static final String KEY_USER_PHONE_HOME = "user_phonenumber";
    private SQLiteStatement mSQLiteStatement = null;

    public static final String TABLE_CREATE = "create table if not exists " + CONTACT_INFO_TABLE
            + "(" + KEY_CONTACT_ID + " long primary key, " + KEY_CONTACT_NAME + " text, "
            + KEY_USER_PHONE_HOME + " long);";
    public final String STR_INSERT_STATEMENT_CONTACTS = "insert into " + CONTACT_INFO_TABLE
            + "(" + KEY_CONTACT_ID + "," + KEY_CONTACT_NAME + "," + KEY_USER_PHONE_HOME + ")values(?,?,?)";


    public ContactInfoTable(Context context) {
        super(context);
    }

    @Override
    public SQLiteStatement getSQLiteStatement() {
        if (mSQLiteStatement == null){
            mSQLiteStatement = getSqliteDB().compileStatement(STR_INSERT_STATEMENT_CONTACTS);
        }
        return mSQLiteStatement;
    }

    @Override
    public String getTableName() {
        return CONTACT_INFO_TABLE;
    }

    @Override
    public String[] getAllKey() {
        return new String[]{CONTACT_INFO_TABLE + "." + KEY_CONTACT_ID,
            CONTACT_INFO_TABLE + "." + KEY_CONTACT_NAME, CONTACT_INFO_TABLE + "." + KEY_USER_PHONE_HOME};
    }

    public boolean insertContactInfo(ContactInfo info){
        return getSqliteDB().insert(CONTACT_INFO_TABLE,null,transContactInfo(info)) > 0;
    }

    public boolean insertContactInfoForStat(ContactInfo info){
        getSQLiteStatement().clearBindings();
        getSQLiteStatement().bindLong(1,info.getContactId());
        getSQLiteStatement().bindString(2,info.getContactName());
        getSQLiteStatement().bindString(3,info.getContactNum());
        return getSQLiteStatement().executeInsert() > 0;
    }

    public boolean updateContactInfo(ContactInfo info){
        return getSqliteDB().update(CONTACT_INFO_TABLE,transContactInfo(info),kv(KEY_CONTACT_ID,info.getContactId()),null) > 0;
    }

    public boolean deleteContactInfo(ContactInfo info){
        return getSqliteDB().delete(CONTACT_INFO_TABLE,kv(KEY_CONTACT_ID,info.getContactId()),null) > 0;
    }

    private ContentValues transContactInfo(ContactInfo info){
        ContentValues value = new ContentValues();
        value.put(KEY_CONTACT_ID,info.getContactId());
        value.put(KEY_CONTACT_NAME,info.getContactName());
        value.put(KEY_USER_PHONE_HOME,info.getContactNum());
        return value;
    }
}

建議:

  • 使用Application的Context創建數據庫,在Application生命周期結束時再關閉。
  • 在應用啟動過程中最先初始化數據庫,避免進入應用后再初始化導致相關操作時間變長。

因此,在結束應用時,記得關閉數據庫,并且不能使用某個Activity的Context,否則會導致這個Activity的資源都不會釋放,出現內存泄漏。

接下來,我們進行測試插入10000條數據所消耗的時間:

long start = System.currentTimeMillis();
Log.d("tag","start...");
for (int i = 1; i <= 10000; i ++){
    ContactInfo info = new ContactInfo(i, "姓名" + i, "1389832" + (0000+i));
    boolean insert = mContactInfoTable.insertContactInfo(info);
}
long end = System.currentTimeMillis();
Log.d("tag","it takes " + (end - start) + "ms");

這樣的運行時間大概要花44s左右,接下來我們進行優化。

  • 使用SQLiteStatement:Android系統提供的SQLiteStatement類來將數據插入數據庫,在性能上有一定的提供,并且也解決了SQL注入的問題。
public final String STR_INSERT_STATEMENT_CONTACTS = "insert into " + CONTACT_INFO_TABLE
                + "(" + KEY_CONTACT_ID + "," + KEY_CONTACT_NAME + "," + KEY_USER_PHONE_HOME + ")values(?,?,?)";

@Override
public SQLiteStatement getSQLiteStatement() {
    if (mSQLiteStatement == null){
        mSQLiteStatement = getSqliteDB().compileStatement(STR_INSERT_STATEMENT_CONTACTS);
    }
    return mSQLiteStatement;
}  

public boolean insertContactInfoForStat(ContactInfo info){
    getSQLiteStatement().clearBindings();
    getSQLiteStatement().bindLong(1,info.getContactId());
    getSQLiteStatement().bindString(2,info.getContactName());
    getSQLiteStatement().bindString(3,info.getContactNum());
    return getSQLiteStatement().executeInsert() > 0;
}

重新運行代碼,發現其實性能并沒有提升多少,時間降到了39s左右。

  • 使用事務:若沒有顯示創建任何事務,每執行一次插入操作,系統都會自動創建一個事務,在插入后立即提交,這樣會出現一個問題,如果插入非常頻繁,就會頻繁創建事務,影響插入的效率。

事務有兩個基本特性:原子提交和性能更好。
事務的使用方法如下:

beginTransaction():開啟一個事務
setTransactionSuccessful():設置事務標志成功
endTransaction():檢查事務的標志是否成功,若成功則所有操作都會被提交,否則回滾事務

具體使用如下:

mContactInfoTable.getSqliteDB().beginTransaction();
long start = System.currentTimeMillis();
Log.d("tag","start...");
try{
    for (int i = 1; i <= 10000; i ++){
        ContactInfo info = new ContactInfo(i, "姓名" + i, "1389832" + (0000+i));
        boolean insert = mContactInfoTable.insertContactInfoForStat(info);
                        //Log.d("tag","insert Contact info : " + insert);
    }
    mContactInfoTable.getSqliteDB().setTransactionSuccessful();
} catch (Exception e){
    e.printStackTrace();
} finally {
    mContactInfoTable.getSqliteDB().endTransaction();
}
long end = System.currentTimeMillis();
Log.d("tag","it takes " + (end - start) + "ms");
Log.d("tag","end...");

使用事務提交,插入10000條數據只需要1秒多,可見事務對性能的極大作用。

  • 使用索引:索引維護一個表中或表中某一列或某幾列的順序,這樣就可以快速定位到一組值,而不用掃遍全表。

索引雖然可以提高查詢的速度,但也有兩個明顯的缺點:

  1. 數據庫的插入、更新和刪除使用索引反而更慢,因為刪除、更新字典中的一個字,也要刪除這個字在拼音索引和部首索引中的信息。
  2. 建立索引會增加數據庫的大小。

雖然索引的目的是提高數據庫的性能,但在以下場合不建議使用索引:

  1. 在較小的表中
  2. 在有頻繁的大批量更新或插入操作的表上
  3. 在含有NULL值的列上
  4. 在頻繁操作的列上
  • 異步線程,寫數據庫統一管理:雖然對數據庫優化后性能提高了很多,但相對來說還是一個耗時的操作,因此有必要放到異步系統中操作。同時為了保證數據的同步和避免一些死鎖等待情況,可以考慮雙緩存機制(把一些常用的數據放到內存緩存中,再異步更新到數據庫中)。

內容提供器

如果一個應用通過內容提供器對數據提供了外部訪問的接口,那么任何其他的應用程序就都可以對這部分數據進行訪問。Android系統中自帶的電話簿、短信、媒體庫等程序都提供了類似的訪問接口,這就使得第三方程序可以充分地利用這部分數據來實現更好的功能。

  • ContentResolver的使用:對于每個應用程序來說,如果想要訪問內容提供器中共享的數據,就要借助ContentResolver類,通過Context中的getContextResolver方法來獲取到該類的實例。

內容URI

內容URI給內容提供器中的數據建立了一個唯一的標識符,它主要由兩部分組成:authority和path。authority是對不同的應用程序做區分的,一般采用包名形式;path則是用于對同一應用程序中不同的表做區分的。

比如某個程序的包名為com.example.app,程序內含有表table1和table2,那么其內容URI如下:

content://com.example.app.provider/table1
content://com.example.app.provider/table2

CRUD

  • 獲取到內容URI后,我們還需要將其解析成Uri對象作為參數傳入:
Uri uri = Uri.parse("content://com.example.app.provider/table1");
  • 獲取到Uri對象后,就可以對其進行CRUD操作:
//projection:指定查詢某個應用程序下的一張表
//selection:指定where的約束條件
//selectionArgs:為where中的占位符提供具體的值
//orderBy:指定查詢結果的排序方式
Cursor cursor = getContentResolver().query(uri, projection,selection,selectionArgs,sortOrder);

//插入操作:
ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);

//更新操作:
ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ?",new String[]{"text",1});

//刪除操作:
getContentResolver().delete(uri,"column1 = ?", new String[]{"text"});

如下為讀取聯系人列表的例子:

public class MainActivity extends AppCompatActivity {
    ArrayAdapter<String> adapter;
    List<String> contactList = new ArrayList<>();
    Button button;
    ListView listView;
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        listView = (ListView) findViewById(R.id.list);
        button = (Button) findViewById(R.id.button);
        adapter = new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,contactList);
        listView.setAdapter(adapter);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},1);
        }

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                readContacts();
            }
        });

    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    readContacts();
                }else {
                    Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
    private void readContacts(){
        Cursor cursor = null;
        try{
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null,null,null,null,null);
            if (cursor != null){
                while (cursor.moveToNext()){
                    String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                    contactList.add(displayName + "\n" + number);
                }
                adapter.notifyDataSetChanged();
            }
        } catch (Exception e){
            e.printStackTrace();
        } finally {
            if (cursor != null){
                cursor.close();
            }
        }
    }

}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,796評論 18 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,674評論 25 708
  • Android中存儲方式有很多種,下面我給分享自己的學習總結,有錯誤希望大家指出,讓我們一起成長: 1、使用Sha...
    夢想追求者閱讀 1,221評論 0 1
  • 昨天在微信聊天的時候突然得知老友要辭職,深交多年的朋友突然蹦出這么一句話,一項不正經的我都不知道怎么接話了。...
    韌針小郭閱讀 121評論 0 0
  • ??一次,跟孩子交流,說起了“石碾”。 她很好奇,“這到底是個什么樣東西?” 我的記憶一下子回到了從前,跟“石碾”...
    做一個有態度的人閱讀 550評論 0 1