对于Android App内存泄漏问题开发者会比较困扰,并且类似问题难以复现,比较难定位,一但发生很可能是灾难性的,有千里之堤毁于蚁穴之势。App开发者对内存泄漏问题能够在测试阶段发现并且解决是非常重要的。
什么是内存泄漏?简单来讲是在开发应用时,没有释放掉不需要的内存资源。比如对象不需要了,但是由于没有释放对它的引用,
GC
(Garbage Collection) 无法回收相应的内存资源,这部分内存就无法被利用了。这种情况就是所谓的“内存泄漏”。
Android内存问题涉及的知识点还是非常多的,下面从开发者角度把内存泄漏问题分三个部分来介绍一下,目的是能够对于内存泄漏问题有一个清晰的了解和分析:
Android内存管理
内存分析工具使用
内存泄漏demo
Android
内存管理
Android每个App默认是运行在一个独立进程中, 这个进程运行在一个独立的VM(Virtual Machine)空间,可以参考下面JVM架构图。Android是怎么管理这些App的内存的呢?
JVM架构
Android 4.4之前一直使用的是
Dalvik
虚拟机作为App的运行的VM, Android 4.4中引入了
ART
(Android Runtime)作为备选VM,Android5.0起正式将ART作为默认VM。
ART相比Dalvik有以下优点:
1)
Ahead-of-time(AOT) compilation instead of Just-in-time (JIT)
Dalvik中采用的是JIT来做动态翻译的,将dex或odex中并排的Dalvik code运行态翻译成native code去执行。JIT的引入使得Dalvik提升了3~6倍的性能,而在ART中完全抛弃了Dalvik的JIT,使用了AOT,直接在安装时用dex2oat将其完全翻译成native code。这一技术的引入,使得VM执行指令的速度又一重大提升。
2)
Improvedgarbage collection
Dalvik GC
的过程
ART GC
的过程
1、当GC被触发时候,其会去查找所有活动的对象,这个时候整个程序与虚拟机内部的所有线程就会挂起,这样目的是在较少的堆栈里找到所引用的对象。需要注意的是这个回收动作是和应用程序同时执行(非并发)。
2、GC对符合条件的对象进行标记
3、GC对标记的对象进行回收
4、恢复所有线程的执行现场继续运行
1、GC将会锁住java堆,扫描并进行标记
2、标记完毕释放掉java堆的锁,并且挂起所有线程
3、GC对标记的对象进行回收
4、恢复所有线程的执行现场继续运行
5、重复2-4直到结束
ART主要的改善点在将其非并发过程改变成了部分并发。另外对内存重新分配管理,使得执行时间缩短,据官方测试数据,GC效率提高了l2倍。
3)
Improvedmemory usage and reduce fragmentation
Dalvik的内存管理特点是内存碎片化严重,当然这也是Mark and Sweep算法带来的弊端。该
算法分为两个阶段:标记(mark)和清除(sweep)。
在标记阶段,collector从mutator根对象开始进行遍历,对从根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
Mark and Sweep算法
ART的解决方案:它将java分了一块空间命名为Large-Object-Space,这块内存空间的引入用来专门存放large object。同时ART又引入了moving collector的技术,即将不连续的物理内存块进行对齐。对齐了后内存碎片化就得到了很好的解决。Large-Object-Space的引入能够有效提高内存的利用率,根官方统计,ART的内存利用率提高了10倍左右。
Dalvik和ART都是使用
paging
和
memory-mapping(mmapping)
来管理内存的。这就意味着, 任何被分配的内存都会持续存在, 唯一释放这块内存方式就是释放对象引用
(
让对象GC Root不可达
)
, 故而让GC程序来回收内存。关于如何管理应用的进程与内存分配,Android
官网
有比较详细说明。
内存分析工具使用
工欲善其事,必先利其器。
接下来会介绍三个内存分析工具:Android Studio自带的Memory Monitor、MAT工具和插件工具Leakcanary
工具1:MemoryMonitor
Memory Monitor
是 Android Studio内置的, 官方的内存监测工具,它是图形化的展示当前应用的内存状态, 包括已分配内存, 空闲内存, 内存实时动态等。
Memory Monitor
图中标注1:GC按钮, 点击执行一次GC操作。
图中标注2:Dump Java Heap按钮, 点击会在该调试工程的captures目录下生成一个类似这样"com.talkingdata.demo_2017.08.09_13.35.hprof"命名的hprof文件。针对文件的分析可以参考Google官网描述:HPROF Analyzer
。
图中标注3:Allocation Traking按钮, 点击一次开始, 再次点击结束,同样会在captrures目录生成一个文件, 类似"com.talkingdata.demo_2017.08.09_13.35.alloc"命名的alloc文件,针对文件的分析可以参考Google官网描述:
Allocation Tracker
。
工具2:MAT
Eclipse MAT
(
MemoryAnalyzer
)是一个快速且功能丰富的Java Heap分析工具, 可以帮助我们寻找内存泄露, 减少内存消耗.
MAT可以分析程序(成千上万的对象产生过程中)生成的Heap dumps文件, 它会快速计算出对象的Retained Size, 来展示是哪些对象没有被GC, 自动生成内存泄露疑点的报告。详细的使用方法可以查阅
官方文档
。下面简单介绍常用的方式:
获取heap dumps
可以使用 Android Studio 获取 heap dump。点击 Monitors 中的 Dump Java Heap 按钮后,会得到一个 .hprof 文件。生成的 .hprof 文件默认在项目的根目录的 captures 目录下。
heap dumps
因为MAT是用来分析Java程序的hprof文件的,和Android导出的hprof文件的格式有一定区别,所以需要转换为标准格式。
有两种方式可供选择:Android SDK中给我们提供了转换的工具,即platform-tools/hprof-conv ,使用如下命令即可转换我们的hprof文件格式:
hprof-conv[
源hprof文件的路径] [输出的hprof文件路径]
或者选择Captures选项卡,右键相应的.hprof 文件,并选择 Export to standard .hprof。
Exportto standard .hprof
使用MAT分析
打开MAT工具,并加载之前导出的hprof文件,会进入Overview界面。可以从界面中看到Retained Size最大的几个对象。
MATOverview
Histogram
它列出了按类别分组的对象,MAT可以非常快速地计算各类别的大小和个数,并显示在列表中,这是深入分析的重要指标。它有多种分组方式:
Group by class/Group by superclass/Group by classloader/Group by package。甚至还可以按线程分组,不过这需要打开thread_overview。
Histogram
Dominator Tree
DominatorTree列出了最大的对象。下一级别会显示那些被立即阻止垃圾回收的对象。右键单击可以查看传出和传入的引用或查看Path to GC Roots,以查看保留对象的引用链。
DominatorTree
Path to GC Roots
GC Roots的路径显示了阻止对象被垃圾回收的引用链。有黄点的对象是GC Roots,即被假定为活着的对象。通常GC Roots是当前在线程或系统类的调用堆栈上的对象。用这个方法可以快速找到对象没有被回收的原因。
Path to GC Roots
工具3:Leakcanary
LeakCanary
是
square
出的一款开源的用来做内存泄露检测的工具。
被测试App集成
LeakCanary
之后, 工具检测到潜在的内存泄露后, 会弹出Toast提示,并在测试手机桌面生成一个Leaks的icon:
点击该icon进入Leaks界面, 可以比较清晰的看到内存泄露疑点
对于源码感兴趣的同学可以参考下面:
源码文件结构说明
AbstractAnalysisResultService.java
ActivityRefWatcher.java -- Activity监控者,监控其生命周期
AndroidDebuggerControl.java --Android Debug控制开关,就是判断Debug.isDebuggerConnected()
AndroidExcludedRefs.java -- 内存泄漏基类
AndroidHeapDumper.java --生成.hrpof的类
AndroidWatchExecutor.java -- Android监控线程,延迟5s执行
DisplayLeakService.java -- 显示通知栏的内存泄漏,实现了AbstractAnalysisResultService.java
LeakCanary.java --对外类,提供install(
this
)方法
ServiceHeapDumpListener.java
internal --这个文件夹用于显示内存泄漏的情况(界面相关)
DisplayLeakActivity.java --内存泄漏展示的Activity
DisplayLeakAdapter.java --内存泄漏展示ListView适配器
DisplayLeakConnectorView.java --内存泄漏展示连接器
FutureResult.java
HeapAnalyzerService.java 在另一个进程启动的Service,用于接收数据并发送数据到界面
LeakCanaryInternals.java
LeakCanaryUi.java
MoreDetailsView.java
RefWatcher
创建
watch()
方法使用
1)
RefWatcher.watch() 创建一个 KeyedWeakReference 到要被监控的对象。
2)
然后在后台线程检查引用是否被清除,如果没有,调用GC。
3)
如果引用还是未被清除,把 heap 内存 dump 到 APP 对应的文件系统中的一个 .hprof 文件中。
4)
在另外一个进程中的 HeapAnalyzerService 有一个 HeapAnalyzer 使用HAHA 解析这个文件。
5)
得益于唯一的 reference key, HeapAnalyzer 找到 KeyedWeakReference,定位内存泄露。
6)
HeapAnalyzer 计算 到 GC roots 的最短强引用路径,并确定是否是泄露。如果是的话,建立导致泄露的引用链。
7)
引用链传递到 APP 进程中的DisplayLeakService, 并以通知的形式展示出来。
检测
Activity
1)在Application onCreate()中调用 LeakCanary.install(this)
Github示例:
public
class
ExampleApplication
extends
Application {
@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)
LeakCanary.install()
会返回一个
RefWatcher
public
static
RefWatcher install(Application application) {
return
refWatcher(application).listenerServiceClass(DisplayLeakService.
class
)
.excludedRefs(AndroidExcludedRefs.createAppDefaults().build())
.buildAndInstall();
3)
buildAndInstall()
同时也会启用一个
ActivityRefWatcher
,用于自动监控调用
Activity.onDestroy()
之后泄露的
activity
。
public
RefWatcher buildAndInstall() {
RefWatcher refWatcher = build();
if
(refWatcher != DISABLED) {
LeakCanary.enableDisplayLeakActivity(context);
ActivityRefWatcher.install((Application) context, refWatcher);
return
refWatcher;
public
void
watchActivities() {
// Make sure you don't get installed twice.
stopWatchingActivities();
//注册 LifecycleCallbacks,用于观察activity是否被回收
application.registerActivityLifecycleCallbacks(lifecycleCallbacks);
void
onActivityDestroyed(Activity activity) {
refWatcher.watch(activity);
注:registerActivityLifecycleCallbacks 时API 14引入的监控方式,如要兼容 API 14 以下版本,请重写ActivityonDestroy()在其中调用refWatcher.watch(activity)
检测
Fragment
public
abstract
class
BaseFragment
extends
Fragment {
@Override
public
void
onDestroy() {
super
.onDestroy();
RefWatcher refWatcher = ExampleApplication.getRefWatcher(getActivity());
refWatcher.watch(
this
);
检测其他对象
RefWatcher refWatcher = ExampleApplication.getRefWatcher(
this
);
refWatcher.watch(Object);
查看Log
xxx:leakcanary D/LeakCanary: In com.talkingdata.demo:1.0:1.
xxx:leakcanary D/LeakCanary: * com.talkingdata.demo.app.AppBaseFunction has leaked:
xxx:leakcanary D/LeakCanary: * GC ROOT android.location.LocationManager$ListenerTransport.mListener
xxx:leakcanary D/LeakCanary: * references com.talkingdata.demo.BaseActivity$LocationTracker.mContext
xxx:leakcanary D/LeakCanary: * leaks com.talkingdata.demo.app.AppBaseFunction instance
xxx:leakcanary D/LeakCanary: * Retaining: 1.2 kB.
xxx:leakcanary D/LeakCanary: * Reference Key: 11489a95-635c-44f5-831d-38fa21bb0595
xxx:leakcanary D/LeakCanary: * Device: Huawei google Nexus 6P angler
xxx:leakcanary D/LeakCanary: * Android Version: 8.0.0 API: 26 LeakCanary: 1.6-SNAPSHOT
xxx:leakcanary D/LeakCanary: * Durations: watch=5016ms, gc=168ms, heap dump=1248ms, analysis=89686ms
xxx:leakcanary D/LeakCanary: * Details:
内存泄漏Demo
假设有一个单例的ListenerManager,可以add/remove Listener,有一个Activity,实现了该Listener,且这个Activity中持有大对象BigObject,BigObject中包含一个大的字符串数组和一个Bitmap List。
代码片段如下:
ListenerManager
public
class
ListenerManager {
private
static
ListenerManager sInstance;
private
ListenerManager() {}
private
List<SampleListener> listeners =
new
ArrayList<>();
public
static
ListenerManager getInstance() {
if
(sInstance ==
null
) {
sInstance =
new
ListenerManager();
return
sInstance;
public
void
addListener(SampleListener listener) {
listeners.add(listener);
public
void
removeListener(SampleListener listener) {
listeners.remove(listener);
MemoryLeakActivity
public
class
MemoryLeakActivity
extends
AppCompatActivity
implements
SampleListener {
private
BigObject mBigObject =
new
BigObject();
@Override
protected
void
onCreate(Bundle savedInstanceState) {
super
.onCreate(savedInstanceState);
setContentView(R.layout.activity_memory_leak);
ListenerManager.getInstance().addListener(
this
);
@Override
public
void
doSomething() {
1)使用Memory Monitor 分析
启动我们要检测的Activity(MemoryLeakActivity), 然后退出, 在monitor中查看内存变化。操作步骤和结果如下:
步骤1
:点击"Analyzer Tasks"视图中的启动按钮, 启动分析。
步骤2:
查看"Analysis Result"中的分析结果,点击"Leaked Activityes"中的具体实例, 该实例的引用关系将会展示在"Reference Tree"视图中。
步骤3:
根据"Reference Tree"视图中的引用关系,查找
leak
的activity
, 也就是谁
Dominate
这个activity对象。
可以看到是ListenerManager的静态单例sInstance最终支配了MemoryLeakActivity.sIntance连接到GC Roots, 故而导致MemoryLeakActivityGC Roots可达, 导致activity无法被回收。
Heap Viewer查看内存消耗
上述步骤,可以让我们快速定位可能的内存泄露。除了内存泄露, 还有内存消耗过大。我们可以在Heap Viewer中查看分析内存的消耗点, 如下:
2)
使用MAT工具分
析
相对与Android Studio的Memory Monitor, HPROF工具来说, MAT的使用显得更加生涩、难以理解些,但是MAT功能很全面。Android Studio导出的hprof文件需要转换下才可以在MAT中使用,转换命令如下:
$ hprof-conv com.anly.samples_2016.10.31_15.07.hprof mat.hprof
Histogram定位内存消耗
MAT中很多视图的第一行, 都可以输入正则, 来匹配我们关注的对象实例。
Dominate Tree查看支配关系
使用OQL查询相关对象
对于Android App开发来说,大部分的内存问题都跟四大组件, 尤其是Activity相关, 故而我们会想查出所有Activity实例的内存占用情况, 可以使用OQL来查询:
GC路径定位问题
上面几个视图都可以让我们很快速的找到内存的消耗点,接下来我们要分析的就是为何这些个大对象没有被回收。
对象没有被回收是因为他有到GC Roots的可达路径
。
那么我们就来分析下这条路径(Path toGC Roots), 看看是谁在这条路中"搭桥"。
如下, 进入该对象的"path2gc"视图:
会发现与HPROF Analyzer异曲同工,找出了是ListenerManager的静态实例导致了MemoryLeakActivity无法回收。
3) 使用Leakcanary工具分析
步骤1:加入LeakCanary
app的build.gradle中加入:
dependencies {
debugCompile
'com.squareup.leakcanary:leakcanary-android:1.5'
releaseCompile
'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
testCompile
'com.squareup.leakcanary:leakcanary-android-no-op:1.5'
Application中加入:
public
class
SampleApplication
extends
Application {
@Override
public
void
onCreate() {
super
.onCreate();
LeakCanary.install(
this
);
步骤2:操作要检测的界面, 查看结果
当发生可疑内存泄露时, 会在桌面生成一个"Leaks"的图标,点击进去可以看到内存泄露的疑点报告:
可以看到内存泄漏的分析结果和之前两个工具结果一致。
内存问题的分析, 无外乎分析对象的内存占用(Retained Size), 找出Retained Size大的对象, 找到其直接支配(Immediate Dominator), 跟踪其GC可达路径(Path to GC Roots), 从而找到是谁让这个大对象活着。
对于上面三种工具可以混合使用,一般情况下Android Studio自带的工具结合LeakCanary就能分析内存问题,MAT有更专业的一些功能,比如Heap比较等等值得探索。
为了更好的分享开发经验
每周四我们都会准备一篇技术干货与大家分享!
专治各种疑难杂症
希望你会喜欢!
返回搜狐,查看更多
责任编辑:
声明:该文观点仅代表作者本人,搜狐号系信息发布平台,搜狐仅提供信息存储空间服务。