Source: Cai Peipei (@Vince Cai Peipei)
Link: androidtest.org/android-graphics-performance-pattens/
Related Reading:
10 Tips to Improve Android Performance
[Essentials] New Season – Android Performance Optimization Model
Android Performance Optimization (Must-Have for Interviews)
Summary of Android Performance Analysis Tools
The Most Comprehensive and Systematic Android UI Performance Optimization Materials
The UI is the most critical part of an Android application that directly affects user experience. If the code implementation is poor, the UI may stutter and cause the application to consume a lot of memory.
Companies like ours that create ROMs are even more different; pre-installed applications must be very smooth, as the first impression given to customers or users is speed. A stuttering and slow application experience will affect customers’ or users’ confidence and evaluation of the product, so it cannot be ignored.
Table of Contents
-
1. Android Rendering Knowledge
-
1.1 Drawing Principles
-
1.2 Frame Drops
-
1.3 Why 60 FPS?
-
1.4 Garbage Collection
-
1.5 UI Thread
-
1.6 Vertical Synchronization
-
1.7 UI Drawing Mechanism and Rasterization
-
2. Detection and Resolution
-
2.1 Detection Dimensions
-
2.2 Debugging Tools
-
2.3 How to Solve
-
3. UI Overdraw
-
3.1 Concept of Overdraw
-
3.2 Tracking Overdraw
-
3.3 Causes of Overdraw
-
3.4 Impact of Irregular XML Layouts on Drawing
-
3.5 Source Code Related
-
4. Rendering Performance
-
4.1 Concept of Rendering Performance
-
4.2 Tracking Rendering Performance
-
4.3 Causes of Poor Rendering Performance
-
4.4 Detection Explanation
-
4.5 Supplementary Explanation of UI Drawing Mechanism
-
5. Reasonableness of Layout Boundaries
-
6. UI Optimization Suggestions for Developers
-
6.1 Optimizing Layout Structure
-
6.2 Optimizing Processing Logic
-
6.3 Effectively Using DEBUG Tools
-
Appendix
1. Android Rendering Knowledge
1.1 Drawing Principles
The Android system requires that each frame must be drawn within 16ms. Smoothly completing a frame means that any special frame must execute all rendering code (including commands sent to the GPU and CPU to draw to the buffer) within 16ms to maintain a smooth experience. This speed allows the system to render at about 60 frames per second (1 second / 0.016 frames per second = 62.5 frames per second) during animations and input events.
If your application does not complete the drawing of this frame within 16ms, for example, if it takes 24ms to draw this frame, then frame drops will occur.
The system is preparing to draw a new frame on the screen, but this frame is not ready, so there will be no drawing operation, and the screen will not refresh. Feedback to the user means that the user is staring at the same image for 32ms instead of 16ms, meaning that frame drops have occurred.
1.2 Frame Drops
Frame drops are a very core issue in user experience. The current frame is discarded, and it cannot maintain the previous frame rate after that. This discontinuous interval is likely to attract the user’s attention, which is what we often refer to as stuttering or lack of smoothness.
There are many reasons for frame drops, such as:
-
Spending too much time redrawing most of the elements in the UI, which wastes CPU cycles;
-
Severe overdraw, spending too much time drawing objects that the user cannot see;
-
A large number of animations repeating over and over, consuming CPU and GPU resources;
-
Frequent triggering of garbage collection;
1.3 Why 60 FPS?
The Android system requires that each frame must be drawn within 16ms, so the frame rate per second is about 60 frames per second (1 second / 0.016 frames per second = 62.5 frames per second). But why is 60 FPS used as the performance measurement standard for apps? This is because the collaboration between the human eye and brain cannot perceive updates to the screen beyond 60 FPS.
Most Android devices on the market have a screen refresh rate of 60 Hz. Of course, exceeding 60 FPS is meaningless, as the human eye cannot perceive the difference. 24 FPS is the frame rate that the human eye can perceive as continuous linear motion, which is the common frame rate for film reels, as this frame rate is sufficient to support the content expressed in most film images while minimizing costs. However, below 30 FPS, it is impossible to smoothly present stunning visual content, at which point 60 FPS is needed to achieve the desired effect. For more information on FPS knowledge, see ‘Wiki’.
The performance goal of an application’s UI is to maintain 60 FPS, which means you only have 16 ms (1 second / 60 frame rate) to handle all tasks for each frame.
1.4 Garbage Collection
The garbage collector is a mechanism that automatically releases memory that is no longer referenced during the application’s runtime, commonly referred to as GC. Frequent GC is also one of the main culprits causing serious performance issues.
As mentioned earlier, smoothly completing a frame means that all rendering code must be completed within 16ms. Frequent GC severely limits the remaining time within a frame, and if the work done by GC exceeds the necessary work, the less time is left for the application to maintain a smooth frame rate. The closer it gets to 16ms, the more likely it is to cause stuttering when garbage collection events are triggered.
Note that Android 4.4 introduced a new ART virtual machine to replace the Dalvik virtual machine. Their mechanisms are quite different; in simple terms:
-
The GC of the Dalvik virtual machine is very resource-intensive, and even a well-performing Android device will easily consume 10-20 ms of time under normal conditions;
-
The GC of the ART virtual machine dynamically improves garbage collection efficiency, and the interruptions in ART typically occur within 2-3 ms, resulting in a significant performance improvement over the Dalvik virtual machine;
The ART virtual machine has a significant performance improvement over the Dalvik virtual machine regarding garbage collection, but the 2-3 ms recovery time is still enough to exceed the 16ms frame rate limit. Therefore, even though garbage collection is no longer a resource-intensive behavior after Android 5.0, it should still be avoided as much as possible, especially during animations, as it may lead to noticeable frame drops for users.
For more detailed information on the ART and Dalvik virtual machine garbage collection mechanisms, you can ‘click here’ and ‘me’ for in-depth understanding.
1.5 UI Thread
The UI thread is the main thread of the application, and many performance and stuttering issues arise from performing a large amount of work on the main thread.
Therefore, all resource-intensive operations, such as IO operations, network operations, SQL operations, and list refreshes, should be implemented using background processes and should not occupy the main thread. The main thread is the UI thread, which is key to maintaining the smoothness of the program;
In Android 5.0, the Android framework introduced the ‘Render Thread’ to send actual rendering operations to the GPU. This thread alleviates some of the operations reduced from the UI thread. However, input, scrolling, and animations still occur on the UI thread, as the thread must be able to respond to operations.
1.6 Vertical Synchronization
Vertical synchronization is a new technology introduced in Android 4.1 through Project Butter in the UI architecture, along with technologies such as Triple Buffer and HWComposer, all aimed at improving UI smoothness.
For example, if you take a photo, then rotate it 5 degrees and take another photo, and then cut and splice the middle of the two photos together, you get the following image:
The middle part has a clear difference, equivalent to the result of the device refresh rate and frame rate being inconsistent.
Generally speaking, the GPU’s frame rate should be higher than the refresh rate to avoid stuttering or frame drops. If the screen refresh rate is faster than the frame rate, the screen will display the same image between two frames. When this discontinuous situation occurs repeatedly, the user will clearly feel the animation stuttering or frame drops and then return to normal, which we often refer to as flickering, frame skipping, or delay.
Applications should avoid these frame rate drops to ensure that the GPU can complete data acquisition and writing before the screen refresh, ensuring smooth animations.
1.7 UI Drawing Mechanism and Rasterization
Most rendering operations rely on two hardware components: the CPU and GPU. The CPU is responsible for Measure, layout, Record, Execute computational operations, while the GPU is responsible for rasterization operations. Non-essential view components will bring extra CPU computational operations and occupy additional GPU resources.
Rasterization can split resources such as Button, Shape, Path, Bitmap into different pixels for display. This operation is time-consuming, so the GPU is introduced to speed up the rasterization process.
The CPU is responsible for calculating the UI components into polygons, textures, and then handing them over to the GPU for rasterized rendering, and then passing the processing results to the screen for display.
In Android, the display of resource components (such as Bitmaps, Drawable) is all packaged into a unified texture and then passed to the GPU.
The display of images is first calculated by the CPU and loaded into memory, then passed to the GPU for rendering.
The display of text is first converted into textures by the CPU, then passed to the GPU for rendering, and when the CPU draws individual characters, it re-references the content rendered by the GPU.
Displaying animations is even more complex; we need to complete all CPU and GPU calculations, drawings, rendering, etc., within 16 ms to achieve a smooth application experience.
2. Detection and Resolution
2.1 Detection Dimensions
Depending on the different business needs and the required testing granularity, there will be different detection dimensions. The dimensions required for the interface performance testing in my current business are as follows:
-
UI overdraw; (detect overdraw)
-
Rendering performance; (detect UI rendering performance under strict mode)
-
Reasonableness of layout boundaries; (detect the reasonableness of element display)
In some special tests, certain user scenarios may also include other hidden detection dimensions, such as:
-
OpenGL tracking analysis;
-
GPU view update reasonableness;
-
Flash hardware layer update reasonableness;
-
Animation acceleration/deceleration state issue detection;
-
……
2.2 Debugging Tools
Detecting and solving UI performance issues largely depends on your application architecture. Fortunately, Android provides many debugging tools, and knowing how to use these tools is important, as they can help us debug and analyze UI performance issues to give the application a better performance experience. Below are some common Android UI performance debugging tools:
2.2.1 Hierarchy View
Hierarchy View is built into the Android SDK and is commonly used to check whether the view structure of the interface is overly complex, understand which views are overdrawn, and how to improve them. See the official usage tutorial (requires VPN): ‘click here’, official introduction ‘click here’.
2.2.2 Lint
Lint is a static code scanning tool included with ADT, which can provide improvement suggestions for unreasonable or risky modules in XML layout files and project code. Adding the editor’s WeChat: AMEPRE, the official tips for actual use of Lint are as follows:
-
Contains useless branches; it is recommended to remove them;
-
Contains useless parent controls; it is recommended to remove them;
-
Warns that the layout depth is too deep;
-
It is recommended to use compound drawables;
-
It is recommended to use merge tags;
-
……
More official introduction of Lint ‘click here’.
2.2.3 Systrace
Systrace is built into Android DDMS and can be used to track graphics, view, and window information to discover some deep issues. It’s quite cumbersome and has many limitations, so I rarely use it in practical debugging. Official introduction ‘click here’ and ‘me’.
2.2.4 Track
Track is built into Android DDMS and is a great tool for tracking which methods are time-consuming when constructing views, down to every function, whether it’s an application function or a system function. We can easily see where frame drops occur and the call situation of all functions in that frame to find and optimize the problem. Official introduction ‘click here’.
2.2.5 OverDraw
By opening ‘Debug GPU Overdraw’ in the developer options of the Android device settings app, you can view the overdraw situation of all interfaces and branch interfaces of the application, facilitating optimization. Official introduction ‘click here’.
2.2.6 GPU Presentation Mode Analysis
By enabling ‘GPU Presentation Mode Analysis’ in the developer options of the Android device settings app, you can obtain the rendering time of each of the last 128 frames, analyzing the performance rendering performance and performance bottlenecks. Official introduction ‘click here’.
2.2.7 StrictMode
By enabling ‘Strict Mode’ in the developer options of the Android device settings app, you can see which operations take too long on the main thread. When some operations violate strict mode, the edges of the screen will flash red, and relevant StrictMode information will be output to the LOGCAT log.
2.2.8 Animator Duration Scale
By enabling ‘Window Animation Scale’ / ‘Transition Animation Scale’ / ‘Animation Duration Scale’ in the developer options of the Android device settings app, you can speed up or slow down the animation time to see if there are any issues with the animations under accelerated or slowed conditions.
2.2.9 Show Hardware Layer Updates
By enabling ‘Show Hardware Layer Updates’ in the developer options of the Android device settings app, when the Flash hardware layer is being updated, it will be displayed in green. Using this tool allows you to see which unexpected layouts are updated during animations, facilitating optimization to achieve better application performance. Example ‘Optimizing Android Hardware Layers’ (requires VPN): ‘click here’.
2.3 How to Solve
As mentioned earlier, the required testing dimensions for my company are as follows:
-
UI overdraw; (detect overdraw)
-
Rendering performance; (detect UI rendering performance under strict mode)
-
Reasonableness of layout boundaries; (detect the reasonableness of element display)
Therefore, the following will focus on these two points, explaining how to solve them specifically from concepts, tracking, digging out the root causes, and the tools for troubleshooting, as well as providing optimization suggestions for developers.
3. UI Overdraw (OverDraw)
3.1 Concept of Overdraw
Overdraw is a term that indicates that the drawing times of a certain component on a pixel point on the screen exceed 1 time.
In simple terms, drawing an interface can be likened to a graffiti artist painting a wall; graffiti is a labor-intensive task, and each point on the wall may be painted in various colors during the graffiti process, but the final color presented can only be one. This means that the non-final colors painted during the graffiti process, which are invisible to passersby, are a waste of time, effort, and resources, and there is significant room for improvement. Similarly, drawing an interface that spends too much time drawing stacked, invisible elements is wasting CPU cycles and rendering time!
Official example, the activated card by the user is on top, while those that are not activated are below, spending too much time drawing invisible objects.
3.2 Tracking Overdraw
By enabling ‘Debug GPU Overdraw’ in the developer options of the Android device settings app, you can view the overdraw situation of all interfaces and branch interfaces of the application, facilitating optimization.
Android will display different shades of colors on the screen to indicate overdraw:
-
No color: No overdraw, meaning a pixel point is drawn once, displaying the original color of the application;
-
Blue: 1x overdraw, meaning a pixel point is drawn twice;
-
Green: 2x overdraw, meaning a pixel point is drawn three times;
-
Light red: 3x overdraw, meaning a pixel point is drawn four times;
-
Dark red: 4x overdraw and above, meaning a pixel point is drawn five times or more;
The hardware performance of the device is limited; when overdraw causes the application to consume more resources (beyond available resources), performance will degrade, manifesting as stuttering, lack of smoothness, ANR, etc. To maximize the application’s performance and experience, it is necessary to minimize overdraw, aiming for more blue blocks rather than red blocks.
In practical testing, the following two points are commonly used as testing indicators for overdraw, controlling the overdraw within an agreed reasonable range:
-
All interfaces and branch interfaces of the application do not exceed 4X overdraw (dark red areas);
-
In all interfaces and branch interfaces of the application, the total area of 3X overdraw (light red areas) does not exceed 1/4 of the visible area of the screen;
3.3 Causes of Overdraw
Overdraw largely comes from the problem of overlapping views, as well as unnecessary background overlaps.
Official example: if every View in an application has a background, it will look like the first image, while after removing these unnecessary backgrounds (referring to the default background of the Window, the background of the Layout, and the possible backgrounds of text and images), the effect will look like the second image, with basically no overdraw situation.
3.4 Impact of Irregular XML Layouts on Drawing
When the depth of the node tree in the layout file is too deep, and the tags and attributes in the XML are too many, it will have disastrous effects on the display of the interface.
To display an interface, the first step is to parse the layout, and after requestLayout, a series of measure, layout, and draw operations must be performed. If the layout file is too deeply nested and has too many tags and attributes, the execution time of each step will be affected, and the display of the interface will only occur after all these operations are completed. Therefore, the longer the execution time of each operation, the longer the display time will be.
3.5 Source Code Related
For those capable and interested in viewing the source code, the location of the overdraw source code is: /frameworks/base/libs/hwui/OpenGLRenderer.cpp, you can go study it.
if (Properties::debugOverdraw & getTargetFbo() == 0) {
const Rect* clip = &mTilingClip;
mRenderState.scissor().setEnabled(true);
mRenderState.scissor().set(clip->left,
mState.firstSnapshot()->getViewportHeight() – clip->bottom,
clip->right – clip->left,
clip->bottom – clip->top);
// 1x overdraw
mRenderState.stencil().enableDebugTest(2);
drawColor(mCaches.getOverdrawColor(1), SkXfermode::kSrcOver_Mode);
// 2x overdraw
mRenderState.stencil().enableDebugTest(3);
drawColor(mCaches.getOverdrawColor(2), SkXfermode::kSrcOver_Mode);
// 3x overdraw
mRenderState.stencil().enableDebugTest(4);
drawColor(mCaches.getOverdrawColor(3), SkXfermode::kSrcOver_Mode);
// 4x overdraw and higher
mRenderState.stencil().enableDebugTest(4, true);
drawColor(mCaches.getOverdrawColor(4), SkXfermode::kSrcOver_Mode);
mRenderState.stencil().disable();
}
}
4. Rendering Performance
4.1 Concept of Rendering Performance
Rendering performance is often the culprit of frame drops, and this problem is quite common and troublesome. Fortunately, Android provides us with a powerful tool to easily track performance rendering issues and see what causes your application to stutter or drop frames.
4.2 Tracking Rendering Performance
By enabling the ‘GPU Presentation Mode Analysis’ option in the developer options of the Android device settings app, and selecting ‘Display as Bar Graph on Screen’.
This tool will display the GPU drawing graphics data of the last 128 frames of the current interface in real-time on the Android device’s screen, including StatusBar, NavBar, and GPU drawing graphics bar chart data of the current interface. We generally only need to care about the GPU drawing graphics data of the current interface.
There are a total of 128 small bar graphs on the interface, representing the GPU drawing graphics data of the last 128 frames of the current interface. Each small bar graph represents the time consumed for rendering this frame, and the higher the bar graph, the longer the time consumed. As the interface refreshes, the bar graph information will also refresh in real-time.
There is a green line in the middle representing 16 ms, and maintaining smooth animations hinges on keeping these vertical bars as much as possible below the green line. Anytime they exceed the green line, you risk losing a frame of content.
Each bar graph is composed of three colors: blue, red, and yellow.
-
Blue represents the time taken to draw the Display List for this frame. In simple terms, it records the time spent updating the view on the screen. In code language, it refers to the time spent executing the view’s onDraw method, creating or updating each view’s Display List.
-
Red represents the time taken to render the Display List for this frame using OpenGL. In simple terms, it records the time taken to execute view rendering. In code language, it refers to the time spent using Android’s OpenGL ES API to render the Display List.
-
Yellow represents the time the CPU waits for the GPU to process. In simple terms, it is the wait time for the CPU to receive a response from the GPU after sending commands. In code language, this is a blocking call.
In practical testing, the following two points are commonly used as testing indicators for rendering performance, controlling the rendering performance within an agreed reasonable range:
-
During the execution of all functions and branch functions of the application, the area involved in the bar graphs should maintain at least 90% below the green line;
-
From the perspective of user experience, subjectively judge whether the application feels acceptable in terms of stuttering during all operations on Android devices with 512 MB of memory.
4.3 Causes of Poor Rendering Performance
When you see the blue line is high, it may be due to your view suddenly becoming invalid and needing to redraw or due to overly complex custom views taking too long.
When you see the red line is high, it may be due to your view needing to redraw (for example, when the screen rotates from portrait to landscape, the current interface is recreated) or due to complex custom views that are complicated to draw, leading to excessive time consumption. For example, the following view:
When you see the yellow line is high, it indicates that you are giving too much work to the GPU, and too many views require OpenGL commands for drawing and processing, causing the CPU to wait too long for a reply from the GPU.
4.4 Detection Explanation
This tool can effectively help you find rendering-related issues, identify performance bottlenecks causing the application to stutter or slow down, and optimize at the code level. It can even allow product designers to improve their designs to achieve a good user experience.
When detecting rendering performance, it is often accompanied by enabling ‘Strict Mode’ to see which scenarios take too long on the UI thread (main thread).
Additionally, some powerful but rarely used tools can assist in analyzing rendering performance, such as:
-
HierarchyViewer: This tool is commonly used to check whether the view structure of the interface is overly complex, understand which views are overdrawn, and how to improve them;
-
Tracer for OpenGL: This tool collects all drawing commands sent to the GPU from the UI interface. It is commonly used to assist developers in debugging and locating some difficult rendering detail issues that the HierarchyViewer tool cannot identify.
4.5 Supplementary Explanation of UI Drawing Mechanism
As mentioned above, layouts and UI components are first computed by the CPU into polygons and textures that the GPU can recognize and draw, and then handed over to the GPU for rasterized rendering, and finally, the processing results are passed to the screen for display. The operation of ‘computing into objects that the GPU can recognize and draw’ is accomplished with the help of the DisplayList. The DisplayList contains the data information to be rasterized and rendered by the GPU to the screen.
The DisplayList is created when a view needs to be rendered for the first time. When that view needs to be redrawn due to changes such as movement, the GPU only needs to execute one rendering command to update it on the screen. However, if the drawing content in the view changes (for example, if it becomes invisible), the previous DisplayList can no longer be used, and the system will recreate the DisplayList, render it, and update it on the screen. The performance of this process depends on the complexity of the view.
5. Reasonableness of Layout Boundaries
This chapter is relatively simple, and considering that it should not have an audience, I will not elaborate on it.
6. UI Optimization Suggestions for Developers
6.1 Optimizing Layout Structure
Complex layout structures can slow down rendering speed and create performance bottlenecks. We can optimize using the following common and effective layout principles:
-
Avoid complex View hierarchies. The more complex the layout, the more bloated it becomes, and the more likely it is to encounter performance issues. Look for the most resource-efficient way to display nested content;
-
Try to avoid using RelativeLayout at the top of the view hierarchy. RelativeLayout is resource-intensive because it requires two measurements to ensure that it handles all layout relationships, and this issue becomes more severe with the increase of RelativeLayout in the view hierarchy;
-
In similar situations, it is recommended to use LinearLayout instead of RelativeLayout, as LinearLayout has better performance; if relative layout is indeed needed for branches, consider using a more optimized GridLayout, which has pre-processed branch view relationships to avoid the double measurement issue;
-
For relatively complex layouts, it is advisable to use RelativeLayout, as it can easily implement layouts that would require nested LinearLayouts;
-
Avoid using AbsoluteLayout;
-
Extract reusable components and reuse them with
tags. If a certain layout is used in multiple places within the application, write it as a layout component to facilitate reuse across different UIs. Official detailed explanation ‘click here’ -
Use merge tags to reduce layout nesting levels, official detailed explanation ‘click here’;
-
Remove unnecessary invisible backgrounds. For layouts with multiple background colors, only keep the topmost color visible to the user, and remove other invisible colors to reduce ineffective drawing operations;
-
Avoid using layout_weight attributes as much as possible. Using LinearLayouts with layout_weight attributes requires each child component to be measured twice, consuming excessive system resources. This issue is especially important when using ListView and GridView tags, as child components will be repeatedly created. Balanced layouts can be achieved using a RelativeLayout with a 0dp view as a divider; if not, then…;
-
A reasonable layout structure should be wide and shallow, rather than narrow and deep;
6.2 Optimizing Processing Logic
-
Load views on demand. Certain resource-intensive views that are not frequently reused can be loaded only when needed to improve UI rendering speed;
-
Use ViewStub tags to load infrequently used layouts;
-
Dynamic inflation of views is better in performance than using ViewStub tags’ setVisibility, although in some cases, using ViewStub tags is more appropriate;
-
Avoid unnecessary resource-intensive operations to save precious computation time;
-
Avoid heavy operations on the UI thread. Resource-intensive operations (such as IO operations, network operations, SQL operations, list refreshes, etc.) should be implemented using background processes and should not occupy the UI thread. The UI thread is the main thread, which is key to maintaining the smoothness of the program, and should only handle core UI operations, such as manipulating view properties and drawings;
-
Minimize wake-up mechanisms. We often use broadcasts to receive expected messages and events, but excessive responses beyond the actual needs can consume unnecessary Android device performance and resources. Therefore, we should minimize wake-up mechanisms and turn off broadcasts when the application is unconcerned with these disappearances and events, carefully selecting which Intents to respond to.
-
Consider low-end devices, such as those with 512 MB of memory, dual-core CPUs, and low resolutions, to ensure your application can meet the needs of devices at different levels.
-
Optimize the application’s startup speed. When starting an application, the prompt display of the interface can provide a good user experience. To start faster, you can delay loading some UIs and avoid initializing code at the application level.
6.3 Effectively Using DEBUG Tools
-
Make good use of the debugging tools provided by Android to track the performance of the main functions of the application;
-
Make good use of the debugging tools provided by Android to track the memory allocation of the main functions of the application;
Appendix
This article was first published on androidtest.org: ‘Comprehensive Guide to Android UI Performance Optimization’
References
-
Developing for Android I Understanding the Mobile Context
-
Developing for Android IX Tools
-
Training for Android developers
-
Android developers
-
Google developers
-
Google I/O 2013
About Java and Android Big Cow Channel
The Big Cow Channel for Java and Android is a public account with tens of thousands of followers discussing Java and Android development, sharing and creating the most valuable articles to help you become an expert in this field!
We discuss the cutting-edge technologies in Android and Java development: Android performance optimization, pluginization, cross-platform, dynamic, strengthening and anti-cracking, etc., as well as design patterns/software architecture, formed by a team of engineers from BAT.
Follow us for a red envelope, reply: ‘Baidu’, ‘Ali’, ‘Tencent’ for surprises!!! After following, you can join the WeChat group. The group consists of experts from Baidu, Ali, and Tencent.
Welcome to follow us, let’s discuss technology together, scan and press the QR code below to quickly follow us. or search for the WeChat public account: JANiubility.
Public Account: JANiubility