該文主要從三個(gè)方面去介紹稳衬,分別為Activity的布局流程,資源加載流程以及換膚思路坐漏。
Activity的布局流程
回顧我們寫app的習(xí)慣薄疚,創(chuàng)建Activity,寫xxx.xml,在Activity里面setContentView(R.layout.xxx). 我們寫的是xml赊琳,最終呈現(xiàn)出來的是一個(gè)一個(gè)的界面上的UI控件街夭,那么setContentView到底做了什么事,使得XML里面的內(nèi)容躏筏,變成了UI控件呢板丽?首先先來看一張圖,
從圖中我們可以看到主要的兩個(gè)東西,LayoutInflater 和setFactory埃碱。LayoutInflater是將xml轉(zhuǎn)化為界面元素的控制類猖辫,在這里面會(huì)得到xml布局的相應(yīng)控件的相關(guān)屬性并進(jìn)行view的創(chuàng)建,然后會(huì)調(diào)用Factory的onCreateView()進(jìn)行view的轉(zhuǎn)換砚殿。
LayoutInflater
關(guān)于LayoutInflater的介紹啃憎,可以看下下面這個(gè)的博客,寫的很詳細(xì)似炎,在這就不一一去添代碼講流程了辛萍,畢竟用別人寫好的就很香。
Android 中LayoutInflater(布局加載器)之介紹篇
下面我們只挑我需要的代碼講羡藐。
在setContentView中贩毕,之后調(diào)用 LayoutInflater.inflate 方法,來解析 XML 資源文件仆嗦,
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
inflate有很多重載方法辉阶,最終調(diào)用的是下面的這個(gè)方法:
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 {
final String name = parser.getName();
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);
} else {
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
ViewGroup.LayoutParams params = null;
if (root != null) {
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {
temp.setLayoutParams(params);
}
}
rInflateChildren(parser, temp, attrs, true);
if (root != null && attachToRoot) {
root.addView(temp, params);
}
if (root == null || !attachToRoot) {
result = temp;
}
}
} catch (XmlPullParserException e) {
}
return result;
}
}
從代碼中我們可以看到,先判斷是不是TAG_MERGE標(biāo)簽瘩扼,如果是睛藻,則調(diào)用rInflate()方法對(duì)其處理:
使用 merge 標(biāo)簽必須有父布局,且依賴于父布局加載
merge 并不是一個(gè) ViewGroup邢隧,也不是一個(gè) View,它相當(dāng)于聲明了一些視圖冈在,等待被添加倒慧,解析過程中遇到 merge 標(biāo)簽會(huì)將 merge 標(biāo)簽下面的所有子 view 添加到根布局中
merge 標(biāo)簽在 XML 中必須是根元素
相反的 include 不能作為根元素,需要放在一個(gè) ViewGroup 中
使用 include 標(biāo)簽必須指定有效的 layout 屬性
使用 include 標(biāo)簽不寫寬高是沒有關(guān)系的包券,會(huì)去解析被 include 的layout
而這些解析的過程中纫谅,最終會(huì)調(diào)用rInflateChildren(),所以說為什么復(fù)雜的布局會(huì)產(chǎn)生卡頓溅固,XmlResourseParser 對(duì) XML 的遍歷付秕,隨著布局越復(fù)雜,層級(jí)嵌套越多侍郭,所花費(fèi)的時(shí)間也越長(zhǎng)询吴,調(diào)用 onCreateView 與 createView 方法是通過反射創(chuàng)建 View 對(duì)象導(dǎo)致的耗時(shí)。
在 Android 10上亮元,新增 tryInflatePrecompiled 方法是為了減少 XmlPullParser 解析 XML 的時(shí)間猛计,但是用一個(gè)全局變量 mUseCompiledView 來控制是否啟用 tryInflatePrecompiled 方法,根據(jù)源碼分析爆捞,mUseCompiledView 始終為 false奉瘤,所以 tryInflatePrecompiled 方法目前在 release 版本中不可使用。
不是則調(diào)用createViewFromTag來創(chuàng)建View煮甥,然后判斷root是否為null盗温,如果不為null藕赞,拿到root的params,如果不添加到父布局root中卖局,就將解析到的LayoutParams設(shè)置到該view中去斧蜕。具體的操作規(guī)則如下:
當(dāng) attachToRoot == true 且 root != null 時(shí)吼驶,新解析出來的 View 會(huì)被 add 到 root 中去惩激,然后將 root 作為結(jié)果返回
當(dāng) attachToRoot == false 且 root != null 時(shí)蟹演,新解析的 View 會(huì)直接作為結(jié)果返回风钻,而且 root 會(huì)為新解析的 View 生成 LayoutParams 并設(shè)置到該 View 中去
當(dāng) attachToRoot == false 且 root == null 時(shí),新解析的 View 會(huì)直接作為結(jié)果返回
那我們繼續(xù)看createViewFromTag酒请。
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) {
}
}
在該方法中可以看到骡技,先調(diào)用tryCreateView()去創(chuàng)建View,在這個(gè)方法里面就是調(diào)用下面需要講的Factory羞反,所以放在后面講布朦,onCreateView最終調(diào)用的是createView,而在createView中采用的是反射的方式創(chuàng)建View實(shí)例昼窗。
setFactory
講這個(gè)之前先看幾個(gè)圖是趴,
我們?cè)诓季种袑懙氖荁utton,TextView,ImageView,但是在AS的Layout Inspector功能查看下,變成了AppCompatButton,AppCompatTextView,AppComaptImageView,那到底是我們的按鈕真的已經(jīng)在編譯的時(shí)候自動(dòng)變成了AppCompatXXX系列澄惊,還是只是單純的在這個(gè)工具里面看的時(shí)候我們的控件只是顯示給我們看到的名字是AppCompatXXX系列而已唆途。
我們把我們的Activity的父類做下修改,改為:
public class TestActivity extends AppCompatActivity{
......
}
變?yōu)?public class TestActivity extends Activity{
......
}
我們?cè)賮聿榭聪翷ayout Inspector界面:
我們可以看到掸驱,控件就自動(dòng)變成了我們布局里面寫的控件名稱了肛搬,造成這種現(xiàn)象的原因就是下面我們要分析的。我們首先來看tryCreateView()方法毕贼。
public final View tryCreateView(@Nullable View parent, @NonNull String name,
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;
}
如果mFactory2不為null的時(shí)候就會(huì)調(diào)用mFactory2.onCreateView()温赔,
public interface Factory2 extends Factory {
@Nullable
View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs);
}
Factory2是一個(gè)接口,具體的實(shí)現(xiàn)類是AppCompatDelegateImpl鬼癣。
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
if (mAppCompatViewInflater == null) {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
String viewInflaterClassName =
a.getString(R.styleable.AppCompatTheme_viewInflaterClass);
if ((viewInflaterClassName == null)
|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {
// Either default class name or set explicitly to null. In both cases
// create the base inflater (no reflection)
mAppCompatViewInflater = new AppCompatViewInflater();
} else {
try {
Class viewInflaterClass = Class.forName(viewInflaterClassName);
mAppCompatViewInflater =
(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor()
.newInstance();
} catch (Throwable t) {
Log.i(TAG, "Failed to instantiate custom view inflater "
+ viewInflaterClassName + ". Falling back to default.", t);
mAppCompatViewInflater = new AppCompatViewInflater();
}
}
}
boolean inheritContext = false;
if (IS_PRE_LOLLIPOP) {
inheritContext = (attrs instanceof XmlPullParser)
// If we have a XmlPullParser, we can detect where we are in the layout
? ((XmlPullParser) attrs).getDepth() > 1
// Otherwise we have to use the old heuristic
: shouldInheritContext((ViewParent) parent);
}
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
會(huì)調(diào)用mAppCompatViewInflater的createView()陶贼,
final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = createTextView(context, attrs);
verifyNotNull(view, name);
break;
case "ImageView":
view = createImageView(context, attrs);
verifyNotNull(view, name);
break;
case "Button":
view = createButton(context, attrs);
verifyNotNull(view, name);
break;
case "EditText":
view = createEditText(context, attrs);
verifyNotNull(view, name);
break;
case "Spinner":
view = createSpinner(context, attrs);
verifyNotNull(view, name);
break;
case "ImageButton":
view = createImageButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckBox":
view = createCheckBox(context, attrs);
verifyNotNull(view, name);
break;
case "RadioButton":
view = createRadioButton(context, attrs);
verifyNotNull(view, name);
break;
case "CheckedTextView":
view = createCheckedTextView(context, attrs);
verifyNotNull(view, name);
break;
case "AutoCompleteTextView":
view = createAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "MultiAutoCompleteTextView":
view = createMultiAutoCompleteTextView(context, attrs);
verifyNotNull(view, name);
break;
case "RatingBar":
view = createRatingBar(context, attrs);
verifyNotNull(view, name);
break;
case "SeekBar":
view = createSeekBar(context, attrs);
verifyNotNull(view, name);
break;
default:
// The fallback that allows extending class to take over view inflation
// for other tags. Note that we don't check that the result is not-null.
// That allows the custom inflater path to fall back on the default one
// later in this method.
view = createView(context, name, attrs);
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check its android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
return new AppCompatTextView(context, attrs);
}
從這里我們知道,為什么我們?cè)谇懊娴慕貓D中看到的變成那樣了待秃。
那這個(gè)Factory2什么時(shí)候設(shè)置的骇窍?
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();//1
delegate.onCreate(savedInstanceState);
super.onCreate(savedInstanceState);
}
重點(diǎn)就在這個(gè)注釋1,
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory2(layoutInflater, this);
} else {
if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
資源加載流程
常規(guī)操作先上一張圖锥余,
從圖中可以知道腹纳,
1、ResourcesManager管理著一個(gè)Resources類
2、Resources類里有他的實(shí)現(xiàn)類ResourcesImpl嘲恍,各種創(chuàng)建足画,調(diào)用,getColor等方法都是在實(shí)現(xiàn)類里實(shí)現(xiàn)的
3佃牛、ResourcesImpl里管理著一個(gè)AssetManager
4淹辞、AssetManager負(fù)責(zé)從apk里獲取資源,寫入資源等 addAssetPath()
下面我們來分析一下俘侠。起點(diǎn)是ActivityThread.java handleBindApplication()方法 象缀,在這加載Application的。
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
return createAppContext(mainThread, packageInfo, null);
}
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;
}
在handleBindApplication中創(chuàng)建ContextImpl爷速,然后setResources.而這個(gè)資源是從LoadedApk中獲取到央星。
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;
}
然后調(diào)用ResourcesManager中的getResources(),
public @Nullable Resources getResources(@Nullable IBinder activityToken,
@Nullable String resDir,
@Nullable String[] splitResDirs,
@Nullable String[] overlayDirs,
@Nullable String[] libDirs,
int displayId,
@Nullable Configuration overrideConfig,
@NonNull CompatibilityInfo compatInfo,
@Nullable ClassLoader classLoader) {
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);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
根據(jù)傳過來的參數(shù)封裝成key.這個(gè)key在后面用來生成Resources。
private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
synchronized (this) {
if (DEBUG) {
Throwable here = new Throwable();
here.fillInStackTrace();
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
}
if (activityToken != null) {
final ActivityResources activityResources =
getOrCreateActivityResourcesStructLocked(activityToken);
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(activityResources.activityResources,
sEmptyReferencePredicate);
// Rebase the key's override config on top of the Activity's base override.
if (key.hasOverrideConfiguration()
&& !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
final Configuration temp = new Configuration(activityResources.overrideConfig);
temp.updateFrom(key.mOverrideConfiguration);
key.mOverrideConfiguration.setTo(temp);
}
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
} else {
// Clean up any dead references so they don't pile up.
ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);
// Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
if (resourcesImpl != null) {
if (DEBUG) {
Slog.d(TAG, "- using existing impl=" + resourcesImpl);
}
return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
// We will create the ResourcesImpl object outside of holding this lock.
}
// 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;
}
// Add this ResourcesImpl to the cache.
mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
final Resources resources;
if (activityToken != null) {
resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
resourcesImpl, key.mCompatInfo);
} else {
resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
}
return resources;
}
}
首先從軟引用中獲取惫东,如果獲取不到莉给,則調(diào)用createResourcesImpl創(chuàng)建。
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key);
if (assets == null) {
return null;
}
final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
final Configuration config = generateConfig(key, dm);
final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);
if (DEBUG) {
Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
}
return impl;
}
ResourcesImpl的創(chuàng)建需要AssetManager作為參數(shù)廉沮。
關(guān)于AssetManager的詳細(xì)解析請(qǐng)參考下面這篇文章颓遏。
Android資源管理框架:Asset Manager的創(chuàng)建過程
換膚思路
具體思路為:
1.收集xml數(shù)據(jù),根據(jù)View創(chuàng)建過程的Factory2(源碼里拷貝過來就行)需要修改的地方就是View創(chuàng)建完事以后滞时,將需要修改的屬性及他的View記錄下來(比如要改color叁幢、src、backgrand)
2.讀取皮膚包里的內(nèi)容坪稽。先通過assets.addAssetPath()加載進(jìn)來遥皂,這樣就能通過assetManager來獲取皮膚包里的資源了
3.如果遇到了需要替換的屬性(color、src刽漂、backgrand等)那就替換,通過assetManager里的方法
另外參考下面兩篇文章:
Android 換膚那些事兒弟孟, Resource包裝流 贝咙?AssetManager替換流?
參考資料
Android 中LayoutInflater(布局加載器)之介紹篇