原創(chuàng)-轉(zhuǎn)載請(qǐng)注明出處
Android動(dòng)態(tài)加載插件資源
最近在看app的換膚功能。簡(jiǎn)單的來說就是動(dòng)態(tài)讀取插件apk中的資源手负,需要進(jìn)行換膚的控件所用到的資源在主apk和插件apk中各維護(hù)了一份宁赤,且資源名稱相同鹃两。
插件聽起來高大上犁苏,但其實(shí)就是一個(gè)apk文件场绿。所以我們所要做的啊犬,就是怎么樣能讓插件中的資源加載進(jìn)本地灼擂,并且讀取到。
Resource的創(chuàng)建
在app內(nèi)部加載資源使用的是context.getResources(),context中g(shù)etResources()方法是一個(gè)抽象方法觉至,具體的實(shí)現(xiàn)在ContextImpl類中剔应。
Resources resources = packageInfo.getResources(mainThread);
參數(shù)packageInfo指向的是一個(gè)LoadedApk對(duì)象,這個(gè)LoadedApk對(duì)象描述的是當(dāng)前正在啟動(dòng)的Activity組所屬的Apk语御。
進(jìn)入到LoadedApk的getResources(mainThread)方法
public Resources getResources(ActivityThread mainThread) {
if (mResources == null) {
mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, this);
}
return mResources;
}
LoadedApk類的成員函數(shù)getResources首先檢查其成員變量mResources的值是否等于null峻贮。如果不等于的話,那么就會(huì)將它所指向的一個(gè)Resources對(duì)象返回給調(diào)用者应闯,否則的話纤控,就會(huì)調(diào)用參數(shù)mainThread的成員函數(shù)getTopLevelResources來獲得這個(gè)Resources對(duì)象,然后再返回給調(diào)用者碉纺。 mainThread指向一個(gè)ActivityThread對(duì)象船万。
public final class ActivityThread {
......
final HashMap<ResourcesKey, WeakReference<Resources> > mActiveResources
= new HashMap<ResourcesKey, WeakReference<Resources> >();
Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
Resources r;
synchronized (mPackages) {
......
WeakReference<Resources> wr = mActiveResources.get(key);
r = wr != null ? wr.get() : null;
......
if (r != null && r.getAssets().isUpToDate()) {
......
return r;
}
}
......
AssetManager assets = new AssetManager();
if (assets.addAssetPath(resDir) == 0) {
return null;
}
......
r = new Resources(assets, metrics, getConfiguration(), compInfo);
......
synchronized (mPackages) {
WeakReference<Resources> wr = mActiveResources.get(key);
Resources existing = wr != null ? wr.get() : null;
if (existing != null && existing.getAssets().isUpToDate()) {
// Someone else already created the resources while we were
// unlocked; go ahead and use theirs.
r.getAssets().close();
return existing;
}
// XXX need to remove entries when weak references go away
mActiveResources.put(key, new WeakReference<Resources>(r));
return r;
}
}
}
在其中創(chuàng)建了AssertManager對(duì)象,assets.addAssetPath(resDir)這句話的意思是把資源目錄里的資源都加載到AssetManager對(duì)象中 如果我們把一個(gè)未安裝的apk的路徑傳給這個(gè)方法骨田,那么apk中的資源就被加載到AssetManager對(duì)象里面了唬涧。但它是一個(gè)隱藏方法,需要反射調(diào)用盛撑。有了AssertManager對(duì)象就能創(chuàng)建Resources對(duì)象了。
AssetManager介紹
Provides access to an application's raw asset files; see Resources for the way most applications will want to retrieve their resource data. This class presents a lower-level API that allows you to open and read raw files that have been bundled with the application as a simple stream of bytes.
AssetManager提供了應(yīng)用的原始資源捧搞,通過它可以讓應(yīng)用程序檢索他們的資源數(shù)據(jù)抵卫。
在ResourcesImpl類中存在AssetManager的引用mAsset.舉個(gè)例子看下Resources怎么通過AssetManager加載數(shù)據(jù).看下Resources的getString()方法
public String getString(@StringRes int id) throws NotFoundException {
return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
Resources將資源id傳給了AssetManager的getResourceText方法狮荔。從AssetManager中返回了資源數(shù)據(jù)。有興趣大家可以深入研究一下介粘,這里不做過多介紹殖氏。 接下來我們寫一個(gè)小demo看看更換皮膚包的簡(jiǎn)單實(shí)現(xiàn)。
Demo
首先如果只加載本地皮膚包(帶有皮膚資源的apk)的時(shí)候姻采,我們將皮膚包放入assets文件夾內(nèi)雅采,再在初始化的時(shí)候加載進(jìn)sdcard中。如果要下載皮膚包慨亲,則直接下載進(jìn)sdcard指定目錄中婚瓜。我們現(xiàn)在只做本地皮膚包的更換。
先進(jìn)行初始化的操作:
//先定義全局的名稱和存儲(chǔ)路徑
private static final String APK_NAME = "sample.apk";
private static final String APK_DIR = Environment.
getExternalStorageDirectory() + File.separator + APK_NAME;
public void init(Context context) {
File pluginFile = new File(APK_DIR);
if (pluginFile.exists()) {
pluginFile.delete();
}
InputStream is = null;
FileOutputStream fos = null;
try {
is = context.getAssets().open(APK_NAME);
fos = new FileOutputStream(APK_DIR);
int bytes;
byte[] byteArr = new byte[1024 * 4];
while ((bytes = is.read(byteArr, 0, 1024 * 4)) != -1) {
fos.write(byteArr, 0, bytes);
}
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(APK_DIR, PackageManager.GET_ACTIVITIES);
mSkinPackageName = mInfo.packageName;
AssetManager assetManager = AssetManager.class.newInstance();
Method method = assetManager.getClass().getMethod("addAssetPath", String.class);
method.invoke(assetManager, pluginFile.getAbsolutePath());
mSuperResources = context.getResources();
mResources = new Resources(assetManager, mSuperResources.getDisplayMetrics(),
mSuperResources.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
讀取assets下的sample.apk刑棵,將其放進(jìn)sdcard中巴刻。通過反射創(chuàng)建AssetManager,并調(diào)用addAssetPath方法,將apk的路徑傳入AssetManager中蛉签。再new 一個(gè)Resources對(duì)象胡陪,傳入上步生成的AssetManager對(duì)象,這時(shí)就拿到了皮膚包apk的Resources對(duì)象碍舍。初始化完成柠座。
接下來在布局中放入一個(gè)TextView,動(dòng)態(tài)替換TextView控件用到的資源。
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="20sp"
android:layout_centerHorizontal="true"
android:textColor="@color/colorPrimaryDark"
android:background="@mipmap/ic_launcher"
/>
代碼實(shí)現(xiàn)
btnLoad = (Button) findViewById(R.id.btn_load);
tvName = (TextView) findViewById(R.id.tv_name);
btnLoad.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tvName.setBackground(ResourceManager.getInstance()
.loadMipmapResource(R.mipmap.ic_launcher));
tvName.setText(ResourceManager.getInstance()
.loadStringResource(R.string.app_name));
tvName.setTextColor(ResourceManager.getInstance()
.loadColorResource(R.color.colorPrimaryDark));
}
});
ok,這時(shí)候我們看下ResourceManager.getInstance().loadMipmapResource(R.mipmap.ic_launcher)的實(shí)現(xiàn)片橡;
public Drawable loadMipmapResource(int resId){
return mResources.getDrawable(findTrueResId(resId,"mipmap"));
}
/**
* 找到插件app中rescource的真正id
* @param resId 主app中的資源id
*/
private int findTrueResId(int resId,String defType){
String entryName = mSuperResources.getResourceEntryName(resId);
Log.e(TAG, "entryName " + entryName);
String resourceName = mSuperResources.getResourceName(resId);
Log.e(TAG, "resourceName " + resourceName);
int trueResId = mResources.getIdentifier(entryName, defType,mSkinPackageName);
Log.e(TAG, "trueResId " + trueResId);
return trueResId;
}
上面的代碼,mSuperResources是當(dāng)前apk的Resources對(duì)象妈经,通過getResourceEntryName(resId),拿到resId對(duì)應(yīng)的名稱.
還有一個(gè)方法锻全,getResourceName狂塘,這個(gè)和getResourceEntryName的區(qū)別在于,getResourceName拿到的全名包括包名鳄厌,getResourceEntryName拿到的是簡(jiǎn)短名稱.這里我們使用getResourceEntryName方法荞胡。
調(diào)用皮膚包的Resources對(duì)象的getIdentifier方法,會(huì)返回資源在皮膚包中的真實(shí)id了嚎,將真實(shí)id拿到后泪漂,就可以調(diào)用getDrawable(trueId)來加載資源了。來看下打印出來的日志歪泳。
xyz.ibat.pluginsloader E/DONG: entryName ic_launcher
xyz.ibat.pluginsloader E/DONG: resourceName xyz.ibat.pluginsloader:mipmap/ic_launcher
xyz.ibat.pluginsloader E/DONG: trueResId 2130903047
加載string和color和上述方法原理相同萝勤。我們來看下最終效果。
換膚前:
換膚后:
喜歡的話就點(diǎn)個(gè)贊吧呐伞,每個(gè)贊都是我前進(jìn)的動(dòng)力~
參考資料
Android應(yīng)用程序資源管理器(Asset Manager)的創(chuàng)建過程分析
Android源碼分析-資源加載機(jī)制