Android内存泄漏的本质原因、解决办法、操作实例
今年最后一个迭代终于结束了,把过程中碰到的不熟悉的东西拉出来学习总结一下
内存泄漏的本质是:【一个(巨大的)短生命周期对象的引用被一个长生命周期(异步生命周期)的对象持有】
这个东西分为两个部分
所以解决方法就是(以下只要阻断一处就OK了)
所以就能推出大部分场景了(从上面的套路直接可以得到几种场景)
其他套路外会导致内存泄漏的场景(其实深究原因也是套路内的)
一些tips
- 获得一个(巨大的)短生命周期的对象
- 这个【巨大的短生命周期的对象】在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退出时,及时将线程也停止并释放掉
- 套路四:把这个对象传入了一个异步线程
- 不要获得一个(巨大的)短生命周期的对象,假如不需要的时候
- 比如最容易无意识获得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一直指向它),那么没有相应的删除机制,很可能导致集合所占用的内存只增不减
- 在activity的onDestroy方法中手动吧所有需要置为Null的静态变量置为null
- 不要在类初始时初始化静态成员。可以考虑lazy初始化
- 尽量不要在静态内部类中使用非静态外部成员变量,使用的话也用弱引用
- 在 Java 的实现过程中,也要考虑其对象释放,最好的方法是在不使用某对象时,显式地将此对象赋值为 null,比如使用完Bitmap后先调用 recycle(),再赋为null,清空对图片等资源有直接引用或者间接引用的数组(使用 array.clear() ;array = null)等,最好遵循谁创建谁释放的原则
附录:
非静态内部类隐式自动持有外部类的强引用
- 这是个非常容易发生的情况,比如你随便new一个匿名内部类的时候就会发生,比如new一个clicklistener,new一个Runnable,new一个Handler,但是这种情况本身没有问题,问题是在搭配其他条件的时候发生的
- 这种情况说白了就是:非静态内部类的对象会持有外部对象的强引用
- 为什么会这样,很好理解: 因为内部类要能使用外部类的资源,就是通过这个引用实现的.
弱引用持有的写法(以handler持有acitivty为例)
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方法中传入上一个acitivity
- 把一些常用的或者公共方法放到一个工具类里,写成静态(static)的形式,如果这个方法需要传递一个参数(外部短生命周期对象的引用)的话
- 原因
- 要想造成内存泄漏,你的工具类对象本身要持有指向传入对象的引用才行!但是当你的业务方法调用工具类的静态方法时,会生产一个称为方法栈帧的东西(每次方法调用,JVM都会生成一个方法栈帧),当方法调用结束返回的时候,当前方法栈帧就已经被弹出了并且被释放掉了。 整个过程结束时,工具类对象本身并不会持有传入对象的引用。
- 把对象引用传递给静态方法(不是静态方法也是一样的),在调用结束时,工具类对象本身并不会引用传入的对象。所以就没有问题。