看到过一些很多app都有换肤场景的功能,多数都是从服务器上下载资源然后再使用的,这就解决了资源可选择使用,减轻apk的资源大小,并能很好的提高用户体验。
在android中如何实现这个功能呢,其实可以利用动态加载实现对资源文件的调用,大概意思就是说利用Dalvikvm 中的classloader来加载我们需要的apk中的“我们需要的某个类”或者某个资源,他和java中反射机制一个道理,在java虚拟机中可以利用classloader 加载class文件,反射出其中的对象和方法。android中可以利用这个逻辑,去尝试使用ClassLoader的子类DexClassloader来调用任何位置的dex或apk.
为此,为了更好的让观者理解,可先阅读代码。先放一下demo效果图:
项目关系图:
1.接口程序,主要是为了统一调用的接口
因为这是一个简单的demo,不做太深入的分析,但能快速的理解其用意。如这个接口中,主要是想实现2个功能:弹出来自插件工程中的弹出框和获取插件中的皮肤资源(这里就假设是一个图片了)
/**
* 创建一个接口:用于更新
* @author jan
*/
public interface SkinChangeInferface {
/**
* 获取当前皮肤名字,以弹出框的形式
* @param context
*/
public void showSkinNameInDialog(Context context);
/**
* 获取皮肤参数相关的类
* @param context
* @return
*/
public MySkinBean getMySkin(Context context);
}
MySkinBean.java - 关于皮肤的实体类
public class MySkinBean implements Parcelable {
private long bgImageId;
private String skinName;
public MySkinBean() {
}
public MySkinBean(Parcel parcel) {
this.bgImageId = parcel.readInt();
this.skinName = parcel.readString();
}
public long getBgImageId() {
return bgImageId;
}
public void setBgImageId(long bgImageId) {
this.bgImageId = bgImageId;
}
public String getSkinName() {
return skinName;
}
public void setSkinName(String skinName) {
this.skinName = skinName;
}
public static final Parcelable.Creator<MySkinBean> CREATOR = new Creator<MySkinBean>() {
@Override
public MySkinBean[] newArray(int size) {
return new MySkinBean[size];
}
@Override
public MySkinBean createFromParcel(Parcel source) {
return new MySkinBean(source);
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int arg1) {
dest.writeLong(bgImageId);
dest.writeString(skinName);
}
}
这个接口工程,我们需要它作为一个library为主项目ChangeSkinDemo的库来使用,而皮肤插件项目也需要这个lib,但是相同的class是不能被andoroid重复加载的!所以我们需将这个接口工程打成jar的形式供插件项目使用。
将打好的skin-pligin.jar加入到skinSky插件工程中去,记住,这里的引用方式应该选择addJar或者addExternal Jars,这么做在插件打包的时候不会集成到apk中,避免重复的系统编译,而主项目可以以addLirary去引用,这么做动态加载的时候不会在不同的dex中用同一个加载器 加载同一个class而引发异常。
好,我们去看看主程序怎么做的,以下是主项目demo结构图:
主要看BaseActivity这个父类,我们在其中实现了如何加载外部dex资源到主程序的Resources中,还有通过DexClassloader来调用插件实现的接口方法。
public class BaseActivity extends Activity {
public static String TAG = BaseActivity.class.getSimpleName();
public static final String SKIN_IMPL_CLASSNAME = "org.jan.skin.impl.SkinImpl";
protected static List<BaseActivity> activityList = new ArrayList<BaseActivity>();
protected Context mContext;
//资源管理类
protected AssetManager mAssetManager;
//我们app的资源类
protected Resources mResources;
protected Theme mTheme;
//类加载器,他与父类的PathClassLoader有一个差别,就是DexClassLoader可以加载指定path的dex、jar、apk
//而PathClassLoader只能加载/data/app中的apk,也就是已经安装到手机中的apk。
protected DexClassLoader mDexClassLoader;
private String relasePath;
/**
* 加载目标apk dex中的资源。
* addAssetPath是一个隐藏的方法,我们可以通过他传入一个apk(zip)来调用其中的资源,
* 然后这里将获取Resources,与主项目的资源整合在一起。
* @param dexPath
*/
protected void loadResources(String dexPath) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod(
"addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),
superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(super.getTheme());
}
protected DexClassLoader getDexClassLoader(String dexPath,String optimizedDirectory ) {
try {
mDexClassLoader = new DexClassLoader(dexPath, optimizedDirectory, null, getClassLoader());
} catch (Exception e) {
Log.e(TAG, "getDexClassLoader调用出错:", e);
return null;
}
return mDexClassLoader;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
activityList.add(this);
mContext = this;
}
@Override
protected void onDestroy() {
activityList.remove(this);
super.onDestroy();
}
/** 以下三个方法请务必实现 */
@Override
public Resources getResources() {
if(mResources!=null){
return mResources;
}
return super.getResources();
}
@Override
public Theme getTheme() {
if(mTheme!=null){
return mTheme;
}
return super.getTheme();
}
@Override
public AssetManager getAssets() {
if(mAssetManager!=null){
return mAssetManager;
}
return super.getAssets();
}
/**
* 在这里,我们通过dexclassloader来调用皮肤插件apk中的方法,反射其中的背景id
* @param apkPath
*/
@SuppressLint("NewApi")
protected void changeSkin(String apkPath){
File apkFile = new File(apkPath);
if(!apkFile.exists()){
Toast.makeText(mContext, "皮肤未下载", Toast.LENGTH_SHORT).show();
return;
}
mDexClassLoader = getDexClassLoader(apkPath, relasePath);
Class skinChangeImpl;
try {
skinChangeImpl = mDexClassLoader.loadClass(SKIN_IMPL_CLASSNAME);
SkinChangeInferface skinChange = (SkinChangeInferface) skinChangeImpl.newInstance();
MySkinBean skin = skinChange.getMySkin(mContext);
for(BaseActivity activity : activityList){
activity.changeBackground(apkPath,(int)skin.getBgImageId());
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}catch (NullPointerException e) {
e.printStackTrace();
}
}
/**
* 这里使用了比较粗暴简单的方式更好布局的背景。
* @param resPath
* @param resId
*/
@SuppressLint("NewApi")
private void changeBackground(String resPath ,int resId) {
loadResources(resPath);
Log.d(TAG, "changeBack --backId==" + resId);
//获取当前view下的根布局来修改背景
View rootView = ((ViewGroup) (getWindow().getDecorView().findViewById(android.R.id.content))).getChildAt(0);
rootView.setBackground(getResources().getDrawable(resId));
}
public String getRelasePath() {
return relasePath;
}
public void setRelasePath(String relasePath) {
this.relasePath = relasePath;
}
}
皮肤选择界面的代码,写的比较简单,就是点击换肤而已。
/**
* 选择皮肤的列表界面
* @author jan
*
*/
public class SkinListActivity extends BaseActivity {
private DexClassLoader mClassLoader;
private ListView mListView;
private String relasePath;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.skin_list);
mListView = (ListView) findViewById(R.id.skin_listview);
//资源释放的路径
relasePath = getDir("dex", MODE_PRIVATE).getAbsolutePath();
setRelasePath(relasePath);
fillListData();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
//这里填充一下数据,模拟了apk的下载好位置在当前apk的安装位置/cache目录
private void fillListData() {
String[] skinArray = { getString(R.string.sky),
getString(R.string.starry_sky) };
ArrayAdapter<String> adapter = new ArrayAdapter<String>(mContext,
android.R.layout.simple_list_item_multiple_choice, skinArray);
mListView.setAdapter(adapter);
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view,
int position, long id) {
String filesDir = getCacheDir().getAbsolutePath();
String apkPath;
if (position == 0) {
apkPath = filesDir + File.separator + "sky.apk";
changeSkin(apkPath);
showDialogFromSkin(apkPath);
} else if (position == 1) {
apkPath = filesDir + File.separator + "Starry.apk";
changeSkin(apkPath);
showDialogFromSkin(apkPath);
}
}
});
}
@SuppressLint("NewApi")
private void showDialogFromSkin(String dexPath) {
try {
Class skinImplCls;
SkinChangeInferface skinInt;
mClassLoader = getDexClassLoader(dexPath, relasePath);
skinImplCls = mClassLoader.loadClass(SKIN_IMPL_CLASSNAME);
skinInt = (SkinChangeInferface) skinImplCls.newInstance();
skinInt.showSkinNameInDialog(mContext);
} catch (Exception e) {
e.printStackTrace();
}
}
}
最后,我们就只要简单的实现插件工程中的那个接口方法即可
下面这段是SkinSky插件项目中的实现,主要为了保证接口调用一致性,这里的插件包名都规定统一了。
public class SkinImpl implements SkinChangeInferface {
@Override
public void showSkinNameInDialog(Context context) {
AlertDialog.Builder builder = new Builder(context);
builder.setMessage("你好,这是来自蓝天皮肤的提示");
builder.setTitle(R.string.app_name);
builder.setNegativeButton("取消", new Dialog.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
});
Dialog dialog = builder.create();
dialog.show();
}
@Override
public MySkinBean getMySkin(Context context) {
MySkinBean skin = new MySkinBean();
skin.setBgImageId(R.drawable.tk_skin);
skin.setSkinName("蓝天皮肤");
return skin;
}
}
然后打包apk,push到安装好的主项目的cache下
运行一下程序吧,感觉有点意思。但实际情况我们还需要做很多规则的统一和定制,这篇遐想篇只是带个观者一个思路,将其拓展是很有意思的事情。