Windows 8 Metro開發疑難雜症(六)——APP的掛起狀態


APP的掛起狀態我在前面兩篇關於導航的博客里面已經有提到,我這么說吧,目前版本(包括最新的RTM版)都是有一個bug的。下面我會給你演示這個bug。在這之前我先講下這個掛起問題的臨床表現吧。
不知道你們有沒有注意過,就是當你打開一個APP的時候瀏覽了一會然后切換到其他APP, 過一段時間以后再切換回原來的APP的時候你會發現原來的APP回到首頁了,並不是離開APP的時候那個頁面,這里有兩個原因會發生這種情況。這種情況在調試里面叫“掛起並關閉”,怎么查看APP是否處於這種狀態,很簡單,就是屏幕左邊彈出一列你所有打開的APP列表,如果有APP的縮略圖變成啟動頁圖標的時候,那么說明這個APP處於這種狀態,如果APP的縮略圖是你離開APP的時候的頁面的截圖那么APP處於正常運行狀態。下面我介紹下引起上面提到的問題的原因。

1.APP開發的時候根本就沒有處理掛起狀態

2.APP開發的時候處理了掛起狀態,但是由於系統的一個Bug導致APP在掛起的時候crash,所以當你從掛起狀態恢復的時候由於沒有數據恢復只能從首頁開始

這個導致Crash的API是Frame.GetNavigationState()方法(只有當你導航的時候傳遞的參數是復雜類型的時候才會引發這個bug,這個就是我在前面兩篇博客中提到的問題),如果你用了VS的項目模版,SuspensionManager這個類里面的SaveFrameNavigationState這個方法會調用Frame.GetNavigationState()方法,這個方法主要的作用就是保存Frame的導航狀態,這樣當你從掛起狀態恢復的時候APP才能正確的恢復狀態,也就是你離開APP的時候是哪個頁面回來的時候還會在那個頁面(這個是非常重要的,如果你沒有恢復導航狀態,那么可以說你的數據就算保存了也是沒用的,因為APP在恢復的時候根本就沒用到你保存的數據),恢復導航狀態是調用  Frame.SetNavigationState這個方法。

下面我演示這個bug。

首先使用VS創建一個GridAPP類型的項目。

因為項目模版的三個頁面的傳遞的參數的類型都是字符串,所以不會出現這種問題,這里我們需要做一些改動。先改下GroupedItemsPage里面的ItemView_ItemClick方法的代碼,原來的代碼是:

        void ItemView_ItemClick(object sender, ItemClickEventArgs e)
        {
            // 導航至相應的目標頁,並
            // 通過將所需信息作為導航參數傳入來配置新頁
            var itemId = ((SampleDataItem)e.ClickedItem).UniqueId;
            this.Frame.Navigate(typeof(ItemDetailPage), itemId);
        }

現在我們要改成

     void ItemView_ItemClick(object sender, ItemClickEventArgs e)
        {
            // 導航至相應的目標頁,並
            // 通過將所需信息作為導航參數傳入來配置新頁
            this.Frame.Navigate(typeof(ItemDetailPage), e.ClickedItem);
        }

就是把原來傳遞ID的現在直接把對象傳遞過去,下面我們還要改下ItemDetailPage里面LoadState方法的代碼,原來代碼如下:

   protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
        {
            // 允許已保存頁狀態重寫要顯示的初始項
            if (pageState != null && pageState.ContainsKey("SelectedItem"))
            {
                navigationParameter = pageState["SelectedItem"];
            }

            // TODO: 創建適用於問題域的合適數據模型以替換示例數據
            var item = SampleDataSource.GetItem((String)navigationParameter);
            this.DefaultViewModel["Group"] = item.Group;
            this.DefaultViewModel["Items"] = item.Group.Items;
            this.flipView.SelectedItem = item;
        }

現在代碼如下:

     protected override void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
        {
            // TODO: 創建適用於問題域的合適數據模型以替換示例數據
            var item = (SampleDataItem)navigationParameter;
            this.DefaultViewModel["Group"] = item.Group;
            this.DefaultViewModel["Items"] = item.Group.Items;
            this.flipView.SelectedItem = item;
        }

 

現在可以直接運行了,運行后我們點擊一個項進入詳情頁面。下面就開始調試掛起狀態。
在調試的時候在VS的工具欄點擊鼠標右鍵會出來一個toolbar列表,這里面把調試位置這個toolbar選上(默認是未選擇狀態),如圖

這時候來調試掛起狀態,點擊“掛起並關閉”,如圖: 

 這時候就出問題了,APP直接Crash

因為SaveAsync這個方法調用了前面我提到的Frame.GetNavigationState方法導致的Crash,各位可以自己斷點設置過去看看。由於Frame.GetNavigationState這個bug存在,可以這么說,你開發的APP幾乎是沒法正真的實現數據保存和恢復的。而事實上目前商店中的很多APP都有這樣的情況,國外的不說,我只說國內的,國內很多的APP基本上都有這樣的情況(包括我目前開發的一款APP),只要APP進入掛起狀態,那么你重新切換回來的時候就是從首頁開始的。這里要說下,APP何時會進入掛起狀態,這個是系統來決定的,如果內存不夠了那么除了當前運行的APP,其他的APP肯定會進入掛起狀態。

那么這個問題有沒有解決方法呢?答案是有的,但是不完美,如何不完美我后面會提到,我下面先說下如何解決這個問題。

既然我們的參數不能傳遞復雜類型,那么只能傳遞簡單類型或者沒有參數傳遞。而我目前提供的方法就是“不傳遞參數”,這里說的“不傳遞參數”並不是真的就不傳了,只是我們需要換一種傳遞參數的方法,也就是我們在使用Frame.Navigate方法的時候不會傳遞參數了,只能自己寫一個方法來完成傳遞參數的目的。

當我們使用VS自帶的模版創建項目的時候,都會有一個Common文件夾的,里面有一個LayoutAwarePage類,這個類也是我們創建頁面的基類,我們需要對這個類進行改動下以便達到我們的目的。首先我們需要在LayoutAwarePage這個類里面添加兩個方法,代碼如下:

   private static object nextPageParam;
        /// <summary>
        /// 如果傳遞的對象是復雜類型,那么使用本方法來導航頁面
        /// </summary>
        /// <param name="pagetype"></param>
        /// <param name="obj"></param>
        public void Navigate(Type pagetype, object obj)
        {
            nextPageParam = obj;
            this.Frame.Navigate(pagetype);
        }
        public void Navigate(Type pagetype)
        {
            this.Frame.Navigate(pagetype);
        }

下面還要對里面的OnNavigatedTo方法中的代碼進行改動,以便我們能正確的傳遞參數,並且能保存我們傳遞的參數,這樣頁面恢復的時候還能使用原來的參數。代碼如下:

 

       protected override void OnNavigatedTo(NavigationEventArgs e)
        {
            // 通過導航返回緩存頁不應觸發狀態加載
            if (this._pageKey != null) return;
            var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
            this._pageKey = "Page-" + this.Frame.BackStackDepth;

            if (e.NavigationMode == NavigationMode.New)
            {
                // 在向導航堆棧添加新頁時清除向前導航的
                // 現有狀態
                var nextPageKey = this._pageKey;
                int nextPageIndex = this.Frame.BackStackDepth;
                while (frameState.Remove(nextPageKey))
                {
                    nextPageIndex++;
                    nextPageKey = "Page-" + nextPageIndex;
                }
                //如果nextPageParam不為空,那么我們需要保存這個參數以便恢復的時候能正常恢復
                if (nextPageParam != null)
                {
                    string key = this._pageKey + "_NextPageParam";
                    frameState[key] = nextPageParam;
                    this.LoadState(nextPageParam, null);
                    nextPageParam = null;
                }
                else
                // 將導航參數傳遞給新頁
                this.LoadState(e.Parameter, null);
            }
            else
            {
                string key = this._pageKey + "_NextPageParam";
                if (frameState.ContainsKey(key))
                {
                    this.LoadState(frameState[key], (Dictionary<String, Object>)frameState[this._pageKey]);
                }
                else
                // 通過將相同策略用於加載掛起狀態並從緩存重新創建
                // 放棄的頁,將導航參數和保留頁狀態傳遞
                // 給頁
                this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]);
            }
        }

只要用上面這段代碼替換原來的代碼就可以了。下面我們得修改下調用的方法,還是修改GroupedItemsPage里面的ItemView_ItemClick方法,把原來的    this.Frame.Navigate(typeof(ItemDetailPage), e.ClickedItem);改成現在的      this.Navigate(typeof(ItemDetailPage), e.ClickedItem);因為我們在基類里面添加了Navigate方法,所以我們在使用的時候可以直接使用this.Navigate來導航,現在試着運行APP,你會發現還是Crash,但是Crash的原因不同了,這次的Crash報的錯誤信息是無法序列化對象SampleDataItem。為什么無法序列化SampleDataItem對象呢?因為SuspensionManager在保存數據的時候是使用DataContractSerializer來把一個字典集合序列化保存到文件中的,而這個字典的類型是Dictionary<string, object>,也就是說SuspensionManager在序列化字典的時候根本不知道這個字典保存的類型是什么類型,這時候就需要手動添加KnownTypes了,也就是我們要把所有保存到字典中的類型添加到KnownTypes集合中,這樣SuspensionManager在序列化的時候就能正確序列化集合了,這里我選擇在APP.cs中添加,在APP的OnLaunched方法里面添加,SuspensionManager.KnownTypes.Add(typeof(Data.SampleDataItem));把這段代碼加進去就行了。

       SuspensionManager.RegisterFrame(rootFrame, "AppFrame");
                SuspensionManager.KnownTypes.Add(typeof(Data.SampleDataItem));
                if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
                {
                    // 僅當合適時才還原保存的會話狀態
                    try
                    {
                        await SuspensionManager.RestoreAsync();
                    }
                    catch (SuspensionManagerException)
                    {
                        //還原狀態時出現問題。
                        //假定沒有狀態並繼續
                    }
                }

到這里還沒完,因為能被序列化的只有是被標記了[DataContract]的類才能被序列化(包括所有的父類),到這當然還沒完,既然標記了[DataContract]那么肯定是要對屬性做標記的,不然沒有被標記的屬性是不會被序列化的。對於做過WCF的肯定會很熟悉如何標記了。標記完了現在就可以直接運行,你會發現現在可以正常掛起了。並且離開的時候是哪個頁面,回來的時候還是在那個頁面。

其實這里面的標記有點復雜,因為SampleDataGroup和SampleDataItem涉及到循環引用,所以直接用[DataContract]標記是沒用的,必須使用 [DataContract(IsReference = true)]這個來標記。具體看我源碼

 

好了,到這里對於數據的保存方面的內容告一段落。

 

點擊源碼下載

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM