Android内存泄漏的本质原因、解决办法、操作实例
- 获得一个(巨大的)短生命周期的对象
- 这个【巨大的短生命周期的对象】在Android中最有可能的就是【Activity】了
- 最容易无意识获得它的方式就是【非静态内部类隐式自动持有外部类的强引用】
- 把这个对象赋值给了一个长生命周期的对象
- 这个有一些常见的套路
- 套路一:直接赋值给了一个类的静态成员
- 这个静态成员的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收
- 套路二:一个匿名内部handler的形式持有acitivity,然后message又持有handler,最后长生命周期的Looper持有了这个message
- Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的
- 当Android应用启动的时候,会先创建一个UI主线程的Looper对象,Looper实现了一个简单的消息队列,一个一个的处理里面的Message对象。主线程Looper对象在整个应用生命周期中存在。
- 当在主线程中初始化Handler时,该Handler和Looper的消息队列关联,同时发送到消息队列的Message会引用发送该消息的Handler对象
- 只要Handler发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有
- 当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏
- 套路三:一个匿名内部Runnable持有acitivity,然后这个Runnable有一个耗时任务,这个耗时任务的生命周期比acitivity长
- 异步任务AsyncTask和Runnable都是一个匿名内部类,因此它们对当前Activity都有一个隐式引用
- 线程产生的内存泄漏主要原因在于线程生命周期的不可控。比如线程是Activity的内部类,则线程对象中保存了Activity的一个引用,当线程的run函数耗时较长没有结束时,线程对象是不会被销毁的,因此它引用的老的Activity也不会被销毁,因此就出现了内存泄漏的问题
- Java中的Thread有一个特点就是她们都是直接被GC Root所引用,也就是说Dalvik虚拟机对所有被激活状态的线程都是持有强引用,导致GC永远都无法回收掉这些线程对象,除非线程被手动停止并置为null或者用户直接kill进程操作。所以当使用线程时,一定要考虑在Activity退出时,及时将线程也停止并释放掉
- 套路四:把这个对象传入了一个异步线程
所以解决方法就是(以下只要阻断一处就OK了)
- 不要获得一个(巨大的)短生命周期的对象,假如不需要的时候
- 比如最容易无意识获得Activity的方式就是【非静态内部类隐式自动持有外部类的强引用】
- 为了不让内部类自动持有外部类的强引用,把原来的【非静态内部类或者是匿名内部类】重写为【静态内部类】就可以了,也就是独立出来加一个static
- 或者是不要使用内部类,抽出来成为一个外部类
- (这样做之后内部类里就无法直接使用外部环境了(调用外部类的变量和方法),如果要使用的话,就通过构造方法传进来)
- (这样就避免了内部类自动的无法控制的持有全部外部环境,只让内部类使用指定的外部环境的资源)
- 需要获得一个(巨大的)短生命周期的对象时,使用弱引用
- 如果一定要持有acitivty的引用,那就把这个引用改成弱引用
- 不过在【非静态内部类或者是匿名内部类】的情况下,需要先重写为【静态内部类】;因为得先把自动持有acitivity这东西废了,再通过构造方法把activity传进来,才能把acitivity的引用改为弱引用
- 不要赋值给了一个长生命周期的对象,假如可以的话
- 所以就是不需要使用静态变量的地方就不用
- 不过前面的hander和runnable就没有办法了,改不了
- 控制这个长生命周期的对象的生命周期,假如可以的话
- 比如静态变量,就可以在该清空的时候清空
- 把【(巨大的)短生命周期的对象】换成【(巨大的)长生命周期的对象】
所以就能推出大部分场景了(从上面的套路直接可以得到几种场景)
- 套路一:单例持有外部引用
- 发生场景
- 网络访问库中VolleyCreator单例持有外部Context
- ToastUtil中Toast单例持有外部Context
- NewMsgReceiver单例中的成员变量observers存入了外部Activity引用
- 原因:
- 单例的静态特性使得单例的生命周期和应用的生命周期一样长
- 解决方法:
- 适合使用方法五
- 单例引用Context要注意Context的生命周期,一般的Context可以使用ApplicationContext,对于单例成员变量注意在onDestory移除引用,比如观察者模式取消注册。
- 或者直接在单例里取ApplicationContext就好了
- 套路一:activity的静态变量持有自己的引用
- 这是之前在场景 “一个acitivity杀掉前面的另一个acitivity” 时我们经常使用的方法
- 解决办法
- 可以使用方法四:在onDestroy的时候清空这个静态变量
- 或者不使用这种方法来杀activity,使用eventbus
- 套路一:使用非静态内部类创建静态实例
- 很容易理解,你这个对象持有context,然后把这个对象赋值给静态变量,那这个context就直接泄漏了
- 举例:在启动频繁的Activity中,为了避免重复创建相同的数据资源,会出现:
- 在Activity内部创建了一个非静态内部类的单例,每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏
- public class MainActivity extends AppCompatActivity {
- private static TestResource mResource = null;
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- if(mResource == null){
- mResource = new TestResource();
- }
- //...
- }
- class TestResource {
- //...
- }
- }
- 这种必须要使用静态变量,同时不知道什么时候释放,解决方法就是方法一,直接把非静态内部类改成静态就好了
- 套路二:非静态Handler持有外部Context引用
- 这种情况由于handler需要持有activity,所以使用方法二
- 然后具体是:
- 创建一个静态Handler内部类,然后对Handler持有的对象使用弱引用,这样在回收时也可以回收Handler持有的对象
- 这样虽然避免了Activity泄漏,不过Looper线程的消息队列中还是可能会有待处理的消息,所以更好的做法是在Activity的Destroy时或者Stop时应该移除消息队列中的消息
- 另外,还可以使用开源库WeakHandler,一个防止内存泄漏的Handler,详见banner源码WeakHandler.java
- public class MainActivity extends AppCompatActivity {
- private MyHandler mHandler = new MyHandler(this);
- private TextView mTextView ;
- private static class MyHandler extends Handler {
- private WeakReference<Activity> reference;
- public MyHandler(Activity activity) {
- reference = new WeakReference<>(activity);
- }
- @Override
- public void handleMessage(Message msg) {
- if(reference != null && reference.get() != null ){
- Activity activity = reference.get();
- activity.mTextView.setText("");
- }
- }
- }
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- mTextView = (TextView)findViewById(R.id.textview);
- loadData();
- }
- private void loadData() {
- //...request
- Message message = Message.obtain();
- mHandler.sendMessage(message);
- }
- @Override
- protected void onDestroy() {
- super.onDestroy();
- //使用mHandler.removeCallbacksAndMessages(null);是为了移除消息队列中所有消息和所有的Runnable
- mHandler.removeCallbacksAndMessages(null);
- }
- }
- 套路三:线程造成的内存泄漏
- 比较常见,这两个示例可能每个人都写过:
- new AsyncTask<Void, Void, Void>() {
- @Override
- protected Void doInBackground(Void... params) {
- SystemClock.sleep(10000);
- return null;
- }
- }.execute();
- new Thread(new Runnable() {
- @Override
- public void run() {
- SystemClock.sleep(10000);
- }
- }).start();
- 如果任务可能执行很久,那就需要处理了,使用方法一或方法二就OK了
- //在Activity销毁时候也应该取消相应的任务AsyncTask.cancel(),避免任务在后台执行浪费资源
- static class MyAsyncTask extends AsyncTask<Void, Void, Void> {
- private WeakReference<Context> weakReference;
- public MyAsyncTask(Context context) {
- weakReference = new WeakReference<>(context);
- }
- @Override
- protected Void doInBackground(Void... params) {
- SystemClock.sleep(10000);
- return null;
- }
- @Override
- protected void onPostExecute(Void aVoid) {
- super.onPostExecute(aVoid);
- MainActivity activity = (MainActivity) weakReference.get();
- if (activity != null) {
- //...
- }
- }
- }
- static class MyRunnable implements Runnable{
- @Override
- public void run() {
- SystemClock.sleep(10000);
- }
- }
- //——————使用——————
- new Thread(new MyRunnable()).start();
- new MyAsyncTask(this).execute();
其他套路外会导致内存泄漏的场景(其实深究原因也是套路内的)
- MainActivity和HomeFragment EventBus没有反注册
- 解决方案: onDestory注意反注册
- 资源未关闭造成的内存泄漏
- 对于使用了BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏
- 有些代码并不造成内存泄露,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存
- Bitmap没调用recycle()
- Bitmap对象在不使用时,我们应该先调用recycle()释放内存,然后将它设置为null。因为加载Bitmap对象的内存空间,一部分是java的,一部分C的(因为Bitmap分配的底层是通过JNI调用的)。而这个recycle()就是针对C部分的内存释放
- ViewHolder
- 构造 Adapter 时,没有使用缓存的 convertView ,每次都在创建新的 converView。这里推荐使用ViewHolder
- 集合类
- 集合类如果仅仅有添加元素的方法,而没有相应的删除机制,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等即有静态引用或final一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减
一些tips
- 在activity的onDestroy方法中手动吧所有需要置为Null的静态变量置为null
- 不要在类初始时初始化静态成员。可以考虑lazy初始化
- 尽量不要在静态内部类中使用非静态外部成员变量,使用的话也用弱引用
- 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ;array = null)等,最好遵循谁创建谁释放的原则
附录:
- 这是个非常容易发生的情况,比如你随便new一个匿名内部类的时候就会发生,比如new一个clicklistener,new一个Runnable,new一个Handler,但是这种情况本身没有问题,问题是在搭配其他条件的时候发生的
- 这种情况说白了就是:非静态内部类的对象会持有外部对象的强引用
- 为什么会这样,很好理解: 因为内部类要能使用外部类的资源,就是通过这个引用实现的.
|
public static class MyHandler extends Handler
//声明一个弱引用对象
WeakReference<MainActivity> mReference;
MyHandler(MainActivity activity)
//在构造器中传入Activity,创建弱引用对象
mReference = new WeakReference<MainActivity>(activity);
}
public void handleMessage(Message msg)
//在使用activity之前先判空处理
if (mReference != null && mReference.get() != null)
mReference.get().text.setText(hello word);
}
}
}
|
- 场景
- 比如静态启动acitivity方法中传入上一个acitivity
- 把一些常用的或者公共方法放到一个工具类里,写成静态(static)的形式,如果这个方法需要传递一个参数(外部短生命周期对象的引用)的话
- 原因
- 要想造成内存泄漏,你的工具类对象本身要持有指向传入对象的引用才行!但是当你的业务方法调用工具类的静态方法时,会生产一个称为方法栈帧的东西(每次方法调用,JVM都会生成一个方法栈帧),当方法调用结束返回的时候,当前方法栈帧就已经被弹出了并且被释放掉了。 整个过程结束时,工具类对象本身并不会持有传入对象的引用。
- 把对象引用传递给静态方法(不是静态方法也是一样的),在调用结束时,工具类对象本身并不会引用传入的对象。所以就没有问题。
Android内存泄漏的本质原因、解决办法、操作实例的更多相关文章
- Android - 内存泄漏的情况以及解决方法
[译]Android内存泄漏的八种可能(上) Android防止内存泄漏的八种方法(下). Static Activities 在类中定义了静态Activity变量,把当前运行的Activity实例赋 ...
- Android内存泄漏的各种原因详解
转自:http://mobile.51cto.com/abased-406286.htm 1.资源对象没关闭造成的内存泄漏 描述: 资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我 ...
- 前端知识体系:JavaScript基础-作用域和闭包-闭包的实现原理和作用以及堆栈溢出和内存泄漏原理和相应解决办法
闭包的实现原理和作用 闭包: 有权访问另一个函数作用域中的变量的函数. 创建闭包的常见方式就是,在一个函数中创建另一个函数. 闭包的作用: 访问函数内部变量.保持函数在环境中一直存在,不会被垃圾回收机 ...
- Android 内存泄漏分析与解决方法
在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...
- Android内存泄漏原因
这段时间调试APP的时候,发现程序在加载了过多的bitmap后会崩溃.查看了日志,原来是发生了内存溢出(OOM).第一次遇到这样的问题,那就慢慢排查吧. 内存优化可以参考胡凯大神的博客Android内 ...
- 【转】android 内存泄漏相关收藏博客。
关于android内存泄漏的研究 博客建了几个月,都没有去写,一是因为当时换工作,然后又是新入职(你懂的,好好表现),比较忙:二是也因为自己没有写博客的习惯了.现在还算是比较稳定了,加上这个迭代基 ...
- Android内存泄漏分析及调试
尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先 ...
- Android内存泄漏检测利器:LeakCanary
Android内存泄漏检测利器:LeakCanary MAR 28TH, 2016 是什么? 一言以蔽之:LeakCanary是一个傻瓜化并且可视化的内存泄露分析工具 为什么需要LeakCanary? ...
- Android - 内存泄漏 + 垃圾回收(GC)概念
Android内存泄露——全解析和处理办法 内存泄露 说到内存泄露,就不得不提到内存溢出,这两个比较容易混淆的概念,我们来分析一下. 内存泄露:程序在向系统申请分配内存空间后(new),在使用完毕后未 ...
随机推荐
- Lua脚本和C++交互(一)
现在,越来越多的C++服务器和客户端融入了脚本的支持,尤其在网游领域,脚本语言已经渗透到了方方面面,比如你可以在你的客户端增加一个脚本,这个脚本将会帮你在界面上显示新的数据,亦或帮你完成某些任务,亦或 ...
- Cookie和Session机制详解
会话(Session)跟踪是Web程序中常用的技术,用来跟踪用户的整个会话.常用的会话跟踪技术是Cookie与Session.Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端 ...
- 原:Myeclipse10+Egit+bitbucket实现版本控制
1.首先在https://bitbucket.org注册账号,建立仓库(repository),这部分有问题的可以看https://confluence.atlassian.com/display/B ...
- zip&unzip范例
范例: zip命令可以用来将文件压缩成为常用的zip格式.unzip命令则用来解压缩zip文件. 1. 我想把一个文件abc.txt和一个目录dir1压缩成为yasuo.zip: # zip -r y ...
- .net 将DLL程序集生成到指定目录中
.在程序集右键属性 .在程序集属性界面中找到生成事件 在预先生成事件命令行添加: IF NOT EXIST "$(ProjectDir)..\Bin" MD "$(Pro ...
- 【EF框架】使用params参数传值防止SQL注入报错处理
通过SqlParameter传时间参数,代码如下: var param = new List<SqlParameter>(); param.Add(new SqlParameter(&qu ...
- Android设计和开发系列第二篇:Action Bar(Design)
Action Bar The action bar is a dedicated piece of real estate at the top of each screen that is gene ...
- webpack4 优化记录
webpack4.0优化那些事儿 一 缩小文件搜索范围 1 include & exclude 1) action 限制编译范围 2) useage module: { rules: [ { ...
- 原生js--cookie操作的封装
封装cookie的操作:查询cookie个数.查询所有cookie的键.获取cookie.设置cookie.删除cookie.清除全部cookie /** * cookieStorage */func ...
- 使用MSBUILD 构建时出错 error MSB3086: 任务未能使用 SdkToolsPath“”或注册表项“XXX”找到“LC.exe”,请确保已设置 SdkToolsPath。
如果项目有添加有WB引用,比如引用其它网站的WEB服务等,那么VS在编译时会自动生成个 [项目名称].Serializers.dll的文件,就是把引用服务中的相关对象信息生成硬编码的程序集,以提高效率 ...