最近實習的時候發(fā)現(xiàn)公司的換膚框架挺好的咒唆,所以本周打算學習一下安卓換膚的實現(xiàn)(2020.7.7 06:29)
換膚基礎(chǔ)1
——inflate布局流程甚垦,View的構(gòu)造
布局是承載在activity
上的茶鹃,不管是view 還是fragment,他們都是以activity為基礎(chǔ)艰亮,所以先從activity的創(chuàng)建開始看闭翩;
用戶進程通過binder通知ams開啟一個新的activity,ams經(jīng)過自己的activitystack處理后最終通過binder像activityThread發(fā)送一個hanlder消息迄埃,在activity的handleMessage中去真實創(chuàng)建activity對象
最終在ActivityThread的performLaunchActivity()
創(chuàng)建:
/** 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;
}
- 創(chuàng)建ContextImpl疗韵,裝飾器模式的裝飾對象,activity是被裝飾的對象
- 通過classloader獲取class對象侄非,再通過反射創(chuàng)建真實的Activity對象
- 創(chuàng)建Application對象
- 創(chuàng)建一個Window對象蕉汪,當前window為null
- 通過attach和window進行綁定
- 調(diào)用onCreate()
setContentView():
public void setContentView(int layoutResID) {
...
installDecor();
mLayoutInflater.inflate(layoutResID, mContentParent);
...
}
在activity中調(diào)用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;
}
}
- 創(chuàng)建根view
temp
- 通過inflate的第三個參數(shù)判斷是否
attachToRoot
:
false:
設(shè)置root的參數(shù),不綁定到root逞怨;
true:
不設(shè)置root的參數(shù)者疤,綁定給root,幫我們調(diào)用root.addView(), 如果parent為null骇钦,同false;
補充一下xml的解析方式
createViewFromTag(): 創(chuàng)建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
去創(chuàng)建view,factory是預(yù)留給開發(fā)者自定義的一個創(chuàng)建view的接口
- 如果factory創(chuàng)建失敗則調(diào)用Layoutinflater.onCreateView()宛渐,通過反射調(diào)用view的構(gòu)造方法
onCreateView()
最終調(diào)用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;
}
}
- 先嘗試從緩存獲取構(gòu)造器
- 通過
mConstructorSignature
獲取view的二參構(gòu)造器竞漾,并放入緩存 - 反射創(chuàng)建view對象
市面上的換膚框架基本基于這兩種實現(xiàn)方式:
- 通過Factory接口生成View
- 自定義LayoutInflater眯搭,改寫
createView()
自定義創(chuàng)建View
換膚基礎(chǔ)2
——資源的加載
資源是在handleBindApplication()
中的Application對象創(chuàng)建的過程中加載,關(guān)于handleBindApplication()的調(diào)用時機在前面文章講過,此處不在贅述业岁;
在handleBindApplication()
中的具體創(chuàng)建時機是在創(chuàng)建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()創(chuàng)建ResourcesImpl對象
- 放入緩存
- 通過ResourcesImpl獲取 Resources對象,先從緩存找笔时,沒有就新建
在創(chuàng)建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
文件扒怖,在實現(xiàn)換膚框架中,我們新建一個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創(chuàng)建的時候記錄所有的
View
及其Attr(background低散,drawable俯邓,color)
,可以通過改寫Factory或者LayoutInflater實現(xiàn)所有需要換膚的資源在插件里需要存放一個同名的文件,將這個插件傳給
AssertManager
熔号,交給native層去load稽鞭,然后可以得到一個插件對應(yīng)的Resource
執(zhí)行換膚,遍歷第一步記錄的所有屬性引镊,通過id在插件的Resource對象中尋找同名的資源朦蕴,加載給對應(yīng)的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,F(xiàn)ragment基類實現(xiàn)IDynamicNewView 接口統(tǒng)計所有加載過的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
對象設(shè)置給當前Application的LayoutInflater
;
當LayoutInflater有了factory對象后弟头,會優(yōu)先使用factory對象去創(chuàng)建View梦重,factory對象創(chuàng)建View的實現(xiàn)可以參考LayoutInflater的具體創(chuàng)建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中獲取是否需要使用皮膚包
- 創(chuàng)建View亮瓷,具體參考LayoutInflater的實現(xiàn)
- 將加載出來的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創(chuàng)建的時候,調(diào)用
SkinManager
的換膚方法設(shè)置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對象對應(yīng)的資源)
除师,如果插件對應(yīng)的Resources對象為null或者沒有開啟夜間皮膚則直接使用原本的資源 - 通過
resId
獲取name
沛膳,在通過name,屬性汛聚,皮膚包路徑
锹安,傳給插件Resources對象,然后Resource對象
會通過AssertManager
的一個native方法獲取到對應(yīng)的夜間資源id
(我們自己設(shè)置的名稱相同倚舀,值不相同)叹哭; - 通過這個
darkResId
,我們就可以將其傳給darkResource
獲取到對應(yīng)的資源文件,返回給上層即可