【Android源码】资源加载AssetManager源码分析 -- app是如何加载资源以及我们是如何从内存中获取

通常情况下,当我们需要使用资源的时候,都是通过api直接调用:

getResources().getDrawable(R.mipmap.ic_launcher);

通过getResources()的众多方法可以获取到整个apk包里面的资源,那么我们是如何获取到资源的?这些资源又是如何被加载到内存中的?
今天我们来一起分析一下app是如何加载资源文件的。

资源加载过程

首先我们通过getResources()来看一下Resource是在哪里被创建出来的。

// AppCompatActivity.java
@Override
public Resources getResources() {
   if (mResources == null && VectorEnabledTintResources.shouldBeUsed()) {
       mResources = new VectorEnabledTintResources(this, super.getResources());
   }
   return mResources == null ? super.getResources() : mResources;
}

// Context.java
public abstract Resources getResources();

一路点击过去发现,最终又到了Context这个上下文中,而Context是一个抽象类,所以我们需要找到其实现类ContextImpl.java

// ContextImpl.java
@Override
public Resources getResources() {
   return mResources;
}

private ContextImpl(ContextImpl container, ActivityThread mainThread,
       LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
       Display display, Configuration overrideConfiguration, int createDisplayWithId) {
            
    Resources resources = packageInfo.getResources(mainThread);
    mResources = resources; 
}

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

// ResourcesManager.java
Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs,
       String[] libDirs, int displayId, LoadedApk pkgInfo) {
   return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs,
           displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader());
}

发现在ContextImpl.java中Resources是被ResourcesManager的getTopLevelResources方法返回的。ResourcesManager是资源的管理类,我们着重看一下它。

public @NonNull Resources getResources(@Nullable IBinder activityToken,
       @Nullable String resDir,
       @Nullable String[] splitResDirs,
       @Nullable String[] overlayDirs,
       @Nullable String[] libDirs,
       int displayId,
       @Nullable Configuration overrideConfig,
       @NonNull CompatibilityInfo compatInfo,
       @Nullable ClassLoader classLoader) {
   try {
       Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
       // 生成资源key对象
       final ResourcesKey key = new ResourcesKey(
               resDir,
               splitResDirs,
               overlayDirs,
               libDirs,
               displayId,
               overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
               compatInfo);
       classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
       return getOrCreateResources(activityToken, key, classLoader);
   } finally {
       Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
   }
}

private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken,
       @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
   synchronized (this) {

            // 从缓存中获取Resources
       if (activityToken != null) {
           final ActivityResources activityResources =
                   getOrCreateActivityResourcesStructLocked(activityToken);
                // 通过资源key对象获取缓存Resources
           ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
           if (resourcesImpl != null) {
               if (DEBUG) {
                   Slog.d(TAG, "- using existing impl=" + resourcesImpl);
               }
               return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                       resourcesImpl);
           }
       } else {
            // 通过资源key对象获取缓存Resources
           ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
           if (resourcesImpl != null) {
               if (DEBUG) {
                   Slog.d(TAG, "- using existing impl=" + resourcesImpl);
               }
               return getOrCreateResourcesLocked(classLoader, resourcesImpl);
           }
       }
   }

   // 没有缓存创建ResourcesImpl
   ResourcesImpl resourcesImpl = createResourcesImpl(key);

   synchronized (this) {
        // 通过资源key对象获取缓存Resources
       ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
       if (existingResourcesImpl != null) {
           resourcesImpl.getAssets().close();
           resourcesImpl = existingResourcesImpl;
       } else {
           // 如果缓存中没有,将创建好的ResourceImpl加入到缓存中
           mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
       }

       final Resources resources;
       if (activityToken != null) {
            // 通过资源key对象获取缓存Resources
           resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                   resourcesImpl);
       } else {
            // 通过资源key对象获取缓存Resources
           resources = getOrCreateResourcesLocked(classLoader, resourcesImpl);
       }
       return resources;
   }
}

生成资源Resources对象的步骤:

  1. 首先生成资源Key对象,用于将资源对象添加到缓存map中或者从缓存map中查找资源对象
  2. 通过activityToken判断是否关联Activity,并从缓存中通过key获取缓存的ResourcesImpl对象,如果有直接返回
  3. 如果没有缓存的ResourcesImpl对象,则直接创建新的缓存ResourcesImpl对象,通过createResourcesImpl(key)来创建。
  4. 创建完之后在将其加入缓存Map中,并最终返回Resources.setImpl(impl)方法,创建出Resources对象并返回。

所以我们需要着重看下createResourcesImpl方法,这个方法才是创建ResourcesImpl的核心。

private @NonNull ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
   final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
   daj.setCompatibilityInfo(key.mCompatInfo);

   final AssetManager assets = createAssetManager(key);
   final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
   final Configuration config = generateConfig(key, dm);
   final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
   if (DEBUG) {
       Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
   }
   return impl;
}

可以发现这儿方法创建了AssetManager资产管理类:

@VisibleForTesting
protected @NonNull AssetManager createAssetManager(@NonNull final ResourcesKey key) {
   AssetManager assets = new AssetManager();
   if (key.mResDir != null) {
       if (assets.addAssetPath(key.mResDir) == 0) {
           throw new Resources.NotFoundException("failed to add asset path " + key.mResDir);
       }
   }
   return assets;
}

// LoadedApk.java
private void setApplicationInfo(ApplicationInfo aInfo) {
   final int myUid = Process.myUid();
   aInfo = adjustNativeLibraryPaths(aInfo);
   mApplicationInfo = aInfo;
   mAppDir = aInfo.sourceDir;
   mResDir = aInfo.uid == myUid ? aInfo.sourceDir : aInfo.publicSourceDir;
}

AssetManager是用来加载制定路径下的资源文件的,所以我们想要获取的资源文件都是在这个类中找到的。而key.mResDir就是app的资源路径。

所以AssetManager才是整个资源加载的关键所在。

public AssetManager() {
   synchronized (this) {
       if (DEBUG_REFS) {
           mNumRefs = 0;
           incRefsLocked(this.hashCode());
       }
       init(false);
       if (localLOGV) Log.v(TAG, "New asset manager: " + this);
       ensureSystemAssets();
   }
}

private native final void init(boolean isSystem);

private native final int loadResourceValue(int ident, short density, TypedValue outValue,
            boolean resolve);

在创建AssetManager的时候,它首先会去调用native层中的init方法

static void android_content_AssetManager_init(JNIEnv* env, jobject clazz, jboolean isSystem)
{
    if (isSystem) {
        verifySystemIdmaps();
    }
    //  AssetManager.cpp
    AssetManager* am = new AssetManager();
    if (am == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", "");
        return;
    }

    am->addDefaultAssets();

    ALOGV("Created AssetManager %p for Java object %p\n", am, clazz);
    env->SetLongField(clazz, gAssetManagerOffsets.mObject, reinterpret_cast<jlong>(am));
}

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    String8 path(root);
    // kSystemAssets -> framework/framework-res.apk  
    // 加载系统的资源如颜色、图片、文字
    path.appendPath(kSystemAssets);

    return addAssetPath(path, NULL);
}

bool AssetManager::addAssetPath(const String8& path, int32_t* cookie)
{
    asset_path ap;

    // 判断是否已经加载过了
    for (size_t i=0; i<mAssetPaths.size(); i++) {
        if (mAssetPaths[i].path == ap.path) {
            if (cookie) {
                *cookie = static_cast<int32_t>(i+1);
            }
            return true;
        }
    }

    // 检查路径是否有一个androidmanifest.xml
    Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
            kAndroidManifest, Asset::ACCESS_BUFFER, ap);
    if (manifestAsset == NULL) {
        // 如果不包含任何资源
        delete manifestAsset;
        return false;
    }
    delete manifestAsset;
    // 添加 
    mAssetPaths.add(ap);
    if (mResources != NULL) {
        appendPathToResTable(ap);
    }

    return true;
}

bool AssetManager::appendPathToResTable(const asset_path& ap) const {

    Asset* ass = NULL;
    ResTable* sharedRes = NULL;
    bool shared = true;
    bool onlyEmptyResources = true;
    MY_TRACE_BEGIN(ap.path.string());

    Asset* idmap = openIdmapLocked(ap);
    size_t nextEntryIdx = mResources->getTableCount();
    ALOGV("Looking for resource asset in '%s'\n", ap.path.string());
    // 资源包路径不是一个文件夹
    if (ap.type != kFileTypeDirectory) {
        if (nextEntryIdx == 0) {
            // mAssetPaths中存储的第一个资源包路径是系统资源的路径,
            // 即framework-res.apk的路径,它在zygote启动时已经加载了
            // 可以通过mZipSet.getZipResourceTable获得其ResTable对象
            sharedRes = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTable(ap.path);
            // 对于APP来说,肯定不为NULL
            if (sharedRes != NULL) {
                // 得到系统资源包路径中resources.arsc个数
                nextEntryIdx = sharedRes->getTableCount();
            }
        }
        // 当参数是mAssetPaths中除第一个以外的其他资源资源包路径,
        // 比如app自己的资源包路径时,走下面的逻辑
        if (sharedRes == NULL) {
            // 检查该资源包是否被其他进程加载了,这与ZipSet数据结构有关,后面在详细介绍
            ass = const_cast<AssetManager*>(this)->
                mZipSet.getZipResourceTableAsset(ap.path);
            if (ass == NULL) {
                ALOGV("loading resource table %s\n", ap.path.string());
                // 创建Asset对象,就是打开resources.arsc
                ass = const_cast<AssetManager*>(this)->
                    openNonAssetInPathLocked("resources.arsc",
                                             Asset::ACCESS_BUFFER,
                                             ap);
                if (ass != NULL && ass != kExcludedAsset) {
                    ass = const_cast<AssetManager*>(this)->
                        mZipSet.setZipResourceTableAsset(ap.path, ass);
                }
            }
            // 只有在zygote启动时,才会执行下面的逻辑
            // 为系统资源创建 ResTable,并加入到mZipSet里。
            if (nextEntryIdx == 0 && ass != NULL) {
                ALOGV("Creating shared resources for %s", ap.path.string());
                // 创建ResTable对象,并把前面与resources.arsc关联的Asset对象,加入到这个ResTabl中
                sharedRes = new ResTable();
                sharedRes->add(ass, idmap, nextEntryIdx + 1, false);
                sharedRes = const_cast<AssetManager*>(this)->
                    mZipSet.setZipResourceTable(ap.path, sharedRes);
            }
        }
    } else {
        ALOGV("loading resource table %s\n", ap.path.string());
        ass = const_cast<AssetManager*>(this)->
            openNonAssetInPathLocked("resources.arsc",
                                     Asset::ACCESS_BUFFER,
                                     ap);
        shared = false;
    }

    if ((ass != NULL || sharedRes != NULL) && ass != kExcludedAsset) {
        ALOGV("Installing resource asset %p in to table %p\n", ass, mResources);
        // 系统资源包时
        if (sharedRes != NULL) {
            ALOGV("Copying existing resources for %s", ap.path.string());
            mResources->add(sharedRes);
        } else {
            // 非系统资源包时,将与resources.arsc关联的Asset对象加入到Restable中
            // 此过程会解析resources.arsc文件。
            ALOGV("Parsing resources for %s", ap.path.string());
            mResources->add(ass, idmap, nextEntryIdx + 1, !shared);
        }
        onlyEmptyResources = false;

        if (!shared) {
            delete ass;
        }
    } else {
        mResources->addEmpty(nextEntryIdx + 1);
    }

    if (idmap != NULL) {
        delete idmap;
    }
    MY_TRACE_END();

    return onlyEmptyResources;
}

获取系统路径并解析resources.arsc这个文件映射表,其中存放的就是资源的索引。

至此,系统资源加载就完成了。

获取资源

现在所有资源的索引已经加载到内存中了,我们就需要获取资源并加载了。

<ImageView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:src="@drawable/image"/>

我们看一下ImageView的src属性图片是如何加载的,通常情况下,src属性和我们自定义属性很想,都是自定义的属性,只不过一个是系统定义的,一个是我们定义的,所有我们直接看ImageView的构造函数:

public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
       int defStyleRes) {
   super(context, attrs, defStyleAttr, defStyleRes);
   final TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);

   final Drawable d = a.getDrawable(R.styleable.ImageView_src);
    if (d != null) {
       setImageDrawable(d);
   }   
}

@Nullable
public Drawable getDrawable(@StyleableRes int index) {
   final TypedValue value = mValue;
   if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
       if (value.type == TypedValue.TYPE_ATTRIBUTE) {
           throw new UnsupportedOperationException(
                   "Failed to resolve attribute at index " + index + ": " + value);
       }
       return mResources.loadDrawable(value, value.resourceId, mTheme);
   }
   return null;
}

最后也是通过Resources的loadDrawable(value, value.resourceId, mTheme)方法来获取到资源:

// Resources.java
@NonNull
Drawable loadDrawable(@NonNull TypedValue value, int id, @Nullable Theme theme)
       throws NotFoundException {
   return mResourcesImpl.loadDrawable(this, value, id, theme, true);
}

// ResourcesImpl.java
@Nullable
Drawable loadDrawable(Resources wrapper, TypedValue value, int id, Resources.Theme theme,
       boolean useCache) throws NotFoundException {
   try {
       Drawable dr;
       if (cs != null) {
           dr = cs.newDrawable(wrapper);
       } else if (isColorDrawable) {
           dr = new ColorDrawable(value.data);
       } else {
           dr = loadDrawableForCookie(wrapper, value, id, null);
       }

       return dr;
   } catch (Exception e) {
       throw nfe;
   }
}

加载drawable的时候,首先会现在缓存中获取,如果有就直接返回,如果没有的话,说明没有加载过这个资源文件,就需要先加载并缓存到caches中,所有我们看loadDrawableForCookie方法:

private Drawable loadDrawableForCookie(Resources wrapper, TypedValue value, int id,
       Resources.Theme theme) {
   final String file = value.string.toString();

   final Drawable dr;

   Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, file);
   try {
       if (file.endsWith(".xml")) {
        // 类似于shape的xml文件
           final XmlResourceParser rp = loadXmlResourceParser(
                   file, id, value.assetCookie, "drawable");
           dr = Drawable.createFromXml(wrapper, rp, theme);
           rp.close();
       } else {
        // 不是xml文件,就是图片文件,就将其加载到内存中
        // value.assetCookie这个cookie就是图片在native层中的mAssetPaths中的索引
           final InputStream is = mAssets.openNonAsset(
                   value.assetCookie, file, AssetManager.ACCESS_STREAMING);
           dr = Drawable.createFromResourceStream(wrapper, value, is, file, null);
           is.close();
       }
   } catch (Exception e) {
       Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
       final NotFoundException rnf = new NotFoundException(
               "File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
       rnf.initCause(e);
       throw rnf;
   }
   Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);

   return dr;
}

loadDrawableForCookie方法会首先判断是否是.xml文件,如果是xml文件,则直接使用xml解析并创建Drawable对象返回,如果不是xml文件,则认为是图片的资源文件,使用流的方式将资源读取到缓存中。

至此为止,资源是如何加载的和如何从内存中获取资源都完成了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容