精华内容
下载资源
问答
  • atxserver2接入iOS设备

    2021-07-23 05:37:42
    atxserver2是一款可以远程控制Android和iOS设备的设备管理平台。该平台使用的技术栈为:Python3+NodeJS+RethinkDB 1.前期准备工作 Python >= 3.6(如果想使用tidevice,python>=3.7) WebDriverAgent(用 appium fork ...

    atxserver2是一款可以远程控制AndroidiOS设备的设备管理平台。该平台使用的技术栈为:Python3+NodeJS+RethinkDB

    1.前期准备工作

    • Python >= 3.6(如果想使用tidevice,python>=3.7)
    • WebDriverAgent(用 appium fork 的wda,因为appium 1.9.0在WDA中新增了一个 mjpegServer,这个是用来做屏幕同步)(下载链接:https://github.com/appium/WebDriverAgent)
    • NodeJS 8

    1)安装python(Welcome to Python.org 

     在console中输入:python3 

    查看是否安装成功

    2)安装WebDriverAgent(简称wda)(可以参考ATX 文档 - iOS 真机如何安装 WebDriverAgent · TesterHome

    a.下载wda

    git clone https://github.com/appium/WebDriverAgent
    
    a.也可以直接下载稳定release版本
    b.cd直接进入该wda文件目录(下面的b,c操作都是基于这个目录下进行安装)

    b.安装Carthage

    ​1.执行brew install carthage 或者直接下载Carthage.pkg(https://github.com/Carthage/Carthage/releases),此时在WebDriverAgent文件下会创建一个Cartfile文件夹 
    
    2.执行carthage update --use-xcframeworks,在wda文件下会创建一个Carthage文件夹和Cartfile.resolved文件  (这个步骤按需完成) 

    c.执行./Scripts/bootstrap.sh

    d.打开wda文件目录下的WebDriverAgent.xcodeproj

    1.安装到真机上都是需要证书签名的,这里xcode也需要一个注册过的Apple ID即Team即3(如何导入证书以及添加Apple ID可以百度)
    2.bundle identifier即4具有唯一性,不可以重复,但是前面的io.*.webdriver像证书一样是一定的,这个是不可以随便写的,可以在后面添加如io.*.webdriver-test1或者-test2任意的字符,把下面这几个文件的bundle identifier和team都统一为一个(图1,2,3)

    e.运行与测试

    1.菜单栏选择目标设备Destination选择连接的真机:Product->Destination->连接机型
    2.Scheme 选择 WebDriverAgentRunner: Product->Scheme->WebDriverAgentRunner
    3.最后运行 Product -> Test
    4.查看控制台View->Debug Area->Activate Console 如果出现下图证明成功啦

     图4

    手机上也会出现一下的图标

         5.端口转发

            有些国产的 iPhone 机器通过手机的 IP 和端口还不能访问,此时需要将手机的端口转发到 Mac 上。

    1.brew install libimobiledevice --HEAD
    2.iproxy 8100 8100
    3.http://localhost:8100/status 出现下图可以确认 WDA 运行成功

      图1

    图2

    图3

    3)安装nodeJS 8(版本最好不要高于8)

    a.安装brew,直接安装国内镜像源(Homebrew是一款Mac OS平台下的软件包管理工具,拥有安装、卸载、更新、查看、搜索等很多实用的功能。可以实现包管理,而不用关心各种依赖和文件路径) 

    /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
    

    b.安装nvm(一个node的版本管理工具)

    cd ~
    vim .bash_profile
    
    在文件里添加如下配置:
    export NVM_DIR=~/.nvm
    source $(brew --prefix nvm)/nvm.sh
    
    重新source使配置文件生效
    source .bash_profile 
    
    vim ~/.zshrc
    在文件里最后一行添加如下配置:
    source ~/.bash_profile

    c.使用nvm安装node

    nvm ls-remote 查看 所有的node可用版本
    
    nvm install xxx 下载你想要的版本
    
    nvm use xxx 使用指定版本的node
    
    nvm alias default xxx 每次启动终端都使用该版本的node

    2.ATXServer2接入iOS移动设备

    目前本机部署环境:

            Mac:10.15

            xcode:12.4

            node:8.9.1

    1)安装数据库rethinkdb

    a.部署方式1:docker(参考https://github.com/openatx/atxserver2

       1.Mac安装docker

    brew cask install docker

       2.切换到代码目录

    docker-compose up

    b.部署方式2: 通过源码方式部署(我使用的是这种)

       1.先准备好一个rethinkdb服务器

    brew update && brew install rethinkdb

       2.启动rethinkdb服务器

    rethinkdb
    
    或rethinkdb --port-offset 1 --directory rethinkdb_data2 --join localhost:29015

          访问http://localhost:8080/出现下图证明rethinkdb服务器启动好了

    2)部署atxserver2平台(https://github.com/openatx/atxserver2

    a.将代码clone到本地

    git clone https://github.com/Jodie/atxserver2.git

    b.打开命令终端进入该项目

    1).pip3 install -r requirements.txt
    2).python3 main.py --port 4000

    c.访问http://localhost:4000,若出现下图证明成功

    3)atxserver2接入iOS设备(https://github.com/openatx/atxserver2-ios-provider

    前期准备:

    1.连接iOS设备

    a.确保设备已经解锁
    b.使用数据线将苹果手机连接到电脑上(Mac)
    c.当出现是否信任该设备时选择是

    2.设备开启自动化

    1.按下HOME -> 设置(Settings) -> 开发者(Developer) -> Enable UI Automation
    2.回到 设置(Settings) -> Safari浏览器 -> 翻到最后 高级(Advanced) -> 打开 Web检查器(Web inspector)
    3.设置(Settings) -> 通用 -> 设备管理 -> 点击开发者应用中的栏目

    3.持续运行的设备设置

    默认情况下设备会锁屏的,而当设备锁屏的时候,就自动化不了了。最简单的一个办法就是保持设备常亮

    1.Home -> 设置(Settings) -> 显示与亮度(Settings & Brightness)
    2.亮度调到低(可以是最低)
    3.自动锁定(Auto-Lock) 设置为 永不(Never)

    a.安装libimobiledevice工具包

    brew uninstall --ignore-dependencies libimobiledevice
    brew uninstall --ignore-dependencies usbmuxd
    
    brew install --HEAD usbmuxd
    brew unlink usbmuxd
    brew link usbmuxd
    
    brew install --HEAD libimobiledevice
    brew install ideviceinstaller
    brew link --overwrite ideviceinstaller

    b. 将代码clone到本地

    git clone https://github.com/jodie/atxserver2-ios-provider

    c. 打开命令终端进入该项目

    pip3 install -r requirements.txt

    d. 避免命令行运行出错,运行一次即可

    sudo xcode-select -s /Applications/Xcode.app/Contents/Developer

    e. 解锁keychain,防止签名权限不足问题

    security unlock-keychain ~/Library/Keychains/login.keychain

    f. 启动

    1.SERVER_URL="http://localhost:4000"       # 这里修改成atxserver2的地址
    2.WDA_DIRECTORY="/Users/sjh/Documents/cxm_ios/WebDriverAgent-3.12.0"    # WDA项目地址
    3.python3 main.py -s $SERVER_URL -W $WDA_DIRECTORY

    如果出现下图证明接入成功

    3.过程中常见问题以及解决方案

    1.wda阶段问题

    问题 1:

    解决方法:
    遇到这个问题,说明手机上已经有一个 WebDriverAgent 的应用了,只是 BundleID 不一致,需要先将手机上的卸载掉,重新运行 Product -> Test

     问题 2:

    解决方法:
    提示其实已经说了,进入 设置 通用 设备管理 开发者应用 然后点击信任,之后再重新运行一遍 Product -> Test

    问题3: 原因是在使用Xcode高版本时,步骤七会编译失败(git地址https://github.com/appium/WebDriverAgent/pull/286/files

     解决方案:

    • WebDriverAgent/PrivateHeaders/XCTest/XCTestCase.h,加入以下代码到下图中红框位置

    @property(nonatomic) BOOL shouldSetShouldHaltWhenReceivesControl; // @synthesize shouldSetShouldHaltWhenReceivesControl=_shouldSetShouldHaltWhenReceivesControl;

    • WebDriverAgent/WebDriverAgentLib/Utilities/FBFailureProofTestCase.m,修改红框中代码26~36行为下方内容,替换后的代码如下图
    //  self.internalImplementation = (_XCTestCaseImplementation *)[FBXCTestCaseImplementationFailureHoldingProxy proxyWithXCTestCaseImplementation:self.internalImplementation];
      if ([self respondsToSelector:@selector(internalImplementation)]) {
        // The `internalImplementation` API has been removed since Xcode 11.4
          self.internalImplementation =
            (_XCTestCaseImplementation *)[FBXCTestCaseImplementationFailureHoldingProxy
                                          proxyWithXCTestCaseImplementation:self.internalImplementation];
        } else {
          // https://github.com/appium/appium/issues/13949
          self.shouldSetShouldHaltWhenReceivesControl = NO;
          self.shouldHaltWhenReceivesControl = NO;
        }
    

    问题4:执行./Scripts/bootstrap.sh遇到这个问题

    解决方案:

    执行touch /tmp/helper.xcconfig

    执行vim /tmp/helper.xcconfig

    中输入export XCODE_XCCONFIG_FILE=/tmp/helper.xcconfig

    执行carthage build --platform iOS --no-use-binaries --cache-builds

    问题5:xcframework和framework的切换问题

    解决方案:

     将Carthage/bulid文件下的RoutingHTTPServer.xcframework导入到xcode,将1设置为not embed,导入的设置为embed&sign,并且选择"工程配置-> Build Settings -> Build Options -> Validate Workspace",将该配置设置为Yes。如图1,不然会出现图2类似的错误

    图1

    图2

    问题6:进行pip3 install -r requirements.txt报错

     解决方案:

    pip install six -i http://pypi.douban.com/simple --trusted-host pypi.douban.com 

    问题7:

    解决方案:

    node升级到8,并且执行python3 main.py -s $SERVER_URL -W $WDA_DIRECTORY时console会出现没有导入httpx模块需要手动导入

    pip3 install httpx

    展开全文
  • 两种方式获取IOS设备型号,一种可以根据机型进行处理,另外一种则是按照屏幕比例进行分类,一起来了解一下吧~

    一,通过机器型号校验

    1.1 逻辑解释

    https://www.theiphonewiki.com/wiki/Models上有所有IOS设备型号的代码(最新设备会同步更新),下拉可以看到各种iPhone手机的代码型号,如下图红框:
    在这里插入图片描述

    找到设备型号后,即可在代码中进行如下逻辑处理

    if(SystemInfo.deviceModel.Equals("iPhone13,4"))
    {
    	 todo... 是iPhone 13 Pro Max 型号设备
    }
    

    1.2 实现代码

     #region 校验IPhone机型
    
        /// <summary>
        /// 当前运行设备型号
        /// -1:默认, 0:正常iph, 1:X系列iph, 2:Ipad
        /// </summary>
        private static int _iphoneDevice = -1;
        
        /// <summary>
        /// Apple苹果设备型号代码deviceModel
        /// 校验当前运行IPhone机型
        /// 0:正常iph, 1:X系列iph, 2:Ipad
        /// </summary>
        /// <returns></returns>
        public static int GetIphoneDevice()
        {
            if (_iphoneDevice != -1) return _iphoneDevice;
     
    #if UNITY_EDITOR
            _iphoneDevice = 0;
    #elif UNITY_IOS
            string modelStr = SystemInfo.deviceModel;
            string modelType = modelStr.ToLower().Trim().Substring(0, 3);
            if (modelType == "iph")
            {
                // iPhoneX:"iPhone10,3","iPhone10,6"  iPhoneXR:"iPhone11,8"  iPhoneXS:"iPhone11,2"  iPhoneXS Max:"iPhone11,6"
                // iPhone 12Pro "iPhone13,3" iPhone 12Pro "iPhone13,4"
                bool IsIphoneXDevice = modelStr.Equals("iPhone10,3") || modelStr.Equals("iPhone10,6") ||
                                       modelStr.Equals("iPhone11,8") || modelStr.Equals("iPhone11,2") ||
                                       modelStr.Equals("iPhone11,6") || 
                                       modelStr.Equals("iPhone13,3") || modelStr.Equals("iPhone13,4");
                _iphoneDevice = IsIphoneXDevice ? 1 : 0;
            }
            else if (modelType == "ipa")
            {
                //iPad机型
                _iphoneDevice = 2;
            }
            else
            {
                _iphoneDevice = 0;
            }
    #else
            _iphoneDevice = 0;
    #endif
            //Debug.Log("当前机型ID:" + _iphoneDevice);
            return _iphoneDevice;
        }
    

    缺点:每当有新的型号的手机发布时,都需要手动添加更新一下,否则会出现新机型是x系列的情况,按照正常机型处理的情况。


    二,通过长宽比校验

    通过设备的长宽比来校验设备型号,长宽比是1.53是Pad,长宽比在1.53f1.9之间,则是正常机型,长宽比大于1.9的都是长屏手机,则都可安装x系列做适配处理

    直接上代码:

    /// <summary>
    /// 当前运行设备型号
    /// 0:正常iph, 1:X系列iph, 2:Ipad
    /// </summary>
    private int platformType = 0;
    
    private void SetScreenType()
    {
        float width = Screen.width;
        float height = Screen.height;
        float value = width / height;
        if (value < 1.53f)
        {
            platformType = 2;
        }
        else if (value >= 1.53f && value < 1.9f)
        {
            platformType = 0;
        }
        else
        {
            platformType = 1;
        }
        // todo...各种屏幕设置
        //Screen.autorotateToLandscapeLeft = true;
        //Screen.autorotateToLandscapeRight = true;
        //Screen.autorotateToPortrait = false;
        //Screen.autorotateToPortraitUpsideDown = false;
        //Screen.orientation = ScreenOrientation.AutoRotation;
    }
    
    展开全文
  • 获取iOS设备唯一标示UUID

    千次阅读 2021-01-11 22:21:08
    在开发过程中,我们经常会被要求获取每个设备的唯一标示,以便后台做相应的处理。我们来看看有哪些方法来获取设备的唯一标示,然后再分析下这些方法的利弊。具体可以分为如下几种:UDIDIDFAIDFVMACkeychain下面我们...

    在开发过程中,我们经常会被要求获取每个设备的唯一标示,以便后台做相应的处理。我们来看看有哪些方法来获取设备的唯一标示,然后再分析下这些方法的利弊。

    具体可以分为如下几种:

    UDID

    IDFA

    IDFV

    MAC

    keychain

    下面我们来具体分析下每种获取方法的利弊

    1、UDID

    什么是UDID

    UDID 「Unique Device Identifier Description」是由子母和数字组成的40个字符串的序号,用来区别每一个唯一的iOS设备,包括 iPhones, iPads, 以及 iPod touches,这些编码看起来是随机的,实际上是跟硬件设备特点相联系的,另外你可以到iTunes,pp助手或itools等软件查看你的udid(设备标识)

    如下图所示:

    image

    UDID是用来干什么的?

    UDID可以关联其它各种数据到相关设备上。例如,连接到开发者账号,可以允许在发布前让设备安装或测试应用;也可以让开发者获得iOS测试版进行体验。苹果用UDID连接到苹果的ID,这些设备可以自动下载和安装从App Store购买的应用、保存从iTunes购买的音乐、帮助苹果发送推送通知、即时消息。 在iOS 应用早期,UDID被第三方应用开发者和网络广告商用来收集用户数据,可以用来关联地址、记录应用使用习惯……以便推送精准广告。

    为什么苹果反对开发人员使用UDID?

    iOS 2.0版本以后UIDevice提供一个获取设备唯一标识符的方法uniqueIdentifier,通过该方法我们可以获取设备的序列号,这个也是目前为止唯一可以确认唯一的标示符。 许多开发者把UDID跟用户的真实姓名、密码、住址、其它数据关联起来;网络窥探者会从多个应用收集这些数据,然后顺藤摸瓜得到这个人的许多隐私数据。同时大部分应用确实在频繁传输UDID和私人信息。 为了避免集体诉讼,苹果最终决定在iOS 5 的时候,将这一惯例废除,开发者被引导生成一个唯一的标识符,只能检测应用程序,其他的信息不提供。现在应用试图获取UDID已被禁止且不允许上架。

    所以这个方法作废

    2、IDFA

    全名:advertisingIdentifier

    获取代码:

    #import

    NSString *adId = [[[ASIdentifierManager sharedManager] advertisingIdentifier] UUIDString];

    来源:iOS6.0及以后

    说明:直译就是广告id, 在同一个设备上的所有App都会取到相同的值,是苹果专门给各广告提供商用来追踪用户而设的,用户可以在 设置|隐私|广告追踪 里重置此id的值,或限制此id的使用,故此id有可能会取不到值,但好在Apple默认是允许追踪的,而且一般用户都不知道有这么个设置,所以基本上用来监测推广效果,是戳戳有余了。

    注意:由于idfa会出现取不到的情况,故绝不可以作为业务分析的主id,来识别用户。

    3、IDFV

    全名:identifierForVendor

    获取代码:

    NSString *idfv = [[[UIDevice currentDevice] identifierForVendor] UUIDString];

    来源:iOS6.0及以后

    说明:顾名思义,是给Vendor标识用户用的,每个设备在所属同一个Vender的应用里,都有相同的值。其中的Vender是指应用提供商,但准确点说,是通过BundleID的反转的前两部分进行匹配,如果相同就是同一个Vender,例如对于com.taobao.app1, com.taobao.app2 这两个BundleID来说,就属于同一个Vender,共享同一个idfv的值。和idfa不同的是,idfv的值是一定能取到的,所以非常适合于作为内部用户行为分析的主id,来标识用户,替代OpenUDID。

    注意:如果用户将属于此Vender的所有App卸载,则idfv的值会被重置,即再重装此Vender的App,idfv的值和之前不同。

    4、MAC地址

    然而在iOS 7中苹果再一次无情的封杀mac地址,使用之前的方法获取到的mac地址全部都变成了02:00:00:00:00:00。

    5、Keychain

    Paste_Image.png

    我们可以获取到UUID,然后把UUID保存到KeyChain里面。

    这样以后即使APP删了再装回来,也可以从KeyChain中读取回来。使用group还可以可以保证同一个开发商的所有程序针对同一台设备能够获取到相同的不变的UDID。

    但是刷机或重装系统后uuid还是会改变。

    把下面两个类文件放到你的项目中

    KeychainItemWrapper.h文件

    ********************************

    #import

    @interface KeychainItemWrapper : NSObject

    {

    NSMutableDictionary *keychainItemData; // The actual keychain item data backing store.

    NSMutableDictionary *genericPasswordQuery; // A placeholder for the generic keychain item query used to locate the item.

    }

    @property (nonatomic, retain) NSMutableDictionary *keychainItemData;

    @property (nonatomic, retain) NSMutableDictionary *genericPasswordQuery;

    // Designated initializer.

    - (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;

    - (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;

    - (void)setObject:(id)inObject forKey:(id)key;

    - (id)objectForKey:(id)key;

    // Initializes and resets the default generic keychain item data.

    - (void)resetKeychainItem;

    @end

    KeychainItemWrapper.h文件

    ********************************

    #import "KeychainItemWrapper.h"

    #import

    /*

    These are the default constants and their respective types,

    available for the kSecClassGenericPassword Keychain Item class:

    kSecAttrAccessGroup - CFStringRef

    kSecAttrCreationDate - CFDateRef

    kSecAttrModificationDate - CFDateRef

    kSecAttrDescription - CFStringRef

    kSecAttrComment - CFStringRef

    kSecAttrCreator - CFNumberRef

    kSecAttrType - CFNumberRef

    kSecAttrLabel - CFStringRef

    kSecAttrIsInvisible - CFBooleanRef

    kSecAttrIsNegative - CFBooleanRef

    kSecAttrAccount - CFStringRef

    kSecAttrService - CFStringRef

    kSecAttrGeneric - CFDataRef

    See the header file Security/SecItem.h for more details.

    */

    @interface KeychainItemWrapper (PrivateMethods)

    /*

    The decision behind the following two methods (secItemFormatToDictionary and dictionaryToSecItemFormat) was

    to encapsulate the transition between what the detail view controller was expecting (NSString *) and what the

    Keychain API expects as a validly constructed container class.

    */

    - (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert;

    - (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert;

    // Updates the item in the keychain, or adds it if it doesn't exist.

    - (void)writeToKeychain;

    @end

    @implementation KeychainItemWrapper

    @synthesize keychainItemData, genericPasswordQuery;

    - (id)initWithAccount:(NSString *)account service:(NSString *)service accessGroup:(NSString *) accessGroup;

    {

    if (self = [super init])

    {

    NSAssert(account != nil || service != nil, @"Both account and service are nil. Must specifiy at least one.");

    // Begin Keychain search setup. The genericPasswordQuery the attributes kSecAttrAccount and

    // kSecAttrService are used as unique identifiers differentiating keychain items from one another

    genericPasswordQuery = [[NSMutableDictionary alloc] init];

    [genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

    [genericPasswordQuery setObject:account forKey:(id)kSecAttrAccount];

    [genericPasswordQuery setObject:service forKey:(id)kSecAttrService];

    // The keychain access group attribute determines if this item can be shared

    // amongst multiple apps whose code signing entitlements contain the same keychain access group.

    if (accessGroup != nil)

    {

    #if TARGET_IPHONE_SIMULATOR

    // Ignore the access group if running on the iPhone simulator.

    //

    // Apps that are built for the simulator aren't signed, so there's no keychain access group

    // for the simulator to check. This means that all apps can see all keychain items when run

    // on the simulator.

    //

    // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the

    // simulator will return -25243 (errSecNoAccessForItem).

    #else

    [genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];

    #endif

    }

    // Use the proper search constants, return only the attributes of the first match.

    [genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];

    [genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];

    NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];

    NSMutableDictionary *outDictionary = nil;

    if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)

    {

    // Stick these default values into keychain item if nothing found.

    [self resetKeychainItem];

    //Adding the account and service identifiers to the keychain

    [keychainItemData setObject:account forKey:(id)kSecAttrAccount];

    [keychainItemData setObject:service forKey:(id)kSecAttrService];

    if (accessGroup != nil)

    {

    #if TARGET_IPHONE_SIMULATOR

    // Ignore the access group if running on the iPhone simulator.

    //

    // Apps that are built for the simulator aren't signed, so there's no keychain access group

    // for the simulator to check. This means that all apps can see all keychain items when run

    // on the simulator.

    //

    // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the

    // simulator will return -25243 (errSecNoAccessForItem).

    #else

    [keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];

    #endif

    }

    }

    else

    {

    // load the saved data from Keychain.

    self.keychainItemData = [self secItemFormatToDictionary:outDictionary];

    }

    [outDictionary release];

    }

    return self;

    }

    - (id)initWithIdentifier: (NSString *)identifier accessGroup:(NSString *) accessGroup;

    {

    if (self = [super init])

    {

    // Begin Keychain search setup. The genericPasswordQuery leverages the special user

    // defined attribute kSecAttrGeneric to distinguish itself between other generic Keychain

    // items which may be included by the same application.

    genericPasswordQuery = [[NSMutableDictionary alloc] init];

    [genericPasswordQuery setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

    [genericPasswordQuery setObject:identifier forKey:(id)kSecAttrGeneric];

    // The keychain access group attribute determines if this item can be shared

    // amongst multiple apps whose code signing entitlements contain the same keychain access group.

    if (accessGroup != nil)

    {

    #if TARGET_IPHONE_SIMULATOR

    // Ignore the access group if running on the iPhone simulator.

    //

    // Apps that are built for the simulator aren't signed, so there's no keychain access group

    // for the simulator to check. This means that all apps can see all keychain items when run

    // on the simulator.

    //

    // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the

    // simulator will return -25243 (errSecNoAccessForItem).

    #else

    [genericPasswordQuery setObject:accessGroup forKey:(id)kSecAttrAccessGroup];

    #endif

    }

    // Use the proper search constants, return only the attributes of the first match.

    [genericPasswordQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];

    [genericPasswordQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnAttributes];

    NSDictionary *tempQuery = [NSDictionary dictionaryWithDictionary:genericPasswordQuery];

    NSMutableDictionary *outDictionary = nil;

    if (! SecItemCopyMatching((CFDictionaryRef)tempQuery, (CFTypeRef *)&outDictionary) == noErr)

    {

    // Stick these default values into keychain item if nothing found.

    [self resetKeychainItem];

    // Add the generic attribute and the keychain access group.

    [keychainItemData setObject:identifier forKey:(id)kSecAttrGeneric];

    if (accessGroup != nil)

    {

    #if TARGET_IPHONE_SIMULATOR

    // Ignore the access group if running on the iPhone simulator.

    //

    // Apps that are built for the simulator aren't signed, so there's no keychain access group

    // for the simulator to check. This means that all apps can see all keychain items when run

    // on the simulator.

    //

    // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the

    // simulator will return -25243 (errSecNoAccessForItem).

    #else

    [keychainItemData setObject:accessGroup forKey:(id)kSecAttrAccessGroup];

    #endif

    }

    }

    else

    {

    // load the saved data from Keychain.

    self.keychainItemData = [self secItemFormatToDictionary:outDictionary];

    }

    [outDictionary release];

    }

    return self;

    }

    - (void)dealloc

    {

    [keychainItemData release];

    [genericPasswordQuery release];

    [super dealloc];

    }

    - (void)setObject:(id)inObject forKey:(id)key

    {

    if (inObject == nil) return;

    id currentObject = [keychainItemData objectForKey:key];

    if (![currentObject isEqual:inObject])

    {

    [keychainItemData setObject:inObject forKey:key];

    [self writeToKeychain];

    }

    }

    - (id)objectForKey:(id)key

    {

    return [keychainItemData objectForKey:key];

    }

    - (void)resetKeychainItem

    {

    OSStatus junk = noErr;

    if (!keychainItemData)

    {

    self.keychainItemData = [[NSMutableDictionary alloc] init];

    }

    else if (keychainItemData)

    {

    NSMutableDictionary *tempDictionary = [self dictionaryToSecItemFormat:keychainItemData];

    junk = SecItemDelete((CFDictionaryRef)tempDictionary);

    NSAssert( junk == noErr || junk == errSecItemNotFound, @"Problem deleting current dictionary." );

    }

    // Default attributes for keychain item.

    [keychainItemData setObject:@"" forKey:(id)kSecAttrAccount];

    [keychainItemData setObject:@"" forKey:(id)kSecAttrLabel];

    [keychainItemData setObject:@"" forKey:(id)kSecAttrDescription];

    // Default data for keychain item.

    [keychainItemData setObject:@"" forKey:(id)kSecValueData];

    }

    - (NSMutableDictionary *)dictionaryToSecItemFormat:(NSDictionary *)dictionaryToConvert

    {

    // The assumption is that this method will be called with a properly populated dictionary

    // containing all the right key/value pairs for a SecItem.

    // Create a dictionary to return populated with the attributes and data.

    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];

    // Add the Generic Password keychain item class attribute.

    [returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

    // Convert the NSString to NSData to meet the requirements for the value type kSecValueData.

    // This is where to store sensitive data that should be encrypted.

    NSString *passwordString = [dictionaryToConvert objectForKey:(id)kSecValueData];

    [returnDictionary setObject:[passwordString dataUsingEncoding:NSUTF8StringEncoding] forKey:(id)kSecValueData];

    return returnDictionary;

    }

    - (NSMutableDictionary *)secItemFormatToDictionary:(NSDictionary *)dictionaryToConvert

    {

    // The assumption is that this method will be called with a properly populated dictionary

    // containing all the right key/value pairs for the UI element.

    // Create a dictionary to return populated with the attributes and data.

    NSMutableDictionary *returnDictionary = [NSMutableDictionary dictionaryWithDictionary:dictionaryToConvert];

    // Add the proper search key and class attribute.

    [returnDictionary setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];

    [returnDictionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];

    // Acquire the password data from the attributes.

    NSData *passwordData = NULL;

    if (SecItemCopyMatching((CFDictionaryRef)returnDictionary, (CFTypeRef *)&passwordData) == noErr)

    {

    // Remove the search, class, and identifier key/value, we don't need them anymore.

    [returnDictionary removeObjectForKey:(id)kSecReturnData];

    // Add the password to the dictionary, converting from NSData to NSString.

    NSString *password = [[[NSString alloc] initWithBytes:[passwordData bytes] length:[passwordData length]

    encoding:NSUTF8StringEncoding] autorelease];

    [returnDictionary setObject:password forKey:(id)kSecValueData];

    }

    else

    {

    // Don't do anything if nothing is found.

    NSAssert(NO, @"Serious error, no matching item found in the keychain.\n");

    }

    [passwordData release];

    return returnDictionary;

    }

    - (void)writeToKeychain

    {

    NSDictionary *attributes = NULL;

    NSMutableDictionary *updateItem = NULL;

    OSStatus result;

    if (SecItemCopyMatching((CFDictionaryRef)genericPasswordQuery, (CFTypeRef *)&attributes) == noErr)

    {

    // First we need the attributes from the Keychain.

    updateItem = [NSMutableDictionary dictionaryWithDictionary:attributes];

    // Second we need to add the appropriate search key/values.

    [updateItem setObject:[genericPasswordQuery objectForKey:(id)kSecClass] forKey:(id)kSecClass];

    // Lastly, we need to set up the updated attribute list being careful to remove the class.

    NSMutableDictionary *tempCheck = [self dictionaryToSecItemFormat:keychainItemData];

    [tempCheck removeObjectForKey:(id)kSecClass];

    #if TARGET_IPHONE_SIMULATOR

    // Remove the access group if running on the iPhone simulator.

    //

    // Apps that are built for the simulator aren't signed, so there's no keychain access group

    // for the simulator to check. This means that all apps can see all keychain items when run

    // on the simulator.

    //

    // If a SecItem contains an access group attribute, SecItemAdd and SecItemUpdate on the

    // simulator will return -25243 (errSecNoAccessForItem).

    //

    // The access group attribute will be included in items returned by SecItemCopyMatching,

    // which is why we need to remove it before updating the item.

    [tempCheck removeObjectForKey:(id)kSecAttrAccessGroup];

    #endif

    // An implicit assumption is that you can only update a single item at a time.

    result = SecItemUpdate((CFDictionaryRef)updateItem, (CFDictionaryRef)tempCheck);

    NSAssert( result == noErr, @"Couldn't update the Keychain Item." );

    }

    else

    {

    // No previous item found; add the new one.

    result = SecItemAdd((CFDictionaryRef)[self dictionaryToSecItemFormat:keychainItemData], NULL);

    NSAssert( result == noErr, @"Couldn't add the Keychain Item." );

    }

    }

    @end

    我们在写一个工具类用来保存UUID到keychain和从keychain中读取UUID.

    实现代码

    AppUntils.m文件

    *********************

    #import

    #import "KeychainItemWrapper.h"

    #pragma mark - 保存和读取UUID

    +(void)saveUUIDToKeyChain{

    KeychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];

    NSString *string = [keychainItem objectForKey: (__bridge id)kSecAttrGeneric];

    if([string isEqualToString:@""] || !string){

    [keychainItem setObject:[self getUUIDString] forKey:(__bridge id)kSecAttrGeneric];

    }

    }

    +(NSString *)readUUIDFromKeyChain{

    KeychainItemWrapper *keychainItemm = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:nil];

    NSString *UUID = [keychainItemm objectForKey: (__bridge id)kSecAttrGeneric];

    return UUID;

    }

    + (NSString *)getUUIDString

    {

    CFUUIDRef uuidRef = CFUUIDCreate(kCFAllocatorDefault);

    CFStringRef strRef = CFUUIDCreateString(kCFAllocatorDefault , uuidRef);

    NSString *uuidString = [(__bridge NSString*)strRef stringByReplacingOccurrencesOfString:@"-" withString:@""];

    CFRelease(strRef);

    CFRelease(uuidRef);

    return uuidString;

    }

    写入UUID到keychain

    我们最好在程序启动之后把UUID写入到keychain,代码如下:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

    {

    [AppUtils saveUUIDToKeyChain];

    }

    读取UUID

    在需要读取的地方直接调用AppUtils的类方法readUUIDFromKeyChain即可。

    注意

    1.设置非ARC编译环境

    因为KeychainItemWrapper.m文件是在非ARC环境下运行的,所以需要设置非arc编译环境,

    如图所示:

    image

    2. 让同一开发商的所有APP在同一台设备上获取到UUID相同

    在每个APP的项目里面做如下设置

    2.1、设置accessgroup

    keychainItemWrapper *keychainItem = [[KeychainItemWrapper alloc] initWithAccount:@"Identfier" service:@"AppName" accessGroup:@"YOUR_BUNDLE_SEED.com.yourcompany.userinfo"];

    此处设置accessGroup为YOUR_BUNDLE_SEED.com.yourcompany.userinfo

    2.2、创建plist文件

    然后在项目相同的目录下创建KeychainAccessGroups.plist文件。

    该文件的结构是一个字典,其中中最顶层的节点必须是一个键为“keychain-access-groups”的Array,并且该Array中每一项都是一个描述分组的NSString。YOUR_BUNDLE_SEED.com.yourcompany.userinfo就是要设置的组名。

    如图:

    image

    2.3、 设置code signing

    接着在Target--->Build Settings---->Code Signing栏下的Code Signing Entitlements右侧添加KeychainAccessGroups.plist

    如图:

    image

    这样就可以保证每个app都是从keychain中读取出来同一个UUID

    参考文章:

    iOS10注意:

    iOS10系统导入上面的两个类运行会崩溃,需要做如下处理:

    Paste_Image.png

    展开全文
  • 先看下效果,真机打开科普UDIDU D I D (Unique Device Identifier),唯一标示符,是iOS设备的一个唯一识别码,每台iOS设备都有一个独一无二的编码,UDID其实也是在设备量产的时候,生成随机的UUID写入到iOS设备硬件或者...

    通过Safari与mobileconfig获取iOS设备UDID(设备唯一标识符)

    本文基于在线安装Profile来实现获取UDID。

    先看下效果,真机打开

    科普

    UDID

    U D I D (Unique Device Identifier),唯一标示符,是iOS设备的一个唯一识别码,每台iOS设备都有一个独一无二的编码,UDID其实也是在设备量产的时候,生成随机的UUID写入到iOS设备硬件或者某一块存储器中,所以变成了固定的完全不会改变的一个标识,用来区别每一个唯一的iOS设备,包括 iPhones, iPads, 以及 iPod touches

    随着苹果对程序内获取UDID封杀的越来越严格,私有api已经获取不到UDID,Mac地址等信息,继而出现了使用钥匙串配合uuid等等方法变相实现

    由于近期项目需求是设备授权的形式使用软件,使用钥匙串等方法不完全能解决问题,因为重置或重做系统都会清除uuid然后重新存入,所以想到了用safari的方式获取设备真实的UDID

    MDM

    iOS支持企业级的MDM(Mobile Device Managment),也就是所谓的移动设备管理,目的就是让企业能够方便的管理 iPhone、Pad等移动设备。具体的做法是通过在系统中安装配置文件(Profiles)的方式实现各种功能,设备管理,设备安全,获取设备信息,设备配置,备份和恢复等几类功能,可以根据不同应用场景实现很多具体小功能。

    Over-the-Air Profile Delivery and Configuration

    一个配置的profile描述文件允许你基于iOS设备发布配置信息,如果你需要配置大量设备的邮件设置,网络设置,或者设备的证书,配置文件可以轻松完成。

    iOS配置描述文件包含选多可以指定的设置,包括:

    Passcode policies 密码策略

    Restrictions on device features (disabling the camera, for example) 设备特性限制(例如摄像头)

    Wi-Fi settings WIFI设置

    VPN settings VPN设置

    Email server settings 邮件服务器设置

    Exchange settings Exchange设置

    LDAP directory service settings LDAP目录服务设置

    CalDAV calendar service settings CalDAV日历服务设置

    Web clips 桌面快捷方式

    Credentials and keys 凭证和密钥

    Advanced cellular network settings 高级蜂窝网络设置

    一、通过苹果Safari浏览器获取iOS设备UDID步骤

    苹果公司允许开发者通过iOS设备和Web服务器之间的某个操作(其实就是MDM的获取设备信息功能),来获得IOS设备的UDID(包括其他的一些参数)。以下为简要概述:

    1、在你的Web服务器上创建一个.mobileconfig的XML格式的描述文件;

    2、用户在所有操作之前必须通过某个点击操作完成.mobileconfig描述文件的安装;

    3、服务器需要的数据,比如:UDID,需要在.mobileconfig描述文件中配置好,以及服务器接收数据的URL地址;

    4、当用户设备完成数据的手机后,返回提示给客户端用户;

    ota开发流程图

    二、.mobileconifg

    在这篇文章中,主要讲如何获得标识符。其实还可以获取更多信息,以下是一个获得UDID示例.mobileconfig配置

    Objective-C

    PayloadContent

    URL

    https://dev.skyfox.org/udid/receive.php

    DeviceAttributes

    UDID

    IMEI

    ICCID

    VERSION

    PRODUCT

    PayloadOrganization

    dev.skyfox.org

    PayloadDisplayName

    查询设备UDID

    PayloadVersion

    1

    PayloadUUID

    3C4DC7D2-E475-3375-489C-0BB8D737A653

    PayloadIdentifier

    dev.skyfox.profile-service

    PayloadDescription

    本文件仅用来获取设备ID

    PayloadType

    Profile Service

    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

    -//Apple//DTD PLIST 1.0//EN""http://www.apple.com/DTDs/PropertyList-1.0.dtd">

    PayloadContent

    URL

    https://dev.skyfox.org/udid/receive.php

    DeviceAttributes

    UDID

    IMEI

    ICCID

    VERSION

    PRODUCT

    PayloadOrganization

    dev.skyfox.org

    PayloadDisplayName

    查询设备UDID

    PayloadVersion

    1

    PayloadUUID

    3C4DC7D2-E475-3375-489C-0BB8D737A653

    PayloadIdentifier

    dev.skyfox.profile-service

    PayloadDescription

    本文件仅用来获取设备ID

    PayloadType

    ProfileService

    你需要填写回调数据的URL和PayloadUUID。该PayloadUUID仅仅是随机生成的唯一字符串,类似bundleid,一般是域名倒置,用来标识唯一。

    iOS12 mobileconfig中的URL要用https地址(例如 https://dev.skyfox.org/udid/receive.php)。否者会报ATS错误。

    注意:mobileconfig下载时设置文件内容类型Content Type为:application/x-apple-aspen-config

    服务器上的文件

    当访问mobileconfig文件不能直接下载时,可能就需要设置mime content type了,application/x-apple-aspen-config,

    设置content type大体上两种方法

    1.服务器容器设置

    .htaccess增加如下配置

    Shell

    AddType application/x-apple-aspen-config .mobileconfig

    1

    2

    3

    AddTypeapplication/x-apple-aspen-config.mobileconfig

    2.php等动态语言直接设置

    PHP

    header('Content-type: application/x-apple-aspen-config; chatset=utf-8');

    header('Content-Disposition: attachment; filename="company.mobileconfig"');

    echo $mobileconfig;

    1

    2

    3

    header('Content-type: application/x-apple-aspen-config; chatset=utf-8');

    header('Content-Disposition: attachment; filename="company.mobileconfig"');

    echo$mobileconfig;

    三、iOS设备安装.mobileconfig描述文件

    新建一个用于下载mobileconfig的网页,这里我命名为udid.php

    Objective-C

    获取您的UDID

    1

    2

    3

    4

    5

    6

    7

    8

    9

    10

    11

    12

    13

    14

    15

    16

    17

    18

    获取您的UDID

    UUDI:

    1.点击获取您的UDID

    2.验证ipa

    展开全文
  • ios设备操作命令

    2021-05-10 16:30:26
    在mac电脑上操作ios设备 文档 libimobiledevice https://github.com/libimobiledevice/ ideviceinstaller https://blog.csdn.net/mochacha_/article/details/105617328 xcrun ...utm_med
  • Qt Creator连接iOS设备

    2021-04-16 07:42:00
    Qt Creator连接iOS设备连接iOS设备配置设备查看设备连接状态指定支持的iOS版本在模拟器上测试管理模拟器检查当前的Xcode版本 连接iOS设备 您可以使用USB电缆将iOS设备连接到本地计算机,以运行从Qt Creator为它们...
  • 获取iOS设备的UDID

    2021-04-07 15:24:42
    UDID,是iOS设备的唯一识别码,每台iOS设备都有一个独一无二的编码,这个编码,就称为识别码,也叫做UDID(Unique Device Identifier) 2. 如果获取UDID 强烈推荐这个获取方法,点击此链接...
  • ADB相当于iOS设备

    2021-07-19 18:23:32
    I was looking some instrument like Android ADB in order to debug iOS devices. I've found iOS instrument, a tool of the XCode that is able to debug app on mobile iOS devices. This instrument is helpful...
  • @supports(-webkit-touch-callout:none) { /*针对IOS的css*/ }
  • 对于 iOS / Android 来说, 由于操作系统对于权限管理的不同策略,导致某些操作在 Android 上可以由程序自动执行, 而在 iOS 上需要用户介入操作. 比如:1. 打开蓝牙模块.2. 打开 Wifi 模块.3. 建立 Wifi 热点.4. 连接 ...
  • 我们在进行各个系统的原生开发时,都有对应的方法获取设备信息,那么在使用Flutter时如何获取设备相关的相关信息呢?我们本文就来介绍一个Flutter插件:Flutter Device Info下面我们来逐步介绍如何获取设备信息。...
  • web测试-ios设备模拟器(iOS Simulator)

    千次阅读 2021-01-11 22:15:11
    前言虽然 Chrome DevTools 可以模拟手机的环境,但与真实...安装ios simulator捆绑于xcode,直接上appStore 搜索Xcode进行安装如何使用1.打开xcode,需要新建一个项目,通过”Xcode-Open Developer Tool-Simulato...
  • iOS设备 历代 机型对照表

    千次阅读 2021-08-07 15:08:07
    -----------iPhone----------- iPhone 2,1(iPhone 3GS 产品型号:国行 - A1325;国际版 - A1303) iPhone 3,1(iPhone 4 GSM 产品型号:A1332) iPhone 3,2(iPhone 4 8G 新制程版,目前新出的国行 8G 版均为此型号,...
  • 如题,flutter开发出来的ios包,经常要经过内部测试,需要加入新iphone手机的udid才能参与包的下载和测试。 解决方法(这个是蒲公英平台的一个快速获取udid工具,借用下????): 完毕。 ...
  • window使用chrome调试ios设备的H5页面(Safari和APP) 1:用管理员身份打开PowerShell,不同操作系统打开方式不太一样,win10的话在搜索栏搜索就能找到( 2:查看PowerShell版本 命令:Get-Host | Select-Object ...
  • 如何获取iOS 设备的 UDID UDID,是 iOS 设备的一个唯一识别码,每台 iOS 设备都有一个独一无二的编码,这个编码,我们称之为识别码,也叫做UDID( Unique Device Identifier)。 在我们申请IOS开发描述文件的时候,...
  • 下面是一个可以作为脚本基础的示例(由culebra自动生成):#! /usr/bin/env python# -*- coding: utf-8 -*-"""Copyright (C) 2013-2018 Diego Torres MilanoCreated on 2018-08-09 by Culebra v15.4.0__ __ __ __/ \ /...
  • ios设备mdm的实现过程

    2020-12-31 12:14:46
    一)配置IIS加密连接,ios系统升级7.1后已经无法使用http进行企业内部署,为了满足mdm的加密需求以及大厅的初始化安装需要进行生成自签名证书1)配置MIMEcer application/x-x509-ca-cert.mobileconfig application/x-...
  • 嗯,在这篇文章中我们将告诉你如何正确使用这一新功能你自己的Android / iOS手机连接到Windows 10 如何Android / iOS手机连接到Windows 10 1.在你的电脑上按下赢了我热键 2.从系统设置点击手机 3.下一步,点击添加...
  • 下面就请随小编一起,看看国外科技网站148apps撰稿人卡特·多森(Carter Dotson)为我们带来的图文指南:当一台新的AirPort无线基站首次开启,用户可在iOS设备的“设置”→“无线局域网”中选择“设置AirPort基站”,...
  • windows端 ios设备安装ipa软件 首先要保证 有 iTunes 安装完成以后 浏览文件夹里面的内容会找到一个叫sideloadly_warp.dll的文件 把这个叫sideloadly_warp.dll的文件重命名成sideloadly.dll,然后再打开sideloadly就...
  • 获取 iOS 设备 UDID

    2020-12-30 16:18:56
    iOS 设备上打开下面的地址,即可方便的获取到当前设备的 UDID。 https://www.pgyer.com/tools/udid 如需批量获取UDID请点击批量获取UDID 注意:请根据网页的提示,安装蒲公英提供的描述文件。如果手机设置了锁屏...
  • 如上图下载这个两个软件并打开,用数据线将你的手机连接到电脑,到时会有一个弹框询问你是否信任,点击信任,在打开HBuilderX点击运行即可! 就写这么多了。day day up!
  • 管理员刚刚更改了IOS设备上一个接口的IP地址。为了将这些更改应用到设备上,我们还必须做什么?()更多相关问题请从脑内四条多巴胺能神经通路分析氯丙嗪的药理作用和不良反应。This dining room is _____ only by ...
  • Airplay是苹果的投屏协议,由于该协议并未公开,目前IOS设备投屏到Windows端的软件效果各不相同。本文通过对比各个软件的使用效果,希望能够找到令人满意的投屏软件。本人使用的设备为Ipad pro,分辨率为2388*1688,...
  • uni app 自定义基座 真机调试 ios设备

    千次阅读 2021-10-19 11:24:59
    需要注意的是,我们在选择制作ios自定义基座的时候,填入的 '证书profile文件' 和 '私钥证书' 都应该是应用开发版的证书,而不是应用打包正式版的证书. 如果使用应用打包正式版的证书,制作自定义基座,在app运行到设备...
  • iOS 设备震动

    2021-03-26 12:15:11
    2 iOS10 后震动方法 UIImpactFeedbackGenerator UIImpactFeedbackGenerator *generator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy]; [generator impactOccurred]; 枚举定义...
  • 怎么解锁iPhone / iPad呢?...能够删除iCloud激活锁,从...iToolab UnlockGo for mac(iOS设备解锁软件)v4.6.1免激活版 iToolab UnlockGo for mac软件功能 立即解锁iPhone / iPad的密码 iToolab UnlockGo可让您轻.
  • 偶然发现苹果手机上这个picker弹出来的时候,自己写的组件竟然覆盖在了这个picker的上面,遮挡住了picker,不是全部遮挡,是遮挡一部分。以为是z-index层级的问题,找问题找bug找了一上午,最后发现: ...
  • 一种IOS设备的集中式Wifi室内定位方法【技术领域】[0001] 本发明属于无线通信室内定位技术领域,具体涉及一种IOS设备的集中式Wifi室 内定位方法。【背景技术】[0002] 近年来,随着无线通信技术的发展以及移动智能...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 235,422
精华内容 94,168
关键字:

ios设备

友情链接: topX.zip