Android 11 文件复制流程

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

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

1

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

20

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

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
第1章 搭建Android源码工作环境 1.1 Android系统架构 1.2 搭建开发环境 1.2.1 下载源码 1.2.2 编译源码 1.2.3 利用Eclipse调试system_process 1.3 本章小结 第2章 深入理解Java Binder和MessageQueue 2.1 概述 2.2 Java层中的Binder架构分析 2.2.1 Binder架构总览 2.2.2 初始化Java层Binder框架 2.2.3 addService实例分析 2.2.4 Java层Binder架构总结 2.3 心系两界的MessageQueue 2.3.1 MessageQueue的创建 2.3.2 提取消息 2.3.3 nativePollOnce函数分析 2.3.4 MessageQueue总结 2.4 本章小结 第3章 深入理解SystemServer 3.1 概述 3.2 SystemServer分析 3.2.1 main函数分析 3.2.2 Service群英会 3.3 EntropyService分析 3.4 DropBoxManagerService分析 3.4.1 DBMS构造函数分析 3.4.2 dropbox日志文件的添加 3.4.3 DBMS和settings数据库 3.5 DiskStatsService和DeviceStorageMonitorService分析 3.5.1 DiskStatsService分析 3.5.2 DeviceStorageManagerService分析 3.6 SamplingProfilerService分析 3.6.1 SamplingProfilerService构造函数分析 3.6.2 SamplingProfilerIntegration分析 3.7 ClipboardService分析 3.7.1 复制数据到剪贴板 3.7.2 从剪切板粘贴数据 3.7.3 CBS中的权限管理 3.8 本章小结 第4章 深入理解PackageManagerService 4.1 概述 4.2 初识PackageManagerService 4.3 PKMS的main函数分析 4.3.1 构造函数分析之前期准备工作 4.3.2 构造函数分析之扫描Package 4.3.3 构造函数分析之扫尾工作 4.3.4 PKMS构造函数总结 4.4 APK Installation分析 4.4.1 adb install分析 4.4.2 pm分析 4.4.3 installPackageWithVerification函数分析 4.4.4 APK 安装流程总结 4.4.5 Verification介绍 4.5 queryIntentActivities分析 4.5.1 Intent及IntentFilter介绍 4.5.2 Activity信息的管理 4.5.3 Intent 匹配查询分析 4.5.4 queryIntentActivities总结 4.6 installd及UserManager介绍 4.6.1 installd介绍 4.6.2 UserManager介绍 4.7 本章学习指导 4.8 本章小结 第5章 深入理解PowerManagerService 5.1 概述 5.2 初识PowerManagerService 5.2.1 PMS构造函数分析 5.2.2 init分析 5.2.3 systemReady分析 5.2.4 BootComplete处理 5.2.5 初识PowerManagerService总结 5.3 PMS WakeLock分析 5.3.1 WakeLock客户端分析 5.3.2 PMS acquireWakeLock分析 5.3.3 Power类及LightService类介绍 5.3.4 WakeLock总结 5.4 userActivity及Power按键处理分析 5.4.1 userActivity分析 5.4.2 Power按键处理分析 5.5 BatteryService及BatteryStatsService分析 5.5.1 BatteryService分析 5.5.2 BatteryStatsService分析 5.5.3 BatteryService及BatteryStatsService总结 5.6 本章学习指导 5.7 本章小结 第6章 深入理解ActivityManagerService 6.1 概述 6.2 初识ActivityManagerService 6.2.1 ActivityManagerService的main函数分析 6.2.2 AMS的 setSystemProcess分析 6.2.3 AMS的 installSystemProviders函数分析 6.2.4 AMS的 systemReady分析 6.2.5 初识ActivityManagerService总结 6.3 startActivity分析 6.3.1 从am说起 6.3.2 AMS的startActivityAndWait函数分析 6.3.3 startActivityLocked分析 6.4 Broadcast和BroadcastReceiver分析 6.4.1 registerReceiver流程分析 6.4.2 sendBroadcast流程分析 6.4.3 BROADCAST_INTENT_MSG消息处理函数 6.4.4 应用进程处理广播分析 6.4.5 广播处理总结 6.5 startService之按图索骥 6.5.1 Service知识介绍 6.5.2 startService流程图 6.6 AMS中的进程管理 6.6.1 Linux进程管理介绍 6.6.2 关于Android中的进程管理的介绍 6.6.3 AMS进程管理函数分析 6.6.4 AMS进程管理总结 6.7 App的 Crash处理 6.7.1 应用进程的Crash处理 6.7.2 AMS的handleApplicationCrash分析 6.7.3 AppDeathRecipient binderDied分析 6.7.4 App的Crash处理总结 6.8 本章学习指导 6.9 本章小结 第7章 深入理解ContentProvider 7.1 概述 7.2 MediaProvider的启动及创建 7.2.1 Context的getContentResolver函数分析 7.2.2 MediaStore.Image.Media的query函数分析 7.2.3 MediaProvider的启动及创建总结 7.3 SQLite创建数据库分析 7.3.1 SQLite及SQLiteDatabase家族 7.3.2 MediaProvider创建数据库分析 7.3.3 SQLiteDatabase创建数据库的分析总结 7.4 Cursor 的query函数的实现分析 7.4.1 提取query关键点 7.4.2 MediaProvider 的query分析 7.4.3 query关键点分析 7.4.4 Cursor query实现分析总结 7.5 Cursor close函数实现分析 7.5.1 客户端close的分析 7.5.2 服务端close的分析 7.5.3 finalize函数分析 7.5.4 Cursor close函数总结 7.6 ContentResolver openAssetFileDescriptor函数分析 7.6.1 openAssetFileDescriptor之客户端调用分析 7.6.2 ContentProvider的 openTypedAssetFile函数分析 7.6.3 跨进程传递文件描述符的探讨 7.6.4 openAssetFileDescriptor函数分析总结 7.7 本章学习指导 7.8 本章小结 第8章 深入理解ContentService和AccountManagerService 8.1 概述 8.2 数据更新通知机制分析 8.2.1 初识ContentService 8.2.2 ContentResovler 的registerContentObserver分析 8.2.3 ContentResolver的 notifyChange分析 8.2.4 数据更新通知机制总结和深入探讨 8.3 AccountManagerService分析 8.3.1 初识AccountManagerService 8.3.2 AccountManager addAccount分析 8.3.3 AccountManagerService的分析总结 8.4 数据同步管理SyncManager分析 8.4.1 初识SyncManager 8.4.2 ContentResolver 的requestSync分析 8.4.3 数据同步管理SyncManager分析总结 8.5 本章学习指导 8.6 本章小结

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值