之前写了一个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,导致一个存档被覆盖了,好在有备份。