一、热更新适用场景
热更新主要用于无需用户重新下载安装 App的场景,常见如下:
- 紧急修复线上 Bug,尤其是影响核心功能或用户体验的严重问题,避免等待应用商店审核周期。
- 快速迭代小功能或调整 UI 细节,比如节日活动界面切换、按钮文案修改,灵活应对短期需求。
- 动态配置业务规则,例如电商 App 调整商品折扣策略、内容 App 更新推荐算法参数,无需发版即可生效。
二、OC 热更新:基于动态运行时(Runtime)+ JSPatch(经典方案)
OC 热更新核心依赖动态消息转发机制,可通过 JSBridge(如 JSPath、WCDB)实现远程下发脚本修复 Bug,无需发版。
2.1.适用场景
- 紧急修复线上 Crash,如某按钮点击事件因数组越界崩溃。
- 临时调整业务逻辑,如关闭某活动入口(无需等待应用商店审核)。
2.2.代码示例(以 JSPath 修复按钮点击崩溃为例)
2.2.1.OC 原生代码(有 Bug 版) 假设HomeViewController的btnClick方法因未判断数组为空导致崩溃:
objective-c
// HomeViewController.m
#import "HomeViewController.h"
@implementation HomeViewController
- (IBAction)btnClick:(UIButton *)sender {
NSArray *data = nil; // 线上环境可能因接口异常返回nil
NSString *text = data[0]; // 直接取值,必崩溃
sender.titleLabel.text = text;
}
@end
2.2.2.热更新脚本(远程下发的 JS 代码) 通过 JSPath 重写btnClick方法,添加空值判断,修复崩溃:
javascript运行
// 远程下发的JS脚本(修复逻辑)
defineClass("HomeViewController", {
// 重写btnClick方法,覆盖原生实现
btnClick: function(sender) {
var data = nil; // 模拟原生代码的变量
// 新增空值判断,避免崩溃
if (data && data.length > 0) {
var text = data[0];
sender.setTitle_forState(text, 0); // 调用OC的setTitle:forState:方法
} else {
sender.setTitle_forState("数据加载中", 0); // 友好提示
}
}
});
**2.2.3.OC 端集成 JSPath(加载远程脚本)**在 App 启动时请求远程脚本并执行,实现热更新:
objective-c
// AppDelegate.m
#import "AppDelegate.h"
#import <JSPath/JSPath.h> // 导入JSPath库
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 1. 请求远程热更新脚本(实际项目需加缓存、签名校验)
NSURL *scriptURL = [NSURL URLWithString:@"https://your-server.com/fix-home-btn.js"];
NSData *scriptData = [NSData dataWithContentsOfURL:scriptURL];
if (scriptData) {
NSString *script = [[NSString alloc] initWithData:scriptData encoding:NSUTF8StringEncoding];
// 2. 执行JS脚本,生效热更新
[JSPath evaluateScript:script];
}
return YES;
}
@end
2.3.OC 线上 Crash 修复(Release 环境,基于 JSPatch)
该方案适用于线上紧急修复(如按钮点击崩溃、接口数据解析错误),通过远程下发 JS 脚本覆盖原生方法,无需等待应用商店审核。
详细集成步骤(共 6 步,含安全校验)
2.3.1.集成 JSPatch SDK(手动导入)
- 从 JSPatch 官网(https://jspatch.com/ )下载最新 SDK,解压后得到
JSPatch.framework。 - 打开 Xcode 项目,将
JSPatch.framework拖拽到项目中,勾选Copy items if needed和对应的 Target,点击Finish。 - 进入项目的
Build Phases→Link Binary With Libraries,确认JSPatch.framework已添加,且状态为Required。
2.3.2.配置 Info.plist(开启网络权限)
-
右键点击
Info.plist→ 选择Open As→Source Code,添加以下代码(允许 HTTP 请求,用于测试;正式环境建议用 HTTPS):xml
<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict>
2.3.3.添加安全校验(防止恶意脚本,必做)
-
为避免第三方伪造脚本,需对远程 JS 脚本进行签名校验。在服务器端用私钥对 JS 脚本签名,客户端用公钥验证。
-
客户端代码(在
AppDelegate.m中添加公钥):objective-c
#import "AppDelegate.h" #import <JSPatch/JSPatch.h> @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 设置JSPatch的公钥(对应服务器私钥) [JSPatch setupKey:@"你的RSA公钥字符串"]; return YES; } @end
2.3.4.编写有 Crash 的 OC 原生代码
-
假设
OrderViewController的payBtnClick方法因未判断字典为空导致崩溃:objective-c
// OrderViewController.h #import <UIKit/UIKit.h> @interface OrderViewController : UIViewController - (IBAction)payBtnClick:(UIButton *)sender; @end // OrderViewController.m #import "OrderViewController.h" @implementation OrderViewController - (IBAction)payBtnClick:(UIButton *)sender { NSDictionary *orderInfo = nil; // 线上接口异常时返回nil NSString *orderId = orderInfo[@"order_id"]; // 直接取值,必崩溃 [self requestPayWithOrderId:orderId]; } - (void)requestPayWithOrderId:(NSString *)orderId { // 支付请求逻辑 } @end
-
编写并上传热更新 JS 脚本到服务器
-
编写修复 Crash 的 JS 脚本(添加空值判断),命名为
fix_order_crash.js:javascript运行
// 重写OrderViewController的payBtnClick方法,覆盖原生实现 defineClass("OrderViewController", { payBtnClick: function(sender) { var orderInfo = nil; // 模拟原生变量 // 新增空值判断,避免崩溃 if (orderInfo && orderInfo.order_id) { var orderId = orderInfo.order_id; self.requestPayWithOrderId(orderId); // 调用原生支付方法 } else { // 弹出提示,告知用户订单信息加载失败 var alert = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles( "提示", "订单信息加载失败,请重试", nil, "确定", nil ); alert.show(); } } }); -
在服务器端用私钥对
fix_order_crash.js签名,生成签名文件(如fix_order_crash.js.sig),与 JS 脚本一同放在服务器目录(如https://your-server.com/hotfix/)。
-
-
客户端加载并执行热更新脚本
-
在
AppDelegate.m中添加代码,启动时请求远程脚本并执行:objective-c
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [JSPatch setupKey:@"你的RSA公钥字符串"]; // 1. 异步请求远程JS脚本和签名文件 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 请求JS脚本 NSURL *jsURL = [NSURL URLWithString:@"https://your-server.com/hotfix/fix_order_crash.js"]; NSData *jsData = [NSData dataWithContentsOfURL:jsURL]; // 请求签名文件 NSURL *sigURL = [NSURL URLWithString:@"https://your-server.com/hotfix/fix_order_crash.js.sig"]; NSData *sigData = [NSData dataWithContentsOfURL:sigURL]; if (jsData && sigData) { // 2. 验证签名并执行脚本(签名不通过则不执行) BOOL success = [JSPatch evaluateScriptWithData:jsData signature:sigData]; if (success) { NSLog(@"热更新脚本执行成功"); } else { NSLog(@"热更新脚本签名验证失败,拒绝执行"); } } }); return YES; } -
测试:将有 Crash 的 App 安装到设备,启动后会自动加载 JS 脚本,点击 "支付按钮" 时不再崩溃,而是弹出提示,修复生效。
-
三、Swift热更新
在 Swift 开发中,热更新(无需重新 App Store 审核即可动态更新部分功能)是一个常见需求。结合「Inject(Debug 环境实时调试 JavaScriptCore(Release 环境)」的方案,可以兼顾开发效率(实时调试)和线上热更能力(安全合规),以下是具体实现思路和细节:
3.1.Debug 环境:用 Inject 实现实时代码注入
Inject 是一款 iOS 开发工具,支持在 Debug 模式下实时注入代码更改(无需重新编译 / 运行),适合快速调试 UI 逻辑或业务代码,本质是通过动态库(dylib)注入实现代码替换。
1. Inject 原理
Inject 通过在 Xcode 构建时生成动态库,将修改后的代码编译为 dylib 并注入到运行中的 App 进程,替换原有类 / 方法的实现,实现「修改即生效」的效果。
2. 配置步骤
(1)安装 Inject
通过 Homebrew 安装:
bash
brew install inject
(2)Xcode 项目配置
-
在项目的
Build Phases中添加New Run Script Phase,输入脚本:bash
if [ "$CONFIGURATION" = "Debug" ]; then /usr/local/bin/inject -i "你的的App Bundle ID" -s "${SRCROOT}" fi(
Bundle ID替换为你的 App 唯一标识,SRCROOT是项目根目录路径) -
确保项目的
Debug配置中,Enable Bitcode设为NO(Inject 不支持 Bitcode)。
3. 使用方式
- 运行 App 到模拟器 / 真机(Debug 模式)。
- 修改 Swift 代码(如 UI 布局、按钮点击逻辑),保存后按下
Cmd + S,Inject 会自动编译并注入更改,App 中立即生效(无需重启)。
适用场景:快速调试 UI 样式、简单业务逻辑(如按钮点击后的弹窗文案式),加速开发迭代。
3.2.Release 环境:用 JavaScriptCore 实现热更新
JavaScriptCore 是 iOS 原生框架(JavaScriptCore.framework),允许在 Swift 中执行 JavaScript 代码,通过「远程加载载本地 JS 脚本」动态更新业务逻辑,规避苹果对「动态代码热更的限制(苹果禁止动态加载原生生日志代码,但允许执行脚本)。
1. 核心原理
- 将需要热更新的业务逻辑(如数据解析、UI 配置、简单交互算)用 JavaScript 编写,存放在服务器。
- App 启动 / 运行时从服务器下载最新 JS 脚本(或读取本地缓存)。
- 通过 JavaScriptCore 桥接 Swift 与 JS:Swift 暴露原生方法给 JS(如弹窗、网络请求),JS 执行逻辑后调用原生方法更新 UI。
2. 实现步骤
(1)基础配置:导入 JavaScriptCore
swift
import JavaScriptCore
(2)定义 JS 与 Swift 的交互协议
通过 JSExport 协议暴露 Swift 方法给 JS:
swift
// 定义协议(需继承 JSExport)
@objc protocol JSBridgeExport: JSExport {
// 暴露给 JS 的方法:显示弹窗
func showAlert(title: String, message: String)
// 暴露数据给 JS:当前用户信息
var userInfo: [String: Any] { get }
}
// 实现桥接类
class JSBridge: NSObject, JSBridgeExport {
weak var viewController: UIViewController? // 持有视图控制器用于操作UI
// 实现弹窗方法
func showAlert(title: String, message: String) {
DispatchQueue.main.async {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "确定", style: .default))
self.viewController?.present(alert, animated: true)
}
}
// 用户信息
var userInfo: [String: Any] {
return ["name": "张三", "age": 25]
}
}
(3)加载并执行 JS 脚本
class HotUpdateManager {
private let context = JSContext() // JS 执行上下文
init() {
setupJSContext()
loadJSFromServer() // 从服务器加载JS
}
// 配置 JS 上下文,绑定桥接对象
private func setupJSContext() {
let bridge = JSBridge()
bridge.viewController = UIApplication.shared.keyWindow?.rootViewController
// 将桥接对象注入 JS 环境,JS 中可通过 `nativeBridge` 调用方法
context?.setObject(bridge, forKeyedSubscript: "nativeBridge" as NSCopying & NSObjectProtocol)
// 配置 JS 异常处理
context?.exceptionHandler = { _, exception in
print("JS 错误:\(exception?.description ?? "未知错误")")
}
}
// 从服务器下载并执行 JS 脚本
private func loadJSFromServer() {
// 模拟网络请求(实际项目中用 URLSession)
let jsURL = URL(string: "https://你的服务器地址/hotupdate.js")!
URLSession.shared.dataTask(with: jsURL) { [weak self] data, _, error in
guard let data = data, error == nil, let jsCode = String(data: data, encoding: .utf8) else {
print("JS 下载失败")
return
}
// 执行 JS 代码
self?.context?.evaluateScript(jsCode)
}.resume()
}
}
(4)编写 JS 脚本(热更新逻辑)
服务器上的 hotupdate.js 示例(实现一个「点击按钮后显示用户信息」的逻辑):
javascript运行
// JS 中调用原生方法
function showUserInfo() {
// 通过 native桥接对象获取用户信息
var user = nativeBridge.userInfo;
// 调用原生弹窗窗方法
nativeBridge.showAlert("用户信息", "姓名:" + user.name + ",年龄:" + user.age);
}
// 触发逻辑(比如App启动后自动执行,或通过原生生按钮点击调用)
showUserInfo();
(5)在 Swift 中触发 JS 逻辑
需要时可从 Swift 主动调用 JS 函数:
swift
// 调用 JS 中的 showUserInfo 函数
context?.evaluateScript("showUserInfo()")
3. 安全性处理
- 签名校验:对下载的 JS 脚本进行签名校验(如用 RSA 签名),防止被篡改。
- 本地缓存:首次下载后缓存 JS 到本地,网络异常时使用缓存版本。
- 白名单限制:限制 JS 可调用的原生方法,避免敏感操作(如支付、权限修改)。
3.3.方案组合与局限性
1. 组合优势
- Debug 阶段:用 Inject 实时调试,提升开发效率(无需反复编译)。
- Release 阶段:用 JavaScriptCore 实现业务逻辑热更新,避免频繁发版。
2. 局限性
- Inject:仅支持 Debug 环境,不能用于线上(依赖动态库注入,苹果审核不允许)。
- JavaScriptCore :
- 只能更新用 JS 编写的逻辑,无法修改原生 UI 控件的结构(如新增一个
UIButton)。 - 复杂逻辑(如动画、图形处理)用 JS 实现性能较差,适合简单业务(数据解析、配置控制、轻量交互)。
- 需遵守苹果审核规则:不能通过 JS 动态下载原生执行原生代码(如 Swift/Objective-C 代码),否则会被拒。
- 只能更新用 JS 编写的逻辑,无法修改原生 UI 控件的结构(如新增一个
3.4.替代方案补充
如果需要更强大的热更新能力(如动态更新原生 UI),可了解:
- React Native/Flutter:通过跨平台框架的「JS Bundle 热更新」实现(需注意苹果审核政策)。
- JSPatch/Weex:早期流行的 JS 热更框架,但苹果对动态执行代码的限制趋严,需谨慎使用。
综上,「Inject + JavaScriptCore」是一套轻量、合规的方案,适合合中小型需快速调试且线上只需更新简单逻辑的场景
四、SwiftUI 页面样式热更新(Debug 环境,基于 Inject)
该方案适用于开发阶段,无需重新编译,实时调整字体、颜色、布局等样式,提升调试效率,Release 环境不建议使用(存在安全风险)。详细集成步骤(共 5 步)
4.1.添加 Inject 依赖(通过 SPM)
- 打开 Xcode 项目,点击项目名称 → 选择
Package Dependencies→ 点击左下角+号。 - 在搜索框输入 Inject 的 GitHub 地址:
https://github.com/johnno1962/Inject,点击Add Package。 - 选择需要集成的 Target(如主 App Target),点击
Add Package完成依赖导入。
4.2.配置 Build Settings(开启热更新支持)
- 点击项目名称 → 选择对应 Target → 进入
Build Settings标签页。 - 搜索
Other Linker Flags,在Debug配置下添加-Xlinker -interposable(Release 配置无需添加)。 - 搜索
Enable Modules,确保设置为Yes(避免依赖导入报错)。
-
编写支持热更新的 SwiftUI 代码
-
在需要热更新的 View 文件中导入
Inject,并添加@ObserveInjection属性和.enableInjection()修饰符:swift
import SwiftUI import Inject struct ProfileView: View { // 1. 监听代码变化,触发热更新 @ObserveInjection var inject // 2. 可热更新的样式变量(修改后实时生效) private let titleColor: Color = .blue // 改为.red后,保存即刷新 private let titleFont: Font = .system(size: 20) // 改为24后实时变大 var body: some View { VStack(spacing: 30) { Text("我的主页") .font(titleFont) .foregroundColor(titleColor) Image(systemName: "person.circle.fill") .resizable() .frame(width: 80, height: 80) } .padding() .enableInjection() // 3. 启用当前View的热更新 } }
-
4.3.运行项目并测试热更新
- 选择
Debug模式,连接模拟器或真机,点击 Xcode 的Run按钮(▶️)。 - 项目运行后,回到 Xcode,修改
titleColor为.red,或调整titleFont的大小,按下Command+S保存文件。 - 此时模拟器 / 真机上的
ProfileView会实时刷新样式,无需重新点击 Run。
4.4.可选:排除不需要热更新的代码
-
若某段代码不想被热更新(如核心逻辑),可添加
@NonInjected属性:swift
// 该变量修改后不会热更新,需重新编译 @NonInjected private let fixedText: String = "不可修改的文本"
五、OC与Swift对比
OC 的动态运行时特性确实支持热更新,而 Swift 早期因静态编译特性难以实现,但是Swift 现在可以实现热更新功能。
Swift 可以通过一些第三方库和工具来实现热更新,例如 Inject、InjectionIII 等。
以 Inject 为例,开发者可以通过 SPM 安装该依赖库,然后在 Xcode 的 Build Settings 中给 Debug 配置添加 "-Xlinker -interposable",并在代码中进行相应的配置,如在 SwiftUI 中添加 "@ObserveInjection var inject" 和 ".enableInjection ()",即可实现热更新功能。
此外,也可以使用 JavaScriptCore 框架,通过在 Swift 应用中嵌入 JavaScript 代码来间接实现 Swift 代码的热更新。
5.1. 性能:Swift 更优,尤其在复杂计算场景
- Swift :静态编译语言,编译时会做更多优化(如类型检查、函数内联),运行速度比 OC 快约20%-40% ,复杂数据处理(如列表渲染、算法计算)时优势更明显。
- OC:动态运行时语言,执行时需通过 "消息转发" 机制查找方法,额外消耗性能,在高并发或密集计算场景下表现较弱。
5.2. 安全性:Swift 从语法层面规避风险
- Swift :强制类型安全,不允许隐式类型转换(如
int不能直接转string);默认变量非空(non-optional),从根源减少 "空指针崩溃",这是 OC 崩溃的高频原因。 - OC :类型检查宽松,支持隐式转换;指针和
nil使用灵活但无强制约束,需开发者手动处理空值,容易因疏忽引发崩溃。
5.3. 开发效率:Swift 更简洁,OC 兼容成本高
- Swift :语法更简洁(如省略分号、用
let/var声明变量、闭包简化写法),同等功能代码量比 OC 少30%-50% ;自带Optionals、Generics等现代特性,减少重复代码。 - OC:语法繁琐(如方法声明需写完整参数名、必须导入头文件);不支持泛型、元组等特性,实现复杂逻辑时需写更多 "模板代码";且与 Swift 混编时需维护桥接文件,增加适配成本。
5.4. 生态与兼容性:OC 兼容老项目,Swift 是未来主流
- Swift:Apple 官方主推,每年更新版本(如 Swift 6 将引入更多性能优化),新系统特性(如 SwiftUI、WidgetKit)优先支持 Swift;但早期版本(Swift 3 前)兼容性差,升级需修改大量代码。
- OC:生态成熟,几乎所有老项目、第三方库(如早期 SDK)都基于 OC 开发,适合维护 legacy 项目;但 Apple 已不再为 OC 新增核心特性,长期看会逐渐被 Swift 替代。
5.5. 内存管理:Swift 更智能,OC 依赖手动规范
- Swift :默认使用
ARC(自动引用计数),且对内存循环引用的处理更友好(如weak修饰符使用更直观,闭包中可通过[weak self]快速避免循环引用)。 - OC :虽也支持
ARC,但早期项目可能存在MRC(手动引用计数)代码,需手动管理retain/release;且 block(类似 Swift 闭包)的内存循环引用逻辑更复杂,新手易踩坑。
// 初始逻辑:a + b
function dynamicAdd(a, b) {
return a + b; // 调用后返回8
}
// 热更新后逻辑:a * b(无需修改Swift代码,仅更新JS脚本)
// function dynamicAdd(a, b) {
// return a * b; // 调用后返回15
// }