美文网首页源码Android知识Android开发
插件式换肤框架搭建 - 资源加载源码分析

插件式换肤框架搭建 - 资源加载源码分析

作者: 红橙Darren | 来源:发表于2017-03-08 19:40 被阅读4197次

    1. 概述


    我们终于要开始写插件式换肤框架了,如果一上来就写或者直接从网上去下载别人写好的代码会很坑爹,直接去写你会发现根本没法下手,直接从网上下载你会发现有的时候6.0不行,5.0上面某些自带的效果出不来,出了问题要搞好几天而且完全是蒙的,今天首先来看一下google的源码到底是怎么去加载源码的。
     
    所有分享大纲:2017Android进阶之路与你同行

    视频讲解地址:http://pan.baidu.com/s/1bC3lAQ

    2. 资源加载源码分析


    2.1 我们先来看一下ImageView的scr属性到底是怎么加载图片资源的:

        <ImageView
            android:layout_width="wrap_content"
            android:src="@drawable/app_icon"
            android:layout_height="wrap_content" />
    
        // ImageView.java 解析属性
        final TypedArray a = context.obtainStyledAttributes(
                    attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
        // 通过TypedArray获取图片
        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
        if (d != null) {
          setImageDrawable(d);
        }
    
        // TypedArray.getDrawable() 方法
        public Drawable getDrawable(@StyleableRes int index) {
           // 省略部分代码....
           // 加载资源其实是通过mResources去获取的
           return mResources.loadDrawable(value, value.resourceId, mTheme);
        }
    

    2.2 Resource创建过程分析:
      
       我们在Activity中也经常这样使用context.getResources().getColor(R.id.title_color),那么这个Resources实例是怎么创建的呢?我们可以先从context的实现类ContextImpl入手

    private ContextImpl(ContextImpl container, ActivityThread mainThread,
                LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
                Display display, Configuration overrideConfiguration, int createDisplayWithId) {
           ......
           Resources resources = packageInfo.getResources(mainThread);
    
           if (resources != null) {
           // 不会走此分支,因为6.0中还不支持多屏显示,虽然已经有不少相关代码了,7.0以及正式支持多屏操作了
              if (displayId != Display.DEFAULT_DISPLAY
                || overrideConfiguration != null
                || (compatInfo != null && compatInfo.applicationScale
                        != resources.getCompatibilityInfo().applicationScale)) {
                  ......
                }
           }
           ......
           mResources = resources;
    }
    
    // packageInfo.getResources 方法
    public Resources getResources(ActivityThread mainThread) {
           // 缓存机制,如果LoadedApk中的mResources已经初始化则直接返回,
           // 否则通过ActivityThread创建resources对象
           if (mResources == null) {
               mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                       mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
           }
           return mResources;
    }
    

    最终会来到ResourcesManager的getResources方法

        public @NonNull Resources getResources(@Nullable IBinder activityToken,
                @Nullable String resDir, //app资源文件夹路径,实际上是apk文件的路径,如/data/app/包名/base.apk
                @Nullable String[] splitResDirs, //针对一个app由多个apk组成(将原本一个apk切片为若干apk)时,每个子apk中的资源文件夹
                @Nullable String[] overlayDirs,
                @Nullable String[] libDirs, // app依赖的共享jar/apk路径
                int displayId,
                @Nullable Configuration overrideConfig,
                @NonNull CompatibilityInfo compatInfo,
                @Nullable ClassLoader classLoader) {
            try {
                Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
                // 以apk路径为参数创建key
                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);
            }
        }
    
    
        private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
                @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
            synchronized (this) {
                // .......
                
                if (activityToken != null) {
                    // 根据key从缓存里面找找 ResourcesImpl 
                    ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                    if (resourcesImpl != null) {
                        if (DEBUG) {
                            Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                        }
                        // 如果 resourcesImpl 有 那么根据resourcesImpl 和classLoader 从缓存找找 Resource
                        return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                                resourcesImpl);
                    }
    
                    // We will create the ResourcesImpl object outside of holding this lock.
                } else {
                    // .......
                }
            }
    
            // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
            // 这个比较重要  createResourcesImpl 通过 key
            ResourcesImpl resourcesImpl = createResourcesImpl(key);
    
            synchronized (this) {
                // .......
                final Resources resources;
                if (activityToken != null) {
                    // 根据resourcesImpl和classLoader获取Resources 
                    resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl);
                } else {
                    resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
                }
                return resources;
            }
        }
    
        private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
            final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
            daj.setCompatibilityInfo(key.mCompatInfo);
            // 创建AssetManager 
            final AssetManager assets = createAssetManager(key);
            final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
            final Configuration config = generateConfig(key, dm);
            // 根据AssetManager 创建一个ResourcesImpl 其实找资源是: Resources -> ResourcesImpl -> AssetManager
            final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
            if (DEBUG) {
                Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
            }
            return impl;
        }
        
        @VisibleForTesting
        protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
            // 创建一个AssetManager对象
            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.
            // 将app中的资源路径都加入到AssetManager对象中
            if (key.mResDir != null) {
                // 这个方法很重要,待会我们就是用它去加载皮肤的apk
                if (assets.addAssetPath(key.mResDir) == 0) {
                    throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
                }
            }
    
            if (key.mLibDirs != null) {
                for (final String libDir : key.mLibDirs) {
                    // 仅仅选择共享依赖中的apk,因为jar中不会有资源文件
                    if (libDir.endsWith(".apk")) {
                        // Avoid opening files we know do not have resources,
                        // like code-only .jar files.
                        if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                            Log.w(TAG, "Asset path '" + libDir +
                                    "' does not exist or contains no resources.");
                        }
                    }
                }
            }
            return assets;
        }
    
        /**
         * Gets an existing Resources object if the class loader and ResourcesImpl are the same,
         * otherwise creates a new Resources object.
         */
        private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
                @NonNull ResourcesImpl impl) {
            // Find an existing Resources that has this ResourcesImpl set.
            final int refCount = mResourceReferences.size();
            for (int i = 0; i < refCount; i++) {
                WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
                // 从软引用缓存里面找一找
                Resources resources = weakResourceRef.get();
                if (resources != null &&
                        Objects.equals(resources.getClassLoader(), classLoader) &&
                        resources.getImpl() == impl) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing ref=" + resources);
                    }
                    return resources;
                }
            }
    
            // Create a new Resources reference and use the existing ResourcesImpl object.
            // 创建一个Resources ,Resource有好几个构造方法,每个版本之间有稍微的差别 
            // 有的版本是用的这一个构造方法 Resources(assets, dm, config, compatInfo)
            Resources resources = new Resources(classLoader);
            resources.setImpl(impl);
            // 加入缓存
            mResourceReferences.add(new WeakReference<>(resources));
            if (DEBUG) {
                Slog.d(TAG, "- creating new ref=" + resources);
                Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
            }
            return resources;
        }
    

    【看了这么多我们大致可以总结一下Resources的创建流程了:】

    • packageInfo.getResources(mainThread) -> mainThread.getTopLevelResources() -> mResourcesManager.getResources() -> getOrCreateResources() 这里首先会找ResourcesImpl缓存如果有则会获取Resource缓存返回;
    • 如果没有ResourcesImpl缓存,那么回去创建ResourcesImpl,ResourcesImpl的创建依赖于AssetManager ;
    • AssetManager的创建是通过直接实例化对象调用了一个addAssetPath(path)方法把应用的apk路径添加到AssetManager,addAssetPath()方法请看源码解释。
    • 创建好ResourcesImpl之后会再去缓存中找Resource如果没有,那么则会创建Resource并将其缓存,创建我们看到的源码是new Resources(classLoader),resources.setImpl(impl) 而不同的版本可能是 new Resources(assets, dm, config, compatInfo) 具体请看6.0源码。

    3. 加载皮肤资源


    如果大致知道了资源的加载流程以及Resource的创建过程,现在我们要去加载另外一个apk中的资源就好办了,只需要自己创建一个Resource对象,下面这段代码网上找一大堆,如果分析过源码相信你会有更深的认识:

    public class MainActivity extends AppCompatActivity {
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            try {
                Resources superRes = getResources();
                // 创建AssetManager,但是不能直接new所以只能通过反射
                AssetManager assetManager = AssetManager.class.newInstance();
                // 反射获取addAssetPath方法
                Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
                // 皮肤包的路径:  本地sdcard/plugin.skin
                String skinPath = Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator+"plugin.skin";
                // 反射调用addAssetPath方法
                addAssetPathMethod.invoke(assetManager, skinPath);
                // 创建皮肤的Resources对象
                Resources skinResources = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
                // 通过资源名称,类型,包名获取Id
                int bgId = skinResources.getIdentifier("main_bg","drawable","com.hc.skin");
                Drawable bgDrawable = skinResources.getDrawable(bgId);
                // 设置背景
                findViewById(R.id.activity_main).setBackgroundDrawable(bgDrawable);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    4. AssetManager创建过程分析

    下面的分析希望不要有强迫症,看不懂其实也不打紧因为涉及到JNI。通过前面的分析可知,Android系统中实际对资源的管理是AssetManager类.每个Resources对象都会关联一个AssetManager对象,Resources将对资源的操作大多数委托给了AssetManager。当然有些源码还有一层 ResourcesImpl 刚刚我们也看到了。
      另外还会存在一个native层的AssetManager对象与java层的这个AssetManager对象相对应,而这个native层AssetManager对象在内存的地址存储在java层的AssetManager.mObject中。所以在java层AssetManager的jni方法中可以快速找到它对应的native层的AssetManager对象。

    4.1 AssetManager的init()

         /**
         * Create a new AssetManager containing only the basic system assets.
         * Applications will not generally use this method, instead retrieving the
         * appropriate asset manager with {@link Resources#getAssets}.    Not for
         * use by applications.
         * {@hide}
         */
        public AssetManager() {
            synchronized (this) {
                if (DEBUG_REFS) {
                    mNumRefs = 0;
                    incRefsLocked(this.hashCode());
                }
                init(false);
                if (localLOGV) Log.v(TAG, "New asset manager: " + this);
                ensureSystemAssets();
            }
        }
    
       // ndk的源码路径
       // frameworks/base/core/jni/android_util_AssetManager.cpp
       // frameworks/base/libs/androidfw/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/framework-res.apk  
        // 初始化的时候会去加载系统的framework-res.apk资源
        // 也就是说我们为什么能加载系统的资源如颜色、图片、文字等等
        path.appendPath(kSystemAssets);
    
        return addAssetPath(path, NULL);
    }
    

    4.2 AssetManager的addAssetPath(String path)方法

    bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
    {
        asset_path ap;
    
        // 省略一些校验代码
    
        // 判断是否已经加载过了
        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;
            }
        }
    
        // 检查路径是否有一个androidmanifest . xml
        Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
                kAndroidManifest, Asset::ACCESS_BUFFER, ap);
        if (manifestAsset == NULL) {
            // 如果不包含任何资源
            delete manifestAsset;
            return false;
        }
        delete manifestAsset;
        // 添加 
        mAssetPaths.add(ap);
    
        // 新路径总是补充到最后
        if (cookie) {
            *cookie = static_cast<int32_t>(mAssetPaths.size());
        }
    
        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());
        // 资源包路径不是一个文件夹,那就是一个apk文件了
        if (ap.type != kFileTypeDirectory) {
            // 对于app来说,第一次执行时,肯定为0,因为mResources刚创建,还没对其操作
            // 下面的分支 指挥在参数是系统资源包路径时,才执行,
            // 而且系统资源包路径是首次被解析的
            // 第二次执行appendPathToResTable,nextEntryIdx就不会为0了
            if (nextEntryIdx == 0) {
                // mAssetPaths中存储的第一个资源包路径是系统资源的路径,
                // 即framework-res.apk的路径,它在zygote启动时已经加载了
                // 可以通过mZipSet.getZipResourceTable获得其ResTable对象
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.getZipResourceTable(ap.path);
                // 对于APP来说,肯定不为NULL
                if (sharedRes != NULL) {
                    // 得到系统资源包路径中resources.arsc个数
                    nextEntryIdx = sharedRes->getTableCount();
                }
            }
            // 当参数是mAssetPaths中除第一个以外的其他资源资源包路径,
            // 比如app自己的资源包路径时,走下面的逻辑
            if (sharedRes == NULL) {
                // 检查该资源包是否被其他进程加载了,这与ZipSet数据结构有关,后面在详细介绍
                ass = const_cast<AssetManager*>(this)->
                    mZipSet.getZipResourceTableAsset(ap.path);
                // 对于app自己的资源包来说,一般都会都下面的逻辑
                if (ass == NULL) {
                    ALOGV("loading resource table %s\n", ap.path.string());
                    // 创建Asset对象,就是打开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);
                    }
                }
                // 只有在zygote启动时,才会执行下面的逻辑
                // 为系统资源创建 ResTable,并加入到mZipSet里。
                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对象,并把前面与resources.arsc关联的Asset对象,加入到这个ResTabl中
                    sharedRes = new ResTable();
                    sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
                    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 {
                // 非系统资源包时,将与resources.arsc关联的Asset对象加入到Restable中
                // 此过程会解析resources.arsc文件。
                ALOGV("Parsing resources for %s", ap.path.string());
                mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
            }
            onlyEmptyResources = false;
    
            if (!shared) {
                delete ass;
            }
        } else {
            mResources->addEmpty(nextEntryIdx + 1);
        }
    
        if (idmap != NULL) {
            delete idmap;
        }
        MY_TRACE_END();
    
        return onlyEmptyResources;
    }
    

    大家应该之前了解过这个文件resources.arsc, 如果没了解过可以在网上找篇文章看一下。apk在打包的时候会生成它,我们解压apk就应该能够看到他。这里面基本都是存放的资源的索引,之所以不同的分辨率可以加载不同的图片它可是个大功臣。

    5. 资源的查找过程

    现在我们回到最开始的loadDrawable()方法,drawable资源是有实际资源文件的。这类资源索引的过程大体上分为两个步骤,解析资源ID代表的资源的路径;装载资源文件并缓存。
      drawable是缓存到Resources.mDrawableCache中。加载drawable的时候,要先检查下这个缓存中是否有,有的话,直接返回,就不需要加载了。没有缓存的话,说明还没加载该资源文件,所以要先加载加载之后在缓存到mDrawableCache中。而loadDrawable()方法中又是通过loadDrawableForCookie()来加载drawable的:

        private Drawable loadDrawableForCookie(TypedValue value, int id, Theme theme) {
            // drawable资源项的值是一个字符串,代表文件的路径
            if (value.string == null) {
                throw new NotFoundException("Resource \"" + getResourceName(id) + "\" ("
                        + Integer.toHexString(id) + ") is not a Drawable (color or path): " + value);
            }
    
            final String file = value.string.toString();
    
           .
            final Drawable dr;
    
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
            try {
                if (file.endsWith(".xml")) {
                    final XmlResourceParser rp = loadXmlResourceParser(
                            file, id, value.assetCookie, "drawable");
                    dr = Drawable.createFromXml(this, rp, theme);
                    rp.close();
                } else {
                    // 如果drawable是图片文件的话,打开它
                    // assetCookie-1就是图片所在的资源包路径在native层AssetManager.mAssetPaths数组中的索引
                    // 下面这个方法就是打开这个文件了
                    final InputStream is = mAssets.openNonAsset(
                            value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                    dr = Drawable.createFromResourceStream(this, value, is, file, null);
                    is.close();
                }
            } catch (Exception e) {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
                final NotFoundException rnf = new NotFoundException(
                        "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
                rnf.initCause(e);
                throw rnf;
            }
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    
            return dr;
        }
    

    到这里为止就彻底搞清楚资源的查找与加载过程了:索引+加载+缓存。

    所有分享大纲:2017Android进阶之路与你同行

    视频讲解地址:http://pan.baidu.com/s/1bC3lAQ

    相关文章

      网友评论

      • SharryChoo:呼~第一次看native层的代码看的这么深入😁,也只有辉哥写的文章才能看的这么舒服了,nice~
      • 碎碎想:Android Support Library 23.2也有夜间模式,他的调用简单方便,楼主这两种的区别大吗?
        红橙Darren: @碎碎想 可以说大,而且很大

      本文标题:插件式换肤框架搭建 - 资源加载源码分析

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