From 6839d71ea8f2840a9f2e614f69894e51da17cdfb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:22:08 +0800 Subject: [PATCH 001/183] add OperatingSystem --- ...66\346\236\204\347\256\200\344\273\213.md" | 37 +++ ...ps\347\232\204\345\214\272\345\210\253.md" | 39 +++ ...15\344\275\234\347\263\273\347\273\237.md" | 287 +++++++++++++++++ .../2.\350\277\233\347\250\213.md" | 303 ++++++++++++++++++ ...05\345\255\230\347\256\241\347\220\206.md" | 277 ++++++++++++++++ .../4.\350\260\203\345\272\246.md" | 87 +++++ OperatingSystem/5.IO.md | 42 +++ ...07\344\273\266\347\256\241\347\220\206.md" | 67 ++++ ...45\345\274\217\347\263\273\347\273\237.md" | 27 ++ ...8.\350\231\232\346\213\237\346\234\272.md" | 134 ++++++++ 10 files changed, 1300 insertions(+) create mode 100644 "Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" create mode 100644 "OperatingSystem/2.\350\277\233\347\250\213.md" create mode 100644 "OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" create mode 100644 "OperatingSystem/4.\350\260\203\345\272\246.md" create mode 100644 OperatingSystem/5.IO.md create mode 100644 "OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" create mode 100644 "OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" create mode 100644 "OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" diff --git "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" new file mode 100644 index 00000000..f75f78d3 --- /dev/null +++ "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" @@ -0,0 +1,37 @@ +1.系统架构 +=== + +### 架构三要素 + +#### 构件 + +构件在软件领域是指可复用的模块,它可以是被封装的对象类、类树、一些功能模块、软件框架(framework)、软件架构(或体系结构Architectural)、文档、分析件、设计模式(Pattern)。但是,操作集合、过程、函数即使可以复用也不能成为一个构件。 + +##### 构件的属性: +1. 有用性(Usefulness):构件必须提供有用的功能。 +2. 可用性(Usability):构件必须易于理解和使用,可以正常运行。 +3. 质量(Quality):构件及其变形必须能正确工作,质量好坏与可用性相互补充。 +4. 适应性(Adaptability):构件应该易于通过参数化等方式再不同环境中进行配置,比较高端一点的复用性,接收外界各种入参,产生不同的结果,健壮性比较高。 +5. 可移植性(Portability):构件应能在不同的硬件运行平台和软件环境中工作,可移植性比较好,跨平台。 + + +#### 模式(Pattern) + +其实就是解决某一类问题的方法论,是生产经验和生活经验中经过抽象和升华提炼出来的核心知识体系。 模式就是一个完整的流程闭环,能够解决一些问题的通用方法(比如资本运作、玩家不同的需求等),软件中的模式大多源于生活,是人类智慧的结晶。 + +#### 规划 + +规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全民长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 + + +系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" index 90346bfb..417c7785 100644 --- "a/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/Http\344\270\216Https\347\232\204\345\214\272\345\210\253.md" @@ -9,8 +9,14 @@ HTTP与HTTPS的区别 - 1997年发布HTTP/1.1: 持久连接(长连接)、节约带宽、HOST域、管道机制、分块传输编码。 + 长连接是指的TCP连接,也就是说复用的是TCP连接。即长连接情况下,多个HTTP请求可以复用同一个TCP连接,这就节省了很多TCP连接建立和断开的消耗。 + + 此外,长连接并不是永久连接的。如果一段时间内(具体的时间长短,是可以在header当中进行设置的,也就是所谓的超时时间),这个连接没有HTTP请求发出的话,那么这个长连接就会被断掉。 + - 2015年发布HTTP/2:多路复用、服务器推送、头信息压缩、二进制协议等。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/http1.1vs2.jpg) 多路复用:通过单一的HTTP/2连接请求发起多重的请求-响应消息,多个请求stream共享一个TCP连接,实现多留并行而不是依赖建立多个TCP连接。 @@ -27,6 +33,25 @@ HTTP与HTTPS的区别 - 传输速度快 + + +### HTTP请求响应过程 + +你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 URL 地址为 `http://www.someSchool.edu/someDepartment/home.index`,当我们输入网址并点击回车时,浏览器内部会进行如下操作 + +- DNS服务器会首先进行域名的映射,找到访问`www.someSchool.edu`所在的地址,然后HTTP 客户端进程在 80 端口发起一个到服务器 `www.someSchool.edu` 的 TCP 连接(80 端口是 HTTP 的默认端口)。在客户和服务器进程中都会有一个`套接字`与其相连。 +- HTTP 客户端通过它的套接字向服务器发送一个 HTTP 请求报文。该报文中包含了路径 `someDepartment/home.index` 的资源,我们后面会详细讨论 HTTP 请求报文。 +- HTTP 服务器通过它的套接字接受该报文,进行请求的解析工作,并从其`存储器(RAM 或磁盘)`中检索出对象 [www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到](http://www.someSchool.edu/someDepartment/home.index,然后把检索出来的对象进行封装,封装到) HTTP 响应报文中,并通过套接字向客户进行发送。 +- HTTP 服务器随即通知 TCP 断开 TCP 连接,实际上是需要等到客户接受完响应报文后才会断开 TCP 连接。 +- HTTP 客户端接受完响应报文后,TCP 连接会关闭。HTTP 客户端从响应中提取出报文中是一个 HTML 响应文件,并检查该 HTML 文件,然后循环检查报文中其他内部对象。 +- 检查完成后,HTTP 客户端会把对应的资源通过显示器呈现给用户。 + +至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的`请求-响应`全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。 + + + + + ## HTTPS Https并非是应用层的一种新协议。只是http通信接口部分用SSL(安全套接字层)和TLS(安全传输层协议)代替而已。即添加了加密及认证机制的HTTP称为HTTPS(HTTP Secure). @@ -135,7 +160,21 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS +### Https 请求慢的解决办法 + +1. 不通过DNS解析,直接访问IP + +2. 解决连接无法复用 + + http/1.0协议头里可以设置Connection:Keep-Alive或者Connection:Close,选择是否允许在一定时间内复用连接(时间可由服务器控制)。但是这对App端的请求成效不大,因为App端的请求比较分散且时间跨度相对较大。 + + 方案1.基于tcp的长连接 (主要) 移动端建立一条自己的长链接通道,通道的实现是基于tcp协议。基于tcp的socket编程技术难度相对复杂很多,而且需要自己定制协议。但信息的上报和推送变得更及时,请求量爆发的时间点还能减轻服务器压力(避免频繁创建和销毁连接) + + 方案2.http long-polling 客户端在初始状态发送一个polling请求到服务器,服务器并不会马上返回业务数据,而是等待有新的业务数据产生的时候再返回,所以链接会一直被保持。一但结束当前连接,马上又会发送一个新的polling请求,如此反复,保证一个连接被保持。 存在问题: 1)增加了服务器的压力 2)网络环境复杂场景下,需要考虑怎么重建健康的连接通道 3)polling的方式稳定性不好 4)polling的response可能被中间代理cache住 …… + + 方案3.http streaming 和long-polling不同的是,streaming方式通过再server response的头部增加“Transfer Encoding:chuncked”来告诉客户端后续还有新的数据到来 存在问题: 1)有些代理服务器会等待服务器的response结束之后才将结果推送给请求客户端。streaming不会结束response 2)业务数据无法按照请求分割 …… + 方案4.web socket 和传统的tcp socket相似,基于tcp协议,提供双向的数据通道。它的优势是提供了message的概念,比基于字节流的tcp socket使用更简单。技术较新,不是所有浏览器都提供了支持。 diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" new file mode 100644 index 00000000..00696a2d --- /dev/null +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" @@ -0,0 +1,287 @@ + + +# 1.操作系统 + + + +操作系统(Operating System, 简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是一种由引导程序(bootloader)启动并管理计算机中所有程序生命周期的系统程序。任何其他软件都必须在操作系统的支持下才能运行,操作系统能有效组织和管理系统中的各种软、硬件资源,合理组织计算机系统的工作流程并控制程序的执行,为用户提供一个良好的操作环境。 目前比较为人所知的操作系统有Microsoft的Windows系统、Apple的Mac及以Linux为内核的各种Linux发行版(Centos/Ubuntu等)。 现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口及各种输入\输出设备构成。 + +作用: 它可以帮我们管理计算机的各种资源,协助我们完成各种复杂繁琐的任务。 + + + +### 以现代标准而言,一个标准PC的操作系统应该提供以下功能 + + + +### 用户界面(User interface) + + + +普通用户操作电脑是需要用户界面的,没有用户界面的电脑对于普通用户来说就是灾难。你能想象家里的老人或者上了年纪的人用电脑却没有鼠标的场景吗?你能想象使用黑框框来做 PPT 吗?你能想象使用黑框框来浏览网页吗? +专业的 IT 工作者有时候会使用黑框框纯粹是工作需要,在有些场景下,黑框框比用户界面更有效率一些。而类似于服务器场景的开发工作基本上是没有用户界面的,当然一直强调的是专业场景,这个世界上能流畅使用黑框框的人占总人口的比例太少太少。 +用户界面这项伟大的发明诞生于施乐公司,经乔布斯和比尔盖茨商业化运作后得以让世人发现它的伟大之处。用户界面的发明就相当于简体汉字的出现一般,简体汉字的推行让中国的文盲率大幅降低,而用户界面的发明则大幅降低了计算机的使用难度,所以你很难想象现代的操作系统没有用户界面。 + + + +#### 进程管理(Processing management) + + + +先来解释一下什么是进程。进程是计算机进行资源管理以及调度的基本单位,是程序的执行实体。这种说法比较正统,或者你可以将一个进程看成是计算机正在进行的一项任务。计算机里面有很多各式各样功能的进程,那么多进程如何比较好的运行是个深奥的学问。 +你可以想象一下:你打开了微信、word 文档、音乐软件,你想一边跟同事进行交流文档的内容应该如何写,一边在 word 文档敲下你的构思,然后在这个过程中你还听着音乐。在这个过程中,计算机需要将微信的网络保持连接,持续收发微信的信息,随时保存你的 word 文档到磁盘上,解码音乐流并播放出来。这一系列程序运转如何能让你产生一个假象:你以为它们是在同时运行的,但其实它们在同一时刻只有一个在运行。这就是进程管理和调度。 + + + +#### 内存管理(Memory management) + +内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是成为虚拟内存,但是完全是两个不同方向的技术。 +进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 +进程退出销毁时,内存的回收也是很重要的,否则很容易就会产生内存溢出,占着茅坑不拉屎,导致其他的进程都憋死。 + + + +#### 文件系统(File system) + + + +文件系统与用户的距离很近,每个人平常在使用计算机的时候或多或少都会留下一些数据,而这些数据通常会保留在磁盘里面。磁盘如果不进行格式化的话,普通人是没法使用的。磁盘里面其实就是一些布满磁性物质的盘片,在计算机的世界里数据是 0 和 1 组成的,那对应的在磁盘里面就是磁性的正负极,也就是说计算机的一个文本数据要保存到磁盘中,那就需要将文本数据的电气化信号 0 和 1 通过磁盘翻译为磁性正负极并保存起来。 +磁盘格式化的过程就是将文件系统架设到磁盘上,这样可以更好的管理磁盘的数据。你可以将磁盘未格式化之前的数据看做是一堆杂乱无章散落在地上的书,而文件系统就是一个有编排顺序的书架,格式化的过程就是将这堆书一本本按编排顺序放到书架上。这个比喻不太恰当,因为格化式操作通常来说会清掉数据,就相当于将书里面的字都清掉了,放到书架上的书里面都是空白页,所以格式化的时候请谨慎。 + + + +#### 网络通信(Networking) + + + +我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 +整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。由于网络这块内容实在是太复杂,后面我会另开一个专栏进行讲解。 + + + +#### 设备管理 + + + +计算机上有很多设备,比如CPU、内存、网卡、声卡、显卡、硬盘等。那什么是设备管理? +在计算机中除了 CPU 和内存,对于其他一切输入输出设备的管理统称为设备管理。 +计算机中的设备分为输入和输出设备。以 CPU 为中心,凡是向 CPU 输送数据的设备统称为输入设备,例如鼠标、键盘、摄像头等;同样以 CPU 为中心,凡是从 CPU 获取数据的设备统称为输出设备,如显示器等。有些设备既是输入设备也是输入设备,比如网卡等。 +一个比较常见的场景是当我们将 U盘插进电脑的 USB 插孔时,电脑能实时识别出 U 盘设备,那计算机为啥能实时识别出这些设备呢?之后会有章节讨论一下这个话题。 + + + +## 计算机的组成 + + + +计算机由处理器、存储器和输入\输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: + +- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) +- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也成为实存储器(real memory)或主存储器(primary memory)。 +- 输入\输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 +- 系统总线(System bus)在处理器、内存和输入\输出模块间提供通信的设施。 + + + +处理器的一种功能是与存储器叫唤数据。为此,它通常使用两个内部寄存器: + +- 存储器地址寄存器(Memory Address Register, MAR),用于确定下一次读/写的存储器地址。 +- 存储器缓冲寄存器(Memory Buffer Register,MBR),存放要写入存储器的数据或从存储器中读取的数据。 + +同理,输入/输出地址存储器(I/O Address Register,简称I/O AR或I/O地址寄存器)用于确定一个特定的输入/输出设备,输入/输出缓冲寄存器(I/O Buffer Register,简称I/O BR或I/O缓冲寄存器)用于在输入/输出模块和处理器间交换数据。 + + + +内存模块由一组单元组成,这些单元由顺序编号的地址定义。每个单元包含一个二进制数,它可解释为一个指令或数据。输入/输出模块在外部设备与处理器和存储器之间传送数据。输入/输出模块包含内存缓冲区,用于临时保存数据,直到它们被发送出去。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/computer_cpu_memory_io.png?raw=true) + + + +### 计算机打开电源后的执行 + +在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常相应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 + +然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 + +1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 +2. 寻址0xFFFF0(ROM BIOS营社区) +3. 检查RAM、键盘、显示器、磁盘、主板等硬件 +4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 +5. 设置 cs=0x07c0,ip=0x0000开始执行 + + + +## CPU(Central Processing Unit) + +CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。CPU(中央处理器)是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。其实在现在一些CPU中,一级缓存也会分为一级数据缓存和一级指令缓存。 + +由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些`寄存器`来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。 + + + +**程序是把寄存器作为对象来描述的** + +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 + +**累加寄存器:**存储执行运算的数据和运算后的数据。 + +**标志寄存器:**存储运算处理后的CPU的状态。 + +**程序计数器:**存储下一条指令所在内存的地址。 + +**基址寄存器:**存储数据内存的起始地址。 + +**变址寄存器:**存储基址寄存器的相对地址。 + +**通用寄存器:**存储任意数据。 + +**指令寄存器:**存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 + +**栈寄存器:**存储栈区域的起始地址。 + +其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 + + + +**计算机执行的原理是: 取指执行 ** + +处理器执行的程序是由一组保存在存储器中的指令组成的。最简单的指令处理包括两步: 处理器从存储器中一次读(取)一条指令,然后执行每条指令。程序执行是由不断重复的取指令和执行指令的过程组成的。指令执行可能涉及很多操作,具体取决于指令本身。 + +在典型的处理器中,程序计数器(Program Counter, PC)保存下一次要取的指令地址。除非出现其它情况,否则处理器在每次取值令后总是递增PC,以便能按顺序取下一条指令(即位于下一个存储器地址的指令)。取到的指令放在处理器的一个寄存器中,这个寄存器称为指令寄存器(Instruction Register,IR)。指令中包含确定处理器将要执行的操作的位,处理器解释指令并执行对应的操作。大体上,这些动作可分为4类: + +- 处理器-存储器:数据可以从处理器传送到存储器,或从存储器传送到处理器。 +- 处理器-I/O:通过处理器和I/O模块间的数据传送,数据可以输出到外部设备,或从外部设备向处理器输入数据。 +- 数据处理:处理器可以执行很多与数据相关的算术操作或逻辑操作。 +- 控制: 某些指令可以改变执行顺序。例如,处理器从地址为149的存储单元中取出一条指令,该指令指向下一条指令应该从地址为182的存储单元中取,这样处理器就会把程序计数器置为182.因此在下一个取指阶段,将从地址182的存储单元而非150的存储单元中取指令。 + + + +## CPU 指令执行过程 + +那么 CPU 是如何执行一条条的指令的呢? + +几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 + +- `取指令`阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- `指令译码`阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 +- `执行指令`阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 +- `访问取数`阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 +- `结果写回`阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; + + + +## 存储器 + +在任何一种计算机中,第二种主要部件都是存储器。在理想情况下,存储器应该极为迅速(快于执行一条指令,这样CPU就不会受到存储器的限制),充分大并且非常便宜。但是目前的技术无法同时满足这三个目标。 + +- 存取时间越快,每“位”的价格越高 +- 容量越大,每“位”的价格越低 +- 容量越大,存取速度越慢 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/computer_storage_type.png?raw=true) + +于是出现了多级存储器组织结构: + +- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用预CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32*32位,而在64位CPU中为64*64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 + +- 高速缓存,它多数由硬件控制。 + +- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是告诉的、容量较小的告诉缓存的扩展。 + +- 磁盘:磁盘同RAM相比,成本降低了,但是随机访问数据时间也慢了。其低速的原因是因为磁盘是一种机械装置。一个磁盘中有一个或多个金属盘片,他们以一定的速度旋转,从边缘开始有一个机械臂悬横在盘面上,这类似于老式播放塑料唱片的唱片机。 + + + + ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/changpianji.jpg?raw=true) + + 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 + +这里要说一下闪存(flash memory),在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + + + + + +## 内存 + + + +内存包括主存(内存条,基于DRAM(动态RAM))与高速缓存(Cache,基于SRAM(静态RAM,静态RAM速度很快但是成本很高,所以用于在CPU内部充当缓冲))两部分。可能是由于Cache相较内存条容量很小,毕竟内存容量只计内存条大小,加上重要性也不及内存条,一般人或许不知道Cache,所以就忽略了高速缓存Cache,直接将主存--内存条等同了内存吧。计算器内存条采用的是DRAM(动态随机存储器),即计算机的主存。通常所说的内存容量即指内存条DRAM的大小。 + +高速缓冲存储器Cache主要是为了解决CPU和主存速度不匹配而设计的。Cache一般由SRAM(静态随机存储器)芯片实现,它的存取速度接近CPU,快于DRAM,存储容量小于DRAM。它比主存的优先级高,CPU存取信息时优先访问Cache,找不到的话再去主存DRAM中找,同时把信息周围的数据块从主存复制到Cache中。 现代计算机系统基本都采用Cache-主存-辅存(即外存储器)三级存储系统。其中CPU可直接访问Cache和主存,辅存则通过主存与CPU交换信息。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_type.png?raw=true) + + + +### 主存的分类 + +- RAM(Random-access memory) + + 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”,因为CPU工作的速度比RAM的读写速度快,所以CPU读写RAM时需要花费时间等待,这样就使CPU的工作速度下降。人们为了提高CPU读写程序和数据的速度,在RAM和CPU之间增加了高速缓存(Cache)部件。Cache的内容是随机存储器(RAM)中部分存储单元内容的副本 + +- ROM(Read-Only Memory) + + 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变 + + + +### 缓存 + +位于CPU与内存之间的临时存储器,它的容量比内存小的多但是交换速率却比内存要快得多。缓存的出现主要是为了解决CPU运算速率与内存读写速率不匹配的矛盾,缓存往往使用的是RAM,L1 Cache(一级缓存)是CPU第一层高速缓存,一般L1缓存的容量通常在32—256KB,L1分为数据Cache,指令Cache,L2 Cache(二级缓存)是CPU的第二层高速缓存,分内部和外部两种芯片,内部的芯片二级缓存运行速率与主频相同,而外部的二级缓存则只有主频的一半,缓存只是内存中少部分数据的复制品。二级缓存是比一级缓存速率更慢,容量更大的内存,主要就是做一级缓存和内存之间数据临时交换的地方用。为了适应速率更快的处理器。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_cache_memory.png?raw=true) + + + +缓存基本上都是采用SRAM存储器,SRAM是英文Static RAM的缩写,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 + + + +缓存的工作原理是当CPU要读取一个数据时,首先从CPU缓存中查找,找到就立即读取并送给CPU处理;没有找到,就从速率相对较慢的内存中读取并送给CPU处理,同时把这个数据所在的数据块调入缓存中,可以使得以后对整块数据的读取都从缓存中进行,不必再调用内存。正是这样的读取机制使CPU读取缓存的命中率非常高,一般把静态RAM缓存叫一级缓存,而把后来增加的动态RAM叫二级缓存。 + + + + + + + +## 系统软件和应用软件 + + + +- 系统软件是指控制和协调计算机以及外部设备,支持应用软件开发和运行的系统,是无需用户干预的各种程序的集合。主要功能是调度、监控和维护计算机系统。例如: 操作系统和一系列的基本工具(编译器、数据库管理、存储器格式化、用户身份验证、网络连接)。 +- 应用软件是和系统软件相对的,是用户可以使用的各种程序设计语言,以及用各种程序设计语言编译的应用程序的集合,分为应用软件包和用户程序。例如:互联网软件、多媒体软件、协作软件等。 + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/2.\350\277\233\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213.md" new file mode 100644 index 00000000..cad290fc --- /dev/null +++ "b/OperatingSystem/2.\350\277\233\347\250\213.md" @@ -0,0 +1,303 @@ +# 1.进程 + + + +狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 + +广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 + +进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。 + + + +进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 [2] + +进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的[代码](https://baike.baidu.com/item/代码),还包括当前的活动,通过[程序计数器](https://baike.baidu.com/item/程序计数器)的值和处理[寄存器](https://baike.baidu.com/item/寄存器)的内容来表示。 + +进程由三部分组成: + +- 一段可执行的程序 +- 程序所需要的相关数据(变量、工作空间、缓冲区等) +- 程序的执行上下文 + +最后一部分是根本。执行上下文(execution context)又称为进程状态(process state),是操作系统用来管理和控制进程所需的内部数据。这种内部信息和进程是分开的,因为操作系统信息不允许被进程直接访问。上下文包括操作系统管理进程及处理器正确执行进程所需的所有信息,包括各种处理器寄存器的内容,如程序计数器和数据寄存器。它还包括操作系统使用的信息,如进程优先级及进程是否在等待I/O事件的完成。 + + + +在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 + +## 多道程序设计模型 + +采用多道程序设计可以提高CPU利用率。严格地说,如果进程用于计算的平均时间是进程在内存中停留时间的20%,且内存中同时有5个进程,则CPU将一直满负载运行。然而,这个模型在现实中过于乐观,因为它假设这5个进程不会同时等待I/O。 + +更好的模型是从概率的角度来看CPU的利用率。假设一个进程等待I/O操作的时间与其停留在内存的时间比为p。当内存中同时有n个进程时,则所有n个进程都在等待I/O(此时CPU空转)的概率为p的n次方。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/duodao_module.png?raw=true) + +从上图中可以看到,如果进程花费80%的时间等待I/O,为使CPU的浪费低于10%,至少要有10个进程同时在内存中。 + + + +## 进程的特性 + +1. 动态性 + + 动态性是进程的最基本特征,它是程序执行过程,它是有一定的生命期。它由创建而产生、由调度而执行,因得不到资源而暂停,并由撤消而死亡。而程序是静态的,它是存放在介质上一组有序指令的集合,无运动的含义。 + +2. 并发性 + + 并发性是进程的重要特征,同时也是OS的重要特征。并发性指多个进程实体同存于内存中,能在一段时间内同时运行。而程序是不能并发执行。 + +3. 独立性 + + 进程是一个能独立运行的基本单位,即是一个独立获得资源和独立调度的单位,而程序不作为独立单位参加运行。 + +4. 异步性 + + 进程按各自独立的不可预知的速度向前推进,即进程按异步方式进行,正是这一特征,将导致程序执行的不可再现性,因此OS必须采用某种措施来限制各进程推进序列以保证各程序间正常协调运行。 + + + +进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 + +## 进程的状态 + + + +- 就绪态:进程做好了准备,只要有机会就开始执行 +- 运行态:进程正在执行 +- 堵塞/等待态:进程在某些事件发生前补鞥呢执行,如I/O操作完成 +- 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 +- 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 + +### 进程由哪几部分组成? + +进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: + +- 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 +- 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 +- 进程控制块:记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。 + +##### 进程控制块 + +进程执行的任意时刻,都可由如下元素来表征: + +- 标识符:与进程相关的唯一标识符,用来区分其他进程 +- 状态:若进程正在执行,则进程处于运行态 +- 优先级:相对于其他进程的优先顺序 +- 程序计数器:程序中即将执行的下一条指令的地址 +- 内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享内存块的指针 +- 上下文数据:进程执行时处理器的寄存器中的数据 +- I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 +- 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 + +上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。进程控制块(PCB)是进程实体的重要组成部分,它记录了操作系统所需要的、用于描述进程情况及控制进程所需要的全部信息。原来不能独立运行的程序或数据,通过 PCB 就可以成为一个可以独立运行的基本单位。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个 PCB;当进程结束时,系统又收回其 PCB,该进程也随之消亡。简单的总结就是进程控制块主要包括下述四个方面的信息: + +1. 进程标识符信息:用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 + +2. 进程调度信息:用于描述与进程调度有关的状态信息,包括进程状态、进程优先权、调度信息和等待事件等。进程状态指明进程当前的状态,作为进程调度和对换时的依据;进程优先权说明进程使用 CPU 的优先级别,其中优先权高的进程将优先获得 CPU;调度信息描述与进程调度算法相关的信息,如进程等待时间、已运行的时间等;等待事件是指进程由运行态转变为阻塞态时所等待发生的事件。 + +3. CPU 状态信息:用于保留进程运行时 CPU 的各种信息,使得进程暂停运行后,下次重新运行时能从上次停止的地方继续运行。CPU 状态信息通常包含通用寄存器、控制和状态寄存器、用户栈指针等。CPU 状态字记载了程序执行的状态信息,如条件码、外中断屏蔽标识、执行状态(核心态或用户态)标识等。 + +4. 进程控制信息(process control information):包括进程资源、控制机制等一些进程运行时所需要的信息,如: + - 程序和数据地址:该进程的程序和数据所在的内存和外存地址,以便该进程在次运行时,能够找到程序和数据。 + - 进程同步和通信机制:实现进程同步和通信时所采用的机制,如消息队列指针、信号量等。 + - 资源清单:除 CPU 外,进程所需的全部资源和已经分配到的资源。 + - 链接指针:用于指向该进程所在队列的下一个进程的 PCB 首地址。 + + + +## 进程创建 + +操作系统决定创建一个新进程时,会按如下步骤操作: + +1. 为新进程分配一个唯一的进程标识符。此时,主进程表中会添加一个新表项,每个进程一个表项。 +2. 为进程分配空间。这包括进程映像中的所有元素。因此,操作系统必须知道私有用户地址空间(程序和数据)和用户栈需要多少空间。 +3. 初始化进程控制块。进程表示部分包括进程id和其他相关的id,如父进程id等。处理器状态信息部分的多数项目通常初始化为0,但程序计数器(置为程序入口点)和系统栈指针(定义进程栈边界)除外。进程控制信息部分根据标准的默认值和该进程请求的特性来初始化。例如,进程的状态通常初始化为就绪或就绪/挂起。 +4. 设置正确的链接。例如,若操作系统将每个调度队列都维护为一个链表,则新进程必须放在继续或就绪/挂起链表中。 +5. 创建或扩充其他数据结构。例如,操作系统可因编制账单和/或评估性能,为每个进程维护一个记账文件。 + + + +## 进程切换 + + + +表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 + + + +进程切换可在操作系统从当前正运行进程中获得控制权的任何时刻发生。首先考虑系统终端。实际上,大多数操作系统都会区分两种系统终端:一种称为终端,另一种称为陷阱。前者与当前正运行进程无关的某种外部事件相关,如完成一次I/O操作。后者与当前正运行进程产生的错误或异常条件相关,如非法的文件访问。对于普通终端,控制权首先转给终端处理器,终端处理器完成一些基本的辅助工作后,再将控制权给与已发生的特定中断相关的操作系统进程。示例如下: + +- 时钟中断:操作系统确定当前正运行进程的执行时间是否已超过最大允许时间段(时间片,即进程中断前可以执行的最大时间段)。若超过,进程就切换到就绪态,并调入另一个进程。 +- I/O中断:操作系统确定是否已发生I/O活动。若I/O活动是一个或多个进程正在等待的事件,则操作系统就把所处于堵塞态的进程转换为就绪态( 堵塞/挂起态进程转换为就绪/挂起态)。操作系统必须决定是继续执行当前处于运行态的进程,还是让具有高优先级的就绪态进程抢占这个进程。 +- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外村中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。期望的块调入内存后,该进程置为就绪态。 + +对于陷阱(trap),操作系统则确定错误或异常条件是否致命。致命时,当前正运行进程置为退出态,并切换进程。不致命时,操作系统的动作将取决于错误的性质和操作系统的设计,操作系统可能会尝试恢复程序,或简单的通知用户。操作系统可能会切换进程或继续当前运行的进程。 + + + +## 并发 + +在单处理器多道程序设计系统中,进程会被交替的执行,因而表现出一种并发执行的外部特征。即使不能实现真正的并行处理,并且在进程间来回切换也需要一定的开销,但是交替执行在处理效率和程序结构上还是会带来很多好处。在多处理系统中,不仅可以交替的执行进程,而且可以重叠执行进程。 + +进程的相对执行速度不可预测,它取决于其他进程的活动、操作系统处理中断的方式以及操作系统的调度策略,这样就会有以下问题: + +1. 全局资源的共享充满了危险。例如,如果两个进程都使用同一个全局变量,并且都对该变量执行读写操作,那么不同的读写执行顺序是非常关键的。 +2. 操作系统很难对资源进行最优化分配。例如,进程A可能请求使用一个特定的I/O通道,并获取控制权,但它在使用这个通道前已被堵塞,而操作系统仍然锁定这个通道以防止其他进程使用,这是最难以令人满意的。事实上,这种情况有可能导致死锁。 +3. 定位程序设计错误非常困难。这是因为结果通常是不确定的和不可再现的。 + +所以由于并发带来的这些问题,操作系统必须关注的问题如下: + +1. 操作系统必须能够跟踪不同的进程,这可以使用进程控制块来实现。 +2. 操作系统必须为每个活动进程分配和释放各种资源。 +3. 操作系统必须保护每个进程的数据和物理资源,避免其他进程的无意干扰。 +4. 一个进程的功能和输出结果必须与执行速度无关。 + + + +### 互斥的要求 + +1. 必须强制实施互斥。在于相同资源或共享对象的临界区有关的所有进程中,一次只允许一个进程进入临界区。 +2. 一个在非临界区停止的进程不能干啥其他进程。 +3. 绝不允许出现需要访问临界区的进程被无限延迟的情况,即不会死锁或饥饿。 +4. 没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入。 +5. 对相关进程的执行速度和处理器的数量没有任何要求和限制。 +6. 一个进程驻留在临界区中的时间必须是有限的。 + + + +## 进程间通信(Inter Process Communication,IPC) + + + +进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 + + + +高级通信方式 + +- 共享存储系统 + +多台服务器访问同一个存储设备的同一分区 + +- 消息传递系统 + +进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 + +- 管道通信 + +发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + +管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + + + +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 + + + +### 进程的实现 + +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 + + + +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 + + + +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 + + + +## 进程调度 + +进程调度就是处理器调度(上下文切换) + +### 调度级别 + +- 高级调度 + +作业调度,把后备作业调入内存运行 + +- 中级调度 + +在虚拟存储器中引入,在内,外存交换区进行进程对换 + +- 低级调度 + +进程调度,把就绪队列里的某个进程获得CPU执行权 + +### 调度方式 + +- 可剥夺 + + 当一个进程运行时,基于某种原则,剥夺已经分配给它的处理器,将之分配给其他进程,原则有:优先权原则,短进程优先原则,时间片原则。 + +- 不可剥夺 + +一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 + +### 调度算法 + +- 先进先出 + +按照进入就绪队列的进程顺序,不加其他条件干涉 + +- 短进程优先 + +优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 +平均周转时间:16,平均周转时间越小,调度性能越好 + +- 轮转法 + - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** + - 多级队列: 让系统中所有进程分成若干类,每类一级 + +## 死锁 + +两个以上的进程相互请求对方已经占有的资源,导致无限期的等待。 + +### 产生条件 + +- 互斥条件 +- 请求保持 +- 不可剥夺 +- 环路条件 + +### 解决方法 + +- 鸵鸟策略:不理睬 +- 预防策略:破坏产生条件中任意一个 +- 避免策略: 精心分配资源,动态避免死锁 +- 检测与解除死锁:系统自动检测,并且解除 + + + + + + + +## 进程和线程的区别 + +线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 + + + +进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。 + +1. 简而言之,一个程序至少有一个进程,一个进程至少有一个线程。 +2. 线程的划分尺度小于进程,使得多线程程序的并发性高。 +3. 另外,进程在执行过程中拥有独立的内存单元,而多个线程共享内存,从而极大地提高了程序的运行效率。 +4. 线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。 +5. 从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" new file mode 100644 index 00000000..a4b40e39 --- /dev/null +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -0,0 +1,277 @@ +# 3. 内存管理 + +常用概念: + +- 页框:内存中固定长度的快 +- 页:固定长度的数据库,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 +- 段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合) + + + +内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: +1、内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 +2、地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 +3、内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 +4、存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 + + + +进程对应的内存空间中所包含的5种不同的数据区: + +- 代码段(code segment):又称文本段,用来存放指令,运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 + +- 数据段(data segment): 存储初始化的全局变量和初始化的static变量。数据段中的数据的生存期是随程序持续性(随进程持续性):进程创建就存在,进程死亡就消失。 + +- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0. +- rodata段:只读数据 比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如,全局作用域中的 const int ival = 10,ival 存放在 .rodata 段;再如,函数局部作用域中的 printf("Hello world %d\n", c); 语句中的格式字符串 "Hello world %d\n",也存放在 .rodata 段。 + +- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) + +- 栈(stack):栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束就自动回收空间。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 + +上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。 + + + +## 内存的演变 + +在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于 ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch 时,之前内存里的内容怎么办?简单粗暴的方式就是先 dump 到磁盘上,然后再从磁盘上 restore 之前 dump 的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如 code / stack / heap。一个 16 KB 的地址空间可能长这样: + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) + +Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,进入虚拟内存。 + + + +Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。 + +在讨论进程空间细节前,这里先要澄清下面几个问题: + +l 第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 + +l 第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 + +l 第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 + + + +![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9pbWFnZXMyMDE3LmNuYmxvZ3MuY29tL2Jsb2cvMTMxMTQxMi8yMDE4MDIvMTMxMTQxMi0yMDE4MDIxNTEyMjY1NDM1OS0xNDcxMTE4NzM4LnBuZw?x-oss-process=image/format,png) + + + +### 固定分区 + + + +大多数内存管理方案都假定操作系统占据内存中的某些固定部分,而内存中的其余部分则供多个用户进程使用。管理用户内存空间的最简单的方案就是对它分区,以形成若干边界固定的区域。 + +固定分区分为两种: + +- 使用大小相等的分区:此时小于等于分区大小的任何进程都可以装入任何可用的分区中。若所有分区都已满且没有进程处于就绪态或运行态,则操作系统可以换出一个进程的所有分区,并装入另一个进程,使得处理器有事可做。但是大小相等的分区有两个难点: + + - 程序可能太大而不能放到一个分区中。此时程序员必须使用覆盖技术设计程序,使得任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。 + + - 内存的利用率非常低。任何程序,急事很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 + +- 使用大小不等的分区:大小不等的分区可以缓解上面的两个问题,但是不能完全解决。 + +虽然大小不等的分区带来了一定的灵活性,但是分区的数量在系统生成阶段已经确定,因而限制了系统中活动(未挂起)进程的数量。而且由于分区大小是在系统生成阶段事先设置的,因而小作业不能有效的利用分区空间。 + + + +### 动态分区 + +为了克服固定分区的确定,提出了动态分区。对于动态分区,分区长度和数量是可变的。程序装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。但是对于一个64MB的内存,如果装入前三个进程已经占用了很大一部分,剩下的部分对于第四个进程来说又太小,这样在内存的末尾就剩下一个“空洞”。而等第二个进程结束后腾出的足够的空间来装入第四个进程,但是由于第四个进程比第二个进程小,所有这里又形成了另一个小"空洞"。这样最终在内存中就会形成许多小空洞。随着时间的推移,内存中形成了越来越多的碎片,内存的利用率随之下降。这种现象称为外部碎片(external fragmentation),指在所有分区外的存储空间变成了越来越多的碎片,这与前面所讲的内部碎片正好对应。 + +客服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 + + + +### 伙伴系统 + +固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 + + + +### 重定位 + + + +在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的摸个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: + +- 逻辑地址(logical address)是指与当前数据再内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 +- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 +- 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 + +系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。通常情况下,被加载进程中所有内存访问都相对于程序的开始点。因此,在执行包括这类访问的指令时,需要有把相对地址转换为物理内存地址的硬件机制。这类地址转换就需要一个特殊的处理器寄存器(基址寄存器),其内容是程序在内存中的起始地址。还有一个界限寄存器指明程序的终止位置。 + + + +### 分页 + +大小不等的固定分区和大小可变的分区技术在内存的使用上都是低效的,前者会产生内部碎片,后者会产生外部碎片。但是,如果内存被划分成大小固定、相等的块,切块相对比较小,每个进程也被分成同样大小的小块,那么进程中称为页的块可以分配到内存中称为页框的可用块。这样在使用分页技术时,每个进程在内存中浪费的空间,仅仅是进程最后一页的一小部分形成的内部碎片。没有任何外部碎片。 + +这样操作系统需要为每个进程维护一个页表(page table)。页表给出了该进程的每页所对应页框的位置。在程序中每个逻辑地址包括一个页号和该页中的偏移量。在分页中,逻辑地址到物理地址的转换仍然由处理器硬件完成,给出逻辑地址(页号,偏移量)后,处理器使用页表产生物理地址(页框号,偏移量)。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/page_memory_1.png?raw=true) + +采用分页技术的分区相当小,一个程序可以占据多个分区,并且这些分区不需要是连续的。 + +为了使分页方案更加方便,规定页和页框的大小必须是2的幂,以便容易的表示出相对地址。 + + + +### 分段 + +细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管短有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 + +由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 + +采用大小不等的段的另一个结果是,逻辑地址和物理地址间不再是简单的对应关系。类似于分页,在简单的分段方案中,每个进程都有一个段表,系统也会维护一个内存中的空闲块列表。每个段表都必须给出相应段在内存中的起始地址,还必须指明段的长度,以确保不会使用无效地址。 + + + + + +分页和分段的两个特点: + +- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 +- 一个进程可划分为许多块(页和段),在执行过程中,这些块不需要连续的位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能。 + +由于上面这两个特点的存在,那么在一个进程的执行过程中,该进程不需要所有页或段都在内存中。如果内存中保存有待取的下一条指令所在块(段或页)及待访问的下一个数据单元所在块,那么执行至少可以暂时继续下去。我们用术语“块”来表示页或段。处理器在需要访问一个不在内存中的逻辑地址时,会产生一个中断,这表明出现了内存访问故障。操作系统会把被中断的进程置于堵塞态。要继续执行这个进程,操作系统必须把包含引发访问故障的逻辑地址的进程块读入内存。为此操作系统产生一个磁盘I/O读请求。产生I/O请求后,在执行磁盘I/O期间,操作系统可以调度另一个进程运行。在需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统则把由于缺少该块而被堵塞的进程置为就绪态。这样的话就会有两种提高系统利用率的方法: + +- 在内存中保留多个进程。由于对任何特定的进程都仅装入它的某些块,因此有足够的空间来放置更多的进程。这样,在任何时刻这些进程中至少有一个处于就绪态,于是处理器就得到了更有效的利用。 +- 进程可以比内存的全部空间还大。程序占用的内存空间的大小是程序设计的最大限制之一。没有这种方案时,程序员必须清楚的知道有多少内存空间可用。若编写的程序太大,程序员就必须设计出能把程序分成块的方法,这些块可按某种覆盖策略分别加载。通过基于分页或分段的虚拟内存,这项工作可由操作系统和硬件完成。对程序员而言,他所处理的是一个巨大的内存,大小与磁盘存储器有关。操作系统在需要时会自动的把进程块装入内存。 + +由于进程只能在内存中执行,因此这个存储器成为实存储器(real memory),简称实存。但程序员或用户感觉到的是一个更大的内存,且通常分配在磁盘上,这称为虚拟内存(virtual memory),简称虚存。虚存支持更有效的系统并发度,并能解除用户与内存之间没有必要的紧密约束。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/page_vs_fragment.png?raw=true) + + + + + +## 虚拟内存 + +面对越来越大的程序,常常产生程序>内存的问题,为解决这种问题,虚拟内存的概念得到普及. + +要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 + +虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 + +**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个 块,每个块被成为一页或页面. + +**程序运行时,并不是所有页都在物理内存中**: + +- 当程序引用一部分在物理内存的地址空间时,由硬件直接执行必要的映射; +- 当程序引用一部分不在物理内存的地址空间时,有操作系统将缺失的页装入物理内存,并重新运行 + +虚拟内存基于分段和分页这两种基本技术或基于这两种技术中的一种。虚拟内存机制允许程序以逻辑方式访问存储器,而不考虑物理内存上可用的空间数量。虚存的构想是为了满足有多个用户作业同时驻留在内存中的要求,因此在一个进程被写出到副主存储器中且后续进程被读入时,连续的进程执行之间将不会脱节。进程大小不同时,若处理器在很多进程间切换,则很难把它们紧密的压入内存,因此人们引入了分页系统。在分页系统中,进程由许多固定大小的块组成。这些块成为页。程序通过虚地址(virtual address)访问,虚地址由页号和页中的偏移量组成。进程的每页都可置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址(real address)或物理地址之间的动态映射。 + + + +### 分页访问过程: + +1. CPU中包含**MMU内存管理单元**,用于管理虚拟地址空间到物理内存地址的映射. +2. 假设物理内存地址大小为32k,每4k为一个页框.虚拟地址空间分页,每个页面大小等于一个页框 +3. 当程序想要访问一个虚拟地址x, +4. 指令将x送到MMU, +5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中: +6. 若在,MMU将x转化为物理内存地址y +7. 若不在,则进行缺页中断,操作系统在物理内存中找到一个使用较少的页面回收掉,将需要访问的页面读到被回收的页面处,再将x转化为物理内存地址访问 + + + + + +### 虚拟内存的操作系统策略 + +#### 1. 读取策略 + +读取策略决定某页何时取入内存,常用的方法有如下两种: + +- 请求分页 + + 只有当访问到某页中的一个单元时才将改页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 + +- 预先分页 + + 对于预先分页,读取的页并不是缺页中断请求的页。预先分页利用了大多数辅存设备(如磁盘)的特性,这些设备有寻道时间和合理的延迟。若一个进程的页连续存储在辅存中,则一次读取许多连续的页要比隔一段时间读取一页有效。当然,若大多数额外读取的页未引用到,则这个策略是低效的。进程首次启动时,可采用预先分页策略,此时程序员须以某种方式制定需要的页。 + +#### 2. 放置策略 + +防止策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 + +#### 3. 置换策略 + +以便处理在必须读取一个新页时,应该置换内存中的哪一页。当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。所有策略的目标都是移出最近最不能访问的页。 + + + +#### 4. 驻留集管理 + +对于分页式虚拟内存,在准备执行时,不需要也不可能把一个进程的所有页都读入内存。因此,操作系统必须决定读取多少页,即决定给特定的进程分配多大的内存空间。 + + + +#### 5.清楚策略 + +与读取策略相反,清楚策略用于确定何时将已修改的一页写回辅存。通常有两种选择: + +- 请求式清楚 + + 只有当一页被选择用于置换时才能被写回辅存。 + +- 预约式清楚 + + 将这些已修改的多页在需要使用他们所占据的页框之前成批写回辅存。 + +#### 6.加载控制 + +加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间话费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 + + + + + +## Android内存管理 + +Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: + +- ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 +- Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 +- Low Memory Killer:大部分移动设备不具备置换能力(因为闪存的使用寿命因素)。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 + + + + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" new file mode 100644 index 00000000..bb435432 --- /dev/null +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -0,0 +1,87 @@ +# 4.调度 + + + +在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处理堵塞状态。典型的调度有四种: + +### 长程调度 + +决定哪个程序可以进入系统中处理,因此它控制了系统的并发度。一旦允许进入,作业或用户程序就成为进程,并添加到供短程调度程序使用的队列中,等待调度。 + + + +### 中程调度 + +决定加入部分或全部位于内存中的进程集合。它是交换功能的一部分。典型情况下,换入(swapping-in)决定取决于管理系统并发度的需求。 + + + +### 短程调度 + +决定处理器执行哪个可运行进程。 + +长程调度程序执行的频率相对较低,并且只是大致决定是否接受新进程和接受哪个新进程。要进行交换决定,中程调度程序需要执行的稍频繁一些。短程调度程序,也成为分派程序(dispatcher),执行的最频繁,它精确的决定下次执行哪个进程。导致当前进程堵塞或抢占当前运行进程的事发生时,调用短程调度程序。这类事件包括: + +- 时钟中断 +- I/O中断 +- 操作系统调用 +- 信号(如信号量) + + + +### I/O调度 + +决定可用I/O设备处理哪个进程挂起的I/O请求 + + + +## 多处理器调度 + + + +多处理器系统分为以下几类: + +- 松耦合、分布式多处理器、集群:又一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 +- 专用处理器:I/O处理器是一个典型的例子。此时,有一个通用的主处理器,专用处理器由主处理器控制,并为主处理器提供服务。 +- 紧耦合多处理器:由一些列共享同一个内存并受操作系统完全控制的处理器组成。 + + + +多处理器中的调度设计三个相互关联的问题: + +- 把进程分配到处理器 + + 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的声明周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 + + + +- 在单处理器上使用多道程序设计 + + 单处理器能够在许多进程间切换,以达到较高的利用率和更好的性能。 + +- 一个进程的实际分派 + + 与多处理器调度相关的最后一个设计问题是选择哪个进程运行。在多道程序单处理器上,与简单的先来先服务策略相比,使用优先级或基于使用历史的高级调度算法可以提高性能。考虑多处理器时,这些复杂性可能是不必要的,甚至可能起到相反的效果,而相对比较简单的方法可能会更有效,而且开销比较低。 + + + + + +### 线程调度 + +在多处理器线程调度和处理器分配的各种方案中,有四个比较突出的方法: + +- 负载分配:进程不分配到某个特定的从处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 +- 组调度:一组相关的的线程基于一对一的原则,同时调度到一组处理器上运行。 +- 专用处理器分配:这种方法与负载分配方法正好相反,它通过把线程指定到处理器来定义隐式的调度。每个程序在其执行过程中,都分配给一组处理器,处理器的数量与程序中线程的数量相等。程序终止时,处理器返回总处理器池,以便分配给另一个程序。 +- 动态调度:在执行期间,进程中线程的数据可以改变,允许动态地改变进程中的线程数量,这就使得操作系统可以通过调整负载情况来提高利用率。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git a/OperatingSystem/5.IO.md b/OperatingSystem/5.IO.md new file mode 100644 index 00000000..bf043437 --- /dev/null +++ b/OperatingSystem/5.IO.md @@ -0,0 +1,42 @@ +# 5.I/O + + + +计算机系统中参与I/O的外设大体上可分为如下三类: + +- 人可读:适用于计算机用户间的交互,如打印机和终端。终端又包括显示器和键盘,以及其他一些可能的设备,如鼠标。 +- 机器可读:适用于与电子设备通信,如磁盘驱动器、USB密钥、传感器、控制器和执行器。 +- 通信:适用于与远程设备通信,如数字线路驱动器和调制解调器。 + +执行I/O的三种技术: + +- 程序控制I/O:处理器代表一个进程给I/O模块发送一个I/O命令,该进程进入忙等待,直到操作完成才能继续执行。 +- 中断驱动I/O:处理器代表进程向I/O模块发出一个I/O命令。有两种可能性:若来自进程的I/O指令是非堵塞的,则处理器继续执行发出I/O命令的进程的后续指令。若I/O指令是堵塞的,则处理器执行的下一条指令来自操作系统,它将当前的进程设置为堵塞态并调度其他进程。 +- 直接存储器访问(DMA):一个DMA模块控制内存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发请求,且只有在整个数据块传送结束后,它才被中断。 + + + +### 磁盘高速缓存 + + + +高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的告诉缓存中。若在则该请求可通过这个告诉缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 + + + +磁盘的高速缓存有两个问题: + +- 当一个I/O请求从磁盘高速缓存中得到满足时,磁盘高速缓存中的数据必须传送到发送请求的进程。这可以通过在内存中把这一块数据从磁盘高速缓存传送到分配给该用户进程的存储空间中,或简单地使用一个共享内存,传送指向磁盘高速缓存中相应项的指针。 +- 置换策略。当一个新扇区被读入磁盘高速缓存时,必须换出一个已存在的块。因此这就需要一个页面置换算法。 + - 最近最少使用算法(LRU,Least Recently Used) + - 最不常使用页面置换算法(LFU, Least Frequently Used) + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" new file mode 100644 index 00000000..50a05239 --- /dev/null +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -0,0 +1,67 @@ +# 6.文件管理 + +文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 + + + +文件系统不但提供存储数据(组织为文件)的手段,而且提供一系列对文件进行操作的功能接口。典型的操作如下: + +- 创建 +- 删除 +- 打开 +- 关闭 +- 读 +- 写 + + + +### 文件系统架构 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/file_system.png?raw=true) + + + + + +- 设备驱动(device drivers):程序直接与外围设备通信。设备驱动程序负责启动设备上的I/O操作,处理I/O请求的完成。 +- 基本文件系统或物理I/O层:是与计算机系统外部环境的基本接口。这一层处理在磁盘间或磁带系统间交换的数据块,因此它关注的是这些块在辅存和内存缓冲区中的位置,而非数据的内容或所涉及的文件结构。 +- 基本I/O管理程序(basic I/O supervisor);负责所有文件I/O的初始化和终止。在这一层,需要一定的控制结构来维护设备的输入/输出、调度和文件状态。基本I/O管理程序是操作系统的一部分。 +- 逻辑I/O(logical I/O)使用户和应用程序能够访问记录。因此基本文件系统处理的是数据块,而逻辑I/O模块处理的是文件记录。逻辑I/O提供一种通用的记录I/O的能。 + + + + + +## Android文件系统 + +Android使用了Linux中的文件管理功能。Android文件系统目录与Linux安装目录类似,只是前者有一些特有的特性。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_file_system.png?raw=true) + +Android文件系统目录的顶层部分: + +- system目录包含操作系统的核心部分,核心部分包括系统的二进制文件、系统库文件和配置文件。它还包含Android的基本应用,如闹钟、计算器和相机。系统映像是锁定的,文件系统只为用户提供只读权限。 +- data目录是应用程序存储文件时的首选位置。当系统中安装了一个新的应用程序时,以下这些操作都有data目录有关: + - .apk文件放置在/data/app中 + - 以应用为中心的库文件安装在/data/data/<应用名称>目录中。这个目录是特定应用程序的沙盒区域,只有该应用可以访问,其他应用不能访问。 + - 建立应用相关的文件数据库。 +- cache目录用于存储应用的临时数据。该区域存储Android系统频繁访问的数据和应用组件。清理高速缓存不会影响到个人数据,而只会简单地清理其中的已有数据,继续使用设备时,其中的数据会自动创建。 +- mnt/sdcard目录不是设备内部的内存分区,而是sd卡的分区,sd卡是一种用于android设备的非易失性的存储卡。 + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" new file mode 100644 index 00000000..5cddf7fc --- /dev/null +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -0,0 +1,27 @@ +# 7.嵌入式系统 + + + +为完成某个特定功能而设计的,或许有附加机制或其他部分的计算机硬件和软件结合体。在许多情况下,嵌入式系统是一个更大系统或产品中的一部分,如汽车中的防抱死系统。 + + + +## 嵌入式Linux + +嵌入式Linux是指运行在嵌入式系统中的Linux。嵌入式Linux是Linux的一个版本,是基于嵌入式设备的大小和硬件限制而定制的,它同时包括一些软件包,用于支持设备商运行的服务和应用。因此嵌入式Linux的内核比普通Linux的内核要小得多。 + +台式机/服务器Linux与嵌入式Linux的一个关键区别是: 台式机/服务器软件通常是在运行平台上编译的,而嵌入式Linux通常在一个平台上编译,但运行于另一个平台,后者称为交叉编译。 + + + +Android是基于Linux内核的一个嵌入式系统,因此我们可以认为Android是嵌入式Linux的一个例子。但是,很多嵌入式Linux开发人员不认为Android系统是嵌入式Linux的实例。他们认为,传统的嵌入式系统拥有固定的功能,而且在出厂时就已确定。Adnroid能支持各种的应用,因此要比普通平台性操作系统强大得多。而且,Android是垂直一体化的系统,包括针对Linux内核的特定修改。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" new file mode 100644 index 00000000..73d02145 --- /dev/null +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -0,0 +1,134 @@ +# 8.虚拟机 + +通常而言,应用程序直接运行在PC或服务器的操作系统上。每台PC或服务器在同一时间只运行一个操作系统。因此,应用程序供应商需要为每个操作系统或平台重写应用程序的部分代码,才能使应用能够得到系统支持的运行。要支持多种操作系统,应用程序供应商需要创建、管理和维护多种硬件与操作系统基础设施,这一过程需要耗费昂贵的代价和大量的资源。处理这个问题的有效策略之一称为虚拟化(virtualization),虚拟化技术可使一台PC或服务器同时运行多个操作系统或一个系统的多个会话。一台运行虚拟化软件的机器能在同一平台上运行大量的应用程序,包括那些运行在不同操作系统上的应用程序。实际上,主机操作系统能支持多个虚拟机(virtual machines),每个虚拟机都有特定操作系统的特性。 + +启用虚拟化的解决方案是虚拟机监视器(VMM),现在通常称为虚拟机管理程序(Hypervisor)。该软件介于硬件和虚拟机之间,以资源代理的形式存在。简而言之,它使多个虚拟机安全地共存于一台物理服务器主机并共享主机的资源。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/virtual_1.png?raw=true) + + + +## Java虚拟机 + + + +尽管Java虚拟机(JVM)用“虚拟机”作为其名称的一部分,但其实现和用途与我们前面所讲的模型不同。虚拟机管理程序支持在主机上运行一个或多个虚拟机。这些虚拟机独立的处理工作负载,支持操作系统和应用,且在它们自身看来,访问一系列提供计算、存储和输入/输出的资源。Java虚拟机的目的是,无须更改任何Java代码就可在任意硬件平台的任意操作系统上,提供运行时空间。两种模型的目的都是通过使用某种程度的抽象化来实现平台无关性。 + +JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾手机的堆、一个存储代码和常量的方法区。 + +JVM支持多个线程,每个线程都有自己的寄存器和堆栈区,且所有线程共享栈和方法区。 + + + +## Android虚拟机 + + + +Android平台的虚拟机称为Dalvik,Dalvik VM(DVM)执行格式为Dalvik Executable(.dex格式)的文件,即为高效存储和内存映射执行而优化的格式。DVM可以运行由Java编译器编译的类,该编译器以用“dx”工具转换为本地格式。 + +虚拟机运行在Linux内核的顶部,它依赖于底层的功能(如线程和底层的内存管理)。Dalvik核心类库的目的是,为那些使用标准Java编程的人员提供熟悉的开发环境,但它是专门为满足小型移动设备的需要而设计的。 + + + +每个Android应用程序都运行在自己的进程中,有自己的Dalvik运行实例。Dalvik是可以在一台设备上高效执行多个副本的虚拟机。 + + + + + +### dex文件系统 + +DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写在文本文件中)转换为字节码,然后将字节码翻译成DVM虚拟机可读和可用的.dex文件。本质上,类文件被转换为.dex文件(像Java虚拟机中的jar文件),然后在DVM上读取和执行。类文件中的重复数据在.dex文件中只包含一次,以节省空间开销。在安装时,这个可执行文件还可根据移动设备进一步修改和优化。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_vs_dalvik.png?raw=true) + +上图中的.jar文件的布局,它包含一个或多个类文件。这些类文件聚合为一个.dex文件,并存储为一个android安装包文件(.apk)。所有类文件中的不同常量池集中为单个常量池,在.dex文件中组织为常量类型。允许类的常量池共享,因此可使常量值的重复减至最低。类似的,类文件中的类、域、方法、属性也在.dex文件中集中到一起。 + + + + + +## Android进程结构 + +如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) + + + +如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 + +其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 + +应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 + + + +### Zygote + +Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 + +实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 + + + +## Android进程模型 + + + +Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 + +Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 + + + +### 启动进程 + +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) + +上图展示了一个新进程中启动活动的流程: + +1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 +2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 +6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 +7. 新进程读取应用程序的源码,开始运行。 +8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 +9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 + +注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 + + + +### 进程声明周期 + +活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 + +Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: + +- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill +- 该设置参数的存在是为了和旧版本的内核兼容 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) + +让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! From 357e4fed3bae520c57beffca1d9947f4c47fee5e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:36:25 +0800 Subject: [PATCH 002/183] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 2 +- ...13\344\270\216\347\272\277\347\250\213.md" | 2 +- OperatingSystem/{5.IO.md => 5.I:O.md} | 0 README.md | 28 +++++++++++++++---- 4 files changed, 25 insertions(+), 7 deletions(-) rename "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" => "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" (99%) rename "OperatingSystem/2.\350\277\233\347\250\213.md" => "OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" (99%) rename OperatingSystem/{5.IO.md => 5.I:O.md} (100%) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" similarity index 99% rename from "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" rename to "OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 00696a2d..d1351546 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -1,6 +1,6 @@ -# 1.操作系统 +# 1.操作系统简介 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" similarity index 99% rename from "OperatingSystem/2.\350\277\233\347\250\213.md" rename to "OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index cad290fc..5f19d32d 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -1,4 +1,4 @@ -# 1.进程 +# 2.进程与线程 diff --git a/OperatingSystem/5.IO.md b/OperatingSystem/5.I:O.md similarity index 100% rename from OperatingSystem/5.IO.md rename to OperatingSystem/5.I:O.md diff --git a/README.md b/README.md index 316ffbc9..41171f32 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,6 @@ Android学习笔记 - [史上最适合Android开发者学习的Go语言教程](https://github.com/CharonChui/GolangStudyNote) - [史上最适合Android开发者学习的iOS开发教程](https://github.com/CharonChui/iOSStudyNote) - - [源码解析][43] - [自定义View详解][1] - [Activity界面绘制过程详解][2] @@ -32,7 +31,6 @@ Android学习笔记 - [Volley源码分析][15] - [Retrofit详解(上)][16] - [Retrofit详解(下)][17] - - [Dagger2][199] - [1.Dagger2简介(一).md][200] - [2.Dagger2入门demo(二).md][201] @@ -43,7 +41,6 @@ Android学习笔记 - [7.Dagger2之dagger-android(七).md][206] - [8.Dagger2与MVP(八).md][207] - [9.Dagger2原理分析(九).md][212] - - [音视频开发][44] - [搭建nginx+rtmp服务器][18] - [视频播放相关内容总结][19] @@ -98,6 +95,18 @@ Android学习笔记 - [11.OpenGL ES滤镜][242] - [弹幕][243] - [Android弹幕实现][244] +- [操作系统][263] + - [1.操作系统简介][264] + - [2.进程与线程][265] + - [3.内存管理][266] + - [4.调度][267] + - [5.I/O][268] + - [6.文件管理][269] + - [7.嵌入式系统][270] + - [8.虚拟机][271] +- [架构设计][272] + - [1.架构简介][273] + - [图片加载][45] - [Glide简介(上)][25] - [Glide简介(下)][26] @@ -556,8 +565,17 @@ Android学习笔记 [260]:https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/H265.md "H265" [261]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P.md "P2P" [262]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P%E5%8E%9F%E7%90%86_NAT%E7%A9%BF%E9%80%8F.md "P2P原理_NAT穿透" - - +[263]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem "操作系统" +[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md "1.操作系统简介" +[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B.md "2.进程和线程" +[266]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md "3.内存管理" +[267]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md "4.调度" +[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.IO.md "5.I/O" +[269]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md "6.文件管理" +[270]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md "7.嵌入式系统" +[271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" +[272]: https://github.com/CharonChui/AndroidNote/tree/master/Architect "架构设计" +[273]: https://github.com/CharonChui/AndroidNote/blob/master/Architect/1.%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B.md "1.架构简介" Developed By From a13218a2a97024f7288ee69e52a41a4c2342f579 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 09:40:53 +0800 Subject: [PATCH 003/183] update README --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 41171f32..91a59d53 100644 --- a/README.md +++ b/README.md @@ -566,11 +566,13 @@ Android学习笔记 [261]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P.md "P2P" [262]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF/P2P%E5%8E%9F%E7%90%86_NAT%E7%A9%BF%E9%80%8F.md "P2P原理_NAT穿透" [263]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem "操作系统" -[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F.md "1.操作系统简介" -[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B.md "2.进程和线程" +[264]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md "1.操作系统简介" +[265]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md "2.进程和线程" [266]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md "3.内存管理" [267]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md "4.调度" -[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.IO.md "5.I/O" + +[268]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md "5.I/O" + [269]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md "6.文件管理" [270]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md "7.嵌入式系统" [271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" From 9d6f06a3625eebf1c80612e7da8d02e9cba8bc28 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 10:19:46 +0800 Subject: [PATCH 004/183] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 70 ++++++++++--------- ...13\344\270\216\347\272\277\347\250\213.md" | 2 + 2 files changed, 38 insertions(+), 34 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index d1351546..b6dd93ea 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -35,7 +35,7 @@ #### 内存管理(Memory management) -内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是成为虚拟内存,但是完全是两个不同方向的技术。 +内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是称为虚拟内存,但是完全是两个不同方向的技术。 进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 进程退出销毁时,内存的回收也是很重要的,否则很容易就会产生内存溢出,占着茅坑不拉屎,导致其他的进程都憋死。 @@ -55,7 +55,7 @@ 我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 -整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。由于网络这块内容实在是太复杂,后面我会另开一个专栏进行讲解。 +整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。 @@ -74,16 +74,16 @@ -计算机由处理器、存储器和输入\输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: +计算机由处理器、存储器和输入/输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: - 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) -- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也成为实存储器(real memory)或主存储器(primary memory)。 -- 输入\输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 -- 系统总线(System bus)在处理器、内存和输入\输出模块间提供通信的设施。 +- 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也称为实存储器(real memory)或主存储器(primary memory)。 +- 输入/输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 +- 系统总线(System bus):在处理器、内存和输入/输出模块间提供通信的设施。 -处理器的一种功能是与存储器叫唤数据。为此,它通常使用两个内部寄存器: +处理器的一种功能是与存储器交换数据。为此,它通常使用两个内部寄存器: - 存储器地址寄存器(Memory Address Register, MAR),用于确定下一次读/写的存储器地址。 - 存储器缓冲寄存器(Memory Buffer Register,MBR),存放要写入存储器的数据或从存储器中读取的数据。 @@ -100,12 +100,14 @@ ### 计算机打开电源后的执行 -在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常相应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 +在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(Basic Input Output System, BIOS)的程序。在BIOS内有底层I/O软件,包括读键盘、写屏幕、进行磁盘I/O以及其他过程。在计算机启动时,BIOS开始运行。它首先检查所安装的RAM数量、键盘和其他基本设备是否已安装并正常响应。接着,它开始扫描PCIe和PCI总线并找出连在上面的所有设备。即插即用设备也被记录下来。如果现有的设备和系统上一次启动时的设备不同,则新的设备将被配置。然后BIOS通过尝试存储在CMOS存储器中的设备清单决定启动设备。用户可以在系统刚启动之后进入一个BIOS配置程序,对设备清单进行修改。典型的,如果存在CD-ROM(有时是USB),则系统试图从中启动(之前重装系统就是这么操作的)。如果失败,系统将从硬盘启动。启动设备上的第一个扇区被读入内存并执行。这个扇区中包含一个对保存在启动扇区末尾的分区表检查的程序,以确定哪个分区是活动的。然后,从该分区读入第二个启动装载模块。来自活动分区的这个装载模块被读入操作系统,并启动之。 然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 +如果是从普通硬盘启动,可简单列举为如下步骤: + 1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 -2. 寻址0xFFFF0(ROM BIOS营社区) +2. 寻址0xFFFF0(ROM BIOS映射区) 3. 检查RAM、键盘、显示器、磁盘、主板等硬件 4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 5. 设置 cs=0x07c0,ip=0x0000开始执行 @@ -114,9 +116,7 @@ ## CPU(Central Processing Unit) -CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。CPU(中央处理器)是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。其实在现在一些CPU中,一级缓存也会分为一级数据缓存和一级指令缓存。 - -由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些`寄存器`来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。 +CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 @@ -124,27 +124,27 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 -**累加寄存器:**存储执行运算的数据和运算后的数据。 +***累加寄存器***:存储执行运算的数据和运算后的数据。 -**标志寄存器:**存储运算处理后的CPU的状态。 +***标志寄存器***:存储运算处理后的CPU的状态。 -**程序计数器:**存储下一条指令所在内存的地址。 +***程序计数器***:存储下一条指令所在内存的地址。 -**基址寄存器:**存储数据内存的起始地址。 +***基址寄存器***:存储数据内存的起始地址。 -**变址寄存器:**存储基址寄存器的相对地址。 +***变址寄存器***:存储基址寄存器的相对地址。 -**通用寄存器:**存储任意数据。 +***通用寄存器***:存储任意数据。 -**指令寄存器:**存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +***指令寄存器***:存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 -**栈寄存器:**存储栈区域的起始地址。 +***栈寄存器***:存储栈区域的起始地址。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 -**计算机执行的原理是: 取指执行 ** +***计算机执行的原理是: 取指执行*** 处理器执行的程序是由一组保存在存储器中的指令组成的。最简单的指令处理包括两步: 处理器从存储器中一次读(取)一条指令,然后执行每条指令。程序执行是由不断重复的取指令和执行指令的过程组成的。指令执行可能涉及很多操作,具体取决于指令本身。 @@ -163,11 +163,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 -- `取指令`阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 -- `指令译码`阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 -- `执行指令`阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 -- `访问取数`阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 -- `结果写回`阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; +- 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 +- 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 +- 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 +- 结果写回阶段,作为最后一个阶段,结果写回(Write Back,WB)阶段把执行指令阶段的运行结果数据“写回”到某种存储形式:结果数据经常被写到CPU的内部寄存器中,以便被后续的指令快速地存取; @@ -185,7 +185,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 于是出现了多级存储器组织结构: -- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用预CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32*32位,而在64位CPU中为64*64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 +- 寄存器: 最快、最小和最贵的存储器类型由位于处理器内部的寄存器组成。它们用与CPU相同的材料制成,所以和CPU一样快。显然,访问它们是没有时延的。其典型的存储容量是,在32位CPU中为32x32位,而在64位CPU中为64x64位。在这两种情况下,其存储容量都小于1KB。典型情况下,一个处理器包含多个寄存器,某些处理器包含上百个寄存器。 - 高速缓存,它多数由硬件控制。 @@ -197,9 +197,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/changpianji.jpg?raw=true) - 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 - -这里要说一下闪存(flash memory),在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 + + 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + + 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存支制成的。 @@ -223,11 +225,11 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 - RAM(Random-access memory) - 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”,因为CPU工作的速度比RAM的读写速度快,所以CPU读写RAM时需要花费时间等待,这样就使CPU的工作速度下降。人们为了提高CPU读写程序和数据的速度,在RAM和CPU之间增加了高速缓存(Cache)部件。Cache的内容是随机存储器(RAM)中部分存储单元内容的副本 + 随机存取存储器,一般使用动态半导体存储器件(DRAM),对于CPU来说,RAM是主要存放数据和程序的地方,所以也叫做“主存”。 - ROM(Read-Only Memory) - 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变 + 只读存储器,出厂时其内容由厂家用掩膜技术写好,只可读出,但无法改写。信息已固化在存储器中,一般用于存放系统程序BIOS和用于微程序,断电也没有关系,放ROM的数据一辈子都不会变。 @@ -241,7 +243,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 -缓存基本上都是采用SRAM存储器,SRAM是英文Static RAM的缩写,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 +缓存基本上都是采用SRAM存储器,它是一种具有静态存取功能的存储器,不需要刷新电路即能保存它内部存储的数据。不像DRAM内存那样需要刷新电路,每隔一段时间,固定要对DRAM刷新充电一次,否则内部的数据即会消失,因此SRAM具有较高的性能,但是SRAM也有它的缺点,即它的集成度较低,相同容量的DRAM内存可以设计为较小的体积,但是SRAM却需要很大的体积,这也是不能将缓存容量做得太大的重要原因。它的特点归纳如下:优点是节能、速率快、不必配合内存刷新电路、可提高整体的工作效率,缺点是集成度低、相同的容量体积较大、而且价格较高,只能少量用于关键性系统以提高效率。 @@ -272,7 +274,7 @@ CPU 是计算机的大脑,它主要和内存进行交互,从内存中提取 - +- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 5f19d32d..92e17b16 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -294,7 +294,9 @@ +- [上一篇:1.操作系统简介][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md] +- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] --- From db0f8bb241c36c4701b22274b39d6572986954e7 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 26 Nov 2020 13:28:20 +0800 Subject: [PATCH 005/183] update operation syste --- ...73\347\273\237\347\256\200\344\273\213.md" | 2 +- ...13\344\270\216\347\272\277\347\250\213.md" | 137 +++++++++++------- ...05\345\255\230\347\256\241\347\220\206.md" | 62 ++++---- .../4.\350\260\203\345\272\246.md" | 5 +- OperatingSystem/5.I:O.md | 3 + ...07\344\273\266\347\256\241\347\220\206.md" | 5 + ...45\345\274\217\347\263\273\347\273\237.md" | 3 + ...8.\350\231\232\346\213\237\346\234\272.md" | 75 +--------- 8 files changed, 137 insertions(+), 155 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index b6dd93ea..1129070f 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -274,7 +274,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 92e17b16..316d0c47 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -10,7 +10,7 @@ -进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 [2] +进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 进程是一个具有独立功能的程序关于某个数据集合的一次运行活动。它可以申请和拥有系统资源,是一个动态的概念,是一个活动的实体。它不只是程序的[代码](https://baike.baidu.com/item/代码),还包括当前的活动,通过[程序计数器](https://baike.baidu.com/item/程序计数器)的值和处理[寄存器](https://baike.baidu.com/item/寄存器)的内容来表示。 @@ -58,8 +58,6 @@ -进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。 - ## 进程的状态 @@ -76,34 +74,22 @@ - 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 -- 进程控制块:记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。 +- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对 PCB 的操作。 -##### 进程控制块 +#### 进程控制块 进程执行的任意时刻,都可由如下元素来表征: -- 标识符:与进程相关的唯一标识符,用来区分其他进程 -- 状态:若进程正在执行,则进程处于运行态 -- 优先级:相对于其他进程的优先顺序 +- 标识符:与进程相关的唯一标识符,用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 +- 状态:若进程正在执行,则进程处于运行态。进程状态指明进程当前的状态,作为进程调度和对换时的依据; +- 优先级:相对于其他进程的优先顺序,说明进程使用CPU的优先级别,其中优先级高的进程将优先获得CPU。 - 程序计数器:程序中即将执行的下一条指令的地址 - 内存指针:包括程序代码和进程相关数据的指针,以及与其他进程共享内存块的指针 - 上下文数据:进程执行时处理器的寄存器中的数据 - I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 - 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 -上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。进程控制块(PCB)是进程实体的重要组成部分,它记录了操作系统所需要的、用于描述进程情况及控制进程所需要的全部信息。原来不能独立运行的程序或数据,通过 PCB 就可以成为一个可以独立运行的基本单位。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个 PCB;当进程结束时,系统又收回其 PCB,该进程也随之消亡。简单的总结就是进程控制块主要包括下述四个方面的信息: - -1. 进程标识符信息:用于标识、区分一个进程,通常有外部标识符和内部标识符两类。外部标识符通常是由字母、数字所组成的一个字符串,用户或其他进程访问该进程时使用。内部标识符是操作系统为每个进程赋予的唯一一个整数,是作为内部识别而设置的。 - -2. 进程调度信息:用于描述与进程调度有关的状态信息,包括进程状态、进程优先权、调度信息和等待事件等。进程状态指明进程当前的状态,作为进程调度和对换时的依据;进程优先权说明进程使用 CPU 的优先级别,其中优先权高的进程将优先获得 CPU;调度信息描述与进程调度算法相关的信息,如进程等待时间、已运行的时间等;等待事件是指进程由运行态转变为阻塞态时所等待发生的事件。 - -3. CPU 状态信息:用于保留进程运行时 CPU 的各种信息,使得进程暂停运行后,下次重新运行时能从上次停止的地方继续运行。CPU 状态信息通常包含通用寄存器、控制和状态寄存器、用户栈指针等。CPU 状态字记载了程序执行的状态信息,如条件码、外中断屏蔽标识、执行状态(核心态或用户态)标识等。 - -4. 进程控制信息(process control information):包括进程资源、控制机制等一些进程运行时所需要的信息,如: - - 程序和数据地址:该进程的程序和数据所在的内存和外存地址,以便该进程在次运行时,能够找到程序和数据。 - - 进程同步和通信机制:实现进程同步和通信时所采用的机制,如消息队列指针、信号量等。 - - 资源清单:除 CPU 外,进程所需的全部资源和已经分配到的资源。 - - 链接指针:用于指向该进程所在队列的下一个进程的 PCB 首地址。 +上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 @@ -121,7 +107,9 @@ ## 进程切换 +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 @@ -159,7 +147,7 @@ ### 互斥的要求 1. 必须强制实施互斥。在于相同资源或共享对象的临界区有关的所有进程中,一次只允许一个进程进入临界区。 -2. 一个在非临界区停止的进程不能干啥其他进程。 +2. 一个在非临界区停止的进程不能干涉其他进程。 3. 绝不允许出现需要访问临界区的进程被无限延迟的情况,即不会死锁或饥饿。 4. 没有进程在临界区中时,任何需要进入临界区的进程必须能够立即进入。 5. 对相关进程的执行速度和处理器的数量没有任何要求和限制。 @@ -193,24 +181,6 @@ -**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 - - - -### 进程的实现 - -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 - - - -**操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 - - - -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 `进程表(process table)`。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 - - - ## 进程调度 进程调度就是处理器调度(上下文切换) @@ -219,15 +189,15 @@ - 高级调度 -作业调度,把后备作业调入内存运行 + 作业调度,把后备作业调入内存运行 - 中级调度 -在虚拟存储器中引入,在内,外存交换区进行进程对换 + 在虚拟存储器中引入,在内,外存交换区进行进程对换 - 低级调度 -进程调度,把就绪队列里的某个进程获得CPU执行权 + 进程调度,把就绪队列里的某个进程获得CPU执行权 ### 调度方式 @@ -237,18 +207,18 @@ - 不可剥夺 -一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 + 一单处理器分配给某进程,遍让它一直运行下去,直到进程完成或者发生某种时间而阻塞,才分配给其他进程。 ### 调度算法 - 先进先出 -按照进入就绪队列的进程顺序,不加其他条件干涉 + 按照进入就绪队列的进程顺序,不加其他条件干涉 - 短进程优先 -优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 -平均周转时间:16,平均周转时间越小,调度性能越好 + 优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 + 平均周转时间:16,平均周转时间越小,调度性能越好 - 轮转法 - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** @@ -274,7 +244,76 @@ +## Android进程结构 + +如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 + + + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) + + + +如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 + +其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 + +应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 + + + +### Zygote + +Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 + +实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 + + + +## Android进程模型 + + + +Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 + +Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 + + + +### 启动进程 + +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) + +上图展示了一个新进程中启动活动的流程: + +1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 +2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 +6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 +7. 新进程读取应用程序的源码,开始运行。 +8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 +9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 + +注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 + + + +### 进程生命周期 + +活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 + +Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: + +- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill +- 该设置参数的存在是为了和旧版本的内核兼容 + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) +让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 @@ -294,9 +333,9 @@ -- [上一篇:1.操作系统简介][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md] +- [上一篇:1.操作系统简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md) -- [下一篇:2.进程与线程][https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md] +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) --- diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index a4b40e39..3a54aa0e 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -2,28 +2,31 @@ 常用概念: -- 页框:内存中固定长度的快 -- 页:固定长度的数据库,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 +- 页框:内存中固定长度的块 +- 页:固定长度的数据块,存储在二级存储器中(如磁盘)。数据页可以临时赋值到内存的页框中。 - 段:变长数据块,存储在二级存储器中。整个段可以临时复制到内存的一个可用区域中(分段),或可以将一个段分为许多页,然后将每页单独复制到内存中(分段与分页相结合) -内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: -1、内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 -2、地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 -3、内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 -4、存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 +## 内存管理的功能 + +内存管理的主要操作是处理器把程序装入内存中执行。内存管理的功能有: + +1. 内存空间的分配与回收:由操作系统完成主存储器空间的分配和管理,使程序员摆脱存储分配的麻烦,提高编程效率。 +2. 地址转换:在多道程序环境下,程序中的逻辑地址与内存中的物理地址不可能一致,因此存储管理必须提供地址变换功能,把逻辑地址转换成相应的物理地址。 +3. 内存空间的扩充:利用虚拟存储技术或自动覆盖技术,从逻辑上扩充内存。 +4. 存储保护:保证各道作业在各自的存储空间内运行,互不干扰。 进程对应的内存空间中所包含的5种不同的数据区: -- 代码段(code segment):又称文本段,用来存放指令,运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 +- 代码段(code segment):用来存放程序运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 - 数据段(data segment): 存储初始化的全局变量和初始化的static变量。数据段中的数据的生存期是随程序持续性(随进程持续性):进程创建就存在,进程死亡就消失。 -- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0. -- rodata段:只读数据 比如 printf 语句中的格式字符串和开关语句的跳转表。也就是常量区。例如,全局作用域中的 const int ival = 10,ival 存放在 .rodata 段;再如,函数局部作用域中的 printf("Hello world %d\n", c); 语句中的格式字符串 "Hello world %d\n",也存放在 .rodata 段。 +- BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0.这里很奇怪,一般的书上都会说全局变量和静态变量是会自动初始化的,怎么突然来了个未初始化的变量?其实变量的初始化可以分为显式初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话也会被初始化,那就是不管什么类型都初始化为默认值,这种没有显示初始化的就是这里所说的未初始化。例如整数型的全局变量未初始化的默认隐式初始化的值为0,都是0就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)。BSS的全称是Block Started by Symbol,它属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存能在运行时分配并被有效的清零。BSS节在应用程序的二进制映像文件中并不存在,既不占用磁盘空间,而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。 +- rodata段(read only data):常量区,存放只读数据。比如程序中定义为const的全局变量,“Hello Word”的字符串常量。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。在有的嵌入式系统中,rodata放在ROM中,运行时直接读取,不须加载到RAM内存中。所以在嵌入式开发中,常将已知的常量系数,表格数据等加以const关键字。存放在ROM中,避免占用RAM空间。 - 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) @@ -39,19 +42,17 @@ ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) -Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,进入虚拟内存。 - +Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 - -Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间(具体的原因请看硬件基础部分)。 +Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。 在讨论进程空间细节前,这里先要澄清下面几个问题: -l 第一、4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 +1. 4G的进程地址空间被人为的分为两个部分——用户空间与内核空间。用户空间从0到3G(0xC0000000),内核空间占据3G到4G。用户进程通常情况下只能访问用户空间的虚拟地址,不能访问内核空间虚拟地址。只有用户进程进行系统调用(代表用户进程在内核态执行)等时刻可以访问到内核空间。 -l 第二、用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 +2. 用户空间对应进程,所以每当进程切换,用户空间就会跟着变化;而内核空间是由内核负责映射,它并不会跟着进程改变,是固定的。内核空间地址有自己对应的页表(init_mm.pgd),用户进程各自有不同的页表。 -l 第三、每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 +3. 每个进程的用户空间都是完全独立、互不相干的。不信的话,你可以把上面的程序同时运行10次(当然为了同时运行,让它们在返回前一同睡眠100秒吧),你会看到10个进程占用的线性地址一模一样。 @@ -71,7 +72,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 - 程序可能太大而不能放到一个分区中。此时程序员必须使用覆盖技术设计程序,使得任何时候该程序只有一部分需要放到内存中。当需要的模块不在时,用户程序必须把这个模块装入程序的分区,覆盖该分区中的任何程序和数据。 - - 内存的利用率非常低。任何程序,急事很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 + - 内存的利用率非常低。任何程序,即使很小,都需要占据一个完整的分区。由于装入的数据块小于分区大小,因而导致分区内部存在空间浪费,这种现象称为内部碎片(internal fragmentation)。 - 使用大小不等的分区:大小不等的分区可以缓解上面的两个问题,但是不能完全解决。 @@ -83,7 +84,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 为了克服固定分区的确定,提出了动态分区。对于动态分区,分区长度和数量是可变的。程序装入内存时,系统会给它分配一块与其所需容量完全相等的内存空间。但是对于一个64MB的内存,如果装入前三个进程已经占用了很大一部分,剩下的部分对于第四个进程来说又太小,这样在内存的末尾就剩下一个“空洞”。而等第二个进程结束后腾出的足够的空间来装入第四个进程,但是由于第四个进程比第二个进程小,所有这里又形成了另一个小"空洞"。这样最终在内存中就会形成许多小空洞。随着时间的推移,内存中形成了越来越多的碎片,内存的利用率随之下降。这种现象称为外部碎片(external fragmentation),指在所有分区外的存储空间变成了越来越多的碎片,这与前面所讲的内部碎片正好对应。 -客服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 +克服外部碎片的一种技术就是压缩(compaction)。操作系统不时的移动进程,使得进程占用的空间连续,并使所有空闲空间连成一片。但是压缩的困难之处在于,它是一个非常费时的过程,且会浪费处理器时间。 @@ -97,9 +98,9 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 -在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的摸个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: +在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的某个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: -- 逻辑地址(logical address)是指与当前数据再内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 +- 逻辑地址(logical address)是指与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 - 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 - 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 @@ -125,7 +126,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 ### 分段 -细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管短有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 +细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 @@ -163,7 +164,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 -**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个 块,每个块被成为一页或页面. +**虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个块,每个块被成为一页或页面. **程序运行时,并不是所有页都在物理内存中**: @@ -204,7 +205,7 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 #### 2. 放置策略 -防止策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 +放置策略决定一个进程块驻留在实存中的什么位置。在纯分页系统或段页式系统中,如何放置通常无关紧要,因为地址转换硬件和内存访问硬件能以相同的效率为任何页框组合执行相应的功能。 #### 3. 置换策略 @@ -218,15 +219,15 @@ l 第三、每个进程的用户空间都是完全独立、互不相干的 -#### 5.清楚策略 +#### 5.清除策略 -与读取策略相反,清楚策略用于确定何时将已修改的一页写回辅存。通常有两种选择: +与读取策略相反,清除策略用于确定何时将已修改的一页写回辅存。通常有两种选择: -- 请求式清楚 +- 请求式清除 只有当一页被选择用于置换时才能被写回辅存。 -- 预约式清楚 +- 预约式清除 将这些已修改的多页在需要使用他们所占据的页框之前成批写回辅存。 @@ -244,8 +245,7 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:大部分移动设备不具备置换能力(因为闪存的使用寿命因素)。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 - +- Low Memory Killer:ndorid基于oomKiller原理所扩展的一个多层次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系统无法分配新内存的时候,选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 @@ -263,6 +263,8 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 +- [上一篇:2.进程和线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) +- [下一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" index bb435432..568d6cf7 100644 --- "a/OperatingSystem/4.\350\260\203\345\272\246.md" +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -51,7 +51,7 @@ - 把进程分配到处理器 - 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的声明周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 + 假设多处理器的结构是统一的,即没有哪个处理器在访问内核和I/O设备时具有物理上的特别优势,那么最简单的调度方法是把处理器视为一个资源池,并按照要求把进程分配到相应的处理器。但是这样就牵扯到静态还是动态的问题。如果一个进程从被激活到完成,一直被分配给同一个处理器,那么就需要为每个处理器维护一个专门的短程队列。这种方法的优点是调度的开销较小,因为相对于所有进程,关于处理器的分配只进行一次。静态分配的缺点就是一个处理器可能处于空闲状态,这时其队列为空,而另一个处理器却积压了许多工作。为了防止这种情况,需要使用一个公共队列。所有进程都进入一个全局队列,然后调度到任何一个可用的处理器中。这样,在一个进程的生命周期中,它可以在不同的时间于不同的处理器上执行。另一种分配策略是动态负载平衡,在该策略中,线程能在不同处理器所对应的队列之间转移。Linux采用的就是这种动态分配策略。 @@ -80,6 +80,9 @@ +- [上一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) +- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) + --- diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index bf043437..2a4e19c2 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -33,6 +33,9 @@ +- [上一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) +- [下一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) + diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 50a05239..3621dd4d 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -1,5 +1,7 @@ # 6.文件管理 + + 文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 @@ -54,6 +56,9 @@ Android文件系统目录的顶层部分: +- [上一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) +- [下一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" index 5cddf7fc..e37630e3 100644 --- "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -20,6 +20,9 @@ Android是基于Linux内核的一个嵌入式系统,因此我们可以认为An +- [上一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) +- [下一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) + --- diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 73d02145..0945dd88 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -52,80 +52,7 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 -## Android进程结构 - -如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 - - - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process.png?raw=true) - - - -如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 - -其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 - -应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 - - - -### Zygote - -Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 - -实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 - - - -## Android进程模型 - - - -Linux的传统进程模型是用fork指令来创建新进程,然后用exec指令使用待运行的源码初始化该进程并开始执行。shell负责实现进程执行、创建新进程、执行所需的进程来运行shell指令。当指令结束时,进程被从Linux中移除。 - -Android使用的进程有些不同。活动管理器是Android负责正在运行的应用程序的管理的一部分。活动管理器协调新应用程序进程的启动,决定哪些应用程序能在其中运行,哪些已不再需要。 - - - -### 启动进程 - -为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) - -上图展示了一个新进程中启动活动的流程: - -1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 -2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 -3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 -4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 -5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 -6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 -7. 新进程读取应用程序的源码,开始运行。 -8. 活动管理器将所有即将进行的操作发送给新进程,在此处为“启动活动X”。 -9. 新进程收到指令,启动活动,实体化合适的Java类并执行。 - -注意,当活动启动时,应用程序的进程可能正在运行了。在这种情况下,活动管理器会直接跳转到末尾,向该进程发送一条新指令,让它实体化并执行合适的组件。如果合适,这会导致一个额外的活动实例在应用程序中运行。 - - - -### 进程声明周期 - -活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 - -Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: - -- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill -- 该设置参数的存在是为了和旧版本的内核兼容 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) - -让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 - - - - +- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) --- From 9dd95c870f9057079e458ed9f1165ce9720d4e75 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:23:03 +0800 Subject: [PATCH 006/183] add android kernal --- .../ANR\345\210\206\346\236\220.md" | 66 ++ ...04\351\234\262\345\210\206\346\236\220.md" | 105 -- .../crash\345\210\206\346\236\220.md" | 126 +++ ...03\345\261\200\344\274\230\345\214\226.md" | 47 +- ...66\346\236\204\347\256\200\344\273\213.md" | 4 +- ...73\347\273\237\347\256\200\344\273\213.md" | 141 ++- ...13\344\270\216\347\272\277\347\250\213.md" | 187 +++- ...05\345\255\230\347\256\241\347\220\206.md" | 4 +- OperatingSystem/5.I:O.md | 13 + ...8.\350\231\232\346\213\237\346\234\272.md" | 37 +- ...13\351\227\264\351\200\232\344\277\241.md" | 199 ++++ ...10\346\201\257\346\234\272\345\210\266.md" | 918 ++++++++++++++++++ ...roid Framework\346\241\206\346\236\266.md" | 105 ++ ...ManagerService\347\256\200\344\273\213.md" | 148 +++ ...10\346\201\257\350\216\267\345\217\226.md" | 60 ++ ...30\345\210\266\345\237\272\347\241\200.md" | 51 + ...30\345\210\266\345\216\237\347\220\206.md" | 16 + ...ManagerService\347\256\200\344\273\213.md" | 131 +++ ...ManagerService\347\256\200\344\273\213.md" | 65 ++ ...57\345\212\250\350\277\207\347\250\213.md" | 4 + ...06\345\217\221\350\257\246\350\247\243.md" | 45 +- ...07\347\250\213\350\257\246\350\247\243.md" | 51 +- 22 files changed, 2369 insertions(+), 154 deletions(-) create mode 100644 "AdavancedPart/ANR\345\210\206\346\236\220.md" delete mode 100644 "AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" create mode 100644 "AdavancedPart/crash\345\210\206\346\236\220.md" create mode 100644 "OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" create mode 100644 "OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" create mode 100644 "OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" create mode 100644 "OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" create mode 100644 "OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" create mode 100644 "OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" create mode 100644 "OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" create mode 100644 "OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" diff --git "a/AdavancedPart/ANR\345\210\206\346\236\220.md" "b/AdavancedPart/ANR\345\210\206\346\236\220.md" new file mode 100644 index 00000000..fc8ed472 --- /dev/null +++ "b/AdavancedPart/ANR\345\210\206\346\236\220.md" @@ -0,0 +1,66 @@ +# ANR分析 + +Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 + + +Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: +- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 +- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s +- ContentProvider Timeout:内容提供者,在publish过超时10s +- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 + + + +ANR信息输出到traces.txt文件中 + +traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 +adb pull /data/anr . + + +1) Thread基础信息 + +输出种包含所有的线程,取其中的一条 +"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] + java.lang.Thread.State: BLOCKED (on object monitor) + at Test.rightLeft(Test.java:48) + - waiting to lock <0x00000007d56540a0> (a Test$LeftObject) + - locked <0x00000007d5656180> (a Test$RightObject) + at Test$2.run(Test.java:68) + at java.lang.Thread.run(Thread.java:745) +a) "Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] + +首先描述了线程名是『Thread-1』,然后prio=5表示优先级,tid表示的是线程id,nid表示native层的线程id,他们的值实际都是一个地址,后续给出了对于线程状态的描述,waiting for monitor entry [0x000000011cb30000]这里表示该线程目前处于一个等待进入临界区状态,该临界区的地址是[0x000000011cb30000] +这里对线程的描述多种多样,简单解释下上面出现的几种状态 + + waiting on condition(等待某个事件出现) + waiting for monitor entry(等待进入临界区) + runnable(正在运行) + in Object.wait(处于等待状态) + +作者:silentleaf +链接:https://www.jianshu.com/p/30c1a5ad63a3 +来源:简书 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" "b/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" deleted file mode 100644 index d3ea4562..00000000 --- "a/AdavancedPart/Handler\345\257\274\350\207\264\345\206\205\345\255\230\346\263\204\351\234\262\345\210\206\346\236\220.md" +++ /dev/null @@ -1,105 +0,0 @@ -Handler导致内存泄露分析 -=== - -有关内存泄露请猛戳[内存泄露][1] - -```java -Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something. - } -} -``` -当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 - -一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: -- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. -- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, -当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, -会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收 -- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** - -## 具体分析 -```java -public class SampleActivity extends Activity { - - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); -} -``` -在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. - -## 解决方法 -- 通过程序逻辑来进行保护。 - - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, - `Activity`自然会在合适的时候被回收。 - - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - - 关于`Handler.remove*`方法 - - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 - - `removeMessages(int what)` ——按what来匹配 - - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; -- 将`Handler`声明为静态类。 - 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - -```java -public class MyActivity extends Activity { - private MyHandler mHandler; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mHandler = new MyHandler(this); - } - - @Override - protected void onDestroy() { - // Remove all Runnable and Message. - mHandler.removeCallbacksAndMessages(null); - super.onDestroy(); - } - - static class MyHandler extends Handler { - // WeakReference to the outer class's instance. - private WeakReference mOuter; - - public MyHandler(MyActivity activity) { - mOuter = new WeakReference(activity); - } - - @Override - public void handleMessage(Message msg) { - MyActivity outer = mOuter.get(); - if (outer != null) { - // Do something with outer as your wish. - } - } - } -} -``` - -[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/crash\345\210\206\346\236\220.md" "b/AdavancedPart/crash\345\210\206\346\236\220.md" new file mode 100644 index 00000000..e3dd4ca7 --- /dev/null +++ "b/AdavancedPart/crash\345\210\206\346\236\220.md" @@ -0,0 +1,126 @@ +# crash分析 + + +## Java Crash流程 + +1、首先发生crash所在进程,在创建之初便准备好了defaultUncaughtHandler,用来处理Uncaught Exception,并输出当前crash的基本信息; +2、调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程; +3、接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash; +4、从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox; +5、执行makeAppCrashingLocked: + +创建当前用户下的crash应用的error receiver,并忽略当前应用的广播; +停止当前进程中所有activity中的WMS的冻结屏幕消息,并执行相关一些屏幕相关操作; + +6、再执行handleAppCrashLocked方法: + +当1分钟内同一进程连续crash两次时,且非persistent进程,则直接结束该应用所有activity,并杀死该进程以及同一个进程组下的所有进程。然后再恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程连续crash两次时,且persistent进程,,则只执行恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程未发生连续crash两次时,则执行结束栈顶正在运行activity的流程。 + +7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框; +8、到此,system_server进程执行完成。回到crash进程开始执行杀掉当前进程的操作; +9、当crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked(); +10、最后,执行清理应用相关的四大组件信息。 + + + +- 剩余内存: /proc/meminfo,当系统可用内存小于MemTotal的10%时,非常容易发生OOM和大量GC。 +- PSS和RSS通过/proc/self/smap +- 虚拟内存: 获取大小/proc/self/status,获取具体的分布/proc/self/maps。 + +如果应用堆内存和设备内存比较充足,但还出现内存分配失败,则可能跟资源泄露有关。 +- 获取fd的限制数量:/proc/self/limits。一般单个进程允许打开的最大句柄个数为1024,如果超过800需将所有fd和文件名输出日志进行排查。 +- 获取线程数大小:/proc/self/status一个线程一般占2MB的虚拟内存,线程数超过400个比较危险,需要将所有tid和线程名输出到日志进行排查。 + + + + + + + +Native Crash + + 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 + 定位:so 库剥离调试信息的话,只有相对位置没有具体行号,可以使用 NDK 提供的 addr2line 或 ndk-stack 来定位 + addr2line:根据有调试信息的 so 和相对位置定位实际的代码处 + ndk-stack:可以分析 tombstone 文件,得到实际的代码调用栈 + + + +## ANR + + + + +ANR排查流程 +1、Log获取 +1、抓取bugreport +adb shell bugreport > bugreport.txt +复制代码 +2、直接导出/data/anr/traces.txt文件 +adb pull /data/anr/traces.txt trace.txt +复制代码 +2、搜索“ANR in”处log关键点解读 + + +发生时间(可能会延时10-20s) + + +pid:当pid=0,说明在ANR之前,进程就被LMK杀死或出现了Crash,所以无法接受到系统的广播或者按键消息,因此会出现ANR + + +cpu负载Load: 7.58 / 6.21 / 4.83 +代表此时一分钟有平均有7.58个进程在等待 +1、5、15分钟内系统的平均负荷 +当系统负荷持续大于1.0,必须将值降下来 +当系统负荷达到5.0,表面系统有很严重的问题 + + +cpu使用率 +CPU usage from 18101ms to 0ms ago +28% 2085/system_server: 18% user + 10% kernel / faults: 8689 minor 24 major +11% 752/android.hardware.sensors@1.0-service: 4% user + 6.9% kernel / faults: 2 minor +9.8% 780/surfaceflinger: 6.2% user + 3.5% kernel / faults: 143 minor 4 major + + +上述表示Top进程的cpu占用情况。 +注意 +如果CPU使用量很少,说明主线程可能阻塞。 +3、在bugreport.txt中根据pid和发生时间搜索到阻塞的log处 +----- pid 10494 at 2019-11-18 15:28:29 ----- +复制代码 +4、往下翻找到“main”线程则可看到对应的阻塞log +"main" prio=5 tid=1 Sleeping +| group="main" sCount=1 dsCount=0 flags=1 obj=0x746bf7f0 self=0xe7c8f000 +| sysTid=10494 nice=-4 cgrp=default sched=0/0 handle=0xeb6784a4 +| state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100 +| stack=0xff575000-0xff577000 stackSize=8MB +| held mutexes= +复制代码 +上述关键字段的含义如下所示: + +tid:线程号 +sysTid:主进程线程号和进程号相同 +Waiting/Sleeping:各种线程状态 +nice:nice值越小,则优先级越高,-17~16 +schedstat:Running、Runable时间(ns)与Switch次数 +utm:该线程在用户态的执行时间(jiffies) +stm:该线程在内核态的执行时间(jiffies) +sCount:该线程被挂起的次数 +dsCount:该线程被调试器挂起的次数 +self:线程本身的地址 + +作者:jsonchao +链接:https://juejin.cn/post/6844903972587716621 +来源:掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + diff --git "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" index b5c34641..68dd2232 100644 --- "a/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" +++ "b/AdavancedPart/\345\270\203\345\261\200\344\274\230\345\214\226.md" @@ -1,8 +1,34 @@ 布局优化 === +布局优化的核心问题就是要解决因布局渲染性能不佳而导致应用卡顿的问题。 + + + +## 绘制原理 + +Android的绘制主要是借助CPU和GPU结合刷新机制来共同完成。 + +- CPU负责计算显示内容,包括Measure、Layout等操作,在UI绘制上的缺陷在于容易显示重复的视图组件,这样不仅带来重复的计算操作,而且会占用额外的GPU资源。 +- GPU负责光栅化,将UI元素绘制到屏幕上。 + +例如,文字首先要经过CPU换算成纹理,然后再传递给GPU进行渲染。而图片是先经过CPU计算,然后加载到内存中,最后再传给GPU进行渲染。 + + +## 耗时原因 +分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿: + +1. 首先,系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。 +2. 其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。 +3. 同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。 +4. 最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。 + + +## 优化方式 + - 去除不必要的嵌套和节点 这是最基本的一条,但也是最不好做到的一条,往往不注意的时候难免会一些嵌套等。 + - 首次不需要的节点设置为`GONE`或使用`ViewStud`. - 使用`Relativelayout`代替`LinearLayout`. 平时写布局的时候要多注意,写完后可以通过`Hierarchy Viewer`或在手机上通过开发者选项中的显示布局边界来查看是否有不必要的嵌套。 @@ -146,7 +172,26 @@ - 减少不必要的`Inflate` 如上一步中`stub.infalte()`后将该`View`进行记录或者是`ListView`中`item inflate`的时候。 - + +- 使用ConstraintLayout降低布局嵌套层级 + + - 实现几乎完全扁平化的布局 + - 构建复杂布局性能更高 + - 具有RelativeLayout和LinearLayout的特性 + +- 使用AsyncLayoutInflater异步加载对应的布局 + + - 工作线程加载布局 + - 回调主线程 + - 节省主线程时间 + + AsyncLayoutInflater是通过侧面缓解的方式去缓解布局加载过程中的卡顿,但是它依然存在一些问题: + + - 1、不能设置LayoutInflater.Factory,需要通过自定义AsyncLayoutInflater的方式解决,由于它是一个final,所以需要将代码直接拷处进行修改。 + - 2、因为是异步加载,所以需要注意在布局加载过程中不能有依赖于主线程的操作。 + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" index f75f78d3..fe4d4cdc 100644 --- "a/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" +++ "b/Architect/1.\346\236\266\346\236\204\347\256\200\344\273\213.md" @@ -21,7 +21,7 @@ #### 规划 -规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全民长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 +规划是系统架构中最重要的组成部分,是个人或者组织制定的比较全面长远的发展计划,是对未来整体性、长期性、基本性问题的思考和考量。设计未来整套行动的方案。很早就有规划这个概念了,例如:国家的十一五规划等。当然软件开发也和生活紧密联系,一个大型的系统也需要良好的规划,规划可以说是基石,是系统架构的前提。 系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 @@ -29,7 +29,7 @@ - +​ --- - 邮箱 :charon.chui@gmail.com - Good Luck! diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 1129070f..09bff58c 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -104,13 +104,45 @@ 然后,操作系统询问BIOS,以获得配置信息。对于每种设备,系统检查对应的设备驱动程序是否存在。如果没有,系统要求用户插入含有该驱动程序的CD-ROM(由设备供应商提供)或者从网络上下载驱动程序。一旦有了全部的设备驱动程序,操作系统就将它们调入内核。然后初始化有关表格,创建需要的任何背景进程,并在每个终端上启动登陆程序或GUI。 -如果是从普通硬盘启动,可简单列举为如下步骤: -1. X86 PC开机时CPU处于实模式,开机时会将cs=0xffff,ip=0x0000 -2. 寻址0xFFFF0(ROM BIOS映射区) -3. 检查RAM、键盘、显示器、磁盘、主板等硬件 -4. 将磁盘0磁道0扇区(操作系统引导扇区)读入0x7c00处 -5. 设置 cs=0x07c0,ip=0x0000开始执行 + +不同的处理器和硬解系统可能会采用不同的策略,但是通用系统的启动分为三个步骤: + +- 开机并执行bootloader程序 + + 开机就是给系统开始供电,此时硬件电路会产生一个确定的复位时序,保证CPU是最后一个被复位的器件。为什么CPU要最后被复位呢? 因为,如果CPU是第一个被复位,则当CPU复位后开始运行时,其他硬件内部的寄存器状态可能还没有准备好,比如磁盘或者内存,那这样就可能出现外围硬件初始化的错误。当正确完成复位后,CPU开始执行第一条指令,该指令所在的内存地址是固定的,这由CPU的制造者指定。不同的CPU可能会从不同的地址获取指令,但这个地址必须是固定的,这个固定的地址所保存的程序往往被称为“引导程序“(Bootloader),因为其作用是装载真正的用户程序。 + + 至于如何装载,则是一个策略问题,不同的CPU会提供不同的装载方式,比如有的是通过普通的并口存储器,有的则是通过SD卡,但是无论硬件上使用何种接口装载,装载过程必须提供以下信息,具体包括: + + - 从哪里读取用户程序? + - 用户程序的长度是多少? + - 装载完用户程序后,应该跳转到哪里,即用户程序的执行入口在哪里? + +- 操作系统内核初始化 + + 执行内核程序,这一步所说的内核程序在上一步中指的就是”用户程序“。因为从CPU的角度来看,除了Bootloader之外的所有程序都是用户程序,只是从软件的角度来看,用户程序被分为”内核程序“和”应用程序“,而本步执行的是内核程序。 + + 内核程序初始化时执行的操作包括,初始化各种硬件,包括内存、网络接口、显示器、输入设备、然后建立各种内部数据结构,这些数据结构将用于多线程调度及内存的管理等。当内核初始化完毕后就开始运行具体的应用程序了。在一般情况下,习惯于将第一个应用程序称为”Home程序“。 + +- 执行第一个应用程序 + + 运行Home程序,比如Windows系统的桌面。之所以称为Home程序,是因为通过该程序可以方便的启动其他应用程序。而传统的Linux系统启动后第一个运行的程序一般是一个Terminal。 + + + +### Android系统启动过程 + +目前的Android系统大多运行在ARM处理器之上。ARM本身是一个公司的名称,从技术的角度来看,它又是一种微处理器内核的架构。 + +对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,改段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 + +多数基于ARM的实际硬件系统,会从并口NAND Flash芯片上的0x00000000地址处装载程序。对于一些小型嵌入式系统而言,该地址中的程序就是最终要执行的用户程序;而对于Android而言,该地址中的程序还不是Android程序,而是一个叫做uboot或者fastboot的程序,其作用是初始化硬件设备,比如网口、SDRAM、RS232等,并提供一些调试功能,比如向NAND Flash中写入新的数据,这可用于开发过程中的内核烧写、升级等。 + +当uboot(fastboot)被装载后便开始运行,它一般会先检测用户是否按下了某些特别的按键,这些特别按键是uboot在编译时预先预定好的,用于进入调试模式。如果用户没有按这些特殊的按键,则uboot会从NAND Flash中装载Linux内核,装载的地址是在编译uboot时预先约定好的。 + +Linux内核被装载后,就开始进行内核初始化的过程。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_power_on_start.png) @@ -124,11 +156,11 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 -***累加寄存器***:存储执行运算的数据和运算后的数据。 +***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 ***标志寄存器***:存储运算处理后的CPU的状态。 -***程序计数器***:存储下一条指令所在内存的地址。 +***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 ***基址寄存器***:存储数据内存的起始地址。 @@ -136,7 +168,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 ***通用寄存器***:存储任意数据。 -***指令寄存器***:存储指令。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 ***栈寄存器***:存储栈区域的起始地址。 @@ -171,6 +203,69 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +假设目前一台机器的处理器包含一个累加器(AC)的数据寄存器,所有指令和数据长度均为16位,使用16位的单元或字来组织存储器。指令格式中有4位是操作码。操作码定义了处理器执行的操作。通过指令格式剩下的12位,来直接访问存储器。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_zhiling_1.png) + +如上图中的三个指令中的操作码描述了要执行的操作。 + +下图描述了程序的执行过程。给出的程序片段把地址为940的存储单元中的内容与地址为941的存储单元的内容相加,并将结果保存在后一个单元中。这需要三条指令。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process_demo.png) + +该图中,为把地址为940的存储单元中的内容与地址为941的存储单元中的内容相加,一共需要三个指令周期,每个指令周期都包含一个取指阶段和一个执行阶段。具体步骤为 : + +1. PC中包含第一条指令的地址为300,该指令内容(值为十六进制数1940)被送入指令寄存器IR中,PC增加1。注意,该处理过程中使用了存储器地址寄存器(MAR)和存储器缓冲寄存器(MBR)。为简单起见,这里未显示这些中间寄存器。 +2. IR中取出操作码,也就是最初的4位(对应到这里的十六进制,就是第一位数也就是1),而在上面1对应的二进制操作码是0001 = 从内存中载入AC。剩下的12位(后三个十六进制数)表示的是地址为940。所以这里的意思就是将存储器940地址的单元加载到AC中。 +3. 然后继续去下一条指令,PC现在是301了,所以要从地址为301的存储单元中取下一条指令,也就是(5941),同时PC增加1。这条指令是5941,最初的4位操作码是5,对应的二进制是0101,上面的操作码图标中0101 = 从内存中添加到AC。剩下的12位(后三个十六进制数)表示的是地址为941。 +4. 内存地址为941的存储单元中的内容与AC中以前的内容相加,结果保存在AC中。 +5. 接着读下一个指令,现在PC是302了,从302的存储单元中读取下一个指令为2941,PC同时加1.前四位操作码对应的是2,转换成二进制也就是0010 = 将AC存储到内存。后十二位对应的地址是941。所以这条指令的意思就是将AC中的内容存储到地址为941的存储单元中。 +6. 执行指令,将AC中的内容存储到地址为941的存储单元中。现在941存储单元的内容变成了5. + + + +### 中断 + + + + + +所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千哥不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时报出忙状态,以实现最大的效率。 + + + +利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出相应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 + +从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再回复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 + +为使用中断产生的情况,在指令周期中要增加一个中断阶段。在中断阶段,处理器检查是否有中断发生,即检查是否出现中断信号。若没有中断,处理器继续运行,并在取指周期取当前程序的下一条指令。若有中断,处理器挂起当前程序的执行,并执行一个中断处理程序。这个中断处理程序通常是操作系统的一部分,它确定中断的性质,并执行所需要的操作。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_zhiling_intercept.png) + + + +### 中断处理 + + + + + +当I/O设备完成一次I/O操作时,发生以下硬件事件: + +1. 设备给处理器发送一个中断信号。 +2. 处理器在响应中断前结束当前指令的执行。 +3. 处理器对中断进行测试,确定存在未响应的中断,并给提交中断的设备发送确认信号,确认信号允许该设备取消它的中断信号。 +4. 处理器需要准备把控制权转交给中断程序。首先,需要保存从中断点恢复当前程序所需要的信息,要求的最少信息包括程序状态字(PSW)和保存在程序计数器(PC)中的下一条要执行的指令地址,它们被压入系统控制栈。 +5. 处理器把相应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: +6. 在这一点,与被中断程序相关的程序计数器和PSW被保存到系统栈中,此外,还有一些其他信息被当做正在执行程序的状态的一部分。特别需要保存处理器寄存器的内容,因为中断处理程序可能会用到这些寄存器,因此所有这些值和任何其他状态信息都需要保存。 +7. 中断处理程序现在可以开始处理中断,其中包括检查与I/O操作相关的状态信息或其他引起中断的事件,还可能包括给I/O设备发送附加命令或应答。 +8. 中断处理结束后,被保存的寄存器值从栈中释放并恢复到寄存器中。 +9. 最后的操作是从栈中恢复PSW和程序计数器的值,因此下一条要执行的指令来自前面被中断的程序。 + + + + + ## 存储器 在任何一种计算机中,第二种主要部件都是存储器。在理想情况下,存储器应该极为迅速(快于执行一条指令,这样CPU就不会受到存储器的限制),充分大并且非常便宜。但是目前的技术无法同时满足这三个目标。 @@ -264,13 +359,41 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +## Linux系统 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/linux_archi.png) + + + +## Android系统 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_2.png) + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/![](https://raw.githubusercontent.com/CharonChui/Pictures/master/rtsp_rtp_rtcp.jpeg)) + + +- 应用和框架:应用开发者最关心这一层及访问低层服务的API。 +- Binder IPC:Binder进程间通信机制允许应用框架打破进程的界限来访问Android系统服务代码,从而允许系统的高层框架API与Android的系统服务进行交互。 +- Android系统服务:框架中大部分能够调用系统服务的接口都向开发者开放,以便开发者能够使用底层的硬件和内核功能。Android系统服务分为两部分:媒体服务处理播放和录制媒体文件,系统服务处理应用所需要的系统功能。 +- 硬件抽象层(HAL):HAL提供调用核心层设备驱动的标准接口,以便上层代码不需要关心具体驱动和硬件的实现袭击,Android的HAL与标准的HAL基本一致。 +- Linux内核:Linux内核已被裁剪到满足移动环境的需求。 +Android在Linux内核中增加了两个提升电源管理能力的新功能: 报警和唤醒锁。 +- 报警功能是在Linux内核中实现的,开发者可通过调用运行库中的报警管理器来进行操作。通过报警管理器,应用可以请求定时叫醒服务。报警管理器是内核服务,目的是让应用即使在系统休眠的情况下也能触发警告提醒。这就使得系统随时可以进入休眠状态以节省电能,即使有一个进程有需要被唤醒的服务。 +- 唤醒锁也可以阻止Android系统进入休眠模式。一个应用程序占有一下唤醒锁中的一个: + - full_wake_lock:处理器工作,屏幕亮,键盘亮。 + - partial_wake_lock:处理器工作,屏幕关,键盘关。 + - screen_dim_wake_lock:处理工作,屏幕暗,键盘关。 + - screen_bright_wake_lock:处理器工作,屏幕亮,键盘关。 +当应用要求被管理的外设保持供电时,会通过API请求对应的锁。若无唤醒锁存在,系统就会锁定并关闭设备以节省电能。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 316d0c47..83bfc7f2 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -22,7 +22,11 @@ 最后一部分是根本。执行上下文(execution context)又称为进程状态(process state),是操作系统用来管理和控制进程所需的内部数据。这种内部信息和进程是分开的,因为操作系统信息不允许被进程直接访问。上下文包括操作系统管理进程及处理器正确执行进程所需的所有信息,包括各种处理器寄存器的内容,如程序计数器和数据寄存器。它还包括操作系统使用的信息,如进程优先级及进程是否在等待I/O事件的完成。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process.png) +上图是一种进程管理方法。两个进程A和B存在与内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 + +图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同事,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 @@ -68,6 +72,14 @@ - 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 - 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_state.png) + +### 挂起态 + +当内存中的所有进程都处于堵塞态时,操作系统可把其中的一个进程置为挂起态,并将它移到磁盘。此时内存所释放的空间就可被调入的另一个进程使用。操作系统执行换出操作后,将进程取到内存中的方式有两种:接纳一个新近创建的进程,或调入一个此前挂起的进程。显然,操作系统更倾向于调入一个此前挂起的进程,并为它服务,而非增加系统的总负载数。 + +挂起进程等价于不在内存中的进程。不在内存中的进程,不论他是否在等待一个时间,都不能立即执行。 + ### 进程由哪几部分组成? 进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: @@ -161,23 +173,23 @@ 进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 +- 信号 - -高级通信方式 + 基本原来是:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 - 共享存储系统 -多台服务器访问同一个存储设备的同一分区 + 多台服务器访问同一个存储设备的同一分区 - 消息传递系统 -进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 + 进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 - 管道通信 -发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 -管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + 管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 @@ -230,10 +242,17 @@ ### 产生条件 -- 互斥条件 -- 请求保持 -- 不可剥夺 -- 环路条件 +死锁有三个必要条件: + +- 互斥。一次只有一个进程可以使用一个资源。其他进程不能访问已分配给其他进程的资源。 +- 占有且等待。当一个进程等待其他进程时,继续占有已分配的资源。 +- 不可抢占。不能强行抢占进程已占有的资源。 + +前三个条件都只是死锁存在的必要条件而非充分条件。要产生死锁,还需要第四个条件: + +- 循环等到。存在一个闭合的进程链,每个进程至少占有此链中下一个进程所需的一个资源。 + + ### 解决方法 @@ -244,6 +263,51 @@ +## Linux并发机制 + +Linux为进程间通信和同步提供了各种机制。这里只是几种。 + +- 管道 + + 管道是一个环形缓冲区,它允许两个进程以生产者/消费者的模型进行通信。因此,这是一个先进先出的队列,由一个进程写,由另一个进程度。 + +- 消息 + + 每个进程都有一个与之相关联的消息队列。当程序视图给一个满队列发送信息时,它会被堵塞。当进程视图从一个空队列读取消息时也回被堵塞。 + +- 共享内存 + + 这是虚存中由多个进程共享的一个公共内存块。进程读写共享内存所用的机器指令,与读写虚存空间的其他部分所用的指令相同。每个进程有一个只读或读写的权限。互斥约束不属于共享内存机制的一部分,但必须由使用共享内存的进程提供。 + +- 信号量 + + 信号量实际上是以集合的形式创建的,一个信号量集合中有一个或多个信号量。内核自动完成所有需要的操作,在所有操作完成前,任何其他进程都不能访问信号量。 + + 信号量由如下元素组成: + + - 信号量的当前值 + - 在信号量上操作的最后一个进程的进程ID + - 等待该信号量的值大于当前值的进程数 + - 等待该信号量的值为零的进程数 + +- 信号 + + 信号是用于向一个进程通知发生异步事件的机制。信号类似于硬件中断,但没有优先级,即内核公平地对待所有的信号。 + +- 自旋锁 + + 在Linux中保护临界区的常用技术是自旋锁。在同一个时刻,只有一个线程能够获得自旋锁。其他任何试图获得自旋锁的线程将一直进行尝试(即自旋),知道获得了该锁。 + + + +### Android进程间通信 + +虽然Linux内核包含很多用于进程间通信(IPC)的机制,但是Adnroid系统在IPC中仍然新增了一个连接器。连接器提供了一个轻量级的远程程序调用功能,它在内存和事务处理方面非常高效,非常适合嵌入式系统。 + +连接器被用来传递两个进程之间的交互。进程(客户端)组件发起一个调用,调用直接传递给位于内核的连接器,连接器将其传递给目标进程(服务器端)的目标组件,目标进程返回的结果通过连接器传递给发起调用的进程组件。 + + + ## Android进程结构 如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 @@ -254,7 +318,7 @@ -如上图,首先是init进程,它产生了一些底层守护进程。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 +如上图,首先是init进程,它产生了一些底层守护进程,init进程是所有用户进程的鼻祖。其中一个守护进程是zygote,它是高级Java语言进程的根。Android的init不以传统的方法运行shell,因为典型的Android设备没有本地控制台用于shell访问。作为替代,系统进程adbd监听请求shell访问的远程连接(例如通过USB),按要求为它们创建shell进程。因为Android大部分是用Java语言编写的,所以zygote守护进程以及由它启动的进程是系统的中心。由zygote启动的第一个进程称为system_server,它包含全部核心操作服务,其关键部分是电源管理、包管理、窗口管理和活动管理。 其他进程在需要的时候由zygote创建。这些进程中有一些是“持久的”进程,它们是基本操作系统的组成部分,例如phone进程中的电话栈,它必须保持始终运行。另外的应用程序进程将在系统运行的过程中按需创建和终止。 @@ -264,10 +328,65 @@ ### Zygote -Zygote是在启动时就运行在DVM上的一个进程。每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 +系统中运行的第一个Dalvik虚拟机程序叫做zygote,该名称的意义是“一个卵”,因为接下来的所有Dalvik虚拟机进程都是通过这个卵孵化出来的。Zygote是在启动时就运行在DVM上的一个进程。 + +zygote进程汇总包含两个主要模块,分别如下: + +- Socket服务端:该Socket服务端用于接收启动新的Dalvik进程的命令。 +- Framework共享类及共享资源。当zygote进程启动后,会装载一些共享的类及资源,其中共享类是在preload-classes文件中被定义,共享资源是在preload-resources中被定义。因为zygote进程用于孵化出其他Dalvik进程,因此,这些类和资源装载后,新的Dalvik进程就不需要再装载这些类和资源了,这也就是所谓的共享。 + + + +每当出现创建进程的请求时,Zygote就会产生一个新的DVM虚拟机。Zygote通过在内存中尽可能多的共享内容来最小化产生一个新DVM所消耗的时间。通常而言,许多应用程序都会使用核心库的类和相应的堆结构,而这些内容都是只读的。也就是说,被大部分应用程序使用的这些共享数据和类都是只读而不能改变的。因此,当Zygote加载时,就预加载和初始化了应用程序运行时可能用到的Java核心库和资源。当Zygote创建一个新的DVM时,这部分类不会被分配到新内存。Zygote只是简单地把子进程的这些内存页映射到父进程的相应位置。 实际上,几乎不需要更多的映射页。如果一个类被一个子进程自己的DVM改写,那么Zygote会将受影响的内存复制到子进程中。这种即写即复制的行为在使得最大化共享内存的同时,还能保证应用程序间不会相互影响,并在跨应用程序和进程的边界时保证安全性。 +zygote进程对应的具体程序是app_process,该程序存在于system/bin目录下,启动该程序的指令是在init.rc中进行配置的。 + + + + + +#### System Server进程 + +System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerSystemService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: + +- EntropyService:提供伪随机数 +- PowerManagerService:电源管理服务 +- ActivityManagerService:最核心的服务之一,管理Activity +- TelephonyRegistry:通过该服务注册电话模块的事件响应,比如重启、关闭、启动等 +- PackageManagerService:程序包管理服务 +- AccountManagerService:账户管理服务,是指联系人账户,而不是Linux系统账户 +- ContentService:ContentProvider服务,提供跨进程数据交互 +- BatteryService:电池管理服务 +- LightsService:自然光强度感应传感器服务 +- VibratorService:振动器服务 +- AlarmManagerService:定时器管理服务,提供定时提醒服务 +- WindowManagerService:Framework最核心的服务之一,负责窗口管理 +- BluetoothService:蓝牙服务 +- DevicePolicyManagerService:提供一些系统级别的设置及属性 +- StatusBarManagerService:状态栏管理服务 +- ClipboardService:系统剪切板服务 +- InputMethodManagerService:输入法管理服务 +- NetStatService:网络状态服务 +- NetworkManagementService:网络管理服务 +- ConnectivityService:网络连接管理服务 +- NotificationManagerService:通知栏管理服务 +- LocationManagerService:地理位置服务 +- AudioService:音频管理服务 +- .... + +SystemServer中创建了一个Socket客户端,并有AmS负责管理该客户端,之后所有的Dalvik进程都将通过该Socket客户端间接被启动。当需要启动新的APK进程时,AmS中会通过该Socket客户端向zygote进程的Socket服务端发送一个启动命令,然后zygote会孵化出新的进程。 + + + + + +从系统架构的角度来看,先创建一个zygote并加载共享类的资源,然后通过该zygote去孵化新的Dalvik进程,该架构的特点有两个: + +- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,必须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 +- zygote进程预先会装载共享类和共享资源,这些类及资源实际上就是SDK中定义的大部分类和资源,因此,当通过zygote孵化出新的进程后,新的APK进程只需要去装载APK自身包含的类和资源即可,这就有效的解决了多个APK共享Framework资源的问题。 + ## Android进程模型 @@ -302,6 +421,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 + + ### 进程生命周期 活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 @@ -317,7 +438,47 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj -## 进程和线程的区别 +### Android系统启动的核心流程如下: + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/Andriod-Boot-Process.jpg) + + + +1. 启动电源以及系统启动:当电源按下时引导芯片从预定义的地方(固化在ROM)开始执行(Boot ROM),Boot ROM会去加载引导程序BootLoader到RAM,然后执行。 + +2. 引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。 +3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 +4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 +5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 +6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManager、WindowManager、PackageManager、PowerManager等服务。 +7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 +8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 + + + +对于IPC(Inter-Process Communication, 进程间通信),Linux现有管道、消息队列、共享内存、套接字、信号量、信号这些IPC机制,Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。 + + + +## 线程 + +多线程技术是指把执行一个应用程序的进程划分为可以同时运行的多个线程。 + + + +### 进程和线程的区别 + +进程有如下两个特点: + +- 资源所有权:进程包括存放进程映像的虚拟地址空间,进程映像是程序、数据、栈和进程控制块中定义的属性集。进程总具有对资源的控制权和所有权,这些资源包括内存、I/O通道、I/O设备和文件。操作系统能提供预防进程间发生不必要资源冲突的保护功能。 +- 调度/执行:进程执行时采用一个或多程序的执行路径,不同进程的执行过程会交替执行。因此进程具有执行态和分配给其的优先级,是可被操作系统调度和分派的实体。 + +上面这两个特点是独立的,因此操作系统能分别处理它们。为了区分这两个特点,通常将分派的单位成为线程或轻量级进程,而将拥有资源所有权的单位成为进程或任务。 + +- 线程(thread):可分派的工作单元。它包括处理器上下文环境(包含程序计数器和栈指针)和栈中自身的数据区域。线程顺序执行且可以中断,因此处理器可以转到另一个线程。 +- 进程(process):一个或多个线程和相关系统资源(如包含程序和代码的存储空间、打开的文件和设备)的集合。它严格对应于一个正在执行的程序的概念。通过把一个应用程序分解成多个线程,程序员可以很大程度上控制应用程序的模块性及相关事件的时间安排。 线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 3a54aa0e..2490734b 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -245,11 +245,11 @@ Android包含了标准Linux内核中内存管理设施的许多扩展,具体 - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:ndorid基于oomKiller原理所扩展的一个多层次oomKiller,OOMkiller(Out Of Memory Killer)是在Linux系统无法分配新内存的时候,选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 - +- Low Memory Killer:andorid基于oomKiller原理所扩展的一个多层次oomKiller。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOMkille会选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 +Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 2a4e19c2..29367863 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -11,9 +11,22 @@ 执行I/O的三种技术: - 程序控制I/O:处理器代表一个进程给I/O模块发送一个I/O命令,该进程进入忙等待,直到操作完成才能继续执行。 + - 中断驱动I/O:处理器代表进程向I/O模块发出一个I/O命令。有两种可能性:若来自进程的I/O指令是非堵塞的,则处理器继续执行发出I/O命令的进程的后续指令。若I/O指令是堵塞的,则处理器执行的下一条指令来自操作系统,它将当前的进程设置为堵塞态并调度其他进程。 + - 直接存储器访问(DMA):一个DMA模块控制内存和I/O模块之间的数据交换。为传送一块数据,处理器给DMA模块发请求,且只有在整个数据块传送结束后,它才被中断。 + DMA技术工作流程如下: + + 如果处理器想读或写一块数据时,它通过想DMA模块发送以下信息来给DMA模块发出一条命令: + + - 请求读操作或写操作的信号,通过在处理器和DMA模块之间使用读写控制线发送。 + - 相关I/O设备地址,通过数据线发送。 + - 从存储器中读或向存储器中写的起始地址,在数据线上传送,并由DMA模块保存在其地址寄存器中。 + - 读或写的字数,也通过数据线传送,并由DMA模块保存在其数据计数寄存器中 + + 然后处理器继续执行其他工作,此时它已把这个I/O操作委托给DMA模块。DMA模块直接从存储器中或向存储器中逐字传送整块数据,并且数据不再需要通过处理器。传送结束后,DMA模块给处理器发送一个中断信号。因此,只有在传送开始和结束时才会用到处理器。 + ### 磁盘高速缓存 diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 0945dd88..47f61216 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -30,11 +30,9 @@ Android平台的虚拟机称为Dalvik,Dalvik VM(DVM)执行格式为Dalvik Exec 虚拟机运行在Linux内核的顶部,它依赖于底层的功能(如线程和底层的内存管理)。Dalvik核心类库的目的是,为那些使用标准Java编程的人员提供熟悉的开发环境,但它是专门为满足小型移动设备的需要而设计的。 - - 每个Android应用程序都运行在自己的进程中,有自己的Dalvik运行实例。Dalvik是可以在一台设备上高效执行多个副本的虚拟机。 - +当Java程序运行时,都是由一个虚拟机来解释Java的字节码,它将这些字节码翻译成本地CPU的指令码,然后执行。对Java程序而言,负责解释并执行的就是一个虚拟机,而对于Linux而言,这个进程只是一个普通的进程,它与一个只有一行代码的Hello World可执行程序无本质区别。所以启动一个虚拟机的方法就跟启动任何一个可执行程序的方法是相同的,那就是在命令行下输入可执行程序的名称,并在参数中指定要执行的Java类。而dalvikvm的作用就是创建一个虚拟机并执行参数中指定的Java类。 @@ -50,6 +48,39 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 +### DVM示例 + +下面以一个例子来说明dalvikvm的使用方法: + +1. 首先新建一个Foo.java文件,如下: + + ```java + class Foo { + public static void main(String[] args) { + System.out.println("Hello dalvik"); + } + } + ``` + +2. 然后编译该文件,并生成jar文件,如下: + + ```java + $ javac Foo.java + $ dx --dex --output=foo.jar Foo.class + ``` + + dx工具的作用是将.class转换为dex文件,因为Dalvik虚拟机所执行的程序不是标准的Jar文件,而是将jar经过特别的转换以提高执行效率,而在转换后就是dex文件。dx工具是Android源码的一部分,其路径是在out目录下。dx执行时,--output参数用于指定jar文件的输出路径,注意该jar文件内部包含已经不是纯粹的.class文件,而是dex格式文件,jar仅仅是zip包。 + +3. 生成了该jar包后,就可以把该jar包push到设备中,并执行以下命令: + + ```java + $ adb push foo.jar /data/app + $ adb shell dalvikvm -cp /data/app/foo.jar Foo + Hello dalvik + ``` + + 以上命令首先将jar包push到/data/app目录下,因为该目录一般用于存放应用程序,接着使用adb shell执行dalvikvm程序。dalvikvm的执行语法是: dalvikvm -cp 类路径 类名,从这里可以感觉到,dalvikvm的作用就像在pc上执行java程序一样。 + - [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" new file mode 100644 index 00000000..0a6b51a4 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -0,0 +1,199 @@ +# 1.Android进程间通信 + +## Binder简介 + +Binder,英文的意思是别针、回形针。我们经常用别针把两张纸”别“在一起,而在Android中,Binder用于完成进程间通信(IPC),即把多个进程”别“在一起。比如,普通应用程序可以调用音乐播放服务提供的播放、暂停、停止等功能。Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存。从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用完成的。 + + + +## Linux进程间通信 + +无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: + +- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; +- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信; +- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; +- 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信; +- 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 +- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; + +Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder。 IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。所以对于Android最上层架构而言,最常用的通信方式是: + +- Binder + +- Socket + + Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有: + + - zygote:用于孵化进程,system_server创建进程是通过socket向zygote进程发起请求; + - installd:用于安装App的守护进程,上层PackageManagerService很多实现最终都是交给它来完成; + - lmkd:lowmemorykiller的守护进程,Java层的LowMemoryKiller最终都是由lmkd来完成; + - adbd:这个也不用说,用于服务adb; + - logcatd:这个不用说,用于服务logcat; + - vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。 + + 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。Socket通信方式相对于binder比较简单,这里省略。 + +- Handler + + + +## Android为什么要使用Binder + +Binder作为Android系统提供的一种IPC机制。首先一个问题就是为什么Linux已经有那么多IPC通信的机制,Android还要用Binder。 + +**接下来正面回答这个问题,从5个角度来展开对Binder的分析:** + +**(1)从性能的角度** **数据拷贝次数:**Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 + +**(2)从稳定性的角度** +Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + +仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: + +**(3)从安全的角度** +传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 + +Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 + +针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但**同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。**对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 + +Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 + +**说到这,可能有人要反驳**,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在**Google举办的Android Bootcamp 2016**大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。 + +话题扯远了,继续说Binder。 + +**(4)从语言层面的角度** +大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 + +**另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。**也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在**Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制**,Android中的**Kill Process采用的signal(信号)机制**等等。而**Binder更多则用在system_server进程与上层App层的IPC交互**。 + +**(5) 从公司战略的角度** + +总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 + +而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。 + +**有了这些铺垫,我们再说说Binder的今世前缘** + +Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 + +另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 + +**综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。** + + + +**最后,简单讲讲Android Binder架构** + +Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制。 + +1. **Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。**例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; +2. **Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。**例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 + + + +**总之,一句话"无Binder不Android"。** + + + +前面人都说了Binder的优点,我来讲故事 + +1. 当年Andy Rubin有个公司 Palm 做掌上设备的 就是当年那种PDA 有个系统叫PalmOS 后来palm被收购了以后 Andy Rubin 创立了Android + +2. Palm收购过一个公司叫 Be 里面有个移动系统 叫 BeOS 进程通信自己学了个实现 叫Binder 由一个叫 Dianne Hackbod的人开发并维护 后来Binder 也被用到了 PalmOS里 + +3. Android创立了以后 Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 + +- 如果你是她,你会选择用a.Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 + +- 重写一个新东西 也不会 binder反正是自己写的开源库 + +- 用binder 已经被两个公司用过 而且是自己写的 可靠放心 + +我是她我就选C + +你可以看到 如果当年Dianne没有加入Be 或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 + + + +## Binder框架 + +Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。 + +- 服务端 + + 一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务就必须重载onTransact()方法。 + + 可以想象,重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时传入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。 + +- Binder驱动 + + 任何一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_archi.png) + +- 应用程序客户端 + + 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + + 从这里可以看出,对应用程序开发员来说,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。 + + + +## Binder IPC原理 + + + +每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 + + + +Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下所示: + +![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) + +可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,要掌握Binder机制,首先需要了解系统是如何首次[启动Service Manager](http://gityuan.com/2015/11/07/binder-start-sm/)。当Service Manager启动之后,Client端和Server端通信时都需要先[获取Service Manager](http://gityuan.com/2015/11/08/binder-get-sm/)接口,才能开始通信服务。 + +图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 + +1. **[注册服务(addService)](http://gityuan.com/2015/11/14/binder-add-service/)**:Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。 +2. **[获取服务(getService)](http://gityuan.com/2015/11/15/binder-get-service/)**:Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。 +3. **使用服务**:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。 + +图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。 + + + + + + + +### 2.3 C/S模式 + +BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代表,它们都从IBinder类中派生而来,关系图如下: + +![Binder关系图](http://gityuan.com/images/binder/prepare/Ibinder_classes.jpg) + +- client端:BpBinder.transact()来发送事务请求; +- server端:BBinder.onTransact()会接收到相应事务。 + + + + + +参考: https://www.zhihu.com/question/39440766/answer/93550572 + + + + + + +- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" new file mode 100644 index 00000000..df78f0cf --- /dev/null +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -0,0 +1,918 @@ +# 2.Android线程间通信之Handler消息机制 + + +Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线程间通信,Handler消息机制是由一组MessageQueue、Message、Looper、Handler共同组成的,为了方便且称之为Handler消息机制。 + +有人可能会疑惑,为何Binder/Socket用于进程间通信,能否用于线程间通信呢?答案是肯定,对于两个具有独立地址空间的进程通信都可以,当然也能用于共享内存空间的两个线程间通信,这就好比杀鸡用牛刀。接着可能还有人会疑惑,那handler消息机制能否用于进程间通信?答案是不能,Handler只能用于共享内存地址空间的两个线程间通信,即同进程的两个线程间通信。很多时候,Handler是工作线程向UI主线程发送消息,即App应用中只有主线程能更新UI,其他工作线程往往是完成相应工作后,通过Handler告知主线程需要做出相应地UI更新操作,Handler分发相应的消息给UI主线程去完成,如下图: + +![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/handler_thread_commun.jpg) + +由于工作线程与主线程共享地址空间,即Handler实例对象mHandler位于线程间共享的内存堆上,工作线程与主线程都能直接使用该对象,只需要注意多线程的同步问题。工作线程通过mHandler向其成员变量MessageQueue中添加新Message,主线程一直处于loop()方法内,当收到新的Message时按照一定规则分发给相应的handleMessage()方法来处理。所以说,Handler消息机制用于同进程的线程间通信,其核心是线程间共享内存空间,而不同进程拥有不同的地址空间,也就不能用handler来实现进程间通信。 + + + +消息机制主要包含: + +- Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;Message中有一个用于处理消息的Handler; +- MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);MessageQueue有一组待处理的Message; +- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue。 +- Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; + + +首先想一想平时我们是怎么使用的: +```java +private Handler mHandler = new Handler(new Handler.Callback() { + @Override + public boolean handleMessage(@NonNull Message msg) { + // xxxx. + return false; + } +}); + +mHandler.sendMessage(msg); +``` + +所以这里分析还是要从Handler开始,主要看它的构造函数和sendMessage()方法: + +## 1. Handler构造函数 +```java +final Looper mLooper; +final MessageQueue mQueue; +@UnsupportedAppUsage +final Callback mCallback; +final boolean mAsynchronous; +@UnsupportedAppUsage +IMessenger mMessenger; + +public Handler() { + this(null, false); +} + +public Handler(@Nullable Callback callback) { + this(callback, false); +} +// 可以设置Looper进来 +public Handler(@NonNull Looper looper) { + this(looper, null, false); +} + +public Handler(@Nullable Callback callback, boolean async) { + // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 + if (FIND_POTENTIAL_LEAKS) { + final Class klass = getClass(); + if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && + (klass.getModifiers() & Modifier.STATIC) == 0) { + Log.w(TAG, "The following Handler class should be static or leaks might occur: " + + klass.getCanonicalName()); + } + } + // 1. 如果不设置Looper对象进来,Looper会通过Looper.myLooper()获取Looper对象 + mLooper = Looper.myLooper(); + if (mLooper == null) { + throw new RuntimeException( + "Can't create handler inside thread " + Thread.currentThread() + + " that has not called Looper.prepare()"); + } + // 获取Looper中的MessageQueue + mQueue = mLooper.mQueue; + // 回调接口 + mCallback = callback; + // 设置消息是否为异步处理方式,默认都是同步的 + // If true, the handler calls {@link Message#setAsynchronous(boolean)} for + // each {@link Message} that is sent to it or {@link Runnable} that is posted to it. + // 如果是异步的会调用Message的setAsynchronous方法,然后在MessageQueue中会判断是不是异步的消息 + mAsynchronous = async; +} + +``` + +### 上面看到会通过Looper类的myLooper()获取Looper对象 + +```java +// sThreadLocal.get() will return null unless you've called prepare(). +@UnsupportedAppUsage +static final ThreadLocal sThreadLocal = new ThreadLocal(); + +public static @Nullable Looper myLooper() { + return sThreadLocal.get(); +} + +/** + * Return the {@link MessageQueue} object associated with the current + * thread. This must be called from a thread running a Looper, or a + * NullPointerException will be thrown. + */ +public static @NonNull MessageQueue myQueue() { + return myLooper().mQueue; +} +``` +上面注释写的很明白:除非你调用了prepare()方法,不然sThreadLocal.get()会返回null。那sThreadLocal是啥? prepare()方法又是干啥的?这里还是分两步: + +#### sThreadLocal + +ThreadLocal:线程本地存储区(Thread Local Storage,简称为TLS),每个线程都有自己的私有的本地存储区域,不同线程之间彼此不能访问对方的TLS区域。TLS常用的操作方法: +- ThreadLocal.set(T value):将value存储到当前线程的TLS区域: + + ```java + public void set(T value) { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) + map.set(this, value); + else + createMap(t, value); + } + ``` + +- ThreadLocal.get():获取当前线程TLS区域的数据: + + ```java + public T get() { + Thread t = Thread.currentThread(); + ThreadLocalMap map = getMap(t); + if (map != null) { + ThreadLocalMap.Entry e = map.getEntry(this); + if (e != null) { + @SuppressWarnings("unchecked") + T result = (T)e.value; + return result; + } + } + return setInitialValue(); + } + ``` + + +而sThreadLocal就是线程本地存储变量,它的意义就是在本线程内的任何对象内保持一致。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_1.png) + + + +#### prepare()方法 + +```java +public static void prepare() { + prepare(true); +} + +private static void prepare(boolean quitAllowed) { + if (sThreadLocal.get() != null) { + throw new RuntimeException("Only one Looper may be created per thread"); + } + // 这个参数是是否允许退出 + sThreadLocal.set(new Looper(quitAllowed)); +} + +final MessageQueue mQueue; +final Thread mThread; +private Looper(boolean quitAllowed) { + // 创建MessageQueue,并将quitAllowed传入MessageQueue的构造函数 + mQueue = new MessageQueue(quitAllowed); + // 保存当前的线程 + mThread = Thread.currentThread(); +} +``` +从代码上可以看到prepare()方法的作用是创建一个Looper对象,然后将该Looper对象保存到sThreadLocal中。 + +这里有点麻烦了,我们上面使用的代码中并没有调用Looper.prepare()方法啊,理论上这里应该是null,Handler是无法使用的,为什么我们还能正常使用Handler?Looper.prepare()究竟是什么时候调用的?那我们需要看一下prepare()和prepare(boolean quitAllowed)方法都有哪些地方调用了: + +```java +/** + * Initialize the current thread as a looper, marking it as an + * application's main looper. The main looper for your application + * is created by the Android environment, so you should never need + * to call this function yourself. See also: {@link #prepare()} + */ +public static void prepareMainLooper() { + prepare(false); + synchronized (Looper.class) { + if (sMainLooper != null) { + throw new IllegalStateException("The main Looper has already been prepared."); + } + sMainLooper = myLooper(); + } +} +``` + +发现在Looper类中有个prepareMainLooper()的方法,注释上面写的也比较清楚,说这是Android运行环境创建的应用程序的主looper,你不能自己调用这个方法,那我们看一下这个方法是从哪里调用的,ActivityThread类中的main函数,看到这个main函数,就知道这个类不简单, + +```java +/** + * This manages the execution of the main thread in an + * application process, scheduling and executing activities, + * broadcasts, and other operations on it as the activity + * manager requests. + * + * {@hide} + */ +public final class ActivityThread extends ClientTransactionHandler { + // .... + public static void main(String[] args) { + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); + + // Install selective syscall interception + AndroidOs.install(); + + // CloseGuard defaults to true and can be quite spammy. We + // disable it here, but selectively enable it later (via + // StrictMode) on debug builds, but using DropBox, not logs. + CloseGuard.setEnabled(false); + + Environment.initForCurrentUser(); + + // Make sure TrustedCertificateStore looks in the right place for CA certificates + final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId()); + TrustedCertificateStore.setDefaultUserDirectory(configDir); + + Process.setArgV0(""); + // 调用Looper.prepareMainLooper()方法 + Looper.prepareMainLooper(); + + // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line. + // It will be in the format "seq=114" + long startSeq = 0; + if (args != null) { + for (int i = args.length - 1; i >= 0; --i) { + if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { + startSeq = Long.parseLong( + args[i].substring(PROC_START_SEQ_IDENT.length())); + } + } + } + ActivityThread thread = new ActivityThread(); + thread.attach(false, startSeq); + + if (sMainThreadHandler == null) { + sMainThreadHandler = thread.getHandler(); + } + + if (false) { + Looper.myLooper().setMessageLogging(new + LogPrinter(Log.DEBUG, "ActivityThread")); + } + + // End of event ActivityThreadMain. + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + // Looper.loop()启动 + Looper.loop(); + + throw new RuntimeException("Main thread loop unexpectedly exited"); + } +} +``` + +##### ActivityThread类 + +从上面ActivityThread类的注释中可以看到说ActivityThread是应用程序的主线程。ActivityThread类是Android APP进程的初始类,它的main函数是这个APP进程的入口。 + +[ActivityThread类的详细介绍可以参考这篇文章](),这里就不细说ActivityThread类了,我们只需要知道Android运行环境会启动ActivityThread类,他是主线程,而ActivityThread类中的main函数会调用Looper.prepareMainLooper()和Looper.loop()方法。 + + + +到这里梳理一下上面的TODO: + +1. Looper类构造函数创建MessageQueue +2. ActivityThread调用了Looper.prepareMainLooper()后又调用了Looper.loop()方法,要分析Looper.loop()方法的实现。 + + + +#### MessageQueue的实现 + +```java +private Looper(boolean quitAllowed) { + mQueue = new MessageQueue(quitAllowed); + mThread = Thread.currentThread(); +} + +public final class MessageQueue { + private final boolean mQuitAllowed; + private long mPtr; // used by native code + // Message中的next指向了下一个Message + Message mMessages; + private boolean mBlocked; + + MessageQueue(boolean quitAllowed) { + mQuitAllowed = quitAllowed; + mPtr = nativeInit(); + } + + private native static long nativeInit(); + private native static void nativeDestroy(long ptr); + @UnsupportedAppUsage + private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + private native static void nativeWake(long ptr); + private native static boolean nativeIsPolling(long ptr); + private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); +} +``` + +MessageQueue中有一个native的方法; + +- nativeInit() + - 创建了NativeMessageQueue对象,增加其引用计数,并将NativeMessageQueue指针mPtr保存在Java层的MessageQueue + - 创建了Native Looper对象 + - 调用epoll的epoll_create()/epoll_ctl()来完成对mWakeEventFd和mRequests的可读事件监听 +- nativeDestroy()方法 + - 调用RefBase::decStrong()来减少对象的引用计数 + - 当引用计数为0时,则删除NativeMessageQueue对象 +- nativePollOnce + - 调用Looper::pollOnce()来完成,空闲时停留在epoll_wait()方法,用于等待事件发生或者超时 +- nativeWake + - 调用Looper::wake()来完成,向管道mWakeEventfd写入字符; + + + + + +#### Looper.loop()方法: + +```java +/** +* Run the message queue in this thread. Be sure to call +* {@link #quit()} to end the loop. +*/ +public static void loop() { + // 从sThreadLocal获取当前的Looper + final Looper me = myLooper(); + if (me == null) { + throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); + } + // 获取当前Looper的MessageQueue + final MessageQueue queue = me.mQueue; + + // 确保在权限检查时是基于本地进程,而不是调用进程 + // Make sure the identity of this thread is that of the local process, + // and keep track of what that identity token actually is. + Binder.clearCallingIdentity(); + final long ident = Binder.clearCallingIdentity(); + + // Allow overriding a threshold with a system prop. e.g. + // adb shell 'setprop log.looper.1000.main.slow 1 && stop && start' + final int thresholdOverride = + SystemProperties.getInt("log.looper." + + Process.myUid() + "." + + Thread.currentThread().getName() + + ".slow", 0); + + boolean slowDeliveryDetected = false; + // 不断去循环执行 + for (;;) { + // 去MessageQueue中的下一个Message,可能会堵塞 + Message msg = queue.next(); // might block + // 没有消息就退出该循环 + if (msg == null) { + // No message indicates that the message queue is quitting. + return; + } + + // This must be in a local variable, in case a UI event sets the logger + final Printer logging = me.mLogging; + if (logging != null) { + logging.println(">>>>> Dispatching to " + msg.target + " " + + msg.callback + ": " + msg.what); + } + + try { + // 分发Message,Message.target是Message中保存的当前handler对象,这里相对于调用Handler的dispatchMessage(msg)方法 + msg.target.dispatchMessage(msg); + if (observer != null) { + observer.messageDispatched(token, msg); + } + dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; + } catch (Exception exception) { + if (observer != null) { + observer.dispatchingThrewException(token, msg, exception); + } + throw exception; + } finally { + ThreadLocalWorkSource.restore(origWorkSource); + if (traceTag != 0) { + Trace.traceEnd(traceTag); + } + } + // 恢复调用者信息 + // Make sure that during the course of dispatching the + // identity of the thread wasn't corrupted. + final long newIdent = Binder.clearCallingIdentity(); + // 将Message放入消息池 + msg.recycleUnchecked(); + } +} + +// Looper的quit方法是调用MessageQueue.quit()方法 +public void quit() { + mQueue.quit(false); +} + + +public void quitSafely() { + mQueue.quit(true); +} +``` + +loop()进入循环模式,不断重复下面的操作,直到没有消息时退出循环 + +- 读取MessageQueue的下一条Message; +- 把Message分发给相应的target; +- 再把分发后的Message回收到消息池,以便重复利用。 + +#### Message类 + +这里我们需要看一下Message类中的target及recycleUncheked()方法: + +```java +public final class Message implements Parcelable { + // 消息类别 + public int what; + // 参数1 + public int arg1; + // 参数2 + public int arg2; + // 消息内容 + public Object obj; + // 消息响应方Handler + /*package*/ Handler target; + // 维护下一个消息 + /*package*/ Message next; + // 消息的回调方法 + /*package*/ Runnable callback; + public static final Object sPoolSync = new Object(); + // sPool也是Message对象, + private static Message sPool; + private static int sPoolSize = 0; + + private static final int MAX_POOL_SIZE = 50; + + public static Message obtain(Handler h) { + Message m = obtain(); + // 将handler赋值给target保存 + m.target = h; + + return m; + } + + public void recycle() { + if (isInUse()) { + if (gCheckRecycle) { + throw new IllegalStateException("This message cannot be recycled because it " + + "is still in use."); + } + return; + } + recycleUnchecked(); + } + + void recycleUnchecked() { + // Mark the message as in use while it remains in the recycled object pool. + // Clear out all other details. + flags = FLAG_IN_USE; + what = 0; + arg1 = 0; + arg2 = 0; + obj = null; + replyTo = null; + sendingUid = UID_NONE; + workSourceUid = UID_NONE; + when = 0; + target = null; + callback = null; + data = null; + + synchronized (sPoolSync) { + // 如果当前消息池小于最大的数量限制,就把消息放到消息池中 + if (sPoolSize < MAX_POOL_SIZE) { + next = sPool; + // 把新放入的Message加到链表的表头 + sPool = this; + sPoolSize++; + } + } + } +} +``` + + + +## 2. Handler.sendMessage(Message)和dispatchMessage(Message)方法 + +```java +public void handleMessage(@NonNull Message msg) { +} + +/** + * Handle system messages here. + */ +public void dispatchMessage(@NonNull Message msg) { + if (msg.callback != null) { + // 如果Message存在回调方法,就回调Message.callback.run()方法 + handleCallback(msg); + } else { + if (mCallback != null) { + // 如果Message没有回调方法,而Handler对象中设置了Callback接口,这里就回调Callback.handleMessage(msg)方法 + if (mCallback.handleMessage(msg)) { + return; + } + } + // handler自己的handleMessage方法,默认空实现,子类可重写该方法 + handleMessage(msg); + } +} + +public final boolean sendEmptyMessageDelayed(int what, long delayMillis) { + Message msg = Message.obtain(); + msg.what = what; + return sendMessageDelayed(msg, delayMillis); +} + +private static Message getPostMessage(Runnable r) { + Message m = Message.obtain(); + m.callback = r; + return m; +} + +public final void removeCallbacksAndMessages(@Nullable Object token) { + mQueue.removeCallbacksAndMessages(this, token); +} +``` + + + +## MessageQueue类的常用方法 + +MessageQueue非常重要,因为quit、next等都最终调用的是MessageQueue类。 + + + +```java +public final class MessageQueue { + private final boolean mQuitAllowed; + private long mPtr; // used by native code + // 消息在MessageQueue中使用Message表示,而Message包含一个next变量,该变量指向下一个消息 + // 所以队列中的消息以链表的结构进行保存 + Message mMessages; + + // 获取MessageQueue的下一个Message + Message next() { + // 如果MessageQueue已退出就直接返回 + // Return here if the message loop has already quit and been disposed. + // This can happen if the application tries to restart a looper after quit + // which is not supported. + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + int pendingIdleHandlerCount = -1; // -1 only during first iteration + int nextPollTimeoutMillis = 0; + for (;;) { + if (nextPollTimeoutMillis != 0) { + Binder.flushPendingCommands(); + } + // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 + // 也就是在C环境中创建了一个NativeMessageQueue数据对象。该方法的第一个参数是int型变量,在C环境中该变量 + // 会被强制转换为一个NativeMessageQueue对象。如果消息队列中没消息,当前线程会被挂起。 + // 堵塞操作,当等待nextPollTimeoutMillis时长或消息队列别唤醒,都会返回 + // nextPollTimeoutMillis代表下一个消息到来前,还需要等待的时长;当nextPollTimeoutMillis = -1时,表示消息队列中无消息,会一直等待下去。 + nativePollOnce(ptr, nextPollTimeoutMillis); + + synchronized (this) { + // Try to retrieve the next message. Return if found. + final long now = SystemClock.uptimeMillis(); + Message prevMsg = null; + Message msg = mMessages; + + if (msg != null && msg.target == null) { + // 当当前的消息的Handler为空时,就查询异步消息 + // Stalled by a barrier. Find the next asynchronous message in the queue. + do { + prevMsg = msg; + msg = msg.next; + } while (msg != null && !msg.isAsynchronous()); + } + if (msg != null) { + // 仅仅是为了判断消息所指定的执行时间是否到了,如果到了就返回该消息 + if (now < msg.when) { + // Next message is not ready. Set a timeout to wake up when it is ready. + nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); + } else { + // Got a message. + mBlocked = false; + if (prevMsg != null) { + prevMsg.next = msg.next; + } else { + mMessages = msg.next; + } + msg.next = null; + if (DEBUG) Log.v(TAG, "Returning message: " + msg); + // 设置消息的使用状态,即FLOAG_IN_USE的flag + msg.markInUse(); + return msg; + } + } else { + // No more messages. + nextPollTimeoutMillis = -1; + } + + // Process the quit message now that all pending messages have been handled. + if (mQuitting) { + dispose(); + return null; + } + + // If first time idle, then get the number of idlers to run. + // Idle handles only run if the queue is empty or if the first message + // in the queue (possibly a barrier) is due to be handled in the future. + if (pendingIdleHandlerCount < 0 + && (mMessages == null || now < mMessages.when)) { + pendingIdleHandlerCount = mIdleHandlers.size(); + } + if (pendingIdleHandlerCount <= 0) { + // No idle handlers to run. Loop and wait some more. + mBlocked = true; + continue; + } + + if (mPendingIdleHandlers == null) { + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + // 空闲回调函数 + // Run the idle handlers. + // We only ever reach this code block during the first iteration. + for (int i = 0; i < pendingIdleHandlerCount; i++) { + final IdleHandler idler = mPendingIdleHandlers[i]; + mPendingIdleHandlers[i] = null; // release the reference to the handler + + boolean keep = false; + try { + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf(TAG, "IdleHandler threw exception", t); + } + + if (!keep) { + synchronized (this) { + mIdleHandlers.remove(idler); + } + } + } + + // Reset the idle handler count to 0 so we do not run them again. + pendingIdleHandlerCount = 0; + + // While calling an idle handler, a new message could have been delivered + // so go back and look again for a pending message without waiting. + nextPollTimeoutMillis = 0; + } + } + + // 添加一条消息到消息队列 + boolean enqueueMessage(Message msg, long when) { + // 每条消息必须有一个target + if (msg.target == null) { + throw new IllegalArgumentException("Message must have a target."); + } + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + synchronized (this) { + if (mQuitting) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w(TAG, e.getMessage(), e); + msg.recycle(); + return false; + } + + msg.markInUse(); + msg.when = when; + Message p = mMessages; + boolean needWake; + if (p == null || when == 0 || when < p.when) { + // New head, wake up the event queue if blocked. + msg.next = p; + mMessages = msg; + // 如果是堵塞的,这里就需要唤醒 + needWake = mBlocked; + } else { + // Inserted within the middle of the queue. Usually we don't have to wake + // up the event queue unless there is a barrier at the head of the queue + // and the message is the earliest asynchronous message in the queue. + needWake = mBlocked && p.target == null && msg.isAsynchronous(); + Message prev; + for (;;) { + prev = p; + p = p.next; + if (p == null || when < p.when) { + break; + } + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + msg.next = p; // invariant: p == prev.next + prev.next = msg; + } + + // We can assume mPtr != 0 because mQuitting is false. + if (needWake) { + // 内部会将mMessages消息添加到C环境中的消息队列中,并且如果消息线程正处于挂起状态,则唤醒该线程 + nativeWake(mPtr); + } + } + return true; + } + + + + void quit(boolean safe) { + if (!mQuitAllowed) { + throw new IllegalStateException("Main thread not allowed to quit."); + } + + synchronized (this) { + if (mQuitting) { + return; + } + mQuitting = true; + + if (safe) { + removeAllFutureMessagesLocked(); + } else { + removeAllMessagesLocked(); + } + + // We can assume mPtr != 0 because mQuitting was previously false. + nativeWake(mPtr); + } + } + + // 清除所有的message + private void removeAllMessagesLocked() { + Message p = mMessages; + while (p != null) { + Message n = p.next; + p.recycleUnchecked(); + p = n; + } + mMessages = null; + } + + void removeCallbacksAndMessages(Handler h, Object object) { + if (h == null) { + return; + } + + synchronized (this) { + Message p = mMessages; + // 从消息队列的头开始,移除所有符合条件的消息 + // Remove all messages at front. + while (p != null && p.target == h + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + p.recycleUnchecked(); + p = n; + } + // 移除剩余符合条件的消息 + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && (object == null || n.obj == object)) { + Message nn = n.next; + n.recycleUnchecked(); + p.next = nn; + continue; + } + } + p = n; + } + } + } +``` + + + + + + + +# Handler内存泄露 + + + +有关内存泄露请猛戳[内存泄露][1] + +在上面分析Handler类源码时,其构造函数中第一部分的代码就是 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露。那为什么会导致内存泄露呢? + +```java +Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something. + } +} +``` +当我们这样创建`Handler`的时候`Android Lint`会提示我们这样一个`warning: In Android, Handler classes should be static or leaks might occur.`。 + +一直以来没有仔细的去分析泄露的原因,先把主要原因列一下: +- `Android`程序第一次创建的时候,默认会创建一个`Looper`对象,`Looper`去处理`Message Queue`中的每个`Message`,主线程的`Looper`存在整个应用程序的生命周期. +- `Hanlder`在主线程创建时会关联到`Looper`的`Message Queue`,`Message`添加到消息队列中的时候`Message(排队的Message)`会持有当前`Handler`引用, +当`Looper`处理到当前消息的时候,会调用`Handler#handleMessage(Message)`.就是说在`Looper`处理这个`Message`之前, +会有一条链`MessageQueue -> Message -> Handler -> Activity`,由于它的引用导致你的`Activity`被持有引用而无法被回收 +- **在java中,no-static的内部类会隐式的持有当前类的一个引用。static的内部类则没有。** + +## 具体分析 +```java +public class SampleActivity extends Activity { + + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 发送一个10分钟后执行的一个消息 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { } + }, 600000); + + // 结束当前的Activity + finish(); +} +``` +在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. + +## 解决方法 +- 通过程序逻辑来进行保护。 + - 如果`Handler`中执行的是耗时的操作,在关闭`Activity`的时候停掉你的后台线程。线程停掉了,就相当于切断了`Handler`和外部连接的线, + `Activity`自然会在合适的时候被回收。 + - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 + - 关于`Handler.remove*`方法 + - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 + - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 + - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 + - `removeMessages(int what)` ——按what来匹配 + - `removeMessages(int what, Object object)` ——按what来匹配 + 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; +- 将`Handler`声明为静态类。 + 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 + +```java +public class MyActivity extends Activity { + private MyHandler mHandler; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new MyHandler(this); + } + + @Override + protected void onDestroy() { + // Remove all Runnable and Message. + mHandler.removeCallbacksAndMessages(null); + super.onDestroy(); + } + + static class MyHandler extends Handler { + // WeakReference to the outer class's instance. + private WeakReference mOuter; + + public MyHandler(MyActivity activity) { + mOuter = new WeakReference(activity); + } + + @Override + public void handleMessage(Message msg) { + MyActivity outer = mOuter.get(); + if (outer != null) { + // Do something with outer as your wish. + } + } + } +} +``` + +[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" new file mode 100644 index 00000000..100b61d7 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -0,0 +1,105 @@ +# 3.Android Framework框架 + +## Framework框架 + +Framework定义了客户端组件和服务端组件功能及接口。后面的阐述中,”应用程序“一般是指”.apk“程序。 + +框架中包含三个主要部分,分别是服务端、客户端和Linux驱动。 + + + +### 服务端 + +服务端主要包含两个重要类,分别是WindowManagerService(WmS)和ActivityManagerService(AmS)。 + +WmS的作用是为所有应用程序分配窗口,并管理这些窗口。包括分配窗口的大小,调节各窗口的叠放次序,隐藏或显示窗口。 + +AmS的作用是管理所有应用程序中的Activity。 + +除此之外,在服务端还包括两个消息处理类: + +- KeyQ类:该类为WmS的内部类,继承与KeyInputQueue类,KeyQ对象一旦创建,就立即启动一个线程,该线程会不断地读取用户的UI操作信息,比如按键、触摸屏、trackball、鼠标等,并把这些消息放到一个消息队列QueueEvent类中。 +- InputDispatcherThread类:该类的对象一旦创建,也会立即启动一个线程,该线程会不断地从QueueEvent中取出用户消息,并进行一定的过滤,过滤后,再将这些消息发送给当前活动的客户端程序中。 + + + +### 客户端 + + + +客户端主要包括以下重要类: + +- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。 +- Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 +- PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 +- Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 +- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并相应特定的按键消息等。 +- ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 +- W类:该类继承于Binder,并且是ViewRoot的一个内部类。 +- WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 + + + +### Linux驱动 + +Linux驱动和Framework相关的主要包含两部分: + +- SurfaceFlingger(SF)驱动:每一个窗口都对应一个Surface,SF驱动的作用是把各个Surface显示在同一个屏幕上。 +- Binder驱动:Binder驱动的作用是提供进程间的消息传递。 + + + +## APK程序的运行过程 + +1. 首先,ActivityThread从main()函数开始执行,调用prepareMainLooper()为UI线程创建一个消息队列(MessageQueue)。 +2. 创建一个ActivityThread对象,在ActivityThread的初始化代码中会创建一个H(Handler)对象和一个ApplicationThread(Binder)对象。其中Binder负责接收远程AmS的IPC调用,接收到调用后,则通过Handler把消息发送到消息队列,UI主线程会异步地从消息队列中取出消息并执行相应的操作,比如start、stop、pause等。 +3. UI主线程调用Looper.loop()方法进入消息循环体,进入后就会不断地从消息队列中读取并处理消息。 +4. 当ActivityThread接收到AmS发送发送start某个Activity后,就会创建指定的Activity对象。Activity又会创建PhoneWindow类 ==》 DecorView类 ==》 创建相应的View或ViewGroup。创建完成后,Activity需要把创建好的界面显示到屏幕上,于是调用WindowManager类,后者于是创建一个ViewRoot对象,该对象实际上创建了ViewRoot类和W类,创建ViewRoot对象后,WindowManager再调用WmS提供的远程接口完成添加一个窗口并显示到屏幕上。 +5. 接下来,用户开始在程序界面上操作。KeyQ线程不断把用户消息存储到QueueEvent队列中,InputDispatch而Thread线程逐个去除消息,然后调用WmS中的相应函数处理该消息。当WmS发现该消息属于客户端某个窗口时,就会调用相应窗口的W接口。W类是一个Binder,负责接收WmS的IPC调用,并把调用消息传递给ViewRoot,ViewRoot再把消息传递给UI主线程ActivityThread,ActivityThread解析该消息并作出相应的处理。在客户端程序中,首先处理消息的是DecorView,如果DecorView不想处理某个消息,则可以将该消息传递给其内部包含的子View或者ViewGroup,如果还没有处理,则传递给PhoneWindow,最后再传递给Activity。 + + + +上面启动完后会有几个线程? 每个Binder对象都对应一个线程,Activity启动后会创建一个ViewRoot.W对象,同时ActivityThread会创建一个ApplicationThread对象,这两个对象都继承于Binder,因此会启动两个线程,负责接收Binder驱动发送IPC调用。最后一个主要线程也就是程序本身所在的线程,也叫做用户交互(UI)线程,因为所有的处理用户消息,以及绘制界面的工作都在该线程中完成。 + + + +## 窗口相关概念 + +窗口、Window类、ViewRoot类以及W类的区别和联系: + +- 窗口(Window):这是一个纯语义的说法,即程序员所看到的屏幕上的某个独立的界面,比如一个带有TitleBar的Activity界面、一个对话框、一个Menu菜单等,这些都称之为窗口。从WmS的角度来讲,窗口是接收用户消息的最小单元,WmS内部用特定的类表示一个窗口,以实现对窗口的管理。WmS接收到用户消息后,首先要判断这个消息属于哪个窗口,然后通过IPC调用把这个消息传递给客户端的ViewRoot类。 +- Window类:该类在android.view包中,是一个abstract类,该类是对包含有可视界面的窗口的一种包装,所谓的可视界面就是指各种View或者ViewGroup,一般可以通过res/layout目录下的xml文件描述。 +- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。 +- W类:该类是ViewRoot类的一个内部类,继承于Binder,用于想WmS提供一个IPC接口,从而让WmS控制窗口客户端的行为。 + +描述一个窗口之所以有这么多类的原因在于,窗口的概念存在于客户端和服务端(WmS)之中,客户端所理解的窗口和服务端理解的窗口是不同的,因此,在客户端和服务端会用不同的类来描述窗口。比如在客户端,用户能看到的窗口一般是View或者ViewGroup组成的窗口,而与Activity对应的窗口却是一个DecorView类,而具备常规Phone操作的接口却又是一个PhoneWindow类。所以无论是在客户端还是服务端,对窗口都有不同层面的抽象。 + + + +## Context + + + +Context在应用程序开发中会经常被使用,在一般的计算机书记中,Context被翻译为“上下文”,但是在Android中感觉翻译为“场景”更容易理解一些。 + + + +一个Context意味着一个场景,一个场景就是用户和操作系统交互的一种过程。比如当你打电话时,场景包括电话程序对应的界面以及隐藏在界面后的数据。当你看短信时,场景包括短信界面以及隐藏在后面的数据。这也就是为什么一个Activity就是一个Context,一个Service也是一个Context。因为Android程序员把“场景”抽象为Context类,他们认为用户和操作系统的每一次交互都是一个场景,比如打电话、发短信,这些都是有界面的场景,还有一些没有界面的场景,比如后台运行的服务(Service)。一个应用程序可以认为是一个工作环境,用户在这个工作环境汇总会切换到不同的场景,这就像一个前台秘书,她可能需要接待客人,可能要打印文件,还可能要接听客户电话,而这些就称之为不同的场景,前台秘书可称之为一个应用程序。 + + + +### Context的创建 + +在创建Application、Activity、Service时,AmS通过远程调用到ActivityThread的bindApplication()方法或ActivityThread的scheduleLaunchActivity()或ActivityThread.scheduleCreateService()方法,这里面会去创建ContextImpl并初始化。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..0a5b9007 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,148 @@ +# 4.ActivityManagerService简介 + +ActivityManagerService.java文件,简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService.java和View.java。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 + +AmS所提供的主要功能包括以下几项: + +- 统一调度各应用程序的Activity。应用程序要运行Activity,会首先报告给AmS,然后由AmS决定该Activity是否可以启动,如果可以,AmS再通知应用程序运行指定的Activity。换句话说,运行Activity是各应用程序的内政,AmS并不干预,但是AmS必须知道应用进程都运行了哪些Activity。 +- 内存管理。Android官方声称,Activity退出后,其所在的进程并不会被立即杀死,从而下次在启动该Activity时能够提高启动速度。这些Activity只有当系统内存紧张时,才会被自动杀死,应用程序不用关心这个问题。而这些正是AmS中完成的。 +- 进程管理。AmS向外提供了查询系统正在运行的进程信息的API。 + + + +## Activity调度机制 + + + +在Android中,Activity调度的基本思路是:各应用进程要启动新的Activity或者停止当前的Activity,都要首先报告给AmS,而不能“擅自处理”。AmS在内部为所有应用程序都做了记录,当AmS接到启动或停止的报告时,首先更新内部记录,然后再通知相应客户进程运行或者停止指定的Activity。由于AmS内部有所有Activity的记录,也就理所当然地能够调度这些Activity,并根据Activity和系统内存的状态自动杀死后台的Activity。 + +具体的讲,启动一个Activity有以下集中方式: + +- 在应用程序中调用startActivity()启动指定的Activity。 +- 在Home程序中单击一个应用图标,启动新的Activity。 +- 按Back键,结束当前Activity,自动启动上一个Activity。 +- 长按Home键,显示出当前任务列表,从中选择一个启动。 + +这四种启动方式的主体处理流程都会按照第一种启动方式运行,后三种方式只是在前端消息处理上各有不同。 + +AmS中定义了几个重要的数据类,分别用来保存进程(Process)、活动(Activity)和任务(Task)。 + +### 进程数据类ProcessRecord + +ProcessRecord记录一个进程中的相关信息,该类中内部变量可以分为三个部分: + +- 进程文件信息:也就是与该进程对应的APK文件的内部信息,例如ApplicationInfo、processName等。 +- 该进程的内存状态信息:这些信息将用于Linux系统的Out Of Memory情况的处理,当发生系统内部不够用时,Linux系统会根据进程的内存状态信息,杀掉优先级比较低的进程。 +- 进程中包含的Activity、Provider、Service等:如ArrayList activities; + +### HistoryRecord数据类 + +AmS中使用HistoryRecord数据类来保存每个Activity的信息,这里非常奇怪,Activity本身也是一个类,为什么还要用HistoryRecord来保存Activity的信息,而不是直接用Activity呢?这是因为Activity是具体的功能类,这就好比每一个读者都是一个Activity,而学校要为每一个读者建立一个档案,这些档案中并不包含每个读者具体的学习能力,而只是学生的籍贯信息、姓名、出生日期等,HistoryRecord就是AmS为每一个Activity建立的档案,该数据类中的变量主要包含两部分: + +- 环境信息:该Activity的工作环境,比如隶属于哪个Package,所在的进程名称、文件路径、数据路径、图标主题等。 +- 运行状态信息:比如idle、stop、finishing等,这些变量一般问boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 + +HistoryRecord类也是一个Binder,它基于IApplication.Stub类,因此它可以被IPC调用,一般是在WmS中进行该对象的IPC调用。 + + + +### TaskRecord类 + +AmS中使用任务的概念确保Activity启动和退出的顺序。比如如下启动流程,A、B、C分别代表三个应用程序,数字1、2、3分别代表该应用中的Activity。 + +A1 -> A2 -> A3 -> B1 -> B2 -> C1 -C2,此时应该处于C2,如果AmS中没有任务的概念,此时又要从C2启动B1,那么会存在以下两个问题: + +- 虽然程序上是要启动B1,但是用户可能期望启动B2,因为B1和B2是两个关联的Activity,并且B2已经运行于B1之后。如何提供给程序员一种选择,虽然指定启动B1,但如果B2已经运行,那么就启动B2. +- 假设已经成功从C2跳转到B2,此时如果用户按Back键,是应该回到B1呢,还是应该回到C2? + +任务概念的引入正是为了解决以上两个问题,HistoryRecord中包含一个init task变量,保存该Activity所属哪个任务,程序员可以使用Intent.FLAG_NEW_TASK标识告诉AmS为启动的Activity重新创建一个Task。 + +有了Task的概念后,以上情况就会是这样: + +虽然程序明确指定从C2启动到B1,程序员可以在intent的FLAG中添加NEW_TASK标识,从而使得AmS会判断B1是否已经在mHistory中。如果在,则找到B1所在的Task,并从该Task中的最上面的Activity出运行,此处也就是B2.当然,如果程序的确要启动B1,那么就不要使用NEW_TASK标识,使用的话,mHistory中会有两个B1记录,隶属于不同的Task。 + +TaskRecord类内部变量如下: + +- taskId:每一个任务对应一个Int型的标识 +- intent:创建该任务对应的intent +- numActivities:该任务中的Activity数目 + +TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayList或者HistoryRecord[]之类的变量,这意味着不能直接通过任务id找到其所包含的Activity。 + + + +### AmS中调度相关常量 + +- MAX_ACTIVITIES = 20; + + 系统只能有一个Activity处于执行状态,对于非执行状态的Activity,AmS会在内部暂时缓存起来,而不是立即杀死,但如果后台的Activity超过该常量,则会强制杀死一些优先级较低的Activity。 + +- PAUSE_TIMEOUT = 500; + + 当AmS通知应用程序暂停指定的Activity时,AmS的忍耐是有限的,因为只有500毫秒,如果应用程序在该常量时间内还没有暂停,AmS会强制暂停并关闭该Activity。这就是为什么不能在onPause()中做过多事情的原因。 + +- LAUNCH_TIMEOUT = 10 * 1000:当AmS通知应用程序启动某个Activity时,如果超过10s,AmS就会放弃 + +- PROC_START_TIMEEOUT = 10 * 1000:当AmS启动某个客户进程后,客户进程必须在10秒之内报告AmS自己已经启动,否则AmS会认为指定的客户进程不存在。 + +### 等待序列 + +由于AmS采用Service机制运作,所有的客户进程要做什么事情,都要先请求AmS,因此,AmS内部必须有一些消息序列保存这些请求,并按顺序依次进行相应的操作: + +- final ArrayList mHistory = new ArrayList(); + + 这是最最重要的内部变量,该变量保存了所有正在运行的Activity,所谓正在运行是指该HistoryRecord的finishing状态为true。比如当前和用户交互的Activity属于正在运行,从A1启动到A2,尽管A1看不见了,但是仍然是正在运行。从A2按Home键回到桌面,A2也是正在运行,但如果从A2按Back键回到A1,这时A2就不是正在运行状态了,它会从mHistory中删除掉 。 + +- private final ArrayList mLRUActivities = new ArrayList(); + + LRU代表Latest Recent Used,即最近所用的Activity的列表,它不像mHistory仅保存正在运行的Activity,mLRUActirity会保存所有过去启动过的Activity。 + +- final ArrayList mPendingActivityLaunches = new ArrayList(); + + 当AmS内部还没有准备好时,如果客户进程请求启动某个Activity,那么会被暂时保存到该变量中,这也就是pending的含义。这中情况一般发生在系统启动时,系统进程会查询系统中所有属性为Persisitant的客户进程,此时由于AmS也正在启动,因此,会暂时保存这些请求。 + +- final ArrayList mStoppingActivitiies; + + 在AmS的设计中,有这样一个理念:优先启动,其次再停止。即当用户请求启动A2时,如果A1正在运行,AmS首先会暂停A1,然后启动A2.当A2启动后再停止A1.在这个过程中,A1会被临时保存到mStoppingActivities中,直到A2启动后并处于空闲时,再回过头来停止mStoppingActivities中保存的HistoryRecord列表。 + +- final ArrayList mFinishingActivities; + + 和mStoppingActivities类似,当AmS认为某个Activity已经处于finish状态时,不会立即杀死该Activity,而是会保存到该变量中,直到超过系统设定的警戒线后,才去回收该变量中的Activity。 + +- HistoryRecord mPausingActivity + + 正在暂停的Activity,该变量只有在暂停某个Activity时才有值,代表正在暂停的Activity + +- HistoryRecord mResumedActivity + + 当前正在运行的Activity,这里的正在运行不一定是正在与用户交互。比如当用户请求执行A2时,当前正在运行的A1,此时AmS会首先暂停A1,而在暂停的过程中,AmS会通知WmS暂停获取用户消息,而此时mResumedActivity依然是A1. + +- HistoryRecord mFocusedActivity + + 这里的Focus并非是正在和用户交互,而是AmS通知WmS应该和用户交互的Activity,而在WmS真正处理这个消息之前,用户还是不能和该Activity交互。 + +- HistoryRecord mLastPausedActivity + + 上一次暂停的Activity + + + +当系统内存低时,AmS会要求客户端释放内存,而可能会释放Surface对应的内存,而这是由WmS具体完成的。当WmS释放了Surface内存后,该Surface对应的窗口就无效了,则该窗口对应的Activity也就无效了,则Activity所在的进程也就无效了。 + + + +## startActivi()启动流程 + + + +- 调用startActivity()方法后,通知到AmS,AmS收到请求startActivity()后,会首先暂停当前的Activity,因此这时候AmS需要判断mResumeActivity是否为空,也就是当前有没有正在运行的activity。一般情况下,该值都不会为空。 +- 如果为空的话继续往下走,如果不为空的话,AmS会通知当前mResumeActivity对应的Activity所在的进程暂停,然后AmS就不管了,当那个进程暂停完后会报告AmS,这时AmS开始执行completePaused()。该方法中会去检查要启动的目标Activity是否存在mHistory列表中,如果存在说明目标进程还在运行,只是目标Activity处于stop状态,还没有finish,所以会通知B进程则通过handleResumeActivity()方法来resume目标Activity。 +- 如果不存在那需要去检查目标Activity所在的进程是否存在。如果不存在则必须首先启动对应的进程。这时AmS调用Process进程类启动一个新的进程,新的进程会从ActivityThread的main()函数处开始执行,当对应进程启动后,B进程会报告AmS自己已经启动,于是执行AmS的attachApplication()方法,该方法可理解为B进程请求AmS给自己安排(attach)一个具体要执行的Activity,此时AmS继续调用resumeTopActivity(),通知B进程执行指定的Activity。 +- 首先判断目标HistoryRecord在B进程中不存在,则B调用handleLaunchActivity()创建一个该Activity实例。如果已经存在,则调用handleResumeActivity()恢复已有的Activity运行。这个逻辑的意思就是说,在ActivityThread中可以存在同一个Activity的多个实例,对应了AmS中mHistory的多个HistoryRecord对象。在一般情况下,当调用startActivity的FLAG为NEW_TASK时,AmS会首先从mHIstory中找到指定Activity所在的Task,然后启动Task中的最后一个Activity。如果FLAG不为NEW_TASK,那么AmS会在当前Task中重新创建一个HistoryRecord。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" new file mode 100644 index 00000000..f731b4e9 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" @@ -0,0 +1,60 @@ +# 5.Android消息获取 + +在Android 2.0及以前的所有版本中,获取用户消息的方式基本上都是相同的,具体如下: + +- 在WmS中有一个子类KeyQ,该类基于KeyInputQueue类,而该类内部则包含一个线程对象,即KeyQ对象是一个线程对象。该线程的任务是调用native方法从输入设备中读取用户消息,包括案件、触摸屏、鼠标、轨迹球等各种消息,并把读取到的消息保存到一个QueueEvent队列中。 + +- 在WmS中有另外一个子类叫做InputDispatcherThread,该类也是一个线程类,即内部包含一个线程。该线程的任务就是从上面的QueueEvent队列中读取用户消息,并对这些消息进行一定的加工,然后判断应该把这个消息发送给哪个应用窗口。 +- 在每一个应用窗口对象ViewRoot中都包含一个W子类,该类是一个Binder类,InputDispatchThread通过IPC方式调用W所提供的函数。从而把消息发送给对应的客户端窗口。 + +2.2版本中的这种处理过程有两点被Android社区所诟病。 + +- 对所有原始消息的加工都在KeyQ类的Java代码中完成,这加大了消息处理的延迟。 +- InputDispatcherThread是通过Binder方式传递按键消息的,而Binder的延迟降低了用户操作的相应速度。 + +以上两点给用户的体验就是界面操作的延迟,比如滑动触摸屏,界面的移动会延迟于指尖的移动。于是从2.3开始,Android团队对消息处理逻辑进行了重构: + +- 获取消息的代码全部使用C++完成,包括对消息进行加工转换。 +- 抛弃了使用Binder方式传递用户消息到客户端,而是使用Linux的Pipe机制。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/input_message_process.png) + +1. 首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入的消息,该线程和InputDispatcher线程都在系统进程(system_process)空间中运行。InputDispatcher线程从自己的消息队列中取出原始消息,取出的消息有可能经过两种方式进行派发。 + - 经过管道(Pipe)直接派发到客户窗口中。 + - 先派发到WmS中,由WmS经过一定的处理,如果WmS没有处理该消息,则再派发给客户窗口中,否则,不派发到客户窗口。 +2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitore中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 +3. 当InputDispatcher得到用户消息后,会根据NativeInputManager中保存的所有窗口信息判断当前的活动窗口是哪个,并把消息传递给该活动窗口。另外,如果是按键消息,InputDispatcher会先回调InputManager中定义的回调函数,这既会回调InputMonitor中的回调函数,又会回调WmS中定义的相关函数,所以这些回调函数的返回值类型是boolean。对于系统按键消息,比如“Home键”、电话按键等,WmS内部会按默认的方式处理,并返回false,从而InputDispatcher不会继续把这些按键消息传递给客户窗口。对于触摸屏消息,InputDispatcher则直接传递给客户窗口。 +4. 在InputDispatcher和客户窗口之间使用了管道(Pipe)机制进行消息传递。Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存,并产生一个Pipe对象。每个Pipe对象内部都会自动创建两个文件描述符,一个用于读,另一个用于写。应用程序可以调用pipe()函数产生一个Pipe对象,并获得该对象中的读、写文件描述符。文件描述符是全局唯一的,从而使得两个进程之间可以借助这两个描述符,一个往管道中写数据,另一个从管道中读数据。管道只能是单向的,因此,如果两个进程要进行双向消息传递,必须创建两个管道。当客户窗口请求WmS创建窗口时,WmS内部会创建两个管道,其中一个管道用于InputDispatcher向客户窗口传递消息,另一个用户客户窗口向InputDispatcher报告消息的执行结果。因此,有多少个客户窗口,就有多少个管道与InputDispatcher相连。 +5. 由于创建管道属于Linux系统调用,Java不能直接调用,另外也由于程序结构的需要,Android中把调用管道的相关操作封装到了InputChannel.java类中,同时该类中保存了InputDispatcher和客户窗口的管道收、发描述符。因此,InputChannel也可以理解为一个“通道”,在InputDispatcher端存在一个服务端消息通道(serverChannel)。在客户窗口端存在一个客户端消息通道(clientChannel)。管道和通道的区别是,管道是Linux系统的概念,使用pipe()系统调用就可以创建一个管道,而通道是Android内部定义的一个概念,主要保存了通信双发所使用的管道描述符。 + + + +### 事件分发 + + + +#### 按键消息派发 + +1. 首先,在ViewRoot中定义了一个InputHandler对象,当底层得到按键消息后,会回调到该InputHandler对象的handleKey()函数,该函数内部发送一个异步DISPATCH_KEY消息,消息的处理函数为deliverKeyEvent(),该函数内部分以下三步执行: + - 调用mView.dispatchKeyEventPreime(),这里的PreIme的意思就是在Ime之前,即输入法之前。因为对于View系统来讲,如果有输入法窗口存在,会先将案件消息派发给输入法窗口,只有当输入法窗口没有处理该消息时,才会把消息继续派发给真正的视图。所以如果想在输入法之前处理某些按键消息,可以重写该dispatchKeyEventPreime()方法,如果该函数返回为true,则可以直接返回了,但是在返回之前如果WmS要求返回一个处理回执,则需要先调用finishInputEvent()报告给WmS已经处理的该消息,从而使得WmS可以继续派发下一个消息。 + - 接下来就需要把该消息派发到输入法窗口。当然如果此时输入法窗口不存在,就直接派发到真正的视图。 + - 调用deliverKeyEventToViewHierarchy(),将消息派发给真正的视图。该函数内部又分为四步: + - 调用checkForLeavingTouchModeAndConsume()判断该消息是否会导致离开触摸模式,并且会消耗掉该消息,一般情况下该函数总是会返回false。 + - 调用mView.dispatchKeyEvent()将消息派发给根视图。对于应用窗口而言,根视图就是PhoneWindow的DecorView对象。而DecorView的dispatchKeyEvent内部会判断去处理音量键、系统快捷键等 + - 如果应用程序中没有处理该消息,则默认会判断该消息是否会引起视图焦点的变化,如果会就进行焦点切换。 + + + + + +和按键派发类似,当消息获取模块通过pipe将消息传递给客户端,InputQueue中的next()函数内部调用nativePollOnce()函数中会读取该消息,如果有消息,则回调ViewRoot内部的mInputHandler对象的dispatchMothion()函数,该函数仅仅是发起一个DISPATCH_POINTER异步消息,消息的处理函数是deliverPointerEvent()。执行完该函数后,调用finishInputEvent()向消息获取模块发送一个回执,以便其进行下一次消息派发。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" new file mode 100644 index 00000000..b4f24bb7 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -0,0 +1,51 @@ +# 6.屏幕绘制基础 + +Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口中进行具体的视图内容绘制;对于每个客户端而言,他们都感觉自己独占了屏幕,而对于服务端而言,它会给每一个客户端窗口分配不同的层值,并根据用户的交互情况动态改变窗口的层值,这就给用户造成了所谓的前台窗口和后台窗口的概念; + +Android的屏幕绘制架构如下图: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_draw_process_archi.jpg) + +- SurfaceFlinger服务进程:简称sf。该进程在整个系统中只有一个实例,在系统开机后自动运行,它的作用是给每个客户端分配窗口,程序中用Surface类表示这个窗口。正如Surface字面的意义,它是一个平面,即每个窗口是一个平面,每个平面在程序中都对应一段内存,也就是所谓的屏幕缓冲区,不同窗口的缓冲区大小不同,这取决于该窗口的大小,即宽度和高度,一般来讲缓冲区的大小为宽度x高度。sf的客户端必须使用SurfaceFlinger的客户端接口驱动来和sf打交道,系统中使用该接口驱动的最重要的进程就是SystemServer进程。 +- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 +- WmS收到这个请求后,会通过Surface类的JNI调用到SurfaceFlinger_client驱动,通过该client接口驱动请求sf进程创建指定的窗口。于是sf创建一段屏幕缓冲区,并在sf内部记录该窗口,然后sf会把该窗口的缓冲区地址传递给WmS,WmS再用这个地址去初始化APK程序传入的Surface对象,并最终回到APK程序中。此时APK程序中的Surface对象是一个真正的Surface对象了,因为它包含的屏幕缓冲区已经由sf创建并备案了。 +- APK程序有了这个Surface后,就可以给这个平面上绘制任意的内容了,比如绘制矩阵、绘制文本、绘制图片等。然后Surface类本质上仅仅表示了一个平面,而绘制不同图片显然是一种操作,而不是一段数据,因此Android中使用了一个叫做Skia的绘图驱动库,该库使用C/C++语言编写而成,其作用就是能够进行各种平面绘制。在程序中用Canvas类来表示这个功能对象,Canvas类有很多绘制函数,比如drawColor()、drawLine()等。Surface类包含了一个函数lockCanvas(),APK应用程序可以通过该函数返回一个Canvas功能对象,然后就可以调用该对象的各种绘制函数完成对平面的绘制。 + + + +### 相关类 + +- Surface类:该类用于描述一个绘制平面,其内部仅仅包含了该平面的大小,在屏幕上的位置,以及一段屏幕缓冲内存区。不过在Java端,不能直接访问这段内存,同时也不能通过该类直接设置平面的大小和位置,而只能通过SurfaceHolder类。 + + 一般情况下,客户端程序对应的Surface都是由底层的ViewRoot类进行创建的,而ViewRoot中创建Surface的函数在SDK中没有开放,所以应用程序不能通过ViewRoot类直接创建Surface对象,而只能通过SurfaceView类间接创建。 + + 对于SDK开发的APK程序而言,不能直接创建Surface对象,而只能使用WindowManager类的addView()方法创建一个窗口,因为Surface的构造函数都是@hide的,即不会出现在SDK中。Surface类有两个构造函数: + + - public Surface() {} + + 使用该构造函数创建的Surface对象仅仅是一个空壳,因为每个Surface内部都会对应一段屏幕缓冲区内存,对于空壳子Surface而言,这段内存不存在。 + + - public Surface(SurfaceSession s, int pid, int display, int w, int h, int format, int flags) + + 该构造函数包含窗口大小相关的参数,使用该构造函数会创建一个真正的Surface对象。 + + WindowManager类的addView()函数会创建一个ViewRoot对象,而ViewRoot类则使用Surface的无参数构造函数创建了一个Surface对象,此时该Surface是一个空壳,然后ViewRoot类调用WmS中的IWindowSession服务为该Surface对象分配真正的屏幕缓冲区内容。之后Surface的构造函数回去创建一个Canvas对象,然后调用init()函数来初始化该Surface对象,该函数内部会首先获得一个SurfaceComposerClient对象,该类正式native层面上的SurfaceFlinger服务的客户端对象,然后调用该对象的createSurface()函数创建一个真正的Surface对象。之后调用Surface的lock()函数获取一个SurfaceInfo对象。该SurfaceInfo对象中获取该Surface对应屏幕缓冲区内存地址。再用这个地址构造一个SkBitmap对象,之后用SkBitmap对象构造一个SkCanvas对象,这个SkCanvas对象是底层真正进行绘制的功能类对象,Java层面的Canvas类仅仅是该类的包装而已,之后将Canvas对象返回到Java端。 + +- Canvas类:是一个功能类,该类包含各种绘制函数,例如drawColor()、drawLine()等。构造Canvas对象时,必须为该Canvas指定一段内存地址,因为绘制的结果实际上就是给这段内存地址中填充不同的像素值。这段内存有两种类型,一种是普通的内存,另一种是屏幕缓冲区内存,当Canvas对应的内存为屏幕缓冲区内存时,绘制函数执行后就可以在屏幕上看到,如果是一段普通内存,则不会在屏幕上看到,不过却可以将这段内存复制到拥有屏幕缓冲内存的Canvas中,这种方式就是游戏开发中常用的方式。 + +- Drawable类:是一个抽象类,该类是一个功能类,但它与Canvas的相同之处是两者可以给内存缓冲区中绘制图案,两者的区别有两点: + + - Drawable类内部不存在一段内存缓冲区,当应用程序需要绘制某种图案时,可以将一个包含内存缓冲区的Canvas对象传递给Drawable,然后Drawable就可以给该Canvas上绘制相应的团。 + - 每个具体的Drawable对象仅仅绘制某个特定的图案,SDK中包含的Drawable实现类有BitmapDrawable、NinePatchDrawable等。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" new file mode 100644 index 00000000..a5def400 --- /dev/null +++ "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" @@ -0,0 +1,16 @@ +# 7.View绘制原理 + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..7fa274dc --- /dev/null +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,131 @@ +# 8.WindowManagerService简介 + + +WmS是Android中图形用户接口的引擎,它管理者所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 + +在WmS中,窗口是由两部分内容构成: + +- 描述该窗口的类WindowState。 +- 该窗口在屏幕上对应的界面Surface。 + +它的功能可以归纳为两个: + +- 保持窗口的层次关系,以便SurfaceFlinger能够据此绘制屏幕 +- 把窗口信息传递给InputManager对象,以便InputDispatcher能够把输入消息派发给和屏幕上显示一致的窗口。 + +Android采用层叠式布局,这种布局的特点在于允许多个窗口层叠显示。该布局一般都需要一个窗口管理服务端。从程序设计的角度看,有两种设计模式可以实现服务端: + +- 独立进程方式 + + 使用一个独立的进程专门用于屏幕的绘制和消息处理,所有的其他引用程序当需要创建窗口时,通过进程通信的方式请求管理服务创建窗口。比如Linux上的X-window就是一种独立进程的方式,它使用Socket通信的方式,通知窗口管理服务进行窗口的创建及交互消息传递。 + +- 共享库方式 + + 使用一段共享程序,该段共享程序中保存了所有客户端的窗口信息,共享库和每个客户端程序都运行于同一个进程之间。Windows操作系统使用的就是这种方式,很多嵌入式系统也使用这种方式。该方式的有点是窗口管理的开销比较小,尤其是窗口的交互,因为它不需要进程间通信,其缺点就是任何一个客户端的不适当操作都可能导致窗口系统崩溃。 + + + + + +在WmS内部逻辑中,会进行三种常见的操作,具体的操作可能会对应不同的函数名称,这三种常见的操作为assign layer、perform layout以及place surface: + +- Assign layer的意思是为窗口分配层值。在WmS中,每个窗口都是用WindowState类来描述,而窗口要在界面上显示时,需要制定窗口的层值。从用户的视角来看,层值越大,其窗口越靠近用户,窗口之间的层叠正式按照层值进行的。 +- perform layout的意思是计算窗口的大小。每个窗口对应都必须有一个大小,即窗口大小,perform layout将根据状态栏大小、输入法窗口的状态、窗口动画状态计算该窗口的大小。 +- place surface的意思是调整Surface对象的属性,并重新将其显示到屏幕上。由于assign layer和perform layout的执行结果影响的仅仅是WindowState中的参数,而能够显示到屏幕上的窗口都包含一个Surface对象,因此只有将以上执行结果的窗口层值、大小设置到Surface对象中,屏幕上才能看出该窗口的变化。place surface的过程就是将这些值赋值给Surface对象,并告诉Surface Flinger服务重新显示这些Surface对象。 + + + +### WmS接口结构 + +WmS接口结构是指WmS功能模块与其他模块之间的交互接口,其中主要包括与AmS模块及应用程序客户端的接口, + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/wms_api.png) + +该结构中的主要交互过程如下: + +- 应用程序在Activity中添加、删除窗口。具体实现就是通过调用WindowManager类的addView()和removeView()函数完成,这会转而调用ViewRoot类的相关方法,然后通过IPC调用到WmS中的相关方法完成添加、删除过程。 +- 当AmS通知ActivityThread销毁某个Activity时,ActivityThread会直接调用WindowManager中的removeView()方法删除窗口。 +- AmS中直接调用WmS,这种调用一般都不是请求WmS创建或删除窗口,而是告诉WmS一些其他信息。 + +在WmS内部,全权接管了输入消息的处理和屏幕的绘制。其中输入消息的处理是借助于InputManager类完成的。而绘制屏幕则是借助于SurfaceFlinger模块完成的,SurfaceFlinger是Linux的一个驱动,它内部会使用芯片的图形加速引擎完成对界面的绘制。 + + + +### WindowState + +由于WmS是用来管理窗口的,因此需要定义一个专门的类来表示窗口,WmS中表示窗口的类就是WindowState。从设计原理的角度来说,似乎使用WindowState类来表示一个窗口就可以了,但是从程序实现的角度来讲,为了变成的便利性及程序逻辑的清晰性,WmS类内部还定义了两个额外的用来表示窗口的类,分别是WindowToken和AppWindowToken。为什么还需要这两个额外的类? + +- 每个窗口都会对应一个WindowState对象。因为窗口的本质就是由WindowState类描述的数据对象,WindowState类中记录作为一个窗口应该有的全部属性,比如窗口的大小,在屏幕上的层值,以及窗口动画过程的各种状态信息。 +- WindowToken描述的是窗口对应的token的相关属性,每个窗口都会对应一个WindowToken对象,但是一个窗口的所有子窗口将对应同一个WindowToken对象,即多对一的关系。 +- 如果窗口是由Activity创建的,即该窗口对应一个Activity,那么该窗口同时对应一个AppWindowToken对象。 + + + +### Session + +和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS回味每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 + + + + + +## 创建窗口 + + + +创建窗口的时机可分为两种: + +- 程序员主动调用WindowManager类的addView()方法。 +- 当用户启动一个新的Activity或者显示一个对话框、菜单栏等的时候,在这种情况下,程序员并不是直接调用addView()函数,但是这些类的内部同样会间接调用addView()函数。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/create_window_process.png) + +- 客户端调用WindowManager类的addView()方法后,该方法会创建一个新的ViewRoot对象,然后调用ViewRoot类的setView()方法,该方法中会通过IPC方式调用WmS类中内联类Session的add()方法。 +- Session类的add()方法又会间接调用WmS的addWindow()方法,该方法内部又分为三个小过程: + - 第一个过程是进行前置处理,即首先判断参数的合法性,以确保接下来的添加操作能够顺利进行。 + - 第二个过程是具体添加和窗口相关的数据。 + - 第三个过程是后置处理,即添加窗口会引起相关状态的变化,因此需要把这些变化反应到相关的数据中。 + + + + + +## AmS与WmS的交互 + + + +AmS和WmS都是窗口管理系统的核心,不过AmS侧重于对Activity的管理,而WmS侧重于对窗口的管理。系统启动后首先是由AmS接管主控制权力,然后AmS开始调度并运行Activity,WmS作为AmS的辅助服务,接受应用程序的请求创建窗口。当窗口显示后,用户和窗口的交互控制则交由WmS和应用程序本身来完成,而当应用程序需要启动新的Activity时,则又交给AmS去处理,系统就这样周而复始的运行。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/ams_wms.png) + +以上过程是在两个进程、三个独立线程中异步完成的,从代码的角度解释以上执行过程: + +- 当要启动B时,AmS会调用WmS的addAppToken()添加一个token,该token对应的是新的Activity。然后再调用WmS的setAppStartingWindow()告诉WmS启动窗口的标题和图标,以便WmS能根据这两个信息创建一个启动窗口。WmS接收到这个命令后就开始去创建启动窗口了,创建的具体过程就是标准的添加窗口的过程。 +- 与此同时,AmS还去启动B对应的进程,如果进程已经存在,则运行一个ActivityThread实例。每个Activity都是从ActivityThread开始执行的,ActivityThread类是客户端程序的主类,Activity仅仅是一个回调而已。 +- 在接下来的一段时间里,AmS处于空闲状态。WmS内部则开始创建启动窗口,并可能已经创建完毕了启动窗口,但暂时不能显示该启动窗口。而ActivityThread内部也忙碌的启动进程并使ActivityThread就绪。 +- 当ActivityThread就绪后,就会通过IPC调用AmS的attachApplication(),通知AmS自己已经就绪,可以运行任何指定的Activity了。当AmS收到这个通知后,一方面会调用WmS的setAppVisibility()使其开始显示启动窗口,并调用WmS中的setFocusedApp()将新的AppWindowToken设为焦点窗口,然而此时由于真正的窗口还没有就绪,所以焦点窗口被调整为Null。另一方面则调用ActivityThread中内联类ApplicationThread的scheduleLaunchActivity(),请求其开始运行指定的Activity,这最终会调用执行到Activity类的onCreate()函数中。 +- 在接下来的一段时间里,在WmS中,由于真正的Activity窗口还没有被创建,因此当前的焦点窗口被调整为null,并且开始了启动动画。另一方面,在ActivityRecord类中则开始运行Activity的onCreate(),该函数最终会调用到setContentView(),这会间接的创建一个真正的Activity窗口。 +- 当ActivityThread内部执行到创建真正的Activity窗口时,会调用到WmS中的addWindow()函数,在该函数中,当添加完新窗口后,就会把焦点调整到新窗口中。 + + + +## 销毁Surface的过程 + +Surface的销毁有两种情况: + +- AmS调用WmS的setAppVisibility()设置指定应用窗口的可视状态。该方法会调用到relayoutWindow()方法,这里面会销毁窗口对应的Surface。 +- 当要删除窗口时,这个很容易理解,窗口都不在了,内部的Surface自然应该被销毁。 + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" new file mode 100644 index 00000000..803a99df --- /dev/null +++ "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" @@ -0,0 +1,65 @@ +# 9.PackageManagerService简介 + +程序包管理主要包含三部分内容: + +- 提供一个能够根据intent匹配到具体的Activity、Provider、Service。即当应用程序调用startActivity(intent)时,能够把参数中指定的intent转换成一个具体的包含了程序包名称及具体Component名称的信息,以便Java类加载器加载具体的Component。 +- 进行权限检查。即当应用程序调用某个需要一定权限的函数调用时,系统能够判断调用者是否具备该权限,从而保证系统的安全。 +- 提供安装、删除应用程序接口。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/package_manager_service_archi.png) + +该框架可以分为三层: + +- 应用程序层 + + 应用程序需要使用包管理服务时,调用ContextImpl类的getPackageManager()函数返回一个PackageManager对象,然后调用该对象所提供的各种API接口。 + +- PmS服务层 + + 和AmS、WmS等其他系统服务一样,包管理服务运行于SystemServer进程。PmS服务运行时,使用了两个目录下的XML文件保存相关的包管理信息。 + + - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两件时间。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 + - 第二个目录是/data/system/packages.xml,该文件保存了所有安装程序的基本包信息,有点像系统的注册表,比如程序的包名称是什么,安装包路径在哪里,程序都是用了哪些系统权限。 + + PmS在启动时,会从这两个目录中解析相关的XML文件,从而建立一个庞大的包信息树,应用程序可以间接从这个信息树中查询所有所需的程序包信息。 + + 除了PmS服务外,还有两个辅助系统服务用于程序安装。 一个是DefaultContainerService,该服务主要用于把安装程序复制到程序目录中。另一个是Installer服务,该服务实际上并不是一个Binder,而是一个Socket客户端,PmS直接和该Socket客户端交互。Socket的服务端主要完成程序文件的解压工作及数据目录创建,比如从APK文件中提取dex文件,删除dalvik-cache(会把每个apk的dex文件放到该目录,方便提升执行速度)目录下的dex文件,创建程序专属的程序目录等。 + +- 数据文件层 + + 就像所有操作系统一样,Android中的程序也由相关的程序文件组成,这些程序文件可以分为三个部分: + + - 程序文件 + + 所有的系统程序保存在/system/app目录下,所有的第三方应用程序保存在/data/app目录下,该目录中的APK与原始的APK文件的唯一区别是文件的名称不同,原始文件可以任意命名,而该目录下的文件名称是以包名进行命名,并自动增加一个"-x"后缀,比如com.android.haii.debugjar-1.apk。当同样一个程序第二次安装时,后面的数字1会变成数字2,而当第三次再安装时,又会变成数字1,有点像“乒乓”机制。。/data/dalvik-cache目录保存了程序中的执行代码。一个APK实际上是一个Jar压缩类型的文件,压缩包中包含了各种资源文件、资源索引文件、AndroidManifest文件及程序文件,当应用程序运行前,PmS会从APK文件中提取出代码文件,也就是所谓的dex文件,并将该文件存储在该目录下,以便以后能够快速运行该程序。比如: data@app@com.android.xxx-1.apk@classes.dex + + - framework库文件 + + 这些库文件存在于/system/framework目录下,库文件类型是APK或者Jar,系统开机后,dalvik虚拟机会加载这些库文件,而在PmS启动时,如果这些Jar或者APK文件还没有被转换为dex文件,则PmS会将这些库文件转换为dex文件,并保存到/data/dalvik-cache目录下。 + + - 应用程序所使用的数据文件 + + 应用程序可以使用三种数据保存方式,分为为参数存储、数据库存储、文件存储。这三种存储方式对应的数据文件一般都保存到/data/data/xxx目录下,xxx代表程序的包名。 + + + +安装及卸载程序的操作都是由PmS完成,安装程序的过程包括在程序目录下创建以包名称命名的程序文件、创建程序数据目录,以及把程序信息保存到相关的配置文件packages.xml中,卸载则是一个相反的操作。 + + + + + +### aipalign优化APK内部存储 + +所谓的内部存储优化是指,为了提高APK程序的加载速度,从而对APK中相关的数据进行边界对齐。因为从底层NAND Flash的角度来讲,读取NAND时,是以一个扇区进行读取的,因此,如果相关的数据能够在同一个扇区中,肯定会提高读取速度。zipalign的作用正是将APK包中的不同类型的数据文件进行边界对齐。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" index 43ba62de..cc1cdbb3 100644 --- "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" +++ "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -2,6 +2,10 @@ Activity启动过程 === 前两天面试了天猫的开发,被问到了`Activity`启动过程,不懂啊.... + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/app launch summary.jpg) + + 今天就来分析一下,我们开启`Activity`主要有两种方式: - 通过桌面图标启动,桌面就是`Launcher`其实他也是一个应用程序,他也是继承`Activity`。 diff --git "a/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" "b/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" index 66fb66eb..1ef073c8 100644 --- "a/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/Android Touch\344\272\213\344\273\266\345\210\206\345\217\221\350\257\246\350\247\243.md" @@ -3,27 +3,35 @@ Android Touch事件分发详解 先说一些基本的知识,方便后面分析源码时能更好理解。 - 所有`Touch`事件都被封装成`MotionEvent`对象,包括`Touch`的位置、历史记录、第几个手指等. - - 事件类型分为`ACTION_DOWN`,`ACTION_UP`,`ACTION_MOVE`,`ACTION_POINTER_DOWN`,`ACTION_POINTER_UP`,`ACTION_CANCEL`, 每个 一个完整的事件以`ACTION_DOWN`开始`ACTION_UP`结束,并且`ACTION_CANCEL`只能由代码引起.一般对于`CANCEL`的处理和`UP`的相同。 `CANCEL`的一个简单例子:手指在移动的过程中突然移动到了边界外,那么这时`ACTION_UP`事件了,所以这是的`CANCEL`和`UP`的处理是一致的。 - -- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件. - +- 事件的处理分别为`dispatchTouchEveent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件.这些方法的返回值如果是true表示事件被当前视图消费掉。 - 事件从`Activity.dispatchTouchEveent()`开始传递,只要没有停止拦截,就会从最上层(`ViewGroup`)开始一直往下传递,子`View`通过`onTouchEvent()`消费事件。(隧道式向下分发). - -- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件,那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). +- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件(不是clickable或longclickable),那么该事件会反序网上传递(从该`View`传递给自己的`ViewGroup`,然后再传给更上层的`ViewGroup`直至传递给`Activity.onTouchEvent()`). (冒泡式向上处理). - - 如果`View`没有消费`ACTION_DOWN`事件,之后其他的`MOVE`、`UP`等事件都不会传递过来. - - 事件由父`View(ViewGroup)`传递给子`View`,`ViewGroup`可以通过`onInterceptTouchEvent()`方法对事件进行拦截,停止其往下传递,如果拦截(返回`true`)后该事件 会直接走到该`ViewGroup`中的`onTouchEvent()`中,不会再往下传递给子`View`.如果从`DOWN`开始,之后的`MOVE`、`UP`都会直接在该`ViewGroup.onTouchEvent()`中进行处理。 如果子`View`之前在处理某个事件,但是后续被`ViewGroup`拦截,那么子`View`会接收到`ACTION_CANCEL`. +- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费,而`onTouchEvent()`又优先于`onCickListener.onClick()`。 +- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 +- ViewGroup默认不拦截任何事件,返回false。 +- View的onTouchEvent默认都会消费事件,返回true,除非它是不可点击的(clickable和longclickable都为false),View的longClickable默认都是false,clickable对于Button等为true,而TextView等为false。 +- View的enable属性不影响onTouchEvent的默认返回值。 +- 通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。 + +在Android系统中,拥有事件传递处理能力的类有以下三种: + +- Activity:拥有分发和消费两个方法。 +- ViewGroup:拥有分发、拦截和消费三个方法。 +- View:拥有分发、消费两个方法。 + + + +对触摸屏进行操作时,Linux就会收到相应的硬件中断,然后将中断加工成原始的输入事件并写入相应的设备节点中。而Android输入系统所做的事情概括起来说就是监控这些设备节点,当某个设备节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中找到合适的事件接收者,并派发给它。当点击事件产生后,事件会传递给当前的Activity,由Activity中的PhoneWindow处理,PhoneWindow再把事件处理工作交给DecorView,之后再由DecorView将事件处理工作交给ViewGroup。 -- `OnTouchListener`优先于`onTouchEvent()`对事件进行消费。 -- `TouchTarget`是保存手指点击区域属性的一个类,手指的所有移动过程都会被它记录下来, 包含被`touch`的`View`。 废话不多说,直接上源码,源码妥妥的是最新版5.0: 我们先从`Activity.dispatchTouchEveent()`说起: @@ -43,9 +51,11 @@ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } + // 首先交给本Activity对应的Window来进行分发,如果分发了,就返回true,事件循环结束 if (getWindow().superDispatchTouchEvent(ev)) { return true; } + // 如果window返回了false,就意味着所有view的ontouchevent都返回了false,那么只能是Activity来决定消费不消费 return onTouchEvent(ev); } ``` @@ -97,8 +107,9 @@ private final class DecorView extends FrameLayout implements RootViewSurfaceTake ... } ``` -它集成子`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 +它继承自`FrameLayout`所有很多时候我们在用布局工具查看的时候发现`Activity`的布局`FrameLayout`的。就是这个原因。 好了,我们接着看`DecorView`中的`superDispatchTouchEvent()`方法。 + ```java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); @@ -134,6 +145,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); + // 重置FLAG_DISALLOW_INTERCEPT resetTouchState(); // 如果是`Down`,那么`mFirstTouchTarget`到这里肯定是`null`.因为是新一系列手势的开始。 // `mFirstTouchTarget`是处理第一个事件的目标。 @@ -142,10 +154,12 @@ public boolean dispatchTouchEvent(MotionEvent ev) { // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) // Check for interception. final boolean intercepted; + // 当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,反之被ViewGroup拦截时,mFirstTouchTarget则为null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 标记事件不允许被拦截, 默认是`false`, 该值可以通过`requestDisallowInterceptTouchEvent(true)`方法来设置, - // 通知父`View`不要拦截该`View`上的事件。 + // 通知父`View`不要拦截该`View`上的事件。FLG_DISALLOW_INTERCEPT是在View中通过 + // reqeustDisallowInterceptTouchEvent来设置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 判断该`ViewGroup`是否要拦截该事件。`onInterceptTouchEvent()`方法默认返回`false`即不拦截。 @@ -339,7 +353,7 @@ public boolean dispatchTouchEvent(MotionEvent ev) { } return handled; } -``` +``` 接下来还要说说`dispatchTransformedTouchEvent()`方法,虽然上面也说了大体功能,但是看一下源码能说明另一个问题: ```java @@ -511,8 +525,7 @@ public boolean onTouchEvent(MotionEvent event) { if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } - // A disabled view that is clickable still consumes the touch - // events, it just doesn't respond to them. + // 只要view的clickable和long_clickable有一个是true,onTouchEvent就会返回true消耗这个事件。 return (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); } @@ -708,7 +721,7 @@ public boolean performClick() { 讲到这里就明白了。`onTouchEvent()`中的`ACTION_UP`中会调用`performClick()`方法。 -到这里,就全部分析完了,这一块还是比较麻烦的,中间查了很多资料,有些地方自己可能也理解的不太对,如果有哪里理解的不对的地方,还请大家指出来。谢谢。 +到这里,就全部分析完了。 --- diff --git "a/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" "b/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" index 57e2712b..e4168121 100644 --- "a/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/View\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" @@ -3,6 +3,7 @@ View绘制过程详解 界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, 但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... + ```java /** * The top of a view hierarchy, implementing the needed protocol between View @@ -18,13 +19,23 @@ public final class ViewRootImpl implements ViewParent, } ``` -`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始。 +`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始,你看这个名字起的多好,叫执行遍历,看到名字就能知道内部的实现: + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_performTraversals.png) + 首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 我也没有全部弄清楚,我只是把里面的步骤标注了下。 + ```java private void performTraversals() { // ... 此处省略源代码N行 - + if (mFirst || windowShouldResize || insetsChanged || + viewVisibilityChanged || params != null || mForceNextWindowRelayout) { + // 第一或者resize等都会调用relayoutWindow,而该函数内部会调用sWindowSession.relayout() + // 方法来请求WmS按照指定的大小重新分配窗口大小,并会为客户窗口创建的mSurface对象分配真正的现存 + // 等该函数返回后,应用程序就可以在该Surface中绘制了。 + relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); + } // 是否需要Measure if (!mStopped) { boolean focusChangedDueToTouchMode = ensureTouchModeLocally( @@ -35,6 +46,7 @@ private void performTraversals() { // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 + // 这里lp代表的是根视图的LayoutParams、lp.width和lp.height直接来源于用户的定义比如WRAP_CONTENT、MATCH_PARENT等 int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); @@ -159,7 +171,14 @@ private void performTraversals() { `Measure` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_measure.png) + + + `performMeasure`方法如下: + ```java private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); @@ -199,6 +218,7 @@ public final void measure(int widthMeasureSpec, int heightMeasureSpec) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; + // adjust是微调某个MeasureSpec的大小 widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } @@ -556,7 +576,14 @@ ps:譬如我们设置了`setMeasuredDimension(10, 10)`,那么不管布局中怎 `Layout` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_layout.png) + + + `performLayout`方法源码如下: + ```java private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { @@ -598,6 +625,7 @@ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth " during layout: running second layout pass"); view.requestLayout(); } + // desiredWindowWidth和desiredWindowHeight是屏幕的尺寸 measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight); mInLayout = true; @@ -825,7 +853,12 @@ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth `Draw` === + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_draw.png) + 绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: + ```java private void performDraw() { if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { @@ -887,6 +920,7 @@ private void performDraw() { ```java private void draw(boolean fullRedrawNeeded) { Surface surface = mSurface; + // 首先检查surface是否有效,正常情况下都是有效的,除非WmS发生异常不能为该客户端分配有效的Surface if (!surface.isValid()) { return; } @@ -941,6 +975,8 @@ private void draw(boolean fullRedrawNeeded) { } final Rect dirty = mDirty; + // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 + // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); @@ -982,7 +1018,10 @@ private void draw(boolean fullRedrawNeeded) { } if (!dirty.isEmpty() || mIsAnimating) { + // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 + // 因此这里需要根据不同的模式,进行不同的操作 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // 硬件绘制 // Draw with hardware renderer. mIsAnimating = false; boolean invalidateRoot = false; @@ -1023,7 +1062,7 @@ private void draw(boolean fullRedrawNeeded) { return; } - // draw的部分在这里。。。内部会用canvas去画 + // 软件绘制 if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { return; } @@ -1032,6 +1071,7 @@ private void draw(boolean fullRedrawNeeded) { if (animating) { mFullRedrawNeeded = true; + // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 scheduleTraversals(); } } @@ -1039,6 +1079,7 @@ private void draw(boolean fullRedrawNeeded) { 我们看一下`drawSoftware`方法: ```java /** + * 使用CPU的软件绘制方式 * @return true if drawing was successful, false if an error occurred */ private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, @@ -1570,6 +1611,10 @@ private void performDraw() { 而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... +# MeasureSpec + +MeasureSpec是View类的一个静态内部类,用来说明如何测量这个类。MeasureSpec表示的是一个32位的整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存分配。 + --- - 邮箱 :charon.chui@gmail.com From 81e42a8d57dfa487bb1086dcab28954bfbc84074 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:39:51 +0800 Subject: [PATCH 007/183] add android kernal --- .../ANR\345\210\206\346\236\220.md" | 66 ------------------- ...345\217\212ANR\345\210\206\346\236\220.md" | 20 ++++-- README.md | 27 +++++++- 3 files changed, 40 insertions(+), 73 deletions(-) delete mode 100644 "AdavancedPart/ANR\345\210\206\346\236\220.md" rename "AdavancedPart/crash\345\210\206\346\236\220.md" => "AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" (84%) diff --git "a/AdavancedPart/ANR\345\210\206\346\236\220.md" "b/AdavancedPart/ANR\345\210\206\346\236\220.md" deleted file mode 100644 index fc8ed472..00000000 --- "a/AdavancedPart/ANR\345\210\206\346\236\220.md" +++ /dev/null @@ -1,66 +0,0 @@ -# ANR分析 - -Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 - - -Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: -- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 -- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s -- ContentProvider Timeout:内容提供者,在publish过超时10s -- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 - - - -ANR信息输出到traces.txt文件中 - -traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 -adb pull /data/anr . - - -1) Thread基础信息 - -输出种包含所有的线程,取其中的一条 -"Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] - java.lang.Thread.State: BLOCKED (on object monitor) - at Test.rightLeft(Test.java:48) - - waiting to lock <0x00000007d56540a0> (a Test$LeftObject) - - locked <0x00000007d5656180> (a Test$RightObject) - at Test$2.run(Test.java:68) - at java.lang.Thread.run(Thread.java:745) -a) "Thread-1" prio=5 tid=0x00007fde73872800 nid=0x4a03 waiting for monitor entry [0x000000011cb30000] - -首先描述了线程名是『Thread-1』,然后prio=5表示优先级,tid表示的是线程id,nid表示native层的线程id,他们的值实际都是一个地址,后续给出了对于线程状态的描述,waiting for monitor entry [0x000000011cb30000]这里表示该线程目前处于一个等待进入临界区状态,该临界区的地址是[0x000000011cb30000] -这里对线程的描述多种多样,简单解释下上面出现的几种状态 - - waiting on condition(等待某个事件出现) - waiting for monitor entry(等待进入临界区) - runnable(正在运行) - in Object.wait(处于等待状态) - -作者:silentleaf -链接:https://www.jianshu.com/p/30c1a5ad63a3 -来源:简书 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 - - - - - - - - - - - - - - - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/AdavancedPart/crash\345\210\206\346\236\220.md" "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" similarity index 84% rename from "AdavancedPart/crash\345\210\206\346\236\220.md" rename to "AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" index e3dd4ca7..40db0adf 100644 --- "a/AdavancedPart/crash\345\210\206\346\236\220.md" +++ "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" @@ -48,10 +48,23 @@ Native Crash -## ANR +# ANR分析 +Application Not Responding,字面意思就是应用无响应,稍加解释就是用户的一些操作无法从应用中获取反馈 +Android系统中的应用被Activity Manager及Window Manager两个系统服务监控着,Android系统会在如下情况展示出ANR的对话框: +- Service Timeout:比如前台服务在20s内未执行完成;后台服务超过200没有执行 +- BroadcastQueue Timeout:比如前台广播在10s内未执行完成,后台60s +- ContentProvider Timeout:内容提供者,在publish过超时10s +- InputDispatching Timeout: 输入事件分发超时5s,包括按键和触摸事件。 + + + +ANR信息输出到traces.txt文件中 + +traces.txt文件是一个ANR记录文件,用于开发人员调试,目录位于/data/anr中,无需root权限即可通过pull命令获取,下面的命令可以将traces.txt文件拷贝到当前目录下 +adb pull /data/anr . ANR排查流程 1、Log获取 @@ -111,10 +124,7 @@ sCount:该线程被挂起的次数 dsCount:该线程被调试器挂起的次数 self:线程本身的地址 -作者:jsonchao -链接:https://juejin.cn/post/6844903972587716621 -来源:掘金 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + diff --git a/README.md b/README.md index 91a59d53..af998819 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,16 @@ Android学习笔记 - [6.文件管理][269] - [7.嵌入式系统][270] - [8.虚拟机][271] + - [Android内核][274] + - [1.Android进程间通信][275] + - [2.Android线程间通信之Handler消息机制][276] + - [3.Android Framework框架][277] + - [4.ActivityManagerService简介][278] + - [5.Android消息获取][279] + - [6.屏幕绘制基础][280] + - [7.View绘制原理][281] + - [8.WindowManagerService简介][282] + - [9.PackageManagerService简介][283] - [架构设计][272] - [1.架构简介][273] @@ -183,7 +193,7 @@ Android学习笔记 - [ApplicationId vs PackageName][77] - [ART与Dalvik][78] - [BroadcastReceiver安全问题][79] - - [Handler导致内存泄露分析][80] + - [Crash及ANR分析][80] - [Library项目中资源id使用case时报错][81] - [Mac下配置adb及Android命令][82] - [MaterialDesign使用][83] @@ -378,7 +388,7 @@ Android学习笔记 [77]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ApplicationId%20vs%20PackageName.md "ApplicationId vs PackageName" [78]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ART%E4%B8%8EDalvik.md "ART与Dalvik" [79]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/BroadcastReceiver%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98.md "BroadcastReceiver安全问题" -[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Handler导致内存泄露分析" +[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Crash及ANR分析" [81]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Library%E9%A1%B9%E7%9B%AE%E4%B8%AD%E8%B5%84%E6%BA%90id%E4%BD%BF%E7%94%A8case%E6%97%B6%E6%8A%A5%E9%94%99.md "Library项目中资源id使用case时报错" [82]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Mac%E4%B8%8B%E9%85%8D%E7%BD%AEadb%E5%8F%8AAndroid%E5%91%BD%E4%BB%A4.md "Mac下配置adb及Android命令" [83]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/MaterialDesign%E4%BD%BF%E7%94%A8.md "MaterialDesign使用" @@ -578,6 +588,19 @@ Android学习笔记 [271]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md "8.虚拟机" [272]: https://github.com/CharonChui/AndroidNote/tree/master/Architect "架构设计" [273]: https://github.com/CharonChui/AndroidNote/blob/master/Architect/1.%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B.md "1.架构简介" +[274]: https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem/AndroidKernal "Android内核" +[275]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/1.Android%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md "1.Android进程间通信" +[276]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md "2.Android线程间通信之Handler消息机制" +[277]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md "3.Android Framework框架" +[278]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md "4.ActivityManagerService简介" +[279]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md "5.Android消息获取" +[280]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md "6.屏幕绘制基础" +[281]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md "7.View绘制原理" +[282]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md "8.WindowManagerService简介" + +[283]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md "9.PackageManagerService简介" + + Developed By From 969ea96417418e8461b022b5f7aa9a936ac7e327 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 1 Dec 2020 21:42:28 +0800 Subject: [PATCH 008/183] update readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index af998819..cb8b3b84 100644 --- a/README.md +++ b/README.md @@ -388,7 +388,8 @@ Android学习笔记 [77]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ApplicationId%20vs%20PackageName.md "ApplicationId vs PackageName" [78]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ART%E4%B8%8EDalvik.md "ART与Dalvik" [79]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/BroadcastReceiver%E5%AE%89%E5%85%A8%E9%97%AE%E9%A2%98.md "BroadcastReceiver安全问题" -[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Handler%E5%AF%BC%E8%87%B4%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E5%88%86%E6%9E%90.md "Crash及ANR分析" +[80]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Crash%E5%8F%8AANR%E5%88%86%E6%9E%90.md "Crash及ANR分析" + [81]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Library%E9%A1%B9%E7%9B%AE%E4%B8%AD%E8%B5%84%E6%BA%90id%E4%BD%BF%E7%94%A8case%E6%97%B6%E6%8A%A5%E9%94%99.md "Library项目中资源id使用case时报错" [82]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/Mac%E4%B8%8B%E9%85%8D%E7%BD%AEadb%E5%8F%8AAndroid%E5%91%BD%E4%BB%A4.md "Mac下配置adb及Android命令" [83]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/MaterialDesign%E4%BD%BF%E7%94%A8.md "MaterialDesign使用" From 2bb6382ef5ee14cb8d5b58b858bbf32c1f1d0220 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 2 Dec 2020 21:47:15 +0800 Subject: [PATCH 009/183] update README --- ...73\347\273\237\347\256\200\344\273\213.md" | 42 +++++++------- ...13\344\270\216\347\272\277\347\250\213.md" | 57 +++++++------------ ...05\345\255\230\347\256\241\347\220\206.md" | 37 +++++++----- .../4.\350\260\203\345\272\246.md" | 12 ++-- OperatingSystem/5.I:O.md | 8 ++- ...07\344\273\266\347\256\241\347\220\206.md" | 2 + ...45\345\274\217\347\263\273\347\273\237.md" | 2 +- ...8.\350\231\232\346\213\237\346\234\272.md" | 5 +- ...13\351\227\264\351\200\232\344\277\241.md" | 54 +++++++++--------- 9 files changed, 115 insertions(+), 104 deletions(-) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 09bff58c..4e34b27f 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -148,29 +148,29 @@ Linux内核被装载后,就开始进行内核初始化的过程。 ## CPU(Central Processing Unit) -CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 +CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路(Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 **程序是把寄存器作为对象来描述的** -使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类。 +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: -***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 -***标志寄存器***:存储运算处理后的CPU的状态。 +- ***标志寄存器***:存储运算处理后的CPU的状态。 -***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 +- ***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 -***基址寄存器***:存储数据内存的起始地址。 +- ***基址寄存器***:存储数据内存的起始地址。 -***变址寄存器***:存储基址寄存器的相对地址。 +- ***变址寄存器***:存储基址寄存器的相对地址。 -***通用寄存器***:存储任意数据。 +- ***通用寄存器***:存储任意数据。 -***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +- ***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 -***栈寄存器***:存储栈区域的起始地址。 +- ***栈寄存器***:存储栈区域的起始地址。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 @@ -191,11 +191,11 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 ## CPU 指令执行过程 -那么 CPU 是如何执行一条条的指令的呢? +那么CPU是如何执行一条条的指令的呢? 几乎所有的冯·诺伊曼型计算机的CPU,其工作都可以分为5个阶段:**取指令、指令译码、执行指令、访存取数、结果写回**。 -- 取指令阶段是将内存中的指令读取到 CPU 中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 +- 取指令阶段是将内存中的指令读取到CPU中寄存器的过程,程序寄存器用于存储下一条指令所在的地址 - 指令译码阶段,在取指令完成后,立马进入指令译码阶段,在指令译码阶段,指令译码器按照预定的指令格式,对取回的指令进行拆分和解释,识别区分出不同的指令类别以及各种获取操作数的方法。 - 执行指令阶段,译码完成后,就需要执行这一条指令了,此阶段的任务是完成指令所规定的各种操作,具体实现指令的功能。 - 访问取数阶段,根据指令的需要,有可能需要从内存中提取数据,此阶段的任务是:根据指令地址码,得到操作数在主存中的地址,并从主存中读取该操作数用于运算。 @@ -230,13 +230,13 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千哥不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时报出忙状态,以实现最大的效率。 +所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千个不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时保持忙状态,以实现最大的效率。 -利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出相应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 +利用中断功能,处理器可以在I/O操作的执行过程中去执行其他命令。在这期间,如果I/O操作已经完成,此时外部设备在做好服务的准备后,即它准备好从处理器接收更多的数据时,外部设备的I/O模块给处理器发送一个中断请求信号。这时处理器会做出响应,暂停当前程序的处理,转去处理服务于特定I/O设备的程序,这种程序被称为中断处理程序(interupt handler)。在对该设备的服务响应完成后,处理器恢复原来的执行。 -从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再回复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 +从用户程序的角度来看,中断打断了正常执行的序列。中断处理完成后,再恢复执行。因此,用户程序并不需要为中断添加任何特殊的代码,处理器和操作系统负责挂起用户程序,然后在同一个地方恢复执行。 为使用中断产生的情况,在指令周期中要增加一个中断阶段。在中断阶段,处理器检查是否有中断发生,即检查是否出现中断信号。若没有中断,处理器继续运行,并在取指周期取当前程序的下一条指令。若有中断,处理器挂起当前程序的执行,并执行一个中断处理程序。这个中断处理程序通常是操作系统的一部分,它确定中断的性质,并执行所需要的操作。 @@ -256,7 +256,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 2. 处理器在响应中断前结束当前指令的执行。 3. 处理器对中断进行测试,确定存在未响应的中断,并给提交中断的设备发送确认信号,确认信号允许该设备取消它的中断信号。 4. 处理器需要准备把控制权转交给中断程序。首先,需要保存从中断点恢复当前程序所需要的信息,要求的最少信息包括程序状态字(PSW)和保存在程序计数器(PC)中的下一条要执行的指令地址,它们被压入系统控制栈。 -5. 处理器把相应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: +5. 处理器把响应此中断的中断处理程序入口地址装入程序计数器。每类中断可由一个中断处理程序,具体取决于计算机系统架构和操作系统的设计。如果有多个中断程序,这一信息可能已包含在最初的中断信号中,否则处理器必须给发中断的设备发送请求,以获取含有所需信息的响应。一旦装入程序计数器,处理器就继续执行下一个指令周期,该指令周期也从取指开始。由于取指是由程序计数器的内容决定的,因此控制权被转交给中断处理程序,该程序会引起以下操作: 6. 在这一点,与被中断程序相关的程序计数器和PSW被保存到系统栈中,此外,还有一些其他信息被当做正在执行程序的状态的一部分。特别需要保存处理器寄存器的内容,因为中断处理程序可能会用到这些寄存器,因此所有这些值和任何其他状态信息都需要保存。 7. 中断处理程序现在可以开始处理中断,其中包括检查与I/O操作相关的状态信息或其他引起中断的事件,还可能包括给I/O设备发送附加命令或应答。 8. 中断处理结束后,被保存的寄存器值从栈中释放并恢复到寄存器中。 @@ -284,7 +284,7 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 - 高速缓存,它多数由硬件控制。 -- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是告诉的、容量较小的告诉缓存的扩展。 +- 主存:这是存储器系统的主力。主存通常称为随机访问存储器(Random Access Memory,RAM),内存是计算机中主要的内部内部存储器系统。内存中的每个单元位置都有唯一的地址对应,而且大多数机器指令会访问一个或多个内存地址。内存通常是高速的、容量较小的高速缓存的扩展。 - 磁盘:磁盘同RAM相比,成本降低了,但是随机访问数据时间也慢了。其低速的原因是因为磁盘是一种机械装置。一个磁盘中有一个或多个金属盘片,他们以一定的速度旋转,从边缘开始有一个机械臂悬横在盘面上,这类似于老式播放塑料唱片的唱片机。 @@ -294,9 +294,9 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 有时,还有一些实际上不是磁盘的磁盘,比如固态硬盘(Solid State Disk,SSD)。固态硬盘并没有可以移动的部分,外形也不像唱片那样,并且数据是存储在存储器(闪存)中的。与磁盘唯一的相似之处就是它也存储了大量即使在电源关闭时也不会丢失的数据。 - 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数吗相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 + 这里要说一下闪存(flash memory),闪存是一种基于硅芯片的存储介质,可以用电写入或擦除。在便捷式电子设备中,闪存通常作为存储媒介。闪存是数码相机中的胶卷。是便捷式音乐播放器的磁盘。这仅仅是闪存用途的两项。闪存在速度上介于RAM和磁盘之间。另外,与磁盘存储器不同的是,如果闪存擦除次数过多,就被磨损了。 - 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存支制成的。 + 那闪存和固态硬盘有什么区别?固态硬盘也是将数据存储在闪存中。在存储行业中使用的最简单的类比之一是闪存就像鸡蛋,而SSD硬盘就像煎蛋卷一样。煎蛋卷主要是由鸡蛋制作的,而SSD硬盘主要由闪存制成的。 @@ -397,6 +397,10 @@ Android在Linux内核中增加了两个提升电源管理能力的新功能: + + +---- + - [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 83bfc7f2..3f5369e6 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -24,9 +24,9 @@ ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/cpu_process.png) -上图是一种进程管理方法。两个进程A和B存在与内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 +上图是一种进程管理方法。两个进程A和B存在于内存中的某些部分,给每个进程(包含程序、数据和上下文信息)分配了一块存储器区域,并且在由操作系统建立和维护的进程表中进行了记录。进程表包含记录每个进程的表项,表项内容包括指向包含进程的存储块地址的指针,还包括该进程的部分和全部上下文。执行上下文的其余部分存放在别处,可能和进程本身存在一起,通常还可能保存在内存中的一块独立区域。进程索引寄存器(process index register)包含当前正在控制处理器的进程在进程表中的索引。程序计数器(program counter)指向该进程中下一条待执行的指令。基址寄存器中保存该存储器区域的开始地址。 -图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同事,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 +图中表示,进程索引寄存器表明进程B正在执行。以前执行的进程被临时中断,在A中断的同时,所有寄存器的内容被记录在其执行上下文环境中,以后操作系统就可以执行进程切换。恢复进程A的执行。进程切换过程中包括保存B的上下文和恢复A的上下文。在程序计数器中载入指向A的程序区域的值时,进程A自动恢复执行。 在任何多道程序设计系统中,CPU由一个进程快速切换至另一个进程,使每个进程各运行几十或几百毫秒。严格来说,在某一个瞬间,CPU只能运行一个进程。但在1秒钟内,它可能运行多个进程,这样就产生并行的错觉。CPU在各进程之间来回切换,这种快速的切换称为多道程序设计。 @@ -68,8 +68,8 @@ - 就绪态:进程做好了准备,只要有机会就开始执行 - 运行态:进程正在执行 -- 堵塞/等待态:进程在某些事件发生前补鞥呢执行,如I/O操作完成 -- 新建态:刚刚创建的进程,操作系统还未把他加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 +- 堵塞/等待态:进程在某些事件发生前不能执行,如I/O操作完成 +- 新建态:刚刚创建的进程,操作系统还未把它加入可执行进程组,它通常是进程控制块已经创建但还未加载到内存中的新进程 - 退出态:操作系统从可执行进程组中释放出的进程,要么它自身已停止,要么它因某种原因被取消 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_state.png) @@ -82,11 +82,11 @@ ### 进程由哪几部分组成? -进程是程序的一次运行过程,它是由程序段、数据段和进程控制块 PCB 组成的一个实体,其中: +进程是程序的一次运行过程,它是由程序段、数据段和进程控制块PCB组成的一个实体,其中: - 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 -- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对 PCB 的操作。 +- 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对PCB的操作。 #### 进程控制块 @@ -101,7 +101,7 @@ - I/O状态信息:包括显式I/O请求、分配给进程的I/O设备和被进程使用的文件列表等 - 记账信息:包括处理器时间总和、使用的时钟数总和、时间限制、及账号等 -上述列表信息存放在一个被称为进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过 PCB 感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 +上述列表信息存放在一个进程控制块(process control block)的数据结构中,控制块由操作系统创建和管理。系统通过PCB感知进程的存在,并对其进行有效管理和控制。系统创建一个新进程时,为它建立一个PCB;当进程结束时,系统又收回其PCB,该进程也随之消亡。 @@ -119,7 +119,7 @@ ## 进程切换 -操作系统为了执行进程间的切换,会维护着一张表格,这张表就是 进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +操作系统为了执行进程间的切换,会维护着一张表格,这张表就是进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 **操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 @@ -131,7 +131,7 @@ - 时钟中断:操作系统确定当前正运行进程的执行时间是否已超过最大允许时间段(时间片,即进程中断前可以执行的最大时间段)。若超过,进程就切换到就绪态,并调入另一个进程。 - I/O中断:操作系统确定是否已发生I/O活动。若I/O活动是一个或多个进程正在等待的事件,则操作系统就把所处于堵塞态的进程转换为就绪态( 堵塞/挂起态进程转换为就绪/挂起态)。操作系统必须决定是继续执行当前处于运行态的进程,还是让具有高优先级的就绪态进程抢占这个进程。 -- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外村中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。期望的块调入内存后,该进程置为就绪态。 +- 内存失效:处理器遇到一个引用不在内存中的字的虚存地址时,操作系统就必须从外存中把包含这一引用的内存块(页或段)调入内存。发出调入内存块的I/O请求后,内存失效进程将进入堵塞态。操作系统然后切换进程,恢复另一个进程的执行。在等该期望的内存块调入内存后,该进程置为就绪态。 对于陷阱(trap),操作系统则确定错误或异常条件是否致命。致命时,当前正运行进程置为退出态,并切换进程。不致命时,操作系统的动作将取决于错误的性质和操作系统的设计,操作系统可能会尝试恢复程序,或简单的通知用户。操作系统可能会切换进程或继续当前运行的进程。 @@ -175,7 +175,7 @@ - 信号 - 基本原来是:两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 + 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 - 共享存储系统 @@ -349,7 +349,7 @@ zygote进程对应的具体程序是app_process,该程序存在于system/bin #### System Server进程 -System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerSystemService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: +System Server进程是由zygote进程fork而来,System Server是zygote孵化的第一个Dalvik进程,SystemServer仅仅是该进程的别名,而该进程具体对应的程序依然是app_process,因为System是从app_process中孵化出来的。System Server负责启动和管理整个Java framework,SystemServer进程在Android的运行环境中扮演了“神经中枢”的作用,APK应用中能够直接交互的大部分系统服务都在该进程中运行,常见的有WindowManagerServer(WmS)、ActivityManagerService(AmS)、PackageManagerServer(PmS)等,这些系统服务都是以一个线程的方式存在于SystemServer进程中。SystemServer的main()函数会首先创建一个ServerThread对象,该对象是一个线程,然后直接运行该线程。而在ServerThread的run()方法内部真正启动各种服务线程,都有: - EntropyService:提供伪随机数 - PowerManagerService:电源管理服务 @@ -384,7 +384,7 @@ SystemServer中创建了一个Socket客户端,并有AmS负责管理该客户 从系统架构的角度来看,先创建一个zygote并加载共享类的资源,然后通过该zygote去孵化新的Dalvik进程,该架构的特点有两个: -- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,必须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 +- 每一个进程都是一个Dalvik虚拟机,而Dalvik虚拟机是一个类似于Java虚拟机的程序,并且从开发的过程来看,与标准的Java程序开发基本一致。因此对于程序员来讲,不须要学习新的语言,并可以使用Java程序在过去几十年中已经成熟的各种类库资源。 - zygote进程预先会装载共享类和共享资源,这些类及资源实际上就是SDK中定义的大部分类和资源,因此,当通过zygote孵化出新的进程后,新的APK进程只需要去装载APK自身包含的类和资源即可,这就有效的解决了多个APK共享Framework资源的问题。 @@ -401,7 +401,7 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 ### 启动进程 -为了启动新进程,活动管理器需要与zygote通信。活动管理器首先开始,它创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 +为了启动新进程,活动管理器需要与zygote通信。活动管理器首先创建一个与zygote相连的专用接口,通过接口发送一条指令,表示它需要启动一个进程。这条指令主要描述需要创建的沙箱、新进程运行所需要的UID以及需要遵守的安全性制约。zygote需要作为根来运行:创建新进程时,它合理配置运行所需的UID,最终下放权限,将进程改为该UID。 ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_start_process.png?raw=true) @@ -409,8 +409,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 1. 某个现有进程(如应用程序启动器)调用活动管理器,发出意图,描述它想要启动的新活动。 2. 活动管理器要求封装管理器将这个意图解析为一个明确的组件。 -3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新锦成。 -4. zygote进行一次fork指令,克隆自己来创造一个新进程,下方权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 +3. 活动管理器判断这个应用程序的进程并未正在运行,然后向zygote请求一个具有合适UID的新进程。 +4. zygote进行一次fork指令,克隆自己来创造一个新进程,下放权限并配置新进程的UID和沙箱,初始化该进程的Dalvik,使得Java runtime开始完全执行。例如,它需要在fork后启动垃圾收集等线程。 5. 新进程如今是一个zygote的克隆,并运行着完全配置好的Java环境。它回调活动管理器,询问后者“我该做什么”。 6. 活动管理器返回即将启动的应用程序的完整信息,如源码位置等。 7. 新进程读取应用程序的源码,开始运行。 @@ -421,23 +421,6 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 - - -### 进程生命周期 - -活动管理器也负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供其,据此可判断该进程的重要程度。 - -Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定器oom_adj。/proc//oom_adj: - -- 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill -- 该设置参数的存在是为了和旧版本的内核兼容 - -![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) - -让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 - - - ### Android系统启动的核心流程如下: @@ -452,9 +435,9 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj 3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 -6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManager、WindowManager、PackageManager、PowerManager等服务。 +6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManagerService、WindowManagerService、PackageManagerService、PowerManagerService等服务。 7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 -8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AMS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 +8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AmS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 @@ -494,9 +477,13 @@ Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj +--- + + + - [上一篇:1.操作系统简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/1.%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E7%AE%80%E4%BB%8B.md) -- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) +- [下一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) --- diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 2490734b..fa5e3048 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -19,7 +19,7 @@ -进程对应的内存空间中所包含的5种不同的数据区: +进程对应的内存空间中所包含的6种不同的数据区: - 代码段(code segment):用来存放程序运行代码的一块内存空间。此空间大小在代码运行前就已经确定。内存空间一般属于只读,某些架构的代码也允许可写。 @@ -28,7 +28,7 @@ - BSS段(bss segment):存储未初始化的全局变量和未初始化的static变量。bss段中数据的生存期随进程持续性。bss段中的数据一般默认为0.这里很奇怪,一般的书上都会说全局变量和静态变量是会自动初始化的,怎么突然来了个未初始化的变量?其实变量的初始化可以分为显式初始化和隐式初始化,全局变量和静态变量如果程序员自己不初始化的话也会被初始化,那就是不管什么类型都初始化为默认值,这种没有显示初始化的就是这里所说的未初始化。例如整数型的全局变量未初始化的默认隐式初始化的值为0,都是0就没必要把每个0都存储起来,从而节省磁盘空间,这是BSS的主要作用)。BSS的全称是Block Started by Symbol,它属于静态内存分配。BSS节不包含任何数据,只是简单的维护开始和结束的地址,即总大小,以便内存能在运行时分配并被有效的清零。BSS节在应用程序的二进制映像文件中并不存在,既不占用磁盘空间,而只在运行的时候占用内存空间,所以如果全局变量和静态变量未初始化那么其可执行文件要小很多。 - rodata段(read only data):常量区,存放只读数据。比如程序中定义为const的全局变量,“Hello Word”的字符串常量。有些系统中rodata段是多个进程共享的,目的是为了提高空间利用率。在有的嵌入式系统中,rodata放在ROM中,运行时直接读取,不须加载到RAM内存中。所以在嵌入式开发中,常将已知的常量系数,表格数据等加以const关键字。存放在ROM中,避免占用RAM空间。 -- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) +- 堆(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc、realloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减) - 栈(stack):栈是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。栈的生存期随代码块持续性,代码块运行就给你分配空间,代码块结束就自动回收空间。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进先出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。 @@ -38,11 +38,11 @@ ## 内存的演变 -在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于 ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch 时,之前内存里的内容怎么办?简单粗暴的方式就是先 dump 到磁盘上,然后再从磁盘上 restore 之前 dump 的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如 code / stack / heap。一个 16 KB 的地址空间可能长这样: +在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch时,之前内存里的内容怎么办?简单粗暴的方式就是先dump到磁盘上,然后再从磁盘上restore之前dump的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。因此操作系统需要对物理内存做一层抽象,也就是「地址空间」(Address Space),一个进程的地址空间包含了该进程所有相关内存,比如code/stack/heap。一个16KB的地址空间可能长这样: ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/memory_1.jpg?raw=true) -Stack 和 Heap 中间有一块 free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 +Stack和Heap中间有一块free space,即使没有用,也被占着,那如何才能解放这块区域呢,那就是虚拟内存。 Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自互不干涉的进程地址空间。该空间是块大小为4G的线性虚拟空间,用户所看到和接触到的都是该虚拟地址,无法看到实际的物理内存地址。利用这种虚拟地址不但能起到保护操作系统的效果(用户不能直接访问物理内存),而且更重要的是,用户程序可使用比实际物理内存更大的地址空间。 @@ -138,7 +138,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 分页和分段的两个特点: -- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 +- 进程中的所有内存访问的都是逻辑地址,这些逻辑地址会在运行时把动态地址转换为物理地址。这意味着一个进程可被换入或换出内存,因此进程可在执行过程中的不同时刻占据内存中的不同区域。 - 一个进程可划分为许多块(页和段),在执行过程中,这些块不需要连续的位于内存中。动态运行时地址转换和页表或段表的使用使得这一点成为可能。 由于上面这两个特点的存在,那么在一个进程的执行过程中,该进程不需要所有页或段都在内存中。如果内存中保存有待取的下一条指令所在块(段或页)及待访问的下一个数据单元所在块,那么执行至少可以暂时继续下去。我们用术语“块”来表示页或段。处理器在需要访问一个不在内存中的逻辑地址时,会产生一个中断,这表明出现了内存访问故障。操作系统会把被中断的进程置于堵塞态。要继续执行这个进程,操作系统必须把包含引发访问故障的逻辑地址的进程块读入内存。为此操作系统产生一个磁盘I/O读请求。产生I/O请求后,在执行磁盘I/O期间,操作系统可以调度另一个进程运行。在需要的块读入内存后,产生一个I/O中断,控制权交回给操作系统,而操作系统则把由于缺少该块而被堵塞的进程置为就绪态。这样的话就会有两种提高系统利用率的方法: @@ -162,14 +162,14 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 -虚拟内存机制使得期望运行大于物理内存的程序称为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 +虚拟内存机制使得期望运行大于物理内存的程序成为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 **虚拟内存**的**基本思想**是:每个程序都拥有自己的地址空间,这个空间被分割成多个块,每个块被成为一页或页面. **程序运行时,并不是所有页都在物理内存中**: - 当程序引用一部分在物理内存的地址空间时,由硬件直接执行必要的映射; -- 当程序引用一部分不在物理内存的地址空间时,有操作系统将缺失的页装入物理内存,并重新运行 +- 当程序引用一部分不在物理内存的地址空间时,由操作系统将缺失的页装入物理内存,并重新运行 虚拟内存基于分段和分页这两种基本技术或基于这两种技术中的一种。虚拟内存机制允许程序以逻辑方式访问存储器,而不考虑物理内存上可用的空间数量。虚存的构想是为了满足有多个用户作业同时驻留在内存中的要求,因此在一个进程被写出到副主存储器中且后续进程被读入时,连续的进程执行之间将不会脱节。进程大小不同时,若处理器在很多进程间切换,则很难把它们紧密的压入内存,因此人们引入了分页系统。在分页系统中,进程由许多固定大小的块组成。这些块成为页。程序通过虚地址(virtual address)访问,虚地址由页号和页中的偏移量组成。进程的每页都可置于内存中的任何地方,分页系统提供了程序中使用的虚地址和内存中的实地址(real address)或物理地址之间的动态映射。 @@ -179,9 +179,9 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 1. CPU中包含**MMU内存管理单元**,用于管理虚拟地址空间到物理内存地址的映射. 2. 假设物理内存地址大小为32k,每4k为一个页框.虚拟地址空间分页,每个页面大小等于一个页框 -3. 当程序想要访问一个虚拟地址x, -4. 指令将x送到MMU, -5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中: +3. 当程序想要访问一个虚拟地址x +4. 指令将x送到MMU +5. MMU根据x的虚拟地址,判断其对应的页面是否在物理内存中 6. 若在,MMU将x转化为物理内存地址y 7. 若不在,则进行缺页中断,操作系统在物理内存中找到一个使用较少的页面回收掉,将需要访问的页面读到被回收的页面处,再将x转化为物理内存地址访问 @@ -197,7 +197,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 - 请求分页 - 只有当访问到某页中的一个单元时才将改页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 + 只有当访问到某页中的一个单元时才将该页取入内存。若内存管理的其他策略比较合适,将发生下述情况:当一个进程首次启动时,会在一段时间出现大量的缺页中断,取入越来越多的页后,局部性原理表明大多数将来访问的页都是最近去读的页。因此在一段时间后错误会逐渐减少,缺页中断的数量会降低到很低。 - 预先分页 @@ -233,7 +233,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 #### 6.加载控制 -加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间话费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 +加载控制会影响到驻留内存中的进程数量,这称为系统并发度。加载控制策略在有效的内存管理中非常重要。如果某一时刻驻留的进程太少,那么所有进程都处于堵塞态的概率就较大,因为会有许多时间花费在交换上。另一方面,如果驻留的进程太多,平均每个进程的驻留集大小将会不够用,此时会频繁发生缺页中断,从而导致系统抖动。 @@ -244,14 +244,21 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: - ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 + - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 -- Low Memory Killer:andorid基于oomKiller原理所扩展的一个多层次oomKiller。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOMkille会选择性杀掉进程,到oom的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOMKiller,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。 +- Low Memory Killer:andorid基于OOM Killer原理所扩展的一个多层次OOM Killer。在Android中运行了一个OOM进程,该进程启动时会首先向Linux内核中把自己注册为一个OOM Killer,即当Linux内核的内存管理模块检测到系统内存低的时候会通知已经注册的OOM进程,然后这些OOM kille会选择性杀掉进程,到OOM的时候,系统可能已经不太稳定,而LowMemoryKiller是一种根据内存阈值级别触发的内存回收的机制,在系统可用内存较低时,就会选择性杀死进程的策略,相对OOM Killer,更加灵活。主存耗尽时,使用大量内存的应用不是让步对内存的使用就是被终结。这个功能可让系统通知应用释放内存,如果应用不配合,则终结应用。这些都是由活动管理器来负责判断何时进程不再被需要。活动管理器记录一个进程中运行的所有活动、接收器、服务以及内容提供者,据此可判断该进程的重要程度。 + + Android内核中的内存溢出强制结束指令是使用一个进程的oom_adj进行严格排序,决定哪个进程需要优先强制结束。活动管理器负责基于每个进程的状态,通过将其归类于几个主要用途,从而合理设定其oom_adj。/proc//oom_adj: + - 取值是-17到+15,取值越高,越容易被干掉。如果是-17,则表示不能被kill + - 该设置参数的存在是为了和旧版本的内核兼容 -Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 + ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_process_adj.png?raw=true) + 让RAM内存不足时,系统已经完成了进程的配置,使得内存溢出强制结束命令优先中止缓存(cache)类型的进程,尝试重新取得足够的所需内存,随后中止界面(home)类别、服务(service)类别,以此类推。在同一个oom_adj水平中,它将优先中止内存占用较大的进程。 + Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋里内存作为最大内存,所以AmS中采用了自动杀死优先级较低进程的方法以达到释放内存的目的。 @@ -261,6 +268,8 @@ Android的底层Linux未采用磁盘虚拟内存机制,程序只能使用屋 +--- + - [上一篇:2.进程和线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) diff --git "a/OperatingSystem/4.\350\260\203\345\272\246.md" "b/OperatingSystem/4.\350\260\203\345\272\246.md" index 568d6cf7..97635ae5 100644 --- "a/OperatingSystem/4.\350\260\203\345\272\246.md" +++ "b/OperatingSystem/4.\350\260\203\345\272\246.md" @@ -2,7 +2,7 @@ -在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处理堵塞状态。典型的调度有四种: +在多道程序设计系统中,内存中有多个进程,每个进程要么正在处理器上运行,要么正在等待某些事件的发生,比如I/O完成。处理器调度的目的是:以满足系统目标(如响应时间、吞吐率、处理器效率)的方式,把进程分配到一个或多个处理器上执行。处理器(或处理器组)通过执行某个进程而保持忙状态,而此时其他进程处于堵塞状态。典型的调度有四种: ### 长程调度 @@ -41,13 +41,13 @@ 多处理器系统分为以下几类: -- 松耦合、分布式多处理器、集群:又一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 +- 松耦合、分布式多处理器、集群:由一系列相对自治的系统组成,每个处理器都有自身的内存和I/O通道。 - 专用处理器:I/O处理器是一个典型的例子。此时,有一个通用的主处理器,专用处理器由主处理器控制,并为主处理器提供服务。 - 紧耦合多处理器:由一些列共享同一个内存并受操作系统完全控制的处理器组成。 -多处理器中的调度设计三个相互关联的问题: +多处理器中的调度涉及三个相互关联的问题: - 把进程分配到处理器 @@ -71,17 +71,19 @@ 在多处理器线程调度和处理器分配的各种方案中,有四个比较突出的方法: -- 负载分配:进程不分配到某个特定的从处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 +- 负载分配:进程不分配到某个特定的处理器。系统维护一个就绪线程的全局队列,每个处理器只要空闲就从队列中选择一个线程。 - 组调度:一组相关的的线程基于一对一的原则,同时调度到一组处理器上运行。 - 专用处理器分配:这种方法与负载分配方法正好相反,它通过把线程指定到处理器来定义隐式的调度。每个程序在其执行过程中,都分配给一组处理器,处理器的数量与程序中线程的数量相等。程序终止时,处理器返回总处理器池,以便分配给另一个程序。 - 动态调度:在执行期间,进程中线程的数据可以改变,允许动态地改变进程中的线程数量,这就使得操作系统可以通过调整负载情况来提高利用率。 +--- + - [上一篇:3.内存管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/3.%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86.md) -- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) +- [下一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) --- diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 29367863..35535833 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -18,7 +18,7 @@ DMA技术工作流程如下: - 如果处理器想读或写一块数据时,它通过想DMA模块发送以下信息来给DMA模块发出一条命令: + 如果处理器想读或写一块数据时,它通过向DMA模块发送以下信息来给DMA模块发出一条命令: - 请求读操作或写操作的信号,通过在处理器和DMA模块之间使用读写控制线发送。 - 相关I/O设备地址,通过数据线发送。 @@ -33,7 +33,7 @@ -高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的告诉缓存中。若在则该请求可通过这个告诉缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 +高速缓冲存储器(cache memory)通常指比内存小且比内存块的存储器,它位于内存和处理器之间。这种高速缓冲存储器利用局部性原理来减少平均存储器的存取时间。同样的原理也适用于磁盘存储器。磁盘高速缓存是内存中为磁盘扇区设置的一个缓冲区,它包含有磁盘中某些扇区的副本。出现对某一特定扇区的I/O请求时,首先会进行检测,以确定该扇区是否在磁盘的高速缓存中。若在则该请求可通过这个高速缓存来满足。若不在,则把被请求的扇区从磁盘读到磁盘高速缓存中。 @@ -46,6 +46,10 @@ +--- + + + - [上一篇:4.调度](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/4.%E8%B0%83%E5%BA%A6.md) - [下一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 3621dd4d..4940fb5d 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -54,6 +54,8 @@ Android文件系统目录的顶层部分: +--- + - [上一篇:5.I/O](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/5.I:O.md) diff --git "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" index e37630e3..ac818dfb 100644 --- "a/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" +++ "b/OperatingSystem/7.\345\265\214\345\205\245\345\274\217\347\263\273\347\273\237.md" @@ -18,7 +18,7 @@ Android是基于Linux内核的一个嵌入式系统,因此我们可以认为An - +--- - [上一篇:6.文件管理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/6.%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86.md) - [下一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) diff --git "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" index 47f61216..ea7fd949 100644 --- "a/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" +++ "b/OperatingSystem/8.\350\231\232\346\213\237\346\234\272.md" @@ -16,7 +16,7 @@ 尽管Java虚拟机(JVM)用“虚拟机”作为其名称的一部分,但其实现和用途与我们前面所讲的模型不同。虚拟机管理程序支持在主机上运行一个或多个虚拟机。这些虚拟机独立的处理工作负载,支持操作系统和应用,且在它们自身看来,访问一系列提供计算、存储和输入/输出的资源。Java虚拟机的目的是,无须更改任何Java代码就可在任意硬件平台的任意操作系统上,提供运行时空间。两种模型的目的都是通过使用某种程度的抽象化来实现平台无关性。 -JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾手机的堆、一个存储代码和常量的方法区。 +JVM可描述为一个抽象的计算设备,它包含指令集、一个PC(程序计数器)寄存器、一个用来保存变量和结果的栈、一个保存运行时数据和垃圾收集的堆、一个存储代码和常量的方法区。 JVM支持多个线程,每个线程都有自己的寄存器和堆栈区,且所有线程共享栈和方法区。 @@ -81,9 +81,10 @@ DVM运行Java语言的应用和代码。标准的Java编译器将源代码(写 以上命令首先将jar包push到/data/app目录下,因为该目录一般用于存放应用程序,接着使用adb shell执行dalvikvm程序。dalvikvm的执行语法是: dalvikvm -cp 类路径 类名,从这里可以感觉到,dalvikvm的作用就像在pc上执行java程序一样。 - +--- - [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) +- [下一篇:Android内核](https://github.com/CharonChui/AndroidNote/tree/master/OperatingSystem/AndroidKernal) --- diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 0a6b51a4..68b17d24 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -11,9 +11,9 @@ Binder,英文的意思是别针、回形针。我们经常用别针把两张 无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: - 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; -- 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信; -- 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; -- 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信; +- 消息队列:信息会复制两次,会有额外的CPU消耗;不合适频繁或信息量大的通信; +- 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; +- 套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信; - 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 - 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; @@ -44,7 +44,7 @@ Binder作为Android系统提供的一种IPC机制。首先一个问题就是为 **接下来正面回答这个问题,从5个角度来展开对Binder的分析:** -**(1)从性能的角度** **数据拷贝次数:**Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 +**(1)从性能的角度** :数据拷贝次数,Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 **(2)从稳定性的角度** Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 @@ -52,18 +52,14 @@ Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client) 仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: **(3)从安全的角度** -传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 +传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 -针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但**同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。**对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 +针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 -**说到这,可能有人要反驳**,Android就算用了Binder架构,而现如今Android手机的各种流氓软件,不就是干着这种偷窥隐射,后台偷偷跑流量的事吗?没错,确实存在,但这不能说Binder的安全性不好,因为Android系统仍然是掌握主控权,可以控制这类App的流氓行为,只是对于该采用何种策略来控制,在这方面android的确存在很多有待进步的空间,这也是google以及各大手机厂商一直努力改善的地方之一。在Android 6.0,google对于app的权限问题作为较多的努力,大大收紧的应用权限;另外,在**Google举办的Android Bootcamp 2016**大会中,google也表示在Android 7.0 (也叫Android N)的权限隐私方面会进一步加强加固,比如SELinux,Memory safe language(还在research中)等等,在今年的5月18日至5月20日,google将推出Android N。 - -话题扯远了,继续说Binder。 - **(4)从语言层面的角度** 大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 @@ -73,11 +69,11 @@ Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用 总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 -而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下 开源与商业化共存的一个成功典范。 +而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 **有了这些铺垫,我们再说说Binder的今世前缘** -Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 +Binder是基于开源的[OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 @@ -87,10 +83,10 @@ Binder是基于开源的 [OpenBinder](https://link.zhihu.com/?target=http%3A//ww **最后,简单讲讲Android Binder架构** -Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制。 +Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制: -1. **Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。**例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; -2. **Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。**例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 +1. Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; +2. Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 @@ -100,21 +96,21 @@ Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_ser 前面人都说了Binder的优点,我来讲故事 -1. 当年Andy Rubin有个公司 Palm 做掌上设备的 就是当年那种PDA 有个系统叫PalmOS 后来palm被收购了以后 Andy Rubin 创立了Android +1. 当年Andy Rubin有个公司Palm做掌上设备的就是当年那种PDA有个系统叫PalmOS后来palm被收购了以后 Andy Rubin创立了Android -2. Palm收购过一个公司叫 Be 里面有个移动系统 叫 BeOS 进程通信自己学了个实现 叫Binder 由一个叫 Dianne Hackbod的人开发并维护 后来Binder 也被用到了 PalmOS里 +2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫Binder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 -3. Android创立了以后 Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 +3. Android创立了以后Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 -- 如果你是她,你会选择用a.Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 +- 如果你是她,你会选择用Linux已有的进程通信手段吗? 不会,要不当年也不会搞个新东西出来 -- 重写一个新东西 也不会 binder反正是自己写的开源库 +- 重写一个新东西?也不会,binder反正是自己写的开源库 -- 用binder 已经被两个公司用过 而且是自己写的 可靠放心 +- 用binder?已经被两个公司用过而且是自己写的可靠放心 -我是她我就选C +我是她我就用binder。 -你可以看到 如果当年Dianne没有加入Be 或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 +你可以看到 如果当年Dianne没有加入Be或者Be没有被收购 ,又或者Dianne没有和Andy加入Android 那Android也不一定会用binder。 @@ -146,7 +142,7 @@ Binder是一种架构,这种架构提供了服务端接口、Binder驱动、 -每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 +每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl(设备驱动程序中设备控制接口函数,进程与内核通信的一种方法)等方法跟内核空间的驱动进行交互。 @@ -154,7 +150,7 @@ Binder通信采用C/S架构,从组件视角来说,包含Client、Server、Se ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) -可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程,要掌握Binder机制,首先需要了解系统是如何首次[启动Service Manager](http://gityuan.com/2015/11/07/binder-start-sm/)。当Service Manager启动之后,Client端和Server端通信时都需要先[获取Service Manager](http://gityuan.com/2015/11/08/binder-get-sm/)接口,才能开始通信服务。 +可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。 图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 @@ -189,8 +185,14 @@ BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代 +--- + +- [上一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) -- [上一篇:7.嵌入式系统](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/7.%E5%B5%8C%E5%85%A5%E5%BC%8F%E7%B3%BB%E7%BB%9F.md) + +- [下一篇:2.Android线程间通信之Handler消息机制](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md) + + --- From cd425547bca4d9585c4c3f435bdc32fc6eef5bdf Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 4 Dec 2020 19:48:54 +0800 Subject: [PATCH 010/183] format text --- ...13\351\227\264\351\200\232\344\277\241.md" | 2 - ...10\346\201\257\346\234\272\345\210\266.md" | 85 +- ...roid Framework\346\241\206\346\236\266.md" | 7 +- ...ManagerService\347\256\200\344\273\213.md" | 15 +- ...10\346\201\257\350\216\267\345\217\226.md" | 11 +- ...30\345\210\266\345\237\272\347\241\200.md" | 5 +- ...30\345\210\266\345\216\237\347\220\206.md" | 1613 +++++++++++++++++ ...ManagerService\347\256\200\344\273\213.md" | 9 +- ...ManagerService\347\256\200\344\273\213.md" | 10 +- 9 files changed, 1704 insertions(+), 53 deletions(-) diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 68b17d24..7263e837 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -183,8 +183,6 @@ BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代 - - --- - [上一篇:8.虚拟机](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/8.%E8%99%9A%E6%8B%9F%E6%9C%BA.md) diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" index df78f0cf..dbcaf9bb 100644 --- "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -15,7 +15,7 @@ Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线 - Message:消息分为硬件产生的消息(如按钮、触摸)和软件生成的消息;Message中有一个用于处理消息的Handler; - MessageQueue:消息队列的主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);MessageQueue有一组待处理的Message; -- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue。 +- Handler:消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);Handler中有Looper和MessageQueue的成员变量。 - Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; @@ -57,7 +57,7 @@ public Handler(@NonNull Looper looper) { } public Handler(@Nullable Callback callback, boolean async) { - // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 + // 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露 if (FIND_POTENTIAL_LEAKS) { final Class klass = getClass(); if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) && @@ -73,7 +73,7 @@ public Handler(@Nullable Callback callback, boolean async) { "Can't create handler inside thread " + Thread.currentThread() + " that has not called Looper.prepare()"); } - // 获取Looper中的MessageQueue + // 获取Looper中的MessageQueue赋值给当前的MessageQueue变量 mQueue = mLooper.mQueue; // 回调接口 mCallback = callback; @@ -181,11 +181,11 @@ private Looper(boolean quitAllowed) { ```java /** - * Initialize the current thread as a looper, marking it as an - * application's main looper. The main looper for your application - * is created by the Android environment, so you should never need - * to call this function yourself. See also: {@link #prepare()} - */ +* Initialize the current thread as a looper, marking it as an +* application's main looper. The main looper for your application +* is created by the Android environment, so you should never need +* to call this function yourself. See also: {@link #prepare()} +*/ public static void prepareMainLooper() { prepare(false); synchronized (Looper.class) { @@ -393,11 +393,11 @@ public static void loop() { Trace.traceEnd(traceTag); } } - // 恢复调用者信息 + // 恢复调用者信息 // Make sure that during the course of dispatching the // identity of the thread wasn't corrupted. final long newIdent = Binder.clearCallingIdentity(); - // 将Message放入消息池 + // 将Message放入消息池 msg.recycleUnchecked(); } } @@ -503,8 +503,8 @@ public void handleMessage(@NonNull Message msg) { } /** - * Handle system messages here. - */ + * Handle system messages here. + */ public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { // 如果Message存在回调方法,就回调Message.callback.run()方法 @@ -571,7 +571,7 @@ public final class MessageQueue { if (nextPollTimeoutMillis != 0) { Binder.flushPendingCommands(); } - // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 + // 该方法的作用是从消息队列中取出一个消息。MessageQueue中没有保存消息队列,真正的消息队列在JNI的C代码中 // 也就是在C环境中创建了一个NativeMessageQueue数据对象。该方法的第一个参数是int型变量,在C环境中该变量 // 会被强制转换为一个NativeMessageQueue对象。如果消息队列中没消息,当前线程会被挂起。 // 堵塞操作,当等待nextPollTimeoutMillis时长或消息队列别唤醒,都会返回 @@ -640,7 +640,7 @@ public final class MessageQueue { } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } - // 空闲回调函数 + // 空闲回调函数 // Run the idle handlers. // We only ever reach this code block during the first iteration. for (int i = 0; i < pendingIdleHandlerCount; i++) { @@ -770,7 +770,7 @@ public final class MessageQueue { synchronized (this) { Message p = mMessages; - // 从消息队列的头开始,移除所有符合条件的消息 + // 从消息队列的头开始,移除所有符合条件的消息 // Remove all messages at front. while (p != null && p.target == h && (object == null || p.obj == object)) { @@ -779,7 +779,7 @@ public final class MessageQueue { p.recycleUnchecked(); p = n; } - // 移除剩余符合条件的消息 + // 移除剩余符合条件的消息 // Remove all messages after front. while (p != null) { Message n = p.next; @@ -807,7 +807,7 @@ public final class MessageQueue { -有关内存泄露请猛戳[内存泄露][1] +有关内存泄露请猛戳[内存泄露](https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) 在上面分析Handler类源码时,其构造函数中第一部分的代码就是 匿名类、内部类和本地类都必须申请为static,否则会警告可能出现内存泄露。那为什么会导致内存泄露呢? @@ -815,7 +815,7 @@ public final class MessageQueue { Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { - // do something. + // do something. } } ``` @@ -832,24 +832,25 @@ Handler mHandler = new Handler() { ```java public class SampleActivity extends Activity { - private final Handler mHandler = new Handler() { - @Override - public void handleMessage(Message msg) { - // do something - } - } + private final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + // do something + } + } - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - // 发送一个10分钟后执行的一个消息 - mHandler.postDelayed(new Runnable() { - @Override - public void run() { } - }, 600000); - - // 结束当前的Activity - finish(); + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // 发送一个10分钟后执行的一个消息 + mHandler.postDelayed(new Runnable() { + @Override + public void run() { } + }, 600000); + + // 结束当前的Activity + finish(); + } } ``` 在`finish()`的时候,该`Message`还没有被处理,`Message`持有`Handler`,`Handler`持有`Activity`,这样会导致该`Activity`不会被回收,就发生了内存泄露. @@ -861,14 +862,13 @@ public class SampleActivity extends Activity { - 如果`Handler`是被`delay`的`Message`持有了引用,那么在`Activity`的`onDestroy()`方法要调用`Handler`的`remove*`方法,把消息对象从消息队列移除就行了。 - 关于`Handler.remove*`方法 - `removeCallbacks(Runnable r)` ——清除r匹配上的Message。 - - `removeC4allbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 - - `removeCallbacksAndMessages(Object token)` ——清除token匹配上的Message。 + - `removeCallbacks(Runnable r, Object token)` ——清除r匹配且匹配token(Message.obj)的Message,token为空时,只匹配r。 + - `removeCallbacksAndMessages(Object token)` ——清除所有callback以及token匹配上的Message,如果token是null就会清楚所有callback和message。我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; - `removeMessages(int what)` ——按what来匹配 - `removeMessages(int what, Object object)` ——按what来匹配 - 我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; + - 将`Handler`声明为静态类。 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 - ```java public class MyActivity extends Activity { private MyHandler mHandler; @@ -905,9 +905,14 @@ public class MyActivity extends Activity { } ``` -[1]:(https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F.md) +--- + + + +- [上一篇:1.Android进程间通信](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/1.Android%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md) +- [下一篇:3.Android Framework框架](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md) diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" index 100b61d7..f550d1ee 100644 --- "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -33,7 +33,7 @@ AmS的作用是管理所有应用程序中的Activity。 - Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 - PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 - Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 -- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并相应特定的按键消息等。 +- DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。 - ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 - W类:该类继承于Binder,并且是ViewRoot的一个内部类。 - WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 @@ -69,7 +69,7 @@ Linux驱动和Framework相关的主要包含两部分: - 窗口(Window):这是一个纯语义的说法,即程序员所看到的屏幕上的某个独立的界面,比如一个带有TitleBar的Activity界面、一个对话框、一个Menu菜单等,这些都称之为窗口。从WmS的角度来讲,窗口是接收用户消息的最小单元,WmS内部用特定的类表示一个窗口,以实现对窗口的管理。WmS接收到用户消息后,首先要判断这个消息属于哪个窗口,然后通过IPC调用把这个消息传递给客户端的ViewRoot类。 - Window类:该类在android.view包中,是一个abstract类,该类是对包含有可视界面的窗口的一种包装,所谓的可视界面就是指各种View或者ViewGroup,一般可以通过res/layout目录下的xml文件描述。 -- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。 +- ViewRoot类:该类是android.view包中,客户端申请创建窗口时需要一个客户端代理,用以和WmS进行交互,这个就是ViewRoot的功能,每个客户端的窗口都会对应一个ViewRoot类。ViewRoot类在Android2.2之后就被ViewRootImpl替换了。但是为了方便,后面还是会用ViewRoot类来介绍。 - W类:该类是ViewRoot类的一个内部类,继承于Binder,用于想WmS提供一个IPC接口,从而让WmS控制窗口客户端的行为。 描述一个窗口之所以有这么多类的原因在于,窗口的概念存在于客户端和服务端(WmS)之中,客户端所理解的窗口和服务端理解的窗口是不同的,因此,在客户端和服务端会用不同的类来描述窗口。比如在客户端,用户能看到的窗口一般是View或者ViewGroup组成的窗口,而与Activity对应的窗口却是一个DecorView类,而具备常规Phone操作的接口却又是一个PhoneWindow类。所以无论是在客户端还是服务端,对窗口都有不同层面的抽象。 @@ -94,7 +94,10 @@ Context在应用程序开发中会经常被使用,在一般的计算机书记 +--- +- [上一篇:2.Android线程间通信之Handler消息机制](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/2.Android%E7%BA%BF%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E4%B9%8BHandler%E6%B6%88%E6%81%AF%E6%9C%BA%E5%88%B6.md) +- [下一篇:4.ActivityManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md) diff --git "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" index 0a5b9007..3e88e552 100644 --- "a/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/4.ActivityManagerService\347\256\200\344\273\213.md" @@ -1,6 +1,6 @@ # 4.ActivityManagerService简介 -ActivityManagerService.java文件,简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService.java和View.java。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 +ActivityManagerService简称AmS,是Android内核的三大核心功能之一,另外两个是WindowManagerService和View。Activity的管理实际上由三个主要类完成,分别是AmS、ActivityRecord及ActivityTask。AmS内部用ActivityRecord来表示一个Activity对象。ActivityStack是一个描述Task的类,所有和Task相关的数据以及控制都由ActivityStack来实现。 AmS所提供的主要功能包括以下几项: @@ -16,7 +16,7 @@ AmS所提供的主要功能包括以下几项: 在Android中,Activity调度的基本思路是:各应用进程要启动新的Activity或者停止当前的Activity,都要首先报告给AmS,而不能“擅自处理”。AmS在内部为所有应用程序都做了记录,当AmS接到启动或停止的报告时,首先更新内部记录,然后再通知相应客户进程运行或者停止指定的Activity。由于AmS内部有所有Activity的记录,也就理所当然地能够调度这些Activity,并根据Activity和系统内存的状态自动杀死后台的Activity。 -具体的讲,启动一个Activity有以下集中方式: +具体的讲,启动一个Activity有以下几种方式: - 在应用程序中调用startActivity()启动指定的Activity。 - 在Home程序中单击一个应用图标,启动新的Activity。 @@ -40,7 +40,7 @@ ProcessRecord记录一个进程中的相关信息,该类中内部变量可以 AmS中使用HistoryRecord数据类来保存每个Activity的信息,这里非常奇怪,Activity本身也是一个类,为什么还要用HistoryRecord来保存Activity的信息,而不是直接用Activity呢?这是因为Activity是具体的功能类,这就好比每一个读者都是一个Activity,而学校要为每一个读者建立一个档案,这些档案中并不包含每个读者具体的学习能力,而只是学生的籍贯信息、姓名、出生日期等,HistoryRecord就是AmS为每一个Activity建立的档案,该数据类中的变量主要包含两部分: - 环境信息:该Activity的工作环境,比如隶属于哪个Package,所在的进程名称、文件路径、数据路径、图标主题等。 -- 运行状态信息:比如idle、stop、finishing等,这些变量一般问boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 +- 运行状态信息:比如idle、stop、finishing等,这些变量一般为boolean类型,这些状态值与应用程序中的onCreate、onPause、onStart等状态有所不同。 HistoryRecord类也是一个Binder,它基于IApplication.Stub类,因此它可以被IPC调用,一般是在WmS中进行该对象的IPC调用。 @@ -131,7 +131,7 @@ TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayLis -## startActivi()启动流程 +## startActiviy()启动流程 @@ -142,6 +142,13 @@ TaskRecord中并没有该任务中所包含的Activity的列表,比如ArrayLis +--- + +- [上一篇:3.Android Framework框架](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/3.Android%20Framework%E6%A1%86%E6%9E%B6.md) +- [下一篇:5.Android消息获取](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md) + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" index f731b4e9..3fe0e833 100644 --- "a/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" +++ "b/OperatingSystem/AndroidKernal/5.Android\346\266\210\346\201\257\350\216\267\345\217\226.md" @@ -24,7 +24,7 @@ 1. 首先,InputReader线程会持续调用输入设备的驱动,读取所有用户输入的消息,该线程和InputDispatcher线程都在系统进程(system_process)空间中运行。InputDispatcher线程从自己的消息队列中取出原始消息,取出的消息有可能经过两种方式进行派发。 - 经过管道(Pipe)直接派发到客户窗口中。 - 先派发到WmS中,由WmS经过一定的处理,如果WmS没有处理该消息,则再派发给客户窗口中,否则,不派发到客户窗口。 -2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitore中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 +2. 应用程序添加窗口时,会在本地创建一个ViewRoot对象,然后通过IPC调用WmS中的Session对象的addWindow()方法,从而请求WmS创建一个窗口。WmS会把窗口的相关信息保存在内部的一个窗口列表类InputMonitor中,然后使用InputManager类把这些窗口信息传递给InputDispatcher线程。传递的过程中,InputManager类需要调用JNI代码,把这些窗口信息传递给NativeInputManager对象中。 3. 当InputDispatcher得到用户消息后,会根据NativeInputManager中保存的所有窗口信息判断当前的活动窗口是哪个,并把消息传递给该活动窗口。另外,如果是按键消息,InputDispatcher会先回调InputManager中定义的回调函数,这既会回调InputMonitor中的回调函数,又会回调WmS中定义的相关函数,所以这些回调函数的返回值类型是boolean。对于系统按键消息,比如“Home键”、电话按键等,WmS内部会按默认的方式处理,并返回false,从而InputDispatcher不会继续把这些按键消息传递给客户窗口。对于触摸屏消息,InputDispatcher则直接传递给客户窗口。 4. 在InputDispatcher和客户窗口之间使用了管道(Pipe)机制进行消息传递。Pipe是Linux的一种系统调用,Linux会在内核地址空间中开辟一段共享内存,并产生一个Pipe对象。每个Pipe对象内部都会自动创建两个文件描述符,一个用于读,另一个用于写。应用程序可以调用pipe()函数产生一个Pipe对象,并获得该对象中的读、写文件描述符。文件描述符是全局唯一的,从而使得两个进程之间可以借助这两个描述符,一个往管道中写数据,另一个从管道中读数据。管道只能是单向的,因此,如果两个进程要进行双向消息传递,必须创建两个管道。当客户窗口请求WmS创建窗口时,WmS内部会创建两个管道,其中一个管道用于InputDispatcher向客户窗口传递消息,另一个用户客户窗口向InputDispatcher报告消息的执行结果。因此,有多少个客户窗口,就有多少个管道与InputDispatcher相连。 5. 由于创建管道属于Linux系统调用,Java不能直接调用,另外也由于程序结构的需要,Android中把调用管道的相关操作封装到了InputChannel.java类中,同时该类中保存了InputDispatcher和客户窗口的管道收、发描述符。因此,InputChannel也可以理解为一个“通道”,在InputDispatcher端存在一个服务端消息通道(serverChannel)。在客户窗口端存在一个客户端消息通道(clientChannel)。管道和通道的区别是,管道是Linux系统的概念,使用pipe()系统调用就可以创建一个管道,而通道是Android内部定义的一个概念,主要保存了通信双发所使用的管道描述符。 @@ -54,6 +54,15 @@ + +--- + +- [上一篇:4.ActivityManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/4.ActivityManagerService%E7%AE%80%E4%BB%8B.md) +- [下一篇:6.屏幕绘制基础](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md) + + + + --- - 邮箱 :charon.chui@gmail.com diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index b4f24bb7..95e4f084 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -7,7 +7,7 @@ Android的屏幕绘制架构如下图: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_draw_process_archi.jpg) - SurfaceFlinger服务进程:简称sf。该进程在整个系统中只有一个实例,在系统开机后自动运行,它的作用是给每个客户端分配窗口,程序中用Surface类表示这个窗口。正如Surface字面的意义,它是一个平面,即每个窗口是一个平面,每个平面在程序中都对应一段内存,也就是所谓的屏幕缓冲区,不同窗口的缓冲区大小不同,这取决于该窗口的大小,即宽度和高度,一般来讲缓冲区的大小为宽度x高度。sf的客户端必须使用SurfaceFlinger的客户端接口驱动来和sf打交道,系统中使用该接口驱动的最重要的进程就是SystemServer进程。 -- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 +- 当一个APK程序需要创建窗口时,会使用WindowManager类的addView()函数,该函数会创建一个ViewRoot(ViewRoot类在Android2.2之后就被ViewRootImpl替换了,这里因为方便后面还是会叫做ViewRoot)对象,而ViewRoot类中会使用Surface的无参构造函数创建一个Surface对象,此时该Surface仅仅是一个空壳,然后调用WindowManager类向WmS服务发起一个请求,请求的参数中包含该Surface对象。虽然它表示的是一个窗口,但它必须经过初始化后才真正能够对应一个再屏幕上显示的窗口,而初始化的本质就是给该Surface对象分配一段屏幕缓冲区的内存。 - WmS收到这个请求后,会通过Surface类的JNI调用到SurfaceFlinger_client驱动,通过该client接口驱动请求sf进程创建指定的窗口。于是sf创建一段屏幕缓冲区,并在sf内部记录该窗口,然后sf会把该窗口的缓冲区地址传递给WmS,WmS再用这个地址去初始化APK程序传入的Surface对象,并最终回到APK程序中。此时APK程序中的Surface对象是一个真正的Surface对象了,因为它包含的屏幕缓冲区已经由sf创建并备案了。 - APK程序有了这个Surface后,就可以给这个平面上绘制任意的内容了,比如绘制矩阵、绘制文本、绘制图片等。然后Surface类本质上仅仅表示了一个平面,而绘制不同图片显然是一种操作,而不是一段数据,因此Android中使用了一个叫做Skia的绘图驱动库,该库使用C/C++语言编写而成,其作用就是能够进行各种平面绘制。在程序中用Canvas类来表示这个功能对象,Canvas类有很多绘制函数,比如drawColor()、drawLine()等。Surface类包含了一个函数lockCanvas(),APK应用程序可以通过该函数返回一个Canvas功能对象,然后就可以调用该对象的各种绘制函数完成对平面的绘制。 @@ -40,7 +40,10 @@ Android的屏幕绘制架构如下图: +--- +- [上一篇:5.Android消息获取](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/5.Android%E6%B6%88%E6%81%AF%E8%8E%B7%E5%8F%96.md) +- [下一篇:7.View绘制原理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md) diff --git "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" index a5def400..325c231f 100644 --- "a/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" +++ "b/OperatingSystem/AndroidKernal/7.View\347\273\230\345\210\266\345\216\237\347\220\206.md" @@ -2,11 +2,1624 @@ +界面窗口的根布局是`DecorView`,该类继承自`FrameLayout`.说到`View`绘制,想到的就是从这里入手,而`FrameLayout`继承自`ViewGroup`。感觉绘制肯定会在`ViewGroup`或者`View`中, +但是木有找到。发现`ViewGroup`实现`ViewParent`接口,而`ViewParent`有一个实现类是`ViewRootImpl`, `ViewGruop`中会使用`ViewRootImpl`... +```java +/** + * The top of a view hierarchy, implementing the needed protocol between View + * and the WindowManager. This is for the most part an internal implementation + * detail of {@link WindowManagerGlobal}. + * + * {@hide} + */ +@SuppressWarnings({"EmptyCatchBlock", "PointlessBooleanExpression"}) +public final class ViewRootImpl implements ViewParent, + View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { + + } +``` +`View`的绘制过程从`ViewRootImpl.performTraversals()`方法开始,你看这个名字起的多好,叫执行遍历,看到名字就能知道内部的实现: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_performTraversals.png) +首先先说明一下,这部分代码比较多,逻辑也比较麻烦,很容易弄晕,如果感觉看起来费劲,就跳过这一块,直接到下面的Measure、Layout、Draw部分开始看。 +我也没有全部弄清楚,我只是把里面的步骤标注了下。 +```java +private void performTraversals() { + // ... 此处省略源代码N行 + if (mFirst || windowShouldResize || insetsChanged || + viewVisibilityChanged || params != null || mForceNextWindowRelayout) { + // 第一或者resize等都会调用relayoutWindow,而该函数内部会调用sWindowSession.relayout() + // 方法来请求WmS按照指定的大小重新分配窗口大小,并会为客户窗口创建的mSurface对象分配真正的现存 + // 等该函数返回后,应用程序就可以在该Surface中绘制了。 + relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); + } + // 是否需要Measure + if (!mStopped) { + boolean focusChangedDueToTouchMode = ensureTouchModeLocally( + (relayoutResult&WindowManagerGlobal.RELAYOUT_RES_IN_TOUCH_MODE) != 0); + if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth() + || mHeight != host.getMeasuredHeight() || contentInsetsChanged) { + // 这里是获取widthMeasureSpec,这俩参数不是一般的尺寸数值,而是将模式和尺寸组合在一起的数值. + // getRootMeasureSpec方法内部会使用MeasureSpec.makeMeasureSpec()方法来组装一个MeasureSpec, + // 当lp.width参数等于MATCH_PARENT的时候,MeasureSpec的specMode就等于EXACTLY,当lp.width等于WRAP_CONTENT的时候,MeasureSpec的specMode就等于AT_MOST。 + // 并且MATCH_PARENT和WRAP_CONTENT时的specSize都是等于windowSize的,也就意味着根视图总是会充满全屏的。 + // 这里lp代表的是根视图的LayoutParams、lp.width和lp.height直接来源于用户的定义比如WRAP_CONTENT、MATCH_PARENT等 + int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); + int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); + + if (DEBUG_LAYOUT) Log.v(TAG, "Ooops, something changed! mWidth=" + + mWidth + " measuredWidth=" + host.getMeasuredWidth() + + " mHeight=" + mHeight + + " measuredHeight=" + host.getMeasuredHeight() + + " coveredInsetsChanged=" + contentInsetsChanged); + + // 调用PerformMeasure方法。 + // Ask host how big it wants to be + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + + // Implementation of weights from WindowManager.LayoutParams + // We just grow the dimensions as needed and re-measure if + // needs be + int width = host.getMeasuredWidth(); + int height = host.getMeasuredHeight(); + boolean measureAgain = false; + + if (lp.horizontalWeight > 0.0f) { + width += (int) ((mWidth - width) * lp.horizontalWeight); + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, + MeasureSpec.EXACTLY); + measureAgain = true; + } + if (lp.verticalWeight > 0.0f) { + height += (int) ((mHeight - height) * lp.verticalWeight); + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, + MeasureSpec.EXACTLY); + measureAgain = true; + } + + if (measureAgain) { + if (DEBUG_LAYOUT) Log.v(TAG, + "And hey let's measure once more: width=" + width + + " height=" + height); + performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + layoutRequested = true; + } + } + + final boolean didLayout = layoutRequested && !mStopped; + boolean triggerGlobalLayoutListener = didLayout + || mAttachInfo.mRecomputeGlobalAttributes; + // 是否需要Layout + if (didLayout) { + // 调用performLayout方法。 + performLayout(lp, desiredWindowWidth, desiredWindowHeight); + + // By this point all views have been sized and positioned + // We can compute the transparent area + + if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { + // start out transparent + // TODO: AVOID THAT CALL BY CACHING THE RESULT? + host.getLocationInWindow(mTmpLocation); + mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], + mTmpLocation[0] + host.mRight - host.mLeft, + mTmpLocation[1] + host.mBottom - host.mTop); + + host.gatherTransparentRegion(mTransparentRegion); + if (mTranslator != null) { + mTranslator.translateRegionInWindowToScreen(mTransparentRegion); + } + + if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { + mPreviousTransparentRegion.set(mTransparentRegion); + mFullRedrawNeeded = true; + // reconfigure window manager + try { + mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); + } catch (RemoteException e) { + } + } + } + + if (DBG) { + System.out.println("======================================"); + System.out.println("performTraversals -- after setFrame"); + host.debug(); + } + } + + // 是否需要Draw + if (!cancelDraw && !newSurface) { + if (!skipDraw || mReportNextDraw) { + if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).startChangingAnimations(); + } + mPendingTransitions.clear(); + } + // 调用performDraw方法 + performDraw(); + } + } else { + if (viewVisibility == View.VISIBLE) { + // Try again + scheduleTraversals(); + } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { + for (int i = 0; i < mPendingTransitions.size(); ++i) { + mPendingTransitions.get(i).endChangingAnimations(); + } + mPendingTransitions.clear(); + } + } + + mIsInTraversal = false; +} +``` + +从上面源码可以看出,`performTraversals()`方法中会依次做三件事: +- `performMeasure()`, 内部是` mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);`测量`View`大小。这里顺便提一下,这个`mView`是什么?它就是`Window`最顶成的`View(DecorView)`,它是`FrameLayout`的子类。 +- `performLayout()`, 内部是`mView.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());`视图布局,确定`View`位置。 +- `performDraw()`, 内部是`draw(fullRedrawNeeded);` 绘制界面。 + +至此`View`绘制的三个过程已经展现: + +`Measure` +=== + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_measure.png) + + + +`performMeasure`方法如下: + +```java +private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); + try { + mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } +} +``` + +在`performMeasure()`方法中会调用`View.measure()`方法, 源码如下: +```java +/** + *

+ * This is called to find out how big a view should be. The parent + * supplies constraint information in the width and height parameters. + *

+ * + *

+ * The actual measurement work of a view is performed in + * {@link #onMeasure(int, int)}, called by this method. Therefore, only + * {@link #onMeasure(int, int)} can and must be overridden by subclasses. + *

+ * + * + * @param widthMeasureSpec Horizontal space requirements as imposed by the + * parent + * @param heightMeasureSpec Vertical space requirements as imposed by the + * parent + * + * @see #onMeasure(int, int) + */ +public final void measure(int widthMeasureSpec, int heightMeasureSpec) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int oWidth = insets.left + insets.right; + int oHeight = insets.top + insets.bottom; + // adjust是微调某个MeasureSpec的大小 + widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); + heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); + } + + // Suppress sign extension for the low bytes + long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL; + if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2); + + if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || + widthMeasureSpec != mOldWidthMeasureSpec || + heightMeasureSpec != mOldHeightMeasureSpec) { + + // first clears the measured dimension flag + mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; + + resolveRtlPropertiesIfNeeded(); + + int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 : + mMeasureCache.indexOfKey(key); + if (cacheIndex < 0 || sIgnoreMeasureCache) { + // 调用onMeasure方法 + // measure ourselves, this should set the measured dimension flag back + onMeasure(widthMeasureSpec, heightMeasureSpec); + mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } else { + long value = mMeasureCache.valueAt(cacheIndex); + // Casting a long to int drops the high 32 bits, no mask needed + setMeasuredDimensionRaw((int) (value >> 32), (int) value); + mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } + + // flag not set, setMeasuredDimension() was not invoked, we raise + // an exception to warn the developer + if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { + // 重写onMeausre方法的时,必须调用setMeasuredDimension或者super.onMeasure方法,不然就会走到这里报错。 + // setMeasuredDimension中回去改变mPrivateFlags的值 + throw new IllegalStateException("onMeasure() did not set the" + + " measured dimension by calling" + + " setMeasuredDimension()"); + } + + mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; + } + + mOldWidthMeasureSpec = widthMeasureSpec; + mOldHeightMeasureSpec = heightMeasureSpec; + + mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | + (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension +} +``` + +在`measure`方法中会调用`onMeasure`方法。`ViewGroup`的子类会重写该方法来进行测量大小,因为`mView`是`DecorView`, +而`DecorView`是`FrameLayout`的子类。所以我们看一下`FrameLayout.onMeasure`方法: +`FrameLayout.onMeasure`源码如下: +```java +/** + * {@inheritDoc} + */ +@Override +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int count = getChildCount(); + + final boolean measureMatchParentChildren = + MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || + MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; + mMatchParentChildren.clear(); + + int maxHeight = 0; + int maxWidth = 0; + int childState = 0; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (mMeasureAllChildren || child.getVisibility() != GONE) { + // 调用该方法去测量每个子View + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + maxWidth = Math.max(maxWidth, + child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); + maxHeight = Math.max(maxHeight, + child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); + childState = combineMeasuredStates(childState, child.getMeasuredState()); + if (measureMatchParentChildren) { + if (lp.width == LayoutParams.MATCH_PARENT || + lp.height == LayoutParams.MATCH_PARENT) { + mMatchParentChildren.add(child); + } + } + } + } + + // Account for padding too + maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); + maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); + + // Check against our minimum height and width + maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight()); + maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth()); + + // Check against our foreground's minimum height and width + final Drawable drawable = getForeground(); + if (drawable != null) { + maxHeight = Math.max(maxHeight, drawable.getMinimumHeight()); + maxWidth = Math.max(maxWidth, drawable.getMinimumWidth()); + } + + setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState), + resolveSizeAndState(maxHeight, heightMeasureSpec, + childState << MEASURED_HEIGHT_STATE_SHIFT)); + + count = mMatchParentChildren.size(); + if (count > 1) { + for (int i = 0; i < count; i++) { + final View child = mMatchParentChildren.get(i); + + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + if (lp.width == LayoutParams.MATCH_PARENT) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth() - + getPaddingLeftWithForeground() - getPaddingRightWithForeground() - + lp.leftMargin - lp.rightMargin, + MeasureSpec.EXACTLY); + } else { + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeftWithForeground() + getPaddingRightWithForeground() + + lp.leftMargin + lp.rightMargin, + lp.width); + } + + if (lp.height == LayoutParams.MATCH_PARENT) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight() - + getPaddingTopWithForeground() - getPaddingBottomWithForeground() - + lp.topMargin - lp.bottomMargin, + MeasureSpec.EXACTLY); + } else { + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTopWithForeground() + getPaddingBottomWithForeground() + + lp.topMargin + lp.bottomMargin, + lp.height); + } + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } +} +``` + +我们看到内部会调用`measureChildWithMargins()`方法,该方法源码如下: +```java +/** + * Ask one of the children of this view to measure itself, taking into + * account both the MeasureSpec requirements for this view and its padding + * and margins. The child must have MarginLayoutParams The heavy lifting is + * done in getChildMeasureSpec. + * + * @param child The child to measure + * @param parentWidthMeasureSpec The width requirements for this view + * @param widthUsed Extra space that has been used up by the parent + * horizontally (possibly by other children of the parent) + * @param parentHeightMeasureSpec The height requirements for this view + * @param heightUsed Extra space that has been used up by the parent + * vertically (possibly by other children of the parent) + */ +protected void measureChildWithMargins(View child, + int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, + mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin + + heightUsed, lp.height); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); +} +``` +里面就是对该子`View`调用了`measure`方法,我们假设这个`View`已经不是`ViewGroup`了,就会又和上面一样,又调用`onMeasure`方法, +下面我们直接看一下`View.onMeasure()`方法: +`View.onMeasure()`方法的源码如下: +```java +/** + *

+ * Measure the view and its content to determine the measured width and the + * measured height. This method is invoked by {@link #measure(int, int)} and + * should be overriden by subclasses to provide accurate and efficient + * measurement of their contents. + *

+ * + *

+ * CONTRACT: When overriding this method, you + * must call {@link #setMeasuredDimension(int, int)} to store the + * measured width and height of this view. Failure to do so will trigger an + * IllegalStateException, thrown by + * {@link #measure(int, int)}. Calling the superclass' + * {@link #onMeasure(int, int)} is a valid use. + *

+ * + *

+ * The base class implementation of measure defaults to the background size, + * unless a larger size is allowed by the MeasureSpec. Subclasses should + * override {@link #onMeasure(int, int)} to provide better measurements of + * their content. + *

+ * + *

+ * If this method is overridden, it is the subclass's responsibility to make + * sure the measured height and width are at least the view's minimum height + * and width ({@link #getSuggestedMinimumHeight()} and + * {@link #getSuggestedMinimumWidth()}). + *

+ * + * @param widthMeasureSpec horizontal space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * @param heightMeasureSpec vertical space requirements as imposed by the parent. + * The requirements are encoded with + * {@link android.view.View.MeasureSpec}. + * + * @see #getMeasuredWidth() + * @see #getMeasuredHeight() + * @see #setMeasuredDimension(int, int) + * @see #getSuggestedMinimumHeight() + * @see #getSuggestedMinimumWidth() + * @see android.view.View.MeasureSpec#getMode(int) + * @see android.view.View.MeasureSpec#getSize(int) + */ +protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // 如果不重写onMeasure方法,默认会调用getDefaultSize获取大小,下面会说getDefaultSize这个方法。 + setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)); +} +``` + +`setMeasuredDimension()`方法如下: +```java +/** + *

This method must be called by {@link #onMeasure(int, int)} to store the + * measured width and measured height. Failing to do so will trigger an + * exception at measurement time.

+ * + * @param measuredWidth The measured width of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * @param measuredHeight The measured height of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + */ +protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) { + boolean optical = isLayoutModeOptical(this); + if (optical != isLayoutModeOptical(mParent)) { + Insets insets = getOpticalInsets(); + int opticalWidth = insets.left + insets.right; + int opticalHeight = insets.top + insets.bottom; + + measuredWidth += optical ? opticalWidth : -opticalWidth; + measuredHeight += optical ? opticalHeight : -opticalHeight; + } + setMeasuredDimensionRaw(measuredWidth, measuredHeight); +} +``` +`setMeasuredDimensionRaw()`方法如下: +```java +/** + * Sets the measured dimension without extra processing for things like optical bounds. + * Useful for reapplying consistent values that have already been cooked with adjustments + * for optical bounds, etc. such as those from the measurement cache. + * + * @param measuredWidth The measured width of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * @param measuredHeight The measured height of this view. May be a complex + * bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + */ +private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) { + // 赋值给mMeasuredWidth,getMeasuredWidth就会调用该值。 + mMeasuredWidth = measuredWidth; + mMeasuredHeight = measuredHeight; + + // 这就是重写onMeasure方法时如果不调用setMeasuredDimension方法时为什么会报错的原因。 + mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET; +} +``` + +我们接着看一下上面用到的`getDefaultSize()`方法,源码如下: +```java +/** + * Utility to return a default size. Uses the supplied size if the + * MeasureSpec imposed no constraints. Will get larger if allowed + * by the MeasureSpec. + * + * @param size Default size for this view + * @param measureSpec Constraints imposed by the parent + * @return The size this view should be. + */ +public static int getDefaultSize(int size, int measureSpec) { + int result = size; + // measureSpec值用于获取宽度(高度)的规格和大小,解析出对应的size和mode + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; +} +``` +`getDefaultSize`方法又会使用到`MeasureSpec`类,文档中对`MeasureSpec`是这样介绍的`A MeasureSpec is comprised of a size and a mode. There are three possible modes:` +- MeasureSpec.EXACTLY The parent has determined an exact size for the child. The child is going to be given those bounds regardless of how big it wants to be. + 理解成MATCH_PARENT或者在布局中指定了宽高值,如layout:width='50dp' +- MeasureSpec.AT_MOST The child can be as large as it wants up to the specified size.理解成WRAP_CONTENT,这是的值是父View可以允许的最大的值,只要不超过这个值都可以。 +- MeasureSpec.UNSPECIFIED The parent has not imposed any constraint on the child. It can be whatever size it wants. 这种情况比较少,一般用不到。 + +这里简单总结一下上面的过程: +```java +performMeasure() { + - 1.调用View.measure方法 + mView.measure(): + - 2.measure内部会调用onMeasure方法,但是因为这里mView是DecorView,所以会调用FrameLayout的onMeasure方法。 + onMeasure(FrameLayout) + - 3. 内部设置ViewGroup的宽高 + setMeasuredDimension + 并且对每个子View进行遍历测量 + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + - 4. 对每个子View调用measureChildWithMargins方法 + measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0); + -5. measureChildWithMargins内部调用子View的measure方法 + meausre + - 6. measure方法内部又调用onMeasure方法 + onMeasure(View) + - 7. onMeasure方法内部调用setMeasuredDimension + setMeasuredDimension + - 8. setMeasuredDimension内部调用setMeasuredDimensionRaw + setMeasuredDimensionRaw + } +} +``` + +从上面代码中能看到`measure`是`final`的,我们可以重写`onMeasure`来实现`measure`过程。 +到这里基本都讲完了,我们在开发中会按照需要重写`onMeasure`方法,然后调用`setMeasuredDimension`方法设置大小, +ps:譬如我们设置了`setMeasuredDimension(10, 10)`,那么不管布局中怎么设置这个`View`的大小 +都是没用的,最后显示出来大小都是10*10。 + +`Layout` +=== + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_layout.png) + + + +`performLayout`方法源码如下: + +```java +private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { + mLayoutRequested = false; + mScrollMayChange = true; + mInLayout = true; + + final View host = mView; + if (DEBUG_ORIENTATION || DEBUG_LAYOUT) { + Log.v(TAG, "Laying out " + host + " to (" + + host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")"); + } + + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout"); + try { + // 把刚才测量的宽高设置进来 + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + + mInLayout = false; + int numViewsRequestingLayout = mLayoutRequesters.size(); + if (numViewsRequestingLayout > 0) { + // requestLayout() was called during layout. + // If no layout-request flags are set on the requesting views, there is no problem. + // If some requests are still pending, then we need to clear those flags and do + // a full request/measure/layout pass to handle this situation. + ArrayList validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, + false); + if (validLayoutRequesters != null) { + // Set this flag to indicate that any further requests are happening during + // the second pass, which may result in posting those requests to the next + // frame instead + mHandlingLayoutInLayoutRequest = true; + + // Process fresh layout requests, then measure and layout + int numValidRequests = validLayoutRequesters.size(); + for (int i = 0; i < numValidRequests; ++i) { + final View view = validLayoutRequesters.get(i); + Log.w("View", "requestLayout() improperly called by " + view + + " during layout: running second layout pass"); + view.requestLayout(); + } + // desiredWindowWidth和desiredWindowHeight是屏幕的尺寸 + measureHierarchy(host, lp, mView.getContext().getResources(), + desiredWindowWidth, desiredWindowHeight); + mInLayout = true; + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + + mHandlingLayoutInLayoutRequest = false; + + // Check the valid requests again, this time without checking/clearing the + // layout flags, since requests happening during the second pass get noop'd + validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true); + if (validLayoutRequesters != null) { + final ArrayList finalRequesters = validLayoutRequesters; + // Post second-pass requests to the next frame + getRunQueue().post(new Runnable() { + @Override + public void run() { + int numValidRequests = finalRequesters.size(); + for (int i = 0; i < numValidRequests; ++i) { + final View view = finalRequesters.get(i); + Log.w("View", "requestLayout() improperly called by " + view + + " during second layout pass: posting in next frame"); + view.requestLayout(); + } + } + }); + } + } + + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + mInLayout = false; +} +``` + +内部会调用`layout()`方法,因为`host`是`mView`,`ViewGroup`中重写了`layout`方法,并调用了`super.layout`. +所以我们直接看`View.layout()`方法,该方法源码如下: +```java +/** + * Assign a size and position to a view and all of its + * descendants + * + *

This is the second phase of the layout mechanism. + * (The first is measuring). In this phase, each parent calls + * layout on all of its children to position them. + * This is typically done using the child measurements + * that were stored in the measure pass().

+ * + *

Derived classes should not override this method. + * Derived classes with children should override + * onLayout. In that method, they should + * call layout on each of their children.

+ * + * @param l Left position, relative to parent + * @param t Top position, relative to parent + * @param r Right position, relative to parent + * @param b Bottom position, relative to parent + */ +@SuppressWarnings({"unchecked"}) +public void layout(int l, int t, int r, int b) { + if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { + onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); + mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; + } + + int oldL = mLeft; + int oldT = mTop; + int oldB = mBottom; + int oldR = mRight; + + // 这部分是判断这个View的大小是否已经发生了变化,来判断是否需要重绘。 + boolean changed = isLayoutModeOptical(mParent) ? + setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); + + if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { + // 内部调用onLayout方法 + onLayout(changed, l, t, r, b); + mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; + + ListenerInfo li = mListenerInfo; + if (li != null && li.mOnLayoutChangeListeners != null) { + ArrayList listenersCopy = + (ArrayList)li.mOnLayoutChangeListeners.clone(); + int numListeners = listenersCopy.size(); + for (int i = 0; i < numListeners; ++i) { + listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB); + } + } + } + + mPrivateFlags &= ~PFLAG_FORCE_LAYOUT; + mPrivateFlags3 |= PFLAG3_IS_LAID_OUT; +} +``` +这里会调用`onLayout`方法,同样因为`mView`是`FrameLayout`的子类,所以我们要看`FrameLayout`的`onLayout`方法, +这里我们先看一下`ViewGroup.onLayout`方法: +```java +/** + * {@inheritDoc} + */ +@Override +protected abstract void onLayout(boolean changed, + int l, int t, int r, int b); +``` +是个抽象方法,所以`ViewGroup`的子类都需要实现该方法。 +我们看一下`FrameLayout.onLayout`方法,源码如下: +```java + /** + * {@inheritDoc} + */ +@Override +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + layoutChildren(left, top, right, bottom, false /* no force left gravity */); +} + +void layoutChildren(int left, int top, int right, int bottom, + boolean forceLeftGravity) { + final int count = getChildCount(); + + final int parentLeft = getPaddingLeftWithForeground(); + final int parentRight = right - left - getPaddingRightWithForeground(); + + final int parentTop = getPaddingTopWithForeground(); + final int parentBottom = bottom - top - getPaddingBottomWithForeground(); + + mForegroundBoundsChanged = true; + + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int width = child.getMeasuredWidth(); + final int height = child.getMeasuredHeight(); + + int childLeft; + int childTop; + + int gravity = lp.gravity; + if (gravity == -1) { + gravity = DEFAULT_CHILD_GRAVITY; + } + + final int layoutDirection = getLayoutDirection(); + final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection); + final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) { + case Gravity.CENTER_HORIZONTAL: + childLeft = parentLeft + (parentRight - parentLeft - width) / 2 + + lp.leftMargin - lp.rightMargin; + break; + case Gravity.RIGHT: + if (!forceLeftGravity) { + childLeft = parentRight - width - lp.rightMargin; + break; + } + case Gravity.LEFT: + default: + childLeft = parentLeft + lp.leftMargin; + } + + switch (verticalGravity) { + case Gravity.TOP: + childTop = parentTop + lp.topMargin; + break; + case Gravity.CENTER_VERTICAL: + childTop = parentTop + (parentBottom - parentTop - height) / 2 + + lp.topMargin - lp.bottomMargin; + break; + case Gravity.BOTTOM: + childTop = parentBottom - height - lp.bottomMargin; + break; + default: + childTop = parentTop + lp.topMargin; + } + //调用子View的layout方法 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` +而`View.layout`方法,又会调用到`View.onLayout`方法,我们假设这个子`View`不是`ViewGroup`. +看一下`View.onLayout`方法源码如下: +```java +/** + * Called from layout when this view should + * assign a size and position to each of its children. + * + * Derived classes with children should override + * this method and call layout on each of + * their children. + * @param changed This is a new size or position for this view + * @param left Left position, relative to parent + * @param top Top position, relative to parent + * @param right Right position, relative to parent + * @param bottom Bottom position, relative to parent + */ +protected void onLayout(boolean changed, int left, int top, int right, int bottom) { +} +``` +是一个空方法,这是因为`Layout`需要`ViewGroup`来控制进行。 + +这里也总结一下`layout`的过程。 +```java +private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, + int desiredWindowHeight) { + - 1. host.layout + host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); + -2. layout方法会分别调用setFrame()和onLayout()方法 + setFrame() + onLayout() + -3. 因为host是mView也就是DecorView也就是FrameLayout的子类。FrameLayout的onLayout方法如下 + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + -4. 遍历每个子View,并分别调用layout方法。 + child.layout(childLeft, childTop, childLeft + width, childTop + height); + } + } +} +``` + +`Draw` +=== + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/view_draw.png) + +绘制阶段是从`ViewRootImpl`中的`performDraw`方法开始的: + +```java +private void performDraw() { + if (mAttachInfo.mDisplayState == Display.STATE_OFF && !mReportNextDraw) { + return; + } + + final boolean fullRedrawNeeded = mFullRedrawNeeded; + mFullRedrawNeeded = false; + + mIsDrawing = true; + Trace.traceBegin(Trace.TRACE_TAG_VIEW, "draw"); + try { + // 开始draw了 + draw(fullRedrawNeeded); + } finally { + mIsDrawing = false; + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } + + // For whatever reason we didn't create a HardwareRenderer, end any + // hardware animations that are now dangling + if (mAttachInfo.mPendingAnimatingRenderNodes != null) { + final int count = mAttachInfo.mPendingAnimatingRenderNodes.size(); + for (int i = 0; i < count; i++) { + mAttachInfo.mPendingAnimatingRenderNodes.get(i).endAllAnimators(); + } + mAttachInfo.mPendingAnimatingRenderNodes.clear(); + } + + if (mReportNextDraw) { + mReportNextDraw = false; + if (mAttachInfo.mHardwareRenderer != null) { + mAttachInfo.mHardwareRenderer.fence(); + } + + if (LOCAL_LOGV) { + Log.v(TAG, "FINISHED DRAWING: " + mWindowAttributes.getTitle()); + } + if (mSurfaceHolder != null && mSurface.isValid()) { + mSurfaceHolderCallback.surfaceRedrawNeeded(mSurfaceHolder); + SurfaceHolder.Callback callbacks[] = mSurfaceHolder.getCallbacks(); + if (callbacks != null) { + for (SurfaceHolder.Callback c : callbacks) { + if (c instanceof SurfaceHolder.Callback2) { + ((SurfaceHolder.Callback2)c).surfaceRedrawNeeded( + mSurfaceHolder); + } + } + } + } + try { + mWindowSession.finishDrawing(mWindow); + } catch (RemoteException e) { + } + } +} +``` +内部会调用`draw`方法,`draw`方法源码如下: +```java +private void draw(boolean fullRedrawNeeded) { + Surface surface = mSurface; + // 首先检查surface是否有效,正常情况下都是有效的,除非WmS发生异常不能为该客户端分配有效的Surface + if (!surface.isValid()) { + return; + } + + if (DEBUG_FPS) { + trackFPS(); + } + + if (!sFirstDrawComplete) { + synchronized (sFirstDrawHandlers) { + sFirstDrawComplete = true; + final int count = sFirstDrawHandlers.size(); + for (int i = 0; i< count; i++) { + mHandler.post(sFirstDrawHandlers.get(i)); + } + } + } + + scrollToRectOrFocus(null, false); + + if (mAttachInfo.mViewScrollChanged) { + mAttachInfo.mViewScrollChanged = false; + mAttachInfo.mTreeObserver.dispatchOnScrollChanged(); + } + + boolean animating = mScroller != null && mScroller.computeScrollOffset(); + final int curScrollY; + if (animating) { + curScrollY = mScroller.getCurrY(); + } else { + curScrollY = mScrollY; + } + if (mCurScrollY != curScrollY) { + mCurScrollY = curScrollY; + fullRedrawNeeded = true; + } + + final float appScale = mAttachInfo.mApplicationScale; + final boolean scalingRequired = mAttachInfo.mScalingRequired; + + int resizeAlpha = 0; + if (mResizeBuffer != null) { + long deltaTime = SystemClock.uptimeMillis() - mResizeBufferStartTime; + if (deltaTime < mResizeBufferDuration) { + float amt = deltaTime/(float) mResizeBufferDuration; + amt = mResizeInterpolator.getInterpolation(amt); + animating = true; + resizeAlpha = 255 - (int)(amt*255); + } else { + disposeResizeBuffer(); + } + } + + final Rect dirty = mDirty; + // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 + // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 + if (mSurfaceHolder != null) { + // The app owns the surface, we won't draw. + dirty.setEmpty(); + if (animating) { + if (mScroller != null) { + mScroller.abortAnimation(); + } + disposeResizeBuffer(); + } + return; + } + + if (fullRedrawNeeded) { + mAttachInfo.mIgnoreDirtyState = true; + dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f)); + } + + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Draw " + mView + "/" + + mWindowAttributes.getTitle() + + ": dirty={" + dirty.left + "," + dirty.top + + "," + dirty.right + "," + dirty.bottom + "} surface=" + + surface + " surface.isValid()=" + surface.isValid() + ", appScale:" + + appScale + ", width=" + mWidth + ", height=" + mHeight); + } + + mAttachInfo.mTreeObserver.dispatchOnDraw(); + + int xOffset = 0; + int yOffset = curScrollY; + final WindowManager.LayoutParams params = mWindowAttributes; + final Rect surfaceInsets = params != null ? params.surfaceInsets : null; + if (surfaceInsets != null) { + xOffset -= surfaceInsets.left; + yOffset -= surfaceInsets.top; + + // Offset dirty rect for surface insets. + dirty.offset(surfaceInsets.left, surfaceInsets.right); + } + + if (!dirty.isEmpty() || mIsAnimating) { + // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 + // 因此这里需要根据不同的模式,进行不同的操作 + if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { + // 硬件绘制 + // Draw with hardware renderer. + mIsAnimating = false; + boolean invalidateRoot = false; + if (mHardwareYOffset != yOffset || mHardwareXOffset != xOffset) { + mHardwareYOffset = yOffset; + mHardwareXOffset = xOffset; + mAttachInfo.mHardwareRenderer.invalidateRoot(); + } + mResizeAlpha = resizeAlpha; + + dirty.setEmpty(); + + mBlockResizeBuffer = false; + mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this); + } else { + // If we get here with a disabled & requested hardware renderer, something went + // wrong (an invalidate posted right before we destroyed the hardware surface + // for instance) so we should just bail out. Locking the surface with software + // rendering at this point would lock it forever and prevent hardware renderer + // from doing its job when it comes back. + // Before we request a new frame we must however attempt to reinitiliaze the + // hardware renderer if it's in requested state. This would happen after an + // eglTerminate() for instance. + if (mAttachInfo.mHardwareRenderer != null && + !mAttachInfo.mHardwareRenderer.isEnabled() && + mAttachInfo.mHardwareRenderer.isRequested()) { + + try { + mAttachInfo.mHardwareRenderer.initializeIfNeeded( + mWidth, mHeight, mSurface, surfaceInsets); + } catch (OutOfResourcesException e) { + handleOutOfResourcesException(e); + return; + } + + mFullRedrawNeeded = true; + scheduleTraversals(); + return; + } + + // 软件绘制 + if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { + return; + } + } + } + + if (animating) { + mFullRedrawNeeded = true; + // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 + scheduleTraversals(); + } +} +``` +我们看一下`drawSoftware`方法: +```java +/** + * 使用CPU的软件绘制方式 + * @return true if drawing was successful, false if an error occurred + */ +private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, + boolean scalingRequired, Rect dirty) { + + // Draw with software renderer. + final Canvas canvas; + try { + final int left = dirty.left; + final int top = dirty.top; + final int right = dirty.right; + final int bottom = dirty.bottom; + + canvas = mSurface.lockCanvas(dirty); + + // The dirty rectangle can be modified by Surface.lockCanvas() + //noinspection ConstantConditions + if (left != dirty.left || top != dirty.top || right != dirty.right + || bottom != dirty.bottom) { + attachInfo.mIgnoreDirtyState = true; + } + + // TODO: Do this in native + canvas.setDensity(mDensity); + } catch (Surface.OutOfResourcesException e) { + handleOutOfResourcesException(e); + return false; + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not lock surface", e); + // Don't assume this is due to out of memory, it could be + // something else, and if it is something else then we could + // kill stuff (or ourself) for no reason. + mLayoutRequested = true; // ask wm for a new surface next time. + return false; + } + + try { + if (DEBUG_ORIENTATION || DEBUG_DRAW) { + Log.v(TAG, "Surface " + surface + " drawing to bitmap w=" + + canvas.getWidth() + ", h=" + canvas.getHeight()); + //canvas.drawARGB(255, 255, 0, 0); + } + + // If this bitmap's format includes an alpha channel, we + // need to clear it before drawing so that the child will + // properly re-composite its drawing on a transparent + // background. This automatically respects the clip/dirty region + // or + // If we are applying an offset, we need to clear the area + // where the offset doesn't appear to avoid having garbage + // left in the blank areas. + if (!canvas.isOpaque() || yoff != 0 || xoff != 0) { + canvas.drawColor(0, PorterDuff.Mode.CLEAR); + } + + dirty.setEmpty(); + mIsAnimating = false; + attachInfo.mDrawingTime = SystemClock.uptimeMillis(); + mView.mPrivateFlags |= View.PFLAG_DRAWN; + + if (DEBUG_DRAW) { + Context cxt = mView.getContext(); + Log.i(TAG, "Drawing: package:" + cxt.getPackageName() + + ", metrics=" + cxt.getResources().getDisplayMetrics() + + ", compatibilityInfo=" + cxt.getResources().getCompatibilityInfo()); + } + try { + canvas.translate(-xoff, -yoff); + if (mTranslator != null) { + mTranslator.translateCanvas(canvas); + } + canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0); + attachInfo.mSetIgnoreDirtyState = false; + + // 内部会去调用View.draw(); + mView.draw(canvas); + } finally { + if (!attachInfo.mSetIgnoreDirtyState) { + // Only clear the flag if it was not set during the mView.draw() call + attachInfo.mIgnoreDirtyState = false; + } + } + } finally { + try { + surface.unlockCanvasAndPost(canvas); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Could not unlock surface", e); + mLayoutRequested = true; // ask wm for a new surface next time. + //noinspection ReturnInsideFinallyBlock + return false; + } + + if (LOCAL_LOGV) { + Log.v(TAG, "Surface " + surface + " unlockCanvasAndPost"); + } + } + return true; +} +``` +代码中调用了`mView.draw()`方法,所以我们看一下`FrameLayout.draw()`方法: +```java +/** + * {@inheritDoc} + */ +@Override +public void draw(Canvas canvas) { + super.draw(canvas); + + if (mForeground != null) { + final Drawable foreground = mForeground; + + if (mForegroundBoundsChanged) { + mForegroundBoundsChanged = false; + final Rect selfBounds = mSelfBounds; + final Rect overlayBounds = mOverlayBounds; + + final int w = mRight-mLeft; + final int h = mBottom-mTop; + + if (mForegroundInPadding) { + selfBounds.set(0, 0, w, h); + } else { + selfBounds.set(mPaddingLeft, mPaddingTop, w - mPaddingRight, h - mPaddingBottom); + } + + final int layoutDirection = getLayoutDirection(); + Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(), + foreground.getIntrinsicHeight(), selfBounds, overlayBounds, + layoutDirection); + foreground.setBounds(overlayBounds); + } + + foreground.draw(canvas); + } +} +``` +内部调用了`super.draw()`,而`ViewGroup`没有重写该方法,所以直接看`View`的`draw()`方法. +`View.draw()`方法如下: +```java +/** + * Manually render this view (and all of its children) to the given Canvas. + * The view must have already done a full layout before this function is + * called. When implementing a view, implement + * {@link #onDraw(android.graphics.Canvas)} instead of overriding this method. + * If you do need to override this method, call the superclass version. + * + * @param canvas The Canvas to which the View is rendered. + */ +public void draw(Canvas canvas) { + final int privateFlags = mPrivateFlags; + final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && + (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState); + mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN; + + // 这里注释说的很明白了,draw的6个步骤。 + /* + * Draw traversal performs several drawing steps which must be executed + * in the appropriate order: + * + * 1. Draw the background + * 2. If necessary, save the canvas' layers to prepare for fading + * 3. Draw view's content, 调用onDraw方法绘制自身 + * 4. Draw children, 调用dispatchDraw方法绘制子View + * 5. If necessary, draw the fading edges and restore layers + * 6. Draw decorations (scrollbars for instance) + */ + + // Step 1, draw the background, if needed + int saveCount; + + if (!dirtyOpaque) { + drawBackground(canvas); + } + + // skip step 2 & 5 if possible (common case) + final int viewFlags = mViewFlags; + boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0; + boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0; + if (!verticalEdges && !horizontalEdges) { + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } + + // we're done... + return; + } + + /* + * Here we do the full fledged routine... + * (this is an uncommon case where speed matters less, + * this is why we repeat some of the tests that have been + * done above) + */ + + boolean drawTop = false; + boolean drawBottom = false; + boolean drawLeft = false; + boolean drawRight = false; + + float topFadeStrength = 0.0f; + float bottomFadeStrength = 0.0f; + float leftFadeStrength = 0.0f; + float rightFadeStrength = 0.0f; + + // Step 2, save the canvas' layers + int paddingLeft = mPaddingLeft; + + final boolean offsetRequired = isPaddingOffsetRequired(); + if (offsetRequired) { + paddingLeft += getLeftPaddingOffset(); + } + + int left = mScrollX + paddingLeft; + int right = left + mRight - mLeft - mPaddingRight - paddingLeft; + int top = mScrollY + getFadeTop(offsetRequired); + int bottom = top + getFadeHeight(offsetRequired); + + if (offsetRequired) { + right += getRightPaddingOffset(); + bottom += getBottomPaddingOffset(); + } + + final ScrollabilityCache scrollabilityCache = mScrollCache; + final float fadeHeight = scrollabilityCache.fadingEdgeLength; + int length = (int) fadeHeight; + + // clip the fade length if top and bottom fades overlap + // overlapping fades produce odd-looking artifacts + if (verticalEdges && (top + length > bottom - length)) { + length = (bottom - top) / 2; + } + + // also clip horizontal fades if necessary + if (horizontalEdges && (left + length > right - length)) { + length = (right - left) / 2; + } + + if (verticalEdges) { + topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength())); + drawTop = topFadeStrength * fadeHeight > 1.0f; + bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength())); + drawBottom = bottomFadeStrength * fadeHeight > 1.0f; + } + + if (horizontalEdges) { + leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength())); + drawLeft = leftFadeStrength * fadeHeight > 1.0f; + rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength())); + drawRight = rightFadeStrength * fadeHeight > 1.0f; + } + + saveCount = canvas.getSaveCount(); + + int solidColor = getSolidColor(); + if (solidColor == 0) { + final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG; + + if (drawTop) { + canvas.saveLayer(left, top, right, top + length, null, flags); + } + + if (drawBottom) { + canvas.saveLayer(left, bottom - length, right, bottom, null, flags); + } + + if (drawLeft) { + canvas.saveLayer(left, top, left + length, bottom, null, flags); + } + + if (drawRight) { + canvas.saveLayer(right - length, top, right, bottom, null, flags); + } + } else { + scrollabilityCache.setFadeColor(solidColor); + } + + // Step 3, draw the content + if (!dirtyOpaque) onDraw(canvas); + + // Step 4, draw the children + dispatchDraw(canvas); + + // Step 5, draw the fade effect and restore layers + final Paint p = scrollabilityCache.paint; + final Matrix matrix = scrollabilityCache.matrix; + final Shader fade = scrollabilityCache.shader; + + if (drawTop) { + matrix.setScale(1, fadeHeight * topFadeStrength); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, right, top + length, p); + } + + if (drawBottom) { + matrix.setScale(1, fadeHeight * bottomFadeStrength); + matrix.postRotate(180); + matrix.postTranslate(left, bottom); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, bottom - length, right, bottom, p); + } + + if (drawLeft) { + matrix.setScale(1, fadeHeight * leftFadeStrength); + matrix.postRotate(-90); + matrix.postTranslate(left, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(left, top, left + length, bottom, p); + } + + if (drawRight) { + matrix.setScale(1, fadeHeight * rightFadeStrength); + matrix.postRotate(90); + matrix.postTranslate(right, top); + fade.setLocalMatrix(matrix); + p.setShader(fade); + canvas.drawRect(right - length, top, right, bottom, p); + } + + canvas.restoreToCount(saveCount); + + // Step 6, draw decorations (scrollbars) + onDrawScrollBars(canvas); + + if (mOverlay != null && !mOverlay.isEmpty()) { + mOverlay.getOverlayView().dispatchDraw(canvas); + } +} +``` + +上面会调用`onDraw`和`dispatchDraw`方法。 +我们先看一下`View.onDraw`方法: +```java +/** + * Implement this to do your drawing. + * + * @param canvas the canvas on which the background will be drawn + */ +protected void onDraw(Canvas canvas) { +} +``` +是空方法,这是也很好理解,因为每个`View`的展现都不一样,例如`TextView`、`ProgressBar`等, +所以`View`不会去实现`onDraw`方法,具体是要子类去根据自己的显示要求实现该方法。 + +再看一下`dispatchDraw`方法,这个方法是用来绘制子`View`的,所以要看`ViewGroup.dispatchDraw`方法,`View.dispatchDraw`是空的。 +```java +/** + * {@inheritDoc} + */ +@Override +protected void dispatchDraw(Canvas canvas) { + boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode); + final int childrenCount = mChildrenCount; + final View[] children = mChildren; + int flags = mGroupFlags; + + if ((flags & FLAG_RUN_ANIMATION) != 0 && canAnimate()) { + final boolean cache = (mGroupFlags & FLAG_ANIMATION_CACHE) == FLAG_ANIMATION_CACHE; + + final boolean buildCache = !isHardwareAccelerated(); + for (int i = 0; i < childrenCount; i++) { + final View child = children[i]; + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE) { + final LayoutParams params = child.getLayoutParams(); + attachLayoutAnimationParameters(child, params, i, childrenCount); + bindLayoutAnimation(child); + if (cache) { + child.setDrawingCacheEnabled(true); + if (buildCache) { + child.buildDrawingCache(true); + } + } + } + } + + final LayoutAnimationController controller = mLayoutAnimationController; + if (controller.willOverlap()) { + mGroupFlags |= FLAG_OPTIMIZE_INVALIDATE; + } + + controller.start(); + + mGroupFlags &= ~FLAG_RUN_ANIMATION; + mGroupFlags &= ~FLAG_ANIMATION_DONE; + + if (cache) { + mGroupFlags |= FLAG_CHILDREN_DRAWN_WITH_CACHE; + } + + if (mAnimationListener != null) { + mAnimationListener.onAnimationStart(controller.getAnimation()); + } + } + + int clipSaveCount = 0; + final boolean clipToPadding = (flags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK; + if (clipToPadding) { + clipSaveCount = canvas.save(); + canvas.clipRect(mScrollX + mPaddingLeft, mScrollY + mPaddingTop, + mScrollX + mRight - mLeft - mPaddingRight, + mScrollY + mBottom - mTop - mPaddingBottom); + } + + // We will draw our child's animation, let's reset the flag + mPrivateFlags &= ~PFLAG_DRAW_ANIMATION; + mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED; + + boolean more = false; + final long drawingTime = getDrawingTime(); + + if (usingRenderNodeProperties) canvas.insertReorderBarrier(); + // Only use the preordered list if not HW accelerated, since the HW pipeline will do the + // draw reordering internally + final ArrayList preorderedList = usingRenderNodeProperties + ? null : buildOrderedChildList(); + final boolean customOrder = preorderedList == null + && isChildrenDrawingOrderEnabled(); + for (int i = 0; i < childrenCount; i++) { + int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i; + final View child = (preorderedList == null) + ? children[childIndex] : preorderedList.get(childIndex); + if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { + // 调用drawChild方法 + more |= drawChild(canvas, child, drawingTime); + } + } + if (preorderedList != null) preorderedList.clear(); + + // Draw any disappearing views that have animations + if (mDisappearingChildren != null) { + final ArrayList disappearingChildren = mDisappearingChildren; + final int disappearingCount = disappearingChildren.size() - 1; + // Go backwards -- we may delete as animations finish + for (int i = disappearingCount; i >= 0; i--) { + final View child = disappearingChildren.get(i); + more |= drawChild(canvas, child, drawingTime); + } + } + if (usingRenderNodeProperties) canvas.insertInorderBarrier(); + + if (debugDraw()) { + onDebugDraw(canvas); + } + + if (clipToPadding) { + canvas.restoreToCount(clipSaveCount); + } + + // mGroupFlags might have been updated by drawChild() + flags = mGroupFlags; + + if ((flags & FLAG_INVALIDATE_REQUIRED) == FLAG_INVALIDATE_REQUIRED) { + invalidate(true); + } + + if ((flags & FLAG_ANIMATION_DONE) == 0 && (flags & FLAG_NOTIFY_ANIMATION_LISTENER) == 0 && + mLayoutAnimationController.isDone() && !more) { + // We want to erase the drawing cache and notify the listener after the + // next frame is drawn because one extra invalidate() is caused by + // drawChild() after the animation is over + mGroupFlags |= FLAG_NOTIFY_ANIMATION_LISTENER; + final Runnable end = new Runnable() { + public void run() { + notifyAnimationListener(); + } + }; + post(end); + } +} +``` +可以看到上面的方法中会调用`drawChild`方法,该方法如下: +```java +/** + * Draw one child of this View Group. This method is responsible for getting + * the canvas in the right state. This includes clipping, translating so + * that the child's scrolled origin is at 0, 0, and applying any animation + * transformations. + * + * @param canvas The canvas on which to draw the child + * @param child Who to draw + * @param drawingTime The time at which draw is occurring + * @return True if an invalidate() was issued + */ +protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return child.draw(canvas, this, drawingTime); +} +``` + +这里也简单总结一下`draw`的过程: +```java +// 1. ViewRootImpl.performDraw() +private void performDraw() { + // 2. ViewRootImpl.draw() + draw(fullRedrawNeeded); + // 3. ViewRootImpl.drawSoftware + drawSoftware + // 4. 内部调用mView.draw,也就是FrameLayout.draw(). + mView.draw()(FrameLayout) + // 5. FrameLayout.draw方法内部会调用super.draw方法,也就是View.draw方法. + super.draw(canvas); + // 6. View.draw方法内部会分别调用onDraw绘制自己以及dispatchDraw绘制子View. + onDraw + // 绘制子View + dispatchDraw + // 7. dispatchDraw方法内部会遍历所有子View. + for (int i = 0; i < childrenCount; i++) { + // 8. 对每个子View分别调用drawChild方法 + drawChild() + // 9. drawChild方法内部会对该子View调用draw方法,进行绘制。然后draw又会调用onDraw等,循环就开始了。 + child.draw() + } +} +``` + +最后补充一个小问题: `getWidth()`与`getMeasuredWidth()`有什么区别呢? +一般情况下这两个的值是相同的,`getMeasureWidth()`方法在`measure()`过程结束后就可以获取到了,而`getWidth()`方法要在`layout()`过程结束后才能获取到。 +而且`getMeasureWidth()`的值是通过`setMeasuredDimension()`设置的,但是`getWidth()`的值是通过视图右边的坐标减去左边的坐标计算出来的。如果我们在`layout`的时候将宽高 +不传`getMeasureWidth`的值,那么这时候`getWidth()`与`getMeasuredWidth`的值就不会再相同了,当然一般也不会这么干... + +# MeasureSpec + +MeasureSpec是View类的一个静态内部类,用来说明如何测量这个类。MeasureSpec表示的是一个32位的整型值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的内存分配。 + +--- + +- [上一篇:6.屏幕绘制基础](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/6.%E5%B1%8F%E5%B9%95%E7%BB%98%E5%88%B6%E5%9F%BA%E7%A1%80.md) +- [下一篇:8.WindowManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md) diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" index 7fa274dc..cb3820e1 100644 --- "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -1,7 +1,7 @@ # 8.WindowManagerService简介 -WmS是Android中图形用户接口的引擎,它管理者所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 +WmS是Android中图形用户接口的引擎,它管理着所有窗口,包括创建、删除窗口以及将某个窗口设置为焦点等。 在WmS中,窗口是由两部分内容构成: @@ -63,7 +63,7 @@ WmS接口结构是指WmS功能模块与其他模块之间的交互接口,其 ### Session -和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS回味每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 +和SurfaceFlinger直接打交道的类本来是SurfaceSession。当应用程序需要创建Surface时,会请求WmS去完成创建的工作,WmS会为每一个应用程序分配一个SurfaceSession对象。然而一个surfaceSession对象不足以表示一个客户端,因此,WmS定义了Session类,它可被认为是SurfaceSession的一个包装。Session对象是当应用程序调用WmS的openSession()函数时创建的,而应用程序又是在ViewRoot类中调用openSession的。 @@ -122,6 +122,11 @@ Surface的销毁有两种情况: +--- + +- [上一篇:7.View绘制原理](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/7.View%E7%BB%98%E5%88%B6%E5%8E%9F%E7%90%86.md) +- [下一篇:9.PackageManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md) + diff --git "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" index 803a99df..83a469b5 100644 --- "a/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/9.PackageManagerService\347\256\200\344\273\213.md" @@ -20,7 +20,7 @@ 和AmS、WmS等其他系统服务一样,包管理服务运行于SystemServer进程。PmS服务运行时,使用了两个目录下的XML文件保存相关的包管理信息。 - - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两件时间。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 + - 第一个目录是system/etc/permissions:该目录下的所有xml文件用于permission的管理,具体包含两个事件。第一个是定义系统中都包含了哪些feature,应用程序可以在AndroidManifest.xml中使用use-feature标签声明程序都需要哪些feature。 - 第二个目录是/data/system/packages.xml,该文件保存了所有安装程序的基本包信息,有点像系统的注册表,比如程序的包名称是什么,安装包路径在哪里,程序都是用了哪些系统权限。 PmS在启动时,会从这两个目录中解析相关的XML文件,从而建立一个庞大的包信息树,应用程序可以间接从这个信息树中查询所有所需的程序包信息。 @@ -58,6 +58,14 @@ + +--- + +- [上一篇:8.WindowManagerService简介](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/8.WindowManagerService%E7%AE%80%E4%BB%8B.md) + + + + --- - 邮箱 :charon.chui@gmail.com From ddd5aaecaf8526f0fd3ab3c02a128b19b2a41a5c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 9 Dec 2020 20:26:16 +0800 Subject: [PATCH 011/183] add files --- ...73\347\273\237\347\256\200\344\273\213.md" | 8 +- ...13\344\270\216\347\272\277\347\250\213.md" | 4 + ...13\351\227\264\351\200\232\344\277\241.md" | 283 ++++++++++++++---- ...roid Framework\346\241\206\346\236\266.md" | 17 +- ...72\347\241\200\347\237\245\350\257\206.md" | 2 +- 5 files changed, 257 insertions(+), 57 deletions(-) rename "VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" (97%) diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 4e34b27f..82ba0f2a 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -134,7 +134,7 @@ 目前的Android系统大多运行在ARM处理器之上。ARM本身是一个公司的名称,从技术的角度来看,它又是一种微处理器内核的架构。 -对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,改段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 +对于ARM处理器,当复位完毕后,处理器首先还行其片上ROM中的一小块程序。这块ROM的大小一般只有几KB,该段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 多数基于ARM的实际硬件系统,会从并口NAND Flash芯片上的0x00000000地址处装载程序。对于一些小型嵌入式系统而言,该地址中的程序就是最终要执行的用户程序;而对于Android而言,该地址中的程序还不是Android程序,而是一个叫做uboot或者fastboot的程序,其作用是初始化硬件设备,比如网口、SDRAM、RS232等,并提供一些调试功能,比如向NAND Flash中写入新的数据,这可用于开发过程中的内核烧写、升级等。 @@ -146,6 +146,12 @@ Linux内核被装载后,就开始进行内核初始化的过程。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_start.png) + + + + + ## CPU(Central Processing Unit) CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从内存中提取指令并执行它。它是一块超大规模的集成电路(Integrated Circuit),是信息处理、程序运行的最终执行单元。其功能主要是解释计算机指令以及处理计算机软件中的数据。由于访问内存获取执行或数据要比执行指令花费的时间长,因此所有的 CPU 内部都会包含一些寄存器来保存关键变量和临时结果。因此,在指令集中通常会有一些指令用于把关键字从内存中加载到寄存器中,以及把关键字从寄存器存入到内存中。从功能方面来看,CPU的内部主要由寄存器,控制器,运算器构成,各部分之间由电流信号相互连通。其中运算器负责算术运算和逻辑运算,控制器负责计算指令的解析,产生各种控制指令,寄存器组用来临时存放参加运算的数据和计算的中间结果。CPU计算结果最终需要写到内存中,内存的存取速度远低于CPU,为提升数据交换速率,CPU内部一般还集成了高速缓存(CACHE),其中缓存分为一级缓存和二级缓存,一级缓存和CPU速率相当,二级缓存次之。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 3f5369e6..8dd5f952 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -324,7 +324,11 @@ Linux为进程间通信和同步提供了各种机制。这里只是几种。 应用程序通过调用操作系统提供的库与操作系统进行交互,这些库合起来构成Android框架(Android framework)。这些库中有一些可以在进程内部执行其工作,但是许多库需要与其他进程执行进程间通信,作者通常是在system_server进程中提供服务的。 +### ServiceManager +Android Binder的管理服务。 + +对于Binder驱动而言,**ServiceManager是一个守护进程,更是Android系统各个服务的管理者**。Android系统中的各个服务,都是添加到ServiceManager中进行管理的,而且每个服务都对应一个服务名。当Client获取某个服务时,则通过服务名来从ServiceManager中获取相应的服务。 ### Zygote diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 7263e837..2f089353 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -1,26 +1,52 @@ # 1.Android进程间通信 + + ## Binder简介 Binder,英文的意思是别针、回形针。我们经常用别针把两张纸”别“在一起,而在Android中,Binder用于完成进程间通信(IPC),即把多个进程”别“在一起。比如,普通应用程序可以调用音乐播放服务提供的播放、暂停、停止等功能。Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存。从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用完成的。 -## Linux进程间通信 +## 进程间通信 无论是Android系统,还是各种Linux衍生系统,各个组件、模块往往运行在各种不同的进程和线程内,这里就必然涉及进程/线程之间的通信。对于IPC(Inter-Process Communication, 进程间通信),Linux目前有一下这些IPC机制: -- 管道:在创建时分配一个page大小的内存,缓存区大小比较有限; -- 消息队列:信息会复制两次,会有额外的CPU消耗;不合适频繁或信息量大的通信; -- 共享内存:无须复制,共享缓冲区直接附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决; -- 套接字:作为更通用的接口,传输效率低,主要用于不同机器或跨网络的通信; -- 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。 -- 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等; +- 管道(Pipe) + + 管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条。管道的一端连接一个进程的输出。这个进程会向管道中放入信息。管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息。 + + - 管道是半双工的,数据只能向一个方向流动。需要双方通信时,需要建立起两个管道。 + - 只能用于父子进程或兄弟进程之间(具有亲缘关系的进程)。比如fork或exec创建的新进程,在使用exec创建新进程时,需要将管道的文件描述符作为参数传递给exec创建的新进程。当父进程与使用fork创建的子进程直接通信时,发送数据的进程关闭读端,接受数据的进程关闭写端。 + - 管道只能在本地计算机中使用,而不可用于网络间的通信。 + +- 命名管道(FIFO) + + 命名管道是一种特殊类型的文件,它在系统中以文件形式存在。这样克服了管道的弊端,他可以允许没有亲缘关系的进程间通信。 + +- 共享内存(Share Memory) + + 共享内存是多个进程之间共享内存区域的一种进程间的通信方式,由IPC为进程创建一个特殊地址范围,它将出现在该进程的地址空间中。其他进程可以将同一段共享内存连接到自己的地址空间中。所有进程都可以访问共享内存的地址,如果一个进程向共享内存中写入数据,所做的改动将立刻被其他进程看到。 + + - 共享内存是IPC最快捷的方式,共享内存方式直接将某段内存段进行映射,多个进程间的共享内存是同一块的物理空间,仅仅映射到各进程的地址不同而已,因此不需要进行复制,可以直接使用此段空间。 + - 共享内存本身并没有同步机制,需要程序员自己控制。 + +- 内存映射(Memory Map) + + 内存映射是由一个文件到一块内存的映射,在此之后进程操作文件,就像操作进程空间里的内存地址一样。 + +- 套接字(Soket) + + 套接字机制不但可以在单机的不同进程间通信,而且可以在跨网机器间进程通信。 + + 套接字的创建和使用与管道是有区别的,套接字明确的将客户端与服务器区分开来,可以实现多个客户端连到同一个服务器。 Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用的是Socket机制,在上层system server、media server以及上层App之间更多的是采用Binder。 IPC方式来完成跨进程间的通信。对于Android上层架构中,很多时候是在同一个进程的线程之间需要相互通信,例如同一个进程的主线程与工作线程之间的通信,往往采用的Handler消息机制。所以对于Android最上层架构而言,最常用的通信方式是: - Binder + Android中最常用的跨进程通信方式。 + - Socket Socket通信方式也是C/S架构,比Binder简单很多。在Android系统中采用Socket通信方式的主要有: @@ -32,11 +58,62 @@ Android额外还有Binder IPC机制,Android OS中的Zygote进程的IPC采用 - logcatd:这个不用说,用于服务logcat; - vold:即volume Daemon,是存储类的守护进程,用于负责如USB、Sdcard等存储设备的事件处理。 - 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。Socket通信方式相对于binder比较简单,这里省略。 + 等等还有很多,这里不一一列举,Socket方式更多的用于Android framework层与native层之间的通信。 - Handler - + 主要用于线程之间的通信。 + + + +### 进程隔离 + +进程隔离是操作系统为了保护进程之间不相互干扰而设计的,避免进程A写入进程B的情况发生,其实现就是使用虚拟地址空间,两个进程虚拟地址不同,这样就可以防止A进程写入数据到B进程。也就是说,操作系统的不同进程之间,数据不共享,在每个进程看来,自己都独享了整个系统空间,完全不知道其他进程的存在,因此一个进程想要与另一个进程通信,需要某种系统机制才能完成。 + + + +### 用户空间/内核空间 + +Linux Kernel是操作系统的核心,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。 + +对于Kernel这中高安全级别的功能,显然是不允许其它应用程序随便调用或访问的,所以需要对Kernel提供一定的保护机制,这个保护机制用来告诉那些应用程序,你只可以访问某些许可的资源,不许可的资源是不能访问的,于是操作系统就把kernel和上层的应用程序抽象的隔离开,分别称之为kenel space和user space,即内核空间和用户空间。 + + + +### 系统调用 内核态/用户态 + +虽然从逻辑上抽离出用户空间和内核空间,但是不可避免的是,总有那么一些用户空间需要访问内核的资源:比如应用程序访问文件、网络这种,那么这种情况下该怎么处理? + +用户空间访问内核空间的唯一方式就是系统调用,通过这个统一入口,所有的资源访问都是在内核的控制下执行,以免导致用户程序对系统资源的越权访问,从而保障了系统的安全和稳定。 + +Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。 + +当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。 + +当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。 + +系统调用主要通过如下两个函数来实现: + +``` +copy_from_user() //将数据从用户空间拷贝到内核空间 +copy_to_user() //将数据从内核空间拷贝到用户空间 +``` + + + +### 内核模块/驱动 + +通过系统调用,用户空间可以访问内核空间,那么如果一个用户空间想与另外一个用户空间进行通信该怎么处理? + +那就是让操作系统内核添加支持,传统的linux通信机制,比如socket、管道等都是内核的一部分,因此通过内核支持来实现进程间通信自然是没有问题的,但是binder并不是linux系统内核的一部分,那它是怎么做到访问内核空间的呢?这就得益于 Linux 的**动态内核可加载模块**(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。 + +> 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 **Binder 驱动**(Binder Dirver)(尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的)。 + + + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_qudong.png) + + ## Android为什么要使用Binder @@ -44,61 +121,83 @@ Binder作为Android系统提供的一种IPC机制。首先一个问题就是为 **接下来正面回答这个问题,从5个角度来展开对Binder的分析:** -**(1)从性能的角度** :数据拷贝次数,Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,但共享内存方式一次内存拷贝都不需要;从性能角度看,Binder性能仅次于共享内存。 +- **从性能的角度** : -**(2)从稳定性的角度** -Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + 对比与Linux的通信机制,socket是一个通用接口,导致其传输效率低、开销大。管道和消息队列因为采用存储转发的方式,所以至少需要拷贝2次数据,效率低。而共享内存虽然在传输时没有拷贝数据,但其控制机制复杂(比如跨进程通信时,需获取对方进程的pid,需要多种机制协同操作)。如果在APP级别,多拷贝一次或许没什么问题,但是如果上升到系统级别,系统内部通信频次是极高的,如果效率不够,用户体验会很差。 -仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: + + + 具体原因如下,我们先来看看传统的 IPC 方式中,进程之间是如何实现通信的。 + + 通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy*from*user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy*to*user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。 + + 这种传统的 IPC 通信方式有两个问题: -**(3)从安全的角度** -传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。 + - 性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝; + - 接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。 -Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 + + + 那Binder是怎么操作的呢?这就不得不通道 Linux 下的另一个概念:**内存映射**。 + + Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。 + + 内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。 -针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 + 一次完整的 Binder IPC 通信过程通常是这样: -Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 + 1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区; -**(4)从语言层面的角度** -大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 + 2. 接着在内核空间开辟一块内核缓存区,建立**内核缓存区**和**内核中数据接收缓存区**之间的映射关系,以及**内核中数据接收缓存区**和**接收进程用户空间地址**的映射关系; -**另外,Binder是为Android这类系统而生,而并非Linux社区没有想到Binder IPC机制的存在,对于Linux社区的广大开发人员,我还是表示深深佩服,让世界有了如此精湛而美妙的开源系统。**也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在**Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制**,Android中的**Kill Process采用的signal(信号)机制**等等。而**Binder更多则用在system_server进程与上层App层的IPC交互**。 + 3. 发送方进程通过系统调用 copy*from*user() 将数据 copy 到内核中的**内核缓存区**,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 -**(5) 从公司战略的角度** + Client(数据发送端)先从自己的用户进程空间把IPC数据通过copy_from_user()拷贝到内核空间。而Server端(数据接收端)与内核共享数据(mmap到同一块物理内存),不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。 + +- **从稳定性的角度** + Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别, 需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。 + +仅仅从以上两点,各有优劣,还不足以支撑google去采用binder的IPC机制,那么更重要的原因是: -总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 +- **从安全的角度** + Linux的IPC机制在本身的实现中,并没有安全措施,得依赖上层协议来进行安全控制。而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐私数据、后台造成手机耗电等等问题。Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,**Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行**。Android 6.0,也称为Android M,在6.0之前的系统是在App第一次安装时,会将整个App所涉及的所有权限一次询问,只要留意看会发现很多App根本用不上通信录和短信,但在这一次性权限权限时会包含进去,让用户拒绝不得,因为拒绝后App无法正常使用,而一旦授权后,应用便可以胡作非为。 -而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 + 针对这个问题,google在Android M做了调整,不再是安装时一并询问所有权限,而是在App运行过程中,需要哪个权限再弹框询问用户是否给相应的权限,对权限做了更细地控制,让用户有了更多的可控性,但同时也带来了另一个用户诟病的地方,那也就是权限询问的弹框的次数大幅度增多。对于Android M平台上,有些App开发者可能会写出让手机异常频繁弹框的App,企图直到用户授权为止,这对用户来说是不能忍的,用户最后吐槽的可不光是App,还有Android系统以及手机厂商,有些用户可能就跳果粉了,这还需要广大Android开发者以及手机厂商共同努力,共同打造安全与体验俱佳的Android手机。 -**有了这些铺垫,我们再说说Binder的今世前缘** + Android中权限控制策略有SELinux等多方面手段。传统IPC只能由用户在数据包里填入UID/PID;另外,可靠的身份标记只有由IPC机制本身在内核中添加。其次传统IPC访问接入点是开放的,无法建立私有通道。从安全角度,Binder的安全性更高。 -Binder是基于开源的[OpenBinder](https://link.zhihu.com/?target=http%3A//www.angryredplanet.com/~hackbod/openbinder/docs/html/BinderIPCMechanism.html)实现的,OpenBinder是一个开源的系统IPC机制,最初是由 [Be Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Be_Inc.) 开发,接着由[Palm, Inc.](https://link.zhihu.com/?target=https%3A//en.wikipedia.org/wiki/Palm%2C_Inc.)公司负责开发,现在OpenBinder的作者在Google工作,既然作者在Google公司,在用户空间采用Binder 作为核心的IPC机制,再用Apache-2.0协议保护,自然而然是没什么问题,减少法律风险,以及对开发成本也大有裨益的,那么从公司战略角度,Binder也是不错的选择。 +- **从语言层面的角度** + 大家多知道Linux是基于C语言(面向过程的语言),而Android是基于Java语言(面向对象的语句),而对于Binder恰恰也符合面向对象的思想,将进程间通信转化为通过对某个Binder对象的引用调用该对象的方法,而其独特之处在于Binder对象是一个可以跨进程引用的对象,它的实体位于一个进程中,而它的引用却遍布于系统的各个进程之中。可以从一个进程传给其它进程,让大家都能访问同一Server,就像将一个对象或引用赋值给另一个引用一样。Binder模糊了进程边界,淡化了进程间通信过程,整个系统仿佛运行于同一个面向对象的程序之中。从语言层面,Binder更适合基于面向对象语言的Android系统,对于Linux系统可能会有点“水土不服”。 -另外,再说一点关于OpenBinder,在2015年OpenBinder以及合入到Linux Kernel主线 3.19版本,这也算是Google对Linux的一点回馈吧。 +- **从公司战略的角度** -**综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。** + 总所周知,Linux内核是开源的系统,所开放源代码许可协议GPL保护,该协议具有“病毒式感染”的能力,怎么理解这句话呢?受GPL保护的Linux Kernel是运行在内核空间,对于上层的任何类库、服务、应用等运行在用户空间,一旦进行SysCall(系统调用),调用到底层Kernel,那么也必须遵循GPL协议。 + 而Android 之父 Andy Rubin对于GPL显然是不能接受的,为此,Google巧妙地将GPL协议控制在内核空间,将用户空间的协议采用Apache-2.0协议(允许基于Android的开发商不向社区反馈源码),同时在GPL协议与Apache-2.0之间的Lib库中采用BSD证授权方法,有效隔断了GPL的传染性,仍有较大争议,但至少目前缓解Android,让GPL止步于内核空间,这是Google在GPL Linux下开源与商业化共存的一个成功典范。 +综合上述5点,可知Binder是Android系统上层进程间通信的不二选择。 -**最后,简单讲讲Android Binder架构** -Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_server进程后,在system_server进程中出初始化支持整个Android framework的各种各样的Service,而这些Service从大的方向来划分,分为Java层Framework和Native Framework层(C++)的Service,几乎都是基于BInder IPC机制: -1. Java framework:作为Server端继承(或间接继承)于Binder类,Client端继承(或间接继承)于BinderProxy类。例如 ActivityManagerService(用于控制Activity、Service、进程等) 这个服务作为Server端,间接继承Binder类,而相应的ActivityManager作为Client端,间接继承于BinderProxy类。 当然还有PackageManagerService、WindowManagerService等等很多系统服务都是采用C/S架构; -2. Native Framework层:这是C++层,作为Server端继承(或间接继承)于BBinder类,Client端继承(或间接继承)于BpBinder。例如MediaPlayService(用于多媒体相关)作为Server端,继承于BBinder类,而相应的MediaPlay作为Client端,间接继承于BpBinder类。 +### 疑问 +到这里又迷糊了,你上面说了这么多Binder这么好,那为啥Android里面还有地方用Socket?SystemServer和Zygote之间的通信为啥不用Binder? +SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork的问题。UNIX上C++程序设计守则3中规定:多线程程序里不准使用fork。 -**总之,一句话"无Binder不Android"。** +而Binder通讯是需要多线程操作的,代理对象对Binder的调用是在Binder线程,需要再通过Handler调用主线程来操作。比如AMS与应用进程通讯,AMS的本地代理IApplicationThread通过调用ScheduleLaunchActivity,调用到的应用进程ApplicationThread的ScheduleLaunchActivity是在Binder线程,需要再把参数封装为一个ActivityClientRecord,sendMessage发送给H类(主线程Handler,ActivityThread内部类)。主要原因是害怕父进程binder线程有锁,然后子进程的主线程一直在等待其子线程(从父进程拷贝过来的子进程)的资源,但是其实父进程的子进程并没有被拷贝过来,造成死锁。 +所以fork不允许存在多线程。而非常巧的是Binder通讯偏偏就是多线程,所以干脆父进程(Zygote)这个时候就不使用Binder线程了。 -前面人都说了Binder的优点,我来讲故事 + +所以也并非Linux现有的IPC机制不够好,相反地,经过这么多优秀工程师的不断打磨,依然非常优秀,每种Linux的IPC机制都有存在的价值,同时在Android系统中也依然采用了大量Linux现有的IPC机制,根据每类IPC的原理特性,因时制宜,不同场景特性往往会采用其下最适宜的。比如在Android OS中的Zygote进程的IPC采用的是Socket(套接字)机制,Android中的Kill Process采用的signal(信号)机制等等。而Binder更多则用在system_server进程与上层App层的IPC交互。 + +### 讲故事 1. 当年Andy Rubin有个公司Palm做掌上设备的就是当年那种PDA有个系统叫PalmOS后来palm被收购了以后 Andy Rubin创立了Android -2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫Binder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 +2. Palm收购过一个公司叫Be里面有个移动系统叫BeOS,进程通信自己写了个实现叫OpenBinder由一个叫Dianne Hackbod的人开发并维护后来Binder也被用到了PalmOS里 3. Android创立了以后Andy从Palm带走了一大批人,其中就有Dianne。Dianne成为安卓系统总架构师。 @@ -114,73 +213,149 @@ Binder在Android系统中江湖地位非常之高。在Zygote孵化出system_ser + + ## Binder框架 -Binder是一种架构,这种架构提供了服务端接口、Binder驱动、客户端接口三个模块。 +在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。其中Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与Binder驱动的交互来间接的实现跨进程通信。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_framework.png) -- 服务端 +- Server 一个Binder服务端实际上就是一个Binder类的对象,该对象一旦创建,内部就启动一个隐藏线程。该线程接下来会接收Binder驱动发送的消息,收到消息后,会执行到Binder对象中的onTransact()函数,并按照该函数的参数执行不同的服务代码。因此,要实现一个Binder服务就必须重载onTransact()方法。 可以想象,重载onTransact()函数的主要内容是把onTransact()函数的参数转换为服务函数的参数,而onTransact()函数的参数来源是客户端调用transact()函数时传入的,因此,如果transact()有固定格式的输入,那么onTransact()就会有固定格式的输出。 + ##### Binder线程池 + + 每个Server进程在启动时会创建一个binder线程池,并向其中注册一个Binder线程;之后Server进程也可以向binder线程池注册新的线程,或者Binder驱动在探测到没有空闲binder线程时会主动向Server进程注册新的的binder线程。对于一个Server进程有一个最大Binder线程数限制,默认为16个binder线程,例如Android的system_server进程就存在16个线程。对于所有Client端进程的binder请求都是交由Server端进程的binder线程来处理的。 + - Binder驱动 + 和路由器一样,Binder驱动虽然默默无闻,却是通信的核心。尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,以字符驱动设备中的misc设备注册在设备目录/dev下,用户通过/dev/binder访问该它。驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。驱动和应用程序之间定义了一套接口协议,主要功能由ioctl()接口实现,不提供read(),write()接口,因为ioctl()灵活方便,且能够一次调用实现先写后读以满足同步交互,而不必分别调用write()和read()。Binder驱动的代码位于linux目录的drivers/misc/binder.c中。 + + + 任何一个服务端Binder对象被创建时,同时会在Binder驱动中创建一个mRemote对象,该对象的类型也是Binder类。客户端要访问远程服务时,都是通过mRemote对象。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_archi.png) -- 应用程序客户端 + 在Binder驱动层,每个接收端进程都有一个todo队列,用于保存发送端进程发送过来的binder请求,这类请求可以由接收端进程的任意一个空闲的binder线程处理;接收端进程存在一个或多个binder线程,在每个binder线程里都有一个todo队列,也是用于保存发送端进程发送过来的binder请求,这类请求只能由当前binder线程来处理。binder线程在空闲时进入可中断的休眠状态,当自己的todo队列或所属进程的todo队列有新的请求到来时便会唤醒,如果是由所需进程唤醒的,那么进程会让其中一个线程处理响应的请求,其他线程再次进入休眠状态。 - 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + +- Client + + Server向ServiceManager注册了Binder实体及其名字后,Client就可以通过名字获得该Binder的引用了。Client也利用保留的0号引用向ServiceManager请求访问某个Binder:我申请获得名字叫张三的Binder的引用。ServiceManager收到这个连接请求,从请求数据包里获得Binder的名字,在查找表里找到该名字对应的条目,从条目中取出Binder的引用,将该引用作为回复发送给发起请求的Client。从面向对象的角度,这个Binder对象现在有了两个引用:一个位于ServiceManager中,一个位于发起请求的Client中。如果接下来有更多的Client请求该Binder,系统中就会有更多的引用指向该Binder,就象java里一个对象存在多个引用一样。而且类似的这些指向Binder的引用是强类型,从而确保只要有引用Binder实体就不会被释放掉。通过以上过程可以看出,ServiceManager象个火车票代售点,收集了所有火车的车票,可以通过它购买到乘坐各趟火车的票-得到某个Binder的引用。 + + ##### 匿名 Binder + + 并不是所有Binder都需要注册给ServiceManager广而告之的。Server端可以通过已经建立的Binder连接将创建的Binder实体传给Client,当然这条已经建立的Binder连接必须是通过实名Binder实现。由于这个Binder没有向ServiceManager注册名字,所以是个匿名Binder。Client将会收到这个匿名Binder的引用,通过这个引用向位于Server中的实体发送请求。匿名Binder为通信双方建立一条私密通道,只要Server没有把匿名Binder发给别的进程,别的进程就无法通过穷举或猜测等任何方式获得该Binder的引用,向该Binder发送请求。 + + + + 客户端想要访问远程服务,必须获取远程服务在Binder对象中对应的mRemote引用,获得该mRemote对象后,就可以调用其transact()方法,调用该方法后,客户端线程进入Binder驱动,Binder驱动就会挂起当前线程,并向远程服务发送一个消息,消息中包含了客户端传进来的包裹。服务端拿到包裹后,会对包裹进行拆解,然后执行指定的服务函数,执行完毕后,再把执行结果放入客户端提供的reply包裹中。然后服务端向Binder驱动发送一个notify的消息,从而使得客户端线程从Binder驱动的代码区返回到客户端代码区。transact()的最后一个参数的含义是执行IPC调用的模式,分为两种:一种是双向,用常量0表示,其含义是服务端执行完指定的服务后返回一定的数据。另一种是单向,用常量1表示,其含义是不返回任何数据。最后,客户端就可以从reply中解析返回的数据了,同样,返回包裹中包含的数据也必须是有序的,而且这个顺序也必须是服务端和客户端事先约定好的。 + 从这里可以看出,对应用程序开发员来说,客户端似乎是直接调用远程服务对应的Binder,而事实上则是通过Binder驱动进行了中转。即存在两个Binder对象,一个是服务端的Binder对象,另一个则是Binder驱动中的Binder对象,所不同的是Binder驱动中的对象不会再额外产生一个线程。 +- ServiceManager + + ServiceManager本身的工作很简单:注册服务、查询服务、列出所有服务,启动一个死循环来解析Binder驱动读写动作,进行事务处理。ServiceManager用于管理系统中的各种服务。架构图如下所示: + + ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) + + 可以看出无论是注册服务和获取服务的过程都需要ServiceManager(与DNS类似),需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。Binder机制使用的是代理模式,在Server端的对象是实际对象,其他各个进程端所持有的都是Server端对象的Proxy代理对象,ServiceManager中会有一个类似map的结构,会存储Server端的信息,当Server端进程初始化时,会向ServiceManager中注册自己的信息。当client端想要访问时,也需要先向ServiceManager进行查询,当进行通信时,数据会流经内核空间中的binder驱动,此时驱动会对要传递的数据做转换。 + + 和DNS类似,ServiceManager的作用是将字符形式的Binder名字转化成Client中对该Binder的引用,使得Client能够通过Binder名字获得对Server中Binder实体的引用。注册了名字的Binder叫实名Binder,就象每个网站除了有IP地址外还有自己的网址。Server创建了Binder实体,为其取一个字符形式,可读易记的名字,将这个Binder连同名字以数据包的形式通过Binder驱动发送给ServiceManager,通知ServiceManager注册一个名叫张三的Binder,它位于某个Server中。驱动为这个穿过进程边界的Binder创建位于内核中的实体节点以及ServiceManager对实体的引用,将名字及新建的引用打包传递给ServiceManager。ServiceManager收数据包后,从中取出名字和引用填入一张查找表中。 + + 细心的读者可能会发现其中的蹊跷:ServiceManager是一个进程,Server是另一个进程,Server向ServiceManager注册Binder必然会涉及进程间通信。当前实现的是进程间通信却又要用到进程间通信,这就好象蛋可以孵出鸡前提却是要找只鸡来孵蛋。Binder的实现比较巧妙:预先创造一只鸡来孵蛋:ServiceManager和其它进程同样采用Binder通信,ServiceManager是Server端,有自己的Binder对象(实体),其它进程都是Client,需要通过这个Binder的引用来实现Binder的注册,查询和获取。ServiceManager提供的Binder比较特殊,它没有名字也不需要注册,当一个进程使用BINDER_SET_CONTEXT_MGR命令将自己注册成ServiceManager时Binder驱动会自动为它创建Binder实体(这就是那只预先造好的鸡)。其次这个Binder的引用在所有Client中都固定为0(handle=0)而无须通过其它手段获得。也就是说,一个Server若要向ServiceManager注册自己Binder就必须通过0这个引用号和ServiceManager的Binder通信。类比网络通信,0号引用就好比域名服务器的地址,你必须预先手工或动态配置好。要注意这里说的Client是相对ServiceManager而言的,一个应用程序可能是个提供服务的Server,但对ServiceManager来说它仍然是个Client。 + + 图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的。 + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_client_server.png) + + + + + -## Binder IPC原理 +## Binder 通信过程 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_communication.jpg) -每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl(设备驱动程序中设备控制接口函数,进程与内核通信的一种方法)等方法跟内核空间的驱动进行交互。 +- 首先,一个进程使用 BINDER*SET*CONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager; +- Server通过驱动向ServiceManager中进行服务注册,表明可以对外提供服务。ServiceManager有一个全局的service列表svcinfo,用来缓存所有服务的handler和name。驱动为这个Binder创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。 -Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下所示: +- 客户端与服务端通信,需要拿到服务端的对象,由于进程隔离,客户端拿到的其实是服务端的代理,也可以理解为引用。客户端通过Client 通过名字,在 Binder 驱动的帮助下从ServiceManager的svcinfo中查找服务,ServiceManager返回服务的代理。 -![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) +- Server进程启动之后,会进入中断等待状态,等待Client的请求。 -可以看出无论是注册服务和获取服务的过程都需要ServiceManager,需要注意的是此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。 +- 当Client需要和Server通信时,会通过BinderProxy将我们的请求参数发送给 内核,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态。 -图中Client/Server/ServiceManage之间的相互通信都是基于Binder机制。既然基于Binder机制通信,那么同样也是C/S架构,则图中的3大步骤都有相应的Client端与Server端。 +- Binder驱动收到请求之后,会唤醒Server进程,Binder驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间。 +- Server进程解析出请求内容,并将回复内容发送给Binder驱动。 +- 接着,Binder驱动收到回复之后,唤醒Client进程。Binder驱动还会反馈信息给Client,告诉Client:它发送给Binder驱动的请求,Binder驱动已经完成。 +- 接着,Binder驱动还会反馈信息给Server,告诉Server:它发送给Binder驱动的回复,Binder驱动已经收到。 +- Server将回复发送成功之后,再次进入等待状态,等待Client的请求。 +- 最后,Binder驱动将回复转发给Client。 -1. **[注册服务(addService)](http://gityuan.com/2015/11/14/binder-add-service/)**:Server进程要先注册Service到ServiceManager。该过程:Server是客户端,ServiceManager是服务端。 -2. **[获取服务(getService)](http://gityuan.com/2015/11/15/binder-get-service/)**:Client进程使用某个Service前,须先向ServiceManager中获取相应的Service。该过程:Client是客户端,ServiceManager是服务端。 -3. **使用服务**:Client根据得到的Service信息建立与Service所在的Server进程通信的通路,然后就可以直接与Service交互。该过程:client是客户端,server是服务端。 -图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与[Binder驱动](http://gityuan.com/2015/11/01/binder-driver/)进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_all_process.jpg) +## Binder的内存管理 +ServiceManager启动后,会通过系统调用mmap向内核空间申请128K的内存,用户进程会通过mmap向内核申请(1M-8K)的内存空间。 +这里用户空间mmap (1M-8K)的空间,为什么要减去8K,而不是直接用1M? -### 2.3 C/S模式 +Android的git commit记录: -BpBinder(客户端)和BBinder(服务端)都是Android中Binder通信相关的代表,它们都从IBinder类中派生而来,关系图如下: +> Modify the binder to request 1M - 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space. -![Binder关系图](http://gityuan.com/images/binder/prepare/Ibinder_classes.jpg) +大致的意思是:kernel的“backing store”需要一个保护页,这使得1M用来分配碎片内存时变得很差,所以这里减去两页来提高效率,因为减去一页就变成了奇数。 -- client端:BpBinder.transact()来发送事务请求; -- server端:BBinder.onTransact()会接收到相应事务。 +​ +**系统定义:**BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2) = (1M- sysconf(_SC_PAGE_SIZE) * 2) +**这里的8K,其实就是两个PAGE的SIZE, 物理内存的划分是按PAGE(页)来划分的,一般情况下,一个Page的大小为4K。** + +**内核会增加一个guard page,再加上内核本身的guard page,正好是两个page的大小,减去后,就是用户空间可用的大小。** + +在内存分配这块,还要分为32位和64位,32位的系统很好区分,虚拟内存为4G,用户空间从低地址开始占用3G,内核空间占用剩余的1G。 + +ARM32内存占用分配: + +![img](https://img-blog.csdnimg.cn/20200329164623413.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) + +但随着现在的硬件发展越来越迅速,应用程序的运算也越来越复杂,占用空间越来越大,原有的4G虚拟内存已经不能满足用户的需求,因此,现在的Android基本都是用64位的内存机制。 + +理论上讲,64位的地址总线可以支持高达16EB(2^64)的内存。AMD64架构支持52位(4PB)的地址总线和48位(256TB)的虚拟地址空间。在linux arm64中,如果页的大小为4KB,使用3级页表转换或者4级页表转换,用户空间和内核空间都支持有39bit(512GB)或者48bit(256TB)大小的虚拟地址空间。 + +2^64 次方太大了,Linux 内核只采用了 64 bits 的一部分(开启 CONFIG_ARM64_64K_PAGES 时使用 42 bits,页大小是 4K 时使用 39 bits),该文假设使用的页大小是 4K(VA_BITS = 39) + +ARM64 有足够的虚拟地址,用户空间和内核空间可以有各自的 2^39 = 512GB 的虚拟地址。 + +ARM64内存占用分配: + +![img](https://img-blog.csdnimg.cn/20200329164637770.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) + +用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 参考: https://www.zhihu.com/question/39440766/answer/93550572 +[https://zhuanlan.zhihu.com/p/35519585](https://zhuanlan.zhihu.com/p/35519585) + --- diff --git "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" index f550d1ee..0e052371 100644 --- "a/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" +++ "b/OperatingSystem/AndroidKernal/3.Android Framework\346\241\206\346\236\266.md" @@ -29,13 +29,28 @@ AmS的作用是管理所有应用程序中的Activity。 客户端主要包括以下重要类: -- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。 +- ActivityThread类:该类为应用程序的主线程类,所有的APK程序都有且仅有一个ActivityThread类,程序的入口为该类中的static main()函数。ActivityThread是Android Framework中一个非常重要的类,它代表一个应用进程的主线程,其职责就是调度及执行在该线程中运行的四大组件。 + + 注意到此处的ActivityThread创建于SystemServer进程中。 + + 由于SystemServer中也运行着一些系统APK,例如framework-res.apk、SettingsProvider.apk等,因此也可以认为SystemServer是一个特殊的应用进程。 + + AMS负责管理和调度进程,因此AMS需要通过Binder机制和应用进程通信。 + + 为此,Android提供了一个IApplicationThread接口,该接口定义了AMS和应用进程之间的交互函数。 + - Activity类:该类为APK程序中的一个最小运行单元,一个APK程序中可以包含多个Activity对象,ActivityThread类会根据用户操作选择运行哪个Activity对象。 + - PhoneWindow类:该类继承于Window类,同时,PhoneWindow类内部包含了一个DecorView对象。简而言之,PhoneWindow是把一个FrameLayout进行了一定的包装,并提供了一组通用的窗口操作接口。 + - Window类:该类提供了一组通用的窗口(Window)操作API,这里的窗口仅仅是程序层面上的,WmS所管理的窗口并不是Window类,而是一个View或者ViewGroup类,一般就是指DecorView类,即一个DecorView就是WmS所管理的一个窗口。Window是一个abstract类型。 + - DecorView类:该类是一个FrameLayout的子类,并且是PhoneWindow中的一个内部类。Decor的英文是Decoration,即”装饰“的意思,DecorView就是对普通的FrameLayout进行了一定的装饰,比如添加一个通用的Titlebar,并响应特定的按键消息等。 + - ViewRoot类:WmS管理客户端窗口时,需要通知客户端进行某种操作,这些都是通过异步消息完成的,实现的方式就是使用Handler,ViewRoot类就是继承于Handler,其作用主要是接收WmS的通知。 + - W类:该类继承于Binder,并且是ViewRoot的一个内部类。 + - WindowManager类:客户端要申请创建一个窗口,而具体创建窗口的任务是由WmS完成的,WindowManager类就像是一个部门经理,谁有什么需求就告诉它,由它和WmS进行交互,客户端不能直接和WmS进行交互。 diff --git "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" similarity index 97% rename from "VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index a6e48d63..7af53a77 100644 --- "a/VideoDevelopment/\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -36,7 +36,7 @@ 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 -- ***帧率***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 +- ***帧率(Frame Rate)***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) - ***关键帧***:相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 From 1750ce75d3e7d0f0e09b3123f1bd239cc0376fa6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 31 Dec 2020 14:13:11 +0800 Subject: [PATCH 012/183] udpate --- .../3.\345\206\205\345\255\230\347\256\241\347\220\206.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index fa5e3048..33824b74 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -243,7 +243,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 Android包含了标准Linux内核中内存管理设施的许多扩展,具体如下: -- ASHMem:这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 +- ASHMem(Anonymous Shared Memory):这个功能提供匿名共享内存,它将内存抽象为文件描述符。文件描述符可以传递给另一个进程以共享内存。 - Pmen:这个功能分配虚拟内存,使的它在物理上是连续的,因此对于那些不支持虚拟内存的设备非常实用。 From 3d5dc96ccef71bfe1313139381c88778f72c3e83 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 11 Jan 2021 15:13:08 +0800 Subject: [PATCH 013/183] update --- ...73\347\273\237\347\256\200\344\273\213.md" | 158 ++++++++++-- ...13\344\270\216\347\272\277\347\250\213.md" | 237 ++++++++++++++++-- ...05\345\255\230\347\256\241\347\220\206.md" | 63 ++++- OperatingSystem/5.I:O.md | 71 ++++++ ...07\344\273\266\347\256\241\347\220\206.md" | 74 ++++++ ...13\351\227\264\351\200\232\344\277\241.md" | 54 +++- ...72\347\241\200\347\237\245\350\257\206.md" | 146 +++++++---- ...255\346\224\276\345\231\250MediaPlayer.md" | 30 +++ 8 files changed, 737 insertions(+), 96 deletions(-) create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 82ba0f2a..0b4d80e4 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -10,6 +10,15 @@ +操作系统也是一种软件,但是操作系统是一种非常复杂的软件。操作系统提供了几种抽象模型 + +- 文件:对 I/O 设备的抽象 +- 虚拟内存:对程序存储器的抽象 +- 进程:对一个正在运行程序的抽象 +- 虚拟机:对整个操作系统的抽象 + + + ### 以现代标准而言,一个标准PC的操作系统应该提供以下功能 @@ -76,7 +85,7 @@ 计算机由处理器、存储器和输入/输出部件组成,每类部件都有一个或多个模块。这些部件以某种方式互连,以实现计算机执行程序的主要功能。因此,计算机有4个主要的结构化部件: -- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU) +- 处理器(Processor):控制计算机的操作,执行数据处理功能。只有一个处理器时,它通常指中央处理器(CPU). - 内存(Main memory):存储数据和程序。此类存储器通常是易失性的,即当计算机关机时,存储器的内容会丢失。相对于此的是磁盘存储器,当计算机关机时,它的内容不会丢失。内存通常也称为实存储器(real memory)或主存储器(primary memory)。 - 输入/输出模块(I/O modules):在计算机和外部环境之间移动数据。外部环境由各种外部设备组成,包括辅助存储器设备(如硬盘)、通信设备和终端。 - 系统总线(System bus):在处理器、内存和输入/输出模块间提供通信的设施。 @@ -148,9 +157,32 @@ Linux内核被装载后,就开始进行内核初始化的过程。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_system_start.png) +- Boot ROM: 当手机处于关机状态时,长按Power键开机,引导芯片开始从固化在`ROM`里的预设代码开始执行,然后加载引导程序到`RAM`; +- Boot Loader:这是启动Android系统之前的引导程序,主要是检查RAM,初始化硬件参数等功能。 +Android平台的基础是Linux内核,比如ART虚拟机最终调用底层Linux内核来执行功能。Linux内核的安全机制为Android提供相应的保障,也允许设备制造商为内核开发硬件驱动程序。 +- 启动Kernel的swapper进程(pid=0):该进程又称为idle进程, 系统初始化过程Kernel由无到有开创的第一个进程, 用于初始化进程管理、内存管理,加载Display,Camera Driver,Binder Driver等相关工作; +- 启动kthreadd进程(pid=2):是Linux系统的内核进程,会创建内核工作线程kworkder,软中断线程ksoftirqd,thermal等内核守护进程。`kthreadd进程是所有内核进程的鼻祖`。 +这里的Native系统库主要包括init孵化来的用户空间的守护进程、HAL层以及开机动画等。启动init进程(pid=1),是Linux系统的用户进程,`init进程是所有用户进程的鼻祖`。 + +- init进程会孵化出ueventd、logd、healthd、installd、adbd、lmkd等用户守护进程; +- init进程还启动`servicemanager`(binder服务管家)、`bootanim`(开机动画)等重要服务 +- init进程孵化出Zygote进程,Zygote进程是Android系统的第一个Java进程(即虚拟机进程),`Zygote是所有Java进程的父进程`,Zygote进程本身是由init进程孵化而来的。 + +- Zygote进程,是由init进程通过解析init.rc文件后fork生成的,Zygote进程主要包含: + - 加载ZygoteInit类,注册Zygote Socket服务端套接字 + - 加载虚拟机 + - 提前加载类preloadClasses + - 提前加载资源preloadResouces +- System Server进程,是由Zygote进程fork而来,`System Server是Zygote孵化的第一个进程`,System Server负责启动和管理整个Java framework,包含ActivityManager,WindowManager,PackageManager,PowerManager等服务。 +- Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包含AudioFlinger,Camera Service等服务。 +- Zygote进程孵化出的第一个App进程是Launcher,这是用户看到的桌面App; +- Zygote进程还会创建Browser,Phone,Email等App进程,每个App至少运行在一个进程上。 +- 所有的App进程都是由Zygote进程fork生成的。 + + ## CPU(Central Processing Unit) @@ -158,25 +190,35 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -**程序是把寄存器作为对象来描述的** +CPU 主要由两部分构成:`控制单元` 和 `算术逻辑单元(ALU)` -使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: +- 控制单元:从内存中提取指令并解码执行 +- 算数逻辑单元(ALU):处理算数和逻辑运算 -- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +CPU 是计算机的心脏和大脑,它和内存都是由许多晶体管组成的电子部件。它接收数据输入,执行指令并处理信息。它与输入/输出(I / O)设备进行通信,这些设备向 CPU 发送数据和从 CPU 接收数据。 -- ***标志寄存器***:存储运算处理后的CPU的状态。 +CPU 的内部由**寄存器、控制器、运算器和时钟**四部分组成,各部分之间通过电信号连通。 -- ***程序计数器(Program Counter, PC)***:存储下一条指令所在内存的地址。 +- `寄存器`是中央处理器内的组成部分。它们可以用来暂存指令、数据和地址。可以将其看作是内存的一种。根据种类的不同,一个 CPU 内部会有 20 - 100个寄存器。 +- `控制器`负责把内存上的指令、数据读入寄存器,并根据指令的结果控制计算机 +- `运算器`负责运算从内存中读入寄存器的数据 +- `时钟` 负责发出 CPU 开始计时的时钟信号 -- ***基址寄存器***:存储数据内存的起始地址。 -- ***变址寄存器***:存储基址寄存器的相对地址。 -- ***通用寄存器***:存储任意数据。 +**程序是把寄存器作为对象来描述的** -- ***指令寄存器(Instruction Register, IR)***:存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +使用高级语言编写的程序会在编译后转化成机器语言,然后通过CPU内部的寄存器来处理。不同类型的CPU,其内部寄存器的数量,种类以及寄存器存储的数值范围都是不同的。根据功能的不同,我们可以将寄存器大致划分为八类: -- ***栈寄存器***:存储栈区域的起始地址。 +- ***累加寄存器简称累加器(Accumulator, AC)***:是一个通用寄存器。存储临时的执行运算的数据和运算后的数据。 +- ***标志寄存器***:存储运算处理后的CPU的状态。 +- ***程序计数器(Program Counter, PC)***:记录将要取出的指令的地址。存储下一条指令所在内存的地址。 +- ***基址寄存器***:存储数据内存的起始地址。 +- ***变址寄存器***:存储基址寄存器的相对地址。 +- ***通用寄存器***:存储任意数据。 +- ***指令寄存器(Instruction Register, IR)***:记录最近取出的指令。存储指令,CPU取到的指令存放在处理器的一个寄存器中,这个寄存器就是指令寄存器。CPU内部使用,程序员无法通过程序对该寄存器进行读写操作。 +- ***堆栈寄存器(stack pointer)***:目的是跟踪调用堆栈,存储栈区域的起始地址。指向内存中当前栈的顶端。堆栈指针会包含输入过程中的有关参数、局部变量以及没有保存在寄存器中的临时变量。 +- **程序状态字寄存器(PSW(Program Status Word))**:这个寄存器是由操作系统维护的8个字节(64位) long 类型的数据集合。它会跟踪当前系统的状态。除非发生系统结束,否则我们可以忽略 PSW 。用户程序通常可以读取整个PSW,但通常只能写入其某些字段。PSW 在系统调用和 I / O 中起着重要作用。 其中,程序计数器,累加寄存器,标志寄存器,指令寄存器和栈寄存器都只有一个,其他的寄存器一般有多个。 @@ -230,11 +272,25 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -### 中断 +#### 操作系统的两种CPU状态 + +- 内核态(Kernel Mode):运行操作系统程序 +- 用户态(User Mode):运行用户程序 + +操作系统只需要这两种状态,同时这两种状态由对应的两种指令: +- 特权(privilege)指令:只能由操作系统使用、用户程序不能使用的指令 +- 非特权指令:用户程序可以使用的指令 +用户态 -> 内核态的唯一途径是通过中断/异常/陷入机制。 +内核态 -> 用户态是通过设置程序状态字PSW。 +一旦 CPU 决定去实施中断后,程序计数器和 PSW 就会被压入到当前堆栈中并且 CPU 会切换到内核态。设备编号可以作为内存的一个引用,用来寻找该设备中断处理程序的地址。这部分内存称作`中断向量(interrupt vector)`。一旦中断处理程序(中断设备的设备驱动程序的一部分)开始后,它会移除栈中的程序计数器和 PSW 寄存器,并把它们进行保存,然后查询设备的状态。在中断处理程序全部完成后,它会返回到先前用户程序尚未执行的第一条指令。 + +### 中断/异常 + +可以说操作系统是由“中断驱动”或者“时间驱动”的。中断/异常是CPU对系统发生的某个事件作出的一种反应。 所有计算机都提供了允许其他模块(I/O、存储器)中断处理器正常处理过程的机制。中断最初是用于提高处理器效率的一种手段。例如,多数I/O设备都要远慢于处理器,处理器必须暂停并保持空闲,直到打印机完成工作。暂停的时间长度可能相当于成百上千个不涉及存储器的指令周期,显然,这对于处理器的使用来说是非常浪费的。这种只有一个单独程序的情况,称为单道程序设计。在单道程序设计中处理器话费一定的运行时间进行计算,直到遇到一个I/O指令,这时它必须等到该I/O指令结束后才能继续执行。这种问题是可以避免的,就是存储器可以保存多个程序,在一个程序等待时通过切换去执行其他的程序,这种处理称为多道程序设计或多任务处理。它是现代操作系统的主要方案。多道程序设计的目的是为了让处理器和I/O设备(包括存储设备)同时保持忙状态,以实现最大的效率。 @@ -250,12 +306,38 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -### 中断处理 +### 举个栗子 + +- 中断 + + 有一天我正在看书的时候,来电话了,这时候我需要执行中断,我会把当前看到的书的进度用书签标记,然后去接电话,等电话接完后,再返回从书签的位置继续读书。 + +- 异常 + + 同样我在读书,但是由于新买的书纸张太硬,一不小心把手划破流血了,那这个时候就是异常,我同样需要把当前看到的书的进度用书签标记,然后去用创可贴来处理伤口,等处理完后再返回书签的位置继续读书。 +### 中断/异常机制工作原理 + +硬件和软件相互配合而使计算机系统得以充分发挥能力。 + +- 硬件的作用 -- 中断/异常响应 + + 捕获中断源发出的中断/异常请求,以一定方式响应,将处理器控制权交给特定的处理程序。发现中断、接受中断都是由硬件来完成。 + +- 软件的作用 -- 中断/异常处理程序 + + 识别中断/异常类型并完成相应的处理。 + + + + + +### 中断处理 + 当I/O设备完成一次I/O操作时,发生以下硬件事件: 1. 设备给处理器发送一个中断信号。 @@ -270,6 +352,21 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 +与每一 I/O 类相关联的是一个称作 `中断向量(interrupt vector)` 的位置(靠近内存底部的固定区域)。它包含中断服务程序的入口地址。假设当一个磁盘中断发生时,用户进程 3 正在运行,则中断硬件将程序计数器、程序状态字、有时还有一个或多个寄存器压入堆栈,计算机随即跳转到中断向量所指示的地址。这就是硬件所做的事情。然后软件就随即接管一切剩余的工作。 + +当中断结束后,操作系统会调用一个 C 程序来处理中断剩下的工作。在完成剩下的工作后,会使某些进程就绪,接着调用调度程序,决定随后运行哪个进程。然后将控制权转移给一段汇编语言代码,为当前的进程装入寄存器值以及内存映射并启动该进程运行,下面显示了中断处理和调度的过程。 + +1. 硬件压入堆栈程序计数器等 +2. 硬件从中断向量装入新的程序计数器 +3. 汇编语言过程保存寄存器的值 +4. 汇编语言过程设置新的堆栈 +5. C 中断服务器运行(典型的读和缓存写入) +6. 调度器决定下面哪个程序先运行 +7. C 过程返回至汇编代码 +8. 汇编语言过程开始运行新的当前进程 + +一个进程在执行过程中可能被中断数千次,但关键每次中断后,被中断的进程都返回到与中断发生前完全相同的状态。 + ## 存储器 @@ -377,10 +474,6 @@ CPU(中央处理器)是计算机的大脑,它主要和内存进行交互,从 -![](https://raw.githubusercontent.com/CharonChui/Pictures/master/![](https://raw.githubusercontent.com/CharonChui/Pictures/master/rtsp_rtp_rtcp.jpeg)) - - - - 应用和框架:应用开发者最关心这一层及访问低层服务的API。 - Binder IPC:Binder进程间通信机制允许应用框架打破进程的界限来访问Android系统服务代码,从而允许系统的高层框架API与Android的系统服务进行交互。 - Android系统服务:框架中大部分能够调用系统服务的接口都向开发者开放,以便开发者能够使用底层的硬件和内核功能。Android系统服务分为两部分:媒体服务处理播放和录制媒体文件,系统服务处理应用所需要的系统功能。 @@ -403,17 +496,44 @@ Android在Linux内核中增加了两个提升电源管理能力的新功能: +## 一个程序的执行过程 +```c +#include +int main(int argc, char *argv[]) { + puts("hello world"); + return 0; +} +``` ----- -- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) + +1. 用户通过命令或者图标点击等通知操作系统执行hello world程序。 +2. 操作系统会去找到hello world程序的相关信息,检查其类型是否是可执行文件,并通过程序的首部信息,确定代码和数据在可执行文件中的位置并计算出对应的磁盘块地址。 +3. 操作系统创建一个新的进程,并将hello world程序的执行文件映射到该进程结构,表示由该进程执行该hello world程序。 +4. 操作系统为hello world程序设置cpu上下文环境并跳到程序开始处。 +5. 执行hello world程序的第一条指令,这时候会发生缺页异常(内存中没有该程序) +6. 操作系统开始分配一页物理内存,并将前面计算出的磁盘块地址将代码从磁盘读入内存,然后继续执行hello world程序。 +7. hello world程序执行puts函数(系统调用),想要在显示器上写入字符串。 +8. 操作系统找到要将字符串送往的显示设备,通常设备是由一个进程控制的,所以,操作系统将要写的字符串送给该进程。 +9. 控制设备的进程告诉设备的窗口系统它要显示字符串。窗口系统确定这是一个合法的操作,然后将字符串转换成像素,将像素写入设备的存储影响区。 +10. 视频硬件将像素转换成显示器可接收的一组控制/数据信号。 +11. 显示器解释信号,激发液晶屏。 +12. 这样我们就能在屏幕上看到了"hello world"。 + +---- + +- [下一篇:2.进程与线程](https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/2.%E8%BF%9B%E7%A8%8B%E4%B8%8E%E7%BA%BF%E7%A8%8B.md) + + + + --- diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 8dd5f952..c96b8f66 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -1,14 +1,18 @@ # 2.进程与线程 +### 为什么要引入进程的概念? +我们知道I/O操作是耗时的,CPU在执行的一个程序的时候这个程序可能会有一些耗时的操作,而CPU的执行是非常快的,那如果这种情况下,CPU一直等待这个程序执行完耗时的操作再继续执行,就会导致CPU的使用率大大降低,为了提高CPU的使用率,就引入了多道程序设计,也就是说有多个程序执行,当执行到一个程序时如果遇到耗时的操作,那CPU就切换到另一个程序继续执行,等第一个程序的耗时操作执行完后再切换到第一个程序执行,这个切换过程不能只切换PC的指针,还需要记录一些其他的程序的信息,所以为了描述这种程序的信息,引入了进程的概念。 -狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 -广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 -进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。 +## 进程概念 +狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being executed)。 +广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是[操作系统](https://baike.baidu.com/item/操作系统/192)动态执行的[基本单元](https://baike.baidu.com/item/基本单元),在传统的[操作系统](https://baike.baidu.com/item/操作系统)中,进程既是基本的[分配单元](https://baike.baidu.com/item/分配单元),也是基本的执行单元。 + +进程的概念主要有两点:第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括[文本](https://baike.baidu.com/item/文本)区域(text region)、数据区域(data region)和[堆栈](https://baike.baidu.com/item/堆栈)(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有[处理](https://baike.baidu.com/item/处理)器赋予程序生命时(操作系统执行之),它才能成为一个活动的实体,我们称其为[进程](https://baike.baidu.com/item/进程)。可以简单的理解为:进程 = 资源 + 指令执行序列。 进程是60年代初首先由[麻省理工学院](https://baike.baidu.com/item/麻省理工学院)的[MULTICS系统](https://baike.baidu.com/item/MULTICS系统)和IBM公司的[CTSS](https://baike.baidu.com/item/CTSS)/360系统引入的。 @@ -62,7 +66,7 @@ -## 进程的状态 +## 进程的状态(五状态进程模型) @@ -88,7 +92,9 @@ - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 - 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对PCB的操作。 -#### 进程控制块 +#### 进程控制块(PCB) + +又称进程描述符、进程属性,是操作系统用于管理进程的一个专门数据结构。PCB是操作系统感知进程存在的唯一标志。进程和PCB是一一对应的。 进程执行的任意时刻,都可由如下元素来表征: @@ -105,6 +111,8 @@ +进程表: 所有进程的PCB集合。 + ## 进程创建 操作系统决定创建一个新进程时,会按如下步骤操作: @@ -119,8 +127,24 @@ ## 进程切换 + + +### 进程表(Process Table) + 操作系统为了执行进程间的切换,会维护着一张表格,这张表就是进程表(process table)。每个进程占用一个进程表项。该表项包含了进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、所打开文件的状态、账号和调度信息,以及其他在进程由运行态转换到就绪态或阻塞态时所必须保存的信息,从而保证该进程随后能再次启动,就像从未被中断过一样。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_table.png?raw=true) + +上面是典型的进程表表项中的一些字段: + +第一列内容与进程管理有关,第二列内容与存储管理有关,第三列内容与文件管理有关。 + + + + + + + **操作系统最底层的就是调度程序**,在它上面有许多进程。所有关于中断处理、启动进程和停止进程的具体细节都隐藏在调度程序中。事实上,调度程序只是一段非常小的程序。 表面上看,进程切换很简单。在某个时刻,操作系统中断一个正在运行的进程,将另一个进程置于运行模式,并把控制权交给后者。然而,这会引发若干个问题。首先,什么事件触发了进程的切换? 其次,必须认识到模式切换和进程切换键的区别。 @@ -171,31 +195,61 @@ -进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 +以Linux为例,进程间的信息交换,具体内容分为:控制信息交换和数据交换,控制信息的交换为低级通信,数据的交换为高级通信。 -- 信号 +- Socket套接字 + + socket 提供端到端的双相通信。一个套接字可以与一个或多个进程关联。就像管道有命令管道和未命名管道一样,套接字也有两种模式,套接字一般用于两个进程之间的网络通信,网络套接字需要来自诸如TCP(传输控制协议)或较低级别UDP(用户数据报协议)等基础协议的支持。 + +- Signals信号/信号量 + + 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。信号量是一个特殊的变量。用于进程间传递信息的一个整数值。 + + 信号是 UNIX 系统最先开始使用的进程间通信机制,因为 Linux 是继承于 UNIX 的,所以 Linux 也支持信号机制,通过向一个或多个进程发送`异步事件信号`来实现,信号可以从键盘或者访问不存在的位置等地方产生;信号通过 shell 将任务发送给子进程。 + + 你可以在 Linux 系统上输入 `kill -l` 来列出系统使用的信号。 + + 进程可以选择忽略发送过来的信号,但是有两个是不能忽略的:`SIGSTOP` 和 `SIGKILL` 信号。SIGSTOP 信号会通知当前正在运行的进程执行关闭操作,SIGKILL 信号会通知当前进程应该被杀死。除此之外,进程可以选择它想要处理的信号,进程也可以选择阻止信号,如果不阻止,可以选择自行处理,也可以选择进行内核处理。如果选择交给内核进行处理,那么就执行默认处理。 + + 操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。 - 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。 +- Shared Memory共享内存 -- 共享存储系统 + 多台服务器访问同一个存储设备的同一分区。 - 多台服务器访问同一个存储设备的同一分区 + 两个进程之间还可以通过共享内存进行进程间通信,其中两个或者多个进程可以访问公共内存空间。两个进程的共享工作是通过共享内存完成的,一个进程所作的修改可以对另一个进程可见(很像线程间的通信)。 -- 消息传递系统 +- Message Queue消息传递系统/消息队列 进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 -- 管道通信 + 一听到消息队列这个名词你可能不知道是什么意思,消息队列是用来描述内核寻址空间内的内部链接列表。可以按几种不同的方式将消息按顺序发送到队列并从队列中检索消息。每个消息队列由 IPC 标识符唯一标识。消息队列有两种模式,一种是`严格模式`, 严格模式就像是 FIFO 先入先出队列似的,消息顺序发送,顺序读取。还有一种模式是 `非严格模式`,消息的顺序性不是非常重要。 - 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 +- Pipe管道通信 + + 在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。shell 中的`管线 pipelines` 就是用管道实现的,当 shell 发现输出 + 发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 + + + 管道分为无名管道和命名管道,前者用于父子进程通信,后者用于任意进程通信。 + + 入先出队列 FIFO 通常被称为 `命名管道(Named Pipes)`,命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为,写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。 + + + + + + + + ## 进程调度 -进程调度就是处理器调度(上下文切换) +当一个计算机是多道程序设计系统时,会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时,就会发生这种情况。如果只有一个 CPU 可用,那么必须选择接下来哪个进程/线程可以运行。操作系统中有一个叫做调度程序(scheduler) 的角色存在,它就是做这件事儿的,该程序使用的算法叫做调度算法(scheduling algorithm) 。进程调度就是处理器调度(上下文切换)。 ### 调度级别 @@ -223,18 +277,56 @@ ### 调度算法 -- 先进先出 +- 先来先服务(first-come,first-serverd) 按照进入就绪队列的进程顺序,不加其他条件干涉 -- 短进程优先 +- 最短作业优先(Shortest Job First) 优先选出就绪队列中CPU执行时间最短的进程,例如:就绪队列有4个进程P1,P2,P3,P4,执行时间为:16,12,4,3 按照短进程优先,则周转时间(从进程提交到进程完成的时间间隔)分别为:35,19,7,3 - 平均周转时间:16,平均周转时间越小,调度性能越好 + 平均周转时间:16,平均周转时间越小,调度性能越好。 + + 在所有的进程都可以运行的情况下,最短作业优先的算法才是最优的。 + +- 最短剩余时间优先(Shortest Remaining Time Next) + + 使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。 - 轮转法 - - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中**所有队列均可获得一个时间片的处理器运行** - - 多级队列: 让系统中所有进程分成若干类,每类一级 + + 一种最古老、最简单、最公平并且最广泛使用的算法就是轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾。 + + - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中所有队列均可获得一个时间片的处理器运行。 + - 多级队列: 让系统中所有进程分成若干类,每类一级。 + +- 优先级调度 + + 每个进程都被赋予一个优先级,优先级高的进程优先运行。 + + + + + +## 内核栈与用户栈的区别 + +每个进程一般会有两个栈,一个用户栈,一个内核栈,存在于内核空间。 + +当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容时用户堆栈的地址,使用的是用户栈。 + +当进程在内核空间时,cpu堆栈指针寄存器里的内容是内核栈空间的地址,使用内核栈。 + +内核栈是**内存**中属于操作系统空间的一块区域,主要用途为: + +- 保护中断现场 +- 保护操作系统子程序间相互调用的参数、返回值、返回点以及子程序函数的局部变量 + +用户栈是用户进程空间中的一块区域,用于保存用户进程的子程序间相互作用的参数、返回值以及相关局部变量 + +**当进程因为中断或者系统调用而陷入内核态,进程所使用的堆栈也要从用户栈转到内核栈**。进程陷入内核态后,先把用户态堆栈的地址保存在内核栈中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈到内核栈的转换。**当进程从内核态回复为用户态时,在内核态之后的最后将保存在内核栈里面的用户栈地址恢复到堆栈指针寄存器即可**。这样就实现了内核栈和用户栈的互转。 + +注意:**每次进程从用户态陷入内核的时候得到的内核栈都是空的,所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器即可。** + + ## 死锁 @@ -453,6 +545,111 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 多线程技术是指把执行一个应用程序的进程划分为可以同时运行的多个线程。 +为什么要有线程呢? + +在传统的操作系统中,每个进程都有一个地址空间和一个控制线程。事实上,这是大部分进程的定义。不过,在许多情况下,经常存在同一地址空间中运行多个控制线程的情形,这些线程就像是分离的进程。 + +上面说到进程 = 资源 + 指令执行序列。那能不能将资源和指令执行分开,组合成一个资源 + 多个指令执行序列的方式? 这种不切换资源,只切换执行指令的方式就是线程。线程保留了并发的优点,避免了进程切换的代价。 + + + +线程不像是进程那样具备较强的独立性。同一个进程中的所有线程都会有完全一样的地址空间,这意味着它们也共享同样的全局变量。由于每个线程都可以访问进程地址空间内每个内存地址,**「因此一个线程可以读取、写入甚至擦除另一个线程的堆栈」**。线程之间除了共享同一内存空间外,还具有如下不同的内容 + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/process_thread_compare.png?raw=true) + +上图左边的是同一个进程中`每个线程共享`的内容,上图右边是`每个线程`中的内容。也就是说左边的列表是进程的属性,右边的列表是线程的属性。 + +**「线程之间的状态转换和进程之间的状态转换是一样的」**。 + +每个线程都会有自己的堆栈,如下图所示 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_heap.png?raw=true) + +#### 线程系统调用 + +进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数(比如 `thread_create`)创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符,该标识符就是新线程的名字。 + +当一个线程完成工作后,可以通过调用一个函数(比如 `thread_exit`)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程中,可以通过调用函数例如 `thread_join` ,表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。 + +另一个常见的线程是调用 `thread_yield`,它允许线程自动放弃 CPU 从而让另一个线程运行。这样一个调用还是很重要的,因为不同于进程,线程是无法利用时钟中断强制让线程让出 CPU 的。 + + + +### 线程实现 + +主要有三种实现方式 + +- 在用户空间中实现线程; +- 在内核空间中实现线程; +- 在用户和内核空间中混合实现线程。 + +下面我们分开讨论一下 + +#### 在用户空间中实现线程 + +第一种方法是把整个线程包放在用户空间中,内核对线程一无所知,它不知道线程的存在。所有的这类实现都有同样的通用结构 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_user.png?raw=true) + +> `运行时系统(Runtime System)` 也叫做运行时环境,该运行时系统提供了程序在其中运行的环境。此环境可能会解决许多问题,包括应用程序内存的布局,程序如何访问变量,在过程之间传递参数的机制,与操作系统的接口等等。编译器根据特定的运行时系统进行假设以生成正确的代码。通常,运行时系统将负责设置和管理堆栈,并且会包含诸如垃圾收集,线程或语言内置的其他动态的功能。 + +在用户空间管理线程时,每个进程需要有其专用的`线程表(thread table)`,用来跟踪该进程中的线程。这些表和内核中的进程表类似,不过它仅仅记录各个线程的属性,如每个线程的程序计数器、堆栈指针、寄存器和状态。该线程表由运行时系统统一管理。当一个线程转换到就绪状态或阻塞状态时,在该线程表中存放重新启动该线程的所有信息,与内核在进程表中存放的信息完全一样。 + + + +在用户空间中实现线程要比在内核空间中实现线程具有这些方面的优势:考虑如果在线程完成时或者是在调用 `pthread_yield` 时,必要时会进程线程切换,然后线程的信息会被保存在运行时环境所提供的线程表中,然后,线程调度程序来选择另外一个需要运行的线程。保存线程的状态和调度程序都是`本地过程`,**所以启动他们比进行内核调用效率更高。因而不需要切换到内核,也就不需要上下文切换,也不需要对内存高速缓存进行刷新,因为线程调度非常便捷,因此效率比较高**。 + +在用户空间实现线程还有一个优势就是**它允许每个进程有自己定制的调度算法**。例如在某些应用程序中,那些具有垃圾收集线程的应用程序(知道是谁了吧)就不用担心自己线程会不会在不合适的时候停止,这是一个优势。用户线程还具有较好的可扩展性,因为内核空间中的内核线程需要一些表空间和堆栈空间,如果内核线程数量比较大,容易造成问题。 + + + +尽管在用户空间实现线程会具有一定的性能优势,但是劣势还是很明显的,你如何实现`阻塞系统调用`呢?假设在还没有任何键盘输入之前,一个线程读取键盘,让线程进行系统调用是不可能的,因为这会停止所有的线程。所以,**使用线程的一个目标是能够让线程进行阻塞调用,并且要避免被阻塞的线程影响其他线程**。 + +与阻塞调用类似的问题是`缺页中断`问题,实际上,计算机并不会把所有的程序都一次性的放入内存中,如果某个程序发生函数调用或者跳转指令到了一条不在内存的指令上,就会发生页面故障,而操作系统将到磁盘上取回这个丢失的指令,这就称为`缺页故障`。而在对所需的指令进行读入和执行时,相关的进程就会被阻塞。如果只有一个线程引起页面故障,内核由于甚至不知道有线程存在,通常会把整个进程阻塞直到磁盘 I/O 完成为止,尽管其他的线程是可以运行的。 + +另外一个问题是,如果一个线程开始运行,该线程所在进程中的其他线程都不能运行,除非第一个线程自愿的放弃 CPU,在一个单进程内部,没有时钟中断,所以不可能使用轮转调度的方式调度线程。除非其他线程能够以自己的意愿进入运行时环境,否则调度程序没有可以调度线程的机会。 + +### 在内核中实现线程 + +现在我们考虑使用内核来实现线程的情况,此时不再需要运行时环境了。另外,每个进程中也没有线程表。相反,在内核中会有用来记录系统中所有线程的线程表。当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。 + +当某个线程希望创建一个新线程或撤销一个已有线程时,它会进行一个系统调用,这个系统调用通过对线程表的更新来完成线程创建或销毁工作。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_kernel.png?raw=true) + +内核中的线程表持有每个线程的寄存器、状态和其他信息。这些信息和用户空间中的线程信息相同,但是位置却被放在了内核中而不是用户空间中。另外,内核还维护了一张进程表用来跟踪系统状态。 + +所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程。但是在用户实现中,运行时系统始终运行自己的线程,直到内核剥夺它的 CPU 时间片(或者没有可运行的线程存在了)为止。 + + + +由于在内核中创建或者销毁线程的开销比较大,所以某些系统会采用可循环利用的方式来回收线程。当某个线程被销毁时,就把它标志为不可运行的状态,但是其内部结构没有受到影响。稍后,在必须创建一个新线程时,就会重新启用旧线程,把它标志为可用状态。 + +如果某个进程中的线程造成缺页故障后,内核很容易的就能检查出来是否有其他可运行的线程,如果有的话,在等待所需要的页面从磁盘读入时,就选择一个可运行的线程运行。这样做的缺点是系统调用的代价比较大,所以如果线程的操作(创建、终止)比较多,就会带来很大的开销。 + + + +## 在用户和内核空间中混合实现线程 + +结合用户空间和内核空间的优点,设计人员采用了一种`内核级线程`的方式,然后将用户级线程与某些或者全部内核线程多路复用起来 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/user_kernel_thread_os.png?raw=true) + +在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度。采用这种方法,内核只识别内核级线程,并对其进行调度。其中一些内核级线程会被多个用户级线程多路复用。 + + + +**导致系统出现死锁的情况** + +死锁的出现需要同时满足下面四个条件 + +> - 互斥(Mutual Exclusion):一次只能有一个进程使用资源。如果另一个进程请求该资源,则必须延迟请求进程,直到释放该资源为止。 +> - 保持并等待(Hold and Wait):必须存在一个进程,该进程至少持有一个资源,并且正在等待获取其他进程当前所持有的资源。 +> - 无抢占(No Preemption):资源不能被抢占,也就是说,在进程完成其任务之后,只能由拥有它的进程自动释放资源。 +> - 循环等待(Circular Wait) :必须存在一组 {p0,p1,..... pn} 的等待进程,使 p0 等待 p1 持有的资源,p1 等待由 p2 持有的资源, pn-1 正在等待由 pn 持有的资源,而 pn 正在等待由 p0 持有的资源。 + ### 进程和线程的区别 @@ -467,7 +664,7 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 - 线程(thread):可分派的工作单元。它包括处理器上下文环境(包含程序计数器和栈指针)和栈中自身的数据区域。线程顺序执行且可以中断,因此处理器可以转到另一个线程。 - 进程(process):一个或多个线程和相关系统资源(如包含程序和代码的存储空间、打开的文件和设备)的集合。它严格对应于一个正在执行的程序的概念。通过把一个应用程序分解成多个线程,程序员可以很大程度上控制应用程序的模块性及相关事件的时间安排。 -线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。 +线程比进程更轻量级,所以它们比进程更容易(更快)创建,也更容易撤销。在许多系统中,创建一个线程比创建一个进程要快10~100倍。线程会共享它所在进程的地址空间和其他资源。 diff --git "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" index 33824b74..31ab0e09 100644 --- "a/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" +++ "b/OperatingSystem/3.\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -34,6 +34,21 @@ 上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。 +## 地址空间 + +如果要使多个应用程序同时运行在内存中,必须要解决两个问题:`保护`和 `重定位`。第一种解决方式是用`保护密钥标记内存块`,并将执行过程的密钥与提取的每个存储字的密钥进行比较。这种方式只能解决第一种问题(破坏操作系统),但是不能解决多进程在内存中同时运行的问题。 + +还有一种更好的方式是创造一个存储器抽象:`地址空间(the address space)`。就像进程的概念创建了一种抽象的 CPU 来运行程序,地址空间也创建了一种抽象内存供程序使用。 + +#### 基址寄存器和变址寄存器 + +最简单的办法是使用`动态重定位(dynamic relocation)`技术,它就是通过一种简单的方式将每个进程的地址空间映射到物理内存的不同区域。还有一种方式是使用基址寄存器和变址寄存器。 + +- 基址寄存器:存储数据内存的起始位置 +- 变址寄存器:存储应用程序的长度。 + +每当进程引用内存以获取指令或读取、写入数据时,CPU 都会自动将`基址值`添加到进程生成的地址中,然后再将其发送到内存总线上。同时,它检查程序提供的地址是否大于或等于`变址寄存器` 中的值。如果程序提供的地址要超过变址寄存器的范围,那么会产生错误并中止访问。 + ## 内存的演变 @@ -90,7 +105,17 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 ### 伙伴系统 -固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 +固定分区和动态分区方案都有缺陷。固定分区方案限制了活动进程数量,且如果可用分区的大小与进程大小很不匹配,那么内存空间的利用率会非常低。动态分区的维护特别复杂,并且会引入进行压缩的额外开销。更有吸引力的一种折中方案是伙伴系统。 它是一种经典的内存分配方案,是一种特殊的“分离适配”算法。主要思想是将内存按2的幂进行划分,组成若干空闲块链表,查找该链表找到能满足进程需求的最佳匹配块。 + +算法: + +- 首先将整个可用空间看做一块:2的幂,这里假设一共有1M的内存。 +- 假设进程申请的空间大小为s,这里假设为100k,如果满足2的u-1次幂 < s <= 2的u次幂,则分配整个块,否则,将块划分为两个大小相等的伙伴,大小为2的u-1次幂,那这两个大小相等的伙伴就是伙伴关系。等内存回收后,具有伙伴关系的两块还可以合并到一起,组成一个更大的内存块。 + - 1M内存分为两块。 + - 512k、512k仍然大于100k,将第一块再分为两块。 + - 256k、256k、512k,而第一块256k仍然大于100k,第一块256k再分配。 + - 128k、128k、256k、512k。这个时候64k < 100k < 128k,所以把第一个128k分配给当前申请100k内存的进程。 + - 等该进程的内存回收后,前两个128k的内存具有伙伴关系,还可以合并成256k。合并后和后面的256k又是一个伙伴,又合并成512k,发现和后面的512k又是一个伙伴,就又合并成了1M的内存块。 @@ -101,11 +126,13 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 在大小相等的分区中一个进程在其声明周期中可能占据不同的分区。首次创建一个进程映像时,它被装入内存中的某个分区。以后该进程可能被换出,当它再次被换入时,可能被指定到与上一次不同的分区中。动态分区也存在同样的情况,压缩后内存中的进程也可能发生移动。因此,进程访问(指令和数据单元)的位置不是固定的。进程被换入或在内存中移动时,指令和数据单元的位置也发生变化。为了解决这个问题,需要区分几种地址类型: - 逻辑地址(logical address)是指与当前数据在内存中的物理分配地址无关的访问地址,在执行对内存的访问之前必须把它转换为物理地址。 -- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。 +- 相对地址(relative address)是逻辑地址的一个特例,他是相对于某些已知点(通常是程序的开始处)的存储单元。用户程序经过编译、汇编后形成目标代码,目标代码通常采用相对地址的形式,其首地址为0,其余地址相对于首地址而编址。 - 物理地址(physical address)或绝对地址是数据在内存中的实际位置。 系统采用运行时动态加载的方式把使用相对地址的程序加载到内存。通常情况下,被加载进程中所有内存访问都相对于程序的开始点。因此,在执行包括这类访问的指令时,需要有把相对地址转换为物理内存地址的硬件机制。这类地址转换就需要一个特殊的处理器寄存器(基址寄存器),其内容是程序在内存中的起始地址。还有一个界限寄存器指明程序的终止位置。 +将逻辑地址转换为物理地址的操作就叫做重定位。而把地址进行转换的功能部件就叫做内存管理单元(MMU, Memory Management Unit)。 + ### 分页 @@ -126,7 +153,7 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 ### 分段 -细分用户程序的另一种可选方案是分段。采用分段技术,可以把程序和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 +细分用户程序的另一种可选方案是分段。采用分段技术,可以把用户进程地址空间按程序的自身的逻辑关系和与其相关的数据划分到几个段(fragment)中。尽管段有最大长度限制,但并不要求所有程序的所有段的长度都相等。同样内存空间也会被动态的划分为若干长度不相同的取悦,称为物理段,每个物理段由起始地址和长度确定。和分页一样,采用分段技术时的逻辑地址也是由两部分组成:段号和偏移量。 以段为单位进行分配,每段在内存中占据连续空间,但各段之间可以不相邻。 由于使用大小不等的段,分段类似于动态分区。在未采用覆盖方案或使用虚存的情况下,为执行一个程序,需要把它的所有段都装入内存。与动态分区不同的是,在分段方案中,一个程序可以占据多个分区,并且这些分区不要求是连续的。分段消除了内部碎片,但是和动态分区一样,他会产生外部碎片。不过由于进程被分成多个小块,因此外部碎片也会很小。 @@ -154,12 +181,22 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 +### 段页式存储管理方案 + +综合页式、段式方案的优点,客服两者的缺点。用户进程先按段划分,每一段再按页面划分。内存分配还是以页为单位进行分配。 + + + + + ## 虚拟内存 面对越来越大的程序,常常产生程序>内存的问题,为解决这种问题,虚拟内存的概念得到普及. +虚拟内存是一种内存分配方案,是一项可以用来辅助内存分配的机制。我们知道,应用程序是按页装载进内存中的。但并不是所有的页都会装载到内存中,计算机中的硬件和软件会将数据从 RAM 临时传输到磁盘中来弥补内存的不足。如果没有虚拟内存的话,一旦你将计算机内存填满后,计算机会对你说呃,不,对不起,您无法再加载任何应用程序,请关闭另一个应用程序以加载新的应用程序。对于虚拟内存,计算机可以执行操作是查看内存中最近未使用过的区域,然后将其复制到硬盘上。虚拟内存通过复制技术实现了 妹子,你快来看哥哥能装这么多程序 的资本。复制是自动进行的,你无法感知到它的存在。 + 要有效的使用处理器和I/O设备,就需要在内存中保留尽可能多的进程。此外,还需要解除程序在开发时对程序使用内存大小的限制。解决这两个问题的途径就是虚拟内存技术。采用虚拟内存技术时,所有的地址访问都是逻辑访问,并在运行时转换为实地址。 虚拟内存机制使得期望运行大于物理内存的程序成为可能。其方法是将程序放在磁盘上,而将主存作为一种缓存,用来保存最频繁使用的部分程序。这种机制需要快速的映像内存地址,以便把程序生成的地址转换为有关字节在RAM中的物理地址。这种映像由CPU中的一个称为存储器管理单元(Memory Management Unit,MMU)的部件来完成。 @@ -211,7 +248,17 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 以便处理在必须读取一个新页时,应该置换内存中的哪一页。当内存中的所有页框都被占据,且需要读取一个新页以处理一次缺页中断时,置换策略决定置换当前内存中的哪一页。所有策略的目标都是移出最近最不能访问的页。 +- `最优算法`在当前页面中置换最后要访问的页面。不幸的是,没有办法来判定哪个页面是最后一个要访问的,`因此实际上该算法不能使用`。然而,它可以作为衡量其他算法的标准。 +- `NRU` 算法根据 R 位和 M 位的状态将页面氛围四类。从编号最小的类别中随机选择一个页面。NRU 算法易于实现,但是性能不是很好。存在更好的算法。 +- `FIFO` 会跟踪页面加载进入内存中的顺序,并把页面放入一个链表中。有可能删除存在时间最长但是还在使用的页面,因此这个算法也不是一个很好的选择。 +- `第二次机会`算法是对 FIFO 的一个修改,它会在删除页面之前检查这个页面是否仍在使用。如果页面正在使用,就会进行保留。这个改进大大提高了性能。 +- `时钟` 算法是第二次机会算法的另外一种实现形式,时钟算法和第二次算法的性能差不多,但是会花费更少的时间来执行算法。 +- `LRU` 算法是一个非常优秀的算法,但是没有`特殊的硬件(TLB)`很难实现。如果没有硬件,就不能使用 LRU 算法。 +- `NFU` 算法是一种近似于 LRU 的算法,它的性能不是非常好。 +- `老化` 算法是一种更接近 LRU 算法的实现,并且可以更好的实现,因此是一个很好的选择 +- 最后两种算法都使用了工作集算法。工作集算法提供了合理的性能开销,但是它的实现比较复杂。`WSClock` 是另外一种变体,它不仅能够提供良好的性能,而且可以高效地实现。 +总之,**「最好的算法是老化算法和WSClock算法」**。他们分别是基于 LRU 和工作集算法。他们都具有良好的性能并且能够被有效的实现。还存在其他一些好的算法,但实际上这两个可能是最重要的。 #### 4. 驻留集管理 @@ -237,6 +284,16 @@ Linux操作系统采用虚拟内存管理技术,使得每个进程都有各自 +## 内存映射 + +进程通过一个系统调用(mmap)将一个文件(或部分)映射到其虚拟地址空间的一部分,访问这个文件就像访问内存中的一个大数组,而不是对文件进行读写。 + +在多数实现中,在映射共享的页面时不会实际读入页面的内容,而是在访问页面时,页面才会被每次一页的读入(虚拟内存的缺页处理),磁盘文件则被当做后备存储。 + +当进程退出或显式地解除文件映射时,所有被修改页面会写回文件。 + + + ## Android内存管理 diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.I:O.md index 35535833..87a8fe44 100644 --- a/OperatingSystem/5.I:O.md +++ b/OperatingSystem/5.I:O.md @@ -29,6 +29,25 @@ +## I/O进程 + +I/O进程是专门处理系统中的I/O请求和I/O中断工作的。是系统进程,一般赋予最高优先级。一旦被唤醒,它可以很快抢占处理机投入运行。当I/O进程开始运行后,首先关闭中断,然后用receive去接收消息。如果没有消息,则开中断将自己堵塞,如果有消息,就去判断消息的类型是I/O请求还是I/O中断,然后再分别处理。 + +- I/O请求的进入 + + - 用户程序:调用send将I/O请求发送给I/O进程。调用block将自己堵塞,直到I/O任务完成后被唤醒。 + - 系统:利用wakeup唤醒I/O进程,完成用户所要求的I/O处理。 + +- I/O中断的进入 + + 当I/O中断发生时,内核中的中断处理程序发送一条消息给I/O进程,由I/O进程负责判断并处理中断。 + + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/io_intercept_os.png?raw=true) + + 当一个 I/O 设备完成它的工作后,它就会产生一个中断(默认操作系统已经开启中断),它通过在总线上声明已分配的信号来实现此目的。主板上的中断控制器芯片会检测到这个信号,然后执行中断操作。 + +​ + ### 磁盘高速缓存 @@ -46,6 +65,58 @@ + + + + + + +每个设备控制器都会有一个应用程序与之对应,设备控制器通过应用程序的接口通过中断与操作系统进行通信。设备控制器是硬件,而设备驱动程序是软件。 + +### 内存映射 I/O + +每个控制器都会有几个寄存器用来和 CPU 进行通信。通过写入这些寄存器,操作系统可以命令设备发送数据,接收数据、开启或者关闭设备等。通过从这些寄存器中读取信息,操作系统能够知道设备的状态,是否准备接受一个新命令等。 + +为了控制`寄存器`,许多设备都会有`数据缓冲区(data buffer)`,来供系统进行读写。 + +那么问题来了,CPU 如何与设备寄存器和设备数据缓冲区进行通信呢?存在两个可选的方式。第一种方法是,每个控制寄存器都被分配一个 `I/O 端口(I/O port)`号,这是一个 8 位或 16 位的整数。所有 I/O 端口的集合形成了受保护的 I/O 端口空间,以便普通用户程序无法访问它(只有操作系统可以访问)。使用特殊的 I/O 指令像是 + +- + +``` +IN REG,PORT +``` + +CPU 可以读取控制寄存器 PORT 的内容并将结果放在 CPU 寄存器 REG 中。类似的,使用 + +- + +``` +OUT PORT,REG +``` + +CPU 可以将 REG 的内容写到控制寄存器中。大多数早期计算机,包括几乎所有大型主机,如 IBM 360 及其所有后续机型,都是以这种方式工作的。 + +第二个方法是 PDP-11 引入的,它将**「所有控制寄存器映射到内存空间」**中。 + +### 直接内存访问 + +无论一个 CPU 是否具有内存映射 I/O,它都需要寻址设备控制器以便与它们交换数据。CPU 可以从 I/O 控制器每次请求一个字节的数据,但是这么做会浪费 CPU 时间,所以经常会用到一种称为直接内存访问(Direct Memory Access) 的方案。它意味着 CPU 授予 I/O 模块权限在不涉及 CPU 的情况下读取或写入内存。也就是 DMA 可以不需要 CPU 的参与。这个过程由称为 DMA 控制器(DMAC)的芯片管理。由于 DMA 设备可以直接在内存之间传输数据,而不是使用 CPU 作为中介,因此可以缓解总线上的拥塞。DMA 通过允许 CPU 执行任务,同时 DMA 系统通过系统和内存总线传输数据来提高系统并发性。为了简化,我们假设 CPU 通过单一的系统总线访问所有的设备和内存,该总线连接 CPU 、内存和 I/O 设备,如下图所示 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/bus_os.png?raw=true) + +#### DMA 工作原理 + +首先 CPU 通过设置 DMA 控制器的寄存器对它进行编程,所以 DMA 控制器知道将什么数据传送到什么地方。DMA 控制器还要向磁盘控制器发出一个命令,通知它从磁盘读数据到其内部的缓冲区并检验校验和。当有效数据位于磁盘控制器的缓冲区中时,DMA 就可以开始了。 + +DMA 控制器通过在总线上发出一个`读请求`到磁盘控制器而发起 DMA 传送,这是第二步。这个读请求就像其他读请求一样,磁盘控制器并不知道或者并不关心它是来自 CPU 还是来自 DMA 控制器。通常情况下,要写的内存地址在总线的地址线上,所以当磁盘控制器去匹配下一个字时,它知道将该字写到什么地方。写到内存就是另外一个总线循环了,这是第三步。当写操作完成时,磁盘控制器在总线上发出一个应答信号到 DMA 控制器,这是第四步。 + +然后,DMA 控制器会增加内存地址并减少字节数量。如果字节数量仍然大于 0 ,就会循环步骤 2 - 步骤 4 ,直到字节计数变为 0 。此时,DMA 控制器会打断 CPU 并告诉它传输已经完成了。 + + + + + --- diff --git "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" index 4940fb5d..e6908122 100644 --- "a/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" +++ "b/OperatingSystem/6.\346\226\207\344\273\266\347\256\241\347\220\206.md" @@ -1,6 +1,8 @@ # 6.文件管理 +文件是对磁盘的抽象。 +所谓未见是指一组带标识(标识即为文件名)的、在逻辑上有完整意义的信息项的序列。 文件管理系统是一组系统软件,它为使用文件的用户和应用程序提供服务,包括文件访问、目录维护和访问控制。文件管理系统通常被视为一个由操作系统提供服务的系统服务,而不是操作系统的一部分,但是在任何系统中,至少有一部分文件管理功能是由操作系统执行的。 @@ -54,6 +56,78 @@ Android文件系统目录的顶层部分: +### 文件系统布局 + +文件系统存储在`磁盘`中。大部分的磁盘能够划分出一到多个分区,叫做`磁盘分区(disk partitioning)` 或者是`磁盘分片(disk slicing)`。每个分区都有独立的文件系统,每块分区的文件系统可以不同。磁盘的 0 号分区称为 `主引导记录(Master Boot Record, MBR)`,用来`引导(boot)` 计算机。在 MBR 的结尾是`分区表(partition table)`。每个分区表给出每个分区由开始到结束的地址。 + +当计算机开始引 boot 时,BIOS 读入并执行 MBR。 + +#### 引导块 + +MBR 做的第一件事就是`确定活动分区`,读入它的第一个块,称为`引导块(boot block)` 并执行。引导块中的程序将加载分区中的操作系统。为了一致性,每个分区都会从引导块开始,即使引导块不包含操作系统。引导块占据文件系统的前 4096 个字节,从磁盘上的字节偏移量 0 开始。引导块可用于启动操作系统。 + +除了从引导块开始之外,磁盘分区的布局是随着文件系统的不同而变化的。通常文件系统会包含一些属性,如下 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/file_system_os.png?raw=true) + +#### 超级块 + +紧跟在引导块后面的是 `超级块(Superblock)`,超级块 的大小为 4096 字节,从磁盘上的字节偏移 4096 开始。超级块包含文件系统的所有关键参数 + +- 文件系统的大小 +- 文件系统中的数据块数 +- 指示文件系统状态的标志 +- 分配组大小 + +在计算机启动或者文件系统首次使用时,超级块会被读入内存。 + +#### 空闲空间块 + +接着是文件系统中`空闲块`的信息,例如,可以用位图或者指针列表的形式给出。 + +#### 碎片 + +这里不得不提一个叫做`碎片(fragment)`的概念,也称为片段。一般零散的单个数据通常称为片段。磁盘块可以进一步分为固定大小的分配单元,片段只是在驱动器上彼此不相邻的文件片段。 + +#### inode + +然后在后面是一个 `inode(index node)`,也称作索引节点。它是一个数组的结构,每个文件有一个 inode,inode 非常重要,它说明了文件的方方面面。每个索引节点都存储对象数据的属性和磁盘块位置。 + +inode 节点主要包括了以下信息 + +- 模式/权限(保护) +- 所有者 ID +- 组 ID +- 文件大小 +- 文件的硬链接数 +- 上次访问时间 +- 最后修改时间 +- inode 上次修改时间 + +文件分为两部分,索引节点和块。一旦创建后,每种类型的块数是固定的。你不能增加分区上 inode 的数量,也不能增加磁盘块的数量。 + +紧跟在 inode 后面的是根目录,它存放的是文件系统目录树的根部。最后,磁盘的其他部分存放了其他所有的目录和文件。 + +### 文件的实现 + +最重要的问题是记录各个文件分别用到了哪些磁盘块。不同的系统采用了不同的方法。下面我们会探讨一下这些方式。分配背后的主要思想是`有效利用文件空间`和`快速访问文件` ,主要有三种分配方案 + +- 连续分配 +- 链表分配 +- 索引分配 + + + + + + + + + +- [参考: ](https://mp.weixin.qq.com/s/oLvTFWibCH53Fv5lj4mTnQ) + + + --- diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 2f089353..11a00bf6 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -108,6 +108,8 @@ copy_to_user() //将数据从内核空间拷贝到用户空间 那就是让操作系统内核添加支持,传统的linux通信机制,比如socket、管道等都是内核的一部分,因此通过内核支持来实现进程间通信自然是没有问题的,但是binder并不是linux系统内核的一部分,那它是怎么做到访问内核空间的呢?这就得益于 Linux 的**动态内核可加载模块**(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。 > 在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 **Binder 驱动**(Binder Dirver)(尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的)。 +> +> 从进程角度来看IPC机制,每个Android的进程,只能运行在自己进程所拥有的虚拟地址空间。对应一个4GB的虚拟地址空间,其中3GB是用户空间,1GB是内核空间,当然内核空间的大小是可以通过参数配置调整的。对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。 @@ -261,6 +263,8 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork - ServiceManager + ServiceManager是Binder IPC通信过程中的守护进程,本身也是一个Binder服务,但并没有采用libbinder中的多线程模型来与Binder驱动通信,而是自行编写了binder.c直接和Binder驱动来通信,并且只有一个循环binder_loop来进行读取和处理事务,这样的好处是简单而高效。ServiceManager是由init进程通过解析init.rc文件而创建的。 + ServiceManager本身的工作很简单:注册服务、查询服务、列出所有服务,启动一个死循环来解析Binder驱动读写动作,进行事务处理。ServiceManager用于管理系统中的各种服务。架构图如下所示: ![ServiceManager](http://gityuan.com/images/binder/prepare/IPC-Binder.jpg) @@ -276,10 +280,24 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_client_server.png) - + + +ServiceManger集中管理系统内的所有服务,通过权限控制进程是否有权注册服务,通过字符串名称来查找对应的Service; 由于ServiceManger进程建立跟所有向其注册服务的死亡通知, 那么当服务所在进程死亡后, 会只需告知ServiceManager. 每个Client通过查询ServiceManager可获取Server进程的情况,降低所有Client进程直接检测会导致负载过重。 +**ServiceManager启动流程:** +1. 打开binder驱动,并调用mmap()方法分配128k的内存映射空间:binder_open(); +2. 通知binder驱动使其成为守护进程:binder_become_context_manager(); +3. 验证selinux权限,判断进程是否有权注册或查看指定服务; +4. 进入循环状态,等待Client端的请求:binder_loop()。 +5. 注册服务的过程,根据服务名称,但同一个服务已注册,重新注册前会先移除之前的注册信息; +6. 死亡通知: 当binder所在进程死亡后,会调用binder_release方法,然后调用binder_node_release.这个过程便会发出死亡通知的回调. + +ServiceManager最核心的两个功能为查询和注册服务: + +- 注册服务:记录服务名和handle信息,保存到svclist列表; +- 查询服务:根据服务名查询相应的的handle信息。 ## Binder 通信过程 @@ -348,7 +366,39 @@ ARM64内存占用分配: ![img](https://img-blog.csdnimg.cn/20200329164637770.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lpcmFuZmVuZw==,size_16,color_FFFFFF,t_70) -用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间。 +用户地址空间(服务端-数据接收端)和内核地址空间都映射到同一块物理地址空间,这也是Binder进程间通信效率高的核心机制所在。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_physical_memory.jpg?raw=true) + +虚拟进程地址空间(vm_area_struct)和虚拟内核地址空间(vm_struct)都映射到同一块物理内存空间。当Client端与Server端发送数据时,Client(作为数据发送端)先从自己的进程空间把IPC通信数据`copy_from_user`拷贝到内核空间,而Server端(作为数据接收端)与内核共享数据,不再需要拷贝数据,而是通过内存地址空间的偏移量,即可获悉内存地址,整个过程只发生一次内存拷贝。一般地做法,需要Client端进程空间拷贝到内核空间,再由内核空间拷贝到Server进程空间,会发生两次拷贝。 + +对于进程和内核虚拟地址映射到同一个物理内存的操作是发生在数据接收端,而数据发送端还是需要将用户态的数据复制到内核态。到此,可能有读者会好奇,为何不直接让发送端和接收端直接映射到同一个物理空间,那样就连一次复制的操作都不需要了,0次复制操作那就与Linux标准内核的共享内存的IPC机制没有区别了,对于共享内存虽然效率高,但是对于多进程的同步问题比较复杂,而管道/消息队列等IPC需要复制2两次,效率较低。这里就不先展开讨论Linux现有的各种IPC机制跟Binder的详细对比,总之Android选择Binder的基于速度和安全性的考虑。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_memory_map.jpg?raw=true) + + + +### Binder驱动 + +Binder驱动是Android专用的,但底层的驱动架构与Linux驱动一样。binder驱动在以misc设备进行注册,作为虚拟字符设备,没有直接操作硬件,只是对设备内存的处理。主要是驱动设备的初始化(binder_init),打开 (binder_open),映射(binder_mmap),数据操作(binder_ioctl),binder_ioctl()函数负责在两个进程间收发IPC数据和IPC reply数据。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_driver.png?raw=true) + +### 系统调用 + +用户态的程序调用Kernel层驱动是需要陷入内核态,进行系统调用(`syscall`),比如打开Binder驱动方法的调用链为: open-> __open() -> binder_open()。 open()为用户空间的方法,__open()便是系统调用中相应的处理方法,通过查找,对应调用到内核binder驱动的binder_open()方法,至于其他的从用户态陷入内核态的流程也基本一致。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_syscall.png?raw=true) + +简单说,当用户空间调用open()方法,最终会调用binder驱动的binder_open()方法;mmap()/ioctl()方法也是同理,在BInder系列的后续文章从用户态进入内核态,都依赖于系统调用过程。 + + + + + +在Android系统开机过程中,Zygote启动时会有一个[虚拟机注册过程](http://gityuan.com/2016/02/13/android-zygote/#jnistartreg),该过程调用AndroidRuntime::`startReg`方法来完成jni方法的注册。 + + diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 7af53a77..4e733af8 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -5,86 +5,81 @@ 基本概念 --- -- ***媒体***:是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 +- 媒体 -- ***多媒体***:是声音、动画、文字、图像和录像等各种媒体的组合,以图文并茂,生动活泼的动态形式表现出来,给人以很强的视觉冲击力,留下深刻印象. + 是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 -- ***多媒体技术***:是将文字、声音、图形、静态图像、动态图像与计算集成在一起的技术。它要解决的问题是计算机进一步帮助人类按最自然的和最习惯的方式接受和处理信息。 +- 多媒体 -- ***流媒体***:流媒体是指采用流式传输的方式在`Internet`播放的连续时基媒体格式,实际指的是一种新的媒体传送方式,而不是一种新的媒体格式(在网络上传输音/视频等多媒体信息现在主要有下载和流式传输两种方式)流式传输分两种方法:实时流式传输方式(`Realtime Streaming`)和顺序流式传输方式(`Progressive Streaming`)。 + 是声音、动画、文字、图像和录像等各种媒体的组合,以图文并茂,生动活泼的动态形式表现出来,给人以很强的视觉冲击力,留下深刻印象. -- ***多媒体文件***:是既包括视频又包括音频,甚至还带有脚本的一个集合,也可以叫容器。 +- 多媒体技术 -- ***媒体编码***:是文件当中的视频和音频所采用的压缩算法。也就是说一个`avi`的文件,当中的视频编码有可能是`A`,也可能是`B`,而其音频编码有可能是`1`,也有可能是`2`。 + 是将文字、声音、图形、静态图像、动态图像与计算集成在一起的技术。它要解决的问题是计算机进一步帮助人类按最自然的和最习惯的方式接受和处理信息。 -- ***转码***:指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式。 +- 流媒体 -- ***帧***:帧就是一段数据的组合,它是数据传输的基本单位。就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。一帧就是一副静止的画面,连续的帧就形成动画,如电视图像等。 + 流媒体是指采用流式传输的方式在`Internet`播放的连续时基媒体格式,实际指的是一种新的媒体传送方式,而不是一种新的媒体格式(在网络上传输音/视频等多媒体信息现在主要有下载和流式传输两种方式)流式传输分两种方法:实时流式传输方式(`Realtime Streaming`)和顺序流式传输方式(`Progressive Streaming`)。 -- ***视频***:连续的图象变化每秒超过24帧(`Frame`)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面,看上去是平滑连续的视觉效果,这样连续的画面叫做视频. +- 多媒体文件 -- ***音频***:人类能听到的声音都成为音频,但是一般我们所说到的音频时存储在计算机里的声音。 + 既包括视频又包括音频,甚至还带有脚本的一个集合,也可以叫容器。 -- ***比特率(码率)***:码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 - - 动态码率(VBR: Variable Bit Rate) +- 媒体编码 - 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 + 是文件当中的视频和音频所采用的压缩算法。也就是说一个`avi`的文件,当中的视频编码有可能是`A`,也可能是`B`,而其音频编码有可能是`1`,也有可能是`2`。 - 内容复杂的片段采用较大的码率,这样既保证了播放质量,又兼顾了数据量的限制。例如RMVB视频文件,其中的VB就是指VBR,表示采用动态比特率编码方式,达到播放质量与体积兼得的效果。 +- 转码 - - 静态比特率(CBR: Constant Bit Rate) + 指将一段多媒体包括音频、视频或者其他的内容从一种编码格式转换成为另外一种编码格式。 - 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 - -- ***帧率(Frame Rate)***:帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 - ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) +- 帧 -- ***关键帧***:相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 + 帧就是一段数据的组合,它是数据传输的基本单位。就是影像动画中最小单位的单幅影像画面,相当于电影胶片上的每一格镜头。一帧就是一副静止的画面,连续的帧就形成动画,如电视图像等。 +- 视频 + 连续的图象变化每秒超过24帧(`Frame`)画面以上时,根据视觉暂留原理,人眼无法辨别单幅的静态画面,看上去是平滑连续的视觉效果,这样连续的画面叫做视频. +- 音频 -视频格式(封装格式/容器) ---- - + 人类能听到的声音都成为音频,但是一般我们所说到的音频时存储在计算机里的声音。 +- 比特率(码率) + +码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 + +- 动态码率(VBR: Variable Bit Rate) + + 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 + + 内容复杂的片段采用较大的码率,这样既保证了播放质量,又兼顾了数据量的限制。例如RMVB视频文件,其中的VB就是指VBR,表示采用动态比特率编码方式,达到播放质量与体积兼得的效果。 + +- 静态比特率(CBR: Constant Bit Rate) + + 比特率恒定,图像内容复杂的片段质量不稳定,图像内容简单的片段质量较好。 + +- 帧率(Frame Rate) + +帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 + ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) + +- 关键帧 -目前我们经常见的视频格式无非就是两大类: - -1. 影像格式(Video) -2. 流媒体格式(Stream Video) - -在影像格式中还可以根据出处划分为三大种: + 相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 -1. `AVI`格式 - `AVI`英文全称为`Audio Video Interleaved`,即音频视频交错格式,是微软公司于1992年11月推出、作为其`Windows`视频软件一部分的一种多媒体容器格式。 - `AVI`文件将音频(语音)和视频(影像)数据包含在一个文件容器中,允许音视频同步回放。类似`DVD`视频格式,`AVI`文件支持多个音视频流。`AVI`信息主要应用在多媒体光盘上,用来保存电视、电影等各种影像信息。 - 其中数据块包含实际数据流,即图像和声音序列数据。这是文件的主体,也是决定文件容量的主要部分。视频文件的大小等于该文件的数据率乘以该视频播放的时间长度, - 索引块包括数据块列表和它们在文件中的位置,以提供文件内数据随机存取能力。文件头包括文件的通用信息,定义数据格式,所用的压缩算法等参数。 - `AVI`没有`MPEG`这么复杂,从`Windows 3.1`时代,它就已经面世了。它最直接的优点就是兼容好、调用方便而且图象质量好,因此也常常与`DVD`相并称。 - 但它的缺点也是十分明显的:体积大。也是因为这一点,我们才看到了`MPEG-1`和`MPEG-4`的诞生。2小时影像的`AVI`文件的体积与`MPEG-2`相差无几, - 不过这只是针对标准分辨率而言的:根据不同的应用要求,`AVI`的分辨率可以随意调。窗口越大,文件的数据量也就越大。降低分辨率可以大幅减低它的体积, - 但图象质量就必然受损。与`MPEG-2`格式文件体积差不多的情况下,`AVI`格式的视频质量相对而言要差不少,但制作起来对电脑的配置要求不高,经常有人先录制好了`AVI`格式的视频,再转换为其他格式。 -2. `MOV`格式 - `MOV`即`QuickTime`影片格式,它是`Apple`公司开发的一种音频、视频文件格式,用于存储常用数字媒体类型。当选择`QuickTime(*.mov)`作为“保存类型”时,动画将保存为`·mov`文件。`QuickTime`用于保存音频和视频信息,包括`Apple Mac OS,MicrosoftWindows95/98/NT/2003/XP/VISTA`,甚至`WINDOWS7`在内的所有主流电脑平台支持。`QuickTime`因具有跨平台、存储空间要求小等技术特点,而采用了有损压缩方式的`MOV`格式文件,画面效果较AVI格式要稍微好一些。到目前为止,它共有4个版本,其中以`4.0`版本的压缩率最好。这种编码支持16位图像深度的帧内压缩和帧间压缩,帧率每秒10帧以上。这种格式有些非编软件也可以对它实行处理,其中包括`ADOBE`公司的专业级多媒体视频处理软件`AFTEREFFECTS和PREMIERE`。 +- 刷新率 -3. `MPEG/MPG/DAT`: - 这是由国际标准化组织`ISO(International Standards Organization)`与`IEC(International Electronic Committee)`联合开发的一种编码视频格式。`MPEG`是运动图像压缩算法的国际标准,现已被几乎所有的计算机平台共同支持。MPEG也是`Motion Picture Experts Group`的缩写。这类格式包括了`MPEG-1, MPEG-2`和`MPEG-4`在内的多种视频格式。`MPEG-1`相信是大家接触得最多的了,因为目前其正在被广泛地应用在`VCD`的制作和一些视频片段下载的网络应用上面,大部分的`VCD`都是用 `MPEG1`格式压缩的( 刻录软件自动将`MPEG1`转为`.DAT`格式),使用`MPEG-1`的压缩算法,可以把一部120分钟长的电影压缩到1.2GB左右大小。`MPEG-2`则是应用在`DVD` 的制作,同时在一些`HDTV`(高清晰电视广播)和一些高要求视频编辑、处理上面也有相当多的应用。使用`MPEG-2`的压缩算法压缩一部120分钟长的电影可以压缩到5-8GB的大小(`MPEG2`的图像质量`MPEG-1`与其无法比拟的)。 + 刷新率是指屏幕每秒画面被刷新的次数,刷新率分为垂直刷新率和水平刷新率,一般提到的刷新率通常是指垂直刷新率。垂直刷新率表示屏幕上图像每秒重绘多少次,也就是每秒屏幕刷新的次数,以Hz(赫兹)为单位。刷新率越高,图像就越稳定,图像显示就越自然清晰,对眼镜的影响也越小。刷新率月底,图像闪烁和抖动的就越厉害,眼镜疲劳的越快。一般来说,如果能达到80Hz以上的刷新率,就可以完全消除图像闪烁和抖动感。 -在流媒体格式中同样还可以划分为三种: +- DTS -1. `RM`格式 - `Real Networks`公司所制定的音频/视频压缩规范`Real Media`中的一种,`Real Player`能做的就是利用`Internet`资源对这些符合`Real Media`技术规范的音频/视频进行实况转播。在`Real Media`规范中主要包括三类文件:`RealAudio`、`Real Video`和`Real Flash`(`Real Networks`公司与`Macromedia`公司合作推出的新一代高压缩比动画格式)。`REAL VIDEO`(RA、RAM)格式由一开始就是定位就是在视频流应用方面的,也可以说是视频流技术的始创者。它可以在用56K`MODEM`拨号上网的条件实现不间断的视频播放,从`RealVideo`的定位来看,就是牺牲画面质量来换取可连续观看性。其实`RealVideo`也可以实现不错的画面质量,由于`RealVideo`可以拥有非常高的压缩效率,很多人把`VCD`编码成`RealVideo`格式的,这样一来,一张光盘上可以存放好几部电影。`REAL VIDEO`存在颜色还原不准确的问题,`RealVideo`就不太适合专业的场合,但`RealVideo`出色的压缩效率和支持流式播放的特征,使得`RealVideo`在网络和娱乐场合占有不错的市场份额。 -2. `MOV/QT`格式 - `MOV`也可以作为一种流文件格式。`QuickTime`能够通过`Internet`提供实时的数字化信息流、工作流与文件回放功能,为了适应这一网络多媒体应用,`QuickTime`为多种流行的浏览器软件提供了相应的`QuickTime Viewer`插件,能够在浏览器中实现多媒体数据的实时回放。 -3. `ASF`格式 - `ASF`(`Advanced Streaming format`高级流格式)。`ASF`是`MICROSOFT`为了和现在的`Real player`竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。`ASF`使用了`MPEG4`的压缩算法,压缩率和图像的质量都很不错。因为`ASF`是以一个可以在网上即时观赏的视频“流”格式存在的,所以它的图像质量比`VCD`差一点点并不出奇,但比同是视频“流”格式的`RAM`格式要好。 `ASF`支持任意的压缩/解压缩编码方式,并可以使用任何一种底层网络传输协议,具有很大的灵活性。`ASF`流文件的数据速率可以在`28.8Kbps`到`3Mbps`之间变化。用户可以根据自己应用环境和网络条件选择一个合适的速率,实现`VOD`点播和直播。 + Decode Time Stamp,主要用于标识读入内存中的比特流在什么时候开始送入解码器中进行解码。 -4. `FLV`格式`FLV`是`FLASH VIDEO`的简称,一种流媒体封装格式,`FLV`流媒体格式是随着`Flash MX`的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入`Flash`后,使导出的`SWF`文件体积庞大,不能在网络上很好的使用等问题。因此`FLV`格式成为了当今主流视频格式 +- PTS + Presentation Time Stamp,主要用于度量解码后的视频帧什么时候被显示出来。 -叫做容器很好理解,mp4就是一个容器,里面有moov文件表示文件的信息等,还有音频、画面等信息,他们统一到 -一起,放到一个容器里面,就组成了视频。如果觉得难以理解,可以想象成一瓶番茄酱。最外层的瓶子好比这个容器封装(Container),瓶子上注明的原材料和加工厂地等信息好比元信息(Metadata),瓶盖打开(解封装)后,番茄酱本身好比经过压缩处理过后的编码内容,番茄和调料加工成番茄酱的过程就好比编码(Codec),而原材料番茄和调料则好比最原本的内容元素(Content)。 音视频压缩编码标准 @@ -142,6 +137,53 @@ - `MP3`:(`MPEG-1 or MPEG-2 Audio Layer III`),是当曾经非常流行的一种数字音频编码和有损压缩格式,它被设计来大幅降低音频数据量。它是在1991 年,由位于德国埃尔朗根的研究组织`Fraunhofer-Gesellschaft`的一组工程师发明和标准化的。`MP3`的普及曾对音乐产业造成极大的冲击与影响。 - `WMA`:(`Windows Media Audio`)由微软公司开发的一种数字音频压缩格式,本身包括有损和无损压缩格式。 +--- + + + + +视频格式(封装格式/容器) +--- + +把编码后的音视频数据以一定格式封装到一个容器。 + +目前我们经常见的视频格式无非就是两大类: + +1. 影像格式(Video) +2. 流媒体格式(Stream Video) + +在影像格式中还可以根据出处划分为三大种: + +1. `AVI`格式 + `AVI`英文全称为`Audio Video Interleaved`,即音频视频交错格式,是微软公司于1992年11月推出、作为其`Windows`视频软件一部分的一种多媒体容器格式。 + `AVI`文件将音频(语音)和视频(影像)数据包含在一个文件容器中,允许音视频同步回放。类似`DVD`视频格式,`AVI`文件支持多个音视频流。`AVI`信息主要应用在多媒体光盘上,用来保存电视、电影等各种影像信息。 + 其中数据块包含实际数据流,即图像和声音序列数据。这是文件的主体,也是决定文件容量的主要部分。视频文件的大小等于该文件的数据率乘以该视频播放的时间长度, + 索引块包括数据块列表和它们在文件中的位置,以提供文件内数据随机存取能力。文件头包括文件的通用信息,定义数据格式,所用的压缩算法等参数。 + `AVI`没有`MPEG`这么复杂,从`Windows 3.1`时代,它就已经面世了。它最直接的优点就是兼容好、调用方便而且图象质量好,因此也常常与`DVD`相并称。 + 但它的缺点也是十分明显的:体积大。也是因为这一点,我们才看到了`MPEG-1`和`MPEG-4`的诞生。2小时影像的`AVI`文件的体积与`MPEG-2`相差无几, + 不过这只是针对标准分辨率而言的:根据不同的应用要求,`AVI`的分辨率可以随意调。窗口越大,文件的数据量也就越大。降低分辨率可以大幅减低它的体积, + 但图象质量就必然受损。与`MPEG-2`格式文件体积差不多的情况下,`AVI`格式的视频质量相对而言要差不少,但制作起来对电脑的配置要求不高,经常有人先录制好了`AVI`格式的视频,再转换为其他格式。 +2. `MOV`格式 + `MOV`即`QuickTime`影片格式,它是`Apple`公司开发的一种音频、视频文件格式,用于存储常用数字媒体类型。当选择`QuickTime(*.mov)`作为“保存类型”时,动画将保存为`·mov`文件。`QuickTime`用于保存音频和视频信息,包括`Apple Mac OS,MicrosoftWindows95/98/NT/2003/XP/VISTA`,甚至`WINDOWS7`在内的所有主流电脑平台支持。`QuickTime`因具有跨平台、存储空间要求小等技术特点,而采用了有损压缩方式的`MOV`格式文件,画面效果较AVI格式要稍微好一些。到目前为止,它共有4个版本,其中以`4.0`版本的压缩率最好。这种编码支持16位图像深度的帧内压缩和帧间压缩,帧率每秒10帧以上。这种格式有些非编软件也可以对它实行处理,其中包括`ADOBE`公司的专业级多媒体视频处理软件`AFTEREFFECTS和PREMIERE`。 + +3. `MPEG/MPG/DAT`: + 这是由国际标准化组织`ISO(International Standards Organization)`与`IEC(International Electronic Committee)`联合开发的一种编码视频格式。`MPEG`是运动图像压缩算法的国际标准,现已被几乎所有的计算机平台共同支持。MPEG也是`Motion Picture Experts Group`的缩写。这类格式包括了`MPEG-1, MPEG-2`和`MPEG-4`在内的多种视频格式。`MPEG-1`相信是大家接触得最多的了,因为目前其正在被广泛地应用在`VCD`的制作和一些视频片段下载的网络应用上面,大部分的`VCD`都是用 `MPEG1`格式压缩的( 刻录软件自动将`MPEG1`转为`.DAT`格式),使用`MPEG-1`的压缩算法,可以把一部120分钟长的电影压缩到1.2GB左右大小。`MPEG-2`则是应用在`DVD` 的制作,同时在一些`HDTV`(高清晰电视广播)和一些高要求视频编辑、处理上面也有相当多的应用。使用`MPEG-2`的压缩算法压缩一部120分钟长的电影可以压缩到5-8GB的大小(`MPEG2`的图像质量`MPEG-1`与其无法比拟的)。 + +在流媒体格式中同样还可以划分为三种: + +1. `RM`格式 + `Real Networks`公司所制定的音频/视频压缩规范`Real Media`中的一种,`Real Player`能做的就是利用`Internet`资源对这些符合`Real Media`技术规范的音频/视频进行实况转播。在`Real Media`规范中主要包括三类文件:`RealAudio`、`Real Video`和`Real Flash`(`Real Networks`公司与`Macromedia`公司合作推出的新一代高压缩比动画格式)。`REAL VIDEO`(RA、RAM)格式由一开始就是定位就是在视频流应用方面的,也可以说是视频流技术的始创者。它可以在用56K`MODEM`拨号上网的条件实现不间断的视频播放,从`RealVideo`的定位来看,就是牺牲画面质量来换取可连续观看性。其实`RealVideo`也可以实现不错的画面质量,由于`RealVideo`可以拥有非常高的压缩效率,很多人把`VCD`编码成`RealVideo`格式的,这样一来,一张光盘上可以存放好几部电影。`REAL VIDEO`存在颜色还原不准确的问题,`RealVideo`就不太适合专业的场合,但`RealVideo`出色的压缩效率和支持流式播放的特征,使得`RealVideo`在网络和娱乐场合占有不错的市场份额。 +2. `MOV/QT`格式 + `MOV`也可以作为一种流文件格式。`QuickTime`能够通过`Internet`提供实时的数字化信息流、工作流与文件回放功能,为了适应这一网络多媒体应用,`QuickTime`为多种流行的浏览器软件提供了相应的`QuickTime Viewer`插件,能够在浏览器中实现多媒体数据的实时回放。 +3. `ASF`格式 + `ASF`(`Advanced Streaming format`高级流格式)。`ASF`是`MICROSOFT`为了和现在的`Real player`竞争而发展出来的一种可以直接在网上观看视频节目的文件压缩格式。`ASF`使用了`MPEG4`的压缩算法,压缩率和图像的质量都很不错。因为`ASF`是以一个可以在网上即时观赏的视频“流”格式存在的,所以它的图像质量比`VCD`差一点点并不出奇,但比同是视频“流”格式的`RAM`格式要好。 `ASF`支持任意的压缩/解压缩编码方式,并可以使用任何一种底层网络传输协议,具有很大的灵活性。`ASF`流文件的数据速率可以在`28.8Kbps`到`3Mbps`之间变化。用户可以根据自己应用环境和网络条件选择一个合适的速率,实现`VOD`点播和直播。 + +4. `FLV`格式`FLV`是`FLASH VIDEO`的简称,一种流媒体封装格式,`FLV`流媒体格式是随着`Flash MX`的推出发展而来的视频格式。由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,它的出现有效地解决了视频文件导入`Flash`后,使导出的`SWF`文件体积庞大,不能在网络上很好的使用等问题。因此`FLV`格式成为了当今主流视频格式 + + +叫做容器很好理解,mp4就是一个容器,里面有moov文件表示文件的信息等,还有音频、画面等信息,他们统一到 +一起,放到一个容器里面,就组成了视频。如果觉得难以理解,可以想象成一瓶番茄酱。最外层的瓶子好比这个容器封装(Container),瓶子上注明的原材料和加工厂地等信息好比元信息(Metadata),瓶盖打开(解封装)后,番茄酱本身好比经过压缩处理过后的编码内容,番茄和调料加工成番茄酱的过程就好比编码(Codec),而原材料番茄和调料则好比最原本的内容元素(Content)。 + 一些音视频的参数含义 --- diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" new file mode 100644 index 00000000..405341cd --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/2.\347\263\273\347\273\237\346\222\255\346\224\276\345\231\250MediaPlayer.md" @@ -0,0 +1,30 @@ +2.系统播放器MediaPlayer +=== + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mediaplayer_state.png?raw=true) + + + + + + + + + + + + + + +相关知识: + +- [封装格式](https://en.wikipedia.org/wiki/Comparison_of_video_container_formats) +- [视频编码方式](https://en.wikipedia.org/wiki/Comparison_of_video_codecs) +- [音频编码方式](https://en.wikipedia.org/wiki/Comparison_of_audio_coding_formats) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From 02d97cc227dea999e4d81d7a44debf14a4c501c5 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 18 Feb 2021 15:39:35 +0800 Subject: [PATCH 014/183] update --- ...13\344\270\216\347\272\277\347\250\213.md" | 4 +- ...13\351\227\264\351\200\232\344\277\241.md" | 41 +++++++++++++++++ ...30\345\210\266\345\237\272\347\241\200.md" | 44 +++++++++++++++++++ ...ManagerService\347\256\200\344\273\213.md" | 4 ++ ...57\345\212\250\350\277\207\347\250\213.md" | 10 ++++- 5 files changed, 99 insertions(+), 4 deletions(-) diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index c96b8f66..9801b918 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -293,7 +293,7 @@ 使用这个算法,调度程序总是选择剩余运行时间最短的那个进程运行。 - 轮转法 - + 一种最古老、最简单、最公平并且最广泛使用的算法就是轮询算法(round-robin)。每个进程都会被分配一个时间段,称为时间片(quantum),在这个时间片内允许进程运行。如果时间片结束时进程还在运行的话,则抢占一个 CPU 并将其分配给另一个进程。如果进程在时间片结束前阻塞或结束,则 CPU 立即进行切换。轮询算法比较容易实现。调度程序所做的就是维护一个可运行进程的列表,就像下图中的 a,当一个进程用完时间片后就被移到队列的末尾。 - 简单轮转: 就绪进程按FIFO排队,按照一定时间间隔让处理机分配给队列中的进程,就绪队列中所有队列均可获得一个时间片的处理器运行。 @@ -402,7 +402,7 @@ Linux为进程间通信和同步提供了各种机制。这里只是几种。 ## Android进程结构 -如同传统的Linux系统一样,Android的第一个用户空间进程是init,它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 +如同传统的Linux系统一样,Android的第一个用户空间进程是init(init的PID值是0),它是所有其他进程的根。然而,Android的init启动的守护进程是不同的,这些守护进程更多的聚焦于底层细节(管理文件系统和硬件访问),而不是高层用户设施,例如调度定时任务。Android还有一层额外的进程,它们运行Dalvik的Java语言环境,负责执行系统中所有以Java实现的部分。 diff --git "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" index 11a00bf6..48a912cc 100644 --- "a/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ "b/OperatingSystem/AndroidKernal/1.Android\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" @@ -221,6 +221,47 @@ SystemServer和Zygote之间通信不使用Socket的原因是为了要解决fork 在Android系统的Binder机制中,由一系统组件组成,分别是Client、Server、Service Manager和Binder驱动程序,其中Client、Server和Service Manager运行在用户空间,Binder驱动程序运行内核空间。其中Service Manager和Binder驱动由系统提供,而Client、Server由应用程序来实现。其中,核心组件便是Binder驱动程序了,Service Manager提供了辅助管理的功能,Client和Server正是在Binder驱动和Service Manager提供的基础设施上,进行Client-Server之间的通信。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与Binder驱动的交互来间接的实现跨进程通信。 +如果统观Binder中的各个组成元素,就会惊奇的发现它和TCP/IP网络有很多相似之处: + +- Binder驱动 -> 路由器 +- Service Manager -> DNS +- Binder Client -> 客户端 +- Binder Server -> 服务端 + +TCP/IP中一个典型的服务连接过程(比如客户端通过浏览器方位Google主页): + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_tcp_ip.png) + +在这个简化的流程图中共出现了四种角色,即Client、Server、DNS和Router。它们的目标是让Client和Server建立连接,主要分为如下几个步骤。 + +- Client向DNS查询Google.com的IP地址。 + + 显然,Client一定要先知道DNS的IP地址,才有可能向它发起查询。DNS的IP设置是在客户端接入网络前就完成了的,否则Client将无法正常访问域名服务器。当然,如果Client已经知晓了Server的IP,那么完全可以跨越这一步而直接与Server连接。比如Windows操作系统就提供了一个hosts文件,用于查询常用网址域名与其IP地址的对应关系。当用户需要访问某个网址时,系统会先从这个文件中判断是否已经存在有这个域名的对应IP。如果有就不需要再大费周折地向DNS查询了,从而加快访问速度。 + +- DNS将查询结果返回Client。 + + 因为Client的IP地址对于DNS也必须是可知的,这些信息都会被封装在TCP/IP包中。 + +- Client发起连接。 + +- Client在得到Google.com的IP地址后,就可以据此来向Google服务器发起连接了。 + +在这一些列流程中,我们并没有特别提及Router的作用。因为它所担负的责任是将数据包投递到用户设定的目标IP中,即Router是整个通信结构中的基础。 + +从这个典型的TCP/IP通信中,我们还能得到什么提示呢? + +首先,在TCP/IP参考模型中,对于IP层及以上的用户来说,IP地址是他们彼此沟通的凭证,任何用户在整个互联网中的IP标志符都是唯一的。 + +其次,Router是构建一个通信网络的基础,它可以根据用户填写的目标IP正确的把数据包发送到位。 + +最后DNS角色并不是必需的,它的出现是为了帮助人们使复杂难记的IP地址与可读性更强的域名建立关联,并提供查询功能。而客户端能使用DNS的前提是它已经正确配置了DNS服务器的IP地址。 + +Binder的本质目标用一句话来描述,就是进程1(客户端)希望与进程2(服务端)进行互访。但因为它们之间是跨进程(跨网络)的,所以必须借助于Binder驱动(路由器)来把请求正确投递到对方所在进程(网络)中。而参与通信的进程们需要持有Binder“颁发”的唯一标志(IP地址)。和TCP/IP网络类似,Binder中的DNS也并不是必需的,前提是客户端能记住它要访问的进程的Binder标志(IP地址)。而且要特别注意这个标志是动态IP,这就意味着即使客户端记住了本次通信过程中目标进程的唯一标志,下一次访问仍然需要重新获取,这无疑加大了客户端的难度。DNS的出现可以完美的解决这个问题,用于管理Binder标志与可读性更强的域名间的对应关系,并向用户提供查询功能。既然Service Manager是DNS,那么它的IP地址是什么呢? Binder机制对此作了特别规定,Service Manager在Binder通信过程中的唯一标志永远都是0. + + + + + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/binder_framework.png) - Server diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index 95e4f084..620d6f10 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -39,6 +39,50 @@ Android的屏幕绘制架构如下图: - 每个具体的Drawable对象仅仅绘制某个特定的图案,SDK中包含的Drawable实现类有BitmapDrawable、NinePatchDrawable等。 + +Linux内核提供了统一的framebuffer显示驱动。设备节点/dev/graphics/fb*或者/dev/fb*,其中fb0表示第一个Monitor,当前系统中只用到了一个显示屏。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_surface_flinger.png) + +Android的HAL层提供了Gralloc,包括fb和gralloc两个设备。前者负责打开内核中的framebuffer、初始化配置,并提供了post、setSwapInterval等操作接口。后者则管理帧缓冲区的分配和释放。这就意味着上层元素只能通过Gralloc来间接访问帧缓冲区,从而保证了系统对framebuffer的有序使用和统一管理。 + +另外,HAL层的另一重要模块是“Composer”,如其名所示,它为厂商自定制“UI合成”提供了接口。Composer的直接使用者是SurfaceFlinger中的HWComposer,后者除了管理Composer的HAL模块外,还负责VSync信号的产生和控制。VSync则是Project Butter工程中加入的一种同步机制,它既可以由硬件产生,也可以通过软件来moni(VsyncThread)。 + + + +Framebuffer是系内核系统提供的图形硬件的抽象描述。之所以称为buffer,是因为它也占用了系统存储空间的一部分,是一块包含屏幕显示信息的缓冲区。由此可见,在一切都是文件的Linux系统中,Framebuffer被看成了终端显示设备的化身。 + +另外,Framebuffer借助于Linux文件系统向上层应用提供了统一而高效的操作接口,从而让用户空间中运行的程序可以在不做太多修改的情况下去适配多种显示设备,无论它们属于哪家厂商、什么型号,都由Framebuffer内部来兼容。 + +在Android系统的GUI设计理念中,提供了两种本地窗口: + +- 面向管理者(SurfaceFlinger) + + 既然SurfaceFlinger扮演了系统中所有UI界面的管理者,那么它无可厚非需要直接或间接地持有“本地窗口。这个窗口就是FramebufferNativeWindow。 + +- 面向应用程序 + + 这类本地窗口是Surface。 + + + + + + + + + + + + + + + + + + + + --- diff --git "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" index cb3820e1..949eaffd 100644 --- "a/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" +++ "b/OperatingSystem/AndroidKernal/8.WindowManagerService\347\256\200\344\273\213.md" @@ -13,6 +13,10 @@ WmS是Android中图形用户接口的引擎,它管理着所有窗口,包括 - 保持窗口的层次关系,以便SurfaceFlinger能够据此绘制屏幕 - 把窗口信息传递给InputManager对象,以便InputDispatcher能够把输入消息派发给和屏幕上显示一致的窗口。 + + +打个比方,就像一出由N个演员参与的话剧:SurfaceFlinger是摄像机,WMS是导演,ViewRoot则是演员个体。摄像机(SurfaceFlinger)的作用是单一而规范的,它负责客观地捕获当前的画面,然后真是的呈现给观众。导演(WMS)则会考虑到话剧的舞台效果和视觉美感,如他需要根据实际情况来安排各个演员的排序站位,谁在前谁在后,都会影响到演出的”画面效果“与”剧情编排“,而各个演员的长相和标清(ViewRoot)则更多的取决于他们自身的条件与努力。正式通过这三者的”各司其职“,才能最终为观众呈现出异常美妙绝伦的”视觉盛宴“。 + Android采用层叠式布局,这种布局的特点在于允许多个窗口层叠显示。该布局一般都需要一个窗口管理服务端。从程序设计的角度看,有两种设计模式可以实现服务端: - 独立进程方式 diff --git "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" index cc1cdbb3..5c0d5d6c 100644 --- "a/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" +++ "b/SourceAnalysis/Activity\345\220\257\345\212\250\350\277\207\347\250\213.md" @@ -1496,9 +1496,15 @@ final void startActivityLocked(ActivityRecord r, boolean newTask, 在Android应用程序框架层中,是由ActivityManagerService组件负责为Android应用程序创建新的进程的,它本来也是运行在一个独立的进程之中,不过这个进程是在系统启动的过程中创建的。 ActivityManagerService组件一般会在什么情况下会为应用程序创建一个新的进程呢?当系统决定要在一个新的进程中启动一个Activity或者Service时,它就会创建一个新的进程了, - 然后在这个新的进程中启动这个Activity或者Service + 然后在这个新的进程中启动这个Activity或者Service。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/ams_start_activity.png) + +无论以什么方式发起一个Activity的启动流程,最终都会调用到AMS的startActivity的函数。而在AMS真正启动一个Activity之前,需要经过众多烦琐的判断和准备工作,这些工作在AMS内部都是由一些列以startActivity开头的函数来进行逐步处理的。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_start_binder.png) + - --- - 邮箱 :charon.chui@gmail.com From fead41f580ac363bd0004af47851c622ba5a5530 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 15 Mar 2021 14:42:49 +0800 Subject: [PATCH 015/183] update --- .../\345\205\263\351\224\256\345\270\247.md" | 33 +++++- ...47\350\203\275\344\274\230\345\214\226.md" | 14 +++ .../TS.md" | 104 +++++++++++++++++- ...01\350\243\205\346\240\274\345\274\217.md" | 8 ++ 4 files changed, 152 insertions(+), 7 deletions(-) diff --git "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" index da3274dc..5366ba8d 100644 --- "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" +++ "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" @@ -1,21 +1,42 @@ 关键帧 === -**编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 -在H.264压缩标准中I帧、P帧、B帧用于表示传输的视频画面。 + +### I、P、B帧 + +在H.264协议中定义了3中帧,完整编码的帧叫I帧、参考之前的I帧生成的只对差异部分进行编码的帧叫P帧、还有一种参考前后的帧进行编码的帧叫B帧。 - I帧(Intra coded picture):帧内编码图像帧简称关键帧,这一帧画面的完整保留,解码时只需要本帧数据就可以完成.I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个,GOP就是指两个I帧之间的距离。 我们在做直播的时候,想要能达到秒开的效果,因为I帧是能不依赖其他帧独立完成解码的,这就意味着当播放器接收到I帧就能立马渲染出来,而接收到P、B帧则需要等待依赖的帧而不能立马完成解码和渲染,这个期间就会黑屏,所以可以在服务端通过缓存GOP,保证播放端在接入直播时能先获取到I帧马上渲染出画面,从而提高起播速度。在直播服务器中,支持设置一个cache,用于存放GOP,直播服务器缓存了当前的GOP序列,当播放端请求数据的时候,CDN会从I帧返回给客户端,从而保证客户端可以快速进行播放。当然由于缓存的是之前的视频信息,当音频数据到达播放端后,为了音视频同步播放器会进行视频的快进处理。 - P帧(Predictive coded picture):预测编码图像帧简称差别帧,这一帧跟之前的一个关键帧或P帧的差别,解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终的画面。也就是说P帧没有完整的画面数据,只有与前一帧的画面差别的数据。 + - B帧(Bidirectionally predicted picture):双向预测编码图像帧简称双向差别帧,也就是B帧记录的是本帧与前后帧的差别。也就是要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧数据的叠加来获取最终的画面。B帧压缩率高,但是解码时会更耗CPU。 -- GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 - 编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + + + +### GOP + +这3中帧用于表示传输的视频画面。在H.264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束,中间部分也被称为一个GOP。 + +GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 +编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 + +**编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 + +### IDR + +一个序列(GOP)的第一个图像叫做IDR图像(立即刷新图像),IDR图像都是I帧图像。H.264引入IDR图像是为了解码的重新同步,当解码器解码到IDR图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找下一个参考集,开始解码一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像数据来解码。一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以是一个I帧,然后一直是P帧、B帧。当运动变化多时,一个序列可能会比较短,比如只包含一个I帧和几个P帧、B帧。 - IDR(Instantaneous Decoding Refresh)--即时解码刷新。 I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。 + + 那么IDR帧与I帧的区别是什 么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 + + + I和IDR帧都是使用帧内预测的。它们都是同一个东西而已,在编码和解码中为了方便,要首个I帧和其他I帧区别开,所以才把第一个首个I帧叫IDR,这样就方便控制编码和解码流程。 IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。而I帧不具有随机访问的能力,这个功能是由IDR承担。 IDR会导致DPB(DecodedPictureBuffer 参考帧列表——这是关键所在)清空,而I不会。IDR图像一定是I图像,但I图像不一定是IDR图像。一个序列中可以有很多的I图像,I图像之后的图像可以引用I图像之间的图像做运动参考。一个序列中可以有很多的I图像,I图像之后的图象可以引用I图像之间的图像做运动参考。 对于IDR帧来说,在IDR帧之后的所有帧都不能引用任何IDR帧之前的帧的内容,与此相反,对于普通的I-帧来说,位于其之后的B-和P-帧可以引用位于普通I-帧之前的I-帧。从随机存取的视频流中,播放器永远可以从一个IDR帧播放,因为在它之后没有任何帧引用之前的帧。但是,不能在一个没有IDR帧的视频中从任意点开始播放,因为后面的帧总是会引用前面的帧 。 收到 IDR 帧时,解码器另外需要做的工作就是:把所有的 PPS 和 SPS 参数进行更新。 @@ -36,11 +57,11 @@ P帧和B帧极大的节省了数据量,节省出来的空间可以用来多保 - +### PTS和DTS 【为什么会有PTS和DTS的概念】 -通过上面的描述可以看出:P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。 +通过上面的描述可以看出:P帧需要参考前面的I帧或P帧才可以生成一张完整的图片,而B帧则需要参考前面I帧或P帧及其后面的一个P帧才可以生成一张完整的图片。这样就带来了一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就引入了另外两个概念:DTS 和 PTS。在没有B帧的情况下,DTS和PTS的输出顺序是一样的。因为B 帧打乱了解码和显示的顺序,所以一旦存在B帧,PTS与DTS势必就会 不同。 【PTS和DTS】 diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index d1c96ecb..aebda409 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -66,6 +66,20 @@ 空间换时间。双播放器方案原理比较简单,就是在当前视频起播之后,预准备下一个视频的播放器,两个播放器循环使用。下一个视频的播放器一旦准备好,加上视频流已加载到本地,则起播非常快,几乎是视频直出的效果。但由于播放器准备更耗时,用户滑到下一个视频时,并不能百分百保证此视频的播放器已准备好。 +### 预取策略 + +1,修改AndroidVideoCache进行预加载 + 2,线程池并发缓存并控制视频缓存优先级(线程池线程数为3,先加入的先缓存),一次预加载8个视频,item创建时开始预加载,item销毁时,取消预加载 + 3,等待下页第一个视频预加载完成,才会进入下一页视频,保证滑到的视频都是可以立马观看的。(一页视频为8个) + 4,当前视频开始播放之后才会进行预加载 + 5,区分快滑慢滑两种模式,快滑时取消当前所有预加载,慢滑不取消,因为快滑大概率滑到一个未预加载的视频,慢滑大概率滑到一个已经预加载的视频,保证滑到视频尽快播放。 + 6,当视频播放后,根据滑动方向,会将取消的预加载进行恢复,正向滑动就恢复当前之后之后的预加载任务,反选滑动就恢复当前视频之前的预加载任务。 + 7,限制网速,当时视频还没播放的时候,会限制预下载的网速(慢滑不会取消预加载),通过让下载线程sleep来限制网速。 + 8,ijkplayer起播参数配置。 + 9,视频压缩和处理(让视频参数都位于视频首部)。 + + + #### MOOV后置 很多手机为了偷懒,录制完视频后才知道视频信息,所以把moov放到末尾。对于moov放到视频最后的情况下,就要多一次seek,当从头开始解析的时候如果发现未找到moov信息,需要将其seek到尾部再去寻找,这样会导致起播慢。 diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" index c0a2daad..947ab46a 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/TS.md" @@ -3,23 +3,125 @@ TS -TODO +TS的全称为MPEG2-TS,TS即Transport Stream的缩写。它是分包发送的,每一个包长188字节(还有192和204字节的包)。包的结构为,包头4字节(第一个字节为0x47),负载为184字节。 +VD的音视频格式为MPEG2-PS,全称是Program Stream。而TS的全称则是Transport Stream。MPEG2-PS主要应用于存储的具有固定时长的节目,如DVD电影,而MPEG-TS则主要应用于实时传送的节目,比如实时广播的电视节目。这两种格式的主要区别是什么呢?简单地打个比喻说,你将DVD上的VOB文件的前面一截cut掉(或者干脆就是数据损坏),那么就会导致整个文件无法解码了,而电视节目是你任何时候打开电视机都能解码(收看)的。在TS流里可以填入很多类型的数据,如视频、音频、自定义信息等。所以,MPEG2-TS格式的特点就是要求从视频流的任一片段开始都是可以独立解码的。 +我们可以看出,TS格式是主要用于直播的码流结构,具有很好的容错能力。通常TS流的后缀是.ts、.mpg或者.mpeg,多数播放器直接支持这种格式的播放。TS流中不包含快速seek的机制,只能通过协议层实现seek。HLS协议基于TS流实现的。 +## TS格式详解 +TS文件(流)可以分为三层:TS层(Transport Stream)、PES层(Packet Elemental Stream)、ES层(Elementary Stream)。 +ES层就是音视频数据,PES层是在音视频数据上加了时间戳等对数据帧的说明信息,TS层是在PES层上加入了数据流识别和传输的必要信息。TS文件(码流)由多个TS Packet组成的。 +![image-20210309114928103](https://raw.githubusercontent.com/CharonChui/Pictures/master/ts_archi.png?raw=true) +### TS层 +TS包大小固定为188字节,TS层分为三个部分:TS Header、Adaptation Field、Payload。 +TS Header固定4个字节;Adaptation Field可能存在也可能不存在,主要作用是给不足188字节的数据做填充;Payload是PES数据。 +#### 1. TS Header + +TS包的包头提供关于传输方面的信息。 + +TS包的包头长度不固定,前4个字节是固定的,后面可能跟有自适应字段(适配域)。4个字节是最小包头。 + +包头的结构体字段如下: + +- sync_byte(同步字节):固定为0x47;该字节由解码器识别,使包头和有效负载可相互分离。 +- transport_error_indicator(传输错误标志):‘1’表示在相关的传输包中至少有一个不可纠正的错误位。当被置1后,在错误被纠正之前不能重置为0。 +- payload_unit_start_indicator(负载起始标志):为1时,表示当前TS包的有效载荷中包含PES或者PSI的起始位置;在前4个字节之后会有一个调整字节,其的数值为后面调整字段的长度length。因此有效载荷开始的位置应再偏移1+[length]个字节。 +- transport_priority(传输优先级标志):‘1’表明当前TS包的优先级比其他具有相同PID, 但此位没有被置‘1’的TS包高。 +- PID:指示存储与分组有效负载中数据的类型。 +- transport_scrambling_control(加扰控制标志):表示TS流分组有效负载的加密模式。空包为‘00’,如果传输包包头中包括调整字段,不应被加密。其他取值含义是用户自定义的。 +- adaptation_field_control(适配域控制标志):表示包头是否有调整字段或有效负载。‘00’为ISO/IEC未来使用保留;‘01’仅含有效载荷,无调整字段;‘10’ 无有效载荷,仅含调整字段;‘11’ 调整字段后为有效载荷,调整字段中的前一个字节表示调整字段的长度length,有效载荷开始的位置应再偏移[length]个字节。空包应为‘10’。 +- continuity_counter(连续性计数器):随着每一个具有相同PID的TS流分组而增加,当它达到最大值后又回复到0。范围为0~15。 + +#### 2. TS Adaptation Field + +Adaptation Field的长度要包含传输错误指示符标识的一个字节。 + +PCR是节目时钟参考,PCR、DTS、PTS都是对同一个系统时钟的采样值,PCR是递增的,因此可以将其设置为DTS值,音频数据不需要PCR。 + +打包TS流时PAT和PMT表是没有Adaptation Field的,不够的长度直接补0xff即可。 + +视频流和音频流都需要加adaptation field,通常加在一个帧的第一个ts包和最后一个ts包里,中间的ts包不加。 + +#### 3. TS Payload + +TS包中Payload所传输的信息包括两种类型:视频、音频的PES包以及辅助数据;节目专用信息PSI。 + +TS包也可以是空包。空包用来填充TS流,可能在重新进行多路复用时被插入或删除。 + +视频、音频的ES流需进行打包形成视频、音频的 PES流。辅助数据(如图文电视信息)不需要打成PES包。 + +### PES层 & ES 层 + +#### PES层 + +PES结构如图: + +![pes](https://raw.githubusercontent.com/CharonChui/Pictures/master/pes.png?raw=true) + +从上面的结构图可以看出,PES层是在每一个视频/音频帧上加入了时间戳等信息,PES包内容很多,下面我们说明一下最常用的字段: + +- pes start code:开始码,固定为0x000001。 +- stream id:音频取值(0xc0-0xdf),通常为0xc0;视频取值(0xe0-0xef),通常为0xe0。 +- pes packet length:后面pes数据的长度,0表示长度不限制,只有视频数据长度会超过0xffff。 +- pes data length:后面数据的长度,取值5或10。 +- pts:33bit值 +- dts:33bit值 + +关于时间戳PTS和DTS的说明: + +1. PTS是显示时间戳、DTS是解码时间戳。 +2. 视频数据两种时间戳都需要,音频数据的PTS和DTS相同,所以只需要PTS。 + +有PTS和DTS两种时间戳是B帧引起的,I帧和P帧的PTS等于DTS。如果一个视频没有B帧,则PTS永远和DTS相同。 + +从文件中顺序读取视频帧,取出的帧顺序和DTS顺序相同。DTS算法比较简单,初始值 + 增量即可,PTS计算比较复杂,需要在DTS的基础上加偏移量。 + +音频的PES中只有PTS(同DTS),视频的I、P帧两种时间戳都要有,视频B帧只要PTS(同DTS)。 + +#### ES 层 + +ES层指的就是音视频数据。 + +一般的,视频为H.264视频,音频为AAC音频。 + + + + + +## TS流生成及解析流程 + +### TS 流生成流程 + +- 将原始音视频数据压缩之后,压缩结果组成一个基本码流(ES)。 +- 对ES(基本码流)进行打包形成PES。 +- 在PES包中加入时间戳信息(PTS/DTS)。 +- 将PES包内容分配到一系列固定长度的传输包(TS Packet)中。 +- 在传输包中加入定时信息(PCR)。 +- 在传输包中加入节目专用信息(PSI) 。 +- 连续输出传输包形成具有恒定比特率的MPEG-TS流。 + +### TS 流解析流程 + +- 复用的MPEG-TS流中解析出TS包; +- 从TS包中获取PAT及对应的PMT; +- 从而获取特定节目的音视频PID; +- 通过PID筛选出特定音视频相关的TS包,并解析出PES; +- 从PES中读取到PTS/DTS,并从PES中解析出基本码流ES; +- 将ES交给解码器,获得压缩前的原始音视频数据。 diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" index 8899f1a8..a81d1123 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217.md" @@ -8,6 +8,14 @@ +封装,也叫多路复用(mux)。封装的目的一般为了在一个文件(流)中能同时存储视频(video)、音频(audio)、字幕(subtitle)等内容——这也正是“复用”的含义所在(分时复用)。封装还有另一个作用是在网络环境下确保数据的可靠快速传输。 + +编码的目的是为了压缩媒体数据。有别于通用文件数据的压缩,在图像或音频压缩的时候,可以借助图像特性(如前后关联、相邻图块关联)或声音特性(听觉模型)进行压缩,可以达到比通用压缩技术更高的压缩比。 + + + + + ## 音频编码格式 From c9f76c0a10c3270e2d36c8d9d1feee299bd7b326 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 Mar 2021 21:07:24 +0800 Subject: [PATCH 016/183] add notes --- ...01\350\231\232\345\274\225\347\224\250.md" | 15 +- ...20\347\240\201\345\210\206\346\236\220.md" | 792 ++++++++++++++++++ ...20\347\240\201\350\257\246\350\247\243.md" | 6 +- .../\345\205\263\351\224\256\345\270\247.md" | 2 +- 4 files changed, 804 insertions(+), 11 deletions(-) create mode 100644 "SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" diff --git "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" index 1f4346c1..e5dba493 100644 --- "a/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" +++ "b/JavaKnowledge/\345\274\272\345\274\225\347\224\250\343\200\201\350\275\257\345\274\225\347\224\250\343\200\201\345\274\261\345\274\225\347\224\250\343\200\201\350\231\232\345\274\225\347\224\250.md" @@ -16,21 +16,22 @@ - 强引用(Strong Reference) - + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 - + - 软引用(SoftReference) - + 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。 + **软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用, 如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - 弱引用(WeakReference) - - 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + + 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的声明周期。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 @@ -60,12 +61,12 @@ ``` - 虚引用(PhantomReference) - + "虚引用"顾名思义,就是形同虚设,也成为幽灵引用或幻影引用,它是最弱的一种引用关系。就只是一个标识,对象的生命周期不受期影响,这货估计就是个临时工把,遇到事情的时候想到了你,没有事情的时候,秒秒钟拿出去顶锅,开除。 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 - 虚引用必须和引用队列 `(ReferenceQueue)`联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 + 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 diff --git "a/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" new file mode 100644 index 00000000..62669286 --- /dev/null +++ "b/SourceAnalysis/LeakCanary\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -0,0 +1,792 @@ +LeakCanary源码分析 +=== + +[LeakCanary](https://github.com/square/leakcanary)是一个用于检测内存泄漏的工具,可以用于Java和Android,是由著名开源组织Square贡献。 + +强烈建议使用LeakCanary 2.x版本,更高效、使用更简单,而且没有任何Java代码,它当泄露引用到达5时才会发起heap dump,同时使用了全新的heap parser,减少内存占用,提升速度。只需要在dependencies中加入leakcanary的依赖即可。而且debugimplementation只在debug模式下有效,所以不用担心用户在正式环境下也会出现LeanCanary收集。而且是完全使用kotlin实现了,同时使用了[see Shark](https://square.github.io/leakcanary/shark/)来进行heap内存分析,更节省内存。 + + + +### 使用 + +只需在app的build.gradle中按如下配置即可,没有任何其他代码: + +```xml +dependencies { + // debugImplementation because LeakCanary should only run in debug builds. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.6' +} +``` + + + +到这里你肯定会有个疑问:一行Java代码都没有,它是如何做到监控的? 后面我们就带着这个疑问来进行源码查看。不过再看源码之前,我们需要先了解一些引用的知识[**强引用、软引用、弱引用、虚引用**](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%BC%BA%E5%BC%95%E7%94%A8%E3%80%81%E8%BD%AF%E5%BC%95%E7%94%A8%E3%80%81%E5%BC%B1%E5%BC%95%E7%94%A8%E3%80%81%E8%99%9A%E5%BC%95%E7%94%A8.md) + +这篇文章中在介绍弱引用时是这样说的: + +同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 + +在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 + +而LeakCanary的原因也正是使用了弱引用的这个特点。 + +### 监测原理 + +ReferenceQueue+WeakReference+手动调用GC可实现这个需求。 + +WeakReference和ReferenceQueue联合使用, +当被WeakReference引用的对象的生命周期结束,一旦被GC检查到,GC将会把该对象添加到ReferenceQueue中,待ReferenceQueue处理。 +当GC过后对象一直不被加入ReferenceQueue,它可能存在内存泄漏。 + +LeakCanary的基础是一个叫做ObjectWatcher Android的library。它hook了Android的生命周期,当activity和fragment 被销毁并且应该被垃圾回收时候自动检测。这些被销毁的对象被传递给ObjectWatcher, ObjectWatcher持有这些被销毁对象的弱引用(weak references)。如果弱引用在等待5秒钟并运行垃圾收集器后仍未被清除,那么被观察的对象就被认为是保留的(retained,在生命周期结束后仍然保留),并存在潜在的泄漏。LeakCanary会在Logcat中输出这些日志。 + +### 监测内容 + +LeakCanary会自动监测如下对象的泄露情况: + +- destroyed `Activity` instances +- destroyed `Fragment` instances +- destroyed fragment `View` instances +- cleared `ViewModel` instances + + + +### 监测步骤 + +一旦LeakCanary被安装,它会自动监测和上报泄露,分为以下四步: + +1. Detecting retained objects. +2. Dumping the heap. +3. Analyzing the heap. +4. Categorizing leaks. + + + +好了,我们接下来从源码的角度来看一下具体的实现,但是看到代码我懵逼了,从哪里看呢? 我完全没头绪。 + +![leakcanary_source](https://raw.githubusercontent.com/CharonChui/Pictures/master/leakcanary_source.png) + +那就继续看官方介绍吧。 + +## 1. Detecting retained objects[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#1-detecting-retained-objects) + +LeakCanary hooks into the Android lifecycle to automatically detect when activities and fragments are destroyed and should be garbage collected. These destroyed objects are passed to an `ObjectWatcher`, which holds [weak references](https://en.wikipedia.org/wiki/Weak_reference) to them. LeakCanary automatically detects leaks for the following objects: + +- destroyed `Activity` instances +- destroyed `Fragment` instances +- destroyed fragment `View` instances +- cleared `ViewModel` instances + +You can watch any objects that is no longer needed, for example a detached view or a destroyed presenter: + +``` +AppWatcher.objectWatcher.watch(myDetachedView, "View was detached") +``` + +If the weak reference held by `ObjectWatcher` isn’t cleared after **waiting 5 seconds** and running garbage collection, the watched object is considered **retained**, and potentially leaking. LeakCanary logs this to Logcat: + +``` +D LeakCanary: Watching instance of com.example.leakcanary.MainActivity + (Activity received Activity#onDestroy() callback) + +... 5 seconds later ... + +D LeakCanary: Scheduling check for retained objects because found new object + retained +``` + +LeakCanary waits for the count of retained objects to reach a threshold before dumping the heap, and displays a notification with the latest count. + + + + + +## 2. Dumping the heap[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#2-dumping-the-heap) + +When the count of retained objects reaches a threshold, LeakCanary dumps the Java heap into a `.hprof` file (a **heap dump**) stored onto the Android file system (see [Where does LeakCanary store heap dumps?](https://square.github.io/leakcanary/faq/#where-does-leakcanary-store-heap-dumps)). Dumping the heap freezes the app for a short amount of time, during which LeakCanary displays the following toast: + + + +## 3. Analyzing the heap[¶](https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#3-analyzing-the-heap) + +LeakCanary parses the `.hprof` file using [Shark](https://square.github.io/leakcanary/shark/) and locates the retained objects in that heap dump. + + + +## How does LeakCanary get installed by only adding a dependency?[¶](https://square.github.io/leakcanary/faq/#how-does-leakcanary-get-installed-by-only-adding-a-dependency) + +On Android, content providers are created after the Application instance is created but before Application.onCreate() is called. The `leakcanary-object-watcher-android` artifact has a non exported ContentProvider defined in its `AndroidManifest.xml` file. When that ContentProvider is installed, it adds activity and fragment lifecycle listeners to the application. + +对于Application的onCreate()方法,官方文档中是这样说的: + +Called when the application is starting, before any activity, service, or receiver objects(excluding content providers)have been created. + + + +看到这里,大呼内行...这个实现太巧妙了。尽然是通过在AndroidManifest.xml中注册一个内容提供者,这个内容提供者的创建会在Application.onCreate()之前,在它创建的时候去做install的操作。 + + + +这也太牛逼了,之前从来没想到ContentProvider竟然还有这个功能,以后估计很多第三方的库都会这样来实现,但是这样也会有一个问题,就是会影响到应用的冷启时间,好在LeakCanary只是在Debug的时候使用,也就不会存在这个问题了。 + +好了,可以看代码了,先看一下这些module的依赖关系,Gradle中的module下Task中的help中的dependencies: + +``` +debugRuntimeClasspath - Runtime classpath of compilation 'debug' (target (androidJvm)). ++--- project :leakcanary-android +| +--- project :leakcanary-android-core +| | +--- project :shark-android +| | | +--- project :shark +| | | | +--- project :shark-graph +| | | | | +--- project :shark-hprof +| | | | | | +--- project :shark-log +| | | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 +| | | | | | | +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.21 +| | | | | | | \--- org.jetbrains:annotations:13.0 +| | | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | | \--- com.squareup.okio:okio:2.2.2 +| | | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.2.60 -> 1.4.21 (*) +| | | | +--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | | \--- com.squareup.okio:okio:2.2.2 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android +| | | +--- project :leakcanary-object-watcher +| | | | +--- project :shark-log (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | +--- project :leakcanary-android-utils +| | | | +--- project :shark-log (*) +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | | +--- com.squareup.curtains:curtains:1.0.1 +| | | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.4.21 (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android-androidx +| | | +--- project :leakcanary-object-watcher-android (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :leakcanary-object-watcher-android-support-fragments +| | | +--- project :leakcanary-object-watcher-android (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | +--- project :plumber-android +| | | +--- project :shark-log (*) +| | | +--- project :leakcanary-android-utils (*) +| | | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| | \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +| \--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) +\--- org.jetbrains.kotlin:kotlin-stdlib:1.3.72 -> 1.4.21 (*) + +``` + + + +那我们就去`leakcanary-object-watcher-android`库的AndroidManifest.xml中看一下这个内容提供者: + +```xml + + + +``` + +继续看一下AppWatcherInstaller类的实现: + +```kotlin +/** + * Content providers are loaded before the application class is created. [AppWatcherInstaller] is + * used to install [leakcanary.AppWatcher] on application start. + */ +internal sealed class AppWatcherInstaller : ContentProvider() { + + /** + * [MainProcess] automatically sets up the LeakCanary code that runs in the main app process. + */ + internal class MainProcess : AppWatcherInstaller() + + /** + * When using the `leakcanary-android-process` artifact instead of `leakcanary-android`, + * [LeakCanaryProcess] automatically sets up the LeakCanary code + */ + internal class LeakCanaryProcess : AppWatcherInstaller() + + override fun onCreate(): Boolean { + val application = context!!.applicationContext as Application + AppWatcher.manualInstall(application) + return true + } + + override fun query( + uri: Uri, + strings: Array?, + s: String?, + strings1: Array?, + s1: String? + ): Cursor? { + return null + } + + override fun getType(uri: Uri): String? { + return null + } + + override fun insert( + uri: Uri, + contentValues: ContentValues? + ): Uri? { + return null + } + + override fun delete( + uri: Uri, + s: String?, + strings: Array? + ): Int { + return 0 + } + + override fun update( + uri: Uri, + contentValues: ContentValues?, + s: String?, + strings: Array? + ): Int { + return 0 + } +} +``` + +上面的注释说的很明白,这个内容提供者的主要目的就是调用AppWatcher.install,我们继续看一下AppWatcher.manualInstall()方法: + +```kot + @JvmOverloads + fun manualInstall( + application: Application, + retainedDelayMillis: Long = TimeUnit.SECONDS.toMillis(5), + watchersToInstall: List = appDefaultWatchers(application) + ) { + // 检查是否是主线程 + checkMainThread() + // 检查是否已经installed + check(!isInstalled) { + "AppWatcher already installed" + } + check(retainedDelayMillis >= 0) { + "retainedDelayMillis $retainedDelayMillis must be at least 0 ms" + } + this.retainedDelayMillis = retainedDelayMillis + if (application.isDebuggableBuild) { + // debug模式的话就初始化泄露信息的logcat打印 + LogcatSharkLog.install() + } + // Requires AppWatcher.objectWatcher to be set + LeakCanaryDelegate.loadLeakCanary(application) + + watchersToInstall.forEach { + // 不传递的话,默认为appDefaultWatchers,将这里面的四个类型的检测器各自初始化 + it.install() + } + } + + // appDefaultWatchers()的实现为: + fun appDefaultWatchers( + application: Application, + reachabilityWatcher: ReachabilityWatcher = objectWatcher + ): List { + return listOf( + ActivityWatcher(application, reachabilityWatcher), + FragmentAndViewModelWatcher(application, reachabilityWatcher), + RootViewWatcher(reachabilityWatcher), + ServiceWatcher(reachabilityWatcher) + ) + } + +``` + +这里我们以ActivityWatcher为例,看一下它的install实现: + +```kotlin +/** + * Expects activities to become weakly reachable soon after they receive the [Activity.onDestroy] + * callback. + */ +class ActivityWatcher( + private val application: Application, + private val reachabilityWatcher: ReachabilityWatcher +) : InstallableWatcher { + + private val lifecycleCallbacks = + object : Application.ActivityLifecycleCallbacks by noOpDelegate() { + override fun onActivityDestroyed(activity: Activity) { + reachabilityWatcher.expectWeaklyReachable( + activity, "${activity::class.java.name} received Activity#onDestroy() callback" + ) + } + } + + override fun install() { + application.registerActivityLifecycleCallbacks(lifecycleCallbacks) + } + + override fun uninstall() { + application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) + } +} +``` + +这里面的实现也很简单,就是通过Application.registerActivityLifecycleCallbacks然后在activity destroy的回调中去执行ReachabilityWatcher接口的expectWeaklyReachable,ReachabilityWatcher接口的实现类是ObjectWatcher类: + +```kotlin +class ObjectWatcher constructor( + private val clock: Clock, + private val checkRetainedExecutor: Executor, + /** + * Calls to [watch] will be ignored when [isEnabled] returns false + */ + private val isEnabled: () -> Boolean = { true } +) : ReachabilityWatcher { + // ... + // Map集合,用来存放要观察对象的key和弱引用,代码会为每个观察的对象生成一个唯一的key和弱应用 + private val watchedObjects = mutableMapOf() + // 这个队列是与弱引用联合使用,当弱引用中的对象被回收后,这个弱引用会被放到这个队列中。 + // 也就是说只要存在与这个队列中的弱引用就代表该弱引用所包含的对象被回收了。 + private val queue = ReferenceQueue() + + @Synchronized override fun expectWeaklyReachable( + watchedObject: Any, + description: String + ) { + if (!isEnabled()) { + return + } + // 清理操作:先移除watchedObjects和queue中已经被回收的对象的弱引用 + removeWeaklyReachableObjects() + // 生成唯一的key + val key = UUID.randomUUID() + .toString() + val watchUptimeMillis = clock.uptimeMillis() + // 用要监听的对象创建KeyedWeakReference + val reference = + KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue) + SharkLog.d { + "Watching " + + (if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") + + (if (description.isNotEmpty()) " ($description)" else "") + + " with key $key" + } + + watchedObjects[key] = reference + // 这是一个延迟任务,延迟五秒后再次执行moveToRetained()方法 + checkRetainedExecutor.execute { + moveToRetained(key) + } + } +} +``` + +我们继续看moveToRetained()方法的实现: + +```kotlin + @Synchronized private fun moveToRetained(key: String) { + // 该方法会调用removeWeaklyReachableObjects再去执行一次清理操作 + // 因为这个期间对象可能已经被回收了,所以需要再清理一次 + removeWeaklyReachableObjects() + val retainedRef = watchedObjects[key] + // 等再执行完清理操作后,那还在watchedObjects集合中的对象就是没有被回收的对象了 + if (retainedRef != null) { + retainedRef.retainedUptimeMillis = clock.uptimeMillis() + onObjectRetainedListeners.forEach { it.onObjectRetained() } + } + } +``` + +继续看OnObjectRetainedListener接口的onObjectRetained()方法,这里它的实现类是InternalLeakCanary: + +```kotlin + private lateinit var heapDumpTrigger: HeapDumpTrigger + + override fun onObjectRetained() = scheduleRetainedObjectCheck() + + fun scheduleRetainedObjectCheck() { + if (this::heapDumpTrigger.isInitialized) { + heapDumpTrigger.scheduleRetainedObjectCheck() + } + } +``` + +继续看HeapDumpTrigger类的scheduleRetainedObjectCheck(): + +```kotlin + fun scheduleRetainedObjectCheck( + delayMillis: Long = 0L + ) { + val checkCurrentlyScheduledAt = checkScheduledAt + if (checkCurrentlyScheduledAt > 0) { + return + } + checkScheduledAt = SystemClock.uptimeMillis() + delayMillis + backgroundHandler.postDelayed({ + checkScheduledAt = 0 + checkRetainedObjects() + }, delayMillis) + } +``` + +这里会立即执行checkRetainedObjects()方法: + +```kotlin +private fun checkRetainedObjects() { + // 调用ObjectWatcher中watchedObjects集合中对象retainedUptimeMillis != -1的数量 + // 这些是可能发生泄漏的对象数量 + var retainedReferenceCount = objectWatcher.retainedObjectCount + if (retainedReferenceCount > 0) { + // 如果数量> 0,调用gc + gcTrigger.runGc() + // 调用完gc后重新获取数量 + retainedReferenceCount = objectWatcher.retainedObjectCount + } + // 如果个数小于5,不做操作等待5秒再次进行检查未回收的个数,一直循环,直到大于等于5个或者等于0个,为了防止频发回收堆造成卡顿。 + // 大于5个后,如果处于debug模式,会再等20秒,再次执行循环gc的操作。防止debug模式会减慢回收 + if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return + + val now = SystemClock.uptimeMillis() + val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis + // 距离上次堆栈分析是否大于等于1分钟,如果没有超过一分钟,也需要再次延迟(1分钟-当前距离上次的时间)再次循环gc的操作 + if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) { + onRetainInstanceListener.onEvent(DumpHappenedRecently) + showRetainedCountNotification( + objectCount = retainedReferenceCount, + contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait) + ) + scheduleRetainedObjectCheck( + delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis + ) + return + } + + dismissRetainedCountNotification() + val visibility = if (applicationVisible) "visible" else "not visible" + // 最终调用dump + dumpHeap( + retainedReferenceCount = retainedReferenceCount, + retry = true, + reason = "$retainedReferenceCount retained objects, app is $visibility" + ) + } +``` + +继续看dumpHeap()方法: + +```kotlin + private fun dumpHeap( + retainedReferenceCount: Int, + retry: Boolean, + reason: String + ) { + saveResourceIdNamesToMemory() + val heapDumpUptimeMillis = SystemClock.uptimeMillis() + KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis + // HeapDumper.dumpHeap()方法来获取Heap信息,生成hprof文件 + when (val heapDumpResult = heapDumper.dumpHeap()) { + is NoHeapDump -> { + if (retry) { + SharkLog.d { "Failed to dump heap, will retry in $WAIT_AFTER_DUMP_FAILED_MILLIS ms" } + scheduleRetainedObjectCheck( + delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS + ) + } else { + SharkLog.d { "Failed to dump heap, will not automatically retry" } + } + showRetainedCountNotification( + objectCount = retainedReferenceCount, + contentText = application.getString( + R.string.leak_canary_notification_retained_dump_failed + ) + ) + } + is HeapDump -> { + lastDisplayedRetainedObjectCount = 0 + lastHeapDumpUptimeMillis = SystemClock.uptimeMillis() + // 清楚早于heapDumpUptimeMillis之前的元素 + objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis) + // 将heap信息文件交给HeapAnalyzerService进行分析 + HeapAnalyzerService.runAnalysis( + context = application, + heapDumpFile = heapDumpResult.file, + heapDumpDurationMillis = heapDumpResult.durationMillis, + heapDumpReason = reason + ) + } + } + } +``` + + + +所以我们要分两部分量看: + +##### 1. HeapDumper接口的实现类是AndroidHeapDumper.dumpHeap() + +```kotlin + override fun dumpHeap(): DumpHeapResult { + val heapDumpFile = leakDirectoryProvider.newHeapDumpFile() ?: return NoHeapDump + + val waitingForToast = FutureResult() + showToast(waitingForToast) + + if (!waitingForToast.wait(5, SECONDS)) { + SharkLog.d { "Did not dump heap, too much time waiting for Toast." } + return NoHeapDump + } + + val notificationManager = + context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Notifications.canShowNotification) { + val dumpingHeap = context.getString(R.string.leak_canary_notification_dumping) + val builder = Notification.Builder(context) + .setContentTitle(dumpingHeap) + val notification = Notifications.buildNotification(context, builder, LEAKCANARY_LOW) + notificationManager.notify(R.id.leak_canary_notification_dumping_heap, notification) + } + + val toast = waitingForToast.get() + + return try { + val durationMillis = measureDurationMillis { + // 系统Debug.dumpHprofData方法进行堆转储,保存在yyyy-MM-dd_HH-mm-ss_SSS.hprof + Debug.dumpHprofData(heapDumpFile.absolutePath) + } + if (heapDumpFile.length() == 0L) { + SharkLog.d { "Dumped heap file is 0 byte length" } + NoHeapDump + } else { + HeapDump(file = heapDumpFile, durationMillis = durationMillis) + } + } catch (e: Exception) { + SharkLog.d(e) { "Could not dump heap" } + // Abort heap dump + NoHeapDump + } finally { + cancelToast(toast) + notificationManager.cancel(R.id.leak_canary_notification_dumping_heap) + } + } +``` + + + + + +##### 2. HeapAnalyzerService.runAnalysis() + +```kotlin +companion object { + private const val HEAPDUMP_FILE_EXTRA = "HEAPDUMP_FILE_EXTRA" + private const val HEAPDUMP_DURATION_MILLIS_EXTRA = "HEAPDUMP_DURATION_MILLIS_EXTRA" + private const val HEAPDUMP_REASON_EXTRA = "HEAPDUMP_REASON_EXTRA" + private const val PROGUARD_MAPPING_FILE_NAME = "leakCanaryObfuscationMapping.txt" + + fun runAnalysis( + context: Context, + heapDumpFile: File, + heapDumpDurationMillis: Long? = null, + heapDumpReason: String = "Unknown" + ) { + val intent = Intent(context, HeapAnalyzerService::class.java) + intent.putExtra(HEAPDUMP_FILE_EXTRA, heapDumpFile) + intent.putExtra(HEAPDUMP_REASON_EXTRA, heapDumpReason) + heapDumpDurationMillis?.let { + intent.putExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, heapDumpDurationMillis) + } + startForegroundService(context, intent) + } + + private fun startForegroundService( + context: Context, + intent: Intent + ) { + if (SDK_INT >= 26) { + context.startForegroundService(intent) + } else { + // Pre-O behavior. + context.startService(intent) + } + } + } +``` + +这里面会去启动HeapAnalyzerService,然后会执行到onHandleIntentInForeground()中: + +```kotlin +override fun onHandleIntentInForeground(intent: Intent?) { + if (intent == null || !intent.hasExtra(HEAPDUMP_FILE_EXTRA)) { + SharkLog.d { "HeapAnalyzerService received a null or empty intent, ignoring." } + return + } + + // Since we're running in the main process we should be careful not to impact it. + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND) + // 取出传递过来的hprof文件信息 + val heapDumpFile = intent.getSerializableExtra(HEAPDUMP_FILE_EXTRA) as File + val heapDumpReason = intent.getStringExtra(HEAPDUMP_REASON_EXTRA) + val heapDumpDurationMillis = intent.getLongExtra(HEAPDUMP_DURATION_MILLIS_EXTRA, -1) + + val config = LeakCanary.config + // 将解析结果保存到HeapAnalysis中 + val heapAnalysis = if (heapDumpFile.exists()) { + // 解析该文件 + analyzeHeap(heapDumpFile, config) + } else { + missingFileFailure(heapDumpFile) + } + val fullHeapAnalysis = when (heapAnalysis) { + is HeapAnalysisSuccess -> heapAnalysis.copy( + dumpDurationMillis = heapDumpDurationMillis, + metadata = heapAnalysis.metadata + ("Heap dump reason" to heapDumpReason) + ) + is HeapAnalysisFailure -> heapAnalysis.copy(dumpDurationMillis = heapDumpDurationMillis) + } + onAnalysisProgress(REPORTING_HEAP_ANALYSIS) + // 将解析结果回调回去 + config.onHeapAnalyzedListener.onHeapAnalyzed(fullHeapAnalysis) + } +``` + +继续查看analyzeHeap() : + +```kotlin + private fun analyzeHeap( + heapDumpFile: File, + config: Config + ): HeapAnalysis { + val heapAnalyzer = HeapAnalyzer(this) + + val proguardMappingReader = try { + ProguardMappingReader(assets.open(PROGUARD_MAPPING_FILE_NAME)) + } catch (e: IOException) { + null + } + return heapAnalyzer.analyze( + heapDumpFile = heapDumpFile, + leakingObjectFinder = config.leakingObjectFinder, + referenceMatchers = config.referenceMatchers, + computeRetainedHeapSize = config.computeRetainedHeapSize, + objectInspectors = config.objectInspectors, + metadataExtractor = config.metadataExtractor, + proguardMapping = proguardMappingReader?.readProguardMapping() + ) + } +``` + +继续使用Shark库中的HeapAnalyzer.analyze()方法: + +```kotlin +fun analyze( + heapDumpFile: File, + leakingObjectFinder: LeakingObjectFinder, + referenceMatchers: List = emptyList(), + computeRetainedHeapSize: Boolean = false, + objectInspectors: List = emptyList(), + metadataExtractor: MetadataExtractor = MetadataExtractor.NO_OP, + proguardMapping: ProguardMapping? = null + ): HeapAnalysis { + val analysisStartNanoTime = System.nanoTime() + + if (!heapDumpFile.exists()) { + val exception = IllegalArgumentException("File does not exist: $heapDumpFile") + return HeapAnalysisFailure( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + exception = HeapAnalysisException(exception) + ) + } + + return try { + listener.onAnalysisProgress(PARSING_HEAP_DUMP) + // 生成hprof文件中Record的关系图 + val sourceProvider = ConstantMemoryMetricsDualSourceProvider(FileSourceProvider(heapDumpFile)) + sourceProvider.openHeapGraph(proguardMapping).use { graph -> + val helpers = + FindLeakInput(graph, referenceMatchers, computeRetainedHeapSize, objectInspectors) + // 构建从GC Roots到监测对象的最短引用路径,并返回结果 + val result = helpers.analyzeGraph( + metadataExtractor, leakingObjectFinder, heapDumpFile, analysisStartNanoTime + ) + val lruCacheStats = (graph as HprofHeapGraph).lruCacheStats() + val randomAccessStats = + "RandomAccess[" + + "bytes=${sourceProvider.randomAccessByteReads}," + + "reads=${sourceProvider.randomAccessReadCount}," + + "travel=${sourceProvider.randomAccessByteTravel}," + + "range=${sourceProvider.byteTravelRange}," + + "size=${heapDumpFile.length()}" + + "]" + val stats = "$lruCacheStats $randomAccessStats" + result.copy(metadata = result.metadata + ("Stats" to stats)) + } + } catch (exception: Throwable) { + HeapAnalysisFailure( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + exception = HeapAnalysisException(exception) + ) + } + } +``` + +```kotlin +private fun FindLeakInput.analyzeGraph( + metadataExtractor: MetadataExtractor, + leakingObjectFinder: LeakingObjectFinder, + heapDumpFile: File, + analysisStartNanoTime: Long + ): HeapAnalysisSuccess { + listener.onAnalysisProgress(EXTRACTING_METADATA) + val metadata = metadataExtractor.extractMetadata(graph) + + val retainedClearedWeakRefCount = KeyedWeakReferenceFinder.findKeyedWeakReferences(graph) + .filter { it.isRetained && !it.hasReferent }.count() + + // This should rarely happens, as we generally remove all cleared weak refs right before a heap + // dump. + val metadataWithCount = if (retainedClearedWeakRefCount > 0) { + metadata + ("Count of retained yet cleared" to "$retainedClearedWeakRefCount KeyedWeakReference instances") + } else { + metadata + } + + listener.onAnalysisProgress(FINDING_RETAINED_OBJECTS) + val leakingObjectIds = leakingObjectFinder.findLeakingObjectIds(graph) + + val (applicationLeaks, libraryLeaks, unreachableObjects) = findLeaks(leakingObjectIds) + + return HeapAnalysisSuccess( + heapDumpFile = heapDumpFile, + createdAtTimeMillis = System.currentTimeMillis(), + analysisDurationMillis = since(analysisStartNanoTime), + metadata = metadataWithCount, + applicationLeaks = applicationLeaks, + libraryLeaks = libraryLeaks, + unreachableObjects = unreachableObjects + ) + } +``` + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" "b/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" index ada7be25..dfe613b2 100644 --- "a/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/butterknife\346\272\220\347\240\201\350\257\246\350\247\243.md" @@ -261,7 +261,7 @@ private Map findAndParseTargets(RoundEnvironment env) return targetClassMap; } ``` - + 继续看一下`parseBindView()`方法: ```java private void parseBindView(Element element, Map targetClassMap, @@ -484,7 +484,7 @@ static final Map, ViewBinder> BINDERS = new LinkedHashMap<>(); 那`$_ViewBinder`类里面都是什么内容呢? 我们去看一下该类的代码,但是它生成的代码在哪里呢? ![image](https://github.com/CharonChui/Pictures/blob/master/butterknife_apt_genierate_code.png?raw=true) - + 开始看一下`SimpleActivity_ViewBinder.bind()`方法: ```java @@ -565,7 +565,7 @@ public class SimpleActivity_ViewBinding implements Unb } ``` 可以看到他内部会通过`findViewByid()`等来找到对应的`View`,然后将其赋值给`target.xxxx`,所以这样就相当于把所有的控件以及事件都给初始化了,以后就可以直接使用了,通过这里也可以看到我们在使用注解的时候不要把控件或者方法声明为`private`的。 - + 总结一下: diff --git "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" index 5366ba8d..a3884aec 100644 --- "a/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" +++ "b/VideoDevelopment/\345\205\263\351\224\256\345\270\247.md" @@ -33,7 +33,7 @@ GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组 - IDR(Instantaneous Decoding Refresh)--即时解码刷新。 I帧:帧内编码帧是一种自带全部信息的独立帧,无需参考其它图像便可独立进行解码,视频序列中的第一个帧始终都是I帧。 - 那么IDR帧与I帧的区别是什 么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 + 那么IDR帧与I帧的区别是什么呢?因为H264采用了多帧预测,所以I帧之后的P帧有可能会参考I 帧之前的帧,这就使得在随机访问的时候不能以找到I帧作为参考条 件,因为即使找到I帧,I帧之后的帧还是有可能解析不出来,而IDR帧 就是一种特殊的I帧,即这一帧之后的所有参考帧只会参考到这个IDR 帧,而不会再参考前面的帧。在解码器中,一旦收到一个IDR帧,就会 立即清理参考帧缓冲区,并将IDR帧作为被参考的帧。 From 5b27ac2733fdc43cd7822a1c70aeb1932e242309 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 Mar 2021 21:10:29 +0800 Subject: [PATCH 017/183] update README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cb8b3b84..1a456b7f 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Android学习笔记 - [ListView源码分析][8] - [VideoView源码分析][9] - [View绘制过程详解][10] + - [LeakCanary源码分析][284] - [网络部分][11] - [HttpURLConnection详解][12] - [HttpURLConnection与HttpClient][13] @@ -601,7 +602,7 @@ Android学习笔记 [283]: https://github.com/CharonChui/AndroidNote/blob/master/OperatingSystem/AndroidKernal/9.PackageManagerService%E7%AE%80%E4%BB%8B.md "9.PackageManagerService简介" - +[ 284 ]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/LeakCanary%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md. "LeakCanary源码分析" Developed By From 085c3030829e7bf8d749321ad62e96a0bd874608 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 19 Mar 2021 19:17:54 +0800 Subject: [PATCH 018/183] update --- ...27\256\351\242\230\345\210\206\346\236\220.md" | 15 ++++++++++++++- "JavaKnowledge/Git\347\256\200\344\273\213.md" | 11 +++++------ ...63\273\347\273\237\347\256\200\344\273\213.md" | 8 ++++++++ ...50\213\344\270\216\347\272\277\347\250\213.md" | 4 ++-- ...66\210\346\201\257\346\234\272\345\210\266.md" | 6 +++++- ...73\230\345\210\266\345\237\272\347\241\200.md" | 14 ++++++++++++++ 6 files changed, 48 insertions(+), 10 deletions(-) diff --git "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" index 793904c7..e7dcaaa0 100644 --- "a/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/AdavancedPart/OOM\351\227\256\351\242\230\345\210\206\346\236\220.md" @@ -5,6 +5,19 @@ OOM问题分析 OOM(OutOfMemoryError),最近线上版本出现了大量线程OOM的crash,尤其是华为Android 9.0系统的手机,占总OOM量的85%左右。 + + +## 内存指标概念 + + + +- USS(Unique Set Size): 物理内存,进程独占的内存 +- PSS(Proportional Set Size): 物理内存,PSS = USS + 按比例包含共享库 +- RSS(Resident Set Size): 物理内存,RSS = USS + 包含共享库 +- VSS(Virtual Set Size): 虚拟内存,VSS = RSS + 未分配实际物理内存 + + + ### OOM分类 #### [XXXClassName] of length XXX would overflow“是系统限制String/Array的长度所致,这种情况比较少。 @@ -761,7 +774,7 @@ nonvoluntary_ctxt_switches: 328 ``` 当线程数(可以在/proc/pid/status 中的threads项实时查看)超过/proc/sys/kernel/threads-max 中规定的上限时产生 OOM 崩溃。 ``` - + ## 定位验证方法: Thread.UncaughtExceptionHandler捕获到OutOfMemoryError时记录/proc/pid目录下的如下信息: diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index c1022154..2a33f604 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -91,7 +91,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` 先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.jpg) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) 图中的`index`部分就是暂存区 - 安装好git后我们要先配置一下。以便`git`跟踪。 @@ -99,7 +99,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` git config --global user.name "xxx" git config --global user.email "xxx@xxx.com" - ``` + ``` 上面修改后可以使用`cat ~/.gitconfig`查看 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 `git config --list`来查看所有的配置。 @@ -135,7 +135,6 @@ git push // 把所有文件从本地仓库推送进远程仓库 简单用法: `git cherry-pick ` - - `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 - `git diff` @@ -172,7 +171,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 - `–after` ——显示某个日期之后发生的提交 - `–before` ——显示发生某个日期之前的提交 - + - `git reflog` 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 @@ -249,7 +248,7 @@ git push // 把所有文件从本地仓库推送进远程仓库 ``` git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 - ``` + ``` 或`git reset --hard` 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 @@ -473,5 +472,5 @@ $ git log --graph --pretty=oneline --abbrev-commit - +​ diff --git "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" index 0b4d80e4..d7e13d3a 100644 --- "a/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" +++ "b/OperatingSystem/1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md" @@ -523,7 +523,15 @@ int main(int argc, char *argv[]) { +## Linux操作系统特点 +Linux是类Unix系统,借鉴了Unix的设计并实现相关接口,但并非Unix。Linux是由Linus Torvalds于1991年创造的开源免费系统,采用GNU GPL协议保护,下面列举Linux的一些主要特点: + +- Linux系统中万物皆为文件,这种抽象方便操作数据或设备,只需一套统一的系统接口open, read, write, close即可完成对文件的操作 +- Linux是单内核,支持动态加载内核模块,可在运行时根据需求动态加载和卸载部分内核代码; +- Linux内核支持可抢占; +- Linux内核创建进程,采用独特的fork()系统调用,创建进程较高效; +- Linux内核并不区分进程和线程,对于内核而言,进程与线程无非是共享资源的区别,对CPU调度来说并没有显著差异。 diff --git "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" index 9801b918..7cf76645 100644 --- "a/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" +++ "b/OperatingSystem/2.\350\277\233\347\250\213\344\270\216\347\272\277\347\250\213.md" @@ -529,8 +529,8 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 2. 引导程序BootLoader:BootLoader是在Android系统开始运行前的一个小程序,主要用于把系统OS拉起来并运行。 3. Linux内核启动:当内核启动时,设置缓存、被保护存储器、计划列表、加载驱动。当其完成系统设置时,会先在系统文件中寻找init.rc文件,并启动init进程。 -4. init进程启动:初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 -5. Zygote进程启动:zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 +4. init进程启动(是所有用户进程的父进程(或者父父进程)):初始化和启动属性服务,init进程会孵化出eadbd、logd、等用户守护进程,还会启动ServiceManager(binder服务管家)、botanic(开机动画)等服务,并且启动Zygote进程。 +5. Zygote进程启动(zygote是所有上层Java进程的父进程,zygote的父进程是init进程):zygote进程是Android系统的第一个Java进程(即虚拟机进程),它是所有Java进程的父进程。它会创建JVM并为其注册JNI方法,创建服务器端Socket,启动SystemServer进程。并且,zygote进程在启动的时候会创建DVM或者ART。因此通过从zygote进程fork创建的应用程序进程和systemserver进程都可以在内部获取一个DVM或者ART的实例副本。它还会提前加载类preloadClasses和提前加载资源preloadResouces。 6. SystemServer进程启动:System Server是zygote孵化的第一个进程,它会启动Binder线程池和SystemServiceManager,并且启动各种系统服务,包括ActivityManagerService、WindowManagerService、PackageManagerService、PowerManagerService等服务。 7. Media Server进程,是由init进程fork而来,负责启动和管理整个C++ framework,包括AudioFlinger,Camera Service等服务。 8. Launcher启动:是zygote孵化的第一个App进程,被SystemServer进程启动的AmS会启动Launcher,Launcher启动后会将已安装应用的快捷图标显示到系统桌面上。zygote还会创建Broweer、Phone、Email等App进程,每个App至少运行在一个进程上。 diff --git "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" index dbcaf9bb..ba1a361e 100644 --- "a/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" +++ "b/OperatingSystem/AndroidKernal/2.Android\347\272\277\347\250\213\351\227\264\351\200\232\344\277\241\344\271\213Handler\346\266\210\346\201\257\346\234\272\345\210\266.md" @@ -19,6 +19,10 @@ Binder/Socket用于进程间通信,而Handler消息机制用于同进程的线 - Looper:不断循环执行(Looper.loop),按分发机制将消息分发给目标处理者。Looper有一个MessageQueue消息队列; + +![handle_msg_arch](https://raw.githubusercontent.com/CharonChui/Pictures/master/handle_msg_arch.png) + + 首先想一想平时我们是怎么使用的: ```java private Handler mHandler = new Handler(new Handler.Callback() { @@ -866,7 +870,7 @@ public class SampleActivity extends Activity { - `removeCallbacksAndMessages(Object token)` ——清除所有callback以及token匹配上的Message,如果token是null就会清楚所有callback和message。我们更多需要的是清除以该`Handler`为`target`的所有`Message(Callback)`就调用如下方法即可`handler.removeCallbacksAndMessages(null)`; - `removeMessages(int what)` ——按what来匹配 - `removeMessages(int what, Object object)` ——按what来匹配 - + - 将`Handler`声明为静态类。 静态类不持有外部类的对象,所以你的`Activity`可以随意被回收。但是不持有`Activity`的引用,如何去操作`Activity`中的一些对象? 这里要用到弱引用 ```java diff --git "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" index 620d6f10..e24311b8 100644 --- "a/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" +++ "b/OperatingSystem/AndroidKernal/6.\345\261\217\345\271\225\347\273\230\345\210\266\345\237\272\347\241\200.md" @@ -66,11 +66,25 @@ Framebuffer是系内核系统提供的图形硬件的抽象描述。之所以称 +无论开发者使用什么渲染 API,一切内容都会渲染到“Surface”。Surface 表示缓冲队列中的生产方,而缓冲队列通常会被 SurfaceFlinger 消耗。在 Android 平台上创建的每个窗口都由 Surface 提供支持。所有被渲染的可见 Surface 都被 SurfaceFlinger 合成到显示部分。 +### 图像流生产方 +图像流生产方可以是生成图形缓冲区以供消耗的任何内容。例如 OpenGL ES、Canvas 2D 和 mediaserver 视频解码器。 +### 图像流消耗方 + +图像流的最常见消耗方是 SurfaceFlinger,该系统服务会消耗当前可见的 Surface,并使用窗口管理器中提供的信息将它们合成到显示部分。SurfaceFlinger 是可以修改所显示部分内容的唯一服务。SurfaceFlinger 使用 OpenGL 和 Hardware Composer 来合成一组 Surface。 + +其他 OpenGL ES 应用也可以消耗图像流,例如相机应用会消耗相机预览图像流。非 GL 应用也可以是使用方,例如 ImageReader 类。 + +### 硬件混合渲染器 + +显示子系统的硬件抽象实现。SurfaceFlinger 可以将某些合成工作委托给 Hardware Composer,以分担 OpenGL 和 GPU 上的工作量。SurfaceFlinger 只是充当另一个 OpenGL ES 客户端。因此,在 SurfaceFlinger 将一个或两个缓冲区合成到第三个缓冲区中的过程中,它会使用 OpenGL ES。这样使合成的功耗比通过 GPU 执行所有计算更低。 + +[Hardware Composer HAL](https://source.android.com/devices/graphics/architecture#hwcomposer) 则进行另一半的工作,并且是所有 Android 图形渲染的核心。Hardware Composer 必须支持事件,其中之一是 VSYNC(另一个是支持即插即用 HDMI 的热插拔)。 From 77f48f87b66aac56e896ace05a8ff479e54655d8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 2 Apr 2021 22:03:37 +0800 Subject: [PATCH 019/183] update kotlin note --- ...&\347\261\273&\346\216\245\345\217\243.md" | 1050 +++++++++++++++++ ...76\350\256\241\346\250\241\345\274\217.md" | 458 +++++++ ...05\350\201\224\345\207\275\346\225\260.md" | 742 ++++++++++++ ...0\347\273\204&\351\233\206\345\220\210.md" | 554 +++++++++ ...7&\345\205\263\351\224\256\345\255\227.md" | 224 ++++ ...2\344\270\276&\345\247\224\346\211\230.md" | 216 +++- ...47\346\211\277\351\227\256\351\242\230.md" | 211 ++++ ...5\345\260\204&\346\211\251\345\261\225.md" | 859 ++++++++++++++ .../8.Kotlin_\345\215\217\347\250\213.md" | 97 ++ ...\346\225\231\347\250\213(\344\270\200).md" | 534 --------- ...\346\225\231\347\250\213(\344\270\203).md" | 96 -- ...\346\225\231\347\250\213(\344\270\211).md" | 196 --- ...\346\225\231\347\250\213(\344\271\235).md" | 8 + ...\346\225\231\347\250\213(\344\272\214).md" | 88 -- ...\346\225\231\347\250\213(\345\205\253).md" | 121 +- ...\346\225\231\347\250\213(\345\205\255).md" | 204 ---- ...\346\225\231\347\250\213(\345\215\201).md" | 35 +- ...\346\225\231\347\250\213(\345\233\233).md" | 581 --------- 18 files changed, 4421 insertions(+), 1853 deletions(-) create mode 100644 "KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" create mode 100644 "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" create mode 100644 "KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" create mode 100644 "KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" create mode 100644 "KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" rename "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" => "KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" (62%) create mode 100644 "KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" create mode 100644 "KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" create mode 100644 "KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" new file mode 100644 index 00000000..b4432c22 --- /dev/null +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -0,0 +1,1050 @@ +1.Kotlin_简介 +=== + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) + +在`5月18`日谷歌在`I/O`开发者大会上宣布,将`Kotlin`语言作为安卓开发的一级编程语言。并且会在`Android Studio 3.0`版本全面支持`Kotlin`。 + +- `Kotlin`是一个基于`JVM`的新的编程语言,由[JetBrains](https://www.jetbrains.com/)开发。`JetBrains`作为目前广受欢迎的 +`Java IDE IntelliJ`的提供商,在`Apache`许可下已经开源其`Kotlin`编程语言。 +- `Kotlin`可以编译成`Java`字节码,也可以编译成`JavaScript`,方便在没有`JVM`的设备上运行。 +- `Kotlin`已正式成为`Android`官方开发语言。 + +[Kotlin官网](https://kotlinlang.org/) + +`JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 +一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 + +很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 +的一句话: +> 拥抱变化 + +`Google`做事,向来言出必行,之前在推行`Android Studio`时也是一片骂声,吐槽各种不好用,各种慢。但是现在`Android Studio`基本都已经普及了。 +我相信`Kotlin`也不会例外。所以我们不仅要学,还要要认真的学。 + +## `Kotlin`的特性 + +- 它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。 +- `Kotlin`是一种兼容`Java`的语言 +- `Kotlin`比`Java`更安全,能够静态检测常见的陷阱。如:引用空指针 +- `Kotlin`比`Java`更简洁,通过支持`variable type inference,higher-order functions (closures),extension functions,mixins +and first-class delegation`等实现 +- `Kotlin`可与`Java`语言无缝通信。这意味着我们可以在`Kotlin`代码中使用任何已有的`Java`库;同样的`Kotlin`代码还可以为`Java`代码所用 +- `Kotlin`在代码中很少需要在代码中指定类型,因为编译器可以在绝大多数情况下推断出变量或是函数返回值的类型。这样就能获得两个好处:简洁与安全 + +## `Kotlin`优势 + +- 全面支持`Lambda`表达式 +- 数据类`Data classes` +- 函数字面量和内联函数`Function literals & inline functions` +- 函数扩展`Extension functions` +- 空安全`Null safety` +- 智能转换`Smart casts` +- 字符串模板`String templates` +- 主构造函数`Primary constructors` +- 类委托`Class delegation` +- 类型推判`Type inference` +- 单例`Singletons` +- 声明点变量`Declaration-site variance` +- 区间表达式`Range expressions` + + +上面说简洁简洁,到底简洁在哪里?这里先用一个例子开始,在`Java`开发过程中经常会写一些`Bean`类: +```java +package com.charon.kotlinstudydemo; + +public class Person { + private int age; + private String name; + private float height; + private float weight; + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public float getHeight() { + return height; + } + + public void setHeight(float height) { + this.height = height; + } + + public float getWeight() { + return weight; + } + + public void setWeight(float weight) { + this.weight = weight; + } + + @Override + public String toString() { + return "Person name is : " + name + " age is : " + age + " height is :" + + height + " weight is :" + weight; + } +} +``` +使用`Kotlin`: +```kotlin +package com.charon.kotlinstudydemo + +data class Person( + var name: String, + var age: Int, + var height: Float, + var weight: Float) +``` +这个数据类,它会自动生成所有属性和它们的访问器,以及一些有用的方法,比如`toString()`方法。 +这里插一嘴,从上面的例子中我们可以看到对于包的声明基本是一样的,唯一不同的是`kotlin`中后面结束不用分号。 + +## 创建`Kotlin`项目 + +`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 +[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 +直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_create_kotlin.png?raw=true) + +创建完成后我们看一下`MainActivity`的代码: +```kotlin +// 定义包 +package com.charon.kotlinstudydemo + +// 导入 +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +// 定义类,继承AppCompatActivity +class MainActivity : AppCompatActivity() { + + // 重写方法用overide,函数名用fun声明 参数是a: 类型的形式 ?是啥?它是指明该对象可能为null, + // 如果有了?那在调用该方法的时候参数可以传递null进入,如果没有?传递null就会报错 + override fun onCreate(savedInstanceState: Bundle?) { + // super + super.onCreate(savedInstanceState) + // 调用方法 + setContentView(R.layout.activity_main) + } +} +``` + +我们就从`MainActivity`的代码开始介绍一些基本的语法。 + +## 变量 + +变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。如果var代表了varible(变量),那么val可看成value(值)的缩写,但是也有人觉得这样并不直观或准确,而是把val解释成varible+final,即通过val声明的变量具有Java中的final关键字的效果(我们通过查看对val语法反编译后转化的java代码,从中可以很清楚的发现它是用final实现的。),也就是引用不可变。因此,val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。 + +声明: +```kotlin +var age: Int = 18 +val name: String = "charon" + +val book = Book("Thinking in Java") // 用val声明的book对象的引用不可变 +book.name = "Diving into Kotlin" +book.printName() // Diving into Kotlin +``` + +再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 + +字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: +```kotlin +var age = 18 // int +val name = "charon" // string +var height = 180.5f // flat +var weight = 70.5 // double +``` + +在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 +当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 +的,但是有一些不同之处你可能需要考虑到: + +- 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: + ```kotlin + private var age = 18 + private var weight = age.toFloat() + ``` +- 字符(`Char`)不能直接作为一个数字来处理。在需要时我们需要把他们转换为一个数字: + ```kotlin + val c: Char='c' + val i: Int = c.toInt() + ``` +- 位运算也有一点不同。在`Android`中,我们经常在`flags`中使用`或`: + ```java + // Java + int bitwiseOr = FLAG1 | FLAG2; + int bitwiseAnd = FLAG1 & FLAG2; + ``` + + ```kotlin + // Kotlin + val bitwiseOr = FLAG1 or FLAG2 + val bitwiseAnd = FLAG1 and FLAG2 + ``` + +- 一个`String`可以像数组那样访问,并且被迭代: + ```kotlin + var s = "charon" + var c = s[2] + + for (a in s) { + Log.e("@@@", a +""); + } + ``` + +### 优先使用val来避免副作用 + +在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 + +简单来说,副作用就是修改了某处的某些东西,比如说: + +- 修改了外部变量的值 +- IO操作,如写数据到磁盘 +- UI操作,如修改了一个按钮的可操作状态 + +来看一个实际的例子:先用va来声明一个变量a,然后在count函数内部对其进行自增操作: + +```kotlin +val a = 1 +fun count(x: Int) { + a = a + 1 + println(x + a) +} +``` + +如果执行两次count(1)函数,第一次的执行结果是3、第二次的执行结果是4。这显然是受到了外部变量a的影响,这个就是典型的副作用。 + + + +## 编译期常量 + +已知值的属性可以使用`const`修饰符标记为编译期常量(类似`java`中的`public static final`)。 +`const`只能修复`val`不能修复`var`,这些属性需要满足以下要求: +- 位于顶层或者是`object`的一个成员 +- 用`String`或原生类型值初始化 +- 没有自定义`getter` + +```kotlin +// Const val are only allowed on top level or in objects +const val NAME: String = "charon" + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} +``` + +## 后端变量`Backing Fields`. + +在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 + +例如: + +```kotlin +var count = 1 +var size: Int = 2 +set(value) { + Log.e("text", "count : ${count++}") + size = if (value > 10) 15 else 0 +} +``` +这个例子中就会内存溢出。 + +`kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 +我们在使用的时候,用`field`代替属性本身进行操作。 + +## 延迟初始化 + +我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 +假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 +延迟其初始化,此属性用`lateinit`修饰符修饰。 + +```kotlin +class MainActivity : AppCompatActivity() { + lateinit var name : String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + var test = MainActivity() + // 要先调用方法让其初始化 + test.init() + // 再使用其属性 + Log.e("@@@", test.name) + } + + fun init() { + // 延迟初始化 + name = "charon" + } +} +``` +需要注意的是,我们在使用的时候,一定要确保属性是被初始化过的,通常先调用初始化方法,否则会有异常。 +如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的。 +```kotlin +private lateinit var test: String + +private fun switchFragment(position: Int) { + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null") + check(test) + } +} +``` +会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` + +除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: +```kotlin +private val test by lazy { "haha" } + +private fun switchFragment(position: Int) { + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null ${test}") + check(test) + } +} +``` +执行结果: +``` +test is not null haha +``` + +那`lateinit`和`by lazy`有什么区别呢? + +- `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 +- `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 + +lazy的背后是接受一个lambda并返回一个Lazy实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。 + +另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。例如: + +```kotlin +val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { + // 并行模式 + if (color == "yellow") "male" else "female" +} + +val sex: String by lazy(LazyThreadSafetyMode.NONE) { + // 不做任何线程保证也不会有任何线程开销 + if (color == "yellow") "male" else "female" +} +``` + + + +## 类的定义:使用`class`关键字 + +类可以包含: +- 构造函数和初始化块 +- 函数 +- 属性 +- 嵌套类和内部类 +- 对象声明 + + +```kotlin +class MainActivity{ + +} +``` + +如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: +```kotlin +class Person(name: String, age: Int) +``` + +### 创建类的实例 + +```kotlin +val person = Person("charon", 18) +``` + +上面的类有一个默认的构造函数。 + +注意:创建类的实例不用`new`了啊。 + +### 构造函数 + +在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 + +#### 主构造函数 + +主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后: +```kotlin +class Person constructor(name: String, surname: String) { +} +``` +如果主构造函数没有任何注解或者可见性修饰符,可以省略`constructor`关键字: +```kotlin +class Person(name: String, surname: String) { +} +``` + +主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块中: + +```kotlin +class Person constructor(name: String, surname: String) { + init { + print("name is $name and surname is $surname") + } +} +``` + +如果构造函数有注解或可见性修饰符,那么`constructor`关键字是必需的,并且这些修饰符在它前面: +```kotlin +class Person private @Inject constructor(name: String, surname: String) { + init { + print("name is $name and surname is $surname") + } +} +``` + +#### 次构造函数 + +类也可以声明前缀有`constructor`的次构造函数: +```kotlin +class Person{ + constructor(name: String) { + print("name is $name") + } +} +``` + +如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 +委托到同一个类的另一个构造函数用`this`关键字即可: +```kotlin +class Person constructor(name: String) { + constructor(name: String, surName: String) : this(name) { + Log.d("@@@", "name is : $name surName is : $surName") + } +} +``` +使用该对象: +```kotlin +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + Person("charon", "chui") + } +} +``` +就会在`logcat`上打印: +`09-20 16:51:19.738 6010-6010/com.charon.kotlinstudydemo D/@@@: name is : charon surName is : chui` + +如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是`public`。 +如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数: + +```kotlin +class Person private constructor(name: String) { +} +``` + +#### 构造方法默认参数 + +```kotlin +class Bird(val weight: Double = 0.00, val age: Int = 0, val color: String = "blue") + +val bird1 = Bird(color = "black") +val bird2 = Bird(weight = 1000.00, color = "black") +``` + +上面在Bird类中使用了val或者var来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面也使得我们再构造类的语法上得到了简化。事实上,构造方法的参数名前当然可以没有val和var。然而带上它们之后就等价于在Bird类内部声明了一个同名的属性,我们可以用this来进行调用。比如,上面定义的Bird类就类似于一下实现: + +```kotlin +// 构造方法参数名前没有val +class Bird (weight: Double = 0.00, age: Int = 0, color: String = "blue"){ + val weight: Double + val age: Int + val color: String + init { + this.weight = weight // 构造方法参数可以在init语句中被调用 + this.age = age + this.color = color + } +} +``` + +#### init语句块 + +Kotlin引入了一种叫作init语句块的语法,它属于上述构造方法的一部分,两者在表现形式上确实分离的。Bird类的构造方法在类的外部,它只能对参数进行赋值。如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如: + +```kotlin +class Bird(weight: Double, aget: Int, color: String) { + init { + println("the weight is ${weight}") + } +} +``` + +当没有val或者var的时候,构造函数的参数可以在init语句块被直接调用。除此之外,不能在其他地方使用。以下是一个错误的用法: + +```kotlin +class Bird(weight: Double, age: Int, color: String) { + fun printWeight() { + print(weight) // Unresolved reference: weight + } +} +``` + +事实上,我们的构造方法还可以拥有多个init,他们会在对象被创建时按照类中从上到下的顺序先后执行。例如: + +```kotlin +class Bird(weight: Double, aget: Int, color: String) { + val weight: Double + val age: Int + val color: String +} + +init { + this.weight = weight + this.age = age +} +init { + this.color = color +} + +``` + +可以发现,多个init语句块有利于进一步对初始化的操作进行职能分离,这在复杂的业务开发中显得特别有用。 + + + +## 数据类:使用`data class`定义 + +数据类是一种非常强大的类: + +```java +public class Artist { + private long id; + private String name; + private String url; + private String mbid; + + public long getId() { + return id; + } + + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getMbid() { + return mbid; + } + + public void setMbid(String mbid) { + this.mbid = mbid; + } + + @Override public String toString() { + return "Artist{" + + "id=" + id + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + ", mbid='" + mbid + '\'' + + '}'; + } +} +``` + +使用`Kotlin`: + +```kotlin +data class Artist( + var id: Long, + var name: String, + var url: String, + var mbid: String) +``` + +通过数据类,会自动提供以下函数: + +- 所有属性的`get() set()`方法 +- `equals()` +- `hashCode()` +- `copy()` +- `toString()` +- 一系列可以映射对象到变量中的函数(后面再说)。 + +如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。 +这个任务是非常重复且不简洁的。 + +举个例子,如果要修改`Person`类中`charon`的`age`: + +```kotlin +data class Person(val name: String, + val age: Int) +``` + +```kotlin +val charon = Person("charon", 18) +val charon2 = charon.copy(age = 19) +``` + +如上,我们拷贝了`charon`对象然后只修改了`age`的属性而没有修改这个对象的其它状态。 + +如果你要在Kotlin声明一个数据类,必须满足以下几点条件: + +- 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的。 +- 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明 +- data class之前不能用abstract、open、sealed或者inner进行修饰 +- 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 + + + + + +## 多声明 + +多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 + +```kotlin +var personD = PersonData("PersonData", 20, "male") +var (name, age) = personD + + +Log.d("test", "name = $name, age = $age") + +//输出 +name = PersonData, age = 20 +``` + +上面的多声明,大概可以翻译成这样: + +```kotlin +var name = f1.component1() +var age = f1.component2() +``` + +## 继承 + +在`Kotlin`中所有类都有一个共同的超类`Any`,这对于没有超类型声明的类是默认超类: + +```kotlin +class Person // 从 Any 隐式继承 +``` + +`Any`不是`java.lang.Object`。它除了`equals()`、`hashCode()`和`toString()`外没有任何成员。 +`Kotlin`中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, +要么就禁止继承。所以我们只能继承那些明确声明`open`或者`abstract`的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后: + +```kotlin +open class Person(num: Int) +// 继承 +class SuperPerson(num: Int) : Person(num) +``` + +如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 +如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 +注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: + +```kotlin +class MyView : View { + constructor(ctx: Context) : super(ctx) + constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) +} +``` + +在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是open,如果不加open,那它在转化为Java代码时就是final的: + +```kotlin +class Bird { + val weight: Double = 500.0 + val color: String = "blue" + val age: Int = 1 + fun fly() {} +} +``` + +将Bird类编译后转换为Java的代码: + +```java +public final class Bird { + private final double weight = 500.0; + private final String color = "blue"; + private final int age = 1; + public final double getWeight() { + return this.weight; + } + public final String getColor() { + return this.color; + } + public final int getAge() { + return this.age; + } + public final void fly() { + + } +} +``` + + + +## 覆盖 + +##### 方法覆盖 + + +只能重写显示标注可覆盖的方法: + +```kotlin +open class Person(num: Int) { + open fun changeName(name: String) { + + } + + fun changeAge(age: Int) { + + } +} + +class SuperPerson(num: Int) : Person(num) { + override fun changeName(name: String) { + // 通过super关键字调用超类实现 + super.changeName(name) + } +} +``` + +`SuperPerson.changeName()`方法前面必须加上`override`标注,不然编译器将会报错。如果像上面`Person.changeAge()`方法没有标注`open`, +则子类中不能定义相同的方法: + +```kotlin +class SuperPerson(num: Int) : Person(num) { + override fun changeName(name: String) { + super.changeName(name) + } + + // 编译器报错 + fun changeAge(age: Int) { + + } + // 重载是可以的 + fun changeAge(name: String) { + + } + // 重载是可以的 + fun changeAge(age: Int, name: String) { + + } +} +``` + +标记为`override`的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,可以使用`final`关键字: + +```kotlin +open class SuperPerson(num: Int) : Person(num) { + final override fun changeName(name: String) { + super.changeName(name) + } +} +``` + +##### 属性覆盖 + +属性覆盖与方法覆盖类似,只能覆盖显示标明`open`的属性,并且要用`override`开头: + +```kotlin +open class Person(num: Int) { + open val name: String = "" + + open fun changeName(name: String) { + + } + + fun changeAge(age: Int) { + + } +} + +open class SuperPerson(num: Int) : Person(num) { + override val name: String + get() = super.name + + final override fun changeName(name: String) { + super.changeName(name) + } + +} +``` + +每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 + + + +## 抽象类 + +类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 +言而喻。 + +我们可以用一个抽象成员覆盖一个非抽象的开放成员: + +```kotlin +open class Base { + open fun f() {} +} + +abstract class Derived : Base() { + override abstract fun f() +} +``` + + + +## 注释 + +和`Java`差不多 + +```kotlin +// 这是一个行注释 + +/* 这是一个多行的 + 块注释。 */ +``` + + + + + + +## 接口:使用`interface`关键字 + +```kotlin +interface FlyingAnimal { + fun fly() +} +``` + +虽然Kotlin接口支持属性声明,然而它在Java源码中是通过一个get方法来实现的。在接口的属性并不能像Java接口那样,被直接赋值一个常量。如以下这样是错误的: + +```kotlin +interface Flyer { + val height = 1000 // error Property initializers are not allowed in interfaces + val speed: Int + // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的 + fun fly() { + println("I can fly") + } +} +``` + +Kotlin提供了另外一种方式来实现这种效果: + +```kotlin +interface Flyer { + val height + get() = 1000 +} +``` + + + +## 函数:通过`fun`关键字定义 + +```kotlin +fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) +} +``` +如果你没有指定它的返回值,它就会返回`Unit`与`Java`中的`void`类似,但是`Unit`是一个类型,而void只是一个关键字。`Unit`可以省略。 +你当然也可以指定任何其它的返回类型: + +```kotlin +fun maxOf(a: Int, b: Int): Int { + if (a > b) { + return a + } else { + return b + } +} +``` + +### 表达式函数体 + +然而如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号: + +```kotlin +fun add(x: Int,y: Int) : Int = x + y // 省略了{} +``` + +Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 + + + +我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: + +```kotlin +fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, message, length).show() +} +``` +上面代码中第二个参数`length`指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数: + +```kotlin +toast("Hello") +toast("Hello", Toast.LENGTH_LONG) +``` + +### 自定义`get set`方法: + +`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: +`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: + +```kotlin +class Person constructor() { + var name: String = "" + get() = field + set(value) { + field = "$value" + } + + var age: Int = 0 + get() = field + set(value) { + field = value + } +} +``` +按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 + +### 可变长参数函数:使用`vararg`关键字 + +```kotlin +fun vars(vararg v:Int){ + for(vt in v){ + print(vt) + } +} + +// 测试 +fun main(args: Array) { + vars(1,2,3,4,5) // 输出12345 +} +``` + +### 命名风格 + +如果拿不准的时候,默认使用`Java`的编码规范,比如: + +- 使用驼峰法命名(并避免命名含有下划线) +- 类型名以大写字母开头 +- 方法和属性以小写字母开头 +- 使用4个空格缩进 +- 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 + + +### 冒号 + +类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: + +```kotlin +interface Foo : Bar { + fun foo(a: Int): T +} +``` + +### 类头格式化 + +有少数几个参数的类可以写成一行: + +```kotlin +class Person(id: Int, name: String) +``` + +具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。 此外,右括号应该另起一行。如果我们使用继承, +那么超类构造函数调用或者实现接口列表应位于与括号相同的行上: + +```kotlin +class Person( + id: Int, + name: String, + surname: String +) : Human(id, name) { + // …… +} +``` + +对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中: + +```kotlin +class Person( + id: Int, + name: String, + surname: String +) : Human(id, name), + KotlinMaker { + // …… +} +``` + +### `Unit`:让函数调用皆为表达式 + +如果函数返回`Unit`类型,该返回类型应该省略: + +```kotlin +fun foo() { // 省略了 ": Unit" + +} +``` + +之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: + +```java +void foo() { + System.out.println("return nothing") +} +``` + +所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 + +Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 + + + +[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" new file mode 100644 index 00000000..5a3a5989 --- /dev/null +++ "b/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -0,0 +1,458 @@ +11.Kotlin_设计模式 +=== + + + +## 工厂模式 + +简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。 + +假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑: + +```kotlin +interface Computer { + val cpu: String +} +class PC(override val cpu: String = "Core") : Computer +class Server(override val cpu: String = "Xeon") : Computer + +enum class ComputerType { + PC, Server +} + +class ComputerFactory { + fun produce(type: ComputerType): Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + val pc = ComputerFactory().produce(ComputerType.PC) + println(pc.cpu) // Core +} +``` + +以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 + +### 用单例代替工厂类 + +我们已经知道Kotlin支持用object来实现Java中的单例模式。所以可以实现一个ComputerFactory单例,而不是一个工厂类: + +```kotlin +object ComputerFactory { + fun produce(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + // 这样我们就不用再每次都创建对象了 + val pc = ComputerFactory.produce(ComputerType.PC) + println(pc.cpu) +} +``` + +由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符重载invoke方法来代替produce,从而进一步简化表达: + +```kotlin +object ComputerFactory { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } +} +fun main() { + // 这样就会非常简洁 + val pc = ComputerFactory(ComputerType.PC) + println(pc.cpu) +} +``` + + + +### 伴生对象创建静态工厂方法 + +上面的工厂模式实现已经足够优雅,然而依旧不够完美: 我们是否可以直接通过Computer()而不是ComputerFactory()来创建一个实例呢? + +我们可以通过在Computer接口中定义一个伴生对象,这样就能实现以上的需求: + +```kotlin +interface Computer { + val cpu: String + companion object { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } + } +} + +fun main() { + val pc = Computer(ComputerType.PC) + println(pc.cpu) +} +``` + +我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用Factory来命名Computer的伴生对象,如下: + +```kotlin +interface Computer { + val cpu: String + companion object Factory { + operator fun invoke(type: ComputerType) : Computer { + return when (type) { + ComputerType.PC -> PC() + ComputerType.Server -> Server() + } + } + } +} + +fun main() { + val pc = Computer.Factory(ComputerType.PC) + println(pc.cpu) +} +``` + + + +### 扩展伴生对象方法 + +依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: + +比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么可以如下实现: + +```kotlin +fun Computer.Factory.fromCPU(cpu: String) : ComputerType? = when(cpu) { + "Core" -> ComputerType.PC + "Xeon" -> ComputerType.Server + else -> null +} +fun main() { + val pc = Computer.Factory.fromCPU("Core") + println(pc) +} +``` + + + +### 内联函数简化抽象工厂 + +Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。 + + + +##### 抽象工厂模式 + +为创建一组相关或相互依赖的对象提供一个接口,而且无须指定他们的具体类。 + +```kotlin +interface Computer +class Dell: Computer +class Asus: Computer +class Acer: Computer + +class DellFactory: AbstractFactory() { + override fun produce() = Dell() +} +class AsusFactory: AbstractFactory() { + override fun produce() = Asus() +} +class AcerFactory: AbstractFactory() { + override fun produce() = Acer() +} + +abstract class AbstractFactory { + abstract fun produce(): Computer + companion object { + operator fun invoke(factory: AbstractFactory): AbstractFactory { + return factory + } + } +} +fun main(args: Array) { + val dellFactory = AbstractFactory(DellFactory()) + val dell = dellFactory.produce() + println(dell) +} +``` + +可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 + +由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 + +```kotlin +abstract class AbstractFactory { + abstract fun produce(): Computer + companion object { + // 增加reified关键字 + inline operator fun invoke(): AbstractFactory = + when (T::class) { + Dell::class -> DellFactory() + Asus::class -> AsusFactory() + Acer::class -> AcerFactory() + else -> throw IllegalArgumentException() + } + } +} +``` + +这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码: + +- 通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性。 +- 要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。 + +再来看看通过上面内联函数改善后的工厂类的创建语法表达: + +```kotlin +fun main(args: Array) { + val dellFactory = AbstractFactory() + val dell = dellFactory.produce() + println(dell) +} +``` + +现在终于可以用类似创建一个泛型类对象的方式,来构建一个抽象工厂具体对象了。 + + + +## 构造者模式 + +构造者模式与单例模式一样,它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 + +工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 + +一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的确定,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 + +构建者模式可以避免以上问题,我们用Kotlin来实现Java中的构建者模式: + +```kotlin +class Robot private constructor ( + val code: String, + val battery: String?, + val height: Int?, + val weight: Int?) { + + class Builder(val code: String) { + private var battery: String? = null + private var height: Int? = null + private var weight: Int? = null + + fun setBattery(battery: String?): Builder { + this.battery = battery + return this + } + + fun setHeight(height: Int): Builder { + this.height = height + return this + } + + fun setWeight(weight: Int): Builder { + this.weight = weight + return this + } + fun build(): Robot { + return Robot(code, battery, height, weight) + } + } + } +} + +var robot = Robot.Builder("007") + .setBattery("R6") + .setHeight(100) + .setWeight(80) + .build() +``` + +为了避免代码太长,上面的例子中只选择了4个属性,其中code是必须属性,battery、height、weight为可选属性。我们来分析一下它的具体思路: + +- Robot类内部定义了一个嵌套类Builder,由它负责创建Robot对象 +- Robot类的构造函数用private进行修饰,这样可以确保使用者无法直接通过Robot声明实例 +- 通过在Builder类中定义set方法来对可选的属性进行设置 +- 最终调用Builder类中的build方法来返回一个Robot对象 + +这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 + +然而,构建者模式也存在一些不足: + +- 如果业务需求的参数很多,代码依然会显得比较长 +- 你可能会在使用Builder的时候忘记在最后调用build方法 +- 由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。 + +事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 + +#### 具名的可选参数 + +Kotlin中的函数和构造器都支持这一特性,它主要表现为两点: + +- 在具体化一个参数的取值时,可以通过带上它的参数名,而不是它在所有参数中的位置决定。 +- 由于参数可以设置默认值,这允许我们只给出部分参数的取值,而不必是所有的参数。 + +因此,我们可以直接使用Kotlin中原生的语法特性来实现构建者模式的效果。现在重新设计以上的Robot例子: + +```kotlin +class Robot( + val code: String, + val battery: String? = null, + val height: Int? = null, + val weight: Int? = null +) + + +private fun main() { + val robot1 = Robot(code = "007") + val robot2 = Robot(code = "007", battery = "R6") + val robot3 = Robot(code = "007", height = 100, weight = 80) + + println(robot1) +} +``` + +可以发现,相比构建者模式,通过具名的可选参数构造类具有很多优点: + +- 代码变得十分简单,这不仅表现在Robot类的结构体代码量,我们在声明Robot对象时的语法也要更加简洁 +- 声明对象时,每个参数名都可以是显式的,并且无须按照顺序书写,非常方便灵活 +- 由于Robot类的每个对象都是val声明的,相较构建者模式中的var的方案更加安全,这在要求多线程并发安全的业务场景中会显得更有优势。 + +此外,如果你的类的功能足够简单,更好的思路是用data class直接声明一个数据类。数据类同样支持以上的所有特性。 + + + +#### require方法对参数进行约束 + +我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: + +```kotlin +fun build(): Robot { + if (weight != null && battery == null) { + throw IllegalArgumentException("Battery should be determined when setting weight.") + } else { + return Robot(code, battery, height, weight) + } +} +``` + +这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢? + +显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 + +```kotlin +class Robot( + val code: String, + val battery: String? = null, + val height: Int? = null, + val weight: Int? = null +) { + init { + require(weight == null || battery != null) { + "Battery should be determined when setting weight." + } + } +} +``` + +可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式,因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 + + + + + +## 观察者模式 + +观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 + +简单来说,观察者模式无非做两件事情: + +- 订阅者(observer)添加或删除对发布者(publisher)的状态监听。 +- 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。 + +Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用它们来实现一个动态更新股价的例子。 + +```kotlin +class StockUpdate: Observable() { + val observers = mutableSetOf() + fun setStockChanged(price: Int) { + this.observers.forEach { it.update(this, price)} + } +} +class StockDisplay: Observer { + override fun update(o: Observable, price: Any) { + if (o is StockUpdate) { + println("The latest stock price is ${price}") + } + } +} +fun main(args: Array) { + val su = StockUpdate() + val sd = StockDisplay() + su.observers.add(sd) + su.setStockChanged(100) +} +``` + +上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。 + +我们可以先用这一委托属性来改造以上的程序: + +```kotlin +import kotlin.properties.Delegates + +interface StockUpdateListener { + fun onRise(price: Int) + fun onFall(price: Int) +} + +class StockDisplay: StockUpdateListener { + override fun onRise(price: Int) { + println("The latest stock price has risen to ${price}") + } + override fun onFall(price: Int) { + println("The latest stock price has fell to ${price}") + } +} + +class StockUpdate { + var listeners = mutableSetOf() + var price: Int by Delegates.observable(0) {_, old, new -> + listeners.forEach { + if (new > old) it.onRise(price) else it.onFall(price) + } + } +} +fun main(args: Array) { + val su = StockUpdate() + val sd = StockDisplay() + su.listeners.add(sd) + su.price = 100 + su.price = 98 +} +// 执行结果 +The latest stock price has risen to 100 +The latest stock price has fell to 98 +``` + +如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的API接口只有一个,API调用者必须承担更多的职责。 + +显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRise和onFall方法,实现了解耦。 + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" new file mode 100644 index 00000000..23458c79 --- /dev/null +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -0,0 +1,742 @@ +Kotlin之Lambda&内联函数(七) +=== + + + +函数式语言一个典型的特征就在于函数是头等公民-我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个函数,例如: + +```kotlin +fun foo(x: Int) { + fun double(y: Int): Int { + return y * 2 + } + println(double(x)) +} + +执行foo(1)结果为2 +``` + + + +## 高阶函数 + +Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。 + +```kotlin +fun foo(x: Int) { + fun double(y: Int): Int { + return y * 2 + } + println(double(x)) +} +``` + +### 抽象和高阶函数 + +我们会善于对熟悉 或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 + +过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。 + +然而,在我们以往熟悉的编程中,过程限制为只能接收数据为参数,这个无疑限制了进一步抽象的能力。 + +由于我们经常会遇到一些同样的程序设计模式能够用于不同的过程,比如一个包含了正整数的列表,需要对它的元素进行各种转换操作,例如对所有元素都乘以3,或者都除以2。我们就需要提供一种模式,同时接收这个列表及不同的元素操作过程,最终返回一个新的列表。 + +为了把这种类似的模式描述为相应的概念,我们就需要构造出一种更加高级的过程,表现为:接收一个或多个过程为参数,或者以一个过程作为返回结果。这个就是所谓的高阶函数,你可以把它理解为“以其他函数作为参数或返回值的函数”。高阶函数是一种更加高级的抽象机制,它极大地增强了语言的表达能力。 + + + +### 实例: 函数作为参数的需求 + +Shaw因为旅游喜欢上了地理,然后他建了一个所有国家的数据库。作为一名程序员,他设计了一个CountryApp类对国家数据进行操作。Shaw偏好欧洲的国家,于是他设计了一个程序来获取欧洲的所有国家。 + +```kotlin +data class Country { + val name: String, + val continient: String, + val population: Int +} + +class CountryApp { + fun filterCountries(countries: List): List { + val res = mutableListOf() + for (c in countries) { + if (c.continent == "EU") { // EU代表欧洲 + res.add(c) + } + } + return res + } +} +``` + +后来Shaw对非洲也产生了兴趣,于是他又改进了上述方法的实现,支持根据具体的州来筛选国家。 + +```kotlin +fun filterCountries(countries: List, continient: String): List { + val res = mutableListOf() + for (c in countries) { + if (c.continient == continient) { + res.add(c) + } + } + return res +} +``` + +以上程序具备了一定的复用性。然而,Shaw的地理知识越来越丰富了,他想对国家的特点做进一步的研究,比如筛选具有一定人口规模的国家,于是代码又变成下面这个样子: + +```kotlin +fun filterCountries(countries: List, continient: String, population: Int) : List { + val res = mutableListOf() + for (c in countries) { + if (c.continient == continient && c.population > population) { + res.add(c) + } + } + return res +} +``` + +新增了一个population的参数来代表人口(单位:万)。Shaw开始感觉到不对劲,如果按照现有的设计,更多的筛选条件会作为方法参数而不断增加,而且业务逻辑也会高度耦合。 + +解决问题的核心在于对filterCountries方法进行解耦,我们能否把所有的筛选逻辑行为都抽象成一个参数呢?传入一个类对象是一种解决方法,我们可以根据不同的筛选需求创建不同的子类,它们都各自实现了一个校检方法。然而,Shaw了解到Kotlin是支持高阶函数的,理论上我们同样可以把筛选的逻辑变成一个方法来传入,这样思路更简单。 + +他想要进一步了解高级的特性,所以很快写了一个新的测试类: + +```kotlin +class CountryTest { + fun isBigEuropeanCountry(country: Country): Boolean { + return country.continient == "EU" && country.population > 10000 + } +} +``` + +调用isBigEuropeanCountry方法就能够判断一个国家是否是一个人口超过1亿的欧洲国家。然而,怎样才能把这个方法变成filterCountries方法的一个参数呢?要实现这一点似乎要先解决以下两个问题: + +- 方法作为参数传入,必须像其他参数一样具备具体的类型信息 + + 在kotlin中,函数类型的格式非常简单: + + - 通过 -> 符号来组织参数类型和返回值类型,左边是参数类型,右边是返回值类型 + - 必须用一个括号来包裹参数类型,如果是一个没有参数的函数类型,参数类型部分就用()表示 + - 返回值类型即使是Unit,也必须显式声明 + + 举个例子: + + ```kotlin + (Int) -> Unit + () -> Unit + (Int, String) -> Unit + // 还支持为声明参数指定名字 + (errCode: Int, errMsg: String) -> Unit + // ?表示可选,可在某种情况下为空 + ((errCode: Int, errMsg: String?) -> Unit)? + ``` + + 在学习了Kotlin函数类型知识之后,Shaw重新定义了filterCountries方法的参数声明: + + ```kotlin + // 增加了一个函数类型的参数test + fun filterCountries(countries: List, test: (Country) -> Boolean): List { + val res = mutableListOf() + for (c in countries) { + // 直接调用test函数来进行筛选 + if (test(c)) { + res.add(c) + } + } + return res + } + ``` + + 接下来就是如何把isBigEuropeanCountry方法传递给filterCountries呢? 直接把isBigEuropeanCountry当参数肯定不行,因为函数名并不是一个表达式不具有类型信息。所以我们需要的是一个单纯的方法引用表达式。 + +- 需要把isBigEuropeanCountry的方法引用当做参数传递给filterCountries + + Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写: + + ```kotlin + countryTest::isBigEuropeanCountry + ``` + + 于是,Shaw便使用了方法引用来传递参数: + + ```kotlin + val countryApp = CountryApp() + val countryTest = CountryTest() + val countries = ... + + countryApp.filterContries(countries, countryTest::isBigEuropeanCountry) + ``` + +经过重构后的程序显然比之前要优雅许多,程序可以根据任意的筛选需求,调用同一个filterCountries方法来获取国家数据。 + +#### 方法引用表达式更多使用场景 + +此外,我们还可以直接通过这种语法,来定义一个类的构造方法引用变量。 + +```kotlin +class Book(val name: String) { + fun main(args: Array) { + val getBook = ::Book + println(getBook("Dive into Kotlin").name) + } +} +``` + +可以发现,getBook类型为(name: String) -> Book。类似的道理,如果我们要引用某个类的成员变量,如Book类中的name,就可以这样引用: + +```kotlin +Book::name +``` + +以上创建的Book::name的类型为(Book) -> String。 + + + +## 匿名函数 + +再来思考下上面代码中的CountryTest类,这仍算不上是一种很好的方案。因为每增加一个需求,我们都需要在类中专门写一个新增的筛选方法。然而Shaw的需求很多都是临时性的,不需要被复用。Shaw觉得这样还是比较麻烦,他打算用匿名函数对程序进一步的优化。 + +Kotlin支持在缺省函数名的情况下,直接定义一个函数。所以isBigEuropeanCountry方法我们可以直接定义为: + +```kotlin +// 没有函数名字 +fun(country: Country): Boolean { + return country.continient == "EU" && country.population > 10000 +} +``` + +于是,Shaw直接调用filterCountries,如下: + +```kotlin +countryApp.filterCountries(countries, fun(country: Country): Boolean) { + return country.continient == "EU" && country.population > 10000 +}) +``` + +这一次我们甚至不需要CountryTest这个类了,代码的简洁性又上了一层楼。Shaw开始意识到Kotlin这门语言的魅力,很快他发现还有一种语法可以让代码更简单,这就是Lambda表达式。 + +我们继续看上面的filterCountries方法的匿名函数,会发现: + +- fun(country: Country)显得比较啰嗦,因为编译器会推导类型,所以只需要一个代表变量的country就行了。 +- return关键字也可以省略,这里返回的是一个有值的表达式 +- 模仿函数类型的语法,我们可以用 -> 把函数和返回值连接在一起 + +因此,简化后的表达就变成了这个样子: + +```kotlin +countryApp.filterCountries(countries, { + country -> + country.continient == "EU" && country.population > 10000 +}) +``` + +这就是Lambda表达式,它与匿名函数一样,是一种函数字面量。 + +Lambda的语法: + +- 一个Lambda表达式必须通过{}来包裹 +- 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明 +- 如果Lambda变量声明了函数类型,那么Lambda的参数部分的类型就可以省略 + +此外,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值类型就是返回值类型,如: + +```kotlin +val foo = { x: Int -> + val y = x + 1 + y // 返回值是y +} +``` + +## Lambda表达式 + + +> “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 +> `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 + +`Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 + +```java +// 没有使用Lambda的老方法: +button.addActionListener(new ActionListener(){ + public void actionPerformed(ActionEvent ae){ + System.out.println("Actiondetected"); + } +}); +// 使用Lambda: +button.addActionListener(()->{ + System.out.println("Actiondetected"); +}); + + +// 不采用Lambda的老方法: +Runnable runnable1=new Runnable(){ + @Override + public void run(){ + System.out.println("RunningwithoutLambda"); + } +}; +// 使用Lambda: +Runnable runnable2=()->{ + System.out.println("RunningfromLambda"); +}; +``` + +`Lambda`能让代码更简洁,Kotlin的支持如下: + +- `lambda`表达式总是被大括号括着 +- 其参数(如果有的话)在`->`之前声明(参数类型可以省略), +- 函数体(如果存在的话)在`->`后面。 + +`Lambda`表达式是定义匿名函数的简单方法。由于`Lambda`表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分, +所以它是非常有用的。在`Kotlin`语言中,可以将一函数作为另一函数的参数。 + +`Lambda`表达式由箭头左侧函数的参数(在圆括号里的内容)定义的,将值返回到箭头右侧。 +`view.setOnClickListener({ view -> toast("Click")})` +在定义函数时,必须在箭头的左侧用方括号,并指定参数值,而函数的执行代码在箭头右侧。如果左侧不使用参数,甚至可以省去左侧部分: +`view.setOnClickListener({ toast("Click") })` +如果函数的最后一个参数是一个函数的话,可以将作为参数的函数移到圆括号外面: +`view.setOnClickListener() { toast("Click") }` + + +先看一个例子: + +```kotlin +fun compare(a: String, b: String): Boolean { + return a.length < b.length +} +max(strings, compare) +``` +就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。 +```kotlin +max(strings, (a,b)->{a.length < b.length}) +``` + +`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: + +- 参数的类型 +- 返回值的类型 + +但这些真的必要吗?`a.length < b.length`很明显返回一个`Boolean`的值,再就是`max`的定义中肯定也定义了这个函数的参数类型和返回值类型。 +这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和`Lambda`很接近了。 + +```kotlin +val sum: (Int, Int) -> Int = { x, y -> x + y } +``` + +`Lambda`表达式就是被大括号括着的那一部分,在`->`符号之前有参数声明,函数体跟在一个`->`符号之后。 +而且此`Lambda`表达式之前有一个匿名的函数声明(在此例中两个`Int`型的输入,一个`Int`型的返回值),这个声明是可以不使用的。 +则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 +返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 + +## Lambda开销 + +```kotlin +fun foo(int: Int) = { + print(int) +} +listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo +``` + +这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于: + +```kotlin +listOf(1, 2, 3).forEach { item -> foo(item) } +``` + +默认情况下,我们可以直接用it来代表item,而不需要用item -> 进行声明。 + +我们看一下foo函数用IDE转换后的Java代码: + +```java +@JvmStatic +@NotNull +public static final Function0 foo(final int var0) { + return (Function0)(new Function0() { + // $FF: synthetic method + // $FF: bridge method + public Ojbect invoke() { + this.invoke(); + return Unit.INSTANCE; + } + public final void invoke() { + int var1 = var0; + System.out.printlln(var1); + } + }); +} +``` + +以上是字节码反编译的Java代码,从中我们可以发现Kotlin实现Lambda表达式的机理。 + +### Function类型 + +Kotlin在JVM层设计了Function类型(Function0、Function1 ... Function22、FunctionN)来兼容Java的Lambda表达式,其中的后缀数字代表了Lambda参数的数量,如以上的foo函数构建的其实是一个无参Lambda,所以对应的接口是Function0,如果有一个参数那么对应的就是Function1.它在源码是如下定义的: + +```kotlin +package kotlin.jvm.functions +interface Function1 : kotlin.Function { + fun invoke(p1: P1) : R +} +``` + +可见每个Function类型都有一个invoke方法。设计Function类型的主要目的之一就是要兼容Java,实现在Kotlin中也能调用Java的Lambda。在Java中,实际上并不支持把函数作为参数,而是通过函数式接口来实现这一特性。 + +foo函数的返回类型是Function()。这也意味着,如果我们调用了foo(n),那么实质上仅仅是构造了一个Function()对象。这个对象并不等价于我们要调用的过程本身。通过源码可以发现,需要调用Function()的invoke方法才能执行println方法。所以上面的例子必须如下修改,才能最终打印出我们想要的结果: + +```kotlin +fun foo(int: Int) = { + print(int) +} +listOf(1, 2, 3).forEach { foo(it).invoke() } // 增加了invoke调用 +``` + + + +但是invoke这种语法显得丑陋,不符合Kotlin简洁表达的设计理念,所以我们还可以用熟悉的括号调用来替代invoke,如下所示: + +```kotlin +listOf(1, 2, 3).forEach{ foo(it)() } +``` + +#### 闭包 + +在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。 + +一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。 + +与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改,如下: + +```kotlin +var sum = 0 +listOf(1, 2, 3).filter { it > 0 }.forEach { + sum += it +} +println(sum) // 6 +``` + +## 内联函数 + +在我开始学的时候,一直没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。 + + + +## 优化Lambda开销 + +在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 + +#### 1. invokedynamic + +在讲述内联函数具体的语法之前,我们先来看看Java中是如何解决这个问题的。与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE 7之后通过invokedynamic技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。这种做法的好处主要体现在: + +- 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。 +- 与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现, 这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对编译策略不断优化升级 +- JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,这极大地提升了开发的体验。 + + + +#### 2. 内联函数 + +invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。 + +因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 + +所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。 + +这里通过一个实际的例子,看看Kotlin的内联函数是具体如何操作的: + +```kotlin +fun main(args: Array) { + foo { + println("dive into Kotlin...") + } +} + +fun foo(block: () -> Unit) { + println("before block") + block() + println("end block") +} +``` + +首先,我们声明了一个高阶函数foo,可以接受一个类型为() -> Unit的Lambda,然后在main函数中调用它。以下是通过字节码反编译的相关Java代码: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + foo((Function0)null.INSTANCE); +} + +public static final void foo(@NotNull Function0 block) { + Intrinsics.checkParameterIsNotNull(block, "block"); + String var1 = "before block"; + System.out.println(var1); + block.invoke(); + var1 = "end block"; + System.out.println(var1); +} +``` + +据我们所知,调用foo就会产生一个Function()类型的block类,然后通过invovke方法来执行,这会增加额外的生成类和调用开销。现在,我们给foo函数加上inline修饰符,如下: + +```kotlin +inline fun foo(block: () -> Unit) { + println("before block") + block() + println("end block") +} +``` + +再来看看相应的Java代码: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + String va1 = "before block"; + System.out.println(var1); + // block函数体在这里开始粘贴 + String var2 = "dive into Kotlin..."; + System.out.println(var2); + // block函数体在这里结束粘贴 + var1 = "end block"; + System.out.println(var1); +} + +public static final void foo(@NotNull Function0 block) { + Intrinsics.checkParameterIsNotNull(block, "block"); + String var2 = "before block"; + System.out.println(var2); + block.invoke(); + var2 = "end block"; + System.out.println(var2); +} +``` + +果然,foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置。试想下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法势必会被调用很多次。通过inline的语法,我们可以彻底消除这种额外调用,从而节省了开销。 + +内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过Kotlin的集合类API文档或者源码实现就会发现,集合函数式API,如map、filter都被定义成内联函数,如: + +```kotlin +inline fun Array.map { + transform: (T) -> R +}: List + +inline fun Array.filter { + predicate: (T) -> Boolean +}: List +``` + +这个很容易理解,由于这些方法都接收Lambda作为参数,同时都需要对集合元素进行遍历操作,所以把相应的实现进行内联无疑是非常适合的。 + +但是内联函数不是万能的,以下情况我们应避免使用内联函数: + +- 由于JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。 +- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。 +- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把他们声明为internal。 + + + +#### noinline: 避免参数被内联 + +通过上面的例子我们已经知道,如果在一个函数的开头加上inline修饰符,那么它的函数体及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接受多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,这个又该如何处理? + +解决这个问题也很简单,Kotlin在引入inline的同时,也新增了noinline关键字,我们可以把它加在不想要被内联的参数开头,该参数便不会具有内联的效果: + +```kotlin +fun main(args: Array) { + foo ( { + println("I am inlined...") + }, { + println("I am not inlined...") + }) +} + +inline fun foo(block1: () -> Unit, noinline block2: () -> Unit) { + println("before block") + block1() + block2() + println("end block") +} + +``` + +同样的方法,再来看看反编译的Java版本: + +```java +public static final void main(@NotNull String[] args) { + Intrinsics.checkParameterIsNotNull(args, "args"); + Function0 block2$iv = (Function0)null.INSTANCE; + String var2 = "before block"; + System.out.println(var2); + // block1 被内联了 + String var3 = "I am inlined..."; + System.out.println(var3); + // block2 还是原样 + block2$iv.invoke(); + System.out.println(var2); +} +public static final void foo(@NotNull Function0 block1, @NotNull Function0 block2) { + Intrinsics.checkParameterIsNotNull(block1, "block1"); + Intrinsics.checkParameterIsNotNull(block2, "block2"); + String var3 = "before block"; + System.out.println(var3); + block1.invoke(); + block2.invoke(); + var3 = "end block"; + System.out.println(var3); +} +``` + +可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。 + +#### 非局部返回 + +Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。 + +以下是我们常见的局部返回的例子: + +```kotlin +fun main(args: Array) { + foo() +} +fun localReturn() { + return +} +fun foo() { + println("before local return") + localReturn() + println("after local return") + return +} +// 运行结果 +before local return +after local return +``` + +正如我们所熟知的,localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。我们再把这个函数换成Lambda表达式的版本: + +```kotlin +fun main(args: Array) { + foo { return } +} +fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +Error:(2, 11)Kotlin: 'return' is not allowed here +``` + +这时,编译器报错了,就是说在Kotlin中,正常情况下Lambda表达式不允许存在return关键字。这时候,内联函数又可以排上用场了。我们把foo进行内联后再试试看: + +```kotlin +fun main(args: Array) { + foo { return } +} +inline fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +before local return +``` + +编译顺利通过了,但结果与我们的局部返回效果不同,Lambda的return执行后直接让foo函数退出了执行。如果你仔细考虑一下,可能很快就想出了原因。因为内联函数foo的函数体及参数Lambda会直接替代具体的调用。所以实际产生的代码中,retrurn相当于是直接暴露在main函数中,所以returning()之后的代码自然不会执行,这个就是所谓的非局部返回。 + +#### 使用标签实现Lambda非局部返回 + +另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。同样以上的例子,我们可以在不声明inline修饰符的情况下,这么做来实现相同的效果: + +```kotlin +fun main(args: Array) { + foo { return@foo } +} +fun foo(returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +before local return +``` + +非局部返回尤其在循环控制中显得特别有用,比如Kotlin的forEach接口,它接收的就是一个Lambda参数,由于它也是一个内联函数,所以我们可以直接在它调用的Lambda中执行return退出上一层的程序。 + +```kotlin +fun hasZeros(list: List): Boolean { + list.forEach { + if (it == 0) return true // 直接返回foo函数结果 + } + return false +} +``` + +#### crossinline + +值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子: + +```kotlin +fun main(args: Array) { + foo { return } +} +inline fun foo(crossinline returning: () -> Unit) { + println("before local return") + returning() + println("after local return") + return +} +// 运行结果 +Error: (2, 11) Kotlin: 'return' is not allowed here +``` + +#### 具体化参数类型 + +除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。 + +```kotlin +fun main(args: Array) { + getType() +} +inline fun getType() { + print(T::class) +} +// 运行结果 +class kotlin.Int +``` + +这个特性在Android开发中也格外有用。比如在Java中,当我们要调用startActivity时,通常需要把具体的目标视图类作为一个参数。然而,在Kotlin中,我们可以用reified来进行简化: + +```kotlin +inline fun Activity.startActivity() { + startActivity(Intent(this, T::class.java)) +} +``` + +这样,我们进行视图导航就非常容易了,如: + +```kotlin +startActivity() +``` + + + + + + + + + +[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" new file mode 100644 index 00000000..44f17930 --- /dev/null +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -0,0 +1,554 @@ +Kotlin学习教程(三) +=== + +前面介绍了基本语法和编码规范后,接下来学习下基本类型。 + +在`Kotlin`中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。 一些类型可以有特殊的内部表示——例如, +数字、字符和布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述`Kotlin`中使用的基本类型: +数字、字符、布尔值、数组与字符串。 + +### 数字 + +`Kotlin`处理数字在某种程度上接近`Java`,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如`Java`中`int`可以隐式转换为`long`), +另外有些情况的字面值略有不同。 + +`Kotlin`提供了如下的内置类型来表示数字: + +``` +Type Bit width +Double 64 +Float 32 +Long 64 +Int 32 +Short 16 +Byte 8 +```` + +注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 + + +### 字面常量 + +数值常量字面值有以下几种: +- 十进制:123 +- `Long`类型用大写`L`标记:`123L` +- 十六进制:`0x0F` +- 二进制:`0b00001011` + +注意: 不支持八进制 + +`Kotlin`同样支持浮点数的常规表示方法: + +默认`double`:123.5、123.5e10,`Float`用`f`或者`F`标记:`123.5f` + +你可以使用下划线使数字常量更易读 + +```kotlin +val oneMillion = 1_000_000 +val creditCardNumber = 1234_5678_9012_3456L +val socialSecurityNumber = 999_99_9999L +val hexBytes = 0xFF_EC_DE_5E +val bytes = 0b11010010_01101001_10010100_10010010 +``` + +### 显式转换 + +由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题: + +```kotlin +// 假想的代码,实际上并不能编译: +val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer) +val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long) +print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long +``` + +所以同一性还有相等性都会在所有地方悄无声息地失去。 +因此较小的类型不能隐式转换为较大的类型。 这意味着在不进行显式转换的情况下我们不能把`Byte`型值赋给一个`Int`变量。 + +```kotlin +val b: Byte = 1 // OK, 字面值是静态检测的 +val i: Int = b // 错误 +``` + +我们可以显式转换来拓宽数字 +```kotlin +val i: Int = b.toInt() // OK: 显式拓宽 +``` + +每个数字类型支持如下的转换: + +```kotlin +toByte(): Byte +toShort(): Short +toInt(): Int +toLong(): Long +toFloat(): Float +toDouble(): Double +toChar(): Char +``` + +### 运算 + +这是完整的位运算列表(只用于`Int`和`Long`): + +```kotlin +shl(bits) – 有符号左移 (Java 的 <<) +shr(bits) – 有符号右移 (Java 的 >>) +ushr(bits) – 无符号右移 (Java 的 >>>) +and(bits) – 位与 +or(bits) – 位或 +xor(bits) – 位异或 +inv() – 位非 +相等性检测:a == b 与 a != b +比较操作符:a < b、 a > b、 a <= b、 a >= b +区间实例以及区间检测:a..b、 x in a..b、 x !in a..b +|| – 短路逻辑或 +&& – 短路逻辑与 +! - 逻辑非 +``` + + +### 字符串 + +字符串用`String`类型表示。字符串是不可变的。字符串的元素——字符可以使用索引运算符访问:`s[i]`。可以用`for`循环迭代字符串: + +```kotlin +for (c in str) { + println(c) +} +``` +`Kotlin`有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本。转义字符串很像`Java`字符串: +```kotlin +val s = "Hello, world!\n" +``` +转义采用传统的反斜杠方式。 + +原生字符串 使用三个引号`"""`分界符括起来,内部没有转义并且可以包含换行和任何其他字符: + +```kotlin +val text = """ + for (c in "foo") + print(c) +""" +``` +你可以通过`trimMargin()`函数去除前导空格: + +```kotlin +val text = """ + |Tell me and I forget. + |Teach me and I remember. + |Involve me and I learn. + |(Benjamin Franklin) + """.trimMargin() +``` + +### 字符串模板 + +字符串可以包含模板表达式,即一些小段代码,会求值并把结果合并到字符串中。模板表达式以美元符`$`开头,由一个简单的名字构成: + +```kotlin +val i = 10 +val s = "i = $i" // 求值结果为 "i = 10" +``` + +或者用花括号括起来的任意表达式: +```kotlin +val s = "abc" +val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" +``` + +### 字符串判等 + +Kotlin中的判等性主要有两种类型: + +- 结构相等。通过操作符==来判断两个对象的内容是否相等。 +- 引用相等。通过操作符===来判断两个对象的引用是否一样,与之相反的判断操作符是!==。如果比较的是运行时的原始类型,比如Int,那么===判断的效果也等价于==。 + +```kotlin +var a = "Java" +var b = "Java" +var c = "Kotlin" +var d = "Kot" +var e = "lin" +var f = d + e + +a == b // true +a === b // true +c == f // true +c === f // false +``` + + + +### 引用相等 + +引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。 + +### 结构相等 + +结构相等由`==`以及其否定形式`!==`操作判断。按照惯例,像`a == b`这样的表达式会翻译成 +`a?.equals(b) ?: (b === null)` +也就是说如果`a`不是`null`则调用`equals(Any?)`函数,否则即`a`是`null`检查`b`是否与`null`引用相等。 + +```kotlin +val a: Int = 10000 +print(a === a) // 输出“true” +val boxedA: Int? = agaomnh +val anotherBoxedA: Int? = a +print(boxedA === anotherBoxedA) // !!!输出“false”!!! +``` + +另一方面,它保留了相等性: + +```kotlin +val a: Int = 10000 +print(a == a) // 输出“true” +val boxedA: Int? = a +val anotherBoxedA: Int? = a +print(boxedA == anotherBoxedA) // 输出“true” +``` + + + +### 修饰符 + +`Kotlin`中修饰符是与`Java`中的有些不同。在`kotlin`中默认的修饰符是`public`,这节约了很多的时间和字符。 + +- `private` + `private`修饰符是最限制的修饰符,和`Java`中`private`一样。它表示它只能被自己所在的文件可见。所以如果我们给一个类声明为`private`, + 我们就不能在定义这个类之外的文件中使用它。 + 另一方面,如果我们在一个类里面使用了private修饰符,那访问权限就被限制在这个类里面了。甚至是继承这个类的子类也不能使用它。 + +- `protected`. + 在Java中是包、类及子类可访问,而在Kotlin中只允许类及子类。 + +- `internal` + 它与Java的default有点像但也有所区别。如果是一个定义为`internal`的包成员的话,对所在的整个`module`可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。 + 比如如果写了一个`private`类,那么它的`internal`修饰的函数的可见性就会限制与它所在的这个类的可见性。 + +- `public`. + 你应该可以才想到,这是最没有限制的修饰符。这是默认的修饰符,成员在任何地方被修饰为public,很明显它只限制于它的领域。 + +### 数组 + +数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 +设置数组对应位置的值。 +`Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 + +```kotlin +val array = arrayOf(1, 2, 3) +val countries = arrayOf("UK", "Germany", "Italy") +val numbers = intArrayOf(10, 20, 30) +val array1 = Array(10, { k -> k * k }) +val longArray = emptyArray() +val studentArray = Array(2) +studentArray[0] = Student("james") +``` + +和`Java`不一样的是`Kotlin`的数组是容器类,提供了`ByteArray`,`CharArray`,`ShortArray`,`IntArray`,`LongArray`,`BooleanArray`, +`FloatArray`和`DoubleArray`。 + +### 集合 + +`Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 +改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 + +可变集合,顾名思义,就是可以改变的集合。可变集合都会有一个修饰前缀“Mutable”,比如MutableList。这里的改变是指改变集合中的元素,比如以下可变集合: + +```kotlin +val list = mutableListOf(1, 2, 3, 4, 5) +list[0] = 0 // 变成[0, 2, 3, 4, 5] +``` + + + +`Kotlin`没有专门的语法结构创建`list`或`set`。要用标准库的方法如`listOf()`、`mutableListOf()`、`setOf()`、`mutableSetOf()`。 +创建`map`可以用`mapOf(a to b, c to d)`。 + +```kotlin +fun main(args : Array) { + var lists = listOf("a", "b", "c") + for(list in lists) { + println(list) + } +} +``` + +```kotlin +fun main(args : Array) { + var map = TreeMap() + map["0"] = "0 haha" + map["1"] = "1 haha" + map["2"] = "2 haha" + + println(map["1"]) +} +``` + +```kotlin +val numbers: MutableList = mutableListOf(1, 2, 3) +val readOnlyView: List = numbers +println(numbers) // 输出 "[1, 2, 3]" +numbers.add(4) +println(readOnlyView) // 输出 "[1, 2, 3, 4]" +readOnlyView.clear() // -> 不能编译 + +val strings = hashSetOf("a", "b", "c", "c") +assert(strings.size == 3) +``` + +Kotlin中提供了很多操作结合的函数,例如: + +```kotlin +val newList = list.map{it * 2} // 对集合遍历,在遍历过程中,给每个元素都乘以2,得到一个新的集合 + +val mStudents = students.filter{it.sex == "m"} // 筛选出性别为男的学生 + +val scoreTotal = students.sumBy{it.score} // 拥挤和中的sumby实现求和 +``` + +#### 通过序列提高效率 + +```kotlin +val list = listOf(1, 2, 3, 4, 5) +list.filter {it > 2}.map {it * 2} +``` + +上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多,这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 + +```kotlin +list.asSequence().filter {it > 2}.map {it * 2}.toList() +``` + +首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? + +在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 + + + +#### 序列的操作方式 + +```kotlin +list.asSequence().filter {it > 2}.map {it * 2}.toList() +``` + +在这个例子中,我们序列总共执行了两类操作分别是: + +- `filter{it > 2}.map{it * 2}`:filter和map的操作返回的都是序列,我们将这类操作称为中间操作。 +- `toList()`:这一类操作将序列转换为List,我们将这类操作称为末端操作。 + +其实,Kotlin中序列的操作就分为两类: + +- 中间操作 + + 中间操作都是采用惰性求值的,例如: + + ```kotlin + list.asSequence().filter { + println("filter($it)") + }.map { + println("map($it)") + } + ``` + + 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 + +- 末端操作 + + 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了,我们给上面的例子加上末端操作: + + ```kotlin + list.asSequence().filter { + println("filter($it)") + it > 2 + }.map { + println("map($it)") + it * 2 + }.toList() + // 结果 + filter(1) + filter(2) + filter(3) + map(3) + filter(4) + map(4) + filter(5) + map(5) + [6, 8, 10] + ``` + + 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作,以此类推。放映到我们这个例子上面,就是第一个元素执行了filter之后再去执行map,然后第二个元素也是这样。 + +#### 序列可以是无限的 + +在介绍惰性求值的时候,我们提过一点,就是惰性求值最大的好处是可以构造出一个无限的数据类型。那么我们能否使用序列来构造出一个无限的数据类型呢?答案是肯定的。 + +那接下来,该怎么去实现一个自然数数列呢?采用一般的列表肯定是不行的,因为构造一个列表必须列举出列表中的元素,而我们是没有办法将自然数全部列举出来的。 + +我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。好看Kotlin也给我们提供了这样一个方法,去创建无限的数列: + +```kotlin +val naturalNumList = generateSequence(0) { it + 1} +``` + +通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: + +```kotlin +naturalNumList.takeWhile{it <= 9}.toList() +[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] +``` + + + + + +### 可`null`类型 + + +因为在`Kotlin`中一切都是对象,一切都是可`null`的。当某个变量的值可以为`null`的时候,必须在声明处的类型后添加`?`来标识该引用可为空。 +`Kotlin`通过`?`将是否允许为空分割开来,比如`str:String`为不能空,加上`?`后的`str:String?`为允许空,通过这种方式,将本是不能确定的变 +量人为的加入了限制条件。而不符合条件的输入,则会在`IDE`上显示编译错误而无法执行。 + +```kotlin +var value1: String +value1 = null // 编译错误 Null can not be a value of a non-null type String + +var value2 : String? +value2 = null // 编译通过 +``` + +在对变量进行操作时,如果变量是可能为空的,那么将不能直接调用,因为编译器不知道你的变量是否为空,所以编译器就要求你一定要对变量进行判断 + +```kotlin +var str : String? = null +// 编译错误 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? +str.length +// 编译能通过,这表示如果str不为空的时候执行length方法 +str?.length +``` + +那么问题来了,我们知道在`java`中`String.length`返回的是`int`,上面的`str?.length`既然编译通过了,那么它返回了什么?我们可以这么写: + +`var result = str?.length` + +这么写编译器是能通过的,那么`result`的类型是什么呢?在`Kotlin`中,编译器会自动根据结果判断变量的类型,翻译成普通代码如下: + +```kotlin +if(str == null) + result = null; // 这里result为一个引用类型 +else + result = str.length; // 这里result为Int +``` + +那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的符号`:`双 +感叹号`!!`,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: + +```kotlin +var str : String? = null +var result : Int = str!!.length +``` + +这样的话,就能保证`result`的数据类型,但是这样还有一个问题,那就是`str`的定义是可为空的,上面的代码中,`str`就是空,这时候下面的操作虽然 +不会报编译异常,但是运行时就会见到我们熟悉的空指针异常`NullPointerExectpion`,这显然不是我们希望见到的,也不是`kotlin`愿意见到的。 +`java`中的三元操作符大家应该都很熟悉了,`kotlin`中也有类似的,它很好的解决了刚刚说到的问题。在`kotlin`中,三元操作符是`?:`,写起来也 +比`java`要方便一些。 + +```kotlin +var str : String? = null +var result = str?.length ?: -1 +//等价于 +var result : Int = if(str != null) str.length else -1 +``` + +`if null`缩写 + +```kotlin +val data = …… +val email = data["email"] ?: throw IllegalStateException("Email is missing!") +``` + +如果`?:`左侧表达式非空,`elvis`操作符就返回其左侧表达式,否则返回右侧表达式。 +请注意,当且仅当左侧为空时,才会对右侧表达式求值。 + + +##### `!!`操作符 + +我们可以写`b!!`,这会返回一个非空的`b`值 +(例如:在我们例子中的`String`)或者如果`b`为空,就会抛出一个空指针异常: + +```kotlin +val l = b!!.length +``` + +因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 + + +#### 安全的类型转换 + +如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 +另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: + +```kotlin +val aInt: Int? = a as? Int +``` + +#### 可空类型的集合 + +如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 + +```kotlin +val nullableList: List = listOf(1, 2, null, 4) +val intList: List = nullableList.filterNotNull() +``` + + + +### 使用类型检测及自动类型转换 + +`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, +无需显式转换: + +```kotlin +fun getStringLength(obj: Any): Int? { + if (obj !is String) return null + + // `obj` 在这一分支自动转换为 `String`,这是因为Kotlin的编译器帮我们做了转换 + // 这称为Kotlin中的智能转换(Smart Casts)。官方文档中这样介绍: 当且仅当Kotlin的编译器 + // 确定在类型检查后该变量不会再改变,才会产生Smart Casts。 + return obj.length +} +``` + +### 返回和跳转 + +`Kotlin`有三种结构化跳转表达式: + +- `return`:默认从最直接包围它的函数或者匿名函数返回。 +- `break`:终止最直接包围它的循环。 +- `continue`:继续下一次最直接包围它的循环。 + +在`Kotlin`中任何表达式都可以用标签`label`来标记。标签的格式为标识符后跟`@`符号,例如:`abc@`、`fooBar@`都是有效的标签。 + +要为一个表达式加标签,我们只要在其前加标签即可。 + +```kotlin +loop@ for (i in 1..100) { + for (j in 1..100) { + if (……) break@loop + } +} +``` + + + + + +[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) +[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" new file mode 100644 index 00000000..43178b4e --- /dev/null +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -0,0 +1,224 @@ +Kotlin学习教程(四) +=== + +## `if`表达式 + +在`Kotlin`中,`if`是一个表达式,即它会返回一个值。因此就不需要三元运算符`条件 ? 然后 : 否则`,因为普通的`if`就能胜任这个角色。 +`if`的分支可以是代码块,最后的表达式作为该块的值: + +```kotlin +val max = if (a > b) { + print("Choose a") + a +} else { + print("Choose b") + b +} +``` + +## `when`表达式 + +`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 +式。 +与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 + +对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: +```kotlin +when (x){ + 1 -> print("x == 1") + 2 -> print("x == 2") + else -> { + print("I'm a block") + print("x is neither 1 nor 2") + } +} +``` + +因为它是一个表达式,它也可以返回一个值。我们需要考虑什么时候作为一个表达式使用,它必须要覆盖所有分支的可能性或者实现`else`分支。否则它不会被 +编译成功: + +```kotlin +val result = when (x) { + 0, 1 -> "binary" + else -> "error" +} +``` + +如你所见,条件可以是一系列被逗号分割的值。但是它可以更多的匹配方式。比如,我们可以检测参数类型并进行判断: + +```kotlin +when(view) { + is TextView -> view.setText("I'm a TextView") + is EditText -> toast("EditText value: ${view.getText()}") + is ViewGroup -> toast("Number of children: ${view.getChildCount()} ") + else -> view.visibility = View.GONE +} +``` + +### 示例:when对if else的改造 + +```kotlin +fun schedule(day: Day, sunny: Boolean) = { + if (day == Day.SAT) { + basketball() + } else if (day == Day.SUN) { + fishing() + } else if (day == Day.FRI) { + appointment() + } else { + if (sunny) { + library() + } else { + study() + } + } +} +``` + +上面的例子中因为存在不少if else分支,代码显得不够优雅,更好的改进方法是用when表达式来优化: + +```kotlin +fun schedule(sunny: Boolean, day: Day) = when (day) { + Day.SAT -> basketball() + Day.SUN -> fishing() + Day.FRI -> appointment() + else -> when { + // when关键字的参数可以省略 + sunny -> library() + else -> study() + } +} +``` + +一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接,不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 + +到这里你可能会说上面的例子中,这样嵌套子when表达式,层次依旧比较深。要知道when表达式是很灵活的,我们很容易通过如下修改来解决这个问题: + +```kotlin +fun schedule(sunny: Boolean, day: Day) = when { + day == Day.SAT -> basketball() + day == Day.SUN -> fishing() + day == Day.FRI -> appointment() + sunny -> library() + else -> study() +} +``` + +这样就会更优雅了。 + +## for循环 + +```kotlin +val items = listOf("apple", "banana", "kiwi") +for (item in items) { + println(item) +} + +for (i in array.indices) + print(array[i]) +``` + +在Kotlin中用in关键字来检查一个元素是否是一个区间或集合中的成员。如果我们在in前面加上感叹号,那么就是相反的判断结果。 + +### Ranges + +`Range`表达式使用一个`..`操作符。表示就是一个该范围内的数据的数组,包含头和尾 + +```kotlin +var nums = 1..100 +for(num in nums) { + println(num) + // 打印出1 2 3 ....100 +} +``` + +```kotlin +if(i >= 0 && i <= 10) + println(i) +``` + +转换成 + +```kotlin +if (i in 0..10) + println(i) +``` + +Ranges默认会自增长,所以如果像以下的代码: + +```kotlin +for (i in 10..0) + println(i) +``` + +它就不会做任何事情。但是你可以使用`downTo`函数: + +```kotlin +for(i in 10 downTo 0) + println(i) +``` + +我们可以在`Ranges`中使用`step`来定义一个从`1`到一个值的不同的空隙: + +```kotlin +for (i in 1..4 step 2) println(i) +for (i in 4 downTo 1 step 2) println(i) +``` + +### Until + +上面的`Range`是包含了头和尾,那如果只想包含头不包含尾呢? 就要用`until` + +```kotlin +var nums = 1 until 100 +for(num in nums) { + println(num) + // 这样打印出来是1 2 3 .....99 +} +``` + + + +上面in、step、downTo、until这几个,他们可以不通过点号,而是通过中缀表达式来被调用,从而让语法变得更加简洁直观。 + + + +### `Kotlin`用到的关键字 + +- `var`:定义变量 +- `val`:定义常量 +- `fun`:定义方法 +- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 +- `vararg`:可变参数 +- `$`:字符串模板(取值) +- 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), +- `ushr`(无符号右移),`xor`(按位异或),`inv`(按位取反) +- `in`:在某个范围中 检查值是否在或不在(`in/!in`)范围内或集合中 +- `downTo`:递减,循环时可用,每次减1 +- `step`:步长,循环时可用,设置每次循环的增加或减少的量 +- `when`:`Kotlin`中增强版的`switch`,可以匹配值,范围,类型与参数 +- `is`:判断类型用,类似于`Java`中的`instanceof()`,`is`运算符检查表达式是否是类型的实例。 如果一个不可变的局部变量或属性是指定类型, + 则不需要显式转换 +- `private`仅在同一个文件中可见 +- `protected`同一个文件中或子类可见 +- `public`所有调用的地方都可见 +- `internal`同一个模块中可见 +- `abstract`抽象类标示 +- `final`标示类不可继承,默认属性 +- `enum`标示类为枚举 +- `open`类可继承,类默认是`final`的 +- `annotation`注解类 +- `init`主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块(`initializer blocks`)中 +- `field`只能用在属性的访问器内。特别注意的是,`get set`方法中只能能使用`filed`。属性访问器就是`get set`方法。 +- `:`用于类的继承,变量的定义 +- `..`围操作符(递增的) `1..5`,`2..6`千万不要`6..2` +- `::`作用域限定符 +- `inner`类可以标记为`inner {: .keyword }`以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用 +- `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 + 用到 + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" similarity index 62% rename from "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" rename to "KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 8f1e87bb..4bcc58cc 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\224).md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -95,9 +95,15 @@ val outter = Outter() outter.Inner().execute() ``` +#### 内部类vs嵌套类 +在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类,必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 -匿名内部类 +内部类和嵌套类有明显的差别,具体体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 + + + +#### 匿名内部类 ```kotlin // 通过对象表达式来 创建匿名内部类的对象,可以避免重写抽象类的子类和接口的实现类,这和Java中匿名内部类的是接口和抽象类的延伸一致。 @@ -124,8 +130,9 @@ mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { }) ``` +### 枚举 -### 枚举 +与Java中的enum语法大体类似,无非多了一个class关键字,表示它是一个枚举类。 ```kotlin enum class Day { @@ -133,16 +140,23 @@ enum class Day { THURSDAY, FRIDAY, SATURDAY } ``` -枚举可以带参数: +不过Kotlin中的枚举类当然没有那么简单,由于它是一种类,我们可以猜测它自然应该可以拥有构造函数,以及定义额外的属性和方法。 ```kotlin -enum class Icon(val res: Int) { - UP(R.drawable.ic_up), - SEARCH(R.drawable.ic_search), - CAST(R.drawable.ic_cast) +enum class DayOfWeek(val day: Int) { + MON(1), + TUE(2), + WEN(3), + THU(4), + FRI(5), + SAT(6), + SUN(7) + ; // 需要注意的是,当在枚举类中存在额外的方法或或属性定义,则必须强制加上分号,虽然你可能不会喜欢这个语法 + fun getDayNumber(): Int { + return day + } } -val searchIconRes = Icon.SEARCH.res ``` 枚举可以通过`String`匹配名字来获取,我们也可以获取包含所有枚举的`Array`,所以我们可以遍历它。 ```kotlin @@ -157,6 +171,42 @@ val searchPosition: Int = Icon.SEARCH.ordinal() ### 密封类 +Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承,比如: + +```kotlin +sealed class Bird { + open fun fly() = "I can fly" + class Eagle : Bird() +} +``` + +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: + +```java +public abstract class Bird { + @NotNull + public String fly() { + return "I can fly" + } + private Bird() { + + } + // $FF: synthetic method + public Bird(DefaultConstructorMarker $constructor_maker) { + this(); + } + + public static final class Eagle extends Bird { + public Eagle() { + super((DefaultConstructorMarker) null); + } + } +} + +``` + + + 密封类用来表示受限的类继承结构:当一个值为有限集中的 类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 @@ -229,8 +279,103 @@ val s = try { x as String } catch(e: ClassCastException) { null } ### 对象`(Object)` +在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的,但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 + +在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 + +#### 伴生对象 + +先看一个Java例子: + +```java +public class Prize { + private String name; + private int count; + private int type; + + public Prize(String name, int count, int type) { + this.name = name; + this.count = count; + this.type = type; + } + + static int TYPE_REDPACK = 0; + static int TYPE_COUPON = 1; + + static boolean isRedpack(Prize prize) { + return prize.type == TYPE_REDPACK; + } + + public static void main(String[] args) { + Prize prize = new Prize("hongbao", 10, Prize.TYPE_REDPACK); + System.out.println(Prize.isRedpack(prize)); + } +} +``` + +上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 + +那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说,这是一种利用companion object两个关键字创造的语法。 + +伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 + +现在将上面的例子改写成伴生对象的版本: + +```kotlin +class Prize(val name: String, val count: Int, val type: Int) { + companion object { + val TYPE_REDPACK = 0 + val TYPE_COUPON = 1 + + fun isRedpack(prize: Prize): Boolean { + return prize.type == TYPE_REDPACK + } + } + + fun main(args: Array) { + val prize = Prize("hongbao", 10, Prize.TYPE_REDPACK) + print(Prize.isRedpack(prize)) + } +} +``` + +可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 + +伴生对象的另一个作用是可以实现工厂方法模式。前面也说过如何从构造方法实现工厂方法模式,然而这种方法存在以下缺点: + +- 利用多个构造方法语义不够明确,只能靠参数区分 +- 每次获取对象时都需要重新创建对象 + +你会发现,伴生对象也是实现工厂方法模式的另一种思路,可以改进以上的两个问题。 + +```kotlin +class Prize private constructor(val name: String, val count: Int, val type: Int) { + companion object { + val TYPE_COMMON = 1 + val TYPE_REDPACK = 2 + val TYPE_COUPON = 3 + + val defaultCommonPrize = Prize("common", 10, Prize.TYPE_COMMON) + + fun newRedpackPrize(name: String, count: int) = Prize(name, count, Prize.TYPE_REDPACK) + fun newCouponPrize(name: String, count: Int) = Prize(name, count, Prize.TYPE_COUPON) + fun defaultCommonPrize() = defaultCommonPrize // 无须构造新对象 + } + + fun main(args: Array) { + val redpackPrize = Prize.newRedpackPrize("hongbao", 10) + val couponPrize = Prize.newCouponPrize("shiyuan", 10) + vval commonPrize = Prize.defaultCommonPrize() + } +} +``` + +总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 + + + 声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 -它们。事实上,对象就是具有单一实现的数据类型。 +它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 ```kotlin object Resource { @@ -240,14 +385,59 @@ object Resource { ### 单例 +因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。 + ```kotlin object Resource { val name = "Name" } ``` -因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 -对象的实例在我们第一次使用时,被创建。所以这里有一个懒惰实例化:如果一个对象永远不会被使用,这个实例永远不会被创建。 +因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 + +看一下自动生成的java代码: + +```java +public final class SingleTon { + @NotNull + private static String name; + @NotNull + public static final SingleTon INSTANCE; + + @NotNull + public final String getName() { + return name; + } + + public final void setName(@NotNull String var1) { + Intrinsics.checkNotNullParameter(var1, ""); + name = var1; + } + + private SingleTon() { + } + + static { + SingleTon var0 = new SingleTon(); + INSTANCE = var0; + name = ""; + } +} +``` + +这里官网中对object的介绍是: object declarations are initialized lazily, when accessed for the first time + +为什么上面说它是饿汉式? 这个从上面反编译的代码可以看到确实有一个static的静态代码块,静态代码块具有懒惰加载特性,但它这个是加载特性,并不是我们说的: + +```java +public static Instance getInstance() { + if (instance == null) { + instance = new Instance(); + } +} +``` + +具体可以看: [Kotlin Object Declarations Are Initialized in Static Block](https://hanru-yeh.medium.com/kotlin-object-declarations-are-initialized-in-static-block-5e1c2e1c3401) ### 对象表达式 @@ -267,6 +457,10 @@ recycler.adapter = object : RecyclerView.Adapter() { ``` 例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 +object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 + + + ### 伴生对象`(Companion Object)` diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" new file mode 100644 index 00000000..7017a62f --- /dev/null +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -0,0 +1,211 @@ +Kotlin学习教程之多继承问题 +=== + + + +继承和实现是面向对象程序设计中不变的主题。Java是不支持类的多继承的,Kotlin亦是如此。我们他们要这样设计呢? 现实中,其实多继承的需求经常会出现,然而类的多继承方式会导致进程关系上语义的混淆。 + + + +### 骡子的多继承困惑 + +如果你了解C++,应该知道C++中的类是支持多重继承机制的。然而,C++中存在一个经典的钻石问题--骡子的多继承困惑。我们假设Java的类也支持多继承,然后模仿C++中类似的语法,来看看它到底会导致什么问题: + +```java +abstract class Animal { + abstract public void run(); +} +class Horse extends Animal { + @Override + public void run() { + System.out.println("I am run very fast"); + } +} +class Donkey extends Animal { + @Override + public void run() { + System.out.println("I am run very slow"); + } +} +class Mule extends Horse, Donkey { + // 骡子 + ... +} +``` + +这是一段伪代码,这段代码的含义是: + +- 马和驴都继承了Animal类,并实现了Animal中的run抽象方法。 +- 骡子是马和驴的杂交产物,它拥有两者的特性,于是Mule利用多继承同时继承了Horse和Donkey + +目前看起来没有问题,然而当我们打算在Mule中实现run方法的时候,问题就产生了:Mule到底是继承Horse的run方法还是Donkey的run方法?这个就是经典的钻石问题。 + +![image-20210325104002353](https://raw.githubusercontent.com/CharonChui/Pictures/master/mule_problem.png?raw=true) + +所以钻石问题也被称为棱形继承问题。可以发现,类的多继承如果使用不当,就会在继承关系上产生歧义。而且,多继承还会给代码维护带来很多困扰:一来代码的耦合度会提高,二来各种类之间的关系令人眼花缭乱。 + +于是Kotlin和Java一样只支持类的单继承。那么面对多继承的需求,在Kotlin中该如何解决呢? + + + +#### 接口实现多继承 + +接口支持多实现,所以一个类可以实现多个接口,这是Java经常干的事。Kotlin的接口与Java和类似,但它除了可以定义带默认实现的方法之外,还可以声明抽象的属性。下面就是用Kotlin中的接口来实现多继承: + +```kotlin +interface Flyer { + fun fly() + fun kind() = "flying animals" +} +interface Animal { + val name: String + fun eat() + fun kind() = "flying animals" +} + +class Bird(override val name: String) : Flyer, Animal { + override fun eat() { + println("I can eat") + } + override fun fly() { + println("I can fly") + } + override fun kind() = super.kind() +} + +fun main(args: Array.kind()。当然我们也可以主动实现方法,覆盖父接口的方法。如: + +```kotlin +override fun kind() = "a flying ${this.name}" +// 最终的执行结果就是 +a flying sparrow +``` + +通过这个例子,可以看出实现接口的语法: + +- 在Kotlin中实现一个接口时,需要实现接口中没有默认实现的方法及未初始化的属性,若同时实现多个接口,而接口间又有相同方法名的默认实现时,则需要主动指定使用哪个接口的方法或者重写方法。 +- 如果是默认的接口方法,你可以在实现类中通过super这种方式调用它,其中T为拥有该方法的接口名。 +- 在实现接口的属性和方法时,都必须带上override关键字,不能省略。 + +#### 内部类解决多继承问题 + +在Java中可以将一个类的定义放在另一个类的定义内部,这就是内部类。由于内部类可以继承一个与外部无关的类,所以这保证了内部类的独立性,可以用它这个特性来尝试解决多继承的问题。 + +```kotlin +open class Horse { + fun runFast() { + println("I can run fast") + } +} +open class Donkey { + fun doLongTimeThing() { + println("I can do some thing long time") + } +} +class Mule { + fun runFast() { + HorseC().runFast() + } + fun doLongTimeThing() { + DonkeyC().doLongTimeThing() + } + + private inner class HorseC : Horse() + private inner class DonkeyC : Donkey() +} +``` + +上面的例子可以看到: + +- 可以在一个类的内部定义多个内部类,每个内部类的实例都有自己的独立状态,它们与外部对象的信息相互独立。 +- 通过让内部类HorseC、DonkeyC分别继承Horse和Donkey这两个外部类,我们可以在Mule类中定义它们的实例对象,从而获得了Horse和Donkey两者不同的状态和行为。 +- 可以利用private修饰内部类,使得其他类都不能访问内部类,这样可以具有非常良好的封闭性。 + +所以在某些场合下,内部类确实是一种解决多继承非常好的思路。 + + + +#### 使用委托代替多继承 + +委托是一种特殊的类型,用于方法事件委托,比如你调用A类的methodA方法,其实背后是B类的methodA去执行。 + +印象中,要实现委托并不是一件非常自然直观的事情。但庆幸的是,Kotlin简化了这种语法,我们只需要通过by关键字就可以实现委托的效果。比如之前提过的by lazy语法,其实就是利用委托实现的延迟初始化语法。 + +```kotlin +val laziness: String by lazy { + println("I will hava a value") + "I an a lazy initialized string" +} +``` + +下面通过委托来替代多继承实现需求: + +```kotlin +interface CanFly { + fun fly() +} +interface CanEat { + fun eat() +} + +open class Flyer : CanFly { + override fun fly() { + println("I can fly") + } +} +open class Animal : CanEat { + override fun eat() { + println("I can eat") + } +} +class Bird(flyer: Flyer, animal: Animal) : CanFly by flyer, CanEat by animal {} + +fun main(args: Array) { + val flyer = Flyer() + val animal = Animal() + val b = Bird(flyer, animal) + b.fly() + b.eat() +} +``` + +有人可能会有疑问: 首先,委托方式怎么跟接口实现多继承如此相似,而且好像也并没有简单多少。其次,这种方式好像跟组合也很想,那么它到底有什么优势? 主要有以下两点: + +- 前面说到接口是无状态的,所以即使它提供了默认方法实现也是很简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。我们可以利用上面委托的这种方式,虽然它也是接口委托,但它是用一个具体的类去实现方法逻辑,可以拥有更强大的能力。 +- 假设我们需要继承的类是A,委托对象是B、C,我们再具体调用的时候并不是像组合一样A.B.method,而是可以直接调用A.method,这更能表达A拥有该method的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的。 + + + + + + + + + + + + + + + + + + + + +[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" new file mode 100644 index 00000000..fdbd19e2 --- /dev/null +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -0,0 +1,859 @@ +Kotlin学习教程(六) +=== + +### 注解 + +注解是将元数据附加到代码的方法。要声明注解,请将`annotation`修饰符放在类的前面: + +```kotlin +annotation class Fancy +``` + +注解的附加属性可以通过用元注解标注注解类来指定: + +- `@Target`指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等) +- `@Retention`指定该注解是否存储在编译后的`class`文件中,以及它在运行时能否通过反射可见(默认都是`true`) +- `@Repeatable`允许在单个元素上多次使用相同的该注解 +- `@MustBeDocumented`指定该注解是公有`API`的一部分,并且应该包含在生成的`API`文档中显示的类或方法的签名中 + + +```kotlin +@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, + AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) +@Retention(AnnotationRetention.SOURCE) +@MustBeDocumented +annotation class Fancy +``` + +### 用法 + +```kotlin +@Fancy class Foo { + @Fancy fun baz(@Fancy foo: Int): Int { + return (@Fancy 1) + } +} +``` + +如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加`constructor`关键字,并将注解添加到其前面: + +```kotlin +class Foo @Inject constructor(dependency: MyDependency) { + // …… +} +``` + +### 反射 + +反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 +`Kotlin`让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。 + +在`Java`平台上,使用反射功能所需的运行时组件作为单独的`JAR`文件(`kotlin-reflect.jar`)分发。这样做是为了减少不使用反射功能的应用程序所需的 +运行时库的大小。如果你需要使用反射,请确保该`.jar`文件添加到项目的`classpath`中。 + + +### 类引用 + +最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对静态已知的`Kotlin`类的引用,可以使用类字面值语法: + +```kotlin +val c = MyClass::class +``` +该引用是`KClass`类型的值。 +通过使用对象作为接收者,可以用相同的`::class`语法获取指定对象的类的引用: + +```kotlin +val widget: Widget = …… +assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" } +``` + +当我们有一个命名函数声明如下: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +``` +我们可以很容易地直接调用它`isOdd(5)`,但是我们也可以把它作为一个值传递。例如传给另一个函数。为此我们使用`::`操作符: + +```kotlin +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 输出 [1, 3] +``` + +### 扩展 + +扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 + +```kotlin +// 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 +fun Context.toast(msg : String){ + Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() + Log.d("text", "Toast msg : $msg") +} + +// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法 +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + ...... + toast("hello, Extension") + } +} + +// 输出 +Toast msg : hello, Extension +``` + +按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 +上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 +它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 +自己的`toast`扩展函数。有关`Anko`后面会讲到。 + +`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 +扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 +扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 +扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: +```kotlin +fun receiverType.functionName(params){ + body +} +receiverType:表示函数的接收者,也就是函数扩展的对象 +functionName:扩展函数的名称 +params:扩展函数的参数,可以为NULL +``` + +在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 +`this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 + +扩展函数的原理: 通过查看对应的Java代码可以发现: + +```kotlin +package com.st.stplayer.extension + +fun MutableList.exchange(fromIndex: Int, toIndex: Int) { + val tmp = this[fromIndex] + this[fromIndex] = this[toIndex] + this[toIndex] = tmp +} +``` + + + +```java +package com.st.stplayer.extension; + +import java.util.List; +import kotlin.Metadata; +import kotlin.jvm.internal.Intrinsics; +import org.jetbrains.annotations.NotNull; + +@Metadata( + mv = {1, 4, 2}, + bv = {1, 0, 3}, + k = 2, + d1 = {"\u0000\u0012\n\u0000\n\u0002\u0010\u0002\n\u0002\u0010!\n\u0002\u0010\b\n\u0002\b\u0003\u001a \u0010\u0000\u001a\u00020\u0001*\b\u0012\u0004\u0012\u00020\u00030\u00022\u0006\u0010\u0004\u001a\u00020\u00032\u0006\u0010\u0005\u001a\u00020\u0003¨\u0006\u0006"}, + d2 = {"exchange", "", "", "", "fromIndex", "toIndex", "stplayer_debug"} +) +public final class ExtenndsKt { + public static final void exchange(@NotNull List $this$exchange, int fromIndex, int toIndex) { + Intrinsics.checkNotNullParameter($this$exchange, "$this$exchange"); + int tmp = ((Number)$this$exchange.get(fromIndex)).intValue(); + $this$exchange.set(fromIndex, $this$exchange.get(toIndex)); + $this$exchange.set(toIndex, tmp); + } +} +``` + +通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 + + + + + +##### 扩展函数的作用域 + +在平时写扩展时,我们通常会把扩展方法放到一个文件中,例如ViewExtends.kt: + +```kotlin +package com.charon.ext + +... +val View.isVisible: Boolean + get() = visibility == View.VISIBLE + +fun View.toBitmap(): Bitmap? { + clearFocus() + isPress = false + val willNotCache = willNotCacheDrawing() + setWillNotCacheDrawing(false) + // Reset the drawing cache background color to fully transparent + // for the duration of this operation + val color = drawingCacheBackgroundColor + drawingCacheBackgroundColor = 0 + if (color != 0) destroyDrawingCache() + buildDrawingCache() + val cacheBitmap = drawingCache + if (cacheBitmap == null) { + Log.e("Views", "failed to get bitmap from $this", RuntimeException()) + return null + } + val bitmap = Bitmap.createBitmap(cacheBitmap) + // Restore the view + destroyDrawingCache() + setWillNotCacheDrawing(willNotCache) + drawingCacheBackgroundColor = color + return bitmap +} +``` + +我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: + +```kotlin +class Extends { + fun MutableList.exchange(fromIndex: Int, toIndex: Int) { + val tmp = this[fromIndex] + this[fromIndex] = this[toIndex] + this[toIndex] = tmp + } +} +``` + +当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: + +```java +public static final class Extends { + public final void exchange(@NotNull list $receiver, int fromIndex, int toIndex) { + Intrinsics.checkParameterIsNotNull($receiver, "$receiver"); + int tmp = ((Number)$receiver.get(fromIndex)).intValue(); + $receiver.set(fromIndex, $receiver.get(toIndex)); + $receiver.set(toIndex, Integer.valueOf(tmp)); + } +} +``` + +我们看到,exchange方法上已经没有static关键字的修饰了。所以当扩展方法在一个Class内部时,我们只能在该类和该类的子类中进行调用。 + + + +##### 成员方法优先级总高于扩展函数 + +```kotlin +class Son { + fun foo() = println("son called member foo") +} +``` + +它包含一个成员方法foo(),加入我们哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: + +```kotlin +fun Son.foo() = println("son called extension foo") + +object Test { + @JvmStatic + fun main(args: Array) { + Son().foo() + } +} +``` + +在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? + +但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了。所以在使用时,我们必须注意: 同名的类成员方法的优先级总高于扩展函数。 + + + +#### 被滥用的扩展函数 + +扩展函数在开发中为我们提供了非常多的便利,但是在实际应用中,我们可能会将这个特性滥用。例如ImageLoaderUtils这个加载网络图片为例: + +```kotlin +fun Context.loadImage(url: String, imageView: ImageView) { + GlideApp.with(this) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(imageView) +} + +// 在ImageActivity.kt中使用 +this.loadImage(url, imageView) +``` + +也许你在用的时候并没有感觉出什么奇怪的地方,但是实际上,我们并没有以任何方式扩展现有类。上述代码仅仅为了在函数调用的时候省去参数,这是一种滥用扩展机制的行为。 + +我们知道,Context作为"God Object",已经承担了很多责任。我们基于Context扩展,还很可能产生ImageView与传入上下文周期不一致导致的很多问题。 + +正确的做法应该是在ImageView上进行扩展: + +```kotlin +fun ImageView.loadImage(url: String) { + GlideApp.with(this.context) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(this) +} +``` + +这样在调用的时候,不仅省去了更多的参数,而且ImageView的声明周期也得到了保证。 + +在实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片的请求框架进行二次封装: + +```kotlin +object ImageLoader { + fun with(context: Context, url: String, imageView: ImageView) { + GlideApp.with(context) + .load(url) + .placeholder(R.mipmap.img_default) + .error(R.mipmap.ic_error) + .into(imageView) + } +} +``` + +所以,虽然扩展函数能够提供许多便利,我们还是应该注意在恰当的地方使用它,否则会造成不必要的麻烦。 + + + +##### 扩展属性 + + +扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有`set`和`get`方法,并且要求实现这两个方法,不然会提示编译错误。 +因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过`get`和`set`对这个属性进行读写操作的时候也不能使用 +`field`指代属性本体。可以使用`this`,依然表示的目标类。 + +```kotlin +// 扩展了一个属性paddingH +var View.panddingH : Int + get() = (paddingLeft + paddingRight) / 2 + set(value) { + setPadding(value, paddingTop, value, paddingBottom) + } + +// 设置值 +text.panddingH = 100 +``` + +给`View`扩展了一个属性`paddingH`,并给属性增加了`set`和`get`方法,然后可以在`activity`中通过`textview`调用。 + +扩展属性与扩展函数一样,其本质也是对应Java中的静态方法。由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。 + + +##### 静态扩展 + +`kotlin`中的静态用关键字`companion`表示,但是它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的, +这样就将静态部分统一包装了起来。静态部分的访问和`java`一致,直接使用类名+静态属性/方法名调用。 + +```kotlin +// 定义静态部分 +class Extension { + companion object{ + var name = "Extension" + } +} + +// 通过类名+属性名直接调用 +toast("hello, ${Extension.name}") + +// 输出 +Toast msg : hello, Extension +``` +上面例子中,`companion object`一起是修饰关键字,`part`是方法块的名称。其中方法块名称`part`可以省略,如果省略的话,默认缺省名为 +`Companion` + +静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。 + +```kotlin +class Extension { + companion object part{ + var name = "Extension" + } +} + +// part为静态方法块名称 +fun Extension.part.upCase() : String{ + return name.toUpperCase() +} + +// 调用一下 +toast("hello, ${Extension.name}") +toast("hello, ${Extension.upCase()}") + +//输出 +Toast msg : hello, Extension +Toast msg : hello, EXTENSION +``` + + + +#### 标准库中的扩展函数 + +1. run + +先看一下run方法,它是利用扩展实现的,定义如下: + +```kotlin +public inline fun T.run(block: T.() -> R): R = block() +``` + +简单来说,run是任何类型T的通用扩展函数,run中执行了返回类型为R的扩展函数block,最终返回该扩展函数的结果。 + +在run函数中我们拥有一个单独的作用域,能够重新定义一个nickName变量,并且它的作用域只存在于run函数中: + +```kotlin +fun testFoo() { + val nickName = "111" + run { + val nickName = "xxx" + println(nickName) // xxx + } + println(nickName) // 111 +} +``` + +这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。 + +例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: + +```kotlin +run { + if (!isLogin) loginDialog else getNewAccountDialog +}.show() +``` + + + +2. apply + +`apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象. + +```kotlin +fun testApply() { + ArrayList().apply { + add("testApply") + add("testApply") + add("testApply") + println("this = " + this) + }.let { println(it) } +} + +// 运行结果 +// this = [testApply, testApply, testApply] +// [testApply, testApply, testApply] +``` + +let函数和apply函数很像,唯一不同的是返回值。apply返回的是原来的对象,而let返回的是闭包里面的值。 + +3. let + +他的定义为: + +```kotlin +public inline fun T.let(block: (T) -> R): R = block(this) +``` + + + +如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定return,与run一样,它同样限制了变量的作用域。 + +```kotlin +private var test: String? = null + +private fun switchFragment(position: Int) { + test?.let { + LogUtil.e("@@@", "test is not null") + } +} +``` + +说到可能有人会觉得没什么用,用`if`判断下是不是空不就完了. + +```kotlin +private var test: String? = null + +private fun switchFragment(position: Int) { +// test?.let { +// LogUtil.e("@@@", "test is null") +// } + + if (test == null) { + LogUtil.e("@@@", "test is null") + } else { + LogUtil.e("@@@", "test is not null ${test}") + check(test) // 报错 + } +} +``` + +但是会报错:`Smart cast to 'String' is impossible, beacuase 'test' is a mutable property that could have been changed by this time` + + + +4. also + +also是Kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版。 + +```kotlin +public inline fun T.also(block: (T) -> Unit): T { block(this); return this } +``` + +与apply一致,它的返回值是该函数的接受者。 + +```kotlin +class Kot { + val student: Student? = getStu() + var age = 0 + fun dealStu() { + val result = student?.also { stu -> + this.age += stu.age + println(this.age) + println(stu.age) + this.age + } + } +} +``` + + + +#### `sNullOrEmpty | isNullOrBlank` + +```kotlin +public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0 + +public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() + +// If we do not care about the possibility of only spaces... +if (number.isNullOrEmpty()) { + // alert the user to fill in their number! +} + +// when we need to block the user from inputting only spaces +if (name.isNullOrBlank()) { + // alert the user to fill in their name! +} +``` + +#### `with`函数 + +with和apply这两个方法最大的作用就是可以让那个我们在写Lambda的时候,省略需要多次书写的对象名,默认用this关键字来指向它,this可以省略。 + +`with`是一个非常有用的函数,它包含在`Kotlin`的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。 +这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为`this`一样使用所有它的`public`方法和属性。 +当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。 + +```kotlin +fun testWith() { + with(ArrayList()) { + add("testWith") + add("testWith") + add("testWith") + println("this = " + this) + } +} +// 运行结果 +// this = [testWith, testWith, testWith] +``` + +#### `repeat`函数 + +`repeat`函数是一个单独的函数,定义如下: + +```kotlin +/** + * Executes the given function [action] specified number of [times]. + * + * A zero-based index of current iteration is passed as a parameter to [action]. + */ +@kotlin.internal.InlineOnly +public inline fun repeat(times: Int, action: (Int) -> Unit) { + contract { callsInPlace(action) } + + for (index in 0..times - 1) { + action(index) + } +} +``` + +通过代码很容易理解,就是循环执行多少次`block`中内容。 + +```kotlin +fun main(args: Array) { + repeat(3) { + println("Hello world") + } +} +``` + +运行结果是: + +```kotlin +Hello world +Hello world +Hello world +``` + +#### also + +also是kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版: + +```kotlin +public inline fun T.also(block: (T) -> Unit): T { + block(this); return this +} +``` + +与apply一致,它的返回值是该函数的接收者。 + +### 调度方式对扩展函数的影响 + +Kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚地了解静态和动态调度之间的区别。 + +#### 静态与动态调度 + +先用一个Java的例子: + +```java +class Base { + public void fun() { + System.out.println("I'm Base foo!"); + } +} +class Extended extends Base { + @Override + public void fun() { + System.out.println("I'm Extended foo!"); + } +} +Base base = new Extended(); +base.fun(); +``` + +毫无疑问,因为重写了fun方法,所以最终肯定会执行I'm Extended foo!。 + +变量base具有编译时类型Base和运行时类型Extended。当我们调用时,base.foo()将动态调度该方法,这意味着运行时类型(Extended)的方法被调用。 + +当我们调用重载方法时,调度变为静态并且仅取决于编译时类型。 + +```java +void foo(Base base) { + ... +} +void foo(Extended extended) { + ... +} +public static void main(String[] args) { + Base base = new Extended(); + foo(base); +} +``` + +在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。 + + + +#### 扩展函数始终静态调度 + +可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 + +为了方便理解,举一个例子: + +```kotlin +open class Base +class Extended: Base() +fun Base.foo() = "I'm Base.foo!" +fun Extended.foo() = "I'm Extended.foo!" +fun main(args: Array) { + val instance: Base = Extended() + val instance2 = Extended() + println(instance.foo()) + println(instance2.foo()) +} +// 执行结果 +I'm Base.foo! +I'm Extended.foo! +``` + +由于只考虑了编译时类型,第一个打印将调用Base.foo(),而第二个打印将调用Extended.foo()。 + + + + + +## 用扩展函数封装Utils + +在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: + +```java +public class NetworkUtils { + public static boolean isMobileConnected(Context context) { + if (context != null) { + ConnectivityManager mConnectivityManager = (ConnectivityManager)context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE); + if (mMobilieNetworkInfo != null) { + return mMobileNetworkInfo.isAvailable(); + } + } + return false; + } +} +``` + +在需要调用的地方,我们通常会这样写: + +```java +boolean isConnected = NetworkUtils.isMobileConnected(context); +``` + +虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: + +```java +boolean isConnected = context.isMobileConnected(); +``` + +由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。但是在Kotlin中,我们通过扩展函数就能简单的实现: + +```kotlin +fun Context.isMobileConnected(): Boolean { + val mNetworkInfo = connectivityManager.activeNetworkInfo + if (mNetworkInfo != null) { + return mNetworkInfo.isAvailable + } + return false +} +``` + +我们只需要将以上代码放入对应文件中即可。这时我们已经摆脱了类的束缚,使用方式如下: + +```kotlin +val isConnected = context.isMobileConnected(); +``` + +值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext,防止出现声明周期不一致导致的内存泄露或者其他问题。 + + + +## 运算符重载 + +  Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 + +语法:运算符重载使用的是operator关键字,在指定函数的前面加上operator关键字,就可以实现运算符重载了。 +指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus()  - 对应minus() +可以在类中,对同一运算符进行多次重载,来满足不同的需求。 + +运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。或者理解成是对已有的运算符赋予他们新的含义。重载的修饰符是operator。 + +举例: + +比如我们的+号,它的含义是两个数值相加: 1+1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 + +下面我们实现一个Person数据类,然后重载Int的 + 号运算符,让一个Int对象可以直接和Person对象相加。返回的结果是这个Int对象减去Person对象的age的值。 + +```kotlin +data class Person(var name: String, var age: Int) +// 通过扩展的方式来实现运算符重载 +operator fun Int.plus(b: Person): Int { + return this - b.age +} +fun main() { + val person1 = Person("A", 3) + val testInt = 5 + println("result : ${testInt + person1}") //输出结果=2 +} +``` + +再比如,我们可以对Person数据类进行重载+号运算符,让Person对象可以直接调用+号来做一些函数操作。 + +```kotlin +data class Person(var name: String, var age: Int) { + operator fun plus(other: Person): Person { + return Person("${this.name} + ${other.name}", this.age + other.age) + } +} +fun main() { + val person1 = Person("A", 3) + val person2 = Person("B", 4) + val person3 = person1 + person2 + println("person3 = ${person3}") // person3 = Person(name=A + B, age=7) +} +``` + +一些场景运算符对应的函数名如下: + +### 一元前缀操作符 + +| 表达式 | 翻译为 | +| :----- | :--------------- | +| `+a` | `a.unaryPlus()` | +| `-a` | `a.unaryMinus()` | +| `!a` | `a.not()` | + + + +### 算术运算符 + +| 表达式 | 翻译为 | +| :------ | ---------------------------------- | +| `a + b` | `a.plus(b)` | +| `a - b` | `a.minus(b)` | +| `a * b` | `a.times(b)` | +| `a / b` | `a.div(b)` | +| `a % b` | `a.rem(b)`、 `a.mod(b)` (已弃用) | +| `a..b` | `a.rangeTo(b)` | + +### “In”操作符 + +| 表达式 | 翻译为 | +| :-------- | :--------------- | +| `a in b` | `b.contains(a)` | +| `a !in b` | `!b.contains(a)` | + + + +### 索引访问操作符 + +| 表达式 | 翻译为 | +| :-------------------- | :----------------------- | +| `a[i]` | `a.get(i)` | +| `a[i, j]` | `a.get(i, j)` | +| `a[i_1, ……, i_n]` | `a.get(i_1, ……, i_n)` | +| `a[i] = b` | `a.set(i, b)` | +| `a[i, j] = b` | `a.set(i, j, b)` | +| `a[i_1, ……, i_n] = b` | `a.set(i_1, ……, i_n, b)` | + +### 调用操作符 + +| 表达式 | 翻译为 | +| :---------------- | :----------------------- | +| `a()` | `a.invoke()` | +| `a(i)` | `a.invoke(i)` | +| `a(i, j)` | `a.invoke(i, j)` | +| `a(i_1, ……, i_n)` | `a.invoke(i_1, ……, i_n)` | + +### 相等与不等操作符 + +| 表达式 | 翻译为 | +| :------- | :-------------------------------- | +| `a == b` | `a?.equals(b) ?: (b === null)` | +| `a != b` | `!(a?.equals(b) ?: (b === null))` | + +### 比较操作符 + +| 表达式 | 翻译为 | +| :------- | :-------------------- | +| `a > b` | `a.compareTo(b) > 0` | +| `a < b` | `a.compareTo(b) < 0` | +| `a >= b` | `a.compareTo(b) >= 0` | +| `a <= b` | `a.compareTo(b) <= 0` | + + + + + +[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) +[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" new file mode 100644 index 00000000..d732bbaf --- /dev/null +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -0,0 +1,97 @@ +8.Kotlin_协程 +=== + +Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它更容易提升系统的高并发处理能力。 + +一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 +并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 +协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 +订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 + + + +### 起源 + +协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 + +线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 + + + + +### 阻塞 vs 挂起 + +基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中一个会导致一些 +重要的任务被延迟。 + +另一方面,协程挂起几乎是无代价的。不需要上下文切换或者`OS`的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制: +作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。 + +另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 + +#### 挂起函数 + +当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: + +```kotlin +suspend fun doSomething(foo: Foo): Bar { + …… +} +``` + +这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 +获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, +必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 +(源自`kotlinx.coroutines`库): + +```kotlin +fun async(block: suspend () -> T) +``` + +这里的`async()`是一个普通函数(不是挂起函数),但是它的`block`参数具有一个带`suspend`修饰符的函数类型:`suspend() -> T`。 +所以,当我们将一个`lambda`表达式传给`async()`时,它会是挂起`lambda`表达式,于是我们可以从中调用挂起函数: + +```kotlin +async { + doSomething(foo) + …… +} +``` + +继续该类比,`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: +```kotlin +async { + …… + val result = computation.await() + …… +} + +``` + +更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 + +请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: +```kotlin +fun main(args: Array) { + doSomething() // 错误:挂起函数从非协程上下文调用 +} +``` + +还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: +```kotlin +interface Base { + suspend fun foo() +} + +class Derived: Base { + override suspend fun foo() { …… } +} +``` + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" deleted file mode 100644 index 1d48ec61..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\200).md" +++ /dev/null @@ -1,534 +0,0 @@ -Kotlin学习教程(一) -=== - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) - -在`5月18`日谷歌在`I/O`开发者大会上宣布,将`Kotlin`语言作为安卓开发的一级编程语言。并且会在`Android Studio 3.0`版本全面支持`Kotlin`。 - -- `Kotlin`是一个基于`JVM`的新的编程语言,由[JetBrains](https://www.jetbrains.com/)开发。`JetBrains`作为目前广受欢迎的 -`Java IDE IntelliJ`的提供商,在`Apache`许可下已经开源其`Kotlin`编程语言。 -- `Kotlin`可以编译成`Java`字节码,也可以编译成`JavaScript`,方便在没有`JVM`的设备上运行。 -- `Kotlin`已正式成为`Android`官方开发语言。 - -[Kotlin官网](https://kotlinlang.org/) - -`JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 -一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 - -很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 -的一句话: -> 拥抱变化 - -`Google`做事,向来言出必行,之前在推行`Android Studio`时也是一片骂声,吐槽各种不好用,各种慢。但是现在`Android Studio`基本都已经普及了。 -我相信`Kotlin`也不会例外。所以我们不仅要学,还要要认真的学。 - - -### `Kotlin`的特性 - -- 它更加易表现:这是它最重要的优点之一。你可以编写少得多的代码。 -- `Kotlin`是一种兼容`Java`的语言 -- `Kotlin`比`Java`更安全,能够静态检测常见的陷阱。如:引用空指针 -- `Kotlin`比`Java`更简洁,通过支持`variable type inference,higher-order functions (closures),extension functions,mixins -and first-class delegation`等实现 -- `Kotlin`可与`Java`语言无缝通信。这意味着我们可以在`Kotlin`代码中使用任何已有的`Java`库;同样的`Kotlin`代码还可以为`Java`代码所用 -- `Kotlin`在代码中很少需要在代码中指定类型,因为编译器可以在绝大多数情况下推断出变量或是函数返回值的类型。这样就能获得两个好处:简洁与安全 - - -### `Kotlin`优势 - -- 全面支持`Lambda`表达式 -- 数据类`Data classes` -- 函数字面量和内联函数`Function literals & inline functions` -- 函数扩展`Extension functions` -- 空安全`Null safety` -- 智能转换`Smart casts` -- 字符串模板`String templates` -- 主构造函数`Primary constructors` -- 类委托`Class delegation` -- 类型推判`Type inference` -- 单例`Singletons` -- 声明点变量`Declaration-site variance` -- 区间表达式`Range expressions` - - -上面说简洁简洁,到底简洁在哪里?这里先用一个例子开始,在`Java`开发过程中经常会写一些`Bean`类: -```java -package com.charon.kotlinstudydemo; - -public class Person { - private int age; - private String name; - private float height; - private float weight; - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public float getHeight() { - return height; - } - - public void setHeight(float height) { - this.height = height; - } - - public float getWeight() { - return weight; - } - - public void setWeight(float weight) { - this.weight = weight; - } - - @Override - public String toString() { - return "Person name is : " + name + " age is : " + age + " height is :" - + height + " weight is :" + weight; - } -} -``` -使用`Kotlin`: -```kotlin -package com.charon.kotlinstudydemo - -data class Person( - var name: String, - var age: Int, - var height: Float, - var weight: Float) -``` -这个数据类,它会自动生成所有属性和它们的访问器,以及一些有用的方法,比如`toString()`方法。 -这里插一嘴,从上面的例子中我们可以看到对于包的声明基本是一样的,唯一不同的是`kotlin`中后面结束不用分号。 - - -### 创建`Kotlin`项目 - -`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 -[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 -直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 - - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/studio_create_kotlin.png?raw=true) - -创建完成后我们看一下`MainActivity`的代码: -```kotlin -// 定义包 -package com.charon.kotlinstudydemo - -// 导入 -import android.support.v7.app.AppCompatActivity -import android.os.Bundle - -// 定义类,继承AppCompatActivity -class MainActivity : AppCompatActivity() { - - // 重写方法用overide,函数名用fun声明 参数是a: 类型的形式 ?是啥?它是指明该对象可能为null, - // 如果有了?那在调用该方法的时候参数可以传递null进入,如果没有?传递null就会报错 - override fun onCreate(savedInstanceState: Bundle?) { - // super - super.onCreate(savedInstanceState) - // 调用方法 - setContentView(R.layout.activity_main) - } -} -``` - -我们就从`MainActivity`的代码开始介绍一些基本的语法。 - -### 变量 - -变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。 - -`val`与`Java`中使用的`final`很相似。一个不可变对象意味着它在实例化之后就不能再去改变它的状态了。如果你需要一个这个对象修改之后的版本, -那就会再创建一个新的对象。 - -声明: -```kotlin -var age: Int = 18 -val name: String = "charon" -``` - -再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 - -字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: -```kotlin -var age = 18 // int -val name = "charon" // string -var height = 180.5f // flat -var weight = 70.5 // double -``` - -在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 -当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 -的,但是有一些不同之处你可能需要考虑到: - -- 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: - ```kotlin - private var age = 18 - private var weight = age.toFloat() - ``` -- 字符(`Char`)不能直接作为一个数字来处理。在需要时我们需要把他们转换为一个数字: - ```kotlin - val c: Char='c' - val i: Int = c.toInt() - ``` -- 位运算也有一点不同。在`Android`中,我们经常在`flags`中使用`或`: - ```java - // Java - int bitwiseOr = FLAG1 | FLAG2; - int bitwiseAnd = FLAG1 & FLAG2; - ``` - - ```kotlin - // Kotlin - val bitwiseOr = FLAG1 or FLAG2 - val bitwiseAnd = FLAG1 and FLAG2 - ``` - -- 一个`String`可以像数组那样访问,并且被迭代: - ```kotlin - var s = "charon" - var c = s[2] - - for (a in s) { - Log.e("@@@", a +""); - } - ``` - - -##### 编译期常量 - -已知值的属性可以使用`const`修饰符标记为编译期常量(类似`java`中的`public static final`)。 -`const`只能修复`val`不能修复`var`,这些属性需要满足以下要求: -- 位于顶层或者是`object`的一个成员 -- 用`String`或原生类型值初始化 -- 没有自定义`getter` - -```kotlin -// Const val are only allowed on top level or in objects -const val NAME: String = "charon" - -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} -``` - - -##### 后端变量`Backing Fields`. - -在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 - -例如: - -```kotlin -var count = 1 -var size: Int = 2 -set(value) { - Log.e("text", "count : ${count++}") - size = if (value > 10) 15 else 0 -} -``` -这个例子中就会内存溢出。 - -`kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。 - - -##### 延迟初始化 - -我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 -假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 -延迟其初始化,此属性用`lateinit`修饰符修饰。 - -```kotlin -class MainActivity : AppCompatActivity() { - lateinit var name : String - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - var test = MainActivity() - // 要先调用方法让其初始化 - test.init() - // 再使用其属性 - Log.e("@@@", test.name) - } - - fun init() { - // 延迟初始化 - name = "charon" - } -} -``` -需要注意的是,我们在使用的时候,一定要确保属性是被初始化过的,通常先调用初始化方法,否则会有异常。 -如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的。 -```kotlin -private lateinit var test: String - -private fun switchFragment(position: Int) { - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null") - check(test) - } -} -``` -会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` - -除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: -```kotlin -private val test by lazy { "haha" } - -private fun switchFragment(position: Int) { - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null ${test}") - check(test) - } -} -``` -执行结果: -``` -test is not null haha -``` - -那`lateinit`和`by lazy`有什么区别呢? - -- `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 -- `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 - - -### 类的定义:使用`class`关键字 - -类可以包含: -- 构造函数和初始化块 -- 函数 -- 属性 -- 嵌套类和内部类 -- 对象声明 - - -```kotlin -class MainActivity{ - -} -``` - -如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: -```kotlin -class Person(name: String, age: Int) -``` - -##### 创建类的实例 - -```kotlin -val person = Person("charon", 18) -``` - -上面的类有一个默认的构造函数。 - -注意:创建类的实例不用`new`了啊。 - - -### 构造函数 - -在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 - -##### 主构造函数 - -主构造函数是类头的一部分:它跟在类名(和可选的类型参数)后: -```kotlin -class Person constructor(name: String, surname: String) { -} -``` -如果主构造函数没有任何注解或者可见性修饰符,可以省略`constructor`关键字: -```kotlin -class Person(name: String, surname: String) { -} -``` - -主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块中: - -```kotlin -class Person constructor(name: String, surname: String) { - init { - print("name is $name and surname is $surname") - } -} -``` - -如果构造函数有注解或可见性修饰符,那么`constructor`关键字是必需的,并且这些修饰符在它前面: -```kotlin -class Person private @Inject constructor(name: String, surname: String) { - init { - print("name is $name and surname is $surname") - } -} -``` - -##### 次构造函数 - -类也可以声明前缀有`constructor`的次构造函数: -```kotlin -class Person{ - constructor(name: String) { - print("name is $name") - } -} -``` - -如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 -委托到同一个类的另一个构造函数用`this`关键字即可: -```kotlin -class Person constructor(name: String) { - constructor(name: String, surName: String) : this(name) { - Log.d("@@@", "name is : $name surName is : $surName") - } -} -``` -使用该对象: -```kotlin -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - Person("charon", "chui") - } -} -``` -就会在`logcat`上打印: -`09-20 16:51:19.738 6010-6010/com.charon.kotlinstudydemo D/@@@: name is : charon surName is : chui` - -如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是`public`。 -如果你不希望你的类有一个公有构造函数,你需要声明一个带有非默认可见性的空的主构造函数: - -```kotlin -class Person private constructor(name: String) { -} -``` - - -### 接口:使用`interface`关键字 - -```kotlin -interface FlyingAnimal { - fun fly() -} -``` - -### 函数:通过`fun`关键字定义 - -```kotlin -fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) -} -``` -如果你没有指定它的返回值,它就会返回`Unit`与`Java`中的`void`类似,但是`Unit`是一个真正的对象。`Unit`可以省略, -你当然也可以指定任何其它的返回类型: -```kotlin -fun maxOf(a: Int, b: Int): Int { - if (a > b) { - return a - } else { - return b - } -} -``` - -然而如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号: -```kotlin -fun add(x: Int,y: Int) : Int = x + y -``` - -我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: -```kotlin -fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { - Toast.makeText(this, message, length).show() -} -``` -上面代码中第二个参数`length`指定了一个默认值。这意味着你调用的时候可以传入第二个值或者不传,这样可以避免你需要的重载函数: - -```kotlin -toast("Hello") -toast("Hello", Toast.LENGTH_LONG) -``` - -##### 自定义`get set`方法: - -`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: -`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: - -```kotlin -class Person constructor() { - var name: String = "" - get() = field - set(value) { - field = "$value" - } - - var age: Int = 0 - get() = field - set(value) { - field = value - } -} -``` -按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 - - -##### 可变长参数函数:使用`vararg`关键字 - -```kotlin -fun vars(vararg v:Int){ - for(vt in v){ - print(vt) - } -} - -// 测试 -fun main(args: Array) { - vars(1,2,3,4,5) // 输出12345 -} -``` - - -### 注释 - -和`Java`差不多 - -```kotlin - -// 这是一个行注释 - -/* 这是一个多行的 - 块注释。 */ -``` - - -[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" deleted file mode 100644 index face6efd..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\203).md" +++ /dev/null @@ -1,96 +0,0 @@ -Kotlin学习教程(七) -=== - -这篇文章主要学习下`lambda`表达式。因为后续一些例子会用到。 - - -> “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 -> `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 - -`Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 - -```java -// 没有使用Lambda的老方法: -button.addActionListener(new ActionListener(){ - public void actionPerformed(ActionEvent ae){ - System.out.println("Actiondetected"); - } -}); -// 使用Lambda: -button.addActionListener(()->{ - System.out.println("Actiondetected"); -}); - - -// 不采用Lambda的老方法: -Runnable runnable1=new Runnable(){ - @Override - public void run(){ - System.out.println("RunningwithoutLambda"); - } -}; -// 使用Lambda: -Runnable runnable2=()->{ - System.out.println("RunningfromLambda"); -}; -``` - -`Lambda`能让代码更简洁,而主打简洁的`Kotlin`怎么可能不支持呢? 当然会支持。 - -下面来看看一个简短的概述: - -- `lambda`表达式总是被大括号括着 -- 其参数(如果有的话)在`->`之前声明(参数类型可以省略), -- 函数体(如果存在的话)在`->`后面。 - -`Lambda`表达式是定义匿名函数的简单方法。由于`Lambda`表达式避免在抽象类或接口中编写明确的函数声明,进而也避免了类的实现部分, -所以它是非常有用的。在`Kotlin`语言中,可以将一函数作为另一函数的参数。 - -`Lambda`表达式由箭头左侧函数的参数(在圆括号里的内容)定义的,将值返回到箭头右侧。 -`view.setOnClickListener({ view -> toast("Click")})` -在定义函数时,必须在箭头的左侧用方括号,并指定参数值,而函数的执行代码在箭头右侧。如果左侧不使用参数,甚至可以省去左侧部分: -`view.setOnClickListener({ toast("Click") })` -如果函数的最后一个参数是一个函数的话,可以将作为参数的函数移到圆括号外面: -`view.setOnClickListener() { toast("Click") }` - - -先看一个例子: - -```kotlin -fun compare(a: String, b: String): Boolean { - return a.length < b.length -} -max(strings, compare) -``` -就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。 -```kotlin -max(strings, (a,b)->{a.length < b.length}) -``` - -`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: - -- 参数的类型 -- 返回值的类型 - -但这些真的必要吗?`a.length < b.length`很明显返回一个`Boolean`的值,再就是`max`的定义中肯定也定义了这个函数的参数类型和返回值类型。 -这么明显的事为什么不让计算机自己去做而要让人写代码去做呢?这就是匿名函数的好处了。到这里,我们已经和`Lambda`很接近了。 - -```kotlin -val sum: (Int, Int) -> Int = { x, y -> x + y } -``` - -`Lambda`表达式就是被大括号括着的那一部分,在`->`符号之前有参数声明,函数体跟在一个`->`符号之后。 -而且此`Lambda`表达式之前有一个匿名的函数声明(在此例中两个`Int`型的输入,一个`Int`型的返回值),这个声明是可以不使用的。 -则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 -返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 - - - -[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" deleted file mode 100644 index cb0eb4e2..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\270\211).md" +++ /dev/null @@ -1,196 +0,0 @@ -Kotlin学习教程(三) -=== - -前面介绍了基本语法和编码规范后,接下来学习下基本类型。 - -在`Kotlin`中,所有东西都是对象,在这个意义上讲我们可以在任何变量上调用成员函数和属性。 一些类型可以有特殊的内部表示——例如, -数字、字符和布尔值可以在运行时表示为原生类型值,但是对于用户来说,它们看起来就像普通的类。 在本节中,我们会描述`Kotlin`中使用的基本类型: -数字、字符、布尔值、数组与字符串。 - -### 数字 - -`Kotlin`处理数字在某种程度上接近`Java`,但是并不完全相同。例如,对于数字没有隐式拓宽转换(如`Java`中`int`可以隐式转换为`long`), -另外有些情况的字面值略有不同。 - -`Kotlin`提供了如下的内置类型来表示数字: - -``` -Type Bit width -Double 64 -Float 32 -Long 64 -Int 32 -Short 16 -Byte 8 -```` - -注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 - - -### 字面常量 - -数值常量字面值有以下几种: -- 十进制:123 -- `Long`类型用大写`L`标记:`123L` -- 十六进制:`0x0F` -- 二进制:`0b00001011` - -注意: 不支持八进制 - -`Kotlin`同样支持浮点数的常规表示方法: - -默认`double`:123.5、123.5e10,`Float`用`f`或者`F`标记:`123.5f` - -你可以使用下划线使数字常量更易读 - -```kotlin -val oneMillion = 1_000_000 -val creditCardNumber = 1234_5678_9012_3456L -val socialSecurityNumber = 999_99_9999L -val hexBytes = 0xFF_EC_DE_5E -val bytes = 0b11010010_01101001_10010100_10010010 -``` - -### 引用相等 - -引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。 - -### 结构相等 - -结构相等由`==`以及其否定形式`!==`操作判断。按照惯例,像`a == b`这样的表达式会翻译成 -`a?.equals(b) ?: (b === null)` -也就是说如果`a`不是`null`则调用`equals(Any?)`函数,否则即`a`是`null`检查`b`是否与`null`引用相等。 - -```kotlin -val a: Int = 10000 -print(a === a) // 输出“true” -val boxedA: Int? = agaomnh -val anotherBoxedA: Int? = a -print(boxedA === anotherBoxedA) // !!!输出“false”!!! -``` - -另一方面,它保留了相等性: -```kotlin -val a: Int = 10000 -print(a == a) // 输出“true” -val boxedA: Int? = a -val anotherBoxedA: Int? = a -print(boxedA == anotherBoxedA) // 输出“true” -``` - -### 显式转换 - -由于不同的表示方式,较小类型并不是较大类型的子类型。 如果它们是的话,就会出现下述问题: - -```kotlin -// 假想的代码,实际上并不能编译: -val a: Int? = 1 // 一个装箱的 Int (java.lang.Integer) -val b: Long? = a // 隐式转换产生一个装箱的 Long (java.lang.Long) -print(a == b) // 惊!这将输出“false”鉴于 Long 的 equals() 检测其他部分也是 Long -``` - -所以同一性还有相等性都会在所有地方悄无声息地失去。 -因此较小的类型不能隐式转换为较大的类型。 这意味着在不进行显式转换的情况下我们不能把`Byte`型值赋给一个`Int`变量。 - -```kotlin -val b: Byte = 1 // OK, 字面值是静态检测的 -val i: Int = b // 错误 -``` - -我们可以显式转换来拓宽数字 -```kotlin -val i: Int = b.toInt() // OK: 显式拓宽 -``` - -每个数字类型支持如下的转换: - -```kotlin -toByte(): Byte -toShort(): Short -toInt(): Int -toLong(): Long -toFloat(): Float -toDouble(): Double -toChar(): Char -``` - -### 运算 - -这是完整的位运算列表(只用于`Int`和`Long`): - -```kotlin -shl(bits) – 有符号左移 (Java 的 <<) -shr(bits) – 有符号右移 (Java 的 >>) -ushr(bits) – 无符号右移 (Java 的 >>>) -and(bits) – 位与 -or(bits) – 位或 -xor(bits) – 位异或 -inv() – 位非 -相等性检测:a == b 与 a != b -比较操作符:a < b、 a > b、 a <= b、 a >= b -区间实例以及区间检测:a..b、 x in a..b、 x !in a..b -|| – 短路逻辑或 -&& – 短路逻辑与 -! - 逻辑非 -``` - - -### 字符串 - -字符串用`String`类型表示。字符串是不可变的。字符串的元素——字符可以使用索引运算符访问:`s[i]`。可以用`for`循环迭代字符串: - -```kotlin -for (c in str) { - println(c) -} -``` -`Kotlin`有两种类型的字符串字面值: 转义字符串可以有转义字符,以及原生字符串可以包含换行和任意文本。转义字符串很像`Java`字符串: -```kotlin -val s = "Hello, world!\n" -``` -转义采用传统的反斜杠方式。 - -原生字符串 使用三个引号`"""`分界符括起来,内部没有转义并且可以包含换行和任何其他字符: - -```kotlin -val text = """ - for (c in "foo") - print(c) -""" -``` -你可以通过`trimMargin()`函数去除前导空格: - -```kotlin -val text = """ - |Tell me and I forget. - |Teach me and I remember. - |Involve me and I learn. - |(Benjamin Franklin) - """.trimMargin() -``` - -### 字符串模板 - -字符串可以包含模板表达式,即一些小段代码,会求值并把结果合并到字符串中。模板表达式以美元符`$`开头,由一个简单的名字构成: - -```kotlin -val i = 10 -val s = "i = $i" // 求值结果为 "i = 10" -``` - -或者用花括号括起来的任意表达式: -```kotlin -val s = "abc" -val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" -``` - - -[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) -[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" index 9d2b1b1c..1a484128 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" @@ -170,6 +170,14 @@ class MyActivity : Activity() { ``` `textView`是对`Activity`的一项扩展属性,与在`activity_main.xml`中的声明具有同样类型。 +这里,你肯定会想,虽然省略了R.id.几个字符,但是引入是否会造成性能问题? 值得引入、使用 kotlin-android-extensions吗?如果我们对其反编译,就可以看到对应Java代码的实现,在第一次使用空间的时候,会在缓存集合中进行查找,有就直接使用,没有就通过findViewById进行查找,并添加到缓存的集合中。其还提供了$clearFindViewByIdCache()方法用于清除缓存,在我们想要彻底替换界面控件时可以使用。 + +在Fragment的onDestroyView()方法中默认调用了$clearFindViewByIdCache()清除缓存,而Activity没有。 + +所以我们并没有完全离开findViewById,只是Kotlin的扩展插件利用缓存的方式让我们开发更方便、更快捷。 + + + ### 网络请求 diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" deleted file mode 100644 index a4d12dad..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\272\214).md" +++ /dev/null @@ -1,88 +0,0 @@ -Kotlin学习教程(二) -=== - -上一篇文章介绍了`Kotlin`的基本语法,我感觉在继续学习更多知识之前有必要单独介绍以下编码规范。 - -不管学什么东西,开始形成的习惯以后想改都比较困难。所以开始就用规范的方式学习是最好的。 - - -### 命名风格 - -如果拿不准的时候,默认使用`Java`的编码规范,比如: - -- 使用驼峰法命名(并避免命名含有下划线) -- 类型名以大写字母开头 -- 方法和属性以小写字母开头 -- 使用4个空格缩进 -- 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 - - -### 冒号 - -类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: - -```kotlin -interface Foo : Bar { - fun foo(a: Int): T -} -``` - -### `Lambda`表达式 - -在`lambda`表达式中, 大括号左右要加空格,分隔参数与代码体的箭头左右也要加空格。`lambda`表达应尽可能不要写在圆括号中: - -```kotlin -list.filter { it > 10 }.map { element -> element * 2 } -``` - -### 类头格式化 - -有少数几个参数的类可以写成一行: - -```kotlin -class Person(id: Int, name: String) -``` - -具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。 此外,右括号应该另起一行。如果我们使用继承, -那么超类构造函数调用或者实现接口列表应位于与括号相同的行上: - -```kotlin -class Person( - id: Int, - name: String, - surname: String -) : Human(id, name) { - // …… -} -``` -对于多个接口,应首先放置超类构造函数调用,然后每个接口应位于不同的行中: -```kotlin -class Person( - id: Int, - name: String, - surname: String -) : Human(id, name), - KotlinMaker { - // …… -} -``` - - -### `Unit` - -如果函数返回`Unit`类型,该返回类型应该省略: -```kotlin -fun foo() { // 省略了 ": Unit" - -} -``` - - -[上一篇:Kotlin学习教程(一)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md) -[下一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" index ee364cba..6ed5d235 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" @@ -4,6 +4,8 @@ Kotlin学习教程(八) `Kotlin`协程 --- +Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它 + 一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 @@ -421,130 +423,31 @@ class Group(val name: String) { -常用操作符及函数 ---- +### Any -#### `let`操作符 +我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 -如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定`return` -```kotlin -private var test: String? = null - -private fun switchFragment(position: Int) { - test?.let { - LogUtil.e("@@@", "test is not null") - } -} -``` +但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 -说到可能有人会觉得没什么用,用`if`判断下是不是空不就完了. -```kotlin -private var test: String? = null - -private fun switchFragment(position: Int) { -// test?.let { -// LogUtil.e("@@@", "test is null") -// } - - if (test == null) { - LogUtil.e("@@@", "test is null") - } else { - LogUtil.e("@@@", "test is not null ${test}") - check(test) // 报错 - } -} -``` -但是会报错:`Smart cast to 'String' is impossible, beacuase 'test' is a mutable property that could have been changed by this time` - -#### `sNullOrEmpty | isNullOrBlank` - -```kotlin -public inline fun CharSequence?.isNullOrEmpty(): Boolean = this == null || this.length == 0 - -public inline fun CharSequence?.isNullOrBlank(): Boolean = this == null || this.isBlank() - -// If we do not care about the possibility of only spaces... -if (number.isNullOrEmpty()) { - // alert the user to fill in their number! -} - -// when we need to block the user from inputting only spaces -if (name.isNullOrBlank()) { - // alert the user to fill in their name! -} -``` -#### `with`函数 -`with`是一个非常有用的函数,它包含在`Kotlin`的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。 -这表示所有我们在括号中编写的代码都是作为对象(第一个参数)的一个扩展函数,我们可以就像作为`this`一样使用所有它的`public`方法和属性。 -当我们针对同一个对象做很多操作的时候这个非常有利于简化代码。 +#### Any:非空类型的跟类型 -```kotlin -fun testWith() { - with(ArrayList()) { - add("testWith") - add("testWith") - add("testWith") - println("this = " + this) - } -} -// 运行结果 -// this = [testWith, testWith, testWith] -``` +与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: -#### `repeat`函数 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) -`repeat`函数是一个单独的函数,定义如下: -```kotlin -/** - * Executes the given function [action] specified number of [times]. - * - * A zero-based index of current iteration is passed as a parameter to [action]. - */ -@kotlin.internal.InlineOnly -public inline fun repeat(times: Int, action: (Int) -> Unit) { - contract { callsInPlace(action) } +与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: - for (index in 0..times - 1) { - action(index) - } -} -``` -通过代码很容易理解,就是循环执行多少次`block`中内容。 -```kotlin -fun main(args: Array) { - repeat(3) { - println("Hello world") - } -} -``` -运行结果是: ```kotlin -Hello world -Hello world -Hello world +class Animal(val weight: Double) ``` -#### `apply`函数 +#### Any?:所有类型的根类型 -`apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象 -```kotlin -fun testApply() { - ArrayList().apply { - add("testApply") - add("testApply") - add("testApply") - println("this = " + this) - }.let { println(it) } -} +如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 -// 运行结果 -// this = [testApply, testApply, testApply] -// [testApply, testApply, testApply] -``` -`run`函数和`apply`函数很像,只不过run函数是使用最后一行的返回,apply返回当前自己的对象。 [上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" deleted file mode 100644 index f672121e..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\255).md" +++ /dev/null @@ -1,204 +0,0 @@ -Kotlin学习教程(六) -=== - -### 注解 - -注解是将元数据附加到代码的方法。要声明注解,请将`annotation`修饰符放在类的前面: - -```kotlin -annotation class Fancy -``` - -注解的附加属性可以通过用元注解标注注解类来指定: - -- `@Target`指定可以用该注解标注的元素的可能的类型(类、函数、属性、表达式等) -- `@Retention`指定该注解是否存储在编译后的`class`文件中,以及它在运行时能否通过反射可见(默认都是`true`) -- `@Repeatable`允许在单个元素上多次使用相同的该注解 -- `@MustBeDocumented`指定该注解是公有`API`的一部分,并且应该包含在生成的`API`文档中显示的类或方法的签名中 - - -```kotlin -@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, - AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.EXPRESSION) -@Retention(AnnotationRetention.SOURCE) -@MustBeDocumented -annotation class Fancy -``` - -### 用法 - -```kotlin -@Fancy class Foo { - @Fancy fun baz(@Fancy foo: Int): Int { - return (@Fancy 1) - } -} -``` - -如果需要对类的主构造函数进行标注,则需要在构造函数声明中添加`constructor`关键字,并将注解添加到其前面: - -```kotlin -class Foo @Inject constructor(dependency: MyDependency) { - // …… -} -``` - -### 反射 - -反射是这样的一组语言和库功能,它允许在运行时自省你的程序的结构。 -`Kotlin`让语言中的函数和属性做为一等公民、并对其自省(即在运行时获悉一个名称或者一个属性或函数的类型)与简单地使用函数式或响应式风格紧密相关。 - -在`Java`平台上,使用反射功能所需的运行时组件作为单独的`JAR`文件(`kotlin-reflect.jar`)分发。这样做是为了减少不使用反射功能的应用程序所需的 -运行时库的大小。如果你需要使用反射,请确保该`.jar`文件添加到项目的`classpath`中。 - - -### 类引用 - -最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对静态已知的`Kotlin`类的引用,可以使用类字面值语法: - -```kotlin -val c = MyClass::class -``` -该引用是`KClass`类型的值。 -通过使用对象作为接收者,可以用相同的`::class`语法获取指定对象的类的引用: - -```kotlin -val widget: Widget = …… -assert(widget is GoodWidget) { "Bad widget: ${widget::class.qualifiedName}" } -``` - -当我们有一个命名函数声明如下: - -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -``` -我们可以很容易地直接调用它`isOdd(5)`,但是我们也可以把它作为一个值传递。例如传给另一个函数。为此我们使用`::`操作符: - -```kotlin -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 输出 [1, 3] -``` - -### 扩展 - -扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 - -```kotlin -// 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 -fun Context.toast(msg : String){ - Toast.makeText(this, msg, Toast.LENGTH_SHORT).show() - Log.d("text", "Toast msg : $msg") -} - -// Activity类,由于所有Activity都是Context的子类,所以可以直接使用扩展的toast方法 -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - ...... - toast("hello, Extension") - } -} - -// 输出 -Toast msg : hello, Extension -``` - -按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 -上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 -它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 -自己的`toast`扩展函数。有关`Anko`后面会讲到。 - -`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 -扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 -扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 -扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: -```kotlin -fun receiverType.functionName(params){ - body -} -receiverType:表示函数的接收者,也就是函数扩展的对象 -functionName:扩展函数的名称 -params:扩展函数的参数,可以为NULL -``` - -在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 -`this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 - - -##### 扩展属性 - - -扩展属性和扩展方法类似,是对目标类的属性进行扩展。扩展属性也会有`set`和`get`方法,并且要求实现这两个方法,不然会提示编译错误。 -因为扩展并不是在目标类上增加了这个属性,所以目标类其实是不持有这个属性的,我们通过`get`和`set`对这个属性进行读写操作的时候也不能使用 -`field`指代属性本体。可以使用`this`,依然表示的目标类。 - -```kotlin -// 扩展了一个属性paddingH -var View.panddingH : Int - get() = (paddingLeft + paddingRight) / 2 - set(value) { - setPadding(value, paddingTop, value, paddingBottom) - } - -// 设置值 -text.panddingH = 100 -``` - -给`View`扩展了一个属性`paddingH`,并给属性增加了`set`和`get`方法,然后可以在`activity`中通过`textview`调用。 - - -##### 静态扩展 - -`kotlin`中的静态用关键字`companion`表示,但是它不是修饰属性或方法,而是定义一个方法块,在方法块中的所有方法和属性都是静态的, -这样就将静态部分统一包装了起来。静态部分的访问和`java`一致,直接使用类名+静态属性/方法名调用。 - -```kotlin -// 定义静态部分 -class Extension { - companion object part{ - var name = "Extension" - } -} - -// 通过类名+属性名直接调用 -toast("hello, ${Extension.name}") - -// 输出 -Toast msg : hello, Extension -``` -上面例子中,`companion object`一起是修饰关键字,`part`是方法块的名称。其中方法块名称`part`可以省略,如果省略的话,默认缺省名为 -`Companion` - -静态的扩展和普通的扩展类似,但是在目标类要加上静态方法块的名称,所以如果我们要对一个静态部分扩展,就要先知道静态方法块的名称才行。 - -```kotlin -class Extension { - companion object part{ - var name = "Extension" - } -} - -// part为静态方法块名称 -fun Extension.part.upCase() : String{ - return name.toUpperCase() -} - -// 调用一下 -toast("hello, ${Extension.name}") -toast("hello, ${Extension.upCase()}") - -//输出 -Toast msg : hello, Extension -Toast msg : hello, EXTENSION -``` - - - -[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) -[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" index e70017a7..7e4012f9 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" @@ -1,40 +1,7 @@ Kotlin学习教程(十) === - -### `Kotlin`用到的关键字 - -- `var`:定义变量 -- `val`:定义常量 -- `fun`:定义方法 -- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 -- `vararg`:可变参数 -- `$`:字符串模板(取值) -- 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), -- `ushr`(无符号右移),`xor`(按位异或),`inv`(按位取反) -- `in`:在某个范围中 检查值是否在或不在(`in/!in`)范围内或集合中 -- `downTo`:递减,循环时可用,每次减1 -- `step`:步长,循环时可用,设置每次循环的增加或减少的量 -- `when`:`Kotlin`中增强版的`switch`,可以匹配值,范围,类型与参数 -- `is`:判断类型用,类似于`Java`中的`instanceof()`,`is`运算符检查表达式是否是类型的实例。 如果一个不可变的局部变量或属性是指定类型, -则不需要显式转换 -- `private`仅在同一个文件中可见 -- `protected`同一个文件中或子类可见 -- `public`所有调用的地方都可见 -- `internal`同一个模块中可见 -- `abstract`抽象类标示 -- `final`标示类不可继承,默认属性 -- `enum`标示类为枚举 -- `open`类可继承,类默认是`final`的 -- `annotation`注解类 -- `init`主构造函数不能包含任何的代码。初始化的代码可以放到以`init`关键字作为前缀的初始化块(`initializer blocks`)中 -- `field`只能用在属性的访问器内。特别注意的是,`get set`方法中只能能使用`filed`。属性访问器就是`get set`方法。 -- `:`用于类的继承,变量的定义 -- `..`围操作符(递增的) `1..5`,`2..6`千万不要`6..2` -- `::`作用域限定符 -- `inner`类可以标记为`inner {: .keyword }`以便能够访问外部类的成员。内部类会带有一个对外部类的对象的引用 -- `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 -用到 +- [上一篇:Kotlin学习教程(九)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" deleted file mode 100644 index 3cc7aa0c..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\233\233).md" +++ /dev/null @@ -1,581 +0,0 @@ -Kotlin学习教程(四) -=== - - -### 数据类:使用`data class`定义 - -数据类是一种非常强大的类。在[Kotlin学习教程(一)][1]中最开始的用的简洁的示例代码就是一个数据类。这里我们再拿过来: -```java -public class Artist { - private long id; - private String name; - private String url; - private String mbid; - - public long getId() { - return id; - } - - - public void setId(long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - - public String getMbid() { - return mbid; - } - - public void setMbid(String mbid) { - this.mbid = mbid; - } - - @Override public String toString() { - return "Artist{" + - "id=" + id + - ", name='" + name + '\'' + - ", url='" + url + '\'' + - ", mbid='" + mbid + '\'' + - '}'; - } -} -``` -使用`Kotlin`: -```kotlin -data class Artist( - var id: Long, - var name: String, - var url: String, - var mbid: String) -``` - -通过数据类,会自动提供以下函数: -- 所有属性的`get() set()`方法 -- `equals()` -- `hashCode()` -- `copy()` -- `toString()` -- 一系列可以映射对象到变量中的函数(后面再说)。 - -如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。 -这个任务是非常重复且不简洁的。 - -举个例子,如果要修改`Person`类中`charon`的`age`: - -```kotlin -data class Person(val name: String, - val age: Int) -``` - -```kotlin -val charon = Person("charon", 18) -val charon2 = charon.copy(age = 19) -``` -如上,我们拷贝了`charon`对象然后只修改了`age`的属性而没有修改这个对象的其它状态。 - -### 多声明 - -多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 - -```kotlin -var personD = PersonData("PersonData", 20, "male") -var (name, age) = personD - - -Log.d("test", "name = $name, age = $age") - -//输出 -name = PersonData, age = 20 -``` - -上面的多声明,大概可以翻译成这样: - -```kotlin -var name = f1.component1() -var age = f1.component2() -``` - - -### 继承 - -在`Kotlin`中所有类都有一个共同的超类`Any`,这对于没有超类型声明的类是默认超类: -```kotlin -class Person // 从 Any 隐式继承 -``` - -`Any`不是`java.lang.Object`。它除了`equals()`、`hashCode()`和`toString()`外没有任何成员。 -`Kotlin`中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, -要么就禁止继承。所以我们只能继承那些明确声明`open`或者`abstract`的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后: -```kotlin -open class Person(num: Int) -// 继承 -class SuperPerson(num: Int) : Person(num) -``` -如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 -如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 -注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: -```kotlin -class MyView : View { - constructor(ctx: Context) : super(ctx) - constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) -} -``` - -### 覆盖 - -##### 方法覆盖 - - -只能重写显示标注可覆盖的方法: -```kotlin -open class Person(num: Int) { - open fun changeName(name: String) { - - } - - fun changeAge(age: Int) { - - } -} - -class SuperPerson(num: Int) : Person(num) { - override fun changeName(name: String) { - // 通过super关键字调用超类实现 - super.changeName(name) - } -} -``` -`SuperPerson.changeName()`方法前面必须加上`override`标注,不然编译器将会报错。如果像上面`Person.changeAge()`方法没有标注`open`, -则子类中不能定义相同的方法: -```kotlin -class SuperPerson(num: Int) : Person(num) { - override fun changeName(name: String) { - super.changeName(name) - } - - // 编译器报错 - fun changeAge(age: Int) { - - } - // 重载是可以的 - fun changeAge(name: String) { - - } - // 重载是可以的 - fun changeAge(age: Int, name: String) { - - } -} -``` - -标记为`override`的成员本身是开放的,也就是说,它可以在子类中覆盖。如果你想禁止再次覆盖,可以使用`final`关键字: - -```kotlin -open class SuperPerson(num: Int) : Person(num) { - final override fun changeName(name: String) { - super.changeName(name) - } -} -``` - -##### 属性覆盖 - -属性覆盖与方法覆盖类似,只能覆盖显示标明`open`的属性,并且要用`override`开头: - -```kotlin -open class Person(num: Int) { - open val name: String = "" - - open fun changeName(name: String) { - - } - - fun changeAge(age: Int) { - - } -} - -open class SuperPerson(num: Int) : Person(num) { - override val name: String - get() = super.name - - final override fun changeName(name: String) { - super.changeName(name) - } - -} -``` - -每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 - - - -### 抽象类 - -类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 -言而喻。 - -我们可以用一个抽象成员覆盖一个非抽象的开放成员: -```kotlin -open class Base { - open fun f() {} -} - -abstract class Derived : Base() { - override abstract fun f() -} -``` - -### 修饰符 - -`Kotlin`中修饰符是与`Java`中的有些不同。在`kotlin`中默认的修饰符是`public`,这节约了很多的时间和字符。 - -- `private` - `private`修饰符是最限制的修饰符,和`Java`中`private`一样。它表示它只能被自己所在的文件可见。所以如果我们给一个类声明为`private`, - 我们就不能在定义这个类之外的文件中使用它。 - 另一方面,如果我们在一个类里面使用了private修饰符,那访问权限就被限制在这个类里面了。甚至是继承这个类的子类也不能使用它。 - -- `protected`. - 与`Java`一样,它可以被成员自己和继承它的成员可见。 - -- `internal` - 如果是一个定义为`internal`的包成员的话,对所在的整个`module`可见。如果它是一个其它领域的成员,它就需要依赖那个领域的可见性了。 - 比如如果写了一个`private`类,那么它的`internal`修饰的函数的可见性就会限制与它所在的这个类的可见性。 - -- `public`. - 你应该可以才想到,这是最没有限制的修饰符。这是默认的修饰符,成员在任何地方被修饰为public,很明显它只限制于它的领域。 - - -### 数组 - -数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 -设置数组对应位置的值。 -`Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 -```kotlin -val array = arrayOf(1, 2, 3) -val countries = arrayOf("UK", "Germany", "Italy") -val numbers = intArrayOf(10, 20, 30) -val array1 = Array(10, { k -> k * k }) -val longArray = emptyArray() -val studentArray = Array(2) -studentArray[0] = Student("james") -``` - -和`Java`不一样的是`Kotlin`的数组是容器类,提供了`ByteArray`,`CharArray`,`ShortArray`,`IntArray`,`LongArray`,`BooleanArray`, -`FloatArray`和`DoubleArray`。 - -### 集合 - - -`Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 -改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 - -`Kotlin`没有专门的语法结构创建`list`或`set`。要用标准库的方法如`listOf()`、`mutableListOf()`、`setOf()`、`mutableSetOf()`。 -创建`map`可以用`mapOf(a to b, c to d)`。 - -```kotlin -fun main(args : Array) { - var lists = listOf("a", "b", "c") - for(list in lists) { - println(list) - } -} -``` - -```kotlin -fun main(args : Array) { - var map = TreeMap() - map["0"] = "0 haha" - map["1"] = "1 haha" - map["2"] = "2 haha" - - println(map["1"]) -} -``` - -```kotlin -val numbers: MutableList = mutableListOf(1, 2, 3) -val readOnlyView: List = numbers -println(numbers) // 输出 "[1, 2, 3]" -numbers.add(4) -println(readOnlyView) // 输出 "[1, 2, 3, 4]" -readOnlyView.clear() // -> 不能编译 - -val strings = hashSetOf("a", "b", "c", "c") -assert(strings.size == 3) -``` - - -### 可`null`类型 - - -因为在`Kotlin`中一切都是对象,一切都是可`null`的。当某个变量的值可以为`null`的时候,必须在声明处的类型后添加`?`来标识该引用可为空。 -`Kotlin`通过`?`将是否允许为空分割开来,比如`str:String`为不能空,加上`?`后的`str:String?`为允许空,通过这种方式,将本是不能确定的变 -量人为的加入了限制条件。而不符合条件的输入,则会在`IDE`上显示编译错误而无法执行。 - -```kotlin -var value1: String -value1 = null // 编译错误 Null can not be a value of a non-null type String - -var value2 : String? -value2 = null // 编译通过 -``` -在对变量进行操作时,如果变量是可能为空的,那么将不能直接调用,因为编译器不知道你的变量是否为空,所以编译器就要求你一定要对变量进行判断 -```kotlin -var str : String? = null -// 编译错误 Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String? -str.length -// 编译能通过,这表示如果str不为空的时候执行length方法 -str?.length -``` - -那么问题来了,我们知道在`java`中`String.length`返回的是`int`,上面的`str?.length`既然编译通过了,那么它返回了什么?我们可以这么写: - -`var result = str?.length` - -这么写编译器是能通过的,那么`result`的类型是什么呢?在`Kotlin`中,编译器会自动根据结果判断变量的类型,翻译成普通代码如下: - -```kotlin -if(str == null) - result = null; // 这里result为一个引用类型 -else - result = str.length; // 这里result为Int -``` -那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的符号`:`双 -感叹号`!!`,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: -```kotlin -var str : String? = null -var result : Int = str!!.length -``` - -这样的话,就能保证`result`的数据类型,但是这样还有一个问题,那就是`str`的定义是可为空的,上面的代码中,`str`就是空,这时候下面的操作虽然 -不会报编译异常,但是运行时就会见到我们熟悉的空指针异常`NullPointerExectpion`,这显然不是我们希望见到的,也不是`kotlin`愿意见到的。 -`java`中的三元操作符大家应该都很熟悉了,`kotlin`中也有类似的,它很好的解决了刚刚说到的问题。在`kotlin`中,三元操作符是`?:`,写起来也 -比`java`要方便一些。 - -```kotlin -var str : String? = null -var result = str?.length ?: -1 -//等价于 -var result : Int = if(str != null) str.length else -1 -``` - -`if null`缩写 - -```kotlin -val data = …… -val email = data["email"] ?: throw IllegalStateException("Email is missing!") -``` - -如果`?:`左侧表达式非空,`elvis`操作符就返回其左侧表达式,否则返回右侧表达式。 -请注意,当且仅当左侧为空时,才会对右侧表达式求值。 - - -##### `!!`操作符 - -我们可以写`b!!`,这会返回一个非空的`b`值 -(例如:在我们例子中的`String`)或者如果`b`为空,就会抛出一个空指针异常: -```kotlin -val l = b!!.length -``` - -因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 - - -#### 安全的类型转换 - -如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 -另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: - -```kotlin -val aInt: Int? = a as? Int -``` - -#### 可空类型的集合 - -如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 -```kotlin -val nullableList: List = listOf(1, 2, null, 4) -val intList: List = nullableList.filterNotNull() -``` - -### 表达式 - -##### `if`表达式 - -在`Kotlin`中,`if`是一个表达式,即它会返回一个值。因此就不需要三元运算符`条件 ? 然后 : 否则`,因为普通的`if`就能胜任这个角色。 -`if`的分支可以是代码块,最后的表达式作为该块的值: -```kotlin -val max = if (a > b) { - print("Choose a") - a -} else { - print("Choose b") - b -} -``` - - -##### `when`表达式 - -`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 -式。 -与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 - -对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: -```kotlin -when (x){ - 1 -> print("x == 1") - 2 -> print("x == 2") - else -> { - print("I'm a block") - print("x is neither 1 nor 2") - } -} -``` - -因为它是一个表达式,它也可以返回一个值。我们需要考虑什么时候作为一个表达式使用,它必须要覆盖所有分支的可能性或者实现`else`分支。否则它不会被 -编译成功: - -```kotlin -val result = when (x) { - 0, 1 -> "binary" - else -> "error" -} -``` - -如你所见,条件可以是一系列被逗号分割的值。但是它可以更多的匹配方式。比如,我们可以检测参数类型并进行判断: - -```kotlin -when(view) { - is TextView -> view.setText("I'm a TextView") - is EditText -> toast("EditText value: ${view.getText()}") - is ViewGroup -> toast("Number of children: ${view.getChildCount()} ") - else -> view.visibility = View.GONE -} -``` - -##### for循环 - -```kotlin -val items = listOf("apple", "banana", "kiwi") -for (item in items) { - println(item) -} - -for (i in array.indices) - print(array[i]) -``` - -### 使用类型检测及自动类型转换 - -`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, -无需显式转换: - -```kotlin -fun getStringLength(obj: Any): Int? { - if (obj !is String) return null - - // `obj` 在这一分支自动转换为 `String` - return obj.length -} -``` - -### 返回和跳转 - -`Kotlin`有三种结构化跳转表达式: - -- `return`:默认从最直接包围它的函数或者匿名函数返回。 -- `break`:终止最直接包围它的循环。 -- `continue`:继续下一次最直接包围它的循环。 - -在`Kotlin`中任何表达式都可以用标签`label`来标记。标签的格式为标识符后跟`@`符号,例如:`abc@`、`fooBar@`都是有效的标签。 - -要为一个表达式加标签,我们只要在其前加标签即可。 -```kotlin -loop@ for (i in 1..100) { - for (j in 1..100) { - if (……) break@loop - } -} -``` - - -### Ranges - -`Range`表达式使用一个`..`操作符。表示就是一个该范围内的数据的数组,包含头和尾 - -```kotlin -var nums = 1..100 -for(num in nums) { - println(num) - // 打印出1 2 3 ....100 -} -``` - -```kotlin -if(i >= 0 && i <= 10) - println(i) -``` -转换成 - -```kotlin -if (i in 0..10) - println(i) -``` -Ranges默认会自增长,所以如果像以下的代码: -```kotlin -for (i in 10..0) - println(i) -``` -它就不会做任何事情。但是你可以使用`downTo`函数: -```kotlin -for(i in 10 downTo 0) - println(i) -``` - -我们可以在`Ranges`中使用`step`来定义一个从`1`到一个值的不同的空隙: -```kotlin -for (i in 1..4 step 2) println(i) -for (i in 4 downTo 1 step 2) println(i) -``` - -### Until - -上面的`Range`是包含了头和尾,那如果只想包含头不包含尾呢? 就要用`until` - -```kotlin -var nums = 1 until 100 -for(num in nums) { - println(num) - // 这样打印出来是1 2 3 .....99 -} -``` - - -[上一篇:Kotlin学习教程(三)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md) -[下一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) - - -[1]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md "Kotlin学习教程(一)" - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! - From f38bb8f2c5427b9d9e0e4852e6874ecdf64d6a34 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 8 Apr 2021 17:26:40 +0800 Subject: [PATCH 020/183] update kotlin notes --- ...&\347\261\273&\346\216\245\345\217\243.md" | 185 +++-- ...76\350\256\241\346\250\241\345\274\217.md" | 79 +- ...05\350\201\224\345\207\275\346\225\260.md" | 37 +- ...0\347\273\204&\351\233\206\345\220\210.md" | 50 +- ...7&\345\205\263\351\224\256\345\255\227.md" | 15 +- ...2\344\270\276&\345\247\224\346\211\230.md" | 134 ++-- ...47\346\211\277\351\227\256\351\242\230.md" | 12 +- ...5\345\260\204&\346\211\251\345\261\225.md" | 351 +++++++-- .../8.Kotlin_\345\215\217\347\250\213.md" | 702 +++++++++++++++++- .../9.Kotlin_androidktx.md | 160 +--- ...\346\225\231\347\250\213(\345\205\253).md" | 460 ------------ ...\346\225\231\347\250\213(\345\215\201).md" | 13 - ...72\347\241\200\347\237\245\350\257\206.md" | 8 +- ...04\344\273\266\345\260\201\350\243\205.md" | 45 ++ ...47\350\203\275\344\274\230\345\214\226.md" | 35 + .../HLS.md" | 8 + 16 files changed, 1366 insertions(+), 928 deletions(-) rename "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" => "KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" (75%) rename "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" => KotlinCourse/9.Kotlin_androidktx.md (56%) delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" delete mode 100644 "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index b4432c22..995523d5 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -1,4 +1,4 @@ -1.Kotlin_简介 +1.Kotlin_简介&变量&类&接口 === ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_kotlin.jpeg?raw=true) @@ -13,7 +13,7 @@ [Kotlin官网](https://kotlinlang.org/) `JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 -一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 +一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 但是JetBrains团队设计Kotlin所要面临的第一个问题就是必须兼容他们所拥有的数百万行Java代码库,这也代表了Kotlin基于整个Java社区所承载的使命之一,即需要与现有的Java代码完全兼容。这个背景也决定了Kotlin的核心目标--为Java程序员提供一门更好的变成语言。 很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 的一句话: @@ -113,8 +113,7 @@ data class Person( ## 创建`Kotlin`项目 -`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`,目前早就有预览版了 -[Android Studio Preview](https://developer.android.com/studio/preview/index.html)(个人感觉很好用,比2.3.3版本强多了)。 +`Google`宣布在`Android Studio 3.0`版本会全面支持`Kotlin`, 直接通过`New Project`创建就可以,与创建普通`Java`项目唯一不同的是要勾选`Include Kotlin support`的选项。 @@ -143,7 +142,7 @@ class MainActivity : AppCompatActivity() { } ``` -我们就从`MainActivity`的代码开始介绍一些基本的语法。 + ## 变量 @@ -161,7 +160,7 @@ book.printName() // Diving into Kotlin 再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 -字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型: +字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践时省略变量的类型我们可以让编译器自己去推断出具体的类型,**Kotlin拥有比Java更加强大的类型推导功能,这避免了静态类型语言在编码时需要书写大量类型的弊端**: ```kotlin var age = 18 // int val name = "charon" // string @@ -170,8 +169,7 @@ var weight = 70.5 // double ``` 在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 -当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似 -的,但是有一些不同之处你可能需要考虑到: +当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似的,但是有一些不同之处你可能需要考虑到: - 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: ```kotlin @@ -216,10 +214,10 @@ var weight = 70.5 // double - IO操作,如写数据到磁盘 - UI操作,如修改了一个按钮的可操作状态 -来看一个实际的例子:先用va来声明一个变量a,然后在count函数内部对其进行自增操作: +来看一个实际的例子:先用var来声明一个变量a,然后在count函数内部对其进行自增操作: ```kotlin -val a = 1 +var a = 1 fun count(x: Int) { a = a + 1 println(x + a) @@ -252,6 +250,8 @@ class MainActivity : AppCompatActivity() { ## 后端变量`Backing Fields`. +`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: + 在`kotlin`的`getter`和`setter`是不允许本身的局部变量的,因为属性的调用也是对`get`的调用,因此会产生递归,造成内存溢出。 例如: @@ -259,21 +259,42 @@ class MainActivity : AppCompatActivity() { ```kotlin var count = 1 var size: Int = 2 -set(value) { - Log.e("text", "count : ${count++}") - size = if (value > 10) 15 else 0 -} + set(value) { + Log.e("text", "count : ${count++}") + size = if (value > 10) 15 else 0 + } ``` 这个例子中就会内存溢出。 `kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。 +我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 + +```kotlin +class A { + var count = 1 + var size: Int = 2 + set(value) { + field = if (value > 10) 15 else 0 + } + get() { + return if (field == 15) 1 else 0 + } +} +fun main() { + val a = A() + a.size = 11 + println("${a.size}") +} +// +1 +``` + + ## 延迟初始化 我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 -假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 -延迟其初始化,此属性用`lateinit`修饰符修饰。 +假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),与`Kotlin`的规则是相背的,此时我们可以声明一个属性并延迟其初始化,此属性用`lateinit`修饰符修饰。 ```kotlin class MainActivity : AppCompatActivity() { @@ -628,10 +649,6 @@ val charon2 = charon.copy(age = 19) - data class之前不能用abstract、open、sealed或者inner进行修饰 - 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 - - - - ## 多声明 多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 @@ -718,6 +735,32 @@ public final class Bird { +### Any + +我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 + +但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 + + + +#### Any:非空类型的跟类型 + +与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) + +与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: + +```kotlin +class Animal(val weight: Double) +``` + +#### Any?:所有类型的根类型 + +如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 + + + ## 覆盖 ##### 方法覆盖 @@ -812,8 +855,7 @@ open class SuperPerson(num: Int) : Person(num) { ## 抽象类 -类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不 -言而喻。 +类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不言而喻。 我们可以用一个抽象成员覆盖一个非抽象的开放成员: @@ -859,7 +901,7 @@ interface FlyingAnimal { interface Flyer { val height = 1000 // error Property initializers are not allowed in interfaces val speed: Int - // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的 + // 可以支持默认实现方法,反编译可以看到是通过静态内部类来提供fly方法的默认实现的,Java8也开始支持了接口方法的默认实现 fun fly() { println("I can fly") } @@ -908,8 +950,6 @@ fun add(x: Int,y: Int) : Int = x + y // 省略了{} Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 - - 我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: ```kotlin @@ -924,27 +964,7 @@ toast("Hello") toast("Hello", Toast.LENGTH_LONG) ``` -### 自定义`get set`方法: - -`Kotlin`会默认创建`set get`方法,我们也可以自定义`get set`方法: -`kotlin`预留了一个在`set`和`get`中访问的变量`field`关键字: - -```kotlin -class Person constructor() { - var name: String = "" - get() = field - set(value) { - field = "$value" - } - var age: Int = 0 - get() = field - set(value) { - field = value - } -} -``` -按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 ### 可变长参数函数:使用`vararg`关键字 @@ -961,7 +981,33 @@ fun main(args: Array) { } ``` -### 命名风格 + + +### `Unit`:让函数调用皆为表达式 + +如果函数返回`Unit`类型,该返回类型应该省略: + +```kotlin +fun foo() { // 省略了 ": Unit" + +} +``` + +之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: + +```java +void foo() { + System.out.println("return nothing") +} +``` + +所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 + +Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 + + + +## 命名风格 如果拿不准的时候,默认使用`Java`的编码规范,比如: @@ -972,6 +1018,29 @@ fun main(args: Array) { - 公有函数应撰写函数文档,这样这些文档才会出现在`Kotlin Doc`中 + +### 类布局 + +通常,一个类的内容按以下顺序排列: + +- 属性声明与初始化块 +- 次构造函数 +- 方法声明 +- 伴生对象 + +不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。 + +将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。 + +### 接口实现布局 + +在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要, 还要插入用于实现的额外的私有方法) + +### 重载布局 + +在类中总是将重载放在一起。 + + ### 冒号 类型和超类型之间的冒号前要有一个空格,而实例和类型之间的冒号前不要有空格: @@ -1016,28 +1085,6 @@ class Person( } ``` -### `Unit`:让函数调用皆为表达式 - -如果函数返回`Unit`类型,该返回类型应该省略: - -```kotlin -fun foo() { // 省略了 ": Unit" - -} -``` - -之所以不能说Java中的函数调用皆是表达式,是因为存在特例void。众所周知,在Java中如果声明的函数没有返回值,那么它就需要用void来修饰,如: - -```java -void foo() { - System.out.println("return nothing") -} -``` - -所以foo()就不具有值和类型信息,它就不能算作一个表达式。在Kotlin中,函数在所有的情况下都具有返回类型,所以他们引入了Unit来替代Java中的void关键字。 - -Unit与Int一样,都是一种类型,然而它不代表任何信息,用面向对象的术语来描述就是一个单例,它的实例只有一个,可写为()。 - [下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) diff --git "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" similarity index 75% rename from "KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" rename to "KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" index 5a3a5989..fc6a644e 100644 --- "a/KotlinCourse/11.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,11 +1,11 @@ -11.Kotlin_设计模式 +10.Kotlin_设计模式 === - ## 工厂模式 -简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类与多个子类的时候,我们可以通过这种模式来创建子类对象。 +简单工厂的模式,它的核心作用是通过一个工厂类隐藏对象实例的创建逻辑,而不需要暴露给客户端。典型的使用场景就是当拥有一个父类 +与多个子类的时候,我们可以通过这种模式来创建子类对象。 假设现在有一个电脑加工厂,同时生产个人电脑和服务器主机。我们用熟悉的工厂模式设计描述其业务逻辑: @@ -34,7 +34,9 @@ fun main() { } ``` -以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 +以上代码通过调用ComputerFactory类的produce方法来创建不同的Computer子类对象,这样我们就把创建实例的逻辑和客户端之间实现解耦。 +这是用Kotlin模仿Java中很标准的工厂模式设计,它改善了程序的可维护性,但创建对象的表达上却显得不够简洁。 +当我们在不同的地方创建Computer的子类对象时,我们都需要先创建一个ComputerFactory类对象。 ### 用单例代替工厂类 @@ -56,7 +58,8 @@ fun main() { } ``` -由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符重载invoke方法来代替produce,从而进一步简化表达: +由于我们通过传入Computer类型来创建不同的对象,所以这里的produce又显得多余。我们可以用运算符重载来通过operator操作符 +重载invoke方法来代替produce,从而进一步简化表达: ```kotlin object ComputerFactory { @@ -101,7 +104,8 @@ fun main() { } ``` -我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用Factory来命名Computer的伴生对象,如下: +我们可以直接通过Computer来调用其伴生对象中的方法。当然,如果你觉得还是Factory这个名字好,那么也没有问题,我们可以用 +Factory来命名Computer的伴生对象,如下: ```kotlin interface Computer { @@ -126,7 +130,9 @@ fun main() { ### 扩展伴生对象方法 -依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: +依靠伴生对象的特性,我们已经很好地实现了经典的工厂模式。同时,这种方式还有一种优势,它比原有Java中的设计更加强大。 +假设实际业务中我们是Computer接口的使用者,比如它是工程引入的第三方类库,所有的类的实现细节都得到了很好的隐藏。 +那么,如果我们希望进一步改造其中的逻辑,Kotlin中伴生对象的方式同样可以依靠其扩展函数的特性,很好的实现这一需求: 比如我们希望给Computer增加一种功能,通过CPU型号来判断电脑类型,那么可以如下实现: @@ -146,7 +152,10 @@ fun main() { ### 内联函数简化抽象工厂 -Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌,比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护,所以这时候我们就需要引入抽象工厂模式。 +Kotlin中的内联函数有一个很大的作用,就是可以具体化参数类型。利用这一特性,可以改进一种更复杂的工厂模式,称为抽象工厂。 +上面的例子中已经用工厂模式很好的处理了一个产品登记结构的问题。但是如果现在引入了品牌商的概念,我们有好几个不同的电脑品牌, +比如Dell、Asus、Acer,那么就有必要再增加一个工厂类。然而,我们并不希望对每个模型都建立一个工厂,这会让代码变得难以维护, +所以这时候我们就需要引入抽象工厂模式。 @@ -185,9 +194,12 @@ fun main(args: Array) { } ``` -可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 +可以看出,每个电脑品牌拥有一个代表电脑产品的类,它们都实现了Computer接口。此外每个品牌也还有一个用于生产电脑的 +AbstractFactory子类,可通过AbstractFactory类的伴生对象中的invoke方法,来构造具体品牌的工厂类对象。 -由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 +由于Kotlin语法的简洁,以上例子的抽象工厂类的设计也比较直观。然而,当你每次创建具体的工厂类时,都需要传入一个具体的 +工厂类对象作为参数进行构造,这个在语法上显然不够优雅。下面我们就来看看,如何用Kotlin中的内联函数来改善这一情况。 +我们所需要做的,就是去重新实现AbstractFactory类中的invoke方法。 ```kotlin abstract class AbstractFactory { @@ -205,7 +217,8 @@ abstract class AbstractFactory { } ``` -这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。我们来分析下这段代码: +这下我们的invoke方法定义的前缀变长了很多,但是不要害怕,如果你已经掌握了内联函数的具体应用,应该会很容易理解它。 +我们来分析下这段代码: - 通过将invoke方法用inline定义为内联函数,我们就可以引入reified关键字,使用具体化参数类型的语法特性。 - 要具体化的参数类型为Computer,在invoke方法中我们通过判断它的具体类型,来返回对应的工厂类对象。 @@ -228,9 +241,12 @@ fun main(args: Array) { 构造者模式与单例模式一样,它主要做的事情就是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。 -工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性:代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 +工厂模式和构造函数都存在相同的问题,就是不能很好地扩展到大量的可选参数。假设我们现在有个机器人类,它含有多个属性: +代号、名字、电池、重量、高度、速度、音量等。很多产品都不具有其中的某些属性,比如不能走、不能发声,甚至有的机器人也不需要电池。 -一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式,即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少,但同样存在明显的确定,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 +一种糟糕的做法就是设计一个一开头你所看到Robot类,把所有的属性都作为构造函数的参数。或者,你也可能采用过重叠构造器模式, +即先提供一个只有必要参数的构造函数,然后再提供其他更多的构造函数,分别具有不同情况的可选属性。虽然这种模式在调用的时候改进不少, +但同样存在明显的缺点,因为随着构造函数的参数数量增加,很快我们就会失去控制,代码变得难以维护。 构建者模式可以避免以上问题,我们用Kotlin来实现Java中的构建者模式: @@ -281,7 +297,8 @@ var robot = Robot.Builder("007") - 通过在Builder类中定义set方法来对可选的属性进行设置 - 最终调用Builder类中的build方法来返回一个Robot对象 -这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 +这种链式调用的设计看起来确实优雅了很多,同时对于可选参数的设置也显得比较语义化。此外,构建者模式另外一个好处就是解决了 +多个可选参数的问题,当我们创建对象实例时,只需要用set方法对需要的参数进行赋值即可。 然而,构建者模式也存在一些不足: @@ -289,7 +306,8 @@ var robot = Robot.Builder("007") - 你可能会在使用Builder的时候忘记在最后调用build方法 - 由于在创建对象的时候,必须先创建它的构造器,因此额外增加了多余的开销,在某些十分注重性能的情况下,可能就存在一定的问题。 -事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时,是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 +事实上,当用Kotlin设计程序时,我们可以在绝大多数情况下避免使用构建者模式。《Effective Java》在介绍构建者模式时, +是这样子描述它的:本质上builder模式模拟了具名的可选参数。幸运的是,Kotlin也是这样一门拥有具名可选参数的编程语言。 #### 具名的可选参数 @@ -330,7 +348,8 @@ private fun main() { #### require方法对参数进行约束 -我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: +我们再来看看构建者模式的另外一个作用,就是可以在build方法中对参数添加约束条件。举个例子,假设一个机器人的重量必须根据 +电池的型号决定,那么在未传入电池型号之前,你便不能对weight属性进行赋值,否则就会抛出异常。现在重新修改一下上面build方法的实现: ```kotlin fun build(): Robot { @@ -344,7 +363,8 @@ fun build(): Robot { 这种在build方法中对参数进行约束的手段,可以让业务变得更加安全。那么,通过具名的可选参数来构造类的方案该如何实现呢? -显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 +显然,我们同样可以在Robot类的init方法中增加以上的校检代码。然而在Kotlin中,我们在类或函数中还可以使用require关键字 +进行参数限制,本质上它是一个内联的方法,有点类似于Java的assert。 ```kotlin class Robot( @@ -361,22 +381,22 @@ class Robot( } ``` -可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式,因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 - - - +可见,Kotlin的require方法可以让我们的参数约束代码在语义上变得更加友好。总的来说,在Kotlin中我们应该尽量避免使用构建者模式, +因为Kotlin支持具名的可选参数,这让我们可以在构造一个具有多个可选参数类的场景中,设计出更加简洁并利于维护的代码。 ## 观察者模式 -观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时,需要通知相应的观察者,使这些观察者对象能够自动更新。 +观察者模式定义了一个一对多的依赖关系,让一个或多个观察者对象监听一个主题对象。这样一来,当被观察者状态发生改变时, +需要通知相应的观察者,使这些观察者对象能够自动更新。 简单来说,观察者模式无非做两件事情: - 订阅者(observer)添加或删除对发布者(publisher)的状态监听。 - 发布者状态改变时,将事件通知给监听它的所有观察者,然后观察者执行响应逻辑。 -Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用它们来实现一个动态更新股价的例子。 +Java自身的标准库提供了java.util.Observable类和java.util.Observer接口,来帮助实现观察者模式,接下来我们就采用 +它们来实现一个动态更新股价的例子。 ```kotlin class StockUpdate: Observable() { @@ -400,9 +420,8 @@ fun main(args: Array) { } ``` -上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性,也可以利用它来实现同样的场景。 - -我们可以先用这一委托属性来改造以上的程序: +上面是通过Kotlin使用Java标准库中的类和方法来实现了观察者模式。事实上,Kotlin的标准库额外引入了可被观察的委托属性, +也可以利用它来实现同样的场景。我们可以先用这一委托属性来改造以上的程序: ```kotlin import kotlin.properties.Delegates @@ -441,9 +460,13 @@ The latest stock price has risen to 100 The latest stock price has fell to 98 ``` -如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应,我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得臃肿。换个角度,如果我们把发布者的事件推送看成一个第三方服务,那么它提供的API接口只有一个,API调用者必须承担更多的职责。 +如果你仔细思考,会发现实现java.util.Observer接口的类只能覆写update方法来编写响应逻辑,也就是说如果存在多种不同的逻辑响应, +我们也必须通过在该方法中进行区分实现,显然这会让订阅者的代码显得冗余。换个角度,如果我们把发布者的事件推送看成一个第三方服务, +那么它提供的API接口只有一个,API调用者必须承担更多的职责。 -显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中实现该接口的onRise和onFall方法,实现了解耦。 +显然,使用Delegates.observable()的方案更加灵活。它提供了三个参数,依次代表委托属性的元数据KProperty对象、旧值以及新值。 +通过额外定义一个StockUpdateListener接口,我们可以把上涨和下跌的不同响应逻辑封装成接口方法,从而在StockDisplay中 +实现该接口的onRise和onFall方法,实现了解耦。 diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 23458c79..77f1b45e 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -1,23 +1,6 @@ -Kotlin之Lambda&内联函数(七) +2.Kotlin_高阶函数&Lambda&内联函数 === - - -函数式语言一个典型的特征就在于函数是头等公民-我们不仅可以像类一样在顶层直接定义一个函数,也可以在一个函数内部定义一个函数,例如: - -```kotlin -fun foo(x: Int) { - fun double(y: Int): Int { - return y * 2 - } - println(double(x)) -} - -执行foo(1)结果为2 -``` - - - ## 高阶函数 Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。 @@ -33,7 +16,7 @@ fun foo(x: Int) { ### 抽象和高阶函数 -我们会善于对熟悉 或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 +我们会善于对熟悉或重复的事物进行抽象,比如2岁左右的小孩就会开始认识数字1、2、3....之后,我们总结除了一些公共的行为,如对数字做加减、求立方,这被称为过程,它接收的数字是一种数据,然后也可能产生另一种数据。 过程也是一种抽象,几乎我们所熟悉的所有高级语言都包含了定义过程的能力,也就是函数。 @@ -131,6 +114,8 @@ class CountryTest { (errCode: Int, errMsg: String) -> Unit // ?表示可选,可在某种情况下为空 ((errCode: Int, errMsg: String?) -> Unit)? + // 表示传入一个类型为Int的参数,然后返回另一个类型为(Int) -> Unit的函数 + (Int) -> ((Int) -> Unit) ``` 在学习了Kotlin函数类型知识之后,Shaw重新定义了filterCountries方法的参数声明: @@ -190,7 +175,19 @@ class Book(val name: String) { Book::name ``` -以上创建的Book::name的类型为(Book) -> String。 +以上创建的Book::name的类型为(Book) -> String。当我们再对Book类对象的集合应用一些函数式API的时候,这会显得格外有用,比如: + +```kotlin +fun main(args: Array) { + val bookNames = listOf ( + Book("Thinking in java") + Book("Dive into Kotlin") + ).map(Book::name) + println(bookNames) +} +``` + + diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index 44f17930..3cab6d55 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(三) +3.Kotlin_数字&字符串&数组&集合 === 前面介绍了基本语法和编码规范后,接下来学习下基本类型。 @@ -314,15 +314,26 @@ val list = listOf(1, 2, 3, 4, 5) list.filter {it > 2}.map {it * 2} ``` -上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合,因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多,这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 +上面的写法很简洁,在处理集合时,类似于上面的操作能够帮助我们解决大部分的问题。 +但是list中的元素非常多的时候(比如超过10万),上面的操作在处理集合的时候就会显得比较低效。 +因为filter方法和map方法都会返回一个新的集合,也就是说上面的操作会产生两个临时集合, +因为list会先调用filter方法,然后产生的集合会再次调用map方法。如果list中的元素非常多, +这将会是一笔不小的开销。为了解决这一问题,序列(Sequence)就出现了。 ```kotlin list.asSequence().filter {it > 2}.map {it * 2}.toList() ``` -首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候,就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候,不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? +首先通过asSequence方法将一个列表转换为序列,然后在这个序列上进行相应的操作,最后通过 +toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。 +因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候, +就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候, +不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? -在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 +#### 惰性求值 +在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。 +在使用惰性求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用时才去求值。 +通过这种方式,不仅能得到性能上的提升,还有一个重要的好处就是它可以构造出一个无限的数据类型。 @@ -351,11 +362,17 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() } ``` - 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 + 上面操作中的println方法根本没有被执行,这说明filter和map方法的执行被延迟了,这就是惰性求值的体现。 + 惰性求值也被称为延迟求值,通过前面的定义我们知道,惰性求值仅仅在该值被需要的时候才会真正去求值。 + 那么这个”被需要“的状态怎么去触发呢?这就需要另外一个操作了-末端操作。 - 末端操作 - 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果,比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾,在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了,我们给上面的例子加上末端操作: + 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。 + 末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果, + 比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾, + 在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了, + 我们给上面的例子加上末端操作: ```kotlin list.asSequence().filter { @@ -377,7 +394,11 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() [6, 8, 10] ``` - 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后,第二个元素再去执行所有的操作,以此类推。放映到我们这个例子上面,就是第一个元素执行了filter之后再去执行map,然后第二个元素也是这样。 + 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。 + 普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。 + 而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后, + 第二个元素再去执行所有的操作,以此类推。放到我们这个例子上面,就是第一个元素执行了filter之后再去执行map, + 然后第二个元素也是这样。 #### 序列可以是无限的 @@ -385,13 +406,16 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() 那接下来,该怎么去实现一个自然数数列呢?采用一般的列表肯定是不行的,因为构造一个列表必须列举出列表中的元素,而我们是没有办法将自然数全部列举出来的。 -我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。好看Kotlin也给我们提供了这样一个方法,去创建无限的数列: +我们知道,自然数是有一定规律的,就是最后一个数永远是前一个数加1的结果,我们只需要实现一个列表,让这个列表描述这种规律,那么也就相当于实现了一个无限的自然数数列。 +好在Kotlin也给我们提供了这样一个方法,去创建无限的数列: ```kotlin val naturalNumList = generateSequence(0) { it + 1} ``` -通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候,才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: +通过上面这一行代码,我们就非常简单的实现了自然数数列,上面我们调用了一个方法generateSequence来创建序列。 +我们知道序列是惰性求值的,所以上面创建的序列是不会把所有的自然数都列举出来的,只有在我们调用一个末端操作的时候, +才去列举我们所需要的列表。比如我们要从这个自然数列表中取出前10个自然数: ```kotlin naturalNumList.takeWhile{it <= 9}.toList() @@ -399,9 +423,6 @@ naturalNumList.takeWhile{it <= 9}.toList() ``` - - - ### 可`null`类型 @@ -480,7 +501,7 @@ val email = data["email"] ?: throw IllegalStateException("Email is missing!") val l = b!!.length ``` -因此,如果你想要一个 NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 +因此,如果你想要一个NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 #### 安全的类型转换 @@ -540,9 +561,6 @@ loop@ for (i in 1..100) { ``` - - - [上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) [下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" index 43178b4e..f2196189 100644 --- "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(四) +4.Kotlin_表达式&关键字 === ## `if`表达式 @@ -18,8 +18,8 @@ val max = if (a > b) { ## `when`表达式 -`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。然后它会运行右边的表达 -式。 +`when`表达式与`Java`中的`switch/case`类似,但是要强大得多。这个表达式会去试图匹配所有可能的分支直到找到满意的一项。 +然后它会运行右边的表达式。 与`Java`的`switch/case`不同之处是参数可以是任何类型,并且分支也可以是一个条件。 对于默认的选项,我们可以增加一个`else`分支,它会在前面没有任何条件匹配时再执行。条件匹配成功后执行的代码也可以是代码块: @@ -90,7 +90,8 @@ fun schedule(sunny: Boolean, day: Day) = when (day) { } ``` -一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接,不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 +一个完整的when表达式类似switch语句,由when关键字开始,用花括号包含多个逻辑分支,每个分支由-> 连接, +不再需要switch的break(这真是一个恼人的关键字),由上往下匹配,一直匹配完为止,否则执行else分支的逻辑,类似switch的default。 到这里你可能会说上面的例子中,这样嵌套子when表达式,层次依旧比较深。要知道when表达式是很灵活的,我们很容易通过如下修改来解决这个问题: @@ -179,7 +180,7 @@ for(num in nums) { -上面in、step、downTo、until这几个,他们可以不通过点号,而是通过中缀表达式来被调用,从而让语法变得更加简洁直观。 +上面in、step、downTo、until这几个,他们可以不通过点号,而是通过**中缀表达式**来被调用,从而让语法变得更加简洁直观。 @@ -187,8 +188,8 @@ for(num in nums) { - `var`:定义变量 - `val`:定义常量 -- `fun`:定义方法 -- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值 +- `fun`:定义函数 +- `Unit`:默认方法返回值,类似于`Java`中的`void`,可以理解成返回没什么用的值类型 - `vararg`:可变参数 - `$`:字符串模板(取值) - 位运算符:`or`(按位或),`and`(按位与),`shl`(有符号左移),`shr`(有符号右移), diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 4bcc58cc..d6ea8ce6 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(五) +5.Kotlin_内部类&密封类&枚举&委托 === @@ -97,9 +97,11 @@ outter.Inner().execute() #### 内部类vs嵌套类 -在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类,必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 +在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类, +必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 -内部类和嵌套类有明显的差别,具体体现在:内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 +内部类和嵌套类有明显的差别,具体体现在: +- 内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 @@ -180,7 +182,8 @@ sealed class Bird { } ``` -Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。 +但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: ```java public abstract class Bird { @@ -206,7 +209,6 @@ public abstract class Bird { ``` - 密封类用来表示受限的类继承结构:当一个值为有限集中的 类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合 也是受限的,但每个枚举常量只存在一个实例,而密封类 @@ -279,9 +281,40 @@ val s = try { x as String } catch(e: ClassCastException) { null } ### 对象`(Object)` -在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的,但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 +在Java中,static是非常重要的特性,它可以用来修饰类、方法或属性。然而static修饰的内容都属于类的,而不是某个具体对象的, +但在定义时却与普通的变量和方法混杂在一起,显得格格不入。 + +在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。 +当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 + +声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 +它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 + +```kotlin +object Resource { + val name = "Name" +} +``` + +### 对象表达式 + +对象也能用于创建匿名类实现。 + +```java +recycler.adapter = object : RecyclerView.Adapter() { + override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { + } + + override fun getItemCount(): Int { + } + + override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { + } +} +``` +例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 -在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 +object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 #### 伴生对象 @@ -313,11 +346,15 @@ public class Prize { } ``` -上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法,也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 +上面是很常见的Java代码,也许你已经习惯了,但是如果仔细思考,会发现这种语法其实并不是非常好。因为在一个类中既有静态变量、静态方法, +也有普通变量、普通方法的声明。然而,静态变量和静态方法是属于一个类的,普通变量、普通方法是属于一个具体对象的。 +虽然有static作为区分,然而在代码结构上职能并不是区分得很清晰。 -那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说,这是一种利用companion object两个关键字创造的语法。 +那么,有没有一种方式能将这两部分代码清晰的分开,但又不失语义化呢?Kotlin中引入了伴生对象的概念,简单来说, +这是一种利用companion object两个关键字创造的语法。 -伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 +伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样, +全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 现在将上面的例子改写成伴生对象的版本: @@ -339,7 +376,8 @@ class Prize(val name: String, val count: Int, val type: Int) { } ``` -可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 +可以发现,该版本在语义上更清晰了。而且,companion object用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通 +方法和属性清晰的区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。 伴生对象的另一个作用是可以实现工厂方法模式。前面也说过如何从构造方法实现工厂方法模式,然而这种方法存在以下缺点: @@ -370,19 +408,28 @@ class Prize private constructor(val name: String, val count: Int, val type: Int) } ``` -总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 +总的来说,伴生对象是Kotlin中用来代替static关键字的一种方式,任何在Java类内部用static定义的内容都可以用Kotlin中的伴生对象来实现。 +然而,他们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。 - - -声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问 -它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 - -```kotlin -object Resource { - val name = "Name" +每个类都可以实现一个伴生对象,它是该类的所有实例共有的对象。它将类似于`Java`中的静态字段。 +```java +class App : Application() { + companion object { + lateinit var instance: App + private set + } + + override fun onCreate() { + super.onCreate() + instance = this + } } ``` +在这例子中,创建一个由`Application`扩展的(派送)的类,并且在`companion object`中存储它的唯一实例。 +`lateinit`表示这个属性开始是没有值得,但是,在使用前将被赋值(否则,就会抛出异常)。 +`private set`用于说明外部类不能对其进行赋值。 + ### 单例 因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。 @@ -393,7 +440,8 @@ object Resource { } ``` -因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 +因为对象就是具有单一实现的数据类型,所以在`kotlin`中对象就是单例。 +单例对象会在系统加载的时候初始化,当然全局只有一个,所以它是饿汉式的单例。 看一下自动生成的java代码: @@ -439,50 +487,6 @@ public static Instance getInstance() { 具体可以看: [Kotlin Object Declarations Are Initialized in Static Block](https://hanru-yeh.medium.com/kotlin-object-declarations-are-initialized-in-static-block-5e1c2e1c3401) -### 对象表达式 - -对象也能用于创建匿名类实现。 - -```java -recycler.adapter = object : RecyclerView.Adapter() { - override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) { - } - - override fun getItemCount(): Int { - } - - override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): RecyclerView.ViewHolder { - } -} -``` -例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 - -object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 - - - - -### 伴生对象`(Companion Object)` - -每个类都可以实现一个伴生对象,它是该类的所有实例共有的对象。它将类似于`Java`中的静态字段。 -```java -class App : Application() { - companion object { - lateinit var instance: App - private set - } - - override fun onCreate() { - super.onCreate() - instance = this - } -} -``` - -在这例子中,创建一个由`Application`扩展的(派送)的类,并且在`companion object`中存储它的唯一实例。 -`lateinit`表示这个属性开始是没有值得,但是,在使用前将被赋值(否则,就会抛出异常)。 -`private set`用于说明外部类不能对其进行赋值。 - ### 委托(代理) diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" index 7017a62f..29bd7403 100644 --- "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -1,4 +1,4 @@ -Kotlin学习教程之多继承问题 +6.Kotlin_多继承问题 === @@ -191,16 +191,6 @@ fun main(args: Array) { - - - - - - - - - - [上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) [下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" index fdbd19e2..42330826 100644 --- "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -1,4 +1,4 @@ -Kotlin学习教程(六) +7.Kotlin_注解&反射&扩展 === ### 注解 @@ -79,9 +79,20 @@ val numbers = listOf(1, 2, 3) println(numbers.filter(::isOdd)) // 输出 [1, 3] ``` -### 扩展 +## 扩展 -扩展是`kotlin`中非常重要的一个特性,它能让我们对一些已有的类进行功能增加、简化,使他们更好的应对我们的需求。 +扩展是`kotlin`中非常重要的一个特性,扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 +扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 +扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 +扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: +```kotlin +fun receiverType.functionName(params){ + body +} +receiverType:表示函数的接收者,也就是函数扩展的对象 +functionName:扩展函数的名称 +params:扩展函数的参数,可以为NULL +``` ```kotlin // 对Context的扩展,增加了toast方法。为了更好的看到效果,我还加了一段log日志 @@ -102,25 +113,10 @@ class MainActivity : AppCompatActivity() { Toast msg : hello, Extension ``` -按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很。 -上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数,可以被任何`Context`或者 -它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将它作为普通方法来直接调用。当然了`Anko`中已经包括了 -自己的`toast`扩展函数。有关`Anko`后面会讲到。 - -`Kotlin`扩展函数允许我们在不改变已有类的情况下,为类添加新的函数。 -扩展函数是指对类的方法进行扩展,写法和定义方法类似,但是要声明目标类,也就是对哪个类进行扩展,`kotlin`中称之为`Top Level`。 -扩展函数表现得就像是属于这个类的一样,而且我们可以使用`this`关键字和调用所有`public`方法。 -扩展函数可以在已有类中添加新的方法,不会对原类做修改,扩展函数定义形式: -```kotlin -fun receiverType.functionName(params){ - body -} -receiverType:表示函数的接收者,也就是函数扩展的对象 -functionName:扩展函数的名称 -params:扩展函数的参数,可以为NULL -``` - -在上面我们举的扩展的例子就是扩展函数.其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 +按照通常的做法,会写一个`ToastUtils`工具类,或者在`BaseActivity`中实现`toast`。但是使用扩展函数就会简单很多。 +上面的例子就是在`Context`中添加新的方法,让我们以更简单的方式去显示`toast`,并且不用传入任何`context`参数, +可以被任何`Context`或者它的子类调用。我们可以在任何地方(例如一个工具类文件中)声明这个函数,然后在`Activity`中将 +它作为普通方法来直接调用。其中`Context`就是目标类`Top Level`,我们把它放到方法名前,用点`.`表示从属关系。在方法体中用关键字 `this`对本体进行调用。和普通方法一样,如果有返回值,在方法后面跟上返回类型,我这里没有返回值,所以直接省略了。 扩展函数的原理: 通过查看对应的Java代码可以发现: @@ -135,8 +131,6 @@ fun MutableList.exchange(fromIndex: Int, toIndex: Int) { } ``` - - ```java package com.st.stplayer.extension; @@ -162,10 +156,8 @@ public final class ExtenndsKt { } ``` -通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 - - - +通过上面的Java代码可以看出,我们可以将扩展函数理解为静态方法。而静态方法的特点是:它独立于该类的任何对象,且不依赖类的特定实例, +被该类的所有实例共享。此外,被public修饰的静态方法本质上也就是全局方法。所以扩展函数不会带来额外的性能消耗。 ##### 扩展函数的作用域 @@ -175,7 +167,6 @@ public final class ExtenndsKt { ```kotlin package com.charon.ext -... val View.isVisible: Boolean get() = visibility == View.VISIBLE @@ -204,7 +195,8 @@ fun View.toBitmap(): Bitmap? { } ``` -我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: +我们知道在同一个包内是可以直接调用exchange方法的。如果需要在其他包中调用,只需要import相应的方法即可,这与调用Java全局静态方法类似。 +除此之外,实际开发时我们也可能会将扩展函数定义在一个Class内部统一管理: ```kotlin class Extends { @@ -216,7 +208,8 @@ class Extends { } ``` -当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: +当扩展函数定义在Extends类内部时,情况就与之前不一样了,这个时候你会发现,之前的exchange方法无法调用了(之前调用位置在Extends类外部)。 +借助IDEA我们可以查看到它对应的Java代码,这里展示关键部分: ```java public static final class Extends { @@ -241,7 +234,7 @@ class Son { } ``` -它包含一个成员方法foo(),加入我们哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: +它包含一个成员方法foo(),假如哪天心血来潮,想对这个方法做特殊实现,利用扩展函数可能会写出如下代码: ```kotlin fun Son.foo() = println("son called extension foo") @@ -254,9 +247,11 @@ object Test { } ``` -在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时,Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? +在我们的预期中,我们希望调用的是扩展函数foo(),但是输出的结果为: son called member foo。这表明当扩展函数和现有类的成员方法同时存在时, +Kotlin将会默认使用类的成员方法。看起来似乎不够合理,并且很容易引发一些问题:我定义了新的方法,为什么还是调用了旧的方法? -但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难:我们把不应该更改的方法改变了。所以在使用时,我们必须注意: 同名的类成员方法的优先级总高于扩展函数。 +但是换一个角度来思考,在多人开发的时候,如果每个人都对Son扩展了foo方法,是不是很容易造成混淆。对于第三方类库来说甚至是一场灾难: +我们把不应该更改的方法改变了。所以在使用时,我们必须注意:**同名的类成员方法的优先级总高于扩展函数**。 @@ -293,7 +288,7 @@ fun ImageView.loadImage(url: String) { } ``` -这样在调用的时候,不仅省去了更多的参数,而且ImageView的声明周期也得到了保证。 +这样在调用的时候,不仅省去了更多的参数,而且ImageView的生命周期也得到了保证。 在实际项目中,我们还需要考虑网络请求框架替换及维护的问题,一般会对图片的请求框架进行二次封装: @@ -411,7 +406,8 @@ fun testFoo() { 这个范围函数本身似乎不是很有用。但是相比范围,还有一点不错的是,它返回范围内最后一个对象。 -例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog,如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: +例如现在有这么一个场景:用户点击领取新人奖励的按钮,如果用户此时没有登陆则弹出loginDialog, +如果已经登录则弹出领取奖励的getNewAccountDialog。我们可以使用以下代码来处理这个逻辑: ```kotlin run { @@ -419,8 +415,6 @@ run { }.show() ``` - - 2. apply `apply`函数是这样的,调用某对象的`apply`函数,在函数范围内,可以任意调用该对象的任意方法,并返回该对象. @@ -450,8 +444,6 @@ let函数和apply函数很像,唯一不同的是返回值。apply返回的是 public inline fun T.let(block: (T) -> R): R = block(this) ``` - - 如果对象的值不为空,则允许执行这个方法。返回值是函数里面最后一行,或者指定return,与run一样,它同样限制了变量的作用域。 ```kotlin @@ -513,7 +505,6 @@ class Kot { ``` - #### `sNullOrEmpty | isNullOrBlank` ```kotlin @@ -591,18 +582,6 @@ Hello world Hello world ``` -#### also - -also是kotlin 1.1版本中新加入的内容,它像是let和apply函数的加强版: - -```kotlin -public inline fun T.also(block: (T) -> Unit): T { - block(this); return this -} -``` - -与apply一致,它的返回值是该函数的接收者。 - ### 调度方式对扩展函数的影响 Kotlin是一种静态类型语言,我们创建的每个对象不仅具有运行时,还具有编译时类型。在使用扩展函数时,要清楚地了解静态和动态调度之间的区别。 @@ -649,10 +628,10 @@ public static void main(String[] args) { 在这种情况下,即使base本质上是Extended的实例,最终还是会执行Base的方法。 - #### 扩展函数始终静态调度 -可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数,因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 +可能你会好奇,这和扩展有什么关系?我们知道,扩展函数都有一个接收器(receiver),由于接收器实际上只是字节代码中编译方法的参数, +因此你可以重载它。但不能覆盖它。这可能是成员和扩展函数之间最重要的区别:前者是动态调度的,后者总是静态调度的。 为了方便理解,举一个例子: @@ -675,12 +654,10 @@ I'm Extended.foo! 由于只考虑了编译时类型,第一个打印将调用Base.foo(),而第二个打印将调用Extended.foo()。 - - - ## 用扩展函数封装Utils -在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: +在Java中,我们习惯将常用的代码放到对应的工具类中,例如ToastUtils、NetworkUtils、ImageLoaderUtils等。 +以NetworkUtils为例,该类中我们通常会放入Android经常需要使用的网络相关方法。比如,我们现在有一个判断手机网络是否可用的方法: ```java public class NetworkUtils { @@ -703,13 +680,16 @@ public class NetworkUtils { boolean isConnected = NetworkUtils.isMobileConnected(context); ``` -虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: +虽然用起来比没有封装之前优雅了很多,但是每次都要传入context,造成的烦琐我们先不计较,重要是可能会让调用者忽视context +和mobileNetwork间的强关系。作为代码的使用者,我们更希望在调用时省略NetworkUtils类名,并且让isMobileConnected可以 +看起来像context的一个属性或方法。我们期望的是下面这样的使用方式: ```java boolean isConnected = context.isMobileConnected(); ``` -由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。但是在Kotlin中,我们通过扩展函数就能简单的实现: +由于Context是Android SDK自带的类,我们无法对其进行修改。在Java中目前只能通过继承Context新增静态成员方法来实现。 +但是在Kotlin中,我们通过扩展函数就能简单的实现: ```kotlin fun Context.isMobileConnected(): Boolean { @@ -727,23 +707,25 @@ fun Context.isMobileConnected(): Boolean { val isConnected = context.isMobileConnected(); ``` -值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext,防止出现声明周期不一致导致的内存泄露或者其他问题。 +值得一提的是,在Android中对Context的生命周期需要进行很好的把控。这里我们应该使用ApplicationContext, +防止出现生命周期不一致导致的内存泄露或者其他问题。 ## 运算符重载 -  Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 +Kotlin允许我们对所有的运算符(+ - * / % ++ --),以及其他关键字进行重载,从而拓展这些运算符与关键字的用法。 语法:运算符重载使用的是operator关键字,在指定函数的前面加上operator关键字,就可以实现运算符重载了。 -指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus()  - 对应minus() +指定函数,不同的运算符对应的重载函数是不同的,比如:+ 对应plus() 、 - 对应minus() 可以在类中,对同一运算符进行多次重载,来满足不同的需求。 -运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。或者理解成是对已有的运算符赋予他们新的含义。重载的修饰符是operator。 +运算符重载实际上是函数重载,本质上是对运算符函数的调用,从运算符到对应函数的映射过程由编译器完成。 +或者理解成是对已有的运算符赋予他们新的含义。 举例: -比如我们的+号,它的含义是两个数值相加: 1+1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 +比如我们的+号,它的含义是两个数值相加: 1 + 1 = 2。 +号对应的函数名是plus。我们可以对+号这个函数进行重载,让它实现减法的效果。 下面我们实现一个Person数据类,然后重载Int的 + 号运算符,让一个Int对象可以直接和Person对象相加。返回的结果是这个Int对象减去Person对象的age的值。 @@ -846,6 +828,243 @@ fun main() { +#### 函数引用 + +当我们有一个命名函数声明如下: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +``` + +我们可以很容易地直接调用它`(isOdd(5))`,但是我们也可以把它作为一个值传递。例如传给另一个函数。 +为此,我们使用`::`操作符: + +```kotlin +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 输出 [1, 3] +``` + +这里`::isOdd`是函数类型`(Int) -> Boolean`的一个值。 + +当上下文中已知函数期望的类型时`::`可以用于重载函数。 + +例如: + +```kotlin +fun isOdd(x: Int) = x % 2 != 0 +fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove" + +val numbers = listOf(1, 2, 3) +println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int) +``` + +或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文: + +```kotlin +val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String) +``` + +如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 +例如`String::toCharArray`为类型`String`提供了一个扩展函数:`String.() -> CharArray`。 + + +#### 属性引用 + +要把属性作为`Kotlin`中的一等对象来访问,我们也可以使用`::`运算符: + +```kotlin +var x = 1 + +fun main(args: Array) { + println(::x.get()) // 输出 "1" + ::x.set(2) + println(x) // 输出 "2" +} +``` + +表达式`::x`求值为`KProperty`类型的属性对象,它允许我们使用 +`get()`读取它的值,或者使用`name`属性来获取属性名。更多信息请参见 +关于`KProperty`类的文档。 + +对于可变属性,例如`var y = 1`,`::y`返回`KMutableProperty`类型的一个值, +该类型有一个`set()`方法。 + +属性引用可以用在不需要参数的函数处: + +```kotlin +val strs = listOf("a", "bc", "def") +println(strs.map(String::length)) // 输出 [1, 2, 3] +``` + +要访问属于类的成员的属性,我们这样限定它: + +```kotlin +class A(val p: Int) + +fun main(args: Array) { + val prop = A::p + println(prop.get(A(1))) // 输出 "1" +} +``` + + +## Kotlin类型别名 + +类型别名为现有类型提供替代名称。 +如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。 + +它有助于缩短较长的泛型类型。 +例如,通常缩减集合类型是很有吸引力的: + +```kotlin +typealias NodeSet = Set + +typealias FileTable = MutableMap> +``` + +你可以为函数类型提供另外的别名: + +```kotlin +typealias MyHandler = (Int, String, Any) -> Unit + +typealias Predicate = (T) -> Boolean +``` + +你可以为内部类和嵌套类创建新名称: + +```kotlin +class A { + inner class Inner +} +class B { + inner class Inner +} + +typealias AInner = A.Inner +typealias BInner = B.Inner +``` + + +类型别名不会引入新类型。 +它们等效于相应的底层类型。 +当你在代码中添加`typealias Predicate`并使用`Predicate`时,`Kotlin`编译器总是把它扩展为`(Int) -> Boolean`。 +因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然: + +```kotlin +typealias Predicate = (T) -> Boolean + +fun foo(p: Predicate) = p(42) + +fun main(args: Array) { + val f: (Int) -> Boolean = { it > 0 } + println(foo(f)) // 输出 "true" + + val p: Predicate = { it > 0 } + println(listOf(1, -2).filter(p)) // 输出 "[1]" +} +``` + + +## 文档 + +用来编写`Kotlin`代码文档的语言(相当于`Java`的`JavaDoc`)称为`KDoc`。本质上`KDoc`是将`JavaDoc`的块标签`(block tags)`语法( +扩展为支持`Kotlin`的特定构造)和`Markdown`的内联标记`(inline markup)`结合在一起。 + + +#### 生成文档 + +`Kotlin`的文档生成工具称为[Dokka](https://github.com/Kotlin/dokka)。 + +`Dokka`有`Gradle`、`Maven`和`Ant`的插件,因此你可以将文档生成集成到你的构建过程中。 + + +像`JavaDoc`一样,`KDoc`注释也以`/**`开头、以`*/`结尾。注释的每一行可以以 +星号开头,该星号不会当作注释内容的一部分。 + +按惯例来说,文档文本的第一段(到第一行空白行结束)是该元素的 +总体描述,接下来的注释是详细描述。 + +每个块标签都以一个新行开始且以`@`字符开头。 + +以下是使用`KDoc`编写类文档的一个示例: + +```kotlin +/** + * 一组*成员*。 + * + * 这个类没有有用的逻辑; 它只是一个文档示例。 + * + * @param T 这个组中的成员的类型。 + * @property name 这个组的名称。 + * @constructor 创建一个空组。 + */ +class Group(val name: String) { + /** + * 将 [member] 添加到这个组。 + * @return 这个组的新大小。 + */ + fun add(member: T): Int { …… } +} +``` + +`KDoc`目前支持以下块标签`(block tags)`: + +- `@param` <名称> + + 用于函数的值参数或者类、属性或函数的类型参数。 + 为了更好地将参数名称与描述分开,如果你愿意,可以将参数的名称括在 + 方括号中。因此,以下两种语法是等效的: + + ```kotlin + @param name 描述。 + @param[name] 描述。 + ``` + +- `@return` + + 用于函数的返回值。 + +- `@constructor` + + 用于类的主构造函数。 + +- `@receiver` + + 用于扩展函数的接收者。 + +- `@property` <名称> + + 用于类中具有指定名称的属性。这个标签可用于在 + 主构造函数中声明的属性,当然直接在属性定义的前面放置`doc`注释会很别扭。 + +- `@throws` <类>,`@exception` <类> + + 用于方法可能抛出的异常。因为`Kotlin`没有受检异常,所以也没有期望所有可能的异常都写文档,但是当它会为类的用户提供有用的信息时,仍然可以使用这个标签。 + +- `@sample` <标识符> + + 将具有指定限定的名称的函数的主体嵌入到当前元素的文档中,以显示如何使用该元素的示例。 + +- `@see` <标识符> + + 将到指定类或方法的链接添加到文档的另请参见块。 + +- @author + + 指定要编写文档的元素的作者。 + +- `@since` + + 指定要编写文档的元素引入时的软件版本。 + +- `@suppress` + + 从生成的文档中排除元素。可用于不是模块的官方`API`的一部分但还是必须在对外可见的元素。 + +`KDoc`不支持`@deprecated`这个标签。作为替代,请使用`@Deprecated`注解。 + + + [上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index d732bbaf..4826886f 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -3,20 +3,330 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它更容易提升系统的高并发处理能力。 -一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 -并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 -协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 -订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 +### 起源 +协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够, +一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 +线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程 +是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 -### 起源 -协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够,一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 -线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 +## 使用 + +如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: + +```groovy +dependencies { + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' +} +``` + +## 示例 + +```kotlin +import kotlinx.coroutines.* + +fun main() { + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) // 非阻塞的等待 1 秒钟(默认时间单位是毫秒) + println("World!") // 在延迟后打印输出 + } + println("Hello,") // 协程已在等待时主线程还在继续 + Thread.sleep(2000L) // 阻塞主线程 2 秒钟来保证 JVM 存活 +} +// 运行结果 +Hello, +World! +``` + +本质上,协程是轻量级的线程。它们在某些CoroutineScope上下文中与launch协程构建器一起启动。 +这里我们在ClobalScope中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。 + +```kotlin +import kotlinx.coroutines.* + +fun main() { + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) + println("World!") + } + println("Hello,") // 主线程中的代码会立即执行 + runBlocking { // 但是这个表达式阻塞了主线程 + delay(2000L) // ……我们延迟 2 秒来保证 JVM 的存活 + } +} +``` + +调用了runBlocking的主线程会一直阻塞直到runBlocking内部的协程执行完毕。 + +这个示例可以使用更合乎惯用法的方式重写,使用runBlocking来包装main函数的执行: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // 开始执行主协程 + GlobalScope.launch { // 在后台启动一个新的协程并继续 + delay(1000L) + println("World!") + } + println("Hello,") // 主协程在这里会立即执行 + delay(2000L) // 延迟 2 秒来保证 JVM 存活 +} +``` + +这里的runBlocking { …… }作为用来启动顶层主协程的适配器。 我们显式指定了其返回类型Unit, +因为在Kotlin中main函数必须返回Unit类型。runBlocking为最高级的协程,也就是主协程,launch创建的协程能够在 +runBlocking中运行(反过来是不行的)。所以上面的代码可以看做是在一个线程中创建了一个主协程,然后在主协程中创建了一个输出为“World!”的子协程。 + +### 等待一个作业 + +上面我们使用delay(2000L)来让主线程延迟2秒,保证JVM的存活,但是这样并不是一个好的实现方案,因为在很多情况下我们并不知道耗时的任务要执行多久。 + +```kotlin +val job = GlobalScope.launch { // 启动一个新协程并保持对这个作业的引用 + delay(1000L) + println("World!") +} +println("Hello,") +job.join() // 等待直到子协程执行结束 +``` + +加了job.join()后,程序就会一直等待,直到我们启动的协程结束。注意,这里的等待是非堵塞式的等待,不会将当前线程挂起。 + +### 结构化的并发 + +协程的实际使用还有一些需要改进的地方。 当我们使用 `GlobalScope.launch` 时,我们会创建一个顶层协程。虽然它很轻量, +但它运行时仍会消耗一些内存资源。如果我们忘记保持对新启动的协程的引用,它还会继续运行。如果协程中的代码挂起了会怎么样 +(例如,我们错误地延迟了太长时间),如果我们启动了太多的协程并导致内存不足会怎么样? +必须手动保持对所有已启动协程的引用并 [join](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html) 之很容易出错。 + +有一个更好的解决办法。我们可以在代码中使用结构化并发。 我们可以在执行操作所在的指定作用域内启动协程, 而不是像通常使用线程 +(线程总是全局的)那样在 [GlobalScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) 中启动。 + +在我们的示例中,我们使用 [runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) +协程构建器将 `main` 函数转换为协程。 包括 `runBlocking` 在内的每个协程构建器都将 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) +的实例添加到其代码块所在的作用域中。 我们可以在这个作用域中启动协程而无需显式 `join` 之,因为外部协程(示例中的 `runBlocking`) +直到在其作用域中启动的所有协程都执行完毕后才会结束。因此,可以将我们的示例简化为: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // this: CoroutineScope + launch { // 在 runBlocking 作用域中启动一个新协程 + delay(1000L) + println("World!") + } + println("Hello,") +} +``` + + +### 作用域构建器 + +除了由不同的构建器提供协程作用域之外,还可以使用 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +构建器声明自己的作用域。它会创建一个协程作用域并且在所有已启动子协程执行完毕之前不会结束。 + +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 与 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +可能看起来很类似,因为它们都会等待其协程体以及所有子协程结束。 +主要区别在于,[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) +方法会*阻塞*当前线程来等待, 而 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) +只是挂起,会释放底层线程用于其他用途。 由于存在这点差异, +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 是常规函数, +而 [coroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/coroutine-scope.html) 是挂起函数。 + +可以通过以下示例来演示: + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { // this: CoroutineScope + launch { + delay(200L) + println("Task from runBlocking") + } + + coroutineScope { // 创建一个协程作用域 + launch { + delay(500L) + println("Task from nested launch") + } + + delay(100L) + println("Task from coroutine scope") // 这一行会在内嵌 launch 之前输出 + } + + println("Coroutine scope is over") // 这一行在内嵌 launch 执行完毕后才输出 +} +// 执行结果 +Task from coroutine scope +Task from runBlocking +Task from nested launch +Coroutine scope is over +``` + +### 提取函数重构 + +我们来将 `launch { …… }` 内部的代码块提取到独立的函数中。当你对这段代码执行“提取函数”重构时,你会得到一个带有 `suspend` 修饰符的新函数。 +这是你的第一个*挂起函数*。在协程内部可以像普通函数一样使用挂起函数, 不过其额外特性是,同样可以使用其他挂起函数(如本例中的 `delay`)来*挂起*协程的执行。 + +```kotlin +import kotlinx.coroutines.* + +fun main() = runBlocking { + launch { doWorld() } + println("Hello,") +} + +// 这是你的第一个挂起函数 +suspend fun doWorld() { + delay(1000L) + println("World!") +} + +// 执行结果 +Hello, +World! +``` + +### 取消协程的执行 + +在一个长时间运行的应用程序中,你也许需要对你的后台协程进行细粒度的控制。 比如说,一个用户也许关闭了一个启动了协程的界面, +那么现在协程的执行结果已经不再被需要了,这时,它应该是可以被取消的。 +该 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html) +函数返回了一个可以被用来取消运行中的协程的 [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html): + +```kotlin +val job = launch { + repeat(1000) { i -> + println("job: I'm sleeping $i ...") + delay(500L) + } +} +delay(1300L) // 延迟一段时间 +println("main: I'm tired of waiting!") +job.cancel() // 取消该作业 +job.join() // 等待作业执行结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +main: Now I can quit. +``` + +一旦main函数调用了 `job.cancel`,我们在其它的协程中就看不到任何输出,因为它被取消了。 这里也有一个可以使 +[Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) +挂起的函数 [cancelAndJoin](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/cancel-and-join.html) +它合并了对 [cancel](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/cancel.html) +以及 [join](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/join.html) 的调用。 + + + +### 取消是协作的 + +协程的取消是*协作*的。一段协程代码必须协作才能被取消。 所有 `kotlinx.coroutines` 中的挂起函数都是 *可被取消的* 。 +它们检查协程的取消,并在取消时抛出 [CancellationException](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html)。 +然而,如果协程正在执行计算任务,并且没有检查取消的话,那么它是不能被取消的,就如如下示例代码所示: + +```kotlin +val startTime = System.currentTimeMillis() +val job = launch(Dispatchers.Default) { + var nextPrintTime = startTime + var i = 0 + while (i < 5) { // 一个执行计算的循环,只是为了占用 CPU + // 每秒打印消息两次 + if (System.currentTimeMillis() >= nextPrintTime) { + println("job: I'm sleeping ${i++} ...") + nextPrintTime += 500L + } + } +} +delay(1300L) // 等待一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消一个作业并且等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +job: I'm sleeping 3 ... +job: I'm sleeping 4 ... +main: Now I can quit. +``` + +运行示例代码,并且我们可以看到它连续打印出了“I'm sleeping”,甚至在调用取消后, 作业仍然执行了五次循环迭代并运行到了它结束为止。 + +### 使计算代码可取消 + +我们有两种方法来使执行计算的代码可以被取消。第一种方法是定期调用挂起函数来检查取消。 +对于这种目的 [yield](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html) 是一个好的选择。 +另一种方法是显式的检查取消状态。让我们试试第二种方法。 + +将前一个示例中的 `while (i < 5)` 替换为 `while (isActive)` 并重新运行它。 + +```kotlin +val startTime = System.currentTimeMillis() +val job = launch(Dispatchers.Default) { + var nextPrintTime = startTime + var i = 0 + while (isActive) { // 可以被取消的计算循环 + // 每秒打印消息两次 + if (System.currentTimeMillis() >= nextPrintTime) { + println("job: I'm sleeping ${i++} ...") + nextPrintTime += 500L + } + } +} +delay(1300L) // 等待一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消该作业并等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +main: Now I can quit. +``` + +你可以看到,现在循环被取消了。[isActive](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/is-active.html) +是一个可以被使用在 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 中的扩展属性。 + + +### 在 `finally` 中释放资源 + +我们通常使用如下的方法处理在被取消时抛出 [CancellationException](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/index.html) +的可被取消的挂起函数。比如说,`try {……} finally {……}` 表达式以及 Kotlin 的 `use` 函数一般在协程被取消的时候执行它们的终结动作: + +```kotlin +val job = launch { + try { + repeat(1000) { i -> + println("job: I'm sleeping $i ...") + delay(500L) + } + } finally { + println("job: I'm running finally") + } +} +delay(1300L) // 延迟一段时间 +println("main: I'm tired of waiting!") +job.cancelAndJoin() // 取消该作业并且等待它结束 +println("main: Now I can quit.") +// 执行结果 +job: I'm sleeping 0 ... +job: I'm sleeping 1 ... +job: I'm sleeping 2 ... +main: I'm tired of waiting! +job: I'm running finally +main: Now I can quit. +``` ### 阻塞 vs 挂起 @@ -29,7 +339,7 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 -#### 挂起函数 +#### 挂起函数 当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: @@ -38,7 +348,6 @@ suspend fun doSomething(foo: Foo): Bar { …… } ``` - 这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, 必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 @@ -68,7 +377,6 @@ async { ``` -更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: ```kotlin @@ -76,7 +384,6 @@ fun main(args: Array) { doSomething() // 错误:挂起函数从非协程上下文调用 } ``` - 还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: ```kotlin interface Base { @@ -88,6 +395,379 @@ class Derived: Base { } ``` +### 默认顺序调用 + +假设我们在不同的地方定义了两个进行某种调用远程服务或者进行计算的挂起函数。我们只假设它们都是有用的,但是实际上它们在这个示例中只是为了该目的而延迟了一秒钟: + +```kotlin +suspend fun doSomethingUsefulOne(): Int { + delay(1000L) // 假设我们在这里做了一些有用的事 + return 13 +} + +suspend fun doSomethingUsefulTwo(): Int { + delay(1000L) // 假设我们在这里也做了一些有用的事 + return 29 +} +``` + +如果需要按 *顺序* 调用它们,我们接下来会做什么——首先调用 `doSomethingUsefulOne` *接下来* 调用 `doSomethingUsefulTwo`,并且计算它们结果的和吗? +实际上,如果我们要根据第一个函数的结果来决定是否我们需要调用第二个函数或者决定如何调用它时,我们就会这样做。 + +我们使用普通的顺序来进行调用,因为这些代码是运行在协程中的,只要像常规的代码一样 *顺序* 都是默认的。 +下面的示例展示了测量执行两个挂起函数所需要的总时间: + +```kotlin +val time = measureTimeMillis { + val one = doSomethingUsefulOne() + val two = doSomethingUsefulTwo() + println("The answer is ${one + two}") +} +println("Completed in $time ms") +``` + +它的打印输出如下: + +``` +The answer is 42 +Completed in 2017 ms +``` + +### 使用 async 并发 + +如果 `doSomethingUsefulOne` 与 `doSomethingUsefulTwo` 之间没有依赖,并且我们想更快的得到结果,让它们进行 *并发* 吗? +这就是 [async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) 可以帮助我们的地方。 + +在概念上,[async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) +就类似于 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html)。 +它启动了一个单独的协程,这是一个轻量级的线程并与其它所有的协程一起并发的工作。不同之处在于 `launch` 返回一个 +[Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html) 并且不附带任何结果值, +而 `async` 返回一个 [Deferred](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/index.html) —— +一个轻量级的非阻塞可取消的future,这代表了一个将会在稍后提供结果的 promise。你可以使用 `.await()` 在一个延期的值上得到它的最终结果, +但是 `Deferred` 也是一个 `Job`,所以如果需要的话,你可以取消它。 + +```kotlin +val time = measureTimeMillis { + val one = async { doSomethingUsefulOne() } + val two = async { doSomethingUsefulTwo() } + println("The answer is ${one.await() + two.await()}") +} +println("Completed in $time ms") +``` +它的打印输出如下: + +``` +The answer is 42 +Completed in 1017 ms +``` + +这里快了两倍,因为两个协程并发执行。请注意,使用协程进行并发总是显式的。 + +### 惰性启动的 async + +可选的,[async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) +可以通过将 `start` 参数设置为 [CoroutineStart.LAZY](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-start/-l-a-z-y.html) 而变为惰性的。 +在这个模式下,只有结果通过 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) 获取的时候协程才会启动, +或者在 `Job` 的 [start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html) 函数调用的时候。运行下面的示例: + +```kotlin +val time = measureTimeMillis { + val one = async(start = CoroutineStart.LAZY) { doSomethingUsefulOne() } + val two = async(start = CoroutineStart.LAZY) { doSomethingUsefulTwo() } + // 执行一些计算 + one.start() // 启动第一个 + two.start() // 启动第二个 + println("The answer is ${one.await() + two.await()}") +} +println("Completed in $time ms") +``` +它的打印输出如下: + +``` +The answer is 42 +Completed in 1017 ms +``` + +因此,在先前的例子中这里定义的两个协程没有执行,但是控制权在于程序员准确的在开始执行时调用 +[start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html)。 +我们首先 调用 `one`,然后调用 `two`,接下来等待这个协程执行完毕。 + +注意,如果我们只是在 `println` 中调用 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html), +而没有在单独的协程中调用 [start](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/start.html),这将会导致顺序行为, +直到 [await](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-deferred/await.html) 启动该协程 执行并等待至它结束, +这并不是惰性的预期用例。 在计算一个值涉及挂起函数时,这个 `async(start = CoroutineStart.LAZY)` 的用例用于替代标准库中的 `lazy` 函数。 + + + +## 协程上下文与调度器 + +协程总是运行在一些以 [CoroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/) +类型为代表的上下文中,它们被定义在了 Kotlin 的标准库里。 + +协程上下文是各种不同元素的集合。其中主元素是协程中的 [Job](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-job/index.html), +我们在前面的文档中见过它以及它的调度器,而本文将对它进行介绍。 + +### 调度器与线程 + +协程上下文包含一个 *协程调度器* (参见 [CoroutineDispatcher](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/index.html)) +它确定了相关的协程在哪个线程或哪些线程上执行。协程调度器可以将协程限制在一个特定的线程执行,或将它分派到一个线程池,亦或是让它不受限地运行。 + +所有的协程构建器诸如 [launch](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html) +和 [async](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/async.html) 接收一个可选的 +[CoroutineContext](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.coroutines/-coroutine-context/) 参数,它可以被用来显式的为一个新协程或其它上下文元素指定一个调度器。 + +尝试下面的示例: + +```kotlin +launch { // 运行在父协程的上下文中,即 runBlocking 主协程 + println("main runBlocking : I'm working in thread ${Thread.currentThread().name}") +} +launch(Dispatchers.Unconfined) { // 不受限的——将工作在主线程中 + println("Unconfined : I'm working in thread ${Thread.currentThread().name}") +} +launch(Dispatchers.Default) { // 将会获取默认调度器 + println("Default : I'm working in thread ${Thread.currentThread().name}") +} +launch(newSingleThreadContext("MyOwnThread")) { // 将使它获得一个新的线程 + println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}") +} +``` + +``` +Unconfined : I'm working in thread main +Default : I'm working in thread DefaultDispatcher-worker-1 +newSingleThreadContext: I'm working in thread MyOwnThread +main runBlocking : I'm working in thread main +``` + +当调用 `launch { …… }` 时不传参数,它从启动了它的 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) +中承袭了上下文(以及调度器)。在这个案例中,它从 `main` 线程中的 `runBlocking` 主协程承袭了上下文。 + +[Dispatchers.Unconfined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html) +是一个特殊的调度器且似乎也运行在 `main` 线程中,但实际上, 它是一种不同的机制,这会在后文中讲到。 + +当协程在 [GlobalScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html) 中启动时, +使用的是由 [Dispatchers.Default](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-default.html) 代表的默认调度器。 +默认调度器使用共享的后台线程池。 所以 `launch(Dispatchers.Default) { …… }` 与 `GlobalScope.launch { …… }` 使用相同的调度器。 + +[newSingleThreadContext](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/new-single-thread-context.html) +为协程的运行启动了一个线程。 一个专用的线程是一种非常昂贵的资源。 在真实的应用程序中两者都必须被释放,当不再需要的时候, +使用 [close](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-executor-coroutine-dispatcher/close.html) 函数, +或存储在一个顶层变量中使它在整个应用程序中被重用。 + + + +### 非受限调度器 vs 受限调度器 + +[Dispatchers.Unconfined](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-unconfined.html) +协程调度器在调用它的线程启动了一个协程,但它仅仅只是运行到第一个挂起点。挂起后,它恢复线程中的协程,而这完全由被调用的挂起函数来决定。 +非受限的调度器非常适用于执行不消耗 CPU 时间的任务,以及不更新局限于特定线程的任何共享数据(如UI)的协程。 + +另一方面,该调度器默认继承了外部的 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html)。 +[runBlocking](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程的默认调度器, +特别是, 当它被限制在了调用者线程时,继承自它将会有效地限制协程在该线程运行并且具有可预测的 FIFO 调度。 + +``` +launch(Dispatchers.Unconfined) { // 非受限的——将和主线程一起工作 + println("Unconfined : I'm working in thread ${Thread.currentThread().name}") + delay(500) + println("Unconfined : After delay in thread ${Thread.currentThread().name}") +} +launch { // 父协程的上下文,主 runBlocking 协程 + println("main runBlocking: I'm working in thread ${Thread.currentThread().name}") + delay(1000) + println("main runBlocking: After delay in thread ${Thread.currentThread().name}") +} +``` + +执行后的输出: + +``` +Unconfined : I'm working in thread main +main runBlocking: I'm working in thread main +Unconfined : After delay in thread kotlinx.coroutines.DefaultExecutor +main runBlocking: After delay in thread main +``` + +所以,该协程的上下文继承自 `runBlocking {...}` 协程并在 `main` 线程中运行, +当 [delay](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/delay.html) 函数调用的时候, +非受限的那个协程在默认的执行者线程中恢复执行。 + +> 非受限的调度器是一种高级机制,可以在某些极端情况下提供帮助而不需要调度协程以便稍后执行或产生不希望的副作用, 因为某些操作必须立即在协程中执行。 非受限调度器不应该在通常的代码中使用。 + + + +### 命名协程以用于调试 + +当协程经常打印日志并且你只需要关联来自同一个协程的日志记录时, +则自动分配的 id 是非常好的。然而,当一个协程与特定请求的处理相关联时或做一些特定的后台任务, +最好将其明确命名以用于调试目的。 [CoroutineName](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-name/index.html) +上下文元素与线程名具有相同的目的。当[调试模式](http://www.kotlincn.net/docs/reference/coroutines/coroutine-context-and-dispatchers.html#调试协程与线程)开启时, +它被包含在正在执行此协程的线程名中。 + +下面的例子演示了这一概念: + +```kotlin +log("Started main coroutine") +// 运行两个后台值计算 +val v1 = async(CoroutineName("v1coroutine")) { + delay(500) + log("Computing v1") + 252 +} +val v2 = async(CoroutineName("v2coroutine")) { + delay(1000) + log("Computing v2") + 6 +} +log("The answer for v1 / v2 = ${v1.await() / v2.await()}") +``` + +程序执行使用了 `-Dkotlinx.coroutines.debug` JVM 参数,输出如下所示: + +``` +[main @main#1] Started main coroutine +[main @v1coroutine#2] Computing v1 +[main @v2coroutine#3] Computing v2 +[main @main#1] The answer for v1 / v2 = 42 +``` + + +### 组合上下文中的元素 + +有时我们需要在协程上下文中定义多个元素。我们可以使用 `+` 操作符来实现。 比如说,我们可以显式指定一个调度器来启动协程 +并且同时显式指定一个命名: + +```kotlin +launch(Dispatchers.Default + CoroutineName("test")) { + println("I'm working in thread ${Thread.currentThread().name}") +} +``` + +这段代码使用了 `-Dkotlinx.coroutines.debug` JVM 参数,输出如下所示: + +``` +I'm working in thread DefaultDispatcher-worker-1 @test#2 +``` + + + +### 协程作用域 + +让我们将关于上下文,子协程以及作业的知识综合在一起。假设我们的应用程序拥有一个具有生命周期的对象,但这个对象并不是一个协程。 +举例来说,我们编写了一个 Android 应用程序并在 Android 的 activity 上下文中启动了一组协程来使用异步操作拉取并更新数据以及执行动画等等。 +所有这些协程必须在这个 activity 销毁的时候取消以避免内存泄漏。当然,我们也可以手动操作上下文与作业,以结合 activity 的生命周期与它的协程, +但是 `kotlinx.coroutines` 提供了一个封装:[CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 的抽象。 +你应该已经熟悉了协程作用域,因为所有的协程构建器都声明为在它之上的扩展。 + +我们通过创建一个 [CoroutineScope](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/index.html) 实例来管理协程的生命周期, +并使它与 activity 的生命周期相关联。`CoroutineScope` 可以通过 [CoroutineScope()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html) +创建或者通过[MainScope()](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html) 工厂函数。 +前者创建了一个通用作用域,而后者为使用 [Dispatchers.Main](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-dispatchers/-main.html) +作为默认调度器的 UI 应用程序 创建作用域: + +``` +class Activity { + private val mainScope = MainScope() + + fun destroy() { + mainScope.cancel() + } + // 继续运行…… +``` + +现在,我们可以使用定义的 `scope` 在这个 `Activity` 的作用域内启动协程。 +对于该示例,我们启动了十个协程,它们会延迟不同的时间: + +``` +// 在 Activity 类中 + fun doSomething() { + // 在示例中启动了 10 个协程,且每个都工作了不同的时长 + repeat(10) { i -> + mainScope.launch { + delay((i + 1) * 200L) // 延迟 200 毫秒、400 毫秒、600 毫秒等等不同的时间 + println("Coroutine $i is done") + } + } + } +} // Activity 类结束 +``` + +在 main 函数中我们创建 activity,调用测试函数 `doSomething`,并且在 500 毫秒后销毁这个 activity。 +这取消了从 `doSomething` 启动的所有协程。我们可以观察到这些是由于在销毁之后, 即使我们再等一会儿,activity 也不再打印消息。 + +``` +val activity = Activity() +activity.doSomething() // 运行测试函数 +println("Launched coroutines") +delay(500L) // 延迟半秒钟 +println("Destroying activity!") +activity.destroy() // 取消所有的协程 +delay(1000) // 为了在视觉上确认它们没有工作 +``` + +这个示例的输出如下所示: + +``` +Launched coroutines +Coroutine 0 is done +Coroutine 1 is done +Destroying activity! +``` + +你可以看到,只有前两个协程打印了消息,而另一个协程在 `Activity.destroy()` 中单次调用了 `job.cancel()`。 + + +## 通道 + +延期的值提供了一种便捷的方法使单个值在多个协程之间进行相互传输。 通道提供了一种在流中传输值的方法。 + +### 通道基础 + +一个 [Channel](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-channel/index.html) +是一个和 `BlockingQueue` 非常相似的概念。其中一个不同是它代替了阻塞的 `put` 操作并提供了挂起的 +[send](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/send.html), +还替代了阻塞的 `take` 操作并提供了挂起的 [receive](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-receive-channel/receive.html)。 + +``` +val channel = Channel() +launch { + // 这里可能是消耗大量 CPU 运算的异步逻辑,我们将仅仅做 5 次整数的平方并发送 + for (x in 1..5) channel.send(x * x) +} +// 这里我们打印了 5 次被接收的整数: +repeat(5) { println(channel.receive()) } +println("Done!") +``` +这段代码的输出如下: + +``` +1 +4 +9 +16 +25 +Done! +``` + +### 关闭与迭代通道 + +和队列不同,一个通道可以通过被关闭来表明没有更多的元素将会进入通道。 在接收者中可以定期的使用 `for` 循环来从通道中接收元素。 + +从概念上来说,一个 [close](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.channels/-send-channel/close.html) +操作就像向通道发送了一个特殊的关闭指令。 这个迭代停止就说明关闭指令已经被接收了。所以这里保证所有先前发送出去的元素都在通道关闭前被接收到。 + +``` +val channel = Channel() +launch { + for (x in 1..5) channel.send(x * x) + channel.close() // 我们结束发送 +} +// 这里我们使用 `for` 循环来打印所有被接收到的元素(直到通道被关闭) +for (y in channel) println(y) +println("Done!") +``` + diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" b/KotlinCourse/9.Kotlin_androidktx.md similarity index 56% rename from "KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" rename to KotlinCourse/9.Kotlin_androidktx.md index 1a484128..aaef520a 100644 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\344\271\235).md" +++ b/KotlinCourse/9.Kotlin_androidktx.md @@ -1,139 +1,6 @@ -Kotlin学习教程(九) +9.Kotlin_androidktx === - -`Kotlin`团队为`Android`开发提供了一套超越标准语言功能的工具: - -- `Kotlin Android Extensions`是一个编译器扩展,可以让您摆脱代码中的`findViewById()`调用,并将其替换为合成编译器生成的属性。 -- `Anko`是一个提供围绕`Android API`和`DSL`的一组`Kotlin`友好的包装器,可以用`Kotlin`代码替换`layout .xml`文件。 - - -### `Anko` - -[Anko](https://github.com/Kotlin/anko)是`Kotlin`官方出品用于`Android`开发的库。 - - -> `Anko is a Kotlin library which makes Android application development faster and easier. It makes your code clean and -> easy to read, and lets you forget about rough edges of the Android SDK for Java.` - - -> Anko consists of several parts: -> -> Anko Commons: a lightweight library full of helpers for intents, dialogs, logging and so on; - > Intents; - > Dialogs and toasts; - > Logging; - > Resources and dimensions; -> Anko Layouts: a fast and type-safe way to write dynamic Android layouts; -> Anko SQLite: a query DSL and parser collection for Android SQLite; -> Anko Coroutines: utilities based on the kotlinx.coroutines library. - -`Anko`使用`DSL`提供了很多便捷的功能,可以直接用代码去写布局,不过我还是接受不了,感觉用`xml`写布局把布局和逻辑区分开挺好,这里就不介绍了,想用的可以去看看。 - -这里就用几个简单的`commons`中的例子,平时想要启动另一个`activity`我们经常这样写: -```kotlin -val intent = Intent(this, SomeOtherActivity::class.java) -intent.putExtra("id", 5) -intent.setFlag(Intent.FLAG_ACTIVITY_SINGLE_TOP) -startActivity(intent) -``` -这里要四行代码,太多了,`anko`提供了更简单的方式: -```kotlin -startActivity(intentFor("id" to 5).singleTop()) -``` -如果不设置`flag`的话还可以这样写: -```kotlin -startActivity("id" to 5) -``` - -而且`anko`还提供了一些常用的`intent`的封装功能: - -- 打电话`makeCall(number) without tel` -- 发短信`sendSMS(number, [text]) without sms` -- 浏览网页`browse(url)` -- 分享`share(text, [subject])` -- 发邮件`email(email, [subject], [text])` - -进行`toast`和`snakebar`提示: -```kotlin -toast("Hi there!") -toast(R.string.message) -longToast("Wow, such duration") - -snackbar(view, "Hi there!") -snackbar(view, R.string.message) -longSnackbar(view, "Wow, such duration") -snackbar(view, "Action, reaction", "Click me!") { doStuff() } -``` - -对话框: -```kotlin -alert("Hi, I'm Roy", "Have you tried turning it off and on again?") { - yesButton { toast("Oh…") } - noButton {} -}.show() - -// 列表对话框 -val countries = listOf("Russia", "USA", "Japan", "Australia") -selector("Where are you from?", countries, { dialogInterface, i -> - toast("So you're living in ${countries[i]}, right?") -}) - -// 进度对话框 -val dialog = progressDialog(message = "Please wait a bit…", title = "Fetching data") -``` - -`log`: -```kotlin -class SomeActivity : Activity(), AnkoLogger { - private fun someMethod() { - info("London is the capital of Great Britain") - debug(5) // .toString() method will be executed - warn(null) // "null" will be printed - } -} -``` -或者: -```kotlin -class SomeActivity : Activity() { - private val log = AnkoLogger(this) - private val logWithASpecificTag = AnkoLogger("my_tag") - - private fun someMethod() { - log.warning("Big brother is watching you!") - } -} -``` - -`dimens`: - -可以直接使用`px2dip`和`px2sp`来进行尺寸转换。 - - -异步操作: -```kotlin -doAsync { - // Long background task - uiThread { - result.text = "Done" - } -} -``` - -使用`anko`: -```kotlin -// 创建一个verticallayout并且添加edittext和button,并给button设置点击事件 -verticalLayout { - val name = editText() - button("Say Hello") { - onClick { toast("Hello, ${name.text}!") } - } -} -``` - - -### Kotlin Android Extensions - 相信每一位安卓开发人员对`findViewById()`这个方法再熟悉不过了,毫无疑问,潜在的`bug`和脏乱的代码令后续开发无从下手的。 尽管存在一系列的 开源库能够为这个问题带来解决方案,然而对于运行时依赖的库,需要为每一个`View`注解变量字段。 @@ -227,31 +94,6 @@ public class ForecastRequest(val zipCode: String) { ``` -### 创建Application - -```kotlin -class App : Application() { - companion object { - private var instance: Application? = null - fun instance() = instance!! - } - override fun onCreate() { - super.onCreate() - instance = this - } -} - -``` - - - - -- with() - -```kotlin -inline fun with(t: T, body: T.() -> Unit) { t.body() } -``` - [上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" deleted file mode 100644 index 6ed5d235..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\205\253).md" +++ /dev/null @@ -1,460 +0,0 @@ -Kotlin学习教程(八) -=== - -`Kotlin`协程 ---- - -Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它 - -一些`API`启动长时间运行的操作(例如网络`IO`、文件`IO`、`CPU`或`GPU`密集型任务等),并要求调用者阻塞直到它们完成。协程提供了一种避免阻塞线程 -并用更廉价、更可控的操作替代线程阻塞的方法:协程挂起。 -协程通过将复杂性放入库来简化异步编程。程序的逻辑可以在协程中顺序地表达,而底层库会为我们解决其异步性。该库可以将用户代码的相关部分包装为回调、 -订阅相关事件、在不同线程(甚至不同机器!)上调度执行,而代码则保持如同顺序执行一样简单。 - - -### 阻塞 vs 挂起 - -基本上,协程计算可以被挂起而无需阻塞线程。线程阻塞的代价通常是昂贵的,尤其在高负载时,因为只有相对少量线程实际可用,因此阻塞其中一个会导致一些 -重要的任务被延迟。 - -另一方面,协程挂起几乎是无代价的。不需要上下文切换或者`OS`的任何其他干预。最重要的是,挂起可以在很大程度上由用户库控制: -作为库的作者,我们可以决定挂起时发生什么并根据需求优化/记日志/截获。 - -另一个区别是,协程不能在随机的指令中挂起,而只能在所谓的挂起点挂起,这会调用特别标记的函数。 - -#### 挂起函数 - -当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: - -```kotlin -suspend fun doSomething(foo: Foo): Bar { - …… -} -``` - -这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 -获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, -必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 -(源自`kotlinx.coroutines`库): - -```kotlin -fun async(block: suspend () -> T) -``` - -这里的`async()`是一个普通函数(不是挂起函数),但是它的`block`参数具有一个带`suspend`修饰符的函数类型:`suspend() -> T`。 -所以,当我们将一个`lambda`表达式传给`async()`时,它会是挂起`lambda`表达式,于是我们可以从中调用挂起函数: - -```kotlin -async { - doSomething(foo) - …… -} -``` - -继续该类比,`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: -```kotlin -async { - …… - val result = computation.await() - …… -} - -``` - -更多关于`async/await`函数实际在`kotlinx.coroutines`中如何工作的信息可以在这里找到。 - -请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: -```kotlin -fun main(args: Array) { - doSomething() // 错误:挂起函数从非协程上下文调用 -} -``` - -还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: -```kotlin -interface Base { - suspend fun foo() -} - -class Derived: Base { - override suspend fun foo() { …… } -} -``` - -`Kotlin`解构声明 ---- - - -有时把一个对象解构成很多变量会很方便: - -```kotlin -val (name, age) = person -``` - -这种语法称为解构声明。 一个解构声明可以同时创建多个变量。 - -我们已经声明了两个新变量:`name`和`age`,并且可以独立使用它们: -```kotlin -println(name) -println(age) -``` - -一个解构声明会被编译成以下代码: -```kotlin -val name = person.component1() -val age = person.component2() -``` - -其中的`component1()`和`component2()`函数是在`Kotlin`中广泛使用的约定原则的另一个例子。 - -任何表达式都可以出现在解构声明的右侧,只要可以对它调用所需数量的`component`函数即可。 -当然,可以有`component3()`和`component4()`等等。 - -请注意,`componentN()`函数需要用`operator`关键字标记,以允许在解构声明中使用它们。 - -解构声明也可以用在`for{: .keyword }`循环中: -```kotlin -for ((a, b) in collection) { …… } -``` -变量`a`和`b`的值取自对集合中的元素上调用`component1()`和`component2()`的返回值。 - -例:从函数中返回两个变量 -让我们假设我们需要从一个函数返回两个东西。例如,一个结果对象和一个某种状态。 -在`Kotlin`中一个简洁的实现方式是声明一个数据类并返回其实例: -```kotlin -data class Result(val result: Int, val status: Status) -fun function(……): Result { - // 各种计算 - return Result(result, status) -} -// 现在,使用该函数: -val (result, status) = function(……) -``` -因为数据类自动声明`componentN()`函数,所以这里可以用解构声明。 - -注意:我们也可以使用标准类`Pair`并且让`function()`返回`Pair`, -但是让数据合理命名通常更好。 - -例:解构声明和映射 -可能遍历一个映射`(map)`最好的方式就是这样: -```kotlin -for ((key, value) in map) { - // 使用该 key、value 做些事情 -} -``` -为使其能用,我们应该 -通过提供一个`iterator()`函数将映射表示为一个值的序列, -通过提供函数`component1()`和`component2()`来将每个元素呈现为一对。 -当然事实上,标准库提供了这样的扩展: -```kotlin -operator fun Map.iterator(): Iterator> = entrySet().iterator() -operator fun Map.Entry.component1() = getKey() -operator fun Map.Entry.component2() = getValue() -``` -因此你可以在`for{: .keyword }`-循环中对映射(以及数据类实例的集合等)自由使用解构声明。 - - -`Kotlin`反射 ---- - - -最基本的反射功能是获取`Kotlin`类的运行时引用。要获取对 -静态已知的`Kotlin`类的引用,可以使用类字面值语法: -```kotlin -val c = KClass::class -``` - -该引用是`KClass`类型的值。 - -请注意,`Kotlin`类引用与`Java`类引用不同。要获得`Java`类引用, -请在`KClass`实例上使用`.java`属性,也就是`KClass::class.java` - - -#### 函数引用 - -当我们有一个命名函数声明如下: -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -``` - -我们可以很容易地直接调用它`(isOdd(5))`,但是我们也可以把它作为一个值传递。例如传给另一个函数。 -为此,我们使用`::`操作符: -```kotlin -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 输出 [1, 3] -``` -这里`::isOdd`是函数类型`(Int) -> Boolean`的一个值。 - -当上下文中已知函数期望的类型时`::`可以用于重载函数。 - -例如: -```kotlin -fun isOdd(x: Int) = x % 2 != 0 -fun isOdd(s: String) = s == "brillig" || s == "slithy" || s == "tove" - -val numbers = listOf(1, 2, 3) -println(numbers.filter(::isOdd)) // 引用到 isOdd(x: Int) -``` - -或者,你可以通过将方法引用存储在具有显式指定类型的变量中来提供必要的上下文: -```kotlin -val predicate: (String) -> Boolean = ::isOdd // 引用到 isOdd(x: String) -``` -如果我们需要使用类的成员函数或扩展函数,它需要是限定的。 -例如`String::toCharArray`为类型`String`提供了一个扩展函数:`String.() -> CharArray`。 - - -#### 属性引用 - -要把属性作为`Kotlin`中的一等对象来访问,我们也可以使用`::`运算符: - -```kotlin -var x = 1 - -fun main(args: Array) { - println(::x.get()) // 输出 "1" - ::x.set(2) - println(x) // 输出 "2" -} -``` -表达式`::x`求值为`KProperty`类型的属性对象,它允许我们使用 -`get()`读取它的值,或者使用`name`属性来获取属性名。更多信息请参见 -关于`KProperty`类的文档。 - -对于可变属性,例如`var y = 1`,`::y`返回`KMutableProperty`类型的一个值, -该类型有一个`set()`方法。 - -属性引用可以用在不需要参数的函数处: -```kotlin -val strs = listOf("a", "bc", "def") -println(strs.map(String::length)) // 输出 [1, 2, 3] -``` -要访问属于类的成员的属性,我们这样限定它: -```kotlin -class A(val p: Int) - -fun main(args: Array) { - val prop = A::p - println(prop.get(A(1))) // 输出 "1" -} -``` - - -Kotlin类型别名 ---- - -类型别名为现有类型提供替代名称。 -如果类型名称太长,你可以另外引入较短的名称,并使用新的名称替代原类型名。 - -它有助于缩短较长的泛型类型。 -例如,通常缩减集合类型是很有吸引力的: -```kotlin -typealias NodeSet = Set - -typealias FileTable = MutableMap> -``` - -你可以为函数类型提供另外的别名: -```kotlin -typealias MyHandler = (Int, String, Any) -> Unit - -typealias Predicate = (T) -> Boolean -``` -你可以为内部类和嵌套类创建新名称: - -```kotlin -class A { - inner class Inner -} -class B { - inner class Inner -} - -typealias AInner = A.Inner -typealias BInner = B.Inner -``` - - -类型别名不会引入新类型。 -它们等效于相应的底层类型。 -当你在代码中添加`typealias Predicate`并使用`Predicate`时,`Kotlin`编译器总是把它扩展为`(Int) -> Boolean`。 -因此,当你需要泛型函数类型时,你可以传递该类型的变量,反之亦然: - -```kotlin -typealias Predicate = (T) -> Boolean - -fun foo(p: Predicate) = p(42) - -fun main(args: Array) { - val f: (Int) -> Boolean = { it > 0 } - println(foo(f)) // 输出 "true" - - val p: Predicate = { it > 0 } - println(listOf(1, -2).filter(p)) // 输出 "[1]" -} -``` - - - -文档 ---- - - -用来编写`Kotlin`代码文档的语言(相当于`Java`的`JavaDoc`)称为`KDoc`。本质上`KDoc`是将`JavaDoc`的块标签`(block tags)`语法( -扩展为支持`Kotlin`的特定构造)和`Markdown`的内联标记`(inline markup)`结合在一起。 - - -#### 生成文档 - -`Kotlin`的文档生成工具称为[Dokka](https://github.com/Kotlin/dokka)。 - -`Dokka`有`Gradle`、`Maven`和`Ant`的插件,因此你可以将文档生成集成到你的构建过程中。 - - -像`JavaDoc`一样,`KDoc`注释也以`/**`开头、以`*/`结尾。注释的每一行可以以 -星号开头,该星号不会当作注释内容的一部分。 - -按惯例来说,文档文本的第一段(到第一行空白行结束)是该元素的 -总体描述,接下来的注释是详细描述。 - -每个块标签都以一个新行开始且以`@`字符开头。 - -以下是使用`KDoc`编写类文档的一个示例: -```kotlin -/** - * 一组*成员*。 - * - * 这个类没有有用的逻辑; 它只是一个文档示例。 - * - * @param T 这个组中的成员的类型。 - * @property name 这个组的名称。 - * @constructor 创建一个空组。 - */ -class Group(val name: String) { - /** - * 将 [member] 添加到这个组。 - * @return 这个组的新大小。 - */ - fun add(member: T): Int { …… } -} -``` - -`KDoc`目前支持以下块标签`(block tags)`: - -- `@param` <名称> - - 用于函数的值参数或者类、属性或函数的类型参数。 - 为了更好地将参数名称与描述分开,如果你愿意,可以将参数的名称括在 - 方括号中。因此,以下两种语法是等效的: - ```kotlin - @param name 描述。 - @param[name] 描述。 - ``` - -- `@return` - - 用于函数的返回值。 - -- `@constructor` - - 用于类的主构造函数。 - -- `@receiver` - - 用于扩展函数的接收者。 - -- `@property` <名称> - - 用于类中具有指定名称的属性。这个标签可用于在 - 主构造函数中声明的属性,当然直接在属性定义的前面放置`doc`注释会很别扭。 - -- `@throws` <类>,`@exception` <类> - - 用于方法可能抛出的异常。因为`Kotlin`没有受检异常,所以也没有期望所有可能的异常都写文档,但是当它会为类的用户提供有用的信息时,仍然可以使用这个标签。 - -- `@sample` <标识符> - - 将具有指定限定的名称的函数的主体嵌入到当前元素的文档中,以显示如何使用该元素的示例。 - -- `@see` <标识符> - - 将到指定类或方法的链接添加到文档的另请参见块。 - -- @author - - 指定要编写文档的元素的作者。 - -- `@since` - - 指定要编写文档的元素引入时的软件版本。 - -- `@suppress` - - 从生成的文档中排除元素。可用于不是模块的官方`API`的一部分但还是必须在对外可见的元素。 - -`KDoc`不支持`@deprecated`这个标签。作为替代,请使用`@Deprecated`注解。 - - -#### 内联标记 - -对于内联标记,`KDoc`使用常规`Markdown`语法,扩展了支持用于链接到代码中其他元素的简写语法。 - -链接到元素 -要链接到另一个元素(类、方法、属性或参数),只需将其名称放在方括号中: -``` -为此目的,请使用方法 [foo]。 -``` -如果要为链接指定自定义标签(label),请使用 Markdown 引用样式语法: -``` -为此目的,请使用[这个方法][foo]。 -``` -你还可以在链接中使用限定的名称。请注意,与 JavaDoc 不同,限定的名称总是使用点字符 -来分隔组件,即使在方法名称之前: -``` -使用 [kotlin.reflect.KClass.properties] 来枚举类的属性。 -``` -链接中的名称与正写文档的元素内使用该名称使用相同的规则解析。 -特别是,这意味着如果你已将名称导入当前文件,那么当你在`KDoc`注释中使用它时, -不需要再对其进行完整限定。 - -请注意`KDoc`没有用于解析链接中的重载成员的任何语法。因为`Kotlin`文档生成 -工具将一个函数的所有重载的文档放在同一页面上,标识一个特定的重载函数 -并不是链接生效所必需的。 - - - -### Any - -我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 - -但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 - - - -#### Any:非空类型的跟类型 - -与Object作为Java类层级结构的顶层类似,Any类型是Kotlin中所有非空类型(如String、Int)的超类,如: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) - -与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: - -```kotlin -class Animal(val weight: Double) -``` - -#### Any?:所有类型的根类型 - -如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 - - - - -[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" "b/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" deleted file mode 100644 index 7e4012f9..00000000 --- "a/KotlinCourse/Kotlin\345\255\246\344\271\240\346\225\231\347\250\213(\345\215\201).md" +++ /dev/null @@ -1,13 +0,0 @@ -Kotlin学习教程(十) -=== - -- - - -[上一篇:Kotlin学习教程(九)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" index 4e733af8..7964053a 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/1.\351\237\263\350\247\206\351\242\221\345\237\272\347\241\200\347\237\245\350\257\206.md" @@ -47,8 +47,9 @@ - 比特率(码率) + 码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是`kbps`即千位(bit)每秒,也就是每秒钟传送多少个千位的信息。 通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件,但是文件体积与取样率是成正比的,所以几乎所有的编码格式重视的都是如何用最低的码率达到最少的失真。但是因为编码算法不一样,所以也不能用码率来统一衡量音质或者画质.小写的b表示bit(位),大写的B表示byte(字节),一个字节=8个位,即1B=8b;前面的k表示1024的意思,即1024个位(Kb)或者1024个字节(KB),表示文件的大小单位,一般使用KB。1KB/s=8Kbps。码率(kbps)=文件大小(KB)*8/时间(秒)。 - + - 动态码率(VBR: Variable Bit Rate) 比特率可以随着图像复杂程度的不同而随之变化。图像内容简单的片段采用较小的码率,图像 @@ -61,9 +62,10 @@ - 帧率(Frame Rate) + 帧/秒(`frames per second`)的缩写帧率即每秒显示帧数,帧率表示图形处理器处理场时每秒钟能够更新的次数。高的帧率可以得到更流畅、更逼真的动画。一般来说`30fps`就是可以接受的,但是将性能提升至`60fps`则可以明显提升交互感和逼真感,但是一般来说超过`75fps`一般就不容易察觉到有明显的流畅度提升了。如果帧率超过屏幕刷新率只会浪费图形处理的能力,因为监视器不能以这么快的速度更新,这样超过新率的帧率就浪费掉了。 ![image](https://github.com/CharonChui/Pictures/blob/master/fps.gif?raw=true) - + - 关键帧 相当于二维动画中的原画,指角色或者物体运动或变化中的关键动作所处的那一帧,它包含了图像的所有信息,后来帧仅包含了改变了的信息。如果你没有足够的关键帧,你的影片品质可能比较差,因为所有的帧从别的帧处产生。对于一般的用途,一个比较好的原则是每5秒设一个关键键。但如果时那种实时传输的流文件,那么要考虑传输网络的可靠度,所以要1到2秒增加一个关键帧。 @@ -210,7 +212,7 @@ YUV --- -YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。其中Y表示明亮度也就是灰阶值,它是基础信号。U和V表示的则是色度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV,也叫YCbCr。人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 +YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。其中Y表示明亮度也就是灰阶值,它是基础信号。U表示色度,V表示浓度,UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV,也叫YCbCr。人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" new file mode 100644 index 00000000..16479a5f --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/11.\346\222\255\346\224\276\347\273\204\344\273\266\345\260\201\350\243\205.md" @@ -0,0 +1,45 @@ +11.播放组件封装 +=== + +通常播放器的开发都设计成一定程度的分层,将视频帧的显示、进度条、控制键、音量调节、预览图、字幕、弹幕、频道列表、后续播放推荐等截面功能与音视频播放进行剥离,以使代码模块化,架构清晰。 + +为连接播放器截面和音视频播放,通常需设计一套状态机机制,音视频播放层需要负责包括解码器在内的软硬件初始化,搭建Pipeline以及进行播放控制。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index aebda409..b64ab96e 100644 --- "a/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -101,6 +101,41 @@ IP竞速 DNS解析加快,通常,DNS解析,意味着要把一个域名为xxx.com解析成ip过程,平时请求网页,网络差,就会打开网页半天。 +### 播放的关键指标:QOS +QOS(Quality of Service,服务质量)主要指网络环境下服务满足用户的程度,在视频服务的语境下也可认为是Quality of Streaming,即流媒体服务的质量。通常,QOS可以由一系列指标表达,如传输速度、响应时间、发送顺序、正确率等。就视频服务来说,QOS由多项约定俗成的技术指标构成,包括播放成功率、错误率、Re-buffer(卡顿)次数和时间、起始时间、快进响应时间、视频码率、延迟等。 +通行的QOS指标大致可分为两类: +- 一类用于衡量用户可在多大概率上得到服务,如播放成功率和错误率。 +- 另一类描述了用户所获取到服务的水平,如卡顿次数、时间、起始时间、快进时间、视频码率和延迟。 + +播放成功率描述了用户在尝试播放视频时启动成功的比率,可由所有成功开始播放的次数除以用户尝试的总数,常见于后端视频失效的情形。 +播放错误率意在针对播放过程中至少单个视频或音频帧被播放的情况下发生的错误,可能的原因包括播放器崩溃、硬件关闭、网络断开等,需要用户干预才能恢复播放。 +在视频服务质量日渐提升的今天,播放错误率出现的概率通常在千分之一以下甚至更低,用户最常见且容易不满的当属视频卡顿(也有人称之为缓冲率),即播放器无法即时得到流媒体传输的视频片段而需等待下载的情形。 +卡顿可能短程的发生,也可能持续很长时间,根据一些公司的研究,用户在观看视频点播时遇到一次以上的卡顿,会导致观看时间缩短一半,对直播用户的影响还要更甚于此。卡顿指标即包含单位时间内的卡顿次数也包含卡顿累计时间的维度,优化卡顿时间的最常见的方式是利用CDN和码率自适应算法。 + +视频卡顿的一类特殊情形是起始播放时的卡顿,通常计算从用户点击播放到第一帧呈现在屏幕上为止的时间长度,因为获取最初可用的视频片段需要一定时间,包括后台服务准备资源、下载视频开始的片段、初始化软硬件等。 +与播放过程中的卡顿不同,用户有等待数秒的心理预期,据调研2007年的大部分用户能接受10s以内的播放起始时间,但在2017年,5s的视频起始等待已被认为是非常糟糕的体验,中国许多视频服务商都提出了“秒开”的概念,力图将用户习以为常的起始时间固定在1s以内。另一类情形是快进时间,与起始时间非常类似,意指用户在点击快进后到视频呈现在屏幕之间的时间长度。 +优化起始时间可以通过将起始视频片段预先置于CDN的边缘节点,降低起始码率,增加播放器初始化并行度,预先建立网络连接等方式。此外,播放器还可以通过插入片头动画,持续播放快进前的视频片段直至快进后的视频帧准备好等手段降低用户的主观等待时间。 + +用户观看视频的平均码率也是一项核心指标,用于反馈视频的清晰度程度,针对直播服务,节目延迟时间也是核心指标,没有人原因在观看足球比赛时,隔壁已经为进球欢呼,而自己的电视上球员尚未开始射门,通常的计算标准是节目应播出的时间与实际屏幕上播放时间的间隔。带来延迟的除软件处理速度、网络传输速度外,编码器,源服务器及CDN服务器带来的缓存队列,播放器中解码器和渲染硬件均会引入大小不同的延迟。 + +QOS数据由后台服务整合后将被应用于图表呈现、统计报告、分析优化、监控报警等用途,是产品、开发、运维、数据分析等团队依靠的基础。 + +为更好的分析特定问题,收集关于某一用户播放过程的全部信息并按时序加以呈现,可以有效地帮助理解因果关系,信息将包括用户行为、执行时间、下载计时、码率切换记录、错误类型、CDN节点位置、服务器日志甚至一些计算的中间结果,将可有效地推断例如开始播放较为缓慢或者某次卡顿如何发生的原因。例如: +``` +xx:xx Player Seek Start +xx:xx Player Seek End +xx:xx New State: Buffering +.... +``` + +Conviva在2011年的分享中提到以下观点,缓冲次数始终是最主要的影响用户体验的因素,在观看90分钟长度的OTT直播中,增加1%的缓冲比率将使用户减少3分钟的观看时间,同时认为,在直播过程中,平均码率的影响要比点播中更大。 +在2016年的一篇文章中,研究者利用YouSlow的数据分析得到的看法是,缓冲对退出观看的影响比启动时间要高六倍。 +### QOE +视频公司在编码和传输上进行的优化,其指向的目标无疑是提升用户体验,或严格来讲,是获取更多的收入和利润。衡量编码或传输的优化表现,除了直接使用QOS指标如观看码率、启动时间、缓冲次数等,将其余QOE(Quality of Experience)指标连接则更可以直观地反应出成效。 +不断优化表现直接与收入关联的主要原因是,收入统计通常计出多门且较为滞后,可采用的替代指标包括注册用户数、活跃用户数、视频观察市场、观看次数、播放占比、付费率、留存率、分享率、满意度等。 + + + ### 监控 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" index 707b4758..b2f222a2 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HLS.md" @@ -198,6 +198,14 @@ live m3u8文件列表需要不断更新,更新规则: +## TS相比MP4的劣势 + +TS文件虽然适于优先电视领域的应用,但在带宽使用上与DASH所使用的扩展格式MP4相比有巨大的劣势。首先TS包按188字节分割过于细小,其包头虽然仅占4字节,累积起来仍嫌浪费。其次,重复插入的PSI包对持续的视频播放而言颇显肿余(虽然HLS协议的后期版本通过EXT-X-MAP标签支持独立的仅含PSI包的TS文件,但将引入兼容问题)。更重要的是,由于TS包的字节数限制,当视频或音频包不足字节数时,需要加上许多无用的填充字节。据不同来源统计,三项合计,TS文件平均要浪费4%~13%的带宽,设计较好的封装格式,在寸带宽寸金的互联网世界中,意味着节约千万甚至上亿美元。 + + + + + #### 测试地址: From 63cf2e444cbae7c637aa8fabb4bfd594e477555b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 8 Apr 2021 19:26:22 +0800 Subject: [PATCH 021/183] update README --- ...&\347\261\273&\346\216\245\345\217\243.md" | 5 ++- ...76\350\256\241\346\250\241\345\274\217.md" | 8 ++++ ...05\350\201\224\345\207\275\346\225\260.md" | 4 +- ...0\347\273\204&\351\233\206\345\220\210.md" | 4 +- ...7&\345\205\263\351\224\256\345\255\227.md" | 6 +++ ...2\344\270\276&\345\247\224\346\211\230.md" | 4 +- ...47\346\211\277\351\227\256\351\242\230.md" | 4 +- ...5\345\260\204&\346\211\251\345\261\225.md" | 4 +- .../8.Kotlin_\345\215\217\347\250\213.md" | 2 + KotlinCourse/9.Kotlin_androidktx.md | 12 +----- README.md | 40 +++++++++---------- 11 files changed, 52 insertions(+), 41 deletions(-) diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index 995523d5..526cfff2 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -1087,7 +1087,10 @@ class Person( -[下一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) + + + +- [下一篇:2.Kotlin_高阶函数&Lambda&内联函数](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md) --- diff --git "a/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" index fc6a644e..e7afcfdc 100644 --- "a/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/KotlinCourse/10.Kotlin_\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -470,9 +470,17 @@ The latest stock price has fell to 98 +- [上一篇:9.Kotlin_androidktx](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md) +参考 +=== + +- [Kotlin for Android Developers](https://leanpub.com/kotlin-for-android-developers) +- [Resources to Learn Kotlin](https://developer.android.com/kotlin/resources.html) +- [Kotlin语言中文站](https://www.kotlincn.net/docs/reference/coding-conventions.html) + --- diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 77f1b45e..8be83bb9 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -729,8 +729,8 @@ startActivity() -[上一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +- [上一篇:1.Kotlin_简介&变量&类&接口](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/1.Kotlin_%E7%AE%80%E4%BB%8B%26%E5%8F%98%E9%87%8F%26%E7%B1%BB%26%E6%8E%A5%E5%8F%A3.md) +- [下一篇:3.Kotlin_数字&字符串&数组&集合](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md) --- diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index 3cab6d55..f7eb5bf6 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -561,8 +561,8 @@ loop@ for (i in 1..100) { ``` -[上一篇:Kotlin学习教程(二)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md) -[下一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) +- [上一篇:2.Kotlin_高阶函数&Lambda&内联函数](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md) +- [下一篇:4.Kotlin_表达式&关键字](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md) --- diff --git "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" index f2196189..7c3921ea 100644 --- "a/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" +++ "b/KotlinCourse/4.Kotlin_\350\241\250\350\276\276\345\274\217&\345\205\263\351\224\256\345\255\227.md" @@ -218,6 +218,12 @@ for(num in nums) { - `object`对象声明并且它总是在`object{: .keyword }`关键字后跟一个名称。对象表达式:在要创建一个继承自某个(或某些)类型的匿名类的对象会 用到 + + + +- [上一篇:3.Kotlin_数字&字符串&数组&集合](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md) +- [下一篇:5.Kotlin_内部类&密封类&枚举&委托](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md) + --- - 邮箱 :charon.chui@gmail.com diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index d6ea8ce6..0f50c432 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -670,8 +670,8 @@ class MutableUser(val map: MutableMap) { ``` -[上一篇:Kotlin学习教程(四)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md) -[下一篇:Kotlin学习教程(六)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md) +- [上一篇:4.Kotlin_表达式&关键字](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md) +- [下一篇:6.Kotlin_多继承问题](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md) --- diff --git "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" index 29bd7403..3dbf5fe7 100644 --- "a/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" +++ "b/KotlinCourse/6.Kotlin_\345\244\232\347\273\247\346\211\277\351\227\256\351\242\230.md" @@ -191,8 +191,8 @@ fun main(args: Array) { -[上一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) -[下一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) +- [上一篇:5.Kotlin_内部类&密封类&枚举&委托](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md) +- [下一篇:7.Kotlin_注解&反射&扩展](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md) --- diff --git "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" index 42330826..8bd40753 100644 --- "a/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" +++ "b/KotlinCourse/7.Kotlin_\346\263\250\350\247\243&\345\217\215\345\260\204&\346\211\251\345\261\225.md" @@ -1067,8 +1067,8 @@ class Group(val name: String) { -[上一篇:Kotlin学习教程(五)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md) -[下一篇:Kotlin学习教程(七)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md) +- [上一篇:6.Kotlin_多继承问题](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md) +- [下一篇:8.Kotlin_协程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md) --- diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index 4826886f..04243795 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -769,6 +769,8 @@ println("Done!") ``` +- [上一篇:7.Kotlin_注解&反射&扩展](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md) +- [下一篇:9.Kotlin_androidktx](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md) --- diff --git a/KotlinCourse/9.Kotlin_androidktx.md b/KotlinCourse/9.Kotlin_androidktx.md index aaef520a..5316334e 100644 --- a/KotlinCourse/9.Kotlin_androidktx.md +++ b/KotlinCourse/9.Kotlin_androidktx.md @@ -96,19 +96,11 @@ public class ForecastRequest(val zipCode: String) { -[上一篇:Kotlin学习教程(八)](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md) -[下一篇:Kotlin学习教程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md) +- [上一篇:8.Kotlin_协程](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md) +- [下一篇:10.Kotlin_设计模式](https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/10.Kotlin_%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -参考 -=== - -- [Kotlin for Android Developers](https://leanpub.com/kotlin-for-android-developers) -- [Resources to Learn Kotlin](https://developer.android.com/kotlin/resources.html) -- [Kotlin语言中文站](https://www.kotlincn.net/docs/reference/coding-conventions.html) - - --- - 邮箱 :charon.chui@gmail.com diff --git a/README.md b/README.md index 1a456b7f..78f801b2 100644 --- a/README.md +++ b/README.md @@ -144,16 +144,16 @@ Android学习笔记 - [Icon制作][223] - [Kotlin学习][48] - - [Kotlin学习教程(一)][180] - - [Kotlin学习教程(二)][181] - - [Kotlin学习教程(三)][182] - - [Kotlin学习教程(四)][183] - - [Kotlin学习教程(五)][184] - - [Kotlin学习教程(六)][185] - - [Kotlin学习教程(七)][186] - - [Kotlin学习教程(八)][187] - - [Kotlin学习教程(九)][188] - - [Kotlin学习教程(十)][197] + - [1.Kotlin_简介&变量&类&接口][180] + - [2.Kotlin_高阶函数&Lambda&内联函数][181] + - [3.Kotlin_数字&字符串&数组&集合][182] + - [4.Kotlin_表达式&关键字][183] + - [5.Kotlin_内部类&密封类&枚举&委托][184] + - [6.Kotlin_多继承问题][185] + - [7.Kotlin_注解&反射&扩展][186] + - [8.Kotlin_协程][187] + - [9.Kotlin_androidktx][188] + - [10.Kotlin_设计模式][197] @@ -492,15 +492,15 @@ Android学习笔记 [179]: https://github.com/CharonChui/AndroidNote/blob/master/BasicKnowledge/XmlPullParser.md "XmlPullParser" -[180]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%80).md "Kotlin学习教程(一)" -[181]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%8C).md "Kotlin学习教程(二)" -[182]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%89).md "Kotlin学习教程(三)" -[183]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%9B%9B).md "Kotlin学习教程(四)" -[184]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%BA%94).md "Kotlin学习教程(五)" -[185]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AD).md "Kotlin学习教程(六)" -[186]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B8%83).md "Kotlin学习教程(七)" -[187]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%85%AB).md "Kotlin学习教程(八)" -[188]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E4%B9%9D).md "Kotlin学习教程(九)" +[180]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/1.Kotlin_%E7%AE%80%E4%BB%8B%26%E5%8F%98%E9%87%8F%26%E7%B1%BB%26%E6%8E%A5%E5%8F%A3.md "1.Kotlin_简介&变量&类&接口" +[181]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/2.Kotlin_%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0%26Lambda%26%E5%86%85%E8%81%94%E5%87%BD%E6%95%B0.md "2.Kotlin_高阶函数&Lambda&内联函数.md" +[182]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/3.Kotlin_%E6%95%B0%E5%AD%97%26%E5%AD%97%E7%AC%A6%E4%B8%B2%26%E6%95%B0%E7%BB%84%26%E9%9B%86%E5%90%88.md "3.Kotlin_数字&字符串&数组&集合" +[183]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/4.Kotlin_%E8%A1%A8%E8%BE%BE%E5%BC%8F%26%E5%85%B3%E9%94%AE%E5%AD%97.md "4.Kotlin_表达式&关键字" +[184]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/5.Kotlin_%E5%86%85%E9%83%A8%E7%B1%BB%26%E5%AF%86%E5%B0%81%E7%B1%BB%26%E6%9E%9A%E4%B8%BE%26%E5%A7%94%E6%89%98.md "5.Kotlin_内部类&密封类&枚举&委托" +[185]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/6.Kotlin_%E5%A4%9A%E7%BB%A7%E6%89%BF%E9%97%AE%E9%A2%98.md "6.Kotlin_多继承问题" +[186]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/7.Kotlin_%E6%B3%A8%E8%A7%A3%26%E5%8F%8D%E5%B0%84%26%E6%89%A9%E5%B1%95.md "7.Kotlin_注解&反射&扩展" +[187]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/8.Kotlin_%E5%8D%8F%E7%A8%8B.md "8.Kotlin_协程" +[188]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/9.Kotlin_androidktx.md "9.Kotlin_androidktx" [189]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%85%AB%E7%A7%8D%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95.md "八种排序算法" [190]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%AE%80%E4%BB%8B.md "线程池简介" [191]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md "设计模式" @@ -509,7 +509,7 @@ Android学习笔记 [194]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/ConstraintLaayout%E7%AE%80%E4%BB%8B.md "ConstraintLaayout简介" [195]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Http%E4%B8%8EHttps%E7%9A%84%E5%8C%BA%E5%88%AB.md "Http与Https的区别" [196]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Top-K%E9%97%AE%E9%A2%98.md "Top-K问题" -[197]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/Kotlin%E5%AD%A6%E4%B9%A0%E6%95%99%E7%A8%8B(%E5%8D%81).md "Kotlin学习教程(十)" +[197]: https://github.com/CharonChui/AndroidNote/blob/master/KotlinCourse/10.Kotlin_%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md "10.Kotlin_设计模式" [198]: https://github.com/CharonChui/AndroidNote/blob/master/AppPublish/%E4%BD%BF%E7%94%A8Jenkins%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%8A%A8%E5%8C%96%E6%89%93%E5%8C%85.md "使用Jenkins实现自动化打包" [199]: https://github.com/CharonChui/AndroidNote/tree/master/Dagger2 "Dagger2" [200]: https://github.com/CharonChui/AndroidNote/blob/master/Dagger2/1.Dagger2%E7%AE%80%E4%BB%8B(%E4%B8%80).md "1.Dagger2简介(一).md" From 096b5da5a9def2ac9e7953213e6f6f7f71ba9f3d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 9 Apr 2021 15:54:50 +0800 Subject: [PATCH 022/183] update kotlin notes --- ...&\347\261\273&\346\216\245\345\217\243.md" | 134 ++++++++++++++++-- ...05\350\201\224\345\207\275\346\225\260.md" | 72 ++++++++++ ...0\347\273\204&\351\233\206\345\220\210.md" | 126 ++++++++++++++-- ...2\344\270\276&\345\247\224\346\211\230.md" | 58 +++++++- .../8.Kotlin_\345\215\217\347\250\213.md" | 19 +++ 5 files changed, 385 insertions(+), 24 deletions(-) diff --git "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" index 526cfff2..c4e3454b 100644 --- "a/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" +++ "b/KotlinCourse/1.Kotlin_\347\256\200\344\273\213&\345\217\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" @@ -204,6 +204,20 @@ var weight = 70.5 // double } ``` + + +### 变量保存了指向对象的引用 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/variable.jpg?raw=true) + +当该对象被赋值给变量时,这个对象本身并不会被直接赋值给当前的变量。相反,该对象的引用会被赋值给该变量。因为当前的变量存储的是对象的引用,因此它可以访问该对象。 + +如果你使用val来声明一个变量,那么该变量所存储的对象的引用将不可修改。然而如果你使用var声明了一个变量,你可以对该变量重新赋值。例如,如果我们使用代码: `x = 6`,将x的值赋为6,此时会创建一个值为6的新Int对象,并且x会存放该对象的引用。下面新的引用会替代原有的引用值被存放在x中: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_chage.jpg?raw=true) + +**注意: 在Java中,数字类型是原生类型,所以变量存储的是实际数值。但是在Kotlin中的数字也是对象,而变量仅仅存储该数字对象的引用,并非对象本身。** + ### 优先使用val来避免副作用 在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 @@ -267,7 +281,7 @@ var size: Int = 2 这个例子中就会内存溢出。 `kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 +我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。setter通过field标识更新变量属性值。field指的是属性的支持字段,你可以将其视为对属性的底层值的引用。在getter和setter中使用field代替属性名称很重要,因为这样可以阻止你陷入无限循环中。 ```kotlin class A { @@ -289,7 +303,17 @@ fun main() { 1 ``` +如果我们不手动写getter和setter方法,编译器会在编译代码时添加以下代码段: + +```kotlin +var myProperty: String + get() = field + set(value) { + field = value + } +``` +这意味着无论何时当你使用点操作符来获取或设置属性值时,实际上你总是调用了属性的getter或是setter。那么,为什么编译器要这么做呢?为属性添加getter和setter意味着有访问该属性的标准方法。getter处理获取值的所有请求,而setter处理所有属性值设置的请求。因此,如果你想要改变处理这些请求的方式,你可以在不破坏任何人代码的前提下进行。通过将其包装在getter和setter中来输出对属性的直接访问称为数据隐藏。 ## 延迟初始化 @@ -375,7 +399,20 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { ## 类的定义:使用`class`关键字 +当你在定义类的时候,你需要想想该类所创建的对象需要什么。你需要考虑: + +- 每个对象自身的特点 + + 对象自身的特点称为属性(properties)。它们代表了对象自身的状态(数据),并且该类中的每一个对象都有自己独特的数值。例如,一个狗(Dog)类可能有名字(name)、体重(weight)和品种(breed)属性。一个歌曲(Song)类可能有标题(title)和演唱者(artist)属性。 + +- 每个对象的行为 + + 对象的行为是它们的函数(functions)。它们决定了对象的行为,并且可能回使用对象的属性。例如,之前提到的Dog类,可能具有吠叫(bark)函数;Song这个类可能会有播放(play)函数。 + + + 类可以包含: + - 构造函数和初始化块 - 函数 - 属性 @@ -383,29 +420,67 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { - 对象声明 -```kotlin -class MainActivity{ +你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以拥有自己独有的属性值。例如,每个Dog对象都有自己的名称、重量和品种属性,每个Dog的属性值都可以是不同的。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_1.jpg?raw=true) + + + + +```kotlin +class Dog(val name: String, var weight: Int, val breed: String){ + fun bark() { + + } } ``` 如果有参数的话你只需要在类名后面写上它的参数,如果这个类没有任何内容可以省略大括号: ```kotlin -class Person(name: String, age: Int) +class Dog(val name: String, var weight: Int, val breed: String) ``` ### 创建类的实例 ```kotlin -val person = Person("charon", 18) +val myDog = Dog("Fido", 70, "Mixed" ) ``` 上面的类有一个默认的构造函数。 注意:创建类的实例不用`new`了啊。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_dog_sample.jpg?raw=true) + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_class_dog.jpg?raw=true) + +类中所定义的函数又称为成员函数。有时也被称为方法。 + +#### 创建对象的执行过程 + +```kotlin +var myDog = Dog("Fido", 70, "Mixed") +``` + +1. 系统会为每个传入Dog构造函数的参数创建一个对象。它会创建一个值为“Fido”的String,一个值为70的Int,以及一个值为“Mixed”的String。 +2. 系统会为一个新的Dog对象分配空间,并且Dog构造函数会被调用。 +3. Dog构造函数定义了三个属性:名称、重量以及品种。在这个现象背后,每一个属性实际上是一个变量。对于构造函数中定义的每个属性,都会有一个相应类型的变量被创建。 +4. 相应的变量的引用将会被赋值给Dog的属性。例如,值为“Fido”的String将会被赋值给name属性。 +5. 最后,这个新的Dog对象的引用将会被赋值给名为myDog的Dog变量。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_dog_new.jpg?raw=true) + + + + + + + ### 构造函数 +构造函数包含了初始化对象所需的代码。它在对象被分配给引用之前运行,这意味着你有机会对对象进行一些内部操作以便其被使用。大多人使用构造函数来定义对象的属性,并且给这些属性赋值。每当你创建一个新的对象,该对象所属的类的构造函数将会被调用。构造函数在你初始化对象时被调用。它通常被用于定义对象的属性,并且对属性赋值。 + 在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 #### 主构造函数 @@ -616,6 +691,8 @@ data class Artist( var mbid: String) ``` +数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。例如,假设你创建了两个属性值完全相同的Recipe对象,使用==操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。 + 通过数据类,会自动提供以下函数: - 所有属性的`get() set()`方法 @@ -649,7 +726,13 @@ val charon2 = charon.copy(age = 19) - data class之前不能用abstract、open、sealed或者inner进行修饰 - 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类 -## 多声明 + + +与任何其他类一样,你可以向数据类添加属性和方法,只需要将它们包含在类主体中。但是有一个大问题。在编译器生成数据类的方法实现时,比如覆盖equals方法和创建copy方法,它仅包含在主构造函数中定义的属性。因此如果你在数据类主体中定义添加的属性,则它们不会被包含到任何编译器生成的方法中。 + +### 数据类定义了componentN方法 + +定义数据类时,编译器会自动向该类添加一组方法,你可以将其作为访问对象属性值的替代方法。它们被称为componentN方法,其中N表示被访问属性的编号(按声明排序)。 多声明,也可以理解为变量映射,这就是编译器自动生成的`componentN()`方法。 @@ -689,6 +772,10 @@ open class Person(num: Int) class SuperPerson(num: Int) : Person(num) ``` +冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 + + + 如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: @@ -849,7 +936,7 @@ open class SuperPerson(num: Int) : Person(num) { } ``` -每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,你也可以用一个`var`属性覆盖一个`val`属性,但反之则不行。 +每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,如果某个属性在父类中被定义为val,你可以在子类中使用var属性覆盖它。只需要覆盖该属性并将其声明为var即可。请注意,这只适用于这一种方式。如果尝试使用val覆盖var属性,编译器将会感到沮丧并拒绝编译你的代码。 @@ -886,9 +973,10 @@ abstract class Derived : Base() { - ## 接口:使用`interface`关键字 +接口可以让你在父类层次结构之外定义共同的行为接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点,而且使用起来更加灵活。 + ```kotlin interface FlyingAnimal { fun fly() @@ -964,6 +1052,26 @@ toast("Hello") toast("Hello", Toast.LENGTH_LONG) ``` +### 无参主函数 + +如果你使用的是Kotlin1.2或更早的版本,若想正常运行程序,你的主函数必须写成如下形式: + +```kotlin +fun main(args: Array) { + // ... +} +``` + +从Kotlin1.3版本器,你可以忽略main函数的参数,写成如下形式: + +```kotlin +fun main() { + // ... +} +``` + + + ### 可变长参数函数:使用`vararg`关键字 @@ -981,6 +1089,16 @@ fun main(args: Array) { } ``` +如果你有一个现有的值数组,则可以通过在数组名前加上`*`来将这些值传递给该函数。星号`(*)`被称为扩展运算符,以下是它的一些使用示例: + +```kotlin +vval myArray = arrayOf(1, 2, 3, 4, 5) +val mList = vars(*myArray) +val mList2 = vars(0, *myArray, 6, 7) +``` + + + ### `Unit`:让函数调用皆为表达式 diff --git "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" index 8be83bb9..78f38efb 100644 --- "a/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" +++ "b/KotlinCourse/2.Kotlin_\351\253\230\351\230\266\345\207\275\346\225\260&Lambda&\345\206\205\350\201\224\345\207\275\346\225\260.md" @@ -252,6 +252,8 @@ val foo = { x: Int -> > “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 > `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包(注意和数学传统意义上的不同)。 + + `Java 8`的一个大亮点是引入`Lambda`表达式,使用它设计的代码会更加简洁。 ```java @@ -327,6 +329,50 @@ val sum: (Int, Int) -> Int = { x, y -> x + y } 则此`Lambda`表达式变成`val sum = { x: Int, y: Int -> x + y }`,此时`Lambda`表达式会根据主体中的最后一个(或可能是单个)表达式会视为 返回值。当然,在某些特定情况下,`x`、`y`的类型了是可以推断的,所以`val sum = { x, y -> x + y }`。 + + +通过调用lambda来执行它的代码你可以使用invoke函数调用lambda,并传入参数的值。例如,以下代码定义了变量addInts,并将用于将两个Int参数相加的lambda赋值给它。然后代码调用了该lambda,传入参数值6和7,并将结果赋值给变量result: + +```kotlin +val addInts = { x: Int, y: Int -> x + y } +val result = addInts.invoke(6, 7) +// 还可以使用如下快捷方式调用lambda: +val result = addInts(6, 7) + +``` + +### lambda表达式类型 + +就像任何其他类型的对象一样,lambda也具有类型。然而,lambda类型的不同点在于,它不会为lambda的实现指定类名,而是指定lambda的参数和返回值的类型。lambda类型的格式如下: + +```kotlin +(parameters) -> return_type +``` + +因此,如果你的lambda具有单独的Int参数并返回一个字符串,如下代码所示: + +```kotlin +val msg = { x: Int -> "xxx" } +``` + +其类型为: + +```kotlin +(Int) -> String +``` + +如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用: + +```kotlin +val add: (Int, Int) -> Int + +add = { x: Int, y: Int -> x + y } +``` + +Lambda类型也被认为是函数类型。 + + + ## Lambda开销 ```kotlin @@ -344,6 +390,32 @@ listOf(1, 2, 3).forEach { item -> foo(item) } 默认情况下,我们可以直接用it来代表item,而不需要用item -> 进行声明。 +你可以将单独的参数替换为it。 + +如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。 + +要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量: + +```kotlin +val addFive: (Int) -> Int = { x -> x + 5 } +``` + +由于lambda具有单独的参数x,而且编译器能够推断出x为Int类型,因此我们可以省略该x参数,并在lambda的主体中使用it替换它: + +```kotlin +val addFive: (Int) - Int = { it + 5 } +``` + +在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型: + +```kotlin +val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型 +``` + + + + + 我们看一下foo函数用IDE转换后的Java代码: ```java diff --git "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" index f7eb5bf6..2a31cef7 100644 --- "a/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" +++ "b/KotlinCourse/3.Kotlin_\346\225\260\345\255\227&\345\255\227\347\254\246\344\270\262&\346\225\260\347\273\204&\351\233\206\345\220\210.md" @@ -27,6 +27,7 @@ Byte 8 注意在`Kotlin`中字符不是数字,字符用`Char`类型表示。它们不能直接当作数字 + ### 字面常量 数值常量字面值有以下几种: @@ -87,6 +88,40 @@ toDouble(): Double toChar(): Char ``` +#### 数值类型转换背后发生了什么 + +```kotlin +var x = 5 // 这行代码创建了一个Int类型的变量x以及一个Int类型值为5的对象。x保存了该对象的引用 +var z : Long = x.toLong() // 这行代码创建了一个新的Long变量z。x对象的toLong()函数被调用并且创建了一个值为5的Long对象,该Long对象的引用被存储在z中 +``` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_type_change.jpg?raw=true) + +该方法可以较好地应用于从存储小数据的类型转换为能存储较大数据的类型。那么,如果该数值超出了新对象所能存储的范围该怎么办? + +试图将一个大的数值放入一个容量较小的变量中就好比试图将桶装咖啡倒入小茶杯中。有些咖啡会被倒入茶杯中,但是有些会溢出。 + +假如你想将Long的值放入Int中。正如我们之前所提到的,Long可以容纳比Int更大的数字。 + +因此如果Long的值在Int可存储的范围之内,那么从Long转换为Int是没有问题的。例如,将一个值为42的Long转换为Int将得到一个值为42的Int: + +```kotlin +var x = 42L +var y: Int = x.toInt() // 值为42 +``` + +但是如果Long的值超出了Int能容纳的范围,那么编译器将会舍弃超出的部分,此时你会得到一个奇怪(仍可计算)的数值。例如: + +```kotlin +var x = 1234567890123 +var y: Int = x.toInt() +println(y) // 1912276171 +``` + +这设计数值正负、位运算、二进制等其他一些计算机知识。这里不再细说。 + + + ### 运算 这是完整的位运算列表(只用于`Int`和`Long`): @@ -231,12 +266,23 @@ print(boxedA == anotherBoxedA) // 输出“true” ### 数组 +你可以将数组想象成一托盘的杯子,其中每个杯子都是一个变量。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/array.jpg?raw=true) + + + 数组用类`Array`实现,并且还有一个`size`属性及`get`和`set`方法,由于使用`[]`重载了`get`和`set`方法,所以我们可以通过下标很方便的获取或者 设置数组对应位置的值。 `Kotlin`标准库提供了`arrayOf()`创建数组和`xxArrayOf`创建特定类型数组 ```kotlin -val array = arrayOf(1, 2, 3) +val myArray = arrayOf(1, 2, 3) +``` + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/array_myarray.jpg?raw=true) + +```kotlin val countries = arrayOf("UK", "Germany", "Italy") val numbers = intArrayOf(10, 20, 30) val array1 = Array(10, { k -> k * k }) @@ -250,6 +296,30 @@ studentArray[0] = Student("james") ### 集合 +Kotlin有三个主要的集合类型(List、Set和Map),每一个都有不同的用途。 + +- List——当顺序很重要 + + List知道而且在意索引的位置。它知道List中的元素在哪里,而且你可以使多个元素指向同一个对象。 + +- Set——当唯一性很重要 + + Set不允许重复,而且不在意值的存放顺序。你不可以使多个元素指向同一个对象,或是被认为相等的两个对象。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_list_set.jpg?raw=true) + +- Map——当键检索很重要 + + Map使用键值对,它知道与给定键相关联的值。你可以使两个键指向同一个对象,但不可以有重复的键。键通常为String类型(因此你可以创建例如键值对属性列表),但它也可以是任意对象。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_map.jpg?raw=true) + + + +简单的List、Set和Map是不可变的,这意味着集合被初始化后不能再添加或移除元素。如果想要添加或移除元素,Kotlin提供了可变的子类型作为替代方案:MutableList、MutableSet和MutableMap。因此,如果想要利用List的所有优势,并希望能够更新其内容,请使用MutableList。 + + + `Kotlin`的`List`类型是一个提供只读操作如`size`、`get`等的接口。和`Java`类似,它继承自`Collection`进而继承自`Iterable`。 改变`list`的方法是由`MutableList`加入的。这一模式同样适用于`Set/MutableSet`及`Map/MutableMap`。 @@ -503,16 +573,6 @@ val l = b!!.length 因此,如果你想要一个NPE,你可以得到它,但是你必须显式要求它,否则它不会不期而至。 - -#### 安全的类型转换 - -如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 -另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: - -```kotlin -val aInt: Int? = a as? Int -``` - #### 可空类型的集合 如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用`filterNotNull`来实现。 @@ -526,7 +586,7 @@ val intList: List = nullableList.filterNotNull() ### 使用类型检测及自动类型转换 -`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用, +`is`运算符检测一个表达式是否某类型的一个实例。 如果一个不可变的局部变量或属性已经判断出为某类型,那么检测后的分支中可以直接当作该类型使用,在大多数情况下,is操作符会进行智能转换。转换表示编译器将变量当作与其声明的类型不同的类型,而智能转换是说编译器替你自动地进行转换。 无需显式转换: ```kotlin @@ -540,6 +600,48 @@ fun getStringLength(obj: Any): Int? { } ``` + + +只要编译器能够保证在介于判断对象类型和被使用之间不能修改变量,is操作符就会进行智能转换。 + +例如,在上面的代码中,编译器知道在介于调用is操作符和调用String的某个方法之间,item变量不能被赋予另一类型的引用。但是在一些特殊情况下,智能转换不会生效。例如,is操作符不会对类中的var属性进行智能转换,那是因为编译器无法保证别的代码不会溜进来更新该属性。这意味着如下代码将不能编译,因为编译器不能将r变量智能转换为一个Wolf对象: + +```kotlin +class MyRomable { + var r: Roamable = Wolf() + + fun myFunction() { + if (r is Wolf) { + r.eat() // 编译器无法智能的将Roamable的r属性转换成一个Wolf对象,这事因为编译器不能保证在判断r属性类型和使用它的器件,其它代码不会更新该属性,因此这段代码不能编译成功。 + } + } +} +``` + +那么遇到这种情况我们应该如何处理呢?你无须记住所有不能使用智能转换的场景。如果你尝试使用智能转换的方式不合理,编译器会提醒你。编译器会提醒你。 + +#### 安全的类型转换 + +如果对象不是目标类型,那么常规类型转换可能会导致`ClassCastException`。 +另一个选择是使用安全的类型转换,如果尝试转换不成功则返回`null{: .keyword }`: + +```kotlin +val aInt: Int? = a as? Int +``` + +如果你想要访问某个潜在对象的行为,但编译器无法对其进行智能转换,你可以显式地将该对象转换成合适的类型。假设你能够确定名为r的Roamable类型变量保存的是Wolf对象的引用。在这种情况下,你可以使用as操作符去复制一份Roamable类型变量中保存的引用,并强制地将该引用赋给一个新的Wolf类型变量。然后你就可以使用该Wolf类型变量去访问Wolf的行为。具体代码如下: + +```kotlin +if (r is Wolf) { + var wolf = r as Wolf // 这段代码显式地将对象转换为Wolf类型,使你可以调用它的方法 + wolf.eat() +} +``` + + + + + ### 返回和跳转 `Kotlin`有三种结构化跳转表达式: diff --git "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" index 0f50c432..1d18d2c4 100644 --- "a/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" +++ "b/KotlinCourse/5.Kotlin_\345\206\205\351\203\250\347\261\273&\345\257\206\345\260\201\347\261\273&\346\236\232\344\270\276&\345\247\224\346\211\230.md" @@ -171,9 +171,60 @@ val searchName: String = Icon.SEARCH.name() val searchPosition: Int = Icon.SEARCH.ordinal() ``` -### 密封类 -Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承,比如: + +枚举可以让你创建受限制的一组值,但在某些情况下,你需要更多的灵活性。假设你希望能在应用程序中使用两种不同的消息类型:一种用于”成功“,另一种用于”失败“,并且你希望能够将邮件限制为这两种类型。 + +如果你用枚举类对此进行建模,代码会如下: + +```kotlin +enum class MessageType(var msg: String) { + SUCCESS("Yay!"), + FAILURE("Boo!") +} +``` + +但是使用这种方法存在两个问题: + +- 每个值都是一个常量,并且仅作为单个实例存在。 + + 例如,你无法只在特定情况下修改SUCCESS的msg属性--因为一旦更改,代码中其他SUCCESS出现的地方也会被相应更改。 + +- 每个值必须有相同的属性和函数。 + + 向FAILURE值中添加Exception属性十分有用,它可以帮助你检查哪里出错了。但是枚举类不允许你这么做。 + +那么有其他的方法吗? 密封类可以拯救你。 + +### 密封类 + +Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承。密封类就像枚举类的加强版本。它允许你将类层次结构限制为一组特定的子类型,每个子类型都可以定义自己的属性和函数。与枚举类不同,你可以创建每种类型的多个实例。 你可以通过使用sealed前缀类名来创建密封类。例如,以下代码创建一个名为MessageType的密封类,其中包含名为MessageSuccess和MessageFailure的子类型。每个子类型都有一个名为msg的String属性,并且MessageFailure子类型中有一个额外的名为e的Exception属性: + +```kotlin +sealed class MessageType +// MessageSuccess和MessageFailure从MessageType继承而来,并且它们自己的类型是在自己的构造函数中定义的 +class MessageSuccess(var msg: String) : MessageType() +class MessageFailure(var msg: String, var e: Exception) : MessageType() +``` + +由于MessageType是一个有限子类的密封类。你可以使用when来检查每个子类型,这样可以避免使用额外的else子句,如以下代码所示: + +```kotlin +fun main(args: Array) { + val messageSuccess = MessageSuccess("Yay!") + val messageSuccess2 = MessageSuccess("It worked!") + val messageFailure = MessageFailure("Boo!", Exception("Gone wrong.")) + + var myMessageType: MessageType = messageFailure + val myMessage = when(myMessageType) { + is MessageSuccess -> myMessageType.msg + is MessageFailure -> myMessageType.msg + myMessageType.e.message + } + println(myMessage) +} +``` + +Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的。 ```kotlin sealed class Bird { @@ -182,8 +233,7 @@ sealed class Bird { } ``` -Kotlin通过sealed关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承他。 -但这种方式也有它的局限性。即它不能被初始化,因为它背后是基于一个抽象类实现的,这一点可以从它转换后的Java代码看出: +这一点可以从它转换后的Java代码看出: ```java public abstract class Bird { diff --git "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" index 04243795..727998d8 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -13,6 +13,8 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 +协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 + ## 使用 如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: @@ -25,6 +27,8 @@ dependencies { ## 示例 +在代码中,我们使用GlobalScope.launch在后台运行一个新的协程。在幕后,它创建了一个新的线程使协程在其中运行, + ```kotlin import kotlinx.coroutines.* @@ -44,6 +48,21 @@ World! 本质上,协程是轻量级的线程。它们在某些CoroutineScope上下文中与launch协程构建器一起启动。 这里我们在ClobalScope中启动了一个新的协程,这意味着新协程的生命周期只受整个应用程序的生命周期限制。 + + +使用runBlocking在相同作用域内运行协程如果希望代码在相同线程、不同协程中运行,你可以使用runBlocking函数。runBlocking是一个高阶函数,它会阻塞当前线程,直到传入其中的代码运行完成。runBlocking函数定义了一个作用域,传入其中的代码继承该作用域。在本例中,我们可以使用该作用域在同一线程中运行不同的协程。 + + + +delay函数暂停当前协程在这种情况下,更好的解决方案是使用协程的delay函数取而代之。该函数与Thread.sleep有相似的效果,除了它会暂停当前协程而不是当前线程。它将协程挂起指定的时长,这允许运行该线程中的其他代码。delay函数可用于以下两种情况: + +- 在协程中使用。 +- 在某个编译器知道将会暂停或挂起的函数中使用,在函数中调用可挂起的函数时(例如delay),该函数必须被标记为suspend。 + +例如,以下代码将协程延迟1秒: + + + ```kotlin import kotlinx.coroutines.* From 79db2bf48778f9b04c4beed7405ce4a8b819e6cb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 12 Apr 2021 14:39:23 +0800 Subject: [PATCH 023/183] add os notes --- ...MaterialDesign\344\275\277\347\224\250.md" | 281 +++++++++++++++++- .../Parcelable\345\217\212Serializable.md" | 2 + ...73\347\273\237\347\256\200\344\273\213.md" | 96 +++++- ...13\344\270\216\347\272\277\347\250\213.md" | 37 ++- ...05\345\255\230\347\256\241\347\220\206.md" | 40 ++- 5 files changed, 441 insertions(+), 15 deletions(-) diff --git "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" "b/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" index c6f3788d..549c18e9 100644 --- "a/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" +++ "b/AdavancedPart/MaterialDesign\344\275\277\347\224\250.md" @@ -26,8 +26,9 @@ Material Design Theme --- - 禁止Action Bar - 可以通过使用`Material theme`来让应用使用`Material Design`。想要使用`ToolBar`需要先禁用`ActionBar`。 - 可以通过自定义`theme`继承`Theme.AppCompat.Light.NoActionBar`或者在`theme`中通过以下配置来进行。 + ToolBar相当于是ActionBar的替代版,因此需要制定一个不带ActionBar的主题, + 可以通过自定义`theme`继承`Theme.AppCompat.Light.NoActionBar`或者在`theme`中通过以下配置来进行禁用`ActionBar`。 + ```xml false true @@ -59,8 +60,8 @@ Material Design Theme 配置的这几种颜色分别如下图所示: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/material_color.png?raw=true) - 里面没有`colorAccent`的颜色,这个颜色是设置`Checkbox`等控件选中时的颜色。 - +里面没有`colorAccent`的颜色,唯独colorAccent这个属性比较难理解,它不是用来指定Checkbox`等某一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色。 + 在`values-v21`中的`style.xml`中同样自定义`AppTheme`主题: ```xml - ``` - - 配置的这几种颜色分别如下图所示: - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/material_color.png?raw=true) -里面没有`colorAccent`的颜色,唯独colorAccent这个属性比较难理解,它不是用来指定Checkbox`等某一个按钮的颜色,而是更多表达了一个强调的意思,比如一些控件的选中状态也会使用colorAccent的颜色。 - - 在`values-v21`中的`style.xml`中同样自定义`AppTheme`主题: - ```xml - - ``` - -- 在`Manifest`文件中设置`AppTheme`主题: - - ```xml - - - - - ``` - 这里说一下为什么要在`values-v21`中也自定义个主题,这是为了能让在`21`以上的版本能更好的使用`Material Design`, -在21以上的版本中会有更多的动画、特效等。 - -- 让Activity继承AppCompatActivity - - ```java - public class MainActivity extends AppCompatActivity { - ... - } - ``` - -- 在布局文件中进行声明 - - 声明`toolbar.xml`,我们把他单独放到一个文件中,方便多布局使用: - ```xml - - ``` - 在`Activity`的布局中使用`ToolBar`: - - ```xml - - - - - - - - - - ``` - -- 在Activity中设置ToolBar - ```java - public class MainActivity extends AppCompatActivity{ - private Context mContext; - private Toolbar mToolbar; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mToolbar.setTitle(R.string.app_name); - // 将ToolBar设置为ActionBar,这样一设置后他就能像ActionBar一样直接显示menu目录中的菜单资源 - // 如果不用该方法,那ToolBar就只是一个普通的View,对menu要用inflateMenu去加载布局。 - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - } - ``` - -到这里运行项目就可以了,就可以看到一个简单的`ToolBar`实现。 - -接下来我们看一下`ToolBar`中具体有哪些内容: - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ToolBar_content.jpg?raw=true) - -我们可以通过对应的方法来修改他们的属性: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/toolbarCode.png?raw=true) - -ToolBar最左侧的这个按钮就叫做HomeAsUp按钮,它默认的图标是一个返回的箭头,含义是返回上一个活动,可以通过setDisplayHomeAsUpEnabled来让导航按钮显示出来。 可以在onOptionsItemSelected()方法中对HomeAsUp安阿牛的点击事件进行处理,HomeAsUp按钮的id永远都是android.R.id.home。 - -​ - -对于`ToolBar`中的`Menu`部分我们可以通过一下方法来设置: -```java -toolbar.inflateMenu(R.menu.menu_main); -toolbar.setOnMenuItemClickListener(); -``` -或者也可以直接在`Activity`的`onCreateOptionsMenu`及`onOptionsItemSelected`来处理: -```java -@Override -public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; -} - -@Override -public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); -} -``` -`menu`的实现如下: -```xml - - - - - - - -``` - -如果想要对`NavigationIcon`添加点击实现: -```java -toolbar.setNavigationOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - onBackPressed(); - } -}); -``` - -运行后发现我们强大的`Activity`切换动画怎么在`5.0`一下系统上实现呢?`support v7`包也帮我们考虑到了。使用`ActivityOptionsCompat` -及`ActivityCompat.startActivity`,但是悲剧了,他对4.0一下基本都无效,而且就算在4.0上很多动画也不行,具体还是用其他 -大神在`github`写的开源项目吧。 - - -- 动态取色Palette - - `Palette`这个类中可以提取一下集中颜色:   - - - Vibrant (有活力) - - Vibrant dark(有活力 暗色) - - Vibrant light(有活力 亮色) - - Muted (柔和) - - Muted dark(柔和 暗色) - - Muted light(柔和 亮色) - - ```java - //目标bitmap,代码片段 - Bitmap bm = BitmapFactory.decodeResource(getResources(), - R.drawable.kale); - Palette palette = Palette.generate(bm); - if (palette.getLightVibrantSwatch() != null) { - //得到不同的样本,设置给imageview进行显示 - iv.setBackgroundColor(palette.getLightVibrantSwatch().getRgb()); - iv1.setBackgroundColor(palette.getDarkVibrantSwatch().getRgb()); - iv2.setBackgroundColor(palette.getLightMutedSwatch().getRgb()); - iv3.setBackgroundColor(palette.getDarkMutedSwatch().getRgb()); - } - ``` - -使用DrawerLayout ---- - -- 布局中的使用 - -```xml - - - - - - - - - - - - - - - - - -``` - -使用DrawerLayout后可以实现类似SlidingMenu的效果。但是怎么将DrawerLayout与ToolBar结合起来呢? 还有再结合Navigation Tabs -以及ViewPager。下面我就直接上代码了。 - -先看布局: activity_main.xml -```xml - - - - - - - - - - - - - - - - - -``` - -MainActivity的代码: -```java -public class MainActivity extends AppCompatActivity { - - private Context mContext; - - private Toolbar mToolbar; - private PagerSlidingTabStrip mScrollingTabs; - private ViewPager mViewPager; - private MainPagerAdapter mPagerAdapter; - private ActionBarDrawerToggle mDrawerToggle; - private DrawerLayout mDrawerLayout; - - private List mTitles; - private List mFragments; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - mContext = this; - - findView(); - setToolBar(); - initView(); - initDrawerFragment(); - } - - private void findView() { - mToolbar = (Toolbar) findViewById(R.id.toolbar); - mScrollingTabs = (PagerSlidingTabStrip) findViewById(R.id.psts_main); - mViewPager = (ViewPager) findViewById(R.id.vp_main); - mDrawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); - } - - - private void setToolBar() { - mToolbar.setTitle(R.string.app_name); - setSupportActionBar(mToolbar); - getSupportActionBar().setDisplayShowHomeEnabled(true); - } - - private void initView() { - mFragments = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mFragments.add(new FriendsFragment()); - } - - mTitles = new ArrayList<>(); - for (int xxx = 0; xxx < 5; xxx++) { - mTitles.add("Tab : " + xxx); - } - - mPagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), mFragments, mTitles); - mViewPager.setAdapter(mPagerAdapter); - mScrollingTabs.setDividerColor(Color.TRANSPARENT); - mScrollingTabs.setIndicatorHeight(10); - mScrollingTabs.setUnderlineHeight(0); - mScrollingTabs.setTextSize(50); - mScrollingTabs.setTextColor(Color.BLACK); - mScrollingTabs.setSelectedTextColor(Color.WHITE); - mScrollingTabs.setViewPager(mViewPager); - - } - - private void initDrawerFragment() { - mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, mToolbar, R.string.drawer_open, R.string.drawer_close) { - @Override - public void onDrawerOpened(View drawerView) { - super.onDrawerOpened(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerClosed(View drawerView) { - super.onDrawerClosed(drawerView); - MainActivity.this.invalidateOptionsMenu(); - } - - @Override - public void onDrawerSlide(View drawerView, float slideOffset) { - super.onDrawerSlide(drawerView, slideOffset); - mToolbar.setAlpha(1 - slideOffset / 2); - } - }; - - mDrawerLayout.setDrawerListener(mDrawerToggle); - mDrawerToggle.syncState(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.menu_main, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - // Handle action bar item clicks here. The action bar will - // automatically handle clicks on the Home/Up button, so long - // as you specify a parent activity in AndroidManifest.xml. - int id = item.getItemId(); - - //noinspection SimplifiableIfStatement - if (id == R.id.action_settings) { - return true; - } - if (id == R.id.action_search) { - Toast.makeText(getApplicationContext(), "Search action is selected!", Toast.LENGTH_SHORT).show(); - return true; - } - return super.onOptionsItemSelected(item); - } - -} -``` -最后再看一下`DrawerFragment`的代码: -```java -public class DrawerFragment extends Fragment { - private Context mContext; - private RecyclerView mRecyclerView; - private NavigationDrawerAdapter mAdapter; - private static String[] titles = null; - - public DrawerFragment() { - - } - - public static List getData() { - List data = new ArrayList<>(); - - // preparing navigation drawer items - for (int i = 0; i < titles.length; i++) { - NavDrawerItem navItem = new NavDrawerItem(); - navItem.setTitle(titles[i]); - data.add(navItem); - } - return data; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - mContext = getActivity(); - // drawer labels - titles = getActivity().getResources().getStringArray(R.array.nav_drawer_labels); - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - // Inflating view layout - View layout = inflater.inflate(R.layout.fragment_navigation_drawer, container, false); - mRecyclerView = (RecyclerView) layout.findViewById(R.id.drawerList); - mRecyclerView.setHasFixedSize(true); - mAdapter = new NavigationDrawerAdapter(getActivity(), getData()); - mRecyclerView.setAdapter(mAdapter); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getActivity())); - mAdapter.setOnRecyclerViewListener(new NavigationDrawerAdapter.OnRecyclerViewListener() { - @Override - public void onItemClick(int position) { - Toast.makeText(mContext, getData().get(position).getTitle(), Toast.LENGTH_SHORT).show(); - startActivity(new Intent(getActivity(), FriendsActivity.class)); - } - - @Override - public boolean onItemLongClick(int position) { - return false; - } - }); - return layout; - } - -} -``` -上面的`PagerSlidingTabStrip`是开源项目,我改了下,添加了一个选中时的文字颜色改变。 - -[Demo地址](https://github.com/CharonChui/MaterialLibrary) - -### NavigationView - -NavigationView包含两个部分:menu和headerLayout。menu是用来在NavigationView中显示具体的菜单项的,headerLayout则是用来在NavigationView中显示头部布局的。 - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/navigator_view.png?raw=true) - -```xml - - - - - - - - - - - -``` - - - -drawer_header.xml 如下 - -```xml - - - - - - - - -``` - - - -drawer_view.xml 如下: - -```xml - - - - - - - - - - - - - - - - -``` - - - -我们给 Header 和 Menu 添加点击事件: - -```java -final NavigationView navigationView = (NavigationView) findViewById(R.id.navigation_view); - navigationView.getHeaderView(0).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - drawerLayout.closeDrawer(navigationView); - Toast.makeText(MainActivity.this, "Header View is clicked!", Toast.LENGTH_SHORT).show(); - } - }); - navigationView.setNavigationItemSelectedListener(new NavigationView.OnNavigationItemSelectedListener() { - @Override - public boolean onNavigationItemSelected(@NonNull MenuItem item) { - switch (item.getItemId()) { - case R.id.menu_home: - Toast.makeText(MainActivity.this, "Home is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_settings: - Toast.makeText(MainActivity.this, "Settings is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_share: - Toast.makeText(MainActivity.this, "Share is clicked!", Toast.LENGTH_SHORT).show(); - break; - case R.id.menu_about: - Toast.makeText(MainActivity.this, "About is clicked!", Toast.LENGTH_SHORT).show(); - break; - } - drawerLayout.closeDrawer(navigationView); - return false; - } - }); -``` - - - -### CoordinatorLayout - -**一、CoordinatorLayout 的作用** - -从名字可以看出,这个ViewGroup是用来协调它的子View的,CoordinatorLayout 作为一个 **“super-powered FrameLayout”**,主要有以下两个作用: - -1. 作为顶层布局; -2. 作为协调子View之间交互的容器。 - -CoordinatorLayout也是在`com.android.support.design`包中的组件。 - -通过为 CoordinatorLayout 的子View指定 Behaviors,你可以在单一父View下提供许多不同的交互,同时也可以让子View间各自进行交互。 -#### CoordinatorLayout与FloadingActionButton - -```xml - - - - -``` - - - -```java -public void onClick(View v) { - switch (v.getId()) { - case R.id.fab: - Snackbar.make(findViewById(R.id.contentView), "Snackbar", Snackbar.LENGTH_SHORT).show(); - break; - } -} -``` - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/smacker_floatingbutton.webp?raw=true) - -可以看到FloatingActionButton会被SmackBar遮挡,为了解决遮挡的问题,就需要使用到CoordinatorLayout。CoordinatorLayout可以说是一个加强版的FrameLayout,这个布局也是由Design Support库提供的。它在普通情况下的作用和FrameLayout基本一致,不过既然是Design Support库中提供的布局,那么就必然有一些Material Design的魔力了。 -事实上,CoordinatorLayout可以监听其所有子控件的各种事件,然后自动帮助我们做出最为合理的响应。举个简单的例子,刚才弹出的Snackbar提示将悬浮按钮遮挡住了,而如果我们能让CoordinatorLayout监听到Snackbar的弹出事件,那么它会自动将内部的FloatingActionButton向上偏移,从而确保不会被Snackbar遮挡到。 - -```xml - - - - - -``` - - - -悬浮按钮自动向上偏移了Snackbar的同等高度,从而确保不会被遮挡住,当Snackbar消失的时候,悬浮按钮会自动向下偏移回到原来位置。 -另外悬浮按钮的向上和向下偏移也是伴随着动画效果的,且和Snackbar完全同步,整体效果看上去特别赏心悦目。 - - - -或者我们也可以不把不过FloatingActionButton放到布局中,只是make()方法时传入的view是在布局中即可,我们回过头来再思考一下,刚才说的是CoordinatorLayout可以监听其所有子控件的各种事件,但是Snackbar好像并不是CoordinatorLayout的子控件吧,为什么它却可以被监听到呢? - -其实道理很简单,还记得我们在Snackbar的make()方法中传入的第一个参数吗?这个参数就是用来指定Snackbar是基于哪个View来触发的,刚才我们传入的是FloatingActionButton本身,而FloatingActionButton是CoordinatorLayout中的子控件,因此这个事件就理所应当能被监听到了。你可以自己再做个试验,如果给Snackbar的make()方法传入一个DrawerLayout,那么Snackbar就会再次遮挡住悬浮按钮,因为DrawerLayout不是CoordinatorLayout的子控件,CoordinatorLayout也就无法监听到Snackbar的弹出和隐藏事件了。 - - - -#### CollapsingToolbarLayout - -可折叠式标题栏,CollapsingToolbarLayout是一个作用于Toolbar基础之上的布局,它也是由Design Support库提供的。CollapsingToolbarLayout可以让Toolbar的效果变得更加丰富,不仅仅是展示一个标题栏,而是能够实现非常华丽的效果。 - - - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/CollapsingToolbarLayout.gif?raw=true) - - - -可以看到,我们在CollapsingToolbarLayout中定义了一个ImageView和一个Toolbar,也就意味着,这个高级版的标题栏将是由普通的标题栏加上图片组合而成的。这里定义的大多数属性我们都是见过的,就不再解释了,只有一个app:layout_collapseMode比较陌生。它用于指定当前控件在CollapsingToolbarLayout折叠过程中的折叠模式,其中Toolbar指定成pin,表示在折叠的过程中位置始终保持不变,ImageView指定成parallax,表示会在折叠的过程中产生一定的错位偏移,这种模式的视觉效果会非常好。 - -### NestedScrollView - -NestedScrollView 即 支持嵌套滑动的ScrollView。 - -因此,我们可以简单的把NestedScrollView类比为ScrollView,其作用就是作为控件父布局,从而具备(嵌套)滑动功能。 - -NestedScrollView与ScrollView的区别就在于NestedScrollView支持 *嵌套滑动*,无论是作为父控件还是子控件,嵌套滑动都支持,且默认开启。 - -因此,在一些需要支持嵌套滑动的情景中,比如一个ScrollView内部包裹一个RecyclerView,那么就会产生滑动冲突,这个问题就需要你自己去解决。而如果使用NestedScrollView包裹RecyclerView,嵌套滑动天然支持,你无需做什么就可以实现前面想要实现的功能了。 - -我们通常为RecyclerView增加一个Header和Footer的方法是通过定义不同的viewType来区分的,而如果使用NestedScrollView,我们完全可以把RecyclerView当成一个单独的控件,然后在其上面增加一个控件作为Header,在其下面增加一个控件作为Footer。 - -虽然NestedScrollView内嵌RecyclerView和其他控件可以实现Header和Footer,但还是不推荐上面这种做法(建议还是直接使用RecyclerView自己添加Header和Footer),因为虽然NestedScrollView支持嵌套滑动,但是在实际应用中,嵌套滑动可能会带来其他的一些奇奇怪怪的副作用,Google 也推荐我们能不使用嵌套滑动就尽量不要使用。 - - - -作者:Whyn -链接:https://www.jianshu.com/p/f55abc60a879 -来源:简书 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 - - - - - - - - -Ripple效果 ---- - -个人非常喜欢的效果。相当于给点击事件加上了动态的赶脚。。。 - - -假设现在有一个`Button`的`selector`,我们想给这个`Button`加上`Ripple`效果,肿么办? -新建一个`xml`文件,用`ripple`包裹`selector`,然后在`Button`的`backgroud`直接引用这个`xml`就好了。 -```xml - - - - - - - - -``` -但是很遗憾,`ripple`是5.0才有的,而且`support`包中没有实现该功能的扩展。 -`5.0`的这些效果还是无法在低版本上实现,包括一些`TextView`等样式,现在可以用大神的开源项目 -[MaterialDesignLibrary](https://github.com/navasmdc/MaterialDesignLibrary) - - -RecyclerView ---- - -`ListView`的升级版,还有什么理由不去用呢? 同样他也在`support v7`包中。 -``` -compile 'com.android.support:recyclerview-v7:21.+' -``` -通过`mRecyclerView.setLayoutManager(new LinearLayoutManager(this)); `设置为`LinearLayoutManager`来实现水平或者竖直 -方向的`ListView`。 - - -阴影 ---- - -通过对`View`设置`backgroud`后再添加`android:elevation="2dp"`来实现背景大小。 - - - -[更多实例请看MaterialSample](https://github.com/CharonChui/MaterialSample.git) - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! diff --git "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" "b/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" deleted file mode 100644 index 396665c9..00000000 --- "a/ArchitectureComponents/2.\351\233\206\346\210\220(\344\272\214).md" +++ /dev/null @@ -1,164 +0,0 @@ -2.集成(二) -=== - - -首先在`Project`目录中的`build.gradle`中添加`google()`仓库(大部分项目可能都已经有了): - -``` -allprojects { - repositories { - jcenter() - google() - } -} -``` - -然后在`app`的`build.gradle`中添加对应的依赖,如: - -``` -implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" -``` -如果想要使用`kotlin`开发的话,可以在后面加上`-ktx`后缀就可以了,如下: -``` -implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" -``` - -`Lifecycle`依赖: ---- - -`Lifecycle`依赖包括`LiveData`和`ViewModel` - -``` -dependencies { - def lifecycle_version = "1.1.1" - - // ViewModel and LiveData - implementation "android.arch.lifecycle:extensions:$lifecycle_version" - // alternatively - just ViewModel - implementation "android.arch.lifecycle:viewmodel:$lifecycle_version" // use -ktx for Kotlin - // alternatively - just LiveData - implementation "android.arch.lifecycle:livedata:$lifecycle_version" - // alternatively - Lifecycles only (no ViewModel or LiveData). - // Support library depends on this lightweight import - implementation "android.arch.lifecycle:runtime:$lifecycle_version" - - annotationProcessor "android.arch.lifecycle:compiler:$lifecycle_version" - // alternately - if using Java8, use the following instead of compiler - implementation "android.arch.lifecycle:common-java8:$lifecycle_version" - - // optional - ReactiveStreams support for LiveData - implementation "android.arch.lifecycle:reactivestreams:$lifecycle_version" - - // optional - Test helpers for LiveData - testImplementation "android.arch.core:core-testing:$lifecycle_version" -} -``` - - -`Room`依赖: ---- - -`Room`的依赖包括`testing Room migrations`和`Room RxJava` - -``` -dependencies { - def room_version = "1.1.1" - - implementation "android.arch.persistence.room:runtime:$room_version" - annotationProcessor "android.arch.persistence.room:compiler:$room_version" - - // optional - RxJava support for Room - implementation "android.arch.persistence.room:rxjava2:$room_version" - - // optional - Guava support for Room, including Optional and ListenableFuture - implementation "android.arch.persistence.room:guava:$room_version" - - // Test helpers - testImplementation "android.arch.persistence.room:testing:$room_version" -} - -``` - -`Paging`依赖 ---- - -``` -dependencies { - def paging_version = "1.0.0" - - implementation "android.arch.paging:runtime:$paging_version" - - // alternatively - without Android dependencies for testing - testImplementation "android.arch.paging:common:$paging_version" - - // optional - RxJava support, currently in release candidate - implementation "android.arch.paging:rxjava2:1.0.0-rc1" -} -``` - -`Navigation`依赖 ---- - -> Navigation classes are already in the androidx.navigation package, but currently depend on Support Library 27.1.1, and associated Arch component versions. Version of Navigation with AndroidX dependencies will be released in the future. - -``` -dependencies { - def nav_version = "1.0.0-alpha02" - - implementation "android.arch.navigation:navigation-fragment:$nav_version" // use -ktx for Kotlin - implementation "android.arch.navigation:navigation-ui:$nav_version" // use -ktx for Kotlin - - // optional - Test helpers - androidTestImplementation "android.arch.navigation:navigation-testing:$nav_version" // use -ktx for Kotlin -} -``` - - -`Safe args`依赖 ---- - -想要使用`Safe args`,需要在`Project`顶层的`build.gradle`中配置以下路径: -``` -buildscript { - repositories { - google() - } - dependencies { - classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha02" - } -} -``` -并且在`app`或`module`中`build.gradle`中: -``` -apply plugin: "androidx.navigation.safeargs" -``` - -`WorkManager`依赖 ---- - -> WorkManager classes are already in the androidx.work package, but currently depend on Support Library 27.1, and associated Arch component versions. Version of WorkManager with AndroidX dependencies will be released in the future. - - -``` -dependencies { - def work_version = "1.0.0-alpha03" - - implementation "android.arch.work:work-runtime:$work_version" // use -ktx for Kotlin - - // optional - Firebase JobDispatcher support - implementation "android.arch.work:work-firebase:$work_version" - - // optional - Test helpers - androidTestImplementation "android.arch.work:work-testing:$work_version" -} -``` - - -[上一篇: 1.简介(一)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/1.%E7%AE%80%E4%BB%8B(%E4%B8%80).md) -[下一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" "b/ArchitectureComponents/3.Lifecycle(\344\270\211).md" deleted file mode 100644 index b034723b..00000000 --- "a/ArchitectureComponents/3.Lifecycle(\344\270\211).md" +++ /dev/null @@ -1,175 +0,0 @@ -3.Lifecycle(三) -=== - - -`Android`开发中,经常需要管理生命周期。举个栗子,我们需要获取用户的地址位置,当这个`Activity`在显示的时候,我们开启定位功能,然后实时获取到定位信息,当页面被销毁的时候,需要关闭定位功能。 -```java -class MyLocationListener { - public MyLocationListener(Context context, Callback callback) { - // ... - } - - void start() { - // connect to system location service - } - - void stop() { - // disconnect from system location service - } -} - - -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - @Override - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, (location) -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - myLocationListener.start(); - // manage other components that need to respond - // to the activity lifecycle - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - // manage other components that need to respond - // to the activity lifecycle - } -} -``` - -上面的代码看起来还挺简单,但是当定位功能需要满足一些条件下才开启,那么会变得复杂多了。可能在执行`Activity`的`stop`方法时,定位的`start`方法才刚刚开始执行,比如如下代码,这样生命周期管理就变得很麻烦了。 -```java -class MyActivity extends AppCompatActivity { - private MyLocationListener myLocationListener; - - public void onCreate(...) { - myLocationListener = new MyLocationListener(this, location -> { - // update UI - }); - } - - @Override - public void onStart() { - super.onStart(); - Util.checkUserStatus(result -> { - // what if this callback is invoked AFTER activity is stopped? - if (result) { - myLocationListener.start(); - } - }); - } - - @Override - public void onStop() { - super.onStop(); - myLocationListener.stop(); - } -} -``` - -`android.arch.lifecycle`包提供的类和接口可帮助您用简单和独立的方式解决这些问题。 - -`Lifecycle`类是一个持有组件(`activity`或`fragment`)生命周期信息的类,其他对象可以观察该状态。`Lifecycle`使用两个重要的枚举部分来管理对应组件的生命周期的状态: - -- `Event`:生命周期事件由系统来分发,这些事件对应于`Activity`和`Fragment`的生命周期函数。 - -- `State`:`Lifecycle`对象所追踪的组件的当前状态 - - - - -```kotlin -class MainActivity : AppCompatActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - // lifecycle是LifecycleOwner接口的getLifecycle()方法得到的,从com.android.support:appcompat-v7:26.1.0开始activity和fragment都实现了该接口 - lifecycle.addObserver(MyObserver()) - } -} -``` - -```kotlin -class MyObserver : LifecycleObserver{ - @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) - fun connectListener() { - Log.e("@@@", "connect") - } - - @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) - fun disconnectListener() { - Log.e("@@@", "disconnect") - } -} -``` -上面的`lifecycle.addObserver(MyObserver()) `的完整写法应该是`aLifecycleOwner.getLifecycle().addObserver(new MyObserver())`而`aLifecycleOwner`一般是实现了`LifecycleOwner`的类,比如`Activity/Fragment` - - - -`LifecycleOwner` ---- - -那什么是`LifecycleOwner`呢?实现`LifecycleOwner`接口就表示这是个有生命周期的类,他有一个`getLifecycle ()`方法是必须实现的。 - -对于前面提到的监听位置的例子。可以把`MyLocationListener`实现`LifecycleObserver`,然后在`Lifecycle(Activity/Fragment)`的`onCreate`方法中初始化。这样`MyLocationListener`就能自行处理生命周期带来的问题。 - - - -从`Support Library 26.1.0`开始`Activity/Fragment`已经实现了`LifecycleOwner`接口。 -如果想在自定义的类中实现`LifecyclerOwner`,就需要用到[LifecycleRegistry](https://developer.android.com/reference/android/arch/lifecycle/LifecycleRegistry)类,并且需要自行发送`Event`: - -```java -public class MyActivity extends Activity implements LifecycleOwner { - private LifecycleRegistry mLifecycleRegistry; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - mLifecycleRegistry = new LifecycleRegistry(this); - mLifecycleRegistry.markState(Lifecycle.State.CREATED); - } - - @Override - public void onStart() { - super.onStart(); - mLifecycleRegistry.markState(Lifecycle.State.STARTED); - } - - @NonNull - @Override - public Lifecycle getLifecycle() { - return mLifecycleRegistry; - } -} -``` - - -`Lifecycles`的最佳建议: - -- 保持`UI Controllers(Activity/Fragment)`中代码足够简洁。一定不能包含如何获取数据的代码,要通过`ViewModel`获取`LiveData`形式的数据。 -- 用数据驱动`UI`,`UI`的职责就是根据数据改变显示的内容,并且把用户操作`UI`的行为传递给`ViewModel`。 -- 把业务逻辑相关的代码放到`ViewModel`中,把`ViewModel`看成是链接`UI`和`App`其他部分的纽带。但`ViewModel`不能直接获取数据,要通过调用其他类来获取数据。 -- 使用`DataBinding`来简化`View`(布局文件)和`UI Controllers(Activity/Fragment)`之间的代码 -- 如果布局本身太过复杂,可以考虑创建一个`Presenter`类来处理UI相关的改变。虽然这么做会多写很多代码,但是对于保持`UI`的简介和可测试性是有帮助的。 -- 不要在`ViewModel`中持有任何`View/Activity`的`context`。否则会造成内存泄露。 - - -[上一篇: 2.集成(二)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/2.%E9%9B%86%E6%88%90(%E4%BA%8C).md) -[下一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/4.LiveData(\345\233\233).md" "b/ArchitectureComponents/4.LiveData(\345\233\233).md" deleted file mode 100644 index 143fdc3f..00000000 --- "a/ArchitectureComponents/4.LiveData(\345\233\233).md" +++ /dev/null @@ -1,315 +0,0 @@ -4.LiveData(四) -=== - -> LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state. - - -`LiveData`是一种持有可被观察数据的类。和其他可被观察的类不同的是,`LiveData`是有生命周期感知能力的,这意味着它可以在`activities`,`fragments`,或者`services`生命周期是活跃状态时更新这些组件。那么什么是活跃状态呢?上篇文章中提到的`STARTED`和`RESUMED`就是活跃状态,只有在这两个状态下`LiveData`是会通知数据变化的。 - -要想使用`LiveData`(或者这种有可被观察数据能力的类)就必须配合实现了`LifecycleOwner`的对象使用。在这种情况下,当对应的生命周期对象`DESTROYED`时,才能移除观察者。这对`Activity`或者`Fragment`来说显得尤为重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。 - - - - -使用`LiveData`的优点: - -- `UI`和实时数据保持一致 因为`LiveData`采用的是观察者模式,这样一来就可以在数据发生改变时获得通知,更新`UI`。 -- 避免内存泄漏,观察者被绑定到组件的生命周期上,当被绑定的组件销毁(`destory`)时,观察者会立刻自动清理自身的数据。 -- 不会再产生由于`Activity`处于`stop`状态而引起的崩溃 例如:当`Activity`处于后台状态时,是不会收到`LiveData`的任何事件的。 -- 不需要再解决生命周期带来的问题`LiveData`可以感知被绑定的组件的生命周期,只有在活跃状态才会通知数据变化。 -- 实时数据刷新,当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据 -- 解决`Configuration Change`问题,在屏幕发生旋转或者被回收再次启动,立刻就能收到最新的数据。 -- 数据共享,如果对应的`LiveData`是单例的话,就能在`app`的组件间分享数据。 - - - -使用`LiveData`: - -- 创建一个持有某种数据类型的`LiveData`(通常是在`ViewModel`中) -- 创建一个定义了`onChange()`方法的观察者。这个方法是控制`LiveData`中数据发生变化时,采取什么措施 (比如更新界面)。通常是在`UI Controller`(`Activity/Fragment`)中创建这个观察者。 -- 通过`observe()`方法连接观察者和`LiveData`。`observe()`方法需要携带一个`LifecycleOwner`类。这样就可以让观察者订阅`LiveData`中的数据,实现实时更新。 - - -创建`LiveData`对象 ---- - -`LiveData`是一个数据的包装。具体的包装对象可以是任何数据,包括集合(比如`List`)。`LiveData`通常在`ViewModel`中创建,然后通过`getter`方法获取。具体可以看一下代码: -```java -public class NameViewModel extends ViewModel { - -// Create a LiveData with a String -private MutableLiveData mCurrentName; - - public MutableLiveData getCurrentName() { - if (mCurrentName == null) { - mCurrentName = new MutableLiveData(); - } - return mCurrentName; - } - -// Rest of the ViewModel... -} -``` - -观察`LiveData`中的数据 ---- - - -通常情况下都是在组件的`onCreate()`方法中开始观察数据,原因有以下两点: - -- 系统会多次调用`onResume()`方法 -- 确保`Activity/Fragment`在处于活跃状态时立刻可以展示数据。 - -```java -public class NameActivity extends AppCompatActivity { - - private NameViewModel mModel; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - // Other code to setup the activity... - - // Get the ViewModel. - mModel = ViewModelProviders.of(this).get(NameViewModel.class); - - - // Create the observer which updates the UI. - final Observer nameObserver = new Observer() { - @Override - public void onChanged(@Nullable final String newName) { - // Update the UI, in this case, a TextView. - mNameTextView.setText(newName); - } - }; - - // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer. - mModel.getCurrentName().observe(this, nameObserver); - } -} -``` - -更新`LiveData`对象 ---- - - -如果想要在`UI Controller`中改变`LiveData`中的值呢?(比如点击某个`Button`把性别从男设置成女)。`LiveData`并没有提供这样的功能,但是`Architecture Component`提供了`MutableLiveData`这样一个类,可以通过`setValue(T)`和`postValue(T)`方法来修改存储在`LiveData`中的数据。`MutableLiveData`是`LiveData`的一个子类,从名称上也能看出这个类的作用。举个直观点的例子: - -```java -mButton.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - String anotherName = "John Doe"; - mModel.getCurrentName().setValue(anotherName); - } -}) -``` -调用`setValue()`方法就可以把`LiveData`中的值改为`John Doe`。同样通过这种方法修改`LiveData`中的值同样会触发所有对这个数据感兴趣的类。那么`setValue()`和`postValue()`有什么不同呢?区别就是`setValue()`只能在主线程中调用,而`postValue()`可以在子线程中调用。 - - -`Room`和`LiveData`配合使用 ---- - -`Room`可以返回`LiveData`的数据类型。这样对数据库中的任何改动都会被传递出去。这样修改完数据库就能获取最新的数据,减少了主动获取数据的代码。 - -继承`LiveData`扩展功能 ---- - -`LiveData`的活跃状态包括:`STARTED`或者`RESUMED`两种状态。那么如何在活跃状态下把数据传递出去呢?下面是示例代码: - -```java -public class StockLiveData extends LiveData { - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - public StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` - -上面有三个重要的方法: - -- The onActive() method is called when the LiveData object has an active observer. This means you need to start observing the stock price updates from this method. -- The onInactive() method is called when the LiveData object doesn't have any active observers. Since no observers are listening, there is no reason to stay connected to the StockManager service. -- The setValue(T) method updates the value of the LiveData instance and notifies any active observers about the change. - -可以像下面这样使用`StockLiveData`: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - LiveData myPriceListener = ...; - myPriceListener.observe(this, price -> { - // Update the UI. - }); - } -} -``` -上面`observe()`方法中的第一个参数传递的是`fragment`的实例,该`fragment`实现了`LifecycleOwner`接口。这样做是为了将`observer`和`Lifecycle`对象绑定到一起,这意味着: -- 如果当前的`Lifecycle`对象不是出于活跃期,就算`value`值有改变也不会回调到`observer`中 -- 在`Lifecycle`对象销毁后哦,`observer`对象也会自动移除 - -实际上`LiveData`对象是适应生命周期也就意味着你需要在多个`activities`,`fragments`和`services`中进行共享,所以通常我们会将`LiveData`的示例设计成单例的: -```java -public class StockLiveData extends LiveData { - private static StockLiveData sInstance; - private StockManager mStockManager; - - private SimplePriceListener mListener = new SimplePriceListener() { - @Override - public void onPriceChanged(BigDecimal price) { - setValue(price); - } - }; - - @MainThread - public static StockLiveData get(String symbol) { - if (sInstance == null) { - sInstance = new StockLiveData(symbol); - } - return sInstance; - } - - private StockLiveData(String symbol) { - mStockManager = new StockManager(symbol); - } - - @Override - protected void onActive() { - mStockManager.requestPriceUpdates(mListener); - } - - @Override - protected void onInactive() { - mStockManager.removeUpdates(mListener); - } -} -``` -这样就可以在`fragment`中像如下这样使用: -```java -public class MyFragment extends Fragment { - @Override - public void onActivityCreated(Bundle savedInstanceState) { - StockLiveData.get(getActivity()).observe(this, price -> { - // Update the UI. - }); - } -} -``` - -转换LiveData ---- - -你可能有时会在`LiveData`分发给`observers`之前想要修改一下存储在`LiveData`中的值,或者你想根据当前的值进行修改返回另一个值。`Lifecycle`提供了`Transformations`类来通过里面的`helper`方法解决这种问题。 - -- `Transformations.map()` - -可以将`LiveData`中的数据进行改变。 - - -```java -LiveData userLiveData = ...; -LiveData userName = Transformations.map(userLiveData, user -> { - user.name + " " + user.lastName -}); -``` -将`LiveData`中的`User`数据转换成`String` - - -- `Transformations.switchMap()` - -```java -private LiveData getUser(String id) { - ...; -} - -LiveData userId = ...; -LiveData user = Transformations.switchMap(userId, id -> getUser(id) ); -``` - - -和上面的`map()`方法很像。区别在于传递给`switchMap()`的函数必须返回`LiveData`对象。 -和`LiveData`一样,`Transformation`也可以在观察者的整个生命周期中存在。只有在观察者处于观察`LiveData`状态时,`Transformation`才会运算。`Transformation`是延迟运算的(`calculated lazily`),而生命周期感知的能力确保不会因为延迟发生任何问题。 - -如果在`ViewModel`对象的内部需要一个`Lifecycle`对象,那么使用`Transformation`是一个不错的方法。举个例子:假如有个`UI`组件接受输入的地址,返回对应的邮政编码。那么可以 实现一个`ViewModel`和这个组件绑定: -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository; - } - - private LiveData getPostalCode(String address) { - // DON'T DO THIS - return repository.getPostCode(address); - } -} - -``` - -看代码中的注释,有个`// DON'T DO THIS`(不要这么干),这是为什么?有一种情况是如果`UI`组件被回收后又被重新创建,那么又会触发一次`repository.getPostCode(address)`询,而不是重用上次已经获取到的查询。那么应该怎样避免这个问题呢?看一下下面的代码: - -```java -class MyViewModel extends ViewModel { - private final PostalCodeRepository repository; - private final MutableLiveData addressInput = new MutableLiveData(); - public final LiveData postalCode = - Transformations.switchMap(addressInput, (address) -> { - return repository.getPostCode(address); - }); - - public MyViewModel(PostalCodeRepository repository) { - this.repository = repository - } - - private void setInput(String address) { - addressInput.setValue(address); - } -} -``` - -`postalCode`变量的修饰符是`public`和`final`,因为这个变量的是不会改变的。哎?不会改变?那我输入不同的地址还总返回相同邮编?先打住,`postalCode`这个变量存在的作用是把输入的`addressInput`转换成邮编,那么只有在输入变化时才会调用`repository.getPostCode()`方法。这就好比你用`final`来修饰一个数组,虽然这个变量不能再指向其他数组,但是数组里面的内容是可以被修改的。绕来绕去就一点:当输入是相同的情况下,用了`switchMap()`可以减少没有必要的请求。并且同样,只有在观察者处于活跃状态时才会运算并将结果通知观察者。 - - - - -合并多个`LiveData`中的数据 ---- - -`MediatorLiveData`是`LiveData`的子类,可以通过`MediatorLiveData`合并多个`LiveData`来源的数据。同样任意一个来源的`LiveData`数据发生变化,`MediatorLiveData`都会通知观察他的对象。说的有点抽象,举个例子。比如`UI`接收来自本地数据库和网络数据,并更新相应的`UI`。可以把下面两个`LiveData`加入到`MeidatorLiveData`中: - -- 关联数据库的`LiveData` -- 关联联网请求的`LiveData` -相应的`UI`只需要关注`MediatorLiveData`就可以在任意数据来源更新时收到通知。 - - - -[上一篇: 3.Lifecycle(三)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/3.Lifecycle(%E4%B8%89).md) -[下一篇: 5.ViewModel(五)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/5.ViewModel(%E4%BA%94).md) - - - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" "b/ArchitectureComponents/5.ViewModel(\344\272\224).md" deleted file mode 100644 index 98ea2f37..00000000 --- "a/ArchitectureComponents/5.ViewModel(\344\272\224).md" +++ /dev/null @@ -1,138 +0,0 @@ -5.ViewModel(五) -=== - -`ViewModel`是用来存储`UI`层的数据,以及管理对应的数据,当数据修改的时候,可以马上刷新`UI`。 - -`Android`系统提供控件,比如`Activity`和`Fragment`,这些控件都是具有生命周期方法,这些生命周期方法被系统调用。 - -当这些控件被销毁或者被重建的时候,如果数据保存在这些对象中,那么数据就会丢失。比如在一个界面,保存了一些用户信息,当界面重新创建的时候,就需要重新去获取数据。当然了也可以使用控件自动再带的方法,在`onSaveInstanceState`方法中保存数据,在`onCreate`中重新获得数据,但这仅仅在数据量比较小的情况下。如果数据量很大,这种方法就不能适用了。 - -另外一个问题就是,经常需要在`Activity`中加载数据,这些数据可能是异步的,因为获取数据需要花费很长的时间。那么`Activity`就需要管理这些数据调用,否则很有可能会产生内存泄露问题。最后需要做很多额外的操作,来保证程序的正常运行。 - -同时`Activity`不仅仅只是用来加载数据的,还要加载其他资源,做其他的操作,最后`Activity`类变大,就是我们常讲的上帝类。也有不少架构是把一些操作放到单独的类中,比如`MVP`就是这样,创建相同类似于生命周期的函数做代理,这样可以减少`Activity`的代码量,但是这样就会变得很复杂,同时也难以测试。 - -`AAC`中提供`ViewModel`可以很方便的用来管理数据。我们可以利用它来管理`UI`组件与数据的绑定关系。`ViewModel`提供自动绑定的形式,当数据源有更新的时候,可以自动立即的更新`UI`。 - - -实现`ViewModel` ---- - -```java -public class MyViewModel extends ViewModel { - private MutableLiveData> users; - public LiveData> getUsers() { - if (users == null) { - users = new MutableLiveData>(); - loadUsers(); - } - return users; - } - - private void loadUsers() { - // Do an asynchronous operation to fetch users. - } -} -``` - -然后可以再`activity`像如下这样获取数据: -```java -public class MyActivity extends AppCompatActivity { - public void onCreate(Bundle savedInstanceState) { - // Create a ViewModel the first time the system calls an activity's onCreate() method. - // Re-created activities receive the same MyViewModel instance created by the first activity. - - MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class); - model.getUsers().observe(this, users -> { - // update UI - }); - } -} -``` - -在`activity`重建后,它会收到在第一个`activity`中创建的同一个`MyViewModel`实例,当所属的`activity`销毁后,`framework`会调用`ViewModel`对象的`onCleared()` -方法来清除资源。 - - -`ViewModel`的生命周期 ---- - -`ViewModel`在获取`ViewModel`对象时会通过`ViewModelProvider`的传递来绑定对应的声明周期。 -`ViewModel`只有在`Activity finish`或者`Fragment detach`之后才会销毁。 - - - - - - -在`Fragments`间分享数据 ---- - -有时候一个`Activity`中的两个或多个`Fragment`需要分享数据或者相互通信,这样就会带来很多问题,比如数据获取,相互确定生命周期。 - -使用`ViewModel`可以很好的解决这个问题。假设有这样两个`Fragment`,一个`Fragment`提供一个列表,另一个`Fragment`提供点击每个`item`现实的详细信息。 - - -```java -public class SharedViewModel extends ViewModel { - private final MutableLiveData selected = new MutableLiveData(); - - public void select(Item item) { - selected.setValue(item); - } - - public LiveData getSelected() { - return selected; - } -} - -public class MasterFragment extends Fragment { - private SharedViewModel model; - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - itemSelector.setOnClickListener(item -> { - model.select(item); - }); - } -} - -public class DetailFragment extends LifecycleFragment { - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class); - model.getSelected().observe(this, { item -> - // update UI - }); - } -} -``` - -两个`Fragment`都是通过`getActivity()`来获取`ViewModelProvider`。这意味着两个`Activity`都是获取的属于同一个`Activity`的同一个`ShareViewModel`实例。 -这样做优点如下: - -- `Activity`不需要写任何额外的代码,也不需要关心`Fragment`之间的通信。 -- `Fragment`不需要处理除`SharedViewModel`以外其他的代码。这两个`Fragment`不需要知道对方是否存在。 -- `Fragment`的生命周期不会相互影响 - - - - -`ViewModel`和`SavedInstanceState`对比 ---- - -`ViewModel`使得在`configuration change`(旋转屏幕等)保存数据变的十分方便,但是这不能用于应用被系统杀死时持久化数据。举个简单的例子,有一个界面展示国家信息。 -不应该把整个国家信息放到`SavedInstanceState`里,而是把国家对应的`id`放到`SavedInstanceState`,等到界面恢复时,再通过`id`去获取详细的信息。这些详细的信息应该被存放在数据库中。 - - - - - - -[上一篇: 4.LiveData(四)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/4.LiveData(%E5%9B%9B).md) -[下一篇: 6.Room(六)](https://github.com/CharonChui/AndroidNote/blob/master/ArchitectureComponents/6.Room(%E5%85%AD).md) - - ---- - -- 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/Jetpack\347\256\200\344\273\213.md" "b/Jetpack/Jetpack\347\256\200\344\273\213.md" new file mode 100644 index 00000000..e29d72c2 --- /dev/null +++ "b/Jetpack/Jetpack\347\256\200\344\273\213.md" @@ -0,0 +1,161 @@ +# Jetpack简介 + +JetPack是Google推出的一些库的集合。是Android基础支持库SDK以外的部分。包含了组件、工具、架构方案等...开发者可以自主按需选择接入具体的哪个库。 + +## 背景 + +### Support库 + +早之前的Android更新迭代是,所有的功能更新都是跟随着每一个特定的Android版本所发布的。 +例如: +- Fragment是在Android 3.0更新的。 +- Material Design组件是在Android 5.0上更新的 +但是由于Android用户庞大,每一次Android更新都无法覆盖所有用户的,同时因为手机厂商众多,但支持有限,也无法为自己生产的所有设备保持迭代到最新的Android版本,所以用户所持有的设备上Android版本是层次不齐的。 +从技术的角度来说,因为用户的设备版本不一致,导致Android工程师在维护项目的时候,会遇到很多难以解决的问题。为了解决这些由于Android版本不一致而出现的兼容性问题,Google推出了Support库。 + +Support库是针对Framework API的补充,Framework API跟随每一个Android版本所发布,和设备具有强关联性,Support API是开发者自主集成的,最终会包含在我们所发布的应用中,这样我们就可以在最新的Android版本上进行应用的开发,同时使用Support API解决各种潜在的兼容性问题,帮助开发者在不同Android版本的设备上实现行为一致的工作代码。 + +### Support 库的弊端 +最早的Support库发布于2011年,版本号为:android.support.v4,也就是我们所熟知的v4库,2013年在v4的基础上,Android团队发布了v7库,版本号为:android.support.v7,之后还发布了用于特定场景的v8、v13、v14、v17。 +如果是前几年刚开始学习Android的同学们,一定都对这些奇怪的数字很疑惑,4、7、8、13、14、17 到底都是什么意思? +拿第一代支持库v4举例,最初本意是指:该支持库最低可以支持到API 4(Android 1.4)的设备,v7表示最低支持 API 7(Android 2.1)的设备,但随着Android版本的持续更新,API 4以及API 7的设备早就淘汰了。在2017年7月Google将所有支持库的最低API支持版本提高到了API 14(Android 4.0),但由于包名无法修改,所以还是沿用之前的v4、v7命名标准,所以就出现了Support库第一个无法解决的问题:版本增长混乱。 + +与此同时Support库还面临一个非常严峻的问题:架构设计本身导致的严重依赖问题。最早的Support库是v4,v7是基于v4进行的补充,因为v4、v7太过庞大,功能集中,所以如果想要开发新的支持库,也只能在v4、v7的基础上进行二次开发,比方说我们后期常用的,RecyclerView、CardView等等。 + +这样就会产生很严重的重复依赖的问题,在无论是使用官方库,还是第三方库的时候,我们都需要保持整个项目中Support库版本一致,我相信很多人都在这个问题上踩过坑,虽然还是有办法解决这个问题,但无形中增加了很多工作量。 + +我们都知道“组合优于继承”这句话,但Support库在最初的架构设计上,却采用了重继承轻组合的方式,我猜这可能是因为开发Support库的人和开发Framework API的是同一批人有关,Framework API里有种各种继承逻辑,例如我们常用的 Activity、Fragment、View。 + +虽然在后期Google尝试拆分Support库,例如推出了独立的支持库:support:design、support:customtabs等,但并不能从根源解决依赖的问题。 + +### Android X +从Goole IO 2017开始。Google开始推出Architecture Component,ORM库Room,用户生命周期管理的ViewModel/LiveData. +Goole IO 2018将Support lib更名为androidx.将许多Google认为是正确的方案和实践集中起来。可以说AndroidX的出现就是为了解决长久以来Support库混乱的问题,你也可以把AndroidX理解为更强大的Support库。 +AndroidX将原有的Support库拆分为85个大大小小的支持库,抛弃了之前与API最低支持相关联的版本命名规范,重置为1.0.0,并且每一个库在之后都会按照严格的语义版本控制规则进行版本控制。 +同时通过组合依赖的方式,我们可以选择自己需要的组件库,而不是像Support一样全部依赖,一定程度上也减小了应用的体积。 +很重要的一点,就是它不会随着特定的Android版本而更新,它是由开发者自主控制,同时包含在我们所发布的应用程序中。 + + +以上种种,现在统称为JetPack. +Jetpack的出现是为了彻底解决这两个致命的问题: +1. Support 库版本增长混乱 +2. Support 库重复依赖 +如果Jetpack仅仅是针对Support库的重构,那它并没有了不起的,因为这只是Google解决了它自身因为历史原因所产生的代码问题。 +更重要的是Jetpack为大家提供了一系列的最佳实践,包含:架构、数据处理、后台任务处理、动画、UI 各个方面,无需纠结于各种因为Android本身而出现的问题,而是让我们把更多的精力放在业务需求的实现上,这才是Jetpack真正了不起的地方。其最核心的出发点就是帮助开发者快速构建出稳定、高性能、测试友好同时向后兼容的APP。 + +Jetpack相当于Google把自己的Android生态重新整理了一番。确立了Android未来的版图和大方向。 + +## 组成部分 + +前面讲到过,JetPack是一系列库和工具的集合,它更多是Google的一个提出的一个概念,或者说态度。 +并非所有的东西都是每年在IO大会上新推出的,它也包含了对现有基础库的整理和扩展。在大部分项目中其实我们都有用到JetPack的内容,也许你只是不知道而已。让我们以上帝视角来看看整个JetPack除了你熟悉的部分,还有哪些是你不熟悉但是听过的内容。看看他们都能做些什么事情。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_jetpack.png) + +[Jetpack完整组件列表](https://developer.android.com/jetpack/androidx/explorer) + +### Foundation(基础组件): +基础组件提供了横向功能,例如向后兼容性、测试以及Kotlin语言的支持。它包含如下组件库: +- Android KTX:Android KTX 是一组 Kotlin 扩展程序,它优化了供Kotlin使用的Jetpack和Android平台的API。以更简洁、更愉悦、更惯用的方式使用Kotlin进行Android开发。 +- AppCompat:提供了一系列以AppCompat开头的API,以便兼容低版本的Android开发。Jetpack基础中的AppCompat库包含v7库中的所有组件([支持库软件包](https://developer.android.com/topic/libraries/support-library/packages#v7-appcompat))。 其中包括AppCompat,Cardview,GridLayout,MediaRouter,Palette,RecyclerView,Renderscript,Preferences,Leanback,Vector Drawable,Design,Custom选项卡等。此外,该库为材质设计用户界面提供了实现支持,这使得AppCompat对 开发人员。 以下是android应用程序的一些关键领域,这些领域很难构建,但是可以使用AppCompat库轻松进行设计: 一般都是为了兼容 Android L以下版本,来提供Material Design的效果: + - Toolbar + - ContextCompat + - AppCompatDialog +- annotation:注解,提升代码可读性,内置了Android中常用的注解 +- Multidex(多Dex处理):为方法数超过 64K 的应用启用多 dex 文件。Security(安全):按照安全最佳做法读写加密文件和共享偏好设置。 +- Test(测试):用于单元和运行时界面测试的 Android 测试框架。 + +### Architecture(架构组件) +架构组件可帮助开发者设计稳健、可测试且易维护的应用。它包含如下组件库: +- Data Binding(数据绑定):数据绑定库是一种支持库,借助该库,可以使用声明式将布局中的界面组件绑定到应用中的数据源。 +- Lifecycles:方便管理Activity和Fragment生命周期,帮助开发者书写更轻量、易于维护的代码。 +- ViewModel:以生命周期感知的方式存储和管理与UI相关的数据。 +- LiveData:是一个可观察的数据持有者类。与常规observable不同,LiveData是有生命周期感知的。 +- Navigation:处理应用内导航所需的一切。 +- Paging:帮助开发者一次加载和显示小块数据。按需加载部分数据可减少网络带宽和系统资源的使用。 +- Room:Room持久性库在SQLite上提供了一个抽象层,帮助开发者更友好、流畅的访问SQLite数据库。 +- WorkManager:即使应用程序退出或设备重新启动,也可以轻松地调度预期将要运行的可延迟异步任务。 +- hilt:基于Dagger的Android依赖注入框架绑定View和Model +- startup:自动处理依赖初始化 +- datastore:Preferences的替代类,支持异步、更加安全 + + + +### Behavior(行为组件) +行为组件可帮助开发者的应用与标准Android服务(如通知、权限、分享和Google助理)相集成。它包含如下组件库: +- CameraX:帮助开发者简化相机应用的开发工作。它提供一致且易于使用的 API 界面,适用于大多数 Android 设备,并可向后兼容至 Android 5.0(API 级别 21)。 +- DownloadManager下载管理器:可处理长时间运行的HTTP下载,并在出现故障或在连接更改和系统重新启动后重试下载。 +- Media & playback(媒体&播放):用于媒体播放和路由(包括 Google Cast)的向后兼容 API。 +- Notifications(通知):提供向后兼容的通知 API,支持 Wear 和 Auto。 +- Permissions(权限):用于检查和请求应用权限的兼容性 API。 +- Preferences(偏好设置):提供了用户能够改变应用的功能和行为能力。 +- Sharing(共享):提供适合应用操作栏的共享操作。 +- Slices(切片):创建可在应用外部显示应用数据的灵活界面元素。 + +### UI(界面组件) +大多数的UI组件其实都包含在基础组件中的appcompat中,这里是一些独立组件库存在的UI组件,界面组件可提供各类view和辅助程序,让应用不仅简单易用,还能带来愉悦体验。它包含如下组件库: +- drawerlayout:抽屉布局 +- recyclerview:可复用的滑动列表 +- constraintlayout:约束布局 +- compose*: Jetpack compose声明式UI +- coordinatorlayout:顶层布局继承自Framelayout,可以实现子View之间的联动交互效果 +- swiperefreshlayout:下拉刷新布局 +- viewpager2:分页布局 +- Material Design Components * : MD组件 +- Animation & Transitions(动画&过度):提供各类内置动画,也可以自定义动画效果。 +- Emoji(表情符号):使用户在未更新系统版本的情况下也可以使用表情符号。 + - EmojiTextView + - EmojiEditTExt + - EmojiButton +- Fragment:组件化界面的基本单位。 +- Layout(布局):xml书写的界面布局或者使用Compose完成的界面。 + 用户界面结构(如应用程序的活动)由Layout定义。 它定义了View和ViewGroup对象。 可以通过两种方式创建View和ViewGroup:通过以XML声明UI元素或通过编写代码(即以编程方式)。 Jetpack的这一部分涵盖了一些最常见的布局,例如LinearLayout,RelativeLayout和全新的ConstraintLayout。 而且,官方的Jetpack布局文档提供了一些指导,以使用RecyclerView创建项目列表以及使用CardView创建卡布局。 用户可以看到一个视图。 EditView,TextView和Button是View的示例。 另一方面,ViewGroup是一个容器对象,它定义了View的布局结构,因此它是不可见的。 ViewGroup的示例是LinearLayout,RelativeLayout和ConstraintLayout。 +- Palette(调色板):从调色板中提取出有用的信息。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jetpack_compose.png?raw=true) + +[Android Jetpack Compose](https://developer.android.com/jetpack/compose)是2019 Google/IO大会上推出的一种声明式的UI开发框架,经过一年左右的演进,现在到了alpha阶段。Jetpack Compose是用于构建原生界面的新款Android工具包。它可简化并加快Android上的界面开发。使用更少的代码、强大的工具和直观的KotlinAPI,快速让应用生动而精彩,从此不再需要写xml,使用声明式的Compose函数来构建页面UI。 + +Compose由androidx中的6个Maven组ID构成。每个组都包含一套特定用途的功能,并各有专属的版本说明。下表介绍了各个组及指向其版本说明的链接: +- compose.animation:在Jetpack Compose应用中构建动画,丰富用户的体验。 +- compose.compiler:借助Kotlin编译器插件,转换@Composable functions(可组合函数)并启用优化功能。 +- compose.foundation:使用现成可用的构建块编写Jetpack Compose应用,还可扩展Foundation以构建您自己的设计系统元素。 +- compose.material:使用现成可用的Material Design组件构建Jetpack Compose UI。这是更高层级的Compose入口点,旨在提供与www.material.io上描述的组件一致的组件。 +- compose.runtime:Compose的编程模型和状态管理的基本构建块,以及Compose编译器插件针对的核心运行时。 +- compose.ui:与设备互动所需的Compose UI的基本组件,包括布局、绘图和输入。 +```kotlin +class MainActivity : AppCompatActivity() { + overridefun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + Text("Hello, Android技术杂货铺") + } + } +} +``` +废弃部分: +- asynclayoutinflater:异步生成UI +- localbroadcastmanager:本地广播 +- viewpager:使用vviewpager2 +- cardview:使用MaterialCardView替代 + + +## Jetpack的意义 +Jetpack里目前包含的内容,未来也会是Google大力维护和扩展的内容。对应开发者来说也是值得去学习使用的且相对无后顾之忧的。JetPack里没有的,除开一些优秀的第三方库,未来应该也会慢慢被新的API替代,逐渐边缘化,直至打上Deprecate注解。 + +以当下的环境来说,要开发出一个完全摆脱JetPack的APP是很难做到的。但是反过来讲JetPack也远远没有到成熟的地步,目前也还存在亟待解决的问题,未来可以做的事情还有很多。 + +关于使用的话,并不是所有库都建议使用,因为目前还有很多库在alpha版本。但是作为学习还是很有必要的,能给你日常的开发中多提供一些思路,这些是无可厚非的。 + + + + + +## Jetpack库列表及集成 + +[Jetpack各库版本及gradle集成使用](https://developer.android.com/jetpack/androidx/releases/activity) + + + +## 参考 +- [Jetpack Compose初体验 ](https://easyliu-ly.github.io/2020/12/12/android_jetpack/compose/) diff --git "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" similarity index 96% rename from "ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" rename to "Jetpack/architecture/1.\347\256\200\344\273\213.md" index 7716d3b9..5345872d 100644 --- "a/ArchitectureComponents/1.\347\256\200\344\273\213(\344\270\200).md" +++ "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" @@ -1,4 +1,4 @@ -1.简介(一) +1.简介 === 应用开发者面临的常见问题 @@ -97,6 +97,8 @@ override fun onDestroy() { 除了有可能引发内存泄漏的风险, 数据持久化也是一个经常困扰我们的问题.通常在屏幕旋转后,`UI`的对象都会被销毁重建,这将导致原来的对象数据不得不重新创建和获取,浪费资源的同时也会影响用户的体验. 通常的解决方法是,通过`SavedInstanceState`来存取数据,但`SavedInstanceState`存储的数据一般比较小,且数据对象还是必须重新构建. +为了将代码解耦以应对日益膨胀的代码量,工程师在应用程序中引入了“架构”的概念。使之在不影响应用程序各模块组件间通信的同时,还能够保持模块的相对独立。这样不仅有利于后期维护,也有利于代码测试。 + 上述两个问题可以通过使用`AAC`架构解决. diff --git "a/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" new file mode 100644 index 00000000..2b6cdb21 --- /dev/null +++ "b/Jetpack/architecture/10.DataStore\347\256\200\344\273\213.md" @@ -0,0 +1,205 @@ +# 10.DataStore简介 + +Jetpack DataStore 是一种数据存储解决方案,允许您使用[协议缓冲区](https://developers.google.com/protocol-buffers)存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 + +如果您当前在使用 [`SharedPreferences`](https://developer.android.com/reference/kotlin/android/content/SharedPreferences) 存储数据,请考虑迁移到 DataStore。 + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是 DataStore。DataStore 非常适合简单的小型数据集,不支持部分更新或参照完整性。 + +## Preferences DataStore 和 Proto DataStore + +DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。 + +- **Preferences DataStore** 像SharedPreferences一样,以键值对的形式进行基本类型的数据存储。DataStore 基于 Flow 实现异步存储,避免因为阻塞主线程带来的ANR问题 +- **Proto DataStore** 基于Protobuf实现任意自定义类型的数据存储,需要定义Protobuf的IDL,但是可以保证类型安全的访问 + + + +如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容: + +```groovy +// Preferences DataStore (SharedPreferences like APIs) +dependencies { + implementation "androidx.datastore:datastore-preferences:1.0.0-beta01" + + // optional - RxJava2 support + implementation "androidx.datastore:datastore-preferences-rxjava2:1.0.0-beta01" + + // optional - RxJava3 support + implementation "androidx.datastore:datastore-preferences-rxjava3:1.0.0-beta01" +} +// Alternatively - use the following artifact without an Android dependency. +dependencies { + implementation "androidx.datastore:datastore-preferences-core:1.0.0-beta01" +} +``` + + + + + +Jetpack DataStore 是一种数据存储解决方案,允许您使用[协议缓冲区](https://developers.google.com/protocol-buffers)存储键值对或类型化对象。DataStore 使用 Kotlin 协程和 Flow 以异步、一致的事务方式存储数据。 + +如果您当前在使用 [`SharedPreferences`](https://developer.android.com/reference/kotlin/android/content/SharedPreferences) 存储数据,请考虑迁移到 DataStore。 + +**注意**:如果您需要支持大型或复杂数据集、部分更新或参照完整性,请考虑使用 [Room](https://developer.android.com/training/data-storage/room),而不是 DataStore。DataStore 非常适合简单的小型数据集,不支持部分更新或参照完整性。 + +## Preferences DataStore 和 Proto DataStore + +DataStore 提供两种不同的实现:Preferences DataStore 和 Proto DataStore。 + +- **Preferences DataStore** 使用键存储和访问数据。此实现不需要预定义的架构,也不确保类型安全。 +- **Proto DataStore** 将数据作为自定义数据类型的实例进行存储。此实现要求您使用[协议缓冲区](https://developers.google.com/protocol-buffers)来定义架构,但可以确保类型安全。 + +## 设置 + +如需在您的应用中使用 Jetpack DataStore,请根据您要使用的实现向 Gradle 文件添加以下内容: + +[类型化](https://developer.android.com/topic/libraries/architecture/datastore#类型化-datastore)[偏好设置](https://developer.android.com/topic/libraries/architecture/datastore#偏好设置-datastore) + +``` +// Typed DataStore (Typed API surface, such as Proto)dependencies { implementation "androidx.datastore:datastore:1.0.0-beta01" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.0.0-beta01" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.0.0-beta01"}// Alternatively - use the following artifact without an Android dependency.dependencies { implementation "androidx.datastore:datastore-core:1.0.0-beta01"} +``` + +**注意**:如果您将 `datastore-preferences-core` 工件与 Proguard 搭配使用,就必须手动将 Proguard 规则添加到 `proguard-rules.pro` 文件中,以免您的字段遭到删除。您可以点击[此处](https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:datastore/datastore-preferences/proguard-rules.pro)查找必要的规则。 + +## 使用 Preferences DataStore 存储键值对 + +Preferences DataStore 实现使用 [`DataStore`](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore) 和 [`Preferences`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/Preferences) 类将简单的键值对保留在磁盘上。 + +### 创建 Preferences DataStore + +使用由 [`preferencesDataStore`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/package-summary#dataStore) 创建的属性委托来创建 `Datastore` 实例。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性访问该实例。这样可以更轻松地将 `DataStore` 保留为单例。此外,如果您使用的是 RxJava,请使用 [`RxPreferenceDataStoreBuilder`](https://developer.android.com/reference/kotlin/androidx/datastore/rxjava2/RxDataStoreBuilder)。必需的 `name` 参数是 Preferences DataStore 的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +// At the top level of your kotlin file:val Context.dataStore: DataStore by preferencesDataStore(name = "settings") +``` + +### 从 Preferences DataStore 读取内容 + +由于 Preferences DataStore 不使用预定义的架构,因此您必须使用相应的键类型函数为需要存储在 `DataStore` 实例中的每个值定义一个键。例如,如需为 int 值定义一个键,请使用 [`intPreferencesKey()`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#intPreferencesKey(kotlin.String))。然后,使用 [`DataStore.data`](https://developer.android.com/reference/kotlin/androidx/datastore/core/DataStore#data) 属性,通过 `Flow` 提供适当的存储值。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val EXAMPLE_COUNTER = intPreferencesKey("example_counter")val exampleCounterFlow: Flow = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0} +``` + +### 将内容写入 Preferences DataStore + +Preferences DataStore 提供了一个 [`edit()`](https://developer.android.com/reference/kotlin/androidx/datastore/preferences/core/package-summary#edit) 函数,用于以事务方式更新 `DataStore` 中的数据。该函数的 `transform` 参数接受代码块,您可以在其中根据需要更新值。转换块中的所有代码均被视为单个事务。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 }} +``` + +## 使用 Proto DataStore 存储类型化的对象 + +Proto DataStore 实现使用 DataStore 和[协议缓冲区](https://developers.google.com/protocol-buffers)将类型化的对象保留在磁盘上。 + +### 定义架构 + +Proto DataStore 要求在 `app/src/main/proto/` 目录的 proto 文件中保存预定义的架构。此架构用于定义您在 Proto DataStore 中保存的对象的类型。如需详细了解如何定义 proto 架构,请参阅 [protobuf 语言指南](https://developers.google.com/protocol-buffers/docs/proto3)。 + +``` +syntax = "proto3"; option java_package = "com.example.application"; option java_multiple_files = true; message Settings { int32 example_counter = 1; } +``` + +**注意**:您的存储对象的类在编译时由 proto 文件中定义的 `message` 生成。请务必重新构建您的项目。 + +### 创建 Proto DataStore + +创建 Proto DataStore 来存储类型化对象涉及两个步骤: + +1. 定义一个实现 `Serializer` 的类,其中 `T` 是 proto 文件中定义的类型。此序列化器类会告知 DataStore 如何读取和写入您的数据类型。请务必为该序列化器添加默认值,以便在尚未创建任何文件时使用。 +2. 使用由 `dataStore` 创建的属性委托来创建 `DataStore` 的实例,其中 `T` 是在 proto 文件中定义的类型。在您的 Kotlin 文件顶层调用该实例一次,便可在应用的所有其余部分通过此属性委托访问该实例。`filename` 参数会告知 DataStore 使用哪个文件存储数据,而 `serializer` 参数会告知 DataStore 第 1 步中定义的序列化器类的名称。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +object SettingsSerializer : Serializer { override val defaultValue: Settings = Settings.getDefaultInstance() override suspend fun readFrom(input: InputStream): Settings { try { return Settings.parseFrom(input) } catch (exception: InvalidProtocolBufferException) { throw CorruptionException("Cannot read proto.", exception) } } override suspend fun writeTo( t: Settings, output: OutputStream) = t.writeTo(output)}val Context.settingsDataStore: DataStore by dataStore( fileName = "settings.pb", serializer = SettingsSerializer) +``` + +### 从 Proto DataStore 读取内容 + +使用 `DataStore.data` 显示所存储对象中相应属性的 `Flow`。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleCounterFlow: Flow = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter } +``` + +### 将内容写入 Proto DataStore + +Proto DataStore 提供了一个 [`updateData()`](https://developer.android.com/reference/kotlin/androidx/datastore/DataStore#updatedata) 函数,用于以事务方式更新存储的对象。`updateData()` 为您提供数据的当前状态,作为数据类型的一个实例,并在原子读-写-修改操作中以事务方式更新数据。 + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() }} +``` + +## 在同步代码中使用 DataStore + +**注意**:请尽可能避免在 DataStore 数据读取时阻塞线程。阻塞界面线程可能会导致 [ANR](https://developer.android.com/topic/performance/vitals/anr) 或界面卡顿,而阻塞其他线程可能会导致[死锁](https://en.wikipedia.org/wiki/Deadlock)。 + +DataStore 的主要优势之一是异步 API,但可能不一定始终能将周围的代码更改为异步代码。如果您使用的现有代码库采用同步磁盘 I/O,或者您的依赖项不提供异步 API,就可能出现这种情况。 + +Kotlin 协程提供 [`runBlocking()`](https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) 协程构建器,以帮助消除同步与异步代码之间的差异。您可以使用 `runBlocking()` 从 DataStore 同步读取数据。RxJava 在 `Flowable` 上提供阻塞方法。以下代码会阻塞发起调用的线程,直到 DataStore 返回数据: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +val exampleData = runBlocking { context.dataStore.data.first() } +``` + +对界面线程执行同步 I/O 操作可能会导致 ANR 或界面卡顿。您可以通过从 DataStore 异步预加载数据来减少这些问题: + +[Kotlin](https://developer.android.com/topic/libraries/architecture/datastore#kotlin)[Java](https://developer.android.com/topic/libraries/architecture/datastore#java) + +``` +override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. }} +``` + +这样,DataStore 可以异步读取数据并将其缓存在内存中。以后使用 `runBlocking()` 进行同步读取的速度可能会更快,或者如果初始读取已经完成,可能也可以完全避免磁盘 I/O 操作。 + + + + + + + + + + + +https://developer.android.com/topic/libraries/architecture/datastore#typed-datastore + +https://android-developers.googleblog.com/2020/09/prefer-storing-data-with-jetpack.html + + +https://blog.csdn.net/zzw0221/article/details/109274610 + +SharedPreferences 有着许多缺陷: 看起来可以在 UI 线程安全调用的同步 API 其实并不安全、没有提示错误的机制、缺少事务 API 等等。DataStore 是 SharedPreferences 的替代方案,它解决了 Shared Preferences 的绝大部分问题。DataStore 包含使用 Kotlin 协程和 Flow 实现的完全异步 API,可以处理数据迁移、保证数据一致性,并且可以处理数据损坏。 + +https://blog.csdn.net/weixin_42324979/article/details/112650189?utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7EBlogCommendFromMachineLearnPai2%7Edefault-3.control + + + +缺点: + +- 目前Jetpack Security没有支持DataStore,所以不能像SharedPreference一样支持加密 +- 不能安全的进行IPC,这点相对于SharedPreferences没有提升,有较强IPC需求的话首选MMKV +- 使用PB进行序列化时需要额外定义IDL,这会产生一定工作量 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" new file mode 100644 index 00000000..38ba0a7e --- /dev/null +++ "b/Jetpack/architecture/11.Hilt\347\256\200\344\273\213.md" @@ -0,0 +1,16 @@ +# 11.Hilt简介 + + + + + +https://zhuanlan.zhihu.com/p/335631378 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" new file mode 100644 index 00000000..18a39661 --- /dev/null +++ "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" @@ -0,0 +1,487 @@ +# 12.Navigation简介 + +单个Activity嵌套多个Fragment的UI架构模式,已经被大多数Android工程师所接受和采用。但是,对Fragment的管理一直是一件比较麻烦的事情。工程师需要通过FragmentManager和FragmentTransaction来管理Fragment之间的切换。页面的切换通常还包括对应用程序App bar的管理、Fragment间的切换动画,以及Fragment间的参数传递。纯代码的方式使用起来不是特别友好,并且Fragment和App bar在管理和使用的过程中显得很混乱。 + +为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。 + +它具有以下优势: + +- 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。 +- 通过destination和action完成页面间的导航。 +- 方便添加页面切换动画。 +- 页面间类型安全的参数传递。 +- 通过NavigationUI类,对菜单、底部导航、抽屉菜单导航进行统一的管理。 +- 支持深层链接DeepLink。 + +Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 `NavHostFragment`。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。 + +## 依赖 + +如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖: + +``` +dependencies { + def nav_version = "2.3.5" + + // Java language implementation + implementation "androidx.navigation:navigation-fragment:$nav_version" + implementation "androidx.navigation:navigation-ui:$nav_version" + + // Kotlin + implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" + implementation "androidx.navigation:navigation-ui-ktx:$nav_version" + + // Feature module Support + implementation "androidx.navigation:navigation-dynamic-features-fragment:$nav_version" + + // Testing Navigation + androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" + + // Jetpack Compose Integration + implementation "androidx.navigation:navigation-compose:1.0.0-alpha10" +} + +``` + + + + + +## Navigation的主要元素 + +导航组件由以下三个关键部分组成: + +1. Navigation Graph + + 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为*目标*)以及用户可以通过应用获取的可能路径。 + +2. NavHost + + 显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。 + +3. NavController + + 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + +在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。 + + + +## Navigation Graph(导航图) + +导航图是一种资源文件,其中包含您的所有目的地和操作。该图表会显示应用的所有导航路径。 + + + +如需向项目添加导航图,请执行以下操作: + +1. 在“Project”窗口中,右键点击 `res` 目录,然后依次选择 **New > Android Resource File**。此时系统会显示 **New Resource File** 对话框。 +2. 在 **File name** 字段中输入名称,例如“nav_graph”。 +3. 从 **Resource type** 下拉列表中选择 **Navigation**,然后点击 **OK**。 + +``` + + + +``` + +`` 元素是导航图的根元素。当您向图表添加目的地和连接操作时,可以看到相应的 `` 和 `` 元素在此处显示为子元素。如果您有[嵌套图表](https://developer.android.com/guide/navigation/navigation-nested-graphs),它们将显示为子 `` 元素。 + +## 向 Activity 添加 NavHost + +导航宿主是 Navigation 组件的核心部分之一。导航宿主是一个空容器,用户在您的应用中导航时,目的地会在该容器中交换进出。 + +导航宿主必须派生于 [`NavHost`](https://developer.android.com/reference/androidx/navigation/NavHost)。Navigation 组件的默认 `NavHost` 实现 ([`NavHostFragment`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment)) 负责处理 Fragment 目的地的交换。 + + + +### 通过 XML 添加 NavHostFragment + +以下 XML 示例显示了作为应用主 Activity 一部分的 `NavHostFragment`: + +```xml + + + + + + + + + + +``` + +NavHostFragment是一个特殊的Fragment,我们需要将其添加到Activity的布局文件中,作为其他Fragment的容器。 + +请注意以下几点: + +- `android:name` 属性包含 `NavHost` 实现的类名称。 +- `app:navGraph` 属性将 `NavHostFragment` 与导航图相关联。导航图会在此 `NavHostFragment` 中指定用户可以导航到的所有目的地。 +- `app:defaultNavHost="true"` 属性确保您的 `NavHostFragment` 会自动处理系统返回键,即当用户按下手机的返回按钮时,系统能自动将当前所展示的Fragment退出。请注意,只能有一个默认 `NavHost`。如果同一布局(例如,双窗格布局)中有多个宿主,请务必仅指定一个默认 `NavHost`。 + + + +```xml + + // 起始fragment + + +``` + +### Action + +```xml + + + + + + + + +``` + +在导航图中,操作由 `` 元素表示。操作至少应包含自己的 ID 和用户应转到的目的地的 ID。 + +## 导航到目的地 + +导航到目的地是使用 [`NavController`](https://developer.android.com/reference/androidx/navigation/NavController) 完成的,它是一个在 `NavHost` 中管理应用导航的对象。每个 `NavHost` 均有自己的相应 `NavController`。您可以使用以下方法之一检索 `NavController`: + +**Kotlin**: + +- [`Fragment.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/fragment/package-summary#findnavcontroller) +- [`View.findNavController()`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#(android.view.View).findNavController()) +- [`Activity.findNavController(viewId: Int)`](https://developer.android.com/reference/kotlin/androidx/navigation/package-summary#findnavcontroller) + +**Java**: + +- [`NavHostFragment.findNavController(Fragment)`](https://developer.android.com/reference/androidx/navigation/fragment/NavHostFragment#findNavController(android.support.v4.app.Fragment)) +- [`Navigation.findNavController(Activity, @IdRes int viewId)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.app.Activity, int)) +- [`Navigation.findNavController(View)`](https://developer.android.com/reference/androidx/navigation/Navigation#findNavController(android.view.View)) + +使用 `FragmentContainerView` 创建 `NavHostFragment`,或通过 `FragmentTransaction` 手动将 `NavHostFragment` 添加到您的 Activity 时,尝试通过 `Navigation.findNavController(Activity, @IdRes int)` 检索 Activity 的 `onCreate()` 中的 `NavController` 将失败。您应改为直接从 `NavHostFragment` 检索 `NavController`。 + +```kotlin +val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment +val navController = navHostFragment.navController +navController.navigate(R.id.action_blankFragment_to_blankFragment2) +``` + +对于按钮,您还可以使用 [`Navigation`](https://developer.android.com/reference/androidx/navigation/Navigation) 类的 [`createNavigateOnClickListener()`](https://developer.android.com/reference/androidx/navigation/Navigation#createNavigateOnClickListener(int)) 便捷方法导航到目的地,如下例所示: + +```kotlin +button.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.next_fragment, null)) + +``` + + + +## 使用 DeepLinkRequest 导航 + +您可以使用 [`navigate(NavDeepLinkRequest)`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(androidx.navigation.NavDeepLinkRequest)) 直接导航到[隐式深层链接目的地](https://developer.android.com/guide/navigation/navigation-deep-link#implicit),如下例所示: + +```kotlin +val request = NavDeepLinkRequest.Builder + .fromUri("android-app://androidx.navigation.app/profile".toUri()) + .build() +findNavController().navigate(request) + +``` + +## 导航和返回堆栈 + +Android 会维护一个[返回堆栈](https://developer.android.com/guide/components/activities/tasks-and-back-stack),其中包含您之前访问过的目的地。当用户打开您的应用时,应用的第一个目的地就放置在堆栈中。每次调用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法都会将另一目的地放置到堆栈的顶部。点按**向上**或**返回**会分别调用 [`NavController.navigateUp()`](https://developer.android.com/reference/androidx/navigation/NavController#navigateUp()) 和 [`NavController.popBackStack()`](https://developer.android.com/reference/androidx/navigation/NavController#popBackStack()) 方法,用于移除(或弹出)堆栈顶部的目的地。 + +`NavController.popBackStack()` 会返回一个布尔值,表明它是否已成功返回到另一个目的地。当返回 `false` 时,最常见的情况是手动弹出图的起始目的地。 + +如果该方法返回 `false`,则 `NavController.getCurrentDestination()` 会返回 `null`。您应负责导航到新目的地,或通过对 Activity 调用 `finish()` 来处理弹出情况,如下例所示: + +```kotlin + +if (!navController.popBackStack()) { + // Call finish() on your Activity + finish() +} + +``` + +## popUpTo 和 popUpToInclusive + +使用操作进行导航时,您可以选择从返回堆栈上弹出其他目的地。例如,如果您的应用具有初始登录流程,那么在用户登录后,您应将所有与登录相关的目的地从返回堆栈上弹出,这样返回按钮就不会将用户带回登录流程。 + +如需在从一个目的地导航到另一个目的地时弹出目的地,请在关联的 `` 元素中添加 `app:popUpTo` 属性。`app:popUpTo` 会告知 Navigation 库在调用 `navigate()` 的过程中从返回堆栈上弹出一些目的地。属性值是应保留在堆栈中的最新目的地的 ID。 + +您还可以添加 `app:popUpToInclusive="true"`,以表明在 `app:popUpTo` 中指定的目的地也应从返回堆栈中移除。 + +## 通过 引用其他导航图 + +在导航图中,您可以使用 `include` 引用其他图。虽然这在功能上与使用嵌套图相同,但 `include` 可让您使用其他项目模块或库项目中的图,如以下示例所示: + +```xml + + + + + + + + + + + ... + +``` + +```xml + + + + + + +``` + + + +## 创建全局操作 + +您可以使用全局操作来创建可由多个目的地共用的通用操作。例如,您可能想要不同目的地中的多个按钮导航到同一应用主屏幕。 + +```xml + + + + ... + + + + +``` + +如需在代码中使用某个全局操作,请将该全局操作的资源 ID 传递到每个界面元素的 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 方法,如以下示例所示: + +```kotlin +viewTransactionButton.setOnClickListener { view -> + view.findNavController().navigate(R.id.action_global_mainFragment) +} +``` + +## 使用 Safe Args 实现类型安全的导航 + +如需在目的地之间导航,建议使用 Safe Args Gradle 插件。此插件可生成简单的对象和构建器类,以便在目的地之间实现类型安全的导航。我们强烈建议您在导航以及[在目的地之间传递数据](https://developer.android.com/guide/navigation/navigation-pass-data#Safe-args)时使用 Safe Args。 + + + +如需将 [Safe Args](https://developer.android.com/topic/libraries/architecture/navigation/navigation-pass-data#Safe-args) 添加到您的项目中,请在顶层 `build.gradle` 文件中包含以下 `classpath`: + +```xml +buildscript { + repositories { + google() + } + dependencies { + def nav_version = "2.3.5" + classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" + } +} +``` + +您还必须应用以下两个可用插件之一。 + +如需生成适用于 Java 模块或 Java 和 Kotlin 混合模块的 Java 语言代码,请将以下行添加到**应用或模块**的 `build.gradle` 文件中: + +`apply plugin: "androidx.navigation.safeargs"` + +此外,如需生成适用于 Kotlin 独有的模块的 Kotlin 代码,请添加以下行: + +`apply plugin: "androidx.navigation.safeargs.kotlin"` + +根据[迁移到 AndroidX](https://developer.android.com/jetpack/androidx/migrate#migrate)) 文档,您的 [`gradle.properties` 文件](https://developer.android.com/studio/build#properties-files)中必须具有 `android.useAndroidX=true`。 + +启用 Safe Args 后,生成的代码会包含已定义的每个操作的类和方法,以及与每个发送目的地和接收目的地相对应的类。 + +Safe Args 为生成操作的每个目的地生成一个类。生成的类名称会在源目的地类名称的基础上添加“Directions”。例如,如果源目的地的名称为 `SpecifyAmountFragment`,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + +生成的类为源目的地中定义的每个操作提供了一个静态方法。该方法接受任何定义的[操作参数](https://developer.android.com/guide/navigation/navigation-pass-data)为参数,并返回可直接传递到 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController.html?skip_cache=true#navigate(androidx.navigation.NavDirections)) 的 [`NavDirections`](https://developer.android.com/reference/androidx/navigation/NavDirections.html?skip_cache=true) 对象。 + + + +### Safe Args 示例 + +例如,假设我们的导航图包含一个操作,该操作将两个目的地 `SpecifyAmountFragment` 和 `ConfirmationFragment` 连接起来。`ConfirmationFragment` 接受您作为操作的一部分提供的单个 `float` 参数。 + +Safe Args 会生成一个 `SpecifyAmountFragmentDirections` 类,其中只包含一个 `actionSpecifyAmountFragmentToConfirmationFragment()` 方法和一个名为 `ActionSpecifyAmountFragmentToConfirmationFragment` 的内部类。这个内部类派生自 `NavDirections` 并存储了关联的操作 ID 和 `float` 参数。然后,您可以将返回的 `NavDirections` 对象直接传递到 `navigate()`,如下例所示: + +```kotlin +override fun onClick(v: View) { + val amount: Float = ... + val action = + SpecifyAmountFragmentDirections + .actionSpecifyAmountFragmentToConfirmationFragment(amount) + v.findNavController().navigate(action) +} +``` + +## 传递参数 + +Navigation 支持您通过定义目的地参数将数据附加到导航操作。例如,用户个人资料目的地可能会根据用户 ID 参数来确定要显示哪个用户。 + +通常情况下,强烈建议您仅在目的地之间传递最少量的数据。例如,您应该传递键来检索对象而不是传递对象本身,因为在 Android 上用于保存所有状态的总空间是有限的。如果您需要传递大量数据,不妨考虑使用 [`ViewModel`](https://developer.android.com/reference/androidx/lifecycle/ViewModel)(如[在 Fragment 之间共享数据](https://developer.android.com/topic/libraries/architecture/viewmodel#sharing)中所述)。 + +```xml + + + +``` + +通过声明argement节点来指定参数。 + +启用 Safe Args 后,生成的代码会为每个操作包含以下类型安全的类和方法,以及每个发送和接收目的地。 + +- 为生成操作的每一个目的地创建一个类。该类的名称是在源目的地的名称后面加上“Directions”。例如,如果源目的地是名为 `SpecifyAmountFragment` 的 Fragment,则生成的类的名称为 `SpecifyAmountFragmentDirections`。 + + 该类会为源目的地中定义的每个操作提供一个方法。 + +- 对于用于传递参数的每个操作,都会创建一个 inner 类,该类的名称根据操作的名称确定。例如,如果操作名称为 `confirmationAction,`,则类名称为 `ConfirmationAction`。如果您的操作包含不带 `defaultValue` 的参数,则您可以使用关联的 action 类来设置参数值。 + +- 为接收目的地创建一个类。该类的名称是在目的地的名称后面加上“Args”。例如,如果目的地 Fragment 的名称为 `ConfirmationFragment,`,则生成的类的名称为 `ConfirmationFragmentArgs`。可以使用该类的 `fromBundle()` 方法检索参数。 + +``` +override fun onClick(v: View) { val amountTv: EditText = view!!.findViewById(R.id.editTextAmount) val amount = amountTv.text.toString().toInt() val action = SpecifyAmountFragmentDirections.confirmationAction(amount) v.findNavController().navigate(action)} +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 bundle 并使用其内容。使用 `-ktx` 依赖项时,Kotlin 用户还可以使用 `by navArgs()` 属性委托来访问参数。 + +```kotlin +val args: ConfirmationFragmentArgs by navArgs() + +override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val tv: TextView = view.findViewById(R.id.textViewAmount) + val amount = args.amount + tv.text = amount.toString() +} +``` + +## 使用 Bundle 对象在目的地之间传递参数 + +如果您不使用 Gradle,仍然可以使用 `Bundle` 对象在目的地之间传递参数。创建 `Bundle` 对象并使用 [`navigate()`](https://developer.android.com/reference/androidx/navigation/NavController#navigate(int)) 将它传递给目的地,如下所示: + +``` +val bundle = bundleOf("amount" to amount)view.findNavController().navigate(R.id.confirmationAction, bundle) +``` + +在接收目的地的代码中,请使用 [`getArguments()`](https://developer.android.com/reference/androidx/fragment/app/Fragment#getArguments()) 方法来检索 `Bundle` 并使用其内容: + +```kotlin +val tv = view.findViewById(R.id.textViewAmount) +tv.text = arguments?.getString("amount") +``` + + + +## NavigationUI + +导航图是Navigation组件中很重要的一部分,它可以帮助我们快速了解页面之间的关系,再通过NavController便可以完成页面的切换工作。而在页面的切换过程中,通常还伴随着App bar中menu菜单的变化。对于不同的页面,App bar中的menu菜单很可能是不一样的。App bar中的各种按钮和菜单,同样承担着页面切换的工作。例如,当ActionBar左边的返回按钮被单击时,我们需要响应该事件,返回到上一个页面。既然Navigation和App bar都需要处理页面切换事件,那么,为了方便管理,Jetpack引入了NavigationUI组件,使App bar中的按钮和菜单能够与导航图中的页面关联起来。 + +`NavigationUI` 支持以下顶部应用栏类型: + +- [`Toolbar`](https://developer.android.com/reference/android/widget/Toolbar) +- [`CollapsingToolbarLayout`](https://developer.android.com/reference/com/google/android/material/appbar/CollapsingToolbarLayout) +- [`ActionBar`](https://developer.android.com/reference/androidx/appcompat/app/ActionBar) + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + ... + + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment + val navController = navHostFragment.navController + val appBarConfiguration = AppBarConfiguration( + topLevelDestinationIds = setOf(), + fallbackOnNavigateUpListener = ::onSupportNavigateUp + ) + findViewById(R.id.toolbar) + .setupWithNavController(navController, appBarConfiguration) +} + +``` + + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git a/Jetpack/architecture/13.Jetpack MVVM.md b/Jetpack/architecture/13.Jetpack MVVM.md new file mode 100644 index 00000000..0ed02013 --- /dev/null +++ b/Jetpack/architecture/13.Jetpack MVVM.md @@ -0,0 +1,47 @@ +# 13.Jetpack MVVM + +项目地址:[android-architecture](https://github.com/googlesamples/android-architecture) +`Google`将该项目命名为`Android`的架构蓝图,我想从名字上已可以看穿一切。 + +在它的官方介绍中是这样说的: + +> The Android framework offers a lot of flexibility when it comes to defining how to organize and architect an Android app. This freedom, whilst very valuable, can also result in apps with large classes, inconsistent naming and architectures (or lack of) that can make testing, maintaining and extending difficult. + +> Android Architecture Blueprints is meant to demonstrate possible ways to help with these common problems. In this project we offer the same application implemented using different architectural concepts and tools. + +> You can use these samples as a reference or as a starting point for creating your own apps. The focus here is on code structure, architecture, testing and maintainability. However, bear in mind that there are many ways to build apps with these architectures and tools, depending on your priorities, so these shouldn't be considered canonical examples. The UI is deliberately kept simple. + +Jetpack MVVM 是 MVVM 模式在 Android 开发中的一个具体实现,是 Android中 Google 官方提供并推荐的 MVVM实现方式。 +不仅通过数据驱动完成彻底解耦,还兼顾了 Android 页面开发中其他不可预期的错误,例如Lifecycle 能在妥善处理 页面生命周期 避免view空指针问题,ViewModel使得UI发生重建时 无需重新向后台请求数据,节省了开销,让视图重建时更快展示数据。 +首先,请查看下图,该图显示了所有模块应如何彼此交互: + +各模块对应MVVM架构: + +View层:Activity/Fragment +ViewModel层:Jetpack ViewModel + Jetpack LivaData +Model层:Repository仓库,包含 本地持久性数据 和 服务端数据 + +View层 包含了我们平时写的Activity/Fragment/布局文件等与界面相关的东西。 +ViewModel层 用于持有和UI元素相关的数据,以保证这些数据在屏幕旋转时不会丢失,并且还要提供接口给View层调用以及和仓库层进行通信。 +仓库层 要做的主要工作是判断调用方请求的数据应该是从本地数据源中获取还是从网络数据源中获取,并将获取到的数据返回给调用方。本地数据源可以使用数据库、SharedPreferences等持久化技术来实现,而网络数据源则通常使用Retrofit访问服务器提供的Webservice接口来实现。 +另外,图中所有的箭头都是单向的,例如View层指向了ViewModel层,表示View层会持有ViewModel层的引用,但是反过来ViewModel层却不能持有View层的引用。除此之外,引用也不能跨层持有,比如View层不能持有仓库层的引用,谨记每一层的组件都只能与它相邻层的组件进行交互。 +这种设计打造了一致且愉快的用户体验。无论用户上次使用应用是在几分钟前还是几天之前,现在回到应用时都会立即看到应用在本地保留的数据。如果此数据已过期,则应用的Repository将开始在后台更新数据。 + +有人可能会有疑惑:怎么完全没有提 DataBinding、双向绑定? +实际上,这也是我之前的疑惑。 没有提 是因为: + +我不想让读者 一提到 MVVM 就和DataBinding联系起来 +我想让读者 抓住 MVVM 数据驱动 的本质。 +而DataBinding提供的双向绑定,是用来完善Jetpack MVVM 的工具,其本身在业界又非常具有争议性。 +掌握本篇内容,已经是Google推荐的开发架构,就已经实现 MVVM 模式。在Google官方的 应用架构指南 中 也同样丝毫没有提到 DataBinding。 + + +## 参考 +- [“终于懂了“系列:Jetpack AAC完整解析(四)MVVM - Android架构探索!](https://juejin.cn/post/6921321173661777933) + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! ` \ No newline at end of file diff --git "a/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" new file mode 100644 index 00000000..0fc2bd0b --- /dev/null +++ "b/Jetpack/architecture/14.findViewById\347\232\204\350\277\207\345\216\273\345\217\212\346\234\252\346\235\245.md" @@ -0,0 +1,22 @@ +# 14.findViewById的过去及未来 + +We have lots of alternatives for this, and you may wonder why do we need another solution. Let’s compare the different solutions based on these criteria: null-safety, compile-time safety, and speed. + +| Column 1 | **[ButterKnife](https://github.com/JakeWharton/butterknife)** | [**Kotlin Synthetics**](https://developer.android.com/kotlin/ktx) | [**Data Binding**](https://developer.android.com/topic/libraries/data-binding) | [**findViewById**](https://developer.android.com/reference/android/app/Activity#findViewById(int)) | [View Binding](https://developer.android.com/topic/libraries/view-binding) | +| --------------------- | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | +| **Fast** | ❌ * | ✅ | ❌ * | ✅ | ✅ | +| **Null-safe** | ❌ | ❌ | ✅ | ❌ | ✅ | +| **Compile-time safe** | ❌ | ❌ | ✅ | ✅ ** | ✅ | + +\* ButterKnife and Data Binding solutions are slower because they use an annotation-based approach + ** `findViewById()` is compile-time safe since API 26 because we don’t need to cast the type of view anymore. + +https://juejin.cn/post/6905942568467759111 + +https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527 + + + +## 参考 +- [Kotlin 插件的落幕,ViewBinding 的崛起](https://juejin.cn/post/6905942568467759111) +- [How Android Access View Item: The Past to the Future](https://medium.com/mobile-app-development-publication/how-android-access-view-item-the-past-to-the-future-bb003ae84527) \ No newline at end of file diff --git "a/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" new file mode 100644 index 00000000..43f23558 --- /dev/null +++ "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" @@ -0,0 +1,400 @@ +# 2.ViewBinding简介 + +ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工具,在Android Studio 3.6中添加的一个新功能,更准确的说,它是DataBinding的一个更轻量变体,为什么要使用View Binding呢?答案是性能。许多开发者使用Data Binding库来引用Layout XML中的视图,而忽略它的其他强大功能。相比来说,自动生成代码ViewBinding其实比DataBinding性能更好。但是传统的方式使用View Binding却不是很好,因为会有很多样板代码(垃圾代码)。 + +通过ViewBinding,你可以更轻松的编写可与视图交互的代码。在模块中启用视图绑定之后,系统会为该模块中的每个XML布局文件生成一个绑定类。绑定类的实例包含对在相应布局中具有ID的所有视图的直接引用。在大多数情况下,视图绑定会替代findViewById。 + +## 使用方法 + +### 1.build.gradle中开启 +在build.gradle文件中的androidj节点添加如下代码: +``` +android { + ... + buildFeatures { + viewBinding true + } +} +``` +重新编译后系统会为每个布局文件生成对应的Binding类,该类中包含对应布局中具有id的所有视图的直接饮用。生成类的目录在app/build/generated/data_binding_base_class_source_out中。 +如果项目中存在多个模块,则需要在每个模块的build.gradle文件中都加上该配置。 +假设某个布局文件的名称为result_profile.xml: + +```xml + + + +