内存优化
内存管理机制
Android应用在Android虚拟机上运行,应用程序的内存分配和垃圾回收机制都由虚拟机完成。
Android系统中内存管理通过new关键字来为对象分配内存,内存的释放由垃圾回收器(GC)完成。在开发过程中不必显式的管理内存。
在Android系统,虚拟机有两种运行模式:Dalvik和ART(待深入)
内存回收机制 Generation Heap Memory
Android高级系统版本中,针对堆(Heap)有一个Generation Heap Memory模型。
Generation Heap Memory模型中,最近分配的对象存放在Young Generation区域,对象在某个时机触发GC(Garbage Collection) ,没有被回收的对象会根据不同的规则,有的可能会被移动到Old Generation,最后累计一定时间再移动到Permanent Generation区域。 系统会根据内存中不同的内存数据分别执行不同的GC操作。
GC通过对象是否 被存活 对象所引用来确定是否回收该对象,进而动态回收无任何引用的对象来释放内存空间。
Young Generation
YG被分为3个区域,一个较Eden区,两个内存较小的Survivor区,比例为8:1:1。Eden区存放新生对象,Survivor区用来存放每次垃圾回收后存活的对象。
Eden(伊甸园)区用来存放新建出来的对象,当经历一次GC而存活下来的对象会被复制到Survivor区的S0区域,再经过一次GC仍然存活下来的对象会被复制到Survivor区的S1区域,对象会被这样反复捣腾15次之后进入Old Generation。
Old Generation
老年代用来存放年轻代复制过来的对象,一般年老代的对象生命周期比较长。
大对象(需要连续内存空间的Java对象,典型就是很长的字符串以及数组)直接进入年老代。直接进入老年代的好处是避免Eden区和两个Survivor区之间复制算法执行的时候产生大量内存复制。
Permanent Generation
指内存的永久保存区域,这个区域不归Java堆内存范围。Class在被Loader时就会被放到此,如果Java应用很大,例如类(class)很多,那么建议增大这个区域的大小来满足加载这些类的内存需求
用于存放静态的类和方法,持久代对内存回收没有显著的影响。
安卓分配与回收:
Android系统并不会对Heap中空闲内存区域做碎片整理。
系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间。
优化内存的意义
Android(虚拟机)上,申请和释放内存实在系统层承担的,自动分配对象的内存,然后虚拟机跟踪每个内存对象,一旦决定要释放哪个对象,就会将内存释放到内存堆中,这个过程不需要开发者去干涉,这种机制,在Android系统中有一个垃圾回收器,也就是GC。
在GC的过程中,任何线程都会被暂定,也包括UI线程,并且不同区域释放内存的速度也不相同,GC完成之后才能继续执行原线程。
但是,如果大量重复???,处理其他事情的时间就会被压缩,进而影响到渲染工作。这种短时间内申请大量内存的时候,并且极少被有效释放,就会导致内存泄漏。一旦剩余内存达到阈值,垃圾回收活动就会被启动。在GC频繁工作构成汇总消耗大量时间,并且可能导致卡顿。
结论:频繁的GC会加剧卡顿的情况,影响流畅性,因此,尽量减少GC行为,提高应用的流畅度,减少卡顿发生的概率。
如果内存空间的峰值达到内存空间的阈值,或者频繁地发生这种内存峰值(毛刺现象),刚好这个峰值时,需要申请一大块内存空间,就会由于堆内存空间不足导致OOM。
总的来说导致OOM的主要原因是申请的内存荏苒不能满足应用程序这次需要的内存(内存不足)。
避免内存泄漏
泄漏的定义
Java对象有自己的生命周期,当这个对象不再被使用时,本应该被垃圾回收掉,但是由于某些原因,对象没有被引用也没有被回收,仍然存在于内存中,这就意味着对象已经泄漏了。(无用但是没死)
常见场景
1.资源性对象未关闭
资源对象(如Cursor,File文件等)往往都用了一些缓存,在不使用时,应该关闭它们。
它们的缓存不仅存在于Java虚拟机中,还存在于Java虚拟机以外。如果仅仅将对象引用置为null,而不关闭它们,往往会造成内存泄漏。
todo 缺少例子
2.注册对象未注销
如果注册对象在注册后没有注销,会导致观察者列表中维持这个对象的引用,阻止垃圾回收,一般发生在注册广播接收器,注册观察者等。
todo:观察者模式需要了解
举例:假设一个Activity要监听电话服务,获取信号强度等信息,在Act定义一个Listener对象,并且注册到TelephoneManager服务中,理论上在Act退出后Act对象被释放。
但是 释放Act对象时,忘记取消之前注册的Listener对象,这就回导致Act对象无法被回收,如果不断进入Act,最终会犹豫大量Act对象无法被回收而引起频繁GC,甚至导致OOM。
3.类的静态变量持有大数据对象
静态变量长期持有对象的引用,阻止垃圾回收,如果静态变量持有大的数据对象,如Bitmap等,很容易引起内存不足等问题。
4.非静态内部类的静态实例 ???
非静态内部类会持有一个外部类的引用,如果非静态内部类的实例是静态的,就会间接长期持有外部类的引用,阻止垃圾回收。
public class MainActivity extends AppCompatActivity{
TestModule mTestModule = null;
void onCreat(Bundle savedInstanceState){
setContentView();
if(null == mTstModule){
mTsetModule = new TestModule(this);
}
}
class TestModule{
private Context mContext = null;
public TestModule(Context ctx){
mContext = ctx;
}
}
}
5.Handler临时性内存泄漏
mHandler是Handler的非静态匿名内部类的实例,所以它持有外部类Active的引用,并且消息队列是在一个Looper线程中不断轮询处理消息,那么就会出现一种情况,当Act退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有mHandler实例引用,mHander又持有Act的引用,所以导致Act的内存资源无法回收,引发泄漏。
避免内存泄漏需要修改两个地方:
- 使用静态Handler内部类,让后堆Handler持有的对象使用弱引用,这样回收时,也可以回收Handler所持有的对象。
- 在Activity的Destroy或者Stop时,应该取消消息队列中的消息,避免Looper线程的消息队列中有待处理的消息要处理。
public class HanderFixActivity extends AppCompatActivity {
private MyHandler myHandler = new MyHandler(this);
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
/**
* Handler子类,重写handleMessage方法
*/
private static class MyHandler extends Handler {
private WeakReference<Context> mContext = null;
public MyHandler(Context context) {
mContext = new WeakReference<Context>(context);//外面传进来的上下文赋值
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
/**
* 异步操作,发送消息
*/
private void doGetDataAsyncTask() {
Message message = Message.obtain();
message.what = 1;
message.obj = "存储数据";
myHandler.sendMessage(message);
}
@Override
protected void onDestroy() {
myHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
}
6.容器中的对象没有清理造成的内存泄漏
通常把一个对象的引用加入集合中,在不需要该对象时,如果没有把它引用从集合中清理掉,这个集合就会越来越大。如果这个集合是static,情况会更糟。
7.WebView
Android中WebView不仅仅存在很大的兼容问题,不同Android版本的WebView也存在差异,加上不同厂商定制ROM中WebView也存在差异。更严重的是WebView都存在泄漏问题,在应用中还要使用一次,内存就不会被释放。
通常的解决办法为WebView开启独立的一个进程,使用AIDL与应用程序进行通信,WebView所在的进程可以根据业务需求选择销毁时机,达到正常释放内存的目的。
优化内存空间
没有内存泄漏并不意味着不需要对内存进行优化,在移动设备上,由于存储空间有限,因此使用最小内存对象或者较少的资源开销,同时能让GC更高效的回收资源,保持稳定高效的运行才最好。
(更小的内存,干更多的活)
1.对象引用
Java1.2开始引入三种对象引用方式:
软引用(SoftReference)、弱引用(WeakReference)、强引用(PhantomReference)。
如果没有指定引用对象类型,默认是强引用。
-
强引用
强引用是使用普遍的引用。如果对象是强引用,GC就绝不会回收它。当内存不足时,Java虚拟机会抛出OM错误,不会回收强引用的对象来解决内存不足的问题。因此,如果是强引用对象,在生命周期中如果不再需要使用定记得释放掉或转成弱引用,以便回收。
-
软引用
软引用在保持引用对象的同时,保证在虚拟机报告内存不足的情况之前,清除所有的软引用。
关键之处在于,GC在运行时可能会(也可能不会)释放弱引用对象。对象是否被释放取决GC的算法以及GC运行时可用内存数量如果对象是软引用,则内存空间足够,GC时不会回收它,如果内存不足,就会回收这些对象的内存,并且在内有回收该对象时,该对象可以被程序使用。软引用可用来实现内存敏感的速缓存。
-
弱引用 弱引用类的一个典型用途就是规范化映射canonicalized mapping)。对于生命周期较长且创建开销不大的对象,弱引用比较有用。垃圾收集器运行时如果碰到了弱引用对象,将释放 WeakReference引用对象。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器时一个优先级很低的线程,因此不定会很快发现那些只具有弱引用的对象。
-
虚引用 虚引用只能用于跟踪即将???
2.减少不必要的内存开销
-
AutoBoxing
boolean(8bits)
int(32bits)
float(32bits)
long(64bits)
复杂数据类型(通过一个类描述),如Boolean、Integer(16字节)、Float等
Integer num = 0;//16字节 for(int i =0;i < 100;i++){//int 4字节 num +=i; } -
内存复用
在Android中,某些资源可以重复使用,并且系统也会提供相应的接口或者方法。
-
1.有效利用系统自带资源
系统内置了大量资源,比如颜色,字符串,常用Icon,还有些动画和页面的样式及简单的布局,可以直接使用。
好处:直接使用系统自带资源可以减少内存的开销,减少APK体积提高复用性。
-
2.视图复用
出现大量重复子组件,可以使用ViewHolder实现ConvertView复用,这基本上是所有空间的处理方式,如ListView,GridView等。
-
3.对象池
可以在程序设计的时候就显试地在程序中创建对象池,然后实现复用逻辑 减少对象的重复创建,从而减少内存的分配与回收
-
4.BitMap对象复用
利用Bitmap中inBitmap的高级特性,提高系统在Bitmap的分配与释放效率,不仅可以达到内存复用,还能提高读写速度 使用inBitmap属性可以告知Bitmap解析器尝试使用已经存在的内存区域,新生成的Bitmap对象会尝试使用之前那张bitmap在heap中占据的pixel data内存区域,而不是申请一块新内存。
3.使用最优的数据类型
-
HashMap和ArrayMap
HashMap是一个用哈希表实现建值对的结合,向HashMap中put元素时,先根据Key的HashCode重新计算Hash值,根据hash值得到这个元素在数组中的位置
Key-(hashcode)--->hash-通过哈希值找到数组中的位置---->如果没有元素----->直接放到该数组中的该位置
------>如果有元素------->这个位置上的元素将以链表的形式存放,新的在链头,旧的在链尾。
向HashMap插入一个对象前,会有一个通向Hash阵列的索引,在索引位置中,保存这个Key对象的值。
这就存在巨大冲突,当多个对象散列于阵列相同位置时,就会有散列冲突问题。因此,HashMap会配置一 个大的数组来减少这种潜在的冲突,并且会有其他逻辑防止链接算法和一些冲突发生。
这种大数组对于内存的开销来说很大,从内存节省的角度来讲,是非常不理想的。
HashMap<Integer, String> hashMap = new HashMap<>();
hashMap.put(1,"请叫我第一名");
hashMap.put(2,"请叫我第二名");
hashMap.put(3,"请叫我第三名");
String value = hashMap.get(1);
ArrayMap
AM提供了和HM一样的功能,避免了过多的内存开销
AM使用2个小数组。一个存放Key Hash过后的顺序列表,一个按Key的顺序记录Key-Value值,根据Key数组的顺序,交织在一起。
需要获取Value时,AM会计算Key转Hash值,然后使用二分查找对应的Index,然后通过指针找到另一个数组中的值。如果第二组Key和前一个Key不一致,就认为发生碰撞冲突。为了解决这个问题,AM以Key为中心,分别上下展开,逐个对比查找。
因此带来一个问题,随着AM中对象数量增加,需要访问单独对象的时间也会变长。
AM把想要删除的元素放到最后,其他元素前移,或者复制一份,并删除想要的值。插入需要重新配置数组,添加完成后移动所有元素来保证AM顺序。
总的来说:AM中执行插入或者删除操作,比HM要差一些,但是涉及小对象,影响不大。因为小对象时,HM比AM更占用内存资源。
AM遍历起来比HM更简单高效。
HashMap<Object,Object> mHashMap =new HashMap<>();
for (Iterator it =mHashMap.entrySet().iterator(); it.hasNext(); ) {
Object obj = it.next();
}
ArrayMap<Object,Object> mArrayMap = new ArrayMap<>();
for (int i = 0; i < mArrayMap.size(); i++) {
Object key = mArrayMap.keyAt(i);
}
使用AM的条件:
对象数目小于1000,但访问比较多,或者插入和删除不高
当有映射容器,有映射发生,碧清所有映射容器也是AM。
Tips:在性能优化这方面,有时候需要用时间换空间,有时候用空间换时间,找到两者的平衡点最为关键,达到最佳效果
-
枚举
public final int ONE = 1; public final int TWO = 2; public int getNum(int num) throws IllegalAccessException { switch (num){ case ONE: return 1; case TWO: return 2; default: throw new IllegalAccessException("Unknow"); } }getNum(int num)参数不安全,如果传入不是1,2就会抛异常,也就是说对参数没有约束,会导致业务上带来一个异常逻辑,增加了不安全因素。
public enum NUMBER{ NUMBER_ONE, NUMBER_TWO; }; public int getNum(NUMBER number) throws IllegalAccessException { switch (number){ case NUMBER_ONE: return 1; case NUMBER_TWO: return 2; default: throw new IllegalArgumentException("Unknow"); } }可以看出,参数被约束到枚举中,这样就不会再做容错处理。
枚举的最大优点是类型安全,但是内存开销是定义常量的三倍。
-
LruCache
最近最少使用缓存,它用强引用保存需要缓存的对象,内部维护一个队列(实际是LinkedhashMap内部的双向链表,LruCache对其进行了封装,添加了线程安全操作),当其中的一个值被访问时,它被放到队列的尾部,当缓存满了,头部的值会被丢弃,之后可以被垃圾回收。
private LruCache<String, Bitmap> mLruCache;
/**
* 构造函数
*/
public MyImageLoader() {
//设置最大缓存空间为运行时内存的 1/8
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 8;
mLruCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
//计算一个元素的缓存大小
return value.getByteCount();
}
};
}
/**
* 添加图片到 LruCache
*
* @param key
* @param bitmap
*/
public void addBitmap(String key, Bitmap bitmap) {
if (getBitmap(key) == null) {
mLruCache.put(key, bitmap);
}
}
/**
* 从缓存中获取图片
*
* @param key
* @return
*/
public Bitmap getBitmap(String key) {
return mLruCache.get(key);
}
/**
* 从缓存中删除指定的 Bitmap
*
* @param key
*/
public void removeBitmapFromMemory(String key) {
mLruCache.remove(key);
}
几个重要方法:
1.public final V get(K key)
返回cache中key对应的值,调用方法后,被访问的值会移动到队列的尾部。
2.public final V put(K key, V value)
根据key存放value,存放的值会被移动到队列微博。
3.protected int sizeOf(K key, V value)
返回每个缓存对象的大小,用来判断缓存是否快满了,这个方法必须重写。
4.protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue)
当一个缓存对象被丢弃时调用的方法,这是个空方法,可以重写不是必须。
总结:Android官网推荐使用LruCache作为图片内存缓存,里面保存一定数量强引用。
LruCache不能太大也不能太小。
相关文章
《内存优化》