Android资源加载源码分析

在android开发过程中,我们会用到/res目录下的文件(图片,颜色,布局文件等),通过getResource()方法我们能很方便的使用这个资源。这些资源会被一起打包到apk文件中,如下图:

1493133495(1).png

真正的资源放在res目录下,Android应用程序资源的编译和打包之后就生成一个资源索引文件resources.arsc,通过resources.arsc能过准确的找到对应的资源文件,关于resources.arsc的详细解释,可参考这般文章http://blog.csdn.net/beyond702/article/details/49228115

那这些资源是怎么加载到,又是何时加载到应用程序中的呢?这篇文章想跟大家分享的就是Android中资源的加载和匹配。

在app启动过程中,在什么时候进行app资源的加载(图片,布局文件,颜色等)? 如何将资源与应用关联在一起?

代码中通过getResources()获得Resoures对象,然后通过Resource的相关方法进行获取相关资源,所以我们看Resoures对象是在什么时候创建的就能推断出app资源加载的节点,看源码:

@Override
public Resources getResources() {
    return mBase.getResources();
}

我看到我们在Activity中调用的getResources()方法其实是mBase.getResources(),也就是说调用的mBase中的getResources()方法,而mBase的数据类型其实是Context,是一个抽象类。代码如下:

public class ContextWrapper extends Context {
Context mBase;

public ContextWrapper(Context base) {
    mBase = base;
}

/**
 * Set the base context for this ContextWrapper.  All calls will then be
 * delegated to the base context.  Throws
 * IllegalStateException if a base context has already been set.
 * 
 * @param base The new base context for this wrapper.
 */
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}
什么是Context?

说到Context,作为一个android工程师肯定很熟悉,在开发过程中,Context是一个非常重要的的类型,Context意为上下文,也就是程序的运行环境。它分装了很多重要的操作,如startActiviy()、sendBroadcast()、bindService()等,因此,Context对开发者来说最重要的高层接口。Context只是一个定义了很多接口的抽象类,这些接口的真正实现其实是通过Context的子类ContextImpl类中,这种设计模式叫做外光模式(感兴趣的同学可以去研究一下什么是“外观模式”)。

先看看ContextImpl的构造方法,看源码:

private ContextImpl(ContextImpl container, ActivityThread mainThread,
    LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
    Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
//获取包的信息
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
 /代码省略/
if (compatInfo == null) {
    compatInfo = (displayId == Display.DEFAULT_DISPLAY)
            ? packageInfo.getCompatibilityInfo()
            : CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
//获取对应资源
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
    if (displayId != Display.DEFAULT_DISPLAY
            || overrideConfiguration != null
            || (compatInfo != null && compatInfo.applicationScale
                    != resources.getCompatibilityInfo().applicationScale)) {

        if (container != null) {
            // This is a nested Context, so it can't be a base Activity context.
            // Just create a regular Resources object associated with the Activity.
            resources = mResourcesManager.getResources(
                    activityToken,
                    packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(),
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    packageInfo.getClassLoader());
        } else {
            // This is not a nested Context, so it must be the root Activity context.
            // All other nested Contexts will inherit the configuration set here.
            resources = mResourcesManager.createBaseActivityResources(
                    activityToken,
                    packageInfo.getResDir(),
                    packageInfo.getSplitResDirs(),
                    packageInfo.getOverlayDirs(),
                    packageInfo.getApplicationInfo().sharedLibraryFiles,
                    displayId,
                    overrideConfiguration,
                    compatInfo,
                    packageInfo.getClassLoader());
        }
    }
}
mResources = resources;
}

在ContextImpl的构造方法中会初始化化该进程的各个字段,例如资源、包信息、屏幕配置等。通过packageInfo可以得到Resources。所以我们发现要知道app资源的加载时间节点取决于ContextImpl的创建时间。

我们来看看 ContextImpl是什么时候赋值给Activity的mBase的?

先来讲讲app的启动,app在启动是,首先会fork一个子进程,并且调用ActivityThread.mian方法启动该进程。ActivityThread又会构建Application对象,然后和Activity、ContextImpl关联起来,最后会调用Activity的onCreate()、onStart()、onResume()函数使Activity运行起来 ,此时app的界面就出现在我们面前了。main函数会间接地调用ActivityThread中的handleLaunchActivity函数启动默认的Activity,handleLaunchActivity代码如下:

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
//代码省略
 Activity a = performLaunchActivity(r, customIntent);
//代码省略
}

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// 代码省略
Activity activity = null;
try {
    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
//创建Activity
    activity = mInstrumentation.newActivity(
            cl, component.getClassName(), r.intent);
    StrictMode.incrementExpectedActivityCount(activity.getClass());
    r.intent.setExtrasClassLoader(cl);
    r.intent.prepareToEnterProcess();
    if (r.state != null) {
        r.state.setClassLoader(cl);
    }
} catch (Exception e) {
    if (!mInstrumentation.onException(activity, e)) {
        throw new RuntimeException(
            "Unable to instantiate activity " + component
            + ": " + e.toString(), e);
    }
}
try {
//创建Application
    Application app = r.packageInfo.makeApplication(false, mInstrumentation)

    if (activity != null) {
   //构建ContextImpl   createBaseContextForActivity方法返回的是ContextImpl对象
        Context appContext = createBaseContextForActivity(r, activity);
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        
 //建立Activity与ContextImpl、Application的关联 
        activity.attach(appContext, this, getInstrumentation(), r.token,
                r.ident, app, r.intent, r.activityInfo, title, r.parent,
                r.embeddedID, r.lastNonConfigurationInstances, config,
                r.referrer, r.voiceInteractor, window); 
//代码省略
       
//回调Activity的onCreate方法
        if (r.isPersistable()) {
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }
} catch (SuperNotCalledException e) {
    throw e;
} catch (Exception e) {
    if (!mInstrumentation.onException(activity, e)) {
        throw new RuntimeException(
            "Unable to start activity " + component
            + ": " + e.toString(), e);
    }
}
return activity;
}

从代码中我可以看到,每次创建Activiy都会创建ContextImpl与这个activity关联起来,当然关联起来的还有Application,只是Application只会创建一次。ContextImpl最终会被ContentWrapper类的mBase字段引用。

总结一下,获取资源的操作实际上是由ContextImpl来完成的,Activity、Service等组件的getResource方法最终都会转发给ContextImpl类型的mBase字段。也就是调用了ContextImpl的getResource函数,而这个Resource在ContextImpl关联到 Activity之前就会初始化Resource对象。

Android资源是如何进行匹配的

根据上面的内容我们知道在ContextImpl构造函数中进行Resource的初始化,那我们看看 Resource是如何进行初始化的。

private ContextImpl(ContextImpl container, ActivityThread mainThread,
    LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
    Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mPackageInfo = packageInfo;
//代码省略
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
        || overrideConfiguration != null
        || (compatInfo != null && compatInfo.applicationScale
                != resources.getCompatibilityInfo().applicationScale)) {

    if (container != null) {
        // This is a nested Context, so it can't be a base Activity context.
        // Just create a regular Resources object associated with the Activity.
//根据设备配置获取对应的资源 
        resources = mResourcesManager.getResources(
                activityToken,
                packageInfo.getResDir(),
                packageInfo.getSplitResDirs(),
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                packageInfo.getClassLoader());
    } else {
        // This is not a nested Context, so it must be the root Activity context.
        // All other nested Contexts will inherit the configuration set here.
        resources = mResourcesManager.createBaseActivityResources(
                activityToken,
                packageInfo.getResDir(),
                packageInfo.getSplitResDirs(),
                packageInfo.getOverlayDirs(),
                packageInfo.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfiguration,
                compatInfo,
                packageInfo.getClassLoader());
    }
}
}
mResources = resources;
}

在通过mPackageInfo得到对应的资源之后,最终都会调用ResourceManager的中的方法来根据设备配置等相关信息获取对应的资源,也就是资源的适配。已ResourceManager中的getResources方法为例,代码如下:

public @Nullable 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");
//已apk路径、屏幕设备id、配置等构建一个资源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();
//根据这个key和ativityToke在mActivityResourceReferences中查看是否加载过这个资源,如果     
//有直接返回,如果没有加载过生成一个Resource返回并保存到 mActivityResourceReferences
//中。
    return getOrCreateResources(activityToken, key, classLoader);
} finally {
    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}

在android23之前版本ResourcesManager通过mActiveResources字段管理Resource,它的数据类型是ArrayMap<ResourcesKey, WeakReference<Resources> >,在android23之后ResourcesManager通过mActivityResourceReferences字段管理Resources,它的数据类型是WeakHashMap<IBinder, ActivityResources>,ActivityResources的结构代码如下:

private static class ActivityResources {
public final Configuration overrideConfig = new Configuration();
public final ArrayList<WeakReference<Resources>> activityResources = new ArrayList<>();
}

已android23之前Resources的管理方式为例说明,代码如下:

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) {
//判断是否加载过该资源
    WeakReference<Resources> wr = mActiveResources.get(key);
    r = wr != null ? wr.get() : null;
    if (r != null && r.getAssets().isUpToDate()) {
    //已加载过,直接返回
        return r;
    }
}
//没有加载过,构建AssetManager对象
AssetManager assets = new AssetManager();
   //将APK路径添加到AssetManager的资源路径中
if (resDir != null) {
    if (assets.addAssetPath(resDir) == 0) {
        return null;
    }
}
//屏幕分辨率
DisplayMetrics dm = getDisplayMetricsLocked(displayId);
//设备配置
Configuration config;
//创建Resources
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) {
    //缓存资源 
    mActiveResources.put(key, new WeakReference<Resources>(r));
    return r;
}
}

首先会以APK路径、屏幕设备id、配置等构建一个资源key,根据这个key到ResourcesManager类的mActiveResources中查询是否 加载已经加载过该Apk资源,如果含有缓存那么直接使用缓存。这个mActiveResources维护了当前应用程序进程中加载的每一个APK文件及其对应的Resources对象的对应关系。如果没有缓存,那么就会创建一个,并且保存在mActiveResources中。

在没有资源缓存的情况下,ActivityThread会创建一个AssetManager对象,并且调用AssetManager对象的addAssetPath函数来将参数resDir作为它的资源目录,这个Dir就是Apk文件的绝对路径。创建了一个新的AssetManager对象之后,会将这个AssetManager对象作为Resource构造的第一个参数来构建一个新的Resources对象。这个新创建的Resources对象会以前面所创建的ResourcesKey对象为键值缓存在mActiveResources所描述的一个HashMap中,以便重复使用该资源时无需重复创建。

总结

以上就是关于android资源加载和匹配的源码分析,通过对资源加载机制的学习,可以帮助我们重更深的角度理解一个app在android系统中的运行原理。据我说知,目前很多android插件技术也是基于android的资源加载机制实现的。最后希望我的文章能对大家有所帮助,文中有任何错误欢迎大家指出。

参考:
1.《Android源码设计模式解析与实战》
2.Android源码

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

推荐阅读更多精彩内容