android 内存优化

什么是内存泄漏?

如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这中情况就是内存泄漏。

在Android开发中,一些不好的编程习惯会导致我们的开发的app存在内存泄漏的情况。下面介绍一些在Android开发中常见的内存泄漏场景及优化方案。

单例导致内存泄漏

单例模式在Android开发中会经常用到,但是如果使用不当就会导致内存泄漏。因为单例的静态特性使得它的生命周期同应用的生命周期一样长,如果一个对象已经没有用处了,但是单例还持有它的引用,那么在整个应用程序的生命周期它都不能正常被回收,从而导致内存泄漏。

public class AppSettings {private static AppSettings sInstance;private Context mContext;private AppSettings(Context context) {this.mContext = context;}public static AppSettings getInstance(Context context) {if (sInstance == null) {sInstance = new AppSettings(context);}return sInstance;}
}

像上面代码中这样的单例,如果我们在调用getInstance(Context context)方法的时候传入的context参数是Activity、Service等上下文,就会导致内存泄漏。

以Activity为例,当我们启动一个Activity,并调用getInstance(Context context)方法去获取AppSettings的单例,传入Activity.this作为context,这样AppSettings类的单例sInstance就持有了Activity的引用,当我们退出Activity时,该Activity就没有用了,但是因为sIntance作为静态单例(在应用程序的整个生命周期中存在)会继续持有这个Activity的引用,导致这个Activity对象无法被回收释放,这就造成了内存泄漏。

为了避免这样单例导致内存泄漏,我们可以将context参数改为全局的上下文:

private AppSettings(Context context) {this.mContext = context.getApplicationContext();
}

全局的上下文Application Context就是应用程序的上下文,和单例的生命周期一样长,这样就避免了内存泄漏。

单例模式对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用Activity的上下文,而是使用Application的上下文。

静态变量导致内存泄漏

静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。

比如下面这样的情况,在Activity中为了避免重复的创建info,将sInfo作为静态变量:

public class MainActivity extends AppCompatActivity {private static Info sInfo;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);if (sInfo != null) {sInfo = new Info(this);}}
}class Info {public Info(Activity activity) {}
}

Info作为Activity的静态成员,并且持有Activity的引用,但是sInfo作为静态变量,生命周期肯定比Activity长。所以当Activity退出后,sInfo仍然引用了Activity,Activity不能被回收,这就导致了内存泄漏。

在Android开发中,静态持有很多时候都有可能因为其使用的生命周期不一致而导致内存泄漏,所以我们在新建静态持有的变量的时候需要多考虑一下各个成员之间的引用关系,并且尽量少地使用静态持有的变量,以避免发生内存泄漏。当然,我们也可以在适当的时候讲静态量重置为null,使其不再持有引用,这样也可以避免内存泄漏。

非静态内部类导致内存泄漏

非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄漏。

非静态内部类导致的内存泄漏在Android开发中有一种典型的场景就是使用Handler,很多开发者在使用Handler是这样写的:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);start();}private void start() {Message msg = Message.obtain();msg.what = 1;mHandler.sendMessage(msg);}private Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message msg) {if (msg.what == 1) {// 做相应逻辑}}};
}

也许有人会说,mHandler并未作为静态变量持有Activity引用,生命周期可能不会比Activity长,应该不一定会导致内存泄漏呢,显然不是这样的!

熟悉Handler消息机制的都知道,mHandler会作为成员变量保存在发送的消息msg中,即msg持有mHandler的引用,而mHandler是Activity的非静态内部类实例,即mHandler持有Activity的引用,那么我们就可以理解为msg间接持有Activity的引用。msg被发送后先放到消息队列MessageQueue中,然后等待Looper的轮询处理(MessageQueue和Looper都是与线程相关联的,MessageQueue是Looper引用的成员变量,而Looper是保存在ThreadLocal中的)。那么当Activity退出后,msg可能仍然存在于消息对列MessageQueue中未处理或者正在处理,那么这样就会导致Activity无法被回收,以致发生Activity的内存泄漏。

通常在Android开发中如果要使用内部类,但又要规避内存泄漏,一般都会采用静态内部类+弱引用的方式。

public class MainActivity extends AppCompatActivity {private Handler mHandler;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);mHandler = new MyHandler(this);start();}private void start() {Message msg = Message.obtain();msg.what = 1;mHandler.sendMessage(msg);}private static class MyHandler extends Handler {private WeakReference<MainActivity> activityWeakReference;public MyHandler(MainActivity activity) {activityWeakReference = new WeakReference<>(activity);}@Overridepublic void handleMessage(Message msg) {MainActivity activity = activityWeakReference.get();if (activity != null) {if (msg.what == 1) {// 做相应逻辑}}}}
}

mHandler通过弱引用的方式持有Activity,当GC执行垃圾回收时,遇到Activity就会回收并释放所占据的内存单元。这样就不会发生内存泄漏了。

上面的做法确实避免了Activity导致的内存泄漏,发送的msg不再已经没有持有Activity的引用了,但是msg还是有可能存在消息队列MessageQueue中,所以更好的是在Activity销毁时就将mHandler的回调和发送的消息给移除掉。

@Override
protected void onDestroy() {super.onDestroy();mHandler.removeCallbacksAndMessages(null);
}

非静态内部类造成内存泄漏还有一种情况就是使用Thread或者AsyncTask。

比如在Activity中直接new一个子线程Thread:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new Thread(new Runnable() {@Overridepublic void run() {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();}
}

或者直接新建AsyncTask异步任务:

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);new AsyncTask<Void, Void, Void>() {@Overrideprotected Void doInBackground(Void... params) {// 模拟相应耗时逻辑try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}return null;}}.execute();}
}

很多初学者都会像上面这样新建线程和异步任务,殊不知这样的写法非常地不友好,这种方式新建的子线程Thread和AsyncTask都是匿名内部类对象,默认就隐式的持有外部Activity的引用,导致Activity内存泄漏。要避免内存泄漏的话还是需要像上面Handler一样使用静态内部类+弱应用的方式(代码就不列了,参考上面Hanlder的正确写法)。

未取消注册或回调导致内存泄漏

比如我们在Activity中注册广播,如果在Activity销毁后不取消注册,那么这个刚播会一直存在系统中,同上面所说的非静态内部类一样持有Activity引用,导致内存泄漏。因此注册广播后在Activity销毁后一定要取消注册。

public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);this.registerReceiver(mReceiver, new IntentFilter());}private BroadcastReceiver mReceiver = new BroadcastReceiver() {@Overridepublic void onReceive(Context context, Intent intent) {// 接收到广播需要做的逻辑}};@Overrideprotected void onDestroy() {super.onDestroy();this.unregisterReceiver(mReceiver);}
}

在注册观察者模式的时候,如果不及时取消也会造成内存泄漏。比如使用Retrofit+RxJava注册网络请求的观察者回调,同样作为匿名内部类持有外部引用,所以需要记得在不用或者销毁的时候取消注册。

Timer和TimerTask导致内存泄漏

Timer和TimerTask在Android中通常会被用来做一些计时或循环任务,比如实现无限轮播的ViewPager:

public class MainActivity extends AppCompatActivity {private ViewPager mViewPager;private PagerAdapter mAdapter;private Timer mTimer;private TimerTask mTimerTask;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);init();mTimer.schedule(mTimerTask, 3000, 3000);}private void init() {mViewPager = (ViewPager) findViewById(R.id.view_pager);mAdapter = new ViewPagerAdapter();mViewPager.setAdapter(mAdapter);mTimer = new Timer();mTimerTask = new TimerTask() {@Overridepublic void run() {MainActivity.this.runOnUiThread(new Runnable() {@Overridepublic void run() {loopViewpager();}});}};}private void loopViewpager() {if (mAdapter.getCount() > 0) {int curPos = mViewPager.getCurrentItem();curPos = (++curPos) % mAdapter.getCount();mViewPager.setCurrentItem(curPos);}}private void stopLoopViewPager() {if (mTimer != null) {mTimer.cancel();mTimer.purge();mTimer = null;}if (mTimerTask != null) {mTimerTask.cancel();mTimerTask = null;}}@Overrideprotected void onDestroy() {super.onDestroy();stopLoopViewPager();}
}

当我们Activity销毁的时,有可能Timer还在继续等待执行TimerTask,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即cancel掉Timer和TimerTask,以避免发生内存泄漏。

集合中的对象未清理造成内存泄漏

这个比较好理解,如果一个对象放入到ArrayList、HashMap等集合中,这个集合就会持有该对象的引用。当我们不再需要这个对象时,也并没有将它从集合中移除,这样只要集合还在使用(而此对象已经无用了),这个对象就造成了内存泄漏。并且如果集合被静态引用的话,集合里面那些没有用的对象更会造成内存泄漏了。所以在使用集合时要及时将不用的对象从集合remove,或者clear集合,以避免内存泄漏。

资源未关闭或释放导致内存泄漏

在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄漏。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄漏。

属性动画造成内存泄漏

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏。

@Override
protected void onDestroy() {super.onDestroy();mAnimator.cancel();
}

WebView造成内存泄漏

关于WebView的内存泄漏,因为WebView在加载网页后会长期占用内存而不能被释放,因此我们在Activity销毁后要调用它的destory()方法来销毁它以释放内存。

另外在查阅WebView内存泄漏相关资料时看到这种情况:

Webview下面的Callback持有Activity引用,造成Webview内存无法释放,即使是调用了Webview.destory()等方法都无法解决问题(Android5.1之后)。

最终的解决方案是:在销毁WebView之前需要先将WebView从父容器中移除,然后在销毁WebView。详细分析过程请参考这篇文章:WebView内存泄漏解决方法。

@Override
protected void onDestroy() {super.onDestroy();// 先从父控件中移除WebViewmWebViewContainer.removeView(mWebView);mWebView.stopLoading();mWebView.getSettings().setJavaScriptEnabled(false);mWebView.clearHistory();mWebView.removeAllViews();mWebView.destroy();
}

总结

内存泄漏在Android内存优化是一个比较重要的一个方面,很多时候程序中发生了内存泄漏我们不一定就能注意到,所有在编码的过程要养成良好的习惯。总结下来只要做到以下这几点就能避免大多数情况的内存泄漏:

构造单例的时候尽量别用Activity的引用;

静态引用时注意应用对象的置空或者少用静态引用;

使用静态内部类+软引用代替非静态内部类;

及时取消广播或者观察者注册;

耗时任务、属性动画在Activity销毁时记得cancel;

文件流、Cursor等资源及时关闭;

Activity销毁时WebView的移除和销毁。

那么对应Android的内存优化的建议就很直观了,基本围绕了一个点:

  • Android中Activity等占据较大的内存空间对象,在不用的时候,一定保证当前对象的直接引用和间接引用全部被置为空。内存才能被释放。否则,就会有内存泄漏。

Java内存优化最根本的准则,就是努力使你的程序适配Java的GC机制。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/792065.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

单元测试 mockito(二)

1.返回指定值 2.void返回值指定插桩 3.插桩的两种方式 when(obj.someMethod()).thenXxx():其中obj可以是mock对象 doXxx().wien(obj).someMethod():其中obj可以是mock/spy对象 spy对象在没有插桩时是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的&#x…

好物视频素材在哪找?视频素材大全app下载

创作优质视频内容不仅仅是一种艺术&#xff0c;也是一种科学&#xff0c;需要对素材的深刻理解和精心挑选。掌握了这些高清无水印视频素材&#xff0c;您就拥有了创作引人入胜视频内容的强大工具。以下是更多精选的视频素材网站&#xff0c;旨在为您的视频项目提供更广阔的视野…

Python | Leetcode Python题解之第10题正则表达式匹配

题目&#xff1a; 题解&#xff1a; class Solution:def isMatch(self, s: str, p: str) -> bool:m, n len(s), len(p)dp [False] * (n1)# 初始化dp[0] Truefor j in range(1, n1):if p[j-1] *:dp[j] dp[j-2]# 状态更新for i in range(1, m1):dp2 [False] * (n1) …

专升本--python运算符总结

运算优先级 同一个等级是没有先后顺序的&#xff0c;此外&#xff0c;赋值语言的先后问题&#xff1a; 赋值的顺序从上往下&#xff0c;同一行一般都是代表同时进行赋值&#xff0c;如图所示&#xff1a; 一.and A and B&#xff0c;若A,B有任意一个为假&#xff08;0&#x…

希尔排序和快排里的小区间优化

希尔排序 希尔排序是插入排序的优化。 当一串数是逆序时&#xff0c;那么每插入一个数&#xff0c;前面的数都会向后面挪动。 那么这是插入排序的时间复杂度&#xff0c;就会达到O(n^2) 希尔排序是对数组里的数进行预排序。 防止插入排序出现最坏的情况。 预排序&#xf…

代码随想录-图论

797.所有可能的路径&#xff1a; . - 力扣&#xff08;LeetCode&#xff09; class Solution {List<List<Integer>> ansnew LinkedList<>();List<Integer> listnew LinkedList<>();public List<List<Integer>> allPathsSourceTarg…

ABC318 F - Octopus

解题思路 对于每个宝藏维护个区间&#xff0c;答案一定在这些区间中对于每个区间的端点由小到大排序对于每个点进行判断&#xff0c;若当前位置合法&#xff0c;则该点一定为一个右端点则该点到前一个端点之间均为合法点若前一个点不合法&#xff0c;则一定是某一个区间限制的…

Vue3:使用Pinia存储、读取、修改数据

一、存储数据 Pinia插件中&#xff0c;存储数据的配置项是state count.ts import {defineStore} from piniaexport const useCountStore defineStore(count,{// 真正存储数据的地方state(){return {sum:6}} })loveTalk.ts import {defineStore} from piniaexport const use…

Xen Server 8 Install

Xen Sevrer 前言 XenServer&#xff08;以前称为 Citrix Hypervisor&#xff09;是业界领先的平台&#xff0c;实现了经济高效的桌面、服务器和云虚拟化基础结构。XenServer 支持任意规模或类型的组织整合计算资源&#xff0c;以及将计算资源转换为虚拟工作负载&#xff0c;从…

RESTful API说明

RESTful API&#xff08;Representational State Transfer&#xff09;是一种用于设计网络应用程序的架构风格。它基于 HTTP 协议&#xff0c;通过使用统一的资源标识符&#xff08;URL&#xff09;来访问和操作资源。 RESTful API 的设计原则包括&#xff1a; 资源标识符&am…

SpringBoot2升级到SpringBoot3总结

最近公司在做监控日志平台的迁移&#xff0c;从NewRelic迁移到Dynatrace&#xff0c;为了配合迁移&#xff0c;有一个前提就是把SpringBoot2升级到SpringBoot3。 我们这边的项目大多数都是KotlinSpringBoot2.X的技术栈&#xff0c;现在要全部升级到最新的SpringBoot3.2.2或者S…

c++协程详解(二)

前言 这是c协程实现第二篇&#xff0c;这里开始我们将开始真正意义上开始实现协程。对协程基础流程不清楚的&#xff0c;可以看我的第一篇。 后续可能需要一定的模板知识&#xff0c;可以看下我的模板的文章&#xff0c;那些知识就完全够用了。本篇将实现一个协程封装的异步任…

Redis慢日志

SLOWLOG 是用来读取和重置 Redis 慢查询日志的命令&#xff0c;Redis 2.2.12 版本开始支持 1.Redis 慢查询日志概述 客户端从发送命令到获取返回结果经过了以下几个步骤&#xff1a; 1. 客户端发送命令 2. 该命令进入 Redis 队列排队等待执行 3. Redis 开始执行命令 - Red…

浅析JavaWeb内存马基础原理与查杀思路

文章目录 前言Java内存马内存马分类&原理JavaWeb三大组件注入Servlet内存马注入Filter型内存马JAVA Agent内存马 哥斯拉木马0x01 WebShell0x02 MemShell0x03 FilterShell0x04 Arthas排查0x05 scanner查杀 总结 前言 几年前写过《Web安全-一句话木马》&#xff0c;主要介绍…

PurpleKeep:提供Azure管道以创建基础设施并执行Atomic测试

关于PurpleKeep PurpleKeep是一款功能强大的安全测试自动化工具&#xff0c;该工具能够通过提供Azure管道以创建基础设施&#xff0c;并帮助广大研究人员执行Atomic测试。 随着攻击技术种类的迅速增加&#xff0c;以及EDR&#xff08;端点检测和响应&#xff09;和自定义检测规…

二叉树层序遍历 及相关题目

1&#xff0c;力扣102 给你二叉树的根节点 root &#xff0c;返回其节点值的 层序遍历 。 &#xff08;即逐层地&#xff0c;从左到右访问所有节点&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;[[3],[9,20],[15,7]]示例…

Canvas背景绘制-24

本节会详细介绍下&#xff0c;如何绘制面板的背景。 概述 常用的技术称为图块复制(blitting)&#xff0c;即从离屏缓冲区中将内容发生变化的那部分背景图像复制到屏幕上&#xff0c;还有其它两种方法是将所有内容擦除并重新绘制&仅重绘内容发生变化的那部分区域。一般是用…

网络:HTTP协议

目录 序列化与反序列化 守护进程 网络计算器的实现 HTTP协议 http的代码演示 HTTPS 初步理解三次握手&#xff0c;四次挥手 ①tcp是面向连接的通信协议&#xff0c;在通信之前&#xff0c;需要进行3次握手&#xff0c;来进行连接的建立(谁connect谁握手) ②当tcp在断开…

稀碎从零算法笔记Day35-LeetCode:字典序的第K小数字

要考虑完结《稀碎从零》系列了哈哈哈 这道题和【LC.42 接雨水】&#xff0c;我愿称之为【笔试界的颜良&文丑】 题型&#xff1a;字典树、前缀获取、数组、树的先序遍历 链接&#xff1a;440. 字典序的第K小数字 - 力扣&#xff08;LeetCode&#xff09; 来源&#xff1…

Linux是怎么发送一个网络包的?

目录 摘要 1 从 send 开始 2 传输层 3 网络层 4 网络接口层 4.1 邻居子系统 4.2 网络设备子系统 4.3 软中断发送剩余的 skb 4.4 硬中断又触发软中断 总结 摘要 一个网络包的发送&#xff0c;始于应用层&#xff0c;经层层协议栈的封装&#xff0c;终于网卡。今天来循…