渲染刷新机制
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所用时间超过一般值,或者一大堆一起执行会耗费庞大的帧象时间,这是很麻烦的事情。
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