我的应用运行正常,直到我在安装后的第一次启动中断初始化过程,只要初始化过程尚未完成,就退出并启动应用程序几次。处理逻辑和AsyncTask可以很好地处理这个问题,所以我没有遇到任何不一致,但我的堆有问题。当我在应用程序设置中执行此令人不安的退出和启动时,它会越来越多,这将导致OutOfMemory错误。我已经通过MAT分析堆已经发现了泄漏,但我仍然有另一个泄漏,我无法隔离 有关背景信息:我将应用程序上下文,列表和时间戳存储在静态类中,以便能够从应用程序中的任何位置访问它,而无需使用构造函数的繁琐的传递引用。 无论如何,这个静态类(ApplicationContext)肯定有问题,因为它会因区域列表而导致内存泄漏。区域对象处理GeoJSON数据。这就是这个类的样子:
public class ApplicationContext extends Application {
private static Context context;
private static String timestamp;
private static List<Zone> zones = new ArrayList<Zone>();
public void onCreate() {
super.onCreate();
ApplicationContext.context = getApplicationContext();
}
public static Context getAppContext() {
return ApplicationContext.context;
}
public static List<Zone> getZones() {
return zones;
}
public static void setData(String timestamp, List<Zone> zones) {
ApplicationContext.timestamp = timestamp;
ApplicationContext.zones = zones;
}
public static String getTimestamp() {
return timestamp;
}
}
我已经尝试像这样存储区域
ApplicationContext.zones = new ArrayList(zones);
但它没有效果。我已经尝试将zones属性放入另一个静态类,因为ApplicationContext在所有其他类之前加载(由于AndroidManifest中的条目),这可能导致这种行为,但这也不是问题。
setData在我的“ProcessController”中被调用两次。一旦进入doUpdateFromStorage,就进入doUpdateFromUrl(String)。这个类看起来像这样:
public final class ProcessController {
private HttpClient httpClient = new HttpClient();
public final InitializationResult initializeData() {
String urlTimestamp;
try {
urlTimestamp = getTimestampDataFromUrl();
if (isModelEmpty()) {
if (storageFilesExist()) {
try {
String localTimestamp = getLocalTimestamp();
if (isStorageDataUpToDate(localTimestamp, urlTimestamp)) {
return doDataUpdateFromStorage();
}
else {
return doDataUpdateFromUrl(urlTimestamp);
}
}
catch (IOException e) {
return new InitializationResult(false, Errors.cannotReadTimestampFile());
}
}
else {
try {
createNewFiles();
return doDataUpdateFromUrl(urlTimestamp);
}
catch (IOException e) {
return new InitializationResult(false, Errors.fileCreationFailed());
}
}
}
else {
if (isApplicationContextDataUpToDate(urlTimestamp)) {
return new InitializationResult(true, "");
}
else {
return doDataUpdateFromUrl(urlTimestamp);
}
}
}
catch (IOException e1) {
return new InitializationResult(false, Errors.noTimestampConnection());
}
}
private String getTimestampDataFromUrl() throws IOException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
return httpClient.getDataFromUrl(FileType.TIMESTAMP);
}
private String getJsonDataFromUrl() throws IOException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
return httpClient.getDataFromUrl(FileType.JSONDATA);
}
private String getLocalTimestamp() throws IOException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
return PersistenceManager.getFileData(FileType.TIMESTAMP);
}
private List<Zone> getLocalJsonData() throws IOException, ParseException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
}
private InitializationResult doDataUpdateFromStorage() throws InterruptedIOException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
try {
ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());
return new InitializationResult(true, "");
}
catch (IOException e) {
return new InitializationResult(false, Errors.cannotReadJsonFile());
}
catch (ParseException e) {
return new InitializationResult(false, Errors.parseError());
}
}
private InitializationResult doDataUpdateFromUrl(String urlTimestamp) throws InterruptedIOException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
String jsonData;
List<Zone> zones;
try {
jsonData = getJsonDataFromUrl();
zones = JsonStringParser.parse(jsonData);
try {
PersistenceManager.persist(jsonData, FileType.JSONDATA);
PersistenceManager.persist(urlTimestamp, FileType.TIMESTAMP);
ApplicationContext.setData(urlTimestamp, zones);
return new InitializationResult(true, "");
}
catch (IOException e) {
return new InitializationResult(false, Errors.filePersistError());
}
}
catch (IOException e) {
return new InitializationResult(false, Errors.noJsonConnection());
}
catch (ParseException e) {
return new InitializationResult(false, Errors.parseError());
}
}
private boolean isModelEmpty() {
if (ApplicationContext.getZones() == null || ApplicationContext.getZones().isEmpty()) {
return true;
}
return false;
}
private boolean isApplicationContextDataUpToDate(String urlTimestamp) {
if (ApplicationContext.getTimestamp() == null) {
return false;
}
String localTimestamp = ApplicationContext.getTimestamp();
if (!localTimestamp.equals(urlTimestamp)) {
return false;
}
return true;
}
private boolean isStorageDataUpToDate(String localTimestamp, String urlTimestamp) {
if (localTimestamp.equals(urlTimestamp)) {
return true;
}
return false;
}
private boolean storageFilesExist() {
return PersistenceManager.filesExist();
}
private void createNewFiles() throws IOException {
PersistenceManager.createNewFiles();
}
}
也许这是另一个有用的信息,这个ProcessController是由应用程序设置中的MainActivity的AsyncTask调用的:
public class InitializationTask extends AsyncTask<Void, Void, InitializationResult> {
private ProcessController processController = new ProcessController();
private ProgressDialog progressDialog;
private MainActivity mainActivity;
private final String TAG = this.getClass().getSimpleName();
public InitializationTask(MainActivity mainActivity) {
this.mainActivity = mainActivity;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
ProcessNotification.setCancelled(false);
progressDialog = new ProgressDialog(mainActivity);
progressDialog.setMessage("Processing.\nPlease wait...");
progressDialog.setIndeterminate(true); //means that the "loading amount" is not measured.
progressDialog.setCancelable(true);
progressDialog.show();
};
@Override
protected InitializationResult doInBackground(Void... params) {
return processController.initializeData();
}
@Override
protected void onPostExecute(InitializationResult result) {
super.onPostExecute(result);
progressDialog.dismiss();
if (result.isValid()) {
mainActivity.finalizeSetup();
}
else {
AlertDialog.Builder dialog = new AlertDialog.Builder(mainActivity);
dialog.setTitle("Error on initialization");
dialog.setMessage(result.getReason());
dialog.setPositiveButton("Ok",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
mainActivity.finish();
}
});
dialog.show();
}
processController = null;
}
@Override
protected void onCancelled() {
super.onCancelled();
Log.i(TAG, "onCancelled executed");
Log.i(TAG, "set CancelNotification status to cancelled.");
ProcessNotification.setCancelled(true);
progressDialog.dismiss();
try {
Log.i(TAG, "clearing files");
PersistenceManager.clearFiles();
Log.i(TAG, "files cleared");
}
catch (IOException e) {
Log.e(TAG, "not able to clear files.");
}
processController = null;
mainActivity.finish();
}
}
这是JSONParser的正文。 (更新:我将方法设置为静态,但问题仍然存在。)我省略了JSON对象中的对象创建,因为我不认为这是错误:
public class JsonStringParser {
private static String TAG = JsonStringParser.class.getSimpleName();
public static synchronized List<Zone> parse(String jsonString) throws ParseException, InterruptedIOException {
JSONParser jsonParser = new JSONParser();
Log.i(TAG, "start parsing JSON String with length " + ((jsonString != null) ? jsonString.length() : "null"));
List<Zone> zones = new ArrayList<Zone>();
//does a lot of JSON parsing here
Log.i(TAG, "finished parsing JSON String");
jsonParser = null;
return zones;
}
}
这是显示问题的堆转储:
这是详细列表,显示此问题与arraylist有关。
任何想法在这里有什么问题?顺便说一句:由于没有详细信息,我不知道其他泄漏是什么。
可能很重要:此图显示了我一次又一次不启动和停止应用程序时的状态。这是一个干净的开始图。但是,当我多次开始和停止时,由于空间不足,可能会导致问题。
这是一个真正的崩溃图。我在初始化时多次启动并停止了应用程序:
[UPDATE]
我没有将Android上下文存储到我的ApplicationContext类中并使PersistenceManager非静态,从而缩小了一点。这个问题没有改变,所以我完全相信它与全局存储Android上下文的事实无关。它仍然是上图中的“问题可疑1”。所以我必须对这个庞大的列表做些什么,但是什么呢?我已经尝试将其序列化,但是取消分配此列表需要的时间比20秒长,所以这不是一个选项。
现在我尝试了不同的东西。我踢出了整个ApplicationContext,所以我不再有任何静态引用了。我试图在MainActivity中保存Zone对象的ArrayList。虽然我至少重构了运行应用程序所需的部分,所以我甚至没有将Array或Activity传递给我需要它的所有类,我仍然以不同的方式遇到同样的问题,所以我的猜测是区域对象本身就是问题所在。或者我无法正确读取堆转储。请参阅下面的新图表。这是一个简单的应用程序启动而没有干扰的结果。
[UPDATE]
我得出的结论是没有内存泄漏,因为“内存在一个实例中累积”听起来不像是泄漏。问题是一次又一次地启动和停止会启动新的AsyncTasks,如图所示,因此解决方案是不启动新的AsyncTask。我在SO上找到了一个可能的解决方案,但它对我来说还不行。
答案 0 :(得分:3)
首先,我必须同意埃米尔的意见:
“构造函数传递引文”是有助于避免的 像这样的问题。老实说,以这种方式使用静态肯定是一个 这样创建内存泄漏的方法,特别是静态的 参考你的背景。
这也适用于代码中的所有其他static
方法。 static
方法与全局函数没有什么不同。你正在那里建造一个充满static
方法的大意大利面板。特别是当他们开始共享一些状态时,它迟早会崩溃或创建一些模糊的结果,而这些结果是你无法通过适当的设计得到的,特别是在存在高度多线程平台的情况下,如Android。
我还注意到的是,请注意onCancelled
的{{1}}方法在AsyncTask
完成之前不会被调用。因此,您的全局取消标记(doInBackground
)或多或少没有价值(如果仅在显示的代码段落中使用)。
同样,根据您发布的记忆图像,ProcessNotification.isCancelled()
列表中仅包含“31”项。该举多少钱?它增加了多少?如果它实际上增加了,那么结果可能是zones
方法,这又是JsonStringParser.parse
。如果它在某些缓存中保存了一个项目列表,并且控制逻辑无法正常工作(例如,在存在多个线程同时访问它的情况下),则每次调用它时都可能会向该缓存添加项目。
static
,因此在关闭应用程序时不会(必要)清除此数据。 static
被初始化一次,并且出于本案例的目的,在(物理vm-)过程停止之前,永远不会去初始化。但是,即使应用程序已停止(see for example a wonderful explanation here),Android也不保证该进程被终止。因此,您可能会在(可能是解析)代码的某些static
部分中累积一些数据。 static
变量仍然没有值。全局函数zones
可能不是线程安全的,并且会将多个数据多次放入最终返回的列表中,从而产生越来越大的列表。同样,通常不使用parse
方法(并注意多线程)可以避免这种情况。 (代码不完整,因此猜测,甚至可能还有其他东西潜伏在那里。)
答案 1 :(得分:3)
在AsyncTask中,您拥有Context:MainActivity的引用。当你启动几个AsyncTask时,它们将由ExecutorService排队。因此,所有AsyncTask,如果它们长时间运行,将是“活着的”(不是垃圾收集)。他们每个人都会在活动中保留一个参考。因此,你所有的活动都将保持活力。
这是一个真正的内存泄漏,因为Android会想要垃圾收集不再显示的Activity。你的AsyncTasks会阻止它。所有活动都保存在记忆中。
我建议您尝试RoboSpice Motivations了解有关此问题的更多信息。在这个应用程序中,我们解释了为什么不应该使用AsyncTasks进行长时间运行操作。还有一些解决方法可以让你使用它们,但它们很难实现。
解决此问题的一种方法是使用WeakReference指向AsyncTask类中的活动。如果您仔细使用它们,则可以避免您的活动不被垃圾收集。
实际上,RoboSpice是一个允许在服务中执行网络请求的库。这种方法非常有趣,它会创建一个与您的活动无关的上下文(服务)。因此,您的请求可以根据需要进行,并且不会干扰Android的垃圾回收行为。
您可以使用RoboSpice的两个模块来处理REST请求。一个用于Spring Android,另一个用于Google Http Java Client。这两个库都可以简化JSON解析。
答案 2 :(得分:2)
我假设您修复了对MainActivity的引用,但我想提一下另一个问题......
您声明解析需要20秒。如果你“中断”应用程序,这个处理不会消失。
根据你在这里显示的代码,似乎99%的20秒花在JsonStringParser.parse()中。
如果我看一下你的评论“在这里做了很多JSON解析”,我假设你的应用程序调用了JSONParser.something(),它会在20秒内停留。即使JsonStringParser是静态的,每次调用JsonStringParser.parse()都会创建一个JSONParser()的新副本,我的猜测是使用大量内存。
一个需要20秒的后台进程是一项非常重要的任务,而且在我看到的JSON解析器中,很多对象都会被创建和销毁,并且会消耗大量的周期。
所以我认为你的根本原因是你启动了JSONParser.something()的第二个(或第三个或第四个)副本,因为它们中的每个都将独立执行并尝试分配许多内存块,并保持运行超过20秒,因为他们将不得不共享CPU周期。多个JSONParser对象的组合内存分配会杀死您的系统。
总结:
答案 3 :(得分:1)
我觉得这可能是怎么可能的,但是我的眼睛看上去却眼前一亮。
检查您是否未从本地存储中加载数据,向其中添加更多数据,然后将其保存回本地磁盘。
以下方法与程序的其他部分结合使用。
如果调用了以下内容,然后由于某种原因调用了getDatafromURL,那么我相信你会不断增加数据集。
这至少是我的出发点。加载,追加和保存。
ApplicationContext.setData(getLocalTimestamp(), getLocalJsonData());
private List<Zone> getLocalJsonData() throws IOException, ParseException {
if (ProcessNotification.isCancelled()) {
throw new InterruptedIOException();
}
return JsonStringParser.parse(PersistenceManager.getFileData(FileType.JSONDATA));
}
否则我认为问题在于您的解析代码,或者您用于保存数据的静态类之一。
答案 4 :(得分:0)
我的最终解决方案
我现在找到了自己的解决方案。当我多次启动和停止应用程序时,它运行稳定并且不会产生内存泄漏。这个解决方案的另一个优点是我能够踢出所有这些ProcessNotification.isCancelled()
部分。
关键是在ApplicationContext中保存对InitializationTask的引用。使用这种方法,当我开始一个新的MainActivity时,我可以在新的MainActivity中恢复正在运行的AsyncTask。这意味着我从不启动多个AsyncTask,但我将每个新的MainActivity实例附加到当前正在运行的任务。旧活动将被分离。这看起来像这样:
ApplicationContext中的新方法:
public static void register(InitializationTask initializationTask) {
ApplicationContext.initializationTask = initializationTask;
}
public static void unregisterInitializationTask() {
initializationTask = null;
}
public static InitializationTask getInitializationTask() {
return initializationTask;
}
<强> MainActivity
(我必须将progressDialog放在这里,否则如果我停止并开始新的活动则不会显示):
@Override
protected void onStart() {
super.onStart();
progressDialog = new ProgressDialog(this);
progressDialog.setMessage("Processing.\nPlease wait...");
progressDialog.setIndeterminate(true); // means that the "loading amount" is not measured.
progressDialog.setCancelable(true);
progressDialog.show();
if (ApplicationContext.getInitializationTask() == null) {
initializationTask = new InitializationTask();
initializationTask.attach(this);
ApplicationContext.register(initializationTask);
initializationTask.execute((Void[]) null);
}
else {
initializationTask = ApplicationContext.getInitializationTask();
initializationTask.attach(this);
}
}
MainActivity的“onPause”包含initializationTask.detach();
和progressDialog.dismiss();
。 finalizeSetup();
也驳回了对话。
InitializationTask包含两个方法:
public void attach(MainActivity mainActivity) {
this.mainActivity = mainActivity;
}
public void detach() {
mainActivity = null;
}
该任务的 onPostExecute 会调用ApplicationContext.unregisterInitializationTask();
。