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阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,097评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,563评论 25 707
  • Android中存储方式有很多种,下面我给分享自己的学习总结,有错误希望大家指出,让我们一起成长: 1、使用Sha...
    梦想追求者阅读 1,198评论 0 1
  • 昨天在微信聊天的时候突然得知老友要辞职,深交多年的朋友突然蹦出这么一句话,一项不正经的我都不知道怎么接话了。...
    韧针小郭阅读 101评论 0 0
  • ��一次,跟孩子交流,说起了“石碾”。 她很好奇,“这到底是个什么样东西?” 我的记忆一下子回到了从前,跟“石碾”...
    做一个有态度的人阅读 518评论 0 1