最近实习的时候发现公司的换肤框架挺好的,所以本周打算学习一下安卓换肤的实现(2020.7.7 06:29)
换肤基础1
——inflate布局流程,View的构造
布局是承载在activity
上的,不管是view 还是fragment,他们都是以activity为基础,所以先从activity的创建开始看;
用户进程通过binder通知ams开启一个新的activity,ams经过自己的activitystack处理后最终通过binder像activityThread发送一个hanlder消息,在activity的handleMessage中去真实创建activity对象
最终在ActivityThread的performLaunchActivity()
创建:
/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;
// 分析1
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
try {
// 分析2
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
}
try {
// 分析3
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
// 分析4
window = r.mPendingRemoveWindow;
}
appContext.setOuterContext(activity);
// 分析5
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
activity.mStartedActivity = false;
int theme = r.activityInfo.getThemeResource();
if (theme != 0) {
activity.setTheme(theme);
}
activity.mCalled = false;
if (r.isPersistable()) {
// 分析6
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
r.activity = activity;
}
r.setState(ON_CREATE);
}
return activity;
}
- 创建ContextImpl,装饰器模式的装饰对象,activity是被装饰的对象
- 通过classloader获取class对象,再通过反射创建真实的Activity对象
- 创建Application对象
- 创建一个Window对象,当前window为null
- 通过attach和window进行绑定
- 调用onCreate()
setContentView():
public void setContentView(int layoutResID) {
...
installDecor();
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}
在activity中调用setContentView()
会做两件事情:
- 初始化DecorView
- 通过inflater加载布局文件
inflate():
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
// 分析1
// 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) {
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
// 分析2
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);
// 分析2
if (root != null && attachToRoot) {
root.addView(temp, params);
}
}
return result;
}
}
- 创建根view
temp
- 通过inflate的第三个参数判断是否
attachToRoot
:
false:
设置root的参数,不绑定到root;
true:
不设置root的参数,绑定给root,帮我们调用root.addView(), 如果parent为null,同false;
补充一下xml的解析方式
createViewFromTag(): 创建view
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
try {
// 分析1
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 {
// 分析2
if (-1 == name.indexOf('.')) {
view = onCreateView(parent, name, attrs);
} else {
view = createView(name, null, attrs);
}
}
}
return view;
}
}
- 首先会通过
mFactory2 , mFactory
去创建view,factory是预留给开发者自定义的一个创建view的接口
- 如果factory创建失败则调用Layoutinflater.onCreateView(),通过反射调用view的构造方法
onCreateView()
最终调用createView()
// 成员变量
static final Class<?>[] mConstructorSignature = new Class[] {
Context.class, AttributeSet.class};
public final View createView(String name, String prefix, AttributeSet attrs)
throws ClassNotFoundException, InflateException {
// 分析1
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
try {
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
// 分析2
clazz = mContext.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
}
Object lastContext = mConstructorArgs[0];
if (mConstructorArgs[0] == null) {
// Fill in the context if not already within inflation.
mConstructorArgs[0] = mContext;
}
Object[] args = mConstructorArgs;
args[1] = attrs;
// 分析3
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]));
}
mConstructorArgs[0] = lastContext;
return view;
}
}
- 先尝试从缓存获取构造器
- 通过
mConstructorSignature
获取view的二参构造器,并放入缓存 - 反射创建view对象
市面上的换肤框架基本基于这两种实现方式:
- 通过Factory接口生成View
- 自定义LayoutInflater,改写
createView()
自定义创建View
换肤基础2
——资源的加载
资源是在handleBindApplication()
中的Application对象创建的过程中加载,关于handleBindApplication()的调用时机在前面文章讲过,此处不在赘述;
在handleBindApplication()
中的具体创建时机是在创建ApplicationContext的时候:
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
null);
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.getInstance().getResources():
/**
* A list of Resource references that can be reused.
*/
private final ArrayList<WeakReference<Resources>> mResourceReferences = new ArrayList<>();
public @Nullable Resources getResources() {
try {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
final ResourcesKey key = new ResourcesKey(
resDir,
splitResDirs,
overlayDirs,
libDirs,
displayId,
overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
compatInfo);
classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
return getOrCreateResources(activityToken, key, classLoader);
}
}
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
// 分析1
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
// 分析2
// If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
ResourcesImpl resourcesImpl = createResourcesImpl(key);
if (resourcesImpl == null) {
return null;
}
// 分析3
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
// 分析4
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
return resources;
}
}
// 分析4详细代码
private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
@NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
// Find an existing Resources that has this ResourcesImpl set.
final int refCount = mResourceReferences.size();
for (int i = 0; i < refCount; i++) {
WeakReference<Resources> weakResourceRef = mResourceReferences.get(i);
Resources resources = weakResourceRef.get();
// 如果缓存中的classloader和resourceImpl一样就使用缓存
if (resources != null &&
Objects.equals(resources.getClassLoader(), classLoader) &&
resources.getImpl() == impl) {
return resources;
}
}
// Create a new Resources reference and use the existing ResourcesImpl object.
Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
: new Resources(classLoader);
resources.setImpl(impl);
mResourceReferences.add(new WeakReference<>(resources));
return resources;
}
- 首先尝试从缓存中找ResourcesImpl
- 如果缓存没有,createResourcesImpl()创建ResourcesImpl对象
- 放入缓存
- 通过ResourcesImpl获取 Resources对象,先从缓存找,没有就新建
在创建ResourcesImpl
的时候会借助一个 AssetManager
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
...
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
return impl;
}
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
final AssetManager.Builder builder = new AssetManager.Builder();
...
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/,false /*overlay*/));
...
return builder.build();
}
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay)
throws IOException {
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
// 分析1
ApkAssets apkAssets = mLoadedApkAssets.get(newKey);
if (apkAssets != null) {
return apkAssets;
}
// 分析2
// We must load this from disk.
if (overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path),
false /*system*/);
} else {
apkAssets = ApkAssets.loadFromPath(path, false /*system*/, sharedLib);
}
mLoadedApkAssets.put(newKey, apkAssets);
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets;
}
- 缓存中找mLoadedApkAssets
- 从磁盘读取
apkAssets
ApkAssets.loadOverlayFromPath():
public static @NonNull ApkAssets loadOverlayFromPath(@NonNull String idmapPath, boolean system)
throws IOException {
return new ApkAssets(idmapPath, system, false /*forceSharedLibrary*/, true /*overlay*/);
}
private ApkAssets(@NonNull String path, boolean system, boolean forceSharedLib, boolean overlay)
throws IOException {
Preconditions.checkNotNull(path, "path");
mNativePtr = nativeLoad(path, system, forceSharedLib, overlay);
mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/);
}
看到native就全部都走通了,将path传给native,由C/C++去加载磁盘,加载完以后会将C的指针传给java;native加载的东西就是apk中的resources.arsc
文件,在实现换肤框架中,我们新建一个module,在里面放入夜间的资源,在通过某些手段将传给native的path替换成新建的这个夜间module;
AssertManager的核心native方法():
// Resource name/ID native methods.
private static native @AnyRes int nativeGetResourceIdentifier(long ptr, @NonNull String name,
@Nullable String defType, @Nullable String defPackage);
private static native @Nullable String nativeGetResourceName(long ptr, @AnyRes int resid);
private static native @Nullable String nativeGetResourcePackageName(long ptr,
@AnyRes int resid);
private static native @Nullable String nativeGetResourceTypeName(long ptr, @AnyRes int resid);
private static native @Nullable String nativeGetResourceEntryName(long ptr, @AnyRes int resid);
换肤整体思路
在View创建的时候记录所有的
View
及其Attr(background,drawable,color)
,可以通过改写Factory或者LayoutInflater实现所有需要换肤的资源在插件里需要存放一个同名的文件,将这个插件传给
AssertManager
,交给native层去load,然后可以得到一个插件对应的Resource
执行换肤,遍历第一步记录的所有属性,通过id在插件的Resource对象中寻找同名的资源,加载给对应的View
下面分析一下GitHub上的一个换肤框架源码的核心类:
Android-Skin-Loader
1. SkinManager的初始化
public class SkinManager implements ISkinLoader{
private static SkinManager instance; // 单例
private String skinPackageName; // 插件的包名
private Resources mResources; // 插件的Resources对象
private String skinPath; // 插件的目录
private boolean isDefaultSkin = false; // 是否使用皮肤
}
public void load(){
String skin = SkinConfig.getCustomSkinPath(context);
load(skin, null);
}
/**
* Load resources from apk in asyc task
* @param skinPackagePath path of skin apk
* @param callback callback to notify user
*/
public void load(String skinPackagePath, final ILoaderListener callback) {
PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
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());
SkinConfig.saveSkinPath(context, skinPkgPath);
skinPath = skinPkgPath;
isDefaultSkin = false;
mResources = skinResource;
}
2. Activity,Fragment基类实现IDynamicNewView 接口统计所有加载过的View
public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{
// 分析1
private SkinInflaterFactory mSkinInflaterFactory;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mSkinInflaterFactory = new SkinInflaterFactory();
// 分析2
getLayoutInflater().setFactory(mSkinInflaterFactory);
}
@Override
protected void onResume() {
super.onResume();
SkinManager.getInstance().attach(this);
}
@Override
protected void onDestroy() {
super.onDestroy();
SkinManager.getInstance().detach(this);
mSkinInflaterFactory.clean();
}
protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){
mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId);
}
}
- 自定义一个
Factory
对象 Factory的作用上文有详细说到 - 将自定义的
Factory
对象设置给当前Application的LayoutInflater
;
当LayoutInflater有了factory对象后,会优先使用factory对象去创建View,factory对象创建View的实现可以参考LayoutInflater的具体创建View的过程;
public class SkinInflaterFactory implements Factory {
public View onCreateView(String name, Context context, AttributeSet attrs) {
// 分析1
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);
if (!isSkinEnable){
return null;
}
//分析2
View view = createView(context, name, attrs);
if (view == null){
return null;
}
//分析3
parseSkinAttr(context, attrs, view);
return view;
}
//分析2
private View createView(Context context, String name, AttributeSet attrs) {
View view = null;
try {
if (-1 == name.indexOf('.')){
if ("View".equals(name)) {
view = LayoutInflater.from(context).createView(name, "android.view.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);
}
if (view == null) {
view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);
}
}else {
view = LayoutInflater.from(context).createView(name, null, attrs);
}
}
return view;
}
}
- 首先会从SF中获取是否需要使用皮肤包
- 创建View,具体参考LayoutInflater的实现
- 将加载出来的View使用插件的资源
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();
for (int i = 0; i < attrs.getAttributeCount(); i++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
if(attrValue.startsWith("@")){
try {
int id = Integer.parseInt(attrValue.substring(1));
String entryName = context.getResources().getResourceEntryName(id);
String typeName = context.getResources().getResourceTypeName(id);
// 分析1
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
}
}
}
if(!ListUtils.isEmpty(viewAttrs)){
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mSkinItems.add(skinItem);
if(SkinManager.getInstance().isExternalSkin()){
// 分析2
skinItem.apply();
}
}
}
@Override
public void apply(View view) {
if(RES_TYPE_NAME_COLOR.equals(attrValueTypeName)){
view.setBackgroundColor(SkinManager.getInstance().getColor(attrValueRefId));
}else if(RES_TYPE_NAME_DRAWABLE.equals(attrValueTypeName)){
Drawable bg = SkinManager.getInstance().getDrawable(attrValueRefId);
view.setBackgroundDrawable(bg);
}
}
- 找出所有的资源封装成
SkinAttr
对象,包括属性的名称(background)、属性的id值(int类型),属性的id值(@+id,string类型),属性的值类型(color)
,全部保存到集合中 - 在View创建的时候,调用
SkinManager
的换肤方法设置bg/color
SkinManager.geyDrawable()
@SuppressLint("NewApi")
public Drawable getDrawable(int resId){
// 分析1
Drawable originDrawable = context.getResources().getDrawable(resId);
if(mResources == null || isDefaultSkin){
return originDrawable;
}
// 分析2
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
Drawable trueDrawable = null;
try{
// 分析3
if(android.os.Build.VERSION.SDK_INT < 22){
trueDrawable = mResources.getDrawable(trueResId);
}else{
trueDrawable = mResources.getDrawable(trueResId, null);
}
}catch(NotFoundException e){
e.printStackTrace();
trueDrawable = originDrawable;
}
return trueDrawable;
}
- 首先获取主项目的资源
(即原本的Resources对象对应的资源)
,如果插件对应的Resources对象为null或者没有开启夜间皮肤则直接使用原本的资源 - 通过
resId
获取name
,在通过name,属性,皮肤包路径
,传给插件Resources对象,然后Resource对象
会通过AssertManager
的一个native方法获取到对应的夜间资源id
(我们自己设置的名称相同,值不相同); - 通过这个
darkResId
,我们就可以将其传给darkResource
获取到对应的资源文件,返回给上层即可