Android老旧机型存储空间管理:内置存储与TF卡容量获取完整方案实现

作者:袖梨 2026-06-02

针对老旧Android设备开发中存储空间展示的痛点,本文提供了一套完整的解决方案,涵盖TF卡路径获取、容量计算及可视化展示等关键技术点。

Android 老设备存储空间展示:机身存储 + TF 卡容量获取完整实现

在电视、机顶盒等嵌入式设备开发过程中,经常需要同时展示内置存储和外置TF卡的容量信息。传统手机方案仅适配内置存储,无法兼容外置存储路径获取、厂商定制系统特性以及老旧设备的特殊适配需求。

本文提供的解决方案包含完整工具类和业务页面实现:通过反射调用StorageManager隐藏API获取TF卡真实路径与挂载状态,结合StatFs计算总空间和剩余空间。同时兼容厂商系统属性读取、空间倍率换算和存储容量兜底修正,内置进度条UI、容量格式化展示以及无TF卡空态布局,代码可直接在老旧项目、TV和嵌入式设备中复用。

1. 权限配置

首先在AndroidManifest.xml中添加必要权限:

<!-- SD卡挂载权限 -->
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
    tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

2. 布局文件实现

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/black"
    android:orientation="vertical">
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        
        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="@dimen/x2"
            android:text=""
            android:textColor="@color/white"
            android:textSize="@dimen/x11" />
        
        <TextView
            android:layout_width="match_parent"
            android:layout_height="1px"
            android:layout_marginTop="@dimen/x3"
            android:background="@drawable/line_gradient" />
    </LinearLayout>
    
    <TextView
        android:id="@+id/tv_xtkj"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginRight="@dimen/x4"
        android:text=""
        android:textColor="@color/white"
        android:textSize="@dimen/x9" />
    
    <SeekBar
        android:id="@+id/seekbar1"
        android:layout_width="match_parent"
        android:layout_height="@dimen/x6"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginTop="@dimen/x2"
        android:layout_marginRight="@dimen/x4"
        android:layout_marginBottom="@dimen/x2"
        android:max="255"
        android:maxHeight="@dimen/x6"
        android:minHeight="@dimen/x6"
        android:progressDrawable="@drawable/seekbar_style"
        android:thumb="@null"
        android:thumbOffset="0dip" />
    
    <TextView
        android:id="@+id/tv_xtkj_yy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginRight="@dimen/x4"
        android:text=""
        android:textColor="@color/white"
        android:textSize="@dimen/x10" />
    
    <TextView
        android:id="@+id/tv_tfk"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginTop="@dimen/x15"
        android:layout_marginRight="@dimen/x4"
        android:text=""
        android:textColor="@color/white"
        android:textSize="@dimen/x9"
        android:visibility="gone" />
    
    <SeekBar
        android:id="@+id/seekbar2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginTop="@dimen/x2"
        android:layout_marginRight="@dimen/x4"
        android:layout_marginBottom="@dimen/x2"
        android:max="255"
        android:maxHeight="@dimen/x6"
        android:minHeight="@dimen/x6"
        android:progressDrawable="@drawable/seekbar_style2"
        android:thumb="@null"
        android:thumbOffset="0dip"
        android:visibility="gone" />
    
    <TextView
        android:id="@+id/tv_tfk_yy"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="@dimen/x4"
        android:layout_marginRight="@dimen/x4"
        android:text=""
        android:textColor="@color/white"
        android:textSize="@dimen/x10"
        android:visibility="gone" />
    
    <LinearLayout
        android:id="@+id/ll_no_data"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:orientation="vertical"
        android:visibility="gone">
        <ImageView
            android:layout_width="@dimen/x57"
            android:layout_height="@dimen/x55"
            android:layout_marginTop="@dimen/x15"
            android:src="@mipmap/img_kzt"/>
        <TextView
            android:id="@+id/tv_tips"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textSize="@dimen/x12"
            android:layout_marginTop="@dimen/x5"
            android:layout_marginLeft="@dimen/x6"
            android:layout_marginRight="@dimen/x6"
            android:textColor="@color/white"
            android:text="暂无内容,请插上TF卡"/>
    </LinearLayout>
</LinearLayout>

3. Java代码实现

public class StorageSpaceActivity extends BaseActivity<ActivityStorageSpaceBinding> {
    TextView tv_title;
    TextView tv_xtkj;  //系统空间
    SeekBar seekbar1;
    TextView tv_xtkj_yy; //系统空间 已用
    TextView tv_tfk;  //TF卡
    SeekBar seekbar2;
    TextView tv_tfk_yy; //TF卡 已用
    LinearLayout ll_no_data;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_storage_space);
        //隐藏状态栏
        this.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);

        tv_title = findViewById(R.id.tv_title);
        tv_title.setText("存储空间");

        tv_xtkj = findViewById(R.id.tv_xtkj);
        seekbar1 = findViewById(R.id.seekbar1);
        tv_xtkj_yy = findViewById(R.id.tv_xtkj_yy);
        tv_tfk = findViewById(R.id.tv_tfk);
        seekbar2 = findViewById(R.id.seekbar2);
        tv_tfk_yy = findViewById(R.id.tv_tfk_yy);
        ll_no_data = findViewById(R.id.ll_no_data);
        seekbar1.setFocusable(false);
        seekbar2.setFocusable(false);

        tv_xtkj.setVisibility(View.GONE);
        seekbar1.setVisibility(View.GONE);
        tv_xtkj_yy.setVisibility(View.GONE);
        initdata();
    }

    @Override
    protected void init() {
    }

    @Override
    public int setLayoutID() {
        return R.layout.activity_storage_space;
    }

    private void initdata() {
        try {
            float total = SDCardUtils.getTotalSize(StorageSpaceActivity.this);
            int hongyao = SystemPropertiesProxy.getInt(StorageSpaceActivity.this, SDCardUtils.MEMORY_FR, 0);
            //剩余空间
            float freeSpace = SDCardUtils.getFreeSize(StorageSpaceActivity.this, hongyao);
            Log.e("TAG", "total " + total);
            Log.e("TAG", "hongyao " + hongyao);
            Log.e("TAG", "freeSpace " + freeSpace);
            tv_xtkj.setText("系统空间(可用" + freeSpace + ")");
            //已使用
            float usemem = (float) (Math.round((total - freeSpace) * 100)) / 100;
            tv_xtkj_yy.setText("已用" + usemem + "/" + total);
            seekbar1.setMax((int) (total));
            seekbar1.setProgress((int) (usemem));
        } catch (Exception e) {
            //内存路径
            String innert = Environment.getExternalStorageDirectory().getPath();
            //总空间
            long total = SDCardUtils.getTotalInternalMemorySize(innert);
            if(total > 20L * 1024L * 1024L * 1024L){
                total = 32L * 1024L * 1024L * 1024L;
            }else if(total > 10L * 1024L * 1024L * 1024L){
                total = 16L * 1024L * 1024L * 1024L;
            }else if(total > 6L * 1024L * 1024L * 1024L){
                total = 8L * 1024L * 1024L * 1024L;
            }else if(total > 4L * 1024L * 1024L * 1024L){
                total = 6L * 1024L * 1024L * 1024L;
            }else {
                total = 4L * 1024L * 1024L * 1024L;
            }
            //剩余空间
            long freeSpace = SDCardUtils.getFreeSpace(innert);
            Log.e("TAG", "total2 " + total);
            Log.e("TAG", "freeSpace2 " + freeSpace);
            tv_xtkj.setText("系统空间(可用" + Formatter.formatFileSize(StorageSpaceActivity.this, freeSpace) + ")");
            //已使用
            long usemem = total - freeSpace;
            tv_xtkj_yy.setText("已用" + Formatter.formatFileSize(StorageSpaceActivity.this, usemem) + "/" + Formatter.formatFileSize(StorageSpaceActivity.this, total));
            seekbar1.setMax((int) (total / 1024));
            seekbar1.setProgress((int) (usemem / 1024));
        }

        //SD卡路径
        String sDcardDir = SDCardUtils.getTfStorageDirectory(this);
        if (!TextUtils.isEmpty(sDcardDir)) {
            //总空间
            long total2 = SDCardUtils.getTotalInternalMemorySize(sDcardDir);
            if(total2 > 0){
                tv_tfk.setVisibility(View.VISIBLE);
                seekbar2.setVisibility(View.VISIBLE);
                tv_tfk_yy.setVisibility(View.VISIBLE);
            }
            //剩余空间
            long freeSpace2 = SDCardUtils.getFreeSpace(sDcardDir);
            Log.e("TAG", "total3 " + total2);
            Log.e("TAG", "freeSpace3 " + freeSpace2);
            tv_tfk.setText("TF卡(可用" + Formatter.formatFileSize(StorageSpaceActivity.this, freeSpace2) + ")");
            //已使用
            long sdusemem = total2 - freeSpace2;
            tv_tfk_yy.setText("已用" + Formatter.formatFileSize(StorageSpaceActivity.this, sdusemem) + "/" + Formatter.formatFileSize(StorageSpaceActivity.this, total2));
            seekbar2.setMax((int) (total2 / 1024));
            seekbar2.setProgress((int) (sdusemem / 1024));
        }else {
            ll_no_data.setVisibility(View.VISIBLE);
        }
    }
}

4. 工具类实现

public class SDCardUtils {
    
    private static final String TYPE_CACHE = "cache";
    
    private static final String TYPE_FILES = "files";
    
    private static String sTfDir = "";
    private static final String MEMORY_ROM = "persist.sys.memory_rom";//总内存属性
    public static final String MEMORY_FR = "ro.sys.memory_rom.fr"; //真假空间属性
    
    /**
     * SD卡是否挂载
     * 
     * @return
     */
    public static boolean isMounted() {
        String status = Environment.getExternalStorageState();
        return status.equals(Environment.MEDIA_MOUNTED) ? true : false;
    }

    //判断是否存在外置tf卡
    public static boolean isStorageTf(Context context) {
        try {
            StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
            Method getVolumeStateMethod = StorageManager.class.getMethod("getVolumeState", new Class[]{String.class});
            String state = (String) getVolumeStateMethod.invoke(sm, getTfStoragePath(context));
            if (state.equals(Environment.MEDIA_MOUNTED)) {
                return true;
            }
        } catch (Exception e) {
            Log.e("TF error", "not find tf", e);
        }
        return false;
    }

    //获取外置tf卡路径
    public static String getTfStoragePath(Context context) {
        try {
            @SuppressLint("WrongConstant")
            StorageManager sm = (StorageManager) context.getSystemService("storage");
            Method getVolumePathsMethod = StorageManager.class.getMethod("getVolumePaths", new Class[0]);
            String[] paths = (String[]) getVolumePathsMethod.invoke(sm, new Object[]{});
            // second element in paths[] is secondary storage path
            return paths[1];
        } catch (Exception e) {
            Log.e("TF error", "get Tf failed", e);
        }
        return null;
    }
    
    /**
     * 获取tf卡根目录
     * @param context
     * @return
     */
    public static String getExternalStorageDirectoryPath(Context context) {
        sTfDir = getTfStorageDirectory(context);
        return sTfDir;
    } 
    
    /**
     * 获取tf卡根目录
     * @param context
     * @return
     */
    public static String getTfStorageDirectory(Context context) {
        String tfDir = null;
        StorageManager sm = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
        Class<?> smc = sm.getClass();
        
        try {
            Method getPaths = smc.getMethod("getVolumePaths", new  Class[0]);
            String[] paths = (String[])getPaths.invoke(sm, new  Object[]{});
            if (paths.length >= 2) {
                tfDir = paths[1];
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        
        return tfDir;
    }

    /**
     * 获取手机内部总的存储空间
     * @return
     */
    public static long getTotalInternalMemorySize(String rootPath) {
        StatFs stat = new StatFs(rootPath);
        long blockSize = stat.getBlockSizeLong();
        long totalBlocks = stat.getBlockCountLong();
        return (totalBlocks * blockSize);
    }

    /**
     * 获取剩余内存空间
     */
    public static long getFreeSpace(String rootPath) {
        StatFs stat = new StatFs(rootPath);
        //获取单个数据块的大小(Byte)
        long blockSize = stat.getBlockSizeLong();
        //空闲的数据块的数量
        long availableBlocks = stat.getAvailableBlocksLong();
        //单位Byte
        return availableBlocks * blockSize;
    }

    public static float getFreeSize(Context context,int isTrue){
        float freeSize = getAvailableSpace(context);
        if(isTrue == 1){
            freeSize = freeSize * 2;
        }
        return freeSize;
    }

    public static float getTotalSize(Context context ){
        String free_memory = SystemPropertiesProxy.get(context,MEMORY_ROM);
        float freeSize = Float.parseFloat(free_memory);
        return freeSize;
    }

    public static float getAvailableSpace(Context context){
        String path = "/storage/emulated/0";
        StatFs statFs = new StatFs(path);
        long blockSize = statFs.getBlockSizeLong();
        long availableBlocks = statFs.getAvailableBlocksLong();
        long ava_length = availableBlocks*blockSize;
        float f = Float.parseFloat(String.valueOf(ava_length));
        float  available   =  (float)(Math.round(((f/1024/1024/1024)-0.01)*100))/100;
        return (float) (available) ;
    }
}

5. 代码实现详解

5.1 权限说明

MOUNT_UNMOUNT_FILESYSTEMS是老设备文件挂载必备权限,虽然在高版本中被标记为受保护权限,但在机顶盒和老旧安卓设备上仍需声明才能正常读取TF卡状态。

5.2 反射获取TF卡路径和挂载状态

getTfStoragePathgetTfStorageDirectory方法通过反射StorageManager的隐藏方法getVolumePaths获取第二分区路径作为TF卡路径。isStorageTf方法则通过反射getVolumeState判断TF卡是否正常挂载,这是嵌入式设备的标准实现方式。

5.3 StatFs存储空间计算原理

使用StatFs获取块大小、总块数和可用块数,通过计算得到总容量和剩余容量,再使用系统Formatter自动适配GB/MB单位进行格式化展示。

5.4 厂商系统属性适配

persist.sys.memory_rom
ro.sys.memory_rom.fr

针对厂商定制系统常见的虚拟内存特性,通过读取系统属性获取标称总容量和空间倍率标识,在代码中实现动态倍率换算,确保适配各种魔改系统。

5.5 SeekBar作为进度条禁用拖动

通过设置focusable=false和移除滑块,将SeekBar改造为只读的存储占用进度展示条,符合设备系统存储页面的交互规范。

本文提供的解决方案全面覆盖了老旧Android设备存储空间展示的各类技术难点,从底层API调用到UI展示都做了充分适配,可直接应用于各类嵌入式设备开发场景。

相关文章

精彩推荐