APK插件加载资源实现

前言

插件化编程现在非常的火热,通常用来解决65536问题,插件通常被做成不同的apk模块,每个模块专门负责某种业务逻辑,主APK通过调用动态加载插件里的代码和资源实现宿主和插件的交互。为了了解插件APK文件如何使用,这里通过读取APK插件的资源来实现换肤功能。

实现效果

这里写图片描述

生成资源插件

只要在Android Studio中创建一个新的项目,项目里不需要有任何的Activity组件,再把需要的图片资源和颜色资源都定义一下,最后在命令行执行gradle assemble会发现在build/outputs/apk里找到了app-debug.apk文件,它就是包含资源的apk插件包。可以通过adb push方法将这个插件包推到Android手机的/sdcard/目录下,方便主应用APK能够找到插件文件。

// 在插件资源包里定义的颜色资源
<color name="content_text_color">#00ff00</color>
<color name="menu_text_color">#ff00ff</color>

C:\Users>adb push appres.apk /sdcard/
appres.apk: 1 file pushed. 4.1 MB/s (1720401 bytes in 0.399s)

换肤实现

前面已经将插件资源放到了/sdcard/目录下,为了能够读取到外部文件对于Android6.0之后的系统需要申请外部存储读取权限,关于动态申请权限的代码逻辑这里就不再赘述,现在开始定义简单的界面布局。布局分为SlideMenu部分和内容部分,两者都有一个背景ImageView和一个展示文案的TextView,这里需要对背景图和文案的颜色做修改,默认情况下使用的是主APK中的资源,如果用户要求切换皮肤就会从插件APK中读取资源并且设置到背景和文案上。

<android.support.v4.widget.DrawerLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".DrawerActivity">

    <FrameLayout
        android:id="@+id/main_container"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/content"
            android:src="@drawable/skin_content_bg"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <TextView
            android:id="@+id/content_text"
            android:textColor="@color/skin_red"
            android:layout_gravity="center"
            android:textSize="50sp"
            android:text="@string/app_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </FrameLayout>

    <FrameLayout
        android:id="@+id/slideMenu"
        android:layout_gravity="left"
        android:layout_width="250dp"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/menu"
            android:src="@drawable/skin_menu_bg"
            android:scaleType="centerCrop"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <TextView
            android:id="@+id/menu_text"
            android:textColor="@color/skin_orange"
            android:layout_gravity="center"
            android:textSize="50sp"
            android:text="@string/app_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </FrameLayout>
</android.support.v4.widget.DrawerLayout>

用户可以通过选项菜单来切换不同的皮肤,其中ResourceManager对象保存了默认的资源和插件APK中的资源,用户点击修改会使用插件中的资源,点击还原会自动使用原始的资源。

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    switch (id) {
        case R.id.action_change_skin:
            ResourceManager.ResourceEntity entity = ResourceManager.getInstance()
            .getResource(ResourceManager.APK_RESOURCE);
            initViews(entity);
            return true;
        case R.id.action_reset_skin:
            ResourceManager.ResourceEntity defEntity = ResourceManager.getInstance()
            .getResource(ResourceManager.DEFAULT_RESOURCE);
            initViews(defEntity);
            return true;
    }
    return super.onOptionsItemSelected(item);
}

// 将资源设置到背景和文案视图对象上
private void initViews(ResourceManager.ResourceEntity entity) {
    menuBg.setImageDrawable(entity.menuBg);
    contentBg.setImageDrawable(entity.contentBg);
    menuText.setTextColor(entity.menuColor);
    contentText.setTextColor(entity.contentColor);
}

其中的ResourceEntity就是用来存储资源的数据类,这里主要来看ResourceManager.getInstance().getResource(resourceType)根据用户指定的资源类型加载资源对象的实现。

public class ResourceManager {
    // 资源类型
    public static final String DEFAULT_RESOURCE = "default_resource";
    public static final String APK_RESOURCE = "apk_resource";

    // 资源对象缓存
    private Map<String, ResourceEntity> mCacheMap = new HashMap<>();

    private ResourceManager() {
        // 初始化默认的资源,使用的是主APK内定义的资源
        ResourceEntity defaultEntity = new ResourceEntity();
        Resources resources = Application.getContext().getResources();
        defaultEntity.contentBg = resources.getDrawable(R.drawable.skin_content_bg);
        defaultEntity.menuBg = resources.getDrawable(R.drawable.skin_menu_bg);
        defaultEntity.contentColor = resources.getColor(R.color.skin_red);
        defaultEntity.menuColor = resources.getColor(R.color.skin_orange);
        mCacheMap.put(DEFAULT_RESOURCE, defaultEntity);
    }

    private static class ResourceManagerHolder {
        private static final ResourceManager INSTANCE = new ResourceManager();
    }

    public static ResourceManager getInstance() {
        return ResourceManagerHolder.INSTANCE;
    }

    public void add(String key, ResourceEntity entity) {
        mCacheMap.put(key, entity);
    }

    public ResourceEntity getResource(String key) {
        ResourceEntity entity = mCacheMap.get(key);
        // 如果没有对应的资源对象
        if (entity == null) {
            // 如果请求的是插件资源类型
            if (APK_RESOURCE.equals(key)) {
                // 加载插件资源类型
                entity = loadResources();
                mCacheMap.put(key, entity);
                return entity;
            } else {
                return mCacheMap.get(DEFAULT_RESOURCE);
            }
        } else {
            return entity;
        }
    }

    /**
     * 从插件APK中获取资源
    */
    private ResourceEntity loadResources() {
        try {
            // 反射生成AssetManager对象
            AssetManager assetManager = AssetManager.class.newInstance();
            // 获取addAssetPath方法
            Method method = AssetManager.class.getMethod("addAssetPath", String.class);
            // 获取插件的路径
            String path = Environment.getExternalStorageDirectory() + File.separator + "appres.apk";
            // 将插件的路径添加到AssetManager里
            method.invoke(assetManager, path);

            Resources originResources = Application.getContext().getResources();
            // 插件APK的packageName名称
            String pkgName = "com.example.apkresource";

            // 生成插件APK对应的Resources对象
            Resources resources = new Resources(assetManager, 
            originResources.getDisplayMetrics(), originResources.getConfiguration());
            // 获取插件中的背景图资源id
            int contentBgId = resources.getIdentifier("content_bg", "drawable", pkgName);
            int menuBgId = resources.getIdentifier("menu_bg", "drawable", pkgName);
            // 获取插件中文案颜色资源id
            int contentColorId = resources.getIdentifier("content_text_color", "color", pkgName);
            int menuColorId = resources.getIdentifier("menu_text_color", "color", pkgName);

            // 从Resources对象中获取插件的资源数据并保存到ResouceEntity中
            ResourceEntity entity = new ResourceEntity();
            entity.menuBg = resources.getDrawable(menuBgId);
            entity.contentBg = resources.getDrawable(contentBgId);
            entity.menuColor = resources.getColor(menuColorId);
            entity.contentColor = resources.getColor(contentColorId);

            return entity;
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static class ResourceEntity {
        public Drawable contentBg;
        public Drawable menuBg;
        public int contentColor;
        public int menuColor;
    }
}

上面的ResourceManager对象使用了单例模式,内部会保存插件资源和默认资源,在插件资源未被初始化的情况下使用AssetManager和Resources两个工具类获取插件内的资源数据,最后通过前面定义的initViews方法将插件资源设置到背景和文案视图上实现换肤功能。

猜你喜欢

转载自blog.csdn.net/xingzhong128/article/details/80342283