美文网首页
10.源码阅读(插件式换肤-安卓Resources加载资源的过程

10.源码阅读(插件式换肤-安卓Resources加载资源的过程

作者: 任振铭 | 来源:发表于2018-04-04 19:44 被阅读350次

    我们知道,每一个View的子类都可以设置backgroud,那么这个背景是如何加载出来的呢?

    找到View的构造方法

    public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        ......
        case com.android.internal.R.styleable.View_background:
                        background = a.getDrawable(attr);
                        break;
        ......
    }
    
    @Nullable
        public Drawable getDrawable(@StyleableRes int index) {
            return getDrawableForDensity(index, 0);
        }
        @Nullable
        public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
             ......
             return mResources.loadDrawable(value, value.resourceId, density, mTheme);
             ......
        }
    

    看到这一行

    return mResources.loadDrawable(value, value.resourceId, density, mTheme);
    

    进入Resources中

    @NonNull
        Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
                throws NotFoundException {
            return mResourcesImpl.loadDrawable(this, value, id, density, theme);
        }
    

    可以看到,背景最终是被Resources中的ResourcesImpl加载得到Drawable的,ResourcesImpl在Resources构造中创建出来

    @Deprecated
        public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
            this(null);
            mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
        }
    
        private Resources() {
            ......
            mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                    new DisplayAdjustments());
        }
    

    我们再来看Resources是如何创建的,平常我们获取一些资源文件的时候,会这样获取Resources

    context.getResources()
    

    我们知道Context是抽象类,所以直接到Context的子类ContextImpl中去找

    @Override
        public Resources getResources() {
            return mResources;
        }
    

    那么这个mResources是什么时候创建的,找到这个方法

    void setResources(Resources r) {
            if (r instanceof CompatResources) {
                ((CompatResources) r).setContext(this);
            }
            mResources = r;
        }
    

    然后看到很多地方调用了这个方法

    
    c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                        getDisplayAdjustments(displayId).getCompatibilityInfo()));
    
    c.setResources(createResources(mActivityToken, pi, null, displayId, null,
                        getDisplayAdjustments(displayId).getCompatibilityInfo()));
    
    context.setResources(packageInfo.getResources());
     
    context.setResources(ResourcesManager.getInstance().getResources(
                    mActivityToken,
                    mPackageInfo.getResDir(),
                    paths,
                    mPackageInfo.getOverlayDirs(),
                    mPackageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    null,
                    mPackageInfo.getCompatibilityInfo(),
                    classLoader));
    
    context.setResources(resourcesManager.createBaseActivityResources(activityToken,
                    packageInfo.getResDir(),
                    splitDirs,
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    classLoader));
    
    

    这样的方法有好几个,而且我们找不到其他地方给mResources赋值的地方,初步可以判断,Resources就是这样创建的,顺着这几个方法去看,你会发现,尽管setResources方法有好几种形式,但最后都会进入到ResourcesManger这个类中,这个方法的注释可以看看,Resources是会被缓存的,一个Resources的生命周期和这个Activity同等,当classloader改变,Resources也会改变

    /**
         * Gets or creates a new Resources object associated with the IBinder token. References returned
         * by this method live as long as the Activity, meaning they can be cached and used by the
         * Activity even after a configuration change. If any other parameter is changed
         * (resDir, splitResDirs, overrideConfig) for a given Activity, the same Resources object
         * is updated and handed back to the caller. However, changing the class loader will result in a
         * new Resources object.
         * <p/>
         * If activityToken is null, a cached Resources object will be returned if it matches the
         * input parameters. Otherwise a new Resources object that satisfies these parameters is
         * returned.
         *
         * @param activityToken Represents an Activity. If null, global resources are assumed.
         * @param resDir The base resource path. Can be null (only framework resources will be loaded).
         * @param splitResDirs An array of split resource paths. Can be null.
         * @param overlayDirs An array of overlay paths. Can be null.
         * @param libDirs An array of resource library paths. Can be null.
         * @param displayId The ID of the display for which to create the resources.
         * @param overrideConfig The configuration to apply on top of the base configuration. Can be
         * null. Mostly used with Activities that are in multi-window which may override width and
         * height properties from the base config.
         * @param compatInfo The compatibility settings to use. Cannot be null. A default to use is
         * {@link CompatibilityInfo#DEFAULT_COMPATIBILITY_INFO}.
         * @param classLoader The class loader to use when inflating Resources. If null, the
         * {@link ClassLoader#getSystemClassLoader()} is used.
         * @return a Resources object from which to access resources.
         */
    public @Nullable Resources getResources(@Nullable IBinder activityToken,
                @Nullable String resDir,
                @Nullable String[] splitResDirs,
                @Nullable String[] overlayDirs,
                @Nullable String[] libDirs,
                int displayId,
                @Nullable Configuration overrideConfig,
                @NonNull CompatibilityInfo compatInfo,
                @Nullable ClassLoader classLoader) {
            try {
                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
                final ResourcesKey key = new ResourcesKey(
                        resDir,
                        splitResDirs,
                        overlayDirs,
                        libDirs,
                        displayId,
                        overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                        compatInfo);
                classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
                return getOrCreateResources(activityToken, key, classLoader);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    
    /**
         * Gets an existing Resources object set with a ResourcesImpl object matching the given key,
         * or creates one if it doesn't exist.
         *
         * @param activityToken The Activity this Resources object should be associated with.
         * @param key The key describing the parameters of the ResourcesImpl object.
         * @param classLoader The classloader to use for the Resources object.
         *                    If null, {@link ClassLoader#getSystemClassLoader()} is used.
         * @return A Resources object that gets updated when
         *         {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
         *         is called.
         */
        private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
                @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
            synchronized (this) {
    
                ......
                //下边是分两种情况,当activityToken(IBinder)是否为null的情况下根据key获取ResourcesImpl,
                //只要这个ResourcesImpl存在,就会直接得到Resources缓存返回或者新创建一个Resources返回
                if (activityToken != null) {
                    ......
                    //根据key获取与之对应的ResourcesImpl缓存
                    ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                    if (resourcesImpl != null) {
                        ......
                        // 只要根据key获取到的ResourcesImpl不为null,就根据这个ResourcesImpl去获取缓存的
                        //Resources,如果又这个Resources缓存,就返回,没有就创建,具体看这个方法
                        return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                                resourcesImpl, key.mCompatInfo);
                    }
    
                    // We will create the ResourcesImpl object outside of holding this lock.
    
                } else {
                    ......
                    ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                    if (resourcesImpl != null) {
                        ......
                        return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                    }
                }
            
            //当程序走到这里的时候,说明ResourcesImpl没有找到,Resources也就没有得到,那么这里就是根据
            //key创建出一个ResourcesImpl来,程序第一次运行的时候肯定会首先走到这里,所以,上边的代码可以
            //不用太重点的去看,接下来我们看看ResourcesImpl是如何被创建出来的,见方法createResourcesImpl
            // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
            if (resourcesImpl == null) {
                return null;
            }
    
            synchronized (this) {
                ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
                if (existingResourcesImpl != null) {
                    //从缓存中获取
                    ......
                    resourcesImpl.getAssets().close();
                    resourcesImpl = existingResourcesImpl;
                } else {
                    // 将创建的ResourcesImpl缓存起来
                    mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
                }
    
                final Resources resources;
                //在此针对activityToken是否为null分别处理,在getOrCreateResourcesForActivityLocked和getOrCreateResourcesLocked
                //这两个方法中,我们重点关注,Resources不存在缓存的情况,所以,最终会看到Resourses的创建,
                //见下边的方法
                if (activityToken != null) {
                    resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                } else {
                    resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }
                return resources;
            }
        
            //Resources的创建,这里看到根据条件的不同有两种方式获取,一个是new CompatResources,一个是
            //new Resources,进入到CompatResources类中,我们看到这个构造最终也会调用Resources的一个构造
            //方法public Resources(@Nullable ClassLoader classLoader) 返回Resources,可见这个Resources是new
            //出来的
            Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                    : new Resources(classLoader);
            //给Resources设置ResourcesImpl
            resources.setImpl(impl);
            //加入缓存
            mResourceReferences.add(new WeakReference<>(resources));
    

    getOrCreateResourcesForActivityLocked

    /**
         * Gets an existing Resources object tied to this Activity, or creates one if it doesn't exist
         * or the class loader is different.
         */
        private @NonNull Resources getOrCreateResourcesForActivityLocked(@NonNull IBinder activityToken,
                @NonNull ClassLoader classLoader, @NonNull ResourcesImpl impl,
                @NonNull CompatibilityInfo compatInfo) {
            ......
                //有缓存获取缓存
                Resources resources = weakResourceRef.get();
    
                ......
                    return resources;
                ......
            //没有缓存创建出来然后加入缓存
            Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                    : new Resources(classLoader);
            resources.setImpl(impl);
            activityResources.activityResources.add(new WeakReference<>(resources));
            ......
            return resources;
        }
    

    createResourcesImpl,可以看到ResourcesImpl的创建依赖于这几个对象AssetManager,DisplayMetrics,Configuration,DisplayAdjustments

    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
            final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
            daj.setCompatibilityInfo(key.mCompatInfo);
    
            final AssetManager assets = createAssetManager(key);
            if (assets == null) {
                return null;
            }
    
            final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
            final Configuration config = generateConfig(key, dm);
            final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
            ......
            return impl;
        }
    

    着重看AssetManager的创建,这个类很关键

    /**
         * Creates an AssetManager from the paths within the ResourcesKey.
         *
         * This can be overridden in tests so as to avoid creating a real AssetManager with
         * real APK paths.
         * @param key The key containing the resource paths to add to the AssetManager.
         * @return a new AssetManager.
        */
        @VisibleForTesting
        protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
            AssetManager assets = new AssetManager();
    
            // resDir can be null if the 'android' package is creating a new Resources object.
            // This is fine, since each AssetManager automatically loads the 'android' package
            // already.
            if (key.mResDir != null) {
                // 将app中的资源路径都加入到AssetManager对象中,下边的方法都可以不看,我们重点
                //关注这个方法,可以说,应用之所以能加载资源,就是通过AssetManager以及调用addAssetPath对他设置的
                //资源路径
                if (assets.addAssetPath(key.mResDir) == 0) {
                    Log.e(TAG, "failed to add asset path " + key.mResDir);
                    return null;
                }
            }
    
           .....
            return assets;
        }
    

    getDisplayMetrics,也只是new了一个DisplayMetrics

    @VisibleForTesting
        protected @NonNull DisplayMetrics getDisplayMetrics(int displayId, DisplayAdjustments da) {
            DisplayMetrics dm = new DisplayMetrics();
            final Display display = getAdjustedDisplay(displayId, da);
            if (display != null) {
                display.getMetrics(dm);
            } else {
                dm.setToDefaults();
            }
            return dm;
        }
    

    generateConfig,Configuration也是new出来的

    private Configuration generateConfig(@NonNull ResourcesKey key, @NonNull DisplayMetrics dm) {
            Configuration config;
           ....
                config = new Configuration(getConfiguration());
           ....
            return config;
        }
    

    Resources的创建中有一个个关键的类,就是ResourcesImpl,这个类的创建需要几个重要的信息,其中之一就是AssetManager,通过直接实例话一个AssetManager对象并给这个对象设置资源路径,这是#Resources可以获取到文件资源的基础,DisplayMetrics或者Configuration则相当于一些固定设置,AssetManager中设置的这个路径,其实就是我们将要设置的apk的路径,从这个apk中获取资源

    如此一来,我们获取到了Resources,就可以自由的去获取资源文件了

    那么我们再回到最初,看看Resources是如何loadDrawable的

    Resources中

    @NonNull
        Drawable loadDrawable(@NonNull TypedValue value, int id, int density, @Nullable Theme theme)
                throws NotFoundException {
            return mResourcesImpl.loadDrawable(this, value, id, density, theme);
        }
    

    ResourcesImpl中

    @Nullable
        Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                int density, @Nullable Resources.Theme theme)
                throws NotFoundException {
            ......
    
                Drawable dr;
                boolean needsNewDrawableAfterCache = false;
                if (cs != null) {
                    dr = cs.newDrawable(wrapper);
                } else if (isColorDrawable) {
                    dr = new ColorDrawable(value.data);
                } else {
                    dr = loadDrawableForCookie(wrapper, value, id, density, null);
                }
                ......
    
                return dr;
            ......
    
    /**
         * Loads a drawable from XML or resources stream.
         */
        private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
                int id, int density, @Nullable Resources.Theme theme) {
           
            
            ......
    
            final Drawable dr;
    
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
            try {
                //如果资源是xml文件
                if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXmlForDensity(wrapper, rp, density, theme);
                    rp.close();
                } else {
                    //如果资源是图片资源,打开它得到流,然后解析得到drawable
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
                    is.close();
                }
            ......
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    
            return dr;
        }
    

    经过上面的分析,皮肤切换的思路已经有了,下载apk文件,这个文件中包含有另一个皮肤的各种资源文件,通过Resources去加载这个apk中的资源,达到换肤的效果,关键代码如下

                //点击从手机中一个apk中获取图片资源并且设置给ImageView显示
                //获取系统的两个参数
                Resources superResources = getResources();
                //创建assetManger(无法直接new因为被hide了,所以用反射)
                AssetManager assetManager = AssetManager.class.newInstance();
                //添加资源目录(addAssetPath也是一样被hide无法直接调用)
                Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                method.setAccessible(true);//如果是私有的,添上防止万一某一天他变成了私有的
                method.invoke(assetManager,Environment.getExternalStorageDirectory().getAbsolutePath()+File.separator+"red.skin");//注意你资源的名字要一致  
                //DisplayMetrics和Configuration的对象可以直接new出来,这里使用的是从getResources得到的Resources中获取,其实也是new出来的
                Resources resources = new Resources(assetManager,superResources.getDisplayMetrics(),superResources.getConfiguration());
                //用创建好的Resources获取资源(注意着三个参数,第一个是要获取资源的名字,我们设置的是girl,不要忘了,第二个参数代表这个资源在哪个文件夹中,第三个参数表示要获取资源的apk的包名,缺一不可)
                int identifier = resources.getIdentifier("girl", "drawable", "com.example.myapplication");
                if (identifier  != 0){
                    Drawable drawable = resources.getDrawable(identifier);
                    mImage.setImageDrawable(drawable);
                }
    

    下面是native端AssetManager初始化的过程,为什么我们的app可以调用系统提供好的资源,以及这些资源是如何加载的,可以在这里得到答案

    AssetManager的init()

    public AssetManager() {
            synchronized (this) {
                if (DEBUG_REFS) {
                    mNumRefs = 0;
                    incRefsLocked(this.hashCode());
                }
                init(false);
                if (localLOGV) Log.v(TAG, "New asset manager: " + this);
                ensureSystemAssets();
            }
        }
    //android_util_AssetManager.cpp
    //AssetManager.cpp
    private native final void init(boolean isSystem);
    
    static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem) { 
        if (isSystem) { 
            verifySystemIdmaps();
         } 
        // AssetManager.cpp 
        AssetManager* am = new AssetManager(); 
        if (am == NULL) {
             jniThrowException(env, "java/lang/OutOfMemoryError", ""); 
            return; 
        } 
        am->addDefaultAssets();
         ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
         env->SetLongField(clazz, gAssetManagerOffsets.mObject,reinterpret_cast<jlong>(am));
     }
    
    
    bool AssetManager::addDefaultAssets() { 
        const char* root = getenv("ANDROID_ROOT");       
        LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set"); 
        String8 path(root); 
        // 初始化的时候加载系统的framework-res.apk    
        path.appendPath(kSystemAssets); 
        return addAssetPath(path, NULL); 
    }
    
    

    AssetManager的addAssetPath(String path)方法

    bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
    {
        AutoMutex _l(mLock);
    
        asset_path ap;
    
        String8 realPath(path);
        if (kAppZipName) {
            realPath.appendPath(kAppZipName);
        }
        ap.type = ::getFileType(realPath.string());
        if (ap.type == kFileTypeRegular) {
            ap.path = realPath;
        } else {
            ap.path = path;
            ap.type = ::getFileType(path.string());
            if (ap.type != kFileTypeDirectory && ap.type != kFileTypeRegular) {
                ALOGW("Asset path %s is neither a directory nor file (type=%d).",
                     path.string(), (int)ap.type);
                return false;
            }
        }
    
        // Skip if we have it already.
        for (size_t i=0; i<mAssetPaths.size(); i++) {
            if (mAssetPaths[i].path == ap.path) {
                if (cookie) {
                    *cookie = static_cast<int32_t>(i+1);
                }
                return true;
            }
        }
    
        ALOGV("In %p Asset %s path: %s", this,
             ap.type == kFileTypeDirectory ? "dir" : "zip", ap.path.string());
    
        // Check that the path has an AndroidManifest.xml
        Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
                kAndroidManifest, Asset::ACCESS_BUFFER, ap);
        if (manifestAsset == NULL) {
            // This asset path does not contain any resources.
            delete manifestAsset;
            return false;
        }
        delete manifestAsset;
    
        mAssetPaths.add(ap);
    
        // new paths are always added at the end
        if (cookie) {
            *cookie = static_cast<int32_t>(mAssetPaths.size());
        }
    
    #ifdef HAVE_ANDROID_OS
        // Load overlays, if any
        asset_path oap;
        for (size_t idx = 0; mZipSet.getOverlay(ap.path, idx, &oap); idx++) {
            mAssetPaths.add(oap);
        }
    #endif
    
        if (mResources != NULL) {
            appendPathToResTable(ap);
        }
    
        return true;
    }
    bool AssetManager::appendPathToResTable(const asset_path& ap) const {
        // skip those ap's that correspond to system overlays
        if (ap.isSystemOverlay) {
            return true;
        }
    
        Asset* ass = NULL;
        ResTable* sharedRes = NULL;
        bool shared = true;
        bool onlyEmptyResources = true;
        MY_TRACE_BEGIN(ap.path.string());
        Asset* idmap = openIdmapLocked(ap);
        size_t nextEntryIdx = mResources->getTableCount();
        ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
        if (ap.type != kFileTypeDirectory) {
            if (nextEntryIdx == 0) {
                // The first item is typically the framework resources,
                // which we want to avoid parsing every time.
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.getZipResourceTable(ap.path);
                if (sharedRes != NULL) {
                    // skip ahead the number of system overlay packages preloaded
                    nextEntryIdx = sharedRes->getTableCount();
                }
            }
            if (sharedRes == NULL) {
                ass = const_cast<AssetManager*>(this)->
                    mZipSet.getZipResourceTableAsset(ap.path);
                if (ass == NULL) {
                    ALOGV("loading resource table %s\n", ap.path.string());
                    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);
                    }
                }
                
                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());
                    sharedRes = new ResTable();
                    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");
                    addSystemOverlays(overlaysListPath.string(), ap.path, sharedRes, nextEntryIdx);
    #endif
                    sharedRes = const_cast<AssetManager*>(this)->
                        mZipSet.setZipResourceTable(ap.path, sharedRes);
                }
            }
        } else {
            ALOGV("loading resource table %s\n", ap.path.string());
            ass = const_cast<AssetManager*>(this)->
                openNonAssetInPathLocked("resources.arsc",
                                         Asset::ACCESS_BUFFER,
                                         ap);
            shared = false;
        }
    
        if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
            ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
            if (sharedRes != NULL) {
                ALOGV("Copying existing resources for %s", ap.path.string());
                mResources->add(sharedRes);
            } else {
                ALOGV("Parsing resources for %s", ap.path.string());
                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;
    }
    

    相关文章

      网友评论

          本文标题:10.源码阅读(插件式换肤-安卓Resources加载资源的过程

          本文链接:https://www.haomeiwen.com/subject/qlclhftx.html