使用 Xcode 和 Android Studio 管理 iOS 和 Android 项目版本

在移动应用开发和运营的过程中,版本管理是一个老生常谈的基础问题,一些版本的基本概念也常常会困扰我们的研发和运营人员。同时,手动管理软件版本,也常常会因为不小心导致后续的发布和更新问题。

这里,我准备了一些 iOS 和 Android 版本的基础知识,以及如何在应用中获取版本信息和如何使用 Xcode 和 Android Studio 自动管理版本号。

版本号解释

无论是 iOS 还是 Android 都定义了两个版本属性:

  • iOS
    Info.plist 中定义

    • CFBundleShortVersionString
      在Xcode中解释为 Version ,这个就是我们常说的版本号,一般用户可见,通常由 <主版本号>.<次版本号>.<维护号> 三部分组成,主要用来识别不同时期不同功能的产品。
    • CFBundleVersion
      在Xcode中解释为 Build ,一般用于应用市场和程序内部识别版本,作为更新判断的依据,通常是一个递增的 INT 类型。
    这两个值可以在 Xcode 的项目信息里面进行管理,
    
    ![](/images/2016/01/202344-aec6606ddfe8fce5.png)
    img_01.png
    当然,你也可以直接编辑 `Info.plist`。
  • Android
    AndroidManifest.xml 中定义

    • android:versionName
      对应 iOS 中的 CFBundleShortVersionString 版本号,用作产品管理。
    • android:versionCode
      对应 iOS 中的 CFBundleVersion 编译号,作为内部识别。
    在使用 Eclipse 开发时,可以通过直接编辑 `AndroidManifest.xml` 文件修改,在使用 Android Studio 时,这些信息由 Gradle 脚本管理,找到并打开项目目录下 **app** 目录内的 `build.gradle` 文件,版本信息在 `defaultConfig` 段,
    
    ![](/images/2016/01/202344-f85f22862d22bac3.png)
    img_02.png

程序内获取版本信息

一般在应用的关于页面,我们都会显示应用的版本,方便客服定位问题,在应用检查更新时,也常常需要用到版本信息。其实,无论是在 iOS 上,还是 Android 平台,获取版本信息都比较简便:

  • iOS
    索引 Bundle 信息中的相关字段:

    <span class="hljs-built_in">NSBundle</span> *bundle = [<span class="hljs-built_in">NSBundle</span> mainBundle];
    <span class="hljs-built_in">NSString</span> *name = [[bundle localizedInfoDictionary] objectForKey:<span class="hljs-string">@"CFBundleDisplayName"</span>];
    <span class="hljs-built_in">NSString</span> *version = [bundle objectForInfoDictionaryKey:<span class="hljs-string">@"CFBundleShortVersionString"</span>];
    <span class="hljs-built_in">NSString</span> *build = [bundle objectForInfoDictionaryKey:<span class="hljs-string">@"CFBundleVersion"</span>];
    <span class="hljs-built_in">NSString</span> *fullVersion = [<span class="hljs-built_in">NSString</span> stringWithFormat:<span class="hljs-string">@"version: %@ (%@)"</span>, version, build];`</pre>
    

    上述的代码中,name 为本地化的程序名称,version 为版本号,build 为编译号,’fullVersion’ 则将版本号和编译号组成一个完整的版本信息。

  • Android
    获取 PackageInfo 中的相关信息:

    `  PackageInfo pi = sContext.getPackageManager().getPackageInfo(sContext.getPackageName(), 0);
        String versionName = pi.versionName;
        int versionCode = pi.versionCode;
        String fullVersion = String.format("version: %s (%d)", versionName, versionCode);`

    同样,versionName 为版本号,versionCode 为编译号,不同的是,在 Android 中 versionCode 使用 int 类型存储。

    Xcode 和 Android Studio 编译号自增

    一般应用的 版本号 都会由 产品经理项目经理 决定,根据产品所处的阶段和功能决定修改版本号的哪一个段。但 编译号 更多时候是根据每次发布递增,有时候还会遇到同一个版本号对应多个编译号的情况,如,紧急修复了一个关键 Bug 并更新发布一个版本,但如果每次发布都要手动去修改编译号回很繁琐,也很容易忘记和出错,而不管是 Xcode 还是 Android Studio 都没有提供自增编译号的功能,我们只有手动添加编译脚本来达到自增的目的。

  • Xcode
    添加编译过程,读取并修改 Info.plist 中的版本信息:

      1.  打开工程,选择编译目标,点击 `Build Phases` 选项卡。
    
    ![](/images/2016/01/202344-58cc11c67dc47124.png)
    img_03.png
    2. 点击左上角的 **+** 添加,选择 `New Run Script Phase` 添加一个编译脚本。
    ![](/images/2016/01/202344-9dc95e741a9bcbdc.png)
    img_04.png
    ![](/images/2016/01/202344-26978b50b967a4f6.png)
    img_05.png
    3. 在脚本编辑其中输入下面的脚本:
    ` #!/bin/bash
         buildNumber=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$INFOPLIST_FILE")
         buildNumber=$(($buildNumber + 1))
         /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$INFOPLIST_FILE"`
    上述脚本使用 PlistBuddy工具,读取 `Info.plist` 中的 `CFBundleVersion` 值,+1 后写回 `Info.plist` 中。 4. 我们需要让该段代码在应用打包前执行,以便将修改应用到App包内,因此我们将该编译过程重命名为 `Increase Version Code` 并移动到 `Copy Bundle Resources`之前。
    ![](/images/2016/01/202344-64419edf1ef84e70.png)
    img_06.png
    5. 这样,当我们每次编译该 Target 时 ,就会执行改脚本将 `CFBundleVersion` 值增加1,但没次调试运行时,该值都会加1,这并不是我们想要的,我们只需要在每次打包发布时增加1就行,因此,我们将 `Run script only wehn installing` 前的勾打上,这样就只会在打包应用时执行改脚本。
    ![](/images/2016/01/202344-03fed786b5227e61.png)
    img_07.png
  • Android Studio
    Android Studio 的 versionCode 自增原理和 Xcode 类似,不同的是我们不能让编译脚本修改自身,而是通过一个额外的 Java Properties 来文件存储版本信息:

      1.  打开工程,选择 `Module: app` 的编译脚本 `build.gradle`。
    
    ![](/images/2016/01/202344-c09c8e11c2be85d0.png)
    img_08.png
    2. 找到 `defaultConfig` 段,替换为下面的脚本:
    ` def versionPropsFile = file('version.properties')
         if (versionPropsFile.canRead()) {
           Properties versionProps = new Properties()
           versionProps.load(new FileInputStream(versionPropsFile))
           def verCode = versionProps['VERSION_CODE'].toInteger()
           versionProps['VERSION_CODE'] = (++verCode).toString()
           versionProps.store(versionPropsFile.newWriter(), null)
           defaultConfig {
               applicationId "com.example.versionexample"
               minSdkVersion 19
               targetSdkVersion 23
               versionCode verCode
               versionName "1.0.1"
           }
         } else {
           throw new GradleException("Could not read version.properties!")
         }`
    这段脚本会打开 app 目录下的 `version.properties` 配置文件,读取 `VERSION_CODE` 字段并增加1后写回,同时将值赋给 `defaultConfig` 中的 `versionCode` 。 3. 此时,点击 Sync 会报错 `Could not read version.properties!`,这是我们刚刚在脚本中抛出的,原因是找不到 `version.properties` 文件,我们需要新建一个。打开命令行定为到项目目录下:
    ` $ cd app/
         $ echo "VERSION_CODE=1" > version.properties`
    再次点击 Sync 就不会报错了。查看 `version.properties` 文件,
    ` $ cat version.properties     #Mon Nov 02 15:18:49 CST 2015
         VERSION_CODE=3`
    发现 VERSION_CODE 已经增加到 3 了,说明该脚本被执行了两次,但这并不符合我们的预期。 4. 同 Xcode 中一样,我们期望仅仅在应用打包时将 `versionCode` 增加 1 。因此我们需要获取编译参数,仅当 `release` 时才将 `versionCode` 增加 1 。修改后的脚本如下:
    ` def versionPropsFile = file('version.properties')
         if (versionPropsFile.canRead()) {
          Properties versionProps = new Properties()
          versionProps.load(new FileInputStream(versionPropsFile))
          def verCode = versionProps['VERSION_CODE'].toInteger()
          def runTasks = gradle.startParameter.taskNames
          if (':app:assembleRelease' in runTasks) {
             versionProps['VERSION_CODE'] = (++verCode).toString()
             versionProps.store(versionPropsFile.newWriter(), null)
          }
           defaultConfig {
               applicationId "com.example.versionexample"
               minSdkVersion 19
               targetSdkVersion 23
               versionCode verCode
               versionName "1.0.1"
           }
         } else {
           throw new GradleException("Could not read version.properties!")
         }
    
    
      这里获取当前task的名称,仅当task包含 `:app:assembleRelease` 时才会将 `versionCode` 加 1 ,否则直接使用读取到的值。
    

小结

版本管理是产品和项目管理中非常重要的一环,但在开发初期也常常容易被忽略,等到遇到问题时候才感到头痛。其实,产品经理和开发人员在产品的立项阶段就应该对产品版本有一个一致的理解,我个人通常在项目框架建立之初就将这些规则脚本添加到项目文件中,以期降低后续版本维护的成本。