LoginSignup
9
8

More than 3 years have passed since last update.

[Android] Disk Fullって怖いよね

Posted at

Androidでは、基本的にシステムもアプリも永続的データは/dataパーティションに書き込む。

そのため、システムの最低限の動作が継続できるように、アプリがデータを書きすぎないための仕組みがいくつか入っている。

本記事では、それらの仕組みについて紹介する。

なお、ソースコードは下記のバージョンを参照している。
- Android: android-10.0.0_r5
- Linux kernel: 5.3.7
- e2fsprogs: 1.45.4

1. ストレージの空き容量を確認するAPI

File#getTotalSpace(), File#getFreeSpace(), File#getUsableSpace()

いずれも最終的にはJNIでstatvfs(3)を、すなわちstatfs(2)を呼び出す。

File#getTotalSpace()statfs.f_blocks * statfs.f_bsizeに相当し、パーティションの全サイズを返す。
df /dataしたときの1K-blocksと一致する。

File#getFreeSpace()statfs.f_bfree * statfs.f_bsizeに相当し、実際の空き領域を返す。
rootユーザでdf /dataしたときのAvailableと一致する。

File#getUsableSpace()statfs.f_bavail * statfs.f_bsizeに相当し、非特権ユーザが使用可能な空き領域を返す。
一般ユーザでdf /dataしたときのAvailableと一致する。

libcore/ojluni/src/main/java/java/io/File.java
    public long getTotalSpace() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
            sm.checkRead(path);
        }
        if (isInvalid()) {
            return 0L;
        }
        return fs.getSpace(this, FileSystem.SPACE_TOTAL);
    }
...
    public long getFreeSpace() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
            sm.checkRead(path);
        }
        if (isInvalid()) {
            return 0L;
        }
        return fs.getSpace(this, FileSystem.SPACE_FREE);
    }
...
    public long getUsableSpace() {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("getFileSystemAttributes"));
            sm.checkRead(path);
        }
        if (isInvalid()) {
            return 0L;
        }
        return fs.getSpace(this, FileSystem.SPACE_USABLE);
    }
libcore/ojluni/src/main/java/java/io/UnixFileSystem.java
    public long getSpace(File f, int t) {
        BlockGuard.getThreadPolicy().onReadFromDisk();
        BlockGuard.getVmPolicy().onPathAccess(f.getPath());

        return getSpace0(f, t);
    }
libcore/ojluni/src/main/native/UnixFileSystem_md.c
#define statvfs64 statvfs
...
JNIEXPORT jlong JNICALL
Java_java_io_UnixFileSystem_getSpace0(JNIEnv *env, jobject this,
                                      jobject file, jint t)
{
    jlong rv = 0L;

    WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
        struct statvfs64 fsstat;
        memset(&fsstat, 0, sizeof(fsstat));
        if (statvfs64(path, &fsstat) == 0) {
            switch(t) {
            case java_io_FileSystem_SPACE_TOTAL:
                rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
                               long_to_jlong(fsstat.f_blocks));
                break;
            case java_io_FileSystem_SPACE_FREE:
                rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
                               long_to_jlong(fsstat.f_bfree));
                break;
            case java_io_FileSystem_SPACE_USABLE:
                rv = jlong_mul(long_to_jlong(fsstat.f_frsize),
                               long_to_jlong(fsstat.f_bavail));
                break;
            default:
                assert(0);
            }
        }
    } END_PLATFORM_STRING(env, path);
    return rv;
}

statvfs(3)は、Androidの独自libcであるbionicで実装されており、statfs(2)を呼び出す。

bionic/libc/bionic/statvfs.cpp
int statvfs(const char* path, struct statvfs* result) {
  struct statfs tmp;
  int rc = statfs(path, &tmp);
  if (rc != 0) {
    return rc;
  }
  __statfs_to_statvfs(tmp, result);
  return 0;
}
__strong_alias(statvfs64, statvfs);

man statfs(2)によると、f_bfreeはすべてのfree block、f_bavailは非特権ユーザにとってのfree block。
f_bfreef_bavailの差分の詳細については後述する。

               fsblkcnt_t f_bfree;   /* Free blocks in filesystem */
               fsblkcnt_t f_bavail;  /* Free blocks available to
                                        unprivileged user */

StorageManager#getAllocatableBytes()

Android 8.0から導入された、アプリが確保可能な最大サイズを返すAPI。
大きなファイルを書き込む場合、事前に空き領域を確認するならこのAPIがもっとも適切な値を返す。

計算式は(File#getUsableSpace() + cacheClearable) - StorageManager#getStorageLowBytes()
File#getUsableSpace()に削除可能なキャッシュのサイズを加え、システムとして残して置かなければならないサイズまでの余裕を求めている。

削除可能なキャッシュのサイズは、StorageStatsManager#getCacheBytes() - StorageManager#getStorageCacheBytesで求めている。

注意点として、SystemServiceへのBinder呼び出しが発生するので、完了までに数秒かかる可能性がある。
そのためUIスレッドから呼び出すべきではなく、30秒に1回以上呼び出すべきではない。

frameworks/base/services/core/java/com/android/server/StorageManagerService.java
    public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) {
        flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);

        final StorageManager storage = mContext.getSystemService(StorageManager.class);
        final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
        final long token = Binder.clearCallingIdentity();
        try {
            // In general, apps can allocate as much space as they want, except
            // we never let them eat into either the minimum cache space or into
            // the low disk warning space. To avoid user confusion, this logic
            // should be kept in sync with getFreeBytes().
            final File path = storage.findPathForUuid(volumeUuid);

            final long usable = path.getUsableSpace();
            final long lowReserved = storage.getStorageLowBytes(path);
            final long fullReserved = storage.getStorageFullBytes(path);

            if (stats.isQuotaSupported(volumeUuid)) {
                final long cacheTotal = stats.getCacheBytes(volumeUuid);
                final long cacheReserved = storage.getStorageCacheBytes(path, flags);
                final long cacheClearable = Math.max(0, cacheTotal - cacheReserved);

                if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                    return Math.max(0, (usable + cacheClearable) - fullReserved);
                } else {
                    return Math.max(0, (usable + cacheClearable) - lowReserved);
                }
            } else {
                // When we don't have fast quota information, we ignore cached
                // data and only consider unused bytes.
                if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                    return Math.max(0, usable - fullReserved);
                } else {
                    return Math.max(0, usable - lowReserved);
                }
            }
        } catch (IOException e) {
            throw new ParcelableException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

StorageManager#getAllocatableBytes()を呼び出した結果、必要なサイズの空きがない場合は、
Intent.ACTION_MANAGE_STORAGEEXTRA_UUIDEXTRA_REQUESTED_BYTESを付与して投げればユーザにディスクの解放を促すことができる。
下記のようにDeletionHelperActivityが立ち上がる。

Screenshot_1572827591.png

File#getFreeSpace()File#getUsableSpaceの差分はなに?

File#getFreeSpace()File#getUsableSpaceの差分は、
ext4ファイルシステムの場合、特権ユーザしか確保できない領域のサイズと、extent用の予約領域からなる。

下記はext4のstatfs(2)の実装である。

fs/ext4/super.c
static int ext4_statfs(struct dentry *dentry, struct kstatfs *buf)
{
    struct super_block *sb = dentry->d_sb;
    struct ext4_sb_info *sbi = EXT4_SB(sb);
    struct ext4_super_block *es = sbi->s_es;
    ext4_fsblk_t overhead = 0, resv_blocks;
    u64 fsid;
    s64 bfree;
    resv_blocks = EXT4_C2B(sbi, atomic64_read(&sbi->s_resv_clusters));

    if (!test_opt(sb, MINIX_DF))
        overhead = sbi->s_overhead;

    buf->f_type = EXT4_SUPER_MAGIC;
    buf->f_bsize = sb->s_blocksize;
    buf->f_blocks = ext4_blocks_count(es) - EXT4_C2B(sbi, overhead);
    bfree = percpu_counter_sum_positive(&sbi->s_freeclusters_counter) -
        percpu_counter_sum_positive(&sbi->s_dirtyclusters_counter);
    /* prevent underflow in case that few free space is available */
    buf->f_bfree = EXT4_C2B(sbi, max_t(s64, bfree, 0));
    buf->f_bavail = buf->f_bfree -
            (ext4_r_blocks_count(es) + resv_blocks);
    if (buf->f_bfree < (ext4_r_blocks_count(es) + resv_blocks))
        buf->f_bavail = 0;
    buf->f_files = le32_to_cpu(es->s_inodes_count);
    buf->f_ffree = percpu_counter_sum_positive(&sbi->s_freeinodes_counter);
    buf->f_namelen = EXT4_NAME_LEN;
    fsid = le64_to_cpup((void *)es->s_uuid) ^
           le64_to_cpup((void *)es->s_uuid + sizeof(u64));
    buf->f_fsid.val[0] = fsid & 0xFFFFFFFFUL;
    buf->f_fsid.val[1] = (fsid >> 32) & 0xFFFFFFFFUL;

#ifdef CONFIG_QUOTA
    if (ext4_test_inode_flag(dentry->d_inode, EXT4_INODE_PROJINHERIT) &&
        sb_has_quota_limits_enabled(sb, PRJQUOTA))
        ext4_statfs_project(sb, EXT4_I(dentry->d_inode)->i_projid, buf);
#endif
    return 0;
}

特権ユーザしか確保できない領域のブロック数は、スーパーブロックのs_r_blocks_count_loに記録されている。
ext4_r_blocks_count(es)のサイズはtune2fs -lで確認できる。
"Reserved block count" * "Block size"が特権ユーザ用に予約されているサイズとなる。

$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep -e "Reserved block count:" -e "Block size:"
Reserved block count:     4096
Block size:               4096

extent用の予約領域は、全体の2%か4096クラスタの小さい方のサイズが割り当てられる。
ちなみに、extentはext4で導入されたファイルのデータ位置の管理方式の一つで、連続領域をまとめて表現することで効率を上げている。
なぜextent方式を使うときだけ予備領域が必要になるかはよくわからなかった。
(おわかりの方がいらっしゃいましたら、ぜひ教えてください)

実際の設定値は/sys/fs/ext4/{block_device_name}/reserved_clustersからクラスタ数が確認できる。
クラスタのサイズは、tune2fs -lFragment sizeで確認できる。

fs/ext4/super.c
static void ext4_set_resv_clusters(struct super_block *sb)
{
    ext4_fsblk_t resv_clusters;
    struct ext4_sb_info *sbi = EXT4_SB(sb);

    /*
     * There's no need to reserve anything when we aren't using extents.
     * The space estimates are exact, there are no unwritten extents,
     * hole punching doesn't need new metadata... This is needed especially
     * to keep ext2/3 backward compatibility.
     */
    if (!ext4_has_feature_extents(sb))
        return;
    /*
     * By default we reserve 2% or 4096 clusters, whichever is smaller.
     * This should cover the situations where we can not afford to run
     * out of space like for example punch hole, or converting
     * unwritten extents in delalloc path. In most cases such
     * allocation would require 1, or 2 blocks, higher numbers are
     * very rare.
     */
    resv_clusters = (ext4_blocks_count(sbi->s_es) >>
             sbi->s_cluster_bits);

    do_div(resv_clusters, 50);
    resv_clusters = min_t(ext4_fsblk_t, resv_clusters, 4096);

    atomic64_set(&sbi->s_resv_clusters, resv_clusters);
}
$ adb shell 'cat /sys/fs/ext4/$(basename $(mount | grep " on /data " | cut -d" " -f1))/reserved_clusters'
4096
$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep -e "Fragment size:"
Fragment size:            4096
lib/e2p/ls.c
void list_super2(struct ext2_super_block * sb, FILE *f)
{
...
    if (ext2fs_has_feature_bigalloc(sb))
        fprintf(f, "Cluster size:             %u\n",
            EXT2_CLUSTER_SIZE(sb));
    else
        fprintf(f, "Fragment size:            %u\n",
            EXT2_CLUSTER_SIZE(sb));
...

2. ストレージの空き容量をモニターするSystemService DeviceStorageMonitorService

DEFAULT_CHECK_INTERVAL(1分)ごとに、File#getUsableSpace()を確認し、
下記3段階の閾値と比較して下回ったときに処理を実行する。

  • StorageManager#getStorageLowBytes() * 1.5

    • PackageManagerService#freeStorage()を呼び出しキャッシュを解放する。
  • StorageManager#getStorageLowBytes()

    • Notification Centerに容量不足を通知する。この通知をtapすると、DeletionHelperActivityに遷移する。
    • Intent.ACTION_DEVICE_STORAGE_LOWをBroadcastする。
  • StorageManager#getStorageLowBytes()

    • Intent.ACTION_DEVICE_STORAGE_FULLをBroadcastする。

なお、dumpsys devicestoragemonitor force-low -fで、擬似的にLEVEL_LOWにすることができる。
また、dumpsys devicestoragemonitor reset -fで、元に戻すことができる。
(force-fullも欲しかった。。。)

frameworks/base/services/core/java/com/android/server/storage/DeviceStorageMonitorService.java
    @WorkerThread
    private void check() {
        final StorageManager storage = getContext().getSystemService(StorageManager.class);
        final int seq = mSeq.get();

        // Check every mounted private volume to see if they're low on space
        for (VolumeInfo vol : storage.getWritablePrivateVolumes()) {
            final File file = vol.getPath();
            final long fullBytes = storage.getStorageFullBytes(file);
            final long lowBytes = storage.getStorageLowBytes(file);

            // Automatically trim cached data when nearing the low threshold;
            // when it's within 150% of the threshold, we try trimming usage
            // back to 200% of the threshold.
            if (file.getUsableSpace() < (lowBytes * 3) / 2) {
                final PackageManagerService pms = (PackageManagerService) ServiceManager
                        .getService("package");
                try {
                    pms.freeStorage(vol.getFsUuid(), lowBytes * 2, 0);
                } catch (IOException e) {
                    Slog.w(TAG, e);
                }
            }

            // Send relevant broadcasts and show notifications based on any
            // recently noticed state transitions.
            final UUID uuid = StorageManager.convert(vol.getFsUuid());
            final State state = findOrCreateState(uuid);
            final long totalBytes = file.getTotalSpace();
            final long usableBytes = file.getUsableSpace();

            int oldLevel = state.level;
            int newLevel;
            if (mForceLevel != State.LEVEL_UNKNOWN) {
                // When in testing mode, use unknown old level to force sending
                // of any relevant broadcasts.
                oldLevel = State.LEVEL_UNKNOWN;
                newLevel = mForceLevel;
            } else if (usableBytes <= fullBytes) {
                newLevel = State.LEVEL_FULL;
            } else if (usableBytes <= lowBytes) {
                newLevel = State.LEVEL_LOW;
            } else if (StorageManager.UUID_DEFAULT.equals(uuid) && !isBootImageOnDisk()
                    && usableBytes < BOOT_IMAGE_STORAGE_REQUIREMENT) {
                newLevel = State.LEVEL_LOW;
            } else {
                newLevel = State.LEVEL_NORMAL;
            }

            // Log whenever we notice drastic storage changes
            if ((Math.abs(state.lastUsableBytes - usableBytes) > DEFAULT_LOG_DELTA_BYTES)
                    || oldLevel != newLevel) {
                EventLogTags.writeStorageState(uuid.toString(), oldLevel, newLevel,
                        usableBytes, totalBytes);
                state.lastUsableBytes = usableBytes;
            }

            updateNotifications(vol, oldLevel, newLevel);
            updateBroadcasts(vol, oldLevel, newLevel, seq);

            state.level = newLevel;
        }

        // Loop around to check again in future; we don't remove messages since
        // there might be an immediate request pending.
        if (!mHandler.hasMessages(MSG_CHECK)) {
            mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_CHECK),
                    DEFAULT_CHECK_INTERVAL);
        }
    }

ストレージがLEVEL_LOWになったときに表示される通知は以下のとおり。

Screenshot_1572828758.png

StorageManager.getStorageLowBytes()

AndroidのストレージがLEVEL_LOWだとする閾値を求めるAPI。
デフォルトでは、ストレージサイズの5%か、500MiBの小さい方になる。
閾値となる割合もサイズも、Settingsで変更可能。
割合はsys_storage_threshold_percentage、サイズはsys_storage_threshold_max_bytes

frameworks/base/core/java/android/os/storage/StorageManager.java
    private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
    private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);
...
    @UnsupportedAppUsage
    public long getStorageLowBytes(File path) {
        final long lowPercent = Settings.Global.getInt(mResolver,
                Settings.Global.SYS_STORAGE_THRESHOLD_PERCENTAGE, DEFAULT_THRESHOLD_PERCENTAGE);
        final long lowBytes = (path.getTotalSpace() * lowPercent) / 100;

        final long maxLowBytes = Settings.Global.getLong(mResolver,
                Settings.Global.SYS_STORAGE_THRESHOLD_MAX_BYTES, DEFAULT_THRESHOLD_MAX_BYTES);

        return Math.min(lowBytes, maxLowBytes);
    }

StorageManager.getStorageFullBytes

AndroidのストレージがLEVEL_FULLだとする閾値を求めるAPI。
デフォルトでは、1MiBになる。
Settings sys_storage_full_threshold_bytesで変更可能。

frameworks/base/core/java/android/os/storage/StorageManager.java
    private static final long DEFAULT_FULL_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(1);
...
    @UnsupportedAppUsage
    public long getStorageFullBytes(File path) {
        return Settings.Global.getLong(mResolver, Settings.Global.SYS_STORAGE_FULL_THRESHOLD_BYTES,
                DEFAULT_FULL_THRESHOLD_BYTES);
    }

3. アプリケーションが暴走して書き過ぎてしまうのを防止する

Android8.1まではquotactl(2)で上限が設定されていた。
installdによってアプリのインストール時に設定され、アプリによらず全てストレージサイズの9割になっていた。
quotaはuidごとに設定されるので、各アプリの使用サイズがストレージサイズの9割を超えたら、EDQUOTで書き込みが失敗するようになる。
ファイルシステムレベルでの書き込み禁止なので漏れの可能性が少なく、悪さをしたアプリ以外は正常動作を継続することができる。

ただしこの方式では、例えば2つアプリが5割ずつ書き込んだ場合はエラーにならず、
システムの動作に影響を及ぼしてしまう可能性があった。

frameworks/native/cmds/installd/InstalldNativeService.cpp
static int prepare_app_quota(const std::unique_ptr<std::string>& uuid, const std::string& device,
        uid_t uid) {
    if (device.empty()) return 0;

    struct dqblk dq;
    if (quotactl(QCMD(Q_GETQUOTA, USRQUOTA), device.c_str(), uid,
            reinterpret_cast<char*>(&dq)) != 0) {
        PLOG(WARNING) << "Failed to find quota for " << uid;
        return -1;
    }

#if APPLY_HARD_QUOTAS
    if ((dq.dqb_bhardlimit == 0) || (dq.dqb_ihardlimit == 0)) {
        auto path = create_data_path(uuid ? uuid->c_str() : nullptr);
        struct statvfs stat;
        if (statvfs(path.c_str(), &stat) != 0) {
            PLOG(WARNING) << "Failed to statvfs " << path;
            return -1;
        }

        dq.dqb_valid = QIF_LIMITS;
        dq.dqb_bhardlimit =
            (((static_cast<uint64_t>(stat.f_blocks) * stat.f_frsize) / 10) * 9) / QIF_DQBLKSIZE;
        dq.dqb_ihardlimit = (stat.f_files / 2);
        if (quotactl(QCMD(Q_SETQUOTA, USRQUOTA), device.c_str(), uid,
                reinterpret_cast<char*>(&dq)) != 0) {
            PLOG(WARNING) << "Failed to set hard quota for " << uid;
            return -1;
        } else {
            LOG(DEBUG) << "Applied hard quotas for " << uid;
            return 0;
        }
    } else {
        // Hard quota already set; assume it's reasonable
        return 0;
    }
#else
    // Hard quotas disabled
    return 0;
#endif
}

そこで、Android9以降では、FileSystemのReserved blockを活用するようになっている。
reserved_diskグループを用意し、特別にReserved blockへの書き込みを許可することで、
たとえアプリにディスクが書き潰されたとしても、最低限のシステムが動作できるようにしている。

$ adb shell 'tune2fs -l $(mount | grep " on /data " | cut -d" " -f1)' | grep "Reserved blocks"
Reserved blocks uid:      0 (user root)
Reserved blocks gid:      1065 (group reserved_disk)

例えば、zygotevoldはinitスクリプトでreserved_diskグループに追加されている。
といってもこれらはrootユーザで動作しているので、もともとReserved blockへ書き込み可能だが。
おそらく今後のメンテナンスのことを考えてグループに追加しているのだろう。
root以外のユーザでreserved_diskグループに属しているユーザは今回の調査では見つけられなかった。

system/vold/vold.rc
service vold /system/bin/vold \
        --blkid_context=u:r:blkid:s0 --blkid_untrusted_context=u:r:blkid_untrusted:s0 \
        --fsck_context=u:r:fsck:s0 --fsck_untrusted_context=u:r:fsck_untrusted:s0
    class core
    ioprio be 2
    writepid /dev/cpuset/foreground/tasks
    shutdown critical
    group root reserved_disk

なお、Reserved blockの数は、tune2fs -m or tune2fs -rで設定できる。
試したエミュレータの環境では、/dataパーティションの5%がReserved blockとなっていた。

4. 十分な空き領域があるストレージにしかアプリのインストールをしないPackageHelper#resolveInstallVolume

PackageHelper#resolveInstallVolume()でインストール先volumeを探すときに、
StorageManager#getAllocatableBytes()params.sizeBytesを比較する。

frameworks/base/services/core/java/com/android/server/pm/PackageInstallerService.java
    private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
frameworks/base/core/java/com/android/internal/content/PackageHelper.java
    public static String resolveInstallVolume(Context context, SessionParams params,
            TestableInterface testInterface) throws IOException {
        final StorageManager storageManager = testInterface.getStorageManager(context);
        final boolean forceAllowOnExternal = testInterface.getForceAllowOnExternalSetting(context);
        final boolean allow3rdPartyOnInternal =
                testInterface.getAllow3rdPartyOnInternalConfig(context);
        // TODO: handle existing apps installed in ASEC; currently assumes
        // they'll end up back on internal storage
        ApplicationInfo existingInfo = testInterface.getExistingAppInfo(context,
                params.appPackageName);

        // Figure out best candidate volume, and also if we fit on internal
        final ArraySet<String> allCandidates = new ArraySet<>();
        boolean fitsOnInternal = false;
        VolumeInfo bestCandidate = null;
        long bestCandidateAvailBytes = Long.MIN_VALUE;
        for (VolumeInfo vol : storageManager.getVolumes()) {
            if (vol.type == VolumeInfo.TYPE_PRIVATE && vol.isMountedWritable()) {
                final boolean isInternalStorage = ID_PRIVATE_INTERNAL.equals(vol.id);
                final UUID target = storageManager.getUuidForPath(new File(vol.path));
                final long availBytes = storageManager.getAllocatableBytes(target,
                        translateAllocateFlags(params.installFlags));
                if (isInternalStorage) {
                    fitsOnInternal = (params.sizeBytes <= availBytes);
                }
                if (!isInternalStorage || allow3rdPartyOnInternal) {
                    if (availBytes >= params.sizeBytes) {
                        allCandidates.add(vol.fsUuid);
                    }
                    if (availBytes >= bestCandidateAvailBytes) {
                        bestCandidate = vol;
                        bestCandidateAvailBytes = availBytes;
                    }
                }
            }
        }

(おまけ) ストレージへの書き込み負荷試験を行うアプリStorageEater

今回の振る舞いを実験するために、各APIでストレージの状況を表示しつつ、FileOutputStreamでひたすらデータを書く負荷アプリを作った。

takeoverjp/StorageEater: Android storage stress test application.

注意
ストレージフルを発生させるため、端末がまともに動かなくなる可能性があります。
復旧手段がない or 消えたら困るデータがストレージに保存されている場合は、動かさないでください。

[書き込み前]
Screenshot_1572829916.png

[書き込み後]
Screenshot_1572828794.png

まとめ

永続データを保存するのは、ほぼすべてのアプリが必要とする機能だからか、特別なPermissionは必要ない。
だからといって適当に使うと、特にレガシーなAndroidでは取り返しのつかない不具合を引き起こす可能性がある。
アプリを開発する場合は外部ストレージやCacheなど他の選択肢を考慮した上で、どうしても内部ストレージでないといけないときのみきちんとStorageManager#getAllocatableBytes()を確認した上で保存しなければならない。
一方で、プラットフォームを開発する場合は、無遠慮に書き込んでくるアプリのことも踏まえ、最低限のシステムの動作(せめてアプリのデータ削除までたどり着けること)を保証しなければならない。

参考

9
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
8