iOS 获取手机上已经安装的应用
前言
公司内部有一给自己的 App 发布后台,类似于 FIR 那样的存在, 有完整的 LDAP 账号登录。每天的 daily build 和 历史发布版都会放到那里去。然而每次都要登录后台扫描二维码下载实在是太麻烦了,我们就打算做一个客户端。
于是问题来了:如何知道我手机上安装的*物说版本号比我当前的要低呢?
方案们
方案一 剪贴板共享
我们知道同一个证书下的软件,是可以共享剪贴板的,所以我们可以把*物说的版本号丢到剪贴板里存下来。
但问题在于:
-
*物说要运行过
-
致命伤:必须同一个 账号的 证书,内部 APP 肯定会去用企业证书,而*物说可能是线上证书,可能是企业证书。
所以这个方案被否掉了。
方案二 URLScheme
把*物说的 URLScheme 写成 GiftTalk233 这样的方案,然后在项目中 CanOpenURL 确认安装。
当问题在于:
iOS 9 之后,CanOpenURL 的白名单有上限,而且要修改编译脚本过于麻烦,这个方案也被否掉了。
于是,常规方案都不可行,那么要祭出大杀器了,私有 API
私有 API
毕竟是内部使用的 APP,所以用用私有 API 也没有什么问题。经过查阅越狱开发的文档,我们发现了这么一个私有库:MobileInstallation.framework
使用起来还是蛮方便的,直接:
CFDictionaryRef dict = MobileInstallationLookup(NULL);
NSLog(@"%@", (NSDictionary*) dict);
然而,这个库正常情况下是不会被 Link 进来的,于是我们该怎么做呢?
链接私有库
经过前几篇文章,我们知道动态链接库都是写在 Mach-O 的头部。然而它*终是经过 unix 的一个系统函数 dlopen 来加载的,这货是可以突破沙盒环境的,不信你可以给 dlopen 下个断点看看。所以我们可以手动调用 dlopen 加载想要的私有库。
void* libHandle = dlopen("/System/Library/PrivateFrameworks/MobileInstallation.framework/MobileInstallation",RTLD_NOW);
if (libHandle) {
NSLog(@"加载成功!");
}
但是对于 C 语言函数来说,他在编译时就被换成了对应的函数指针,所以我们想调用上述方法并没有那么容易。而如果是 OC 的方法的话,我们直接通过 runtime 就可以拿到对应的结果了。
所以现在我们需要拿个函数指针做个映射:
这样就可以了!
然鹅,我拿这个这个东西去给同事得瑟的时候,被打脸了。
我拿了一台公司的 iPhone 4 iOS 7 做的开发,这个工作一切正常。但 iOS 8 以上,func 取不到。吓得我赶紧去 Github 上看头文件。
https://github.com/MP0w/iOS-Headers/blob/master/iOS8.1/PrivateFrameworks/MobileInstallation/MIInstallerClient.h
惊了个呆,iOS 8 居然重写了这个 framework,但仔细观察了一下,貌似有一个方法是我们想要的:
- (void)fetchInstalledAppsWithOptions:(id)arg1 completion:(CDUnknownBlockType)arg2
好吧,我之前树的 Flag 应验了,那就 iOS 8 以上换一个写法吧。
真机运行得到:
required to have an entitlement named “com.apple.private.mobileinstall.allowedSPI” with an array containing “CopyInstalledAppsForLaunchServices” to call the MobileInstallation SPI
Wut?
entitlement.plist
若你对 iOS 签名机制了机的话,entitlement *对不陌生。不过我相信大多数开发者都很少和他打交道,这里我来简单说一下。
我们都知道签名需要证书的 profile 文件,而 entitlement 是一个授权机制,他里面约定了很多 iOS 的更高级的权限。有点像 Android 写在 manifest 里面的 permission。
比如:
-
访问 HealthKit 需要添加
com.apple.developer.healthkit
-
使用 Network Extension 查找
WI-FI
需要添加com.apple.developer.networking.HotspotHelper
但是,这个是需要苹果授权的,且与 profile 对应的。于是我们想在 entitlement 里面添加 com.apple.private.mobileinstall.allowedSPI
是没问题,但是是无法通过签名,会提示找不到对应的 profile。
于是我们想调用私有的 entitlement 的话,越狱设备可以随便搞,但不越狱的话开发者层面是没有什么可以绕过去的方法 (但做安全的蒸米大大说其实是有解的,若有朋友知道欢迎留言)。
仿佛走入了死胡同
柳暗花明又一村
我们都知道同步推是有企业证书版本的,且能查看我们本地的全部 APP。于是,我们对着他们的代码找一下呗~
经过一番查找,我们定位到了一个叫做 ApplicationManager 的类,这个 loadInstalledApplications 的方法很抢眼
然后在逻辑结构中可以看到:
他很鸡贼的判断了一下系统版本是不是大于 8.0,如果不是,就用左边的 MobileInstallation.framework 来做,否则调用了一个方法,叫做 suoyouyianzhuangdeyingyong
。我能吐槽这名字么…
通过下面代码,我们可以发现一个类叫做 LSApplicationWorkspace
在 Github 的私有头文件里面搜了一下,惊了个呆:
https://github.com/nst/iOS-Runtime-Headers/blob/master/Frameworks/MobileCoreServices.framework/LSApplicationWorkspace.h
他居然是 MobileCoreServices 的私有函数!我们连 dlopen 都省了。
于是,照猫画虎,我们有:
任务完成!