位元組同學推薦_編寫高質量Objective-C程式碼的52個有效方法

語言: CN / TW / HK

我正在參加「掘金·啟航計劃」

-- 此文獻給一位深夜奮戰在一線的位元組跳動面試官 :)

緣起: 2017年10月17日, 用Kindle3拜讀完 Matt Galloway的<>即<<編寫高質量iOS與 OS X程式碼的52個有效方法>>後就在簡書上整理了這52個有效的Tips, 至今已近5年時間. 今日有幸跟一位位元組的小夥伴暢聊技術, 很是開心. 最後讓這位奮戰在一線的位元組跳動面試官同學安利一本對他的技術發展影響較大的書, 他遂推薦了這本. 面對簡書這篇帖子5年一共218的閱讀量. 不禁一起感嘆一本樸實的好書不應該知道的人那麼少. OC真的被拋棄了嗎? 拋棄OC的成本比想象的高! OC作為一個久經考驗並支援iPhone改變世界的語言, 依然有著頑強的生命力. 所以再次在掘金, 這個我認為目前最好的中文技術社群之一, 釋出此文. 與君共勉, 作為一個普通的MOP開發Coder, 有空讀讀這52個接地氣方法, 真的很有效~ 簡書原文連結

image.png

第1章 熟悉Objective-C

  1. 瞭解Objective-C語言的起源
  2. Objective-C為C語言添加了面向物件特性,是其超集.Objective-C使用動態繫結的訊息結構,也就是說,在執行時才會檢查物件型別.接收一條訊息之後,究竟應執行何種程式碼,由執行期環境而非編譯器來決定.
  3. 理解C語言的核心概念有助於寫好Objective-C程式.尤其要掌握記憶體模型與指標.
  4. 在類的標頭檔案中儘量少引入其他標頭檔案
  5. 除非確有必要,否則不要引入標頭檔案.一般來說,應該在某個類的標頭檔案中使用向前宣告來提及別的類,並在實現檔案中引入那些類的標頭檔案.這樣做可以儘量降低類之間的耦合(coupling).
  6. 有時無法使用向前宣告,比如要宣告某個類遵循一項協議.這種情況下,儘量把"該類遵循某協議"的這條宣告移至"class-continuation分類中".如果不行的話,就把協議單獨放在一個頭檔案中,然後將其引入.
  7. 多用字面量語法,少用與之等價的方法
  8. 應該使用字面量語法來建立字串、數值、陣列、字典.與建立此類物件的常規方法相比,這麼做更加簡明扼要.
  9. 應該通過取下標操作來訪問陣列下標或字典中的鍵所對應的元素.
  10. 用字面量語法建立陣列或字典時,若值中有nil,則會丟擲異常.因此,務必確保值裡不含nil.
  11. 多用型別常量,少用#define預處理指令
  12. 不要用預處理指令定義常量.這樣定義出來的常量不含型別資訊,編譯器只是會在編譯前據此執行查詢與替換操作.即使有人重新定義了常量值.編譯器也不會產生警告資訊,這將導致應用程式中的常量值不一致.
  13. 在實現檔案中使用static const來定義"只在編譯單元內可見的常量"(translation-unit-specific constant).由於此類常量不會再全域性符號中表示,所以無需為其名稱加字首.
  14. 在標頭檔案中使用extern來宣告全域性常量,並在相關實現檔案中定義其值.這種常量要出現在全域性符號表中,所以其名稱應加以區隔,通常用與之相關的類名做字首.
  15. 用列舉表示狀態、選項、狀態碼
  16. 應該用列舉來表示狀態機的狀態、傳遞給方法的選項以及狀態碼等值,給這些值起個易懂的名字.
  17. 如果把傳遞給某個方法的選項表示為列舉型別,而多個選項又可同時使用,那麼就將各選項值定義為2的冪,以便通過按位或操作將其組合起來.
  18. 用NS_ENUM與NS_OPTION巨集來定義列舉型別,並指明其底層資料型別.這樣做可以確保列舉是用開發者所選的底層資料型別實現出來的,而不會採用編譯器所選的型別.
  19. 在處理列舉型別的switch語句中不要實現default分支.這樣的話,加入新列舉之後,編譯器就會提示開發者:switch語句並未處理所有列舉.

第2章 物件、訊息、執行期

  1. 理解"屬性"這一概念
  2. 可以用@property語法來定義物件中所封裝的資料.
  3. 通過"特質"來指定儲存資料所需的正確語義.
  4. 在設定屬性所對應的例項變數時,一定要遵從該屬性所宣告的語義.
  5. 開發iOS程式時應該使用nonatomic屬性,因為atomic屬性會嚴重影響效能.
  6. 在物件內部儘量直接訪問例項變數
  7. 在物件內部讀取資料時,應該直接通過例項變數來讀,而寫入資料時,則應通過屬性來寫.
  8. 在初始化方法及dealloc方法中,總是應該直接通過例項變數來讀寫資料.
  9. 有時會使用惰性初始化技術配置某份資料,這種情況下,需要通過屬性來讀取資料.
  10. 理解"物件等同性"這一概念
  11. 若想檢測物件的等同性,請提供"isEqual:"與hash方法.
  12. 相同的物件必須具有相同的雜湊碼,但是兩個雜湊碼相同的物件卻未必相同.
  13. 不要盲目地逐個監測每條屬性,而是應該依照具體需求來制定檢測方法.
  14. 編寫hash方法時,應該使用計算速度快而且雜湊碼碰撞機率低的演算法.
  15. 以"類族模式"隱藏實現細節
  16. 類族模式可以把實現細節隱藏在一套簡單的公共介面後面.
  17. 系統框架中經常使用類族.
  18. 從類族的公共抽象基類中繼承子類時要當心,若有開發文件,則應首先閱讀.
  19. 在既有類中使用關聯物件存放自定義資料
  20. 可以通過"關聯物件"機制來把兩個物件連起來.
  21. 定義關聯物件時可指定記憶體管理語義,用以模仿定義屬性時所採用的"擁有關係"與"非擁有關係".
  22. 只有在其他做法不可行時才應選用關聯物件,因為這種做法通常會引入難於查詢的bug.
  23. 理解 objec_msgSend的作用
  24. 訊息由接收者、選擇子及引數構成.給某物件"傳送訊息"(invoke a message)也就相當於在該物件上"呼叫方法"(call a method).
  25. 發給某物件的全部訊息都要由"動態訊息派發系統"(dynamic message dispatch system)來處理,該系統會查出對應的方法,並執行其程式碼
  26. 理解訊息轉發機制
  27. 若物件無法響應某個選擇子,則進入訊息轉發流程.
  28. 通過執行期的動態方法解析功能,我們可以在需要用到某個方法時再將其加入類中.
  29. 物件可以把其無法解讀的某些選擇子轉交給其他物件來處理.
  30. 經過上述兩步之後,如果還是沒辦法處理選擇子,那就啟動完整的訊息轉發機制.
  31. 用"方法調配技術"除錯"黑盒方法"
  32. 在執行期,可以向類中新增或替換選擇子所對應的方法實現.
  33. 使用另一份實現來替換原有的方法實現,這道工序叫做"方法調配",開發者常用此技術向原有實現中新增新功能.
  34. 一般來說,只有除錯程式的時候才需要在執行期修改方法實現,這種做法不宜濫用.
  35. 理解"類物件"的用意
  36. 每個例項都有一個指向Class物件的指標,用以表明其型別,而這些Class物件則構成了類的繼承體系.
  37. 如果物件的型別無法在編譯期確定,那麼就應該使用型別資訊查詢方法來探知.
  38. 儘量使用型別資訊查詢方法來確定物件型別.而不要直接比較類物件,因為某些物件可能實現了訊息轉發功能.

第3章 介面與API設計

  1. 用字首避免名稱空間衝突
  2. 選擇與你的公司、應用程式或二者皆有關聯之名稱作為類名的字首,並在所有程式碼中均使用這一字首.
  3. 若自己所開發的應用程式庫中用到了第三方庫,則應為其中的名稱加上字首.
  4. 提供"全能初始化方法"
  5. 在類中提供一個全能初始化方法,並於文件裡指明.其他初始化方法均應呼叫此方法.
  6. 若全能初始化方法與超類不同,則需覆寫超類中的對應方法.
  7. 如果超類的初始化方法不適用於子類,那麼應該覆寫這個超類方法,並在其中丟擲異常.
  8. 實現description方法
  9. 實現description方法返回一個有意義的字串,用以描述該例項.
  10. 若想在除錯時打印出更詳盡的物件描述資訊,則應實現debugDescription方法.
  11. 儘量使用不可變物件
  12. 儘量建立不可變的物件.
  13. 若某個屬性僅可於物件內部修改,則在"class-continuation分類"中將其由readonly屬性擴充套件為readwrite屬性.
  14. 不要把可變的collection作為屬性公開,而應提供相關方法,以此修改物件中的可變collection.
  15. 使用清晰而協調的命名方式
  16. 起名時應遵從標準的Objective-C命名規範,這樣創建出來的介面更容易為開發者所理解.
  17. 方法名要言簡意賅,從左至右讀起來要像個日常用語中的句子才好.
  18. 方法名裡不要使用縮略後的型別名稱.
  19. 給方法其名時的第一要務就是確保其風格於你自己的程式碼所要整合的框架相符.
  20. 為私有方法名加字首
  21. 給私有方法的名稱加上字首,這樣可以很容易地將其同公共方法區分開來.
  22. 不要單用一個下劃線做私有方法的字首,因為這種做法是預留給蘋果公司用的.
  23. 理解Objective-C錯誤模型
  24. 只有發生了可使整個應用程式崩潰的嚴重錯誤時,才應使用異常.
  25. 在錯誤不那麼嚴重的情況下,可以指派"委託方法"(delegate method)來處理錯誤,也可以把錯誤資訊放在NSError物件裡,經由"輸出引數"返回給呼叫者.
  26. 理解NSCopying協議
  27. 若想令自己所寫的物件具有拷貝功能,則需實現NSCopying協議.
  28. 如果自定義的物件分為可變版本,那麼就要同時實現NSCopying與NSMutableArray協議.
  29. 複製物件時需決定採用淺拷貝還是深拷貝,一般情況下應該儘量執行淺拷貝.
  30. 如果你所寫的物件需要深拷貝,那麼可考慮新增一個專門執行深拷貝的方法.

第4章 協議與分類

  1. 通過委託與資料來源協議進行物件間通訊要點
  2. 委託模式為物件提供了一套介面,使其可由此將相關事件告知其他物件.
  3. 將委託物件應該支援的介面定義成協議,在協議中把可能需要處理的事件定義成方法.
  4. 當某物件需要從另一個物件中獲取資料時,可以使用委託模式.這種情景下,該模式亦稱"資料來源協議"(data source protocal).
  5. 若有必要,可實現含有位段的結構體,將委託物件是否能響應相關協議方法這一資訊快取至其中.
  6. 將類的實現程式碼分散到便於管理的數個分類之中
  7. 使用分類機制把類的實現程式碼劃分成易於管理的小塊.
  8. 將應該視為"私有"的方法歸入名叫Private的分類中,以隱藏實現細節.
  9. 總是為第三方類的分類名稱加字首
  10. 向第三方類中新增分類時,總應給其名稱加上你專用的字首.
  11. 向第三方類中新增分類時,總應給其中方法名加上你專用的字首.
  12. 勿在分類中宣告屬性
  13. 把封裝資料所用的全部屬性都定義在主接口裡.
  14. 在"class-continuation分類"之外的其他分類中,可以定義存取方法,但儘量不要定義屬性(屬性是用來封裝資料的).
  15. 使用"class-continuation分類"隱藏實現細節
  16. 通過"class-continuation分類"向類中新增例項變數.
  17. 如果某屬性在主介面中宣告為"只讀",而類的內部又要用設定方法修改此屬性,那麼就在"class-continuation分類"中將其擴充套件為"可讀寫".
  18. 把私有方法的原型宣告在"class-continuation分類"裡面.
  19. 若想使類所遵循的協議不為人知,則可於"class-continuation分類"中宣告.
  20. 通過協議提供匿名物件
  21. 協議可在某種程度上提供匿名型別.具體的物件型別可以淡化成遵從某協議的id型別,協議裡規定了物件所應實現的方法.
  22. 使用匿名物件來隱藏型別名稱(或類名).
  23. 如果具體型別不重要,重要的是物件能夠響應(定義在協議裡的)特定方法,那麼可使用匿名物件來表示.

第5章 記憶體管理

  1. 理解引用計數
  2. 引用計數機制通過可以遞增遞減的計數器來管理記憶體.物件建立好之後,其保留計數至少為1.若保留計數為正,則物件繼續存活.當保留計數降為0時,物件就被銷燬了.
  3. 在物件生命週期中,其餘物件通過引用來保留或釋放此物件.保留與釋放操作分別會遞增及遞減保留計數.
  4. 以ARC簡化引用計數 ps:實際上,ARC在呼叫(retain、release、autorelease、dealloc)這些方法時.並不通過普通的Objective-C訊息派發機制,而是直接呼叫其C語言版本.這樣做效能更好,因為保留及釋放操作需要頻繁執行,所以直接呼叫底層函式能節省很多CPU週期.比方說,ARC會呼叫與retain等價的底層函式,objc_retain.這也是不能複寫retain、release或autorelease的緣由,因為這些方法從來不會被直接呼叫.
  5. 有ARC之後,程式設計師就無須(必須,不是需要)擔心記憶體管理問題了.使用ARC來程式設計,可省去類中的"樣板程式碼".
  6. ARC管理物件生命週期的辦法基本上就算是:在合適的地方插入"保留"及"釋放"操作.在ARC環境下,變數的記憶體管理語義可以通過修飾符指明,而原來則需要手工執行"保留"及"釋放"操作.
  7. 有方法所返回的物件,其記憶體管理語義總是通過方法名來體現.ARC將此確定為開發者必須遵守的規則.
  8. ARC只負責管理Objective-C物件的記憶體.尤其要注意:CoreFoundation物件不歸ARC管理,開發者必須適時呼叫CFretain/CFRelease.
  9. 在dealloc方法中只釋放引用並解除監聽
  10. 在dealloc方法裡,應該做的事情就是釋放指向其他物件的引用,並取消原來訂閱的"鍵值觀察"(KVO)或NSNotificationCenter等通知,不要做其他事情.
  11. 如果物件持有檔案描述符等系統資源,那麼應該專門編寫一個方法來釋放此種資源.這樣的類要和其使用者約定:用完資源後必須呼叫close方法.
  12. 執行非同步任務的方法不應再dealloc裡呼叫;只能在正常狀態下執行的那些方法也不應在dealloc裡呼叫,因為此時物件已處於正在回收的狀態了.
  13. 編寫"異常安全程式碼"時留意記憶體管理問題
  14. 捕獲異常時,一定要注意將try塊內所創立的物件清理乾淨.
  15. 在預設情況下,ARC不生成安全處理異常所需的清理程式碼.開啟編譯器標誌後,可生成這種程式碼,不過會導致應用程式變大,而且會降低執行效率.
  16. 以弱引用避免保留環
  17. 將某些引用設為weak,可避免出現"保留環".
  18. weak引用可以自動清空,也可以不自動清空.自動清空(autonilling)是隨著ARC而引入的新特性,由執行期系統來實現.在具備自動清空功能的弱引用上,可以隨意讀取其資料,因為這種引用不會指向已回收過的物件.
  19. 以"自動釋放池塊"降低記憶體峰值
  20. 自動釋放池排布在棧中,物件收到aurorelease訊息後,系統將其放入最頂端的池裡.
  21. 合理運用自動釋放池,可降低應用程式的記憶體峰值.
  22. @autoreleasepool這種新式寫法能創建出更為輕便的自動釋放池.
  23. 用"殭屍物件"除錯記憶體管理問題
  24. 系統在回收物件時,可以不將其真的回收,而是把它轉化為殭屍物件.通過環境變數NSZombieEnabled可開啟此功能.
  25. 系統會修改物件的isa指標,令其指向特殊的殭屍類,從而使該物件變為殭屍物件.殭屍類能夠響應所有的選擇子,響應方式為:列印一條包含訊息內容及其接收者的訊息,然後終止應用程式.
  26. 不要使用retainCount
  27. 物件的保留計數看似有用,實則不然,因為任何給定時間點上的"絕對保留技術"(absolute retain count)都無法反映物件生命期的全貌.
  28. 引入ARC之後,retainCount方法就正式廢止了,在ARC下呼叫該方法會導致編譯器報錯.

## 第6章 塊與大中樞派發 37. 理解"塊"這一概念 - 塊是C、C++、Objective-C中的詞法閉包. - 塊可接受引數,也可以返回值. - 塊可以分配在棧或堆上,也可以是全域性的.分配在棧上的塊可拷貝到堆裡,這樣的話,就和標準的Objective-C物件一樣,具備引用計數了. 38. 為常用的塊型別建立typedef - 以typedef重新定義塊型別,可令塊變數用起來更加簡單. - 定義新型別時應遵從現有的命名習慣,勿使其名稱與別的型別相沖突. - 不妨為同一個塊簽名定義多個類型別名.如果要重構的程式碼使用了塊型別的某個別名,那麼只要修改相應的typedef中的塊簽名即可,無須改動其他typedef. 39. 用handler塊降低程式碼分散程度 - 在建立物件時,可以使用內聯的handler塊將相關業務邏輯一併宣告. - 在有多個例項需要監控時,如果採用委託模式,那麼經常需要根據傳入的物件來切換,而若改用handler塊來實現,則可直接將塊與相關物件放在一起. - 設計API時如果用到了handler塊,那麼可以增加一個引數,使呼叫者可通過此引數來決定應該把塊安排在哪個佇列上執行. 40. 用塊引用其所屬物件時不要出現保留環 - 如果塊所捕獲的物件直接或間接地保留了塊本身,那麼就得當心保留環問題. - 一定要找個適當的時機解除保留環,而不能把責任推給API的呼叫者. 41. 多用派發佇列,少用同步鎖 - 派發佇列可用來表述同步語義(synchronization sematic),這種做法要比@synchronized塊或NSLock物件更簡單. - 將同步與非同步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這麼做卻不會阻塞執行非同步派發的執行緒. - 使用同步佇列及柵欄塊,可以令同步行為更加高效. 42. 多用GCD、少用performSelector系列方法 - performSelector系列方法在記憶體管理方面容易有疏失.它無法確定將要執行的選擇子具體是什麼,因而ARC編譯器也就無法插入適當的記憶體管理方法. - performSelector系列方法所能處理的選擇子太過於侷限了,選擇子的返回值型別及傳送給方法的引數個數都受到限制. - 如果想把任務放在另一個執行緒上執行,那麼最好不要用performSelector系列方法,而是應該把任務封裝到塊裡,然後呼叫大中樞派發機制的相關方法來實現. 43. 掌握GCD及操作佇列的使用時機 - 在解決多執行緒與任務管理問題時,派發佇列並非唯一方案. - 操作佇列提供了一套高層的Objective-C API,能實現純GCD所具備的絕大部分功能,而且還能完成更為複雜的操作,哪些操作若改用GCD來實現,則需另外編寫程式碼. 44. 通過Dispatch Group機制,根據系統資源狀況來執行任務 (在併發佇列中,執行任務所用的的併發執行緒數量,取決於各種因素,而GCD主要是根據系統資源狀況來判定這些因素的.) - 一系列任務可歸入一個dispatch group之中.開發者可以在這組任務執行完畢時獲得通知. - 通過dispatch group,可以在併發式派發佇列裡同時執行多項任務.此時GCD會根據系統資源狀況來排程這些併發執行的任務.開發者若自己來實現此功能,則需要編寫大量程式碼. 45. 使用dispatch_once來執行只需執行一次的執行緒安全程式碼 - 經常需要編寫"只需要執行一次的執行緒安全程式碼"(thread-safe single-code execution).通過GCD所提供的dispatch_once函式,很容易就能實現此功能. - 標記應該宣告在static或global作用域中,這樣的話,在把只需執行一次的塊傳給dispatch_once函式時,傳進去的標記也是相同的. 46. 不要使用dispatch_get_current_queue - dispatch_get_current_queue函式的行為常常與開發者所預期的不同.此函式已經廢棄,只應做除錯之用. - 由於派發佇列是按層級來組織的,所以無法單用某個佇列物件來描述"當前佇列"這一概念. - dispatch_get_current_queue函式用於解決由不可重入的程式碼所引發的死鎖,然而能用此函式解決的問題,通常也能改用"佇列特定資料"來解決.

第7章 系統框架

  1. 熟悉系統框架
  2. 許多系統框架都可以直接使用.其中最重要的是Foundation與CoreFoundation,這兩個框架提供了構建應用程式所需的許多核心功能.
  3. 很多常見任務都能用框架來做,例如音訊與影片處理、網路通訊、資料管理等.
  4. 請記住:用純C寫成的框架與用Objective-C寫成的一樣重要,若想成為優秀的Objective-C開發者,應該掌握C語言的核心概念.
  5. 多用塊列舉,少用for迴圈
  6. 遍歷collection有四種方式.最基本的辦法是for迴圈,其次是NSEnumenrator遍歷法及快速遍歷法,最新、最先進的方式則是"塊列舉法".
  7. "塊列舉法",本身就能通過GCD來併發執行遍歷操作,無須另行編寫程式碼.而採用其他便利方式則無法輕易實現這一點.
  8. 若提前知道待遍歷的collection含有何種物件,則應該修改塊簽名,指出物件的具體型別.
  9. 對自定義其記憶體管理語義的collection使用無縫橋接 ps:在使用Foundation框架中的字典物件是會遇到一個大問題,那就是其鍵的記憶體管理語義為"拷貝",而值得語義卻是"保留".除非使用強大的無縫橋接技術,否則無法改變其語義.
  10. 通過無縫橋接技術,可以在Foundation框架中的Objective-C物件與CoreFoundation框架中的C語言結構之間來回轉換.
  11. 在CoreFoundation層面建立collection時,可以指定許多回調函式,這些函式表示此collection應如何處理其元素.然後,可以運用無縫橋接技術,將其轉換成具備特殊記憶體管理語義的Objective-C collection.
  12. 構建快取是選用NSCache而非NSDicitionary
  13. 實現快取時應選用NSCache而非NSDictionary物件.因為NSCache可以提供優雅的自動刪減功能,而且是"執行緒安全的",此外,它與字典不同,並不會拷貝鍵.
  14. 可以給NSCache物件設定上限,用以限制快取中的物件總個數及"總成本",而這些尺度則定義了快取刪減其中物件的時機.但是絕對不要把這些尺度當成可靠的"硬限制"(hard limit),它們僅對NSCache起指導作用.
  15. 將NSPurgeableData與NSCache搭配使用,可實現自動清除資料的功能,也就是說,當NSPurgeableData物件所佔用記憶體為系統所丟棄時,該物件自身也會從快取中移除.
  16. 如果快取使用得當,那麼應用程式的響應速度就能提高.只有那種"重新計算起來很費事的"資料,才值得放入快取,比如那些需要從網路獲取或從硬碟讀取的資料.
  17. 精簡initialize與load的實現程式碼 ps:整數可以在編譯期定義,然而可變陣列不行,因為它是個Objective-C物件,所以建立例項之前必須先啟用執行期系統.注意,某些Objective-C物件也可以在編譯期建立,例如NSString例項.然而建立下面這種物件會令編譯器報錯: static NSMutableArray * kSomeObjects = [NSMutableArray new];
  18. 在載入階段,如果實現了load方法,那麼系統就會呼叫它.分類裡也可以定義此方法,類的load方法要比分類中的先呼叫.與其他方法不同,load方法不參與覆寫機制.
  19. 首次使用某個類之前,系統會想起傳送initialeze訊息.由於此方法遵從普通的覆寫規則,所以通常應該在裡面判斷當前要初始化的是哪個類.
  20. load與initialize方法都應該實現得精簡一些,這有助於保持應用程式的響應能力,也能減少引入"依賴環"(interdependency cycle)的機率.
  21. 無法在編譯期設定的全域性變數,可以放在initialize方法裡初始化.
  22. 別忘了NSTimer會保留其目標物件
  23. NSTimer物件會保留其目標,直到計時器本身失效為止,呼叫invalidate方法可令計時器失效,另外,一次性的計時器在觸發完任務之後也會消失.
  24. 反覆執行任務的計時器(repeating timer),很容易引入保留環,如果這種計時器的目標物件又保留了計時器本身,那肯定會導致保留環.這種環狀保留關係,可能是直接發生的,也可能是通過物件圖裡的其他物件間接發生的.
  25. 可以擴充NSTimer的功能,用"塊"來打破保留環.不過,除非NSTimer將來在公共接口裡提供此功能,否則必須建立分類,將相關實現程式碼加入其中.

PS:讀書時做的純手打筆記,如有錯誤,歡迎指正.

發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~

ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~