1. 为什么第二篇要绕开语法糖,专攻 UIKit 和签名
RubyMotion 的 iOS 开发系列写到第二篇,我默认你已经过了motion create demo的兴奋期,也知道了rake能编出原生 App。但你很可能卡在下一个路口:页面怎么写?控件怎么布局?为什么真机跑不起来?如果你带着这几个问题往下看,这篇会帮你省下至少一个星期的试错时间。如果还没有装好环境,建议立刻回头补第一篇,因为这里不会再讲怎么装依赖和初始化项目。
RubyMotion 最有意思的地方是它用 Ruby 语法调用整个 UIKit,但底层生成的还是原生机器码,没有 WebView、没有 JavaScript 桥,App 启动速度和内存表现和 Objective-C 写的没差别。这句话换个角度看:你不是"在 Ruby 里写 iOS",而是"用 Ruby 写 iOS"。这两者差别很大,决定你遇到问题时查阅哪类文档。Ruby 侧只负责语法组织和简化表达,点击事件、生命周期的名字、AutoLayout 的行为,全部按 UIKit 的规则来。很多从纯 Ruby 转过来的朋友栽跟头,就是因为只记得 Ruby 的灵活,忘了 iOS 系统的那套回调约定。
接着说第二篇的思路。系列第一篇讲的是motion create、rake和 Ruby 类如何映射成 Objective-C 类,算是热身。第二篇要直接面对真实开发:纯代码 UI 怎么写才不别扭,页面跳转和生命周期需要注意什么,模拟器黑屏怎么救,真机免费签名怎么过,以及发布上架时哪些坑和 RubyMotion 无关。下面每个章节都能对应到真实工程的一个阶段,你按顺序走完,一个可运行的 App 就算真正落地了。
适合谁读?如果你是 Ruby 党,对 Xcode 的工程文件、证书配置、模拟器协作还没建立直觉,请重点看第 4 和第 6 章;如果你已经是 iOS 原生开发,想换个语言体验,前 3 章读起来会很快,但建议别跳过 Rakefile 那节,因为 RubyMotion 的构建流程和 CocoaPods 之间的摩擦很可能让你惊讶。这三个角色我都见过,各自的痛点很不一样:Ruby 党容易轻视 UIKit 规则,iOS 原生开发者容易受不了 Ruby 的动态性,而新手则需要同时补两边。所以我尽量把每一步都拆得具体一点,能贴代码就贴代码,不搞抽象的心法。
1.1 RubyMotion 的定位:是编译器不是解释器
RubyMotion 通常被理解成"用 Ruby 写 iOS",这个说法没错,但不够准确。它更接近一种把 Ruby 源码编译成机器码的闭源编译器,同时把运行时和 Objective-C runtime 打通。你写的每一个 Ruby 类,最终会变成 Objective-C 类,方法通过 runtime 动态派发,所以和 Cocoa 的交互几乎是无缝的。这也是它能保持接近原生性能的原因:App 里没有嵌入解释器,每一行 Ruby 在编译阶段都被翻译成可执行的机器指令、或可被 runtime 转发的 Objective-C 结构。
这个定位决定了你能用 Ruby 生态里的一部分 gem,但又不是全部。因为要经过编译期转换,纯 Ruby gem 里如果用了太多运行时元编程、eval、C 扩展,很可能在 RubyMotion 里直接报错。很多人第一次接触 RubyMotion,想着把自己的 Rails 代码搬进来,结果报了一堆unexpected token和unknown type,就是因为没理解这条边界。所以不要把 RubyMotion 当成"Ruby 解释器跑在手机上",它是一个有严格前端约束的编译器。
1.2 第二篇的主线:先跑通 UI,再谈真机和发布
如果你已经会把"Hello"显示在界面上,恭喜你,接下来就是最难熬的一段:布局和交互。很多 RubyMotion 教程只会列代码,不解释为什么addTarget的方法名要写成字符串,也不解释为什么motion build --release后签名总失败。第二篇的主线就是把这些"没人写"的部分补全。
我建议你跟着这篇文章的节奏这么做:先在模拟器上把一个纯代码页面跑通,包括一个 UILabel、一个 UIButton 和一个点击事件;然后处理导航栈,让两个页面互跳;最后再碰真机和证书。不要一上来就集成 AFNetworking、Alamofire 或各种增强 gem,那样只会把问题混在一起,排查时分不清是谁的错。等项目骨架简化到只剩必要组件后,再引入网络层、数据层,你会发现很多"诡异错误"其实是依赖版本和 Xcode 版本打架。
1.3 环境版本和必要的 RubyMotion 认知
这篇的示例基于 RubyMotion 6.x。新版已经支持 Apple Silicon Mac 和多个 Xcode 版本,但如果你还在用老版本,某些rake任务的名字会不一样。我建议至少升级到 6.3 之后的版本,它修复了不少模拟器 SDK 路径问题。如果你用的是教育版或免费试用版,请注意它可能限制同时运行的任务数,但对单机开发影响不大。
Xcode 方面,如果你只用模拟器开发,14 和 15 差别不大;如果要做真机调试,iOS 16 以上设备必须处理开发者模式。RubyMotion 官方文档有时滞后于 Xcode 发布,所以遇到invalid active developer path这类错误,先去执行xcode-select -p确认命令行工具指向的是当前 Xcode,而不是残留的 Command Line Tools。这个问题很常见,而且和 RubyMotion 本身关系不大,属于 macOS 工具链的基本排查。
2. 第一个能跑的 RubyMotion 页面,这样搭最舒服
2.1 用 Rakefile 掌控工程设置
motion create HelloRuby后,根目录的 Rakefile 是核心。RubyMotion 没有.pbxproj,所有 target、签名、资源文件、依赖都在 Rakefile 里配置。它的基本结构是:
Motion::Project::App.setup do |app| app.name = "HelloRuby" app.identifier = "com.example.helloruby" app.spec_files = Dir.glob("spec/**/*.rb") app.files = Dir.glob("app/**/*.rb") app.deployment_target = "15.0" app.development = true endapp.files默认包含app/**/*.rb,基本不用改。app.deployment_target决定了最低支持的 iOS 系统,建议按你的目标用户设。app.identifier不要乱填,真机签名和推送都要用到它,而且上架后 App ID 不能随意修改前缀。Rakefile 本身是一段 Ruby 代码,所以你可以在里面写变量、循环、条件分支,轻松实现 debug/release 两套配置。
这种灵活性是 Xcode 工程文件给不了的:比如你可以用一个环境变量让不同开发者使用不同证书,也可以读取 CI 平台上自动生成的参数来切换构建 target。但也因为它是代码,如果你写错变量名或调用了不存在的方法,构建时才会炸出来。因此在项目初期就加上 RuboCop 或者rake static做静态检查,能省掉很多低级错误。
2.2 纯代码构建 UIKit,不拖 Storyboard
RubyMotion 入门最大的坎是:很多教程说要删除 Storyboard,用纯代码写 UI。对,我极度推荐纯代码。不是故事板不好,而是在 RubyMotion 里维护 Storyboard 的成本太高——你要为每个 ViewController 写一个对应的 Ruby 类,还要处理@IBOutlet和 selector 名称,错误难查。纯代码的方式就是直接创建一个UIViewController子类,重写viewDidLoad,在里面创建控件、设置约束、挂上事件。
来看一个实际可跑的示例。我建了一个HomeController,它继承自UIViewController,在viewDidLoad里创建了一个 UILabel 和约束:
class HomeController < UIViewController def viewDidLoad super view.backgroundColor = UIColor.systemBackgroundColor label = UILabel.alloc.initWithFrame(CGRectZero) label.text = "欢迎回来" label.font = UIFont.preferredFontForTextStyle(UIFontTextStyleHeadline) label.textAlignment = NSTextAlignmentCenter label.translatesAutoresizingMaskIntoConstraints = false view.addSubview(label) NSLayoutConstraint.activateConstraints([ label.centerXAnchor.constraintEqualToAnchor(view.centerXAnchor), label.centerYAnchor.constraintEqualToAnchor(view.centerYAnchor), label.leadingAnchor.constraintGreaterThanOrEqualToAnchor(view.safeAreaLayoutGuide.leadingAnchor, constant:20) ]) end endtranslatesAutoresizingMaskIntoConstraints = false在纯代码布局里几乎必写,否则你加了约束也不会按预期生效。UILabel.alloc.initWithFrame(CGRectZero)和UILabel.new都能行,然后通过约束决定真正位置。唯一要注意的是,viewDidLoad里一定要调用super,不然系统的 view 装载过程会不完整,轻则控件不显示,重则启动崩溃。这是纯 Ruby 背景开发者最容易漏掉的一步。
2.3 为什么不要迷信 motion-layout 这种 DSL
RubyMotion 社区有个 gem 叫 motion-layout,能用 DSL 写类似 Android 的约束。我早期用过,确实能少写不少NSLayoutConstraint的样板。但问题在于:它的 API 是一套字符串 DSL,对复杂约束、比例约束以及 SafeArea 支持不全,出了布局警告时,你反而要去理解它翻译成 UIKit 约束后发生了什么。
如果你只做简单页面,可以直接用 frame 计算,比如label.frame = [[20, 100], [width - 40, 32]]。这种写法的好处是直观,坏处是屏幕旋转或 iPad 分屏时会错位。我的建议是:单页静态控件用 frame,动态布局和自适应用原生 AutoLayout 约束。RubyMotion 里写 AutoLayout 的代码量虽然比 Objective-C 少一点,但比 Swift 的语法还是啰嗦,要不要用第三方 DSL,取决于你能接受多少黑魔法。像motion-layout这种 DSL 确实能提升开发速度,但遇到系统升级导致 API 变化时,你只能等 gem 作者更新,这种维护风险是隐性成本。
2.4 事件的 selector 字符串,千万别说错
UIControl 的addTarget要求传一个字符串方法名。在 RubyMotion 里,action:"did_tap_button:"会被转换成 Objective-C selector,匹配到同名 Ruby 方法。如果方法名带冒号,说明它接受一个 sender 参数;没有冒号则不传参。这两个不能混,否则点击没反应。我写过不少次方法名少个冒号导致事件不触发,查了半天。
所以我的建议是:button action 统一起名为did_tap_xxx:,放进一个固定位置,然后在viewDidLoad末尾用一段集中addTarget。这里也想提醒:addTarget的 target 参数是self,会被 UIKit 以 weak 方式持有。如果你的按钮被父视图持有而 controller 也要释放,事件不会导致 retain cycle,但反过来,如果闭包通过 block 捕获了 controller,controller 又被按钮持有,就很容易泄漏。用 Target-Action 而非闭包,至少能避开一半的循环引用问题。
3. 生命周期和页面跳转,RubyMotion 没帮你省这部分
3.1 生命周期方法名一个都不能拼错
viewDidLoad、viewWillAppear:、viewDidDisappear:,在 RubyMotion 里你直接定义同名 Ruby 方法。注意冒号:带参数的方法名必须和系统 selector 完全一致。比如viewWillAppear:接收一个animated参数,如果漏了冒号,iOS 不会在你进入页面时调用它,你可能会在页面显示前错过数据请求。
常见错误是写了def viewWillAppear而没有animated,RubyMotion 可能不会报警,页面正常显示但你的 viewWillAppear 没触发。所以一开始花 10 分钟把这几个生命周期方法打印一遍,确认触发顺序,比反复猜强。我通常会在基类里把生命周期统一打NSLog,然后用Motion::Log控制级别,只在 debug 时输出。这样一旦页面的生命周期不符合预期,第一眼就能发现是方法名写错还是系统调用被父类拦截。
3.2 push、present 和返回时的内存注意
UINavigationController 的 push 和 pop 大家都懂,但需要提醒一点:RubyMotion 的闭包和 block 在 UIKit 里用多了,容易出现 retain cycle。比如你在一个 UIButton 的闭包回调里引用了self,而 self 又持有这个 button,那么 controller 就释放不了。轻则一个页面反复进入后内存涨,重则 App 莫名其妙卡死。用 Target-Action 就能规避一部分。
present 一个 modal 时,如果modalPresentationStyle是 pageSheet,在 iPad 上右上角的关闭按钮不会被你的 dismiss 代码自动接管,用户下拉关闭时不会触发你预设的销毁逻辑。你要根据业务决定是否要隐藏这个系统关闭按钮,或者在viewDidDisappear里处理退出逻辑。另外,从 presented controller 调用presentingViewController.dismissViewControllerAnimated也是常见写法,注意别在 dismiss 后再访问 presented controller 的 view,否则会触发重建,带来额外的崩溃隐患。
3.3 SafeArea、分屏和横幅,绕不开的 iOS 行为
iOS 15 以后,导航栏配置出现了新的UINavigationBarAppearance,如果你不设置,页面滚动时导航栏会变透明,标题字体也可能跟随系统。RubyMotion 里可以直接调用这个类,但代码比较长。我在项目里把导航栏配置封装成一个基类方法,所有子 controller 继承它,保证视觉一致。
分屏和 SafeArea:如果你的 App 要在 iPad 分屏下工作,布局锚点不要写死到view.bounds,一定要用safeAreaLayoutGuide。keyWindow的获取也很重要,老项目用UIApplication.sharedApplication.keyWindow在 iOS 13 以后会提示 deprecated,最好用UIApplication.sharedApplication.connectedScenes找到 active scene 里的 window。写自定义横幅通知时,这个 window 获取方式直接决定你的横幅能不能盖在正确的层级。很多人做"仿 iOS 通知横幅"功能,动画做得像模像样,但到了原生层却不知道用addSubview到哪个 window,就是没搞清楚 UIWindow 的层级结构。
4. 真机调试和免费证书,这是 RubyMotion 劝退重灾区
4.1 模拟器黑屏,先别急着怪代码
RubyMotion 模拟器调试除了跑起来,还包括每次改代码后的增量编译。但你在魔术般快速编译的同时,也会遇到黑屏、闪退、Failed to launch等随机 bug。此时先执行rake clean,再看是不是 Xcode 和模拟器 SDK 冲突。如果还不行,xcrun simctl list devices,确认当前 Simulator 运行时存在。我之前遇到过一台公司的 Mac,因为装了多个 Xcode 版本,xcrun一直指向旧工具链,导致新模拟器无法启动。
模拟器缓存问题比想象中多。CoreSimulator 服务卡住时,所有模拟器都会变得很慢。sudo killall -9 com.apple.CoreSimulator.CoreSimulatorService是社区里通用的大招,执行后模拟器和 Simulator.app 需要重新启动。只要你的代码逻辑不涉及持久化状态,这招基本无害。不过要提醒:这个命令会把当前所有模拟器杀干净,如果你的 App 状态存在模拟器里,下一次启动会像全新开机一样。所以有调试数据时,慎用这招。
4.2 真机安装:开发者模式、信任证书和 Rakefile
iOS 16 之后,真机调试第一步不是连上 Xcode,而是打开"设置 -> 隐私与安全性 -> 开发者模式"。如果你找不到这个入口,说明这台设备没有处于开发者模式开关状态,或是被 MDM 策略限制了。打开后手机会重启,然后再次连接 Mac。RubyMotion 连真机时,电脑上首次会弹"是否信任此电脑",手机上也有一份"信任此电脑",两个都要点。
然后才是签名。个人免费账号在 Xcode 里登录后,Rakefile 里可以设置:
app.codesign_certificate = "iPhone Developer: Your Name (TEAMID)" app.provisioning_profile = "/path/to/Your.mobileprovision"如果你不用 Xcode 创建配置文件,RubyMotion 会在编译时提示no identity found。最快的解法:先在 Xcode 里随便开一个 iOS 工程,在 Signing & Capabilities 里选择你的 Personal Team,让 Xcode 自动把这个设备加入 provisioning profile,然后关掉那个工程,再用rake device跑 RubyMotion 真机目标。很多"不信任开发者"的弹窗都在这个阶段解决。还有一个小技巧:真机调试时,模拟器编译产物和真机编译产物是分开的,如果发现 Xcode 里证书正常但rake还报错,先看看是不是上次 rootless 安装导致 RubyMotion 的模拟器缓存没清。
4.3 免费证书的七天限制,怎么安排开发节奏
免费证书不是无限用的,个人 Apple ID 创建的开发证书大约七天会失效。意思是你昨天还能跑,今天突然启动就崩,或者 Xcode 提示"a valid provisioning profile for this executable was not found"。这个限制没办法绕,只能用付费开发者账号或者每周重新签名一次。开发阶段,我一般会把"重新签名"排成周任务,或者做一个收尾脚本,每次固定时间拉最新状态给真机重跑一遍。
收费账号的签名在开发者中心创建,RubyMotion 会用钥匙串里的证书自动匹配。这里有个细节:免费证书的 Team ID 是个人 ID,你上架时必须用付费账号重新生成 Provisioning Profile,不能把测试真机签名直接提交 App Store Connect。我在项目里专门写了两个 Rakefile 任务,一个rake device:debug一个rake archive:release,分别用不同的app.codesign_certificate变量,避免把证书弄混。如果团队有多个人,建议在 Rakefile 里读取环境变量BUILD_SIGN_CERT,不要在代码里硬编码证书名。
免费证书的坑远不止七天失效。我在项目中遇到过的错误整理成一张速查表:
| 错误信息 | 常见原因 | 处理办法 |
|---|---|---|
no identity found | 钥匙串中没有匹配的开发者证书 | 到 Xcode 账号里添加 Apple ID,等待证书生成 |
Your device is not registered | 设备 UUID 未加入 provisioning profile | 在开发者网站或 Xcode 的 Devices 窗口注册设备 |
code signing certificate ... couldn't be found | Rakefile 里的证书名和钥匙串不一致 | 用security find-identity -p codesigning -v查真实名称 |
provisioning profile ... doesn't include signing certificate | profile 和证书不匹配 | 重新下载并安装正确的 mobileprovision |
4.4 崩溃定位:.ips文件里的秘密
真机崩溃时,控制台可能只输出App was terminated by signal 11。更可靠的方式是在 Mac 上打开~/Library/Logs/DiagnosticReports,找以 App 名开头的.ips文件。这个文件有 JSON 结构,里面exceptionReason和faultingThread能告诉你崩溃线程和调用栈。RubyMotion 的调用栈是 Objective-C selector,对应关系可以通过motion symbolize任务映射成 Ruby 方法名,不过前提是你保留了build里的 debug symbols。
如果崩溃发生在 launch 阶段,除了符号化,还要检查是否缺少NSPhotoLibraryUsageDescription这类权限描述,或者尝试在application:didFinishLaunchingWithOptions:里加一个NSLog,看启动流程走到哪一步。这种"给流程打点"的手段,比盯着空白屏幕猜要高效得多。我见过最离谱的崩溃是启动时某个资源文件没被正确打包,导致读取 nil 后访问内存,这种现象在真机上常见,模拟器反而不容易复现,所以真机崩溃不要只看模拟器 debug 日志。
5. 第三方库、资源和自动化:RubyMotion 的现实世界
5.1 CocoaPods 接入的边界
RubyMotion 可以接 CocoaPods,但不像原生 Podfile 那么常规。在 Rakefile 里:
app.pods do pod 'AFNetworking', '~> 4.0' end然后运行rake pod:install。注意 RubyMotion 对 Pod 的解析依赖 podspec 中的静态库设置。如果那个 Pod 用了 Swift、dynamic framework、或者在新版本 Xcode 里编译不过,你的rake build就会挂在 Pod 阶段,这不是 RubyMotion 的 bug,而是兼容边界。我建议把 Pod 版本锁死,pod 'AFNetworking', '4.0.1',不要用波浪号,除非你想体会每天构建都变的心情。
选库时有几条经验:优先选 Objective-C 写的、更新频率低的库;避免选重度依赖 networking 或 UI 的 Swift 库;如果你真的需要某个 Swift Pod,可以考虑在工程里用app.vendor_project引入编译好的 framework,但 RubyMotion 对 Swift 框架的桥接仍不完美。实际项目中,网络层我用的是 RubyMotion 的AFMotion封装,而不是直接 Pod AFNetworking,省掉很多头疼。AFMotion本质是 AFNetworking 的 Ruby 包装,API 简洁,社区维护也还算稳定。
5.2 Resources 资源文件和 Asset Catalog
RubyMotion 把resources目录下的文件打包到 App 资源里,UIImage.imageNamed("icon.png")就能读。因为不走 Xcode 的 Asset Catalog,所以如果你的图标要支持多尺寸,还是得在 Rakefile 里用app.icons明确声明所有尺寸。例如:
app.icons = { 'iPhone' => ['Icon.png', 'Icon@2x.png', 'Icon@3x.png'], 'iPad' => ['Icon-76.png', 'Icon-76@2x.png', 'Icon-152.png'] }LaunchScreen 更麻烦,建议直接用 Storyboard 文件放在resources里,同时也要在 Rakefile 里指定app.launchscreen_storyboard = "LaunchScreen"。资源文件更新不同步的老问题,我归因于rake clean次数太少导致的隐性缓存,所以遇到改动资源不生效,第一时间 clean,不要折腾Simulator -> Device -> Erase All Content and Settings。在团队协作中,我还会给资源文件加版本号,比如logo_2.png,虽然难看点,但能避免证书和资源的缓存歧义。
5.3 自动化测试能做到什么程度
RubyMotion 自带rake spec,可以写单元测试和部分功能测试。spec目录下按 Ruby 的 RSpec 风格写,跑得很快。但 UI 自动化就尴尬了:RubyMotion 没有官方提供与 XCUITest 的完整桥接,你可以在spec里驱动 UI 事件吗?有 gem 如motion-fixtures做小范围 UI 测试,和 Appium 结合也可以,但需要你额外写 Ruby 服务端脚本。这意味着 UI 自动化的成本不低。
我的实际做法是:核心的业务逻辑用rake spec覆盖,UI 上的自动化依赖原生 XCUITest。这不是 RubyMotion 的缺陷,而是 iOS 自动化测试本来就比较封闭。若项目以 RubyMotion 为主,你可以在 Run Script 阶段调用 Xcode 的xcodebuild test,把原生 UI 测试 target 和 RubyMotion 编译产物放一起。不过这属于进阶玩法,不建议第一版就做,先把核心 ruby 逻辑测好,UI 层靠回归清单人工过更加现实。
5.4 与原生 SDK 混编和桥接技巧
RubyMotion 要集成第三方原生 SDK,通常有两个方案:一个是 CocoaPods 包装,另一个是app.vendor_project直接引入 SDK 的源码目录。后者更可控,也更容易踩坑。vendor_project的声明大概长这样:
app.vendor_project("vendor/MySDK", :static, headers: ["vendor/MySDK/include"])引入后,Ruby 层就能通过MySDK模块调用。需要注意:SDK 头文件如果有 C++ 接口,你可能需要额外指定cflags,或者提供一个 Objective-C 包装层。我倾向于写一层薄薄的兼容层,只暴露几个 Ruby 友好的方法,避免 RubyMotion 和 C++/Swift API 直接纠缠。
桥接的核心原则是:不要在 Ruby 层直接展开复杂的 C 结构体,最好由 Objective-C 类封装后返回字典、数组等 Ruby 好用对象。在实际开发中,我甚至会给第三方 SDK 做一个SDKBridge类,把所有extern "C"函数封装成SDKBridge的类方法,Ruby 端只调用这个类。这样,无论 SDK 底层怎么变,只要接口不变,Ruby 层代码几乎不用动。这是 RubyMotion 混编不被劝退的关键。
6. 构建发布:从签名到 App Store 的完整避坑清单
6.1 Release 构建与签名校验
发布版通常用motion build --release。这个命令会生成build/iPhoneOS-.../YourApp.app。在打包之前,先在 Rakefile 里确认 Release 模式的codesign_certificate指向 "iPhone Distribution" 证书。你也可以不指定,直接用 Xcode 的xcodebuild生成 .ipa,但 Rakefile 才是 RubyMotion 最可控的地方。
构建完成后,用codesign -dvvvv app/YourApp.app查看签名信息,重点看Authority=iPhone Distribution: ...和TeamIdentifier是否一致。如果发现是iPhone Developer,说明你用了开发签名,上传到 TestFlight 多半会被拒。建议在 Rakefile 里设置:
app.codesign_certificate = "iPhone Distribution: Your Company (TEAMID)" app.provisioning_profile = "/path/to/distribution.mobileprovision"如果你的 app 有扩展组件,比如通知扩展、今日组件,需要在 Rakefile 里用app.extensions单独配置,否则扩展签名很容易被漏掉,导致整个 Archive 非法。这一步在原生 Xcode 工程里由 target 自动处理,但在 RubyMotion 里需要手动声明。
6.2 TestFlight 和浏览器唤起安装的配置
TestFlight 上架前,同样要过一遍 App Store Connect 的审核,但外部测试审核比正式上架快。上传 .ipa 时,可以直接用 Transporter 或xcrun altool。altool命令大概如下:
xcrun altool --upload-app -f YourApp.ipa -t ios -u you@example.com -p app-specific-password注意密码要用 App 专用密码,不能直接用 Apple ID 密码。浏览器唤起安装 App 有两种正规姿势:Universal Links 和自定义 URL Scheme。RubyMotion 里需要配置app.info_plist里的CFBundleURLTypes或associatedDomains。我一般建议优先 Universal Links,因为它的体验更接近系统级跳转,而且不会因为 App 未安装时弹出"无法打开"而被浏览器拦截。设置 Universal Links 后,后端要托管 apple-app-site-association 文件,这个麻烦程度不小,但安全性远高于 URL Scheme。URL Scheme 容易被其他 App 抢注,Universal Links 虽然也没有彻底解决所有安全问题,但至少由系统校验域名声明。
6.3 上架被拒的常见原因,和 RubyMotion 没关系
很多 RubyMotion 开发者被拒后第一反应是"是不是 RubyMotion 编译的 App 不被认可"。其实苹果不会因为语言和工具链拒绝你,除非你的二进制存在异常内存或隐私调用。被拒最多的还是:
- 缺少权限用途说明:用相机、相册、定位时必须添加
Info.plist描述文案。 - App 内购买项目没有恢复购买按钮。
- 界面在 iPhone 和 iPad 分屏下布局变形。
- 使用了私有 API 或
UIApplication的未公开方法。
这些和 RubyMotion 无关。你只要把原生 App 会遇到的问题过一遍,审核就能过。另外,NSAllowsArbitraryLoads设为 YES 会加大被拒风险,能用 HTTPS 就不要开纯 HTTP 权限。如果你的 App 有登录功能,记得提供注销入口,苹果审核对账号状态保留很敏感,这也不是 RubyMotion 专属要求。
6.4 日常版本迭代的维护心得
RubyMotion 项目的维护,我最看重的是 Rakefile 的版本管理和Gemfile.lock。如果几个 Mac 上都要开发,必须把 Gemfile 锁定版本,否则 gem 更新可能导致编译行为变化。就像 Lockfile 对 Node 和 Python 项目的作用,RubyMotion 也一样需要稳定依赖。
其次,保持 Xcode 版本稳定。RubyMotion 官方发布新的 Xcode 支持往往滞后,别在正式项目里赶时髦升级系统工具链。每做一个迭代,先用rake clean后完整构建一次,确保 Release 环境无缓存依赖。我遇到过 debug 正常、release 崩溃的情况,最后发现是NSLog条件编译和app.optimize的不同路径导致。在 release 模式下,RubyMotion 会做更多优化,某些未初始化的 Ruby 变量行为会不一样,所以必须保持"preflight 构建"的习惯。
7. 项目适配性判断与维护技巧
7.1 什么项目适合 RubyMotion,什么不适合
文章写到这里,第 7 章就是最真实的经验部分。我在接 RubyMotion 项目之前,曾经以为它只是个玩具。真做下来发现,写 UI 时 Ruby 的紧凑确实能提升效率,一个简单的页面加逻辑,代码量比 Objective-C 少一半。但它的社区小、资料旧,排查问题比 Swift 项目更需要耐心。所以不要盲目上 RubyMotion,先问自己三个问题:团队里 Ruby 程序员多不多?App 的 UI 复杂度是不是集中在表单、列表、详情页?是否必须深度集成最新 Swift 生态 SDK?
我的实际体会是,RubyMotion 适合的目标非常明确:团队里 Ruby 程序员多,产品周期短,UI 复杂度不高,需要快速出原生 App。如果项目要求大量 Swift SDK 集成,或者团队只有 Objective-C/Swift 背景,那用 RubyMotion 反而增加沟通成本。这个"适不适合"的判断,比写代码本身更重要。很多技术选型失败不是工具不行,是团队和场景没对上。如果你的项目只是内部工具,UI 规模不大,RubyMotion 的快速迭代特性会非常舒服;反之,如果你在做金融类 App,需要接入大量风控和加密 SDK,那混编成本可能远超它带来的开发效率增益,这时候就老实退回原生。
7.2 我推荐的一套维护小技巧
最后分享一个小技巧:在 Rakefile 里加一个rake log:device任务,把真机日志实时拉到终端,这样你调试时不用一直盯 Xcode 的 Console。我的写法是:
desc "Show device log" task :"log:device" do exec "idevicesyslog" end需要提前装libimobiledevice,但真机接上后真的省不少事。另外,建议在.gitignore里加上build/和.bundle/,避免大量编译产物和 gem 依赖进入仓库。如果你要给不同客户出包,可以把证书名、Bundle Identifier、图标路径都做成 Rakefile 里的可配置常量,毕竟 RubyMotion 最大的优势就是构建脚本是真正的代码,这份灵活性不用白不用。
下一篇我打算聊聊怎么用 RubyMotion 做更复杂的网络层封装和离线存储,大家有遇到 RubyMotion 的奇葩编译问题,欢迎在留言区一起交流。