最全的Android内存优化技巧
如需转载请评论或简信,并注明出处,未经允许不得转载
目录
前言
在Android中,内存是十分宝贵的资源,内存优化有助于提高用户的体验,所以学习内存优化技巧是非常重要的。本文主要介绍性能优化的一些手段,但是为了便于理解以及融会贯通,建议先了解Android内存管理机制
减小对象的内存占用
尽量减少新分配出来的对象占用内存的大小,使用更加轻量的对象
- 使用性能高的数据结构
基本数据类型的包装类占用内存较大,如果不是特别需要包装类的话,就不要用包装类(可以从 Allocation Tracker 看到,Integer
要占 16 字节,而 int 只占 4 个字节)。Hashmap
的泛型规定了只能用包装类,所以 get,put 等操作均会涉及到自动拆装箱,在操作频繁时可能会出现性能问题
我们可以用 Android 中提供的一些容器来替代 Java 中的一些容器,比如:SparseBoolArray
,SparseIntArray
,SparseLongArray
,LongSparseArray
等,以上这几个容器的key
都是集合名中的那个基本类型,value
都是Object
。他们都是用 ArrayMap
实现的,ArrayMap
会对 key
占用空间进行压缩,并且可以通过普通 for
循环进行遍历( keyAt(i), valueAt(i) )这样遍历效率高于迭代器遍历(foreach
底层就是迭代器)。在容器 size
小于 1000 或者嵌套 map
的情况下,适合用 Sparse
系列替换 HashMap
HashMap 的数据结构如下:
可以看到,HaspMap
创建时直接申请了一些空间来存 key
的hash
值,在 key
数量没有占满这些空间时,就很浪费。我们再来看看 ArrayMap
的数据结构 :
有关Hashmap
及hash
算法分析见:深入理解 hashcode() 和 HashMap 中的hash 算法
- 避免在Android里面使用Enum
Enum比起使用static常量会至少增加2倍以上bytes的APK大小,增加5到10倍的RAM
Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android
具体原因见:Android中避免使用枚举类(Enum)
- 减小Bitmap对象的内存占用
Bitmap
极容易消耗内存,减小创建出来的Bitmap
的内存占用可谓是重中之重,通常来说有以下两个措施:
-
inSampleSize
:缩放比例,在把图片载入内存之前,我们需要先计算出一个合适的缩放比例,避免不必要的大图载入。 -
decode format
:解码格式,选择ARGB_8888
/RBG_565
/ARGB_4444
/ALPHA_8
更多关于Bitmap的优化技巧:Bitmap性能优化
- 使用压缩后的图片
在涉及给到资源图片时,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用更小的图片。尽量使用更小的图片不仅可以减少内存的使用,还能避免出现大量的InflationException
。假设有一张很大的图片被XML
文件直接引用,很有可能在初始化视图时会因为内存不足而发生InflationException
,这个问题的根本原因其实是发生了OOM
这里推荐一个比较好用的图片压缩网站:tinypng
- 某些场景下,用shape替代图片
有时候会有一些渐变或者描边的UI效果,如果可以用<shape/>
画的就尽量不要直接引用图片资源了,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:startColor="#000000"
android:angle="0"
android:endColor="#ffffff"/>
</shape>
- 代码混淆&减少不必要的类、类库
不必要的类、对象以及功能库会带来巨大的性能开销(每个类的实例在运行内存中占12-16个字节)。代码混淆可以去除无用代码,通过语意模糊对类、字段进行重命名,从而缩小、优化代码,产生更少量的内存映射
有关代码混淆的更多知识见:最全的Android代码混淆使用手册
- 使用Parcelable进行内存间数据传输
Parcelable
是Android特有的类,Parcelable
的性能比Serializable
好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity
间传输数据。而Serializable
可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable(因为android不同版本Parcelable
可能不同,所以不推荐使用Parcelable
进行数据持久化)
内存对象的复用
减少内存对象的复用总的来说一般有两种思路
- 使用对象池技术
- 利用系统框架既有的某些复用特性,减少对象的重复创建
- 复用系统自带的资源
Android系统本身内置了很多的资源,比如string
、color
、drawable
、anim
、style
以及简单layout
等,这些资源都可以在应用程序中直接引用。这样做不仅能减小APK的大小,还可以在一定程度上减少内存的开销
- 列表item的复用
在ListView
/GridView/RecyclerView
等出现大量重复子组件的视图里对itemView
的复用
延伸:现在很多人都推荐用RecyclerView
替代ListView
,但是Google为什么没有给ListView
划上一杠呢,关于RecyclerView和ListView的差异,可以看Android ListView 与 RecyclerView 对比浅析—缓存机制
- Bitmap对象的复用
1)显示RecyclerView
这种需要大量显示图片的控件里,使用LRU的机制来缓存处理好的Bitmap
有关LRU缓存机制更详细的介绍:LRU缓存机制
2)利用inBitmap
的高级特性提高Android系统在Bitmap
分配与释放执行效率
关于inBitmap
具体见:Bitmap性能优化
- 使用handler.obtainMessage()创建Message对象
不要再用new Message()
的方式创建Message
对象啦!handler.obtainMessage()
内部使用了对象池技术,可以有效帮助我们减少创建Message
对象的内存消耗
具体源码分析见:对象池技术:如何正确创建对象
避免对象的内存泄露
内存对象的泄漏,会导致一些不再使用的对象无法及时释放,这样一方面占用了宝贵的内存空间,很容易导致后续需要分配内存的时候,空闲空间不足而出现OOM。显然,这还使得每级Generation的内存区域可用空间变小,GC就会更容易被触发,容易出现内存抖动,从而引起性能问题
关于内存泄漏相关问题具体见:写给程序员的内存泄漏治理手册
内存使用策略优化
优化布局层次,减少内存消耗
越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的时候考虑使用自定义View来达到目的
关于布局优化问题可参考:Android布局优化
使用StringBuilder拼接字符串
在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder
来替代频繁的“+”,否则有可能引起内存抖动
关于内存抖动的介绍见:五分钟了解内存抖动
避免在onDraw()内创建对象
类似onDraw()
等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加内存的使用,而且很容易引起频繁的gc,甚至是内存抖动
选择合适的文件夹存放资源文件
我们知道hdpi
/xhdpi
/xxhdpi
等等不同dpi的文件夹下的图片在不同的设备上会经过scale
的处理。例如我们只在hdpi
的目录下放置了一张100 * 100的图片,那么根据换算关系,xxhdpi
的手机去引用那张图片就会被拉伸到200 * 200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,需要放到assets
或者nodpi
的目录下
try catch某些大内存分配的操作
在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,可以考虑在catch
里面尝试一次降级的内存分配操作。例如decode bitmap
的时候,catch
到OOM,可以尝试把采样比例再增加一倍之后,再次尝试decode
设计合适的缓存大小
例如,在设计ListView
或者GridView
的Bitmap
的LruCache
的时候,需要考虑的点有:
- 应用程序剩下了多少可用的内存空间
- 有多少图片会被一次呈现到屏幕上
- 有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕
- 设备的屏幕大小与密度是多少(一个
xhdpi
的设备会比hdpi
需要一个更大的缓存来hold住同样数量的图片) - 不同的页面针对
Bitmap
的设计的尺寸与配置是什么 - 页面图片被访问的频率是否存在其中的一部分比其他的图片具有更高的访问频繁(如果是,可以保存那些最常访问的到内存中,或者为不同组别的
Bitmap
(按访问频率分组)设置多个LruCache
容器)
使用IntentService
当启动常驻服务时,系统会优先保持服务在后台不断运行,也就是系统会倾向为了保留这个Service
而一直保留Service
所在的进程,这减少了系统能够存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率。另外还需要注意当这个Service
完成任务之后因为停止Service
失败而引起的内存泄漏,建议使用IntentService
,他会在任务执行完成后主动调用stopSelf()
关于Service和IntentService
具体见:Service详解
合理使用数据引用类型
根据不同的引用场景,选择不同的引用类型
-
强引用(
StrongReference
)
强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError
错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。强引用其实也就是我们平时A a = new A()
这个意思 -
软引用(
SoftReference
)
如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存
软引用可以和一个引用队列(ReferenceQueue
)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中 -
弱引用(
WeakReference
)
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象
弱引用可以和一个引用队列(ReferenceQueue
)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中 -
虚引用(
PhantomReference
)
“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue
)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中
谨慎使用SharePreference
- 对于同一个sp,会将整个xml导入到内存中,容易出现为了读取一个配置而将几百k的数据写入内存,造成内存浪费
- 每个sp存储的键值对不宜过多,否则在加载文件数据到内存时会耗时过长,导致sp的相关
get
或put
方法被阻塞,造成UI卡顿及内存浪费 - 频繁更改的配置项和不常更改的配置项应该分开为不同的sp存放
谨慎使用抽象编程
很多时候,开发者会使用抽象类作为”好的编程习惯”,因为抽象能够提升代码的灵活性与可维护性。然而,抽象会导致一个显著的额外内存开销,那些代码会被mapping
到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们
谨慎使用依赖注入框架
依赖注入框架在Android端越来越流行了(如ButterKnife
),因为这些框架往往可以简化代码,方便开发。然而,这些依赖注入框架会通过扫描代码执行许多初始化的操作,这会导致你的代码需要大量的内存空间来mapping
代码,而且mapped pages
会长时间的被保留在内存中。所以如何取舍,就根据实际项目需要来决定了
对依赖注入感兴趣的可以看:手把手教你实现仿ButterKnife依赖注入框架
谨慎使用多进程
使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著增加内存(空进程也会占用约1M的内存)。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用多进程
一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:一个用来操作UI,另外一个给后台的Service
谨慎使用帧动画
帧动画是非常消耗内存的,在使用帧动画之前,我们尽量考虑一下这个动画效果能不能通过补间动画或属性动画来实现。如果是大图做帧动画或者上百帧的动画,那么在低端设备上就极有可能出现OOM,这时候我们可以使用SurfaceView
来实现“帧动画”。
对SurfaceView
感兴趣的可以看帧动画导致OOM?手把手教你用SurfaceView实现帧动画
SurfaceView
继承之View
,但拥有独立的绘制表面,即它不与其宿主窗口共享同一个绘图表面,可以单独在一个线程进行绘制,并不会占用主线程的资源
谨慎使用static对象
因为static
的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎声明static
对象
谨慎使用large heap
当我们非常确定我们的应用需要用到更多的内存,并且知道为什么这些内存必须被保留时,我们可以去使用large heap
。(例如一个大图片的编辑应用)。因为使用额外的内存空间会影响系统整体的体验,并且会使得每次gc的运行时间更长。我们可以通过在manifest
的application
标签下添加largeHeap=true
来为应用声明一个更大的heap阈值。之后可以通过执行ActivityManager.getMemoryClass()
来检查实际获取到的heap阈值
onTrimMemory()
Android 4.0后提供的一个API,在应用生命周期的任何阶段,调用 onTrimMemory()
获取应用程序 当前内存使用情况(以内存级别进行识别),可根据该方法返回的内存紧张级别参数 来释放内存。当视图变为隐藏状态时,则释放内存。当用户跳转到不同的应用 & 视图不再显示时, 应释放应用视图所占的资源
- Application.onTrimMemory()
- Activity.onTrimMemory()
- Fragement.OnTrimMemory()
- Service.onTrimMemory()
- ContentProvider.OnTrimMemory()
TRIM_MEMORY_UI_HIDDEN
这个等级比较常用,此时释放所占用的资源能显著的提高系统的缓存处理容量具体操作:实现当前
Activity
类的onTrimMemory()
后,当用户离开视图时会得到通知;若得到返回的参数 =TRIM_MEMORY_UI_HIDDEN
即代表视图变为隐藏状态,则可释放视图所占用的资源
总结
其实内存优化不光光是要学习一些优化技巧,同时也要学习更多的基础知识,因为很多时候一些内存问题其实是一些不规范的代码写法导致的。所以很多性能问题都尽量在开发过程中去避免,而不是开发完之后再想着有空再回来进行性能优化