【Android Framework系列】第15章 Fragment+ViewPager与Viewpager2相关原理

news2024/11/29 0:17:50

1 前言

上一章节【Android Framework系列】第14章 Fragment核心原理(AndroidX版本)我们学习了Fragment的核心原理,本章节学习常用的Fragment+ViewPager以及Fragment+ViewPager2的相关使用和一些基本的源码分析。

在这里插入图片描述

2 Fragment+ViewPager

我们常用的两个PagerAdapter的实现类,也就是FragmentStatePagerAdapterFragmentPagerAdapter,今天,我们就来学习一下它们的使用方法,并进行对比。

2.1 FragmentPagerAdapter 和 FragmentStatePagerAdapter 的区别

1. fragments对象的处理:
FragmentPagerAdapter:范围外fragments会保存在内存中(detach),但是fragment对应的View会被销毁
FragmentStatePagerAdapter:范围外fragments不会保存在内存中(remove),View也会被销毁。
2. 状态的处理:
FragmentPagerAdapter:范围外fragments对应的SavedState会保存
FragmentStatePagerAdapter:只保存范围内fragments对应的SavedState。这个SavedStateFragment的生命周期回调中供外部传参数,和Activity类似。
3. 适用场景:相同数量的fragmentsFragmentPagerAdapter内存较大,但页面切换更友好;FragmentStatePagerAdapter内存占用少,页面切换稍差。

因此FragmentPagerAdapter适用于Fragment数量少的情况,FragmentStatePagerAdapter适用于Fragment数量多的情况。

我们首先来看下FragmentPagerAdapter

	@Override
    public Object instantiateItem(ViewGroup container, int position) {
        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        final long itemId = getItemId(position);

        // Do we already have this fragment?
        //判断请求的Fragment是否已经被生成过
        String name = makeFragmentName(container.getId(), itemId);
        Fragment fragment = mFragmentManager.findFragmentByTag(name);
        if (fragment != null) {
            if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment);
            // 当前缓存有则直接使用
            mCurTransaction.attach(fragment);
        } else {
            fragment = getItem(position); 
            //调用这个方法来生成新的Fragment
            if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment);
            // 将新生成的Fragment存储起来,以便以后再次用到时,直接attach()
            mCurTransaction.add(container.getId(), fragment,
                    makeFragmentName(container.getId(), itemId)); 
        }
   		if (fragment != mCurrentPrimaryItem) {
            fragment.setMenuVisibility(false);
            if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
                mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
            } else {
                fragment.setUserVisibleHint(false);
            }
        }

        return fragment;
    }
    
    @Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object
                + " v=" + fragment.getView());
        mCurTransaction.detach(fragment);
        if (fragment.equals(mCurrentPrimaryItem)) {
            mCurrentPrimaryItem = null;
        }
    }

FragmentPagerAdapterdestroyItem方法中调用了detach()只是改变Fragment状态,说明只有消除整个adapter时候,才能将生成的Fragment都消除掉,不然就会直接在内存中。

接下来我们对比FragmentStatePagerAdapter

  @Override
    public Object instantiateItem(@NonNull ViewGroup container, int position) {
        // If we already have this item instantiated, there is nothing
        // to do.  This can happen when we are restoring the entire pager
        // from its saved state, where the fragment manager has already
        // taken care of restoring the fragments we previously had instantiated.
        // mFragments中对应位置有Fragment的情况下直接返回
        if (mFragments.size() > position) {
            Fragment f = mFragments.get(position);
            if (f != null) {
                return f;
            }
        }

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }

        Fragment fragment = getItem(position);
        if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment);
        if (mSavedState.size() > position) {
            Fragment.SavedState fss = mSavedState.get(position);
            if (fss != null) {
                fragment.setInitialSavedState(fss);
            }
        }
        while (mFragments.size() <= position) {
            mFragments.add(null);
        }
        fragment.setMenuVisibility(false);
        if (mBehavior == BEHAVIOR_SET_USER_VISIBLE_HINT) {
            fragment.setUserVisibleHint(false);
        }

        mFragments.set(position, fragment);
        mCurTransaction.add(container.getId(), fragment);

        if (mBehavior == BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
            mCurTransaction.setMaxLifecycle(fragment, Lifecycle.State.STARTED);
        }

        return fragment;
    }
    
  	@Override
    public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
        Fragment fragment = (Fragment) object;

        if (mCurTransaction == null) {
            mCurTransaction = mFragmentManager.beginTransaction();
        }
        if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object
                + " v=" + ((Fragment)object).getView());
        while (mSavedState.size() <= position) {
            mSavedState.add(null);
        }
        mSavedState.set(position, fragment.isAdded()
                ? mFragmentManager.saveFragmentInstanceState(fragment) : null);
        mFragments.set(position, null);

		// 缓存中移除fragment,下次使用得重新创建
        mCurTransaction.remove(fragment);
        if (fragment.equals(mCurrentPrimaryItem)) {
            mCurrentPrimaryItem = null;
        }
    }

FragmentStatePagerAdapterdestroyItem方法调用的时候把Fragment移除了,因此下次使用需要重新创建Fragment

3 Fragment+ViewPager2

ViewPager2Android Jetpack库中的一个组件,是用于在应用程序中实现页面切换和滑动效果的容器。其实ViewPager2本身继承自RecyclerView。可以复习下我们的【Android Framework系列】第12章 RecycleView相关原理及四级缓存策略分析

3.1 ViewPager2的作用和用途

3.1.1 ViewPager2使用场景

是一个功能强大的滑动容器,可以应用于多种场景中,提供了灵活的页面切换和布局定制功能,使得应用程序界面更加丰富和交互性强,可以用于以下场景:

  1. 实现引导页或欢迎页
    ViewPager2可以用于创建引导页或欢迎页,让用户通过滑动浏览介绍应用程序功能或展示欢迎内容。

  2. 创建图片浏览器
    ViewPager2可以用于创建图片浏览器,允许用户通过滑动来切换不同的图片,并支持缩放和手势交互。

  3. 构建轮播图
    ViewPager2非常适合构建轮播图功能,可以通过适配器动态加载不同的轮播项,并提供自动循环滚动的功能。

  4. 实现选项卡式布局
    结合TabLayoutViewPager2可以用于创建选项卡式布局,让用户通过滑动选项卡来切换不同的内容页面。

  5. 创建垂直滑动页面
    ViewPager不同,ViewPager2支持垂直方向的滑动,因此可以用于创建垂直滑动的页面布局,例如垂直滑动的导航菜单或垂直的新闻列表。

  6. 实现分页数据展示
    ViewPager2可以用于展示分页数据,例如将大量数据按页加载并在每一页中展示一部分内容。

  7. 嵌套滑动布局
    ViewPager2可以与其他滑动组件(如RecyclerView)嵌套使用,实现复杂的滑动布局结构。

  8. 实现自定义的滑动效果
    通过使用自定义的转换器(Transformer),可以实现各种炫酷的页面切换效果,例如渐变、缩放、旋转等。

3.1.2 ViewPager2相较于ViewPager的改进和优势

ViewPager2是对ViewPager的改进版本,提供了更好的性能、更灵活的适配器和更丰富的功能。它是构建滑动页面布局的首选组件,可以在应用程序中实现各种滑动页面的需求,并提供更好的用户体验,大致有以下几点改进和优势:

  1. 支持垂直滑动
    ViewPager2是在ViewPager的基础上进行改进的,最显著的改进之一是支持垂直滑动。而在ViewPager中,只支持水平滑动。这使得ViewPager2在创建垂直布局或特定场景下的垂直滑动功能更加方便和灵活。

  2. 更好的性能和稳定性
    ViewPager2内部实现使用了RecyclerView作为容器,而不再依赖于ViewPager的实现方式。这使得ViewPager2具有RecyclerView的优势,例如更好的性能和内存管理更流畅的滑动体验及更好的布局回收和复用机制。同时,ViewPager2还解决了ViewPager一些已知的问题和不稳定性,如条目位置错乱、刷新数据的不及时等。

  3. 支持使用Fragment作为页面
    ViewPager不同,ViewPager2直接支持使用Fragment作为页面,而无需通过FragmentPagerAdapterFragmentStatePagerAdapter进行适配。这简化了页面管理和生命周期处理,并提供了更直观和一致的使用体验。

  4. 更灵活的适配器
    ViewPager2引入了新的适配器接口,即RecyclerView.Adapter的子类RecyclerView.Adapter。这使得适配器的创建和管理更加灵活,同时提供了更多的功能和扩展性。

  5. 更丰富的功能和接口
    ViewPager2提供了许多新的功能和接口,例如支持页面预加载、更强大的页面切换动画支持、更丰富的回调接口等。这些功能和接口使得开发者能够更好地控制和定制ViewPager2的行为和外观。

3.2 FragmentStateAdapter

上面我们了解了ViewPager2对比ViewPager的一些区别和优化点,下面我们继续看一下ViewPager2对应的Adapter
我们知道ViewPager2继承自RecyclerView,那么它所对应的FragmentStateAdapter必定继承自RecyclerView.Adapter,这点小伙伴们应该能理解。那么我们在RecyclerView中的每个Item加入Fragment作为容器展示。
RecyclerView.Adapter关注的是ViewHolder的复用,但是在FragmentStateAdapter中的Framgent是不会复用的,即有多少个item就应该创建多少个Fragment,那么这其中是如何转换的呢?

我们先来看看FragmentStateAdapter源码:

public abstract class FragmentStateAdapter extends
        RecyclerView.Adapter<FragmentViewHolder> implements StatefulAdapter {
        
    // 通过FragmentStateAdapter声明中的泛型可以知道,
    // ViewPager2之所以能够在RecyclerView的基础上能对外屏蔽对ViewHolder的使用,
    // 其内部是借助FragmentViewHolder实现的,其内部就new了一个FrameLayout。
    @Override
    public final FragmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return FragmentViewHolder.create(parent);
    }

    @Override
    public final void onBindViewHolder(final @NonNull FragmentViewHolder holder, int position) {
        final long itemId = holder.getItemId();
        final int viewHolderId = holder.getContainer().getId();
        final Long boundItemId = itemForViewHolder(viewHolderId); // item currently bound to the VH
        if (boundItemId != null && boundItemId != itemId) {
            removeFragment(boundItemId);
            mItemIdToViewHolder.remove(boundItemId);
        }

        mItemIdToViewHolder.put(itemId, viewHolderId); // this might overwrite an existing entry
        // 内部会最终回调到createFragment用来创建当前Fragment
        ensureFragment(position);

        /** Special case when {@link RecyclerView} decides to keep the {@link container}
         * attached to the window, but not to the view hierarchy (i.e. parent is null) */
        final FrameLayout container = holder.getContainer();
        if (ViewCompat.isAttachedToWindow(container)) {
            if (container.getParent() != null) {
                throw new IllegalStateException("Design assumption violated.");
            }
            container.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
                @Override
                public void onLayoutChange(View v, int left, int top, int right, int bottom,
                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
                    if (container.getParent() != null) {
                        container.removeOnLayoutChangeListener(this);
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }
		
		// 回收已经不在item集合中的Fragment,节省内存开销
        gcFragments();
    }

   	private void ensureFragment(int position) {
        long itemId = getItemId(position);
        if (!mFragments.containsKey(itemId)) {
            // TODO(133419201): check if a Fragment provided here is a new Fragment
            Fragment newFragment = createFragment(position);
            newFragment.setInitialSavedState(mSavedStates.get(itemId));
            mFragments.put(itemId, newFragment);
        }
    }
    
    void gcFragments() {
        if (!mHasStaleFragments || shouldDelayFragmentTransactions()) {
            return;
        }

        // Remove Fragments for items that are no longer part of the data-set
        Set<Long> toRemove = new ArraySet<>();
        for (int ix = 0; ix < mFragments.size(); ix++) {
            long itemId = mFragments.keyAt(ix);
            if (!containsItem(itemId)) {
                toRemove.add(itemId);
                mItemIdToViewHolder.remove(itemId); // in case they're still bound
            }
        }

        // Remove Fragments that are not bound anywhere -- pending a grace period
        if (!mIsInGracePeriod) {
            mHasStaleFragments = false; // we've executed all GC checks

            for (int ix = 0; ix < mFragments.size(); ix++) {
                long itemId = mFragments.keyAt(ix);
                if (!isFragmentViewBound(itemId)) {
                    toRemove.add(itemId);
                }
            }
        }

        for (Long itemId : toRemove) {
            removeFragment(itemId);
        }
    }


	// onViewAttachToWindow的时候调用placeFragmentInViewHolder,
	// 将FragmentViewHolder的container与当前Fragment绑定
    @Override
    public final void onViewAttachedToWindow(@NonNull final FragmentViewHolder holder) {
        placeFragmentInViewHolder(holder);
        gcFragments();
    }

 	void placeFragmentInViewHolder(@NonNull final FragmentViewHolder holder) {
        Fragment fragment = mFragments.get(holder.getItemId());
        if (fragment == null) {
            throw new IllegalStateException("Design assumption violated.");
        }
        FrameLayout container = holder.getContainer();
        View view = fragment.getView();

        if (!fragment.isAdded() && view != null) {
            throw new IllegalStateException("Design assumption violated.");
        }

        // { f:added, v:notCreated, v:notAttached} -> schedule callback for when created
        if (fragment.isAdded() && view == null) {
            scheduleViewAttach(fragment, container);
            return;
        }

        // { f:added, v:created, v:attached } -> check if attached to the right container
        if (fragment.isAdded() && view.getParent() != null) {
            if (view.getParent() != container) {
                addViewToContainer(view, container);
            }
            return;
        }

        // { f:added, v:created, v:notAttached} -> attach view to container
        if (fragment.isAdded()) {
            addViewToContainer(view, container);
            return;
        }

        // { f:notAdded, v:notCreated, v:notAttached } -> add, create, attach
        if (!shouldDelayFragmentTransactions()) {
            scheduleViewAttach(fragment, container);
            mFragmentManager.beginTransaction()
                    .add(fragment, "f" + holder.getItemId())
                    .setMaxLifecycle(fragment, STARTED)
                    .commitNow();
            mFragmentMaxLifecycleEnforcer.updateFragmentMaxLifecycle(false);
        } else {
            if (mFragmentManager.isDestroyed()) {
                return; // nothing we can do
            }
            mLifecycle.addObserver(new LifecycleEventObserver() {
                @Override
                public void onStateChanged(@NonNull LifecycleOwner source,
                        @NonNull Lifecycle.Event event) {
                    if (shouldDelayFragmentTransactions()) {
                        return;
                    }
                    source.getLifecycle().removeObserver(this);
                    if (ViewCompat.isAttachedToWindow(holder.getContainer())) {
                        placeFragmentInViewHolder(holder);
                    }
                }
            });
        }
    }
    
}
public final class FragmentViewHolder extends ViewHolder {
    private FragmentViewHolder(@NonNull FrameLayout container) {
        super(container);
    }

	// FragmentViewHolder实际上就创建了一个FrameLayout
    @NonNull static FragmentViewHolder create(@NonNull ViewGroup parent) {
        FrameLayout container = new FrameLayout(parent.getContext());
        container.setLayoutParams(
                new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                        ViewGroup.LayoutParams.MATCH_PARENT));
        container.setId(ViewCompat.generateViewId());
        container.setSaveEnabled(false);
        return new FragmentViewHolder(container);
    }

    @NonNull FrameLayout getContainer() {
        return (FrameLayout) itemView;
    }
}

通过上面源码分析可以知道,虽然Fragment没有被复用,但是通过复用了ViewHoldercontainer实现了Framgent的交替显示

4 总结

总结一下本章节的内容:

  1. Fragment+ViewPager只能横向滚动,性能相对较差。对应不同的Adapter效果不一样。
  2. Fragment+ViewPager使用FragmentPagerAdapter,范围外fragments会保存在内存中(detach),但是fragment对应的View会被销毁,fragments对应的SavedState会保存,FragmentPagerAdapter内存较大但页面切换更友好,适用于Fragment数量少的情况
  3. Fragment+ViewPager使用FragmentStatePagerAdapter,范围外fragments不会保存在内存中(remove),View也会被销毁。只保存范围内fragments对应的SavedState。这个SavedStateFragment的生命周期回调中供外部传参数,和Activity类似。内存占用少,页面切换稍差。适用于Fragment数量多的情况。
  4. Fragment+ViewPager2能横向或纵向滚动,继承自RecyclerView所以基本有其所有的优点,包括内存占用、缓存管理等。FragmentStateAdapter继承自RecyclerView.Adapter,虽然Fragment没有被复用,但是通过复用了ViewHoldercontainer实现了Framgent的交替显示。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1028254.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【基本数据结构 四】线性数据结构:队列

学习了栈后,再来看看第四种线性表结构,也就是队列,队列和栈一样也是一种受限的线性表结构,和栈后进先出的操作方式不同的是,队列是FIFO的结构,也就是先进先出的操作方式。 队列的定义 队列这个概念非常好理解。可以把它想象成排队买票,先来的先买,后来的人只能站末尾…

iTOP-RK3568开发板Linux 修改kernel logo

本文档配套资料在网盘资料“iTOP-3568 开发板\02_【iTOP-RK3568 开发板】开发资料\10_Linux 系统开发配套资料\05_Linux 修改内核 logo 配套资料”路径下。 5.3.1 准备 logo 系统默认内核 logo&#xff0c;如下图所示&#xff1a; 如 果 想 要 替 换 这 个 logo, 首 先 要 制…

SAP服务器文件管理

SAP服务器文件管理 文件说明&#xff1a;对于SAP服务器的文件管理&#xff0c;系统给出3个事物码&#xff0c;分别是显示目录的AL11&#xff0c;下载文件的 CG3Y和上传文件的CG3Z。 AL11显示目录:以查找系统参数文件为例&#xff0c;在前台执行事物码AL11进入&#xff0c;如图…

【面试经典150 | 数组】多数元素

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a;哈希表方法二&#xff1a;排序方法三&#xff1a;摩尔投票法 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主…

HelpLook全新升级!定制AI问答机器人,企业内容中心焕新

一直以来&#xff0c;企业都在努力解决内外部“企业知识管理”问题&#xff1a;从纸质手册发放&#xff0c;转线上电子文档传阅(pdf/ppt/word等)&#xff0c;再到整理客户常见问题(FAQ)和内部知识库(wiki)&#xff0c;但始终没有找到一套完整方案将“企业知识”很好地集中管理及…

不得不爱的AI艺术写真头像二维码生成小程序开发

最近什么最火&#xff1f;AI最火&#xff01; AI里什么最火&#xff1f;艺术写真生成和二维码美化最火。 一款小程序集合了高还原度的AI写真艺术照和二维码美化&#xff0c;你们说香还是不香&#xff1f; 并且加入了输入心愿就能生成独一无二的个性头像功能&#xff0c;直接…

zabbix自定义监控内容案例

一、自定义监控内容 案列&#xff1a;自定义监控客户端服务器登录的人数需求&#xff1a;限制登录人数不超过 3 个&#xff0c;超过 3 个就发出报警信息 1、在客户端创建自定义key 明确需要执行的linux命令 创建zabbix监控项配置文件&#xff0c;用于自定义Key #在zabbix的…

vmware创建的虚拟机无法连接外网

在我本机中使用vmware创建虚拟机后&#xff0c;安装 docker 时使用wget 命令下载docker的安装文件 报错找不到资源&#xff0c;然后通过ping www.baidu.com 发现也ping不通&#xff0c;经过一番折腾可以访问外网了&#xff0c;将步骤记录下来&#xff1b; 1、设置虚拟机的网络…

【Python小项目之Tkinter应用】随机点名/抽奖工具大优化:新增查看历史记录窗口!语音播报功能!修复预览文件按钮等之前版本的bug!

文章目录 前言一、实现思路二、关键代码查看历史记录按钮语音播报按钮三、完整代码总结前言 老生常谈,先看效果:(订阅专栏可获取完整代码) 初始状态下,我们为除了【设置】外的按钮添加弹窗,提示用户在使用工具之前要先【设置】。在设置界面,我们主要修改了【预览文件】…

golang 自动生成文件头

安装koroFileHeader控件 打开首选项&#xff0c;进入设置&#xff0c;配置文件头信息"fileheader.customMade": {"Author": "lmy","Date": "Do not edit", // 文件创建时间(不变)// 文件最后编辑者"LastEditors"…

研究生选控制嵌入式还是机器视觉好?

研究生选控制嵌入式还是机器视觉好&#xff1f; 我是嵌入式/硬件方向转的算法&#xff0c;现在是公司的算法负责人&#xff0c;如果再让我选一次&#xff0c;我是不会再选嵌入式方 向&#xff0c;嵌入式如果只做技术是没前途的。 你要是有一定自学能力&#xff0c;能自己在学校…

好物周刊#7:炫酷的浏览器标签页

村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Qexo 一个快速、强大、漂亮的在线 Hexo 编辑器&#xff0c;支持以下功能&#xff1a; 自定义图床上传图片在线配置编辑在线页面管理开放 API自…

Microsoft Windows 部署工具包(MDT)

什么是Microsoft部署工具包(MDT) Microsoft部署工具包(MDT)使用 Windows 评估和部署工具包(ADK)将操作系统部署到网络中的 Windows 客户端和服务器计算机。它支持零接触安装(ZTI)、轻接触安装(LTI)和用户驱动安装(UDI&#xff09;。 Microsoft部署工具可用于将基于 Windows 的…

【FAQ】安防监控视频云存储平台EasyNVR对接EasyNVS时,一直不上线该如何解决?

视频安防监控平台EasyNVR可支持设备通过RTSP/Onvif协议接入&#xff0c;并能对接入的视频流进行处理与多端分发&#xff0c;包括RTSP、RTMP、HTTP-FLV、WS-FLV、HLS、WebRTC等多种格式。 近期有用户在使用安防视频平台EasyNVR对接上级平台EasyNVS时&#xff0c;出现了一直不上线…

AI AIgents时代-(四.)应用上手

HuggingGPT & MetaGPT . &#x1f7e2; HuggingGPT HuggingGPT是一个多模型调用的 Agent 框架&#xff0c;利用 ChatGPT 作为任务规划器&#xff0c;根据每个模型的描述来选择 HuggingFace 平台上可用的模型&#xff0c;最后根据模型的执行结果生成总结性的响应。 这个项…

优化系统报错提示信息,提高人机交互(三)

对于业务比较复杂的接口&#xff0c;可能存在方法嵌套&#xff0c;每个方法都可能会报错&#xff0c;出现异常&#xff0c;那么需要把异常信息返回给接口调用者&#xff0c;如何实现呢&#xff1f; &#xff08;1&#xff09;捕获异常进行处理&#xff0c;不返回 controller代码…

【网络八股】TCP八股

网络八股 请简述TCP/IP模型中每层的作用&#xff0c;典型协议和典型设备介绍一下三次握手的过程介绍一下四次挥手的过程必须三次握手吗&#xff0c;两次不行吗&#xff1f;为什么ACK数据包消耗TCP的序号吗三次握手中可以携带应用层数据吗四次挥手时&#xff0c;可以携带应用层数…

时序数据库 TimescaleDB 安装与使用

TimescaleDB 是一个时间序列数据库&#xff0c;建立在 PostgreSQL 之上。然而&#xff0c;不仅如此&#xff0c;它还是时间序列的关系数据库。使用 TimescaleDB 的开发人员将受益于专门构建的时间序列数据库以及经典的关系数据库 (PostgreSQL)&#xff0c;所有这些都具有完整的…

收款码的费率都是多少

不管是微信还是支付宝&#xff0c;商户最低的收款手续费率可以达到0.2%费率。一般我们普通商户的收款费率一般在0.6左右&#xff0c;当然也有使用0.3的&#xff0c;也就是1万元的费率是30-60块钱&#xff0c;对于一些流水比较大的商家来说&#xff0c;确实很有必要把这个手续费…

【Java 基础篇】Java字节字符流详解:轻松读写文本与二进制数据

在Java编程中&#xff0c;对文件和数据的读写操作是非常常见的任务。为了满足不同需求&#xff0c;Java提供了多种流类来处理输入和输出。本篇博客将详细介绍Java中的字节流和字符流&#xff0c;以及它们的使用方法&#xff0c;帮助初学者更好地理解和运用这些流来处理文件和数…