android 内存泄漏(如何防止内存泄漏)

由于项目中大量出现内存泄漏导致内存使用量增多而不能立马释放,不得不研究内存泄漏,接下来我们切入主题。
以下都是本人收集和总结的内容:

1. 什么是内存泄漏

一般情况下内存泄漏是由忘记释放分配的内存导致的,而逻辑上的内存泄漏则是由于忘记在对象不再被使用的时候释放对其的引用导致的。如果一个对象仍然存在强引用,垃圾回收器就无法对其进行垃圾回收。

2. android中的存储泄漏

在安卓平台,泄漏 Context 对象问题尤其严重。这是因为像 Activity 这样的 Context 对象会引用大量很占用内存的对象,例如 View 层级,以及其他的资源。如果 Context 对象发生了内存泄漏,那它引用的所有对象都被泄漏了。安卓设备大多内存有限,如果发生了大量这样的内存泄漏,那内存将很快耗尽。
如果一个对象的合理生命周期没有清晰的定义,那判断逻辑上的内存泄漏将是一个见仁见智的问题。幸运的是,activity 有清晰的生命周期定义,使得我们可以很明确地判断 activity 对象是否被内存泄漏。onDestroy() 函数将在 activity 被销毁时调用,无论是程序员主动销毁 activity,还是系统为了回收内存而将其销毁。如果 onDestroy 执行完毕之后,activity 对象仍被 heap root 强引用,那垃圾回收器就无法将其回收。所以我们可以把生命周期结束之后仍被引用的 activity 定义为被泄漏的 activity。
Activity 是非常重量级的对象,所以我们应该极力避免妨碍系统对其进行回收。然而有多种方式会让我们无意间就泄露了 activity 对象。我们把可能导致 activity 泄漏的情况分为两类,一类是使用了进程全局(process-global)的静态变量,无论 APP 处于什么状态,都会一直存在,它们持有了对 activity 的强引用进而导致内存泄漏,另一类是生命周期长于 activity 的线程,它们忘记释放对 activity 的强引用进而导致内存泄漏。

3. 常见导致App内存泄漏的情况

3.1 静态 Activity

泄漏 activity 最简单的方法就是在 activity 类中定义一个 static 变量,并且将其指向一个运行中的 activity 实例。如果在 activity 的生命周期结束之前,没有清除这个引用,那它就会泄漏了。这是因为 activity(例如 MainActivity) 的类对象是静态的,一旦加载,就会在 APP 运行时一直常驻内存,因此如果类对象不卸载,其静态成员就不会被垃圾回收。

public class MainActivity extends AppCompatActivity {

private static MainActivity activity;

@Override
protected void onCreate(Bundle savedInstanceState) {
  View saButton = findViewById(R.id.sa_button);
  saButton.setOnClickListener(new View.OnClickListener() {
    @Override public void onClick(View v) {
      setStaticActivity();
      nextActivity();
    }
  });
}


void setStaticActivity() {
  activity = this;
}
  
  void nextActivity() {
        Intent intent = new Intent(this, DestinationActivity.class);
        startActivity(intent);
        SystemClock.sleep(600);
        finish();
    }
}

3.2 静态 View

另一种类似的情况是对经常启动的 activity 实现一个单例模式,让其常驻内存可以使它能够快速恢复状态。然而,就像前文所述,不遵循系统定义的 activity 生命周期是非常危险的,也是没必要的,所以我们应该极力避免。
但是如果我们有一个创建起来非常耗时的 View,在同一个 activity 不同的生命周期中都保持不变呢?所以让我们为它实现一个单例模式。现在一旦 activity 被销毁,内存就会泄漏!因为一旦 view 被加入到界面中,它就会持有 context 的强引用,也就是我们的 activity。由于我们通过一个静态成员引用了这个 view,所以我们也就引用了 activity,因此 activity 就发生了泄漏。所以一定不要把加载的 view 赋值给静态变量,如果你真的需要,那一定要确保在 activity 销毁之前将其从 view 层级中移除

public class MainActivity extends AppCompatActivity {
 private static View view;
@Override
protected void onCreate(Bundle savedInstanceState) {
  View svButton = findViewById(R.id.sv_button);
  svButton.setOnClickListener(new View.OnClickListener() {
    @Override 
    public void onClick(View v) {
      setStaticView();
      nextActivity();
    }
  });
}

void setStaticView() {
  view = findViewById(R.id.sv_button);
}


}

3.3 内部类

现在让我们在 activity 内部定义一个类,也就是内部类。这样做的原因有很多,比如增加封装性和可读性。如果我们创建了一个内部类的对象,并且通过静态变量持有了 activity 的引用,那也会发生 activity 泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
     View icButton = findViewById(R.id.ic_button);
     icButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          createInnerClass();
          nextActivity();
      }});
    }

    
  void createInnerClass() {
      class InnerClass {
      }
      inner = new InnerClass();
  }
}

3.4 Handlers

同样的,定义一个匿名的 Runnable 对象并将其提交到 Handler 上也可能导致 activity 泄漏。Runnable 对象间接地引用了定义它的 activity 对象,而它会被提交到 Handler 的 MessageQueue 中,如果它在 activity 销毁时还没有被处理,那就会导致 activity 泄漏了。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    
      View hButton = findViewById(R.id.h_button);
      hButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          createHandler();
          nextActivity();
      }
  });
 }

   void createHandler() {
    new Handler() {
        @Override public void handleMessage(Message message) {
            super.handleMessage(message);
        }
    }.postDelayed(new Runnable() {
        @Override public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
} 

3.5 Threads

同样的,使用 Thread 和 TimerTask 也可能导致 activity 泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          spawnThread();
          nextActivity();
      }
    });
 }
 void spawnThread() {
     new Thread() {
         @Override public void run() {
             while(true);
         }
     }.start();
 }  
}


3.6 Timer Tasks

只要它们是通过匿名类创建的,尽管它们在单独的线程被执行,它们也会持有对 activity 的强引用,进而导致内存泄漏。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          scheduleTimer();
          nextActivity();
      }
    });
 }

void scheduleTimer() {
    new Timer().schedule(new TimerTask() {
        @Override
        public void run() {
            while(true);
        }
    }, Long.MAX_VALUE >> 1);
  }
   
}

3.7 Sensor Manager

系统服务可以通过 context.getSystemService 获取,它们负责执行某些后台任务,或者为硬件访问提供接口。如果 context 对象想要在服务内部的事件发生时被通知,那就需要把自己注册到服务的监听器中。然而,这会让服务持有 activity 的引用,如果程序员忘记在 activity 销毁时取消注册,那就会导致 activity 泄漏了。

public class MainActivity extends AppCompatActivity {
  private static Object inner;
  @Override
  protected void onCreate(Bundle savedInstanceState) {

      View tButton = findViewById(R.id.t_button);
      tButton.setOnClickListener(new View.OnClickListener() {
      @Override 
      public void onClick(View v) {
          registerListener() ;
          nextActivity();
      }
    });
 }

  void registerListener() {
         SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
         Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
         sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    } 
}

总结:在编写代码时要注意释放不该有的内存,最坏的情况下,你的 APP 可能会由于大量的内存泄漏而内存耗尽,进而闪退,但它并不总是这样。相反,内存泄漏会消耗大量的内存,但却不至于内存耗尽,这时,APP 会由于内存不够分配而频繁进行垃圾回收。垃圾回收是非常耗时的操作,会导致严重的卡顿。在 activity 内部创建对象时,一定要格外小心,并且要经常测试是否存在内存泄漏。

4. 使用as工具检测内存泄漏

在我们的日常追求构建更好的应用程序时,我们作为开发者需要考虑多方面的因素,其中之一是要确保我们的应用程序不会崩溃。崩溃的一个常见原因是内存泄漏。这方面的问题可以以各种形式表现出来。在大多数情况下,我们看到内存使用率稳步上升,直到应用程序不能分配更多的资源和必然崩溃。在Java中这往往导致一个OutOfMemoryException异常被抛出。在某些的情况下,泄漏的类甚至可以坚持足够长的时间来接收注册的回调,导致一些非常奇怪的错误,并往往抛出了臭名昭著的IllegalStateException异常
所以接下来用as工具检测一些常见的内存泄露:

4.1 系统服务注册

public class LeaksActivity extends Activity implements LocationListener {

    private LocationManager locationManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leaks);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, this);
    }

    // Listener implementation omitted
}

我们让了Android 的LocationManager通知我们位置更新。所有我们需要设置这是系统服务本身和一个回调来接收更新。在这里,我们实现了服务本身位置的接口,这意味着LocationManager将开始使用。现在,如果该装置被旋转,新的活动将被创建取代已经注册为位置更新旧的。由于系统服务肯定比其他生命周期长,使它不可能垃圾收集回收仍依赖于特定活动的资源,从而导致内存泄漏。然后该装置的反复旋转将引起非可回收活动填满存储器,最终导致一个OutOfMemoryException异常。但为了解决内存泄漏,我们首先必须要能够找到它。
接下来用android studio 的内存监控,实时监控内存使用与分配,从使用内存异常中找到内存溢出的问题:

而Android监视器Android Studio中2.1.png

任何资源配置的交互将在这里体现出来,使之可以进行跟踪应用程序的资源使用情况。接下来我们对上面的案例进行分析,首先开启我们的应用程序,然后旋转设备后,马上执行Dump Java Heap ,就回生成一份 hprof 文件。

这么复杂的内存堆栈。淡定,我们首先找到自己刚刚执行的类,然后点击查看Analyzer Tasks,可以看到出来一个界面把Detect Leaked Activities(检查泄露内存)勾选上,接下来对Analysis Results里面的数据进行分析

分析内存结构.png

我们选中内存泄漏的activity,查看Reference Tree,可以清楚的看到一个服务的回调,就是之前地位系统服务的回调接口。知道了内存泄漏的地方,我们就可以把他解决掉

public class LeaksActivity extends Activity implements LocationListener {

    private LocationManager locationManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leaks);
        locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER,
                TimeUnit.MINUTES.toMillis(5), 100, this);
    }

    // Listener implementation omitted

     @Override
      protected void onDestroy() {
            locationManager.removeUpdates(this);
            super.onDestroy();
      }
}

再进行旋转设备就不会内存泄漏。

4.2 内部类

java中内部类使用很广泛,然而有时候我们往往忽略内部类的生命周期,导致内存泄漏。接下来我们分析常见的Andr​​oid activity:


public class AsyncActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        new BackgroundTask().execute();
    }

    private class BackgroundTask extends AsyncTask<Void, Void, String> {

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            textView.setText(result);
        }
    }
}

以上这种情况非多。问题来了,AsyncActivity中创建了一个匿名内部类BackgroundTask,同时开启一个线程正在执行任务并且持有内存资源,如果在HTTP请求的情况下,这可能需要很长的时间,尤其在网速较慢的情况。更加容易内存泄漏。
接下来我们进行同样的内存分析:

BackgroundTask内存分析.png

果然内存泄漏的罪魁祸首是BackgroundTask,是不是不刺激,我们再对代码进行修改,执行后再次分析。

public class AsyncActivity extends Activity {

    TextView textView;
    AsyncTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        task = new BackgroundTask(textView).execute();
    }
    
    @Override
    protected void onDestroy() {
        task.cancel(true);
        super.onDestroy();
    }

    private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final TextView resultTextView;

        public BackgroundTask(TextView resultTextView) {
            this.resultTextView = resultTextView;
        }
        
        @Override
        protected void onCancelled() {
            // Cancel task. Code omitted.
        }

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            resultTextView.setText(result);
        }
    }
}

现在,内部类隐式引用已经解决,我们通过内部类传入一个控件,再进入同样的操作,看是否出现内存泄漏

BackgroundTask构造内存分析.png

呵呵,有出现一个新的内存泄漏,接下来我们冷静再次分析一次,发现BackgroundTask中的resultTextView还有 Textview的强引用,那么该怎么解决这个问题了,最简单就是给resultTextView加上WeakReference,以为当把最好一个强引用回收后,垃圾回收器就回考虑弱引用下的回收。写个例子大家就明白了:

public class AsyncActivity extends Activity {

    TextView textView;
    AsyncTask task;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async);
        textView = (TextView) findViewById(R.id.textView);

        task = new BackgroundTask(textView).execute();
    }

    @Override
    protected void onDestroy() {
        task.cancel(true);
        super.onDestroy();
    }
  private static class BackgroundTask extends AsyncTask<Void, Void, String> {

        private final WeakReference<TextView> textViewReference;

        public BackgroundTask(TextView resultTextView) {
            this.textViewReference = new WeakReference<>(resultTextView);
        }
        
        @Override
        protected void onCancelled() {
            // Cancel task. Code omitted.
        }

        @Override
        protected String doInBackground(Void... params) {
            // Do background work. Code omitted.
            return "some string";
        }

        @Override
        protected void onPostExecute(String result) {
            TextView view = textViewReference.get();
            if (view != null) {
                view.setText(result);
            }
        }
    }    
}

注意,在onPostExecute我们要检查控件是否为空验证,以防报空。
在运行分析任务activity就不再被泄漏!

4.3 匿名类

说白了就是匿名内部类,我们接下来对最近很火的Retrofit的网络调用进行分析,不废话,直接搞:

public class ListenerActivity extends Activity {

    TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listener);
        textView = (TextView) findViewById(R.id.textView);

        GitHubService service = ((LeaksApplication) getApplication()).getService();
        service.listRepos("google")
                .enqueue(new Callback<List<Repo>>() {
                    @Override
                    public void onResponse(Call<List<Repo>> call,
                                           Response<List<Repo>> response) {
                        int numberOfRepos = response.body().size();
                        textView.setText(String.valueOf(numberOfRepos));
                    }

                    @Override
                    public void onFailure(Call<List<Repo>> call, Throwable t) {
                        // Code omitted.
                    }
                });
    }
}

其实和内部类分析的结果很相似,注意的是该activity的内存一直会在网络请求完毕才消失。(以后写代码一点要注意这一点)


Retrofit网络调用.png

处理结果和内部类一样,大家看看代码就知道了:

public class ListenerActivity extends Activity {

    TextView textView;
    Call call;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_listener);
        textView = (TextView) findViewById(R.id.textView);

        GitHubService service = ((LeaksApplication)getApplication()).getService();

        call = service.listRepos("google");

        call.enqueue(new RepoCallback(textView));
    }
}
   @Override
    protected void onDestroy() {
        call.cancel();
        super.onDestroy();
    }

    private static class RepoCallback implements Callback<List<Repo>> {

        private final WeakReference<TextView> resultTextView;

        public RepoCallback(TextView resultTextView) {
            this.resultTextView = new WeakReference<>(resultTextView);
        }

        @Override
        public void onResponse(Call<List<Repo>> call,
                Response<List<Repo>> response) {
            TextView view = resultTextView.get();
            if (view != null) {
                int numberOfRepos = response.body().size();
                view.setText(String.valueOf(numberOfRepos));
            }
        }

        @Override
        public void onFailure(Call<List<Repo>> call, Throwable t) {
            // Code omitted.
        }
    }

总结:
我们要学会使用工具来对自己的代码负责,对app的性能进行提升。一些常见的问题和处理方式已经在上面的例子中说明,谢谢你能读这篇博客,说明你是一个很有责任心的程序员。

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

推荐阅读更多精彩内容

  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    _痞子阅读 1,586评论 0 8
  • Android 内存泄漏总结 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏...
    apkcore阅读 1,193评论 2 7
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    DreamFish阅读 775评论 0 5
  • 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。内存泄漏大家都不陌生了,简单粗俗的讲,...
    宇宙只有巴掌大阅读 2,329评论 0 12
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 170,566评论 25 707