如何检测当前运行的应用程序是不是是从应用商店安装的?

Posted

技术标签:

【中文标题】如何检测当前运行的应用程序是不是是从应用商店安装的?【英文标题】:How can I detect if the currently running app was installed from the app store?如何检测当前运行的应用程序是否是从应用商店安装的? 【发布时间】:2013-08-16 21:23:44 【问题描述】:

ios 中有没有一种方法可以以编程方式检查当前运行的应用程序是否是从 iOS App Store 安装的?这与通过 Xcode、TestFlight 或任何非官方分发源运行的应用程序形成鲜明对比。

这是在无法访问应用程序源代码的 SDK 的上下文中。

说清楚 - 我正在寻找一些签名,可以这么说,给应用程序(可能是由苹果公司),在不依赖任何预处理器标志或其他构建配置的情况下,任何应用程序都可以在运行时访问.

【问题讨论】:

请澄清。 SDK是什么意思?你在建图书馆吗?您是否正在构建一个输出 App à la Titanium 的 SDK?在这种情况下,您仍然在构建应用程序,因此虽然您可能无法访问“子应用程序”源代码,但您绝对可以控制正在运行的应用程序。 要明确 - 您是否正在寻找一些签名,可以这么说,给应用程序(可能由 Apple),不依赖任何预处理器标志或其他构建配置,可以访问运行时有任何应用程序吗? @CarlVeazey 是的,就是这样。 @magma 不,我正在构建一个纯 SDK,其他开发人员可以下载并集成到他们的 Xcode 构建中。 【参考方案1】:

从 App Store 下载的应用有一个由应用商店添加的iTunesMetadata.plist 文件:

NSString *file=[NSHomeDirectory() stringByAppendingPathComponent:@"iTunesMetadata.plist"];
if ([[NSFileManager defaultManager] fileExistsAtPath:file]) 
    // probably a store app

也许您可能想检查此文件是否存在。

更新

在 iOS8 中,应用程序包已被移动。根据@silyevsk 的说法,plist 现在比 [新应用程序主包路径] 高一级,位于 /private/var/mobile/Containers/Bundle/Application/4A74359F-E6CD-44C9-925D-AC82E‌​​B5EA837 /iTunesMetadata.plist,不幸的是,无法从应用程序访问它(权限被拒绝)

2015 年 11 月 4 日更新

看来检查收据名称会有所帮助。必须注意的是,这个解决方案略有不同:它不会返回我们是否正在运行 App Store 应用程序,而是返回我们是否正在运行测试版 Testflight 应用程序。根据您的上下文,这可能有用也可能没用。

最重要的是,这是一个非常脆弱的解决方案,因为收据名称可能随时更改。无论如何我都会报告它,以防您没有其他选择:

// Objective-C
BOOL isRunningTestFlightBeta = [[[[NSBundle mainBundle] appStoreReceiptURL] lastPathComponent] isEqualToString:@"sandboxReceipt"];

// Swift
let isRunningTestFlightBeta = NSBundle.mainBundle().appStoreReceiptURL?.lastPathComponent=="sandboxReceipt"

来源:Detect if iOS App is Downloaded from Apple's Testflight

HockeyKit 是如何做到的

通过组合各种检查,您可以猜测应用是在模拟器、Testflight 构建还是在 AppStore 构建中运行。

这是来自 HockeyKit 的片段:

BOOL bit_isAppStoreReceiptSandbox(void) 
#if TARGET_OS_SIMULATOR
  return NO;
#else
  NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL;
  NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent;
  
  BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"];
  return isSandboxReceipt;
#endif


BOOL bit_hasEmbeddedMobileProvision(void) 
  BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
  return hasEmbeddedMobileProvision;


BOOL bit_isRunningInTestFlightEnvironment(void) 
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (bit_isAppStoreReceiptSandbox() && !bit_hasEmbeddedMobileProvision()) 
    return YES;
  
  return NO;
#endif


BOOL bit_isRunningInAppStoreEnvironment(void) 
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (bit_isAppStoreReceiptSandbox() || bit_hasEmbeddedMobileProvision()) 
    return NO;
  
  return YES;
#endif


BOOL bit_isRunningInAppExtension(void) 
  static BOOL isRunningInAppExtension = NO;
  static dispatch_once_t checkAppExtension;
  
  dispatch_once(&checkAppExtension, ^
    isRunningInAppExtension = ([[[NSBundle mainBundle] executablePath] rangeOfString:@".appex/"].location != NSNotFound);
  );
  
  return isRunningInAppExtension;

来源:GitHub - bitstadium/HockeySDK-iOS - BITHockeyHelper.m

一个可能的 Swift 类,基于 HockeyKit 的类,可能是:

//
//  WhereAmIRunning.swift
//  https://gist.github.com/mvarie/63455babc2d0480858da
//
//  ### Detects whether we're running in a Simulator, TestFlight Beta or App Store build ###
//
//  Based on https://github.com/bitstadium/HockeySDK-iOS/blob/develop/Classes/BITHockeyHelper.m
//  Inspired by https://***.com/questions/18282326/how-can-i-detect-if-the-currently-running-app-was-installed-from-the-app-store
//  Created by marcantonio on 04/11/15.
//

import Foundation

class WhereAmIRunning 
    
    // MARK: Public
    
    func isRunningInTestFlightEnvironment() -> Bool
        if isSimulator() 
            return false
         else 
            if isAppStoreReceiptSandbox() && !hasEmbeddedMobileProvision() 
                return true
             else 
                return false
            
        
    
    
    func isRunningInAppStoreEnvironment() -> Bool 
        if isSimulator()
            return false
         else 
            if isAppStoreReceiptSandbox() || hasEmbeddedMobileProvision() 
                return false
             else 
                return true
            
        
    

    // MARK: Private

    private func hasEmbeddedMobileProvision() -> Bool
        if let _ = NSBundle.mainBundle().pathForResource("embedded", ofType: "mobileprovision") 
            return true
        
        return false
    
    
    private func isAppStoreReceiptSandbox() -> Bool 
        if isSimulator() 
            return false
         else 
            if let appStoreReceiptURL = NSBundle.mainBundle().appStoreReceiptURL,
                let appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent
                where appStoreReceiptLastComponent == "sandboxReceipt" 
                    return true
            
            return false
        
    
    
    private func isSimulator() -> Bool 
        #if arch(i386) || arch(x86_64)
            return true
            #else
            return false
        #endif
    
    

要点:GitHub - mvarie/WhereAmIRunning.swift

2016 年 12 月 9 日更新

用户 halileohalilei 报告说“这不再适用于 iOS10 和 Xcode 8。”。我没有验证这一点,但请查看更新后的 HockeyKit 源代码(参见函数 bit_currentAppEnvironment):

来源:GitHub - bitstadium/HockeySDK-iOS - BITHockeyHelper.m

随着时间的推移,上面的类已经被修改,它似乎也可以处理iOS10。

2020 年 10 月 6 日更新

曲棍球已被 Microsoft 的 AppCenter SDK 弃用/放弃并取而代之。

这是他们的 App Store / Testflight 构建检测类(代码下方的存储库链接):

MSUtility+Environment.h

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#import <Foundation/Foundation.h>

#import "MSUtility.h"

/*
 * Workaround for exporting symbols from category object files.
 */
extern NSString *MSUtilityEnvironmentCategory;

/**
 *  App environment
 */
typedef NS_ENUM(NSInteger, MSEnvironment) 

  /**
   *  App has been downloaded from the AppStore.
   */
  MSEnvironmentAppStore = 0,

  /**
   *  App has been downloaded from TestFlight.
   */
  MSEnvironmentTestFlight = 1,

  /**
   *  App has been installed by some other mechanism.
   *  This could be Ad-Hoc, Enterprise, etc.
   */
  MSEnvironmentOther = 99
;

/**
 * Utility class that is used throughout the SDK.
 * Environment part.
 */
@interface MSUtility (Environment)

/**
 * Detect the environment that the app is running in.
 *
 * @return the MSEnvironment of the app.
 */
+ (MSEnvironment)currentAppEnvironment;

@end

MSUtility+Environment.m

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#import "MSUtility+Environment.h"

/*
 * Workaround for exporting symbols from category object files.
 */
NSString *MSUtilityEnvironmentCategory;

@implementation MSUtility (Environment)

+ (MSEnvironment)currentAppEnvironment 
#if TARGET_OS_SIMULATOR || TARGET_OS_OSX || TARGET_OS_MACCATALYST
  return MSEnvironmentOther;
#else

  // MobilePovision profiles are a clear indicator for Ad-Hoc distribution.
  if ([self hasEmbeddedMobileProvision]) 
    return MSEnvironmentOther;
  

  /**
   * TestFlight is only supported from iOS 8 onwards and as our deployment target is iOS 8, we don't have to do any checks for
   * floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_6_1).
   */
  if ([self isAppStoreReceiptSandbox]) 
    return MSEnvironmentTestFlight;
  

  return MSEnvironmentAppStore;
#endif


+ (BOOL)hasEmbeddedMobileProvision 
  BOOL hasEmbeddedMobileProvision = !![[NSBundle mainBundle] pathForResource:@"embedded" ofType:@"mobileprovision"];
  return hasEmbeddedMobileProvision;


+ (BOOL)isAppStoreReceiptSandbox 
#if TARGET_OS_SIMULATOR
  return NO;
#else
  if (![NSBundle.mainBundle respondsToSelector:@selector(appStoreReceiptURL)]) 
    return NO;
  
  NSURL *appStoreReceiptURL = NSBundle.mainBundle.appStoreReceiptURL;
  NSString *appStoreReceiptLastComponent = appStoreReceiptURL.lastPathComponent;

  BOOL isSandboxReceipt = [appStoreReceiptLastComponent isEqualToString:@"sandboxReceipt"];
  return isSandboxReceipt;
#endif


@end

来源:GitHub - microsoft/appcenter-sdk-apple - MSUtility+Environment.m

【讨论】:

"NSHomeDirectory()" 将返回“~/iTunesMetadata.plist”的路径,这听起来像是在应用程序沙箱之外。您确定这是检查的正确路径吗? @MichaelDautermann:好点!但是在 iOS 中 NSHomeDirectory() 的行为有所不同:“在 iOS 中,主目录是应用程序的沙箱目录。在 OS X 中,它是应用程序的沙箱目录或当前用户的主目录(如果应用程序不在沙箱中)” 我会试试这个并验证它是否有效,感谢您的建议。 @kevlar - 这在 ios8 中停止为我工作,您是否看到相同的行为 其实是上一层,在/private/var/mobile/Containers/Bundle/Application/4A74359F-E6CD-44C9-925D-AC82E‌​B5EA837/iTunesMetadata.plist,不幸的是,这可以'不能从应用程序访问(权限被拒绝)【参考方案2】:

如果您谈论的是您自己的应用程序,您可以添加一个状态,如果它是作为商店版本的一部分构建的(例如编译器条件),则返回 true,在其他情况下返回 false。

如果您在谈论另一个应用程序,那么查询沙盒之外的其他应用程序并不容易或直接(或者甚至不可能)。

【讨论】:

感谢您的快速回复迈克尔。我已经编辑了我的问题以指定我正在构建一个 SDK,因此我无法直接访问应用程序的代码。你知道有什么解决方法吗?【参考方案3】:

由于@magma 的代码不再适用于 IOS11.1 这是一个有点冗长的解决方案。

我们检查应用商店中的应用版本并将其与 Bundle 中的版本进行比较

static func isAppStoreVersion(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 else 
        throw VersionError.invalidBundleInfo
    
    let urlString = "https://itunes.apple.com/gb/lookup?bundleId=\(identifier)"
    guard let url = URL(string:urlString) else  throw VersionError.invalidBundleInfo 
    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 appStoreVersion = result["version"] as? String else 
          throw VersionError.invalidResponse
        
        completion(appStoreVersion == currentVersion, nil)
       catch 
        completion(nil, error)
      
    
    task.resume()
    return task

这样称呼

DispatchQueue.global(qos: .background).async 

    _ = try? VersionManager.isAppStoreVersion  (appStoreVersion, error) in
      if let error = error 
        print(error)
       else if let appStoreVersion = appStoreVersion, appStoreVersion == true 
         // app store stuf
       else 
        // other stuff

      
    


enum VersionError: Error 
    case invalidResponse, invalidBundleInfo

【讨论】:

好的,如果应用程序版本不是最新的会发生什么? 我不太明白你的问题。我的代码只是将捆绑包版本与应用商店上的版本进行比较。 当新版本在商店发布时,并非所有用户都会立即更新(旧 iOS 版本,App Store Connect 中的部分更新设置等)。因此,使用旧版本的用户将看到调试版本的行为。 如果应用商店的版本高于捆绑版本,您可以推断它是尚未更新的旧版本。如果相同,则为更新版本,如果捆绑包高于应用商店,则您正在运行未发布或调试版本。【参考方案4】:

我的观察是当一个设备连接到 Xcode,然后我们打开管理器,切换到设备窗格它会列出所有未从 App Store 安装的应用程序。所以你需要做的是下载 Xcode,然后连接你的设备,进入设备面板,查看哪些应用程序是从非 App Store 源安装的。这是最简单的解决方案。

【讨论】:

我相信 Kevlar 希望能够以编程方式(即从他的代码中)而不是通过 Xcode 手动执行此操作。

以上是关于如何检测当前运行的应用程序是不是是从应用商店安装的?的主要内容,如果未能解决你的问题,请参考以下文章

检测当前订阅是不是为 Google Play 商店的试用版?

android 应用程序如何检测安装它的商店?

检查 Windows 10 机器中是不是安装了非应用商店应用程序

是否可以在运行时可靠地检测到哪个商店安装了 Android 应用程序(Google Play 或亚马逊市场)?

检测当前订阅是否在Google Play商店试用版中?

vivo如何关闭应用安装检测