React-Native 原生 APP 更新

当一个 APP在运行的时候, 开发者想要将自己的代码更新到用户的手机上时, 一般都有两种方案, 一是热更新, 二就是 APP 更新.
热更新暂且不说,这篇文章就讲讲 APP 如何更新

App更新流程

  1. 在 App 打开时请求接口或文件, 获取远程版本/版本更新说明/地址等等重用信息
  2. 通过库或者原生方案, 获取 App 的当前版本
  3. 比较远程版本和当前版本的区别(可以使用库,也可以自己写一个比较方案)
  4. 通过获取到的链接进行操作(可以跳转到对应网站下载,类似蒲公英这种,可以是 apk 链接, 通过安卓原生方法下载, 也可以是 App Store 链接)

大致的流程图

详细说明:

  1. 这些远程信息,可以是接口, 这样可以有一个中台来控制, 当然也可以是一个文件, 让运维来控制
    关于信息,不止于远程版本, 在项目里还可以添加其他属性,如: versionCode, versionCodeSwitch , notUpdate , deleteApp
    1.1 versionCode 通过 code 来升级版本,一般是一个数字(在 ios 里提交 App Store 的时候有需要用到的地方), 这样 versionName 并不会增加, 但是如果添加了 versionCode, 如果要升级 versionName, versionCode 也需要增加
    1.2 versionCodeSwitch 用来控制是否要根据versionCode来更新, 一般我都是在测试和其他环境开启,生产环境关闭的
    1.3 notUpdate 是否要根据远程信息来更新, 一般都是开启状态
    1.3 deleteApp 安卓 app 需要卸载重新安装, 因为直接安装可能存在某些问题, 将会使用此信息,先删除 APP, 再重新下载
  2. 获取当前手机的信息,方案较多, 我使用的是 react-native-device-info 这个库, 这个库里面提供的信息较全, 当然也可以使用原生方法, 来获取APP的信息
  3. 关于本地版本号和原生版本号之间的对比也是可以使用库,也可以自己写, 这边推荐两个库,下载量都是百万以上的: semver-comparecompare-versions, 这边附上我的 versionName 比较方案, 较为简陋:
    /**
     * 比较两版本号 
     * @param currentVersion 
     * @return boolean 
     * true=需要更新 false=不需要 
     */
    compareVersion = (currentVersion: string): boolean => {
        const {versionName: remoteVersion} = this.remoteInfo || {}
        if (!remoteVersion) {
            return false
        }
        if (currentVersion === remoteVersion) {
            return false
        }
        const currentVersionArr = currentVersion.split('.')
        const remoteVersionArr = remoteVersion.split('.')
        for (let i = 0; i < 3; i++) {
            if (Number(currentVersionArr[i]) < Number(remoteVersionArr[i])) {
                return true
            }
        } 
        return false
    }
    
  4. 关于下载 app 有很多方案, 最简单的就是跳转链接到第三方平台, 像蒲公英这样的, 使用 RN 提供的 Linking 方法来直接跳转
    当然安卓是可以直接通过自己提供的地址下载的, 这里提供一个方法(此方法来源于网络):
    @ReactMethod
    public void installApk(String filePath, String fileProviderAuthority) {
        File file = new File(filePath);
        if (!file.exists()) {
            Log.e("RNUpdater", "installApk: file doe snot exist '" + filePath + "'");
            // FIXME this should take a promise and fail it
     return;
        }
        if (Build.VERSION.SDK_INT >= 24) {
            // API24 and up has a package installer that can handle FileProvider content:// URIs
     Uri contentUri;
            try {
                contentUri = FileProvider.getUriForFile(getReactApplicationContext(), fileProviderAuthority, file);
            } catch (Exception e) {
                // FIXME should be a Promise.reject really
     Log.e("RNUpdater", "installApk exception with authority name '" + fileProviderAuthority + "'", e);
                throw e;
            }
            Intent installApp = new Intent(Intent.ACTION_INSTALL_PACKAGE);
            installApp.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
            installApp.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            installApp.setData(contentUri);
            installApp.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME, reactContext.getApplicationInfo().packageName);
            reactContext.startActivity(installApp);
        } else {
            // Old APIs do not handle content:// URIs, so use an old file:// style
     String cmd = "chmod 777 " + file;
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (Exception e) {
                e.printStackTrace();
            }
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            intent.setDataAndType(Uri.parse("file://" + file), "application/vnd.android.package-archive");
            reactContext.startActivity(intent);
        }
    }
    
    如果是我们自己提供下载服务,需要注意的是带宽, 如果网速过慢则用户体验过差, 还有就会带来更多的流量消耗, 其中的取舍,需要开发者决定

更新APP信息

在打包时, 通过脚本更新接口或者文件信息, 当然这个得看具体的打包方案
比如我现在的方案是使用 Jenkins 打包, 在打包时使用 shell 脚本更新对应信息(有需要也可以使用其他语言脚本):

  1. 首先定义需要获取的文件地址
    androidVersionFilePath="$WORKSPACE/android/app/build.gradle"  // 通过此文件获取安卓的版本信息
    iosVersionFilePath="$WORKSPACE/ios/veronica/Info.plist" // 通过此文件获取iOS的版本信息
    changeLogPath="$WORKSPACE/change.log" // 将版本更新信息存储在此文件中
    
  2. 通过文件地址, 获取打完包后的版本信息
    getAndroidVersion(){
      androidVersion=$(cat $androidVersionFilePath  | grep "versionName" | awk '{print $2}' | sed 's/\"//g')
      androidCode=$(cat $androidVersionFilePath  | grep "versionCode " | awk '{print $2}' | sed 's/\"//g')
      androidDelete=$(cat $androidVersionFilePath  | grep "deleteApp" | awk '{print $4}' | sed 's/\"//g')
      return 0
    }
    
    getIOSVersion(){
      rows=$(awk '/CFBundleShortVersionString/ {getline; print}' $iosVersionFilePath)
      iosVersion=$(echo "$rows" | sed -ne 's/<string>\(.*\)<\/string>/\1/p')
      iosVersion=$(echo "$iosVersion" | sed 's/^[[:space:]]*//')
    
      rows2=$(awk '/VersionCode/ {getline; print}' $iosVersionFilePath)
      iosCode=$(echo "$rows2" | sed -ne 's/<string>\(.*\)<\/string>/\1/p')
      iosCode=$(echo "$iosCode" | sed 's/^[[:space:]]*//')
      return 0
    }
    
    desc=$(cat $changeLogPath | tr "\n" "#")
    
  3. 替换现有文件中的信息:
    sed -i '' "s/\"releaseInfo\":.*$/\"releaseInfo\": \"$desc\"/"  $JsonPath/$fileName
    sed -i '' "s/\"versionName\":.*$/\"versionName\": \"$versionName\",/"  $JsonPath/$fileName
    sed -i '' "s/\"versionCode\":.*$/\"versionCode\": \"$versionCode\",/"  $JsonPath/$fileName
    sed -i '' "s/\"deleteApp\":.*$/\"deleteApp\": \"$deleteApp\",/"  $JsonPath/$fileName
    
    我的文件是以 json 作为格式的,说明文字是可以任意填写的,会触发一些解析问题:
    • 不允许出现会造成 JSON.parse 解析失败的符号, 如 \ , ````, \n ,\r, \" 等等
    • 因为说明文字的换行我是通过 # 切割的, 所以也不允许出现这个符号

大致流程图

总结

关于 APP 原生版本的更新流程基本就是这样,当然这个流程不光适用 APP, 也可以用于 PC 软件的更新
除了原生版本的更新,还有热更新, 也是非常重要的, 我将会在后面的博客中解析他

posted @ 2021-03-11 15:39  Grewer  阅读(846)  评论(0编辑  收藏  举报