(二) App 佈局優化實戰

子曰:溫故而知新,可以爲師矣。 《論語》-- 孔子


作爲性能優化專欄的第二篇,我們就來說一說 App 佈局優化的各種解決方案。


一、繪製原理

1. CPU:中央處理器。CPU 的任務繁多,做邏輯計算外,還要做內存管理、顯示操作,因此在實際運算的時候性能會大打折扣,在沒有 GPU 的時代,不能顯示覆雜的圖形,其運算速度遠跟不上今天覆雜三維遊戲的要求。即使 CPU 的工作頻率超過 2GHz 或更高,對它繪製圖形提高也不大。這時 GPU 的設計就出來了。

2. GPU:- 爲了提高圖形顯示效率以及複雜的圖形,設計出了 GPU。主要是爲了幫助 CPU 分擔圖形顯示,它負責柵格化(UI 元素繪製到屏幕上)。下面顯示一張 GPU 的圖:

CPU、GPU

  1. 黃色的 Control 爲控制器,用於協調控制整個 CPU 的運行,包括取出指令、控制其他模塊的運行等。

  2. 綠色的 ALU ( Arithmetic Logic Unit )是算術邏輯單元,用於進行數學、邏輯運算。

  3. 橙色的 Cache 和 DRAM 分別爲緩存和 RAM ,用於存儲信息。

從結構圖可以看出, CPU 的控制器較爲複雜,而 ALU 數量較少。因此 CPU 擅長各種複雜的邏輯運算,但不擅長數學尤其是浮點運算。 我們頁面是一個個像素點,以16進制展示,假如某個背景要從白色到紅色,明顯就是進制之間的轉化,那麼 ALU 顯然擅長這個,所以在 GPU 中 ALU 數量較多的原因。

3. xml 顯示過程: Android 開發的 xml 佈局是如何顯示到頁面上? 下面這一張圖說明了整個流程:
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 實現
  • ActivitysetContentView() 方法。

具體做法:

@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 佈局優化的各種方案 就說完了,各位小夥伴可以在項目中借鑑文章中給出的思路優化實戰一把。


碼字不易,如果本篇文章對您哪怕有一點點幫助,請不要吝嗇您的點贊,我將持續帶來更多優質文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章