Android动态资源加载原理和应用

来源:互联网 发布:免费恢复u盘数据软件 编辑:程序博客网 时间:2024/06/03 20:25

动态加载资源原理

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

public Resources getResources() {    return mResources;}
mResources是在创建ContextImp对象后的init方法里面创建的

mResources = mPackageInfo.getResources(mainThread);
调用了LoadedApk的getResources方法
public Resources getResources(ActivityThread mainThread) {    if (mResources == null) {        mResources = mainThread.getTopLevelResources(mResDir,                Display.DEFAULT_DISPLAY, null, this);    }    return mResources;}
又调用到了ActivityThread类的getTopLevelResources方法

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使用resDir和其他参数来构造,这里主要是resDir参数,表明资源文件所在的路径。也就是APK程序所在路径。

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方法,提高代码的一致性。

@Overridepublic Resources getResources() {return resources == null ? super.getResources() : resources;}@Overridepublic AssetManager getAssets() {return assetManager == null ? super.getAssets() : assetManager;}
于是在加载了资源之后就可以通过该Resources对象获取对应路径下面的资源了。

动态加载资源

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


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

宿主程序的代码具体如下

private AssetManager assetManager;private Resources resources;private RadioGroup rg;private ImageView iv;@Overrideprotected 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() {@Overridepublic 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());}@Overridepublic Resources getResources() {return resources == null ? super.getResources() : resources;}@Overridepublic 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>();@Overrideprotected 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() {@Overridepublic 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() {@Overridepublic 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());}@Overridepublic Resources getResources() {return resources == null ? super.getResources() : resources;}@Overridepublic 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>

代码点此下载

1 0
原创粉丝点击