12

初期化プロセスがまだ終了していない限り、アプリを数回終了して起動することにより、インストール後の最初の開始時に初期化プロセスを中断するまで、アプリは正常に実行されます。処理ロジックと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で可能な解決策を見つけましたが、まだうまくいきません。

メモリエラー4 メモリエラー5

4

5 に答える 5

3

まず第一に、私はエミールに同意する必要があります:

「コンストラクターによる面倒な参照の受け渡し」は、このような問題を回避するのに役立ちます。正直なところ、このように静的を使用することは、特にコンテキストへの静的参照を使用して、このようなメモリリークを作成する1つの方法です。

これは、コード内の他のすべてのstaticメソッドにも当てはまります。staticメソッドは、グローバル関数と実際には違いはありません。staticあなたはそこで方法でいっぱいの大きなスパゲッティプレートを作っています。特に、ある状態を共有し始めると、遅かれ早かれクラッシュしたり、適切なデザインでは得られない不明瞭な結果が作成されたりします。特に、Androidのように非常にマルチ読み取り可能なプラットフォームが存在する場合はそうです。

また、私の目を引いたのは、終了する前にのonCancelledメソッドAsyncTaskが呼び出されないことに注意してくださいdoInBackground。したがって、グローバルキャンセルフラグ(ProcessNotification.isCancelled())は、多かれ少なかれ価値がありません(表示されているコードパッセージでのみ使用されている場合)。

また、あなたが投稿した思い出の画像から、zonesリストには「たった」31個のアイテムが含まれています。それはいくら保持することになっていますか?それはどのくらい増加しますか?それが実際に増加する場合、カルプリントはJsonStringParser.parseメソッドにある可能性がありますstatic。あるキャッシュにアイテムのリストを保持していて、制御ロジックが正しく機能していない場合(たとえば、同時にアクセスする複数のスレッドが存在する場合)、呼び出されるたびにそのキャッシュにアイテムが追加される可能性があります。

  • 推測1:解析方法がstaticであるため、アプリケーションのシャットダウン時にこのデータは(必然的に)クリーンアップされません。staticsは一度初期化されますが、この場合、(物理vm-)プロセスが停止するまで初期化が解除されることはありません。ただし、Androidは、アプリケーションが停止した場合でも、プロセスが強制終了されることを保証しません(たとえば、ここですばらしい説明を参照してください)。したがってstatic、(おそらく解析している)コードの一部にデータを蓄積する可能性があります。
  • 推測2:アプリケーションを数回再起動しているため、バックグラウンドスレッドが複数回並行して実行されています(仮定:アプリケーションを再起動するたびに、新しいスレッドが生成されます。コードにはこれに対するガードがないことに注意してください)。これは最初の解析がまだ実行されており、グローバルzones変数がまだ値を保持していないため、別の解析が開始されます。グローバル関数parseはスレッドセーフではない可能性があり、複数のデータをリストに複数回入れて最終的に返されるため、リストはどんどん大きくなります。繰り返しますが、これは一般的にメソッドを持たないことで回避されstaticます(そしてマルチスレッドに注意してください)。

(コードは完全ではないので、推測します。そこには他のものが潜んでいる可能性さえあります。)

于 2012-12-11T19:05:44.497 に答える
3

AsyncTask内で、Context:MainActivityの参照を所有しています。複数のAsyncTaskを開始すると、ExecutorServiceによってキューに入れられます。したがって、すべてのAsyncTaskは、長時間実行されている場合、「アライブ」になります(ガベージコレクションではありません)。そして、それらのそれぞれは、アクティビティに関する参照を保持します。その結果、あなたのすべての活動も生き続けることになります。

Androidは、表示されなくなったアクティビティをガベージコレクションするため、これは実際のメモリリークです。そしてあなたのAsyncTasksはそれを防ぎます。すべてのアクティビティはメモリに保持されます。

この問題についてさらに学ぶために、RoboSpiceMotivationsを試してみることをお勧めします。このアプリでは、長時間実行する操作にAsyncTasksを使用しない理由を説明します。それらを使用できるようにする回避策はまだいくつかありますが、実装するのは困難です。

この問題を取り除く1つの方法は、WeakReferenceを使用してAsyncTaskクラス内のアクティビティを指すことです。それらを注意深く使用すれば、ガベージコレクションされないようにアクティビティを回避できます。

実際、RoboSpiceは、サービス内でネットワーク要求を実行できるようにするライブラリです。このアプローチは非常に興味深いものであり、アクティビティにリンクされていないコンテキスト(サービス)を作成します。したがって、リクエストには必要なだけ時間がかかる可能性があり、Androidのガベージコレクションの動作を妨げることはありません。

RESTリクエストを処理するために使用できるRoboSpiceの2つのモジュールがあります。1つはSpringAndroid用で、もう1つはGoogle HttpJavaClient用です。どちらのライブラリもJSONの解析を容易にします。

于 2012-12-15T07:59:04.470 に答える
2

MainActivityへの参照を修正したと思いますが、別の問題について言及したいと思います...

解析には20秒かかるとのことです。また、アプリを「中断」しても、この処理は終了しません。

ここに示すコードから、その20秒の99%がJsonStringParser.parse()内で費やされているようです。

「ここで多くのJSON解析を実行します」というコメントを見ると、アプリがJSONParser.something()を呼び出し、20秒間離れていると思います。JsonStringParserは静的ですが、JsonStringParser.parse()を呼び出すたびに、JSONParser()の新しいコピーが作成され、多くのメモリを使用していると思います。

20秒かかるバックグラウンドプロセスは非常に大きなタスクです。JSONパーサーで見たように、今回は多くのオブジェクトが作成および破棄され、多くのサイクルが消費されます。

したがって、ここでの根本的な原因は、JSONParser.something()の2番目(または3番目または4番目)のコピーを開始することだと思います。それぞれが独立して実行され、メモリの多くのチャンクを割り当てようとし、20秒より長く実行され続けるためです。 CPUサイクルを共有する必要があるためです。複数のJSONParserオブジェクトのメモリ割り当てを組み合わせることで、システムが停止します。

要約する:

  • 最初のJsonStringParser.parse()が強制終了または完了するまで、別のJsonStringParser.parse()を開始しないでください。
  • つまり、アプリを「中断」したときにJsonStringParser.parse()を停止する方法を見つけるか、アプリを再起動したときに実行中のコピーを再利用する必要があります。
于 2012-12-16T06:05:56.667 に答える
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));
}

それ以外の場合、問題は解析コードか、データの保存に使用している静的クラスの1つにあると思います。

于 2012-12-12T08:58:49.433 に答える
0

私の最終的な解決策

私は今、自分で解決策を見つけました。アプリケーションを何度も起動および停止しても、安定して動作し、メモリリークは発生しません。このソリューションのもう1つの利点は、このすべての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には、さらに2つのメソッドが含まれています。

public void attach(MainActivity mainActivity) {
    this.mainActivity = mainActivity;
}

public void detach() {
    mainActivity = null;
}

タスクのonPostExecuteはを呼び出しますApplicationContext.unregisterInitializationTask();

于 2012-12-23T16:01:32.200 に答える