1、什么是 Fragment,以及它和 Activity 的区别?

Fragment 是 Android 系统中的一个 UI 组件,用于创建 Activity 加载的屏幕内容的一部分。
简单来说,Fragment 就像是一个小型的 Activity,可以嵌套在 Activity 中使用。

和activity相比,Fragment有以下的不同之处:

  • 生命周期:Activity 和 Fragment 有自己的生命周期和回调方法,在大多数生命周期方法中,Activity 和其包含的 Fragment 都会收到相应的回调。

  • 布局和视图:Activity 通常使用 setContentView() 方法设置布局文件来加载视图,而 Fragment 则需要通过 onCreateView() 方法生成其所包含视图的界面元素,并且需要添加到一个 ViewGroup 容器中。

  • 可重用性:由于 Fragment 可以在多个 Activity 或其他 Fragment 中重复使用,因此具有更好的可重用性和模块化程度。

  • 分屏支持:Fragment 提供了对分屏模式的更好支持,在 Android 应用程序支持分屏的情况下,能够让多个 Fragment 更加灵活地协同工作,同时实现最佳用户体验。

总而言之,Fragment 是为了提高代码的可重用性和模块化程度而设计的,可以嵌套在 Activity 中使用,使得 Activity 内部的各个组件耦合度更低、代码结构更清晰。


2、Fragment 生命周期是怎样的?如何在代码中管理 Fragment 的生命周期?

Fragment 有自己的生命周期和回调方法,它们包括以下几个部分:

  • onCreate():当 Fragment 第一次被创建时调用,通常进行一些初始化工作。

  • onCreateView():创建 Fragment 的视图层次结构,并返回该层次结构的根视图。

  • onStart():Fragment 可见但不是在前台时调用。

  • onResume():Fragment 现在可见并且正在运行,这时候用户与之交互。

  • onPause():另一个 activity 进入前台,Fragment 不可见,但仍然存在。

  • onStop():Fragment 完全不可见时调用,可停止动画或其他消耗 CPU/网络资源的操作。

  • onDestroyView():当 Fragment 的视图被移除时调用,用于清理界面或释放资源。

  • onDestroy():Fragment 被销毁时调用,用于清理任何剩余的资源

  • onDetach():当 Fragment 与 Activity 解除关联时调用,即 Fragment 不再依附于任何 Activity

在代码中管理 Fragment 的生命周期,主要通过 FragmentTransaction 对象实现。可以使用 add()、replace()、remove()、attach() 和 detach() 方法来添加、替换、删除、附加和分离 Fragment,从而显式地控制其生命周期。
此外,还可以通过调用 commit() 或 commitNow() 方法立即或延迟提交事务。

FragmentManager fragmentManager = getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

// 创建新 Fragment 实例
ExampleFragment fragment = new ExampleFragment(); 
fragmentTransaction.add(R.id.fragment_container, fragment);
fragmentTransaction.commit();

这里的 R.id.fragment_container 是一个 FrameLayout 容器,用于显示 Fragment 的视图层次结构。在创建 Fragment 时可以传递参数、设置标签等,便于之后找到和管理它们。

3、说一下 Fragment 之间通信的方式以及优缺点?

Fragment 之间可以使用以下几种方式进行通信:

  • 使用 Activity 作为中介:当两个 Fragment 都依附于同一个 Activity 时,它们可以通过 Activity 对象进行相互通信。例如,一个 Fragment 可以调用 getActivity() 方法获取对 Activity 的引用,然后调用其公共方法或属性来与另一个 Fragment 交互

      • 优点:简单易用,无需额外配置
      • 缺点:较为耦合,需要在 Activity 中定义接口和实现
  • 使用 Bundle 参数传递数据:可以将需要传递的参数封装在 Bundle 中,然后通过 setArguments()/getArguments() 方法进行传递和获取。例如:

Bundle args = new Bundle();
args.putInt("key", value);
fragment.setArguments(args);

这样就可以在目标 Fragment 中通过 getArguments() 方法获取对应的参数值了。

    • 优点:简单快捷,无需关注实现细节
    • 缺点:只适用于少量数据传递,复杂数据结构稍显不足

使用接口回调:通过定义接口并在其中声明一些方法,再让包含该 Fragment 的 Activity 实现该接口,并在需要的时候调用方法,从而实现 Fragment 之间的通信。

  1. 优点:解耦,能够处理复杂的逻辑关系。
  2. 缺点:需要代码量较多、手动创建接口,不太适合简单的数据通信。

使用共享 ViewModel:考虑到多个 Fragment 可能需要共享同一个数据对象,可以使用 Android Architecture Components 提供的 ViewModel。它可以在跨多个 Fragment 的情况下保存和管理数据,并确保多个 Activity 和 Fragment 共享一个 VM 实例。

  1. 优点:方便碎片间共享数据,并且具有生命周期感知能力
  2. 缺点:引入另一个框架,增加了复杂度**

综上所述,不同的 Fragment 通信方式各有优缺点,应根据实际需求选择最合适的方式来实现。例如,如果只是传递少量参数,则可以选择 Bundle 参数传递或通过 Activity 中介来实现;如果需要处理一些复杂逻辑关系,可以使用接口回调模式;如果需要分享大量数据或状态信息,则应该使用 ViewModel

关于ViewModel,这里给出一份官方提供的参考代码来分析:

public class DiceUiState {
    private final Integer firstDieValue;
    private final Integer secondDieValue;
    private final int numberOfRolls;

    // ...
}

public class DiceRollViewModel extends ViewModel {

    private final MutableLiveData<DiceUiState> uiState =
        new MutableLiveData(new DiceUiState(null, null, 0));
    public LiveData<DiceUiState> getUiState() {
        return uiState;
    }

    public void rollDice() {
        Random random = new Random();
        uiState.setValue(
            new DiceUiState(
                random.nextInt(7) + 1,
                random.nextInt(7) + 1,
                uiState.getValue().getNumberOfRolls() + 1
            )
        );
    }
}

这是一个使用Android Architecture Components中的ViewModel和LiveData实现的掷骰子应用的代码片段。DiceUiState是一个不可变的数据类,包含两个整数和一个整数,分别表示两个骰子的值和掷骰子的次数。DiceRollViewModel是一个ViewModel类,包含一个MutableLiveData对象,它的值可以被更改,用于存储应用程序的状态。rollDice()方法通过生成两个随机数来更新uiState的值。

LiveData对象可以通过getUiState()方法被观察。这段代码展示了如何使用LiveData和ViewModel来管理应用程序的状态。

然后我们可以从 activity 中访问 ViewModel:

public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.
        DiceRollViewModel model = new ViewModelProvider(this).get(DiceRollViewModel.class);
        model.getUiState().observe(this, uiState -> {
            // update UI
        });
    }
}

4、如何将 Fragment 添加到 Activity 中?

首先第一种方法:

  1. 在布局文件中添加一个 FrameLayout 等容器组件,用于显示 Fragment 的视图层次结构。例如:
<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />
  1. 创建一个 Fragment 对象,并在其 onCreateView() 方法中返回该 Fragment 的布局。
public class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_example, container, false);
    }
}

这里的 R.layout.fragment_example 可以自定义 Fragment 布局的 ID,一般来说就是一个 XML 文件,用于定义界面元素和样式。

  1. 在 Activity 中使用 FragmentManager 进行 Fragment 操作,通过 add()、replace()、remove()、attach() 和 detach() 等方法添加、替换、删除、附加和分离 Fragment。例如:
// 获取 FragmentManager
val fragmentManager = supportFragmentManager

// 执行 FragmentTransaction 操作
val transaction = fragmentManager.beginTransaction()

// 实例化并添加新的 Fragment
val fragment = ExampleFragment()
transaction.add(R.id.fragment_container, fragment)

// 提交事务
transaction.commit()

需要注意的是,为了能够正常处理 Fragment 的生命周期,必须遵循 ActivityFragment 的生命周期管理规范,例如在 onCreate() 方法中创建 Fragment,设立一个托管 Fragment 的容器等。此外,还可以进行类似于获取 Fragment 对象、复用布局等操作,具体可根据实际需求进行调整。


5、Fragment 的 addToBackStack() 方法有什么作用?

addToBackStack() 方法是 FragmentManager 提供的一个方法,主要作用是将当前 Fragment 的事务添加到返回栈中(Back Stack)。

当用户按下设备的 “返回” 按钮时,会触发回退行为,系统会重新启动上一个 Activity 栈的管理器,并从栈顶把之前进栈的最后一个 Fragment 弹出,显示到屏幕上。因此,通过 addToBackStack() 方法将 Fragment 添加到返回栈中,就能够实现在弹出该 Fragment 时,自动恢复到上一个 Fragment 或者上一个 Activity 页面的效果,向用户提供更好的交互体验。

例如,可以在一个包含多个 FragmentActivity 中,将当前的 Fragment 实例添加到返回栈中,然后通过 popBackStackImmediate()、popBackStack() 方法弹出并恢复到前一个 Fragment:

// 添加到返回栈中
supportFragmentManager.beginTransaction()
    .replace(R.id.container, someFragment)
    .addToBackStack(null)
    .commit();

// 弹出并恢复到前一个 Fragment
if (supportFragmentManager.popBackStackImmediate()) {
    // 成功弹出前一个 Fragment
} else {
    // 回退栈为空,直接关闭 Activity
}

需要注意的是,每个 Fragment Transction 都可以向返回栈中添加多个 Fragment 对象。当你插入多个 Fragment 后,用户按下 “返回” 按钮时可能会看到多个回退 Fragment 页面依次展示。

因此,在使用 addToBackStack() 方法时,应该根据实际需求来决定添加的 Fragment 是否需要利用返回栈,避免用户体验效果不佳或造成混淆。同时,在添加到返回栈时,还要考虑内存消耗和性能问题,避免出现内存泄漏或卡顿的情况。


6、Fragment 宽高计算的时机,是在哪个回调方法里进行的?

Fragment 的计算宽高的时机是在其视图第一次创建时进行的,即在 onCreateView() 方法中。

Fragment 首次创建时,需要根据布局文件或代码中设置的布局参数来测量并计算出视图的宽高。这个过程是由 Fragment 进行的,并将最终的宽高信息存储在视图层次结构的 LayoutParams 中。该 ViewGroup 会使用这些参数对位于自己器内的子 View 进行定位和排列。

因此,在实现 Fragment 视图界面时,需要在 onCreateView() 方法中完成对布局文件或代码中设置的布局参数进行处理和测量的操作,以确保 Fragment 在初始时得到正确的大小和位置。例如:

public class ExampleFragment extends Fragment {
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_example, container, false);

        // 计算 Fragment 的宽高
        measureView(view);

        return view;
    }

    private void measureView(View view) {
        // 获取 LayoutInflater
        LayoutInflater inflater = getLayoutInflater();

        // 测量 Fragment 的宽高
        int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(inflater.getContext().getResources().getDisplayMetrics().widthPixels,
                View.MeasureSpec.EXACTLY);
        int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
        view.measure(widthMeasureSpec, heightMeasureSpec);
    }
}

需要注意的是,虽然在 onCreateView() 方法中可以进行宽高测量和计算,但是该方法只会在 Fragment 第一次创建时触发。如果需要更新 Fragment 的视图大小和位置,需要调用 requestLayout()getViewTreeObserver().addOnGlobalLayoutListener() 等相关方法来实现。同时还要避免出现视图测量不准确或不合理的情况,而导致界面显示异常。


7、状态丢失的情况下如何恢复 Fragment 状态?

Android 中,当 Activity 被系统销毁或由于其他原因被回收后,其中的 Fragment 状态也会随之消失。为了防止用户在应用重启后出现不良体验,需要保证 Fragment 的数据及状态得到保存并恢复。

下面是一种常见的解决方案,可以在恢复 Fragment 状态时使用:

  1. 将要在 Fragment 中传递和保存的数据保存在 Bundle 中。

  2. FragmentonSaveInstanceState() 回调方法中将 Bundle 信息保存起来。

  3. FragmentonCreateView() 方法中检查 savedInstanceState 是否为空,并根据需要还原数据。

根据需要修改 FragmentsetRetainInstance() 方法,以控制 Fragment 是否保留实例状态。

例如:

public class ExampleFragment extends Fragment {

    private static final String KEY_EXAMPLE = "123456789";
    private String mExample;

    public ExampleFragment() {
        // Required empty public constructor
    }

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

        if (savedInstanceState != null) { 
            // 恢复状态
            mExample = savedInstanceState.getString(KEY_EXAMPLE);
        }
    }

    @NonNull
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_example, container, false);

        // 继续执行 Fragment 的相关操作
        // ...

        return view;
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        // 保存状态数据
        outState.putString(KEY_EXAMPLE, mExample);
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();

        // 重置数据
        mExample = null;
    }
}

需要注意的是,自行保留 Fragment 状态时需要考虑内存消耗问题,避免出现内存泄漏等问题。

同时还要定期清理无用状态数据,以确保应用始终处于良好的运行状态。另外,在 onSaveInstanceState() 方法中一般只保存必要的数据,避免对性能产生不必要的影响。


8、FragmentStatePagerAdapter 和 FragmentPagerAdapter 有什么区别?使用场景分别是什么?

FragmentStatePagerAdapterFragmentPagerAdapter 是两个常用的 ViewPager 适配器,它们都可以用来管理 Fragment。

两者的主要区别如下:

  • 对于 FragmentStatePagerAdapter,当 Fragment 不再使用时(比如滑出屏幕),会被销毁;下次需要时又会重新创建。而 FragmentPagerAdapter 则只是销毁视图,不会销毁对象。这导致 FragmentStatePagerAdapter 更加适合处理大量的 Fragment。

  • 因为 FragmentStatePagerAdapter 会销毁 Fragment,所以最好避免调用 FragmentsetRetainInstance(true) 方法,否则会导致 Fragment 的状态无法恢复。而 FragmentPagerAdapter 则没有这样的限制。*

使用场景分别为:

  1. 适用于数据量较大且变化多端时,FragmentStatePagerAdapter 在不需要显示的 Fragment 上释放资源,防止性能问题

  2. 如果数据量小且稳定,FragmentPagerAdapter 可以达到更好的效果。


9、Fragment 的使用有哪些注意事项?

对于 Fragment 使用的注意事项如下:

  • 避免频繁替换 Fragment,因为该操作可能会消耗大量的资源和时间。

  • 在创建 Fragment 时必须使用无参的构造函数,不要在 Fragment 中定义带参数的构造函数。如果需要传递参数,应该通过 Fragment.setArguments(Bundle) 方法来实现。

  • Fragment 跟随 Activity 生命周期,但两者并不一直相同。在某些情况下,Activity 已经进入 Destroy 状态,但 Fragment 仍然存在于回退栈中。如果不加以处理,会导致内存泄漏等问题。

  • 在使用 Fragment 时,建议将视图控制交给 Fragment 的布局文件或者代码来实现,尽量减少在 Activity 中对 Fragment 视图的操作。

  • 尽量避免在 Fragment 中进行耗时或阻塞操作,以免影响到主线程的运行。若必须执行此类操作,则可以使用 AsyncTask,Loader 或 RxJava 等异步处理框架来解决。

  • 数据源不同FragmentStatePagerAdapter 数据源是一个 Fragment 列表,会在用户滑动 ViewPager 时自动销毁和重建 Fragment,并调用 onSaveInstanceState() 方法保存状态。而 FragmentPagerAdapter 数据源则是已经创建的 Fragment 列表,不会被销毁,也不会调用 onSaveInstanceState() 方法。

  • 性能差异当数据量很大时,使用 FragmentStatePagerAdapter 性能更好,因为它会及时清理不再需要的 Fragment 引用,避免对内存造成过大的压力。而 FragmentPagerAdapter 则会一直以列表形式维护所有 Fragment 引用,可能导致内存占用过大


10、Fragment 使用注意事项?

  1. Fragment 中访问 Activity 必须通过 getActivity() 方法,调用期间需要进行非空检查

  2. 内部变量的读写应该保证线程安全性,例如可以采用 synchronized 关键字或其他方式。

synchronized是java的一个关键字,用于实现线程同步。在Java中,多线程并发访问同一个共享资源时,会产生竞态条件(race condition)的问题,从而导致数据不一致或异常结果。

synchronized关键字可以保证在同一时刻只有一个线程可以访问被synchronized修饰的对象或方法,从而避免了竞态条件的问题

  1. 避免直接操作 Fragment 生命周期方法(如 onResume、onPause),这么做会破坏原有的生命周期流程。

  2. 尽量减少在 onCreateView() 方法中的耗时操作,以加快界面展示速度。

  3. 如果需要传递参数,可以使用 Bundle,在 Fragment 增加 new Instance 静态方法,并在 ActivitynewInstance 方法进行调用。

  4. 为了避免内存泄漏等风险,离开当前的 Fragment 时,需要及时清空相关资源并将变量设置为 null。

  5. 原则上 Fragment 不应该直接和其他 Fragment 或者 Activity 直接通信,而是通过定义接口实现回调,避免出现耦合过度的情况

更多推荐