Project 总结

上个星期做了些东西,对学到、用到的东西做个总结。同时这也是我进入角色之后的第一个完整的任务,在完成的过程当中遇到一些问题,积累了一些经验,所以详细记录下

描述

  • 现状:发布器的表情显示,原来有四组表情(三组默认+最近表情),作为资源文件打在apk中
  • 需求:客户端从服务端下载配置好的表情,放在第一组默认表情的最前面,与默认表情一起使用,满足默认表情所有的逻辑:正文显示,输入框显示、最近表情逻辑等,同时通过一个版本号来管理,服务端可通过版本号增加、删除、或更新所有的可配置表情
  • 备注:

    1. 配置表情显示在默认表情第一组最前面,默认表情后延;
    2. 版本号升级后表情全量替换;
    3. 只要有一个文件下载失败,或加载出错,所有的配置表情均不显示

简单来讲就是,客户端通过一个接口发送当前的版本号,服务端接收后处理,并决定是否下发可配置表情和下发数量,实际下发的是表情文件的url,客户端解析出表情的属性之后,按照对应的url下载表情文件,存放在本地目录中,之后,发布器就可以加载这些表情并显示,和默认表情一起使用。像QQ,和微信客户端里的表情包类似,不同的是,这些表情包不是作为一个分组单独显示,而是要和其中一组表情混合,看似简单,也是这个任务里最坑的地方

思路

基本上可以分成这样几个过程:

  • 向服务端发送当前版本号,请求结果,并解析
  • 保存图片的配置信息,并下载图片文件保存到本地目录,下载后的路径需要与配置信息同步
  • 界面加载配置好的表情图片,处理与默认表情的使用逻辑
  • 文章正文和输入框能够解析出表情并正确显示

同时,整个请求、解析、下载、存储直到表情配置好的整个过程均在后台执行,配置好后界面才可以加载,所以,配置信息成为整个过程中沟通的接合部分。

  1. 请求接口:请求接口使用已有的一个,增加了版本号字段,请求的时机是在客户端前后台切换时调用,所以可能会在短时间内回来两次结果,需要处理同步问题
  2. 版本号:配置信息解析出之后,首先更新本地版本号,由于是全量替换,所以将该值存储在读取较快的SharedPreference中,而将每一个表情的配置信息按字段存储在Sqlite数据库中。这个想法其实有些犹豫,因为目前的设计是全量替换,但实际中应该是像微信的表情包那样,一组一组的,供用户选择下载,一组的表情有一个共同的标识,加载时直接放一个标签页,所以数据库暂预留了一个字段。不知道他们怎么会有这样别扭的需求
  3. 数据库:由于是全量替换,每次更新时需要先清数据库
  4. 图片文件的下载:解析出表情的url之后,启动Http去下载图片文件,不明白为什么不能把一组表情做到一个url里一次性下载,而是一个表情一个url,一个一个的下(抱怨一下)。这里考虑建立一个下载队列,将解析好的url扔进去,每一个新开一个线程下载,不过目前还是放在for循环里顺序处理了,可能他们也觉得这么多url有些问题吧,暂时作为一个优化点,不影响整体流程
  5. 图片文件的存储:没有选择放在sdcard,一是要面临没有sdcard的情况,二是内部存储读取速度是否会快一些(直觉,没有深入对比过),在/data/data/package_name/files/下新建文件夹存放,存储完成之后,需要将文件的路径与数据库中的配置信息统一
  6. 界面加载:加载之前先检查本地路径是否有效,只要有一个无效,所有的配置表情均不显示,同时取出对应的url去下载这张图片。
    默认表情直接打在apk里,可以作为资源文件用R.drawable.id来标识和读取,本地文件的读取依赖路径名,所以两者一起显示的时候,最坑的地方就来了:资源文件的id是int,本地文件的路径是String,两者无法放在一个List里给View的Adapter,如果给本地文件分配一个int标识,需要防止与资源文件相同引发冲突。所以,就去调研了下R.java中id生成规则,然后定义可配表情的id规则为:
1
public static final int CONFIG_INITIAL_ID = 0x7ff00000;

其中:7ff0表示可配置表情,与默认的资源文件区分(这里详细说明为何这样定义是有效的),接下来两位表示表情类型(如果加入表情分组下发的情况),最后两位表示具体表情,可以表示16*16=256种表情,基本满足要求。

这样,按照这种规则生成的id,与表情的配置信息对应,在表情下载成功之后也存放在数据库中。

界面加载时,读取id,名称,路径,将id加入默认表情的list,扩展原有的<id,名称>,<名称,id>两个Map,新建<id,路径>的List。

  • id的list用于加载图片时顺序查找,选用ArrayList
  • <id,名称>:用于点击图片时查找在输入框中显示的字符,实际显示的是表情标记:[ 表情名 ]
  • <名称,id>:用于正文中表情标记的文本替换
  • <id,路径>:针对可配表情的情况,需要将id转换成路径

之所以会有前两个Map,是View中表情的显示方式相关的:

点击表情时只是拿到图片id,然后按照<id,名称>查找对应的名称,加上[]形式表情的文字格式,当View加载文字时,检测到[]这种格式,会认为这里应该是一个图片,用ImageSpan替代原来的文本,所以取出中间的部分,按照<名称,id>查找资源,当然可配表情还要继续在`拿到具体的路径。

按照这种需求,<id,名称>选择SparseArray<String><名称,id>选择HashMap<String,int><id,路径>选择SparseArray<String>SparseArray的介绍请走这里

优化

缓存

对比测试了一下加载本地文件和资源文件的速度,在一个中等配置的手机上,读取文件平均耗时3.9ms(中间出现了一次46ms,SHIT),但自带的资源文件只有0.5ms,这个差距也有点太大了。

分析发现,读取资源文件有很多时间全是:0ms的情况,又找资料学习了下res文件的加载流程,考虑加入缓存机制,构造了基于软引用和LRU算法的缓存机制,使用之后,读取本地文件的耗时缩减到0.59ms,效果还是很明显的。

总结

  1. 本次需求看似简单,实际有两个困难的地方:资源id分配、缓存。
  2. 总共用了5天的时间开发(包括一个周末):本地逻辑3天,接口联调1天,缓存优化1天。造成进度缓慢的原因主要有两个:a.对代码结构的熟悉程度,b.对整体的没有规划安排。教训和经验是,在接口和数据还没有确定的情况下,习惯性地上来就直接写代码,导致接口确定时所有的工作又要重新做。事实上,在拿到一项任务之后,首先在整体上规划,评估时间,将流程细分,对于不确定的数据暂时模拟,跑通那些不会轻易更改和变动的逻辑
  3. 对使用到和学习到的知识点另篇总结
  4. 第一次任务,所以写的比较详细,今后不会这样繁冗,只总结经验和学习到的知识点

See Also