详解Android插件化开发-资源访问

动态加载技术(也叫插件化技术),当项目越来越庞大的时候,我们通过插件化开发不仅可以减轻应用的内存和CPU占用,还可以实现热插拔,即在不发布新版本的情况下更新某些模块。

通常我们把安卓资源文件制作成插件的形式,无外乎有一下几种:

zip、jar、dex、APK(未安装APK、安装APK)

对于用户来讲未安装的APK才是用户所需要的,不安装、不重启,无声无息的加载资源文件,这正是我们开发者追求的结果。

但是,开发中宿主程序调起未安装的插件apk,一个很大的问题就是资源如何访问,这些资源文件的ID都映射在gen文件夹下的R.Java中,而插件中凡是以R开头的资源都不能访问。究其原因是因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源。

那么开发中该怎么办呢,今天我们来一起探讨一下插件化开发中资源文件访问的解决方案。

想必大家在开发中都写过类似代码,例如,在主程序访问字符串文件

this.getResources().getString(R.string.app_name);

这里的this,其实就是Context,上下文对象。通常我们的的APK安装路径为:

/data/apk/packagename~1/base.apk

APK启动,Context通过类加载器加载完毕后,会去APK中加载资源文件。想必大家都知道,Activity的工作主要是通过ContextImpl来完成的, Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中,也就是说,只要实现这两个方法,就可以解决资源问题了。

/** Return an AssetManager instance for your application‘s package. */

public abstract AssetManager getAssets();

/** Return a Resources instance for your application‘s package. */

public abstract Resources getResources();

我们若是想使用这两个方法,需要实例化Context对象,通常我们可以根据APK中的包名完成Context对象的创建:

Context pluginContext = this.createPackageContext("com.castiel.demo",flags);

但是这样做有个前提,必须要求初始化时加载的是自己APK,如果我们加载的是未安装的插件APK,这么做肯定就不可取了。为啥呢,看源码:

Resources resources = packageInfo.getResources(mainThread);
        if (resources != null) {
            if (activityToken != null
                    || displayId != Display.DEFAULT_DISPLAY
                    || overrideConfiguration != null
                    || (compatInfo != null && compatInfo.applicationScale
                            != resources.getCompatibilityInfo().applicationScale)) {
                resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                        packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                        packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                        overrideConfiguration, compatInfo, activityToken);
            }
        }
        mResources = resources;

Resources在这里被赋值,我们再去代码中第一行的packageInfo,它来自LoadedApk类,其中的getResources方法如下:


public Resources getResources(ActivityThread mainThread) {
        if (mResources == null) {
            mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs,
                    mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this);
        }
        return mResources;
    }

该方法采用单例模式,注意其中的getTopLevelResources()方法中的第一个参数mResDir,我们继续找其源头,在ActivityThread类中,发现了:

    /**
     * Creates the top level resources for the given package.
     */
    Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
            String[] libDirs, int displayId, Configuration overrideConfiguration,
            LoadedApk pkgInfo) {
        return mResourcesManager.getTopLevelResources(resDir, splitResDirs, overlayDirs, libDirs,
                displayId, overrideConfiguration, pkgInfo.getCompatibilityInfo(), null);
    }

重点看里面的resDir参数,我们再往上找源码,最终找到ResourcesManager类,找到getTopLevelResources()方法:

    /**
     * Creates the top level Resources for applications with the given compatibility info.
     *
     * @param resDir the resource directory.
     * @param overlayDirs the resource overlay directories.
     * @param libDirs the shared library resource dirs this app references.
     * @param compatInfo the compability info. Must not be null.
     * @param token the application token for determining stack bounds.
     */
    public Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
        final float scale = compatInfo.applicationScale;
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
        Resources r;
        synchronized (this) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
            }
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
            //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
            if (r != null && r.getAssets().isUpToDate()) {
                if (false) {
                    Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                            + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                }
                return r;
            }
        }
        //if (r != null) {
        //    Slog.w(TAG, "Throwing away out-of-date resources!!!! "
        //            + r + " " + resDir);
        //}

        AssetManager assets = new AssetManager();
        // resDir can be null if the ‘android‘ package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the ‘android‘ package
        // already.
        if (resDir != null) {
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }

        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }

        if (overlayDirs != null) {
            for (String idmapPath : overlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }

        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (assets.addAssetPath(libDir) == 0) {
                    Slog.w(TAG, "Asset path ‘" + libDir +
                            "‘ does not exist or contains no resources.");
                }
            }
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();
        if (!isDefaultDisplay || hasOverrideConfig) {
            config = new Configuration(getConfiguration());
            if (!isDefaultDisplay) {
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
            }
            if (hasOverrideConfig) {
                config.updateFrom(key.mOverrideConfiguration);
            }
        } else {
            config = getConfiguration();
        }
        r = new Resources(assets, dm, config, compatInfo, token);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }

        synchronized (this) {
            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;
        }
}

该方法的注释中,明确指出@param resDir the resource directory,加载本地资源目录,加载自己的APK。

通过以上的分析,我们知道getResources()方法通过AssetManager加载自己的APK,那么我们要想加载未安装的插件APK,唯有自定义实现一个Resources类,专门用来加载未安装的APK。但是我试过了,直接重写不行,为啥,因为Android并没有提供Resource构造方法中的AssetManager的构造方法,我们看下源码:

    /**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

接着再看一下Resource构造方法中的AssetManager参数源码

    /**
     * Create a new AssetManager containing only the basic system assets.
     * Applications will not generally use this method, instead retrieving the
     * appropriate asset manager with {@link Resources#getAssets}.    Not for
     * use by applications.
     * {@hide}
     */
    public AssetManager() {
        synchronized (this) {
            if (DEBUG_REFS) {
                mNumRefs = 0;
                incRefsLocked(this.hashCode());
            }
            init(false);
            if (localLOGV) Log.v(TAG, "New asset manager: " + this);
            ensureSystemAssets();
        }
    }

注意注释中的{@hide},隐藏起来了,Android系统不让我们使用。既然不让我们直接使用,那我们可以采用反射的方式来拿到AssetManager。接下来我把自定义的实现类贴出来,给大家示例:

/**
 *
 * @ClassName: MyPluginResources
 * @Description: 自定义插件资源文件获取工具类
 * @author 猴子搬来的救兵http://blog.csdn.net/mynameishuangshuai
 * @version
 */
public class MyPluginResources extends Resources{

    public MyPluginResources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        super(assets, metrics, config);
    }

    /**
     * 自定义返回插件的资源文件的Resource方法
     * @param resources
     * @param assets
     * @return
     */
    public static MyPluginResources getPluginResources(Resources resources,AssetManager assets){
        MyPluginResources pluginResources = new MyPluginResources(assets, resources.getDisplayMetrics(), resources.getConfiguration());
        return pluginResources;
    } 

    //自己定义加载插件APK的AssetsManager
    public static AssetManager getPluginAssetsManager(File apkFile,Resources resources) throws ClassNotFoundException{
        // 由于系统没有提供AssetManager的实例化方法,因此我们使用反射
        Class<?> forName = Class.forName("android.content.res.AssetManager");
        Method[] declaredMethods = forName.getDeclaredMethods();
        for(Method method :declaredMethods){
            if(method.getName().equals("addAssetPath")){
                try {
                    AssetManager assetManager = AssetManager.class.newInstance();
                    // 调用addAssetPath方法,参数为我们插件APK的路径
                    method.invoke(assetManager, apkFile.getAbsolutePath());
                    return assetManager;
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }
}

这样,我们在项目中就可以使用我们自定义的AssetManager来获取未安装插件APK中的资源文件

AssetManager assetManager = PluginResources.getPluginAssetsManager(apkFile,
            this.getResources());

参考:《Android开发艺术探索》

时间: 2024-10-17 05:48:39

详解Android插件化开发-资源访问的相关文章

Android插件化开发---运行未安装apk中的Service

如果你还不知道什么叫插件化开发,那么你应该先读一读之前写的这篇博客:Android插件化开发,初入殿堂 上一篇博客主要从整体角度分析了一下Android插件化开发的几个难点与动态加载没有被安装的apk中的Activity和资源的方法.其实一般的插件开发主要也就是加载个Activity,读取一些资源图片之类的.但是总有遇到特殊情况的时候,比如加载Service. 要动态加载Service,有两种思路:一是通过NDK的形式,将Service通过C++运行起来(这种方法我没有尝试,只听群里的朋友说实现

Android插件化开发,初入殿堂

好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架CJFrameForAndroid. 好久没有写博客了,这次准备写写我这几天的研究成果--Android插件化开发框架CJFrameForAndroid. 背景交代 首先,你需要知道什么是插件化开发.就拿最常见的QQ来说,在第三个界面动态那里有个管理,点开后可以选择很多的增植功能,这里腾讯只放了一些网页应用,那么如果未来想加入一个打飞机游戏,要怎么做?让用户重新安装吗,这就是插件化开发所解决的问题. 用一句话来概括插

Android 插件化开发-主题皮肤更换

参考 http://www.2cto.com/kf/201501/366859.html 本项目是以插件化开发思想进行的,主要工作和代码如下 资源文件,这里以color资源为例 1.首先我们需要准备一个皮肤包,这个皮肤包里面不会包含任何Activity,里面只有资源文件,这里我为了简单,仅仅加入一个color.xml(其实就相当于Android系统中的framework_res.apk) <!--?xml version="1.0" encoding="utf-8&qu

Android插件化开发之解决Atlas组件在宿主的注册问题

OpenAtlas有一个问题,就是四大组件必须在Manifest文件中进行注册,那么就必然带来一个问题,插件中的组件都要重复在宿主中注册.像Service,ContentProvider等组件目前没有什么好的解决方法,只能在宿主中注册.但是像Activity,显然是有解决方法的,就是使用Fragment代替Activity,Activity只是作为一个放Fragment的容器,那么不仅在插件中不用再清单文件中注册,就连宿主的注册问题也一并解决了.那么,解决方案呢,没错,就是之前写的一篇博文And

Android 插件化开发(三):资源插件化

在前面的文章中我们成功的加载了外部的Dex(Apk)并执行了插件的Bean代码.这时我们会想,能不能加载并运行插件Apk的Activity.答案当然是能,否则后续我们的研究就没意义了,但是想实现Activity的插件化运行,我们必须要解决一个问题——如何使用插件中的资源. 本文我们就讲一下插件的资源加载机制,并讲述一下如何实现资源的插件化. 一.资源的加载机制 Android的资源文件分为两类: 第一类是res目录下存放的可编辑的资源文件,这类文件在编译时系统会自动在R文件中生成资源文件的16进

Android插件化开发-hook动态代理

首先,我们阐述为什么android需要插件化: 1:由于业务的增长,app的方法数逐渐达到65535(有人说用于检索方法数的列表大小使用short存储的,其实我看了源码之后并没有发现相关信息,并对此说法产生了怀疑,不过最后找到的结果就是,65535这个限制可能是由于dalvik的bytecode大小限制的,具体的可以查看官方文档). 2:一个模块的变化都要整体编译一次app,维护成本太大了,用插件开发app会好很多 对于以上问题解决方案不少,著名的有h5,hybird,不过这些都没有native

android插件化开发——加载广播

阅读本文前,先阅读前面几篇: http://blog.csdn.net/u013022222/article/details/51171720 引言 在android开发过程中,我们不可避免的会使用广播,比如,侦听开机,侦听短信. 而对于广播,我想很多人都知道他有两种类型,动态广播,通过代码在runtime进行register, 像这样: IntentFilter intentFilter = new IntentFilter("com.chan.plugin.receiver");

Android插件化开发之DexClassLoader动态加载dex、jar小Demo

一.温故动态加载ClassLoader机制 如果对Android的ClassLoader加载机制不熟悉,猛戳Android插件化开发动态加载基础之ClassLoader工作机制 http://blog.csdn.net/u011068702/article/details/53248960 二.介绍 我们知道在Android中可以跟java一样实现动态加载jar,但是Android使用德海Dalvik VM,不能直接加载java打包jar的byte code,需要通过dx工具来优化Dalvik

Android插件化开发之OpenAtlas插件启动方式与插件启动广播

到现在为止已经写了6篇文章了 Android插件化开发之OpenAtlas初体验 Android插件化开发之OpenAtlas生成插件信息列表 Android插件化开发之OpenAtlas资源打包工具补丁aapt的编译 Android插件化开发之OpenAtlas插件适配 Android插件化开发之解决OpenAtlas组件在宿主的注册问题 Android插件化开发之OpenAtlas中四大组件与Application功能的验证 这篇文章主要介绍一下OpenAtlas插件的几种启动方式,在Atl