Android Handler用法

Android Handler用法

  • 为什么要设计Handler机制?
  • Handler的用法
    • 1、创建Handler
    • 2、Handler通信
      • 2.1 sendMessage 方式
      • 2.2 post 方式
  • Handler常用方法
    • 1、延时执行
    • 2、周期执行
  • HandlerThread用法
    • 主线程-创建Handler
    • 子线程-创建Handler
  • FAQ
    • Message是如何创建
    • 主线程中Looper的轮询死循环为何没有阻塞主线程
    • Handler内存泄漏问题及解决方案
    • Handler为什么会持有Activity的引用?

参考文档:https://developer.android.google.cn/guide/components/processes-and-threads?hl=zh-cn#java
在这里插入图片描述

Android UI操作并非线程安全。因此,请不要在工作线程(即子线程)中操纵界面。您可以通过界面线程对界面进行所有操作。Android 的单线程模型有以下两条规则:

  • 请勿阻塞UI线程
  • 请勿从UI线程以外的线程进行UI操作

为什么要设计Handler机制?

一个耗时的操作,比如需要联网读取数据或者读取本地较大的一个文件或者数据库查询,如果把这些操作放在主线程中,界面会出现假死现象, 如果5秒钟还没有完成的话,阻塞了UI线程会收到Android系统的一个错误提示 “强制关闭”,这糟糕的体验会造成严重的损失,所以不能阻塞UI线程,以确保应用界面的响应能力。
这时候就需要把这些耗时的操作,放到子线程中去执行,但是在子线程执行完以后,又需要将结果更新到UI页面,此时就涉及到子线程UI更新的操作。那为什么安卓规定不能在子线更新UI? 最根本的原因是多线程并发的问题,假设在一个Activity中,有多个线程去更新UI,并且都没有加锁机制,就会产生更新界面错乱,所以子线程中更新UI是不安全的,而在一个线程中更新UI是变得比较合理,那自然就是主线程了,当然主线程也可以叫UI线程了。
综上所述,所以安卓应用需要这样的机制:

  1. 只能在主线程更新UI,并且所有更新UI的操作,都要在主线程的消息队列当中去轮询处理。
  2. 耗时的操作在子线程,更新UI的操作在主线程,他们之间的交互需要实现线程间通信。

Handler用于实现多线程通信和管理UI线程的消息处理。它为开发者提供了一种简单有效的方法来处理异步任务和更新UI界面。
当然,Android已经提供了多种从其他线程访问UI线程的方式。以下列出了几种有用的方法:

  • Activity.runOnUiThread(Runnable)
  • View.post(Runnable)
  • Handler.post(Runnable)

Activity.runOnUiThread(Runnable)方法:如果在UI线程,直接更新UI;如果非UI线程,使用的是post。

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();runOnUiThread(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

View.post(Runnable) 方法:

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();findViewById(R.id.button_send).post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

Handler.post(Runnable)方法

private Handler mHandler =new Handler();  //默认主线程
...
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();mHandlers.post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}});}
}).start();

随着操作变得越来越复杂,这种代码也会变得复杂且难以维护。为了处理与工作线程(子线程)的更复杂的交互,建议在UI线程中使用 Handler 处理从子线程传送的消息。

工作原理:Handler运行在主线程(UI线程)中, 它与子线程可以通过Message对象来传递数据和消息,然后把这些消息放入主线程队列中,按照先进先出的原则配合主线程逐个进行更新UI的操作。另外,驱动这套机制运行的核心是Looper.loop() 里的死循环。

@Override
public void run() {mTid = Process.myTid();Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。synchronized (this) {mLooper = Looper.myLooper();  // 获取准备好的Looper对象notifyAll();}Process.setThreadPriority(mPriority);onLooperPrepared();Looper.loop();  // 启动LoopermTid = -1;
}

Handler的用法

下面的这种写法是可以实现刷新UI的功能,但是它违背了单线程模型:Android UI操作并不是线程安全的,并且这些操作必须在UI线程中执行。但是如果与UI无关的操作如上传/下载,数据库,可以使用此种写法。

new Thread(new Runnable() {@Overridepublic void run() {textviewCurrentStatus.invalidate();}
}).start();

优点:避免了创建新线程带来的线程切换开销。
缺点:Handler发送的消息会保存在消息队列中,如果一直发送大量的消息,将可能导致消息队列过长,影响应用的响应能力。LiveData和RxJava等现在比较流行的框架,能够替代Handler实现更优异的异步编程和UI更新。

1、创建Handler

一般在主线程中创建Handler如下:

private Handler mHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}super.handleMessage(msg);}
};

在子线程中创建Handler,最好使用HandlerThread。如果不使用HandlerThread,必须要手动启动Looper,具体如下:

private Handler mHandler;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程进行耗时操作doSomethings();Looper.prepare();  // 准备LoopermHandler= new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {// todo 实际还是在主线程更新UIdoSomethingsAboutUI();return false;}});Looper.loop();    // 启动Looper}}).start();
}

2、Handler通信

使用Handler通信,有两种方法将消息加入消息队列中:post()方法和sendMessage()方法。
– sendMessage()方法是异步方式。即加入消息到消息队列中后,不会立即执行此消息,而是等待消息阻塞的处理程序返回。 — 存疑
– post()方法是同步方式。即加入消息到消息队列中后,会直接处理此消息,不必等待消息阻塞的处理程序返回。— 存疑

2.1 sendMessage 方式

首先,需要定义好handler需要处理的业务。

private Handler myHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}super.handleMessage(msg);}
};

在需要的时机,发送消息触发handler调用业务。

new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = new Message(); //或者Message msg = mHandler.obtainMessage();message.what = 1;msg.arg1 = 100;msg.obj = "message content";myHandler.sendMessage(message);}
}).start();

另外一种常见写法,本质都是一样的:

private Handler mHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {switch (msg.what){case 1:// todo 在主线程更新UIdoSomethingsAboutUI();break;}return false;}
});
....
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}
}).start();

2.2 post 方式

通过调用 Handler 的 post() 方法,将 Runnable 对象通过 MessageQueue 发送到消息队列中,即可让主线程处理相应的操作。这种方式可以用于解决在子线程中不能进行 UI 操作的问题,例如我们可以在子线程中通过 post 方式将更新 UI 的任务传递到主线程来完成,这样就不会因为在非 UI 线程中更新 UI 而导致 ANR(Application Not Responding)了。

private Handler mHandler =new Handler();
...
mHandler.post(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}
});

注意:post方法虽然发送的是一个实现了Runnable接口的类对象,但是它并非创建了一个新线程,而是执行了该对象中的run方法。也就是说,整个run中的操作和主线程处于同一个线程。这样对于那些简单的操作,似乎并不会影响。但是对于耗时较长的操作,就会出现“假死”。为了解决这个问题,就需要使得handler绑定到一个新开启线程的消息队列上,在这个处于另外线程的上的消息队列中处理传过来的Runnable对象和消息。

在主线程中使用Handler,可以直接使用getMainLooper()获取主线程Looper对象,并创建Handler实例。例如,在Activity中实现在子线程中更新UI:

private Handler mHandler = new Handler(Looper.getMainLooper());
...
new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomething();mHandler.post(new Runnable() {@Overridepublic void run() {// todo 在主线程更新UIdoSomethingsAboutUI();}});}
}).start();

Handler常用方法

1、延时执行

3秒后执行UI更新的代码。

mHandler.postDelayed(new Runnable() {@Overridepublic void run() {// todo UI更新代码doSomethingsAboutUI();}
},3000);

2、周期执行

有时候需要按时反复周期性的执行一些任务
2.1 使用Timer和TimerTask 实现

private Timer mTimer = new Timer();
private TimerTask mTimerTask = new TimerTask() {@Overridepublic void run() {Message message = new Message();message.what = 1;mHandler.sendMessage(message);}
};private Handler mHandler = new Handler() {public void handleMessage(Message msg) {switch (msg.what) {case 1:// todo 处理定时或周期性的业务doSomethings();break;}super.handleMessage(msg);}
};

在需要触发的时机,调用即可

mTimer.schedule(mTimerTask, 10000);

2.2 使用postDelayed和sendMessage实现

private int index = 0;private Handler mHandler = new Handler(Looper.getMainLooper()) {public void handleMessage(Message msg) {switch (msg.what) {case 1:updateOnTick();break;}super.handleMessage(msg);}
};private void updateOnTick(){mHandler.postDelayed(new Runnable() {@Overridepublic void run() {// todo 周期性执行doSomethings();Log.d(TAG, "run: " + (index++));mHandler.sendMessage(Message.obtain(mHandler,1));}}, 2000);  // 2秒执行一次
}

HandlerThread用法

HandlerThread常用于需要在后台执行耗时任务,并与UI线程进行交互的场景。比如,每隔6秒需要切换一下TextView的显示数据,虽然可以在UI线程中执行,但是这样的操作长时间占用UI线程,很容易让UI线程卡顿甚至崩溃,所以最好在子线程HandlerThread中调用这种业务。
HandlerThread能新建一个拥有Looper的线程。这个Looper能够用来新建其他的Handler。但需要注意的是,新建的HandlerThread需要及时回收,否则容易内存泄露。

非UI线程的业务也可以使用HandlerThread消息机制,因为不会干扰或阻塞UI线程,而且通过消息可以多次重复使用当前线程,也可以多个Handler也可以共享一个Looper,节省开支。

一个线程只能创建一个Looper,一个Looper个创建多个Handler。

使用HandlerThread可以实现以下功能和优势:

  1. 后台线程执行任务:HandlerThread在后台创建一个工作线程,可以在该线程中执行耗时任务,而不会阻塞UI线程,保证了应用的响应性和流畅性。
  2. 消息处理和线程间通信:HandlerThread内部封装了Looper和Handler,可以轻松地实现消息的发送和处理,以及线程间的通信。通过HandlerThread,可以将耗时任务的结果发送到UI线程进行更新,或者接收UI线程发送的消息进行处理。
  3. 简化线程管理:HandlerThread将线程的创建和管理进行了封装,开发人员只需要关注业务逻辑的实现,而无需手动创建和管理线程,减少了线程管理的复杂性。

主线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);mHandlerThread = new HandlerThread("子线程HandlerThread");mHandlerThread.start();mHandler= new Handler(mHandlerThread.getLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);switch (msg.what){case 1:Log.d(TAG, "handleMessage: " + mHandlerThread.getName());// todo 在主线程更新UIdoSomethingsAboutUI();break;}}};
}
...
private void sendMessages(){new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}}).start();
}@Override
protected void onPause() {super.onPause();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);
}@Override
protected void onDestroy() {super.onDestroy();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);// 释放资源mHandlerThread.quit();
}

子线程-创建Handler

private Handler mHandler;
private HandlerThread mHandlerThread;@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);new Thread(new Runnable() {@Overridepublic void run() {mHandlerThread = new HandlerThread("子线程HandlerThread");mHandlerThread.start();mHandler= new Handler(mHandlerThread.getLooper()) {@Overridepublic void handleMessage(@NonNull Message msg) {super.handleMessage(msg);switch (msg.what){case 1:Log.d(TAG, "handleMessage: " + mHandlerThread.getName());// todo 在主线程更新UIdoSomethingsAboutUI();break;}}};}}).start();
}
...
private void sendMessages(){new Thread(new Runnable() {@Overridepublic void run() {// todo 在子线程中进行耗时操作doSomethings();Log.d(TAG, "sendMessages: 耗时操作");Message message = mHandler.obtainMessage(1);mHandler.sendMessage(message);}}).start();
}@Override
protected void onPause() {super.onPause();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);  // 删除所有消息 mHandler.removeCallbacksAndMessages(null);
}@Override
protected void onDestroy() {super.onDestroy();// 防止退出界面后Handler还在执行mHandler.removeMessages(1);   // 删除所有消息 mHandler.removeCallbacksAndMessages(null);// 释放资源mHandlerThread.quit();
}

注意:1、子线程中创建了Looper,当没有消息的时候子线程将会被block,无法被回收,所以我们需要手动调用quit 方法将消息删除并且唤醒looper,然后next方法返回null退出loop。
2、在主线程和子线程中,使用HandlerThread创建Handler,基本没有区别。但如果没有使用HandlerThread,在子线程中需要先创建Looper,再创建Handler。具体如下:

private Handler mHandler ;
...
new Thread(new Runnable() {@Overridepublic void run() {Looper.prepare(); // 创建Looper的子线程,然后创建MessageQueue,最后进行绑定。mHandler = new Handler(new Handler.Callback() {@Overridepublic boolean handleMessage(@NonNull Message msg) {return false;}});Looper.loop();  // 启动Looper}
}).start();

FAQ

Message是如何创建

首先考虑一个问题,屏幕刷新率 60Hz(即每秒刷新60次),每次刷新要用到 3 个 Message,也就是每秒钟至少要创建180个Message。这样不断的创建回收,就会出现内存抖动的问题,从而导致 GC、屏幕卡顿等问题。
为了解决上面的问题,采用 Message 了享元的设计模式,使用 obtain() 方法创建。在Handler 中创建两个线程池队列,一个是我们比较熟悉的 MessageQueue,另一个就是回收池 sPool(最大长度是 50 复用池)。MessageQueue 中 Message 回收时,我们将清空数据的 Message 放回到 sPool 队列中。创建 Manager,我们直接从 sPool 池中取出来就可以了。
应用场景:地图、股票、RecyclerView复用等对数据的处理都使用了享元模式。

主线程中Looper的轮询死循环为何没有阻塞主线程

Looper 轮询是死循环,但是当没有消息的时候他会 block(阻塞, 阻塞代码没有执行计时操作),ANR 是当我们处理点击事件的时候 5s 内没有响应,我们在处理点击事件的时候也是用的 Handler,所以一定会有消息执行,并且 ANR 也会发送 Handler 消息,所以不会阻塞主线程。Looper 是通过 Linux 系统的 epoll 实现的阻塞式等待消息执行(有消息执行无消息阻塞),而 ANR 是消息处理耗时太长导致无法执行剩下需要被执行的消息触发了 ANR。Handler底层为什么用epoll,为什么不用select和poll? Socket 非阻塞 IO 中 select 需要全部轮询不适合,poll 也是需要遍历和 copy,效率太低了。epoll 非阻塞式 IO,使用句柄获取 APP 对象,epoll 无遍历,无拷贝。还使用了红黑树(解决查询慢的问题)。

Handler内存泄漏问题及解决方案

内部类持有外部类的引用导致了内存泄漏,如果 Activity 退出的时候,MessageQueue中还有一个 Message 没有执行,这个 Message 持有了 Handler 的引用,而 Handler 持有了 Activity 的引用,导致 Activity 无法被回收,导致内存泄漏。这种问题可以使用 static 关键字修饰,在 onDestory 的时候将消息清除。
简单理解:当 Handler 为非静态内部类时,其持有外部类 Activity 对象,所以导致 static Looper -> mMainLooper -> MessageQueue -> Message -> Handler -> MainActivity,这个引用链中 Message 如果还在 MessageQueue 中等待执行,则会导致 Activity 一直无法被释放和回收。
根本原因:因为Looper需要循环处理消息,但一个线程只有一个Looper,而一个线程中可以有多个Handler,MessageQueue中消息Message 执行时不知道要通知哪个Handler执行任务,所以在Message创建时target引用了Handler对象,用于回调执行的消息。
如果Handler是Activity这种短生命周期对象的非静态内部类时,则创建出来的Handler对象会持有该外部类Activity的引用,当页面销毁时,还在队列的Message持有着Handler对象,而Handler正持有着外部类Activity,就会导致 Activity无法被gc回收,从而导致内存泄漏。
解决办法:
1、Handler不能是Activity这种短生命周期的对象类的内部类;
2、在 Activity销毁时,将创建的 Handler中的消息队列清空并结束所有任务。
3、将handler设置成static,static变量是全局变量,不能够自动引用外部类变量,这时Handler 就不再持有 Activity,Activity就可以正常释放。

Handler为什么会持有Activity的引用?

创建Handler时,采用的是匿名内部类或者成员内部类的方式,而内部类会默认持有外部类的引用,也就是Handler对象会默认持有Activity的引用。

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

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

相关文章

云端芳华、运维之美

今天,在我们享受互联网服务带来的便利与高效的同时,有一群人默默地在幕后为我们提供支持,他们就是云端运维人员。 值此五一国际劳动节来临之际,我们要真诚感谢他们辛勤的劳动和奉献!

springcloud自定义全局异常

自行创建一个实体类 /*** 全局异常处理类**/ ControllerAdvice public class GlobalExceptionHandler {ExceptionHandler(Exception.class) ResponseBody public Result error(Exception e){e.printStackTrace(); return Result.fail();}/*** 自定义异常处理方法* param e * re…

2023-2024年汽车行业报告/方案合集(精选345份)

汽车行业报告/方案(精选345份) 2023-2024年 来源:2023-2024年汽车行业报告/方案合集(精选345份) 【以下是资料目录】 2023中国汽车科技50强 2023中国智能汽车产业发展与展望 2023比亚迪海豹汽车拆解报告 2023新能…

unity制作app(3)--gps定位

1.unity中定位Unity之GPS定位(高德解析)_unity gps定位-CSDN博客 代码需要稍微修改一下,先把脚本绑到一个button上试一试! 2.先去高德地图认证(app定位) 创建应用和 Key-Web服务 API | 高德地图API (ama…

C#知识|汇总方法重载与静态方法应用技巧

哈喽,你好,我是雷工! 今天学习C#方法重载与静态方法应用技巧的相关内容。 01 方法重载有什么好处? 1.1、可以有效的减少类的对外接口(只显示一个方法比较简洁),从而降低类的复杂度。 1.2、方便…

【Vue 2.x】学习vue之二组件

文章目录 Vue二组件第五章es6文件导入出1、导出export 组件(component)1、定义2、模块化与组件化3、组件的分类1、非单文件组件非单文件三步骤创建组件标准写法简化写法组件的嵌套非单文件的不足之处 2、单文件组件vue单文件组件的使用脚手架创建项目重点…

qml之text

显示字体 Text {text: "The quick brown fox"color: "#303030"font.family: "Ubuntu"font.pixelSize: 28x:200y:200}Text {width:84elide: Text.ElideMiddle y:text1.ytext1.height20text: "一个很长很长的句子 !!!!!!!!!!"style…

全新神经网络架构KAN一夜爆火!200参数顶30万,MIT华人一作 | 最新快讯

白交衡宇发自凹非寺 量子位公众号 QbitAI 一种全新的神经网络架构 KAN,诞生了! 与传统的 MLP 架构截然不同,且能用更少的参数在数学、物理问题上取得更高精度。 比如,200 个参数的 KANs,就能复现 DeepMind 用 30 万参数…

Linux 进程间通信之匿名管道

💓博主CSDN主页:麻辣韭菜💓   ⏩专栏分类:Linux知识分享⏪   🚚代码仓库:Linux代码练习🚚   🌹关注我🫵带你学习更多Linux知识   🔝 目录 前言 一. 进程间通信介绍 1.进程间通…

【linuxC语言】stat函数

文章目录 前言一、stat函数二、示例代码总结 前言 在Linux系统编程中,stat() 函数是一个非常重要的工具,用于获取文件的元数据信息。无论是在系统管理、文件处理还是应用开发中,都可能会用到 stat() 函数。通过调用 stat() 函数,…

AI视频教程下载:用ChatGPT提示词开发AI应用和GPTs

在这个课程中,你将深入ChatGPT的迷人世界,学习如何利用其能力构建创新和有影响力的工具。你将发现如何创建不仅吸引而且保持用户参与度的应用程序,将流量驱动到你的网站,并开辟新的货币化途径。 **课程的主要特点:** …

Hive优化以及相关参数设置

1.表层面设计优化 1.1 表分区 分区表实际上就是对应一个 HDFS 文件系统上的独立的文件夹,该文件夹下是该分区所有的数据文件。Hive 中的分区就是分目录,把一个大的数据集根据业务需要分割成小的数据集。在查询时通过 WHERE 子句中的表达式选择查询所需要…

抖音小店运营实战班,全新升级 从零到进阶精通 分享月销百万小店核心秘密

课程内容: 1 2024抖音电商发展趋势及抖店运营策略(直播2024 0412).mp4 2 1-1抖音小店入驻流程(直播2024 04 12),mp4 3 1-2个体店铺VS企业店铺有什么区别(直播20240412).mp4 4 1-3抖音小店店铺搭建(直播2024 04 12).mp4 5 2-1-如何避免违禁词(附违禁词大全)(直播…

微软如何打造数字零售力航母系列科普07 - Azure PlayFab:你从未想过的世界上最大的开发工具(平台)

Azure PlayFab:你从未想过的世界上最大的开发工具 微软的James Gwertzman告诉GamesIndustry.biz Academy他帮助开发者成功的使命 制作游戏比以往任何时候都更容易上手。现在有无数的游戏引擎可供选择,其中大多数是免费的,PC空间的店面也同样重…

链表经典面试题上

目录 创作不易,如若对您有帮助,还望三连,谢谢!!! 题目一:203. 移除链表元素 - 力扣(LeetCode) 题目二:206. 反转链表 - 力扣(LeetCode&#xff…

python学习笔记----安装pycharm(1)

一、安装pycharm 1. 下载并安装pycharm https://www.jetbrains.com/pycharm/download2.汉化pycharm 安装插件并重启IDE完成汉化 二、 第一个python程序

【机器学习】机器学习在教育领域的应用场景探索

🧑 作者简介:阿里巴巴嵌入式技术专家,深耕嵌入式人工智能领域,具备多年的嵌入式硬件产品研发管理经验。 📒 博客介绍:分享嵌入式开发领域的相关知识、经验、思考和感悟,欢迎关注。提供嵌入式方向…

axios 中文文档 翻译

0.18.0的版本更新有一段时间了,使用起来跟原先基本没有什么变化。但是增加了一些功能,例如错误处理的辨别,于07-06-2018重新翻译和校验了该翻译,更正了一些错别字和表达不准的地方,但是难免仍有错误,欢迎指…

GitLab服务器的搭建

GitLab服务器的搭建 为公司搭建一台代码托管服务器 服务器规格:2vCPUs4GiB20G 操作系统:RockyLinux8.8 下载软件 gitlab官网:http://about.gitlab.com 在官网下载比较麻烦,推荐从《清华大学开源软件镜像站》下载 清华大学开…

38-3 Web应用防火墙 - 安装配置WAF

首先需要安装Centos 7 虚拟机:Centos7超详细安装教程_centos7安装教程-CSDN博客 安装配置WAF 在桌面环境中,右键点击打开终端,首先执行以下步骤: 1)安装必要的工具: 输入命令: sudo su yum install -y wget epel-release 2)第二步,安装依赖工具,输入以下命令: y…