0%

Android内存泄漏分析

1. 前言

今年,公司加大了对app性能优化的投入,把性能优化作为技术部门的KPI之一。在做需求的同时,我们也会分析并解决app中存在的内存泄漏。

2. 内存泄漏

内存泄漏主要是指程序运行过程中,动态分配内存之后,而没有及时将不再使用的内存释放,造成系统内存浪费,严重会影响程序性能,甚至会导致程序崩溃。这也是C/C++语言饱受诟病的特性之一。虽然Java语言引用了GC机制,使得内存泄漏的可能性大大降低,但是在某些情况下,还是有可能导致垃圾内存无法及时释放。下面分析几种Android开发过程中有可能导致的内存泄漏。

2.1 静态变量引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class MainUtils {
private static MainUtils ourInstance ;
private Context mContext;
public static MainUtils getInstance(Context context) {
if (ourInstance == null) {
ourInstance = new MainUtils(context);
}
return ourInstance;
}
private MainUtils(Context context) {
mContext = context;
}
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MainUtils utils = MainUtils.getInstance(this);
}
}

在横竖屏切换的时候,会先导致MainActivity销毁重建,然而MainUtils有一个静态变量引用了MainActivity,因此会导致内存泄漏,具体原因后面分析。

2.2 多线程

多线程也是Android中导致内存泄漏的原因之一。常见的因为多线程导致内存泄漏有如下几种

2.2.1 Handler

在Activity、Fragment或者View用Handler.PostDelayed(Runnable, 2000)执行定时操作(如每隔一分钟刷新二维码),在Activity、Fragment或者View销毁时,没有把Handler中的Message和Callback销毁导致内存泄漏。

2.2.2 AsyncTask

在Activity或者Fragment甚至是View、Adapter(正常来说View和Adapter不应该有AsyncTask)中使用了AsyncTask,在类被销毁时没有及时把AsyncTask销毁掉,导致内存泄漏。

2.2.3 TimerTask

使用了TimerTask作为定时任务,如定期查看某个文件夹或文件是否存在,但是传入了MainActivity这个实例(如2.1)导致泄漏。

2.2.4 网络操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_start).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(new Runnable() {
@Override
public void run() {
try {
start();
} catch (IOException | InterruptedException e) {
}
}
}).start();
}
});
}
//https://api.github.com/users/octocat/repos
private void start() throws IOException, InterruptedException {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();
GitHubService service = retrofit.create(GitHubService.class);
Call<List<Repo>> call = service.listRepos("octocat");
List<Repo> repos = call.execute().body();
System.out.println(repos.toString());
// 模拟网络耗时
Thread.sleep(2000);
}
}

上述例子中,执行网络操作过程中,如果用户退出Acitvity,便会导致内存泄漏。

2.3 动画

最常见的场景Activity、Fragment持有了View,在View动画还没有执行完之前退出了Fragment或者Activity,此时便会产生内存泄漏(如轮播图)。

3. 分析工具

3.1 Android Monitors

使用Android Monitors来查看内存抖动,并结合代码分析来解决内存泄漏问题是一个比较好的方案。关于hprof文件中的信息是从哪里的(JVMTI)和hprof文件的格式可以参考以下两篇文章

  • HPROF: A Heap/CPU Profiling Tool
  • HPROF 输出文件的说明
    Android Studio中hprof文件各个字段的含义,可以参考这里:HPROF Viewer and Analyzer。
    需要注意的是Android Mionitor导出的hprof文件不是标准的hprof文件,需要切换到captures,然后右键—>Export to standard hprof才能导出标准hptof文件,才能被MAT工具识别。如下图

下面我们结合Android Monitors分析例子2.1中导致的内存泄漏。假设我们怀疑某个操作会导致内存泄漏(拿旋转屏幕作为例子)
刚开始运行时,内存图如下,占用内存为2.49M:

旋转2次之后,内存发现内存发生了微小抖动,内存图如下:

之后再点几次GC

我们发现使用内存为2.55M,比操作前(2.49M)增加了0.06M,结合hprof文件就可以知道MainActivity在内存中有两个实例,但是按照正常逻辑,应该只有一个MainActivity实例,者说明发生了内存泄漏。再继续查看两个MainActivity的引领链,发现0号MainActivity实例被MainUtils一个静态变量实例引用了,这就找到了问题的根源。

3.2 LeakCanary

LeakCanary是square公司推出的一个Android和Java内存泄漏检测工具。其官方介绍为**A memory leak detection library for Android and Java.**。使用非常简单

  1. 在build.gradle文件中添加
    1
    2
    debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1'
    releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1'
  2. 在Application中添加
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
    // This process is dedicated to LeakCanary for heap analysis.
    // You should not init your app in this process.
    return;
    }
    LeakCanary.install(this);
    // Normal app init code...
    }
    重新运行2.1中代码,旋转几次屏幕后,leakCanary便能检测出MainActivity发生了泄漏,如下图

并且把泄漏类及路径都标记出来了!其内部主要是用类似于MAT的内存检测工具haha,其主要原理如下:

  1. RefWatcher.watch() creates a KeyedWeakReference to the watched object.
  2. Later, in a background thread, it checks if the reference has been cleared and if not it triggers a GC.
  3. If the reference is still not cleared, it then dumps the heap into a .hprof file stored on the file system.
  4. HeapAnalyzerService is started in a separate process and HeapAnalyzer parses the heap dump using > HAHA.
  5. eapAnalyzer finds the KeyedWeakReference in the heap dump thanks to a unique reference key and > locates the leaking reference.
  6. HeapAnalyzer computes the shortest strong reference path to the GC Roots to determine if there is a leak, and then builds the chain of references causing the leak.
  7. The result is passed back to DisplayLeakService in the app process, and the leak notification is shown.

看了下源码,核心原理是在每个Activity.onDestroy的时候封装成带key弱引用KeyedWeakReference,然后如下处理:

  1. key对应弱引用是否存在,若是,转2。若否,不存在内存泄漏
  2. 手动触发Gc,再次判断key对应弱引用是否存在,若是,转3。若否,不存在内存泄漏
  3. 分析hprof文件,再次判断key对应弱引用是否存在,若是,转4。若否,不存在内存泄漏
  4. 生成分析报告
    就不上代码分析了,想了解更多请参考:

3.3 MAT

MAT工具使用相对简单,具体用法可以参考文章:使用MAT分析应用的内存信息

4. 几点建议

  • 对于使用Handler的类,在销毁前中调用Handler.removeCallbacksAndMessages(null);
  • 对于使用动画的View,在销毁前调用View..clearAnimation();
  • 使用多线程的类,销毁时需要把线程给stop.
  • 对于Cursor、Bitmap、Broadcast、系统监听(如传感器监听、网络状态监听等)IO流等资源,使用完后关闭或反注册。

5. 参考

Android Studio - HPROF文件查看和分析工具
Android 内存使用分析和程序性能分析
HPROF 输出文件的说明
JVMTI 和 Agent 实现
基于 JVMTI 实现 Java 线程的监控