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();
}
}
除了 onCreate
和 onCreateView
方法之外,此片段还有 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。
我有一个使用单个 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();
}
}
除了 onCreate
和 onCreateView
方法之外,此片段还有 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。