Android热更新之微信Tinker集成接入Bugly热更新

最近公司项目中需要集成热更新功能,由于刚开始接入的时候踩了很多坑,所以现在记录一下集成的过程. 集成过程参考了Bugly官方文档热更新使用指南https://bugly.qq.com/docs/user-guide/instruction-manual-android-hotfix/?v=20170815114059热更新是Bugly为解决开发者紧急修复线上bug,而无需重新发版让用户无感知就能把问题修复的一项能力。Bugly目前采用微信Tinker的开源方案,开发者只需要集成我们提供的SDK就可以实现自动下载补丁包、合成、并应用补丁的功能,提供了热更新管理后台让开发者对每个版本补丁进行管理。 为什么使用Bugly热更新? 无需关注Tinker是如何合成补丁的 无需自己搭建补丁管理后台 无需考虑后台下发补丁策略的任何事情 无需考虑补丁下载合成的时机,处理后台下发的策略 提供了更加方便集成Tinker的方式 通过HTTPS及签名校验等机制保障补丁下发的安全性 丰富的下发维度控制,有效控制补丁影响范围 提供了应用升级一站式解决方案第一步:添加插件依赖 工程根目录下“build.gradle”文件中添加: buildscript { repositories { jcenter() } dependencies { // tinkersupport插件, 其中lastest.release指拉取最新版本,也可以指定明确版本号,例如1.0.4 classpath"com.tencent.bugly:tinker-support:latest.release"//拉取的是最新版本 } }这里写代码片 第二步:集成SDK gradle配置 在app module的“build.gradle”文件中添加: android { defaultConfig { ndk { //设置支持的SO库架构 abiFilters 'armeabi' //, 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a' } } } dependencies { compile "com.android.support:multidex:1.0.1" //其中latest.release指代最新版本号,也可以指定明确的版本号,例如1.2.0 compile'com.tencent.bugly:crashreport_upgrade:latest.release' //其中latest.release指代最新版本号,也可以指定明确的版本号,例如2.2.0 compile'com.tencent.bugly:nativecrashreport:latest.release' }在app module的“build.gradle”文件中添加: // 依赖插件脚本 apply from: 'tinker-support.gradle'tinker-support.gradle内容如下所示:注:需要在app module下创建tinker-support.gradle这个文件 apply plugin: 'com.tencent.bugly.tinker-support' def bakPath = file("${buildDir}/bakApk/") /** * 此处填写每次构建生成的基准包目录 */ def baseApkDir = "app-0908-17-42-31" /** * 对于插件各参数的详细解析请参考 */ tinkerSupport { // 开启tinker-support插件,默认值true enable = true // 指定归档目录,默认值当前module的子目录tinker autoBackupApkDir = "${bakPath}" // 编译补丁包时,必需指定基线版本的apk,默认值为空 // 如果为空,则表示不是进行补丁包的编译 // @{link tinkerPatch.oldApk } baseApk = "${bakPath}/${baseApkDir}/app-release.apk" // 对应tinker插件applyMapping baseApkProguardMapping = "${bakPath}/${baseApkDir}/app-release-mapping.txt" // 对应tinker插件applyResourceMapping baseApkResourceMapping = "${bakPath}/${baseApkDir}/app-release-R.txt" // 构建基准包和补丁包都要指定不同的tinkerId,并且必须保证唯一性 // tinkerId = "base-1.0.4" tinkerId = "patch-1.0.4" // 是否启用覆盖tinkerPatch配置功能,默认值false // 开启后tinkerPatch配置不生效,即无需添加tinkerPatch overrideTinkerPatchConfiguration = true // 构建多渠道补丁时使用 // buildAllFlavorsDir = "${bakPath}/${baseApkDir}" //支持加固需要添加的属性 isProtectedApp = true // 是否开启反射Application模式 enableProxyApplication = true } /** * 一般来说,我们无需对下面的参数做任何的修改 * 对于各参数的详细介绍请参考: * https://github.com/Tencent/tinker/wiki/Tinker-%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 */ tinkerPatch { oldApk = "${bakPath}/${baseApkDir}/app-release.apk" ignoreWarning = false useSign = true dex { dexMode = "jar" pattern = ["classes*.dex"] loader = [] } lib { pattern = ["lib/*/*.so"] } res { pattern = ["res/*", "r/*", "assets/*", "resources.arsc", "AndroidManifest.xml"] ignoreChange = [] largeModSize = 100 } packageConfig { } sevenZip { zipArtifact = "com.tencent.mm:SevenZip:1.1.10" // path = "/usr/local/bin/7za" } buildConfig { keepDexApply = false //tinkerId = "1.0.1-base" //applyMapping = "${bakPath}/${appName}/app-release-mapping.txt" // 可选,设置mapping文件,建议保持旧apk的proguard混淆方式 //applyResourceMapping = "${bakPath}/${appName}/app-release-R.txt" // 可选,设置R.txt文件,通过旧apk文件保持ResId的分配 } }第三步:初始化SDK ...

July 2, 2019 · 3 min · jiezi

在React Native中集成热更新

最近,在项目DYTT集成了热更新,简单来说,就是不用重新下载安装包即可达到更新应用的目的,也不算教程吧,这里记录一下。1.热更新方案目前网上大概有两个比较广泛的方式,分别是react-native-pushyreact-native-code-push前者是由ReactNative中文网推出的代码热更新服务,后者是由微软老大哥推出的,当然不仅仅是为React Native,还包括其他原生方式。综合考虑之下,选择了react-native-code-push。2.安装code-push1.安装code-pushnpm install -g code-push-cli2.注册登录账号code-push register这时候会自动启动浏览器打开网页并提供一个codePush AccessKey,然后命令行里出现需要输入access key输入之后就登录成功了。(貌似在本机上以后都不用登录了,暂不清楚保持登录持续多久)3.添加一个CodePush应用code-push app add myProject android react-native注意填写app的名称,OS(android/ios),平台(react-native),并且android和ios需要创建两个应用创建完成会出现两个keynameDeployment KeyProduction(一串37位的key)Staging(一串37位的key)Production是对应生产环境的,Staging是对应开发环境的。这个对于我们来说其实没什么区别,只是为了方便测试,所以搞了两个环境3.react-native应用接入code-push1.安装react-native-code-pushyarn add react-native-code-push# linkreact-native link react-native-code-push2.原生配置目前只测试了android,ios有兴趣的可以自行测试上面提到了两个key值,现在需要配置在原生目录里1.打开android/app/build.gradleandroid { … buildTypes { debug { … // Note: CodePush updates should not be tested in Debug mode as they are overriden by the RN packager. However, because CodePush checks for updates in all modes, we must supply a key. buildConfigField “String”, “CODEPUSH_KEY”, ‘""’ … } releaseStaging { … buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_STAGING_KEY>"’//注意这里的引号 … } release { … buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_PRODUCTION_KEY>"’ … } } …}如果遇到打包错误,可加上matchingFallbacks = [‘release’, ‘debug’],不知道是不是个别情况,如果没有的请忽略。修改versionName为3位数的版本号(code-push要求)defaultConfig { applicationId “com.dytt” minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 2 versionName “2.1.0”//默认为2位版本号 // ndk { // abiFilters “armeabi-v7a”, “x86” // } }release { //… matchingFallbacks = [‘release’, ‘debug’]//加上这一句 buildConfigField “String”, “CODEPUSH_KEY”, ‘"<INSERT_PRODUCTION_KEY>"’ //… }2.打开MainApplication.java@Overrideprotected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( … new CodePush(BuildConfig.CODEPUSH_KEY, MainApplication.this, BuildConfig.DEBUG), // Add/change this line. … );}这样就实现了key的动态部署,即在什么环境下使用什么key以上文档参考自https://github.com/Microsoft/react-native-code-push/blob/master/docs/multi-deployment-testing-android.md4.客户端更新策略1.导入react-native-code-push这里需要在应用的根组件上添加CodePush配置import CodePush from “react-native-code-push”;如果你的环境支持Decorator(修饰符),可以这样@codePush(options: CodePushOptions)class MyApp extends Component<{}> {}普通的写法class MyApp extends Component<{}> {}MyApp = codePush(codePushOptions)(MyApp);export default MyApp;这里的codePushOptions是更新的配置选项checkFrequency (codePush.CheckFrequency) 指定您要检查更新的时间,默认为codePush.CheckFrequency.ON_APP_START。installMode (codePush.InstallMode) 指定何时安装可选更新,默认为codePush.InstallMode.ON_NEXT_RESTART。…详细的配置可参考https://github.com/Microsoft/react-native-code-push/blob/master/docs/api-js.md2.更新策略默认情况下,CodePush会在app每次启动的时候去检测是否有更新,如果有,app会自动下载并在下次打开app时安装这种更新方式是静默的,用户根本察觉不到。如果我们需要给一点更新提示,可以使用默认的弹出框,也就是react-native自带的Alert,点击后立即安装class MyApp extends Component {}MyApp = codePush({ updateDialog: true, installMode: codePush.InstallMode.IMMEDIATE})(MyApp);当然,你可以对弹出框做少量的自定义,比如标题,按钮的文字等updateDialog: { optionalIgnoreButtonLabel: ‘稍后’, optionalInstallButtonLabel: ‘立即更新’, optionalUpdateMessage: ‘有新版本了,是否更新?’, title: ‘更新提示’},这些是默认的更新方式,那么如何自定义呢。我们可以用到CodePush.checkForUpdate来手动检查更新,然后弹出一个自定义窗口const RemotePackage = await CodePush.checkForUpdate(deploymentKey);if(RemotePackage){ this.modal.init(RemotePackage);//打开弹窗}这里需要注意的是,在checkForUpdate(或其他需要填写deploymentKey的地方)的时候,如果在debug模式下,如果不填写deploymentKey,会提示缺少deploymentKey,我们可以临时写一个固定的方便测试。在正式环境下,这里是不需要填写,它会根据系统自动获取我们在之前配置的那些deploymentKey值然后可以通过RemotePackage.download和LocalPackage.install来完成下载和安装install = async () => { LayoutAnimation.easeInEaseOut(); this.setState({status:1})//download const LocalPackage = await this.RemotePackage.download((progress)=>{ this.setState({ receivedBytes:progress.receivedBytes }) Animated.timing( this.width, { toValue: parseFloat(progress.receivedBytes / progress.totalBytes).toFixed(2), duration: 150 } ).start(); }) this.setState({status:2})//downloadComplete await LocalPackage.install(LocalPackage.isMandatory?CodePush.InstallMode.IMMEDIATE:CodePush.InstallMode.ON_NEXT_RESUME); if(!LocalPackage.isMandatory){ this.setState({status:3}) this.setVisible(false); }else{ ToastAndroid && ToastAndroid.show(‘下次启动完成更新’, ToastAndroid.SHORT); }}具体实现可以参考项目DYTT3.打包Releasecd android# 生成Release(Production)包gradlew assembleRelease# 生成Release(Staging)包gradlew assembleReleaseStaging其实都一样,只是环境区别5.发布code-push更新这一步很简单,集成了打包和发布code-push release-react dyttAndroid android –t 2.1.0 –dev false –d Production –des “1.修复了已知BUG\n 2.测试code push” –m true这里注意–t 2.1.0,有以下几种规则1.2.3 仅仅只有1.2.3的版本* 所有版本1.2.x 主要版本1,次要版本2的任何修补程序版本1.2.3 - 1.2.7 1.2.3版本到1.2.7版本>=1.2.3 <1.2.7 大于等于1.2.3版本小于1.2.7的版本~1.2.3 大于等于1.2.3版本小于1.3.0的版本^1.2.3 大于等于1.2.3版本小于2.0.0的版本–d表示开发版本,可选择Production和Staging–m表示是否强制更新当然还有很多操作,比如删除某些更新,回滚等,可以参考官方文档https://github.com/Microsoft/react-native-code-push小节总的来说,这次热更新集成还是挺容易,里面碰到的几个误区在上面也已经提到过,欢迎大家多多关注我的项目DYTT^^ ...

January 11, 2019 · 2 min · jiezi

从PHP迁移至Golang - 热更新篇

上篇大致提到的Golang的热更新,本篇将详细论述。1、什么是热更新网络上有这么一个例子来形容热更新,我觉得很形象很贴切:一架行驶在高速上的大卡车,行驶过程中突然遭遇爆胎,热更新则是要求在不停车的情况下将车胎修补好,且补胎过程中卡车需要保持正常行驶。软件的热更新就是指在保持系统正常运行的情况下对系统进行更新升级。常见的情况有:系统服务升级、修复现有逻辑、服务配置更新等。2、热更新原理先来看下Nginx热更新是如何做的?Nginx支持运行中接收信号,方便开发者控制进程。1)首先备份原有的Nginx二进制文件,并用新编译好的Nginx二进制文件替换旧的2)然后向master进程发送USR2信号。此时Nginx进程会启动一个新版本Nginx,该新版本Nginx进程会发起一个新的master进程与work进程。即此时会有两个Nginx实例在运行,一起处理新来的请求。3)再向原master进程发送WINCH信号,它会逐渐关闭相关work进程,此时原master进程仍保持监听新请求但不会发送至其下work进程,而是交给新的work进程4)最后等到所有原work进程全部关闭,向原master进程发送QUIT信号,终止原master进程,至此,完成Nginx热升级。注:在*nix系统中,信号(Signal)是一种进程间通信机制,它给应用程序提供一种异步的软件中断,使应用程序有机会接受其他程序或终端发送的命令(即信号)。同样地,Golang热更新也可以采取类似的处理。如上篇所述,都是利用用户自定义信号USR2。注:Plugin包方式的Golang热更新本文暂不讨论。3、热更新实现Golang热更新可以细分为服务热『更新』(即热升级,类比Nginx的restart命令)与配置文件热更新(类比Nginx的reload命令)。接下来从实现细节处依次讨论。3.1 服务热更新大致流程如下:1)Golang服务进程运行时监听USR2信号2)进程收到USR2信号后,fork子进程(启动新版本服务),并将当前socket句柄等进程环境交给它3)新进程开始监听socket请求4)等待旧服务连接停止主要代码示例如下:监听USR2信号func (a *app) signalHandler(wg *sync.WaitGroup) { ch := make(chan os.Signal, 10) signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR2) for { sig := <-ch switch sig { case syscall.SIGINT, syscall.SIGTERM: // 确保接收到INT/TERM信号时可以触发Golang标准的进程终止行为 signal.Stop(ch) a.term(wg) return case syscall.SIGUSR2: err := a.preStartProcess() if err != nil { a.errors <- err } // 发起新进程 if _, err := a.net.StartProcess(); err != nil { a.errors <- err } } }}复制当前进程socket连接,发起新进程execSpec := &syscall.ProcAttr{Env: os.Environ(),Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},}fork, err := syscall.ForkExec(os.Args[0], os.Args, execSpec)…详细源码可见:https://scalingo.com/articles…以上仅为代码示例,目前已经成熟的开源实现主要有:endless和facebook的grace,原理基本类似,fork一个子进程,子进程监听原有父进程socket端口,父进程优雅退出。在实际的生产环境中推荐使用以上开源库,关于热更新开源库的使用非常方便,下面是facebook的grace库的例子:引入github.com/facebookgo/grace/gracehttp包func main() { app := gin.New()// 项目中时候的是gin框架 router.Route(app) var server *http.Server server = &http.Server{ Addr: “:8080”, Handler: app, } gracehttp.Serve(server)}利用go build命令编译,生成服务的可执行文件。然后再用shell封装一下服务命令,生成restat.sh命令文件#!/bin/shps aux | grep wingocount=ps -ef | grep "wingo" | grep -v "grep" | wc -lecho ““if [ 0 == $count ]; then echo “Wingo starting…” sudo ./wingo & echo “Wingo started"else echo “Wingo Restarting…” sudo kill -USR2 $(ps -ef | grep “wingo” | grep -v grep | awk ‘{print $2}’) echo “Wingo Restarted"fisleep 1ps aux | grep wingo注:其中wingo为服务的二进制名称。于是,便可通过执行./restart.sh命令,达到对服务的热升级目的。3.2 配置文件热更新配置文件热更新是指在不停止服务的情况下,重新加载服务所有配置文件。与3.1服务热升级原理一样,利用用户自定义信号:USR1,即可实现服务的配置文件热更新。1)服务监听USR1信号2)服务接收到USR1信号后,停止接受新的连接,等待当前连接停止,重新载入配置文件,重启服务器,从而实现相对平滑的不停服的更改。主要代码实现:// LoadAllConf 调用加载配置文件函数// load为具体加载配置文件方法func LoadAllConf(load func(bool)) { load(true) listenSIGUSR1(load)}// listenSIGUSR1 监听SIGUSR1信号func listenSIGUSR1(f func(bool)) { s := make(chan os.Signal, 1) signal.Notify(s, syscall.SIGUSR1) go func() { for { <-s f(false) log.Println(“Reloaded”) } }()}详细源码可见:https://www.openmymind.net/Go…利用go build命令编译,生成服务的可执行文件。然后再用shell封装一下配置重载命令,生成reload.sh命令文件#!/bin/shps aux | grep wingoecho ““echo “Wingo Reloading…“sudo kill -USR1 $(ps -ef | grep “wingo” | grep -v grep | awk ‘{print $2}’)echo “Wingo Reloaded"echo ““sleep 1ps aux | grep wingo于是,便可通过执行./reload.sh命令,达到对服务的配置文件热升级目的。4、总结本文主要描述了Golang服务热升级与配置文件热更新原理与主要代码实现,本质上也不是什么新内容,如果之前读过《Unix环境高级编程》,就会觉得很亲切。底层原理基本上是利用了信号这个软件中断机制,在运行中改变常驻进程的行为。 ...

December 3, 2018 · 2 min · jiezi