Android 性能优化综合篇

节制地使用Service

当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存
并且,系统可以在LRU cache当中缓存的进程数量也会减少导致切换应用程序的时候耗费更多性能

如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况

当界面不可见时释放内存

我们如何才能知道程序界面是不是已经不可见了呢?

Activity中重写onTrimMemory()方法,然后在这个方法中监听TRIM_MEMORY_UI_HIDDEN这个级别.

1
2
3
4
5
6
7
8
9
@Override  
public void onTrimMemory(int level) {
super.onTrimMemory(level);
switch (level) {
case TRIM_MEMORY_UI_HIDDEN:
// 进行资源释放操作
break;
}
}

注意:
onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发。
这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用
我们一般在onstop()执行下列操作:

比如说取消网络连接或者注销广播接收器等.

当内存紧张时释放内存

我们接着上面的一点onTrimMemory()的操作。
手机内存降低的时候及时通知我们。我们应该根据回调中传入的级别来去决定如何释放应用程序的资源

  • TRIM_MEMORY_RUNNING_MODERATE 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,系统可能会开始根据LRU缓存规则来去杀死进程了。
  • TRIM_MEMORY_RUNNING_LOW 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,我们应该去释放掉一些不必要的资源以提升系统的性能,同时这也会直接影响到我们应用程序的性能。
  • TRIM_MEMORY_RUNNING_CRITICAL 表示应用程序仍然正常运行,但是系统已经根据LRU缓存规则杀掉了大部分缓存的进程了。这个时候我们应当尽可能地去释放任何不必要的资源,不然的话系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如说后台运行的服务。

以上是当我们的应用程序正在运行时的回调,那么如果我们的程序目前是被缓存的,则会收到以下几种类型的回调:

  • TRIM_MEMORY_BACKGROUND 表示手机目前内存已经很低了,系统准备开始根据LRU缓存来清理进程。这个时候我们的程序在LRU缓存列表的最近位置,是不太可能被清理掉的,但这时去释放掉一些比较容易恢复的资源能够让手机的内存变得比较充足,从而让我们的程序更长时间地保留在缓存当中,这样当用户返回我们的程序时会感觉非常顺畅,而不是经历了一次重新启动的过程。
  • TRIM_MEMORY_MODERATE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置,如果手机内存还得不到进一步释放的话,那么我们的程序就有被系统杀掉的风险了。
  • TRIM_MEMORY_COMPLETE 表示手机目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,系统会最优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。

避免在Bitmap上浪费内存

当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。因为Bitmap的大小事依靠通过计算像素点宽高的之积。

使用优化过的数据集合

Android API当中提供了一些优化过后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等来替换对应HashMap.

为什么可以达到优化的效果呢
SparseArray避免掉了基本数据类型转换成对象数据类型的时间

知晓内存的开支情况

  • 使用枚举通常会比使用静态常量要消耗两倍以上的内存,在Android开发当中我们应当尽可能地不使用枚举。
  • 任何一个Java类,包括内部类、匿名类,都要占用大概500字节的内存空间。
  • 任何一个类的实例要消耗12-16字节的内存开支,因此频繁创建实例也是会一定程序上影响内存的。
  • 在使用HashMap时,即使你只设置了一个基本数据类型的键,比如说int,但是也会按照对象的大小来分配内存,大概是32字节,而不是4字节。因此最好的办法就是像上面所说的一样,使用优化过的数据集合。

谨慎使用抽象编程

在Android上使用抽象会带来额外的内存开支,因为抽象的编程方法需要编写额外的代码,虽然这些代码根本执行不到,但是却也要映射到内存当中,不仅占用了更多的内存,在执行效率方面也会有所降低

尽量避免使用依赖注入框架

这些框架为了要搜寻代码中的注解,通常都需要经历较长的初始化过程,并且还可能将一些你用不到的对象也一并加载到内存当中。这些用不到的对象会一直占用着内存空间,可能要过很久之后才会得到释放,相较之下,也许多敲几行看似繁琐的代码才是更好的选择

使用ProGuard简化代码

除了混淆之外,它还具有压缩和优化代码的功能。ProGuard会对我们的代码进行检索,删除一些无用的代码,并且会对类、字段、方法等进行重命名,重命名之后的类、字段和方法名都会比原来简短很多,这样的话也就对内存的占用变得更少了

使用多个进程

通过对service进行设置。如下:

一个用于UI展示,另一个则用于在后台持续地播放音乐

对应service的设置:

1
2
<service android:name=".PlaybackService"
android:process=":background" />

监控app的内存

每个程序都会有可使用的内存上限,这被称为堆大小(Heap Size)。例如:

堆大小也已经由Nexus One时的32MB,变成了Nexus 5时的192MB

如何检测当前手机允许占用多少内存?

1
2
ActivityManager manager = (ActivityManager)getSystemService(Context.ACTIVITY_SERVICE);
int heapSize = manager.getMemoryClass();

注意:
MB为单位进行返回的.当程序超过这个值,将出现OutOfMemoryError.我们可以根据堆大小来决定缓存数据的容量.

怎样才能去监听系统的GC过程呢

系统每进行一次GC操作时,都会在LogCat中打印一条日志,我们只要去分析这条日志就可以了,日志的基本格式

1
D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>,  <Pause_time>

首先第一部分GC_Reason,这个是触发这次GC操作的原因

  • GC_CONCURRENT: 当我们应用程序的堆内存快要满的时候,系统会自动触发GC操作来释放内存。
  • GC_FOR_MALLOC: 当我们的应用程序需要分配更多内存,可是现有内存已经不足的时候,系统会进行GC操作来释放内存。
  • GC_HPROF_DUMP_HEAP: 当生成HPROF文件的时候,系统会进行GC操作,关于HPROF文件我们下面会讲到。
  • GC_EXPLICIT: 这种情况就是我们刚才提到过的,主动通知系统去进行GC操作,比如调用System.gc()方法来通知系统。或者在DDMS中,通过工具按钮也是可以显式地告诉系统进行GC操作的。

第二部分:Amount_freed,表示系统通过这次GC操作释放了多少内存:

第三部分:Heap_stats中会显示当前内存的空闲比例以及使用情况(活动对象所占内存 / 当前程序总内存)

最后:Pause_time表示这次GC操作导致应用程序暂停的时间

注意: 2.3之前GC操作是不能并发进行的,后面的版本可以并发。几乎感觉不到GC阻塞程序的时间。

避免创建不必要的对象

场景:

  • 如果我们有一个需要拼接的字符串,那么可以优先考虑使用StringBuffer或者StringBuilder来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。

  • 在没有特殊原因的情况下,尽量使用基本数据类来代替封装数据类型int比Integer要更加高效,其它数据类型也是一样。

  • 当一个方法的返回值是String的时候,通常可以去判断一下这个String的作用是什么,如果我们明确地知道调用方会将这个返回的String再进行拼接操作的话,可以考虑返回一个StringBuffer对象来代替因为这样可以将一个对象的引用进行返回,而返回String的话就是创建了一个短生命周期的临时对象

  • 正如前面所说,基本数据类型要优于对象数据类型,类似地,基本数据类型的数组也要优于对象数据类型的数组。另外,两个平行的数组要比一个封装好的对象数组更加高效,举个例子,Foo[]和Bar[]这样的两个数组,使用起来要比Custom(Foo,Bar)[]这样的一个数组高效得多

静态优于抽象

如果你并不需要访问一个对象中的某些字段,只是想调用它的某个方法来去完成一项通用的功能,那么可以将这个方法设置成静态方法,这会让调用的速度提升15%-20%。这样就避免去创建一个对象了。

对常量使用static final修饰符

这种优化方式只对基本数据类型以及String类型的常量有效对于其它数据类型的常量是无效的
但是,对于任何常量都是用static final的关键字来进行声明仍然是一种非常好的习惯.

使用增强型for循环语法

  • mArray.length 最慢 这是因为每次比较都需要计算一个length
  • len 记录数组的长度 最快
  • foreach 中间

多使用系统封装好的API

我们在编写程序时如果可以使用系统提供的API就应该尽量使用,系统提供的API完成不了我们需要的功能时才应该自己去写,因为使用系统的API在很多时候比我们自己写的代码要快得多,它们的很多功能都是通过底层的汇编模式执行的.
例如:

使用循环的方式来对数组中的每一个元素一一进行赋值当然是可行的,但是如果我们直接使用系统中提供的System.arraycopy()方法将会让执行效率快9倍以上

避免在内部调用Getters/Setters方法

在Android上这个技巧就不再是那么的受推崇了,因为字段搜寻要比方法调用效率高得多,我们直接访问某个字段可能要比通过getters方法来去访问这个字段快3到7倍不过我们肯定不能仅仅因为效率的原因就将封装这个技巧给抛弃了,编写代码还是要按照面向对象思维的,但是我们可以在能优化的地方进行优化,比如说避免在内部调用getters/setters方法

1
2
3
4
5
6
7
public int getSum() {
return getOne() + getTwo();
}
# 应该写为这种形式:
public int getSum() {
return one + two;
}

加快应用的启动速度

参考引用来源:Android性能优化之加快应用启动速度

启动方式

  1. 冷启动:当启动应用时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用,这个启动方式就是冷启动。
  2. 热启动:当启动应用时,后台已有该应用的进程(例:按back键、home键,应用虽然会退出,但是该应用的进程是依然会保留在后台,可进入任务列表查看),所以在已有进程的情况下,这种启动会从已有的进程中来启动应用,这个方式叫热启动。
特点:
  1. 冷启动:冷启动因为系统会重新创建一个新的进程分配给它,所以会先创建和初始化Application类,再创建和初始化MainActivity类(包括一系列的测量、布局、绘制),最后显示在界面上。
  2. 热启动:热启动因为会从已有的进程中来启动,所以热启动就不会走Application这步了,而是直接走MainActivity(包括一系列的测量、布局、绘制),所以热启动的过程只需要创建和初始化一个MainActivity就行了,而不必创建和初始化Application,因为一个应用从新进程的创建到进程的销毁,Application只会初始化一次
采取的策略
  1. 在Application的构造器方法、attachBaseContext()、onCreate()方法中不要进行耗时操作的初始化,一些数据预取放在异步线程中,可以采取Callable实现
  2. 对于sp的初始化,因为sp的特性在初始化时候会对数据全部读出来存在内存中,所以这个初始化放在主线程中不合适,反而会延迟应用的启动速度,对于这个还是需要放在异步线程中处理。
  3. 对于MainActivity,由于在获取到第一帧前,需要对contentView进行测量布局绘制操作,尽量减少布局的层次,考虑StubView的延迟加载策略,当然在onCreate、onStart、onResume方法中避免做耗时操作。
针对第一帧的处理

对于应用的启动时间,只能是尽量的避免一些耗时的、非必要的操作在主线程中,这样相对可以缩减一部分启动的耗时,另外一方面在等待第一帧显示的时间里,可以加入一些配置以增加体验,比如加入Activity的background,这个背景会在显示第一帧前提前显示在界面上

针对SplashActivity的优化

参考引用资料:Android性能优化之Splash页应该这样设计

SplashActivityMainActivity进行合并的方式:

一开始还是显示MainActivity,SplashActivity变为SplashFragment,然后放一个FrameLayout作为根布局去显示SplashFragment界面,这样在SplashFragment显示时候利用显示的2~4s间的空隙时间做网络请求去加载数据,这样待SplashFragment显示完后再remove,这样将看到的是有内容的MainActivity,就不必再去等待网络请求去返回数据了

load Splash View和ContentView合二为一了一起加载的方式:
可能会影响应用的启动时间,这时我们可以用ViewStub延迟加载MainActivity中某些View从而减去这个影响。
即:使用ViewStub在SplashFragment显示时再进行加载额外的View

防止内存抖动

所谓的内存抖动,就是短时间产生大量的对象,导致gc回收率降低,无法回收对象.很可能会造成界面卡顿.

措施
1、可以使用对象池来管理对象,减少对象创建的次数,在使用完成之后再手动释放对象池中的对象
2、不要在for、while等循环体中执行对象的创建
3、避免在onDraw()方法中执行对象的创建
4、采用预分配策略来减少一次性创建大量的数据

预分配:就是在程序刚启动的时候就事先创建一些即将要使用到的数据,这样可以在需要使用到这些数据的时候提供更快的加载速度,这种行为就叫做预分配

资料:Android性能优化典例(一)

Toast,常用使用类的优化

  • 对于吐司-Toast:

    使用getApplicationContext()来代替当前Activity的Context.

  • 属性设置:

    1
    android:animateLayoutChanges="true"

ViewGroup中设置这个属性。当视图的添加和删除就有默认的动画效果了,使得界面变换更加平滑.

  • listview为空时显示默认view
    1
    mListView.setEmptyView(View view);

资料引用:Android性能优化之界面UI篇

优化工具的使用

继续查看:Android最佳性能实践(二)——分析内存的使用情况

使用Systrace工具进行优化
给 App 提速:Android 性能优化总结

TraceView工具使用:
正确使用Android性能分析工具——TraceView

【凯子哥带你学Android】Andriod性能优化之列表卡顿——以“简书”APP为例

资料:

Android最佳性能实践(一)——合理管理内存
Android最佳性能实践(二)——分析内存的使用情况
Android最佳性能实践(三)——高性能编码优化

Android之SharedPreferences内部原理浅析