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;
}
解决以上两个问题,换肤就好实现了。