zoukankan      html  css  js  c++  java
  • 详解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开发艺术探索》

  • 相关阅读:
    zend guard 4/5 破解版和免过期办法,已补授权Key一枚,成功注册。
    一身冷汗,PHP应该禁用的函数
    CentOS 5.5 安装和卸载桌面
    php加速模块 eAccelerator open_basedir错误解决办法
    配置电信网通双线双IP的解决办法
    php安装igbinary模块
    ubuntu系统VNC服务器安装配置
    python3 之 闭包实例解析 Be
    python3 之 内置函数enumerate Be
    python3 之 匿名函数 Be
  • 原文地址:https://www.cnblogs.com/Free-Thinker/p/6755524.html
Copyright © 2011-2022 走看看