一、Flutter动态化更新到底是啥?为啥要搞它?
很多做Flutter开发的朋友应该都有过这种糟心经历:App上线后突然发现一个影响核心功能的小bug,或者要加个临时活动页面,结果得重新打包、提交应用商店审核,等个三五天才能生效,用户早就流失了。动态化更新就是为了解决这个问题的——不用重新安装整个App,就能给已经装在用户手机里的Flutter应用加功能、改bug。
但要搞动态化,有个绕不开的大问题:安全。比如有人给你发个假的更新包,偷偷往你App里加盗刷银行卡的代码,那可就麻烦了。所以得有一套办法,保证更新包是官方发的,没被改过,这就用到了两个核心技术:签名校验和差分补丁。
二、动态化的核心安全关卡:签名校验和差分补丁
2.1 签名校验:给更新包贴“官方防伪标签”
签名校验的逻辑其实和我们平时查商品防伪码一样。官方发更新包的时候,会用自己的“私人印章”(也就是私钥)给更新包盖个章,生成一个独一无二的签名值。用户手机里的App会提前存好官方的“公开印章”(公钥),拿到更新包后,用公钥来验证这个签名值是不是真的——如果验证通过,说明更新包确实是官方发的,没被篡改过。
2.2 差分补丁:只传改了的部分,省流量还快
差分补丁的原理也很简单:比如你之前的App里有个叫“首页”的页面,原来的代码是100行,后来你改了其中10行,那差分补丁就只包含这10行改动的内容,而不是把整个首页的代码再传一遍。这样更新包的体积会特别小,用户更新的时候流量用得少,速度也快。
三、Android和iOS上的实际操作对比
3.1 先搞清楚两个平台的核心限制
在开始写代码之前,得先知道两个平台对动态化的要求不一样:
- Android:本身对代码执行的限制比较松,只要能拿到权限,随便执行下载的代码,所以动态化的实现空间很大。
- iOS:苹果管得特别严,不允许App下载并执行新的原生代码,只能用苹果允许的方式加载资源,比如Flutter的业务逻辑代码是用Dart写的,属于中间代码,苹果管得相对松一点,但也有很多限制。
3.2 签名校验的代码实现(统一用Flutter)
这里我们统一用Flutter来写签名校验的代码,这样能更清楚地对比两个平台的差异。首先得准备好官方的私钥和公钥,私钥用来给更新包签名,公钥存在App里用来验证。
// 统一技术栈:Flutter
// 功能:验证更新包的签名是否合法
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:pointycastle/export.dart';
// 1. 提前存到App里的官方公钥(实际开发中要加密存储,这里为了演示简化)
const String publicKeyPem = '''
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...(省略实际公钥内容)
-----END PUBLIC KEY-----
''';
// 2. 验证签名的核心方法
bool verifySignature(String updateContent, String signature) {
try {
// 把PEM格式的公钥转成可用的格式
Uint8List publicKeyBytes = base64.decode(
publicKeyPem.replaceAll('-----BEGIN PUBLIC KEY-----', '')
.replaceAll('-----END PUBLIC KEY-----', '')
.replaceAll('\n', ''),
);
RSAPublicKey publicKey = RSAKeyParser.parseKeyFromBytes(publicKeyBytes) as RSAPublicKey;
// 把更新包内容转成字节
Uint8List contentBytes = utf8.encode(updateContent);
// 把签名转成字节
Uint8List signatureBytes = base64.decode(signature);
// 用公钥验证签名
RSASigner signer = RSASigner(SHA256Digest(), '0609608648016503040201');
signer.init(false, PublicKeyParameter<RSAPublicKey>(publicKey));
return signer.verifySignature(contentBytes, RSASignature(signatureBytes));
} catch (e) {
print('签名验证失败:$e');
return false;
}
}
3.2.1 Android上的签名校验注意点
Android上的签名校验和上面的代码几乎一样,但有个小细节:Android 10及以上版本,App的存储权限变了,所以更新包要存在沙盒目录里,不能随便存在手机的公共目录。另外,Android允许我们用Dart代码加载更新包,只要验证通过,直接执行就行。
3.2.2 iOS上的签名校验注意点
iOS上的签名校验代码和上面完全一样,但要注意:苹果不允许我们随便加载新的Dart代码,所以更新包的内容必须是和原有App兼容的,比如只能是修改后的业务逻辑代码,不能是新增的原生功能。另外,iOS上的公钥要存在App的Info.plist里,或者用钥匙串加密存储,不能硬编码在代码里(硬编码会被苹果审核拒绝)。
3.3 差分补丁的代码实现(统一用Flutter)
差分补丁的实现我们用一个叫“bsdiff”的工具,它可以生成两个文件之间的差分补丁,也可以用补丁还原原来的文件。
// 统一技术栈:Flutter
// 功能:用差分补丁还原更新后的内容
import 'dart:io';
import 'package:bsdiff/bsdiff.dart';
// 1. 用旧文件和差分补丁生成新文件
Future<File> applyPatch(File oldFile, File patchFile, File newFile) async {
try {
// 读取旧文件的内容
Uint8List oldContent = await oldFile.readAsBytes();
// 读取差分补丁的内容
Uint8List patchContent = await patchFile.readAsBytes();
// 用补丁生成新内容
Uint8List newContent = Bsdiff.apply(oldContent, patchContent);
// 把新内容写入新文件
await newFile.writeAsBytes(newContent);
return newFile;
} catch (e) {
print('应用差分补丁失败:$e');
rethrow;
}
}
3.3.1 Android上的差分补丁注意点
Android上的差分补丁实现很顺利,只要能拿到旧文件和补丁文件的路径,直接调用上面的方法就行。另外,Android允许我们把新生成的文件放在App的私有目录里,下次启动App的时候,直接加载这个新文件就行。
3.3.2 iOS上的差分补丁注意点
iOS上的差分补丁有个大问题:苹果不允许App动态加载新的可执行文件,所以差分补丁只能用来修改App的资源文件,比如图片、文字、布局,不能用来修改核心的业务逻辑代码。如果要修改业务逻辑代码,必须用苹果允许的方式,比如把更新包做成一个新的Dart库,然后用动态加载的方式加载,但这种方式很容易被苹果审核拒绝。
四、实际应用场景、优缺点和注意事项
4.1 应用场景
- 紧急bug修复:比如App的支付功能突然出问题,不用重新打包,直接发个更新包修复。
- 临时活动页面:比如电商App要搞个618活动,不用重新提交审核,直接发个活动页面的更新包。
- 功能灰度测试:比如新开发的功能只给部分用户测试,不用重新打包,直接给测试用户发更新包。
4.2 技术优缺点
优点
- 速度快:不用重新安装App,用户体验好。
- 成本低:不用重新提交应用商店审核,节省时间和人力成本。
- 灵活:可以随时修改App的内容,不用受应用商店审核的限制。
缺点
- 安全风险:如果签名校验被破解,更新包被篡改,会给用户带来安全隐患。
- 平台限制:iOS上的动态化限制很多,实现难度大。
- 兼容性问题:如果更新包和原有App不兼容,会导致App崩溃。
4.3 注意事项
- 签名校验一定要做:这是保证更新包安全的核心,不能省略。
- 公钥要加密存储:不能硬编码在代码里,防止被别人拿到后篡改更新包。
- 差分补丁要做兼容性测试:更新包必须和原有App兼容,不能随便修改核心代码。
- 要遵守平台规则:iOS上的动态化不能违反苹果的规则,不然会被下架。
五、总结
Flutter动态化更新是个很实用的技术,能帮我们解决很多问题,但也带来了很多安全挑战。签名校验和差分补丁是动态化的核心技术,两个平台的实现逻辑差不多,但因为平台规则的不同,实际操作起来差异很大。Android上的动态化实现比较简单,iOS上的实现难度大,限制多。
在实际开发中,我们要根据自己的需求和平台规则,选择合适的动态化方案,同时要做好安全防护,保证用户的安全。
评论
围绕“Flutter动态化更新落地的安全挑战,签名校验与差分补丁在Android和iOS上的双端实践对比”参与讨论