在上篇文章中,笔者带领你们学习了布局优化涉及到的绘制原理、优化工具、监测手段等等知识。若是对这块内容还不了解的建议先看看《深刻探索Android布局优化(上)》。本篇,为深刻探索Android布局优化的下篇。这篇文章包含的主要内容以下所示:html
下面,笔者将与你们一块儿进入进行布局优化的实操环节。前端
布局优化的方法有不少,大部分主流的方案笔者已经在Android性能优化之绘制优化里讲解过了。下面,我将介绍一些其它的优化方案。java
使用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);
在使用子线程建立视图控件的时候,咱们能够把子线程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; } } }
在第三小节中,咱们对Android的布局加载原理进行了深刻地分析,从中咱们得出了布局加载过程当中的两个耗时点:github
很明显,咱们没法从根本上去解决这两个问题,可是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是经过侧面缓解的方式去缓解布局加载过程当中的卡顿,可是它依然存在一些问题:
因为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); } }
由上分析可知,在布局加载的过程当中有两个主要的耗时点,即IO操做和反射,而AsyncLayoutInflater仅仅是缓解,那么有什么方案能从根本上去解决这个问题呢?
若是使用Java代码写布局,无疑从Xml文件进行IO操做的过程和反射获取View实例的过程都将被抹去。虽然这样从本质上解决了问题,可是也引入了一些新问题,如不便于开发,可维护性差等等。
那么,还有没有别的更好的方式呢?
答案就是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框架还存在一些问题:
对于第2个问题,咱们须要修改X2C框架的源码,当发现是TextView等控件时,须要直接使用new的方式去建立一个AppCompatTextView等兼容类型的控件。于此同时,它还有以下两个小的点不支持,可是这个问题不大:
首先,对于Android视图绘制的原理,咱们必需要有必定的了解,关于这块,你们能够参考下[Android View的绘制流程
](https://jsonchao.github.io/20...。
对于视图绘制的性能瓶颈,大概有如下三点:
那么,如何减小布局的层级及复杂度呢?
基本上只要遵循如下两点便可:
为了提高布局的绘制速度,Google推出了ConstraintLayout,它的特色以下:
接下来,咱们来简单使用一下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下的一系列属性,其实都很是简单,这里就很少作介绍了。
除此以外,还有如下方式能够减小布局层级和复杂度:
在视图的绘制优化中,还有一个比较重要的优化点,就是避免过渡绘制,这个笔者已经在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(); }
从以上代码可知,这里是直接进行绘制的,此时显示的布局过渡绘制背景以下所示:
能够看到,图片的背景都叠加起来了,这个时候,咱们须要在绘制的时候使用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优化事后的布局过渡绘制背景以下所示:
注意:
咱们还能够经过canvas.quickReject方法来判断是否没和某个矩形相交,以跳过非矩形区域的绘制操做。
固然,对视图的绘制优化还有其它的一些优化操做,好比:
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的布局优化还有更深刻的优化方式吗?
没错,下面,笔者就来和你们一块儿来探索布局优化的进阶方案。
Litho是Facebook开源的一款在Android上高效创建UI的声明式框架,它具备如下特色:
接下来,咱们在项目里面来使用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));
显示效果以下所示:
在上面的示例中,咱们仅仅是将Text这个子组件设置给了LithoView,后续为了实现更复杂的布局,咱们须要使用带多个子组件的根组件去替换它。
由上可知,在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();
运行项目,显示界面以下所示:
那上述过程是如何进行构建的呢?
它看起来就像有一个LithoSpec的类名,而且在项目构建以后生成了一个与LithoSpec有着一样包名的Litho类,以下所示:
相似于Litho这种类中的全部方法参数都会由Litho进行自动填充。此外,基于这些规格,将会有一些额外的方法由注解处理器自动生成,例如上述示例中Column或Row中的Text的TextSizeSp、backgroundColor等方法。(Row和Column分别对应着Flexox中的行和列,它们都实现了Litho中另外一种特殊的组件Layout)
MountSpec是用来生成可挂载类型组件的一种规范,它的做用是用来生成渲染具体的View或者Drawable的组件。同LayoutSpec相似,它必须使用@MountSpec注解来标注,并实现一个标注了@onCreateMountContent的方法。可是MountSpec的实现要比Layout更加地复杂,由于它拥有本身的生命周期,以下所示:
MountSpec的生命周期流转图以下所示:
在使用Litho完成了两个实例的开发以后,相信咱们已经对Litho的布局方式已经有了一个感性的认知。那么,Litho究竟是如何进行布局优化的呢?在布局优化中它所作的核心工做有哪些?
Litho在布局优化中所作的核心工做包括如下三点:
在前文中,咱们知道Android的布局加载过程一般会前后涉及到measure、layout、draw过程,而且它们都是在主线程执行的,若是方法执行过程当中耗时太多,则主界面必然会产生卡顿现象。
还记得咱们在前面介绍的PrecomputedText,它内部将measure与layout的过程放在了异步线程进行初始化,而Litho与PrecomputedText相似,也是将measre与layout的过程进行了异步化,核心原理就是利用CPU的闲置时间提早在异步线程中完成measure和layout的过程,仅在UI线程中完成绘制工做。
那么Android为何不本身实现异步布局呢?
主要有如下两缘由:
通过以前的学习,咱们了解到Litho采用了一套自有的布局引擎Yoga,它会在布局的过程当中去检测出没必要要的布局嵌套层级,并自动去减小多余的层级以实现布局的扁平化,这能够显著减小渲染时的递归调用,加快渲染速度。例如,在实现一个图片带多个文字的布局中,咱们一般会至少有两个布局层级,固然,你也可使用TextView的drawableStart方法 + 代码动态布局使用Spannable/Html.fromHtml(用来实现多种不一样规格的文字) + lineSpaceExtra/lineSpacingMultiplier(用来调整多行文本的显示间距)来将布局层级降为一层,可是这种实现方式比较繁琐,而经过使用Litho,咱们能够把下降布局嵌套层级的任务所有丢给布局引擎Yoga去处理。由前面可知,Litho是使用Flexbox来建立布局的,并最终生成带有层级结构的组件树。经过使用Yoga来进行布局计算,可使用Flexbox的相对布局变成了只有一层嵌套的绝对布局。相比于ConstraintLayout,对于实现复杂布局的时候可读性会更好一些,由于ConstraintLayout此时会有过多的约束条件,这会致使可读性变差。此外,Litho自身还提供了许多挂载Drawable的基本视图组件,相比Viwe组件使用它们能够显著减小内存占用(一般会减小80%的内存占用)。Litho实现布局自动扁平化的原理图以下所示:
使用了RecyclerView与ListView这么久,咱们明白它是以viewType为粒度来对一个组件集合统一进行缓存与回收的,而且,当viewType的类型越多,其对组件集合的缓存与回收的效果就会越差。相对于RecyclerView与ListView缓存与回收的粗粒度而言,Litho实现了更细粒度的回收机制,它是以Text、image、video等单个Component为粒度来做为其基准的,具体实现原理为在item回收前,会把LithoView中挂载的各个绘制单元进行解绑拆分出来,由Litho本身的缓存池去分类回收,而后在展现前由LithoView按照组件树的样式挂载组装各个绘制单元,这样就达到了细粒度复用的目的。毫无疑问,这不只提升了其缓存的命中率与内存的使用率,也下降了提升了其滚动刷新的频率。更细粒度复用优化内存的原图以下所示:
由上图能够看出,滑出屏幕的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();
最终的显示效果以下所示:
若是咱们须要显示不一样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(); } }
最终的显示效果以下所示:
除此以外,咱们还能够有更多的方式去定义@Prop,以下所示:
@Prop(optional = true, resType = ResType.DIMEN_OFFSET) int shadowRadius,
上面定义了一个可选的Prop,传入的shadowRadius是支持dimen规格的,如px、dp、sp等等。
使用Litho,在布局性能上有很大的提高,可是开发成本过高,由于须要本身去实现不少的组件,而且其组件须要在编译时才能生成,不可以进行实时预览,可是能够把Litho封装成Flexbox布局的底层渲染引擎,以此实现上层的动态化,具体实现原理可参见Litho在美团动态化方案MTFlexbox中的实践。
Flutter能够说是2019最火爆的框架之一了,它是 Google 开源的 UI 工具包,帮助开发者经过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。对于Android来讲,FLutter可以创做媲美原生的高性能应用,应用使用 Dart语言进行 开发。Flutter的架构相似于Android的层级架构,每一层都创建在前一层之上,其架构图以下所示:
在Framework层中,Flutter经过在 widgets 层组合基础 widgets 来构建 Material 层,而 widgets 层自己则是经过对来自 Rendering 层的低层次对象组合而来。而在Engine层,Flutter集成了Skia引擎用于进行栅格化,而且使用了Dart虚拟机。
接下来,咱们以Flutter、原生Android、其它跨平台框架如RN来作比较,它们的图形绘制调用层级图以下所示:
能够看到,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绘制原理,它主要是经过VSYNC信号来使UI线程和GPU线程有条不紊的周期性的去渲染界面,其绘制原理图以下所示:
绘制步骤大体以下:
此外,Flutter 也采用了相似 Litho 的props属性不可变、Reat单向数据流的方案,用于将视图与数据分离。对于Flutter这一大前端领域的核心技术,笔者也是充满兴趣,后续会有计划对此进行深刻研究,敬请期待。
在Android 5.0以后,Android引进了RenderThread,它可以实现动画的异步渲染。可是目前支持RenderThread彻底渲染的动画,只有两种,即ViewPropertyAnimator和CircularReveal(揭露动画)。对于CircularReveal使用比较简单且功能较为单一,就很少作过多的描述了。下面我简单说下ViewPropertyAnimator中如何去利用RenderThread。
/** * 使用反射的方式去建立对应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; } }
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); }
ViewPropertyAnimator animator = v.animate().scaleY(2).setDuration(2000); AnimHelper.onStartBeforeConfig(animator, v); animator.start();
当前,若是是作音视频或图像处理的工做,常常须要对图片进行高斯模糊、放大、锐化等操做,可是这里涉及大量的图片变换操做,例如缩放、裁剪、二值化以及降噪等。而图片的变换又涉及大量的计算任务,这个时候咱们能够经过RenderScript去充分利用手机的GPU计算能力,以实现高效的图片处理。
而RenderScript的工做流程须要经历以下三个步骤:
因为RenderScript主要是用于音视频、图像处理等细分领域,这里笔者就不继续深刻扩展了,对于NDK、音视频领域的知识,笔者在今年会有一系列学习计划,目前大纲已经定制好了,若是有兴趣的朋友,能够了解下:Awesome-Android-NDK。
我在作布局优化的过程当中,用到了不少的工具,可是每个工具都有它不一样的使用场景,不一样的场景应该使用不一样的工具。下面我从线上和线下两个角度来进行分析。
好比说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具备如下特性:
同时,在线下,若是要去优化布局加载带来的时间消耗,那就须要检测每个布局的耗时,对此我使用的是AOP的方式,它没有侵入性,同时也不须要别的开发同窗进行接入,就能够方便地获取每个布局加载的耗时。若是还要更细粒度地去检测每个控件的加载耗时,那么就须要使用LayoutInflaterCompat.setFactory2这个方法去进行Hook。
此外,我还使用了LayoutInspector和Systrace这两个工具,Systrace能够很方便地看到每帧的具体耗时以及这一帧在布局当中它真正作了什么。而LayoutInspector能够很方便地看到每个界面的布局层级,帮助咱们对层级进行优化。
分析完布局的加载流程以后,咱们发现有以下四点可能会致使布局卡顿:
对此,咱们的优化方式有以下几种:
对于Android的布局优化,笔者以一种自顶向下,层层递进的方式和你们一块儿深刻地去探索了Android中如何将布局优化作到极致,其中主要涉及如下八大主题:
能够看到,布局优化看似是Android性能优化中最简单的专项优化项,可是笔者却花费了整整3、四万字的篇幅才能比较完整地将其核心知识传授给你们。所以,不要小看每个专项优化点,深刻进去,一定满载而归。
一、国内Top团队大牛带你玩转Android性能分析与优化 第五章 布局优化
六、骚年你的屏幕适配方式该升级了!-smallestWidth 限定符适配方案
十二、GAPID-Graphics API Debugger
1八、Test UI performance-gfxinfo
1九、使用dumpsys gfxinfo 测UI性能(适用于Android6.0之后)
2五、[[Google Flutter 团队出品] 深刻了解 Flutter 的高性能图形渲染](https://www.bilibili.com/vide...
若是这个库对您有很大帮助,您愿意支持这个项目的进一步开发和这个项目的持续维护。你能够扫描下面的二维码,让我喝一杯咖啡或啤酒。很是感谢您的捐赠。谢谢!
<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>
欢迎关注个人微信:
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>
2千人QQ群, Awesome-Android学习交流群,QQ群号:959936182, 欢迎你们加入~