Android渲染机制和丢帧分析

http://blog.csdn.net/bd_zengxinxin/article/details/52525781

自己编写App的时候,有时会感觉界面卡顿,尤其是自定义View的时候,大多数是因为布局的层次过多,存在不必要的绘制, 或者onDraw等方法中过于耗时。那么究竟需要多快,才能给用户一个流畅的体验呢?那么就需要简单了解下Android的渲染机制:

Android系统每隔16ms发出VSYNC信号,触发对UI进行渲染,那么整个过程如果保证在16ms以内就能达到一个流畅的画面。 那么如果操作超过了16ms就会发生下面的情况:

如果系统发生的VSYNC信号,而此时无法进行渲染,还在做别的操作,那么就会导致丢帧的现象, (大家在察觉到APP卡顿的时候,可以看看logcat控制台,会有drop frames类似的警告)。这样的话,绘制就会在下一个16ms的时候才进行绘制, 即使只丢一帧,用户也会发现卡顿的。

那为什么是16ms,16ms意味着着1000/60hz,相当于60fps,那么只要解释为什么是60fps。

这是因为人眼与大脑之间的协作无法感知超过60fps的画面更新。12fps大概类似手动快速翻动书籍的帧率, 这明显是可以感知到不够顺滑的。24fps使得人眼感知的是连续线性的运动,这其实是归功于运动模糊的 效果。 24fps是电影胶圈通常使用的帧率,因为这个帧率已经足够支撑大部分电影画面需要表达的内容,同时能够最大的减少费用支出。 但是低于30fps是 无法顺畅表现绚丽的画面内容的,此时就需要用到60fps来达到想要的效果,当然超过60fps是没有必要的。

有了对Android渲染机制基本的认识以后,那么我们的卡顿的原因就在于没有办法在16ms内完成该完成的操作, 而主要因素是在于没有必要的layouts、invalidations、Overdraw。渲染的过程是由CPU与GPU协作完成, 下面一张图很好的展示出了CPU和GPU的工作,以及潜在的问题,检测的工具和解决方案。

我们需要知道: 1.通过Hierarchy Viewer去检测渲染效率,去除不必要的嵌套 2.通过Show GPU Overdraw去检测Overdraw,最终可以通过移除不必要的背景以及使用canvas.clipRect解决大多数问题。

Overdraw

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次的UI结构里面, 如果不可见的UI也在做绘制的操作,这就会导致某些像素区域被绘制了多次。这就浪费大量的CPU以及GPU资源。

当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用越来越多的层叠组件来实现这种视觉效果的怪圈。这很容易导致大量的性能问题, 为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。

我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,可以观察UI上的Overdraw情况。

蓝色,淡绿,淡红,深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

Overdraw有时候是因为你的UI布局存在大量重叠的部分,还有的时候是因为非必须的重叠背景。例如某个Activity有一个背景, 然后里面 的Layout又有自己的背景,同时子View又分别有自己的背景。仅仅是通过移除非必须的背景图片,这就能够减少大量的红色Overdraw区域, 增加 蓝色区域的占比。这一措施能够显著提升程序性能。

Overdraw 的处理方案一:移除不必要的background

下面看一个简单的展示ListView的例子: activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:paddingLeft="@dimen/activity_horizontal_margin"
              android:paddingRight="@dimen/activity_horizontal_margin"
              android:background="@android:color/white"
              android:paddingTop="@dimen/activity_vertical_margin"
              android:paddingBottom="@dimen/activity_vertical_margin"
              android:orientation="vertical"
    >
    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/narrow_space"
        android:textSize="@dimen/large_text_size"
        android:layout_marginBottom="@dimen/wide_space"
        android:text="@string/header_text"/>
    <ListView
        android:id="@+id/id_listview_chats"
        android:layout_width="match_parent"
        android:background="@android:color/white"
        android:layout_height="wrap_content"
        android:divider="@android:color/transparent"
        android:dividerHeight="@dimen/divider_height"/>
</LinearLayout>
   

item的布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/chat_padding_bottom">
    <ImageView
        android:id="@+id/id_chat_icon"
        android:layout_width="@dimen/avatar_dimen"
        android:layout_height="@dimen/avatar_dimen"
        android:src="@drawable/joanna"
        android:layout_margin="@dimen/avatar_layout_margin" />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/darker_gray"
        android:orientation="vertical">
        <RelativeLayout
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="@android:color/white"
            android:textColor="#78A"
            android:orientation="horizontal">
            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentLeft="true"
                android:padding="@dimen/narrow_space"
                android:text="@string/hello_world"
                android:gravity="bottom"
                android:id="@+id/id_chat_name" />
            <TextView xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:textStyle="italic"
                android:text="@string/hello_world"
                android:padding="@dimen/narrow_space"
                android:id="@+id/id_chat_date" />
        </RelativeLayout>
        <TextView xmlns:android="http://schemas.android.com/apk/res/android"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:padding="@dimen/narrow_space"
            android:background="@android:color/white"
            android:text="@string/hello_world"
            android:id="@+id/id_chat_msg" />
    </LinearLayout>
</LinearLayout>
   

Activity的代码

public class OverDrawActivity01 extends AppCompatActivity
{
    private ListView mListView;
    private LayoutInflater mInflater ;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_overdraw_01);
        mInflater = LayoutInflater.from(this);
        mListView = (ListView) findViewById(R.id.id_listview_chats);
        mListView.setAdapter(new ArrayAdapter<Droid>(this, -1, Droid.generateDatas())
        {
            @Override
            public View getView(int position, View convertView, ViewGroup parent)
            {
                ViewHolder holder = null ;
                if(convertView == null)
                {
                    convertView = mInflater.inflate(R.layout.chat_item,parent,false);
                    holder = new ViewHolder();
                    holder.icon = (ImageView) convertView.findViewById(R.id.id_chat_icon);
                    holder.name = (TextView) convertView.findViewById(R.id.id_chat_name);
                    holder.date = (TextView) convertView.findViewById(R.id.id_chat_date);
                    holder.msg = (TextView) convertView.findViewById(R.id.id_chat_msg);
                    convertView.setTag(holder);
                }else
                {
                    holder = (ViewHolder) convertView.getTag();
                }
                Droid droid = getItem(position);
                holder.icon.setBackgroundColor(0x44ff0000);
                holder.icon.setImageResource(droid.imageId);
                holder.date.setText(droid.date);
                holder.msg.setText(droid.msg);
                holder.name.setText(droid.name);
                return convertView;
            }
            class ViewHolder
            {
                ImageView icon;
                TextView name;
                TextView date;
                TextView msg;
            }
        });
    }
}
    

现在的效果是:

注意,我们的需求是整体是Activity是个白色的背景。

开启Show GPU Overdraw以后:

对比上面的参照图,可以发现一个简单的ListView展示Item,竟然很多地方被过度绘制了4X 。 那么,其实主要原因是由于该布局文件中存在很多不必要的背景,仔细看上述的布局文件,那么开始移除吧。

  • 不必要的Background 1

我们主布局的文件已经是background为white了,那么可以移除ListView的白色背景

  • 不必要的Background 2

Item布局中的LinearLayout的android:background=”@android:color/darker_gray”

  • 不必要的Background 3

Item布局中的RelativeLayout的android:background=”@android:color/white”

  • 不必要的Background 4

Item布局中id为id_msg的TextView的android:background=”@android:color/white”

这四个不必要的背景也比较好找,那么移除后的效果是:

对比之前的是不是好多了,接下来还存在一些不必要的背景.

  • 不必要的Background 5

这个背景比较难发现,主要需要看Adapter的getView的代码,上述代码你会发现,首先为每个icon设置了背景色 (主要是当没有icon图的时候去显示),然后又设置了一个头像。那么就造成了overdraw,有头像的完全没必要去绘制背景,所有修改代码:

Droid droid = getItem(position);
                if(droid.imageId ==-1)
                {
                    holder.icon.setBackgroundColor(0x4400ff00);
                    holder.icon.setImageResource(android.R.color.transparent);
                }else
                {
                    holder.icon.setImageResource(droid.imageId);
                    holder.icon.setBackgroundResource(android.R.color.transparent);
                }
    

ok,还有最后一个,这个也是非常容易被忽略的。

  • 不必要的Background 6

记得我们之前说,我们的这个Activity要求背景色是白色,我们的确在layout中去设置了背景色白色,那么这里注意下, 我们的Activity的布局最终会添加在DecorView中,这个View会中的背景是不是就没有必要了, 所以我们希望调用mDecor.setWindowBackground(drawable);,那么可以在Activity调用getWindow().setBackgroundDrawable(null);

setContentView(R.layout.activity_overdraw_01);
        getWindow().setBackgroundDrawable(null);
    

ok,一个简单的listview显示item,我们一共找出了6个不必要的背景,现在再看最后的Show GPU Overdraw 与最初的比较。

对比参照图,基本已经达到了最优的状态。

Overdraw 的处理方案二:clipRect的妙用

我们在自定义View的时候,经常会由于疏忽造成很多不必要的绘制,比如大家看下面这样的图:

多张卡片叠加,那么如果你是一张一张卡片从左到右的绘制,效果肯定没问题,但是叠加的区域肯定是过度绘制了。 并且material design对于界面设计的新的风格更容易造成上述的问题。那么有什么好的方法去改善呢? 答案是有的,android的Canvas对象给我们提供了很便利的方法clipRect就可以很好的去解决这类问题。

下面通过一个实例来展示,那么首先看一个效果图:

左边显示的时效果图,右边显示的是开启Show Override GPU之后的效果,可以看到,卡片叠加处明显的过度渲染了。

上面已经说了使用cliprect方法,那么我们目标直指自定义View的onDraw方法。修改后的代码:

 @Override
    protected void onDraw(Canvas canvas)
    {
        super.onDraw(canvas);
        canvas.save();
        canvas.translate(20, 120);
        for (int i = 0; i < mCards.length; i++)
        {
            canvas.translate(120, 0);
            canvas.save();
            if (i < mCards.length - 1)
            {
                canvas.clipRect(0, 0, 120, mCards[i].getHeight());
            }
            canvas.drawBitmap(mCards[i], 0, 0, null);
            canvas.restore();
        }
        canvas.restore();
    }
    

分析得出,除了最后一张需要完整的绘制,其他的都只需要绘制部分;所以我们在循环的时候,给i到n-1都添加了clipRect的代码。

最后的效果图:

可以看到,所有卡片变为了淡紫色,对比参照图,都是1X过度绘制.

如果你按照上面的修改,会发现最终效果图不是淡紫色,而是青色(2X),那是为什么呢?因为你还忽略了一个优化的地方, 本View已经有了不透明的背景,完全可以移除Window的背景了,即在Activity中,添加getWindow().setBackgroundDrawable(null);代码。

VSYNC(Vertical Synchronization 垂直同步)

为了理解App是如何进行渲染的,我们必须了解手机硬件是如何工作,那么就必须理解什么是VSYNC。

在讲解VSYNC之前,我们需要了解两个相关的概念:

1.Refresh Rate:代表了屏幕在一秒内刷新屏幕的次数,这取决于硬件的固定参数,例如60Hz。 2.Frame Rate:代表了GPU在一秒内绘制操作的帧数,例如30fps,60fps。

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

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

通常来说,帧率超过刷新频率只是一种理想的状况,在超过60fps的情况下,GPU所产生的帧数据会因为等待VSYNC的刷新信息而被Hold住, 这样能够保持每次刷新都有实际的新的数据可以显示。但是我们遇到更多的情况是帧率小于刷新频率。

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

时间: 2024-08-01 22:44:12

Android渲染机制和丢帧分析的相关文章

Android消息处理机制(源码分析)

前言 虽然一直在做应用层开发,但是我们组是核心系统BSP,了解底层了解Android的运行机制还是很有必要的.就应用程序而言,Android系统中的Java应用程序和其他系统上相同,都是靠消息驱动来工作的,它们大致的工作原理如下: 1. 有一个消息队列,可以往这个消息队列中投递消息. 2. 有一个消息循环,不断从消息队列中取出消息,然后处理 . 为了更深入的理解Android的消息处理机制,这几天空闲时间,我结合<深入理解Android系统>看了Handler.Looper.Message这几

深入理解Android渲染机制

基础知识 CPU: 中央处理器,它集成了运算,缓冲,控制等单元,包括绘图功能.CPU将对象处理为多维图形,纹理(Bitmaps.Drawables等都是一起打包到统一的纹理). GPU:一个类似于CPU的专门用来处理Graphics的处理器, 作用用来帮助加快格栅化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制. OpenGL ES:是手持嵌入式设备的3DAPI,跨平台的.功能完善的2D和3D图形应用程序接口API,有一套固定渲染管线流程. OpenGL ES详解 D

深入Android渲染机制

1.知识储备 CPU: 中央处理器,它集成了运算,缓冲,控制等单元,包括绘图功能.CPU将对象处理为多维图形,纹理(Bitmaps.Drawables等都是一起打包到统一的纹理). GPU:一个类似于CPU的专门用来处理Graphics的处理器, 作用用来帮助加快格栅化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制. OpenGL ES是手持嵌入式设备的3DAPI,跨平台的.功能完善的2D和3D图形应用程序接口API,有一套固定渲染管线流程. 附相关OpenGL渲染流

Android Binder机制(1):Binder架构分析

从这篇博客开始,将进入Binder机制的分析系列,顺序是先讲解Binder机制的框架,理解了整体思想后,再深入分析各层的细节实现,最后会实现一个自己的本地服务. 1.Binder的历史 BeOS是Be公司在1991年开发的运行在BeBOX硬件上的一款操作系统,与同期的其他操作系统不同,它是一款基于GUI设计的操作系统. George Hoffman(在LinkedIn上可以找到这哥们,简直是活着的传奇)当时任Be公司的工程师,他启动了一个名为OpenBinder的项目,该项目的宗旨是研究一个高效

Android Binder机制(3) 本地服务注册过程

本博客将讲解本地服务的注册过程,为了方便大家更好地理解,选择了MediaPlayer Service作为例子. 启动并注册MediaPlayer Service的代码在frameworks/base/media/mediaserver/main_mediaserver.cpp中,如下: main_mediaserver.cpp 1 2 3 4 5 6 7 8 9 10 11 12 int main(int argc, char** argv) { sp<ProcessState>proc(Pr

【前端优化之渲染优化】大屏android手机动画丢帧的背后

前言 上周我与阿里的宇果有一次技术的交流,然后对天猫H5站点做了一些浅层次的分析,后面点时间基本天天都会有联系,中途聊了一些技术细节.聊了双方团队在干什么,最后聊到了前端优化.因为我本身参与了几次携程H5站点的优化,在这方面有一些心得,但是与宇果交流的过程中发现我们在优化的时候忽略了一些细节. 携程做优化的时候整个重心基本放到了尺寸的缩减,和宇果的交流过程中他提出了渲染优化,其实渲染优化无非是减少回流,对于减少回流我们也有一些概念,我一直认为这个事情应该业务开发关注而不是框架关注(事实上框架也无

Android Binder机制分析(5) Binder_ioctl()分析

引言 在博客Android Binder机制(3)本地服务注册过程这篇博客中我们详细讲解了本地服务的注册过程,除了一个地方之外,那就是IPCThreadState::waitForResponse()方法中的talkWithDriver(),而在talkWithDriver()中调用了binder_ioctl(),由于内容太多,所以专门写一篇博客进行分析. 实际上,不只是在服务注册过程中会调用到Binder Driver中的binder_ioctl(),在服务检索.服务使用阶段都会调用到bind

Android事件分发机制详解(2)----分析ViewGruop的事件分发

首先,我们需要 知道什么是ViewGroup,它和普通的View有什么区别? ViewGroup就是一组View的集合,它包含很多子View和ViewGroup,是Android 所有布局的父类或间接父类. 但ViewGroup也是一个View,只不过比起View,它可以包含子View和定义布局参数的功能. 现在,通过一个Demo演示Android中ViewGroup的事件分发机制. 首先我们来自定义一个布局,命名为MyLayout,继承自LinearLayout,如下 所示: public c

转自Android内存机制分析1——了解Android堆和栈

转自http://www.cnblogs.com/mythou/p/3202238.html 昨天用Gallery做了一个图片浏览选择开机画面的功能,当我加载的图片多了就出现OOM问题.以前也出现过这个问题,那时候并没有深究.这次打算好好分析一下Android的内存机制. 因为我以前是做VC++开发,因此对C++在Window下的内存机制还是比较了解.不过转到Android后,一直都没有刻意去处理内存问题,因为脑子里一直想着Java的GC机制.不过现在想想,自己对Android的GC和内存管理并