You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
asyncfunctionsyncInternal(options={},syncStatusChangeCallback,downloadProgressCallback,handleBinaryVersionMismatchCallback){constsyncOptions= ...
// 流程监听处理syncStatusChangeCallback= ...
try{// 通知 Native 端已就绪,清理 pending packageawaitCodePush.notifyApplicationReady();// 获取更新const remotePackage =awaitcheckForUpdate(syncOptions.deploymentKey,handleBinaryVersionMismatchCallback);// 定义下载安装constdoDownloadAndInstall=async{ ... }// 是否应该忽略当前 packageconstupdateShouldBeIgnored=awaitshouldUpdateBeIgnored(remotePackage,syncOptions);if(!remotePackage||updateShouldBeIgnored){if(updateShouldBeIgnored){log("An update is available, but it is being ignored due to having been previously rolled back.");}
...
}else{// 开始下载安装returnawaitdoDownloadAndInstall();}}catch(error){
...
}};
在 OTA 后台可以查看所有资源下发、安装更新情况,以及支持针对特定设备或用户推送更新的前提下,加入了 build version 作为是否 failed package 的判断逻辑。当某个特殊用户或设备安装更新失败时,而当前更新包又确定是正常的时候,就可以下发一个 build version 更大的包,这样就可以继续沿用现有的 package(hash 值不变),再次下发。
最近在托哥的带领下,参与了小鹏 App 的技术专项开发,主要也是因为公司上市后,部门和业务都有相应的变化。为了提供更高的交付效率,提出了服务营销效能交付的技术专项。主要包括:
目前已经启动的是在搭建热更新体系,现已进入与 OTA 平台联调阶段。本篇主要针对搭建过程中,对 JavaScript 和 iOS 端改造的一些简单记录。
CodePush热更流程简述
CodePush 在 JavaScript 端暴露的接入方式很简单,只需要在入口文件调用
CodePush.sync(syncOptions)(App)
即可,实际该函数是一个高阶函数,其内部最终调用的是 syncInternal 函数,因此可以从该入口函数开始源码阅读之旅。进入 syncInternal 函数可以发现,整个 CodePush 的流程状态就是在这里定义的,不过可以先忽略,重点关注主要流程。以下为流程简单摘要:简单梳理:
notifyApplicationReady
:用来清理 Native 端的 pending package,这里涉及到 CodePush 的回滚逻辑:loadBundle
方法,重新进入[CodePush bundleURL]
流程(有 update package 则加载,无则加载 binary version )checkForUpdate
:开始检查更新,会获取本地配置信息,比如版本号、deployment key 和 package hash,作为请求参数传到服务端。其返回的 remotePackage 是与本地 package (如有)合并的结果,其中包含了 isFailedPackage 的关键信息,会作为是否忽略当前 package 的依据shouldUpdateBeIgnored
:是否忽略当前 package,里面的判断涉及到 isFailedPackagedoDownloadAndInstall
:检查更新返回的 package 符合要求,进入下载和安装流程,主要工作在 Native 端:当一个 package 安装成功之后,沙盒中会存在 /Library/Application Support/CodePush/ 目录,其完整内容:
如上,每个 package 对应一个 hash 目录,codepush.json 中保存了当前以及上一个(如有) package 的信息。package 内的 app.json 文件,存有该 package 所有的信息:
下一步,就是 package 加载的环节。小鹏 App 的热更策略是下次启动生效,所以 package 安装成功之后是不做处理的。当 App 下次启动时,会进入
[CodePush bundleURL]
的逻辑,其主要是判断沙盒中是否有新 package 的信息,如有则返回该文件路径;否则返回 main bundle 的路径,即当前运行的是 binary version。整个热更流程基本如上所述,更详尽的内容可以直接参考源码。
App背景
小鹏 App 属于混编 App,支持 bundle 分包和按需加载。由于 CodePush 并不支持分包热更,经过一番技术预研后,需要对其进行针对性改造,涉及 JavaScript 和 Native 两端。改造之前,需要明确的一些关键点:
以上基本规则在改造过程中具备一定的指导意义,最终源码也只针对这些场景,所以会与官方流程有比较大的出入。总结下来,会有以下几个改造点:
技术改造
注册unbundle
小鹏 App 之前已经支持了分包,可以按需加载不同业务 bundle,其中比较关键的流程是:
- sourceURLForBridge:
中返回 main.unbundle 的路径上面已经提到,当前 package 信息会存在于沙盒中的某个目录,unbundle 注册的首要任务其实就是获取正确的路径,然后沿用现有的逻辑进行注册。CodePush 内部已经有获取当前 package 路径的方法,可以增加接口将其暴露出来:
上面 packageFile 返回的是 main.unbundle 的路径,而我们需要的文件所在的目录,用于拼接其他业务 unbundle,所以这里只要返回目录即可。最终现有 RNBundleLoader.m 修改:
独立验签过程
验签抽离的思路其实很简单,不过会涉及 JavaScript 和 Native 两端的改造:
首先是 Native 端,原有方法的下载结束回调中没有回传信息,与改造后的流程不大相符,我们是需要拿到验签用的信息,然后回传给 JavaScript 端的,故弃用原有的下载方法:
JavaScript 端也比较简单,在拿到更新之后的验签信息后,手动调用 Native 端的验签处理。这里略去了一些异常情况的处理,只列出最主要的相关改动:
syncStatusChangeCallback
沿用 CodePush 的状态同步处理,SIGNATURE_START、SIGNATURE_SUCCESS 都是新增的状态,当然还有其他。除此之外,CodePush 模块还要新增验签的桥接方法,其内容基本与原有验签逻辑一致,这里便不再细述。忽略下载失败
CodePush 原有逻辑中,当 package 在下载、更新、验签的过程中,只要其中一个环节出现错误,都将当成一个 failed package,在
shouldUpdateBeIgnored
判断中会被作为忽略的 package。由于下载过程中场景的多样性,package 在下载安装过程中出现的错误,都不作为其无效的依据,取而代之的,是在验签过程中的成功与否。所以这里涉及两处改动:
safeFailedUpdate
的处理signatureVerification
中,如果有环节失败,那么就标记为 failed package在 CodePush.m 中:
以上只列出部分示例,更具体的可参考修改后的 CodePush.m 源码。
修改isFailedUpdate逻辑
在资源下发的过程中,面对众多的机型设备,不排除存在兼容问题,导致在更新包下载安装成功之后,但是验签过程当中失败了,或是验签完成但下次启动时,没有正常运行 JavaScript bundle,进入了回滚逻辑,这些情况下,都会把当前包作为 failed package 保存到本地。
在 OTA 后台可以查看所有资源下发、安装更新情况,以及支持针对特定设备或用户推送更新的前提下,加入了 build version 作为是否 failed package 的判断逻辑。当某个特殊用户或设备安装更新失败时,而当前更新包又确定是正常的时候,就可以下发一个 build version 更大的包,这样就可以继续沿用现有的 package(hash 值不变),再次下发。
相关改动:
回滚逻辑调整
CodePush 的原始逻辑中,当下载并运行了一次回滚包之后,再次启动会进入回滚逻辑,主要有两个关键步骤:
在删除掉当前回滚包的文件目录后,会重新 reload bundle。测试童鞋在验证回滚功能的时候,当运行一次回滚包之后,再次启动时 App 会闪退。按上面分析,实际就是再次启动时,进入了回滚的操作,一般文件删除不会有问题,猜测问题应该出现在 reload bundle 上。
经过本地调试,基本可以确定这个猜想。与托哥确认后,小鹏 App 在进行分包之后,不支持 bundle 的 reload(暂时未能理解其技术原理,需深入学习),现 Android 端在回滚时并没有进行 bundle 的重新加载,只是单纯的删除目录,而且回滚的操作发生在返回 bundle URL 路径之前。参照 Android 端的处理,直接去掉
[self loadBundle]
,iOS 端回滚功能正常跑通。这番修改之后,虽然功能正常运行,但自己依然有疑问:在注册完 main.unbundle,进入 CodePush 流程删除了本地回滚包且没有触发 reload 的情况下,App 当前运行的是哪里的 business.unbunde ?什么时候注册的?
先梳理现阶段的流程:
以上基本就是一个从 ReactNative 初始化,到分包加载的完整流程,其中也列出了需要特别关注的 CodePush 模块初始化。这里面有几个比较关键的步骤:
1.2
:返回 main.unbundle 路径,可能为 binary version,或是更新包的路径。很明显,这是回滚包的文件目录2.4、2.5
:CodePush 模块的初始化,里面做了包的回滚操作,在该步骤之后,本地已经没有回滚包的目录2.6、2.7、3.1
:一个队列组的操作,在 NativeModule 初始化完毕、main.unbundle 运行完毕后,触发 RCTJavaScriptDidLoadNotification 通知,进行 business.unbundle 注册,需要特别注意的是,此时所有 business.unbundle 的目录前缀已经是 binary version 或是上一个热更包的路径总结就是:内存中注册的 main.unbundle ,实际位于回滚包中,而后续注册的 business.unbundle,是属于上一个可运行版本的包中的!由于拆包之后, App 自身和回滚包中的 main.unbundle 并无二异,所以当前流程回滚后 App 运行是正常的。
实际上并不能保证两者中的 main.unbundle 是一致的,有可能某个更新包中修改了 main.unbundle,而 business.unbundle 又依赖于该改动,那么以上流程就会有问题。为了从根源上规避这种情况的发生,iOS 端调整了回滚逻辑,保持与 Android 端一致:在返回 main.unbundle URL 之前,进行回滚操作,如有回滚包则删除,这样可以确保返回的 URL 是上一个可用的版本。
涉及的改动:
-initializeUpdateAfterRestart
中的处理是必须移除的,因为一个更新包下载安装之后,刚刚提前的回滚判断逻辑会将该 pending update 里面的 isLoading 置为 true,如果此时又进入了initializeUpdateAfterRestart
中的回滚判断,就会被当成回滚包直接删除了,最终结果就是每次启动 App 都会去下载安装更新包。总结
以上,基本就是这次大部分的改造了,当然这里没有列出事件上报的内容。事件上报需要关注的,是在什么时候上报,需要上报什么内容,比如回滚包信息、下载中断时的进度等等,在一个流程比较清晰的前提下,其他的源码逻辑其实都比较简单,这里就不再赘述了。
目前测试下来,基本跑通了测试童鞋的所有用例,效果是比较乐观的。功能上线之后,后续应该关注每次更新活动中,安装更新成功或是失败的占比,还有上报到 OTA 的错误信息,以便做针对性的调休和流程的优化升级。
The text was updated successfully, but these errors were encountered: