zoukankan      html  css  js  c++  java
  • 第1章 有状态流处理简介

    Apache Flink是一个分布式流处理引擎,提供了直观而富有表现力的api,以此来实现有状态的流处理应用程序。它以一种容错的方式有效地在大规模集群上运行这样的应用程序。2014年4月,Flink加入了Apache软件基金会作为孵化项目,2015年1月,成为顶级项目。从一开始,Flink就拥有一个非常活跃且不断增长的用户和贡献者社区。到今天为止,已经有超过350人对Flink做出了贡献,并且它已经发展成为最成熟的开源流处理引擎之一,为跨不同行业和全球的许多公司和企业提供大规模的关键业务应用程序。

        流处理技术正在被任何规模的公司和企业迅速采用,因为它为许多已建立的用例提供了优越的解决方案,同时也促进了新的应用程序、软件架构和商业机会。在本章中,我们将讨论为什么有状态流处理变得如此流行,并评估其潜力。我们将回顾传统的数据处理应用程序架构并指出它们的局限性。接着,我们将介绍基于有状态流处理的应用程序设计,与传统方法相比,它具有许多有趣的特性和优点。我们简要讨论了开源流处理器的发展,并帮助您在本地Flink实例上运行第一个流应用程序。最后,我们告诉你当你读这本书的时候你会学到什么。

    传统的数据基础设施

        公司使用许多不同的应用程序来运行业务,如企业资源规划(ERP)系统、客户关系管理(CRM)软件或基于web的应用程序。所有这些系统通常都采用了分层的架构:用于数据处理(应用程序本身)和数据存储(事务性数据库系统),如图所示。

    图片

        应用程序通常连接到外部服务或面对用户,并持续处理传入事件,如订单、邮件或网站上的单击。在处理事件时,应用程序通过在远程数据库系统上运行事务来读取其状态或更新其状态。通常,数据库系统服务于多个应用程序,这些应用程序甚至经常访问相同的数据库或表。当应用程序需要扩展时,这种设计可能会导致问题。由于多个应用程序可能使用相同的数据表示或共享相同的基础设施,因此更改表的模式或扩展数据库系统需要仔细的规划和大量的工作。最近一种克服应用程序紧密捆绑的方法是微服务设计模式。微服务被设计成小型的、自包含的、独立的应用程序。他们遵循UNIX哲学,即只做一件事,然后把它做好。更复杂的应用程序是通过相互连接几个微服务来构建的,这些微服务仅通过标准化接口(如RESTful HTTP连接)进行通信。由于微服务彼此之间严格解耦,并且只能通过定义良好的接口进行通信,因此每个微服务都可以使用自定义技术栈(包括编程语言、库和数据存储)来实现。微服务和所有必需的软件和服务通常被绑定并部署在独立的容器中。下图描述了一个微服务体系结构。

    图片

        存储在公司的各种事务数据库系统中的数据可以提供关于公司业务各个方面的有价值的信息。例如,可以对订单处理系统的数据进行分析,以获得随时间的推移,销售量的增长,确定延迟发货的原因或预测未来的销售量,以调整库存。然而,事务性数据通常分布在几个独立的数据库系统中,如果能够对其进行联合分析,就会变得更有价值。此外,通常需要将数据转换为通用格式。

        IT系统中的公共组件是数据仓库,而不是直接在事务数据库上运行分析查询。数据仓库是用于分析查询工作负载的专用数据库系统。为了填充数据仓库,需要将事务性数据库系统管理的数据复制到其中。将数据复制到数据仓库的过程称为extract-transform-load (ETL)。ETL流程从事务性数据库中提取数据,将其转换为公共表示,其中可能包括验证、值规范化、编码、重复数据删除和模式转换,最后将其加载到分析数据库中。ETL过程可能非常复杂,通常需要技术成熟的解决方案来满足性能需求。为了保持数据仓库的数据是最新的,ETL进程需要定期运行。

        数据导入到数据仓库后,就可以查询和分析数据。通常,数据仓库上执行两类查询。第一种类型是定期报告查询,它计算与业务相关的统计数据,如收入、用户增长或生产产量。这些度量标准被组合到有助于评估业务情况的报告中。第二种类型是即席查询(ad-hoc queries),其目的是为特定问题提供答案并支持对业务至关重要的决策。这两种查询都是由数据仓库以批处理方式执行的,即查询的数据输入完全可用,查询返回计算结果后终止。体系结构如图所示。

    图片

        在Apache Hadoop兴起之前,专门的分析数据库系统和数据仓库一直是数据分析工作负载的主要解决方案。然而,随着Hadoop的日益流行,公司意识到很多有价值的数据被排除在他们的数据分析过程之外。通常,这些数据有的是非结构化的,即,不严格遵循关系模式,有的是过于庞大而无法在关系数据库系统中有效地存储。今天,Apache Hadoop生态系统的组件成为了许多企业和公司的IT基础设施的组成部分,他们不是将所有数据插入关系数据库系统,而是将大量数据(如日志文件、社交媒体或web点击日志)写入Hadoop的分布式文件系统(HDFS)或其他批量数据存储(如Apache HBase)中,这样可以以较低的成本提供巨大的存储容量。驻留在这种存储系统中的数据可以被多个SQL-on-Hadoop引擎访问,例如Apache Hive、Apache Drill或Apache Impala。但是,在Hadoop生态系统的存储系统和执行引擎中,基础架构的整体操作模式基本上与传统的数据仓库架构相同,即,数据被周期性地提取并加载到数据存储中,然后通过周期性查询或即席查询,以批处理的方式进行处理。

    有状态的流处理

         一个重要的发现是,几乎所有的数据都是作为连续的事件流创建的。想想用户在网站或移动应用程序中的交互、订单的产生、服务器日志或传感器测量;所有这些数据都是事件流。事实上,很难找到一次性生成的有限的完整数据集的例子。有状态流处理是一种用于处理无界事件流的应用程序设计模式,适用于公司IT基础结构中的许多不同用例。在讨论它的用例之前,我们简要地解释下什么是有状态流处理以及它是如何工作的。

        任何处理事件流的应用程序,如果不是只处理一次(record-at-a-time)的场景,都需要是有状态的,即有能力存储和访问中间数据。当应用程序接收到一个事件时,它可以执行涉及从状态读取数据或将数据写入状态的任意计算。原则上,状态可以存储在许多不同的地方,包括程序变量、本地文件、嵌入式或外部数据库。

        Apache Flink将应用程序状态存储在内存中或嵌入式数据库中,而不是远程数据库中。由于Flink是一个分布式系统,因此需要保护本地状态不受故障影响,以避免在应用程序或机器出现故障时丢失数据。Flink通过定期将应用程序状态的一致检查点写入远程持久存储来保证这一点。状态、状态一致性和Flink的检查点机制将在接下来的章节中详细讨论。下图显示了一个有状态的Flink应用程序。

    图片

        有状态的流处理应用程序常常从事件日志中获取它们的传入事件。事件日志会存储和分发事件流。事件被写入一个持久的、仅支持追加的日志中,这意味着写入事件的顺序不能改变。写入事件日志的流可以由相同或不同的使用者多次读取。由于日志的只追加属性,事件总是以完全相同的顺序发布给所有使用者。有几种事件日志系统可以作为开源软件使用,Apache Kafka是最流行的,或者作为云计算提供商提供的集成服务。

        将运行在Flink上的有状态流应用程序与事件日志连接起来非常有趣,原因有很多。在这种体系结构中,事件日志充当事件的来源,因为它持久保存输入事件,并可以以确定的顺序重播它们。在失败的情况下,Flink通过从先前采取的检查点恢复状态来恢复有状态流应用程序,并重置事件日志上的读位置。应用程序将重播(并快进)事件日志中的输入事件,直到它到达流的尾部。此技术用于从故障中恢复,但也可用于更新应用程序、修复bug和修复以前发出的结果、将应用程序迁移到不同的集群或使用不同的应用程序版本执行测试。

        如前所述,有状态流处理是一种灵活多变的设计模式,可以用于处理许多不同的用例。在接下来的文章中,我们将介绍三种通常使用有状态流处理来实现的应用程序,1)事件驱动的应用程序,2)数据管道应用程序,3)数据分析应用程序,并给出实际应用程序的例子。我们将这些类描述为不同的模式,以强调有状态流处理的通用性。但是,大多数实际应用程序结合了多个有状态流处理应用程序的特征,这再次显示了该应用程序设计模式的灵活性。

    事件驱动的应用程序

        事件驱动的应用程序是有状态的流应用程序,它接收事件流并在接收的事件上应用业务逻辑。根据业务逻辑,事件驱动的应用程序可以触发一些操作,比如发送警报或电子邮件,或者将事件写入流出事件流,这些事件流可能由另一个事件驱动的应用程序使用。

    事件驱动应用程序的典型用例包括:

    • 实时推荐,例如,在顾客浏览零售商网站时推荐产品。

    • 模式检测或复杂事件处理(CEP),例如,用于信用卡交易中的欺诈检测。

    • 异常检测,例如,检测试图入侵计算机网络的行为。

    事件驱动的应用程序是前面讨论的微服务的发展。它们通过事件日志而不是REST调用进行通信,并将应用程序数据作为本地状态保存,而不是将其写入外部数据存储并从外部数据存储(如事务数据库或键值存储)读取。下图描绘了一个由事件驱动的流应用程序组成的服务架构。

    图片

        事件驱动的应用程序是一种有趣的设计模式,因为与单独存储和计算层的传统体系结构或流行的微服务体系结构相比,它们提供了一些好处。本地状态访问,即与对远程数据存储的读写查询相比,从内存或本地磁盘进行读写操作可以提供非常好的性能。扩展和容错不需要特别考虑,因为这些方面是由流处理引擎处理的。最后,通过利用事件日志作为输入源,可以可靠地存储应用程序的完整输入并可以确定地重播。这是非常有吸引力的,特别是与Flink的保存点功能相结合,它可以将应用程序的状态重置为以前一致的保存点。通过重置(可能修改过的)应用程序的状态并重播输入,可以修复应用程序的错误并修复其影响,在不丢失状态的情况下部署应用程序的新版本,或者运行测试。我们知道有一家公司决定基于事件日志和事件驱动的应用程序构建社交网络的后端,利用了这些特性。

        事件驱动的应用程序对运行它们的流处理引擎有很高的要求。业务逻辑受到它能够控制状态和时间的多少的限制。这方面取决于流处理引擎的api、它提供的状态类型以及它对事件时间处理的支持的质量。此外,有且仅有一次的状态一致性和扩展应用程序的能力是基本需求。Apache Flink内置支持这些功能,是运行事件驱动应用程序的一个非常好的选择。

    数据管道和实时ETL

        当今的IT体系结构包括许多不同的数据存储,如关系数据库系统和专用数据库系统、事件日志、分布式文件系统、内存缓存和搜索索引。所有这些系统都以不同的表示形式和数据结构存储数据,为它们的特定目的提供最佳性能。公司的数据存储分布在多个这样的系统中。例如,在webshop中提供的产品信息可以存储在事务数据库、web缓存和搜索索引中。由于数据的这种复制,数据存储必须保持同步。

        定期ETL作业在存储系统之间移动数据的传统方法通常不能足够快地传播更新。相反,一种常见的方法是将所有更改写入作为事实来源的事件日志。事件日志将更改发布给使用者,将更新合并到受影响的数据存储中。根据用例和数据存储,更新需要在合并之前进行处理。例如,它们需要规范化、join或用额外的数据进行填充,或预聚合,即,也通常由ETL进程执行的各种转换。

        以低延迟加载、转换和插入数据是有状态流处理应用程序的另一个常见用例。我们称这种类型的应用程序为数据管道。对数据管道的额外要求是在短时间内处理大量数据的能力,即,支持高吞吐量和扩展应用程序的能力。操作数据管道的流处理器还应该具有许多源和接收连接器,用于从各种存储系统和格式读取数据并将数据写入其中。同样,Flink提供了操作数据管道所需的所有功能,并包含许多连接器。

    数据流分析应用程序

        在本章前面,我们描述了数据分析管道的通用架构。ETL作业周期性地将数据导入到数据存储中,然后通过特别的或预定的查询处理数据。基本的操作模式——批处理——是相同的,不管架构是基于数据仓库还是Hadoop生态系统的组件。虽然定期将数据加载到数据分析系统的方法多年来一直是最先进的,但它有一个明显的缺点。

        显然,ETL作业和报告查询的周期性会导致相当长的延迟。根据调度间隔,可能需要几个小时或几天的时间才能将某些数据展示在报告中。在某种程度上,可以通过使用数据管道应用程序将数据导入数据存储来减少延迟。然而,即使使用连续的ETL,在查询处理事件之前也总是会有一定的延迟。在过去,延迟几小时甚至几天分析数据通常是可以接受的,因为对新结果或新见解的快速反应并没有产生显著的优势。然而,在过去的十年里,这种情况发生了巨大的变化。快速的数字化和互联系统的出现使得实时收集更多的数据并立即对这些数据采取行动成为可能,例如根据变化的条件进行调整或个性化用户体验。在线零售商能够在用户浏览其网站时向他们推荐产品;手机游戏可以向用户赠送虚拟礼物,让他们留在游戏中,或者在适当的时候提供游戏内购买;制造商可以监视机器的行为并触发维护操作以减少生产中断。所有这些用例都需要收集实时数据,以低延迟分析数据,并立即对结果做出响应。传统的面向批处理的体系结构不能处理这样的用例。

        您可能不会惊讶于有状态流处理是构建低延迟分析管道的正确技术。流分析应用程序不是等待定期触发,而是不断地接收事件流,并通过合并最新的低延迟事件来维护更新结果。这类似于数据库系统用于更新物化视图的视图维护技术。通常,流应用程序将其结果存储在支持有效更新的外部数据存储中,如数据库或键值存储。另外,Flink提供了一个称为可查询状态的特性,允许用户将应用程序的状态作为key查找表公开,并使外部应用程序可以访问它。流分析应用程序的实时更新结果可用于为仪表板应用程序提供动力,如图所示。

    图片

        除了将事件合并到分析结果中的时间更短之外,流分析应用程序还有一个不太明显的优势。传统的分析管道由几个独立的组件组成,比如ETL进程、存储系统,在基于hadoop的环境中,还包括一个数据处理引擎和调度器来触发作业或查询。这些组件需要精心编排,特别是错误处理和故障恢复可能会变得很有挑战性。

        相反,运行有状态流应用程序的流处理引擎负责所有处理步骤,包括事件加载、包括状态维护的连续计算和更新结果。此外,流处理引擎负责从故障中恢复,保证有且仅有一次的状态一致性,并且应该能够调整应用程序的并行性。支持流分析应用程序的额外要求是支持事件时间处理,以产生正确和确定的结果,以及在短时间内处理大量数据的能力,如高吞吐量。Flink为所有这些需求提供了很好的答案。

    流分析应用程序的典型用例是:

    • 监控手机网络的质量。

    • 分析移动应用程序中的用户行为。

    • 消费者技术中实时数据的在线分析。

    虽然在本书中没有涉及,但值得一提的是,Flink还提供了对流上的SQL分析查询的支持。许多公司已经建立了基于Flink SQL的流分析服务,既可以供内部使用,也可以公开提供给付费客户。

    开源流处理的演变

        数据流处理并不是一项新技术。最早的研究原型和商业产品可以追溯到20世纪90年代末。然而,流处理技术在最近的发展在很大程度上是由成熟的开源流处理引擎的可用性所驱动的。今天,分布式开源流处理引擎为不同行业的许多企业中的关键业务应用程序提供支持,如(在线)零售、社交媒体、电信、游戏和银行。开源软件是这一趋势的主要驱动力,主要有两个原因。

    1. 开源流处理软件是每个人都可以评估和使用的软件。

    2. 由于许多开源社区的努力,可伸缩流处理技术正在迅速成熟和快速发展

        仅Apache软件基金会就有十几个与流处理相关的项目。新的分布式流处理项目不断地进入开源阶段,并以新的特性和功能挑战着最先进的技术。这些新特性通常被早期的流处理引擎所采用。此外,开源软件的用户请求或贡献一些支持他们的用例所缺少的新特性。通过这种方式,开源社区不断地改进其项目的功能,并进一步推动流处理的技术边界。我们将简要回顾一下过去,看看开源流处理的起源和现状。

        第一代得到大量采用的分布式开源流处理引擎主要关注具有毫秒延迟的事件处理,并提供了在发生故障时事件不会丢失的保证。这些系统具有相当底层的api,并且没有为流应用程序的准确和一致的结果提供内置支持,因为结果取决于到达事件的时间和顺序。此外,即使事件在失败的情况下不会丢失,它们也可以被多次处理。与保证准确结果的批处理程序相比,第一代开源流处理引擎用结果的准确性换取了更好的延迟。数据处理系统可以提供快速或准确的结果,这形成了所谓Lambda架构的设计,如图所示。

    图片

        Lambda架构使用由低延迟流处理引擎支持的Speed Layer来增强传统的周期性批处理架构。到达Lambda架构的数据由流处理器接收,并写入批存储(如HDFS)。流处理器以接近实时的方式计算可能不准确的结果,并将结果写入一个速度表。写入批处理存储引擎的数据由批处理处理引擎定期处理。将准确的结果写入批处理表,并删除速度表中相应的不准确结果。应用程序通过合并来自speed表的最新但仅是近似的结果和来自batch表的较早但更准确的结果,以此使用来自服务层的结果。Lambda架构旨在改善原始批处理架构的结果延迟。然而,这种方法有几个明显的缺点。首先,对于两个具有不同api的独立处理系统,它需要两个在语义上等价的应用程序逻辑实现。其次,流处理器计算的最新结果不是精确的,而是近似的。第三,Lambda架构很难设置和维护。一个完整的配置包括一个流处理器、一个批处理处理器、一个快速存储、一个批处理存储,以及从批处理器获取数据和调度批处理作业的工具。

        下一代的分布式开源流处理引擎在第一代的基础上进行了改进,提供了更好的故障保证,并确保在出现故障时,每个记录只向结果输出一次。此外,编程api从底层的操作符接口发展到高级的具有更多内置原语的api。然而,一些改进,如更高的吞吐量和更好的故障保证,是以将处理延迟从毫秒增加到秒为代价的。此外,结果仍然依赖于时间和到达事件的顺序,即结果不仅取决于数据,还取决于外部条件,如硬件利用率。

        第三代分布式开源流处理引擎修正了结果对时间和到达事件顺序的依赖。结合严格的有且仅有一次的语义,这一代的系统是第一个能够计算一致和准确结果的开源流处理器。通过仅基于实际数据计算结果,这些系统还能够以与“实时”数据相同的方式处理历史数据,即数据一产生就被加载。另一个改进是消除了延迟和吞吐量的权衡。虽然以前的流处理器只能提供高吞吐量或低延迟,但是第三代的系统能够同时满足这两种情况。这一代的流处理器使得lambda架构过时了。

        除了目前讨论的系统特性,如容错、性能和结果精度之外,流处理引擎还不断添加新的操作特性。由于流处理应用程序通常需要在停机时间最少的情况下全天候运行,因此许多流处理引擎增加了一些特性,如高可用性设置、与资源管理器(如YARN或Mesos)的紧密集成,以及动态扩展流处理应用程序的能力。其他特性包括支持升级应用程序代码或将作业迁移到不同的集群或流处理引擎的新版本,而不会丢失应用程序的当前状态。

    Flink概览

        Apache Flink是第三代分布式流处理引擎,具有很好的性能,可以提供高吞吐量、低延迟的精确流处理。特别是以下特点,让它脱颖而出:

    • Flink支持事件时间和处理时间语义。尽管事件顺序混乱,但事件时间提供一致和准确的结果。处理时间适用于延迟要求非常低的应用程序。

    • Flink支持有且仅有一次的状态一致性保证。

    • Flink实现了毫秒级的延迟,能够每秒处理数百万个事件。Flink应用程序可以扩展到在数千个cores上运行。

    • Flink的特性是分层api,在表达性和易用性方面进行了不同的权衡。本书介绍了DataStream API和ProcessFunction,它们为常见的流处理操作(如窗口和异步操作)提供了基础,以及用于精确控制状态和时间的接口。本书不讨论Flink的关系API、SQL和linq样式的表API。

    • Flink提供了最常用的存储系统(如Apache Kafka、Apache Cassandra、Elasticsearch、JDBC、Kinesis和(分布式)文件系统(如HDFS和S3)的连接器。

    • 由于其高可用性设置(没有单点故障)、与YARN和Apache Mesos的紧密集成、从故障中快速恢复以及动态扩展作业的能力,Flink能够全天候运行流应用程序,停机时间很少。

    • Flink支持jobs的应用程序代码更新并将作业迁移到不同的Flink集群,而不会丢失应用程序的状态。

    • 详细、可定制的应用程序度量有助于提前识别问题并对问题做出反应。

    • 最后但并非最不重要的是,Flink也是一个成熟的批处理程序。

        除了这些特性之外,由于其易于使用的api, Flink是一个非常适合开发人员的框架。嵌入式执行模式以单个JVM进程的形式启动Flink应用程序,该JVM进程可用于在IDE中运行和调试Flink作业。这个特性在开发和测试Flink应用程序时非常方便。

        接下来,我们将引导您完成启动local集群和执行第一个流应用程序的过程,以便让您对Flink有一个初步印象。我们要运行应用程序转换和聚合随机产生的温度传感器读数的时间。为此,您的系统需要安装Java 8(或更高版本)。我们将描述UNIX环境的步骤。如果您正在运行Windows,我们建议您使用Linux虚拟机或者Cygwin(用于Windows的Linux环境)。

    1、访问Apache Flink的网页flink.apache.org,下载Apache Flink 1.4.0的无hadoop二进制发行版。

    2、解压文件

    tar xvfz flink-1.4.0-bin-scala_2.11.tgz

    3、启动一个本地Flink集群

    cd flink-1.4.0
    ./bin/start-cluster.sh

    4、通过在浏览器中输入URL http://localhost:8081打开web UI界面。如图1-8所示,您将看到关于刚刚启动的本地Flink集群的一些统计信息。它将显示单个任务管理器(Flink的工作进程)被连接,一个任务槽(任务管理器提供的资源单元)可用。

    图片

    5、下载包含本书所有示例程序的JAR文件。

    //注意:您也可以按照存储库的README文件上的步骤自己构建JAR文件。
    wget https://streaming-with-flink.github.io/examples/download/examples-scala.jar

    6、通过指定应用程序入口类和JAR文件,在本地集群上运行该示例

    ./bin/flink run -c io.github.streamingwithflink.AverageSensorReadings examples-scala.jar

    7、检查web仪表板。您应该看到在“运行作业”下列出的作业。如果单击该作业,您将看到关于正在运行的作业的操作符的数据流和实时指标,类似于图1-9中的屏幕截图。

    图片

    8、作业的输出被写入到标准的Flink工作进程中,该进程在默认情况下被重定向到./log文件夹中的一个文件中。例如,可以使用tail命令监视不断生成的输出,如下所示

    tail -f ./log/flink-<user>-jobmanager-<hostname>.out
    //您应该看到以下行被写入文件
    SensorReading(sensor_2,1480005737000,18.832819812267438)
    SensorReading(sensor_5,1480005737000,52.416477673987856)
    SensorReading(sensor_3,1480005737000,50.83979980099426)
    SensorReading(sensor_4,1480005737000,-17.783076985394775)

    输出可以这样读取:SensorReading的第一个字段是一个sensorId,第二个字段是1970-01-01-00:00以来的时间戳(以毫秒为单位),第三个字段是超过5秒的平均温度。

    9、由于您正在运行一个流应用程序,它将继续运行,直到您取消它。您可以通过在web仪表板中选择作业并单击页面顶部的CANCEL按钮来实现这一点。

    10、最后,应该停止本地Flink集群

    ./bin/stop-cluster.sh

    就是这样。您刚刚安装并启动了您的第一个本地Flink集群,并运行了您的第一个Flink DataStream程序!当然,关于Apache Flink的流处理还有很多要学习的,这就是本书的内容。

    你将在这本书中学到什么

    这本书将教你使用Apache Flink进行流处理的所有知识。第2章讨论了流处理的基本概念和挑战,第3章讨论了Flink的系统架构。第4章至第8章将指导您设置一个开发环境,介绍DataStream API的基础知识,并详细介绍Flink的时间语义和窗口操作符、它到外部系统的连接器以及Flink容错操作符状态的细节。第9章讨论了如何在各种环境中设置和配置Flink集群,最后第10章讨论了如何操作、监视和维护全天候运行的流应用程序。

  • 相关阅读:
    实现qsort(和qsort差一个数量级啊,伤自尊了)
    广度优先遍历目录(Windows平台、C++)
    在CentOS上以源码编译的方式安装Greenplum数据库
    Java泛型函数的运行时类型检查的问题
    Android代码的几点小技巧
    关于矢量图片资源向后兼容:CompatVectorFromResourcesEnabled标志的使用
    指定Android Studio编译工程时的源文件编码
    安卓日历同步的一些要点
    Android Studio编译错误:Unexpected lock protocol found in lock file. Expected 3, found 0.
    系统信息命令
  • 原文地址:https://www.cnblogs.com/lanblogs/p/15162663.html
Copyright © 2011-2022 走看看