Android 为什么在使用 NavController 导航时会创建两个相同的片段

Android why are two identical Fragments created when navigating with NavController

我有一个使用单个 activity 和多个片段方法的应用程序,我使用 NavController 进行导航。不幸的是,当导航到包含任意 class 中的 Runnable 的片段时,正在创建该片段的两个相同实例,我不明白为什么。

这里是主要代码 activity:

public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding binding;

    public static DB_SQLite_Helper sqLite_DB;

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        View view = binding.getRoot();
        getSupportActionBar().hide();
        setContentView(view);

        sqLite_DB = new DB_SQLite_Helper(this);
    }
}

nav_graph 中的 Home-Fragment 是您可以在此处看到的 FR_Menu 片段:

public class FR_Menu extends Fragment implements View.OnClickListener{

    private FragmentMenuBinding binding;


    public FR_Menu() {

    }


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

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentMenuBinding.inflate(inflater, container, false);
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        binding.buttonExit.setOnClickListener(this);
        binding.buttonTest.setOnClickListener(this);
        return binding.getRoot();
    }

    @Override
    public void onClick(View view) {

        if(view.getId() == R.id.button_test) {
            Navigation.findNavController(getView()).navigate(FR_MenuDirections.actionFRMenuToTest());
        }

        if(view.getId() == R.id.button_exit) {
            getActivity().finishAndRemoveTask();
        }

    }
}

这里我只有一个 OnClickListener 并通过使用 Navigation.findNavController(getView()).navigate(FR_MenuDirections.actionFRMenuToTest()); 行中的 navController 导航到带有 Runnable 的 Fragment。到目前为止,一切都很好。现在创建了名为 Test 的带有 Runnable 的片段。在这里你可以看到这个片段的代码:

public class Test extends Fragment {

    private Handler handler = new Handler();
    int helpCounterRun =0;
    private boolean viewHasBeenCreated = false;
    private FragmentTestBinding binding;

    public Test() {

    }

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

    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        binding = FragmentTestBinding.inflate(inflater, container, false);
        viewHasBeenCreated = true;
        getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        countDownTime();
        return binding.getRoot();
    }

    private void updateScreen() {
        Log.e("LogTag", "Method updateScreen - this: " + this);
    }

    private void countDownTime(){

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                helpCounterRun++;
                Log.e("LogTag", "Method run - helpCounterRun: " + helpCounterRun);
                Log.e("LogTag", "Method run - this: " + this);
                if(viewHasBeenCreated) {
                    countDownTime();
                }
            }

        }, 100);
        updateScreen();
    }

}

除了 onCreateonCreateView 方法之外,此片段还有 2 个基本方法。在 updateScreen 方法中,将当前 Fragment 打印到控制台。在 countDownTime 方法中创建了一个 Runnable 并增加了一个辅助变量 helpCounterRun。辅助变量的值和 Runnable 的当前实例被打印到控制台。输出如下所示:

2022-04-18 10:01:33.742 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 1
2022-04-18 10:01:33.743 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@78ea3f9
2022-04-18 10:01:33.745 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.277 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 1
2022-04-18 10:01:34.278 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@9c893ee
2022-04-18 10:01:34.278 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.294 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 2
2022-04-18 10:01:34.305 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@f8db08f
2022-04-18 10:01:34.306 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 2
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@8b9ef1c
2022-04-18 10:01:34.382 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.414 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 3
2022-04-18 10:01:34.414 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@9ad8725
2022-04-18 10:01:34.415 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.503 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 3
2022-04-18 10:01:34.503 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@9ae00fa
2022-04-18 10:01:34.504 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}
2022-04-18 10:01:34.531 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 4
2022-04-18 10:01:34.562 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@ebec6ab
2022-04-18 10:01:34.562 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c)}
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method run - helpCounterRun: 4
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method run - this: com.example.game.Test@b04e108
2022-04-18 10:01:34.611 25086-25086/com.example.game E/LogTag: Method updateScreen - this: Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e}

updateScreen 方法的输出中可以看出,此片段的 2 个实例已创建并且同时 运行。一个具有 id Test{706103f} (335e5b64-5e97-4f3d-ac1b-8f5a1fcc559c),另一个具有 Test{5140689} (c513c6da-fb15-4273-bea2-dfd89382d9e8) id=0x7f08013e},因此辅助变量 helpCounter 在递增之前使用相同的值打印了 2 次。

我的问题是为什么会这样。我没有看到我的代码的任何部分明确创建了 Fragment Test 的 2 个实例。您是否知道这种奇怪行为的原因可能是什么以及我该如何解决它?

提醒:没有人知道为什么会这样吗?

当您调用 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) 时,您会强制 activity 经历 configuration change - 从纵向变为横向。默认情况下,Android 将销毁您的 activity(以及其中的所有片段)并按照您要求的方向重新创建它。

这就是您收到消息 Method updateScreen - getActivity(): null 的原因 - 由于您的配置更改,该片段及其所在的 activity 已被完全破坏。

但是,即使在片段的视图被销毁之后,您也永远不会停止一遍又一遍地调用 countDownTime()。这意味着您造成了永久内存泄漏。

您已经在跟踪片段的视图是否是通过您的 viewHasBeenCreated 创建的,但您从未将其设置回 false - 您想要覆盖 onDestroyView() 并使用那是您的视图已被破坏的信号。这也是使用 removeCallbacksAndMessages()

删除尚未 运行 的任何 postDelayed 调用的合适位置
@Override
public void onDestroyView() {
    super.onDestroyView();

    // Reset your variable to false
    viewHasBeenCreated = false;

    // And clean up any postDelayed callbacks that are waiting to fire
    handler.removeCallbacksAndMessages(null);
}

请注意,您不需要手动跟踪 viewHasBeenCreated - 您可以使用 getView() != null 进行相同的检查,但要确保您确实清理了 Handler onDestroyView(),您根本不需要进行此检查,因为您将保证在创建视图时只 运行ning。