Android performance optimization page optimization

Foreword:

1. There are many articles on the Internet about how to optimize interface performance, but they often talk about how to solve the problem of memory fluctuation. Although the two are related, they cannot be completely equated. Memory fluctuation does affect the drawing of the interface to a certain extent, but it is only one of the reasons and cannot be confused. And there are a lot of introductions about over-drawing, etc., and they all feel the same.

2. The purpose of writing this article is to help beginners, intermediates, including some advanced Android developers, to quickly locate/solve the interface stuck problems encountered in practical application scenarios, or to guide a reasonable direction to solve the problem. Specific solutions still require specific solutions.

3. Of course, after fully understanding the principle of this article, it will also be helpful for Android interviews.

1. How to measure whether the interface is stuck

There are mainly three generations of solutions to measure whether the page is stuck. Although there are some monitoring frameworks such as perfdog in the middle, they basically belong to the principles of these three generations.

1.1 The first generation solution: Looper registers callback method (representative framework: BlockCanary)

1.1.1 Principle introduction:

The core principle is to use the callback in Looper to judge the execution time of the task in each Message. If we register that the callback object is the main thread Looper, then we can know the execution time of each task in the main thread. And if a task takes too long to execute, it will cause a stuck problem.

So in a strict sense, this solution should be to detect whether the main thread is stuck, not whether the interface is drawn smoothly.

The specific principle will not be described in detail here. If you want to know the principle, you can refer to Chapter 7 of my other article:

Android source code learning-Handler mechanism and its six core points - Sharing + Recording - CSDN Blog

1.1.2 How to use:

There are more detailed methods in BlockCanary, the links are as follows:

GitHub - markzhai/AndroidPerformanceMonitor: A transparent ui-block detection library for Android. (known as BlockCanary)

Here I provide a simple version of the use method, the effect is basically the same, the code is as follows:

public class ANRMonitor extends BaseMonitor {

    final static String TAG = "anr";

    public static void init(Context context) {
        if (true) {
            return;
        }
        ANRMonitor anrMonitor = new ANRMonitor();
        anrMonitor.start(context);
        Log.i(TAG, "ANRMonitor init");
    }

    private void start(Context context) {
        Looper mainLooper = Looper.getMainLooper();
        mainLooper.setMessageLogging(printer);
        HandlerThread handlerThread = new HandlerThread(ANRMonitor.class.getSimpleName());
        handlerThread.start();
        //时间较长,则记录堆栈
        threadHandler = new Handler(handlerThread.getLooper());
    }

    private long lastFrameTime = 0L;
    private Handler threadHandler;
    private long mSampleInterval = 40;

    private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                //开始进行记录
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 20) {
                    //todo 这里超过20毫秒卡顿了
                    Log.i(TAG, "ANR:" + it + ", useTime:" + useTime);
                }
                threadHandler.removeCallbacks(mRunnable);
            }
        }
    };


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            threadHandler.postDelayed(mRunnable, mSampleInterval);
        }
    };
}

1.1.3 Conclusion:

In this way, we can get the execution time of each main thread task. According to the standard 16ms refresh, then each main thread task should not exceed 16ms, otherwise it means that the interface is stuck.

1.2 Second-generation solution: Choreographer registers rendering callback (representative framework: Tencent GT)

The defect of the first-generation solution is that the main thread is stuck, and this main thread lag does not necessarily cause user-perceived lag. For example, if the user stops on a certain page and does not operate, even if the main thread is blocked at this time, the user will not feel it. The interface drawing we want is stuck, and it should be biased towards the entire drawing process.

1.2.1 Principle introduction:

The entire process of View drawing is first notified from the child to the parent layer by layer, and the top layer is ViewRootImpl. After being notified to ViewRootImpl, it will create an interface drawing message and then register it with Choreographer. Choreographer will use the native mechanism to ensure that the callback is once every 16ms, and after the callback, the interface drawing process will be executed.

The core point is in the callback. The callback notifies the doFrame method to draw the interface. There are four callbacks in the doFrame, of which CALLBACK_TRAVERSAL is the real notification to execute the entire drawing process.

try {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
 
            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
 
            mFrameInfo.markAnimationsStart();
            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
 
            mFrameInfo.markPerformTraversalsStart();
            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);//这里是核心
 
            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
        } finally {
            AnimationUtils.unlockAnimationClock();
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }

So we can do a little trick for the other three, for example, we can register the callback of CALLBACK_ANIMATION. In this way, if the CALLBACK_ANIMATION callback is a 16ms timing callback notification, it can be proved that CALLBACK_ANIMATION is also a callback notification received at a refresh rate of 16ms.

Another article of mine has a more detailed introduction. If you are interested, you can read it:

Android source code learning - View drawing process - Sharing + Recording - CSDN Blog

1.2.2 How to use: We can implement a class of FPSFrameCallBacl, and then send

public class FPSFrameCallback implements Choreographer.FrameCallback {
   private static final String TAG = "FPS_TEST";
    private long mLastFrameTimeNanos = 0;
    private long mFrameIntervalNanos;
    public FPSFrameCallback(long lastFrameTimeNanos) {
        mLastFrameTimeNanos = lastFrameTimeNanos;
        mFrameIntervalNanos = (long)(1000000000 / 60.0);
    }
    @Override
    public void doFrame(long frameTimeNanos) {
        //初始化时间
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = frameTimeNanos;
        }
        final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if(skippedFrames>30){
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
        }
        mLastFrameTimeNanos=frameTimeNanos;
        //注册下一帧回调
        Choreographer.getInstance().postFrameCallback(this);
    }
}

Registration code:

 Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));

1.2.3 Conclusion:

The second-generation solution has well met our needs to evaluate only whether the interface drawing is smooth. But there are also some problems, such as the data is not intuitive, the use of animation callbacks may affect the animation drawing process and so on.

1.3 The third generation solution: Window registration rendering callback

The first two generations were researched by the developers themselves, and the third generation is an official Google product, so it is also the most authoritative solution with the most intuitive and complete data.

1.3.1 Introduction to the principle:

Our final rendering will be converted into one by one rendering task Renderer by ViewRootImpl, and register callbacks with the native layer to obtain the number of lost frames.

The specific details will be explained in a separate chapter later.

1.3.2 How to use:

getWindow().addOnFrameMetricsAvailableListener(new Window.OnFrameMetricsAvailableListener() {
            @Override
            public void onFrameMetricsAvailable(Window window, FrameMetrics frameMetrics, int dropCountSinceLastInvocation) {

            }
        });

1.3.3 Conclusion:

The third-generation solution has perfectly helped us determine whether it is stuck, but it cannot help us directly find the cause of the problem, so we need to use various means to investigate.

2. The means of checking the interface stuck

2.1 Reasons for Caton:

Caton is mainly divided into three categories in my opinion,

The first type is the CPU problem. For example, the CPU is overloaded due to the excessive amount of calculation, so that normal calculation cannot be performed.

The second type is the GC problem. When the virtual machine has frequent GC, the task of the main thread will be suspended.

The third category is the blocking of the main thread. For example, our main thread performs time-consuming operations, or adds inappropriate locks, or even has a complex layout or too many nesting levels.

2.2 Option 1:

For problem 1, we can use tools such as perfdog to see the CPU load rate.

2.3 Option 2:

In response to the second question, there are too many articles of this kind on the Internet, and the mainstream is this type, so I will not expand it here.

Readers can Baidu

2.4 Option three:

For problem 3, it is often the cause that really causes the interface drawing to freeze. So that's what we're going to focus on. The scheme I use here is the Looper callback scheme introduced in 1.1 above.

Now that we can pass the callback, we know the task execution time of each main thread. Then we start a new thread during this period of time, and continuously dump the stack status of the main thread to know where the main thread is blocked.

As an example, I am reading a file on the main thread and it takes 100ms. Then I capture the stack of the main thread every 20 milliseconds. The code stack of the main thread stack reading the file will be captured at least 5 times, then we can know that there is a problem with this code.

As another example, a RecyclerView loads hundreds of itemViews in one interface. When you first enter the interface, there is a high probability that it will be stuck. At this time, we will find that most of the code stacks print the onCreateViewHolder() method. The more frequent, the higher the probability of occurrence. We will know that it is caused by the frequent creation of ViewHolder.

Below is the tool code I wrote to simply troubleshoot the stuck problem:

public class ANRMonitor extends BaseMonitor {

    final static String TAG = "anr";

    public static void init(Context context) {
        //开关
        if (true){
            return;
        }
        ANRMonitor anrMonitor = new ANRMonitor();
        anrMonitor.start(context);
        Log.i(TAG, "ANRMonitor init");
    }

    private void start(Context context) {
        Looper mainLooper = Looper.getMainLooper();
        mainLooper.setMessageLogging(printer);
        HandlerThread handlerThread = new HandlerThread(ANRMonitor.class.getSimpleName());
        handlerThread.start();
        //时间较长,则记录堆栈
        threadHandler = new Handler(handlerThread.getLooper());
        mCurrentThread = Thread.currentThread();
    }

    private long lastFrameTime = 0L;
    private Handler threadHandler;
    private long mSampleInterval = 40;
    private Thread mCurrentThread;//主线程
    private final Map<String, String> mStackMap = new HashMap<>();

    private Printer printer = new Printer() {
        @Override
        public void println(String it) {
            long currentTimeMillis = System.currentTimeMillis();
            //其实这里应该是一一对应判断的,但是由于是运行主线程中,所以Dispatching之后一定是Finished,依次执行
            if (it.contains("Dispatching")) {
                lastFrameTime = currentTimeMillis;
                //开始进行记录
                threadHandler.postDelayed(mRunnable, mSampleInterval);
                synchronized (mStackMap) {
                    mStackMap.clear();
                }
                return;
            }
            if (it.contains("Finished")) {
                long useTime = currentTimeMillis - lastFrameTime;
                //记录时间
                if (useTime > 20) {
                    //todo 要判断哪里耗时操作导致的
                    Log.i(TAG, "ANR:" + it + ", useTime:" + useTime);
                    //大于100毫秒,则打印出来卡顿日志
                    if (useTime > 100) {
                        synchronized (mStackMap) {
                            Log.i(TAG, "mStackMap.size:" + mStackMap.size());
                            for (String key : mStackMap.keySet()) {
                                Log.i(TAG, "key:" + key + ",state:" + mStackMap.get(key));
                            }
                            mStackMap.clear();
                        }
                    }
                }
                threadHandler.removeCallbacks(mRunnable);
            }
        }
    };


    private Runnable mRunnable = new Runnable() {
        @Override
        public void run() {
            doSample();
            threadHandler
                    .postDelayed(mRunnable, mSampleInterval);
        }
    };

    protected void doSample() {
        StringBuilder stringBuilder = new StringBuilder();

        for (StackTraceElement stackTraceElement : mCurrentThread.getStackTrace()) {
            stringBuilder
                    .append(stackTraceElement.toString())
                    .append("\n");
        }
        synchronized (mStackMap) {
            mStackMap.put(mStackMap.size() + "", stringBuilder.toString());
        }
    }

}

3. Solve the actual case of interface stuck

3.1 Case 1. Solve high-frequency CPU computing through algorithm optimization

Requirements and problem scenarios:

The data on the page is continuously received through Bluetooth, combined with the interpretation queried in the local database, and finally spliced ​​into the final Model for rendering. It was found that the refresh rate did not reach the expected speed and did not follow suit.

Investigation process:

Through the tools in 2.4, it is found that the main thread is not blocked. But the sliding operation just feels a little stuck, which is not smooth. Through perdog, found high CPU usage. Therefore, I initially suspected that it was caused by complex operations, so I checked the code and found that there were two-layer for loops and a large amount of collected data.

solution:

1. Scheme 1, optimization algorithm

The following is the old code structure, input 1000 values, match the appropriate value from 10W pieces of data, and then take the first 100.

private List<String> show() {
        List<Model> list = new ArrayList<>();//长度10W
        List<String> input = new ArrayList<>();//长度1000
        
        List<String> showList = new ArrayList<>();
        for (String key : input) {
            for (Model model : list) {
                if (model.name.equals(key)) {
                    showList.add(model.value);
                }
            }
        }
        return showList.subList(0, 100);
    }

Then we first convert the longer list to map, and then jump out of the loop after taking 100. The computational efficiency is greatly improved.

The optimized code is as follows:

//优化后代码
    private List<String> show2() {
        List<Model> list = new ArrayList<>();//长度10W
        List<String> input = new ArrayList<>();//长度1000

        Map<String, Model> cache = new HashMap<>();
        for (Model model : list) {
            cache.put(model.name, model);
        }
        List<String> showList = new ArrayList<>();
        for (int i = 0; i < Math.min(input.size(), 100); i++) {
            Model model = cache.get(input.get(i));
            showList.add(model.value);
        }
        return showList.subList(0, 100);
    }

2. Option 2, switch to JNI to realize or try bit operation

If the code algorithm itself has no room for optimization, and there are many business operations, and the final output value is not much, you can consider switching to JNI for implementation or converting to bit operations to improve efficiency, and I will not give examples here.

3.2 Case 2. Main thread blocking problem

Requirements and problem scenarios:

Entering a page, I found that every time I entered, there would be a clear sense of stuttering.

Investigation process:

Through the tools in 2.4, it was found that there was a large number of code stacks in the log, so it was suspected that the problem was here. The final positioning is caused by the main thread IO operation.

solution:

1. Scheme 1, asynchronous loading

IO is a time-consuming operation, use a thread to read, and notify the main line to refresh the UI after the read is completed:

 //3.2案例 优化代码
        new Thread(() -> {
            String s = "";
            try {
                InputStream is = getAssets().open("content.txt");
                List<String> strings = IOHelper.readListStrByCode(is, "utf-8");
                s = strings.get(0);
            } catch (IOException e) {
                Log.i("lxltest", e.getMessage());
                e.printStackTrace();
            }
            String show = s;
            handler.post(() -> title.setText(show));
        }).start();

PS: In order to keep the code concise and intuitive, there is no need to use the thread pool, and so are the subsequent scenarios.

 full sample code link

android_all_demo/PerformanceCaseActivity.java at master · aa5279aa/android_all_demo · GitHub

3.3 Case 3. Solve the problem of high frequency refresh of RecyclerView

Requirements and problem scenarios:

The requirement is very simple, similar to looking at the stock price, requesting a service every 100 milliseconds, and then fetching the returned data and displaying it to the user.

The simple code is as follows:

    RecyclerView recyclerView;
    ModelAdapter adapter;
    boolean flag = true;
    Handler handler = new Handler();

    @RequiresApi(api = Build.VERSION_CODES.N)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_recycler);
        recyclerView = findViewById(R.id.recycler_view);
        new Thread(() -> {
            while (flag) {
                List<Map<String, String>> data = getResponse();
                notifyData(data);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private void notifyData(List<Map<String, String>> data) {
        handler.post(() -> {
            adapter.data = data;
            adapter.notifyDataSetChanged();
        });
    }

However, we found in actual operation that sliding up and down in this scenario is stuck.

Investigation process:

First, we used the tools provided in article 2.4 to scan and found that there were a lot of stacks as shown in the following figure:

So we know the cause of the problem, which is the freeze caused by the frequent creation of ViewHolder. So why create ViewHolder frequently? With this question in mind, we went deep into the source code of RcyclerView and finally knew the reason. Every time the notifyDataSetChanged() method is called, a recycling operation is triggered. Since the default number of caches in RecyclerBin is 5, and we display 15 pieces of data on one page, 10 of these 15 ItemViews are released.

solution:

1. Option 1: Change the size of the cache

Do the following, increase the number of caches, and the problem will be solved.

recyclerView.getRecycledViewPool().setMaxRecycledViews(0,15);

2. Option 2: Optimization Algorithm

Of course, we can also solve the problem through the data level, the data returned by the service, combined with the existing data calculation, calculate which data has changed, and only update the data that has changed.

full sample code link

android_all_demo/PerformanceCaseActivity.java at master · aa5279aa/android_all_demo · GitHub

3.4 Case 4. Complicated page enters Caton for the first time

Requirements and problem scenarios:

A page with a lot of elements and a complex interface, when we clicked to enter for the first time, we found that it would take 1-2 seconds to enter after clicking, which gave people the feeling of obviously not following.

Investigation process:

Or through the ANRMonitor tool, we analyze the log to see what caused it.

In the end, we found that in the log, the most time-consuming stack was printed to the setContentView method. Then it means that it is time-consuming to create the layout.

solution:

1. Option 1, preloading

In general, complex pages will not be splash pages. So we can use preloading to convert xml into View in advance before entering the complex page. When a complex page is onCreate, it is judged whether there is a corresponding View in the cache. If it exists, it will be used in the cache, and if it does not exist, it will be created.

Add cache:

 private var cacheMap = HashMap<String, View>()

    fun addCachePageView(pageClassName: String, layoutId: Int) {
        if (cacheMap[pageClassName] != null) {
            return
        }
        val context = DemoApplication.getInstance()
        val inflate = View.inflate(context, layoutId, null)
        inflate.measure(1, 1)
        cacheMap[pageClassName] = inflate
    }

Use cache:

View cachePageView = PageViewCache.Companion.getInstance().getCachePageView(PrepareMiddleActivity.class.getName());
        if (cachePageView != null) {
            setContentView(cachePageView);
        } else {
            setContentView(R.layout.prepare_middle_page);
        }

Full sample code link https://github.com/aa5279aa/android_all_demo/blob/master/DemoClient/app/src/main/java/com/xt/client/activitys/PrepareActivity.kt

3.5 Case 5. Resolve the refresh of high frequency waveforms

Requirements and problem scenarios:

The effect diagram is shown in the figure below, and the refresh frequency of each waveform diagram should reach more than 10 times per second.

At this time, we found that although we notify the refresh according to the method of 10 times per second, in fact, it can only be refreshed 2 to 3 times per second.

Investigation process:

Using the tools provided by 2.4, I found that the reason for the lag is that there are two major time consumptions:

1. Data coordinate calculation of each graph,

2. measure, layout and other processes.

solution:

1. Scheme 1, data and rendering decoupling

We can start a thread to perform coordinate calculation and put the calculated data in the cache.

In the main thread, the calculated data is obtained from the cache at regular intervals of 100 milliseconds and rendered directly. This way data is decoupled from rendering.

2. Transfer surfaceView to achieve

Since it is a custom View itself, it can also be implemented in SurfaceView to make full use of GPU performance.

full sample code link

This piece of code has not been declassified, so it is not yet open source. Some example codes are provided for reference:

public class DataSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
...省略代码
 /**
     * 绘制方波图和波形图
     */
    protected void drawArc(Canvas canvas) {
        //长度不对等或者没有初始化,则不绘制
         if (mShowPointList.length != mSettings.length || !isInit) {
            return;
        }
//        canvas.save();
        float startX = mMargin;
        float startY = mMargin - mOffset;
        RectF rectF;
        for (int index = 0; index < mShowPointList.length; index++) {
            List<Integer> integers = mShowPointList[index];
            ShowPointSetting setting = mSettings[index];
            int count = integers.size();
            if (setting.showType == ShowConstant.ShowTypeWave) {
                count--;
            }
            float itemWidth = itemCoordWidth / count;//每一个的宽度
            //绘制背景  mBgPaint
            rectF = new RectF(startX, startY, startX + itemViewWidth - mMargin * 2, startY + itemViewHeight - mMargin * 2);
            if (mIndex == index) {
                canvas.drawRoundRect(rectF, mItemRound, mItemRound, mBgClickPaint);
            } else {
                canvas.drawRoundRect(rectF, mItemRound, mItemRound, mBgPaint);
            }

            float itemX = startX + mItemPadding;
            float itemY = startY + mItemPadding;
            float nextY = 0;
            float[] pts = new float[integers.size() * 8 - 4];
            for (int innerIndex = 0; innerIndex < count; innerIndex++) {
                Integer value = integers.get(innerIndex);
                if (value != null) {
                    value = value > setting.showMaxValue ? setting.showMaxValue : value;
                    itemY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * value / setting.showMaxValue;
                    if (setting.showType == ShowConstant.ShowTypeSquare) {
                        pts[innerIndex * 8 + 0] = itemX;
                        pts[innerIndex * 8 + 1] = itemY;
                        pts[innerIndex * 8 + 2] = itemX + itemWidth;
                        pts[innerIndex * 8 + 3] = itemY;
                    }
                }
                itemX = itemX + itemWidth;
                //方形图逻辑
                if (setting.showType == ShowConstant.ShowTypeSquare) {
                    if (innerIndex != count - 1) {
                        Integer nextValue = integers.get(innerIndex + 1);
                        if (value != null && nextValue != null) {
                            nextValue = nextValue > setting.showMaxValue ? setting.showMaxValue : nextValue;
                            nextY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * nextValue / setting.showMaxValue;
                            pts[innerIndex * 8 + 4] = itemX;
                            pts[innerIndex * 8 + 5] = itemY;
                            pts[innerIndex * 8 + 6] = itemX;
                            pts[innerIndex * 8 + 7] = nextY;
                        }
                    } else {
                        //绘制坐标
                        canvas.drawText(String.valueOf(innerIndex + 2), itemX - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);
                    }
                } else if ((setting.showType == ShowConstant.ShowTypeWave)) {
                    if (value != null && integers.get(innerIndex + 1) != null) {
                        nextY = startY + mItemPadding + mItemTopHeight + itemCoordHeight - itemCoordHeight * integers.get(innerIndex + 1) / setting.showMaxValue;
                        pts[innerIndex * 8 + 4] = itemX - itemWidth;
                        pts[innerIndex * 8 + 5] = itemY;
                        pts[innerIndex * 8 + 6] = itemX;
                        pts[innerIndex * 8 + 7] = nextY;
                    }
                    if (innerIndex == count - 1) {
                        //绘制坐标
                        canvas.drawText(String.valueOf(innerIndex + 2), itemX - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);
                    }
                }
                //绘制坐标
                canvas.drawText(String.valueOf(innerIndex + 1), itemX - itemWidth - 5, startY + mItemPadding + mItemTopHeight + itemCoordHeight + 20, mFontPaint);

//                mWaveShadowPaint.set
                //渐变色
//                canvas.drawRect(itemX - itemWidth, itemY, itemX, startY + mItemPadding + mItemTopHeight + itemCoordHeight, mWaveShadowPaint);

                //绘制虚线
                canvas.drawLine(itemX, startY + mItemPadding + mItemTopHeight, itemX, startY + mItemPadding + mItemTopHeight + itemCoordHeight, mEffectPaint);
            }
            //绘制最大值
            if (!StringUtil.emptyOrNull(setting.showMaxValueShow)) {
                canvas.drawText(setting.showMaxValueShow, startX + mItemPadding + 5, startY + mItemPadding + mItemTopHeight + 30, mFontPaint);//ok
            }

            //todo 绘制描述
            canvas.drawText(setting.showDesc, startX + mItemPadding, startY + mItemPadding + 30, mDescPaint);

            //todo 描述当前值
            String currentStr = String.valueOf(mCurrents[index]);
            float v = mCurrentPaint.measureText(currentStr);
            canvas.drawText(currentStr, startX + mItemPadding + itemCoordWidth - v, startY + mItemPadding + 30, mCurrentPaint);

            //绘制方形图的线
            canvas.drawLines(pts, mWavePaint);

            if (index % mAttr.widthCount == (mAttr.widthCount - 1)) {
                startX = mMargin;
                startY += itemViewHeight;
            } else {
                startX += itemViewWidth;
            }
        }
//        canvas.restore();
    }
}

3.6 Case 6. Page optimization for loading hundreds of pieces of data on one screen

Requirements and problem scenarios:

In some scenarios, we need to load a large amount of data on a page for users to see. As shown in the figure below, a large amount of data is displayed on one screen. It turned out to be implemented using RecyclerView. We found that every time you enter this page, it takes 2-3 seconds to enter. As shown below:

Investigation process:

It is also checked with the detection tool. We found that most of the time-consuming stacks are displayed by the method onCreateViewHolder. With hundreds of itemViews per square meter, hundreds of ItemViews need to be created, which is naturally a time-consuming operation. Of course, it is not only time-consuming to create an itemView, hundreds of itemViews need to execute methods such as measure, layout, and draw, and the time-consuming is also quite huge.

solution:

Option 1, Custom View

Creating hundreds of Views and rendering hundreds of them must be time-consuming. Can we just create one, render one, and display all the data. Of course, the solution is to customize the View.

In the custom View, we can calculate the position of each Item data separately, which is directly drawn with canvas. In this way, only one layout needs to be created, and measure/layout/draw each executes only once. In practice, the efficiency has been greatly improved.

Link to full sample code:

The specific custom View code will not be posted. You can refer to one of my open source projects, which will have more detailed function implementations.

GitHub - September26/ExcelView: An android project, which is implemented imitating the functions of excel in WPS, and further functional expansion.

3.7 Case 7. Optimization of complex long-screen pages

Requirements and problem scenarios:

A page with complex content, first of all, the outer layer is RecyclerView, which contains several modules. In each module, RecyclerView contains several final controls.

First of all, the waterfall layout is used, so the final control changes will affect the layout of the overall module.

In the end, we found that when the data changed frequently and refreshed frequently, the page was not smooth, and there was a clear sense of lag. 

Investigation process:

Similarly, when we used the detection tool to detect, we found that most of the stacks were printed into the notify process. So we can simply infer that the lag is caused by calling too many notifyChangeds.

solution:

So how to reduce the number of notifyChanged? Technically, there seems to be nothing to optimize, unless all are implemented with custom views, but then the development cost is too high.

Option 1, only notify when data changes

We can compare the old and new data to know the data that has changed, and only notify and refresh these data.

Option 2, level-by-level refresh

The two-level RecyclerView, if possible, should naturally be refreshed to achieve the smallest granularity. We can divide data changes into two types. One is to cause the height of the module to change, and the other is to affect only its own line.

For the first one, I can call the refresh notification using the adapter corresponding to the outer RecyclerView.

mRecyclerViewAdapt.data[positionMajor].items.clear()
mRecyclerViewAdapt.data[positionMajor].items.addAll(arrayList)
mRecyclerViewAdapt.notifyItemChanged(positionMajor)

Second, we only need to get the adapter notification refresh corresponding to the inner RecyclerView.

val recyclerView: RecyclerView = mRecyclerViewAdapt.getViewByPosition(positionMajor, R.id.recycler_view) as RecyclerView
val adapter = recyclerView.getTag(R.id.tag_adapter)  //as GasWidgetDashboard.MultipleItemQuickAdapter
when (adapter) {
        is GasWidgetDashboard.MultipleItemQuickAdapter -> adapter.notifyItemChanged(positionMinor)
        is GasWidgetForm.QuickAdapter                  -> adapter.notifyItemChanged(positionMinor)
}

4. Summary

4.1. Find the cause first and then solve it

There are various interface performance optimizations, but the most important thing is to find the cause of the problem. Then discuss how to solve the performance problem according to the reason. Blindly copying various optimization modes will not work. For example, it is obviously a main thread IO operation that causes the freeze, but blindly optimizing the memory, it is naturally impossible to solve the problem.

4.2. Knowledge points required for interface performance optimization

If you want to solve the interface performance problem perfectly, you still have to have a certain knowledge reserve. With this knowledge reserve, you can quickly help us troubleshoot the problem and get along with a reasonable solution.

4.2.1 Understand the Handler mechanism

This can help us to troubleshoot the main thread stuck.

4.2.2 The entire drawing process of View also needs to be clear

From the notification of changing data, to the measurement of each child View, etc.

4.3.3 Commonly used ViewGroup-level container implementation principles should be mastered

This type of container such as RecyclerView, RelativeLayout, ConstraintLayout and so on.

4.3.4 Finally, some abstract and logical thinking skills are also required.

The custom Views exemplified above all require a certain abstraction ability and logical thinking ability to know how to implement them.

The above are my personal suggestions, if you are interested, you can prepare accordingly.

5. Remarks

5.1 Declaration

Since the optimized solution in this article is based on the company's existing projects, in order to avoid the leakage of sensitive content, the relevant code and legends are demonstrated by demo, which leads to some ugly interfaces. Please understand.

5.2 This article involves the project address

https://github.com/sollyu/3a5fcd5eeeb90696

https://github.com/aa5279aa/android_all_demo

https://github.com/aa5279aa/CommonLibs

5.3 Links to references cited in this article:

Android source code learning-Handler mechanism and its six core points - Sharing + Recording - CSDN Blog

5.4 Acknowledgements

grateful

@sollyu (sollyu) · GitHub

Support for the creation of this article.

Author GitHub: @  https://github.com/aa5279aa/

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324200354&siteId=291194637