Android 优化——存储优化
交换数据格式
Google 推出的 Protocal Buffers 是一种更轻便高效的存储结构,但消耗内存较大。
FlatBuffers 同样由 Google 推出,专注性能,适合移动端。占用存储比 Protocal 要大。
SharePreferences 优化
-
当 SharedPreferences 文件还没有被加载到内存时,调用 getSharedPreferences 方法会初始化文件并读入内存,这容易导致 耗时更长。
-
Editor 的 commit 或者 apply 方法的区别在于同步写入和异步 写入,以及是否需要返回值。在不需要返回值的情况下,使用 apply 方法可以极大提高性能。
-
SharedPreferences 类 中的 commitToMemory() 会锁定 SharedPreference 对象,put() 和 getEditor() 方法会锁定 Editor 对象,在写入磁盘时更会锁定一个写入锁。因此,最好的优化方法就是避免频繁地读写 SharedPreferences,减少无谓的调用。对于 SharedPreferences 的批量操作,最好先获取一个 editor 进行批量操作,然后调用 apply 方法。
Bitmap 解码
- 4.4 以上 decodeFile 内部没有使用缓存,效率不高。要使用 decodeStream,同时传入的文件流为 BufferedInputStream。
- decodeResource 同样存在性能问题,用 decodeResourceStream。
数据库优化
-
使用 StringBuilder 代替 String
-
查询时返回更少的结果集及更少的字段
查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间及内存,更多的字段会导致更多的内存消耗。
-
少用 cursor.getColumnIndex
根据性能调优过程中的观察 cursor.getColumnIndex 的时间消耗跟 cursor.getInt 相差无几。可以在建表的时候用 static 变量记住某列的 index,直接调用相应 index 而不是每次查询。
-
异步线程
Android 中数据不多时表查询可能耗时不多,不会导致 ANR,不过大于 100ms 时同样会让用户感觉到延时和卡顿,可以放在线程中运行,但 sqlite 在并发方面存在局限,多线程控制较麻烦,这时候可使用单线程池,在任务中执行 db 操作,通过 handler 返回结果和 UI 线程交互,既不会影响 UI 线程,同时也能防止并发带来的异常。
-
SQLiteOpenHelper 维持一个单例
因为 SQLite 对多线程的支持并不是很完善,如果两个线程同时操作数据库,因为数据库被另一个线程占用, 这种情况下会报“Database is locked” 的异常。所以在数据库管理类中使用单例模式,就可以保证无论在哪个线程中获取数据库对象,都是同一个。
最好的方法是所有的数据库操作统一到同一个线程队列管理,而业务层使用缓存同步,这样可以完全避免多线程操作数据库导致的不同步和死锁问题。
-
Application 中初始化
- 使用 Application 的 Context 创建数据库,在 Application 生命周期结束时再关闭。
- 在应用启动过程中最先初始化完数据库,避免进入应用后再初始化导致相关操作时间变长。
-
少用 AUTOINCREMENT
主键加上 AUTOINCREMENT 后,可以保证主键严格递增,但并不能保证每次都加 1,因为在插入失败后,失败的行号不会被复用,会造成主键有间隔,继而使 INSERT 耗时 1 倍以上。
这个 AUTOINCREMENT 关键词会增加 CPU,内存,磁盘空间和磁盘 I/O 的负担,所以 尽量不要用,除非必需。通常情况下都不是必需的。
事务
使用事务的两大好处是原子提交和更优性能:
- 原子提交:意味着同一事务内的所有修改要么都完成要么都不做,如果某个修改失败,会自动回滚使得所有修改不生效。
- 更优性能:Sqlite 默认会为每个插入、更新操作创建一个事务,并且在每次插入、更新后立即提交。这样如果连续插入 100 次数据实际是创建事务、执行语句、提交这个过程被重复执行了 100 次。如果显式的创建事务,这个过程只做一次,通过这种一次性事务可以使得性能大幅提升。尤其当数据库位于 sd 卡时,时间上能节省两个数量级左右。
主要三个方法:beginTransaction,setTransactionSuccessful,endTransaction。
SQLiteStatement
使用 Android 系统提供的 SQLiteStatement 来插入数据,在性能上有一定的提高,并且也解决了 SQL 注入的问题。
SQLiteStatement statement = dbOpenHelper.getWritableDatabase().compileStatement("INSERT INTO EMPERORS(name, dynasty, start_year) values(?,?,?)");
statement.clearBindings();
statement.bindString(1, "Max");
statement.bindString(2, "Luk");
statement.bindString(3, "1998");
statement.executeInsert();
SQLiteStatement 只能插入一个表中的数据,在插入前要清除上一次的数据。
索引
索引就像书本的目录,目录可以快速找到所在页数,数据库中索引可以帮助快速找到数据,而不用全表扫描,合适的索引可以大大提高数据库查询的效率。
优点:大大加快了数据库检索的速度,包括对单表查询、连表查询、分组查询、排序查询。经常是一到两个数量级的性能提升,且随着数据数量级增长。
缺点:
- 索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。
- 在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。
分类
-
直接创建索引和间接创建索引
- 直接创建: 使用 sql 语句创建,Android 中可以在 SQLiteOpenHelper 的 onCreate 或是 onUpgrade 中直接 excuSql 创建语句,如
CREATE INDEX mycolumn_index ON mytable (myclumn)
- 间接创建: 定义主键约束或者唯一性键约束,可以间接创建索引,主键默认为唯一索引。
- 直接创建: 使用 sql 语句创建,Android 中可以在 SQLiteOpenHelper 的 onCreate 或是 onUpgrade 中直接 excuSql 创建语句,如
-
普通索引和唯一性索引
- 普通索引:
CREATEINDEXmycolumn_indexONmytable(myclumn)
- 唯一性索引:保证在索引列中的全部数据是唯一的,对聚簇索引和非聚簇索引都可以使用,语句为
CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)
- 普通索引:
-
单个索引和复合索引
- 单个索引:索引建立语句中仅包含单个字段,如上面的普通索引和唯一性索引创建示例。
- 复合索引:又叫组合索引,在索引建立语句中同时包含多个字段,如
CREATEINDEXname_indexONusername(firstname,lastname)
,其中 firstname 为前导列。
-
聚簇索引和非聚簇索引 (聚集索引,群集索引)
- 聚簇索引:物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列,如
CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW
,其中WITH ALLOW_DUP_ROW
表示允许有重复记录的聚簇索引 - 非聚簇索引:
CREATEUNCLUSTEREDINDEXmycolumn_cindexONmytable(mycolumn)
,索引默认为非聚簇索引
- 聚簇索引:物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列,如
使用场景
- 当某字段数据更新频率较低,查询频率较高,经常有范围查询
(>, <, =,>=, <=)
或order by
、group by
发生时建议使用索引。并且选择度(一个字段中唯一值的数量 / 总的数量)越大,建索引越有优势 - 经常同时存取多列,且每列都含有重复值可考虑建立复合索引
使用规则
- 对于复合索引,把使用最频繁的列做为前导列 (索引中第一个字段)。如果查询时前导列不在查询条件中则该复合索引不会被使用。如
create unique index PK_GRADE_CLASS on student (grade, class)
,select * from student where class = 2
未使用到索引,select * from dept where grade = 3
使用到了索引 - 避免对索引列进行计算,对 where 子句列的任何计算如果不能被编译优化,都会导致查询时索引失效
select * from student where tochar(grade)=’2
- 比较值避免使用 NULL
- 多表查询时要注意是选择合适的表做为内表。连接条件要充份考虑带有索引的表、行数多的表,内外表的选择可由公式:外层表中的匹配行数
*
内层表中每一次查找的次数确定,乘积最小为最佳方案。实际多表操作在被实际执行前,查询优化器会根据连接条件,列出几组可能的连接方案并从中找出系统开销最小的最佳方案 - 查询列与索引列次序一致
- 用多表连接代替 EXISTS 子句
- 把过滤记录数最多的条件放在最前面
- 善于使用存储过程,它使 sql 变得更加灵活和高效 (Sqlite 不支持存储过程)
其它通用优化
- 经常用的数据读取后缓存起来,以免多次重复读写造成“写入放大”
- 子线程读写数据
- ObjectOutputStream 在序列化磁盘时,会把内存中的每个对象保存到磁盘,在保存对象的 时候,每个数据成员会带来一次 I/O 操作。在 ObjectOutputStream 上面再封装一个输出流 ByteArrayOutputStream 或 BufferedOutputStream,先将对象序列化后的信息写到缓存区中,然后再一次性地写到磁盘上;相应的,用 ByteArrayInputStream 或 BufferedInputStream 替代 ObjectInputStream。
- 合理选择缓冲区 Buffer 的大小。太小导致 I/O 操作次数增多,太大导致申请时间变长。比如 4-8 KB。