Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载)

本文主要是介绍Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

今日份的总结

    • 一、背景
    • 二、特意声明
    • 三、最常见的优化方案
    • 四、项目结构
    • 五、极致的懒加载
      • 5.1 极致的懒加载
      • 5.2 懒加载方案
      • 5.3 FragmentPagerAdapter与FragmentStatePagerAdapter
      • 5.4 FragmentPagerAdapter的刷新问题
      • 5.5 FragmentPagerAdapter刷新的正确姿势
    • 六、神奇的的预加载(预加载View,而不是data)
      • 6.1 需要预加载什么
      • 6.2 修改AsyncLayoutInflater
      • 6.3 装饰器模式
    • 七、总结
        • 懒加载:
        • 预加载:
        • 从ContextWrapper、MutableContextWrapper类的设计中学到了 ↓

一、背景

明天是周二,正好是我们团队每周一次的技术分享,我会把前段时间花了几天在干其他活的同时,整的一套诡异的冷启动速度优化方案分享一下。

二、特意声明

我这边文章的内容不会涉及网上变地都是的常规的优化方案~ ,同时,平时工作的时候,工作内容杂且多,所以这个优化方案也不是特别成熟,仅供参考吧~

三、最常见的优化方案

数据懒加载,比如Fragment用户不可见时不进行数据的获取
优化布局层级,减少首次inflate layout的耗时
将绝大部分sdk的初始化放线程池中运行
能用ViewStub的就用ViewStub,按需加载layout
一定要尽量避免启动过程中,出现的主线程去unpack一些全局配置的数据
不仅仅是三方库可以放子线程进行,一些时效性要求没那么高的逻辑都可以放子线程

四、项目结构

在我们的Android项目中,应用过了闪屏之后会进入到主屏 - MainActivity,这个地方我吐槽很多次了,广告闪屏作为launcher真的不是特别靠谱,最好的方式应该是从MainActivity里面来启动AdActivity,甚至是不用Activity,采用一个全屏的AdView都可以。

先简单介绍一下我们项目中MainActivity涉及到的结构:

简单的画了个图,简直是。。画图界的耻辱。。。

大概看看意思就可以了,我在组内分享就是用的这个草图,急着下班,就不重新画了。。

在这里插入图片描述

当App冷启动的时候,肉眼可见的要初始化的东西太多了,本身Fragment就是一个相对重的东西。比Activity要轻量很多,但是比View又要重

我们首页大概是 4-5个tab,每个tab都是一个Fragment,且第一个tab内嵌了4个Fragment,我这一次的优化主要将目标瞄准了首页的 tab1 以及tab1内嵌的四个tab

五、极致的懒加载

5.1 极致的懒加载

平时见到的懒加载:

就是初始化fragment的时候,会连同我们写的网络请求一起执行,这样非常消耗性能,最理想的方式是,只有用户点开或滑动到当前fragment时,才进行请求网络的操作。因此,我们就产生了懒加载这样一个说法。
但是。。。。

由于我们首屏4个子Tab都是继承自一个基类BaseLoadListFragment,数据加载的逻辑非常的死,按照上述的改法,影响面太大。后续可能会徒增烦恼

5.2 懒加载方案

首屏加载时,只往ViewPager中塞入默认要展示的tab,剩余的tab用空的占位Fragment代替
当用户滑动到其他tab时,比如滑动到好友动态tab,就用FriendFragment把当前的EmptyPlaceholderFragment替换掉,然后adapter.notifyDataSetChanged
当四个Tab全部替换为数据tab时,清除掉EmptyFragment的引用,释放内存

说到这里,又不得不提一个老生常谈的一个坑,因为我们的首页是用的ViewPager + FragmentPagerAdapter来进行实现的。因此就出现了一个坑:

ViewPager + FragmentPagerAdapter组合使用,调用notifyDataSetChanged()方法无效,无法刷新Fragment列表
下面我会对这个问题进行一下详细的介绍

5.3 FragmentPagerAdapter与FragmentStatePagerAdapter

当我们要使用ViewPager来加载Fragment时,官方为我们提供了这两种Adapter,都是继承自PagerAdapter。

区别,上官方描述:

FragmentPagerAdapter

This version of the pager is best for use when there are a handful of
typically more static fragments to be paged through, such as a set of
tabs. The fragment of each page the user visits will be kept in
memory, though its view hierarchy may be destroyed when not visible.
This can result in using a significant amount of memory since fragment
instances can hold on to an arbitrary amount of state. For larger sets
of pages, consider FragmentStatePagerAdapter.

FragmentStatePagerAdapter

This version of the pager is more useful when there are a large number
of pages, working more like a list view. When pages are not visible to
the user, their entire fragment may be destroyed, only keeping the
saved state of that fragment. This allows the pager to hold on to much
less memory associated with each visited page as compared
toFragmentPagerAdapter at the cost of potentially more overhead when
switching between pages

总结:

使用FragmentStatePagerAdapter时,如果tab对于用户不可见了,Fragment就会被销毁,FragmentPagerAdapter则不会,使用FragmentPagerAdapter时,所有的tab上的Fragment都会hold在内存里
当tab非常多时,推荐使用FragmentStatePagerAdapter
当tab不多,且固定时,推荐用FragmentPagerAdapter
我们项目中就是使用的ViewPager+FragmentPagerAdapter。

5.4 FragmentPagerAdapter的刷新问题

正常情况,我们使用adapter时,想要刷新数据只需要:

更新dataSet
调用notifyDataSetChanged()
但是,这个在这个Adapter中是不适用的。因为(这一步没耐心的可以直接看后面的总结):

默认的PagerAdapter的destoryItem只会把Fragment detach掉,而不会remove
当再次调用instantiateItem的时候,之前detach掉的Fragment,又会从mFragmentManager中取出,又可以attach了
在这里插入图片描述

3,ViewPager的dataSetChanged代码如下:

在这里插入图片描述

4,且adapter的默认实现
在这里插入图片描述

简单总结一下:

  1. ViewPager的dataSetChanged()中会去用adapter.getItemPosition来判断是否要移除当前Item(position = POSITION_NONE时remove)
  2. PagerAdapter的getItemPosition默认实现为POSITION_UNCHANGED

上述两点导致ViewPager构建完成Adapter之后,不会有机会调用到Adapter的instantiateItem了。

再者,即使重写了getItemPosition方法,每次返回POSITION_NONE,还是不会替换掉Fragment,这是因为instantiateItem方法中,会根据getItemId()去从FragmetnManager中找到已经创建好的Fragment返回回去,而getItemId()的默认实现是return position。

5.5 FragmentPagerAdapter刷新的正确姿势

重写getItemId()和getItemPosition()

class TabsAdapter extends FragmentPagerAdapter {private ArrayList<Fragment> mFragmentList;private ArrayList<String> mPageTitleList;private int mCount;TabsAdapter(FragmentManager fm, ArrayList<Fragment> fragmentList, ArrayList<String> pageTitleList) {super(fm);mFragmentList = fragmentList;mCount = fragmentList.size();mPageTitleList = pageTitleList;}@Overridepublic Fragment getItem(int position) {return mFragmentList.get(position);}@Overridepublic CharSequence getPageTitle(int position) {return mPageTitleList.get(position);}@Overridepublic int getCount() {return mCount;}@Overridepublic long getItemId(int position) {//这个地方的重写非常关键,super中是返回position,//如果不重写,还是会继续找到FragmentManager中缓存的Fragmentreturn mFragmentList.get(position).hashCode();}@Overridepublic int getItemPosition(@NonNull Object object) {//不在数据集合里面的话,return POSITION_NONE,进行item的重建int index = mFragmentList.indexOf(object);if (index == -1) {return POSITION_NONE;} else {return mFragmentList.indexOf(object);}}void refreshFragments(ArrayList<Fragment> fragmentList) {mFragmentList = fragmentList;notifyDataSetChanged();}}

其他的相关代码:

(1)实现ViewPager.OnPageChangeListener 来监控ViewPager的滑动状态,才可以在滑动到下一个tab的时候进行Fragment替换的操作,其中mDefaultTab是我们通过接口返回的当前启动展示的tab序号

  @Overridepublic void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {}@Overridepublic void onPageSelected(int position) {mCurrentSelectedTab = position;}@Overridepublic void onPageScrollStateChanged(int state) {if (!hasReplacedAllEmptyFragments && mCurrentSelectedTab != mDefaultTab && state == 0) {//当满足: 1. 没有全部替换完 2. 当前tab不是初始化的默认tab(默认tab不会用空的Fragment去替换) 3. 滑动结束了,即state = 0replaceEmptyFragmentsIfNeed(mCurrentSelectedTab);}}

备注:
onPageScrollStateChanged接滑动的状态值。一共有三个取值:

  • 0:什么都没做
  • 1:开始滑动
  • 2:滑动结束

一次引起页面切换的滑动,state的顺序分别是: 1 -> 2 -> 0

(2)进行Fragment的替换,这里因为我们的tab数量是可能根据全局config信息而改变的,所以这个地方写的稍微纠结了一些。

 /*** 如果全部替换完了,直接return* 替换过程:* 1. 找到当前空的tab在mEmptyFragmentList 中的实际下标** @param tabId 要替换的tab的tabId - (当前空的Fragment在adapter数据列表mFragmentList的下标)*/private void replaceEmptyFragmentsIfNeed(int tabId) {if (hasReplacedAllEmptyFragments) {return;}int tabRealIndex = mEmptyFragmentList.indexOf(mFragmentList.get(tabId)); //找到当前的空Fragment在 mEmptyFragmentList 是第几个if (tabRealIndex > -1) {if (Collections.replaceAll(mFragmentList, mEmptyFragmentList.get(tabRealIndex), mDataFragmentList.get(tabRealIndex))) {mTabsAdapter.refreshFragments(mFragmentList); //将mFragmentList中的相应empty fragment替换完成之后刷新数据boolean hasAllReplaced = true;for (Fragment fragment : mFragmentList) {if (fragment instanceof EmptyPlaceHolderFragment) {hasAllReplaced = false;break;}}if (hasAllReplaced) {mEmptyFragmentList.clear(); //全部替换完成的话,释放引用}hasReplacedAllEmptyFragments = hasAllReplaced;}}}

六、神奇的的预加载(预加载View,而不是data)

Android在启动过程中可能涉及到的一些View的预加载方案:

WebView提前创建好,因为webview创建的耗时较长,如果首屏有h5的页面,可以提前创建好。
Application的onCreate时,就可以开始在子线程中进行后面要用到的Layout的inflate工作了,最先想到的应该是官方提供的AsyncLayoutInflater
填充View的数据的预加载,今天的内容不涉及这一项

6.1 需要预加载什么

直接看图,这个是首页四个子Tab Fragment的基类的layout,因为某些东西设计的不合理,导致层级是非常的深,直接导致了首页上的三个tab加上FeedMainFragment自身,光将这个View inflate出来的时间就非常长。因此我们考虑在子线程中提前inflate layout

在这里插入图片描述

6.2 修改AsyncLayoutInflater

官方提供了一个类,可以来进行异步的inflate,但是有两个缺点:

  1. 每次都要现场new一个出来
  2. 异步加载的view只能通过callback回调才能获得(死穴)

因此决定自己封装一个AsyncInflateManager,内部使用线程池,且对于inflate完成的View有一套缓存机制。而其中最核心的LayoutInflater则直接copy出来就好。

先看AsyncInflateManager的实现,这里我直接将代码copy进来,而不是截图了,这样你们如果想用其中部分东西,可以直接copy:


/*** @author zoutao* <p>* 用来提供子线程inflate view的功能,避免某个view层级太深太复杂,主线程inflate会耗时很长,* 实就是对 AsyncLayoutInflater进行了抽取和封装*/
public class AsyncInflateManager {private final String TAG = getClass().getSimpleName();private static AsyncInflateManager sInstance;private ConcurrentHashMap<String, AsyncInflateItem> mInflateMap; //保存inflateKey以及InflateItem,里面包含所有要进行inflate的任务private ConcurrentHashMap<String, CountDownLatch> mInflateLatchMap;private ExecutorService mThreadPool; //用来进行inflate工作的线程池private AsyncInflateManager() {mThreadPool = new ThreadPoolExecutor(4, 4, 0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<Runnable>());mInflateMap = new ConcurrentHashMap<>();mInflateLatchMap = new ConcurrentHashMap<>();}public static AsyncInflateManager getInstance() {if (sInstance == null) {synchronized (AsyncInflateManager.class) {if (sInstance == null) {sInstance = new AsyncInflateManager();}}}return sInstance;}/*** 用来获得异步inflate出来的view** @param context* @param layoutResId 需要拿的layoutId* @param parent      container* @param inflateKey  每一个View会对应一个inflateKey,因为可能许多地方用的同一个 layout,但是需要inflate多个,用InflateKey进行区分* @param inflater    外部传进来的inflater,外面如果有inflater,尽量传进来,比如给Fragment setView回去,*                    如果用直接new的LayoutInflater,会抛异常,需要直接用Fragment自带的Inflater* @return 最后inflate出来的view*/@UiThread@NonNullpublic View getInflatedView(Context context, int layoutResId, @Nullable ViewGroup parent, String inflateKey, @NonNull LayoutInflater inflater) {if (!TextUtils.isEmpty(inflateKey) && mInflateMap.containsKey(inflateKey)) {AsyncInflateItem item = mInflateMap.get(inflateKey);CountDownLatch latch = mInflateLatchMap.get(inflateKey);if (item != null) {View resultView = item.inflatedView;if (resultView != null) {//拿到了view直接返回removeInflateKey(inflateKey);replaceContextForView(resultView, context);return resultView;}if (item.isInflating() && latch != null) {//没拿到view,但是在inflate中,等待返回try {latch.wait();} catch (InterruptedException e) {Log.e(TAG, e.getMessage(), e);}removeInflateKey(inflateKey);if (resultView != null) {replaceContextForView(resultView, context);return resultView;}}//如果还没开始inflate,则设置为false,UI线程进行inflateitem.setCancelled(true);}}//拿异步inflate的View失败,UI线程inflatereturn inflater.inflate(layoutResId, parent, false);}/*** inflater初始化时是传进来的application,inflate出来的view的context没法用来startActivity,* 因此用MutableContextWrapper进行包装,后续进行替换*/private void replaceContextForView(View inflatedView, Context context) {if (inflatedView == null || context == null) {return;}Context cxt = inflatedView.getContext();if (cxt instanceof MutableContextWrapper) {((MutableContextWrapper) cxt).setBaseContext(context);}}@UiThreadpublic void asyncInflateViews(Context context, AsyncInflateItem... list) {if (list == null || list.length == 0) {return;}asyncInflateViews(context, Arrays.asList(list));}@UiThreadpublic void asyncInflateViews(Context context, List<AsyncInflateItem> list) {if (list == null || list.isEmpty()) {return;}for (AsyncInflateItem item : list) {asyncInflate(context, item);}}@UiThreadprivate void asyncInflate(Context context, AsyncInflateItem item) {if (item == null || item.layoutResId == 0 || mInflateMap.containsKey(item.inflateKey) || item.isCancelled() || item.isInflating()) {return;}onAsyncInflateReady(item);inflateWithThreadPool(context, item);}private void onAsyncInflateReady(AsyncInflateItem item) {mInflateMap.put(item.inflateKey, item);mInflateLatchMap.put(item.inflateKey, new CountDownLatch(1));}private void onAsyncInflateStart(AsyncInflateItem item) {item.setInflating(true);}private void onAsyncInflateEnd(AsyncInflateItem item, boolean success) {item.setInflating(false);CountDownLatch latch = mInflateLatchMap.get(item.inflateKey);if (latch != null) {latch.countDown();}if (success && item.callback != null) {removeInflateKey(item.inflateKey);//回主线程调callbackThreadUtil.runOnMainThread(new Runnable() {@Overridepublic void run() {item.callback.onInflateFinished(item);}});}}private void removeInflateKey(String inflateKey) {mInflateLatchMap.remove(inflateKey);mInflateMap.remove(inflateKey);}private void inflateWithThreadPool(Context context, AsyncInflateItem item) {mThreadPool.execute(new Runnable() {@Overridepublic void run() {if (!item.isInflating() && !item.isCancelled()) {try {onAsyncInflateStart(item);item.inflatedView = new BasicInflater(context).inflate(item.layoutResId, item.parent, false);onAsyncInflateEnd(item, true);} catch (RuntimeException e) {Log.e(TAG, "Failed to inflate resource in the background! Retrying on the UI thread", e);onAsyncInflateEnd(item, false);}}}});}/*** copy from AsyncLayoutInflater - actual inflater*/private static class BasicInflater extends LayoutInflater {private static final String[] sClassPrefixList = new String[]{"android.widget.", "android.webkit.", "android.app."};BasicInflater(Context context) {super(context);}public LayoutInflater cloneInContext(Context newContext) {return new BasicInflater(newContext);}protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {for (String prefix : sClassPrefixList) {try {View view = this.createView(name, prefix, attrs);if (view != null) {return view;}} catch (ClassNotFoundException ignored) {}}return super.onCreateView(name, attrs);}}
}

这里我用一个AsyncInflateItem来管理一次要inflate的一个单位,

/*** @author zoutao*/
public class AsyncInflateItem {String inflateKey;int layoutResId;ViewGroup parent;OnInflateFinishedCallback callback;View inflatedView;private boolean cancelled;private boolean inflating;public AsyncInflateItem(String inflateKey, int layoutResId) {this(inflateKey, layoutResId, null, null);}public AsyncInflateItem(String inflateKey, int layoutResId, ViewGroup parent, OnInflateFinishedCallback callback) {this.layoutResId = layoutResId;this.parent = parent;this.callback = callback;this.inflateKey = inflateKey;}boolean isCancelled() {synchronized (this) {return cancelled;}}void setCancelled(boolean cancelled) {synchronized (this) {this.cancelled = cancelled;}}boolean isInflating() {synchronized (this) {return inflating;}}void setInflating(boolean inflating) {synchronized (this) {this.inflating = inflating;}}
}

经过这样的封装,外面可以直接在Application的onCreate中,开始异步的inflate view的任务。调用如下:

AsyncInflateUtil.startTask();
/*** @author zoutao*/
public class AsyncInflateUtil {public static void startTask() {Context context = new MutableContextWrapper(CommonContext.getApplication());AsyncInflateManager.getInstance().asyncInflateViews(context,new AsyncInflateItem(InflateKey.TAB_1_CONTAINER_FRAGMENT, R.layout.fragment_main),new AsyncInflateItem(InflateKey.SUB_TAB_1_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_2_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_3_FRAGMENT, R.layout.fragment_load_list),new AsyncInflateItem(InflateKey.SUB_TAB_4_FRAGMENT, R.layout.fragment_load_list));}public class InflateKey {public static final String TAB_1_CONTAINER_FRAGMENT = "tab1";public static final String SUB_TAB_1_FRAGMENT = "sub1";public static final String SUB_TAB_2_FRAGMENT = "sub2";public static final String SUB_TAB_3_FRAGMENT = "sub3";public static final String SUB_TAB_4_FRAGMENT = "sub4";}
}

注意:这里会有一个坑。就是在Application的onCreate中,能拿到的Context只有Application,这样inflate的View,View持有的Context就是Application,这会导致一个问题。

如果用View.getContext()这个context去进行Activity的跳转就会。。抛异常

Calling startActivity() from outside of an Activity context requires
the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

而如果想要传入Activity来创建LayoutInflater,时机又太晚。众所周知,Context是一个抽象类,实现它的包装类就是ContextWrapper,而Activity、Appcation等都是ContextWrapper的子类,然而,ContextWrapper还有一个神奇的子类,

package android.content;/*** Special version of {@link ContextWrapper} that allows the base context to* be modified after it is initially set.*/
public class MutableContextWrapper extends ContextWrapper {public MutableContextWrapper(Context base) {super(base);}/*** Change the base context for this ContextWrapper. All calls will then be* delegated to the base context.  Unlike ContextWrapper, the base context* can be changed even after one is already set.* * @param base The new base context for this wrapper.*/public void setBaseContext(Context base) {mBase = base;}
}

6.3 装饰器模式

可以看到Android上Context的设计采用了装饰器模式,装饰器模式极大程度的提高了灵活性。这个例子对我最大的感受就是,当官方没有提供MutableContextWrapper这个类时,其实我们自己也完全可以通过同样的方式去进行实现。思维一定要灵活~

七、总结

常见的启动速度优化的方案有:

  1. 数据懒加载,比如Fragment用户不可见时不进行数据的获取
  2. 优化布局层级,减少首次inflate layout的耗时
  3. 将绝大部分sdk的初始化放线程池中运行
  4. 能用ViewStub的就用ViewStub,按需加载layout
  5. 一定要尽量避免启动过程中,出现的主线程去unpack一些全局配置的数据
  6. 不仅仅是三方库可以放子线程进行,一些时效性要求没那么高的逻辑都可以放子线程

这些都可以在网上找到大量的文章以及各个大佬的实现方案。

首先,优化的大方向肯定先定好

  1. 懒加载
  2. 预加载
懒加载:

首屏加载时,只往ViewPager中塞入默认要展示的tab,剩余的tab用空的占位Fragment代替
当用户滑动到其他tab时,比如滑动到好友动态tab,就用FriendFragment把当前的EmptyPlaceholderFragment替换掉,然后adapter.notifyDataSetChanged
当四个Tab全部替换为数据tab时,清除掉EmptyFragment的引用,释放内存

预加载:
  1. Application onCreate方法中,针对后续所有的Fragment,在子线程中将Layout先给inflate出来
  2. 针对inflate完成的View加入一套缓存的存取机制,以及等待机制
  3. 如果正在inflate,则进行阻塞等待
  4. 如果已经inflate完成了,取出view,并释放缓存对于View的引用
  5. 如果还没有开始Inflate,则在UI线程直接进行inflate

这些方案仅供参考~~~

从ContextWrapper、MutableContextWrapper类的设计中学到了 ↓

写代码的时候,首先要进行设计,选用最合适的设计模式,这样后续赚到的远远大于写一个文档、想一个设计所耗费的时间和脑力成本

我的简书 邹啊涛涛涛的简书
我的CSDN 邹啊涛涛涛的CSDN
我的掘金 邹啊涛涛涛的掘金

这篇关于Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/378675

相关文章

Java中实现线程的创建和启动的方法

《Java中实现线程的创建和启动的方法》在Java中,实现线程的创建和启动是两个不同但紧密相关的概念,理解为什么要启动线程(调用start()方法)而非直接调用run()方法,是掌握多线程编程的关键,... 目录1. 线程的生命周期2. start() vs run() 的本质区别3. 为什么必须通过 st

Linux实现线程同步的多种方式汇总

《Linux实现线程同步的多种方式汇总》本文详细介绍了Linux下线程同步的多种方法,包括互斥锁、自旋锁、信号量以及它们的使用示例,通过这些同步机制,可以解决线程安全问题,防止资源竞争导致的错误,示例... 目录什么是线程同步?一、互斥锁(单人洗手间规则)适用场景:特点:二、条件变量(咖啡厅取餐系统)工作流

Java中常见队列举例详解(非线程安全)

《Java中常见队列举例详解(非线程安全)》队列用于模拟队列这种数据结构,队列通常是指先进先出的容器,:本文主要介绍Java中常见队列(非线程安全)的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录一.队列定义 二.常见接口 三.常见实现类3.1 ArrayDeque3.1.1 实现原理3.1.2

springboot加载不到nacos配置中心的配置问题处理

《springboot加载不到nacos配置中心的配置问题处理》:本文主要介绍springboot加载不到nacos配置中心的配置问题处理,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录springboot加载不到nacos配置中心的配置两种可能Spring Boot 版本Nacos

SpringBoot3中使用虚拟线程的完整步骤

《SpringBoot3中使用虚拟线程的完整步骤》在SpringBoot3中使用Java21+的虚拟线程(VirtualThreads)可以显著提升I/O密集型应用的并发能力,这篇文章为大家介绍了详细... 目录1. 环境准备2. 配置虚拟线程方式一:全局启用虚拟线程(Tomcat/Jetty)方式二:异步

如何解决Druid线程池Cause:java.sql.SQLRecoverableException:IO错误:Socket read timed out的问题

《如何解决Druid线程池Cause:java.sql.SQLRecoverableException:IO错误:Socketreadtimedout的问题》:本文主要介绍解决Druid线程... 目录异常信息触发场景找到版本发布更新的说明从版本更新信息可以看到该默认逻辑已经去除总结异常信息触发场景复

Android学习总结之Java和kotlin区别超详细分析

《Android学习总结之Java和kotlin区别超详细分析》Java和Kotlin都是用于Android开发的编程语言,它们各自具有独特的特点和优势,:本文主要介绍Android学习总结之Ja... 目录一、空安全机制真题 1:Kotlin 如何解决 Java 的 NullPointerExceptio

使用Python获取JS加载的数据的多种实现方法

《使用Python获取JS加载的数据的多种实现方法》在当今的互联网时代,网页数据的动态加载已经成为一种常见的技术手段,许多现代网站通过JavaScript(JS)动态加载内容,这使得传统的静态网页爬取... 目录引言一、动态 网页与js加载数据的原理二、python爬取JS加载数据的方法(一)分析网络请求1

IDEA下"File is read-only"可能原因分析及"找不到或无法加载主类"的问题

《IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题》:本文主要介绍IDEA下Fileisread-only可能原因分析及找不到或无法加载主类的问题,具有很好的参... 目录1.File is read-only”可能原因2.“找不到或无法加载主类”问题的解决总结1.File

SpringBoot中HTTP连接池的配置与优化

《SpringBoot中HTTP连接池的配置与优化》这篇文章主要为大家详细介绍了SpringBoot中HTTP连接池的配置与优化的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、HTTP连接池的核心价值二、Spring Boot集成方案方案1:Apache HttpCl