戰(zhàn):從Demo到上架的完整工程化指南)
RubyMotion 這個(gè)框架圈子一直不算大但真上手用過(guò)的人多數(shù)都會(huì)覺(jué)得回不去純 Objective-C 的寫(xiě)法。系列前兩篇我們完成了環(huán)境搭建、跑通了第一個(gè) iOS demo很多朋友留言問(wèn)的問(wèn)題出奇一致demo 能跑然后呢這篇就是來(lái)填這個(gè)坑的——從“能跑的 demo”到“能交付上架的 App”中間隔著開(kāi)發(fā)者模式、簽名打包、UI 適配、自動(dòng)化測(cè)試這幾座山。這篇偏向?qū)嵅儆?RubyMotion 的人多數(shù)是追求迭代效率的獨(dú)立開(kāi)發(fā)者或小團(tuán)隊(duì)文章里的所有步驟我都會(huì)貼出實(shí)際命令和配置你照著敲就能少踩一半的坑。1. 內(nèi)容整體設(shè)計(jì)與思路拆解1.1 從“跑通”到“交付”的跨越很多 RubyMotion 新手最大的誤區(qū)是把框架當(dāng)成“用 Ruby 寫(xiě)一個(gè)解釋器殼子”覺(jué)得反正底層是 Ruby運(yùn)行時(shí)性能不用擔(dān)心寫(xiě)完 UI 就能上架。實(shí)際完全不是這么回事。RubyMotion 編譯出來(lái)的是真正的機(jī)器碼底層走的還是 iOS 原生框架這一點(diǎn)既是它的優(yōu)勢(shì)也是它的束縛——優(yōu)勢(shì)在于性能、內(nèi)存管理和系統(tǒng) API 的完整掌控權(quán)都在你手里束縛在于iOS 平臺(tái)的開(kāi)發(fā)者模式、簽名機(jī)制、上架審核、多任務(wù)分屏、設(shè)備規(guī)格適配這些平臺(tái)規(guī)則一個(gè)都躲不掉。系列前兩篇解決的問(wèn)題大概是“如何讓 RubyMotion 跑起來(lái)”而這篇精要要解決的問(wèn)題是“如何讓 RubyMotion 的產(chǎn)物像一個(gè)正經(jīng)的原生 iOS App”。我建議你在動(dòng)工前先想清楚三件事第一這個(gè) App 的目標(biāo)系統(tǒng)版本是多少低了會(huì)多寫(xiě)一堆兼容代碼高了會(huì)丟掉部分存量用戶(hù)第二團(tuán)隊(duì)里有沒(méi)有人能處理證書(shū)和上架流程RubyMotion 把這部分復(fù)雜度轉(zhuǎn)嫁給了 Xcode 工具鏈繞不開(kāi)第三UI 是純代碼編寫(xiě)還是引入樣式庫(kù)這直接影響后續(xù)的適配工作量。這三件事想清楚后面的工程量能砍掉三分之一。1.2 RubyMotion 在 iOS 開(kāi)發(fā)里的定位與取舍RubyMotion 的本質(zhì)是編譯器和運(yùn)行時(shí)它把 Ruby 源碼編譯成 ARM 機(jī)器碼同時(shí)暴露了 Cocoa Touch 的全部 API。你可以用UIView.alloc.initWithFrame這種原生寫(xiě)法也可以用BW::ProgressHUD這類(lèi) RubyMotion 社區(qū)封裝。我在實(shí)際項(xiàng)目里的取舍原則很樸素能用原生 API 解決的問(wèn)題優(yōu)先用原生原生寫(xiě)起來(lái)特別啰嗦的比如字符串格式化、數(shù)據(jù)模型類(lèi)才用 Ruby 語(yǔ)法糖。這樣做的原因很簡(jiǎn)單原生 API 的資料最多Stack Overflow 上的 Objective-C 代碼你可以無(wú)障礙地翻譯成 RubyMotion 寫(xiě)法而冷門(mén) RubyMotion 封裝一旦出現(xiàn)維護(hù)斷檔踩坑的成本比省下的那幾行代碼高得多。這套取舍思路直接影響你后面遇到的每一個(gè)問(wèn)題——真實(shí)設(shè)備調(diào)試報(bào)錯(cuò)時(shí)第一反應(yīng)應(yīng)該是去查對(duì)應(yīng) Objective-C 接口的行為而不是懷疑 RubyMotion 本身有問(wèn)題。我把這個(gè)原則放在整篇文章的第一節(jié)不是說(shuō)教而是因?yàn)楹竺嫠袑?shí)操包括構(gòu)建配置、簽名調(diào)試、分屏適配都建立在這條理論上。2. 開(kāi)發(fā)環(huán)境與開(kāi)發(fā)者模式實(shí)戰(zhàn)2.1 開(kāi)發(fā)者模式到底要不要開(kāi)很多朋友在 iOS 16 之后的系統(tǒng)上連接真機(jī)調(diào)試Xcode 里死活看不到設(shè)備系統(tǒng)設(shè)置里翻半天也不知道問(wèn)題出在哪。這里要先明確一個(gè)概念開(kāi)發(fā)者模式Developer Mode是 iOS 16 開(kāi)始強(qiáng)制執(zhí)行的新安全檢查機(jī)制它的作用是防止普通用戶(hù)側(cè)載開(kāi)發(fā)者包。不開(kāi)這個(gè)模式Xcode 和 RubyMotion 的rake device都無(wú)法在真機(jī)上安裝調(diào)試包模擬器不受影響。開(kāi)啟方法非常簡(jiǎn)單打開(kāi) iPhone/iPad 的“設(shè)置 – 隱私與安全性”拉到最底部找到“開(kāi)發(fā)者模式”打開(kāi)后系統(tǒng)會(huì)提示重啟設(shè)備重啟后二次確認(rèn)即可。如果你看不到這個(gè)選項(xiàng)大概率是系統(tǒng)版本低于 iOS 16或者在設(shè)置里搜索關(guān)鍵詞沒(méi)匹配到。我在幫朋友排查時(shí)還碰到過(guò)一個(gè)冷門(mén)情況設(shè)備連接 Mac 后如果 Xcode 版本過(guò)舊開(kāi)發(fā)者模式的開(kāi)關(guān)不會(huì)出現(xiàn)先升級(jí) Xcode 再處理。注意開(kāi)發(fā)者模式只影響調(diào)試和側(cè)載不影響你從 App Store 正常下載應(yīng)用。開(kāi)啟后設(shè)備的安全性提示會(huì)多一條“允許從 Xcode 安裝 App”這是預(yù)期行為不用慌。開(kāi)啟之后用 Xcode 連接一次設(shè)備讓系統(tǒng)完成“信任此電腦”的配對(duì)然后 RubyMotion 的構(gòu)建就能識(shí)別真機(jī)了。這里有個(gè)細(xì)節(jié)值得多說(shuō)一句開(kāi)發(fā)者模式開(kāi)啟后真機(jī)調(diào)試簽名仍然需要。很多純看教程的朋友以為開(kāi)了模式就萬(wàn)事大吉結(jié)果rake device還是報(bào)簽名錯(cuò)誤這就是下一節(jié)要解決的問(wèn)題。2.2 RubyMotion 的模擬器與真機(jī)調(diào)試配置RubyMotion 的構(gòu)建命令區(qū)分目標(biāo)和運(yùn)行環(huán)境常用的是rake build構(gòu)建模擬器包、rake device構(gòu)建真機(jī)包、rake simulator構(gòu)建并啟動(dòng)模擬器、rake clean清理中間產(chǎn)物。這些命令最終都會(huì)調(diào)用 Xcode 工具鏈所以 Xcode 的命令行工具必須安裝完整。模擬器調(diào)試有個(gè)優(yōu)勢(shì)不要求證書(shū)和簽名構(gòu)建速度快適合早期的 UI 迭代和邏輯調(diào)試。真機(jī)調(diào)試則能測(cè)到推送、相機(jī)、振動(dòng)、后臺(tái)任務(wù)這些模擬器無(wú)法準(zhǔn)確模擬的能力。我的建議是日常邏輯和界面開(kāi)發(fā)用模擬器每集成一個(gè)涉及硬件的功能就上真機(jī)驗(yàn)證一次不要攢到最后一起測(cè)否則定位問(wèn)題時(shí)變量太多。真機(jī)調(diào)試的基建配置主要有三部分第一Xcode 里配置好 Apple ID 賬號(hào)和團(tuán)隊(duì)第二在 developer.apple.com 后臺(tái)注冊(cè)設(shè)備的 UDID第三創(chuàng)建一個(gè)匹配 App ID 的開(kāi)發(fā)者證書(shū)和描述文件。RubyMotion 項(xiàng)目里對(duì)應(yīng)Rakefile的配置項(xiàng)是app.codesign_certificate、app.provisioning_profile和app.developer_entitlements后面專(zhuān)門(mén)講配置。這里先記住一個(gè)原則模擬器跑不通的簽名問(wèn)題八成是證書(shū)信任鏈的問(wèn)題真機(jī)裝不上的問(wèn)題九成是描述文件或 UDID 的問(wèn)題。3. 構(gòu)建、打包與上架全流程3.1 Rakefile 的構(gòu)建設(shè)計(jì)與簽名配置RubyMotion 項(xiàng)目的核心配置都在Rakefile里這文件既是構(gòu)建腳本也是簽名配置中心。我第一次建項(xiàng)目時(shí)被Rakefile里長(zhǎng)長(zhǎng)一串a(chǎn)pp.xxx給繞暈過(guò)后來(lái)理清楚后發(fā)現(xiàn)真正需要手動(dòng)改的就那么幾項(xiàng)。Motion::Project::App.setup do |app| app.name MyApp app.identifier com.example.myapp app.codesign_certificate Apple Development: youremail.com (TEAMID) app.provisioning_profile ProvisioningProfile.mobileprovision app.developer false endapp.identifier這個(gè)值非常重要它就是 App 的 Bundle ID在 Apple 后臺(tái)創(chuàng)建 App ID、配置描述文件、上架時(shí)填寫(xiě)的標(biāo)識(shí)必須和它完全一致差一個(gè)字符都過(guò)不了校驗(yàn)。app.codesign_certificate可以從鑰匙串里拷貝證書(shū)名稱(chēng)app.provisioning_profile指向你下載的描述文件路徑。app.developer false表示構(gòu)建 App Store 發(fā)布版本為true時(shí)構(gòu)建的是開(kāi)發(fā)調(diào)試版。實(shí)際項(xiàng)目中我習(xí)慣用環(huán)境變量區(qū)分構(gòu)建模式比如rake device默認(rèn)用開(kāi)發(fā)證書(shū)RUBYMOTION_RELEASE1 rake device時(shí)切換成發(fā)布證書(shū)。這樣省去來(lái)回改 Rakefile 的麻煩也降低了誤用證書(shū)提審的風(fēng)險(xiǎn)。簽名配置這一塊做對(duì)了后面上架流程就是直線操作。3.2 archive、上傳與 App Store 上架細(xì)節(jié)RubyMotion 構(gòu)建上架包并不像 Xcode 工程那樣在界面上點(diǎn) Product – Archive而是用命令行工具封裝。rake archive會(huì)生成.xcarchive格式的歸檔文件之后用xcodebuild -exportArchive或者直接配合 Xcode Organizer 導(dǎo)出。新版 Xcode 也可以用 Transporter 直接上傳但 archive 這步繞不開(kāi)。具體流程我梳理成下面這張表方便對(duì)照階段命令/操作關(guān)鍵點(diǎn)構(gòu)建歸檔rake archive確保app.developer false否則歸檔的是 debug 包導(dǎo)出 IPAXcode Organizer 或xcodebuild -exportArchive選擇 App Store Connect 分發(fā)方式上傳Transporter 或xcrun altool需要 App 專(zhuān)用密碼填寫(xiě)元數(shù)據(jù)App Store Connect 后臺(tái)截圖、描述、隱私政策缺一不可提交審核App Store Connect 后臺(tái)等待審核通常 1-5 個(gè)工作日這里有個(gè)容易忽略的細(xì)節(jié)歸檔前必須把App Store Connect里的應(yīng)用創(chuàng)建好Bundle ID 要匹配否則exportArchive會(huì)提示找不到對(duì)應(yīng)的 App。很多人在 RubyMotion 里折騰半天最后發(fā)現(xiàn)是后臺(tái)少建了一個(gè)應(yīng)用條目。另外archive產(chǎn)物里的Info.plist經(jīng)常需要手動(dòng)確認(rèn)版本號(hào)和構(gòu)建號(hào)RubyMotion 默認(rèn)取的是app.version和app.short_version這些字段我會(huì)在每次 release 前單獨(dú)過(guò)一遍。3.3 免費(fèi)證書(shū)的踩坑與兜底方案Apple 的免費(fèi) Apple ID 賬號(hào)確實(shí)支持真機(jī)調(diào)試和個(gè)人開(kāi)發(fā)但限制非常多簽名證書(shū)只有 7 天有效期描述文件里只能包含一個(gè)設(shè)備這三點(diǎn)直接影響 RubyMotion 的開(kāi)發(fā)體驗(yàn)。你可能會(huì)想先用免費(fèi)賬號(hào)把開(kāi)發(fā)做完上架前再轉(zhuǎn)付費(fèi)賬號(hào)行不行答案是行但要留出重新簽名和重新描述文件的緩沖時(shí)間。免費(fèi)證書(shū)使用過(guò)程中最常見(jiàn)的坑是有效期過(guò)期后rake device構(gòu)建報(bào)簽名錯(cuò)誤。解決辦法不是瘋狂重裝 Xcode而是去后臺(tái)刪除舊證書(shū)創(chuàng)建新的開(kāi)發(fā)者證書(shū)并重新下載描述文件然后把鑰匙串里的舊證書(shū)刪掉。注意iOS 設(shè)備上已經(jīng)安裝的 App 會(huì)變成灰色不可用這是正常的吊銷(xiāo)表現(xiàn)重新安裝就能恢復(fù)。提示如果你只是自己玩或者做內(nèi)部工具免費(fèi)證書(shū)完全夠用如果計(jì)劃上架或者給多位測(cè)試同學(xué)分發(fā)盡早開(kāi)通付費(fèi)開(kāi)發(fā)者賬號(hào)一年幾杯咖啡錢(qián)省下的時(shí)間成本遠(yuǎn)超票價(jià)。4. UI 規(guī)范適配與分屏處理的正確姿勢(shì)4.1 RubyMotion 里寫(xiě)原生 UI 的幾種方式RubyMotion 寫(xiě) UI本質(zhì)上是調(diào)用 UIKit。最直接的方式是純代碼創(chuàng)建視圖比如label UILabel.alloc.initWithFrame(CGRectMake(16, 100, 200, 44)) label.text hello RubyMotion label.textColor UIColor.blackColor view.addSubview(label)這種寫(xiě)法跟原生的 Objective-C 一一對(duì)應(yīng)好處是底層的 frame 布局規(guī)則、Auto Layout 約束、Safe Area 概念都能用得上查資料無(wú)障礙。RubyMotion 社區(qū)還有motion-kit這種 DSL 布局庫(kù)支持類(lèi)似 CSS 的樣式和約束鏈?zhǔn)秸Z(yǔ)法但我的建議是項(xiàng)目初期先用手寫(xiě) frame 和 Auto Layout跑順了再?zèng)Q定要不要引入樣式庫(kù)。理由是每個(gè)額外依賴(lài)都會(huì)帶來(lái)一層抽象出問(wèn)題時(shí)你總要跳回原生 API 去理解和排查少一層抽象就少一層坑。4.2 尺寸類(lèi)與安全區(qū)的適配邏輯iOS 適配的核心是“尺寸類(lèi)”和“安全區(qū)”兩個(gè)概念。尺寸類(lèi)Size Classes分成緊湊和常規(guī)兩類(lèi)iPhone 豎屏是緊湊寬度、常規(guī)高度橫屏可能變成常規(guī)寬度、緊湊高度iPad 大部分情況是常規(guī)寬度。RubyMotion 里可以通過(guò)traitCollection.horizontalSizeClass和verticalSizeClass判斷當(dāng)前環(huán)境動(dòng)態(tài)調(diào)整布局。安全區(qū)則是從 iPhone X 之后引入的概念頂部齊劉海、底部 Home 指示條都是不安全區(qū)域。如果你還在用CGRectMake(0, 0, screen_width, screen_height)這種古老寫(xiě)法控件大概率會(huì)被狀態(tài)欄遮擋或者被底部 Home 條覆蓋。正確做法是使用safeAreaLayoutGuidelet guide view.safeAreaLayoutGuide label.topAnchor.constraintEqualToAnchor(guide.topAnchor).active true對(duì)應(yīng) RubyMotion 的寫(xiě)法是label.topAnchor.constraintEqualToAnchor(view.safeAreaLayoutGuide.topAnchor).active true。這套規(guī)則在 iPhone 新舊機(jī)型之間差異巨大如果你只適配了某個(gè)固定機(jī)型上架后用戶(hù)投訴界面錯(cuò)亂是必然結(jié)果。4.3 分屏與多任務(wù)適配的檢查清單熱詞里頻繁出現(xiàn)“iOS 分屏”這塊很多人以為只跟 iPad 有關(guān)實(shí)際上 iPhone 的橫豎屏切換、畫(huà)中畫(huà)、分組多任務(wù)都和布局邏輯相關(guān)。RubyMotion 開(kāi)發(fā)的多屏適配重點(diǎn)在于生命周期管理和布局刷新。iOS 13 以后引入了 Scene 生命周期RubyMotion 需要正確實(shí)現(xiàn)scene(_:willConnectTo:options:)和sceneDidBecomeActive等回調(diào)App 才能正確處理分屏?xí)r的界面重建。如果你的 App 是通過(guò) AppDelegate 的didFinishLaunchingWithOptions搭建根視圖在 iPad 上進(jìn)入分屏模式時(shí)可能會(huì)出現(xiàn)界面閃爍或者布局錯(cuò)亂因?yàn)橄到y(tǒng)需要按新的尺寸重新繪制。排查方法是打開(kāi)“設(shè)置 – 隱私與安全性”里的“日志記錄與分析”查看分屏切換時(shí)的崩潰日志。我整理了一個(gè)自測(cè)清單每次發(fā)版前過(guò)一遍豎屏、橫屏下關(guān)鍵按鈕不被安全區(qū)遮擋iPad 分屏 50/50 和 70/30 的尺寸都能正常操作切換分屏后內(nèi)容不重復(fù)加載、數(shù)據(jù)不丟鍵盤(pán)彈出時(shí)輸入框不被遮擋5. 自動(dòng)化測(cè)試與打包速度排查實(shí)錄5.1 motion-spec 快速上手RubyMotion 自帶motion-spec測(cè)試框架在項(xiàng)目目錄下運(yùn)行rake spec就能執(zhí)行測(cè)試。它沿用了 RSpec 的語(yǔ)法describe/context/it 的組織方式對(duì) Ruby 用戶(hù)非常友好describe 計(jì)算器 do it 兩個(gè)數(shù)相加 do calc Calculator.new calc.add(2, 3).should 5 end end這個(gè)測(cè)試跑在模擬器里所以可以測(cè) UI 元素、控制器跳轉(zhuǎn)和網(wǎng)絡(luò)層邏輯但注意網(wǎng)絡(luò)請(qǐng)求要寫(xiě)在測(cè)試?yán)镆С?mock否則測(cè)試速度和穩(wěn)定性都受影響。我給團(tuán)隊(duì)的規(guī)范是模型層邏輯盡量都用 spec 覆蓋控制器層只測(cè)關(guān)鍵跳轉(zhuǎn)和生命周期UI 展示型代碼不做斷言。原因是 UI 測(cè)試維護(hù)成本太高經(jīng)常因?yàn)閳A角改了幾個(gè)像素就掛掉一片實(shí)際收益很低。5.2 Xcode 打包突然很慢的排查實(shí)錄“Xcode 打包突然很慢”是熱詞里的高頻痛點(diǎn)也完全適用于 RubyMotion 場(chǎng)景。我遇到過(guò)一次真實(shí)案例同一個(gè)項(xiàng)目前一天rake archive五分鐘搞定第二天突然要四十分鐘排查后發(fā)現(xiàn)問(wèn)題不在 RubyMotion 代碼而在 Xcode 構(gòu)建系統(tǒng)。慢的原因通常出在三個(gè)位置第一是DerivedData緩存膨脹這個(gè)目錄默認(rèn)在~/Library/Developer/Xcode/DerivedData里面是編譯中間產(chǎn)物和索引積累多了會(huì)拖慢整個(gè)工具鏈第二是 Spotlight 索引正在全盤(pán)掃描系統(tǒng)在后臺(tái)重建索引時(shí) CPU 占用很高第三是簽名驗(yàn)證環(huán)節(jié)每次歸檔都會(huì)重新校驗(yàn)證書(shū)鏈鑰匙串里裝了一堆過(guò)期證書(shū)時(shí)會(huì)額外增加耗時(shí)。常用的排查手段就兩步先打開(kāi)終端運(yùn)行sudo fs_usage -w | grep mdworker看看是不是索引進(jìn)程在跑是的話等索引完成或者排除目錄再清空 DerivedDatarm -rf ~/Library/Developer/Xcode/DerivedData/*。清完緩存后重新構(gòu)建的速度往往會(huì)恢復(fù)正常。RubyMotion 自己的編譯緩存通常在build目錄下也可以用rake clean清理。5.3 常見(jiàn)問(wèn)題與排查技巧速查表我把這幾個(gè)月在 RubyMotion 項(xiàng)目里遇到的高頻問(wèn)題整理成了一張速查表方便你直接對(duì)照癥狀可能原因排查方向rake device找不到真機(jī)開(kāi)發(fā)者模式未開(kāi)啟或未信任電腦確認(rèn)設(shè)置里的開(kāi)發(fā)者模式并重啟設(shè)備簽名報(bào)錯(cuò)No signing certificate證書(shū)過(guò)期或鑰匙串里名稱(chēng)不匹配新建證書(shū)并更新 Rakefile 的證書(shū)名真機(jī)裝不上 App設(shè)備 UDID 未注冊(cè)或描述文件不匹配后臺(tái)注冊(cè) UDID重新生成描述文件分屏切換后界面錯(cuò)亂Scene 生命周期未處理檢查 scene delegate 的回調(diào)實(shí)現(xiàn)構(gòu)建速度突然下降DerivedData 或索引問(wèn)題清緩存、關(guān)掉系統(tǒng)索引這張表不神秘但調(diào)試時(shí)能幫你少走彎路。遇到問(wèn)題時(shí)先對(duì)照表格判斷大類(lèi)再針對(duì)性看日志比在文檔里亂翻效率高很多。坦白說(shuō)用 RubyMotion 做 iOS 開(kāi)發(fā)的這兩年我最大的體會(huì)是框架的門(mén)檻不在 Ruby而在 iOS 平臺(tái)的工程化能力。開(kāi)發(fā)者模式、簽名打包、UI 適配、自動(dòng)化測(cè)試這些內(nèi)容用 Xcode 也要學(xué)用 React Native、Flutter 也繞不開(kāi)RubyMotion 只是把語(yǔ)法換成了更順手的 Ruby平臺(tái)規(guī)則一點(diǎn)都沒(méi)少。所以在動(dòng)手前別抱“純 Ruby 就能上架”的幻想老老實(shí)實(shí)把證書(shū)和構(gòu)建流程跑通后面反而是坦途。最后再分享一個(gè)小技巧把 Rakefile 和證書(shū)配置全部寫(xiě)進(jìn) Git換電腦后一條bundle install加一條rake就能恢復(fù)環(huán)境這份配置我已經(jīng)吃了半年灰從來(lái)沒(méi)翻過(guò)車(chē)。