zoukankan      html  css  js  c++  java
  • 向256 MB内存的Windows Phone提供应用的最佳实践指导

    简介

    为了使得应用能在256 MB的Windows Phone设备上运行需要进行一些改动。

    首先,与512 MB的设备相比,256 MB手机上的内存使用/分配方式是不同的。运行在256 MB上的应用仍然有同等数量的内存(90 MB),但60 MB之后的“工作集”将会被分页。因此虽然它允许应用最多使用90 MB,应用使用少于60 MB可以运行得更好。第二,256 MB设备不支持潜在无界的内存消耗的计划任务。

    本文提供了最佳实践指导和技巧用来满足60 MB的目标,并且处理其他的小的平台变化。

    Tip #1——总是使用模拟器的256 MB选项测试程序

    Windows Phone SDK 7.1.1模拟器使得你可以在256 MB和512 MB内存之间进行选择。一旦你选择了其中一个模拟器将分配相应大小的内存(与实体机一样)。

    Windows Phone emulator memory options.png

    建议最好的做法是总是使用256 MB作为模拟器部署所有应用程序的默认选择。使用这种方法确保在256 MB的内存上应用程序的任何问题在部署之前就能解决。另外,虽然模拟器能够很好地模拟真实情况下的内存分配,但如果可能的话,我们建议你在256 MB的设备上测试一下。

    Tip #2——使用Windows Phone Memory profiler

    Windows Phone SDK 7.1包括Windows Phone Memory Profiler。该工具使得你能够看见当前内存的分配图,分析特定时间段内内存的使用情况,看到可行性的建议和一系列的托管堆。所有的Visual Studio版本上都有Memory Profiler。 阅读Techniques for memory analysis of Windows Phone apps一文来了解揭示应用内存使用情况的更多信息。

    Tip #3——创建一个helper类来检测应用程序是否安装在256 MB的手机上

    Windows Phone 7.5包括获取手机上运行的应用的最大可用工作集的属性。如果最大值小于90 MB(甚至是94371840字节)则该应用应当被当成256 MB的手机。我们建议你创建一个通用的属性帮助你为低内存手机写if-then-else 条件,如下所示:

    public static class LowMemoryHelper
    {
    public static bool IsLowMemDevice { get; set; }
     
    static LowMemoryHelper()
    {
    try
    {
    Int64 result = (Int64)DeviceExtendedProperties.GetValue("ApplicationWorkingSetLimit");
    if (result < 94371840L)
    IsLowMemDevice = true;
    else
    IsLowMemDevice = false;
    }
    catch (ArgumentOutOfRangeException)
    {
    // Windows Phone OS update not installed, which indicates a 512-MB device.
    IsLowMemDevice = false;
    }
    }
    }

    例如我们可以这么写:

    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
    if (!LowMemoryHelper.IsLowMemDevice)
    Allocate80MbOfMemory();
    else
    DontAllocate80MbOfMemory();
    }

    目的是为256 MB手机使用if-then-elses ,但有时是不可避免的。

    Tip #4——不支持PeriodicTask 和 ResourceIntensiveTasks

    256 MB 手机不支持PeriodicTask 和 ResourceIntensiveTask 类,如果试图使用将throw 一个异常。这两个类是开发者在特定的限制下将代码作为后台进程执行。很容易明白为什么不支持这些类。ResourceIntensiveTask 可以没有上限地运行任何代码。想象你有一个256 MB的设备,操作系统使用大概100 MB,一个应用使用60 MB的工作集另一个后台进程使用另外60 MB的工作集。这很有可能导致手机崩溃。激活另一个后台进程(例如,需要15 MB的背景音乐)将导致整个手机内存不足。

    100MB + 60MB + 60MB + 15MB ≈ 256MB

    类似的计算解释了为什么PeriodicTask不能被使用。使用10个后台代理,每个需要6 MB的内存,10 PeriodicTasks 就相当于另一个应用程序使用60 MB的内存。关于PeriodicTask 和 ResourceIntensiveTask 的更多信息请参考Background Agents Overview for Windows Phone。要特别注意后台声音的BackgroundTasks 和后台文件传输和Scheduled Alarms 及 Reminders将继续在256 MB的手机上运行。

    Tip: 应用程序仍然可以包含PeriodicTask 和 ResourceIntensiveTask的代码。当你试图运行它们时候将抛出异常。

    推荐的最佳做法是总是在代码里使用if-then-else (#3里所提到的)而不要在256 MB手机里对PeriodicTask 或 ResourceIntensiveTasks 进行初始化,如下所示:

    private void Application_Launching(object sender, LaunchingEventArgs e)
    {
    if (!LowMemoryHelper.IsLowMemDevice)
    InitializePeriodicTaskToUpdateLiveTiles();
    else
    InitializePushNotificationsToUpdateLiveTiles();
    }

    有一些使用情况可以使用其他手段来弥补失去的特性。例如PeriodicTask 支持的Live Tiles能被Push Notification支持的Live Tiles所代替。

    在Windows Phone 7.5 中后台代理只是想为程序提供额外的功能而不是核心功能,用户可以关闭这些功能。然而,如果你的应用围绕PeriodicTask 或 ResourceIntensiveTask 最好让它不支持256 MB的应用程序。

    Tip #5——使用WebBrowserTask代替<WebBrowser />控件来显示任意未测试的网页

    在Windows Phone 7里,可以使用Internet Explorer <WebBrowser /> 控件将任何URL的页面装载到应用程序上。然而,一些网页可能会导致过度的内存消耗。尤其是那些不是为手机网页浏览器量身定做的网站更可能引起手机上严重的内存消耗。

    例如, <WebBrowser /> 指向包括代码的不是为手机浏览器定义的网页,例如

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <phone:WebBrowser Source="http://www.yvettesbridalformal.com"
    VerticalAlignment="Stretch"
    HorizontalAlignment="Stretch" />
    </Grid>

    将引起大量的内存使用。

    注意不是所有的网站将在256 MB手机上引起问题。例如,对一个更加现代化的网站来说,内存消耗将在一个可接受的范围内,如下所示:

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <phone:WebBrowser Source="http://developer.nokia.com"
    VerticalAlignment="Stretch"
    HorizontalAlignment="Stretch" />
    </Grid>

    甚至当导航到测试过的网站,确保它们只导航到先前的测试过的页面这一点很重要。一个选择是阻止导航到已知的使用过多内存的网站。一般情况下最好限制导航到已经成功测试过的已知网站而不限制到特定的网站。

    <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
    <phone:WebBrowser Source="http://developer.nokia.com"
    Navigating="WebBrowser_Navigating"
    VerticalAlignment="Stretch"
    HorizontalAlignment="Stretch" />
    </Grid>
    private void WebBrowser_Navigating(object sender, NavigatingEventArgs e)
    {
    if (e.Uri.OriginalString == "http://www.yvettesbridalformal.com")
    {
    e.Cancel = true;
    }
    }

    为了保证你的应用程序在256 MB手机上<WebBrowser /> 控件不会带来内存问题,简单的方法是一直使用WebBrowserTask。WebBrowserTask将打开一个单独的应用程序并且当内存紧张的时候墓碑化应用程序。因此确保你的应用程序支持墓碑机制。

    private void Button_Click(object sender, RoutedEventArgs e)
    {
    new WebBrowserTask()
    {
    Uri = new Uri("http://developer.nokia.com", UriKind.Absolute)
    }.Show();
    }

    为<WebBrowser /> 控件推荐的最佳做法是将<WebBrowser />中的所有可能导致内存问题的所有页面在内存分析器下运行。请确保任何时刻的内存消耗不超过90 MB。如果你不能限制应用程序中的外部链接(Reddit类似的应用程序),不要使用<WebBrowser /> 控件,而是使用WebBrowserTask。另一个可能性是一种混合的方法监测<WebBrowser /> 的内存使用,如果它超过90 MB然后你将控件从visual tree中移除并启动WebBrowserTask。

    Tip #6——使用BingMapsTask代替Bing <Map />控件

    Bing <Map /> 控件用来加载地图。Maps是由很多小的点阵图组成的,它取决于地图的纬度和经度的位置、放大系数和其它的少数因素。那是许多潜在的位图。每次用户与Bing <Map /> 控件相互作用的时候新的位图将被从必应地图服务器上下载下来并载入到内存中。甚至一个小的Maps控件也能加载很多的图片。

    <m:Map 
    xmlns:m="clr-namespace:Microsoft.Phone.Controls.Maps;assembly=Microsoft.Phone.Controls.Maps"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"/>

    当导航到该地图两分钟后你将看到内存几乎是50 MB,很多图片被初始化并且产生5个不同的垃圾收集器。对于512 MB 上90 MB的工作集的手机来说50 MB比256 MB设备上的60 MB工作集的问题小得多。因此需要更加注意256 MB手机上的Bing <Map /> 控件。 一个可能的很快的但不太好的修复方法是使得必应地图控件成为非交互性的。

    <m:Map 
    xmlns:m="clr-namespace:Microsoft.Phone.Controls.Maps;assembly=Microsoft.Phone.Controls.Maps"
    HorizontalAlignment="Stretch"
    VerticalAlignment="Stretch"
    IsHitTestVisible="False" />

    通过设置IsHitTestVisible=False 可以本质上说Bing Maps控件是静态的。通过在Bing <Maps /> 控件里禁止用户的导航,你可以基本上避免这个控件带来的大量图片的加载。将要加载的图片只是来自于你所设置的最初的属性。

    然而,如果你需要允许用户在map里导航,上面那种就用不了了。在这种情况下建议你使用BingMapsTask。BingMapsTask将打开一个单独的应用程序并且当内存紧张的时候墓碑化应用程序。因此请确保你的应用程序支持墓碑机制。

    private void Button_Click(object sender, RoutedEventArgs e)
    {
    new BingMapsTask()
    {
    SearchTerm = "Espoo, Finland"
    }.Show();
    }

    推荐最好的做法是使用BingMapsTask代替Bing <Map /> 控件。另外一个可能性是一种混合的方法监测Bing <Map/> 的内存使用情况,如果它超过90 MB然后你将控件从visual tree中移除并启动BingMapsTask。

    Tip #7——考虑降低图片的质量

    图片至少消耗其在硬盘所占大小的那么多的内存。非移动的优化的图片的过度使用将不可避免地导致非常大的内存使用量。减少图片内存痕迹有很多方法:使用480x800图片最大尺寸,广泛地选择图片格式(PNG或JPG)并减少所需要图片的质量。 例如,这个URL有一张4913x3400的图片

    [File:Windows Phone image memory large.png]

    你可以看到右上方的<MemoryCounter /> 显示了该图片占用了13MB-16MB的内存,而不是默认Windows Phone 7应用程序所占用的6 MB的内存。分配了8 MB的内存。你可以在Windows Phone上缩减该图片的最大分辨率,这样将节省大量内存。

    让我们将图片的宽度缩成800像素,这也差不多是WP7上所需要的任何图片的最大宽度。通过限制图片的最大分别率我们不会失去高质量性也不会影响用户体验但降低了内存占用量。

    [File:Windows Phone image memory small.png]

    我们可以看到仅仅通过将该图片的大小从4913x3400 重新设置为 800x554就将其内存占用量从13-17MB 降低到 9-13MB。为<Image /> 控件推荐的最佳做法是只要有可能就选择低分辨率的图片。如果在你的实际情况中低分辨率的图片不可用,与其在服务器上调整图片的大小不如不显示该图片。

    Tip #8——考虑用使用数据虚拟化的ListBox代替一长串的图片

    正如我们刚刚所看到的图片有可能占用大量的内存。但是因为一个图片就占用了不小的内存那一连串的图片占用的内存将相当的多。让我们看一个使用一连串的Flickr图片的例子。

    作为第一步,我们要下载Flickr.net API并向FlickrNetWP7 集合添加一个引用。接下来我们搜索Flickr并将他们添加到UI:

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
    Flickr flickr = new Flickr("<flickr API token>", "<flickr API secret>");
    flickr.PhotosSearchAsync(new PhotoSearchOptions(null, "nokia"),
    result =>
    {
    Dispatcher.BeginInvoke(() =>
    lst.ItemsSource = result.Result);
    });
    }

    连同它们相应的标题列出这些图片:

    <ListBox x:Name="lst" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> 
    <ListBox.ItemTemplate>
    <DataTemplate>
    <StackPanel Orientation="Horizontal">
    <Image Source="{Binding LargeUrl}" Width="200" Stretch="UniformToFill"/>
    <TextBlock Text="{Binding Title}" Margin="2" />
    </StackPanel>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>

    运行该应用程序并对其进行内存分析你将看到内存超过了90 MB。随着各个图片被初始化,你甚至可以看到内存使用量上的小“台阶”。

    Windows Phones list with images memory use.png

    一种解决方法是限制所显示图片的总数量为10然后放到一个单独的页面上。另一个方法是从使用大的Flickr图片转向使用更小的Flickr。在该例中我们限制一次只显示10个小的Flickr图片。

    <ListBox x:Name="lst" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"> 
    <ListBox.ItemTemplate>
    <DataTemplate>
    <StackPanel Orientation="Horizontal">
    <Image Source="{Binding SmallUrl}" Width="200" Stretch="UniformToFill"/>
    <TextBlock Text="{Binding Title}" Margin="2" />
    </StackPanel>
    </DataTemplate>
    </ListBox.ItemTemplate>
    </ListBox>
    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
    Flickr flickr = new Flickr("2191ef82aa075c112349ff21c45f4b27", "224975f561e65fc4");
    flickr.PhotosSearchAsync(new PhotoSearchOptions(null, "amazing everyday"),
    result =>
    {
    Dispatcher.BeginInvoke(() =>
    lst.ItemsSource = result.Result.Take(10));
    });
    }

    当我们为该应用程序运行内存分析器时我们可以看到一个更合理的内存消耗量。

    我们使用非常少的功能变化改变了该应用程序的内存占用量。当然,针对每个应用程序你需要衡量长列表的图片是不是有意义。推荐给开发人员的最佳做法是查阅任何长列表的图片,分析那些页面的内存占用情况并根据需要改变用户体验。第一步是分析用使用了数据虚拟化的ListBox代替后的内存占用,例如Peter Torr's LazyListBoxDavid Anson's DeferredLoadListBox。如果只是改变ListBox控件变体不起作用,你就需要考虑用户体验的变换了。例如假设使用分页,将不同类别的数据放在不同的页面上或使用Silverlight Toolkit LongListSelector。用户体验的变化取决于特定的商业域和用户体验要求。

    Tip #9——考虑禁止页面转换

    页面转换是用户在不同的页面之间进行导航的动画(例如flip-in 和 flip-out)。在WP7上那些动画一般都是通过使用TransitionFrame 和 TransitionService的Silverlight Toolkit for Windows Phone来完成的。在这个部分我们将致力于使用TransitionFrame 和 TransitionService对内存消耗的影响。我们将看到页面转换的内存消耗量大约是5 MB。与90 MB的工作集相比,5 MB对60 MB的工作集来说也是挺重要的。任何或所有的页面转换将引起该内存占用而不仅仅是Silverlight Toolkit的实现。同时有两个页面产生动画的实现将内在地消耗不少的内存。

    我们从下载Silverlight Toolkit for Windows Phone开始我们的例子。你可以使用NuGet安装Silverlight Toolkit或安装MSI并添加Microsoft.Phone.Controls.Toolkit 引用。一旦你完成这些你将需要对你的程序做两点改变。第一是在App.xaml.cs里使用TransitionFrame而不使用老式的PhoneApplicationFrame。

    //RootFrame = new PhoneApplicationFrame();
    RootFrame = new TransitionFrame();

    对于每个页面我们想要启用页面转换因为我们将要添加下面的XAML代码来指定动画发生的位置。若要了解怎样使用TransitionFrame 的更多信息请关注WindowsPhoneGeek的Windows Phone 7 Navigation Transitions Step By Step指导。

    <toolkit:TransitionService.NavigationInTransition>
    <toolkit:NavigationInTransition>
    <toolkit:NavigationInTransition.Backward>
    <toolkit:TurnstileTransition Mode="BackwardIn"/>
    </toolkit:NavigationInTransition.Backward>
    <toolkit:NavigationInTransition.Forward>
    <toolkit:TurnstileTransition Mode="ForwardIn"/>
    </toolkit:NavigationInTransition.Forward>
    </toolkit:NavigationInTransition>
    </toolkit:TransitionService.NavigationInTransition>
    <toolkit:TransitionService.NavigationOutTransition>
    <toolkit:NavigationOutTransition>
    <toolkit:NavigationOutTransition.Backward>
    <toolkit:TurnstileTransition Mode="BackwardOut"/>
    </toolkit:NavigationOutTransition.Backward>
    <toolkit:NavigationOutTransition.Forward>

     <toolkit:TurnstileTransition Mode="ForwardOut"/>
    </toolkit:NavigationOutTransition.Forward>
    </toolkit:NavigationOutTransition>
    </toolkit:TransitionService.NavigationOutTransition>

    当运行该应用程序时我们发现当前内存占用量约为12 MB最高占用量约为17 MB。

    如果我们禁止页面转换我们可以发现当前内存使用量将有所降低,但变化最明显的是最高内存使用量。我们可以通过原先的PageTransitionFrame 而不是使用TransitionFrame来禁止页面转换。

    RootFrame = new PhoneApplicationFrame();
    //RootFrame = new TransitionFrame();

    当我们禁止页面转换后再运行该应用程序我们发现内存占用量或多或少的降低了。

    在下图中我们可以清晰地看见简化了的应用的实验结果:

    Windows Phone transition memory effect.png

    当页面转换能实现Metro UI "fast and fluid"的原理,即使我们只有60 MB的可用内存的时候损失5MB-7MB的内存也不为过。这是一个微妙的平衡。对大多数应用程序来说对256 MB设备禁用页面转换起作用,除非你能保证整个应用程序的内存使用量不超过55MB~。

    if (LowMemoryHelper.IsLowMemDevice)
    {
    RootFrame = new PhoneApplicationFrame();
    }
    else
    {
    RootFrame = new TransitionFrame();
    }

    推荐开发人员的最佳做法是在256 MB的设备上禁用页面转换,除非在256 MB的设备上测试过他

    们的应用程序并且整个内存使用量不超过90 MB。

    Tip #10——避免多次初始化同一个SoundEffects

    很多XNA游戏甚至是少数Silverlight应用程序使用SoundEffect 来播放简短的音频。最常见的是在游戏里添加声音效果。这种效果可以带来枪声、移动、碰撞和击打的效果。那些事件频繁发生在游戏中。然而,初始化多个SoundEffects 但不处置它们将引起不少的临时内存。若要了解关于在WP7 中使用SoundEffect 的更多信息请关注Maarten Struys的Adding Sound Effects to a Windows Phone 7 Silverlight Application

    256 MB设备一个糟糕的做法是每次播放声音都初始化一个SoundEffect 。

    private void Button_Click(object sender, RoutedEventArgs e)
    {
    SoundEffect beep = SoundEffect.FromStream(
    Application.GetResourceStream(
    new Uri("NokiaBeep.wav", UriKind.RelativeOrAbsolute))
    .Stream);
     
    FrameworkDispatcher.Update();
    beep.Play();
    }

    如果我们运行该应用程序并多次短而快速连续地点击按钮,其内存使用情况如下所示:

    Windows Phone Sound Memory Footprint.png

    你将会看到即使SoundEffects 不能被重复使用或配置,SoundEfffect将触发Garbage Collector运行GC事件。这种情况是可以被512 MB的设备所接受的。但是由于逐渐累积的内存在垃圾回收之前就能轻易地超过60 MB,这对256 MB的设备来说是不好的。在我们的一个简单的例子中我们几乎没有visuals和确切的游戏逻辑能够达到40MB~的内存使用量。当你的工作集仅仅只有60 MB的时候,不处置或再利用SoundEffects 内存的使用量越来越明显。

    XNA开发社区提供了很多种好的方法处理SoundEffects。你可以使用timer全体地处理soundeffects;你可以将可重复利用的SoundEffects保存在字典中,等等。在我们的例子中将SoundEffect 保存了起来当页面不再可见并不再被需要的时候处理掉它。

    private SoundEffect beep = null;
    private void Button_Click(object sender, RoutedEventArgs e)
    {
    if (beep == null)
    {
    beep = SoundEffect.FromStream(
    Application.GetResourceStream(
    new Uri("NokiaBeep.wav", UriKind.RelativeOrAbsolute))
    .Stream);
    }
     
    FrameworkDispatcher.Update();
    beep.Play();
    }
     
    protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
    {
    base.OnNavigatedFrom(e);
    beep.Dispose();
    }

    通过做出这个小的改变我们只分配了一个SoundEffect,该应用的行为是相同的并且我们已经很大地减少了内存使用量。在内存中我们不再分配很多SoundEffects 而是只分配了一个。推荐的最佳做法是避免多次初始化同一个SoundEffects ,当需要时保存初始化过的SoundEffects 不需要时处理掉它们。

    Tip #11——压缩XNA assets

    XNA游戏能够将非常多的assets加载到内存中所以很容易超过90 MB。它将通过压缩使用的vector data 或 textures减少一个元素整体的内存使用量。最乐观的情形下你可以在不影响质量和不影响CPU和GPU表现的同时减少assets在硬盘上的大小和在内存中的大小。在大多数情况下数据压缩很有可能引起XNA assets质量下降的问题。

    针对你的XNA应用程序所拥有的assets压缩有几个选择。例如,通过放弃位置的精确信息(通过normalizing vector data)节省模型的25%(每32字节节省8字节)。另一个可能是使用DXT compression algorithms降低整个asset的质量。这里的想法并不是“压缩”在内存中的内容而没有消耗显著的CPU 或 GPU时间。这不是为了减少在硬盘上的大小而是减少在内存中的大小。例如JPG压缩对我们不起作用因为它只是压缩在硬盘上的大小;然而DXT既能压缩硬盘的占用量还能压缩内存的占用量。

    这所有的压缩算法将作为一个简易包装的步骤在开发机器上运行。此外当它依赖算法运行的时候压缩算法将被应用程序所使用。若要了解XNA asset 压缩的介绍信息,请参考Shawn Hargreaves的Compressed GPU data formats

    XNA游戏的最后一点是根据游戏的循环和当前的内存消耗量计划调用垃圾收集器(GC.Collect)。推荐XNA开发人员的最佳做法是分析它们内存的使用量,如果需要的话(超过90 MB)探究asset压缩策略。关于XNA内存优化的详细消息请参考Improving Memory Use in XNA Games

    Tip #12——小心查找和排除内存泄露

    内存泄露是由当正常使用应用程序突然出乎意料地、逐步地分配内存但之后不再重新分配那部分内存所引起的。内存泄露的常见迹象是随着每次页面导航都减少了几MB的内存并且在重启应用之前那些内存无法回收利用。256 MB上内存泄露情况与512 MB设备上内存泄露情况一样频繁并且没有什么区别。然而由于低内存工作集(60 MB 超过 90 MB)的情况尤为明显。例如在50 MB内存上运行的WP7 应用程序可以在512 MB的设备上泄露40 MB,但在256 MB设备上只能泄露10 MB的工作集。在用户明显感到内存泄露之前对应用程序的使用时间是相当少的。

    若要了解关于怎样诊断WP7上的内存泄露请参考Windows Phone的博客Memory Profiling for Application Performance

    推荐的最佳做法是了解怎样Find Managed Memory Leaks in WPF and Silverlight applications,在长时间的使用过程中分析应用程序并根据整个应用的性能监听用户。如果用户反映"随着时间的推移应用逐渐的变得很‘累’ "或“当使用应用一个小时后应用总是崩溃”,那些就是内存泄露的常见标志。在特定时间内看看管理堆上有些什么并判断它是不是应该在那儿。

     
  • 相关阅读:
    java_爬虫_从腾讯视频播放界面爬取视频真实地址
    杂_小技巧_将网页上的内容通过亚马逊邮箱传到kindle中
    java_基础_接口和抽象类
    知乎上的50道SQL练习题
    第 4 章 WebDriver API
    第 2 章 测试环境搭建
    第 1 章 自动化测试基础
    【软件测试】9.QC管理学习(类禅道)学习
    01 Python简介、环境安装、变量、数据类型
    【MySQL面试指南】
  • 原文地址:https://www.cnblogs.com/Yukang1989/p/2828320.html
Copyright © 2011-2022 走看看