[Android]關於換膚功能的遐想篇

看到過一些很多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下


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

示例相關代碼下載如意門傳送鏈接


發佈了70 篇原創文章 · 獲贊 36 · 訪問量 22萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章