由DocumentsProvider或磁盘上的原始文件支持的文档的表示
继承关系和功能
从图片中可以看出,它是一个抽象类,并且直接继承自Object。它是一个使用的程序类,用于模拟传统的File界面。它提供了一个文档树的简化视图,但是使用它有大量的开销。为获得最佳的性能和更丰富的功能集还是建议使用用DocumentsContract方法和常量。
和File的区别
- Documents将它们的显示名称以及多媒体资源类型(MIME TYPE)表示为单独的字段,而不是依赖于文件扩展名。某些文档提供者可能会选择把扩展名附加在到显示名称,但这是实现上面的细节。
- 在父子关系上,一个文档可能是多个目录的子目录。所以它并不知道它的父目录是谁,因此Documents没有很强的路径概念。我们在从父目录到子目录遍历是相对简单的,但是反过来就了不可以。
- 每个文档在对应的提供程序中都会有一个唯一的标识符。但是它是提供者的一个不透明的细节,所以它无法被解析。
方法
我们在访问DocumentFile的实例类时,可以通过getUri()
获取表示该对象的基础文档的Uri
,然后可以通过openInputStream(uri)
来获取该对象的内容相关的流。这个方法是在ContentResolver里的。
然后呢,这个类共有以下几种方法。
然后呢,Android Studio里非常棒的一点就是可视化,我们可以通过方法的前面的标志来观察它的类型。比如第一个构造器,它前面是一个红色的圆圈带一个m。m我们都懂是“method”的意思,那红色代表着什么呢,通过定位我们可以确定,红色代表public
。所以它的这个DocumentFile(DocumentFile)
的构造方法是public的。
然后我们发现这个方法就是指定了改DocumentFile的父目录的DocumentFile对象,细心的小伙伴发现了,也就是说对于每个DocumentFile对象是有
parent
的,那为什么官方文档特意告诉我们从子目录遍历父目录是不可以的呢?别急,我们看一下返回该parent
的方法。通过注释我们可以看到,它是只定义在用户选择的树,在一些特殊的情况可能没有。然后呢,因为DocumentsProvider提供了一个从父目录到子目录的正相匹配,所以这里为了便利就提供了一个反向的从子目录到父目录的匹配。但是当Documents Tree结构发生改变的时候,这个方法返回的可能就是错误的。所以这个方法还是要慎用呢。接下来我们继续观察其他方法。
这些方法前面的标识和之前的public标识有一点不同,那就是它们的左下角都有一个中空的菱形标志,这个代表着什么呢。通过定位,我们可以知道这个菱形标识其实代表着
static
属性,也就是静态方法。所以这四个方法都public static
的。
在这两个方法中,我们可以看到
fromFile
方法直接返回了一个RawDocumentFile
对象,而fromSingleUri
方法则是在SDK ≥ 19 即 KITKAT(Android 4.4)及以上的版本才会返回SingleDocumentFile
对象,低于这个版本直接返回null。至于这两个对象代表什么,我们先不管。我们可以理解fromFile这个方法是没有版本限制的,但是fromSingleUri有,所以我们在4.4以下版本的设备可以使用fromFile
方法来创建DocumentFile对象,4.4及以上版本可以使用fromSingleUri
方法。
fromTreeUri
这里不仅有版本限制SDK ≥ 21即LOLLIPOP(Android 5.0)及以上的版本才不会返回null。并且返回的是第三个新类型TreeDocumentFile
,和前面的RawDocumentFile
以及SingleDocumentFile
都不一样呢,但是从DocumentFile
的后缀这一点我们可以推断这三个类都是抽象类DocumentFile
的子类。至于它们的区别,我们后面分析一下。第四个方法isDocumentUri
方法则是判断给定的Uri是否是DocumentsProvider支持的,这个方法也有版本限制,和fromSingleUri
一样,sdk≥19才可以正确的使用,并且调用的是DocumentsContract
的isDocumentUri
方法。所以说DocumentsProvider和DocumentsContract这两个类也很重要。
最后我们来看一下另一个特殊的标识,这五个方法左边的标识有两道灰色的边边,这个其实代表着
abstract
的属性,也就是说这些方法都是抽象方法,需要它的非抽象子类来实现这些方法。
由于是抽象方法,所以我们想看实现的细节只能到对应的子类中去看,但是注释已经很好地表示了它们的用法。分别是创建一个当前目录的子Document和子目录,这两个方法的返回对象都是DocumentFile,也都有可能返回空。
子类
通过继承关系,我们可以看到,DocumentFile这个抽象类一共有三个子类,分别是上面提到的。
RawDocumentFile
SingleDocumentFile
TreeDocumentFile
RawDocumentFile
我们来看一下这个类的结构图。
有一个很明显的区别就是RawDocumentFile没有一个方法是abstract的,这也就代表这个类不是一个抽象类,它实现了父类的抽象方法。并且它自己相对于DocumentFile多了一个
File
类型的私有字段mFile。至于它的用法我们可以在下面的方法中看到。
可以看到构造器里是需要File对象的,并且createFile方法也会试图创建一个新的File对象,将它作为RawDocumentFile构造器的参数。值得注意的是createNewFile
这个方法,我也是第一次见到这个方法。
它的作用是当且仅当这个路径下该名字的文件不存在时去原子性地创建一个新的并且空的文件。先是检测对应路径有无写权限,然后判断File是否不可用了,最后调用了FileSystem
的createFileExclusively
创建一个文件。关于SecuritySystem
和FileSystem
的代码,我这边都看不到。前者似乎是保密代码?后者是个抽象类,但是找不到子类。看来这些系统相关的代码想看到的话需要一些特殊手段呢。通常我以为new File()的时候就已经创建一个新文件了,但是我看了下File的构造器发现,它只是做了两件事一个是调用FileSystem
的resolve
方法来对路径进行解析赋值给自己的path成员变量,第二件事情就是调用FileSystem
的prefixLength
方法计算文件路径的前缀的长度。这是一个final transient int
的成员变量。
看了下它的用法,是和
getName
,getParent
,getParentFile
,getAbsoluteFile
,getCanonicalFile
还有私有的readObject
和一个static的代码块相关。感觉应该是为了便于计算父目录的一个计数变量吧。我们回到RawDocumentFile
,通过这几个方法我们可以看出其实RawDocumentFile
是对File的一层封装,它的所有方法其实都是调用了File对象的相关方法从而返回了对应的结果。所以说DocumentFile
的fromFile
方法是适用于所有Android 版本的,但是高版本的时候最好还是使用fromSingleUri
或者fromTreeUri
这两个方法,毕竟既然想用DocumentFile
嘛,否则的话直接用File
就可以了。
SingleDocumentFile
我们看一下SingleDocumentFile。
感觉是自己找错了包,我这是androidx的包,这里看不到createFile和createDirectory的源码,但是可以推测它和
RawDocumentFile
的对应方法肯定是有区别的,至于这个类的其他方法则都是调用了DocumentsContractAip19
的对应方法,至于这个类则是对DocumentsContract
进行了一层封装。但是它有一个额外的要求就是SDK ≥ 19。
TreeDocumentFile
最后我们看一下TreeDocumentFile。
好耶,可以看到源码了。这里它的createFile方法其实是调用了DocumentsContract
的createDocument
方法。然后它会返回一个Uri
对象。我们看一下这个方法。
首先初始化一个
Bundle
对象,这个对象也大有来头。它的注释说它是一个从字符串到变化的Parcelable值的映射,中文是"捆"的意思,就是把字符串和Parcelable
捆绑在一起。我只是大概的知道它是和资源文件相关的,但是具体用法还不清楚,后面有空学习一下。回到这个方法,我们看到它在初始化Bundle
后,然后调用了putParcelable
方法将父目录的Uri
作为"uri"字段插入Bundle
的map中。putString
方法也是将"mime_type"和"_display_name"这两个字段对应的字符串填充。最后调用了ContentResolver
的call方法。我们来看一下这个方法。
我们可以看到传进去的方法名是
我们可以看到call
方法里会通过acquireProvider
获取到IContentProvider
,这是个接口,最终调用了它的call
方法。我们可以看ContentProvider
的相关方法。
好的,又是一层套娃,这个mInterface
指向的是ContentProvider.this
,所以它的call
方法也是ContentProvider
的call
,如下。
ContentProvider
有两个子类DocumentsProvider
和SliceProvider
,我感觉应该是DocumentsProvider
吧。我去看看它重写的call
方法。
很明显我们传进来的方法名"android:createDocument"是符合条件的,所以调用的是callUnchecked
方法。当我看到这个方法的代码的时候,我有一种踏破铁鞋无觅处,得来全不费功夫 的感觉。这个方法方法体很长,但是其实大部分都是if...else...。至于我们传进来的METHOD_CREATE_DOCUMENT
其实对应着这部分的代码。
我们看不到enforceWritePermissionInner
和buildDocumentUriMaybeUsingTree
这两个方法的实现细节,但是我们可以从它们的命名推断它们的作用。分别是强制打开写权限以及可能使用树结构来构建Document的Uri。然后这个方法返回的是一个Bundle
对象,会在DocumentsContract
的createDocument
里对其作相应的处理,然后返回Uri
对象给TreeDocumentFile
。然后TreeDocumentFile
的其他方法使用也是DocumentsContractApi19
的对应方法。
TreeDocumentFile - listFiles
接下来我们来看看listFiles
方法,这个方法是返回当前目录下所有的子目录的DocumentFile
。
这里也获取到了当前Context
的ContentResolver
对象,关键寒暑是DocumentsContract
的buildChildDocumentsUriUsingTree
这个方法。
这里涉及到一个权限让用户可以选择一个目录子树来做和Document相关的操作。然后我们可以看到这个Uri
的scheme
是“content”,这也就是它和"file"的scheme
不同的地方了。所以DocumentFile
的Uri
无法去初始化生成一个File
对象。作为参数treeUri是TreeDocumentFile
的mUri
属性,parentDocumentId则是通过调用了DocumentsContract
的getDocumentId
方法生成的。
这个方法主要调用了Uri
的getPathSegments
方法。这个方法我们可以理解为将Uri
的路径根据'/'拆分成一个List
。然后这里就判断传进来Uri
符不符合Document
的Uri
的格式。符合就返回对应的DocumentId,否则直接报错。
然后遍历子目录的时候会先初始化一个Cursor
对象,汉语就是光标的意思。然后调用了ContentResolver
的query
方法,这个方法和那个数据库的方法有点像呢。我们看一下query
的最终实现,虽然前面其实套了几层娃。
先研究一下注释。首先参数里第一个Uri
要求是"content"作为scheme的。这点是符合要求的。第二个则是指定了哪些列会被返回,如果传null,则返回所有的列,比较低效。这里我们传进来的是只有一个"document_id"的数组,所以也只会返回对应的一列数据。第三个查询参数是一个包含额外信息的Bundle
对象,可以包括SQL 格式的参数。SQL是一个数据库相关的东西,我数据库稀烂,这里就不说了嗷。第四个是CancellatinSignal
类型的,用来取消查询这个过程的。如果这次操作真的取消了,那么则会抛出OperationCanceledException
。这里呢,我们后面两个参数都是null,所以暂时用不上呢。
这里我们看剩下的代码,首先是通过acquireUnstableProvider
获得一个IContentProvider
对象。和上面的call
方面有点类似,不过call
方法里调用的是acquireProvider
方法。这次加了一个Unstable
(不稳定),可能是为了省时间吧。稳定性和效率总是相斥的嘛。然后很神奇的初始化了一个叫stableProvider
的IContentProvider
对象和一个Cursor
对象。在try - catch里面调用了unstableProvider
的query
方法,这次穿进去的参数又多了mPackageName
和mAttributionTag
这两个参数。我们去ContentProvider
里面瞅瞅。
继续套娃,调用了mInterface
的query
方法。我们还是到DocumentsProvider
里去看看实现细节。
这里根据Uri
匹配的结果进行了不同方法的调用。但是这些方法都是抽象的,要搞到DocumentsProvider
的子类才能看到呢。我们回到ContentResolver
的query
那里。如果我们报错了,DeadObjectException
很神奇的错误呢,远程进程被杀掉了?,但是我们还是持有一个不稳定的引用呢。这肯定不利于系统回收资源,所以我们需要赶快释放这个引用。然后才调用了acquireProvider
方法获取一个稳定的IContentProvider
。看来ContentProvider
可以做到跨进程间通信呢。
最后调用了Cursor
的getCount
方法,没想到这里都能抛出RuntimeException
呢。最后对Cursor
进行封装,封装到了CursorWrapperInner
对象里。Cursor
是个接口,CursorWrapper
实现了这个接口,而CursorWrapperInner
是它的子类。最后吐给了TreeDocumentFile
。然后在它的listFiles
方法里调用getString
获取到String
类型的documentId,再调用DocumentsContract
的buildDocumentUriUsingTree
方法构建出Uri
对象加到list里。当查询完成后,再通过对Uri
的list遍历生成了一个DocumentFile
的数组。
DocumentsContract
接下来我们来看看DocumentsContract这个类,Contract是"合同","契约"的意思,而它的官方注释就是表明它是定义了一个documents provider和平台的合约。然后注释的代码里展示应该就是一些DocumentFile
对应的Uri
的样式。里面有几个字段"tree","document"。这些在getDocumentId
和getTreeDocumentId
里都用用到呢。
我们可以理解这个类的作用就是规定了一个DocumentsProvider
应该提供什么样格式的DocumentFile
给平台,让用户来对其进行操作。观察它的结构可以看出,这个类有大量的public static
的字段。
其中有一些和我们刚才提到的call
等方法都是相关的。并且它的方法也很多,这里就不详细介绍了。用得到的时候再用就ok。
值得一提的它里面定义了三个内部类Document
,Root
,Path
。前面两个类里定义了很多public static
的成员。而Path
是实现了Parcelable
的一个static final
的类。它的作用是持有一个从一个Document到一个在它之下的特定的Document的路径,它有两个成员变量String
类型的mRootId
以及List<String>
的mPath
。这个应该是一个工具类。
DocumentsProvider
这个类是ContentProvider
的子类。我们看看它的官方注释。它提供给持久文件的读写功能。比如存储在本地磁盘上或者云储存服务上的文件。当我们创建DocumentsProvider时需要在AndroidManifest.xml
文件里进行声明。
我们可以看到里面声明了android.permission.MANAGE_DOCUMENTS
权限以及要求至少版本是Kitkat。
这里主要介绍了Document
这个类,它可以是一个数据流也可以是一个包含附加文档的文件夹。每个目录代表一个包含不少于0个documents的子树的根。每个Document可以有不同的能力,比如实现openDocumentThumbnail
来实现返回缩略图的能力。Document
的COLUMN_FLAGS
会描述对应哪些能力。最后就是关键的一点。每个document都会有一个独一无二的引用就是Document
的COLUMN_DOCUMENT_ID
。这个值是一旦返回后就不可以改变的。
并且神奇的是单个document可能包含在不同目录下。
最后就是
Root
这个类,它表示的就是一个document tree的顶端。是不是很熟悉,这两个类其实就定义在DocumentsContract
中。
其实这个类的一些方法我们在上面DocumentFile
的子类中就有介绍。我们可以理解这是一个专用于Document
相关操作的ContentProvider
的子类。至于ContentProvider
呢,作为Android四大组件之一,Android新手肯定也是要迈过这道坎呢。