iOS无线安装

作者: Cherry_06 | 来源:发表于2020-08-11 17:53 被阅读0次

OTA
OTA:Over-the-Air,iOS4新加的一项技术,脱离Appstore,实现从自己的服务器下载并安装iOS应用。

实现原理
通过Safari解析链接中的"items-services://"来实现直接安装。(个人账号需要注册设备,企业账号无需注册设备)
Safari会去读取installIPA.plist中的信息,如:iOS应用的名称、版本、安装等。

准备工作

  1. 支持HTTPS的服务器
  2. ipa文件
  3. mainifest.plist文件
  4. icon文件:2张 尺寸: 512x512 57x57

实现步骤

  1. 用Xcode打包release版本
  2. 搭建本地Web服务器
  3. 开启HTTPS
  4. 编写好对应的.plist文件,xcode出包时可以勾选 include mainifest for over-the-air installation,会自动生成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>【xxxxx】</string>
                </dict>
                <dict>
                    <key>kind</key>
                    <string>display-image</string>
                    <key>url</key>
                    <string>【xxxxx】</string>
                </dict>
                <dict>
                    <key>kind</key>
                    <string>full-size-image</string>
                    <key>url</key>
                    <string>【xxxxx】</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>【xxxxx】</string>
                <key>bundle-version</key>
                <string>【xxxxx】</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>【xxxxx】</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

字段参考:

字段 说明
software-package https://***.ipa .ipa 安装包地址
display-image https://***.png .57x57 图片地址
full-size-image https://***.png .512x512 图片地址
bundle-identifier com.xxx.xxxx 包名
bundle-version x.x.x 版本号
title 包名 应用名称
  1. 上传ipa、.plist、ca证书到Web服务器,配置好index.html
    通过<a>标签,跳转itms-services协议链接的方式,来下载安装App
    OTA安装协议参考:
itms-services://?action=download-manifest&url=【替换为 manifest.plist 文件地址】

index示例:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>达人店App下载</title>
</head>
<body>
<a href="itms-services://?action=download-manifest&url=https://coding.net/u/richluisx/p/talent-shop/git/raw/master/manifest.plist">点击安装</a>
</body>
</html>

如何检测app是否已经安装?

因为安装app的过程不在当前的程序中进行,而是通过safari跳转url。根据苹果的沙箱原则,程序内是无法监听到安装后续事件的。只能寻找其它方式。

第一种方式是通过系统的canOpenURL来判断,但是这种方式有两个明显的问题:

  1. iOS9以上需要进行白名单设置,在info.plist里面添加
  2. 需要知道目标app的schemeURL,而且可以给另外一个app配置同样的URL schemes的方式来进行欺骗。

第二种方式是通过私有API来获取安装列表,LSApplicationWorkspace这个类是管理app的私有类。
iOS10及以下可通过allApplications获取安装列表,iOS11只能通过私有api containerWithIdentifier:error 获取是否安装某个应用,iOS12没有权限获取app列表了,只能通过应用的相关插件installedPlugins判断是否安装了某个应用,如果应用本身没有使用插件,就不好判断了。

iOS10及以下:

    Class LSApplicationWorkspace_class = objc_getClass("LSApplicationWorkspace");
    NSObject* workspace = [LSApplicationWorkspace_class performSelector:@selector(defaultWorkspace)];
    NSArray *allApplications = [workspace performSelector:@selector(allApplications)];//这样就能获取到手机中安装的所有App

    Class LSApplicationProxy_class = object_getClass(@"LSApplicationProxy");
    for (LSApplicationProxy_class in allApplications)
    {
        //这里可以查看一些信息
        NSString *bundleID = [LSApplicationProxy_class performSelector:@selector(applicationIdentifier)];
        NSString *version =  [LSApplicationProxy_class performSelector:@selector(bundleVersion)];
        NSString *shortVersionString =  [LSApplicationProxy_class performSelector:@selector(shortVersionString)];
        NSLog(@"bundleID:%@\n version: %@\n ,shortVersionString:%@\n", bundleID,version,shortVersionString);
    }

iOS11以上iOS12以下:

- (BOOL)isAppInstalled:(NSString *)bundleId {
    NSBundle *container = [NSBundle bundleWithPath:@"/System/Library/PrivateFrameworks/MobileContainerManager.framework"];
    if ([container load]) {
        Class appContainer = NSClassFromString(@"MCMAppContainer");

        id test = [appContainer performSelector:@selector(containerWithIdentifier:error:) withObject:bundleId withObject:nil];
        NSLog(@"%@", test);
        if (test) {
            return YES;
        } else {
            return NO;
        }
    }
    return NO;
}

iOS12以上:

- (void)showAppList {
    id space = [NSClassFromString(@"LSApplicationWorkspace") performSelector:@selector(defaultWorkspace)];
    NSArray *plugins = [space performSelector:@selector(installedPlugins)];
    NSMutableSet *list = [[NSMutableSet alloc] init];
    for (id plugin in plugins) {
        id bundle = [plugin performSelector:@selector(containingBundle)];
        if (bundle) [list addObject:bundle];
    }
    for (id plugin in list) {
        id appIdentifier = [plugin performSelector:@selector(applicationIdentifier)];
        if (appIdentifier) {
            NSLog(@"app identifier is %@", appIdentifier);
        }
    }
}

下面是LSApplicationWorkspace私有类的一些方法:

@interface LSApplicationWorkspace:NSObject{

} 

+ (id)defaultWorkspace; 

- (id)URLOverrideForURL:(id)arg1; 

- (void)_LSClearSchemaCaches; 

- (bool)_LSPrivateRebuildApplicationDatabasesForSystemApps:(bool)arg1 

                                                  internal:(bool)arg2 

                                                      user:(bool)arg3; 

- (void)_clearCachedAdvertisingIdentifier; 

- (void)addObserver:(id)arg1; 

- (id)allApplications; 

- (id)allInstalledApplications; 

- (id)applicationForOpeningResource:(id)arg1; 

- (id)applicationForUserActivityDomainName:(id)arg1; 

- (id)applicationForUserActivityType:(id)arg1; 

- (bool)applicationIsInstalled:(id)arg1; 

- (id)applicationsAvailableForHandlingURLScheme:(id)arg1; 

- (id)applicationsAvailableForOpeningDocument:(id)arg1; 

- (id)applicationsOfType:(unsigned long long)arg1; 

- (id)applicationsWithAudioComponents; 

- (id)applicationsWithExternalAccessoryProtocols; 

- (id)applicationsWithSettingsBundle; 

- (id)applicationsWithUIBackgroundModes; 

- (id)applicationsWithVPNPlugins; 

- (void)clearAdvertisingIdentifier; 

- (void)clearCreatedProgressForBundleID:(id)arg1; 

- (id)delegateProxy; 

- (id)deviceIdentifierForAdvertising; 

- (id)deviceIdentifierForVendor; 

- (id)directionsApplications; 

- (bool)establishConnection; 

- (bool)getClaimedActivityTypes:(id*)arg1 domains:(id*)arg2; 

- (void)getKnowledgeUUID:(id*)arg1 andSequenceNumber:(id*)arg2; 

- (bool)installApplication:(id)arg1 

              withOptions:(id)arg2 

                    error:(id*)arg3 

                usingBlock:(id)arg4; 

- (bool)installApplication:(id)arg1 withOptions:(id)arg2 error:(id*)arg3; 

- (bool)installApplication:(id)arg1 withOptions:(id)arg2; 

- (bool)installPhaseFinishedForProgress:(id)arg1; 

- (id)installProgressForApplication:(id)arg1 withPhase:(unsigned long long)arg2; 

- (id)installProgressForBundleID:(id)arg1 makeSynchronous:(unsigned char)arg2; 

- (id)installedPlugins; 

- (id)installedVPNPlugins; 

- (bool)invalidateIconCache:(id)arg1; 

- (bool)openApplicationWithBundleID:(id)arg1; 

- (bool)openSensitiveURL:(id)arg1 withOptions:(id)arg2; 

- (bool)openURL:(id)arg1 withOptions:(id)arg2; 

- (bool)openURL:(id)arg1; 

- (id)operationToOpenResource:(id)arg1 

            usingApplication:(id)arg2 

    uniqueDocumentIdentifier:(id)arg3 

              sourceIsManaged:(bool)arg4 

                    userInfo:(id)arg5 

                    delegate:(id)arg6; 

- (id)operationToOpenResource:(id)arg1 

            usingApplication:(id)arg2 

    uniqueDocumentIdentifier:(id)arg3 

                    userInfo:(id)arg4 

                    delegate:(id)arg5; 

- (id)operationToOpenResource:(id)arg1 

            usingApplication:(id)arg2 

    uniqueDocumentIdentifier:(id)arg3 

                    userInfo:(id)arg4; 

- (id)operationToOpenResource:(id)arg1 usingApplication:(id)arg2 userInfo:(id)arg3; 

- (id)placeholderApplications; 

- (id)pluginsWithIdentifiers:(id)arg1 protocols:(id)arg2 version:(id)arg3; 

- (id)privateURLSchemes; 

- (id)publicURLSchemes; 

- (bool)registerApplication:(id)arg1; 

- (bool)registerApplicationDictionary:(id)arg1 

            withObserverNotification:(unsigned long long)arg2; 

- (bool)registerApplicationDictionary:(id)arg1; 

- (bool)registerPlugin:(id)arg1; 

- (id)remoteObserver; 

- (void)removeInstallProgressForBundleID:(id)arg1; 

- (void)removeObserver:(id)arg1; 

- (bool)uninstallApplication:(id)arg1 withOptions:(id)arg2 usingBlock:(id)arg3; 

- (bool)uninstallApplication:(id)arg1 withOptions:(id)arg2; 

- (bool)unregisterApplication:(id)arg1; 

- (bool)unregisterPlugin:(id)arg1; 

- (id)unrestrictedApplications; 

- (bool)updateSINFWithData:(id)arg1 

            forApplication:(id)arg2 

                  options:(id)arg3 

                    error:(id*)arg4; 

@end

私有库上appstore会被拒,可以用反射机制混淆加密字符串避免,在代表中不要出现私有函数字样。

如何获取app安装进度?
目前查找到或许可以从服务器入手: 参考链接

Preventing the web server (Apache in this case) from gzip-ing the file (which is useless anyway) enabled the progress indicator for me:

# Don't compress images and compressed files SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|mbtiles|zip|ipa|tgz|gz|bz2)$ no-gzip dont-vary  

具体可实现性,欢迎有经验的大神留言~

安装失败的原因分析(提示无法下载应用程序)
安装失败主要还得从ipa文件上分析,大概就是签名不匹配、硬件版本和软件版本不匹配等,可以从以下几个方面排查一下:

  1. 导出的ipa选择了Appstore方式
    这种方式的ipa文件,只适合上传到苹果App Store,并不能通过企业签名来安装。

  2. Ad-hoc方式导出的包,但是没有添加苹果UDID。

  3. 企业in-house方式的ipa包,但是证书已过期或者被撤销。

  4. 生成ipa时,没有设置正确的Architecture。(如果需要在某个设备上安装,ipa必须支持那个设备的Architecture。)

  5. App支持的iOS系统版本,高于当前的设备系统版本。

  6. 设备上已经安装了这个App,且已经安装的App和要安装的App是用不同证书打包的。

  7. info.plist文件中的LSRequiresIPhoneOS没有设置,或者设置了NO。

这种情况下,导出的ipa包没有包含Payload文件夹,而是被一个叫做Applications的文件夹代替,这样的ipa包会被iOS判定为无效的安装包。

  1. ipa包是破解过的,或者是一个使用破解Xcode方式打包生成的ipa安装包,或者是通过iTunes生成的ipa安装包。

  2. 网络出现中断或异常

相关文章

网友评论

    本文标题:iOS无线安装

    本文链接:https://www.haomeiwen.com/subject/knuydktx.html