问题背景
入职新公司,最近被分配了个Bug,一个MTK的Android 12 项目中,测试发现在插入三星的32G sd卡后,且将其格式化内部存储空间时,发现sd卡显示的总内存对应为64GB? 如果只是作为便携式存储时,则显示正常。并且竞品的显示都是正常的。如下图所示,图1 为只作为便携式存储,图2为格式化为内部存储。
图1
图2
问题分析
一开始怀疑之所以显示为64G,是因为设备本身有32G,添加sd卡后,sd卡的总存储显示就成了设备原有存储+sd 卡的存储大小,认为应该也不算问题,只是机制不同。but,一番分析后,我错了。
在 Settings 中,如图2中显示内存界面最终在StorageDashboardFragment.java 文件中.
StorageDashboardFragment.java 中调用了StorageUsageProgressBarPreferenceController::setSelectedStorageEntry方法
public class StorageUsageProgressBarPreferenceController extends BasePreferenceController {
/** Set StorageEntry to display. */
public void setSelectedStorageEntry(StorageEntry storageEntry) {
mStorageEntry = storageEntry;
getStorageStatsAndUpdateUi();
}
private void getStorageStatsAndUpdateUi() {
ThreadUtils.postOnBackgroundThread(() -> {
try {
if (mStorageEntry == null || !mStorageEntry.isMounted()) {
throw new IOException();
}
//代码1
if (mStorageEntry.isPrivate()) {
// StorageStatsManager can only query private storages.
mTotalBytes = mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());
mUsedBytes = mTotalBytes
- mStorageStatsManager.getFreeBytes(mStorageEntry.getFsUuid());
} else {//代码2
final File rootFile = mStorageEntry.getPath();
if (rootFile == null) {
Log.d(TAG, "Mounted public storage has null root path: " + mStorageEntry);
throw new IOException();
}
mTotalBytes = rootFile.getTotalSpace();
mUsedBytes = mTotalBytes - rootFile.getFreeSpace();
}
} catch (IOException e) {
// The storage device isn't present.
mTotalBytes = 0;
mUsedBytes = 0;
}
if (mUsageProgressBarPreference == null) {
return;
}
mIsUpdateStateFromSelectedStorageEntry = true;
ThreadUtils.postOnMainThread(() -> updateState(mUsageProgressBarPreference));
});
}
}
- 代码1:只有当选择的页面是内部存储,或者是将sd卡作为内部存储后,才会走该代码,通过
mStorageStatsManager.getTotalBytes方法获取总空间和可用空间 - 代码2: 只有sd卡作为便携存储设备时,走该代码逻辑,通过
File对象的方法获取sd卡的总空间和可用空间
显然,导致存储空间显示为 64G的原因就是 mStorageStatsManager.getTotalBytes 这段代码.
接下来我们继续分析该代码调用逻辑。
android.app.usage.StorageStatsManager
class StorageStatsManager {
private final IStorageStatsManager mService;
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();
}
}
}
IStorageStatsManager 对应的service 类为com.android.server.usage.StorageStatsService
class StorageStatsService{
private final StorageManager mStorage;
public long getTotalBytes(String volumeUuid, String callingPackage) {
// NOTE: No permissions required
if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {//代码1
return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
} else {//代码2
final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
if (vol == null) {
throw new ParcelableException(
new IOException("Failed to find storage device for UUID " + volumeUuid));
}
return FileUtils.roundStorageSize(vol.disk.size);
}
}
}
重点就在这个FileUtils.roundStorageSize方法中了
StorageManager 使用UUID_PRIVATE_INTERNAL字段标注了设备本身的内存空间卷的uuid.sd卡作为内部空间将走到代码2 处。在加入日志后
class StorageStatsService{
private final StorageManager mStorage;
public long getTotalBytes(String volumeUuid, String callingPackage) {
// NOTE: No permissions required
if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {//代码1
return FileUtils.roundStorageSize(mStorage.getPrimaryStorageSize());
} else {//代码2
final VolumeInfo vol = mStorage.findVolumeByUuid(volumeUuid);
if (vol == null) {
throw new ParcelableException(
new IOException("Failed to find storage device for UUID " + volumeUuid));
}
Slog.e(TAG," vol.disk.size:"+vol.disk.size);
Slog.e(TAG," roundStorageSize:"+FileUtils.roundStorageSize(vol.disk.size));
return FileUtils.roundStorageSize(vol.disk.size);
}
}
}
打印结果为
vol.disk.size:32010928128L
roundStorageSize:64000000000
由此发现问题出现在 FileUtils.roundStorageSize方法中
android.os.FileUtils
class FileUtils {
/**
* 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;
}
}
该方法让数据的非0部分都是2的整数倍。但是这种方法就导致了当数据即使只是超过 1 byte 也会出现直接显示双倍的大小。并且,我搜索了下google 自己的Settings代码,好像也是这样的。
至于Google 为什么这么计算,我有个大概的猜想,在市面上一般磁盘的大小、sd卡的大小都是使用 10进制的。所以往往标注了 32GB的存储卡,可能实际大小都是不足 32G的,不仅没有 2^32 byte,甚至没有32E9 byte.因此大部分时侯显示的都是正确的,而这边这张 sd卡过于实在。。。。居然超过了该大小,因此导致了该问题的出现。
问题解决
问题的解决方法,其实就是 将StorageUsageProgressBarPreferenceController 类中的
mStorageStatsManager.getTotalBytes(mStorageEntry.getFsUuid());方法改为如下代码
public static long getTotalBytes(String volumeUuid) {
storageManager = context.getApplicationContext().getSystemService(StorageManager.class);
if (Objects.equals(volumeUuid,StorageManager.UUID_PRIVATE_INTERNAL)) {
return FileUtils.roundStorageSize(storageManager.getPrimaryStorageSize());
} else {
final VolumeInfo vol = storageManager.findVolumeByUuid(volumeUuid);
if (vol == null) {
throw new ParcelableException(
new IOException("Failed to find storage device for UUID " + volumeUuid));
}
return vol.disk.size;
}