前言html
這篇文章會從源碼的角度分析,LayoutInflater
將xml文件實例化爲一個view
對象的流程java
咱們會發現,其中有兩個部分是耗時的主要來源android
這兩點,又跟Xml的複雜程度成正相關,Xml越複雜,則遞歸調用所消耗的時間就越長,就產生了咱們所說的,卡頓問題git
BlinkLayout是LayoutInflater中的一個內部類,它自己是是FrameLayout
的子類,若是當前標籤爲TAG_1995
,則建立一個隔0.5秒閃爍一次的BlinkLayout來承載它的佈局內容github
源碼註釋也頗有意思,寫了Let's party like it's 1995!
, 聽說是爲了慶祝1995
年的復活節
bash
LayoutInflater
app
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);
}
...
return view;
}
複製代碼
具體使用也很簡單框架
<blink
android:layout_below="@id/iv_qr_code"
android:layout_centerHorizontal="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Android研習社"
android:textColor="#157686"
android:textSize="55sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</blink>
複製代碼
效果以下,這種效果也適合來作EditText中光標的閃爍效果ide
掃描上方二維碼關注「Android研習社」公衆號,獲取更多學習資料!函數
ps: 想深刻學習的都關注了,還不趕快關注一波?
概覽
LayoutInflater是一個抽象類,它的建立,並非交由App層處理的,而是調用了from()
的靜態函數,經由系統服務LAYOUT_INFLATER_SERVICE
,最終建立了一個LayoutInflater
的子類對象--PhoneLayoutInflater
重要函數解析
這個函數比較簡單,就是根據傳遞過來的Context對象,調用getSystemService()
來獲取對應的系統服務,並賦值給LayoutInflater
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater = //LayoutInflate是一個系統服務,最終返回的是`PhoneLahyoutInflater`
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
複製代碼
而Context
自己是一個抽象類,它真正的實例化對象,是ContextImpl
, 在這個類的getSystemService()
函數中,真正執行獲取系統服務的類,是SystemServiceRegistry
,其中又封裝了一個ServiceFetcher
來獲取真正的系統服務,全部的系統服務,都是存儲在一個map集合--SYSTEM_SERVICE_FETCHERS
當中,這裏實際上是一個get
方法,是從這個map集合中取出對應的系統服務
LayoutInflater
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
複製代碼
SystemServiceRegistry
/** * Gets a system service from a given context. */
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
複製代碼
關於對應的服務的添加,也就是調用了SYSTEM_SERVICE_FETCHERS
的put函數,這個動做是交由registerService()
來完成的
/** * Statically registers a system service with the context. * This method must be called during static initialization only. */
private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) {
SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
}
複製代碼
SystemServiceRegistry
這個類中有一個靜態代碼塊,是用來完成全部服務的註冊的,這裏咱們只關心LAYOUT_INFLATER_SERVICE
對應的服務是如何註冊的
static{
...
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
new CachedServiceFetcher<LayoutInflater>() {
@Override
public LayoutInflater createService(ContextImpl ctx) {
return new PhoneLayoutInflater(ctx.getOuterContext());
}});
...
}
複製代碼
正如咱們以前所說,這裏最終是建立了一個PhoneLayoutInflater
並返回的,到這裏LayoutInflater的建立流程就分析完了
思考
爲何要交由系統服務來作,而不是直接建立一個PhoneLayoutInflater
的實例對象?
總體流程
實例化的調用流程咱們都很熟悉了,調用layoutInflater
的inflater()
函數,傳入一個xml的resId
參數就能夠了
重要函數解析
這個函數就是咱們把Xml佈局文件實例化爲一個View對象的入口了
LayoutInflater
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); //這段代碼實際上是必然返回null的,由於當前版本寫死了預編譯的Enable爲false
if (view != null) {
return view;
}
XmlResourceParser parser = res.getLayout(resource); //獲取XmlBlock.Parser對象
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
複製代碼
此處又調用了inflate(parser, root, attachToRoot)
這個函數,來對Xml佈局進行解析
這裏看到對一些熟悉的標籤,好比include
,merge
,的處理,具體細節請看下面的源碼及註釋
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {//XmlPullParser是一個接口
//此函數是真正執行將xml解析爲視圖view的過程,此處的parser爲根據xml佈局獲取到的parser對象
synchronized (mConstructorArgs) {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
final Context inflaterContext = mContext;
final inflateAttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root; //須要返回的view對象
try {
advanceToRootNode(parser); //對START_TAG和END_TAG進行判斷和處理
final String name = parser.getName(); //獲取當前標籤
if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "
+ name);
System.out.println("**************************");
}
if (TAG_MERGE.equals(name)) { //若是使用merge標籤
if (root == null || !attachToRoot) { //使用merge標籤必須有父佈局,且依賴於父佈局加載,不然就會拋出異常
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, inflaterContext, attrs, false);//遞歸(Recursive)生成佈局視圖
} else { //若是不使用merge標籤,建立tmp 做爲臨時的根節點,並最終賦值給result返回
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs); ////根據標籤名建立一個view
ViewGroup.LayoutParams params = null;
if (root != null) { //若是rootView不爲空
if (DEBUG) {
System.out.println("Creating params from root: " +
root);
}
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs); //根據rootView生成layoutparams
if (!attachToRoot) { //若是attachToRoot爲false
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params); //設置一個臨時的params
}
}
if (DEBUG) {
System.out.println("-----> start inflating children");
}
// 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不爲空,且attachToRoot爲true
root.addView(temp, params); //把temp添加到到rootview中
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) { // 若是root爲空且attachToRoot爲false
result = temp; //將temp,也就是根結點的View賦值給result
}
}
}
...
return result; //返回結果
}
}
複製代碼
從上面的代碼中咱們也能夠看到,無論是merge標籤,仍是非merge標籤,最終都會調用到rInflate()
這個函數,這個是用於遞歸向下遍歷xml佈局,最終調用createViewFromTag()
函數來反射建立View對象
具體細節請看下面的源碼及註釋
void rInflate(XmlPullParser parser, View parent, Context context, AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
final int depth = parser.getDepth();
int type;
boolean pendingRequestFocus = false;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) { //若是需REQUEST_FOCUS標籤
pendingRequestFocus = true;
consumeChildElements(parser);
} else if (TAG_TAG.equals(name)) { //若是是「tag」標籤
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) { //若是是 Include標籤
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs); //對 include標籤進行解析
} else if (TAG_MERGE.equals(name)) { //若是是merge標籤
throw new InflateException("<merge /> must be the root element"); //直接拋出異常
} else { //其餘標籤
final View view = createViewFromTag(parent, name, context, attrs); //根據Tag建立view,反射建立View
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true); //遞歸調用rInflate函數
viewGroup.addView(view, params);
}
}
if (pendingRequestFocus) {
parent.restoreDefaultFocus();
}
if (finishInflate) {
parent.onFinishInflate();
}
}
複製代碼
終於到了咱們的重頭戲,也是真正根據解析到的Tag標籤去反射建立View的函數
LayoutInflater
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs, boolean ignoreThemeAttr) {
if (name.equals("view")) {
name = attrs.getAttributeValue(null, "class");
}
// Apply a theme wrapper, if allowed and one is specified.
if (!ignoreThemeAttr) {
final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
final int themeResId = ta.getResourceId(0, 0);
if (themeResId != 0) {
context = new ContextThemeWrapper(context, themeResId);
}
ta.recycle();
}
try {
View view = tryCreateView(parent, name, context, attrs); //嘗試使用Factory來闖將View對象
if (view == null) { //若是tryCreateView返回null
final Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = context;
try {
//sample:com.aiwinn.base.widget.CameraSurfaceView
if (-1 == name.indexOf('.')) { //若是當前Tag含有「.」
view = onCreateView(context, parent, name, attrs);
} else {
view = createView(context, name, null, attrs);
}
} finally {
mConstructorArgs[0] = lastContext;
}
}
return view;
}
...
}
複製代碼
在這個函數中會首先調用tryCreateView()
來獲取View對象,若是爲null,則進一步調用createView()
函數來建立View對象
public final View createView(@NonNull Context viewContext, @NonNull String name, @Nullable String prefix, @Nullable AttributeSet attrs) throws ClassNotFoundException, InflateException {
//根據Tag反射建立view
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
Class<? extends View> clazz = null;
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);
if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);//把prefix和name進行拼接,獲取到對應的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);
} else {
...
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
final View view = constructor.newInstance(args); //根據獲取到的構造器建立一個View的實例化對象
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;
}catch{
...
}
}
}
複製代碼
這裏的代碼其實在耗時上算是比較重量級了,由於是使用反射來建立的,通常的說法是,反射比直接建立對象要慢3倍,iReader
的x2c
框架就是基於這一點去作的優化
這裏先獲取到Resources
對象--mResources
,這個對象的建立是由createResources()
來完成的,這裏最終是交由ResourcesManager
這個類來獲取對應的resources
ContextImpl
private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName, int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
final String[] splitResDirs;
final ClassLoader classLoader;
try {
splitResDirs = pi.getSplitPaths(splitName);
classLoader = pi.getSplitClassLoader(splitName);
} catch (NameNotFoundException e) {
throw new RuntimeException(e);
}
return ResourcesManager.getInstance().getResources(activityToken,
pi.getResDir(),
splitResDirs,
pi.getOverlayDirs(),
pi.getApplicationInfo().sharedLibraryFiles,
displayId,
overrideConfig,
compatInfo,
classLoader);
}
複製代碼
ResourcesManager
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);
}
}
複製代碼
ContextImpl
@Override
public Resources getResources() {
return mResources;
}
複製代碼
由這裏咱們也能夠推斷,LayoutInflater
交由服務來建立來建立,是由於其須要獲取系統服務才能獲取的某些資源
inflate()
函數裏還涉及到一個重要的類, XmlResourceParser
,這個類是負責對xml的標籤進行遍歷解析的,它的真正的實現類是XmlBlock
的內部類XmlBlock.Parser
,而真正完成xml的遍歷操做的函數都是由XmlBlock
來實現的,爲了提高效率,該函數都是經過JNI調用native的函數來作的,對應的native層是android_util_XmlBlock.cpp
XmlBlock.java
@FastNative
/*package*/ static final native int nativeNext(long state);
@FastNative
private static final native int nativeGetNamespace(long state);
@FastNative
/*package*/ static final native int nativeGetName(long state);
@FastNative
private static final native int nativeGetText(long state);
@FastNative
private static final native int nativeGetLineNumber(long state);
...
複製代碼
``android_util_XmlBlock.cpp`
static jint android_content_XmlBlock_nativeNext(JNIEnv* env, jobject clazz, jlong token) {
ResXMLParser* st = reinterpret_cast<ResXMLParser*>(token);
if (st == NULL) {
return ResXMLParser::END_DOCUMENT;
}
do {
ResXMLParser::event_code_t code = st->next();
switch (code) {
case ResXMLParser::START_TAG:
return 2;
case ResXMLParser::END_TAG:
return 3;
case ResXMLParser::TEXT:
return 4;
case ResXMLParser::START_DOCUMENT:
return 0;
case ResXMLParser::END_DOCUMENT:
return 1;
case ResXMLParser::BAD_DOCUMENT:
goto bad;
default:
break;
}
} while (true);
bad:
jniThrowException(env, "org/xmlpull/v1/XmlPullParserException",
"Corrupt XML binary file");
return ResXMLParser::BAD_DOCUMENT;
}
複製代碼
這個函數是Android10的源碼裏面新增的一個函數,是用來根據xml預編譯生成的dex,經過反射來生成對應的View,從而減小XmlPullParser解析Xml的時間 -- 放到編譯期來進行-- 的一個優化 ,而反射獲取對應的View時能夠直接獲取到預編譯的View對象,而不須要遞歸調用rInflate
這裏基本上就是真正完全解決了複雜佈局致使的卡頓問題
View tryInflatePrecompiled(@LayoutRes int resource, Resources res, @Nullable ViewGroup root, boolean attachToRoot) {
if (!mUseCompiledView) {
return null;
}
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate (precompiled)");
// Try to inflate using a precompiled layout.
String pkg = res.getResourcePackageName(resource);
String layout = res.getResourceEntryName(resource);
//依然是經過反射的方式,根據已經建立的mPrecompiledClassLoader來反射生成view對象
try {
Class clazz = Class.forName("" + pkg + ".CompiledView", false, mPrecompiledClassLoader); //獲取到預編譯生成的view對象的Class類
Method inflater = clazz.getMethod(layout, Context.class, int.class);
View view = (View) inflater.invoke(null, mContext, resource);
if (view != null && root != null) {
// We were able to use the precompiled inflater, but now we need to do some work to
// attach the view to the root correctly.
XmlResourceParser parser = res.getLayout(resource);
try {
AttributeSet attrs = Xml.asAttributeSet(parser);
advanceToRootNode(parser);
ViewGroup.LayoutParams params = root.generateLayoutParams(attrs);
if (attachToRoot) {
root.addView(view, params);
} else {
view.setLayoutParams(params);
}
} finally {
parser.close();
}
}
return view;
} catch (Throwable e) {
if (DEBUG) {
Log.e(TAG, "Failed to use precompiled view", e);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
return null;
}
複製代碼
下一篇文章,咱們會提出一些優化方案,來解決(或者說)減緩複雜佈局產生的卡頓問題,敬請期待!
參考文章:
1. https://www.reddit.com/r/androiddev/comments/3sekn8/lets_party_like_its_1995_from_the_layoutinflater/
2. https://www.cnblogs.com/liyilin-jack/p/10282385.html
3. https://blog.csdn.net/axi295309066/article/details/60128009
4. https://github.com/RTFSC-Android/RTFSC/blob/master/BlinkLayout.md
5. https://juejin.im/post/5c789b0ce51d454fbd5a8baa
複製代碼
因爲研究過程當中並未記錄全部參考文章,若有疏漏,還請私聊,謝謝
鄭重聲明
本文版權歸Android研習社
全部,未經容許禁止轉載,侵權必究!