Android动态资源加载原理和应用(初级版)

本文详细解析了Android中动态加载资源的原理,介绍了如何利用AssetManager和Resources类实现资源的动态加载,包括换肤和主题更换等功能。通过创建自定义的Resources对象并添加外部资源路径,实现了访问其他APK资源的能力。

转载自:http://blog.youkuaiyun.com/cauchyweierstrass/article/details/51067729

动态加载资源原理

通常我们调用getResources()方法获取资源文件

[java]  view plain  copy
  1. public Resources getResources() {  
  2.     return mResources;  
  3. }  
mResources是在创建ContextImp对象后的init方法里面创建的

[java]  view plain  copy
  1. mResources = mPackageInfo.getResources(mainThread);  
调用了LoadedApk的getResources方法
[java]  view plain  copy
  1. public Resources getResources(ActivityThread mainThread) {  
  2.     if (mResources == null) {  
  3.         mResources = mainThread.getTopLevelResources(mResDir,  
  4.                 Display.DEFAULT_DISPLAY, nullthis);  
  5.     }  
  6.     return mResources;  
  7. }  
又调用到了ActivityThread类的getTopLevelResources方法

[java]  view plain  copy
  1. Resources getTopLevelResources(String resDir, int displayId, Configuration overrideConfiguration, CompatibilityInfo compInfo) {  
  2.     ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable);  
  3.     Resources r;  
  4.     synchronized (mPackages) {  
  5.         // ...  
  6.         WeakReference<Resources> wr = mActiveResources.get(key);  
  7.         r = wr != null ? wr.get() : null;  
  8.         if (r != null && r.getAssets().isUpToDate()) {  
  9.             if (false) {  
  10.                 Slog.w(TAG, "Returning cached resources " + r + " " + resDir  
  11.                         + ": appScale=" + r.getCompatibilityInfo().applicationScale);  
  12.             }  
  13.             return r;  
  14.         }  
  15.     }  
  16.       
  17.     AssetManager assets = new AssetManager();  
  18.     assets.setThemeSupport(compInfo.isThemeable);  
  19.     if (assets.addAssetPath(resDir) == 0) {  
  20.         return null;  
  21.     }  
  22.   
  23.     // ...  
  24.   
  25.     r = new Resources(assets, dm, config, compInfo);  
  26.     if (false) {  
  27.         Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "  
  28.                 + r.getConfiguration() + " appScale="  
  29.                 + r.getCompatibilityInfo().applicationScale);  
  30.     }  
  31.   
  32.     synchronized (mPackages) {  
  33.         WeakReference<Resources> wr = mActiveResources.get(key);  
  34.         Resources existing = wr != null ? wr.get() : null;  
  35.         if (existing != null && existing.getAssets().isUpToDate()) {  
  36.             // Someone else already created the resources while we were  
  37.             // unlocked; go ahead and use theirs.  
  38.             r.getAssets().close();  
  39.             return existing;  
  40.         }  
  41.           
  42.         // XXX need to remove entries when weak references go away  
  43.         mActiveResources.put(key, new WeakReference<Resources>(r));  
  44.         return r;  
  45.     }  
  46. }  
ResourcesKey使用resDir和其他参数来构造,这里主要是resDir参数,表明资源文件所在的路径。也就是APK程序所在路径。

[java]  view plain  copy
  1. ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, compInfo.applicationScale, compInfo.isThemeable);  
上面代码的主要逻辑是获取Resources对象,从一个Map变量mActiveResources获取,这个Map维护了ResourcesKey和WeakReference<Resources>的对应关系。如果不存在就创建它,并且添加到Map中。

因此只要这个Map中包含多个指向不同资源路径的Resources对象或者说我们有指向不同路径的资源的Resources对象,就可以访问多个路径的资源,即有实现访问其他APK文件中的资源的可能。

创建Resources对象的主要逻辑为

[java]  view plain  copy
  1. AssetManager assets = new AssetManager();  
  2. assets.setThemeSupport(compInfo.isThemeable);  
  3.     if (assets.addAssetPath(resDir) == 0) {  
  4.         return null;  
  5. }  
  6.  r = new Resources(assets, dm, config, compInfo);  
首先创建AssetManager对象,然后用其创建Resources对象。我们以前使用getAssets方法读取assets文件夹中的文件,其实他就是在这里创建的。

AssetManager的构造函数:

[java]  view plain  copy
  1. public AssetManager() {  
  2.     synchronized (this) {  
  3.         if (DEBUG_REFS) {  
  4.             mNumRefs = 0;  
  5.             incRefsLocked(this.hashCode());  
  6.         }  
  7.         init();  
  8.         if (localLOGV) Log.v(TAG, "New asset manager: " + this);  
  9.         ensureSystemAssets();  
  10.     }  
  11. }  
init()函数也是一个native函数,其native代码在android_util_AssetManager.cpp中

[cpp]  view plain  copy
  1. static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)  
  2. {  
  3.     AssetManager* am = new AssetManager();  
  4.     if (am == NULL) {  
  5.         jniThrowException(env, "java/lang/OutOfMemoryError""");  
  6.         return;  
  7.     }  
  8.     // 将Framework的资源文件添加到AssertManager对象的路径中。  
  9.     am->addDefaultAssets();  
  10.   
  11.     ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);  
  12.     env->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);  
  13. }  
  14.   
  15. bool AssetManager::addDefaultAssets()  
  16. {  
  17.     // /system  
  18.     const char* root = getenv("ANDROID_ROOT");  
  19.     LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");  
  20.   
  21.     String8 path(root);  
  22.     // kSystemAssets定义为static const char* kSystemAssets = "framework/framework-res.apk";  
  23.     // 因此,path为/system/framework/framework-res.apk,framework对应的资源文件  
  24.     path.appendPath(kSystemAssets);  
  25.   
  26.     return addAssetPath(path, NULL);  
  27. }  
到此为止,在创建AssetManager的时候完成了添加framework资源,然后添加本应用的资源路径,即调用addAssetPath方法

[java]  view plain  copy
  1. /** 
  2.  * Add an additional set of assets to the asset manager.  This can be 
  3.  * either a directory or ZIP file.  Not for use by applications.  Returns 
  4.  * the cookie of the added asset, or 0 on failure. 
  5.  * {@hide} 
  6.  */  
  7. public native final int addAssetPath(String path);  
也是一个native方法,其native代码在android_util_AssetManager.cpp中

[cpp]  view plain  copy
  1. static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz, jstring path)  
  2. {  
  3.     ScopedUtfChars path8(env, path);  
  4.     if (path8.c_str() == NULL) {  
  5.         return 0;  
  6.     }  
  7.     AssetManager* am = assetManagerForJavaObject(env, clazz);  
  8.     if (am == NULL) {  
  9.         return 0;  
  10.     }  
  11.   
  12.     void* cookie;  
  13.     // 在native代码中完成添加资源路径的工作  
  14.     bool res = am->addAssetPath(String8(path8.c_str()), &cookie);  
  15.   
  16.     return (res) ? (jint)cookie : 0;  
  17. }  
可以看到,Resources对象的内部AssetManager对象包含了framework的资源还包含了应用程序本身的资源,因此这也就是为什么能使用getResources函数获得的resources对象来访问系统资源和本应用资源的原因。

受此过程的提醒,我们是不是可以自己创建一个Resources对象,让它的包含我们指定路径的资源,就可以实现访问其他的资源了呢?答案是肯定的,利用这个思想可以实现资源的动态加载,换肤、换主题等功能都可以利用这种方法实现。

于是,主要思想就是创建一个AssetManager对象,利用addAssetPath函数添加指定的路径,用其创建一个Resources对象,使用该Resources对象获取该路径下的资源。

需要注意的是addAssetPath函数是hide的,可以使用反射调用。

[java]  view plain  copy
  1. public void loadRes(String path){  
  2.     try {  
  3.         assetManager = AssetManager.class.newInstance();  
  4.         Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);  
  5.         addAssetPath.invoke(assetManager, path);  
  6.     } catch (Exception e) {  
  7.     }  
  8.     resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());  
  9.     // 也可以根据资源获取主题  
  10. }  
这里的参数path就是APK文件的路径,可以通过以下方式获取

[java]  view plain  copy
  1. getPackageManager().getApplicationInfo("xxx"0).sourceDir;  
并且还可以重写Context的getResources方法,getAsset方法,提高代码的一致性。

[java]  view plain  copy
  1. @Override  
  2. public Resources getResources() {  
  3.     return resources == null ? super.getResources() : resources;  
  4. }  
  5. @Override  
  6. public AssetManager getAssets() {  
  7.     return assetManager == null ? super.getAssets() : assetManager;  
  8. }  
于是在加载了资源之后就可以通过该Resources对象获取对应路径下面的资源了。

动态加载资源

两种不同风格的按钮,默认的是本应用提供的资源,还有一种作为另一个单独的插件APK程序存放在手机的其他路径中,当选择不同的风格时加载不同的图片资源。


插件APK仅仅包含了一些资源文件。

宿主程序的代码具体如下

[java]  view plain  copy
  1. private AssetManager assetManager;  
  2.     private Resources resources;  
  3.     private RadioGroup rg;  
  4.     private ImageView iv;  
  5.   
  6.     @Override  
  7.     protected void onCreate(Bundle savedInstanceState) {  
  8.         super.onCreate(savedInstanceState);  
  9.         setContentView(R.layout.activity_main);  
  10.         iv = (ImageView) findViewById(R.id.iv);  
  11.         rg = (RadioGroup) findViewById(R.id.rg);  
  12.         rg.setOnCheckedChangeListener(new OnCheckedChangeListener() {  
  13.             @Override  
  14.             public void onCheckedChanged(RadioGroup group, int checkedId) {  
  15.                 switch (checkedId) {  
  16.                 case R.id.default_skin:  
  17.                     assetManager = null;  
  18.                     resources = null;  
  19.                     iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));  
  20.                     break;  
  21.                 case R.id.skin1:  
  22.                     String dexPath = "";  
  23.                     try {  
  24.                         dexPath = getPackageManager().getApplicationInfo("com.example.plugin"0).sourceDir;  
  25.                     } catch (NameNotFoundException e) {  
  26.                         e.printStackTrace();  
  27.                     }  
  28.                     loadRes(dexPath);  
  29.                     // 由于重写了getResources方法,因此这时返回的是我们自己维护的Resources对象,因此可以访问到他的编号id的资源  
  30.                     iv.setImageDrawable(getResources().getDrawable(0x7f020000));  
  31.                     break;  
  32.                 }  
  33.             }  
  34.         });  
  35.     }  
  36.       
  37.     public void loadRes(String path){     
  38.         try {  
  39.             assetManager = AssetManager.class.newInstance();  
  40.             Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);  
  41.             addAssetPath.invoke(assetManager, path);  
  42.         } catch (Exception e) {  
  43.         }  
  44.         resources = new Resources(assetManager, super.getResources().getDisplayMetrics(), super.getResources().getConfiguration());  
  45.     }  
  46.       
  47.       
  48.     @Override  
  49.     public Resources getResources() {  
  50.         return resources == null ? super.getResources() : resources;  
  51.     }  
  52.       
  53.     @Override  
  54.     public AssetManager getAssets() {  
  55.         return assetManager == null ? super.getAssets() : assetManager;  
  56.     }  
可以查到,插件APK中的额ic_launcher图片的id为0x7f020000,于是可以通过该id值获取到对应的资源

[java]  view plain  copy
  1. public static final int ic_launcher=0x7f020000;  
当然这样的耦合性太高了,可以用来说明原理,但看起来不是很直观,因为这个id只有查看了插件APK的代码才知道,因此可以让插件APK提供返回这个id的函数,由宿主APK来调用,具体可以通过反射也可以通过接口。

插件APK提供getImageId函数获取图片资源的id

[java]  view plain  copy
  1. public class Plugin {  
  2.     public static int getImageId() {  
  3.         return R.drawable.ic_launcher;  
  4.     }  
  5. }  
这样在加载完资源后,可以调用以下方法来获取该图片资源

[java]  view plain  copy
  1. private void setImage(String dexPath) {  
  2.         DexClassLoader loader = new DexClassLoader(dexPath, getApplicationInfo().dataDir, nullthis.getClass().getClassLoader());  
  3.         try {  
  4.             Class<?> clazz = loader.loadClass("com.example.plugin.Plugin");  
  5.             Method getImageId = clazz.getMethod("getImageId");  
  6.             int ic_launcher = (int) getImageId.invoke(clazz);  
  7.             iv.setImageDrawable(getResources().getDrawable(ic_launcher));  
  8.         } catch (Exception e) {  
  9.             e.printStackTrace();  
  10.         }  
  11.     }  

插件管理的一种方式

对于每个插件,在AndroidManifest.xml中声明一个空的Activity,并添加他的action,比如:

[html]  view plain  copy
  1. <activity  
  2.             android:name=".plugin" >  
  3.             <intent-filter>  
  4.                 <action android:name="android.intent.plugin" />  
  5.             </intent-filter>  
  6.         </activity>  
这样在宿主程序中就可以查到对应的插件,以供选择加载。

[java]  view plain  copy
  1. PackageManager pm = getPackageManager();  
  2. List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);  
  3. ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;  
  4. dexPaths.add(activityInfo.applicationInfo.sourceDir);  
效果:


宿主程序的代码

[java]  view plain  copy
  1. private AssetManager assetManager;  
  2. private Resources resources;  
  3. private LinearLayout ll;  
  4. private ImageView iv;  
  5. private Button btn;  
  6. private List<String> dexPaths = new ArrayList<String>();  
  7.   
  8. @Override  
  9. protected void onCreate(Bundle savedInstanceState) {  
  10.     super.onCreate(savedInstanceState);  
  11.     setContentView(R.layout.activity_main);  
  12.     iv = (ImageView) findViewById(R.id.iv);  
  13.     ll = (LinearLayout) findViewById(R.id.ll);  
  14.     btn = (Button) findViewById(R.id.btn);  
  15.     btn.setOnClickListener(new OnClickListener() {  
  16.         @Override  
  17.         public void onClick(View v) {  
  18.             resources = null;  
  19.             iv.setImageDrawable(getResources().getDrawable(R.drawable.ic_launcher));  
  20.         }  
  21.     });  
  22.       
  23.     Intent intent = new Intent("android.intent.plugin");  
  24.     PackageManager pm = getPackageManager();  
  25.     final List<ResolveInfo> resolveinfos = pm.queryIntentActivities(intent, 0);  
  26.     for (int i = 0; i < resolveinfos.size(); i++) {  
  27.         final ActivityInfo activityInfo = resolveinfos.get(i).activityInfo;  
  28.         dexPaths.add(activityInfo.applicationInfo.sourceDir);  
  29.         // 根据查询到的插件数添加按钮  
  30.         final Button btn = new Button(this);  
  31.         btn.setText("风格" +(i+1));  
  32.         btn.setTag(i);  
  33.         ll.addView(btn, new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT));  
  34.         btn.setOnClickListener(new OnClickListener() {  
  35.               
  36.             @Override  
  37.             public void onClick(View v) {  
  38.                 int index = (Integer)btn.getTag();  
  39.                 String dexPath = dexPaths.get(index);  
  40.                 loadRes(dexPath);  
  41.                 setImage(resolveinfos.get(index).activityInfo);  
  42.                   
  43.             }  
  44.         });  
  45.     }  
  46. }  
  47.   
  48. private void setImage(ActivityInfo activityInfo) {  
  49.     DexClassLoader loader = new DexClassLoader(activityInfo.applicationInfo.sourceDir, getApplicationInfo().dataDir, nullthis.getClass().getClassLoader());  
  50.         try {  
  51.             Class<?> clazz = loader.loadClass(activityInfo.packageName + ".Plugin");  
  52.             Method getImageId = clazz.getMethod("getImageId");  
  53.             int ic_launcher = (int) getImageId.invoke(clazz);  
  54.             iv.setImageDrawable(getResources().getDrawable(ic_launcher));  
  55.         } catch (Exception e) {  
  56.             e.printStackTrace();  
  57.         }  
  58. }  
  59.   
  60. public void loadRes(String path) {  
  61.         try {  
  62.             assetManager = AssetManager.class.newInstance();  
  63.             Method addAssetPath = AssetManager.class.getMethod("addAssetPath",  
  64.                     String.class);  
  65.             addAssetPath.invoke(assetManager, path);  
  66.         } catch (Exception e) {  
  67.             e.printStackTrace();  
  68.         }   
  69.     resources = new Resources(assetManager, super.getResources()  
  70.             .getDisplayMetrics(), super.getResources().getConfiguration());  
  71. }  
  72.   
  73. @Override  
  74. public Resources getResources() {  
  75.     return resources == null ? super.getResources() : resources;  
  76. }  
  77.   
  78. @Override  
  79. public AssetManager getAssets() {  
  80.     return assetManager == null ? super.getAssets() : assetManager;  
  81. }  
两个插件程序:

com.example.plugin

    |-- Plugin.java

com.example.plugin2

    |-- Plugin.java

Plugin类的内容一样,为提供给宿主程序反射调用的类

注册空的activity

[java]  view plain  copy
  1. <activity  
  2.       android:name=".plugin"  
  3.       android:label="@string/name" >  
  4.       <intent-filter>  
  5.             <action android:name="android.intent.plugin" />  
  6.       </intent-filter>  
  7. </activity>  

代码点此下载


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值