Android screen refresh mechanism and optimization scheme~

Author: A Jianjun

screen refresh mechanism

basic concept

  • Refresh rate : The number of screen refreshes per second, the unit is Hz, such as 60Hz, the refresh rate depends on the fixed parameters of the hardware.
  • Frame rate : the number of frames that the GPU draws in one second, in fps. Android uses 60fps, that is, the GPU can draw up to 60 frames per second, and the frame rate changes dynamically. For example, when the picture is still, the GPU does not draw, and the frame rate is 0, and the data in the buffer is still refreshed on the screen. , which is the last frame data operated by the GPU.

The display does not display the picture on the screen at one time, but scans from left to right, top to bottom, and sequentially displays the pixels of the entire screen, but this process is so fast that the human eye cannot detect the change. Taking a screen with a refresh rate of 60 Hz as an example, the time consumption of this process is: 1000 / 60 ≈ 16.6ms.

The mechanism of screen refresh is roughly: CPU performs application layer measurement, layout and drawing operations, and submits the data to GPU after completion, GPU further processes the data and caches the data. The frequency (16.6ms) fetches data from the buffer to fill the pixels.

screen tearing

If the data in one screen comes from two different frames, the picture will appear tearing. The screen refresh rate is fixed, for example, every 16.6ms to fetch data from the buffer and display a frame. Ideally, the frame rate and refresh rate should be consistent, that is, every time a frame is drawn, the display will display a frame. However, the writing data of CPU and GPU is uncontrollable, so some data in the buffer may be rewritten without being displayed at all, that is, the data in the buffer may come from different frames. When the screen is refreshed, it is not displayed at this time. The state of the buffer is known, so the frame captured from the buffer is not a complete frame, that is, the screen is torn.

So how to solve this problem? The Android system uses double buffering + VSync

Double buffering : Let the drawing and the display have their own buffers. The GPU writes the completed frame of image data to the BackBuffer, while the display uses the FrameBuffer. When the screen is refreshed, the FrameBuffer will not change. When the BackBuffer is ready, They are exchanged. When does the exchange take place? That depends on VSync.

VSync : When the device screen is refreshed and before the next frame is refreshed, because there is no screen refresh, this period is the best time for cache exchange. At this time, the hardware screen will send out a pulse signal to inform the GPU and CPU that it can be exchanged. This is the Vsync signal.

drop frame

Sometimes, when the layout is complex or the performance of the device is poor, the CPU cannot guarantee that the drawing will be completed within 16.6ms. Here, the system does another processing. When the BackBuffer is being filled with data, the system will lock the BackBuffer. If it is time for the GPU to exchange two Buffers, and your application is still filling data into the BackBuffer, it will find that the BackBuffer is locked, and it will give up the exchange.
The consequence of this is that the mobile phone screen still displays the original image, which is the so-called frame drop.

Optimization direction

If you want the screen to run smoothly, you must ensure that all UI measurements, layout and drawing time are within 16.6ms, because the cooperation between the human eye and the brain cannot perceive screen updates exceeding 60fps, that is, 1000 / 60Hz = 16.6ms , that is to say, the user will perceive a lag after 16.6ms.

Layer optimization

The fewer layers, the faster the View can be drawn. There are two commonly used solutions.

  • Reasonable use of RelativeLayout and LinearLayout : LinearLayout is preferred for the same hierarchy, because RelativeLayout needs to consider the relative position relationship between views, which requires more calculations and higher system overhead, but using LinearLayout sometimes increases the nesting level. At this time You should use RelativeLayout.

  • Use the merge tag : it will directly add its child elements to the merge tag Parent, so that no extra layers are introduced. It can only be used in the root element of the layout file, and the merge tag cannot be used in the ViewStub. When the inflate layout itself uses merge as the root node, it needs to be placed in the ViewGroup and set attachToRoot to true.

A layout can be reused. When using include to introduce a layout, you can consider merge as the root node. The layout in the merge root node depends on the parent layout of the include layout. When writing XML, you can first use the parent layout as the root node, and then replace it with merge after completion, so that we can preview the effect.

merge_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="World" />

</merge>

The parent layout is as follows:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    tools:context=".MainActivity">

    <include layout="@layout/merge_layout" />

</LinearLayout>

If you need to import the merge_layout layout file through inflate, you can import it like this:

class MyLinearLayout(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
    
    

    init {
    
    
        LayoutInflater.from(context).inflate(R.layout.merge_layout, this, true)
    }
}

The first parameter is the merge layout file id, the second parameter is the ViewGroup to add the subview to, and the third parameter is whether to add the loaded view to the ViewGroup.

It should be noted that the layout of the merge tag cannot set padding, such as this:

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="30dp">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="World" />

</merge>

The above padding will not take effect. If padding needs to be set, it can be set in its parent layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:padding="30dp"
    tools:context=".MainActivity">

    <include layout="@layout/merge_layout" />

</LinearLayout>

ViewStub

ViewStub is a lightweight View, a view object that is invisible, does not occupy the layout position, and occupies very small resources. You can specify a layout for ViewStub. When the layout is loaded, only the ViewStub will be initialized. When the ViewStub is set to visible or inflate, the layout pointed to by the ViewStub will be loaded and instantiated. You can use ViewStub to set whether to display a certain layout.

ViewStub can only be used to load one layout file, and it can only be loaded once, after which the ViewStub object will be set to empty. It is suitable for a scenario where a certain layout will not change after loading and you want to control the display and hiding of a layout file. A typical scenario is that when the data returned by our network request is empty, we often display a default interface, indicating that there is no data.

view_stub_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical">

    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="no data" />

</LinearLayout>

Introduced via ViewStub

<?xml version="1.0" encoding="utf-8"?>
<layout 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">

    <data>

        <variable
            name="click"
            type="com.example.testapp.MainActivity.ClickEvent" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{click::showView}"
            android:text="show" />

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{click::hideView}"
            android:text="hide" />

        <ViewStub
            android:id="@+id/default_page"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout="@layout/view_stub_layout" />

    </LinearLayout>
</layout>

Then inflate in the code, where the button is clicked to control its display and hiding.

class MainActivity : AppCompatActivity() {
    
    

    private var viewStub: ViewStub? = null

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        val binding =
            DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
        binding.click = ClickEvent()
        viewStub = binding.defaultPage.viewStub
        if (!binding.defaultPage.isInflated) {
    
    
            viewStub?.inflate()
        }
    }

    inner class ClickEvent {
    
    
        // 后面 ViewStub 已经回收了,所以只能用 GONE 和 VISIBLE
        fun showView(view: View) {
    
    
            viewStub?.visibility = View.VISIBLE
        }

        fun hideView(view: View) {
    
    
            viewStub?.visibility = View.GONE
        }
    }
}

overdraw

Overdrawing means that a certain pixel on the screen is drawn multiple times within the same frame time. In a multi-level overlapping UI structure, if the invisible UI is also doing drawing operations, some pixel areas will be overdrawn. Draws multiple times, wasting CPU and GPU resources.

We can open the developer options of the mobile phone, turn on the switch to debug GPU overdrawing, and then we can view the overdrawing situation through different color areas. What we have to do is minimize red and see more blue.

  • Colorless: No overdraw, every pixel is drawn once.
  • Blue: Each pixel is drawn once more, and blue is still acceptable.
  • Green: Each pixel is drawn twice more.
  • Crimson: Each pixel is drawn 4 times or more, which affects performance and needs to be optimized. Crimson areas should be avoided.

Optimization method :

  • Reduce unnecessary background: For example, Activity often has a default background, which is held by DecorView. When the custom layout has a full-screen background, the background of DecorView is useless to us, but it will generate Once Overdraw, you can kill it.
window.setBackgroundDrawable(null)
  • Optimization of custom View: When customizing View, a certain area may be drawn multiple times, resulting in overdrawing. The drawing area can be specified through the canvas.clipRect method, which can save CPU and GPU resources, and drawing instructions outside the clipRect area will not be executed.

AsyncLayoutInflater

The setContentView function is executed on the UI thread, and there are a series of time-consuming actions: XML parsing, View reflection creation and other processes are all executed on the UI thread, and AsyncLayoutInflater executes these processes asynchronously to keep the UI thread High response.

implementation 'androidx.asynclayoutinflater:asynclayoutinflater:1.0.0'

class TestActivity : AppCompatActivity() {
    
    

    override fun onCreate(savedInstanceState: Bundle?) {
    
    
        super.onCreate(savedInstanceState)
        AsyncLayoutInflater(this).inflate(R.layout.activity_test, null) {
    
     view, _, _ ->
            setContentView(view)
        }
    }
}

In this way, the loading process of the UI is migrated to the child thread, which ensures the high response of the UI thread. Special attention should be paid when using it. When calling the UI, you must wait for its initialization to complete, otherwise it may cause a crash.

Compose

Compared with the traditional XML layout method, Jetpack Compose has stronger composability, higher efficiency and better development experience. I believe it will become the mainstream method of Android UI development in the future.

The traditional XML layout method is written based on declarative XML code, using a large number of XML tags to describe the UI structure, XML files are parsed and constructed to generate View objects, and they are added to the View tree. In Compose, UI code is organized into composable functions, and each function is responsible for building a specific UI element. The rendering of UI elements is directly managed by the Compose runtime, and the Composable function will be called to calculate and generate The final view in the current UI state.


In fact, in addition to layout optimization and UI optimization, Android performance optimization also includes memory optimization, network optimization, freeze optimization, storage optimization, etc. In order to let everyone understand everything at once, it is integrated into "Android Performance Optimization" Core Knowledge Points Manual" , you can refer to the following:

"APP Performance Tuning Advanced Manual":https://qr18.cn/FVlo89

Startup optimization

Memory optimization

UI

optimization Network optimization

Bitmap optimization and image compression optimization

Multi-threaded concurrency optimization and data transmission efficiency optimization

Volume package optimization

"Android Performance Tuning Core Notes Summary":https://qr18.cn/FVlo89

"Android Performance Monitoring Framework":https://qr18.cn/FVlo89

Guess you like

Origin blog.csdn.net/maniuT/article/details/130174611