精通安卓性能优化-第一章(九)
2014-05-27 10:30
302 查看
SQLite
很多应用程序不是SQLite的重度使用者,因此非常幸运的不需要去担心太多处理数据库的性能问题。然而,如果需要去优化Android应用中SQLite相关的代码的情况下需要知道几个概念:(1) SQLite状态
(2) 事务(Transaction)
(3) 查询
NOTE:本节不打算成为SQLite的完整指南,而是提供给你几个点去保证有效的使用数据库。完整的指南,参考www.sqlite.org和Android在线文档。
本节所涉及到的优化不会使得代码更加难以阅读和维护,所以需要养成使用它们的习惯。
SQLite Statements
开始的时候,SQL 语句是简单的字符串,比如:(1) CRTEATE TABLE cheese (name TEXT, origin TEXT)
(2) INSERT INTO cheese VALUES(‘Roquefort’, ‘Roquefort-sur-Soulzon’)
第一条语句创建一个名字为cheese,包含两列名字分别为name和origin的表。第二行将在表中插入新的一行。因为它们是简单的字符串,语句在执行前需要被解释或者编译。当你调用比如SQLiteDatabase.execSQL的时候,这些语句的编译在内部执行,如Listing 1-18所示。
Listing 1-18 执行简单的SQLite语句
SQLiteDatabase db = SQLiteDatabase.create(null); // memory-backed database db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)"); db.execSQL("INSERT INTO cheese VALUES ('Roquefort', 'Roquefort-sur-Soulzon')"); db.close(); // 记得关闭数据库
NOTE:许多SQLite相关的方法会抛异常。
事实证明,执行SQLite语句需要相当一段时间。除了编译,语句本身可能需要被创建。因为String是不可变的,这可能导致和在computeRecursivelyFasterUsingBigInteger()使用大数字BigInteger同样的性能问题。我们现在聚焦于插入语句的性能问题,毕竟,一个表可能仅被创建一次,但是许多行可能会添加、修改或者删除。
如果我们要建立cheeses的一个全面的数据库,我们可能会以许多插入语句结束,如Listing 1-19所示。对每一个插入语句,一个String将被创建,另外execSQL将被调用,每个cheese添加到数据库的时候,都会在内部解析SQL语句。
Listing 1-19 创建一个全面的Cheeses数据库
public class Cheeses { private static final String[] sCheeseNames = { "Abbaye de Belloc", "Abbaye du Mont des Cats", ... "Vieus Boulogne" }; private static final String[] sCheeseOrigins = { "Notre-Dame de Belloc", "Mont des Cats", ... "Boulogne-sur-Mer" }; private final SQLiteDatabase db; public Cheeses() { db = SQLiteDatabase.create(null); // memory-backed database db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)"); } public void populateWithStringPlus() { int i = 0; for(String name:sCheeseNames) { String origin = sCheeseOrigins[i++]; String sql = "INSERT INTO cheese VALUES(\"" + name + "\",\"" + origin + "\")"; db.execSQL(sql); } } }
在Galaxy Tab 10.1,添加650个cheese到memory-backed的数据库需要393毫秒或者每行需要0.6毫秒。
一个明显的改进是使得sql语句的创建更快。在这种情况下,通过+操作符去连接字符串不是最有效的方法,通过StringBuilder或者调用String.format去改善性能是可能的。这两个新的方法展示在Listing 1-20中。它们只是优化了传给execSQL的字符串的创建方式,这两个优化不是SQL相关的。
Listing 1-20 创建SQL语句字符串更快的方式
public void populateWithStringFormat() { int i=0; for (String name : sCheeseNames) { String origin = sCheeseOrigins[i++]; String sql = String.format("INSERT INTO cheese VALUES(\"%s\", \"%s\")", name, origin); db.execSQL(sql); } } public void populateWithStringBuilder() { StringBuilder builder = new StringBuilder(); builder.append("INSERT INTO cheese VALUES(\""); int resetLength = builder.length(); int i=0; for(String name : sCheeseNames) { String origin = sCheeseOrigins[i++]; builder.setLength(resetLength); // 重设位置 builder.append(name).append("\", \"").append(origin).append("\")"); // chain calls db.execSQL(builder.toString()); } }
添加同样数量的cheese,String.format版本需要436毫秒,而StringBuilder需要371毫秒。String.format版本的实现比原来的实现慢,而StringBuilder版本的只是稍快一些。
尽管这三个方法在创建String的方式上不同,它们的共同点是调用execSQL,即仍然需要去做实际的语句的编译。因为所有的语句非常相似(它们只是cheese的name和origin不同),我们可以使用compileStatement(),在循环外仅仅去编译一次。这个实现如Listing 1-21所示。
Listing 1-21 编译SQLite语句
public void populateWithCompileStatement() { SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)"); int i=0; for (String name : sCheeseNames) { String origin = sCheeseOrigins[i++]; stmt.clearBindings(); stmt.bindString(1, name); // 用name替换第一个问号 stmt.bindString(2, origin); // 用origin替换第二个问号 stmt.executeInsert(); } }
因为语句的编译仅需要一次而不是650次,而且binding value比起编译是更加轻量级的操作,这个方法的性能明显提高,它创建这个数据库仅需要268毫秒。它同样有着使代码更易读的优势。
Android同样提供了额外的API插入数值到数据库,通过ContentValues对象,它包含列名和数值。在Listing 1-22的实现中,实际上非常接近populateWithCompileStatement,“INSERT INTO cheese VALUES”字符串甚至没有出现,因为这部分插入语句暗示在da.insert()的调用中。然而,这个实现的性能低于我们使用populateWithCompileStatement()达到的,它需要352毫秒去完成。
Listing 1-22 使用ContentValues填充数据库
public void populateWithContentValues() { ContentValues values = new ContentValues(); int i=0; for(String name : sCheeseNames) { String origin = sCheeseOrigins[i++]; values.clear(); values.put("name", name); values.put("origin", origin); db.insert("cheese", null, values); } }
最快的实现通常也是最灵活的实现,因为在语句中允许更多的选择。比如,你可以通过"INSERT OR FAIL"或者"INSERT OR IGNORE"而不是简单的INSERT。
NOTE: Android 3.0中android.database和android.database.sqlite做了许多变化。比如,Activity类中的managerdQuery, startManagingCursor和stopManagingCursor方法被弃用,用CursorLoader代替。
Android同样定义了几个可以提高性能的类。比如,你可以使用DatabaseUtils.InsertHelper在数据库中插入多行,这样编译SQL的插入语句仅一次。现在它的实现和我们实现populateWithCompileStatement()的方式一致,尽管它并不提供同样的灵活性(比如, FAIL或者ROLLBACK)。
即使性能不是必须的,你也可以使用DatabaseUtils类中的静态方法去简化你的实现。
Transactions
上面的实例没有显式的创建任何事务,然而每次插入操作会自动创建一个,并且在插入完成后立即提交。显式的创建一个事务允许两件事情:1) 原子提交(Atomic Commit)
2) 更好的性能
第一个Feature很重要但是不是从性能角度看的。原子提交(Atomic commit)意味着全部或者没有对数据库的修改发生。一个事务不可以仅提交一部分。在我们的例子中,我们可以认为所有的650个cheese插入是一个事务。我们成功的创建了cheese列表或者没有,但是我们对一部分成功没有兴趣。实现如Listing 1-23所示:
Listing 1-23 在一个事务中插入所有的Cheese
public void populateWithComileStatementOneTransaction () { try { db.beginTransaction(); SQLiteStatement stmt = db.compileStatement("INSERT INTO cheese VALUES(?,?)"); int i=0; for(String name : sCheeseNames) { String origin = sCheeseOrigins[i++]; stmt.clearBindings(); stmt.bindString(1, name); // 使用name替换第一个问号 stmt.bindString(2, origin); // 使用origin替换第二个问号 stmt.executeInsert(); } db.setTransactionSuccessful(); // 移除这个调用,不会有任何改变提交 } catch (Exception e) { // 在这里处理异常 } finally { db.endTransaction(); // 这个调用必须在finally块中 } }
这个实现需要166毫秒执行完。这是一个非常大的提升(快了大约100毫秒),有人会说这两种实现都适用于大多数应用程序,因为通常不会这么快插入那么多行。实际上,大多数应用程序通常作为某个用户操作的响应,每次只访问一行。最重要的点是数据库是内存支持(memory-backed)的,并且没有存储到持久性存储器(SD卡或者内部Flash存储)。使用数据库,很多时间花费在访问持续存储器上(读/写),会比访问易失性存储器慢很多。通过在内部持续性存储器创建数据库,我们可以验证单个事务的效果。在持续性存储设备上创建数据库如Listing
1-24所示。
Listing 1-24 在存储器上创建数据库
public Cheeses (String path) { // path可能已经使用getDatabasePath("fromage.db")创建好了 // 可能需要用一个mkdirs保证path存在 // File file = new File(path); // File parent = new File(file.getParent()); // parent.mkdirs(); db = SQLiteDatabase.openOrCreateDatabase(path, null); db.execSQL("CREATE TABLE cheese (name TEXT, origin TEXT)"): }
当数据库在存储器而不是内存的时候,populateWithCompileStatement()需要大约34秒完成(每行52毫秒),populateWithCompileStatementOneTransaction()需要少于200毫秒。不需要多说了,一个事务的方式对我们的问题来说是一个更好的解决方案。这些数据显然依赖于使用的存储器类型。在外部的SD卡上存储数据库会更慢因此使得一个事务的方式更加吸引人。
NOTE:当在存储器上创建数据库的时候保证父路径存在。参考Context.getDatabasePath和File.mkdirs。为了方便,使用SQLiteOpenHelper代替手动创建数据库。
查询
使查询更快的方式也限制了对数据库的访问,特别是在存储器上。一个数据库的查询简单的返回一个Cursor对象,可以用来迭代所有的结果。Listing 1-25给出了迭代所有行的两个方法。第一个方法创建了一个Cursor获取数据库的所有的两列,第二个方法仅获取第一列。Listing 1-25 迭代所有的行
public void iterateBothColumns() { Cursor c = db.query("cheese", null, null, null, null, null, null); if (c.moveToFirst()) { do { } while (c.moveToNext()); } c.close(); // 记得完成的时候关闭cursor(否则会在某点发生Exception) } public void iterateFirstColumn() { Cursor c = db.query("cheese", new String[] {"name"}, null, null, null, null, null); // 唯一的不同 if(c.moveToFirst()) { do { } while (c.moveToNext()); } c.close(); }
就像期望的,因为根本不需要去从第二列读数据,第二个方法更快:23毫秒和61毫秒(当使用多事务)。当所有的行作为一个事务添加的时候,迭代所有的行更快:11毫秒(iterateBothColumns)和7毫秒(iterateFirsteColumn)。就像可以看到的,只获取你关心的数据。在查询的时候选择合适的参数调用可以导致不同的性能。你可以减少数据库的访问如果你仅需要特定数量的行,在查询调用的时候指定限制的参数。
TIP:更加深入的查找功能(使用indexing)考虑使用SQLite的FTS(full-text search)扩展(使用索引)。参考:www.sqlite.org/fts3.html
相关文章推荐
- 精通安卓性能优化-第一章(三)
- 精通安卓性能优化-第一章(五)
- 精通安卓性能优化-第一章(六)
- 精通安卓性能优化-第一章(一)
- 精通安卓性能优化-第一章(四)
- 精通安卓性能优化-第一章(八)
- 精通安卓性能优化-第一章(十)- 总结
- 精通安卓性能优化-第一章(二)
- 精通安卓性能优化-第一章(七)
- 精通安卓性能优化-第四章(二)
- 精通安卓性能优化-第五章(四)
- 精通安卓性能优化-第六章(一)
- 精通安卓性能优化-第二章(五)
- 精通安卓性能优化-第二章(四)
- 精通安卓性能优化-第四章(三)
- 精通安卓性能优化-第四章(四)
- 精通安卓性能优化-第五章(二)
- 精通安卓性能优化-第三章(二)
- 精通安卓性能优化(Pro Android Apps Performance Optimization)-前言
- 精通安卓性能优化-第三章(一)