JAVA并发编程【Semaphore】源码解析

news2024/12/23 18:24:16

文章目录

  • 一、Semaphore基础概念
  • 二、接口限流场景模拟
  • 三、Semaphore源码解析
    • 3.1、Semaphore结构解析
      • 3.1.1、Sync源码
      • 3.1.2、NonfairSync源码
      • 3.1.3、FairSync源码
    • 3.2、Semaphore重点方法源码解析


本章节将对Semaphore类中所有方法进行源码解析并提供部分代码案例。可以让读者全面了解该类提供的核心功能和该类使用的场景。在阅读本章之前希望您对AQS(AbstractQueuedSynchronizer)有一定了解,如果您曾阅读过AbstractQueuedSynchronizer源码或者是ReentrantLock源码,那本章节内容阅读起来就很轻车熟路。

一、Semaphore基础概念

Semaphore翻译过来的意思是信号、信号量。该类的主要作用是用于限制线程访问共享资源的数量。通俗一点来说,就是起到限流的作用。比如我们最常见的接口限流:当我们对外暴露一个接口时,应该对该接口的最大并行数进行控制。而Semaphore正好可以控制线程访问共享资源的数量,因此在单机部署的情况下,可以使用Semaphore来处理接口限流。synchronized关键字想必大家并不会感到陌生,synchronized可以控制共享资源同时只能被一个线程访问,而Semaphore更像是synchronized的升级版,Semaphore允许用户自定义共享资源同时被多少个线程访问。这个值可以是1也可是任意整数,但主要这个值不能大于int类型的最大值,也就是最大值不能超过2147483647


二、接口限流场景模拟

案例中初始化一个Semaphore对象,设置了2个凭证,这就代表该类控制最多两个线程可以同时访问共享资源。构建了10个模拟请求线程,同时对共享资源访问,通过允许结果可以看到,10个线程同时访问时,只有2个线程获取到了凭证从而能访问共享资源,而剩余的8个线程都被接口拒绝,而无法访问共享资源。

package uct;

import java.util.concurrent.Semaphore;

/**
 * @Author: DI.YIN
 * @Date: 2024/6/14 17:22
 * @Version:
 * @Description:
 **/
public class SemaphoreDemo {

    private static Semaphore semaphore = new Semaphore(2); //限制最多2个线程可以访问共享资源

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) { //构建10个线程,模拟外部线程调用接口
            ThreadRequest threadRequest = new ThreadRequest(semaphore, "线程[" + i + i + "]");
            threadRequest.start();
        }
    }

    /**
     * 外部线程具体实现
     */
    public static class ThreadRequest extends Thread {

        private Semaphore semaphore;

        ThreadRequest(Semaphore semaphore, String threadName) {
            this.semaphore = semaphore;
            super.setName(threadName);
        }

        @Override
        public void run() {
            if (semaphore.tryAcquire()) { //尝试获取凭证,如果获取失败,则返回false
                System.out.println("获取凭证成功,可以进行访问");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("访问完成,释放凭证");
                semaphore.release(); //释放凭证,可供后续线程继续获取
            } else {
                System.out.println("接口限流,本次请求被拒绝");
            }
        }
    }
}

在这里插入图片描述


三、Semaphore源码解析

从第二小节可以看出,Semaphore通过凭证数来判断是否允许线程访问共享资源,当线程来临时,通过调用acquire方法或者是tryAcquire来尝试或者凭证。两个方法最大的差异在于,acquire方法是会一直阻塞线程,直至能够从Semaphore中获取到凭证,而tryAcquire只是尝试获取,如果获取失败,则会立马返回结果,并不会阻塞线程。当线程获取到凭证时,Semaphore中的凭证数就会随之减少,如果凭证数小于0时,Semaphore就会阻塞线程(调用acquire方法)或者是立马返回false结果(调用tryAcquire方法)。当线程访问完共享资源后,应该调用release方法释放所拥有的凭证,否则其他等待线程将一直被阻塞。

3.1、Semaphore结构解析

Semaphore类主要由三个内部类构成,其结构与ReentrantLock类十分相似。Semaphore类中自定义了SyncNonfairSyncFairSync类。Sync类继承于AbstractQueuedSynchronizerAbstractQueuedSynchronizer为实现依赖于先进先出 (FIFO) 等待队列的阻塞锁定和相关同步器(信号量、事件,等等)提供一个基础的框架,AQSJAVA的锁实现中有大量的使用。NonfairSync又叫做非公平锁,而FairSync称之为公平锁。NonfairSyncFairSync又继承了Sync,因此这两个类具有AQS的特性。这里不再对AbstractQueuedSynchronizer做过多阐述,因为AbstractQueuedSynchronizer自身设计较为复杂并且源码理解有一定难度,本章节无法一次性说完。所以需要您先对AbstractQueuedSynchronizer的设计有一定的了解。

在这里插入图片描述

3.1.1、Sync源码

Sync类继承于AQS,Sync作为NonfairSyncFairSync的父类,其自身内部也有一定的实现。构造方法Sync(int permits) 用于设置最大同步访问共享资源数。在AbstractQueuedSynchronizer 中有一个stateint变量,用于维护这里传入的凭证数量。Sync类的nonfairTryAcquireShared方法用于线程尝试获取acquires个凭证,该方法默认作为非公平锁NonfairSync获取凭证数的实现。for循环加上ACS从而规避多线程数据不一致问题。

/**
     * Synchronization implementation for semaphore.  Uses AQS state
     * to represent permits. Subclassed into fair and nonfair
     * versions.
     * 信号量的同步实现。使用AQS状态代表许可证。子类分为公平和非公平版本。
     */
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits); //设置共享资源最大可以同步访问的线程数。
        }

        final int getPermits() {
            return getState();//获取许可证数量
        }

        //尝试获取许可证,只有获取到许可证的才能访问共享资源
        final int nonfairTryAcquireShared(int acquires) {
            for (; ; ) {
                int available = getState();//获取当前可用的许可证数
                int remaining = available - acquires;//计算扣减后剩余的许可数
                if (remaining < 0 || compareAndSetState(available, remaining)) //如果许可数小于0,则直接返回,或者是通过CAS重置许可证数成功,则返回
                    return remaining;
            }
        }

        //尝试释放release个许可证
        protected final boolean tryReleaseShared(int releases) { //state值加releases个,通过ACS通知多线程
            for (; ; ) {
                int current = getState(); //获取当前许可证数
                int next = current + releases;//修改许可证数量
                if (next < current) // overflow 超过int类型最大值,则值为负数
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next)) //通过CAS更新许可证数量
                    return true;
            }
        }

        final void reducePermits(int reductions) {
            for (; ; ) {
                int current = getState();
                int next = current - reductions;
                if (next > current) // underflow
                    throw new Error("Permit count underflow");
                if (compareAndSetState(current, next))
                    return;
            }
        }

        //将许可证数量设置为0,表示不允许任何线程访问共享资源
        final int drainPermits() {
            for (; ; ) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

3.1.2、NonfairSync源码

NonfairSync基本上没有对Sync 进行扩展,其实现也是基于Sync

    /**
     * NonFair version 非公平锁
     */
    static final class NonfairSync extends Sync {
        private static final long serialVersionUID = -2694183684443567898L;

        NonfairSync(int permits) {
            super(permits);
        }
        //尝试获取acquires个凭证,其底层调用Sync的nonfairTryAcquireShared方法
        protected int tryAcquireShared(int acquires) { 
            return nonfairTryAcquireShared(acquires);
        }
    }

3.1.3、FairSync源码

FairSync继承于Sync,其tryAcquireShared方法并不像NonfairSync一样基于Sync来实现,因为公平锁和非公平锁最大的区别在于,公平锁讲究先进先出,也就是最先进入等待队列的线程,应该优先被唤醒尝试去获取凭证。而非公平锁自身随机的。

    /**
     * 公平锁
     */
    static final class FairSync extends Sync {
        private static final long serialVersionUID = 2014338818796000944L;

        FairSync(int permits) {
            super(permits);
        }

        //获取acquires个凭证,hasQueuedPredecessors用于判断链表中是否还存在等待的线程
        protected int tryAcquireShared(int acquires) {
            for (; ; ) {
                if (hasQueuedPredecessors())
                    return -1;
                int available = getState();
                int remaining = available - acquires;
                if (remaining < 0 ||
                        compareAndSetState(available, remaining))
                    return remaining;
            }
        }
    }

3.2、Semaphore重点方法源码解析

Semaphore其自身实现方法并不多,更多的实现是基于AbstractQueuedSynchronizer,现在我们从Semaphore源码从上往下进行解析。
在这里插入图片描述
构造函数Semaphore(int permits)Semaphore(int permits, boolean fair) 都需要传入一个int类型的permits,这个值将保存到AbstractQueuedSynchronizerstate中,用于控制多线程最大并行数量。而布尔值fair用于指明底层采用公平锁FairSync还是非公平锁NonfairSync。默认采用非公平锁。

 /**
     * Creates a {@code Semaphore} with the given number of
     * permits and nonfair fairness setting.
     *
     * @param permits the initial number of permits available.
     *                This value may be negative, in which case releases
     *                must occur before any acquires will be granted.
     */
    public Semaphore(int permits) {
        sync = new NonfairSync(permits); //设置许可证数量,默认采用不公平锁
    }

    /**
     * Creates a {@code Semaphore} with the given number of
     * permits and the given fairness setting.
     *
     * @param permits the initial number of permits available.
     *                This value may be negative, in which case releases
     *                must occur before any acquires will be granted.
     * @param fair    {@code true} if this semaphore will guarantee
     *                first-in first-out granting of permits under contention,
     *                else {@code false}
     */
    public Semaphore(int permits, boolean fair) {
        sync = fair ? new FairSync(permits) : new NonfairSync(permits); //根据布尔值判断底层采用哪种模式的锁
    }

acquire方法用于尝试获取一个凭证,如果获取失败(当前不存在可用凭证)则当前线程将会被阻塞,直至有线程调用release方法释放凭证,唤醒阻塞线程。

 /**
     * Acquires a permit from this semaphore, blocking until one is
     * available, or the thread is {@linkplain Thread#interrupt interrupted}.
     * 从该信号量获取许可,阻塞直到可用,或者线程为被中断{@linkplain thread#interrupt interrupt}
     *
     * <p>Acquires a permit, if one is available and returns immediately,
     * reducing the number of available permits by one.获得许可证(如果有)并立即返回,将可用许可证的数量减少一个
     *
     * <p>If no permit is available then the current thread becomes
     * disabled for thread scheduling purposes and lies dormant until
     * one of two things happens:
     * 如果没有可用的许可,则当前线程将出于线程调度目的而被禁用,并处于休眠状态,直到发生以下两种情况之一:
     * 1. 某些线程调用release方法释放了许可,或者是当前线程被中断
     * <ul>
     * <li>Some other thread invokes the {@link #release} method for this
     * semaphore and the current thread is next to be assigned a permit; or
     * <li>Some other thread {@linkplain Thread#interrupt interrupts}
     * the current thread.
     * </ul>
     *
     * <p>If the current thread:
     * <ul>
     * <li>has its interrupted status set on entry to this method; or
     * <li>is {@linkplain Thread#interrupt interrupted} while waiting
     * for a permit,
     * </ul>
     * then {@link InterruptedException} is thrown and the current thread's
     * interrupted status is cleared.
     *
     * @throws InterruptedException if the current thread is interrupted
     */
    public void acquire() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

acquireSharedInterruptibly方法来自于AbstractQueuedSynchronizertryAcquireShared方法的实现在NonfairSync或者FairSync中,尝试获取n个凭证,如果返回负数,表示当前没有可用的凭证,则进入doAcquireSharedInterruptibly方法,doAcquireSharedInterruptibly方法将当前线程构建成一个Node对象放入链表中。调用LockSupport.park阻塞线程。当然如果当前存在可用凭证,则线程无需阻塞。Semaphore通过判断线程是否获取凭证来判断是否应该让线程阻塞。从而实现线程并发控制。

   public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0) //如果获取许可证失败,那么进入doAcquireSharedInterruptibly方法
            doAcquireSharedInterruptibly(arg); //将线程放入同步队列,阻塞
    }

  private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
        final Node node = addWaiter(Node.SHARED); //将线程构建成Node节点,放入队列
        boolean failed = true;
        try {
            for (; ; ) {
                final Node p = node.predecessor(); //当前节点的前一个节点
                if (p == head) { //如果他的前一个节点是头节点
                    int r = tryAcquireShared(arg); //再次尝试获取凭证
                    if (r >= 0) {
                        setHeadAndPropagate(node, r); //将当前节点设置为头节点
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //阻塞线程
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node); //如果获取到凭证,那么将线程移出队列
        }
    }

tryAcquire方法当比于acquire方法少了阻塞线程那一步,当调用tryAcquire时,线程将尝试获取一个凭证,如果获取失败,则直接返回flase结果,而不像acquire那样放入链表中阻塞线程。nonfairTryAcquireShared方法的具体实现在Sync中,如果该方法返回的值大于0,则表示线程获取到凭证,否则获取凭证失败。

     public boolean tryAcquire() {
        return sync.nonfairTryAcquireShared(1) >= 0;
    }

    final int nonfairTryAcquireShared(int acquires) {
            for (; ; ) {
                int available = getState();//获取当前可用的许可证数
                int remaining = available - acquires;//计算扣减后剩余的许可数
                if (remaining < 0 || compareAndSetState(available, remaining)) //如果许可数小于0,则直接返回,或者是通过CAS重置许可证数成功,则返回
                    return remaining;
            }
        }    

tryAcquire(long timeout, TimeUnit unit) 方法 与 acquire 方法的实现十分相近,只是设置一个等待时间,如果超过该时间如果线程还未获取当凭证,则返回false。acquire 方法是一直等待,而tryAcquire(long timeout, TimeUnit unit) 是给定等待时间,超时则返回结果。

    //在指定的时间尝试获取凭证
   public boolean tryAcquire(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
    }

    public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (Thread.interrupted())
            throw new InterruptedException();
        return tryAcquireShared(arg) >= 0 ||
                doAcquireSharedNanos(arg, nanosTimeout);//如果tryAcquireShared返回负数,则进入doAcquireSharedNanos方法进行阻塞休眠,调用LockSupport.parkNanos进行睡眠
    }

release方法用于释放一个凭证,释放的凭证将加到state上,以便于其他线程进行获取。tryReleaseShared方法的具体实现在Sync类。通过CAS来重置AQS上state的数量。

     public void release() {
        sync.releaseShared(1);
    }
    
        //尝试释放release个许可证
        protected final boolean tryReleaseShared(int releases) { //state值加releases个,通过ACS通知多线程
            for (; ; ) {
                int current = getState(); //获取当前许可证数
                int next = current + releases;//修改许可证数量
                if (next < current) // overflow 超过int类型最大值,则值为负数
                    throw new Error("Maximum permit count exceeded");
                if (compareAndSetState(current, next)) //通过CAS更新许可证数量
                    return true;
            }
        }

availablePermits 发放用于获取当前可用凭证数,其底层也就是调用AbstractQueuedSynchronizergetstate方法获取state属性的当前值

  public int availablePermits() {
        return sync.getPermits();
    }
   
    abstract static class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 1192457210091910933L;

        Sync(int permits) {
            setState(permits);
        }

        final int getPermits() {
            return getState(); //调用AbstractQueuedSynchronizer的方法
        }
   }     

drainPermits方法用于获取当前可用许可证数,并将许可证数设置为0,这样就会导致接下来的所有线程无法访问共享资源,通过for加CAS来规避多线程的影响。

        //将许可证数量设置为0,表示不允许任何线程访问共享资源
        final int drainPermits() {
            for (; ; ) {
                int current = getState();
                if (current == 0 || compareAndSetState(current, 0))
                    return current;
            }
        }
    }

hasQueuedThreads则是判断当前链表中是否还存在阻塞等待的线程,判断条件则是判断当前表头和表尾是否一致,如果是一致的,则表示当前链表为空链表,不存在阻塞的线程。

  public final boolean hasQueuedThreads() {
        return head != tail;
    }

getQueuedThreads则是获取当前链表中所有阻塞的线程,其核心操作就是遍历链表,然后将链表的所有线程放入集合中,一次性返回

    public final Collection<Thread> getQueuedThreads() {
        ArrayList<Thread> list = new ArrayList<Thread>();
        for (Node p = tail; p != null; p = p.prev) { //遍历链表
            Thread t = p.thread;
            if (t != null)
                list.add(t);
        }
        return list;
    }

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

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

相关文章

服务器win10server,python安装paddleocr的踩坑日记

最近由于需要图像文字识别的简单业务&#xff0c;研究了一下&#xff0c;一是用大厂的文字识别api&#xff0c;如百度腾讯等&#xff0c;但这种免费版只有有限的调用次数&#xff0c;如百度只有每月只有1000次调用额度&#xff0c;个人也够用&#xff0c;但由于业务量大&#x…

通天星CMSV6车载定位监控平台 point_manage/merge SQL注入致RCE漏洞复现

0x01 产品简介 通天星CMSV6车载定位监控平台拥有以位置服务、无线3G/4G视频传输、云存储服务为核心的研发团队,专注于为定位、无线视频终端产品提供平台服务,通天星CMSV6产品覆盖车载录像机、单兵录像机、网络监控摄像机、行驶记录仪等产品的视频综合平台。 0x02 漏洞概述 …

泛微E9开发 根据判断条件,控制字段的编辑/必填属性

根据判断条件&#xff0c;控制字段的编辑/必填属性 1、需求说明2、实现方法3、扩展知识点1. 注册钩子事件&#xff0c;指定动作完成后触发1.1 接口名称及参数说明1.2 案例 2. 改变单个字段显示属性(只读/必填等)2.1 参数说明2.2 案例 1、需求说明 当字段“填报人”和字段“姓名…

测试基础15:测试用例设计方法-场景设计(流程分析)

课程大纲 1、定义 系统多个功能串联形成业务流程&#xff0c;不仅需要验证正确的主流程&#xff0c;而且需要验证各个功能点各种异常情况。 2、应用场景 与因果图&判定表方法的相似之处&#xff1a;界面需手动填写的输入框少&#xff0c;基本只需选择有限的几个&#xff08…

小型数据中心是什么?如何建设?

在数字化时代&#xff0c;小型数据中心正成为许多企业和组织加强数据管理和服务扩展的理想选择。与传统大型数据中心相比&#xff0c;小型数据中心以其灵活性、高效性和相对较低的运营成本吸引着越来越多的关注。然而&#xff0c;要成功建设一个小型数据中心&#xff0c;并确保…

TC3xx启动的功能安全机制浅析(1)

目录 1.SM基本概念 2.芯片启动阶段safety相关解读 3.小结 之前描述TC3xx Boot Firmware逻辑时提到了功能安全的内容&#xff0c;但没有完全展开&#xff1b;启动阶段与功能安全相关的内容如下图所示&#xff1a; 其中&#xff0c; 灰色背景指在BootRom Fireware里的运行逻辑…

720漫游工具又双叒叕上新了一批新功能

一、720漫游全景图片上传支持「自定义水印」 全景图片素材上传支持自定义水印设置&#xff0c;通过自定义水印&#xff0c;可以在全景图片上打上自定义的水印图片保护用户版权利益&#xff0c;同时强化自身品牌露出。具体操作如下&#xff1a; 打开「创建720漫游作品页」-选择…

一文搞懂Linux命令行下载OneDrive分享文件

一文搞懂Linux命令行下载OneDrive分享文件 什么问题&#xff1f; 因为OneDrive有些坑&#xff0c;无法从分享界面获取真实下载链接&#xff0c;比如下面这个链接&#xff1a; https://connecthkuhk-my.sharepoint.com/:f:/g/personal/jhyang13_connect_hku_hk/EsEgHtGOWbJIm…

信息系统分析与设计:重点内容|UML在线绘制|数据库技术

目录 UML在线绘图工具信息系统分析与设计第1章 系统思想第2章 信息、管理与信息系统第3章 信息系统建设概论&#x1f31f;第4章 系统规划&#x1f31f;第5章 系统分析概述第6章 流程建模&#x1f31f;业务流程图DFD数据流图&#x1f31f;数据字典 第7章 用例建模(用例图)&#…

【扫雷游戏】C语言详解

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;C语言 &#x1f680;本系列文章为个人学习…

这才是真正的在线VS Code,其他的只能算是在线文本编辑器

前言 在数字化时代&#xff0c;编程已成为各行各业不可或缺的技能。然而&#xff0c;传统的编程环境往往需要在本地安装复杂的开发工具和软件&#xff0c;这不仅占用了大量的存储空间&#xff0c;还可能导致系统资源的浪费。为此在网上冲浪找了许多在线代码编辑器&#xff0c;…

昂科烧录器支持HangShun航顺芯片的32位微控制器HK32F030C8T6

芯片烧录行业领导者-昂科技术近日发布最新的烧录软件更新及新增支持的芯片型号列表&#xff0c;其中HangShun航顺芯片的32位微控制器HK32F030C8T6已经被昂科的通用烧录平台AP8000所支持。 HK32F030C8T6使用ARM Cortex-M0内核&#xff0c;最高工作频率96 MHz&#xff0c;内置最…

国际网络专线的开通流程

1. 选择服务商&#xff1a;首先&#xff0c;您需要选择一个可靠的服务商来提供国际网络专线服务。确保服务商具有良好的声誉和专业知识&#xff0c;以便为您提供高质量的网络连接和支持。 2. 评估需求&#xff1a;在与服务商沟通之前&#xff0c;您需要明确自己的网络需求。这…

TikTok美妆护肤热销背后:达人的力量与品牌崛起

TikTok不仅是一个娱乐和社交的平台&#xff0c;更是一个强大的电商平台。在美妆护肤领域&#xff0c;TikTok更是展现出其强大的带货能力&#xff0c;成为美妆护肤类商品热销的新势力。本文Nox聚星将和大家探讨TikTok上美妆护肤类商品的特点、热销原因&#xff0c;以及美妆博主和…

虚拟机中VSCode+gcc环境配置

一、安装VSCode 1、在官网下载软件包: 地址:Documentation for Visual Studio Code 2、下载后在放置deb包的文件夹直接打开终端,然后输入sudo dpkg -i code_1.90.2-1718751586_amd64.deb 3、安装成功提示,并显示该图标

使用Fiddler如何创造大量数据!

1、找到评论提交接口 找到我们的评论 2、构造数据 怎么再次发送呢&#xff1f; 这里发送了4次 我们创造了4条数据&#xff0c;我们再去评论区瞅瞅 3、如何解决图片显示问题&#xff1f; 手机端-设置-Wlan-高级-网址不适用代理&#xff0c;将不需要图片的域名加入 4、不抓包的…

手机看cad图的软件有哪些?软件推荐

手机看cad图的软件有哪些&#xff1f;随着科技的不断发展&#xff0c;CAD图纸在手机上的查看和编辑需求日益增加。为了满足这一需求&#xff0c;市面上涌现出了众多手机CAD看图软件。本文将为大家推荐四款优秀的手机CAD看图软件&#xff0c;并分别介绍它们的功能特点、受众定位…

iOS之如何创建.framework静态库

番外&#xff1a;想要查看如何创建.a静态库可前往看我iOS之如何创建.a静态库-CSDN博客这篇文章。 一、创建framework项目 创建framework工程要选择iOS --> Cocoa Touch Framework输入项目名称PrintFramework也是编译生成的framework的名称。framework的名称也可以以后在项目…

iPhone卡在恢复模式无法退出时,有那些退出恢复模式方法?

iPhone用户有时会遇到需要将手机进入恢复模式的情况。恢复模式可以帮助解决一些软件问题&#xff0c;但如果iPhone卡在恢复模式&#xff0c;不知道如何退出就会非常麻烦。小编将介绍几种iPhone退出恢复模式的方法。 一、苹果手机的恢复模式是什么意思 iPhone的恢复模式是针对i…

从基础到高级:视频直播美颜SDK的开发教学

本篇文章&#xff0c;小编将从基础到高级&#xff0c;详细讲解视频直播美颜SDK的开发过程&#xff0c;帮助开发者更好地掌握这一技术。 一、基础知识 什么是视频直播美颜SDK&#xff1f; 视频直播美颜SDK包含了一系列用于视频处理的功能模块&#xff0c;特别是美颜效果的实现…