看到过一些很多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; } }
运行一下程序吧,感觉有点意思。但实际情况我们还需要做很多规则的统一和定制,这篇遐想篇只是带个观者一个思路,将其拓展是很有意思的事情。
示例相关代码下载如意门传送链接