了解我们当前activity的层级
上面这张图想必大家都已经有所了解,在这里我就不多说了。
查找activity加载这些代码的时候有一个点顺便说一下,我自己也是过一段时间就忘记了。
我们使用layoutinflater去实例化布局的时候,第三个参数总是传递false。
//实例化的时候会先创建一个temp的View
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
//传递false才会进去
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
//添加根View的布局参数
temp.setLayoutParams(params);
}
}
除了自定义View之外我们平时这种实例化xml的肯定都要啊,所以都传递false。
Activity的布局是怎么加载的
所有的View都是通过createView创建的,反射创建并缓存起来。
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
...
try {
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
在创建之前有这么一行代码:
View view = tryCreateView(parent, name, context, attrs);
public final View tryCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context,
@NonNull AttributeSet attrs) {
if (name.equals(TAG_1995)) {
// Let's party like it's 1995!
return new BlinkLayout(context, attrs);
}
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);
}
return view;
}
mFactory为空的时候才会正常走流程,默认就是空的,mFactory2 是继承了mFactory2,这两个接口都是只有一个方法就是上面的createViewFromTag,他们的实现类都实现了它。
那么我们想要在实例化View的时候做一些事情有两种方式,一种直接替换了layoutInflater,一种直接塞进去一个mFactory,让它走我们的mFactory流程创建,而不走正常的创建流程。
我们自己的创建流程必要的代码直接复制系统正常的创建流程,在其中的关键点加入我们自己的代码即可。
编译后的APK资源
resource文件下面记录了res下面的资源文件的索引,ID、name、索引位置default
我们的平时也经常用resource的资源,这没什么好解释的。
加载资源
我们平时使用资源都是从context里获取resource,context的实现类是ContextImpl,直接找ContextImpl
public ContextImpl getSystemContext() {
synchronized (this) {
if (mSystemContext == null) {
mSystemContext = ContextImpl.createSystemContext(this);
}
return mSystemContext;
}
}
@UnsupportedAppUsage
static ContextImpl createSystemContext(ActivityThread mainThread) {
LoadedApk packageInfo = new LoadedApk(mainThread);
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, null,
0, null, null);
//关键代码,从包信息获取资源
context.setResources(packageInfo.getResources());
context.mResources.updateConfiguration(context.mResourcesManager.getConfiguration(),
context.mResourcesManager.getDisplayMetrics());
context.mIsSystemOrSystemUiContext = true;
return context;
}
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");
}
//从资源管理器ResourcesManager的getResources方法返回资源resource
//关键参数名mResDir
mResources = ResourcesManager.getInstance().getResources(null, mResDir,
splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
getClassLoader(), null);
}
return mResources;
}
public @Nullable Resources getResources(
//持有资源地址
@Nullable String resDir,
....
//资源的实现类,30版本是放在前面,之前是放在return后面创建的
//资源的创建肯定在这个类里面,后面不用跟了
ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key);
...
//通过createResources创建资源
return createResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
@NonNull ResourcesKey key) {
ResourcesImpl impl = findResourcesImplForKeyLocked(key);
if (impl == null) {
//类似这种代码,第一次进来肯定是null的
impl = createResourcesImpl(key);
if (impl != null) {
mResourceImpls.put(key, new WeakReference<>(impl));
}
}
return impl;
}
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
...
//关键代码,跟进去,impl持有AssetManager
final AssetManager assets = createAssetManager(key);
...
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
//30版本是建造者设计模式,以前是直接new,直接set。
//以前再跟进去是一个native方法现在直接提供java层的给开发者使用
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,
读取apk的资源文件,是一个private,不过我们可以向办法使用它
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = null;
if (mLoadedApkAssets != null) {
apkAssets = mLoadedApkAssets.get(newKey);
if (apkAssets != null && apkAssets.isUpToDate()) {
return apkAssets;
}
}
// Optimistically check if this ApkAssets exists somewhere else.
final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey);
if (apkAssetsRef != null) {
apkAssets = apkAssetsRef.get();
if (apkAssets != null && apkAssets.isUpToDate()) {
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(newKey);
}
}
// We must load this from disk.
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), 0 /*flags*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
}
if (mLoadedApkAssets != null) {
mLoadedApkAssets.put(newKey, apkAssets);
}
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
这个类的层次其实非常简单,我们平时的new Rosource().getXXXX,你可以点进去看下,调用ContextImpl,再跟进去又调用到ContextImpl的AssetManager。
换肤思路
1、利用LayoutInflater或者mFactory2创建View的过程收集XML信息
2、记录要换肤的属性,一个界面有多个View,一个View又有多个属性,设计一个对象持有它们。
3、读取准备好的皮肤包内容,需要先加载进来,使用反射。
4、执行换肤,将要替换的View的属性重新设置,资源从换肤APK里面的资源获取。
在View的创建过程中记录需要换肤的属性,例如下面这些:
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
1、先创建一个对象,记录View的属性名字和资源ID,也就是上面那些。
2、它是被另外一个类持有,作为一个成员变量,另外一个变量记录着View对象。
3、再用一个数组持有当前界面所有的View。
4、换肤的时候顶部的statusBarColorResId和navigationBarColor也要换。
private static int[] STATUSBAR_COLOR_ATTRS = {android.R.attr.statusBarColor, android.R.attr
.navigationBarColor
};
```java
public static int[] getResId(Context context, int[] attrs) {
int[] resIds = new int[attrs.length];
TypedArray a = context.obtainStyledAttributes(attrs);
for (int i = 0; i < attrs.length; i++) {
resIds[i] = a.getResourceId(i, 0);
}
a.recycle();
return resIds;
}
5.0以上才行
//获得 statusBarColor 与 nanavigationBarColor (状态栏颜色)
//当与 colorPrimaryDark 不同时 以statusBarColor为准
int[] resIds = getResId(activity, STATUSBAR_COLOR_ATTRS);
int statusBarColorResId = resIds[0];
int navigationBarColor = resIds[1];
自定义View需要自己定义个接口,让所有的自定义View实现它,并设计一个方法,在方法里执行即可。
在Aplication监听所有的界面创建,在setContentView替换掉工厂factory,并收集好信息。
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//控件的收集在这里完成
setContentView(R.layout.activity_main);
在setcontectView走完创建View的流程之后我们也就收集了所有的信息。
根据标志位是否换肤判断,之后我们可以对当前主APK里面的View的同名属性在插件APK加载的资源里面查找,如果能找到,则使用插件APK的资源,就达到了换肤目的。
虽然说设置View的属性这样会设置两次,但是性能还是非常好,无闪烁延迟等问题。