2024年基于 Android 的文件同步设计方案_android同步文件夹,2024大厂安卓面试集合

最后

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2021面试真题解析

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!


然后尝试立即同步该行为,如果成功就擦除本地行为记录,否则会在下一次对整个文件目录同步的时候进行同步。由于对用户的行为的同步被放在对整个目录同步之前。因此,在该方案中,这些**用户操作的时序性是无法保证的**。


第一种方案的槽点比较多,作为踩坑的方案,最初我并没有考虑多设备同步等情况。不过,它也有一些值得借鉴的地方。比如,通过文件的 md5 来判断文件是否发生了修改;引入垃圾箱机制,本地删除的时候将文件移动到垃圾箱而不是直接删除,由此可以避免误删导致的数据丢失等。


### 2、方案设计


#### 2.1 行为抽象


首先,我们对用户在软件内外(用户有可能直接通过文件管理器操作笔记文件)的行为进行抽象。由此,可得以下五种行为:新增、删除、修改、重命名和移动。重命名操作可以被视为在当前目录内进行移动,因此移动和重命名可以归为一类。所以,用户的行为总计 4 种。另外,根据用户是对本地文件进行操作还是对服务器上的文件进行操作,又可以分成两类。所以,这里需要的考虑的用户行为共 8 种。




|  | 新增 | 删除 | 修改 | 移动/重命名 |
| --- | --- | --- | --- | --- |
| 本地 |  |  |  |  |
| 服务器 |  |  |  |  |


提前考虑好各种情况,有助于防止我们在设计流程的时候出现遗漏。


#### 2.2 实时同步


考虑到维护文件状态可能出现的复杂情况,比如用户在软件内做了移动操作,然后又通过文件管理器对文件进行了移动等情况。最好的方式是当用户在软件内操作完成后立即进行同步。同步完成之后再将本地维护的状态擦除掉。这样既能够体现同步的实时性,又能够尽可能避免出现意外的情况。所以,新的同步方案采用了实时同步和整个目录同步相结合的方式。


在产品的设计上,本次改动在设置里直接取消了用户关闭实时同步的选项。这是为了避免引入复杂的逻辑,造成用户费解。在这种情况下,帮用户做决策比给用户很多选择更好。


#### 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 = syncState.serverLastModifiedTime != null
&& remoteFile.lastUpdate != null
&& remoteFile.lastUpdate.after(syncState.serverLastModifiedTime)

/** Check is file not changed remotely by last modified time. */
private fun isFileNotChangedRemotely(
syncState: NoteLastSyncState,
remoteFile: CloudResource
): Boolean = syncState.serverLastModifiedTime != null
&& remoteFile.lastUpdate != null
&& !remoteFile.lastUpdate.after(syncState.serverLastModifiedTime)


最后值得一提的字段是 `lastSyncMd5`,顾名思义,它是文件的 md5 值,是在文件被写入到本地磁盘之后记录到数据库中的。使用该字段,在远程和本地文件的 md5 不一致的时候,我们可以和之前的方案一样,判断文件是本地还是远程的文件发生了改动。


### 3、同步方案


#### 3.1 流程图


![](https://img-blog.csdnimg.cn/img_convert/c3d3b9684ad75f14b4cfbd75acc7800a.webp?x-oss-process=image/format,png)


整个流程图比较长,大致可以几个部分,我已经在图中标出。


顶部是对之前生成的一些文件的删除和对图片信息的同步,属于本软件特有的部分,可以忽略。然后是整体的循环结构。流程图比较复杂,实际编码会清晰一些。即,我是通过 BFS 算法遍历本地文件树进行同步的。在对目录进行遍历的时候会先读取其对应的服务器目录下所有文件的状态以及本地存储的所有子文件的状态到 `remoteFiles`。然后,通过对比本地的文件状态和远程的文件状态进行同步。一个文件或者目录同步完成之后会从 `remoteFiles` 中移除。



runBackground(onFinished, onInterrupted) { failures ->
val visitors = mutableListOf(File(path))
val count = AtomicInteger(0)
while (visitors.isNotEmpty() && !interrupted) {
try {
val directory = visitors.removeAt(0)
val dirRelativePath = sm.relativePathOf(directory.path)

        // Read contents of directory from cloud.
        val listResult = server.list(dirRelativePath)
        val remoteFiles: MutableMap<String, CloudResource> = if (listResult.isFailed) {
            log { "failed to read contents of directory [$dirRelativePath] from cloud, code [${listResult.code}], msg [${listResult.message}] ." }
            val synced = syncDirectoryWhenFailedReadRemotely(directory, failures)
            if (synced) {
                continue
            }
            mutableMapOf()
        } else {
            insertDirectoryLastSyncState(directory)
            listResult.data.toMutableMap()
        }

        // Read last sync records from local database.
        val syncRecords = mutableMapOf<String, NoteLastSyncState>()
        DB.get().noteLastSyncStateDao().getByParent(dirRelativePath).forEach {
            syncRecords[it.path ?: ""] = it
        }

        // Travel under directory and handle files.
        directory.listFiles()?.forEach { file ->
            if (interrupted) {
                return@forEach
            }

            val fileRelativePath = sm.relativePathOf(file.path)

            // Files should be ignored.
            if (fileRelativePath == "/$SETTING_FOLDER_NAME/$SETTING_IMAGE_MODEL_DATA") {
                return@forEach
            }

            val syncRecord = syncRecords[fileRelativePath]

            if (file.isDirectory) {
                visitors.add(file)

                // The directory exists in cloud: remove from remote files.
                if (remoteFiles.containsKey(fileRelativePath)) {
                    remoteFiles.remove(fileRelativePath)
                }
            } else if (file.isFile) {
                Thread.sleep(timeDelayMillis.toLong())
                val remoteFile = remoteFiles[fileRelativePath]

                // Sync a single file.
                syncFile(file, syncRecord, remoteFile, failures)

                if (remoteFile != null) {
                    remoteFiles.remove(fileRelativePath)
                }
                notifyProgressChanged(count.addAndGet(1), onProgress)
            }
        }

        // Handle left remote resources.
        syncRemoteResourcesNotFoundLocally(remoteFiles, failures, count, onProgress)
    } catch (t: Throwable) {
        t.printStackTrace()
        log { "failed to sync folder with exception: $t" }
    }
}

}


`remoteFiles` 剩下的部分就是远程存在而本地不存在的文件或者目录。它们又可能存在几种情况,被本地删除、远程新增或者被本地移动到其他目录。然后,再根据数据库中的状态记录,对三种情况进行判断。


具体同步流程代码比较长,不便于贴出,后续我会将文件同步逻辑提取出来,开源出一个通用的框架。


#### 3.2 类设计


由于后续考虑支持更多的云服务器,所以,在新的同步方案中,我也对类结构进行了设计。首先是针对服务器的设计,



/** 云同步服务器接口封装 */
interface ICloudServer {

/** 读取文件内容 */

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

5721767424)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值