Android 资源加载与匹配

Android 应用程序资源编译和打包之后就生成一个资源索引表文件 resources.arsc,这个应用程序资源会被打包到 APK 文件中。Android 应用程序在运行过程中,通过一个 Resource 来获取资源,实际上 Resource 内部是通过 AssetManager 来获取资源的。

ContextImpl

获取资源的操作最终试游 ContextImpl 来完成的,Activity,Service 等组件的 getResource 函数最终都转发给了 ContextImpl 类型的 mBase 字段,也就行先调用 ContextImple 的 getResource 先获取 Resouce ,然后在进行其他的操作,而 Resource 在 ContextImple 关联到 Activity 之前,就会初始化 Resource 对象,代码如下:

 private final Activity performLaunchActivity(ActivityClientRecord r,Intent customIntent){
        //...
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            //create Activity
            activity = mInstrumentation.newActivity(cl,compoent.getClassName(),r.intent);
            //...
        }catch (Exception e){
            //..
        }
        try {
            //create Application
            Application app = r.packageInfo.makeApplication(false,mInstrumentation);
            if (activity != null){
                //create ContextImpl
                ContextImpl  appContext = createBaseContextForActivity(r,activity);
                //...
            }
        }catch(Exception e){
            //...
        }
    }
    
    
    private Context createBaseContextForActivity(ActivityClientRecord r,final  Activity activity){
        //create ContextImpl
        ContextImpl appContext = ContextImpl.createActivityContext(this,r.packageInfo,r.token);
        //Outter Context is Activity
        appContext.setOuterContext(activity);
        Context baseContext = appContext;
        //...
        return baseContext;
    }

在上述 performLaunchActivity 函数中:
1、首先创建了 Activity 和 Application 对象
2、通过 createBaseContextForActivity 创建了一个 ContextImpl 对象
在 createBaseContextForActivity 中会调用 ContextImple 类的静态函数 createActivityContext 函数。

static ContextImpl craeteActivityContext(ActivityThread mainThread,LoadeApk packageInfo,IBinder activityToken){
    //...
    return new ContextImpl(null,mainThread,packageInfo,activityToken,null,false,null,null);
}

private ContextImpl(ContextImpl container,ActivityThread mainThread,LoadeApk packageInfo,IBinder activityToken,
                    UserHandler user,boolean restricted,Display  dispaly,Configuration overrideConfiguration){
    
    mOuterContext = this;
    mainThread = mainThread;
    
    //获取包信息
    mPackageInfo = packageInfo;
    //获取资源管理器
    mResourcesManager = ResoucesManager.getInstance();
    // ...
    
    Resouces resources = packageInfo.getResouce(mainThread);
    if(resources != null){
        if(activityToken != null || dispaly != null || overrideConfiguration != null || 
        (compatInfo != null && compatInfo.applicationScale != resources.getCompatibilityInfo().applicationScale)){
            //根据设备配置获取对应的资源
            resources = mResourcesManager.getTopLevelResource(packageInfo.getResDir(),
            packageInfo.getApplicationInfo().sharedLibraryFiles,displayId,overrideConfiguration,compatInfo,activityToken
            );
        }
    }
    mResources = resources;
    //...
}

createActivityContext 函数中最终调用了 ContextImpl 的构造函数,在该函数中会初始化该进程的各个字段,例如资源、包信息、屏幕配置等。这里只关心与资源相关的代码。在通过 mPackageInfo 得到对应的资源之后,会调用 ResoucesManager 的 getTopLevelResources 来根据设备配置等相关信息来得到对应的资源,也就是资源的适配。getTopLevelResources 代码如下:

public Resources getTopLevelResouces(String resDir,String[] splitResDirs,
        String[] overlayDirs,String[] libDirs,int dispaly,Configuration overrideConfiguration,CompatibilityInfo compatInfo,IBinder token)
        {
        final float scale = compatInfo.applicationScale;
        ResoucesKey key = new ResoucesKey(resDir,dispalyId,overrideConfiguration,scale,token);
        Resouces r;
        sychronized(this){
            // 判断是否已经加载过该资源
            WeakReference wr = mActivityResouces.get(key);
            r = wr != null ?wr.get() : null;
            if(r != null && r.getAssets().isUpToDate()){
                return r;
            }
        }
        // ...
        //2、还未加载过,则构建 AssetManager 对象
        AssetManager assets = new AssertManager();
        //3、将 APK 路径添加到 AssetManager 的 资源路径中
        if(resDir != null){
            if(assets.addAssetPath(resDir) == 0){
                return null;
            }
        }
        
        //...
        // 屏幕分辨率
        DisplayMetrics dm = getDisplayMetricsLocked(dispalyId);
        //设备配置
        Configuration config;
        //创建资源,参数 1 为 AssetManager
        r = new Resouces(assets,dm,config,compatInfo,token);
        sychronized(this){
        //缓存资源
        mActivityResouces.put(key,new WeakReference(r));
        return r;
        }
    
}

1、以 APK 路径,屏幕设备 id,配置等构建一个资源 key,根据这个 key 到 ActivityThread 类 mActivityResouces(HashMap 类型)中查询是否已经加载过 APK 资源,如果有,直接使用缓存,如果没有缓存,就创建一个并缓存。

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

AssetManager

接下来分析 AssetManager 类的构造函数和成员函数 addAssetPath 的实现,接着再 分析 Resouces 类的构造函数的实现。

    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 static void ensureSystemAssets() {
        synchronized (sSync) {
            if (sSystem == null) {
                //系统资源的 AssetManager 对象
                AssetManager system = new AssetManager(true);
                system.makeStringBlocks(null);
                sSystem = system;
            }
        }
    }

1、在 AssetManager 的构造函数中,会调用 init 函数初始化,这个函数是 Native 层方法
2、调用 ensureSystemAssets 函数来加载系统资源,这些资源保存在 mSystem 对象中。

init 函数:

static void android_content_AssetManager_init(JNIEvn* env,jobject clazz){
    
    if(isSystem){
        verifySystemIdmaps();
    }
    // 创建 Native  的 AssetManager
    AssertManager * am = new AssertManager();
    //添加默认的资源
    am->addDefaultAssets();
    //将 Native 层的 AssetManager 对象存储到 Java 层 的 AssetManager 的 mObject 字段中
    env-> SetIntFieLd(clazz,gAssetManagerOffsets.mObject,(jint)am);
}

在 init 函数中,首先创建了一个 Native 层的 AssetManager 对象,然后添加了默认的系统资源,最后将这个 AssetManager 对象转换为整数型并且传递给 java 层的 AssetManager 的 mObject 字段,这样 Java 层就相当于存有了一个 Native 层 AssetManager 的句柄。


static const char * kSystemAssets = "framework/framework-res.apk";

bool AssertManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    //构建系统路径
    String8 path(root);
    //系统路径 _+ framework 资源 APK 路径,拼接成完整路径
    path.appendPath(kSystemAssets);
    return addAssetPath(path,NULL);
}

代码也比较简单,就是拼接系统资源路径,最后将该路径传递给 addAssetPath 函数中,实际上就是将该系统路径添加到 AssetManager 的资源路径列表中。


bool AssertManager:;addAssetPath(const String8& path,void ** cookie){
    AutoMutex _1(mLock);
    assets_path ap;
    Sting8 realPath(path);
    
    if(kAppZipName){
        realPath.appendPath(kAppZipName);
    }
    //判断  path 路径是否存在以及是否是文件类型,如果路径不合法则直接返回 false
    //ap.type = ::getFileType(realPath.string());
    if(ap.type == kFileTypeRegular){
        ap.path = realPath;
    }else{
        ap.path = path;
        ap.type = ::getFileType(path.string());
        if(ap.type != kFileTypeDierctory && ap.type != kFileTypeRegular){
            // 。。。
            return false;
        }
    }
    
    //添加到资源路径列表中
    mAssetPath.add(ap);
    //..
    return true;
}

在 java 层的 AssetManager 的 addAssetPath 函数实际上是调用 Native 层的 addAssetPathNative 函数。这个 addAssetPathNative 函数在 Native 对应的 android_content_AssetManager_addAssetPath 函数中。这个函数会从 Java 层的 AssetManager 对象中获取到 mObject 字段,该字段存储了由 Native 层 AssetManager 指针转换而来的整型值。然后通过这个值逆向转换为 Nativie 层的 AssetManager 对象。然后将 APK 路径添加到资源路径中。

总结一下就是,首先在 Java 层的 AssetManager 构造函数中调用了 init 函数初始化系统资源,创建与初始化完毕之后会调用 Java 层的 AssetManager 对象的 addAssetPath 函数将 APK 路径传递给 Native 层的 AssetManager ,使得 资源被添加到 Native 的 AssetManager 中。
最后,在 ResoucesManager 的 getTopLevelResouces 函数中将初始化好的 AssetManager、设备配置等做为参数构造 Resouces 对象。
如下:



public Resouces(AssertManager assets,DisplayMetrics metrics,Configuration config,CompatibilityInfo compatInfo,IBinder token){
    mAsssets = assets;
    mMetrics.setToDefaults();
    //更新配置
    updateConfiguration(config,metrics);
    assets.ensureStringBlocks();
}

在 Resouces 构造函数中,会调用 updateConfiguration 来更新配置,然后调用 assets.ensureStringBlocks() 创建字符串资源池。

总结:
应用的资源存储在 APK中,一个 AssetManager 可以管理多个路径的资源文件,Resouce 通过 AsssetManager 获取资源。那么,我们可以通过 AssetManager 的 addAssetPath 方法添加 APK 路径,可以达到资源替换的目的。

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

你可能感兴趣的:(Android 资源加载与匹配)