背景
面试的是一家做智能硬件的科技公司,技能要求除了音视屏、iOS/Flutter性能相关技术外,更多的还关注个人对常用开发技巧是否有探索性,下面列举几个在开发中我比较容易忽略的技能点。
面试题
- 问题1:「同一个项目中如何跑多个域名」
答:- 问题的本质:
这个问题真正考量的是怎么让马甲包不被机审干掉。, - 错误的回答:我的回答是:
新增Target->不同的BundleID->按ID选择不同的baseUrl, 做文件和代码的混淆,这个回到方向是对的,但是没有讲透,我的回答是一套「一套代码跑多个包」的标准做法,项目混淆做马甲包,会被机审判别为马甲包而触发Guideline 4.3(重复应用)。且混淆会增加测试的工作量,如果在同一个工程里手动改代码混淆,每个包都要重新全量回归 → 成本爆炸。 - 核心矛盾:审核4.3相似度检测主要看这些维度
1.1、二进制代码相似度(Mach-o里的类名,方法名,函数调用)
1.2、资源文件Hash(图片、xib、storyboard、json)
1.3、符号表/字符串常量
1.4、UI结构和页面跳转的关系
1.5、账号、ip、上传设备指纹等关联信息。
- 问题的本质:
多 Target 方案的代码是 100% 相同的,只是编译期换了 BundleID 和几个常量。机审一扫,代码指纹几乎一致 → 秒判马甲。「切域名」只是最表层需求,真正的难点是让每个包看起来像不同团队独立开发的 App。
正确回答:行业中最新的做法--用自动化的等价变换换掉代码指纹,而不是通过人肉去更改。面试官想知道的是自动化「同构异性」,也就是脚本生成内容相同,文件和函数不同的项目—— 这是马甲包工作室的核心资产。逻辑不变、指纹全换
// 自动化异构前
class LoginViewController: UIViewController {
// 自动化异构后
class AbcXyzWidget: UIViewController {
// 逻辑完全一致,只换指纹
// 自动化异构前
func handleLogin() {
// 自动化异构后
func p9x_doZk() { // + 插入垃圾函数、打乱顺序
// 自动化异构前
let url = "https://api.a.com"
// 自动化异构后
let url = decrypt(kX9) // 字符串加密
}
}
所以问题的本质是两个层面:
1.1、多环境切换域名
单纯的多域名/多环境的切换可以用Target+xcconfig+编译宏来区分,运行时按照配置选择baseUrl,这是标准的多环境方案。
1.2、真正的马甲包(避免4.3审核指南)
但要上多个马甲包过审,多个Target共用一份二进制会被相似度检测为重复应用,当前行业的做法「用脚本对母工程做自动化 同构异形」,批量重命名类、方法、插入无用的代码、加密字符串、资源改哈希、参数化换壳,因为都是语义上的等价变换,只需要回归母包一次,批量生成马甲包的默认功能一致的,不需要额外增加测试量。
- 问题2、「iOS和flutter项目中多语言配置阿拉伯语,是如何进行适配的?」
答:
iOS端原生适配阿拉伯语:
1、首先和其他地区的文本配置一样,创建ar.lproj本地化文件夹,再放置到 Localizable.strings中。
在iOS中Auto Layout 布局适配天然支持RTL(right-to-left),核心规则:1、不要用left/right,而是用leading/trailing替代,2、leading在LTR语言中表示left,但在RTL语言中等于right。
第三方库SnapKit用法和原理是一样的。
总结:iOS对RTL布局提供了系统级的支持,开发者的工作就是正确使用API和布局方式,让系统自动完成翻转。
1、布局基础:使用Auto Layout 或SnapKit的语义约束,关键一步是用leading和trailing属性来替代left和rignt,当系统语言切换到阿拉伯语时,leading会从左边变为右边,trailing也会相应的变化,从而实现自动翻转。
Flutter端原生适配阿拉伯语:
Flutter对RTL的支持也同样出色,它的设计理念就是“适配框架”,开发者只需要遵循规范即可。
1、文本和布局方向:Flutter是通过Directionality组件来控制文本和布局方向的,它会根据当前的语言环境,自动设置textDirection.rtl,当设置是阿拉伯语言时,MaterialApp会自动实现布局的自动翻转。
2、在Row和Column组件中MainAxisAlignment.start/end 和 CrossAxisAlignment.start/end 来代替 left/right。
总结:无论是leading/trailing和left/right还是flutter中的start/end和left/right他们的区别都是坐标系的不同。
- 问题3、如何处理Flutter项目集成到iOS端在启动时出现白屏或首次加载Flutter页面时出现白屏的问题
答:
这是一个景点的Flutter混合开发问题,白屏的本质是Flutter Engine初始化和首帧渲染之前的时间差造成的。
白屏的两个阶段:
1、Engine初始化时白屏:
从创建FlutterEngine或FlutterViewController到Engine就绪,Flutter还没有渲染的能力,Native侧也没有提供任何的占位试图,所以显示系统默认的白色背景。
2、首帧渲染前的闪白
Engine就绪后Flutter首帧上屏之间,如果Flutter侧runApp()之前有任何的耗时操作,会延长这段空白。
解决方式:
方案1、预初始化FlutterEngine,在AppDelegate中提前创建并预热Engine
// AppDelegate.swift
import Flutter
@main
class AppDelegate: FlutterAppDelegate {
// 持有 Engine 防止被释放
lazy var flutterEngine = FlutterEngine(name: "my_flutter_engine")
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// 启动时预热 Engine
flutterEngine.run()
// 注册插件(如果有)
GeneratedPluginRegistrant.register(with: flutterEngine)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
方案2:使用Flutter闪屏(Splash screen),在Eengine初始化期间显示一个和FlutterUI一致的占位图。首帧渲染完成后再移除闪屏。
问题4:动态库和静态库的区别
答:
1、链接的时机不一样:静态库在编译链接阶段合并到主程序的,动态库是在运行时动态加载的
2、静态库代码会被完整的赋值到每个使用到它的可执行文件中,而动态库只会存储一份,多进程共享内存中的同一份副本。
3、静态库会导致App体积增大,但启动无需额外的加载,动态库体积小,动态库会增加启动加载的开销,切节省运行时内存
4、静态库常见以.a.framewofk。动态库常见以.framewofk/.dylib/.tbd
问题5:iOS 中如何 防止第三方库重复导入冲突,比如一个项目手动导入AFN,我的SDK是pod集成AFN,版本不一致,如何解决这个冲突
答:
核心的矛盾点是两个相同的符号在链接阶段的冲突,同时运行可能加载的错误的版本导致API不兼容。
方案一:符号重命名,将SDK内部使用的AFN所有类名、函数名、重命名,从源头消除冲突。比如使用前缀重命名(脚本自动化批量重命名)。
方案二:framework隔离,将SDK打包成动态库Dynamic Framework,运行各自加载自己的版本,无不干扰。
问题6:iOS如何处理让build编译得更快
答:iOS中构建时的编译速度的系统性方法,涵盖了从配置项到架构优化的各个层面的优化。
1、build setting项调优
需要在Debug环境下添加以下配置项:
- SWIFT_COMPILATION_MODE设置为Incremental(只重编译变更文件,发布时用 Whole Module 做全量优化)。
- DEBUG_INFORMATION_FORMAT设置为dwarf(也就是不带.DSYM,跳过DSYM的生成,可以减少Debug的构建时间)
- SWIFT_OPTIMIZATION_LEVEL设置为none(不做耗时的优化,编译就会变得很快)
2、依赖和模块化 - 1、二进制化第三方库:对稳定不变的库 可以使用:binary => true。
- 2、模块化拆分,模块化解耦可最大化并行编译,不过要避免过深的依赖和循环依赖。
3、 构建缓存和产物的复用 - DerivedData的管理:不要频繁去清理DerivedData。
- 预编译头文件:oc项目中合理去使用PCH,减少头文件的重复解析。
4、构建阶段的优化 - 清理 Build Phases 脚本:将脚本从 Run Script阶段迁移到CI/CD阶段,或者是手动触发。
- 资源的处理:合并大量小图片,减少不必要的资源复制阶段。
问题6:IAP和ApplePay的实现步骤
- IAP
// ============================================================================
// 【完整实现思路总览 —— 含非代码步骤】
// ============================================================================
//
// 一、App Store Connect / 账号侧(非代码,必须先做)
// --------------------------------------------------------------------------
// 1. 在 Apple Developer 开通「Paid Applications Agreement」(付费应用协议),
// 并填好银行/税务/联系信息。否则商品会一直「缺少元数据/不可用」。
// 2. 在 App Store Connect 创建 App(Bundle ID 必须与 Xcode 工程一致,
// 本工程示例:com.yeahka.xTool)。
// 3. 进入 App →「功能 / App 内购买项目」创建商品:
// - 消耗型:如 com.yeahka.xTool.coins.100
// - 非消耗型:如 com.yeahka.xTool.removeads
// - 自动续期订阅:先建「订阅群组」,再建订阅商品
// 4. 每个商品填写:参考名称、价格、本地化描述、审核截图等,并提交审核
// (商品可随版本审,也可单独审,视后台状态而定)。
// 5. 创建 Sandbox 测试账号:用户与访问 → 沙盒 → 测试员。
// 真机测试时:设置 → App Store → 沙盒账户 登录(不要在系统 iCloud 里登沙盒号)。
// 6. 若做订阅:配置订阅群组、等级、免费试用/推介促销(可选)。
// 7. (强烈建议)配置 App Store Server Notifications V2 回调地址,
// 用于服务端感知退款、续订失败、宽限期等,避免只靠客户端。
//
// 二、Xcode 工程侧(非业务代码,但要配)
// --------------------------------------------------------------------------
// 1. Signing & Capabilities → + In-App Purchase。
// 2. Bundle Identifier 与 Connect 一致。
// 3. 真机调试(模拟器对 IAP 支持不完整,建议真机 + Sandbox)。
// 4. 部署目标:本文件使用 StoreKit 1(SKPaymentQueue),兼容 iOS 11+。
// 若最低版本 ≥ iOS 15,可逐步迁移 StoreKit 2(Product / Transaction)。
//
// 三、客户端代码主流程(本类负责)
// --------------------------------------------------------------------------
// 步骤 A:App 启动最早时机 addTransactionObserver
// —— 这是防漏单的第一道防线。未 finish 的交易会在下次启动再次回调。
// 步骤 B:请求商品信息 SKProductsRequest(拿价格、本地化标题)
// 步骤 C:发起购买 SKPaymentQueue.add(SKPayment)
// 步骤 D:在 updatedTransactions 里根据 state 分支处理
// 步骤 E:purchased / restored → 落本地 pending → 服务端验票 → 发奖
// 步骤 F:发奖成功后才 finishTransaction,并删除本地 pending
// 步骤 G:启动/回前台扫描本地 pending + 系统未完成交易,做补单
//
// 四、「漏单」与「付了没发奖」分别怎么防
// --------------------------------------------------------------------------
// 【系统漏单】钱扣了,交易还在 SKPaymentQueue,但 App 没处理完:
// → 启动就监听队列;永不在发奖成功前 finish。
// 【业务漏发】验票/发奖失败,或 finish 后发奖失败:
// → 本地 IAPPendingOrderStore 持久化 + 重试;
// → 服务端以 transactionId 幂等,允许安全重试。
//
// 五、绝对不要做的事
// --------------------------------------------------------------------------
// - 不要在客户端用「自己算的」成功状态直接发高价值道具而不验票;
// - 不要支付成功回调里立刻 finishTransaction;
// - 不要忽略 restored / 启动时队列里已有的交易;
// - 不要用生产环境收据打 Sandbox 校验地址(服务端要区分环境)。
//
// ============================================================================
- ApplePay
// ============================================================================
// 【Apple Pay 实现思路 —— 含非代码步骤】
// ============================================================================
//
// 重要概念区分:
// - IAP(StoreKit):卖「数字内容/权益」(金币、会员、去广告),苹果抽成。
// - Apple Pay(PassKit):卖「实物/服务」等(电商、线下核销),钱走你的收单行,
// 苹果不按 IAP 抽成。二者不要混用,审核会拒。
//
// 一、非代码准备
// --------------------------------------------------------------------------
// 1. Apple Developer → Certificates, Identifiers & Profiles → Identifiers
// → 选中 App ID → 勾选 Apple Pay。
// 2. 创建 Merchant ID(例如 merchant.com.yeahka.xTool)。
// 3. 为 Merchant ID 创建 Payment Processing Certificate
// (通常由支付服务商/收单行提供 CSR,或按其文档生成)。
// 4. Xcode → Signing & Capabilities → + Apple Pay,勾选对应 Merchant ID。
// 5. 与支付网关签约(Stripe / 银联 / Adyen / 自有收单等),拿到:
// - merchantId
// - 后端解密 paymentToken 的能力
// - 支持的银行卡网络(Visa/MasterCard/UnionPay...)
// 6. 真机测试:设备需登录钱包并添加可用卡;模拟器能力有限。
//
// 二、代码主流程
// --------------------------------------------------------------------------
// 1. PKPaymentAuthorizationController.canMakePayments 判断设备是否可用;
// 2. 组装 PKPaymentRequest(币种、国家、商户号、账单明细);
// 3. present 授权页,用户用 Face ID / 侧边按钮确认;
// 4. didAuthorizePayment 拿到 payment.token,发给你们服务端;
// 5. 服务端把 token 交给收单行扣款,返回成功/失败;
// 6. 用 completion(.success / .failure) 告诉系统结果,关闭授权页。
//
// 三、安全注意
// --------------------------------------------------------------------------
// - paymentToken 只能服务端处理,客户端不要尝试「自己解析扣款」;
// - 金额以服务端二次校验为准,防止被篡改 request;
// - 授权成功 ≠ 你的业务订单完成,要以收单行结果为准再发货/发物流。
//
// ============================================================================