利用插件化和注解实现动态换肤

402 阅读4分钟

超简单的动态化和换肤方案

我们知道插件化技术就是在自己本程序中加载其他apk的资源和Activity,为了能访问其他apk中的资源,我们需要拿到该apk中的Resources,通过Resources可以拿到所有资源。

Resources构造器的三个参数,AssetManager,DisplayMetrics,Configuration

后面两个参数是跟手机屏幕适配相关的,咱们程序是运行在一个手机上,自然宿主程序的这两个参数和插件apk的是一样的,关键就是AssetManager了,查看源码可以知道,Resources资源是真正通过AssetManager加载的,查看AssetManager构造器源码发现被谷歌Hide了,没办法只能通过反射了。

AssetManager assetManager = AssetManager.class.newInstance();

当然,new出来的assetManager是没有灵魂的,需要跟插件apk关联起来。没错,就是addAssetPath方法。

很不幸,这个方法也是hide,我们需要再次反射。

Method method = assetManager.getClass().getMethod("addAssetPath", String.class);

然后执行就好了

method.invoke(assetManager, path);//addAssetPath后才能管理path路径下的apk

这样,我们的动态换肤就有了理论基础了,为了换肤,我们需要知道哪些控件需要更改颜色,背景,前景。也就是需要标记它们。

为了收集view们,我决定采用注解的办法。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PluginResourceFirst {

    int text_color() default 0;
    int bg_color() default 0;
    int bg_image() default 0;
    int src_image() default 0;
    int text_str() default 0;

}

定义了注解类后,我们需要知道这个控件需要哪些资源,如背景色,背景图片,文字颜色等,甚至可以设置文本的文字。

@PluginResourceFirst(text_color = R.color.color_text, text_str = R.string.text)
public TextView text;

怎么收集这个注解和变量呢,可以在onCreate方法里注册它。

ThemeChangeUtils.getInstance().register(this);

这个类就是负责收集页面和里面所有注解过的控件的。

既然收集好了,那在下载完插件后,就可以遍历这些控件,然后重新设置插件apk里的资源达到换肤的目的了。

/**
 * 更新主题皮肤
 */
public void updateTheme() {
    Set<?> set = objectMap.keySet();
    Iterator<?> iterator = set.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        List<FieldBean> fields = objectMap.get(obj);

        for (FieldBean fieldBean : fields) {

            if (View.class.isAssignableFrom(fieldBean.field.getType())) {
                Field field = fieldBean.field;
                try {
                    View view = (View) field.get(obj);
                    Class clazz = fieldBean.field.getType();
                    PluginResourceFirst pluginResourceFirst = fieldBean.pluginResourceFirst;
                    int bg_color = pluginResourceFirst.bg_color();
                    int text_color = pluginResourceFirst.text_color();
                    int bg_image = pluginResourceFirst.bg_image();
                    int src_image = pluginResourceFirst.src_image();
                    int text_str = pluginResourceFirst.text_str();

                    System.out.println(String.format("view:%s, bg_color:%s, text_color:%s, bg_image:%s, src_image:%s, text_str:%s",
                            view.getClass().getSimpleName(), bg_color, text_color, bg_image, src_image, text_str));


                    if (View.class.isAssignableFrom(clazz)) {//只要是view,就可以换背景
                        if (bg_color > 0) {
                            view.setBackgroundColor(MyResource.getResource().getColor(bg_color));
                        }

                        if (bg_image > 0) {
                            view.setBackground(MyResource.getResource().getDrawable(bg_image));
                        }
                    }


                    if (TextView.class == clazz) {
                        TextView textView = (TextView) view;
                        if(text_color > 0){
                            textView.setTextColor(MyResource.getResource().getColor(text_color));
                        }

                        if(text_str > 0){
                            textView.setText(MyResource.getResource().getString(text_str));
                        }

                    } else if (ImageView.class == clazz || ImageButton.class == clazz) {
                        ImageView imageView = (ImageView) view;
                        if(src_image > 0){
                            imageView.setImageDrawable(MyResource.getResource().getDrawable(src_image));
                        }
                    } else if (Button.class == clazz) {
                        Button button = (Button) view;
                        if(text_color > 0){
                            button.setTextColor(MyResource.getResource().getColor(text_color));
                        }

                        if(text_str > 0){
                            button.setText(MyResource.getResource().getString(text_str));
                        }
                    }


                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }


    }
}

没错,动态换肤原理就是这么简单!

接下来来看看怎么跳转到插件apk里的activity中去。

我们知道,activity需要注册到manifest文件中去,而插件activity是注册不到宿主apk中的manifest中去的,怎么办呢,可以利用代理activity,我们跳转到代理activity中,而代理activity真实去调用插件apk中的所有生命周期和与activity相关的其他方法。

想要加载插件apk中的activity,我们需要拿到DexClassLoader

//获取当前应用的私有存储路径
File file = context.getDir("dex", Context.MODE_PRIVATE);
//获取path路径下的dex文件的类加载器
dexClassLoader = new DexClassLoader(path, file.getAbsolutePath(), null, context.getClassLoader());

同时我们要跳转到一个activity,首先需要找到他,通过完整包名

//获取包管理器
PackageManager packageManager = context.getPackageManager();
packageInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);

我们需要有一个接口来连接代理activity和插件activity

public interface PluginInterface {
    void onAttach(Activity activity);
    void onCreate(Bundle saveBundle);
    void onStart();
    void onResume();
    void onPause();
    void onStop();
    void onDestory();

    void onSaveInstanceState(Bundle state);
    boolean onTouchEvent(MotionEvent event);
    void onBackPressed();
}

这样,插件activity实现这个接口后,宿主的代理activity就可以通过接口的形式访问插件activity的所有生命周期方法了。

当然,插件activity每次都实现这个接口也太麻烦了,我们用BaseActivity吧。

public class BaseActivity extends Activity implements PluginInterface {
    public Activity that;




    @Override
    public void onAttach(Activity activity) {
        that=activity;
    }

    @Override
    public void setContentView(View view) {
        if(that == null){
            super.setContentView(view);
        }else {
            that.setContentView(view);
        }
    }


    @Override
    public void setContentView(int layoutResID) {
        that.setContentView(layoutResID);
    }


    @Override
    public <T extends View> T findViewById(int id) {
        return that.findViewById(id);
    }

    @Override
    public Intent getIntent() {
        return that.getIntent();
    }


    @Override
    public ClassLoader getClassLoader() {
        return that.getClassLoader();
    }

    @Override
    public LayoutInflater getLayoutInflater() {
        return that.getLayoutInflater();
    }

    @Override
    public void startActivity(Intent intent) {
        Intent inte = new Intent();
        inte.putExtra("name", intent.getComponent().getClassName());
        that.startActivity(inte);
    }

    @Override
    public ApplicationInfo getApplicationInfo() {
        return that.getApplicationInfo();
    }

    @Override
    public Window getWindow() {
        return that.getWindow();
    }

    @Override
    public WindowManager getWindowManager() {
        return that.getWindowManager();
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onCreate(Bundle saveBundle) {
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onStart() {
    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onResume() {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onPause() {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onStop() {

    }

    @SuppressLint("MissingSuperCall")
    @Override
    public void onDestory() {

    }

    @Override
    public void onSaveInstanceState(Bundle state) {

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }


    @Override
    public void onBackPressed() {
    }
}

因为插件apk没有安装,没有上下文,所以我们需要传代理activity到这里来代理实现各个需要上下文的方法。

接下来我们只要在ProxyActivity中调用插件apk生命周期方法就好了,要注意classloader和resources需要用插件apk的。

@Override
protected void onCreate(Bundle savedInstanceState) {

    String activityName = getIntent().getStringExtra("name");

    try {
        Class<?> clazz = PluginManager.getDefault().getDexClassLoader().loadClass(activityName);
        Object obj = clazz.newInstance();

        if(obj instanceof PluginInterface){
            pluginInterface = (PluginInterface)obj;
            pluginInterface.onAttach(this);
            pluginInterface.onCreate(new Bundle());
        }


    } catch (Exception e) {
        e.printStackTrace();
    }
    super.onCreate(savedInstanceState);
}


@Override
public Resources getResources() {
    return PluginManager.getDefault().getResource();
}


@Override
public ClassLoader getClassLoader() {
    return PluginManager.getDefault().getDexClassLoader();
}

原理就是这些了,非常简单,自己撸一遍才能加深理解哦,需要源码的请访问:

https://github.com/lingxiaoming/ApkLoadDemo