Android性能优化
在上一篇中介绍了性能优化的概念、内存泄漏和性能优化方式
Android性能优化(上)
我们继续说说Android性能优化
数据库性能优化
索引
简单的说,索引就像书本的目录,目录可以快速找到所在页数,数据库中索引可以帮助快速找到数据,而不用全表扫描,合适的索引可以大大提高数据库查询的效率。
(1). 优点
大大加快了数据库检索的速度,包括对单表查询、连表查询、分组查询、排序查询。经常是一到两个数量级的性能提升,且随着数据数量级增长。
(2). 缺点
索引的创建和维护存在消耗,索引会占用物理空间,且随着数据量的增加而增加。在对数据库进行增删改时需要维护索引,所以会对增删改的性能存在影响。
使用场景
A.当某字段数据更新频率较低,查询频率较高,经常有范围查询(>, =, <=)或order by、group by发生时建议使用索引。并且选择度越大,建索引越有优势,这里选择度指一个字段中唯一值的数量/总的数量。
B. 经常同时存取多列,且每列都含有重复值可考虑建立复合索引
索引分类
直接创建索引和间接创建索引
直接创建: 使用sql语句创建,Android中可以在SQLiteOpenHelper的onCreate或是onUpgrade中直接excuSql创建语句,语句如
CREATE INDEX mycolumn_index ON mytable (myclumn)
间接创建: 定义主键约束或者唯一性键约束,可以间接创建索引,主键默认为唯一索引。
普通索引和唯一性索引
普通索引:CREATE INDEX mycolumn_index ON mytable (myclumn)
唯一性索引:保证在索引列中的全部数据是唯一的,对聚簇索引和非聚簇索引都可以使用,语句为CREATE UNIQUE COUSTERED INDEX myclumn_cindex ON mytable(mycolumn)
单个索引和复合索引
单个索引:索引建立语句中仅包含单个字段,如上面的普通索引和唯一性索引创建示例。
复合索引:又叫组合索引,在索引建立语句中同时包含多个字段,语句如:CREATE INDEX name_index ON username(firstname, lastname),其中firstname为前导列。
聚簇索引和非聚簇索引(聚集索引,群集索引)
聚簇索引:物理索引,与基表的物理顺序相同,数据值的顺序总是按照顺序排列,语句为:CREATE CLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn) WITH ALLOW_DUP_ROW,其中WITH ALLOW_DUP_ROW表示允许有重复记录的聚簇索引
非聚簇索引:CREATE UNCLUSTERED INDEX mycolumn_cindex ON mytable(mycolumn)
索引默认为非聚簇索引
使用规则
对于复合索引,把使用最频繁的列做为前导列(索引中第一个字段)。如果查询时前导列不在查询条件中则该复合索引不会被使用。
如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不支持存储过程::>_<::>
事务
使用事务的两大好处是原子提交和更优性能。
原子提交
原子提交意味着同一事务内的所有修改要么都完成要么都不做,如果某个修改失败,会自动回滚使得所有修改不生效。
更优性能
Sqlite默认会为每个插入、更新操作创建一个事务,并且在每次插入、更新后立即提交。
这样如果连续插入100次数据实际是创建事务->执行语句->提交这个过程被重复执行了100次。如果我们显式的创建事务->执行100条语句->提交会使得这个创建事务和提交这个过程只做一次,通过这种一次性事务可以使得性能大幅提升。尤其当数据库位于sd卡时,时间上能节省两个数量级左右。
Sqlte显示使用事务,示例代码如下:
public void insertWithOneTransaction() {
SQLiteDatabase db = sqliteOpenHelper.getWritableDatabase();
//开始一个事务
db.beginTransaction();
try {
for (int i = 0; i < 100; i++) {
db.insert(yourTableName, null, value);
}
// 设置当前事务成功
db.setTransactionSuccessful();
} catch (Exception e) {
e.printStackTrace();
} finally {
//结束事务
db.endTransaction();
}
}
其中sqliteOpenHelper.getWritableDatabase()表示得到写表权限。
其他Sqlite的优化
语句的拼接使用StringBuilder代替String
简单的String相加会导致创建多个临时对象消耗性能。StringBuilder的空间预分配性能好得多。如果你对字符串的长度有大致了解,如100字符左右,可以直接new StringBuilder(128)指定初始大小,减少空间不够时的再次分配。
查询时返回更少的结果集及更少的字段。
查询时只取需要的字段和结果集,更多的结果集会消耗更多的时间及内存,更多的字段会导致更多的内存消耗。
少用cursor.getColumnIndex
根据性能调优过程中的观察cursor.getColumnIndex的时间消耗跟cursor.getInt(索引号)相差无几。可以在建表的时候用static变量记住某列的index,直接调用相应index而不是每次查询。
public static final String HTTP_RESPONSE_TABLE_ID = _ID;
public static final String HTTP_RESPONSE_TABLE_RESPONSE = "response";
public List getData() {
……
cursor.getString(cursor.getColumnIndex(HTTP_RESPONSE_TABLE_RESPONSE));
……
}
//优化为
public static final String HTTP_RESPONSE_TABLE_ID = _ID;
public static final String HTTP_RESPONSE_TABLE_RESPONSE = "response";
public static final int HTTP_RESPONSE_TABLE_ID_INDEX = 0;
public static final int HTTP_RESPONSE_TABLE_URL_INDEX = 1;
public List getData() {
……
cursor.getString(HTTP_RESPONSE_TABLE_RESPONSE_INDEX);
……
}
异步线程
Sqlite是常用于嵌入式开发中的关系型数据库,完全开源。与Web常用的数据库Mysql、Oracle db、sql server不同,Sqlite是一个内嵌式的数据库,数据库服务器就在你的程序中,无需网络配置和管理,数据库服务器端和客户端运行在同一进程内,减少了网络访问的消耗,简化了数据库管理。不过Sqlite在并发、数据库大小、网络方面存在局限性,并且为表级锁,所以也没必要多线程操作。
Android中数据不多时表查询可能耗时不多,不会导致ANR,不过大于100ms时同样会让用户感觉到延时和卡顿,可以放在线程中运行,但Sqlite在并发方面存在局限,多线程控制较麻烦,这时候可使用单线程池,在任务中执行db操作,通过Handler返回结果和UI线程交互,既不会影响UI线程,同时也能防止并发带来的异常。
可使用Android提供的AsyncQueryHandler或类似如下代码完成:
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
db.insert("你的表名", null, value);
handler.sendEmptyMessage(xx);
}
});
移动网络优化
一个网络请求可以简单分为连接服务器 -> 获取数据两个部分。
其中连接服务器前还包括DNS解析的过程;获取数据后可能会对数据进行缓存。
连接服务器优化策略
不用域名,用IP直连
省去 DNS 解析过程,DNS 全名 Domain Name System,解析意指根据域名得到其对应的 IP 地址。
如:http://www.codekk.com 的域名解析结果就是104.236.147.76。
首次域名解析一般需要几百毫秒,可通过直接向IP而非域名请求,节省掉这部分时间,同时可以预防域名劫持等带来的风险。
当然为了安全和扩展考虑,这个IP可能是一个动态更新的IP列表,并在IP不可用情况下通过域名访问。
服务器合理部署
配合上面说到的动态 IP 列表,支持优先级,每次根据地域、网络类型等选择最优的服务器IP进行连接。
对于服务器端还可以调优服务器的TCP拥塞窗口大小、重传超时时间(RTO)、最大传输单元(MTU)等。
获取数据优化策略
连接复用
节省连接建立时间,如开启keep-alive
Http 1.1 默认启动了keep-alive。对于Android来说默认情况下HttpURLConnection 和HttpClient都开启了keep-alive。
其他网络请求框架也可以进行相应配置
请求合并
即将多个请求合并为一个进行请求,比较常见的就是网页中的CSS Image Sprites。 如果某个页面内请求过多,也可以考虑做一定的请求合并。
减小请求数据大小
对于 POST 请求,Body 可以做 Gzip 压缩,如日志。
对请求头进行压缩
这个http 1.1不支持,spdy及http 2.0支持。http 1.1 可以通过服务端对前一个请求的请求头进行缓存,后面相同请求头用md5之类的id来表示即可。
CDN缓存静态资源
缓存常见的图片、JS、CSS 等静态资源。
减小返回数据大小
压缩
一般API数据使用Gzip压缩
精简数据格式
如JSON代替 XML,WebP代替其他图片格式
对于不同的设备不同网络返回不同的内容 如不同分辨率图片大小。
增量更新
需要数据更新时,可考虑增量更新。如常见的服务端进行bsdiff,客户端进行 bspatch。
大文件下载
支持断点续传,并缓存Http Resonse的ETag标识,下次请求时带上,从而确定是否数据改变过,未改变则直接返回304。
数据缓存
缓存获取到的数据,在一定的有效时间内再次请求可以直接从缓存读取数据。
现在的网络请求框架都可进行相应的缓存配置
今天周末,就说这么多(懒),祝大家周末Happy