写点什么

Android 性能优化之内存优化,android 最新开发语言

用户头像
Android架构
关注
发布于: 刚刚

列出内存中的所有实例类型对象和其个数以及大小,并在顶部的 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 上总共能释放掉的内存大小。

查找内存泄漏具体位置

常规方式

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

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

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


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


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

  • 2、在 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、从首页开始用依次 dump 出每个页面的内存快照文件,然后利用 MAT 的对比功能,找出每个页面相对于上个页面内存里主要增加了哪些东西,做针对性优化。

  • 4、利用 Android Memory Profiler 实时观察进入每个页面后的内存变化情况,然后对产生的内存较大波峰做分析。


此外,除了运行时内存的分析优化,我们还可以对 App 的静态内存进行分析与优化。静态内存指的是在伴随着 App 的整个生命周期一直存在的那部分内存,那我们怎么获取这部分内存快照呢?


首先,确保打开每一个主要页面的主要功能,然后回到首页,进开发者选项去打开"不保留后台活动"。然后,将我们的 app 退到后台,GC,dump 出内存快照。最后,我们就可以将对 dump 出的内存快照进行分析,看看有哪些地方是可以优化的,比如加载的图片、应用中全局的单例数据配置、静态内存与缓存、埋点数据、内存泄漏等等。

3、常见内存泄漏场景

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

1、资源性对象未关闭

对于资源性对象不再使用时,应该立即调用它的 close()函数,将其关闭,然后再置为 null。例如 Bitmap 等资源未关闭会造成内存泄漏,此时我们应该在 Activity 销毁时及时关闭。

2、注册对象未注销

例如 BraodcastReceiver、EventBus 未注销造成的内存泄漏,我们应该在 Activity 销毁时及时注销。

3、类的静态变量持有大数据对象

尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。

4、单例造成的内存泄漏

优先使用 Application 的 Context,如需使用 Activity 的 Context,可以在传入 Context 时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取 Context,如果获取不到,则直接 return 即可。

5、非静态内部类的静态实例

该实例的生命周期和应用一样长,这就导致该静态实例一直持有该 Activity 的引用,Activity 的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用 Context,尽量使用 Application Context,如果需要使用 Activity Context,就记得用完后置空让 GC 可以回收,否则还是会内存泄漏。

6、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 机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似 AsyncTask 或是线程造成的内存泄漏,我们也可以将 AsyncTask 和 Runnable 类独立出来或者使用静态内部类。

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

在退出程序之前,将集合里的东西 clear,然后置为 null,再退出程序

8、WebView

WebView 都存在内存泄漏的问题,在应用中只要使用一次 WebView,内存就不会被释放掉。我们可以为 WebView 开启一个独立的进程,使用 AIDL 与应用的主进程进行通信,WebView 所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。

9、使用 ListView 时造成的内存泄漏

在构造 Adapter 时,使用缓存的 convertView。

4、内存泄漏监控

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


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


public class LeakCnaryService extends DisplayLeakServcie {


private final String TAG = “LeakCanaryService”;


@Overrideprotected void afterDefaultHandling(HeapDump heapDump, AnalysisResult result, String leakInfo) {...}}


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


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

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

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


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


public class BaseApplication extends Application {


@Overridepublic void onCreate() {super.onCreate();mRefWatcher = LeakCanary.install(this, LeakCanaryService.calss, AndroidExcludedRefs.createAppDefaults().build());}


...


}


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

注意

LeakCanaryService 需要在 AndroidManifest 中注册。

四、优化内存空间

1、对象引用

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

1、强引用

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

2、软引用

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


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

3、弱引用

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


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

4、虚引用

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

2、减少不必要的内存开销

1、AutoBoxing

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

检测方式

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

2、内存复用

对于内存复用,有如下四种可行的方式:


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

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

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

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

3、使用最优的数据类型

1、HashMap 与 ArrayMap

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


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


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


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

2、使用 IntDef 和 StringDef 替代枚举类型

使用枚举类型的 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);

5、inBitmap

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


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

6、图片放置优化

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

7、在 App 可用内存过低时主动释放内存

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

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

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

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

9、避免创作不必要的对象

例如,我们可以在字符串拼接的时候使用 StringBuffer,StringBuilder。

10、自定义 View 中的内存优化

例如,在 onDraw 方法里面不要执行对象的创建,一般来说,都应该在自定义 View 的构造器中创建对象。

11、其它的内存优化注意事项

除了上面的一些内存优化点之外,这里还有一些内存优化的点我们需要注意,如下所示:


  • 尽使用 static final 优化成员变量。

  • 使用增强型 for 循环语法。

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

  • 在合适的时候适当采用软引用和弱引用。

  • 采用内存缓存和磁盘缓存。

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

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

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


  • 1、单一职责

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

  • 3、接口隔离


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

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<Void, Void, Bitmap> {


private String mUrl;private final WeakReference<ImageView> imageViewWeakReference;


public BitmapLoadTask(String url, ImageView imageView) {mUrl = url;imageViewWeakReference = new WeakReference<ImageView>(imageView);}


@Overrideprotected Bitmap doInBackground(Void... params) {Bitmap bitmap = null;BitmapDrawable drawable = null;


synchronized (mPauseWorkLock) {while (mPauseWork && !isCancelled()) {try {mPauseWorkLock.wait();} catch (InterruptedException e) {e.printStackTrace();}}}


if (bitmap == null&& !isCancelled()&& imageViewWeakReference.get() != null&& !mExitTasksEarly) {bitmap = downLoadBitmap(mUrl);}return bitmap;}


@Overrideprotected void onPostExecute(Bitmap bitmap) {if (isCancelled() || mExitTasksEarly) {bitmap = null;}


ImageView imageView = imageViewWeakReference.get();if (bitmap != null && imageView != null) {setImageBitmap(imageView, bitmap);}}


@Overrideprotected 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();}


@Overrideprotected 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() {


@Overridepublic 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 > mHeight || width > mWidth) {final int halfHeight = height / 2;final int halfWidth = width / 2;while ((halfHeight / inSampleSize) > mHeight&& (halfWidth / inSampleSize) > 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.整体内存开销不可控,虽然减少了单个图片开销,但是在片非常多的情况下,没有合理管理机制仍然对性能有严重影的。


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

2、实现三级缓存

内存--本地--网络

1、内存缓存

使用软引用和弱引用(SoftReference or WeakReference)来实现内存池是以前的常用做法,但是现在不建议。从 API 9 起(Android 2.3)开始,Android 系统垃圾回收器更倾向于回收持有软引用和弱引用的对象,所以不是很靠谱,从 Android 3.0 开始(API 11)开始,图片的数据无法用一种可遇见的方式将其释放,这就存在潜在的内存溢出风险。 使用 LruCache 来实现内存管理是一种可靠的方式,它的主要算法原理是把最近使用的对象用强引用来存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。使用 LruCache 实现一个图片的内存缓存的代码如下所示:


public class MemoryCache {


private final int DEFAULT_MEM_CACHE_SIZE = 1024 * 12;private LruCache<String, Bitmap> 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 > 0) {cacheSize = Math.round(sizePer * Runtime.getRuntime().maxMemory() / 1024);}


mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {@Overrideprotected int sizeOf(String key, Bitmap value) {final int bitmapSize = getBitmapSize(value) / 1024;return bitmapSize == 0 ? 1 : bitmapSize;}


@Overrideprotected 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 >= Build.VERSION_CODES.KITKAT) {return bitmap.getAllocationByteCount();}if (Build.VERSION.SDK_INT >= 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 支持内存复用,也就是 Bitmap


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


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


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {mReusableBitmaps = Collections.synchronizedSet(newHashSet<SoftReference<Bitmap>>());}


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


if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue));}


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


public static Bitmap decodeSampledBitmapFromStream(InputStream is, BitmapFactory.Options options, ImageCache cache) {if (Build.VERSION.SDK_INT >= 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 && !mReusableBitmaps.isEmpty()) {final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();Bitmap item;


while (iterator.hasNext()) {item = iterator.next().get();


if (null != item && item.isMutable()) {if (canUseForInBitmap(item, options)) {


Log.v("TEST", "canUseForInBitmap!!!!");


bitmap = item;


// Remove from reusable set so it can't be used againiterator.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 < Build.VERSION_CODES.KITKAT) {return candidate.getWidth() == targetOptions.outWidth

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android性能优化之内存优化,android最新开发语言