Android 内存优化不完全手册

我的技术博客:移动开发小水吧

什么是内存泄漏

只要是现代智能电子设备,不管或大或小,都会有一个叫做内存的硬件,在手机中这个硬件的参数尤为重要,是我们评价一个手机好坏的标准之一。

以Android手机为例,我们开发的程序如果想要运行起来,就需要开启一个独立的进程,而这个进程如果想要运行起来,就必须占用一部分的内存,这就是我们的应用和手机内存之间的关系了。
说到这里,我们就可以聊聊内存泄漏了(以下内容来自百度百科)。

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃。

百度百科的解释已经很明白了,具体到程序方面来说,基本上就是该回收的对象由于一些原因无法正常回收,这样的对象越来越多导致手机的内存占用率居高不下,可用的空闲内存越来越少,从而频繁的触发垃圾回收机制,以至于降低了应用的流畅度,严重的时候会导致内存溢出(OOM Out Of Memory)的问题,导致程序崩溃。

至于什么是内存溢出,请自行搜索学习,该知识点不在本篇教程内。

Java的四种引用

聊到Java的四种引用,其实也是一个知识点的加固,因为一些考虑到内存泄漏的解决方案中,需要用到这里的知识点,所以先讲解给大家:

  • 强引用:强引用是Java中最普通的引用,随意创建一个对象然后在其他的地方引用一下,就是强引用,强引用的对象Java宁愿OOM也不会回收他
  • 软引用: 软引用是比强引用弱的引用,在Java gc的时候,如果软引用所引用的对象被回收,首次gc失败的话会继而回收软引用的对象,软引用适合做缓存处理 可以和引用队列(ReferenceQueue)一起使用,当对象被回收之后保存他的软引用会放入引用队列
  • 弱引用: 弱引用是比软引用更加弱的引用,当Java执行gc的时候,如果弱引用所引用的对象被回收,无论他有没有用都会回收掉弱引用的对象,不过gc是一个比较低优先级的线程,不会那么及时的回收掉你的对象。 可以和引用队列一起使用,当对象被回收之后保存他的弱引用会放入引用队列
  • 虚引用: 虚引用和没有引用是一样的,他必须和引用队列一起使用,当Java回收一个对象的时候,如果发现他有虚引用,会在回收对象之前将他的虚引用加入到与之关联的引用队列中。可以通过这个特性在一个对象被回收之前采取措施

具体用法可以自己查询一下,写几个例子,这里就不展开讲了。

常见内存泄漏

  • 非静态内部类和匿名内部类造成的内存泄漏

    这里首先我们了解一下内部类和静态内部类的区别:

    两种类对比项 静态内部类 非静态内部类
    与外部类的引用关系 如果没有传入参数则无引用关系 自动获得强引用关系
    被调用时需要外部实例 不需要 需要
    能否使用外部类的非静态成员 不能
    生命周期 自主的生命周期 依赖外部类,甚至更长

    首先我们要考虑一下,在Android里什么时候我们会用到内部类呢?

    最常见的就是Handler、AysncTask和Thread这三个类了,所以这就会造成一个内存泄漏的隐患,那就是如果我们正在通过Handler请求网络数据,这时我们突然关闭了Activity,但是网络请求并没有结束,此时作为内部类的Handler还持有外部Activity的强引用关系,从而导致该Activity的内存并没有被正常回收,这就发生了内存泄漏,而且还有可能造成一些不可预测的空指针的问题。

    下面将三个类的内存泄漏及解决方案都列出来,以后遇到类似的问题就可以避免了:

    1. AsyncTask

      public class AsyncTaskOOMActivity extends AppCompatActivity {
          @Override
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              new MyAsyncTask().execute();
          }
          class MyAsyncTask extends AsyncTask<Void,Integer,String>{
              @Override
              protected String doInBackground(Void... voids) {
                  try {
                      Thread.sleep(5000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  return "";
              }
          }
      }
      

      这种写法的AsyncTask在AndroidStudio中会有警告提示,提示的内容是:

      This AsyncTask class should be static or leaks might occur (com.jasoncool.study.oom.asynctask.AsyncTaskOOMActivity.MyAsyncTask) less... (Ctrl+F1)
      A static field will leak contexts. Non-static inner classes have an implicit reference to their outer class. If that outer class is for example a Fragment or Activity, then this reference means that the long-running handler/loader/task will hold a reference to the activity which prevents it from getting garbage collected. Similarly, direct field references to activities and fragments from these longer running instances can cause leaks. ViewModel classes should never point to Views or non-application Contexts.

      说白了其实就是告诉你,非静态内部类这么用就内存泄漏了,让你改正过来,我们看一下改后的代码:

      public class AsyncTaskOOMActivity extends AppCompatActivity {
          @Override
          protected void onCreate(@Nullable Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              new MyAsyncTask().execute();
          }
         static class MyAsyncTask extends AsyncTask<Void,Integer,String>{
              @Override
              protected String doInBackground(Void... voids) {
                  try {
                      Thread.sleep(5000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
                  return "";
              }
          }
      }
      

      就是这么简单,加一个static关键字就药到病除了。

      1. Handler

      Handler是我们在开发过程中比较常见的工具类了,几乎是每个Android程序员入门必须掌握的知识点,但是这个类也是经常出现内存泄漏的元凶,如果使用不好,会带来很多意想不到的内存泄漏,下面请看代码:

      public class HandlerOOMActivity extends AppCompatActivity {
      
          private TextView mTextView;
          private Handler mHandler = new Handler(){
              @Override
              public void handleMessage(Message msg) {
                  mTextView.setText("内存溢出了");
              }
          };
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              mTextView = (TextView) findViewById(R.id.text);//模拟内存泄露
              loadData();
              this.finish();
          }
      
          private void loadData(){
              mHandler.postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      //什么都不干 就延迟180秒
                  }
              }, 3 * 60 * 1000);
      
          }
      
          @Override
          protected void onDestroy() {
              super.onDestroy();
          }
      }
      

      这个例子其实和上一个AsyncTask非常类似,只不过这里多了一个概念就是在内部类中我们引用了外部类的TextView,如果简单的将Handler改成静态内部类的话,我们还记得 静态内部类有一个特点就是我们无法似乎用外部类的非静态资源,那这个TextView就无法在Handler中进行使用了,那么该如何解决呢?其实我们可以使用传入外部Activity的Context的方式来解决该问题,为了保险起见,还可以将该Context设置成弱引用的方式使用,并且我们再onDestroy的时候,还可以使用 mHandler.removeCallbacksAndMessages(null);方法来中端handler的请求和回调,并将handler置空,具体看下面的代码:

      public class HandlerOOMActivity extends AppCompatActivity {
      
          private TextView mTextView;
          private SoftHandler mHandler = new SoftHandler(this);
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              mTextView = (TextView) findViewById(R.id.text);//模拟内存泄露
              loadData();
              this.finish();
          }
      
          public static class SoftHandler extends Handler{
              WeakReference<Activity> weakContext;
               public SoftHandler(Activity context) {
                  weakContext = new WeakReference<>(context);
              }
              @Override
              public void handleMessage(Message msg) {
                  HandlerOOMActivity weakActivity= (HandlerOOMActivity)weakContext.get();
                  weakActivity.mTextView.setText("内存不溢出了");
              }
          };
      
          private void loadData(){
              mHandler.postDelayed(new Runnable() {
                  @Override
                  public void run() {
                      //什么都不干 就延迟180秒
                  }
              }, 3 * 60 * 1000);
      
          }
          
          @Override
          protected void onDestroy() {
              super.onDestroy();
              mHandler.removeCallbacksAndMessages(null);
              mHandler=null;
          }
      }
      

      可以看到,虽然写法复杂了一些,但是也非常好理解,这样基本上就可以避免Handler的内存溢出了。

      3.Thread

      我们在开发过程中,有时候会新建一个线程执行一些耗时操作,但是写耗时操作时我们建立的线程一般都是匿名内部类的方式创建的,匿名内部类也会持有外部类的强引用,一样会遇到内存泄漏的问题,请看代码:

      public class ThreadOOMActivity extends AppCompatActivity {
      
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              new Thread(new Runnable() {
                  @Override
                  public void run() {
                      try {
                          Thread.sleep(50000);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                  }
              }).start();
          }
      }
      

      这个new Thread就是一个匿名内部类了,如果在线程休眠的这段时间内我关闭了该Activity的话,一样会造成内存泄漏的问题,建议的写法应当是:

      public class ThreadOOMActivity extends AppCompatActivity {
          SoftThread softThread;
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
             softThread=new SoftThread();
             softThread.start();
          }
      
          static class SoftThread  extends  Thread{
              @Override
              public void run() {
                  //自己的扩展...
                  try {
                      Thread.sleep(50000);
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
          }
      }
      
  • 单例模式引起的内存泄漏

    单例模式是开发人员最经常接触的一个设计模式了,其主要目的是为了让应用中只生成一个对象在应用中使用,因为创建对象也要耗费一定的资源,频繁创建对象肯定不如长期使用一个对象要合理,所以单例模式的应用非常广泛,一般的代码如下:

    public class SingleUtils {
        private static volatile SingleUtils mInstance = null;
        private Context context;
        private SingleUtils (Context context){
            this.context = context;
        }
     
        public static SingleUtils getInstance(Context context){
            if(mInstance == null){
                synchronized (Singleton.class) {
                    if(mInstance == null)
                mInstance = new SingleUtils (context);
                }
                }
            }
            return mInstance;
        }
     
        public Object getObject(){//根据业务逻辑传入参数
            //返回业务逻辑结果,这里需要用到context
        }
    }
    

    一般只要不出现重大的程序问题,单例模式的生命周期都是跟应用的生命周期一致的,如果我们把一个Activity的Context传给了单例模式的逻辑中,那么这个单例模式的静态变量就会引用了我们的Activity的Context,在默认的情况下就会造成即便关闭了该Activity,也无法在内从中回收该Activity,从而导致了内存泄漏。

    解决方案:

    将该Activity的Context换成生命周期较长的ApplicationContext,ApplicationContext是跟着应用的生命周期而建立和销毁的,这样就和单例模式的一致了,也避免了内存泄漏问题的发生。

  • 资源不及时回收引起的内存泄露

    图片加载是手机端占用内存的大户,处理不好图片很容易造成内存泄漏,甚至导致内存溢出,不过处理图片需要讲解的东西太多了,我在这里主要说一下Bitmap的内存泄漏,至于如何优化图片的技术,如:图片质量压缩、图片尺寸裁剪、改变图片颜色模式等知识点请自行搜索学习,我就不展开讨论了。

    说到Bitmap内存泄漏,其实最大的问题就是没有及时的回收资源,只要在图片使用完后调用recylce()方法,并将Bitmap对象赋值为null就可以了。

    很多地方其实都适用这种处理方式,比如 Sqlite数据库在查询结束后要记得关闭Cursor.close(),又或者我们在处理完文件流后要记得InputStream.close(),以及EventBus在使用完成后也要记得unregister(),等等,这些知识点要在平时开发的过程中很注意,不然就会为未来的项目维护埋下各种深坑,谨记。

  • WebView引起的内存泄漏

    WebView主要是用来加载网页的,在H5大行其道的今天,WebView的使用场景非常常见,合理的释放WebView占用的资源也是处理内存泄漏的关键,下面我们来看一下WebView如何释放资源,代码如下:

    private void destroyWebView() {
            if (mWebView != null) {
                mWebView.pauseTimers();
                mWebView.removeAllViews();
                mWebView.destroy();
                mWebView = null;
            }
        }
    
  • 属性动画引起的内存泄漏

    属性动画中有一种动画的执行时间模式是ValueAnimator.INFINITE,这种方式是无限循环的。如果页面逻辑比较复杂,属性动画可能会和Handler配合使用,在这种情况下,就有可能造成属性动画内存泄漏了,解决问题的方法也比较简单,在关闭该页面前,在onDestory方法中调用属性动画的cancel()方法,并将该动画的对象置为Null即可。

  • Service引起的内存泄漏

    Service停止失败,也会导致内存泄漏,因为系统会将这个Service所在的进程进行保留,如果该进程耗内存很大,便会造成内存泄漏,容易莫名的引发GC。

    所以在使用Service方面,可以尽量尝试使用IntentService,IntentService的本质其实就是Service+HandlerThread,它有很多优点,其中一个就是IntentService在执行完指定的任务后会自行关闭,不用用户手动关闭,避免了内存泄漏的发生,下面来看一下IntentService的源码:

    package android.app;
    
    import android.annotation.WorkerThread;
    import android.annotation.Nullable;
    import android.content.Intent;
    import android.os.Handler;
    import android.os.HandlerThread;
    import android.os.IBinder;
    import android.os.Looper;
    import android.os.Message;
    
    public abstract class IntentService extends Service {
        private volatile Looper mServiceLooper;
        private volatile ServiceHandler mServiceHandler;
        private String mName;
        private boolean mRedelivery;
    
        private final class ServiceHandler extends Handler {
            public ServiceHandler(Looper looper) {
                super(looper);
            }
    
            @Override
            public void handleMessage(Message msg) {
                onHandleIntent((Intent)msg.obj);
                stopSelf(msg.arg1);
            }
        }
    
        /**
         * Creates an IntentService.  Invoked by your subclass's constructor.
         */
        public IntentService(String name) {
            super();
            mName = name;
        }
    
        /**
         * Sets intent redelivery preferences.  Usually called from the constructor
         */
        public void setIntentRedelivery(boolean enabled) {
            mRedelivery = enabled;
        }
    
        @Override
        public void onCreate() {
            // TODO: It would be nice to have an option to hold a partial wakelock
            // during processing, and to have a static startService(Context, Intent)
            // method that would launch the service & hand off a wakelock.
    
            super.onCreate();
            HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
            thread.start();
    
            mServiceLooper = thread.getLooper();
            mServiceHandler = new ServiceHandler(mServiceLooper);
        }
    
        @Override
        public void onStart(@Nullable Intent intent, int startId) {
            Message msg = mServiceHandler.obtainMessage();
            msg.arg1 = startId;
            msg.obj = intent;
            mServiceHandler.sendMessage(msg);
        }
    
        /**
         * You should not override this method for your IntentService. Instead,
         */
        @Override
        public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
            onStart(intent, startId);
            return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
        }
    
        @Override
        public void onDestroy() {
            mServiceLooper.quit();
        }
    
        /**
         * Unless you provide binding for your service, you don't need to implement this
         */
        @Override
        @Nullable
        public IBinder onBind(Intent intent) {
            return null;
        }
    
        /**
         */
        @WorkerThread
        protected abstract void onHandleIntent(@Nullable Intent intent);
    }
    

    源码并不多,大家应该要通读一下,其中关键一行代码是26行:stopSelf(msg.arg1);

    这里stopSelf(msg.arg1),会先看队列中是否有消息待处理,如果有则继续处理后面的消息,没有才会将Service销毁。

  • Static引起的内存泄漏

    在非必要的情况下,不要轻易的使用Static来修饰成员变量,这会导致其生命周期和app进程的生命周期一样,从而在静态变量大量存在的情况下会导致应用内存占用量大的问题,且更容易被系统回收,解决方案就是尽量用懒加载的方式来定义成员变量。Static引起的内存泄漏

  • 用缓存避免内存泄漏

    很常见的一个例子就是图片的三级缓存结构,分别为网络缓存,本地缓存以及内存缓存。在内存缓存逻辑类中,通常会定义这样的集合类。

    private HashMap<String, Bitmap> mMemoryCache = new HashMap<String, Bitmap>();//String类为该图片对应url
    

    三级缓存结构过程介绍:

    在用户切换到展示图片的界面时,当然是优先判断内存缓存是否为Null,不为空直接展示图片,若为空,同样的逻辑去判断本地缓存(不为空便设置内存缓存并展示图片),本地缓存再为空才会根据该图片的url用网络下载类去下载该图片并展示图片(当然了,下载到图片后会有设置本地缓存以及内存缓存的操作)。

    内存泄漏的问题就出现在内存缓存中:只要HashMap对象实例被引用,而Bitmap对象又都是强引用,Bitmap中图片越来越多,即便是内存溢出了,垃圾回收器也不会处理。

解决方案:

(1)我们可以选择使用软引用,从而在内存不足时,垃圾回收器更容易回收Bitmap垃圾。

private HashMap<String, SoftReference<Bitmap>> mMemoryCache = new HashMap<String, SoftReference<Bitmap>>();

(2)Android2.3以后,SoftReference不再可靠。垃圾回收期更容易回收它,不再是内存不足时才回收软引用。那么缓存机制便失去了意义。

Google官方建议使用LruCache作为缓存的集合类。其实内部封装了LinkedHashMap。内部原理是一直判断集合大小是否超出给定的最大值,超出就把最早最少使用的对象踢出集合。

private LruCache<String, Bitmap> mMemoryCache = new LruCache<String, Bitmap>
((int)(Runtime.getRuntime().maxMemory()/8)){ 
//用最大内存的1/8分配给这个集合使用
//让这个集合知道每个图片的大小
@Override
protected int sizeOf(String key, Bitmap value){
int byteCount = value.getRowBytes() * value.getHeight();//计算图片大小,每行字节数*高度
return byteCount;
  }
}; 
  • 集合类导致的内存泄漏

    集合类添加元素后,仍引用着集合元素对象,导致该集合中的元素对象无法被回收,从而导致内存泄露,举个例子:

     static List<Object> objectList = new ArrayList<>();
       for (int i = 0; i < 10; i++) {
           Object obj = new Object();
           objectList.add(obj);
           obj = null;
        }
    

    在这个例子中,循环多次将 new 出来的对象放入一个静态的集合中,因为静态变量的生命周期和应用程序一致,而且他们所引用的对象 Object 也不能释放,这样便造成了内存泄露。

    解决方法:在集合元素使用之后从集合中删除,等所有元素都使用完之后,将集合置空。

     objectList.clear();
     objectList = null;
    

内存泄漏总结

说了这么多,其实这些知识点很多朋友在其他技术文章中也都看到过了,我这里也只是做了归纳和总结,其中比较关键的就是在开发的过程中一定要心细,不要怕麻烦,对一些可能存在的内存泄漏一定要及时的处理,养成良好的编程习惯和责任心。

平时在开发的过程中,除了知道这些内存泄漏的知识点外还应当学习和利用一些工具来帮助自己分析应用的内存泄漏问题,这里我先推荐两个:一个是Android端的LeakCanary,还一个是AndroidStudio3.0出来的Profiler分析器,至于怎么用,大家可以自行查询相关文档学习,我这里就只是抛个砖。

谢谢你看完!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容