最近处理一个Android 11大文件复制时间进度栏卡在100%的问题,处理过程中梳理了一下文件复制流程。
首先,根据UI找到复制文件的代码入口:
逻辑代码在DirectoryFragment.java→handleMenuItemClick()方法当中(别问是怎么找到这个类里面的,问就是基本操作)
private boolean handleMenuItemClick(MenuItem item) {
if (mInjector.pickResult != null) {
mInjector.pickResult.increaseActionCount();
}
MutableSelection<String> selection = new MutableSelection<>();
mSelectionMgr.copySelection(selection);
//以下会根据用户选择的操作执行对应的功能,如复制、移动、删除、查看、重命名等
switch (item.getItemId()) {
case R.id.action_menu_select:
case R.id.dir_menu_open:
openDocuments(selection);
mActionModeController.finishActionMode();
return true;
case R.id.action_menu_open_with:
case R.id.dir_menu_open_with:
showChooserForDoc(selection);
return true;
case R.id.dir_menu_open_in_new_window:
mActions.openSelectedInNewWindow();
return true;
case R.id.action_menu_share:
case R.id.dir_menu_share:
mActions.shareSelectedDocuments();
return true;
case R.id.action_menu_delete:
case R.id.dir_menu_delete:
// deleteDocuments will end action mode if the documents are deleted.
// It won't end action mode if user cancels the delete.
mActions.deleteSelectedDocuments();
return true;
case R.id.action_menu_copy_to:
//执行复制操作
transferDocuments(selection, null, FileOperationService.OPERATION_COPY);
// TODO: Only finish selection mode if copy-to is not canceled.
// Need to plum down into handling the way we do with deleteDocuments.
mActionModeController.finishActionMode();
return true;
case R.id.action_menu_compress:
transferDocuments(selection, mState.stack,
FileOperationService.OPERATION_COMPRESS);
// TODO: Only finish selection mode if compress is not canceled.
// Need to plum down into handling the way we do with deleteDocuments.
mActionModeController.finishActionMode();
return true;
// TODO: Implement extract (to the current directory).
case R.id.action_menu_extract_to:
transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT);
// TODO: Only finish selection mode if compress-to is not canceled.
// Need to plum down into handling the way we do with deleteDocuments.
mActionModeController.finishActionMode();
return true;
case R.id.action_menu_move_to:
if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) {
mInjector.dialogs.showOperationUnsupported();
return true;
}
// Exit selection mode first, so we avoid deselecting deleted documents.
mActionModeController.finishActionMode();
transferDocuments(selection, null, FileOperationService.OPERATION_MOVE);
return true;
case R.id.action_menu_inspect:
case R.id.dir_menu_inspect:
mActionModeController.finishActionMode();
assert selection.size() <= 1;
DocumentInfo doc = selection.isEmpty()
? mActivity.getCurrentDirectory()
: mModel.getDocuments(selection).get(0);
mActions.showInspector(doc);
return true;
case R.id.dir_menu_cut_to_clipboard:
mActions.cutToClipboard();
return true;
case R.id.dir_menu_copy_to_clipboard:
mActions.copyToClipboard();
return true;
case R.id.dir_menu_paste_from_clipboard:
pasteFromClipboard();
return true;
case R.id.dir_menu_paste_into_folder:
pasteIntoFolder();
return true;
case R.id.action_menu_select_all:
case R.id.dir_menu_select_all:
mActions.selectAllFiles();
return true;
case R.id.action_menu_rename:
case R.id.dir_menu_rename:
// Exit selection mode first, so we avoid deselecting deleted
// (renamed) documents.
mActionModeController.finishActionMode();
renameDocuments(selection);
return true;
case R.id.dir_menu_create_dir:
mActions.showCreateDirectoryDialog();
return true;
case R.id.dir_menu_view_in_owner:
mActions.viewInOwner();
return true;
case R.id.action_menu_sort:
mActions.showSortDialog();
return true;
default:
if (DEBUG) {
Log.d(TAG, "Unhandled menu item selected: " + item);
}
return false;
}
}
此方法根据用户的操作选择执行不同的功能,如复制、移动、删除、查看、重命名等,这里我们看的是复制的代码,查看transferDocuments(selection, null, FileOperationService.OPERATION_COPY)里面的逻辑
private void transferDocuments(
final Selection<String> selected, @Nullable DocumentStack destination,
final @OpType int mode) {
if (selected.isEmpty()) {
return;
}
switch (mode) {
case FileOperationService.OPERATION_COPY:
Metrics.logUserAction(MetricConsts.USER_ACTION_COPY_TO);
break;
case FileOperationService.OPERATION_COMPRESS:
Metrics.logUserAction(MetricConsts.USER_ACTION_COMPRESS);
break;
case FileOperationService.OPERATION_EXTRACT:
Metrics.logUserAction(MetricConsts.USER_ACTION_EXTRACT_TO);
break;
case FileOperationService.OPERATION_MOVE:
Metrics.logUserAction(MetricConsts.USER_ACTION_MOVE_TO);
break;
}
UrisSupplier srcs;
try {
ClipStore clipStorage = DocumentsApplication.getClipStore(getContext());
srcs = UrisSupplier.create(selected, mModel::getItemUri, clipStorage);
} catch (IOException e) {
throw new RuntimeException("Failed to create uri supplier.", e);
}
final DocumentInfo parent = mActivity.getCurrentDirectory();
final FileOperation operation = new FileOperation.Builder()
.withOpType(mode)
.withSrcParent(parent == null ? null : parent.derivedUri)
.withSrcs(srcs)
.build();
if (destination != null) {
operation.setDestination(destination);
final String jobId = FileOperations.createJobId();
mInjector.dialogs.showProgressDialog(jobId, operation);
//启动service,在后台完成复制操作
FileOperations.start(
mActivity,
operation,
mInjector.dialogs::showFileOperationStatus,
jobId);
return;
}
// Pop up a dialog to pick a destination. This is inadequate but works for now.
// TODO: Implement a picker that is to spec.
mLocalState.mPendingOperation = operation;
//启动activity,让用户选择目标路径
final Intent intent = new Intent(
Shared.ACTION_PICK_COPY_DESTINATION,
Uri.EMPTY,
getActivity(),
PickActivity.class);
// Set an appropriate title on the drawer when it is shown in the picker.
// Coupled with the fact that we auto-open the drawer for copy/move operations
// it should basically be the thing people see first.
int drawerTitleId;
switch (mode) {
case FileOperationService.OPERATION_COPY:
drawerTitleId = R.string.menu_copy;
break;
case FileOperationService.OPERATION_COMPRESS:
drawerTitleId = R.string.menu_compress;
break;
case FileOperationService.OPERATION_EXTRACT:
drawerTitleId = R.string.menu_extract;
break;
case FileOperationService.OPERATION_MOVE:
drawerTitleId = R.string.menu_move;
break;
default:
throw new UnsupportedOperationException("Unknown mode: " + mode);
}
intent.putExtra(DocumentsContract.EXTRA_PROMPT, drawerTitleId);
// Model must be accessed in UI thread, since underlying cursor is not threadsafe.
List<DocumentInfo> docs = mModel.getDocuments(selected);
// Determine if there is a directory in the set of documents
// to be copied? Why? Directory creation isn't supported by some roots
// (like Downloads). This informs DocumentsActivity (the "picker")
// to restrict available roots to just those with support.
intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode);
// This just identifies the type of request...we'll check it
// when we reveive a response.
startActivityForResult(intent, REQUEST_COPY_DESTINATION);
}
里面主要是干了两件重要的事儿:
- 启动一个Service,以便复制操作能在后台运行
/**
* Tries to start the activity. Returns the job id.
* @param jobId Optional job id. If null, then it will be auto-generated.
*/
public static String start(Context context, FileOperation operation, Callback callback,
@Nullable String jobId) {
if (DEBUG) {
Log.d(TAG, "Handling generic 'start' call.");
}
String newJobId = jobId != null ? jobId : createJobId();
Intent intent = createBaseIntent(context, newJobId, operation);
if (callback != null) {
callback.onOperationResult(Callback.STATUS_ACCEPTED, operation.getOpType(),
operation.getSrc().getItemCount());
}
context.startService(intent);
return newJobId;
}
/**
* Starts the service for an operation.
*
* @param jobId A unique jobid for this job.
* Use {@link #createJobId} if you don't have one handy.
* @return Id of the job.
*/
public static Intent createBaseIntent(
Context context, String jobId, FileOperation operation) {
Intent intent = new Intent(context, FileOperationService.class);
intent.putExtra(EXTRA_JOB_ID, jobId);
intent.putExtra(EXTRA_OPERATION, operation);
return intent;
}
- 启动了一个Activity,然后获取Activity返回的回调,也就是要拿到用户选择的目标路径
主要看Service里面的逻辑,oncreate()生命周期方法里面主要做了一些必要对象初始化的工作
@Override
public void onCreate() {
// Allow tests to pre-set these with test doubles.
if (executor == null) {
executor = Executors.newFixedThreadPool(POOL_SIZE);
}
if (deletionExecutor == null) {
deletionExecutor = Executors.newCachedThreadPool();
}
if (handler == null) {
// Monitor tasks are small enough to schedule them on main thread.
handler = new Handler();
}
if (foregroundManager == null) {
foregroundManager = createForegroundManager(this);
}
if (notificationManager == null) {
notificationManager = getSystemService(NotificationManager.class);
}
UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
features = new Features.RuntimeFeatures(getResources(), userManager);
setUpNotificationChannel();
if (DEBUG) {
Log.d(TAG, "Created.");
}
mPowerManager = getSystemService(PowerManager.class);
}
onStartCommand()生命周期方法里面执行了复制操作的核心逻辑
@Override
public int onStartCommand(Intent intent, int flags, int serviceId) {
// TODO: Ensure we're not being called with retry or redeliver.
// checkArgument(flags == 0); // retry and redeliver are not supported.
String jobId = intent.getStringExtra(EXTRA_JOB_ID);
assert(jobId != null);
if (DEBUG) {
Log.d(TAG, "onStartCommand: " + jobId + " with serviceId " + serviceId);
}
if (intent.hasExtra(EXTRA_CANCEL)) {
handleCancel(intent);
} else {
FileOperation operation = intent.getParcelableExtra(EXTRA_OPERATION);
//复制文件操作在这里完成
handleOperation(jobId, operation);
}
// Track the service supplied id so we can stop the service once we're out of work to do.
mLastServiceId = serviceId;
return START_NOT_STICKY;
}
主要在handleOperation()方法里面
private void handleOperation(String jobId, FileOperation operation) {
synchronized (mJobs) {
if (mWakeLock == null) {
mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
}
if (mJobs.containsKey(jobId)) {
Log.w(TAG, "Duplicate job id: " + jobId
+ ". Ignoring job request for operation: " + operation + ".");
return;
}
//创建复制任务
Job job = operation.createJob(this, this, jobId, features);
if (job == null) {
return;
}
assert (job != null);
if (DEBUG) {
Log.d(TAG, "Scheduling job " + job.id + ".");
}
Future<?> future = getExecutorService(operation.getOpType()).submit(job);
//启动任务
mJobs.put(jobId, new JobRecord(job, future));
// Acquire wake lock to keep CPU running until we finish all jobs. Acquire wake lock
// after we create a job and put it in mJobs to avoid potential leaking of wake lock
// in case where job creation fails.
mWakeLock.acquire();
}
}
handleOperation()里面执行了FileOperation的createJob()方法创建了一个Job对象,FileOperation是一个抽象类,描述了文件的一些操作行为,它有4个子类:
- CopyOperation:复制
- MoveDeleteOperation:移动删除
- ExtractOperation:解压
- CompressOperation:压缩
这里复制操作创建的是CopyOperation对象,相应的createJob()创建的是CopyJob对象,因为CopyJob实现了Runnable接口,所以可以将这一任务放到线程池里执行,通过ExecutorService.submit()开始执行复制操作,submit()执行后会执行CopyJob里面的run()方法,我们主要看CopyJob里面的run()方法的逻辑
@Override
public final void run() {
if (isCanceled()) {
// Canceled before running
return;
}
mState = STATE_STARTED;
listener.onStart(this);
try {
//准备配置
boolean result = setUp();
if (result && !isCanceled()) {
mState = STATE_SET_UP;
//开始复制
start();
}
} catch (RuntimeException e) {
// No exceptions should be thrown here, as all calls to the provider must be
// handled within Job implementations. However, just in case catch them here.
Log.e(TAG, "Operation failed due to an unhandled runtime exception.", e);
Metrics.logFileOperationErrors(operationType, failedDocs, failedUris);
} finally {
mState = (mState == STATE_STARTED || mState == STATE_SET_UP) ? STATE_COMPLETED : mState;
finish();
listener.onFinished(this);
// NOTE: If this details is a JumboClipDetails, and it's still referred in primary clip
// at this point, user won't be able to paste it to anywhere else because the underlying
mResourceUris.dispose();
}
}
可以看到先执行了setUp(),这里主要是为复制前做一些准备工作,比如创建CopyJobProgressTracker对象用来更新复制进度,同时检查磁盘剩余空间是否足够,setUp()执行完后开始执行start()方法,这里面开始执行复制操作
@Override
void start() {
mProgressTracker.start();
DocumentInfo srcInfo;
for (int i = 0; i < mResolvedDocs.size() && !isCanceled(); ++i) {
srcInfo = mResolvedDocs.get(i);
if (DEBUG) {
Log.d(TAG,
"Copying " + srcInfo.displayName + " (" + srcInfo.derivedUri + ")"
+ " to " + mDstInfo.displayName + " (" + mDstInfo.derivedUri + ")");
}
try {
// Copying recursively to itself or one of descendants is not allowed.
if (mDstInfo.equals(srcInfo) || isDescendentOf(srcInfo, mDstInfo)) {
Log.e(TAG, "Skipping recursive copy of " + srcInfo.derivedUri);
onFileFailed(srcInfo);
} else {
//执行复制,更新进度
processDocumentThenUpdateProgress(srcInfo, null, mDstInfo);
}
} catch (ResourceException e) {
Log.e(TAG, "Failed to copy " + srcInfo.derivedUri, e);
onFileFailed(srcInfo);
}
}
Metrics.logFileOperation(operationType, mResolvedDocs, mDstInfo);
}
start()方法里面通过for循环来复制所有文件,因为用户可能选择了多个文件,每个文件通过processDocumentThenUpdateProgress()方法来复制的
private void processDocumentThenUpdateProgress(DocumentInfo src, DocumentInfo srcParent,
DocumentInfo dstDirInfo) throws ResourceException {
processDocument(src, srcParent, dstDirInfo);
mProgressTracker.onDocumentCompleted();
}
processDocumentThenUpdateProgress()里面就两行代码,processDocument()是执行复制操作,该方法执行完后通过mProgressTracker.onDocumentCompleted()更新完成进度,看下processDocument()里面的逻辑
/**
* Copies a the given document to the given location.
*
* @param src DocumentInfos for the documents to copy.
* @param srcParent DocumentInfo for the parent of the document to process.
* @param dstDirInfo The destination directory.
* @throws ResourceException
*
* TODO: Stop passing srcParent, as it's not used for copy, but for move only.
*/
void processDocument(DocumentInfo src, DocumentInfo srcParent,
DocumentInfo dstDirInfo) throws ResourceException {
// TODO: When optimized copy kicks in, we'll not making any progress updates.
// For now. Local storage isn't using optimized copy.
// When copying within the same provider, try to use optimized copying.
// If not supported, then fallback to byte-by-byte copy/move.
if (src.authority.equals(dstDirInfo.authority)) {
if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {
try {
//使用优化复制方式
if (DocumentsContract.copyDocument(wrap(getClient(src)), src.derivedUri,
dstDirInfo.derivedUri) != null) {
Metrics.logFileOperated(operationType, MetricConsts.OPMODE_PROVIDER);
return;
}
} catch (FileNotFoundException | RemoteException | RuntimeException e) {
Log.e(TAG, "Provider side copy failed for: " + src.derivedUri
+ " due to an exception.", e);
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_QUICK_COPY, src.derivedUri);
}
// If optimized copy fails, then fallback to byte-by-byte copy.
if (DEBUG) {
Log.d(TAG, "Fallback to byte-by-byte copy for: " + src.derivedUri);
}
}
}
// If we couldn't do an optimized copy...we fall back to vanilla byte copy.
//使用字节复制的方式
byteCopyDocument(src, dstDirInfo);
}
这里面提供了两种方式来实现复制,一种是优化后的,一种是通过byteCopyDocument()复制字节的方式来实现,这里通过log日志发现是通过第二种方式来实现的,看下byteCopyDocument()的逻辑
void byteCopyDocument(DocumentInfo src, DocumentInfo dest) throws ResourceException {
final String dstMimeType;
final String dstDisplayName;
if (DEBUG) {
Log.d(TAG, "Doing byte copy of document: " + src);
}
//以下读取源文件的属性
// If the file is virtual, but can be converted to another format, then try to copy it
// as such format. Also, append an extension for the target mime type (if known).
if (src.isVirtual()) {
String[] streamTypes = null;
try {
streamTypes = src.userId.getContentResolver(service).getStreamTypes(src.derivedUri,
"*/*");
} catch (RuntimeException e) {
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OBTAIN_STREAM_TYPE, src.derivedUri);
throw new ResourceException(
"Failed to obtain streamable types for %s due to an exception.",
src.derivedUri, e);
}
if (streamTypes != null && streamTypes.length > 0) {
dstMimeType = streamTypes[0];
final String extension = MimeTypeMap.getSingleton().
getExtensionFromMimeType(dstMimeType);
dstDisplayName = src.displayName +
(extension != null ? "." + extension : src.displayName);
} else {
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OBTAIN_STREAM_TYPE, src.derivedUri);
throw new ResourceException("Cannot copy virtual file %s. No streamable formats "
+ "available.", src.derivedUri);
}
} else {
dstMimeType = src.mimeType;
dstDisplayName = src.displayName;
}
// Create the target document (either a file or a directory), then copy recursively the
// contents (bytes or children).
Uri dstUri = null;
try {
dstUri = DocumentsContract.createDocument(
wrap(getClient(dest)), dest.derivedUri, dstMimeType, dstDisplayName);
} catch (FileNotFoundException | RemoteException | RuntimeException e) {
if (e instanceof DeadObjectException) {
releaseClient(dest);
}
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_CREATE_DOCUMENT, dest.derivedUri);
throw new ResourceException(
"Couldn't create destination document " + dstDisplayName + " in directory %s "
+ "due to an exception.", dest.derivedUri, e);
}
if (dstUri == null) {
// If this is a directory, the entire subdir will not be copied over.
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_CREATE_DOCUMENT, dest.derivedUri);
throw new ResourceException(
"Couldn't create destination document " + dstDisplayName + " in directory %s.",
dest.derivedUri);
}
DocumentInfo dstInfo = null;
try {
dstInfo = DocumentInfo.fromUri(dest.userId.getContentResolver(service), dstUri,
dest.userId);
} catch (FileNotFoundException | RuntimeException e) {
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_QUERY_DOCUMENT, dstUri);
throw new ResourceException("Could not load DocumentInfo for newly created file %s.",
dstUri);
}
//如果是文件夹,则执行copyDirectoryHelper(),如果是文件,则执行copyFileHelper()
if (Document.MIME_TYPE_DIR.equals(src.mimeType)) {
copyDirectoryHelper(src, dstInfo);
} else {
copyFileHelper(src, dstInfo, dest, dstMimeType);
}
}
前面主要是读取了源文件的属性,复制操作主要在copyFileHelper()和copyDirectoryHelper(),这两个方法的区别在于,一个是复制文件,一个是复制文件夹,复制文件夹采用了递归的方式,本质还是复制文件,看一下copyFileHelper()的逻辑
private void copyFileHelper(DocumentInfo src, DocumentInfo dest, DocumentInfo destParent,
String mimeType) throws ResourceException {
AssetFileDescriptor srcFileAsAsset = null;
ParcelFileDescriptor srcFile = null;
ParcelFileDescriptor dstFile = null;
InputStream in = null;
ParcelFileDescriptor.AutoCloseOutputStream out = null;
boolean success = false;
try {
// If the file is virtual, but can be converted to another format, then try to copy it
// as such format.
if (src.isVirtual()) {
try {
srcFileAsAsset = getClient(src).openTypedAssetFileDescriptor(
src.derivedUri, mimeType, null, mSignal);
} catch (FileNotFoundException | RemoteException | RuntimeException e) {
if (e instanceof DeadObjectException) {
releaseClient(src);
}
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OPEN_FILE, src.derivedUri);
throw new ResourceException("Failed to open a file as asset for %s due to an "
+ "exception.", src.derivedUri, e);
}
srcFile = srcFileAsAsset.getParcelFileDescriptor();
try {
in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);
} catch (IOException e) {
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OPEN_FILE, src.derivedUri);
throw new ResourceException("Failed to open a file input stream for %s due "
+ "an exception.", src.derivedUri, e);
}
Metrics.logFileOperated(operationType, MetricConsts.OPMODE_CONVERTED);
} else {
try {
srcFile = getClient(src).openFile(src.derivedUri, "r", mSignal);
} catch (FileNotFoundException | RemoteException | RuntimeException e) {
if (e instanceof DeadObjectException) {
releaseClient(src);
}
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OPEN_FILE, src.derivedUri);
throw new ResourceException(
"Failed to open a file for %s due to an exception.", src.derivedUri, e);
}
in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);
Metrics.logFileOperated(operationType, MetricConsts.OPMODE_CONVENTIONAL);
}
try {
dstFile = getClient(dest).openFile(dest.derivedUri, "w", mSignal);
} catch (FileNotFoundException | RemoteException | RuntimeException e) {
if (e instanceof DeadObjectException) {
releaseClient(dest);
}
Metrics.logFileOperationFailure(
appContext, MetricConsts.SUBFILEOP_OPEN_FILE, dest.derivedUri);
throw new ResourceException("Failed to open the destination file %s for writing "
+ "due to an exception.", dest.derivedUri, e);
}
out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);
try {
// If we know the source size, and the destination supports disk
// space allocation, then allocate the space we'll need. This
// uses fallocate() under the hood to optimize on-disk layout
// and prevent us from running out of space during large copies.
//获取StorageManager
final StorageManager sm = service.getSystemService(StorageManager.class);
final long srcSize = srcFile.getStatSize();
final FileDescriptor dstFd = dstFile.getFileDescriptor();
if (srcSize > 0 && sm.isAllocationSupported(dstFd)) {
//分配容量大小
sm.allocateBytes(dstFd, srcSize);
}
try {
final Int64Ref last = new Int64Ref(0);
//执行复制操作
FileUtils.copy(in, out, mSignal, Runnable::run, (long progress) -> {
final long delta = progress - last.value;
last.value = progress;
Log.i(TAG, "copyFileHelper: progress="+progress+",total="+srcSize+",delta="+delta);
//更新进度
makeCopyProgress(delta);
});
Log.i(TAG, "copyFileHelper: finish copy");
} catch (OperationCanceledException e) {
if (DEBUG) {
Log.d(TAG, "Canceled copy mid-copy of: " + src.derivedUri);
}
return;
}
// Need to invoke Os#fsync to ensure the file is written to the storage device.
try {
Log.i(TAG, "copyFileHelper: start to check copy result");
//同步数据到硬盘
Os.fsync(dstFile.getFileDescriptor());
Log.i(TAG, "copyFileHelper: finish to check copy result");
} catch (ErrnoException error) {
// fsync will fail with fd of pipes and return EROFS or EINVAL.
if (error.errno != OsConstants.EROFS && error.errno != OsConstants.EINVAL) {
throw new SyncFailedException(
"Failed to sync bytes after copying a file.");
}
}
// Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.
try {
Log.i(TAG, "copyFileHelper: start to close IO");
Os.close(dstFile.getFileDescriptor());
Log.i(TAG, "copyFileHelper: finish to close IO");
} catch (ErrnoException e) {
throw new IOException(e);
}
srcFile.checkError();
} catch (IOException e) {
Metrics.logFileOperationFailure(
appContext,
MetricConsts.SUBFILEOP_WRITE_FILE,
dest.derivedUri);
throw new ResourceException(
"Failed to copy bytes from %s to %s due to an IO exception.",
src.derivedUri, dest.derivedUri, e);
}
if (src.isVirtual()) {
convertedFiles.add(src);
}
success = true;
} finally {
if (!success) {
if (dstFile != null) {
try {
dstFile.closeWithError("Error copying bytes.");
} catch (IOException closeError) {
Log.w(TAG, "Error closing destination.", closeError);
}
}
if (DEBUG) {
Log.d(TAG, "Cleaning up failed operation leftovers.");
}
mSignal.cancel();
try {
deleteDocument(dest, destParent);
} catch (ResourceException e) {
Log.w(TAG, "Failed to cleanup after copy error: " + src.derivedUri, e);
}
}
// This also ensures the file descriptors are closed.
FileUtils.closeQuietly(in);
FileUtils.closeQuietly(out);
Log.i(TAG, "copyFileHelper: copy completed");
}
}
这里是文件复制的核心代码,其核心思想是通过StorageManager分配出所需要的存储空间,然后通过FileUtils.copy()完成真正的复制操作。看一下FileUtils.copy()的实现原理
/**
* Copy the contents of one FD to another.
* <p>
* Attempts to use several optimization strategies to copy the data in the
* kernel before falling back to a userspace copy as a last resort.
*
* @param count the number of bytes to copy.
* @param signal to signal if the copy should be cancelled early.
* @param executor that listener events should be delivered via.
* @param listener to be periodically notified as the copy progresses.
* @return number of bytes copied.
* @hide
*/
public static long copy(@NonNull FileDescriptor in, @NonNull FileDescriptor out, long count,
@Nullable CancellationSignal signal, @Nullable Executor executor,
@Nullable ProgressListener listener) throws IOException {
if (sEnableCopyOptimizations) {
try {
final StructStat st_in = Os.fstat(in);
final StructStat st_out = Os.fstat(out);
if (S_ISREG(st_in.st_mode) && S_ISREG(st_out.st_mode)) {
return copyInternalSendfile(in, out, count, signal, executor, listener);
} else if (S_ISFIFO(st_in.st_mode) || S_ISFIFO(st_out.st_mode)) {
return copyInternalSplice(in, out, count, signal, executor, listener);
}
} catch (ErrnoException e) {
throw e.rethrowAsIOException();
}
}
// Worse case fallback to userspace
return copyInternalUserspace(in, out, count, signal, executor, listener);
}
这里通过Linux里面的文件管理系统来处理,Os.fstat()获取文件的状态,然后通过S_ISREG()判断是否是常规文件操作,S_ISFIFO表示是否是FIFO模式,两者实现方式大同小异,看下copyInternalSendfile()的实现方式
/**
* Requires both input and output to be a regular file.
*
* @hide
*/
@VisibleForTesting
public static long copyInternalSendfile(FileDescriptor in, FileDescriptor out, long count,
CancellationSignal signal, Executor executor, ProgressListener listener)
throws ErrnoException {
long progress = 0;
long checkpoint = 0;
long t;
while ((t = Os.sendfile(out, in, null, Math.min(count, COPY_CHECKPOINT_BYTES))) != 0) {
progress += t;
checkpoint += t;
count -= t;
if (checkpoint >= COPY_CHECKPOINT_BYTES) {
if (signal != null) {
signal.throwIfCanceled();
}
if (executor != null && listener != null) {
final long progressSnapshot = progress;
executor.execute(() -> {
listener.onProgress(progressSnapshot);
});
}
checkpoint = 0;
}
}
if (executor != null && listener != null) {
final long progressSnapshot = progress;
executor.execute(() -> {
listener.onProgress(progressSnapshot);
});
}
return progress;
}
这里主要通过Os.sendfile()来进行分块复制
/**
* See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>.
*/
public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref offset, long byteCount) throws ErrnoException {
return Libcore.os.sendfile(outFd, inFd, offset, byteCount);
}
可以看到并没有通过java的IO操作来实现,而是在Linux的内核中完成的,见官网说明:https://man7.org/linux/man-pages/man2/sendfile.2.html
回到问题本质,复制进度达到100%后通知栏需要1min左右的时间才消失,在CopyJob的copyFileHelper()方法中我们看到,当FileUtils.copy()执行完复制操作之后,紧跟着执行了Os.fsync()动作,这个操作也是在Linux内核中完成的,主要作用是将所有的缓冲内容保存到磁盘中,见官网说明:https://man7.org/linux/man-pages/man2/fsync.2.html
在Os.fsync(dstFile.getFileDescriptor())前后分别加上日志,看看这个操作耗时多少
可以看到,这个操作费非常耗时,将近1min,但它是复制功能必不可少的操作,属于正常现象,后经过底层同事排查,是底层SD卡读写策略配置不当导致的。
至此,文件复制流程分析完毕。