深刻探索Android佈局優化(下)

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

在上篇文章中,筆者帶領你們學習了佈局優化涉及到的繪製原理、優化工具、監測手段等等知識。若是對這塊內容還不瞭解的建議先看看《深刻探索Android佈局優化(上)》。本篇,爲深刻探索Android佈局優化的下篇。這篇文章包含的主要內容以下所示:html

  • 六、佈局優化常規方案
  • 七、佈局優化的進階方案
  • 八、佈局優化的常見問題

下面,筆者將與你們一塊兒進入進行佈局優化的實操環節。前端

6、佈局優化常規方案

佈局優化的方法有不少,大部分主流的方案筆者已經在Android性能優化之繪製優化裏講解過了。下面,我將介紹一些其它的優化方案。java

一、佈局Inflate優化方案演進

一、代碼動態建立View

使用Java代碼動態添加控件的簡單示例以下:android

Button button=new Button(this);        
button.setBackgroundColor(Color.RED);
button.setText("Hello World");
ViewGroup viewGroup = (ViewGroup) LayoutInflater.from(this).inflate(R.layout.activity_main, null);
viewGroup.addView(button);
二、替換MessageQueue來實現異步建立View

在使用子線程建立視圖控件的時候,咱們能夠把子線程Looper的MessageQueue替換成主線程的MessageQueue,在建立完須要的視圖控件後記得將子線程Looper中的MessageQueue恢復爲原來的。在Awesome-WanAndroid項目下的UiUtils的Ui優化工具類中,提供了相應的實現,代碼以下所示:git

/**
 * 實現將子線程Looper中的MessageQueue替換爲主線程中Looper的
 * MessageQueue,這樣就可以在子線程中異步建立UI。
 *
 * 注意:須要在子線程中調用。
 *
 * @param reset 是否將子線程中的MessageQueue重置爲原來的,false則表示須要進行替換
 * @return 替換是否成功
 */
public static boolean replaceLooperWithMainThreadQueue(boolean reset) {
    if (CommonUtils.isMainThread()) {
        return true;
    } else {
        // 一、獲取子線程的ThreadLocal實例
        ThreadLocal<Looper> threadLocal = ReflectUtils.reflect(Looper.class).field("sThreadLocal").get();
        if (threadLocal == null) {
            return false;
        } else {
            Looper looper = null;
            if (!reset) {
                Looper.prepare();
                looper = Looper.myLooper();
                // 二、經過調用MainLooper的getQueue方法區獲取主線程Looper中的MessageQueue實例
                Object queue = ReflectUtils.reflect(Looper.getMainLooper()).method("getQueue").get();
                if (!(queue instanceof MessageQueue)) {
                    return false;
                }
                // 三、將子線程中的MessageQueue字段的值設置爲主線的MessageQueue實例
                ReflectUtils.reflect(looper).field("mQueue", queue);
            }

            // 四、reset爲false,表示須要將子線程Looper中的MessageQueue重置爲原來的。
            ReflectUtils.reflect(threadLocal).method("set", looper);
            return true;
        }
    }
}
三、AsynclayoutInflater異步建立View

在第三小節中,咱們對Android的佈局加載原理進行了深刻地分析,從中咱們得出了佈局加載過程當中的兩個耗時點:github

  • 一、佈局文件讀取慢:IO過程。
  • 二、建立View慢:使用反射,比直接new的方式要慢3倍。佈局嵌套層級越多,控件個數越多,反射的次數就會越頻繁。

很明顯,咱們沒法從根本上去解決這兩個問題,可是Google提供了一個從側面解決的方案:使用AsyncLayoutInflater去異步加載對應的佈局,它的特色以下:web

  • 一、工做線程加載佈局。
  • 二、回調主線程。
  • 三、節省主線程時間。

接下來,我將詳細地介紹AsynclayoutInflater的使用。json

首先,在項目的build.gradle中進行配置:canvas

implementation 'com.android.support:asynclayoutinflater:28.0.0'

而後,在Activity中的onCreate方法中將setContentView註釋:api

super.onCreate(savedInstanceState);
// 內部分別使用了IO和反射的方式去加載佈局解析器和建立對應的View
// setContentView(R.layout.activity_main);

接着,在super.onCreate方法前繼續佈局的異步加載:

// 使用AsyncLayoutInflater進行佈局的加載
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);
            // findViewById、視圖操做等
    }
});
super.onCreate(savedInstanceState);

接下來,咱們來分析下AsyncLayoutInflater的實現原理與工做流程。

因爲咱們是使用new的方式建立的AsyncLayoutInflater,因此咱們先來看看它的構造函數:

public AsyncLayoutInflater(@NonNull Context context) {
    // 1
    this.mInflater = new AsyncLayoutInflater.BasicInflater(context);
    // 2
    this.mHandler = new Handler(this.mHandlerCallback);
    // 3
    this.mInflateThread = AsyncLayoutInflater.InflateThread.getInstance();
}

在註釋1處,建立了一個BasicInflater,它內部的onCreateView並無使用Factory作AppCompat控件兼容的處理:

protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
    String[] var3 = sClassPrefixList;
    int var4 = var3.length;

    for(int var5 = 0; var5 < var4; ++var5) {
        String prefix = var3[var5];

        try {
            View view = this.createView(name, prefix, attrs);
            if (view != null) {
                return view;
            }
        } catch (ClassNotFoundException var8) {
        }
    }

    return super.onCreateView(name, attrs);
}

由前面的分析可知,在createView方法中僅僅是作了反射建立出對應View的處理。

接着,在註釋2處,建立了一個全局的Handler對象,主要是用於將異步線程建立好的View實例及其相關信息回調到主線程。

最後,在註釋3處,獲取了一個用於異步加載View的線程實例。

接着,咱們繼續跟蹤AsyncLayoutInflater實例的inflate方法:

@UiThread
public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull AsyncLayoutInflater.OnInflateFinishedListener callback) {
    if (callback == null) {
        throw new NullPointerException("callback argument may not be null!");
    } else {
        // 1
        AsyncLayoutInflater.InflateRequest request = this.mInflateThread.obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        this.mInflateThread.enqueue(request);
    }
}

在註釋1處,這裏使用InflateRequest對象將咱們傳進來的三個參數進行了包裝,並最終將這個InflateRequest對象加入了mInflateThread線程中的一個ArrayBlockingQueue中:

public void enqueue(AsyncLayoutInflater.InflateRequest request) {
    try {
        this.mQueue.put(request);
      } catch (InterruptedException var3) {
        throw new RuntimeException("Failed to enqueue async inflate request", var3);
    }
}

而且,在InflateThread這個靜態內部類的靜態代碼塊中調用了其自身實例的start方法以啓動線程:

static {
    sInstance.start();
}

public void run() {
    while(true) {
        this.runInner();
    }
}

public void runInner() {
    AsyncLayoutInflater.InflateRequest request;
    try {
        // 1
        request = (AsyncLayoutInflater.InflateRequest)this.mQueue.take();
    } catch (InterruptedException var4) {
        Log.w("AsyncLayoutInflater", var4);
        return;
    }

    try {
        // 2
        request.view = request.inflater.mInflater.inflate(request.resid, request.parent, false);
    } catch (RuntimeException var3) {
        Log.w("AsyncLayoutInflater", "Failed to inflate resource in the background! Retrying on the UI thread", var3);
    }

    // 3
    Message.obtain(request.inflater.mHandler, 0, request).sendToTarget();
}

在run方法中,使用了死循環的方式去不斷地調用runInner方法,在runInner方法中,首先在註釋1處從ArrayBlockingQueue隊列中獲取一個InflateRequest對象,而後在註釋2處將異步加載好的view對象賦值給了InflateRequest對象,最後,在註釋3處,將請求做爲消息發送給了Handler的handleMessage:

private Callback mHandlerCallback = new Callback() {
    public boolean handleMessage(Message msg) {
        AsyncLayoutInflater.InflateRequest request = (AsyncLayoutInflater.InflateRequest)msg.obj;
        // 1
        if (request.view == null) {
            request.view = AsyncLayoutInflater.this.mInflater.inflate(request.resid, request.parent, false);
        }

        request.callback.onInflateFinished(request.view, request.resid, request.parent);
        AsyncLayoutInflater.this.mInflateThread.releaseRequest(request);
        return true;
    }
};

在handleMessage方法中,當異步加載獲得的view爲null時,此時在註釋1處還作了一個fallback處理,直接在主線程進行view的加載,以此兼容某些異常狀況,最後,就調用了回調接口的onInflateFinished方法將view的相關信息返回給Activity對象。

小結

由以上分析可知,AsyncLayoutInflater是經過側面緩解的方式去緩解佈局加載過程當中的卡頓,可是它依然存在一些問題:

  • 一、不能設置LayoutInflater.Factory,須要經過自定義AsyncLayoutInflater的方式解決,因爲它是一個final,因此須要將代碼直接拷處進行修改。
  • 二、由於是異步加載,因此須要注意在佈局加載過程當中不能有依賴於主線程的操做。

因爲AsyncLayoutInflater僅僅只能經過側面緩解的方式去緩解佈局加載的卡頓,所以,咱們下面將介紹一種從根本上解決問題的方案。對於AsynclayoutInflater的改進措施,能夠查看祁同偉同窗封裝以後的代碼,具體的改進分析能夠查看Android AsyncLayoutInflater 限制及改進,這裏附上改進以後的代碼:

/**
* 實現異步加載佈局的功能,修改點:
* 1. 單一線程;
* 2. super.onCreate以前調用沒有了默認的Factory;
 * 3. 排隊過多的優化;
*/
public class AsyncLayoutInflaterPlus {

    private static final String TAG = "AsyncLayoutInflaterPlus";
    private Handler mHandler;
    private LayoutInflater mInflater;
    private InflateRunnable mInflateRunnable;
    // 真正執行加載任務的線程池
    private static ExecutorService sExecutor = Executors.newFixedThreadPool(Math.max(2,
        Runtime.getRuntime().availableProcessors() - 2));
    // InflateRequest pool
    private static Pools.SynchronizedPool<AsyncLayoutInflaterPlus.InflateRequest> sRequestPool = new Pools.SynchronizedPool<>(10);
    private Future<?> future;

    public AsyncLayoutInflaterPlus(@NonNull Context context) {
        mInflater = new AsyncLayoutInflaterPlus.BasicInflater(context);
        mHandler = new Handler(mHandlerCallback);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent, @NonNull CountDownLatch countDownLatch,
                    @NonNull AsyncLayoutInflaterPlus.OnInflateFinishedListener callback) {
        if (callback == null) {
            throw new NullPointerException("callback argument may not be null!");
        }
        AsyncLayoutInflaterPlus.InflateRequest request = obtainRequest();
        request.inflater = this;
        request.resid = resid;
        request.parent = parent;
        request.callback = callback;
        request.countDownLatch = countDownLatch;
        mInflateRunnable = new InflateRunnable(request);
        future = sExecutor.submit(mInflateRunnable);
    }

    public void cancel() {
        future.cancel(true);
    }

    /**
    * 判斷這個任務是否已經開始執行
    *
    * @return
    */
    public boolean isRunning() {
        return mInflateRunnable.isRunning();
    }

    private Handler.Callback mHandlerCallback = new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            AsyncLayoutInflaterPlus.InflateRequest request = (AsyncLayoutInflaterPlus.InflateRequest) msg.obj;
            if (request.view == null) {
                request.view = mInflater.inflate(
                    request.resid, request.parent, false);
            }
            request.callback.onInflateFinished(
                request.view, request.resid, request.parent);
            request.countDownLatch.countDown();
            releaseRequest(request);
            return true;
        }
    };

    public interface OnInflateFinishedListener {
        void onInflateFinished(View view, int resid, ViewGroup parent);
    }

    private class InflateRunnable implements Runnable {
        private InflateRequest request;
        private boolean isRunning;

        public InflateRunnable(InflateRequest request) {
            this.request = request;
        }

        @Override
        public void run() {
            isRunning = true;
            try {
                request.view = request.inflater.mInflater.inflate(
                    request.resid, request.parent, false);
            } catch (RuntimeException ex) {
                // Probably a Looper failure, retry on the UI thread
                Log.w(TAG, "Failed to inflate resource in the background! Retrying on the UI"
                    + " thread", ex);
            }
            Message.obtain(request.inflater.mHandler, 0, request)
                .sendToTarget();
        }

        public boolean isRunning() {
            return isRunning;
        }
    }

    private static class InflateRequest {
        AsyncLayoutInflaterPlus inflater;
        ViewGroup parent;
        int resid;
        View view;
        AsyncLayoutInflaterPlus.OnInflateFinishedListener callback;
        CountDownLatch countDownLatch;

        InflateRequest() {
        }
    }

    private static class BasicInflater extends LayoutInflater {
        private static final String[] sClassPrefixList = {
                "android.widget.",
                "android.webkit.",
                "android.app."
        };

        BasicInflater(Context context) {
            super(context);
            if (context instanceof AppCompatActivity) {
                // 加上這些能夠保證AppCompatActivity的狀況下,super.onCreate以前
                // 使用AsyncLayoutInflater加載的佈局也擁有默認的效果
                AppCompatDelegate appCompatDelegate = ((AppCompatActivity) context).getDelegate();
                if (appCompatDelegate instanceof LayoutInflater.Factory2) {
                    LayoutInflaterCompat.setFactory2(this, (LayoutInflater.Factory2) appCompatDelegate);
                }
            }
        }

        @Override
        public LayoutInflater cloneInContext(Context newContext) {
            return new AsyncLayoutInflaterPlus.BasicInflater(newContext);
        }

        @Override
        protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
            for (String prefix : sClassPrefixList) {
                try {
                    View view = createView(name, prefix, attrs);
                    if (view != null) {
                    return view;
                    }
                } catch (ClassNotFoundException e) {
                    // In this case we want to let the base class take a crack
                    // at it.
                }
            }

            return super.onCreateView(name, attrs);
        }
    }

    public AsyncLayoutInflaterPlus.InflateRequest obtainRequest() {
        AsyncLayoutInflaterPlus.InflateRequest obj = sRequestPool.acquire();
        if (obj == null) {
            obj = new AsyncLayoutInflaterPlus.InflateRequest();
        }
        return obj;
    }

    public void releaseRequest(AsyncLayoutInflaterPlus.InflateRequest obj) {
        obj.callback = null;
        obj.inflater = null;
        obj.parent = null;
        obj.resid = 0;
        obj.view = null;
        sRequestPool.release(obj);
    }
}

/**
* 調用入口類;同時解決加載和獲取View在不一樣類的場景
*/
public class AsyncLayoutLoader {

    private int mLayoutId;
    private View mRealView;
    private Context mContext;
    private ViewGroup mRootView;
    private CountDownLatch mCountDownLatch;
    private AsyncLayoutInflaterPlus mInflater;
    private static SparseArrayCompat<AsyncLayoutLoader> sArrayCompat = new SparseArrayCompat<AsyncLayoutLoader>();

    public static AsyncLayoutLoader getInstance(Context context) {
        return new AsyncLayoutLoader(context);
    }

    private AsyncLayoutLoader(Context context) {
        this.mContext = context;
        mCountDownLatch = new CountDownLatch(1);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent) {
        inflate(resid, parent, null);
    }

    @UiThread
    public void inflate(@LayoutRes int resid, @Nullable ViewGroup parent,
                    AsyncLayoutInflaterPlus.OnInflateFinishedListener listener) {
        mRootView = parent;
        mLayoutId = resid;
        sArrayCompat.append(mLayoutId, this);
        if (listener == null) {
            listener = new AsyncLayoutInflaterPlus.OnInflateFinishedListener() {
                @Override
                public void onInflateFinished(View view, int resid, ViewGroup parent) {
                    mRealView = view;
                }
            };
        }
        mInflater = new AsyncLayoutInflaterPlus(mContext);
        mInflater.inflate(resid, parent, mCountDownLatch, listener);
    }

    /**
    * getLayoutLoader 和 getRealView 方法配對出現
    * 用於加載和獲取View在不一樣類的場景
    *
    * @param resid
    * @return
    */
    public static AsyncLayoutLoader getLayoutLoader(int resid) {
        return sArrayCompat.get(resid);
    }

    /**
    * getLayoutLoader 和 getRealView 方法配對出現
    * 用於加載和獲取View在不一樣類的場景
    *
    * @param resid
    * @return
    */
    public View getRealView() {
        if (mRealView == null && !mInflater.isRunning()) {
            mInflater.cancel();
            inflateSync();
        } else if (mRealView == null) {
            try {
                mCountDownLatch.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            setLayoutParamByParent(mContext, mRootView, mLayoutId, mRealView);
        } else {
            setLayoutParamByParent(mContext, mRootView, mLayoutId, mRealView);
        }
        return mRealView;
    }


    /**
    * 根據Parent設置異步加載View的LayoutParamsView
    *
    * @param context
    * @param parent
    * @param layoutResId
    * @param view
    */
    private static void setLayoutParamByParent(Context context, ViewGroup parent, int layoutResId, View view) {
        if (parent == null) {
            return;
        }
        final XmlResourceParser parser = context.getResources().getLayout(layoutResId);
        try {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            ViewGroup.LayoutParams params = parent.generateLayoutParams(attrs);
            view.setLayoutParams(params);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            parser.close();
        }
    }

    private void inflateSync() {
        mRealView = LayoutInflater.from(mContext).inflate(mLayoutId, mRootView, false);
    }
}
四、使用X2C進行佈局加載優化

由上分析可知,在佈局加載的過程當中有兩個主要的耗時點,即IO操做和反射,而AsyncLayoutInflater僅僅是緩解,那麼有什麼方案能從根本上去解決這個問題呢?

使用Java代碼寫佈局?

若是使用Java代碼寫佈局,無疑從Xml文件進行IO操做的過程和反射獲取View實例的過程都將被抹去。雖然這樣從本質上解決了問題,可是也引入了一些新問題,如不便於開發,可維護性差等等。

那麼,還有沒有別的更好的方式呢?

答案就是X2C。

X2C

X2C項目地址

X2C框架保留了XML的優勢,並解決了其IO操做和反射的性能問題。開發人員只須要正常寫XML代碼便可,在編譯期,X2C會利用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'

而後,在對應的MainActivity類上方添加以下註解,讓MainActivity知道咱們使用的佈局是activity_main:

@Xml(layouts = "activity_main")
public class MainActivity extends AppCompatActivity implements OnFeedShowCallBack {

接着,將onCreate方法中setContentView的原始方式改成X2C的設置方式:

X2C.setContentView(MainActivity.this, R.layout.activity_main);

最後,咱們再Rebuild項目,會在build下的generated->source->apt->debug->com.zhangyue.we.x2c下自動生成X2C127_activity_main這個類:

public class X2C127_activity_main implements IViewCreator {
@Override
public View createView(Context context) {
    return new com.zhangyue.we.x2c.layouts.X2C127_Activity_Main().createView(context);
    }
}

在這個類中又繼續調用了layout目錄下的X2C127_Activity_Main實例的createView方法,以下所示:

public class X2C127_Activity_Main implements IViewCreator {
    @Override
    public View createView(Context ctx) {
        Resources res = ctx.getResources();

        ConstraintLayout constraintLayout0 = new ConstraintLayout(ctx);

        RecyclerView recyclerView1 = new RecyclerView(ctx);
        ConstraintLayout.LayoutParams layoutParam1 = new ConstraintLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);
        recyclerView1.setId(R.id.recycler_view);
        recyclerView1.setLayoutParams(layoutParam1);
        constraintLayout0.addView(recyclerView1);

        return constraintLayout0;
    }
}

從上可知,裏面採用了new的方式建立了相應的控件,並設置了對應的信息。

接下來,咱們回到X2C.setContentView(MainActivity.this, R.layout.activity_main)這個方法,看看它內部究竟作了什麼處理:

/**
 * 設置contentview,檢測若是有對應的java文件,使用java文件,不然使用xml
 *
 * @param activity 上下文
 * @param layoutId layout的資源id
 */
public static void setContentView(Activity activity, int layoutId) {
    if (activity == null) {
        throw new IllegalArgumentException("Activity must not be null");
    }
    // 1
    View view = getView(activity, layoutId);
    if (view != null) {
        activity.setContentView(view);
    } else {
        activity.setContentView(layoutId);
    }
}

在註釋1處,經過getView方法獲取到了對應的view,咱們繼續跟蹤進去:

public static View getView(Context context, int layoutId) {
    IViewCreator creator = sSparseArray.get(layoutId);
    if (creator == null) {
        try {
            int group = generateGroupId(layoutId);
            String layoutName = context.getResources().getResourceName(layoutId);
            layoutName = layoutName.substring(layoutName.lastIndexOf("/") + 1);
            String clzName = "com.zhangyue.we.x2c.X2C" + group + "_" + layoutName;
            // 1
            creator = (IViewCreator) context.getClassLoader().loadClass(clzName).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        //若是creator爲空,放一個默認進去,防止每次都調用反射方法耗時
        if (creator == null) {
            creator = new DefaultCreator();
        }
        sSparseArray.put(layoutId, creator);
    }
    // 2
    return creator.createView(context);
}

能夠看到,這裏採用了一個sSparseArray集合去存儲佈局對應的視圖建立對象creator,若是是首次建立creator的話,會在註釋1處使用反射的方式去加載處對應的creator對象,而後將它放入sSparseArray集中,最後在註釋2處調用了creator的createView方法去使用new的方式去建立對應的控件。

可是,X2C框架還存在一些問題:

  • 一、部分Java屬性不支持。
  • 二、失去了系統的兼容(AppCompat)

對於第2個問題,咱們須要修改X2C框架的源碼,當發現是TextView等控件時,須要直接使用new的方式去建立一個AppCompatTextView等兼容類型的控件。於此同時,它還有以下兩個小的點不支持,可是這個問題不大:

  • merge標籤 ,在編譯期間沒法肯定xml的parent,因此沒法支持。
  • 系統style,在編譯期間只能查到應用的style列表,沒法查詢系統style,因此只支持應用內style。

二、使用ConstraintLayout下降佈局嵌套層級

首先,對於Android視圖繪製的原理,咱們必需要有必定的瞭解,關於這塊,你們能夠參考下[Android View的繪製流程
](https://jsonchao.github.io/20...

對於視圖繪製的性能瓶頸,大概有如下三點:

  • 一、測量、佈局、繪製每一個階段的耗時。
  • 二、自頂而下的遍歷,當嵌套層級過多時,遍歷耗時會比較明顯。
  • 三、無效的嵌套佈局或不合理使用RelativeLayout可能會致使觸發屢次繪製。

那麼,如何減小布局的層級及複雜度呢?

基本上只要遵循如下兩點便可:

  • 一、減小View樹層級。
  • 二、寬而淺,避免窄而深。

爲了提高佈局的繪製速度,Google推出了ConstraintLayout,它的特色以下:

  • 一、實現幾乎徹底扁平化的佈局。
  • 二、構建複雜佈局性能更高。
  • 三、具備RelativeLayout和LinearLayout的特性。

接下來,咱們來簡單使用一下ConstraintLayout來優化一下咱們的佈局。

首先,下面是咱們的原始佈局:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/ll_out"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="5dp">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal">

        <com.facebook.drawee.view.SimpleDraweeView
            android:id="@+id/iv_news"
            android:layout_width="80dp"
            android:layout_height="80dp"
            android:scaleType="fitXY" />

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="10dp"
            android:textSize="20dp" />
    </LinearLayout>

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="right"
        android:padding="3dp"
        android:text="來自NBA官網"
        android:textSize="14dp" />
</LinearLayout>

能夠看到,它具備三層嵌套結構,而後咱們來使用ConstraintLayout來優化一下這個佈局:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/ll_out"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="5dp">

    <com.facebook.drawee.view.SimpleDraweeView
        android:id="@+id/iv_news"
        android:layout_width="80dp"
        android:layout_height="80dp"
        android:scaleType="fitXY"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:paddingLeft="10dp"
        android:textSize="20dp"
        app:layout_constraintLeft_toRightOf="@id/iv_news"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="@id/iv_news" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:padding="3dp"
        android:text="來自NBA官網"
        android:textSize="14dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/iv_news" />
</android.support.constraint.ConstraintLayout>

通過ConstraintLayout以後,佈局的嵌套層級變爲了2級,若是佈局比較複雜,好比有5,6,7層嵌套層級,使用Contraintlayout以後下降的層級會更加明顯。對於其app下的一系列屬性,其實都很是簡單,這裏就很少作介紹了。

除此以外,還有如下方式能夠減小布局層級和複雜度:

  • 一、不嵌套使用RelativeLayout。
  • 二、不在嵌套LinearLayout中使用weight。
  • 三、使用merge標籤,它可以減小一個層級,但只能用於根View。

三、過渡繪製優化

在視圖的繪製優化中,還有一個比較重要的優化點,就是避免過渡繪製,這個筆者已經在Android性能優化之繪製優化一文的第四小節詳細分析過了。最後這裏補充一下自定義View中使用clipRect的一個實例。

首先,咱們自定義了一個DroidCardsView,他能夠存放多個疊加的卡片,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;

            // 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);

        }

        // 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();
}

從以上代碼可知,這裏是直接進行繪製的,此時顯示的佈局過渡繪製背景以下所示:

image

能夠看到,圖片的背景都疊加起來了,這個時候,咱們須要在繪製的時候使用clipRect讓系統去識別可繪製的區域,所以咱們在自定義的DroidCardsView的onDraw方法去使用clipRect:

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;
            
            // 一、clipRect方法和繪製先後成對使用canvas的save方法與restore方法。
            canvas.save();
            // 二、使用clipRect指定繪製區域,這裏的mCardSpacing是指的相鄰卡片最左邊的間距,須要在動態建立DroidCardsView的時候傳入。

            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();
}

在註釋1處,首先須要在clipRect方法和繪製先後成對使用canvas的save方法與restore方法用來對畫布進行操做。接着,在註釋2處,使用clipRect指定繪製區域,這裏的mCardSpacing是指的相鄰卡片最左邊的間距,須要在動態建立DroidCardsView的時候傳入。最後,在註釋3處調用實際繪製卡片的方法。

使用clipRect優化事後的佈局過渡繪製背景以下所示:

image

注意:

咱們還能夠經過canvas.quickReject方法來判斷是否沒和某個矩形相交,以跳過非矩形區域的繪製操做

固然,對視圖的繪製優化還有其它的一些優化操做,好比:

  • 一、使用ViewStub、Merge,ViewStub是一種高效佔位符,用於延遲初始化。
  • 二、onDraw中避免建立大對象,進行耗時操做。
  • 三、TextView的優化,好比利用它的drawableLeft屬性。此外,也能夠使用Android 9.0以後的 PrecomputedText,它將文件的measure與layout過程進行了異步化。可是須要注意,若是要顯示的文本比較少,反而會形成沒必要要的Scheduling delay,建議文本字符大於200時才使用,並記得使用其兼容類PrecomputedTextCompat,它在9.0以上使用PrecomputedText進行優化,在5.0~9.0使用StaticLayout進行優化。具體調用代碼以下所示:
Future<PrecomputedTextCompat> future = PrecomputedTextCompat.getTextFuture(
                「text」, textView.getTextMetricsParamsCompat(), null);
textView.setTextFuture(future);

到這裏,筆者就將常規的佈局優化講解完了,是否是頓時感受實力大增呢?

<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/15/16fa6d9b683d6201?w=351&h=273&f=jpeg&s=10429" width=30%>
</div>

若是你此時心裏已經YY到這種程度,那我只能說:

<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/15/16fa6d9b6b785e40?w=250&h=250&f=gif&s=94966" width=30%>
</div>

對於Android的佈局優化還有更深刻的優化方式嗎?

沒錯,下面,筆者就來和你們一塊兒來探索佈局優化的進階方案。

7、佈局優化的進階方案

一、使用異步佈局框架Litho

Litho是Facebook開源的一款在Android上高效創建UI的聲明式框架,它具備如下特色:

  • 聲明式:它使用了聲明式的API來定義UI組件。
  • 異步佈局:它能夠提早佈局UI,而不會阻塞UI線程。
  • 視圖扁平化:它使用了Facebook開源的另外一款佈局引擎Yoga進行佈局,以自動減小UI包含的ViewGroup數量
  • 細粒度的回收:能夠回收文本或圖形等任何組件,並能夠在用戶界面的任何位置重複使用
  • 內部不只支持使用View來渲染視圖,還可使用更輕量的Drawable來渲染視圖。Litho實現了大量使用Drawable來渲染的基礎組件,能夠進一步使佈局扁平化
簡單使用Litho

接下來,咱們在項目裏面來使用Litho。

一、首先,咱們須要配置Litho的相關依賴,以下所示:

// 項目下
repositories {
    jcenter()
}

// module下
dependencies {
    // ...
    // Litho
    implementation 'com.facebook.litho:litho-core:0.33.0'
    implementation 'com.facebook.litho:litho-widget:0.33.0'

    annotationProcessor 'com.facebook.litho:litho-processor:0.33.0'

    // SoLoader
    implementation 'com.facebook.soloader:soloader:0.5.1'

    // For integration with Fresco
    implementation 'com.facebook.litho:litho-fresco:0.33.0'

    // For testing
    testImplementation 'com.facebook.litho:litho-testing:0.33.0'
    
    // Sections (options,用來聲明去構建一個list)
    implementation 'com.facebook.litho:litho-sections-core:0.33.0'
    implementation 'com.facebook.litho:litho-sections-widget:0.33.0'
    compileOnly 'com.facebook.litho:litho-sections-annotations:0.33.0'

    annotationProcessor 'com.facebook.litho:litho-sections-processor:0.33.0'
}

二、而後,在Application下的onCreate方法中初始化SoLoader:

@Override
public void onCreate() {
    super.onCreate();

    SoLoader.init(this, false);
}

從以前的介紹可知,咱們知道Litho使用了Yoga進行佈局,而Yoga包含有native依賴,在Soloader.init方法中對這些native依賴進行了加載。

三、最後,在Activity的onCreate方法中添加以下代碼便可顯示單個的文本視圖:

// 一、將Activity的Context對象保存到ComponentContext中,並同時初始化
// 一個資源解析者實例ResourceResolver供其他組件使用。
ComponentContext componentContext = new ComponentContext(this);

// 二、Text內部使用建造者模式以實現組件屬性的鏈式調用,下面設置的text、
// TextColor等屬性在Litho中被稱爲Prop,此概念引伸字React。
Text lithoText = Text.create(componentContext)
        .text("Litho text")
        .textSizeDip(64)
        .textColor(ContextCompat.getColor(this, R.color.light_deep_red))
            .build();

// 三、設置一個LithoView去展現Text組件:LithoView.create內部新建了一個
// LithoView實例,並用給定的Component(lithoText)進行初始化
setContentView(LithoView.create(componentContext, lithoText));

顯示效果以下所示:

image

在上面的示例中,咱們僅僅是將Text這個子組件設置給了LithoView,後續爲了實現更復雜的佈局,咱們須要使用帶多個子組件的根組件去替換它。

使用自定義Component

由上可知,在Litho中的視圖單元叫作Component,即組件,它的設計理念來源於React組件化的思想。每一個組件持有描述一個視圖單元所必須的屬性與狀態,用於視圖佈局的計算工做。視圖最終的繪製工做是由組件指定的繪製單元(View或Drawable)來完成的。接下來,咱們使用Litho提供的自定義Component的功能,它可以讓咱們實現更復雜的Component,這裏咱們來實現一個相似ListView的列表。

首先,咱們先來實現一個ListItem Component,它就如ListView的itemView同樣。在下面的實戰中,咱們將會學習到全部的基礎知識,這將會支撐你後續能實現更多更復雜的Component。

而後,在Litho中,咱們須要先寫一個Spec類去聲明Component所對應的佈局,在這裏須要使用@LayoutSpec規範註解(除此以外,Litho還提供了另外一種類型的組件規範:Mount Spec)。代碼以下所示:

@LayoutSpec
public class ListItemSpec {

    @OnCreateLayout
    static Component onCreateLayout(ComponentContext context) {
        // Column的做用相似於HTML中的<div>標籤
        return Column.create(context)
                .paddingDip(YogaEdge.ALL, 16)
                .backgroundColor(Color.WHITE)
                .child(Text.create(context)
                            .text("Litho Study")
                            .textSizeSp(36)
                         .textColor(Color.BLUE)
                            .build())
                .child(Text.create(context)
                            .text("JsonChao")
                            .textSizeSp(24)
                         .textColor(Color.MAGENTA)
                            .build())
                .build();
    }
}

而後,框架會使用APT技術去幫助生成對應的ListItem Component 類。最後,咱們在Activity的onCreate中將上述第一個例子中的第二步改成以下:

// 二、構建ListItem組件
ListItem listItem = ListItem.create(componentContext).build();

運行項目,顯示界面以下所示:

image

那上述過程是如何進行構建的呢?

它看起來就像有一個LithoSpec的類名,而且在項目構建以後生成了一個與LithoSpec有着一樣包名的Litho類,以下所示:

image

image

相似於Litho這種類中的全部方法參數都會由Litho進行自動填充。此外,基於這些規格,將會有一些額外的方法由註解處理器自動生成,例如上述示例中Column或Row中的Text的TextSizeSp、backgroundColor等方法。(Row和Column分別對應着Flexox中的行和列,它們都實現了Litho中另外一種特殊的組件Layout)

補充:MountSpec規範

MountSpec是用來生成可掛載類型組件的一種規範,它的做用是用來生成渲染具體的View或者Drawable的組件。同LayoutSpec相似,它必須使用@MountSpec註解來標註,並實現一個標註了@onCreateMountContent的方法。可是MountSpec的實現要比Layout更加地複雜,由於它擁有本身的生命週期,以下所示:

  • @OnPrepare:準備階段,用於進行一些初始化操做。
  • @OnMeasure:負責佈局的計算工做。
  • @OnBoundsDefined:在佈局計算完成以後、掛載視圖以前作一些操做。
  • @OnCreateMountContent:若是沒有能夠複用的視圖單元,則調用它去建立須要掛載的視圖。
  • @OnMount:掛載視圖,用於完成佈局相關的設置。
  • @OnBind:綁定視圖,用於完成數據和視圖的綁定。
  • @OnUnBind:解綁視圖,與@OnBind相對,主要用於重置視圖的數據屬性,避免出現數據複用的問題。
  • @OnUnmount:卸載視圖,與@OnMount相對,主要用於重置視圖的佈局相關的屬性,避免出現佈局複用的問題。

MountSpec的生命週期流轉圖以下所示:

image

在使用Litho完成了兩個實例的開發以後,相信咱們已經對Litho的佈局方式已經有了一個感性的認知。那麼,Litho究竟是如何進行佈局優化的呢?在佈局優化中它所作的核心工做有哪些?

Litho在佈局優化中所作的核心工做包括如下三點:

  • 一、異步佈局化。
  • 二、佈局自動扁平化。
  • 三、更細粒度地優化RecyclerView中組件的緩存與回收的方法。
一、異步佈局化

在前文中,咱們知道Android的佈局加載過程一般會前後涉及到measure、layout、draw過程,而且它們都是在主線程執行的,若是方法執行過程當中耗時太多,則主界面必然會產生卡頓現象。

還記得咱們在前面介紹的PrecomputedText,它內部將measure與layout的過程放在了異步線程進行初始化,而Litho與PrecomputedText相似,也是將measre與layout的過程進行了異步化,核心原理就是利用CPU的閒置時間提早在異步線程中完成measure和layout的過程,僅在UI線程中完成繪製工做

那麼Android爲何不本身實現異步佈局呢?

主要有如下兩緣由:

  • 一、由於View的屬性是可變的,只要屬性發生變化就可能致使佈局變化,因此須要從新計算佈局,那麼提早異步去計算佈局的意義就不大了。而Litho組件的屬性是不可變的,所以它的佈局計算結果也是不變的。
  • 二、提早異步佈局須要去提早建立好接下來用到的若干條目的視圖,可是Android原生的View做爲視圖單元,不只包含一個視圖的全部屬性,並且還負責視圖的繪製工做。若是要在繪製前提早去計算佈局,就須要預先去持有大量未展現的View實例,這將會大大增長App進程的內存佔用。對於Litho的組件來講,它只是視圖屬性的一個集合,僅僅負責計算佈局,繪製工做由指定的繪製單元來完成。所以在Litho中,提早建立好下面要用到的多個條目的組件,是不會有性能問題的。二者的繪製原理簡圖以下所示:

image

二、佈局自動扁平化

通過以前的學習,咱們瞭解到Litho採用了一套自有的佈局引擎Yoga,它會在佈局的過程當中去檢測出沒必要要的佈局嵌套層級,並自動去減小多餘的層級以實現佈局的扁平化,這能夠顯著減小渲染時的遞歸調用,加快渲染速度。例如,在實現一個圖片帶多個文字的佈局中,咱們一般會至少有兩個佈局層級,固然,你也可使用TextView的drawableStart方法 + 代碼動態佈局使用Spannable/Html.fromHtml(用來實現多種不一樣規格的文字) + lineSpaceExtra/lineSpacingMultiplier(用來調整多行文本的顯示間距)來將佈局層級降爲一層,可是這種實現方式比較繁瑣,而經過使用Litho,咱們能夠把下降佈局嵌套層級的任務所有丟給佈局引擎Yoga去處理。由前面可知,Litho是使用Flexbox來建立佈局的,並最終生成帶有層級結構的組件樹。經過使用Yoga來進行佈局計算,可使用Flexbox的相對佈局變成了只有一層嵌套的絕對佈局。相比於ConstraintLayout,對於實現複雜佈局的時候可讀性會更好一些,由於ConstraintLayout此時會有過多的約束條件,這會致使可讀性變差。此外,Litho自身還提供了許多掛載Drawable的基本視圖組件,相比Viwe組件使用它們能夠顯著減小內存佔用(一般會減小80%的內存佔用)。Litho實現佈局自動扁平化的原理圖以下所示:

image

三、更細粒度地優化RecyclerView中組件的緩存與回收的方法

使用了RecyclerView與ListView這麼久,咱們明白它是以viewType爲粒度來對一個組件集合統一進行緩存與回收的,而且,當viewType的類型越多,其對組件集合的緩存與回收的效果就會越差。相對於RecyclerView與ListView緩存與回收的粗粒度而言,Litho實現了更細粒度的回收機制,它是以Text、image、video等單個Component爲粒度來做爲其基準的,具體實現原理在item回收前,會把LithoView中掛載的各個繪製單元進行解綁拆分出來,由Litho本身的緩存池去分類回收,而後在展現前由LithoView按照組件樹的樣式掛載組裝各個繪製單元,這樣就達到了細粒度複用的目的。毫無疑問,這不只提升了其緩存的命中率與內存的使用率,也下降了提升了其滾動刷新的頻率。更細粒度複用優化內存的原圖以下所示:

image

由上圖能夠看出,滑出屏幕的itemType1會被拆分紅一個個的視圖單元。其中LithoView容器由Recycler緩存池回收,而其餘視圖單元則由Litho的緩存池分類回收,例如分類爲Img緩存池、Text緩存池等等。

如今,咱們對Litho已經比較瞭解了,它彷佛很完美,可是任何事物都有其弊端,在學習一個新的事物時,咱們不只僅只去使用與瞭解它的優點,更應該對它的缺陷與弊端了如指掌。Litho在佈局的過程當中,使用了相似React的單向數據流設計,而且因爲Litho是使用代碼進行動態佈局,這大大增長了佈局的複雜度,並且,代碼佈局是沒法實時預覽的,這也增長了開發調試時的難度。

綜上,對於某些性能性能要求高的場景,能夠先使用Litho佈局的方式去替換,特別是須要利用好Litho中的RecyclerViewCollectionComponent與sections去充分提高RecylerView的性能。

如今,咱們來使用RecyclerViewCollectionComponent與sections去建立一個可滾動的列表單元。

接下來,咱們須要使用SectionsAPI,它能夠將列表分爲多個Section,而後編寫GroupSectionSpec註解類來聲明每一個Section須要呈現的內容及其使用的數據。下面,咱們建立一個ListSectoinSpec:

// 一、能夠理解爲一個組合Sectoin規格
@GroupSectionSpec
public class ListSectionSpec {

    @OnCreateChildren
    static Children onCreateChildren(final SectionContext context) {
        Children.Builder builder = Children.create();

        for (int i = 0; i < 20; i++) {
            builder.child(
                   // 單組件區域用來包含一個特定的組件 
                   SingleComponentSection.create(context)
                    .key(String.valueOf(i))
                    .component(ListItem.create(context).build())
        };

        return builder.build();
    }
}

而後,咱們將MainActivity onCreate方法中的步驟2替換爲以下代碼:

// 二、使用RecyclerCollectionComponent去繪製list
    RecyclerCollectionComponent recyclerCollectionComponent = RecyclerCollectionComponent.create(componentContext)
            // 使下拉刷新實現
            .disablePTR(true)
            .section(ListSection.create(new SectionContext(componentContext)).build())
            .build();

最終的顯示效果以下所示:

image

若是咱們須要顯示不一樣UI的ListItem該怎麼辦呢?

這個時候咱們須要去自定義Component的屬性,即props,它是一種不可變屬性(此外還有一種可變屬性稱爲State,可是其變化是由組件內部進行控制的,例如輸入框、Checkbox等都是由組件內部去感知用戶的行爲,並由此更新組件的State屬性),你設置的這些屬性將會改變Component的行爲或表現。Props是Component Spec中方法的參數,而且使用@Prop註解。

下面,咱們使用props將ListItemSpec的onCreateLayout修改成可自定義組件屬性的方法,以下所示:

@LayoutSpec
public class ListItemSpec {

    @OnCreateLayout
    static Component onCreateLayout(ComponentContext context,
                                @Prop int bacColor,
                                @Prop String title,
                                @Prop String subTitle,
                                @Prop int textSize,
                                @Prop int subTextSize) {
        // Column的做用相似於HTML中的<div>標籤
        return Column.create(context)
                .paddingDip(YogaEdge.ALL, 16)
                .backgroundColor(bacColor)
                .child(Text.create(context)
                            .text(title)
                         .textSizeSp(textSize)
                         .textColor(Color.BLUE)
                            .build())
                .child(Text.create(context)
                            .text(subTitle)
                         .textSizeSp(subTextSize)
                         .textColor(Color.MAGENTA)
                            .build())
                .build();
    }
}

奇妙之處就發生在咱們所定義的@Prop註解與註解處理器之間,註解處理器以一種智能的方對組件構建過程當中所關聯的屬性生成了對應的方法

接下來,咱們再修改ListSectionSpec類,以下所示:

@GroupSectionSpec
public class ListSectionSpec {

    @OnCreateChildren
    static Children onCreateChildren(final SectionContext context) {
        Children.Builder builder = Children.create();

        for (int i = 0; i < 20; i++) {
            builder.child(
                    SingleComponentSection.create(context)
                    .key(String.valueOf(i))
                    .component(ListItem.create(context)
                            .bacColor(i % 2 == 0 ? Color.BLUE:Color.MAGENTA)
                            .title("第" + i + "次練習")
                         .subTitle("JsonChao")
                            .textSize(36)
                            .subTextSize(24)
                            .build())
            );
        }

        return builder.build();
    }
}

最終的顯示效果以下所示:

image

除此以外,咱們還能夠有更多的方式去定義@Prop,以下所示:

@Prop(optional = true, resType = ResType.DIMEN_OFFSET) int shadowRadius,

上面定義了一個可選的Prop,傳入的shadowRadius是支持dimen規格的,如px、dp、sp等等。

小結

使用Litho,在佈局性能上有很大的提高,可是開發成本過高,由於須要本身去實現不少的組件,而且其組件須要在編譯時才能生成,不可以進行實時預覽,可是能夠把Litho封裝成Flexbox佈局的底層渲染引擎,以此實現上層的動態化,具體實現原理可參見Litho在美團動態化方案MTFlexbox中的實踐

二、使用Flutter實現高性能的UI佈局

Flutter能夠說是2019最火爆的框架之一了,它是 Google 開源的 UI 工具包,幫助開發者經過一套代碼庫高效構建多平臺精美應用,支持移動、Web、桌面和嵌入式平臺。對於Android來講,FLutter可以創做媲美原生的高性能應用,應用使用 Dart語言進行 開發。Flutter的架構相似於Android的層級架構,每一層都創建在前一層之上,其架構圖以下所示:

image

在Framework層中,Flutter經過在 widgets 層組合基礎 widgets 來構建 Material 層,而 widgets 層自己則是經過對來自 Rendering 層的低層次對象組合而來。而在Engine層,Flutter集成了Skia引擎用於進行柵格化,而且使用了Dart虛擬機。

那麼Flutter的圖形性能爲什麼可以媲美原生應用呢?

接下來,咱們以Flutter、原生Android、其它跨平臺框架如RN來作比較,它們的圖形繪製調用層級圖以下所示:

image

能夠看到,Flutter框架的代碼徹底取代了Java層的框架代碼,因此只要當Flutter框架中Dart代碼的效率能夠媲美原生框架的Java代碼的時候,那麼整體的Flutter App的性能就可以媲美原生的APP。而反觀其它流行的跨平臺框架如RN,它首先須要調用自身的Js代碼,而後再去調用Java層的代碼,這裏比原生和Flutter的App顯然多出來一個步驟,因此它的性能確定是不及原生的APP的。此外,Flutter App不一樣於原生、RN,它內部是直接包含了Skia渲染引擎的,只要Flutter SDK進行升級,Skia就可以升級,這樣Skia的性能改進就可以同步到Flutter框架之中。而對於Android原生和RN來講,只能等到Android系統升級才能同步Skia的性能改進。

而Flutter又是如何實現高性能UI佈局的呢?

接下來,咱們來大體瞭解一下Flutter的UI繪製原理,它主要是經過VSYNC信號來使UI線程和GPU線程有條不紊的週期性的去渲染界面,其繪製原理圖以下所示:

image

繪製步驟大體以下:

  • 一、首先 UI Runner 會執行 root isolate(可簡單理解爲Dart VM的線程),它會告訴引擎層有幀要渲染,當須要渲染則會調用到Engine的ScheduleFrame()來註冊VSYNC信號回調,一旦觸發回調doFrame(),並當它執行完成後,便會移除回調方法,也就是說一次註冊一次回調。
  • 二、當須要再次繪製則須要從新調用到ScheduleFrame()方法,該方法的惟一重要參數regenerate_layer_tree決定在幀繪製過程是否須要從新生成layer tree,仍是直接複用上一次的layer tree。
  • 三、接着,執行的是UI線程繪製過程當中最核心的WidgetsBinding的drawFrame()方法,而後會建立layer tree視圖樹。
  • 四、而後 Layer Tree 會交給 GPU Task Runner 進行合成和柵格化。
  • 五、最後,GPU Task Runner會利用Skia庫結合GL或Vu'lkan將layer tree提供的信息轉化爲平臺可執行的GPU指令。

此外,Flutter 也採用了相似 Litho 的props屬性不可變、Reat單向數據流的方案,用於將視圖與數據分離。對於Flutter這一大前端領域的核心技術,筆者也是充滿興趣,後續會有計劃對此進行深刻研究,敬請期待。

三、使用RenderThread 與 RenderScript

在Android 5.0以後,Android引進了RenderThread,它可以實現動畫的異步渲染。可是目前支持RenderThread徹底渲染的動畫,只有兩種,即ViewPropertyAnimator和CircularReveal(揭露動畫)。對於CircularReveal使用比較簡單且功能較爲單一,就很少作過多的描述了。下面我簡單說下ViewPropertyAnimator中如何去利用RenderThread。

一、在ViewPropertyAnimator類系中,有一個ViewPropertyAnimatorRT ,它的主要做用就把動畫交給RenderThread去處理。所以,咱們須要先去建立對應view的ViewPropertyAnimatorRT,代碼以下所示:
/**
 * 使用反射的方式去建立對應View的ViewPropertyAnimatorRT(非hide類)
 */
private static Object createViewPropertyAnimatorRT(View view) {
    try {           
        Class<?> animRtClazz = Class.forName("android.view.ViewPropertyAnimatorRT");
        Constructor<?> animRtConstructor = animRtClazz.getDeclaredConstructor(View.class);
        animRtConstructor.setAccessible(true);
        Object animRt = animRtConstructor.newInstance(view);            
        return animRt;
    } catch (Exception e) {            
        Log.d(TAG, "建立ViewPropertyAnimatorRT出錯,錯誤信息:" + e.toString());           
        return null;
    }
}
二、接下來,咱們須要將ViewPropertyAnimatorRT設置給ViewPropertyAnimator的mRTBackend字段,這樣ViewPropertyAnimator才能利用它去將動畫交給RenderThread處理,以下所示:
private static void setViewPropertyAnimatorRT(ViewPropertyAnimator animator, Object rt) {       
 try {
        Class<?> animClazz = Class.forName("android.view.ViewPropertyAnimator");
        Field animRtField = animClazz.getDeclaredField("mRTBackend");
        animRtField.setAccessible(true);
        animRtField.set(animator, rt);
    } catch (Exception e) {
        Log.d(TAG, "設置ViewPropertyAnimatorRT出錯,錯誤信息:" + e.toString());
    }
}

/**
 * 在animator.start()即執行動畫開始以前配置的方法
 */
public static void onStartBeforeConfig(ViewPropertyAnimator animator, View view) {
    Object rt = createViewPropertyAnimatorRT(view);
    setViewPropertyAnimatorRT(animator, rt);
}
三、最後,在開啓動畫以前將ViewPropertyAnimatorRT實例設置進去便可,以下所示:
ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000);
AnimHelper.onStartBeforeConfig(animator, v);
animator.start();

當前,若是是作音視頻或圖像處理的工做,常常須要對圖片進行高斯模糊、放大、銳化等操做,可是這裏涉及大量的圖片變換操做,例如縮放、裁剪、二值化以及降噪等。而圖片的變換又涉及大量的計算任務,這個時候咱們能夠經過RenderScript去充分利用手機的GPU計算能力,以實現高效的圖片處理

而RenderScript的工做流程須要經歷以下三個步驟:

  • 一、RenderScript運行時API:提供進行運算的API。
  • 二、反射層:至關於NDK中的JNI膠水代碼,它是一些由Android編譯工具自動生成的類,對咱們寫的RenderScript代碼進行包裝,以使得安卓層可以和RenderScript進行交互。
  • 三、安卓框架:經過調用反射層來訪問RenderScript運行時。

因爲RenderScript主要是用於音視頻、圖像處理等細分領域,這裏筆者就不繼續深刻擴展了,對於NDK、音視頻領域的知識,筆者在今年會有一系列學習計劃,目前大綱已經定製好了,若是有興趣的朋友,能夠了解下:Awesome-Android-NDK

8、佈局優化的常見問題

一、你在作佈局優化的過程當中用到了哪些工具?

我在作佈局優化的過程當中,用到了不少的工具,可是每個工具都有它不一樣的使用場景,不一樣的場景應該使用不一樣的工具。下面我從線上和線下兩個角度來進行分析。

好比說,我要統計線上的FPS,我使用的就是Choreographer這個類,它具備如下特性:

  • 一、可以獲取總體的幀率。
  • 二、可以帶到線上使用。
  • 三、它獲取的幀率幾乎是實時的,可以知足咱們的需求。

同時,在線下,若是要去優化佈局加載帶來的時間消耗,那就須要檢測每個佈局的耗時,對此我使用的是AOP的方式,它沒有侵入性,同時也不須要別的開發同窗進行接入,就能夠方便地獲取每個佈局加載的耗時。若是還要更細粒度地去檢測每個控件的加載耗時,那麼就須要使用LayoutInflaterCompat.setFactory2這個方法去進行Hook。

此外,我還使用了LayoutInspector和Systrace這兩個工具,Systrace能夠很方便地看到每幀的具體耗時以及這一幀在佈局當中它真正作了什麼。而LayoutInspector能夠很方便地看到每個界面的佈局層級,幫助咱們對層級進行優化。

二、佈局爲何會致使卡頓,你又是如何優化的?

分析完佈局的加載流程以後,咱們發現有以下四點可能會致使佈局卡頓:

  • 一、首先,系統會將咱們的Xml文件經過IO的方式映射的方式加載到咱們的內存當中,而IO的過程可能會致使卡頓。
  • 二、其次,佈局加載的過程是一個反射的過程,而反射的過程也會可能會致使卡頓。
  • 三、同時,這個佈局的層級若是比較深,那麼進行佈局遍歷的過程就會比較耗時。
  • 四、最後,不合理的嵌套RelativeLayout佈局也會致使重繪的次數過多。

對此,咱們的優化方式有以下幾種:

  • 一、針對佈局加載Xml文件的優化,咱們使用了異步Inflate的方式,即AsyncLayoutInflater。它的核心原理是在子線程中對咱們的Layout進行加載,而加載完成以後會將View經過Handler發送到主線程來使用。因此不會阻塞咱們的主線程,加載的時間所有是在異步線程中進行消耗的。而這僅僅是一個從側面緩解的思路。
  • 二、後面,咱們發現了一個從根源解決上述痛點的方式,即便用X2C框架。它的一個核心原理就是在開發過程咱們仍是使用的XML進行編寫佈局,可是在編譯的時候它會使用APT的方式將XML佈局轉換爲Java的方式進行佈局,經過這樣的方式去寫佈局,它有如下優勢:一、它省去了使用IO的方式去加載XML佈局的耗時過程。二、它是採用Java代碼直接new的方式去建立控件對象,因此它也沒有反射帶來的性能損耗。這樣就從根本上解決了佈局加載過程當中帶來的問題。
  • 三、而後,咱們可使用ConstraintLayout去減小咱們界面佈局的嵌套層級,若是原始佈局層級越深,它能減小的層級就越多。而使用它也能避免嵌套RelativeLayout佈局致使的重繪次數過多。
  • 四、最後,咱們可使用AspectJ框架(即AOP)和LayoutInflaterCompat.setFactory2的方式分別去創建線下全局的佈局加載速度和控件加載速度的監控體系。

三、作完佈局優化有哪些成果產出?

  • 一、首先,咱們創建了一個體系化的監控手段,這裏的體系還指的是線上加線下的一個綜合方案,針對線下,咱們使用AOP或者ARTHook,能夠很方便地獲取到每個佈局的加載耗時以及每個控件的加載耗時。針對線上,咱們經過Choreographer.getInstance().postFrameCallback的方式收集到了FPS,這樣咱們能夠知道用戶在哪些界面出現了丟幀的狀況。
  • 二、而後,對於佈局監控方面,咱們設立了FPS、佈局加載時間、佈局層級等一系列指標。
  • 三、最後,在每個版本上線以前,咱們都會對咱們的核心路徑進行一次Review,確保咱們的FPS、佈局加載時間、佈局層級等達到一個合理的狀態。

9、總結

對於Android的佈局優化,筆者以一種自頂向下,層層遞進的方式和你們一塊兒深刻地去探索了Android中如何將佈局優化作到極致,其中主要涉及如下八大主題:

  • 一、繪製原理:CPUGPU、Android圖形系統的總體架構、繪製線程、刷新機制。
  • 二、屏幕適配:OLED 屏幕和 LCD 屏幕的區別、屏幕適配方案。
  • 三、優化工具:使用Systrace來進行佈局優化、利用Layout Inspector來查看視圖層級結構、採用Choreographer來獲取FPS以及自動化測量 UI 渲染性能的方式(gfxinfo、SurfaceFlinger等dumpsys命令)。
  • 四、佈局加載原理:佈局加載源碼分析、LayoutInflater.Factory分析。
  • 五、獲取界面佈局耗時:使用AOP的方式去獲取界面加載的耗時、利用LayoutInflaterCompat.setFactory2去監控每個控件加載的耗時。
  • 六、佈局優化常規方案:使用AOP的方式去獲取界面加載的耗時、利用LayoutInflaterCompat.setFactory2去監控每個控件加載的耗時。
  • 七、佈局優化的進階方案:使用異步佈局框架Litho、使用Flutter實現高性能的UI佈局、使用RenderThread實現動畫的異步渲染與 利用RenderScript實現高效的圖片處理。
  • 八、佈局優化的常見問題。

能夠看到,佈局優化看似是Android性能優化中最簡單的專項優化項,可是筆者卻花費了整整3、四萬字的篇幅才能比較完整地將其核心知識傳授給你們。所以,不要小看每個專項優化點,深刻進去,一定滿載而歸

參考連接:

一、國內Top團隊大牛帶你玩轉Android性能分析與優化 第五章 佈局優化

二、極客時間之Android開發高手課 UI優化

三、手機屏幕的前世此生 可能比你想的還精彩

四、OLED 和 LCD 什麼區別?

五、Android 目前穩定高效的UI適配方案

六、騷年你的屏幕適配方式該升級了!-smallestWidth 限定符適配方案

七、dimens_sw github

八、一種極低成本的Android屏幕適配方式

九、騷年你的屏幕適配方式該升級了!-今日頭條適配方案

十、今日頭條屏幕適配方案終極版正式發佈!

十一、使用Systrace分析UI性能

十二、GAPID-Graphics API Debugger

1三、Android性能優化之渲染篇

1四、Android 屏幕繪製機制及硬件加速

1五、Android 圖形處理官方教程

1六、Vulkan - 高性能渲染

1七、Android Vulkan Tutorial

1八、Test UI performance-gfxinfo

1九、使用dumpsys gfxinfo 測UI性能(適用於Android6.0之後)

20、TextureView API

2一、PrecomputedText API

2二、Litho Tutorial

2三、基本功 | Litho的使用及原理剖析

2四、Flutter官方文檔中文版

2五、[[Google Flutter 團隊出品] 深刻了解 Flutter 的高性能圖形渲染](https://www.bilibili.com/vide...

2六、Flutter渲染機制—UI線程

2七、RenderThread:異步渲染動畫

2八、RenderScript官方文檔

2九、RenderScript :簡單而快速的圖像處理

30、RenderScript渲染利器

讚揚

若是這個庫對您有很大幫助,您願意支持這個項目的進一步開發和這個項目的持續維護。你能夠掃描下面的二維碼,讓我喝一杯咖啡或啤酒。很是感謝您的捐贈。謝謝!

<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/15/16fa6da03e696446?w=1080&h=1457&f=jpeg&s=93345" width=20%><img src="https://user-gold-cdn.xitu.io/2020/1/15/16fa6dad17fbc777?w=990&h=1540&f=jpeg&s=110691" width=20%>
</div>


Contanct Me

● 微信:

歡迎關注個人微信: bcce5360

● 微信羣:

微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

<div align="center">
<img src="https://user-gold-cdn.xitu.io/2020/1/14/16fa269fa57fc738?w=848&h=1096&f=png&s=426288" width=35%>
</div>

● QQ羣:

2千人QQ羣, Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~

About me

很感謝您閱讀這篇文章,但願您能將它分享給您的朋友或技術羣,這對我意義重大。

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索