zoukankan      html  css  js  c++  java
  • 使用 ObjectDataSource 缓存数据

    简介

    就计算机科学而言 , 缓存 过程包括成本昂贵的数据或信息的获取 , 以及将备份存储在可快速访问的位置。对于数据驱动的应用程序,大型、复杂的查询通常会消耗大量应用程序执行时间。要提升这类应用程序的性能,通常的做法是,将昂贵的数据库查询的结果存储在应用程序的内存中。

    ASP.NET 2.0 提供了许多缓存方式。整个网页或用户控件所呈现的标记可通过输出缓存 进行缓存。同样, ObjectDataSource 和 SqlDataSource 控件也提供了缓存功能,允许在控件级对数据进行缓存。而 ASP.NET 的数据缓存 提供了丰富的缓存 API ,允许页面开发人员通过编程缓存对象。在本教程和接下来的三个教程中,我们将研究如何使用 ObjectDataSource 的缓存功能和数据缓存。我们也将探索如何在启动时缓存应用级数据,以及如何通过使用 SQL 缓存依赖项使缓存的数据保持最新状态。这些教程中没有涉及输出缓存。有关输出缓存的详细信息,参见 ASP.NET 2.0 中的输出缓存 。

    缓存可以应用在架构中的任意位置 , 从数据访问层直至表示层。在本教程中,我们将探讨如何通过 ObjectDataSource 控件将缓存应用到表示层。在下一篇教程中,我们将研究如何在业务逻辑层缓存数据。

    主要缓存概念

    通过获取成本昂贵的数据并将备份存储在便于访问的位置 , 缓存可以极大地改善应用程序的整体性能和可伸缩性。由于缓存仅保留实际基础数据的备份,如果基础数据发生改变,缓存中的数据可能会 过期。为解决此问题,页面开发人员可以指定适当的标准,将缓存项从缓存中 清除。可以使用的标准包括:

    • 基于时间的标准 – 可以向缓存中添加项目 , 项目的驻留时间固定或可变。例如,页面开发人员可以指定一个 60 秒的时间段。当时间段固定不变时,在将项目添加到缓存后,无论访问该项目的频率有多高,缓存项都会在 60 秒后清除。当时间段灵活可变时,缓存项会在最后一次访问超过 60 秒后清除。
    • 基于依赖项的标准 – 在向缓存中添加项目时 , 可以将项目与依赖项结合。当项目的依赖项发生改变时,该项目将从缓存中清除。依赖项可以是一个文件、另一个缓存项,或这两者的混合体。ASP.NET 2.0 也启用了 SQL 缓存依赖项,允许开发人员向缓存中添加项目,并在基础数据库数据发生改变时将其清除。我们将在后面的使用 SQL 缓存依赖项教程中讨论 SQL 缓存依赖项。

    无论指定哪种清除标准 , 在基于时间的标准或基于依赖项的标准得到满足之前 , 缓存中的项目也可能被清除。如果容量达到极限,缓存会在添加新项目之前删除现有项目。因此,当通过编程处理缓存的数据时,很重要的一点是,应假设缓存的数据可能不存在。在下一篇教程在架构中缓存数据 中,我们将研究从缓存中访问数据的具体模式。

    缓存是提升应用程序性能的较为经济的方式。就像 Steven Smith 在他的文章 ASP.NET Caching: Techniques and Best Practices中阐述的一样:

    “缓存是获得 ‘上佳’ 性能的一种好方法,不需要太多的时间和分析。……内存很便宜,因此,如果通过缓存 30 秒钟的输出(而非花费一天或一周的时间优化代码或数据库)就可以获得所需的性能,那么请采用缓存解决方案(假设 30 秒的旧数据是可接受的)然后继续。…… 最后 , 糟糕的设计总会显现出来 , 因此 , 开发人员当然应该正确地设计应用程序。但如果你只需在今天获得足够的性能,缓存是一种出色的方式。重构应用程序可以在稍后时间充足的情况下进行。”

    尽管缓存可以提供显著的性能提升 , 它并不适合所有情况 , 如使用实时、频繁更新数据的应用程序 , 或不接受过期数据 ( 甚至短期过期数据 ) 的应用程序。但对于大部分应用程序,缓存都是适用的。有关 ASP.NET 2.0 中缓存技术的更多背景,参见ASP.NET 2.0 入门教程 的 缓存性能 部分。

    步骤1 :创建缓存网页

    在开始研究 ObjectDataSource 的缓存功能之前 , 我们会先花一些时间在我们的网站项目中创建ASP.NET 页面。这些页面需要在本教程和后面三个教程中使用。首先,添加名为 Caching 的新文件夹。接下来,将以下 ASP.NET 页面添加到该文件夹,确保每个页面与 Site.master 主页面相关联:

    • Default.aspx
    • ObjectDataSource.aspx
    • FromTheArchitecture.aspx
    • AtApplicationStartup.aspx
    • SqlCacheDependencies.aspx

    图1 :为缓存相关教程添加 ASP.NET 页面

    Caching 文件夹中的Default.aspx 与其他文件夹中的相同 , 用于列出该部分的教程。回想一下, SectionLevelTutorialListing.ascx 用户控件提供本功能。因此,您可以从 Solution Explorer 中将此 User 控件拖拽到页面设计视图进行添加。

    图2 :向 Default.aspx 添加 SectionLevelTutorialListing.ascx 用户控件

    最后 , 将这些页面作为条目添加到 Web.sitemap 文件中。具体地说,就是在 “Working with Binary Data” <siteMapNode> 的后面添加以下标记:

    <siteMapNode title="Caching" url="~/Caching/Default.aspx"  
        description="Learn how to use the caching features of ASP.NET 2.0."> 
        <siteMapNode url="~/Caching/ObjectDataSource.aspx"  
            title="ObjectDataSource Caching"  
            description="Explore how to cache data directly from the  
                ObjectDataSource control." /> 
        <siteMapNode url="~/Caching/FromTheArchitecture.aspx"  
            title="Caching in the Architecture"  
            description="See how to cache data from within the  
                architecture." /> 
        <siteMapNode url="~/Caching/AtApplicationStartup.aspx"  
            title="Caching Data at Application Startup"  
            description="Learn how to cache expensive or infrequently-changing  
                queries at the start of the application." /> 
        <siteMapNode url="~/Caching/SqlCacheDependencies.aspx"  
            title="Using SQL Cache Dependencies"  
            description="Examine how to have data automatically expire from the  
                cache when its underlying database data is modified." /> 
    </siteMapNode>

    更新 Web.sitemap 后 , 花点时间用浏览器查看一下教程网站。现在,左侧的菜单包含了缓存教程的条目。<0}

    图3 :现在 , 网站映射中包含缓存教程的条目

    步骤2 :在网页中显示产品列表

    本教程将研究如何使用 ObjectDataSource 控件的内置缓存功能。但在考虑这些功能之前,我们首先需要一个可进行操作的页面。让我们创建一个网页,该网页使用 GridView 列出由 ObjectDataSource 从 ProductsBLL 类获得的产品信息。

    首先,在 Caching 文件夹中打开ObjectDataSource.aspx 页面。从 Toolbox 中将 GridView 拖放到 Designer , 将它的 ID 属性设置为 Products , 并从它的智能标记中选择将其绑定到名为ProductsDataSource 的新ObjectDataSource 控件。设置 ObjectDataSource 使用 ProductsBLL 类。

    <0}

    图4 :设置 ObjectDataSource 使用 ProductsBLL 类

    在本页面 , 我们要创建一个可编辑的 GridView 。当 ObjectDataSource 中缓存的数据发生变化时,我们可以通过 GridView 的界面看到发生了什么。保留 SELECT 选项卡中下拉列表的默认值, GetProducts() ,但将 UPDATE 选项卡中的所选项目更改为接受 productName 、 unitPrice 和 productIDtab 作为输入参数的 UpdateProduct 重载方法。

    图5 :将 UPDATE 选项卡的下拉列表设为适当的 UpdateProduct 重载方法

    最后,将 INSERT 和 DELETE 选项卡中的下拉列表设置为 “(None)” , 然后单击 Finish 。一旦完成 Configure Data Source 向导, Visual Studio 会将 ObjectDataSource 的 OldValuesParameterFormatString 属性设置为 original_{0} 。就像在前面的教程插入、更新和删除数据概述 中探讨的那样,该属性需从声明性语法中删除,或设置回默认值 {0} ,这样我们的更新工作流在运行中才不会出错。

    此外,在此向导完成时,Visual Studio 会为每个产品数据字段 在GridView 中 添加一个字段。将除 ProductName 、 CategoryName 和 UnitPrice 之外的所有 BoundField 删除。然后,将上述 3 个 BoundField 的 HeaderText 属性分别更新为 “Product” 、 “Category” 和 “Price” 。由于 ProductName 字段是必须的,将 BoundField 转换为 TemplateField 并向 EditItemTemplate 中添加一个 RequiredFieldValidator 。同样,将 UnitPrice BoundField 转换为 TemplateField 并添加一个 CompareValidator ,来确保用户输入的值是大于或等于 0 的有效货币值。在这些更改之外,开发人员还可以对外观进行任意修改,如,使 UnitPrice 值向右对齐,或指定 UnitPrice 文本在只读和编辑界面的格式。

    在 GridView 的智能标记中选中 “Enable Editing” 复选框 , 允许对 GridView 进行编辑。同时,选中 “Enable Paging” 和 “Enable Sorting” 复选框。<0}

    注意 :如果需要回顾如何定制 GridView 的编辑界面 ,请参考前面的定制数据修改界面 教程。

    图6 :允许 GridView 支持编辑、排序和分页

    在进行了这些 GridView 修改后 ,GridView 和 ObjectDataSource 的声明标记应如下所示 :

    <asp:GridView ID="Products" runat="server" AutoGenerateColumns="False"  
        DataKeyNames="ProductID" DataSourceID="ProductsDataSource"  
        AllowPaging="True" AllowSorting="True"> 
        <Columns> 
            <asp:CommandField ShowEditButton="True" /> 
            <asp:TemplateField HeaderText="Product" SortExpression="ProductName"> 
                <EditItemTemplate> 
                    <asp:TextBox ID="ProductName" runat="server"  
                        Text='<%# Bind("ProductName") %>'></asp:TextBox> 
                    <asp:RequiredFieldValidator  
                        ID="RequiredFieldValidator1" Display="Dynamic"  
                        ControlToValidate="ProductName" SetFocusOnError="True" 
                        ErrorMessage="You must provide a name for the product." 
                        runat="server">*</asp:RequiredFieldValidator> 
                </EditItemTemplate> 
                <ItemTemplate> 
                    <asp:Label ID="Label2" runat="server"  
                        Text='<%# Bind("ProductName") %>'></asp:Label> 
                </ItemTemplate> 
            </asp:TemplateField> 
            <asp:BoundField DataField="CategoryName" HeaderText="Category"  
                ReadOnly="True" SortExpression="CategoryName" /> 
            <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice"> 
                <EditItemTemplate> 
                    $<asp:TextBox ID="UnitPrice" runat="server" Columns="8"  
                        Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox> 
                    <asp:CompareValidator ID="CompareValidator1" 
                        ControlToValidate="UnitPrice" Display="Dynamic"  
                        ErrorMessage="You must enter a valid currency value with no  
                            currency symbols. Also, the value must be greater than  
                            or equal to zero." 
                        Operator="GreaterThanEqual" SetFocusOnError="True"  
                        Type="Currency" runat="server"  
                        ValueToCompare="0">*</asp:CompareValidator> 
                </EditItemTemplate> 
                <ItemStyle HorizontalAlign="Right" /> 
                <ItemTemplate> 
                    <asp:Label ID="Label1" runat="server"  
                        Text='<%# Bind("UnitPrice", "{0:c}") %>' /> 
                </ItemTemplate> 
            </asp:TemplateField> 
        </Columns> 
    </asp:GridView> 
     
    <asp:ObjectDataSource ID="ProductsDataSource" runat="server" 
        OldValuesParameterFormatString="{0}" SelectMethod="GetProducts"  
        TypeName="ProductsBLL" UpdateMethod="UpdateProduct"> 
        <UpdateParameters> 
            <asp:Parameter Name="productName" Type="String" /> 
            <asp:Parameter Name="unitPrice" Type="Decimal" /> 
            <asp:Parameter Name="productID" Type="Int32" /> 
        </UpdateParameters> 
    </asp:ObjectDataSource>

    如图 7 所示,可编辑的 GridView 列出了数据库中每种产品的名称、类别和价格。花几分钟的时间对该页面的功能进行测试 – 对结果进行排序、翻页、编辑一条记录。

    图7 :每个产品的名称、类别和价格显示在可排序、分页和可编辑的GridView 中

    步骤3 :检查ObjectDataSource 何时在请求数据

    ID 为 Products 的 GridView 通过调用 ProductsDataSource ObjectDataSource 的 Select 方法来 检索 数据 , 并将数据显示出来。ObjectDataSource 为业务逻辑层的 ProductsBLL 类创建实例并调用该类的 GetProducts() 方法,该方法又会调用数据访问层的 ProductsTableAdapter 的 GetProducts() 方法。数据访问层的方法连接到 Northwind 数据库并发出已配置好的 SELECT 查询。然后,查询数据返回到数据访问层,由数据访问层将其打包到 NorthwindDataTable 中。DataTable 对象依次返回给业务逻辑层、 ObjectDataSource 、 GridView 。之后, GridView 为 DataTable 中的每个 DataRow 创建一个 GridViewRow 对象,每个 GridViewRow 最终呈现为 HTML ,返回到客户端并显示在访问者的浏览器中。

    每次 GridView 需要绑定到基础数据 , 这些事件就会按上述顺序发生。例如:首次访问页面;将数据从一个页面传递到另一个页面;为 GridView 排序;以及通过 GridView 内置的编辑或删除界面来修改 GridView 的数据。如果禁用了 GridView 的视图状态,在每次页面回传时也会重新绑定 GridView 。我们也可以通过调用 DataBind() 方法明确地将 GridView 重新绑定数据。

    为了更清楚地揭示从数据库中检索数据的频率,让我们来显示一条消息,指示重新检索数据的时间。在 GridView 上添加一个名为 ODSEvents 的 Label Web 控件。清空其 Text 属性,并将 EnableViewState 属性设为 False 。在 Label 控件下方再添加一个 Button Web 控件,将其 Text 属性设为 “Postback” 。

    图8 :在 GridView 上向页面添加 Label 和 Button 控件

    在数据访问工作流中 , 首先触发 ObjectDataSource 的 Selecting 事件 , 然后创建基础对象并调用对应的配置好的方法。为上述事件创建一个 event handler ,并添加以下代码:

    protected void ProductsDataSource_Selecting(object sender,  
        ObjectDataSourceSelectingEventArgs e) 

        ODSEvents.Text = "-- Selecting event fired"; 
    }

    每次 ObjectDataSource 向架构请求数据 ,Label 控件都会显示文本 “Selecting event fired” 。

    在浏览器中访问该页面。首次访问该页时 , 页面上显示文本 “Selecting event fired” 。单击 “Postback” 按钮时,我们注意到文本消失了(假设 GridView 的 EnableViewState 属性设置为默认值 True )。这是因为 , 在页面回传时 ,GridView 从视图状态进行重建 , 因此不必通过 ObjectDataSource 获取数据。然而,数据的排序、分页或编辑都会使 GridView 重新绑定到数据源,因此 “Selecting event fired” 文本会重新出现。

    图9 :GridView 重新绑定到数据源时,显示 “Selecting event fired”

    图10 :单击 Postback 按钮导致 GridView 从视图状态进行重建

    每次进行数据分页或排序都要重新检索数据库数据 , 这看起来有些浪费资源。由于我们在使用默认分页,在显示第一个页面时, ObjectDataSource 已经检索了所有记录。即使 GridView 不支持排序和分页,任何用户首次访问页面时, GridView 都必须从数据库检索数据(如果禁用了视图状态,那么每次页面回传时也会检索数据)。但如果 GridView 对所有用户显示相同的数据,那么这些额外的数据库请求都是多余的。这时,我们可以缓存 GetProducts() 方法返回的结果,并将 GridView 与这些缓存的结果绑定。

    步骤4 :使用 ObjectDataSource 缓存数据

    简单地设置几个属性,我们就可以配置 ObjectDataSource ,使其自动将检索数据缓存到 ASP.NET 数据缓存中。以下列表总结了与缓存相关的 ObjectDataSource 属性:

    • EnableCaching– 要启用缓存必须设置为 True 。默认值为 False 。
    • CacheDuration – 数据缓存的时间,以秒为单位。默认值为 0 。只有当 EnableCaching 设置为 True 且 CacheDuration 设置为大于 0 的值时, ObjectDataSource 才会缓存数据。
    • CacheExpirationPolicy– 可设置为 Absolute 或 Sliding 。如果设置为 Absolute ,ObjectDataSource 缓存检索数据的时间为 CacheDuration 的值 ; 如果设置为 Sliding , 仅当未访问数据的时间达到 CacheDuration 的值时 , 数据才会过期。默认值为 Absolute 。
    • CacheKeyDependency – 使用该属性将 ObjectDataSource 的缓存条目与当前缓存依赖项关联起来。通过使相关的 CacheKeyDependency 过期,我们可以提前将 ObjectDataSource 的数据条目从缓存中清除。该属性经常用于使 SQL 缓存依赖项与 ObjectDataSource 的缓存关联,我们会在后面的使用SQL 缓存依赖项教程中讨论这个主题。

    让我们对 ProductsDataSource ObjectDataSource 进行配置 , 使其缓存数据的时间段为固定的 30 秒。将 ObjectDataSource 的 EnableCaching 属性设置为 True , 将 CacheDuration 属性设置为 30 。保留 CacheExpirationPolicy 属性的默认值 Absolute 。

    图11 :将 ObjectDataSource 的数据缓存时间设置为 30 秒

    保存更改,重新在浏览器中访问该页面。首次访问页面时,页面显示 “Selecting event fired” 文本,因为最初还未进行数据缓存。但单击 “Postback” 按钮、排序、分页或单击 Edit 或 Cancel 按钮触发的页面回传不会 再显示 “Selecting event fired” 文本。这是因为,只有当 ObjectDataSource 从基础对象获取数据时, Selecting 事件才会触发;从数据缓存中获取数据不会触发 Selecting 事件。

    30 秒之后 , 数据将从缓存中清除。如果调用了 ObjectDataSource 的 Insert 、Update 或 Delete 方法,数据也会从缓存中清除。因此,以下情况都会导致 ObjectDataSource 从基础对象获取数据,并在触发 Selecting 事件时显示 “Selecting event fired” 文本:超过 30 秒、单击 Update 按钮、排序、分页或单击 Edit 或 Cancel 按钮。然后,系统再将这些返回的结果存储在数据缓存中。

    注意 :如果 “Selecting event fired” 文本频繁出现,甚至在我们认为 ObjectDataSource 应使用缓存数据的时候也是如此,很可能是因为内存限制。如果内存不足,由 ObjectDataSource 添加到缓存的数据可能已被清除。如果 ObjectDataSource 没有进行正常的数据缓存,或只是偶尔进行数据缓存,请关闭一些应用程序来释放内存,然后再试一次。

    图 12 阐明了ObjectDataSource 的缓存工作流。当屏幕上出现 “Selecting event fired” 文本时,原因是数据未在缓存中,且必须从基础对象中检索。而当此文本消失时,意味着数据位于缓存中。当数据从缓存中返回时,系统未进行基础对象的调用,因此,没有执行数据库查询。

    图12 :ObjectDataSource 在数据缓存中存储和检索数据

    每个 ASP.NET 应用程序都有自己的数据缓存实例 , 所有页面和访问者可以共享该实例。这意味着,所有访问页面的用户也可以共享由 ObjectDataSource 在数据缓存中存储的数据。为验证这一点,我们在浏览器中打开 ObjectDataSource.aspx 页面。在首次访问该页面时,屏幕上出现 “Selecting event fired” 文本(假设由前面的测试添加到缓存的数据到此时已从缓存中清除了)。打开第二个浏览器实例,将第一个浏览器实例中的 URL 复制并粘贴到第二个实例中。在第二个浏览器实例中, “Selecting event fired” 文本没有显示,因为该实例使用的是第一个实例缓存的数据。

    在向缓存中插入检索数据时 ,ObjectDataSource 使用一个缓存键值 , 该值包括 :CacheDuration 和 CacheExpirationPolicy 属性值 ;ObjectDataSource 使用的基础业务对象的类型 , 该类型通过 TypeName 属性 ( 本示例中为 ProductsBLL ) 指定 ;SelectMethod 属性的值以及 SelectParameters 参数集中参数的名称和参数值 ;StartRowIndex 和 MaximumRows 属性的值 , 这些属性在执行 自定义分页时使用。

    将这些属性组合成一个缓存键值 , 可以确保在这些值发生改变时有唯一的缓存条目。例如,在以前的教程中,我们使用 ProductsBLL 类的 GetProductsByCategoryID(categoryID)来获取特定类别的所有产品信息。假设一位用户在页面查看饮料类 (CategoryID 为 1 ) 的产品信息。如果 ObjectDataSource 在缓存结果时没有考虑 SelectParameters 的值,当另一位用户登录页面查看调味品类的产品信息时,恰好饮料类产品的信息位于缓存中,第二位用户将会看到缓存的饮料类产品信息,而非他想查看的调味品信息。通过根据这些属性(其中包括 SelectParameters 的值)改变缓存键值, ObjectDataSource 会分别为饮料类和调味品类维护缓存条目。

    过期数据问题

    在调用 ObjectDataSource 的 Insert 、Update 或 Delete 方法时 ,ObjectDataSource 会自动将其项目从缓存中清除。这样做的好处是,当用户在页面上修改数据时,系统清除缓存条目,从而避免了过期数据的问题。然而,使用缓存的 ObjectDataSource 还是可能会显示过期数据。最简单的示例是直接在数据库中修改数据。例如,数据库管理员运行一个脚本在数据库中修改一些记录。

    我们也可以对此情景进行一些细微的扩展。尽管在调用 ObjectDataSource 的数据修改方法时, ObjectDataSource 会将它的项目从缓存中清除,但清除的是与 ObjectDataSource 属性值( CacheDuration 、 TypeName 、 SelectMethod 等)的特定组合相匹配的缓存项目。如果我们有两个 ObjectDataSource ,它们使用不同的 SelectMethod 或 SelectParameter ,但可以更新相同的数据。但第一个 ObjectDataSource 更新一行记录并使自己的缓存条目失效时,第二个 ObjectDataSource 仍为相应的行应用缓存中的数据。下面,我们创建一些页面来展示该功能。首先,创建一个页面,在其中显示可编辑的 GridView 。该 GridView 对应的 ObjectDataSource 使用缓存并配置为通过 ProductsBLL 类的 GetProducts() 方法获取数据。再为本页面添加一个可编辑的 GridView 和一个 ObjectDataSource (或另外创建一个页面),但这个 ObjectDataSource 设置为使用 GetProductsByCategoryID(categoryID) 方法。由于两个 ObjectDataSource 的 SelectMethod 属性不同,因此他们拥有各自的缓存值。如果在第一个 GridView 中编辑一个产品,然后将数据重新绑定到第二个 GridView (通过分页、排序等),第二个 GridView 中显示的产品数据仍然是原有缓存数据,没有反映出第一个 GridView 作出的更改。

    简而言之,如果能够接受存在过期数据的可能性,你可以只使用基于时间的到期机制,但须为重视数据时新性的场合设置较短的到期时间。如果无法接受陈旧的数据,你可以放弃缓存或使用 SQL 缓存依赖项(假设正在缓存数据库数据)。我们将在后面的教程中探讨 SQL 缓存依赖项。

    小结

    在本教程中,我们研究了 ObjectDataSource 的内置缓存功能。仅设置几个属性,我们就可以使 ObjectDataSource 将指定 SelectMethod 返回的结果缓存到 ASP.NET 数据缓存中。CacheDuration 和 CacheExpirationPolicy 属性分别指示缓存项目的时间段以及该时间段为固定值还是可变值。CacheKeyDependency 属性将 ObjectDataSource 的所有缓存条目与现有的缓存依赖项关联到一起。该属性(通常与 SQL 缓存依赖项一起使用)可用于在达到基于时间的期限之前,将 ObjectDataSource 的条目从缓存中清除。

    由于 ObjectDataSource 只是将自己的数据缓存到数据缓存中,我们可通过编程复制 ObjectDataSource 的这种内置功能。在表示层这样做没有意义,因为 ObjectDataSource 已经在该层提供了这种功能。但我们可以在架构的一个独立的层中实现缓存功能。为此,我们需要重复 ObjectDataSource 使用的逻辑。在下一篇教程中,我们将讨论如何通过编程在架构内部使用数据缓存。

    快乐编程!

  • 相关阅读:
    恐怖如斯
    java在vscode中配置环境的坑
    python的迭代器模块
    一个模仿输入print就有这么多知识点
    30个python常用小技巧
    第一个只出现一次的字符
    UIScrollView属性
    iOS 中UISlider常用知识点
    iOS中UISegmentedControl常用属性
    iOS触摸事件
  • 原文地址:https://www.cnblogs.com/rinack/p/5672489.html
Copyright © 2011-2022 走看看