Why layout optimization

If the layout is nested too deeply, or for some other reason the layout rendering performance is poor, it can lead to application stuttering

Principles of Android Drawing

Three of the most important concepts are involved in Android’s screen refresh.

1. CPU: Perform operations such as measure, layout and draw of the application layer, and submit data to GPU after drawing.

2. GPU: Further process the data and cache it.

3. Screen: consists of pixels filled with data from the buffer at a fixed frequency (16.6ms, 60 frames per second).

The summary is: the CPU draws and submits the data, the GPU further processes and caches the data, 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 that the screen refreshes at a fixed rate of 16.6ms, but the timing of our application layer triggering the drawing is completely random (for example, we can touch the screen at any time to trigger the 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.

The GPU only writes drawing data to the Back Buffer, and the GPU regularly exchanges the Back Buffer and Frame Buffer at a frequency of 60 times/second, which keeps the synchronization with the screen refresh rate.

Although we have introduced double buffering mechanism, we know that when the layout is complex or the device performance is poor, the CPU cannot guarantee to complete the calculation of the drawing data in 16.6ms, so the system does another processing here.

The Back Buffer is locked when your application is filling data into it.

If you get to the point where the GPU swaps two buffers and your application is still filling in the Back Buffer, the GPU will notice that the Back Buffer is locked and it will abandon the swap.

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

Layout loading analysis

Start with the setContentView method, which calls the getDeleate().setContentView(resid) method, Layoutinflater.from (this.mContext).inflate(resId, contentParent) is then called to fill the layout. This is followed by a call to getLayout, in which the XML layout file is loaded and parsed using loadXmlResourceParser, and a call to createViewFromTag, which creates a view based on the tag. The view is created by Factory or Factory2. If Factory2 is not null, the view is created by Factory. If Factory2 is not null, the view is created by Factory. MPrivateFactory is a hidden API that only the Framework can call. MPrivateFactory is a hidden API that only the framework can call. MPrivateFactory is a hidden API that only the framework can call. The view is then created by subsequent logic either through onCreateView or createView through reflection. The specific flow chart is as follows:

Specific source logic analysis clear reference: juejin.cn/post/687044…

From this analysis, there are two optimizable points for layout loading

  • IO operation optimization
  • Reflection optimization

Obtaining the interface layout time

Do optimization, first to know where to optimize, so to get the interface layout time

Manual buried point

Do this manually before and after the setContentView execution, but this approach has the following disadvantages

  • Not enough grace
  • Code is intrusive

AOP

A brief word on the use of AOP

First, in order to use AOP on Android, AspectJ needs to be introduced. In the project root directory, build.gradle, add:

The classpath 'com. Hujiang. Aspectjx: gradle - android plugin - aspectjx: 2.0.0'Copy the code

Then, under build.gradle in your app directory, add:

Apply the plugin: 'android - aspectjx' implement org. Aspectj: aspectjrt: 1.8 + 'Copy the code

We’re going to use AOP to capture the time it takes to lay out an interface, so our starting point is the setContentView method, which declares an @aspect-annotated PerformanceAop class. Then, we can implement the setContentView method in the PerformanceAop class as follows:

@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(); } LogHelper.i(name + " cost " + (System.currentTimeMillis() - time)); }Copy the code

To obtain the method’s time, we must use the @around annotation, so that the first parameter ProceedingJoinPoint provides the proceed method to execute our setContentView method, The elapsed time of the setContentView method can be obtained before and after this method. Execution indicates that inside the execution of the setContentView method we call the getSetContentViewTime method that we’ve written. The * in parentheses is the wildcard that matches any Activity’s setContentView method. The number and type of method parameters are not limited.

LayoutInflaterCompat.setFactory2

Both methods capture the time after the entire layout has been loaded. What if you want to capture the loading time of individual controls? Here to introduce LayoutInflaterCompat. SetFactory2 way (after you see with Compat fields are compatible API), its use must be super. Call before onCreate.

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) { long start = System.currentTimeMillis(); View view = getDelegate().createView(parent, name, context, attrs); long cost = System.currentTimeMillis() - start; Log.d("onCreateView", "==" + name + "==cost==" + cost); return view; } @Override public View onCreateView(String name, Context context, AttributeSet attrs) { return null; }}); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}Copy the code

LayoutInflaterCompat. SetFactory2 API is not only a statistical View can create time, in fact, we can also be used to replace the system control operations, such as one day made a demand to our product manager will apply TextView unified into some kind of style, We can do it this way. Such as:

LayoutInflaterCompat.setFactory2(getLayoutInflater(), new LayoutInflater.Factory2() { @Override public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {if(textutils.equals ("TextView",name)){// Replace our own TextView} return null; } @override public View onCreateView(String name, Context Context, AttributeSet attrs) {return null; }});Copy the code

We can copy the code as long as we define this method in the onCreate of the base Activity class.

Specific source logic clear reference: juejin.cn/post/687044…

Layout loading optimization

AsyncLayoutInflater

Based on the two performance problems of layout loading, Google provides us with a class AsyncLayoutInflater, which can solve the problem of layout loading time from the side. Its characteristics are as follows

  • 1. The worker thread loads the layout.
  • 2. Call back the main thread.
  • 3, save the main thread time.

We need to configure it in Gradle, for example:

Implementation 'com. Android. Support: asynclayoutinflater: 28.0.0-1'Copy the code

Use:

public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { 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); }}); super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); }}Copy the code

Specific source code analysis logic please refer to: juejin.cn/post/684490…

X2C

X2C project address

The X2C framework retains the benefits of XML and addresses the performance issues of IO operations and reflection. Developers only need to write XML code normally, and X2C uses APT to translate XML code into Java code at compile time. This translates runtime time into compile time

Configuration:

AnnotationProcessor 'com. Zhangyue. We: x2c - apt: 1.1.2' implementation 'com. Zhangyue. We: x2c - lib: 1.0.6'Copy the code

Use:

@Xml(layouts = "activity_main") public class MainActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // setContentView(R.layout.activity_main); }}Copy the code

However, there are some problems with the X2C framework:

  • Some Java properties are not supported.
  • Lost system compatibility (AppCompat)

For the second problem, we need to modify the source code of X2C framework, when it is found to be TextView and other controls, we need to directly use the new way to create a compatible type of control such as AppCompatTextView. At the same time, it does not support the following two small points, but this is not a problem:

  • The merge tag, which cannot determine the parent of XML at compile time, is not supported.
  • System style: Only the application style list can be queried during compilation. System style cannot be queried. Therefore, only in-app style is supported.

The other way

  • Anko: Maintenance has been stopped
  • Jetpact Compose: Google’s new responsive layout, little information yet

General layout optimization

Reduce the level

Use RelativeLayout and LinearLayout wisely. Use Merge wisely.

RelativeLayout also suffers from poor performance because it measures its sub-view twice. But if you have a weight property in the LinearLayout, you’ll also take two measurements, but because you don’t have any more dependencies, you’ll still be more efficient than a RelativeLayout. Note that since Android is highly fragmented, using RelativeLayout makes the layout you build more adaptable.

Merge tag Parent (Merge tag Parent) Merge tag Parent (Merge tag Parent) Pay attention to

  • Merge can only be used on the root element of the layout XML file.
  • When you use merge to load a layout, you must specify a ViewGroup as its parent and set the loaded attachToRoot parameter to true.
  • The Merge tag cannot be used in the ViewStub. The reason is that no attachToRoot is set at all in the Inflate method of the ViewStub.

Increase display speed

A ViewStub is a lightweight View that is invisible, does not take up layout space, and consumes very little resources. You can specify a layout for the ViewStub, and only the ViewStub is initialized when the layout is loaded, and then the layout pointed to by the ViewStub is loaded and instantiated when the ViewStub is made visible or when viewstub.inflate () is called. The layout properties of the ViewStub are then passed to the layout to which it points.

Note:

  • ViewStub can only be loaded once, after which the ViewStub object is left empty. So it is not suitable for situations where you need to show hide on demand.
  • The ViewStub can only be used to load a layout file, not a specific View.
  • Merge labels cannot be nested in the ViewStub.

Layout of reuse

Android layout reuse can be achieved through the include tag.

summary

Finally, here are some tips I use to do layout optimization on a regular basis:

  • Use tags to load some unusual layouts.
  • Use wrAP_content as little as possible. Wrap_content will increase the calculation cost of layout measure. If the width and height are known to be fixed, wrAP_content is not used.
  • Replace RL and LL with TextView.
  • Optimize with low end machines to find performance bottlenecks.
  • Using TextView replaced multiline text line spacing: lineSpacingExtra/lineSpacingMultiplier.
  • Use Spannable/ html.fromhtml to replace text of various sizes.
  • Use the separators of the LinearLayout as much as possible.
  • Use Space to add spacing.
  • Use lint + Alibaba protocol to fix problems.
  • If there are too many levels of nesting, consider using a constrained layout.

Layout optimization analysis tool

Systrace

Focus on Frames

First of all, select our current application process in the left column. Under the application process column, there is a column called Frames. We can see three different small circles, green, yellow and red, as shown in the picture below:

Each small circle in the figure represents the state of the current frame, and the corresponding relationship is roughly as follows:

  • Normal: green.
  • Frame loss: yellow.
  • Serious frame loss: red.

In addition, if we select a frame, we can also see the corresponding Alerts alarm information in the details box at the bottom of the view to help us troubleshoot problems. In addition, for Android devices greater than or equal to 5.0 (i.e. API Level21), the work of creating frames is split between the UI thread and the Render thread. In versions of Android prior to 5.0, all the work of creating frames was done on the UI thread. Next, let’s look at the detail diagram corresponding to this frame, as shown below:

Corresponding to this frame, we see that there are two possible drawing problems: Excessive times of measure and layout are caused by excessive Bitmap and nesting levels of layout, which requires us to find the corresponding Bitmap of this frame in the project for optimization, and select a more efficient layout method for the problem of too many nesting levels of layout, which will be introduced in detail later.

Pay attention to Alerts bar

In addition, the Display interface of Systrace also provides an Alert box in the right sidebar to display all possible drawing performance problems detected by Systrace and the corresponding number, as shown in the following figure:

In this case, we can think of the Alert box as a list of bugs that need to be fixed, and usually improvements in one area can eliminate this type of Alert in all classes in the application, so don’t worry about the number of alerts here.

Layout Inspector

Layout Inspector is a tool that comes with AndroidStudio. Its main purpose is to view the view hierarchy.

The specific operation path is as follows:

Click on the Tools toolbar -> Layout Inspector in the third column -> select the current processCopy the code

Choreographer

Choreographer is designed to capture FPS and can be used online in real time, but only after Api 16. The call code is as follows:

Choreographer.getInstance().postFrameCallback();
Copy the code

The complete code for getting an FPS using Choreographer is as follows:

private long mStartFrameTime = 0; private int mFrameCount = 0; /** * private static Final Long MONITOR_INTERVAL = 160L; private static final long MONITOR_INTERVAL_NANOS = MONITOR_INTERVAL * 1000L * 1000L; Private static final Long MAX_INTERVAL = 1000L; /** * private static final Long MAX_INTERVAL = 1000L; @TargetApi(Build.VERSION_CODES.JELLY_BEAN) private void getFPS() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) { return; } Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() { @Override public void doFrame(long frameTimeNanos) { if (mStartFrameTime == 0) { mStartFrameTime = frameTimeNanos; } long interval = frameTimeNanos - mStartFrameTime; if (interval > MONITOR_INTERVAL_NANOS) { double fps = (((double) (mFrameCount * 1000L * 1000L)) / interval) * MAX_INTERVAL; // log output FPS logutils. I (" current real-time FPS value is: "+ FPS); mFrameCount = 0; mStartFrameTime = 0; } else { ++mFrameCount; } Choreographer.getInstance().postFrameCallback(this); }}); }Copy the code

In this way, we can achieve real-time access to the FPS of the application’s interface. But we need to rule out cases where there is no action on the page, that is, we only do statistics when the interface is drawn. We can use addOnDrawListener to listen for the drawing behavior of the interface. The code is as follows:

getWindow().getDecorView().getViewTreeObserver().addOnDrawListener
Copy the code

When a frame is lost, we can obtain the current page information, View information and operation path of the application and report it to the APM background to reduce the difficulty of secondary investigation. In addition, we defined continuous frame loss of more than 700 milliseconds as frozen frames, i.e., continuous frame loss of more than 42 frames. At this point, the user will feel more obvious lag phenomenon, therefore, we can calculate the more valuable frozen frame rate. Frame freeze rate is the ratio of the occurrence of frame freeze in all time. We can greatly improve the smoothness of the application by addressing the areas where frozen frames occur.