标签: Android内存优化

Android内存优化4 了解java GC 垃圾回收机制2 GC执行finalize的过程

Android内存优化4 了解java GC 垃圾回收机制2 GC执行finalize的过程

1. finalize的作用

 

  • finalize()是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法。
  • finalize()与C++中的析构函数不是对应的。C++中的析构函数调用的时机是确定的(对象离开作用域或delete掉),但Java中的finalize的调用具有不确定性
  • 不建议用finalize方法完成“非内存资源”的清理工作,但建议用于:① 清理本地对象(通过JNI创建的对象);② 作为确保某些非内存资源(如Socket、文件等)释放的一个补充:在finalize方法中显式调用其他资源释放方法。其原因可见下文[finalize的问题]

2. finalize的问题

  • 一些与finalize相关的方法,由于一些致命的缺陷,已经被废弃了,如System.runFinalizersOnExit()方法、Runtime.runFinalizersOnExit()方法
  • System.gc()与System.runFinalization()方法增加了finalize方法执行的机会,但不可盲目依赖它们
  • Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行
  • finalize方法可能会带来性能问题。因为JVM通常在单独的低优先级线程中完成finalize的执行
  • 对象再生问题:finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的
  • finalize方法至多由GC执行一次(用户当然可以手动调用对象的finalize方法,但并不影响GC对finalize的行为)

3. finalize的执行过程(生命周期)

 

(1) 首先,大致描述一下finalize流程:当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。
(2) 具体的finalize流程:
对象可由两种状态,涉及到两类状态空间,一是终结状态空间 F = {unfinalized, finalizable, finalized};二是可达状态空间 R = {reachable, finalizer-reachable, unreachable}。各状态含义如下:
  • unfinalized: 新建对象会先进入此状态,GC并未准备执行其finalize方法,因为该对象是可达的
  • finalizable: 表示GC可对该对象执行finalize方法,GC已检测到该对象不可达。正如前面所述,GC通过F-Queue队列和一专用线程完成finalize的执行
  • finalized: 表示GC已经对该对象执行过finalize方法
  • reachable: 表示GC Roots引用可达
  • finalizer-reachable(f-reachable):表示不是reachable,但可通过某个finalizable对象可达
  • unreachable:对象不可通过上面两种途径可达
状态变迁图:
%title插图%num
变迁说明:
  1. 新建对象首先处于[reachable, unfinalized]状态(A)
  2. 随着程序的运行,一些引用关系会消失,导致状态变迁,从reachable状态变迁到f-reachable(B, C, D)或unreachable(E, F)状态
  3. 若JVM检测到处于unfinalized状态的对象变成f-reachable或unreachable,JVM会将其标记为finalizable状态(G,H)。若对象原处于[unreachable, unfinalized]状态,则同时将其标记为f-reachable(H)。
  4. 在某个时刻,JVM取出某个finalizable对象,将其标记为finalized并在某个线程中执行其finalize方法。由于是在活动线程中引用了该对象,该对象将变迁到(reachable, finalized)状态(K或J)。该动作将影响某些其他对象从f-reachable状态重新回到reachable状态(L, M, N)
  5. 处于finalizable状态的对象不能同时是unreahable的,由第4点可知,将对象finalizable对象标记为finalized时会由某个线程执行该对象的finalize方法,致使其变成reachable。这也是图中只有八个状态点的原因
  6. 程序员手动调用finalize方法并不会影响到上述内部标记的变化,因此JVM只会至多调用finalize一次,即使该对象“复活”也是如此。程序员手动调用多少次不影响JVM的行为
  7. 若JVM检测到finalized状态的对象变成unreachable,回收其内存(I)
  8. 若对象并未覆盖finalize方法,JVM会进行优化,直接回收对象(O)
  9. 注:System.runFinalizersOnExit()等方法可以使对象即使处于reachable状态,JVM仍对其执行finalize方法

4. 一些代码示例

(1) 对象复活

  1. publicclass publicstaticnull publicstaticvoidthrows new null );
  2. ifnull
  3. else null );
  4. ifnull else
  5. protectedvoidthrows super this }

(2)覆盖finalize方法以确保资源释放

作为一个补充操作,以防用户忘记“关闭“资源,JDK中FileInputStream、FileOutputStream、Connection类均用了此”技术“,下面代码摘自FileInputStream类

  1. /**
  2.  * Ensures that the <code>close</code> method of this file input stream is
  3.  * called when there are no more references to it.
  4.  *
  5.  * @exception  IOException  if an I/O error occurs.
  6.  * @see        java.io.FileInputStream#close()
  7.  */ protectedvoidthrows ifnull          * will ensure that finalizer is only called when
  8.          * safe to do so. All references using the fd have
  9.          * become unreachable. We can call close()
  10.          */ }

Android内存优化 了解java内存分配

Android内存优化-了解java内存分配

JVM内存模型

Java虚拟机(Java Virtual Machine=JVM)的内存空间分为五个部分,分别是:
1. 程序计数器
2. Java虚拟机栈
3. 本地方法栈
4. 堆
5. 方法区。

下面对这五个区域展开深入的介绍。

1. 程序计数器

1.1. 什么是程序计数器?

程序计数器是一块较小的内存空间,可以把它看作当前线程正在执行的字节码的行号指示器。也就是说,程序计数器里面记录的是当前线程正在执行的那一条字节码指令的地址。
注:但是,如果当前线程正在执行的是一个本地方法,那么此时程序计数器为空。

1.2. 程序计数器的作用

程序计数器有两个作用:

  1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。
  2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。

1.3. 程序计数器的特点

  1. 是一块较小的存储空间
  2. 线程私有。每条线程都有一个程序计数器。
  3. 是唯一一个不会出现OutOfMemoryError的内存区域。
  4. 生命周期随着线程的创建而创建,随着线程的结束而死亡。

2. Java虚拟机栈(JVM Stack)

2.1. 什么是Java虚拟机栈?

Java虚拟机栈是描述Java方法运行过程的内存模型。
Java虚拟机栈会为每一个即将运行的Java方法创建一块叫做“栈帧”的区域,这块区域用于存储该方法在运行过程中所需要的一些信息,这些信息包括:

  1. 局部变量表
    存放基本数据类型变量、引用类型的变量、returnAddress类型的变量。
  2. 操作数栈
  3. 动态链接
  4. 方法出口信息

当一个方法即将被运行时,Java虚拟机栈首先会在Java虚拟机栈中为该方法创建一块“栈帧”,栈帧中包含局部变量表、操作数栈、动态链接、方法出口信息等。当方法在运行过程中需要创建局部变量时,就将局部变量的值存入栈帧的局部变量表中。
当这个方法执行完毕后,这个方法所对应的栈帧将会出栈,并释放内存空间。

注意:人们常说,Java的内存空间分为“栈”和“堆”,栈中存放局部变量,堆中存放对象。
这句话不完全正确!这里的“堆”可以这么理解,但这里的“栈”只代表了Java虚拟机栈中的局部变量表部分。真正的Java虚拟机栈是由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法出口信息。

2.2. Java虚拟机栈的特点

  1. 局部变量表的创建是在方法被执行的时候,随着栈帧的创建而创建。而且,局部变量表的大小在编译时期就确定下来了,在创建的时候只需分配事先规定好的大小即可。此外,在方法运行的过程中局部变量表的大小是不会发生改变的。
  2. Java虚拟机栈会出现两种异常:StackOverFlowError和OutOfMemoryError。
    a) StackOverFlowError:
    若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的*大深度的时候,就抛出StackOverFlowError异常。
    b) OutOfMemoryError:
    若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。
  3. Java虚拟机栈也是线程私有的,每个线程都有各自的Java虚拟机栈,而且随着线程的创建而创建,随着线程的死亡而死亡。

注:StackOverFlowError和OutOfMemoryError的异同?
StackOverFlowError表示当前线程申请的栈超过了事先定好的栈的*大深度,但内存空间可能还有很多。
而OutOfMemoryError是指当线程申请栈时发现栈已经满了,而且内存也全都用光了。

3. 本地方法栈

3.1. 什么是本地方法栈?

本地方法栈和Java虚拟机栈实现的功能类似,只不过本地方法区是本地方法运行的内存模型。

本地方法被执行的时候,在本地方法栈也会创建一个栈帧,用于存放该本地方法的局部变量表、操作数栈、动态链接、出口信息。

方法执行完毕后相应的栈帧也会出栈并释放内存空间。

也会抛出StackOverFlowError和OutOfMemoryError异常。

4. 堆

4.1. 什么是堆?

堆是用来存放对象的内存空间。
几乎所有的对象都存储在堆中。

4.2. 堆的特点

  1. 线程共享
    整个Java虚拟机只有一个堆,所有的线程都访问同一个堆。而程序计数器、Java虚拟机栈、本地方法栈都是一个线程对应一个的。
  2. 在虚拟机启动时创建
  3. 垃圾回收的主要场所。
  4. 可以进一步细分为:新生代、老年代。
    新生代又可被分为:Eden、From Survior、To Survior。
    不同的区域存放具有不同生命周期的对象。这样可以根据不同的区域使用不同的垃圾回收算法,从而更具有针对性,从而更高效。
  5. 堆的大小既可以固定也可以扩展,但主流的虚拟机堆的大小是可扩展的,因此当线程请求分配内存,但堆已满,且内存已满无法再扩展时,就抛出OutOfMemoryError。

5. 方法区

5.1. 什么是方法区?

Java虚拟机规范中定义方法区是堆的一个逻辑部分。
方法区中存放已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等。

5.2. 方法区的特点

  1. 线程共享
    方法区是堆的一个逻辑部分,因此和堆一样,都是线程共享的。整个虚拟机中只有一个方法区。
  2. 永久代
    方法区中的信息一般需要长期存在,而且它又是堆的逻辑分区,因此用堆的划分方法,我们把方法区称为老年代。
  3. 内存回收效率低
    方法区中的信息一般需要长期存在,回收一遍内存之后可能只有少量信息无效。
    对方法区的内存回收的主要目标是:对常量池的回收 和 对类型的卸载。
  4. Java虚拟机规范对方法区的要求比较宽松。
    和堆一样,允许固定大小,也允许可扩展的大小,还允许不实现垃圾回收。

5.3. 什么是运行时常量池?

方法区中存放三种数据:类信息、常量、静态变量、即时编译器编译后的代码。其中常量存储在运行时常量池中。

我们一般在一个类中通过public static final来声明一个常量。这个类被编译后便生成Class文件,这个类的所有信息都存储在这个class文件中。

当这个类被Java虚拟机加载后,class文件中的常量就存放在方法区的运行时常量池中。而且在运行期间,可以向常量池中添加新的常量。如:String类的intern()方法就能在运行期间向常量池中添加字符串常量。

当运行时常量池中的某些常量没有被对象引用,同时也没有被变量引用,那么就需要垃圾收集器回收。

6. 直接内存

直接内存是除Java虚拟机之外的内存,但也有可能被Java使用。

在NIO中引入了一种基于通道和缓冲的IO方式。它可以通过调用本地方法直接分配Java虚拟机之外的内存,然后通过一个存储在Java堆中的DirectByteBuffer对象直接操作该内存,而无需先将外面内存中的数据复制到堆中再操作,从而提升了数据操作的效率。

直接内存的大小不受Java虚拟机控制,但既然是内存,当内存不足时就会抛出OOM异常。

综上所述

      1. Java虚拟机的内存模型中一共有两个“栈”,分别是:Java虚拟机栈和本地方法栈。
        两个“栈”的功能类似,都是方法运行过程的内存模型。并且两个“栈”内部构造相同,都是线程私有。
        只不过Java虚拟机栈描述的是Java方法运行过程的内存模型,而本地方法栈是描述Java本地方法运行过程的内存模型。
      2. Java虚拟机的内存模型中一共有两个“堆”,一个是原本的堆,一个是方法区。方法区本质上是属于堆的一个逻辑部分。堆中存放对象,方法区中存放类信息、常量、静态变量、即时编译器编译的代码。
      3. 堆是Java虚拟机中*大的一块内存区域,也是垃圾收集器主要的工作区域。
      4. 程序计数器、Java虚拟机栈、本地方法栈是线程私有的,即每个线程都拥有各自的程序计数器、Java虚拟机栈、本地方法区。并且他们的生命周期和所属的线程一样。
        而堆、方法区是线程共享的,在Java虚拟机中只有一个堆、一个方法栈。并在JVM启动的时候就创建,JVM停止才销毁。

Android内存优化总结&实践-腾讯Bugly干货分享

 

导语

智能手机发展到今天已经有十几个年头,手机的软硬件都已经发生了翻天覆地的变化,特别是Android阵营,从一开始的一两百M到今天动辄4G,6G内存。然而大部分的开发者观看下自己的异常上报系统,还是会发现各种内存问题仍然层出不穷,各种OOM为crash率贡献不少。Android开发发展到今天也是已经比较成熟,各种新框架,新技术也是层出不穷,而内存优化一直都是Android开发过程一个不可避免的话题。 恰好*近做了内存优化相关的工作,这里也对Android内存优化相关的知识做下总结。

在开始文章之前推荐下公司同事翻译整理版本《Android性能优化典范 – 第6季》,因为篇幅有限这里我对一些内容只做简单总结,同时如果有不正确内容也麻烦帮忙指正。

本文将会对Android内存优化相关的知识进行总结以及*后案例分析(一二部分是理论知识总结,你也可以直接跳到第三部分看案例):

一、 Android内存分配回收机制
二 、Android常见内存问题和对应检测,解决方式。
三、 JOOX内存优化案例
四 、总结

工欲善其事必先利其器,想要优化App的内存占用,那么还是需要先了解Android系统的内存分配和回收机制。

一 ,Android内存分配回收机制

参考Android 操作系统的内存回收机制[1],这里简单做下总结:

从宏观角度上来看Android系统可以分为三个层次
1. Application Framework,
2. Dalvik 虚拟机
3. Linux内核。

这三个层次都有各自内存相关工作:

1. Application Framework

Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:

%title插图%num

  • Empty process(空进程)
  • Background process(后台进程)
  • Service process(服务进程)
  • Visible process(可见进程)
  • Foreground process(前台进程)

系统需要进行内存回收时*先回收空进程,然后是后台进程,以此类推*后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。

由此也衍生了很多进程保活的方法(提高优先级,互相唤醒,native保活等等),出现了国内各种全家桶,甚至各种杀不死的进程。

Android中由ActivityManagerService 集中管理所有进程的内存资源分配。

2. Linux内核

%title插图%num

参考QCon大会上阿里巴巴的Android内存优化分享[2],这里*简单的理解就是ActivityManagerService会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收(lowmemorykillerOom_killer)。这里只是大概的流程,中间过程还是很复杂的,有兴趣的同学可以一起研究,代码在系统源码ActivityManagerService.Java中。

3. Dalvik虚拟机

Android进程的内存管理分析[3],对Android中进程内存的管理做了分析。

Android中有Native Heap和Dalvik Heap。Android的Native Heap言理论上可分配的空间取决了硬件RAM,而对于每个进程的Dalvik Heap都是有大小限制的,具体策略可以看看android dalvik heap 浅析[4]。

Android App为什么会OOM呢?其实就是申请的内存超过了Dalvik Heap的*大值。这里也诞生了一些比较”黑科技”的内存优化方案,比如将耗内存的操作放到Native层,或者使用分进程的方式突破每个进程的Dalvik Heap内存限制。

Android Dalvik Heap与原生Java一样,将堆的内存空间分为三个区域,Young Generation,Old Generation, Permanent Generation。

%title插图%num

*近分配的对象会存放在Young Generation区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation,*后累积一定时间再移动到Permanent Generation区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。

GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是*短的,Old Generation其次,Permanent Generation*长。

GC时会导致线程暂停,导致卡顿,Google在新版本的Android中优化了这个问题, 在ART中对GC过程做了优化揭秘 ART 细节 —— Garbage collection[5],据说内存分配的效率提高了10倍,GC的效率提高了2-3倍(可见原来效率有多低),不过主要还是优化中断和阻塞的时间,频繁的GC还是会导致卡顿。

上面就是Android系统内存分配和回收相关知识,回过头来看,现在各种手机厂商鼓吹人工智能手机,号称18个月不卡顿,越用越快,其实很大一部分Android系统的内存优化有关,无非就是利用一些比较成熟的基于统计,机器学习的算法定时清理数据,清理内存,甚至提前加载数据到内存。

二 ,Android常见内存问题和对应检测,解决方式

1. 内存泄露

不止Android程序员,内存泄露应该是大部分程序员都遇到过的问题,可以说大部分的内存问题都是内存泄露导致的,Android里也有一些很常见的内存泄露问题[6],这里简单罗列下:

  • 单例(主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放)
  • 静态变量(同样也是因为生命周期比较长)
  • Handler内存泄露[7]
  • 匿名内部类(匿名内部类会引用外部类,导致无法释放,比如各种回调)
  • 资源使用完未关闭(BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap)

对Android内存泄露业界已经有很多优秀的组件其中LeakCanary*为知名(Square出品,Square可谓Android开源界中的业界良心,开源的项目包括okhttp, retrofit,otto, picasso, Android开发大神Jake Wharton就在Square),其原理是监控每个activity,在activity ondestory后,在后台线程检测引用,然后过一段时间进行gc,gc后如果引用还在,那么dump出内存堆栈,并解析进行可视化显示。使用LeakCanary可以快速地检测出Android中的内存泄露。

正常情况下,解决大部分内存泄露问题后,App稳定性应该会有很大提升,但是有时候App本身就是有一些比较耗内存的功能,比如直播,视频播放,音乐播放,那么我们还有什么能做的可以降低内存使用,减少OOM呢?

2. 图片分辨率相关

分辨率适配问题。很多情况下图片所占的内存在整个App内存占用中会占大部分。我们知道可以通过将图片放到hdpi/xhdpi/xxhdpi等不同文件夹进行适配,通过xml android:background设置背景图片,或者通过BitmapFactory.decodeResource()方法,图片实际上默认情况下是会进行缩放的。在Java层实际调用的函数都是或者通过BitmapFactory里的decodeResourceStream函数

public static Bitmap decodeResourceStream(Resources res, TypedValue value,
        InputStream is, Rect pad, Options opts) {

    if (opts == null) {
        opts = new Options();
    }

    if (opts.inDensity == 0 && value != null) {
        final int density = value.density;
        if (density == TypedValue.DENSITY_DEFAULT) {
            opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
        } else if (density != TypedValue.DENSITY_NONE) {
            opts.inDensity = density;
        }
    }

    if (opts.inTargetDensity == 0 && res != null) {
        opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
    }

    return decodeStream(is, pad, opts);
}

decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个*佳的显示效果,并且Bitmap的大小将比原始的大,可以参考下腾讯Bugly的详细分析Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?。

关于Density、分辨率、-hdpi等res目录之间的关系:

%title插图%num

举个例子,对于一张1280×720的图片,如果放在xhdpi,那么xhdpi的设备拿到的大小还是1280×720而xxhpi的设备拿到的可能是1920×1080,这两种情况在内存里的大小分别为:3.68M和8.29M,相差4.61M,在移动设备来说这几M的差距还是很大的。

尽管现在已经有比较先进的图片加载组件类似Glide,Facebook Freso, 或者老牌Universal-Image-Loader,但是有时就是需要手动拿到一个bitmap或者drawable,特别是在一些可能会频繁调用的场景(比如ListView的getView),怎样尽可能对bitmap进行复用呢?这里首先需要明确的是对同样的图片,要 尽可能复用,我们可以简单自己用WeakReference做一个bitmap缓存池,也可以用类似图片加载库写一个通用的bitmap缓存池,可以参考GlideBitmapPool[8]的实现。

我们也来看看系统是怎么做的,对于类似在xml里面直接通过android:background或者android:src设置的背景图片,以ImageView为例,*终会调用Resource.java里的loadDrawable:

Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {

    // Next, check preloaded drawables. These may contain unresolved theme
    // attributes.
    final ConstantState cs;
    if (isColorDrawable) {
        cs = sPreloadedColorDrawables.get(key);
    } else {
        cs = sPreloadedDrawables[mConfiguration.getLayoutDirection()].get(key);
    }

    Drawable dr;
    if (cs != null) {
        dr = cs.newDrawable(this);
    } else if (isColorDrawable) {
        dr = new ColorDrawable(value.data);
    } else {
        dr = loadDrawableForCookie(value, id, null);
    }

    ...

    return dr;
}  

可以看到实际上系统也是有一份全局的缓存,sPreloadedDrawables, 对于不同的drawable,如果图片时一样的,那么*终只会有一份bitmap(享元模式),存放于BitmapState中,获取drawable时,系统会从缓存中取出这个bitmap然后构造drawable。而通过BitmapFactory.decodeResource()则每次都会重新解码返回bitmap。所以其实我们可以通过context.getResources().getDrawable再从drawable里获取bitmap,从而复用bitmap,然而这里也有一些坑,比如我们获取到的这份bitmap,假如我们执行了recycle之类的操作,但是假如在其他地方再使用它是那么就会有”Canvas: trying to use a recycled bitmap android.graphics.Bitmap”异常。

3. 图片压缩

BitmapFactory 在解码图片时,可以带一个Options,有一些比较有用的功能,比如:

  • inTargetDensity 表示要被画出来时的目标像素密度
  • inSampleSize 这个值是一个int,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例(1 / inSampleSize)缩小bitmap的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如,width=100,height=100,inSampleSize=2,那么就会将bitmap处理为,width=50,height=50,宽高降为1 / 2,像素数降为1 / 4
  • inJustDecodeBounds 字面意思就可以理解就是只解析图片的边界,有时如果只是为了获取图片的大小就可以用这个,而不必直接加载整张图片。
  • inPreferredConfig 默认会使用ARGB_8888,在这个模式下一个像素点将会占用4个byte,而对一些没有透明度要求或者图片质量要求不高的图片,可以使用RGB_565,一个像素只会占用2个byte,一下可以省下50%内存。
  • inPurgeableinInputShareable 这两个需要一起使用,BitmapFactory.java的源码里面有注释,大致意思是表示在系统内存不足时是否可以回收这个bitmap,有点类似软引用,但是实际在5.0以后这两个属性已经被忽略,因为系统认为回收后再解码实际会反而可能导致性能问题
  • inBitmap 官方推荐使用的参数,表示重复利用图片内存,减少内存分配,在4.4以前只有相同大小的图片内存区域可以复用,4.4以后只要原有的图片比将要解码的图片大既可以复用了。

4. 缓存池大小

现在很多图片加载组件都不仅仅是使用软引用或者弱引用了,实际上类似Glide 默认使用的事LruCache,因为软引用 弱引用都比较难以控制,使用LruCache可以实现比较精细的控制,而默认缓存池设置太大了会导致浪费内存,设置小了又会导致图片经常被回收,所以需要根据每个App的情况,以及设备的分辨率,内存计算出一个比较合理的初始值,可以参考Glide的做法。

5. 内存抖动

什么是内存抖动呢?Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时还会导致OOM。

%title插图%num

一个很经典的案例是string拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的log的时候), 见Android优化之String篇[9]。

而内存抖动为什么会引起OOM呢?

主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回OOM了。

比如我们坐地铁的时候,假设你没带公交卡去坐地铁,地铁的售票机就只支持5元,10元,而哪怕你这个时候身上有1万张1块的都没用(是不是觉得很反人类..)。当然你可以去兑换5元,10元,而在Android系统里就没那么幸运了,系统会直接拒*为你分配内存,并扔一个OOM给你(有人说Android系统并不会对Heap中空闲内存区域做碎片整理,待验证)。

其他

常用数据结构优化,ArrayMap及SparseArray是android的系统API,是专门为移动设备而定制的。用于在一定情况下取代HashMap而达到节省内存的目的,具体性能见HashMap,ArrayMap,SparseArray源码分析及性能对比[10],对于key为int的HashMap尽量使用SparceArray替代,大概可以省30%的内存,而对于其他类型,ArrayMap对内存的节省实际并不明显,10%左右,但是数据量在1000以上时,查找速度可能会变慢。

枚举,Android平台上枚举是比较争议的,在较早的Android版本,使用枚举会导致包过大,在个例子里面,使用枚举甚至比直接使用int包的size大了10多倍 在stackoverflow上也有很多的讨论, 大致意思是随着虚拟机的优化,目前枚举变量在Android平台性能问题已经不大,而目前Android官方建议,使用枚举变量还是需要谨慎,因为枚举变量可能比直接用int多使用2倍的内存。

ListView复用,这个大家都知道,getView里尽量复用conertView,同时因为getView会频繁调用,要避免频繁地生成对象

谨慎使用多进程,现在很多App都不是单进程,为了保活,或者提高稳定性都会进行一些进程拆分,而实际上即使是空进程也会占用内存(1M左右),对于使用完的进程,服务都要及时进行回收。

尽量使用系统资源,系统组件,图片甚至控件的id

减少view的层级,对于可以 延迟初始化的页面,使用viewstub

数据相关:序列化数据使用protobuf可以比xml省30%内存,慎用shareprefercnce,因为对于同一个sp,会将整个xml文件载入内存,有时候为了读一个配置,就会将几百k的数据读进内存,数据库字段尽量精简,只读取所需字段。

dex优化,代码优化,谨慎使用外部库, 有人觉得代码多少于内存没有关系,实际会有那么点关系,现在稍微大一点的项目动辄就是百万行代码以上,多dex也是常态,不仅占用rom空间,实际上运行的时候需要加载dex也是会占用内存的(几M),有时候为了使用一些库里的某个功能函数就引入了整个庞大的库,此时可以考虑抽取必要部分,开启proguard优化代码,使用Facebook redex使用优化dex(好像有不少坑)。

三 案例

JOOX是IBG一个核心产品,2014年发布以来已经成为5个国家和地区排名*的音乐App。东南亚是JOOX的主要发行地区,实际上这些地区还是有很多的低端机型,对App的进行内存优化势在必行。

上面介绍了Android系统内存分配和回收机制,同时也列举了常见的内存问题,但是当我们接到一个内存优化的任务时,我们应该从何开始?下面是一次内存优化的分享。

1. 首先是解决大部分内存泄露。

不管目前App内存占用怎样,理论上不需要的东西*好回收,避免浪费用户内存,减少OOM。实际上自JOOX接入LeakCanary后,每个版本都会做内存泄露检测,经过几个版本的迭代,JOOX已经修复了几十处内存泄露。

%title插图%num

2. 通过MAT查看内存占用,优化占用内存较大的地方。

JOOX修复了一系列内存泄露后,内存占用还是居高不下,只能通过MAT查看到底是哪里占用了内存。关于MAT的使用,网上教程无数,简单推荐两篇MAT使用教程[11],MAT – Memory Analyzer Tool 使用进阶[12]。

点击Android Studio这里可以dump当前的内存快照,因为直接通过Android Sutdio dump出来的hprof文件与标准hprof文件有些差异,我们需要手动进行转换,利用sdk目录/platform-tools/hprof-conv.exe可以直接进行转换,用法:hprof-conv 原文件.hprof 新文件.hprof。只需要输入原文件名还有目标文件名就可以进行转换,转换完就可以直接用MAT打开。

%title插图%num

下面就是JOOX打开App,手动进行多次gc的hprof文件。

这里我们看的是Dominator Tree(即内存里占用内存*多的对象列表)。

%title插图%num

  • Shallo Heap:对象本身占用内存的大小,不包含其引用的对象内存。
  • Retained Heap: Retained heap值的计算方式是将retained set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。

*眼看去 居然有3个8M的对象,加起来就是24M啊 这到底是什么鬼?

%title插图%num

我们通过List objects->with incoming references查看(这里with incoming references表示查看谁引用了这个对象,with outgoing references表示这个对象引用了谁)

%title插图%num

通过这个方式我们看到这三张图分别是闪屏,App主背景,App抽屉背景。

%title插图%num

这里其实有两个问题:

  • 这几张图原图实际都是1280×720,而在1080p手机上实测这几张图都缩放到了1920×1080
  • 闪屏页面,其实这张图在闪屏显示过后应该可以回收,但是因为历史原因(和JOOX的退出机制有关),这张图被常驻在后台,导致无谓的内存占用。

优化方式:我们通过将这三张图从xhdpi挪动到xxhdpi(当然这里需要看下图片显示效果有没很大的影响),以及在闪屏显示过后回收闪屏图片。
优化结果:

%title插图%num

从原来的8.29×3=24.87M 到 3.68×2=7.36M 优化了17M(有没一种万马奔腾的感觉。。可能有时费大力气优化很多代码也优化不了几百K,所以很多情况下内存优化时优化图片还是比较立竿见影的)。

同样方式我们发现对于一些默认图,实际要求的显示要求并不高(图片相对简单,同时大部分情况下图片加载会成功),比如下面这张banner的背景图:

%title插图%num

优化前1.6M左右,优化后700K左右。

同时我们也发现了默认图片一个其他问题,因为历史原因,我们使用的图片加载库,设置默认图片的接口是需要一个bitmap,导致我们原来几乎每个adapter都用BitmapFactory decode了一个bitmap,对同一张默认图片,不但没有复用,还保存了多份,不仅会造成内存浪费,而且导致滑动偶尔会卡顿。这里我们也对默认图片使用全局的bitmap缓存池,App全局只要使用同一张bitmap,都复用了同一份。

另外对于从MAT里看到的图片,有时候因为看不到在项目里面对应的ID,会比较难确认到底是哪一张图,这里stackoverflow上有一种方法,直接用原始数据通过GIM还原这张图片。

这里其实也看到JOOX比较吃亏一个地方,JOOX不少地方都是使用比较复杂的图片,同时有些地方还需要模糊,动画这些都是比较耗内存的操作,Material Design出来后,很多App都遵循MD设计进行改版,通常默认背景,默认图片一般都是纯色,不仅App看起来比较明亮轻快,实际上也省了很多的内存,对此,JOOX后面对低端机型做了对应的优化。

3. 我们也对RDM上的OOM进行了分析,发现其实有些OOM是可以避免的。

下面这个crash就是上面提到的在LsitView的adapter里不停创建bitmap,这个地方是我们的首页banner位,理论上App一打开就会缓存这张默认背景图片了,而实际在使用过一段时间后,才因为为了解码这张背景图而OOM, 改为用全局缓存解决。

%title插图%num

下面这个就是传说中的内存抖动

%title插图%num

实际代码如下,因为打Log而进行了字符串拼接,一旦这个函数被比较频繁地调用,那么就很有可能会发生内存抖动。这里我们新版本已经改为使用stringbuilder进行优化。

%title插图%num

还有一些比较奇怪的情况,这里是我们扫描歌曲文件头的时候发生的,有些文件头居然有几百M大,导致一次申请了过大的内存,直接OOM,这里暂时也无法修复,直接catch住out of memory error。

%title插图%num

4. 同时我们对一些逻辑代码进行调整,比如我们的App主页的第三个tab(Live tab)进行了数据延迟加载,和定时回收。

%title插图%num

这里因为这个页面除了有大图还有轮播banner,实际强引用的图片会有多张,如果这个时候切到其他页面进行听歌等行为,这个页面一直在后台缓存,实际是很浪费耗内存的,同时为优化体验,我们又不能直接通过设置主页的viewpager的缓存页数,因为这样经常都会回收,导致影响体验,所以我们在页面不可见后过一段时间,清理掉adapter数据(只是清空adapter里的数据,实际从网络加载回来的数据还在,这里只是为了去掉界面对图片的引用),当页面再次显示时再用已经加载的数据显示,即减少了很多情况下图片的引用,也不影响体验。

5. *后我们也遇到一个比较奇葩的问题,在我们的RDM上报上有这样一条上报

%title插图%num

我们在stackoverflow上看到了相关的讨论,大致意思是有些情况下比如息屏,或者一些省电模式下,频繁地调System.gc()可能会因为内核状态切换超时的异常。这个问题貌似没有比较好的解决方法,只能是优化内存,尽量减少手动调用System.gc()

优化结果

我们通过启动App后,切换到我的音乐界面,停留1分钟,多次gc后,获取App内存占用

%title插图%num

多次试验结果都差不多,这里只截取了其中一次,有28M的优化效果。
当然不同的场景内存占用不同,同时上面试验结果是通过多次手动触发gc稳定后的结果。对于使用其他第三方工具不手动gc的情况下,试验结果可能会差异比较大。

对于上面提到的JOOX里各种图片背景等问题,我们做了动态的优化,对不同的机型进行优化,对特别低端的机型设置为纯色背景等方式,*终优化效果如下:

%title插图%num

%title插图%num

平均内存降低41M。

本次总结主要还是从图片方面下手,还有一点逻辑优化,已经基本达到优化目标。

四 总结

上面写了很多,我们可以简单总结,目前Andorid内存优化还是比较重要一个话题,我们可以通过各种内存泄露检测组件,MAT查看内存占用,Memory Monitor跟踪整个App的内存变化情况, Heap Viewer查看当前内存快照, Allocation Tracker追踪内存对象的来源,以及利用崩溃上报平台从多个方面对App内存进行监控和优化。上面只是列举了一些常见的情况,当然每个App功能,逻辑,架构也都不一样,造成内存问题也是不尽相同,掌握好工具的使用,发现问题所在,才能对症下药。

 


%title插图%num

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的情况以及解决方案。智能合并功能帮助开发同学把每天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数*多的崩溃,精准定位功能帮助开发同学定位到出问题的代码行,实时上报可以在发布后快速的了解应用的质量情况,适配*新的 iOS, Android 官方操作系统,鹅厂的工程师都在使用!

Android代码内存优化建议-OnTrimMemory优化

OnTrimMemory
OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。
本文通过问答的方式,从各个方面来讲解 OnTrimMemory 回调的使用过程和效果。想要开发高性能且用户体验良好的 Android 应用,那么这篇文章你不应该错过。

OnTrimMemory回调的作用?
OnTrimMemory是Android在4.0之后加入的一个回调,任何实现了ComponentCallbacks2接口的类都可以重写实现这个回调方法.OnTrimMemory的主要作用就是指导应用程序在不同的情况下进行自身的内存释放,以避免被系统直接杀掉,提高应用程序的用户体验.
Android系统会根据不同等级的内存使用情况,调用这个函数,并传入对应的等级:
TRIM_MEMORY_UI_HIDDEN 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者Back键导致应用的UI界面不可见.这时候应该释放一些资源.
TRIM_MEMORY_UI_HIDDEN 这个等级比较常用,和下面六个的关系不是很强,所以单独说.

下面3个等级是当我们的应用程序真正运行时的回调:
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缓存列表的*边缘位置,系统会*优先考虑杀掉我们的应用程序,在这个时候应当尽可能地把一切可以释放的东西都进行释放。
哪些组件可以实现OnTrimMemory回调?
Application.onTrimMemory()
Activity.onTrimMemory()
Fragement.OnTrimMemory()
Service.onTrimMemory()
ContentProvider.OnTrimMemory()

OnTrimMemory回调中可以释放哪些资源?
通常在架构阶段就要考虑清楚,我们有哪些东西是要常驻内存的,有哪些是伴随界面存在的.一般情况下,有下面几种资源需要进行释放:
缓存 缓存包括一些文件缓存,图片缓存等,在用户正常使用的时候这些缓存很有作用,但当你的应用程序UI不可见的时候,这些缓存就可以被清除以减少内存的使用.比如第三方图片库的缓存.
一些动态生成动态添加的View. 这些动态生成和添加的View且少数情况下才使用到的View,这时候可以被释放,下次使用的时候再进行动态生成即可.比如原生桌面中,会在OnTrimMemory的TRIM_MEMORY_MODERATE等级中,释放所有AppsCustomizePagedView的资源,来保证在低内存的时候,桌面不会轻易被杀掉.
2.1 例子:释放不常用到的View.
代码出处:Launcher

Launcher.java:
@Override
public void onTrimMemory(int level) {
super.onTrimMemory(level);
if (level >= ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
mAppsCustomizeTabHost.onTrimMemory();
}
}

AppsCustomizeTabHost.java:
public void onTrimMemory() {
mContent.setVisibility(GONE);
// Clear the widget pages of all their subviews – this will trigger the widget previews
// to delete their bitmaps
mPagedView.clearAllWidgetPages();
}

AppsCustomizePagedView.java:
public void clearAllWidgetPages() {
cancelAllTasks();
int count = getChildCount();
for (int i = 0; i < count; i++) {
View v = getPageAt(i);
if (v instanceof PagedViewGridLayout) {
((PagedViewGridLayout) v).removeAllViewsOnPage();
mDirtyPageContent.set(i, true);
}
}
}

PagedViewGridLayout.java
@Override
public void removeAllViewsOnPage() {
removeAllViews();
mOnLayoutListener = null;
setLayerType(LAYER_TYPE_NONE, null);
}

OnTrimMemory和onStop的关系?
onTrimMemory()方法中的TRIM_MEMORY_UI_HIDDEN回调只有当我们程序中的所有UI组件全部不可见的时候才会触发,这和onStop()方法还是有很大区别的,因为onStop()方法只是当一个Activity完全不可见的时候就会调用,比如说用户打开了我们程序中的另一个Activity。
因此,我们可以在onStop()方法中去释放一些Activity相关的资源,比如说取消网络连接或者注销广播接收器等,但是像UI相关的资源应该一直要等到onTrimMemory(TRIM_MEMORY_UI_HIDDEN)这个回调之后才去释放,这样可以保证如果用户只是从我们程序的一个Activity回到了另外一个Activity,界面相关的资源都不需要重新加载,从而提升响应速度。
需要注意的是,onTrimMemory的TRIM_MEMORY_UI_HIDDEN 等级是在onStop方法之前调用的.

OnTrimMemory和OnLowMemory的关系?
在引入OnTrimMemory之前都是使用OnLowMemory回调,需要知道的是,OnLowMemory大概和OnTrimMemory中的TRIM_MEMORY_COMPLETE级别相同,如果你想兼容api<14的机器,那么可以用OnLowMemory来实现,否则你可以忽略OnLowMemory,直接使用OnTrimMemory即可.

为什么要调用OnTrimMemory?
尽管系统在内存不足的时候杀进程的顺序是按照LRU Cache中从低到高来的,但是它同时也会考虑杀掉那些占用内存较高的应用来让系统更快地获得更多的内存。
所以如果你的应用占用内存较小,就可以增加不被杀掉的几率,从而快速地恢复(如果不被杀掉,启动的时候就是热启动,否则就是冷启动,其速度差在2~3倍)。
所以说在几个不同的OnTrimMemory回调中释放自己的UI资源,可以有效地提高用户体验。

有哪些典型的使用场景?
常驻内存的应用
一些常驻内存的应用,比如Launcher、安全中心、电话等,在用户使用过要退出的时候,需要调用OnTrimMemory来及时释放用户使用的时候所产生的多余的内存资源:比如动态生成的View、图片缓存、Fragment等。
有后台Service运行的应用 这些应用不是常驻内存的,意味着可以被任务管理器杀掉,但是在某些场景下用户不会去杀。
这类应用包括:音乐、下载等。用户退出UI界面后,音乐还在继续播放,下载程序还在运行。这时候音乐应该释放部分UI资源和Cache。

关于Android内存优化你应该知道的一切

介绍

在Android系统中,内存分配与释放分配在一定程度上会影响App性能的—鉴于其使用的是类似于Java的GC回收机制,因此系统会以消耗一定的效率为代价,进行垃圾回收。
在中国有句老话:”由俭入奢易,由奢返俭难”。而此谚语也似乎正适应于Android的内存使用。GC回收机制给程序员省去了像C语言程序员那样手动释放内存的工作,但是也带来了一系列的”雷”—动辄内存泄漏,再甚者稍微不慎就会OOM。
这篇文章将会介绍Android的内存管理机制并解释几种在此机制下对内存有影响的几个比较关键的因素。另外,还会介绍如何提高内存管理、检测并避免内存泄漏,以及如何分析内存分配情况

Here we go!!

Android内存管理机制

Android内存模型并没有交换空间(swap space)的概念,而是使用分页(paging)和内存映射(memory-mapping)管理内存,这意味着不管是分配新的对象还是使用已有的映射页这些内存仍然被占据在RAM里而不能被扇出。因此完全释放你app内存的唯一方式是释放对象引用以便于能被垃圾回收器回收。
Dalvik虚拟机为每一个App分配相应大小的可用内存空间,从2M开始到32M(此*大值根据不同的厂商一般会有不同),不可否认,在当前国内各大手机厂商疯狂的拼硬件的时代,这个每个App的可用内存甚至被提高到了256M,这有效的避免了很多OOM的情况,但是如果程序员因此就不管内存管理任意而为,会为此付出严重代价的(App高卸载率).
Android系统会将在后台运行的App进程保存在一个LRU cache中(不懂的自行百度)。当系统内存紧张时,它会根据LRU的策略kill掉一些优先级比较低的进程。当然,究竟哪一个App是当前占用内存*大的程序也是它kill进程时所考虑的一个因素。如果你希望自己的App在后台运行时能尽可能长的”活着”,不被系统kill掉,就要好好的思考如何避免被kill。比如在App转到后台运行之前,尽可能的将没有用的内存给释放掉,这样会减少Android系统打印错误日志甚至终止App的可能性。

如何提高Android内存使用

Android系统是世界上使用率*高的手机系统。每年都有成千上万的年轻人转入到开发Android系统的行列中,但是这些人中,能真正写出稳定、可扩展性强的代码的还是少数。

以下是提高内存使用的几条建议:
  1. 慎用桥接模式,虽然从程序的设计角度来看,抽象能够帮助我们创建更加灵活的软件架构。但是在手机系统中,这种设计模式有可能会造成很多副作用。除非大有必要,否则尽量不要用桥接模式
  2. 避免使用枚举Enum,一个Enum分配的空间是一个普通常量的两倍,因此尽量少使用枚举
  3. 试着使用Android框架优化后的数据容器,譬如:SparseArray, SparseBooleanArray, 以及 LongSparseArray containers. 使用这些类来替代HashMap的使用。原因是传统的 HashMap 在内存上的实现十分的低效,因为它需要为 HashMap 中每一项在内存中建立映射关系. 另外, SparseArray类非常高效因为它避免了对key和value的自动封箱. 万事都有两面性,这些个被优化过的容器也不例外,千万记住SparseArray等容器并不适应于内部元素很多的集合,当集合的长度超过1000条时,使用SparseArray进行增删改查的效率远比HashMap低
  4. 避免创建不需要的对象。对于生命周期较短的临时变量,尽量想办法规避掉每次都要去创建它,这样GC回收被强制调用机会就会更少,留给Android系统进行UI渲染或者音频加载的时间就会更多,从而避免了卡顿现象
  5. 检测App内存中的可用堆的大小,在代码中可以通过动态的调用ActivityManager::getMemoryClass()方法来查询你的App中的可用内存堆大小。如果系统检测到需要分配的内存大小超过了此值,则会抛出OOM错误
  6. **可以适当适应onTrimMemory回调方法。OnTrimMemory 回调是 Android 4.0 之后提供的一个API,这个 API 是提供给开发者的,它的主要作用是提示开发者在系统内存不足的时候,通过处理部分资源来释放内存,从而避免被 Android 系统杀死。这样应用在下一次启动的时候,速度就会比较快。
  7. 当使用Service应当小心小心再小心!当你需要启动一个服务在后台执行一项任务时,应当在其完成工作之后尽快的停止此服务。可以考虑使用IntentService—当在子线程完成耗时操作之后,IntentService会自动停止并结束自身。然而在实际开发中经常会碰到需要服务去执行一项耗时比较长的任务,比如:音乐播放器,下载APP等等。像这样的应用可以分隔为两个进程:一个进程负责 UI 工作, 另外一个则在后台服务中运行其它的工作. 在AndroidManifest 文件中为各个组件申明 android:process 属性就可以分隔为不同的进程。注意一点:在后台运行的Service*对不能处理或者持有任何UI,否则系统可能会分配双倍甚至三倍的空间来维护UI资源!!
  8. 当你加载 bitmap 时, 需要根据当前设备的分辨率加载相应分辨率的bitmap进入内存,如果下载下来的原图分辨率比设备分辨率高则要压缩它. 要小心bitmap的分辨率增加后所占用的内存也要进行相应的增加(平方级increase2的增长), 因为它是根据x和y的大小来增加内存占用的
  9. 使用代码混淆工具 ProGuard 通过去除没有用的代码和通过语义模糊来重命名类, 字段和方法来缩小, 优化和混淆你的代码. 使用它能使你的代码更简洁, 更少量的RAM映射页.如果构建apk后你没有做后续的任何处理(包括根据你的证书进行签名), 你必须运行 zipalign 工具为你的apk进行优化, 如果不这样做会导致你的应用使用更多的内存,zipalign之后像资源这样的东西不会再从apk中映射(mmap)入内存.注意:goole play store 不接受没有进行zipalign的apk

针对以上几条,后续会单独再post几篇blog单独讲解。

如何避免内存泄漏

程序员在分配内存时如果考虑到了上述9条建议,或许会给App在效率上带来不小的收益,并且可以在后台时依然坚挺(更持久!)。 但是这一切的努力都会因为一个叫做内存泄漏的东东而萎了! 这玩意就如同可乐的存在一样,少喝一点还能扛得住,但是多了的话。。你懂得! 以下是几个常见的造成内存泄漏的情况:

  • 当查询完数据库之后,及时关闭Cursor对象。
  • 记得在Activity的onPause方法中调用unregisterReceiver()方法,解注册广播
  • 避免Content内存泄漏,比如在4.0.1之前的版本上不要讲Drawer对象置为static。当一个Drawable绑定到了View上,实际上这个View对象就会成为这个Drawable的一个callback成员变量,上面的例子中静态的sBackground持有TextView对象lable的引用,而lable只有Activity的引用,而Activity会持有其他更多对象的引用。sBackground生命周期要长于Activity。当屏幕旋转时,Activity无法被销毁,这样就产生了内存泄露问题。
  • 尽量不要在Activity中使用非静态内部类,因为非静态内部类会隐式持有外部类实例的引用,当非静态内部类的引用的声明周期长于Activity的声明周期时,会导致Activity无法被GC正常回收掉。
  • 谨慎使用线程Thread!!这条是很多人会犯的错误: Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机对所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。所以当使用线程时,一定要考虑在Activity退出时,及时将线程也停止并释放掉
  • 使用Handler时,要么是放在单独的类文件中,要么就是使用静态内部类。因为静态的内部类不会持有外部类的引用,所以不会导致外部类实例的内存泄露–详情请参阅Android中Handler引起的内存泄露

如何分析内存的使用情况

在Mac终端(windows的cmd)中,可以使用adb logcat命令来查看或者统计内存的具体使用情况,另外还可以指定包名来查看相应App的内存使用情况。除此之外,还可以使用三方的工具来分析Android内存的使用情况,比如:DDMS、MAT(Memory Analyzer tool).

在adb logcat中,通常能看到GC相关的log如下图所示

Dalvik GC log

GC_Reason 触发GC回收的原因,可能包含以下几种情况:

  • GC_FOR_ALLOC, 这个是说我们的应用尝试去分配内存而这时候和heap已经快满了(不够用了),这个时候系统会把我们的应用停下来然后进行内存回收,通常heap size会增大
  • GC_CONCURRENT,这个应该的当我们的Heap size 快要被填满的时候触发的一个并发的内存回收
  • GC_EXPLICIT,这个是主动调用系统gc方法触发的GC(在DDMS 点击GC就可以看到)
  • GC_HPROF_DUMP_HEAP 我们在做内存分析创建HPROF(MAT可以分析该文件)的时候会打印

Amount feed 表示本次垃圾收集释放了多少内存

Heap_stats 当前空闲内存占总内存的百分比

External memory stats 表示API 10及以下的外部分配内存,已分配内存/导致垃圾回收的阈值

Pause_time 应用暂停的时间

通常情况下,生成的GC log越大,表示内存的分配与释放发生的频率越高,这种情况下往往会非常影响用户体验!

使用DDMS查看并追踪堆内存的分配情况

通过DDMS,程序员可以很轻松的检测指定进程的内存分配情况。你可以通过“Heap”标签查看*新的实时的堆内存信息,这样可以帮助你辨别出究竟是哪一个操作*有可能造成大量的内存分配。 “Allocation Tracker” 标签显示的是*近所有的内存分配—包含分配对象的类型,是在哪个线程中分配等信息。一下图片演示的是使用DDMS展示进程信息—包含了当前进程、对内存分配统计信息。

heap_stat

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速