Android开发之Fragment知识总结

这是一篇自己很早总结的,一直在笔记里存着,再放都发霉了,拿出来晒晒。

手机从塞班时代百花争艳到乔布斯的方块范畴,再到现在的不到不小的16:9,然后就是华为、三星的折叠系列,不出意料未来的手机又会到一个个性化高峰。这些对于我们开发者又是一个里程碑的挑战,以前我们只需要维护好一个页面,现在为了实行多样化需求,我们要尽可能在单个页面上做更多的碎片处理。

Fragment的生命周期(对比Activity)

Fragment的实例过程是跟随在Activity生命周期后边的。
Fragment的生命周期

Fragment有两种创建方式:静态和动态。静态创建方式是直接在Activity的xml中布局Fragment,动态方式一般是在Activity的onCreat()方法下动态引入Fragment,因此两种情况生命周期在初始化过程略有不同,以下是Fragment的周期:

静态方式初始化过程:
静态方式初始化过程

动态方式初始化过程:
动态方式初始化过程

退到后台过程:
界面退到后台

恢复到前台过程:
界面恢复到前台

静态方式屏幕旋转过程:
静态方式屏幕旋转过程

动态方式屏幕旋转过程:
动态方式屏幕旋转过程

销毁过程:
界面销毁

onSaveInstanceState
Fragment的onSaveInstanceState和Activity的onSaveInstanceState执行时间一样,也是处于onPause和onStop之间。

我的声明周期为什么和你的不一致?
有些同学问为什么我Demo里Fragment声明周期却比Activity的早,比如Fragment的onStart方法比Activity的onStart()执行早呢?其实Activity的onStart方法执行的时候会执行父类的onStart方法(super.onStart()),此时由Activity的super.onStart()影响Fragment的onStart方法的启动,然而你的日志却在Activity的super.onStart方法之后,然后就有这种疑问了,Activity生命周期的super方法均会影响Fragment的生命周期,所以我们真正去生命周期时候要顺藤摸瓜,否则结果恰恰相反。

    //Activity
    @Override
    protected void onStart() {
        Log.e(TAG, "++++++++++++++-onStart()");
        super.onStart();
    }

    ...

    //Fragment
    @Override
    public void onStart() {
        Log.e(TAG,"-----------+onStart()");
        super.onStart();
    }

为什么动态方式创建我执行屏幕旋转时初始化过程Fragment的初始化执行了两编,并且伴随着还有一编销毁过程?
大家知道屏幕旋转会使Activity的生命周期重建,并且onSaveInstanceState也会执行相应状态保存,然后在重建的时候进行释放。动态创建是在super.onCreate(savedInstanceState);这句之后,因此父类的onCreate()方法会根据savedInstanceState状态做判断,这里会将之前存储的Fragment作相应的销毁,然后在进行实例加载。所以我们动态加载的时候一般会从savedInstanceState中根据Fragment的Tag取出相应的Fragment,然后用事务在进行加载。

    //Activity源码
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        this.mFragments.attachHost((Fragment)null);
        super.onCreate(savedInstanceState);
        FragmentActivity.NonConfigurationInstances nc = (FragmentActivity.NonConfigurationInstances)this.getLastNonConfigurationInstance();
        if (nc != null && nc.viewModelStore != null && this.mViewModelStore == null) {
            this.mViewModelStore = nc.viewModelStore;
        }

        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable("android:support:fragments");
            this.mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
            if (savedInstanceState.containsKey("android:support:next_request_index")) {
                this.mNextCandidateRequestIndex = savedInstanceState.getInt("android:support:next_request_index");
                int[] requestCodes = savedInstanceState.getIntArray("android:support:request_indicies");
                String[] fragmentWhos = savedInstanceState.getStringArray("android:support:request_fragment_who");
                if (requestCodes != null && fragmentWhos != null && requestCodes.length == fragmentWhos.length) {
                    this.mPendingFragmentActivityResults = new SparseArrayCompat(requestCodes.length);

                    for(int i = 0; i < requestCodes.length; ++i) {
                        this.mPendingFragmentActivityResults.put(requestCodes[i], fragmentWhos[i]);
                    }
                } else {
                    Log.w("FragmentActivity", "Invalid requestCode mapping in savedInstanceState.");
                }
            }
        }

        if (this.mPendingFragmentActivityResults == null) {
            this.mPendingFragmentActivityResults = new SparseArrayCompat();
            this.mNextCandidateRequestIndex = 0;
        }

        this.mFragments.dispatchCreate();
    }

FragmentActivity和Activity的区别

fragment是3.0以后的东西,为了在低版本中使用fragment就要用到android-support-v4.jar兼容包,而fragmentActivity就是这个兼容包里面的,它提供了操作fragment的一些方法,其功能跟3.0及以后的版本的Activity的功能一样。下面是API中的原话:

FragmentActivity is a special activity provided in the Support Library to handle fragments on system versions older than API level 11. If the lowest system version you support is API level 11 or higher, then you can use a regular Activity.

  1. fragmentactivity 继承自activity,用来解决android3.0 之前没有fragment的api,所以在使用的时候需要导入support包,同时继承fragmentActivity,这样在activity中就能嵌入fragment来实现你想要的布局效果。
  2. 当然3.0之后你就可以直接继承自Activity,并且在其中嵌入使用fragment了。
  3. 获得Manager的方式也不同
    3.0以下:getSupportFragmentManager()
    3.0以上:getFragmentManager()

Fragment常用形式

终于讲到Fragment的加载形式了,上边提到Fragment的加载形式有两种:静态加载和动态加载。

静态创建

静态创建方式是直接在Activity的xml中布局Fragment,这个很简单,但是一定要注意两点:

  1. xml中的fragment一定要有一个id或者tag。
  2. 所对应的fragment一定要返回一个视图。

xml布局:

    <fragment
        android:id="@+id/myfragment_1"
        android:name="com.example.admin.TestFragment"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

fragment类:

public class TestFragment extends Fragment {

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment, container, false);
    }
}

动态创建

动态创建方式是需要在Activity的onCreat()方法下动态引入Fragment,由于上边提到在屏幕旋转或者Activity被无意回收销毁前系统会执行onSaveInstanceState()方法保存相应的状态,然后在Activity的onCreat方法执行时savedInstanceState会释放相应资源,此时如果直接在onCreat方法中创建有可能会走两遍生命周期,因此我们要利用这个特点直接从相应状态中去取,去不到时再创建也不迟。

    //在Activity中onCreat中实现
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        FragmentManager fm = getSupportFragmentManager();
        FragmentTransaction transaction = fm.beginTransaction();
        if(savedInstanceState != null){
            tf = (TestFragment) fm.findFragmentByTag("TestFragment");
        }
        if (tf == null){
            tf = new TestFragment();
        }
        transaction.replace(R.id.id_content, tf, "TestFragment");
        transaction.commit();
    }

动态创建涉及到两个类:FragmentManager和FragmentTransaction。

FragmentManager

FragmentManager是一个抽象类,定义了一些和 Fragment 相关的操作和内部类/接口。

//开启一系列对 Fragments 的操作 
public abstract FragmentTransaction beginTransaction(); 
//FragmentTransaction.commit() 是异步执行的,如果你想立即执行,可以调用这个方法 
public abstract boolean executePendingTransactions(); 
//根据 ID 找到从 XML 解析出来的或者事务中添加的 Fragment 
//首先会找添加到 FragmentManager 中的,找不到就去回退栈里找 
public abstract Fragment findFragmentById(@IdRes int id); 
//跟上面的类似,不同的是使用 tag 进行查找 
public abstract Fragment findFragmentByTag(String tag); 
//弹出回退栈中栈顶的 Fragment,异步执行的 
public abstract void popBackStack(); 
//立即弹出回退栈中栈顶的,直接执行哦 
public abstract boolean popBackStackImmediate(); 
//返回栈顶符合名称的,如果传入的 name 不为空,在栈中间找到了 Fragment,那将弹出这个 Fragment 上面的所有 Fragment 
//有点类似启动模式的 singleTask 的感觉 
//如果传入的 name 为 null,那就和 popBackStack() 一样了 
//异步执行 
public abstract void popBackStack(String name, int flags); 
//同步版的上面 
public abstract boolean popBackStackImmediate(String name, int flags); 
//和使用 name 查找、弹出一样 
//不同的是这里的 id 是 FragmentTransaction.commit() 返回的 id 
public abstract void popBackStack(int id, int flags); 
//你懂得 
public abstract boolean popBackStackImmediate(int id, int flags); 
//获取回退栈中的元素个数 
public abstract int getBackStackEntryCount(); 
//根据索引获取回退栈中的某个元素 
public abstract BackStackEntry getBackStackEntryAt(int index); 
//添加或者移除一个监听器 
public abstract void addOnBackStackChangedListener(OnBackStackChangedListener listener); 
public abstract void removeOnBackStackChangedListener(OnBackStackChangedListener listener); 
//还定义了将一个 Fragment 实例作为参数传递 
public abstract void putFragment(Bundle bundle, String key, Fragment fragment); 
public abstract Fragment getFragment(Bundle bundle, String key); 
//获取 manager 中所有添加进来的 Fragment 
public abstract List<Fragment> getFragments();

我们获取FragmentManager有两种包引入:

//针对4.0之前的方式,能兼容更低的版本,引入的包是v4包,所对应的fragment也要时v4包下的。(推荐)
FragmentManager fm = getSupportFragmentManager();

//针对4.0之后的方式,引入的包是v7包,所对应的fragment也要是v7包下的。
FragmentManager fm = getFragmentManager();

FragmentTransaction

做过数据库的童鞋都有事务的印象,主要针对数据的增删改查。FragmentTransaction就是对Fragment的“增删改查”。FragmentTransaction的相关操作大概有以下相关方法。

  1. add(id, fragment) —— 增加framgent到队列中,并显示该fragment到指定布局中。
    生命周期调用:
    当fragment与activity连接并被建立时(onAttach()、onCreate()被调用过)
    onCreateView()、onActivityCreated()、onStart()、onResume()。
    当fragment与activity未连接并未被建立时(onAttach()、onCreate()未被调用过)
    onAttach()、onCreate()、onCreateView()、onActivityCreated()、onStart()、onResume()。
    注意:同一个Fragmen不能增加到队列两次或多次。
  2. show(fragment) —— 显示队列中的指定framgent。
    生命周期的调用:
    当队列中不存在该fragment时,回调onAttach()、onCreate()。
    当队列中存在该fragment时并被调用过hide(fragment)时,回调onHiddenChange(boolean)。
    其他情况没有回调函数。
  3. replace(id, fragment) —— 先检查队列中是否已经存在,存在就会崩溃,不存在就会进入队列并把其他fragment清出队列,最后显示该fragment到指定布局中。
    生命周期的调用:同add(id, fragment)。
  4. remove(fragment) —— 销毁队列中指定的fragment。
    生命周期调用:
    当队列中不存在该fragment时,不会有任何反应。
    当队列中存在该fragment时,fragment的生命周期执行情况主要依赖是否当前fragment进入到返回栈。
  5. hide(fragment) —— 隐藏队列中指定的fragment,相当于调用视图的.setVisibility(View.GONE)
    生命周期的调用:
    当队列中存在该fragment时,回调onHiddenChange(boolen)
    当队列中不存在该fragment时,回调onAttach()、onCreate()、onHiddenChange(boolen)。
  6. detach(fragment) —— 销毁指定frament的视图,并且该fragment的onCreateView(……)不能再被调用(除非调用attach(fragment)重新连接)
    生命周期的调用:
    当队列中存在该fragment时,回调onDestroyView()
    当队列中不存在该fragment时,回调onAttach()、onCreate()。
  7. attach(fragment) —— 创建指定fragment的视图。标识该fragment的onCreateView(……)能被调用。
    生命周期的调用:
    当队列中存在该fragment时且被调用detach(fragment)时,回调createView()、onActivityCreated()、onResume()。
    当队列中不存在该fragment时,回调onAttach()、onCreate()。
    其他情况没有用。
  8. addToBackStack(string) —— 使本次事务增加的fragment进入当前activity的返回栈中。当前参数是对返回栈的描述,没什么实际用途。传入null即可。
  9. commit() —— 提交本次事务,可在非主线程中被调用。主要用于多线程处理情况。
  10. commitNow() —— 提交本次事务,只在主线程中被调用。 这时候addToBackStack(string)不可用。

针对事务,我们最常用的有两种:

  1. replace() --父容器中只能存在一个fragment,好处就是生命周期好管理。
  2. add(), show(), hide() --父容器中根据还需要add一次,后边根据情况show或者hide,好处就是省内存空间。

Fragment进出栈管理

我们知道在多个activity的情况下按下返回键会返回到上一页,但是一个activity中有多个fragment时按下返回键却直接退出了当前Activity。我们知道Activity有任务栈,用户通过startActivity将Activity加入栈,点击返回按钮将Activity出栈。Fragment也有类似的栈,称为回退栈(Back Stack),回退栈是由FragmentManager管理的(上边谈到FragmentManager使用的相关方法都已经提到)。

加入回退栈

默认情况下,Fragment事务是不会加入回退栈的,如果想将Fragment加入回退栈并实现事物回滚,首先需要在commit()方法之前调用事务的以下方法将其添加到回退栈中:addToBackStack(String tag) //标记本次的回滚操作

弹出回退栈

Fragment的回退非常简单,默认每次只能回退到上一步操作,并不能一次性回退到我们想要的位置,这在开发的时候是不灵活的。所以需要我们来多了解事物回滚的相关原理来解决这样的问题,其实在Fragment回退时,默认调用FragmentManager的popBackStack()方法将最上层的操作弹出回退栈。当栈中有多层时,我们可以根据id或TAG标识来指定弹出到的操作所在层:

  • popBackStack(int id, int flags):其中id表示提交变更时commit()的返回值。
  • popBackStack(String name, int flags):其中name是addToBackStack(String tag)中的tag值。

在上面2个方法里面,都用到了flags,其实flags有两个取值:0或FragmentManager.POP_BACK_STACK_INCLUSIVE。当取值0时,表示除了参数指定这一层之上的所有层都退出栈,指定的这一层为栈顶层;当取值POP_BACK_STACK_INCLUSIVE时,表示连着参数指定的这一层一起退出栈。
如果想要了解回退栈中Fragment的情况,可以通过以下2个方法来实现:

  • getBackStackEntryCount():获取回退栈中Fragment的个数。
  • getBackStackEntryAt(int index):获取回退栈中该索引值下的Fragment。

使用popBackStack()来弹出栈内容的话,调用该方法后会将事物操作插入到FragmentManager的操作队列,只有当轮询到该事物时才能执行。如果想立即执行事物的话,可以使用下面这几个方法:

  • popBackStackImmediate()
  • popBackStackImmediate(String tag)
  • popBackStackImmediate(String tag, int flag)
  • popBackStackImmediate(int id, int flag)

Fragment懒加载

什么是懒加载,单例模式有一个懒汉模式,和这种有异曲同工,即所见即所得,例如页面可见时才加载数据,既节省内存,又能提示用户当前数据正在处理,不必感到迷茫。Fragment的懒加载主要用到setUserVisibleHint()这个方法。

setUserVisibleHint()使用范围必须在一组有序的Fragment时(FragmentPageAdapter)才会起作用,单个Fragment是不起作用的,并且该方法调用不一定在Fragment正常的生命周期之内。见源码注释:
setUserVisibleHint源码

在FragmentPageAdapter中,假如有四张fragment:

初始时:
先执行setUserVisibleHint,将一次性加载在内存中的每个fragement(默认一次性加载两个Fragment)的该方法执行一遍,先默认都初始为false,最后将可见的那个fragment(第一个)初始为true。然后执行Fragment的生命周期:
setUserVisibleHint -------> onAttach、 onCreat…
懒加载初始时声明周期

滑动到第二页时:
此时该fragment的部分生命周期已经走过(例如:onCreatView),但是该fragment的可见情况发生了变化,所以在第二页的时候只执行了setUserVisibleHint,不再执行onCreatView等生命周期方法。不过滑到第二页的时候顺便把第三页的相关方法初始一下:
懒加载滑动到第二页时生命周期

滑到最后一页时:
假如这里一组只有四个Fragment,因为接下来没有第5个Fragment,所以不用进行预加载了,只需要做两步操作:

  1. 把前一页的可见性置为false,把本页的可见性置为true。
  2. 把上上一页的生命周期关闭(内存中只允许放两页,所以要把老的fragment出栈)。

懒加载滑到最后一页时时生命周期
懒加载滑到最后一页时

间接跳转:
假如从第四页直接跳到第一页(这里不是滑动,是导航跳转),可见性上先根据第一页的预加载方式进行逐个初始关闭,把第四页关闭,在把第一页打开。生命周期上先预处理第一页第二页的生命开启,接着将第三页第四页的生命关闭。举一反三其他间接跳转也类似。
懒加载间接跳转生命周期

综上:其实这里只需要考虑两件事:

  1. 设置可见性(初始可见性为false,关闭老页面可见性为false, 开启当前页可见性为true)。
  2. 设置生命周期(预加载新的生命周期,关闭老的生命周期)。

因为这两件事是交替变换,可见性为true但生命周期不一定初始化,此时是不能做一些有关界面的数据填充;生命周期准备好了但是可见性为false,加载的数据用户看不到,资源显的浪费。所以在数据加载时要同时考虑这两种情况,这里设置两个全局变量(mIsPrepare:标识生命周期; mIsVisible :标识可见状态)来标识这两个情况:

@Override
protected void loadData(Bundle savedInstanceState) {
  mIsPrepare = true;
  onLazyLoad();
}

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
  super.setUserVisibleHint(isVisibleToUser);
  mIsVisible = isVisibleToUser;
  onLazyLoad();
}

private void onLazyLoad() {
  if (mIsPrepare) {
    // 初始相关数据,以便于在滑动过程中能看到下一页有预处理数据
    initData();
    if (mIsVisible) {
      // 加载最终数据
      queryData(true);
    }
  }
}

Fragment常见问题及解决方式

以下两个坑是对我印象最深,早在还未毕业在实习的时候与fragment第一次亲密接触就遇到了。

Fragment的getActivity为null

这种情况主要是主Activity被回收,然后在恢复的时候重新实例了一个新的Activity,导致原来的Fragment找不到原来的Activity,既然知道原因了,我们可以从以下三种情况入手:

  1. Activity销毁不缓存任何状态,这样在Activity重置时Fragment也会跟着重置。

    @Override
    protected void onSaveInstanceState(Bundle outState) {
    //super.onSaveInstanceState(outState);
    }
    

    这里说明一下:Activity的onSaveInstanceState是保存Activity被回收时的状态,同理Fragment的也是,因此这里只需把Activity的super.onSaveInstanceState(outState)注释即可,Fragment自行根据情况选择是否注释。

  2. Fragment内部onAttach时把当前Activity的上下文保存起来。(推荐)
    当出现这个问题时,只要在Fragment 中声明一个上下文类型的成员变量,并在onAttach()方法中将getActivity()的值赋给变量,在Fragment中使用到getActivity()时,都是用context即可。也就是说在将Fragment挂载到Activity时,就获得到了这个activity,并将这个activity保存起来了,这样就可以直接使用而不需要每次使用时都调用一遍getActivity()了。

    protected Context context;
    @Override  
        public void onAttach(Activity activity){  
            super.onAttach(activity);  
            context = activity;  
        }
    
  3. 使用全局Application的上下文。

同一容器中多个Fragment重叠

这种情况也是当所属Activity被回收重置时,由于一个容器中多个fragment找不到归属,本来有些是需要隐藏的,结果却都显示出来了。
解决方法也有两种方法:

  1. 不保存Activity的状态,让Fragment重新创建实例化。和上边getActivity()为null的第一种处理方式一样。
  2. 在Activity的onCreate(Bundle savedInstanceState)方法里判断savedInstanceState是否为空,不为空则根据fragment的tag,将fragment的实例找出来,然后根据情况显示还是隐藏。
        if(savedInstanceState != null){
            tf = (TestFragment) fm.findFragmentByTag("TestFragment");
        }
        if (tf == null){
            tf = new TestFragment();
            transaction.add(R.id.id_content, tf, "TestFragment");
            transaction.commit();
        }else{
            transaction.show(tf);
        } 
    

Fragment与Fragment、Activity通信

首先说明一下官方不建议Fragment与Fragment直接进行通信的,这会造成碎片耦合,不易后期维护和扩展,一般都是有Activity作为中间桥梁进行转接。以下是Fragment与Fragment、Fragment与Activity之间通信的方法归总。

Handler消息推送方式

例如Activity向Fragment中传递消息,思想是在Fragment中注册handler,然后将此Handler实例通过Activity的公有方法传递给Activity,最后Activity再合适的情况下触发handler的sendMessage()。具体过程如下:

1.Fragment注册Handler,用于将来的消息接收。

private Handler mHandler = new Handler(){ 
    @Override 
    public void handleMessage(Message msg) { 
        super.handleMessage(msg); 
        Bundle bundle = msg.getData(); 
        strHandler = bundle.getString("hehe"); 
        textView.setText(strHandler+TwoFragment.class.getName()); 
    }
};

2.Activity中暴露公有setHandler方法,用于Fragment将注册的handler传给Activity.

public void setHandler(Handler handler){
    this.handler = handler;
}

3.Fragment将Handler实例传递给Activity

public void onAttach(Context context) {
    super.onAttach(context); 
    MainActivity activity = (MainActivity) getActivity(); 
    activity.setHandler(mHandler); 
}

4.Activity事件响应,发送Handler消息

tvMain.setOnClickListener(new View.OnClickListener() { 
    @Override 
    public void onClick(View v) { 
        //如果是activity相应事件,发送msg 
        if(handler == null){ 
            return; 
        } 
        Message message = new Message(); 
        Bundle bundle = new Bundle(); 
        bundle.putString("hehe","我是activity发送的msg"); 
        message.setData(bundle); 
        handler.sendMessage(message); 
    } 
});

其实不建议用handler处理,因为你会发现他耦合性挺重的,既要在Fragment中注册又要在Activity中handler发射,并且用不好还会出现内存泄露的情况。

广播发送方式

这个应该都明白,假如现在Fragment向Activity通信,我们只需在Activity中注册广播,然后由Fragment发送广播,Activity即可接收到。

如果项目中fragment不是很多话,可以用广播传递,当然要注意广播的注销,方法也比较简单。

事件总线发送方式

我们常用的事件总线如EventBus,EventBus是一款针对Android优化的发布/订阅事件总线。简化了应用程序内各组件间、组件与后台线程间的通信。可以利用EventBus代替广播进行发送,其过程和上边广播发送方式类似,这里就不多说了。

EventBus虽然更加简单,但是采用反射机制,造成性能上问题,并且EventBus不设好标示,将来甚至都不知道消息是从哪里传来的。

使用自定义接口方式

Fragment中创建接口,Activity中实现接口,然后Fragment在需要的地方通过接口实例调用接口方法,从而Activity中即可接收到。

1.Fragment中创建接口

public interface IListener{
    void changerSomething(String msg);
}

2.Fragment中创建接收接口实例方法

public void setListener(IListener listener){
    this.mListener = listener;
}

3.Activity中Fragment实例实现自身接口实例

mFragment.setListener(new Ilistener{
    public void changerSomething(String msg){
        tvMain.setText(msg);
    }
});

4.Fragment中调用接口实例

button.setOnClickListener(new View.OnClickListener() { 
    @Override 
    public void onClick(View v) { 
        if(mListener != null){
            mListener.changerSomething("我是通过接口传递到MainActivity中");
        }
    } 
});

以上几种方式都有缺点,看起来这一种还不错,简单,解耦,并且完全可定制化,但是你会发现每个Fragment都要写这么多代码是很耐受的。所以接下来我们需要对这种方式进行一个完美封装,让其想EventBus一样,一行代码即可搞定。

万能接口方式

这里就不贴代码了,封装嘛,考验一下大家的奇思妙想。

最后

整个Fragment的相关知识点这里做了一个完全总结,文章有点长,需耐心看了,当然Fragment也不仅仅这一点,例如FragmentDialog代替Dialog,更多的需要我们在开发的过程中去挖掘学习。

发布了50 篇原创文章 · 获赞 39 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/li0978/article/details/105671207