【CSDN 编者按】对于众多 Android 程序员而言,在需求与应用性能之间,主要精力都会放到新需求的开发。随着项目复杂度的增加,应用性能越来越低,出现各种问题。程序员们奔波于各种“救火现场”,疲于奔命。本文作者 Aritra Roy 分享了自己在 Android 应用程序开发过程中所遇以及思考,针对内存泄漏提炼出一套可以应用于开发中的方法论。也许会让你的开发效率事半功倍。

作者 | Aritra Roy,Android 开发者

译者 | 罗昭成,责编 | 唐小引

封图 | CSDN 付费下载自东方 IC

出品 | CSDN(ID:CSDNnews)

以下为译文:

我们都知道,写一个 Android 的应用很容易,但是要写一个高性能的应用可就不容易了。以我的个人经验来说,在 App 的开发过程中,主要的精力都会放在新功能、新模块、新组件的开发上。

开发过程中,看得见的 UI 比看不见的性能更能吸引我们的目光。所以我强制自己将“优化应用程序(如内存泄漏)”的优先级提高,并养成习惯。

长期以来,不关注性能,带来了很多的技术债。经过一年多的努力调整, 比起一年前,我有很多的心得体会。

对于很多开发者来说,内存泄漏都是一个老大难的问题。关于处理内存泄漏,你有可能会觉得太难,又或是太费时,又或者是觉得完全没有意义。但我要告诉你的是,事实并非如此。当你开始处理这些问题的时候,你会发现,这感觉超级棒。

在本篇文章中,我会以尽可能简单的方式讲解这些问题,即使你是一个初学者,也可以学习到如何构建一个高质量、高性能的应用。

垃圾回收

Java 是一个非常强大的语言。在 Android 中,我们几乎不会像 C / C++ 那样,手动分配和释放内存。因为 Java 会自动清理内存。

让我们来思考一个问题,如果 Java 内建的垃圾回收系统可以在我们不需要的时候自动回收内存,那我们为什么还需要关心内存呢?是因为垃圾回收机制不够完善吗?

当然不是,Java 垃圾回收机制当然是完善的。垃圾回收机制是可以正常工作,但是,如果我们的应用程序出现 Bug, 导致垃圾回收器不能正常检查出不需要的内存块,就会导致问题。

总体来说,是我们自己的代码错误导致垃圾回收不可用。不可否认,垃圾回收机制是 Java 最伟大的设计之一。

关于垃圾回收器

在处理内存问题之前,你需要了解垃圾回收器的工作原理。它的概念非常简单,但在它背后,有着极其复杂的逻辑。但是你也别担心,我们也只关心一些简单的概念。

如图所示,Android 或者 Java 应用程序都有一个起点,从对象的初始化,并且调用方法。我们可以认为,这个点就是图中的 "GC Roots"。有一些对象引用被 GC Roots 直接持有,剩下的则由它们自己创建并持有。

如此,整个引用链构成了内存树。垃圾回收器从 GC roots 开始,直接或间接的链接着其它的对象。当整个遍历结束,还有一些不能被访问到的对象,就是变成了垃圾,这些对象就会被垃圾回收器回收。

内存泄漏

到现在,你已经知道了垃圾回收的概念,也知道了垃圾回收在 Android 中是如何管理内存的。下面,我们将深入研究一下内存泄漏。

简单来说,内存泄漏是指你的对象已经使用结束,但是它却不能被释放掉。每个对象在完成它自己的生命周期过后,都需要从内存中清理出来。但是如果一个对象被另一个对象直接或间接的持有,垃圾回收器不能检查出它已经使用结束。朋友们,这样子就导致了内存泄漏。

值得庆幸的是,我们并不需要太担心所有的内存泄漏,因为并不是所有的内存泄漏都是有害的。有一些内存泄漏是无关痛痒(只泄漏几 KB 的内存),并且,在 Android Framwork 层也会有一些内存泄漏,但是你并不需要去修复,因为它们对 App 的性能影响微乎其微,你可以忽略。

但是有一些会引起 App 崩溃, 或者严重卡顿。这些都是需要你时刻注意的。

为什么要解决内存泄漏?

没有人会想使用一个又慢又占内存的应用。如果使用一段时间就会崩溃,你的用户也会“崩溃”掉,如果长时间出现这样子的问题,你的用户会毫不犹豫的卸载掉你的应用,并且再也不会使用它。

如果你的应用中存在内存泄漏,垃圾回收器不能回收不使用的内存,随着用户使用时间的增长,内存的占用会越来越多。如此下去,当系统不能在给它分配更多内存的时候,就会导致 OutOfMemoryError, 然后应用程序会崩溃掉。

垃圾回收有利有弊,垃圾回收是一庞大的系统,在应用中,尽可能少的让垃圾回收器运行,这样对应用体验会更好。

随着你的应用使用的堆内存逐渐增加,Short GC 就会触发,来保证立即清理无用对象。现在这些快速清理内存的 GC 运行在不同的线程中,这些 GC 不会导致你的应用变慢。

但是如果你的应用中存在严重的内存泄漏,Short GC 没有办法回收内存,并且占用内存持续增加,这将会导致 Larger GC 被触发。它会将整个应用程序挂起,阻塞大概 50~100ms,这会导致应用程序变慢并且有可能不能使用。

修复内存泄漏,减少对 App 的影响,给用户提供更好的体验。

如何发现内存泄漏?

现在,你已经认识到,你需要修复隐藏在你 App 中的内存泄漏。但是,我们如何才能找到它们呢?

Android Studio 为我们提供了一个非常强大的工具:Monitors。

通过它,你能看到网络、CPU、GPU、内存的使用情况。

在调试运行 App 的时候,要密切关注内存监视器。内存泄漏的第一个现象就是,在使用的过程中,内存一直增加,不能减少,即使你把 APP 退到后台也不能释放。内存分配监视器能够清楚的看到不同对象所占用的内存,可以清楚的知道哪个对象占用内存较多,需要处理。

但是,它本身还不够,它需要你指定时间,然后转存出对应的内存堆。这是一个很无趣的工作。

幸运的是,我们现在已经有更好的方式来实现。LeakCanary, 一个和 App 一起运行的库,它会在内存泄漏的时候,转存出内存信息,然后给我们发送一个通知并给我们一个有用的栈信息。

常见的内存泄漏

从我的经验来看,有很多相似且经常出现内存泄漏的问题,你在你每天的开发中,都有可能会遇到它们。一但你清楚了它们发生的时间、地点、原因 ,你就可以很轻松的修复它们。

  • 未取消的 Listener

很多时候,你在 Activity/Fragment 中注册了一个 Listener, 但是忘记取消注册了。如果你的运气不好,它很可能会引起一个严重的内存泄漏问题。一般来说,这些 Listener 的 注册与取消注册是同步出现的,在你使用的时候需要注册,在不使用的时候需要取消注册。

举个例子,当我们的应用程序需要获取定位的时候,需要使用 LocationManager,你会从系统服务中拿到它,并且给其设置一个地理位置更新的回调:

private void registerLocationUpdats() {mManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);mManager.requestLocationUpdates(LocationManager.GPS_PROVIDER,TimeUnit.MINUTES.toMillis(1), 100, this);
}

在代码中,可以看出来,使用了 Activity 自己来实现了地理位置更新的回调。LocationManager 会持有这个回调的引用。当你退出了这个页面,Android 系统会调用 onDestory() ,但是垃圾回收器并不能清理掉它,因为 LocationManager 持有它的强引用。

当然,解决方案也很简单,就是在 onDestory() 方法中,取消注册就可以了。

@Override
public void onDestroy() {super.onDestroy();if (mManager != null) {mManager.removeUpdates(this);}
}

  • 内部类

内部类在 Java 和 Android 开发中经常用到,非常简单,但是如果使用不当,也会造成严重的内存泄漏。让我们先来看一个简单的例子:

public class BadActivity extends Activity {private TextView mMessageView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.layout_bad_activity);mMessageView = (TextView) findViewById(R.id.messageView);new LongRunningTask().execute();}private class LongRunningTask extends AsyncTask<Void, Void, String> {@Overrideprotected String doInBackground(Void... params) {// 做一些耗时操作return "Am finally done!";}@Overrideprotected void onPostExecute(String result) {mMessageView.setText(result);}}
}

这是一个很简单的 Activity 页面,在页面启动的时候,在后台启动了一个耗时的任务(比如说,复杂的数据库查询或者是很慢的网络)。等到任务执行结束,把拿到的结果显示到页面上。看起来,这样做并没有问题。事实上,非静态的内部类会隐式的持有外部类的引用(在这里,就是 Activity)。如果在耗时任务执行完之前,你旋转屏幕或者退出这个页面,垃圾回收器就不能从内存中清理掉 Activity 的实例。这个简单的问题会导致很严重的内存泄漏问题。

当然,解决方案也非常地简单,如下:

public class GoodActivity extends Activity {private AsyncTask mLongRunningTask;private TextView mMessageView;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.layout_good_activity);mMessageView = (TextView) findViewById(R.id.messageView);mLongRunningTask = new LongRunningTask(mMessageView).execute();}@Overrideprotected void onDestroy() {super.onDestroy();mLongRunningTask.cancel(true);}private static class LongRunningTask extends AsyncTask<Void, Void, String> {private final WeakReference<TextView> messageViewReference;public LongRunningTask(TextView messageView) {this.messageViewReference = new WeakReference<>(messageView);}@Overrideprotected String doInBackground(Void... params) {String message = null;if (!isCancelled()) {message = "I am finally done!";}return message;}@Overrideprotected void onPostExecute(String result) {TextView view = messageViewReference.get();if (view != null) {view.setText(result);}}}
}

正如你看到的代码,首先我将非静态内部类改成了静态内部类,这样它就不会持有外部类的引用了。当然,使用静态的内部类,非静态的变量就不能访问了。所以我们需要将 TextView 通过构造方法把它传过去。

在这里,我强烈推荐使用 WeakReference ,它能更好的避免引起内存泄漏。你应该去学习 Java 中关于不同引用类型的知识:

http://javarevisited.blogspot.in/2014/03/difference-between-weakreference-vs-softreference-phantom-strong-reference-java.html

  • 匿名内部类

匿名内部类也是在开发过程中经常使用到的一个东西,它的定义和使用都非常的简洁。但以我的经验来看,匿名内部类造成了大量的内存泄漏的问题。

匿名内部类与非静态内部类相似,造成内部类的原因也和上面说的一样。你有可能在好多地方都使用了匿名内部类,如果使用不当,会严重影响 App 的性能。

public class MoviesActivity extends Activity {private TextView mNoOfMoviesThisWeek;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.layout_movies_activity);mNoOfMoviesThisWeek = (TextView) findViewById(R.id.no_of_movies_text_view);MoviesRepository repository = ((MoviesApp) getApplication()).getRepository();repository.getMoviesThisWeek().enqueue(new Callback<List<Movie>>() {@Overridepublic void onResponse(Call<List<Movie>> call,Response<List<Movie>> response) {int numberOfMovies = response.body().size();mNoOfMoviesThisWeek.setText("No of movies this week: " + String.valueOf(numberOfMovies));}@Overridepublic void onFailure(Call<List<Movie>> call, Throwable t) {// Oops.}});}
}

上面的例子中,我使用一个常用的网络库 Retrofit 发送了一个网络请求,然后在 TextView 中显示返回的结果。很明显,那个 Callback 对象持有 Activity 的引用。如果现在网络很慢,在网络响应回来之前,页面旋转或者关闭,就会导致 Activity 泄漏。

我强烈建议,在需要的时候,尽量使用静态的内部类,而非匿名内部类。当然,我的意思不是不在使用匿名内部类,如果你需要使用匿名内部类,你需要注意引起内存泄漏的问题,保证不会出现问题。

  • Bitmaps

在应用中,你看到的所有图片都是 Bitmap 对象,包含了所有的像素数据。现在这些 Bitmap 数据非常的大,一个处理不好,就会引起 OOM, 造成 APP 崩溃。在 APP 中使用的图片资源生成的 Bitmap 会由系统进行管理,但是如果你需要自己处理 Bitmap ,要记住,使用完过后要调用 bitmap.recycle() 来释放资源。

在处理 Bitmap 时,需要将一张大的图缩放变小过后,在使用,多重用同一个图片数据。Google 官方有一个关于处理 Bitmap 内存的文档:

https://developer.android.com/training/displaying-bitmaps/manage-memory.html

  • Contexts

另一个是关于 Context 的滥用引起的内存泄漏。Activity / Application / Service 都是继承自 Context 并实现它们自己的功能,但是你也需要搞清楚它们之间的区别,什么是 activity 级别的 Context,什么是 application 级别的 Context,根据项目需求的场景去选择使用哪一个 Context 。错误地使用 Activity Context,导致引用不能被释放,就会引起内存泄漏。

结语

现在,你知道了什么是垃圾回收器,什么是内存泄漏,内存泄漏给你带来的影响。你也知道如何检测和修复内存泄漏。

从现在开始,构建高质量/高性能的应用。处理内存泄漏不仅能让你的应用有更好的用户体验,也能让你成为更好的开发者。

原文:Everything You Need To Know About Memory Leaks In Android Apps

链接:https://blog.aritraroy.in/everything-you-need-to-know-about-memory-leaks-in-android-apps-655f191ca859

【END】

大众学python的时代,作为程序员你怎么看?

https://edu.csdn.net/topic/python115?utm_source=csdn_bw

 热 文 推 荐 

点击阅读原文,即刻获取《程序员大本营》精粹期刊。

你点的每个“在看”,我都认真当成了喜欢

火速收藏!Android 开发者必会的内存泄漏指南相关推荐

  1. Android 开发者必知的开发资源

    英文原文:Bongzimo  翻译: ImportNew-黄小非 译文链接:http://www.importnew.com/3988.html Android 开发者必知的开发资源 随着Androi ...

  2. 高效Android开发者必须知道的4个工具

    移动app开发是一个漫长而费力的过程.然而,现在的企业总是希望能够尽快发布app.幸运的是,我们有很多帮助移动开发人员加快工作步伐的工具. 可用的工具集随着移动平台新版本的出现而不断更新.是的,我们很 ...

  3. Android 如何做一次内存泄漏大排查

    转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/112335970 本文出自[赵彦军的博客] 文章目录 前言 把内存泄漏的地方找出来 ...

  4. 【Android -- 学习笔记】Handler 内存泄漏分析及解决

    一.介绍 首先,请浏览下面这段 handler 代码: public class SampleActivity extends Activity {private final Handler mLea ...

  5. Android开发-Handler引起的内存泄漏-实验、分析、总结。

    介绍 最近在恶补Handler的知识,其中就涉及到了Handler引起的内存泄露问题,网络上有很多的分析文章.我就按照这些文章的思路,写代码验证,主要是验证和记录.  使用的内存检测工具是:LeakC ...

  6. Android MVP(三)内存泄漏分析与动态代理

    博主声明: 转载请在开头附加本文链接及作者信息,并标记为转载.本文由博主 威威喵 原创,请多支持与指教. 本文首发于此   博主:威威喵  |  博客主页:https://blog.csdn.net/ ...

  7. [贝聊科技]使用Android Studio和MAT进行内存泄漏分析

    1.Java内存分配策略 Java 程序运行时的内存分配策略有三种:静态分配.栈式分配和堆式分配.对应的存储区域如下: 静态存储区(方法区):主要存放静态数据.全局 static 数据和常量.这块内存 ...

  8. Android 性能优化 - 彻底解决内存泄漏

    起源 有趣的灵魂千奇百怪,内存泄漏的也是各式各样 我在15年写过一遍 文章 < android中常见的内存泄漏和解决办法>http://blog.csdn.net/wanghao20090 ...

  9. 解决android中EditText导致的内存泄漏问题

    开发中用到了LeankCanary,在一个简单的页面中(例如 :仅仅 包含Edittext),也会导致内训泄漏,为此,我在网上找了大量资料,最终解决. 例如一个布局: <LinearLayout ...

最新文章

  1. mysql 5.7 full_MySQL5.7默认打开ONLY_FULL_GROUP_BY 解决方案
  2. ★教师工资为什么这么低?/整理
  3. 设计模式实战-策略模式(Strategy Pattern)
  4. Docker认识、安装及使用(windows7)
  5. 近5年133个Java面试题 你会几个?
  6. java keystore ca_PKCS12 Java Keystore from CA and User certificate in java
  7. Ranger知识地图
  8. nginx基础概念(100%)之request
  9. 联邦快递“误运”华为包裹遭调查 联邦快递:全力配合
  10. (转)WriteOnce and RunAnyWhere
  11. oracle pl/sql 无监听程序
  12. GitHub新神器,宇宙第一编辑器--VS Code!危
  13. linux 修改默认语言
  14. 迈高图手机版_迈高图地图数据下载器
  15. (转)前端文摘:深入解析浏览器的幕后工作原理
  16. 缓解眼睛疲劳的一些解决方案
  17. 使用netstat命令统计established状态的连接数
  18. 三菱FX系列PLC备份电池电压低报警的处理方法
  19. IDM短信发送接口设计说明
  20. Speedoffice(word)如何生成目录

热门文章

  1. 【小结】背知识点的小技巧
  2. 华为vrrp默认优先级_华为vrrp配置
  3. 利用ros3djs接收pointcloud2在web端显示
  4. notepad++ python指定anaconda环境以及代码补全设置
  5. python查看dll中所有函数_Python中的函数
  6. 如何让地面不起灰_地面不平能铺地板吗?木地板不平怎么修复
  7. 目标检测算法 2020_One-stage目标检测算法综述
  8. 径向涡轮膨胀机行业调研报告 - 市场现状分析与发展前景预测
  9. php insert方法,lInsert 命令/方法/函数
  10. linux tar权限不够,Linux用户,群组,权限 ,tar命令