本文鏈接:https://blog.csdn.net/wangliverpool4/article/details/82115369
之前寫了一個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;
}
其中:
至此,文件已經成功複製到Windows下面了。
至於寫文件,基本和上面差不多,因爲我曾經不小心把FileOpenMode改成了Write,導致一個存檔被覆蓋了,好在有備份。