Author:xqforever
Time:2013.5.28
最近从事Android插件相关工作,调研了动态换肤的一些方法。
1、需求:
皮肤插件不需要安装
只包括图像,文字等资源,不含有任何逻辑代码
可动态加载替换升级
加载实现简单,View的加载交给Android系统完成
2、过程:
目前对插件加载,可以查看DexClassLoader用法,获取AssetManager和Resources
DexClassLoader classLoader =new DexClassLoader(apkPath, dexPath, null,
getClassLoader());
AssetManager am = (AssetManager)AssetManager.class.newInstance();
am.getClass().getMethod("addAssetPath",String.class)
.invoke(am,apkPath);
//创建Resources
ResourcessuperRes = getResources();
Resources res= new Resources(am, superRes.getDisplayMetrics(),
superRes.getConfiguration());
通过资源Resources获取文字,图片或者layout信息。
Resources的getResource().getIdentifier(String,category,
packageName);
例如:
int id=resource.getResource().getIdentifier("image", "drawable",
"com.example.test");
Drawble drawable= resource.getResource().getDrawable(id);
可以获取图片资源,类似的颜色资源,字符串资源,布局资源等等,都可以通过此种方式得到。
可以假设,如果在主程序中定义控件,然后将资源从皮肤包中取出来,通过view的set属性方法注入,可以从理论上实现换肤功能。
但这种方式缺点在于代码量繁琐,做了很多Android系统绘制控件的工作,重复造轮子,框架开发也繁琐。
理想的换肤框架,应该是皮肤插件为主程序提供资源,主程序撰写逻辑,而View的填充完全交给Android系统完成。
思路:主程序定义一个theme的xml布局文件,
android:layout_height="match_parent"
android:orientation="vertical" >
在资源包中包含有同样的theme布局文件
例如皮肤1中
android:layout_height="match_parent"
android:orientation="vertical" >
控件的id设置相同,便于换肤框架的实现。
主程序中加载main布局,
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity" >
android:layout_height="wrap_content"
android:orientation="horizontal" >
android:layout_width="fill_parent"
android:layout_height="wrap_content" />
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
main布局通过一个layout,id叫customUi来加载皮肤包的theme布局,实现换肤。
view = (LinearLayout) ((LayoutInflater) mContext
.getSystemService(Context.LAYOUT_INFLATER_SERVICE))
.inflate(
currentSkinResource.getResource().getLayout(
layoutId),null);
这个方法能够加载皮肤包资源的layout布局,当然要通过上文所述方法获取layout的id。
然后将这个View,加入到主界面的layout中,就能实现动态加载。View的绘制完全由Android完成。
思路虽然很简单,但是layout里面带有很多的坑,因为其对资源文件的引用,是在编译过程中写死的,而对于同一个image控件记载图片,由于Android图片缓存机制,也只能通过更换layout方法加载一次,
经过几天的摸索,现有的成果和大家分享下:
Android的资源变量是整个Application唯一的,即Resouces中的mSystem,而Asserts也是唯一的,即Resouces中的mAssets。
可以通过反射机制去对这两个变量进行注入。
即布局文件加载前,保存当前的resource和assets,然后将皮肤包中得到的Resouces和Asserts注入。
SkinResourceskinResource = skinResources.get(skinResIndex);
ResourcescurrentResources = skinResource.getResource();
intlayoutId = currentResources.getIdentifier(layoutXml, "layout",
skinResource.getPackageName());
Resourcesresources = mContext.getResources();
Class>clazz = resources.getClass();
Field[]fs = clazz.getDeclaredFields();
//System.out.println(fs.length);
for(int i = 0; i < fs.length; i++) {
System.out.println(fs[i].getName());
if(fs[i].getName().equals("mSystem")) {
Fieldf = fs[i];
f.setAccessible(true);
Objectval = f.get(resources);
System.out.println("name:"+ f.getName() + "\t value = " + val);
f.set(resources,currentResources);
f.setAccessible(false);
}
if(fs[i].getName().equals("mAssets")) {
Fieldf = fs[i];
f.setAccessible(true);
Objectval = f.get(resources);
System.out.println("name:"+ f.getName() + "\t value = " + val);
f.set(resources,currentResources.getAssets());
f.setAccessible(false);
}
if(fs[i].getName().equals("mDrawableCache")) {
Fieldf = fs[i];
f.setAccessible(true);
LongSparseArray
list= (LongSparseArray
.get(resources);
list.clear();
f.setAccessible(false);
}
SkinResource 是一个自定义的类,里面有Resouces和PackageName,其余代码主要做注入功能,对mDrawableCache清空,这样layout注入时,图片资源也能更新。
注入后,主程序的layout布局就能对其中定义的控件进行引用,
例如:button1 = (Button)skinManager.findViewByID("button1", skin);
最后在布局结束后,需要还原现场,将原来系统的res和assets还原,同样通过注入方式。
3、结果:通过这个框架,可以实现换肤功能。
缺陷如下:系统inflate->资源的索引->资源实际位置->绘制View。我们对res和assets通过反射注入只是做到了资源实际位置的替换,但是索引却依然是主程序生成的,所以要注意皮肤包和主程序的索引的一致性。
layout解析,会涉及layout对资源的引用,而对这个引用的解析,是查询R.java被编译成二进制的文件获取资源地址的。R.java文件即为索引作用,映射到具体的Resouces文件。
由于R.java无法实现注入,是根据编译平台,版本自动生成,所以必须保证皮肤包的R.java文件与主程序的R.java文件相同,或者是对各种图片,字符串等文件的顺序一致。R.java打包一致外,可能还需要编译平台以及编译版本的一致性(此条件未验证),才能保证主程序的引用。
理论解决方案:将所有res中的资源,按照在主程序中加载的顺序,依此添加进皮肤包,因为R.java文件记住的是索引,所以在皮肤文件不改名情况下,可以对文件内容进行更改,这样就能生成皮肤包,且主程序能正确的识别并且换肤。
这种方法扩展性对于版本升级的扩展性差, 对以前版本的皮肤包不能兼容。
另附上插件App为安装时,换肤机制实现。
apk格式
apk之间读取数据的条件是:有相同签名并且AndroidManifest.xml中配置android:sharedUserId有相同的属性值,这样两个apk运行在同一个进程中,就能互相访问数据了。
方法如下:
a)应用程序和皮肤程序的AndroidManifest.xml中配置
例如:android:sharedUserId="com.zj"
b)访问资源的方法
Contextcontext = createPackageContext("com.zj.skin",Context.CONTEXT_IGNORE_SECURITY);
获取到com.zj.skin对应的Context,通过返回的context对象就可以访问到com.zj.skin中的任何资源。
例如:应用apk要获得皮肤apk中的bg.png,可按照上述框架获取资源id,然后通过id获取相应图片资源或者layout布局。
Drawabledrawable = context.getResources().getDrawable(id);
这样就得到了图片的引用,其他xml资源文件的获取方式也是类似的。
获取了皮肤插件的Context后,就解决了索引与资源的对应问题,也就是可以不用通过Res和Assets注入与还原这两部分,然后采用本文描述的换肤框架即可。