转载地址:http://blog.csdn.net/yuanzeyao/article/details/42386549
在Android中,所有的资源都在res目录下存放,包括drawable,layout,strings,anim等等,当我们向工程中加入任何一个资源时,会在R类中相应会为该 资源分配一个id,我们在应用中就是通过这个id来访问资源的,相信做过Andorid开发的朋友对于这些肯定不会陌生,所以这个也不是我今天想要说的,我今天想和大家一起学习的是Android是如何管理资源的,在Android系统中,资源大部分都是通过xml文件定义的(drawable是图片),如layout,string,anim都是xml文件,而对于layout,anim和strings等xml文件仅仅是解析xml文件,读取指定的值而已,但是对于layout文件中控件的解析就比较复杂了,例如对于一个Button,需要解析它所有的属性值,这个是如何实现的呢。
这里我们首先要考虑一个问题,就是一个控件有哪些属性是如何定义的?比如TextView具有哪些属性?为什么我设置TextView的样式只能用style而不能用android:theme?这些信息都是在哪里定义的,想要弄清楚这个问题,就必须从源码工程招答案,我使用的是android4.1工程,如果你使用的是其他版本的,那么可能用些出入。
先看三个文件
1、d:\android4.1\frameworks\base\core\res\res\values\attrs.xml
看到attrs.xml文件,不知道你有没有想起什么?当我们在自定义控件的时候,是不是会创建一个attrs.xml文件?使用attrs.xml文件的目的其实就是给我们自定义的控件添加属性,打开这个目录后,你会看到定义了一个叫"Theme"的styleable,如下(我只截取部分)
- <declare-styleable name="Theme">
-
-
-
- <eat-comment />
-
-
- <attr name="colorForeground" format="color" />
-
- <attr name="colorForegroundInverse" format="color" />
-
- <attr name="colorBackground" format="color" />
在这个文件中,定义了Android中大部分可以使用的属性,这里我说的是“定义”而不是“声明”,同名在语法上面最大的区别就是定义要有format属性,而声明没有format属性。
2、d:\android4.1\frameworks\base\core\res\res\values\attrs_manifest.xml
这个文件的名字和上面的文件的名字很像,就是多了一个manifest,故名思议就是定义了AndroidManifest.xml文件中的属性,这里面有一个很重要的一句话
- <attr name="theme" format="reference" />
定义了一个theme属性,这个就是我们平时在Activity上面使用的theme属性
3、d:\android4.1\frameworks\base\core\res\res\values\themes.xml
这个文件开始定义了一个叫做"Theme" 的sytle,如下(截图部分)
- <style name="Theme">
-
- <item name="colorForeground">@android:color/bright_foreground_dark</item>
- <item name="colorForegroundInverse">@android:color/bright_foreground_dark_inverse</item>
- <item name="colorBackground">@android:color/background_dark</item>
- <item name="colorBackgroundCacheHint">?android:attr/colorBackground</item>
这个就是我们平时在Application或者Activity中使用的Theme,从这里可以看出,Theme也是一种style,那为什么style只能永远View/ViewGorup,而Theme只能用于Activity或者Application呢?先记住此问题,我们后续会为你解答
我们再来整合这三个文件的内容吧,首先在attrs.xml文件中,定义了Android中大部分的属性,也就是说以后所有View/Activity中大部分的属性就是在这里定义的,然后在attrs_manifest.xml中定义了一个叫做theme的属性,它的值就是再themes文件中定义的Theme或者继承自“Theme”的style。
有了上面的知识后,我们再来分析上面说过的两个问题:
1、TextView控件(其他控件也一样)的属性在哪里定义的。
2、既然Theme也是style,那为什么View只能用style,Activity只能使用theme?
所有View的属性定义都是在attrs.xml文件中的,所以我们到attrs.xml文件中寻找TextView的styleable吧
- <declare-styleable name="TextView">
- <!-- Determines the minimum type that getText() will return.
- The default is "normal".
- Note that EditText and LogTextBox always return Editable,
- even if you specify something less powerful here. -->
- <attr name="bufferType">
- <!-- Can return any CharSequence, possibly a
- Spanned one if the source text was Spanned. -->
- <enum name="normal" value="0" />
-
- <enum name="spannable" value="1" />
-
- <enum name="editable" value="2" />
- </attr>
-
- <attr name="text" format="string" localization="suggested" />
-
- <attr name="hint" format="string" />
-
- <attr name="textColor" />
上面的属性我只截取了部分,请注意,这里所有的属性都是进行“声明”,你去搜索这个styleable,会发现在TextView的styleable中不会找到theme这个属性的声明,所以你给任何一个view设置theme属性是没有效果的。请看下面一段代码就知道为什么了。
定义一个attrs.xml
- <?xml version="1.0" encoding="utf-8"?>
- <resources>
- <declare-styleable name="MyTextView">
- <attr name="orientation">
- <enum name="horizontal" value="0" />
- <enum name="vertical" value="1" />
- </attr>
- </declare-styleable>
- </resources>
定义一个MyTextView
- public class MyTextView extends TextView {
- private static final String TAG = "MyTextView";
- public MyTextView(Context context)
- {
- super(context);
- }
- public MyTextView(Context context, AttributeSet attrs)
- {
- super(context, attrs);
-
- TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.MyTextView);
- String value=ta.getString(R.styleable.MyTextView_orientation);
- Log.d("yzy", "value1--->"+value);
- ta.recycle();
- }
- }
在attrs.xml我为MyTextView定义了一个orientation属性,然后再MyTextView的构造函数中去读取这个属性,这里就涉及到TypeArray这个类,我们发现得到TypeArray需要传入R.style.MyTextView这个值,这个就是系统为我们访问MyTextView这个styleable提供的一个id,当我们需要拿到orientation这个属性的值时,我们通过R.style.MyTextView_orientation拿到,由于MyTextView中没有定义或者声明theme属性,所以我们找不到R.styleable.MyTextView_theme这个id,所以导致我们无法解析它的theme属性。同样回到TextView这个styleable来,由于TextView的styleable中没有定义theme属性,所以theme对于TextView是没有用的。所以即使你在TextView里面加入theme属性,即使编译器不会给你报错,这个theme也是被忽略了的。
我们再来看看Activity的属性是如何定义的,由于Activity是在AndroidManigest.xml文件中定义的,所以我们到attrs_manifest.xml中查找。
- <declare-styleable name="AndroidManifestActivity" parent="AndroidManifestApplication">
- <!-- Required name of the class implementing the activity, deriving from
- {@link android.app.Activity}. This is a fully
- qualified class name (for example, com.mycompany.myapp.MyActivity); as a
- short-hand if the first character of the class
- is a period then it is appended to your package name. -->
- <attr name="name" />
- <attr name="theme" />
- <attr name="label" />
- <attr name="description" />
- <attr name="icon" />
- <attr name="logo" />
- <attr name="launchMode" />
- <attr name="screenOrientation" />
- <attr name="configChanges" />
- <attr name="permission" />
- <attr name="multiprocess" />
- <attr name="process" />
- <attr name="taskAffinity" />
- <attr name="allowTaskReparenting" />
- <attr name="finishOnTaskLaunch" />
- <attr name="finishOnCloseSystemDialogs" />
- <attr name="clearTaskOnLaunch" />
- <attr name="noHistory" />
- <attr name="alwaysRetainTaskState" />
- <attr name="stateNotNeeded" />
- <attr name="excludeFromRecents" />
- <!-- Specify whether the activity is enabled or not (that is, can be instantiated by the system).
- It can also be specified for an application as a whole, in which case a value of "false"
- will override any component specific values (a value of "true" will not override the
- component specific values). -->
- <attr name="enabled" />
- <attr name="exported" />
- <!-- Specify the default soft-input mode for the main window of
- this activity. A value besides "unspecified" here overrides
- any value in the theme. -->
- <attr name="windowSoftInputMode" />
- <attr name="immersive" />
- <attr name="hardwareAccelerated" />
- <attr name="uiOptions" />
- <attr name="parentActivityName" />
- </declare-styleable>
很明显,Activity对于的styleable中是声明了theme的,所以它可以解析theme属性。
上面两个问题都已经解答完了,下面来讨论另一个话题,就是Resources的获取过程。
在我的另外一篇文章曾经讨论过这个话题更深层次理解Context 这里我们再来学习一下Resources的获取过程。
在Android系统中,获取Resources主要有两种方法,通过Context获取和PackageManager获取
首先,我们看看我们通过Context获取,下面这张图是Context相关类的类图
![](https://img-blog.csdn.net/20150104103842705?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVhbnpleWFv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
从图中可以看出,Context有两个子类,一个是ContextWrapper,另一个是ContextImpl,而ContextWrapper依赖于ContextImpl。结合源码,我们会发现,Context是一个抽象类,它的真正实现类就是ContextImpl,而ContextWrapper就像他的名字一样,仅仅是对Context的一层包装,它的功能都是通过调用属性mBase完成,该mBase实质就是指向一个ContextImpl类型的变量。我们获取Resources时就是调用Context的getResources方法,那么我们直接看看ContextImpl的getResources方法吧
- @Override
- public Resources getResources() {
- return mResources;
- }
我们发现这个方法很简单,就是返回mResources属性,那么这个属性是在哪里 赋值的呢,通过寻找发现,其实就是在创建ContextImpl,通过调用Init进行赋值的(具体逻辑参照《更深层次理解Context》).这里我先给出getResource方法的时序图,然后跟踪源码。
![](https://img-blog.csdn.net/20150104110812840?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveXVhbnpleWFv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center)
先从init方法开始吧
- final void init(LoadedApk packageInfo,
- IBinder activityToken, ActivityThread mainThread,
- Resources container, String basePackageName) {
- mPackageInfo = packageInfo;
- mBasePackageName = basePackageName != null ? basePackageName : packageInfo.mPackageName;
- mResources = mPackageInfo.getResources(mainThread);
-
- if (mResources != null && container != null
- && container.getCompatibilityInfo().applicationScale !=
- mResources.getCompatibilityInfo().applicationScale) {
- if (DEBUG) {
- Log.d(TAG, "loaded context has different scaling. Using container's" +
- " compatiblity info:" + container.getDisplayMetrics());
- }
- mResources = mainThread.getTopLevelResources(
- mPackageInfo.getResDir(), container.getCompatibilityInfo());
- }
- mMainThread = mainThread;
- mContentResolver = new ApplicationContentResolver(this, mainThread);
-
- setActivityToken(activityToken);
- }
我们发现,对mResource进行赋值,是通过调用LoadedApk中的getResource进行的,传入了ActivityThead类型的参数
- public Resources getResources(ActivityThread mainThread) {
- if (mResources == null) {
- mResources = mainThread.getTopLevelResources(mResDir, this);
- }
- return mResources;
- }
在getResources方法中,其实就是调用了ActivityThrad的getTopLevelResources方法,其中mResDir就是apk文件的路径(对于用户安装的app,此路径就在/data/app下面的某一个apk),从时序图中可以知道,getTopLevelResources其实就是调用了一个同名方法,我们直接看它的同名方法吧
- Resources getTopLevelResources(String resDir, CompatibilityInfo compInfo) {
- ResourcesKey key = new ResourcesKey(resDir, compInfo.applicationScale);
- Resources r;
- synchronized (mPackages) {
-
- if (false) {
- Slog.w(TAG, "getTopLevelResources: " + resDir + " / "
- + compInfo.applicationScale);
- }
- WeakReference<Resources> wr = mActiveResources.get(key);
- r = wr != null ? wr.get() : null;
-
- if (r != null && r.getAssets().isUpToDate()) {
- if (false) {
- Slog.w(TAG, "Returning cached resources " + r + " " + resDir
- + ": appScale=" + r.getCompatibilityInfo().applicationScale);
- }
- return r;
- }
- }<span style="font-family: Arial, Helvetica, sans-serif;">;</span>
-
-
-
-
-
-
-
- AssetManager assets = new AssetManager();
- if (assets.addAssetPath(resDir) == 0) {
- return null;
- }
-
-
- DisplayMetrics metrics = getDisplayMetricsLocked(null, false);
- r = new Resources(assets, metrics, getConfiguration(), compInfo);
- if (false) {
- Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
- + r.getConfiguration() + " appScale="
- + r.getCompatibilityInfo().applicationScale);
- }
-
- synchronized (mPackages) {
- WeakReference<Resources> wr = mActiveResources.get(key);
- Resources existing = wr != null ? wr.get() : null;
- if (existing != null && existing.getAssets().isUpToDate()) {
-
-
- r.getAssets().close();
- return existing;
- }
-
-
- mActiveResources.put(key, new WeakReference<Resources>(r));
- return r;
- }
- }
这段代码的逻辑不复杂,首先从mActiveResouuces中通过key拿到资源,如果资源不为null,并且是最新的,那么直接返回,否则创建一个AssetManager对象,并调用AssetManager的addAssetPath方法,然后使用创建的AssetManager为参数,创建一个Resources对象,保存并返回。通过上面的时序图,我们发现在创建AssetManager的时候,在其构造函数中调用init方法,我们看看init方法做了什么吧
- private native final void init();
居然是一个本地方法,那么我们只有看看对应的Jni代码了
- static void android_content_AssetManager_init(JNIEnv* env, jobject clazz)
- {
- 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->SetIntField(clazz, gAssetManagerOffsets.mObject, (jint)am);
- }
这个里面调用了本地的AssetManager的addDefaultAssets方法
- bool AssetManager::addDefaultAssets()
- {
- const char* root = getenv("ANDROID_ROOT");
- LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");
-
- String8 path(root);
- path.appendPath(kSystemAssets);
-
- return addAssetPath(path, NULL);
- }
这例的ANDROID_ROOT保存的就是/system路径,而kSystemAssets是
- static const char* kSystemAssets = "framework/framework-res.apk";
还记得framework-res.apk是什么吗,就是系统所有的资源文件。
到这里终于明白了,原理就是将系统的资源加载进来。
接下来看看addAssetPath方法吧,进入源码后,你会发现它也是一个本地方法,也需要看jni代码
- static jint android_content_AssetManager_addAssetPath(JNIEnv* env, jobject clazz,
- jstring path)
- {
- ScopedUtfChars path8(env, path);
- if (path8.c_str() == NULL) {
- return 0;
- }
-
- AssetManager* am = assetManagerForJavaObject(env, clazz);
- if (am == NULL) {
- return 0;
- }
-
- void* cookie;
- bool res = am->addAssetPath(String8(path8.c_str()), &cookie);
-
- return (res) ? (jint)cookie : 0;
- }
这里调用了本地AssetManager方法的addAssetPath方法。和系统资源一样,都被加载进来了。
下面看看PackageManager获取Resource的流程吧
在PackageManager里面获取资源调用的是getResourcesForApplication方法,getResourcesForApplication也有一个同名方法,我们看办正事的那个吧,
- @Override public Resources getResourcesForApplication(
- ApplicationInfo app) throws NameNotFoundException {
- if (app.packageName.equals("system")) {
- return mContext.mMainThread.getSystemContext().getResources();
- }
- Resources r = mContext.mMainThread.getTopLevelResources(
- app.uid == Process.myUid() ? app.sourceDir
- : app.publicSourceDir, mContext.mPackageInfo);
- if (r != null) {
- return r;
- }
- throw new NameNotFoundException("Unable to open " + app.publicSourceDir);
- }
首先判断包名是否是system,如果不是那么直接调用ActivityThread的getTopLevelResources方法。不过这里会根据当前应用的应用的uid和进程Id相等,如果相等则传入app.sourceDir,否则传入publicSourceDir,但是根据经验时期sourceDir和publicSource一般情况下是相同的。后面的逻辑和Context中的是一样的,这里就不在说了。