Android插件化开发实现动态换肤

来源:互联网 发布:淘宝店铺网红 编辑:程序博客网 时间:2024/05/31 19:10

今晚实在不想coding,于是想着整理点知识点,那么简单整理了下插件化开发实现动态更换皮肤。插件化开发大家应该不陌生或多或少用过或听过,插件化开发在项目业务拓展、模块化等方面有不小优势,当然实现一个完美的插件是有困难的。本文如果存在问题恳请指正!欢迎评论交流哦!
效果图:

这里写图片描述

1、换肤方案分析

  1. res下放多种皮肤的资源文件

  2. 加载插件apk使用其中的皮肤资源

方案一:
优点:容易实现。
缺点:res下放多种皮肤的资源文件,无疑会加大apk文件大小,而且资源文件是写死的,不利于后期拓展,若有其他皮肤需求只能通过版本迭代。
方案二:
优点:不会加大apk包,更容易扩展,有新的皮肤只需下载新的插件包即可,无需更新。(相对于方案一)
缺点:相对于方案一实现较困难,若有更多业务处理需要插件,如使用插件中的四大组件及处理它们的生命周期则是比较麻烦的,需用到动态代理。不过还好有360手机助手团队,在github上开源出来了DroidPlugin插件框架,https://github.com/Qihoo360/DroidPlugin,大家以后用这个就OK啦!

2、换肤实现分析

通过插件化实现更换皮肤,其实做的就是把插件apk中的皮肤资源文件拿到当前apk中使用而已。那么不妨先看下如何获取资源文件;

getResources().getDrawable(R.drawable.ic_launcher);

以上代码通过getResources()获取Resources对象,通过getDrawable(int id)获取Drawable对象,其中参数id R.drawable.ic_launcher是R文件类中的静态内部类drawable类的ic_launcher成员变量的值,如下。

public final class R {    public static final class drawable {        public static final int ic_launcher=0x7f020000;    } }

那么如何加载一个插件apk中的资源文件呢?肯定也是先获取Resources对象,在获取Drawable对象。不过使用getResources()方法显然是不可行的,因为插件是一个apk,要想拿到里面的资源必须先把该apk加载到内存中来,然后利用反射拿到相关类的方法并使用。如果你对反射技术不是很了解可以看下http://blog.csdn.net/magic_jss/article/details/52187726;
基本步骤:

  • 通过DexClassLoader把插件apk加载到内存
  • 利用反射技术获取相关类的方法并使用,构造Resources对象

通过DexClassLoader加载插件apk,只需指定插件路径及创建一个优化目录即可,不在赘述,本文主要结合源码讲解如何通过反射构造Resources对象。

查看getResources()源码:

    //ContextThemeWrapper.java    @Override    public Resources getResources() {        if (mResources != null) {            return mResources;        }        if (mOverrideConfiguration == null) {            mResources = super.getResources();            return mResources;        } else {            Context resc = createConfigurationContext(mOverrideConfiguration);            //通过源码可以看出mResources对象是通过Context的getResources()方法获取的,查看Context源码            mResources = resc.getResources();            return mResources;        }    }

查看Context源码:

    // 抽象类  则通过查看该类的实现类ContextImpl    public abstract class Context {        /** Return a Resources instance for your application's package. */        // 抽象方法        public abstract Resources getResources();    }

查看ContextImpl源码:

    @Override    public Resources getResources() {        return mResources;    }    @Override    public Context createConfigurationContext(Configuration overrideConfiguration) {        if (overrideConfiguration == null) {            throw new IllegalArgumentException("overrideConfiguration must not be null");        }        ContextImpl c = new ContextImpl();        c.init(mPackageInfo, null, mMainThread);        // mResources的获取时机 然后看下ResourcesManager源码        c.mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),                getDisplayId(), overrideConfiguration, mResources.getCompatibilityInfo(),                mActivityToken);        return c;    }

查看ResourcesManager源码:

    /**     * Creates the top level Resources for applications with the given compatibility info.     */    public Resources getTopLevelResources(String resDir, int displayId,            Configuration overrideConfiguration, CompatibilityInfo compatInfo,            IBinder token) {        // ...        Resources r;        AssetManager assets = new AssetManager();        if (assets.addAssetPath(resDir) == 0) {            return null;        }        r = new Resources(assets, dm, config, compatInfo, token);        // ...        return r;    }

通过以上源码可以看出要想获取Resources对象,要先得到AssetManager对象,并执行addAssetPath()方法,那这样的话就可以通过反射进行处理了。想毕到这里,大家不用看下面的代码也能实现相关内容处理了吧?

3、换肤代码实现

由于上面对基本实现原理进行了梳理,下面实现代码则不做过多解读了,有问题可以留言哈。

PluginUtils.java

/** * Created by magic on 2016年8月10日.插件操作工具类 */public class PluginUtils {    /**     * 获取插件Apk的AssetManager对象     *      * @param apk     * @return     */    @SuppressWarnings({ "unchecked", "rawtypes" })    public static AssetManager getPluginAssetManager(File apk) throws Exception {        // 字节码文件对象        Class c = AssetManager.class;        AssetManager assetManager=(AssetManager) c.newInstance();        // 获取addAssetPath方法对象        Method method = c.getDeclaredMethod("addAssetPath", String.class);        method.invoke(assetManager, apk.getAbsolutePath());        return assetManager;    }    /**     * 获取插件Apk的Resources对象     *      * @param assets     * @param metrics     *            系统尺寸和分辨率描述对象     * @param config     *            配置     * @return     */    public static Resources getPluginResources(AssetManager assets,            DisplayMetrics metrics, Configuration config) {        Resources resources = new Resources(assets, metrics, config);        return resources;    }//  public Resources(AssetManager assets, DisplayMetrics metrics,Configuration config) {//      this(assets, metrics, config,CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);//  }}

Activity.java

/** * Created by magic on 2016年8月10日.获取其他Apk的资源文件,实现动态换肤 */public class MainActivity extends Activity implements OnClickListener {    Button btn_daytime, btn_night;    RelativeLayout container;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        initView();    }    private void initView() {        btn_daytime = (Button) findViewById(R.id.btn_daytime);        btn_night = (Button) findViewById(R.id.btn_night);        container = (RelativeLayout) findViewById(R.id.container);        btn_daytime.setOnClickListener(this);        btn_night.setOnClickListener(this);        getResources().getDrawable(R.drawable.ic_launcher);    }    @SuppressWarnings("deprecation")    @SuppressLint("NewApi")    @Override    public void onClick(View arg0) {        switch (arg0.getId()) {        case R.id.btn_daytime:            container.setBackgroundResource(R.drawable.img_day);            break;        case R.id.btn_night:            String skinName = "Test_Plugin_Skin.apk";            String skinPath = this.getCacheDir() + File.separator + skinName;            System.out.println("skinPath:" + skinPath);            String skinPackageName = "com.magic.test_plugin_skin";            File file = new File(skinPath);            if (file.exists()) {                Toast.makeText(this, "skin exists", Toast.LENGTH_SHORT).show();            } else {                // 网络下载皮肤逻辑                try {                    InputStream inputStream = this.getAssets().open(                            "Test_Plugin_Skin.apk");                    BufferedInputStream bis = new BufferedInputStream(                            inputStream);                    BufferedOutputStream bos = new BufferedOutputStream(                            new FileOutputStream(skinPath));                    int len;                    byte[] bs = new byte[1024];                    while ((len = bis.read(bs)) != -1) {                        bos.write(bs, 0, len);                    }                    Toast.makeText(this, "skin download finish",                            Toast.LENGTH_SHORT).show();                    bis.close();                    bos.close();                } catch (IOException e) {                    e.printStackTrace();                }            }            try {                // 获取插件Apk的AssetManager对象                AssetManager assetManager = PluginUtils                        .getPluginAssetManager(file);                // 获取插件Apk的Resources对象                Resources resources = PluginUtils.getPluginResources(                        assetManager, this.getResources().getDisplayMetrics(),                        this.getResources().getConfiguration());                // 类加载器                DexClassLoader dexClassLoader = new DexClassLoader(                        file.getAbsolutePath(), this.getDir(skinName,                                Context.MODE_PRIVATE).getAbsolutePath(), null,                        this.getClassLoader());                // 反射拿到R.drawable类的字节码文件对象                Class<?> c = dexClassLoader.loadClass(skinPackageName                        + ".R$drawable");                Field[] fields = c.getDeclaredFields();                for (Field field : fields) {                    if (field.getName().equals("img_night")) {                        int imgId = field.getInt(R.drawable.class);                        Drawable background = resources.getDrawable(imgId);                        container.setBackgroundDrawable(background);                    }                }            } catch (Exception e) {                e.printStackTrace();            }            break;        }    }}

xml文件

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:id="@+id/container"    android:layout_width="match_parent"    android:layout_height="match_parent" >    <Button        android:id="@+id/btn_daytime"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="白天模式" />    <Button        android:id="@+id/btn_night"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:layout_below="@id/btn_daytime"        android:text="夜间模式" /></RelativeLayout>

整个项目下载地址:http://download.csdn.net/detail/magic_jss/9619510;

END!晚安!

2 0
原创粉丝点击