详解Android插件化开发-资源访问
2016-10-05 15:32
477 查看
动态加载技术(也叫插件化技术),当项目越来越庞大的时候,我们通过插件化开发不仅可以减轻应用的内存和CPU占用,还可以实现热插拔,即在不发布新版本的情况下更新某些模块。
通常我们把安卓资源文件制作成插件的形式,无外乎有一下几种:
zip、jar、dex、APK(未安装APK、安装APK)
对于用户来讲未安装的APK才是用户所需要的,不安装、不重启,无声无息的加载资源文件,这正是我们开发者追求的结果。
但是,开发中宿主程序调起未安装的插件apk,一个很大的问题就是资源如何访问,这些资源文件的ID都映射在gen文件夹下的R.java中,而插件中凡是以R开头的资源都不能访问。究其原因是因为宿主程序中并没有插件的资源,所以通过R来加载插件的资源是行不通的,程序会抛出异常:无法找到某某id所对应的资源。
那么开发中该怎么办呢,今天我们来一起探讨一下插件化开发中资源文件访问的解决方案。
想必大家在开发中都写过类似代码,例如,在主程序访问字符串文件
这里的this,其实就是Context,上下文对象。通常我们的的APK安装路径为:
/data/apk/packagename~1/base.apk
APK启动,Context通过类加载器加载完毕后,会去APK中加载资源文件。想必大家都知道,Activity的工作主要是通过ContextImpl来完成的, Activity中有一个叫mBase的成员变量,它的类型就是ContextImpl。注意到Context中有如下两个抽象方法,看起来是和资源有关的,实际上Context就是通过它们来获取资源的。这两个抽象方法的真正实现在ContextImpl中,也就是说,只要实现这两个方法,就可以解决资源问题了。
我们若是想使用这两个方法,需要实例化Context对象,通常我们可以根据APK中的包名完成Context对象的创建:
但是这样做有个前提,必须要求初始化时加载的是自己APK,如果我们加载的是未安装的插件APK,这么做肯定就不可取了。为啥呢,看源码:
Resources在这里被赋值,我们再去代码中第一行的packageInfo,它来自LoadedApk类,其中的getResources方法如下:
该方法采用单例模式,注意其中的getTopLevelResources()方法中的第一个参数mResDir,我们继续找其源头,在ActivityThread类中,发现了:
重点看里面的resDir参数,我们再往上找源码,最终找到ResourcesManager类,找到getTopLevelResources()方法:
该方法的注释中,明确指出@param resDir the resource directory,加载本地资源目录,加载自己的APK。
通过以上的分析,我们知道getResources()方法通过AssetManager加载自己的APK,那么我们要想加载未安装的插件APK,唯有自定义实现一个Resources类,专门用来加载未安装的APK。但是我试过了,直接重写不行,为啥,因为Android并没有提供Resource构造方法中的AssetManager的构造方法,我们看下源码:
接着再看一下Resource构造方法中的AssetManager参数源码
注意注释中的{@hide},隐藏起来了,Android系统不让我们使用。既然不让我们直接使用,那我们可以采用反射的方式来拿到AssetManager。接下来我把自定义的实现类贴出来,给大家示例:
这样,我们在项目中就可以使用我们自定义的AssetManager来获取未安装插件APK中的资源文件
参考:《Android开发艺术探索》
通常我们把安卓资源文件制作成插件的形式,无外乎有一下几种:
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开发艺术探索》
相关文章推荐
- 详解Android插件化开发-资源访问
- 详解Android插件化开发-资源访问
- 【Android 应用开发】Android屏幕适配解析 - 详解像素,设备独立像素,归一化密度,精确密度及各种资源对应的尺寸密度分辨率适配问题
- Android插件化开发之OpenAtlas资源打包工具补丁aapt的编译
- Android 插件化开发之动态加载技术三个关键问题详解
- 【Android 应用开发】Android屏幕适配解析 - 详解像素,设备独立像素,归一化密度,精确密度及各种资源对应的尺寸密度分辨率适配问题
- Android插件化开发 第三篇 [加载插件资源]
- Android应用开发之PNG、IconFont、SVG图标资源优化详解
- Android插件化开发之动态加载三个关键问题详解
- 10天学通Android开发(6)-资源访问
- Android WebView 开发 资源释放,处理详解
- 【Android开发学习笔记】【高级】【随笔】插件化——资源加载
- Android 样式开发:Drawable分类资源汇总详解(一)
- Android开发进阶(四)--深入Android通过Apache HTTP访问HTTP资源
- Android开发之使用URL访问网络资源
- Android应用开发之PNG、IconFont、SVG图标资源优化详解
- Android开发之浏览器用法实例详解(调用uc,opera,qq浏览器访问网页)
- Android 插件化开发——Hook机制的动态代理详解
- Android 插件化开发之获取APK的代码与资源
- Android插件化开发 第三篇 [加载插件资源]