前言
鹭岛厦门是个很美丽的海滨城市, 给我的感觉很舒适和悠闲, 据说政府对到那工作的高新技术人才第一年有 10 万元的奖励, 因为这个原因我很有兴趣的参加了一个厦门公司的面试. 他们主要是研发 VOIP 方面的技术, 对手机应用的性能优化和音频算法有较高的要求.
他们招人的薪资半年内涨了 30 万元, 都一直都没有招到合适的人. 为什么呢? 因为他们要求技术好的同时, 英语也要好. 什么才叫好呢? 就是可以用流利的英语和国外的团队无障碍交流.
这就为难很多程序员了. 这里就不再讲英语的励志故事了, 我们回到技术面试上. 厦门这家公司对技术的要求还是比较高, 问了很多对 Android 机制的理解问题, 为什么面试官很在意对机制的理解呢? 因为实际的项目中很多性能问题都是由于缺乏对 Android 运行机制的正确理解的程序员引发的. 除了机制问题, 现在印象比较深的就是关于 SQLite 数据库操作的性能优化问题.
面试题: 如何对 SQLite 数据库中进行大量的数据插入?
Android 系统内置了 SQLite 数据库, 并且提供了一整套的 API 用于对数据库进行增删改查操作. SQLite 是一个轻量的, 跨平台的, 开源的数据库引擎. SQLite 每个数据库都是以单个文件 (.db) 的形式存在, 这些数据都是以 B-Tree 的数据结构形式存储在磁盘上.
使用 SQLiteDatabase 的 insert,delete 等方法或者 execSQL 方法默认都开启了事务, 如果操作的顺利完成才会更新. db 数据库. 事务的实现是依赖于名为 rollback journal 文件, 借助这个临时文件来完成原子操作和回滚功能.
大家可以在 / data/data/<packageName>/databases / 目录下看到一个和数据库同名的. db-journal 文件.
SQLite 想要执行操作, 需要将程序中的 SQL 语句编译成对应的 SQLiteStatement, 比如 "select * from table1", 每执行一次都需要将这个 String 类型的 SQL 语句转换成 SQLiteStatement. 如下 insert 的操作最终都是将 ContentValues 转成 SQLiteStatementi:
- public long insertWithOnConflict(String table, String nullColumnHack,
- ContentValues initialValues, int conflictAlgorithm) {
- // 省略部份代码
- SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
- try {
- return statement.executeInsert();
- } finally {
- statement.close();
- }
- } finally {
- releaseReference();
- }
- }
对于批量处理插入或者更新的操作, 我们可以重用 SQLiteStatement, 使用 SQLiteDatabase 的 beginTransaction()方法开启一个事务, 样例如下:
- try
- {
- sqLiteDatabase.beginTransaction();
- SQLiteStatement stat = sqLiteDatabase.compileStatement(insertSQL);
- // 插入 10000 次
- for (int i = 0; i < 10000; i++)
- {
- stat.bindLong(1, 123456);
- stat.bindString(2, "test");
- stat.executeInsert();
- }
- sqLiteDatabase.setTransactionSuccessful();
- }
- catch (SQLException e)
- {
- e.printStackTrace();
- }
- finally
- {
- // 结束
- sqLiteDatabase.endTransaction();
- sqLiteDatabase.close();
- }
我在华为 Nexus 6P 上对常见的几种做法做了一下测试.
直接使用 SQL 语句进行插入
直接使用 SQL 语句插入, 添加事务
使用 ContentValues 方式, 添加事务
使用 SQLiteStatement 方式, 添加事务
结果如下图:
从数据上看, 第四种方式使用 SQLiteStatement 最快, 不过只要添加了事务(或者说只需要一个事务, 不是每条插入都使用事务), 后三种方式的差别并不大. 所以针过这个题目的插入的优化可以通过 "SQLiteStatement + 事务" 的方式显著提高效率.
查询方面的优化一般可以通过建立索引. 建立索引会对插入和更新的操作性能产生影响, 使用索引需要考虑实际情况进行利弊权衡, 对于查询操作量级较大, 业务对要求查询要求较高的, 还是推荐使用索引. 所以这会有一个取舍问题, 看你的项目是查询频繁还是插入和修改频繁. 当然还有一些小的优化细节, 如果面试官问到也可以说几点(如 limit).
线程问题
SQLite 的同步锁精确到数据库级, 粒度比较大, 不像别的数据库有表锁, 行锁. 同一个时间只允许一个连接进行写入操作.
如果有大量的数据处理, 那么肯定不合适于在 UI 线程去操作, 这时就要考虑多线程的问题了. 我们如果开一个工作线程去操作 SQLite 数据库, 如批量地插入可能需要 30 秒钟, 而这个时间 UI 线程也要从数据库读取一下数据展示给用户, 那么这个时候 UI 线程能读取到这个数据库吗? 大家可以思考一下这个问题.
我们常常在多线程中只使用一个 SQLiteDatabase 引用, 在用 SQLiteDataBase.close()的时需要注意调是否还有别的线程在使用这个实例. 如果一个线程操作完成后就直接 close 了, 别一个正在使用这个数据库的线程就会异常. 所以有些人会直接把 SQLiteDatabase 的实例放在 Application 中, 让它们的生命周期一致. 也有的做法是写一个计数器, 当计数器为 0 时才真正关闭数据库.
使用 ORM 的问题
目前网上有很多开源的 ORM(对象关系数据映射)框架, 如 greenDAO,ormlite 等等. 在使用这些框架有必要很了解一下它们的利弊, 特别是一些使用反射的框架, 对性能的影响会比较大. 有些框架在多线程同步方面也会产生一些问题, 所以使用时要有所顾虑.
Realm 是最近兴起的一个专注于移动设备数据库的库, 其核心是使用 C++ 编写, 号称很多时候数据的存取速度比 SQLite 要快很多. 不过在我的一些项目中, 发现它读取并不比 SQLite 快.
小结
在实践中我们总结出一条守则:"不要用 Helloworld 来测试自己的框架(或代码), 要测就要用真实的数据和环境."
特别是针对数据库方面, 如果只用几条简单的数据进行测式, 那么你会很容易傲娇和满足, 而忽视了很多问题. 没有经过真实数据 (或大量数据) 测试之前, 不要对自己的代码太过自信.
最后
在现在这个金三银四的面试季, 我自己在网上也搜集了很多资料做成了文档和架构视频资料免费分享给大家[包括高级 UI, 性能优化, 架构师课程, NDK,Kotlin, 混合式开发(ReactNative+Weex),Flutter 等架构技术资料] , 希望能帮助到您面试前的复习且找到一个好的工作, 也节省大家在网上搜索资料的时间来学习.
来源: http://www.jianshu.com/p/5f4a1f456a74