Objective-C Associated Objects 的實現原理

来源:http://www.cnblogs.com/lurenq/archive/2017/06/12/6993527.html
-Advertisement-
Play Games

我們知道,在 Objective-C 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變數,這似乎成為了 Objective-C 的一個明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects 來彌補這一不足。本文將結合 runtime 源碼深入探究 O ...


我們知道,在 Objective-C 中可以通過 Category 給一個現有的類添加屬性,但是卻不能添加實例變數,這似乎成為了 Objective-C 的一個明顯短板。然而值得慶幸的是,我們可以通過 Associated Objects 來彌補這一不足。本文將結合 runtime 源碼深入探究 Objective-C 中 Associated Objects 的實現原理。

在閱讀本文的過程中,讀者需要著重關註以下三個問題:

  1. 關聯對象被存儲在什麼地方,是不是存放在被關聯對象本身的記憶體中?
  2. 關聯對象的五種關聯策略有什麼區別,有什麼坑?
  3. 關聯對象的生命周期是怎樣的,什麼時候被釋放,什麼時候被移除?

 

這是我寫這篇文章的初衷,也是本文的價值所在。

使用場景

按照 Mattt Thompson 大神的文章 Associated Objects 中的說法,Associated Objects 主要有以下三個使用場景:

  1. 為現有的類添加私有變數以幫助實現細節;
  2. 為現有的類添加公有屬性;
  3. 為 KVO 創建一個關聯的觀察者。

 

從本質上看,第 1 、2 個場景其實是一個意思,唯一的區別就在於新添加的這個屬性是公有的還是私有的而已。就目前來說,我在實際工作中使用得最多的是第 2 個場景,而第 3 個場景我還沒有使用過。

相關函數

與 Associated Objects 相關的函數主要有三個,我們可以在 runtime 源碼的 runtime.h 文件中找到它們的聲明:

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);

這三個函數的命名對程式員非常友好,可以讓我們一眼就看出函數的作用:

  • objc_setAssociatedObject 用於給對象添加關聯對象,傳入 nil 則可以移除已有的關聯對象;
  • objc_getAssociatedObject 用於獲取關聯對象;
  • objc_removeAssociatedObjects 用於移除一個對象的所有關聯對象。

 

objc_removeAssociatedObjects 函數我們一般是用不上的,因為這個函數會移除一個對象的所有關聯對象,將該對象恢覆成“原始”狀態。這樣做就很有可能把別人添加的關聯對象也一併移除,這並不是我們所希望的。所以一般的做法是通過給 objc_setAssociatedObject 函數傳入 nil來移除某個已有的關聯對象。

key 值

關於前兩個函數中的 key 值是我們需要重點關註的一個點,這個 key 值必須保證是一個對象級別(為什麼是對象級別?看完下麵的章節你就會明白了)的唯一常量。一般來說,有以下三種推薦的 key 值:

  1. 聲明 static char kAssociatedObjectKey; ,使用 &kAssociatedObjectKey 作為 key 值;
  2. 聲明 static void *kAssociatedObjectKey = &kAssociatedObjectKey; ,使用 kAssociatedObjectKey 作為 key 值;
  3. 用 selector ,使用 getter 方法的名稱作為 key 值。

 

我個人最喜歡的(沒有之一)是第 3 種方式,因為它省掉了一個變數名,非常優雅地解決了計算科學中的兩大世界難題之一(命名)。

關聯策略

在給一個對象添加關聯對象時有五種關聯策略可供選擇:

 

關聯策略等價屬性說明
OBJC_ASSOCIATION_ASSIGN @property (assign) or @property (unsafe_unretained) 弱引用關聯對象
OBJC_ASSOCIATION_RETAIN_NONATOMIC @property (strong, nonatomic) 強引用關聯對象,且為非原子操作
OBJC_ASSOCIATION_COPY_NONATOMIC @property (copy, nonatomic) 複製關聯對象,且為非原子操作
OBJC_ASSOCIATION_RETAIN @property (strong, atomic) 強引用關聯對象,且為原子操作
OBJC_ASSOCIATION_COPY @property (copy, atomic) 複製關聯對象,且為原子操作

 

 

其中,第 2 種與第 4 種、第 3 種與第 5 種關聯策略的唯一差別就在於操作是否具有原子性。由於操作的原子性不在本文的討論範圍內,所以下麵的實驗和討論就以前三種以例進行展開。

實現原理

在探究 Associated Objects 的實現原理前,我們還是先來動手做一個小實驗,研究一下關聯對象什麼時候會被釋放。本實驗主要涉及 ViewController 類和它的分類 ViewController+AssociatedObjects 。:本實驗的完整代碼可以在這裡 AssociatedObjects 找到,其中關鍵代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface ViewController (AssociatedObjects)

@property (assign, nonatomic) NSString *associatedObject_assign;
@property (strong, nonatomic) NSString *associatedObject_retain;
@property (copy,   nonatomic) NSString *associatedObject_copy;

@end

@implementation ViewController (AssociatedObjects)

- (NSString *)associatedObject_assign {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setAssociatedObject_assign:(NSString *)associatedObject_assign {
    objc_setAssociatedObject(self, @selector(associatedObject_assign), associatedObject_assign, OBJC_ASSOCIATION_ASSIGN);
}

- (NSString *)associatedObject_retain {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setAssociatedObject_retain:(NSString *)associatedObject_retain {
    objc_setAssociatedObject(self, @selector(associatedObject_retain), associatedObject_retain, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)associatedObject_copy {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setAssociatedObject_copy:(NSString *)associatedObject_copy {
    objc_setAssociatedObject(self, @selector(associatedObject_copy), associatedObject_copy, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

在 ViewController+AssociatedObjects.h 中聲明瞭三個屬性,限定符分別為 assign, nonatomicstrong, nonatomic 和 copy, nonatomic ,而在 ViewController+AssociatedObjects.m 中相應的分別用 OBJC_ASSOCIATION_ASSIGN 、OBJC_ASSOCIATION_RETAIN_NONATOMIC 、OBJC_ASSOCIATION_COPY_NONATOMIC 三種關聯策略為這三個屬性添加“實例變數”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__weak NSString *string_weak_assign = nil;
__weak NSString *string_weak_retain = nil;
__weak NSString *string_weak_copy   = nil;

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    self.associatedObject_assign = [NSString stringWithFormat:@"leichunfeng1"];
    self.associatedObject_retain = [NSString stringWithFormat:@"leichunfeng2"];
    self.associatedObject_copy   = [NSString stringWithFormat:@"leichunfeng3"];

    string_weak_assign = self.associatedObject_assign;
    string_weak_retain = self.associatedObject_retain;
    string_weak_copy   = self.associatedObject_copy;
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//    NSLog(@"self.associatedObject_assign: %@", self.associatedObject_assign); // Will Crash
    NSLog(@"self.associatedObject_retain: %@", self.associatedObject_retain);
    NSLog(@"self.associatedObject_copy:   %@", self.associatedObject_copy);
}

@end

在 ViewController 的 viewDidLoad 方法中,我們對三個屬性進行了賦值,並聲明瞭三個全局的 __weak 變數來觀察相應對象的釋放時機。此外,我們重寫了 touchesBegan:withEvent: 方法,在方法中分別列印了這三個屬性的當前值。

在繼續閱讀下麵章節前,建議讀者先自行思考一下 self.associatedObject_assign 、self.associatedObject_retain 和 self.associatedObject_copy 指向的對象分別會在什麼時候被釋放,以加深理解。

實驗

我們先在 viewDidLoad 方法的第 28 行打上斷點,然後運行程式,點擊導航欄右上角的按鈕 Push到 ViewController 界面,程式將停在斷點處。接著,我們使用 lldb 的 watchpoint 命令來設置觀察點,觀察全局變數 string_weak_assign 、string_weak_retain 和 string_weak_copy 的值的變化。正確設置好觀察點後,將會在 console 中看到如下的類似輸出:

設置觀察點

點擊繼續運行按鈕,有一個觀察點將被命中。我們先查看 console 中的輸出,通過將這一步列印的 old value 和上一步的 new value 進行對比,我們可以知道本次命中的觀察點是 string_weak_assign ,string_weak_assign 的值變成了 0x0000000000000000 ,也就是 nil 。換句話說 self.associatedObject_assign 指向的對象已經被釋放了,而通過查看左側調用棧我們可以知道,這個對象是由於其所在的 autoreleasepool 被 drain 而被釋放的,這與我前面的文章《Objective-C Autorelease Pool 的實現原理 》中的表述是一致的。提示,待會你也可以放開 touchesBegan:withEvent: 中第 31 行的註釋,在 ViewController 出現後,點擊一下它的 view ,進一步驗證一下這個結論。

設置觀察點

接下來,我們點擊 ViewController 導航欄左上角的按鈕,返回前一個界面,此時,又將有一個觀察點被命中。同理,我們可以知道這個觀察點是 string_weak_retain 。我們查看左側的調用棧,將會發現一個非常敏感的函數調用 _object_remove_assocations ,調用這個函數後 ViewController 的所有關聯對象被全部移除。最終,self.associatedObject_retain 指向的對象被釋放。

設置觀察點

點擊繼續運行按鈕,最後一個觀察點 string_weak_copy 被命中。同理,self.associatedObject_copy 指向的對象也由於關聯對象的移除被最終釋放。

設置觀察點

結論

由這個實驗,我們可以得出以下結論:

  1. 關聯對象的釋放時機與被移除的時機並不總是一致的,比如上面的 self.associatedObject_assign 所指向的對象在 ViewController 出現後就被釋放了,但是 self.associatedObject_assign 仍然有值,還是保存的原對象的地址。如果之後再使用 self.associatedObject_assign 就會造成 Crash ,所以我們在使用弱引用的關聯對象時要非常小心;
  2. 一個對象的所有關聯對象是在這個對象被釋放時調用的 _object_remove_assocations 函數中被移除的。

 

接下來,我們就一起看看 runtime 中的源碼,來驗證下我們的實驗結論。

objc_setAssociatedObject

我們可以在 objc-references.mm 文件中找到 objc_setAssociatedObject 函數最終調用的函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
    // retain the new value (if any) outside the lock.
    ObjcAssociation old_association(0, nil);
    id new_value = value ? acquireValue(value, policy) : nil;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        if (new_value) {
            // break any existing association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i != associations.end()) {
                // secondary table exists
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    j->second = ObjcAssociation(policy, new_value);
                } else {
                    (*refs)[key] = ObjcAssociation(policy, new_value);
                }
            } else {
                // create the new association (first time).
                ObjectAssociationMap *refs = new ObjectAssociationMap;
                associations[disguised_object] = refs;
                (*refs)[key] = ObjcAssociation(policy, new_value);
                object->setHasAssociatedObjects();
            }
        } else {
            // setting the association to nil breaks the association.
            AssociationsHashMap::iterator i = associations.find(disguised_object);
            if (i !=  associations.end()) {
                ObjectAssociationMap *refs = i->second;
                ObjectAssociationMap::iterator j = refs->find(key);
                if (j != refs->end()) {
                    old_association = j->second;
                    refs->erase(j);
                }
            }
        }
    }
    // release the old value (outside of the lock).
    if (old_association.hasValue()) ReleaseValue()(old_association);
}

在看這段代碼前,我們需要先瞭解一下幾個數據結構以及它們之間的關係:

  1. AssociationsManager 是頂級的對象,維護了一個從 spinlock_t 鎖到 AssociationsHashMap 哈希表的單例鍵值對映射;
  2. AssociationsHashMap 是一個無序的哈希表,維護了從對象地址到 ObjectAssociationMap 的映射;
  3. ObjectAssociationMap 是一個 C++ 中的 map ,維護了從 key 到 ObjcAssociation 的映射,即關聯記錄;
  4. ObjcAssociation 是一個 C++ 的類,表示一個具體的關聯結構,主要包括兩個實例變數,_policy 表示關聯策略,_value 表示關聯對象。

 

每一個對象地址對應一個 ObjectAssociationMap 對象,而一個 ObjectAssociationMap 對象保存著這個對象的若幹個關聯記錄。

弄清楚這些數據結構之間的關係後,再回過頭來看上面的代碼就不難了。我們發現,在蘋果的底層代碼中一般都會充斥著各種 if else ,可見寫好 if else 後我們就距離成為高手不遠了。開個玩笑,我們來看下麵的流程圖,一圖勝千言:

objc_setAssociatedObject

objc_getAssociatedObject

同樣的,我們也可以在 objc-references.mm 文件中找到 objc_getAssociatedObject 函數最終調用的函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
id _object_get_associative_reference(id object, void *key) {
    id value = nil;
    uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            ObjectAssociationMap *refs = i->second;
            ObjectAssociationMap::iterator j = refs->find(key);
            if (j != refs->end()) {
                ObjcAssociation &entry = j->second;
                value = entry.value();
                policy = entry.policy();
                if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) ((id(*)(id, SEL))objc_msgSend)(value, SEL_retain);
            }
        }
    }
    if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
        ((id(*)(id, SEL))objc_msgSend)(value, SEL_autorelease);
    }
    return value;
}

看懂了 objc_setAssociatedObject 函數後,objc_getAssociatedObject 函數對我們來說就是小菜一碟了。這個函數先根據對象地址在 AssociationsHashMap 中查找其對應的 ObjectAssociationMap 對象,如果能找到則進一步根據 key 在 ObjectAssociationMap 對象中查找這個 key 所對應的關聯結構 ObjcAssociation ,如果能找到則返回 ObjcAssociation 對象的 value 值,否則返回 nil 。

objc_removeAssociatedObjects

同理,我們也可以在 objc-references.mm 文件中找到 objc_removeAssociatedObjects 函數最終調用的函數:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

這個函數負責移除一個對象的所有關聯對象,具體實現也是先根據對象的地址獲取其對應的 ObjectAssociationMap 對象,然後將所有的關聯結構保存到一個 vector 中,最終釋放 vector中保存的所有關聯對象。根據前面的實驗觀察到的情況,在一個對象被釋放時,也正是調用的這個函數來移除其所有的關聯對象。

給類對象添加關聯對象

看完源代碼後,我們知道對象地址與 AssociationsHashMap 哈希表是一一對應的。那麼我們可能就會思考這樣一個問題,是否可以給類對象添加關聯對象呢?答案是肯定的。我們完全可以用同樣的方式給類對象添加關聯對象,只不過我們一般情況下不會這樣做,因為更多時候我們可以通過 static 變數來實現類級別的變數。我在分類 ViewController+AssociatedObjects 中給 ViewController 類對象添加了一個關聯對象 associatedObject ,讀者可以親自在 viewDidLoad方法中調用一下以下兩個方法驗證一下:

1
2
+ (NSString *)associatedObject;
+ (void)setAssociatedObject:(NSString *)associatedObject;

總結

讀到這裡,相信你對開篇的那三個問題已經有了一定的認識,下麵我們再梳理一下:

  1. 關聯對象與被關聯對象本身的存儲並沒有直接的關係,它是存儲在單獨的哈希表中的;
  2. 關聯對象的五種關聯策略與屬性的限定符非常類似,在絕大多數情況下,我們都會使用 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的關聯策略,這可以保證我們持有關聯對象;
  3. 關聯對象的釋放時機與移除時機並不總是一致,比如實驗中用關聯策略 OBJC_ASSOCIATION_ASSIGN進行關聯的對象,很早就已經被釋放了,但是並沒有被移除,而再使用這個關聯對象時就會造成 Crash 。

 

在弄懂 Associated Objects 的實現原理後,可以幫助我們更好地使用它,在出現問題時也能儘快地定位問題,最後希望本文能夠對你有所幫助。

參考鏈接

http://nshipster.com/associated-objects/ 
http://kingscocoa.com/tutorials/associated-objects/

http://blog.csdn.net/u014220518/article/details/52873164

https://www.oschina.net/code/snippet_2248391_52784

DEMO下載:

http://download.csdn.net/detail/u014220518/9659110


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

-Advertisement-
Play Games
更多相關文章
  • IE每個版本判斷 <script type="text/javascript"> var isIE = function(ver){ var b = document.createElement('b'); b.innerHTML = '<!--[if IE '+ ver +']><i></i><! ...
  • 把以上兩個文件放到同一目錄 運行index.html文件就可以看到效果了。 如果出現 : TypeError: Cannot set property '$render' of undefined 說明angular的版本太低,這個版本要求至少在v1.2.30以上. github傳送門:https: ...
  • 本人做前端兩年,感覺沒什麼進步,自己還是太菜了,想通過博客來記錄一下自己學習進度,以便日後自己查看和想學習Vue的朋友參考,大家互相學習,有寫得不對的地方歡迎大家指點,助我成長成大神,哈哈!!! ...
  • (1)全局環境 在全局環境使用this,它指的就是頂層對象window。 (2)構造函數 構造函數中的this,指的是實例對象。 (3)對象的方法 當A對象的方法被賦予B對象,該方法中的this就從指向A對象變成了指向B對象。所以要特別小心,將某個對象的方法賦值給另一個對象,會改變this的指向。 ...
  • 一,工程圖。 二,代碼。 RootViewController.m #import "RootViewController.h" //加入頭文件 #import "UIImageView+WebCache.h" @interface RootViewController () @end @imple ...
  • 先看效果: 我並沒有找到有設置ListView分割線的屬性 下麵是一個比較簡單的實現,如果有同學有更好的實現,歡迎留言,讓我們共同進步。我的敘述不一定准確 實現的方法就是在DataTemplate里包一個Border 1、首先自定義一個UserControl來實現佈局,文件名為ItemView.xa ...
  • 1.插件化 ①插件化技術是Android技術領域的集大成者。基於插件化技術,企業可以隨時發佈新功能、修複線上bug,而不需要重新發版。②本解決方案將提供動態替換、靜態代理兩種成熟的插件化實現方式,企業可以根據自身實際情況,決定在項目中採取哪一種。③在提供插件化框架的同時,還將提供增量更新、伺服器插件 ...
  • CAR_TUNE_VALUE 是用來校準 流過電池上的電流 與 系統偵測到的電流 的一致性, 校準接法 正常接法如下圖, 電池流過的電流等於系統流過的電流, 校準的接法如下圖, 電池一樣需要給電,但是 battery ground 需要和 system ground 接在一起,這個會形成一個迴路, ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...