preface

In fact, Android rendering optimization can be divided into two parts, namely layout (UI) optimization and stuck optimization, and the core problem of layout optimization is to solve the problem of application stuck due to poor layout rendering performance, so it can be considered as a subset of stuck optimization. This paper mainly includes the following contents: 1. Why to carry out layout optimization and Android drawing, layout loading principle 2. Method of obtaining the loading time of layout file 3. Introduces some methods and methods of layout optimization 4. Why abandon these optimizations?

1. Why layout optimization?

Why layout optimization? The answer is obvious, if the layout is too deeply nested, or for some other reason, the layout rendering performance is poor, it can lead to application stuttering. So how exactly does the layout render poorly? First, we should understand the principles of Android drawing and layout loading

androidDraw the principle

The three most important concepts involved in Android’s screen refresh (briefly introduced here for ease of understanding)

  • CPU: performs the application layermeasure,layout,drawAnd so on. After drawing, submit the data toGPU
  • GPU: Further process the data and cache the data
  • Screen: Consists of pixels filled with data from the buffer at a fixed rate (16.6ms, or 60 frames per second)

To sum up:CPUAfter drawing, submit data,GPUThe data is further processed and cached, and finally the screen reads the data from the buffer and displays it

Double buffering mechanism

After looking at the above flow chart, it is easy to think of a problem. The screen refreshes at a fixed rate of 16.6ms, but the time when our application layer triggers drawing is completely random (for example, we can touch the screen at any time to trigger drawing). What happens if the screen is reading data into the buffer at the same time that the GPU is writing to it? It is possible that part of the screen is a previous frame, and part of the screen is another frame, which is obviously unacceptable, so how do you solve this problem?

So, in the screen refresh, The Android system introduced double buffering

GPUOnly toBack BufferIs written to draw data, andGPUWill be exchanged regularlyBack BufferandFrame Buffer, the exchange rate is also 60 times per second, which keeps pace with the screen refresh rate.



Although we introduced double buffering, we knew that when the layout was complex, or the device performance was poor,CPUIt is not guaranteed to complete the calculation of drawing data in 16.6ms, so here the system does another processing.

When your application is going toBack BufferThe system will fill in the dataBack BufferThe lock.

If theGPUSwapping twoBufferYour application is still movingBack BufferTo fill in the data,GPUWill findBack BufferIt’s locked. It’s going to give up the exchange.

The consequence of this is that the screen still displays the original image, which is often referred to as dropping frames

Layout loading principle

As can be seen from the above, the reason for frame drop is that CPU cannot complete the calculation of drawing data within 16.6ms. The reason layout loading can cause frames to drop is because it is a time-consuming operation on the main thread, which may cause the CPU to fail to complete the data calculation on time

Layout loading is mainly throughsetContentViewTo implement, we will not post source code here, let’s take a look at its sequence diagram



We can see that in thesetContentViewThere are two main time-consuming operations in

  • 1. The parsingxmlTo obtainXmlResourceParserThis is the IO process
  • 2. BycreateViewFromTagTo createViewObject, using reflection

These two points are the reasons why layout loading can stall and are also performance bottlenecks for layouts

2. Obtain the method of loading the layout file

If we need to optimize the layout problem, the first and most important thing is: to determine quantitative standards so we first introduce several methods to obtain the loading time of layout files

Regular access to

First, a general approach

val start = System.currentTimeMillis()
setContentView(R.layout.activity_layout_optimize)
val inflateTime = System.currentTimeMillis() - start
Copy the code

This is easy, because setContentView is a synchronous method, and if you want to calculate the elapsed time, you just subtract the elapsed time from the elapsed time

AOP(Aspectj.ASM)

The above method is simple, but it is not elegant, and the code is intrusive. If you want to measure all activities, you need to duplicate the related methods in the base class, which is more troublesome

    @Around("execution(* android.app.Activity.setContentView(..) )"
    public void getSetContentViewTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.toShortString();
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i("aop inflate",name + " cost " + (System.currentTimeMillis() - time));
    }
Copy the code

Aspectj, it’s a little bit simpler, but the annotation above means that inside the execution of the setContentView method we’re going to call the getSetContentViewTime method that we’ve written so that we can get the time that it takes and we can look at the printed log

I/aop inflate: AppCompatActivity.setContentView(..) cost 69
I/aop inflate: AppCompatActivity.setContentView(..) cost 25
Copy the code

This enables non-intrusive monitoring of the loading time of each page layout

Gets the elapsed time of any control

Sometimes, in order to know exactly which control takes time to load, for example, if we add a custom View and want to monitor its performance, we can use setFactory2 to listen for the load time of each control. Let’s first review the setContentView method

    public final View tryCreateView(@Nullable View parent, @NonNull String name,
      	...
        View view;
        if(mFactory2 ! =null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if(mFactory ! =null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null; }...return view;
    }
Copy the code

Before the actual reflection instantiates the XML node, the onCreateView method of mFactory2 is called so that if we override the onCreateView method and add time statistics before and after it, we can get the loading time of each control

    private fun initItemInflateListener(a){
        LayoutInflaterCompat.setFactory2(layoutInflater, object : Factory2 {
            override fun onCreateView(
                parent: View? , name:String,
                context: Context,
                attrs: AttributeSet
            ): View? {
                val time = System.currentTimeMillis()
                val view = delegate.createView(parent, name, context, attrs)
                Log.i("inflate Item",name + " cost " + (System.currentTimeMillis() - time))
                return view
            }

            override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
                return null}})}Copy the code

As shown above: The actual method to create a View is still to call delegate.createView, but we’ve just done a bit before and after it. Note that initItemInflateListener needs to be called before onCreate so that it’s easy to monitor the loading time of each control

3. Some methods of layout loading optimization are introduced

There are two main reasons for slow layout loading, one is IO and the other is reflection, so our optimization ideas generally have two 1’s. 2. Root solution (no IO required, reflection processes such as X2C,Anko,Compose, etc.)

AsyncLayoutInflaterplan

AsyncLayoutInflater is used to help asynchronously load layouts, Inflate (int, ViewGroup, OnInflateFinishedListener) method has finished running OnInflateFinishedListener will return to the View on the main thread callback; This is done for lazy loading of the UI or for high responsiveness to user actions.

In short, we know that by default the setContentView function is executed in the UI thread with a series of time-consuming actions: Xml parsing, View reflection creation and other processes are also performed in the UI thread. AsyncLayoutInflater helps us to perform these processes asynchronously to keep the UI thread responsive.

Use as follows:

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        new AsyncLayoutInflater(AsyncLayoutActivity.this)
                .inflate(R.layout.async_layout, null.new AsyncLayoutInflater.OnInflateFinishedListener() {
                    @Override
                    public void onInflateFinished(View view, int resid, ViewGroup parent) { setContentView(view); }});// Other operations
    }
Copy the code

This has the advantage of migrating the UI loading process to child threads, ensuring that the UI thread is highly responsive and sacrificing ease of use, as well as the possibility of crashing if the UI is called during initialization

X2Cplan

X2C is a set of layout loading framework of Iread open source. Its main idea is to translate the layout that needs to be translated and generate corresponding Java files during compilation, so that for developers to write the layout or write the original XML, but for the program, the runtime load is the corresponding Java files. This shifts the runtime overhead to compile time as shown in the raw XML file:


      
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:paddingLeft="10dp">

  <include
      android:id="@+id/head"
      layout="@layout/head"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerHorizontal="true" />

  <ImageView
      android:id="@+id/ccc"
      style="@style/bb"
      android:layout_below="@id/head" />
</RelativeLayout>
Copy the code

Java files generated by X2C

public class X2C_2131296281_Activity_Main implements IViewCreator {
  @Override
  public View createView(Context ctx, int layoutId) {
    	Resources res = ctx.getResources();

        RelativeLayout relativeLayout0 = new RelativeLayout(ctx);
        relativeLayout0.setPadding((int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,10,res.getDisplayMetrics())),0.0.0);

        View view1 =(View) new X2C_2131296283_Head().createView(ctx,0);
        RelativeLayout.LayoutParams layoutParam1 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        view1.setLayoutParams(layoutParam1);
        relativeLayout0.addView(view1);
        view1.setId(R.id.head);
        layoutParam1.addRule(RelativeLayout.CENTER_HORIZONTAL,RelativeLayout.TRUE);

        ImageView imageView2 = new ImageView(ctx);
        RelativeLayout.LayoutParams layoutParam2 = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,(int)(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,1,res.getDisplayMetrics())));
        imageView2.setLayoutParams(layoutParam2);
        relativeLayout0.addView(imageView2);
        imageView2.setId(R.id.ccc);
        layoutParam2.addRule(RelativeLayout.BELOW,R.id.head);

        returnrelativeLayout0; }}Copy the code

Use X2C. SetContentView instead of the original setContentView as shown below

// this.setContentView(R.layout.activity_main);
X2C.setContentView(this, R.layout.activity_main);
Copy the code

X2C benefits 1. It solves the performance problems 2 while preserving XML. According to X2C statistics, the loading time can be reduced to 1/3 of the original

X2C problem 1. Some attributes cannot be set by code,Java is not compatible 2. 3. The kotlin-Android-extensions plug-in is not supported, sacrificing some ease-of-use

Ankoplan

Anko is a powerful library developed by JetBrains that supports writing UI using kotlin DSL, as shown below

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle? , persistentState:PersistableBundle?). {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)}}class MyActivityUI : AnkoComponent<MyActivity> {
    override fun createView(ui: AnkoContext<MyActivity>) = with(ui) {
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello, ${name.text}!")}}}}}Copy the code

As shown above,Ankousekotlin DSLImplement the layout, it is more than we useJavaCreating a layout dynamically is a lot easier, mainly more concise, it and owningxmlCreating a hierarchy of the layout makes it easier to read

At the same time, it removesIOWith the reflection process, the performance is better as followsAnkowithXMLPerformance comparison of

The above data comes from: Android.jlelse. Eu / 400-ftL-…

AnKo recommends that you use Jetpack Compose instead, because AnKo has stopped maintenance

Composeplan

Compose is a new addition to Jetpack, a new UI library announced by the Android team at I/O 2019. Currently in Beta, Compose uses pure Kotlin for simplicity and ease of use, But instead of wrapping up viewgroups like Anko did, Compose doesn’t wrap up the View and ViewGroup systems to make writing easier. Instead, Compose does away with the system altogether and rewrites the entire rendering mechanism from the inside out.

To be sure,Compose is the official alternative to XML

Compose’s main advantage is that it is simple and easy to use. Its declarative UI 2. Removes XML and uses only Kotlin

Because this article is not about Compose, I will not cover Compose. In general, Compose is the direction of future Android UI development, and you can check it out for yourself

4. Why give up using these optimization methods?

There are a lot of layout loading optimization methods introduced above, but I didn’t use them in the end in the project. This is because there are several reasons from the beginning to the end. 1. Some methods (such as AsyncLayoutInflater,X2C) sacrifice ease of use. Although the performance is improved, the development becomes more difficult. 2. Promoting difficult 3.Compose in the team is the direction of android UI development in the future, but it is still in the Beta stage, and I believe it will become an effective means for us to replace XML after Release 4. Most importantly, for our project, the layout loading time was not the main time consuming area, so the optimization benefit was small and the effort could be put into other areas

As shown below, we subtract the time before and after setConteView to get the loading time of the layout, while onWindowFocusChanged is the time when the Activity is really visible. Subtract it from the time of onCreate to get the page display time. The test effect in our project is as follows:

Android 5.0 I/Log: inflateTime:33 I/Log: activityShowTime:252 I/Log: inflateTime:11 I/Log: activityShowTime:642 I/Log: InflateTime :83 I/Log: activityShowTime:637 Android 10.0 I/Log: inflateTime:11 I/Log: activityShowTime:88 I/Log: inflateTime:5 I/Log: activityShowTime:217 I/Log: inflateTime:27 I/Log: activityShowTime:221Copy the code

I have done tests on android5.0 and 10.0 mobile phones respectively. In our project, the loading time of layout is not very long, and they also take a small proportion in the visible process of the whole page, so we come to the conclusion: For our project, the layout loading time is not the main time, and the optimization benefit is not big, which is the reason for the change from entry to give up

Some conventional optimization techniques

In fact, we also have some common methods to optimize layout loading in actual development, such as optimizing layout levels and avoiding excessive drawing. These simple methods may be applied to the project

Optimize layout hierarchy and complexity

ConstraintLayout is a completely flat layout with fewer layers.RelativeLayout itself should not be nested. In a nested LinearLayout, try not to use weight, because weight will re-measure 4 twice. The merge tag is recommended to reduce one level. 5. Use the ViewStub lazy loading

Avoid overdrawing

1. Remove redundant background colors and reduce the use of complex shapes. 2. Custom views are drawn using clipRect shielding for masked views

conclusion

This article mainly introduces the following contents: 1. Andrid drawing principle and layout loading principle 2. How to acquire quantitative load android layout takes 3. Introduces some layout load optimization method and means (AsyncLayoutInflater, X2C, Anko, Compose, etc.) 4. It introduced that we did not introduce the above optimization methods because the layout loading time optimization benefit was not large in the project

Show Me The Code

Source code for this article

The resources

Step back from Android layout optimization