Android应用性能优化最佳实践.2.4 避免过度绘制

简介:

2.4 避免过度绘制


过度绘制(Overdraw)是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构(如带背景的TextView)中,如果不可见的UI也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费多余的CPU以及GPU资源。

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

我们一般在XML布局和自定义控件中绘制,因此可以看出导致过度绘制的主要原因是:

XML布局->控件有重叠且都有设置背景

View自绘-> View.OnDraw里面同一个区域被绘制多次

2.4.1 过度绘制检测工具

要知道是否有过度绘制的情况,可以通过手机设置中的开发者选项,打开Show GPU Overdraw选项,打开后会有不同的颜色区域表示不同的过度绘制次数,如图2-34所示。

具体步骤如下:

1)系统版本要求:需要Android 4.1以上版本。

2)在手机的“设置”→“开发者选项”中打开“显示GPU过度重绘”开关(注:对未默认开启硬件加速的界面需要同时打开“强制进行GPU渲染”开关)。

3)在设置时,如果有App已经打开,需要终止App进程,重新启动。

4)然后即可通过界面的颜色判断界面重绘的严重程度。

打开后可以根据不同的颜色观察UI上的Overdraw情况,蓝色、淡绿、淡红、深红代表4种不同程度的Overdraw情况,不同颜色的含义如下:

无色:没有过度绘制,每个像素绘制了1次。

蓝色:每个像素多绘制了1次。大片的蓝色还是可以接受的。如果整个窗口是蓝色的,可以尝试优化减少一次绘制。

绿色:每个像素多绘制了2次。

淡红:每个像素多绘制了3次。一般来说,这个区域不超过屏幕的1/4是可以接受的。

深红:每个像素多绘制了4次或者更多。严重影响性能,需要优化,避免深红色区域。

我们的目标是尽量减少红色Overdraw,看到更多的蓝色区域。

2.4.2 如何避免过度绘制

1.?布局上的优化

在XML布局上,如果出现了过度绘制的情况,可以使用Hierarchy View来查看具体的层级情况,可以通过XML布局优化来减少层级。需要注意的是,在使用XML文件布局时,会设置很多背景,如果不是必需的,尽量移除。布局优化总结为以下几点:

移除XML中非必需的背景,或根据条件设置。

移除Window默认的背景。

按需显示占位背景图片。

使用Android自带的一些主题时,activity往往会被设置一个默认的背景,这个背景由DecorView持有。当自定义布局有一个全屏的背景时,比如设置了这个界面的全屏黑色背景,DecorView的背景此时对我们来说是无用的,但是它会产生一次Overdraw。因此没有必要的话,也可以移除,代码如下:

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    this.getWindow().setBackgroundDrawable(null);

}

针对ListView中的Avatar ImageView的设置,在getView的代码中,判断是否获取对应的Bitmap,获取Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到时,才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。

2.?自定义View优化

事实上,由于我们的产品设计总是追求更华丽的视觉效果,仅仅通过布局优化很难做到最好,这时可以对复杂的控件使用自定义View来实现,虽然自定义View减少了Layout的层级,但在实际绘制时也是会过度绘制的。原因是有些过于复杂的自定义View(通常重写了onDraw方法),Android系统无法检测在onDraw中具体会执行什么操作,无法监控并自动优化,也就无法避免Overdraw了。但是在自定义View中可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。canvas.clipRect()可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制,并且可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。接下来介绍使用一个自定义View避免OverDraw的案例。

2.4.3 案例:无过度绘制View的实现

我们来实现一个四张图片叠加的自定义View。为了方便看到效果,这四张图片都有一定的重合区域,如果直接绘制,由于系统是不知道有重合区域,就会导致过度绘制,打开Show GPU Overdraw后看到如图2-35所示的效果图,可看出叠加层次越多,过度绘制就越严重。

从图2-35中可以看出,重叠部分都有过度绘制的情况,接下来通过一个例子来避免这种情况,以下代码是描述其中一张图片的类。

    public class SingleCard {

    public RectF area;

    private Bitmap bitmap;

    private Paint paint = new Paint();

    public SingleCard(RectF area) {

        this.area = area;

    }

    public void setBitmap(Bitmap bitmap) {

        this.bitmap = bitmap;

    }

    public void draw(Canvas canvas) {

        canvas.drawBitmap(bitmap, null, area, paint);

    }

}

实现布局的Fragment及控制代码OverDrawFragement如代码清单2-5所示。

代码清单2-5 OverDrawFragement

protected View createView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {

    View view = inflater.inflate(R.layout.fm_overdraw, container, false);

    multicardsView = (MultiCardsView) view.findViewById(R.id.cardview);

    multicardsView.enableOverdrawOpt(true);

    int width = getResources().getDisplayMetrics().widthPixels;

    int height = getResources().getDisplayMetrics().heightPixels;

    int cardWidth = width /3;

    int cardHeight = height /3;

    int yOffset = 40;

    int xOffset = 40;

    for (int i = 0; i < cardResId.length; i++) {

        SingleCard cd = new SingleCard(new RectF(xOffset, yOffset, xOffset + card

Width, yOffset + cardHeight));

        Bitmap bitmap = loadImageResource(cardResId[i], cardWidth, cardHeight);

        cd.setBitmap(bitmap);

        multicardsView.addCards(cd);

        xOffset += cardWidth / 3;

    }

    Button overdraw = (Button) view.findViewById(R.id.btn_overdraw);

    overdraw.setOnClickListener(new View.OnClickListener() {

                                @Override

                                public void onClick(View v) {

                                    multicardsView.enableOverdrawOpt(false);

                                }

                            }

    );

    Button perfectdraw = (Button)  

    view.findViewById(R.id.btn_perfectdraw);

    perfectdraw.setOnClickListener(new View.OnClickListener() {

                                @Override

                                public void onClick(View v) {

                                    multicardsView.enableOverdrawOpt(true);

                                }

                            }

    );

    return view;

}

实现图片叠加的自定义View:MultiCardsView,如代码清单2-6所示。

代码清单2-6 MultiCardsView

    public class MultiCardsView extends View{

    private ArrayList<SingleCard> cardsList = new

    ArrayList<SingleCard>(5);

    private boolean enableOverdrawOpt = true;

    public MultiCardsView(Context context) {

        this(context, null, 0);

    }

    public MultiCardsView(Context context, AttributeSet attrs) {

        this(context, attrs, 0);

    }

    public MultiCardsView(Context context, AttributeSet attrs, int defStyleAttr) {

        super(context, attrs, defStyleAttr);

    }

    public void addCards(SingleCard card) {

        cardsList.add(card);

    }

    // 设置是否消除过度绘制

    public void enableOverdrawOpt(boolean enableOrNot) {

        this.enableOverdrawOpt = enableOrNot;

        invalidate();

    }

    @Override

    public void onDraw(Canvas canvas) {

        super.onDraw(canvas);

        if (cardsList == null || canvas == null)

            return;

        Rect clip = canvas.getClipBounds();

        GLog.d("draw", String.format("clip bounds %d %d %d %d", clip.left,

        clip.top, clip.right, clip.bottom));

        // 根据enableOverdrawOpt值来调用不同的绘制方法,对比效果

        if (enableOverdrawOpt) {

            drawCardsWithotOverDraw(canvas, cardsList.size() - 1);

        } else {

            drawCardsNormal(canvas, cardsList.size() - 1);

        }

    }

    // 实现没有过度绘制的方法

    protected void drawCardsWithotOverDraw(Canvas canvas, int index) {

        if (canvas == null || index < 0 || index >= cardsList.size())

            return;

        SingleCard card = cardsList.get(index);

        // 判断是否没和某个卡片相交,从而跳过那些非矩形区域内的绘制操作

        if (card != null && !canvas.quickReject(card.area, Canvas.EdgeType.BW)) {

            int saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);

            // 只绘制可见区域

            if (canvas.clipRect(card.area, Region.Op.DIFFERENCE)) {

                drawCardsWithotOverDraw(canvas, index - 1);

            }

            canvas.restoreToCount(saveCount);

            saveCount = canvas.save(Canvas.CLIP_SAVE_FLAG);

            // 只绘制可见区域

            if (canvas.clipRect(card.area)) {

                Rect clip = canvas.getClipBounds();

                card.draw(canvas);

            }

            canvas.restoreToCount(saveCount);

        }else{

            drawCardsWithotOverDraw(canvas, index - 1);

        }

    }

    // 普通绘制

    protected void drawCardsNormal(Canvas canvas, int index) {

        if (canvas == null || index < 0 || index >= cardsList.size())

            return;

        SingleCard card = cardsList.get(index);

        if (card != null) {

            drawCardsNormal(canvas, index - 1);

            card.draw(canvas);

        }

    }

}

代码清单2-6在OnDraw时调用了两个不同的方法,常用绘制方法drawCards-Normal()以及无过度绘制方法drawCards-WithotOverDraw(),效果如图2-36所示。

可以看出,使用drawCardsWithotOver-Draw()避免了过度绘制,从代码清单2-6中可以看到,调用了两个关键的方法:

快速判断Canvas是否需要绘制:Canvas. QuickReject。

在绘制一个单元之前,首先判断该单元的区域是否在Canvas的剪切域内。若不在,直接返回,避免CPU和GPU的计算和渲染工作。

避免绘制越界:Canvas.ClipRect。

每个绘制单元都有自己的绘制区域,绘制前,Canvas.ClipRect(Region.Op. INTERSECT)帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内,才会被绘制,其他的区域被忽视。这个API可以很好地帮助那些有多组重叠组件的自定义View来控制显示的区域。clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

这个案例可以避免层次很多的自定义View导致过度绘制的问题。

在listview或其他容器控件中,itemview如果比较复杂,建议实现成一个自绘View,使用此案例来绘制可以使listview滑动更流畅。

相关实践学习
部署Stable Diffusion玩转AI绘画(GPU云服务器)
本实验通过在ECS上从零开始部署Stable Diffusion来进行AI绘画创作,开启AIGC盲盒。
相关文章
|
15天前
|
IDE Java 开发工具
深入探索安卓应用开发:从环境搭建到第一个"Hello, World!"应用
本文将引导读者完成安卓应用开发的初步入门,包括安装必要的开发工具、配置开发环境、创建第一个简单的安卓项目,以及解释其背后的一些基本概念。通过一步步的指导和解释,本文旨在为安卓开发新手提供一个清晰、易懂的起点,帮助读者顺利地迈出安卓开发的第一步。
197 65
|
15天前
|
存储 Java Android开发
探索安卓应用开发:构建你的第一个"Hello World"应用
【9月更文挑战第24天】在本文中,我们将踏上一段激动人心的旅程,深入安卓应用开发的奥秘。通过一个简单而经典的“Hello World”项目,我们将解锁安卓应用开发的基础概念和步骤。无论你是编程新手还是希望扩展技能的老手,这篇文章都将为你提供一次实操体验。从搭建开发环境到运行你的应用,每一步都清晰易懂,确保你能顺利地迈出安卓开发的第一步。让我们开始吧,探索如何将一行简单的代码转变为一个功能齐全的安卓应用!
|
2天前
|
安全 Java API
Java 泛型在安卓开发中的应用
在Android开发中,Java泛型广泛应用于集合类、自定义泛型类/方法、数据绑定、适配器及网络请求等场景,有助于实现类型安全、代码复用和提高可读性。例如,结合`ArrayList`使用泛型可避免类型转换错误;自定义泛型类如`ApiResponse&lt;T&gt;`可处理不同类型API响应;RecyclerView适配器利用泛型支持多种视图数据;Retrofit结合泛型定义响应模型,明确数据类型。然而,需注意类型擦除导致的信息丢失问题。合理使用泛型能显著提升代码质量和应用健壮性。
|
19天前
|
开发框架 搜索推荐 开发工具
打造个性化安卓应用:从零开始的Flutter之旅
【8月更文挑战第51天】本文是一篇面向初学者的Flutter入门教程,旨在通过简单易懂的语言和实际代码示例,引导读者步入跨平台移动应用开发的世界。文章首先介绍了Flutter的基本概念和优势,然后逐步展示了如何搭建开发环境、创建第一个Flutter应用,并实现了一个简单的待办事项列表。最后,文章探讨了Flutter在实现高性能和美观界面方面的潜力,鼓励读者发挥创意,探索更多可能。
70 15
|
10天前
|
监控 安全 Java
Kotlin 在公司上网监控中的安卓开发应用
在数字化办公环境中,公司对员工上网行为的监控日益重要。Kotlin 作为一种基于 JVM 的编程语言,具备简洁、安全、高效的特性,已成为安卓开发的首选语言之一。通过网络请求拦截,Kotlin 可实现网址监控、访问时间记录等功能,满足公司上网监控需求。其简洁性有助于快速构建强大的监控应用,并便于后续维护与扩展。因此,Kotlin 在安卓上网监控应用开发中展现出广阔前景。
9 1
|
24天前
|
搜索推荐 Java Android开发
打造个性化安卓应用:从设计到发布的全程指南
【9月更文挑战第15天】本篇文章将带领读者踏上一段激动人心的旅程,从构思一个独特的安卓应用想法开始,直至将其变为现实并成功发布。我们将一起探索如何捕捉灵感、设计界面、编写代码以及最终将应用推向市场。无论你是编程新手还是有经验的开发者,这篇文章都将为你提供宝贵的洞见和实用的技巧,让你的应用在竞争激烈的市场中脱颖而出。
59 17
|
20天前
|
Java Android开发 UED
🧠Android多线程与异步编程实战!告别卡顿,让应用响应如丝般顺滑!🧵
在Android开发中,为应对复杂应用场景和繁重计算任务,多线程与异步编程成为保证UI流畅性的关键。本文将介绍Android中的多线程基础,包括Thread、Handler、Looper、AsyncTask及ExecutorService等,并通过示例代码展示其实用性。AsyncTask适用于简单后台操作,而ExecutorService则能更好地管理复杂并发任务。合理运用这些技术,可显著提升应用性能和用户体验,避免内存泄漏和线程安全问题,确保UI更新顺畅。
48 5
|
21天前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
96 3
|
28天前
|
存储 Java 编译器
🔍深入Android底层,揭秘JVM与ART的奥秘,性能优化新视角!🔬
【9月更文挑战第12天】在Android开发领域,深入了解其底层机制对提升应用性能至关重要。本文详述了从早期Dalvik虚拟机到现今Android Runtime(ART)的演变过程,揭示了ART通过预编译技术实现更快启动速度和更高执行效率的奥秘。文中还介绍了ART的编译器与运行时环境,并提出了减少DEX文件数量、优化代码结构及合理管理内存等多种性能优化策略。通过掌握这些知识,开发者可以从全新的角度提升应用性能。
45 11
|
21天前
|
存储 API Android开发
"解锁Android权限迷宫:一场惊心动魄的动态权限请求之旅,让你的应用从平凡跃升至用户心尖的宠儿!"
随着Android系统的更新,权限管理成为应用开发的关键。尤其在Android 6.0(API 级别 23)后,动态权限请求机制的引入提升了用户隐私保护,要求开发者进行更精细的权限管理。
47 2