当前位置:主页 > 移动开发 > Android代码 >

Android高级开发之性能优化典范

时间:2022-07-24 10:15:41 | 栏目:Android代码 | 点击:

本章介绍android高级开发中,对于性能方面的处理。主要包括电量,视图,内存三个性能方面的知识点。

1.视图性能

(1)Overdraw简介

    Overdraw就是过度绘制,是指在一帧的时间内(16.67ms)像素被绘制了多次,理论上一个像素每次只绘制一次是最优的,但是由于重叠的布 局导致一些像素会被多次绘制,而每次绘制都会对应到CPU的一组绘图命令和GPU的一些操作,当这个操作耗时超过16.67ms时,就会出现掉帧现象,表现为应用卡顿,所以对重叠不可见元素的重复绘制会产生额外的开销,需要尽量减少Overdraw的发生。

(2)Overdraw检测

    Android提供了测量Overdraw的选项,在开发者选项-调试GPU过度绘制(Show GPU Overdraw),打开选项就可以看到当前页面Overdraw的状态,就可以观察屏幕的绘制状态。该工具会使用三种不同的颜色绘制屏幕,来指示 overdraw发生在哪里以及程度如何,其中:

 •没有颜色: 意味着没有overdraw。像素只画了一次。

 •蓝色: 意味着overdraw 1倍。像素绘制了两次。大片的蓝色还是可以接受的(若整个窗口是蓝色的,可以摆脱一层)。 

•绿色: 意味着overdraw 2倍。像素绘制了三次。中等大小的绿色区域是可以接受的但你应该尝试优化、减少它们。

•浅红: 意味着overdraw 3倍。像素绘制了四次,小范围可以接受。

 •暗红: 意味着overdraw 4倍。像素绘制了五次或者更多。这是错误的,要修复它们。

提高程序在视图方面的性能, 总的原则就是:尽量避免重叠不可见元素的绘制。

(3)Overdraw改良

1)合理选择控件容器

    Android提供的Layout控件主要包括 LinearLayout、TableLayout、FrameLayout、RelativeLayout。同一个界面可以使用不同的容器控件来表达,但是各个容器控件描述界面的复杂度是不一样的。一般来LinearLayout最易,RelativeLayout较复杂。 LinearLayout只能用来描述一个方向上连续排列的控件,而RelativeLayout几乎可以用于描述任意复杂度的界面。。综上所述: LinearLayout易用,效率高,表达能力有限。RelativeLayout复杂,表达能力强,效率低。从减少overdraw的角度来看,LinearLayout会增加控件数的层级,自然是RelativeLayout 更优,但是当某一界面在使用LinearLayout并不会比RelativeLayout带来更多的控件数和控件层级时,LinearLayout则是首选。

2)去掉window的默认背景

   当使用Android自带的一些主题时,window会被默认添加一个纯色的背景,这个背景是被DecorView持有的。当自定义布局时又添加了一张背景图或者设置背景色,那么DecorView的background此时是无用的,但是它会产生一次Overdraw,带来绘制性能损耗。去掉window的背景可以在onCreate()中setContentView()之后调用 getWindow().setBackgroundDrawable(null);或者在theme中添加 android:windowbackground="null"。

3)去掉其他不必要的背景

    父容器若已经有了背景,可不设置对应子控件的背景,及大的布局背景已经设置,应避免设置局部重复的背景。

4)自定义View处理

    对于自定义的view视图,可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

5)ViewStub高效占位符

    当遇到这样的情况,运行时动态根据条件来决定显示哪个View或布局。常用的做法是把View都写在上面,先把它们的可见性都设为 View.GONE,然后在代码中动态的更改它的可见性。这种模式的缺点是耗费资源。虽然把View 的初始View.GONE但是在Inflate布局的时候View仍然会被Inflate,程序运行时仍然会创建对象,会被实例化,会被设置属性,会耗费内存等资源。

   推荐的做法是使用android.view.ViewStub,ViewStub是一个轻量级的View,它一个看不见的,不占布局位置,占用资源非常小的控件。可以为ViewStub指定一个布局,在Inflate布局的时候,只有ViewStub会被初始化,然后当ViewStub被设置为可见的时候,或是调用了ViewStub.inflate()的时候,ViewStub所向的布局就会被Inflate和实例化,然后ViewStub的布局属性都会传给它所指向的布局。这样,就可以使用ViewStub来方便的在运行时,要还是不要显示某个布局。

6)善用draw9patch

    给ImageView加一个边框,通常在ImageView后面设置一张背景图,露出边框便完美解决问题,此时这个 ImageView,设置了两层drawable,两层drawable的重叠区域去绘制了两次,导致 overdraw。优化方案: 将背景drawable制作成draw9patch,并且将和前景重叠的部分设置为透明。由于Android的2D渲染器会优化draw9patch中的透明区域,从而优化了这次overdraw。

7)Merge 

    使用Merge标签来做容器控件。第一种子视图不需要指定任何针对父视图的布局属性,就是说父容器仅仅是个容器,子视图只需要直接添加到父视图上用于显示 就 行。另外一种是假如需要在LinearLayout里面嵌入一个布局 (或者视图),而恰恰这个布局(或者视图)的根节点也是LinearLayout,这样就多了一层没有用的嵌套,无疑这样只会拖慢程序速度。而这个时候如 果我们使用merge根标签就可以避免那样的问题。

2.内存性能

(1)内存分配与回收

    每一个进程的Dalvik Heap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的Dalvik Heap Size,它可以随着需要进行增长,但是增长行为会有一个系统为它设定上限。

    逻辑上讲的Heap Size和实际物理意义上使用的内存大小是不对等的,Proportional Set Size(PSS)记录了应用程序自身占用以及与其他进程进行共享的内存。

    Android 系统并不会对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发GC操作,从而腾 出更多空闲的内存空间。在Android的高级系统版本里面针对Heap空间有一个Generational Heap Memory的模型,最近分配的对象会存放在Young Generation区域。当这个对象在该区域停留的时间达到一定程度,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的GC操作。例如,刚分配到Young Generation区域的对象通常更容易被销毁回收,同时在Young Generation区域的GC操作速度会比Old Generation区域的GC操作速度更快(如图1所示)。

 

图1  根据不同内存数据类型执行不同GC操作

    每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阀值时,会触发GC操作,以便腾出空间来存放其他新的对象(如图2所示)。

 

图2  对象值临近阀值触发GC操作

    通常情况下,GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历树结构查找20000个对象比起遍历50个对象自然是要慢 很多的。

(2)内存测试插件

1)LeakCanary简介

    LeakCanary是一个用于检测内存泄漏的工具,可以用于Java和Android,是由著名开源组织Square贡献。

2)LeakCanary工作原理

 •RefWatcher.watch()创建一个KeyedWeakReference到北监控的对象。

 •接下来,在后台线程中检测这个引用是否被清除,如果没有将会触发GC。

•如果引用仍然没有清除,将heap内存dump到一个.hprof的文件存放到手机系统里。

 •HeapAnalyzerService在另外一个独立的进程中启动,使用HeapAnalyzer解析heap内存通过HAHA这个项目

 •HeapAnalyzer计算出到GC ROOTs的最短强引用路径决定是否发生Leak,然后建立导致泄漏的引用链。 结果被回传到应用程序进程的DisplayLeakService中,然后显示一个泄漏的通知。

(3)内存优化

1)谨慎使用large heap

    Android设备根据硬件与软件的设置差异而存在不同大小的内存空间,他们为应用程序设置了不同大小的Heap限制阈值。设计时可以通过调用getMemoryClass()来获取应用的可用Heap大小。在一些特殊的情景下,你可以通过在manifest的application标签下添加 largeHeap=true的属性来为应用声明一个更大的heap空间。然后,你可以通过getLargeMemoryClass()来获取到这个更大的heap size阈值。

    然而,声明得到更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的因为你需要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存并且知道为什么这些内存必须被保留时才去使用large heap。因此请谨慎使用large heap属性。使用额外的内存空间会影响系统整体的用户体验,并且会使得每次gc的运行时间更长。

2)综合考虑设备内存阈值与其他因素设计合适的缓存大小

    例如,在设计ListView或者GridView的Bitmap LRU缓存的时候,需要考虑的点有:

 •应用程序剩下了多少可用的内存空间?

 •有多少图片会被一次呈现到屏幕上?有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕?

 •设备的屏幕大小与密度是多少? 一个xhdpi的设备会比hdpi需要一个更大的Cache来hold住同样数量的图片。

 •不同的页面针对Bitmap的设计的尺寸与配置是什么,大概会花费多少内存?

 •页面图片被访问的频率?是否存在其中的一部分比其他的图片具有更高的访问频繁?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache容器。

3)资源文件需要选择合适的文件夹进行存放

   hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一 张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高 的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。

4)Try catch某些大内存分配的操作

    在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode。

5)谨慎使用static对象

    因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。

6)特别留意单例对象中不合理的持有

    虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。

7)珍惜Services资源

    应用需要在后台使用service,除非它被触发并执行一个任务,否则其他时候Service都应该是停止状态。另外需要注意当这个service 完成任务之后因为停止service失败而引起的内存泄漏。 当你启动一个Service,系统会倾向为了保留这个Service而一直保留Service所在的进程。这使得进程的运行代价很高,因为系统没有办法把 Service所占用的RAM空间腾出来让给其他组件,另外Service还不能被Paged out。这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会导致系统内存使用不稳定,从而无法继续保持住所有目前正在运行的service。 建议使用IntentService,它会在处理完交代给它的任务之后尽快结束自己。

8)使用ProGuard来剔除不需要的代码

   ProGuard能够通过移除不需要的代码,重命名类,域与方法等等对代码进行压缩,优化与混淆。使用ProGuard可以使得你的代码更加紧凑,这样能够减少mapping代码所需要的内存空间。

9)使用更加轻量的数据结构

   考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构。

   HashMap的容器,相比起 Android专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效,在于他们避免了对key与value的自动装箱 (autoboxing),并且避免了装箱后的解箱。

10)避免在Android里面使用Enum

  Android 官方培训课程提到过“Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.”,具体原理请参考《Android性能优化典范(三)》,所以请避免在Android里面使用到枚举。

11)减小Bitmap对象的内存占用

    Bitmap是一个极容易消耗内存的大胖子,减小创建出来的Bitmap的内存占用可谓是重中之重,通常来说有以下2个措施:
 •inSampleSize:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。
 •decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。

12)使用更小的图片

    在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException。假设有一张很大的图片被XML文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException,这个问题的根本原因其实是发生了OOM。

13)内存对象的重复利用

  大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码时显式地在程序里创建对象池,然后处理好复用的实现逻辑。要么就是利用系统框架既有的某些复用特性,减少对象的重复创建,从而降低内存的分配与回收。

14)复用系统自带的资源

    Android 系统本身内置了很多的资源,比如字符串、颜色、图片、动画、样式以及简单布局等,这些资源都可以在应用程序中直接引用。这样做不仅能减少应用程序的自身负重,减小APK的大小,还可以在一定程度上减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异、不符合需求的情况,还是需要应用程序自身内置进去。

15)注意Cursor对象是否及时关闭

    在程序中我们经常会进行查询数据库的操作,但时常会存在不小心使用Cursor之后没有及时关闭的情况。这些Cursor的泄露,反复多次出现的话会对内存管理产生很大的负面影响,我们需要谨记对Cursor对象的及时关闭。

16)避免在onDraw方法里面执行对象的创建

   类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

17)StringBuilder

    在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”。

18)注意Activity的泄漏

 通常来说,Activity的泄漏是内存泄漏里面最严重的问题,它占用的内存多,影响面广,需要特别注意以下两种情况导致的Activity泄漏:

 •内部类引用导致Activity的泄露

   最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,都有可能因为Handler继 续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference的方式来达到断开Handler与Activity之间存在引用关系的目的。

•Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。

    内部类引起的泄漏不仅仅会发生在Activity上,其他任何内部类出现的地方,都需要特别留意!可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。

19)考虑使用Application Context而不是Activity Context

    对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。

(3)电量优化

    电量其实是目前手持设备最宝贵的资源之一,大多数设备都需要不断的充电来维持继续使用。不幸的是,对于开发者来说,电量优化是他们最后才会考虑的的事情。但是可以确定的是,千万不能让你的应用成为消耗电量的大户。

有下面一些措施能够显著减少电量的消耗:

 •我们应该尽量减少唤醒屏幕的次数与持续的时间,使用WakeLock来处理唤醒的问题,能够正确执行唤醒操作并根据设定及时关闭操作进入睡眠状态。
 •某些非必须马上执行的操作,例如上传歌曲,图片处理等,可以等到设备处于充电状态或者电量充足的时候才进行。
 •触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。关于网络请求引起无线信号的电量消耗。

1)消耗电量的几个主要原因、功能

 •大数据量的网络传输(网络)

 •不停的网络切换(网络)

 •解析大量的数据(CPU)

2)关于网络方面的优化

 •网络请求之前,检查网络连接。没有网络连接不进行请求

 •判断网络类型,针对特定的数据在特定的网络下请求。例如:大量数据传输的时候,在wifi下请求。wifi下下载数据耗电量只有2、3、4G的1/3.

 •使用效率高的解析工具。根据具体业务数据量的大小,选择合适的解析工具。例如android上面的协议解析一般推荐json。

 •使用GZIP压缩方式下载数据,能减少网络流量,缩短下载时间

•合理使用缓存,避免重复操作

 •使用推送,代替循环请求

 •触发网络请求的操作,每次都会保持无线信号持续一段时间,我们可以把零散的网络请求打包进行一次操作,避免过多的无线信号引起的电量消耗。

 •是JobScheduler API所做的事情。它会根据当前的情况与任务,组合出理想的唤醒时间,例如等到正在充电或者连接到WiFi的时候,或者集中任务一起执行。我们可以通过这个API实现很多免费的调度算法。

3)电量优化策略

 •检查全部唤醒锁, 是否存在冗余或者无用的位置.

 •集中相关的数据请求, 统一发送; 精简数据, 减少无用数据的传输.

 •分析和统计等非重要操作, 可以在电量充足或连接WIFI时进行, 参考JobScheduler.

 •精简冗余的服务(Service), 避免长时间执行耗电操作.

 •注意定位信息的获取, 使用后及时关闭.

您可能感兴趣的文章:

相关文章