轻读一下 Android 应用开发中的 assets 目录

2019-08-07

关键字:APK预置文件、预置配置文件、res,raw与assets的区别


 

在Android的应用开发中,难免会遇到外部文件的预置需求。例如图像、音视频、配置文件、字体等等。对于图像,我们很容易会想到将它们存放在 res/drawable 目录或者是 res/mipmap 目录下。但对于其它类型的文件,就得另寻它法了。

 

比较常见的可以保存任意类型文件的地方主要有两个:

1、res/raw 目录;

2、assets 目录。

 

drawable/mipmap、raw、assets 三者都可以用来存储一些外部资源文件,那它们之间各自有什么优缺点呢?

 

首先,在 drawable 或 mipmap 下只能保存图像文件或图像描述文件,但在这两个目录下保存的图像文件在编译时会建立一张“索引表”。这个索引信息会被统一保存在一个名称为 R.java 的文件中。在程序的任何地方都可以直接通过 R.drawable.xxx 的形式来使用图片资源。

 

res/raw 目录算是一个比较特殊的资源文件目录。它被设计用于保存一些二进制文件,即在这个目录下的所有文件都会被记录到“索引表”中,但是编译系统不会去动里面的文件。raw 目录下的文件放进去时是什么样的,编译成 APK 以后还是什么样。这个目录比较适合保存一些音视频等二进制文件。

 

res 目录下的资源文件夹,不管是 raw 还是 drawable 或 mipmap,都不能自由地设计子目录层级关系。不管你有多少文件,都只能放在同一级目录中。

 

assets 目录是一个非常自由的目录。它就像是Android应用中的“三不管”地带,不会为里面的文件建立索引、不会限制目录层级关系、不会处理里面的文件。如果你想更好地管理自己的外部资源文件,建议使用 assets 目录。

 

本篇文章,我们重点来讲解一下 Android 的 assets 目录。

 

1、assets 简介

 

assets 在 APK 工程中就是一个普通的目录而已。不管是 Eclipse 创建的工程还是 Android Studio 创建的工程,在它的工程根目录下都可以发现(或者自己创建)一个 assets 目录,如下图所示

assets 目录是专门用于保存各种外部文件的。常见的有:图像、音视频、配置文件、字体、自带数据库等。之所以说它适合用来管理这些文件,是因为应用程序在编译时不会去处理这个目录下的文件,但是却会将它们打包进 APK 中。而其它你随便创建的目录在编译时就会被直接忽略掉。同时,你可以在 assets 目录内任意创建目录层级关系,这对于有大量外部文件需要集成的应用来说,就能很方便地分类管理了。

 

在 APK 开发中,有一种管理配置信息的做法比较常见:直接将配置信息文件放入 assets 目录中管理,程序首次运行时将这里面的配置信息拷贝到外部的可操作的目录下,后续程序的运行均靠这份保存在外部的配置信息为准,assets 中的信息仅作为原始配置信息的备份。

 

但是,assets 目录在使用上也还是有一点小缺憾的。

 

assets 目录内的文件在程序打包发布以后就是只读的。就是你只能读取那里面的文件,而无法修改或增加文件。这条特性其实也可以理解,因为应用程序一旦打包发布了,它就应该是只读的。而 assets 目录又是直接保存在 APK 内部的,所以它自然也不能修改或增加内容了。实在要想增加内容,通过 Database 或者 SharedPreferences 往 /data/data 目录下保存就好了嘛。再或者这两者不能满足你的要求,你也可以直接将它们保存在 sdcard 下面嘛。反正现在市面上的 APK  在 sdcard 里创建自己的数据文件夹的可不少。

 

2、assets 开发

 

关于读取 assets 目录下的文件,Android 提供了一个 android.content.res.AssetManager 类来实现。这个类的签名体如下图所示

这里我们需要关注的方法有以下几个:

1、构造方法

2、open() 方法

3、openFd() 方法

4、openNonAssetFd() 方法

5、openXmlResourceParser() 方法

 

构造方法

这里我们注意到 AssetManager 的构造方法的权限是 default,这意味着我们无法在我们的程序中通过 new 的方式来实例化它(在Android4.4 中它是 public 修饰的)。通常,我们可以通过两种方式来得到 AssetManager 的实例:1、通过 Context 实例的 getAssets() 方法;2、通过 Resources 实例的 getAssets() 方法

 

open(string) & open(string,int)

这两个方法的作用是一样的。都是将 assets 目录下的某个文件封装成 InputStream 的形式以供使用。说白了就是让我们读文件用的。

 

两个方法中的 string 参数都指的是“文件名”,其实应该说是文件的相对路径更合适,它需要的是某个文件在 assets 目录下的相对路径。例如:dir1/file1.png , dir2/dir3/dir4/file2.avi。

 

第二个方法中还有一个 int 型参数,它是“访问模式”,就是将 assets 目录下的文件以什么模式来打开的意思。它一共有以下 4 种模式可供选择:

1、ACCESS_UNKNOW

无模式。其代表的值是 0。

2、ACCESS_RANDOM

这个不应该翻译成随机访问模式,无序访问模式会更适合一点。这种模式下文件的访问只会打开其中一段内容,然后再根据你的需要向流的前方或后方移动读取指针。其代表的值是 1。

3、ACCESS_STREAMING

顺序读取模式。文件将会被从头部打开,然后按顺序向后面移动读取数据。其代表的值是 2。

4、ACCESS_BUFFER 

缓存读取模式。读取时会将整个文件直接读取到内存中,这种模式适合小文件的读取。其代表的值是 3。

 

在 open(string) 中,它使用的文件读取模式是 ACCESS_STREAMING 模式。

 

openFd(string)

将 assets 目录中的文件以 FileDescriptor 的形式打开,返回一个 AssetFileDescriptor 实例。

 

openNonAssetFd(string) & openNonAssetFd(int,string)

这个其实和上面的 openFd() 是一样的。只不过它是跳出了 assets 目录的范围限定,它是站在工程根目录的视角来打开文件的 FileDescriptor 的。换句话说,它允许打开 APK 中任意位置的文件的 AssetFileDescriptor 实例。

 

openXmlResourceParser(int,string)

打开 assets 目录下的 xml 形式的文件,直接返回 XmlResourceParser 实例。其实就是官方替我们做了从 InputStream 到 XML 解析器之间的转换,有助于增加一些开发效率而已。

 

 

那接下来,我们通过实例来演示一下 assets 目录下的文件的读取方法。首先第一个是直接读取最普通的文件的方法

try {
    InputStream is = this.getAssets().open("moutain.png");
    Log.d("type1", "File available:" + is.available());

    InputStream is2 = this.getResources().getAssets().open("river.png");
    Log.d("type1", "File available2:" + is2.available());
} catch (IOException e) {
    e.printStackTrace();
}

执行的结果如下所示:

 D/type1: File available:3
 D/type1: File available2:11416

当然,最后一定不要忘记将用完的 InputStream 资源关掉!!!

 

第二个是读取自定义目录层级的文件的方法

try {
    InputStream is = this.getAssets().open("sences/nature/forest.png");
    Log.d("type2", "File available:" + is.available());

} catch (IOException e) {
    e.printStackTrace();
}

执行的结果如下所示:

 D/type2: File available:32114

同样,不要忘记调用 InputStream 的 close() 方法哦。

 

第三个是以文件描述符形式读取的方法

try {
    AssetFileDescriptor afd = this.getAssets().openFd("river.png");
    Log.d("type3", "File available:" + afd.getLength());

    InputStream is = afd.createInputStream();
    Bitmap bm = BitmapFactory.decodeStream(is);

    is.close();
    afd.close();

    iv01.setImageBitmap(bm);
} catch (IOException e) {
    e.printStackTrace();
}

这里将一张图片以 AssetFileDescriptor 的形式读取出来,并转换成 Bitmap 显示在 ImageView 上,这段代码的执行结果如下图所示

 

 

3、assets 深度剖析

 

这一小节我们来探究一下 AssetManager 的“前世今生”。

 

首先来看看 AssetManager 实例是怎么来的。通过前面的介绍,我们已经知道了可以直接通过 Activity 实例来调用 getAssets() 方法以取得 AssetManager 实例,或者是通过 Activity 实例里的 getResources() 得到 Resources 的实例以后再调用 getAssets() 来得到 AssetManager 实例。那我们应该都知道,所谓的 Activity 里提供的 getAssets() 方法或者是 getResources() 方法,都是被定义在 android.app.Context 类中的方法,如下图所示

但是它们在 Context 类中都是抽象方法。那这两个方法的具体实现在哪呢?在 android.app.ContextImpl 类上。如下图所示

通过上图,我们很意外地发现,原来通过 Context 的 getAssets() 和通过 Resources 的 getAssets() 走的路线竟然是完全一样的!!!

 

好吧,来不及在这感慨了,我们得接着看看 mResources 又是怎么来的。

这个 mResource 是通过 packageInfo 得到的。而 packageInfo 又是在 ContextImpl 构造的时候传进来的。

我们这里且不管是谁 new 了 ContextImpl 的对象,我们只需要知道这个 packageInfo 是 LoadedApk 类的实例就好了。我们下面直接去看看 LoadedApk 类里的 getResources() 方法。

好嘛,又跳到 ActivityThread 类里去了,跟过去

牛皮,又跳到 ResourcesManager 里去了,没办法,只能再跟。不过在跟踪之前这里必须说一下,这个方法中的第一个参数 resDir 指的是这个 APK 在文件系统下的路径,例如,你是通过 install 方式安装的,它就会在 /data/app 下,你是系统应用它就会在 /system/app 或 /system/priv-app 下。总之这个参数的作用就是把当前正在运行的 APK 的地址传进去,因为后面要去解析它里面的资源的。

 

ResourcesManager 里的 getTopLevelResources 方法比较长,这里只贴需要我们关注的代码。我们在这个方法里终于发现有人实例化了 AssetManager 类。

同时下面一点的地方还发现了这样一段代码

将刚创建出来的 AssetManager 实例传给 Resources 类实例化去了。传它的作用想也不用想就知道是将这个 assets 对象保存起来,以便后面调用 Resources 类的 getAssets() 方法时好返回了。而事实上,Resources 类中的 getAssets() 方法所干的事也确实就是简单地返回这个 assets 对象的引用而已。下图是 Resources 类中的代码实现

 

前面我们简单地了解了 AssetManager 的由来过程。简单总结一下就是:

1、每个 APK 在启动时都会实例化属于自己的 Context 对象;

2、APK 里的资源文件统一具化为 Resources 实例,并由 ResourceManager 管理;

3、assets 目录统一由一个 AssetManager 实例管理;

4、AssetManager 实例在 APK 启动时创建;

 

 

上面是 AssetManager 在应用层的流程。当然其实 AssetManager 还有一个在 Java 以下的流程,这个流程挺复杂的,我这边也不是很有时间,而且感觉了解的太过深入的价值不是很高,就不再继续分析了。不过不排除以后我有兴趣时会继续跟踪下去。

 


 

posted @ 2019-08-07 14:35  大窟窿  阅读(8395)  评论(0编辑  收藏  举报