Android 源码分析Resource加载,实现换肤
android中资源有图片,颜色,string,styles等等...
那是如何加载出来的呢?
通过资源id,在activity中调用getResources()方法
看源码,其实这个方法来自于context
public class ContextThemeWrapper extends ContextWrapper {
...
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
...
Context是抽象类,最终的实现类是ContextImpl
在ContextImpl构造方法中创建Resources
packageInfo.getResources(mainThread)
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
......
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setConfiguration(overrideConfiguration);
mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
: ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
// packageInfo类型是LoadedApk
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
mResources = resources;
LoadedApk类中getResources()方法
public Resources getResources() {
if (mResources == null) {
final String[] splitPaths;
try {
splitPaths = getSplitPaths(null);
} catch (NameNotFoundException e) {
// This should never fail.
throw new AssertionError("null split not found");
}
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader());
}
return mResources;
}
Resources实际是在ResourcesManager中直接new出来的
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
/**
* Creates a new Resources object with CompatibilityInfo.
*
* @param classLoader class loader for the package used to load custom
* resource classes, may be {@code null} to use system
* class loader
* @hide
*/
public Resources(@Nullable ClassLoader classLoader) {
mClassLoader = classLoader == null ? ClassLoader.getSystemClassLoader() : classLoader;
}
那一样的我们可以打包自己的资源,仿照源码的方式来加载资源
自己创建Resources
Resources resources = context.getResources();
Class<AssetManager> assetManagerClass = AssetManager.class;
AssetManager assetManager = null;
try {
//创建AssetManager
assetManager = assetManagerClass.newInstance();
//反射执行addAssetPath方法,添加资源所在路径
Method addAssetPath = assetManagerClass.getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, skinPath);
mSkinResource = new Resources(assetManager, resources.getDisplayMetrics(), resources.getConfiguration());
mSkinPageName = context.getPackageManager()
.getPackageArchiveInfo(skinPath, PackageManager.GET_RECEIVERS).packageName;
} catch (Exception e) {
e.printStackTrace();
}
AssetManager 初始化调用的jni方法 init()底层会创建AssetManager
并添加默认的系统资源路径“framework/framwork-res.apk”+“assets”,所以我们可以使用系统的资源
然后加载resources.arsc
image.png
apk打包会生成R.java文件, resources.arsc(资源映射信息)就是App的资源索引表
有个注意点:添加资源路径,底层会判断有没有注册清单文件,如果没有不会加载
// 检查路径是否有androidmanifest . xml
Asset* manifestAsset = const_cast<AssetManager*>(this)->openNonAssetInPathLocked(
kAndroidManifest, Asset::ACCESS_BUFFER, ap);
if (manifestAsset == NULL) {
delete manifestAsset;
return false;
}
资源加载通过Resources,而我们可以做到自己构建Resources
来加载其他apk里的相同名称的资源
从而实现替换资源,达到换肤的目的还需要拦截view的创建
//拦截View的创建 LayoutInflater是单例,由系统创建的服务,存在static hashMap中
LayoutInflater layoutInflater = LayoutInflater.from(this);
LayoutInflaterCompat.setFactory2(layoutInflater, this);
源码已经告诉我们了Hook you can supply that is called when inflating from a LayoutInflater
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(String name, Context context, AttributeSet attrs);
}
view的创建过程中,mFactory判断是否为null,不是null会回调factory的方法,可以创建view
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
if (view == null && mPrivateFactory != null) {
view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
通过LayoutInflaterCompat.setFactory2(layoutInflater, this),拦截View的创建
加载其他资源包的资源就能够实现换肤