Android内存管理(13)常见产生内存泄漏的原因
1.集合类泄漏
集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量 (比如类中的静态属性,全局性的 map 等即有静态引用或 final 一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。我们都喜欢通过 HashMap 做一些缓存之类的事,这种情况就要多留一些心眼。
2.static 成员
如果static成员比它外部的类生命周期长,如果在static成员时引用了外部类,那么外部类不会以被释放。
2.1 static成员易造成内存泄漏
如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。
这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,以为如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。
这里修复的方法是:
不要在类初始时初始化静态成员。可以考虑lazy初始化。 架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。
2.2.单例造成的内存泄漏
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。比如下面一个典型的例子,
1 public class AppManager {
2 private static AppManager instance;
3 private Context context;
4 private AppManager(Context context) {
5 this.context = context;
6 }
7 public static AppManager getInstance(Context context) {
8 if (instance == null) {
9 instance = new AppManager(context);
10 }
11 return instance;
12 }
13 }
这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要:
- 如果此时传入的是 Application 的 Context,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。
- 如果此时传入的是 Activity 的 Context,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。
正确的方式应该改为下面这种方式:
1 public class AppManager {
2 private static AppManager instance;
3 private Context context;
4 private AppManager(Context context) {
5 this.context = context.getApplicationContext();// 使用Application 的context
6 }
7 public static AppManager getInstance(Context context) {
8 if (instance == null) {
9 instance = new AppManager(context);
10 }
11 return instance;
12 }
13 }
或者这样写,连 Context 都不用传进来了:
在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context,
1 ...
2
3 context = getApplicationContext();
4
5 ...
6 /**
7 * 获取全局的context
8 * @return 返回全局context对象
9 */
10 public static Context getContext(){
11 return context;
12 }
13
14 public class AppManager {
15 private static AppManager instance;
16 private Context context;
17 private AppManager() {
18 this.context = MyApplication.getContext();// 使用Application 的context
19 }
20 public static AppManager getInstance() {
21 if (instance == null) {
22 instance = new AppManager();
23 }
24 return instance;
25 }
26 }
27
2.3.非静态内部类创建静态实例造成的内存泄漏
有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:
1 public class MainActivity extends AppCompatActivity {
2 private static TestResource mResource = null;
3 @Override
4 protected void onCreate(Bundle savedInstanceState) {
5 super.onCreate(savedInstanceState);
6 setContentView(R.layout.activity_main);
7 if(mManager == null){
8 mManager = new TestResource();
9 }
10 //...
11 }
12 class TestResource {
13 //...
14 }
15 }
这样就在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏,因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法为:
将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用。
3.匿名内部类易产生内存泄漏
看下面2个情况:
3.1 Runable/Thread造成的内存泄漏
android开发经常会继承实现Activity/Fragment/View,此时如果你使用了匿名类,并被异步线程持有了,那要小心了,如果没有任何措施这样一定会导致泄露
1 public class MainActivity extends Activity {
2 ...
3 Runnable ref1 = new MyRunable();
4 Runnable ref2 = new Runnable() {
5 @Override
6 public void run() {
7
8 }
9 };
10 ...
11 }
ref1和ref2的区别是,ref2使用了匿名内部类。我们来看看运行时这两个引用的内存:
可以看到,ref1没什么特别的。
但ref2这个匿名类的实现对象里面多了一个引用:
this$0这个引用指向MainActivity.this,也就是说当前的MainActivity实例会被ref2持有,如果将这个引用再传入一个异步线程,此线程和此Acitivity生命周期不一致的时候,就造成了Activity的泄露。
3.2.Handler 造成的内存泄漏
Handler 的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。
由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。举个例子:
1 public class SampleActivity extends Activity {
2
3 private final Handler mLeakyHandler = new Handler() {
4 @Override
5 public void handleMessage(Message msg) {
6 // ...
7 }
8 }
9
10 @Override
11 protected void onCreate(Bundle savedInstanceState) {
12 super.onCreate(savedInstanceState);
13
14 // Post a message and delay its execution for 10 minutes.
15 mLeakyHandler.postDelayed(new Runnable() {
16 @Override
17 public void run() { /* ... */ }
18 }, 1000 * 60 * 10);
19
20 // Go back to the previous Activity.
21 finish();
22 }
23 }
在该 SampleActivity 中声明了一个延迟10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。
修复方法:在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:
1 public class SampleActivity extends Activity {
2
3 /**
4 * Instances of static inner classes do not hold an implicit
5 * reference to their outer class.
6 */
7 private static class MyHandler extends Handler {
8 private final WeakReference<SampleActivity> mActivity;
9
10 public MyHandler(SampleActivity activity) {
11 mActivity = new WeakReference<SampleActivity>(activity);
12 }
13
14 @Override
15 public void handleMessage(Message msg) {
16 SampleActivity activity = mActivity.get();
17 if (activity != null) {
18 // ...
19 }
20 }
21 }
22
23 private final MyHandler mHandler = new MyHandler(this);
24
25 /**
26 * Instances of anonymous classes do not hold an implicit
27 * reference to their outer class when they are "static".
28 */
29 private static final Runnable sRunnable = new Runnable() {
30 @Override
31 public void run() { /* ... */ }
32 };
33
34 @Override
35 protected void onCreate(Bundle savedInstanceState) {
36 super.onCreate(savedInstanceState);
37
38 // Post a message and delay its execution for 10 minutes.
39 mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
40
41 // Go back to the previous Activity.
42 finish();
43 }
44 }
综述,即推荐使用静态内部类 + WeakReference 这种方式。每次使用前注意判空。
4.避免 override finalize()
- finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是: 虚拟机调用GC的时间不确定 Finalize daemon线程被调度到的时间不确定
- finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:
含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。 - 含有Finalize方法的object需要至少经过两轮GC才有可能被释放。
5.资源未关闭造成的内存泄漏
对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。
6.一些不良代码造成的内存压力
有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
比如: Bitmap 没调用 recycle()方法,对于 Bitmap 对象在不使用时,我们应该先调用 recycle() 释放内存,然后才它设置为 null. 因为加载 Bitmap 对象的内存空间,一部分是 java 的,一部分 C 的(因为 Bitmap 分配的底层是通过 JNI 调用的 )。 而这个 recyle() 就是针对 C 部分的内存释放。 构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用 ViewHolder。
7.总结
- 对 Activity 等组件的引用应该控制在 Activity 的生命周期之内; 如果不能就不考虑使用 getApplicationContext 或者 getApplication,以避免 Activity 被外部长生命周期的对象引用而泄露。
- 尽量不要在静态变量或者静态内部类中使用非静态外部成员变量(包括context ),即使要使用,也要考虑适时把外部成员变量置空;也可以在内部类中使用弱引用来引用外部类的变量。
- 对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
将内部类改为静态内部类
静态内部类中使用弱引用来引用外部类的成员变量
- Handler 的持有的引用对象最好使用弱引用,资源释放时也可以清空 Handler 里面的消息。比如在 Activity onStop 或者 onDestroy 的时候,取消掉该 Handler 对象的 Message和 Runnable.
- 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap 后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ; array = null)等,最好遵循谁创建谁释放的原则。
- 正确关闭资源,对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销。
- 保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?