C#讀寫iOS應用共享目錄下的文件

本文鏈接: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,導致一個存檔被覆蓋了,好在有備份。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章