子曰:溫故而知新,可以爲師矣。 《論語》-- 孔子
作爲性能優化專欄的第二篇,我們就來說一說 App 佈局優化的各種解決方案。
一、繪製原理
1. CPU
:中央處理器。CPU 的任務繁多,做邏輯計算外,還要做內存管理、顯示操作,因此在實際運算的時候性能會大打折扣,在沒有 GPU 的時代,不能顯示覆雜的圖形,其運算速度遠跟不上今天覆雜三維遊戲的要求。即使 CPU 的工作頻率超過 2GHz 或更高,對它繪製圖形提高也不大。這時 GPU 的設計就出來了。
2. GPU
:- 爲了提高圖形顯示效率以及複雜的圖形,設計出了 GPU。主要是爲了幫助 CPU 分擔圖形顯示,它負責柵格化(UI 元素繪製到屏幕上)。下面顯示一張 GPU 的圖:
-
黃色的 Control 爲控制器,用於協調控制整個 CPU 的運行,包括取出指令、控制其他模塊的運行等。
-
綠色的 ALU ( Arithmetic Logic Unit )是算術邏輯單元,用於進行數學、邏輯運算。
-
橙色的 Cache 和 DRAM 分別爲緩存和 RAM ,用於存儲信息。
從結構圖可以看出, CPU 的控制器較爲複雜,而 ALU 數量較少。因此 CPU 擅長各種複雜的邏輯運算,但不擅長數學尤其是浮點運算。 我們頁面是一個個像素點,以16進制展示,假如某個背景要從白色到紅色,明顯就是進制之間的轉化,那麼 ALU 顯然擅長這個,所以在 GPU 中 ALU 數量較多的原因。
3. xml 顯示過程
: Android 開發的 xml
佈局是如何顯示到頁面上? 下面這一張圖說明了整個流程:
4. 60Hz 刷新頻率
:Android 系統每隔 16ms 發出 VSYNC 信號(1000ms/60=16.66ms) ,觸發對 UI 進行渲染, 如果每次渲染都成功這樣就能夠達到流暢的畫面所需要的 60 fps ,爲了能夠實現 60 fps ,這意味着計算渲染的大多數操作都必須在 16ms 內完成。與手機交互過程中,如觸摸和反饋 60 幀以下人是能感覺出來的。 60 幀以上不能察覺變化,當幀率低於 60 fps 時感覺的畫面的卡頓和遲滯現象。
二、工具選擇
1. Layout Inspector
- Android Studio 自帶工具,在 tools 模塊下。
- 能夠查看視圖層次結構。
2. Choreographer
- 獲取 FPS,線上使用,具備實時性。
- Api 16 之後纔可以使用。
在你的任意界面中加入以下代碼:
private long mStartFrameTime = 0;
private int mFrameCount = 0;
private static final long MONITOR_INTERVAL = 160L; //單次計算FPS使用160毫秒
private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L;
private static final long MAX_INTERVAL = 1000L; //設置計算fps的單位時間間隔1000ms,即fps/s;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void getFPS() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
if (mStartFrameTime == 0) {
mStartFrameTime = frameTimeNanos;
}
long interval = frameTimeNanos - mStartFrameTime;
if (interval > MONITOR_INTERVAL_NANOS) {
double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL;
mFrameCount = 0;
mStartFrameTime = 0;
} else {
++mFrameCount;
}
Choreographer.getInstance().postFrameCallback(this);
}
});
}
運行項目,查看 Log
日誌:
如果你的界面 Log
日誌顯示 fps
都是 60
左右,那麼此頁面繪製是沒有什麼問題的。
三、佈局加載原理
流程
:setContentView --> LayoutInflater --> inflate --> getLayout --> createViewFromTag --> Factory --> createView(反射)。
性能瓶頸
:佈局文件解析是 IO
過程,可能耗性能。創建 View
對象使用的是 反射
。
四、優雅的獲取界面佈局耗時
1) 常規方式
- 覆寫方法,手動埋點。
2) AOP 實現
- 切
Activity
的setContentView()
方法。
具體做法:
@Aspect
public class PerformanceAop {
@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();
}
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
}
}
運行程序,查看 Log
日誌。
3)ARTHook
- 切 setContentView() 方法。
4)獲取每一個控件的加載耗時
以下方法要寫在 super.onCreate()
方法之前。
LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() {
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (TextUtils.equals(name, "TextView")) {
// 生成自定義TextView
}
long time = System.currentTimeMillis();
View view = getDelegate().createView(parent, name, context, attrs);
LogUtils.i(name + " cost " + (System.currentTimeMillis() - time));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
});
運行程序,查看 Log
日誌。
從日誌中可以看到我們這個界面每個控件耗時的時間。
五、異步 Inflate
實戰
1. 背景介紹
- 佈局文件讀取慢:IO 過程。
- 創建 View 慢: 反射(比 new 慢 3 倍)
2. 優化思路: AsyncLayoutInflater
AsyncLayoutInflater
:簡稱異步Inflate
。它是在WorkThread
加載佈局,然後回到主線程。好處是節約主線程時間。使用
:
// 1. 導包
implementation 'com.android.support:asynclayoutinflater:28.0.0-alpha1'
// super.onCreate() 之前寫入
new AsyncLayoutInflater(MainActivity.this).inflate(R.layout.activity_main, null, new AsyncLayoutInflater.OnInflateFinishedListener() {
@Override
public void onInflateFinished(@NonNull View view, int i, @Nullable ViewGroup viewGroup) {
setContentView(view);
mRecyclerView = findViewById(R.id.recycler_view);
mRecyclerView.setLayoutManager(new LinearLayoutManager(MainActivity.this));
mRecyclerView.setAdapter(mNewsAdapter);
mNewsAdapter.setOnFeedShowCallBack(MainActivity.this);
}
});
缺點: 無法向下兼容,在低版本可能有問題,需要自定義解決。
3. 優化思路: X2C
- 保留了 XML 優點,解決其性能問題。
- 開發人員寫 XML,加載 Java 代碼。
原理
:APT 編譯器翻譯 XML 爲 Java 代碼。使用
:
// 1. 導包
annotationProcessor 'com.zhangyue.we:x2c-apt:1.1.2'
implementation 'com.zhangyue.we:x2c-lib:1.0.6'
// 2. 打上標籤
@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity implements OnFeedShowCallBack {
//...
super.onCreate(savedInstanceState);
//setContentView(R.layout.activity_main);
X2C.setContentView(MainActivity.this, R.layout.activity_main);
}
缺點
:由於使用的是 new 的方式而不是使用反射,部分屬性 Java 不支持。同時也失去了系統的兼容(AppCompat)。
五、視圖繪製優化實戰
1. 佈局層級複雜度優化
- 儘量使用
ConstraintLayout
。 - 不嵌套使用
RelativeLayout
。不在嵌套LinearLayout
中使用weight
。 - 使用
merge
標籤,減少層級,但是它只能用於根View
。
2. 像素過度繪製優化
1)調試 GPU
過度繪製,藍色爲可接受。
查看項目佈局:
針對優化:去掉多餘背景色,減少複雜 shape
使用。 避免層級疊加。優化後:
2)自定義 View
使用 clipRect
屏蔽被遮蓋 View
繪製。
如圖的自定義View 就有重疊繪製的區域。在自定義 onDraw()
方法中優化:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Don't draw anything until all the Asynctasks are done and all the DroidCards are ready.
if (mDroids.length > 0 && mDroidCards.size() == mDroids.length) {
// Loop over all the droids, except the last one.
int i;
for (i = 0; i < mDroidCards.size() - 1; i++) {
mCardLeft = i * mCardSpacing;
canvas.save();
// 指定繪製區域
canvas.clipRect(mCardLeft,0,mCardLeft+mCardSpacing,mDroidCards.get(i).getHeight());
// Draw the card. Only the parts of the card that lie within the bounds defined by
// the clipRect() get drawn.
drawDroidCard(canvas, mDroidCards.get(i), mCardLeft, 0);
canvas.restore();
}
// Draw the final card. This one doesn't get clipped.
drawDroidCard(canvas, mDroidCards.get(mDroidCards.size() - 1),
mCardLeft + mCardSpacing, 0);
}
// Invalidate the whole view. Doing this calls onDraw() if the view is visible.
invalidate();
}
查看優化過後的顯示圖:
3)其他繪製優化技巧:
- 使用
Viewstub
: 高效佔位符,延遲初始化。 onDraw()
中避免創建大對象,耗時操作。- 能在一個平面顯示的內容,儘量只用一個容器;能複用的代碼,用
include
處理,可以減少GPU
重複工作: - 主題中的設置:去掉所有
Activity
主題設置中的屬性,直接在styles.xml
文件中設置<item name="android:windowBackground">@null</item>
。 - 非業務需要,不要去設置背景。意思是在 xml 文件中,有些整體區域的背景能不設置就不要設置,可以放在子控件中設置,就在子控件中設置。
寫在文末
紙上得來終覺淺,絕知此事要躬行。 《冬夜讀書示子聿》-- 陸游
好了,關於 App 佈局優化的各種方案
就說完了,各位小夥伴可以在項目中借鑑文章中給出的思路優化實戰一把。