踩坑,Fragment使用遇到那些坑
一、 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,通过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的声明周期:
重点关注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提供的模板,看看它们是怎么写的,然后再根据这个思路进行开发,也是一种不错的选择呢
下一篇: java和php对等的3DES加密算法