Swift:另辟蹊径为App添加版本升级功能|项目复盘

1,911 阅读9分钟

项目简介

这篇文章,其实和之前那篇有那么一点点联系,我们考虑用Swift原生进行爬虫。

从Python到Swift有多远?借鉴思路,编写更Swifty的正则匹配工具类|项目复盘

本文的亮点在于:用Swift爬AppStore的数据,进而解析数据,分析App版本,从而完成版本升级功能。

本文难点是真的可以这样操作么?这样可以吗?尝试,尝试了才知道结果。编码上基本上没有太多,也不复杂的,而关键在于思路是怎么来的,为什么这么做。

项目背景

为App添加版本升级的功能,基本上在现在开发中很常见,偶尔也会在面试中提及。稍微有点开发经验的大佬们,大致思路如下:

App的后台管理有一个版本配置信息,里面保存了一些信息:版本号,是否强制升级,是否正在审核等。App端在某个时机出调用某一个接口,网络请求,获取版本配置信息,进而在App端弹窗提示,进行强制升级;同时注意在App正在进行审核是规避类似升级提示的弹窗。

流程图如下:

image.png

非常简单,对吧?那么来一个假设:

如果,注意是如果。

如果App端服务器没有这么一个后台版本信息配置功能,没有接口可以获取App的版本配置信息,如何为App添加版本升级功能?

也就是说App从App后台获取数据这条路断了,用什么数据去判断版本号,进行版本升级控制?

实践过程

先爬个虫压压惊:

某一次Python的爬虫行为,让我找到了突破口,是的,没错,又是从Python开始的:

import requests

def getWeiXinAppStoreInfo():
    url = "http://itunes.apple.com/cn/lookup?id=414478124"
    # 请求回来的响应
    response = requests.get(url)
    # 响应的网页字符串
    text = response.text
    print(text)

我们看看text拿到的是什么数据,下面是格式化后的:

{
    "resultCount":1,
    "results":[
        {
            "screenshotUrls":[
                "https://is4-ssl.mzstatic.com/image/thumb/Purple114/v4/58/b2/69/58b2695c-e340-bf3d-ec8d-b317ae89aea3/pr_source.png/392x696bb.png",
                "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/7f/34/e8/7f34e8b8-1e5b-854b-556c-5fdfa07fa947/pr_source.png/392x696bb.png",
                "https://is5-ssl.mzstatic.com/image/thumb/Purple124/v4/a0/aa/18/a0aa18e8-e94c-3333-0689-00841259c733/pr_source.png/392x696bb.png",
                "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/83/af/72/83af72ff-a73d-9ad3-5679-234a170c08e1/pr_source.png/392x696bb.png",
                "https://is2-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/c3/be/79/c3be7974-1083-1c01-c12f-bf7acd88321c/0356f5e8-253a-46b0-aae1-34eef8f19d38_1.1.jpg/392x696bb.jpg",
                "https://is4-ssl.mzstatic.com/image/thumb/PurpleSource124/v4/8e/93/fe/8e93fedd-a991-3fc0-2792-17900195ec10/8fb78965-9a18-4c54-b98d-54a277be936e_2.1.jpg/392x696bb.jpg"
            ],
            "ipadScreenshotUrls":[
                "https://is1-ssl.mzstatic.com/image/thumb/Purple114/v4/cf/18/67/cf1867cf-d21e-68d9-3d9b-f321a1a80207/mzl.imkvrcco.jpg/576x768bb.jpg",
                "https://is2-ssl.mzstatic.com/image/thumb/Purple114/v4/d6/e8/6c/d6e86cff-8620-4ffa-fa5d-93a253ab38df/mzl.bwmfmxug.jpg/576x768bb.jpg",
                "https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/fb/d1/c3/fbd1c331-04a0-122f-eea1-6304c452be6e/mzl.gxopfyrb.jpg/576x768bb.jpg"
            ],
            "appletvScreenshotUrls":[

            ],
            "artworkUrl60":"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/a7/32/8c/a7328c6d-247e-578e-64fb-666ba3990947/source/60x60bb.jpg",
            "artworkUrl512":"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/a7/32/8c/a7328c6d-247e-578e-64fb-666ba3990947/source/512x512bb.jpg",
            "artworkUrl100":"https://is2-ssl.mzstatic.com/image/thumb/Purple124/v4/a7/32/8c/a7328c6d-247e-578e-64fb-666ba3990947/source/100x100bb.jpg",
            "artistViewUrl":"https://apps.apple.com/cn/developer/wechat/id614694882?uo=4",
            "supportedDevices":[
                "iPhone5s-iPhone5s",
                "iPadAir-iPadAir",
                "iPadAirCellular-iPadAirCellular",
                "iPadMiniRetina-iPadMiniRetina",
                "iPadMiniRetinaCellular-iPadMiniRetinaCellular",
                "iPhone6-iPhone6",
                "iPhone6Plus-iPhone6Plus",
                "iPadAir2-iPadAir2",
                "iPadAir2Cellular-iPadAir2Cellular",
                "iPadMini3-iPadMini3",
                "iPadMini3Cellular-iPadMini3Cellular",
                "iPodTouchSixthGen-iPodTouchSixthGen",
                "iPhone6s-iPhone6s",
                "iPhone6sPlus-iPhone6sPlus",
                "iPadMini4-iPadMini4",
                "iPadMini4Cellular-iPadMini4Cellular",
                "iPadPro-iPadPro",
                "iPadProCellular-iPadProCellular",
                "iPadPro97-iPadPro97",
                "iPadPro97Cellular-iPadPro97Cellular",
                "iPhoneSE-iPhoneSE",
                "iPhone7-iPhone7",
                "iPhone7Plus-iPhone7Plus",
                "iPad611-iPad611",
                "iPad612-iPad612",
                "iPad71-iPad71",
                "iPad72-iPad72",
                "iPad73-iPad73",
                "iPad74-iPad74",
                "iPhone8-iPhone8",
                "iPhone8Plus-iPhone8Plus",
                "iPhoneX-iPhoneX",
                "iPad75-iPad75",
                "iPad76-iPad76",
                "iPhoneXS-iPhoneXS",
                "iPhoneXSMax-iPhoneXSMax",
                "iPhoneXR-iPhoneXR",
                "iPad812-iPad812",
                "iPad834-iPad834",
                "iPad856-iPad856",
                "iPad878-iPad878",
                "Watch4-Watch4",
                "iPadMini5-iPadMini5",
                "iPadMini5Cellular-iPadMini5Cellular",
                "iPadAir3-iPadAir3",
                "iPadAir3Cellular-iPadAir3Cellular",
                "iPodTouchSeventhGen-iPodTouchSeventhGen",
                "iPhone11-iPhone11",
                "iPhone11Pro-iPhone11Pro",
                "iPadSeventhGen-iPadSeventhGen",
                "iPadSeventhGenCellular-iPadSeventhGenCellular",
                "iPhone11ProMax-iPhone11ProMax",
                "iPhoneSESecondGen-iPhoneSESecondGen",
                "iPadProSecondGen-iPadProSecondGen",
                "iPadProSecondGenCellular-iPadProSecondGenCellular",
                "iPadProFourthGen-iPadProFourthGen",
                "iPadProFourthGenCellular-iPadProFourthGenCellular",
                "iPhone12Mini-iPhone12Mini",
                "iPhone12-iPhone12",
                "iPhone12Pro-iPhone12Pro",
                "iPhone12ProMax-iPhone12ProMax",
                "iPadAir4-iPadAir4",
                "iPadAir4Cellular-iPadAir4Cellular",
                "iPadEighthGen-iPadEighthGen",
                "iPadEighthGenCellular-iPadEighthGenCellular"
            ],
            "isGameCenterEnabled":false,
            "advisories":[
                "频繁/强烈的成人/性暗示题材"
            ],
            "kind":"software",
            "features":[
                "iosUniversal"
            ],
            "minimumOsVersion":"11.0",
            "trackCensoredName":"微信",
            "languageCodesISO2A":[
                "AR",
                "EN",
                "FR",
                "DE",
                "ID",
                "IT",
                "JA",
                "KO",
                "MS",
                "PT",
                "RU",
                "ZH",
                "ES",
                "TH",
                "ZH",
                "TR",
                "VI"
            ],
            "fileSizeBytes":"477456384",
            "sellerUrl":"http://weixin.qq.com",
            "formattedPrice":"免费",
            "contentAdvisoryRating":"17+",
            "averageUserRatingForCurrentVersion":4.36329999999999973425701682572253048419952392578125,
            "userRatingCountForCurrentVersion":5710077,
            "trackViewUrl":"https://apps.apple.com/cn/app/%E5%BE%AE%E4%BF%A1/id414478124?uo=4",
            "trackContentRating":"17+",
            "averageUserRating":4.36329999999999973425701682572253048419952392578125,
            "releaseDate":"2011-01-21T01:32:15Z",
            "trackId":414478124,
            "trackName":"微信",
            "currentVersionReleaseDate":"2021-02-02T08:15:22Z",
            "releaseNotes":"本次更新:
- 解决了一些已知问题。

最近更新:
- 更新了若干功能。",
            "primaryGenreName":"Social Networking",
            "genreIds":[
                "6005",
                "6007"
            ],
            "isVppDeviceBasedLicensingEnabled":true,
            "primaryGenreId":6005,
            "sellerName":"Tencent Technology (Shenzhen) Company Limited",
            "currency":"CNY",
            "version":"8.0.2",
            "wrapperType":"software",
            "artistId":614694882,
            "artistName":"WeChat",
            "genres":[
                "社交",
                "效率"
            ],
            "price":0,
            "description":"微信是一款全方位的手机通讯应用,帮助你轻松连接全球好友。微信可以群聊、进行视频聊天、与好友一起玩游戏,以及分享自己的生活到朋友圈,让你感受耳目一新的移动生活方式。

  为什么要使用微信:
  • 多媒体消息:支持发送视频、图片、文本和语音消息。
  • 群聊和通话:组建高达500人的群聊和高达9人的实时视频聊天。
  • 语音和视频聊天:提供全球的高质量通话。
  • 表情商店:海量动态表情,包括热门卡通人物和电影,让聊天变得更生动有趣。
  • 朋友圈:与好友分享每个精彩瞬间,记录自己的生活点滴。
  • 隐私保护:严格保护用户的隐私安全,是唯一一款通过TRUSTe认证的实时通讯应用。
  • 认识新朋友:通过“雷达加朋友”、“附近的人”和“摇一摇”认识新朋友。
  • 实时位置共享:与好友分享地理位置,无需通过语言告诉对方。
  • 多语言:支持超过20种语言界面,并支持多国语言的消息翻译。
  • 微信运动:支持接入Apple Watch及健康app数据,可在步数排行榜上和朋友一较高下。若需使用,可在“设置-通用-辅助功能”内启用。 
  • 更多功能: 支持跨平台、聊天室墙纸自定义、消息提醒自定义和公众号服务等。",
            "bundleId":"com.tencent.xin",
            "userRatingCount":5710077
        }
    ]
}

从App Store爬取了微信的基本信息,是一大堆json字符串,我们重点看这个字段:"version":"8.0.2",这个是App Store中的微信版本号,因为是App Store上面获取的,那么一定通过了审核,而且是最新的,而微信App通过iOS中的方法,是可以拿到本地版本号的。

也就说,通过本地版本号和从App Store爬取的版本号对比,实现App添加版本升级功能。

Python可以通过rquest库获取数据,那么Swift必然也可以通过网络请求拿到相同的数据。

那么这个下面这个流程就能走通了:

image.png

在App中通过Swift爬App Store信息 --> 返回App相关的json字符串 --> json字符串解析 --> 获取线上版本version--> 对比本地version --> 实现App版本升级功能

思路梳理好了,下面实现起来,也就快了。

Swift代码实现逻辑

  1. 我们先通过上面的格式化的json,生成对应的模型,我非常喜欢使用Swift中原生自带的Codable协议解析json:
/// 爬虫的数据模型
struct AppInfoAtStore: Codable {
    let resultCount: Int?
    let results: [AppInfoResult]?
}

/// 爬虫的详细数据模型
struct AppInfoResult: Codable {
    let advisories: [String]?
    let appletvScreenshotUrls: [String]?
    let artistId: Int?
    let artistName: String?
    let artistViewUrl: String?
    let artworkUrl100: String?
    let artworkUrl512: String?
    let artworkUrl60: String?
    let averageUserRating: Float?
    let averageUserRatingForCurrentVersion : Float?
    let bundleId: String?
    let contentAdvisoryRating: String?
    let currency: String?
    let currentVersionReleaseDate: String?
    let descriptionField : String?
    let features: [String]?
    let fileSizeBytes: String?
    let formattedPrice: String?
    let genreIds: [String]?
    let genres: [String]?
    let ipadScreenshotUrls: [String]?
    let isGameCenterEnabled: Bool?
    let isVppDeviceBasedLicensingEnabled: Bool?
    let kind: String?
    let languageCodesISO2A: [String]?
    let minimumOsVersion: String?
    let price: Float?
    let primaryGenreId: Int?
    let primaryGenreName: String?
    let releaseDate: String?
    let releaseNotes: String?
    let screenshotUrls: [String]?
    let sellerName: String?
    let supportedDevices: [String]?
    let trackCensoredName: String?
    let trackContentRating: String?
    let trackId: Int?
    let trackName: String?
    let trackViewUrl: String?
    let userRatingCount: Int?
    let userRatingCountForCurrentVersion: Int?
    let version: String?
    let wrapperType: String?
}
  1. 通过Swift中的String类中的方法获取数据,然后通过Codable协议解析json为模型,返回需要的有效信息:
/// 通过AppStore进行更新检查
/// 这个可以看成是一个请求,也可以看成是一个爬虫
/// - Parameter appleId: App Store Connect页面 App信息 综合信息的 Apple Id
/// - Returns: AppStore中的版本信息 本地版本信息 是否需要升级 Bool值 true表示需要升级 升级网址
public func checkAppStoreVersion(appleId: String) -> (appStoreVersion: String?, localVersion: String?, isNeedUpdate: Bool, updateURL: String?) {
    guard let url = URL(string: "http://itunes.apple.com/cn/lookup?id=\(appleId)"),
        let jsonResponseString =  try? String(contentsOf: url, encoding: String.Encoding.utf8),
        let data = jsonResponseString.data(using: .utf8) else {
            return (nil, nil, false, nil)
    }

    /// Codable协议解析
    let appInfoAtStore = try? JSONDecoder().decode(AppInfoAtStore.self, from: data)

    /// appStore中的版本号
    let appStoreVersion = appInfoAtStore?.results?.first?.version

    /// 本地版本号
    let localVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String


    guard let aVersion = appStoreVersion, let lVersion = localVersion else {
        return (nil, nil, false, nil)
    }

    /// 是否需要更新
    let isNeedUpdate = aVersion.compare(lVersion).rawValue > 0

    /// 需要跳转更新的地址
    let updateURL = appInfoAtStore?.results?.first?.trackViewUrl

    /// 返回元组信息
    return (aVersion, lVersion, isNeedUpdate, updateURL)
}

到此,通过App Store获取版本信息,完成App版本升级功能的基本完成了。

思考总结

思考

在完成上面的代码,并测试后,我也想了下面一些问题,下面的观点仅仅是我的理解,希望大家能思考互动一下:

  1. Alamofire可以干爬虫这事么?

答:本质上爬虫也是网络请求,所以Alamofire也可以胜任此工作。

  1. 这种方式爬取的version可以控制强制升级么?

答:如果定好规则,是可以的。比如版本号是X.Y.Z这样,用X,Y位的大小判断是否需要强制升级即可。

  1. 这种方式可以规避App在审核过程中提示升级么?

答:可以的,因为在审核过程中,App Store中还没有上架,此时lVersion大于aVersion,isNeedUpdate为false,不会触发升级。

  1. 这种方式有什么风险点?

答:这个url是苹果提供,苹果会不会停止对外暴露这个接口,这个事情我们说了不算,所以还是存在一定的风险的。最好的还是通过App后台控制。

总结

App后台不提供数据支撑,而这个数据又必须网络请求获取到,那么我们可不可以换个源试试?有没有现成网站或者接口支持该操作的?

虽然这种场景不常见(后台如果说App版本管理做不了,就拿砖头pia飞),但是正是因为思考所以才有了这次尝试。

很多人都不太喜欢用Swift中Codable协议做json转model,孰不知原生的这种方式其实挺好用的,从Alamofire5开始已经原生支持Codable协议解析json了。

元组其实是个好东西,这个从Python借鉴的类型,在较少数据的时候,也有发挥的时候。

Swift中的Data类中也有一个方法,可以直接爬数据为二进制,可以去试试喔。

注意事项

刚刚查阅了一下资料,其实发现一些特别要注意的地方,因为使用的搜索微信App信息作为例子,忽略了一些细节:

如果你的应用是在全世界范围内销售的话, 用上面的是没问题的但是,如果仅仅是在部分地区,比如只在中国商店提供下载,就需要在路径是加上国家的缩写cn。

http://itunes.apple.com/cn/lookup?id=appId (appId为应用 id)

否则你将会得到一个 results : [] 的结果。

参考资料:iOS获取 App详细信息的方法

本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情