为什么片段中的 getContext() 有时 returns 为空?

Why getContext() in fragment sometimes returns null?

为什么 getContext() 有时 returns null?我将上下文作为参数传递给 LastNewsRVAdapter.java。但是 LayoutInflater.from(context) 有时会崩溃。我在 Play 控制台上收到了一些崩溃报告。以下是崩溃报告。

java.lang.NullPointerException
com.example.adapters.LastNewsRVAdapter.<init>

java.lang.NullPointerException: 
at android.view.LayoutInflater.from (LayoutInflater.java:211)
at com.example.adapters.LastNewsRVAdapter.<init> (LastNewsRVAdapter.java)
at com.example.fragments.MainFragment.onFailure (MainFragment.java)
or                     .onResponse (MainFragment.java)
at retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall.run 
(ExecutorCallAdapterFactory.java)
at android.os.Handler.handleCallback (Handler.java:808)
at android.os.Handler.dispatchMessage (Handler.java:103)
at android.os.Looper.loop (Looper.java:193)
at android.app.ActivityThread.main (ActivityThread.java:5299)
at java.lang.reflect.Method.invokeNative (Method.java)
at java.lang.reflect.Method.invoke (Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run 
(ZygoteInit.java:825)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:641)
at dalvik.system.NativeStart.main (NativeStart.java)

这是LastNewsRVAdapter.java构造函数。

public LastNewsRVAdapter(Context context, List<LatestNewsData> 
    latestNewsDataList, FirstPageSideBanner sideBanner) {
    this.context = context;
    this.latestNewsDataList = latestNewsDataList;
    inflater = LayoutInflater.from(context);
    this.sideBanner = sideBanner;
}

这是Fragment

里面的代码onCreateView
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    // Inflate the layout for this fragment

    final View view = inflater.inflate(R.layout.fragment_main_sonku_kabar, container, false);
    tvSonkuKabar = view.findViewById(R.id.textview_sonku_kabar_main);
    tvNegizgiKabar = view.findViewById(R.id.textview_negizgi_kabar_main);
    refresh(view);

    final SwipeRefreshLayout swipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.mainRefreshSonkuKabar);
    swipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
        @Override
        public void onRefresh() {
            refresh(view);
            swipeRefreshLayout.setRefreshing(false);
        }
    });
    setHasOptionsMenu(true);
    return view;
}

这是 Fragment

中的 refresh 方法
private void refresh(final View view) {
    sideBanner = new FirstPageSideBanner();

    final RecyclerView rvLatestNews = (RecyclerView) view.findViewById(R.id.recViewLastNews);
    rvLatestNews.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false));
    rvLatestNews.setNestedScrollingEnabled(false);
    App.getApiService().getLatestNews().enqueue(new Callback<LatestNews>() {
        @Override
        public void onResponse(Call<LatestNews> call, Response<LatestNews> response) {
            if (response.isSuccessful() && response.body().isSuccessfull()){
                adapter = new LastNewsRVAdapter(getContext(), response.body().getData(), sideBanner);
                rvLatestNews.setAdapter(adapter);
                tvSonkuKabar.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onFailure(Call<LatestNews> call, Throwable t) {

        }
    });

正如接受的答案所说,您必须研究您使用和缓存上下文的方式。您以某种方式泄漏了导致空指针异常的上下文。


Below Answer is as per the first revision of the question.

使用onAttach()获取上下文。 大多数情况下,您不需要此解决方案,因此仅当任何其他解决方案均无效时才使用此解决方案。而且你可能会创造 activity 泄漏的机会,所以在使用它时要小心。当您离开 fragment

时,您可能需要再次使上下文为空
// Declare Context variable at class level in Fragment
private Context mContext;

// Initialise it from onAttach()
@Override
public void onAttach(Context context) {
    super.onAttach(context);
    mContext = context;
}

这个上下文将在 onCreateView 中可用,因此您应该使用它。


来自Fragment Documentation

Caution: If you need a Context object within your Fragment, you can call getContext(). However, be careful to call getContext() only when the fragment is attached to an activity. When the fragment is not yet attached, or was detached during the end of its lifecycle, getContext() will return null.

首先,正如您在此 link 中所见,片段生命周期内的方法 onCreateView() 位于 onAttach() 之后,因此此时您应该已经有了一个上下文。您可能想知道,为什么 getContext() return 为 null 呢?问题出在您创建适配器的位置:

App.getApiService().getLatestNews().enqueue(new Callback<LatestNews>() {
        @Override
        public void onResponse(Call<LatestNews> call, Response<LatestNews> response) {
            if (response.isSuccessful() && response.body().isSuccessfull()){
                adapter = new LastNewsRVAdapter(getContext(), response.body().getData(), sideBanner);
                rvLatestNews.setAdapter(adapter);
                tvSonkuKabar.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onFailure(Call<LatestNews> call, Throwable t) {

        }
    });

虽然您在 onCreateView() 中指定了一个回调,但这并不意味着该回调中的代码将 运行 此时。它将 运行 并在网络调用完成后创建适配器。考虑到这一点,您的回调可能会在片段生命周期的那个点之后 运行。我的意思是,用户可以进入该屏幕(片段)并转到另一个片段或 return 到前一个片段,然后网络请求完成(和回调 运行s)。如果发生这种情况,则 getContext() 可以 return 如果用户离开片段(可能已调用 onDetach())为 null。

此外,如果 activity 在您的网络请求完成之前被销毁,您也可能会发生内存泄漏。所以你有两个问题。

我对解决这些问题的建议是:

  1. 为了避免空指针异常和内存泄漏,应该在fragment内部的onDestroyView()被调用时取消网络请求(改造returns一个对象可以取消请求:link).

  2. 另一个防止空指针异常的选项是将适配器 LastNewsRVAdapter 的创建移到回调之外,并在片段中保留对它的引用。然后,在回调中使用该引用来更新适配器的内容:

因此 getContext() 未在 onCreateView() 中调用。它在 onResponse() 回调中调用,可以随时调用。如果在您的片段未附加到 activity 时调用它,getContext() 将 return 为空。

此处理想的解决方案是在 activity/fragment 未激活时取消请求(例如用户按下后退按钮,或最小化应用程序)。

另一种解决方案是当您的片段未附加到您的 activity 时,简单地忽略 onResponse() 中的任何内容,如下所示:

当您确定片段已附加到其主机(onResume、onViewCreated 等)时,使用它代替 getContext() :

requireContext()

它不会被 lint 检查,但如果上下文分离,它会抛出异常!

那么你应该通过 if clouse(或其他东西)检查是否为空,或者确保如果程序到达这一行,它就不是空的。

在改装电话或改装中(可能其他人也以同样的方式),它将 return 一个 disposable 必须在 [=19= 之前清除或处置]onDestroy.

编写一个通用方法,确保您永远不会得到 null Context

public class BaseFragment extends Fragment {
    private Context contextNullSafe;

     @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
         /*View creation related to this fragment is finished here. So in case if contextNullSafe is null
         * then we can populate it here.In some discussion in - 
         * and 
         * there are some recommendations to call getContext() or getActivity() after onCreateView() and
         * onViewCreated() is called after the onCreateView() call every time */
        if (contextNullSafe == null) getContextNullSafety();
    }


   @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        contextNullSafe = context;
    }

    /**CALL THIS IF YOU NEED CONTEXT*/
    public Context getContextNullSafety() {
                if (getContext() != null) return getContext();
                if (getActivity() != null) return getActivity();
                if (contextNullSafe != null) return contextNullSafe;
                if (getView() != null && getView().getContext() != null) return getView().getContext();
                if (requireContext() != null) return requireContext();
                if (requireActivity() != null) return requireActivity();
                if (requireView() != null && requireView().getContext() != null)
                    return requireView().getContext();
                
                return null;
            
        }

    /**CALL THIS IF YOU NEED ACTIVITY*/
    public FragmentActivity getActivityNullSafety() {
        if (getContextNullSafety() != null && getContextNullSafety() instanceof FragmentActivity) {
            /*It is observed that if context it not null then it will be
             * the related host/container activity always*/
            return (FragmentActivity) getContextNullSafety();
        }
        return null;
    }