由于项目中大量出现内存泄漏导致内存使用量增多而不能立马释放,不得不研究内存泄漏,接下来我们切入主题。
以下都是本人收集和总结的内容:
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 的内存监控,实时监控内存使用与分配,从使用内存异常中找到内存溢出的问题:
任何资源配置的交互将在这里体现出来,使之可以进行跟踪应用程序的资源使用情况。接下来我们对上面的案例进行分析,首先开启我们的应用程序,然后旋转设备后,马上执行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中内部类使用很广泛,然而有时候我们往往忽略内部类的生命周期,导致内存泄漏。接下来我们分析常见的Android 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,是不是不刺激,我们再对代码进行修改,执行后再次分析。
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的性能进行提升。一些常见的问题和处理方式已经在上面的例子中说明,谢谢你能读这篇博客,说明你是一个很有责任心的程序员。
网友评论