Performance optimization practice of Android complex UI - PTQBookPageView performance optimization record

Author: Peng Taiqiang

1 Evaluation indicators & optimization results

To do performance optimization, you must first know how to measure and express performance. Because performance is a very abstract word, we must quantify and visualize it. Well, because it is UI component optimization, I first chose the GPU rendering mode analysis tool.

In the developer mode on the mobile phone, you can turn on the GPU rendering (rendering) mode analysis tool. Some systems also call it hwui or something. Find it yourself. After turning it on, a histogram will be displayed on the screen, which is intuitive. Look that there are many vertical bars, each vertical bar represents a rendered frame, and the height of these vertical bars represents the relative time it takes to render the previous frame at each stage of rendering. There will be a green line at the bottom of the screen, representing 16.6ms, and if the rendering time of one frame exceeds this green line, the so-called frame drop may occur. To put it simply, the lower each vertical bar is, the better .

This information can roughly tell us:

  • 1. Whether the current rendering time of one frame is within a reasonable range.
  • 2. The time consumption of each stage of rendering, so as to understand which aspects should be optimized to improve the rendering performance of the application.

Here is a brief introduction to this tool. For more detailed content, you can read the official documentation yourself (官方文档同样也给出了对应颜色竖条的排查问题的思路).

So, let's go back to the flipping component. In the previously released version v1.0.1, the drawing process is to calculate all the points first, build the Path, and then honestly draw layer by layer. (例如,先绘制最底层的下一页内容,再绘制翻起来的这一页内容,再绘制阴影,再绘制页面光泽等等...)Now we open the tool and take a look. This is a shocking look , as shown on the left. The bottom green line is 16.6ms. However, after turning the page, full screen vertical bars appear. This height is too scary, far beyond the reasonable range. Each frame takes a lot of time, which is equivalent to a serious drop. frame up. Now, we have to start optimizing.

Then, after a week of thinking and optimization, the final result is the picture on the right. Although there are still several big points that can be optimized, I am too lazy to write, so let’s optimize here first, and set it as v1.1.0 version.

Next, I will talk about the optimization ideas and process for your reference. If there are any deficiencies, you are welcome to discuss and criticize and correct.

2 thoughts

So now we know that the performance problem is very bad. If you want to optimize, you must first figure out which aspects can be optimized, and then how to optimize. The following are some thinking angles.

注:本节仅仅是思考的过程,具体的实现细节方案之类的在后面讨论,甚至,可能有一些本节列出的优化角度经验证后会是不可实现或实现成本巨大的,最后放弃了。

2.1 Compose

This UI component is firstly a component written by Jetpack Compose, so is there any major item that can be optimized in Compose?

1. Whether there is excessive restructuring

Regarding Compose, the first thing that comes to mind is whether the number of reorganizations is reasonable, that is, whether there are excessive reorganizations or unnecessary reorganizations. In fact, when the first version of the code was implemented, I considered this. Using the official Layout Inspector (default in the lower right corner of Android Studio), you can check each @Composable reorganization and confirm whether they are reasonable. Fortunately, , and no excessive reorganization occurred. This optimization point is skipped directly.

2. Reduce the frequency of reorganization and increase the speed of reorganization

Now the fact is: because every time the finger moves even a little bit, it will trigger the gesture monitoring (从log看我的小破测试机大概是10-12ms会触发一次onDrag回调), and then trigger the reorganization, and then recalculate all the points on the screen (而这个计算是很耗时的). After the calculation, the canvas API is called to draw according to the calculation result.

Based on the above factual process, after a brief thought, we can find several angles that may be optimized:

  • Can time-consuming calculations be placed in sub-threads? Then, after the time-consuming calculation is placed in the sub-thread, how to send the result back to Compose to trigger UI update? If this idea can be realized, then the speed of reorganization can be improved.
  • Is the onDrag callback triggered too frequently? Maybe there is no need to trigger gesture monitoring so frequently, that is to say, instead of triggering time-consuming calculations every time onDrag, there is a frequency reduction to ensure that the UI still looks coherent. This directly reduces recombination frequency.

3. Remove the redundant code in the reorganization

This is a very tricky point. For example, if you output a Log in a @Composable block that may be frequently reorganized, or even multiple Logs, on the one hand, it will indeed affect the performance of reorganization. On the other hand, if these Logs Containing State variables may even cause unnecessary reorganization to occur.

Therefore, if you must use Log debugging in the @Composable block, please delete or comment out these Logs after coding.

Regarding the Compose part, I can think of these things that can be optimized for the time being.

2.2 Bitmap

The next direction is the Bitmap direction, because the entire page-turning component, whether it is the algorithm (such as the distortion algorithm, the curve edge algorithm) or the Compose side implementation, is related to the Bitmap, so you must take a good look at the Bitmap-related parts.

2.2.1 The part about Bitmap in the component

Here is a brief mention of the part involving Bitmap in the component implementation, so as not to know what the optimization part is talking about later.

First of all, why is Bitmap used? Because there is a requirement to achieve a text distortion effect similar to that of paper turned up, as shown on the right side of the figure below.

This distorted implementation idea uses the drawBitmapMesh method of Canvas, that is to say, the component needs to perform a "screen capture" operation on any custom @Composable content, draw it into a Bitmap, and then call drawBitmapMesh on this Bitmap to distort it (既然是“截屏”操作,这也就解释了为什么组件支持显示任意的非动态的内容).

Every time the content of the page changes (例如当前页面内容有变化、或者翻页了), it is necessary to redraw the three Bitmaps of the previous page, the current page, and the next page, and these three Bitmaps are all the same size, which is the same size as the component.

The part involving Bitmap in the component will be mentioned here first, and then we will return to our optimization ideas.

1. Bitmap Config

Then, the first thing that comes to mind is the optimization of Bitmap itself, which has nothing to do with the project, such as some common ideas:

  • When loading an image, downsample the image to reduce loading consumption, reduce memory usage, and increase loading speed(这个思路我没有去管,因为组件实现中,Bitmap都是由截屏操作生成的,就是屏幕大小,但其实感觉可能可以针对不同尺寸的屏幕去做适配?因为如果屏幕很高清,生成的图片也会很大,但实际上也许不需要那么大。而因为我没有测试设备(我的设备是一个720*1600的低端机),所以这一块暂时没去管。)
  • Since it is a page-turning component, the paper on that page must be opaque. In this case, we don’t need a transparent Bitmap when generating a Bitmap from a screenshot, that is, the Bitmap can actually use RGB565 format instead of ARGB8888. In this way, the memory The occupancy is directly reduced by half, and the subsequent operations on the Bitmap will be faster.
  • Regarding downsampling, similarly, when drawingBitmapMesh, you will need to set the number of grid points of the mesh. Similarly, reducing the number of grid points will also lead to improved performance.

2. Multiplexing of Bitmap

Since the number of Bitmaps involved in the subsequent drawing of the component is fixed, there are only 3 (previous page, current page, next page), and, in most actual scenarios, the size of the page turning component is fixed Yes, it will not change easily, so you can think that these 3 Bitmaps can actually be reused, that is, when drawing a new Bitmap, the new pixels are directly overlaid on the memory allocated by the original Bitmap, so that there is no need to turn the page or refresh every time When the page is first recycle and then re-create, as long as the component size does not change, redundant memory recovery and reallocation can be avoided.

In addition, since we talk about reuse, we can also think of some other reusable large objects, such as Canvas and Path used in drawing, because their creation and recycling are also native, which can reduce creation and recycling the consumption caused.

2.3 Drawing

As a UI component, another direction of thinking is some common optimization points of UI components.

1. Is the layout nested too deeply?

If the layout of the component is nested too deeply, it will definitely affect the performance, but fortunately this is not the case for this component (PTQBookPageViewInner中也只有一个Box和Canvas), so this optimization point is skipped directly.

2. Whether there is overdrawing

Overdrawing means that the same pixel is updated repeatedly due to poorly written code. We can only see the top layer, so we can consider whether there are a lot of masked areas, since these areas are invisible, they should not be drawn by themselves.

There is also a tool here, which comes with the system, called Debug GPU overdrawing. Open this tool in the developer options, and it will display our overdrawn area on the screen.

Now let's take a look at the area before optimization (the left picture below), if it is red, it can be basically determined, and this is also a big point that can be optimized.

In actual development, some overdrawing is unavoidable, so what we have to do is to reduce overdrawing as much as possible. After thinking about the optimization plan, we achieved the effect of the picture on the right below, reducing some overdrawing and improving performance. .

3. Time-consuming drawing

Some Canvas and Path APIs may be relatively time-consuming, and we should minimize the calls of such APIs

These are the possible optimization points and ideas I can think of so far, and I will start to implement them below.

3 realization

Some details will not be mentioned, such as the reuse of Path, etc. This section will talk about some main parts.

3.1 BitmapController optimization

The main changes in this part are the reuse of Bitmap and the creation process of Bitmap (这个是代码上的优化,不涉及性能).

In PTQBookPageBitmapController, use an array with a size of 3 as the multiplexing pool of Bitmap.

private val bitmapBuffer = arrayOfNulls<Bitmap?>(3)

Call the controller's renderAndSave in the dispatchDraw rewritten by AbstractComposeView, and renderAndSave will provide a Canvas. This Canvas has already prepared the Bitmap. If it can be drawn, it will be drawn by super.dispatchDraw.

override fun dispatchDraw(canvas: Canvas?) {
    controller.renderThenSave(width, height) {
        super.dispatchDraw(it)
    }
}

Take a look at the implementation of renderThenSave.

fun renderThenSave(width: Int, height: Int, render: (drawable: Canvas) -> Unit) {
    //如果不再需要bitmap,则不再绘制了
    if (needBitmapPages.isEmpty() || width <= 0 || height <= 0) {
        return
    }

    //当前需要绘制第几页的
    val first = needBitmapPages.first()

    //这里判断是否需要重新创建Bitmap而不是从复用池去取
    var needNew = false
    if (bitmapBuffer[first.second] == null) {
        needNew = true
    } else {
        //新的大小发生变化(因为config不变,所以bitmap的大小可以认为只受width, height影响,而不再去计算allocationByteCount)
        bitmapBuffer[first.second]!!.let {
            if (width != it.width || height != it.height) {
                it.recycle()
                needNew = true
            }
        }
    }

    //如果需要新创建,则创建一个RGB565格式的Bitmap
    if (needNew) {
        bitmapBuffer[first.second] = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)
    }

    canvas.let {
        //Canvas设置Bitmap以在dispatchDraw时把内容绘制到Bitmap上
        it.setBitmap(bitmapBuffer[first.second]!!)
        //一切准备就绪后,才会回调render,让dispatchDraw给Bitmap填充内容
        render(it)
        //记得清掉引用
        it.setBitmap(null)
    }

    //如果还需要绘制下一张,则继续,否则流程终止
    needBitmapPages.removeFirst()
    if (needBitmapPages.isEmpty()) return
    exeRecompositionBlock?.let { it() }
}

That's all for the logic of Bitmap multiplexing. Next, let's talk about the optimization of overdrawing.

3.2 Drawing optimization

In the second point of Section 2.3, we mentioned that overdrawing is a major item that can be optimized. At the same time, we mentioned in the third point that too large a Path will also affect the time-consuming of calling the Path API. Therefore, this section mainly optimizes these two cases.

For overdrawing, I checked the drawing code, and for the overlapping drawing area, I tried to reduce the drawing of the repeated area as much as possible. The specific code of this part will not be posted. The code is in the @Composable of Canvas of PTQBookPageViewInner.

Regarding the part of overdrawing, I originally wanted to directly use a Bitmap to "synthesize" the distorted map and the base map, so as to reduce the number of draws, but failed after trying. The part of the code that is commented out about synthesizedBitmap is this failed attempt.

As for the problem that the Path is too large, let’s think about it. According to the page point model we abstracted, when the page is close to the vertical state, the endpoint of the Path will extend far and far upwards. I have seen it with the log. Even the y coordinate of some points reaches 200,000 to 300,000, which is exaggerated, so the main factor affecting the Path is too large is when it is vertical, so we need to facilitate calculations for some parts (in some places, the curve is not easy to calculate, I have to design an algorithm but I’m too lazy to think about it) to perform special vertical processing on the drawing layer. Taking the construction of the shadow3 Path in the buildPath function as an example, I calculated the intersection point between the out-of-bounds line and the component frame range to avoid excessive The Path point appears, and the code is as follows.

//shadow3
pathResult.shadowPaths[2].apply {
    moveTo(W)
    lineTo(S1)
    //若接近垂直,则直接画成矩形,否则画梯形
    if (((T1.y - O.y) / (C.y - O.y)).absoluteValue > shadow3VerticalThreshold) {
        lineTo(S1.copy(y = (C.y - O.y).absoluteValue - S1.y))
        lineTo(W.copy(y = (C.y - O.y).absoluteValue - W.y))
    } else {
        /**
         * @since v1.1.0 越界绘制优化:如果Z在BC内,则直接画线,否则求交点
         */
        //给一组log数据供参考
        //buildPath: C.y:0 O.y:1600 upsideDown:true W: Point(x=376.90134, y=0.0) S1: Point(x=523.4354, y=0.0) T1: Point(x=720.0, y=36051.1) Z: Point(x=720.0, y=62926.297)
        //buildPath: C:1600.0 O.y:0 upsideDown:false W: Point(x=380.06815, y=1600.0) S1: Point(x=526.1546, y=1600.0) T1: Point(x=720.0, y=-46201.938) Z: Point(x=720.0, y=-82226.625)
        val S1T1_OBx = Line.withKAndOnePoint(lST.k, S1).x(O.y) //S1T1交OB的x坐标
        val WZ_OBx = Line.withKAndOnePoint(lST.k, W).x(O.y)
        lineTo(if (S1T1_OBx > C.x) T1 else Point(S1T1_OBx, O.y))
        lineTo(if (S1T1_OBx > C.x) Z else Point(WZ_OBx, O.y))
    }
    close()
}

3.3 Unrealized optimizations

This part records unrealized or failed optimizations, but I think the idea may still be useful.

1. Native layer for image synthesis

If there are two pictures that want to be stitched left and right, or four pictures that want to be stitched left and right, but it is more performance-consuming, you can consider ndk development and directly manipulate the pixels of the picture in the native layer, but I failed here because I need to use it first. The canvas API handles images, and it is even more unnecessary to manipulate pixels.

Let me mention here, if you want to manually convert an RGB565 image to ARGB8888, the conversion method for each pixel.

RGB565 is a 16-bit pixel, from high to low are R5, G6, B5, and ARGB8888 is 32 bits, each byte is 8, but there is a pit here, ARGB8888 is ABGR from high to low .

code show as below:

static uint32_t rgb565PixelToArgb8888(uint16_t pixel) {
    uint8_t r = ((pixel >> 11) & 0x1F) * 0xff / 0x1f;
    uint8_t g = ((pixel >> 5) & 0x3F) * 0xff / 0x3f;
    uint8_t b = (pixel & 0x1F) * 0xff / 0x1f;
    return 0xff << 24 | (b & 0xff) << 16 | (g & 0xff) << 8 | (r & 0xff);
}

2. Open sub-thread calculation in Compose and reduce the callback frequency of gestures

This is the optimization idea mentioned in point 2 of Section 2.1. I didn't do it because I was too lazy. The idea of ​​implementation is probably that after the gesture is triggered, a coroutine of another thread is started to perform complex calculations, and then the result is directly sent to the state variable in @Composable with flow (collectAsState), causing the UI to update. To limit the frequency, you can try to use the debounce method of flow.

I didn't realize this part, so the above is just a hypothesis, and there may be other problems in actual operation, but it can be regarded as an idea for reference.

4 Epilogue

The current components have been optimized to a usable level. As mentioned in the article, in fact, they can be further optimized. For example, time-consuming calculations can be placed in new threads, or rewritten in C++. It should be optimized, but I am too lazy to implement it. .

I have learned a lot after optimizing this trip, and I have gained a lot, but the pace of learning cannot be stopped, and there are still many details that need to be learned, so let’s take it step by step.

For the optimization of complex UI, I hope some ideas in this article can help you, so I will write it here.

Android study notes

Android Performance Optimization: https://qr18.cn/FVlo89
Android Vehicle: https://qr18.cn/F05ZCM
Android Reverse Security Study Notes: https://qr18.cn/CQ5TcL
Android Framework Principles: https://qr18.cn/AQpN4J
Android Audio and Video: https://qr18.cn/Ei3VPD
Jetpack (including Compose): https://qr18.cn/A0gajp
Kotlin: https://qr18.cn/CdjtAF
Gradle: https://qr18.cn/DzrmMB
OkHttp Source Code Analysis Notes: https://qr18.cn/Cw0pBD
Flutter: https://qr18.cn/DIvKma
Android Eight Knowledge Body: https://qr18.cn/CyxarU
Android Core Notes: https://qr21.cn/CaZQLo
Android Past Interview Questions: https://qr18.cn/CKV8OZ
2023 Latest Android Interview Question Collection: https://qr18.cn/CgxrRy
Android Vehicle Development Job Interview Exercises: https://qr18.cn/FTlyCJ
Audio and Video Interview Questions:https://qr18.cn/AcV6Ap

Guess you like

Origin blog.csdn.net/weixin_61845324/article/details/131638064