内存优化 订阅
程序在运行时,Windows会将其直接调入到物理内存中,但物理内存毕竟有限,因此,微软又设计了虚拟内存,它其实就是硬盘中的一块空间,Windows会将一些暂时不用,但可能以后会用到的数据从物理内存移动到虚拟内存中,从而保证有足够的物理内存给当前运行的程序使用。所以,电脑的内存=实际物理内存容量+“分页文件”(就是交换文件)。如果需要,“分页文件”会动用硬盘上所有可用空间。内存优化的好处是:在将占用物理内存的程序移动到虚拟内存后,再启动新程序,程序、系统运行的速度会变得更快,提升系统工作效率。 展开全文
程序在运行时,Windows会将其直接调入到物理内存中,但物理内存毕竟有限,因此,微软又设计了虚拟内存,它其实就是硬盘中的一块空间,Windows会将一些暂时不用,但可能以后会用到的数据从物理内存移动到虚拟内存中,从而保证有足够的物理内存给当前运行的程序使用。所以,电脑的内存=实际物理内存容量+“分页文件”(就是交换文件)。如果需要,“分页文件”会动用硬盘上所有可用空间。内存优化的好处是:在将占用物理内存的程序移动到虚拟内存后,再启动新程序,程序、系统运行的速度会变得更快,提升系统工作效率。
信息
提    高
内存的使用效率
优    化
内存的管理
中文名
内存优化
目    的
主要是为了保持虚拟内存的连续性
内存优化技巧方法
如何优化内存的管理,提高内存的使用效率,尽可能地提高运行速度,是我们所关心的问题。下面介绍在Windows操作系统中,提高内存的使用效率和优化内存管理的几种方法。改变页面文件的位置其目的主要是为了保持虚拟内存的连续性。因为硬盘读取数据是靠磁头在磁性物质上读取,页面文件放在磁盘上的不同区域,磁头就要跳来跳去,自然不利于提高效率。而且系统盘文件众多,虚拟内存肯定不连续,因此要将其放到其他盘上。改变(调整大小)页面文件位置的方法是:用鼠标右键点击“我的电脑”,选择“属性→ 高级→性能设置→高级→更改虚拟内存”,在驱动器栏里选择想要改变到的位置即可。值得注意的是,当移动好页面文件后,要将原来的文件删除(系统不会自动删除)。因为C盘扇区最近,所以尽量把虚拟内存设置在C盘,且不要设置其他盘的内存,使其具有连续性。改变页面文件的大小改变了页面文件的位置后,我们还可以对它的大小进行一些调整。调整时我们需要注意,不要将最大、最小页面文件设为等值。因为通常内存不会真正“塞满”,它会在内存储量达到一定程度时,自动将一部分暂时不用的数据放到硬盘中。最小页面文件越大,所占比例就低,执行的速度也就越慢。最大页面文件是极限值,有时打开很多程序,内存和最小页面文件都已“塞满”,就会自动溢出到最大页面文件。所以将两者设为等值是不合理的。一般情况下,最小页面文件设得小些,这样能在内存中尽可能存储更多数据,效率就越高。最大页面文件设得大些,以免出现“满员”的情况。禁用页面文件当拥有了8GB以上的内存时,页面文件的作用将不再明显,因此我们可以将其禁用。方法是:依次进入注册表编辑器“HKEY_LOCAL_MACHINESystemCurrentControlSetControlSession Ma-nagerMemoryManagement”下,在“DisablePa-ging Executive”(禁用页面文件)选项中将其值设为“1”即可。清空页面文件在同一位置上有一个“ClearPageFileAtShutdown(关机时清除页面文件)”,将该值设为“1”。这里所说的“清除”页面文件并非是指从硬盘上完全删除pagefile.sys文件,而是对其进行“清洗”和整理,从而为下次启动Windows XP时更好地利用虚拟内存做好准备。调整高速缓存区域的大小可以在“计算机的主要用途”选项卡中设置系统利用高速缓存的比例(针对Windows 98)。如果系统的内存较多,可选择“网络服务器”,这样系统将用较多的内存作为高速缓存。在CD-ROM标签中,可以直接调节系统用多少内存作为CD-ROM光盘读写的高速缓存。监视内存系统的内存不管有多大,总是会用完的。虽然有虚拟内存,但由于硬盘的读写速度无法与内存的速度相比,所以在使用内存时,就要时刻监视内存的使用情况。Windows操作系统中提供了一个系统监视器,可以监视内存的使用情况。一般情况下如果只有60%的内存资源可用,这时你就要注意调整内存了,不然就会严重影响电脑的运行速度和系统性能,否则会很卡的。及时释放内存空间如果你发现系统的内存不多了,就要注意释放内存。所谓释放内存,就是将驻留在内存中的数据从内存中释放出来。释放内存最简单有效的方法,就是重新启动计算机。另外,就是关闭暂时不用的程序。还有要注意剪贴板中如果存储了图像资料,是要占用大量内存空间的。这时只要剪贴几个字,就可以把内存中剪贴 板上原有的图片冲掉,从而将它所占用的大量的内存释放出来。优化内存中的数据在Windows中,驻留内存中的数据越多,就越要占用内存资源。所以,桌面上和任务栏中的快捷图标不要设置得太多。如果内存资源较为紧张,可以考虑尽量少用各种后台驻留的程序。平时在操作电脑时,不要打开太多的文件或窗口。长时间地使用计算机后,如果没有重新启动计算机,内存中的数据排列就有可能因为比较混乱,从而导致系统性能的下降。这时你就要考虑重新启动计算机。提高系统其他部件的性能计算机其他部件的性能对内存的使用也有较大的影响,如总线类型、CPU、硬盘和显存等。如果显存太小,而显示的数据量很大,再多的内存也是不可能提高其运行速度和系统效率的。如果硬盘的速度太慢,则会严重影响整个系统的工作。提高计算机运算速度优质的内存能提高计算机的内存读取力度,加强计算机各部件功能的协调性,使计算机的运行速度更流畅。
收起全文
精华内容
下载资源
问答
  • Redis内存优化

    千次阅读 2020-04-16 14:50:06
    使用maxmemory参数限制最大可用内存,当超出内存上限maxmemory时使用LRU等删除策略释放空间以及防止所用内存超过服务器物理内存。 2.配置内存回收策略 Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略...

    1.设置内存上限

    使用maxmemory参数限制最大可用内存,当超出内存上限maxmemory时使用LRU等删除策略释放空间以及防止所用内存超过服务器物理内存。

    2.配置内存回收策略

    Redis所用内存达到maxmemory上限时会触发相应的溢出控制策略。具体策略受maxmemory-policy参数控制,Redis支持6种策略,如下所示:

    noeviction:默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息(error)OOM command not allowed when used memory,此 时Redis只响应读操作。

    volatile-lru:根据LRU算法删除设置了超时属性(expire)的键,直到腾出足够空间为止。如果没有可删除的键对象,回退到noeviction策略。

    allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性, 直到腾出足够空间为止。

    allkeys-random:随机删除所有键,直到腾出足够空间为止。

    volatile-random:随机删除过期键,直到腾出足够空间为止。

    volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果 没有,回退到noeviction策略。

    3.键值对优化

    降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。在完整描述业务情况下,键值越短越好。值对象缩减比较复杂,应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。

    4.共享对象池

    共享对象池是指Redis内部维护[0-9999] [0-9999]的整数对象 池,用于节约内存。

    但是共享对象池与maxmemory+LRU策略冲突,使用时需要注意。对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高。

    5.字符串优化

    字符串对象是Redis内部最常用的数据类型。所有的键都是字符串类 型,值对象数据除了整数之外都使用字符串存储。在使用过程中应当尽量优先使用整数,比字符串类型更节省空间。并且要优化字符串使用,避免预分配造成的内存浪费。使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡,使用intset编码优化整数集合。使用ziplist编码的hash结构降低小对象链规模。

    6.编码优化

    Redis对外提供了多种数据类型,但是Redis内部对于不同类型的数据使用的内部编码不一样。内部编码不同将直接影响数据的内存占用和读写效率。

    7.控制键的数量

    当使用Redis存储大量数据时,通常会存在大量键,过多的键同样会消 耗大量内存。

    展开全文
  • 今天来聊一聊Android中内存优化的一些手段。 首先问问自己为什么要内存优化呢? (1):App消耗内存过大,导致手机内存低于内存警戒线的时候,Low Memory Killer机制就会触发,App占用内存越多,被处理掉的机会就越...

    一.引言

    今天来聊一聊Android中内存优化的一些手段。
    首先问问自己为什么要内存优化呢?
    (1):App消耗内存过大,导致手机内存低于内存警戒线的时候,Low Memory Killer机制就会触发,App占用内存越多,被处理掉的机会就越大。
    (2):受虚拟机堆内存限制,出现OOM,内存溢出,程序出现crash。
    (3):频繁的GC会导致内存抖动,并发GC伴随着stop-the-world。GC for Allow 当内存不足以分配给新的对象时触发,它的stop-the-world的时间会很长,导致卡顿。

    掌握内存优化的一些手段是很有必要的,让我们可以在编码时就能做到一些内存上的优化,而不是等到程序开发完成了或者后期程序规模越来越大了才去发现内存问题,那时候恐怕就为时已晚了。

    二.编码时就能做到的内存优化

    下面就来说说我们能够在编码时就能做到的内存优化:
    (1):尽量不要在调用频繁的生命周期或者方法中去创建对象。
    例如View的onDraw方法。
    在这里插入图片描述
    当我们在自定义View,在onDraw方法里创建了一个对象时,发现系统会给我们提示,让我们不要在onDraw()方法里面去创建对象,因为onDraw方法会频繁的被调用,为了避免频繁的GC,所以我们应该尽量避免在调用频繁的方法中去创建对象。

    (2):使用合适的数据结构( 例如使用ArrayMap和SparseArray替代HaspMap)
    ArrayMap和SparseArray都是基于二分法的原理,查询效率比起HashMap要略低,但是使用的内存比HashMap要更少。

    HashMap:默认容量16,每次都是扩容都是原来数组大小的两倍。
    下图为HashMap的扩容代码,左移以为其实就是乘以2,只是由于位运算的效率比较高,系统采用的是位运算的方式。
    HashMap的扩容代码
    ArrayMap :默认初始化两个空数组,扩容为已存储个数的两倍的1.5倍。BASE_SIZE == 8。
    ArrayMap的扩容代码
    SparseArray:默认容量为10,扩容为已存储个数的1.5倍。int值作为key时不会拆装箱。

    public SparseArray() {
          this(10);
        }
    
    public static int growSize(int currentSize) {
          return currentSize <= 4 ? 8 : currentSize * 2;
     }
    

    下面展示三张图片:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    以上三张图是我做的一个小实验,我创建了十万个对象分别使用HashMap,ArrayMap,SparseArray去存储,key值位int,等到内存稳定时,我们去观察App占用内存的大小。第一张图:使用HashMap占用的内存最大,第二张图ArrayMap占用的内存第二,第三张图SparseArray占用的内存最少。HashMap与ArrayMap的key只能为Object,所以在存储key值为int型的对象时,会我们去做一次装箱的操作,把int转为Integer对象,对象占用的内存肯定是比我们int型的值占用的内存更大。这也就能解释,当key值为int时,SparseArray为什么能比ArrayMap占用更少的内存了。

    (3):使用集合时可以使用带参数的构造方法可以减少数组扩容的次数
    List,Map,StringBuilder等等内部通过数组实现类。

    //最好为2的整数次幂
    Map map = new HashMap(32);
    List list = new ArrayList(100);
    StringBuilder sb = new StringBuilder(100);
    

    我们都知道在Java中,数组是不可变的,所以一些类底部的实现机制如果是通过数组来实现的话,它还是可变的,那么必定便随着数组扩容的操作。
    什么时是数组扩容呢?
    简单的说就是如果一个数组的容量不够了,就去创建一个更大的数组,然后把原来数组中存储的值,一个个移到新创建的更大的数组中去。
    如果扩容次数多了,GC的次数也会频繁的增多。
    如果我们预先能够知道需要,存储多少个元素,或者大概多少个元素,我们可以使用带参数的构造方法来创建出这个大小的数组,来减少数组扩容的次数。

    (4):for循环中不要使用“+”号拼接字符串
    “+”底层还是通过StringBuilder实现的,每一次for循环都会创建一个StringBuilder对象。

    当我们使用for循环外部创建StringBuilder,内部使用它拼接字符串,内存图形很平稳。
    在这里插入图片描述

    for循环内部使用+号拼接字符串,图形抖动(内存抖动),一直在GC。
    在这里插入图片描述

    (5):尽可能少的使用枚举
    使用 ENUM 将会增大 DEX 大小,并会增大运行时的内存分配大小。为了弥补 Android 平台不建议使用枚举的缺陷,官方推出了两个注解,IntDef和StringDef,用来提供编译期的类型检查。
    在这里插入图片描述
    枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。而每个枚举常量是一个静态常量字段,使用内部类实现,该内部类继承了枚举类。所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化。

    (6):ListView复用,对象复用
    这个就不多少了,我相信每个Android开发者都知道。

    (7):减少布局的层级
    减少View的个数,减少测量的时间(View显示在前台,经过三个阶段测量,布局和绘制)。能够减轻内存,CPU的负担。

    (8):序列化可以使用Protobuf
    Protobuf是谷歌推出的一款平台无关,语言无关,可扩展的序列化和反序列化技术。有兴趣的朋友可以自行了解一个,这里就不多说了。

    (9):数据库减少使用AUTOINCREMENT关键字
    AUTOINCREMENT关键字的作用保证主键是严格单调递增的。

      public static final String CREATE_BOOK = "create table book ("
    
    + "id integer primary key autoincrement, "//大多数情况下可以去掉
    
    + "author text, "
    
    + "price real, "
    
    + "pages integer, "
    
    + "name text)";
    
    
    db.execSQL(CREATE_BOOK);
    
    如果指定使用AUTOINCREMENT来创建表,会创建sqlite_sequence的内部表来记录该表使用的最大行号,UPDATE,INSERT和DELETE语句可能会修改sqlite_sequence的内容。会带来额外的开销。
    SQLite官网:AUTOINCREMENT关键词会增加CPU,内存,磁盘空间和磁盘I/O
    的负担,所以尽量不要使用,除非必需。通常情况下非必需。
    

    (10):Bitmap优化
    讲到内存优化必不可少的都要提及BitMap优化。因为Bitmap真的是内存占用的大头。

    我们的Bitmap占用多大内存呢?下面介绍两个系统提供的api:

    bitmap.getAllocationByteCount();
    bitmap.getByteCount();

    一般情况下两者是相等的;
    如果通过复用Bitmap来解码图片,被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小。

    下面我们在说说BitMap的内存分配:
    2.3 ~ 7.1:Bitmap的内存是分配在, 虚拟机的 java堆上。受系统分配的虚拟机的内存限制。超出限制OOM,应用程序crash。
    8.0以后:内存分配在 native堆,不需要用户主动回收。不受系统分配的虚拟机的内存限制。
    超出系统可用内存,进程直接挂掉,无crash弹窗,不会出现OOM。(不能因为内存分配在native上就肆无忌惮的使用图片。)
    Android8.0之后源码里通过NativeAllocationRegistry这个类来回收Native层的内存,从而可以实现把bitmap的像素数据放到Native内存中并及时回收。

    Java Heap,这部分的内存区域是由虚拟机管理,通过Java中 new 关键字来申请一块新内存。这块区域的内存是由GC直接管理,能够自动回收内存。这块内存的大小会受到系统限制,当内存超过APP最大可用内存时会OOM。

    Native Heap,这部分内存区域是在C++中申请的,它不受限于APP的最大可用内存限制,而只是受限于设备的物理可用内存限制。它的缺点在于没有自动回收机制,只能通过C++语法来释放申请的内存。

    8.0以前,我们也可以通过特殊的手段将Bitmap的内存分配移至native内存。

    Fresco:将Bitmap内存分配至Ashmem内存。
    Ashmem(Android匿名共享内存),这部分内存类似于Native内存区,但是它是受Android系统底层管理的,当Android系统内存不足时,会回收Ashmem区域中状态是 unpin 的对象内存块,如果不希望对象被回收,可以通过 pin 来保护一个对象

    BitmapFactory.Options = new BitmapFactory.Options();
    options.inPurgeable = true;(被废弃)
    Bitmap bitmap = BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
    

    说完上面的我们说说在加载Bitmap的时候,我们通常要做的几个事:
    1.计算合适的inSampleSize对图片进行压缩
    设置BitmapFactory.Options类的inJustDecodeBounds属性为true,可在Bitmap不被加载到内存的前提下,获取Bitmap的原始宽高。然后通过原始宽高和需要显示控件的宽高计算出合适的inSampleSize(取2的整数次幂,如果不是的话,向下取得最大的2的整数次幂)。对图片进行压缩。

    2.设置合适的inPreferredConfig 值
    这里我只介绍三个属性:
    ALPHA_8:每个像素点仅表示alpha的值,它不会存储任何颜色信息,占8位。
    RGB_565:每个像素用5位R/6位G/5位G来表示,占16位。
    ARGB_8888:每个像素分别用8位存储ARGB,占32位

    如何设置呢?
    png图片使用ARGB_8888。(不设置默认使用ARGB_8888)
    jpg(24位,32位)使用RGB_565
    jpg(8位)使用ALPHA_565
    注意了并不是设置RGB_565解码的Bitmap所占用的内存就一定比ARGB_8888小。
    下面我们做个小实验,找三张图片分别使用不同的值,去加载这个张图片

    png图片在设置三种不同的值所占内存的大小
    在这里插入图片描述

    jpg(24位,32位)图片在设置三种不同的值所占内存的大小
    在这里插入图片描述
    jpg(8位)图片在设置三种不同的值所占内存的大小
    在这里插入图片描述
    第一行是使用ALPHA_565,第二行使用RGB_565,第三行使用ARGB_8888。看完三张图,在看看我上面说的,我相信就已经可以理解了。

    3.图片放在合适的文件夹下

        目录名称                                Density
    res/drawable                       默认密度(跟随ROM)
    res/drawable-hdpi                         240
    res/drawable-ldpi                         120
    res/drawable-mdpi                         160
    res/drawable-xhdpi                        320
    res/drawable-xxhdpi                       480
    

    Android系统在加载这些图片时,会先一步得到当前设备的显示密度,然后到相匹配的drawable的目录下寻找图片资源。如果不存在,会就近获取图片资源,然后将其所在的目录所代表的的密度与当前设备的密度相比,以这个比例来缩放图片。

    获取屏幕密度方式:
    DisplayMetrics dm = context.getResources().getDisplayMetrics();
    int dpi = dm.densityDpi
    如果一张大图放在了mdpi目录,而当前设备显示器为480dpi的超高密屏幕。
    这时Android就会按照3倍大小来缩放这张图片,将它载入内存。

    如果拿不准放入哪个目录,推荐使用Drawable.createFromStream替换getResources().getDrawable来加载,可以绕过Android上面说的默认规则。

    4.设置inBitmap属性

    4.4之前的版本inBitmap只能够重用相同大小的Bitmap内存区域。简单而言,被重用的Bitmap需要与新的Bitmap规格完全一致,否则不能重用。

    4.4之后的版本系统不再限制旧Bitmap与新Bitmap的大小,只要保证旧Bitmap的大小是大于等于新Bitmap大小即可。

    使用inBitmap前,每创建一个bitmap需要独占一块内存。
    在这里插入图片描述
    使用inBitmap后,多个bitmap会复用同一块内存。
    在这里插入图片描述
    5.缓存策略LruCache
    LruCache 顾名思义就是使用LRU缓存策略的缓存,那么LRU是什么呢? 最近最少使用到的(least recently used),就是当超出缓存容量的时候,就优先淘汰链表中最近最少使用的那个数据。
    在这里插入图片描述
    enryRemoved方法:超出缓存容量的时候,调用的方法。
    sizeOf:计算大小的方法。

    6.淘汰掉的Bitmap的缓存池BitmapPool
    可以使用软引用包装那些被淘汰的对象装载到一个集合中(被淘汰的对象不会被立马回收掉)。可以进行复用。
    下面用一张图简单说明一下,图画的不好还请见谅:
    在这里插入图片描述

    (11):避免OOM
    AndroidManifest文件配置largeHeap = true , 使用多进程(需衡量利弊)让应用占用更多的内存,使用jni在native heap上申请空间,使用显存(使用 OpenGL textures 等 API , texture memory 不受虚拟机 heapsize 限制),避免OOM。

    三.常见内存泄漏的处理方案

    说到内存优化,不得不提的就是内存泄漏,在编码过程中,我应该尽量去避免内存泄漏的场景。由于考虑篇幅的原因,下面我列举一个常见的内存泄漏场景,并且给出相应的解决方案。

    1.非静态内部类导致的泄露的统一解决方案:使用静态内部类.持有外部类的弱引用.(Handler,AsyncTask,Thread,Timer等等)
    2.静态对象持有Activity上下文解决方案:使用全局的上下文 getApplcationContext().
    3.静态View导致的内存泄露解决方案:在Activity中onDestory中将View置为null.
    4.WebView导致的内存泄露解决方案:单进程方式或者反射破坏引用链.
    5.经典Mvp导致的内存泄露解决方案:在Activity(Fragment) onDestory时 Presenter与View进行解绑。
    6.注册监听器的泄漏(广播的绑定与解绑,EventBus的绑定与解绑).
    7.IO流,数据库导致的内存泄露(OutputStream、 InputStream以及相关的子类需要在finally块中进行及时关闭释放资源。数据库主要是Cursor游标、数据库的关闭).
    8.集合框架导致的内存泄露 (不用的对象即时移除掉,否则只要集合被引用,集合内添加的元素就不能被回收).
    9.Android系统导致的内存泄漏 InputMethodManager(在15<=API<=23中都存在) TextLine,AudioManger(6.0以下),android.os.Message等。

    LeakCanary列出的已知的系统内存泄漏场景:
    https://github.com/square/leakcanary/blob/9e74a8529ca94287fe0c3b02b7a6b39d51ecd704/leakcanary-android/src/main/java/com/squareup/leakcanary/AndroidExcludedRefs.java

    四.总结

    以上就是我们能够在编码时,就能够做到的一些内存优化,其实所有的方法,无外乎减少GC次数,对象复用,转移内存分配的地点,压缩减少内存分配的大小,不用的对象即时释放,相比以上的方案,我觉得思想的学习是最重要的,希望大家都能写出高性能的app。
    对待已经存在的内存问题,那我们如何去发现呢?内存监控的问题,我们留到下次再说。今天就先说到这里,如果文章对你有帮助的话,希望可以来个赞哦。

    展开全文
  • Android-性能优化-内存优化

    千次阅读 2017-09-22 11:16:56
    Android-性能优化-内存优化 概述 JVM 内存分配机制 详见:JVM 内存分配机制 JVM 垃圾回收机制 详见:JVM 垃圾回收机制 DVM 与 JVM 的区别 虚拟机区别 Dalvik 虚拟机(DVM)是 Android 系统在 ...

    Android-性能优化-内存优化

    相关系列

    概述

    JVM 内存分配机制

    JVM 垃圾回收机制

    DVM 与 JVM 的区别

    • 虚拟机区别

    Dalvik 虚拟机(DVM)是 Android 系统在 java虚拟机(JVM)基础上优化得到的,DVM 是基于寄存器的,而 JVM 是基于栈的,由于寄存器高效快速的特性,DVM 的性能相比 JVM 更好。

    • 字节码区别

    Dalvik 执行 .dex 格式的字节码文件,JVM 执行的是 .class 格式的字节码文件,Android 程序在编译之后产生的 .class 文件会被 aapt 工具处理生成 R.class 等文件,然后 dx 工具会把 .class 文件处理成 .dex 文件,最终资源文件和 .dex 文件等打包成 .apk 文件。

    OOM 代码相关优化

    当应用程序申请的 java heap 空间超过 Dalvik VM HeapGrowthLimit 时溢出。 OOM 并不代表内存不足,只要申请的 heap 超过 Dalvik VM HeapGrowthLimit 时,即使内存充足也会溢出。 效果是能让较多进程常驻内存。

    • Bitmap

    Bitmap 非常消耗内存,而且在 Android 中,读取 bitmap 时, 一般分配给虚拟机的图片堆栈只有 8M,所以经常造成 OOM 问题。 所以有必要针对 Bitmap 的使用作出优化:

    1. 图片显示:加载合适尺寸的图片,比如显示缩略图的地方不要加载大图。
    2. 图片回收:使用完 bitmap,及时使用 Bitmap.recycle() 回收。

    问题:Android 不是自身具备垃圾回收机制吗?此处为何要手动回收?

    Bitmap 对象不是 new 生成的,而是通过 BitmapFactory 生产的。 而且通过源码可发现是通过调用 JNI 生成 Bitma p对象(nativeDecodeStream()等方法)。 所以,加载 bitmap 到内存里包括两部分,Dalvik 内存和 Linux kernel 内存。 前者会被虚拟机自动回收。 而后者必须通过 recycle() 方法,内部调用 nativeRecycle() 让 linux kernel 回收。

    1. 捕获 OOM 异常:程序中设定如果发生 OOM 的应急处理方式。
    2. 图片缓存:内存缓存、硬盘缓存等
    3. 图片压缩:直接使用 ImageView 显示 Bitmap 时会占很多资源,尤其当图片较大时容易发 生OOM。 可以使用 BitMapFactory.Options 对图片进行压缩。
    4. 图片像素:android 默认颜色模式为 ARGB_8888,显示质量最高,占用内存最大。 若要求不高时可采用 RGB_565 等模式。
    5. 图片大小:图片 长度×宽度×单位像素 所占据字节数。

    我们知道 ARGB 指的是一种色彩模式,里面 A 代表 Alpha,R 表示 Red,G 表示 Green,B 表示 Blue。 所有的可见色都是由红绿蓝组成的,所以红绿蓝又称为三原色,每个原色都存储着所表示颜色的信息值,下表中对四种颜色模式的详细描述,以及每种色彩模式占用的字节数。

    模式描述占用字节
    ALPHAAlpha 由 8 位组成1B
    ARGB_44444 个 4 位组成 16 位,每个色彩元素站 4 位2B
    ARGB_88884 个 8 为组成 32 位,每个色彩元素站 8 位(默认)4B
    RGB_565R 为 5 位,G 为 6 位,B 为 5 位共 16 位,没有Alpha2B
    • 对象引用类型
    1. 强引用(Strong Reference):JVM宁愿抛出OOM,也不会让GC回收的对象
    2. 软引用(Soft Reference) :只有内存不足时,才会被GC回收。
    3. 弱引用(weak Reference):在GC时,一旦发现弱引用,立即回收
    4. 虚引用(Phantom Reference):任何时候都可以被 GC 回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。 程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。 可以用来作为 GC 回收 Object 的标志。
    • 缓存池

    对象池:如果某个对象在创建时,需要较大的资源开销,那么可以将其放入对象池,即将对象保存起来,下次需要时直接取出使用,而不用再次创建对象。当然,维护对象池也需要一定开销,故要衡量。

    线程池:与对象池差不多,将线程对象放在池中供反复使用,减少反复创建线程的开销。

    内存泄露相关优化

    当一个对象已经不需要再使用了,本该被回收时,而有另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

    • 单例造成的内存泄漏

    单例模式非常受开发者的喜爱,不过使用的不恰当的话也会造成内存泄漏,由于单例的静态特性使得单例的生命周期和应用的生命周期一样长,这就说明了如果一个对象已经不需要使用了,而单例对象还持有该对象的引用,那么这个对象将不能被正常回收,这就导致了内存泄漏。

    如下这个典例:

    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context;
        }
        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个 Context,所以这个 Context 的生命周期的长短至关重要:

    1. 传入的是 Application 的 Context:这将没有任何问题,因为单例的生命周期和 Application 的一样长。
    2. 传入的是 Activity 的 Context:当这个 Context 所对应的 Activity 退出时,由于该 Context 和 Activity 的生命周期一样长(Activity 间接继承于 Context),所以当前 Activity 退出时它的内存并不会被回收,因为单例对象持有该 Activity 的引用。

    所以正确的单例应该修改为下面这种方式:

    public class AppManager {
        private static AppManager instance;
        private Context context;
        private AppManager(Context context) {
            this.context = context.getApplicationContext();
        }
        public static AppManager getInstance(Context context) {
            if (instance != null) {
                instance = new AppManager(context);
            }
            return instance;
        }
    }
    

    这样不管传入什么 Context 最终将使用 Application 的 Context,而单例的生命周期和应用的一样长,这样就防止了内存泄漏。

    • 非静态内部类创建静态实例造成的内存泄漏

    有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

    public class MainActivity extends AppCompatActivity {
        private static TestResource mResource = null;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            if(mResource == null){
                mResource = new TestResource();
            }
            //...
        }
        class TestResource {
        //...
        }
    }
    

    这样就在 Activity 内部创建了一个非静态内部类的单例,每次启动 Activity 时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而又使用了该非静态内部类创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该 Activity 的引用,导致 Activity 的内存资源不能正常回收。

    正确的做法为:

    将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,请使用 ApplicationContext。

    • Handler 造成的内存泄漏

    Handler 的使用造成的内存泄漏问题应该说最为常见了,平时在处理网络任务或者封装一些请求回调等 api 都应该会借助 Handler 来处理,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏,如下示例:

    public class MainActivity extends AppCompatActivity {
    	private Handler mHandler = new Handler() {
    	    @Override
    	    public void handleMessage(Message msg) {
    	    //...
    	    }
    	};
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            loadData();
        }
        private void loadData(){
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    }
    

    这种创建 Handler 的方式会造成内存泄漏,由于 mHandler 是 Handler 的非静态匿名内部类的实例,所以它持有外部类 Activity 的引用,我们知道消息队列是在一个 Looper 线程中不断轮询处理消息,那么当这个 Activity 退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的 Message 持有 mHandler 实例的引用,mHandler 又持有 Activity 的引用,所以导致该 Activity 的内存资源无法及时回收,引发内存泄漏,所以另外一种做法为:

    public class MainActivity extends AppCompatActivity {
        private MyHandler mHandler = new MyHandler(this);
        private TextView mTextView ;
        private static class MyHandler extends Handler {
            private WeakReference<Context> reference;
            public MyHandler(Context context) {
            reference = new WeakReference<>(context);
            }
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) reference.get();
                if(activity != null){
                activity.mTextView.setText("");
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView)findViewById(R.id.textview);
            loadData();
        }
    
        private void loadData() {
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    }
    

    创建一个静态 Handler 内部类,然后对 Handler 持有的对象使用弱引用,这样在回收时也可以回收 Handler 持有的对象,这样虽然避免了 Activity 泄漏,不过 Looper 线程的消息队列中还是可能会有待处理的消息,所以我们在 Activity 的 Destroy 时或者 Stop 时应该移除消息队列中的消息,更准确的做法如下:

    public class MainActivity extends AppCompatActivity {
        private MyHandler mHandler = new MyHandler(this);
        private TextView mTextView ;
        private static class MyHandler extends Handler {
            private WeakReference<Context> reference;
            public MyHandler(Context context) {
            reference = new WeakReference<>(context);
            }
            @Override
            public void handleMessage(Message msg) {
                MainActivity activity = (MainActivity) reference.get();
                if(activity != null){
                activity.mTextView.setText("");
                }
            }
        }
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mTextView = (TextView)findViewById(R.id.textview);
            loadData();
        }
    
        private void loadData() {
            //...request
            Message message = Message.obtain();
            mHandler.sendMessage(message);
        }
    
        @Override
        protected void onDestroy() {
            super.onDestroy();
            mHandler.removeCallbacksAndMessages(null);
        }
    }
    

    使用 mHandler.removeCallbacksAndMessages(null); 是移除消息队列中所有消息和所有的 Runnable。 当然也可以使用 mHandler.removeCallbacks(); 或 mHandler.removeMessages(); 来移除指定的 Runnable 和 Message。

    • 线程造成的内存泄漏

    对于线程造成的内存泄漏,也是平时比较常见的,异步任务和 Runnable 都是一个匿名内部类,因此它们对当前 Activity 都有一个隐式引用。 如果 Activity 在销毁之前,任务还未完成,那么将导致 Activity 的内存资源无法回收,造成内存泄漏。 正确的做法还是使用静态内部类的方式,如下:

    static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private WeakReference<Context> weakReference;
    
        public MyAsyncTask(Context context) {
            weakReference = new WeakReference<>(context);
        }
    
        @Override
        protected Void doInBackground(Void... params) {
            SystemClock.sleep(10000);
            return null;
        }
    
        @Override
        protected void onPostExecute(Void aVoid) {
            super.onPostExecute(aVoid);
            MainActivity activity = (MainActivity) weakReference.get();
            if (activity != null) {
            //...
            }
        }
    }
    static class MyRunnable implements Runnable{
        @Override
        public void run() {
            SystemClock.sleep(10000);
        }
    }
    //——————
    new Thread(new MyRunnable()).start();
    new MyAsyncTask(this).execute();
    

    这样就避免了 Activity 的内存资源泄漏,当然在 Activity 销毁时候也应该取消相应的任务 AsyncTask::cancel(),避免任务在后台执行浪费资源。

    • 资源使用完未关闭

    BraodcastReceiver,ContentObserver,FileObserver,Cursor,Callback等在 Activity onDestroy 或者某类生命周期结束之后一定要 unregister 或者 close 掉,否则这个 Activity 类会被 system 强引用,不会被内存回收。

    不要直接对 Activity 进行直接引用作为成员变量,如果不得不这么做,请用 private WeakReference mActivity 来做,相同的,对于Service 等其他有自己声明周期的对象来说,直接引用都需要谨慎考虑是否会存在内存泄露的可能。

    其他优化

    • 常用数据结构优化
    1. ArrayMap 及 SparseArray 是 android 的系统 API,是专门为移动设备而定制的。 用于在一定情况下取代 HashMap 而达到节省内存的目的。 对于 key 为 int 的 HashMap 尽量使用 SparceArray 替代,大概可以省 30% 的内存,而对于其他类型,ArrayMap 对内存的节省实际并不明显,10% 左右,但是数据量在 1000 以上时,查找速度可能会变慢。
    2. 在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+”。
    • 枚举

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

    • View 复用
    1. 使用 ListView 时 getView 里尽量复用 conertView,同时因为 getView 会频繁调用,要避免频繁地生成对象。 优先考虑使用 RecyclerView 代替 ListView。
    2. 重复的布局优先使用 ,使用 减少 view 的层级,对于可以延迟初始化的页面,使用 。
    • 谨慎使用多进程

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

    • 系统资源

    尽量使用系统组件,图片甚至控件的 id。 例如:@android:color/xxx,@android:style/xxx。

    使用工具检查内存泄漏

    即使在编码时将上述情况都考虑了,往往会有疏忽的地方,更何况通常情况下是团队开发。 所以不仅仅要在编码时考虑内存优化的情况,当出现内存泄漏时,更有效更准确的定位问题才是最重要的方式。 内存泄漏不像 bug,排查起来相对复杂一些,下面介绍下常用的检查方式。

    使用 Lint 代码静态检查

    Lint 是 Android Studio 自带的工具,使用很简单找到 Analyze -> Inspect Code 然后选择想要扫面的区域即可。

    这里写图片描述

    选择 Lint 扫描区域。

    这里写图片描述

    对可能引起性能问题的代码,Lint 都会进行提示。

    这里写图片描述

    使用 Android Studio 自带的 Monitor Memory 检查

    一般在 Android Studio 的底部可以找到 Android Monitor。

    这里写图片描述

    可以看到当前 App的内存变动比较大,很有可能出现了内存泄漏。 点击 Dump Java Heap,等一段时间会自动生成 Heap Snapshot 文件。

    这里写图片描述

    在 Captures 中可以找到 hprof 文件。

    这里写图片描述

    在右侧找到 Analyzer Tasks 并打开,点击图中 Perform Analysis 按钮开始分析。

    这里写图片描述

    通过分析结果可以看到 TestActivity 泄漏了,从左侧 Reference Tree 中可以看到是 TestActivity 中的 context 泄露了。

    这里写图片描述

    我们来看下代码:

    public class TestActivity extends AppCompatActivity {
    
        private static Context context;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test);
    
            context = this;
    
        }
    }
    

    代码中 context 为静态的引用了当前 Activity 造成了当前 Activity 无法释放。

    一般的通过 使用 Android Studio 自带的 Monitor Memory 可以定位到内存泄漏所在的类,更详细的信息需要借助 Memory Analyzer Tool(MAT)工具。

    使用 Memory Analyzer Tool 检查

    首先下载 Memory Analyzer Tool 下载地址

    在 Android Studio 中先将 hprof 文件导出为 MAT 可以识别的 hprof 文件。

    这里写图片描述

    打开刚才导出的文件。

    这里写图片描述

    经过分析后会显示如下,Leak Suspectss 是一个关于内存泄露猜想的饼图,Problem Suspect 1 是泄露猜想的描述。

    这里写图片描述

    Overview 是一个概况图,把内存的消耗以饼状图形式显示出来,鼠标在每个饼块区域划过或者点击,就会在 Inspector 栏目显示这块区域的相关信息。 MAT 从多角度提供了内存分析,其中包括 Histogram、 Dominator Tree、 Leak Suspects 和 Top consumers 等。
    这里写图片描述

    这里我们使用 Histogram 进行分析,切换到 Histogram 页面。 这个页面主要有 4 个列,Class Name、 Objects、 Shallow Heap 和 Retained Heap。 其中 Class Name 是全类名,Objects 是这个类的对象实例个数。 Shallow Heap 是对象本身占用内存大小,非数组的常规对象,本身内存占用很小,所以这个对泄露分析作用不大。 Retained Heap 指当前对象大小和当前对象能直接或间接引用的对象大小的总和,这个栏目是分析重点。

    这里写图片描述

    内存分析是分析的整个系统的内存泄露,而我们只要查找我们 App 的内存泄露情况。 这无疑增加了很多工作,不过幸亏 Histogram 支持正则表达式查找,在 Regex 中输入我们的包名进行过滤,直奔和我们 App 有关的内存泄露。

    这里写图片描述

    过滤后就显示了我们 App 相关内存信息,按 Retained Heap 大小排列下,发现 MainActivity 和 TestActivity 这两个类问题比较大。 TestActivity 的问题更突出些,所以先从 TestActivity 下手。

    首先看下是哪里的引用导致了 TestActivity 不能被 GC 回收。 右键使用 Merge Shortest Paths to GC Roots 显示距 GC Root 最短路径,当然选择过程中要排除软引用和弱引用,因为这些标记的一般都是可以被回收的。

    这里写图片描述

    进入结果页查看。

    这里写图片描述

    可以看到 TestActivity 不能被 GC 回收是因为 context 没有释放的原因。 我们再来看下代码:

    public class TestActivity extends AppCompatActivity {
    
        private static Context context;
    
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_test);
    
            context = this;
    
        }
    }
    

    使用 LeakCanary 检查

    项目地址:https://github.com/square/leakcanary

    使用方式很简单,参考项目里面的介绍即可。

    这里写图片描述

    ANR

    • 什么是 ANR
    1. ANR:Application Not Responding,即应用无响应。
    2. 为用户在主线程长时间被阻塞是提供交互,提高用户体验。
    3. Android 系统自身的一种检测机制。
    • ANR 的类型

    ANR 一般有三种类型:

    1. KeyDispatchTimeout(5 seconds) : 主要类型按键或触摸事件在特定时间内无响应
    2. BroadcastTimeout(10 seconds) : BroadcastReceiver 在特定时间内无法处理完成
    3. ServiceTimeout(20 seconds) : 小概率类型 Service 在特定的时间内无法处理完成
    • ANR 产生的原因

    超时时间的计数一般是从按键分发给 app 开始。 超时的原因一般有两种:

    1. 当前的事件没有机会得到处理(即 UI 线程正在处理前一个事件,没有及时的完成或者 looper 被某种原因阻塞住了)
    2. 当前的事件正在处理,但没有及时完成。
    • ANR 出现流程分析
    1. 输入时间响应超时导致ANR流程

    在系统输入管理服务进程(InputManagerService)中有一个线程(InputDispathcerThread)专门管理输入事件的分发,在该线程处理输入事件的过程中,回调用 InputDispatcher 对象方法不断的检测处理过程是否超时,一旦超时,则会通过一些列的回调调用 InputMethod 对象的 notifyANR 方法,其会最终出发 AMS 中 handler 对象的 SHOW_NOT_RESPONDING_MSG 这个事件,显示ANR对话框。

    1. 广播发生ANR流程

    广播分为三类:普通的,有序的,异步的。 只有有序(ordered)的广播才会发生超时,而在 AndroidManifest 中注册的广播都会被当做有序广播来处理,会被放在广播的队列中串行处理。 AMS 在处理广播队列时,会设置一个超时时间,当处理一个广播达到超时时间的限制时,就会触发 BroadcastQueue 类对象的 processNextBroadcast 方法来判断是否超时,如果超时,就会终止该广播,触发ANR对话框。

    1. UI线程

    UI 线程主要包括如下:

    Activity : onCreate(), onResume(), onDestroy(), onKeyDown(), onClick(), etc 生命周期方法里。
    AsyncTask : onPreExecute(), onProgressUpdate(), onPostExecute(), onCancel, etc 这些异步更改 UI 界面的方法里。
    Mainthread handler : handleMessage(), post*(runnable r), getMainLooper(), etc 通过 handler 发送消息到主线程的 looper,即占用主线程 looper 的。

    • ANR 执行流程

    了解 ANR 执行流程有利于我们制定 ANR 监控策略和获取 ANR 的相关信息,ANR 的执行步骤如下:

    1. 系统捕获到 ANR 发生;
    2. Process 依次向本进程及其他正在运行的进程发送 Linux 信号量 3;
    3. 进程接收到 Linux 信号量,并向 /data/anr/traces.txt 中写入进程信息;
    4. Log 日志打印 ANR 信息;
    5. 进程进入 ANR 状态(此时可以获取到进程 ANR 信息);
    6. 弹出 ANR 提示框;
    7. 提示框消失,进程回归正常状态。

    由于向 /data/anr/traces.txt 文件中写入信息耗时较长,从 Input ANR 触发到弹出 ANR 提示框一般在 10s 左右(不同 rom 时间不同)。

    • 发生 ANR 如何定位

    当 App 的进程发生 ANR 时,系统让活跃的 Top 进程都进行了一下 dump,进程中的各种 Thread 就都 dump 到这个 trace 文件里了,所以 trace 文件中包含了每一条线程的运行时状态。 traces.txt 的文件放在 /data/anr/ 下. 可以通过 adb 命令将其导出到本地:

    $ adb pull data/anr/traces.txt .
    

    通过分析 traces.txt 文件,查找 App 包名关键信息来定位 ANR。

    参考资料

    Android Bitmap的内存大小是如何计算的?

    Android性能优化之常见的内存泄漏

    使用新版Android Studio检测内存泄露和性能

    Android 应用内存泄漏的定位、分析与解决策略

    Android 系统稳定性 - ANR

    其他系列

    Gradle 系列

    更多文章:

    这是我博客长期更新的项目,欢迎大家 Star。
    https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode

    我的公众号

    欢迎你「扫一扫」下面的二维码,关注我的公众号,可以接受最新的文章推送,有丰厚的抽奖活动和福利等着你哦!?

    如果你有什么疑问或者问题,可以 点击这里 提交 issue,也可以发邮件给我 jeanboy@foxmail.com

    同时欢迎你 Android技术进阶:386463747 来一起交流学习,群里有很多大牛和学习资料,相信一定能帮助到你!

    展开全文
  • 前言成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌...

    前言

    成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~。

    内存优化可以说是性能优化中最重要的优化点之一,可以说,如果你没有掌握系统的内存优化方案,就不能说你对Android的性能优化有过多的研究与探索。本篇,笔者将带领大家一起来系统地学习Android中的内存优化。

    可能有不少读者都知道,在内存管理上,JVM拥有垃圾内存回收的机制,自身会在虚拟机层面自动分配和释放内存,因此不需要像使用C/C++一样在代码中分配和释放某一块内存。Android系统的内存管理类似于JVM,通过new关键字来为对象分配内存,内存的释放由GC来回收。并且Android系统在内存管理上有一个Generational Heap Memory模型,当内存达到某一个阈值时,系统会根据不同的规则自动释放可以释放的内存。即便有了内存管理机制,但是,如果不合理地使用内存,也会造成一系列的性能问题,比如内存泄漏、内存抖动、短时间内分配大量的内存对象等等。下面,我就先谈谈Android的内存管理机制。

    一、Android内存管理机制

    我们都知道,应用程序的内存分配和垃圾回收都是由Android虚拟机完成的,在Android 5.0以下,使用的是Dalvik虚拟机,5.0及以上,则使用的是ART虚拟机。

    1.1、Java对象生命周期

    Java代码编译后生成的字节码.class文件从从文件系统中加载到虚拟机之后,便有了JVM上的Java对象,Java对象在JVM上运行有7个阶段,如下:

    • Created

    • InUse

    • Invisible

    • Unreachable

    • Collected

    • Finalized

    • Deallocated

    1、Created(创建)

    Java对象的创建分为如下几步:

    • 1、为对象分配存储空间。

    • 2、构造对象。

    • 3、从超类到子类对static成员进行初始化,类的static成员的初始化在ClassLoader加载该类时进行。

    • 4、超类成员变量按顺序初始化,递归调用超类的构造方法。

    • 5、子类成员变量按顺序初始化,一旦对象被创建,子类构造方法就调用该对象并为某些变量赋值。

    2、InUse(应用)

    此时对象至少被一个强引用持有。

    3、Invisible(不可见)

    当一个对象处于不可见阶段时,说明程序本身不再持有该对象的任何强引用,虽然该对象仍然是存在的。简单的例子就是程序的执行已经超出了该对象的作用域了。但是,该对象仍可能被虚拟机下的某些已装载的静态变量线程或JNI等强引用持有,这些特殊的强引用称为“GC Root”。被这些GC Root强引用的对象会导致该对象的内存泄漏,因而无法被GC回收。

    4、Unreachable(不可达)

    该对象不再被任何强引用持有。

    5、Collected(收集)

    当GC已经对该对象的内存空间重新分配做好准备时,对象进入收集阶段,如果该对象重写了finalize()方法,则执行它。

    6、Finalized(终结)

    等待垃圾回收器回收该对象空间。

    7、Deallocated(对象空间重新分配)

    GC对该对象所占用的内存空间进行回收或者再分配,则该对象彻底消失。

    注意:
    • 1、不需要使用该对象时,及时置空。

    • 2、访问本地变量优于访问类中的变量。

    1.2 内存分配

    在Android系统中,堆实际上就是一块匿名共享内存。Android虚拟机仅仅只是把它封装成一个mSpace,由底层C库来管理,并且仍然使用libc提供的函数malloc和free来分配和释放内存。

    大多数静态数据会被映射到一个共享的进程中。常见的静态数据包括Dalvik Code、app resources、so文件等等。

    在大多数情况下,Android通过显示分配共享内存区域(如ashmem或者gralloc)来实现动态RAM区域能够在不同进程之间共享的机制。例如,Window Surface在App和Screen Compositor之间使用共享的内存,Cursor Buffers在Content Provider和Clients之间共享内存。

    上面说过,对于Android Runtime有两种虚拟机,Dalvik和ART,它们分配的内存区域块是不同的:

    Dalvik
    • Linear Alloc

    • Zygote Space

    • Alloc Space

    ART
    • Non Moving Space

    • Zygote Space

    • Alloc Space

    • Image Space

    • Large Obj Space

    不管是Dlavik还是ART,运行时堆都分为LinearAlloc(类似于ART的Non Moving Space)、Zygote Space和Alloc Space。Dalvik中的Linear Alloc是一个线性内存空间,是一个只读区域,主要用来存储虚拟机中的类,因为类加载后只需要读的属性,并且不会改变它。把这些只读属性以及在整个进程的生命周期都不能结束的永久数据放到线性分配器中管理,能很好地减少堆混乱和GC扫描,提升内存管理的性能。Zygote Space在Zygote进程和应用程序进程之间共享,Allocation Space则是每个进程独占。Android系统的第一个虚拟机由Zygote进程创建并且只有一个Zygote Space。但是当Zygote进程在fork第一个应用程序进程之前,会将已经使用的那部分堆内存划分为一部分,还没有使用的堆内存划分为另一部分,也就是Allocation Space。但无论是应用程序进程,还是Zygote进程,当他们需要分配对象时,都是在各自的Allocation Space堆上进行。

    当在ART运行时,还有另外两个区块,即ImageSpace和Large Object Space。

    • Image Space:存放一些预加载类,类似于Dalvik中的Linear Alloc。与Zygote Space一样,在Zygote进程和应用程序进程之间共享。

    • Large Object Space:离散地址的集合,分配一些大对象,用于提高GC的管理效率和整体性能。

    注意:Image Space的对象只创建一次,而Zygote Space的对象需要在系统每次启动时,根据运行情况都重新创建一遍。

    1.3、内存回收机制

    在Android的高级系统版本中,针对Heap空间有一个Generational Heap Memory的模型,其中将整个内存分为三个区域:

    • Young Generation(年轻代)

    • Old Generation(年老代)

    • Permanent Generation(持久代)

    模型示意图如下所示:

    1、Young Generation

    由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当次Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。

    2、Old Generation

    一般情况下,年老代中的对象生命周期都比较长。

    3、Permanent Generation

    用于存放静态的类和方法,持久代对垃圾回收没有显著影响。

    总结:内存对象的处理过程如下:
    • 1、对象创建后在Eden区。

    • 2、执行GC后,如果对象仍然存活,则复制到S0区。

    • 3、当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。

    • 4、当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。

    • 5、当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。

    系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。

    执行GC占用的时间与Generation和Generation中的对象数量有关:

    • Young Generation < Old Generation < Permanent Generation

    • Gener中的对象数量与执行时间成正比。

    4、Young Generation GC

    由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。

    5、Old Generation GC

    由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。

    1.4、GC类型

    在Android系统中,GC有三种类型:

    • kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。

    • kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。

    • kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。

    接下来,我们来学会如何分析Android虚拟机中的GC日志,日志如下:

    D/dalvikvm(7030):GC_CONCURRENT freed 1049K, 60% free 2341K/9351K, external 3502K/6261K, paused 3ms 3ms
    

    GC_CONCURRENT是当前GC时的类型,GC日志中有以下几种类型:

    • GC_CONCURRENT:当应用程序中的Heap内存占用上升时,避免Heap内存满了而触发的GC。

    • GC_FOR_MALLOC:这是由于Concurrent GC没有及时执行完,而应用又需要分配更多的内存,这时不得不停下来进行Malloc GC。

    • GC_EXTERNAL_ALLOC:这是为external分配的内存执行的GC。

    • GC_HPROF_DUMP_HEAP:创建一个HPROF profile的时候执行。

    • GC_EXPLICIT:显示调用了System.GC()。(尽量避免)

    再回到上面打印的日志:

    • freed 1049k 表明在这次GC中回收了多少内存。

    • 60% free 2341k/6261K 表明回收后60%的Heap可用,存活的对象大小为2341kb,heap大小是9351kb。

    • external 3502/6261K 是Native Memory的数据。存放Bitmap Pixel Data(位图数据)或者堆以外内存(NIO Direct Buffer)之类的。第一个值说明在Native Memory中已分配3502kb内存,第二个值是一个浮动的GC阈值,当分配内存达到这个值时,会触发一次GC。

    • paused 3ms 3ms 表明GC的暂停时间,如果是Concurrent GC,会看到两个时间,一个开始,一个结束,且时间很短,如如果是其他类型的GC,很可能只会看到一个时间,且这个时间是相对比较长的。并且,越大的Heap Size在GC时导致暂停的时间越长。

    注意:在ART模式下,多了一个Large Object Space,这部分内存并不是分配在堆上,但还是属于应用程序的内存空间。

    在Dalvik虚拟机下,GC的操作都是并发的,也就意味着每次触发GC都会导致其它线程暂停工作(包括UI线程)。而在ART模式下,GC时不像Dalvik仅有一种回收算法,ART在不同的情况下会选择不同的回收算法,比如Alloc内存不够时会采用非并发GC,但在Alloc后,发现内存达到一定阈值时又会触发并发GC。所以在ART模式下,并不是所有的GC都是非并发的。

    总体来看,在GC方面,与Dalvik相比,ART更为高效,不仅仅是GC的效率,大大地缩短了Pause时间,而且在内存分配上对大内存分配单独的区域,还能有算法在后台做内存整理,减少内存碎片。因此,在ART虚拟机下,可以避免较多的类似GC导致的卡顿问题。

    二、优化内存的意义

    • 减少OOM,提高应用稳定性。

    • 减少卡顿,提高应用流畅度。

    • 减少内存占用,提高应用后台运行时的存活率。

    • 减少异常发生和代码逻辑隐患。

    注意:出现OOM是因为内存溢出导致,这种情况不一定会发生在相同的代码,也不一定是出现OOM的代码使用内存有问题,而是刚好执行到这段代码。

    三、避免内存泄漏

    3.1 内存泄漏定义

    Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。

    3.2 使用MAT来查找内存泄漏

    MAT工具可以帮助开发者定位导致内存泄漏的对象,以及发现大的内存对象,然后解决内存泄漏并通过优化内存对象,达到减少内存消耗的目的。

    3.2.1 使用步骤

    1、在https://eclipse.org/mat/downloads.php下载MAT客户端。

    2、从Android Studio进入Profile的Memory视图,选择需要分析的应用进程,对应用进行怀疑有内存问题的操作,结束操作后,主动GC几次,最后export dump文件。

    3、因为Android Studio保存的是Android Dalvik/ART格式的.hprof文件,所以需要转换成J2SE HPROF格式才能被MAT识别和分析。Android SDK自带了一个转换工具在SDK的platform-tools下,其中转换语句为:

    ./hprof-conv file.hprof converted.hprof
    

    4、通过MAT打开转换后的HPROF文件。

    3.2.2 MAT视图

    在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。MAT提供了多种分析维度,其中Histogram、Dominator Tree、Top Consumers和Leak Suspects的分析维度不同。下面分别介绍下:

    1、Histogram

    列出内存中的所有实例类型对象和其个数以及大小,并在顶部的regex区域支持正则表达式查找。

    2、Dominator Tree

    列出最大的对象及其依赖存活的Object。相比Histogram,能更方便地看出引用关系。

    3、Top Consumers

    通过图像列出最大的Object。

    4、Leak Suspects

    通过MAT自动分析内存泄漏的原因和泄漏的一份总体报告。

    分析内存最常用的是Histogram和Dominator Tree两个视图,视图中一共有四列:

    • Class Name:类名。

    • Objects:对象实例个数。

    • Shallow Heap:对象自身占用的内存大小,不包括它引用的对象。非数组的常规对象的Shallow Heap Size由其成员变量的数量和类型决定,数组的Shallow Heap Size由数组元素的类型(对象类型、基本类型)和数组长度决定。真正的内存都在堆上,看起来是一堆原生的byte[]、char[]、int[],对象本身的内存都很小。因此Shallow Heap对分析内存泄漏意义不是很大。

    • Retained Heap:是当前对象大小与当前对象可直接或间接引用到的对象的大小总和,包括被递归释放的。即:Retained Size就是当前对象被GC后,从Heap上总共能释放掉的内存大小。

    3.2.3 查找内存泄漏具体位置

    常规方式:

    • 按照包名类型分类进行实例筛选或直接使用顶部Regex选取特定实例。

    • 右击选中被怀疑的实例对象,选择Merge Shortest Paths to GC Root->exclude all phantom/weak/soft etc references。(显示GC Roots最短路径的强引用)

    • 分析引用链或通过代码逻辑找出原因。

    还有一种更快速的方法就是对比泄漏前后的HPROF数据:

    • 在两个HPROF文件中,把Histogram或者Dominator Tree增加到Compare Basket。

    • 在Compare Basket中单击 ! ,生成对比结果视图。这样就可以对比相同的对象在不同阶段的对象实例个数和内存占用大小,如明显只需要一个实例的对象,或者不应该增加的对象实例个数却增加了,说明发生了内存泄漏,就需要去代码中定位具体的原因并解决。

    注意:如果目标不太明确,可以直接定位当RetainedHeap最大的Object,通过Select incoming references查看引用链,定位到可疑的对象,然后通过Path to GC Roots分析引用链。

    此外,我们知道,当Hash集合中过多的对象返回相同的Hash值时,会严重影响性能,这时可以用Map Collision Ratio查找导致Hash集合的碰撞率较高的罪魁祸首。

    在本人平时的项目开发中,一般会使用如下两种方式来快速对指定页面进行内存泄漏的检测:

    • 1、shell命令+LeakCanary+MAT:运行程序,所有功能跑一遍,确保没有改出问题,完全退出程序,手动触发GC,然后使用adb shell dumpsys meminfo packagename -d命令查看退出界面后Objects下的Views和Activities数目是否为0,如果不是则通过LeakCanary检查可能存在内存泄露的地方,最后通过MAT分析,如此反复,改善满意为止。

    • 2、Profile MEMORY:运行程序,对每一个页面进行内存分析检查。首先,反复打开关闭页面5次,然后收到GC(点击Profile MEMORY左上角的垃圾桶图标),如果此时total内存还没有恢复到之前的数值,则可能发生了内存泄露。此时,再点击Profile MEMORY左上角的垃圾桶图标旁的heap dump按钮查看当前的内存堆栈情况,选择按包名查找,找到当前测试的Activity,如果引用了多个实例,则表明发生了内存泄露。

    3.3 常见内存泄漏场景

    1、资源性对象未关闭

    对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后在置为null。

    2、注册对象未注销
    3、类的静态变量持有大数据对象
    4、非静态内部类的静态实例

    该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。

    解决方案:

    将内部类设为静态内部类或将内部类抽取来作为一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。

    5、Handler临时性内存泄漏

    Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。

    解决方案:

    • 1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。

    • 2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。

    注意:AsyncTask内部也是Handler机制,同样存在内存泄漏风险,当其一般是临时性的。

    6、容器中的对象没清理造成的内存泄漏
    7、WebView

    WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。

    解决方案:

    为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

    3.4 内存泄漏监控

    一般使用LeakCanary进行内存泄漏的监控即可,具体使用和原理分析请参见我之前的文章Android主流三方库源码分析(六、深入理解Leakcanary源码)。

    除了基本使用外,我们还可以自定义处理结果,首先,继承DisplayLeakService实现一个自定义的监控处理Service,代码如下:

    public class LeakCnaryService extends DisplayLeakServcie {
    
        private final String TAG = “LeakCanaryService”;
    
        @Override
        protected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {
            ...
        }
    }
    

    重写afterDefaultHanding方法,在其中处理需要的数据,三个参数的定义如下:

    • heapDump:堆内存文件,可以拿到完成的hprof文件,以使用MAT分析。

    • result:监控到的内存状态,如是否泄漏等。

    • leakInfo:leak trace详细信息,除了内存泄漏对象,还有设备信息。

    然后在install时,使用自定义的LeakCanaryService即可,代码如下:

    public class BaseApplication extends Application {
    
        @Override
        public void onCreate() {
            super.onCreate();
            mRefWatcher = LeakCanary.install(this, LeakCanaryService.calss, AndroidExcludedRefs.createAppDefaults().build());
        }
    
        ...
    
    }
    

    经过这样的处理,就可以在LeakCanaryService中实现自己的处理方式,如丰富的提示信息,把数据保存在本地、上传到服务器进行分析。

    注意:LeakCanaryService需要在AndroidManifest中注册。

    四、优化内存空间

    4.1 对象引用

    从Java 1.2版本开始引入了三种对象引用方式:SoftReference、WeakReference和PhantomReference三个引用类,引用类的主要功能就是能够引用但仍可以被垃圾回收器回收的对象。在引入引用类之前,只能使用Strong Reference,如果没有指定对象引用类型,默认是强引用。

    1、强引用

    如果一个对象具有强引用,GC就绝对不会回收它。当内存空间不足时,JVM会抛出OOM错误。

    2、软引用

    如果一个对象只具有软引用,则内存空间足够,GC时就不会回收它;如果内存不足,就会回收这些对象的内存。可用来实现内存敏感的高速缓存。

    软引用可以和一个ReferenceQueue(引用队列)联合使用,如果软引用引用的对象被垃圾回收器回收,JVM会把这个软引用加入与之关联的引用队列中。

    3、弱引用

    在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

    注意:可能需要运行多次GC,才能找到并释放弱引用对象。

    4、虚引用

    只能用于跟踪即将对被引用对象进行的收集。虚拟机必须与ReferenceQueue类联合使用。因为它能够充当通知机制。

    4.2 减少不必要的内存开销

    1、AutoBoxing

    自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,这样就会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。

    检测方式:使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。

    2、内存复用
    • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。

    • 视图复用:可以使用ViewHolder实现ConvertView复用。

    • 对象池:显示创建对象池,实现复用逻辑,对相同的类型数据使用同一块内存空间。

    • Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。

    4.3 使用最优的数据类型的

    1、HashMap与ArrayMap

    HashMap是一个散列链表,向HashMap中put元素时,先根据key的HashCode重新计算hash值,根据hash值得到这个元素在数组中的位置,如果数组该位置上已经存放有其它元素了,那么这个位置上的元素将以链表的形式存放,新加入的放在链头,最后加入的放在链尾。如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。也就是说,向HashMap插入一个对象前,会给一个通向Hash阵列的索引,在索引的位置中,保存了这个Key对象的值。这意味着需要考虑的一个最大问题是冲突,当多个对象散列于阵列相同位置时,就会有散列冲突的问题。因此,HashMap会配置一个大的数组来减少潜在的冲突,并且会有其他逻辑防止链接算法和一些冲突的发生。

    ArrayMap提供了和HashMap一样的功能,但避免了过多的内存开销,方法是使用两个小数组,而不是一个大数组。并且ArrayMap在内存上是连续不间断的。

    总体来说,在ArrayMap中执行插入或者删除操作时,从性能角度上看,比HashMap还要更差一些,但如果只涉及很小的对象数,比如1000以下,就不需要担心这个问题了。因为此时ArrayMap不会分配过大的数组。

    2、枚举类型

    使用枚举类型的dex size是普通常量定义的dex size的13倍以上,同时,运行时的内存分配,一个enum值的声明会消耗至少20bytes。

    枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。所以Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef和StringDef,用来提供编译期的类型检查。

    注意:使用IntDef和StringDef需要在Gradle配置中引入相应的依赖包:

    compile 'com.android.support:support-annotations:22.0.0'
    
    3、LruCache

    最近最少使用缓存,使用强引用保存需要缓存的对象,它内部维护了一个由LinkedHashMap组成的双向列表,不支持线程安全,LruCache对它进行了封装,添加了线程安全操作。当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近最少被访问的)被丢弃,之后可以被GC回收。

    除了普通的get/set方法之外,还有sizeOf方法,它用来返回每个缓存对象的大小。此外,还有entryRemoved方法,当一个缓存对象被丢弃时调用的方法,当第一个参数为true:表明环处对象是为了腾出空间而被清理时。否则,表明缓存对象的entry被remove移除或者被put覆盖时。

    注意:分配LruCache大小时应考虑应用剩余内存有多大。

    4、图片内存优化

    在Android默认情况下,当图片文件解码成位图时,会被处理成32bit/像素。红色、绿色、蓝色和透明通道各8bit,即使是没有透明通道的图片,如JEPG隔世是没有透明通道的,但然后会处理成32bit位图,这样分配的32bit中的8bit透明通道数据是没有任何用处的,这完全没有必要,并且在这些图片被屏幕渲染之前,它们首先要被作为纹理传送到GPU,这意味着每一张图片会同时占用CPU内存和GPU内存。

    减少内存开销常用方式如下:

    • 1、设置位图的规格:当显示小图片或对图片质量要求不高时可以考虑使用RGB_565,用户头像或圆角图片一般可以尝试ARGB_4444。通过设置inPreferredConfig参数来实现不同的位图规格,代码如下所示:

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    BitmapFactory.decodeStream(is, null, options);
    
    • 2、inSampleSize:位图功能对象中的inSampleSize属性实现了位图的缩放功能,代码如下所示:

    BitampFactory.Options options = new BitmapFactory.Options();
    // 设置为4就是1/4大小的图片。因此,图片大小总会比原始图片小一倍以上。
    options.inSampleSize = 4;
    BitmapFactory.decodeSream(is, null, options);
    
    • 3、inScaled,inDensity和inTargetDensity实现更细的缩放图片:当inScaled设置为true时,系统会按照现有的密度来划分目标密度,代码如下所示:

    BitampFactory.Options options = new BitampFactory.Options();
    options.inScaled = true;
    options.inDensity = srcWidth;
    options.inTargetDensity = dstWidth;
    BitmapFactory.decodeStream(is, null, options);
    

    上述三种方案的缺点:使用了过多的算法,导致图片显示过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。最好的方案是结合这两个方法,达到最佳的性能结合,首先使用inSampleSize处理图片,转换为接近目标的2次幂,然后用inDensity和inTargetDensity生成最终想要的准确大小,因为inSampleSize会减少像素的数量,而基于输出密码的需要对像素重新过滤。但获取资源图片的大小,需要设置位图对象的inJustDecodeBounds值为true,然后继续解码图片文件,这样才能生产图片的宽高数据,并允许继续优化图片。总体的代码如下所示:

    BitmapFactory.Options options = new BitampFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(is, null, options);
    options.inScaled = true;
    options.inDensity = options.outWidth;
    options.inSampleSize = 4;
    Options.inTargetDensity = desWith * options.inSampleSize;
    options.inJustDecodeBounds = false;
    BitmapFactory.decodeStream(is, null, options);
    
    4、inBitmap

    可以结合LruCache来实现,在LruCache移除超出cache size的图片时,暂时缓存Bitamp到一个软引用集合,需要创建新的Bitamp时,可以从这个软用用集合中找到最适合重用的Bitmap,来重用它的内存区域。

    注意:新申请的Bitmap与旧的Bitmap必须有相同的解码格式,并且在Android 4.4之前,只能重用相同大小的Bitamp的内存区域,而Android 4.4之后可以重用任何bitmap的内存区域。

    五、图片管理模块设计与实现

    在设计一个模块时,需要考虑以下几点:

    • 1、单一职责

    • 2、避免不同功能之间的耦合

    • 3、接口隔离

    在编写代码前先画好UML图,确定每一个对象、方法、接口的功能,首先尽量做到功能单一原则,在这个基础上,再明确模块与模块的直接关系,最后使用代码实现。

    5.1 实现异步加载功能

    1.实现网络图片显示

    ImageLoader是实现图片加载的基类,其中ImageLoader有一个内部类BitmapLoadTask是继承AsyncTask的异步下载管理类,负责图片的下载和刷新,MiniImageLoader是ImageLoader的子类,维护类一个ImageLoader的单例,并且实现了基类的网络加载功能,因为具体的下载在应用中有不同的下载引擎,抽象成接口便于替换。代码如下所示:

    public abstract class ImageLoader {
        private boolean mExitTasksEarly = false;   //是否提前结束
        protected boolean mPauseWork = false;
        private final Object mPauseWorkLock = new   Object();
    
        protected ImageLoader() {
    
        }
    
        public void loadImage(String url, ImageView imageView) {
            if (url == null) {
                return;
            }
    
            BitmapDrawable bitmapDrawable = null;
            if (bitmapDrawable != null) {
                imageView.setImageDrawable(bitmapDrawable);
            } else {
                final BitmapLoadTask task = new BitmapLoadTask(url, imageView);
                task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            }
        }
    
        private class BitmapLoadTask extends AsyncTask&lt;Void, Void, Bitmap&gt; {
    
            private String mUrl;
            private final WeakReference&lt;ImageView&gt; imageViewWeakReference;
    
            public BitmapLoadTask(String url, ImageView imageView) {
                mUrl = url;
                imageViewWeakReference = new WeakReference&lt;ImageView&gt;(imageView);
            }
    
            @Override
            protected Bitmap doInBackground(Void... params) {
                Bitmap bitmap = null;
                BitmapDrawable drawable = null;
    
                synchronized (mPauseWorkLock) {
                    while (mPauseWork &amp;&amp; !isCancelled()) {
                        try {
                            mPauseWorkLock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
    
                if (bitmap == null
                        &amp;&amp; !isCancelled()
                        &amp;&amp; imageViewWeakReference.get() != null
                        &amp;&amp; !mExitTasksEarly) {
                    bitmap = downLoadBitmap(mUrl);
                }
                return bitmap;
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                if (isCancelled() || mExitTasksEarly) {
                    bitmap = null;
                }
    
                ImageView imageView = imageViewWeakReference.get();
                if (bitmap != null &amp;&amp; imageView != null) {
                    setImageBitmap(imageView, bitmap);
                }
            }
    
            @Override
            protected void onCancelled(Bitmap bitmap) {
                super.onCancelled(bitmap);
                synchronized (mPauseWorkLock) {
                    mPauseWorkLock.notifyAll();
                }
            }
        }
    
        public void setPauseWork(boolean pauseWork) {
            synchronized (mPauseWorkLock) {
                mPauseWork = pauseWork;
                if (!mPauseWork) {
                    mPauseWorkLock.notifyAll();
                }
            }
        }
    
        public void setExitTasksEarly(boolean exitTasksEarly) {
            mExitTasksEarly = exitTasksEarly;
            setPauseWork(false);
        }
    
        private void setImageBitmap(ImageView imageView, Bitmap bitmap) {
            imageView.setImageBitmap(bitmap);
        }
    
        protected abstract Bitmap downLoadBitmap(String    mUrl);
    }
    

    setPauseWork方法是图片加载线程控制接口,pauseWork控制图片模块的暂停和继续工作,一般在listView等控件中,滑动时停止加载图片,保证滑动流畅。另外,具体的图片下载和解码是和业务强相关的,因此在ImageLoader中不做具体的实现,只是定义类一个抽象方法。

    MiniImageLoader是一个单例,保证一个应用只维护一个ImageLoader,减少对象开销,并管理应用中所有的图片加载。MiniImageLoader代码如下所示:

    public class MiniImageLoader extends ImageLoader {
    
        private volatile static MiniImageLoader sMiniImageLoader = null;
    
        private ImageCache mImageCache = null;
    
        public static MiniImageLoader getInstance() {
            if (null == sMiniImageLoader) {
                synchronized (MiniImageLoader.class) {
                    MiniImageLoader tmp = sMiniImageLoader;
                    if (tmp == null) {
                        tmp = new MiniImageLoader();
                    }
                    sMiniImageLoader = tmp;
                }
            }
            return sMiniImageLoader;
        }
    
        public MiniImageLoader() {
            mImageCache = new ImageCache();
        }
    
        @Override
        protected Bitmap downLoadBitmap(String mUrl) {
            HttpURLConnection urlConnection = null;
            InputStream in = null;
            try {
                final URL url = new URL(mUrl);
                urlConnection = (HttpURLConnection) url.openConnection();
                in = urlConnection.getInputStream();
                Bitmap bitmap = decodeSampledBitmapFromStream(in, null);
                return bitmap;
    
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (urlConnection != null) {
                    urlConnection.disconnect();
                    urlConnection = null;
                }
    
                if (in != null) {
                    try {
                        in.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            return null;
        }
    
        public Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options) {
            return BitmapFactory.decodeStream(is, null, options);
        }
    }
    

    其中,volatile保证了对象从主内存加载。并且,上面的try …cache层级太多,Java中有一个Closeable接口,该接口标识类一个可关闭的对象,因此可以写如下的工具类:

    public class CloseUtils {
    
        public static void closeQuietly(Closeable closeable) {
            if (null != closeable) {
                try {
                    closeable.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    改造后如下所示:

    finally {
        if  (urlConnection != null) {
            urlConnection.disconnect();
        }
        CloseUtil.closeQuietly(in);
    }
    

    同时,为了使ListView在滑动过程中更流畅,在滑动时暂停图片加载,减少系统开销,代码如下所示:

    listView.setOnScrollListener(new AbsListView.OnScrollListener() {
    
        @Override
        public void onScrollStateChanged(AbsListView absListView, int scrollState) {
            if (scorllState == AbsListView.OnScrollListener.SCROLL_STAE_FLING) {
                MiniImageLoader.getInstance().setPauseWork(true);
            } else {
                MiniImageLoader.getInstance().setPauseWork(false);
            }
        }
    }
    
    2 单个图片内存优化

    这里使用一个BitmapConfig类来实现参数的配置,代码如下所示:

    public class BitmapConfig {
    
        private int mWidth, mHeight;
        private Bitmap.Config mPreferred;
    
        public BitmapConfig(int width, int height) {
            this.mWidth = width;
            this.mHeight = height;
            this.mPreferred = Bitmap.Config.RGB_565;
        }
    
        public BitmapConfig(int width, int height, Bitmap.Config preferred) {
            this.mWidth = width;
            this.mHeight = height;
            this.mPreferred = preferred;
        }
    
        public BitmapFactory.Options getBitmapOptions() {
            return getBitmapOptions(null);
        }
    
        // 精确计算,需要图片is流现解码,再计算宽高比
        public BitmapFactory.Options getBitmapOptions(InputStream is) {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            if (is != null) {
                options.inJustDecodeBounds = true;
                BitmapFactory.decodeStream(is, null, options);
                options.inSampleSize = calculateInSampleSize(options, mWidth, mHeight);
            }
            options.inJustDecodeBounds = false;
            return options;
        }
    
        private static int calculateInSampleSize(BitmapFactory.Options    options, int mWidth, int mHeight) {
            final int height = options.outHeight;
            final int width = options.outWidth;
            int inSampleSize = 1;
            if (height &gt; mHeight || width &gt; mWidth) {
                final int halfHeight = height / 2;
                final int halfWidth = width / 2;
                while ((halfHeight / inSampleSize) &gt; mHeight
                        &amp;&amp; (halfWidth / inSampleSize) &gt; mWidth) {
                    inSampleSize *= 2;
                }
            }
    
            return inSampleSize;
        }
    }
    

    然后,调用MiniImageLoader的downLoadBitmap方法,增加获取BitmapFactory.Options的步骤:

    final URL url = new URL(urlString);
    urlConnection = (HttpURLConnection) url.openConnection();
    in = urlConnection.getInputStream();
    final BitmapFactory.Options options =    mConfig.getBitmapOptions(in);
    in.close();
    urlConnection.disconnect();
    urlConnection = (HttpURLConnection)    url.openConnection();
    in = urlConnection.getInputStream();
    Bitmap bitmap = decodeSampledBitmapFromStream(in,    options);
    

    优化后仍存在一些问题:

    • 1.相同的图片,每次都要重新加载;

    • 2.整体内存开销不可控,虽然减少了单个图片开销,但是在片非常多的情况下,没有合理管理机制仍然对性能有严重影的。

    为了解决这两个问题,就需要有内存池的设计理念,通过内存池控制整体图片内存,不重新加载和解码已经显示过的图片。

    5.2 实现三级缓存

    内存–本地–网络

    1、内存缓存

    使用软引用和弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但是现在不建议。从API 9起(Android 2.3)开始,Android系统垃圾回收器更倾向于回收持有软引用和弱引用的对象,所以不是很靠谱,从Android 3.0开始(API 11)开始,图片的数据无法用一种可遇见的方式将其释放,这就存在潜在的内存溢出风险。

    public class MemoryCache {
    
        private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;
        private LruCache&lt;String, Bitmap&gt; mMemoryCache;
        private final String TAG = "MemoryCache";
        public MemoryCache(float sizePer) {
            init(sizePer);
        }
    
        private void init(float sizePer) {
            int cacheSize = DEFAULT_MEM_CACHE_SIZE;
            if (sizePer &gt; 0) {
                cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);
            }
    
            mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    final int bitmapSize = getBitmapSize(value) / 1024;
                    return bitmapSize == 0 ? 1 : bitmapSize;
                }
    
                @Override
                protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                   super.entryRemoved(evicted, key, oldValue, newValue);
                }
            };
        }
    
        @TargetApi(Build.VERSION_CODES.KITKAT)
        public int getBitmapSize(Bitmap bitmap) {
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.KITKAT) {
                return bitmap.getAllocationByteCount();
            }
            if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.HONEYCOMB_MR1) {
                return bitmap.getByteCount();
            }
    
            return bitmap.getRowBytes() * bitmap.getHeight();
        }
    
        public Bitmap getBitmap(String url) {
            Bitmap bitmap = null;
            if (mMemoryCache != null) {
                bitmap = mMemoryCache.get(url);
            }
            if (bitmap != null) {
                Log.d(TAG, "Memory cache exiet");
            }
    
            return bitmap;
        }
    
        public void addBitmapToCache(String url, Bitmap bitmap) {
            if (url == null || bitmap == null) {
                return;
            }
    
            mMemoryCache.put(url, bitmap);
        }
    
        public void clearCache() {
            if (mMemoryCache != null) {
                mMemoryCache.evictAll();
            }
        }
    }
    

    上述代码中cacheSize百分比占比多少合适?可以基于以下几点来考虑:

    • 1.应用中内存的占用情况,除了图片以外,是否还有大内存的数据需要缓存到内存。

    • 2.在应用中大部分情况要同时显示多少张图片,优先保证最大图片的显示数量的缓存支持。

    • 3.Bitmap的规格,计算出一张图片占用的内存大小。

    • 4.图片访问的频率。

    在应用中,如果有一些图片的访问频率要比其它的大一些,或者必须一直显示出来,就需要一直保持在内存中,这种情况可以使用多个LruCache对象来管理多组Bitmap,对Bitmap进行分级,不同级别的Bitmap放到不同的LruCache中。

    2、bitmap内存复用

    从Android3.0开始Bitmap支持内存复用,也就是BitmapFactoy.Options.inBitmap属性,如果这个属性被设置有效的目标用对象,decode方法就在加载内容时重用已经存在的bitmap,这意味着Bitmap的内存被重新利用,这可以减少内存的分配回收,提高图片的性能。代码如下所示:

    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.HONEYCOMB) {
            mReusableBitmaps = Collections.synchronizedSet(newHashSet&lt;SoftReference&lt;Bitmap&gt;&gt;());
    }
    

    因为inBitmap属性在Android3.0以后才支持,在entryRemoved方法中加入软引用集合,作为复用的源对象,之前是直接删除,代码如下所示:

    if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.HONEYCOMB) {
        mReusableBitmaps.add(new SoftReference&lt;Bitmap&gt;(oldValue));
    }
    

    同样在3.0以上判断,需要分配一个新的bitmap对象时,首先检查是否有可复用的bitmap对象:

    public static Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options, ImageCache cache) {
         if (Build.VERSION.SDK_INT &gt;= Build.VERSION_CODES.HONEYCOMB) {
             addInBitmapOptions(options, cache);
         }
         return BitmapFactory.decodeStream(is, null, options);
     }
    
    @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
         options.inMutable = true;
         if (cache != null) {
             Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
             if (inBitmap != null) {
                 options.inBitmap = inBitmap;
             }
         }
    
     }
    

    接着,我们使用cache.getBitmapForResubleSet方法查找一个合适的bitmap赋值给inBitmap。代码如下所示:

    // 获取inBitmap,实现内存复用
    public Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
        Bitmap bitmap = null;
    
        if (mReusableBitmaps != null &amp;&amp; !mReusableBitmaps.isEmpty()) {
            final Iterator&lt;SoftReference&lt;Bitmap&gt;&gt; iterator = mReusableBitmaps.iterator();
            Bitmap item;
    
            while (iterator.hasNext()) {
                item = iterator.next().get();
    
                if (null != item &amp;&amp; item.isMutable()) {
                    if (canUseForInBitmap(item, options)) {
    
                        Log.v("TEST", "canUseForInBitmap!!!!");
    
                        bitmap = item;
    
                        // Remove from reusable set so it can't be used again
                        iterator.remove();
                        break;
                    }
                } else {
                    // Remove from the set if the reference has been cleared.
                    iterator.remove();
                }
            }
        }
    
        return bitmap;
    }
    

    上述方法从软引用集合中查找规格可利用的Bitamp作为内存复用对象,因为使用inBitmap有一些限制,在Android 4.4之前,只支持同等大小的位图。因此使用了canUseForInBitmap方法来判断该Bitmap是否可以复用,代码如下所示:

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static boolean canUseForInBitmap(
            Bitmap candidate, BitmapFactory.Options targetOptions) {
    
        if (Build.VERSION.SDK_INT &lt; Build.VERSION_CODES.KITKAT) {
            return candidate.getWidth() == targetOptions.outWidth
                    &amp;&amp; candidate.getHeight() == targetOptions.outHeight
                    &amp;&amp; targetOptions.inSampleSize == 1;
        }
        int width = targetOptions.outWidth / targetOptions.inSampleSize;
        int height = targetOptions.outHeight / targetOptions.inSampleSize;
    
        int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
    
        return byteCount &lt;= candidate.getAllocationByteCount();
    }
    
    3、磁盘缓存

    由于磁盘读取时间是不可预知的,所以图片的解码和文件读取都应该在后台进程中完成。DisLruCache是Android提供的一个管理磁盘缓存的类。

    • 1、首先调用DiskLruCache的open方法进行初始化,代码如下:

    public static DiskLruCache open(File directory, int appVersion, int valueCou9nt, long maxSize)
    

    directory一般建议缓存到SD卡上。appVersion发生变化时,会自动删除前一个版本的数据。valueCount是指Key与Value的对应关系,一般情况下是1对1的关系。maxSize是缓存图片的最大缓存数据大小。初始化DiskLruCache的代码如下所示:

    private void init(final long cacheSize,final File cacheFile) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (mDiskCacheLock) {
                    if(!cacheFile.exists()){
                        cacheFile.mkdir();
                    }
                    MLog.d(TAG,"Init DiskLruCache cache path:" + cacheFile.getPath() + "\r\n" + "Disk Size:" + cacheSize);
                    try {
                        mDiskLruCache = DiskLruCache.open(cacheFile, MiniImageLoaderConfig.VESION_IMAGELOADER, 1, cacheSize);
                        mDiskCacheStarting = false;
                        // Finished initialization
                        mDiskCacheLock.notifyAll();
                        // Wake any waiting threads
                    }catch(IOException e){
                        MLog.e(TAG,"Init err:" + e.getMessage());
                    }
                }
            }
        }).start();
    }
    

    如果在初始化前就要操作写或者读会导致失败,所以在整个DiskCache中使用的Object的wait/notifyAll机制来避免同步问题。

    • 2、写入DiskLruCache

    首先,获取Editor实例,它需要传入一个key来获取参数,Key必须与图片有唯一对应关系,但由于URL中的字符可能会带来文件名不支持的字符类型,所以取URL的MD4值作为文件名,实现Key与图片的对应关系,通过URL获取MD5值的代码如下所示:

    private String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }
    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i &lt; bytes.length; i++) {
            String hex = Integer.toHexString(0xFF &amp; bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }
    

    然后,写入需要保存的图片数据,图片数据写入本地缓存的整体代码如下所示:

     public void saveToDisk(String imageUrl, InputStream in) {
        // add to disk cache
        synchronized (mDiskCacheLock) {
            try {
                while (mDiskCacheStarting) {
                    try {
                        mDiskCacheLock.wait();
                    } catch (InterruptedException e) {}
                }
                String key = hashKeyForDisk(imageUrl);
                MLog.d(TAG,"saveToDisk get key:" + key);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (in != null &amp;&amp; editor != null) {
                    // 当 valueCount指定为1时,index传0即可
                    OutputStream outputStream = editor.newOutputStream(0);
                    MLog.d(TAG, "saveToDisk");
                    if (FileUtil.copyStream(in,outputStream)) {
                        MLog.d(TAG, "saveToDisk commit start");
                        editor.commit();
                        MLog.d(TAG, "saveToDisk commit over");
                    } else {
                        editor.abort();
                        MLog.e(TAG, "saveToDisk commit abort");
                    }
                }
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    接着,读取图片缓存,通过DiskLruCache的get方法实现,代码如下所示:

    public Bitmap  getBitmapFromDiskCache(String imageUrl,BitmapConfig bitmapconfig) {
        synchronized (mDiskCacheLock) {
            // Wait while disk cache is started from background thread
            while (mDiskCacheStarting) {
                try {
                    mDiskCacheLock.wait();
                } catch (InterruptedException e) {}
            }
            if (mDiskLruCache != null) {
                try {
    
                    String key = hashKeyForDisk(imageUrl);
                    MLog.d(TAG,"getBitmapFromDiskCache get key:" + key);
                    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
                    if(null == snapShot){
                        return null;
                    }
                    InputStream is = snapShot.getInputStream(0);
                    if(is != null){
                        final BitmapFactory.Options options = bitmapconfig.getBitmapOptions();
                        return BitmapUtil.decodeSampledBitmapFromStream(is, options);
                    }else{
                        MLog.e(TAG,"is not exist");
                    }
                }catch (IOException e){
                    MLog.e(TAG,"getBitmapFromDiskCache ERROR");
                }
            }
        }
        return null;
    }
    

    最后,要注意读取并解码Bitmap数据和保存图片数据都是有一定耗时的IO操作。所以这些方法都是在ImageLoader中的doInBackground方法中调用,代码如下所示:

    @Override
    protected Bitmap doInBackground(Void... params) {
    
        Bitmap bitmap = null;
        synchronized (mPauseWorkLock) {
            while (mPauseWork &amp;&amp; !isCancelled()) {
                try {
                    mPauseWorkLock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        if (bitmap == null &amp;&amp; !isCancelled()
                &amp;&amp; imageViewReference.get() != null &amp;&amp; !mExitTasksEarly) {
            bitmap = getmImageCache().getBitmapFromDisk(mUrl, mBitmapConfig);
        }
    
        if (bitmap == null &amp;&amp; !isCancelled()
                &amp;&amp; imageViewReference.get() != null &amp;&amp; !mExitTasksEarly) {
            bitmap = downLoadBitmap(mUrl, mBitmapConfig);
        }
        if (bitmap != null) {
            getmImageCache().addToCache(mUrl, bitmap);
        }
    
        return bitmap;
    }
    

    5.3 图片加载三方库

    目前使用最广泛的有Picasso、Glide和Fresco。Glide和Picasso比较相似,但是Glide相对于Picasso来说,功能更多,内部实现更复杂,对Glide有兴趣的同学可以阅读这篇文章Android主流三方库源码分析(三、深入理解Glide源码)。Fresco最大的亮点在与它的内存管理,特别实在低端机和Android 5.0以下的机器上的优势更加明显,而使用Fresco将很好地解决图片占用内存大的问题。因为,Fresco会将图片放到一个特别的内存区域,当图片不再显示时,占用的内存会自动释放。以下总结以下其优点:

    • 1、内存管理。

    • 2、渐进式呈现:先呈现大致的图片轮廓,然后随着图片下载的继续,呈现逐渐清晰的图片。

    • 3、支持更多的图片格式:如Gif和Webp。

    • 4、图像加载策略丰富:其中的Image Pipeline可以为同一个图片指定不同的远程路径,比如先显示已经存在本地缓存中的图片,等高清图下载完成之后在显示高清图集。

    缺点:

    • 安装包过大,所以对图片加载和显示要求不是比较高的情况下建议使用Glide。

    六、内存优化

    对于内存泄漏,其本质可理解为无法回收无用的对象。这里我总结了我在项目中遇到的一些常见的内存泄漏案例(包含解决方案)和常见的内存优化技术。

    6.1、常见的内存泄漏案例(完善3.3小节):
    • 1、单例造成的内存泄漏(使用Application的Context)

    • 2、非静态内部类创建静态实例造成的内存泄漏(将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,就使用Application的Context)

    • 3、Handler造成的内存泄漏(将Handler类独立出来或者使用静态内部类)

    • 4、线程造成的内存泄漏(将AsyncTask和Runnable类独立出来或者使用静态内部类)

    • 5、BraodcastReceiver、Bitmap等资源未关闭造成的内存泄漏(应该在Activity销毁时及时关闭或者注销)

    • 6、使用ListView时造成的内存泄漏(在构造Adapter时,使用缓存的convertView)

    • 7、集合容器中的内存泄露(在退出程序之前,将集合里的东西clear,然后置为null,再退出程序)

    • 8、WebView造成的泄露(为WebView另外开启一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放)

    6.2、常见的内存优化点:

    1、只需要UI提供一套高分辨率的图,图片建议放在drawable-xxhdpi文件夹下,这样在低分辨率设备中图片的大小只是压缩,不会存在内存增大的情况。如若遇到不需缩放的文件,放在drawable-nodpi文件夹下。

    2、图片优化:

    • 颜色模式:RGB_8888->RGB_565

    • 降低图片大小

    • 降低采样率

    3、在App退到后台内存紧张即将被Kill掉时选择重写onTrimMemory()方法去释放掉图片缓存、静态缓存来自保。

    4、item被回收不可见时释放掉对图片的引用:

    • ListView:因此每次item被回收后再次利用都会重新绑定数据,只需在ImageView onDetachFromWindow的时候释放掉图片引用即可。

    • RecyclerView:因为被回收不可见时第一选择是放进mCacheView中,这里item被复用并不会只需bindViewHolder来重新绑定数据,只有被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用。

    5、集合优化:Android提供了一系列优化过后的数据集合工具类,如SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap工具类会相对比较低效,因为它需要为每一个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。

    6、避免创作不必要的对象:字符串拼接使用StringBuffer,StringBuilder。

    7、onDraw方法里面不要执行对象的创建。

    8、使用static final 优化成员变量。

    9、使用增强型for循环语法。

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

    11、适当采用软引用和弱引用。

    12、采用内存缓存和磁盘缓存。

    13、尽量采用静态内部类,可避免潜在由于内部类导致的内存泄漏。

    七、总结

    对于内存优化,一般都是通过使用MAT等工具来进行检查和使用LeakCanary等内存泄漏监控工具来进行监控,以此来发现问题,再分析问题原因,解决发现的问题或者对当前的实现逻辑进行优化,优化完后在进行检查,直到达到预定的性能指标。下一篇,将会深入分析一下Android系统的存储优化相关技术,尽请期待~

    热文推荐:

    1、大厂又有新的开源项目了,赶紧来领取...

    2、面试官问我:一个 TCP 连接可以发多少个 HTTP 请求?我竟然回答不上来...

    3、程序员疑似出bug被吊打!菲律宾的高薪工作机会了解一下?

    4、“一键脱衣”的DeepNude下架后,我在GitHub上找到它涉及的技术

    5、原生Android开发的路该怎么走

    6、太厉害了,终于有人能把TCP/IP 协议讲的明明白白了

    7、腾讯开源超实用的UI轮子库,我是轮子搬运工

    8、腾讯新开源一吊炸天神器—零反射全动态Android插件框架正式开源

                            喜欢 就关注吧,欢迎投稿!

    640?wx_fmt=jpeg

    展开全文
  • php程序内存优化之数组操作优化

    千次阅读 2019-05-10 10:37:56
    这篇文章其实是上篇文章的内存优化部分。博主的php程序在执行的时候,报错: Out of memory (allocated 364904448) (tried to allocate 262144 bytes)       也就是传统的内存不足...
  • 内存作为计算机程序运行最重要的资源之一,需要运行过程中做到合理的资源分配与回收,不合理的内存占用轻则使得用户应用程序运行卡顿、ANR、黑屏,重则导致用户应用程序发生 OOM(out of...
  • Android内存优化之图片优化

    万次阅读 多人点赞 2019-04-08 11:52:23
    关于图片优化,大概如下 ...:Android系统的进程(APP级别)有最大的内存限制,超过这个限制系统就会抛出)OOM错误 图片OOM问题产生的几种情况 1.一个页面一次加载过多的图片 2.加载大图片没有进行压缩 3....
  • Android内存优化

    千次阅读 2018-05-23 14:42:38
    参考Android性能优化(四)之内存优化实战 内存优化的套路: 解决所有的内存泄漏 集成LeakCanary,可以方便的定位出90%的内存泄漏问题; 通过反复进出可疑界面,观察内存增减的情况,Dump Java Heap获取当前堆栈...
  • Android-APP内存优化

    千次阅读 2018-04-09 14:27:10
    为什么要进行内存优化 APP运行内存限制,OOM导致APP崩溃 APP性能:流畅性、响应速度和用户体验
  • Unity内存优化

    千次阅读 2018-05-25 13:43:44
    内存的开销无外乎以下三大部分:1.资源内存占用;2.引擎模块自身内存占用;3.托管堆内存占用。资源内存占用一、纹理纹理资源可以说是几乎所有游戏项目中占据最大内存开销的资源。一个6万面片的场景,网格资源最大才...
  • influxDB内存优化

    千次阅读 2020-06-30 15:44:11
    // 翻译 InfluxDB会在系统上为每个series维护一个内存索引, 而随着这些series的增加,RAM内存使用率也会增加。 如果series cardinality如果太高,就会导致操作系统触发OOMKiller机制, 将Influxdb进程KILL掉. 使用...
  • 本文主要讲解性能优化中的内存优化,希望你们会喜欢 目录 1. 定义 优化处理 应用程序的内存使用、空间占用 2. 作用 避免因不正确使用内存 &amp; 缺乏管理,从而出现 内存泄露(ML)、内存...
  • Android内存优化大总结

    万次阅读 2018-11-14 15:57:44
    内存简介: RAM(random access memory)随机存取存储器。说白了就是内存。 一般Java在内存分配时会涉及到以下区域: 寄存器(Registers):速度最快的存储场所,因为寄存器位于处理器内部,我们在程序...
  • 布局优化   我们可以通过手机开发者选项中的调试GPU过度来查看布局绘制的复杂情况。 避免overdraw,使用RelativeLayout来替换掉多层LineraLayout嵌套 减少View树的层数,Google Api文档中建议View树...
  • 本篇是 Android 内存优化的进阶篇,难度可以说达到了炼狱级别,建议对内存优化不是非常熟悉的仔细看看前篇文章: Android性能优化之内存优化,其中详细分析了以下几大模块: 1)、Android的内存管理机制 2)、优化...
  • c++的内存优化

    千次阅读 2019-01-18 17:43:33
    2.内存优化有哪些方式 3.怎样做内存优化 概述: 我们常常在开发场景下提及的内存是指程序内存. 程序内存可以分为以下五种: 1、 栈区(stack):栈的空间是连续的, 先进后出能保证不会产生内存碎片, 由高地址向低...
  • JVM 内存优化

    千次阅读 2019-07-06 12:39:21
    新上线一个java服务,或者是RPC或者是WEB站点, 内存的设置该怎么设置呢?设置成多大比较合适,既不浪费内存,又不影响性能呢? 分析: 依据的原则是根据Java Performance里面的推荐公式来进行设置。 ...
  • Android 性能优化之内存优化

    万次阅读 2016-12-18 20:33:11
    Android 性能优化之内存优化 Android 应用程序在开发的过程中内存的准确控制是判断一个程序好坏的重要标准之一: 一、假如我们开发的程序内存溢出、泄漏了会引发那些实质性的问题呢?  1、程序卡顿、响应速度变慢。...
  • 【Android 性能优化】—— 详解内存优化的来龙去脉

    万次阅读 多人点赞 2017-03-19 16:43:06
    本文是【Android 性能优化】系列的第二篇文章,我们在第一篇【Android 性能优化】—— UI篇中主要介绍了Android界面的优化的原理以及方法,这一篇中我们将着重介绍Android的内存优化。本文的篇幅很长,但是请不要嫌...
  • Android系统内存优化

    千次阅读 2019-02-23 21:53:58
    内存管理是Linux/Android OS的灵魂,关于内存的知识太多,此章节只为了后续的内存评估和优化对常用知识进行梳理,不对具体概念进行讲解,有必要的话需要先查阅相关知识点。 1.1 Linux内存地址空间 内存管理最基础的...
  • 文章目录前言一、内存优化策略二、具体优化的点1.避免内存泄漏2.Bitmap等大对象的优化策略3.原生API回调释放内存3.内存排查工具总结 前言 在开始之前需要先搞明白一个问题,为什么要做内存优化?或者说做内存优化的...
  • 上篇博客我们写到了 Java/Android 内存的分配以及相关 GC 的详细分析,这篇博客我们会继续分析 Android 中内存泄漏的检测以及相关案例,和 Android 的内存优化相关内容。 Android 内存泄漏案例和检测   常见...
  • 在Java中,内存的分配是由程序完成的,而内存的释放是由垃圾收集器(Garbage Collection,GC)完成的,程序员不需要通过调用函数来释放内存,但也随之带来了内存泄漏的可能
  • Lua性能优化—Lua内存优化

    万次阅读 2017-06-09 14:15:46
    笔者在这里和大家分享了一些在Lua性能优化方面的经验。比如说拿到原始数据后,如何处理过滤数据、信息的经验,从而更快更准确地定位问题。如果大家有更好更精准的处理数据、过滤信息的方法请不吝赐教。
  • CocosCreator客户端优化系列(三):内存优化 转载请保留原文链接:https://blog.csdn.net/zzx023/article/details/85319733 静态资源的内存管理 可以参考官方文档:管理场景 静态资源指的是场景中直接或...
  • iOS性能优化-内存优化

    千次阅读 2018-09-10 14:25:31
    一、为什么需要内存优化 二、内存管理 三、常见问题 四、内存占用 五、检测工具 摘要 一、为什么需要内存优化 The easy answer is users have a better experience. Not only will your app launch faster....
  • 易语言源码易语言物理内存优化源码.rar 易语言源码易语言物理内存优化源码.rar 易语言源码易语言物理内存优化源码.rar 易语言源码易语言物理内存优化源码.rar 易语言源码易语言物理内存优化源码.rar 易语言源码...
  • 易语言模块内存优化.rar 易语言模块内存优化.rar 易语言模块内存优化.rar 易语言模块内存优化.rar 易语言模块内存优化.rar 易语言模块内存优化.rar
  • 背景根据Apple官方WWDC的回答,减少内存可以让用户体验到更快的启动速度,不会因为内存过大而导致Crash,可以让APP存活的更久。对于高德地图来说,根据线上数据的分析,内存过高会导...
  • RocketMQ 内存优化

    千次阅读 2018-12-14 10:40:19
    RocketMQ 内存优化1.启动脚本的内存调整1.mqbroker和mqnamesrv的内存调整 RocketMQ 的默认内存占用非常高,是4×4g的,普通人是消耗不起的,所以第一件事情就是调整RocketMQ的占用内存。 调整RocketMQ的内存目前我所...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 950,292
精华内容 380,116
关键字:

内存优化