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)]这个来标记。具体看我源码

好了,到这里对于数据的保存方面的内容告一段落。

点击源码下载

原文地址:https://www.cnblogs.com/dagehaoshuang/p/2665166.html