• Android插件式换肤以及资源加载流程分析


    前言

    APP更换皮肤的方式有很多,如系统自带的黑夜模式、插件换肤、通过下发配置文件加载不同主题等等,我们这里就浅谈下插件换肤方式。想实现插件换肤功能,我们就需要先弄清楚 :APP是如何完成资源加载的。

    资源加载流程

    这里我们以ImageView加载图片来进行分析,我们先看下ImageView获取drawable的源码:

       public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
                int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
    
            initImageView();
    		...
            final Drawable d = a.getDrawable(R.styleable.ImageView_src);
            if (d != null) {
                setImageDrawable(d);
            }
    	...
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    重点在a.getDrawable(R.styleable.ImageView_src)这段代码,我们继续跟进:
    TypedArray.getDrawable()

        public Drawable getDrawable(@StyleableRes int index) {
            return getDrawableForDensity(index, 0);
        }
    
    • 1
    • 2
    • 3

    TypedArray.getDrawableForDensity()

        public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
         		...
                return mResources.loadDrawable(value, value.resourceId, density, mTheme);
            }
            return null;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    Resources.loadDrawable()

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

    ResourcesImpl.loadDrawable()

     Drawable loadDrawable(@NonNull Resources wrapper, @NonNull TypedValue value, int id,
                int density, @Nullable Resources.Theme theme)
                throws NotFoundException {
                ...
       			//如果使用缓存,先从缓存中取cachedDrawable
                if (!mPreloading && useCache) {
                    final Drawable cachedDrawable = caches.getInstance(key, wrapper, theme);
                    if (cachedDrawable != null) {
              		  cachedDrawable.setChangingConfigurations(value.changingConfigurations);
                        return cachedDrawable;
                    }
                }
        			...
        			// 重点就是loadDrawableForCookie方法
                    dr = loadDrawableForCookie(wrapper, value, id, density);
                    ...
                }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    从上面我们可以看到,资源加载通过Resources这个类,而它又将任务交给它的实现类ResourcesImpl,我们重点分析下ResourcesImpl.loadDrawableForCookie方法:

      private Drawable loadDrawableForCookie(@NonNull Resources wrapper, @NonNull TypedValue value,
                int id, int density) {
    		...
            final String file = value.string.toString();
    		...
                try {
                	//加载xml资源,如drawable下定义的shape.xml文件
                    if (file.endsWith(".xml")) {
                        final String typeName = getResourceTypeName(id);
                        if (typeName != null && typeName.equals("color")) {
                            dr = loadColorOrXmlDrawable(wrapper, value, id, density, file);
                        } else {
                            dr = loadXmlDrawable(wrapper, value, id, density, file);
                        }
                    } else {
                    	//通过mAssets(AssetManager类型)打开资源文件流实现加载
                        final InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
                        final AssetInputStream ais = (AssetInputStream) is;
                        dr = decodeImageDrawable(ais, wrapper, value);
                    }
                } finally {
                    stack.pop();
                }
            } catch (Exception | StackOverflowError 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;
               ...
                }
            }
    
            return dr;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    这里我们可以看到最终是交给AssetManager来进行资源文件访问,读取数据流完成资源加载

    通过上面源码分析,我们知道可以通过Resources来实现资源加载,那系统中Resources又是如何创建的呢?

    Resources创建流程分析

    我们在代码中经常这样使用:context.getResources().getDrawable(),那我们就从context的实现类ContextImpl抓起:

    ### ContextImpl
    
        public Context createApplicationContext(ApplicationInfo application, int flags)
               throws NameNotFoundException {
       		//  找到createResources方法
               c.setResources(createResources(mToken, pi, null, displayId, null,
                       getDisplayAdjustments(displayId).getCompatibilityInfo(), null));
               if (c.mResources != null) {
                   return c;
               }
           }
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    createResources方法跟进

        private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
               int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo,
               List<ResourcesLoader> resourcesLoader) {
           final String[] splitResDirs;
           final ClassLoader classLoader;
           try {
               splitResDirs = pi.getSplitPaths(splitName);
               classLoader = pi.getSplitClassLoader(splitName);
           } catch (NameNotFoundException e) {
               throw new RuntimeException(e);
           }
           return ResourcesManager.getInstance().getResources(activityToken,
                   pi.getResDir(),
                   splitResDirs,
                   pi.getOverlayDirs(),
                   pi.getApplicationInfo().sharedLibraryFiles,
                   displayId,
                   overrideConfig,
                   compatInfo,
                   classLoader,
                   resourcesLoader);
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    ResourcesManager的getResources方法:

        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,
                @Nullable List<ResourcesLoader> loaders) {
            try {
               	...
                return createResources(activityToken, key, classLoader);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    createResources方法如下:

        private @Nullable Resources createResources(@Nullable IBinder activityToken,
                @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
            synchronized (this) {
            	...
            	// Resources的创建需要resourcesImpl
                ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
                if (resourcesImpl == null) {
                    return null;
                }
                if (activityToken != null) {
                    return createResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                } else {
                    return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    createResourcesLocked方法如下:

        private @NonNull Resources createResourcesLocked(@NonNull ClassLoader classLoader,
                @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
            cleanupReferences(mResourceReferences, mResourcesReferencesQueue);
            //系统源码中其实就是通过classLoader直接new了一个Resources,并初始化了resourcesImpl方便后续资源加载
            Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
                    : new Resources(classLoader);
            resources.setImpl(impl);
            resources.setCallbacks(mUpdateCallbacks);
            mResourceReferences.add(new WeakReference<>(resources, mResourcesReferencesQueue));
            if (DEBUG) {
                Slog.d(TAG, "- creating new ref=" + resources);
                Slog.d(TAG, "- setting ref=" + resources + " with impl=" + impl);
            }
            return resources;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    通过上面源码分析,我们可以得出结论:在ApplicationContext创建的时候,就完成了Resources的创建,创建是通过ResourcesManager来完成的。

    那我们是不是就可以通过创建新的Resources来实现插件中资源的访问呢!!

    插件换肤案例

    我们先看下Resources的构造方法:

        @Deprecated
        public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
            this(null);
            mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
        }
    
        /**
         * @hide
         */
        @UnsupportedAppUsage
        public Resources(@Nullable ClassLoader classLoader) {
            mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
        }
    
        /**
         * Only for creating the System resources.
         */
        @UnsupportedAppUsage
        private Resources() {
            this(null);
    
            final DisplayMetrics metrics = new DisplayMetrics();
            metrics.setToDefaults();
    
            final Configuration config = new Configuration();
            config.setToDefaults();
    
            mResourcesImpl = new ResourcesImpl(AssetManager.getSystem(), metrics, config,
                    new DisplayAdjustments());
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    这里有三个构造方法,由于我们需要加载插件中的资源文件,通过上面的分析,我们知道资源访问是需要通过AssetManager来完成的,因此我们使用Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)这个方式来完成插件资源加载:

     private lateinit var iv: ImageView
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
            iv = findViewById<ImageView>(R.id.iv)
            iv.setImageDrawable(getDrawable(R.drawable.b))
            findViewById<Button>(R.id.btn).setOnClickListener {
                //更新皮肤
                updateSkin()
            }
    
        }
    
        private fun updateSkin() {
            //反射调用AssetManager的addAssetPath方法
            val assetMangerClazz = AssetManager::class.java
            val assetManger = assetMangerClazz.newInstance()
            //皮肤存放在当前包路径下
            val skinPath = filesDir.path + File.separator + "skin.skin"
            val method = assetMangerClazz.getDeclaredMethod("addAssetPath", String::class.java)
            method.isAccessible = true
            method.invoke(assetManger, skinPath)
            //创建皮肤的Resources对象
            val skinResources = Resources(assetManger, resources.displayMetrics, resources.configuration)
            //通过资源名称,类型,包获取Id
            val skinId = skinResources.getIdentifier("a", "drawable", "com.crystal.skin")
            val skinDrawable = skinResources.getDrawable(skinId, null)
            iv.setImageDrawable(skinDrawable)
    
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    测试效果:
    请添加图片描述

    总结

    通过源码分析,了解了资源加载的基本流程,对插件换肤的实现有了进一步的认知。

    参考文档

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

    结语

    如果以上文章对您有一点点帮助,希望您不要吝啬的点个赞加个关注,您每一次小小的举动都是我坚持写作的不懈动力!ღ( ´・ᴗ・` )

  • 相关阅读:
    具有计算功能的模拟信号平均值采集隔离放大器
    nodejs 内置模块fs 常用api
    Dread Hunger恐惧饥荒服务器语言与阴谋的狼人杀对决
    力扣 6181. 最长的字母序连续子字符串的长度
    css的预处理
    maven基础学习
    Go语言map底层分析
    机器学习分类模型评价指标之混淆矩阵
    file_put_contents锁的问题
    2024华数杯数学建模竞赛选题建议+初步分析
  • 原文地址:https://blog.csdn.net/a734474820/article/details/127960595