Android资源管理框架-------之资源的缓存和preload(十)

        对于integer、bool等values类型的资源还好,对于比较大的资源比如Drawable等,如果我们每次去获取,都要从resources.arsc中查寻资源相关的信息(对于Drawable来说就是资源路径),然后再根据资源信息(路径)去加载图片,那么这个过程将会非常耗时。所以,为了加快资源的获取速度,同时减少不必要的加载次数,android在资源管理过程中存在着大量的缓存机制,并且还会在Zygote进程起来后就会preload许多系统资源。本文我们挑选一些典型的代码来做分析。

ResourcesManager中对Resources的缓存

        ResourcesManager是Android用来统一管理同一个进程中的所有资源,也就是Resources对象的,应用进程中每一个Resources对象的创建和获取都应该经过ResourcesManager之手。它会对它自己创建的每一个Resources对象做缓存,我们在获取Resources对象的时候,如果缓存中已经存在并且没有过时,那么ResourcesManager就会直接返回缓存中的Resources;如果不存在则会创建对应的Resources,然后把它加入缓存并返回:

//frameworks/base/core/java/android/app/ResourcesManager.java
public class ResourcesManager {
    private static ResourcesManager sResourcesManager;
    
    //...
    /**
     * 缓存在mActiveResources里面,注意这里使用了若引用,
     * 也是为了当进程中没有地方使用这个Resources对象的时候能够
     * 在gc的同时释放掉资源,节省内存
     */
    final ArrayMap<ResourcesKey, WeakReference<Resources> > mActiveResources
            = new ArrayMap<ResourcesKey, WeakReference<Resources> >();

    //非常典型的一个单例模式
    public static ResourcesManager getInstance() {
        synchronized (ResourcesManager.class) {
            if (sResourcesManager == null) {
                sResourcesManager = new ResourcesManager();
            }
            return sResourcesManager;
        }
    }

    public Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
        //根据这些参数构造key
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
        Resources r;
        /**
         * 根据key去缓存中查找
         */
        WeakReference<Resources> wr = mActiveResources.get(key);
        r = wr != null ? wr.get() : null;
        /**
         * 如果查到了并且没有过时,就可以返回了
         * 这里的没有过时是指,AssetManager加载完APK后,这个APK没有再被修改过
         */
        if (r != null && r.getAssets().isUpToDate()) {
            return r;
       }

       //如果缓存中没有,那么就要创建新的对象了
       //创建AssetManager对象
       AssetManager assets = new AssetManager();
       //添加APK本身这个资源包
       if (resDir != null) {
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }
        //添加分片资源包
        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }
        //添加Runtime Resources Overlay资源包
        if (overlayDirs != null) {
            for (String idmapPath : overlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }
        //添加资源共享库
        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (assets.addAssetPath(libDir) == 0) {
                    Slog.w(TAG, "Asset path '" + libDir +
                            "' does not exist or contains no resources.");
                }
            }
        }

        //.......

        //创建Resources对象
        r = new Resources(assets, dm, config, compatInfo, token);

        //......
        
        //放入缓存
        mActiveResources.put(key, new WeakReference<Resources>(r));
        //可以返回了
        return r;
    }
}

        另外需要注意的是,不论是Resources对象还是AssetManager对象,都会有一个静态的实例,代表系统资源,这就是说,在同一进程中,代表系统资源的Resources对象和AssetManager对象只有一个,大家共享之,避免重复:

//frameworks/base/core/java/android/content/res/Resources.java
public class Resources {
    static Resources mSystem = null;
    //典型的单例模式,线程安全
    public static Resources getSystem() {
        synchronized (sSync) {
            Resources ret = mSystem;
            if (ret == null) {
                //构造方法会调用AssetManager.getSystem()
                //并为它设置配置信息,创建StringBlock对象
                ret = new Resources();
                mSystem = ret;
            }

            return ret;
        }
    }
}
//frameworks/base/core/java/android/content/res/AssetManager.java
public final class AssetManager implements AutoCloseable {

    static AssetManager sSystem = null;

    private static void ensureSystemAssets() {
        synchronized (sSync) {
            if (sSystem == null) {
                //创建代表系统资源的AssetManager对象,构造方法中会加载
                //framework-res.apk中的resources.arsc
                AssetManager system = new AssetManager(true);
                //创建StringBlock对象,一个StringBlock代表一个Global String Pool
                system.makeStringBlocks(null);
                sSystem = system;
            }
        }
    }

    public static AssetManager getSystem() {
        //加载framework-res.apk中的resources.arsc
        ensureSystemAssets();
        return sSystem;
    }
}

        AssetManager中的相关逻辑在Android资源管理框架-------之Android中的资源包(二)中已经有所描述,这里就不重复了。

压缩包(APK)、resources.arsc以及ResTable的缓存

        当我们给AssetManager添加资源包的时候,会调用其AddAssetPath方法,AddAssetPath方法调用appendPathToResTable方法,这个方法会根据这个路径去加载压缩包,也就是我们的APK包,然后从中取出resources.arsc,然后再加载resources.arsc中的内容创建ResTable对象。Android对APK包、resources.arsc和系统的ResTable(非应用的)都有缓存机制。先说一下它们缓存相关的数据结构吧:一个APK包对应缓存中的一个ZipFileRO对象、一个resources.arsc对应缓存中的一个Asset对象,而ResTable的缓存还是一个ResTable对象。它们的缓存机制在appendPathToResTable方法中都有体现。这段代码注释得比较多,但对本文而言关键点仅有四处,我们在注释里有*****关键代码(X)*****这样的标记,其它的注释,只是为了帮助大家理解这个方法,比较熟悉的可以只看*****关键代码(X)*****标记的部分。

//frameworks/base/libs/androidfw/AssetManager.cpp
bool AssetManager::appendPathToResTable(const asset_path& ap) const {
    /*
     * 在这里ass表示我们的资源包中的resources.arsc,
     * sharedRes则是用来存放资源包中的resources.arsc中的具体数据
     */
    Asset* ass = NULL;
    ResTable* sharedRes = NULL;
    
    bool shared = true;
    bool onlyEmptyResources = true;
    MY_TRACE_BEGIN(ap.path.string());
    //idmap 是RRO相关的概念,这里不再多说
    Asset* idmap = openIdmapLocked(ap);
    /*
     * 这里要先看一下已经加载过多少个资源包了,
     * 如果没有加载过,那么就认为默认加载的头一个资源包是framework-res.apk
     * 也就是Android的系统资源包
     */
    size_t nextEntryIdx = mResources->getTableCount();

    /*
     * 我们要加载resources.arsc,大多数情况这个文件位于压缩包也就是apk中
     * 对应我们这个if分支
     * 但AssetManager也是支持对解压出来的resources.arsc的
     * 这种情况对应下面的else分支
     */
     //resources.arsc在apk中
    if (ap.type != kFileTypeDirectory) {
        
        if (nextEntryIdx == 0) {// *****关键代码(一)*****
            //加载的是framework-res.apk,也就是Google的Android系统资源包
            //先去缓存中查找系统资源包及其overlay包是否已经加载过,如果加载过,同时会创建
            //ResTable对象,并缓存,也就是sharedRes得到的值肯定不为空
            sharedRes = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTable(ap.path);
            if (sharedRes != NULL) {
                /**
                 * 如果已经创建过了,那么要添加的这个资源包,就应该放到它们后面
                 * 这种情况出现在在同一个进程创建多个不同的AssetManager或者Resources对象的时候,
                 * 每个AssetManager中都会添加系统资源包,这时候就用上这个缓存了
                 */ 
                nextEntryIdx = sharedRes->getTableCount();
            }
        }

        //如果当前要加载的不是系统资源或者是系统资源,但是之前并未加载过,走这里
        if (sharedRes == NULL) {
            // *****关键代码(二)*****
            //查缓存,看看之前是否加载并创建过Asset对象,这里的Asset对象对应的就是加载到
            //内存中的resources.arsc
            ass = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTableAsset(ap.path);
            //如果没有找到,则加载之,即打开resources.arsc,并将得到的ass放入缓存
            if (ass == NULL) {
                ALOGV("loading resource table %s\n", ap.path.string());
                // *****关键代码(三)*****
                /**
                 * 去压缩包中打开resources.arsc
                 * 具体步骤为:先去缓存中查找压缩包(也就是APK)是否已经加载过了,
                 * 如果已经加载过,则直接从缓存中取出APK;否则,先从加载APK。
                 * 然后再从APK中解压出resources.arsc
                 */ 
                ass = const_cast<AssetManager*>(this)->
                    openNonAssetInPathLocked("resources.arsc",
                                             Asset::ACCESS_BUFFER,
                                             ap);
                //放入缓存
                if (ass != NULL && ass != kExcludedAsset) {
                    ass = const_cast<AssetManager*>(this)->
                        mZipSet.setZipResourceTableAsset(ap.path, ass);
                }
            }
            //如果要加载的是系统资源,但这时候还从未加载过,则连同系统资源包的overlay package一并加载
            if (nextEntryIdx == 0 && ass != NULL) {
                // If this is the first resource table in the asset
                // manager, then we are going to cache it so that we
                // can quickly copy it out for others.
                ALOGV("Creating shared resources for %s", ap.path.string());
                //创建ResTable对象
                sharedRes = new ResTable();
                //加入资源包中的resources.arsc以及idmap
                sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
#ifdef HAVE_ANDROID_OS
                
                const char* data = getenv("ANDROID_DATA");
                LOG_ALWAYS_FATAL_IF(data == NULL, "ANDROID_DATA not set");
                String8 overlaysListPath(data);
                overlaysListPath.appendPath(kResourceCache);
                overlaysListPath.appendPath("overlays.list");
                //加载系统资源包的overlay package,并将这些资源包也加入sharedRes,
                addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx);
#endif

                // *****关键代码(四)*****
                //缓存sharedRes
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.setZipResourceTable(ap.path, sharedRes);
            }
        }
    } else {//resources.arsc不在apk中,而在某个路径下
        //加载到内存,创建Asset对象
        ass = const_cast<AssetManager*>(this)->
            openNonAssetInPathLocked("resources.arsc",
                                     Asset::ACCESS_BUFFER,
                                     ap);
        //不是apk中的不缓存,不共享,添完就删
        shared = false;
    }
    if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
        if (sharedRes != NULL) {
            ALOGV("Copying existing resources for %s", ap.path.string());
            //如果是系统资源包,则将系统资源包连同它的overlay package一起添加到我们的
            //mResources对象当中去
            mResources->add(sharedRes);
        } else {
            ALOGV("Parsing resources for %s", ap.path.string());
            //否则只添加我们要加载的这个资源包(其实仅仅是它的resources.arsc和idmap文件而已)
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
        onlyEmptyResources = false;
        //不共享的话,就没必要留着了
        if (!shared) {
            delete ass;
        }
    } else {
        //没找到资源
        ALOGV("Installing empty resources in to table %p\n", mResources);
        mResources->addEmpty(nextEntryIdx + 1);
    }
    if (idmap != NULL) {
        delete idmap;
    }
    MY_TRACE_END();
    return onlyEmptyResources;
}

        先看*****关键代码(一)*****,Android会对framework-res.apk创建一个单独的ResTable对象,并将其缓存。在我们构造新的 java层的AssetManager对象时,每次系统都会往里面添加系统的framework-res.apk资源包。根据我们前面文章的分析,很明显每个Android应用进程或者system_server进程默认都会有代表系统资源的mSystem变量,这个变量是java层Resources类的一个静态变量,在Zygote进程preload资源的时候就会构造。也就是说,在我们的应用程序中,如果我们构造了自己的Resources或者AssetManager对象,系统同样会将framework-res.apk这个资源包添加进去,不过流程将不再是加载framework-res.apk,然后解压出resources.arsc,最后添加到ResTable中;而是从缓存中取出framework-res.apk对应的ResTable对象,然后将其合并到我们新创建的AssetManager对象的ResTable中。我们看sharedRes = const_cast<AssetManager*>(this)->mZipSet.getZipResourceTable(ap.path);这句,其实现为:

//frameworks/base/libs/androidfw/AssetManager.cpp

/**
 * ZipSet类用来管理一个AssetManager中添加的所有APK文件
 * AssetManager中有一个它的实例 mZipSet
 */
ResTable* AssetManager::ZipSet::getZipResourceTable(const String8& path)
{
    //ZipSet中有两个缓存Vector,一个存APK文件,一个存路径,根据路径,找是否加载过APK文件
    int idx = getIndex(path);
    sp<SharedZip> zip = mZipFile[idx];
    //如果没有加载过APK文件,那就先加载再把它放入缓存Vector
    if (zip == NULL) {
        //等下介绍其实现
        zip = SharedZip::get(path);
        mZipFile.editItemAt(idx) = zip;
    }
    /**
     * 接下来的处理就要分开说了
     * 如果是之前并没有缓存过对应的ResTable对象,那么下面这句就会返回空
     * 如果之前已经缓存过ResTable对象,那么会将缓存返回
     */
    return zip->getResourceTable();
}

        那么什么时候缓存系统的ResTable对象呢?看*****关键代码(四)*****的实现:

//frameworks/base/libs/androidfw/AssetManager.cpp
ResTable* AssetManager::ZipSet::setZipResourceTable(const String8& path,
                                                    ResTable* res)
{
    int idx = getIndex(path);
    sp<SharedZip> zip = mZipFile[idx];
    return zip->setResourceTable(res);
}

        这里的zip->getResourceTable()zip->setResourceTable(res)方法就是简单的getter和setter方法,就不贴代码了。也就是说在*****关键代码(四)*****的位置,也就是Zygote preload系统资源的时候,就会将系统资源包对应的ResTable对象缓存到SharedZip即里面去,后面再用,就会直接从里面取。到这里,系统资源包的ResTable对象的缓存就说完了。
        我们再说压缩包,也就是APK文件的缓存机制。一个APK文件对应的数据结构是一个ZipFileRO对象(这里的RO很明显就是read only了)。不过AssetManager为了方便使用,又对它做了封装,这个封装就是`SharedZip:

//frameworks/base/include/androidfw/AssetManager.h
class SharedZip : public RefBase {
    
    //省略其方法的声明

    private:
        SharedZip(const String8& path, time_t modWhen);
        SharedZip(); // <-- not implemented

        //APK的路径
        String8 mPath;
        //APK文件,本质是一个ZIP包
        ZipFileRO* mZipFile;
        //修改时间,主要用来判断我们的ResTable中的资源是否过时
        time_t mModWhen;
        //代表一个resources.arsc
        Asset* mResourceTableAsset;
        //缓存framework-res.apk对应的ResTable对象
        ResTable* mResourceTable;
        //RRO相关,overlay包的路径
        Vector<asset_path> mOverlays;

        static Mutex gLock;
        //已经加载过的APK包
        static DefaultKeyedVector<String8, wp<SharedZip> > gOpen;
    };

        需要注意的是gOpen成员是static的,也就是说SharedZip是会缓存每一个打开过的APK的。前面我们在分析AssetManager::ZipSet::getZipResourceTable的实现时,涉及到了zip = SharedZip::get(path);这句,我们看其实现:

//frameworks/base/libs/androidfw/AssetManager.cpp
sp<AssetManager::SharedZip> AssetManager::SharedZip::get(const String8& path,
        bool createIfNotPresent)/*createIfNotPresent 默认是true*/
{
    AutoMutex _l(gLock);
    time_t modWhen = getFileModDate(path);
    //先去查缓存
    sp<SharedZip> zip = gOpen.valueFor(path).promote();
    //查到了,并且没有问题则返回
    if (zip != NULL && zip->mModWhen == modWhen) {
        return zip;
    }
    if (zip == NULL && !createIfNotPresent) {
        return NULL;
    }
    //没查到在去加载,并加入缓存,下次就可以直接用了
    zip = new SharedZip(path, modWhen);
    gOpen.add(path, zip);
    return zip;

}

        然后,我们再来看看resources.arsc的缓存,这个体现在*****关键代码(二)**********关键代码(三)*****。这句ass = const_cast<AssetManager*>(this)->mZipSet.getZipResourceTableAsset(ap.path)的实现:

//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::ZipSet::getZipResourceTableAsset(const String8& path)
{
    //先拿到APK包,可能走缓存哦
    int idx = getIndex(path);
    sp<SharedZip> zip = mZipFile[idx];
    if (zip == NULL) {
        zip = SharedZip::get(path);
        mZipFile.editItemAt(idx) = zip;
    }
    //再从SharedZip的缓存里找resources.arsc对应的缓存
    return zip->getResourceTableAsset();
}

//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::SharedZip::getResourceTableAsset()
{
    ALOGV("Getting from SharedZip %p resource asset %p\n", this, mResourceTableAsset);
    return mResourceTableAsset;
}

        在*****关键代码(二)*****那里,先去查缓存,如果没查到,则进入*****关键代码(三)*****,去加载APK,解压出resources.arsc,构造其对应的Asset对象:

//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::openNonAssetInPathLocked(const char* fileName, AccessMode mode,
    const asset_path& ap)
{
    //......

    String8 path(fileName);
    //这里还是会走AssetManager::ZipSet::getZip方法,还是会去查APK是否缓存过
    ZipFileRO* pZip = getZipFileLocked(ap);
    if (pZip != NULL) {
        //printf("GOT zip, checking NA '%s'\n", (const char*) path);
        //resources.arsc
        ZipEntryRO entry = pZip->findEntryByName(path.string());
        if (entry != NULL) {
            //printf("FOUND NA in Zip file for %s\n", appName ? appName : kAppCommon);
            //解压出来,并创建Asset对象
            pAsset = openAssetFromZipLocked(pZip, entry, mode, path);
            pZip->releaseEntry(entry);
            }
        }
   }

   //......

}

        在*****关键代码(三)*****后面还会调用mZipSet.setZipResourceTableAsset(ap.path, ass);resources.arsc对应的Asset存入缓存:

//frameworks/base/libs/androidfw/AssetManager.cpp
Asset* AssetManager::ZipSet::setZipResourceTableAsset(const String8& path,
                                                 Asset* asset)
{
    int idx = getIndex(path);
    sp<SharedZip> zip = mZipFile[idx];
    // doesn't make sense to call before previously accessing.
    //存入对应的SharedZip对象中缓存起来
    return zip->setResourceTableAsset(asset);
}

Bag资源的缓存

        对于带有Bag的资源,比如说一个style,它的Bag(可以理解为item)可能会有许多,在同一个AssetManager对象中,当我们获取过其中的某一个Bag后,也会被缓存起来,下次再获取的时候就不必再去resources.arsc中查找解析了。这个缓存在PackageGroup中:

struct ResTable::PackageGroup
{

    //......省略无关的代码

    // Computed attribute bags, first indexed by the type and second
    // by the entry in that type.
    ByteBucketArray<bag_set**>*     bags;
    
    DynamicRefTable                 dynamicRefTable;
}

        bags成员就是用来做缓存的,一个bag_set就代表一个资源项的所有Bag,比如一个R.style.Widget_CompoundButton的所有item都会按照bag的id升序排列的。bags的组织方式大概如下:
在这里插入图片描述

        我们看在获取bag时是怎么利用缓存的:

//frameworks/base/libs/androidfw/ResourceTypes.cpp
ssize_t ResTable::getBagLocked(uint32_t resID, const bag_entry** outBag,
        uint32_t* outTypeSpecFlags) const
{
    if (mError != NO_ERROR) {
        return mError;
    }

    //拿到PackageGroup、Type、Entry的索引
    const ssize_t p = getResourcePackageIndex(resID);
    const int t = Res_GETTYPE(resID);
    const int e = Res_GETENTRY(resID);

    //printf("Get bag: id=0x%08x, p=%d, t=%d\n", resID, p, t);
    //得到PackageGroup对象,合法性检查略去
    PackageGroup* const grp = mPackageGroups[p];
    
    //拿到TypeList,合法性检查略去
    const TypeList& typeConfigs = grp->types[t];
   
    //对entry的index做合法性检查略去
    const size_t NENTRY = typeConfigs[0]->entryCount;
    
    // First see if we've already computed this bag...
    /**
     * 在PackageGroup中,是有对Bag资源做缓存的,先去查缓存
     */
    if (grp->bags) {
        //bags根据资源的type、entry的index做了分类
        //根据type、entry的index找到对应的bag_set
        bag_set** typeSet = grp->bags->get(t);
        if (typeSet) {
            bag_set* set = typeSet[e];
            if (set) {
                //缓存里面有
                if (set != (bag_set*)0xFFFFFFFF) {
                    if (outTypeSpecFlags != NULL) {
                        *outTypeSpecFlags = set->typeSpecFlags;
                    }
                    //返回缓存里的数据,bag_set后面跟着所有的bag_entry
                    *outBag = (bag_entry*)(set+1);
                    //ALOGI("Found existing bag for: %p\n", (void*)resID);
                    //返回bag的数量
                    return set->numAttrs;
                }
                ALOGW("Attempt to retrieve bag 0x%08x which is invalid or in a cycle.",
                     resID);
                return BAD_INDEX;
            }
        }
    }
    
    // Bag not found, we need to compute it!
    /**
     * 如果缓存中木有查到,那么说明我们是第一次获取,
     * 我们就只能自己去解析resources.arsc中对应的数据块来获取了
     */
     bag_set* set = NULL
     
     //去resources.arsc里加载解析,非本文重点,略去,结果会存在set里

     //把查找到的结果存入缓存
     typeSet[e] = set;
     
     //...
}

        这里我们不再详细介绍getBagLocked方法,因为其非常复杂,感兴趣的同学可以看这里:Android资源管理框架-------之Bag资源信息的获取(七)

Drawable资源的缓存

        在Resources类中,对DrawableColorStateList以及Animator等都有缓存,我们这里就以Drawable为例来稍作说明。

//frameworks/base/core/java/android/content/res/Resources.java
public class Resources {

    //......

    /**
     * 存储预加载的Drawable(xml、PNG等)
     * 这个数组只有两个元素,和layoutdirection对应
     * 因为要支持系统根据不同的布局方向,选择不同的图片资源
     * 所以要分开存
     */ 
    private static final LongSparseArray<ConstantState>[] sPreloadedDrawables;
    /**
     * 存储预加载的ColorDrawable(color)
     * ColorDrawable就无所谓布局方向了,不需要用数组
     */
    private static final LongSparseArray<ConstantState> sPreloadedColorDrawables
            = new LongSparseArray<ConstantState>();
    /**
     * Drawable(xml、PNG等)的缓存
     * 这个缓存是按照theme分类的,其中
     * key 也就是这个String表示theme的ID
     * value表示同一theme下的所有Drawable
     */
    private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mDrawableCache =
            new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>();
    /**
     * Drawable(color)的缓存
     * 这个缓存是按照theme分类的,其中
     * key 也就是这个String表示theme的ID
     * value表示同一theme下的所有Drawable
     */
    private final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> mColorDrawableCache =
            new ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>>();

    //......
}

        需要说明的是,LongSparseArray我们可以简单认为它是一个KEY为long类型的Map(网上介绍SparseArray的文章很多,这里就不多说了)。在这里所谓的long类型的KEY具体是什么呢?确切地说,它是cookie值+资源的路径字符串在Global String Pool中的索引(图片)或者是颜色值(Color),也就是说,它代表一个资源的信息;而VALUE则是ConstantState对象的弱引用,它就是最终的资源本身了。ConstantState则是一个抽象类,它的子类是Drawable中非常关键的成员,比如BitmapDrawable中的BitmapStateColorDrawable中的ColorState等等。还有,我们看到preload的Drawable使用的都是强引用,而cache的Drawable则使用的是弱引用,这也就是说,gc的时候,如果这些cache的资源没有被引用,则会被回收掉,以便节省内存。另外,preload的时候只会preload系统的资源,也就是framework-res.apk里的资源,应用自己的资源在preload的时候,系统是不知道的。我们了解了这些基本的数据结构后在来看getDrawable的实现:

    //frameworks/base/core/java/android/content/res/Resources.java
    public Drawable getDrawable(int id) throws NotFoundException {
        final Drawable d = getDrawable(id, null);
        if (d != null && d.canApplyTheme()) {
            Log.w(TAG, "Drawable " + getResourceName(id) + " has unresolved theme "
                    + "attributes! Consider using Resources.getDrawable(int, Theme) or "
                    + "Context.getDrawable(int).", new RuntimeException());
        }
        return d;
    }

        调用了一个重载方法,加了一个参数theme。这里大家可能不解,Drawable还跟theme有关系? 答案是确实有关系,我们可以在theme里指定Drawable的某些特定属性,比如是否抗锯齿等。

    //frameworks/base/core/java/android/content/res/Resources.java
    public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
       
        TypedValue value;
        synchronized (mAccessLock) {
            value = mTmpValue;
            if (value == null) {
                value = new TypedValue();
            } else {
                mTmpValue = null;
            }
            /**
             * 我们知道TypedValue和Res_value基本对应(多了cookie)
             * value.data 里放的值可能是:
             * 如果类型是COLOR,那value.data里放的将会是color值
             * 如果是图片,那value.data里放的将会是cookie值和一个字符串在global string pool中的索引,
             * 这个字符串表示图片的路径
             */
            getValue(id, value, true);
        }
        //根据资源信息去加载资源
        final Drawable res = loadDrawable(value, id, theme);
        synchronized (mAccessLock) {
            if (mTmpValue == null) {
                mTmpValue = value;
            }
        }
        return res;
    }

        接下来才是关键,缓存相关的逻辑都在loadDrawable方法里:

    //frameworks/base/core/java/android/content/res/Resources.java
    Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
        //是colorDrawable还是png、xml
        final boolean isColorDrawable;
        final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches;
        final long key;
        //表示是color,而不是xml、png
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
                && value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
            //指向mColorDrawableCache
            caches = mColorDrawableCache;
            //key是一个颜色值
            key = value.data;
        } else {//png、xml
            isColorDrawable = false;
            //指向mDrawableCache
            caches = mDrawableCache;
            //cookie值 + 字符串索引
            key = (((long) value.assetCookie) << 32) | value.data;
        }

        // First, check whether we have a cached version of this drawable
        // that was inflated against the specified theme.
        /**
         * 我们知道preload资源的时候是资源的第一次加载,
         * 也就没必要查缓存了
         */
        if (!mPreloading) {
            //去缓存里查,查到了直接返回
            final Drawable cachedDrawable = getCachedDrawable(caches, key, theme);
            if (cachedDrawable != null) {
                return cachedDrawable;
            }
        }

        // Next, check preloaded drawables. These are unthemed but may have
        // themeable attributes.
        final ConstantState cs;
        //去preload的资源里查
        if (isColorDrawable) {
            cs = sPreloadedColorDrawables.get(key);
        } else {
            //根据布局方向不同,分开的
            cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
        }

        final Drawable dr;
        //如果查到里了,则根据查到到的ConstantState对象创建对应的drawable对象
        if (cs != null) {
            final Drawable clonedDr = cs.newDrawable(this);
            if (theme != null) {
                dr = clonedDr.mutate();
                dr.applyTheme(theme);
                dr.clearMutated();
            } else {
                dr = clonedDr;
            }
            //没查到,类型是颜色
        } else if (isColorDrawable) {
            dr = new ColorDrawable(value.data);
        } else {
            /**
             * 没查到,类型是xml、png
             * loadDrawableForCookie则会先根据value中的cookie
             * 和value.data拿到资源(xml或者png)的路径,剩下的就是根据
             * 路径加载资源,创建drawable了,这里就不作详细介绍了,有兴趣的
             * 同学可以自己看
             */ 
            dr = loadDrawableForCookie(value, id, theme);
        }

        // If we were able to obtain a drawable, store it in the appropriate
        // cache (either preload or themed).
        //放入缓存
        if (dr != null) {
            //这个changingConfigurations,对应于resources.arsc中ResTable_typeSpec后面跟的
            //typeSpecFlags,表明该项资源都会随着哪些配置的不同而不同
            dr.setChangingConfigurations(value.changingConfigurations);
            cacheDrawable(value, theme, isColorDrawable, caches, key, dr);
        }

        return dr;
    }

        这个方法总结起来也是非常简单,查缓存,查到了返回;查不到则创建资源,然后缓存资源,最后返回。但这里面有两个方法值得一说:getCachedDrawablecacheDrawable。我们一个一个看:

    //frameworks/base/core/java/android/content/res/Resources.java
    private Drawable getCachedDrawable(
            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
            long key, Theme theme) {
        synchronized (mAccessLock) {
            //preload的资源,theme是空的
            final String themeKey = theme != null ? theme.mKey : "";
            //先根据theme的key取出
            final LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey);
            if (themedCache != null) {
                /**
                 * 从LongSparseArray中根据key取出元素,
                 * 由于是弱引用,还需要判断是否被回收等处理
                 * 都是常规操作,这里就不贴代码了。
                 */
                final Drawable themedDrawable = getCachedDrawableLocked(themedCache, key);
                if (themedDrawable != null) {
                    return themedDrawable;
                }
            }

            // No cached drawable, we'll need to create a new one.
            return null;
        }
    }

        再看cacheDrawable方法,顾名思义,它是用来缓存Drawable的,但是在preloading的时候,它的处理有些不同:

    //frameworks/base/core/java/android/content/res/Resources.java
    private void cacheDrawable(TypedValue value, Theme theme, boolean isColorDrawable,
            ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches,
            long key, Drawable dr) {
        //先判空
        final ConstantState cs = dr.getConstantState();
        if (cs == null) {
            return;
        }

        if (mPreloading) {//preloading的时候走这里
            // Preloaded drawables never have a theme, but may be themeable.
            final int changingConfigs = cs.getChangingConfigurations();
            
            if (isColorDrawable) {
                /**
                 * 判断该ColorDrawable是否需要保存
                 * 对于ColorDrawable,如果它会随着字体大小和density外的其它配置
                 * 的不同而不同的话,就没必要保存了。
                 */ 
                if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
                    sPreloadedColorDrawables.put(key, cs);
                }
            } else {
                /**
                 * 判断该Drawable是否需要保存
                 * 对于Drawable,如果它会随着字体大小和density以及布局方向外的其它配置
                 * 的不同而不同的话,就没必要保存了。
                 */ 
                if (verifyPreloadConfig(
                        changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
                    if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
                        // If this resource does not vary based on layout direction,
                        // we can put it in all of the preload maps.
                        sPreloadedDrawables[0].put(key, cs);
                        sPreloadedDrawables[1].put(key, cs);
                    } else {
                        // Otherwise, only in the layout dir we loaded it for.
                        sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
                    }
                }
            }
        } else {//非preloading
            synchronized (mAccessLock) {
                //preload的theme为空
                final String themeKey = theme == null ? "" : theme.mKey;
                //先根据theme的key取出对应的LongSparseArray
                LongSparseArray<WeakReference<ConstantState>> themedCache = caches.get(themeKey);
                //如果是缓存该theme的第一个资源,先创建LongSparseArray
                if (themedCache == null) {
                    themedCache = new LongSparseArray<WeakReference<ConstantState>>(1);
                    caches.put(themeKey, themedCache);
                }
                //放进去
                themedCache.put(key, new WeakReference<ConstantState>(cs));
            }
        }
    }

        我们看看到底是如何判断是否保存资源的:

    private boolean verifyPreloadConfig(@Config int changingConfigurations,
            @Config int allowVarying, @AnyRes int resourceId, @Nullable String name) {
        // We allow preloading of resources even if they vary by font scale (which
        // doesn't impact resource selection) or density (which we handle specially by
        // simply turning off all preloading), as well as any other configs specified
        // by the caller.
        if (((changingConfigurations&~(ActivityInfo.CONFIG_FONT_SCALE |
                ActivityInfo.CONFIG_DENSITY)) & ~allowVarying) != 0) {
            String resName;
            try {
                resName = getResourceName(resourceId);
            } catch (NotFoundException e) {
                resName = "?";
            }
            // This should never happen in production, so we should log a
            // warning even if we're not debugging.
            Log.w(TAG, "Preloaded " + name + " resource #0x"
                    + Integer.toHexString(resourceId)
                    + " (" + resName + ") that varies with configuration!!");
            return false;
        }
        return true;
    }

        也就是说除了CONFIG_FONT_SCALECONFIG_DENSITY以及我们指定的配置外,如果这个drawabe还会随着其它配置的不同而不同,那就不保存了。这里我们明白,对于资源的preload,也并不是说preload完了的所有资源我们都会保存,毕竟一个资源可能会随着配置项的不同而不同,如果都保存,那就太多了。

资源的preload

        我们知道,Android应用进程以及system_server进程都是由Zygote进程fork出来的。Zygote进程在起来后,会preload许多东西,这样做的好处是,fork出来的每一个进程都已经加载好preload的东西了;否则,每创建一个应用进程都要把这些东西都加载一遍,那就影响启动速度了,这些preload的东西中,就包括资源。其调用序列为:main–>preload–>preloadResources:

    //frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    private static void preloadResources() {
        final VMRuntime runtime = VMRuntime.getRuntime();

        Debug.startAllocCounting();
        try {
            System.gc();
            runtime.runFinalizationSync();
            /**
             * 这句会触发对framework-res.apk的加载
             * Resources、AssetManager、ResTable的创建等等
             */
            mResources = Resources.getSystem();
            mResources.startPreloading();
            if (PRELOAD_RESOURCES) {//true
                Log.i(TAG, "Preloading resources...");

                long startTime = SystemClock.uptimeMillis();
                //预加载图片资源
                TypedArray ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_drawables);
                int N = preloadDrawables(runtime, ar);
                ar.recycle();
                Log.i(TAG, "...preloaded " + N + " resources in "
                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
				addBootEvent(new String("Zygote:Preload "+ N + " obtain resources in " +
								(SystemClock.uptimeMillis() - startTime) + "ms"));

                startTime = SystemClock.uptimeMillis();
                ar = mResources.obtainTypedArray(
                        com.android.internal.R.array.preloaded_color_state_lists);
                //预加载ColorStateList资源
                N = preloadColorStateLists(runtime, ar);
                ar.recycle();
                Log.i(TAG, "...preloaded " + N + " resources in "
                        + (SystemClock.uptimeMillis()-startTime) + "ms.");
                /// M: Added for BOOTPROF @{
                addBootEvent(new String("Zygote:Preload "+ N + " resources in " +
                (SystemClock.uptimeMillis() - startTime) + "ms"));
                /// @}
            }
            mResources.finishPreloading();
        } catch (RuntimeException e) {
            Log.w(TAG, "Failure preloading resources", e);
        } finally {
            Debug.stopAllocCounting();
        }
    }

        mResources.startPreloading();mResources.finishPreloading();最关键的就是控制mPreloading的值,前者把值设置为true,后者设置为false。我们可以看一下preloadDrawables方法,preloadColorStateLists就不再介绍了:

    //frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
    private static int preloadDrawables(VMRuntime runtime, TypedArray ar) {
        int N = ar.length();
        //遍历TypedArray
        for (int i=0; i<N; i++) {
            if (Debug.getGlobalAllocSize() > PRELOAD_GC_THRESHOLD) {
                if (false) {
                    Log.v(TAG, " GC at " + Debug.getGlobalAllocSize());
                }
                System.gc();
                runtime.runFinalizationSync();
                Debug.resetGlobalAllocSize();
            }
            //拿到资源id
            int id = ar.getResourceId(i, 0);
            if (false) {
                Log.v(TAG, "Preloading resource #" + Integer.toHexString(id));
            }
            if (id != 0) {
                //getDrawable流程上面已经介绍过,第二个参数为theme,preload的时候是null
                if (mResources.getDrawable(id, null) == null) {
                    throw new IllegalArgumentException(
                            "Unable to find preloaded drawable resource #0x"
                            + Integer.toHexString(id)
                            + " (" + ar.getString(i) + ")");
                }
            }
        }
        //返回preload的个数
        return N;
    }

        本篇就到这里啦,下一篇介绍AssetManager2

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值