Android 性能优化

来源:互联网 发布:淘宝卖家插件推荐 编辑:程序博客网 时间:2024/06/05 03:18

这两天准备换工作,看了一下招聘条件,很多任职要求里有这样的需求,即熟悉Android内存管理机制,以及相应的处理方法。因此在这里做出汇总,以做记录和参考。里面很多都是从书籍或者博客中以及Android文档中摘录过来的。

一、内存管理机制

很多人其实平时在项目中都有接触到内存管理的相关代码,但是并不了解这样做的目的是什么。

Android内存管理,主要分为内存分配与内存回收。

1. 内存分配

Android采用弹性方式来分配内存,即刚开始不会分配太多内存给一个进程,而是以够用为标准,这样避免了内存的浪费。这个标准因设备的内存大小而不同。之后如果内存不够使用了,就会再次分配一定量的内存。

这样做是为了能够节省内存,让更多的已经启动的进程存活在内存中。后面用户再次启动应用,系统就不用重新创建进程,只要恢复已有的进程即可,减少了应用启动时间,提高用户体验。关于这点,其实大家在使用的app的时候,都会发现,如果你刚刚安装或者是刚刚开机之后,启动应用的时间会很长。但是,如果你退出或者切换出去之后,会发现,启动的时间会明显变短,原理大致就在于此。

2. 内存回收

Android继承了Linux的优点,会尽最大限度使用内存。当系统发现内存不足时会,就会选择kill掉进程了,那么按照什么规则kill呢?Android给进程的重要性分了5个层次结构(具体参考我的另一篇博客Android进程和线程)。分别是:

  1. 前台进程,正在与用户交互的Activity或者是调用了startForeground()的service。这个进程一般不会被系统kill,除非内存不足以继续运行该进程了。
  2. 可见进程,未与用户交互,但是仍可见的进程,比如启动了dialog之后的activity或者是绑定到前台进程的service。该级别下,只有为了维持前台进程不得不关闭情况才会被kill。
  3. 服务进程,运行了service(但不属于上面的两个)的进程。只有为了维持前台和可见进程运行才会杀死该类进程。
  4. 后台进程,运行了onStop方法之后的activity所在进程,因为不用于用户交互也不必被看见,因此系统可以随时终止它们。一般,会有很多后台进程,保存在LRU列表中。
  5. 空进程,不含任何活动组件的进程。这种进程的唯一目的是用作缓存以缩短下次在其中运行组件所需的启动时间。为使系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

除此之外,系统也会根据回收收益来选择杀死LRU列表中能回收更多内存的进程。杀死越少进程,对用户体验影响越小。

二、内存优化

这里我先提前唠叨两句,我个人认为,对于我们开发者,主要就是做三件事,防止内存泄漏,防止内存溢出以及缩短响应时间。至于具体的建议做法,也都是按照这三条原则来玩的。(PS:个人观点)。

而且很多问题在我们使用AndroidStudio时,会有lint出现。写完代码看一下哪里有黄色的警告就知道了。

1. 布局优化

要点:少嵌套(为了减少加载时间,过多的嵌套会导致加载时间过久,影响体验)

  • 多使用LinearLayout和RelativeLayout;
  • <include> 标签,这个应该算不上是性能优化,就是减少大家不必要的浪费时间敲代码;
  • <merge>标签,这个是防止相同容器(相同属性)的嵌套,比如FrameLayout等,也是为了减少嵌套,一般与include标签配合使用。
  • ViewStub是一个View,宽高都是0,因此不参与layout和onDraw,目的是为了延迟加载,在我们需要的时候使用setVisibility或者inflate()方法显示即可。最后这个虽说不是少嵌套,但是也是为了能有更快的界面加载,提升体验。

2. 自定义View的优化

简单来说就是不要在频繁回调的方法中创建新的对象,比如onDraw,onMeasure以及ViewGroup的onLayout方法。因为做静态的图还好,只用绘制一次即可,如果绘制动画,可能就要不断循环的调用onDraw了,而谷歌官方的建议是,每帧绘制时间不超过16ms,因此要尽量避免。

3. 内存泄漏优化

  • 静态变量,比如在Activity中使用静态变量引用Context或者是view持有当前Activity引用,都是一样的,都会导致Activity无法回收。这种都很简单,稍微有点常识的都不会这么干。

  • 单例模式,这是我从《Android开发艺术探索上》摘下来的,意思是,如果单例模式持有Activity的引用会导致内存泄漏。其实很多开源框架比如ButterKnife和EventBus等,都是采用在onCreate中注册(bind、regist等),但是也都会提供解注册方法(unbind,unregister等)在onDestroy中解注册。这样就避免单例模式持有Activity引用导致的Activity无法回收。当然我们也可以引申到别的地方,比如我们使用异步任务执行网络访问,如果Activity已经finish了,我们请求结果很久才回调,这样也会造成内存泄漏。因此我们也可以提供绑定与解绑的方法。也可以使用弱引用的方式来避免泄漏。

  • 属性动画导致的内存泄漏。主要就是View会被动画持有,而View又持有Activity,导致Activity无法回收。比如一个无限循环的动画,如果不停止,就会一直持有引用。可以使用animator.cancel()来停止动画。

当然这里也要可以使用MAT等工具进行内存泄漏分析。我用过的一个工具是LeakCanary。

4. ANR

这个很常见,解决方法也很简单,就是在主线程少干点事。

5. ListView优化

  • 使用ViewHolder模式
  • 异步加载
  • 根据滑动状态来控制任务执行频率(比如滑动时暂停加载图片,停止时开始加载)。
  • 硬件加速

6. Bitmap优化

压缩与缓存(内存和磁盘,LRU),还有recycle。不赘述了。

7. 线程优化

也很简单,避免大量new Thread,采用线程池来搞。很多开源异步框架都是采用线程池来实现的,这里就不多说了。

8. 依赖注入框架

这点是我考虑之后加上的。就是关于java的反射机制,这个也是一个性能问题。一般都是注解用到的,但是目前很多的框架使用的是编译时注解,在代码中只是进行反射查找类然后进行注入。如果是用的运行时注解,要对代码进行扫描来获取指定的注解,这样效率就不高了。编译时注解,原理是在编译期间就生成注入的java文件,之后在运行时获取该文件进行注入就可以了,不需要进行扫描等等操作。

三、其他优化建议

  • 不要过多使用枚举,枚举比整形占内存;
  • 使用Adroid特有数据结构,如SpareseArray和Pari等,性能更好;
  • 适当使用软引用和弱引用;
  • 内存缓存与磁盘缓存;
  • 电池使用优化,如果是充电状态,随便花。。。
  • 网络优化

下面是java上的优化建议(从Android文档中摘录),只是微建议,不会有显著的效果提升,这里不涉及算法以及数据结构。两条原则:不必要的事情别做,尽量避免内存分配。

  • 避免创建不必要的对象,通常避免创建短暂的临时对象,会减少GC,对用户体验有直接影响。关于这一点,插一句,设计模式中有个享元模式,就是对于常用的同一个类型的对象不必过多的创建,可以缓存之前使用过的对象。比如Handler机制中的Message,可以通过obtainMessage获取对象,其实就是利用了缓存的Message对象。Message使用链表结构,如果被处理过了,就会被回收到缓存列表里。
    • 如果你有个方法的返回值为string,且这个string总是被append到StringBuilder,你可以将方法直接改为拼接,而不是创建临时对象。
    • 当从一组输入数据中提取字符串时,尽量返回原字符串的子串,而不是创建副本。创建新String对象但它仍然与前者享有共同的char[]。
    • 将多维数组切割成平行的多个一维数组。int类型的数组比Integer对象数组更好,因此俩并行的int数组也比(int, int)对象数组更加有效。当然其它基本类型也一样。
    • 在自己的代码中尽量使用Foo[],Bar[]数组而不是(Foo,Bar)数组,当然为别人设计接口时则按需来。
  • 常量用static final;
  • 如果不需要获取对象的字段,则将方法static,调用速度提升15%-20%;
  • 避免使用内部getter/setter方法(仅针对Android,java、C++等语言使用方法更好),因为Android中方法调用比字段查找费时。没有JIT的情况下,直接字段访问比调用getter方法快3倍,而使用JIT,直接字段访问比getter块7倍;
  • 使用foreach,对于ArrayList,比手写计数循环大约快3倍(有或没有JIT无关),但对于其他集合,增强型for循环语法将完全等同于显式迭代器使用
  • 包内访问而不是内部类
  • 避免使用浮点数,Android中浮点数比整数慢两倍,在速度方面,在强大的硬件下看不出float与double的区别;控件方面,double占内存是float的两倍,但是如果是桌面应用,更倾向于用double。对于整数,一些处理器具有硬件乘法,没有硬件除法,这种情况下整数除法与模运算都在软件中执行。
  • 使用library,一些内置的库方法比自己一般实现的要快,比如System.copy()比使用JIT的手动编码循环快约9倍,有些方法基于native实现更加快速。
  • 小心使用native方法,有时候使用native并不比java快,且调用native花时间,而且有时候要对应不同cpu架构编译不同的代码库,apk体积增大。
  • 在没有JIT设备上,通过具体确切类型比接口调用方法更有效,但是仅仅只有6%的速度差距。没有JIT,缓存字段访问比重复字段访问快约20%,有则访问速度相差不大,因此并不值得去优化。

注:JIT,即时编译器

参考:
Android中的内存管理机制以及正确的使用方式
《Android开发艺术探索》
Android官方文档