检查我的应用在AppStore上是否有新版本

172

我想在用户使用我的应用时手动检查是否有新的更新,并提示他下载新版本。我能通过编程方式检查应用商店中我的应用的版本来实现吗?


11
你可以将一个随机页面放在Web服务器上,该页面仅返回最新版本的字符串表示。在应用程序启动时下载并进行比较,并通知用户。(快速简单的方法) - Pangolin
1
谢谢,但我希望有更好的解决方案,比如某种API,可以调用应用商店的功能,比如搜索我的应用程序编号并获取版本数据。维护一个仅用于此目的的Web服务器需要花费时间,但无论如何还是感谢您的指引! - user542584
我和第一条评论者做了同样的事情。我写了一个只有一个NSNumber版本号的plist文件,然后将其上传到我的网站上。这个网站是我用来支持我的应用程序和应用程序网页的相同网站。在viewDidLoad中,我检查网站上的版本号并检查我的应用程序中的当前版本。然后我有一个预先制作的alertView,它会自动提示更新应用程序。如果您需要,我可以提供代码。 - Andrew
谢谢,我想我也应该尝试一下。 - user542584
2
我已经使用 Google Firebase 实现了一个解决方案。我使用 remoteConfig 来保存所需版本的值,并且当应用程序打开时,我会将应用程序的版本与设置为 Firebase 的版本进行交叉检查。如果应用程序的版本小于 Firebase 的版本,则向用户显示警报。这样,我可以随时强制更新应用程序。 - Stefanos Christodoulides
最好使用Swift本地类型OperatingSystemVersion 使用小数比较应用程序版本更新,如2.5.2 - Leo Dabus
30个回答

111

这是一个简单的代码片段,可以让您了解当前版本是否不同。

-(BOOL) needsUpdate{
    NSDictionary* infoDictionary = [[NSBundle mainBundle] infoDictionary];
    NSString* appID = infoDictionary[@"CFBundleIdentifier"];
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://itunes.apple.com/lookup?bundleId=%@", appID]];
    NSData* data = [NSData dataWithContentsOfURL:url];
    NSDictionary* lookup = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

    if ([lookup[@"resultCount"] integerValue] == 1){
        NSString* appStoreVersion = lookup[@"results"][0][@"version"];
        NSString* currentVersion = infoDictionary[@"CFBundleShortVersionString"];
        if (![appStoreVersion isEqualToString:currentVersion]){
            NSLog(@"Need to update [%@ != %@]", appStoreVersion, currentVersion);
            return YES;
        }
    }
    return NO;
}

注意:在iTunes中输入新版本时,请确保与您发布的应用程序中的版本匹配。否则,上述代码将始终返回“是”,无论用户是否更新。


4
我找到的最棒的解决方案 +1 - Sanjay Changani
1
@MobeenAfzal,我认为你误解了问题和解决方案。上面的解决方案将当前版本与商店中的版本进行比较。如果它们不匹配,则返回YES,否则返回NO。无论应用商店上的历史记录如何,如果当前版本与应用商店版本不同,则上述方法将始终返回YES。一旦用户更新...当前版本等于应用商店版本。如果用户的版本是1.0,应用商店版本是1.2,则上述方法应始终返回YES。 - datinc
1
@MobeenAfzal 我想我明白你的意思了。在代码中,你的版本是1.7,但在iTunes中,你上传的版本是1.6,以便你的用户不知道你跳过了一个版本。如果是这样的话...你需要一个服务器(DropBox可以)来提供你的应用程序版本号,并修改你的代码以访问该端点。如果这就是你所看到的,那么请告诉我,我会在帖子中添加一个警告说明。 - datinc
1
@MobeenAfzal,你的评论是误导性的。如果用户设备上的版本与App Store上的版本不同,则代码将返回预期的YES。即使您发布了1.0版本,然后是1.111版本,它仍将完美地工作。 - datinc
3
当App Store版本高于当前版本时,我们应该显示更新,代码如下:if ([appStoreVersion compare:currentVersion options:NSNumericSearch] == NSOrderedDescending) { NSLog(@"\n\n需要更新。App Store版本 %@ 高于 %@",appStoreVersion, currentVersion); } - Nitesh Borad
显示剩余10条评论

90

Swift 3版本:

func isUpdateAvailable() throws -> Bool {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(identifier)") else {
        throw VersionError.invalidBundleInfo
    }
    let data = try Data(contentsOf: url)
    guard let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] else {
        throw VersionError.invalidResponse
    }
    if let result = (json["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String {
        return version != currentVersion
    }
    throw VersionError.invalidResponse
}

我认为在这种情况下,抛出错误而不是返回false更好,因此我创建了一个VersionError,但也可以是您定义的其他错误或NSError。

enum VersionError: Error {
    case invalidResponse, invalidBundleInfo
}

如果连接速度较慢,考虑从另一个线程调用此函数,这样可以防止当前线程被阻塞。

DispatchQueue.global().async {
    do {
        let update = try self.isUpdateAvailable()
        DispatchQueue.main.async {
            // show alert
        }
    } catch {
        print(error)
    }
}

更新

使用URLSession:

不要使用Data(contentsOf: url)阻塞线程,而是可以使用URLSession

func isUpdateAvailable(completion: @escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "https://itunes.apple.com/lookup?bundleId=\(identifier)") else {
            throw VersionError.invalidBundleInfo
    }
    Log.debug(currentVersion)
    let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
        do {
            if let error = error { throw error }
            guard let data = data else { throw VersionError.invalidResponse }
            let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
            guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String else {
                throw VersionError.invalidResponse
            }
            completion(version != currentVersion, nil)
        } catch {
            completion(nil, error)
        }
    }
    task.resume()
    return task
}

例子:

_ = try? isUpdateAvailable { (update, error) in
    if let error = error {
        print(error)
    } else if let update = update {
        print(update)
    }
}

1
这个答案是同步发出请求的。这意味着在网络连接不好的情况下,您的应用程序可能会在请求返回之前无法使用数分钟。 - uliwitness
7
我不同意,DispatchQueue.global()会给你一个后台队列,在那个队列里面加载数据,只有在数据加载完成后才返回到主队列。 - juanjo
糟糕,我不知怎么错过了第二个代码片段。遗憾的是,在您的答案再次编辑之前,我似乎无法取消踩了 :-( 顺便说一下 - 给定的dataWithContentsOfURL:实际上通过NSURLConnection的同步调用进行,这反过来只是启动一个异步线程并阻塞,使用异步NSURLSession调用可能会减少开销。它们甚至会在完成后在主线程上回调您。 - uliwitness
8
如果你只在特定商店中列出,请注意需要将国家代码添加到URL中 - 例如GB https://itunes.apple.com/\(countryCode)/lookup?bundleId=\(identifier) - Ryan Heitner
1
我建议使用(Result<Bool, Error>) → Void而不是(Bool?, Error?) -> Void,因为后者很难处理。这样我们甚至不需要使用“throws”。 - SwiftiSwift
显示剩余9条评论

30

这个帖子上有一篇精简的优秀答案。使用 Swift 4Alamofire

import Alamofire

class VersionCheck {
  
  public static let shared = VersionCheck()
  
  func isUpdateAvailable(callback: @escaping (Bool)->Void) {
    let bundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
    Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(bundleId)").responseJSON { response in
      if let json = response.result.value as? NSDictionary, let results = json["results"] as? NSArray, let entry = results.firstObject as? NSDictionary, let versionStore = entry["version"] as? String, let versionLocal = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String {
        let arrayStore = versionStore.split(separator: ".").compactMap { Int($0) }
        let arrayLocal = versionLocal.split(separator: ".").compactMap { Int($0) }

        if arrayLocal.count != arrayStore.count {
          callback(true) // different versioning system
          return
        }

        // check each segment of the version
        for (localSegment, storeSegment) in zip(arrayLocal, arrayStore) {
          if localSegment < storeSegment {
            callback(true)
            return
          }
        }
      }
      callback(false) // no new version or failed to fetch app store version
    }
  }
  
}

然后使用它:

VersionCheck.shared.isUpdateAvailable() { hasUpdates in
  print("is update available: \(hasUpdates)")
}

5
我的应用已经上线了,但是同样的API没有返回版本信息。响应为:{ "resultCount":0, "results": [] } - technerd
在某些回调函数之后应该加上return - Libor Zapletal
@LiborZapletal 谢谢。已经修复了问题,并且稍微更新了一下代码。 - budiDino
嘿 @budiDino,看起来这段代码只返回应用程序版本的前两个数字。例如:如果在应用商店上是1.2.3版本,则该代码将仅返回1.2。有没有办法也获取最后一个数字呢?谢谢。 - stackich
@stackich,你尝试过检查从iTunes API返回的“version”:https://itunes.apple.com/lookup?bundleId=\(bundleId)以及本地版本:Bundle.main.infoDictionary?["CFBundleShortVersionString"]吗?不确定为什么其中任何一个会只返回前两个部分,如果存在3个部分的话 :/ - budiDino
显示剩余4条评论

25

Anup Gupta处更新了swift 4代码。

我对这段代码进行了一些修改。现在,函数是从后台队列中调用的,因为连接可能很慢,从而阻塞主线程。

我还使CFBundleName可选,因为所呈现的版本具有“CFBundleDisplayName”,但在我的版本中无法正常工作。因此,现在如果不存在它,它不会崩溃,只是不会在警报中显示应用程序名称。

import UIKit

enum VersionError: Error {
    case invalidBundleInfo, invalidResponse
}

class LookupResult: Decodable {
    var results: [AppInfo]
}

class AppInfo: Decodable {
    var version: String
    var trackViewUrl: String
}

class AppUpdater: NSObject {

    private override init() {}
    static let shared = AppUpdater()

    func showUpdate(withConfirmation: Bool) {
        DispatchQueue.global().async {
            self.checkVersion(force : !withConfirmation)
        }
    }

    private  func checkVersion(force: Bool) {
        let info = Bundle.main.infoDictionary
        if let currentVersion = info?["CFBundleShortVersionString"] as? String {
            _ = getAppInfo { (info, error) in
                if let appStoreAppVersion = info?.version{
                    if let error = error {
                        print("error getting app store version: ", error)
                    } else if appStoreAppVersion == currentVersion {
                        print("Already on the last app version: ",currentVersion)
                    } else {
                        print("Needs update: AppStore Version: \(appStoreAppVersion) > Current version: ",currentVersion)
                        DispatchQueue.main.async {
                            let topController: UIViewController = UIApplication.shared.keyWindow!.rootViewController!
                            topController.showAppUpdateAlert(Version: (info?.version)!, Force: force, AppURL: (info?.trackViewUrl)!)
                        }
                    }
                }
            }
        }
    }

    private func getAppInfo(completion: @escaping (AppInfo?, Error?) -> Void) -> URLSessionDataTask? {
        guard let identifier = Bundle.main.infoDictionary?["CFBundleIdentifier"] as? String,
            let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
                DispatchQueue.main.async {
                    completion(nil, VersionError.invalidBundleInfo)
                }
                return nil
        }
        let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
            do {
                if let error = error { throw error }
                guard let data = data else { throw VersionError.invalidResponse }
                let result = try JSONDecoder().decode(LookupResult.self, from: data)
                guard let info = result.results.first else { throw VersionError.invalidResponse }

                completion(info, nil)
            } catch {
                completion(nil, error)
            }
        }
        task.resume()
        return task
    }
}

extension UIViewController {
    @objc fileprivate func showAppUpdateAlert( Version : String, Force: Bool, AppURL: String) {
        let appName = Bundle.appName()

        let alertTitle = "New Version"
        let alertMessage = "\(appName) Version \(Version) is available on AppStore."

        let alertController = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)

        if !Force {
            let notNowButton = UIAlertAction(title: "Not Now", style: .default)
            alertController.addAction(notNowButton)
        }

        let updateButton = UIAlertAction(title: "Update", style: .default) { (action:UIAlertAction) in
            guard let url = URL(string: AppURL) else {
                return
            }
            if #available(iOS 10.0, *) {
                UIApplication.shared.open(url, options: [:], completionHandler: nil)
            } else {
                UIApplication.shared.openURL(url)
            }
        }

        alertController.addAction(updateButton)
        self.present(alertController, animated: true, completion: nil)
    }
}
extension Bundle {
    static func appName() -> String {
        guard let dictionary = Bundle.main.infoDictionary else {
            return ""
        }
        if let version : String = dictionary["CFBundleName"] as? String {
            return version
        } else {
            return ""
        }
    }
}

我建议同时添加一个确认按钮:

AppUpdater.shared.showUpdate(withConfirmation: true)

或者将其称为这样,以便在其中具有强制更新选项:

AppUpdater.shared.showUpdate(withConfirmation: false)

有没有关于如何测试这个的想法?如果它不能正常工作,唯一调试的方法就是以某种方式调试比应用商店中版本旧的版本。 - David Rector
2
啊,没关系了。我可以简单地将我的本地版本改为“旧一些”。 - David Rector
1
我对你的代码@Vasco印象深刻。只是一个简单的问题,为什么在那个URL中你使用了“http”而不是“https”? - Master AgentX
非常感谢您分享这个解决方案,@Vasco!我很喜欢它 :) 为什么您不使用以下代码:let config = URLSessionConfiguration.background(withIdentifier: "com.example.MyExample.background") 来实现后台请求的URLSession呢? - mc_plectrum
您还可以摆脱强制解包,因为您已经检查了if let appStoreAppVersion = info?.version和trackURL的情况。 - mc_plectrum

16

由于我也遇到了同样的问题,我找到了Mario Hendricks提供的答案。不幸的是,当我尝试应用他的代码到我的项目时,XCode会抱怨Casting问题,说"MDLMaterialProperty没有下标成员"。他的代码试图将这个MDLMaterial...设置为常量"lookupResult"的类型,导致每次都失败。我的解决方案是为我的变量提供一个类型注释NSDictionary,以清楚表明我需要的值的类型。有了这个,我可以访问我需要的"value"。

注意:对于此YOURBUNDLEID,您可以从Xcode项目中获取... "Targets > General > Identity > Bundle Identifier"

所以这里是我的代码,并进行了一些简化:

func appUpdateAvailable() -> Bool
{
    let storeInfoURL: String = "http://itunes.apple.com/lookup?bundleId=YOURBUNDLEID"
    var upgradeAvailable = false
    // Get the main bundle of the app so that we can determine the app's version number
    let bundle = NSBundle.mainBundle()
    if let infoDictionary = bundle.infoDictionary {
        // The URL for this app on the iTunes store uses the Apple ID for the  This never changes, so it is a constant
        let urlOnAppStore = NSURL(string: storeInfoURL)
        if let dataInJSON = NSData(contentsOfURL: urlOnAppStore!) {
            // Try to deserialize the JSON that we got
            if let dict: NSDictionary = try? NSJSONSerialization.JSONObjectWithData(dataInJSON, options: NSJSONReadingOptions.AllowFragments) as! [String: AnyObject] {
                if let results:NSArray = dict["results"] as? NSArray {
                    if let version = results[0].valueForKey("version") as? String {
                        // Get the version number of the current version installed on device
                        if let currentVersion = infoDictionary["CFBundleShortVersionString"] as? String {
                            // Check if they are the same. If not, an upgrade is available.
                            print("\(version)")
                            if version != currentVersion {
                                upgradeAvailable = true
                            }
                        }
                    }
                }
            }
        }
    }
    return upgradeAvailable
}

欢迎提出对这段代码的任何改进建议!


1
这个答案是同步发出请求的。这意味着在网络连接不好的情况下,您的应用程序可能会在请求返回之前无法使用数分钟。 - uliwitness
@Yago Zardo请使用比较函数,否则当用户上传应用时,苹果测试时间显示更新警报视图或苹果将拒绝您的应用。 - Jigar
嘿@Jigar,感谢你的建议。我目前在我的应用程序中不再使用这种方法,因为现在我们在服务器上对所有内容进行版本控制。无论如何,你能更好地解释一下你说的话吗?我没有理解,但它看起来是一个很好的东西需要了解。提前致谢。 - Yago Zardo
感谢 @uliwitness 的提示,它真的帮助我改进了我的代码,让我学习了关于异步和同步请求的知识。 - Yago Zardo
1
喜欢那个金字塔形结构。(考虑使用 guard 代替 if。) - adamjansch
显示剩余2条评论

15

这是我的代码:

NSString *appInfoUrl = @"http://itunes.apple.com/lookup?bundleId=XXXXXXXXX";

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setURL:[NSURL URLWithString:appInfoUrl]];
[request setHTTPMethod:@"GET"];

NSURLResponse *response;
NSError *error;
NSData *data = [NSURLConnection  sendSynchronousRequest:request returningResponse: &response error: &error];
NSString *output = [NSString stringWithCString:[data bytes] length:[data length]];

NSError *e = nil;
NSData *jsonData = [output dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error: &e];

NSString *version = [[[jsonDict objectForKey:@"results"] objectAtIndex:0] objectForKey:@"version"];

1
非常好的正确解决方案,只需更新URL为http://itunes.apple.com/en/lookup?bundleId=xxxxxxxxxx。 - S.J
谢谢,您的评论已应用。 - Roozbeh Zabihollahi
5
实际上,对于我的情况,使用/en/子路径并没有起作用。将其删除后,它就可用了。 - gasparuff
这个答案是同步发出请求的。这意味着在网络连接不好的情况下,您的应用程序可能会在请求返回之前无法使用数分钟。 - uliwitness
1
我不得不使用/en/ https://itunes.apple.com/lookup?bundleId=xxxxxxx,谢谢@gasparuff。 - Fernando Perez

15

只需使用ATAppUpdater。它只有一行代码,线程安全且快速。如果您想要跟踪用户操作,它还具有代理方法。

以下是一个示例:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[ATAppUpdater sharedUpdater] showUpdateWithConfirmation]; // 1 line of code
    // or
    [[ATAppUpdater sharedUpdater] showUpdateWithForce]; // 1 line of code

   return YES;
}

可选的委托方法:

- (void)appUpdaterDidShowUpdateDialog;
- (void)appUpdaterUserDidLaunchAppStore;
- (void)appUpdaterUserDidCancel;

1
这个能在Testflight的beta版本中使用吗?如果不能,有没有其他工具可以使用? - Lukasz Czerwinski
不会,它只比较当前版本与AppStore上最新版本。 - emotality
我们能在Swift中使用这个吗? - Zorayr
实际上它并不总是一个数字样式的版本,因此应该将版本比较公开。 - Itachi
@Itachi,那是5.5年前的事了 :) 这个软件包甚至都不再维护了。 - emotality

14

这是我的版本,使用 Swift 4 和流行的 Alamofire 库(我在我的应用程序中也使用它)。请求是异步的,您可以传递回调函数来在完成时得到通知。

import Alamofire

class VersionCheck {

    public static let shared = VersionCheck()

    var newVersionAvailable: Bool?
    var appStoreVersion: String?

    func checkAppStore(callback: ((_ versionAvailable: Bool?, _ version: String?)->Void)? = nil) {
        let ourBundleId = Bundle.main.infoDictionary!["CFBundleIdentifier"] as! String
        Alamofire.request("https://itunes.apple.com/lookup?bundleId=\(ourBundleId)").responseJSON { response in
            var isNew: Bool?
            var versionStr: String?

            if let json = response.result.value as? NSDictionary,
               let results = json["results"] as? NSArray,
               let entry = results.firstObject as? NSDictionary,
               let appVersion = entry["version"] as? String,
               let ourVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
            {
                isNew = ourVersion != appVersion
                versionStr = appVersion
            }

            self.appStoreVersion = versionStr
            self.newVersionAvailable = isNew
            callback?(isNew, versionStr)
        }
    }
}

使用方法很简单:

VersionCheck.shared.checkAppStore() { isNew, version in
        print("IS NEW VERSION AVAILABLE: \(isNew), APP STORE VERSION: \(version)")
    }

1
使用 ourVersion != appVersion 的问题在于,当 App Store 审核团队检查应用程序的新版本时,它会触发。我们将这些版本字符串转换为数字,然后 isNew = appVersion > ourVersion。 - budiDino
@budidino,你说得对,我只是展示了使用Alamofire的常见方法。如何解释版本完全取决于您的应用程序和版本结构。 - Northern Captain
只是在版本比较中添加一条注释,我更喜欢使用以下代码:let serverVersion = "2.7" let localVersion = "2.6.5" let isUpdateAvailable = serverVersion.compare(localVersion, options: .numeric) == .orderedDescending而不是使用相等比较。 - Chaitu

13

Swift 5(缓存问题已解决)

enum VersionError: Error {
    case invalidResponse, invalidBundleInfo
}

@discardableResult
func isUpdateAvailable(completion: @escaping (Bool?, Error?) -> Void) throws -> URLSessionDataTask {
    guard let info = Bundle.main.infoDictionary,
        let currentVersion = info["CFBundleShortVersionString"] as? String,
        let identifier = info["CFBundleIdentifier"] as? String,
        let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else {
            throw VersionError.invalidBundleInfo
    }
        
    let request = URLRequest(url: url, cachePolicy: URLRequest.CachePolicy.reloadIgnoringLocalCacheData)
    
    let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
        do {
            if let error = error { throw error }
            
            guard let data = data else { throw VersionError.invalidResponse }
                        
            let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any]
                        
            guard let result = (json?["results"] as? [Any])?.first as? [String: Any], let lastVersion = result["version"] as? String else {
                throw VersionError.invalidResponse
            }
            completion(lastVersion > currentVersion, nil)
        } catch {
            completion(nil, error)
        }
    }
    
    task.resume()
    return task
}

实现

            try? isUpdateAvailable {[self] (update, error) in
                if let error = error {
                    print(error)
                } else if update ?? false {
                    // show alert
                }
            }

请添加更多细节以扩展您的答案,例如工作代码或文档引用。 - Community
4
在编写函数时,应该始终为其添加@discardableResult,而不是使用_ = - Laszlo
@Laszlo 谢谢你提醒我。 - Aloha
2
嗨,不确定您所说的缓存问题是否已解决,但是在应用程序发布后,需要几个小时才能刷新缓存。谢谢。 - Houman

8

3
你可以直接在应用商店查看版本号,而不需要在某个地方托管 plist 文件。查看这个答案:https://dev59.com/1mw15IYBdhLWcg3w4_7k#6569307 - Steve Moser
1
iVersion现在自动使用应用商店版本 - 如果您想指定与iTunes上的不同发布说明,则Plist是可选的,但您不需要使用它。 - Nick Lockwood
1
这段代码可以进行一些改进,但比其他发送同步请求的答案要好得多。不过,它处理线程的方式是不好的风格。我会在Github上提交问题。 - uliwitness
该项目现已被弃用。 - Zorayr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接