问题背景

Android12平台上,恢复出厂设置后,已运用空间偏高,空间运用率为28/128=21.9%,产品需求控制在11%以内

Android12存储空间计算的一个误区

源码剖析

当前页面的源码位于packages/apps/Settings/src/com/android/settings/deviceinfo/StorageDashboardFragment.java

    /**
     * VolumeSizeCallbacks exists because StorageCategoryFragment already implements
     * LoaderCallbacks for a different type.
     */
    public final class VolumeSizeCallbacks
            implements LoaderManager.LoaderCallbacks<PrivateStorageInfo> {
        @Override
        public Loader<PrivateStorageInfo> onCreateLoader(int id, Bundle args) {
            final Context context = getContext();
            final StorageManagerVolumeProvider smvp =
                    new StorageManagerVolumeProvider(mStorageManager);
            final StorageStatsManager stats = context.getSystemService(StorageStatsManager.class);
            //运用loader技术加载存储卷信息
            return new VolumeSizesLoader(context, smvp, stats,
                    mSelectedStorageEntry.getVolumeInfo());
        }
        @Override
        public void onLoaderReset(Loader<PrivateStorageInfo> loader) {
        }
        @Override
        public void onLoadFinished(
                Loader<PrivateStorageInfo> loader, PrivateStorageInfo privateStorageInfo) {
            if (privateStorageInfo == null) {
                getActivity().finish();
                return;
            }
			//loader加载完毕,返回存储卷信息
            mStorageInfo = privateStorageInfo;
            //更新UI
            onReceivedSizes();
        }
    }
    private void onReceivedSizes() {
        if (mStorageInfo == null || mAppsResult == null) {
            return;
        }
        if (getView().findViewById(R.id.loading_container).getVisibility() == View.VISIBLE) {
            setLoading(false /* loading */, true /* animate */);
        }
		//已用空间=总空间-可用空间
        final long privateUsedBytes = mStorageInfo.totalBytes - mStorageInfo.freeBytes;
        //绑定ui
        mPreferenceController.setVolume(mSelectedStorageEntry.getVolumeInfo());
        mPreferenceController.setUsedSize(privateUsedBytes);
        mPreferenceController.setTotalSize(mStorageInfo.totalBytes);
        for (int i = 0, size = mSecondaryUsers.size(); i < size; i++) {
            final AbstractPreferenceController controller = mSecondaryUsers.get(i);
            if (controller instanceof SecondaryUserController) {
                SecondaryUserController userController = (SecondaryUserController) controller;
                userController.setTotalSize(mStorageInfo.totalBytes);
            }
        }
		//更新分类item
        mPreferenceController.onLoadFinished(mAppsResult, mUserId);
        updateSecondaryUserControllers(mSecondaryUsers, mAppsResult);
        setSecondaryUsersVisible(true);
    }

VolumeSizesLoader继承自AsyncTaskLoader,主要是要看VolumeSizesLoader是怎么加载存储卷信息的

    @Override
    public PrivateStorageInfo loadInBackground() {
        PrivateStorageInfo volumeSizes;
        try {
            //子线程加载
            volumeSizes = getVolumeSize(mVolumeProvider, mStats, mVolume);
        } catch (IOException e) {
            return null;
        }
        return volumeSizes;
    }
    @VisibleForTesting
    static PrivateStorageInfo getVolumeSize(
            StorageVolumeProvider storageVolumeProvider, StorageStatsManager stats, VolumeInfo info)
            throws IOException {
        //运用StorageVolumeProvider直接获取
        long privateTotalBytes = storageVolumeProvider.getTotalBytes(stats, info);
        long privateFreeBytes = storageVolumeProvider.getFreeBytes(stats, info);
        return new PrivateStorageInfo(privateFreeBytes, privateTotalBytes);
    }

StorageVolumeProvider是一个接口,主要效果便是提供对存储卷的拜访,其实现类为StorageManagerVolumeProvider

    @Override
    public long getTotalBytes(StorageStatsManager stats, VolumeInfo volume) throws IOException {
        return stats.getTotalBytes(volume.getFsUuid());
    }
    @Override
    public long getFreeBytes(StorageStatsManager stats, VolumeInfo volume) throws IOException {
        return stats.getFreeBytes(volume.getFsUuid());
    }
    @WorkerThread
    public @BytesLong long getTotalBytes(@NonNull UUID storageUuid) throws IOException {
        try {
            return mService.getTotalBytes(convert(storageUuid), mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }
    @WorkerThread
    public @BytesLong long getFreeBytes(@NonNull UUID storageUuid) throws IOException {
        try {
            return mService.getFreeBytes(convert(storageUuid), mContext.getOpPackageName());
        } catch (ParcelableException e) {
            e.maybeRethrow(IOException.class);
            throw new RuntimeException(e);
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

能够看到终究调用了远程StorageStatsService的功用

    @Override
    public long getTotalBytes(String volumeUuid, String callingPackage) {
        // NOTE: No permissions required
        if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
            //代码1,获取基础存储巨细并格局化返回(经调试,执行这儿的逻辑)
            return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
        } else {
            final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
            if (vol == null) {
                throw new ParcelableException(
                        new IOException("Failed to find storage device for UUID " + volumeUuid));
            }
            Log.d("jasonwan", "vol.disk.sysPath:"+vol.disk.sysPath+", vol.disk.size:"+vol.disk.size);
            return FileUtils.roundStorageSize(vol.disk.size);
        }
    }
    @Override
    public long getFreeBytes(String volumeUuid, String callingPackage) {
        // NOTE: No permissions required
        final long token = Binder.clearCallingIdentity();
        try {
            final File path;
            try {
                //获取对应存储卷的途径
                path = mStorage.findPathForUuid(volumeUuid);
                Log.d("jasonwan", "free path: "+path.getAbsolutePath()+", and size is:"+path.getUsableSpace());
            } catch (FileNotFoundException e) {
                throw new ParcelableException(e);
            }
            // Free space is usable bytes plus any cached data that we're
            // willing to automatically clear. To avoid user confusion, this
            // logic should be kept in sync with getAllocatableBytes().
            if (isQuotaSupported(volumeUuid, PLATFORM_PACKAGE_NAME)) {
                final long cacheTotal = getCacheBytes(volumeUuid, PLATFORM_PACKAGE_NAME);
                final long cacheReserved = mStorage.getStorageCacheBytes(path, 0);
                final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);
                Log.d("jasonwan", "cacheClearable size: "+cacheClearable);
                return path.getUsableSpace() + cacheClearable;
            } else {
                //返回途径可用空间巨细
                return path.getUsableSpace();
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

看看mStorage.getPrimaryStorageSize()的代码

    /** {@hide} */
    public long getPrimaryStorageSize() {
        File dataDirectory = Environment.getDataDirectory();
        File rootDirectory = Environment.getRootDirectory();
        //代码2
        long total = FileUtils.roundStorageSize(dataDirectory.getTotalSpace() + rootDirectory.getTotalSpace());
        Log.d("jasonwan", "Environment.getDataDirectory():"+ dataDirectory +", totalSpace:"+ dataDirectory.getTotalSpace()+", freeSpace:"+dataDirectory.getFreeSpace()+", usedSpace:"+(dataDirectory.getTotalSpace()-dataDirectory.getFreeSpace()));
        Log.d("jasonwan", "Environment.getRootDirectory():"+ rootDirectory+", totalSpace:"+rootDirectory.getTotalSpace()+", freeSpace:"+rootDirectory.getFreeSpace()+", usedSpace:"+(rootDirectory.getTotalSpace()-rootDirectory.getFreeSpace()));
        Log.d("jasonwan","calculate total:"+total+", real total:"+(dataDirectory.getTotalSpace() + rootDirectory.getTotalSpace()));
        Log.d("jasonwan","real free:"+(dataDirectory.getFreeSpace()+rootDirectory.getFreeSpace()));
        return total;
    }

总巨细便是获取的/data目录和/system目录的总巨细,这儿经过自定义log,打印出实践途径

05-06 00:48:54.353  1222  4361 D jasonwan: Environment.getDataDirectory():/data, totalSpace:101534478336, freeSpace:97527693312, usedSpace:4006785024
05-06 00:48:54.353  1222  4361 D jasonwan: Environment.getRootDirectory():/system, totalSpace:2145386496, freeSpace:1651765248, usedSpace:493621248
05-06 00:48:54.353  1222  4361 D jasonwan: calculate total:128000000000, real total:103679864832
05-06 00:48:54.353  1222  4361 D jasonwan: real free:99179458560

而可用空间的巨细便是获取的/data目录的可用巨细,同样,这儿经过自定义log,打印出实践途径

05-06 00:48:54.358  1222  2079 D jasonwan: free path: /data, and size is:97393475584
05-06 00:48:54.481  1222  4361 D jasonwan: cacheClearable size: 0

这儿有个很奇怪的当地,便是核算出来的byte巨细经过FileUtils.roundStorageSize()或者Formatter.formatBytes()格局化后变大了,代码1处,mStorage.getPrimaryStorageSize()为103679864832,经过FileUtils.roundStorageSize()格局化后变成了128000000000,来看下FileUtils.roundStorageSize()的核算原理

    /**
     * Round the given size of a storage device to a nice round power-of-two
     * value, such as 256MB or 32GB. This avoids showing weird values like
     * "29.5GB" in UI.
     *
     * @hide
     */
    public static long roundStorageSize(long size) {
        long val = 1;
        long pow = 1;
        while ((val * pow) < size) {
            val <<= 1;
            if (val > 512) {
                val = 1;
                pow *= 1000;
            }
        }
        return val * pow;
    }

依据办法注释阐明,该办法将经过四舍五入,将参数值变成接近的2的n次幂,比方:

  • 100->128
  • 129->256
  • 257->512
  • 513->1000

留意,终究不是1024,而是1000,这个跟咱们实践生活是很贴近的,比方咱们去买一个U盘,老板说有2G的,4G的,32G的,但绝不会说有3G的,3.5G的。可见此格局化办法终究会将实在数值变大。而Formatter.formatBytes()在将Bytes变成KB、MB、GB时,会以1000作为计量单位,而不是1024

    /** {@hide} */
    @UnsupportedAppUsage
    public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
        //这儿unit的值终究等于1000,不是1024
        final int unit = ((flags & FLAG_IEC_UNITS) != 0) ? 1024 : 1000;
        final boolean isNegative = (sizeBytes < 0);
        float result = isNegative ? -sizeBytes : sizeBytes;
        int suffix = com.android.internal.R.string.byteShort;
        long mult = 1;
        if (result > 900) {
            suffix = com.android.internal.R.string.kilobyteShort;
            mult = unit;
            result = result / unit;
        }
        if (result > 900) {
            suffix = com.android.internal.R.string.megabyteShort;
            mult *= unit;
            result = result / unit;
        }
        if (result > 900) {
            suffix = com.android.internal.R.string.gigabyteShort;
            mult *= unit;
            result = result / unit;
        }
        if (result > 900) {
            suffix = com.android.internal.R.string.terabyteShort;
            mult *= unit;
            result = result / unit;
        }
        if (result > 900) {
            suffix = com.android.internal.R.string.petabyteShort;
            mult *= unit;
            result = result / unit;
        }
        // Note we calculate the rounded long by ourselves, but still let String.format()
        // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
        // floating point errors.
        final int roundFactor;
        final String roundFormat;
        if (mult == 1 || result >= 100) {
            roundFactor = 1;
            roundFormat = "%.0f";
        } else if (result < 1) {
            roundFactor = 100;
            roundFormat = "%.2f";
        } else if (result < 10) {
            if ((flags & FLAG_SHORTER) != 0) {
                roundFactor = 10;
                roundFormat = "%.1f";
            } else {
                roundFactor = 100;
                roundFormat = "%.2f";
            }
        } else { // 10 <= result < 100
            if ((flags & FLAG_SHORTER) != 0) {
                roundFactor = 1;
                roundFormat = "%.0f";
            } else {
                roundFactor = 100;
                roundFormat = "%.2f";
            }
        }
        if (isNegative) {
            result = -result;
        }
        final String roundedString = String.format(roundFormat, result);
        // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
        // it's okay (for now)...
        final long roundedBytes =
                (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
                : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
        final String units = res.getString(suffix);
        return new BytesResult(roundedString, units, roundedBytes);
    }

综上,实在总巨细103679864832先换算为128000000000,然后格局为GB即为128GB,也便是文章最初咱们看到的总巨细128GB,已运用空间同理。

解决方案

那如果咱们运用实在的bytes巨细来核算空间运用率,则为1-97393475584/103679864832=6.1%,远低于界面显示的21.9%,同时也符合产品的需求。所以这只是核算误差问题,实践运用率并没有那么高。