Android源码分析-资源加载机制

前言

我们知道,在activity内部访问资源(字符串,图片等)是很简单的,只要getResources然后就可以得到Resources对象,有了Resources对象就可以访问各种资源了,这很简单,不过本文不是介绍这个的,本文主要介绍在这套逻辑之下的资源加载机制

资源加载机制

很明确,不同的Context得到的都是同一份资源。这是很好理解的,请看下面的分析
得到资源的方式为context.getResources,而真正的实现位于ContextImpl中的getResources方法,在ContextImpl中有一个成员 private Resources mResources,它就是getResources方法返回的结果,mResources的赋值代码为:
mResources = mResourcesManager.getTopLevelResources(mPackageInfo.getResDir(),
                    Display.DEFAULT_DISPLAY, null, compatInfo, activityToken);
下面看一下ResourcesManager的getTopLevelResources方法,这个方法的思想是这样的:在ResourcesManager中,所有的资源对象都被存储在ArrayMap中,首先根据当前的请求参数去查找资源,如果找到了就返回,否则就创建一个资源对象放到ArrayMap中。有一点需要说明的是为什么会有多个资源对象,原因很简单,因为res下可能存在多个适配不同设备、不同分辨率、不同系统版本的目录,按照android系统的设计,不同设备在访问同一个应用的时候访问的资源可以不同,比如drawable-hdpi和drawable-xhdpi就是典型的例子。

 
  1. public Resources getTopLevelResources(String resDir, int displayId,

  2. Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {

  3. final float scale = compatInfo.applicationScale;

  4. ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale,

  5. token);

  6. Resources r;

  7. synchronized (this) {

  8. // Resources is app scale dependent.

  9. if (false) {

  10. Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);

  11. }

  12. WeakReference<Resources> wr = mActiveResources.get(key);

  13. r = wr != null ? wr.get() : null;

  14. //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());

  15. if (r != null && r.getAssets().isUpToDate()) {

  16. if (false) {

  17. Slog.w(TAG, "Returning cached resources " + r + " " + resDir

  18. + ": appScale=" + r.getCompatibilityInfo().applicationScale);

  19. }

  20. return r;

  21. }

  22. }

  23.  
  24. //if (r != null) {

  25. // Slog.w(TAG, "Throwing away out-of-date resources!!!! "

  26. // + r + " " + resDir);

  27. //}

  28.  
  29. AssetManager assets = new AssetManager();

  30. if (assets.addAssetPath(resDir) == 0) {

  31. return null;

  32. }

  33.  
  34. //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);

  35. DisplayMetrics dm = getDisplayMetricsLocked(displayId);

  36. Configuration config;

  37. boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);

  38. final boolean hasOverrideConfig = key.hasOverrideConfiguration();

  39. if (!isDefaultDisplay || hasOverrideConfig) {

  40. config = new Configuration(getConfiguration());

  41. if (!isDefaultDisplay) {

  42. applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);

  43. }

  44. if (hasOverrideConfig) {

  45. config.updateFrom(key.mOverrideConfiguration);

  46. }

  47. } else {

  48. config = getConfiguration();

  49. }

  50. r = new Resources(assets, dm, config, compatInfo, token);

  51. if (false) {

  52. Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "

  53. + r.getConfiguration() + " appScale="

  54. + r.getCompatibilityInfo().applicationScale);

  55. }

  56.  
  57. synchronized (this) {

  58. WeakReference<Resources> wr = mActiveResources.get(key);

  59. Resources existing = wr != null ? wr.get() : null;

  60. if (existing != null && existing.getAssets().isUpToDate()) {

  61. // Someone else already created the resources while we were

  62. // unlocked; go ahead and use theirs.

  63. r.getAssets().close();

  64. return existing;

  65. }

  66.  
  67. // XXX need to remove entries when weak references go away

  68. mActiveResources.put(key, new WeakReference<Resources>(r));

  69. return r;

  70. }

  71. }

根据上述代码中资源的请求机制,再加上ResourcesManager采用单例模式,这样就保证了不同的ContextImpl访问的是同一套资源,注意,这里说的同一套资源未必是同一个资源,因为资源可能位于不同的目录,但它一定是我们的应用的资源,或许这样来描述更准确,在设备参数和显示参数不变的情况下,不同的ContextImpl访问到的是同一份资源。设备参数不变是指手机的屏幕和android版本不变,显示参数不变是指手机的分辨率和横竖屏状态。也就是说,尽管Application、Activity、Service都有自己的ContextImpl,并且每个ContextImpl都有自己的mResources成员,但是由于它们的mResources成员都来自于唯一的ResourcesManager实例,所以它们看似不同的mResources其实都指向的是同一块内存(C语言的概念),因此,它们的mResources都是同一个对象(在设备参数和显示参数不变的情况下)。在横竖屏切换的情况下且应用中为横竖屏状态提供了不同的资源,处在横屏状态下的ContextImpl和处在竖屏状态下的ContextImpl访问的资源不是同一个资源对象。

代码:单例模式的ResourcesManager类

 
  1. public static ResourcesManager getInstance() {

  2. synchronized (ResourcesManager.class) {

  3. if (sResourcesManager == null) {

  4. sResourcesManager = new ResourcesManager();

  5. }

  6. return sResourcesManager;

  7. }

  8. }

Resources对象的创建过程

通过阅读Resources类的源码可以知道,Resources对资源的访问实际上是通过AssetManager来实现的,那么如何创建一个Resources对象呢,有人会问,我为什么要去创建一个Resources对象呢,直接getResources不就可以了吗?我要说的是在某些特殊情况下你的确需要去创建一个资源对象,比如动态加载apk。很简单,首先看一下它的几个构造方法:

 
  1. /**

  2. * Create a new Resources object on top of an existing set of assets in an

  3. * AssetManager.

  4. *

  5. * @param assets Previously created AssetManager.

  6. * @param metrics Current display metrics to consider when

  7. * selecting/computing resource values.

  8. * @param config Desired device configuration to consider when

  9. * selecting/computing resource values (optional).

  10. */

  11. public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {

  12. this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);

  13. }

  14.  
  15. /**

  16. * Creates a new Resources object with CompatibilityInfo.

  17. *

  18. * @param assets Previously created AssetManager.

  19. * @param metrics Current display metrics to consider when

  20. * selecting/computing resource values.

  21. * @param config Desired device configuration to consider when

  22. * selecting/computing resource values (optional).

  23. * @param compatInfo this resource's compatibility info. Must not be null.

  24. * @param token The Activity token for determining stack affiliation. Usually null.

  25. * @hide

  26. */

  27. public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config,

  28. CompatibilityInfo compatInfo, IBinder token) {

  29. mAssets = assets;

  30. mMetrics.setToDefaults();

  31. if (compatInfo != null) {

  32. mCompatibilityInfo = compatInfo;

  33. }

  34. mToken = new WeakReference<IBinder>(token);

  35. updateConfiguration(config, metrics);

  36. assets.ensureStringBlocks();

  37. }

除了这两个构造方法还有一个私有的无参方法,由于是私有的,所以没法访问。上面两个构造方法,从简单起见,我们应该采用第一个

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)

它接受3个参数,第一个是AssetManager,后面两个是和设备相关的配置参数,我们可以直接用当前应用的配置就好,所以,问题的关键在于如何创建AssetManager,下面请看分析,为了创建一个我们自己的AssetManager,我们先去看看系统是怎么创建的。还记得getResources的底层实现吗,在ResourcesManager的getTopLevelResources方法中有这么两句:

 
  1. AssetManager assets = new AssetManager();

  2. if (assets.addAssetPath(resDir) == 0) {

  3. return null;

  4. }

这两句就是创建一个AssetManager对象,后面会用这个对象来创建Resources对象,ok,AssetManager就是这么创建的,assets.addAssetPath(resDir)这句话的意思是把资源目录里的资源都加载到AssetManager对象中,具体的实现在jni中,大家感兴趣自己去了解下。而资源目录就是我们的res目录,当然resDir可以是一个目录也可以是一个zip文件。有没有想过,如果我们把一个未安装的apk的路径传给这个方法,那么apk中的资源是不是就被加载到AssetManager对象里面了呢?事实证明,的确是这样,具体情况可以参见Android apk动态加载机制的研究(二):资源加载和activity生命周期管理这篇文章。addAssetPath方法的定义如下,注意到它的注释里面有一个{@hide}关键字,这意味着即使它是public的,但是外界仍然无法访问它,因为android sdk导出的时候会自动忽略隐藏的api,因此只能通过反射来调用。

 
  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 final int addAssetPath(String path) {

  8. int res = addAssetPathNative(path);

  9. return res;

  10. }

有了AssetManager对象后,我们就可以创建自己的Resources对象了,代码如下:

 
  1. try {

  2. AssetManager assetManager = AssetManager.class.newInstance();

  3. Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);

  4. addAssetPath.invoke(assetManager, mDexPath);

  5. mAssetManager = assetManager;

  6. } catch (Exception e) {

  7. e.printStackTrace();

  8. }

  9. Resources currentRes = this.getResources();

  10. mResources = new Resources(mAssetManager, currentRes.getDisplayMetrics(),

  11. currentRes.getConfiguration());

有了Resources对象,我们就可以通过Resources对象来访问里面的各种资源了,通过这种方法,我们可以完成一些特殊的功能,比如换肤、换语言包、动态加载apk等,欢迎大家交流。

猜你喜欢

转载自blog.csdn.net/suyimin2010/article/details/81156233