图1为本系统的方法的流程示意图
图2为首次拷贝到X中断需要遍历的文件示意图
图3为非首次拷贝找到X中断点需要遍历的文件示意图
实现的功能
-
支持换机对SD卡的数据迁移
-
大文件按阈值切成切片文件
-
小文件和切片文件按阈值分段拷贝
-
切片文件的恢复
-
小文件和切片文件从换机cache下恢复到三方应用下时的同步删除
-
installd的拷贝、切片、恢复过程的中断
-
installd的拷贝、切片、恢复过程相关信号的重置、未恢复完全的应用数据的清除
-
系统侧对换机线程实时监控、并在换机应用异常退出时触发installd的换机中断流程
-
支持异步查询待换机应用数据的总大小
-
支持异步查询应用数据的恢复进度
优化的效果
-
优化旧手机换机所需保留的空间大小:
最大应用拥有的数据大小空间 ——> 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++; // 递增tarIndexif(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、gidchown(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 errorint 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 segmentpause_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;
}