#####渲染刷新机制 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的现象(画面上下两部分显示内容发生断裂,来自不同的两帧数据发生重叠)。理解图像渲染里面的双重与三重缓存机制,这个概念比较复杂,请移步查看这里:, 还有这里。 通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住,这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。
在这种情况下,某些帧显示的画面内容就会与上一帧的画面相同。糟糕的事情是,帧率从超过60fps突然掉到60fps以下,这样就会发生LAG,JANK,HITCHING等卡顿掉帧的不顺滑的情况。这也是用户感受不好的原因所在。
#####UI卡顿分析
#####UI卡顿的根本原因
Android每个16ms就会绘制一次Activity,通过上述的结论我们知道,如果由于一些原因导致了我们的逻辑、CPU耗时、GPU耗时大于16ms,UI就无法完成一次绘制,那么就会造成卡顿。简单的一句话就是:卡主线程了。
比如说,在16ms内,发生了频繁的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
######发现优化后的斐波那契数列的CPU占用情况,没有出现异常抖动现象。
特别感谢: