近万字深入讲解iOS常见锁及线程安全

news2024/11/18 18:39:48

什么是锁?

在程序中,当多个任务(或线程)同时访问同一个资源时,比如多个操作同时修改一份数据,可能会导致数据不一致。这时候,我们需要“锁”来确保同一时间只有一个任务能够操作这个数据,避免“抢占”问题。简单来说,锁就是一种机制,它能帮助你控制多个任务按顺序来操作资源

按照锁的功能来进行分类,iOS常见的锁:自旋、互斥、递归、条件。。。

一、自旋锁

自旋锁的意思就是当资源被占有时,自旋锁不会引起其他调用者休眠,而是让其他调用者自旋,不停的循环访问自旋锁导致调用者处于busy-wait(忙等状态),直到自旋锁的保持者释放锁。自旋锁是为了实现保护共享资源一种锁机制,在任何时刻只能有一个保持者,也就是说在任何时刻只能有一个可执行单元获得锁。也正是因为其他调用者会保持自旋状态,使得在锁的保持者释放锁时能够即刻获得锁,效率非常高。但我们说调用者时刻自旋也是消耗CPU资源的,所以如果自旋锁的使用者保持锁的时间比较短的话,使用自旋锁是非常合适的,因为在锁释放之后省去了唤醒调用者的时间。

1.OSSpinLock(已弃用)

  • OSSpinLock 是一种轻量级的锁。当一个线程获取不到锁时,它不会进入睡眠状态,而是一直循环检查锁是否可用,这叫“自旋”。
  • 缺点OSSpinLock 已经被弃用,因为它容易导致“优先级反转”(低优先级线程获取锁,高优先级线程等待锁释放,造成高优先级线程无法执行)。

2.os_unfair_lock

  • os_unfair_lockOSSpinLock 的替代品。它解决了优先级反转问题,当一个线程无法获取锁时,会立即休眠而不是自旋。

  • 适用场景:用于短时间的锁定操作,轻量、快速。

var unfairLock = os_unfair_lock_s()

func safeMethod() {
    os_unfair_lock_lock(&unfairLock)
    // 执行共享资源的操作
    os_unfair_lock_unlock(&unfairLock)
}

用GCD模拟多线程,看是否输出是否按顺序输出:

import Foundation

// 初始化不公平锁
var unfairLock = os_unfair_lock_s()

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    os_unfair_lock_lock(&unfairLock)
    
    // 临界区:操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值
    
    // 解锁,允许其他线程访问共享资源
    os_unfair_lock_unlock(&unfairLock)
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

输出:

二、互斥锁

互斥锁和自旋锁类似,都是为了解决对某项资源的互斥使用,并且在任意时刻最多只能有一个执行单元获得锁,与自旋锁不同的是,互斥锁在被持有的状态下,其他资源申请者只能进入休眠状态,当锁被释放后,CPU会唤醒资源申请者,然后获得锁并访问资源。

1.pthread_mutex_t

  • pthread_mutex_t 是 POSIX 线程库提供的底层互斥锁,能确保同一时刻只有一个线程访问共享资源。
  • 适用场景:高效多线程编程,适合对性能要求高的场景。
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)

func safeMethod() {
    pthread_mutex_lock(&mutex)
    // 执行共享资源的操作
    pthread_mutex_unlock(&mutex)
}

同样的GCD模拟多线程,看是否输出是否按顺序输出:

import Foundation
//初始化互斥锁(Mutex)
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    pthread_mutex_lock(&mutex)
    
    // 临界区,操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值

    // 解锁,允许其他线程访问共享资源
    pthread_mutex_unlock(&mutex)
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

输出:

2.NSLock

  • NSLock 是 Cocoa 提供的更高级的互斥锁,它比 pthread_mutex_t 更易于使用。
let lock = NSLock()

func safeMethod() {
    lock.lock()
    // 执行代码
    lock.unlock()
}

GCD模拟多线程:

import Foundation
//初始化互斥锁(NSLock)
var lock = NSLock()

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    lock.lock()
    
    // 临界区,操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值

    // 解锁,允许其他线程访问共享资源
    lock.unlock()
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

3.@synchronized(仅支持 Objective-C)

  • 这是 Objective-C 中提供的自动锁机制,是OC的语法糖,Swift 中无法直接使用。它可以帮助你简化锁定的逻辑。
@synchronized(self) {
    // 执行共享资源操作
}

三、递归锁

递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁。这主要是用在循环或递归操作中。

递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,等到递归完毕之后,所有锁都会释放

1.NSRecursiveLock

  • NSRecursiveLock 是一种递归锁,允许同一个线程多次获取同一把锁而不会导致死锁。这是 NSLock 无法做到的。
  • 适用于需要多次锁定同一资源的场景。
let recursiveLock = NSRecursiveLock()

func recursiveFunction(count: Int) {
    recursiveLock.lock()
    if count > 0 {
        print("Count: \(count)")
        recursiveFunction(count: count - 1)
    }
    recursiveLock.unlock()
}

2.pthread_mutex_t (递归锁)

  • pthread_mutex_t 也可以被设置为递归模式,用法类似 NSRecursiveLock。

四、条件锁

条件是信号量的另一种类型,当某个条件为true时,它允许线程相互发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。 

1.pthread_cond_t

  • pthread_cond_t 是一种条件锁,常与 pthread_mutex_t 搭配使用,允许线程在满足特定条件时进行等待或唤醒。
  • 适用场景:用于需要等待某个条件满足的多线程场景。

2.NSCondition

  • NSCondition 是一个高级条件锁,可以让线程根据某些条件来等待或唤醒。
  • NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。生产者-消费者模式可以查看我的另一篇博客:iOS--生产者-消费者模式理解(附GCD信号量代码实现)-CSDN博客
let condition = NSCondition()
var isReady = false

func producer() {
    condition.lock()
    isReady = true
    condition.signal()  // 唤醒等待中的线程
    condition.unlock()
}

func consumer() {
    condition.lock()
    while !isReady {
        condition.wait()  // 等待条件满足
    }
    // 执行消费操作
    condition.unlock()
}

模拟:

import Foundation

let condition = NSCondition()
var isReady = false

// 生产者方法
func producer() {
    condition.lock()
    print("生产者正在准备资源...")
    isReady = true
    print("资源准备完成,通知消费者")
    condition.signal()  // 唤醒等待的消费者线程
    condition.unlock()
}

// 消费者方法
func consumer() {
    condition.lock()
    print("消费者等待资源...")
    while !isReady {
        condition.wait()  // 等待条件满足
    }
    print("资源已准备好,开始消费资源")
    // 执行消费操作
    condition.unlock()
}

// 模拟并发:使用DispatchQueue进行生产者和消费者的交互
let queue = DispatchQueue.global()

// 消费者等待资源
queue.async {
    consumer()
}

// 模拟生产者延迟生产资源
queue.asyncAfter(deadline: .now() + 2) {
    producer()
}

3.NSConditionLock

  • NSConditionLock 是 NSCondition 的一种变体,基于条件值进行锁定和解锁,适用于更复杂的线程同步场景。

五、信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号。

其实本质上,它通过维护一个计数值来控制同时可以访问某一资源的线程数量

1.dispatch_semaphore_t

  • dispatch_semaphore_t 是 GCD 提供的信号量机制,用于控制同时访问某一资源的线程数量。
  • 适用场景:适合控制并发任务的数量。
let semaphore = DispatchSemaphore(value: 1)

func safeMethod() {
    semaphore.wait()   // 请求资源
    // 执行共享资源操作
    semaphore.signal() // 释放资源
}

2.pthread_mutex_t(作为信号量使用)

  • 可以通过将 pthread_mutex_tpthread_cond_t 配合使用,达到类似信号量的效果。

六、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。

1.pthread_rwlock_t

  • pthread_rwlock_t 是一种读写锁,它允许多个线程同时读取资源,但在写入时会排他性地锁定。
  • 适用场景:适合读多写少的场景。
var rwlock = pthread_rwlock_t()
pthread_rwlock_init(&rwlock, nil)

func readResource() {
    pthread_rwlock_rdlock(&rwlock)  // 加读锁
    // 读取资源
    pthread_rwlock_unlock(&rwlock)  // 解锁
}

func writeResource() {
    pthread_rwlock_wrlock(&rwlock)  // 加写锁
    // 写入资源
    pthread_rwlock_unlock(&rwlock)  // 解锁
}

七、栅栏

栅栏函数在GCD中常用来控制线程同步,在队列中它总是等栅栏之前的任务执行完,然后执行栅栏自己的任务,执行完自己的任务后,再继续执行栅栏后的任务。常用函数有同步栅栏函数(dispatch_barrier_sync)和异步栅栏函数(dispatch_barrier_async)。

import Foundation

// 创建一个并发队列
let queue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

// 异步执行任务1
queue.async {
    print("Task 1")
}

// 异步执行任务2
queue.async {
    print("Task 2")
}

// 使用 barrier 标志的任务,确保在这个任务期间,队列中不会有其他任务执行
queue.async(flags: .barrier) {
    print("Barrier task")
    print("Barrier task2")  // 在屏障任务中执行第二个打印
}

// 异步执行任务3,等待 barrier 任务执行完毕后继续
queue.async {
    print("Task 3")
}

输出顺序

• Task 1 和 Task 2 可能无序输出,因为它们是并发执行的。

• 屏障任务会在之前的任务完成后执行。

• Task 3 将在屏障任务结束后执行。

总结:

线程安全

上述学习的各种常见的线程锁,都是为了更好地管理和调配线程,而在开发中我们往往不可避免地采用多线程并发,这虽然很便利但是存在的巨大的安全隐患

什么是线程安全?

线程安全指的是多个线程可能同时操作同一块内存,从而导致的异常情况,先举个例子:

class User {
    private(set) var name: String = ""
    func setName(_ name: String) {
        self.name = name
    }
}

let user = User()

let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")

queue1.async {
    user.setName("1")
    print(user.name)
}
queue2.async {
    user.setName("2")
    print(user.name)
}

这段代码因为多线程并发同时修改同一个变量,导致可能的结果是测试下来可能会是打印了两个 2,这就不符合我们的预期了,明明第一个 user.setName 传入的是 “1”,打印结果却为 2。

这种情况称为资源竞争,两个线程可能同时操作 user 对象,实际上,除了结果不符合预期外,还可能出现一个经典的崩溃 EXC_BAD_ACCESS,这是因为让两个线程尝试同时操作同一个内存地址导致的。

如何解决资源竞争问题?

很简单,用我们刚学完的锁就可以。这里就不举例子了。。。

其他并发问题

除了上边提到的资源竞争问题,在使用并发的时候还可能导致一些其他问题,也需要注意,比如:

  • 条件竞争:无法同步执行两个或多个线程,导致事件以错误的顺序执行
  • 死锁:两个线程相互等待,这意味着两者都无法继续,线程会卡死
  • 优先级倒置:低优先级任务持有高优先级任务所需的资源,导致执行延迟
  • 线程爆炸:程序中申请的线程数量过多,导致资源耗尽和系统性能下降
  • 线程匮乏:因为其他线程正在占用这个资源,导致其他线程无法访问,从而导致执行延迟

解决方法:

1. 条件竞争
  • 使用锁:使用互斥锁(如NSLock)或信号量(如DispatchSemaphore)确保对共享资源的安全访问。
  • 使用队列:使用串行队列或DispatchQueue的同步方法来控制对共享数据的访问顺序。
2. 死锁
  • 避免嵌套锁:尽量避免一个线程在持有锁时请求另一个锁。
  • 设置锁的顺序:确保所有线程按照相同的顺序获取锁。
  • 使用超时:设置锁的获取超时,防止无限等待。
3. 优先级倒置
  • 优先级提升:在需要时提升低优先级线程的优先级,让所需资源尽快释放。
  • 资源管理:确保高优先级线程能及时访问所需资源,比如使用锁机制。
4. 线程爆炸
  • 限制线程数量:使用线程池管理线程数量,避免创建过多线程。
  • 使用异步任务:采用GCD的队列,减少线程的创建。
5. 线程匮乏
  • 优化资源使用:检查资源访问和锁的使用,确保高效利用资源。
  • 调整线程设计:使用更灵活的线程模型,比如异步编程,减少对共享资源的依赖。

这里重点讲解一下什么是死锁。。。。。

死锁

在 Swift 中,当两个线程都在等待对方释放资源时,就会发生deadlock 死锁。这会导致线程都处于永久等待状态,当主线程死锁,应用的表现上就是崩溃,其他子线程死锁可能导致卡死。

举个例子:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print(1)
        
        DispatchQueue.main.sync {
            print(2)
        }
        
        print(3)
    }
}

上述代码会输出1后程序崩溃。

原因:主线程是处理用户界面和交互的线程。在viewDidLoad中打印1后,想要在主线程上执行另一个任务(打印2)。使用sync意味着希望这个任务立刻完成,而主线程正在执行viewDidLoad方法。因为主线程正在等着这个新任务完成(打印2),但这个任务又在主线程上运行,所以主线程无法继续,导致死锁

再举一个因为互斥锁而引发死锁的例子:

import Foundation

// 创建两个锁
let lock1 = NSLock()
let lock2 = NSLock()

// 线程1任务
func thread1() {
    print("线程1 尝试获取 lock1")
    lock1.lock()
    print("线程1 获取了 lock1")
    
    // 模拟处理一些操作
    sleep(1)
    
    print("线程1 尝试获取 lock2")
    lock2.lock()  // 死锁发生在这里,因为线程2已经持有了 lock2
    
    print("线程1 获取了 lock2")  // 这行永远不会被执行
    lock2.unlock()
    lock1.unlock()
}

// 线程2任务
func thread2() {
    print("线程2 尝试获取 lock2")
    lock2.lock()
    print("线程2 获取了 lock2")
    
    // 模拟处理一些操作
    sleep(1)
    
    print("线程2 尝试获取 lock1")
    lock1.lock()  // 死锁发生在这里,因为线程1已经持有了 lock1
    
    print("线程2 获取了 lock1")  // 这行永远不会被执行
    lock1.unlock()
    lock2.unlock()
}

// 并发队列
let queue = DispatchQueue.global()

// 启动线程
queue.async {
    thread1()
}

queue.async {
    thread2()
}
  1.     线程1 首先获取 lock1,然后等待获取 lock2。
  2.     线程2 首先获取 lock2,然后等待获取 lock1。
  3.     由于两个线程互相等待对方释放锁,导致程序进入死锁,两个线程都无法继续执行。

造成死锁的四个条件:

  1. 互斥条件:某个资源一次只能被一个线程占用。
  2. 占有且等待:一个线程占有一个资源,同时等待其他资源。
  3. 不可剥夺:线程所持有的资源不能被强制剥夺。
  4. 循环等待:两个或多个线程形成一种循环等待关系。

解决死锁的策略

  •     避免锁的循环等待:通过统一的锁顺序,确保线程不会互相等待对方的资源
  •     使用超时机制:锁请求可以设置超时时间,防止无限等待
  •     使用NSRecursiveLock(递归锁):允许同一线程多次获取同一把锁,避免递归调用中的死锁问题

参考:

iOS - 线程中常见的几种锁_unlock tryluck-CSDN博客

谈谈 swift 中的线程安全 - 知乎 (zhihu.com)

讲讲 iOS 中的死锁 (qq.com)

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

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

相关文章

2024年CSP-J认证 CCF信息学奥赛C++ 中小学初级组 第一轮真题-完善程序题解析

2024CCF认证第一轮&#xff08;CSP-J&#xff09;真题 三、完善程序题 第一题 判断平方数 问题&#xff1a;给定一个正整数 n&#xff0c;判断这个数 是不是完全平方数&#xff0c;即存在一个正整数 x 使得 x 的平方等于 n 试补全程序 #include<iostream> #include<…

LabVIEW提高开发效率技巧----错误处理机制

在LabVIEW开发中&#xff0c;错误处理机制至关重要&#xff0c;它不仅有助于提升程序的稳定性&#xff0c;还可以简化调试过程。错误线&#xff08;Error Wire&#xff09;是这一机制的核心工具&#xff0c;能够在各个子VI和模块之间传递错误信息。 1. 统一错误处理 在程序的各…

文心智能体AI大师工坊体验记

文心智能体AI大师工坊体验记 首先来说说什么是智能体&#xff0c;智能体&#xff08;Agent&#xff09;就是指能够感知环境并采取行动以实现特定目标的代理体。它可以是软件、硬件或一个系统&#xff0c;具备自主性、适应性和交互能力。智能体通过感知环境中的变化&#xff08;…

Linux之实战命令13:fuser应用实例(四十七)

简介&#xff1a; CSDN博客专家、《Android系统多媒体进阶实战》一书作者 新书发布&#xff1a;《Android系统多媒体进阶实战》&#x1f680; 优质专栏&#xff1a; Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a; 多媒体系统工程师系列【…

微信小程序导出word和Excel文件

在微信小程序中&#xff0c;实现Excel或Word文件的生成与下载功能通常涉及后端与前端的紧密协作。后端服务负责根据业务需求处理数据&#xff0c;将其转换为Excel或Word格式&#xff0c;并以文件流的形式返回。前端微信小程序则通过发送请求到后端获取这个文件流&#xff0c;接…

17121 求二叉树各种节点数

### 思路 1. 使用先序遍历的方式构造二叉树。 2. 使用递归函数 CreateBiTree 来构造二叉树。 3. 使用递归函数 CountNodes 来统计度为2、度为1和度为0的节点数。 ### 伪代码 1. 定义二叉树节点结构 BiTNode 和二叉树指针 BiTree。 2. 定义 CreateBiTree 函数&#xff1a; -…

java并发之并发关键字

并发关键字 关键字一&#xff1a;volatile 可以这样说&#xff0c;volatile 关键字是 Java 虚拟机提供的轻量级的同步机制。 功能 volatile 有 2 个主要功能&#xff1a; 可见性。一个线程对共享变量的修改&#xff0c;其他线程能够立即得知这个修改。普通变量不能做到这一点&a…

【病毒分析】phobos家族Elbie变种加密器分析报告

1.样本信息 ⽂件名Fast【phobos家族Elbie变种加密器】.exeSHA256e18d3d15a27ffa48cef12de79ac566bfbd96f6f4a1477e5986bc4a100227d8a3MD5f1ecac228e48c7b9758dacfca9356b1fSHA1d9f32b053310a9400fef4d68ae8a8ce70594eaad 2.感染迹象 文件被加密并重命名如下格式1.png.id[8E1…

深入理解 JSX:构建 React 用户界面的利器

目录 一、JSX介绍 1.JSX概念 2.为什么使用JSX,JSX有什么好处? 二、JSX基本语法 1.基本元素: 2.嵌套元素: 3.组件: 4.属性: 5.表达式 6.条件渲染: 7.样式: 三、JSX语法规则 四、JSX编译过程 五、JSX小案例 1.待办事项列表 2.计时器应用 六、总结 一、JSX介…

LLMs之RAG:MemoRAG(利用其记忆模型来实现对整个数据库的全局理解)的简介、安装和使用方法、案例应用之详细攻略

LLMs之RAG&#xff1a;MemoRAG(利用其记忆模型来实现对整个数据库的全局理解)的简介、安装和使用方法、案例应用之详细攻略 目录 MemoRAG的简介 0、更新日志 1、特性 2、路线图 MemoRAG的安装和使用方法 1、安装 安装依赖项 T1、从源码安装 T2、通过pip安装 2、使用方…

可调节基准电压电路设计

1 简介 该电路组合使用了一个放大器&#xff0c;可使基准电压电路在输入电压负值至正的输入电压之间的范围内进行调节&#xff0c;且可增加增益以提高最大负基准电压电平。 2 设计目标 2.1 输入 2.2 输出 ​​​ 2.3 电源 3 电路设计 根据设计目标&#xff0c;最终设计的电…

综合实验1 利用OpenCV统计物体数量

一、实验简介 传统的计数方法常依赖于人眼目视计数&#xff0c;不仅计数效率低&#xff0c;且容易计数错误。通常现实中的对象不会完美地分开&#xff0c;需要通过进一步的图像处理将对象分开并计数。本实验巩固对OpenCV的基础操作的使用&#xff0c;适当的增加OpenCV在图像处…

抽奖拼团卷轴模式系统开发小程序源代码解析

在当今的互联网商业环境中&#xff0c;抽奖、拼团与卷轴模式等创新玩法被广泛应用于小程序开发中&#xff0c;旨在通过多样化的互动方式吸引用户参与&#xff0c;提升用户粘性和平台活跃度。本文将围绕“抽奖拼团卷轴模式系统开发小程序源代码”这一主题&#xff0c;探讨其技术…

【HTTP协议详解-Fiddler抓包工具安装详解-HTTP报文格式-URL详解】

&#x1f308;个人主页&#xff1a;努力学编程’ ⛅个人推荐&#xff1a; c语言从初阶到进阶 JavaEE详解 数据结构 ⚡学好数据结构&#xff0c;刷题刻不容缓&#xff1a;点击一起刷题 &#x1f319;心灵鸡汤&#xff1a;总有人要赢&#xff0c;为什么不能是我呢 &#x1f52d…

安卓13删除下拉栏中的关机按钮版本2 android13删除下拉栏关机按钮

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 顶部导航栏下拉可以看到,底部这里有个设置按钮,点击可以进入设备的设置页面,这里我们将更改为删除,不同用户通过这个地方进入设置。我们之前写过一个文章也是一样的删除…

基于RealSense D435相机实现手部姿态重定向

基于Intel RealSense D435 相机和 MediaPipe的手部姿态检测&#xff0c;进一步简单实现手部姿态与机器人末端的重定向。 假设已经按照【基于 RealSenseD435i相机实现手部姿态检测】配置好所需的库和环境&#xff0c;并且有一个可以控制的机器人接口。 一、手部姿态重定向介绍 …

18924 二叉树的宽度

### 思路 1. 使用广度优先搜索&#xff08;BFS&#xff09;遍历二叉树&#xff0c;记录每一层的节点数。 2. 使用队列来实现BFS&#xff0c;队列中存储节点和其对应的层数。 3. 在遍历过程中&#xff0c;更新每一层的节点数&#xff0c;并记录最大节点数。 ### 伪代码 1. 定义…

uni-app - - - - -vue3使用i18n配置国际化语言

uni-app - - - - -使用i18n配置国际化语言 1. 安装vue-i18n2. 配置文件2.1 创建如下文件2.2 文件配置2.3 main文件导入i18n 3. 页面内使用3.1 template内直接使用3.2 变量接收使用 1. 安装vue-i18n npm install vue-i18n --save2. 配置文件 2.1 创建如下文件 locales文件夹里…

__has_include 报错

作用&#xff1a; 在C或C的预处理阶段&#xff0c;__has_include 是一个编译器特定的宏&#xff0c;主要用于检查编译器是否能够包含指定的头文件。这个宏在Clang和GCC&#xff08;从某个版本开始&#xff09;等编译器中可用&#xff0c;但在所有编译器中可能并不都支持…

气膜乒乓球馆的前景展望—轻空间

乒乓球作为我国的国球&#xff0c;在全球范围内始终保持领先地位&#xff0c;不仅是国民心中的重要运动&#xff0c;也在国际舞台上占据了举足轻重的地位。气膜乒乓球馆作为一种创新的体育设施&#xff0c;通过结合先进的气膜技术与传统乒乓球运动&#xff0c;为爱好者提供了一…