OTA
OTA:Over-the-Air,iOS4新加的一项技术,脱离Appstore,实现从自己的服务器下载并安装iOS应用。
实现原理
通过Safari解析链接中的"items-services://"来实现直接安装。(个人账号需要注册设备,企业账号无需注册设备)
Safari会去读取installIPA.plist中的信息,如:iOS应用的名称、版本、安装等。
准备工作
- 支持HTTPS的服务器
- ipa文件
- mainifest.plist文件
- icon文件:2张 尺寸:
512x512
57x57
实现步骤
- 用Xcode打包release版本
- 搭建本地Web服务器
- 开启HTTPS
- 编写好对应的.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 | 包名 | 应用名称 |
- 上传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来判断,但是这种方式有两个明显的问题:
- iOS9以上需要进行白名单设置,在info.plist里面添加
- 需要知道目标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文件上分析,大概就是签名不匹配、硬件版本和软件版本不匹配等,可以从以下几个方面排查一下:
-
导出的ipa选择了Appstore方式
这种方式的ipa文件,只适合上传到苹果App Store,并不能通过企业签名来安装。 -
Ad-hoc方式导出的包,但是没有添加苹果UDID。
-
企业in-house方式的ipa包,但是证书已过期或者被撤销。
-
生成ipa时,没有设置正确的Architecture。(如果需要在某个设备上安装,ipa必须支持那个设备的Architecture。)
-
App支持的iOS系统版本,高于当前的设备系统版本。
-
设备上已经安装了这个App,且已经安装的App和要安装的App是用不同证书打包的。
-
info.plist文件中的LSRequiresIPhoneOS没有设置,或者设置了NO。
这种情况下,导出的ipa包没有包含Payload文件夹,而是被一个叫做Applications的文件夹代替,这样的ipa包会被iOS判定为无效的安装包。
-
ipa包是破解过的,或者是一个使用破解Xcode方式打包生成的ipa安装包,或者是通过iTunes生成的ipa安装包。
-
网络出现中断或异常
网友评论