Java集合中的HashMap類

来源:https://www.cnblogs.com/yulinfeng/archive/2018/03/13/8558983.html
-Advertisement-
Play Games

jdk1.8.0_144 HashMap作為最常用集合之一,繼承自AbstractMap。JDK8的HashMap實現與JDK7不同,新增了紅黑樹作為底層數據結構,結構變得複雜,效率變得更高。為滿足自身需要,也重新實現了很多AbstractMap中的方法。本文會圍繞HashMap,詳細探討HashM ...


jdk1.8.0_144

         HashMap作為最常用集合之一,繼承自AbstractMap。JDK8的HashMap實現與JDK7不同,新增了紅黑樹作為底層數據結構,結構變得複雜,效率變得更高。為滿足自身需要,也重新實現了很多AbstractMap中的方法。本文會圍繞HashMap,詳細探討HashMap的底層數據結構、擴容機制、併發環境下的死迴圈問題等。

         JDK8同JDK7一樣對Map.Entry進行了重新實現,改了個名字叫——Node,我想這是因為在紅黑樹中更方便理解,方法和JDK7大體相同只是取消了幾個方法。並且此時的Node節點(也就是Entry)結構更加完善:

1 static class Node<K,V> implements Map.Entry<K,V> {
2     final int hash;            //節點hash值
3     final K key;                 //key值
4     V value;                      //value值
5     Node<K,V> next;      //指向的下一個節點
6 
7     //省略,由於JDK8的Map介面新增了幾個compare比較的方法,Node直接就繼承了
8 
9 }

  Node作為HashMap維護key-value的內部數據結構比較簡單,下麵是HashMap重新實現Map的方法。

public int size()

         HashMap並沒有繼承AbstractMap的size方法,而是重寫了此方法。HashMap在類中定義了一個size變數,再此處直接返回size變數而不用調用entrySet方法返回集合再計算。可以猜測這個size變數是當插入一個key-value鍵值對的時候自增。

public boolean isEmpty()

         判斷size變數是否0即可。

public boolean containsKey(Object key)

         AbstractMap通過遍歷Entry節點的方式實現了這個方法,顯然HashMap覺得效率太低並沒有復用而是重寫了這個方法。

         JDK8的HashMap底層數據結構引入了紅黑樹,它的實現要比JDK7略微複雜,我們先來看JDK7關於這個方法的實現。

1 //JDK7,HashMap#containsKey
2 public boolean containsKey(Object key) {
3     return getEntry(key) != null;        //調用getEntry方法
4 }

  getEntry實現的思路也比較簡單,由於JDK7的HashMap是數組+鏈表的數據結構,當key的hash值衝突的時候使用鏈地址法直接加到衝突地址Entry的next指針行程鏈表即可。所以getEntry方法的思路也是先計算key的hash值,計算後再找到它在散列表的下標,找到過再遍歷這個位置的鏈表返回結果即可。

  JDK8加入了紅黑樹,在鏈表的個數達到閾值8時會將鏈表轉換為紅黑樹,如果此時是紅黑樹,則不能通過遍歷鏈表的方式尋找key值,所以JDK8對該方法進行了改進主要是需要遍歷紅黑樹,有關紅黑樹的具體演算法在此不多介紹。

1 //JDK8,HashMap#containsKey
2 public boolean containsKey(Object key) {
3     return getNode(hash(key), key) != null;    //JDK8中新增了一個getNode方法,且將key的hash值計算好後作為參數傳遞。
4 }
5 //HashMap#getNode
6 final Node<K,V> getNode(int hash, Object key) {
7     //此方法相比較於JDK7中的getEntry基本相同,唯一不同的是發現key值衝突過後會通過“first instanceof TreeNode”檢查此時是否是紅黑樹結構。如果是紅黑樹則會調用getTreeNode方法在紅黑樹上進行查詢。如果不是紅黑樹則是鏈表結構,遍歷鏈表即可。
8 }

 public boolean containsValue(Object value)

  遍歷散列表中的元素

public V get(Object key)

   在JDK8中get方法調用了containsKey的方法getNode,這點和JDk7的get方法中調用getEntry方法類似。

  1. 將參數key的hash值和key作為參數,調用getNode方法;
  2. 根據(n - 1) & hash(key)計算key值所在散列桶的下標;
  3. 取出散列桶中的key與參數key進行比較:

         3.1 如果相等則直接返回Node節點;

         3.2 如果不相等則判斷當前節點是否有後繼節點:

                   3.2.1 判斷是否是紅黑樹結構,是則調用getTreeNode查詢鍵值為key的Node   節點;

                   3.2.2 如果是鏈表結構,則遍歷整個鏈表。

public V put(K key, V value)

  這個方法最為關鍵,插入key-value到Map中,在這個方法中需要計算key的hash值,然後通過hash值計算所在散列桶的位置,判斷散列桶的位置是否有衝突,衝突過後需要使用鏈地址法解決衝突,使之形成一個鏈表,從JDK8開始如果鏈表的元素達到8個過後還會轉換為紅黑樹。在插入時還需要判斷是否需要擴容,擴容機制的設計,以及在併發環境下擴容所帶來的死迴圈問題。

  由於JDK7比較簡單,我們先來查看JDK7中的put方法源碼。

JDK7——HashMap#put

 1 //JDK7, HashMap#put
 2 public V put(K key, V value) {
 3     //1. 首先判斷是否是第一次插入,即散列表是否指向空的數組,如果是,則調用inflateTable方法對HashMap進行初始化。
 4     if (table == EMPTY_TABLE) {
 5         inflateTable(threshold);
 6     }
 7     //2. 判斷key是否等於null,等於空則調用putForNullKey方法存入key為null的key-value,HashMap支持key=null。
 8     if (key == null)
 9         return putForNullKey(value);
10     //3. 調用hash方法計算key的hash值,調用indexFor根據hash值和散列表的長度計算key值所在散列表的下標i。
11     int hash = hash(key);
12     int i = indexFor(hash, table.length);
13     //4. 這一步通過迴圈遍歷的方式判斷插入的key-value是否已經在HashMap中存在,判斷條件則是key的hash值相等,且value要麼引用相等要麼equals相等,如果滿足則直接返回value。
14     for (Entry<K,V> e = table[i]; e != null; e = e.next) {//如果插入位置沒有散列衝突,即這個位置沒有Entry元素,則不進入迴圈。有散列衝突則需要遍歷鏈表進行判斷。
15         Object k;
16         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
17             V oldValue = e.value;
18             e.value = value;
19             e.recordAccess(this);
20             return oldValue;
21         }
22     }
23     //插入
24     modCount++;//記錄修改次數,在併發環境下通過迭代器遍歷時會拋出ConcurrentModificationException異常(Fail-Fast機制),就是通過這個變數來實現的。在迭代器初始化過程會將modCount賦給迭代器的ExpectedModCount,是否會拋出ConcurrentModificationException異常的實現就是在迭代過程中判斷modCount是否與ExpectedModCount相等。
25     //插入key-value鍵值對,傳入key的hash值、key、value、散列表的插入位置i
26     addEntry(hash, key, value, i);    
27 }                

 

 1 //JDK7,HashMap#addEntry,這個方法是put方法的實現核心,在其中會判斷是否衝突,是否擴容。
 2 void addEntry(int hash, K key, V value, int bucketIndex) {
 3     //第一步判斷就是是否擴容,需要擴容的條件需要滿足以下兩個:1、Map中的key-value的個數大於等於Map的容量threshold(threshold=散列表容量(數組大小)*負載因數)。2、key值所對應的散列表位置不為null。
 4     if ((size >= threshold) && (null != table[bucketIndex])) {
 5         resize(2 * table.length);        //關鍵的擴容機制,擴容後的大小是之前的兩倍
 6         hash = (null != key) ? hash(key) : 0;        //計算key的hash值
 7         bucketIndex = indexFor(hash, table.length);        //重新計算key所在散列表的下標
 8     }
 9     //創建Entry節點並插入,每次插入都會插在鏈表的第一個位置。
10     createEntry(hash, key, value, bucketIndex);    
11 }

  來看看HashMap是如何擴容的。JDK7HashMap擴容的大小是前一次散列表大小的兩倍2 * table.length

void resize(int newCapacity)

  在這個方法中最核心的是transfer(Entry[], boolean)方法,第一個參數表示擴容後新的散列表引用,第二參數表示是否初始化hash種子。

  結合源碼我們用圖例來說明HashMap在JDK7中是如何進行擴容的。

  假設現在有如下HashMap,初始容量initialCapacity=4,負載因數loadFactor=0.5。初始化時閾值threshold=4*0.5=2。也就是說在插入第三個元素時,HashMap中的size=3大於閾值threshold=2,此時就會進行擴容。我們從來兩種情況來對擴容機制進行分析,一種是兩個key-value未產生散列衝突,第二種是兩個key-value產生了散列衝突。

  1. 擴容時,當前HashMap的key-value未產生散列衝突

  此時當插入第三個key-value時,HashMap會進行擴容,容量大小為之前的兩倍,並且在擴容時會對之前的元素進行轉移,未產生衝突的HashMap轉移較為簡單,直接遍歷散列表對key重新計算出新散列表的數組下標即可。

  2. 擴容時,當前HashMap的key-value產生散列衝突

  在對散列衝突了的元素進行擴容轉移時,需要遍歷當前位置的鏈表,鏈表的轉移若新散列表還是衝突則採用頭插法的方式進行插入,此處需要瞭解鏈表的頭插法。同樣通過for (Entry<K,V> e : table)遍歷散列表中的元素,判斷當前元素e是否為null。由例可知,當遍歷到第2個位置的時候元素e不為null。此時創建臨時變數next=e.next。

  重新根據新的散列表計算e的新位置i,後面則開始通過頭插法把元素插入進入新的散列表。

  通過頭插法將A插入進了新散列表的i位置,此時指針通過e=next繼續移動,待插入元素變成了B,如下所示。

  此時會對B元素的key值進行hash運算,計算出它在新散列表中的位置,無論在哪個位置,均是頭插法,假設還是在位置A上產生了衝突,頭插法後則變成瞭如下所示。

  可知,在擴容過程中,鏈表的轉移是關鍵,鏈表的轉移通過頭插法進行插入,所以正是因為頭插法的原因,新散列表衝突的元素位置和舊散列表衝突的元素位置相反。

  關於HashMap的擴容機制還有一個需要註意的地方,在併發條件下,HashMap不僅僅是會造成數據錯誤,致命的是可能會造成CPU100%被占用,原因就是併發條件下,由於HashMap的擴容機制可能會導致死迴圈。下麵將結合圖例說明,為什麼HashMap在併發環境下會造成死迴圈。

  假設在併發環境下,有兩個線程現在都在對同一個HashMap進行擴容。

  此時線程T1對擴容前的HashMap元素已經完成了轉移,但由於Java記憶體模型的緣故線程T2此時看到的還是它自己線程中HashMap之前的變數副本。此時T2對數據進行轉移,如下圖所示。

  進一步地,在T2中的新散列表中newTable[i]指向了元素A,此時待插入節點變成了B,如下圖所示。

  原本在正常情況下,next會指向null,但由於T1已經對A->B鏈表進行了轉置B->A,即next又指回了A,並且B會插入到T2的newTable[i]中。

  由於此時next不為空,下一步又會將next賦值給e,即e = next,反反覆復A、B造成閉環形成死迴圈。

  所以,千萬不要使用在併發環境下使用HashMap,一旦出現死迴圈CPU100%,這個問題不容易復現及排查。併發環境一定需要使用ConcurrentHashMap線程安全類。

  探討了JDK7中的put方法,接下來看看JDK8新增了紅黑樹HashMap是如何進行put,如何進行擴容,以及如何將鏈表轉換為紅黑樹的。

JDK8——HashMap#put

1 //JDK8, HashMap#put
2 public V put(K key, V value) {
3     //在JDK8中,put方法直接調用了putVal方法,該方法有5個參數:key哈希值,key,value,onlyIfAbsent(如果為ture則Map中已經存在該值的時候將不會把value值替換),evict在HashMap中無意義
4     return putVal(hash(key), key, value, false, true);
5 }

  所以關鍵的方法還是putVal。

 1 //JDK8中putVal方法和JDK7中put方法中的插入步驟大致相同,同樣需要判斷是否是第一次插入,插入的位置是否產生衝突,不同的是會判斷插入的節點是“鏈表節點”還是“紅黑色”節點。
 2 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
 3   //1. 是否是第一次插入,是第一次插入則復用resize演算法,對散列表進行初始化
 4   if ((tab = table) == null || (n = tab.length) == 0)
 5     n = (tab = resize()).length;
 6   //2. 通過i = (n - 1) & hash計算key值所在散列表的下標,判斷tab[i]是否已經有元素存在,即有無衝突,沒有則直接插入即可,註意如果插入的key=null,此處和JDK7的策略略有不同,JDK7是遍歷散列表只要為null就直接插入,而JDK8則是始終會插入第一個位置,即使有元素也會形成鏈表
 7   if ((p = tab[i = (n - 1) & hash]) == null)
 8     tab[i] = newNode(hash, key, value, null);
 9   //3. tab[i]已經有了元素即產生了衝突,如果是JDK7則直接使用頭插法即可,但在JDK8中HashMap增加了紅黑樹數據結構,此時有可能已經是紅黑樹結構,或者處在鏈表轉紅黑樹的臨界點,所以此時需要有幾個判斷條件
10   else {
11      //3.1 這是一個特殊判斷,如果tab[i]的元素hash和key都和帶插入的元素相等,則直接覆蓋value值即可
12    if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
13      e = p;
14     //3.2 待插入節點是一個紅黑樹節點
15     else if (p instanceof TreeNode)
16      e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
17    //3.3 插入後可能繼續是一個鏈表,也有可能轉換為紅黑樹。在元素個數超過8個時則會將鏈表轉換為紅黑樹,所以第一個則需要一個計數器來遍歷計算此時tab[i]上的元素個數
18    else {
19      for (int binCount = 0; ; ++binCount) {
20        if ((e = p.next) == null) {
21          p.next = newNode(hash, key, value, null);        //遍歷到當前元素的next指向null,則通過尾插法插入,這也是和JDK7採用頭插法略微不同的地方
22          if (binCount >= TREEIFY_THRESHOLD - 1) // tab[i]的數量超過了臨界值8,此時將會進行鏈表轉紅黑樹的操作,並跳出迴圈
23            treeifyBin(tab, hash);
24            break;
25              }
26              if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))        //這種情況同3.1,出現了和插入key相同的元素,直接跳出迴圈,覆蓋value值即可,無需插入操作
27                 break;
28              p = e;
29      }
30    }
31    if (e != null) {        //這種情況表示帶插入元素的key在Map中已經存在,此時沒有插入操作,直接覆蓋value值即可
32      V oldValue = e.value;
33      if (!onlyIfAbsent || oldValue == null)
34        e.value = value;
35      afterNodeAccess(e);
36      return oldValue;
37    }
38  }
39  ++modCount;        //修改計數,在使用Iterator迭代器時會和這個變數比較,如果不相等,則會拋出ConcurrentModificationException異常
40  if (++size > threshold)    //判斷是否需要擴容
41    resize();
42  afterNodeInsertion(evict);        //並無意義
43  return null;
44 }

 

  從上面的JDK7和JDK8的put插入方法源碼分析來看,JDK8確實複雜了不少,在沒有耐心的情況下,這個“乾貨”確實顯得比較乾,我試著用下列圖解的方式回顧JDK7和JDK8的插入過程,在對比過後接著對JDK8中的紅黑樹插入、鏈表轉紅黑樹以及擴容作分析。

  綜上JDK7和JDK8的put插入方法大體上相同,其核心均是計算key的hash並通過hash計算散列表的下標,再判斷是否產生衝突。只是在實現細節上略有區別,例如JDK7會對key=null做特殊處理,而JDK8則始終會放置在第0個位置;而JDK7在產生衝突時會使用頭插法進行插入,而JDK8在鏈表結構時會採用尾插法進行插入;當然最大的不同還是JDK8對節點的判斷分為了:鏈表節點、紅黑樹節點、鏈表轉換紅黑樹臨界節點。

  對於紅黑樹的插入暫時不做分析,接下來是對JDK8擴容方法的分析。

 1 // JDK8,HashMap#resize擴容,HashMap擴容的大小仍然是前一次散列表大小的兩倍 
 2 final Node<K,V>[] resize() {
 3  //1. 由於JDK8初始化散列表時復用了resize方法,所以前面是對oldTab的判斷,是否為0(表示是初始化),是否已經大於等於了最大容量。判斷結束後newTab會擴大為oldTab的兩倍,同樣newThr(閾值)也是以前的兩倍。源碼略。
 4  //2. 確定好newTab的大小後接下來就是初始化newTab散列表數組
 5  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 6  table = newTab;
 7  //3. 如果是初始化(即oldTab==null),則直接返回新的散列表數組,不是則進行轉移
 8  //4. 首先還是遍歷散列表
 9  for (int j = 0; j < oldCap; ++j) {
10  //5. e = oldCap[i] != null,則繼續判斷
11    //5.1 當前位置i,是否有衝突,沒有則直接轉移
12    if (e.next == null)
13      newTab[e.hash & (newCap - 1)] = e;    //這裡並沒有對要轉移的元素重新計算hash,對於JDK7來會通過hash(e.getKey()) ^ newCap重新計算e在newTab中的位置,此處則是e.hash & (newCap - 1),減少了重新計算hash的過程。擴容後的位置要麼在原來的位置上,要麼在原索引 + oldCap位置 
14    //5.2 判斷是否是紅黑樹節點
15    else if (e instanceof TreeNode)
16      ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
17    //5.3 判斷是否是鏈表節點
18    else {
19     20    }
21   }
22 }

 

  JDK8的擴容機制相比較於JDK7除了增加對節點是否為紅黑樹的判斷,其餘大致相同,只是做了一些微小的優化。特別在於在JDK8中並不會重新計算key的hash值。

public V remove(Object key)

  如果已經非常清楚put過程,我相信對於HashMap中的其他方法也基本能知道套路。remove刪除也不例外,計算hash(key)以及所在散列表的位置i,判斷i是否有元素,元素是否是紅黑樹還是鏈表。

  這個方法容易陷入的陷阱是key值是一個自定義的pojo類,且並沒有重寫equals和hashCode方法,此時用pojo作為key值進行刪除,很有可能出現“刪不掉”的情況。這需要重寫equals和hashCode才能使得兩個pojo對象“相等”。

  剩下的方法思路大同小異,基本均是計算hash、計算散列表下標i、遍歷、判斷節點類型等等。本文在弄清put和resize方法後,一切方法基本上都能舉一反三。所以在看完本文後,你應該試著問自己以下幾個問題:

  1. HashMap的底層數據結構是什麼?
  2. HashMap的put過程?
  3. HashMap的擴容機制?
  4. 併發環境下HashMap會帶來什麼致命問題?

 

 

這是一個能給程式員加buff的公眾號 


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

-Advertisement-
Play Games
更多相關文章
  • 什麼是字典 字典是另一種可變容器模型,且可存儲任意類型對象。 字典的每個鍵值 key=>value 對用冒號 : 分割,每個鍵值對之間用逗號 , 分割,整個字典包括在花括弧 {} 中 鍵必須是唯一的,但值則不必。 值可以取任何數據類型,但鍵必須是不可變的,如字元串,數字或元組。 示例: 創建字典 方 ...
  • 1.學習Java應用程式是如何工作的 2.構成一個應用程式 3.嚮應用程式傳遞參數 4.學習Java程式是如何組織的 5.在應用程式中創建一個對象 程式Root:輸出225的正平方根 1 package com.jsample;//應用程式位於jsample包中 2 3 public class R ...
  • 1、面向對象思想 (1)面向過程思想:面向著具體的每一個步驟和過程,把每一個步驟和過程完成,然後由這些功能方法相互調用,完成需求。強調的是過程,所有事情都需要自己完成。 (2)面向對象思想:就是不斷的創建對象,使用對象,指揮對象做事情。是一種更符合我們思想習慣的思想(懶人思想,我把事情自己不做,交給 ...
  • 原創聲明:轉載請指明原文出處: http://www.cnblogs.com/schoolbag/diary/2018/03/13/8563019.html 題目:通過子線程讀取每個文件,並統計單詞數,將單詞數返回給主線程相加得出總單詞數 ...
  • Description 給定一顆二叉樹,要求輸出二叉樹的深度以及後序遍歷二叉樹得到的序列。本題假設二叉樹的結點數不超過1000。 Input 輸入數據分為多組,第一行是測試數據的組數n,下麵的n行分別代表一棵二叉樹。每棵二叉樹的結點均為正整數,數據為0代表當前結點為空,數據為-1代表二叉樹數據輸入結 ...
  • I/O處理小練習--保存用戶賬號密碼 用戶輸入姓名和密碼,將每一個姓名和密碼保存到文件中,輸入done時程式結束 需要註意的幾個的地方: 1.FileOutputStream一定要放在迴圈之外,否則這個文件的內容會一直覆蓋,最後什麼也存不到 2.判定done的時候,一定要規定長度,要不然這一判斷條件 ...
  • Description 給定一顆二叉樹,要求輸出二叉樹的深度以及先序遍歷二叉樹得到的序列。本題假設二叉樹的結點數不超過1000。 Input 輸入數據分為多組,第一行是測試數據的組數n,下麵的n行分別代表一棵二叉樹。每棵二叉樹的結點均為正整數,數據為0代表當前結點為空,數據為-1代表二叉樹數據輸入結 ...
  • 1.安裝Flask SQLAlchemy,pymyql驅動 2.使用Flask SQLAlchemy Flask SQLAlchemy資料庫URL 常用的資料庫引擎: 資料庫引擎 | URL | MySql | mysql+pymysql://username:password@host/datab ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...