Android 存储体系:内部存储、SD 卡、U 盘挂载读写、工控文件持久化方案
工控设备的数据比女朋友还难伺候——丢了不行、路径搞错也不行、U盘没挂载就写更不行。今天就把Android存储体系给你从头捋到脚。
一、Android 存储架构总览
Android的存储体系可以简化成三大块:
| 存储类型 | 路径特征 | 是否需要权限 | 典型用途 |
|---|---|---|---|
| 内部存储 | /data/data/包名/ | 不需要 | 应用私有数据、数据库 |
| 外部存储(内部SD) | /storage/emulated/0/ | Android 10+需要 | 共享文件、日志导出 |
| 可移动存储(外部SD/U盘) | /storage/XXXX-XXXX/ | 需要SAF或StorageManager | 数据备份、固件升级 |
小提示:很多人被"外部存储"这个名字坑了。外部存储不是指SD卡——手机自带的那个32GB/128GB存储空间在Android眼里就是"外部存储"(External Storage),它是焊接在主板上的、根本拔不掉的。真正的SD卡叫"可移动存储"(Removable Storage)。
一张图理解它们的关系:
Android 文件系统 ├── 内部存储 (无需权限) │ ├── /data/data/com.example.industrial/ │ │ ├── files/ ← getFilesDir() │ │ ├── databases/ ← SQLite数据库 │ │ ├── shared_prefs/ ← SharedPreferences │ │ └── cache/ ← getCacheDir() │ └── /data/app/ ← APK安装位置 ├── 外部存储 (内部SD,需权限) │ ├── /storage/emulated/0/ │ │ ├── Download/ │ │ ├── DCIM/ │ │ └── Android/data/com.example.industrial/ │ │ ├── files/ ← getExternalFilesDir() │ │ └── cache/ ← getExternalCacheDir() └── 可移动存储 (U盘/SD卡) └── /storage/XXXX-XXXX/ ← 需要StorageManager枚举二、内部存储读写
内部存储是应用专属的沙盒目录,其他APP无法访问,也不需要任何权限。
2.1 常用路径获取方法
// 1. 应用私有文件目录 - 推荐存放配置文件FilefilesDir=context.getFilesDir();// /data/user/0/com.example.industrial/files/// 2. 缓存目录 - 系统空间不足时会自动清理FilecacheDir=context.getCacheDir();// /data/user/0/com.example.industrial/cache/// 3. 数据库默认目录FiledatabaseDir=context.getDatabasePath("my_db");// /data/user/0/com.example.industrial/databases/my_db// 4. SharedPreferences目录// /data/user/0/com.example.industrial/shared_prefs/2.2 配置文件读写
工控场景下,设备参数(采集频率、串口波特率、服务器地址等)需要持久化存储。推荐方案:
publicclassConfigManager{privatestaticfinalStringCONFIG_FILE="industrial_config.json";// 存储到应用私有目录,安全且不需要权限publicstaticvoidsaveConfig(Contextcontext,Stringjson){FileconfigFile=newFile(context.getFilesDir(),CONFIG_FILE);try(FileWriterwriter=newFileWriter(configFile)){writer.write(json);}catch(IOExceptione){e.printStackTrace();}}publicstaticStringloadConfig(Contextcontext){FileconfigFile=newFile(context.getFilesDir(),CONFIG_FILE);if(!configFile.exists())returnnull;try{returnnewString(Files.readAllBytes(configFile.toPath()));}catch(IOExceptione){returnnull;}}}为什么用getFilesDir()而不是外部存储?因为工控设备可能没有外置SD卡,而且应用私有目录不需要权限、其他APP访问不了,数据更安全。
2.3 采集数据的存储策略
对于大量传感器采集数据,SQLite是最成熟的方案:
publicclassDataRepository{privateSQLiteDatabasedatabase;publicDataRepository(Contextcontext){DataDbHelperhelper=newDataDbHelper(context.getFilesDir().getAbsolutePath());database=helper.getWritableDatabase();}publicvoidinsertSensorData(SensorDatadata){ContentValuesvalues=newContentValues();values.put("temperature",data.temperature);values.put("humidity",data.humidity);values.put("timestamp",System.currentTimeMillis());database.insert("sensor_data",null,values);}// 定期清理旧数据,防止存储爆满publicvoidcleanupOldData(longbeforeTimestamp){database.delete("sensor_data","timestamp < ?",newString[]{String.valueOf(beforeTimestamp)});}}内部存储虽然安全,但容量有限(通常几个GB)。对于长期运行且数据量大的工控设备,需要定期清理或转存到外部存储。
三、外部存储读写
3.1 公有目录与私有目录
// 公有目录 - 需要权限,所有APP都能访问FiledownloadDir=Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);// /storage/emulated/0/Download/// 应用私有外部目录 - Android 10+不需要权限,但需要用到分区存储APIFileexternalFilesDir=context.getExternalFilesDir(null);// /storage/emulated/0/Android/data/包名/files/// 应用外部缓存目录FileexternalCacheDir=context.getExternalCacheDir();// /storage/emulated/0/Android/data/包名/cache/在Android 10(API 29)之前,直接使用Environment.getExternalStorageDirectory()获取根目录就行。但从Android 10开始引入分区存储(Scoped Storage),强制应用使用MediaStore API或SAF框架访问共享文件。
工控设备如果跑的是Android 11+,需要在Manifest中声明:
<uses-permissionandroid:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>然后在代码中引导用户授权:
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){if(!Environment.isExternalStorageManager()){Intentintent=newIntent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);startActivity(intent);}}3.2 日志文件导出到外部存储
这是工控很常见的需求——设备出了问题,需要把日志导出来查看:
publicstaticvoidexportLogs(Contextcontext){FilelogDir=newFile(context.getExternalFilesDir(null),"logs");if(!logDir.exists())logDir.mkdirs();FilelogFile=newFile(logDir,"app_"+newSimpleDateFormat("yyyyMMdd_HHmmss",Locale.US).format(newDate())+".log");try(FileWriterwriter=newFileWriter(logFile)){writer.write("=== 工控APP运行日志 ===\n");writer.write("版本: "+BuildConfig.VERSION_NAME+"\n");writer.write("设备: "+Build.MODEL+"\n");writer.write("SDK: "+Build.VERSION.SDK_INT+"\n");// 写入更多日志内容...}catch(IOExceptione){e.printStackTrace();}}四、U盘挂载检测与读写
工控设备经常通过U盘做数据导入导出或固件升级,这是工控和普通手机APP需求差异最大的地方。
4.1 U盘挂载检测
U盘插入后,Android系统会自动挂载到/storage/下的某个目录(通常是/storage/XXXX-XXXX/格式)。我们需要通过StorageManager来获取挂载信息:
publicclassUsbStorageManager{privateStorageManagerstorageManager;publicUsbStorageManager(Contextcontext){storageManager=(StorageManager)context.getSystemService(Context.STORAGE_SERVICE);}/** * 获取所有可移动存储设备的路径 */publicList<File>getRemovableVolumes(){List<File>volumes=newArrayList<>();if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){List<StorageVolume>storageVolumes=storageManager.getStorageVolumes();for(StorageVolumevolume:storageVolumes){// 判断是否可移动存储if(volume.isRemovable()&&volume.getState().equals(Environment.MEDIA_MOUNTED)){FilevolumeDir;if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.R){volumeDir=volume.getDirectory();}else{// Android 10及以下用反射获取路径volumeDir=getVolumePathReflect(volume);}if(volumeDir!=null){volumes.add(volumeDir);}}}}// 兼容性方案:直接扫描 /storage/FilestorageDir=newFile("/storage/");File[]files=storageDir.listFiles();if(files!=null){for(Filefile:files){Stringname=file.getName();// U盘挂载点通常是 XXXX-XXXX 格式if(!name.equals("emulated")&&!name.equals("self")&&file.canRead()&&file.listFiles()!=null){if(!volumes.contains(file)){volumes.add(file);}}}}returnvolumes;}@SuppressWarnings("JavaReflectionMemberAccess")privateFilegetVolumePathReflect(StorageVolumevolume){try{Class<?>clazz=volume.getClass();Methodmethod;if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){method=clazz.getMethod("getPathFile");}else{method=clazz.getMethod("getPath");}return(File)method.invoke(volume);}catch(Exceptione){returnnull;}}}4.2 U盘文件读写
有了路径就可以用标准Java File API读写U盘了。下面是一个完整的文件读写工具类:
publicclassUsbFileManager{/** * 从U盘读取升级文件 */publicstaticFilefindUpgradeFile(FileusbRoot){File[]files=usbRoot.listFiles();if(files==null)returnnull;for(Filefile:files){if(file.getName().toLowerCase().endsWith(".apk")||file.getName().toLowerCase().endsWith(".zip")){returnfile;}}returnnull;}/** * 将采集数据导出到U盘 */publicstaticbooleanexportDataToUsb(FileusbRoot,StringfileName,byte[]data){FileexportDir=newFile(usbRoot,"IndustrialData");if(!exportDir.exists()){exportDir.mkdirs();}FileexportFile=newFile(exportDir,fileName);try(FileOutputStreamfos=newFileOutputStream(exportFile)){fos.write(data);fos.flush();returntrue;}catch(IOExceptione){e.printStackTrace();returnfalse;}}/** * 安全弹出U盘(卸载文件系统) */publicstaticbooleanunmountUsb(StorageManagerstorageManager,StorageVolumevolume){try{// 通过Intent请求卸载(兼容方案)// 实际卸载需要系统签名,普通APP只能通知用户拔出returntrue;}catch(Exceptione){returnfalse;}}}4.3 使用 Storage Access Framework(SAF)
Android 4.4 引入了 SAF 框架,可以不申请存储权限就访问外部存储:
publicclassSafHelper{privatestaticfinalintREQUEST_CODE_OPEN_DIR=2001;publicstaticvoidopenDirectory(Activityactivity){Intentintent=newIntent(Intent.ACTION_OPEN_DOCUMENT_TREE);intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION|Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);activity.startActivityForResult(intent,REQUEST_CODE_OPEN_DIR);}publicstaticUrihandleResult(intrequestCode,intresultCode,Intentdata){if(requestCode==REQUEST_CODE_OPEN_DIR&&resultCode==Activity.RESULT_OK){UritreeUri=data.getData();// 持久化权限,下次启动还能用inttakeFlags=data.getFlags()&(Intent.FLAG_GRANT_READ_URI_PERMISSION|Intent.FLAG_GRANT_WRITE_URI_PERMISSION);returntreeUri;}returnnull;}}对于工控场景,SAF的缺点是每次都要用户手动选择目录,不适合无人值守模式。建议:如果设备跑Android 10以上就用分区存储API,如果跑低版本直接走文件路径更省事。
五、工控文件持久化方案选择
| 数据类型 | 推荐方案 | 存储位置 | 理由 |
|---|---|---|---|
| 配置参数 | SharedPreferences / JSON文件 | getFilesDir() | 数据量小,读写频繁 |
| 采集数据 | SQLite数据库 | getDatabasePath() | 支持索引、事务、条件查询 |
| 图片/视频 | 文件系统 | getExternalFilesDir() | 文件大,需定期清理 |
| 日志文件 | 文件系统 | getFilesDir()+ 定期导出 | 私有目录安全,导出到U盘 |
| 固件升级包 | 文件系统 | U盘 / 私有目录 | 从U盘读取,校验后复制到内部 |
路径获取方法速查表
// ========== 内部存储 ==========context.getFilesDir();// 应用私有文件context.getCacheDir();// 应用缓存context.getDatabasePath("name");// 数据库路径context.getDir("name",MODE_PRIVATE);// 自定义目录Environment.getDataDirectory();// /data// ========== 外部存储(内部SD)==========Environment.getExternalStorageDirectory();// 根目录(Android 10+受限)context.getExternalFilesDir(null);// 应用专属外部目录context.getExternalCacheDir();// 应用外部缓存Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);// ========== 可移动存储 ==========// 通过StorageManager枚举,无标准API六、完整代码示例:U盘数据采集与导出
最后给出一个工控场景的完整示例——检测U盘插入后自动导出当天的采集数据:
publicclassUsbDataExporterextendsBroadcastReceiver{@OverridepublicvoidonReceive(Contextcontext,Intentintent){if(!UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())){return;}// 延迟2秒等待挂载完成newHandler(Looper.getMainLooper()).postDelayed(()->{exportTodayData(context);},2000);}privatevoidexportTodayData(Contextcontext){StorageManagersm=(StorageManager)context.getSystemService(Context.STORAGE_SERVICE);// 枚举可移动存储List<File>usbVolumes=newUsbStorageManager(context).getRemovableVolumes();if(usbVolumes.isEmpty()){Log.w("USB","未检测到可移动存储设备");return;}FileusbRoot=usbVolumes.get(0);StringtodayStr=newSimpleDateFormat("yyyyMMdd",Locale.US).format(newDate());// 从数据库导出今天的采集数据List<SensorData>todayData=queryTodayDataFromDb(context);StringBuildercsvBuilder=newStringBuilder();csvBuilder.append("时间,温度,湿度,压力\n");for(SensorDatadata:todayData){csvBuilder.append(data.toCsvLine()).append("\n");}// 写入U盘booleansuccess=UsbFileManager.exportDataToUsb(usbRoot,"采集数据_"+todayStr+".csv",csvBuilder.toString().getBytes(StandardCharsets.UTF_8));Log.i("USB","数据导出"+(success?"成功":"失败"));}}Android存储看着复杂,其实核心就三句话:私有的放getFilesDir()、共享的放getExternalFilesDir()、U盘走 StorageManager 枚举路径。记住了这三句话,工控文件管理就不怕迷路了。