DocumentFile

DocumentsProvider或磁盘上的原始文件支持的文档的表示


DocumentFile

继承关系和功能

继承关系

从图片中可以看出,它是一个抽象类,并且直接继承自Object。它是一个使用的程序类,用于模拟传统的File界面。它提供了一个文档树的简化视图,但是使用它有大量的开销。为获得最佳的性能和更丰富的功能集还是建议使用用DocumentsContract方法和常量。

和File的区别

  • Documents将它们的显示名称以及多媒体资源类型(MIME TYPE)表示为单独的字段,而不是依赖于文件扩展名。某些文档提供者可能会选择把扩展名附加在到显示名称,但这是实现上面的细节。
  • 在父子关系上,一个文档可能是多个目录的子目录。所以它并不知道它的父目录是谁,因此Documents没有很强的路径概念。我们在从父目录到子目录遍历是相对简单的,但是反过来就了不可以。
  • 每个文档在对应的提供程序中都会有一个唯一的标识符。但是它是提供者的一个不透明的细节,所以它无法被解析。

方法

我们在访问DocumentFile的实例类时,可以通过getUri()获取表示该对象的基础文档的Uri,然后可以通过openInputStream(uri)来获取该对象的内容相关的流。这个方法是在ContentResolver里的。
然后呢,这个类共有以下几种方法。

methods

然后呢,Android Studio里非常棒的一点就是可视化,我们可以通过方法的前面的标志来观察它的类型。比如第一个构造器,它前面是一个红色的圆圈带一个m。m我们都懂是“method”的意思,那红色代表着什么呢,通过定位我们可以确定,红色代表public。所以它的这个DocumentFile(DocumentFile)的构造方法是public的。

red

构造方法

然后我们发现这个方法就是指定了改DocumentFile的父目录的DocumentFile对象,细心的小伙伴发现了,也就是说对于每个DocumentFile对象是有parent的,那为什么官方文档特意告诉我们从子目录遍历父目录是不可以的呢?别急,我们看一下返回该parent的方法。
getParentFile

通过注释我们可以看到,它是只定义在用户选择的树,在一些特殊的情况可能没有。然后呢,因为DocumentsProvider提供了一个从父目录到子目录的正相匹配,所以这里为了便利就提供了一个反向的从子目录到父目录的匹配。但是当Documents Tree结构发生改变的时候,这个方法返回的可能就是错误的。所以这个方法还是要慎用呢。接下来我们继续观察其他方法。

static methods

这些方法前面的标识和之前的public标识有一点不同,那就是它们的左下角都有一个中空的菱形标志,这个代表着什么呢。通过定位,我们可以知道这个菱形标识其实代表着static属性,也就是静态方法。所以这四个方法都public static的。

fromFile && fromSingleUri

在这两个方法中,我们可以看到fromFile方法直接返回了一个RawDocumentFile对象,而fromSingleUri方法则是在SDK ≥ 19 即 KITKAT(Android 4.4)及以上的版本才会返回SingleDocumentFile对象,低于这个版本直接返回null。至于这两个对象代表什么,我们先不管。我们可以理解fromFile这个方法是没有版本限制的,但是fromSingleUri有,所以我们在4.4以下版本的设备可以使用fromFile方法来创建DocumentFile对象,4.4及以上版本可以使用fromSingleUri方法。

fromTreeUri && isDocumentUri

fromTreeUri这里不仅有版本限制SDK ≥ 21即LOLLIPOP(Android 5.0)及以上的版本才不会返回null。并且返回的是第三个新类型TreeDocumentFile,和前面的RawDocumentFile以及SingleDocumentFile都不一样呢,但是从DocumentFile的后缀这一点我们可以推断这三个类都是抽象类DocumentFile子类。至于它们的区别,我们后面分析一下。第四个方法isDocumentUri方法则是判断给定的Uri是否是DocumentsProvider支持的,这个方法也有版本限制,和fromSingleUri一样,sdk≥19才可以正确的使用,并且调用的是DocumentsContractisDocumentUri方法。所以说DocumentsProviderDocumentsContract这两个类也很重要。

public abstract methods

最后我们来看一下另一个特殊的标识,这五个方法左边的标识有两道灰色的边边,这个其实代表着abstract的属性,也就是说这些方法都是抽象方法,需要它的非抽象子类来实现这些方法。

createFile && createDirectory

由于是抽象方法,所以我们想看实现的细节只能到对应的子类中去看,但是注释已经很好地表示了它们的用法。分别是创建一个当前目录的子Document和子目录,这两个方法的返回对象都是DocumentFile,也都有可能返回空。

子类

子类

通过继承关系,我们可以看到,DocumentFile这个抽象类一共有三个子类,分别是上面提到的。

  1. RawDocumentFile
  2. SingleDocumentFile
  3. TreeDocumentFile

RawDocumentFile

我们来看一下这个类的结构图。

RawDocumentFile Structure

有一个很明显的区别就是RawDocumentFile没有一个方法是abstract的,这也就代表这个类不是一个抽象类,它实现了父类的抽象方法。并且它自己相对于DocumentFile多了一个File类型的私有字段mFile。至于它的用法我们可以在下面的方法中看到。

RawDocumentFile-1

可以看到构造器里是需要File对象的,并且createFile方法也会试图创建一个新的File对象,将它作为RawDocumentFile构造器的参数。值得注意的是createNewFile这个方法,我也是第一次见到这个方法。

createNewFile

它的作用是当且仅当这个路径下该名字的文件不存在时去原子性地创建一个新的并且空的文件。先是检测对应路径有无写权限,然后判断File是否不可用了,最后调用了FileSystemcreateFileExclusively创建一个文件。关于SecuritySystemFileSystem的代码,我这边都看不到。前者似乎是保密代码?后者是个抽象类,但是找不到子类。看来这些系统相关的代码想看到的话需要一些特殊手段呢。通常我以为new File()的时候就已经创建一个新文件了,但是我看了下File的构造器发现,它只是做了两件事一个是调用FileSystemresolve方法来对路径进行解析赋值给自己的path成员变量,第二件事情就是调用FileSystemprefixLength方法计算文件路径的前缀的长度。这是一个final transient int的成员变量。

prefixLength

看了下它的用法,是和getName,getParent,getParentFile,getAbsoluteFile,getCanonicalFile还有私有的readObject和一个static的代码块相关。感觉应该是为了便于计算父目录的一个计数变量吧。我们回到RawDocumentFile,通过这几个方法我们可以看出其实RawDocumentFile是对File的一层封装,它的所有方法其实都是调用了File对象的相关方法从而返回了对应的结果。所以说DocumentFilefromFile方法是适用于所有Android 版本的,但是高版本的时候最好还是使用fromSingleUri或者fromTreeUri这两个方法,毕竟既然想用DocumentFile嘛,否则的话直接用File就可以了。

SingleDocumentFile

我们看一下SingleDocumentFile。

SingleDocumentFile

感觉是自己找错了包,我这是androidx的包,这里看不到createFile和createDirectory的源码,但是可以推测它和RawDocumentFile的对应方法肯定是有区别的,至于这个类的其他方法则都是调用了DocumentsContractAip19的对应方法,至于这个类则是对DocumentsContract进行了一层封装。但是它有一个额外的要求就是SDK ≥ 19。

TreeDocumentFile

最后我们看一下TreeDocumentFile。

TreeDocumentFile

好耶,可以看到源码了。这里它的createFile方法其实是调用了DocumentsContractcreateDocument方法。然后它会返回一个Uri对象。我们看一下这个方法。

DocumentsContract - createDocument

首先初始化一个Bundle对象,这个对象也大有来头。它的注释说它是一个从字符串到变化的Parcelable值的映射,中文是"捆"的意思,就是把字符串和Parcelable捆绑在一起。我只是大概的知道它是和资源文件相关的,但是具体用法还不清楚,后面有空学习一下。回到这个方法,我们看到它在初始化Bundle后,然后调用了putParcelable方法将父目录的Uri作为"uri"字段插入Bundle的map中。putString方法也是将"mime_type"和"_display_name"这两个字段对应的字符串填充。最后调用了ContentResolver的call方法。我们来看一下这个方法。

ContentResolver - call

我们可以看到传进去的方法名是

DocumentsContract - METHOD_CREATE_DOCUMENT

我们可以看到call方法里会通过acquireProvider获取到IContentProvider,这是个接口,最终调用了它的call方法。我们可以看ContentProvider的相关方法。

ContentProvider - call - 1

好的,又是一层套娃,这个mInterface指向的是ContentProvider.this,所以它的call方法也是ContentProvidercall,如下。

ContentProvider - call - 2

ContentProvider有两个子类DocumentsProviderSliceProvider,我感觉应该是DocumentsProvider吧。我去看看它重写的call方法。

DocumentsProvider - call - 1

很明显我们传进来的方法名"android:createDocument"是符合条件的,所以调用的是callUnchecked方法。当我看到这个方法的代码的时候,我有一种踏破铁鞋无觅处,得来全不费功夫 的感觉。这个方法方法体很长,但是其实大部分都是if...else...。至于我们传进来的METHOD_CREATE_DOCUMENT其实对应着这部分的代码。

DocumentsProvider - call - 2

我们看不到enforceWritePermissionInnerbuildDocumentUriMaybeUsingTree这两个方法的实现细节,但是我们可以从它们的命名推断它们的作用。分别是强制打开写权限以及可能使用树结构来构建Document的Uri。然后这个方法返回的是一个Bundle对象,会在DocumentsContractcreateDocument里对其作相应的处理,然后返回Uri对象给TreeDocumentFile。然后TreeDocumentFile的其他方法使用也是DocumentsContractApi19的对应方法。

TreeDocumentFile - listFiles

接下来我们来看看listFiles方法,这个方法是返回当前目录下所有的子目录的DocumentFile

TreeDocumentFile - listFiles

这里也获取到了当前ContextContentResolver对象,关键寒暑是DocumentsContractbuildChildDocumentsUriUsingTree这个方法。

DocumentsContract - buildChildDocumentsUriUsingTree

这里涉及到一个权限让用户可以选择一个目录子树来做和Document相关的操作。然后我们可以看到这个Urischeme是“content”,这也就是它和"file"的scheme不同的地方了。所以DocumentFileUri无法去初始化生成一个File对象。作为参数treeUri是TreeDocumentFilemUri属性,parentDocumentId则是通过调用了DocumentsContractgetDocumentId方法生成的。

Intent - ACTION_OPEN_DOCUMENT_TREE

这个方法主要调用了UrigetPathSegments方法。这个方法我们可以理解为将Uri的路径根据'/'拆分成一个List。然后这里就判断传进来Uri符不符合DocumentUri的格式。符合就返回对应的DocumentId,否则直接报错。

DocumentContracts - getDocumentId

然后遍历子目录的时候会先初始化一个Cursor对象,汉语就是光标的意思。然后调用了ContentResolverquery方法,这个方法和那个数据库的方法有点像呢。我们看一下query的最终实现,虽然前面其实套了几层娃。

ContentResolver - query - 1

先研究一下注释。首先参数里第一个Uri要求是"content"作为scheme的。这点是符合要求的。第二个则是指定了哪些列会被返回,如果传null,则返回所有的列,比较低效。这里我们传进来的是只有一个"document_id"的数组,所以也只会返回对应的一列数据。第三个查询参数是一个包含额外信息的Bundle对象,可以包括SQL 格式的参数。SQL是一个数据库相关的东西,我数据库稀烂,这里就不说了嗷。第四个是CancellatinSignal类型的,用来取消查询这个过程的。如果这次操作真的取消了,那么则会抛出OperationCanceledException。这里呢,我们后面两个参数都是null,所以暂时用不上呢。

ContentResolver - query - 2

这里我们看剩下的代码,首先是通过acquireUnstableProvider获得一个IContentProvider对象。和上面的call方面有点类似,不过call方法里调用的是acquireProvider方法。这次加了一个Unstable(不稳定),可能是为了省时间吧。稳定性和效率总是相斥的嘛。然后很神奇的初始化了一个叫stableProviderIContentProvider对象和一个Cursor对象。在try - catch里面调用了unstableProviderquery方法,这次穿进去的参数又多了mPackageNamemAttributionTag这两个参数。我们去ContentProvider里面瞅瞅。

ContentProvider - query

继续套娃,调用了mInterfacequery方法。我们还是到DocumentsProvider里去看看实现细节。

DocumentsProvider - query

这里根据Uri匹配的结果进行了不同方法的调用。但是这些方法都是抽象的,要搞到DocumentsProvider的子类才能看到呢。我们回到ContentResolverquery那里。如果我们报错了,DeadObjectException很神奇的错误呢,远程进程被杀掉了?,但是我们还是持有一个不稳定的引用呢。这肯定不利于系统回收资源,所以我们需要赶快释放这个引用。然后才调用了acquireProvider方法获取一个稳定的IContentProvider。看来ContentProvider可以做到跨进程间通信呢。

ContentResolver - query - 3

最后调用了CursorgetCount方法,没想到这里都能抛出RuntimeException呢。最后对Cursor进行封装,封装到了CursorWrapperInner对象里。Cursor是个接口,CursorWrapper实现了这个接口,而CursorWrapperInner是它的子类。最后吐给了TreeDocumentFile。然后在它的listFiles方法里调用getString获取到String类型的documentId,再调用DocumentsContractbuildDocumentUriUsingTree方法构建出Uri对象加到list里。当查询完成后,再通过对Uri的list遍历生成了一个DocumentFile的数组。


DocumentsContract

接下来我们来看看DocumentsContract这个类,Contract是"合同","契约"的意思,而它的官方注释就是表明它是定义了一个documents provider和平台的合约。然后注释的代码里展示应该就是一些DocumentFile对应的Uri的样式。里面有几个字段"tree","document"。这些在getDocumentIdgetTreeDocumentId里都用用到呢。

DocumentsContract

我们可以理解这个类的作用就是规定了一个DocumentsProvider应该提供什么样格式的DocumentFile给平台,让用户来对其进行操作。观察它的结构可以看出,这个类有大量的public static的字段。

DocumentsContract - fields

其中有一些和我们刚才提到的call等方法都是相关的。并且它的方法也很多,这里就不详细介绍了。用得到的时候再用就ok。

DocumentsContract - methods

值得一提的它里面定义了三个内部类Document,Root,Path。前面两个类里定义了很多public static的成员。而Path是实现了Parcelable的一个static final的类。它的作用是持有一个从一个Document到一个在它之下的特定的Document的路径,它有两个成员变量String类型的mRootId以及List<String>mPath。这个应该是一个工具类。

DocumentsContract - inner class

DocumentsProvider

这个类是ContentProvider的子类。我们看看它的官方注释。它提供给持久文件的读写功能。比如存储在本地磁盘上或者云储存服务上的文件。当我们创建DocumentsProvider时需要在AndroidManifest.xml文件里进行声明。

DocumentsProvider - annotation - 1

我们可以看到里面声明了android.permission.MANAGE_DOCUMENTS权限以及要求至少版本是Kitkat。

DocumentsProvider - annotation - 2

这里主要介绍了Document这个类,它可以是一个数据流也可以是一个包含附加文档的文件夹。每个目录代表一个包含不少于0个documents的子树的根。每个Document可以有不同的能力,比如实现openDocumentThumbnail来实现返回缩略图的能力。DocumentCOLUMN_FLAGS会描述对应哪些能力。最后就是关键的一点。每个document都会有一个独一无二的引用就是DocumentCOLUMN_DOCUMENT_ID。这个值是一旦返回后就不可以改变的。
并且神奇的是单个document可能包含在不同目录下。

DocumentsProvider - annotation -3

最后就是Root这个类,它表示的就是一个document tree的顶端。是不是很熟悉,这两个类其实就定义在DocumentsContract中。

DocumentsProvider - methods

其实这个类的一些方法我们在上面DocumentFile的子类中就有介绍。我们可以理解这是一个专用于Document相关操作的ContentProvider的子类。至于ContentProvider呢,作为Android四大组件之一,Android新手肯定也是要迈过这道坎呢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容