目錄
寫在前面
人到中年不得已,莫愁前路有知己!
本篇是Android性能優化系列專欄第三篇,上一篇中通過圖文加實戰的方式介紹了Android的內存優化,有需要的可以翻看一下:
《你的應用內存優化了嗎?》,今天我們就繼續來說說Android中的佈局優化相關的知識。
一、Android繪製原理及工具選擇
1.1、Android繪製原理
對於Android手機來說,它的畫面渲染依賴於兩個硬件:①、CPU;②、GPU:
- CPU負責計算顯示內容,比如:視圖創建、佈局計算、圖片解碼、文本繪製等
- GPU負責柵格化(UI元素繪製到屏幕上),柵格化:將一些組件比如Button、Bitmap拆分成不同的像素進行顯示然後完成繪製,這個操作相對比較耗時,所以引入GPU來加快柵格化操作
- 16ms發出VSync信號觸發UI渲染,意思就是Android系統要求每一幀都要在16ms內完成,具體到項目中就是不管業務代碼或者其他邏輯代碼有多複雜,想要保證每一幀都很平滑,渲染代碼就應該在16ms內完成
- 大多數的Android設備屏幕刷新頻率:60Hz ,60幀/秒是人眼和大腦之間協作的極限
1.2、優化工具
①、Systrace
- 關注Frames
- 正常:綠色圓點,丟幀:黃色或紅色
- Alerts:Systrace中自動分析並且標註異常性能的條目
上面這張圖是我找的一個使用Systrace生成的.html文件,圖中每一個F的出現就表明出現了一幀,可以看到這兩個F之間的時間間隔比16ms多了不少,Alert type這裏面就是Systrace自動給出的一些提示信息,我們可以根據提示信息來查找修改的方向。
②、Layout Inspector
菜單欄——>Tools——>Layout Inspector
- Android Studio自帶的工具
- 查看試圖層次結構
如果進程中存在多個Activity,還會提示讓我們選擇具體哪個Activity,當然了,我這裏只有一個Activity,直接點擊該進程即可:
上面這張圖就是Layout Inspector爲我們生成的具體檢測界面的信息,這個界面總共是分成了三個部分:
左側:View Tree,即:視圖在佈局中的層次結構
中間:Load Overlay,即:相當於屏幕截圖,並且這個截圖自帶各個視圖控件的邊距
右側:Properties Table,即:選中視圖的佈局屬性,比如我這裏選中的是一個TextView,裏面就會顯示出這個TextView所顯示出的具體信息
對於這個工具我們主要關注的是最左側的這一欄層次結構,有了它就可以很方便的看到當前佈局的層級,比如我這裏的LinearLayout這是當前界面的根佈局,它裏面是一個RecyclerView,對於RecyclerView的每一個Item都有三個控件,這三個控件都處在同一層級,整個條目的層級相對還是比較簡單的。
③、Choreographer
獲取FPS,線上使用,具備實時性
- Api 16之後
- 使用方式是:Choreographer.getInstance().postFrameCallback
這裏寫了一個方法getFPS()來獲取這個APP的FPS情況,方法內部一開始是做了一個保護性操作,確保使用的Choreographer發生在API16之後,然後在doFrame回調中首先判斷是不是統計週期的第一次,如果是就記錄第一次回調的時間,接下來就是判斷時間間隔是否超過預設的閥值160ms,如果超過則計算FPS,計算方式是間隔時間除以間隔時間內發生的次數,如果沒有超過則直接將次數加1。
輸出的結果可以看到基本上都是59和60之間的數值。
二、Android佈局加載原理
2.1、佈局加載流程
①、源碼解析
這一部分我們來看下源碼,因爲內容比較多,我就儘可能的簡單說,對於源碼閱讀的流程我們之前已經說過幾次了,這裏就不再介紹了,基本上就是找到你需要的入口方法,然後一路跟蹤下去,把整個流程串起來,不需要你把每一行的代碼都讀懂。
既然說的是佈局加載,那麼我們首先肯定是找入口方法,這個方法你回想一下每個頁面加載佈局都是調用的什麼方法呢?很簡單啦:
setContentView(R.layout.activity_main);
然後點擊這個方法進入源碼中去就到了AppCompatActivity類的setContentView()方法中:
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
繼續跟蹤點擊setContentView()方法:
發現這是一個抽象方法,此時你需要去找它的實現類AppCompatDelegateImpl中的方法了,點擊左側向下的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);
}
這個方法內部又調用了另一個inflate()方法,所以繼續點擊:
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()方法,入參有一個parser,看了看上下的代碼,知道了它其實是XmlResourceParser的實例,那我們先不去看這個inflate()方法具體的實現,先來看下這個parser究竟是什麼?找到res.getLayout()方法,裏面傳入了我們的資源id,返回的是XmlResourceParser,看名字XML資源解析器,就知道這玩意應該很屌,來吧,繼續點擊getLayout():
@NonNull
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
return loadXmlResourceParser(id, "layout");
}
沒啥實質性的內容,繼續點擊它的實現方法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類型的,然後繼續調用了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文件中的,這個方法就是爲我們具體所寫的佈局文件準備一個XML的解析器,所以它實際上就是一個XML的Pull解析的過程。需要注意的是:android的佈局實際上是一個XML文件,它在加載的時候會首先將它讀取到內存中,這個過程實際上就是一個IO過程,一般在android開發中操作IO都會將其置於工作線程中,所以這裏可能會成爲我們優化的一個方向。
關於這個XmlResourceParser就說到這裏,下面繼續回到上面說的那個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;
}
}
這裏同樣的省略了部分代碼,我們知道日常開發中經常會碰到一些報錯,其實這些報錯在Android的源碼中都是有所體現的,比如這裏定義的關於merge標籤的一個異常信息。接着看createViewFromTag()這個方法,看名字我們應該能大致猜測出來它是幹嘛的了,它應該就是通過一系列的Tag來創建相對應的View,我們點擊該方法跟進:
@UnsupportedAppUsage
private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
return createViewFromTag(parent, name, context, attrs, false);
}
這裏面又調用了另一個createViewFromTag()方法,繼續跟進:
@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;
}
}
這裏就到了重點的地方了,這裏面就是創建View的過程了:
首先:View view = tryCreateView(parent, name, context, attrs); 它通過這個tryCreateView()方法構建出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不爲空則調用Factory2的onCreateView()方法創建View對象,否則判斷Factory是否爲空,如果Factory不爲空則調用Factory的onCreateView()創建View對象,如果都爲空,則View爲空。如果view爲空並且PrivateFactory不爲空,則調用PrivateFactory的onCreateView()方法構建View,需要注意的是PrivateFactory它只用於Fragment標籤的加載。當這些條件都不滿足的時候,我們回到上面的createViewFromTag()方法中接着看,它會走到view==null的條件判斷中去,它會走onCreateView()或者createView(),點擊createView()繼續跟蹤:
@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的構造方法,通過反射的方式將其設置爲外部可調用的,然後下面final View view = constructor.newInstance(args); 這一行它通過構造函數反射創建了View,在這個方法中是真正進行了View的創建,當然這是在沒有使用Factory的情況下哦。這個過程實際上它是使用了反射,反射是有可能導致程序變慢的一個因素,所以這裏也可以作爲我們的一個優化點。
②、佈局加載流程總結
2.2、性能瓶頸
佈局文件解析:IO過程(文件過大時可能會導致卡頓)
創建View對象:反射(使用過多也會導致變慢)
2.3、LayoutInflater.Factory
在上面解讀setContentView的源碼時,我們知道創建View的過程優先是使用Factory2和Factory進行創建,下面對這兩個類作簡要說明:
LayoutInflater.Factory:
- LayoutInflater創建View的一個Hook,Hook其實就是我們可以將自己的代碼掛在它的原始代碼之上,可以對它的流程進行更改
- 定製創建View的過程:比如全局替換自定義TextView等
Factory與Factory2
- Factory2繼承於Factory
- 多了一個參數: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);
}
入參中有個name,來看一下它的註釋,意思就是我們要加載的Tag,比如這個Tag是TextView,那麼通過這個方法返回的就是TextView,實際上如果你繼續跟蹤的話,你會發現這個Tag實際上就是我們平時在佈局中寫的一個個的控件:比如TextView、ImageView等等,它會根據具體的Tag來進行對應View的創建:
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);
}
並且我們對比兩個接口,可以發現Factory2比Factory就是入參多了一個parent,這個parent就是你創建的View的parent,所以綜上可得Factory2比Factory功能上更加強大。
三、優雅獲取界面佈局耗時
隨着項目的不斷升級,項目體量逐漸變大,頁面可能也變的越來越多,然後我們希望能夠在線上進行統計,瞭解到具體哪些頁面用戶在進入時會出現卡頓,佈局文件加載也可能會導致卡頓。
常規方式:覆寫方法(setContentView)、手動埋點上報服務端(不夠優雅,代碼具有侵入性)
AOP方式:切Activity的setContentView(切面點)
@ Around("execution(*android.app.Activity.setContentView(..))")
具體實現:
@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));
}
結果如下:
對AOP忘記了的可以去看本專欄的第一篇啓動優化部分有介紹:《Android啓動優化你真的瞭解嗎?》
思考:如何獲取每一個控件加載耗時?
我們在上面使用setContentView獲取到的是頁面中所有控件的耗時情況,那現在我想要知道這個頁面中各個控件的耗時分佈情況,以便於整體的把控分析並且可以對耗時較多的控件做針對性的優化,這樣一個場景該如何實現呢?由於每個頁面佈局中的控件都是不可控的,有可能多也有可能少,所以我們應該儘量做到低侵入性,這個問題大家可以好好想想,看看有什麼解決方案。
解決方案:使用LayoutInflaterCompat.Factory2(LayoutInflaterCompat是LayoutInflater的兼容類)讓它在創建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;
}
});
結果如下:可以看到我們確實獲取到了列表Item中的每個控件的耗時情況
四、異步Inflate實戰
在上面我們已經說過了佈局文件加載慢主要的原因是有以下兩點:
- 佈局文件讀取慢:IO過程
- 創建View慢:通過反射創建一個對象比直接new一個對象要慢3倍,佈局嵌套層級複雜則反射更多
針對上面說的這兩種情況,相對應的解決套路也就是兩種:
- 根本性解決:去掉IO過程、不使用反射
- 側面緩解:讓主線程不耗時,不影響主線程
這裏針對側面緩解的方案來介紹一種實現方式:AsyncLayoutInflater,谷歌提供的一個類,簡稱異步Inflate
- WorkThread加載佈局,原生是在UI Thread加載佈局
- 加載完成之後回調主線程,此時主線程拿到的是創建完成的View對象可以直接使用
- 節約主線程時間,因爲耗時是發生在了異步線程中,主線程的響應能夠得到保障
使用方式:首先導入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的源碼,理解起來應該不難,這個類內部有一個Handler對象,一個InflateThread類繼承於Thread,還有一個inflate方法,該方法有三個入參resid、parent、callback,同時將這三個參數封裝成了InflateRequest的數據結構,然後加到線程的隊列中,線程中同時有一個run()方法在不斷執行,它會從隊列中取出一條InflateRequest,然後這個request.inflate開始執行inflate()方法並返回request.view,這個方法是執行在子線程中的,最後通過Handler將它回調到主線程中,同時有一個相關聯的Callback,在Callback中進行判斷如果沒有創建完成的話,會回退到主線程中進行佈局的加載,最後將request.view回調到onInflateFinished()方法中,這樣主線程就可以在該方法中拿到對應的view了。
總結:①、不能設置LayoutInflater.Factory(),需要自定義AsyncLayoutInflater解決;②、注意View中不能有依賴主線程的操作
五、X2C框架使用
上面這一部分是介紹了一種側面緩解的方式,那這一部分我們來思考一下從根本上解決該如何實現?
首先來說一下思路哈,其實也沒啥思路,就是利用Java代碼寫佈局,這種方案的特點如下:
- 本質上解決了性能問題(沒有xml文件也就沒有了IO的過程,直接new對象沒有了反射的過程)
- 引入新問題:不便於開發、可維護性差
思路有了但是看着實現起來卻不太現實哈,那咋辦呢?咋辦呢?咋辦呢?嗯,這樣拌,大神還是很多的,我們使用開源方案X2C:
X2C框架介紹:保留XML優點,解決其性能問題
- 開發人員寫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")
③、代碼實戰
將原有的setContentView註釋掉,然後使用X2C.setContentView()來設置佈局,運行之後發現是可以正常加載的,圖中左側圈出來的是使用X2C編譯之後的產物,這個其實就是它的底層實現原理了,我們來看一下:
首先是佈局文件:
<?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 context) {
return new com.zhangyue.we.x2c.layouts.X2C0_Activity_Main().createView(context);
}
}
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;
}
}
可以看到它內部就是將我們佈局文件中的控件全都以Java對象的形式給new出來了。
X2C存在的問題:
- XML中有的部分屬性Java不支持(雖然不多但是也有)
- 失去了系統的兼容(AppCompat,如果你需要使用AppCompatXXX下面的控件可以通過修改X2C源碼來定製化實現相關功能)
六、視圖繪製優化
①、視圖繪製流程
- 測量:確定大小(自頂向下進行視圖樹的遍歷,確定ViewGroup和View應該有多大)
- 佈局:確定位置(執行另一個自頂向下的遍歷操作,ViewGroup會根據測量階段測定的大小確定自己應該擺放的位置)
- 繪製:繪製視圖(對於視圖樹中的每個對象系統都會爲它創建一個Canvas對象,然後向GPU發送一條繪製命令進行繪製)
可能存在的性能問題:
- 每個階段耗時
- 自頂而下的遍歷(如果Layout層級比較深則遍歷也是很耗時的)
- 觸發多次(比如嵌套使用RelativeLayout有可能會導致繪製環節觸發多次)
②、佈局層級及複雜度
編寫佈局的準則:減少View樹層級
- 不嵌套使用RelativeLayout
- 不在嵌套的LinearLayout中使用weight
- merge標籤:減少一個層級,只能用於根View
這裏推薦使用:ConstraintLayout,網上關於它有很多的文章,後面我也準備專門寫一篇它的使用總結
- 實現幾乎完全扁平化佈局
- 構建複雜佈局性能更高
- 具有RelativeLayout和LinearLayout特性
③、過度繪製
- 一個像素最好只被繪製一次
- 調試GPU過度繪製
- 藍色可接受
避免過度繪製方法:
- 去掉多餘背景色,減少複雜shape使用
- 避免層級疊加
- 自定義View使用clipRect屏蔽被遮蓋View繪製(當覆寫onDraw()之後,系統就無法知道View中各個元素的位置和層級關係,就無法做自動優化,即無法自動忽略繪製那些不可見的元素)
④、佈局繪製的其它優化技巧
- ViewStub:高效佔位符、延遲初始化(這個標籤沒有大小,也沒有繪製功能不參與measure和layout過程,資源消耗非常低,一般用於延遲初始化)
- onDraw中避免:創建大對象、耗時操作
- TextView相關優化(setText顯示靜態文本)
關於佈局優化相關的知識點就總結這麼多了,有些地方還是通過實際代碼實踐之後纔能有所體會,好了,天也不早了,人也都走了,今天就先到這裏吧,下期再會!