码个蛋(codeegg) 第 852 次推文

作者:一包纯牛奶链接:https://juejin.im/post/5e085dafe51d45580769a1eb

码妞看世界

大家普遍对于Fragment懒加载的问题比较关心,其实,对于Fragment懒加载问题的处理由来已久,网上不乏相关的优秀文章。但是,由于Fragment生命周期的原因使得懒加载问题的处理并不是那么的优雅。显然,Google也意识到了问题所在。

因此,在Androidx的库中对于Fragment的生命周期状态的控制进行了深度优化,使得我们更容易的去管控Fragment的生命周期,也使得我们更容易的去处理懒加载问题。但是,前提条件是我们要了解Google对于Fragment做了哪些优化。那么就让我们借此机会一起来探究一下吧!( 懒加载称作延迟加载我觉得更贴切一些,所以下文就统称为延迟加载了。 )

一、Fragment延迟加载的前世

虽然本篇文章是对于Fragment新特性的探究,但是我觉得写文章总要有个因果。也为了照顾一下还不太了解什么是延迟加载的同学。我们还是先来了解一下延迟加载,顺便回顾一下Fragment延迟加载的旧方案。

1. 为什么要对Fragment做延迟加载?

首先,我们要搞清楚一个问题。“Fragment延迟加载“中的“延迟”并不指的是延迟加载Fragment,而是延迟加载Fragment中的数据。对于Fragment的使用通常我们会结合ViewPager,ViewPager会默认在当前页面的左右两边至少预加载一个页面以保证ViewPager的流畅性。

我们假设在ViewPager的所有Fragment中都存在网络请求,当我们打开这个页面的时候由于ViewPager的预加载原因,即使在其它Fragment不可见的情况下也会去进行网络请求加载数据。而如果此时用户根本就没有去滑动ViewPager就退出了应用或者切换到了其他页面。那么对于这个不可见的Fragment中的网络请求岂不是既浪费了流量也浪费了手机和服务器的性能?

那么此时有的同学就有问题了。你就不能在Fragment显示的时候去加载数据吗?

问的好!在解答之前我们先来看下Fragment的生命周期。


想必这张图大家应该都非常熟悉了。

当Fragment被预加载的时候,此Fragment的生命周期会从onAttach执行到onResume。显然我们无法通过Fragment的生命周期来控制Fragment的延迟加载。

那么该怎么办呢?我们且往下看。

2. 如何处理Fragment的延迟加载?

通过上一小节的分析我们知道想要在Fragment的生命周期中处理延迟加载的问题显然是走不通的。所以想要处理Fragment的延迟加载就需要另想它法了。

还好,在Fragment中为我们提供了一个 setUserVisibleHint(isVisibleToUser: Boolean) 的方法,这个方法中有一个 isVisibleToUser 的boolean类型的参数,其意义表示当前的Fragment是否对用户可见。

因此,对于Fragment的延迟加载我们便可以通过这个方法来展开。既然要使用setUserVisibleHint(isVisibleToUser: Boolean)那么就应该知道这个方法的调用时机。我们写一个ViewPager嵌套Fragment的例子来打印下日志:


注:上图打印的日志中”position:0“表示当前Fragment,“position:1”表示预加载的Fragment,下同。

可见该方法是在Fragment的onAttach之前就已经被调用了。因此,对于延迟加载我们可以在setUserVisibleHint(isVisibleToUser: Boolean)方法及onViewCreated(view: View, savedInstanceState: Bundle?)添加标志位来控制是否加载数据。我们来看下代码:

abstract class BaseLazyFragment : Fragment() {    /**     * 当前Fragment状态是否可见     */    private var isVisibleToUser: Boolean = false    /**     * 是否已创建View     */    private var isViewCreated: Boolean = false    /**     * 是否第一次加载数据     */    private var isFirstLoad = true    override fun setUserVisibleHint(isVisibleToUser: Boolean) {        super.setUserVisibleHint(isVisibleToUser)        this.isVisibleToUser = isVisibleToUser        onLazyLoad()    }    private fun onLazyLoad() {        if (isVisibleToUser && isViewCreated && isFirstLoad) {            isFirstLoad = false            lazyLoad()        }    }    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {        super.onViewCreated(view, savedInstanceState)        isViewCreated = true        onLazyLoad()    }    protected abstract fun lazyLoad()}

我们通过在Fragment中添加了三个标志位实现了延迟加载的功能。我们到TestFragment尝试一下:

class TestFragment : BaseLazyFragment() {    private var position: Int = 0    override fun setUserVisibleHint(isVisibleToUser: Boolean) {        super.setUserVisibleHint(isVisibleToUser)        val bundle = arguments        position = bundle!!.getInt(KEY_POSITION)    }    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {        val cardView = CardView(inflater, container)        cardView.bind(Card.fromBundle(arguments!!),position)        return cardView.view    }    companion object {        private const val KEY_POSITION = "position"        fun getInstance(card: Card, position: Int): TestFragment {            val fragment = TestFragment()            val bundle = card.toBundle()            bundle.putInt(KEY_POSITION, position)            fragment.arguments = bundle            return fragment        }    }    override fun lazyLoad() {        showToast("Fragment$position is loading data")    }    private fun showToast(content: String) {        Toast.makeText(context, content, Toast.LENGTH_SHORT).show()    }}

我们来看下效果:


嗯!立竿见影,只有当Fragment完全显示出来的时候loading data的操作才被执行。这种延迟加载的方案在Androidx 1.1.0版本以前被广泛应用。而在Androidx 1.1.0版本中,Google对于Fragment进行了优化处理,使得延迟加载也有了新的解决方案。

二、Fragment的setMaxLifecycle探究

上一节中我们讲到因为ViewPager的预加载机制以及Fragment的生命周期无法得以控制,我们不得不通过 setUserVisibleHint(isVisibleToUser: Boolean) 和 onViewCreated(view: View, savedInstanceState: Bundle?) 方法以及添加三个标志位来处理延迟加载,显然这样的代码并不优雅。

当我们将Android项目迁移到Androidx 并将 Androidx 版本升级到1.1.0之后发现,我们第一节中用到的setUserVisibleHint(isVisibleToUser: Boolean)方法已被标记为废弃了!


/** * ... 省略其它注释 * @deprecated Use {@link FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)} * instead. */@Deprecatedpublic void setUserVisibleHint(boolean isVisibleToUser) {    if (!mUserVisibleHint && isVisibleToUser && mState < STARTED            && mFragmentManager != null && isAdded() && mIsCreated) {        mFragmentManager.performPendingDeferredStart(this);    }    mUserVisibleHint = isVisibleToUser;    mDeferStart = mState < STARTED && !isVisibleToUser;    if (mSavedFragmentState != null) {        // Ensure that if the user visible hint is set before the Fragment has        // restored its state that we don't lose the new value        mSavedUserVisibleHint = isVisibleToUser;    }}

并且从注释中可以看到使用 FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State) 方法来替换setUserVisibleHint方法。setMaxLifecycle实在Androidx 1.1.0中新增加的一个方法。

setMaxLifecycle从名字上来看意思是设置一个最大的生命周期,因为这个方法是在 FragmentTransaction 中,因此我们可以知道应该是为Fragment来设置一个最大的生命周期。我们来看下setMaxLifecycle的源码:

/** * Set a ceiling for the state of an active fragment in this FragmentManager. If fragment is * already above the received state, it will be forced down to the correct state. * * 

The fragment provided must currently be added to the FragmentManager to have it's

* Lifecycle state capped, or previously added as part of this transaction. The * {@link Lifecycle.State} passed in must at least be {@link Lifecycle.State#CREATED}, otherwise * an {@link IllegalArgumentException} will be thrown. * * @param fragment the fragment to have it's state capped. * @param state the ceiling state for the fragment. * @return the same FragmentTransaction instance */@NonNullpublic FragmentTransaction setMaxLifecycle(@NonNull Fragment fragment, @NonNull Lifecycle.State state) { addOp(new Op(OP_SET_MAX_LIFECYCLE, fragment, state)); return this;}

这个方法接收一个Fragment参数和一个Lifecycle的状态参数。Lifecycle是Jetpack中很重要的一个库,它具有对Activity和Fragment生命周期感知能力,相信很多同学都应该对Lifecycle都略知一二。

在Lifecycle的State中定义了五种生命周期状态,如下:

public enum State {    /**     * Destroyed state for a LifecycleOwner. After this event, this Lifecycle will not dispatch     * any more events. For instance, for an {@link android.app.Activity}, this state is reached     * right before Activity's {@link android.app.Activity#onDestroy() onDestroy} call.     */    DESTROYED,      /**     * Initialized state for a LifecycleOwner. For an {@link android.app.Activity}, this is     * the state when it is constructed but has not received     * {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} yet.     */    INITIALIZED,      /**     * Created state for a LifecycleOwner. For an {@link android.app.Activity}, this state     * is reached in two cases:     *      *     after {@link android.app.Activity#onCreate(android.os.Bundle) onCreate} call;     *     right before {@link android.app.Activity#onStop() onStop} call.     *      */    CREATED,      /**     * Started state for a LifecycleOwner. For an {@link android.app.Activity}, this state     * is reached in two cases:     *      *     after {@link android.app.Activity#onStart() onStart} call;     *     right before {@link android.app.Activity#onPause() onPause} call.     *      */    STARTED,      /**     * Resumed state for a LifecycleOwner. For an {@link android.app.Activity}, this state     * is reached after {@link android.app.Activity#onResume() onResume} is called.     */    RESUMED;      /**     * Compares if this State is greater or equal to the given {@code state}.     *     * @param state State to compare with     * @return true if this State is greater or equal to the given {@code state}     */    public boolean isAtLeast(@NonNull State state) {        return compareTo(state) >= 0;    }  }

而在 setMaxLifecycle 中接收的生命周期状态要求不能低于 CREATED,否则会抛出一个 IllegalArgumentException 的异常。当传入参数为DESTROYED或者INITIALIZED时则会抛出如下图的异常:


因此除去这两个生命周期外,仅剩下CREATED、STARTED、RESUMED三个生命周期状态的参数可用,那么接下来我们就逐个来研究这三个参数的效果。

1. 不设置setMaxLifecycle

我们先来看下在不设置setMaxLifecycle的时候添加一个Fragment的状态,以便和后边的情况进行对比。首先我们在Activity中添加一个Fragment,代码如下:

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.commit()

启动Activity,我们将该Fragment生命周期的日志打印出来如下:


可以看到这个Fragment生命周期从onAttach一直执行到了onResume, 并且在Activity中成功显示出了Fragment


2. setMaxLifecycle与CREATED

接下来,我们将maxLifecycle设置为CREATED:

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.CREATED)fragmentTransaction.commit()

再来看日志输出:


可以看到该Fragment的生命周期仅仅执行到了onCreate就没再往下执行了。并且Activity中没有加载出来当前Fragment。

那么现在问题来了,假设Fragment已经执行到了onResume, 此时再为Fragment设置一个CREATED的最大生命周期会出现什么样的情况呢?我们通过日志来验证一下:


从日志中可以看到已经执行了onResume的Fragment,将其最大生命周期设置为CREATED后会执行onPause->onStop->onDestoryView。也就是回退到了onCreate的状态。

3. setMaxLifecycle与STARTED

接下来,我们将maxLifecycle设置为STARTED:

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED)fragmentTransaction.commit()

日志输出如下:


可以看到Fragment的生命周期执行到了onStart,并且Activity中成功显示出了当前fragment。

同样,假设Fragment已经执行到了onResume方法再为其设置最大生命周期为 STARTED 会怎样呢?来看日志:


可以看到,设置最大生命周期STARTED后Fragment执行了onPause方法,也就是生命周期退回到了onStart。

4. setMaxLifecycle与RESUMED

最后,我们将maxLifecycle设置为RESUMED:

fragment = TestLifecycleFragment.getInstance(Card.DECK[0], 0)val fragmentTransaction = supportFragmentManager.beginTransaction()fragmentTransaction.add(R.id.ll_fragment, fragment)fragmentTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED)fragmentTransaction.commit()

可以看到此时和第一种情况一样的效果,Fragment的生命周期执行到了 onResume。

而对于已经执行了onResume后的Fragment,再去设置最大生命周期为RESUMED会怎么样呢?因为当前Fragment已经是RESUMED状态了,所以不会再去执行任何代码。

到这里我们可以得出一个结论:

通过setMaxLifecycle方法可以精确控制Fragment生命周期的状态,如果Fragment的生命周期状态小于被设置的最大生命周期,则当前Fragment的生命周期会执行到被设置的最大生命周期;

反之,如果Fragment的生命周期状态大于被设置的最大生命周期,那么则会回退到被设置的最大生命周期。

有了这一结论,在ViewPager中便可以对Fragment的生命周期进行控制,以此来更方便的实现延迟加载功能了。

三、Fragment延迟加载的今生

1、延迟加载新方案之于ViewPager

通过上一小节的分析我们知道了可以通过setMaxLifecycle来设置Fragment的最大生命周期,从而可以实现ViewPager中Fragment的延迟加载。当然,关于生命周期状态处理的操作无需我们自己实现,在Androidx 1.1.0版本中的FragmentStatePagerAdapter已经帮我们实现了,只需要在使用时候传进去相应的参数即可。

FragmentStatePagerAdapter的构造方法接收两个参数,如下:

/** * Constructor for {@link FragmentStatePagerAdapter}. * * If {@link #BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT} is passed in, then only the current * Fragment is in the {@link Lifecycle.State#RESUMED} state, while all other fragments are * capped at {@link Lifecycle.State#STARTED}. If {@link #BEHAVIOR_SET_USER_VISIBLE_HINT} is * passed, all fragments are in the {@link Lifecycle.State#RESUMED} state and there will be * callbacks to {@link Fragment#setUserVisibleHint(boolean)}. * * @param fm fragment manager that will interact with this adapter * @param behavior determines if only current fragments are in a resumed state */public FragmentStatePagerAdapter(@NonNull FragmentManager fm,        @Behavior int behavior) {    mFragmentManager = fm;    mBehavior = behavior;}

第一个FragmentManager 参数不必多说,第二个参数时一个枚举类型的Behavior参数,其可选值如下:

@Retention(RetentionPolicy.SOURCE)@IntDef({BEHAVIOR_SET_USER_VISIBLE_HINT, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT})private @interface Behavior { }

当behavior为BEHAVIOR_SET_USER_VISIBLE_HINT时,Fragment改变的时候,setUserVisibleHint方法会被调用,也就是这个参数其实是为了兼容以前的老代码。并且BEHAVIOR_SET_USER_VISIBLE_HINT参数已经被置为废弃。所以我们的可选参数只剩下了BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT。

当behavior为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时意味着只有当前显示的Fragment会被执行到onResume,而其它Fragment的生命周期都只会执行到onStart。

这一功能时如何实现的呢?我们追随BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT的脚步找到了setPrimaryItem方法,这个方法的作用是设置ViewPager当前显示的Item,其源码如下:

public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) {    Fragment fragment = (Fragment)object;    if (fragment != mCurrentPrimaryItem) {        if (mCurrentPrimaryItem != null) {            mCurrentPrimaryItem.setMenuVisibility(false);            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {                if (mCurTransaction == null) {                    mCurTransaction = mFragmentManager.beginTransaction();                }                mCurTransaction.setMaxLifecycle(mCurrentPrimaryItem, Lifecycle.State.STARTED);            } else {                mCurrentPrimaryItem.setUserVisibleHint(false);            }        }        fragment.setMenuVisibility(true);        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {            if (mCurTransaction == null) {                mCurTransaction = mFragmentManager.beginTransaction();            }            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.RESUMED);        } else {            fragment.setUserVisibleHint(true);        }          mCurrentPrimaryItem = fragment;    }  }

这段代码非常简单易懂,mCurrentPrimaryItem是当前正在显示的item,fragment是接下来要显示的item。可以看到当mBehavior 为BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时,mCurrentPrimaryItem的最大生命周期被设置为了STARTED,而fragment的最大生命周期则被设置为了RESUMED。

而当mBehavior为BEHAVIOR_SET_USER_VISIBLE_HINT时仍然会调用setUserVisibleHint方法,这种情况就不再讨论,因为BEHAVIOR_SET_USER_VISIBLE_HINT也已经被废弃掉了。 那么我们着重来分析一下BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT时的情况:

mCurrentPrimaryItem是当前显示的Fragment,所以该Fragment必然已经执行到了onResume方法,而此时为其设置了最大生命周期STARTED,那么mCurrentPrimaryItem必然会执行onPause退回到STARTED状态。 而fragment当前生命周期状态为onStart,当为其设置了RESUME的最大生命周期状态后,fragment必然会执行onResume方法进入RESUMED状态。

知道了这一结论后,我们再去进行懒加载的控制是不是就异常简单了?此时我们只需要一个flag去标志是否是第一次加载数据就可以了。

因此,懒加载的实现可以如下:

abstract class TestLifecycleFragment : Fragment() {    private var isFirstLoad = true    override fun onResume() {        super.onResume()        if (isFirstLoad) {          isFirstLoad = false            loadData()        }    }    abstract fun loadData()}

2、延迟加载之于ViewPager2

(有机会专门分享一篇讲解ViewPager2的文章。)在分析offScreenPageLimit时候得出过这样一个结论:

ViewPager2的offScreenPageLimit默认值为OFFSCREEN_PAGE_LIMIT_DEFAULT,当setOffscreenPageLimit为OFFSCREEN_PAGE_LIMIT_DEFAULT时候会使用RecyclerView的缓存机制。

默认只会加载当前显示的Fragment,而不会像ViewPager一样至少预加载一个item。

当切换到下一个item的时候,当前Fragment会执行onPause方法,而下一个Fragment则会从onCreate一直执行到onResume。当再次滑动回第一个页面的时候当前页面同样会执行onPuase,而第一个页面会执行onResume。

也就是说在ViewPager2中,默认关闭了预加载机制。没有了预加载机制再谈延迟加载其实也没有任何意义了。所以关于ViewPager2的延迟加载也就不用多说了吧?

只需要将网络请求放到onStart中即可。相信随着ViewPager2的普及延迟加载的概念也会慢慢淡出开发者的视线。

2020/1/4补充:

如果为ViewPager2设置了offScreenPageLimit(1)那结果会是怎样的呢?我们来看日志:


从日志中可以看到ViewPager2预加载了一个Fragment,并且预加载的Fragment的生命周期仅仅执行到了onStart。所以此处我们可以猜测在FragmentStateAdapter中一定设置了setMaxLifecycle(fragment, STARTED),具体源码不再贴出,大家可以自行查看。因此,此时处理懒加载问题其实和ViewPager的懒加载新方案如出一辙了,仅仅需要添加一个boolean值即可。

三、总结

本篇文章对于Fragment的延迟加载进行了深入的探究,并且了解了在Androidx 1.1.0版本中对Fragment最大生命周期状态的控制,从而探究出了Fragment延迟加载的新方案。

对于ViewPager2,因其默认不会进行预加载因此也就意味着我们无需处理ViewPager2的延迟加载问题。好了,这一篇花费了我两个周末(其实是上周末写了一半偷了个懒)的文章到此就结束了,如果你从中学有所收获那么请你不要吝啬留下你的赞。

相关文章:

  • Retrofit 用了那么久,动态代理还不明白

  • 2019年互联网公司福利缩水指南

  • 再见2019!你好2020!

今日问题:

Fragment用得溜嘛?

专属升级社区:《这件事情,我终于想明白了》

判断当前界面是该fragment_学不动也要学!探究Fragment延迟加载的前世今生相关推荐

  1. 学弟学妹:大佬们,别劝了,学不动了,学不动了

    橡皮擦,一个逗趣的互联网高级网虫,为你带来新职场故事,搬来程序员敲门砖. 逛一逛CSDN的热榜,成了每天的日常操作,这里总是充满着乐趣. 这不,今天又看到一篇文章. <奉劝那些刚参加工作的学弟学 ...

  2. java图片不动了_JDK 12又来了,我学不动了...

    原标题:JDK 12又来了,我学不动了... 写在前面 看到 JDK 12又发布了,萌新不知不觉感觉瑟瑟发抖,从 Java 1.8的函数式编程思维和范式 到 Java 1.9的模块化特性的加持 以及还 ...

  3. c++mfc编写实验_零基础学Windows窗口图形界面编程(不用MFC),菜鸟学完变身高手,敢与专业媲美...

    又一门新的计算机课上线啦! 什么课?看下面视频(以下视频是本课第一讲) 用什么语言 我们声明一下(这个在我们课上也跟同学强调):不要为用什么语言掐架!那是初学者的行为,为高手所不为. 第二,我们这门课 ...

  4. 学不动的前端人,该如何跟上前端技术的发展

    总有前端同学问我,前端技术更新太快,学不动了啊,有什么好办法? 说实话,除了学,还真没啥办法. 回看2018年,微信小程序就不说了.支付宝小程序.百度小程序还来凑热闹.React Native.Flu ...

  5. 再见,前端!别更新了,我是学不动了

    01.前端饱和了? 随便打开一个招聘网站,以"前端"为关键字进行搜索,都会看到很多的招聘信息,而且薪资都很高. 据国内权威数据统计,未来五年,我国信息化人才总需求量高达 1500万 ...

  6. 前端学不动了怎么办?3年,5年,10年,都需要规划!

    前端学不动的根本原因是目标感不强或者根本就没有目标.在学习前端之前,我们需要先思考我们要成为一个什么样前端工程师.接下来我们以时间线的方式,来告诉我们什么时间该做什么事儿.本文以普通人为例,也是想给普 ...

  7. ui界面设计课程有哪些:ui设计所学课程

    今天分享的是ui界面设计课程有哪些:ui设计所学课程你知道都要学一些什么吗?表妹高考完说上不了大学就要学习UI设计,在家闹了好久,前几天来我家,我问她ui界面设计课程有哪些:ui设计所学课程你知道都要 ...

  8. 想学python编程-想学Python编程?你真的适合吗?

    原标题:想学Python编程?你真的适合吗? 有的人说我想学什么.我想干什么,很多时候都是头脑发热,单凭一腔热血,可是这样的路即便走上去你又能坚持多久呢?所以,每每有人问我学Python编程怎么样,我 ...

  9. 学python能做什么类型的工作-学Python要先学什么?Python入门方法

    学Python要先学什么?对于零基础的学员来说没有任何的编程基础,应该学习Python基础:计算机组成原理.Python开发环境.Python变量.流程控制语句.高级变量类型.函数应用.文件操作.面向 ...

最新文章

  1. Java 面试题基础概念收集(高级)
  2. Http和Https对比
  3. 命令行避免输入错误文件名_GitHub 60000+ Star 登顶,命令行的艺术
  4. LeetCode 469. 凸多边形(向量叉积)
  5. [AGC003F] Fraction of Fractal 矩阵快速幂
  6. 一起谈.NET技术,ASP.NET MVC Routing概述
  7. 【转贴】谈谈企业经营管理中的八大黑洞
  8. 基于Redis消息的订阅发布应用场景
  9. 【NLP】Transformer框架原理详解-1
  10. js去掉url中的域名的方法
  11. vue实现图片切换效果
  12. TM1637数码管显示
  13. java 英文日期转数字_DatePickerDialog的月份为英文转成数字
  14. Linux中编辑视频字幕
  15. 楚航科技推出车载生命体征探测雷达 提升座舱安全系数
  16. 思维导图XMind中如何换行
  17. 副产品举例_联产品、副产品和等级产品的定义及其之间的联系和区别?
  18. 摄像头驱动_摄像头驱动程序必需的11个ioctl及摄像头数据的获取过程
  19. 第十五个年头了,石墨烯电池出来了吗?
  20. New Internet:大数据挖掘

热门文章

  1. Mysql的垂直分表-新建
  2. Linux的find grep
  3. 小红帽linux操作教程_【免费】Linux从入门到精通教程!
  4. python中random是什么意思_Python代码中的“importrandom”是什么意思?
  5. angularjs 读取mysql_如何使用AngularJS PHP从MySQL获取数据
  6. vs需要迁移_这可能是目前最全面的无服务器迁移实践
  7. 双通道和单通道区别_实测内存通道的区别:单通道比双通道内存更有优势?
  8. python适配器模式角色_Python设计模式之适配器模式原理与用法详解
  9. 【Nginx】应用静态化配置
  10. 【若依(ruoyi)】向DAO中传递动态参数