Glide生命周期原理
本文首发于 vivo互联网技术 微信公众号
链接:https://mp.weixin.qq.com/s/uTv44vJFFJI_l6b5YKSXYQ
作者:连凌能
Android App中图片的展示是很基本也很重要的一个功能,在Android平台上有很多的图片加载解决方案,但是官方认可的是Glide。Android App的页面是有生命周期的,Glide比较好的一个功能就是具有生命周期管理功能,能够根据页面和APP的生命周期来管理图片的加载和停止,也开放接口供用户在内存紧张时手动进行内存管理。本文重点是生命周期源码的分析,不会从简单的使用着手。
一、综述
这是Glide源码分析的第二篇文章,第一篇是《Glide缓存流程》,从资源的获取流程对源码进行分析。本篇会聚焦于生命周期模块的原理。开始之前先思考下面这几个问题:
Glide怎么实现页面生命周期?
Glide为什么对Fragment做缓存?
Glide如何监听网络变化?
Glide如何监测内存?
二、Glide生命周期传递
先来看with函数的执行, 会构造glide单例,而
RequestManagerRetriever在initializeGlide中会进行构造。
// Glide.java public static RequestManager with(@NonNull Activity activity) { return getRetriever(activity).get(activity); } @NonNull private static RequestManagerRetriever getRetriever(@Nullable Context context) { // Context could be null for other reasons (ie the user passes in null), but in practice it will // only occur due to errors with the Fragment lifecycle. Preconditions.checkNotNull( context, "You cannot start a load on a not yet attached View or a Fragment where getActivity() " + "returns null (which usually occurs when getActivity() is called before the Fragment " + "is attached or after the Fragment is destroyed)."); return Glide.get(context).getRequestManagerRetriever(); } @NonNull public static Glide get(@NonNull Context context) { if (glide == null) { synchronized (Glide.class) { if (glide == null) { checkAndInitializeGlide(context); } } } return glide; } private static void checkAndInitializeGlide(@NonNull Context context) { // In the thread running initGlide(), one or more classes may call Glide.get(context). // Without this check, those calls could trigger infinite recursion. if (isInitializing) { throw new IllegalStateException("You cannot call Glide.get() in registerComponents()," + " use the provided Glide instance instead"); } isInitializing = true; initializeGlide(context); isInitializing = false; }
构造完成RequestManagerRetriever通过get返回一个 RequestManager, 如果不在主线程,默认会传入 getApplicationContext,也就是不进行生命周期管理:
在getRequestManagerFragment中先查看当前Activity中有没有FRAGMENT_TAG这个标签对应的Fragment,如果有就直接返回
如果没有,会判断pendingRequestManagerFragments中有没有,如果有就返回
如果没有,就会重写new一个,然后放入到pendingRequestManagerFragments中,然后添加到当前Activity,再给Handler发送一条移除的消息
// RequestManagerRetriever.java @NonNull public RequestManager get(@NonNull Activity activity) { if (Util.isOnBackgroundThread()) { return get(activity.getApplicationContext()); } else { assertNotDestroyed(activity); android.app.FragmentManager fm = activity.getFragmentManager(); return fragmentGet( activity, fm, /*parentHint=*/ null, isActivityVisible(activity)); } } private RequestManager fragmentGet(@NonNull Context context, @NonNull android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) { RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible); RequestManager requestManager = current.getRequestManager(); if (requestManager == null) { // TODO(b/27524013): Factor out this Glide.get() call. Glide glide = Glide.get(context); requestManager = factory.build( glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context); current.setRequestManager(requestManager); } return requestManager; } private RequestManagerFragment getRequestManagerFragment( @NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) { RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); if (current == null) { current = pendingRequestManagerFragments.get(fm); if (current == null) { current = new RequestManagerFragment(); current.setParentFragmentHint(parentHint); if (isParentVisible) { current.getGlideLifecycle().onStart(); } pendingRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(); } } return current; } public boolean handleMessage(Message message) { ... switch (message.what) { case ID_REMOVE_FRAGMENT_MANAGER: android.app.FragmentManager fm = (android.app.FragmentManager) message.obj; key = fm; removed = pendingRequestManagerFragments.remove(fm); break; ... } ... }
这里面需要注意一个问题,就是如果with()函数中传进来的不是Activity,而是Fragment,那么也会去创建一个没有界面的RequestManagerFragment,而它的父Fragment就是传进来的Fragment。
上面为什么需要pendingRequestManagerFragments先进行缓存呢?这个放到下面第二个问题中说明。先接着往下看生命周期的传递。
RequestManagerFragment是一个很重要的类,Glide就是通过它作为生命周期的分发入口,RequestManagerFragment的默认构造函数会实例化一个ActivityFragmentLifecycle,在每个生命周期onStart/onStop/onDestroy中会调用ActivityFragmentLifecycle:
// RequestManagerFragment.java public class RequestManagerFragment extends Fragment { private static final String TAG = "RMFragment"; private final ActivityFragmentLifecycle lifecycle; @Nullable private RequestManager requestManager; public RequestManagerFragment() { this(new ActivityFragmentLifecycle()); } RequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) { this.lifecycle = lifecycle; } @Override public void onStart() { super.onStart(); lifecycle.onStart(); } @Override public void onStop() { super.onStop(); lifecycle.onStop(); } @Override public void onDestroy() { super.onDestroy(); lifecycle.onDestroy(); unregisterFragmentWithRoot(); } ... }
RequestManagerFragment里面有一个实例RequestManager,在前面的fragmentGet,RequestManagerFragment拿到以后会尝试获取它的RequestManager,第一次获取肯定是没有,就会重新构造一个, 通过RequestManagerRetriever构造时传入的RequestManagerFactory工厂类实例化一个RequestManager, 把RequestManagerFragment中的ActivityFragmentLifecycle传进去:
// RequestManagerRetriever.java public interface RequestManagerFactory { @NonNull RequestManager build( @NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context); } private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() { @NonNull @Override public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) { return new RequestManager(glide, lifecycle, requestManagerTreeNode, context); } };
很明显生命周期的关键就在ActivityFragmentLifecycle, 在RequestManagerFragment中相应生命周期中会回调它,那么猜测它肯定是在里面维护了一个观察者列表,相应事件发生的时候进行通知, 看下它的源码:
// ActivityFragmentLifecycle.java class ActivityFragmentLifecycle implements Lifecycle { private final Set<LifecycleListener> lifecycleListeners = Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>()); private boolean isStarted; private boolean isDestroyed; @Override public void addListener(@NonNull LifecycleListener listener) { lifecycleListeners.add(listener); if (isDestroyed) { listener.onDestroy(); } else if (isStarted) { listener.onStart(); } else { listener.onStop(); } } @Override public void removeListener(@NonNull LifecycleListener listener) { lifecycleListeners.remove(listener); } void onStart() { isStarted = true; for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStart(); } } void onStop() { isStarted = false; for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onStop(); } } void onDestroy() { isDestroyed = true; for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) { lifecycleListener.onDestroy(); } } }
所以RequestManagerFragment把这个传给RequestManager后,肯定会注册观察者,看一下RequestManager的相关代码,在构造函数里面lifecycle.addListener(this);,把自己注册为观察者:
// RequestManager.java public class RequestManager implements LifecycleListener, ModelTypes<RequestBuilder<Drawable>> { ... protected final Glide glide; protected final Context context; @Synthetic final Lifecycle lifecycle; private final RequestTracker requestTracker; private final RequestManagerTreeNode treeNode; private final TargetTracker targetTracker = new TargetTracker(); private final Runnable addSelfToLifecycle = new Runnable() { @Override public void run() { lifecycle.addListener(RequestManager.this); } }; private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final ConnectivityMonitor connectivityMonitor; private RequestOptions requestOptions; public RequestManager( @NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) { this( glide, lifecycle, treeNode, new RequestTracker(), glide.getConnectivityMonitorFactory(), context); } // Our usage is safe here. @SuppressWarnings("PMD.ConstructorCallsOverridableMethod") RequestManager( Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) { this.glide = glide; this.lifecycle = lifecycle; this.treeNode = treeNode; this.requestTracker = requestTracker; this.context = context; connectivityMonitor = factory.build( context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker)); if (Util.isOnBackgroundThread()) { mainHandler.post(addSelfToLifecycle); } else { lifecycle.addListener(this); } lifecycle.addListener(connectivityMonitor); setRequestOptions(glide.getGlideContext().getDefaultRequestOptions()); glide.registerRequestManager(this); }
在看下RequestManager对应的生命周期里面, 在这里面分别启动,停止和销毁请求:
// RequestManager @Override public void onStart() { resumeRequests(); targetTracker.onStart(); } @Override public void onStop() { pauseRequests(); targetTracker.onStop(); } @Override public void onDestroy() { targetTracker.onDestroy(); for (Target<?> target : targetTracker.getAll()) { clear(target); } targetTracker.clear(); requestTracker.clearRequests(); lifecycle.removeListener(this); lifecycle.removeListener(connectivityMonitor); mainHandler.removeCallbacks(addSelfToLifecycle); glide.unregisterRequestManager(this); }
三、Glide为什么对Fragment做缓存?
再贴一次RequestManagerRetriever中获取Fragment的代码,前面留了一个疑问,为什么这里会需要一个pendingRequestManagerFragments对Fragment进行缓存。
// RequestManagerRetriever.java /** * Pending adds for RequestManagerFragments. */ @SuppressWarnings("deprecation") @VisibleForTesting final Map<android.app.FragmentManager, RequestManagerFragment> pendingRequestManagerFragments = new HashMap<>(); private RequestManagerFragment getRequestManagerFragment( @NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) { RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG); if (current == null) { current = pendingRequestManagerFragments.get(fm); if (current == null) { current = new RequestManagerFragment(); current.setParentFragmentHint(parentHint); if (isParentVisible) { current.getGlideLifecycle().onStart(); } pendingRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget(); } } return current; }
我们看一个情况:
Glide.with(Context).load(ImageUrl1).into(imageview1); // task1 Glide.with(Context).load(ImageUrl2).into(imageview2); // task2
Android开发应该都知道主线程有一个Handler机制,会往消息队列中放消息,通过Looper按顺序取出来执行。那么主线程中的执行顺序和消息队列中的执行顺序关系是什么?看个栗子:
private void start() { mHandler = new Handler(getMainLooper()); VLog.i("HandlerRunT", "=========Begin!============"); mHandler.post(new Runnable() { @Override public void run() { VLog.i("HandlerRunT", "=========First!============"); } }); VLog.i("HandlerRunT", "=========Middle!============"); mHandler.sendMessage(Message.obtain(mHandler, new Runnable() { @Override public void run() { VLog.i("HandlerRunT", "=========Second!============"); } })); VLog.i("HandlerRunT", "=========End!============"); Next(); } private void Next() { VLog.i("HandlerRunT", "=========Next Begin!============"); mHandler.post(new Runnable() { @Override public void run() { VLog.i("HandlerRunT", "=========Next First!============"); } }); VLog.i("HandlerRunT", "=========Next Middle!============"); mHandler.sendMessage(Message.obtain(mHandler, new Runnable() { @Override public void run() { VLog.i("HandlerRunT", "=========Next Second!============"); } })); VLog.i("HandlerRunT", "=========Next End!============"); }
在start中打印的顺序和它里面的Handler中的信息哪个先打印?start中handler的信息和Next函数中的信息打印顺序是怎样的?看下打印结果:
HandlerRunT: =========Begin!============ HandlerRunT: =========Middle!============ HandlerRunT: =========End!============ HandlerRunT: =========Next Begin!============ HandlerRunT: =========Next Middle!============ HandlerRunT: =========Next End!============ HandlerRunT: =========First!============ HandlerRunT: =========Second!============ HandlerRunT: =========Next First!============ HandlerRunT: =========Next Second!============
Handler中的顺序会在主线程之后,Handler中的消息执行顺序就是队列先进先出。
上面执行到task1的时候,在下面这两行代码,add操作会往消息队列放一个消息,这里标记为msg1:
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
// FragmentManager.java public void enqueueAction(OpGenerator action, boolean allowStateLoss) { if (!allowStateLoss) { checkStateLoss(); } synchronized (this) { if (mDestroyed || mHost == null) { if (allowStateLoss) { // This FragmentManager isn't attached, so drop the entire transaction. return; } throw new IllegalStateException("Activity has been destroyed"); } if (mPendingActions == null) { mPendingActions = new ArrayList<>(); } mPendingActions.add(action); scheduleCommit(); } } private void scheduleCommit() { synchronized (this) { boolean postponeReady = mPostponedTransactions != null && !mPostponedTransactions.isEmpty(); boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1; if (postponeReady || pendingReady) { mHost.getHandler().removeCallbacks(mExecCommit); mHost.getHandler().post(mExecCommit); } } }
那么如果不把task1中构造的RequestManagerFragment放到pendingRequestManagerFragments中,那么在执行task2的时候也会再重新构造一个RequestManagerFragment,并且往主线程中放一个消息msg2,这个时候就会出现重复add的情况。
所以在前面new 出来一个RequestManagerFragment,随后就把它放到pendingRequestManagerFragments中,那么task2再进来的时候从缓存中能取到,就不会再重新new和add了。
那么下一个问题来了,为什么会出现下面这行代码,add后又需要马上发一个消息remove掉?在前面阻止掉task2重复new和add的操作后,就把这个缓存删掉,可以避免内存泄漏和内存压力:
// RequestManagerRetriever.java pendingRequestManagerFragments.put(fm, current); fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss(); handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
四、Glide如何监听网络变化
从上面页面生命周期的分析部分知道,对于任务的控制都是通过RequestManager,还是到它里面去看,实现网络变化监听的就是ConnectivityMonitor:
// RequestManager.java public class RequestManager implements LifecycleListener, ModelTypes<RequestBuilder<Drawable>> { ... protected final Glide glide; protected final Context context; @Synthetic final Lifecycle lifecycle; private final RequestTracker requestTracker; private final RequestManagerTreeNode treeNode; private final TargetTracker targetTracker = new TargetTracker(); private final Handler mainHandler = new Handler(Looper.getMainLooper()); private final ConnectivityMonitor connectivityMonitor; ... RequestManager( Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) { this.glide = glide; this.lifecycle = lifecycle; this.treeNode = treeNode; this.requestTracker = requestTracker; this.context = context; connectivityMonitor = factory.build( context.getApplicationContext(), new RequestManagerConnectivityListener(requestTracker)); if (Util.isOnBackgroundThread()) { mainHandler.post(addSelfToLifecycle); } else { lifecycle.addListener(this); } lifecycle.addListener(connectivityMonitor); ... }
所以也是把它注册为ActivityFragmentLifecycle的观察者,ConnectivityMonitor通过ConnectivityMonitorFactory进行构造,提供了默认实现类DefaultConnectivityMonitorFactory:
// DefaultConnectivityMonitorFactory.java public class DefaultConnectivityMonitorFactory implements ConnectivityMonitorFactory { private static final String TAG = "ConnectivityMonitor"; private static final String NETWORK_PERMISSION = "android.permission.ACCESS_NETWORK_STATE"; @NonNull @Override public ConnectivityMonitor build( @NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener) { int permissionResult = ContextCompat.checkSelfPermission(context, NETWORK_PERMISSION); boolean hasPermission = permissionResult == PackageManager.PERMISSION_GRANTED; return hasPermission ? new DefaultConnectivityMonitor(context, listener) : new NullConnectivityMonitor(); } }
接着就往下看DefaultConnectivityMonitor, 在onStart中registerReceiver监听手机网络状态变化的广播,然后在connectivityReceiver中调用isConnect进行网络状态确认,根据网络状态是否变化,如果有变化就回调监听ConnectivityMonitor.ConnectivityListener:
final class DefaultConnectivityMonitor implements ConnectivityMonitor { private static final String TAG = "ConnectivityMonitor"; private final Context context; @SuppressWarnings("WeakerAccess") @Synthetic final ConnectivityListener listener; @SuppressWarnings("WeakerAccess") @Synthetic boolean isConnected; private boolean isRegistered; private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() { @Override public void onReceive(@NonNull Context context, Intent intent) { boolean wasConnected = isConnected; isConnected = isConnected(context); if (wasConnected != isConnected) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "connectivity changed, isConnected: " + isConnected); } listener.onConnectivityChanged(isConnected); } } }; DefaultConnectivityMonitor(@NonNull Context context, @NonNull ConnectivityListener listener) { this.context = context.getApplicationContext(); this.listener = listener; } private void register() { if (isRegistered) { return; } // Initialize isConnected. isConnected = isConnected(context); try { // See #1405 context.registerReceiver(connectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); isRegistered = true; } catch (SecurityException e) { // See #1417, registering the receiver can throw SecurityException. if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to register", e); } } } private void unregister() { if (!isRegistered) { return; } context.unregisterReceiver(connectivityReceiver); isRegistered = false; } @SuppressWarnings("WeakerAccess") @Synthetic // Permissions are checked in the factory instead. @SuppressLint("MissingPermission") boolean isConnected(@NonNull Context context) { ConnectivityManager connectivityManager = Preconditions.checkNotNull( (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)); NetworkInfo networkInfo; try { networkInfo = connectivityManager.getActiveNetworkInfo(); } catch (RuntimeException e) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e); } // Default to true; return true; } return networkInfo != null && networkInfo.isConnected(); } @Override public void onStart() { register(); } @Override public void onStop() { unregister(); } @Override public void onDestroy() { // Do nothing. } }
ConnectivityMonitor.ConnectivityListener是在RequestManager中传入,有网络重新连接后重启请求:
// RequestManager.java private static class RequestManagerConnectivityListener implements ConnectivityMonitor .ConnectivityListener { private final RequestTracker requestTracker; RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) { this.requestTracker = requestTracker; } @Override public void onConnectivityChanged(boolean isConnected) { if (isConnected) { requestTracker.restartRequests(); } } }
五、Glide如何监测内存
在Glide构造的时候会调用registerComponentCallbacks进行全局注册, 系统在内存紧张的时候回调onTrimMemory,然后根据系统内存紧张级别进行memoryCache/bitmapPool/arrayPool的回收:
// Glide.java public static Glide get(@NonNull Context context) { if (glide == null) { synchronized (Glide.class) { if (glide == null) { checkAndInitializeGlide(context); } } } return glide; } private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) { Context applicationContext = context.getApplicationContext(); ... applicationContext.registerComponentCallbacks(glide); Glide.glide = glide; } @Override public void onTrimMemory(int level) { trimMemory(level); } public void trimMemory(int level) { Util.assertMainThread(); memoryCache.trimMemory(level); bitmapPool.trimMemory(level); arrayPool.trimMemory(level); }
六、总结
再回顾前面的四个问题,我相信聪明的你已经有了答案,文章的各小节标题就是根据问题来进行分析的,这么就不再赘述了,要不有凑字数的嫌疑。Glide的源码是比较庞大而且高质量的,所以一两篇文章是说不清楚的,后面对于Glide的源码分析还会有后续的文章,欢迎关注。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
在 Cloudera Data Flow 上运行你的第一个 Flink 例子
文档编写目的 Cloudera Data Flow(CDF) 作为 Cloudera 一个独立的产品单元,围绕着实时数据采集,实时数据处理和实时数据分析有多个不同的功能模块,如下图所示: 图中 4 个功能模块从左到右分别解释如下: Cloudera Edge Management(CEM),主要是指在边缘设备如传感器上部署 MiNiFi 的 agent 后用于采集数据。 Cloudera Flow Management(CFM),主要是使用 Apache NiFi 通过界面化拖拽的方式实现数据采集,处理和转换。 Cloudera Streaming Processing(CSP),主要包括 Apache Kafka,Kafka Streams,Kafka 的监控 Streams Messaging Manager(SMM),以及跨集群 Kafka topic 的数据复制 Streams Replication Manager(SRM)。 Cloudera Streaming Analytics(CSA),以前这块是使用 Storm 来作为 Native Streaming 来补充 Sp...
- 下一篇
Java实用教程系列之对象的转型
Java今日分享实用的Java教程之对象的转型 体现: 父类的引用可以指向子类的对象接口的引用可以指向实现类的对象转型: 向上转型由子类类型转型为父类类型,或者由实现类类型转型为接口类型向上转型一定会成功,是一个隐式转换向上转型后的对象,将只能访问父类或者接口中的成员向下转型由父类类型转型为子类类型,或者由接口类型转型为实现类类型向下转型可能会失败,是一个显式转换向下转型后的对象,将可以访问子类或者实现类中特有的成员instanceof关键字针对于向下转型的。 如果向下转型不成功,会怎样? 会有一个异常 ClassCastException如何避免这种情况? 在向下转型之前,我们先判断一下这个对象是不是要转型的类型 怎么判断? 关键字 instanceof Animal animal = new Dog();if (animal instanceof Dog) { // 说明animal的确是一个Dog }如果一个类中重写了父类的某一个方法。此时: 如果用这个类的对象来调用这个方法,最终执行的是子类的实现。如果用向上转型后的对象来调用这个方法,执行的依然是子类的实现。因为向上转型后的对...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- CentOS8编译安装MySQL8.0.19
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7设置SWAP分区,小内存服务器的救世主