最近处理一个Android 11大文件复制时间进度栏卡在100%的问题,处理过程中梳理了一下文件复制流程。

首先,根据UI找到复制文件的代码入口:

11 android 文件uri android11文件操作_开发语言

逻辑代码在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())前后分别加上日志,看看这个操作耗时多少

11 android 文件uri android11文件操作_java_02

可以看到,这个操作费非常耗时,将近1min,但它是复制功能必不可少的操作,属于正常现象,后经过底层同事排查,是底层SD卡读写策略配置不当导致的。

至此,文件复制流程分析完毕。