初期化プロセスがまだ終了していない限り、アプリを数回終了して起動することにより、インストール後の最初の開始時に初期化プロセスを中断するまで、アプリは正常に実行されます。処理ロジックと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);
しかし、効果はありませんでした。ApplicationContextが他のすべてのクラスの前にロードされるため(AndroidManifestのエントリのため)、zones属性を別の静的クラスに入れようとしましたが、これも問題ではありません。
setDataは私の「ProcessController」で2回呼び出されます。doUpdateFromStorageに1回、doUpdateFromUrl(String)に1回。このクラスは次のようになります。
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によって呼び出されることは、もう1つの役立つ情報かもしれません。
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の本体です。(更新:メソッドnone staticを設定しましたが、問題は解決しません。)これがエラーであるとは思わないため、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と関係があることを示す詳細リストです。
ここで何が問題なのですか?ところで:詳細情報がないので、他のリークは何ですか。
重要かもしれません:この図は、アプリケーションを何度も起動および停止しない場合のステータスを示しています。クリーンスタートの図です。しかし、何度か開始および停止すると、スペース不足のために問題が発生する可能性があります。
これが実際のクラッシュの図です。初期化中にアプリを数回起動および停止しました:
[更新]
AndroidコンテキストをApplicationContextクラスに保存せず、PersistenceManagerを非静的にすることで、少し絞り込みました。問題は変わっていないので、Androidコンテキストをグローバルに保存しているという事実とは関係がないと確信しています。上のグラフの「問題容疑者1」のままです。だから私はこの巨大なリストで何かをしなければなりませんが、何ですか?私はすでにそれをシリアル化しようとしましたが、このリストの非シリアル化には20秒よりもはるかに長い時間がかかるため、これはオプションではありません。
今、私は別のことを試みました。ApplicationContext全体をキックアウトしたので、静的参照はもうありません。MainActivityでZoneオブジェクトのArrayListを保持しようとしました。少なくともアプリケーションを実行するために必要な部分をリファクタリングしたので、配列またはアクティビティを必要なすべてのクラスに渡さなかったのですが、それでも同じ問題が別の方法で発生しているので、推測します。ゾーンオブジェクト自体がどういうわけか問題であるということ。または、ヒープダンプを正しく読み取ることができません。以下の新しいグラフを参照してください。これは、干渉のない単純なアプリの起動の結果です。
[更新] 「メモリが1つのインスタンスに蓄積される」というのはリークのように聞こえない
ため、メモリリークは発生しないという結論に達しました。問題は、1つのグラフに示されているように、何度も起動および停止すると新しいAsyncTasksが開始されるため、解決策は新しいAsyncTaskを開始しないことです。SOで可能な解決策を見つけましたが、まだうまくいきません。