深刻探索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的绘制流程 这篇文章。

对于视图绘制的性能瓶颈,大概有如下三点:

  • 一、测量、布局、绘制每一个阶段的耗时。
  • 二、自顶而下的遍历,当嵌套层级过多时,遍历耗时会比较明显。
  • 三、无效的嵌套布局或不合理使用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 future = PrecomputedTextCompat.getTextFuture( “text”, textView.getTextMetricsParamsCompat(), null); textView.setTextFuture(future);

到这里,笔者就将常规的布局优化讲解完了,是否是顿时感受实力大增呢?

若是你此时心里已经YY到这种程度,那我只能说:

对于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中如何将布局优化作到极致,其中主要涉及如下八大主题:

  • 一、绘制原理:CPU\GPU、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 的高性能图形渲染

2六、Flutter渲染机制—UI线程

2七、RenderThread:异步渲染动画

2八、RenderScript官方文档

2九、RenderScript :简单而快速的图像处理

30、RenderScript渲染利器

赞扬

若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!


Contanct Me

● 微信:

欢迎关注个人微信:bcce5360

● 微信群:

微信群若是不能扫码加入,麻烦你们想进微信群的朋友们,加我微信拉你进群。

● QQ群:

2千人QQ群,Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~

About me

很感谢您阅读这篇文章,但愿您能将它分享给您的朋友或技术群,这对我意义重大。

但愿咱们能成为朋友,在 Github掘金上一块儿分享知识。