Android插件化换肤原理
1.Android View的加载流程分析
Activity的view都是通过setContentView来实现组件的显示,可以用过源码来开一下Android实现XML布局文件到界面显示的
XXActivity
setContentView
AppCompatActivity
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
getDelegate()
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
if (Build.VERSION.SDK_INT >= 24) {
return new AppCompatDelegateImplN(context, window, callback);
} else if (Build.VERSION.SDK_INT >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else {
return new AppCompatDelegateImplV14(context, window, callback);
}
}
通过一系列的版本兼容适配,最终调用
AppCompatDelegateImplV14中的setContentView,改步骤中创建了DecorView,并使用LayoutInflater将布局文件加载到contentParent(也就是和DecorView绑定的contentParent中)
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
所以主要看LayoutInflater的inflate方法,这里主要创建了XML解析器
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
接着往下走调用inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) 方法:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
try {
advanceToRootNode(parser);
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException(" can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
//标识一 创建xml布局的父容器View
// Temp is the root view that was found in the xml
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);
if (!attachToRoot) {
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
//标识二:创建xml中父容器包括的子View
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
final InflateException ie = new InflateException(e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(inflaterContext, attrs)
+ ": " + e.getMessage(), e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} finally {
// Don't retain static reference on context.
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return result;
}
}
标识一:创建xml布局的父容器View
createViewFromTag(root, name, inflaterContext, attrs);
标识二:创建xml中父容器包括的子View
rInflateChildren(parser, temp, attrs, true);
使用递归的方式调用createViewFromTag方法完成字View的加载
我们接着看createViewFromTag:
@UnsupportedAppUsage
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
try {
View view = tryCreateView(parent, name, context, attrs);
if (view == null) {
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
if (-1 == name.indexOf('.')) {
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
} catch (InflateException e) {
throw e;
} catch (ClassNotFoundException e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
} catch (Exception e) {
final InflateException ie = new InflateException(
getParserStateDescription(context, attrs)
+ ": Error inflating class " + name, e);
ie.setStackTrace(EMPTY_STACK_TRACE);
throw ie;
}
}
tryCreateView方法:
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;
}
首先尝试用3个Fractory创建View,如果成功就直接返回了。注意,我们可以利用这个机制,创建自己的Factory来控制View的创建过程。
如果没有Factory或创建失败,那么走默认逻辑。
先判断name中是否有'.'字符,如果没有,则认为使用android自己的View,此时会在name的前面加上包名"android.view.";如果有这个'.',则认为使用的自定义View,这时无需添加任何前缀,认为name已经包含全包名了。
最终,使用这个全包名的name来创建实例,
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
Constructor extends View> constructor = sConstructorMap.get(name);
Class extends View> clazz = null;
......
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
}
constructor = clazz.getConstructor(mConstructorSignature);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, attrs);
}
}
}
Object[] args = mConstructorArgs;
args[1] = attrs;
return constructor.newInstance(args);
......
}
sConstructorMap缓存了View的构造方法,如果已经加载过,则直接从缓存使用构造方法创建View的实例。没有的化使用ClassLoader反射得到View的构造器
最后通过构造器使用反射,调用了View的两个构造方法反射完成View的创建。
最后将创建完的View执行AddView将视图添加到到DecorView中。
2.res的管理
我们先看Resources获取res值是如何获取的:
Resources中getColor
public int getColor(@ColorRes int id, @Nullable Theme theme) throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type >= TypedValue.TYPE_FIRST_INT
&& value.type <= TypedValue.TYPE_LAST_INT) {
return value.data;
} else if (value.type != TypedValue.TYPE_STRING) {
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
}
final ColorStateList csl = impl.loadColorStateList(this, value, id, theme);
return csl.getDefaultColor();
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl
void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
throws NotFoundException {
boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
if (found) {
return;
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
}
最后通过AssetManager获取相应的值,其类型也是如此
所以我们可查看AssetManager如何创建:
在performLaunchActivity方法中,创建了Application实例
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
走到LoadedApk中,创建了appContext:
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
使用 ContextImpl调用了createAppContext,使用 context.setResources设置了App的Resources实例对象
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo,
String opPackageName) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null, opPackageName);
context.setResources(packageInfo.getResources());
return context;
}
回到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;
}
继续往下走,使用ResourcesManager获取Resources
getOrCreateResources
createResourcesImpl
createAssetManager
创建ApkAssets作为参数,使用AssetManager.Builder创建AssetManager
ApkAssets描述:大概意思是可以使用一个apk的path创建ApkAssets
/**
* Creates a new ApkAssets instance from the given path on disk.
*
* @param path The path to an APK on disk.
* @return a new instance of ApkAssets.
* @throws IOException if a disk I/O error or parsing error occurred.
*/
public static @NonNull ApkAssets loadFromPath(@NonNull String path) throws IOException {
return new ApkAssets(path, false /*system*/, false /*forceSharedLib*/, false /*overlay*/);
}
AssetManager.Builder
public AssetManager build() {
// Retrieving the system ApkAssets forces their creation as well.
final ApkAssets[] systemApkAssets = getSystem().getApkAssets();
final int totalApkAssetCount = systemApkAssets.length + mUserApkAssets.size();
final ApkAssets[] apkAssets = new ApkAssets[totalApkAssetCount];
System.arraycopy(systemApkAssets, 0, apkAssets, 0, systemApkAssets.length);
final int userApkAssetCount = mUserApkAssets.size();
for (int i = 0; i < userApkAssetCount; i++) {
apkAssets[i + systemApkAssets.length] = mUserApkAssets.get(i);
}
// Calling this constructor prevents creation of system ApkAssets, which we took care
// of in this Builder.
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
return assetManager;
}
}
创建了AssetManager对象并调用nativeSetApkAssets设置了AssetManager的参数
到此为止,我们可以得到如下结论
1.app的res资源使用Resources来访问,最终都会交给AssetManager来执行资源的访问和返回
2.AssetManager的实例需要一个路径参数,那么我们可以通过创建自己的AssetManager,并设置一个目标apk路径,以此来实现加载目标apk的res资源
3.换肤思路
经过上面的源码分析,我们大概可以有如下的换肤思路:
1.activity加载View的时候使用Factory来截获View的加载过程,然后加载时记录Activity的每一个View需要调整的属性;
2.制作一个插件apk文件,将需要换肤的res属性定义到该apk中,原则是命名一致;
3.使用AssetManager加载插件apk的,就可以获取到插件apk的res属性,换肤的操作只需要将记录的View重新设置属性完成换肤