iOS——retain和release底层原理

news2024/9/20 1:05:48

retain实现原理

retain的源码:

//使用此方法等价于使用[this retain]
inline id 
objc_object::retain()
{
  //确保对象不是tagged pointer
    ASSERT(!isTaggedPointer());

    return rootRetain(false, RRVariant::FastOrMsgSend);
}
ALWAYS_INLINE id 
objc_object::rootRetain()
{
  //分为快速路径和慢速路径,以优化性能和处理溢出情况。
    return rootRetain(false, RRVariant::Fast);
}
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;
  //为什么有isa?因为需要对引用计数+1,即retain+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
    isa_t oldisa;
    isa_t newisa;

    oldisa = LoadExclusive(&isa.bits);

    if (variant == RRVariant::FastOrMsgSend) {
        // 这些检查只对 objc_retain()
        // 他们在这里是为了避免我们重新装载isa。
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }

    if (slowpath(!oldisa.nonpointer)) {
        // 一个类永远是一个类,所以我们可以执行一次检查
        // 在CAS环外
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }
    //重点
    do {
    // 初始化 transcribeToSideTable 为 false,表示不需要将引用计数转移到 side table
    //这里是先设置默认情况下的transcribeToSideTable
    transcribeToSideTable = false;
    // 将 oldisa 的值赋给 newisa,用于后续的引用计数增加操作
    newisa = oldisa;
    
    // 检查 newisa 是否为 nonpointer isa
    //nonpointer isa 是一种优化技术,用于将额外的信息编码到 isa 指针中。它和taggedPointer是不一样的
    if (slowpath(!newisa.nonpointer)) {
        // 如果 newisa 不是 nonpointer isa,清除 isa 的原子锁
        ClearExclusive(&isa().bits);
        // 如果 tryRetain 为 true,尝试 sidetable 的 tryRetain 方法
        if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
        // 否则调用 sidetable 的 retain 方法
        else return sidetable_retain(sideTableLocked);
    }
    
    // 不检查 newisa.fast_rr,因为我们已经调用了 RR 覆盖
    // 检查对象是否正在被 deallocating
    if (slowpath(newisa.isDeallocating())) {
        // 如果对象正在被 deallocating,清除 isa 的原子锁
        ClearExclusive(&isa().bits);
        // 如果 sideTableLocked 为 true,解锁 side table
        if (sideTableLocked) {
            ASSERT(variant == RRVariant::Full);
            sidetable_unlock();
        }
        // 如果 tryRetain 为 true,返回 nil,表示无法增加引用计数;否则返回对象本身
        if (slowpath(tryRetain)) {
            return nil;
        } else {
            return (id)this;
        }
    }
    
    uintptr_t carry;
    // 增加引用计数,即对 newisa.bits 中的引用计数位进行加一操作
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    
    // 检查引用计数是否溢出
    if (slowpath(carry)) {
        // 如果引用计数溢出,且 variant 不为 Full,清除 isa 的原子锁并调用 rootRetain_overflow
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa().bits);
            return rootRetain_overflow(tryRetain);
        }
        // 保留一半的引用计数在 inline,并准备将另一半复制到 side table
        if (!tryRetain && !sideTableLocked) sidetable_lock();
        sideTableLocked = true;
        transcribeToSideTable = true;
        newisa.extra_rc = RC_HALF;
        newisa.has_sidetable_rc = true;
    }
// 尝试将 newisa 存储到 isa.bits 中,如果存储失败,则循环重新尝试
} while (slowpath(!StoreExclusive(&isa().bits, &oldisa.bits, newisa.bits)));


    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            // 把剩下的一半放到 side table.
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }

    return (id)this;
}

这段代码的逻辑是:首先初始化并赋值变量,然后进入循环,检查 newisa 是否为 nonpointer isa,如果不是,则清除 isa 的原子锁。如果 tryRetain 为真,则尝试调用 sidetable_tryRetain 增加引用计数,否则调用 sidetable_retain 增加引用计数。如果对象正在被释放,则清除 isa 的原子锁,并根据条件返回 nil 或对象。接着,尝试增加引用计数并检查是否溢出,如果溢出,则将部分引用计数转移到 side table 并设置相关标志。循环结束时,检查并处理 side table 的引用计数操作。
我们再看看sidetable_tryRetain方法:

bool
objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
//确保对象的 isa 不是 nonpointer isa(如果支持 nonpointer isa)
    ASSERT(!isa().nonpointer);
#endif
    //从全局的sideTable中获取当前对象的sideTable
    SideTable& table = SideTables()[this];

    // NO SPINLOCK HERE
    // _objc_rootTryRetain() is called exclusively by _objc_loadWeak(), 
    // which already acquired the lock on our behalf.

    // fixme can't do this efficiently with os_lock_handoff_s
    // if (table.slock == 0) {
    //     _objc_fatal("Do not call -_tryRetain.");
    // }

    bool result = true;
    //在 SideTable 的 refcnts 中尝试插入当前对象。如果插入成功,则表示该对象以前没有引用计数记录,新建的条目引用计数为 1。
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_RC_ONE);
    //获取并引用映射中元素的值部分
    auto &refcnt = it.first->second;
    //如果插入了新条目(即条目不存在),什么也不做,因为条目已经初始化为 SIDE_TABLE_RC_ONE。
    if (it.second) {
        //如果条目已经存在,检查是否有 SIDE_TABLE_DEALLOCATING 标志。如果有,设置 result 为 false,表示无法增加引用计数。
    } else if (refcnt & SIDE_TABLE_DEALLOCATING) {
        result = false;
        //如果条目已经存在并且没有 SIDE_TABLE_DEALLOCATING 标志,再检查是否有 SIDE_TABLE_RC_PINNED 标志。如果没有,增加引用计数 SIDE_TABLE_RC_ONE。
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt += SIDE_TABLE_RC_ONE;
    }
    
    return result;
}

这段代码用于尝试增加对象的引用计数。
首先确保对象的 isa 不是 nonpointer isa,然后从全局的 SideTables 中获取当前对象的 SideTable。
在 SideTable 的 refcnts 中尝试插入当前对象。如果插入成功,则表示该对象以前没有引用计数记录,新建的条目引用计数为 1。如果对象已经存在于 SideTable 中,检查引用计数标志:
如果对象正在被释放(DEALLOCATING),返回 false,表示增加引用计数失败。
如果引用计数没有被固定(没有 SIDE_TABLE_RC_PINNED 标志),增加引用计数。

retain总体流程大概如下图:
请添加图片描述

release

源码:

// Equivalent to calling [this release], with shortcuts if there is no override
inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    rootRelease(true, RRVariant::FastOrMsgSend);
}

ALWAYS_INLINE bool 
objc_object::rootRelease()
{
    return rootRelease(true, RRVariant::Fast);
}

inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());

    if (fastpath(!ISA()->hasCustomRR())) {
        // Standard RR of a class is a no-op.
        if (!ISA()->isMetaClass())
            sidetable_release();
        return;
    }

    ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    // 如果是标记指针,直接返回 false
    if (slowpath(isTaggedPointer())) return false;

    bool sideTableLocked = false; // 用于标记侧表是否被锁定

    isa_t newisa, oldisa; // 定义 isa_t 类型的变量 newisa 和 oldisa

    // 加载 isa 值到 oldisa 中
    oldisa = LoadExclusive(&isa().bits);

    // 如果引用计数变种是 FastOrMsgSend
    if (variant == RRVariant::FastOrMsgSend) {
        // 这些检查仅对 objc_release() 有意义
        // 它们在这里是为了避免重新加载 isa
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa().bits); // 清除独占标记
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                // 如果可以调用 Swift 的引用计数方法
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            // 调用 objc_msgSend 的 release 方法
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    // 检查 isa 是否为指针形式
    if (slowpath(!oldisa.nonpointer)) {
        // 一个类永远是一个类,所以我们可以在 CAS 循环外进行此检查
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa().bits); // 清除独占标记
            return false;
        }
    }

#if !ISA_HAS_INLINE_RC
    // 如果不支持内联引用计数,使用侧表
    ClearExclusive(&isa().bits); // 清除独占标记
    return sidetable_release(sideTableLocked, performDealloc);
#else
retry:
    do {
        newisa = oldisa; // 将 oldisa 赋值给 newisa
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa().bits); // 清除独占标记
            return sidetable_release(sideTableLocked, performDealloc);
        }
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa().bits); // 清除独占标记
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock(); // 解锁侧表
            }
            return false;
        }

        // 不检查 newisa.fast_rr; 我们已经调用了任何 RR 重载
        uintptr_t carry;
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); // extra_rc--
        if (slowpath(carry)) {
            // 不清除独占标记
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits)));

    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

underflow:
    // newisa.extra_rc-- 下溢:从侧表借用或析构

    // 放弃 newisa 以撤销递减
    newisa = oldisa;

    if (slowpath(newisa.has_sidetable_rc)) {
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa().bits); // 清除独占标记
            return rootRelease_underflow(performDealloc);
        }

        // 将引用计数从侧表转移到内联存储

        if (!sideTableLocked) {
            ClearExclusive(&isa().bits); // 清除独占标记
            sidetable_lock(); // 锁定侧表
            sideTableLocked = true;
            // 需要重新开始以避免与指针非指针转换的竞争
            oldisa = LoadExclusive(&isa().bits);
            goto retry;
        }

        // 尝试从侧表中删除一些引用计数
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        bool emptySideTable = borrow.remaining == 0; // 如果侧表中没有引用计数,将清空侧表

        if (borrow.borrowed > 0) {
            // 侧表引用计数减少
            // 尝试将它们添加到内联计数
            bool didTransitionToDeallocating = false;
            newisa.extra_rc = borrow.borrowed - 1; // 重新执行原始递减
            newisa.has_sidetable_rc = !emptySideTable;

            bool stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);

            if (!stored && oldisa.nonpointer) {
                // 内联更新失败
                // 立即重试。这可以防止在 LL/SC 架构上发生活锁,
                // 因为侧表访问本身可能会丢失预留
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa().bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            if (!stored) {
                // 内联更新失败
                // 将保留重新放回侧表
                ClearExclusive(&isa().bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa().bits);
                goto retry;
            }

            // 从侧表借用后的递减成功
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
            // 侧表最终为空,进入析构路径
        }
    }

deallocate:
    // 真的要析构了

    ASSERT(newisa.isDeallocating());
    ASSERT(isa().isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        this->performDealloc();
    }
    return true;
#endif // ISA_HAS_INLINE_RC
}

uintptr_t
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa().nonpointer);
#endif
    // 获取当前对象的侧表
    SideTable& table = SideTables()[this];

    bool do_dealloc = false; // 标记是否需要析构

    // 如果未锁定侧表,先锁定它
    if (!locked) table.lock();
    
    // 尝试在侧表的引用计数字典中插入一个新条目
    // 如果该对象不在侧表中,插入 {this, SIDE_TABLE_DEALLOCATING}
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second; // 获取引用计数

  //判断是否进行了插入操作
    if (it.second) {
        // 如果对象之前不在侧表中,表示这是第一次插入
        // 设置 do_dealloc 为 true,表示需要析构
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // 如果引用计数小于 SIDE_TABLE_DEALLOCATING
        // 表示引用计数为负数,可能设置了 SIDE_TABLE_WEAKLY_REFERENCED
        // 设置 do_dealloc 为 true,并将引用计数标记为 SIDE_TABLE_DEALLOCATING
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        // 如果引用计数未固定(未设置 SIDE_TABLE_RC_PINNED)
        // 将引用计数减一
        refcnt -= SIDE_TABLE_RC_ONE;
    }

    // 解锁侧表
    table.unlock();

    // 如果需要析构且 performDealloc 为真,执行析构操作
    if (do_dealloc && performDealloc) {
        this->performDealloc();
    }

    // 返回是否需要析构
    return do_dealloc;
}

实现了 objc_object 的引用计数减少操作,并根据引用计数的变化决定是否需要进行对象的析构。
在这里插入图片描述

dealloc

dealloc用于在对象的引用计数为0的时候,释放该对象,那么在底层,它是如何实现的呢.

inline void objc_object::rootDealloc()
{
    // 如果对象是 tagged pointer(标记指针),直接返回
    if (isTaggedPointer()) return;  // fixme necessary?
    
#if !ISA_HAS_INLINE_RC
    // 如果没有内联引用计数,直接调用 object_dispose 释放对象
    object_dispose((id)this);
#else
    // 如果有内联引用计数,并且满足以下所有条件,则直接释放内存
    if (fastpath(isa().nonpointer &&               // 非指针 ISA
                 !isa().weakly_referenced &&       // 没有弱引用
                 !isa().has_assoc &&               // 没有关联对象
#if ISA_HAS_CXX_DTOR_BIT
                 !isa().has_cxx_dtor &&            // 没有 C++ 析构函数
#else
                 !isa().getClass(false)->hasCxxDtor() && // 没有 C++ 析构函数
#endif
                 !isa().has_sidetable_rc))         // 没有使用 side table 引用计数
    {
        // 确认没有 side table 存在
        assert(!sidetable_present());
        // 直接释放内存
        free(this);
    } 
    else {
        // 否则,调用 object_dispose 释放对象
        object_dispose((id)this);
    }
#endif // ISA_HAS_INLINE_RC
}

用于销毁和释放对象:

id object_dispose(id obj)
{
    // 如果 obj 是空指针,则直接返回 nil
    if (!obj) return nil;

    // 调用 objc_destructInstance 函数,销毁对象实例的内容
    objc_destructInstance(obj);
    
    // 释放对象所占用的内存
    free(obj);

    // 返回 nil,表示对象已被销毁
    return nil;
}
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        // 如果要释放的对象没有采用了优化过的isa引用计数
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        // 如果要释放的对象采用了优化过的isa引用计数,并且有弱引用或者使用了sideTable的辅助引用计数
        clearDeallocating_slow();
   }
   //确保 side table 中没有该对象的引用计数记录。
    assert(!sidetable_present());
}

上面这段代码根据是否采用了优化过的isa做引用计数分为两种:

  1. 要释放的对象没有采用优化过的isa引用计数:会调用sidetable_clearDeallocating() 函数在 side table 中清理对象的引用计数。
void 
objc_object::sidetable_clearDeallocating()
{
    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    //在散列表SideTable中找到对应的引用计数表RefcountMap,拿到要释放的对象的引用计数
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        //如果要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
        //从引用计数表中擦除该对象的引用计数
        table.refcnts.erase(it);
    }

    table.unlock();
}

先找到对应的 SideTable,并对其加锁。
查找引用计数表 refcnts 中该对象的引用计数。
如果找到该对象的引用计数且被弱引用,则清理弱引用。
最后擦除该对象的引用计数,并解锁 SideTable。

  1. 如果该对象采用了优化过的isa引用计数并且该对象有弱引用或者使用了sideTable的辅助引用计数,就会调用clearDeallocating_slow()函数处理引用计数
NEVER_INLINE void

objc_object::clearDeallocating_slow()

{
    assert(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    // 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        //要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //使用了sideTable的辅助引用计数,直接在SideTable中擦除该对象的引用计数
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
    table.unlock();
}

找到对应的 SideTable,并对其加锁。
如果对象被弱引用,清理弱引用。
如果对象使用了 side table 的引用计数,擦除该对象的引用计数。
最后解锁 SideTable。

以上两种情况都涉及weak_clear_no_lock函数, 它的作用就是将被弱引用对象的弱引用指针置为nil.

dealloc的流程:如果对象是 tagged pointer,则直接返回。如果没有内联引用计数,调用 object_dispose 释放对象;如果有内联引用计数,并且对象满足非指针 ISA、没有弱引用、没有关联对象、没有 C++ 析构函数以及没有使用 side table 引用计数等条件,则直接释放内存。否则,调用 object_dispose 进行标准的对象销毁和内存释放。

请添加图片描述

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

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

相关文章

关系代数 | 数据库SQL

文章目录 关系运算符笛卡尔积笛卡尔积应用 运算符符号含义集合运算符并∪交∩差-笛卡尔积专门的关系运算符选择σ投影π连接⋈除 关系运算符 笛卡尔积 集合运算符中&#xff0c;主要对笛卡尔积做解释&#xff1a; 在数学中&#xff0c;两个集合X和Y的笛卡儿积&#xff08;英语…

【Linux】进程控制(一)

1. 进程创建 &#xff08;一&#xff09;认识fork函数 从已存在进程中创建一个新进程&#xff08;新进程为子进程&#xff0c;而原进程为父进程&#xff09; 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核做&#xff1a; 分配新的内存块和内核数…

Allegro PCB--报错

1。 走线上打孔 问题&#xff1a;在走线上打的Via&#xff0c;我通过"Assign net to Via", 给与网络。成功后。 跑Tools\Database check\ Update all DRC(including batch), Via 网络又没有了 原因& 解决方法&#xff1a; VIA没有和走线完全重合 换个方法&#x…

【吊打面试官系列-Redis面试题】说说 Redis 哈希槽的概念?

大家好&#xff0c;我是锋哥。今天分享关于 【说说 Redis 哈希槽的概念&#xff1f;】面试题&#xff0c;希望对大家有帮助&#xff1b; 说说 Redis 哈希槽的概念&#xff1f; Redis 集群没有使用一致性 hash,而是引入了哈希槽的概念&#xff0c;Redis 集群有 16384 个哈希槽&a…

高精度加法,减法,乘法,除法

加法&#xff1a; 大整数该如何储存&#xff1f; 用数组储存&#xff1a; 把个位放在数下标为0的位置&#xff0c;十位放在数组下标为1的位置&#xff08;也就是高位放在数组的后面&#xff09; 因为这样&#xff0c;如果需要增加一位最高位&#xff0c;那我们就可以直接在…

C语言小游戏--贪吃蛇实现

C语言小游戏--贪吃蛇实现 1.游戏实现背景2.Win32 API介绍2.1什么是Win32 API2.2控制台程序(Console)2.3控制台屏幕的坐标COORD2.4GetStdHandle2.4.1函数语法2.4.2函数的使用 2.5GetConsoleCursorInfo2.5.1函数语法2.5.2函数的使用 2.6CONSOLE_CURSOR_INFO2.6.1结构体结构2.6.2结…

自制游戏手柄--电位器的使用

在前面的讨论中&#xff0c;我们考虑了使用陀螺仪来获取手柄的运动情况来进行瞄准&#xff0c; 自制实战吃鸡手柄原理-CSDN博客 也可以使用图像识别来实现&#xff0c;这里我们再考虑下使用电位器来获取运动状态&#xff0c;一个电位器可以获取到一个平面上的旋转情况&#x…

2025年25届新文出炉:如何打造Java SpringBoot Vue个性化课程推荐系统?

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

根据NVeloDocx Word模板引擎生成Word(二)

前面讲到了根据“永久免费开放的E6低代码开发平台”的NVeloDocx Word模版引擎生成Word文件的基础取数方法&#xff0c;包括取本表单字段以及引用字段&#xff0c;详见《根据NVeloDocx Word模板引擎生成Word&#xff08;一&#xff09;》。 针对这种基本的取数方法&#xff0c;…

枚举相关知识点

1.是用户定义的数据类型&#xff0c;为一组相关的常量赋予有意义的名字。 2.enum常量本身带有类型信息&#xff0c;即Weekday.SUN类型是Weekday&#xff0c;编译器会自动检查出类型错误&#xff0c;在编译期间可检查错误。 3.enum定义的枚举类有什么特点。 a.定义的enu…

Ubuntu之源码编译安装nginx

参考&#xff1a;Ubuntu之源码编译安装nginx_ubuntu编译安装nginx-CSDN博客 1.下载源码后进入源码目录&#xff0c;如下&#xff1a; cd /home/jq/wf/nginx-1.26.1 2.下载相应依赖库&#xff1a; apt-get install libpcre3-dev apt-get install openssl libssl-dev apt-get…

互联网中的情绪价值

在互联网中&#xff0c;信息的传递和分享变得越来越便捷了&#xff0c;同时&#xff0c;「情绪价值」在虚拟世界中的作用也愈加凸显。 无论是在社交媒体、即时通讯工具、各类论坛社区以及短视频平台里&#xff0c;情绪价值已然成为一种无形但是至关重要的资产&#xff0c;在默默…

BaseCTF-[Week3] 出题人已疯-快坚持不下去的第六天

DIE 查&#xff0c;发现是 .NET&#xff0c;拖⼊ dnSpy 加密的代码 private void Btn_Submit_Click(object sender, RoutedEventArgs e) { char[] array this.Tb_Input.Text.ToCharArray(); char[] array2 string.Join("", this.sentences).ToCharArray(); for (i…

php、Java、python酒店预约与送餐系统 酒店管理系统 酒店预订入住系统(源码、调试、LW、开题、PPT)

&#x1f495;&#x1f495;作者&#xff1a;计算机源码社 &#x1f495;&#x1f495;个人简介&#xff1a;本人 八年开发经验&#xff0c;擅长Java、Python、PHP、.NET、Node.js、Android、微信小程序、爬虫、大数据、机器学习等&#xff0c;大家有这一块的问题可以一起交流&…

C++第四十七弹---深入理解异常机制:try, catch, throw全面解析

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】 目录 1.C语言传统的处理错误的方式 2.C异常概念 3. 异常的使用 3.1 异常的抛出和捕获 3.2 异常的重新抛出 3.3 异常安全 3.4 异常规范 4.自定义…

宠物狗检测-目标检测数据集(包括VOC格式、YOLO格式)

宠物狗检测-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1roegkaGAURWUVRR-D7OzzA?pwddxv6 提取码&#xff1a;dxv6 数据集信息介绍&#xff1a; 共有20580 张图像和一一对应的标注文件 标…

如何使用微软的Copilot AI工具将Word文档转换为PowerPoint

Copilot 让你可以将 Word 文档转换为 PowerPoint 演示文稿&#xff0c;使你能够以最小的努力制作出有针对性的演示文稿。这个功能是微软AI工具包的一部分&#xff0c;对于那些曾经盯着空白幻灯片不知道从何开始的人来说&#xff0c;这是一个颠覆性的改变。要充分利用这个工具&a…

libtool 中的 .la 文件说明

libtool 中的 .la 文件说明 1 概述 在 Linux 系统中&#xff0c;libtool 是一个用于自动化编译和链接复杂软件项目的工具&#xff0c;特别是那些使用了共享库&#xff08;.so 文件在 Linux 上&#xff0c;.dylib 在 macOS 上&#xff09;的项目。它帮助处理各种编译器和链接器…

快速上手 | 数据可观测性平台 Datavines 自定义SQL规则使用指南

摘要 本文主要介绍在 Datavines平台已有规则不能满足需求的情况下&#xff0c;如何通过自定义SQL规则来实现基于业务特性的数据质量检查。 规则介绍 自定义聚合SQL规则是 Datavines 平台中内置的一个灵活的规则&#xff0c;该规则允许用户通过编写SQL的方式来实现想要的数据质…

透传:利用 vercel 部署 OpenAI Proxy

透传&#xff1a;通俗理解国内ping不通国外大模型&#xff0c;需要做一层代理通过本地调用国外大模型官方的key。 一、利用 vercel 部署 OpenAI Proxy 第一步&#xff1a;Fork OpenEE 这个仓库 https://github.com/openaiee/openaiee 第二步&#xff1a;创建vercel项目 第三步…