几乎所有人都知道,包含安卓,iOS,PC等等几乎所有的软件,所有的前后端程序,都必须要面对一个问题,那就是升级。也许有一些人会问,为什么所有软件都需要升级呢?好吧,也许会有人开玩笑的说:“那肯定是开发程序的人水平不够,永远做不出来一款完美的软件。做出来软件后,需要修修补补才能趋于完善。所以才必须要升级。”这虽然是开玩笑的话,但也不是完全没有道理。如果一个程序员能够做出一个完美的程序,也许这个程序就不需要升级了。当然,这种情况几乎是不存在的。
那么这时候,你可能还是有点疑问“那么到底为什么要升级呢?”答案就是:
- 修复编码错误、逻辑错误、设计错误等一系列存在的问题,提高安全性;
- 与时俱进,新增一些用户需要的功能,或者删除一些用户不需要的功能;
- 进行视觉、操作、流程、功能等一些列优化、改进,提升用户体验;
升级功能看似简单,但是对于一个想持续经营的客户端软件来讲却是一个非常重要的生命线。如果升级程序做不好,造成用户不能升级或者升级失败,就等于说失去了对所有的安装的软件的控制。软件如果想要优化、修补、更新功能只能够重新安装,这对于开发者来讲,这非常严重!!!无疑是相当于灭顶之灾。
在windows8 之前,windows平台一直没有提供一个类似于苹果的AppStore或者Linux的包管理工具。所有在windows下软件的安装、升级、卸载都需要由软件开发者负责。这样就导致windows 下的软件的安装、升级、卸载并没有统一的方式,都是五花八门。但是总体来讲方法都是大同小异,表现方式区别不小。
我们今天主要聊一下window 软件的升级方案发展过程。在早期互联网时代,很多软件的升级都是通过发布离线安装包(升级包)的方式。一般这种包就是一个安装程序,它只负责安装程序需要更新的部分,然后做一些修改注册表之类的事情就达到了升级的功能。也有软件是直接就发布全量的安装包,通过让用户重装来达到升级到最新版本的目的。时至今日,也有很多软件使用的是这种方式,既安装包与升级包为同一个包。这样的好处就是太方便了。也有很多意想不到的坏处,我们下面会讲到。
随着互联网的普及,现在绝大多数PC软件都采用了在线升级的方式。我们先来简单的说一下在线升级.最简单的的在线升级步骤是:
- 首先客户端发送检测消息(本地版本号)到服务器,服务器返回是否需要更新,以及如果需要更新最新更新包的下载地址。
- 如果服务器返回需要更新,客户端就根据返回的升级包地址去下载更新包。
- 执行更新程序的安装,或者解压替换。
上面的是版本号保存在本地,检测是否需要升级放到服务器来进行比较。当然,也可以放到客户端来进行比较,就是下面的步骤:
- 客户端向服务器发送请求信息,服务器返回最新的版本号以及最新更新包下载地址。
- 客户端根据返回的最新版本号与本地的版本号进行对比,如果需要升级,就下载最新安装包。
- 执行更新程序的安装,或者解压替换。
我们还是建议使用第一种比较方式,即,判断是否需要升级放到服务器来控制,这样比较灵活。我们可以控制服务器,来应对突发、异常情况,实现对安装用户的升级控制。
但是对于上面说的在线升级方式,有一种情况,就是升级包就是安装包的情况。这种就需要用户重新安装这样不仅步骤不叫繁琐,安装包大小比较大,下载耗时等一系列缺点。
后来就有了更新包就是只包含改变的文件。这样就可以减少升级包的大小,降低下载时间等一系列问题。但这时候,如果能够使用静默升级,或者其他方式,避免用户介入。就可以方便用户,提升体验。所以,后来又可以将所有更改了的文件,压缩成一个zip包,用户下载完成就可以解压,替换就完成了升级。
再后来,随着敏捷开发方式的普及,软件的升级会变得越来越频繁。对于PC端软件每次升级时主程序以及一些重要的动态库都有可能被更新,所以我们就需求更小的安装包,以及更快捷得的升级方式。于是就产生了比较文件二进制的算法 BsDiff以及谷歌基于BsDiff算法,进一步改进的Courgette算法。这个算法可以让更新包呈数量级别的缩小。使用他们生成的更新包就极度小了,耗时也大大缩短。
另外值得一提的是,随着软件升级包的越来越大,用户更新下载的时间也会越来越长,也有软件采用了静默升级的方式,在用户不知不觉的情况下就进行了升级,给用户的体验会好一些。但是在程序运行的时候,由于文件被占用,进行文件替换可能会出现升级失败。于是谷歌就搞出来一个双目录更新的方法来应对。所谓双目录就是把原来的文件先复制到另一个新的文件夹,更新程序只在新的文件夹进行升级,升级完成后直接从新的目录启动新版本。这也是目前PC端软件比较流行的升级方式。
- 在线升级采用C/S架构;
- 客户端只保留当前版本号;服务器保留所有版本信息,包含 版本号、安装包、更新包、md5值列表(也可以是其他计算文件唯一性的值,比如crc32等);
- 客户端带版本号访问服务器;服务器根据客户端的版本号进行比较,给客户端返回是否需要升级的信息,和如果需要升级,要下载的更新包地址;
- 服务器保存各版本md5列表用以每次发版,制作更新包使用(可以是发版程序来制作);
我们先来看一下常用的升级方式都有哪几种?
全量安装更新:更新包就是安装包(.exe),下载下来安装包和重装的步骤一样,需要用户操作,比如点击下一步来完成更新。
优点:只需要做安装包即可,无需制作更新包,发版前处理方便。
缺点:升级包体积大、下载时间久;需要用户操作,不友好;安装过程失败率高。增量安装更新:更新包为只包含更新文件的安装包(.exe),不能单独安装使用,只能更新使用。
优点:体积小,下载时间短。
缺点:发版前需要单独做一个升级包exe;需要用户操作,不友好;安装失败率高。资源热更新:对于一些程序启动过程中,不被占用的资源,比如图片、音视频、配置文件、js相关资源可以在程序运行中替换的更新方式。
优点:更新方式灵活,体积较小;
缺点:只能更新一些程序运行过程中非占用的资源文件、局限性较大;全量替换更新:将全量文件,压缩成zip包,更新时候下载zip包。将下载的zip包解压到新的目录中,下次启动新目录中的可执行程序,完成更新。
优点:更新包为全量安装文件,制作zip包简单;全自动解压到新目录,无需用户干预,体验好;
缺点:体积大、下载时间久;增量替换更新:将当前版本与最新版之间的差量文件,取最新的制作成zip压缩包。升级时将原有的目录复制到新目录,将压缩包解压替换到新的目录中,完成升级。
优点:提交小,下载快;全自动升级,无需用户干预,体验好;
缺点:发版前需要制作每个版本的升级包(.zip),比较耗时。
- 替换更新方式 体验上 要优于安装方式;
- 增量升级方式更 更新包体积(新速度上) 要优于 全量更新方式;
- 资源热更新局限性很大,不能独自承担一个软件所有的更新任务,可与其他方式配合使用;
- 发版前后台处理的繁琐比让用户升级繁琐要好;
综上所述,在上面几种常见的方式中,我们还是增量替换升级的方式有很大优势。目前增量更新也是一种比较流行的方式。
接下来我们重点说这种比较常见的升级方式-增量替换。再说之前,我们还需要来弄清楚一个问题,就是什么时候用来检测升级、什么时候会执行升级操作,也就是升级时机。
程序检测升级的时机一般就以下几种:
- 程序启动时候检测一次是否升级,如果需要升级,就升完级才能够进入软件。(静默升级除外)
- 程序运行的时候,在后台定时检测是否需要升级。程序退出时候也就停止了检测。
- 做成一个服务,无论程序是否在运行,都会定时检测是否升级,如果升级,就偷偷的进行升级。(感觉很流氓,用的人也很少。)
其中大部分都是程序启动时候先进行检测一次是否升级,如果需要,就执行升级操作,这种方式是常用的检测时机。如果对于升级实时性要求比较高的情况,或者软件长时间运行不关闭的情况,可以考虑第二种情况,即在程序运行过程中,定时检测。
1. 启动器
在提到替换升级方案中,我们要先引入另外一个概念,就是启动器。顾名思义,启动器就是专门负责启动程序的。启动器的原理相当简单,就是读取配置文件中配置(可执行程序的地址),然后进行打开。
为什么要用启动器呢?简单地来讲,就是为了统一程序入口,快捷方式链接到启动器,更新完成后无需更改软件的快捷方式。
具体解释起来就是:这和前面提到的双目录升级方案有关(替换升级方式)。即每一个版本一个文件夹,每一个文件夹就是全量的安装包文件,可以单独运行。这时候这就出现了一个问题,就是我们升级完成后,会出现一个新的文件夹,以后启动也应该启动新的文件夹里面的可执行程序。这时候如果没有启动器,我们就需要更改程序的快捷方式的链接。那么程序有多少快捷方式?都放到哪里?都要找出来并一一的更改,就是一个很大的工程了。所以,我们需要一个统一的程序入口,让快捷方式这些无需因升级而进行更改。
但是具体要运行哪一个版本(文件夹)里的程序呢?这个信息是放在配置文件中的。他的配置文件大概类似于这样的:
配置文件里面内容很简单,就是告诉你,要启动哪一个目录下的那个程序。比如说上面例子中的配置文件就是要告诉启动器要启动 “D:softpath5.0.0.0yapp.exe”这个可执行程序。
启动器我们也有一些要求:
- 尽可能的简单稳定;
- 尽量可以独立运行,无需链接其他动态库(使用纯C++,将其他库静态编译到可执行程序中);
- 兼容性要高,对各种错误要尽可能给出planB(例如,找不到配置文件,要创建新的配置文件,写入默认的地址等);
2. 升级流程
首先,升级我们应该做成一个单独的程序,可以独立运行更好。其次,升级流程应该更可能的全面的处理各种意外情况:
- 单独做成一个程序,可以独立运行;
- 尽可能的全面考虑各种意外情况,处理各种意外,至少升级失败让老师有其他选择;
- 全自动升级, 不出意外无需用户干预;
- 一定要支持安装包完整性校验,校验通过才可以升级;
- 升级程序可以对自身以及启动器进行升级;
- 尽可能支持失败重试、断点续传等功能,提高体验;
- 升级完成后,要更改配置文件,将启动地址更改为最新包;
- launcher.exe: 就是上面说的启动器;
- assist.exe :就是我们的主程序;
- updater.exe : 就是这次的重点-升级程序;
我们上面说了启动器launcher.exe的作用以及主要点,下面就不再赘述。简单说一下主程序在升级过程中的作用。由启动器launcher.exe启动了主程序assist.exe之后:
- assist.exe 首先获取本地的版本号;
- 拿着版本号去访问接口,接口会在后台进行对比,返回对比结果即:是否需要升级、升级包的地址、升级包的md5值;
- 如果需要升级就启动升级程序 updater.exe;
接下来,我们详细说一下主角-升级程序updater.exe的流程:
- 首先把assist.exe的 1~2步重复一遍,即,重新获取本地版本,然后访问接口是否需要更新。这一步主要是为了保证 updater.exe 的程序可以独立运行。就算用户直接双击 updater.exe 也可以正常执行升级过程。
- 下载更新包.zip之后并校验md5值是否正确。这里如果需要断点续传,需要后台接口的支持。由md5就可以判断下载的更新包是否是完整的。损坏了的更新包需要重新下载,如果一直下载失败,则不能继续进行下一步。
- 建立新版本的文件夹V2.0.0.0,并将当前版本V1.0.0.0目录所有文件复制到新版本V2.0.0.0目录下。必须是复制,不能剪切,就算当前目录中有文件被占用,使用复制也不会出现问题,如果使用剪切就会失败。(这也是双目录升级实现的第一步)。
- 将第2步下载的更新包.zip 直接解压到V2.0.0.0目录下。这里如果有重复的,就会直接替换。经历完这一步,V2.0.0.0目录就包含了最新的版本更改,也可以独立运行。(如果updater.exe需要升级,这一步也将最新版本的升级程序进行了升级)。
- 检测V2.0.0.0目录下是否有 launcher.exe 文件,如果有,就代表需要更新启动器。就需要先把launcher.exe替换掉。
- 修改启动器的配置文件 setting.ini, 将V2.0.0.0的地址写入配置文件。
- 启动 launcher.exe重新打开程序,这时候就是进入了V2.0.0.0目录了。完成更新!
点击下面蓝色的字,可以直接跳转到浏览器进行下载最新安装包,这就是我们的planB中的一个, 就算用户失败,也可以提供让用户重新安装的机会,而不是什么都做不了。