最近项目上需要发布测试版iOS app,决定使用Ad Hoc方式小范围发布,这就需要事先获取目标设备的UDID 字符串才可以。本文记录一下采用OTA profile的方式来获取UDID信息的各个步骤。
先简单介绍一下iOS的测试版发布方法。由于Apple的严格限制,导致iOS app无法像安卓那样,直接将编译好的二进制文件提供给测试者,测试人员安装后执行就可以了。Apple规定,目前可以采用如下几种方法来发布测试版app:
- 企业版开发者用户可以直接把编译好的测试版ipa文件放在网页上,用户用手机访问该链接即可自动下载,安装并运行。这个企业版开发者账号年费399刀,但不能发布到App Store。此方法简单易行,测试者只要知道该链接即可进行下载测试,缺点是费用稍高,而且需要注册企业版用户,还用额外准备公司相关材料供Apple审核,手续繁琐。
- 普通开发者用户(99美金一年)可以发布到App Store,也可以通过Test flight或者Ad Hoc这两种方式发布测试版App。 Test flight要求你先上传你的app,然后经过苹果的人工审核通过后,就可以发布出去给目标测试用户。目标测试用户会收到邮件提示,按照邮件提示的链接在手机上安装Test Flight 后打开,就能点击下载测试版ipa文件运行。此法的好处是用户不需要太多操作就可以运行,缺点是需要等待Apple审批,无法迅速发布给测试用户。而Ad Hoc方式就可以快速发布编译好的ipa文件并挂在自己网页上,和企业版用户类似,测试用户也可以通过该链接直接下载并安装运行。但这里有个缺陷,Ad Hoc要求在编译该ipa时就指定哪些设别可以运行此测试版本,这是通过实现注册好相关设备的UDID来实现的,因此如果想在新的手机上也运行测试版,那么就需要先设法获取设别的UDID,然后更新profile和certificate,再用这更新后的信息编译并获取新的ipa,然后重新上传。好处是不需要Apple参与审核,一经发布即可测试。
因为Apple隐藏了直接获取UDID的API,我们需要通过OTA Profile的方法来实现。因为用户需要通过访问一个网页来下载此Profile文件,而这个网页需要通过Apache或者Nginx来提供访问,这里具体的Apache或者Nginx操作略。
第一步,准备一个HTML网页,它提供一个下载链接,该链接指向下一步要生成的OTA profile 文件(这里举例说明,profile文件名我们起名为为enroll.mobileconfig)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <! doctype html> < html lang = "en" > < head > < title >Get iOS Device UDID</ title > </ head > < body > < h2 >Find your iOS device's UDID</ h2 > < div > < a href = "enroll.mobileconfig" >Click to find device UDID</ a > </ div > </ body > </ html > |
第二步,手动创建上面这个enroll.mobileconfig文件,它的名字自定义,可以改。它是一个xml文件,具体可参照如下。注意DeviceAttributes这个key对应的值是一个数组,包含了所有我们想获取的值(近期Apple不断收紧隐私政策从而不再提供某些值)。有些属性名字关键字是固定的,不能动。而那个URL,则应该指向一个网页用来接受iOS提供的设备信息,这里URL我们设计了一个php文件,它将会被用来接受返回的xml信息并再次把XML信息转送到用户手机上显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | <? 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 >PayloadContent</ key > < dict > < key >URL</ key > < string >yourwebsite.com/GetUDID/done.php</ string > < key >DeviceAttributes</ key > < array > < string >UDID</ string > < string >IMEI</ string > < string >MEID</ string > < string >ICCID</ string > < string >VERSION</ string > < string >PRODUCT</ string > < string >MAC_ADDRESS_EN0</ string > < string >DEVICE_NAME</ string > < string >SERIAL</ string > < string >IMSI</ string > < string >ECID</ string > </ array > </ dict > < key >PayloadOrganization</ key > < string >Your company name(自定义,它会显示给用户)</ string > < key >PayloadDisplayName</ key > < string >iOS Profile Service(自定义,它会显示给用户)</ string > < key >PayloadVersion</ key > < integer >1</ integer > < key >PayloadUUID</ key > < string >D84C2DBF-2E70-4D91-A55A-4AB4D56C1234(自定义,global unique,全球唯一就可以)</ string > < key >PayloadIdentifier</ key > < string >com.youwebsite.profile.service(自定义,用于区分不同profile)</ string > < key >PayloadDescription</ key > < string >This profile is used to get device UDID info to test Blizzz's iOS app.(自定义,它会显示给用户)</ string > < key >PayloadType</ key > < string >Profile Service</ string > </ dict > </ plist > |
第3步,如果不sign上面这个证书文件,也可以使用,但是用户端会提示这个证书签名未确认,可能会增加用户疑虑,因此这一步可以考虑sign这个文件。和https认证类似,这个signature过程也需要使用到该web服务器上的几个SSL证书,具体是执行一个命令,使用这几个SSL证书对上面手动创作的那个enroll.mobileconfig进行处理,得到一个新的文件,然后这个新的文件用来替换掉上面那个原始的enroll.mobileconfig,那样用户端就会看到该证书已经被确认。这里我的机器使用了let‘s encrypt提供的SSL证书,因此命令具体如下,署名后的文件名是signed.enroll.mobileconfig,我们记得要把这个文件更新到上面html网页对象的下载链接。
1 | sudo openssl smime -sign -in enroll.mobileconfig -out signed.enroll.mobileconfig -signer /etc/letsencrypt/live/YOUWEBSITE.com/fullchain.pem -inkey /etc/letsencrypt/live/YOUWEBSITE.com/privkey.pem -certfile /etc/letsencrypt/live/YOUWEBSITE.com/fullchain.pem -outform der -nodetach |
第4步,准备一个php文件用来接受iOS返回的设备信息。用户点击按钮并下载该profile证书后,会提示它进入系统设置来找到这个刚下载完的证书并安装。按照提示安装和运行就行,这样iOS系统就会返回一个XML格式的数据流,里面包含了一个XML格式的信息,还有一些署名后生成的二进制数据。大致是这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | xyz..a&*&^**(( 前面是一些乱码字符,由署名产生) <? 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 >IMEI</ key > < string >35 00......</ string > < key >MEID</ key > < string >35056......</ string > < key >PRODUCT</ key > < string >iPhon......</ string > < key >SERIAL</ key > < string >HXY4......</ string > < key >UDID</ key > < string >000081......</ string > < key >VERSION</ key > < string >19......</ string > </ dict > </ plist > !@#¥@xyz..a&*&^**(( 后面是一些乱码字符,由署名产生) |
我们只关心中间的XML部分,因为它包含了我们所需的各种信息。这里我们要写一个PHP代码来实现接收该数据流并转动到另一个URL来实现在用户端显示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | <?php $data = file_get_contents ( 'php://input' ); //剔除前面和后面多余的乱码字符 $plistBegin = '<?xml version="1.0"' ; $plistEnd = '</plist>' ; $pos1 = strpos ( $data , $plistBegin ); $pos2 = strpos ( $data , $plistEnd ); $data2 = substr ( $data , $pos1 , $pos2 - $pos1 +8); echo ( $data2 ); //data2 contains all xml content //file_put_contents("result.txt", $data2);//保存成文件用于调试 //开始解析XML并拿到各值 $xml = xml_parser_create(); xml_parse_into_struct( $xml , $data2 , $vs ); xml_parser_free( $xml ); $UDID = "" ; $CHALLENGE = "" ; $DEVICE_NAME = "" ; $DEVICE_PRODUCT = "" ; $DEVICE_VERSION = "DEVICE_VERSION" ; $iterator = 0; $arrayCleaned = array (); foreach ( $vs as $v ){ if ( $v [ 'level' ] == 3 && $v [ 'type' ] == 'complete' ){ $arrayCleaned []= $v ; } $iterator ++; } $data = "" ; $iterator = 0; foreach ( $arrayCleaned as $elem ){ $data .= "\n==" . $elem [ 'tag' ]. " -> " . $elem [ 'value' ]. "<br/>" ; switch ( $elem [ 'value' ]) { case "PRODUCT" : $DEVICE_PRODUCT = $arrayCleaned [ $iterator +1][ 'value' ]; break ; case "UDID" : $UDID = $arrayCleaned [ $iterator +1][ 'value' ]; break ; case "VERSION" : $DEVICE_VERSION = $arrayCleaned [ $iterator +1][ 'value' ]; break ; } $iterator ++; } $params = "UDID=" . $UDID . "&DEVICE_PRODUCT=" . $DEVICE_PRODUCT . "&VERSION=" . $DEVICE_VERSION ; //关键,强制301跳转到另一个URL并把解析出的各属性值作为URL请求参数带入 ?> |
第5步,准备另一个php文件用来接受上面的跳转到命令传递过来的参数。这里上面例子里写了个complete/index.php,随便写了个如下:
1 2 3 4 5 6 7 8 9 10 | < html > < body > < h2 >Here is the device information.</ h3 > < form name = "myForm" style = "font-size: 1.5em;" > < label for = "POST-udid" >UDID:</ label >< br />< input readonly id = "POST-udid" type = "text" value="<?php echo $_GET['UDID'] ?>" size="36" >< br />< br /> < label for = "POST-version" >Version:</ label >< br />< input readonly id = "POST-version" type = "text" value="<?php echo $_GET['VERSION'] ?>" size="36">< br />< br /> < label for = "POST-product" >Product:</ label >< br />< input readonly id = "POST-product" type = "text" value="<?php echo $_GET['DEVICE_PRODUCT'] ?>" size="36">< br />< br /> </ form > </ body > </ html > |
至此,用户在手机上确认安装完该证书后,就可以直接跳转到浏览器并显示返回的各种数据了