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

來源:互聯網
上載者:User

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)]這個來標記。具體看我源碼

 

好了,到這裡對於資料的儲存方面的內容告一段落。

 

點擊源碼下載

 

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.