Android動態資源載入原理和應用
阿新 • • 發佈:2018-12-29
動態載入資源原理
通常我們呼叫getResources()方法獲取資原始檔
public Resources getResources() {
return mResources;
}
mResources是在建立ContextImp物件後的init方法裡面建立的
mResources = mPackageInfo.getResources(mainThread);
呼叫了LoadedApk的getResources方法又呼叫到了ActivityThread類的getTopLevelResources方法public Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, Display.DEFAULT_DISPLAY, null, this); } return mResources; }
ResourcesKey使用resDir和其他引數來構造,這裡主要是resDir引數,表明資原始檔所在的路徑。也就是APK程式所在路徑。Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compInfo) { ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable); Resources r; synchronized (mPackages) { // ... WeakReference<Resources> wr = mActiveResources.get(key); r = wr != null ? wr.get() : null; if (r != null && r.getAssets().isUpToDate()) { if (false) { Slog.w(TAG, "Returning cached resources " + r + " " + resDir + ": appScale=" + r.getCompatibilityInfo().applicationScale); } return r; } } AssetManager assets = new AssetManager(); assets.setThemeSupport(compInfo.isThemeable); if (assets.addAssetPath(resDir) == 0) { return null; } // ... r = new Resources(assets, dm, config, compInfo); if (false) { Slog.i(TAG, "Created app resources " + resDir + " " + r + ": " + r.getConfiguration() + " appScale=" + r.getCompatibilityInfo().applicationScale); } 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; } }
ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable);
上面程式碼的主要邏輯是獲取Resources物件,從一個Map變數mActiveResources獲取,這個Map維護了ResourcesKey和WeakReference<Resources>的對應關係。如果不存在就建立它,並且新增到Map中。因此只要這個Map中包含多個指向不同資源路徑的Resources物件或者說我們有指向不同路徑的資源的Resources物件,就可以訪問多個路徑的資源,即有實現訪問其他APK檔案中的資源的可能。
建立Resources物件的主要邏輯為
AssetManager assets = new AssetManager();
assets.setThemeSupport(compInfo.isThemeable);
if (assets.addAssetPath(resDir) == 0) {
return null;
}
r = new Resources(assets, dm, config, compInfo);
首先建立AssetManager物件,然後用其建立Resources物件。我們以前使用getAssets方法讀取assets資料夾中的檔案,其實他就是在這裡建立的。
AssetManager的建構函式:
public AssetManager() {
synchronized (this) {
if (DEBUG_REFS) {
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
init();
if (localLOGV) Log.v(TAG, "New asset manager: " + this);
ensureSystemAssets();
}
}
init()函式也是一個native函式,其native程式碼在android_util_AssetManager.cpp中
static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
{
AssetManager* am = new AssetManager();
if (am == NULL) {
jniThrowException(env, "java/lang/OutOfMemoryError", "");
return;
}
// 將Framework的資原始檔新增到AssertManager物件的路徑中。
am->addDefaultAssets();
ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
}
bool AssetManager::addDefaultAssets()
{
// /system
const char* root = getenv("ANDROID_ROOT");
LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
String8 path(root);
// kSystemAssets定義為static const char* kSystemAssets = "framework/framework-res.apk";
// 因此,path為/system/framework/framework-res.apk,framework對應的資原始檔
path.appendPath(kSystemAssets);
return addAssetPath(path, NULL);
}
到此為止,在建立AssetManager的時候完成了新增framework資源,然後新增本應用的資源路徑,即呼叫addAssetPath方法
/**
* Add an additional set of assets to the asset manager. This can be
* either a directory or ZIP file. Not for use by applications. Returns
* the cookie of the added asset, or 0 on failure.
* {@hide}
*/
public native final int addAssetPath(String path);
也是一個native方法,其native程式碼在android_util_AssetManager.cpp中
static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz, jstring path)
{
ScopedUtfChars path8(env, path);
if (path8.c_str() == NULL) {
return 0;
}
AssetManager* am = assetManagerForJavaObject(env, clazz);
if (am == NULL) {
return 0;
}
void* cookie;
// 在native程式碼中完成新增資源路徑的工作
bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
return (res) ? (jint)cookie : 0;
}
可以看到,Resources物件的內部AssetManager物件包含了framework的資源還包含了應用程式本身的資源,因此這也就是為什麼能使用getResources函式獲得的resources物件來訪問系統資源和本應用資源的原因。
受此過程的提醒,我們是不是可以自己建立一個Resources物件,讓它的包含我們指定路徑的資源,就可以實現訪問其他的資源了呢?答案是肯定的,利用這個思想可以實現資源的動態載入,換膚、換主題等功能都可以利用這種方法實現。
於是,主要思想就是建立一個AssetManager物件,利用addAssetPath函式新增指定的路徑,用其建立一個Resources物件,使用該Resources物件獲取該路徑下的資源。
需要注意的是addAssetPath函式是hide的,可以使用反射呼叫。
public void loadRes(String path){
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
} catch (Exception e) {
}
resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
// 也可以根據資源獲取主題
}
這裡的引數path就是APK檔案的路徑,可以通過以下方式獲取
getPackageManager().getApplicationInfo("xxx", 0).sourceDir;
並且還可以重寫Context的getResources方法,getAsset方法,提高程式碼的一致性。
@Override
public Resources getResources() {
return resources == null ? super.getResources() : resources;
}
@Override
public AssetManager getAssets() {
return assetManager == null ? super.getAssets() : assetManager;
}
於是在載入了資源之後就可以通過該Resources物件獲取對應路徑下面的資源了。
動態載入資源
兩種不同風格的按鈕,預設的是本應用提供的資源,還有一種作為另一個單獨的外掛APK程式存放在手機的其他路徑中,當選擇不同的風格時載入不同的圖片資源。
外掛APK僅僅包含了一些資原始檔。
宿主程式的程式碼具體如下
private AssetManager assetManager;
private Resources resources;
private RadioGroup rg;
private ImageView iv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
rg = (RadioGroup) findViewById(R.id.rg);
rg.setOnCheckedChangeListener(new OnCheckedChangeListener() {
@Override
public void onCheckedChanged(RadioGroup group, int checkedId) {
switch (checkedId) {
case R.id.default_skin:
assetManager = null;
resources = null;
iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));
break;
case R.id.skin1:
String dexPath = "";
try {
dexPath = getPackageManager().getApplicationInfo("com.example.plugin", 0).sourceDir;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
loadRes(dexPath);
// 由於重寫了getResources方法,因此這時返回的是我們自己維護的Resources物件,因此可以訪問到他的編號id的資源
iv.setImageDrawable(getResources().getDrawable(0x7f020000));
break;
}
}
});
}
public void loadRes(String path){
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
} catch (Exception e) {
}
resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());
}
@Override
public Resources getResources() {
return resources == null ? super.getResources() : resources;
}
@Override
public AssetManager getAssets() {
return assetManager == null ? super.getAssets() : assetManager;
}
可以查到,外掛APK中的額ic_launcher圖片的id為0x7f020000,於是可以通過該id值獲取到對應的資源public static final int ic_launcher=0x7f020000;
當然這樣的耦合性太高了,可以用來說明原理,但看起來不是很直觀,因為這個id只有查看了外掛APK的程式碼才知道,因此可以讓外掛APK提供返回這個id的函式,由宿主APK來呼叫,具體可以通過反射也可以通過介面。
外掛APK提供getImageId函式獲取圖片資源的id
public class Plugin {
public static int getImageId() {
return R.drawable.ic_launcher;
}
}
這樣在載入完資源後,可以呼叫以下方法來獲取該圖片資源
private void setImage(String dexPath) {
DexClassLoader loader = new DexClassLoader(dexPath, getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
try {
Class<?> clazz = loader.loadClass("com.example.plugin.Plugin");
Method getImageId = clazz.getMethod("getImageId");
int ic_launcher = (int) getImageId.invoke(clazz);
iv.setImageDrawable(getResources().getDrawable(ic_launcher));
} catch (Exception e) {
e.printStackTrace();
}
}
外掛管理的一種方式
對於每個外掛,在AndroidManifest.xml中宣告一個空的Activity,並新增他的action,比如:
<activity
android:name=".plugin" >
<intent-filter>
<action android:name="android.intent.plugin" />
</intent-filter>
</activity>
這樣在宿主程式中就可以查到對應的外掛,以供選擇載入。PackageManager pm = getPackageManager();
List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);
ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;
dexPaths.add(activityInfo.applicationInfo.sourceDir);
效果:
private AssetManager assetManager;
private Resources resources;
private LinearLayout ll;
private ImageView iv;
private Button btn;
private List<String> dexPaths = new ArrayList<String>();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
iv = (ImageView) findViewById(R.id.iv);
ll = (LinearLayout) findViewById(R.id.ll);
btn = (Button) findViewById(R.id.btn);
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
resources = null;
iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));
}
});
Intent intent = new Intent("android.intent.plugin");
PackageManager pm = getPackageManager();
final List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);
for (int i = 0; i < resolveinfos.size(); i++) {
final ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;
dexPaths.add(activityInfo.applicationInfo.sourceDir);
// 根據查詢到的外掛數新增按鈕
final Button btn = new Button(this);
btn.setText("風格" +(i+1));
btn.setTag(i);
ll.addView(btn, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));
btn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int index = (Integer)btn.getTag();
String dexPath = dexPaths.get(index);
loadRes(dexPath);
setImage(resolveinfos.get(index).activityInfo);
}
});
}
}
private void setImage(ActivityInfo activityInfo) {
DexClassLoader loader = new DexClassLoader(activityInfo.applicationInfo.sourceDir, getApplicationInfo().dataDir, null, this.getClass().getClassLoader());
try {
Class<?> clazz = loader.loadClass(activityInfo.packageName + ".Plugin");
Method getImageId = clazz.getMethod("getImageId");
int ic_launcher = (int) getImageId.invoke(clazz);
iv.setImageDrawable(getResources().getDrawable(ic_launcher));
} catch (Exception e) {
e.printStackTrace();
}
}
public void loadRes(String path) {
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath",
String.class);
addAssetPath.invoke(assetManager, path);
} catch (Exception e) {
e.printStackTrace();
}
resources = new Resources(assetManager, super.getResources()
.getDisplayMetrics(), super.getResources().getConfiguration());
}
@Override
public Resources getResources() {
return resources == null ? super.getResources() : resources;
}
@Override
public AssetManager getAssets() {
return assetManager == null ? super.getAssets() : assetManager;
}
兩個外掛程式:
com.example.plugin
|-- Plugin.java
com.example.plugin2
|-- Plugin.java
Plugin類的內容一樣,為提供給宿主程式反射呼叫的類
註冊空的activity
<activity
android:name=".plugin"
android:label="@string/name" >
<intent-filter>
<action android:name="android.intent.plugin" />
</intent-filter>
</activity>
程式碼點此下載