安卓APP的存储目录+ FileProvider,总结持久化数据的技巧

        安卓存储目录分为 内部存储外部存储。 内部存储的目录为  /data/ 目录,  其中 内部存储 在未root的手机上是无法查看的。
        要了解APP的存储目录结构,我们先从 app开始安装时谈起。

一、apk在安装时,涉及到目录

        在系统开始安装一个apk时,系统会先将apk文件复制到  data/app/ 目录下,再解析apk信息,然后dexopt优化操作。
内部存储在未root的手机上虽然无法查看的,但在Android Studio 3.1.4 中可以借助 Device File Explorer 工具来查看 内部存储空间。 这个工具在 AS 的右下角, 如果不小心移除了,可以来下面 右边 这张图里找到。
         打开这个工具,我们可以看到 data/app/ 目录下有许多包名,这些都是 已经安装到手机的 app的 包名。如下图所示:

        以MIUI 8.2为例, 这些不同包名的内部存储了两大块内容,一个so文件;另一个是oat文件。
       so和oat 这两部分文件是系统能运行此app的基础,app的机器代码都保存在这里。  为安全着想,没有root的手机即使借助 Device File Explorer 也不能查看 oat里存储的 odex文件。 

1.1、oat历史介绍

        ART和Dalvik都算是一种Android运行时环境,或者叫做虚拟机,用来解释dex类型文件。但是ART是安装时解释,Dalvik是运行时解释。  4.4 以前的版本使用的是 dalvik 虚拟机,在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

       dalvik: 在5.0 以前 的 Dalvik时代,app每次启动时,系统都需要通过 即时编译器jit(Just-In-Time实时编译) 将dex文件或odex翻译成能被虚拟机加载的native code , 最终产物是相同名称的 dey文件(表示这是一个优化过的dex),这样使得 dalvik 虚拟机的 app启动速度很慢。 

       ART: 5.0 及之后的 ART虚拟机中,完全抛弃了dalvik的JIT, 使用了AOT直接在安装时将其完全翻译成native code.这一技术的引入,使得虚拟机执行指令的速度又一重大提升。
       oat: 是 AOT 在安装apk时 生成的 native code,对应的文件后缀为 *.oat(实际上是一个自定义的elf文件,里面包含的都是本地机器指令)), o是optimize(优化)的缩写,a是android的缩写,t是runTime的意思,oat 是在apk安装时通过dexopt工具将dex文件优化成二进制格式的文件,然后再通过AOT(Ahead-Of-Time 预先编译)生成 能被art虚拟机执行的机器 吗,从而加快app的启动速度。
      在4.4版本上,两种运行时环境共存,可以相互切换,但是在5.0+,Dalvik虚拟机则被彻底的丢弃,全部采用ART。

     

二、apk在运行时,涉及到目录

        上面我们介绍了 安装apk 后的相关目录,在我们运行一个app后,代码里所涉及到的文件 如 数据库、sp、webview缓存等都保存到了  data/data/包名/ 目录下。
        "data/app/包名" 和 "data/data/包名"  都是应用私有目录,只允许应用内部访问, 其它应用的进程是无法访问的。   
       注意:当用户卸载 App 时,系统自动删除 data/data 目录下对应包名的文件夹及其内容。

具体目录 如下图: 

三、sdk所提供的方法与内部存储的对应关系

Android SDK 提供有如下方法可以获取并操作 内部存储 空间下应用 私有目录文件 的方法,都位于 Application Context 中,供开发者直接调用:getFilesDir()getCacheDir()deleteFile()、fileList()、Environment.getDataDirectory();

四、外部存储目录

        外部存储目录的路径为 "Android/data/包名" ,主要是考虑到内部存储空间容量有限,普通用户不能直接直观地查看目录文件等其他原因,Android 在外部存储空间中也提供有特殊目录供应用存放私有文件。
        一般设备都有内置 SD 卡,同时也提供外部 SD 卡拓展,可能对应路径的目录名有所差异。

        值得注意的是,与内部存储空间的应用私有目录不同的是:

第一,默认情况下,系统并不会自动创建外部存储空间的应用私有目录。只有在应用需要的时候,开发人员通过 SDK 提供的 API 创建该目录文件夹和操作文件夹内容。

第二,自 Android 7.0 开始,系统对外部存储目录中 应用私有目录的访问权限进一步限制。其他 App 无法通过 file:// 这种形式的 Uri 直接读写 非自己app外部私有目录下的文件内容,而是需要通过 FileProvider 访问。(关于这个内容,接下来再写一篇文章专门说说 7.0 的适配问题,欢迎关注我的微信公众号:安卓笔记侠。)

第三,宿主 App 可以直接读写内部存储空间中的应用私有目录;而在 4.4 版本开始,宿主 App 才可以直接读写外部存储空间中的应用私有目录,使开发人员无需在 Manifest 文件中或者动态申请外部存储空间的文件读写权限

而相同点在于:同属于应用私有目录,当用户卸载 App 时,系统也会自动删除外部存储空间下的对应 App 私有目录文件夹及其内容。

同样,Android SDK 中也提供有便捷的 API 供开发人员直接操作外部存储空间下的应用私有目录:getExternalFilesDir()、getExternalCacheDir()、Environment.getExternalStorageDirectory();  
区别是,在4.4之后通过 Environment 访问外部 存储空间时需要读写存储卡权限。
注意:对于外部存储空间下的应用私有目录文件,由于普通用户可以自由修改和删除,开发人员在使用时,一定要做好判空处理和异常捕获,防止应用崩溃退出!

五、外部存储中的公有空间

注意:访问外部存储空间 的 非应用私有目录 时记得申请读写权限!

外部存储空间已经为用户默认分类出一些公共目录。开发人员可以通过 Environment 类提供的方法直接获取相应目录的绝对路径,Environment.getExternalStoragePublicDirectory(String type);
传递不同的 type 参数类型即可:Envinonment 类提供诸多 type 参数的常量,比如:
DIRECTORY_MUSIC:Music
DIRECTORY_MOVIES:Movies
DIRECTORY_PICTURES:Pictures
DIRECTORY_DOWNLOADS:Download
以第一个常量为例,音乐类别的公共目录绝对路径为:/storage/emulated/0/Music。如果你使用文件管理器打开设备的外部存储空间的话,均可以看到这些公共目录文件夹。

六、外部存储中的 其它目录

       一般来说,利用两种应用私有目录和公共目录便能够存储应用中需要保存的数据和文件。如果这些还不够的话,那一定是你的开发姿势不对。在 Code Review 的前提下,如果还是不够的话,还可以在外部存储空间自由创建其他目录,通过这个方式获取外部存储空间的绝对路径,然后操作文件:Environment.getExternalStorageDirectory();

七、需要注意的地方

        使用应用私有目录保存应用相关数据,使用公共目录保存应用无关数据(共享数据)。无论哪种情况,都需要做好数据分类保存,便于清除等统一管理。随便打开手机上的几个应用,不难发现,很多应用都包含一个清理缓存的功能。事实上,开发人员清理的就是应用相关数据,也就是应用私有目录下的文件。

        考虑到外部存储空间上的内容可能被用户手动删除,或者卸载拓展 SD 卡等不可控因素,操作前记得使用 Environment 类提供的 API 方法判断容量是否充足、文件是否存在等情况,做好异常捕获,减少应用崩溃率。相信这一定是一个良好的习惯。

八、补充7.0 FileProvider的适配

        在Android 7.0以前, 可以使用file://uri的方式访问外部存储中的 其它应用 的私有目录的文件,但是这有个问题,就是即使不是你自身应用产生的文件,只要知道对方的uri则就可以调用到,这样在安全性上就产生了风险。
     所以Android 7.0后新增了对文件跨进程访问的限制,这个限制会造成,如果使用file://uri的方式访问,则会出现android.os.FileUriExposedException的异常。
        FileProvider 的注册 有两大步骤
1、 在 manifest.xml 中注册

<application>
    ...
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.yourname"
        android:exported="false"
        android:grantUriPermissions="true">
        ...
    </provider>
    ...
</application>

2、在 res/xml 目录下 添加 共享目录标识 文件

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    ...
</paths>

  其中 paths 的 标签  可以配置多组,每一组也有多种选择,具体规则如下:
<files-path>:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;<cache-path>:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;<external-path>:外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径;
<external-files-path>:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
<external-cache-path>:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir();

可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 name 和 path 两个属性。
其中,path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。
注意:path 属性值不能使用具体的独立文件名,只能是目录名。

而 name 属性用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。

如果我们需要分享的文件位于同级别目录下不同的子目录中,就需要添加多个子元素逐一指定要分享的文件目录,或者共享他们通用的父目录也行。

添加完共享目录后,再在 <provider> 元素中使用 <meta-data> 元素将 res/xml 中的 path 文件与注册的 FileProvider 链接起来:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.yourname"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/yourfilename" />
</provider>

        经过这两大步,我们已经完成了 FileProvider 注册。  这样其它 app 就能 使用 我们app FileProvider 所代表的目录下的文件。 具体使用的步骤大概如下 :
1、FileProvider.getUriForFile 构造  contentUri
2、申请 uri 访问权限 
3、startActivity  来启动此 contentUri;

        举几个例子,7.0 及以后的  跨进程访问文件都需要通过 FileProvider来实现, 

a、7.0的 apk 更新功能: apk安装进程需要访问你的app外部存储中私有目录下的apk文件

File apkFile = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "app_sample.apk");
Uri apkUri = FileProvider.getUriForFile(this,
        BuildConfig.APPLICATION_ID+".myprovider", apkFile);

Intent installIntent = new Intent(Intent.ACTION_VIEW);
installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
startActivity(installIntent);

b、调用系统拍照,并保存到外部存储目录中: 相机进程 需要将图片数据写入到 你的app外部存储中私有目录下 的 文件中

String filePath = Environment.getExternalStorageDirectory() + "/images/"+System.currentTimeMillis()+".jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
    outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
        BuildConfig.APPLICATION_ID + ".myprovider", outputFile);

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, contentUri);
startActivityForResult(intent, REQUEST_TAKE_PICTURE);


     

<think>我们将在Android应用中实现选择指定目录下的文件。这通常涉及到使用系统的文件选择器(如Intent.ACTION_GET_CONTENT或Intent.ACTION_OPEN_DOCUMENT),设置初始目录(URI)。但是,需要注意的是,标准的文件选择Intent不直接支持设置初始目录,但我们可以通过以下方式之一来实现: 1. 使用Storage Access Framework (SAF) 的Intent.ACTION_OPEN_DOCUMENT,设置EXTRA_INITIAL_URI(API 26+)来指定初始目录。 2. 如果目标目录在外部存储的特定位置(如Downloads、Documents等),我们可以通过将初始URI设置为该目录的URI来实现。 注意:EXTRA_INITIAL_URI在Android 8.0(API 26)及以上版本可用。 步骤: 1. 确定目标目录的URI。例如,如果我们想打开Downloads目录,可以使用: ```java Uri initialUri = Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getAbsolutePath()); ``` 但是,这种方法返回的是文件路径,而我们需要的是content:// URI。更推荐的方法是使用DocumentsContract.buildDocumentUriUsingTree或者使用MediaStore获取公共目录的URI。 2. 实际上,对于公共目录,我们可以使用MediaStore获取其URI,但更简单的方式是使用Android提供的标准目录URI。例如,Downloads目录的URI可以通过: ```java Uri initialUri = Uri.parse(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).toURI().toString()); ``` 但是,这仍然是file:// URI,在Android 7.0(API 24)及以上,使用file:// URI会受到限制。因此,我们应该使用Content URI。 3. 获取公共目录的Content URI:我们可以使用MediaStore.Downloads.EXTERNAL_CONTENT_URI(仅适用于Android 10+,API 29+)来访问下载目录。对于其他公共目录,也有对应的MediaStore URI。 4. 但是,如果我们想要指定一个非公共目录(例如应用私有目录下的某个文件夹),则无法直接通过Intent设置初始目录,因为其他应用无法访问。因此,我们只能指定公共目录。 5. 创建Intent设置初始目录(仅适用于API 26+): ```java Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 所有文件类型 // 可选:设置选择多个文件 intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 设置初始目录(API 26+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); } startActivityForResult(intent, REQUEST_CODE); ``` 6. 对于低于Android 8.0的版本,我们无法设置初始目录,文件选择器会打开默认目录。 7. 在onActivityResult中处理返回的文件URI。 注意:由于EXTRA_INITIAL_URI在API 26才引入,所以对于低版本,我们无法指定初始目录。 另一种方法:如果我们的应用有MANAGE_EXTERNAL_STORAGE权限(在Android 11+需要特殊申请,且通常不被推荐,因为很难上架),我们可以使用文件路径直接访问,但这里不推荐。 因此,我们主要针对API 26及以上设置初始目录,以下则无法指定。 示例代码: 假设我们想要打开Downloads目录(公共下载目录): ```java private void openFilePicker() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 设置初始目录为Downloads目录(仅对API 26+有效) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 获取Downloads目录的URI,使用MediaStore方式(适用于API 29+)或者使用Environment方式转换 // 注意:在API 29及以上,我们可以使用MediaStore.Downloads.EXTERNAL_CONTENT_URI Uri initialUri; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { initialUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI; } else { // 在API 26到28,我们尝试将文件路径转换为Content URI // 但是,这种方式不可靠,因为不同的设备文件管理器可能不同 // 我们可以使用Environment.getExternalStoragePublicDirectory获取Downloads路径,然后转换为URI File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // 将File转换为content URI需要借助FileProvider?但是FileProvider通常用于私有目录 // 公共目录的URI可以通过MediaStore扫描得到,但这里我们直接使用FileProvider可能不行,因为FileProvider只能用于自己的私有目录和指定的共享目录。 // 因此,对于API 26-28,我们使用一个折中方案:使用Storage Access Framework的树结构URI?但这样比较复杂。 // 实际上,我们可以尝试使用DocumentsContract.buildRootUri和DocumentsContract.buildDocumentUriUsingTree,但需要知道Downloads目录的document id。 // 由于这比较复杂,且低版本设备较少,我们可以只对Android Q及以上设置Downloads目录,其他则使用默认目录。 initialUri = Uri.parse(downloadsDir.getAbsolutePath()); // 这是file:// URI,可能无效 // 或者我们不设置初始URI,让系统打开默认目录 // 所以,这里我们选择不设置,因为无法获取到Downloads的content URI // 因此,我们只对API 29及以上设置 return; // 不设置初始URI } intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); } startActivityForResult(intent, PICK_FILE_REQUEST); } ``` 因此,一个更实际的做法是:如果我们只针对Android 10(API 29)及以上,我们可以直接使用MediaStore.Downloads.EXTERNAL_CONTENT_URI。对于低版本,我们无法可靠地设置Downloads目录,所以不设置初始URI。 另外,我们也可以让用户自己导航到Downloads目录,因为通常文件选择器会记住上次的位置。 总结:在Android中,选择指定目录下的文件(特别是公共目录)可以通过设置Intent的EXTRA_INITIAL_URI来实现,但仅支持API 26+,且对于公共目录的URI获取,在Android 10+才提供方便的MediaStore URI。对于低版本,我们无法可靠地设置初始目录。 因此,我们建议: - 对于Android 10及以上,使用MediaStore提供的URI来设置初始目录到Downloads等公共目录。 - 对于Android 8.0(API 26)到Android 9(API 28),如果我们想设置初始目录,需要获取公共目录的Content URI,这可能需要通过扫描MediaStore或者使用DocumentsContract,但比较复杂,且不一定所有设备都支持。 - 对于Android 8.0以下,忽略初始目录设置。 考虑到兼容性,我们可以只对Android 10及以上设置初始目录,其他情况不设置。 代码示例(完整): ```java private static final int PICK_FILE_REQUEST = 123; private void openFilePicker() { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 允许选择多个文件 // intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); // 设置初始目录:仅当Android 10(API 29)及以上 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // 这里我们指定Downloads目录 Uri initialUri = MediaStore.Downloads.EXTERNAL_CONTENT_URI; intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // 对于Android 8.0(API 26)到Android 9(API 28),我们可以尝试获取Downloads目录,但无法直接获取Content URI // 这里我们可以尝试使用Environment获取路径,然后转换为Content URI?但这样不可靠 // 因此,我们选择不设置,或者使用以下方法(不推荐): // 注意:以下方法在某些设备上可能有效,但不是标准做法 File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); // 尝试使用FileProvider? 但FileProvider不适用于公共目录,而且其他应用可能没有权限访问这个URI // 所以,我们放弃设置初始目录 } startActivityForResult(intent, PICK_FILE_REQUEST); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PICK_FILE_REQUEST && resultCode == RESULT_OK) { if (data != null) { Uri uri = data.getData(); // 处理选择的文件 // 注意:需要持久化权限,因为返回的URI可能只是临时授权 takePersistableUriPermission(uri, data); // 使用uri读取文件 } } } // 获取持久化权限 private void takePersistableUriPermission(Uri uri, Intent data) { int takeFlags = data.getFlags(); takeFlags &= (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); getContentResolver().takePersistableUriPermission(uri, takeFlags); } ``` 注意:在Android 11(API 30)及以上,如果应用的目标SDK为30,则需要在AndroidManifest.xml中添加<queries>元素来声明与文件选择器的交互: ```xml <queries> <!-- 指定文件选择器Intent --> <intent> <action android:name="android.intent.action.OPEN_DOCUMENT" /> <category android:name="android.intent.category.OPENABLE" /> </intent> </queries> ``` 这样,应用才能启动其他应用的文件选择器。 另外,如果用户没有安装文件管理器应用,则可能没有应用响应这个Intent,因此需要处理ActivityNotFoundException。 因此,完整的代码应该包括异常处理: ```java try { startActivityForResult(intent, PICK_FILE_REQUEST); } catch (ActivityNotFoundException e) { // 没有文件管理器应用 Toast.makeText(this, "No file manager installed", Toast.LENGTH_SHORT).show(); } ``` 综上所述,我们可以实现一个文件选择器,尽可能地将初始目录设置为Downloads目录(仅支持API 29+),其他情况则无法保证。 如果应用需要访问特定目录(例如应用私有目录下的文件),则不能使用这种方式,因为其他应用无法访问。在这种情况下,应该使用应用内的文件选择器(例如第三方库)或者将文件复制到公共目录再访问。 相关问题: 1. 如何在Android中读取文件内容? 2. 如何获取文件选择器返回的文件路径? 3. 如何实现自己的文件浏览器? 注意:从返回的URI中读取文件内容需要使用ContentResolver,且不能直接转换为文件路径。</think>在 Android 中实现选择指定目录下的文件,可以通过以下两种主要方法实现。以下是最佳实践和完整实现步骤: ### 方法一:使用 Storage Access Framework (SAF) - 推荐 这是官方推荐的方式,支持 Android 5.0+ (API 21+),通过系统文件选择器实现: ```java // 启动文件选择器 private static final int PICK_FILE_REQUEST = 101; public void openFilePicker(Uri initialUri) { Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("*/*"); // 所有文件类型 // 设置初始目录 (API 26+) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && initialUri != null) { intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, initialUri); } startActivityForResult(intent, PICK_FILE_REQUEST); } // 处理选择结果 @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == PICK_FILE_REQUEST && resultCode == RESULT_OK) { if (data != null) { Uri uri = data.getData(); // 获取持久访问权限 getContentResolver().takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ); // 使用uri读取文件内容 processSelectedFile(uri); } } } // 获取指定目录的URI public Uri getDirectoryUri(String directoryName) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Android 10+ 使用MediaStore获取公共目录URI return MediaStore.Downloads.EXTERNAL_CONTENT_URI; // 下载目录 } else { // Android 5.0-9.0 使用FileProvider File dir = new File(Environment.getExternalStoragePublicDirectory( Environment.DIRECTORY_DOWNLOADS), directoryName); return FileProvider.getUriForFile( this, "com.your.app.fileprovider", dir ); } } // 示例用法:打开Download目录下的"reports"子目录 public void openReportsDirectory() { Uri reportsDirUri = getDirectoryUri("reports"); openFilePicker(reportsDirUri); } ``` ### 方法二:使用自定义文件选择器(适合复杂需求) 当需要完全控制UI或支持旧设备时: ```java public class CustomFilePicker extends Fragment { private File currentDir; public void openDirectory(File directory) { this.currentDir = directory; refreshFileList(); } private void refreshFileList() { List<FileItem> fileList = new ArrayList<>(); File[] files = currentDir.listFiles(); if (files != null) { for (File file : files) { fileList.add(new FileItem( file.getName(), file.isDirectory() ? R.drawable.ic_folder : R.drawable.ic_file, file )); } } // 更新RecyclerView适配器 adapter.setData(fileList); } // RecyclerView点击事件 private void onFileSelected(File file) { if (file.isDirectory()) { openDirectory(file); } else { // 处理文件选择 Uri uri = FileProvider.getUriForFile( requireContext(), "com.your.app.fileprovider", file ); processSelectedFile(uri); } } // 文件项数据类 private static class FileItem { String name; int iconRes; File file; FileItem(String name, int iconRes, File file) { this.name = name; this.iconRes = iconRes; this.file = file; } } } ``` ### 关键配置步骤 1. **AndroidManifest.xml 配置**: ```xml <application> <!-- 添加FileProvider --> <provider android:name="androidx.core.content.FileProvider" android:authorities="com.your.app.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider> <!-- Android 11+ 需要添加查询权限 --> <queries> <intent> <action android:name="android.intent.action.OPEN_DOCUMENT" /> </intent> </queries> </application> ``` 2. **file_paths.xml 配置** (res/xml/file_paths.xml): ```xml <?xml version="1.0" encoding="utf-8"?> <paths> <!-- 访问下载目录 --> <external-path name="downloads" path="Download/" /> <!-- 自定义目录 --> <external-path name="custom_dir" path="MyAppDocuments/" /> </paths> ``` ### 最佳实践建议 1. **权限处理**: ```java // 检查存储权限 private boolean checkStoragePermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { return Environment.isExternalStorageManager(); } else { return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) == PERMISSION_GRANTED; } } ``` 2. **URI 路径处理**: ```java // 获取文件真实路径 (谨慎使用) public String getRealPathFromUri(Uri uri) { String path = null; String[] projection = {MediaStore.Files.FileColumns.DATA}; Cursor cursor = getContentResolver().query(uri, projection, null, null, null); if (cursor != null && cursor.moveToFirst()) { int columnIndex = cursor.getColumnIndexOrThrow(projection[0]); path = cursor.getString(columnIndex); cursor.close(); } return path; } ``` 3. **Android 11+ 适配**: ```xml <!-- AndroidManifest.xml 中添加 --> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> ``` ### 注意事项 1. 优先使用 SAF 方法,它更安全且支持最新存储策略 2. 避免直接使用文件路径,应使用 Content URI 3. 使用 `takePersistableUriPermission()` 保持长期访问权限 4. 对于 Android 10+,使用 MediaStore 访问公共目录 5. 自定义文件选择器需要处理权限和路径解析 ### 文件选择示例流程 ```mermaid graph TD A[用户点击选择文件] --> B{API >= 26?} B -->|是| C[使用SAF设置初始目录] B -->|否| D[使用自定义选择器] C --> E[系统文件选择器] D --> F[展示目录内容] E --> G[用户选择文件] F --> H[用户选择文件/目录] G --> I[获取Content URI] H --> I I --> J[处理文件内容] ``` ### 常见问题解决 - **权限问题**:确保在 Android 11+ 请求 `MANAGE_EXTERNAL_STORAGE` 权限 - **URI 过期**:使用 `takePersistableUriPermission()` 保持权限 - **目录不存在**:先检查 `Environment.getExternalStoragePublicDirectory().exists()` - **空目录**:处理 `listFiles()` 返回 null 的情况 通过以上方法,您可以实现在 Android 应用中选择指定目录下的文件,同时兼容不同 Android 版本和设备。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值