C#读写iOS应用共享目录下的文件

之前写了一个Windows版的fmm2018球探工具,但每次都需要用助手把手机里的存档复制出来,感觉太胃疼,于是想加入一个功能:球探工具自动检测iPhone连接并自动读取共享目录下的存档,然后复制到Windows下加载。
开始做的时候发现这方面的资料太少了,在百度和Google上面翻了半天才找到些许教程,都不太完整,就知道了是要读取iTunesMobileDevice.dll,然后用它提供的接口来获取文件。
然后就在GitHub上找到了:https://github.com/nivalxer/MobileDevice。作者封装好了iTunesMobileDevice.dll的大部分接口,并提供了一个简单的iOS助手例子,感谢作者!
利用上面的接口和例子,我大概搞懂了监听iOS设备连接和断开的逻辑,但并没提供文件操作的代码和例子。于是,还是得我自己去研究。
关于文件操作,百度和Google上找到的信息几乎无一例外地都说是调AMDeviceCreateHouseArrestService或者AMDeviceStartHouseArrestService这两个接口之一。于是乎我也尝试调用了这两个接口,结果每次都是要么抛出内存损坏的异常,要么读出来的数据都是空的。好不容易找到个iFunBox开发组的Facebook,上面把代码贴了出来教大家怎么用AMDeviceCreateHouseArrestService,我照写了也还是不行。因为找到的资料大部分是C++,会不会C++可以而C#不行不得而知。在这一步上我卡了两三天,最后结合各方资料,终于另辟途径成功读取到了文件。在这里分享一下代码,也算是记录自己辛苦研究了这么多天的工作成果。

一、获取iOS中的fmm游戏:

/// <summary>
    /// 获取某个应用
    /// </summary>
    /// <param name="keyword">应用关键字</param>
    /// <returns></returns>
    public Dictionary<object, object> GetApp(string keyword)
    {
        var result = new Dictionary<object, object>();
        try
        {
            var dictAll = new Dictionary<object, object>(); // 第一层参数字典
            var dicSecond = new Dictionary<object, object>(); // 第二层参数字典
            dicSecond.Add("ApplicationType", "User");
            dictAll.Add("Command", "Browse");
            dictAll.Add("ClientOptions", dicSecond);
            var resultDics = GetServiceValue("com.apple.mobile.installation_proxy", dictAll); // 调用com.apple.mobile.installation_proxy来获取所有应用
            foreach (var dicFirst in resultDics)
            {
                var resultDic = (Dictionary<object, object>)dicFirst;
                var apps = (object[])resultDic["CurrentList"];
                foreach (var item in apps)
                {
                    var dic = (Dictionary<object, object>)item;
                    if (dic["CFBundleIdentifier"].ToString().ToUpper().Contains(keyword.ToUpper())) // CFBundleIdentifier相当于应用的唯一ID
                        return dic;
                }
            }
        }
        catch
        {
            // ignore
        }
        return result;
    }

其中GetServiceValue方法如下:

/// <summary>
    /// 获取服务值
    /// </summary>
    /// <param name="serviceName">服务名</param>
    /// <param name="dict">参数字典</param>
    /// <returns></returns>
    public List<object> GetServiceValue(string serviceName, Dictionary<object, object> dict)
    {
        List<object> result = new List<object>();
        try
        {
            var socket = 0;
            var startSocketResult = StartSocketService(serviceName, ref socket);
            if (!startSocketResult)
            {
                StopSocketService(ref socket);
                return result;
            }

            while (SendMessageToSocket(socket, dict))
            {
                var obj = (Dictionary<object, object>)ReceiveMessageFromSocket(socket);
                if (obj["Status"].ToString().Equals("Complete")) break;
                result.Add(obj);
            }
        }
        catch
        {
            throw new Exception();
        }
        return result;
    }

里面用到的所有方法,上面提供的地址里面都有,在这里不一一贴代码了。
从上面的方法里可以获取到fmm的各种信息,下一步要用它的CFBundleIdentifier来获得它共享目录下的所有文件。

二、获取应用共享目录的文件

/// <summary>
    /// 获取应用共享目录下的所有文件和信息
    /// </summary>
    /// <param name="boundId">应用ID</param>
    /// <param name="files">文件集合</param>
    public bool GetDocumentsFiles(object boundId, out Dictionary<string, Dictionary<object, object>> files)
    {
        var result = false;
        files = new Dictionary<string, Dictionary<object, object>>();
        int socket = 0;
        var connPtr = IntPtr.Zero;
        var dirPtr = IntPtr.Zero;
        var dict = new Dictionary<object, object>();
        dict.Add("Command", "VendDocuments");
        dict.Add("Identifier", boundId);
        try
        {
            // 启动com.apple.mobile.house_arrest服务,获得socket句柄
            socket = 0;
            var startSocketResult = StartSocketService("com.apple.mobile.house_arrest", ref socket);
            if (!startSocketResult) return result;

            // 循环发送字典到socket,直到Status = Complete
            while (SendMessageToSocket(socket, dict))
            {
                // 接收返回的信息
                var obj = (Dictionary<object, object>)ReceiveMessageFromSocket(socket);
                if (obj["Status"].ToString().Equals("Complete"))
                {
                    // 当状态为完成时,启动读文件服务,获得连接句柄
                    var sdf = (kAMDError)MobileDevice.AFCConnectionOpen(socket, 0, ref connPtr);
                    if (sdf == kAMDError.kAMDSuccess)
                    {
                        // 用连接句柄和根目录开启读目录服务,获得目录句柄
                        var openDirResult = (kAMDError)MobileDevice.AFCDirectoryOpen(connPtr, PATHDOCUMENTS, ref dirPtr);
                        if (openDirResult == kAMDError.kAMDSuccess)
                        {
                            var fileName = string.Empty;

                            // 用连接句柄和目录句柄读取目录,获得目录字符串
                            var readDirResult = (kAMDError)MobileDevice.AFCDirectoryRead(connPtr, dirPtr, ref fileName);
                            if (readDirResult == kAMDError.kAMDSuccess)
                            {
                                while (!string.IsNullOrEmpty(fileName))
                                {
                                    if (!fileName.Equals(".") && !fileName.Equals(".."))
                                    {
                                        var pathStr = string.Format("{0}/{1}", PATHDOCUMENTS, fileName);
                                        var pathBuff = Encoding.UTF8.GetBytes(pathStr);
                                        var dictPtr = IntPtr.Zero;

                                        // 用连接句柄和文件路径buff获得字典句柄
                                        var infoOpenResult = (kAMDError)MobileDevice.AFCFileInfoOpen(connPtr, pathBuff, ref dictPtr);
                                        var infos = new Dictionary<object, object>();
                                        if (infoOpenResult == kAMDError.kAMDSuccess)
                                        {
                                            var keyPtr = IntPtr.Zero;
                                            var valuePtr = IntPtr.Zero;

                                            // 用字典句柄获得信息字典
                                            while ((kAMDError)MobileDevice.AFCKeyValueRead(dictPtr, ref keyPtr, ref valuePtr) == kAMDError.kAMDSuccess)
                                            {
                                                var keyStr = Marshal.PtrToStringAnsi(keyPtr);
                                                if (string.IsNullOrWhiteSpace(keyStr)) break;
                                                var valueStr = Marshal.PtrToStringAnsi(valuePtr);
                                                infos.Add(keyStr, valueStr);
                                            }
                                            files.Add(fileName, infos);
                                        }
                                    }
                                    MobileDevice.AFCDirectoryRead(connPtr, dirPtr, ref fileName);
                                }
                                result = true;
                            }
                        }
                        MobileDevice.AFCDirectoryClose(connPtr, dirPtr);
                    }
                    MobileDevice.AFCConnectionClose(connPtr);
                }
            }
        }
        catch
        {
            // ignored
        }
        return result;
    }

其中PATHDOCUMENTS = “/Documents”;

现在我们已经拿到共享目录的所有文件了,接下来就是重头戏:将文件复制到Windows下面。

/// <summary>
    /// 复制应用共享目录下某个文件到指定路径
    /// </summary>
    /// <param name="bundleId">应用的CFBundleIdentifier,相当于应用ID</param>
    /// <param name="path">Windows下的目标路径,如: G:\My documents</param>
    /// <param name="fileName">文件全名,如: test.dat</param>
    /// <returns></returns>
    public bool CreateTempFile(object bundleId, string path, object fileName)
    {
        // 此方法严格意义上并不是直接复制粘贴文件,而是读出文件的数据再保存到另一个文件里

        var result = false; // 返回结果

        var connPtr = IntPtr.Zero; // 开启AFCConnectionOpen文件连接服务后获得的句柄

        int socket = 0; // 开启com.apple.mobile.house_arrest服务获得的socket
        var dict = new Dictionary<object, object>(); // com.apple.mobile.house_arrest服务入参字典
        dict.Add("Command", "VendDocuments"); // VendDocuments为遍历共享目录的命令
        dict.Add("Identifier", bundleId); // 应用CFBundleIdentifier
        try
        {
            // 启动com.apple.mobile.house_arrest服务,获得socket
            var startSocketResult = StartSocketService("com.apple.mobile.house_arrest", ref socket);
            if (!startSocketResult) return false;

            // 循环发送字典到socket,直到Status = Complete
            while (SendMessageToSocket(socket, dict))
            {
                // 接收返回的信息
                var obj = (Dictionary<object, object>)ReceiveMessageFromSocket(socket);
                if (obj["Status"].ToString().Equals("Complete"))
                {
                    // 当Status = Complete时,启动连接文件服务AFCConnectionOpen,获得连接句柄connPtr
                    if ((kAMDError)MobileDevice.AFCConnectionOpen(socket, 0, ref connPtr) == kAMDError.kAMDSuccess)
                    {
                        var pathStr = string.Format("{0}/{1}", PATHDOCUMENTS, fileName); // 要读取的文件在应用共享目录中的路径
                        var pathBuff = Encoding.UTF8.GetBytes(pathStr); // 转成byte[]
                        long fileHandle = 0; // 启动读写文件服务AFCFileRefOpen后获得的handle
                        path = string.Format(@"{0}\{1}", path, fileName); // 要复制到Windows下的目标路径

                        // 启动AFCFileRefOpen服务,获得fileHandle
                        if ((kAMDError)MobileDevice.AFCFileRefOpen(connPtr, pathBuff, (int)FileOpenMode.Read, ref fileHandle) == kAMDError.kAMDSuccess)
                        {
                            uint len = 1024*512; // 一次读取的长度
                            var fileStream = new byte[len]; // 文件流

                            // 开始用AFCFileRefRead读文件,读取到的数据储存在fileStream文件流里
                            // fileStream的长度必须与len一样
                            // 先读一次,如果成功,则在Windows下创建文件
                            if ((kAMDError)MobileDevice.AFCFileRefRead(connPtr, fileHandle, fileStream, ref len) == kAMDError.kAMDSuccess)
                            {
                                if (len > 0)
                                {
                                    using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write)) // 在Windows下创建文件
                                    {
                                        fs.Write(fileStream, 0, fileStream.Length);
                                    }

                                    // 开始循环读文件,当len返回0时则表示读取完毕
                                    while ((kAMDError)MobileDevice.AFCFileRefRead(connPtr, fileHandle, fileStream, ref len) == kAMDError.kAMDSuccess)
                                    {
                                        // 将获取的数据追加到Windows的文件里
                                        using (FileStream fs = new FileStream(path, FileMode.Append, FileAccess.Write))
                                        {
                                            fs.Write(fileStream, 0, fileStream.Length);
                                        }
                                        if (len == 0) break;
                                    }
                                    result = true; // 成功读取完文件,结果为true
                                }
                            }

                            MobileDevice.AFCFileRefClose(connPtr, fileHandle); // 关闭文件读写服务
                        }
                    }
                    MobileDevice.AFCConnectionClose(connPtr); // 关闭文件连接服务
                }
            }
        }
        catch (Exception ex)
        {
            // ignored
        }
        return result;
    }

其中:

public enum FileOpenMode
{
    None = 0,
    Read = 2,
    Write = 3
}

至此,文件已经成功复制到Windows下面了。

至于写文件,基本和上面差不多,因为我曾经不小心把FileOpenMode改成了Write,导致一个存档被覆盖了,好在有备份。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值