Android 内存泄漏

  在分享会上听小伙伴对这部分内容做了讲解,发觉在平时的编程中确实有很多问题没有注意到,故记录下来分享给各位,也欢迎各位不吝赐教纠正文中不足之处。


内存泄漏与内存溢出:

  内存溢出简单讲就是程序运行要求的内存大于虚拟机能提供的最大内存,会导致程序崩溃,也就是我们常见的Out Of Memory错误。

  内存泄露指程序未能释放已经不再使用的内存。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于程序设计的失误,导致在释放该段内存之前就失去了对该段内存的控制,从而造成了内存的浪费。少量的内存泄漏并不会影响到程序的运行,但长时间的积累会消耗越来越多的内存,最终导致内存溢出。

  内存的分配是由程序完成的,而内存的释放有 GC(垃圾回收机制)完成。GC 为了能够正确的释放对象, 必须监控每一个对象的运行状态,包括对象的申请,引用,被引用,赋值等。当检测到一个对象完全无用时,便可以对这个对象进行回收。

  
常见内存泄漏:
  理解了以上概念,我们将列举几种在我们平时容易导致内存泄漏的不好编程习惯,并利用 Android Studio 自带的内存分析工具进行检测内存泄漏。

  • 单例造成的内存泄漏
  • 集合类造成的内存泄漏
  • 非静态内部类造成的内存泄漏
  • 匿名内部类/异步线程造成内存泄漏
  • Handler 造成内存泄漏

①单例造成内存泄漏:

  单例由于其静态的特性使得其生命周期跟应用一样长,处理不当极易导致内存泄漏。
  例如:我们经常在程序的开屏页初始化一些,资源读取,网络请求等单例方法。我们现在来模拟一个这样的场景:
  创建一个用来读取 drawable 资源的工具类,并采用单例的设计模式。

public class ResourceReader {
    //单例对象
    private static ResourceReader mInstance = null;
    private Context context;
    //通过 Context 上下文来创建对象
    private ResourceReader(Context context){
        this.context = context;
    }

    public static ResourceReader getInstance(Context context){
        if(mInstance == null){
            mInstance = new ResourceReader(context);
        }
        return mInstance;
    }

    public Drawable getDrawable(int drawableId){
        return ContextCompat.getDrawable(context, drawableId);
    }
}

在 WelcomeActivity 中,通过工具类读取一张图片,在按钮点击后跳转 MainActivity 并将 WelcomeActivity finish 掉。

public class WelcomeActivity extends AppCompatActivity {

    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        this.imageView = (ImageView) findViewById(R.id.welcome_image);
        //通过工具类读取一个 Drawable 资源
        Drawable drawable = ResourceReader.getInstance(this).getDrawable(R.drawable.welcome);
        imageView.setImageDrawable(drawable);
    }

    public void onClick(View view){
        Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();//关闭该Activity
    }
}

在 MainActivity 中依然用工具类加载一个 drawable 资源。

public class MainActivity extends AppCompatActivity {

    private ImageView imageView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Drawable drawable = ResourceReader.getInstance(this).getDrawable(R.drawable.main);
        imageView = (ImageView) findViewById(R.id.main_image);
        imageView.setImageDrawable(drawable);
    }
}

运行效果如下:




  在调试运行的时候实际上 Android Studio 已经在记录调试设备的一些实时信息了,打开 Android Monitor 的 Montiors,可以看到如下界面

这里写图片描述

  我们可以在点击 ② 按钮一小段时间后点击 ③ 按钮(dump java heap),因为点 ② 后会让我们的设备发起一个 GC 回收操作,回收那些无用的对象,因为这些无用的对象不在我们的考虑范围内。
  点完 ② 按钮后,Studio 就开始自己工作了,稍等一下dump 成功后会生成 hprof 文件并自动打开,文件名为进程加时间戳。


<script type="math/tex" id="MathJax-Element-1"> </script>此图由网友提供

  接下来我们点击右侧 Analyzer Tasks 展开后点击,Perform Analyzer 按钮稍后便可以看到 Studio 为我们自动分析的结果。



  由于我们的代码比较简单,在这里便可以很直观的看出我们内存泄漏的 Activity,在较为复杂的代码情况下我们还可以结合 MAT 一起来使用,可以展现更加直观的结果,这里不再详述。

  现在我们分析一下 WelcomeActivity 内存泄漏的原因:
  我们在WelcomeActivity 中调用 drawable 加载工具类时传入了其自身上下文使得ResourceReader 的静态对象拥有了对 WelcomeActivity 的引用,在我们使用完 WelcomeActivity 并调用 finish() 方法时,我们以为其已经销毁并且内存可以被回收,其实不然,由于其被引用GC 系统无法回收这段内存而且我们也失去对这段内存的控制,这便导致了WelcomeActivity 的内存泄漏。

解决办法:
  ①我们在 Activity 被销毁时消除单例对它的引用。这种方法有所限制,并不是所有的情况都适合。例如类中存在非静态属性,则在不同时间调用可能导致其值错乱。
在 ResourceReader 工具类中加入以下代码:

public void reset(){
        if(mInstance != null){
            mInstance = null;
            context = null;
        }
    }

  在 Activity 的销毁方法中调用上边新加入的方法:

@Override
protected void onDestroy() {
      ResourceReader.getInstance(this).reset();
      super.onDestroy();
     }

  ②在单例中我们尽可能的引用生命周期较长的对象,如 Application(推荐)改动也较少,只需要将 Context 改为 ApplicationContext。

public static ResourceReader getInstance(Context context){
        if(mInstance == null){
            //将 Context 改为 ApplicationContext
            mInstance = new ResourceReader(context.getApplicationContext());
        }
        return mInstance;
    }

修改完毕之后我们再次运行程序看看结果:




再看我们的 Analyzer Tasks 的分析





②集合类造成内存泄漏:

  如果某个集合是类的全局变量,如果该变量没有相应的删除机制则很有可能导致该集合占用的内存只增不减。
  模拟一种场景,有一个 Bitmap 的列表在某个操作后会加入一张图片,但是由于设计的缺陷该对象并没有删除元素的机制。

public class GatherActivity extends AppCompatActivity {

    private static final List<Bitmap> bitmapList = new ArrayList<>();
    private TextView textView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_gather);
        textView = (TextView) findViewById(R.id.list_numer);
    }

    public void onClick(View view){
        if(view.getId() == R.id.btn_add){
            //加入Bitmap
            bitmapList.add(BitmapFactory.decodeResource(getResources(), R.drawable.welcome));
        }
        textView.setText(String.format("共有 %d 个元素", bitmapList.size()));
    }

    @Override
    protected void onDestroy() {
        Log.v("ygl","Gather Activity On Destroy");
        super.onDestroy();
    }
}


解决办法:
  ①对于 final static 修饰符一定要慎用。
  ②对于集合一定要在特定的时机进行删除元素,或清空,或转储本地,避免集合所占内存无限制增长。



③非静态内部类造成内存泄漏:

  这是经常被我们忽略的一点,非静态内部类默认会持有对外部类的引用,而该非静态内部类有创建了一个静态实例,如果没有合理释放则会造成内存泄露。

public class NearActivity extends AppCompatActivity {
    //非静态内部类 User 创建的静态实例 mUser
    private static User mUser = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_near);
        mUser = new User();
    }

    @Override
    protected void onDestroy() {
        Log.v("ygl","Near Activity On Destroy");
        super.onDestroy();
    }

    class User{
        private String name;
        private int age;
    }
}

  我们通过上边的分析方法可以得出,在 NearActivity 销毁后其内存并没有被回收。

这里写图片描述

解决办法:
  ①在合适的时机将内部类的静态对象进行销毁,如:

 @Override
    protected void onDestroy() {
        Log.v("ygl","Near Activity On Destroy");
        if(mUser != null){
            mUser = null;
        }
        super.onDestroy();
    }

  ②将内部类定义为静态内部类,使其不与外部类建立关系。



④匿名内部类/异步线程导致内存泄漏:

  匿名内部类会持有一个外部类的引用,如果再将该引用传入异步线程,此线程与外部类的生命周期不再相同,就可能导致外部类对象内存泄漏。
  举一个我们经常用到的场景:

public class SyncActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sync);
        //创建匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                for(int i=0;i<1000;i++){
                    try{
                        Thread.sleep(1000);
                        Log.v("ygl","i="+i);
                    }catch (Exception e){}
                }
            }
        }).start();
        //销毁Activity
        finish();
    }

    @Override
    protected void onDestroy() {
        Log.v("ygl","Sync Activity On Destroy");
        super.onDestroy();
    }
}

  大家可能已经猜到了,由于子线程在 Activity 销毁后依然会继续执行,而内部类 new Runnable() {};拥有对外部类 Activity的引用导致了Activity所占内存无法被回收。

解决办法:
  ①如果是在 Activity 结束后已没有必要运行的线程,在 onDestroy 中中断子线程的运行。
  ②可以考虑用全局的线程池代替在类中声明子线程。


⑤Handler 造成内存泄漏:

  Handler 生命周期和 Activity 是不一致的,当 Activity 被 finish 时,延迟执行任务的 Message 还会继续执行存在于主线程中,它持有 Activity 的 Handler 引用,导致 Activity 无法被回收。

public class SampleActivity extends Activity {

   private final Handler mHandler = new Handler() {
      @Override
      public void handleMessage(Message msg) {
         ...
      }
   }
   @Override
   protected void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       mHandler.postDelayed(new Runnable() {
          @Override
          public void run() { 
             ...
      }
       }, 1000 * 60 * 10);

      finish();
   }
}


解决办法:
  ①在 Activity 结束时可以清空不必要的 Message 消息
  ②采用静态内部类和弱引用结合的方式



总结:

  • 对Activity 等组件的引用应该控制在Activity的生命周期内;如果不能则考虑使用 ApplicationContext,尽量避免Activity被外部长生命周期的对象引用。
  • 尽量不要再静态变量或静态内部类中使用非静态外部成员变量,即使必须使用,应在合适的时机将外部成员变量置空。
  • Handler 资源释放时可以清空Handler 里面的消息。
  • 某些比较占内存的对象最好可以在使用完毕后主动的释放,比如 Bitmap.Recycle(),清空数组等。
  • 对于各种网络流,文件流等要及时地关闭。
  • 对于比较敏感的 单例 静态对象 全局集合等要慎重的考虑。
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值