Android13 SD卡格式化问题分析

问题描述

Android13 上进行SD卡格式化,格式化后显示的SD卡容量为0,退出重新进入,显示正常。

源码分析

首先在Settings->Storage页面,切换到SD card,页面将显示SD card的存储信息

代码位于packages/apps/Settings/src/com/android/settings/deviceinfo/StorageDashboardFragment.java

java 复制代码
    @Override
    public void onResume() {
        super.onResume();

        if (mIsLoadedFromCache) {
            mIsLoadedFromCache = false;
        } else {
            mStorageEntries.clear();
            mStorageEntries.addAll(
                    StorageUtils.getAllStorageEntries(getContext(), mStorageManager));
            Log.d("jasonwan", "---refreshUi-09---");
            //刷新UI
            refreshUi();
        }
        mStorageManager.registerListener(mStorageEventListener);
    }

	private void refreshUi() {
        mStorageSelectionController.setStorageEntries(mStorageEntries);
        mStorageSelectionController.setSelectedStorageEntry(mSelectedStorageEntry);
        //设置已选择的存储项
        mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry);

        mOptionMenuController.setSelectedStorageEntry(mSelectedStorageEntry);
        getActivity().invalidateOptionsMenu();

        // To prevent flicker, hides secondary users preference.
        // onReceivedSizes will set it visible for private storage.
        setSecondaryUsersVisible(false);

        if (!mSelectedStorageEntry.isMounted()) {
            // Set null volume to hide category stats.
            mPreferenceController.setVolume(null);
            return;
        }

        Log.d("jasonwan", "     mStorageCacheHelper.hasCachedSizeInfo()="+mStorageCacheHelper.hasCachedSizeInfo());
        Log.d("jasonwan", "     mSelectedStorageEntry.isPrivate()="+mSelectedStorageEntry.isPrivate());
        //sdcard属于public volume,因此这里的isPrivate()为false
        if (mStorageCacheHelper.hasCachedSizeInfo() && mSelectedStorageEntry.isPrivate()) {
            StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
            mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
            mPreferenceController.setUsedSize(cachedData.totalUsedSize);
            mPreferenceController.setTotalSize(cachedData.totalSize);
            Log.d("jasonwan", "     totalUsedSize="+cachedData.totalUsedSize+", totalSize="+cachedData.totalSize);
        }

        if (mSelectedStorageEntry.isPrivate()) {
            mStorageInfo = null;
            mAppsResult = null;
            // Hide the loading spinner if there is cached data.
            if (mStorageCacheHelper.hasCachedSizeInfo()) {
                //TODO(b/220259287): apply cache mechanism to secondary user
                mPreferenceController.onLoadFinished(mAppsResult, mUserId);
            } else {
                maybeSetLoading(isQuotaSupported());
                // To prevent flicker, sets null volume to hide category preferences.
                // onReceivedSizes will setVolume with the volume of selected storage.
                mPreferenceController.setVolume(null);
            }
            // Stats data is only available on private volumes.
            getLoaderManager().restartLoader(STORAGE_JOB_ID, Bundle.EMPTY, this);
            getLoaderManager()
                 .restartLoader(VOLUME_SIZE_JOB_ID, Bundle.EMPTY, new VolumeSizeCallbacks());
            getLoaderManager().restartLoader(ICON_JOB_ID, Bundle.EMPTY, new IconLoaderCallbacks());
        } else {
            mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
        }
    }

refreshUi方法中,会进行容量大小的显示,sdcard因为是public volume,所以isPrivate()方法为false,sdcard的容量大小计算及显示在mStorageUsageProgressBarController.setSelectedStorageEntry(mSelectedStorageEntry)

java 复制代码
    /** Set StorageEntry to display. */
    public void setSelectedStorageEntry(StorageEntry storageEntry) {
        mStorageEntry = storageEntry;
        //获取sdcard存储状态并更新UI
        getStorageStatsAndUpdateUi();
    }
java 复制代码
private void getStorageStatsAndUpdateUi() {
    // Use cached data for both total size and used size.
    Log.d("jasonwan", "---getStorageStatsAndUpdateUi---");
    Log.d("jasonwan", "     mStorageEntry=\n"+mStorageEntry.toString());
    Log.d("jasonwan", "     mStorageEntry.isMounted()="+mStorageEntry.isMounted());
    Log.d("jasonwan", "     mStorageEntry.isPrivate()="+mStorageEntry.isPrivate());
    //sdcard的isPrivate()为false,因此,这里的if不执行
    if (mStorageEntry != null && mStorageEntry.isMounted() && mStorageEntry.isPrivate()) {
        StorageCacheHelper.StorageCache cachedData = mStorageCacheHelper.retrieveCachedSize();
        mTotalBytes = cachedData.totalSize;
        mUsedBytes = cachedData.totalUsedSize;
        mIsUpdateStateFromSelectedStorageEntry = true;
        Log.d("jasonwan", "     01-mUsedBytes="+mUsedBytes);
        Log.d("jasonwan", "     01-mTotalBytes="+mTotalBytes);
        updateState(mUsageProgressBarPreference);
    }
    // Get the latest data from StorageStatsManager.
    //从StorageStatsManager中获取最新的数据,在子线程执行
    ThreadUtils.postOnBackgroundThread(() -> {
        try {
            if (mStorageEntry == null || !mStorageEntry.isMounted()) {
                throw new IOException();
            }
			//sdcard的isPrivate()为false,因此,这里执行else
            if (mStorageEntry.isPrivate()) {
                // StorageStatsManager can only query private storages.
                mTotalBytes = mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());
                mUsedBytes = mTotalBytes
                        - mStorageStatsManager.getFreeBytes(mStorageEntry.getFsUuid());
                Log.d("jasonwan", "     02-mUsedBytes="+mUsedBytes);
                Log.d("jasonwan", "     02-mTotalBytes="+mTotalBytes);
            } else {
                //获取sdcard的File对象
                final File rootFile = mStorageEntry.getPath();
                Log.d("jasonwan", "     rootFile == null?  "+(rootFile==null?"null":rootFile.getAbsolutePath()));
                if (rootFile == null) {
                    Log.d(TAG, "Mounted public storage has null root path: " + mStorageEntry);
                    throw new IOException();
                }
                //直接通过File对象获取总容量大小
                mTotalBytes = rootFile.getTotalSpace();
                mUsedBytes = mTotalBytes - rootFile.getFreeSpace();
                //自定义log,打印容量大小的值
                Log.d("jasonwan", "     03-mUsedBytes="+mUsedBytes);
                Log.d("jasonwan", "     03-mTotalBytes="+mTotalBytes);
            }
        } catch (IOException e) {
            // The storage device isn't present.
            mTotalBytes = 0;
            mUsedBytes = 0;
            Log.d("jasonwan", "     04-mUsedBytes="+mUsedBytes);
            Log.d("jasonwan", "     04-mTotalBytes="+mTotalBytes);
        }

        if (mUsageProgressBarPreference == null) {
            return;
        }
        mIsUpdateStateFromSelectedStorageEntry = true;
        ThreadUtils.postOnMainThread(() -> updateState(mUsageProgressBarPreference));
    });
}

getStorageStatsAndUpdateUi方法中创建了一个子线程,并在子线程中直接获取sdcard的File对象,并通过File对象的getTotalSpace方法获取总容量大小,通过自定义日志,我们得到以下信息

可以看到sdcard的类型为PUBLIC,diskId为disk:179,0,挂载状态为可写,路径为/storage/70D3-1521,并且最终打印的容量大小为31448498176,单位Bytes,换算下来为32GB,已使用大小为851968,大约为832KB。

此时,点击右上角的菜单,选择"Format"进行格式化

此时点击"Format",确认格式化,系统将对sdcard进行格式化,其代码位于packages/apps/Settings/src/com/android/settings/deviceinfo/StorageWizardFormatConfirm.java

java 复制代码
    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        final Context context = getContext();

        final Bundle args = getArguments();
        final String diskId = args.getString(EXTRA_DISK_ID);
        final String formatForgetUuid = args.getString(EXTRA_FORMAT_FORGET_UUID);
        final boolean formatPrivate = args.getBoolean(EXTRA_FORMAT_PRIVATE, false);

        final DiskInfo disk = context.getSystemService(StorageManager.class)
                .findDiskById(diskId);

        final AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle(TextUtils.expandTemplate(
                getText(R.string.storage_wizard_format_confirm_v2_title),
                disk.getShortDescription()));
        if (formatPrivate) {
            builder.setMessage(TextUtils.expandTemplate(
                    getText(R.string.storage_wizard_format_confirm_v2_body),
                    disk.getDescription(),
                    disk.getShortDescription(),
                    disk.getShortDescription()));
        } else {
            builder.setMessage(TextUtils.expandTemplate(
                getText(R.string.storage_wizard_format_confirm_v2_body_external),
                disk.getDescription(),
                disk.getShortDescription(),
                disk.getShortDescription()));
        }

        builder.setNegativeButton(android.R.string.cancel, null);
        builder.setPositiveButton(
                TextUtils.expandTemplate(getText(R.string.storage_menu_format_option),
                        disk.getShortDescription()),
                (dialog, which) -> {
                    //点击Format按钮
                    Log.d("jasonwan","click format button");
                    final Intent intent = new Intent(context, StorageWizardFormatProgress.class);
                    intent.putExtra(EXTRA_DISK_ID, diskId);
                    intent.putExtra(EXTRA_FORMAT_FORGET_UUID, formatForgetUuid);
                    intent.putExtra(EXTRA_FORMAT_PRIVATE, formatPrivate);
                    context.startActivity(intent);
                });

        return builder.create();
    }

点击完"Format"按钮后,格式化功能将在StorageWizardFormatProgress页面进行

java 复制代码
public class StorageWizardFormatProgress extends StorageWizardBase {
    private static final String TAG = "StorageWizardFormatProgress";

    private static final String PROP_DEBUG_STORAGE_SLOW = "sys.debug.storage_slow";

    private boolean mFormatPrivate;

    private PartitionTask mTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (mDisk == null) {
            finish();
            return;
        }
        setContentView(R.layout.storage_wizard_progress);
        setKeepScreenOn(true);

        mFormatPrivate = getIntent().getBooleanExtra(EXTRA_FORMAT_PRIVATE, false);

        setHeaderText(R.string.storage_wizard_format_progress_title, getDiskShortDescription());
        setBodyText(R.string.storage_wizard_format_progress_body, getDiskDescription());
        setBackButtonVisibility(View.INVISIBLE);
        setNextButtonVisibility(View.INVISIBLE);
        mTask = (PartitionTask) getLastCustomNonConfigurationInstance();
        //创建异步任务进行格式化
        if (mTask == null) {
            mTask = new PartitionTask();
            mTask.setActivity(this);
            mTask.execute();
        } else {
            mTask.setActivity(this);
        }
    }

    @Override
    public Object onRetainCustomNonConfigurationInstance() {
        return mTask;
    }

    public static class PartitionTask extends AsyncTask<Void, Integer, Exception> {
        public StorageWizardFormatProgress mActivity;

        private volatile int mProgress = 20;

        private volatile long mPrivateBench;

        @Override
        protected Exception doInBackground(Void... params) {
            //异步任务执行中,在子线程完成
            final StorageWizardFormatProgress activity = mActivity;
            final StorageManager storage = mActivity.mStorage;
            try {
                Log.d("jasonwan","---formatting---");
                Log.d("jasonwan","      activity.mFormatPrivate="+activity.mFormatPrivate);
                //sdcard属于public volume,这里的mFormatPrivate为false
                if (activity.mFormatPrivate) {
                    storage.partitionPrivate(activity.mDisk.getId());
                    publishProgress(40);

                    final VolumeInfo privateVol = activity.findFirstVolume(TYPE_PRIVATE, 50);
                    final CompletableFuture<PersistableBundle> result = new CompletableFuture<>();
                    if(null != privateVol) {
                        storage.benchmark(privateVol.getId(), new IVoldTaskListener.Stub() {
                            @Override
                            public void onStatus(int status, PersistableBundle extras) {
                                // Map benchmark 0-100% progress onto 40-80%
                                publishProgress(40 + ((status * 40) / 100));
                            }

                            @Override
                            public void onFinished(int status, PersistableBundle extras) {
                                result.complete(extras);
                            }
                        });
                        mPrivateBench = result.get(60, TimeUnit.SECONDS).getLong("run",
                                Long.MAX_VALUE);
                    }

                    // If we just adopted the device that had been providing
                    // physical storage, then automatically move storage to the
                    // new emulated volume.
                    if (activity.mDisk.isDefaultPrimary()
                            && Objects.equals(storage.getPrimaryStorageUuid(),
                                    StorageManager.UUID_PRIMARY_PHYSICAL)) {
                        Log.d(TAG, "Just formatted primary physical; silently moving "
                                + "storage to new emulated volume");
                        storage.setPrimaryStorageUuid(privateVol.getFsUuid(), new SilentObserver());
                    }

                } else {
                    //执行格式化,传入diskId
                    storage.partitionPublic(activity.mDisk.getId());
                }
                return null;
            } catch (Exception e) {
                return e;
            }
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            //更新任务进度
            mProgress = progress[0];
            mActivity.setCurrentProgress(mProgress);
        }

        public void setActivity(StorageWizardFormatProgress activity) {
            mActivity = activity;
            mActivity.setCurrentProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Exception e) {
            //异步任务完成后
            final StorageWizardFormatProgress activity = mActivity;
            if (activity.isDestroyed()) {
                return;
            }

            if (e != null) {
                Log.e(TAG, "Failed to partition", e);
                Toast.makeText(activity, e.getMessage(), Toast.LENGTH_LONG).show();
                activity.finishAffinity();
                return;
            }

            if (activity.mFormatPrivate) {
                // When the adoptable storage feature originally launched, we
                // benchmarked both internal storage and the newly adopted
                // storage and we warned if the adopted device was less than
                // 0.25x the speed of internal. (The goal was to help set user
                // expectations and encourage use of devices comparable to
                // internal storage performance.)

                // However, since then, internal storage has started moving from
                // eMMC to UFS, which can significantly outperform adopted
                // devices, causing the speed warning to always trigger. To
                // mitigate this, we've switched to using a static threshold.

                // The static threshold was derived by running the benchmark on
                // a wide selection of SD cards from several vendors; here are
                // some 50th percentile results from 20+ runs of each card:

                // 8GB C4 40MB/s+: 3282ms
                // 16GB C10 40MB/s+: 1881ms
                // 32GB C10 40MB/s+: 2897ms
                // 32GB U3 80MB/s+: 1595ms
                // 32GB C10 80MB/s+: 1680ms
                // 128GB U1 80MB/s+: 1532ms

                // Thus a 2000ms static threshold strikes a reasonable balance
                // to help us identify slower cards. Users can still proceed
                // with these slower cards; we're just showing a warning.

                // The above analysis was done using the "r1572:w1001:s285"
                // benchmark, and it should be redone any time the benchmark
                // changes.

                Log.d(TAG, "New volume took " + mPrivateBench + "ms to run benchmark");
                if (mPrivateBench > 2000
                        || SystemProperties.getBoolean(PROP_DEBUG_STORAGE_SLOW, false)) {
                    mActivity.onFormatFinishedSlow();
                } else {
                    mActivity.onFormatFinished();
                }
            } else {
                //格式化已完成
                mActivity.onFormatFinished();
            }
        }
    }

    public void onFormatFinished() {
        //跳转到完成页面
        final Intent intent = new Intent(this, StorageWizardFormatSlow.class);
        intent.putExtra(EXTRA_FORMAT_SLOW, false);
        startActivity(intent);
        //关闭当前页面
        finishAffinity();
    }

    public void onFormatFinishedSlow() {
        final Intent intent = new Intent(this, StorageWizardFormatSlow.class);
        intent.putExtra(EXTRA_FORMAT_SLOW, true);
        startActivity(intent);
        finishAffinity();
    }

    private static class SilentObserver extends IPackageMoveObserver.Stub {
        @Override
        public void onCreated(int moveId, Bundle extras) {
            // Ignored
        }

        @Override
        public void onStatusChanged(int moveId, int status, long estMillis) {
            // Ignored
        }
    }
}

StorageWizardFormatProgress页面创建了一个 PartitionTask来进行格式化,格式化由storage.partitionPublic(activity.mDisk.getId())来完成,它通过AIDL调用了远程StorageManagerServicepartitionPublic()方法

java 复制代码
    @Override
    public void partitionPublic(String diskId) {
        enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);

        final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);
        try {
            //StorageManagerService又调用了Vold服务来完成sdcard的格式化
            mVold.partition(diskId, IVold.PARTITION_TYPE_PUBLIC, -1);
            waitForLatch(latch, "partitionPublic", 3 * DateUtils.MINUTE_IN_MILLIS);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

可以看到StorageManagerService又调用了Vold服务来完成sdcard的格式化。

vold即Volume守护进程,用来管理Android中存储类(包含U盘和SD卡)的热拔插事件,处于Kernel和Framework之间,是两个层级连接的桥梁。vold在系统中以守护进程存在,是一个单独的进程,在开机阶段由Init进程拉起。在system/vold/vold.rc中有详细配置。启动之后监听来自kernel的UEvent,挂载U盘并和Framework层的StorageManager通信、设置挂载选项、用户权限等,以实现外部存储对上层app和用户的可见性。

上述PartitionTask执行完后,会跳转到StorageWizardFormatSlow页面,提示格式化已完成,同时关闭当前格式化页面

点击"Done"按钮,关闭当前页面,返回StorageDashboardFragment页面,并执行onResume生命周期方法,重启执行refreshUi方法来获取sdcard大小并更新UI,整个过程通过自定义log,打印日志如下

问题分析

可见整体流程为:

  • 用户进入Settings -> Storage
  • 切换到sdcard,通过File.getTotalSpace()获取sdcard容量大小并显示
  • 点击格式化,通过vold完成sdcard的格式化操作
  • 返回Storage页面,重新获取sdcard的大小。

整个过程涉及到的核心API为File.getTotalSpace()vold进程相关的API,均为Google原生API。

同时抓取了整个过程的logcat日志,发现在sdcard格式化后打印了如下日志

此日志在格式化之前并未输出,说明格式化之后导致sdcard找不到了,因而大小才会显示0。

退出Settings -> Storage页面重新进入,切换到sdcard后显示正常。

可能跟vold有关,已让bsp同事协助排查。

相关推荐
m0_748245179 分钟前
Web第一次作业
java
小码的头发丝、9 分钟前
Java进阶学习笔记|面向对象
java·笔记·学习
m0_5485147713 分钟前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
坊钰41 分钟前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang11 小时前
leetcode hot100 LRU缓存
java·开发语言
会说法语的猪1 小时前
springboot实现图片上传、下载功能
java·spring boot·后端
码农老起1 小时前
IntelliJ IDEA 基本使用教程及Spring Boot项目搭建实战
java·ide·intellij-idea
m0_748239831 小时前
基于web的音乐网站(Java+SpringBoot+Mysql)
java·前端·spring boot
时雨h1 小时前
RuoYi-ue前端分离版部署流程
java·开发语言·前端