資料換機

小汀發表於2024-07-05

圖1為本系統的方法的流程示意圖

圖2為首次複製到X中斷需要遍歷的檔案示意圖

圖3為非首次複製找到X中斷點需要遍歷的檔案示意圖

實現的功能

  1. 支援換機對SD卡的資料遷移
  2. 大檔案按閾值切成切片檔案
  3. 小檔案和切片檔案按閾值分段複製
  4. 切片檔案的恢復
  5. 小檔案和切片檔案從換機cache下恢復到三方應用下時的同步刪除
  6. installd的複製、切片、恢復過程的中斷
  7. installd的複製、切片、恢復過程相關訊號的重置、未恢復完全的應用資料的清除
  8. 系統側對換機執行緒實時監控、並在換機應用異常退出時觸發installd的換機中斷流程
  9. 支援非同步查詢待換機應用資料的總大小
  10. 支援非同步查詢應用資料的恢復進度

最佳化的效果

  • 最佳化舊手機換機所需保留的空間大小:
最大應用擁有的資料大小空間 ——> 1個切片的大小空間
  • 最佳化新手機換機所需保留的空間大小:
兩倍最大應用擁有的資料大小空間 ——> 最大應用擁有的資料+1個切片的大小空間
  • 最佳化複製過程:
無法中斷、無法獲知複製進度 ——> 可中斷、可繼續、可知進度
  • 最佳化恢復過程
無法中斷、無法獲知恢復進度 ——> 可中斷、可繼續、可知進度
  • 最佳化異常情況:
換機異常退出時無法發出中斷指令 ——> 可智慧中斷後臺任務
換機恢復時中斷會導致當前應用資料不完整 ——> 可清除恢復不完整的應用資料

需複製的使用者資料目錄

  • 使用者資料和快取
/data/data/ + 包名 (即data/user/0)
/data/user_de/0/ + 包名
  • sdcard儲存
/data/media/0/Android/data/ + 包名(/mnt/user/0/emulated/0/Android/data/ )
/data/media/0/ + 應用自定義名字
/data/media/0/Download/ + 應用自定義名字
(/sdcard = = /data/media/0 = = /storage/emulated/0 但訪問許可權有些許不同)
frameworks/base倉庫:
frameworks/base/core/java/android/app/IActivityManager.aidl(暴露監控換機程序的介面)
frameworks/base/core/java/android/content/pm/ApplicationInfo.java(系統介面除錯限制開啟isAllowedToUseHiddenApis)
frameworks/base/core/java/android/content/pm/IPackageManager.aidl(暴露pms介面)
frameworks/base/core/java/android/os/IInstalld.aidl(暴露installd介面)
frameworks/base/services/core/java/android/os/IInstalld.aidl(暴露installd介面,和上面那個檔案內容一模一樣)
frameworks/base/services/core/java/com/android/server/pm/Installer.java(封裝native層的真正功能函式)
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java(用於實現換機程序監控的功能)
frameworks/base/services/core/java/com/android/server/pm/IPackageManagerBase.java(繼承IPackageManager.aidl,包裝pms函式)
frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java(呼叫Installer.java的函式)

frameworks/native倉庫:
frameworks/native/cmds/installd/binder/android/os/IInstalld.aidl(暴露installd介面)
frameworks/native/cmds/installd/InstalldNativeService.h(用於宣告的標頭檔案)
frameworks/native/cmds/installd/InstalldNativeService.cpp(對接實現Installer.java的封裝介面)
frameworks/native/cmds/installd/slice.h(用於宣告的標頭檔案)(utils.h)
frameworks/native/cmds/installd/slice.cpp(被InstalldNativeService.cpp呼叫的真正功能實現)(utils.cpp)

主要介面功能簡述

  提供兩個主要介面,copy_files_staged、copy_files_restore,分別負責使用者資料的分段複製和使用者資料的恢復,引數是src、dst作為源路徑和目標路徑,還有單次複製閾值比如100Mb。
  • copy_files_staged:
  複製取換機應用的uid和gid作為複製檔案的新uid和gid,使用廣度優先遍歷進入到上一次複製中斷的路徑位置,然後使用深度優先遍歷的方式遍歷二叉樹目錄結構進行複製,並不斷累計當前複製檔案的大小與單次複製閾值比較。一旦當前檔案大小加上已複製檔案大小總量遇到超過單次複製閾值後,記錄當前檔案的路徑,同時判斷當前檔案是否為超過單次複製閾值的超大檔案,是則使用二進位制輸入輸出流的方式將其切割為n個單次複製閾值大小的切片檔案並用字尾特殊標記。那麼本次呼叫結束並return 返回值提示應用可再次呼叫此分段複製函式。
  • copy_files_restore:
  新應用安裝好後,恢復先判斷dst是/data/data下還是/sdcard下,因為安裝完使用者不開啟應用就不會建立sdcard下的資料目錄,dst就實際不存在。/data/data下取dst的uid和gid,/sdcard下取/sdcard/Download/的uid和gid,作為新恢復檔案的uid和gid。透過深度遍歷訪問檔案樹,將正常檔案恢復到對應目錄下,將同一目錄下屬於同一個超大檔案的切片檔案取出以輸入輸出流的方式合併恢復為原檔案。

主要流程描述

  請參閱圖1,本系統提供了一種樹形目錄結構資料的分段複製方法,可以在所有上層為虛擬樹形目錄結構的系統中使用,此處以智慧手機終端之間的資料傳輸詳解實施方式。如圖1所示本系統方法的流程示意圖
1)在使用者開始傳輸前,獲取和計算出本次傳輸前複製所需的引數資訊。
  在使用者進入傳輸選擇頁面後,後臺進行計算當前裝置的剩餘可用空間大小“freeSize”,選擇待傳輸的資料後,計算出待複製資料大小“copySize”,然後根據自定義演算法公式 OnceCopy = Min(Max(Min(copySize / 20, 2048), 300), freeSize)計算出在300Mb到2048Mb(2Gb)之間且低於使用者剩餘空間的合適的單次複製量“OnceCopy”的具體值。啟動傳輸,此時“資料根目錄”、“目標目錄”、“單次複製量”作為3個引數傳入底層介面。
2)開始傳輸先進行首次分段複製
  複製取“目標目錄”的uid和gid作為複製檔案新的uid和gid,使用深度優先遍歷的方式遍歷二叉樹目錄結構進行複製,並不斷累計當前複製檔案的大小與單次複製閾值比較。如圖2所示的首次複製到X中斷需要遍歷的檔案示意圖。一旦當前檔案大小加上已複製檔案大小總量遇到超過單次複製閾值後,記錄當前檔案的路徑和唯一的索引節點st_ino,同時判斷當前檔案是否為超過單次複製閾值的超大檔案,是則將“遇到大檔案”的訊號量bigfileNow置為true,然後return並提示需再次呼叫此分段複製函式;否則直接return並提示需再次呼叫此分段複製函式。
3)分段資料產生後同時開始非同步傳輸
  此時被首次複製出的分段樹目錄資料開始進行wifi-p2p傳輸到新的裝置,並非同步開始進行第二次分段複製來生成下一次資料傳輸所需的分段資料。經研究分段複製和wifi-p2p傳輸兩者速度均可達到80Mb/s,兩者非同步進行,複製的時間消耗將被傳輸所掩蓋,效果上形似連續傳輸,不會給使用者帶來割裂感。每次分段的資料被傳輸一部分就同時刪除一部分,在下次複製開始前就會將前一次的臨時複製資料完全清除,保證整個傳輸過程所需空間只有一個“單次複製量”OnceCopy的大小。
4)首次複製資料傳輸開始時同時開始下一段複製
  第n次(n>1)開始複製時,將使用貪心演算法逐層匹配上一次記錄下的斷點檔案所屬資料夾,並逐級開啟,使用廣度優先遍歷進入到上一次複製中斷的路徑位置,並透過上次記錄下的斷點檔案的唯一的索引節點st_ino值進行確認。如圖3所示的非首次複製找到X中斷點需要遍歷的檔案示意圖。之後開始從此處檔案開始複製此檔案。如果“遇到大檔案”的訊號量bigfileNow為true,則認定當前為大檔案,使用二進位制輸入輸出流的方式將其切割出1個單次複製閾值大小的切塊檔案命名為”原檔名字+特殊字尾+序號“來標記,並將此切塊檔案存放於待傳輸的已複製目錄中,作為下一次傳輸的物件,並記錄下此時屬於第幾個切塊。
重複以上操作到當前大檔案切至最後一個切塊後,將“遇到大檔案”的訊號量bigfileNow置為false。
重複傳輸和複製直到待複製資料完全被分段傳輸完成,return並提示複製成功。
5)當遇到複製失敗或傳輸失敗
  如果在複製和傳輸過程中因傳輸手機、傳送手機、失敗則會return並提示複製異常,呼叫方可選擇嘗試再次複製,並從已經記錄到系統中的斷點檔案開始繼續複製和傳輸,以模擬p2p形式的高速續傳。(這裡強調區分於現有技術的三方伺服器和使用者終端之間的基於資料網路的低速續傳,此種續傳受資料網路速度和三方伺服器的限制,且斷點是出現在傳輸過程中,但本實現的斷點是產生於複製過程中)
6)資料在新裝置的恢復
  取“目標目錄”的uid和gid作為恢復檔案新的uid和gid。透過深度遍歷訪問檔案樹,將正常檔案恢復到對應目錄下,將同一目錄下屬於同一個超大檔案的切片檔案相對路徑取出存於vector容器,以輸入輸出流的方式按序合併恢復為原檔案。此時使用者即可在新應用許可權下正常使用傳輸得到的新資料。

技術關鍵

大檔案切片

int copy_bigfile_split(const char *src, char *dst, const struct stat64 *pSrcStat, uint options, int uid, int gid,long long splitSize) {
    /* open src
    /* ...
    /* open dest
    /* ...
    */
    int src_fd, dst_fd, stat_result;
    bool foundSlice = false;
    bool lastSlice = false;
    long long remainingBytes = fileSize - lastCopiedSize;
    long long bytesToCopy = std::min(splitSize, remainingBytes);
    std::vector<char> buffer(bytesToCopy);
    curBigFile.read(buffer.data(),bytesToCopy);
    std::streamsize bytesRead = curBigFile.gcount();
    outputFile.write(buffer.data(), bytesRead);
    outputFile.close();
    curBigFile.close();
    std::string rn = dst + std::string(".flyme.slice.");
    ALOGD("rename to rn :  %s", rn.c_str());
    rn.append(tarIndex + 1, '0');
    std::rename(dst ,rn.c_str());
    lastCopiedSize += bytesRead;
    tarIndex++; // 遞增tarIndex
    if(remainingBytes <= splitSize){
        lastSlice = true;
    }else{
        foundSlice = true;
    }
    int mode = 0;
    if (strstr(dst, FLYME_BACKUP_POSTFIX)) {
        mode = 1;
    }
    if((uid > AID_APP && gid > AID_APP) || mode > 0) {
        if(strcmp(dst, FLYME_DATA_DATA) != 0){
            if(fchown(dst_fd, uid, gid) < 0 ) {
                ALOGE("slice copy big file split fchown fail");
            }
        }
    }
    (void) close(src_fd);
    (void) close(dst_fd);
    if(lastSlice){
        ALOGD("slice is lastSlice return 1");
        return 1;
    }else if (foundSlice){
        ALOGD("slice is foundSlice return 0");
        return 0;
    }
    return 0;
}

切片檔案取出與合併

bool restore_split_data(const std::string& srcPath,const std::string& targetPath,const std::string& splitListFilePath, int uid, int gid){
        ALOGD("slice restore_split_data");
        std::regex sliceRegex(".flyme.slice.");
        std::vector<std::string> sliceFiles;
        std::string delim = "/";
        std::string saveSliceFile = "";
        std::string curSliceFile;
        bool mergeFileResult;
        // 獲取本目錄下屬於同一個大檔案的切片檔案列表
        for (const auto& entry : std::filesystem::directory_iterator(srcPath))
        {
            if(std::regex_search(entry.path().string(), sliceRegex)){
                //找到.flyme.slice.字串的位置
                int sliceIndex = entry.path().string().find("flyme.slice",SDCARD_DATAMIGRATION_LENGTH);
                //取出切片檔名
                int index_end = entry.path().string().find_last_of(delim);
                curSliceFile = entry.path().string().substr(index_end+1,sliceIndex-index_end-2);
                //判斷當前entry為屬於同一個大檔案的切片檔案,塞到vector容器sliceFiles中
                //if ...
                sliceFiles.push_back(entry.path().string());
                }
            }
        }
        mergeFileResult = mergeSplitFile(srcPath,targetPath,curSliceFile,saveSliceFile,sliceFiles,uid,gid)
        sliceFiles.clear();
        return true;
}

bool mergeSplitFile(const std::string& srcPath,const std::string& targetPath,std::string curSliceFile, std::string saveSliceFile, std::vector<std::string> sliceFiles, int uid ,int gid){
    // 按照切片檔案的序號排序
    std::sort(sliceFiles.begin(), sliceFiles.end());
    std::string mergedFilePath = targetPath + "/" + saveSliceFile;
    // 合併切片檔案中的內容
    std::ofstream mergedFile(mergedFilePath, std::ios::binary);
    for (const std::string& sliceFile : sliceFiles) {
        //check mergedFile.is_open()
        // ...
        std::string tempSliceDir = srcPath + "/" + std::filesystem::path(sliceFile).filename().string();
        // 讀取切片檔案中的內容並寫入合併檔案
        std::ifstream sliceFileStream(tempSliceDir);
        mergedFile << sliceFileStream.rdbuf();
        sliceFileStream.close();
        // 刪除臨時切片目錄
        ALOGD("slice mergedFilePath :'%s', restored Slicefiles and removefile is '%s',\n", mergedFilePath.c_str(), tempSliceDir.c_str());
        std::remove(tempSliceDir.c_str());
    }
    mergedFile.close();
    sliceFiles.clear();
    ALOGD("slice changeBigFileUgid uid : %d , gid : %d ,curSliceFile : %s",uid,gid,curSliceFile.c_str());
    //最後改變生成檔案的uid、gid
    chown(mergedFilePath.c_str(), uid, gid);
    return true;
}

檔案分段複製

兩函式互相呼叫對檔案樹深度遍歷,單函式內迴圈進行廣度遍歷,複製達到閾值或碰到大檔案後打斷並記錄斷點,返回訊號值提示應用端重新呼叫介面以繼續複製下一段資料。
int copy_directory_staged(const char *src, const char *dst, uint options, int uid, int gid) {
    int ret_val = FLAG_SUCCEED_BACKUP;
    struct stat64 dst_stat;
    DIR *dir;
    int stat_result;
    //ALOGD("slice copy_directory_staged %d  %d \n", uid, gid);
    if (is_restore || first_call_copy || find_first_file) {//廣度優先尋找斷點
        stat_result = stat64(dst, &dst_stat);
        //對stat_result結果做一些error判斷
        //...
        if (stat_result != 0) {
            int mkdir_result = mkdir(dst, 0755);
        }
    }
    //處理
    // Open the directory, and plow through its contents.
    dir = opendir(src);
    if (dir == nullptr) {
        ALOGD("slice acp: unable to open directory '%s': %s\n", src, strerror(errno));
        return FLAG_FAILED_BACKUP;
    }
    while (true) {//單函式內迴圈進行廣度遍歷
        struct dirent* ent;
        char* src_file;
        char* dst_file;
        int src_len, dst_len, name_len;
        ent = readdir(dir);
        if (ent == nullptr)
            break;
        if (strcmp(ent->d_name, "." ) == 0 || strcmp(ent->d_name, "..") == 0) {
            continue;
        }
        name_len = strlen(ent->d_name);
        src_len = strlen(src);
        dst_len = strlen(dst);
        src_file = (char*) malloc(src_len + 1 + name_len + 1);
        memcpy(src_file, src, src_len);
        src_file[src_len] = FSEP;
        memcpy(src_file + src_len + 1, ent->d_name, name_len + 1);
        dst_file = (char*)malloc(dst_len + 1 + name_len + 1);
        memcpy(dst_file, dst, dst_len);
        dst_file[dst_len] = FSEP;
        memcpy(dst_file + dst_len + 1, ent->d_name, name_len + 1);
        
        if (!is_find_file){//廣度優先尋找斷點
            if (is_pause_dir_contain(src,pause_st_dir) || first_call_copy){
                ret_val = copy_file_recursive_staged(src_file, dst_file, options, uid, gid);
                    free(src_file);
                    free(dst_file);
                    break;
                }
            } else {
                continue;
            }
        } else {
            //兩函式互相呼叫對檔案樹深度遍歷
            ret_val = copy_file_recursive_staged(src_file, dst_file, options, uid, gid);
            logger.slice_fprintf("%s slice ready to pause_st_dir22 src: '%s' \n", timeToString().c_str(),src);
            ALOGD("slice ready to pause_st_dir22 src: '%s' \n", src);
            if (ret_val != FLAG_SUCCEED_BACKUP) {
                free(src_file);
                free(dst_file);
                logger.slice_fprintf("%s slice ready to jump outof22 src: '%s' \n", timeToString().c_str(),src);
                ALOGD("slice ready to jump outof22 src: '%s' \n", src);
                break;
            }
        }
        free(src_file);
        free(dst_file);
    }
    int mode = 0;
    if (strstr(dst, FLYME_BACKUP_POSTFIX)) {
        mode = 1;
    }
    if ((uid > AID_APP && gid > AID_APP) || mode > 0) {
        if (strcmp(dst, FLYME_DATA_DATA) != 0){
            if (chown(dst, uid, gid) < 0 ) {
                ALOGE("slice copy dir chown from '%s' to '%s' fail\n", src, dst);
            }
        }
    }
    closedir(dir);
    return ret_val;
}

int copy_file_recursive_staged(const char* src, char* dst,
        unsigned int options, const int uid, const int gid) {
    //add isRestore for compile error
    int ret_val = FLAG_SUCCEED_BACKUP;
    int split_val = 0;
    struct stat64 src_stat;
    int stat_result, stat_error;
    stat_result = lstat64(src, &src_stat);
    if (S_ISDIR(src_stat.st_mode)) {
        //兩函式互相呼叫對檔案樹深度遍歷
        ret_val = copy_directory_staged(src, dst, options, uid, gid);
        chown(dst, uid, gid);
    } else if (S_ISREG(src_stat.st_mode)) {
        if (is_restore){
            //處理是否是在走恢復流程
            //...
        }
        //同時處理遇到大檔案的情況
        // ...
        if (pause_st_ino == 0 || pause_st_ino == src_stat.st_ino) {
            if (find_first_file == false) {
                //廣度優先尋找斷點則需要對目錄進行最少的建立,不反覆建立已經前面傳輸過的目錄
                createDirectory(dst, uid, gid);
                find_first_file = true;
        }
        ////區分繼續切大檔案還是走正常檔案流程
        if (bfconflag == FLAG_BIG_FILE_SLICE_FINISH && pause_st_ino == src_stat.st_ino){
            bfconflag = FLAG_BIG_FILE_SLICE_SWITCH;
        } else {
        //When restart_copy is used to copy pause_st_ino files, the sequence copy can also enter the if logical segment
            pause_st_ino = 0;//保證restart_copy複製時,複製完pause_st_ino檔案後序複製也能正常走進此if邏輯段中
            current_size += src_stat.st_size;
            ALOGD("slice pause_st_ino is '%d', src is '%s', st_size is '%ld', st_ino is '%d', current_size is '%ld'\n", pause_st_ino, src, src_stat.st_size, src_stat.st_ino, current_size);
            if (MAX_ONCE_COPY < current_size) {
                ALOGD("slice now MAX_ONCE_COPY < current_size skipping copy '%s'\n",src);
                pause_st_ino = src_stat.st_ino;
                restartflag = FLAG_RESTART_BACKUP;
                ret_val = FLAG_RESTART_BACKUP;
                if (MAX_ONCE_COPY < src_stat.st_size) {
                    //同時處理遇到大檔案的情況
                    // ...
                    split_val = copy_bigfile_split(src, dst, &src_stat, options, uid, gid,MAX_ONCE_COPY);
                    ret_val = FLAG_RESTART_BACKUP;
                }
                return ret_val;
            }
            ret_val = copy_regular64(src, dst, &src_stat, options, uid, gid);
        } else {//尚未迴圈到上次中斷的點,不進行實際複製,預設已經複製成功
                ALOGD("slice ret_val is '%d', jump this file copy and set ret_val = FLAG_SUCCEED_BACKUP\n", ret_val);
        }
    } else {
        ALOGD("slice skipping unusual file '%s' (mode=0%o)\n", src, src_stat.st_mode);
    }
    return ret_val;
}


bool is_pause_dir_contain(const char *src, std::string pause_st_dir){
    //1、src為傳進來的遞迴目錄,pause_st_dir為當前儲存的切片包的最後檔案
    //2、取src的/或者\ 分隔符後的目錄格式個數,如a/b/c/ 為3個,再從pause_st_dir中擷取相同路徑個數進行比較
    //3、若不同,則返回false,給上面遞迴的地方作判斷用,false則continue;若相同,則返回true,給上面遞迴的地方讓其繼續執行遞迴,相應的下次src會增加一級目錄了,返回true前,執行擷取路徑個數count++
    count = c_in_str(src,FSEP);
    std::string cut_dir = cut_out_n_string(pause_st_dir,"/",count+1);
    int res = strcmp(cut_dir.c_str(),src);
    if (res == 0){
        count++;
        return true;
    }
    ALOGD("slice is_pause_dir_contain count is %d,cut_dir is %s,src is %s,pause_st_dir is %s",count,cut_dir.c_str(),src,pause_st_dir.c_str());
    return false;
}

相關文章