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

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

    点击源码下载

  • 相关阅读:
    Java之事件处理
    Java之图形程序设计
    小议设置path环境变量
    关于JAVA中的编译和解释执行
    并发工具类 CountDownLatch
    线程池
    Properties的小问题
    转换流
    TCP中客户端和服务器的理解
    leetcode_160. 相交链表
  • 原文地址:https://www.cnblogs.com/dagehaoshuang/p/2665166.html
Copyright © 2020-2023  润新知