アクティビティにアタッチされていないフラグメントMyFragment


393

私の問題を表す小さなテストアプリを作成しました。ActionBarSherlockを使用して(Sherlock)Fragmentsでタブを実装しています。

私のコード: TestActivity.java

public class TestActivity extends SherlockFragmentActivity {
    private ActionBar actionBar;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupTabs(savedInstanceState);
    }

    private void setupTabs(Bundle savedInstanceState) {
        actionBar = getSupportActionBar();
        actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);

        addTab1();
        addTab2();
    }

    private void addTab1() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("1");
        String tabText = "1";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "1", MyFragment.class));

        actionBar.addTab(tab1);
    }

    private void addTab2() {
        Tab tab1 = actionBar.newTab();
        tab1.setTag("2");
        String tabText = "2";
        tab1.setText(tabText);
        tab1.setTabListener(new TabListener<MyFragment>(TestActivity.this, "2", MyFragment.class));

        actionBar.addTab(tab1);
    }
}

TabListener.java

public class TabListener<T extends SherlockFragment> implements com.actionbarsherlock.app.ActionBar.TabListener {
    private final SherlockFragmentActivity mActivity;
    private final String mTag;
    private final Class<T> mClass;

    public TabListener(SherlockFragmentActivity activity, String tag, Class<T> clz) {
        mActivity = activity;
        mTag = tag;
        mClass = clz;
    }

    /* The following are each of the ActionBar.TabListener callbacks */

    public void onTabSelected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        // Check if the fragment is already initialized
        if (preInitializedFragment == null) {
            // If not, instantiate and add it to the activity
            SherlockFragment mFragment = (SherlockFragment) SherlockFragment.instantiate(mActivity, mClass.getName());
            ft.add(android.R.id.content, mFragment, mTag);
        } else {
            ft.attach(preInitializedFragment);
        }
    }

    public void onTabUnselected(Tab tab, FragmentTransaction ft) {
        SherlockFragment preInitializedFragment = (SherlockFragment) mActivity.getSupportFragmentManager().findFragmentByTag(mTag);

        if (preInitializedFragment != null) {
            // Detach the fragment, because another one is being attached
            ft.detach(preInitializedFragment);
        }
    }

    public void onTabReselected(Tab tab, FragmentTransaction ft) {
        // User selected the already selected tab. Usually do nothing.
    }
}

MyFragment.java

public class MyFragment extends SherlockFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        new AsyncTask<Void, Void, Void>() {

            @Override
            protected Void doInBackground(Void... params) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException ex) {
                }
                return null;
            }

            @Override
            protected void onPostExecute(Void result){
                getResources().getString(R.string.app_name);
            }

        }.execute();
    }
}

Thread.sleepデータのダウンロードをシミュレートするためのパーツを追加しました。のコードは、のonPostExecute使用をシミュレートするためのものFragmentです。

画面を横向きと縦向きの間で非常に速く回転させると、onPostExecuteコードで例外が発生します。

java.lang.IllegalStateException:Fragment MyFragment {410f6060}がアクティビティにアタッチされていません

MyFragmentその間に新しいものが作成されて、AsyncTask終了前にアクティビティに添付されていたからだと思います。のコードはonPostExecute、接続されていないを呼び出しますMyFragment

しかし、どうすればこれを修正できますか?


1
フラグメントインフレーターからのビューを使用する必要があります。 mView = inflater.inflate(R.layout.my_layout, container, false) リソースを取得する場合は、このビューを使用してください:mView.getResources().***。このバグを修正するのに役立ちます。
フォクシス2017年

@foxisこれContextは、 `mView`にアタッチされているをリークします。
nhaarman 2017年

まだ確認していません。リークを回避するにmViewは、onDestroyでnullを取得する方法を教えてください。
フォクシス2017年

回答:


774

私は非常に簡単な答えを見つけましたisAdded()::

trueフラグメントが現在そのアクティビティに追加されているかどうかを返します。

@Override
protected void onPostExecute(Void result){
    if(isAdded()){
        getResources().getString(R.string.app_name);
    }
}

避けるためにonPostExecuteしたときに呼び出されるのFragmentに接続されていないことはActivityキャンセルすることでAsyncTask一時停止や停止時Fragment。その後、isAdded()もう必要ありません。ただし、このチェックを維持することをお勧めします。


私の場合、別のアプリケーションIntentを起動すると、同じエラーが発生します...何か提案はありますか?
CoDe 2013年

1
developer.android.com/reference/android/app/… ...またisDetached()、APIレベル13で追加されました
Lucas Jota

5
API <11の場合、developer.android.com / reference / android / support / v4 / app /…を使用ています。
nhaarman 2014年

DialogFragmentを使用したときにこの問題に直面しました。dialogFragmentを閉じた後、別のアクティビティを開始しようとしました。次に、このエラーが発生しました。startActivityの後にdismiss()を呼び出すことで、このエラーを回避しました。問題は、フラグメントがアクティビティからすでに切り離されていることでした。
Ataru 2014年

28

問題は、アクティビティからリソースを取得しようとするgetResources()。getString()を使用してリソース(この場合は文字列)にアクセスしようとしていることです。Fragmentクラスの次のソースコードを参照してください。

 /**
  * Return <code>getActivity().getResources()</code>.
  */
 final public Resources getResources() {
     if (mHost == null) {
         throw new IllegalStateException("Fragment " + this + " not attached to Activity");
     }
     return mHost.getContext().getResources();
 }

mHost アクティビティを保持するオブジェクトです。

アクティビティがアタッチされていない可能性があるため、getResources()呼び出しは例外をスローします。

受け入れられた解決策IMHOは、問題を隠しているだけなので、進むべき道ではありません。正しい方法は、アプリケーションコンテキストのように、常に存在することが保証されている別の場所からリソースを取得することです。

youApplicationObject.getResources().getString(...)

getString()フラグメントが一時停止したときに実行する必要があったため、このソリューションを使用しました。ありがとう
Geekarist 2017年

24

ここで2つの異なるシナリオに直面しました。

1)とにかく非同期タスクを終了させたい場合:onPostExecuteが受信したデータを保存し、リスナーを呼び出してビューを更新すると想定します。より効率的にするには、タスクを終了させて​​、ユーザーが来たときにデータを準備します。バック。この場合、私は通常これを行います:

@Override
protected void onPostExecute(void result) {
    // do whatever you do to save data
    if (this.getView() != null) {
        // update views
    }
}

2)ビューを更新できるときにのみ非同期タスクを終了させたい場合:ここで提案するケースでは、タスクはビューを更新するだけで、データストレージは必要ないため、ビューが終了した場合にタスクを終了する手掛かりがありません。もはや示されていません。私はこれをします:

@Override
protected void onStop() {
    // notice here that I keep a reference to the task being executed as a class member:
    if (this.myTask != null && this.myTask.getStatus() == Status.RUNNING) this.myTask.cancel(true);
    super.onStop();
}

私はこれで問題を発見しませんでしたが、フラグメントの代わりにアクティビティからタスクを起動することを含む(おそらく)より複雑な方法も使用しています。

これが誰かを助けることを望みます!:)


18

コードの問題は、AsyncTaskの使用方法にあります。スリープスレッド中に画面を回転させると、次のようになります。

Thread.sleep(2000) 

AsyncTaskはまだ機能しています。これは、フラグメントが再構築される前(回転時)、および同じAsyncTaskインスタンス(回転後)がonPostExecute()を実行する前に、onDestroy()でAsyncTaskインスタンスを適切にキャンセルしなかったためです。古いフラグメントインスタンス(無効なインスタンス)を含むgetResources()を持つリソース:

getResources().getString(R.string.app_name)

これは次と同等です:

MyFragment.this.getResources().getString(R.string.app_name)

したがって、最終的な解決策は、画面を回転するときにフラグメントが再構築される前にAsyncTaskインスタンスを管理し(これがまだ機能している場合はキャンセルする)、移行中にキャンセルされた場合は、ブールフラグを使用して再構築後にAsyncTaskを再起動します。

public class MyFragment extends SherlockFragment {

    private MyAsyncTask myAsyncTask = null;
    private boolean myAsyncTaskIsRunning = true;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(savedInstanceState!=null) {
            myAsyncTaskIsRunning = savedInstanceState.getBoolean("myAsyncTaskIsRunning");
        }
        if(myAsyncTaskIsRunning) {
            myAsyncTask = new MyAsyncTask();
            myAsyncTask.execute();
        }
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putBoolean("myAsyncTaskIsRunning",myAsyncTaskIsRunning);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if(myAsyncTask!=null) myAsyncTask.cancel(true);
        myAsyncTask = null;

    }

    public class MyAsyncTask extends AsyncTask<Void, Void, Void>() {

        public MyAsyncTask(){}

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            myAsyncTaskIsRunning = true;
        }
        @Override
        protected Void doInBackground(Void... params) {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ex) {}
            return null;
        }

        @Override
        protected void onPostExecute(Void result){
            getResources().getString(R.string.app_name);
            myAsyncTaskIsRunning = false;
            myAsyncTask = null;
        }

    }
}

代わりにhelped getResources().***を使用している場合Fragments.this.getResource().***
Prabs

17

それらは、これとアクティビティからのフラグメントのリークに対する非常に巧妙な解決策です。

したがって、getResourceまたはFragmentからアクセスするアクティビティコンテキストに依存するものの場合は、常に次のようにアクティビティステータスとフラグメントステータスをチェックします。

 Activity activity = getActivity(); 
    if(activity != null && isAdded())

         getResources().getString(R.string.no_internet_error_msg);
//Or any other depends on activity context to be live like dailog


        }
    }

7
isAdded()で十分です:final public boolean isAdded(){return mHost!= null && mAdded; }
NguyenDat

私の場合、このチェックは悪質ではなく、これらを追加したにもかかわらずクラッシュします。
デビッド

@David、isAdded十分です。getString()もしクラッシュしたときの状況を見たことがないisAdded == true。アクティビティが表示され、フラグメントが添付されましたか?
CoolMind

14
if (getActivity() == null) return;

一部のケースでも機能します。それからコード実行を中断し、アプリがクラッシュしないことを確認するだけです


10

私は同じ問題に直面しました、私はErickによって参照されるリソースを取得するためにシングルトーンインスタンスを追加するだけです

MainFragmentActivity.defaultInstance().getResources().getString(R.string.app_name);

あなたも使うことができます

getActivity().getResources().getString(R.string.app_name);

これがお役に立てば幸いです。


2

ロードされた設定を使用したアプリケーション設定アクティビティが表示されたときに、同様の問題に直面しました。設定の1つを変更してから、表示コンテンツを回転させて再度設定を変更すると、フラグメント(私の設定クラス)がアクティビティに関連付けられていないというメッセージが表示されてクラッシュします。

デバッグ時、表示コンテンツが回転したときにPreferencesFragmentのonCreate()メソッドが2回呼び出されているように見えました。それはすでに十分に奇妙でした。次に、ブロックの外側にisAdded()チェックを追加しました。このチェックは、クラッシュを示し、問題を解決しました。

以下は、新しいエントリを表示するために設定の概要を更新するリスナーのコードです。これは、PreferenceFragmentクラスを拡張するPreferencesクラスのonCreate()メソッドにあります。

public static class Preferences extends PreferenceFragment {
    SharedPreferences.OnSharedPreferenceChangeListener listener;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        // ...
        listener = new SharedPreferences.OnSharedPreferenceChangeListener() {
            @Override
            public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
                // check if the fragment has been added to the activity yet (necessary to avoid crashes)
                if (isAdded()) {
                    // for the preferences of type "list" set the summary to be the entry of the selected item
                    if (key.equals(getString(R.string.pref_fileviewer_textsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Display file content with a text size of " + listPref.getEntry());
                    } else if (key.equals(getString(R.string.pref_fileviewer_segmentsize))) {
                        ListPreference listPref = (ListPreference) findPreference(key);
                        listPref.setSummary("Show " + listPref.getEntry() + " bytes of a file at once");
                    }
                }
            }
        };
        // ...
    }

これが他の人の役に立つことを願っています!


0

次のようにApplicationクラスを拡張し、静的な「グローバル」Contextオブジェクトを維持する場合、アクティビティの代わりにそれを使用して文字列リソースをロードできます。

public class MyApplication extends Application {
    public static Context GLOBAL_APP_CONTEXT;

    @Override
    public void onCreate() {
        super.onCreate();
        GLOBAL_APP_CONTEXT = this;
    }
}

これを使用Toastすると、ライフサイクルを気にすることなく、リソースの読み込みを回避できます。


5
私は反対投票されていますが、誰もその理由を説明していません。通常、静的コンテキストは悪いですが、静的アプリケーション参照がある場合、それはメモリリークではないと考えていました。
Anthony Chuinard

これはハックに過ぎず、適切なソリューションではないため、回答は否定されます。@nhaarmanが共有するチェックソリューション
Vivek Kumar Srivastava

0

私の場合、フラグメントメソッドが後に呼び出されました

getActivity().onBackPressed();

0

古い投稿ですが、最も投票された回答に驚きました。

これに対する適切な解決策は、onStop(またはフラグメント内の適切な場所)で非同期タスクをキャンセルすることです。これにより、メモリリーク(破壊されたフラグメントへの参照を保持する非同期タスク)が発生せず、フラグメントで何が起こっているかをより適切に制御できます。

@Override
public void onStop() {
    super.onStop();
    mYourAsyncTask.cancel(true);
}

1
最も賛成された答えはこれを含んでいます。また、呼び出されcancelないようにすることはできませんonPostExecute
nhaarman 2017年

cancelを呼び出すと、onPostExecuteが呼び出されないことが保証されます。両方の呼び出しが同じスレッドで実行されるため、cancelを呼び出した後に呼び出されないことが保証されます
Raz
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.