美文网首页Android性能优化AndroidAndroid开发
五、Android性能优化之UI卡顿分析之内存抖动和计算性能优化

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

作者: 香沙小熊 | 来源:发表于2017-09-01 10:24 被阅读882次
    渲染刷新机制

    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的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。

    卡顿掉帧

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

    卡顿掉帧

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

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

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

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

    内存不足时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");
    
    测试用的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
    Android Device Monitor工具 得到没优化之前内存抖动 trace数据
    外部因素之--方法耗时(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使用情况跟踪

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

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

    TraceView分析1..png

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

    TraceView分析2.png

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

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

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

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

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

    TraceView分析3.png

    最终我们确定是我们自己的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使用情况
    发现优化后的斐波那契数列的CPU占用情况,没有出现异常抖动现象。

    特别感谢:
    小楠总
    动脑学院Ricky
    Innost的专栏

    相关文章

      网友评论

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

      本文链接:https://www.haomeiwen.com/subject/pqjqjxtx.html