Android应用程序资源的查找过程分析

        我们知道,在Android系统中,每一个应用程序一般都会配置很多资源,用来适配不同密度、大小和方向的屏幕,以及适配不同的国家、地区和语言等等。这些资源是在应用程序运行时自动根据设备的当前配置信息进行适配的。这也就是说,给定一个相同的资源ID,在不同的设备配置之下,查找到的可能是不同的资源。这个资源查找过程对应用程序来说,是完全透明的。在本文中,我们就详细分析资源管理框架是如何根据ID来查找资源的。

《Android系统源代码情景分析》一书正在进击的程序员网(http://0xcc0xcd.com)中连载,点击进入!

        从前面Android应用程序资源管理器(Asset Manager)的创建过程分析一文可以知道,Android资源管理框架实际就是由AssetManager和Resources两个类来实现的。其中,Resources类可以根据ID来查找资源,而AssetManager类根据文件名来查找资源。事实上,如果一个资源ID对应的是一个文件,那么Resources类是先根据ID来找到资源文件名称,然后再将该文件名称交给AssetManager类来打开对应的文件的,这个过程如图1所示。


图1 应用程序查找资源的过程示意图

        在图1中,Resources类根据资源ID来查到资源名称实际上也是要通过AssetManager类来实现的,这是因为资源ID与资源名称的对应关系是由打包在APK里面的resources.arsc文件中的。当Resources类查找的资源对应的是一个文件的时候,它就会再次将资源名称交给AssetManager,以便后者可以打开对应的文件,否则的话,上一步找到的资源名称就是最终的查找结果。

        从前面Android应用程序资源的编译和打包过程分析一文可以知道,APK包里面的resources.arsc文件是在编译应用程序资源的时候生成的,然后连同其它被编译的以及原生的资源一起打包在一个APK包里面。

        从前面Android资源管理框架(Asset Manager)简要介绍和学习计划一文又可以知道,Android应用程序资源是可以划分是很多类别的,但是从资源查找的过程来看,它们可以归结为两大类。第一类资源是不对应有文件的,而第二类资源是对应有文件的,例如,字符串资源是直接编译在resources.arsc文件中的,而界面布局资源是在APK包里面是对应的单独的文件的。如上所述,不对应文件的资源只需要执行从资源ID到资源名称的转换即可,而对应有文件的资源还需要根据资源名称来打开对应的文件。在本文中,我们就以界面布局资源的查找过程为例,来说明Android资源管理框架查找资源的过程。

       我们知道,每一个Activity组件创建的时候,它的成员函数onCreate都会被调用,而在Activity组件的成员函数onCreate中,我们基本上都无一例外地调用setContentView来设置Activity组件的界面。在调用Activity组件的成员函数setContentView的时候,需要指定一个layout类型的资源ID,以便Android资源管理框架可以找到指定的Xml资源文件来填充(inflate)为Activity组件的界面。接下来,我们就从Activity类的成员函数setContentView开始,分析Android资源管理框架查找layout资源的过程,如图2所示。


图2 类型为layout的资源的查找过程

        这个过程可以分为22个步骤,接下来我们就详细分析每一个步骤。

        Step 1. Activity.setContentView

public class Activity extends ContextThemeWrapper
        implements LayoutInflater.Factory,
        Window.Callback, KeyEvent.Callback,
        OnCreateContextMenuListener, ComponentCallbacks {
    ......

    private Window mWindow;
    ......

    public Window getWindow() {
        return mWindow;
    }

    .....

    public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
    }

    ......
}

        这个函数定义在文件frameworks/base/core/java/android/app/Activity.java中。

        从前面Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析一文可以知道,Activity类的成员变量mWindow指向的是一个PhoneWindow对象,因此,Activity类的成员函数setContentView实际上是调用PhoneWindow类的成员函数setContentView来进一步操作。

        Step 2. PhoneWindow.setContentView

public class PhoneWindow extends Window implements MenuBuilder.Callback {
    ......

    // This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    private ViewGroup mContentParent;
    ......

    private LayoutInflater mLayoutInflater;
    ......

    @Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null) {
            cb.onContentChanged();
        }
    }

    ......
}
        这个函数定义在文件frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java中。

        PhoneWindow类的成员变量mContentParent用来描述一个类型为DecorView的视图对象,或者这个类型为DecorView的视图对象的一个子视图对象,用作UI容器。当它的值等于null的时候,就说明当前正在处理的Activity组件的视图对象还没有创建。在这种情况下,就会调用成员函数installDecor来创建当前正在处理的Activity组件的视图对象。否则的话,就说明是要重新设置当前正在处理的Activity组件的视图。在重新设置之前,首先调用成员变量mContentParent所描述的一个ViewGroup对象来移除原来的UI内容。

        PhoneWindow类的成员变量mLayoutInflater指向的是一个PhoneLayoutInflater对象。PhoneLayoutInflater类是从LayoutInflater类继续下来的,同时它也继承了LayoutInflater类的成员函数inflate。通过调用PhoneWindow类的成员变量mLayoutInflater所指向的一个PhoneLayoutInflater对象的成员函数inflate,也就是从父类继承下来的成员函数inflate,就可以将参数layoutResID所描述的一个UI布局设置到mContentParent所描述的一个视图容器中去。这样就可以将当前正在处理的Activity组件的UI创建出来。

        最后,PhoneWindow类的成员函数还会调用一个Callback接口的成员函数onContentChanged来通知当前正在处理的Activity组件,它的视图内容发生改变了。从前面Android应用程序窗口(Activity)的窗口对象(Window)的创建过程分析一文可以知道,每一个Activity组件都实现了一个Callback接口,并且将这个Callback接口设置到了与它所关联的PhoneWindow的内部去,因此,最后调用的实际上是Activity类的成员函数onContentChanged。

        接下来,我们就继续分析LayoutInflater类的成员函数inflate的实现,以便可以了解Android资源管理框架是如何找到参数layoutResID所描述的UI布局文件的。

        Step 3. LayoutInflater.inflate

public abstract class LayoutInflater {
    ......

    public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    ......

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        ......
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    ......
}

        这个函数定义在文件frameworks/base/core/java/android/view/LayoutInflater.java中。

        LayoutInflater类两个参数版本的成员函数inflate通过调用三个参数版本的成员函数inflate来查找参数resource所描述的UI布局文件。

        在LayoutInflater类三个参数版本的成员函数inflate中,首先是获得用来描述当前运行上下文环境的一个Resources对象,然后接调用这个Resources对象的成员函数getLayout来查找参数resource所描述的UI布局文件。

        Resources类的成员函数getLayout找到了指定的UI布局文件之后,就会打开它。由于Android系统的UI布局文件是一个Xml文件,因此,Resources类的成员函数getLayout打开它之后,得到的是一个XmlResourceParser对象。有了这个XmlResourceParser对象之后,LayoutInflater类三个参数版本的成员函数inflate就将它传递给另外一个三个参数版本的成员函数inflate,以便后者可以通过它来创建一个UI界面。

        接下来,我们就首先分析Resources类的成员函数getLayout的实现,然后再分析LayoutInflater类的另外一个三个参数版本的成员函数inflate的实现。

        Step 4. Resources.getLayout

public class Resources {
    ......

    public XmlResourceParser getLayout(int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
    }

    ......
}
        这个函数定义在文件frameworks/base/core/java/android/content/res/Resources.java中。

        Resources类的成员函数getLayout的实现很简单,它通过调用另外一个成员函数loadXmlResourceParser来查找并且打开由参数id所描述的一个UI布局文件。

        Step 5. Resources.loadXmlResourceParser

public class Resources {
    ......

    /*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
            throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                return loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
            throw new NotFoundException(
                    "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
                    + Integer.toHexString(value.type) + " is not valid");
        }
    }

    ......
}
        这个函数定义在文件frameworks/base/core/java/android/content/res/Resources.java中。

        参数id描述的是一个资源ID,Resources类的成员函数loadXmlResourceParser首先调用另外一个成员函数getValue来获得该资源ID所对应的资源值,并且保存在一个类型为TypedValue的变量value中。在我们这个情景中,参数id描述的是一个类型为layout的资源ID,从前面Android应用程序资源的编译和打包过程分析一文可以知道,类型为layout的资源ID对应的资源值即为一个UI布局文件名称。有了这个UI布局文件名称之后,Resources类的成员函数loadXmlResourceParser接着再调用另外一个四个参数版本的成员函数loadXmlResourceParser来加载对应的UI布局文件,并且得到一个XmlResourceParser对象返回给调用者。

        注意,如果Resources类的成员函数getValue没有找到与参数id所描述的资源,或者找到的资源的值不是字符串类型的,那么Resources类的成员函数loadXmlResourceParser就会抛出一个类型为NotFoundException的异常。

        接下来,我们就首先分析Resources类的成员函数getValue的实现,接着再分析Resources类四个参数版本的成员函数loadXmlResourceParser的实现。

        Step 6. Resources.getValue

public class Resources {
    ......

    /*package*/ final AssetManager mAssets;
    ......

    public void getValue(int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x"
                                    + Integer.toHexString(id));
    }

    ......
}

        这个函数定义在文件frameworks/base/core/java/android/content/res/Resources.java中。

        Resources类的成员变量mAssets指向的是一个AssetManager对象,Resources类的成员函数getValue通过调用它的成员函数getResourceValue来获得与参数id所对应的资源的值。注意,如果AssetManager类的成员函数getResourceValue查找不到与参数id所对应的资源,那么Resources类的成员函数getValue就会抛出一个类型为NotFoundException的异常。

        接下来,我们就继续分析AssetManager类的成员函数getResourceValue的实现。

        Step 7. AssetManager.getResourceValue

public final class AssetManager {
    ......

    private StringBlock mStringBlocks[] = null;
    ......

    /*package*/ final boolean getResourceValue(int ident,
                                               TypedValue outValue,
                                               boolean resolveRefs)
    {
        int block = loadResourceValue(ident, outValue, resolveRefs);
        if (block >= 0) {
            if (outValue.type != TypedValue.TYPE_STRING) {
                return true;
            }
            outValue.string = mStringBlocks[block].get(outValue.data);
            return true;
        }
        return false;
    }

    ......
}

        这个函数定义在文件frameworks/base/core/java/android/content/res/AssetManager.java中。

        AssetManager类的成员函数getResourceValue通过调用另外一个成员函数loadResourceValue来加载参数ident所描述的资源。如果加载成功,那么结果就会保存在参数outValue所描述的一个TypedValue对象中,并且AssetManager类的成员函数loadResourceValue的返回值block大于等于0。

        从前面Android应用程序资源管理器(Asset Manager)的创建过程分析一文可以知道,AssetManager类的成员变量mStringBlock描述的是一个StringBlock数组。这个StringBlock数组中的每一个StringBlock对象描述的都是当前应用程序使用的每一个资源索引表的资源项值字符串资源池。关于资源索引表的格式以及生成过程,可以参考前面Android应用程序资源的编译和打包过程分析一文。

        了解了上述背景之后,我们就可以知道,当AssetManager类的成员函数loadResourceValue的返回值block大于等于0的时候,实际上就表示参数ident所描述的资源项在当前应用程序使用的第block个资源索引表中,而当参数ident所描述的资源项是一个字符串时,那么就可以在第block个资源索引表的资源项值字符串资源池中找到对应的字符串,并且保存在参数outValue所描述的一个TypedValue对象的成员变量string中,以便返回给调用者使用。注意,最终得到的字符串在第block个资源索引表的资源项值字符串资源池中的位置就保存在参数outValue所描述的一个TypedValue对象的成员变量data中。

        接下来,我们就继续分析AssetManager类的成员函数loadResourceValue的实现。

        Step 8. AssetManager.loadResourceValue

public final class AssetManager {
    ......

    /** Returns true if the resource was found, filling in mRetStringBlock and
     *  mRetData. */
    private native final int loadResourceValue(int ident, TypedValue outValue,
                                               boolean resolve);

    ......
}
        这个函数定义在文件frameworks/base/core/java/android/content/res/AssetManager.java中。

        AssetManager类的成员函数loadResourceValue是一个JNI方法,它是由C++层的函数android_content_AssetManager_loadResourceValue来实现的,如下所示:

static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                           jint ident,
                                                           jobject outValue,
                                                           jboolean resolve)
{
    AssetManager* am = assetManagerForJavaObject(e
阅读终点,创作起航,您可以撰写心得或摘录文章要点写篇博文。去创作
  • 36
    点赞
  • 108
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 35
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 35
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

罗升阳

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值