鍍金池/ 教程/ iOS/ Square Register 中的擴(kuò)張
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計 App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

Square Register 中的擴(kuò)張

Square Register 的擴(kuò)張

Square Register 過去 6 年的歷史中,代碼庫和公司都發(fā)生了顯著的變化,由于應(yīng)用程序已經(jīng)從一個簡單的刷卡終端成長為一個全功能銷售終端 (point-of-sale) 系統(tǒng)。公司已經(jīng)從 10 人發(fā)展到 1000 多人,而我們不得不迅速擴(kuò)張。下面是一些我們在前進(jìn)的道路上實現(xiàn)的流程和已經(jīng)學(xué)到的東西。

團(tuán)隊

隨著我們的成長,我們意識到,一旦開發(fā)團(tuán)隊達(dá)到一定規(guī)模,按平臺來組織團(tuán)隊會很低效。相反,我們用“全?!眻F(tuán)隊來負(fù)責(zé)應(yīng)用程序中的特定功能集。這些團(tuán)隊包括 iOS 工程師,Android 工程師和服務(wù)器的工程師。這給了團(tuán)隊更多的自由來集中精力去創(chuàng)造一個更深入,更全面的產(chǎn)品。我們圍繞著餐廳,零售商店,國際化支持,硬件和核心部件 (僅舉幾例),組建了面向功能的團(tuán)隊。當(dāng)團(tuán)隊擁有了足夠的縱向所有權(quán),就為工程師們做出更全面的技術(shù)決定留出了可能,并讓他們擁有了對產(chǎn)品確定的歸屬感。

我們的發(fā)布流程

2014 年之前,Register 的發(fā)布還遵循瀑布模型;我們規(guī)劃一個大的功能集,然后設(shè)定一個未來的最終日期 (三至六個月),然后努力實現(xiàn)這些功能。

這個流程沒法很好地擴(kuò)展。當(dāng)我們?yōu)楫a(chǎn)品增加功能和工程師時瀑布模型變得費力和緩慢。由于發(fā)布版本的所有開發(fā)功能必須一起發(fā)布,某一個功能的延遲或有問題會耽誤整個發(fā)布。為了確保團(tuán)隊持續(xù)保持自主性,我們找到了一個不同的,更有效的方法。

都登上發(fā)布的列車

為了保持高效率,我們總是希望確保我們的流程符合我們的規(guī)模。從 2014 年開始,我們引進(jìn)了一個由 “發(fā)布列車” 組成的新模式。發(fā)布列車優(yōu)化了功能團(tuán)隊的自主權(quán),同時支持持續(xù)發(fā)布。這意味著單個功能可以在它們準(zhǔn)備好的時候就被發(fā)布,而不必等待其他工作的完成。

切換到發(fā)布列車需要改變我們的工作流程:

  • 增量開發(fā) - 功能是逐步發(fā)展的,而不是長期的被隔離的功能分支。
  • 隔離和安全 - 新功能后面都有一個服務(wù)器控制的標(biāo)志。該功能標(biāo)志在開發(fā)環(huán)境之外是不被啟用的,直到準(zhǔn)備發(fā)布之時。
  • 無回退 - 如果某個改變在現(xiàn)有的功能上導(dǎo)致了回退,那這個回退必須被立即修復(fù)。
  • 制定一個時限 - 團(tuán)隊在一個發(fā)布期限內(nèi)可以包含兩到三個功能而不是在某個特定版本僅發(fā)布一個功能。

這意味著,我們的主分支保持在一個穩(wěn)定的狀態(tài)。這就是所說的列車的一部分。

  1. 分支 - 在每個月的月初,會從主分支創(chuàng)建一個發(fā)布分支。
  2. 上車 - 如果一個功能準(zhǔn)備發(fā)布 (基本已經(jīng)沒有問題),它的功能標(biāo)志被啟用。如果不是,它必須等待下一班列車。
  3. 測試和修復(fù) - 每個月的其余時間花費在對發(fā)布分支的錯誤修正上。如果一個團(tuán)隊不需要發(fā)布任何東西,它會繼續(xù)在主分支上工作。
  4. 合并 - 發(fā)布分支上的變化會不斷合并回主分支。
  5. 發(fā)布 - 在月底我們會把發(fā)布分支發(fā)布到 App Store。
  6. 重復(fù) - 之后的每月重復(fù)上述過程。

這有很多的好處:

  • 每個版本之間不會有超過一個月的代碼變更,導(dǎo)致錯誤更少。
  • 只有 bug 修復(fù)會進(jìn)入列車的發(fā)布分支。這意味著更長的“烘焙時間”來證明改變是穩(wěn)定的。
  • 發(fā)布 bug 修復(fù)版本的需求減少了;因為大多數(shù) bug 的修復(fù)都可以等到下一次列車。
  • 通過逐步建立和合并功能,避免了可能會破壞主分支的破壞性大合并。
  • 主分支是不接受回退或高優(yōu)先級的錯誤的。修復(fù)這些是團(tuán)隊的最高優(yōu)先級。
  • 在某個具體日期發(fā)布的壓力減少了。下一個版本不必等待數(shù)月,功能可以在下個月的發(fā)布中就包含進(jìn)去。這意味著團(tuán)隊不需要急著趕工。他們只需在他們對功能的代碼質(zhì)量有信心的時候發(fā)布。這提高了團(tuán)隊的工作效率,士氣和代碼質(zhì)量。

在 2015 年年初,我們對這個流程做了進(jìn)一步改進(jìn):現(xiàn)在發(fā)布分支被按兩個星期的時間為間隔分割和發(fā)布。這意味著團(tuán)隊在今年將有 26 次發(fā)布的機(jī)會。相比 2013 年及更早的每一年只有三個或四個發(fā)布版本而言,這是一個巨大的勝利。更多的發(fā)布機(jī)會意味著交付給客戶更多的功能。

我們的開發(fā)進(jìn)程

Square 的商人依靠 Register 來經(jīng)營生意。因此,它在任何時候都必須是可靠的。我們有嚴(yán)格的流程,以確保設(shè)計、實裝和測試階段的質(zhì)量。

大型工程變更需要設(shè)計和評審

“寫下來是讓你知道你的思維有多草率的最自然的方式” - Guindon

這是我最喜歡的一句話之一,而且它也適用于軟件構(gòu)建!如果你只是在你的頭腦里構(gòu)建軟件的話,該軟件將是有缺陷的。在你腦袋里的形象是很模糊和短暫的;它總是持續(xù)變化的,因此需要寫下來加以澄清和完善。

Square 里每一個大的變化,都要經(jīng)過工程設(shè)計審查。如果你以前從來沒有做過,這聽起來會有點嚇人,但它其實很簡單!這個過程通常需要編寫以下的設(shè)計文檔:

  • 目標(biāo) - 你想實現(xiàn)什么?這個變化對客戶有什么影響?
  • 非目標(biāo) - 你不打算去實現(xiàn)的什么?有什么界限?
  • 度量 - 你將如何衡量成功或失???
  • 調(diào)查 - 你還調(diào)查過其他的什么解決方案 (如果有的話)?你為什么沒有選擇它們呢?
  • 選擇 - 你決定了要做什么?你為什么決定這樣做?
  • 設(shè)計 - 你所選擇的設(shè)計有些什么細(xì)節(jié)?包括 API 概述,技術(shù)細(xì)節(jié),以及 (可能的) 一些頭文件例子,再配上其他任何你認(rèn)為有用的東西。這是你把你的設(shè)計賣給你自己和你的同伴工程師們的地方。

然后,我們會有兩到四個評審來審查文檔,提出問題,并作出最后的決定。這些評審應(yīng)該熟悉你要擴(kuò)展的系統(tǒng)。

這似乎是大量的工作,但它是值得的。最終的結(jié)果將是一個更茁壯和更易理解的設(shè)計。我們不斷地看到,當(dāng)一個變化經(jīng)過了設(shè)計審查,會使得錯誤更少,并降低了復(fù)雜性。另外,作為一個副產(chǎn)品,我們還得到了經(jīng)過評審的系統(tǒng)文檔。棒!

我們的代碼審查過程

因為以下幾個原因,我們的代碼審查過程是很嚴(yán)謹(jǐn)?shù)模?/p>

  • App Store 的時限 - 如果我們發(fā)布了一個 bug,由于 App Store 的審查過程會使得修復(fù)延遲約一個星期給客戶。
  • Register 是至關(guān)重要的 - 查找錯誤很重要,因為 Register 對餐廳、零售商店這些用戶來說非常關(guān)鍵。
  • 大型應(yīng)用程序 - 在一個像 Register 這樣大的應(yīng)用程序里在合并后再去找 bug 是很困難的。

我們的 pull requests 流程是什么呢?每次 PR 必須滿足:

  • 被追蹤了的 - 每一個 PR 都有對應(yīng)的 JIRA issue。
  • 被描述了的 - 每一個變化后面都必須有是什么以及為什么的一個清晰的描述。
  • 是可以被評審的 - Pull request 的 diff 文件必須是 500 行以內(nèi)。大型的 diff 是不允許的。如果一個 diff 比 500 行多很多的話,評審很容易看不到錯誤。
  • 集中 - 不要把重構(gòu)或重命名跟一個新的功能放到一起做。把它們分別開來。
  • 自我審查 - 在增加其他評審之前,所有 PR 的作者都要求對改動先做自我審查。這是為了避免雜散的 NSLog,缺失的測試,不完整的實現(xiàn)方式之類的事情。
  • 有兩個指定的評審?fù)ㄟ^ - 評審員中的一個必須是被改變組件的所有者。我們需要明確的列出評審人員,以確保工程師們知道在他們的審核隊列到底有些什么。
  • 被測試了的 - 包括了改動的穩(wěn)定性和正確性測試。沒有測試的 pull request 會被拒絕。

同樣,評審們被要求:

  • 清楚 - 意見必須清晰,簡明。對于新的工程師,評審應(yīng)包括可遵循的例子。
  • 解釋 - 不要只說 “把 X 改成 Y”;也要解釋了為什么應(yīng)該這樣改。
  • 不要挑剔代碼風(fēng)格 - 這是自動格式化干的事 (接下來會講)。
  • 文檔 - 每個代碼審查意見必須被標(biāo)記為下列之一: — 必須 (“必須在合并前修復(fù)?!? — 意見 (“遲早都需要修復(fù)?!? — 個人喜好 (“我會這樣做,但你不必?!? — 問題 (“這是干嘛的?”)
  • 樂于幫助 - 評審必須有一個樂于幫忙的心態(tài)來進(jìn)行代碼審查。評審的工作是幫助代碼安全地合并,而不是阻止它合并。

在合并之前,所有的測試必須通過。單元測試和我們的自動化集成測試 (使用 KIF) 跑成功前,pull request 的合并都是被禁止的。

一些流程 Tips

我們已經(jīng)開始在做下面的事情來幫助簡化和加快 Register 的開發(fā)過程。

盡可能地為通用流程建立文檔

在 Register 團(tuán)隊的成長中我們學(xué)到的有一件事是“口頭說說”是很糟糕的知識傳遞方式。如果一年只有幾個工程師加入項目的話,這不會是一個問題,但如果一個月就會有幾個工程師加入,尤其是如果他們只是針對臨時項目 (例如臨時需要一個服務(wù)器工程師幫助建立一個特別的功能),這種尺度很快會變得很費時。一個具有標(biāo)準(zhǔn)和團(tuán)隊實踐的保持更新的文檔就變得很重要。這份文檔應(yīng)該包括哪些內(nèi)容呢?

  • 代碼審查指南 (對于提交者和審查者) — “我需要多少個評審人?我什么時候可以合并?“
  • 設(shè)計審查指南 — “我應(yīng)該如何設(shè)計這個功能?”
  • 測試指南 — “我該如何測試呢?我們使用什么測試框架?”
  • 模塊/組件所有者 — “我可以跟誰討論 X?誰建的呢?”
  • 問題跟蹤指南 — “我在哪里可以查找并跟蹤我必須做的事?”

你可能會注意到這里的一個規(guī)律:凡是能在 10 分鐘以內(nèi)回答的問題都應(yīng)清楚地記錄下來。

盡量把低效的工作自動化

需要幾個工程師花數(shù)分鐘的手動流程如果用更多工程師可能需要花更長的時間。任何時候你看到瑣碎的東西花費了大量的時間,都應(yīng)該盡可能讓它自動化。

我們把我們的代碼風(fēng)格指南自動化了

我們最近的一個最大的“自動化”成功案例是我們的 Objective-C 代碼風(fēng)格指南:我們現(xiàn)在使用 clang-format 來自動格式化提交到 Register 及其子模塊的所有代碼。這消除了代碼審查里面的各種“沒有換行”或者“太多的空白”的意見,這意味著審閱者可以專注于真正提高產(chǎn)品質(zhì)量的東西。

我們每天都要合并很多的 pull requests。之前這些“挑剔風(fēng)格”的意見會在每個 pull request 額外花 10-20 分鐘 (鑒于審查者和作者)。這意味著僅是風(fēng)格指南的自動化都讓我們每天節(jié)省了兩小時或更長時間。也就是一個星期 10 個小時。增加太快了!

我們把代碼審查的可視性自動化了

另一個自動化節(jié)省時間的例子是我們每天都會發(fā)送 “Pull Request 狀態(tài)”的郵件。

在這個電子郵件存在之前,每天早晨我們當(dāng)中的 10 到 15 個人會擠在一個桌前站 10 分鐘,分配 pull requests 的審查。而現(xiàn)在,我們每天早上發(fā)出一個包含了所有開放 PR 列表的電子郵件,以及誰被分配來對其進(jìn)行審查。不需要再額外開會了。這意味著我們又多了每天 2 個多小時或每周 10 小時的開發(fā)時間。

這個每天 PR 狀態(tài)電子郵件的另一個好處是,我們可以輕松地跟蹤評審發(fā)生了什么事:花了多長時間,哪個工程師貢獻(xiàn)最大,哪個工程師審查了最多。這有助于揭示那些可能拖累團(tuán)隊的時間分配問題 (比如是否一名工程師做了團(tuán)隊一半的評審?)。

集中的 bug 跟蹤

如果你的 bug 被分散在多個跟蹤器上是不可能發(fā)布無缺陷的產(chǎn)品的。有一個地方可以讓我們看到一切有關(guān)當(dāng)前版本的信息是極其重要的:bug 的數(shù)量,每個工程師未解 bug 的數(shù)量 (是否有誰忙不過來了?),以及 bug 的總體趨勢 (我們修復(fù)它們速度比它們被創(chuàng)建的速度更快嗎?)。

保持共享代碼庫的質(zhì)量

如果只有幾個工程師在做同一個項目,可以很容易地保證質(zhì)量:因為所有的工程師都清楚了解代碼庫,他們也都有強(qiáng)烈的歸屬感。但當(dāng)一個團(tuán)隊擴(kuò)展到 5、10、20 或更多的工程師的時候,維護(hù)這樣的品質(zhì)變得更加困難。重要的是要確保每個組件和功能有明確的負(fù)責(zé)維護(hù)它質(zhì)量的所有者。

每一個組件都需要一個所有者

在 Register,我們最近決定應(yīng)用程序的每個邏輯組件都要有明確的所有者。這些所有者都記錄在一個列表里以便查找。什么是一個組件?它可能是一個框架,它可能是一個面向客戶的功能,它也可能是兩者的某種組合。確切的分界并不重要;最重要的是確保應(yīng)用程序的每一行代碼都是有人所有的。這些所有者要做些什么呢?

  • 他們審查和批準(zhǔn)代碼更改和文檔設(shè)計。
  • 他們了解“關(guān)鍵部位”,以及如何解決它們。
  • 他們可以為新來的工程師提供組件的概述。
  • 他們確保質(zhì)量始終越來越好。

在指派出組件明確所有者后,我們得到了很好的結(jié)果:有明確所有者的組件和默認(rèn)所有人為所有者的組件相比,代碼質(zhì)量是持續(xù)增高的 (bug 率也較低)。

保持主分支是可發(fā)布的

這是我們最近的另一項改變:我們已經(jīng)開始在主分支上嚴(yán)格執(zhí)行“不回退”的規(guī)則。這樣做的好處是什么?我們的主分支現(xiàn)在一直都很穩(wěn)定。如果有人發(fā)現(xiàn)了一個錯誤,他完全不需要去想是否需要提交這個報告。這么做也能減少 QA 的負(fù)擔(dān),因為花在搞清楚問題是否應(yīng)提交或者它們是否重復(fù)上的時間少了。如果發(fā)現(xiàn)了錯誤,就提 bug。

這一策略和發(fā)布列車模型是齊頭并進(jìn)的:幾乎在任何時候,我們都可以從主分支拉一個發(fā)布分支,并在短短幾天內(nèi)發(fā)布到 App Store。這對一個像 Register 這樣的一個大型應(yīng)用程序是非常有價值的,它可以幫助我們盡可能快的做出行動。

鑒于我們的規(guī)模,保持主分支在可發(fā)布狀態(tài),也有助于避免“破窗效應(yīng)(broken windows)”的問題;發(fā)現(xiàn)的時候就修正 bug,確保工程師讓自己保持更高的標(biāo)準(zhǔn)。

從開始就建立可測性

確保 Register 里的每一個組件在建造和設(shè)計的時候都保持了可測試性的初衷是非常重要的。沒有這一點,我們就需要成倍擴(kuò)大手動 QA 的工作量:兩個功能可以通過四種方式進(jìn)行交互,三個功能可以在八個方面互動,等等。顯然,這是不合理的、不可靠的,也是不可擴(kuò)展的。

當(dāng)我們在為某個功能做工程設(shè)計工作的時候,我們不斷地問自己:“這個可以測試嗎?我在讓自動化測試容易進(jìn)行嗎?“

建立可測試性也有一個額外的好處:它引入了所有 API 的二次使用 (即測試本身)。這意味著工程師們不得不花更多的時間思考一個 API 的設(shè)計,確保它在多個情況下都是工作的。其結(jié)果是,這將使得其他工程師重用 API 變得更容易,節(jié)省了未來的時間。

對我們來說,測試不是可選項,而是一個需求。如果你在 Register 提交代碼,必須包括測試。

CI 對 Pull Requests 的重要性

想想看:如果一個開發(fā)團(tuán)隊有 365 個工程師,每位工程師只需要每年弄壞一次主分支,就可以讓項目在整一年都停擺了。這顯然是不能接受的,并且會極大的減慢進(jìn)度和挫敗的開發(fā)團(tuán)隊。

有什么簡單的方法來防止主分支被破壞?首先當(dāng)然是不合并錯誤的代碼!這就是 pull request CI 需要做的,每個 Register 的 pull request 都有一個 CI 在有新提交的時候被觸發(fā)。大約 15 分鐘后,工程師就可以放心的提交 PR,因為他或她不會因此引入任何導(dǎo)致回退的問題。

當(dāng)我們有新加入的工程師時,這會是非常有價值的。他們可以提交代碼,而無需擔(dān)心他們將引入讓主分支不工作的改動。

隊伍不斷壯大時的一些觀察

以下是在過去三年當(dāng) Register 的 iOS 團(tuán)隊不斷壯大的一些個人看法。

沒有什么會是完美的

在一個大的應(yīng)用程序里,你會有大量的代碼。有些代碼是很老的了。但老并不一定意味著壞。只要你有良好的測試覆蓋率,舊代碼將繼續(xù)正常工作。不要把時間花在“清理”那些的履行了需求并且沒有拖累任何人的代碼上去。這種清理過程中你能做的最好的事情就是不破壞任何東西。所以還是把這些時間花在創(chuàng)建新的功能上吧。

花時間來了解代碼之外的東西

在一個大的代碼庫里,你很容易就把所有的時間都花在其中,而無法從外界學(xué)習(xí)新東西。

你怎么解決這個問題?每周花些時間 (我每天預(yù)留一小時) 從你的代碼庫之外的資源來進(jìn)行學(xué)習(xí)。你能從哪兒學(xué)習(xí)呢?可以看看那些聽起來有趣的討論,或者閱讀你覺得有興趣的領(lǐng)域的文章。堅持這樣做,你會發(fā)現(xiàn)這些并行的知識會為你的日常工作帶來很多好處。有時,正是這些小事情會造成結(jié)果的巨大區(qū)別。

技術(shù)累積是需要時間的

很少有可以立即解決的事情,包括技術(shù)累積。如果技術(shù)累積需要很長的時間,不要讓自己感到沮喪,尤其是在一個大的代碼庫里。

想想像體重增加一樣積累技術(shù):你并不會一夜就增加一百磅;它是逐步顯現(xiàn)的。就像減肥一樣,也需要大量的時間和精力來消化技術(shù) - 從來都不會有一個瞬時方案。在累積的同時跟蹤你的進(jìn)步,并確保它在一個合理的速度向下進(jìn)展。

這就是全部了,親們

如果你有任何問題,請隨時通過 k@squareup.com 聯(lián)系我。感謝您的閱讀!

(感謝 Connor Cimowsky, Charles Nicholson, Shuvo Chatterjee, Ben Adida, Laurie Voss, and Michael White 的審查。)

上一篇:調(diào)試下一篇:Omni 內(nèi)部