超简单的动态化和换肤方案
我们知道插件化技术就是在自己本程序中加载其他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