tips-android-sqlite

1
2
3
4
5
6
7
8
9
10
11
08-30 20:27:36.751 E/CursorWindow(  760): Could not allocate CursorWindow '/data/data/com.android.providers.media/databases/external.db' of size 2097152 due to error -12.
08-30 20:27:36.771 E/JavaBinder( 760): *** Uncaught remote exception! (Exceptions are not yet supported across processes.)
08-30 20:27:36.771 E/JavaBinder( 760): android.database.CursorWindowAllocationException: Cursor window allocation of 2048 kb failed. # Open Cursors=781 (# cursors opened by pid 3105=781)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.CursorWindow.<init>(CursorWindow.java:104)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.AbstractWindowedCursor.clearOrCreateWindow(AbstractWindowedCursor.java:198)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.sqlite.SQLiteCursor.fillWindow(SQLiteCursor.java:162)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.sqlite.SQLiteCursor.getCount(SQLiteCursor.java:156)
08-30 20:27:36.771 E/JavaBinder( 760): at android.database.CursorToBulkCursorAdaptor.count(CursorToBulkCursorAdaptor.java:184)
08-30 20:27:36.771 E/JavaBinder( 760): at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:117)
08-30 20:27:36.771 E/JavaBinder( 760): at android.os.Binder.execTransact(Binder.java:338)
08-30 20:27:36.771 E/JavaBinder( 760): at dalvik.system.NativeStart.run(Native Method)

错误原因 CursorWindow缓存数据达到最大限制(2M不同的机器和SQLite版本其值可能不同)后,仍有查询结果集需要缓存,在申请内存分配时申请失败发生了OOM内存溢出;SQLite查询出的数据集cursor,都由native层的CursorWindow进行数据管理,包括内存空间的申请和数据的填充。CursorWindow实际上是共享内存的抽象,以实现跨进程,跨应用数据共享(ContentProvider作为数据通道,也支持跨进程,跨应用的数据访问) 在ContentProvider端透过SQLiteDatabase的封装查询到的数据集保存在CursorWindow所指向的共享内存中,然后通过Binder把这片共享内存传递到ContentResolver端,即查询端。这样客户就可以通过Cursor来访问这块共享内存中的数据集了。 解决办法 保证CursorWindow不会达到最大限制):

  1. 只查询需要的字段;根据UI显示需要,或实际需要查询的字段进行查询,尽量不会表查询
  2. 二进制文件不要存在数据库中;数据库仅适用于保存一些较短文字,整数,布尔,浮点数等一些,易于查询和操作的轻量级的数据,目的也是在于快速搜索和查询。对于像图片,较长的文字(如文章)等大数据,最好直接以文件形式存储在硬盘中,然后在数据库保存它们的访问路径
  3. 对于大数据量的查询采用分段查询方式;无论表中的一条记录数据量如何的小,当条数达到5000级或者万级或者更多的时候,还是会达到最大的限制
  4. 正确的关闭Cursor,释放CursorWindow中不用的资源(需手动调用释放native中的资源,类似3.0之前的Bitmap需要手动释放。调用close的必要性:

从源码看ANDROID中SQLITE是怎么通过CURSORWINDOW读DB的

Sqlite性能优化

(1)编译SQL语句 Sqlite想要执行操作,需要将程序中的sql语句编译成对应的SQLiteStatement,比如select * from record这一句,被执行100次就需要编译100次。对于批量处理插入或者更新的操作,我们可以使用显示编译来做到重用SQLiteStatement。

想要做到重用SQLiteStatement也比较简单,基本如下:

编译sql语句获得SQLiteStatement对象,参数使用?代替 在循环中对SQLiteStatement对象进行具体数据绑定,bind方法中的index从1开始,不是0

如下向person表中插入100条数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
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次。

具体的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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; 1 创建单列索引

CREATE INDEX index_name ON table_name; 1 c.索引的利弊 毋庸置疑,索引加速了我们检索数据表的速度。然而正如西方谚语 “There are two sides of a coin”,索引亦有缺点:

对于增加,更新和删除来说,使用了索引会变慢,比如你想要删除字

列表内容典中的一个字,那么你同时也需要删除这个字在拼音索引和部首索引中的信息。 建立索引会增加数据库的大小,比如字典中的拼音索引和部首索引实际上是会增加字典的页数,让字典变厚的。 为数据量比较小的表建立索引,往往会事倍功半。 所以使用索引需要考虑实际情况进行利弊权衡,对于查询操作量级较大,业务对要求查询要求较高的,还是推荐使用索引的。

(4)查询数据优化 按需获取列信息

db.query(TableDefine.TABLE_RECORD, null, null, null, null, null, null) ; 1 其中上面方法的第二个参数类型为String[],意思是返回结果参考的colum信息,传递null表明需要获取全部的column数据。如果我们不需要所有列的信息,最好指定一下需要的列。

提前获取索引 例如下面的代码,我们可以把获取列索引的代码cursor.getColumnIndex(TableDefine.COLUMN_INSERT_TIME)放到循环外,这样不需要每次获取。

1
2
3
4
5
6
 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,因此建议将这些耗时操作放入异步线程中处理

Android 性能优化之数据库优化(一)

坚持原创技术分享,您的支持将鼓励我继续创作!