持续集成iOS开发技巧iOS进阶

教你搭建App内测下载平台

2017-01-08  本文已影响4327人  知傲

前言

App开发测试过程中,我们会把安装包传到各种第三方的内测分发平台方便下载。这些平台或多或少有这样那样的限制,比如下载量啊、付费啊、不能方便找到历史版本啊。还有一方面,我们经常会打Debug版本的包方便调试,又不希望Debug包流传到外部去,这样就很有必要自己搭一个下载平台,于是就有了这个项目(github地址)。

技术调研

怎么下载

先说安卓,apk文件通过最简单的http/ftp下载就可以安装了,略过。
iOS稍微复杂一点,需要两步才能完成。
第一,下载链接必须是这样的格式

itms-services://?action=download-manifest&url=一个plist文件的地址

第二,plist内容如下

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>items</key>
    <array>
        <dict>
            <key>assets</key>
            <array>
                <dict>
                    <key>kind</key>
                    <string>software-package</string>
                    <key>url</key>
                    <string>ipa文件的地址</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>bundleID</string>
                <key>bundle-version</key>
                <string>1.0</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>AppTitle</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

其中,最重要的就是ipa文件的地址,要求必须是https协议,那就需要SSL证书,幸运的是我们可以信任自签名的证书。下载的过程就是这样,当然我们希望这个链接和plist的生成是自动完成的。

自签名证书

参考如何创建一个自签名的SSL证书(X509)

包信息提取

单单只能下载还不够,我们希望看到更多的信息:App名字、版本号、build号、更新时间、图标等。这些信息虽然可以留给上传者在上传的时候一并带上,但是作为有追求的程序员,把方便留给别人的最基本的,因此我们要从ipa/apk中提取这些信息。
无论是ipa还是apk,本质都是zip压缩文件。
对于iOS的ipa,包信息都放在Info.plist中,主要有CFBundleVersion、CFBundleIdentifier、CFBundleShortVersionString、CFBundleName等。图标文件的名字也是固定的,只要解压就可以得到。不过,苹果对png图片进行了了自定义的pngcrush压缩,有压缩自然就有还原工具pngdefry
对于Android的apk,解压后还能看到AndroidManifest.xml,但是里面的内容经过编码显示为乱码,不方便查看,需要借助开发工具aapt(Android Asset Packaging Tool),方法如下
aapt dump badging apkPath
输出的文本格式如下,不是标准的歌声,需要手动转换一下。

package: name='com.jianshu.haruki' versionCode='16070101' versionName='1.11.2'
sdkVersion:'14'
targetSdkVersion:'22'
...
application: label='简书' icon='res/drawable-hdpi-v4/icon_jianshu_new.png'
...

找轮子

程序员有一个习惯,需要某个东西的时候会先一番搜索,直接用别人写好的,用着用着发现别人写的东西有这样那样的不足,然后撸起袖子自己造一个。这次也不例外,我在github上找到了一个ios-ipa-server,它的特点是简单,ipa文件存储在一个目录下,没有数据库,包信息只有上传时间(其实就是文件更新时间),不能对app归类,只靠文件名区别,不支持上传,如下图:


浏览器访问下载页面时,后端实时解析包信息、解压icon图片,这样做效率是非常低的。
这么多不足我们就有了造轮子的理由了。

自己造一个

既然ios-ipa-server是基于node-express写的,正好我没写过nodejs,那就在它的基础上继续写吧,借机学(zhuang)习(bi)一下。
整个项目的结构是这样的,提供四个API:包上传、获取所有App最新版本、获取某个App的所有版本、动态生成plist文件,数据存储使用sqlite3。

包上传

接口设计如下:

path:
POST /upload

param: 
package:安装包文件

response:
{
        id: 6,
        guid: "46269d71-9fda-76fc-3442-a118d6b08bf1",
        bundleID: "com.jianshu.Hugo",
        version: "2.11.4",
        build: "1608051045",
        icon: "https://10.20.30.233:1234/icon/46269d71-9fda-76fc-3442-a118d6b08bf1.png",
        name: "Hugo",
        uploadTime: "2016-12-01 20:50:05",
        platform: "ios",
        url: "itms-services://?action=download-manifest&url=https://10.20.30.233:1234/plist/46269d71-9fda-76fc-3442-a118d6b08bf1"
}

后端需要拿到安装包,提取出包信息和png图标图片,然后插入到数据库中,最后存储安装包文件和png图片,这也是最关键、最复杂的一个API。

  app.post('/upload', function(req, res) {
    var form = new multiparty.Form();
    form.parse(req, function(err, fields, files) {
      var obj = files.package[0];
      var tmp_path = obj.path;
      parseAppAndInsertToDb(tmp_path, info => {
        storeApp(tmp_path, info["guid"], error => {
          if (error) {
            errorHandler(error,res)
          }
        })
        console.log(info)
        res.send(info)
      }, error => {
        errorHandler(error,res)
      });
    });
  });

接收表单信息用到了multiparty模块,parseAppAndInsertToDb内部完成了包信息的提取和存储,storeApp存储包文件。
parseAppAndInsertToDb的实现如下,

function parseAppAndInsertToDb(filePath, callback, errorCallback) {
  var guid = Guid.create().toString();
  var parse, extract
  if (path.extname(filePath) === ".ipa") {
    parse = parseIpa
    extract = extractIpaIcon
  } else if (path.extname(filePath) === ".apk") {
    parse = parseApk
    extract = extractApkIcon
  }
  Promise.all([parse(filePath),extract(filePath,guid)]).then(values => {
    var info = values[0]
    info["guid"] = guid
    excuteDB("INSERT INTO info (guid, platform, build, bundleID, version, name) VALUES (?, ?, ?, ?, ?, ?);",
    [info["guid"], info["platform"], info["build"], info["bundleID"], info["version"], info["name"]],function(error){
        if (!error){
          callback(info)
        } else {
          errorCallback(error)
        }
    });
  }, reason => {
    errorCallback(reason)
  })
}

首先根据文件后缀名判断安装包类型,因为ipa和apk的处理逻辑不一样,所以分别对应两个方法,包信息的提取和icon提取可以同时进行,所以这里用了Promise.allparseIpaparseApk就是包信息的提取。extractApkIconextractIpaIcon则是icon的提取,extractIpaIcon多了一步还原png图片的处理。
parseIpa用到了ipa-extract-info模块,parseApk则使用了apk-parser3,代码都非常简单。详细可进入github地址

其他

其他三个API则比较简单了,无非就是根据参数取数据,不再赘述。

集成和使用

安装步骤非常简单,首先需要安装node,有了node之后只要一行命令

npm install -g ipapk-server

安装完成之后输入命令

ipapk-server

手机浏览器访问https://ip:port 即可打开下载页面



App的信息获取都设计成了API,提供给开发者更灵活的接入方式,可以做web页面,也可以做成App,我的好朋友mask(人格分裂术)贡献了不少工作,完成默认的web下载页面。
更详细的内容请参考github

写在最后

简书作为一个优质原创内容社区,拥有大量优质原创内容,提供了极佳的阅读和书写体验,吸引了大量文字爱好者和程序员。简书技术团队在这里分享技术心得体会,是希望抛砖引玉,吸引更多的程序员大神来简书记录、分享、交流自己的心得体会。这个专题以后会不定期更新简书技术团队的文章,包括Android、iOS、前端、后端等等,欢迎大家关注。

上一篇下一篇

猜你喜欢

热点阅读