一、Android繪制原理及工具選擇
1.1、Android繪制原理
對于Android手機來說,它的畫面渲染依賴于兩個硬件:1.CPU;2.GPU:
- CPU負責(zé)計算顯示內(nèi)容,比如:視圖創(chuàng)建、布局計算、圖片解碼、文本繪制等
- GPU負責(zé)柵格化(UI元素繪制到屏幕上),柵格化:將一些組件比如Button、Bitmap拆分成不同的像素進行顯示然后完成繪制,這個操作相對比較耗時,所以引入GPU來加快柵格化操作
- 16ms發(fā)出VSync信號觸發(fā)UI渲染,意思就是Android系統(tǒng)要求每一幀都要在16ms內(nèi)完成,具體到項目中就是不管業(yè)務(wù)代碼或者其他邏輯代碼有多復(fù)雜,想要保證每一幀都很平滑,渲染代碼就應(yīng)該在16ms內(nèi)完成
- 大多數(shù)的Android設(shè)備屏幕刷新頻率:60Hz ,60幀/秒是人眼和大腦之間協(xié)作的極限
1.2、優(yōu)化工具
1.Systrace
- 關(guān)注Frames
- 正常:綠色圓點,丟幀:黃色或紅色
- Alerts:Systrace中自動分析并且標(biāo)注異常性能的條目

上面這張圖是我找的一個使用Systrace生成的.html文件,圖中每一個F的出現(xiàn)就表明出現(xiàn)了一幀,可以看到這兩個F之間的時間間隔比16ms多了不少,Alert type這里面就是Systrace自動給出的一些提示信息,我們可以根據(jù)提示信息來查找修改的方向。
②、Layout Inspector
菜單欄——>Tools——>Layout Inspector
- Android Studio自帶的工具
- 查看試圖層次結(jié)構(gòu)
③、Choreographer
獲取FPS,線上使用,具備實時性
- Api 16之后
- 使用方式是:Choreographer.getInstance().postFrameCallback
這里寫了一個方法getFPS()來獲取這個APP的FPS情況,方法內(nèi)部一開始是做了一個保護性操作,確保使用的Choreographer發(fā)生在API16之后,然后在doFrame回調(diào)中首先判斷是不是統(tǒng)計周期的第一次,如果是就記錄第一次回調(diào)的時間,接下來就是判斷時間間隔是否超過預(yù)設(shè)的閥值160ms,如果超過則計算FPS,計算方式是間隔時間除以間隔時間內(nèi)發(fā)生的次數(shù),如果沒有超過則直接將次數(shù)加1。

輸出的結(jié)果可以看到基本上都是59和60之間的數(shù)值。
二、Android布局加載原理
2.1、布局加載流程
1.源碼解析
這一部分我們來看下源碼,因為內(nèi)容比較多,我就盡可能的簡單說,對于源碼閱讀的流程我們之前已經(jīng)說過幾次了,這里就不再介紹了,基本上就是找到你需要的入口方法,然后一路跟蹤下去,把整個流程串起來,不需要你把每一行的代碼都讀懂。
既然說的是布局加載,那么我們首先肯定是找入口方法,這個方法你回想一下每個頁面加載布局都是調(diào)用的什么方法呢?很簡單啦:
setContentView(R.layout.activity_main);
然后點擊這個方法進入源碼中去就到了AppCompatActivity類的setContentView()方法中:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
繼續(xù)跟蹤點擊setContentView()方法:

發(fā)現(xiàn)這是一個抽象方法,此時你需要去找它的實現(xiàn)類AppCompatDelegateImpl中的方法了,點擊左側(cè)向下的fx向下箭頭:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
這個方法中由于傳遞進來的resId也就是布局文件的id,它只在LayoutInflater這一行用到了,所以接著跟蹤這一行,點擊inflate()方法:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
這個方法內(nèi)部又調(diào)用了另一個inflate()方法,所以繼續(xù)點擊:
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()方法,入?yún)⒂幸粋€parser,看了看上下的代碼,知道了它其實是XmlResourceParser的實例,那我們先不去看這個inflate()方法具體的實現(xiàn),先來看下這個parser究竟是什么?找到res.getLayout()方法,里面?zhèn)魅肓宋覀兊馁Y源id,返回的是XmlResourceParser,看名字XML資源解析器,就知道這玩意應(yīng)該很屌,來吧,繼續(xù)點擊getLayout():
@NonNull
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
沒啥實質(zhì)性的內(nèi)容,繼續(xù)點擊它的實現(xiàn)方法loadXmlResourceParser():
@NonNull
@UnsupportedAppUsage
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
throws NotFoundException {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValue(id, value, true);
if (value.type == TypedValue.TYPE_STRING) {
return impl.loadXmlResourceParser(value.string.toString(), id,
value.assetCookie, type);
}
throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
+ " type #0x" + Integer.toHexString(value.type) + " is not valid");
} finally {
releaseTempTypedValue(value);
}
}
這個方法開始是一些對象的聲明,后面是異常的處理,所以看下來真正有用的就是if判斷里面的,它判斷了value.type如果是String類型的,然后繼續(xù)調(diào)用了impl的loadXmlResourceParser()方法,我們點進去看下:
/**
* Loads an XML parser for the specified file.
*
* @param file the path for the XML file to parse
* @param id the resource identifier for the file
* @param assetCookie the asset cookie for the file
* @param type the type of resource (used for logging)
* @return a parser for the specified XML file
* @throws NotFoundException if the file could not be loaded
*/
@NonNull
XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int assetCookie,
@NonNull String type)
throws NotFoundException {
...
//代碼有點多就不貼了,不然文章會很長,大家有需要的自己對照這個過程讀一下源碼,敬請諒解
}
主要看注釋那里的說明哈,Android中的布局都是寫在XML文件中的,這個方法就是為我們具體所寫的布局文件準(zhǔn)備一個XML的解析器,所以它實際上就是一個XML的Pull解析的過程。需要注意的是:android的布局實際上是一個XML文件,它在加載的時候會首先將它讀取到內(nèi)存中,這個過程實際上就是一個IO過程,一般在android開發(fā)中操作IO都會將其置于工作線程中,所以這里可能會成為我們優(yōu)化的一個方向。
關(guān)于這個XmlResourceParser就說到這里,下面繼續(xù)回到上面說的那個inflate()方法中:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
。。。
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 {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
。。。
}
。。。
return result;
}
}
這里同樣的省略了部分代碼,我們知道日常開發(fā)中經(jīng)常會碰到一些報錯,其實這些報錯在Android的源碼中都是有所體現(xiàn)的,比如這里定義的關(guān)于merge標(biāo)簽的一個異常信息。接著看createViewFromTag()這個方法,看名字我們應(yīng)該能大致猜測出來它是干嘛的了,它應(yīng)該就是通過一系列的Tag來創(chuàng)建相對應(yīng)的View,我們點擊該方法跟進:
@UnsupportedAppUsage
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
這里面又調(diào)用了另一個createViewFromTag()方法,繼續(xù)跟進:
@UnsupportedAppUsage
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);
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;
}
}
這里就到了重點的地方了,這里面就是創(chuàng)建View的過程了:
首先:View view = tryCreateView(parent, name, context, attrs); 它通過這個tryCreateView()方法構(gòu)建出View對象,進到這個方法中:
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
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;
}
這個方法里面就是判斷了幾個factory是否為空,首先是Factory2,如果Factory2不為空則調(diào)用Factory2的onCreateView()方法創(chuàng)建View對象,否則判斷Factory是否為空,如果Factory不為空則調(diào)用Factory的onCreateView()創(chuàng)建View對象,如果都為空,則View為空。如果view為空并且PrivateFactory不為空,則調(diào)用PrivateFactory的onCreateView()方法構(gòu)建View,需要注意的是PrivateFactory它只用于Fragment標(biāo)簽的加載。當(dāng)這些條件都不滿足的時候,我們回到上面的createViewFromTag()方法中接著看,它會走到view==null的條件判斷中去,它會走onCreateView()或者createView(),點擊createView()繼續(xù)跟蹤:
@Nullable
public final View createView(@NonNull Context viewContext, @NonNull String name,
@Nullable String prefix, @Nullable AttributeSet attrs)
throws ClassNotFoundException, InflateException {
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);
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 {
// 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 = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);
boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
try {
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]));
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}
}
。。。
}
這個方法里面constructor = clazz.getConstructor(mConstructorSignature); constructor.setAccessible(true); 這兩行首先找到clazz的構(gòu)造方法,通過反射的方式將其設(shè)置為外部可調(diào)用的,然后下面final View view = constructor.newInstance(args); 這一行它通過構(gòu)造函數(shù)反射創(chuàng)建了View,在這個方法中是真正進行了View的創(chuàng)建,當(dāng)然這是在沒有使用Factory的情況下哦。這個過程實際上它是使用了反射,反射是有可能導(dǎo)致程序變慢的一個因素,所以這里也可以作為我們的一個優(yōu)化點。
2.布局加載流程總結(jié)

2.2、性能瓶頸
- 布局文件解析:IO過程(文件過大時可能會導(dǎo)致卡頓)
- 創(chuàng)建View對象:反射(使用過多也會導(dǎo)致變慢)
2.3、LayoutInflater.Factory
在上面解讀setContentView的源碼時,我們知道創(chuàng)建View的過程優(yōu)先是使用Factory2和Factory進行創(chuàng)建,下面對這兩個類作簡要說明:
LayoutInflater.Factory:
- LayoutInflater創(chuàng)建View的一個Hook,Hook其實就是我們可以將自己的代碼掛在它的原始代碼之上,可以對它的流程進行更改
- 定制創(chuàng)建View的過程:比如全局替換自定義TextView等
Factory與Factory2
- Factory2繼承于Factory
- 多了一個參數(shù):parent
我們來看一下它們的源碼,首先來看Factory2:
public interface Factory2 extends Factory {
@Nullable
View onCreateView(@Nullable View parent, @NonNull String name,
@NonNull Context context, @NonNull AttributeSet attrs);
}
可以看到Factory2是一個接口,并且它是繼承自Factory的,來看一下Factory:
public interface Factory {
/**
* Hook you can supply that is called when inflating from a LayoutInflater.
* You can use this to customize the tag names available in your XML
* layout files.
*
* <p>
* Note that it is good practice to prefix these custom names with your
* package (i.e., com.coolcompany.apps) to avoid conflicts with system
* names.
*
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
@Nullable
View onCreateView(@NonNull String name, @NonNull Context context,
@NonNull AttributeSet attrs);
}
入?yún)⒅杏袀€name,來看一下它的注釋,意思就是我們要加載的Tag,比如這個Tag是TextView,那么通過這個方法返回的就是TextView,實際上如果你繼續(xù)跟蹤的話,你會發(fā)現(xiàn)這個Tag實際上就是我們平時在布局中寫的一個個的控件:比如TextView、ImageView等等,它會根據(jù)具體的Tag來進行對應(yīng)View的創(chuàng)建:
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;
case "ToggleButton":
view = createToggleButton(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);
}
并且我們對比兩個接口,可以發(fā)現(xiàn)Factory2比Factory就是入?yún)⒍嗔艘粋€parent,這個parent就是你創(chuàng)建的View的parent,所以綜上可得Factory2比Factory功能上更加強大。
三、優(yōu)雅獲取界面布局耗時
隨著項目的不斷升級,項目體量逐漸變大,頁面可能也變的越來越多,然后我們希望能夠在線上進行統(tǒng)計,了解到具體哪些頁面用戶在進入時會出現(xiàn)卡頓,布局文件加載也可能會導(dǎo)致卡頓。
常規(guī)方式:覆寫方法(setContentView)、手動埋點上報服務(wù)端(不夠優(yōu)雅,代碼具有侵入性)
AOP方式:切Activity的setContentView(切面點)
@ Around("execution(*android.app.Activity.setContentView(..))")
具體實現(xiàn):
@Around("execution(* android.app.Activity.setContentView(..))")
public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
String name = signature.toShortString();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.i(name, " cost " + (System.currentTimeMillis() - time));
}
結(jié)果如下:

思考:如何獲取每一個控件加載耗時?
我們在上面使用setContentView獲取到的是頁面中所有控件的耗時情況,那現(xiàn)在我想要知道這個頁面中各個控件的耗時分布情況,以便于整體的把控分析并且可以對耗時較多的控件做針對性的優(yōu)化,這樣一個場景該如何實現(xiàn)呢?由于每個頁面布局中的控件都是不可控的,有可能多也有可能少,所以我們應(yīng)該盡量做到低侵入性,這個問題大家可以好好想想,看看有什么解決方案。
解決方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容類)讓它在創(chuàng)建View時進行Hook:
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
Log.i(name,"控件耗時:" + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
結(jié)果如下:可以看到我們確實獲取到了列表Item中的每個控件的耗時情況

四、異步Inflate實戰(zhàn)
在上面我們已經(jīng)說過了布局文件加載慢主要的原因是有以下兩點:
- 布局文件讀取慢:IO過程
- 創(chuàng)建View慢:通過反射創(chuàng)建一個對象比直接new一個對象要慢3倍,布局嵌套層級復(fù)雜則反射更多
針對上面說的這兩種情況,相對應(yīng)的解決套路也就是兩種:
- 根本性解決:去掉IO過程、不使用反射
- 側(cè)面緩解:讓主線程不耗時,不影響主線程
這里針對側(cè)面緩解的方案來介紹一種實現(xiàn)方式:AsyncLayoutInflater,谷歌提供的一個類,簡稱異步Inflate
- WorkThread加載布局,原生是在UI Thread加載布局
- 加載完成之后回調(diào)主線程,此時主線程拿到的是創(chuàng)建完成的View對象可以直接使用
- 節(jié)約主線程時間,因為耗時是發(fā)生在了異步線程中,主線程的響應(yīng)能夠得到保障
使用方式:首先導(dǎo)入asynclayoutinflater的依賴庫,這里我們參考谷歌官方文檔中androidx的使用:

然后來修改我們的MainActivity中的onCreate()方法:
@Override
protected void onCreate(Bundle savedInstanceState) {
new AsyncLayoutInflater(this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int resid, @Nullable ViewGroup parent) {
setContentView(view);
mRecycler = findViewById(R.id.mRecycler);
mRecycler.setLayoutManager(new LinearLayoutManager(MainActivity.this));
mRecycler.addItemDecoration(new DividerItemDecoration(MainActivity.this, DividerItemDecoration.VERTICAL));
mRecycler.setAdapter(mAdapter);
mAdapter.setOnFeedShowCallBack(MainActivity.this);
}
});
super.onCreate(savedInstanceState);
// setContentView(R.layout.activity_main);
mAdapter = new FeedAdapter(this, mList);
initData();
// getFPS();
}
有興趣的可以去看一下AsyncLayoutInflater的源碼,理解起來應(yīng)該不難,這個類內(nèi)部有一個Handler對象,一個InflateThread類繼承于Thread,還有一個inflate方法,該方法有三個入?yún)esid、parent、callback,同時將這三個參數(shù)封裝成了InflateRequest的數(shù)據(jù)結(jié)構(gòu),然后加到線程的隊列中,線程中同時有一個run()方法在不斷執(zhí)行,它會從隊列中取出一條InflateRequest,然后這個request.inflate開始執(zhí)行inflate()方法并返回request.view,這個方法是執(zhí)行在子線程中的,最后通過Handler將它回調(diào)到主線程中,同時有一個相關(guān)聯(lián)的Callback,在Callback中進行判斷如果沒有創(chuàng)建完成的話,會回退到主線程中進行布局的加載,最后將request.view回調(diào)到onInflateFinished()方法中,這樣主線程就可以在該方法中拿到對應(yīng)的view了。
總結(jié):
- 不能設(shè)置LayoutInflater.Factory(),需要自定義AsyncLayoutInflater解決;
- 注意View中不能有依賴主線程的操作
五、X2C框架使用
上面這一部分是介紹了一種側(cè)面緩解的方式,那這一部分我們來思考一下從根本上解決該如何實現(xiàn)?
首先來說一下思路哈,其實也沒啥思路,就是利用Java代碼寫布局,這種方案的特點如下:
- 本質(zhì)上解決了性能問題(沒有xml文件也就沒有了IO的過程,直接new對象沒有了反射的過程)
- 引入新問題:不便于開發(fā)、可維護性差
思路有了但是看著實現(xiàn)起來卻不太現(xiàn)實哈,那咋辦呢?咋辦呢?咋辦呢?嗯,這樣拌,大神還是很多的,我們使用開源方案X2C:
X2C框架介紹:保留XML優(yōu)點,解決其性能問題
- 開發(fā)人員寫XML,加載Java代碼
- 原理:APT編譯期翻譯XML為Java代碼
X2C框架的使用方式:
- 添加依賴:app/build.gradle中添加
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
- 添加注解:在使用布局的任意java類或方法上面添加:
@Xml(layouts = "activity_main")
- 代碼實戰(zhàn)

將原有的setContentView注釋掉,然后使用X2C.setContentView()來設(shè)置布局,運行之后發(fā)現(xiàn)是可以正常加載的,圖中左側(cè)圈出來的是使用X2C編譯之后的產(chǎn)物,這個其實就是它的底層實現(xiàn)原理了,我們來看一下:
首先是布局文件:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/mRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
然后是編譯之后的代碼:
public class X2C0_Activity_Main implements IViewCreator {
@Override
public View createView(Context ctx) {
Resources res = ctx.getResources();
LinearLayout linearLayout0 = new LinearLayout(ctx);
linearLayout0.setOrientation(LinearLayout.VERTICAL);
RecyclerView recyclerView1 = new RecyclerView(ctx);
LinearLayout.LayoutParams layoutParam1 = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
recyclerView1.setId(R.id.mRecycler);
recyclerView1.setLayoutParams(layoutParam1);
linearLayout0.addView(recyclerView1);
return linearLayout0;
}
}
可以看到它內(nèi)部就是將我們布局文件中的控件全都以Java對象的形式給new出來了。
X2C存在的問題:
- XML中有的部分屬性Java不支持(雖然不多但是也有)
- 失去了系統(tǒng)的兼容(AppCompat,如果你需要使用AppCompatXXX下面的控件可以通過修改X2C源碼來定制化實現(xiàn)相關(guān)功能)
六、視圖繪制優(yōu)化
1.視圖繪制流程
- 測量:確定大小(自頂向下進行視圖樹的遍歷,確定ViewGroup和View應(yīng)該有多大)
- 布局:確定位置(執(zhí)行另一個自頂向下的遍歷操作,ViewGroup會根據(jù)測量階段測定的大小確定自己應(yīng)該擺放的位置)
- 繪制:繪制視圖(對于視圖樹中的每個對象系統(tǒng)都會為它創(chuàng)建一個Canvas對象,然后向GPU發(fā)送一條繪制命令進行繪制)
可能存在的性能問題:
- 每個階段耗時
- 自頂而下的遍歷(如果Layout層級比較深則遍歷也是很耗時的)
- 觸發(fā)多次(比如嵌套使用RelativeLayout有可能會導(dǎo)致繪制環(huán)節(jié)觸發(fā)多次)
2.布局層級及復(fù)雜度
編寫布局的準(zhǔn)則:減少View樹層級
- 不嵌套使用RelativeLayout
- 不在嵌套的LinearLayout中使用weight
- merge標(biāo)簽:減少一個層級,只能用于根View
這里推薦使用:ConstraintLayout,網(wǎng)上關(guān)于它有很多的文章,后面我也準(zhǔn)備專門寫一篇它的使用總結(jié)
- 實現(xiàn)幾乎完全扁平化布局
- 構(gòu)建復(fù)雜布局性能更高
- 具有RelativeLayout和LinearLayout特性
3.過度繪制
- 一個像素最好只被繪制一次
- 調(diào)試GPU過度繪制
- 藍色可接受
避免過度繪制方法:
- 去掉多余背景色,減少復(fù)雜shape使用
- 避免層級疊加
- 自定義View使用clipRect屏蔽被遮蓋View繪制(當(dāng)覆寫onDraw()之后,系統(tǒng)就無法知道View中各個元素的位置和層級關(guān)系,就無法做自動優(yōu)化,即無法自動忽略繪制那些不可見的元素)
4.布局繪制的其它優(yōu)化技巧
- ViewStub:高效占位符、延遲初始化(這個標(biāo)簽沒有大小,也沒有繪制功能不參與measure和layout過程,資源消耗非常低,一般用于延遲初始化)
- onDraw中避免:創(chuàng)建大對象、耗時操作
- TextView相關(guān)優(yōu)化(setText顯示靜態(tài)文本)
性能系列專欄其他文章
關(guān)于 Android內(nèi)存優(yōu)化你應(yīng)該了解的知識點
關(guān)于 Android啟動優(yōu)化你應(yīng)該了解的知識點
Android卡頓優(yōu)化分析及解決方案,全面掌握!