Andriod中的资源管理

Android的安装包本质上是一个zip压缩文件,我们可以使用解压软件打开。
其中的resources.arsc 是一个二进制格式的文件,与二进制的xml完全不同,appt在对资源进行编译的时候会为每一个资源分配唯一的id,程序在执行的时候会根据这些id来读取特定的资源,而resources.arsc文件正是包含了所有id资源值得一个数据集合,在该文件中,如果某个id对应的资源是string或者数值,那么该文件会直接包含对应的值,如果id对应的资源是某个layout或者drawable资源,那么该文件会存入对应资源的地址。

安装后的目录如下

  • /data/app 安装之后,会有一个以报名命名的文件夹在里面,其中base.apk就是用户安装的apk。系统自带的应用在/system/app下面。
  • /data/dalvik-cache, 每个apk里面都有class.dex,安装之后,所有的dex文件都会放到该目录下面,这样用户在启动APP的时候就可以快速的读取dex文件,而不用解压,在早期版本中,会对dex进行优化,最后生成的odex文件也在这个目录下面,任何程序都可以读写该目录,这就为类的动态加载提供了可能,实际上每个app的dex .

styleable, style, attr, theme

styleable 一般和attr联合使用,用于定义一些属性。从AttributeSet对象中获取这三个属性的时候可以,可以传入R.styleable.xxx 这个参数实际上会被编译成一个int []. 数组的内容正是所包含的attrde id。

AttributeSet和TypeArray

AttributeSet 代表了视图属性的集合, TypedArray 类又是对ArributeSet数据的抽象。context.obtainStyledAttribute() ,该函数的内部实现正是通过遍历set中的每一个属性,找到用户感兴趣的属性,然后把值和属性经过重定位返回一个TypedArray对象。

TypedArray 中的内部mValue(类型为一个int 数组)起到了一个内部缓冲的作用,mData 包含了styleable中所有的属性值, 其长度为 styleable 中属性的个数乘以 AssetManager.STYLE_NUM_ENTRIES,AssetManager.STYLE_NUM_ENTRIES的值为6,它实际上表示有6种数据在mData这个数组当中。

static final int STYLE_NUM_ENTRIES = 6;
static final int STYLE_TYPE = 0;
static final int STYLE_DATA = 1;
static final int STYLE_ASSET_COOKIE = 2;
static final int STYLE_RESOURCE_ID = 3;
static final int STYLE_CHANGING_CONFIGURATIONS = 4;
static final int STYLE_DENSITY = 5;

比如说我先有一个int 类型的属性值,那么它 实际上在mData中占据了6个 存储位置,从 0 -5 位置上的值分别对应上面的类型。再看如何获取string的存储的,核心方法是调用了 TypedArray的 loadStringValueAt 方法。

 public String getString(@StyleableRes int index) {
        if (mRecycled) {
            throw new RuntimeException("Cannot make calls to a recycled instance!");
        }

        index *= AssetManager.STYLE_NUM_ENTRIES;
        final int[] data = mData;
        final int type = data[index+AssetManager.STYLE_TYPE];
        if (type == TypedValue.TYPE_NULL) {
            return null;
        } else if (type == TypedValue.TYPE_STRING) {
            return loadStringValueAt(index).toString();
        }

        final TypedValue v = mValue;
        if (getValueAt(index, v)) {
            final CharSequence cs = v.coerceToString();
            return cs != null ? cs.toString() : null;
        }

        throw new RuntimeException("getString of bad type: 0x" + Integer.toHexString(type));
    }


首页要做的就是找到index在mData中正确的位置,这里将index*=6,如果这里存放的本来就是一个sting那么使用loadStringValueAt,如果不是,那么将调用 TypedValue的 coerceTOsTRING();

再来看 loadStringValueAt方法

private CharSequence loadStringValueAt(int index) {
    final int[] data = mData;
    final int cookie = data[index+AssetManager.STYLE_ASSET_COOKIE];
    if (cookie < 0) {
        if (mXml != null) {
            return mXml.getPooledString(
                data[index+AssetManager.STYLE_DATA]);
        }
        return null;
    }
    return mAssets.getPooledStringForCookie(cookie, data[index+AssetManager.STYLE_DATA]);
}

首先尝试获取字符串的cookie,cookie如果有效的话将会从mXml(mXml是用于解析二进制xml的对象)中去加载,无效的话将调用AssetManager的 getPooledStringForCookie 方法,

获取资源
获取资源通常需要Resource对象,在构建Resource对象的过程中需要一个AssertManager。

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

init 是一个navtive的方法,它会尝试去加载/system/framework-res.apk, 这样APP就可以使用系统提供的资源。

通过尝试去跟Resource. getXXX方法, 最终都是调用了AssetManager的方法。
而AssetManager最终又是采用JNI的方式来获取资源。

static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                       jint ident,
                                                       jshort density,
                                                       jobject outValue,
                                                       jboolean resolve)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }
    const ResTable& res(am->getResources());
    Res_value value;
    ResTable_config config;
    uint32_t typeSpecFlags;
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
#if THROW_ON_BAD_ID
    if (block == BAD_INDEX) {
        jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
        return 0;
    }
#endif
    uint32_t ref = ident;
    if (resolve) {
        block = res.resolveReference(&value, block, &ref, &typeSpecFlags, &config);
#if THROW_ON_BAD_ID
        if (block == BAD_INDEX) {
            jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
            return 0;
        }
#endif
    }
    return block >= 0 ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block;
}

首先通过获取ResTable ,让后从resTable 当中去找到相应的资源,这里的ResTable实际上就是apk文件解压出来之后的resources.arsc。

Framework资源

加载和读取
系统资源是在zygote 进程启动的时候加载的,并且只有在加载系统资源完成之后才开始启动其他的应用进程,从而实现其他应用进程共享系统资源的目标,

方法的调用栈 ZyogteInit.main() -> preload() -> preloadResources() , 系统加载完这些资源之后会在ResoucesImpl通过集合缓存起来。

// Information about preloaded resources.  Note that they are not
// protected by a lock, because while preloading in zygote we are all
// single-threaded, and after that these are immutable.
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
        = new LongSparseArray<>();
private static final LongSparseArray<android.content.res.ConstantState<ComplexColor>>
        sPreloadedComplexColors = new LongSparseArray<>();

在ResouscesImpl 内有一个成员变量mPreloading , 这个值只会在 startPreloading()和 finishPreloading 这两个方法当中被改变,而这两个方法又只会在Zygote进程中被调用,所以当mPreloading为true的时候, 是Zygote 进程正在加载系统资源。所以在ResourcesImpl的cacheDrawble方法当中,可以看到

if (mPreloading) {
        final int changingConfigs = cs.getChangingConfigurations();
        if (isColorDrawable) {
            if (verifyPreloadConfig(changingConfigs, 0, value.resourceId, "drawable")) {
                sPreloadedColorDrawables.put(key, cs);
            }
        } else {
            if (verifyPreloadConfig(
                    changingConfigs, LAYOUT_DIR_CONFIG, value.resourceId, "drawable")) {
                if ((changingConfigs & LAYOUT_DIR_CONFIG) == 0) {
                    // If this resource does not vary based on layout direction,
                    // we can put it in all of the preload maps.
                    sPreloadedDrawables[0].put(key, cs);
                    sPreloadedDrawables[1].put(key, cs);
                } else {
                    // Otherwise, only in the layout dir we loaded it for.
                    sPreloadedDrawables[mConfiguration.getLayoutDirection()].put(key, cs);
                }
            }
        }
    }

如果mPreloading 是true,那么这个时候是在执行ZygoteInit进程,所以这些数据会被缓存到静态变量当中去。这里加载的Framework的资源,加载的仅仅只是一小部分,对于那些非”预装载”的系统资源则不会缓存到静态集合变量中,在这种情况下,如果应用京城需要一个非预装载的资源,则会在各个进程中保持一个资源的缓存。


关于 动态加载皮肤框架的实现

获取新的资源对象 Resources ?
简单的概括一下就是通过AssetManager 来构建一个新的Resouce对象。

AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
Resources superRes = context.getResources();
Resources skinResource = new                        Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());

这样既可以获取到一个指向新资源的 Resource 对象。

在获取新的资源对象的时候,先根据现有资源的id来获取资源的字符串名字,就是我们开发的时候给资源的命名,然后根据命名来在新的 Resource对象中 获取相应的资源。

String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
int trueColor = 0;
try{
    trueColor = mResources.getColor(trueResId);
}catch(NotFoundException e){
    e.printStackTrace();
    trueColor = originColor;
}
return trueColor;

拿到资源之后替换即可。

如何获取需要更换皮肤的View.
在作者的实现如下,
在布局文件中为每一个需要替换皮肤的View 添加了一个属性 skin:enable=”true” , 同时为LayoutInflator指定自定义的Factory, 在LayoutInflator解析这个布局文件的时候,会调用Factory的onCreateView 方法,这样可以在这里获取到所有添加了 skin:enable=”true” 这个标签的View.

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
    mSkinInflaterFactory = new SkinInflaterFactory();
    getLayoutInflater().setFactory(mSkinInflaterFactory);
}

自定义Factory的核心方法如下

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    // if this is NOT enable to be skined , simplly skip it 
    boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
    if (!isSkinEnable){
            return null;
    }
    View view = createView(context, name, attrs);
    if (view == null){
        return null;
    }
    parseSkinAttr(context, attrs, view);
    return view;
}

解决以上两个问题,换肤就好实现了。