安卓开发中的流畅性优化方案研究

2021-11-10 05:27张博涵
电子设计工程 2021年21期
关键词:控件开发者代码

张博涵

(武汉邮电科学研究院,湖北武汉430074)

作为开发者,在日常开发工作中,性能优化不是最难的问题,却是最让人头疼的问题,因为导致性能变差的原因很多,界面布局、功能逻辑、数据处理甚至是不同版本型号的安卓机都会导致不同的问题。性能优化,是为了让程序能够运行得更快、更省、更稳,先前的开发者们就性能问题虽提出了一些中肯的建议,但并不通用。文中将总结先前开发者们的经验,结合在日常开发中的应用,从应用启动速度、页面加载速度以及操作响应速度入手,提出能提升应用程序流畅性的普适优化建议和一些能使应用程序跑得更快的通用方案。

1 启动速度

启动速度慢的原因,主要是源于在初次打开应用时,需要加载大量的功能逻辑和页面显示的资源。解决该问题可以采取以下几点措施。

1.1 减少onCreate()方法的启动时间

一个Activity 的生命周期如图1所示。由图可知,onCreate()方法是打开一个Activity 时启动的第一个方法,其功能为完成启动一个Activity 的必要的初始化工作,如设置页面的布局资源,初始化组件信息等。如果在onCreate()中的代码过多,该环节耗时过长,则会导致启动速度变慢。

图1 Activity生命周期

举例说明,假如一个页面有20 个控件,开发者将这些控件的所有属性信息,全部放在MainActivity中的onCreate()方法内去初始化。接下来进入到命令提示符cmd 内,通过adb 指令的am start-W-n 包名/.LauncherActivity 指令来启动应用程序,其中的TotalTime 即为该次启动App 所花的时间,大约为600 ms。

接下来可以改变代码,写一个私有方法initView(),将初始化各个控件信息的语句全部放在这个方法里,而在onCreate()中只执行initView()方法,解决了其中代码太过冗杂的问题。重复上一段的指令,得到的应用程序启动时间大约为550 ms。

由以上对比可知,onCreate()内代码的数量直接决定应用程序的启动时长,因为本例中的应用程序非常简单,时长差看起来并不大,但是在代码量庞大的应用程序中体现得尤其明显,直接影响到用户的使用体验。

1.2 使用Adapter来提高效率

Adapter 可以理解成是连接前端显示和后端数据库的适配器接口,是数据(Data)和UI(View)之间的重要桥梁,在经常使用的ListView、GridView 等处都要用到创建对应的Adapter 来配合使用[1-2]。Adapter、Data、View 三者的关系可以用图2来表示。

图2 Adapter、View和Data之间的关系

而Adapter 本身包含很多的接口,其具体的结构如图3所示。

图3 Adapter的结构图

在日常开发中,开发者们常绘继承BaseAdapter类,因为其包含很多需要实现和重写的方法,故具有较高的灵活性。其通常被用于ListView、GridView 等列表或表单型的布局当中。通过重写getView()方法来执行和视图相关的一些操作,每当代码运行到这里的时候,都会调用getView()方法来重绘某个List,寻找该List 的item 布局文件,将其中的控件一个个再重新初始化,但很多情况下,使用者并不需要如此频繁地重绘List,甚至在此次App 的使用中,该List只需要加载一次就够了。在这种情况下,频繁地重绘List 毫无疑问会影响页面的启动速度,而且会造成大量的资源浪费,在布局复杂的时候会尤其明显。为了解决该问题,开发者可以通过convertView+ViewHolder来进行优化。首先要写出一个ViewHolder静态类,接下来去判断getView()方法内的第二个参数convertView 是否为空,如果为空,则表示该List 还没有被加载过,这样便可以通过View.inflate()方法来寻找对应控件的ID 并赋值给convertView,接下来获取一个新的ViewHolder 对象,将convertView 中的各个控件绑定到Viewholder 上,再通过convertView 里的setTag()方法将ViewHolder 储存。当convertView判断不为空的时候,便表示之前已经加载过该List了,不用再重新绘制,此时便可以通过convertView 的getTag()方法直接调用储存的ViewHolder,省掉了绘制的时间,大大缩短了页面启动的速度[3-5]。

1.3 减少主线程阻塞时间

减少主线程阻塞的时间是为了防止ANR(Application Not Responding),当用户失去响应超过5 s以上便会造成ANR。造成ANR 的原因有很多,但是总的来说,都是在主线程里进行了太多耗时操作,比如加载图片、申请网络下载、操作数据库等。从界面优化的角度来说,一方面,开发者要保证进行UI 操作的线程只处理一些跟UI 有关的操作,另一方面,如果实在无法避免一些耗时操作,则需要单独开启一个新线程来处理这些操作,并且通过Handler 来处理UI 线程和其他线程之间的交互。

以上方法都是在代码方面作出的改变,除了这些,开发者们也可以使用一些界面优化的工具,比如Android SDK 提供的一个名叫Layoutopt 优化工具,它可以告诉开发者哪些布局或者控件是多余的,可以删除,也可以告诉开发者当前界面布局太多或者嵌套太多,建议删除或重新设计[7-9]。

2 绘制速度

渲染的意思是将各种布局控件绘制到屏幕上,渲染速度便代表了一个完整页面呈现在用户眼前的速度。如果想给用户以流畅的使用体验,便需要加快渲染速度。

安卓的渲染机制如图4所示,从Google 官方推出的性能优化典范可以得知,60 fps 是当前最佳的图像显示速度,所以目前安卓机的刷新频率都被设置为60 fps,为了达到这个要求,开发者们需要在16 ms内完成一次页面刷新的操作。由图4可知,系统每隔16 ms 便会发出一个垂直同步信号VSYNC 来触发事件,并且在16 ms 之内完成界面的更新与渲染。如此一来体现在用户眼中的便是一次流的展示。

图4 流畅状态下的安卓渲染机制

不管因为任何原因导致这一过程没有在16 ms内完成,便会出现掉帧现象,刷新的帧率也会下降。如此一来体现在用户眼中的便是明显的卡顿。掉帧状态下的安卓渲染机制如图5所示。

图5 掉帧状态下的安卓渲染机制

渲染的过程是由GPU 和CPU 协作完成,两者有各自的分工、不同的潜在问题以及不同的解决方案,具体分工如图6所示。总的来说,便是通过HierarchyViewer 来检测渲染的效率,进而找出不必要的布局嵌套以及控件;通过智能手机上的Show GPU OverDraw 来检测多余的背景[10]。

图6 渲染过程分工图

2.1 优化布局避免过度绘制(OverDraw)

过度绘制是屏幕上的某一个像素在同一帧时间里被绘制的次数过多,一般是布局套嵌过多或是背景颜色重叠导致的。开发者可以通过手机上的设置-开发者选项-调试GPU 过度绘制-显示GPU 过度绘制来查看当前页面是否有过度绘制发生。屏幕上深红色覆盖区域表示过度绘制4 次;浅红色覆盖的区域表示被过度绘制了3 次;被蓝色覆盖区域表示被过度绘制了两次;绿色覆盖区域部分被过度绘制一次。如果一个界面被大量的深红色和浅红色覆盖,则表示该页面必须进行优化。

布局优化流程如下:先把容器之间的多层嵌套取消,改为单层结构或两层嵌套,如此一来虽然在表达能力上稍有欠缺,但在性能优化上却颇有成效。然后只给最外层容器设置白色背景,内层容器和控件的背景设置全部取消,如此一来便可以避免多次绘制背景色。最后删除一些没有输出内容的控件,比如充当分割区域的空文本框等,起到简化布局代码的作用。优化过的页面应当是蓝色区域占大多数,红色区域较少,这并不是完美的布局,但是在实际开发中是完全可以接受的。

通过上述步骤可以发现,在实际开发中,开发者首先要考虑布局容器的选择和嵌套,安卓系统提供的Layout容器包括线性布局、相对布局、帧布局等,各有用处也各有优劣。通常来说,描绘能力差的容器更加简洁,但或许需要多层嵌套。而描绘能力强的容器可以实现绝大多数页面,或许也无需多层套嵌,但计算量也非常大。布局设置不一样,即便功能一致,界面大体相同,但在细节上是有差异的。开发者需要判断什么样的差异可以忽略的,什么样的差异不可接受。所以在实际开发中,要做到既能保证性能又能达到要求还能避免过度绘制是非常重要的。

其次开发者可以考虑去掉多余的背景,主要有两种情况:1)在使用某些安卓系统提供的主题时窗口会自带背景,如果再在Layout 容器中设置背景便会造成重叠浪费,此时开发者可以选择在Activity 的onCreat() 方法中,通过将语句 getWindow().setBackgroundDrawable()的赋值设为null 来取消背景。2)在设计布局界面时,被覆盖的部分的背景也要考虑去掉,这种情况非常多见,比如容器覆盖容器,列表覆盖容器,表格覆盖容器等,被覆盖的容器都应设置背景为空。

最后,开发者要着重注意App 的设计思路,要尽量避免过度设计。App 需要有个漂亮的外表,但是有些App 过于注重外表华丽,反而忽略了一个App简洁直观实用才是最重要的,用户并不会喜欢过于华丽复杂的东西,App 也会因此变得不流畅。

除此之外,开发者们还可以通过ViewStub 来设置属性并赋予指向的布局,只需要通过操作ViewStub来决定是否显示指定的布局;也可以使用draw9patch制作图片,给ImageView 制作背景来充当边框,将重叠部分设置为透明,来满足减少过度绘制的要求等。总之,处理不同的情况,需要不同的思路,使用不同的工具,才能保持性能和流畅度之间的平衡。针对过度绘制的优化方案没有谁对谁错,只有合适与否。

2.2 降低onDraw()方法的复杂度

在实际开发中,安卓系统提供的View 通常是无法满足要求的,此时便需要自定义View。自定义View 的绘制大致分以下步骤:onMeasure()方法用于测量布局宽高尺寸以及位置;onLayout()方法用于排列控件;onDraw()方法用于绘制。自定义View 绘制流程如图7所示。

图7 自定义View绘制流程图

由于onDraw()方法的作用是将已经测好宽高尺寸的View 画出来,所以经常面临重复多次调用的情况。开发者很难去限制其调用次数,所以更多时候会选择尽可能地简化onDraw()方法,正如前文中简化onCreate()方法一样。一方面,开发者要避免在onDraw()方法里创造过多的局部变量,这些局部变量会伴随方法的调用而被创建,在一瞬间会占用较多的资源,而如此反复下来必然会降低效率。另一方面,开发者要把耗时的操作移出onDraw(),比如在绘制界面时需要从网络上请求一张图片显示出来,这种情况下需要另开新线程来作处理,然后通过Handler通知onDraw(),图片请求好了,可以加载即可。

3 响应速度

响应速度变慢,出现的最常见的情况就是ANR。当出现ANR 时,程序会失去响应,屏幕会直接在某个界面卡死。如前文提到的一样,造成ANR的原因还是由于把一些耗时的操作放在了主线程内进行,如果广播或者服务在一段时间内没有响应就必会触发ANR。解决方法无外乎以下几种,异步实现、新开线程、消息机制等[11-12]。

除了上述内容,在实际开发中遇到的问题数不胜数,只能结合实际情况去考虑。比如在处理即时更新的界面时,如何既能保证刷新频率,又能限制资源消耗?使用动画时如何合理选择框架,是否需要使用硬件加速?某些使用不频繁的界面是退出再加载,还是隐藏再显示?诸如此类的问题都将是开发过程中的难题[13-14]。

4 结 论

随着技术的发展和手机的普及,应用程序的使用必然越来越广泛,开发者们也需要去满足用户更多的要求。文中提供的思路都是在代码端就可以实现的,不用考虑手机型号和安卓版本,也具有较为广泛的适用性。而测试结果表明,合理的代码设计不仅能够缩短各种操作耗费的时间,也节省了系统资源,使用户的体验更出色,在代码量大的程序上显现更加明显,而且通过精简、合理规划代码,降低代码的耦合度,也会增加代码的可读性,可以使开发者们更轻松地维护和更新代码。而除了流畅性,还有稳定性、节省性等诸多评价一个程序好坏的指标,文中并未讨论。在实际开发中,如何在兼顾这些指标的同时,做到功能不打折扣,外观尽量美观,操作简单清晰是开发者们无法回避的难题。只有不断钻研,广纳建议,收集用户反馈,才能做出跟得上技术进步和审美发展的出色的应用程序[15-16]。

猜你喜欢
控件开发者代码
创世代码
创世代码
创世代码
创世代码
关于.net控件数组的探讨
“85后”高学历男性成为APP开发新生主力军
16%游戏开发者看好VR
栝楼产业开发者谢献忠
ASP.NET服务器端验证控件的使用
基于嵌入式MINIGUI控件子类化技术的深入研究与应用