文章目录
-
引言
-
1. 什么是内存泄露?
-
2. 内存泄漏造成什么影响?
-
3. 什么是LeakCanary?
-
4. LeakCanary工作机制(原理)
-
5. 项目中的实际运用场景及分析方法
-
6 .总结
引言
Andorid项目中我们会使用第三方开源库来检查内存泄露情况,使用时我们得了解其运行原理,并根据反馈日志分析定位问题,并以此解决内存泄露问题。文本从结合内存泄露原理和在项目中的实际的使用场景来解决开发的实际问题。
1. 什么是内存泄露?
一些对象有着有限的生命周期。当这些对象所要做的事情完成了,我们希望他们会被回收掉。但是如果有一系列对这个对象的引用,那么在我们期待这个对象生命周期结束的时候被收回的时候,它是不会被回收的。它还会占用内存,这就造成了内存泄露。持续累加,内存很快被耗尽。
比如,当 Activity.onDestroy 被调用之后,activity 以及它涉及到的 view 和相关的 bitmap 都应该被回收。但是,如果有一个后台线程持有这个 activity 的引用,那么 activity 对应的内存就不能被回收。这最终将会导致内存耗尽,然后因为 OOM 而 crash。
可以用一句话来概括:内存泄露是指不在需要的对象仍然补引用不能被GC回收释放。
内存相关名词回顾
GC(Garbage Collection ):垃圾回收器,会自动回收不在被引用的内存数据。
GC Roots:是一些由堆外指向堆内的引用。(是一组引用而不是对象)
Root:根,数据结构中树的顶级节点也叫root. roots当然是root的复数,就是多个root。
垃圾:内存中已经没有用的对象。Java虚拟中使用一种“可达性分析”的算法来决定对象是否被回收。
可达性分析:
可达性分析算法是从离散数学中的图论引入的,JVM 把内存中所有的对象之间的引用关系看作一张图,通过一组名为”GC Root"的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,最后通过判断对象的引用链是否可达来决定对象是否可以被回收。如下图所示:
比如上图中,对象A/B/C/D/E 与 GC Root 之间都存在一条直接或者间接的引用链,这也代表它们与 GC Root 之间是可达的,因此它们是不能被 GC 回收掉的。而对象M和K虽然被对J 引用到,但是并不存在一条引用链连接它们与 GC Root,所以当 GC 进行垃圾回收时,只要遍历到 J/K/M 这 3 个对象,就会将它们回收。
注意:上图中圆形图标虽然标记的是对象,但实际上代表的是此对象在内存中的引用。包括 GC Root 也是一组引用而并非对象。
GC Root 对象
在Java中,有以下几种对象可以作为 GC Root:
Java虚拟机栈(局部变量表)中引用的对象。
方法区中静态引用指向的对象。
仍处于存活状态中的线程对象。
Native方法中JNI引用的对象。
了解概念时通过最小粒度去分析,最后都会回到问题的本质上去,了解记忆联想再运用,我们才不会出现反复看反复忘的怪圈。
2. 内存泄漏造成什么影响?
它是造成应用程序OOM的主要原因之一。由于android系统为每个应用程序分配的内存有限,当一个应用中产生的内存泄漏比较多时,就难免会导致应用所需要的内存超过这个系统分配的内存限额,这就造成了内存溢出而导致应用Crash。
3. 什么是LeakCanary?
LeakCanary是Square开源框架,是一个Android和Java的内存泄露检测库,如果检测到某个 activity 有内存泄露,LeakCanary 就是自动地显示一个通知,所以可以把它理解为傻瓜式的内存泄露检测工具。通过它可以大幅度减少开发中遇到的oom问题,大大提高APP的质量。
开始使用
在 build.gradle 中加入引用,不同的编译使用不同的引用:
dependencies {
debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3'
releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.3'
}
在 Application 中:
public class ExampleApplication extends Application {
@Override public void onCreate() {
super.onCreate();
LeakCanary.install(this);
}
}
使用 RefWatcher 监控那些本该被回收的对象。
RefWatcher refWatcher = {...};
// 监控
refWatcher.watch(schrodingerCat);
LeakCanary.install() 会返回一个预定义的 RefWatcher,同时也会启用一个 ActivityRefWatcher,用于自动监控调用 Activity.onDestroy() 之后泄露的 activity。
public class ExampleApplication extends Application {
public static RefWatcher getRefWatcher(Context context) {
ExampleApplication application = (ExampleApplication) context.getApplicationContext();
return application.refWatcher;
}
private RefWatcher refWatcher;
@Override public void onCreate() {
super.onCreate();
refWatcher = LeakCanary.install(this);
}
}
使用 RefWatcher 监控 Fragment:
public abstract class BaseFragment extends Fragment {
@Override public void onDestroy() {
super.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(this);
}
}
4. LeakCanary工作机制(原理)
RefWatcher.watch() 创建一个 KeyedWeakReference到要被监控的对象。
然后在后台线程检查引用是否被清除,如果没有,调用GC。
如果引用还是未被清除,把 heap 内存 dump 到 APP对应的文件系统中的一个 .hprof 文件中。
得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
引用链传递到 APP 进程中的 DisplayLeakService, 并以通知的形式展示出来。
5. 项目中的实际运用场景及分析方法
当项目运用起来后:
项目中控制台会打印如下日志:
在项目中使用高德定位时,想要获取一次当前城市的定位场景时,出现了以下日志:
当捕获到内存泄露后,会以通知形式呈现,点击通过进入详情。如下图所示:
为了便于分析,也可以以分享文本的形式发送出去。
┬───
│ GC Root: Local variable in native code
│
├─ com.loc.d$b instance
│ Leaking: UNKNOWN
│ Thread name: 'amapLocManagerThread'
│ ↓ d$b.a
│ ~
├─ com.loc.d instance
│ Leaking: UNKNOWN
│ ↓ d.d
│ ~
├─ java.util.ArrayList instance
│ Leaking: UNKNOWN
│ ↓ ArrayList.elementData
│ ~~~~~~~~~~~
├─ java.lang.Object[] array
│ Leaking: UNKNOWN
│ ↓ Object[].[0]
│ ~~~
├─ com.tywj.navigation.city.ChooseCityActivity$mLocationListener$1 instance
│ Leaking: UNKNOWN
│ Anonymous class implementing com.amap.api.location.AMapLocationListener
│ ↓ ChooseCityActivity$mLocationListener$1.this$0
│ ~~~~~~
╰→ com.tywj.navigation.city.ChooseCityActivity instance
Leaking: YES (ObjectWatcher was watching this because com.tywj.navigation.city.ChooseCityActivity received Activity#onDestroy() callback and Activity#mDestroyed is true)
key = f4638335-5f48-4841-a15a-d236f48d0796
watchDurationMillis = 7463
retainedDurationMillis = 2457
METADATA
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: HUAWEI
LeakCanary version: 2.3
App process name: com.tywj.panda.customer
Analysis duration: 12460 ms
接下来分析日志:
GC Root: Local variable in native code
在泄漏路径的顶部是GC Root。GC Root是一些总是可达的特殊对象。
接着是:
com.loc.d$b instance
│ Leaking: UNKNOWN
│ Thread name: 'amapLocManagerThread'
│ ↓ d$b.a
│ ~
Leaking 的状态有:
NO 表示没有泄露
YES 表示有泄露
UNKNOWN 可能出现了泄露,需要进一步调查分析。
一般推断内存泄露是从最后一个没有泄漏的节点(Leaking: NO )到第一个泄漏的节点(Leaking: YES)之间的引用。
接着往下分析:找到Leaking:YES
Activity#onDestroy() callback and Activity#mDestroyed is true
说明当前Acitvity中出现了内存泄露。在分析引用链时,往上出现了一个Leaking:UNKOWN
com.tywj.navigation.city.ChooseCityActivity$mLocationListener$1 instance
│ Leaking: UNKNOWN
│ Anonymous class implementing com.amap.api.location.AMapLocationListener
│ ↓ ChooseCityActivity$mLocationListener$1.this$0
由此定位到mLocationListener这个对象上可能出现了内存泄露,找到问题,就成功就解决了一半。
于是进一步在代码中分析关于 当前·mLocationListener·相关引用情况。
在代码中的关键代码:
private var mLocationListener = AMapLocationListener { aMapLocation ->
aMapLocation?.let {
if (it.city.isNotEmpty()) {
currentCity = it.city
}
}
}
//得到当前位置
private fun startGetMyCurrentLocation() {
//获取当前位置
var mLocationClient: AMapLocationClient? = null
var mLocationOption: AMapLocationClientOption? = null
//初始化定位
mLocationClient = AMapLocationClient(this)
//(定位问题所在代码)
mLocationClient.setLocationListener(mLocationListener)
mLocationOption = AMapLocationClientOption()
mLocationOption.locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
mLocationOption.isOnceLocation = true
//关闭定位缓存
mLocationOption.isLocationCacheEnable = false
//给定位客户端对象设置定位参数
mLocationClient.setLocationOption(mLocationOption)
//启动定位
mLocationClient.startLocation()
}
思路1:(未解决)
在实现接口时首先想到了内部匿名类来实现.结果没有解决,思路不对。
mLocationClient?.setLocationListener(AMapLocationListener { aMapLocation ->
aMapLocation?.let {
if (it.city.isNotEmpty()) {
currentCity = it.city
}
}
})
分析:内部类的优势之一就是可以访问外部类,不幸的是,导致内存泄漏的原因,就是内部类持有外部类实例的强引用。
思路2:(已解决)
//获取当前位置
private var mLocationClient: AMapLocationClient? = null
private var mLocationOption: AMapLocationClientOption? = null
//初始化
override fun initVariable(savedInstanceState: Bundle?) {
super.initVariable(savedInstanceState)
//初始化定位
mLocationClient = AMapLocationClient(this)
mLocationOption = AMapLocationClientOption()
mLocationListener = AMapLocationListener { aMapLocation ->
aMapLocation?.let {
if (it.city.isNotEmpty()) {
currentCity = it.city
}
}
}
}
//得到当前位置
private fun startGetMyCurrentLocation() {
//设置定位回调监听
mLocationClient?.setLocationListener(AMapLocationListener { aMapLocation ->
aMapLocation?.let {
if (it.city.isNotEmpty()) {
currentCity = it.city
}
}
})
mLocationOption?.locationMode = AMapLocationClientOption.AMapLocationMode.Hight_Accuracy
mLocationOption?.isOnceLocation = true
//关闭定位缓存
mLocationOption?.isLocationCacheEnable = false
//给定位客户端对象设置定位参数
mLocationClient?.setLocationOption(mLocationOption)
//启动定位
mLocationClient?.startLocation()
}
//停止定位
override fun onStop() {
super.onStop()
mLocationClient?.stopLocation()
}
override fun onDestroy() {
super.onDestroy()
destoryLocation()
}
/**
*内存释放
*/
private fun destoryLocation() {
if (null != mLocationListener) {
mLocationListener = null
}
if (null != mLocationClient) {
mLocationClient?.onDestroy()
mLocationClient = null
mLocationOption = null
}
}
分析:对内存进行主动释放操作,同时注意相关的内存引用,也同时做好自查,对类似的对象进行类似处理。 再次运行时,不在提示内存泄露通知。
6 .总结
看过那么多会导致内存泄漏的例子,容易导致吃光手机的内存使垃圾回收处理更为频发,甚至最坏的情况会导致OOM。垃圾回收的操作是很昂贵的开销,会导致肉眼可见的卡顿。所以,实例化的时候注意持有的引用链,并经常进行内存泄漏检查。
内存泄露的本质是长周期对象持有了短周期对象的引用,导致短周期对象该被回收的时候无法被回收,从而导致内存泄露。我们只要顺着LeakCaneray 的给出的引用链一个个的往下找,找到发生内存泄露的地方,切断引用链就可以释放内存了。
内存泄露在工作和面试中是经常会遇到的问题,本文总结的只是很小的一部分体现,内存泄露的运用需要在工作中不断总结和完善。未完待续~
参考:
GC回收机制分代回收策略 - 姜新星