OC對象之旅 weak弱引用實現分析

来源:http://www.cnblogs.com/wws19125/archive/2017/06/10/6978563.html
-Advertisement-
Play Games

Runtime學習 應用源碼學習   Runtime源碼分析,帶你瞭解OC實現過程。其中參考了大量的大神的代碼以及文獻,裡面也有個人的見解,歡迎拍磚,歡迎交流。 兩種常見使用場景 根據調試信息,發現兩者的區別是: 第一種進入到 第二種繞一個遠路,先初始化 兩者最終進入到如下方法 ...


Runtime學習 -- weak應用源碼學習

  Runtime源碼分析,帶你瞭解OC實現過程。其中參考了大量的大神的代碼以及文獻,裡面也有個人的見解,歡迎拍磚,歡迎交流。

兩種常見使用場景

/// weak屬性
@interface XX : XX
@property(nonatomic,weak) Type* weakPtr;
@end

/// 代碼塊中使用
{
    /// 使用__weak
    __weak Type* weakPtr = [[SomeObject alloc] init];
}

根據調試信息,發現兩者的區別是:

  • 第一種進入到 id objc_storeWeak(id *location, id newObj)方法
    ```
    /**
  • This function stores a new value into a __weak variable. It would
  • be used anywhere a __weak variable is the target of an assignment.
  • @param location The address of the weak pointer itself
  • @param newObj The new object this weak ptr should now point to
  • @return \e newObj
    /
    id
    objc_storeWeak(id
    location, id newObj)
    {
    return storeWeak
    (location, (objc_object *)newObj);
    }
    ```
  • 第二種繞一個遠路,先初始化 id objc_initWeak(id *location, id newObj)
    ``` Objective-C
    /**
  • Initialize a fresh weak pointer to some object location.
  • It would be used for code like:
  • (The nil case)
  • __weak id weakPtr;
  • (The non-nil case)
  • NSObject *o = ...;
  • __weak id weakPtr = o;
  • This function IS NOT thread-safe with respect to concurrent
  • modifications to the weak variable. (Concurrent weak clear is safe.)
  • @param location Address of __weak ptr.
  • @param newObj Object ptr.
    /
    id objc_initWeak(id
    location, id newObj)
    {
    if (!newObj) {
    *location = nil;
    return nil;
    }

    return storeWeak
    (location, (objc_object*)newObj);
    }
    ```
  • 兩者最終進入到如下方法

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id
storeWeak(id *location, objc_object *newObj)
{
    ///略去,下麵會進行分析 
    ...
    return (id)newObj;
}

所以重點就在 storeWeak這個方法中,let's do it

分析源碼

storeWeak源碼的如下:

template <HaveOld haveOld, HaveNew haveNew,
          CrashIfDeallocating crashIfDeallocating>
static id storeWeak(id *location, objc_object *newObj)
{
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo<haveOld, haveNew>(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
        goto retry;
    }
    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    /// 註釋大意是通過下麵操作,保證所有的弱引用對象的isa都被初始化,這樣可以防止死鎖,PS,這裡我不是太明白,求指教
    if (haveNew  &&  newObj) {
        /// 下麵的操作是初始化isa
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
            _class_initialize(_class_getNonMetaClass(cls, (id)newObj));

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);

    return (id)newObj;
}
  • template 是C++的一種泛型實現,相當於這裡申明瞭變數或者類型,可以在代碼塊中使用,用於處理不同的未知類型&枚舉。
  • haveOld 弱引用是否已經有所指向
  • haveNew 是否有新的指向
  • CrashIfDeallocating 執行方法時發生Deallocate是否Crash

PS:初始化ISA那部分為何能阻止死鎖,我沒有看懂
該函數流程如下:


流程圖

重點來了:

/// SideTables
oldTable = &SideTables()[oldObj];
newTable = &SideTables()[newObj];
/// taggedPointer是什麼鬼
isTaggedPointer
/// 註冊弱引用
weak_register_no_lock(&newTable->weak_table, (id)newObj, location,crashIfDeallocating);
/// 消除弱引用
weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);

SideTable

SideTable是一個結構體,定義如下

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    ///鎖
    ....
};
  • spinlock_t solck 鎖
  • RefcountMap refcnts 強引用使用,略過
  • weak_table_t weak_table 弱引用表
    SideTable是存放引用關係的,對象通過Hash值操作,在SideTableBuf 中尋找與之對應的SideTableSideTableBuf初始化過程如下:

    alignas(StripedMap<SideTable>) static uint8_t 
    SideTableBuf[sizeof(StripedMap<SideTable>)];
    /// 會在Objc_init中調用該方法
    static void SideTableInit() {
    /// 這句話貌似沒什麼卵用,求指教
    new (SideTableBuf) StripedMap<SideTable>();
    }
    /// 尋找SideTable
    static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
    }

    StripedMap是一個泛型類,並重寫了[]運算符,通過對象的地址,運算出Hash值,通過該hash值找到對象的SideTable
    ```
    template
    class StripedMap {
    enum { CacheLineSize = 64 };

    if TARGET_OS_EMBEDDED

    enum { StripeCount = 8 };

    else

    enum { StripeCount = 64 };

    endif

    struct PaddedT {
    T value alignas(CacheLineSize);
    };
    PaddedT array[StripeCount];
    /// 運算
    static unsigned int indexForPointer(const void *p) {
    uintptr_t addr = reinterpret_cast(p);
    /// 位運算可以控制返回值在0-63之間
    return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }

public:
T& operator[] (const void *p) {
return array[indexForPointer(p)].value;
}
/// 下麵略去
...
}


### taggedPointer
簡單的說,這是一種優化手段,即將對象的值,存入對象的地址中,這些工程師簡直喪心病狂,就為了省一點記憶體嘛!

### 進入正題,看看怎麼實現弱引用的
先看看註冊的過程吧

/**

  • Registers a new (object, weak pointer) pair. Creates a new weak
  • object entry if it does not exist.
  • @param weak_table The global weak table.
  • @param referent The object pointed to by the weak reference.
  • @param referrer The weak pointer address.
    /
    id weak_register_no_lock(weak_table_t
    weak_table, id referent_id,
    id referrer_id, bool crashIfDeallocating)
    {
    /// 轉化為object
    objc_object
    referent = (objc_object *)referent_id;
    objc_object referrer = (objc_object )referrer_id;
    /// 如果是taggedPointer,就沒有引用的過程了
    if (!referent || referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
    deallocating = referent->rootIsDeallocating();
    }
    else {
    BOOL (allowsWeakReference)(objc_object , SEL) =
    (BOOL()(objc_object , SEL))
    object_getMethodImplementation((id)referent,
    SEL_allowsWeakReference);
    if ((IMP)allowsWeakReference == _objc_msgForward) {
    return nil;
    }
    deallocating =
    ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }
    /// 如果正在被銷毀
    if (deallocating) {
    if (crashIfDeallocating) {
    _objc_fatal("Cannot form weak reference to instance (%p) of "
    "class %s. It is possible that this object was "
    "over-released, or is in the process of deallocation.",
    (void*)referent, object_getClassName((id)referent));
    } else {
    return nil;
    }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
    append_referrer(entry, referrer);
    }
    else {
    weak_entry_t new_entry(referent, referrer);
    weak_grow_maybe(weak_table);
    weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the
    // value not change.

    return referent_id;
    }
    ```
    先從這行數的參數說起,參數有4個
  • weak_table_t *weak_table hash表
  • id referent_id, 弱引用對象
  • id *referrer_id, 弱引用指針
  • bool crashIfDeallocating 如果正在Deallocate是否crash

後三個參數不用解釋,主要解釋第一個參數,weak_table_t,定義如下

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries; ///數組,用於存儲引用對象集合
    size_t    num_entries;  /// 存儲數目
    uintptr_t mask; /// 當前分配容量
    uintptr_t max_hash_displacement; /// 已使用容量
};

沒錯,weak_table_t就是寄存在SideTable

  • weak_entry_t *weak_entries; ///數組,用於存儲引用對象集合
  • size_t num_entries; /// 存儲數目
  • uintptr_t mask; /// 當前分配容量
  • uintptr_t max_hash_displacement; /// 已使用容量

定義中我們重點關註weak_entry_t

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

weak_entry_t是最終存放對象和引用指針的地方,referent是被引用的對象,聯合體union釋義如下

  • weak_referrer_t *referrers; 存放引用指針
  • uintptr_t out_of_line_ness : 2 標識當前存儲是否在初始WEAK_INLINE_COUNT個數之內
  • uintptr_t num_refs : PTR_MINUS_2 引用的個數
  • uintptr_t mask; 實際分配容量
  • uintptr_t max_hash_displacement; 實際使用容量,包括已經被釋放的,每次調整容量時會更新重置
  • weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; 當引用個數小於WEAK_INLINE_COUNT時,使用該數組存放。

註冊引用過程中,重點關註下麵代碼:

{
weak_entry_t *entry;
    /// 查找是否已經註冊過了
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        /// 加上去就可以了
        append_referrer(entry, referrer);
    } 
    else {
        /// 新建一個
        weak_entry_t new_entry(referent, referrer);
        /// 調整weak_table_t 的容量大小
        weak_grow_maybe(weak_table);
        /// 插入一個
        weak_entry_insert(weak_table, &new_entry);
    }
}

新建

通過weak_entry_t的源碼,可以看到新建一個weak_entry_t的過程是

  • 將被引用對象賦予referent
  • 將引用指針放入到inline_referrers,因為此時數目還很少

調整weak_table_t的容量大小

static void weak_resize(weak_table_t *weak_table, size_t new_size)
{
    size_t old_size = TABLE_SIZE(weak_table);

    weak_entry_t *old_entries = weak_table->weak_entries;
    weak_entry_t *new_entries = (weak_entry_t *)
        calloc(new_size, sizeof(weak_entry_t));

    weak_table->mask = new_size - 1;
    weak_table->weak_entries = new_entries;
    /// 重置
    weak_table->max_hash_displacement = 0;
    weak_table->num_entries = 0;  // restored by weak_entry_insert below
    
    if (old_entries) {
        weak_entry_t *entry;
        weak_entry_t *end = old_entries + old_size;
        for (entry = old_entries; entry < end; entry++) {
            if (entry->referent) {
                weak_entry_insert(weak_table, entry);
            }
        }
        free(old_entries);
    }
}

// Grow the given zone's table of weak references if it is full.
static void weak_grow_maybe(weak_table_t *weak_table)
{
    size_t old_size = TABLE_SIZE(weak_table);

    // Grow if at least 3/4 full.
    if (weak_table->num_entries >= old_size * 3 / 4) {
        weak_resize(weak_table, old_size ? old_size*2 : 64);
    }
}

當實際的數目大於old_size(old_size就是mask的大小+1),就去調整大小,同時重置max_hash_displacement為0,通過calloc函數,動態分配mask個的記憶體,然後通過迴圈,將原有的weak_entry_t插入到新的容器中,在插入的過程中,更新max_hash_displacement.

weak_table_t插入weak_entry_t

static void weak_entry_insert(weak_table_t *weak_table, weak_entry_t *new_entry)
{
    weak_entry_t *weak_entries = weak_table->weak_entries;
    assert(weak_entries != nil);

    size_t begin = hash_pointer(new_entry->referent) & (weak_table->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_entries[index].referent != nil) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_entries);
        hash_displacement++;
    }
    /// 把新的加進去
    weak_entries[index] = *new_entry;
    /// 引用計數+1
    weak_table->num_entries++;
    /// 擴容前最大占位
    if (hash_displacement > weak_table->max_hash_displacement) {
        weak_table->max_hash_displacement = hash_displacement;
    }
}

過程比較簡單,也是利用hash處理,方便後面查找。

weak_table_t查找對象是通過迴圈遍歷的方式,過程如下

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask; /// 獲取hash值
    size_t index = begin;
    size_t hash_displacement = 0;
    /// 迴圈遍歷,查找
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        // 查找到最大的時候,結束
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

在已有的weak_entry_t中加入引用

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    /// 如果是數組,即個數比較少
    if (! entry->out_of_line()) {
        // Try to insert inline.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line());

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}

該過程同在weak_table_t中插入weak_entry_t如出一轍,要註意的是需要判斷引用的個數,當引用個數大於WEAK_INLINE_COUNT時,需要將原有的引用指針也移到referrers中,同時更新相關計數器。
上面過程的流程如下:

查找流程

消除弱引用

消除弱引用過程同註冊大致相同,只是部分地方是相反操作,不做贅述了


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一、在app/src/main/res下有 AndroidManifest.xml打開,打開後如下圖1 二、日誌工具log log.v() log.d() log.i() log.w() log.e() 在下圖中 中存在輸出 ...
  • 先看效果: 參照Android的實現方式用RadioButton來實現,但是Uwp的RadioButton並沒有安卓的Selector選擇器 下麵是一個比較簡單的實現,如果有同學有更好的實現,歡迎留言,讓我們共同進步。 1、首先自定義一個RadioImageButton控制項,並定義幾個依賴屬性,代碼 ...
  • 1.Android DVM(Dalvik VM)的進程和Linux的進程, 應用程式的進程是同一個概念嗎? DVM(Dalvik VM)指dalvik的虛擬機。每一個Android應用程式都在它自己的進程中運行,都擁有一個獨立的Dalvik虛擬機實例。而每一個DVM都是在Linux 中的一個進程,所 ...
  • iOS CAShapeLayer、CADisplayLink 實現波浪動畫效果 效果圖 代碼已上傳 GitHub:https://github.com/Silence GitHub/CoreAnimationDemo 可以自定義波浪高度、寬度、速度、方向、漸變速度、水的深度等參數。 實現原理 波浪的 ...
  • 作者:Antonio Leiva 時間:Jun 6, 2017 原文鏈接:https://antonioleiva.com/interfaces-kotlin/ 與Java相比,Kotlin介面允許你重用更多的代碼。 原因非常簡單:你能夠向你的介面加代碼。如果你已經試用過Java8,這非常類似。 能 ...
  • 瞭解這一章節,需要先瞭解LayoutInflater這個工具類,我以前分析過:http://www.cnblogs.com/kezhuang/p/6978783.html Window是Activity類中的一個全局變數,Window的作用是輔助Activity(也有可能是其他組件,本章拿Activ ...
  • LayoutInflater是用來解析XML佈局文件,然後生成對象的ViewTree的工具類。是這個工具類的存在,才能讓我們寫起Layout來那麼省勁。 我們接下來進去刨析,看看裡邊的奧秘 我們在使用這個類的時候,通常都是像上面這樣寫,首先通過from函數獲取對象,在調用inflate方法,來生成相 ...
  • Android中整個的View的組裝是採用組合模式。 ViewGroup就相當與樹根,各種Layout就相當於枝幹,各種子View,就相當於樹葉。 至於View類。我們就當它是個種子吧。哈哈! ViewGroup屬於樹根,可以生長數很多枝幹(繼承自定義Layout)而枝幹上有可以長出很多葉子(Tex ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...