Android插件式换肤 & Resource 源码

64 阅读2分钟

插件式换肤框架搭建思路。

思路:
1、 悄悄从网上下载一套皮肤 apk(zip 文件),把它运行起来,它无主界面。
每次获取皮肤的时候,利用反射;
2、 不需要运行,直接可以获取本地皮肤,前提已经从服务器上下载到本地了。
3、 android Support library 支持白天黑夜主题切换(最简单的方式),切换主题加载不同的资源。皮肤在我们的apk里面。

要解决的问题:
如何去获取另外一个apk中的资源。

换肤方案分析

资源加载 Resource 源码阅读

ImageView 如何加载 android:src="@drawable/logo" 这个文件的?
资源的加载,都是通过 Resource 类去读取的。想去获取另外一个apk中的资源,是否可以自己实例化一个?

// ImageView
Drawable d = a.getDrawable(com.android.internal.R.styleable.ImageView_src);
setImageDrawable(d);
// TypedArray
mResource.loadDrawable(value, value.resourceId, mTheme);

 // Resource 实例化  
 ContextThemeWrapper:: super.getResources();  
 ContextWrapper:: mBase.getResources();  
 Context:: getResources(); // 抽象方法  
 ContextImpl.java:: 实现类  
 \--return mResource;  
 \--Resources resources = packageInfo.getResources(mainThread);  
 LoadedApk.java.java:: getResources(ActivityThread mainThread)  
 \--mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,  
 mApplicationInfo.sharedLibraryFiles, Display.DEFAULT\_DISPLAY, null, this);


ActivityThread.java::

Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] legacyOverlayDirs,
                String[] overlayPaths, String[] libDirs, LoadedApk pkgInfo,
                Configuration overrideConfig) {
    return mResourcesManager.getResources(null, resDir, splitResDirs, legacyOverlayDirs,
            overlayPaths, libDirs, null, overrideConfig, pkgInfo.getCompatibilityInfo(),
            pkgInfo.getClassLoader(), null);
}    

ResourcesManager.java::
String resDir apk安装的路径。

r = new Resources(assets, dm, config, compatInfo); // API 23
// assets是 AssetManager资源管理。
Resource(AssetManager assets, DisplayMetrics metrics, Configuration config)

参数详解:

assets AssetManager 资源管理的创建

AssetManager assets = new AssetManager();
assets.addAssetPath(resDir);     // resDir 就是apk的目录

/**
 * Add an additional set of assets to the asset manager.
 * This can be either a directory or ZIP file. Not for use by applications.
 * Returns the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    synchronized (this) {
        int res = addAssetPathNative(path);
        makestringBlocks(mstringBlocks);
        return res;
    }
}

资源加载的总结:
  1. 所有的资源加载,是通过Resource-> 构建对象是直接new的对象;-> AssetManager 其实是Resource的核心实例。
    ->最终是通过AssetManager 获取资源。
  2. AssetManager 实例化:
AssetManager assets = new AssetManager();
assets.addAssetPath(resDir);  // 这个resDir可以是一个zip的路径。

  1. 自己创建 Resource 的实例,去读取资源。
    原来项目中,添加资源图片 drawable/xhdpi/welcome.jpg
    新建一个项目,SkinPlugin, 里面有资源 drawable/xhdpi/image_src.jpg。生成apk(red.skin),
    将skin拷贝到 手机目录中(根目录中)
// 更换皮肤测试
private void testSkin() {
    ImageView imageIv = findViewById(R.id.iv_image);

    findViewById(R.id.tv_text).setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            try {
                // 读取本地 .skin中的资源。
                Resources superRes = getResources();
                AssetManager asset = AssetManager.class.newInstance();
                // 添加本地已经下载好的皮肤资源
                Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
                method.setAccessible(true);
   String resPath = Environment.getExternalStorageDirectory().getAbsolutePath()
                        + File.separator + "red.skin";
   method.invoke(asset, resPath); // 反射执行方法
   Resources resources = new Resources(asset, superRes.getDisplayMetrics(), superRes.getConfiguration());

                // 获取资源id: 根据名字取拿
                int drawableId = resources.getIdentifier("image_src", "drawable", "com.hc.skinplugin");
                Drawable drawable = resources.getDrawable(drawableId);
                imageIv.setImageDrawable(drawable);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

👀关注公众号:Android老皮!!!欢迎大家来找我探讨交流👀