欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于Android實現(xiàn)的文件同步設(shè)計方案

 更新時間:2023年10月23日 08:41:29   作者:開發(fā)者如是說  
隨著用戶對自身數(shù)據(jù)保護(hù)意識的加強,讓用戶自己維護(hù)自己的數(shù)據(jù)也成了獨立開發(fā)產(chǎn)品時的一個賣點,若只針對少量的文件進(jìn)行同步,則實現(xiàn)起來比較簡單,當(dāng)針對一個多層級目錄同步時,情況就復(fù)雜多了,本文我分享下我的設(shè)計思路

1、背景

隨著用戶對自身數(shù)據(jù)保護(hù)意識的加強,讓用戶自己維護(hù)自己的數(shù)據(jù)也成了獨立開發(fā)產(chǎn)品時的一個賣點。若只針對少量的文件進(jìn)行同步,則實現(xiàn)起來比較簡單。當(dāng)針對一個多層級目錄同步時,情況就復(fù)雜多了。鑒于相關(guān)的文章甚少,本文我分享下我的設(shè)計思路。

本文是我在開發(fā)言葉(一個基于文件系統(tǒng)的 Markdown 筆記軟件)過程中整理出的設(shè)計思路。這里的方案是我設(shè)計的第二套方案,在第一個方案的基礎(chǔ)上彌補了很多不足。比之前的版本,同步的速率大幅提升,流量的消耗也大幅降低。

1.1 文件目錄同步的難點

針對文件目錄的同步不像基于數(shù)據(jù)庫的同步那樣靈活。對于文件同步,同步的對象是普通的文件,我們無法通過為其增加時間戳、版本號等信息來判斷哪個文件是最新的。

對多級文件目錄的移動操作的同步也是一個難點。因為移動操作可能會同時移動大量的文件,導(dǎo)致它們文件目錄的變更。若處理不好則容易導(dǎo)致文件丟失或者文件重復(fù)。

文件同步設(shè)計的另一個難點是對云服務(wù)器的兼容。言葉支持的是基于 WebDAV 的同步,將來我還考慮支持更多云服務(wù)器。所以,我需要設(shè)計一個針對不同服務(wù)器的方案而不只是針對 WebDAV 協(xié)議的。即便針對 WebDAV 協(xié)議進(jìn)行設(shè)計,我們也無法保證所有云提供商都會嚴(yán)格按照 WebDAV 協(xié)議進(jìn)行支持。

1.2 第一個版本的方案及其局限性

第一個版本方案的流程圖如下。

這個版本方案的基本思路如下。

通過對比本地和遠(yuǎn)程文件的 md5 來判斷文件是否發(fā)生了變更。在每次同步完成之后會將所有文件的路徑和 md5 值的映射關(guān)系以如下格式寫入到服務(wù)器的一個文本文件中。

/測試/test.txt:ADBF5A778175EE757C34D0EBA4E932BC
/jjsskizs.log:D41D8CD98F00B204E9800998ECF8427E
/Hello.txt:D064F3519426DCD30114B900431FC044
...

如果服務(wù)器中的一個文件不在上述記錄中,我們可以判斷這個文件是服務(wù)器新增的(相對于本地);如果本地的一個文件不在上述記錄中,我們則可以判斷這個文件是本地新增的;如果一個文件存在于上述記錄中而不存在于云服務(wù)器,我們可以判斷該文件是被服務(wù)器刪除;如果一個文件存在于上述記錄而不存在于本地,則可以判斷為被本地刪除。對于文件的移動操作,這種方案會將其分解成刪除和新增兩個操作。

這種方案存在兩個問題:1).該方案需要通過網(wǎng)絡(luò)讀取遠(yuǎn)程的每個文件的 md5 值。這導(dǎo)致該方案流量消耗比較多以及同步耗時比較長。2).在服務(wù)器中維護(hù)狀態(tài)文件還存在當(dāng)用戶在兩個設(shè)備上同步的時候會出現(xiàn)行為沖突問題。比如,一個設(shè)備新增一個文件并寫入映射關(guān)系到該狀態(tài)文件,另一個設(shè)備會將該文件判斷為本地刪除,從而在遠(yuǎn)程刪除該文件。

對于用戶在設(shè)備上的刪除、移動行為,在這種方案中會先將這些行為以如下格式寫入到本地的文本中,

DIR:DELETE:/測試::false:true
DIR:DELETE:/測試目錄::false:true
DIR:DELETE:/新目錄::false:true
...

然后嘗試立即同步該行為,如果成功就擦除本地行為記錄,否則會在下一次對整個文件目錄同步的時候進(jìn)行同步。由于對用戶的行為的同步被放在對整個目錄同步之前。因此,在該方案中,這些用戶操作的時序性是無法保證的。

第一種方案的槽點比較多,作為踩坑的方案,最初我并沒有考慮多設(shè)備同步等情況。不過,它也有一些值得借鑒的地方。比如,通過文件的 md5 來判斷文件是否發(fā)生了修改;引入垃圾箱機制,本地刪除的時候?qū)⑽募苿拥嚼涠皇侵苯觿h除,由此可以避免誤刪導(dǎo)致的數(shù)據(jù)丟失等。

2、方案設(shè)計

2.1 行為抽象

首先,我們對用戶在軟件內(nèi)外(用戶有可能直接通過文件管理器操作筆記文件)的行為進(jìn)行抽象。由此,可得以下五種行為:新增、刪除、修改、重命名和移動。重命名操作可以被視為在當(dāng)前目錄內(nèi)進(jìn)行移動,因此移動和重命名可以歸為一類。所以,用戶的行為總計 4 種。另外,根據(jù)用戶是對本地文件進(jìn)行操作還是對服務(wù)器上的文件進(jìn)行操作,又可以分成兩類。所以,這里需要的考慮的用戶行為共 8 種。

新增刪除修改移動/重命名
本地
服務(wù)器

提前考慮好各種情況,有助于防止我們在設(shè)計流程的時候出現(xiàn)遺漏。

2.2 實時同步

考慮到維護(hù)文件狀態(tài)可能出現(xiàn)的復(fù)雜情況,比如用戶在軟件內(nèi)做了移動操作,然后又通過文件管理器對文件進(jìn)行了移動等情況。最好的方式是當(dāng)用戶在軟件內(nèi)操作完成后立即進(jìn)行同步。同步完成之后再將本地維護(hù)的狀態(tài)擦除掉。這樣既能夠體現(xiàn)同步的實時性,又能夠盡可能避免出現(xiàn)意外的情況。所以,新的同步方案采用了實時同步和整個目錄同步相結(jié)合的方式。

在產(chǎn)品的設(shè)計上,本次改動在設(shè)置里直接取消了用戶關(guān)閉實時同步的選項。這是為了避免引入復(fù)雜的邏輯,造成用戶費解。在這種情況下,幫用戶做決策比給用戶很多選擇更好。

2.3 狀態(tài)維護(hù)

第一種方案的問題之一是它的文件狀態(tài)的維護(hù)。按照之前的分析,將文件的狀態(tài)維護(hù)在服務(wù)器并非最理想的選擇。因此,新的方案采用了將狀態(tài)維護(hù)在本地的方案。新方案中,文件的狀態(tài)被記錄在數(shù)據(jù)庫而不是文件中。這里有兩點考慮:1).為避免一次性讀取大量數(shù)據(jù),減少內(nèi)存占用;2).使用數(shù)據(jù)庫可以進(jìn)行結(jié)構(gòu)化查詢,方便靈活

對本地文件的狀態(tài),我設(shè)計了如下數(shù)據(jù)結(jié)構(gòu)。新的同步方案中,我選用了 Room 作為數(shù)據(jù)庫框架。因此,以下數(shù)據(jù)結(jié)構(gòu)也大致對應(yīng)數(shù)據(jù)庫中的 Shcema,

/** 筆記上次同步狀態(tài) */
@Entity class NoteLastSyncState: Serializable {
    @PrimaryKey(autoGenerate = true) var id: Long? = null
    /** 筆記的路徑 */
    var path: String? = null
    /** 文件相對路徑,直接父路徑,用來根據(jù)父路徑找子路徑 */
    var parent: String? = null
    /** 如果文件時移動過來的話,記錄從哪里移動過來的 */
    var movedFrom: String? = null
    /** 服務(wù)器返回的上次修改的時間,如果有的話,用來判斷遠(yuǎn)程是否修改過 */
    var serverLastModifiedTime: Date? = null
    /** 上次同步時的 Md5 值,用來判斷上次同步完成之后是否又被改動過 */
    var lastSyncMd5: String? = null
    /** 備注信息,冗余字段,用 json 存儲 */
    var remark: String? = null
    /** 上次同步的時間 */
    var lastSyncTime: Date? = null
}

這里的 path 字段是該文件相對于筆記根目錄的路徑。parent 是它的父目錄相對于筆記根目錄的路徑。parent 的作用是用來根據(jù)父目錄查找其所有的子文件/目錄。比如下面的 SQL 就是基于前綴的匹配方式查詢父目錄的子文件/目錄的狀態(tài),

@Query("SELECT * FROM NoteLastSyncState WHERE path LIKE :parent || '%' ")
fun getUnderParent(parent: String): List<NoteLastSyncState>

在實際編碼之前應(yīng)該先做技術(shù)方案。parent 等字段是在方案確定了基礎(chǔ)之上,確定需要用到該字段,才將它們加入到數(shù)據(jù)結(jié)構(gòu)中的。

這里的 movedFrom 用來記錄該文件是從哪個位置移動過來的。在最初設(shè)計方案的時候,我本打算讓移動行為走刪除和新增的邏輯。這種思路雖然可行,但是性能會低。因為每個文件的刪除和新增都要請求一次網(wǎng)絡(luò)。當(dāng)一個目錄下存在很多子孫文件/目錄的時候,請求的數(shù)量會非常多。因此,這里我使用 movedFrom 標(biāo)記文件從何處移動而來。然后,在同步的時候,再根據(jù)該字段,調(diào)用服務(wù)器的移動接口,直接在服務(wù)器進(jìn)行移動操作。這樣一個請求即可完成同步。對于用戶直接通過文件管理器移動目錄或者文件的情況,由于不存在 movedFrom 標(biāo)記,會走刪除和新增的邏輯(被移動的位置刪除,移動到的位置新增)。

這里的 serverLastModifiedTime 用來記錄服務(wù)器返回的文件的上次修改時間。因為當(dāng)我們請求一個目錄的信息的時,可以獲取到該目錄下所有子文件的狀態(tài),其中就可能包含文件的上次修改時間。因此,每次同步完成之后,我們會記錄該文件的上次修改時間。這樣,下次同步的時候,通過對比服務(wù)器和本地數(shù)據(jù)庫中的上次修改時間,我們就可以判斷遠(yuǎn)程是否對文件做了修改,而無需使用文件的 md5. 這樣就可以大幅提升同步的速率并降低流量的消耗。需要注意的是,這里用到的是服務(wù)器的修改時間,因為本地時間是不可靠的。

需要注意的是,我們不能假設(shè)服務(wù)器一定返回文件的上次修改時間字段。因此,它在新的同步方案中是作為判斷邏輯的第一道防線。只有確保該字段一定存在的情況下才會使用它作為判斷依據(jù)。代碼如下所示,

/** 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 值,是在文件被寫入到本地磁盤之后記錄到數(shù)據(jù)庫中的。使用該字段,在遠(yuǎn)程和本地文件的 md5 不一致的時候,我們可以和之前的方案一樣,判斷文件是本地還是遠(yuǎn)程的文件發(fā)生了改動。

3、同步方案

3.1 流程圖

整個流程圖比較長,大致可以幾個部分,我已經(jīng)在圖中標(biāo)出。

頂部是對之前生成的一些文件的刪除和對圖片信息的同步,屬于本軟件特有的部分,可以忽略。然后是整體的循環(huán)結(jié)構(gòu)。流程圖比較復(fù)雜,實際編碼會清晰一些。即,我是通過 BFS 算法遍歷本地文件樹進(jìn)行同步的。在對目錄進(jìn)行遍歷的時候會先讀取其對應(yīng)的服務(wù)器目錄下所有文件的狀態(tài)以及本地存儲的所有子文件的狀態(tài)到 remoteFiles。然后,通過對比本地的文件狀態(tài)和遠(yuǎn)程的文件狀態(tài)進(jìn)行同步。一個文件或者目錄同步完成之后會從 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 剩下的部分就是遠(yuǎn)程存在而本地不存在的文件或者目錄。它們又可能存在幾種情況,被本地刪除、遠(yuǎn)程新增或者被本地移動到其他目錄。然后,再根據(jù)數(shù)據(jù)庫中的狀態(tài)記錄,對三種情況進(jìn)行判斷。

具體同步流程代碼比較長,不便于貼出,后續(xù)我會將文件同步邏輯提取出來,開源出一個通用的框架。

3.2 類設(shè)計

由于后續(xù)考慮支持更多的云服務(wù)器,所以,在新的同步方案中,我也對類結(jié)構(gòu)進(jìn)行了設(shè)計。首先是針對服務(wù)器的設(shè)計,

/** 云同步服務(wù)器接口封裝 */
interface ICloudServer {

    /** 讀取文件內(nèi)容 */
    fun readText(rp: String): Resources<String>

    /** Write text to given file with relative path [rp]. */
    fun writeText(text: String, rp: String): Resources<Boolean>

    /** Read bytes of a cloud file. */
    fun readBytes(rp: String): Resources<ByteArray>

    // .....
}

這個類中定義了服務(wù)器需要實現(xiàn)的方法。比如,WebDAV 對應(yīng)的實現(xiàn)是 WebDAVServer. 當(dāng)后續(xù)需要支持 OneDrive 同步的時候,基于該接口進(jìn)行實現(xiàn)即可。

另外是同步工作類,也是以上流程圖邏輯存在的地方。這里定義了 ICloudSyncWorker 這個接口,

interface ICloudSyncWorker {

    /**
     * Sync a file.
     *
     * @param file the file to sync
     * @param syncRecord note last sync state, might be null
     * @param remoteFile the file info in cloud server
     * @param failures the failures to report, failures will be added to this list.
     */
    fun syncFile(
        file: File,
        syncRecord: NoteLastSyncState?,
        remoteFile: CloudResource?,
        failures: MutableList<ISyncManager.SyncFailure>
    )

    // ...
}

ICloudSyncWorker 中再引用 ICloudServer 進(jìn)行網(wǎng)絡(luò)請求。這樣,我們就提高了以上同步流程的拓展性。類結(jié)構(gòu)如下,

總結(jié)

根據(jù)以上分析和實際測試結(jié)果,第一次同步的時候,兩個方案速率相近,而第一次同步完成之后,新的方案效率就高得多。因為第一次同步的時候,兩種同步方案可能都需要對遠(yuǎn)程的全部文件進(jìn)行拉取。而第一次之后,新的同步方案只需要判斷文件的上次修改時間,因此請求的數(shù)量和所有目錄、子孫目錄的數(shù)量相近(每次至少請求一次目錄下的文件/目錄信息)。實際測試結(jié)果表明,600 個文件同步一次只需要 60s (其中,為避免向服務(wù)器請求過于頻繁,每個文件處理延時時間為 50ms).

以上就是基 Android系統(tǒng)的文件同步設(shè)計思路的分享。

到此這篇關(guān)于基于Android實現(xiàn)的文件同步設(shè)計方案的文章就介紹到這了,更多相關(guān)Android文件同步內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評論