看到過一些很多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下
運行一下程序吧,感覺有點意思。但實際情況我們還需要做很多規則的統一和定製,這篇遐想篇只是帶個觀者一個思路,將其拓展是很有意思的事情。