Android性能优化摘录

本博文内容均摘录自其他各个博文的一些摘要精髓。
参考资料如下:http://hukai.me/blog/archives/

性能相关

App启动

提高程序的启动速度意义重大,很显然,启动时间越短,用户才越有耐心等待打开这个APP进行使用,反之启动时间越长,用户则越有可能来不及等到APP打开就已经切换到其他APP了。程序启动过程中的那些复杂错误的操作很可能导致严重的性能问题。Android系统会根据用户的操作行为调整程序的显示策略,用来提高程序的显示性能。例如,一旦用户点击桌面图标,Android系统会立即显示一个启动窗口,这个窗口会一直保持显示直到画面中的元素成功加载并绘制完第一帧。这种行为常见于程序的冷启动,或者程序的热启动场景(程序从后台被唤起或者从其他APP界面切换回来)。那么关键的问题是,用户很可能会因为从启动窗口到显示画面的过程耗时过长而感到厌烦,从而导致用户没有来得及等程序启动完毕就切换到其他APP了。更严重的是,如果启动时间过长,可能导致程序出现ANR。我们应该避免出现这两种糟糕的情况。
从技术角度来说,当用户点击桌面图标开始,系统会立即为这个APP创建独立的专属进程,然后显示启动窗口,直到APP在自己的进程里面完成了程序的创建以及主线程完成了Activity的初始化显示操作,再然后系统进程就会把启动窗口替换成APP的显示窗口。
这里写图片描述

上述流程里面的绝大多数步骤都是由系统控制的,一般来说不会出现什么问题,可是对于启动速度,我们能够控制并且需要特别关注的地方主要有三处:

1)Activity的onCreate流程,特别是UI的布局与渲染操作,如果布局过于复杂很可能导致严重的启动性能问题。
2)Application的onCreate流程,对于大型的APP来说,通常会在这里做大量的通用组件的初始化操作。
3)目前有部分APP会提供自定义的启动窗口,这里可以做成品牌宣传界面或者是给用户提供一种程序已经启动的视觉效果。
在正式着手解决问题之前,我们需要掌握一套正确测量评估启动性能的方法。所幸的是,Android系统有提供一些工具来帮助我们定位问题。

1)首先是display time:从Android KitKat版本开始,Logcat中会输出从程序启动到某个Activity显示到画面上所花费的时间。这个方法比较适合测量程序的启动时间。
这里写图片描述

2)其次是reportFullyDrawn方法:我们通常来说会使用异步懒加载的方式来提升程序画面的显示速度,这通常会导致的一个问题是,程序画面已经显示,可是内容却还在加载中。为了衡量这些异步加载资源所耗费的时间,我们可以在异步加载完毕之后调用activity.reportFullyDrawn()方法来告诉系统此时的状态,以便获取整个加载的耗时。
这里写图片描述

3)然后是Method Tracing:前面两个方法提供了启动耗时的总时间,可是却无法提供具体的耗时细节。为了获取具体的耗时分布情况,我们可以使用Method Tracing工具来进行详细的测量。
这里写图片描述

4)最后是Systrace:我们可以在onCreate方法里面添加trace.beginSection()与trace.endSection()方法来声明需要跟踪的起止位置,系统会帮忙统计中间经历过的函数调用耗时,并输出报表。
这里写图片描述

启动闪屏

启动闪屏不仅仅可以作为品牌宣传页,还能够减轻用户对启动耗时的感知,但是如果使用不恰当,将适得其反。前面介绍过当点击桌面图标启动APP的时候,程序会显示一个启动窗口,一直到页面的渲染加载完毕。如果程序的启动速度足够快,我们看的闪屏窗口停留显示的时间则会很短,但是当程序启动速度偏慢的时候,这个启动闪屏可以一定程度上减轻用户等待的焦虑感,避免用户过于轻易的关闭应用。

目前大多数开发者都会通过设置启动窗口主题的方式来替换系统默认的启动窗口,通过这种方式只是使用『障眼法』弱化了用户对启动时间的感知,但本质上并没有对启动速度做什么优化。也有些APP通过关闭启动窗口属性android:windowDisablePreview的方式来直接移除系统默认的启动窗口,但是这样的弊端是用户从点击桌面图标到真的看到实际页面的这段时间当中,画面没有任何变化,这样的用户体验是十分糟糕的!
这里写图片描述
这里写图片描述
对于启动闪屏,正确的使用方法是自定义一张图片,把这张图片通过设置主题的方式显示为启动闪屏,代码执行到主页面的onCreate的时候设置为程序正常的主题。
这里写图片描述

这里写图片描述

内存分配:

每一个进程的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操作速度更快。如下图所示:
这里写图片描述
每一个Generation的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。如下图所示:
这里写图片描述
通常情况下,GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历树结构查找20000个对象比起遍历50个对象自然是要慢很多的。
为了整个Android系统的内存控制需要,Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。
ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明你的应用的Heap Size阈值是多少Mb(megabates)。

减小对象的内存占用:

  1. 使用更加轻量的数据结构,例如,我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构,下图演示了HashMap的简要工作原理,相比起Android系统专门为移动操作系统编写的ArrayMap容器,在大多数情况下,都显示效率低下,更占内存。通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
  2. 避免在Android里面使用Enum
  3. 减小Bitmap对象的内存占用或者使用更小的图片
  4. 注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用
  5. 避免在onDraw方法里面执行对象的创建
  6. 避免对象的内存泄露
  7. 资源文件需要选择合适的文件夹进行存放,我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会被拉伸到200200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets或者nodpi的目录下。
  8. 谨慎使用static对象,因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。
  9. 特别留意单例对象中不合理的持有,虽然单例模式简单实用,提供了很多便利性,但是因为单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。
  10. 优化布局层次,减少内存消耗,越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的。
  11. 谨慎使用依赖注入框架,使用类似Guice或者RoboGuice等框架注入代码,在某种程度上可以简化你的代码。然而,那些注入框架会通过扫描你的代码执行许多初始化的操作,这会导致你的代码需要大量的内存空间来mapping代码,而且mapped pages会长时间的被保留在内存中。除非真的很有必要,建议谨慎使用这种技术。

遍历相关:

遍历容器是编程里面一个经常遇到的场景。在Java语言中,使用Iterate是一个比较常见的方法。可是在Android开发团队中,大家却尽量避免使用Iterator来执行遍历操作。下面我们看下在Android上可能用到的三种不同的遍历方法:
这里写图片描述
这里写图片描述
这里写图片描述
使用上面三种方式在同一台手机上,使用相同的数据集做测试,他们的表现性能如下所示:
这里写图片描述
从上面可以看到for index的方式有更好的效率,但是因为不同平台编译器优化各有差异,我们最好还是针对实际的方法做一下简单的测量比较好,拿到数据之后,再选择效率最高的那个方式。

打包APK相关:

减小APK大小:

减少应用程序安装包的大小,不仅仅减少了用户的网络数据流量还减少了下载等待的时间。毋庸置疑,尽量减少程序安装包的大小是十分有必要的。通常来说,减少程序安装包的大小有两条规律:要么减少程序资源的大小,要么就是减少程序的代码量。这里总结一个简易版的减少安装包大小的Checklist:

减少程序图片资源的大小

1)确保在build.gradle文件中开启了minifEnabled与shrinkResources的属性,这两个属性可以帮助移除那些在程序中使用不到的代码与资源,帮助减少APP的安装包大小。
这里写图片描述

3)在符合条件的情况下,使用Vertor Drawable替代传统的PNG/JPEG图片,能够极大的减少图片资源的大小。传统模式下,针对不同dpi的手机都需要提供一套PNG/JPEG的图片,而如果使用Vector Drawable的话,只需要一个XML文件即可。

4)尽量复用已经存在的资源图片,使用代码的方式对已有的资源进行复用,如下图所示:
这里写图片描述

以上几点虽然看起来都微不足道,但是真正执行之后,能够显著减少安装包的资源图片大小。

减少程序的代码量

1)注意因为编译行为额外产生的方法数,例如类似Enum,Protocal Buffer可能导致方法数与类的个数增加。
2)部分引入到工程中的jar类库可能并不是专门针对移动端APP而设计的,他们最开始可能是运用在PC或者Server上的。使用这些类库不仅仅额外增加了包的大小,还增加了编译时间。单纯依靠Proguard可能无法完全移除那些使用不到的方法,最佳的方式是使用一些更加轻量化,专门为Android APP设计的jar类库。

安装包的拆分
设想一下,一个low dpi,API<14的用户手机下载安装的APK里面却包含了大量xxhdpi的资源文件,对于这个用户来说,这个APK是存在很大的资源浪费的。幸好Android平台为我们提供了拆分APK的方法,它能够根据API Level,屏幕大小以及GPU版本的不同进行拆分,使得对应平台的用户下载到最合适自己手机的安装包。
这里写图片描述
更多关于安装包拆分的信息,请查看Configure APK SplitsMaintaining Multiple APKs(由于国内应用分发市场的现状,这一条几乎没有办法执行)。

UI相关:

Profile GPU Rendering:

从Android M系统开始,系统更新了GPU Profiling的工具来帮助我们定位UI的渲染性能问题。早期的CPU Profiling工具只能粗略的显示出Process,Execute,Update三大步骤的时间耗费情况。
这里写图片描述

但是仅仅显示三大步骤的时间耗费情况,还是不太能够清晰帮助我们定位具体的程序代码问题,所以在Android M版本开始,GPU Profiling工具把渲染操作拆解成如下8个详细的步骤进行显示。
这里写图片描述
旧版本中提到的Proces,Execute,Update还是继续得到了保留,他们的对应关系如下:
这里写图片描述
接下去我们看下其他五个步骤分别代表了什么含义:

Sync & Upload:通常表示的是准备当前界面上有待绘制的图片所耗费的时间,为了减少该段区域的执行时间,我们可以减少屏幕上的图片数量或者是缩小图片本身的大小。
Measure & Layout:这里表示的是布局的onMeasure与onLayout所花费的时间,一旦时间过长,就需要仔细检查自己的布局是不是存在严重的性能问题。
Animation:表示的是计算执行动画所需要花费的时间,包含的动画有ObjectAnimator,ViewPropertyAnimator,Transition等等。一旦这里的执行时间过长,就需要检查是不是使用了非官方的动画工具或者是检查动画执行的过程中是不是触发了读写操作等等。
Input Handling:表示的是系统处理输入事件所耗费的时间,粗略等于对于的事件处理方法所执行的时间。一旦执行时间过长,意味着在处理用户的输入事件的地方执行了复杂的操作。
Misc/Vsync Delay:如果稍加注意,我们可以在开发应用的Log日志里面看到这样一行提示:I/Choreographer(691): Skipped XXX frames! The application may be doing too much work on its main thread。这意味着我们在主线程执行了太多的任务,导致UI渲染跟不上vSync的信号而出现掉帧的情况。
上面八种不同的颜色区分了不同的操作所耗费的时间,为了便于我们迅速找出那些有问题的步骤,GPU Profiling工具会显示16ms的阈值线,这样就很容易找出那些不合理的性能问题,再仔细看对应具体哪个步骤相对来说耗费时间比例更大,结合上面介绍的细化步骤,从而快速定位问题,修复问题。

ClipRect & QuickReject

前面有提到过,对不可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。
这里写图片描述
但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。
这里写图片描述
除了clipRect方法之外,我们还可以使用canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。
这里写图片描述
上面的示例图中显示了一个自定义的View,主要效果是呈现多张重叠的卡片。这个View的onDraw方法如下图所示:
这里写图片描述
打开开发者选项中的显示过度渲染,可以看到我们这个自定义的View部分区域存在着过度绘制。那么是什么原因导致过度绘制的呢?
这里写图片描述
下面的代码显示了如何通过clipRect来解决自定义View的过度绘制,提高自定义View的绘制性能:
这里写图片描述
下面是优化过后的效果:
这里写图片描述

可能导致内存泄漏的原因总结:

1)注意Activity的泄漏

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

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

Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。例如view引用了activity context,如果view被其他引用泄漏了了,那么相应activity context也会泄漏,这是一个连环的锅。

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

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

3)内部类导致的泄漏

使用内部类的时候,如果不用static来修饰的话,内部类会持有外部类,所以如果内部类泄漏,会导致外部类也泄漏。我们可以考虑尽量使用static类型的内部类,同时使用WeakReference的机制来避免因为互相引用而出现的泄露。

4)注意临时Bitmap对象的及时回收

虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的。例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。

需要特别留意的是Bitmap类里面提供的createBitmap()方法:
这里写图片描述
这个函数返回的bitmap有可能和source bitmap是同一个,在回收的时候,需要特别检查source bitmap与return bitmap的引用是否相同,只有在不等的情况下,才能够执行source bitmap的recycle方法。
除了Bitmap,还有File、Cursor等

5)注意监听器的注销

在Android程序里面存在很多需要register与unregister的监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的listener,需要记得及时remove这个listener。例如EventBus这个开源库,在Activity的onDestroy函数中,记得要调用unregister函数,否则会持有该activity。

6)注意WebView的泄漏

Android中的WebView存在很大的兼容性问题,不仅仅是Android系统版本的不同对WebView产生很大的差异,另外不同的厂商出货的ROM里面WebView也存在着很大的差异。更严重的是标准的WebView存在内存泄露的问题,主要是因为如果activity中包含webview的话,在activity的ondestroy函数中,调用webview的removeAllViews和destroy也不能释放webview,webview持有activity,所以会导致activity泄漏,目前网上有多种方案,但是都不能解决该问题。所以通常根治这个问题的办法是为WebView开启另外一个进程,通过AIDL与主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放,但是创建新的进程会存在一些问题,例如创建新进程的时候,会创建一个新的application,这里可能会出错,我尝试了下在我的项目中创建新进程,但是在gradle中配置jacoco的东西,然后就出错了,但还未找到原因。

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

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

8)注意异步回调的泄漏:例如一些网络请求的异步回调,当回调发生时,可能activity已经销毁,这时候比较容易发生crash,或者持有activity。但是如果该回调不会重复进行的话,其实只是短暂的泄漏,也并无关系。但是如果该回调会反复的进行(没有显示调用暂停),那么该activity就会泄漏。这种情况主要体现在动画上面,如果动画设置了repeat,那么就会循环往复,所以在activity结束的时候,显示调用动画的结束方法。

Android UI掉帧可能的情况:

掉帧的原因就是主线程执行一些耗时的时间,导致界面无法在16ms内绘制,导致掉帧。以下是可能出现的一些情况。
1)如果写了自定义的progressView,实时在旋转的控件。如果在该控件运行时,调用setContentView的话,会出现掉帧的情况,经测量setContentView比较耗时,需要执行1-3s左右。但是该方法又只能在主线程中调用,所以这时候要注意。

2)加密过程。加密过程只是简单的一个java计算过程而已,但是其实也是相当耗时的。而且加密过程可以在任何线程中运行,如果在主线程运行的话,会占用主线程太多时间,而导致主线程掉帧。所以对于加密这种过程还是放在子线程比较好。
类似这种应该在子线程做,但是结果要放在主线程中执行的过程,开启子线程,使用handler来传递消息的怎么做呢?代码会不会很麻烦。那么这里可以使用RxJava来完成,RxJava可以简单的切换观察者和被观察者运行的线程,简化你代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值