作为开发人员,在咱们的日常开发中,为了构建更好的应用程序,咱们须要思考很多事件以保障利用运行在正规上,其中之一是要确保咱们的应用程序不会解体。利用解体的一个常见起因是内存透露。这方面的问题能够以各种模式体现进去。在大多数状况下,咱们看到内存使用率稳步回升,直到应用程序不能调配更多的资源,并不可避免地解体。在 Java 中这往往导致一个 OutOfMemoryException 异样 被抛出。在某些常见的状况下,泄露的类甚至能够勾留很长时间来接管已注册的回调,这会导致一些十分奇怪的谬误,并往往抛出臭名远扬的IllegalStateException 异样。
为了帮忙别人在代码剖析上缩小破费工夫,我将介绍内存透露的几个例子,论述在 Android Studio 中如何查看它们,当然最重要的是如何将其解决。
申明
在这篇文章中的代码示例的目标是为了促成大家对内存治理有更深的理解,特地是在 java。其通用的体系结构,线程治理和代码示例的 HTTP 申请解决在实在的生产环境并不是现实的,这些示例仅仅为了阐明一个问题:在 Android 中,内存透露是一件要思考的事件。
监听器注册
这真的不应该是个问题,但我常常看到各种注册办法的调用,但他们对应的登记办法却无处可寻。这是透露的潜在起源,因为这些办法明确设计成相互对消。如果没有调用登记办法,被援用的对象曾经被终止后,监听实例可能会持有该对象很长的工夫,从而导致透露内存。在 Android 中,如果该对象是一个 Activity 对象,是特地麻烦的,因为他们往往领有大量的数据。让我通知你,可能是什么样子。
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告诉咱们地位更新。咱们所须要做的就是获取零碎服务自身和设置一个回调来接管更新。在这里,咱们在 Activity 中实现了地位监听接口,这意味着 LocationManager 将持有该 Activity 的援用。当初,如果该设施被旋转,新的 Activity 将被创立并取代曾经注册地位更新接口的旧的 Activity。因为零碎服务存活工夫必定比任何 Activity 都要长,LocationManager依然持有以前的 Activity 的援用,这使 GC 不可能回收依赖于以前的 Activity 的资源,从而导致内存透露。如果重复旋转设施,将导致大量的不可回收的 Activity 填满内存,最终导致OutOfMemoryException 异样。
但为了解决内存透露,咱们首先必须要可能找到它。侥幸的是,Android Studio有一个叫做 Android Monitor的内置工具,咱们能够用它来 察看除利用内存应用状况。咱们须要做的仅仅是关上 Android Monitor 并转到对应 tab,看看应用了多少内存和内存实时分配情况。
1-3R36AJUjtdBArkIxPuED3g.png
任何导致资源分配的交互都在这里反映进去,使之成为跟踪应用程序的资源应用状况的现实场合。为了找到内存泄露,当咱们狐疑在某个工夫点内存被泄露时,咱们须要晓得在该工夫点蕴含了那些内存。对于这个非凡的例子,咱们所要做的就是启动咱们的应用程序,而后旋转设施一次,而后调用 Dump Java Heap 操作(在 Memory 的旁边,从右边数起第三个图标)。这将生成一个 HPROF 文件,其中蕴含咱们调用该操作时的一个内存快照。几秒钟后,Android Studio 会主动关上该文件,给咱们更易于剖析内存的直观示意。
我不会去深刻无关如何剖析微小的内存堆。相同,我会把你的注意力疏导到 Analyzer Tasks(上面截图中的右上角)。为了检测下面的例子中引入的内存透露,你所须要做的检测是查看泄露的 Activity(Detect Leaked Activities),点击播放按钮而后在 Analysis Results 上面就会显示泄露的 Activity 状况。
Paste\_Image.png
如果咱们选中泄露的 Activity,能够失去一个援用树,该援用树能够检测持有该 Activity 的援用。通过寻找深度为零的实例,咱们发现地位管理器中的实例 mListener,是咱们的 Activity 不能被 GC 回收的起因。回到咱们的代码,咱们能够看到,这个援用是因为咱们在requestLocationsUpdates 办法中设置 Activity 作为地位更新回调导致的。通过浏览地位管理器文档,问题很快变得清晰,为了勾销回调设置,咱们简略地调用 removeUpdates 办法就行了。在咱们的例子,因为咱们注册更新是在 onCreate 办法,显然要登记的中央在 onDestroy 办法。
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);
}
@Override
protected void onDestroy() {locationManager.removeUpdates(this);
super.onDestroy();}
// Listener implementation omitted
}
从新构建程序并执行与上述雷同的内存剖析,无论旋转多少次设施,应该都不会导致 Activity 透露。
外部类
外部类在 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);
}
}
}
这种非凡的实现在执行上没有问题。问题是,它保留内存的工夫必定会超过必要的工夫。因为 BackgroundTask 持有一个 AsyncActivity 隐式援用并运行在另一个没有勾销策略的线程上,它将保留 AsyncActivity 在内存中的所有资源连贯,直到后盾线程终止运行。在 HTTP 申请的状况下,这可能须要很长的工夫,尤其是在速度较慢的连贯。
通过执行雷同的步骤,如同后面的示例,并确保长时间运行的后台任务,咱们最终会失去上面的剖析后果。
Paste\_Image.png
从下面的剖析中能够看出,BackgroundTask 的确是这种内存透露的罪魁祸首。咱们第一要务是应用动态类的实现形式来打消指向 Activity 的援用,但这样咱们也不能间接拜访 textView 了。因而咱们还须要增加一个构造函数,把 textView 作为参数传递进来。最初,咱们须要引入 AsyncTask 文档中所述的勾销策略。思考到所有这所有,让咱们看看咱们的代码最终出现。
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);
}
}
}
当初,隐式援用已被打消,咱们通过构造函数传递相干实例,并在适合的中央勾销工作。让咱们再运行剖析工作,看看这种扭转是否打消了内存透露。
Paste\_Image.png
看来咱们还有一些工作要做。依据前一个例子的教训,咱们能够晓得在援用树中高亮标注的实例导致了 Activity 泄露。那么这是什么回事?咱们看一下它的父节点就能够发现 resultTextView 持有一个 mContext 援用,毫无疑问,它就是泄露的 Activity 的援用。那么如何解决这个问题?咱们无奈打消 resultTextView 绑定的 context 援用,因为咱们须要在 BackgroundTask 中应用 resultTextView 的援用,以便更新用户界面。为了解决这个问题,一种简略的办法是应用 WeakReference。咱们持有的resultTextView 援用是强援用,具备避免 GC 回收的能力。相同,WeakReference不保障其援用的实例存活。当一个实例最初一个强援用被删除,GC 会把其资源回收,而不论这个实例是否有弱援用。上面是应用 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 不再被泄露!
匿名类
这种类型的类和外部类有同样的毛病,即他们持有外部类的援用。如同外部类,一个匿名类在 Activity 生命周期之外执行或在其余线程执行工作时,可能会导致内存透露。在这个例子中,我将应用风行的 HTTP 申请库 Retrofit 执行 API 调用,并传递响应给对应回调。依据 Retrofit homepage 下面例子对 Retrofit 进行配置。我会在 Application 中持有 GitHubService 援用,这不是一个特地好的设计,这仅仅服务于这个例子的目标。
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 会始终被持有,就像在内部类的例子。
Paste\_Image.png
依据在内部类的例子中同样的推理,咱们得出一个论断:匿名回调类是内存透露的起因。然而,正如外部类的例子,此代码蕴含两个问题。首先,申请没有勾销策略。其次,须要打消对 Activity 的隐式援用。显著的解决办法:咱们在内部类的例子做了同样的事件。
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.}
}
}
根据上述解决方案,运行剖析工作,将不会再有 Activity 的泄露。
论断
后台任务独立于 Activity 的生命周期运行是一件麻烦事。再加上须要协调用户界面和各种后台任务之间的数据流,如果你不小心,那将是一个劫难。所以要晓得你在做什么,以及你的代码是否对性能有影响。这些基本准则是解决 Activity 的良好开端:
- 尽量应用动态外部类。每个非动态外部类将持有一个外部类的隐式援用,这可能会导致不必要的问题。应用动态外部类代替非动态外部类,并通过弱援用存储一些必要的生命周期援用。
- 思考后盾服务等伎俩, Android 提供了多种在非主线程工作的办法,如 HandlerThread,IntentService 和AsyncTask,它们每个都有本人的优缺点。另外,Android 提供了一些机制来传递信息给主线程以更新 UI。譬如,播送接收器 就能够很不便实现这一点。
- 不要一味依赖垃圾回收器。应用具备垃圾回收性能的语言编码很容易有这样的想法:即没必要思考内存治理。咱们的示例分明地表明,并非如此。因而,请确保你调配的资源都被预期回收。
相干视频:Android 开发中高级进阶我的项目实战:MVP 架构解析以及电商我的项目实战
本文转自 https://www.jianshu.com/p/082766511405,如有侵权,请分割删除。