在产品的设计上,本次改动在设置里直接取消了用户关闭实时同步的选项。这是为了避免引入复杂的逻辑,造成用户费解。在这种情况下,帮用户做决策比给用户很多选择更好。
2.3 状态维护
第一种方案的问题之一是它的文件状态的维护。按照之前的分析,将文件的状态维护在服务器并非最理想的选择。因此,新的方案采用了将状态维护在本地的方案。新方案中,文件的状态被记录在数据库而不是文件中。这里有两点考虑:1).为避免一次性读取大量数据,减少内存占用;2).使用数据库可以进行结构化查询,方便灵活。
对本地文件的状态,我设计了如下数据结构。新的同步方案中,我选用了 Room 作为数据库框架。因此,以下数据结构也大致对应数据库中的 Shcema,
/** 笔记上次同步状态 /
@Entity class NoteLastSyncState: Serializable {
@PrimaryKey(autoGenerate = true) var id: Long? = null
/* 笔记的路径 /
var path: String? = null
/* 文件相对路径,直接父路径,用来根据父路径找子路径 /
var parent: String? = null
/* 如果文件时移动过来的话,记录从哪里移动过来的 /
var movedFrom: String? = null
/* 服务器返回的上次修改的时间,如果有的话,用来判断远程是否修改过 /
var serverLastModifiedTime: Date? = null
/* 上次同步时的 Md5 值,用来判断上次同步完成之后是否又被改动过 /
var lastSyncMd5: String? = null
/* 备注信息,冗余字段,用 json 存储 /
var remark: String? = null
/* 上次同步的时间 */
var lastSyncTime: Date? = null
}
这里的 path
字段是该文件相对于笔记根目录的路径。parent
是它的父目录相对于笔记根目录的路径。parent
的作用是用来根据父目录查找其所有的子文件/目录。比如下面的 SQL 就是基于前缀的匹配方式查询父目录的子文件/目录的状态,
@Query("SELECT * FROM NoteLastSyncState WHERE path LIKE :parent || ‘%’ ")
fun getUnderParent(parent: String): List
在实际编码之前应该先做技术方案。parent
等字段是在方案确定了基础之上,确定需要用到该字段,才将它们加入到数据结构中的。
这里的 movedFrom
用来记录该文件是从哪个位置移动过来的。在最初设计方案的时候,我本打算让移动行为走删除和新增的逻辑。这种思路虽然可行,但是性能会低。因为每个文件的删除和新增都要请求一次网络。当一个目录下存在很多子孙文件/目录的时候,请求的数量会非常多。因此,这里我使用 movedFrom
标记文件从何处移动而来。然后,在同步的时候,再根据该字段,调用服务器的移动接口,直接在服务器进行移动操作。这样一个请求即可完成同步。对于用户直接通过文件管理器移动目录或者文件的情况,由于不存在 movedFrom
标记,会走删除和新增的逻辑(被移动的位置删除,移动到的位置新增)。
这里的 serverLastModifiedTime
用来记录服务器返回的文件的上次修改时间。因为当我们请求一个目录的信息的时,可以获取到该目录下所有子文件的状态,其中就可能包含文件的上次修改时间。因此,每次同步完成之后,我们会记录该文件的上次修改时间。这样,下次同步的时候,通过对比服务器和本地数据库中的上次修改时间,我们就可以判断远程是否对文件做了修改,而无需使用文件的 md5. 这样就可以大幅提升同步的速率并降低流量的消耗。需要注意的是,这里用到的是服务器的修改时间,因为本地时间是不可靠的。
需要注意的是,我们不能假设服务器一定返回文件的上次修改时间字段。因此,它在新的同步方案中是作为判断逻辑的第一道防线。只有确保该字段一定存在的情况下才会使用它作为判断依据。代码如下所示,
/** Check is file changed remotely by last modified time. */
private fun isFileChangedRemotely(
syncState: NoteLastSyncState,
remoteFile: CloudResource
): Boolean = syncS