Android assets

1.应用程序资源管理器assets

assets就是apk工程中的一个普通目录,在每个工程的根目录下都可以发现(或者可以自己创建)一个assets目录。

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

在Android应用开发中,比较常见的可以保存任意类型文件的地方主要有两个:res/raw目录、assets目录。所以,遇到外部文件的预置需求,如音视频、配置文件、字体等时,通常会考虑这两个目录。它们各自的特点是:

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

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

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

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

但是,assets目录在使用上也有一点小缺憾。即assets目录内的文件在程序打包发布以后就是只读的,也就是只能读取里面的文件,而无法修改或增加文件。如果要想增加内容,可以通过Database或者SharedPreferences往/data/data目录下保存就好了。

注:由于应用程序在编译时不会去处理assets目录下的文件,而是直接将它们打包进APK中,这样会造成assets里的文件过大时,编译apk会特别特别慢,因此有时候为了不把assets里的某些文件打包进apk,可以在build.gradle里添加一个设置:

aaptOptions{

    ……

    ignoreAssetsPattern 'file1:file2:file3'

}

 

2.assets 开发

Android使用android.content.res.AssetManager类读取assets目录下的文件。

关于AssetManager有几个方法:

①构造方法

通常可以通过两种方式来得到AssetManager的实例:通过Context实例的getAssets()方法、通过Resources实例的getAssets()方法。

②open()方法:open(string) 、open(string,int)

这两个方法都是将assets目录下的某个文件封装成InputStream的形式供开发者使用,也就是用来读文件的。

其中,参数string指“文件名”,更准确的说是文件的相对路径,即某个文件在assets目录下的相对路径。例如:dir1/file1.png、dir2/dir3/dir4/file2.avi。

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

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

ACCESS_RANDOM:无序访问模式。这种模式下文件的访问只会打开其中一段内容,然后再根据需要向流的前方或后方移动读取指针。其代表的值是1。

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

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

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

③openFd()方法

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

④openNonAssetFd()方法

openNonAssetFd(string)和openNonAssetFd(int,string)

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

⑤openXmlResourceParser()方法

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

接下来,通过实例演示一下assets目录下文件的读取方法。

①直接读取最普通的文件的方法

try{

    InputStream is= this.getAssets().open( "image.png");

    Log.d(TAG, "File available:" + is.available());

    InputStream is2= this.getResources( ).getAssets().open("image2.png");

    Log.d(TAG, "File available2:" + is2.available());

}catch(IOException e) {

    e.printStackTrace();

}

执行的结果如下:

File available:3

File available2:11416

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

②读取自定义目录层级的文件的方法

try{

    InputStream is= this.getAssets().open( "lottie/one/forest.xml");

    Log.d(TAG, "File available:" + is.available());

}catch(IOException e) {

    e.printStackTrace();

}

执行结果如下:

File available:32114

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

③以文件描述符形式读取的方法

try{

    AssetFileDescriptor afd= this.getAssets( ).openFd("river.png");

    Log.d(TAG, "File available:" +afd.getLength());

    InputStream is=afd.createInputStream();

    Bitmap bm=BitmapFactory.decodeStream(is);

    is.close();

    afd.close();

    imageview.setImageBitmap(bm);

}catch(IOException e) {

    e.printStackTrace();

}

将一张图片以AssetFileDescriptor的形式读取出来,并转换成Bitmap显示在ImageView上。

 

3.assets剖析

应用程序中每一个Activity组件都关联有一个ContextImpl对象,这个ContextImpl对象就是用来描述Activity组件的运行上下文环境的。Activity组件继承自Context类,而ContextImpl同样也继承自Context类。在Activity中调用的大部分成员函数都会转发给与它关联的一个ContextImpl对象的对应成员函数来处理,其中就包括用来访问应用程序资源的两个成员函数getResources()和getAssets()。

ContextImpl类的成员函数getResources()返回的是一个Resources对象,有了这个Resources对象之后,就可以通过资源ID来访问那些被编译过的应用程序资源了。

ContextImpl类的成员函数getAssets()返回的是一个AssetManager对象,有了这个AssetManager对象之后,就可以通过文件名来访问那些被编译过或者没有被编译过的应用程序资源文件了。事实上,Resources类也是通过AssetManager类来访问那些被编译过的应用程序资源文件的,不过在访问之前,它会先根据资源ID查找得到对应的资源文件名。

在Android系统中,一个进程可以同时加载多个应用程序,也就是可以同时加载多个APK文件。每一个APK文件在进程中都对应有一个全局的Resourses对象以及一个全局的AssetManager对象。其中这个全局的Resourses对象保存在ContextImpl对象的mResources成员变量中,而这个全局的AssetManager对象保存在这个全局的Resourses对象的成员变量mAssets中。

ContextImpl、Resources和AssetManager的关系图:

2aba9942382248fd863c5afbf43ad8b6.png

Resources类有一个成员函数getAssets(),通过它就可以获得保存在Resources类的成员变量mAssets中的AssetManager,例如ContextImpl类的成员函数getAssets()就是通过调用其成员变量mResources所指向的一个Resources对象的成员函数getAssets()来获得一个可以用来访问应用程序的非编译资源文件的AssetManager。

Android应用程序除了要访问自己的资源外,还需要访问系统的资源。系统的资源打包在/system/framework/framework-res.apk文件中,它在应用程序进程中是通过一个单独的Resources对象和一个单独的AssetManager对象来管理的。这个单独的Resources对象就保存在Resources类的静态成员变量mSystem中,可以通过Resources类的静态成员函数getSystem()获得这个Resources对象,而这个单独的AssetManager对象就保存在AssetManager类的静态成员变量sSystem中,同样可以通过AssetManager类的静态成员函数getSystem()获得这个AssetManager对象。

每一个Activity组件在进程的加载过程中,都会创建一个对应的ContextImpl,并且调用这个ContextImpl的init()方法来初始化Activity组件运行的上下文环境,其中就包括创建用来访问应用程序资源的Resources对象和AssetManager对象:

ContextImpl.init()方法:

class ContextImpl extends Context {  

    LoadedApk mPackageInfo;  

    private Resources mResources;  

    ...…

    final void init(LoadedApk packageInfo,  IBinder activityToken, ActivityThread mainThread) {  

        init(packageInfo, activityToken, mainThread, null);  

    }   

    final void init(LoadedApk packageInfo,  IBinder activityToken, ActivityThread mainThread,  Resources container) {  

        mPackageInfo = packageInfo;  

        mResources = mPackageInfo.getResources( mainThread);  

        ......  

    }   

    ......  

}

参数packageInfo指向的是一个LoadedApk对象,这个LoadedApk对象描述的是当前正在启动的Activity组所属的Apk。用来访问应用程序资源的Resources对象是通过调用参数packageInfo所指向的是一个LoadedApk对象的成员函数getResources来创建的。这个Resources对象创建完成之后,就会保存在ContextImpl类的成员变量mResources中。

LoadedApk.getResources()方法:

final class LoadedApk {  

    private final String mResDir;  

    Resources mResources;  

    ......  

    public Resources getResources(ActivityThread mainThread) {  

        if (mResources == null) {  

            mResources = mainThread.getTopLevelResources(mResDir, this);  

        }  

        return mResources;  

    } 

    ......  

参数mainThread指向了一个ActivityThread对象,这个ActivityThread对象描述的是当前正在运行的应用程序进程。在调用ActivityThread类的getTopLevelResources()方法获得一个Resources对象的时候,需要指定要获取的Resources对象所对应的Apk文件路径,这个Apk文件路径就保存在LoadedApk类的成员变量mResDir中。例如,假设要获取的Resources对象是用来访问系统自带的音乐播放器的资源的,那么对应的Apk文件路径就为/system/app/Music.apk。

ActivityThread.getTopLevelResources()方法:

public final class ActivityThread {  

    ......   

    final HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources  = new HashMap<ResourcesKey, WeakReference<Resources> >();   

    Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {  

        ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);  

        Resources r;  

        synchronized (mPackages) {  

            ......    

            WeakReference<Resources> wr = mActiveResources.get(key);  

            r = wr != null ? wr.get() : null;  

            ......  

           if(r != null && r.getAssets().isUpToDate()){  

                ......  

                return r;  

            }  

        }  

        ......    

        AssetManager assets = new AssetManager();  

        if (assets.addAssetPath(resDir) == 0) {  

            return null;  

        }  

        ......   

        r = new Resources(assets, metrics, getConfiguration(), compInfo);  

        ......  

        synchronized (mPackages) {  

            WeakReference<Resources> wr = mActiveResources.get(key);  

            Resources existing = wr != null ? wr.get() : null;  

            if (existing != null && existing.getAssets().isUpToDate()) {  

                r.getAssets().close();  

                return existing;  

            }  

            mActiveResources.put(key, new WeakReference<Resources>(r));  

            return r;  

        }  

    }   

    ......  

}  

ActivityThread类的成员变量mActiveResources指向的是一个HashMap。这个HashMap用来维护在当前应用程序进程中加载的每一个Apk文件及其对应的Resources对象的对应关系。也就是说,给定一个Apk文件路径,ActivityThread类的成员函数getTopLevelResources可以在成员变量mActiveResources中检查是否存在一个对应的Resources对象。如果不存在,那么就会新建一个,并且保存在ActivityThread类的成员变量mActiveResources中。

参数resDir即为要获取其对应的Resources对象的Apk文件路径,ActivityThread类的成员函数getTopLevelResources首先根据它来创建一个ResourcesKey对象,然后再以这个ResourcesKey对象在ActivityThread类的成员变量mActiveResources中检查是否存在一个Resources对象。如果存在,并且这个Resources对象里面包含的资源文件没有过时,即调用这个Resources对象的成员函数getAssets所获得一个AssetManager对象的成员函数isUpToDate的返回值等于true,那么ActivityThread类的成员函数getTopLevelResources就可以将该Resources对象返回给调用者了。

如果不存在与参数resDir对应的Resources对象,或者存在这个Resources对象,但是存在的这个Resources对象是过时的,那么ActivityThread类的成员函数getTopLevelResources就会新创建一个AssetManager对象,并且调用这个新创建的AssetManager对象的成员函数addAssetPath来将参数resDir所描述的Apk文件路径作为它的资源目录。

创建了一个新的AssetManager对象,还需要这个AssetManager对象来创建一个新的Resources对象。这个新创建的Resources对象需要以前面所创建的ResourcesKey对象为键值缓存在ActivityThread类的成员变量mActiveResources所描述的一个HashMap中,以便以后可以获取回来使用。不过,这个新创建的Resources对象在缓存到ActivityThread类的成员变量mActiveResources所描述的一个HashMap去之前,需要再次检查该HashMap是否已经存在一个对应的Resources对象了,这是因为当前线程在创建新的AssetManager对象和Resources对象的过程中,可能有其它线程抢先一步创建了与参数resDir对应的Resources对象,并且将该Resources对象保存到该HashMap中去了。

如果没有其它线程抢先创建一个与参数resDir对应的Resources对象,或者其它线程抢先创建出来的Resources对象是过时的,那么就会将前面创建的Resources对象缓存到成员变量mActiveResources所描述的一个HashMap中去,并且将前面创建的Resources对象返回给调用者。

 

 

  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值