五、Android性能优化之UI卡顿分析之内存抖动和计算性能优化

渲染刷新机制

VSYNC(垂直刷新/绘制)

60HZ是屏幕刷新理想的频率。60fps---一秒内绘制的帧数。
24帧/秒 电源胶卷时代

在60fps内,系统会得到发送的VSYNC(垂直刷新)信号qu去进行渲染,就会正常地绘制。
60fps要求:每一帧只能停留16ms.

VSYNC:有两个概念
1)Refresh Rate:屏幕在一秒时间内刷新屏幕的次数----有硬件的参数决定,比如60HZ.
2)Frame Rate:GPU在一秒内绘制操作的帧数,比如:60fps。

GPU刷新:GPU帮助我们将UI组件等计算成纹理Texture和三维图形Polygons
同时会使用OpenGL---会将纹理和Polygons缓存在GPU内存里面。

GPU会获取图形数据进行渲染,然后硬件负责把渲染后的内容呈现到屏幕上,他们两者不停的进行协作。

正常显示

不幸的是,刷新频率和帧率并不是总能够保持相同的节奏。如果发生帧率与刷新频率不一致的情况,就会容易出现Tearing的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。

卡顿掉帧

理解图像渲染里面的双重与三重缓存机制,这个概念比较复杂,请移步查看这里:source.android.com/devices/gra…
还有这里article.yeeyan.org/view/37503/…
通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。

卡顿掉帧

在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。

UI卡顿分析
UI卡顿的根本原因

Android每个16ms就会绘制一次Activity,通过上述的结论我们知道,如果由于一些原因导致了我们的逻辑、CPU耗时、GPU耗时大于16ms,UI就无法完成一次绘制,那么就会造成卡顿。简单的一句话就是:卡主线程了。

比如说,在16ms内,发生了频繁的GC:

扫描二维码关注公众号,回复: 13307479 查看本文章
内存不足时GC处理

当这些GC所用时间超过一般值,或者一大堆一起执行会耗费庞大的帧象时间,这是很麻烦的事情。

绘图过程中GC回收 GC回收时间过长导致卡顿 GC回收时间过长导致卡顿
1.外部引起的

比如:Activity里面直接进行网络访问/大文件的IO操作

外部因素之--内存抖动的问题引起卡顿分析

为了模拟UI卡顿,我们利用了WebView加载一张GIF图片:


        WebView webView = (WebView) findViewById(R.id.webview);
        webView.getSettings().setUseWideViewPort(true);
        webView.getSettings().setLoadWithOverviewMode(true);
        webView.loadUrl("file:///android_asset/shiver_me_timbers.gif");
复制代码
然后在GIF在动的时候,执行我们的业务代码,通过GIF的卡顿情况来模拟UI卡顿。

为了模拟内存抖动,我们在GIF动的时候,在主线程执行一下代码:


  /**
     * 排序后打印二维数组,一行行打印
     */
    public void imPrettySureSortingIsFree() {
        int dimension = 300;
        int[][] lotsOfInts = new int[dimension][dimension];
        Random randomGenerator = new Random();
        for(int i = 0; i < lotsOfInts.length; i++) {
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                lotsOfInts[i][j] = randomGenerator.nextInt();
            }
        }

        for(int i = 0; i < lotsOfInts.length; i++) {
            String rowAsStr = "";
            //排序
            int[] sorted = getSorted(lotsOfInts[i]);
            //拼接打印
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                rowAsStr += sorted[j];
                if(j < (lotsOfInts[i].length - 1)){
                    rowAsStr += ", ";
                }
            }
            Log.i("ricky", "Row " + i + ": " + rowAsStr);
        }


   public int[] getSorted(int[] input){
        int[] clone = input.clone();
        Arrays.sort(clone);
        return clone;
    }
复制代码

这段代码主要是模拟大量的堆内存分配与释放String对象,频繁触发GC,导致UI卡顿。通过Memory Monitor可以看出:

程序抖动

内存方面是发生了抖动,但是CPU的占用几乎不动。

为了分析内存的情况,我们结合之前的文章,使用一些工具来分析,因为实际情况是,我们不知道哪里的代码导致UI卡顿。

首先我们使用Android Studio自带的Allocation Tracking工具来跟踪内存分配情况。我们在UI卡顿的过程中收集内存分配的信息如下:

内存分配跟踪 内存分配跟踪

Total allocation:13099(内存分配次数:13039次)
Total size:208.62k (内存分配大小:208.62k)

我们看到MemoryChurnActivity.java 这个类所占内存资源为19.9%,这是很大的,也是很不正常的。
解决办法

解决办法,这个Demo中,为了解决GC频繁的问题,我们可以利用StringBudiler代替String:


  /**
     * 排序后打印二维数组,一行行打印
     */
    public void imPrettySureSortingIsFree() {
        int dimension = 300;
        int[][] lotsOfInts = new int[dimension][dimension];
        Random randomGenerator = new Random();
        for(int i = 0; i < lotsOfInts.length; i++) {
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                lotsOfInts[i][j] = randomGenerator.nextInt();
            }
        }


       //优化以后
        StringBuilder sb = new StringBuilder();
        String rowAsStr = "";
        for(int i = 0; i < lotsOfInts.length; i++) {
            //清除上一行
          sb.delete(0,rowAsStr.length());
            //排序
            int[] sorted = getSorted(lotsOfInts[i]);
            //拼接打印
            for (int j = 0; j < lotsOfInts[i].length; j++) {
                rowAsStr += sorted[j];
                sb.append(sorted[j]);
                if(j < (lotsOfInts[i].length - 1)){
                    sb.append(", ");
                }
            }
            rowAsStr = sb.toString();
            Log.e("main", "Row " + i + ": " + rowAsStr);
        }

    }

    public int[] getSorted(int[] input){
        int[] clone = input.clone();
        Arrays.sort(clone);
        return clone;
    }
复制代码
优化之后的内存使用情况
我们发现内存抖动现象大幅减弱
注意,GC是无法避免的,我们要避免的是频繁的GC,因此这里的优化实质上是内存优化。
使用Android Device Monitor工具分析
点击Tools选择Android Device Monitor
外部因素之--方法耗时(CPU占用)的问题引起卡顿分析

同理,我们利用斐波那契数列来模拟,我们计算到第40个:


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_caching_exercise);

        Button theButtonThatDoesFibonacciStuff = (Button) findViewById(R.id.caching_do_fib_stuff);
        theButtonThatDoesFibonacciStuff.setText("计算斐波那契数列");

        theButtonThatDoesFibonacciStuff.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.i(LOG_TAG, String.valueOf(computeFibonacci(40)));
            }
        });
        WebView webView = (WebView) findViewById(R.id.webview);
        webView.getSettings().setUseWideViewPort(true);
        webView.getSettings().setLoadWithOverviewMode(true);
        webView.loadUrl("file:///android_asset/shiver_me_timbers.gif");
    }

    public int computeFibonacci(int positionInFibSequence) {
        //0 1 1 2 3 5 8
        if (positionInFibSequence <= 2) {
            return 1;
        } else {
            return computeFibonacci(positionInFibSequence - 1)
                    + computeFibonacci(positionInFibSequence - 2);
        }
    }
复制代码

点击Button,我们粗略地通过Monitor进行分析:

可以看到CPU的占用突然提高了,但是内存的使用几乎不动。

我们也可以通过TraceView来进行分析方法的耗时:

可以看到,黑乎乎一篇的就是一些耗时的“重灾区”。我们点击放大重灾区:

这里可以看到调用了我自己Activity方法。

往往实际情况比较复杂,我们如果要知道是哪个类的问题,一般需要不断追溯父方法,也就是找到谁调用了这个方法,最终可以分析出是哪个类有问题。

如果我们要看哪个方法耗时,可以根据右边的一些参数来进行分析。其中,Incl的意思是该方法包括其所调用的其他方法的时间,Excl的意思是不包含其所调用的其他方法的时间(纯粹是本身调用的时间)。Recursive是递归调用的意思。CPU Time就是占用CPU的时间,Real Time的意思就是实际时间,包括内存分配、回收等其他的时间,Real Time比CPU Time大。后面还是一些平均调用时间,一个方法可能本身耗时很少,但是可能会被频繁(递归)调用,这时候就需要分析平均调用时间。

分析耗时的时候,我们要不断追溯子方法的耗时情况:

一路跟踪下来,发现到了第11层以后,耗时百分比就变成0.2%了,那么我们可以暂时确定耗时的根源就是第10层的相关方法。如果你发现,Incl百分比很大,但是该方法本身的Excl百分比很小,那么改方法就不是耗时的根源,如下图所示,读者可以自行分析:

最终我们确定是我们自己的Activity的斐波那契计算的那个方法的耗时导致UI卡顿的。

Profile Panel是Traceview的核心界面,其内涵非常丰富。它主要展示了某个线程(先在Timeline Panel中选择线程)中各个函数调用的情况,包括CPU使用时间、调用次数等信息。而这些信息正是查找hotspot的关键依据。所以,对开发者而言,一定要了解Profile Panel中各列的含义。笔者总结了其中几个重要列的作用,如表1-1所示:
表1-1 Profile Panel各列作用说明
列名
描述

另外,每一个Time列还对应有一个用时间百分比来统计的列(如Incl Cpu Time列对应还有一个列名为Incl Cpu Time %的列,表示以时间百分比来统计的Incl Cpu Time)。

列名 描述
Name 该线程运行过程中所调用的函数名
Incl Cpu Time 某函数占用的CPU时间,包含内部调用其它函数的CPU时间
Excl Cpu Time 某函数占用的CPU时间,但不含内部调用其它函数所占用的CPU时间
Incl Real Time 某函数运行的真实时间(以毫秒为单位),内含调用其它函数所占用的真实时间
Excl Real Time 某函数运行的真实时间(以毫秒为单位),不含调用其它函数所占用的真实时间
Call+Recur Calls/Total 某函数被调用次数以及递归调用占总调用次数的百分比
Cpu Time/Call 某函数调用CPU时间与调用次数的比。相当于该函数平均执行时间
Real Time/Call 同CPU Time/Call类似,只不过统计单位换成了真实时间

另外,每一个Time列还对应有一个用时间百分比来统计的列(如Incl Cpu Time列对应还有一个列名为Incl Cpu Time %的列,表示以时间百分比来统计的Incl Cpu Time)。

解决办法

修改方法(算法),使得方法不耗时。
放到子线程中,例如网络访问、大文件操作等,防止ANR。

例如上述例子中,我们可以使用循环代( caching缓存+批处理思想)替递归实现斐波那契数列的计算:

//优化后的斐波那契数列的非递归算法 caching缓存+批处理思想
public int computeFibonacci(int positionInFibSequence) {
    int prev = 0;
    int current = 1;
    int newValue;
    for (int i=1; i<positionInFibSequence; i++) {
        newValue = current + prev;
        prev = current;
        current = newValue;
    }
    return current;
}
复制代码
发现优化后的斐波那契数列的CPU占用情况,没有出现异常抖动现象。

猜你喜欢

转载自juejin.im/post/7033651770274349069