欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

踩坑,Fragment使用遇到那些坑

程序员文章站 2022-06-15 17:46:52
...

一、 Fragment为什么要用newInstance来初始化:

我们利用Android studio新建fragment的时候,利用谷歌提供的模版,可以看到,新建一个fragment时,fragment的初始化,采用的是静态工厂的形式,具体代码如下:

public class BlankFragment extends Fragment {
    // TODO: Rename parameter arguments, choosenames that match
    // the fragment initialization parameters,e.g. ARG_ITEM_NUMBER
    private static final String ARG_PARAM1 = "param1";
    private static final String ARG_PARAM2 = "param2";
    // TODO: Rename and change types of parameters
    private String mParam1;
    private String mParam2;

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

    public static BlankFragment newInstance(String param1, String param2) {
        BlankFragment fragment = new BlankFragment();
        Bundle args = new Bundle();
        args.putString(ARG_PARAM1, param1);
        args.putString(ARG_PARAM2, param2);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            mParam1 = getArguments().getString(ARG_PARAM1);
            mParam2 = getArguments().getString(ARG_PARAM2);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater,ViewGroup container,
                             BundlesavedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_blank, container, false);
    }
}

从上述代码可以看到,默认的fragment是利用静态工厂的方式对fragment进行初始化的,传入两个参数,但是会有部分人会采用new BlankFragment()的形式对fragment进行初始化,包括我之前也是这么做的,那么,采用new BlankFragment()和Fragment.newInstance()的方式具体有什么不同,下面提供一个小例子来进行说明,上代码:

首先是activity中的布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:orientation="vertical"
        android:id="@+id/layout_top"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

    <LinearLayout
        android:orientation="vertical"
        android:id="@+id/layout_bottom"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp" />

</LinearLayout>

上下排版两个layout,用来摆放fragment,上面的fragment通过new Fragement()的方式创建

代码如下:

@SuppressLint("ValidFragment")

public class TopFragment extends Fragment {


    
    private String text = "默认的文字";



    public TopFragment() {
}



    public TopFragment(String text) {
        
this.text = text;

    }


    
    @Nullable

    @Override

    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment, container, false);

        TextView textView = (TextView) view.findViewById(R.id.tv_content);
textView.setText(text);

        return view;

    }

}

下面的fragment通过Fragment.newInstance()的方式创建,代码如下:

public class BottomFragment extends Fragment {


    
    private static final String PARAM = "param";

    private String mText = "默认的文字";




    public static BottomFragment newInstance(String text) {


        Bundle args = new Bundle();

        args.putString(PARAM, text);

BottomFragment fragment = new BottomFragment();

        fragment.setArguments(args);
return fragment;

    }



    @Override

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

        mText = getArguments().getString(PARAM);

    }



    @Nullable

    @Override

    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {

        View view = inflater.inflate(R.layout.fragment, container, false);

        TextView textView = (TextView) view.findViewById(R.id.tv_content);
textView.setText(mText);

        return view;

    }

}

两个fragment在activity中的调用如下:

public class MainActivity extends AppCompatActivity {


    
    @Override

    protected void onCreate(Bundle savedInstanceState) {
        
super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);


        getFragmentManager().beginTransaction().add(R.id.layout_top, new TopFragment("传进来的文字")).commit();


        getFragmentManager().beginTransaction().add(R.id.layout_bottom, BottomFragment.newInstance("传进来的文字")).commit();


    }

}

下面就是见证奇迹的时刻:运行,上图

踩坑,Fragment使用遇到那些坑

咦,貌似没什么问题啊,你特么在逗我?别急,我们把屏幕横过来,再来看:继续上运行图:

踩坑,Fragment使用遇到那些坑

看出来差别了吗?

上面那个fragment,通过new Fragment()的方式进行初始化的,它挂掉了,工作不正常了,文字恢复成默认的了,而下面通过Fragment.newInstance()进行初始化的依然坚挺!

好了,下面来分析原因,我们知道,activity在默认情况下,切换横竖屏,activity会销毁重建,依附于上面的fragment也会销毁重建,根据这个思路,我们找到fragment重建时调用的代码:

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
    try {
        Class<?> clazz = sClassMap.get(fname);
        if (clazz == null) {
            // Class not found in the cache, see if it's real, and try to add it
            clazz = context.getClassLoader().loadClass(fname);
            if (!Fragment.class.isAssignableFrom(clazz)) {
                throw new InstantiationException("Trying to instantiate a class " + fname
                        + " that is not a Fragment", new ClassCastException());
            }
            sClassMap.put(fname, clazz);
        }
        Fragment f = (Fragment) clazz.getConstructor().newInstance();
        if (args != null) {
            args.setClassLoader(f.getClass().getClassLoader());
            f.setArguments(args);
        }
        return f;
    } catch (ClassNotFoundException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (java.lang.InstantiationException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (IllegalAccessException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": make sure class name exists, is public, and has an"
                + " empty constructor that is public", e);
    } catch (NoSuchMethodException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": could not find Fragment constructor", e);
    } catch (InvocationTargetException e) {
        throw new InstantiationException("Unable to instantiate fragment " + fname
                + ": calling Fragment constructor caused an exception", e);
    }
}

通过以上代码,我们可以看到,fragment是通过反射进行重建的,而且,只调用了无参构造的方法,这也是有部分人通过new Fragment()的方式构建fragment时,遇到屏幕切换时,fragment会报空指针异常的原因注意看代码中f.setArguments(args);
也就是说,fragment在初始化之后会把参数保存在arguments中,当fragment再次重建的时候,它会检查arguments中是否有参数存在,如果有,则拿出来再用,所以我们再onCreate()方法里面才可以拿到之前设置的参数,但是:fragment在重建的时候不会调用有参构造,所以,通过new Fragment()的方法来初始化,fragment重建时,我们设置的参数就没有了

二、 Fragment中调用getActivity()时,报空指针异常:

一般的,我们在代码中请求网络数据后,由于是在子线程中得到的结果,更新UI界面时,要在UI线程中进行,如果是在fragment中,则需要执行以下代码:

// 在UI线程中展示吐司
private void showToast(String text) {
    getActivity().runOnUiThread(() ->
            Toast.makeText(getActivity(),text,Toast.LENGTH_SHORT).show()
    );
}

乍一看,代码貌似没什么问题,运行起来也能正常显示,但是这样写,在系统可用内存较低时,会频繁触发crash(尤其在低内存手机上经常遇到),此时,再执行这样的代码,就会报空指针异常了,下面来分析具体原因:

首先,我们来看一下fragment的声明周期:

踩坑,Fragment使用遇到那些坑

重点关注onAttach()方法和onDetach():

当执行onAttach()时,Fragment已实现与Activity的绑定,在此方法之后调用getActivity()会得到与次Fragment绑定的activity对象;当可用内存过低时,系统会回收Fragment所依附的activity,ye'jiu'sh的onDetach()时,Fragment已实现与Activity解绑,在此方法之后调用getActivity(),由于Fragment已经与Activity解绑,皮之不存毛将焉附?则系统就会返回空指针了。

ok,搞定了具体原因后,再来分析解决办法就变得容易了,我们可以在onAttach()后的任意一个方法执行时,比如onAttach()时,保存一份activity为全局属性,这样一来,下次调用getActivity()时,直接使用我们保存的全局mActivity来代替即可,代码如下:

private Activity mActivity;

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    mActivity = activity;
}

// 在UI线程中展示吐司
private void showToast(String text) {
    mActivity.runOnUiThread(() ->
            Toast.makeText(mAcitivty, text, Toast.LENGTH_SHORT).show()
    );
}

不过,此方法有个小问题:当系统调用onAttach()时,Fragment与Activity已经分离,此时Fragment定然对用户不可见的,既然不可见,还更新界面做什么呢?岂不白白浪费资源嘛?这个问题,我们再第三节中进行分析解决

三、Fragment不调用onResume或者onPause方法:

首先,来看Fragment的源码:

/**
 * Called when the fragment is visible to the user and actively running.
 * This is generally
 * tied to {@link Activity#onResume() Activity.onResume} of the containing
 * Activity's lifecycle.
 */
public void onResume() {
    mCalled = true;
}
/**
 * Called when the Fragment is no longer resumed.  This is generally
 * tied to {@link Activity#onPause() Activity.onPause} of the containing
 * Activity's lifecycle.
 */
public void onPause() {
    mCalled = true;
}

注意看方法上面的注释,调用两个方法,返回的是此Fragment所依附的Activity的声明周期中的onResume()和onPause(),并不是Fragment自身的onResume()和onPause(),那么,如果我们也想实现类似Acitivity的onResume()和onPause(),应该怎么做呢?我们继续翻Fragment的源码,找到这么个方法:

/**
 * Set a hint to the system about whether this fragment's UI is currently visible
 * to the user. This hint defaults to true and is persistent across fragment instance
 * state save and restore.
 *
 * <p>An app may set this to false to indicate that the fragment's UI is
 * scrolled out of visibility or is otherwise not directly visible to the user.
 * This may be used by the system to prioritize operations such as fragment lifecycle updates
 * or loader ordering behavior.</p>
 *
 * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
 *                        false if it is not.
 */
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = !isVisibleToUser;
}

注释中说的很清楚,此方法是告诉系统,当前Fragment是否对用户可见,其中有一个isVisibleToUser参数,我们可以重写这个方法,通过判断isVisibleToUser的值来确定此Fragment是否对用户可见,从而间接实现onResume()和onPause()的功能,具体代码如下:

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);
    if(isVisibleToUser){
        //TODO:执行对用户可见时的代码
    }else {
        //TODO:执行对用户可不见时的代码
    }
}

四、Fragment在kotlin语言中使用kotlin-android-extensions报错的问题:

自从Androidstudio 3.0发布后,kotlin成为Android开发的第一语言,Kotlin由JetBrains公司开发,与Java 100%互通,并具备诸多Java尚不支持的新特性,JetBrains是个拽拽的公司,看不惯eclipse,就开发了Intellij IEDA,感觉eclipse的Android插件不全面,就自己开发了Android studio,不喜欢java的繁琐,就自己搞了kotlin,如果你还记得的话,去年曾有报道称 Google Android 考虑采用苹果的 Swift 语言,而 Swift 就被称为是IOS界的Kotlin,总之:JetBrains公司猛地一塌糊涂,有它的加持,kotlin的发展一路顺风顺水,大有取代java之势!

好了,闲话不多说了,我们来举个栗子吧,上代码:

先看传统的activity中获取控件是怎么写的:

public class MainActivity extends AppCompatActivity {

    private TextView mText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mText = (TextView) findViewById(R.id.tv_result);
        mText.setText("这里展示结果");

    }
}

转换为kotlin后,这样写:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tv_result.text = "这里展示结果"

    }
}

再也不用写该死的findViewById了,控件名字直接拿来用,甚至butterkinfe都下岗了,多方便!这都多亏了kotlin-android-extensions插件,通过这个插件,我们可以直接在kotlin代码中调用xml中的控件,只需要在kotlin代码中引入这么一行就OK:

import kotlinx.android.synthetic.main.activity.*

同样的,我们再Fragment中执行同样的代码:

class ToolsFragment : BaseFragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_tools, container, false)
        layout_translate.setOnClickListener { startActivity(Intent(mActivity, TranslateActivity::class.java)) }
        return view
    }
}

运行,咦?挂了,报的什么错,控件找不到?什么鬼,明明跟activity的写法一模一样的啊!

怎么办?继续翻源码呗:

/**
 * Called to have the fragment instantiate its user interface view.
 * This is optional, and non-graphical fragments can return null (which
 * is the default implementation).  This will be called between
 * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}.
 *
 * <p>If you return a View from here, you will later be called in
 * {@link #onDestroyView} when the view is being released.
 *
 * @param inflater The LayoutInflater object that can be used to inflate
 * any views in the fragment,
 * @param container If non-null, this is the parent view that the fragment's
 * UI should be attached to.  The fragment should not add the view itself,
 * but this can be used to generate the LayoutParams of the view.
 * @param savedInstanceState If non-null, this fragment is being re-constructed
 * from a previous saved state as given here.
 *
 * @return Return the View for the fragment's UI, or null.
 */
@Nullable
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
                         Bundle savedInstanceState) {
    return null;
}

/**
 * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}
 * has returned, but before any saved state has been restored in to the view.
 * This gives subclasses a chance to initialize themselves once
 * they know their view hierarchy has been completely created.  The fragment's
 * view hierarchy is not however attached to its parent at this point.
 * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}.
 * @param savedInstanceState If non-null, this fragment is being re-constructed
 * from a previous saved state as given here.
 */
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
    
}

有一个onViewCreated()方法,此方法注释中有讲,当控件加载完成后,立刻调用此方法,这里就需要提到kotlin-android-extensions插件的工作机制了,kotlin-android-extensions在布局文件加载完成后,会生成一个缓存视图,此时我们直接通过控件名字使用控件,其实就是在缓存视图中对控件进行赋值,因此访问速度更快,代码量更少!

那么,在onCreateView()中直接使用控件,为什么就报错了呢?因为在此时,XML布局尚未完全加载到缓存视图中,此时直接使用控件,自然就会报错了,所以,正确的代码应该这么写:

class ToolsFragment : BaseFragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        val view = inflater.inflate(R.layout.fragment_tools, container, false)
        return view
    }

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        layout_translate.setOnClickListener {
            startActivity(Intent(mActivity,TranslateActivity::class.java))
        }
    }
}

我们在onViewCreated()后再使用控件,此时,缓存视图已创建完毕并返回给系统,控件就可以正常使用了。

那么,又有人问了,为什么在activity中的onCreateView()中直接使用控件就正常呢?这里涉及到setContentView与LayoutInflater的区别,这里面的机制还是比较复杂的,下次再讲

五、Fragment的懒加载:

有时候出于省流量的考虑,或者考虑到性能的关系,我们希望Fragment在对用户可见时,再进行页面加载以及相关逻辑的运行,此时,就需要考虑Fragment的懒加载了,然而,系统并没有给我们提供这样的一个工具,这就需要我们自己来进行实现了,其实我们完全可以参考第三部分的思路来实现,写法如下(kotlin代码):

override fun setUserVisibleHint(isVisibleToUser: Boolean) {
    super.setUserVisibleHint(isVisibleToUser)
    if(isVisibleToUser){
        onFragmentResume()
    }else{
        onFragmentPause()
    }
}

open protected fun onFragmentResume() {

}

open protected fun onFragmentPause() {

}

然而,当我们真正编译执行后,会发现,系统直接报空指针了——控件找不到,什么情况呢?继续翻源码,注意这么一句话:

/**
 * Set a hint to the system about whether this fragment's UI is currently visible
 * to the user. This hint defaults to true and is persistent across fragment instance
 * state save and restore.
 *
 * <p>An app may set this to false to indicate that the fragment's UI is
 * scrolled out of visibility or is otherwise not directly visible to the user.
 * This may be used by the system to prioritize operations such as fragment lifecycle updates
 * or loader ordering behavior.</p>
 *
 * <p><strong>Note:</strong> This method may be called outside of the fragment lifecycle.
 * and thus has no ordering guarantees with regard to fragment lifecycle method calls.</p>
 *
 * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default),
 *                        false if it is not.
 */
public void setUserVisibleHint(boolean isVisibleToUser) {
    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED
            && mFragmentManager != null && isAdded()) {
        mFragmentManager.performPendingDeferredStart(this);
    }
    mUserVisibleHint = isVisibleToUser;
    mDeferStart = mState < STARTED && !isVisibleToUser;
}

注意看注释:This method may be called outside of the fragment lifecycle. and thus has no ordering guarantees with regard to fragment lifecycle method calls,就是说,不能保证这个方法在声明周期中的顺序,那么,我们打印一下LOG(具体LOG这里就不展示了),可以看到,在Fragment首次初始化的时候,setUserVisibleHint()方法是在onCreate()之前调用的,在随后的fragment来回切换时,也会调用setUserVisibleHint(),这说明,当系统首次调用setUserVisibleHint()时,控件尚未加载完成,如果我们在这时进行控件的相关操作,自然就会报空指针了,那么,怎么解决这个问题呢?我们可以立一个flag,在setUserVisibleHint()被调用时,检查onViewCreated()是否已被调用,如果是,则再根据是否对用户可见,执行相应的UI操作,具体代码如下:

    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        isViewCreated = true
        // onViewCreated只会调用一次,当调用此方法时,判断是否对用户可见,如果可见,调用懒加载方法
        if (mIsVisibleToUser) {
            onLazyLoad()
            isFirstVisible = false
            onFragmentResume()
        }
    }

    override fun setUserVisibleHint(isVisibleToUser: Boolean) {
        super.setUserVisibleHint(isVisibleToUser)
        mIsVisibleToUser = isVisibleToUser
        if (!isViewCreated) return  // 如果视图未创建完成,返回
        if (isVisibleToUser) {      // 如果对用户可见
            onFragmentResume()
            if (isFirstVisible) {   // 如果是第一次对用户可见,则调用懒加载
                onLazyLoad()
                isFirstVisible = false
            }
        } else {
            onFragmentPause()
        }
    }

    open protected fun onLazyLoad() {

    }

    open protected fun onFragmentResume() {

    }

    open protected fun onFragmentPause() {

    }

总结:Fragment在安卓开发中是使用率相当高的一个控件,几乎每一个APP中都能见到它的身影,其重要性不言而喻。另外:谷歌对于每一个API都有详细的注释,平时多翻一翻,会大有收获的;再有:我们也可以利用Android studio提供的模板,看看它们是怎么写的,然后再根据这个思路进行开发,也是一种不错的选择呢