android的内存优化一般从以下几个方面考虑:
- 内存泄漏
- 内存抖动
- Bitmap
- 代码质量优化
内存泄漏
内存泄漏的本质:不合理的引用导致引用者对象的生命周期>被引用者对象的生命周期。当回收被引用者对象时发现该对象还在被引用状态,无法被回收,就出现了内存泄漏。
常见的内存泄漏场景:
- 非静态匿名内部类
比如经典的在一个Activity里new一个非static 的Handler对象、在Activity里new了一个非静态的Thread或者Runnable。还有很多类似的,尤其是在Activity或者Fragment内引用一个非静态的匿名内部类时,这个类都会持有外部类,相当于持有了context,极易导致context无法被回收,然后就内存泄漏。
解决方法:我们可以把这个内部类设置为静态的;或者不要使用内部类,在外面写个类;如果涉及到Context的必须持有问题,用Application的Context,因为其生命周期较长且唯一,可以不用回收,这样就不会泄漏。
- Static 关键字修饰的成员变量
我们知道,jvm层面static修饰的变量是放在方法区的,方法区是永久代,基本上不会发送GC,或者说其发送GC的条件非常苛刻,而且在java1.8后出现了元空间,回收更是遥遥无期,也就是说static修饰的变量的生命周期跟你的APP应用生命周期一样,如果你的static修饰的成员变量是Context或者持有Context时,那就会导致这个变量回收不掉,会莫名其妙用了很多内存,我们每次启动一个Activity都会new一个Context对象,如此反复你的内存很快就爆掉了。
解决方法:我们在写程序时不要用static修饰类似占用大资源的对象(例如Context,View)。
- 单例模式
这个跟上面的static原理是一样的,因为单例模式是static的,其生命周期很长,所以要特别小心其持有对象导致内存泄漏。例如:常见的观察者模式+单例模式时,我们会在Activity里实现某个接口(观察者接口),然后把这个接口add到单例模式的ArrayList(被观察者),这个过程就相当于单例对象持有了一个Activity对象,如果在Activity destroy时没有把这个Activity从ArrayList里remove掉,那就造成内存泄漏了。
解决办法:单例模式里持有变量时要注意其生命周期的管理,有+也有-,这样才安全。另外在这种情况里,我们可以适当合理地使用WeakReference。
- 集合类
当一个集合使用完后没有清空其持有的对象。例如:我们往ArrayList里add一个对象时,有的时候我们把是把一个对象add进去,但是ArrayList真正持有的是这个对象的引用,所以即使我们把add的这个对象置null,但是还会依然持有对象的引用,也就等于还持有另外对象。
解决办法:当集合不用时,要清空然后置null。例如:arrayList.clear();arrayList=null;
- 资源对象未关闭
例如:registerBroadcast最后没有unregisterBroadcast;数据库操作的cursor没有close;stream流未关闭;Bitmap没有recycle等;
解决办法:关闭或者释放相关的信息
- 其他情况
例如:在ListView里没有复用好View而是创建了大量的View;Webview使用没有关闭;
内存泄漏分析、跟踪、监测的工具:
- Android Studio的Memory;
- MAT(Memory Analysis Tools);
- Heap Viewer;
- Allocation Tracker;
- LeakCanary;
至于如何利用这些工具进行内存的分析,后续我会慢慢补上。
以上都是理论知识,也是普遍的问题存在,而且相对来说比较容易理解,大部分开发人员也熟知,真正的提升需要在实践中,所以有时间大家可以参考外面文章的同时自己多实践。
内存抖动
所谓的内存抖动就是短时间里,内存出现了反复的波动,其出现的原因主要是因为短时间内,我们写代码时创建了大量的对象,频繁地触发了GC机制回收对象,如果回收的速度赶不上你创建的速度,极有可能就OOM了,而且创建了大量的对象再回收会导致磁盘空间占用比较分散,不利于整体分配内存,会影响内存利用率和使用效率等。避免内存抖动注意以下几种:
- 避免for循环、while循环里new了很多对象;
- 避免View的onDraw()方法里创建对象;
- 避免在引用api时创建了大量对象,比如java的字符串拼接api;
- 避免创建大量的Bitmap,尽量用内存池进行管理;
- 避免创建大量的View, 尽可能复用;
- 其他的能够复用的尽量复用对象,用对象池进行管理;
其实内存抖动还是比较容易定位的,这一块的分析和修改也没有太大的难度,就不做过多的分析了。
Bitmap的使用
在很多的项目中,Bitmap占用的内存达到了整个App占用内存的50%,甚至更高。Bitmap的优化一般从如下几个方面考虑:
- 避免加载过大的图片;
- 及时释放不用的Bitmap;
android在早期版本会把Bitmap占用的内存放在Native里,中间(大概2.2后)转到java heap层了,后面(8.0)又转到Native层了。所以什么时候释放,如何释放Bitmap占用的内存是我们要考虑的重点,例如RecyclerView、ListView、ViewPager加载View,当滑动后View不可见时Bitmap的回收。 - Bimap缓冲池的使用,尽可能复用以前分配的内存;
可以参考GlideBitmapPool的设计。 - 适当的WeakRefrence使用;
主要是利于GC回收内存。 - 根据不同分辨率合理配置图片;
理解不同分辨率和density下图片所占用的内存,进行图片配置。 - 采取合理的图片压缩技术;
主要考虑的参数有inTargetDensity,inSampleSize,inJustDecodeBounds,inPreferredConfig,inBitmap等,要理解这些参数的含义(比较简单)。 - 利用缓存机制在业务层面根据不同的图片使用频繁度进行多缓存池+不同的缓存机制进行优化
代码质量
就是写代码过程里要注意一些小细节。
- 尽量不用或者少用枚举,可以用IntDef/@StringDef + @interface来进行限定参数,因为枚举占内存大且容易来带类型不安全问题。
- 当界面不可见时,适当地释放内存,避免内存占用过高同时也可避免进程被回收,因为不可见的进程内存占用越高被回收的概率越高。可以在Activity的onTrimMemory(int level)方法里做。
- 使用Android系统自带的数据存储结构代码Java自带的,例如SparseArray取代HashMap。
- 使用HashMap、ArrayList这些存储结构时,要适当考虑容量,尽可能避免无端扩容带来的性能浪费。
- 利用好Java的引用类型(强软弱虚),合理选择引用类型。
- 谨慎使用多进程。
- 谨慎使用largeHeap。
- 谨慎使用shareprefercnce。
- 用好ProGuard剔除不必要的代码。
- 选好基础数据类型,能用byte的不用int,能用int的不用long,尽量不用包装类型。
- 注意dex文件和.so文件数量问题,加载过多的这类文件内存也会增加很多。
- 可以考虑利用好匿名共享内存,但是要注意管理好,匿名共享内存自身是管杀不管埋的。
- Parcelable序列化对象时不能太大,否则会爆掉导致crash。
- 尽量选用系统资源,如String,Color,Style等。
- Log信息输出的选择上,最后选用aop式,在正式代码里直接不把日志输出代码编译进去。
- 优化线程的分配,过多的线程分配和线程切换浪费资源(内存+CPU)。在App开发过程中,我们经常会引入很多第三方SDK,在这些SDK里各自都可能存在多线程或者多线程池的情况,我们尽可能选择可配置线程的SDK使用,最后统一用一个线程池进行管理线程。