前言

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);
        }
	...
    }

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

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

TypedArray.getDrawableForDensity()

public Drawable getDrawableForDensity(@StyleableRes int index, int density) {
     		...
            return mResources.loadDrawable(value, value.resourceId, density, mTheme);
        }
        return null;
    }

Resources.loadDrawable()

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

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);
                ...
            }
    }

从上面我们可以看到,资源加载通过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;
    }

这里我们可以看到最终是交给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;
           }
       }
   }

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);
   }

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);
        }
    }

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);
            }
        }
    }

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;
    }

通过上面源码分析,我们可以得出结论:在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());
    }

这里有三个构造方法,由于我们需要加载插件中的资源文件,通过上面的分析,我们知道资源访问是需要通过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)

    }

测试效果:

android 模块中使用资源 android资源加载流程_kotlin

总结

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

参考文档

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

结语

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