精通安卓性能优化-第一章(九)

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值