当前位置: 首页 > news >正文

Android 消息分发机制解读

前言

想必大家都知道Android系统有自己的一套消息分发机制,,从App启动那一刻起,App就创建了主线程的消息分发实例:Looper.sMainLooper,并开始无限循环,也就是App的心脏,一直跳动,负责协调分配来自各方的事件,让App不断响应用户操作,如果主线程出现了异常,也就是心脏跳动异常停止,那么App的生命随之终止,也就是常见的‘进程已停止运行’。那么,你有没有想过,既然他在一直无限循环,为什么没有卡死呢?为什么能看到“应用无响应”?怎么保证界面刷新不受其他事件影响,怎么做到有条不理的处理每一条消息等等这些问题呢,作为一名Android开发者,我想我们有必要对其结构进行简单了解。

思路整理

基于消息分发机制,我们可以从以下几个方面由深到浅去解惑:

  1. Message
  2. MessageQueue 的核心逻辑;
  3. Looper的核心逻辑;
  4. Handler机制;
    在阅读前,你可能需要对数据结构单链表有一定的了解。

源码基于 Android API 33

Message

消息对象,部分源码:


public final class Message implements Parcelable {///*** The targeted delivery time of this message. The time-base is* {@link SystemClock#uptimeMillis}.* @hide Only for use within the tests.*/@UnsupportedAppUsage@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)public long when;//@UnsupportedAppUsage/*package*/ Handler target;//是否为异步消息/*** Returns true if the message is asynchronous, meaning that it is not* subject to {@link Looper} synchronization barriers.** @return True if the message is asynchronous.** @see #setAsynchronous(boolean)*/public boolean isAsynchronous() {return (flags & FLAG_ASYNCHRONOUS) != 0;}
}

关于此类,需要知道的是,我们外部创建的target一般不为空,为空一般是系统内部创建的消息,比如执行Viewinvalidate()就是发送了target为空的异步消息,具体看 消息队列中的分发逻辑。

MessageQueue 消息队列

顾名思义,是一个存放消息Message的队列,主要负责管理消息的插入和取出。每个消息都有对应的创建时间,插入队列中的消息会按时间排序,当调用next时会从队列中取出一条符合条件的Message,如果没有,则next函数会进入休眠状态,直到被唤醒。为了方便理解,下面对该类的核心方法,核心变量分开分析。
大致结构如下:

//源码位于 android.os.MessageQueue @UnsupportedAppUsage
@SuppressWarnings("unused")
private long mPtr; // used by native code @UnsupportedAppUsage
Message mMessages;boolean enqueueMessage(Message msg, long when){}
Message next() {}
@UnsupportedAppUsage
@TestApi
public int postSyncBarrier(){}@UnsupportedAppUsage
@TestApi
public void removeSyncBarrier(int token) {}

mPtr

看起来就是一个指针,源码中注释为: used by native code,也就是说是在Native层用的代码,我们只需知道他是一道闸,堵塞next方法用的。

mMessages

队列的头部消息。

postSyncBarrier ,removeSyncBarrier

这两个方法是发送和移除同步屏障消息,我们可以把它想象成为一道栅栏,能拦截所有同步消息,只允许异步信息通过。当向队列中插入该消息后,消息分发会主动跳过所有同步消息,即使队列中没有异步消息,直至屏障被移除。
不难发现,调用postSyncBarrier时,会返回一个int类型,也就是屏障的标识,自然移除屏障的时候需要传入这个标识。
至于为什么加入这个机制,大致因为,加入队列中有很多消息,此时用户点击了一下界面,如果没有这个机制,那么响应用户点击的操作需要等待其他事件被分发完后才轮到,容易让用户觉得不流畅,所以系统为了UI相关的消息要优先被分发,在队列中插入同步屏障信号,之后响应点击的消息会被优先处理,处理完成后,再把信号移除,继续执行其他分发。
这个机制如果处理不好,屏障没有正常移除,就会出现进程假死的问题,这也就是官方为何把此方法标记成@UnsupportedAppUsage,不给开发者调用。
*那么这样就可以避免不会出问题吗?*不可能,这只是减少了问题的出现概率,还是有机会出现的,屏障信号是系统在更新UI时发送的,如果我们操作不当,频繁在子线程操作UI,可能某一瞬间,发送了超过两个屏障信号,但是只记录到最后一个token,更新完成后,自然只移除了最后添加的屏障,结果就是之前插入的一直挂在队列中,堵塞主线程所有同步消息,也就引发了同步屏障泄露 的问题,App就直接出现了明明有些控件明明还在刷新,但是怎么点都没反应。
这也是为什么官方只允许我们只能在主线程执行更新UI操作原因之一。

enqueueMessage

分发消息,把用户发的Message插入到队列中。关键源码:

boolean enqueueMessage(Message msg, long when){//target为空为屏障消息,屏障信息只能由系统代码发送,用户端发送的消息target不可能是空//这里就解析了为什么用户不能发送没有target的消息if (msg.target == null) {//    throw new IllegalArgumentException("Message must have a target.");}//......省略synchronized (this) {//......省略//1.把当前的对头赋值给pMessage p = mMessages;boolean needWake;//是否需要唤醒next函数if (p == null || when == 0 || when < p.when) { //2.A 如果对头为空(队列没有任何消息)或者when为0(只有刚开机才会存在)或者msg的时间比队头的时间早//把待分发消息插到对头// New head, wake up the event queue if blocked.   msg.next = p;   //把当前消息的next指向p 尽管p为空mMessages = msg; //把当前消息放到对头needWake = mBlocked;//?} else {   //2.B 队列中已有消息,这时需要把当前消息按时间顺序插到队列中// Inserted within the middle of the queue.  Usually we don't have to wake  // up the event queue unless there is a barrier at the head of the queue  // and the message is the earliest asynchronous message in the queue. //3、是否需要唤醒(正在堵塞、队头是屏障信号、当前消息是异步消息 )needWake = mBlocked && p.target == null && msg.isAsynchronous();   Message prev;   for (;;) {   //4、开始循环队列,和队列中消息when做比较,找出当前消息的when值在队列中最适合的位置prev = p;    //此时p的角色为当前遍历的消息,先赋值给上一条消息,保证每次循环能拿到上一条消息和下一条消息,方便插入p = p.next;     //取下一条消息,赋值到pif (p == null || when < p.when) {  //5、如果下一条消息是空(说明已到达队尾),或者当前消息的时刻比下一条消息的时刻小,说明此时的位置最适合,结束循环break;      }//队头符合步骤3处条件,说明有同步屏障信号,并且当前p是异步消息,根据步骤5,能走到这里说消息分发时机还没到,所以不需要唤醒nextif (needWake && p.isAsynchronous()) {   needWake = false;  }   }    //把当前消息入队,插入到上一条消息和p之间msg.next = p; // invariant: p == prev.next  prev.next = msg;}// We can assume mPtr != 0 because mQuitting is false.if (needWake) {   //唤醒next函数,取 消息 分发nativeWake(mPtr);}}return true;
}

next 取消息

从队列中取出一条可用消息(when时机合适),如果有就返回,没有则堵塞,直到mPtr被唤醒。

@UnsupportedAppUsageMessage next() {//......省略//1、下一次唤醒时间int nextPollTimeoutMillis = 0;for (;;) {//2、开始死循环取消息if (nextPollTimeoutMillis != 0) {Binder.flushPendingCommands();}//3、堵塞,时长为nextPollTimeoutMillis,或者主动调用唤醒函数`nativeWake`nativePollOnce(ptr, nextPollTimeoutMillis);//被唤醒后,开始取消息synchronized (this) {//同步锁,保证线程安全// Try to retrieve the next message.  Return if found.//记下当前系统时间,后面判断消息是否达到时间条件final long now = SystemClock.uptimeMillis();Message prevMsg = null;Message msg = mMessages;if (msg != null && msg.target == null) {//4、关键点,target为空,说明当前队头消息为屏障信号,需要优先处理离信号最近的异步消息// Stalled by a barrier.  Find the next asynchronous message in the queue.do {//一直在队列中寻找离信号最近的异步消息,直到没找到或者到达队尾prevMsg = msg;msg = msg.next;//如果一直找到队尾,msg.next是空,结束循环} while (msg != null && !msg.isAsynchronous());}if (msg != null) {//5、此时,如果有屏障信号的话,步骤4肯定走了,msg不为空肯定是异步消息,否则msg必为空//检查该消息是否以达到分发时机if (now < msg.when) {//当前时间还没达到消息的时机,计算还差多久,赋值给nextPollTimeoutMillis,进入下一次堵塞,直到达到时间nextPollTimeoutMillis,再取该消息// Next message is not ready.  Set a timeout to wake up when it is ready.nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);} else {//已达到取消息时机// Got a message.mBlocked = false;//把消息出队if (prevMsg != null) {//说明此消息在队列里面(同步屏障会优先执行里面的异步消息),要把消息出队,把该消息的上一条消息的next直接指向该消息的下一条消息prevMsg.next = msg.next;} else {//说明此消息是队头,直接把当前消息的next放到队头mMessages = msg.next;}//删除出队消息的next指向msg.next = null;if (DEBUG) Log.v(TAG, "Returning message: " + msg);//标注消息正在使用msg.markInUse();//把该消息返回return msg;}} else {//6、没有符合条件消息,继续下一次循环,取符合条件的消息//如果屏障信号没有被移除,又没有异步消息进入队列,那么next函数将陷入死循环,循环线路 3--》4--》6//导致APP所有同步消息无法被处理,表现为软件部分界面卡死(如文本正常刷新,点击事件无法响应)并且不会引发ANR// No more messages.nextPollTimeoutMillis = -1;}// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}// If first time idle, then get the number of idlers to run.// Idle handles only run if the queue is empty or if the first message// in the queue (possibly a barrier) is due to be handled in the future.if (pendingIdleHandlerCount < 0&& (mMessages == null || now < mMessages.when)) {pendingIdleHandlerCount = mIdleHandlers.size();}if (pendingIdleHandlerCount <= 0) {// No idle handlers to run.  Loop and wait some more.mBlocked = true;continue;}if (mPendingIdleHandlers == null) {mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)];}mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers);}// Run the idle handlers.// We only ever reach this code block during the first iteration.for (int i = 0; i < pendingIdleHandlerCount; i++) {final IdleHandler idler = mPendingIdleHandlers[i];mPendingIdleHandlers[i] = null; // release the reference to the handlerboolean keep = false;try {keep = idler.queueIdle();} catch (Throwable t) {Log.wtf(TAG, "IdleHandler threw exception", t);}if (!keep) {synchronized (this) {mIdleHandlers.remove(idler);}}}// Reset the idle handler count to 0 so we do not run them again.pendingIdleHandlerCount = 0;// While calling an idle handler, a new message could have been delivered// so go back and look again for a pending message without waiting.nextPollTimeoutMillis = 0;}}

Looper

一个无限循环的类,他内部持有消息队列MessageQueue的引用,当Looper.loop()后,将会一直调用MessageQueue.next函数获取消息,next在没有消息的时候又会堵塞,让出CPU资源,这就是为什么死循环却没有占满CPU的原因。关键源码:


public final class Looper {public static void prepare() {prepare(true);}public static Looper getMainLooper() {synchronized (Looper.class) {return sMainLooper;}}/*** 循环一次的逻辑* Poll and deliver single message, return true if the outer loop should continue.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")private static boolean loopOnce(final Looper me,final long ident, final int thresholdOverride) {//从队列中取一条消息Message msg = me.mQueue.next(); // might blockif (msg == null) {//由于next没消息会堵塞,所以没消息的时候这里不会执行,除非强制退出// No message indicates that the message queue is quitting.return false;}try {//开始分发取到的消息msg.target.dispatchMessage(msg);if (observer != null) {observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;} catch (Exception exception) {if (observer != null) {observer.dispatchingThrewException(token, msg, exception);}throw exception;} finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) {Trace.traceEnd(traceTag);}}//省略,,,,,,,,,,,,,,,,,,,,,,,// Make sure that during the course of dispatching the// identity of the thread wasn't corrupted.final long newIdent = Binder.clearCallingIdentity();if (ident != newIdent) {Log.wtf(TAG, "Thread identity changed from 0x"+ Long.toHexString(ident) + " to 0x"+ Long.toHexString(newIdent) + " while dispatching to "+ msg.target.getClass().getName() + " "+ msg.callback + " what=" + msg.what);}msg.recycleUnchecked();return true;}/*** 开始无限循环* Run the message queue in this thread. Be sure to call* {@link #quit()} to end the loop.*/@SuppressWarnings("AndroidFrameworkBinderIdentity")public static void loop() {final Looper me = myLooper();if (me == null) {throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");}if (me.mInLoop) {Slog.w(TAG, "Loop again would have the queued messages be executed"+ " before this one completed.");}me.mInLoop = true;// Make sure the identity of this thread is that of the local process,// and keep track of what that identity token actually is.Binder.clearCallingIdentity();final long ident = Binder.clearCallingIdentity();// Allow overriding a threshold with a system prop. e.g.// adb shell 'setprop log.looper.1000.main.slow 1 && stop && start'final int thresholdOverride =SystemProperties.getInt("log.looper."+ Process.myUid() + "."+ Thread.currentThread().getName()+ ".slow", 0);me.mSlowDeliveryDetected = false;for (;;) {//无限循环核心if (!loopOnce(me, ident, thresholdOverride)) {return;}}}
}

Handler

通过Handler,我们可以把消息发送到对应的消息队列中,是用户代码操作消息队列的入口。关键源码:

public class Handler {/*** Use the provided {@link Looper} instead of the default one.** @param looper The looper, must not be null.*/public Handler(@NonNull Looper looper) {this(looper, null, false);}/*** Use the provided {@link Looper} instead of the default one and take a callback* interface in which to handle messages.** @param looper The looper, must not be null.* @param callback The callback interface in which to handle messages, or null.*/public Handler(@NonNull Looper looper, @Nullable Callback callback) {this(looper, callback, false);}
}

上面只列举了几个构造函数,其他一些post消息函数就不一一说明了,主要是Handler创建需要关联一个Looper,发消息的时候又调用了Looper 内部的消息队列的分发消息函数,把消息插入到队列中,完成用户对消息队列的操作。

总结

至此,相信你对消息分发机制也有大概的理解。

相关文章:

Android 消息分发机制解读

前言 想必大家都知道Android系统有自己的一套消息分发机制&#xff0c;&#xff0c;从App启动那一刻起&#xff0c;App就创建了主线程的消息分发实例&#xff1a;Looper.sMainLooper,并开始无限循环&#xff0c;也就是App的心脏&#xff0c;一直跳动&#xff0c;负责协调分配来…...

【ML】LSTM应用——预测股票(基于 tensorflow2)

LSTM 应用预测股票数据 所用数据集&#xff1a;https://www.kaggle.com/datasets/yuanheqiuye/bank-stock 基于&#xff1a;tensorFlow 2.x 数据处理 import numpy as np import pandas as pd from matplotlib import pyplot as plt from sklearn.model_selection import tr…...

汇编语言程序设计实验报告

一、实验一 1、实验内容 &#xff08;1&#xff09;用Debug命令查看寄存器和内存中的内容 &#xff08;2&#xff09;上机过程及程序调试 2、实验目的 &#xff08;1&#xff09;要求掌握使用Debug命令查看寄存器和内存的方法&#xff1b; &#xff08;2&#xff09;通过…...

广域网(WAN)设备通信过程(通信流程、通信步骤、通信顺序、设备通信、主机通信)(MAC地址在本地链路中的作用)跳跃(hop)

文章目录 广域网&#xff08;WAN&#xff09;通信&#xff1a;MAC地址在本地链路中的作用引言MAC地址概述什么是MAC地址&#xff1f;如何工作&#xff1f; MAC地址与广域网MAC地址的局限性IP地址和路由 广域网设备通信过程1. 请求生成2. 封装数据帧3. 确定下一跳4. 数据传输5. …...

ExoPlayer架构详解与源码分析(10)——H264Reader

系列文章目录 ExoPlayer架构详解与源码分析&#xff08;1&#xff09;——前言 ExoPlayer架构详解与源码分析&#xff08;2&#xff09;——Player ExoPlayer架构详解与源码分析&#xff08;3&#xff09;——Timeline ExoPlayer架构详解与源码分析&#xff08;4&#xff09;—…...

智能优化算法应用:基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于粒子群算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.粒子群算法4.实验参数设定5.算法结果6.参考文…...

微积分-序言

大家好&#xff0c;这里我将为大家带来一个全新的专栏“微积分”。在这里我将为大家讲解微积分的内容&#xff0c;我会从最基础的内容开始讲解。争取让零基础的人也可以看懂和学会。 我也会在后续出一些微积分的题&#xff0c;让大家可以进行巩固和提高。 学习微积分那么就需要…...

ArchLinux安装详细步骤

下载&#xff08;略&#xff09;安装VirtualBox&#xff08;略&#xff09;新建虚拟机&#xff08;略&#xff09;启动 进入提示符 进入安装设置界面 archinstall出现界面&#xff1a; 逐项设置。 Disk我选择了ext4 在Profile中 我选择了KDE作为桌面&#xff08;选择后按回车…...

react 学习笔记 李立超老师 | (学习中~)

文章目录 react学习笔记01入门概述React 基础案例HelloWorld三个API介绍 JSXJSX 解构数组 创建react项目(手动)创建React项目(自动) | create-react-app事件处理React中的CSS样式内联样式 | 内联样式中使用state (不建议使用)外部样式表 | CSS Module React组件函数式组件和类组…...

Docker镜像和容器的简单操作

1.镜像管理 搜索镜像&#xff1a; 这种方法只能用于官方镜像库 搜索基于 centos 操作系统的镜像 # docker search centos 按星级搜索镜像&#xff1a; 查找 star 数至少为 100 的镜像&#xff0c;默认不加 s 选项找出所有相关 ubuntu 镜像&#xf…...

章鱼网络进展月报 | 2023.11.1-11.30

章鱼网络大事摘要 1、2023年12月&#xff0c;Octopus 2.0 将会正式启动。 2、隐私协议 Secret Network 宣布使用 Octopus Network 构建的 NEAR-IBC 连接 NEAR 生态。 3、Louis 受邀作为嘉宾&#xff0c;在 NEARCON2023 的多链网络主题沙龙中发言&#xff1a;我们依然处于区…...

基于Maven构建OSGI应用(Maven和OSGI结合)

基于Maven构建OSGI应用。 使用Maven来构建项目&#xff0c;包括项目的创建、子模块buldle的创建等。使用OSGI来实现动态模块化管理&#xff0c;实现模块的热插拔效果&#xff08;即插即用&#xff09;。 创建一个Maven项目&#xff1a;helloworld&#xff0c;并在该项目下创建…...

oracle分组排序后取第一条

在 Oracle 中&#xff0c;可以使用「ROW_NUMBER」函数对某个列进行分组并排序&#xff0c;然后通过「WHERE」语句取第一条记录。 假设有一张「USERS」表&#xff0c;其中包含「ID」、「NAME」、「AGE」和「COUNTRY」列&#xff0c;您可以使用以下 SQL 语句对「AGE」列进行分组…...

MAMBA介绍:一种新的可能超过Transformer的AI架构

有人说&#xff0c;“理解了人类的语言&#xff0c;就理解了世界”。一直以来&#xff0c;人工智能领域的学者和工程师们都试图让机器学习人类的语言和说话方式&#xff0c;但进展始终不大。因为人类的语言太复杂&#xff0c;太多样&#xff0c;而组成它背后的机制&#xff0c;…...

win系统一台电脑安装两个不同版本的mysql教程

文章目录 1.mysql下载zip包&#xff08;地址&#xff09;2.解压在你的电脑上&#xff08;不要再C盘和带中文的路径&#xff09;3.创建my.ini文件4.更改环境变量&#xff08;方便使用, 可选&#xff09;5.打包mysql服务6.初始化mysql的data7.启动刚刚打包的服务8.更改密码 1.mys…...

esp32-s3部署yolox_nano进行目标检测

ESP32-S3部署yolox_nano进行目标检测 一、生成模型部署项目01 环境02 配置TVM包03 模型量化3.1预处理3.2 量化 04 生成项目 二、烧录程序 手上的是ESP32-S3-WROOM-1 N8R8芯片&#xff0c;整个链路跑通了&#xff0c;但是识别速度太慢了&#xff0c;20秒一张图&#xff0c;所以暂…...

TCP传输数据的确认机制

实际的TCP收发数据的过程是双向的。 TCP采用这样的方式确认对方是否收到了数据&#xff0c;在得到对方确认之前&#xff0c;发送过的包都会保存在发送缓冲区中。如果对方没有返回某些包对应的ACK号&#xff0c;那么就重新发送这些包。 这一机制非常强大。通过这一机制&#xf…...

使用Ansible Expect模块实现自动化交互式任务

Ansible是一种功能强大的自动化工具&#xff0c;可用于自动化配置管理、部署和任务执行。其中的Expect模块是Ansible的一个重要组件&#xff0c;它允许我们自动化处理需要与交互式命令行进行交互的任务。本文将介绍如何使用Ansible的Expect模块&#xff0c;并提供一些示例来说明…...

51单片机独立按键以及矩阵按键的使用以及其原理--独立按键 K1 控制 D1 指示灯亮灭以及数码管显示矩阵按键 S1-S16 按下后键值 0-F

IO 的使用–按键 本文主要涉及8051单片机按键的使用&#xff0c;包括独立按键以及矩阵按键的使用以及其原理&#xff0c;其中代码实例包括: 1.独立按键 K1 控制 D1 指示灯亮灭 2.通过数码管显示矩阵按键 S1-S16 按下后键值 0-F 文章目录 IO 的使用--按键一、按键消抖二、独立按…...

chrome安装jsonview

写在前面 通过jsonview可以实现&#xff0c;当http响应时application/json时直接在浏览器格式化显示&#xff0c;增加可读性。本文看下如何安装该插件到chrome中。 1&#xff1a;安装 首先在这里 下载插件包&#xff0c;然后解压备用。接着在chrome按照如下步骤操作&#xf…...

Golang 面试经典题:map 的 key 可以是什么类型?哪些不可以?

Golang 面试经典题&#xff1a;map 的 key 可以是什么类型&#xff1f;哪些不可以&#xff1f; 在 Golang 的面试中&#xff0c;map 类型的使用是一个常见的考点&#xff0c;其中对 key 类型的合法性 是一道常被提及的基础却很容易被忽视的问题。本文将带你深入理解 Golang 中…...

在鸿蒙HarmonyOS 5中实现抖音风格的点赞功能

下面我将详细介绍如何使用HarmonyOS SDK在HarmonyOS 5中实现类似抖音的点赞功能&#xff0c;包括动画效果、数据同步和交互优化。 1. 基础点赞功能实现 1.1 创建数据模型 // VideoModel.ets export class VideoModel {id: string "";title: string ""…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)

概述 在 Swift 开发语言中&#xff0c;各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过&#xff0c;在涉及到多个子类派生于基类进行多态模拟的场景下&#xff0c;…...

JS手写代码篇----使用Promise封装AJAX请求

15、使用Promise封装AJAX请求 promise就有reject和resolve了&#xff0c;就不必写成功和失败的回调函数了 const BASEURL ./手写ajax/test.jsonfunction promiseAjax() {return new Promise((resolve, reject) > {const xhr new XMLHttpRequest();xhr.open("get&quo…...

如何更改默认 Crontab 编辑器 ?

在 Linux 领域中&#xff0c;crontab 是您可能经常遇到的一个术语。这个实用程序在类 unix 操作系统上可用&#xff0c;用于调度在预定义时间和间隔自动执行的任务。这对管理员和高级用户非常有益&#xff0c;允许他们自动执行各种系统任务。 编辑 Crontab 文件通常使用文本编…...

uniapp 开发ios, xcode 提交app store connect 和 testflight内测

uniapp 中配置 配置manifest 文档&#xff1a;manifest.json 应用配置 | uni-app官网 hbuilderx中本地打包 下载IOS最新SDK 开发环境 | uni小程序SDK hbulderx 版本号&#xff1a;4.66 对应的sdk版本 4.66 两者必须一致 本地打包的资源导入到SDK 导入资源 | uni小程序SDK …...

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案

在大数据时代&#xff0c;海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构&#xff0c;在处理大规模数据抓取任务时展现出强大的能力。然而&#xff0c;随着业务规模的不断扩大和数据抓取需求的日益复杂&#xff0c;传统…...

区块链技术概述

区块链技术是一种去中心化、分布式账本技术&#xff0c;通过密码学、共识机制和智能合约等核心组件&#xff0c;实现数据不可篡改、透明可追溯的系统。 一、核心技术 1. 去中心化 特点&#xff1a;数据存储在网络中的多个节点&#xff08;计算机&#xff09;&#xff0c;而非…...

spring Security对RBAC及其ABAC的支持使用

RBAC (基于角色的访问控制) RBAC (Role-Based Access Control) 是 Spring Security 中最常用的权限模型&#xff0c;它将权限分配给角色&#xff0c;再将角色分配给用户。 RBAC 核心实现 1. 数据库设计 users roles permissions ------- ------…...

CppCon 2015 学习:Time Programming Fundamentals

Civil Time 公历时间 特点&#xff1a; 共 6 个字段&#xff1a; Year&#xff08;年&#xff09;Month&#xff08;月&#xff09;Day&#xff08;日&#xff09;Hour&#xff08;小时&#xff09;Minute&#xff08;分钟&#xff09;Second&#xff08;秒&#xff09; 表示…...