1.写在前面的话
前面写过一篇关于Sqlite基本操作的文章,今天我们来学习Android中如何使用Sqlite以及性能优化。
2.Android平台下数据库相关类
SQLiteOpenHelper 抽象类:通过从此类继承实现用户类,来提供数据库打开、关闭等操作函数。
SQLiteDatabase 数据库访问类:执行对数据库的插入记录、查询记录等操作。
SQLiteCursor 查询结构操作类:用来访问查询结果中的记录。
3.Android Sqlite的使用
(1)创建数据库
Android下要使用Sqlite首先要写一个SQLiteOpenHelper的实现类,该类的构造函数如下:
private MyDBHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
super(context, name, factory, version);
}
需要传入的参数解释如下:
name:数据库的名称,用这个名称来打开创建或打开相应的数据库。
factory:用来创建cursor,通常情况下我们传入null,使用默认的就行。
version:数据库的版本,从1开始,可以修改版本号,来除法数据库的更新操作。
对于SQLiteOpenHelper我们般会设计成单例。
private static MyDBHelper myDBHelper;
public static synchronized MyDBHelper getInstance(Context context) {
if (myDBHelper == null) {
Context applicationContext = context.getApplicationContext();
myDBHelper = new MyDBHelper(applicationContext);
}
return myDBHelper;
}
private MyDBHelper(Context context) {
this(context, DB_NAME, null, VERSION);
}
当我们首次使用MyDBHelper来获取数据库时,即调用getWritableDatabase或getReadableDatabase方法时,变会触发DBHelper中的onCreate方法,这时我们可以在数据库中创建表:
@Override
public void onCreate(SQLiteDatabase db) {
StringBuilder sql = new StringBuilder();
sql.append("create table ");
sql.append(TAB_PERSON + "(");
sql.append("id integer,");
sql.append("name char(8),");
sql.append("age int");
sql.append(");");
db.execSQL(sql.toString());
}
以上代码实际执行了一个sql语句create table person(id integer,name char(10), age int);创建了一张person表。
(2)更新数据库
通过修改数据库的版本我们可以触发数据库的更新。这里我们修改数据库的version为2,并在更新时添加一个新的developer表。和创建一样,onUpgrade会在调用getWritableDatabase或getReadableDatabase时触发。
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
StringBuilder sql = new StringBuilder();
sql.append("create table ");
sql.append(TAB_DEVLOPER + "(");
sql.append("id integer,");
sql.append("position char(20),");
sql.append(");");
db.execSQL(sql.toString());
}
(3)增删查改
对于增删查改,我们可以分别调用数据库的insert、delete、query、update方法传入参数来进行操作,当然也可以直接用execSQL方法来执行Sql来进行操作。比如我们要在在person表中插入一条记录,我们可以在MyDBHelper中创建一个inser方法,如下:
public boolean insert(int id, String name, int age) {
ContentValues values = new ContentValues();
values.put("name", name);
values.put("age", age);
long insert = getWritableDatabase().insert(TAB_PERSON, null, values);
return insert >= 1;
}
4.Sqlite性能优化
(1)编译SQL语句
Sqlite想要执行操作,需要将程序中的sql语句编译成对应的SQLiteStatement,比如select * from record这一句,被执行100次就需要编译100次。对于批量处理插入或者更新的操作,我们可以使用显示编译来做到重用SQLiteStatement。
想要做到重用SQLiteStatement也比较简单,基本如下:
编译sql语句获得SQLiteStatement对象,参数使用?代替
在循环中对SQLiteStatement对象进行具体数据绑定,bind方法中的index从1开始,不是0
如下向person表中插入100条数据:
public void insertBatchPreCompile() {
long start = SystemClock.currentThreadTimeMillis();
String sql = "insert into " + TAB_PERSON + " values (?,'test','1');";
SQLiteStatement sqLiteStatement = getReadableDatabase().compileStatement(sql);
int count = 0;
while (count < 100) {
count++;
sqLiteStatement.clearBindings();
sqLiteStatement.bindLong(1, count);
sqLiteStatement.executeInsert();
}
Log.e(TAG, "insert recompile use time " + (SystemClock.currentThreadTimeMillis() - start));
}
(2)显示使用事务
在Android中,无论是使用SQLiteDatabase的insert,delete等方法还是execSQL都开启了事务,来确保每一次操作都具有原子性,使得结果要么是操作之后的正确结果,要么是操作之前的结果。
然而事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。既然属于文件,就符合Unix的文件范型(Open-Read/Write- Close),因而对于批量的修改操作会出现反复打开文件读写再关闭的操作。然而好在,我们可以显式使用事务,将批量的数据库更新带来的journal文件打开关闭降低到1次。
具体的实现代码如下:
public void insertWithTransaction() {
long start = SystemClock.currentThreadTimeMillis();
int count = 0;
try {
getWritableDatabase().beginTransaction();
while (count++ < 100) {
insert(count, "test", 1);
}
getWritableDatabase().setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
getWritableDatabase().endTransaction();
}
Log.e(TAG, "insert traceaction use time " + (SystemClock.currentThreadTimeMillis() - start));
}
使用这两种方式分别优化,对比效果如下:
这里写图片描述
从图中可以看到在插入100条的情况下,使用预编译的方式可以稍微提升性能,但是使用事务,能够使性能提升大概8倍,所以可以看出频繁的IO操作还是比较耗时的。同时使用两种方式进行优化,可以提升17倍,优化效果非常明显。
(3)建立索引
a.索引的概念
索引,使用索引可快速访问数据库表中的特定信息。索引是对数据库表中一列或多列的值进行排序的一种结构。
在关系数据库中,索引是一种与表有关的数据库结构,它可以使对应于表的SQL语句执行得更快。索引的作用相当于图书的目录,可以根据目录中的页码快速找到所需的内容。当表中有大量记录时,若要对表进行查询,第一种搜索信息方式是全表搜索,是将所有记录一一取出,和查询条件进行一一对比,然后返回满足条件的记录,这样做会消耗大量数据库系统时间,并造成大量磁盘I/O操作;第二种就是在表中建立索引,然后在索引中找到符合查询条件的索引值,最后通过保存在索引中的ROWID(相当于页码)快速找到表中对应的记录。
索引是一个单独的、物理的数据库结构,它是某个表中一列或若干列值的集合和相应的指向表中物理标识这些值的数据页的逻辑指针清单。
索引提供指向存储在表的指定列中的数据值的指针,然后根据您指定的排序顺序对这些指针排序。数据库使用索引的方式与您使用书籍中的索引的方式很相似:它搜索索引以找到特定值,然后顺指针找到包含该值的行。
b.建立索引
创建索引的基本语法:
CREATE INDEX index_name ON table_name;
创建单列索引
CREATE INDEX index_name ON table_name;
c.索引的利弊
毋庸置疑,索引加速了我们检索数据表的速度。然而正如西方谚语 “There are two sides of a coin”,索引亦有缺点:
对于增加,更新和删除来说,使用了索引会变慢,比如你想要删除字
- 列表内容典中的一个字,那么你同时也需要删除这个字在拼音索引和部首索引中的信息。
- 建立索引会增加数据库的大小,比如字典中的拼音索引和部首索引实际上是会增加字典的页数,让字典变厚的。
- 为数据量比较小的表建立索引,往往会事倍功半。
所以使用索引需要考虑实际情况进行利弊权衡,对于查询操作量级较大,业务对要求查询要求较高的,还是推荐使用索引的。
(4)查询数据优化
按需获取列信息
db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ;
其中上面方法的第二个参数类型为String[],意思是返回结果参考的colum信息,传递null表明需要获取全部的column数据。如果我们不需要所有列的信息,最好指定一下需要的列。
提前获取索引
例如下面的代码,我们可以把获取列索引的代码cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME)放到循环外,这样不需要每次获取。
private void badQueryWithLoop(SQLiteDatabase db) {
Cursor cursor = db.query(TableDefine.TABLE_RECORD, new String[]{TableDefine.COLUMN_INSERT_TIME}, null, null, null, null, null) ;
while (cursor.moveToNext()) {
long insertTime = cursor.getLong(cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME));
}
}
(5)ContentValues的容量调整
SQLiteDatabase提供了方便的ContentValues简化了我们处理列名与值的映射,ContentValues内部采用了 HashMap来存储Key-Value数据,ContentValues的初始容量是8,如果当添加的数据超过8之前,则会进行双倍扩容操作,因此建议对ContentValues填入的内容进行估量,设置合理的初始化容量,减少不必要的内部扩容操作。
(6)及时关闭Cursor
(7)耗时异步化
数据库的操作,属于本地IO,通常比较耗时,如果处理不好,很容易导致ANR,因此建议将这些耗时操作放入异步线程中处理。
本文Demo下载地址SqliteDemo
网友评论