前言:
你是否有被out of memory搞得焦头烂额过?你是否有过在内存优化问题上被面试官问的哑口无言的经历?那就驻足一小会儿,看看下面的内容。
概念篇
1、内存分配机制
在Android系统中,每一个应用程序都运行在单独的进程中,这个进程是从Zygote进程fork出来的,每个应用进程都对应自己唯一的虚拟机实例,而每个虚拟机都有堆内存阀值限制,即是进程退出了,数据仍然保存在内存中。至于虚拟机中的内存分配机制可以参考JVM运行时的内存分配机制。
2、内存回收机制
Android中的内存回收机制是托管的,当进程空间不足时,其内存由虚拟机垃圾回收机制自动回收。回收的参考依据主要是以下两个方面:
- 进程优先级
当系统需要回收进程时,总是先考虑回收优先级较低的进程。
前台进程 > 可见进程 > 服务进程 > 后台进程 > 空进程
前台进程 | 指正在与用户进行交互的应用进程,该进程数量较少,是最高优先级进程,系统一般不会终止该进程。 |
可见进程 | 能被用户看到,但不能根据根据用户的动作做出相应的反馈。 |
服务进程 | 没有可见界面仍在不断的执行任务的进程,除非在可视进程和前台进程紧缺资源(如:内存资源)才会被终止。 |
后台进程 | 通常系统中有大量的后台进程,终止后台进程不会影响用户体验,随时为优先级更高的进程腾出资源而被终止,优先回收长时间没用使用过的进程。 |
空进程 | 为提高整体系统性能,系统会保存已经完成生命周期的应用程序 ,存在与内存当中,也就是缓存,为下次的启动更加迅速而设计。通常会被定期地终止。 |
- 回收收益
Android总是倾向于kill一个能回收更多内存的进程。
3、Java的四种引用方式
- 强引用(StrongReference)
1.只要某个对象有强引用与之关联,JVM必定不会回收这个对象。
2.即使内存不足,JVM宁愿抛出OutOfMemory错误也不会回收这种对象。
- 软引用(SoftReference)
1.用来描述一些有用但并不是必须的对象。
2.对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
- 弱引用(WeakReference)
1.弱引用是用来描述非必须的对象。
2.当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。
- 虚引用(PhantomReference)
1.不影响对象的生命周期。
2.如果一个对象与虚引用关联,则跟没有引用与之关联一样。
3.在任何时候都可能被垃圾回收器回收。
4、内存泄漏、内存抖动、内存溢出的概念
内存泄漏:指由于疏忽或者错误造成程序未能释放已经不再使用的内存。
内存抖动:指内存频繁的分配和回收,内存大小不断浮动的现象。
内存溢出:指应用程序申请了超过阈值的内存空间。
内存泄漏和内存抖动最终都有可能导致内存溢出,我们直观看到的就是应用程序OOM。
工具篇
1、LeakCanary
LeakCanary是Square公司开源的一个检测内存泄漏的函数库,当我们在项目中加入LeakCanary后,它会在Debug版本中监控Activity、Fragment等的内存泄漏,在检测到内存泄漏时,会发送消息到系统通知栏,同时Logcat上面也会有对应的日志输出。
那么我们要如何使用呢?目前LeakCanary版本为2.4,较之前1.X的使用有所简化。你只需要在应用的build.gradle 中加入下面依赖:
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
}
是的,现在就已经可以使用了,因为LeakCanary2.X 自动初始化,无需手动在添加初始化代码,现在自动支持 fragment,支持 androidx,不再需要依赖于支持库,也不需要依赖于AndroidX。
当检测到内存泄漏时,会发送消息到系统通知栏,如下图:
2、Android Profiler
Android Profiler 是Android Studio 3.x中替代Android Monitor的工具,它提供了实时数据帮助你理解你的APP是怎样使用CPU、Memory、Network、Battery Resources。Memory Profiler是Android Profiler中的一个组件,可以帮助你识别可能会导致应用卡顿、冻结甚至崩溃的内存泄漏和内存抖动。它显示一个应用内存使用量的实时图表,让您可以捕获堆转储、强制执行垃圾回收以及跟踪内存分配。
要想使用它,首先依次点击菜单栏View -> Tool Windows -> Profiler。
然后根据提示,点击左侧“+”选择加载对应app的进程。
这时显示CPU、Memory、Network等的实时图表,我们点击Memory来进入Memory Profiler界面。
上图界面显示的即是实时内存使用图标,不同的颜色则代表着不同的类型所占用的内存。随着鼠标的移动,我们也可以查看之前某一时刻的内存使用情况。
![]() | Java | 从 Java 或 Kotlin 代码分配的对象的内存。 |
| Native | 从 C 或 C++ 代码分配的对象的内存。 即使您的应用中不使用 C++,您也可能会看到此处使用的一些原生内存,因为 Android 框架使用原生内存代表您处理各种任务,如处理图像资源和其他图形时,即使您编写的代码采用 Java 或 Kotlin 语言。 |
![]() | Graphics | 图形缓冲区队列向屏幕显示像素(包括 GL 表面、GL 纹理等等)所使用的内存。(请注意,这是与 CPU 共享的内存,不是 GPU 专用内存。) |
![]() | Stack | 您的应用中的原生堆栈和 Java 堆栈使用的内存。这通常与您的应用运行多少线程有关。 |
![]() | Code | 您的应用用于处理代码和资源(如 dex 字节码、经过优化或编译的 dex 代码、.so 库和字体)的内存。 |
![]() | Others | 您的应用使用的系统不确定如何分类的内存。 |
Allocated | 您的应用分配的 Java/Kotlin 对象数。此数字没有计入 C 或 C++ 中分配的对象。 |
如果需要查看某一时段,则需要先点击左上角Record以记录起始点,然后点击Stop来结束 。
优化篇
1、防止内存泄漏
android中的内存泄露通常是Activity或者Fragment的泄露。不注重内存泄漏问题最终会导致OOM,因此防止内存泄漏时内存优化的必要措施。那么我们该如何防止内存泄漏呢,一句话概括就是在不需要的时候及时释放掉资源。下面总结一些常见的内存泄漏及防止措施。
- 非静态内部类、匿名内部类
泄漏原因:非静态内部类、匿名内部类都会持有外部类的引用,如果非静态内部类或者匿名内部类的生命周期比外部类(比如Activity、Fragment)的生命周期长,就会导致当外部类被回收时无法回收,引起内存泄漏。
解决办法:将非静态内部类、匿名内部类 改成静态内部类,或者直接抽离成一个外部类。如果在静态内部类中,需要引用外部类对象,那么可以将这个引用封装在一个WeakReference中。
- 单例模式持有Context
泄漏原因:当某些单例模式需要持有一个Context来进行初始化时,传入了Activity,则会导致在Activity需要被销毁的时候无法被销毁,因为该单例还持有着它的引用。从来引起了内存泄漏。
解决办法:通过getApplicationContext()方法获取全局的Context来对单例进行初始化。由于Appliacation的生命周期和应用生命周期相同,因此不会引起内存泄漏。
AudioHelper.init(getApplicationContext());
- Handler
泄漏原因:如果在Activity中定义Handler对象,那么Handler肯定是持有Activty的引用,而每个Message对象是持有Handler的引用的(Message对象的target属性持有Handler引用),从而导致Message间接引用到了Activity。如果在Activty destroy之后,消息队列中还有Message对象,Activty是不会被回收的。
解决办法:在onDestory()方法中,移除消息;或者将Handler放在静态内部类中。
@Override
public void onDestroy() {
super.onDestroy();
mhandler.removeMessages(0);
mhandler.removeMessages(1);
}
- 资源对象没关闭
泄漏原因:当我们打开资源时,比如读写文件资源、打开数据库资源、使用Bitmap资源等等一般都会在内存中有缓存,当我们在不需要时没有及时关闭资源,就可能会导致缓存没法及时清除,从而引起内存泄漏。虽然有些对象,如果我们不去关闭,它自己在finalize()函数中会自行关闭。但是这得等到GC回收时才关闭,这样还是会导致缓存驻留一段时间
。
解决办法:及时的关闭资源。
if (bitmap != null){
bitmap.recycle();
}
- WebView
泄漏原因:在android 5.1及以上版本的代码中,WebView可能会存在内存泄露,我们一般在activity中使用webview时会在onDestroy方法中调用mWebView.destroy();来释放webview。根据源码可以知道如果在onDetachedFromWindow之前调用了destroy那就肯定会无法正常反注册了,也就会导致内存泄漏。
解决办法:在销毁webview前一定要onDetachedFromWindow,我们先将webview从它的父view中移除再调用destroy方法。
具体参考这篇文章:Android 5.1 WebView内存泄漏问题及解决。
2、避免内存抖动
内存抖动主要是在短时间内占用大量内存,又短时间内瞬间释放掉,造成内存占用忽高忽低的现象。因此我们应该从以下几点来避免内存抖动。
- 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
- onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
- 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
- 对能够复用的对象,同理可以使用对象池将它们缓存起来。
3、图片优化
众所周知,图片在加载到内存中时会占用大量内存,因此图片优化往往是内存优化的重中之重。首先我们来了解下现如今常见的三级缓存策略:服务器缓存、SD卡缓存、内存缓存。也就是说图片一般是存储在服务器图片库中,当我们需要某一张图片时会首先检测内存中是否拥有、有则直接使用,没有则需要再从本地SD卡中查找,最后才是从服务器中下载,并保存在SD卡中和缓存到内存中。
那么图片的内存缓存策略又是什么样的呢,你可能会想到建立一个缓存池,存放Bitmap对象,当需要是直接从中查找,有则直接使用,没有则从SD卡或者网络服务器中获取。思路是对的,但是也存在问题,当GC时如何确保回收的是那些不常用的Bitmap对象而保留下常用的呢。为了解决这一问题,内存缓存的二级缓存策略诞生了。即建立两个缓存池,一个强引用、一个软引用,使用强引用来保存常用的图片,使用软引用来保存其他图片。
实现二级缓存的方式分为:LinkedHashMap+软引用 和 LruCache+软引用 (LruCache 包含最少使用清除机制)。但实际上LruCache内部实现也是通过LinkedHashMap来实现的。
//这是LruCache的构造方法 可以看到内部实现就是LinkedHashMap
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
对于一些大图加载我们则需要进行压缩处理、一般方式是根据自身显示的View的大小按比例进行压缩,具体压缩方法这里不在赘述了。
4、数据结构优化
数据结构优化需要我们了解各个数据结构的区别,所在的内存大小等,根据自身需求来选择合适的数据结构。
android为了减少内存的使用和装箱拆箱损耗的性能,提供一些特有的数据接口,在 android.util包下面,都是使用数据进行保存,适当的使用这些对象可以优化我们的应用。
5、代码优化
OnTrimMemory是Android在4.0之后加入的一个回调,系统会通知应用程序进程当前状态,以此判断进程是否将被杀死,来及时释放内存,实现OnTrimMemory()方法,当应用内存不足时,我们来手动释放内存。
那么哪些组件能实现onTrimMemory方法呢。
- Application.onTrimMemory()
- Activity.onTrimMemory()
- Fragement.OnTrimMemory()
- Service.onTrimMemory()
- ContentProvider.OnTrimMemory()
尽管系统在内存不足的时候杀进程的顺序是按照LRU Cache中从低到高来的,但是它同时也会考虑杀掉那些占用内存较高的应用来让系统更快地获得更多的内存。因此手动“求饶”,增加存活几率是有必要的。
总结:
为了有头有尾~写个总结。