From 491fdcc3544638e4437692bb351503e9bd225142 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 14 Dec 2022 20:13:16 +0800 Subject: [PATCH 001/128] add notes --- ...66\346\236\204\347\256\200\344\273\213.md" | 73 +++- "Architect/2.UML\347\256\200\344\273\213.md" | 17 + JavaKnowledge/.DS_Store | Bin 0 -> 6148 bytes ...70\350\247\201\347\256\227\346\263\225.md" | 117 ----- ...60\346\215\256\347\273\223\346\236\204.md" | 200 +++++++-- .../\347\256\227\346\263\225.md" | 320 ++++++++++++++ ...04\345\244\215\346\235\202\345\272\246.md" | 179 -------- ...73\347\273\237\347\256\200\344\273\213.md" | 27 +- ...13\344\270\216\347\272\277\347\250\213.md" | 23 +- ...05\345\255\230\347\256\241\347\220\206.md" | 30 +- README.md | 4 +- ...06\351\242\221\345\234\272\346\231\257.md" | 98 +++++ .../FFmpeg/FFmpeg\345\210\207\347\211\207.md" | 12 + ...50\345\221\275\344\273\244\350\241\214.md" | 413 +++++++++++++----- .../FFmpeg/FFmpeg\347\256\200\344\273\213.md" | 46 +- .../FLV.md" | 56 ++- .../M3U8.md" | 84 ++++ ...74\345\274\217\350\257\246\350\247\243.md" | 7 +- 18 files changed, 1211 insertions(+), 495 deletions(-) create mode 100644 JavaKnowledge/.DS_Store delete mode 100644 "JavaKnowledge/\345\270\270\350\247\201\347\256\227\346\263\225.md" rename "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204.md" => "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" (55%) create mode 100644 "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" delete mode 100644 "JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\351\237\263\350\247\206\351\242\221\345\234\272\346\231\257.md" create mode 100644 "VideoDevelopment/FFmpeg/FFmpeg\345\210\207\347\211\207.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/M3U8.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" index fe4d4cdc..6fa7327a 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" @@ -1,6 +1,46 @@ 1.系统架构 === +#### 什么是系统架构 + +关于系统架构,维基百科给出了一个非常好的定义。 +A system architecture is the conceptual model that defines the structure, behavior, and more views of a system.[ +(系统架构是概念模型,定义了系统的结构、行为和更多的视图) + +* 系统架构是一个概念模型。 +* 系统架构定义了系统的结构、行为以及更多的视图。 +关于这个定义,这里给出了另外一种解读,供大家参考。 +* 静。首先,从静止的角度,描述系统如何组成,以及系统的功能在这些组成部分之间是如何划分的。这就是系统的“结构”。一般要描述的是:系统包含哪些子系统,每个子系统有什么功能。在做这些描述时,应感觉自己是一名导游,带着游客在系统的子系统间参观。 +* 动。然后,从动态的角度,描述各子系统之间是如何联动的,它们是如何相互配合完成系统预定的任务或流程的。这就是系统的“行为”。在做这个描述时,应感觉自己是一名电影导演,将系统的各种运行情况通过一个个短片展现出来。 +* 细。最后,在以上两种描述的基础上,从不通的角度,更详细的刻画出系统的细节和全貌。这就是“更多的视图”。 + + + + +好代码的特性: +1. 鲁棒(Solid and Robust) +2. 高效(Fast) +3. 简洁(Maintainable and Simple) +4. 简短(Small) +5. 可测试(Testable) +6. 共享(Re-Usable) +7. 可移植(Portable) +8. 可观测(Observvable)/可监控(Monitorable) +9. 可运维(Operational): 可运维重点关注成本、效率和稳定性三个方面 +10. 可扩展(Scalable and Extensible) + + +工程能力的定义: +使用系统化的方法,在保证质量的前提下,更高效率的为客户/用户持续交付有价值的软件或服务的能力。 + +在《软件开发的201个原则》一书中,将“质量第一”列为全书的第一个原则,可见其重要性。 +Edward Yourdon建议,当你被要求加快测试、忽视剩余的少量Bug、在设计或需求达成一致前就开始编码时,要直接说“不”。 +开发前期的设计文档、技术评审3天以上100%。代码规范,缺乏认真的代码评审。 +降低质量要求,事实上不会降低研发成本,反而会增加整体的研发成本。在研发阶段通过降低质量所“节省”的研发成本,会在软件维护阶段加倍偿还。 + +在研发前期(需求分析和系统设计)多投入资源,相对于把资源都投入在研发后期(编码、测试等),其收益更大。 + + ### 架构三要素 #### 构件 @@ -27,9 +67,40 @@ 系统架构虽然是软件系统的结构,行为,属性的高级抽象,但其根本就是在需求分析的基础行为下,制定技术框架,对需求的技术实现。 +### 系统设计的原则和方法: + +* 单一目的 +* 对外关系清晰 +* 重视资源约束 +* 根据需求做决策 +* 基于模型思考 + + +#### 单一职责原则 + + + +#### 开放-封闭原则 +无论模块是多么的‘封闭’,都会存在一些无法对之封闭的变化。既然不可能完全封闭,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出最有可能发生的变化种类,然后构造抽象来隔离那些变化 + +开放-封闭原则是面向对象设计的核心所在。遵循这个原则可以带来面向对象技术所声称的巨大好处,也就是可维护、可扩展、可复用、灵活性好。开发人员应该仅对程序中呈现出频繁变化的那些部分做出抽象,然而,对于应用程序中的每个部分都刻意地进行抽象同样不是一个好主意。拒绝不成熟的抽象和抽象本身一样重要 + + +#### 依赖倒转原则 + + + +#### 迪米特法则 +迪米特法则首先强调的前提是在类的结构设计上,每一个类都应当尽量降低成员的访问权限,也就是说,一个类包装好自己的private状态,不需要让别的类知道的字段或行为就不要公开 + +我们在程序设计时,类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成波及。也就是说,信息的隐藏促进了软件的复用。” + + + + + -​ --- - 邮箱 :charon.chui@gmail.com - Good Luck! diff --git "a/Architect/2.UML\347\256\200\344\273\213.md" "b/Architect/2.UML\347\256\200\344\273\213.md" index c0259a46..2bcf58de 100644 --- "a/Architect/2.UML\347\256\200\344\273\213.md" +++ "b/Architect/2.UML\347\256\200\344\273\213.md" @@ -2,6 +2,23 @@ 推荐使用[Visual Paradigm](https://www.visual-paradigm.com/cn/),如果是非商业用途可以下载社区版,免费使用 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/uml_demo.png?raw=true) + + +类图分三层,第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法或行为。注意前面的符号,‘+’表示public,‘-’表示private,‘#’表示protected。” + +继承关系用空心三角形+实线来表示。 +接口用空心三角形+虚线来表示。 +关联关系用实线箭头来表示。 +聚合表示一种弱的‘拥有’关系,体现的是A对象可以包含B对象,但B对象不是A对象的一部分。聚合关系用空心的菱形+实线箭头来表示。 +合成(Composition,也有翻译成‘组合’的)是一种强的‘拥有’关系,体现了严格的部分和整体的关系,部分和整体的生命周期一样.合成关系用实心的菱形+实线箭头来表示 +依赖关系(Dependency),用虚线箭头来表示。 + + + + ## 类图 ## 时序图 diff --git a/JavaKnowledge/.DS_Store b/JavaKnowledge/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..bf3070814d6f3de67d704daffef65ea47ad99cae GIT binary patch literal 6148 zcmeHK%}N6?5Kh`^Q;N`og5CmN3%33uUe>n0K)2{YrFPv?7q^?GKNcyKJ$SQsU%;2} z?i+|6eHJG_6}1(;C@M2B`6iQ@Wb^Hk><~g|q3h-e$q+&qsKi(bnh7GurH)C=c%}iV zoTD=AU;7VEuN^x6STs6*BLni?CF|jFaB<(beEt4}10%Oi=zHx>F84@d@x;`0k|kN1 z9oi?NXLo9yZo5!x?aN+82;UjmHD}kW_o|uMq6j)QFQ~Uvg6GyDLCwn4R47!f4)b#rPN=03TVwzkc*{Us zcQf+--}$)yf189oWB?iXR}9cZ$u1ROOXhAJ+nl^>Ip`6n6d4z3d`SUAUBwV9ui|-7 aCE(}M0JIE78o>jCKLUydY> The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack for an item and discover how far it is from the top. When a stack is first created, it contains no items. @@ -55,72 +152,57 @@ When a stack is first created, it contains no items. - 存取其他项很慢 -队列`(Queue)` ---- - +### 链栈 -队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 +栈的顺序存储结构,我们现在来看看栈的链式存储结构,简称为链栈。 +对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是否溢出的问题。 +对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。 -优点: +#### 栈的应用-递归 -- 提供了先进先出的存取方式 -缺点: +把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。 +当然,写递归程序最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。 -- 存取其他项很慢 +那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 +### 队列`(Queue)` +队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。 +队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。 +队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 +优点: -链表`(LinkedList)` ---- +- 提供了先进先出的存取方式 -链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 -链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 -每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,链表比较方便插入和删除操作。 +缺点: -用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象)+指针(指示后继元素存储位置) = 结点。 -以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第`i`个数据元素,必须先找到第`i-1`个数据元素。 +- 存取其他项很慢 -链表的结点结构: -``` - ┌──┬──┐──┐ - │data│next│ - └──┴──┘──┘ -``` -`data`域:存放结点值的数据域    -`next`域:存放结点的直接后继的地址(位置)的指针域(链域)。 +### 链队列 +队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。 -注意: -- 链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。    -- 每个结点只有一个链域的链表称为单链表`(Single Linked List)` +### 串 +串(string)是由零个或多个字符组成的有限序列,又名叫字符串。 -所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 -优点: - -- 和数组相比,链表的优势在于长度不受限制,也不需要连续的内存空间。 -- 在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组想同,实际效率上还是比数组要高很多,所以插入快,删除快 -缺点: - -- 劣势在于随机访问,无法像数组那样直接通过下标找到特定的数据项 -- 查找慢 -- 相对数组只存储元素,链表的元素还要存储其他元素地址,内存开销相对增大 +### 树`(Tree)` -树`(Tree)` ---- +树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 树是由`n(n>=1)`个有限节点组成一个具有层次关系的集合。 它具有以下特点:每个节点有零个或多个子节点;没有父节点的节点称为根节点;每一个非根节点有且只有一个父节点;除了根节点外,每个子节点可以分为多个不相交的子树。 +树中结点的最大层次称为树的深度(Depth)。如果将树中结点的各子树看成从左至右是有次序的,不能互换的,则称该树为有序树,否则称为无序树。 #### 二叉树 @@ -131,6 +213,25 @@ When a stack is first created, it contains no items. ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/binary_tree.jpg?raw=true) +二叉树每个结点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_ercha_list.png?raw=true) +其中data是数据域,lchild和rchild都是指针域,分别存放只想左孩子和右孩子的指针。 +如果有需要,还可以再增加一个指向其双亲的指针域,那样就称之为三叉链表 + +二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。 +##### 二叉树遍历方法 +- 前序遍历 + 规则是若二叉树为空,则空操作返回,否则先访问根结点,然后前序遍历左子树,再前序遍历右子树。 +- 中序遍历 + 规则是若树为空,则空操作返回,否则从根结点开始(注意并不是先访问根结点),中序遍历根结点的左子树,然后是访问根结点,最后中序遍历右子树。 +- 后序遍历 + 规则是若树为空,则空操作返回,否则从左到右先叶子后结点的方式遍历访问左右子树,最后是访问根结点。 +- 层序遍历 + 规则是若树为空,则空操作返回,否则从树的第一层,也就是根结点开始访问,从上而下逐层遍历,在同一层中,按从左到右的顺序对结点逐个访问。 + + + #### 满二叉树和完全二叉树 满二叉树:除最后一层无任何子节点外,每一层上的所有结点都有两个子结点。也可以这样理解,除叶子结点外的所有结点均有两个子结点。节点数达到最大值,所有叶子结点必须在同一层上。 @@ -227,6 +328,19 @@ private boolean isRed(Node x){ } ``` + + + + + + + + + + + + + 哈希表`(Hash)` --- @@ -274,6 +388,8 @@ private boolean isRed(Node x){ 图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、化学、电讯工程、计算机科学以及数学的其他分支中。 +图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 + 优点: diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" new file mode 100644 index 00000000..b57a2d11 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -0,0 +1,320 @@ +算法 +=== + +算法(Algorithm)是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。 + + +算法具有五个基本特性: +- 输入 + 算法具有零个或多个输入 +- 输出 + 算法至少有一个或多个输出 +- 有穷性 + 指算法在执行有限的步骤之后,自动结束而不会出现无限循环,并且每一个步骤在可接受的时间内完成。 +- 确定性 + 算法的每一步骤都具有确定的含义,不会出现二义性 +- 可行性 + 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 + + +### 算法时间复杂度 +在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。 + +这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。 +一般情况下,随着n的增大,T(n)增长最慢的算法为最优算法。 +显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n²)。 +我们分别给它们取了非官方的名称,O(1)叫常数阶、O(n)叫线性阶、O(n²)叫平方阶。 + + + +#### 推导大O阶方法 + +推导大O阶: +1. 用常数1取代运行时间中的所有加法常数。 +2. 在修改后的运行次数函数中,只保留最高阶项。 +3. 如果最高阶项存在且不是1,则去除与这个项相乘的常数。 +得到的结果就是大O阶。 + +##### 常数阶 + +顺序结构的时间复杂度。 + +下面这个算法,也就是刚才的第二种算法(高斯算法),为什么时间复杂度不是O(3),而是O(1)。 +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +这个算法的运行次数函数是f(n)=3。 +根据我们推导大O阶的方法,第一步就是把常数项3改为1。 +在保留最高阶项时发现,它根本没有最高阶项,所以这个算法的时间复杂度为O(1)。 +那假设是这样: + +``` +int sum = 0, n = 100; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +sum = (1 + n) * n / 2; // 执行一次 +printf("%d", sum); // 执行一次 +``` +事实上无论n为多少,上面的两段代码就是3次和12次执行的差异。这种与问题的大小无关(n的多少),执行时间恒定的算法,我们称之为具有O(1)的时间复杂度,又叫常数阶。注意:不管这个常数是多少,我们都记作O(1),而不能是O(3)、O(12)等其他任何数字,这是初学者常常犯的错误。 + + +##### 线性阶 + +循环结构的时间复杂度。 + +下面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码须要执行n次。 +```java +int i; +for (i = 0; i < n; i++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} + +``` +##### 对数阶 +下面的这段代码,时间复杂度是多少呢? +```java +int count = 1; +while (count < n) { + count = count * 2; + // 下面省略时间复杂度为O(1)的程序步骤 + ... +} +``` +由于每次count乘以2之后,就距离n更近了一分。 +也就是说,有多少个2相乘后大于n,则会退出循环。 +由2^x = n得到 x = ㏒(2)N。所以这个循环的时间复杂度为O(㏒n) + + +##### 平方阶 + +下面的例子是一个循环嵌套,它的内循环上面已经分析过,时间复杂度为O(n)。 +```java +int i, j; +for (i = 0; i < n; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` + +而对于外层的循环,不过是内部这个时间复杂度为O(n)的语句,再循环n次。所以这段代码的时间复杂度为O(n2)。 + +如果外循环的循环次数改为了m,时间复杂度就变为O(m×n),如下: +```java +int i, j; +for (i = 0; i < m; i ++ ) { + for (j = 0; j < n; j ++) { + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +所以我们可以总结得出,循环的时间复杂度等于循环体的复杂度乘以该循环运行的次数。那么下面这个循环嵌套,它的时间复杂度是多少呢? +```java +int i, j; +for(i = 0; i < n; i ++) { + for (j = i; j < n; j ++) { // 注意j = i 而不是0 + // 下面省略时间复杂度为O(1)的程序步骤 + ... + } +} +``` +由于当i=0时,内循环执行了n次,当i=1时,执行了n-1次,……当i=n-1时,执行了1次。所以总的执行次数为: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_1.png?raw=true) +用我们推导大O阶的方法: +- 第一条,没有加法常数不予考虑 +- 第二条,只保留最高阶项,因此保留n2/2 +- 第三条,去除这个项相乘的常数,也就是去除1/2 +最终这段代码的时间复杂度为O(n2)。 + +##### 常见的时间复杂度 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/alg_2.png?raw=true) + + + +### 算法空间复杂度 + +我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。还有另一个办法就是,事先建立一个有2 050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。 + +算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。 + +一般情况下,一个程序在机器上执行时,除了需要存储程序本身的指令、常数、变量和输入数据外,还需要存储对数据操作的存储单元。若输入数据所占空间只取决于问题本身,和算法无关,这样只需要分析该算法在实现时所需的辅助单元即可。若算法执行时所需的辅助空间相对于输入数据量而言是个常数,则称此算法为原地工作,空间复杂度为O(1)。 + + +算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`S(n)`定义为该算法所耗费空间的数量级。 +`S(n)=O(f(n))`若算法执行时所需要的辅助空间相对于输入数据量`n`而言是一个常数,则称这个算法的辅助空间为`O(1)`; +递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间, 如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. + +空间复杂度的分析方法: + +- 一个算法的空间复杂度`S(n)`定义为该算法所耗费的存储空间,它也是问题规模`n`的函数。空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度。 +- 一个算法在计算机存储器上所占用的存储空间,包括存储算法本身所占用的存储空间,算法的输入输出数据所占用的存储空间和算法在运行过程中临时占用的存储空间这三个方面。 +- 一个算法的空间复杂度只考虑在运行过程中为局部变量分配的存储空间的大小,它包括为参数表中形参变量分配的存储空间和为在函数体中定义的局部变量分配的存储空间两个部分。 + +  算法的空间复杂度一般也以数量级的形式给出。如当一个算法的空间复杂度为一个常量,即不随被处理数据量`n`的大小而改变时,可表示为`O(1)`; +  当一个算法的空间复杂度与以2为底的`n`的对数成正比时,可表示为`O(log2n)`; +  当一个算法的空间复杂度与`n`成线性比例关系时,可表示为`O(n)`。若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。 + +空间复杂度补充: + +一个程序的空间复杂度是指运行完一个程序所需内存的大小。利用程序的空间复杂度,可以对程序的运行所需要的内存多少有个预先估计。 +一个程序执行时除了需要存储空间和存储本身所使用的指令、常数、变量和输入数据外,还需要一些对数据进行操作的工作单元和存储一些为现实计算所需信息的辅助空间。 +程序执行时所需存储空间包括以下两部分:    + +- 固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。 +- 可变空间,这部分空间的主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。一个算法所需的存储空间用`f(n)`表示。`S(n)=O(f(n))`其中`n`为问题的规模,`S(n)`表示空间复杂度。 + + +时间与空间复杂度比较 +--- + +对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差, +即可能导致占用较多的存储空间;反之,当追求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。 +另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。 + + +通常,我们都使用“时间复杂度”来指运行时间的需求,使用“空间复杂度”指空间需求。 + +当不用限定词地使用“复杂度”时,通常都是指时间复杂度。 + + + + +## 顺序查找算法 + +顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。 + + + + + + +算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, +执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 +算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 +```java +/** + * 1到100所有素数的和 + * 素数就是只能够被1和自身整除的数 + */ +public class dd { + public static void main(String[] args) { + int i = 2; // i 即为所求素数 + System.out.println("i= " + i); + for (i = 3; i <= 100; i = i + 2) { + boolean f = true; + Label: for (int j = 2; j < i; j++) { + if (i % j == 0) { // if(true)时,i为非素数 + f = false; + break Label; // 加了Label貌似只是起到提高效率 + } + } + if (f) {// 当f=true时,i为素数。 + System.out.println("i= " + i); + } + } + } +} + +/** + *古典问题:有一对兔子,从出生后第3个月起每个月都生一对兔子,小兔子长到第四个月后每个月又生一对兔子,假如兔子都不死, + *问每个月的兔子总数为多少? + * + */ +public class aa{ + public static void main(String[] args) { + int i = 0; + for (i = 1; i < 20; i++) { + System.out.println(f(i)); + } + } + + public static int f(int x) { + if(x==1||x==2) { + return 1; + }else { + return f(x-1)+f(x-2); + } + } +} +``` + +3.有两个有序数组`a`,`b`,现需要将其合并成一个新的有序数组。 + +简单的思路就是先放到一个新的数组中,再排序。但是这样的没体现任何算法,这里考的不是快速排序等排序算法。关键应该是如何利用`有序` 已知这个条件。可以这样想,假设两个源数组的长度不一样,那么假设其中短的数组用完了,即全部放入到新数组中去了,那么长数组中剩下的那一段就可以直接拿来放入到新数组中去了。 + +其中用到的思想是:归并排序思想 + +```java +public static int[] merge(int[] a, int[] b) { + int lengthA = a.length; + int lengthB = b.length; + int[] result = new int[lengthA + lengthB]; + + //aIndex:用于标示a数组 bIndex:用来标示b数组 resultIndex:用来标示传入的数组 + int aIndex = 0, bIndex = 0, resultIndex = 0; + + while (aIndex < lengthA && bIndex < lengthB) { + if (a[aIndex] < b[bIndex]) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } else { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + } + + // a有剩余 + while (aIndex < lengthA) { + result[resultIndex++] = a[aIndex]; + aIndex++; + } + + // b有剩余 + while (bIndex < lengthB) { + result[resultIndex++] = b[bIndex]; + bIndex++; + } + return result; +} +```` + + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" "b/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" deleted file mode 100644 index 341bdff2..00000000 --- "a/JavaKnowledge/\347\256\227\346\263\225\347\232\204\345\244\215\346\235\202\345\272\246.md" +++ /dev/null @@ -1,179 +0,0 @@ -算法的复杂度 -=== - - -何为算法 ---- - -算法是对特定问题求解步骤的一种描述,是独立存在的一种解决问题的方法和思想。它是指令的有限序列,其中每一条指令表示一个或多个操作; - -此外,成为一个算法需要满足以下条件或特性: - -- 有穷性。一个算法必须总是在执行有穷步之后结束,且每一步都可在有穷时间内完成。 -- 确定性。算法中每一条指令必须有确切的含义读者理解时不会产生二义性。并且,在任何条件下,算法只有唯一的一条执行路径,即对于相同的输入只能得出相同的输出。 -- 可行性。一个算法是能行的,即算法中描述的操作都是可以通过已经实现的基本运算执行有限次来实现的。 -- 输入。零个或多个的输入。 -- 输出。一个或多个的输出。 - - -算法的设计要求 ---- - -通常设计一个好的算法应考虑达到以下目标: - -- 正确性:对于合法输入能够得到满足的结果;算法能够处理非法处理,并得到合理结果;算法对于边界数据和压力数据都能得到满足的结果。 -- 可读性。算法要方便阅读,理解和交流,只有自己能看得懂,其它人都看不懂,谈和好算法。 -- 健壮性。算法不应该产生莫名其妙的结果,一会儿正确,一会儿又是其它结果。 -- 高性价比,效率与低存储量需求。利用最少的时间和资源得到满足要求的结果,可以通过(时间复杂度和空间复杂度来判定) - - - -算法复杂度分为: - -- 时间复杂度:指执行算法所需要的计算工作量 -- 空间复杂度:指执行这个算法所需要的内存空间 - -简单来说,时间复杂度指的是语句执行次数,空间复杂度指的是算法所占的存储空间。 - - -时间复杂度 ---- - -什么是时间复杂度,算法中某个函数有n次基本操作重复执行,用T(n)表示,现在有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n)) 为算法的渐进时间复杂度,简称时间复杂度。通俗一点讲,其实所谓的时间复杂度,就是找了一个同样曲线类型的函数f(n)来表示这个算法的在n不断变大时的趋势 。当输入量n逐渐加大时,时间复杂性的极限情形称为算法的“渐近时间复杂性”。 - -- 时间频度 - - 一个算法执行所耗费的时间,从理论上是不能算出来的,必须上机运行测试才能知道。但我们不可能也没有必要对每个算法都上机测试,只需知道哪个算法花费的时间多,哪个算法花费的时间少就可以了。并且一个算法花费的时间与算法中语句的执行次数成正比例,哪个算法中语句执行次数多,它花费时间就多。一个算法中的语句执行次数称为语句频度或时间频度。记为`T(n)`。(算法中的基本操作一般指算法中最深层循环内的语句) - -- 时间复杂度 - - 在刚才提到的时间频度中,`n`称为问题的规模,当`n`不断变化时,时间频度`T(n)`也会不断变化。但有时我们想知道它变化时呈现什么规律。为此,我们引入时间复杂度的概念。 - - - -在进行算法分析时,语句总的执行次数`T(n)`是关于问题规模`n`的函数,进而分析`T(n)`随`n`的变化情况并确定`T(n)`的数量级。 -算法的时间复杂度,也就是算法的时间量度,记作`T(n) = O(f(n))`。它表示随问题规模`n`的增大,算法执行时间的增长率和`f(n)`的增长率相同, -称为算法的渐近时间复杂度,简称为时间复杂度。其中`f(n)`是规模`n`的某个函数。 - - -大`O`表示法 ---- - -像前面用`O()`来体现算法时间复杂度的记法,我们称之为大O表示法。用`O()`来体现算法时间复杂度的记法,叫作大`O`记法。 - - -按数量级递增排列,常见的时间复杂度有: -常数阶`O(1)`,对数阶`O(log2n)`(以2为底`n`的对数,下同),线性阶`O(n)`, -线性对数阶`O(nlog2n)`,平方阶`O(n^2)`,立方阶`O(n^3)`,..., -`k`次方阶`O(n^k)`,指数阶`O(2^n)`。随着问题规模`n`的不断增大,上述时间复杂度不断增大,算法的执行效率越低。 - - -##### 推导大`O`阶方法 - -- 用常数1取代运行时间中的所有加法常数。 -- 在修改后的运行次数函数中,只保留最高阶项。 -- 如果最高阶项存在且不是1,则去除与这个项相乘的常数。 - - -##### 常数阶 - -`O(1)`的算法是一些运算次数为常数的算法。例如: -```java -temp=a;a=b;b=temp; -``` - -上面语句共三条操作,单条操作的频度为1,即使他有成千上万条操作,也只是个较大常数,这一类的时间复杂度为`O(1)`。 - -##### 线性阶 - -`O(n)`的算法是一些线性算法。例如: -```java -sum=0; -for(i=0;i Date: Wed, 14 Dec 2022 20:30:13 +0800 Subject: [PATCH 002/128] add notes --- ...\222\345\272\217\347\256\227\346\263\225.md" | 0 README.md | 17 ++++++++++++----- .../Android WebRTC\347\256\200\344\273\213.md" | 0 .../AudioTrack\347\256\200\344\273\213.md" | 0 .../DLNA\347\256\200\344\273\213.md" | 0 ...\200\201MediaCodec\343\200\201MediaMuxer.md" | 0 .../SurfaceView\344\270\216TextureView.md" | 0 ...\247\350\203\275\344\274\230\345\214\226.md" | 0 ...\243\344\270\216\347\241\254\350\247\243.md" | 0 .../FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" | 2 +- ...\250\345\221\275\344\273\244\350\241\214.md" | 2 +- .../FFmpeg/3.FFmpeg\345\210\207\347\211\207.md" | 2 +- 12 files changed, 15 insertions(+), 8 deletions(-) rename "JavaKnowledge/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" => "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" (100%) rename "VideoDevelopment/Android WebRTC\347\256\200\344\273\213.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/Android WebRTC\347\256\200\344\273\213.md" (100%) rename "VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" (100%) rename "VideoDevelopment/DLNA\347\256\200\344\273\213.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/DLNA\347\256\200\344\273\213.md" (100%) rename "VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" (100%) rename "VideoDevelopment/SurfaceView\344\270\216TextureView.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" (100%) rename "VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" (100%) rename "VideoDevelopment/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" => "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" (100%) rename "VideoDevelopment/FFmpeg/FFmpeg\347\256\200\344\273\213.md" => "VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" (99%) rename "VideoDevelopment/FFmpeg/FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" => "VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" (99%) rename "VideoDevelopment/FFmpeg/FFmpeg\345\210\207\347\211\207.md" => "VideoDevelopment/FFmpeg/3.FFmpeg\345\210\207\347\211\207.md" (74%) diff --git "a/JavaKnowledge/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" similarity index 100% rename from "JavaKnowledge/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" rename to "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\205\253\347\247\215\346\216\222\345\272\217\347\256\227\346\263\225.md" diff --git a/README.md b/README.md index f269bd95..fdb60c8b 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Android学习笔记 - [搭建nginx+rtmp服务器][18] - [视频播放相关内容总结][19] - [视频解码之软解与硬解][20] - - [音视频基础知识][21] + - [Android音视频开发][21] - [Android WebRTC简介][22] - [DNS及HTTPDNS][23] - [DLNA简介][24] @@ -70,6 +70,7 @@ Android学习笔记 - [fMP4 vs ts][254] - [fMP4格式详解][255] - [视频封装格式][256] + - [M3U8][321] - [视频编码][257] - [AV1][258] - [H264][259] @@ -96,6 +97,10 @@ Android学习笔记 - [11.OpenGL ES滤镜][242] - [弹幕][243] - [Android弹幕实现][244] + - [FFmpeg][322] + - [1.FFmpeg简介][323] + - [2.FFmpeg常用命令行][324] + - [1.FFmpeg切片][325] - [操作系统][263] - [1.操作系统简介][264] - [2.进程与线程][265] @@ -366,7 +371,7 @@ Android学习笔记 [18]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%90%AD%E5%BB%BAnginx%2Brtmp%E6%9C%8D%E5%8A%A1%E5%99%A8.md "搭建nginx+rtmp服务器" [19]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E7%9B%B8%E5%85%B3%E5%86%85%E5%AE%B9%E6%80%BB%E7%BB%93.md "视频播放相关内容总结" [20]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E8%A7%A3%E7%A0%81%E4%B9%8B%E8%BD%AF%E8%A7%A3%E4%B8%8E%E7%A1%AC%E8%A7%A3.md "视频解码之软解与硬解" -[21]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E8%A7%86%E9%A2%91%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md "音视频基础知识" +[21]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91 "Android音视频开发" [22]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%20WebRTC%E7%AE%80%E4%BB%8B.md "Android WebRTC简介" [23]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/DNS%E5%8F%8AHTTPDNS.md "DNS及HTTPDNS" [24]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/DLNA%E7%AE%80%E4%BB%8B.md "DLNA简介" @@ -432,7 +437,7 @@ Android学习笔记 [84]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/RecyclerView%E4%B8%93%E9%A2%98.md "RecyclerView专题" [85]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E8%A1%8C%E5%A4%A7%E5%85%A8.md "常用命令行大全" [86]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8D%95%E4%BE%8B%E7%9A%84%E6%9C%80%E4%BD%B3%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F.md "单例的最佳实现方式" -[87]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md "数据结构" +[87]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.md "数据结构" [88]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%8E%B7%E5%8F%96%E4%BB%8A%E5%90%8E%E5%A4%9A%E5%B0%91%E5%A4%A9%E5%90%8E%E7%9A%84%E6%97%A5%E6%9C%9F.md "获取今后多少天后的日期" [89]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%89%91%E6%8C%87Offer(%E4%B8%8A).md "剑指Offer(上)" [90]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/剑指Offer(下).md "剑指Offer(下)" @@ -440,7 +445,7 @@ Android学习笔记 [92]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%94%9F%E4%BA%A7%E8%80%85%E6%B6%88%E8%B4%B9%E8%80%85.md "生产者消费者" [93]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E6%95%B0%E6%8D%AE%E5%8A%A0%E5%AF%86%E5%8F%8A%E8%A7%A3%E5%AF%86.md "数据加密及解密" [94]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E6%AD%BB%E9%94%81.md "死锁" -[95]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%B8%B8%E8%A7%81%E7%AE%97%E6%B3%95.md "算法" +[95]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95/%E7%AE%97%E6%B3%95.md "算法" [96]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%BD%91%E7%BB%9C%E8%AF%B7%E6%B1%82%E7%9B%B8%E5%85%B3%E5%86%85%E5%AE%B9%E6%80%BB%E7%BB%93.md "网络请求相关内容总结" [97]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%BA%BF%E7%A8%8B%E6%B1%A0%E7%9A%84%E5%8E%9F%E7%90%86.md "线程池的原理" [98]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java并发编程之原子性、可见性以及有序性.md "Java并发编程之原子性、可见性以及有序性" @@ -539,7 +544,7 @@ Android学习笔记 [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 "设计模式" -[192]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E7%AE%97%E6%B3%95%E7%9A%84%E5%A4%8D%E6%9D%82%E5%BA%A6.md "算法复杂度" +[192]: https://github.com/CharonChui/AndroidNote/tree/master/JavaKnowledge/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95 "数据结构和算法" [193]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86.md "动态代理" [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的区别" @@ -674,6 +679,8 @@ Android学习笔记 [318]: https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/behavior/1.%E7%AE%80%E4%BB%8B.md "1.简介" [319]: https://github.com/CharonChui/AndroidNote/blob/master/Gradle%26Maven/Composing%20builds%E7%AE%80%E4%BB%8B.md "Composing builds简介" [320]: https://github.com/CharonChui/AndroidNote/blob/master/ImageLoaderLibrary/Coil%E7%AE%80%E4%BB%8B.md "Coil简介" +[321]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/M3U8.md "M3U8" + diff --git "a/VideoDevelopment/Android WebRTC\347\256\200\344\273\213.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/Android WebRTC\347\256\200\344\273\213.md" similarity index 100% rename from "VideoDevelopment/Android WebRTC\347\256\200\344\273\213.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/Android WebRTC\347\256\200\344\273\213.md" diff --git "a/VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" similarity index 100% rename from "VideoDevelopment/AudioTrack\347\256\200\344\273\213.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" diff --git "a/VideoDevelopment/DLNA\347\256\200\344\273\213.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/DLNA\347\256\200\344\273\213.md" similarity index 100% rename from "VideoDevelopment/DLNA\347\256\200\344\273\213.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/DLNA\347\256\200\344\273\213.md" diff --git "a/VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" similarity index 100% rename from "VideoDevelopment/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" diff --git "a/VideoDevelopment/SurfaceView\344\270\216TextureView.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" similarity index 100% rename from "VideoDevelopment/SurfaceView\344\270\216TextureView.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" 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/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" similarity index 100% rename from "VideoDevelopment/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" diff --git "a/VideoDevelopment/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" similarity index 100% rename from "VideoDevelopment/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" rename to "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" diff --git "a/VideoDevelopment/FFmpeg/FFmpeg\347\256\200\344\273\213.md" "b/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" similarity index 99% rename from "VideoDevelopment/FFmpeg/FFmpeg\347\256\200\344\273\213.md" rename to "VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" index e70b702b..16e0c81b 100644 --- "a/VideoDevelopment/FFmpeg/FFmpeg\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" @@ -1,4 +1,4 @@ -FFmpeg简介 +1.FFmpeg简介 === FFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPL或GPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多codec都是从头开发的。 diff --git "a/VideoDevelopment/FFmpeg/FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" similarity index 99% rename from "VideoDevelopment/FFmpeg/FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" rename to "VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" index 5452d9a1..4ee12b4e 100644 --- "a/VideoDevelopment/FFmpeg/FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" +++ "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" @@ -1,4 +1,4 @@ -FFmpeg常用命令行 +2.FFmpeg常用命令行 === ### ffmpeg diff --git "a/VideoDevelopment/FFmpeg/FFmpeg\345\210\207\347\211\207.md" "b/VideoDevelopment/FFmpeg/3.FFmpeg\345\210\207\347\211\207.md" similarity index 74% rename from "VideoDevelopment/FFmpeg/FFmpeg\345\210\207\347\211\207.md" rename to "VideoDevelopment/FFmpeg/3.FFmpeg\345\210\207\347\211\207.md" index 6edec0c0..d03ed441 100644 --- "a/VideoDevelopment/FFmpeg/FFmpeg\345\210\207\347\211\207.md" +++ "b/VideoDevelopment/FFmpeg/3.FFmpeg\345\210\207\347\211\207.md" @@ -1,4 +1,4 @@ -FFmpeg常用命令行 +3.FFmpeg切片 === From 82dee50eaa99e9538acd42e83e8b602025f89f03 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 14 Dec 2022 20:46:33 +0800 Subject: [PATCH 003/128] update README.md --- README.md | 46 +++++++++++++++++++++++-------------- VideoDevelopment/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 29 insertions(+), 17 deletions(-) delete mode 100644 VideoDevelopment/.DS_Store diff --git a/README.md b/README.md index fdb60c8b..890fcd12 100644 --- a/README.md +++ b/README.md @@ -45,12 +45,20 @@ Android学习笔记 - [音视频开发][44] - [搭建nginx+rtmp服务器][18] - [视频播放相关内容总结][19] - - [视频解码之软解与硬解][20] - [Android音视频开发][21] - - [Android WebRTC简介][22] + - [Android WebRTC简介][22] + - [DLNA简介][24] + - [AudioTrack简介][214] + - [播放器性能优化][230] + - [MediaExtractor、MediaCodec、MediaMuxer][245] + - [SurfaceView与TextureView][226] + - [视频解码之软解与硬解][20] + - [音视频同步原理][326] + - [音视频场景][327] + - [1.音视频基础知识][328] + - [2.系统播放器MediaPlayer][329] + - [11.播放器组件封装][330] - [DNS及HTTPDNS][23] - - [DLNA简介][24] - - [AudioTrack简介][214] - [流媒体协议][224] - [流媒体协议][246] - [HLS][247] @@ -75,14 +83,11 @@ Android学习笔记 - [AV1][258] - [H264][259] - [H265][260] - - [SurfaceView与TextureView][226] - [关键帧][227] - [CDN及PCDN][228] - [P2P技术][229] - [P2P][261] - [P2P原理_NAT穿透][262] - - [播放器性能优化][230] - - [MediaExtractor、MediaCodec、MediaMuxer][245] - [OpenGL][231] - [1.OpenGL简介][232] - [2.GLSurfaceView简介][233] @@ -370,11 +375,11 @@ Android学习笔记 [17]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Netowork/Retrofit%E8%AF%A6%E8%A7%A3(%E4%B8%8B).md "Retrofit详解(下)" [18]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%90%AD%E5%BB%BAnginx%2Brtmp%E6%9C%8D%E5%8A%A1%E5%99%A8.md "搭建nginx+rtmp服务器" [19]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E7%9B%B8%E5%85%B3%E5%86%85%E5%AE%B9%E6%80%BB%E7%BB%93.md "视频播放相关内容总结" -[20]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E8%A7%A3%E7%A0%81%E4%B9%8B%E8%BD%AF%E8%A7%A3%E4%B8%8E%E7%A1%AC%E8%A7%A3.md "视频解码之软解与硬解" +[20]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/%E8%A7%86%E9%A2%91%E8%A7%A3%E7%A0%81%E4%B9%8B%E8%BD%AF%E8%A7%A3%E4%B8%8E%E7%A1%AC%E8%A7%A3.md "视频解码之软解与硬解" [21]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91 "Android音视频开发" -[22]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%20WebRTC%E7%AE%80%E4%BB%8B.md "Android WebRTC简介" +[22]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/Android%20WebRTC%E7%AE%80%E4%BB%8B.md "Android WebRTC简介" [23]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/DNS%E5%8F%8AHTTPDNS.md "DNS及HTTPDNS" -[24]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/DLNA%E7%AE%80%E4%BB%8B.md "DLNA简介" +[24]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/DLNA%E7%AE%80%E4%BB%8B.md "DLNA简介" [25]: https://github.com/CharonChui/AndroidNote/blob/master/ImageLoaderLibrary/Glide%E7%AE%80%E4%BB%8B(%E4%B8%8A).md "Glide简介(上)" [26]: https://github.com/CharonChui/AndroidNote/blob/master/ImageLoaderLibrary/Glide%E7%AE%80%E4%BB%8B(%E4%B8%8B).md "Glide简介(下)" [27]: https://github.com/CharonChui/AndroidNote/blob/master/ImageLoaderLibrary/%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93%E6%AF%94%E8%BE%83.md "图片加载库比较" @@ -567,7 +572,7 @@ Android学习笔记 [211]: https://github.com/CharonChui/AndroidNote/blob/master/RxJavaPart/6.RxJava%E8%AF%A6%E8%A7%A3%E4%B9%8B%E7%BA%BF%E7%A8%8B%E8%B0%83%E5%BA%A6%E5%8E%9F%E7%90%86(%E5%85%AD).md "6.RxJava详解之线程调度原理(六)" [212]: https://github.com/CharonChui/AndroidNote/blob/master/Dagger2/9.Dagger2%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90(%E4%B9%9D).md "9.Dagger2原理分析(九)" [213]: https://github.com/CharonChui/AndroidNote/blob/master/Tools%26Library/%E8%B0%83%E8%AF%95%E5%B9%B3%E5%8F%B0Sonar.md "调试平台Sonar" -[214]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/AudioTrack%E7%AE%80%E4%BB%8B.md "AudioTrack简介" +[214]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/AudioTrack%E7%AE%80%E4%BB%8B.md "AudioTrack简介" [215]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/OOM%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.md "OOM问题分析" [216]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/ExoPlayer "ExoPlayer" [217]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/ExoPlayer/1.%20ExoPlayer%E7%AE%80%E4%BB%8B.md "1. ExoPlayer简介" @@ -579,11 +584,11 @@ Android学习笔记 [223]: https://github.com/CharonChui/AndroidNote/blob/master/Tools%26Library/Icon%E5%88%B6%E4%BD%9C.md "Icon制作" [224]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE "流媒体协议" [225]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F "视频封装格式" -[226]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/SurfaceView%E4%B8%8ETextureView.md "SurfaceView与TextureView" +[226]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/SurfaceView%E4%B8%8ETextureView.md "SurfaceView与TextureView" [227]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E5%85%B3%E9%94%AE%E5%B8%A7.md "关键帧" [228]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/CDN%E5%8F%8APCDN.md "CDN及PCDN" [229]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/P2P%E6%8A%80%E6%9C%AF "P2P" -[230]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%92%AD%E6%94%BE%E5%99%A8%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md "播放器性能优化" +[230]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/%E6%92%AD%E6%94%BE%E5%99%A8%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md "播放器性能优化" [231]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenGL "OpenGL" [232]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/1.OpenGL%E7%AE%80%E4%BB%8B.md "1.OpenGL简介" [233]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md "2.GLSurfaceView简介" @@ -599,7 +604,7 @@ Android学习笔记 [242]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md "11.OpenGL ES滤镜" [243]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/Danmaku "弹幕" [244]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Danmaku/Android%E5%BC%B9%E5%B9%95%E5%AE%9E%E7%8E%B0.md "Android弹幕实现" -[245]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/MediaExtractor%E3%80%81MediaCodec%E3%80%81MediaMuxer.md "MediaExtractor、MediaCodec、MediaMuxer" +[245]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/MediaExtractor%E3%80%81MediaCodec%E3%80%81MediaMuxer.md "MediaExtractor、MediaCodec、MediaMuxer" [246]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE.md "流媒体协议" [247]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/HLS.md "HLS" [248]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/DASH.md "DASH" @@ -680,9 +685,16 @@ Android学习笔记 [319]: https://github.com/CharonChui/AndroidNote/blob/master/Gradle%26Maven/Composing%20builds%E7%AE%80%E4%BB%8B.md "Composing builds简介" [320]: https://github.com/CharonChui/AndroidNote/blob/master/ImageLoaderLibrary/Coil%E7%AE%80%E4%BB%8B.md "Coil简介" [321]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/M3U8.md "M3U8" - - - +[322]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/FFmpeg "FFmpeg" +[323]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/1.FFmpeg%E7%AE%80%E4%BB%8B.md "1.FFmpeg简介" +[324]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/2.FFmpeg%E5%B8%B8%E7%94%A8%E5%91%BD%E4%BB%A4%E8%A1%8C.md "2.FFmpeg常用命令行" +[325]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/3.FFmpeg%E5%88%87%E7%89%87.md "3.FFmpeg切片" +[326]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/%E9%9F%B3%E8%A7%86%E9%A2%91%E5%90%8C%E6%AD%A5%E5%8E%9F%E7%90%86.md "音视频同步原理" +[327]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/%E9%9F%B3%E8%A7%86%E9%A2%91%E5%9C%BA%E6%99%AF.md "音视频场景" +[328]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/1.%E9%9F%B3%E8%A7%86%E9%A2%91%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md "1.音视频基础知识" +[329]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/2.%E7%B3%BB%E7%BB%9F%E6%92%AD%E6%94%BE%E5%99%A8MediaPlayer.md "2.系统播放器MediaPlayer" +[330]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/11.%E6%92%AD%E6%94%BE%E7%BB%84%E4%BB%B6%E5%B0%81%E8%A3%85.md "11.播放器组件封装" + Developed By diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store deleted file mode 100644 index 63369ec8e7e79eeeaf351066160344353fa5fe36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOHRWu5S@X7BC+X`rLWK%m@06Atk|Ja6c$MxA%VosH{k$WgEO(_jb~VxMq*PT zRLxZO8_&pHREGa#2557ZC+s=H6)G*jq5n&jEf z=?1@%CYpc#%0H&I-3)1iyymlezkPlCxcXUROxs^$rpG;=s(oI%r`?ctJ!*qn7iYj3 za0Z+KXW(EA*uBzR9IT=jat54%V_`tfhkzj%4YOi7Ixv+M0Jwm;2z2QsBqta~!>kAo zgf$hYscbClM( T#9q>Y{vl8Z@yQwZ0|veT^o=pd From 43ba39dbadad6b0f473f9db99257749f2a3e36cb Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 16 Dec 2022 18:26:07 +0800 Subject: [PATCH 004/128] update --- ...41\345\274\217\350\257\246\350\247\243.md" | 17 +++ ...76\350\256\241\346\250\241\345\274\217.md" | 120 +++++++++++++++--- ...&\347\261\273&\346\216\245\345\217\243.md" | 40 ++++++ ...05\350\201\224\345\207\275\346\225\260.md" | 10 ++ ...2\344\270\276&\345\247\224\346\211\230.md" | 19 +++ ...5\345\260\204&\346\211\251\345\261\225.md" | 2 + 6 files changed, 193 insertions(+), 15 deletions(-) diff --git "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index 25d0e1be..6c5ef656 100644 --- "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -29,6 +29,7 @@ Android启动模式详解 - 如果设置了`singleTask`启动模式的`Activity`不是在新的任务中启动时,它会在已有的任务中查看是否已经存在相应的`Activity`实例,如果存在,就会把位于这个`Activity`实例上面的`Activity`全部结束掉, 即最终这个Activity实例会位于任务的堆栈顶端中。以`A`启动`B`来说,当`A`和`B`的`taskAffinity`不同时:第一次创建`B`的实例时,会启动新的`task`,然后将`B`添加到新建的`task`中;否则,将`B`所在`task`中位于`B`之上的全部`Activity`都删除,然后跳转到`B`中。 - `singleInstance` + 顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! 它与`singleTask`有相同之处,也有不同之处。 相同之处:任意时刻,最多只允许存在一个实例。 @@ -38,6 +39,22 @@ Android启动模式详解 - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 + +假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。 + +假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。 +现在FirstActivity 启动SecondActivity,SecondActivity再启动ThirdActivity。 + +然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/activity_launch_mode_singleinstance.png?raw=true) + + + + + + --- diff --git "a/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" index 4b2d8e72..a499894c 100644 --- "a/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -22,6 +22,7 @@ - 开闭原则`(Open Close Principle)` 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是: 为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 + 当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定了系统可复用性、可扩展性的基础,也增加了系统的稳定性。 - 里氏代换原则`(Liskov Substitution Principle)` 里氏代换原则面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 `LSP`是继承复用的基石,只有当衍生类可以替换掉基类, @@ -29,23 +30,35 @@ 里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 - 依赖倒转原则`(Dependence Inversion Principle)` - 这个是开闭原则的基础,具体内容:真对接口编程,依赖于抽象而不依赖于具体。 + 我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。依赖倒置原则(Dependency InversionPrinciple)指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在“里氏替换原则”中所提到的“面向接口编程”,以达到替换底层实现的目的。 + 举个例子,公司总经理制订了下一年度的目标与计划,为了提高办公效率,总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?我想世界上没有以这种方式管理公司的领导吧。公司高层一定会发动IT部门的上层抽象去执行,调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们,这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。 + +- 接口隔离原则`(Interface Segregation Principle)` + 接口隔离原则(Interface Segregation Principle)指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对应一个职能。 -- 接口隔离原则`(Interface Segregation Principle)` 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想, 从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 -- 迪米特法则(最少知道原则)`(Demeter Principle)` - 为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。 + +- 迪米特法则(最少知识原则)`(Demeter Principle)` + 迪米特法则(law of Demeter)也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变得举步维艰。举个例子,我们买了一台游戏机,主机内部集成了非常复杂的电路及电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理,但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装。 + 之前我们学过的“门面模式”就是极好的范例。例如我们去某单位办理一项业务,来到业务大厅一脸茫然,各种填表、盖章等复杂的办理流程让人一头雾水,有可能来回折腾几个小时。假若有一个提供快速通道服务的“门面”办理窗口,那么我们只需简单地把材料递交过去就可以了,“办理人“与“门面”保持最简单的通信,对于门面里面发生的事情,办理人则知之甚少,更没有必要去亲力亲为。要设计出符合迪米特法则的软件,切勿跨越红线,干涉他人内务。系统模块一定要最大程度地隐藏内部逻辑,大门一定要紧锁,防止陌生人随意访问,而对外只适可而止地暴露最简单的接口,让模块间的通信趋向“简单化”“傻瓜化”。 + - 合成复用原则`(Composite Reuse Principle)` 原则是尽量使用合成/聚合的方式,而不是使用继承。 +- 单一职责原则(Single Responsibility Principle) + 我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是单一职责原则(SingleResponsibility Principle)。 + 以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备“高内聚性”,让单个模块变得“简单”“易懂”,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。 1.工厂方法模式`(Factory Method)` --- +制造业是一个国家工业经济发展的重要支柱,而工厂则是其根基所在。程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法(Factory Method)则可以升华为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。 +工厂内部封装的生产逻辑对外部来说像一个黑盒子,外部不需要关心工厂内部细节,外部类只管调用即可。 + 工厂方法模式分为三种: - 普通工厂模式:就是建立一个工厂类,对实现了同一接口的一些类进行实例的创建。 @@ -134,7 +147,12 @@ --- -工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。因为抽象工厂不太好理解,我们先看看图,然后就和代码,就比较容易理解。 +工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。 + +抽象工厂模式(Abstract Factory)是对工厂的抽象化,而不只是制造方法。我们知道,为了满足不同用户对产品的多样化需求,工厂不会只局限于生产一类产品,但是系统如果按工厂方法那样为每种产品都增加一个新工厂又会造成工厂泛滥。所以,为了调和这种矛盾,抽象工厂模式提供了另一种思路,将各种产品分门别类,基于此来规划各种工厂的制造接口,最终确立产品制造的顶级规范,使其与具体产品彻底脱钩。抽象工厂是建立在制造复杂产品体系需求基础之上的一种设计模式,在某种意义上,我们可以将抽象工厂模式理解为工厂方法模式的高度集群化升级版。 +针对这种情况,我们就需要进行产业规划与整合,对现有工厂进行重构。例如,我们可以基于产品品牌与系列进行生产线规划,按品牌划分A工厂与B工厂。具体以汽车工厂举例,A品牌汽车有轿车、越野车、跑车3个系列的产品,同样地,B品牌汽车也包括以上3个系列的产品,如此便形成了两个产品族,分别由A工厂和B工厂负责生产,每个工厂都有3条生产线,分别生产这3个系列的汽车,如图5-1所示。 + + 还是用上面的例子,只是在工厂类这里需要改一下,提供两个不同的工厂类,他们要实现同一个接口: @@ -235,7 +253,8 @@ public class SingleTon { } ``` -似乎解决了之前提到的问题,将`synchronized`关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在`instance`为`null`,并创建对象的时候才需要加锁,性能有一定的提升。 但是,这样的情况,还是有可能有问题的,看下面的情况:在`Java`指令中创建对象和赋值操作是分开进行的,也就是说`instance = new Singleton();`语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能`JVM`会为新`的Singleton`实例分配空间,然后直接赋值给`instance`成员,然后再去初始化这个`Singleton`实例。这样就可能出错了。 +似乎解决了之前提到的问题,将`synchronized`关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在`instance`为`null`,并创建对象的时候才需要加锁,性能有一定的提升。 +但是,这样的情况,还是有可能有问题的,看下面的情况:在`Java`指令中创建对象和赋值操作是分开进行的,也就是说`instance = new Singleton();`语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能`JVM`会为新`的Singleton`实例分配空间,然后直接赋值给`instance`成员,然后再去初始化这个`Singleton`实例。这样就可能出错了。 我们以`A`、`B`两个线程为例: @@ -275,8 +294,11 @@ public class SingleTon { --- +建造者模式(Builder)所构建的对象一定是庞大而复杂的,并且一定是按照既定的制造工序将组件组装起来的,例如计算机、汽车、建筑物等。我们通常将负责构建这些大型对象的工程师称为建造者。建造者模式又称为生成器模式,主要用于对复杂对象的构建、初始化,它可以将多个简单的组件对象按顺序一步步组装起来,最终构建成一个复杂的成品对象。与工厂系列模式不同的是,建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造工序能够产出不同的产品。 + 工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的`Test`测试类结合起来得到的。 + 还是前面的例子,一个`ISender`接口,两个实现类`MailSender`和`SmsSender`。最后,建造者类如下: ```java public class SenderBuilder { @@ -306,6 +328,15 @@ builder.produceMailSender(10); 5.原型模式`(Prototype)` --- +在制造业中通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,如果达到了质量要求,即可参照这个原型进行批量生产。原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类。 +也就是说,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。 + +构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程),再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。 +那么,对于第一份打印出来的原文稿,我们可以称之为“原型文件”,而对于复印过程,我们则可以称之为“原型拷贝” + +想必大家已经明白了类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。 +***原型模式的目的是从原型实例克隆出新的实例,对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择*** + 该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在`Java`中,复制对象是通过`clone()`实现的,先创建一个原型类: ```java @@ -319,6 +350,9 @@ public class Prototype implements Cloneable { 很简单,一个原型类,只需要实现`Cloneable`接口,重写`clone()`方法,此处`clone()`方法可以改成任意的名称,因为`Cloneable`接口是个空接口,你可以任意定义实现类的方法名,如`cloneA`或者`cloneB`,因为此处的重点是`super.clone()`这句话,`super.clone()`调用的是`Object`的`clone()`方法,而在`Object`类中,`clone()`是`native`的。 +我们都知道,Java中的变量分为原始类型和引用类型,所谓浅拷贝是指只复制原始类型的值,比如横坐标x与纵坐标y这种以原始类型int定义的值,它们会被复制到新克隆出的对象中。 +而引用类型同样会被拷贝,但是请注意这个操作只是拷贝了地址引用(指针),也就是说副本与原型中的对象是同一个,因为两个同样的地址实际指向的内存对象是同一个对象。需要注意的是,克隆方法中调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被真正克隆。 + 在这儿,将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念: - 浅复制:将一个对象复制后,基本数据类型的变量都会重新创建,而引用类型,指向的还是原对象所指向的。 @@ -375,11 +409,15 @@ class SerializableObject implements Serializable { 实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。 +从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。究其本质,克隆操作时Java虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率远远高于“new”关键字所触发的实例化操作。看尽世间烦扰,拨开云雾见青天,有时候“简单粗暴”也是一种去繁从简、不绕弯路的解决方案。 + 适配器模式`(Adapter)` --- 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。 +适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时,适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。 + 主要分为三类: @@ -513,10 +551,12 @@ class SerializableObject implements Serializable { - 接口的适配器模式:当不希望实现一个接口中所有的方法时,可以创建一个抽象类`Adapter`,实现所有方法,我们写别的类的时候,继承抽象类即可。 -装饰模式`(Decorator)` +装饰器模式`(Decorator)` --- 装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。 +装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”,它们都是为了增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始对象动态地“包装”完成,是对类实例(对象)“装饰”的结果。 + `Source`类是被装饰类,`Decorator`类是一个装饰类,可以为`Source`类动态的添加一些功能,代码如下: @@ -569,12 +609,15 @@ after decorator! 代理模式`(Proxy)` --- +代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来,使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。 +代理模式不仅能增强原业务功能,更重要的是还能对其进行业务管控。对用户来讲,隐藏于代理中的实际业务被透明化了,而暴露出来的是代理业务,以此避免客户端直接进行业务访问所带来的安全隐患,从而保证系统业务的可控性、安全性。 + 代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。也就是说把专业事情交给专业的人来做。 代理按照代理的创建时期,可以分为两种: - 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的`.class`文件就已经存在了。 -- 动态代理:在程序运行时运用反射机制动态创建而成。 +- 动态代理:在程序运行时运用反射机制动态创建而成。也就是说我们不需要专门针对某个接口去编写代码实现一个代理类,而是在接口运行时动态生成。 静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能,主要用的是`JAVA`的反射机制。 @@ -679,11 +722,14 @@ public static void main(String[] args) { 纵观静态代理与动态代理,它们都能实现相同的功能,而我们看从静态代理到动态代理的这个过程,我们会发现其实动态代理只是对类做了进一步抽象和封装,使其复用性和易用性得到进一步提升而这不仅仅符合了面向对象的设计理念,其中还有AOP的身影,这也提供给我们对类抽象的一种参考。关于动态代理与AOP的关系,个人觉得AOP是一种思想,而动态代理是一种AOP思想的实现! -外观模式`(Facade)` +外观(门面)模式`(Facade)` --- -外观模式是为了解决类与类之家的依赖关系的,像`spring`一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个`Facade`类中,降低了类类之间的耦合度,该模式中没有涉及到接口。 +外观模式是为了解决类与类之家的依赖关系的,像`spring`一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个`Facade`类中,降低了类类之间的耦合度,该模式中没有涉及到接口。 它可能是最简单的结构型设计模式,它能将多个不同的子系统接口封装起来,并对外提供统一的高层接口,使复杂的子系统变得更易使用。 利用门面模式,我们可以把多个子系统“关”在门里面隐藏起来,成为一个整合在一起的大系统,来自外部的访问只需通过这道“门面”(接口)来进行,而不必再关心门面背后隐藏的子系统及其如何运转。总之,无论门面内部如何错综复杂,从门面外部看来总是一目了然,使用起来也很简单。 + +为了更形象地理解门面模式,我们先来看一个例子。早期的相机使用起来是非常麻烦的,拍照前总是要根据场景情况进行一系列复杂的操作,如对焦、调节闪光灯、调光圈等,非专业人士面对这么一大堆的操作按钮根本无从下手,拍出来的照片质量也不高。随着科技的进步,出现了一种相机,叫作“傻瓜相机”,以形容其使用起来的方便性。用户再也不必学习那些复杂的参数调节了,只要按下快门键就可完成所有操作。 + 下面以计算机启动过程为例: ```java @@ -773,8 +819,7 @@ computer closed! 桥接模式`(Bridge)` --- - -桥接模式就是把事物和其具体实现分开,使他们可以各自独立的变化。 +桥接模式(Bridge)能将抽象与实现分离,使二者可以各自单独变化而不受对方约束,使用时再将它们组合起来,就像架设桥梁一样连接它们的功能,如此降低了抽象与实现这两个可变维度的耦合度,以保证系统的可扩展性。 桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的`JDBC`桥`DriverManager`一样,`JDBC`进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是`JDBC`提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。 @@ -850,6 +895,8 @@ this is the second sub! 组合模式`(Composite)` --- +组合模式(Composite)是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。 + 组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便。 ```java @@ -1044,9 +1091,21 @@ public class StrategyTest { ``` 策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/design_mode_celue.png?raw=true) + +相信大家对上图中的计算机、USB接口还有各种设备之间的关系以及使用方法都非常熟悉了,这些模块组成的系统正是策略模式的最佳范例。策略接口就是图中的USB接口。 + +我们通过对计算机USB接口的标准化,使计算机系统拥有了无限扩展外设的能力,需要什么功能只需要购买相关的USB设备。可见在策略模式中,USB接口起到了至关重要的解耦作用。如果没有USB接口的存在,我们就不得不将外设直接“焊接”在主机上,致使设备与主机高度耦合,系统将彻底丧失对外设的替换与扩展能力。 + +变化是世界的常态,唯一不变的就是变化本身。拥有顺势而为、随机应变的能力才能立于不败之地。策略模式的运用能让系统的应变能力得到提升,适应随时变化的需求。接口的巧妙运用让一系列的策略可以脱离系统而单独存在,使系统拥有更灵活、更强大的“可插拔”扩展功能。 + + + 模板方法模式`(Template Method)` --- +模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式(Template Method)则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中,对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。 + 一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用,先看个关系图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/template.jpg?raw=true) @@ -1097,6 +1156,8 @@ public static void main(String[] args) { 观察者模式`(Observer)` --- +观察者模式(Observer)可以针对被观察对象与观察者对象之间一对多的依赖关系建立起一种行为自动触发机制,当被观察对象状态发生变化时主动对外发起广播,以通知所有观察者做出响应。 + 观察者模式很好理解,类似于邮件订阅和`RSS`订阅,当我们浏览一些博客或`wiki`时,经常会看到`RSS`图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。简单的说就是当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。 在`Android`中我们常用的`Button.setOnClickListener()`其实就是用的观察者模式。大名鼎鼎的`RxJava`也是基于这种模式。 @@ -1193,6 +1254,8 @@ observer2 has received! 迭代器模式`(Iterator)` --- +迭代,在程序中特指对某集合中各元素逐个取用的行为。迭代器模式(Iterator)提供了一种机制来按顺序访问集合中的各元素,而不需要知道集合内部的构造。换句话讲,迭代器满足了对集合迭代的需求,并向外部提供了一种统一的迭代方式,而不必暴露集合的内部数据结构。 + 迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。 `MyCollection`中定义了集合的一些操作,`MyIterator`中定义了一系列迭代操作,且持有`Collection`实例,我们来看看实现代码: @@ -1298,9 +1361,15 @@ public static void main(String[] args) { A B C D E ``` +foreach是Java5中引入的一种for的语法增强。如果我们对class文件进行反编译就会发现,对Collection接口的各种实现类来说,foreach本质上还是通过获取迭代器(Iterator)来遍历的。 + +对于任何类型的集合,要防止内部机制不被暴露或破坏,以及确保用户对每个元素有足够的访问权限,迭代器模式起到了至关重要的作用。迭代器巧妙地利用了内部类的形式与集合类分离,然则“藕断丝连”,迭代器依然对其内部的元素保有访问权限,如此便促成了集合的完美封装,在此基础上还提供给用户一套标准的迭代器接口,使各种繁杂的遍历方式得以统一。迭代器模式的应用,能在内部事务不受干涉的前提下,保持一定的对外部开放,让我们“鱼与熊掌兼得”。 + 责任链模式`(Chain of Responsibility)` --- +责任链是由很多责任节点串联起来的一条任务链条,其中每一个责任节点都是一个业务处理环节。责任链模式(Chain ofResponsibility)允许业务请求者将责任链视为一个整体并对其发起请求,而不必关心链条内部具体的业务逻辑与流程走向,也就是说,请求者不必关心具体是哪个节点起了作用,总之业务最终能得到相应的处理。在软件系统中,当一个业务需要经历一系列业务对象去处理时,我们可以把这些业务对象串联起来成为一条业务责任链,请求者可以直接通过访问业务责任链来完成业务的处理,最终实现请求者与响应者的解耦。 + 有多个对象,每个对象持有对下一个对象的引用,这样就会形成一条链,请求在这条链上传递,直到某一对象决定处理该请求。但是发出者并不清楚到底最终那个对象会处理该请求,所以,责任链模式可以实现,在隐瞒客户端的情况下,对系统进行动态的调整。先看看关系图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/zerenlian.jpg?raw=true) @@ -1425,6 +1494,7 @@ public static void main(String[] args) { 备忘录模式`(Memento)` --- +备忘录用来记录曾经发生过的事情,使回溯历史变得切实可行。备忘录模式(Memento)则可以在不破坏元对象封装性的前提下捕获其在某些时刻的内部状态,并像历史快照一样将它们保留在元对象之外,以备恢复之用。 主要目的是保存一个对象的某个状态,以便在适当的时候恢复对象,个人觉得叫备份模式更形象些,通俗的讲下:假设有原始类A,A中有各种属性,A可以决定需要备份的属性,备忘录类B是用来存储A的一些内部状态,类C呢,就是一个用来存储备忘录的,且只能存储,不能修改等操作。做个图来分析一下: @@ -1520,6 +1590,8 @@ public static void main(String[] args) { 状态模式`(State)` --- +状态指事物基于所处的状况、形态表现出的不同的行为特性。状态模式(State)构架出一套完备的事物内部状态转换机制,并将内部状态包裹起来且对外部不可见,使其行为能随其状态的改变而改变,同时简化了事物的复杂的状态变化逻辑。 + 核心思想就是:当对象的状态改变时,同时改变其行为,很好理解!就拿QQ来说,有几种状态,在线、隐身、忙碌等,每个状态对应不同的操作,而且你的好友也能看到你的状态,所以,状态模式就两点: @@ -1602,9 +1674,11 @@ execute the second opt! 根据这个特性,状态模式在日常开发中用的挺多的,尤其是做网站的时候,我们有时希望根据对象的某一属性,区别开他们的一些功能,比如说简单的权限控制等。 + + 访问者模式`(Visitor)` --- - +访问者模式(Visitor)主要解决的是数据与算法的耦合问题,尤其是在数据结构比较稳定,而算法多变的情况下。为了不“污染”数据本身,访问者模式会将多种算法独立归类,并在访问数据时根据数据类型自动切换到对应的算法,实现数据的自动响应机制,并且确保算法的自由扩展。 访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。 @@ -1690,13 +1764,12 @@ execute the second opt! 中介者模式`(Mediator)` --- +中介是在事物之间传播信息的中间媒介。中介模式(Mediator)为对象构架出一个互动平台,通过减少对象间的依赖程度以达到解耦的目的。我们的生活中有各种各样的媒介,如婚介所、房产中介、门户网站、电子商务、交换机组网、通信基站、即时通软件等,这些都与人类的生活息息相关,离开它们我们将举步维艰。 中介者模式也是用来降低类类之间的耦合的,因为如果类类之间有依赖关系的话,不利于功能的拓展和维护,因为只要修改一个对象,其它关联的对象都得进行修改。 如果使用中介者模式,只需关心和`Mediator`类的关系,具体类类之间的关系及调度交给`Mediator`就行,这有点像`spring`容器的作用。 - - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/mediator.jpg?raw=true) `User`类统一接口,`User1`和`User2`分别是不同的对象,二者之间有关联,如果不采用中介者模式,则需要二者相互持有引用,这样二者的耦合度很高,为了解耦, @@ -1785,11 +1858,18 @@ public static void main(String[] args) { // user2 exe! } ``` +众所周知,对象间显式的互相引用越多,意味着依赖性越强,同时独立性越弱,不利于代码的维护与扩展。中介模式很好地解决了这些问题,它能将多方互动的工作交由中间平台去完成,解除了你中有我、我中有你的相互依赖,让各个模块之间的关系变得更加松散、独立,最终增强系统的可复用性与可扩展性,同时也使系统运行效率得到提升。 + 解释器模式`(Interpreter)` --- +解释有拆解、释义的意思,一般可以理解为针对某段文字,按照其语言的特定语法进行解析,再以另一种表达形式表达出来,以达到人们能够理解的目的。类似地,解释器模式(Interpreter)会针对某种语言并基于其语法特征创建一系列的表达式类(包括终极表达式与非终极表达式),利用树结构模式将表达式对象组装起来,最终将其翻译成计算机能够识别并执行的语义树。例如结构型数据库对查询语言SQL的解析,浏览器对HTML语言的解析,以及操作系统Shell对命令的解析。不同的语言有着不同的语法和翻译方式,这都依靠解释器完成。以最常见的Java编程语言为例。当我们以人类能够理解的语言完成了一段程序并命名为Hello.java后,经过调用编译器会生成Hello.class的字节码文件,执行的时候则会加载此文件到内存并进行解释、执行,最终被解释的机器码才是计算机可以理解并执行的指令格式,如下图所示。从Java语言到机器语言,这个跨越语言鸿沟的翻译步骤必须由解释器来完成,这便是其存在的意义: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/design_mode_jieshiqi.png?raw=true) + + 解释器模式是我们暂时的最后一讲,一般主要应用在`OOP`开发中的编译器的开发中,所以适用面比较窄。 @@ -1853,6 +1933,16 @@ public static void main(String[] args) { ``` 基本就这样,解释器模式用来做各种各样的解释器,如正则表达式等的解释器等等! + + +在面向对象的软件设计中,人们经常会遇到一些重复出现的问题。为降低软件模块的耦合性,提高软件的灵活性、兼容性、可复用性、可维护性与可扩展性,人们从宏观到微观对各种软件系统进行拆分、抽象、组装,确立模块间的交互关系,最终通过归纳、总结,将一些软件模式沉淀下来成为通用的解决方案,这就是设计模式的由来与发展。 + + + + + + + --- - 邮箱 :charon.chui@gmail.com 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 d5741133..a6c78e71 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" @@ -357,6 +357,26 @@ private fun switchFragment(position: Int) { ``` 会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` +这里想要判断是否初始化了,需要用isInitialized来判断: +```kotlin +class MyService{ + fun performAction(): String = "foo" +} + +class Test{ + private lateinit var myService: MyService + + fun main(){ + // 如果 myService 对象还未初始化,则进行初始化 + if(!::myService.isInitialized){ + println("hha") + myService = MyService() + } + } +} +``` +注意: :myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省 + 除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: ```kotlin private val test by lazy { "haha" } @@ -627,6 +647,8 @@ class Bird(weight: Double, aget: Int, color: String) { ## 数据类:使用`data class`定义 +数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中,equals()方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。所以我们在Java中创建一个数据类时要写很多代码,但是在Kotlin中你只需要一行代码。 + 数据类是一种非常强大的类: ```java @@ -810,7 +832,14 @@ class SuperPerson(num: Int) : Person(num) 冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 + + +注意: 上面在说道继承的时候`class SuperPerson(num: Int) : Person(num)`在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。 +但是如果类没有主构造函数,如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin +中是允许类中只有次构造函数,没有主构造函数的。当一个类没有显式的定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。 + 如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 + 如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: @@ -820,6 +849,10 @@ class MyView : View { constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } ``` +也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。那么既然没有主构造函数,继承View类的时候也就不需要再在View类后加上括号了。 +其实原因就是这么简单,只是很多人在刚开始学习Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的写法有时候要加上括号,有时候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的。 + +另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。 ### Any @@ -1005,6 +1038,13 @@ interface Flyer { } ``` +一个类实现接口时: +```kotlin +class Bird() : Flyer { + // ... +} +``` +接口的后面不用加上括号,因为它没有构造函数可以去调用。 ## 函数:通过`fun`关键字定义 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 e426256e..6e4ad471 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" @@ -227,6 +227,10 @@ countryApp.filterCountries(countries, { 这就是Lambda表达式,它与匿名函数一样,是一种函数字面量。 +首先来看一下Lambda的定义,如果用最直白的语言来阐述的话,Lambda就是一小段可以作为参数传递的代码。从定义上看,这个功能就很厉害了,因为正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。这里两次使用了“一小段代码”这种描述,那么到底多少代码才算一小段代码呢?Kotlin对此并没有进行限制,但是通常不建议在Lambda表达式中编写太长的代码,否则可能会影响代码的可读性。 + + + Lambda的语法: - 一个Lambda表达式必须通过{}来包裹 @@ -244,6 +248,7 @@ val foo = { x: Int -> ## Lambda表达式 +{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体} > “Lambda 表达式”(lambda expression)其实就是匿名函数,`Lambda`表达式基于数学中的`λ`演算得名,直接对应于其中的`lambda`抽象 > `(lambda abstraction)`,是一个匿名函数,即没有函数名的函数。`Lambda`表达式可以表示闭包。 @@ -368,6 +373,11 @@ add = { x: Int, y: Int -> x + y } Lambda类型也被认为是函数类型。 +当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替. +```kotlin +val list = listOf("Apple", "Bnana", "Orange", "Pear") +val maxLengthFruit = list.maxBy {it.length} +``` ### Lambda开销 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 27224838..75599003 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" @@ -471,6 +471,25 @@ class App : Application() { 在这例子中,创建一个继承`Application`的类,并且在`companion object`中存储它的唯一实例。 `lateinit`表示这个属性开始是没有值的,但是,在使用前将被赋值(否则,就会抛出异常)。 + +不过,上面companion object中的isRedpack()方法其实也并不是静态方法,companionobject这个关键字实际上会在类的内部创建一个伴生类,而isRedpack方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证类始终只会存在一个伴生类对象,因此调用Prize.isRedpack()方法实际上就是调用了Prize类中伴生对象的isRedpack()方法。由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。 + +先来看注解,前面使用的单例类和companion object都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在Java代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。而如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法 + +注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。那么现在不管是在Kotlin中还是在Java中,都可以使用Prize.isRedpack()的写法来调用了。 + +再来看顶层方法,顶层方法指的是那些没有定义在任何类中的方法。Kotlin编译器会将所有的顶层方法全部编译成静态方法,因此只要你定义了一个顶层方法,那么它就一定是静态方法。 +如果我们新建一个Helper.kt的空文件(注意不是类,是空文件),在文件中定义一个方法: +```kotlin +fun doSomething() { + println("do something") +} +``` +刚才已经讲过了,Kotlin编译器会将所有的顶层方法全部编译成静态方法,那么这里又没有类我们要怎么调用这个doSomething()方法呢?如果是在Kotlin代码中调用的话,那就很简单了,所有的顶层方法都可以在任何位置被直接调用,不用管包名路径,也不用创建实例,直接键入doSomething()即可。 + +但如果是在Java代码中调用,你会发现是找不到doSomething()这个方法的,因为Java中没有顶层方法这个概念,所有的方法必须定义在类中。那么这个doSomething()方法被藏在了哪里呢?我们刚才创建的Kotlin文件名叫作Helper.kt,于是Kotlin编译器会自动创建一个叫作HelperKt的Java类,doSomething()方法就是以静态方法的形式定义在HelperKt类里面的,因此在Java中使用HelperKt.doSomething()的写法来调用就可以了。 + + ### 单例 因为一个类的伴生对象跟一个静态类一样,全局只能有一个。这让我们联想到了什么? 没错,就是单例对象。 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 a6a52562..50ff4d0a 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" @@ -666,6 +666,8 @@ if (name.isNullOrBlank()) { #### `with`函数 +with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。 + with和apply这两个方法最大的作用就是可以让那个我们在写Lambda的时候,省略需要多次书写的对象名,默认用this关键字来指向它,this可以省略。 `with`是一个非常有用的函数,它包含在`Kotlin`的标准库中。它接收一个对象和一个扩展函数作为它的参数,然后使这个对象扩展这个函数。 From 0b087067af7d220f825c26a09f5ddb9edcd676fe Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 22 Dec 2022 15:55:07 +0800 Subject: [PATCH 005/128] update notes --- ...05\350\201\224\345\207\275\346\225\260.md" | 101 +++++++++++++++++- ...2\344\270\276&\345\247\224\346\211\230.md" | 86 +++++++++++++++ 2 files changed, 185 insertions(+), 2 deletions(-) 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 6e4ad471..42440cd9 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" @@ -639,7 +639,7 @@ fun main() { ## 优化Lambda开销 -在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 +在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 #### 1. invokedynamic @@ -655,7 +655,8 @@ fun main() { invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。 -因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 +因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 +所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。 所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。 @@ -805,6 +806,7 @@ public static final void foo(@NotNull Function0 block1, @NotNull Function0 block 可以看出,foo函数的block2参数在带上noinline之后,反编译后的Java代码中并没有将其函数体代码在调用处进行替换。 + #### 非局部返回 Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。我们先来看下Kotlin如何支持非局部返回。 @@ -892,6 +894,101 @@ fun hasZeros(list: List): Boolean { } ``` + +##### 为什么要设计noinline + +这里我已经蒙了,前面已经说了内联函数的好处,那为什么Kotlin还要提供一个noinline关键字来排除内联功能呢? +这是因为内联的函数类型参数在编译的时候会被进行代码替换,因此它没有真正的参数属性。 +非内联的函数类型参数可以自由地传递给其他任何函数,因为它就是一个真实的参数,而内联的函数类型参数只允许传递给另外一个内联函数,这也是它最大的局限性。 +另外,内联函数和非内联函数还有一个重要的区别,那就是内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。为了说明这个问题,我们来看下面的例子: +```kotlin +fun printString(str: String, block: (String) -> Unit) { + println("printString begin") + block(str) + println("printString end") +} + +fun main() { + println("main start") + val str = "" + printString(str) { s -> + println("lambda start") + if (s.isEmpty()) return@printString + println(s) + println("lambda end") + } + println("main end") +} +``` +这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下: +``` +main start +printString begin +lambda start +printString end +main end +``` + +可以看到,除了Lambda表达式中return@printString语句之后的代码没有打印,其他的日志是正常打印的,说明return@printString确实只能进行局部返回。但是如果我们将printString()函数声明成一个内联函数,那么情况就不一样了,如下所示: +```kotlin +inline fun printString(str: String, block: (String) -> Unit) { + println("printString begin") + block(str) + println("printString end") +} + +fun main() { + println("main start") + val str = "" + printString(str) { s -> + println("lambda start") + if (s.isEmpty()) return + println(s) + println("lambda end") + } + println("main end") +} +``` +现在printString()函数变成了内联函数,我们就可以在Lambda表达式中使用return关键字了。此时的return代表的是返回外层的调用函数,也就是main()函数,如果想不通为什么的话,可以回顾一下在上一小节中学习的内联函数的代码替换过程。现在重新运行一下程序,打印结果如下: +``` +main start +printString begin +lambda start +``` +可以看到,不管是main()函数还是printString()函数,确实都在return关键字之后停止执行了,和我们所预期的结果一致。 +将高阶函数声明成内联函数是一种良好的编程习惯,事实上,绝大多数高阶函数是可以直接声明成内联函数的,但是也有少部分例外的情况。观察下面的代码示例: +```kotlin +inline fun runRunnable(block: () -> Unit) { + val runnable = Runnable { + block() + } + runnable.run() +} +``` +这段代码在没有加上inline关键字声明的时候绝对是可以正常工作的,但是在加上inline关键字之后就会提示如下: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/inline_noline_error.png?raw=true) + +这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。 + + +而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。 +也就是说,如果我们在高阶函数中创建了另外的Lambda或者匿名类的实现,并且在这些实现中调用函数类型参数,此时再将高阶函数声明成内联函数,就一定会提示错误。 + +那么是不是在这种情况下就真的无法使用内联函数了呢?也不是,比如借助crossinline关键字就可以很好地解决这个问题: +```kotlin +inline fun runRunnable(crossinline block: () -> Unit) { + val runnable = Runnable { + block() + } + runnable.run() +} +``` +可以看到,这里在函数类型参数的前面加上了crossinline的声明,代码就可以正常编译通过了。 +那么这个crossinline关键字又是什么呢?前面我们已经分析过,之所以会提示上面所示的错误,就是因为内联函数的Lambda表达式中允许使用return关键字,和高阶函数的匿名类实现中不允许使用return关键字之间造成了冲突。而crossinline关键字就像一个契约,它用于保证在内联函数的Lambda表达式中一定不会使用return关键字,这样冲突就不存在了,问题也就巧妙地解决了。 + +声明了crossinline之后,我们就无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunnable的写法进行局部返回。总体来说,除了在return关键字的使用上有所区别之外,crossinline保留了内联函数的其他所有特性。 + #### crossinline 值得注意的是,非局部返回虽然在某些场合下非常有用,但可能也存在危险。因为有时候,我们内联的函数所接收的Lambda参数常常来自于上下文其他地方。为了避免带有return的Lambda参数产生破坏,我们还可以使用crossinline关键字来修饰该参数,从而杜绝此类问题的发生。就像这样子: 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 75599003..9c91998e 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" @@ -30,6 +30,49 @@ val t2 = TypedClass(25) val t3 = TypedClass(null) ``` +#### 泛型实化 +泛型实化功能允许我们在泛型函数当中获得泛型的实际类型,这也就使得类似于a is T、T::class.java这样的语法成为了可能。而灵活运用这一特性将可以实现一些不可思议的语法结构,下面我们赶快来看一下吧。到目前为止,我们已经将Android的四大组件全部学完了,除了ContentProvider之外,你会发现其余的3个组件有一个共同的特点,它们都是要结合Intent一起使用的。比如说启动一个Activity就可以这么写: +```kotlin +val intent = Intent(context, TestActivity::class.java) +context.startActivity(intent) +``` +有没有觉得TestActivity::class.java这样的语法很难受呢?当然,如果在没有更好选择的情况下,这种写法也是可以忍受的,但是Kotlin的泛型实化功能使得我们拥有了更好的选择。新建一个reified.kt文件,然后在里面编写如下代码: +```kotlin +inline fun startActivity(context: Context) { + val intent = Intent(context, T::class.java) + context.startActivity(intent) +} +``` +这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来就是神奇的地方了,Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在,如果我们想要启动TestActivity,只需要这样写就可以了:[插图]Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。 +```kotlin +startActivity(context) +``` +Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。 +不过,现在的startActivity()函数其实还是有问题的,因为通常在启用Activity的时候还可能会使用Intent附带一些参数,比如下面的写法: +```kotlin +val intent = Intent(context, TestActivity::class.java) +intent.putExtra("param1", "data") +intent.putExtra("param2", 123) +context.startActivity(intent) +``` +而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助之前在第6章学习的高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示: +```kotlin +inline fun startActivity(context: Context, block: Intent.() -> Unit) { + val intent = Intent(context, T::class.java) + intent.block() + context.startActivity(intent) +} +``` +可以看到,这次的startActivity()函数中增加了一个函数类型参数,并且它的函数类型是定义在Intent类当中的。在创建完Intent的实例之后,随即调用该函数类型参数,并把Intent的实例传入,这样调用startActivity()函数的时候就可以在Lambda表达式中为Intent传递参数了,如下所示: +```kotlin +startActivity(context) { + putExtra("param1", "data") + putExtra("param2", 123) +} +``` +不得不说,这种启动Activity的代码写起来实在是太舒服了,泛型实化和高阶函数使这种语法结构成为了可能. + + ##### 类型擦除 @@ -46,6 +89,10 @@ com.study.jcking.weatherkotlin.exec.Data 声明了一个泛型类`Data`,并实现了两种不同类型的实例。但是在获取类名是,却发现得到了同样的结果 `com.study.jcking.weatherkotlin.exec.Data`,这其实是在编译期擦除了泛型类型声明。 + + + + ### 嵌套类 嵌套类顾名思义,就是嵌套在其他类中的类。而嵌套类外部的类一般被称为包装类或者外部类。 @@ -804,6 +851,45 @@ void test(){ 如果你指定的参数为`LazyThreadSafetyMode.SYNCHRONIZED`,则可以省略,因为`lazy`默认就是使用的`LazyThreadSafetyMode.SYNCHRONIZED`。 + + +那么学习了Kotlin的委托功能之后,我们就可以对by lazy的工作原理进行解密了,它的基本语法结构如下: +```kotlin +val p by lazy { ... } +``` +现在再来看这段代码,是不是觉得更有头绪了呢?实际上,bylazy并不是连在一起的关键字,只有by才是Kotlin中的关键字, +lazy在这里只是一个高阶函数而已。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候, +其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式, +这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。 +简单的实现如下: +```kotlin +class Later(val block: () -> T) { + +} +``` +这里我们首先定义了一个Later类,并将它指定成泛型类。Later的构造函数中接收一个函数类型参数,这个函数类型参数不接收任何参数,并且返回值类型就是Later类指定的泛型。接着我们在Later类中实现getValue()方法,代码如下所示: +```kotlin +class Later(val block: () -> T) { + + var value: Any? = null + + operator fun getValue(any: Any?, prop: KProperty<*>): T { + if (value == null) { + value = block() + } + return value as T + } + +} +``` +这里将getValue()方法的第一个参数指定成了Any?类型,表示我们希望Later的委托功能在所有类中都可以使用。然后使用了一个value变量对值进行缓存,如果value为空就调用构造函数中传入的函数类型参数去获取值,否则就直接返回。由于懒加载技术是不会对属性进行赋值的,因此这里我们就不用实现setValue()方法了。代码写到这里,委托属性的功能就已经完成了。虽然我们可以立刻使用它,不过为了让它的用法更加类似于lazy函数,最好再定义一个顶层函数。这个函数直接写在Later.kt文件中就可以了,但是要定义在Later类的外面,因为只有不定义在任何类当中的函数才是顶层函数。代码如下所示: +```kotlin +fun later(block: () -> T) = Later(block) +``` +我们将这个顶层函数也定义成了泛型函数,并且它也接收一个函数类型参数。这个顶层函数的作用很简单:创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数。现在,我们自己编写的later懒加载函数就已经完成了,你可以直接使用它来替代之前的lazy函数 + + + ##### 可观察属性 如果你要观察一个属性的变化过程,那么可以将属性委托给`Delegates.observable`, `observable`函数原型如下: From a1bc7c97237d7b78abc3c7afefcccdfcfd2b9efd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 22 Dec 2022 19:30:02 +0800 Subject: [PATCH 006/128] update notes --- ...5\344\273\244\350\241\214\345\244\247\345\205\250.md" | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index 53efd6b1..77830f45 100644 --- "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -511,7 +511,16 @@ ifconfig [网络设备] [参数] +- file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + +- pkill命令可以使用程序名代替PID来终止进程,这就方便多了。除此之外,pkill命令也允许使用通配符,当系统出问题时,这是一个非常有用的工具: +# pkill http* +# +该命令将“杀死”所有名称以http起始的进程,比如Apahce Web Server的httpd服务。 +警告 以root身份使用pkill命令时要格外小心。命令中的通配符很容易意外地将系统的重要进程终止。这可能会导致文件系统损坏。 From 5c4e1e1950f310663bace33581e8239adf8992b1 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 8 Feb 2023 20:52:43 +0800 Subject: [PATCH 007/128] update --- .../Git\347\256\200\344\273\213.md" | 681 ++++++++++-------- ...37\347\220\206\345\210\206\346\236\220.md" | 241 ++++--- ...ps\347\232\204\345\214\272\345\210\253.md" | 14 +- ...05\345\255\230\346\250\241\345\236\213.md" | 37 +- ...12\346\234\211\345\272\217\346\200\247.md" | 71 +- .../MD5\345\212\240\345\257\206.md" | 13 +- ...14Synchronized\345\214\272\345\210\253.md" | 80 +- ...13\346\261\240\347\256\200\344\273\213.md" | 38 +- ...46\344\271\240\346\211\213\345\206\214.md" | 16 +- ...70\345\205\263\345\267\245\345\205\267.md" | 481 +++++-------- 10 files changed, 815 insertions(+), 857 deletions(-) diff --git "a/JavaKnowledge/Git\347\256\200\344\273\213.md" "b/JavaKnowledge/Git\347\256\200\344\273\213.md" index d3d372be..fbba61fb 100644 --- "a/JavaKnowledge/Git\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/Git\347\256\200\344\273\213.md" @@ -1,46 +1,54 @@ Git简介 -=== - -`Git`和其他版本控制系统(包括`Subversion`及其他相似的工具)的主要差别在于`Git`对待数据的方法。概念上来区分,其它大部分系统以文件变更列表的方式存储信息。 -这类系统(`CVS、Subversion、Perforce、Bazaar`等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。存储每个文件与初始版本的差异。 - - -`Git`不按照以上方式对待或保存数据。反之,`Git`更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在`Git`中保存项目状态时, -它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,`Git`不再重新存储该文件,而是只保留一个链接指向之前存储的文件。`Git`对待数据更像是一个快照流。 - -`Git`是分布式版本控制系统,集中式和分布式版本控制有什么区别呢? - -- 集中式版本控制系统 - 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_jizhong.jpeg) - -- 分布式版本控制系统 - 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 - 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个 人 的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 - 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) +======= + +`Git`和其他版本控制系统(包括`Subversion`及其他相似的工具)的主要差别在于`Git`对待数据的方法。概念上来区分,其它大部分系统以文件 +变更列表的方式存储信息。 +这类系统(`CVS、Subversion、Perforce、Bazaar`等等)将它们保存的信息看作是一组基本文件和每个文件随时间逐步累积的差异。 +存储每个文件与初始版本的差异。 + +`Git`不按照以上方式对待或保存数据。反之,`Git`更像是把数据看作是对小型文件系统的一组快照。每次你提交更新,或在`Git`中保存项目 +状态时,它主要对当时的全部文件制作一个快照并保存这个快照的索引。为了高效,如果文件没有修改,`Git`不再重新存储该文件,而是只保留一个 +链接指向之前存储的文件。`Git`对待数据更像是一个快照流。 + +`Git`是分布式版本控制系统,集中式和分布式版本控制有什么区别呢? + +- 集中式版本控制系统 + 版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了, + 再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了, + 再放回图书馆。集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上, + 遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_jizhong.jpeg) +- 分布式版本控制系统 + 分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在 + 你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上 + 改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。 + 和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧, + 随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。 + 在实际使用分布式版本控制系统的时候,其实很少在两人之间的电脑上推送版本库的修改,因为可能你们俩不在一个局域网内,两台电脑互相 + 访问不了,也可能今天你的同事病了,他的电脑压根没有开机。因此,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器 + 的作用仅仅是用来方便“交换”大家的修改,没有它大家也一样干活,只是交换修改不方便而已。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_fenbu.jpeg) 版本库 ---- +------ -什么是版本库呢?版本库又名仓库,英文名`repository`,你可以简单理解成一个目录,这个目录里面的所有文件都可以被`Git`管理起来,每个文件的修改、删除,`Git`都能跟踪, -以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 +什么是版本库呢?版本库又名仓库,英文名`repository`,你可以简单理解成一个目录,这个目录里面的所有文件都可以被`Git`管理起来, +每个文件的修改、删除,`Git`都能跟踪,以便任何时刻都可以追踪历史,或者在将来某个时刻可以“还原”。 -所以,创建一个版本库非常简单: +所以,创建一个版本库非常简单: - 创建一个空目录 - 通过`git init`命令把这个目录变成`Git`可以管理的仓库: - 瞬间`Git`就把仓库建好了,而且告诉你是一个空的仓库`(empty Git repository)`,细心的读者可以发现当前目录下多了一个`.git`的目录, - 这个目录是`Git`来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把`Git`仓库给破坏了。 + 瞬间`Git`就把仓库建好了,而且告诉你是一个空的仓库`(empty Git repository)`,细心的读者可以发现当前目录下多了一个`.git`的目录, + 这个目录是`Git`来跟踪管理版本库的,没事千万不要手动修改这个目录里面的文件,不然改乱了,就把`Git`仓库给破坏了。 - 使用命令`git add `,注意,可反复多次使用,添加多个文件; - 使用命令`git commit`,完成。 +Git的五种状态 +------------- -Git的五种状态 ---- - - -`Git`有五种状态,你的文件可能处于其中之一: +`Git`有五种状态,你的文件可能处于其中之一: - 未修改`(origin)` - 已修改`(modified)` @@ -48,37 +56,34 @@ Git的五种状态 - 已提交`(committed)` - 已推送`(pushed)` -已提交表示数据已经安全的保存在本地数据库中。 已修改表示修改了文件,但还没保存到数据库中。 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 +已提交表示数据已经安全的保存在本地数据库中。 +已修改表示修改了文件,但还没保存到数据库中。 +已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 `Git`仓库目录是`Git`用来保存项目的元数据和对象数据库的地方。这是`Git`中最重要的部分,从其它计算机克隆仓库时,拷贝的就是这里的数据。 工作目录是对项目的某个版本独立提取出来的内容。这些从`Git`仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。 暂存区域是一个文件,保存了下次将提交的文件列表信息,一般在`Git`仓库目录中。 有时候也被称作‘索引’,不过一般说法还是叫暂存区域。 -基本的`Git`工作流程如下: +基本的`Git`工作流程如下: - 在工作目录中修改文件。 - 暂存文件,将文件的快照放入暂存区域。 - 提交更新,找到暂存区域的文件,将快照永久性存储到`Git`仓库目录。 - 四个区 ---- +------ -`Git`主要分为四个区: +`Git`主要分为四个区: - 工作区`(Working Area)` - 暂存区`(Stage或Index Area)` - 本地仓库`(Local Repository)` - 远程仓库`(Remote Repository)` +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_buzhou.jpg) - -上面说了`git add`和`git commit`的惭怍,总体分为了三个部分,其实更加详细的来分析,还需要一个`git push`的过程,也就是把更改`push`到远程仓库中。 - -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_buzhou.jpg) - -正常情况下,我们的工作流程就是三个步骤,分别对应上图中的三个箭头线: +正常情况下,我们的工作流程就是三个步骤,分别对应上图中的三个箭头线: ```shell git add . // 把所有文件放入暂存区 @@ -86,12 +91,15 @@ git commit -m "comment" // 把所有文件从暂存区提交进本地仓库 git push // 把所有文件从本地仓库推送进远程仓库 ``` -先上一张图 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) -图中的`index`部分就是暂存区 +先上一张图 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git.png) + +图中的`index`部分就是暂存区 ## 三棵树 -Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树的: + +Git作为一个系统,是以它的一般操作来管理并操纵这三棵树的: | 树 | 用途 | | :---------------- | :----------------------------------- | @@ -101,105 +109,113 @@ Git 作为一个系统,是以它的一般操作来管理并操纵这三棵树 #### HEAD -HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 **该分支上的最后一次提交** 的快照。 +HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示HEAD将是下一次提交的父结点。通常,理解HEAD的最简方式, +就是将它看做**该分支上的最后一次提交**的快照。 #### 索引 -索引是你的 **预期的下一次提交**。 我们也会将这个概念引用为 Git 的“暂存区”,这就是当你运行 `git commit` 时 Git 看起来的样子。 +索引是你的**预期的下一次提交**。我们也会将这个概念引用为Git的“暂存区”,这就是当你运行`git commit`时Git看起来的样子。 #### 工作目录 -最后,你就有了自己的 **工作目录**(通常也叫 **工作区**)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 `.git` 文件夹中。 工作目录会将它们解包为实际的文件以便编辑。 你可以把工作目录当做 **沙盒**。在你将修改提交到暂存区并记录到历史之前,可以随意更改。 +最后,你就有了自己的**工作目录**(通常也叫**工作区**)。 另外两棵树以一种高效但并不直观的方式,将它们的内容存储在`.git`文件夹中。 +工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做**沙盒**。在你将修改提交到暂存区并记录到历史之前,可以随意更改。 +#### Git目录下文件的状态: -#### Git目录下文件的状态: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true)你工作目录下的每一个文件都不外乎这两种状态: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true) -你工作目录下的每一个文件都不外乎这两种状态: - 已跟踪(Tracked) - 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有他们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是Git已经知道的文件 + 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有他们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或 + 已放入暂存区。简而言之,已跟踪的文件就是Git已经知道的文件 - 未跟踪(Untracked) - 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们即不存在与上次快照的记录中,也没有被放入暂存区。 - + 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们即不存在与上次快照的记录中,也没有被放入暂存区。 ## 常用命令 ### git config -安装好git后我们要先配置一下。以便`git`跟踪。 - - ``` - git config --global user.name "xxx" - git config --global user.email "xxx@xxx.com" - ``` - 上面修改后可以使用`cat ~/.gitconfig`查看 - 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 - `git config --list`来查看所有的配置。 - 如果需要查看当前的user.name和user.email的值可以通过`git config user.name` - -### git init -新建仓库 - ``` - mkdir gitDemo - cd gitDemo - git init - ``` - 这样就创建完了。 - -### git clone仓库 - 在某一目录下执行. - `git clone [git path]` - 只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 - -### git add提交文件更改(修改和新增),把当前的修改添加到暂存区 - `git add xxx.txt`添加某一个文件 - `git add .`添加当前目录所有的文件 - -### `git commit`提交,把修改由暂存区提交到仓库中 - `git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 - 或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx - - `git commit`是很小的一件事情,但是往往小的事情往往引不起大家的关注,不妨打开公司的任一个`repo`,查看`commit log`,满篇的`update`和`fix`, - 完全不知道这些`commit`是要做啥。在提交`commit`的时候尽量保证这个`commit`只做一件事情,比如实现某个功能或者修改了配置文件。注意是保证每个`commit` - 只做一件事,而不是让你做了一件事`commit`后就`push`,那样就有点过分了。 - -### `git cherry-pick` - `git cherry-pick`可以选择某一个分支中的一个或几个`commit(s)`来进行操作。例如,假设我们有个稳定版本的分支,叫`v2.0`,另外还有个开发版本的分支`v3.0`,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个`v3.0`中的功能到`v2.0`中,这里就可以使用`cherry-pick`了。 - 就是对已经存在的`commit`进行 再次提交; - 简单用法: - `git cherry-pick ` - `git rebase`命令基本是是一个自动化的`cherry-pick`命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的`cherry-picks`出它们。 -### `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 + +安装好git后我们要先配置一下。以便`git`跟踪。 +```git config --global user.name "xxx" git config --global user.email "xxx@xxx.com"``` +上面修改后可以使用`cat ~/.gitconfig`查看 +如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 +`git config --list`来查看所有的配置。 +如果需要查看当前的user.name和user.email的值可以通过`git config user.name` + +### git init + +新建仓库 +``` +mkdir gitDemo +cd gitDemo +git init +``` +这样就创建完了。 + +### git clone仓库 + +在某一目录下执行. +`git clone [git path]` +只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 + +### git add提交文件更改(修改和新增),把当前的修改添加到暂存区 + +`git add xxx.txt`添加某一个文件 +`git add .`添加当前目录所有的文件 + +### `git commit`提交,把修改由暂存区提交到仓库中 + +`git commit`提交,然后在出来的提示框内查看当前提交的内容以及输入注释。 +或者也可以用`git commit -m "xxx"` 提交到本地仓库并且注释是xxx + +`git commit`是很小的一件事情,但是往往小的事情往往引不起大家的关注,不妨打开公司的任一个`repo`,查看`commit log`, +满篇的`update`和`fix`,完全不知道这些`commit`是要做啥。在提交`commit`的时候尽量保证这个`commit`只做一件事情, +比如实现某个功能或者修改了配置文件。注意是保证每个`commit`只做一件事,而不是让你做了一件事`commit`后就`push`, +那样就有点过分了。 + +### `git cherry-pick` + +`git cherry-pick`可以选择某一个分支中的一个或几个`commit(s)`来进行操作。例如,假设我们有个稳定版本的分支,叫`v2.0`, +另外还有个开发版本的分支`v3.0`,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个`v3.0`中的功能到`v2.0`中, +这里就可以使用`cherry-pick`了。 +就是对已经存在的`commit`进行 再次提交; +简单用法: +`git cherry-pick ` +`git rebase`命令基本是是一个自动化的`cherry-pick`命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的`cherry-picks`出它们。 + +### `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 ### `git diff` - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true) - `git diff`直接查看当前修改未add(暂存staged)的差别 - `git diff --staged`查看已add(到暂存区)的差别 - `git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 - - - 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 - - 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 - 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 - - 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 -如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是 - 远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 - -### `git push` 提交到远程仓库 - 可以直接调用`git push`推送到当前分支 - 或者`git push origin master`推送到远程`master`分支 - `git push origin devBranch`推送到远程`devBranch`分支 - -### `git log`查看当前分支下的提交记录 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true)`git diff`直接查看当前修改未add(暂存staged)的差别 +`git diff --staged`查看已add(到暂存区)的差别`git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 + +- 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 +- 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 + 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 +- 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 + 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是 + 远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 + +### `git push` 提交到远程仓库 + +可以直接调用`git push`推送到当前分支 +或者`git push origin master`推送到远程`master`分支 +`git push origin devBranch`推送到远程`devBranch`分支 +### `git log`查看当前分支下的提交记录 + 用`git log`可以查看提交历史,以便确定要回退到哪个版本。 如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来, -用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 +用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 + ``` git log -p -2 // -p 是仅显示最近的x次提交 git log --stat // stat简略的显示每次提交的内容梗概,如哪些文件变更,多少删除,多少添加 git log --oneline --graph git log --grep="1" ``` -下面是常用的参数: +下面是常用的参数: + - `–-author=“Alex Kras”` ——只显示某个用户的提交任务 - `–-name-only` ——只显示变更文件的名称 - `–-oneline`——将提交信息压缩到一行显示 @@ -210,78 +226,90 @@ git log --grep="1" - `--grep` ——过滤内容 #### Git日志搜索 + 如果你想知道某一个东西是什么时候存在或者引入的。git log命令有许多强大的工具可以通过提交信息甚至是diff的内容来找到某个特定的提交。 -例如,如果我们想找到ZLIB_BUF_MAX常量是什么时候引入的,我们可以使用-S选项来显示新增和删除该字符串的提交: +例如,如果我们想找到ZLIB_BUF_MAX常量是什么时候引入的,我们可以使用-S选项来显示新增和删除该字符串的提交: ```shell git log -S ZLIB_BUF_MAX --oneline ``` -### `git reflog` +### `git reflog` + 可以查看所有操作记录包括`commit`和`reset`操作以及删除的`commit`记录 ### `git reset` -`git reset`命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:`git add`,`git commit`等)。 +`git reset`命令用于将当前HEAD复位到指定状态。一般用于撤消之前的一些操作(如:`git add`,`git commit`等)。 在`git`的一般使用中,如果发现错误的将不想暂存的文件被`git add`进入索引之后,想回退取消,则可以使用命令:`git reset HEAD `, -同时`git add`完毕之后,`git`也会做相应的提示,比如: +同时`git add`完毕之后,`git`也会做相应的提示,比如: + ```shell # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # new file: test.py ``` +`git reset [--hard|soft|mixed|merge|keep] [或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: -`git reset [--hard|soft|mixed|merge|keep] [或HEAD]`:将当前的分支重设`(reset)`到指定的``或者`HEAD`(默认,如果不显示指定``,默认是`HEAD`,即最新的一次提交),并且根据`[mode]`有可能更新索引和工作目录。`mode`的取值可以是`hard、soft、mixed、merged、keep`。下面来详细说明每种模式的意义和效果: - - `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 - - `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 - - `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 +- `--hard`:彻底回退到某一个版本,本地的源码也会变为上一个版本的内容。重删除工作空间改动代码,撤销commit,撤销git add .。所有变更集都会被丢弃。 +- `--mixed`:默认方式,它回退到某个版本,只保留源码,不删除工作空间改动代码,撤销commit,并且撤销git add . 。所有变更集都放在工作区。 +- `--soft`: 回退到某个版本,不删除工作空间改动代码,撤销commit,不撤销git add . ,所有变更集都放在暂存区,如果还要提交直接重新commit即可。 #### 示例 -假设我们进入到一个新目录,其中有一个文件。 我们称其为该文件的 v1 版本,将它标记为蓝色。 现在运行 git init,这会创建一个 Git 仓库,其中的 HEAD 引用指向未创建的 master 分支。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex1.png?raw=true) +假设我们进入到一个新目录,其中有一个文件。 我们称其为该文件的v1版本,将它标记为蓝色。 +现在运行git init,这会创建一个Git仓库,其中的HEAD引用指向未创建的master分支。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex1.png?raw=true) 此时,只有工作目录有内容。 -现在我们想要提交这个文件,所以用git add来获取工作目录中的内容,并将其复制到索引中。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex2.png?raw=true) +现在我们想要提交这个文件,所以用git add来获取工作目录中的内容,并将其复制到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex2.png?raw=true) 接着运行git commit,它会取得索引中的内容并将它保存为一个永久的快照,然后创建一个指向该快照的提交对象,最后更新master来指向本次提交。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex3.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex3.png?raw=true) 此时如果我们运行 git status,会发现没有任何改动,因为现在三棵树完全相同。 -现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的 v2 版本,并将它标记为红色。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex4.png?raw=true) -如果现在运行 git status,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 接着我们运行 git add 来将它暂存到索引中。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex5.png?raw=true) -此时,由于索引和 HEAD 不同,若运行 git status 的话就会看到 “Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交与上一次提交不同。 最后,我们运行 git commit 来完成提交。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex6.png?raw=true) -现在运行 git status 会没有输出,因为三棵树又变得相同了。 +现在我们想要对文件进行修改然后提交它。 我们将会经历同样的过程;首先在工作目录中修改文件。 我们称其为该文件的v2版本,并将它标记为红色。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex4.png?raw=true) +如果现在运行git status,我们会看到文件显示在 “Changes not staged for commit” 下面并被标记为红色,因为该条目在索引与工作目录之间存在不同。 +接着我们运行git add来将它暂存到索引中。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex5.png?raw=true) +此时,由于索引和HEAD不同,若运行git status的话就会看到“Changes to be committed” 下的该文件变为绿色 ——也就是说,现在预期的下一次提交 +与上一次提交不同。 最后,我们运行git commit来完成提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-ex6.png?raw=true) +现在运行git status会没有输出,因为三棵树又变得相同了。 -切换分支或克隆的过程也类似。 当检出一个分支时,它会修改 HEAD 指向新的分支引用,将 索引 填充为该次提交的快照, 然后将 索引 的内容复制到 工作目录 中。 +切换分支或克隆的过程也类似。当检出一个分支时,它会修改HEAD指向新的分支引用,将索引填充为该次提交的快照, +然后将索引的内容复制到工作目录中。 #### 重置的作用 -在以下情景中观察 reset 命令会更有意义。 -为了演示这些例子,假设我们再次修改了 file.txt 文件并第三次提交它。 现在的历史看起来是这样的: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-start.png?raw=true) -让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。 +在以下情景中观察reset命令会更有意义。 + +为了演示这些例子,假设我们再次修改了file.txt 文件并第三次提交它。 现在的历史看起来是这样的: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-start.png?raw=true) +让我们跟着reset看看它都做了什么。它以一种简单可预见的方式直接操纵这三棵树。它做了三个基本操作。 ##### 第 1 步:移动 HEAD + reset 做的第一件事是移动 HEAD 的指向。 这与改变 HEAD 自身不同(checkout 所做的);reset 移动 HEAD 指向的分支。 这意味着如果 HEAD 设置为 master 分支(例如,你正在 master 分支上), 运行 git reset 9e5e6a4 将会使 master 指向 9e5e6a4。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-soft.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-soft.png?raw=true) 无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset --soft,它将仅仅停在那儿。 现在看一眼上图,理解一下发生的事情:它本质上是撤销了上一次 git commit 命令。 当你在运行 git commit 时,Git 会创建一个新的提交,并移动 HEAD 所指向的分支来使其指向该提交。 当你将它 reset 回 HEAD~(HEAD 的父结点)时,其实就是把该分支移动回原来的位置,而不会改变索引和工作目录。 现在你可以更新索引并再次运行 git commit 来完成 git commit --amend 所要做的事情了 + ##### 第 2 步:更新索引(--mixed) + 注意,如果你现在运行 git status 的话,就会看到新的 HEAD 和以绿色标出的它和索引之间的区别。 接下来,reset 会用 HEAD 指向的当前快照的内容来更新索引。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-mixed.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-mixed.png?raw=true) 如果指定 --mixed 选项,reset 将会在这时停止。 这也是默认行为,所以如果没有指定任何选项(在本例中只是 git reset HEAD~),这就是命令将会停止的地方。 现在再看一眼上图,理解一下发生的事情:它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。 ##### 第 3 步:更新工作目录(--hard) + reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 --hard 选项,它将会继续这一步。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-hard.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-hard.png?raw=true) 现在让我们回想一下刚才发生的事情。 你撤销了最后的提交、git add 和 git commit 命令 以及 工作目录中的所有工作。 必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。 在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog 来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复。 @@ -293,8 +321,8 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 - 使索引看起来像 HEAD (若未指定 --hard,则到此停止) - 使工作目录看起来像索引 - ##### 通过路径来重置 + 前面讲述了 reset 基本形式的行为,不过你还可以给它提供一个作用路径。 若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。 这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新,所以重置会继续进行第 2、3 步。 现在,假如我们运行 git reset file.txt (这其实是 git reset --mixed HEAD file.txt 的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 --soft 或 --hard),它会: @@ -303,19 +331,18 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 - 让索引看起来像 HEAD (到此处停止) 所以它本质上只是将 file.txt 从 HEAD 复制到索引中。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path1.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path1.png?raw=true) 它还有 取消暂存文件 的实际效果。 如果我们查看该命令的示意图,然后再想想 git add 所做的事,就会发现它们正好相反。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path2.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path2.png?raw=true) 这就是为什么 git status 命令的输出会建议运行此命令来取消暂存一个文件。我们可以不让 Git 从 HEAD 拉取数据,而是通过具体指定一个提交来拉取该文件的对应版本。 我们只需运行类似于 git reset eb43bf file.txt 的命令即可。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path3.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-path3.png?raw=true) 它其实做了同样的事情,也就是把工作目录中的文件恢复到 v1 版本,运行 git add 添加它, 然后再将它恢复到 v3 版本(只是不用真的过一遍这些步骤)。 如果我们现在运行 git commit,它就会记录一条“将该文件恢复到 v1 版本”的更改, 尽管我们并未在工作目录中真正地再次拥有它。 还有一点同 git add 一样,就是 reset 命令也可以接受一个 --patch 选项来一块一块地取消暂存的内容。 这样你就可以根据选择来取消暂存或恢复内容了。 +### `git checkout`撤销修改或者切换分支 - -### `git checkout`撤销修改或者切换分支 -`git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: +`git checkout -- xx.txt`意思就是将`xx.txt`文件在工作区的修改全部撤销。可能会有两种情况: - 修改后还没有调用`git add`添加到暂存区,现在撤销后就会和版本库一样的状态。 - 修改后已经调用`git add`添加到暂存区后又做了修改,这时候撤销就会回到暂存区的状态。 @@ -323,50 +350,47 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 总的来说`git checkout`就是让这个文件回到最近一次`git commit`或者`git add`的状态。 这里还有一个问题就是我胡乱修改了某个文件内容然后调用了`git add`添加到缓存区中,这时候想丢弃修改该怎么办?也是要分两步: -- 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 -- 使用`git checkout --file`直接丢弃工作区的修改。 +- 使用`git reset HEAD file`命令,将暂存区中的内容回退,这样修改的内容会从暂存区回到工作区。 +- 使用`git checkout --file`直接丢弃工作区的修改。 -`git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 +`git checkout`把当前目录所有修改的文件从`HEAD`都撤销修改。 为什么分支的地方也是用`git checkout`这里撤销还是用它呢?他们的区别在于`--`,如果没有`--`那就是检出分支了。 -`git checkout origin/developer` // 切换到orgin/developer分支 - -上面介绍了两个回退操作`git reset`和`git checkout`,这里就总结一下如何来对修改进行撤销操作: +`git checkout origin/developer` // 切换到orgin/developer分支 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_reset_checkout.png?raw=true) +上面介绍了两个回退操作`git reset`和`git checkout`,这里就总结一下如何来对修改进行撤销操作: -- 已经修改,但是并未执行`git add .`进行暂存 - 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是再工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行 - 撤销操作。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_reset_checkout.png?raw=true) - `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 +- 已经修改,但是并未执行`git add .`进行暂存 + 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是再工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行 + 撤销操作。 -- 已暂存,未提交 - 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: + `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 +- 已暂存,未提交 + 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: - ``` - git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 - git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 - ``` - 或`git reset --hard` + ``` + git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 + git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 + ``` + 或`git reset --hard` - 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 + 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 +- 已提交,未推送 + 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? + 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 + 你提交的脏代码,那么就从远程仓库中把代码恢复把。 -- 已提交,未推送 - 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? - 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 - 你提交的脏代码,那么就从远程仓库中把代码恢复把。 + 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? + `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 +- 已推送到远程仓库 + 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 + 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 - 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? - `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 - -- 已推送到远程仓库 - 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 - 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 - - ``` - git reset --hard HEAD^ // HEAD^代表最新提交的前一次 - git push -f // 强制推送 - ``` + ``` + git reset --hard HEAD^ // HEAD^代表最新提交的前一次 + git push -f // 强制推送 + ``` ### reset checkout的区别 @@ -375,31 +399,40 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 不带路径 运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。 -首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着简单合并一下,这样所有 还未修改过的 文件都会被更新。 而 reset --hard 则会不做检查就全面地替换所有东西。 +首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着 +简单合并一下,这样所有 还未修改过的 文件都会被更新。 而 reset --hard 则会不做检查就全面地替换所有东西。 第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。 -例如,假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。 如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话,develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master。 +例如,假设我们有 master 和 develop 分支,它们分别指向不同的提交;我们现在在 develop 上(所以 HEAD 指向它)。 +如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话, +develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master。 + +所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。 reset 会移动 HEAD 分支的指向,而checkout则移动 +HEAD自身。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-checkout.png?raw=true) -所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但 做法 是非常不同的。 reset 会移动 HEAD 分支的指向,而 checkout 则移动 HEAD 自身。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-checkout.png?raw=true) #### 带路径 + 运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。 它就像 git reset [branch] file 那样用该次提交中的那个文件来更新索引,但是它也会覆盖工作目录中对应的文件。 它就像是 git reset --hard [branch] file(如果 reset 允许你这样运行的话), 这样对工作目录并不安全,它也不会移动 HEAD。 此外,同 git reset 和 git add 一样,checkout 也接受一个 --patch 选项,允许你根据选择一块一块地恢复文件内容。 -### `git revert`撤销提交 +### `git revert`撤销提交 + `git revert`在撤销一个提交的同时会创建一个新的提交,这是一个安全的方法,因为它不会重写提交历史。 - `git revert`是生成一个新的提交来撤销某次提交,此次提交之前的`commit`都会被保留 -- `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 +- `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除制定的`commit` 并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 比如前面普通`commit`是增加一行`a`,那么`revert`内容就是删除一行`a`。 -在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干。 虽然可以直接回滚远程分支, 但有时新的代码也已经合入,直接回滚后最近的提交都要重新操作。 那么有没有只移除某些 Commit 的方式呢?可以一次 revert操作来完成。 +在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干。虽然可以直接回滚远程分支,但有时新的代码也已经合入, +直接回滚后最近的提交都要重新操作。 那么有没有只移除某些Commit的方式呢?可以用一次revert操作来完成。 考虑这个例子,我们提交了 6 个版本,其中 3-4 包含了错误的代码需要被回滚掉。 同时希望不影响到后续的 5-6。 + ```shell * 982d4f6 (HEAD -> master) version 6 * 54cc9dc version 5 @@ -408,18 +441,23 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 * f7742cd version 2 * 6c4db3f version 1 ``` -这种情况在团队协作的开发中会很常见:可能是流程或认为原因不小心合入了错误的代码, 也可能是合入一段时间后才发现存在问题。 总之已经存在后续提交,使得直接回滚不太现实。 +这种情况在团队协作的开发中会很常见:可能是流程或认为原因不小心合入了错误的代码,也可能是合入一段时间后才发现存在问题。 +总之已经存在后续提交,使得直接回滚不太现实。 + +下面的部分就开始介绍具体操作了,同时我们假设远程分支是受保护的(不允许 Force Push)。思路是从产生一个新的 Commit 撤销之前的错误提交。 -下面的部分就开始介绍具体操作了,同时我们假设远程分支是受保护的(不允许 Force Push)。 思路是从产生一个新的 Commit 撤销之前的错误提交。 +使用 git revert 可以撤销指定的提交, 要撤销一串提交可以用 .. 语法。 注意这是一个前开后闭区间, +即不包括commit1,但包括commit2。 -使用 git revert 可以撤销指定的提交, 要撤销一串提交可以用 .. 语法。 注意这是一个前开后闭区间,即不包括 commit1,但包括 commit2。 ```shell git revert --no-commit f7742cd..551c408 git commit -a -m 'This reverts commit 7e345c9 and 551c408' ``` -其中 f7742cd 是 version 2,551c408 是 version 4,这样被移除的是 version 3 和 version 4。 注意 revert 命令会对每个撤销的 commit 进行一次提交,--no-commit 后可以最后一起手动提交。 +其中 f7742cd 是 version 2,551c408 是 version 4,这样被移除的是 version 3 和 version 4。 +注意 revert 命令会对每个撤销的 commit 进行一次提交,--no-commit 后可以最后一起手动提交。 此时 Git 记录是这样的: + ```shell * 8fef80a (HEAD -> master) This reverts commit 7e345c9 and 551c408 * 982d4f6 version 6 @@ -431,24 +469,29 @@ git commit -a -m 'This reverts commit 7e345c9 and 551c408' ``` 现在的 HEAD(8fef80a)就是我们想要的版本,把它 Push 到远程即可。 -git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。 +git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中, +本质上就是撤销或者倒转。 + +### `git rm`删除文件 -### `git rm`删除文件 该文件就不再纳入版本管理了。如果删除之前修改过并且已经放到暂存区域的话,则必须要用强制删除选项 -f(译注:即 force 的首字母),以防误删除文件后丢失修改的内容。 -另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上,用 --cached 选项即可:`git rm --cached readme.txt` +另外一种情况是,我们想把文件从 Git 仓库中删除(亦即从暂存区域移除),但仍然希望保留在当前工作目录中。换句话说,仅是从跟踪清单中删除。 +比如一些大型日志文件或者一堆 .a 编译文件,不小心纳入仓库后,要移除跟踪但不删除文件,以便稍后在 .gitignore 文件中补上, +用 --cached 选项即可:`git rm --cached readme.txt` + +### 分支 -### 分支 -`git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 - ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) +`git`分支的创建和合并都是非常快的,因为增加一个分支其实就是增加一个指针,合并其实就是让某个分支的指针指向某一个位置。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_master_branch.png?raw=true) -#### 创建分支 +#### 创建分支 -`git branch devBranch`创建名为`devBranch`的分支。 -`git checkout devBranch`切换到`devBranch`分支。 +`git branch devBranch`创建名为`devBranch`的分支。 +`git checkout devBranch`切换到`devBranch`分支。 `git checkout -b devBranch`创建+切换到分支`devBranch`。 -`git branch`查看当前仓库中的分支。 +`git branch`查看当前仓库中的分支。 `git branch -r`查看远程仓库的分支。 -`git branch -d devBranch`删除`devBranch`分支。 +`git branch -d devBranch`删除`devBranch`分支。 ``` origin/HEAD -> origin/master @@ -458,15 +501,18 @@ origin/master origin/master_sg origin/offline ``` -`git branch -d devBranch`删除`devBranch`分支。 -当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 +`git branch -d devBranch`删除`devBranch`分支。 +当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有 +被合并,删除失败。 这时如果你要强行删除的话可以使用命令`git branch -D xxx`. 如何删除远程分支呢? + ``` git branch -r -d origin/developer git push origin :developer ``` 如何本地创建分支并推送给远程仓库? + ``` // 本地创建分支 git checkout master //进入master分支 @@ -474,43 +520,39 @@ git checkout -b frommaster //以master为源创建分支frommaster // 推送到远程仓库 git push origin frommaster// 推送到远程仓库所要使用的名字 ``` - -如何切到到远程仓库分支进行开发呢? +如何切到到远程仓库分支进行开发呢? `git checkout -b frommaster origin/frommaster` // 本地新建frommaster分支并且与远程仓库的frommaster分支想关联 -提交更改的话就用 +提交更改的话就用 `git push origin frommaster` -// 重命名分支 +// 重命名分支 `git branch -m new_branch wchar_support` // 查看每一个分支的最后一次提交 `git branch -v` #### `git merge`合并指定分支到当前分支 -`git merge devBranch`将`devBranch`分支合并到`master`。 +`git merge devBranch`将`devBranch`分支合并到`master`。 -### 打`tag` -`git tag v1.0`来进行打`tag`,默认为`HEAD` -`git tag`查看所有`tag` -如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` +### 打`tag` + +`git tag v1.0`来进行打`tag`,默认为`HEAD` +`git tag`查看所有`tag` +如果我想在之前提交的某次`commit`上打`tag`,`git tag v1.0 commitID` 当然也可以在打`tag`时带上参数 `git tag v1.0 -m "version 1.0 released" commitID` `git tag -d xxx`删除xxx -`git show tagName`来查看某`tag`的详细信息。 - - -- 打完`tag`后怎么推送到远程仓库 - `git push origin tagName` +`git show tagName`来查看某`tag`的详细信息。 -- 删除`tag` - `git tag -d tagName` - -- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 - `git push origin:refs/tags/tagName` - -- 忽略文件 - 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 +- 打完`tag`后怎么推送到远程仓库 + `git push origin tagName` +- 删除`tag` + `git tag -d tagName` +- 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 + `git push origin:refs/tags/tagName` +- 忽略文件 + 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 ### amend修改最后一次提交 @@ -524,12 +566,16 @@ $ git commit --amend --no-edit #### 修改多个提交信息 -为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。 +为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交, +基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或 +做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变 +基到的提交来做到。 例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息, 将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i 命令,即 HEAD~2^ 或 HEAD~3。 记住 ~3 可能比较容易,因为你正尝试修改最后三次提交;但是注意实际上指定了以前的四次提交,即想要修改提交的父提交: $ git rebase -i HEAD~3 -再次记住这是一个变基命令——在 HEAD~3..HEAD 范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。 不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。 +再次记住这是一个变基命令——在 HEAD~3..HEAD 范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。 +不要涉及任何已经推送到中央服务器的提交——这样做会产生一次变更的两个版本,因而使他人困惑。 输入 $ git commit --amend @@ -539,26 +585,25 @@ $ git rebase --continue 这个命令将会自动地应用另外两个提交,然后就完成了。 如果需要将不止一处的 pick 改为 edit,需要在每一个修改为 edit 的提交上重复这些步骤。 每一次,Git 将会停止,让你修正提交,然后继续直到完成。 ### 查看远程仓库克隆地址 -`git remote -v` -关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) +`git remote -v` -从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 +关于`git`的工作区、缓存区可以看下图`index`标记部分的区域就是暂存区 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_stage.jpg?raw=true) +从这个图中能看到缓存区的存在,这就是为什么我们新加或者修改之后都要调用`git add`方法后再调用`git commit`。 ### stash(贮藏) stash会处理工作目录的脏的文件--即跟踪文件的修改与暂存的改动--然后将未完成的修改保存到一个栈上,而你可以在任何时候重新应用这些改动(甚至在不同的分支上)。 -假设你现在在`a`分支上开发新版本内容,已经开发了一部分,但是还没有达到可以提交的程度。你需要切换到`b`分支进行另一个升级的开发。那么可以把当前工作的改变隐藏起来,要将一个新的存根推到堆栈上,运行`git stash`命令。 +假设你现在在`a`分支上开发新版本内容,已经开发了一部分,但是还没有达到可以提交的程度。你需要切换到`b`分支进行另一个升级的开发。那么可以把当前工作的改变隐藏起来,要将一个新的存根推到堆栈上,运行`git stash`命令。 ```shell $ git stash Saved working directory and index state WIP on master: ef07ab5 synchronized with the remote repository HEAD is now at ef07ab5 synchronized with the remote repository ``` - -现在,工作目录是干净的,所有更改都保存在堆栈中。 现在使用`git status`命令来查看当前工作区状态: +现在,工作目录是干净的,所有更改都保存在堆栈中。 现在使用`git status`命令来查看当前工作区状态: ```shell $ git status @@ -567,95 +612,122 @@ Your branch is up-to-date with 'origin/master'. nothing to commit, working directory clean ``` - 现在,可以安全地切换分支并在其他地方工作。通过使用`git stash list`命令来查看已存在更改的列表。 ```shell $ git stash list stash@{0}: WIP on master: ef07ab5 synchronized with the remote repository ``` - -这个命令所储藏的修改可以使用`git stash list`列出,使用`git stash show`进行检查,并使用`git stash apply`或`git stash apply stash@{2}`恢复(可能在不同的提交之上)。或者可以用git stash pop将最近的一次stash恢复。调用没有任何参数的`git stash`相当于`git stash save`。在17年10月下旬Git讨论废弃了git stash save命令,代之以现有的git stash push命令。 +这个命令所储藏的修改可以使用`git stash list`列出,使用`git stash show`进行检查,并使用`git stash apply` +或`git stash apply stash@{2}`恢复(可能在不同的提交之上)。或者可以用git stash pop将最近的一次stash恢复。调用没有任何参数 +的`git stash`相当于`git stash save`。在17年10月下旬Git讨论废弃了git stash save命令,代之以现有的git stash push命令。 可以使用git stash drop加上要移除的贮藏的名字来移除它。 +### 区间提交 +你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。 你可以使用 master..experiment 来让 Git 显示这些提交。也就是“在 experiment 分支中而不在 master 分支中的提交”。 -### 区间提交 -你想要查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。 你可以使用 master..experiment 来让 Git 显示这些提交。也就是“在 experiment 分支中而不在 master 分支中的提交”。 ```shell git log master..experiment ``` #### 三点 + 这个语法可以选择出被两个引用 之一 包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行: + ```shell git log master...experiment ``` - - ### Rebase操作 + 官网中将rebase翻译为变基,我感觉理解成改变基点,重新实现更容易理解一些。 -假设目前除master分支之外还有一个experiment分支: +假设目前除master分支之外还有一个experiment分支: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-1.png?raw=true) +我们现在想要把master分支merge一下experiment分支的最新代码。整合分支最容易的方法是merge命令。它会把这两个分支的最新快照(C3和C4)以及两者 +最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5)并提交。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-2.png?raw=true) +其实,还有一种方法:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次。在Git中,这种操作就叫做变基(rebase)。 +你可以使用rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。 +在这个例子中,你可以检出experiment分支,然后将它变基到master分支上: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-1.png?raw=true) -我们现在想要把master分支merge一下experiment分支的最新代码。整合分支最容易的方法是merge命令。它会把这两个分支的最新快照(C3和C4)以及两者最近的共同祖先(C2)进行三方合并,合并的结果是生成一个新的快照(C5)并提交。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-2.png?raw=true) -其实,还有一种方法:你可以提取在C4中引入的补丁和修改,然后在C3的基础上应用一次。在Git中,这种操作就叫做变基(rebase)。你可以使用rebase命令将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。 -在这个例子中,你可以检出experiment分支,然后将它变基到master分支上: ```shell git checkout experiment git rebase master ``` -它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 -![将C4中的修改变基到C3上](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-3.png?raw=true) +它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖 +先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 +![将C4中的修改变基到C3上](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-3.png?raw=true) 现在回到master分支,进行一次快进合并。 + ```shell git checkout master git merge experiment ``` -![master分支的快进合并](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-4.png?raw=true) -此时C4'指向的快照就和上面直接用merge中的C5指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。 -一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁--例如向某个他人维护的项目贡献代码时。在这种情况下,你首先在自己的分支里进行开发,当开发完成时你需要先将你的代码变基到orgin/master上,然后再向主项目提交修改。这样的话,该项目的维护者就不再需要进行整合工作,只需要快进合并即可。 -请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。 +![master分支的快进合并](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-4.png?raw=true) +此时C4'指向的快照就和上面直接用merge中的C5指向的快照一模一样了。这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 +你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的,但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。 +一般我们这样做的目的是为了确保在向远程分支推送时能保持提交历史的整洁--例如向某个他人维护的项目贡献代码时。在这种情况下,你首先在自己 +的分支里进行开发,当开发完成时你需要先将你的代码变基到orgin/master上,然后再向主项目提交修改。这样的话,该项目的维护者就不再需要 +进行整合工作,只需要快进合并即可。 +请注意,无论是通过变基,还是通过三方合并,整合的最终结果所指向的快照始终是一样的,只不过提交历史不同罢了。变基是将一系列提交按照原有 +次序依次应用到另一分支上,而合并是把最终结果合在一起。 ##### 更有趣的变基例子 -在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。假如你创建了一个主题分支server,为服务端添加了一些功能,提交了C3和C4.然后从C3上创建了主题分支client,为客户端添加了一些功能,提交了C8和C9.最后,你回到server分支,又提交了C10. -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-1.png?raw=true) -假设你希望将client中的修改合并到主分支并发布,但暂时并不想合并server中的修改,因为他们还需要经过更全面的测试。这时,你就可以使用git rebase命令的--onto选项,选中在client分支里但不在server分支里的修改(即C8和C9),将它们在master分支上重放: `git rebase --onto master server client` -以上命令的意思是:取出client分支,找出它从server分支分歧之后的补丁,然后把这些补丁在master分支上重放一遍,让client看起来像直接基于master修改一样。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-2.png?raw=true) + +在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。假如你创建了一个主题分支server, +为服务端添加了一些功能,提交了C3和C4.然后从C3上创建了主题分支client,为客户端添加了一些功能,提交了C8和C9.最后,你回到server分支, +又提交了C10. +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-1.png?raw=true) +假设你希望将client中的修改合并到主分支并发布,但暂时并不想合并server中的修改,因为他们还需要经过更全面的测试。这时,你就可以使用 +git rebase命令的--onto选项,选中在client分支里但不在server分支里的修改(即C8和C9), +将它们在master分支上重放: `git rebase --onto master server client` +以上命令的意思是:取出client分支,找出它从server分支分歧之后的补丁,然后把这些补丁在master分支上重放一遍,让client看起来像直接 +基于master修改一样。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-2.png?raw=true) 现在可以快速合并master分支了(快速合并master分支,使之包含来自client分支的修改): + ```shell git checkout master git merge client ``` -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-3.png?raw=true) -接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-3.png?raw=true) +接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支 +(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 `git rebase master server` 如下图,将server中的修改变基到master上所示,server中的代码被“续”到了master后面。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-4.png?raw=true) -然后就可以快进合并主分支master了: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-4.png?raw=true) +然后就可以快进合并主分支master了: + ```shell git checkout master git merge server ``` -至此,client和server分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成下图的样子: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-5.png?raw=true) +至此,client和server分支中的修改都已经整合到主分支里了,你可以删除这两个分支,最终提交历史会变成下图的样子: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-5.png?raw=true) ##### 变基的风险 -奇妙的变基也并非完美无缺,要用它得遵守一条准则: + +奇妙的变基也并非完美无缺,要用它得遵守一条准则: 如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。 -变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用git rebase命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉去并整合他们修改过的提交,事情就会变的一团糟。 -让我们来看一个在公开仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史如下图: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-1.png?raw=true) -然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交记录就会变成这样: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-2.png?raw=true) -接下来,这个人又决定把合并操作回滚,改用变基。继而又用git push --force命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现多出来一些新的提交: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-3.png?raw=true) -结果就是你们两个人的处境都十分尴尬。如果你执行git pull命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库也会变成: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-4.png?raw=true) -这相当于是你将相同的内容又合并了一次,生成了一个新的提交。此时如果你执行git log命令,你会发现有两个调的作者、日期、日志居然是一样的,这会令人感到混乱。此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。很明显对方并不想在提交历史中看到C4和C6,因为之前就是他把这两个提交丢弃的。 +变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。如果你已经将提交推送至某个仓库,而其他人也已经从该 +仓库拉取提交并进行了后续工作,此时,如果你用git rebase命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的 +提交进行整合,如果接下来你还要拉去并整合他们修改过的提交,事情就会变的一团糟。 +让我们来看一个在公开仓库上执行变基操作所带来的问题。假设你从一个中央服务器克隆然后在它的基础上进行了一些开发。你的提交历史如下图: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-1.png?raw=true) +然后,某人又向中央服务器提交了一些修改,其中还包括一次合并。你抓取了这些在远程分支上的修改,并将其合并到你本地的开发分支,然后你的提交 +记录就会变成这样: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-2.png?raw=true) +接下来,这个人又决定把合并操作回滚,改用变基。继而又用git push --force命令覆盖了服务器上的提交历史。之后你从服务器抓取更新,会发现 +多出来一些新的提交: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-3.png?raw=true) +结果就是你们两个人的处境都十分尴尬。如果你执行git pull命令,你将合并来自两条提交历史的内容,生成一个新的合并提交,最终仓库也会变成: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-4.png?raw=true) +这相当于是你将相同的内容又合并了一次,生成了一个新的提交。此时如果你执行git log命令,你会发现有两个调的作者、日期、日志居然是一样的, +这会令人感到混乱。此外,如果你将这一堆又推送到服务器上,你实际上是将那些已经被变基抛弃的提交又找了回来,这会令人感到更加混乱。 +很明显对方并不想在提交历史中看到C4和C6,因为之前就是他把这两个提交丢弃的。 ##### 用变基解决变基 + 如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。 @@ -670,7 +742,7 @@ git merge server - 把查到的这些提交应用在 teamone/master 上面 从而我们将得到与 你将相同的内容又合并了一次,生成了一个新的提交 中不同的结果,如图 在一个被变基然后强制推送的分支上再次执行变基 所示。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-5.png?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/perils-of-rebasing-5.png?raw=true) 要想上述方案有效,还需要对方在变基时确保 C4' 和 C4 是几乎一样的。 否则变基操作将无法识别,并新建另一个类似 C4 的补丁(而这个补丁很可能无法整洁的整合入历史,因为补丁中的修改已经存在于某个地方了)。 在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master。 @@ -680,40 +752,44 @@ git merge server 如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。 #### 变基 vs. 合并 -至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史到底意味着什么。 -有一种观点认为,仓库的提交历史即是 记录实际发生过什么。 它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是一种亵渎,你使用 谎言 掩盖了实际发生过的事情。 如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。 +至此,你已在实战中学习了变基和合并的用法,你一定会想问,到底哪种方式更好。 在回答这个问题之前,让我们退后一步,想讨论一下提交历史 +到底意味着什么。 -另一种观点则正好相反,他们认为提交历史是 项目过程中发生的事。 没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。 +有一种观点认为,仓库的提交历史即是记录实际发生过什么。它是针对历史的文档,本身就有价值,不能乱改。 从这个角度看来,改变提交历史是 +一种亵渎,你使用谎言掩盖了实际发生过的事情。如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来, +让后人能够查阅。 -现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。 - -总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。 +另一种观点则正好相反,他们认为提交历史是项目过程中发生的事。没人会出版一本书的第一版草稿,软件维护手册也是需要反复修订才能方便使用。 +持这一观点的人会使用 rebase 及 filter-branch 等工具来编写故事,怎么方便后来的读者就怎么写。 +现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对 +提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。 +总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两 +种方式带来的便利。 ### git fetch与git pull的区别 -`git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 +`git`中`fetch`命令是将远程分支的最新内容拉到了本地,但是`fecth`后是看不到变化的,如果查看当前的分支,会发现此时本地多了 +一个`FETCH_HEAD`的指针,`checkout`到该指针后才可以查看远程分支的最新内容。 而`git pull`的作用相当于`fetch`和`merge`的组合,会自动合并: -```git +```git git fetch origin master git merge FETCH_HEAD ``` - ### git pull 与git pull --rebase的使用 -使用下面的关系区别这两个操作: +使用下面的关系区别这两个操作: ```git git pull = git fetch + git merge git pull --rebase = git fetch + git rebase ``` -`git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后,用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 - - +`git rebase`的过程中,有时会有`conflit`这时`Git`会停止`rebase`并让用户去解决冲突,解决完冲突后, +用`git add`命令去更新这些内容,然后不用执行`git commit`,直接执行`git rebase --continue`这样`git`会继续`apply`余下的补丁。 ## 参考 @@ -721,10 +797,5 @@ git pull --rebase = git fetch + git rebase --- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - -​ - +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" index 5ebc846c..f610438e 100644 --- "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -9,17 +9,31 @@ HashMap基于Map接口实现,元素以键值对的方式存储,并且允许 其底层数据结构是数组称之为哈希桶,每个桶(bucket)里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。 -通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过 Load Facotr则resize为原来的2倍)。获取对象时,我们 K传给get,它调用hashCodeO()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在JDK8中,如果一个bucket中碰撞冲突的元素超过8哥,则使用红黑树来替换链表,从而提高速度。 - -因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。当HashMap的容量达到threshold域值时,就会触发扩容。扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取得很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量》=8,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时,顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 - - - - +通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash +从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过Load Facotr则resize为原来的2倍)。 +获取对象时,我们将K传给get()方法,它调用hashCodeO()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 +如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来,在JDK8中,如果一个bucket中碰撞冲突的元素超过8个, +则使用红黑树来替换链表,从而提高速度。 + +因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。当HashMap的容量达到threshold域值时,就会触发扩容。 +扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。 +而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。 +因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取的很好, +每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余, +以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算, +发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 +扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算, +以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶, +然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大, +可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值, +依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标, +即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量》=8,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, +顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 ## JDK1.7 -HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算,哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 +HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算, +哈希运算会有重复的哈希值,对于哈希值的冲突,HashMap采用链表来解决的。 在HashMap里有这样的一句属性声明: ```java @@ -28,28 +42,25 @@ transient Entry[] table; 可以看到Map是通过数组的方式来储存Entry那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: ```java static class Entry implements Map.Entry { - final K key; - V value; - Entry next; - final int hash; - ...//More code goes here + final K key; + V value; + Entry next; + final int hash; + ...//More code goes here } ``` -看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry,那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的Entry的next属性。数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 +看到next了吗?next就是为了哈希冲突而存在的。比如通过哈希运算,一个新元素应该在数组的第10个位置,但是第10个位置已经有Entry, +那么好吧,将新加的元素也放到第10个位置,将第10个位置的原有Entry赋值给当前新加的Entry的next属性。 +数组存储的是链表,链表是为了解决哈希冲突的,这一点要注意。 好了,总结一下: - HashMap中有一个叫table的Entry数组。这个数组存储了Entry类的对象。Entry类包含了key-value作为实例变量。 - - table的索引在逻辑上叫做“桶”(bucket),它存储了链表的第一个元素。 - -- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 - +- 每当往Hashmap里面存放key-value对的时候,都会为它们实例化一个Entry对象,这个Entry对象就会存储在前面提到的Entry数组table中。 根据key的hashcode()方法计算出来的hash值来决定在Entry数组的索引(所在的桶)。 - 如果两个key有相同的hash值,他们会被放在table数组的同一个桶里面。 - - key的equals()方法用来确保key的唯一性。 - 接下来看一下put方法: @@ -70,33 +81,33 @@ static class Entry implements Map.Entry { */ public V put(K key, V value) { // 对key做null检查。如果key是null,会被存储到table[0],因为null的hash值总是0。 - if (key == null) - return putForNullKey(value); - // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 - int i = indexFor(hash, table.length); - - for (Entry e = table[i]; e != null; e = e.next) { - // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 - Object k; - // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { - // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), - // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 - V oldValue = e.value; - e.value = value; - e.recordAccess(this); - return oldValue; - } - } - // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 - modCount++; - addEntry(hash, key, value, i); - return null; + if (key == null) + return putForNullKey(value); + // 计算key的hash值,hash值用来找到存储Entry对象的数组的索引。有时候hash函数可能写的很不好,所以JDK的设计者添加了另一个叫做hash()的方法,它接收刚才计算的hash值作为参数 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算在table数组中存储Entry对象的精确的索引 + int i = indexFor(hash, table.length); + + for (Entry e = table[i]; e != null; e = e.next) { + // 如果这个位置已经有了(也就是hash值一样)就用链表来存了。 开始迭代链表 + Object k; + // 直到Entry->next为null,就把当前的Entry对象变成链表的下一个节点。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + // 如果我们再次放入同样的key会怎样呢?它会替换老的value。在迭代的过程中,会调用equals()方法来检查key的相等性(key.equals(k)), + // 如果这个方法返回true,它就会用当前Entry的value来替换之前的value。 + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + // 如果计算出来的索引位置没有元素,就直接把Entry对象放到那个索引上。 + modCount++; + addEntry(hash, key, value, i); + return null; } ``` -再看一下get方法:     +再看一下get方法: ```java /** * Returns the value to which the specified key is mapped, or {@code null} @@ -118,31 +129,33 @@ public V put(K key, V value) { */ public V get(Object key) { // 如果key是null,table[0]这个位置的元素将被返回。 - if (key == null) - return getForNullKey(); - // 计算hash值 - int hash = hash(key.hashCode()); - // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 - for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { - Object k; - // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 - if (e.hash == hash && ((k = e.key) == key || key.equals(k))) - return e.value; - } - return null; + if (key == null) + return getForNullKey(); + // 计算hash值 + int hash = hash(key.hashCode()); + // indexFor(hash,table.length)用来计算要获取的Entry对象在table数组中的精确的位置,使用刚才计算的hash值。 + for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { + Object k; + // 在获取了table数组的索引之后,会迭代链表,调用equals()方法检查key的相等性,如果equals()方法返回true,get方法返回Entry对象的value,否则,返回null。 + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) + return e.value; + } + return null; } ``` - - ## JDK1.8 -在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 +在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方, +数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。 +利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。HashMap中, +如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1), +如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中, +或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_data_structure.jpg) - 既然发生了变化,那这里就以1.8的源码基础上再做一下分析: 1. 首先看一下对应的两个Node类: @@ -167,7 +180,7 @@ public V get(Object key) { public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } - // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 + // 每一个节点的hashcode值,是将key和value的hashCode值异或得到的 public final int hashCode() { return Objects.hashCode(key) ^ Objects.hashCode(value); } @@ -217,7 +230,7 @@ public V get(Object key) { * @return root of tree */ final void treeify(Node[] tab) { - ... + ... } /** @@ -229,7 +242,7 @@ public V get(Object key) { } /** - * 红黑树的插入 + * 红黑树的插入 * Tree version of putVal. */ final TreeNode putTreeVal(HashMap map, Node[] tab, @@ -285,7 +298,7 @@ public V get(Object key) { ```java public class HashMap extends AbstractMap implements Map, Cloneable, Serializable { - // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 + // 默认的初始化容量大小,必须是2的倍数,默认是16,为啥必须是2的倍数,后面会说 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 // 在构造函数中未指定时使用的负载因子。 static final float DEFAULT_LOAD_FACTOR = 0.75f; @@ -329,7 +342,10 @@ public V get(Object key) { ### 确定哈希桶数组索引位置 -不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合,所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的离散性能。先看看源码的实现: +不管增加、删除、查找键值对,定位到哈希桶数组的位置都是很关键的第一步。前面说过HashMap的数据结构是数组和链表的结合, +所以我们当然希望这个HashMap里面的元素位置尽量分布均匀些,尽量使得每个位置上的元素数量只有一个,那么当我们用hash算法求得这个位置的 +时候,马上就可以知道对应位置的元素就是我们要的,不用遍历链表,大大优化了查询的效率。HashMap定位数组索引位置,直接决定了hash方法的 +离散性能。先看看源码的实现: ```java static final int hash(Object key) { @@ -338,18 +354,22 @@ static final int hash(Object key) { } ``` -hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? 主要有两个原因 : +hash函数是先拿到通过key的hashcode,是32位的int值,然后让hashcode的高16位和低16位进行异或操作。为什么要这样设计呢? +主要有两个原因 : 1. 一定要尽可能降低hash碰撞,越分散越好。 2. 算法一定要尽可能高效,因为这是高频操作,因此采用位运算。 -因为hashcode是32位的int值int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算,得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。 - - +因为hashcode是32位的int值int值范围为**-2147483648~2147483647**,前后加起来大概40亿的映射空间。只要哈希函数映射得比较均匀松散, +一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。你想,如果HashMap数组的初始大小才16,用之前需要对数组的长度取模运算, +得到的余数才能用来访问数组下标。源码中模运算就是把散列值和数组长度-1做一个"与"操作,位运算比%运算要快。 - -对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用所计算得到的Hash码值总是相同的。我们首先想到的就是把hash值对数组长度取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的,在HashMap中是这样做的:调用 (table.length -1) & hash来计算该对象应该保存在table数组的哪个索引处。这个方法非常巧妙,它通过 (table.length -1) & hash来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方,当length总是2的n次方时, (table.length -1) & hash运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 +对于任意给定的对象,只要它的hashCode()返回值相同,那么程序调用所计算得到的Hash值总是相同的。我们首先想到的就是把hash值对数组长度 +取模运算,这样一来,元素的分布相对来说是比较均匀的。但是,模运算的消耗还是比较大的, +在HashMap中是这样做的:调用 (table.length -1) & hash来计算该对象应该保存在table数组的哪个索引处。 +这个方法非常巧妙,它通过 (table.length -1) & hash来得到该对象的保存位,而HashMap底层数组的长度总是2的n次方, +当length总是2的n次方时, (table.length -1) & hash运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。 当length总是2的倍数时,h & (length-1) 将是一个非常巧妙的设计: @@ -359,16 +379,16 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc - 但是当h=16时 , length=16 时,那么h & length - 1将得到0了; - 当h=17时, length=16时,那么h & length - 1将得到1了。 -这样保证计算得到的索引值总是位于 table 数组的索引之内。 - +这样保证计算得到的索引值总是位于table数组的索引之内。 -在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。 +在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16), +主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中, +同时不会有太大的开销。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.bmp) - ### put ```java @@ -389,7 +409,7 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc // 3. 如果tab[索引]的值为null,那就说明这个索引没有数组桶,直接新建一个数组桶 tab[i] = newNode(hash, key, value, null); else { - // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 + // 4. 否则就是目前已经存在该索引的数组桶了,要继续判断是链表还是红黑树。 Node e; K k; // 5. 判断table[索引]处的收个元素是否和key一样,如果首个就是那就直接覆盖value,不用再找了 if (p.hash == hash && @@ -400,7 +420,7 @@ hash函数是先拿到通过key 的hashcode,是32位的int值,然后让hashc // 7. 数组桶首个元素不是,并且链表是红黑树,红黑树直接插入键值对 e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value); else { - // 8. 目前为链表,开始遍历链表准备插入 + // 8. 目前为链表,开始遍历链表准备插入 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { // 添加到目前的节点的next上 @@ -475,27 +495,19 @@ final void treeifyBin(Node[] tab, int hash) { resize()方法用于初始化数组或数组扩容,每次扩容后容量为原来的2倍,并进行数据迁移。 - -例如我们从 16 扩展为 32 时,具体的变化如下所示: - +例如我们从16扩展为32时,具体的变化如下所示: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize1.bmp) - - - -因此元素在重新计算hash之后,因为n变为 2 倍,那 n-1的mask范围在高位多1bit (红色),因此新的 index就会发生这样的变化: - +因此元素在重新计算hash之后,因为n变为2倍,那n-1的mask范围在高位多1bit (红色),因此新的index就会发生这样的变化: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize2.bmp) - -因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: - +因此,我们在扩充HashMap的时候,不需要重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了, +是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: ![resize](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize3.bmp) - - -这个设计确实非常的巧妙,既省去了重新计算 hash 值的时间,而且同时,**由于新增的 1bit 是 0 还是 1 可以认为是随机的,因此 resize 的过程,均匀的把之前的冲突的节点分散到新的 bucket 了**。 +这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,**由于新增的1bit是0还是1可以认为是随机的,因此resize的过程, +均匀的把之前的冲突的节点分散到新的bucket了**。 ```java final Node[] resize() { @@ -602,7 +614,8 @@ final Node[] resize() { } ``` -再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。如果evict是false。那么表示是在初始化时调用的 +再看一下往哈希表里插入一个节点的putVal函数,如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value。 +如果evict是false,那么表示是在初始化时调用的。 ```java final V putVal(int hash, K key, V value, boolean onlyIfAbsent, @@ -673,7 +686,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, * 运算尽量都用位运算代替,更高效。 * 对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC -* 取下标 是用 哈希值 与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 +* 取下标 是用哈希值与运算 (桶的长度-1) i = (n - 1) & hash。 由于桶的长度是2的n次方,这么做其实是等于 一个模运算。但是效率更高 * 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 * 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 * 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 @@ -716,7 +729,7 @@ final Node getNode(int hash, Object key) { -## JDK 7 与 JDK 8 中关于 HashMap的对比 +## JDK 7与JDK 8中关于HashMap的对比 1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,便会树化。 2. hash值的计算方式不同 (jdk 8 简化)。 @@ -727,25 +740,33 @@ final Node getNode(int hash, Object key) { - - ## 问题 1. 为什么capcity是2的幂? - 因为算index时用的是(n-1) & hash,这样就能保证n-1是全为1的二进制数,如果不全为1的话,存在某一位为 0,那么0,1与0与的结果都是0,这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。这是为了服务key映射到index的Hash算法的,公式index=hashcode(key)&(length-1),初始长度(16-1),二进制为1111&hashcode结果为hashcode最后四位,能最大程度保持平均,二的幂数保证二进制为1,保持hashcode最后四位。这种算法在保持分布均匀之外,效率也非常高。 + + 因为算index时用的是(n-1) & hash,这样就能保证n-1是全为1的二进制数,如果不全为1的话,存在某一位为0,那么0,1与0与的结果都是0, + 这样便有可能将两个hash不同的值最终装入同一个桶中,造成冲突。所以必须是2的幂。这是为了服务key映射到index的Hash算法的, + 公式index=hashcode(key)&(length-1),初始长度(16-1),二进制为1111&hashcode结果为hashcode最后四位,能最大程度保持平均,二的幂数保证二进制为1,保持hashcode最后四位。这种算法在保持分布均匀之外,效率也非常高。 2. 为什么需要使用加载因子,为什么需要扩容呢? - 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多),扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap 本来是以空间换时间,所以填充比没必要太大。但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 + 因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,因为链表的长度很大(当然最新版本使用了红黑树后会改进很多), + 扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。HashMap本来是以空间换时间,所以填充比没必要太大。 + 但是填充比太小又会导致空间浪费。如果关注内存,填充比可以稍大,如果主要关注查找性能,填充比可以稍小。 3. 为什么 HashMap 是线程不安全的,实际会如何体现? - - 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现,这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 -- 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。且会引起死循环的错误。 + 如果多个线程同时使用put方法添加元素,假设正好存在两个put的key发生了碰撞 (hash值一样),那么根据HashMap的实现, + 这两个key会添加到数组的同一个位置,这样最终就会发生其中一个线程的put的数据被覆盖。 + 如果多个线程同时检测到元素个数超过数组大小 * loadFactor。这样会发生多个线程同时对hash数组进行扩容,都在重新计算元素位置 + 以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失。 + 且会引起死循环的错误。 4. 与HashTable的区别 - Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 + Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable, + 它的并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换, + 需要线程安全的场合可以用ConcurrentHashMap替换。 - 与之相比HashTable是线程安全的,且不允许key、value是null。 - HashTable默认容量是11。 @@ -754,15 +775,12 @@ final Node getNode(int hash, Object key) { - 扩容时,新容量是原来的2倍+1。int newCapacity = (oldCapacity << 1) + 1; - Hashtable是Dictionary的子类同时也实现了Map接口,HashMap是Map接口的一个实现类 -5. 扩容的时候为什么1.8 不用重新hash就可以直接定位原节点在新数据的位置呢? +5. 扩容的时候为什么1.8不用重新hash就可以直接定位原节点在新数据的位置呢? 这是由于扩容是扩大为原数组大小的2倍,用于计算数组位置的掩码仅仅只是高位多了一个1,举个例子: - - 扩容前长度为16,用于计算 (n-1) & hash 的二进制n - 1为0000 1111, - - 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是& 运算,1和任何数 & 都是它本身,那就分二种情况,如下图:原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 - - + 扩容前长度为16,用于计算 (n-1) & hash 的二进制n - 1为0000 1111, + 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是&运算,1和任何数&都是它本身,那就分二种情况, + 原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 参考: @@ -773,11 +791,8 @@ final Node getNode(int hash, Object key) { - - - --- - 邮箱 :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 417c7785..e24c20ec 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" @@ -39,12 +39,12 @@ HTTP与HTTPS的区别 你是不是很好奇,当你在浏览器中输入网址后,到底发生了什么事情?你想要的内容是如何展现出来的?让我们通过一个例子来探讨一下,我们假设访问的 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 客户端会把对应的资源通过显示器呈现给用户。 +- 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客户端会把对应的资源通过显示器呈现给用户。 至此,键入网址再按下回车的全过程就结束了。上述过程描述的是一种简单的`请求-响应`全过程,真实的请求-响应情况可能要比上面描述的过程复杂很多。 @@ -190,4 +190,4 @@ HTTP + 加密 + 认证 + 完整性保护 = HTTPS - 邮箱 :charon.chui@gmail.com - Good Luck! - \ No newline at end of file + \ No newline at end of file diff --git "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" index 90eb117e..1e2fae6a 100644 --- "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" +++ "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -1,12 +1,16 @@ Java内存模型 === -Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它管理的内存划分为五个不同的数据区域(Heap Memory、Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各自的用途、创建时间、销毁时间: +Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它管理的内存划分为五个不同的数据区域(Heap Memory、 +Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各自的用途、创建时间、销毁时间: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_memory_model.jpeg) ## Program Counter Register -程序计数器是一块较小的内存空间,严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。可以看作是当前线程所执行的字节码的行号指示器。 +程序计数器是一块较小的内存空间,严格来说是一个数据结构,用于保存当前正在执行的程序的内存地址,由于Java是支持多线程执行的,所以程序执行的轨迹 +不可能一直都是线性执行。当有多个线程交叉执行时,被中断的线程的程序当前执行到哪条内存地址必然要保存下来,以便用于被中断的线程恢复执行时再按照 +被中断时的指令地址继续执行下去。为了线程切换后能恢复到正确的执行位置,每个线程都需要有一个独立的程序计数器,各个线程之间计数器互不影响,独立存储, +我们称这类内存区域为“线程私有”的内存,这在某种程度上有点类似于“ThreadLocal”,是线程安全的。可以看作是当前线程所执行的字节码的行号指示器。 从上面的介绍中我们知道程序计数器主要有两个作用: @@ -22,12 +26,11 @@ Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它 所有的对象实例和数组都存放到Heap内存中。Heap内存也称为共享内存。多线程可以共享这里面的数据。 - 堆内存在JVM启动的时候被加载(初始大小: -Xms) - - 堆内存在程序运行时会增加或减少 - - 最小值: -Xmx - -- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 +- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先 + 都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制 + 过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_heap_memory.png) @@ -35,7 +38,7 @@ Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它 ## Stack Memory -栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的线程栈,线程stack中包含有关线程调用了哪些方法以达到当前执行点的信息。,也可以称之为调用栈,只要线程执行代码,调用栈就会发生改变。在这个栈中又会包含多个栈帧(Stack Frame),栈是由一个个栈帧组成,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量表、操作栈和方法返回值等信息。 +栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的线程栈,线程stack中包含有关线程调用了哪些方法以达到当前执行点的信息,也可以称之为调用栈,只要线程执行代码,调用栈就会发生改变。在这个栈中又会包含多个栈帧(Stack Frame),栈是由一个个栈帧组成,这些栈帧是与每个方法关联起来的,每运行一个方法就创建一个栈帧,每个栈帧会含有一些局部变量表、操作栈和方法返回值等信息。 局部变量表主要存放了编译器可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。 @@ -51,8 +54,8 @@ Java虚拟机(Java Virtual Machine)在执行Java程序的过程中,会把它 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/stack_heap.png) - 局部变量可以是原始类型,在这种情况下,它完全保留在线程堆栈中。 -- 局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈中,但是对象本身(如果存储在堆中)。 -- 一个对象可能包含方法,而这些方法可能包含局部变量。即使该方法所属的对象存储在堆内存中,这些局部变量也存储在线程堆内存中。 +- 局部变量也可以是对对象的引用。在这种情况下,引用(局部变量)存储在线程堆栈中,但是对象本身(如果有)存储在堆中。 +- 一个对象可能包含方法,而这些方法可能包含局部变量。即使该方法所属的对象存储在堆中,这些局部变量也存储在线程堆栈中。 - 对象的成员变量与对象本身一起存储在堆中。不管成员变量是原始类型时,以及它是对对象的引用时,都是如此。 - 静态类变量也与类定义一起存储在堆中。 - 引用对象的所有线程都可以访问堆上的对象。当线程可以访问对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都将有权访问该对象的成员变量,但是每个线程将拥有自己的局部变量副本。 @@ -92,7 +95,7 @@ Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError JVM会加载、链接、初始化class文件。一个class文件会把其所有所有符号引用都保留在一个位置,即常量池中。 -每个class文件都会有一个对应constant pool。但是class文件中的常量池显示让不够的,因为需要再JVM上执行。这种情况下,需要runtime constant pool来服务JVM的运行。 +每个class文件都会有一个对应constant pool。但是class文件中的常量池显然是不够的,因为需要再JVM上执行。这种情况下,需要runtime constant pool来服务JVM的运行。 Java虚拟机加载的每个类或接口都有其常量池的内部版本,称为运行时常量池(runtime constant pool)。运行时常量池是特定于实现的数据结构,它与类文件中的常量池是一一对应映射的。因此,在最初加载类型之后,该类型的所有符号引用都驻留在该类型的运行时常量池中。包括字符串常量,类和接口名称,字段名称以及类中引用的其他常量。 @@ -109,9 +112,9 @@ System.out.println(str1==str2);//false ```java String str1 = "str"; String str2 = "ing"; - + String str3 = "str" + "ing";//常量池中的对象 -String str4 = str1 + str2; //在堆上创建的新的对象 +String str4 = str1 + str2; //在堆上创建的新的对象 String str5 = "string";//常量池中的对象 System.out.println(str3 == str4);//false System.out.println(str3 == str5);//true @@ -178,7 +181,7 @@ JMM(Java Memory Model)规定了所有的变量都存储在主内存(Main Memor 由上述对JVM内存结构的描述中,我们知道了堆和方法区是线程共享的。而局部变量,方法定义参数和异常处理器参数就不会在线程之间共享,它们不会有内存可见性问题,也不受内存模型的影响。 -Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 +Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 假设线程A与线程B之间如要通信的话,必须要经历下面2个步骤: @@ -212,7 +215,7 @@ Class Reordering { -当然不行! 因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。 +当然不行!因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。 在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。 @@ -226,7 +229,7 @@ Class Reordering { a是在栈内存中的,当main()方法执行结束,a就被清理了。但是你创建的内部类中的方法却可能在main()方法执行完成后再去执行,但是这时候局部变量已经不存在了,那怎么解决这个问题呢? 因此实际上是在访问它的副本,而不是访问原始的局部变量。 -在Java的参数传递中,当基本类型作为参数传递时,传递的是值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的;当对象作为参数传递时,传递的是对象的引用的拷贝,无论你怎么改变这个新的引用的指向,原来的引用是不会改变的(当然如果你通过这个引用改变了对象的内容,那么改变实实在在发生了)。知识点三,当final修饰基本类型变量时,不可更改其值,当final修饰引用变量时,不可更改其指向,只能更改其对象的内容。 +在Java的参数传递中,当基本类型作为参数传递时,传递的是值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的;当对象作为参数传递时,传递的是对象的引用的拷贝,无论你怎么改变这个新的引用的指向,原来的引用是不会改变的(当然如果你通过这个引用改变了对象的内容,那么改变实实在在发生了)。而当final修饰基本类型变量时,不可更改其值,当final修饰引用变量时,不可更改其指向,只能更改其对象的内容。 在Java中内部类会持有外部类的引用和方法中参数的引用,当反编译class文件后,内部类的class文件的构造函数参数中会传入外部类的对象以及方法内局部变量,不管是基本数据类型还是引用变量,如果重新赋值了,会导致内外指向的对象不一致,所以java就暴力的规定使用final,不能重新赋值。 所以用final修饰实际上就是为了变量值(数据)的一致性。 这里所说的数据一致性,对引用变量来说是引用地址的一致性,对基本类型来说就是值的一致性。 @@ -247,9 +250,9 @@ a是在栈内存中的,当main()方法执行结束,a就被清理了。但是 - [Where Has the Java PermGen Gone?](https://www.infoq.com/articles/Java-PERMGEN-Removed/) -​ +​ --- - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" index 1f3a021f..14d1a46c 100644 --- "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" +++ "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" @@ -23,14 +23,19 @@ - 计算`a+b` - 将计算结果写入内存 如果有两个线程`t1`,`t2`在进行这样的操作。`t1`在第二步做完之后还没来得及把数据写回内存就被线程调度器中断了,于是`t2`开始执行,`t2`执行完毕后`t1`又把没有完成的第三步做完。这个时候就出现了错误, - 相当于`t2`的计算结果被无视掉了。所以上面的买碘片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 + 相当于`t2`的计算结果被无视掉了。所以上面的片例子在同步`add`方法之前,实际结果总是小于预期结果的,因为很多操作都被无视掉了。 类似的,像`a++`这样的操作也都不具有原子性。所以在多线程的环境下一定要记得进行同步操作。 ## 可见性(Visibility) 可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 -在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化,当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数);同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 +在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化, +当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。 +(这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数); +同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同 +的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作 +覆盖掉了,这样会产生不可预料的结果。所以,即使有些操作时原子性的,但是如果不具有可见性,那么多个处理器中备份的存在就会使原子性失去意义。 volatile、synchronized、final都可以解决可见性问题。 @@ -38,7 +43,7 @@ volatile、synchronized、final都可以解决可见性问题。 有序性:即程序执行的顺序按照代码的先后顺序执行。 -有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序,重排序分3类 +有序性简单来说就是程序代码执行的顺序是否按照我们编写代码的顺序执行,一般来说,为了提高性能,编译器和处理器会对指令做重排序, 重排序分3类 - 编译器优化重排序,在不改变单线程程序语义的前提下,改变代码的执行顺序 - 指令集并行的重排序,对于不存在数据依赖的指令,处理器可以改变语句对应指令的执行顺序来充分利用CPU资源 @@ -77,52 +82,53 @@ public class Singleton{ } } } - return instance; + return instance; } } ``` 我们先看 instance=newSingleton() 的未被编译器优化的操作 -- 指令 1:分配一块内存 M; -- 指令 2:在内存 M 上初始化 Singleton 对象; -- 指令 3:然后 M 的地址赋值给 instance 变量。 +- 指令 1:分配一块内存M; +- 指令 2:在内存M上初始化Singleton对象; +- 指令 3:然后M的地址赋值给instance变量。 编译器优化后的操作指令 -- 指令 1:分配一块内存 M; -- 指令 2:将 M 的地址赋值给 instance 变量; -- 指令 3:然后在内存 M 上初始化 Singleton 对象。 +- 指令 1:分配一块内存M; +- 指令 2:将M的地址赋值给instance变量; +- 指令 3:然后在内存M上初始化Singleton对象。 -现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化),这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现 instance不等于null了,所以直接返回instance,而此时的 instance 是没有初始化过的。 +现在有A,B两个线程,我们假设线程A先执行getInstance()方法,当执行编译器优化后的操作指令2时(此时候未完成对象的初始化), +这时候发生了线程切换,那么线程B进入,刚好执行到第一次判断instance==nul会发现instance不等于null了,所以直接返回instance, +而此时的instance是没有初始化过的。 - - -Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块只能串行地进入。 +Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义, +而synchronized则是由“一个变量在同一时刻只允许一条线程对其进行lock操作”这条规则来获得的,这个规则决定了持有同一个锁的两个同步块 +只能串行地进入。 ## **先行发生原则:** -如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变得很啰嗦,但是我们在编写Java并发代码的时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的主要依赖。 +如果Java内存模型中所有的有序性都只靠volatile和synchronized来完成,那么有一些操作将会变的很啰嗦,但是我们在编写Java并发代码的 +时候并没有感觉到这一点,这是因为Java语言中有一个“先行发生”(Happen-Before)的原则。这个原则非常重要,它是判断数据是否存在竞争, +线程是否安全的主要依赖。 -先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。下面是Java内存模型下一些”天然的“先行发生关系,这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 +先行发生原则是指Java内存模型中定义的两项操作之间的依序关系,如果说操作A先行发生于操作B,其实就是说发生操作B之前,操作A产生的影响能 +被操作B观察到,“影响”包含了修改了内存中共享变量的值、发送了消息、调用了方法等。下面是Java内存模型下一些”天然的“先行发生关系, +这些先行发生关系无须任何同步器协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法从下列规则推导出来的话, +它们就没有顺序性保障,虚拟机可以对它们进行随意地重排序。 - 程序次序规则(Pragram Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环结构。 - - 管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面“是指时间上的先后顺序。 - - volatile变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读取操作,这里的”后面“同样指时间上的先后顺序。 - - 线程启动规则(Thread Start Rule):Thread对象的start()方法先行发生于此线程的每一个动作。 - - 线程终于规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束,Thread.isAlive()的返回值等作段检测到线程已经终止执行。 - - 线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生。 - - 对象终结规则(Finalizer Rule):一个对象初始化完成(构造方法执行完成)先行发生于它的finalize()方法的开始。 - - 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。 -一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的,一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。 +一个操作”时间上的先发生“不代表这个操作会是”先行发生“,那如果一个操作”先行发生“是否就能推导出这个操作必定是”时间上的先发生“呢?也是不成立的, +一个典型的例子就是指令重排序。所以时间上的先后顺序与先生发生原则之间基本没有什么关系,所以衡量并发安全问题一切必须以先行发生原则为准。 ```java int i = 0; @@ -130,9 +136,13 @@ boolean flag = false; i = 1; //语句1 flag = true; //语句2 ``` -上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? +上面代码定义了一个`int`型变量,定义了一个`boolean`类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的, +那么`JVM`在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗? 不一定,为什么呢?这里可能会发生指令重排序`(Instruction Reorder)`。 -下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢? +下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序 +同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。比如上面的代码中,语句1和语句2谁先执行对最终的程序结果 +并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会 +和代码顺序执行结果相同,那么它靠什么保证的呢? 再看下面一个例子: ```java @@ -158,9 +168,10 @@ while(!inited ){ } doSomethingwithconfig(context); ``` -上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为初始化工作已经完成, -那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。 -从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 +上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此时线程2会以为 +初始化工作已经完成, 那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化, +就会导致程序出错。 从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。也就是说,要想并发程序正确地执 +行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。 @@ -168,4 +179,4 @@ doSomethingwithconfig(context); - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/JavaKnowledge/MD5\345\212\240\345\257\206.md" "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" index 0d2b6bce..62a8dd01 100644 --- "a/JavaKnowledge/MD5\345\212\240\345\257\206.md" +++ "b/JavaKnowledge/MD5\345\212\240\345\257\206.md" @@ -1,11 +1,11 @@ MD5加密 -=== +======= -`MD5`是一种不可逆的加密算法只能将原文加密,不能讲密文再还原去,原来把加密后将这个数组通过`Base64`给变成字符串, +`MD5`是一种不可逆的加密算法只能将原文加密,不能将密文再还原回去,原来把加密后将这个数组通过`Base64`给变成字符串, 这样是不严格的业界标准的做法是对其加密之后用每个字节`&15`然后就能得到一个`int`型的值,再将这个`int`型的值变成16进制的字符串.虽然MD5不可逆, 但是网上出现了将常用的数字用`md5`加密之后通过数据库查询,所以`MD5`简单的情况下仍然可以查出来,一般可以对其多加密几次或者`&15`之后再和别的数运算等, 这称之为*加盐*. - + ```java public class MD5Utils { /** @@ -35,6 +35,7 @@ public class MD5Utils { } ``` ----- -- 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" index 9dd6ce0a..c4ed28a2 100644 --- "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -10,12 +10,10 @@ 这三种缓存的技术难度和制造成本是相对递减的,所以其容量也是相对递增的。 -那么,在有了多级缓存之后,程序的执行就变成了: +那么,在有了多级缓存之后,程序的执行就变成了: 当CPU要读取一个数据时,首先从一级缓存中查找,如果没有找到再从二级缓存中查找,如果还是没有就从三级缓存或内存中查找。 - - > 这就像一家创业公司,刚开始,创始人和员工之间工作关系其乐融融,但是随着创始人的能力和野心越来越大,逐渐和员工之间出现了差距,普通员工原来越跟不上CEO的脚步。老板的每一个命令,传到到基层员工之后,由于基层员工的理解能力、执行能力的欠缺,就会耗费很多时间。这也就无形中拖慢了整家公司的工作效率。 > > 之后,这家公司开始设立中层管理人员,管理人员直接归CEO领导,领导有什么指示,直接告诉管理人员,然后就可以去做自己的事情了。管理人员负责去协调底层员工的工作。因为管理人员是了解手下的人员以及自己负责的事情的。所以,大多数时候,公司的各种决策,通知等,CEO只要和管理人员之间沟通就够了。 @@ -32,11 +30,11 @@ 随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 -**单线程。**cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 +**单线程**:cpu核心的缓存只被一个线程访问。缓存独占,不会出现访问冲突等问题。 -**单核CPU,多线程。**进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 +**单核CPU,多线程**:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 -**多核CPU,多线程。**每个核都至少有一个L1 缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 +**多核CPU,多线程**:每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 在CPU和主存之间增加缓存,在多线程场景下就可能存在**缓存一致性问题**,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 @@ -50,7 +48,7 @@ ### 处理器优化和指令重排 -上面提到在在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 +上面提到在CPU和主存之间增加缓存,在多线程场景下会存在**缓存一致性问题**。除了这种情况,还有一种硬件问题也比较重要。那就是为了使处理器内部的运算单元能够尽量的被充分利用,处理器可能会对输入代码进行乱序执行处理。这就是**处理器优化**。 除了现在很多流行的处理器会对代码进行优化乱序处理,很多编程语言的编译器也会有类似的优化,比如Java虚拟机的即时编译器(JIT)也会做**指令重排**。 @@ -76,7 +74,7 @@ 所以,为了保证并发编程中可以满足原子性、可见性及有序性。有一个重要的概念,那就是——内存模型。 -**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范。**通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 +**为了保证共享内存的正确性(可见性、有序性、原子性),内存模型定义了共享内存系统中多线程程序读写操作行为的规范**。通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。 内存模型解决并发问题主要采用两种方式:**限制处理器优化**和**使用内存屏障**。 @@ -86,31 +84,20 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存(每个线程都分配有单独的处理器缓存,用这些处理器缓存去缓存一些数据,就可以不用再次访问主内存去获取相应的数据,这样就可以提高效率)。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。而JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。对于共享普通变量来说,约定了变量在工作内存中发生变化了之后,必须要回写到工作内存(**迟早要回写但并非马上回写**),*但对于volatile变量则要求工作内存中发生变化之后,必须马上回写到工作内存,而线程读取volatile变量的时候,必须马上到工作内存中去取最新值而不是读取本地工作内存的副本*,此规则保证了前面所说的“当线程A对变量X进行了修改后,在线程A后面执行的其他线程能看到变量X的变动”。 - - - - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm.png) 这里面提到的主内存和工作内存,读者可以简单的类比成计算机内存模型中的主存和缓存的概念。特别需要注意的是,主内存和工作内存与JVM内存结构中的Java堆、栈、方法区等并不是同一个层次的内存划分,无法直接类比。《深入理解Java虚拟机》中认为,如果一定要勉强对应起来的话,从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分。工作内存则对应于虚拟机栈中的部分区域。 **所以,再来总结下,JMM是一种规范,目的是解决由于多线程通过共享内存进行通信时,存在的本地内存数据不一致、编译器会对代码指令重排序、处理器会对代码乱序执行等带来的问题。** - - ## volatile - - 作用:使变量在多个线程间可见(可见性),能够保证volatile变量的可见性,但不能保证volatile变量复合操作的原子性。 `Java`语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而在这个过程中,变量的新值对其他线程是不可见的.而且只当线程进入或者离开同步代码块时才与共享成员变量 -的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存。 - - +的原始值对比。这样当多个线程同时与某个对象交互时,就必须要注意到要让线程及时的得到共享成员变量的变化。也就是说每个线程都有一个自己的本地内存空间,在线程执行时,先把变量从主内存读取到线程自己的本地内存空间,然后再对该变量进行操作,当对该变量操作完后,在某个时间再把变量刷新回主内存。 - volatile如何实现内存可见性。深入来说:通过加入**内存屏障**和**禁止重排序优化**来实现的 - - 对volatile变量执行写操作时,会在写操作后加入一条store屏障指令 - 对volatile变量执行读操作时,会在读操作前加入一条load屏障指令 @@ -119,27 +106,20 @@ Java内存模型规定了所有的变量都存储在主内存中,每条线程 1. **更新主内存。** 2. **向CPU总线发送一个修改信号。** - - **这时监听CPU总线的处理器会收到这个修改信号后,如果发现修改的数据自己缓存了,就把自己缓存的数据失效掉。这样其它线程访问到这段缓存时知道缓存数据失效了,需要从主内存中获取。这样所有线程中的共享变量i就达到了一致性。** ![img](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_mm_1.jpg) **所以volatile也可以看作线程间通信的一种廉价方式。** - - - ### volatile关键字的非原子性 所谓原子性,就是某系列的操作步骤要么全部执行,要么都不执行。 -比如,变量的自增操作 i++,分三个步骤: +比如,变量的自增操作 i++,分三个步骤: - 从内存中读取出变量 i 的值 - - 将i的值加1 - - 将加1后的值写回内存 这说明 i++ 并不是一个原子操作。因为,它分成了三步,有可能当某个线程执行到了第②时被中断了,那么就意味着只执行了其中的两个步骤,没有全部执行。 @@ -152,9 +132,7 @@ volatile修饰的变量并不保证对它的操作(自增)具有原子性。 **综上,仅靠volatile不能保证线程的安全性。(原子性)** - - -### volatile禁止指令重排序 +### volatile禁止指令重排序 代码书写的顺序与实际执行的顺序不同,指令重排序是编译器或处理器为了提高程序性能而做的优化 @@ -170,9 +148,9 @@ volatile禁止指令重排序,典型的应用是单例模式中的双重检查 public class DoubleCheckSingleton { private volatile static DoubleCheckSingleton singleton; - + private DoubleCheckSingleton() {} - + public static DoubleCheckSingleton getInstance() { if(singleton == null) { synchronized (singleton) { @@ -183,7 +161,7 @@ public class DoubleCheckSingleton { } return singleton; } - + } ``` @@ -205,8 +183,6 @@ ctorInstance(memory); //2.初始化对象 指令重排序后,在多线程情况下,可能会发生A线程正在new对象,执行了3,但还没有执行2。此时B线程进入方法获取单例对象,执行同步代码块外的非空判断,发现变量非空,但此时对象还未初始化,B线程获取到的是一个未被初始化的对象。使用volatile修饰后,禁止指令重排序。即,先初始化对象后,再设置instance指向刚分配的内存地址。这样就就不存在获取到未被初始化的对象。 - - ## synchronized **synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。** @@ -222,14 +198,10 @@ ctorInstance(memory); //2.初始化对象 - 然而,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,另一个线程仍然可以访问该`object`中的非`synchronized(this)`同步代码块。 - 尤其关键的是,当一个线程访问`object`的一个`synchronized(this)`同步代码块时,其他线程对`object`中所有其它`synchronized(this)`同步代码块的访问将被阻塞。 - - **在JVM规范中规定了synchronized是通过Monitor对象来实现方法和代码块的同步,但两者实现细节有点一不样。代码块同步是使用monitorenter和monitorexit指令,方法同步是使用另外一种方法实现,细节JVM规范并没有详细说明。但是,方法的同步同样可以使用这两指令来实现。** **monitorenter指令是编译后插入到同步代码块的开始位置,而monitorexit指令是插入到方法结束处和异常处。JVM保证了每个monitorenter都有对应的monitorexit。任何一个对象都有一个monitor与之关联,当且一个monitor被持有后,对象将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象对应monitor的所有权,即尝试获得对象的锁。** - - ### synchronized缺点 #### 有性能损耗 @@ -242,8 +214,6 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 > > 通常提供singal机制:允许正持有“许可”的线程暂时放弃“许可”,等待某个谓词成真(条件变量),而条件成立后,当前进程可以“通知”正在等待这个条件变量的线程,让他可以重新去获得运行许可。 - - #### 产生阻塞 基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁之后,才能进行The Owner区域,其他线程还会继续在Entry Set等待。并且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。 @@ -252,20 +222,13 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 所以,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。 - - ## volatile与Synchronized的区别: - - - synchronized通过加锁的方式,使得其在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。 - - volatile通过在volatile变量的操作前后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。 - - volatile关键字是无法保证原子性的,而synchronized通过monitorenter和monitorexit两个指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,即可保证不会出现CPU时间片在多个线程间切换,即可保证原子性 - - volatile是变量修饰符,而synchronized则作用于一段代码或方法。 -- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 +- volatile只是在线程内存和“主”内存间同步某个变量的值;而synchronized通过锁定和解锁某个监视器同步所有变量的值。显然synchronized要比volatile消耗更多资源。 一句话,那什么时候才能用volatile关键字呢?(千万记住了,重要事情说三遍,感觉这句话过时了) @@ -277,23 +240,12 @@ Monitor其实是一种同步工具,也可以说是一种同步机制,它通 比如上面 count++ ,是获取-计算-写入三步操作,也就是依赖当前值的,所以不能靠volatile 解决问题。 - - - - -参考: +参考: - [再有人问你Java内存模型是什么,就把这篇文章发给他](https://www.hollischuang.com/archives/2550) - [原子性、可见性以及有序性](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E5%8F%AF%E8%A7%81%E6%80%A7%E4%BB%A5%E5%8F%8A%E6%9C%89%E5%BA%8F%E6%80%A7.md) - - --- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - - - - +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" index 1ec4f548..27fff875 100644 --- "a/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" @@ -7,24 +7,24 @@ - 在什么情况下使用线程池? - 单个任务处理的时间比较短 - - 将需处理的任务的数量大 + - 将要处理的任务的数量大 假设一个服务器完成一项任务所需时间为:`T1`创建线程时间,`T2`在线程中执行任务的时间,`T3`销毁线程时间。如果:`T1 + T3`远大于`T2`,则可以采用线程池,以提高服务器性能。 - 一个线程池包括以下四个基本组成部分: - - 线程池管理器(`ThreadPool`):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务 - - 工作线程(`PoolWorker`):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务 + - 线程池管理器(`ThreadPool`):用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务 + - 工作线程(`PoolWorker`):线程池中的线程,在没有任务时处于等待状态,可以循环的执行任务 - 任务接口(`Task`):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等 - 任务队列(`TaskQueue`):用于存放没有处理的任务。提供一种缓冲机制。 - 使用线程池的好处: - 降低资源消耗。通过重复利用减少在创建和销毁线程上所花的时间以及系统资源的开销 - 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, - 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 - - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - + 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 + - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 + - 工作流程 线程池的任务就在于负责这些线程的创建,销毁和任务处理参数传递、唤醒和等待。 - 创建若干线程,置入线程池 @@ -33,22 +33,22 @@ - 否则新建一个线程,并置入线程池,并执行上一步 - 如果创建失败或者线程池已满,根据设计策略选择返回错误或将任务置入处理队列,等待处理 - 销毁线程池 - + - 风险 虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。 - 用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁, - 它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。 - + 用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁, + 它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。 + - 资源不足 - 线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行得很好。但只有恰当地调整了线程池大小时才是这样的。 - 线程消耗包括内存和其它系统资源在内的大量资源。除了`Thread`对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。 - 除此以外`JVM`可能会为每个`Java`线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后虽然线程之间切换的调度开销很小, - 但如果有很多线程,环境切换也可能严重地影响程序的性能。 + 线程池的一个优点在于:相对于其它替代调度机制而言,它们通常执行的很好。但只有恰当地调整了线程池大小时才是这样的。 + 线程消耗包括内存和其它系统资源在内的大量资源。除了`Thread`对象所需的内存之外,每个线程都需要两个可能很大的执行调用堆栈。 + 除此以外`JVM`可能会为每个`Java`线程创建一个本机线程,这些本机线程将消耗额外的系统资源。最后虽然线程之间切换的调度开销很小, + 但如果有很多线程,环境切换也可能严重地影响程序的性能。 如果线程池太大,那么被那些线程消耗的资源可能严重地影响系统性能。在线程之间进行切换将会浪费时间, - 而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。 - 除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如`JDBC`连接、套接字或文件。 - 这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配`JDBC`连接。 - + 而且使用超出比您实际需要的线程可能会引起资源匮乏问题,因为池线程正在消耗一些资源,而这些资源可能会被其它任务更有效地利用。 + 除了线程自身所使用的资源以外,服务请求时所做的工作可能需要其它资源,例如`JDBC`连接、套接字或文件。 + 这些也都是有限资源,有太多的并发请求也可能引起失效,例如不能分配`JDBC`连接。 + 线程池的使用 --- @@ -265,4 +265,4 @@ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/Tools&Library/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" "b/Tools&Library/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" index 8df1f5a3..1fc3e4f8 100644 --- "a/Tools&Library/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" +++ "b/Tools&Library/Markdown\345\255\246\344\271\240\346\211\213\345\206\214.md" @@ -3,7 +3,7 @@ Markdown学习手册 #### 一. 简单功能 功能 | 效果 | Markdown代码 | 备注 -:--|:--|:--|:-- +---|---|---|--- 粗体 | **粗体** | `**粗体**` | 两边加** 斜体 | _斜体_ | `_斜体_` | 两边加_ 中划线 | ~~中划线~~ | `~~中划线~~` | 两边加~~ @@ -146,20 +146,6 @@ def hello(): 单元格1|单元格2 单元格3|单元格4 -而且还可以通过在表头分隔符中添加 `:` 来决定单元格的对齐方向,以下代码: -``` -表头1|表头2|表头3 -:--|:--:|--: -左1|中1|右1 -左2|中2|右2 -``` -效果为: - -表头1|表头2|表头3 -:--|:--:|--: -左1|中1|右1 -左2|中2|右2 - ##### 6. 特殊字符转义 如果需要在正文里使用特殊字符的话,可以用 `\` 来转义 diff --git "a/Tools&Library/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" "b/Tools&Library/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" index 1084d903..0770d1bb 100644 --- "a/Tools&Library/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" +++ "b/Tools&Library/\346\200\247\350\203\275\344\274\230\345\214\226\347\233\270\345\205\263\345\267\245\345\205\267.md" @@ -1,166 +1,135 @@ 性能优化相关工具 -=== +有关性能优化的文章请参考性能优化和布局优化 -有关性能优化的文章请参考[性能优化][1]和[布局优化][2] +DDMS(百宝箱) +查看进程的内存使用 +DDMS可以让你查看到一个进程使用的堆内存。 +在Device了表选择你想查看的进程。 +点击Update Heap按钮来开启查看进程堆内存信息。 +在Heap页面,点击Cause GC来执行垃圾回收。 +在列表中点击一个对象类型来查看它所分配的内存大小。 +追踪对象的内存分配情况 +在Device了表选择你想查看的进程。 +在Allocation Tracker页面,点击Start Tracking按钮来开始追踪。 +点击Get Allocations来查看从你点击Start Tracking之后分配内存的对象列表。 你可以再次点击Get Allocations来添加新分配内存的对象列表。 +停止追踪或者清除数据可以点击Stop Tracking按钮。 +在列表中单独点击一行来查看详细的信息。 +使用文件系统 +DDMS提供了一个文件浏览器来供你查看、拷贝、删除文件。 -### DDMS(百宝箱) +在Device页面,选中你想要查看的设备。 +从设备中拷贝文件,用文件浏览器选择文件后点击Pull file按钮。 +拷贝文件到设备中,点击Push file按钮。 +检查线程信息 +Thread页面可以显示指定进程中当前正在运行的线程。 -##### 查看进程的内存使用 +在Device页面,选择想要查看的进程。 +点击Update Threads按钮。 +在Thread页面,你可以查看对应的线程信息。 +启动方法分析 +方法分析可以追踪一个方法的特定信息,例如调用数量,执行时间、耗时等。如果想要更精确的控制想要收集的数据,可以使用startMethodTracing()和stopMethodTracing()方法。 -`DDMS`可以让你查看到一个进程使用的堆内存。 -1. 在`Device`了表选择你想查看的进程。 -2. 点击`Update Heap`按钮来开启查看进程堆内存信息。 -3. 在`Heap`页面,点击`Cause GC`来执行垃圾回收。 -4. 在列表中点击一个对象类型来查看它所分配的内存大小。 +在你开始使用方法分析之前请知晓如下限制: -##### 追踪对象的内存分配情况 +在Android 2.1及以前的设备必须有SD卡,并且你的应用必须有写入SD卡的权限。 +在Android 2.2及以后的设备中不需要SD卡,trace log文件会直接被导入到开发机上。 +开启方法分析: -1. 在`Device`了表选择你想查看的进程。 -2. 在`Allocation Tracker`页面,点击`Start Tracking`按钮来开始追踪。 -3. 点击`Get Allocations`来查看从你点击`Start Tracking`之后分配内存的对象列表。 你可以再次点击`Get Allocations`来添加新分配内存的对象列表。 -4. 停止追踪或者清除数据可以点击`Stop Tracking`按钮。 -5. 在列表中单独点击一行来查看详细的信息。 +在Device页面,选择想要开启方法分析的进程。 +点击Start Method Profiling按钮。 +在Android 4.4及以后,可以选择trace_based profiling或者基本的sample-based profiling选项。在之前的版本智能使用trace-based profiling。 +在应用中操作界面来执行你想要分析的方法。 +点击Stop Method Profiling按钮。DDMS会停止分析并且将在Start Method Profiling和Stop Method Profiling中间手机的方法分析数据使用Traceview打开。 +使用网络流量工具 +从Android 4.0开始,DDMS包含了一个网络使用情况的页面来支持跟踪应用的网络请求。 -##### 使用文件系统 +如图: -`DDMS`提供了一个文件浏览器来供你查看、拷贝、删除文件。 -1. 在`Device`页面,选中你想要查看的设备。 -2. 从设备中拷贝文件,用文件浏览器选择文件后点击`Pull file`按钮。 -3. 拷贝文件到设备中,点击`Push file`按钮。 +image。 -##### 检查线程信息 +通过检测数据交互的频繁性以及在每次连接中数据的传递量,你可以知道应用中具体可以做的更好更高效的地方。 想要更好的查看引起数据交互的地方,可以使用TrafficsStats这个API,它也可以使用setThreadStatsTag()方法来设置数据使用情况的tag, +首先要讲到的是Systrace,之前在淘宝面试的时候被问到,如有查找UI绘制卡顿的问题,我没答上来。早点知道Systrace就好了(当然只知道工具是不够的,也要懂得里面的原理)。 -`Thread`页面可以显示指定进程中当前正在运行的线程。 +Systrace +Systrace有什么用呢?官方文档中有一片文章的标题是:使用Systrace进行UI性能分析。 -1. 在`Device`页面,选择想要查看的进程。 -2. 点击`Update Threads`按钮。 -3. 在`Thread`页面,你可以查看对应的线程信息。 +在应用程序开发的过程中,你需要检查用户交互是否平滑流畅,运行在稳定的60fps。如果中间出了些问题,导致某一帧延迟,我们想要解决该问题的第一步就是理解系统如何操作的。 -##### 启动方法分析 +Systrace工具可以让你来手机和观察整个android设备的时间信息,也称为trace。它会显示每个时间点上的CPU消耗图,显示每个线程在显示的内容以及每个进程当时在做的操作。 -方法分析可以追踪一个方法的特定信息,例如调用数量,执行时间、耗时等。如果想要更精确的控制想要收集的数据,可以使用`startMethodTracing()`和`stopMethodTracing()`方法。 +在使用Systracv来分析应用之前,你需要手机应用的trace log信息。生成的trace能够让你清晰的观察系统在该时间内所做的任何事情。 -在你开始使用方法分析之前请知晓如下限制: +生成Trace +为了能创建一个trace,你必须要执行如下几个操作。 首先,你要有一个Android 4.1及以上的设备。将该设备设置为debugging,连接你的设备并且安装应用。有些类型的信息,特别是一些硬盘的活动和内核工作队列,需要设备获取root权限才可以。 然而,大多数的Systrace log的数据只需要设备开启开发者debugging就可以了。 -- 在`Android 2.1`及以前的设备必须有`SD`卡,并且你的应用必须有写入`SD`卡的权限。 -- 在`Android 2.2`及以后的设备中不需要`SD`卡,`trace log`文件会直接被导入到开发机上。 +Systrace可以通过命令行或者图形化界面的方式来运行,在Studio中打开Android Device Monitor然后选择Systracv图标image。 -开启方法分析: +下面以命令行的方式为例,这里只讲解一下Android 4.3及以上的使用方式: -1. 在`Device`页面,选择想要开启方法分析的进程。 -2. 点击`Start Method Profiling`按钮。 -3. 在`Android 4.4`及以后,可以选择`trace_based profiling`或者基本的`sample-based profiling`选项。在之前的版本智能使用`trace-based profiling`。 -4. 在应用中操作界面来执行你想要分析的方法。 -5. 点击`Stop Method Profiling`按钮。`DDMS`会停止分析并且将在`Start Method Profiling`和`Stop Method Profiling`中间手机的方法分析数据使用`Traceview`打开。 +确保设备已经通过USB连接并且开启了debugging模式。 +设置一些参数并执行trace命令,例如: +$ cd android-sdk/platform-tools/systrace +$ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm +执行完上面的命令后就会在sdk/platform-tools/systrace/mynewtrace.html生成对应的html文件。 +在设备上执行一些你想要trace的过程。 +悲剧了,生成了对应的html文件,但是我死活打不开。打开是白屏,折腾了我半天,最后终于找到了解决方法: -##### 使用网络流量工具 +Firstly, if anyone is using Chrome v50.0+ on OS X, just try this please. -从`Android 4.0`开始,`DDMS`包含了一个网络使用情况的页面来支持跟踪应用的网络请求。 +open chrome browser and go to "chrome://tracing" in the tracing page, click load and select the systrace generated html file. Secondly, I think it's a bug which is confirmed by Google. -如图: +OK了。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ddms-network.png?raw=true)。 - -通过检测数据交互的频繁性以及在每次连接中数据的传递量,你可以知道应用中具体可以做的更好更高效的地方。 -想要更好的查看引起数据交互的地方,可以使用`TrafficsStats`这个`API`,它也可以使用`setThreadStatsTag()`方法来设置数据使用情况的`tag`, - -首先要讲到的是`Systrace`,之前在淘宝面试的时候被问到,如有查找`UI`绘制卡顿的问题,我没答上来。早点知道`Systrace`就好了(当然只知道工具是不够的,也要懂得里面的原理)。 - -### Systrace - -`Systrace`有什么用呢?官方文档中有一片文章的标题是:使用`Systrace`进行`UI`性能分析。 - -在应用程序开发的过程中,你需要检查用户交互是否平滑流畅,运行在稳定的60fps。如果中间出了些问题,导致某一帧延迟,我们想要解决该问题的第一步就是理解系统如何操作的。 - -`Systrace`工具可以让你来手机和观察整个`android`设备的时间信息,也称为`trace`。它会显示每个时间点上的`CPU`消耗图,显示每个线程在显示的内容以及每个进程当时在做的操作。 - -在使用`Systracv`来分析应用之前,你需要手机应用的`trace log`信息。生成的`trace`能够让你清晰的观察系统在该时间内所做的任何事情。 - -##### 生成Trace - -为了能创建一个`trace`,你必须要执行如下几个操作。 首先,你要有一个`Android 4.1`及以上的设备。将该设备设置为`debugging`,连接你的设备并且安装应用。有些类型的信息,特别是一些硬盘的活动和内核工作队列,需要设备获取`root`权限才可以。 然而,大多数的`Systrace log`的数据只需要设备开启开发者`debugging`就可以了。 - -`Systrace`可以通过命令行或者图形化界面的方式来运行,在`Studio`中打开`Android Device Monitor`然后选择`Systracv`图标![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-button.png?raw=true)。 - - -下面以命令行的方式为例,这里只讲解一下`Android 4.3`及以上的使用方式: - -- 确保设备已经通过`USB`连接并且开启了`debugging`模式。 -- 设置一些参数并执行`trace`命令,例如: - ``` - $ cd android-sdk/platform-tools/systrace - $ python systrace.py --time=10 -o mynewtrace.html sched gfx view wm - ``` - 执行完上面的命令后就会在`sdk/platform-tools/systrace/mynewtrace.html`生成对应的`html`文件。 -- 在设备上执行一些你想要`trace`的过程。 - -悲剧了,生成了对应的`html`文件,但是我死活打不开。打开是白屏,折腾了我半天,最后终于找到了解决方法: - -> Firstly, if anyone is using Chrome v50.0+ on OS X, just try this please. - -> open chrome browser and go to "chrome://tracing" -> in the tracing page, click load and select the systrace generated html file. -> Secondly, I think it's a bug which is confirmed by Google. - -`OK`了。 - - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace_file.png?raw=true =)。 +image。 看不懂!!! -##### 分析Trace +分析Trace +用浏览器打开上面生成的mynewtrace.html。 下面为了能明显的说明,我就用官网提供的例子来说明了。 -用浏览器打开上面生成的`mynewtrace.html`。 下面为了能明显的说明,我就用官网提供的例子来说明了。 +查看Frames +从打开的文件中我们能看到每个应用都有一行frame的圆圈来标示渲染的帧,通常都是绿色的。黄色或者红的圆圈标示超过了我们对保障60fps所需的16毫秒绘制时间。。 可以在文件上面按w键来放大文件以便能更好的观看查找。 -###### 查看Frames -从打开的文件中我们能看到每个应用都有一行`frame`的圆圈来标示渲染的帧,通常都是绿色的。黄色或者红的圆圈标示超过了我们对保障60fps所需的16毫秒绘制时间。。 可以在文件上面按`w`键来放大文件以便能更好的观看查找。 -> ***提示:***在文件上面按`?`键可以查看对应的快捷键。 +***提示:***在文件上面按?键可以查看对应的快捷键。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-unselected.png?raw=true)。 +image。 -在`Android 5.0`以上的设备上,显示工作呗分为`UI Thread`和`Render Thread`。在之前的版本,所有工作都是在`UI Thread`进行的。 点击某个单独的`frame`图标来查看它们所需的时间。 +在Android 5.0以上的设备上,显示工作呗分为UI Thread和Render Thread。在之前的版本,所有工作都是在UI Thread进行的。 点击某个单独的frame图标来查看它们所需的时间。 -###### 观看Alerts +观看Alerts +Systracv会自动分析trace过程的时间,并且通过alerts提示该展现问题,以及建议如何处理。 -`Systracv`会自动分析`trace`过程的时间,并且通过`alerts`提示该展现问题,以及建议如何处理。 +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-selected.png?raw=true) +如上图所示,在你点击一个比较慢的frame时,下面就会显示一个alert。在这种情况下,它直说可能是ListView服用以及重复渲染的问题。 如果你发现UI Thread做了太多的工作,你可以使用TraceView进行代码分析,来找到具体哪些操作导致了消耗时间。 -如上图所示,在你点击一个比较慢的`frame`时,下面就会显示一个`alert`。在这种情况下,它直说可能是`ListView`服用以及重复渲染的问题。 如果你发现`UI Thread`做了太多的工作,你可以使用`TraceView`进行代码分析,来找到具体哪些操作导致了消耗时间。 +如下,你也可以通过点击窗口右边的Alerts来查看当前所有的alert。这样会直接展开Alert窗口。 -如下,你也可以通过点击窗口右边的`Alerts`来查看当前所有的`alert`。这样会直接展开`Alert`窗口。 +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame-selected-alert-tab.png?raw=true) +下面我们以另外一个例子来分析一下它的Frame: -下面我们以另外一个例子来分析一下它的`Frame`: +我们点击某一红色frame来查看这一阵的一些相关警告: +image 可以看到这里说耗时32毫秒,这已经远远超过了16毫秒的绘制时间。 我们可以通过Description来查看描述内容。 +下面是另外一个渲染过慢的例子: -我们点击某一红色`frame`来查看这一阵的一些相关警告: -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-frame-zoomin.png?raw=true) -可以看到这里说耗时32毫秒,这已经远远超过了16毫秒的绘制时间。 我们可以通过`Description`来查看描述内容。 -下面是另外一个渲染过慢的例子: +image 从上图,我们发现了Scheduling delay的警告,加起来能看到总的绘制时间大约是19毫秒。 +Scheduling delay的意思是调度延迟,也就是说一个县城在处理这部分操作时,在很长时间内没有被分配到CPU上面进行运算,这样就导致了很长时间内没有完成操作。 我们选择这一帧中最长的一块,来观察下: +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-frame.png?raw=true) -从上图,我们发现了`Scheduling delay`的警告,加起来能看到总的绘制时间大约是19毫秒。 -`Scheduling delay`的意思是调度延迟,也就是说一个县城在处理这部分操作时,在很长时间内没有被分配到`CPU`上面进行运算,这样就导致了很长时间内没有完成操作。 我们选择这一帧中最长的一块,来观察下: -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-slice.png?raw=true) +我们在上图看到了Wall duration,他代表着这一块开始到结束的总耗时。而在CPU Duration这里显示了CPU在处理该部分消耗的时间。 很明显,真个区域用了18毫秒,但是CPU实际处理只用了4毫秒,也就是说剩下的14毫秒就可能有问题了。 +image -我们在上图看到了`Wall duration`,他代表着这一块开始到结束的总耗时。而在`CPU Duration`这里显示了`CPU`在处理该部分消耗的时间。 很明显,真个区域用了18毫秒,但是`CPU`实际处理只用了4毫秒,也就是说剩下的14毫秒就可能有问题了。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/systrace-2-cpu.png?raw=true) +可以看到,4个线程都比较忙。 -可以看到,4个线程都比较忙。 +选择其中一个线程来查看是哪个应用在使用它,这里看到了一个包名为com.udinic.keepbusyapp的应用。也就是说由于另外一个应用占用了CPU,,导致了我们的应用未能获取到足够的CPU资源。 -选择其中一个线程来查看是哪个应用在使用它,这里看到了一个包名为`com.udinic.keepbusyapp`的应用。也就是说由于另外一个应用占用了`CPU`,,导致了我们的应用未能获取到足够的`CPU`资源。 +追踪代码 +在Android 4.3以上你可以使用Trace类来在代码中添加(很熟悉有木有,在看源码的时候经常看到)。这样能查看到此时你的应用线程都做了哪些操作。 下面的代码展示了如何使用Trace类来追踪两个代码块部分: - -##### 追踪代码 - -在`Android 4.3`以上你可以使用`Trace`类来在代码中添加(很熟悉有木有,在看源码的时候经常看到)。这样能查看到此时你的应用线程都做了哪些操作。 -下面的代码展示了如何使用`Trace`类来追踪两个代码块部分: -```java public void ProcessPeople() { Trace.beginSection("ProcessPeople"); try { @@ -181,240 +150,190 @@ public void ProcessPeople() { Trace.endSection(); // ends "ProcessPeople" } } -``` - -### Traceview +Traceview +Traceview是一个性能测试工具,展示了所有方法的运行时间。 -`Traceview`是一个性能测试工具,展示了所有方法的运行时间。 -> Traceview is a graphical viewer for execution logs that you create by using the Debug class to log tracing information in your code. Traceview can help you debug your application and profile its performance. +Traceview is a graphical viewer for execution logs that you create by using the Debug class to log tracing information in your code. Traceview can help you debug your application and profile its performance. +创建Trace文件 +想要使用Traceview你需要创建一个包含你想分析部分的trace信息的log文件。 有两种方式来生成trace logs: -##### 创建Trace文件 +在你测试的类中添加startMethodTracing()和stopMethodTracing()的代码,来指定开始和结束获取trace信息。这种方式是非常精确的,因为你可以在代码中指定开始和结束的位置。 -想要使用`Traceview`你需要创建一个包含你想分析部分的`trace`信息的`log`文件。 -有两种方式来生成`trace logs`: +使用DDMS中的方法来生成。这种方式就不太精确。虽然这种方式不能精确的指定起始和结束位置,但是如果在你无法修改源代码或者不需要精确时间的时候是非常有用的。 -- 在你测试的类中添加`startMethodTracing()`和`stopMethodTracing()`的代码,来指定开始和结束获取`trace`信息。这种方式是非常精确的,因为你可以在代码中指定开始和结束的位置。 +在开始生成trace log信息时,你需要知道如下的限制条件: -- 使用`DDMS`中的方法来生成。这种方式就不太精确。虽然这种方式不能精确的指定起始和结束位置,但是如果在你无法修改源代码或者不需要精确时间的时候是非常有用的。 +如果你在测试代码中使用,你的应用必须要用WRITE_EXTERNAL_STORAGE的权限。 -在开始生成`trace log`信息时,你需要知道如下的限制条件: +如果你使用DDMS生成: -- 如果你在测试代码中使用,你的应用必须要用`WRITE_EXTERNAL_STORAGE`的权限。 -- 如果你使用`DDMS`生成: +Android 2.1之前必须有SD卡,并且你的应用也要有写入SD卡的权限。 +Android 2.2之后不需要SD卡,trace log文件会直接生成到你的开发机上。 +在测试代码中调用startMethodTracing()方法的时候,你可以指定系统生成trace文件的名字。结束的时候调用stopMethodTracing()方法。这些方法开始和结束时贯穿整个虚拟中的。例如你可以在你activity的onCreate()方法中调用startMethodTracing()方法,然后在activity的onDestroy()方法中调用stopMethodTracing()方法。 - - `Android 2.1`之前必须有`SD`卡,并且你的应用也要有写入`SD`卡的权限。 - - `Android 2.2`之后不需要`SD`卡,`trace log`文件会直接生成到你的开发机上。 - -在测试代码中调用`startMethodTracing()`方法的时候,你可以指定系统生成`trace`文件的名字。结束的时候调用`stopMethodTracing()`方法。这些方法开始和结束时贯穿整个虚拟中的。例如你可以在你`activity`的`onCreate()`方法中调用`startMethodTracing()`方法,然后在`activity`的`onDestroy()`方法中调用`stopMethodTracing()`方法。 -```java // start tracing to "/sdcard/calc.trace" Debug.startMethodTracing("calc"); // ... // stop tracing Debug.stopMethodTracing(); -``` - -在调用`startMethodTracing()`方法的时候,系统创建一个名为`.trace`的文件。它包含方法的二进制`trace`数据和一个线程与方法名的对应集合。 -然后系统就开始生成`trace`数据,直到调用`stopMethodTracing()`方法。如果在你调用`stopMethodTracing()`方法之前系统已经达到了最大的缓冲大小,系统就会停止`trace`并且在控制台发出一个通知。 - -##### 拷贝Trace文件到电脑上 +在调用startMethodTracing()方法的时候,系统创建一个名为.trace的文件。它包含方法的二进制trace数据和一个线程与方法名的对应集合。 +然后系统就开始生成trace数据,直到调用stopMethodTracing()方法。如果在你调用stopMethodTracing()方法之前系统已经达到了最大的缓冲大小,系统就会停止trace并且在控制台发出一个通知。 -在模拟器或者机器上生成`.trace`文件后,你需要拷贝他们到你的电脑上,你可以使用`adb pull`命令来拷贝: -`adb pull /sdcard/calc.trace /tmp` +拷贝Trace文件到电脑上 +在模拟器或者机器上生成.trace文件后,你需要拷贝他们到你的电脑上,你可以使用adb pull命令来拷贝: +adb pull /sdcard/calc.trace /tmp -##### 在Traceview中查看trace文件 +在Traceview中查看trace文件 +运行Traceview并且查看trace文件: -运行`Traceview`并且查看`trace`文件: +打开Android Device Monitor。 +在Android Device Monitor的状态栏中点击DDMS并且选择一个进程。 +点击Start Method Profiling图标开始查看。 +在查看完后点击Stop Method Profiling图标来显示traceview。 +大体的样子如下: -1. 打开`Android Device Monitor`。 -2. 在`Android Device Monitor`的状态栏中点击`DDMS`并且选择一个进程。 -3. 点击`Start Method Profiling`图标开始查看。 -4. 在查看完后点击`Stop Method Profiling`图标来显示`traceview`。 +image -大体的样子如下: - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_1.png?raw=true) - -##### Traceview Layout - -如果你有一个`trace log`文件(通过添加`tracing`代码或者用DDMS生成),你可以把该文件加载到`Traceview`中,这将会把`log`数据显示为两部分: - -- `timeline panel`-展示了每个线程和方法的起始和结束 -- `profile panel`-提供了一个方法中的执行内容的简述 - -##### Timeline Panel +Traceview Layout +如果你有一个trace log文件(通过添加tracing代码或者用DDMS生成),你可以把该文件加载到Traceview中,这将会把log数据显示为两部分: +timeline panel-展示了每个线程和方法的起始和结束 +profile panel-提供了一个方法中的执行内容的简述 +Timeline Panel 每个线程都会以时间从左往右递增的方式在单独的一行中显示它的执行情况, -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_timeline.png?raw=true) - -##### Profile Panel - -显示了一个方法所消耗的时间的概要情况。在表格中会同时显示`inclusive`和`exclusive`的时间。`Exclusive`的时间是该方法所消耗的时间。`InClusive`的时间是该方法消耗的时间加上任何调的方法所消耗的时间。我们简单的将调用的方法叫做`parents`,被调用的方法叫做`children`。如果一个方法被调用,它会显示对应的`parents`和`children`。`parents`会显示一个紫色的背景,`children`会显示一个黄色的背景。最后一列显示了该方法所调用的总数的调用数。在下面的图中我们能看到一共有14个地方调用了`LoadListener.nativeFinished()` . +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview_profile.png?raw=true) +Profile Panel +显示了一个方法所消耗的时间的概要情况。在表格中会同时显示inclusive和exclusive的时间。Exclusive的时间是该方法所消耗的时间。InClusive的时间是该方法消耗的时间加上任何调的方法所消耗的时间。我们简单的将调用的方法叫做parents,被调用的方法叫做children。如果一个方法被调用,它会显示对应的parents和children。parents会显示一个紫色的背景,children会显示一个黄色的背景。最后一列显示了该方法所调用的总数的调用数。在下面的图中我们能看到一共有14个地方调用了LoadListener.nativeFinished() . -- Name 方法名 -- Inclusive CPU Time, CPU在处理该方法以及所有子方法(被它调用的所有方法)所消耗的时间。 -- Exlusive CPU Time, CPU在处理该方法的耗时。 -- Inclusive/Exclusive Real Time, 从方法开始执行到执行结束的耗时。 -- Cal+Rec, 这个方法被调用的次数,以及递归被调用的次数。 -- CPU/Real time per Call, 在处理这个方法时的`CPU`耗时的平均值。 +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview-getview.png?raw=true) +Name 方法名 +Inclusive CPU Time, CPU在处理该方法以及所有子方法(被它调用的所有方法)所消耗的时间。 +Exlusive CPU Time, CPU在处理该方法的耗时。 +Inclusive/Exclusive Real Time, 从方法开始执行到执行结束的耗时。 +Cal+Rec, 这个方法被调用的次数,以及递归被调用的次数。 +CPU/Real time per Call, 在处理这个方法时的CPU耗时的平均值。 +image -上图中`getView`方法被调用了12次,每次`CPU`消耗2.8秒,但是每次调用的总耗时是162秒,这里肯定有问题。 -而看看这个方法的children,我们可以看到这其中的每个方法在耗时方面是如何分布的。Thread.join()方法战局了98%的inclusive real time。这个方法在等待另一个线程结束的时候被调用。在Children中另外一个方法就是Tread.start()方法,而之所以整个方法耗时很长,我猜测是因为在getView()方法中启动了线程并且在等待它的结束。 +上图中getView方法被调用了12次,每次CPU消耗2.8秒,但是每次调用的总耗时是162秒,这里肯定有问题。 而看看这个方法的children,我们可以看到这其中的每个方法在耗时方面是如何分布的。Thread.join()方法战局了98%的inclusive real time。这个方法在等待另一个线程结束的时候被调用。在Children中另外一个方法就是Tread.start()方法,而之所以整个方法耗时很长,我猜测是因为在getView()方法中启动了线程并且在等待它的结束。 但是这个线程在哪儿? 我们在getView()方法中并不能看到这个线程做了什么,因为这段逻辑不在getView()方法之中。于是我找到了Thread.run()方法,就是在线程被创建出来时候所运行的方法。而跟随这个方法一路向下,我找到了问题的元凶。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/traceview-thread.png?raw=true) +image 我发现了BgService.doWork()方法的每次调用花费了将近14毫秒,并且有四十个这东西!而且getView()中还有可能调用多次这个方法,这就解释了为什么getView()方法执行时间如此之长。这个方法让CPU长时间的保持在了繁忙状态。而看看Exclusive CPU time,我们可以看到他占据了80%的CPU时间!此外,根据Exclusive CPU time排序,可以帮我们更好的定位那些耗时很长的方法,而他们很有可能就是造成性能问题的罪魁祸首。 关注这些耗时方法,例如getView(),View#onDraw()等方法,可以很好的帮助我们寻找为什么应用运行缓慢的原因。但有些时候,还会有一些其他的东西来占用宝贵的CPU资源,而这些资源如果被运用在UI的绘制上,也许我们的应用会更加流畅。Garbage Collector垃圾回收机制会不时的运行,回收那些没用的对象,通常来讲这不会影响我们在前台运行的程序。但如果GC被运行的过于频繁,他同样可以影响我们应用的执行效率。而我们该如何知道回收的是否过于频繁了呢… -### Monitors +Monitors +在Android Studio中下方的Android Monitor中可以看到Monitors工具栏,它能不断的去检测内存、网络、CPU的消耗情况。 -在`Android Studio`中下方的`Android Monitor`中可以看到`Monitors`工具栏,它能不断的去检测内存、网络、CPU的消耗情况。 +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/monitor.png?raw=true) +我们可以直接点击Dump Java Heap或者Call GC等按钮,以便更好的去观察内存的使用情况。点击后会生成一个.hprof的文件。生成后直接使用Studio打开.hprof文件即可。 -我们可以直接点击`Dump Java Heap`或者`Call GC`等按钮,以便更好的去观察内存的使用情况。点击后会生成一个`.hprof`的文件。生成后直接使用`Studio`打开`.hprof`文件即可。 +说到这里插一嘴,有关Java垃圾回收机制请参考 -说到这里插一嘴,[有关Java垃圾回收机制请参考][3] +image 在左边能看到所有堆内存中的实例。后面会显示他所占用的内存大小。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/dump_href.png?raw=true) -在左边能看到所有堆内存中的实例。后面会显示他所占用的内存大小。 - -对于内存泄漏的分析可以使用`MAT`或者`LeakCanary`来进行。这里就不仔细说了。 +对于内存泄漏的分析可以使用MAT或者LeakCanary来进行。这里就不仔细说了。 -##### 不同虚拟机的内存管理 +不同虚拟机的内存管理 +Android Monitor使用的Virtual Machine(VM): -`Android Monitor`使用的`Virtual Machine(VM)`: +Android 4.3(API Level 18)及以前的版本使用Dalvik VM . +Android 4.4(API Level 19)默认使用Dalvik VM,Android RunTime(ART)是可选的。 +Android 4.3(API Level 18)及更高的版本使用ART VM. +虚拟之执行垃圾回收。Dalvik虚拟机使用一个标记和清除垃圾收集方案。ART虚拟机使用分代收集算法与标记和清楚手机算法相结合的方式。 Logcat会显示一些垃圾回收相关的信息。 -- `Android 4.3(API Level 18)`及以前的版本使用`Dalvik VM` . -- `Android 4.4(API Level 19)`默认使用`Dalvik VM`,`Android RunTime(ART)`是可选的。 -- `Android 4.3(API Level 18)`及更高的版本使用`ART VM`. +GPU使用情况 +上面也显示了GPU的使用情况,这里要说一句,如果想要显示它,必须要在手机的开发者中心中开启GPU显示配置文件选项,将其设置为显示与adb shell dumpsys gfxinfo。然后再点击Studio中的按钮重新开始就可以看到了。 每一条线意味着一帧被绘制出来了。而线的颜色又代表不同的阶段: -虚拟之执行垃圾回收。Dalvik虚拟机使用一个标记和清除垃圾收集方案。`ART`虚拟机使用分代收集算法与标记和清楚手机算法相结合的方式。 `Logcat`会显示一些垃圾回收相关的信息。 +Draw(蓝色)代表着View.onDraw()方法。如果这个值很高就说明可能是该View比较复杂。 在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。 -##### GPU使用情况 +Prepare(紫色),从Android 6.0开始,一个新的线程被引进来帮助UI线程进行绘制。 这个线程叫做Render Thread。它负责转换它负责转换display list到OpenGL命令并且送至GPU。在这过程中,UI线程可以继续开始处理后面的帧。而在UI线程将所有资源传递给RenderThread过程中所消耗的时间,就是紫色阶段所消耗的时间。如果在这过程中有很多的资源都要进行传递,display list会变得过多过于沉重,从而导致在这一阶段过长的耗时。 -上面也显示了`GPU`的使用情况,这里要说一句,如果想要显示它,必须要在手机的开发者中心中开启`GPU显示配置文件`选项,将其设置为显示与`adb shell dumpsys gfxinfo`。然后再点击`Studio`中的按钮重新开始就可以看到了。 每一条线意味着一帧被绘制出来了。而线的颜色又代表不同的阶段: +Process(红色) 执行Display list中的内容并创建OpenGL命令。如果有过多或者过于复杂的display list需要执行的话,那么这阶段会消耗较长的时间,因为这样的话会有很多的view被重绘。而重绘往往发生在界面的刷新或是被移动出了被覆盖的区域。 -- Draw(蓝色)代表着`View.onDraw()`方法。如果这个值很高就说明可能是该`View`比较复杂。 在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。 -- Prepare(紫色),从`Android 6.0`开始,一个新的线程被引进来帮助`UI`线程进行绘制。 这个线程叫做`Render Thread`。它负责转换它负责转换display list到OpenGL命令并且送至GPU。在这过程中,UI线程可以继续开始处理后面的帧。而在UI线程将所有资源传递给RenderThread过程中所消耗的时间,就是紫色阶段所消耗的时间。如果在这过程中有很多的资源都要进行传递,display list会变得过多过于沉重,从而导致在这一阶段过长的耗时。 +Execute (黄色) – 发送OpenGL命令到GPU。这个阶段是一个阻塞调用,因为CPU在这里只会发送一个含有一些OpenGL命令的缓冲区给GPU,并且等待GPU返回空的缓冲区以便再次传递下一帧的OpenGL命令。而这些缓冲区的总量是一定的,如果GPU太过于繁忙,那么CPU则会去等待下一个空缓冲区。所以,如果我们看到这一阶段耗时比较长,那可能是因为GPU过于繁忙的绘制UI,而造成这个的原因则可能是在短时间内绘制了过于复杂的view。 -- Process(红色) 执行Display list中的内容并创建OpenGL命令。如果有过多或者过于复杂的display list需要执行的话,那么这阶段会消耗较长的时间,因为这样的话会有很多的view被重绘。而重绘往往发生在界面的刷新或是被移动出了被覆盖的区域。 - -- Execute (黄色) – 发送OpenGL命令到GPU。这个阶段是一个阻塞调用,因为CPU在这里只会发送一个含有一些OpenGL命令的缓冲区给GPU,并且等待GPU返回空的缓冲区以便再次传递下一帧的OpenGL命令。而这些缓冲区的总量是一定的,如果GPU太过于繁忙,那么CPU则会去等待下一个空缓冲区。所以,如果我们看到这一阶段耗时比较长,那可能是因为GPU过于繁忙的绘制UI,而造成这个的原因则可能是在短时间内绘制了过于复杂的view。 - -- Measure/Layout(绿色) 代表`Measure`和`Layout`的时间。 - -### Hierarchy Viewer +Measure/Layout(绿色) 代表Measure和Layout的时间。 +Hierarchy Viewer 布局分析工具,非常常用。 -在`Android Device Monitor`中打开`Hierarchy Viewer`即可。 - -- 连接你的手机或者模拟器。 - 出于安全性考虑,`Hierarchy Viewer`只能连接开发者版的系统的手机。 -- 运行程序,并且让界面显示出来。 -- 启动`hierarchy view`工具,接着就能看到左边栏显示出了对应的设备,展开后可以看到一些组件的名称。这个页面包含了应用的界面以及系统的界面。选择你的应用中想要查看的界面直接双击就可以了。 - -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gettingstarted_image005.png?raw=true) -如果你的界面显示的不是这个样子,少一些东西的话,你可以使用`Window>Reset Perspective`来重置样式。 - +在Android Device Monitor中打开Hierarchy Viewer即可。 -但是我并打不开。 -如果你的手机是`Android 4.1`及以上版本你必须要在你的电脑上设置一个`ANDROID_HVPROTO`DE 环境变量才可以。 +连接你的手机或者模拟器。 出于安全性考虑,Hierarchy Viewer只能连接开发者版的系统的手机。 +运行程序,并且让界面显示出来。 +启动hierarchy view工具,接着就能看到左边栏显示出了对应的设备,展开后可以看到一些组件的名称。这个页面包含了应用的界面以及系统的界面。选择你的应用中想要查看的界面直接双击就可以了。 +image 如果你的界面显示的不是这个样子,少一些东西的话,你可以使用Window>Reset Perspective来重置样式。 -- Windows - 增加一个名为`ANDROID_HVPROTO`值为`ddm`的环境变量就可以了。 -- Mac - - 打开`.bash_profile` - - `touch .bash_profile`创建 - - `open -e .bash_profile`打开 - - 添加 - ```java - #Hierarchy Viewer Variable - export ANDROID_HVPROTO=ddm - ``` - - `source .bash_profile` +但是我并打不开。 +如果你的手机是Android 4.1及以上版本你必须要在你的电脑上设置一个ANDROID_HVPROTODE 环境变量才可以。 -如果配置完成后仍然不能用的话,你可以: +Windows 增加一个名为ANDROID_HVPROTO值为ddm的环境变量就可以了。 +Mac +打开.bash_profile +touch .bash_profile创建 +open -e .bash_profile打开 +添加 +#Hierarchy Viewer Variable +export ANDROID_HVPROTO=ddm +source .bash_profile +如果配置完成后仍然不能用的话,你可以: -- 关闭`Android Studio`. -- 执行`add kill-server`,然后`adb start-server`. -- 通过命令行开始`hierarchyviewer`. +关闭Android Studio. +执行add kill-server,然后adb start-server. +通过命令行开始hierarchyviewer. +好了,我们直接打开自己的页面进行查看布局吧(有点慢)。 +它是介个样子滴 : +image -好了,我们直接打开自己的页面进行查看布局吧(有点慢)。 -它是介个样子滴 : -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hierarchy_vierwe_page.png?raw=true) +我们可以看到所有结构,点击对一个的节点,能直接看到该界面的UI效果,并展示该布局包含多少个View,以及该布局measure,draw,layout所消耗的时间。选中Show Extras选项可以看到在右下角看到界面效果。 -我们可以看到所有结构,点击对一个的节点,能直接看到该界面的`UI`效果,并展示该布局包含多少个`View`,以及该布局`measure,draw,layout`所消耗的时间。选中`Show Extras`选项可以看到在右下角看到界面效果。 +选中要查看的节点后点击Profile Node选项,可以看到如下界面: -选中要查看的节点后点击`Profile Node`选项,可以看到如下界面: +image -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hierarchy_profile_node.png?raw=true) +可以看到每个布局都出现了三个点。有不同的颜色,从左到右这三个点分别表示: -可以看到每个布局都出现了三个点。有不同的颜色,从左到右这三个点分别表示: +左边的点代表Draw阶段。 +中间的点代表了Layout阶段。 +右边的点代表了Execute阶段。 +这三个点有分别对应了pipeline中的不同阶段,如下图: +image -- 左边的点代表`Draw`阶段。 -- 中间的点代表了`Layout`阶段。 -- 右边的点代表了`Execute`阶段。 +不同的颜色代表不同的性能: -这三个点有分别对应了`pipeline`中的不同阶段,如下图: -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gettingstarted_image015.png?raw=true) +绿色代表渲染的非常快,至少是其他View的一半。 +黄色代表View渲染比最后那一半的View快。 +红色代表View渲染是几乎是在最慢的那部分中间。 +Hierarchy Viewer测量的是相对的表现能力,所以总会有一个红色的节点,它并不意味者该View一定是绘制的太慢。 -不同的颜色代表不同的性能: +过度绘制 +在开发者选项中将调试GPU过度绘制设置为显示过度绘制区域,就能看到程序的绘制情况。 +过度绘制往往发生在我们需要在一个东西上面绘制另外一个东西,例如在一个红色的背景上画一个黄色的按钮。那么GPU就需要先画出红色背景,再在他上面绘制黄色按钮,此时过度绘制就是不可避免的了。如果我们有太多层需要绘制,那么则会过度的占用GPU导致我们每帧消耗的时间超过16毫秒。 这些过度绘制可能发生在我们给Activity或Fragment设置了全屏的背景,同时又给ListView以及ListView的条目设置了背景色。而通过只设置一次背景色即可解决这样的问题。 -- 绿色代表渲染的非常快,至少是其他`View`的一半。 -- 黄色代表`View`渲染比最后那一半的`View`快。 -- 红色代表`View`渲染是几乎是在最慢的那部分中间。 - - -`Hierarchy Viewer`测量的是相对的表现能力,所以总会有一个红色的节点,它并不意味者该`View`一定是绘制的太慢。 - -### 过度绘制 - -在开发者选项中将调试GPU过度绘制设置为显示过度绘制区域,就能看到程序的绘制情况。 -过度绘制往往发生在我们需要在一个东西上面绘制另外一个东西,例如在一个红色的背景上画一个黄色的按钮。那么GPU就需要先画出红色背景,再在他上面绘制黄色按钮,此时过度绘制就是不可避免的了。如果我们有太多层需要绘制,那么则会过度的占用GPU导致我们每帧消耗的时间超过16毫秒。 -这些过度绘制可能发生在我们给Activity或Fragment设置了全屏的背景,同时又给ListView以及ListView的条目设置了背景色。而通过只设置一次背景色即可解决这样的问题。 - -注意:默认的主题会为你指定一个默认的全屏背景色,如果你的activity又一个不透平的背景盖住了默认的背景色,那么你可以移除主题默认的背景色,这样也会移除一层的过度绘制。这可以通过配置主题配置或是通过代码的方法,在onCreate()方法中调用getWindow().setBackgroundDrawable(null)方法来实现。 -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/overdraw-gif.gif?raw=true) - - -### Hardware Acceleration +注意:默认的主题会为你指定一个默认的全屏背景色,如果你的activity又一个不透平的背景盖住了默认的背景色,那么你可以移除主题默认的背景色,这样也会移除一层的过度绘制。这可以通过配置主题配置或是通过代码的方法,在onCreate()方法中调用getWindow().setBackgroundDrawable(null)方法来实现。 image +Hardware Acceleration 在Honeycomb版本中引入了硬件加速(Hardware Accleration)后,我们的应用在绘制的时候就有了全新的绘制模型。它引入了DisplayList结构,用来记录View的绘制命令,以便更快的进行渲染。但还有一些很好的功能开发者们往往会忽略或者使用不当——View layers。 -使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer,前面透明度部分提到过),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以对一个让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。 -在阅读了ViewPager的源码后,我发现了在滑动的时候会自动为左右两页启动一个硬件层,并且在滑动结束后移除掉。 +使用View layers(硬件层),我们可以将view渲染入一个非屏幕区域缓冲区(off-screen buffer,前面透明度部分提到过),并且根据我们的需求来操控它。这个功能主要是针对动画,因为它能让复杂的动画效果更加的流畅。而不使用硬件层的话,View会在动画属性(例如coordinate, scale, alpha值等)改变之后进行一次刷新。而对于相对复杂的view,这一次刷新又会连带它所有的子view进行刷新,并各自重新绘制,相当的耗费性能。使用View layers,通过调用硬件层,GPU直接为我们的view创建一个结构,并且不会造成view的刷新。而我们可以在避免刷新的情况下对这个结构进行进行很多种的操作,例如x/y位置变换,旋转,透明度等等。总之,这意味着我们可以对一个让一个复杂view执行动画的同时,又不会刷新!这会让动画看起来更加的流畅。 在阅读了ViewPager的源码后,我发现了在滑动的时候会自动为左右两页启动一个硬件层,并且在滑动结束后移除掉。 在两页间滑动的时候创建硬件层也是可以理解的,但对我来说小有不幸。通常来讲加入硬件层是为了让ViewPager的滑动更加流畅,毕竟它们相对复杂。 - 是的,但是再使用硬件layers的时候还是有几点要牢记在心: -- 回收 – 硬件层会占用GPU中的一块内存。只在必要的时候使用他们,比如动画,并且事后注意回收。例如在上面ObjectAnimator的例子中,我们增加了一个动画结束监听以便在动画结束后可以移除硬件层。而在Property animator的例子中,我们使用了withLayers(),这会在动画开始时候自动创建硬件层并且在结束的时候自动移除。 -- 如果你在调用了硬件View layers后改变了View,那么会造成硬件硬件层的刷新并且再次重头渲染一遍view到非屏幕区域缓存中。这种情况通常发生在我们使用了硬件层暂时还不支持的属性(目前为止,硬件层只针对以下几种属性做了优化:otation、scale、x/y、translation、pivot和alpha)。例如,如果你另一个view执行动画,并且使用硬件层,在屏幕滑动他们的同时改变他的背景颜色,这就会造成硬件层的持续刷新。而以硬件层的持续刷新所造成的性能消耗来说,可能让它在这里的使用变得并不那么值。 - -有关如何使用`Hardware Layer`请参考之前写的文章:[通过Hardware Layer提高动画性能][4] - - - -[1]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96.md "性能优化" -[2]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/%E5%B8%83%E5%B1%80%E4%BC%98%E5%8C%96.md "布局优化" -[3]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md "有关Java垃圾回收机制请参考" -[4]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/%E9%80%9A%E8%BF%87Hardware%20Layer%E6%8F%90%E9%AB%98%E5%8A%A8%E7%94%BB%E6%80%A7%E8%83%BD.md "通过Hardware Layer提高动画性能" - ---- +回收 – 硬件层会占用GPU中的一块内存。只在必要的时候使用他们,比如动画,并且事后注意回收。例如在上面ObjectAnimator的例子中,我们增加了一个动画结束监听以便在动画结束后可以移除硬件层。而在Property animator的例子中,我们使用了withLayers(),这会在动画开始时候自动创建硬件层并且在结束的时候自动移除。 +如果你在调用了硬件View layers后改变了View,那么会造成硬件硬件层的刷新并且再次重头渲染一遍view到非屏幕区域缓存中。这种情况通常发生在我们使用了硬件层暂时还不支持的属性(目前为止,硬件层只针对以下几种属性做了优化:otation、scale、x/y、translation、pivot和alpha)。例如,如果你另一个view执行动画,并且使用硬件层,在屏幕滑动他们的同时改变他的背景颜色,这就会造成硬件层的持续刷新。而以硬件层的持续刷新所造成的性能消耗来说,可能让它在这里的使用变得并不那么值。 +有关如何使用Hardware Layer请参考之前写的文章:通过Hardware Layer提高动画性能 -- 邮箱 :charon.chui@gmail.com -- Good Luck! +邮箱 :charon.chui@gmail.com +Good Luck! \ No newline at end of file From 51d0b32977f50e6bd47aab1368bff5d60c23fec1 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 Mar 2023 20:46:04 +0800 Subject: [PATCH 008/128] update notes --- JavaKnowledge/.DS_Store | Bin 6148 -> 6148 bytes ...36\346\224\266\346\234\272\345\210\266.md" | 191 ++++++++----- .../JVM\346\236\266\346\236\204.md" | 137 +++++---- .../MVC\344\270\216MVP\345\217\212MVVM.md" | 22 +- .../Top-K\351\227\256\351\242\230.md" | 17 +- ...77\347\224\250\346\225\231\347\250\213.md" | 22 +- ...60\346\215\256\347\273\223\346\236\204.md" | 130 ++++----- ...&\347\261\273&\346\216\245\345\217\243.md" | 265 ++++++++++-------- VideoDevelopment/.DS_Store | Bin 0 -> 6148 bytes 9 files changed, 443 insertions(+), 341 deletions(-) create mode 100644 VideoDevelopment/.DS_Store diff --git a/JavaKnowledge/.DS_Store b/JavaKnowledge/.DS_Store index bf3070814d6f3de67d704daffef65ea47ad99cae..a157e1c898881dcc71838e834593d0a947bcfb97 100644 GIT binary patch delta 22 dcmZoMXffC@kBQUR(nv?a(8SPi^KzzGQ2Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。老生代采用的回收算法是**标记整理算法。** - - +在垃圾回收的时候,我们往往将堆内存分成**新生代和老生代(大小比例1:2)**,新生代中由Eden和Survivor0,Survivor1组成,**三者的比例是8:1:1**, +新生代的回收机制采用**复制算法**,大部分情况,对象都会首先在Eden区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入s0或者s1,并且对象的 +年龄还会加1(Eden区->Survivor区后对象的初始年龄变为1),当它的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。对象晋升到老年代的年龄 +阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。老生代采用的回收算法是**标记整理算法。** #### 对象优先在eden区分配 @@ -22,10 +26,10 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Gar 大多数情况下,对象在新生代中eden区分配。当eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC. -这里说一下Minor GC 和 Full GC 有什么不同呢? +这里说一下Minor GC和Full GC有什么不同呢? -- 新生代 GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC 非常频繁,回收速度一般也比较快。 -- 老年代 GC(Major GC/Full GC):指发生在老年代的 GC,出现了 Major GC 经常会伴随至少一次的 Minor GC(并非绝对),Major GC 的速度一般会比 Minor GC 的慢 10 倍以上。 +- 新生代GC(Minor GC):指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。 +- 老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC经常会伴随至少一次的Minor GC(并非绝对),Major GC的速度一般会比Minor GC的慢10倍以上。 #### 大对象直接进入老年代 @@ -33,12 +37,12 @@ Java 堆是垃圾收集器管理的主要区域,因此也被称作GC 堆(Gar #### 长期存活的对象将进入老年代 -既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。 - -如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6.),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。 - - +既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机给每个对 +象一个对象年龄(Age)计数器。 +如果对象在Eden出生并经过第一次Minor GC后仍然能够存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1.对象在 +Survivor中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(一般都会说默认为 15 岁,其实默认晋升年龄并不都是15,这个是要区分垃圾收集器的,CMS就是6), +就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数`-XX:MaxTenuringThreshold`来设置。 ## 如何判断对象是垃圾 @@ -63,9 +67,6 @@ ObjB.obj - ObjA ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/yinyongjishu.jpg) - - - ### 根搜索算法 根搜索算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图, @@ -83,7 +84,7 @@ ObjB.obj - ObjA 垃圾回收算法 --- -而手机后的垃圾是通过什么算法来回收的呢: +而收集后的垃圾是通过什么算法来回收的呢: - 标记-清除算法 - 复制算法 @@ -108,7 +109,9 @@ ObjB.obj - ObjA ### 标记复制算法(mark-copy) ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/fuzhisuanfa.png) -原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片问题,但是内存浪费很严重,相当于只能使用 50% 的内存。。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 +原理:思路也很简单,将内存对半分,总是保留一块空着(上图中的右侧),将左侧存活的对象(浅灰色区域)复制到右侧,然后左侧全部清空。避免了内存碎片 +问题,但是内存浪费很严重,相当于只能使用50%的内存。从根集合节点进行扫描,标记出所有的存活对象,并将这些存活的对象复制到一块儿新的内存 +(图中下边的那一块儿内存)上去,之后将原来的那一块儿内存(图中上边的那一块儿内存)全部回收掉 适用场合: @@ -124,7 +127,9 @@ ObjB.obj - ObjA ### 标记-整理算法(Mark-Compact) ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/biaoji_zhengli.png) -原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理),清除完之后,将所有的存活对象左移到一起。 避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理),保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低 GC 的效率。 +原理:从根集合节点进行扫描,标记出所有的存活对象,最后扫描整个内存空间并清除没有标记的对象(即死亡对象)(可以发现前边这些就是标记-清除算法的原理), +清除完之后,将所有的存活对象左移到一起。避免了上述两种算法的缺点,将垃圾对象清理掉后,同时将剩下的存活对象进行整理挪动(类似于 windows 的磁盘碎片整理), +保证它们占用的空间连续,这样就避免了内存碎片问题,但是整理过程也会降低GC的效率。 适用场合: @@ -141,75 +146,84 @@ ObjB.obj - ObjA ### 分代收集算法(Generational Collection) -上述三种算法,每种都有各自的优缺点,都不完美。在现代 JVM 中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类:有些生命周期很短,比如一些局部变量 / 临时对象,而另一些则会存活很久,典型的比如 websocket 长连接中的 connection 对象,如下图: +上述三种算法,每种都有各自的优缺点,都不完美。在现代JVM中,往往是综合使用的,经过大量实际分析,发现内存中的对象,大致可以分为两类: +有些生命周期很短,比如一些局部变量/临时对象,而另一些则会存活很久,典型的比如websocket长连接中的connection对象,如下图: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/62226d097ac54148804119b4c239c802.png) -纵向 y 轴可以理解分配内存的字节数,横向 x 轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在 GC 后活下来。因此诞生了分代的思想,以 Hotspot 为例(JDK 7): +纵向y轴可以理解分配内存的字节数,横向x轴理解为随着时间流逝(伴随着 GC),可以发现大部分对象其实相当短命,很少有对象能在GC后活下来。 +因此诞生了分代的思想,以Hotspot为例(JDK 7): ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/e4ff361d409b6939e6da49a06b6dc677.png) -将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中 Young Genaration 更是又细为分 eden,S0,S1 三个区。 +将内存分成了三大块:年青代(Young Genaration),老年代(Old Generation), 永久代(Permanent Generation),其中Young Genaration更是又细分为eden, +S0,S1三个区。 -结合我们经常使用的一些 jvm 调优参数后,一些参数能影响的各区域内存大小值,示意图如下: +结合我们经常使用的一些jvm调优参数后,一些参数能影响的各区域内存大小值,示意图如下: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/d4e6583cd0ecfa60622526e7a4f13634.png) -注:jdk8 开始,用 MetaSpace 区取代了 Perm 区(永久代),所以相应的 jvm 参数变成 -XX:MetaspaceSize 及 -XX:MaxMetaspaceSize。 +注:jdk8开始,用MetaSpace区取代了Perm区(永久代),所以相应的jvm参数变成-XX:MetaspaceSize及-XX:MaxMetaspaceSize。 -以 Hotspot 为例,我们来分析下 GC 的主要过程: +以Hotspot为例,我们来分析下GC的主要过程: -刚开始时,对象分配在 eden 区,s0(即:from)及 s1(即:to)区,几乎是空着。 +刚开始时,对象分配在eden区,s0(即:from)及s1(即:to)区,几乎是空着。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/8dc1423cc9f85658a946e204dc85ec18.png) -随着应用的运行,越来越多的对象被分配到 eden 区。 +随着应用的运行,越来越多的对象被分配到eden区。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/c9138916377192a8a2590aca3e888049.png) -当 eden 区放不下时,就会发生 minor GC(也被称为 young GC),第 1 步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象,移动到 s0 区(即:4 个淡蓝色的方块挪到 s0 区),然后将黄色的垃圾块清理掉,这一轮过后,eden 区就成空的了。 +当eden区放不下时,就会发生minor GC(也被称为young GC),第1步当然是要先标识出不可达垃圾对象(即:下图中的黄色块),然后将可达对象, +移动到s0区(即:4个淡蓝色的方块挪到s0区),然后将黄色的垃圾块清理掉,这一轮过后,eden区就成空的了。 注:这里其实已经综合运用了“【标记 - 清理 eden】 + 【标记 - 复制 eden->s0】”算法。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/adc376cad0670c6993da8f32f3a88aba.png) -随着时间推移,eden 如果又满了,再次触发 minor GC,同样还是先做标记,这时 eden 和 s0 区可能都有垃圾对象了(下图中的黄色块),注意:这时 s1(即:to)区是空的,s0 区和 eden 区的存活对象,将直接搬到 s1 区。然后将 eden 和 s0 区的垃圾清理掉,这一轮 minor GC 后,eden 和 s0 区就变成了空的了。 +随着时间推移,eden如果又满了,再次触发minor GC,同样还是先做标记,这时eden和s0区可能都有垃圾对象了(下图中的黄色块),注意:这时s1(即:to)区是空的, +s0区和eden区的存活对象,将直接搬到s1区。然后将eden和s0区的垃圾清理掉,这一轮minor GC后,eden和s0区就变成了空的了。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f21d18a0736d0976c4ecf14e82236cb2.png) -继续,随着对象的不断分配,eden 空可能又满了,这时会重复刚才的 minor GC 过程,不过要注意的是,这时候 s0 是空的,所以 s0 与 s1 的角色其实会互换,即:存活的对象,会从 eden 和 s1 区,向 s0 区移动。然后再把 eden 和 s1 区中的垃圾清除,这一轮完成后,eden 与 s1 区变成空的,如下图。 +继续,随着对象的不断分配,eden区可能又满了,这时会重复刚才的minor GC过程,不过要注意的是,这时候s0是空的,所以s0与s1的角色其实会互换, +即:存活的对象,会从eden和s1区,向s0区移动。然后再把eden和s1区中的垃圾清除,这一轮完成后,eden与s1区变成空的,如下图。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f765eb0f46635ae093516f35fb1eee66.png) -对于那些比较“长寿”的对象一直在 s0 与 s1 中挪来挪去,一来很占地方,而且也会造成一定开销,降低 gc 效率,于是有了“代龄 (age)”及“晋升”。 +对于那些比较“长寿”的对象一直在s0与s1中挪来挪去,一来很占地方,而且也会造成一定开销,降低gc效率,于是有了“代龄 (age)”及“晋升”。 -对象在年青代的 3 个区 (edge,s0,s1) 之间,每次从 1 个区移到另 1 区,年龄 +1,在 young 区达到一定的年龄阈值后,将晋升到老年代。下图中是 8,即:挪动 8 次后,如果还活着,下次 minor GC 时,将移动到 Tenured 区。 +对象在年青代的3个区 (edge,s0,s1) 之间,每次从1个区移到另1区,年龄+1,在young区达到一定的年龄阈值后,将晋升到老年代。下图中是8,即:挪动8次后, +如果还活着,下次minor GC时,将移动到Tenured区。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/2705a4ba41ed37bd535adab5b91ffb8f.png) -下图是晋升的主要过程:对象先分配在年青代,经过多次 Young GC 后,如果对象还活着,晋升到老年代。 +下图是晋升的主要过程:对象先分配在年青代,经过多次Young GC后,如果对象还活着,晋升到老年代。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/6d24d96eb137f805c867736a750c2bd9.png) -如果老年代,最终也放满了,就会发生 major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长,会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少 Full GC 的原因。 +如果老年代,最终也放满了,就会发生major GC(即 Full GC),由于老年代的的对象通常会比较多,因为标记 - 清理 - 整理(压缩)的耗时通常会比较长, +会让应用出现卡顿的现象,这也是为什么很多应用要优化,尽量避免或减少Full GC的原因。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ebad989a70f78a40c561df9943234441.png) -注:上面的过程主要来自 oracle 官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden 区放不下,但是 old 区可以放下时,会直接分配到 old 区(即没有晋升这一过程,直接到老年代了)。 +注:上面的过程主要来自oracle官网的资料,但是有一个细节官网没有提到,如果分配的新对象比较大,eden区放不下,但是old区可以放下时,会直接分配到 +old区(即没有晋升这一过程,直接到老年代了)。 下图引自阿里出品的《码出高效 -Java 开发手册》一书,梳理了 GC 的主要过程。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/e663bd3043c6b3465edc1e7313671d69.png) - ## 垃圾回收器 -不算最新出现的神器 ZGC,历史上出现过 7 种经典的垃圾回收器。 +不算最新出现的神器ZGC,历史上出现过7种经典的垃圾回收器。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/ab1c7be2fa4b5d180ffa1f43bf9dfce7.png) -这些回收器都是基于分代的,把 G1 除外,按回收的分代划分,横线以上的 3 种:Serial ,ParNew, Parellel Scavenge 都是回收年青代的,横线以下的 3 种:CMS,Serial Old, Parallel Old 都是回收老年代的。 +这些回收器都是基于分代的,把G1除外,按回收的分代划分,横线以上的3种:Serial ,ParNew, Parellel Scavenge都是回收年青代的, +横线以下的3种:CMS,Serial Old, Parallel Old都是回收老年代的。 @@ -225,66 +239,90 @@ ObjB.obj - ObjA ParNew收集器是Serial收集器新生代的多线程实现,注意在进行垃圾回收的时候依然会stop the world,只是相比较Serial收集器而言它会运行多条进程进行垃圾回收。 -ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。 +ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不 +能百分之百的保证能超越Serial收集器。当然,随着可以使用的CPU的数量增加,它对于GC时系统资源的利用还是很有好处的。它默认开启的收集线程数与CPU的数 +量相同,在CPU非常多(譬如32个,现在CPU动辄4核加超线程,服务器超过32个逻辑CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数 +来限制垃圾收集的线程数。 ### Parallel Scavenge -Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 +Parallel是采用复制算法的多线程新生代垃圾回收器,似乎和ParNew收集器有很多的相似的地方。但是Parallel Scanvenge收集器的一个特点是它所关注的 +目标是吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。 +停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能够提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务, +主要适合在后台运算而不需要太多交互的任务。 ### CMS -全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是 JDK 7 中广泛使用的收集器,有必要多说一下,借一张网友的图说话: +全称:Concurrent Mark Sweep,从名字上看,就能猜出它是并发多线程的。这是JDK 7中广泛使用的收集器,有必要多说一下,借一张网友的图说话: ![一文看懂JVM内存布局及GC原理](https://static001.infoq.cn/resource/image/e8/4e/e8b152cf510b06544c2a13bfb4fc564e.png) -相对Serial Old 收集器或Parallel Old 收集器而言,这个明显要复杂多了,从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。它的收集过程分为四个步骤: +相对Serial Old收集器或Parallel Old 收集器而言,这个明显要复杂多了,从名字(Mark Swep)就可以看出,CMS收集器是基于标记清除算法实现的。 +它的收集过程分为四个步骤: 1. 初始标记(initial mark) 2. 并发标记(concurrent mark) 3. 重新标记(remark) 4. 并发清除(concurrent sweep) -分为 4 个阶段: - -1)Inital Mark 初始标记:主要是标记 GC Root 开始的下级(注:仅下一级)对象,这个过程会 STW,但是跟 GC Root 直接关联的下级对象不会很多,因此这个过程其实很快。 +分为4个阶段: -2)Concurrent Mark 并发标记:根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有 STW。 +- Inital Mark 初始标记 + 主要是标记GC Root开始的下级(注:仅下一级)对象,这个过程会STW,但是跟GC Root直接关联的下级对象不会很多,因此这个过程其实很快。 -3)Remark 再标志:为啥还要再标记一次?因为第 2 步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 +- Concurrent Mark 并发标记 + 根据上一步的结果,继续向下标识所有关联的对象,直到这条链上的最尽头。这个过程是多线程的,虽然耗时理论上会比较长,但是其它工作线程并不会阻塞,没有STW。 -试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上,可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 +- Remark 再标志 + 为啥还要再标记一次?因为第2步并没有阻塞其它工作线程,其它线程在标识过程中,很有可能会产生新的垃圾。 + 试想下,高铁上的垃圾清理员,从车厢一头开始吆喝“有需要扔垃圾的乘客,请把垃圾扔一下”,一边工作一边向前走,等走到车厢另一头时,刚才走过的位置上, + 可能又有乘客产生了新的空瓶垃圾。所以,要完全把这个车厢清理干净的话,她应该喊一下:所有乘客不要再扔垃圾了(STW),然后把新产生的垃圾收走。 + 当然,因为刚才已经把收过一遍垃圾,所以这次收集新产生的垃圾,用不了多长时间(即:STW 时间不会很长)。 -4)Concurrent Sweep:并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 +- Concurrent Sweep + 并行清理,这里使用多线程以“Mark Sweep- 标记清理”算法,把垃圾清掉,其它工作线程仍然能继续支行,不会造成卡顿。 -等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact 标记 - 整理”法,把垃圾清理后,剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 +等等,刚才我们不是提到过“标记清理”法,会留下很多内存碎片吗?确实,但是也没办法,如果换成“Mark Compact标记 - 整理”法,把垃圾清理后, +剩下的对象也顺便排整理,会导致这些对象的内存地址发生变化,别忘了,此时其它线程还在工作,如果引用的对象地址变了,就天下大乱了。 -另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮 GC,才会被清理掉。不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 +另外,由于这一步是并行处理,并不阻塞其它线程,所以还有一个副使用,在清理的过程中,仍然可能会有新垃圾对象产生,只能等到下一轮GC,才会被清理掉。 +不过由于CMS收集器是基于标记清除算法实现的,会导致有大量的空间碎片产生,在为大对象分配内存的时候,往往会出现老年代还有很大的空间剩余,但是无法找 +到足够大的连续空间来分配当前对象,不得不提前开启一次Full GC。 -为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行FullGC完开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 +为了解决这个问题,CMS收集器默认提供了一个-XX:+UseCMSCompactAtFullCollection收集开关参数(默认就是开启的),用于在CMS收集器进行Full GC完 +开启内存碎片的合并整理过程,内存整理的过程是无法并发的,这样内存碎片问题倒是没有了,不过停顿时间不得不变长。虚拟机设计者还提供了另外一个参 +数-XX:CMSFullGCsBeforeCompaction参数用于设置执行多少次不压缩的FULL GC后跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。 -不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。 +不幸的是,它作为老年代的收集器,却无法与jdk1.4中已经存在的新生代收集器Parallel Scavenge配合工作,所以在jdk1.5中使用cms来收集老年代的时候, +新生代只能选择ParNew或Serial收集器中的一个。ParNew收集器是使用-XX:+UseConcMarkSweepGC选项启用CMS收集器之后的默认新生代收集器,也可以使用 +-XX:+UseParNewGC选项来强制指定它。 -虽然仍不完美,但是从这 4 步的处理过程来看,以往收集器中最让人诟病的长时间 STW,通过上述设计,被分解成二次短暂的 STW,所以从总体效果上看,应用在 GC 期间卡顿的情况会大大改善,这也是 CMS 一度十分流行的重要原因。 +虽然仍不完美,但是从这4步的处理过程来看,以往收集器中最让人诟病的长时间STW,通过上述设计,被分解成二次短暂的STW,所以从总体效果上看,应用在GC期间 +卡顿的情况会大大改善,这也是CMS一度十分流行的重要原因。 ### G1 -G1 的全称是 Garbage-First,为什么叫这个名字,呆会儿会详细说明。鉴于 CMS 的一些不足之外,比如: 老年代内存碎片化,STW 时间虽然已经改善了很多,但是仍然有提升空间。G1 就横空出世了,它对于 heap 区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。G1收集器是一款面向服务端应用的垃圾收集器。HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点: +G1的全称是Garbage-First,为什么叫这个名字,呆会儿会详细说明。鉴于CMS的一些不足之外,比如:老年代内存碎片化,STW时间虽然已经改善了很多, +但是仍然有提升空间。G1就横空出世了,它对于heap区的内存划思路很新颖,有点算法中分治法“分而治之”的味道。G1收集器是一款面向服务端应用的垃圾收集器。 +HotSpot团队赋予它的使命是在未来替换掉JDK1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点: 1. 并行与并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。 2. 分代收集:和其他收集器一样,分代的概念在G1中依然存在,不过G1不需要其他的垃圾回收器的配合就可以独自管理整个GC堆。 3. 空间整合:G1收集器有利于程序长时间运行,分配大对象时不会无法得到连续的空间而提前触发一次GC。 -4. 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。 +4. 可预测的非停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,能让使用者明确指定在一个长度为M毫秒的时间片段内, + 消耗在垃圾收集上的时间不得超过N毫秒。 -在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 +在使用G1收集器时,Java堆的内存布局和其他收集器有很大的差别,它将这个Java堆分为多个大小相等的独立区域,虽然还保留新生代和老年代的概念,但是新生 +代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。 虽然G1看起来有很多优点,实际上CMS还是主流。 +如下图,G1将heap内存区,划分为一个个大小相等(1-32M,2的n次方)、内存连续的Region区域,每个region都对应Eden、Survivor、Old、Humongous +四种角色之一,但是region与region之间不要求连续。 -如下图,G1 将 heap 内存区,划分为一个个大小相等(1-32M,2 的 n 次方)、内存连续的 Region 区域,每个 region 都对应 Eden、Survivor 、Old、Humongous 四种角色之一,但是 region 与 region 之间不要求连续。 +注:Humongous,简称H区是专用于存放超大对象的区域,通常>= 1/2 Region Size,且只有Full GC阶段,才会回收H区,避免了频繁扫描、复制 / 移动大对象。 -注:Humongous,简称 H 区是专用于存放超大对象的区域,通常 >= 1/2 Region Size,且只有 Full GC 阶段,才会回收 H 区,避免了频繁扫描、复制 / 移动大对象。 - -所有的垃圾回收,都是基于 1 个个 region 的。JVM 内部知道,哪些 region 的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 +所有的垃圾回收,都是基于1个个region的。JVM内部知道,哪些region的对象最少(即:该区域最空),总是会优先收集这些 region(因为对象少,内存相对较空,肯定快),这也是 Garbage-First 得名的由来,G 即是 Garbage 的缩写, 1 即 First。 ![一文看懂JVM内存布局及GC原理](https://raw.githubusercontent.com/CharonChui/Pictures/master/f1a1bdecf4e56a4440707ce073d73f61.png) @@ -392,25 +430,24 @@ NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器( - - - - # 与GC相关的常用参数 -​ 除了上面提及的一些参数,下面补充一些和GC相关的常用参数: - -- ​ -Xmx: 设置堆内存的最大值。 -- ​ -Xms: 设置堆内存的初始值。 -- ​ -Xmn: 设置新生代的大小。 -- ​ -Xss: 设置栈的大小。 -- ​ -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 -- ​ -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 -- ​ -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 -- ​ -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 -- ​ -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。 -- ​ -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。 -- ​ -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。 +除了上面提及的一些参数,下面补充一些和GC相关的常用参数: + +- -Xmx: 设置堆内存的最大值。 +- -Xms: 设置堆内存的初始值。 +- -Xmn: 设置新生代的大小。 +- -Xss: 设置栈的大小。 +- -PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数的对象将直接在老年代分配。 +- -MaxTenuringThrehold: 晋升到老年代的对象年龄。每个对象在坚持过一次Minor GC之后,年龄就会加1,当超过这个参数值时就进入老年代。 +- -UseAdaptiveSizePolicy: 在这种模式下,新生代的大小、eden和survivor的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、 + 吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和 + 停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。 +- -SurvivorRattio: 新生代Eden区域与Survivor区域的容量比值,默认为8,代表Eden: Suvivor= 8: 1。 +- -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。 +- -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。收集器在工作时,会调整Java堆大小或者其他一些参数,尽可能地把 + 停顿时间控制在MaxGCPauseMills以内。 +- -XX:GCTimeRatio:设置吞吐量大小,它的值是一个0-100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集。 diff --git "a/JavaKnowledge/JVM\346\236\266\346\236\204.md" "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" index 33da8edb..34304137 100644 --- "a/JavaKnowledge/JVM\346\236\266\346\236\204.md" +++ "b/JavaKnowledge/JVM\346\236\266\346\236\204.md" @@ -4,21 +4,26 @@ Java的主要优势在于,它被设计为可在具有WORA(write once, run anyw Java源代码会被JDK内置的Java编译器(javac)编译成称为字节码(即.class文件)的中间状态, -这些字节码是带有操作码操作数的十六进制格式,并且JVM可以将这些指令(无需进一步重新编译)解释为操作系统和底层硬件平台可以理解的本地语言。因此,字节码充当独立于平台的中间状态,该状态可在任何JVM之间移植,而与底层操作系统和硬件体系结构无关。 +这些字节码是带有操作码操作数的十六进制格式,并且JVM可以将这些指令(无需进一步重新编译)解释为操作系统和底层硬件平台可以理解的本地语言。 +因此,字节码充当独立于平台的中间状态,该状态可在任何JVM之间移植,而与底层操作系统和硬件体系结构无关。 -但是,由于JVM是为运行基础硬件和OS结构并与之通信而开发的,因此我们需要为我们的OS版本(Windows,Linux,Mac)和处理器体系结构(x86,x64)选择适当的JVM版本。 +但是,由于JVM是为运行基础硬件和OS结构并与之通信而开发的,因此我们需要为我们的OS版本(Windows,Linux,Mac)和处理器体系结构(x86,x64) +选择适当的JVM版本。 -我们大多数人都知道Java的上述故事,这里的问题是该过程中最重要的组成部分-JVM被当作一个黑匣子教给我们,它可以神奇地解释字节码并执行许多运行时活动,例如JIT(程序执行期间进行实时)编译和GC(垃圾收集)。 +我们大多数人都知道Java的上述故事,这里的问题是该过程中最重要的组成部分-JVM被当作一个黑匣子教给我们,它可以神奇地解释字节码并执行许多运行时活动, +例如JIT(程序执行期间进行实时)编译和GC(垃圾收集)。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/JVM_Architecture.png) ## Class Loader子系统 -JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统将class文件加载到内存中。这称为Java的动态类加载(dynamic class loading)功能。当它在运行时(而非编译时)首次引用类时,它将加载、链接和初始化类文件(.class)。 +JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统将class文件加载到内存中。这称为Java的动态类加载(dynamic class loading)功能。 +当它在运行时(而非编译时)首次引用类时,它将加载、链接和初始化类文件(.class)。 - 加载 - Class Loader的主要任务就是把编译后的.class文件加载到内存中。通常,class加载过程会从加载main类(有声明static main()的类)。所有后续的类加载尝试都是根据已运行的类中的类引用完成的,如以下情况所述: + Class Loader的主要任务就是把编译后的.class文件加载到内存中。通常,class加载过程会从加载main类(有声明static main()的类)。所有后续的 + 类加载尝试都是根据已运行的类中的类引用完成的,如以下情况所述: - 当字节码静态引用一个类时 - 当字节码创建一个类对象时(例如: Person person = new Person("John")) @@ -27,19 +32,24 @@ JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统 - Bootstrap Class Loader - 加载来自rt.jar的标准JDK类,例如引导路径中存在的核心Java API类-$ JAVA_HOME/jre/lib目录(例如java.lang。*包类)。它以C / C ++之类的本地语言实现,并是Java中所有类加载器的父级。 + 加载来自rt.jar的标准JDK类,例如引导路径中存在的核心Java API类-$ JAVA_HOME/jre/lib目录(例如java.lang。*包类)。它以C / C ++ + 之类的本地语言实现,并是Java中所有类加载器的父级。 - Extension Class Loader - 将类加载请求委托给其父类Bootstrap,如果不成功,则从扩展路径中的扩展目录(例如,安全扩展功能)中加载-JAVA_HOME/jre/ext或java.ext.dirs系统指定的任何其他目录的类。该类加载器由Java中的sun.misc.Launcher$ExtClassLoader类实现。 + 将类加载请求委托给其父类Bootstrap,如果不成功,则从扩展路径中的扩展目录(例如,安全扩展功能)中加载-JAVA_HOME/jre/ext或java.ext.dirs + 系统指定的任何其他目录的类。该类加载器由Java中的sun.misc.Launcher$ExtClassLoader类实现。 - System/Application Class Loader - 从系统类路径加载应用程序特定的类,可以使用-cp或-classpath命令来在程序执行时动态设置。它在内部使用映射到java.class.path的环境变量。该类加载器由sun.misc.Launcher$AppClassLoader类用Java实现。 + 从系统类路径加载应用程序特定的类,可以使用-cp或-classpath命令来在程序执行时动态设置。它在内部使用映射到java.class.path的环境变量。 + 该类加载器由sun.misc.Launcher$AppClassLoader类用Java实现。 - 除了上面讨论的3个主要的类加载器,程序员还可以直接在代码本身上创建用户定义的类加载器。这通过类加载器委托模型保证了应用程序的独立性。这种方法用于Tomcat之类的Web应用程序服务器中,以使Web应用程序和企业解决方案独立运行。 + 除了上面讨论的3个主要的类加载器,程序员还可以直接在代码本身上创建用户定义的类加载器。这通过类加载器委托模型保证了应用程序的独立性。这种方法 + 用于Tomcat之类的Web应用程序服务器中,以使Web应用程序和企业解决方案独立运行。 - 每个类加载器都有自己的名称空间来保存已加载的类。当类加载器加载类时,它将基于存储在名称空间中的完全合格的类名称(FQCN:Fully Qualified Class Name)搜索该类,以检查该类是否已被加载。即使该类具有相同的FQCN但具有不同的名称空间,也将其视为不同的类。不同的名称空间意味着该类已由另一个类加载器加载。 + 每个类加载器都有自己的名称空间来保存已加载的类。当类加载器加载类时,它将基于存储在名称空间中的完全合格的类名称(FQCN:Fully Qualified Class Name) + 搜索该类,以检查该类是否已被加载。即使该类具有相同的FQCN但具有不同的名称空间,也将其视为不同的类。不同的名称空间意味着该类已由另一个类加载器加载。 它们遵循4个主要原则: @@ -53,7 +63,11 @@ JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统 - 委托层次结构原则 - 为了满足上述2个原则,JVM遵循一个委托层次结构来为每个请求装入的类选择类加载器。首先从最低的子级别开始,Application Class Loader将接收到的类加载请求委托给Extension Class Loader,然后Extension Class Loader将该请求委托给Bootstrap Class Loader。如果在Bootstrap路径中找到了所请求的类,则将加载该类。否则,该请求将再次被转移回Extension Class Loader加载器中从该Loader的扩展路径或自定义指定的路径中查找类。如果它也失败,则请求将返回到Application Class Loader中从该Loader的System类路径中查找该类,并且如果Application Class Loader也未能加载所请求的类,则将获得运行时异常— java.lang.ClassNotFoundException。 + 为了满足上述2个原则,JVM遵循一个委托层次结构来为每个请求装入的类选择类加载器。首先从最低的子级别开始,Application Class Loader将 + 接收到的类加载请求委托给Extension Class Loader,然后Extension Class Loader将该请求委托给Bootstrap Class Loader。如果在 + Bootstrap路径中找到了所请求的类,则将加载该类。否则,该请求将再次被转移回Extension Class Loader加载器中从该Loader的扩展路径或 + 自定义指定的路径中查找类。如果它也失败,则请求将返回到Application Class Loader中从该Loader的System类路径中查找该类,并且如果 + Application Class Loader也未能加载所请求的类,则将获得运行时异常— java.lang.ClassNotFoundException。 - 无法卸载原则 @@ -73,7 +87,9 @@ JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统 - Verification - 确保.class文件的正确性(代码是否根据Java语言规范正确编写?它是由有效的编译器根据JVM规范生成的吗?)。这是类加载过程中最复杂的测试过程,并且耗时最长。即使链接减慢了类加载过程的速度,它也避免了在执行字节码时多次执行这些检查的需要,从而使整体执行高效而有效。如果验证失败,则会引发运行时错误(java.lang.VerifyError)。例如,执行以下检查: + 确保.class文件的正确性(代码是否根据Java语言规范正确编写?它是由有效的编译器根据JVM规范生成的吗?)。这是类加载过程中最复杂的测试过程, + 并且耗时最长。即使链接减慢了类加载过程的速度,它也避免了在执行字节码时多次执行这些检查的需要,从而使整体执行高效而有效。如果验证失败, + 则会引发运行时错误(java.lang.VerifyError)。例如,执行以下检查: ```java - consistent and correctly formatted symbol table @@ -84,12 +100,10 @@ JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统 - variables are initialized before being read - variables are a value of the correct type ``` - - - - Preparation - 为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。静态字段已创建并初始化为其默认值,但是,在此阶段不执行任何初始化程序或代码,因为这是初始化的一部分。 + 为静态存储和JVM使用的任何数据结构(例如方法表)分配内存。静态字段已创建并初始化为其默认值,但是,在此阶段不执行任何初始化程序或代码, + 因为这是初始化的一部分。 - Resolution @@ -97,46 +111,52 @@ JVM驻留在内存(RAM)上。在执行过程中,会使用Class Loader子系统 - 初始化 - 在这里,将执行每个加载的类或接口的初始化逻辑(例如,调用类的构造函数)。由于JVM是多线程的,因此应在适当同步的情况下非常仔细地进行类或接口的初始化,以避免其他线程尝试同时初始化同一类或接口(即使其成为线程安全的)。 + 在这里,将执行每个加载的类或接口的初始化逻辑(例如,调用类的构造函数)。由于JVM是多线程的,因此应在适当同步的情况下非常仔细地进行类或接口的 + 初始化,以避免其他线程尝试同时初始化同一类或接口(即使其成为线程安全的)。 这是类加载的最后阶段,所有静态变量都分配有代码中定义的原始值,并且将执行静态块(如果有)。这是在类中从上到下,从类层次结构中的父级到子级逐行执行的。 ### 对象的创建 - - Java 对象的创建过程: -1. 类加载检查 - 虚拟机遇到一条 new 指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析和初始化过。如果没有,那必须先执行相应的类加载过程。 -2. 分配内存 - 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 -3. 初始化零值 + Java对象的创建过程: +1. 类加载检查 + 虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载过、解析 + 和初始化过。如果没有,那必须先执行相应的类加载过程。 +2. 分配内存 + 在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需的内存大小在类加载完成后便可确定,为对象分配空间的任务等同于把一块确定大小的内 + 存从Java堆中划分出来。分配方式有 “指针碰撞” 和 “空闲列表” 两种,选择那种分配方式由 Java 堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 +3. 初始化零值 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。 -4. 设置对象头 - 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 -5. 执行init方法 - 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行,所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 +4. 设置对象头 + 初始化零值完成之后,虚拟机要对对象进行必要的设置,例如这个对象是那个类的实例、如何才能找到类的元数据信息、对象的哈希吗、对象的 GC 分代年龄等信息。 + 这些信息存放在对象头中。 另外,根据虚拟机当前运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。 +5. 执行init方法 + 在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚开始, 方法还没有执行, + 所有的字段都还为零。所以一般来说,执行 new 指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。 ### 对象的内存布局 -在 Hotspot 虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据和对齐填充。 -Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 +在Hotspot虚拟机中,对象在内存中的布局可以分为3快区域:对象头、实例数据和对齐填充。 +Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的自身运行时数据(哈希吗、GC分代年龄、锁状态标志等等),另一部分是类型指针,即对象 +指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是那个类的实例。 实例数据部分是对象真正存储的有效信息,也是在程序中所定义的各种类型的字段内容。 -对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 +对齐填充部分不是必然存在的,也没有什么特别的含义,仅仅起占位作用。 因为Hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍, +换句话说就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或2倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。 ### 对象的访问定位 -建立对象就是为了使用对象,我们的Java程序通过栈上的 reference 数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有①使用句柄和②直接指针两种: -1. 句柄 - 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; +建立对象就是为了使用对象,我们的Java程序通过栈上的reference数据来操作堆上的具体对象。对象的访问方式有虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针两种: +1. 句柄 + 如果使用句柄的话,那么Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息; ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_jubing.png) -2. **直接指针:** 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。 +2. 直接指针 + 如果使用直接指针访问,那么 Java 堆对像的布局中就必须考虑如何防止访问类型数据的相关信息,reference 中存储的直接就是对象的地址。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java_refe_direct.png) -这两种对象访问方式各有优势。使用句柄来访问的最大好处是 reference 中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。 - - +这两种对象访问方式各有优势。使用句柄来访问的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针, +而reference本身不需要修改。使用直接指针访问方式最大的好处就是速度快,它节省了一次指针定位的时间开销。 ## Runtime Data Area @@ -146,7 +166,8 @@ Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对 - .class文件是否与Class / Interface / Enum相关 - 修饰符,静态变量和方法信息等。 -然后,对于每个已加载的.class文件,它都会按照java.lang包中的定义,恰好创建一个Class对象来表示堆内存中的文件。稍后,在我们的代码中,可以使用此Class对象读取类级别的信息(类名称,父名称,方法,变量信息,静态变量等)。 +然后,对于每个已加载的.class文件,它都会按照java.lang包中的定义,恰好创建一个Class对象来表示堆内存中的文件。稍后,在我们的代码中,可以使用此 +Class对象读取类级别的信息(类名称,父名称,方法,变量信息,静态变量等)。 [具体可参考Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md) @@ -156,21 +177,23 @@ Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对 字节码的实际执行在这里进行。执行引擎通过读取分配给上述运行时数据区域的数据逐行执行字节码中的指令。 - - ### Interpreter 解释器解释字节码并一对一执行指令。因此,它可以快速解释一个字节码行,但是执行解释后的结果是一项较慢的任务。缺点是,当多次调用一个方法时,每次都需要新的解释和较慢的执行速度。 ### Just-In-Time (JIT) Compiler -如果只有解释器可用,则当多次调用一个方法时,每次也会进行解释,如果有效处理,这将是多余的操作。使用JIT编译器已经可以做到这一点。首先,它将整个字节码编译为本地代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。本机代码存储在缓存中,因此可以更快地执行编译后的代码。 +如果只有解释器可用,则当多次调用一个方法时,每次也会进行解释,如果有效处理,这将是多余的操作。使用JIT编译器已经可以做到这一点。首先,它将整个字 +节码编译为本地代码(机器代码)。然后,对于重复的方法调用,它直接提供了本机代码,使用本机代码的执行比单步解释指令要快得多。本机代码存储在缓存中, +因此可以更快地执行编译后的代码。 -但是,即使对于JIT编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码也存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个方法。自适应编译的想法已在Oracle Hotspot VM中使用。 +但是,即使对于JIT编译器,编译所花费的时间也要比解释器所花费的时间更多。对于仅执行一次的代码段,最好对其进行解释而不是进行编译。同样,本机代码也 +存储在高速缓存中,这是一种昂贵的资源。在这种情况下,JIT编译器会在内部检查每个方法调用的频率,并仅在所选方法发生超过特定时间级别时才决定编译每个 +方法。自适应编译的想法已在Oracle Hotspot VM中使用。 -当JVM供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能: +当JVM供应商引入性能优化时,执行引擎有资格成为关键子系统。在这些工作中,以下4个组件可以大大提高其性能: -- 中间代码生成器生成中间代码。 +- 中间代码生成器生成中间代码。 - 代码优化器负责优化上面生成的中间代码。 - 目标代码生成器负责生成本机代码(即机器代码)。 - Profiler是一个特殊的组件,负责查找性能热点(例如,多次调用一种方法的实例) @@ -179,31 +202,40 @@ Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对 - Oracle Hotspot VMs - Oracle通过流行的JIT编译器模型Hotspot Compiler实现了其标准Java VM的2种实现。通过分析,它可以确定最需要JIT编译的热点,然后将代码的那些性能关键部分编译为本机代码。随着时间的流逝,如果不再频繁调用这种已编译的方法,它将把该方法标识为不再是热点,并迅速从缓存中删除本机代码,并开始在解释器模式下运行。这种方法可以提高性能,同时避免不必要地编译很少使用的代码。此外,Hotspot Compiler可以即时确定使用lining等技术来优化已编译代码的最佳方式。编译器执行的运行时分析使它可以消除在确定哪些优化将产生最大性能收益方面的猜测。 + Oracle通过流行的JIT编译器模型Hotspot Compiler实现了其标准Java VM的2种实现。通过分析,它可以确定最需要JIT编译的热点,然后将代码的那些 + 性能关键部分编译为本机代码。随着时间的流逝,如果不再频繁调用这种已编译的方法,它将把该方法标识为不再是热点,并迅速从缓存中删除本机代码,并开 + 始在解释器模式下运行。这种方法可以提高性能,同时避免不必要地编译很少使用的代码。此外,Hotspot Compiler可以即时确定使用lining等技术来优化 + 已编译代码的最佳方式。编译器执行的运行时分析使它可以消除在确定哪些优化将产生最大性能收益方面的猜测。 这些虚拟机使用相同的运行时(解释器,内存,线程),但是将自定义构建JIT编译器的实现,如下所述。 Oracle Java Hotspot Client VM是Oracle JDK和JRE的默认VM技术。它通过减少应用程序启动时间和内存占用量而在客户端环境中运行应用程序时进行了优化,以实现最佳性能。 - Oracle Java Hotspot Server VM旨在为在服务器环境中运行的应用程序提供最高的程序执行速度。此处使用的JIT编译器称为“高级动态优化编译器”,它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如Java服务器MyApp)来调用Java HotSpot Server VM。 + Oracle Java Hotspot Server VM旨在为在服务器环境中运行的应用程序提供最高的程序执行速度。此处使用的JIT编译器称为“高级动态优化编译器”, + 它使用更复杂和多样化的性能优化技术。通过使用服务器命令行选项(例如Java服务器MyApp)来调用Java HotSpot Server VM。 Oracle的Java Hotspot技术以其快速的内存分配,快速高效的GC以及易于在大型共享内存多处理器服务器中扩展的线程处理能力而闻名。 - IBM AOT (Ahead-Of-Time) Compiling - 这里的特长是这些JVM共享通过共享缓存编译的本机代码,因此已经通过AOT编译器编译的代码可以由另一个JVM使用,而无需编译。另外,IBM JVM通过使用AOT编译器将代码预编译为JXE(Java可执行文件)文件格式,提供了一种快速的执行方式。 + 这里的特长是这些JVM共享通过共享缓存编译的本机代码,因此已经通过AOT编译器编译的代码可以由另一个JVM使用,而无需编译。另外,IBM JVM通过使用 + AOT编译器将代码预编译为JXE(Java可执行文件)文件格式,提供了一种快速的执行方式。 ### Garbage Collector (GC) -只要引用了一个对象,JVM就会认为它是活动的。一旦不再引用对象,因此应用程序代码无法访问该对象,则垃圾收集器将其删除并回收未使用的内存。通常,垃圾回收是在后台进行的,但是我们可以通过调用System.gc()方法来触发垃圾回收(同样,无法保证执行。因此,请调用Thread.sleep(1000)并等待GC完成)。[具体内存回收部分请看JVM垃圾回收机制](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md) +只要引用了一个对象,JVM就会认为它是活动的。一旦不再引用对象,因此应用程序代码无法访问该对象,则垃圾收集器将其删除并回收未使用的内存。通常,垃圾 +回收是在后台进行的,但是我们可以通过调用System.gc()方法来触发垃圾回收(同样,无法保证执行。因此,请调用Thread.sleep(1000)并等待GC完成)。 +[具体内存回收部分请看JVM垃圾回收机制](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md) ### JVM线程 -我们讨论了如何执行Java程序,但没有具体提及执行程序。实际上,为了执行我们前面讨论的每个任务,JVM并发运行多个线程。这些线程中的一些带有编程逻辑,并且是由程序(应用程序线程)创建的,而其余的则是由JVM本身创建的,以承担系统中的后台任务(系统线程)。 +我们讨论了如何执行Java程序,但没有具体提及执行程序。实际上,为了执行我们前面讨论的每个任务,JVM并发运行多个线程。这些线程中的一些带有编程逻辑, +并且是由程序(应用程序线程)创建的,而其余的则是由JVM本身创建的,以承担系统中的后台任务(系统线程)。 -主应用程序线程是作为调用公共静态void main(String [])的一部分而创建的主线程,而所有其他应用程序线程都是由该主线程创建的。应用程序线程执行诸如执行以main()方法开头的指令,在Heap区域中创建对象(如果它在任何方法逻辑中找到新关键字)之类的任务等。 +主应用程序线程是作为调用公共静态void main(String [])的一部分而创建的主线程,而所有其他应用程序线程都是由该主线程创建的。 +应用程序线程执行诸如执行以main()方法开头的指令,在Heap区域中创建对象(如果它在任何方法逻辑中找到新关键字)之类的任务等。 主要的系统线程有: @@ -225,7 +257,8 @@ Hotspot虚拟机的对象头包括两部分信息,第一部分用于存储对 - VM thread - 前提条件是,某些操作需要JVM到达安全点才能执行,在该点不再进行对Heap区域的修改。这种情况的示例是“世界停止”垃圾回收,线程堆栈转储,线程挂起和有偏向的锁吊销。这些操作可以在称为VM线程的特殊线程上执行。 + 前提条件是,某些操作需要JVM到达安全点才能执行,在该点不再进行对Heap区域的修改。这种情况的示例是“世界停止”垃圾回收,线程堆栈转储,线程挂起 + 和有偏向的锁吊销。这些操作可以在称为VM线程的特殊线程上执行。 diff --git "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" index 428ac290..02a3ad83 100644 --- "a/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" +++ "b/JavaKnowledge/MVC\344\270\216MVP\345\217\212MVVM.md" @@ -15,10 +15,10 @@ MVC 分层同时也简化了分组开发。不同的开发人员可同时开发 - 优点 - 耦合性低 - - 重用性高 - - 可维护性高 - - 有利软件工程化管理 - + - 重用性高 + - 可维护性高 + - 有利软件工程化管理 + - 缺点 - 没有明确的定义 - 视图与控制器间的过于紧密的连接 @@ -34,7 +34,7 @@ MVP 所有的交互都发生在`Presenter`内部,而在`MVC`中`View`会直接从`Model`中读取数据而不是通过`Controller`。 在`MVC`里,`View`是可以直接访问`Model`的!从而,`View`里会包含`Model`信息,不可避免的还要包括一些业务逻辑。 在`MVC`模型里,更关注的`Model`的不变,而同时有多个对`Model`的不同显示及`View`。所以,在`MVC`模型里,`Model`不依赖于`View`,但是`View`是依赖于`Model`的。 -不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 +不仅如此,因为有一些业务逻辑在`View`里实现了,导致要更改`View`也是比较困难的,至少那些业务逻辑是无法重用的。 ![image](https://github.com/CharonChui/Pictures/blob/master/is-activity-god-the-mvp-architecture-10-638.jpg?raw=true) @@ -56,8 +56,8 @@ MVP - 缺点 由于对视图的渲染放在了`Presenter`中,所以视图和`Presenter`的交互会过于频繁。还有一点需要明白,如果`Presenter`过多地渲染了视图, - 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 - 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 + 往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么`Presenter`也需要变更了。 + 比如说,原本用来呈现`Html`的`Presenter`现在也需要用于呈现Pdf了,那么视图很有可能也需要变更。 MVVM --- @@ -66,9 +66,11 @@ MVVM是Model-View-ViewModel的简写。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/MVVM.png) -MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致,同样是以VM为核心,但是不同于MVP,MVVM采用了数据双向绑定的方案,替代了繁琐复杂的DOM操作。该模型中,View与VM保持同步,View绑定到VM的属性上,如果VM数据发生变化,通过数据绑定的方式,View会自动更新视图;VM同样也暴露出Model中的数据。 +MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致,同样是以VM为核心,但是不同于MVP,MVVM采用了数据双向绑定的方案,替代了繁琐复杂的DOM操作。 +该模型中,View与VM保持同步,View绑定到VM的属性上,如果VM数据发生变化,通过数据绑定的方式,View会自动更新视图;VM同样也暴露出Model中的数据。 -看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM +看起来MVVM很好的解决了MVC和MVP的不足,但是由于数据和视图的双向绑定,导致出现问题时不太好定位来源,有可能数据问题导致,也有可能业务逻辑中对视图 +属性的修改导致。如果项目中打算用MVVM的话可以考虑使用官方的架构组件ViewModel、LiveData、DataBinding去实现MVVM @@ -83,4 +85,4 @@ MVVM模式将Presener改名为View Model,基本上与MVP模式完全一致, - 邮箱 :charon.chui@gmail.com - Good Luck! - + diff --git "a/JavaKnowledge/Top-K\351\227\256\351\242\230.md" "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" index 4acb0c67..d716c354 100644 --- "a/JavaKnowledge/Top-K\351\227\256\351\242\230.md" +++ "b/JavaKnowledge/Top-K\351\227\256\351\242\230.md" @@ -21,19 +21,28 @@ Top-K问题 - 局部淘汰法 - 该方法与排序方法类似,用一个容器保存前`10000`个数,然后将剩余的所有数字逐一与容器内的最小数字相比,如果所有后续的元素都比容器内的`10000`个数还小,那么容器内这个`10000`个数就是最大`10000`个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这`1`亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为`O(n+m^2)`,其中`m`为容器的大小,即`10000`。 + 该方法与排序方法类似,用一个容器保存前`10000`个数,然后将剩余的所有数字逐一与容器内的最小数字相比,如果所有后续的元素都比容器内的`10000`个数还小, + 那么容器内这个`10000`个数就是最大`10000`个数。如果某一后续元素比容器内最小数字大,则删掉容器内最小元素,并将该元素插入容器,最后遍历完这`1` + 亿个数,得到的结果容器中保存的数即为最终结果了。此时的时间复杂度为`O(n+m^2)`,其中`m`为容器的大小,即`10000`。 - 分治法 - 将`1`亿个数据分成`100`份,每份`100`万个数据,找到每份数据中最大的`10000`个,最后在剩下的`100*10000`个数据里面找出最大的`10000`个。如果`100`万数据选择足够理想,那么可以过滤掉`1`亿数据里面`99%`的数据。`100`万个数据里面查找最大的`10000`个数据的方法如下:用快速排序的方法,将数据分为`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大堆个数`N`小于`10000`个,就在小的那堆里面快速排序一次,找第`10000-n`大的数字;递归以上过程,就可以找到第`10000`大的数。一共需要101次这样的比较。 + 将`1`亿个数据分成`100`份,每份`100`万个数据,找到每份数据中最大的`10000`个,最后在剩下的`100*10000`个数据里面找出最大的`10000`个。 + 如果`100`万数据选择足够理想,那么可以过滤掉`1`亿数据里面`99%`的数据。`100`万个数据里面查找最大的`10000`个数据的方法如下: + 用快速排序的方法,将数据分为`2`堆,如果大的那堆个数`N`大于`10000`个,继续对大堆快速排序一次分成`2`堆,如果大的那堆个数`N`大于`10000`个, + 继续对大堆快速排序一次分成`2`堆,如果大堆个数`N`小于`10000`个,就在小的那堆里面快速排序一次,找第`10000-n`大的数字;递归以上过程,就可 + 以找到第`10000`大的数。一共需要101次这样的比较。 - `Hash`法 - 如果这`1`亿个书里面有很多重复的数,先通过`Hash`法,把这`1`亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间,然后通过分治法或最小堆法查找最大的`10000`个数。 + 如果这`1`亿个数里面有很多重复的数,先通过`Hash`法,把这`1`亿个数字去重复,这样如果重复率很高的话,会减少很大的内存用量,从而缩小运算空间, + 然后通过分治法或最小堆法查找最大的`10000`个数。 - 最小堆 - 首先读入前`10000`个数来创建大小为`10000`的最小堆,建堆的时间复杂度为`O(mlogm)`(`m`为数组的大小即为`10000`),然后遍历后续的数字,并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至`1`亿个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有`10000`个数字。该算法的时间复杂度为`O(nmlogm)`,空间复杂度是10000(常数)。 + 首先读入前`10000`个数来创建大小为`10000`的最小堆,建堆的时间复杂度为`O(mlogm)`(`m`为数组的大小即为`10000`),然后遍历后续的数字, + 并于堆顶(最小)数字进行比较。如果比最小的数小,则继续读取后续数字;如果比堆顶数字大,则替换堆顶元素并重新调整堆为最小堆。整个过程直至`1`亿 + 个数全部遍历完为止。然后按照中序遍历的方式输出当前堆中的所有`10000`个数字。该算法的时间复杂度为`O(nmlogm)`,空间复杂度是10000(常数)。 解决`Top K`问题有两种思路: diff --git "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" index eef7b2d9..1ade6bd3 100644 --- "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" +++ "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" @@ -18,10 +18,23 @@ Vim使用教程 - `i` 进入`insert`模式 - `a(append)` 在光标后进行插入,直接进入`insert`模式 - `o(open a line below)` 在当前行后插入一个新行,直接进入`insert`模式 -- `I` 从改行的最前面开始编辑 -- `A` 从改行的最后面开始编辑 +- `I` 从该行的最前面开始编辑 +- `A` 从该行的最后面开始编辑 - `cw` 替换从光标位置开始到该单词结束位置的所有字符,直接进入`Insert`模式 +在VIM中,有相当一部分命令可以扩展为3部分: + +- 开头的部分是一个数字,代表重复次数; +- 中间的部分是命令; +- 最后的部分,代表命令的对象。 + +比如,命令3de中,3表示执行3次,d是删除命令,e表示从当前位置到单词的末尾。整条命令的意思就是,从当前位置向后,删除3个单词。类似的,命令3ce表示从当前位置向后,删除三个单词,然后进入编辑模式。 + +可以看到,命令组合的前两个部分比较简单,但第三个部分也就是命令对象,技巧就比较多了。所以接下来,我就与你详细介绍下到底有哪些命令对象可以使用。 + +其实,对命令对象并没有一个完整的分类。但我根据经验,将其总结为光标移动命令和文本对象两种。 + +第一种是光标移动命令。比如,$命令是移动光标到本行末尾,那么d$就表示删除到本行末尾;再比如,4}表示向下移动4个由空行隔开的段落,那么d4}就是删除这4个段落。 移动光标 --- @@ -36,7 +49,8 @@ Vim使用教程 - `$` $光标移动当前行尾 - `0` 数字0光标移动当前行首 -- `e` 移动到单词结束位置 +- `e` 向右移动一个单词 +- `w` 向右移动一个单词,与e的区别是w是把光标放到下一个单词的开头,而e是把光标放到这一个单词的结尾。 - `b` 移动到单词开始位置 - `:59` 移动到59行 - `#l` 移动光标到该行第#个字的位置,如`5l` @@ -96,7 +110,7 @@ Vim使用教程 - `>>` 当前行缩进 - `#>>` 当前光标下n行缩进 - `<<` 当前行缩出 -- `>>` 当前光标下n行缩出 +- `#<<` 当前光标下n行缩出 - `: set nu` 会在文件每一行前面显示行号 - `:wq` 保存并退出 diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" index 90661d16..f8e8413f 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -1,19 +1,20 @@ 数据结构 === -结构,简单的理解就是关系,比如分子结构,就是说组成分子的原子之间的排列方式。严格点说,结构是指各个组成部分相互搭配和排列的方式。在现实世界中,不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。那数据结构是什么? +结构,简单的理解就是关系,比如分子结构,就是说组成分子的原子之间的排列方式。严格点说,结构是指各个组成部分相互搭配和排列的方式。在现实世界中, +不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。那数据结构是什么? 数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。 在计算机中,数据元素并不是孤立、杂乱无序的,而是具有内在联系的数据集合。数据元素之间存在的一种或多种特定关系,也就是数据的组织形式。 - 按照视点的不同,我们把数据结构分为逻辑结构和物理结构。 #### 逻辑结构 逻辑结构:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种: - 集合结构 - 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是“同属于一个集合”。数据结构中的集合关系就类似于数学中的集合 + 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是"同属于一个集合"。 + 数据结构中的集合关系就类似于数学中的集合 - 线性结构 线性结构:线性结构中的数据元素之间是一对一的关系 - 树形结构 @@ -22,16 +23,15 @@ 图形结构:图形结构的数据元素是多对多的关系 #### 物理结构 -物理结构(有些成为存储结构):是指数据的逻辑结构在计算机中的存储形式。 +物理结构(也称为存储结构):是指数据的逻辑结构在计算机中的存储形式。 - 顺序存储结构 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的 - 这种存储结构其实很简单,说白了,就是排队占位。大家都按顺序排好,每个人占一小段空间,大家谁也别插谁的队。我们之前学计算机语言时,数组就是这样的顺序存储结构。当你告诉计算机,你要建立一个有9个整型数据的数组时,计算机就在内存中找了片空地,按照一个整型所占位置的大小乘以9,开辟一段连续的空间,于是第一个数组数据就放在第一个位置,第二个数据放在第二个,这样依次摆放。 + 这种存储结构其实很简单,说白了,就是排队占位。大家都按顺序排好,每个人占一小段空间,大家谁也别插谁的队。数组就是这 + 样的顺序存储结构。当你告诉计算机,你要建立一个有9个整型数据的数组时,计算机就在内存中找了片空地,按照一个整型所占位置的大小乘以9,开辟一段 + 连续的空间,于是第一个数组数据就放在第一个位置,第二个数据放在第二个,这样依次摆放。 - 链式存储结构 - 链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置 - - - - + 链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此 + 需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置 常见的数据结构: @@ -47,7 +47,8 @@ ## 线性表 -零个或多个数据元素的有限序列。除第一个元素外,每一个元素有且只有一个直接前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后继元素。数据元素之间的关系是一对一的关系。 +零个或多个数据元素的有限序列。除第一个元素外,每一个元素有且只有一个直接前驱元素,除了最后一个元素外,每一个元素有且只有一个直接后继元素。 +数据元素之间的关系是一对一的关系。 - 序列 也就是说元素之间是有顺序的。 @@ -56,14 +57,16 @@ 线性表的顺序存储结构,在存、读数据时,不管是哪个位置,时间复杂度都是O(1); 而插入或删除时,时间复杂度都是O(n)。 -这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。当然,它的优缺点还不只这些。。 +这就说明,它比较适合元素个数不太变化,而更多是存取数据的应用。当然,它的优缺点还不止这些。 由于线性表的顺序存储结构在插入和删除的时候需要移动大量元素,为了解决这个问题,有了线性表的链式存储结构。 又分为单链表和多链表。 ### 数组`(Array)` -数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。 +数组是一种大小固定的数据结构,对线性表的所有操作都可以通过数组来实现。数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋, +有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。虽然数组一旦创建之后,它的大小就无法改变了,但是当数组不能再存储 +线性表中的新元素时,我们可以创建一个新的大的数组来替换当前数组。这样就可以使用数组实现动态的数据结构。 ```java int[] arr = new int[10]; @@ -78,13 +81,13 @@ int[] arr = new int[10]; 缺点: - 增删慢,插入和删除的花费开销比较大,比如当在第一个位置前插入一个元素,那么首先要把所有的元素往后移动一个位置 -- 大小固定,只能存储单一元素, +- 大小固定,只能存储单一元素 ### 链表 链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。 -每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,链表比较方便插入和删除操作。 +每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。相比于线性表顺序结构,链表比较方便插入和删除操作。 用一组地址任意的存储单元存放线性表中的数据元素。以元素(数据元素的映象)+指针(指示后继元素存储位置) = 结点。 以“结点的序列”表示线性表,称作线性链表(单链表)。单链表是一种顺序存取的结构,为找第`i`个数据元素,必须先找到第`i-1`个数据元素。 @@ -95,20 +98,18 @@ int[] arr = new int[10]; │data│next│ └──┴──┘──┘ ``` -`data`域:存放结点值的数据域    +`data`域:存放结点值的数据域 `next`域:存放结点的直接后继的地址(位置)的指针域(链域)。 - -链表又分为很多种: 静态链表、循环链表、单链表、双向链表 +链表又分为很多种:静态链表、循环链表、单链表、双向链表 注意: -- 链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。    +- 链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。 - 每个结点只有一个链域的链表称为单链表`(Single Linked List)` 所谓的链表就好像火车车厢一样,从火车头开始,每一节车厢之后都连着后一节车厢。 - 单链表插入和删除算法,它们其实都是由两部分组成: 第一部分就是遍历查找第i个元素; 第二部分就是插入和删除元素。 @@ -122,7 +123,7 @@ int[] arr = new int[10]; 优点: - 和数组相比,链表的优势在于长度不受限制,也不需要连续的内存空间。 -- 在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组想同,实际效率上还是比数组要高很多,所以插入快,删除快 +- 在进行插入和删除操作时,不需要移动数据项,故尽管某些操作的时间复杂度与数组相同,实际效率上还是比数组要高很多,所以插入快,删除快 缺点: @@ -130,14 +131,16 @@ int[] arr = new int[10]; - 查找慢 - 相对数组只存储元素,链表的元素还要存储其他元素地址,内存开销相对增大 - ### 栈`(Stack)` 栈是限定仅在表尾进行插入和删除操作的线性表。 我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构。 -> The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack for an item and discover how far it is from the top. -When a stack is first created, it contains no items. +> The Stack class represents a last-in-first-out (LIFO) stack of objects. It extends class Vector with five operations +> that allow a vector to be treated as a stack. The usual push and pop operations are provided, as well as a method to +> peek at the top item on the stack, a method to test for whether the stack is empty, and a method to search the stack +> for an item and discover how far it is from the top. +> When a stack is first created, it contains no items. 栈是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫作栈顶,数据称为压栈,移除数据称为弹栈(就像子弹弹夹装弹和取弹一样)。 对栈的基本操作有`push`(进栈)和`pop`(出栈),前者相当于插入,后者相当于删除最后一个元素。栈有时又叫作`LIFO(Last In First Out)`表, @@ -155,17 +158,23 @@ When a stack is first created, it contains no items. ### 链栈 栈的顺序存储结构,我们现在来看看栈的链式存储结构,简称为链栈。 -对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是否溢出的问题。 -对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。 +对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那此时的计算机操作系统已经面临死机崩溃的情况,而不是这个链栈是 +否溢出的问题。 +对比一下顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1)。对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题, +但它的优势是存取时定位很方便,而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制。所以它们的区别和线性表中讨论的 +一样,如果栈的使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果它的变化在可控范围内,建议使用顺序栈会更好一些。 #### 栈的应用-递归 - 把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。 当然,写递归程序最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。 -那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 +那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。 +递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据, +并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。 +简单的说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出, +用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态。 ### 队列`(Queue)` @@ -173,7 +182,8 @@ When a stack is first created, it contains no items. 队列是只允许在一端进行插入操作、而在另一端进行删除操作的线性表。 队列是一种先进先出(First In First Out)的线性表,简称FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。 -队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 +队列是一种特殊的线性表,特殊之处在于它只允许在表的前端`(front)`进行删除操作,而在表的后端`(rear)`进行插入操作,和栈一样,队列是一种操作受限制 +的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。先进先出,简单暴力的理解就是吃进去拉出来 优点: @@ -195,8 +205,9 @@ When a stack is first created, it contains no items. ### 树`(Tree)` - -树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中:(1)有且仅有一个特定的称为根(Root)的结点;(2)当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 +树(Tree)是n(n≥0)个结点的有限集。n=0时称为空树。在任意一棵非空树中: +- 有且仅有一个特定的称为根(Root)的结点; +- 当n>1时,其余结点可分为m(m>0)个互不相交的有限集T1、T2、……、Tm,其中每一个集合本身又是一棵树,并且称为根的子树(SubTree)。 树是由`n(n>=1)`个有限节点组成一个具有层次关系的集合。 @@ -241,7 +252,8 @@ When a stack is first created, it contains no items. ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/wanquan_binary_tree.jpg?raw=true) -完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。 +完全二叉树是效率很高的数据结构,堆是一种完全二叉树或者近似完全二叉树,所以效率极高,像十分常用的排序算法、Dijkstra算法、Prim算法等都要用堆才 +能优化,二叉排序树的效率也要借助平衡性来提高,而平衡性基于完全二叉树。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/man_binary_tree.png?raw=true) @@ -250,20 +262,25 @@ When a stack is first created, it contains no items. 我们知道一颗基本的二叉排序树他们都需要满足一个基本性质:即树中的任何节点的值大于它的左子节点,且小于它的右子节点。 -按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树),这样势必会导致二叉树的检索效率大大降低`(O(n))`,所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,在平衡二叉搜索树中,其高度一般都良好地维持在`O(log2n)`,大大降低了操作的时间复杂度。如:`AVL`,`SBT`,伸展树,`TREAP` ,红黑树等等。 +按照这个基本性质使得树的检索效率大大提高。我们知道在生成二叉排序树的过程是非常容易失衡的,最坏的情况就是一边倒(只有右/左子树), +这样势必会导致二叉树的检索效率大大降低`(O(n))`,所以为了维持二叉排序树的平衡,大牛们提出了各种平衡二叉树的实现算法,在平衡二叉搜索树中, +其高度一般都良好地维持在`O(log2n)`,大大降低了操作的时间复杂度。如:`AVL`,`SBT`,伸展树,`TREAP` ,红黑树等等。 #### 平衡二叉树 -平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一个子节点,其左右子树的高度都相近。下面给出平衡二叉树的示意图: +平衡二叉树必须具备如下特性:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。也就是说该二叉树的任何一 +个子节点,其左右子树的高度都相近。下面给出平衡二叉树的示意图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/pingheng_binary_tree.jpg?raw=true) - #### 红黑树 -红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组。它是在1972年由`Rudolf Bayer`发明的,他称之为"对称二叉B树",它现代的名字是在`Leo J. Guibas`和`Robert Sedgewick`于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在`O(log n)`时间内做查找,插入和删除,这里的`n`是树中元素的数目。 +红黑树顾名思义就是结点是红色或者是黑色的平衡二叉树,它通过颜色的约束来维持着二叉树的平衡。红黑树是一种自平衡二叉查找树,是在计算机科学中用到的 +一种数据结构,典型的用途是实现关联数组。它是在1972年由`Rudolf Bayer`发明的,他称之为"对称二叉B树",它现代的名字是在`Leo J. Guibas`和 +`Robert Sedgewick`于1978年写的一篇论文中获得的。它是复杂的,但它的操作有着良好的最坏情况运行时间,并且在实践中是高效的: 它可以在`O(log n)` +时间内做查找,插入和删除,这里的`n`是树中元素的数目。 对于一棵有效的红黑树而言我们必须增加如下规则,这也是红黑树最重要的5点规则: @@ -275,7 +292,6 @@ When a stack is first created, it contains no items. 这些约束强制了红黑树的关键性质: 从根到叶子最长的可能路径不多于最短的可能路径的两倍长。结果是这棵树大致上是平衡的。 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/hongheishu.jpg?raw=true) 黑红树节点的`java`表示结构: @@ -328,19 +344,6 @@ private boolean isRed(Node x){ } ``` - - - - - - - - - - - - - 哈希表`(Hash)` --- @@ -367,13 +370,11 @@ private boolean isRed(Node x){ - 堆是完全二叉树 - 常常用数组实现 - ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/heap_1.png?raw=true) 二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆和最小堆。 最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。 - 优点: - 插入、删除快,对最大数据项存取快 @@ -386,8 +387,9 @@ private boolean isRed(Node x){ 图`(Graph)` --- - -图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中,节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、化学、电讯工程、计算机科学以及数学的其他分支中。 +图是一种较线性表和树更为复杂的数据结构,在线性表中,数据元素之间仅有线性关系,在树形结构中,数据元素之间有着明显的层次关系,而在图形结构中, +节点之间的关系可以是任意的,图中任意两个数据元素之间都可能相关。图的应用相当广泛,特别是近年来的迅速发展,已经渗入到诸如语言学、逻辑学、物理、 +化学、电讯工程、计算机科学以及数学的其他分支中。 图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。 @@ -402,25 +404,9 @@ private boolean isRed(Node x){ - - - - - - - - - - - - - - - - - + --- - 邮箱 :charon.chui@gmail.com - Good Luck! - + 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 a6c78e71..72fdaebe 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" @@ -13,7 +13,9 @@ [Kotlin官网](https://kotlinlang.org/) `JetBrains`这家公司非常牛逼,开发了很多著名的软件,他们在使用`Java`的过程中发现`java`比较笨重不方便,所以就开发了`kotlin`,`kotlin`是 -一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 但是JetBrains团队设计Kotlin所要面临的第一个问题就是必须兼容他们所拥有的数百万行Java代码库,这也代表了Kotlin基于整个Java社区所承载的使命之一,即需要与现有的Java代码完全兼容。这个背景也决定了Kotlin的核心目标--为Java程序员提供一门更好的编程语言。 +一种全栈的开发语言,可以用它进行开发`web`、`web`后端、`Android`等。 +但是JetBrains团队设计Kotlin所要面临的第一个问题就是必须兼容他们所拥有的数百万行Java代码库,这也代表了Kotlin基于整个Java社区所承载的使命之一, +即需要与现有的Java代码完全兼容。这个背景也决定了Kotlin的核心目标--为Java程序员提供一门更好的编程语言。 很多开发者都说`Google`学什么不好,非要学苹果,出个`android`的`swift`版本,一定会搞不起来没人用,所以不用浪费时间去学习。在这里想引用马云 的一句话: @@ -143,11 +145,12 @@ class MainActivity : AppCompatActivity() { } ``` - - ## 变量 -变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。如果var代表了varible(变量),那么val可看成value(值)的缩写,但是也有人觉得这样并不直观或准确,而是把val解释成varible+final,即通过val声明的变量具有Java中的final关键字的效果(我们通过查看对val语法反编译后转化的java代码,从中可以很清楚的发现它是用final实现的),也就是引用不可变。因此,val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。 +变量可以很简单地定义成可变`var`(可读可写)和不可变`val`(只读)的变量。如果var代表了variable(变量),那么val可看成value(值)的缩写, +但是也有人觉得这样并不直观或准确,而是把val解释成variable+final,即通过val声明的变量具有Java中的final关键字的 +效果(我们通过查看对val语法反编译后转化的java代码,从中可以很清楚的发现它是用final实现的),也就是引用不可变。 +因此,val声明的变量是只读变量,它的引用不可更改,但并不代表其引用对象也不可变。事实上,我们依然可以修改引用对象的可变成员。 声明: ```kotlin @@ -161,7 +164,8 @@ book.printName() // Diving into Kotlin 再提示一下:`kotlin`中每行代码结束不需要分号了,不要和`java`是的每行都带分号 -字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践是省略变量的类型我们可以让编译器自己去推断出具体的类型,**Kotlin拥有比Java更加强大的类型推导功能,这避免了静态类型语言在编码时需要书写大量类型的弊端**: +字面上可以写明具体的类型。这个不是必须的,但是一个通用的`Kotlin`实践是省略变量的类型我们可以让编译器自己去推断出具体的类型, +**Kotlin拥有比Java更加强大的类型推导功能,这避免了静态类型语言在编码时需要书写大量类型的弊端**: ```kotlin var age = 18 // int val name = "charon" // string @@ -170,40 +174,41 @@ var weight = 70.5 // double ``` 在`Kotlin`中,一切都是对象。没有像`Java`中那样的原始基本类型。 -当然,像`Integer`,`Float`或者`Boolean`等类型仍然存在,但是它们全部都会作为对象存在的。基本类型的名字和它们工作方式都是与`Java`非常相似的,但是有一些不同之处你可能需要考虑到: - -- 数字类型中不会自动转型。举个例子,你不能给`Double`变量分配一个`Int`。必须要做一个明确的类型转换,可以使用众多的函数之一: - ```kotlin - private var age = 18 - private var weight = age.toFloat() - ``` +当然,像`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() - ``` + ```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 - ``` + ```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] + ```kotlin + var s = "charon" + var c = s[2] - for (a in s) { - Log.e("@@@", a + ""); - } - ``` + for (a in s) { + Log.e("@@@", a + ""); + } + ``` @@ -211,9 +216,11 @@ 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中: +如果你使用val来声明一个变量,那么该变量所存储的对象的引用将不可修改。然而如果你使用var声明了一个变量,你可以对该变量重新赋值。 +例如,如果我们使用代码: `x = 6`,将x的值赋为6,此时会创建一个值为6的新Int对象,并且x会存放该对象的引用。下面新的引用会替代原有的引用值被存放在x中: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/var_chage.jpg?raw=true) @@ -221,7 +228,8 @@ var weight = 70.5 // double ### 优先使用val来避免副作用 -在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 +在很多Kotlin的学习资料中,都会传递一个原则:优先使用val来声明变量。这相当正确,但更好的理解可以是:尽可能采用val、不可变对象及纯函数来设计程序。 +关于纯函数的概念,其实就是没有副作用的函数,具备引用透明性。 简单来说,副作用就是修改了某处的某些东西,比如说: @@ -282,7 +290,9 @@ var size: Int = 2 这个例子中就会内存溢出。 `kotlin`为此提供了一种我们要说的后端变量,也就是`field`。编译器会检查函数体,如果使用到了它,就会生成一个后端变量,否则就不会生成。 -我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。setter通过field标识更新变量属性值。field指的是属性的支持字段,你可以将其视为对属性的底层值的引用。在getter和setter中使用field代替属性名称很重要,因为这样可以阻止你陷入无限循环中。 +我们在使用的时候,用`field`代替属性本身进行操作。按照惯例`set`参数的名称是`value`,但是如果你喜欢你可以选择一个不同的名称。 +setter通过field标识更新变量属性值。field指的是属性的支持字段,你可以将其视为对属性的底层值的引用。在getter和setter中使用field代替属性名称 +很重要,因为这样可以阻止你陷入无限循环中。 ```kotlin class A { @@ -314,12 +324,15 @@ var myProperty: String } ``` -这意味着无论何时当你使用点操作符来获取或设置属性值时,实际上你总是调用了属性的getter或是setter。那么,为什么编译器要这么做呢?为属性添加getter和setter意味着有访问该属性的标准方法。getter处理获取值的所有请求,而setter处理所有属性值设置的请求。因此,如果你想要改变处理这些请求的方式,你可以在不破坏任何人代码的前提下进行。通过将其包装在getter和setter中来输出对属性的直接访问称为数据隐藏。 +这意味着无论何时当你使用点操作符来获取或设置属性值时,实际上你总是调用了属性的getter或是setter。那么,为什么编译器要这么做呢? +为属性添加getter和setter意味着有访问该属性的标准方法。getter处理获取值的所有请求,而setter处理所有属性值设置的请求。 +因此,如果你想要改变处理这些请求的方式,你可以在不破坏任何人代码的前提下进行。通过将其包装在getter和setter中来输出对属性的直接访问称为数据隐藏。 ## 延迟初始化 -我们说过,在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 -假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),这就与`Kotlin`的规则是相背的,此时我们可以声明一个属性并延迟其初始化,此属性用`lateinit`修饰符修饰。 +在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 +假如想在类内声明一个`null`属性,在需要时再进行初始化(最典型的就是懒汉式单例模式),这就与`Kotlin`的规则是相背的,此时我们可以声明一个属性并 +延迟其初始化,此属性用`lateinit`修饰符修饰。 ```kotlin class MainActivity : AppCompatActivity() { @@ -342,7 +355,7 @@ class MainActivity : AppCompatActivity() { } ``` 需要注意的是,我们在使用的时候,一定要确保属性是被初始化过的,通常先调用初始化方法,否则会有异常。 -如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的。 +如果只是用`lateinit`声明了,但是还没有调用初始化方法就使用,哪怕你判断了该变量是否为`null`也是会`crash`的: ```kotlin private lateinit var test: String @@ -375,7 +388,7 @@ class Test{ } } ``` -注意: :myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省 +注意: ::myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省 除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: ```kotlin @@ -400,9 +413,11 @@ test is not null haha - `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 - `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 -lazy的背后是接受一个lambda并返回一个Lazy实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。 +lazy的背后是接收一个lambda并返回一个Lazy实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。 -另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。例如: +另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。 +但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。 +你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。例如: ```kotlin val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { @@ -416,6 +431,23 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { } ``` +- 尽量不要使用lateinit来定义不可空类型的变量,可能会在使用时出现null的情况 +- 只读变量(val修饰)可以使用by lazy { }实现懒加载,可变变量(var修饰)使用改写get方法的形式实现懒加载 +```kotlin +// 只读变量 +private val lazyImmutableValue: String by lazy { + "Hello" +} + +// 可变变量 +private var lazyValue: Fragment? = null + get() { + if (field == null) { + field = Fragment() + } + return field + } +``` ## 类的定义:使用`class`关键字 @@ -424,14 +456,15 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { - 每个对象自身的特点 - 对象自身的特点称为属性(properties)。它们代表了对象自身的状态(数据),并且该类中的每一个对象都有自己独特的数值。例如,一个狗(Dog)类可能有名字(name)、体重(weight)和品种(breed)属性。一个歌曲(Song)类可能有标题(title)和演唱者(artist)属性。 + 对象自身的特点称为属性(properties)。它们代表了对象自身的状态(数据),并且该类中的每一个对象都有自己独特的数值。 + 例如,一个狗(Dog)类可能有名字(name)、体重(weight)和品种(breed)属性。一个歌曲(Song)类可能有标题(title)和演唱者(artist)属性。 - 每个对象的行为 - 对象的行为是它们的函数(functions)。它们决定了对象的行为,并且可能会使用对象的属性。例如,上面提到的Dog类,可能具有吠叫(bark)函数;Song这个类可能会有播放(play)函数。 - - + 对象的行为是它们的函数(functions)。它们决定了对象的行为,并且可能会使用对象的属性。例如,上面提到的Dog类,可能具有吠叫(bark)函数; + Song这个类可能会有播放(play)函数。 + 类可以包含: - 构造函数和初始化块 @@ -441,17 +474,14 @@ val sex: String by lazy(LazyThreadSafetyMode.NONE) { - 对象声明 - -你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以拥有自己独有的属性值。例如,每个Dog对象都有自己的名称、重量和品种属性,每个Dog的属性值都可以是不同的。 +你可以将类想象成一个对象的模板,因为它告诉编译器如何创建该特定类的对象。它还将告诉编译器每个对象应该具有哪些属性,并且从该类生成的每个对象都可以 +拥有自己独有的属性值。例如,每个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() { + fun bark() { } } @@ -493,14 +523,11 @@ var myDog = Dog("Fido", 70, "Mixed") ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_dog_new.jpg?raw=true) - - - - - ### 构造函数 -构造函数包含了初始化对象所需的代码。它在对象被分配给引用之前运行,这意味着你有机会对对象进行一些内部操作以便其被使用。大多人使用构造函数来定义对象的属性,并且给这些属性赋值。每当你创建一个新的对象,该对象所属的类的构造函数将会被调用。构造函数在你初始化对象时被调用。它通常被用于定义对象的属性,并且对属性赋值。 +构造函数包含了初始化对象所需的代码。它在对象被分配给引用之前运行,这意味着你有机会对对象进行一些内部操作以便其被使用。大多人使用构造函数来定义对 +象的属性,并且给这些属性赋值。每当你创建一个新的对象,该对象所属的类的构造函数将会被调用。构造函数在你初始化对象时被调用。它通常被用于定义对象的 +属性,并且对属性赋值。 在`Kotlin`中的一个类可以有一个主构造函数和一个或多个次构造函数。 @@ -586,7 +613,9 @@ val bird1 = Bird(color = "black") val bird2 = Bird(weight = 1000.00, color = "black") ``` -上面在Bird类中使用了val或者var来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面也使得我们在构造类的语法上得到了简化。事实上,构造方法的参数名前当然可以没有val和var。然而带上它们之后就等价于在Bird类内部声明了一个同名的属性,我们可以用this来进行调用。比如,上面定义的Bird类就类似于一下实现: +上面在Bird类中使用了val或者var来声明构造方法的参数。这一方面代表了参数的引用可变性,另一方面也使得我们在构造类的语法上得到了简化。事实上, +构造方法的参数名前当然可以没有val和var。然而带上它们之后就等价于在Bird类内部声明了一个同名的属性,我们可以用this来进行调用。 +比如,上面定义的Bird类就类似于一下实现: ```kotlin // 构造方法参数名前没有val @@ -604,7 +633,8 @@ class Bird (weight: Double = 0.00, age: Int = 0, color: String = "blue"){ #### init语句块 -Kotlin引入了一种叫作init语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。Bird类的构造方法在类的外部,它只能对参数进行赋值。如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如: +Kotlin引入了一种叫作init语句块的语法,它属于上述构造方法的一部分,两者在表现形式上却是分离的。Bird类的构造方法在类的外部,它只能对参数进行赋值。 +如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如: ```kotlin class Bird(weight: Double, age: Int, color: String) { @@ -643,11 +673,12 @@ class Bird(weight: Double, aget: Int, color: String) { 可以发现,多个init语句块有利于进一步对初始化的操作进行职能分离,这在复杂的业务开发中显得特别有用。 - - ## 数据类:使用`data class`定义 -数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中,equals()方法用于判断两个数据类是否相等。hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。所以我们在Java中创建一个数据类时要写很多代码,但是在Kotlin中你只需要一行代码。 +数据类通常需要重写equals()、hashCode()、toString()这几个方法。其中,equals()方法用于判断两个数据类是否相等。 +hashCode()方法作为equals()的配套方法,也需要一起重写,否则会导致HashMap、HashSet等hash相关的系统类无法正常工作。 +toString()方法用于提供更清晰的输入日志,否则一个数据类默认打印出来的就是一行内存地址。所以我们在Java中创建一个数据类时要写很多代码, +但是在Kotlin中你只需要一行代码。 数据类是一种非常强大的类: @@ -712,7 +743,8 @@ data class Artist( var mbid: String) ``` -数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。例如,假设你创建了两个属性值完全相同的Artist对象,使用==操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。 +数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。例如,假设你创建了两个属性值完全相同的Artist对象, +使用==操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。 通过数据类,会自动提供以下函数: @@ -745,11 +777,11 @@ val charon2 = charon.copy(age = 19) - 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何用处的。 - 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明 - data class之前不能用abstract、open、sealed或者inner进行修饰 -- 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类,更多可看[Feedback Request: Limitations on Data Classes](https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/) - +- 在Kotlin 1.1版本前数据类只允许实现接口,之后的版本既可以实现接口也可以继承类, + 更多可看[Feedback Request: Limitations on Data Classes](https://blog.jetbrains.com/kotlin/2015/09/feedback-request-limitations-on-data-classes/) - -与任何其他类一样,你可以向数据类添加属性和方法,只需要将它们包含在类主体中。但是有一个大问题,就是在编译器生成数据类的方法实现时,比如覆盖equals方法和创建copy方法,它仅包含在主构造函数中定义的属性。因此如果你在数据类主体中定义添加的属性,则它们不会被包含到任何编译器生成的方法中。 +与任何其他类一样,你可以向数据类添加属性和方法,只需要将它们包含在类主体中。但是有一个大问题,就是在编译器生成数据类的方法实现时, +比如覆盖equals方法和创建copy方法,它仅包含在主构造函数中定义的属性。因此如果你在数据类主体中定义添加的属性,则它们不会被包含到任何编译器生成的方法中。 ### 数据类定义了componentN方法 @@ -761,11 +793,10 @@ val charon2 = charon.copy(age = 19) var personD = PersonData("PersonData", 20, "male") var (name, age) = personD - Log.d("test", "name = $name, age = $age") //输出 -name = PersonData, age = 20 +// name = PersonData, age = 20 ``` 上面的多声明,大概可以翻译成这样: @@ -775,9 +806,13 @@ var name = f1.component1() var age = f1.component2() ``` 数据类的缺点,数据类虽然使用的时候很简单,但是因为它会默认帮我们自动生成很多代码,里面的有些代码其实在某些情况下我们并不需要,例如copy、component等, -如果在项目中使用了大量的数据类,那就会引起包大小增加的问题。具体可见[Data classes in Kotlin: how do they impact application size](https://medium.com/bumble-tech/data-classes-in-kotlin-the-real-impact-of-using-it-6f1fdc909837) +如果在项目中使用了大量的数据类,那就会引起包大小增加的问题。 +具体可见[Data classes in Kotlin: how do they impact application size](https://medium.com/bumble-tech/data-classes-in-kotlin-the-real-impact-of-using-it-6f1fdc909837) -虽然对于release包,使用了R8,ProGuard,DexGuard等优化器。这些可以删除未使用的方法,这意味着它们可以优化数据类。像componentN()、copy()如果没有使用的话,会默认给删除。但是对于toString()、equals()、hashCode()方法则不会删除。对于有些不需要toString()、equals()、hashCode()方法的类如果使用数据类就会导致多生成这些代码,所以在使用数据类的时候不要去为了简单而乱用,也要去想想是否需要这些方法?是否需要设计成数据类。 +虽然对于release包,使用了R8,ProGuard,DexGuard等优化器。这些可以删除未使用的方法,这意味着它们可以优化数据类。 +像componentN()、copy()如果没有使用的话,会默认给删除。但是对于toString()、equals()、hashCode()方法则不会删除。 +对于有些不需要toString()、equals()、hashCode()方法的类如果使用数据类就会导致多生成这些代码,所以在使用数据类的时候不要去为了简单而乱用, +也要去想想是否需要这些方法?是否需要设计成数据类。 ## 继承 @@ -788,7 +823,8 @@ class Person // 从 Any 隐式继承 ``` `Any`不是`java.lang.Object`。它除了`equals()`、`hashCode()`和`toString()`外没有任何成员。 -`在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是open,如果不加open,那它在转化为Java代码时就是final的: +在Java中,类默认是可以被继承的,除非你主动加final修饰符。而在Kotlin中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是open, +如果不加open,那它在转化为Java代码时就是final的: ```kotlin class Bird { @@ -798,9 +834,7 @@ class Bird { fun fly() {} } ``` - -将Bird类编译后转换为Java的代码: - +将Bird类编译后转换为Java的代码: ```java public final class Bird { private final double weight = 500.0; @@ -821,7 +855,7 @@ public final class Bird { } ``` -所以Kotlin`中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, +所以Kotlin中所有的类默认都是不可继承的(`final`),为什么要这样设计呢?引用`Effective Java`书中的第17条:要么为继承而设计,并提供文档说明, 要么就禁止继承。所以我们只能继承那些明确声明`open`或者`abstract`的类:要声明一个显式的超类型,我们把类型放到类头的冒号之后: ```kotlin @@ -830,12 +864,14 @@ open class Person(num: Int) class SuperPerson(num: Int) : Person(num) ``` -冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 - +冒号后面的Person(num)会调用Person类的构造函数,以确保所有的初始化代码(例如给属性赋值)能够被执行。 +调用父类构造函数是强制性的:如果父类有主构造函数,你必须在子类头中调用它,否则代码将无法通过编译。 +请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。 +假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 - -注意: 上面在说道继承的时候`class SuperPerson(num: Int) : Person(num)`在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。 -但是如果类没有主构造函数,如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin +注意: 上面在说到继承的时候`class SuperPerson(num: Int) : Person(num)`在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。 +Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。 +但是如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin 中是允许类中只有次构造函数,没有主构造函数的。当一个类没有显式的定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。 如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 @@ -849,19 +885,19 @@ class MyView : View { constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } ``` -也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。那么既然没有主构造函数,继承View类的时候也就不需要再在View类后加上括号了。 -其实原因就是这么简单,只是很多人在刚开始学习Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的写法有时候要加上括号,有时候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的。 +也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。那么既然没有主构造函数,继承View类 +的时候也就不需要再在View类后加上括号了。其实原因就是这么简单,只是很多人在刚开始学习Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的 +写法有时候要加上括号,有时候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的。 另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。 ### Any -我们都知道,Java并不能在真正意义上被称为一门“ 纯面向对象”语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 - -但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 - +我们都知道,Java并不能在真正意义上被称为一门"纯面向对象"语言,因为它的原始类型(如int)的值与函数等并不能被视作对象。 +但是Kotlin不同,在Kotlin的类型系统中,并不区分原始类型(基本数据类型)和包装类型,我们使用的始终是同一个类型。虽然从严格意义上,我们不能说 +Kotlin是一门纯面向对象的语言,但它显然比Java有更纯的设计。 #### Any:非空类型的跟类型 @@ -869,7 +905,8 @@ class MyView : View { ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/kotlin_any.png?raw=true) -与Java不同的是,Kotlin不区分“原始类型”(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型,则该类型将是Any的直接子类型。如: +与Java不同的是,Kotlin不区分"原始类型"(primitive type)和其他的类型,他们都是同一类型层级结构的一部分。 如果定义了一个没有指定父类型的类型, +则该类型将是Any的直接子类型。如: ```kotlin class Animal(val weight: Double) @@ -879,13 +916,10 @@ class Animal(val weight: Double) 如果说Any是所有非空类型的根类型,那么Any?才是所有类型(可空和非空类型)的根类型。这也就是说?Any?是?Any的父类型。 - - ## 覆盖 ##### 方法覆盖 - 只能重写显示标注可覆盖的方法: ```kotlin @@ -943,7 +977,7 @@ open class SuperPerson(num: Int) : Person(num) { ##### 属性覆盖 -属性覆盖与方法覆盖类似,只能覆盖显示标明`open`的属性,并且要用`override`开头: +属性覆盖与方法覆盖类似,只能覆盖显式标明`open`的属性,并且要用`override`开头: ```kotlin open class Person(num: Int) { @@ -969,13 +1003,12 @@ open class SuperPerson(num: Int) : Person(num) { } ``` -每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,如果某个属性在父类中被定义为val,你可以在子类中使用var属性覆盖它。只需要覆盖该属性并将其声明为var即可。请注意,这只适用于这一种方式。如果尝试使用val覆盖var属性,编译器将会感到沮丧并拒绝编译你的代码。 - - +每个声明的属性可以由具有初始化器的属性或者具有`get`方法的属性覆盖,如果某个属性在父类中被定义为val,你可以在子类中使用var属性覆盖它。 +只需要覆盖该属性并将其声明为var即可。请注意,这只适用于这一种方式。如果尝试使用val覆盖var属性,编译器将会感到沮丧并拒绝编译你的代码。 ## 抽象类 -类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。 需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不言而喻。 +类和其中的某些成员可以声明为`abstract`。抽象成员在本类中可以不用实现。需要注意的是,我们并不需要用`open`标注一个抽象类或者函数——因为这不言而喻。 我们可以用一个抽象成员覆盖一个非抽象的开放成员: @@ -989,8 +1022,6 @@ abstract class Derived : Base() { } ``` - - ## 注释 和`Java`差不多 @@ -1002,13 +1033,11 @@ abstract class Derived : Base() { 块注释。 */ ``` - - - - ## 接口:使用`interface`关键字 -接口可以让你在父类层次结构之外定义共同的行为,接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点,而且使用起来更加灵活。 +接口可以让你在父类层次结构之外定义共同的行为,接口用于为共同行为定义协议,使你可以不依赖严格的继承结构却又可以利用多态。与抽象类类似,接口不能被 +实例化且可以定义抽象或具体的方法和属性,但两者有一个关键的不同点:类可以实现多个接口,但是只能继承于一个直接父类。所以接口不仅拥有抽象类的优点, +而且使用起来更加灵活。 ```kotlin interface FlyingAnimal { @@ -1099,9 +1128,10 @@ Unit与Int一样,都是一种类型,然而它不代表任何信息,用面 fun add(x: Int,y: Int) : Int = x + y // 省略了{} ``` -Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 +Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体 +的情况下我们可以不声明返回值类型,这进一步简化了语法。 -我们可以给参数指定一个默认值使得它们变得可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: +我们可以给参数指定一个默认值使的它们变的可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: ```kotlin fun toast(message: String, length: Int = Toast.LENGTH_SHORT) { @@ -1133,10 +1163,6 @@ fun main() { } ``` - - - - ### 可变长参数函数:使用`vararg`关键字 ```kotlin @@ -1182,13 +1208,14 @@ val mList2 = vars(0, *myArray, 6, 7) - 方法声明 - 伴生对象 -不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。 +不要按字母顺序或者可见性对方法声明排序,也不要将常规方法与扩展方法分开。而是要把相关的东西放在一起,这样从上到下阅读类的人就能够跟进所发生事情的 +逻辑。选择一个顺序(高级别优先,或者相反)并坚持下去。 将嵌套类放在紧挨使用这些类的代码之后。如果打算在外部使用嵌套类,而且类中并没有引用这些类,那么把它们放到末尾,在伴生对象之后。 ### 接口实现布局 -在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要, 还要插入用于实现的额外的私有方法) +在实现一个接口时,实现成员的顺序应该与该接口的成员顺序相同(如果需要,还要插入用于实现的额外的私有方法) ### 重载布局 @@ -1213,7 +1240,7 @@ interface Foo : Bar { class Person(id: Int, name: String) ``` -具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。 此外,右括号应该另起一行。如果我们使用继承, +具有较长类头的类应该格式化,以使每个主构造函数参数位于带有缩进的单独一行中。此外,右括号应该另起一行。如果我们使用继承, 那么超类构造函数调用或者实现接口列表应位于与括号相同的行上: ```kotlin @@ -1239,14 +1266,8 @@ class Person( } ``` - - - - - - [下一篇: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) - --- - 邮箱 :charon.chui@gmail.com diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5eb6b49d607f8326edeb9163d5c1a73b2c412c90 GIT binary patch literal 6148 zcmeHK%}T>S5Z<-5O({YS3OxqA7K~N}@e*S70gUKDr6wfQV45vWY7V84v%Zi|;`2DO zyEy~{-bCz7+5KkcXE*af_lGgY-DP;ln8_G3pdoTp8U)Rat}Pvm$mJXnn+8QT4-yeH zO!OB``0X8b1-}`av-4|j1IiozAkG|XhKryNGl=uOPQYBjR$p*1?O*F$S{ zb_DwI=ybjA8GHK&=hu^$WR{9IMI;B#rR->|;2o4&O|RY}O+-2cdzDkg5)uQ%05L!e zY#sypEHL_;S2|Tp3=ji9Fo64m1r5>BSSplT2XuISMt>U-1$2B%AX*w7jio~HfN+%x zs8YFpVsMoXc1y=O8cT&LopHG`%%fMX9xq(34t7h1Gwvv)o){no>I}5h(8Tlq68 Date: Tue, 7 Mar 2023 21:57:29 +0800 Subject: [PATCH 009/128] format file style --- JavaKnowledge/.DS_Store | Bin 6148 -> 6148 bytes .../Base64\345\212\240\345\257\206.md" | 6 +- .../Git\347\256\200\344\273\213.md" | 104 ++-- ...37\347\220\206\345\210\206\346\236\220.md" | 95 ++- ...12\346\234\211\345\272\217\346\200\247.md" | 5 +- "JavaKnowledge/hashCode\344\270\216equals.md" | 11 +- ...14Synchronized\345\214\272\345\210\253.md" | 8 +- ...50\346\200\201\344\273\243\347\220\206.md" | 9 +- ...01\350\231\232\345\274\225\347\224\250.md" | 13 +- .../\347\256\227\346\263\225.md" | 29 +- ...13\346\261\240\347\256\200\344\273\213.md" | 11 +- ...76\350\256\241\346\250\241\345\274\217.md" | 549 ++++++++++-------- 12 files changed, 452 insertions(+), 388 deletions(-) diff --git a/JavaKnowledge/.DS_Store b/JavaKnowledge/.DS_Store index a157e1c898881dcc71838e834593d0a947bcfb97..1e695797205fc3a8c581396873b3ebfb6bf032c3 100644 GIT binary patch delta 95 zcmZoMXffEJ$;7y0vKCXJyF_)hk)ffEf{~?Bt&T#qrICS-f{C$NZ7nB8Zi?2 delta 96 zcmZoMXffEJ$;7yGvKCXJhh%lNv89oYf}x3_VXclrwWX1Pj)IA?S#2#Rhp4i?bx?eE gPHtX)Hv`,注意,可反复多次使用,添加多个文件; - 使用命令`git commit`,完成。 @@ -123,12 +123,13 @@ HEAD是当前分支引用的指针,它总是指向该分支上的最后一次 #### Git目录下文件的状态: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true)你工作目录下的每一个文件都不外乎这两种状态: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_file_lifecycle.png?raw=true) +你工作目录下的每一个文件都不外乎这两种状态: -- 已跟踪(Tracked) +- 已跟踪(Tracked) 已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有他们的记录,在工作一段时间后,它们的状态可能是未修改,已修改或 已放入暂存区。简而言之,已跟踪的文件就是Git已经知道的文件 -- 未跟踪(Untracked) +- 未跟踪(Untracked) 工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们即不存在与上次快照的记录中,也没有被放入暂存区。 ## 常用命令 @@ -136,7 +137,10 @@ HEAD是当前分支引用的指针,它总是指向该分支上的最后一次 ### git config 安装好git后我们要先配置一下。以便`git`跟踪。 -```git config --global user.name "xxx" git config --global user.email "xxx@xxx.com"``` +``` + git config --global user.name "xxx" + git config --global user.email "xxx@xxx.com" +``` 上面修改后可以使用`cat ~/.gitconfig`查看 如果指向修改仓库中的用户名时可以不加`--global`,这样可以用`cat .git/config`来查看 `git config --list`来查看所有的配置。 @@ -156,7 +160,7 @@ git init 在某一目录下执行. `git clone [git path]` -只是后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 +执行后`Git`会自动把当地仓库的`master`分支和远程仓库的`master`分支对应起来,远程仓库默认的名称是`origin`。 ### git add提交文件更改(修改和新增),把当前的修改添加到暂存区 @@ -181,21 +185,22 @@ git init 就是对已经存在的`commit`进行 再次提交; 简单用法: `git cherry-pick ` + `git rebase`命令基本是是一个自动化的`cherry-pick`命令。它计算出一系列的提交,然后再以它们在其他地方以同样的顺序一个一个的`cherry-picks`出它们。 ### `git status`查看当前仓库的状态和信息,会提示哪些内容做了改变已经当前所在的分支。 ### `git diff` -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true)`git diff`直接查看当前修改未add(暂存staged)的差别 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_diff.webp?raw=true) +`git diff`直接查看当前修改未add(暂存staged)的差别 `git diff --staged`查看已add(到暂存区)的差别`git diff HEAD -- xx.txt`查看工作区与版本库最新版的差别。 - 首先如果我们只是本地修改了一个文件,但是还没有执行`git add .`之前,该如何查看有那些修改。这种情况下直接执行`git diff`就可以了。 - 那如果我们执行了`git add .`操作,然后你再执行`git diff`这时就会发现没有任何结果,这时因为`git diff`这个命令只是检查工作区和暂存区之间的差异。 - 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 + 如果我们要查看暂存区和本地仓库之间的差异就需要加一个参数使用`--staged`参数或者`--cached`,`git diff --cached`。这样再执行就可以看到暂存区和本地仓库之间的差异。 - 现在如果我们把修改使用`git commit`从暂存区提交到本地仓库,再看一下差异。这时候再执行`git diff --cached`就会发现没有任何差异。 - 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是 - 远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 + 如果我们行查看本地仓库和远程仓库的差异,就要换另一个参数,执行`git diff master origin/master`这样就可以看到差异了。 这里面`master`是本地的仓库,而`origin/master`是远程仓库,因为默认都是在主分支上工作,所以两边都是`master`而`origin`代表远程。 ### `git push` 提交到远程仓库 @@ -205,8 +210,7 @@ git init ### `git log`查看当前分支下的提交记录 用`git log`可以查看提交历史,以便确定要回退到哪个版本。 -如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来, -用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 +如果已经使用`git log`查出版本`commit id`后`reset`到某一次提交后,又要重返回来,用`git reflog`查看命令历史,以便确定要回到未来的哪个版本。 ``` git log -p -2 // -p 是仅显示最近的x次提交 @@ -314,8 +318,7 @@ reset 要做的的第三件事情就是让工作目录看起来像索引。 如 必须注意,--hard 标记是 reset 命令唯一的危险用法,它也是 Git 会真正地销毁数据的仅有的几个操作之一。 其他任何形式的 reset 调用都可以轻松撤消,但是 --hard 选项不能,因为它强制覆盖了工作目录中的文件。 在这种特殊情况下,我们的 Git 数据库中的一个提交内还留有该文件的 v3 版本, 我们可以通过 reflog 来找回它。但是若该文件还未提交,Git 仍会覆盖它从而导致无法恢复。 -回顾 -reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止: +回顾reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止: - 移动 HEAD 分支的指向 (若指定了 --soft,则到此停止) - 使索引看起来像 HEAD (若未指定 --hard,则到此停止) @@ -362,30 +365,28 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/git_reset_checkout.png?raw=true) - 已经修改,但是并未执行`git add .`进行暂存 - 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是再工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行 - 撤销操作。 + 如果只是修改了本地文件,但是还没有执行`git add .`这时候我们的修改还是在工作区,并未进入暂存区,我们可以使用:`git checkouot .`或者`git reset --hard`来进行撤销操作。 - `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 + `git add .`的反义词是`git checkout .`做完修改后,如果想要向前一步,让修改进入暂存区执行`git add .`如果想退后一步,撤销修改就执行`git checkout .`。 - 已暂存,未提交 - 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: + 如果已经执行了`git add .`但是还没有执行`git commit -m "comment"`这时候你意识到了错误,想要撤销,可以执行: ``` git reset // git reset 只是把修改退回到了git add .之前的状态,也就是让文件还处于已修改未暂存的状态 git checkout . // 上面让文件处于已修改未暂存的状态,还要执行git checkout .来撤销工作区的状态 ``` - 或`git reset --hard` + 或`git reset --hard` - 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 + 上面两个例子中都使用了`git reset --hard`这个命令也可以完成,这个命令可以一步到位的把你的修改完全恢复到本地仓库的未修改的状态。 - 已提交,未推送 - 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? - 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 - 你提交的脏代码,那么就从远程仓库中把代码恢复把。 + 如果执行了`git add .`又执行了`git commit -m "comment"`提交了代码,这时候代码已经进入到了本地仓库,然而你发现问题了,想要撤销,怎么办? + 执行`git reset --hard origin/master`还是`git reset --hard`命令,只不过这次多了一个参数`origin/master`,这代表远程仓库,既然本地仓库已经有了 + 你提交的脏代码,那么就从远程仓库中把代码恢复把。 - 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? - `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 + 但是上面这样会导致你之前修改的代码都没有了,如果我只是想撤回提交,还想要我之前修改的东西重新回到本地仓库呢? + `git reset --soft HEAD^`,这样就成功的撤销了你的commit。注意,仅仅是撤回commit操作,您写的代码仍然保留。 - 已推送到远程仓库 - 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的 - 代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 + 如果你执行`git add .`后又`commit`又执行了`git push`操作了,这时候你的代码已经进入到了远程仓库中,如果你发现你提交的代码又问题想恢复的话,那你只能先把本地仓库的代码恢复,然后再强制执行`git push`仓做,`push`到远程仓库就可以了。 ``` git reset --hard HEAD^ // HEAD^代表最新提交的前一次 @@ -396,11 +397,10 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 你大概还想知道 checkout 和 reset 之间的区别。 和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。 -不带路径 -运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。 +不带路径运行 git checkout [branch] 与运行 git reset --hard [branch] 非常相似,它会更新所有三棵树使其看起来像 [branch],不过有两点重要的区别。 首先不同于 reset --hard,checkout 对工作目录是安全的,它会通过检查来确保不会将已更改的文件弄丢。 其实它还更聪明一些。它会在工作目录中先试着 -简单合并一下,这样所有 还未修改过的 文件都会被更新。 而 reset --hard 则会不做检查就全面地替换所有东西。 +简单合并一下,这样所有还未修改过的 文件都会被更新。而 reset --hard 则会不做检查就全面地替换所有东西。 第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。 @@ -408,8 +408,7 @@ reset 命令会以特定的顺序重写这三棵树,在你指定以下选项 如果我们运行 git reset master,那么 develop 自身现在会和 master 指向同一个提交。 而如果我们运行 git checkout master 的话, develop 不会移动,HEAD 自身会移动。 现在 HEAD 将会指向 master。 -所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。 reset 会移动 HEAD 分支的指向,而checkout则移动 -HEAD自身。 +所以,虽然在这两种情况下我们都移动 HEAD 使其指向了提交 A,但做法是非常不同的。 reset 会移动 HEAD 分支的指向,而checkout则移动HEAD自身。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reset-checkout.png?raw=true) #### 带路径 @@ -425,8 +424,7 @@ HEAD自身。 - `git revert`是生成一个新的提交来撤销某次提交,此次提交之前的`commit`都会被保留 - `git reset`是回到某次提交,提交及之前的`commit`都会被保留,但是此次之后的修改都会被退回到暂存区 -相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除制定的`commit` -并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 +相比`git reset`它不会改变现在得提交历史。`git reset`是直接删除指定的`commit`并把`HEAD`向后移动了一下。而`git revert`是一次新的特殊的`commit`,`HEAD`继续前进,本质和普通`add commit`一样,仅仅是`commit`内容很特殊。内容是与前面普通`commit`变化的反操作。 比如前面普通`commit`是增加一行`a`,那么`revert`内容就是删除一行`a`。 在 Git 开发中通常会控制主干分支的质量,但有时还是会把错误的代码合入到远程主干。虽然可以直接回滚远程分支,但有时新的代码也已经合入, 直接回滚后最近的提交都要重新操作。 那么有没有只移除某些Commit的方式呢?可以用一次revert操作来完成。 @@ -469,8 +467,7 @@ git commit -a -m 'This reverts commit 7e345c9 and 551c408' ``` 现在的 HEAD(8fef80a)就是我们想要的版本,把它 Push 到远程即可。 -git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中, -本质上就是撤销或者倒转。 +git revert 命令本质上就是一个逆向的 git cherry-pick 操作。 它将你提交中的变更的以完全相反的方式的应用到一个新创建的提交中,本质上就是撤销或者倒转。 ### `git rm`删除文件 @@ -502,8 +499,7 @@ origin/master_sg origin/offline ``` `git branch -d devBranch`删除`devBranch`分支。 -当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有 -被合并,删除失败。 +当时如果在新建了一个分支后进行修改但是还没有合并到其他分支的时候就去使用`git branch -d xxx`删除的时候系统会手提示说这个分支没有被合并,删除失败。 这时如果你要强行删除的话可以使用命令`git branch -D xxx`. 如何删除远程分支呢? @@ -546,13 +542,13 @@ git push origin frommaster// 推送到远程仓库所要使用的名字 `git show tagName`来查看某`tag`的详细信息。 - 打完`tag`后怎么推送到远程仓库 - `git push origin tagName` + `git push origin tagName` - 删除`tag` - `git tag -d tagName` + `git tag -d tagName` - 删除完`tag`后怎么推送到远程仓库,这个写法有点复杂 - `git push origin:refs/tags/tagName` + `git push origin:refs/tags/tagName` - 忽略文件 - 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 + 在`git`根目录下创建一个特殊的`.gitignore`文件,把想要忽略的文件名填进去就可以了,匹配模式最后跟斜杠(/)说明要忽略的是目录,#是注释 。 ### amend修改最后一次提交 @@ -566,10 +562,7 @@ $ git commit --amend --no-edit #### 修改多个提交信息 -为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交, -基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或 -做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变 -基到的提交来做到。 +为了修改在提交历史中较远的提交,必须使用更复杂的工具。 Git 没有一个改变历史工具,但是可以使用变基工具来变基一系列提交,基于它们原来的 HEAD 而不是将其移动到另一个新的上面。 通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。 可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。 例如,如果想要修改最近三次提交信息,或者那组提交中的任意一个提交信息, 将想要修改的最近一次提交的父提交作为参数传递给 git rebase -i 命令,即 HEAD~2^ 或 HEAD~3。 记住 ~3 可能比较容易,因为你正尝试修改最后三次提交;但是注意实际上指定了以前的四次提交,即想要修改提交的父提交: @@ -632,7 +625,7 @@ git log master..experiment ``` #### 三点 -这个语法可以选择出被两个引用 之一 包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行: +这个语法可以选择出被两个引用之一包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 master 或者 experiment 中包含的但不是两者共有的提交,你可以执行: ```shell git log master...experiment @@ -654,8 +647,7 @@ git log master...experiment git checkout experiment git rebase master ``` -它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖 -先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 +它的原理是首先找到这两个分支(即当前分支experiment、变基操作的目标基底分支master)的最近共同祖先C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底C3,最后以此将之前另存为临时文件的修改依序引用。 ![将C4中的修改变基到C3上](https://raw.githubusercontent.com/CharonChui/Pictures/master/basic-rebase-3.png?raw=true) 现在回到master分支,进行一次快进合并。 @@ -691,8 +683,7 @@ git checkout master git merge client ``` ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-3.png?raw=true) -接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支 -(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 +接下来你决定将server分支中的修改也整合进来,使用git rebase 命令可以直接将主题分支(即这里的server)变基到基分支(即这里的master)上。这样做能省去你先切换到server分支,再对其进行变基命令的多个步骤。 `git rebase master server` 如下图,将server中的修改变基到master上所示,server中的代码被“续”到了master后面。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/interesting-rebase-4.png?raw=true) @@ -728,7 +719,7 @@ git merge server ##### 用变基解决变基 -如果你 真的 遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 +如果你真的遭遇了类似的处境,Git 还有一些高级魔法可以帮到你。 如果团队中的某人强制推送并覆盖了一些你所基于的提交,你需要做的就是检查你做了哪些修改,以及他们覆盖了哪些修改。 实际上,Git 除了对整个提交计算 SHA-1 校验和以外,也对本次提交所引入的修改计算了校验和——即 “patch-id”。 @@ -747,7 +738,7 @@ git merge server 在本例中另一种简单的方法是使用 git pull --rebase 命令而不是直接 git pull。 又或者你可以自己手动完成这个过程,先 git fetch,再 git rebase teamone/master。 -如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交, 那你就有大麻烦了,你的同事也会因此鄙视你。 +如果你只对不会离开你电脑的提交执行变基,那就不会有事。 如果你对已经推送过的提交执行变基,但别人没有基于它的提交,那么也不会有事。 如果你对已经推送至共用仓库的提交上执行变基命令,并因此丢失了一些别人的开发所基于的提交,那你就有大麻烦了,你的同事也会因此鄙视你。 如果你或你的同事在某些情形下决意要这么做,请一定要通知每个人执行 git pull --rebase 命令,这样尽管不能避免伤痛,但能有所缓解。 @@ -766,8 +757,7 @@ git merge server 现在,让我们回到之前的问题上来,到底合并还是变基好?希望你能明白,这并没有一个简单的答案。 Git 是一个非常强大的工具,它允许你对 提交历史做许多事情,但每个团队、每个项目对此的需求并不相同。 既然你已经分别学习了两者的用法,相信你能够根据实际情况作出明智的选择。 -总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两 -种方式带来的便利。 +总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史, 从不对已推送至别处的提交执行变基操作,这样,你才能享受到两种方式带来的便利。 ### git fetch与git pull的区别 diff --git "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" index f610438e..a5192325 100644 --- "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -1,35 +1,66 @@ HashMap实现原理分析 -=== - -HashMap基于Map接口实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null,另外HashMap不能保证放入元素的顺序,它是无序的,和放入的顺序并不能相同。 - +=== +HashMap是Map接口的实现,元素以键值对的方式存储,并且允许使用null建和null值,因为key不允许重复,因此只能有一个键为null。 +HashMap被认为是Hashtable的增强版,HashMap是一个非线程安全的容器,如果想构造线程安全的Map考虑使用ConcurrentHashMap。 +HashMap是无序的,因为HashMap无法保证内部存储的键值对的有序性。 + +### 重要属性 + +- 初始容量 + HashMap的默认初始容量是由DEFAULT_INITIAL_CAPACITY属性管理的。 + `static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + HashMap的默认初始容量是 1 << 4 = 16, << 是一个左移操作,它相当于是: + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_1.webp) + +- 最大容量 + HashMap的最大容量是 + `static final int MAXIMUM_CAPACITY = 1 << 30;` + 这里是不是有个疑问?int 占用四个字节,按说最大容量应该是左移 31 位,为什么 HashMap 最大容量是左移 30 位呢?因为在数值计算中,最高位也就是最左位的位 是代表着符号为,0 -> 正数,1 -> 负数,容量不可能是负数,所以 HashMap 最高位只能移位到 2 ^ 30 次幂。 +- 默认负载因子 + HashMap的默认负载因子是 + `static final float DEFAULT_LOAD_FACTOR = 0.75f;` + float 类型所以用 .f 为单位,负载因子是和扩容机制有关,这里大致提一下,后面会细说。扩容机制的原则是当 HashMap 中存储的数量 > HashMap 容量 * 负载因子时,就会把 HashMap 的容量扩大为原来的二倍。 + HashMap 的第一次扩容就在 DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR = 12 时进行。 +- 树化阈值 + HashMap 的树化阈值是 + `static final int TREEIFY_THRESHOLD = 8;` + + 在进行添加元素时,当一个桶中存储元素的数量 > 8 时,会自动转换为红黑树(JDK1.8 特性)。 +- 链表阈值 + HashMap 的链表阈值是 + `static final int UNTREEIFY_THRESHOLD = 6;` 在进行删除元素时,如果一个桶中存储元素数量 < 6 后,会自动转换为链表 - 扩容临界值 `static final int MIN_TREEIFY_CAPACITY = 64;` + 这个值表示的是当桶数组容量小于该值时,优先进行扩容,而不是树化 ## 原理 其底层数据结构是数组称之为哈希桶,每个桶(bucket)里面放的是链表,链表中的每个节点,就是哈希表中的每个元素。 -通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash -从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过Load Facotr则resize为原来的2倍)。 -获取对象时,我们将K传给get()方法,它调用hashCodeO()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 -如果发生碰撞的时候,HashMap通过链表将产生碰撞冲突的元素组织起来,在JDK8中,如果一个bucket中碰撞冲突的元素超过8个, -则使用红黑树来替换链表,从而提高速度。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_hash.webp) +哈希表中哈希函数的设计是相当重要的,这也是建哈希表过程中的关键问题之一。 +建立一个哈希表之前需要解决两个主要问题: + +- 构造一个合适的哈希函数,均匀性 H(key)的值均匀分布在哈希表中 +- 冲突的处理 + +冲突:在哈希表中,不同的关键字值对应到同一个存储位置的现象。 + +当一个值中要存储到HashMap中的时候会根据Key的值来计算出他的hash,通过hash值来确认存放到数组中的位置,如果发生hash冲突就以链表的形式存储,当链表过长的话,HashMap会把这个链表转换成红黑树来存储通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K / V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量 (超过Load Factor则resize为原来的2倍)。 +获取对象时,我们将K传给get()方法,它调用hashCode()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 -因其底层哈希桶的数据结构是数组,所以也会涉及到扩容的问题。当HashMap的容量达到threshold域值时,就会触发扩容。 扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。 而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。 -因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取的很好, -每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余, -以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算, +因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取的很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算, 发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 -扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算, -以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶, -然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大, -可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值, -依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标, -即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量》=8,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, +扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量 >=8,且只有数组长度大于64才处理,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, 顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 +数组的特点:查询效率高,插入删除效率低。 +链表的特点:查询效率低,插入删除效率高。 + +在HashMap底层使用数组加(链表或红黑树)的结构完美的解决了数组和链表的问题,使得查询和插入,删除的效率都很高。 + + ## JDK1.7 HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。HashMap主要是用数组来存储数据的,我们都知道它会对key进行哈希运算, @@ -147,7 +178,7 @@ public V get(Object key) { ## JDK1.8 在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方, -数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。 +数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,且只有数组长度大于64才处理,将链表转换为红黑树。 利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。HashMap中, 如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1), 如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中, @@ -505,6 +536,9 @@ resize()方法用于初始化数组或数组扩容,每次扩容后容量为原 是0的话索引没变,是1的话索引变成 “原索引 + oldCap”。可以看看下图为16扩充为32的resize示意图: ![resize](https://raw.githubusercontent.com/CharonChui/Pictures/master/resize3.bmp) +这里有一个需要注意的点就是在JDK1.8 HashMap扩容阶段重新映射元素时不需要像1.7版本那样重新去一个个计算元素的hash值,而是通过hash & oldCap的值来判断,若为0则索引位置不变,不为0则新索引=原索引+旧数组长度,为什么呢?具体原因如下: + +因为我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。因此,我们在扩充HashMap的时候,不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap 这个设计确实非常的巧妙,既省去了重新计算hash值的时间,而且同时,**由于新增的1bit是0还是1可以认为是随机的,因此resize的过程, 均匀的把之前的冲突的节点分散到新的bucket了**。 @@ -690,7 +724,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, * 扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。 * 因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量 * 利用哈希值 与运算 旧的容量 ,if ((e.hash & oldCap) == 0),可以得到哈希值去模后,是大于等于oldCap还是小于oldCap,等于0代表小于oldCap,应该存放在低位,否则存放在高位。这里又是一个利用位运算 代替常规运算的高效点 -* 如果追加节点后,链表数量》=8,则转化为红黑树 +* 如果追加节点后,链表数量 >= 8,则转化为红黑树 * 插入节点操作时,有一些空实现的函数,用作LinkedHashMap重写使用。 @@ -726,18 +760,22 @@ final Node getNode(int hash, Object key) { ``` - - - ## JDK 7与JDK 8中关于HashMap的对比 -1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,便会树化。 +1. JDK8为红黑树 + 链表 + 数组的形式,当桶内元素大于8时,且只有数组长度大于64才处理,便会树化。 2. hash值的计算方式不同 (jdk 8 简化)。 3. JDK7中table在创建hashmap时分配空间,而8中在put的时候分配。 4. 链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;因为头插法会使链表发生反转,多线程环境下会产生环; 5. 在resize操作中,7 需要重新进行index的计算,而8不需要,通过判断相应的位是0还是1,要么依旧是原index,要么是oldCap + 原 index。 6. 在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容; +把链表转换成红黑树,树化需要满足以下两个条件: + +- 链表长度大于等于8 +- table数组长度大于等于64 +为什么table数组容量大于等于64才树化? + +因为当table数组容量比较小时,键值对节点 hash 的碰撞率可能会比较高,进而导致链表长度较长。这个时候应该优先扩容,而不是立马树化。 ## 问题 @@ -782,6 +820,11 @@ final Node getNode(int hash, Object key) { 扩容后为32后的二进制就高位多了1,============>为0001 1111。因为是&运算,1和任何数&都是它本身,那就分二种情况, 原数据hashcode高位第4位为0和高位为1的情况;第四位高位为0,重新hash数值不变,第四位为1,重新hash数值比原来大16(旧数组的容量)。 +6. HashMap和HashSet的区别: + +HashSet继承于AbstractSet接口,实现了Set、Cloneable、java.io.Serializable接口。HashSet不允许集合中出现重复的值。HashSet底层其实 +就是HashMap,所有对HashSet的操作其实就是对HashMap的操作。所以HashSet也不保证集合的顺序。 + 参考: @@ -794,5 +837,3 @@ final Node getNode(int hash, Object key) { --- - 邮箱 :charon.chui@gmail.com - Good Luck! - - diff --git "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" index 14d1a46c..f90b23af 100644 --- "a/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" +++ "b/JavaKnowledge/Java\345\271\266\345\217\221\347\274\226\347\250\213\344\271\213\345\216\237\345\255\220\346\200\247\343\200\201\345\217\257\350\247\201\346\200\247\344\273\245\345\217\212\346\234\211\345\272\217\346\200\247.md" @@ -1,13 +1,10 @@ # Java并发编程之原子性、可见性以及有序性 - - - 缓存导致的可见性问题 - 线程切换带来的原子性问题 - 编译优化带来的有序性问题 - ## 原子性(Atomicity) 众所周知,原子是构成物质的基本单位,所以原子代表着不可分。 @@ -31,7 +28,7 @@ 可见性指的是当一个线程修改了共享变量后,其他线程能够立即得知这个修改。 在多核处理器中,如果多个线程对一个变量进行操作,但是这多个线程有可能被分配到多个处理器中运行,那么编译器会对代码进行优化, -当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的片上存储器中,等到进行完操作后,再赋值回主存。 +当线程要处理该变量时,多个处理器会将变量从主内存复制一份分别存储在自己的存储器中,等到进行完操作后,再赋值回主存。 (这样做的好处是提高了运行的速度,因为在处理过程中多个处理器减少了同主内存通信的次数); 同样在单核处理器中这样由于备份造成的问题同样存在!这样的优化带来的问题之一是变量可见性——如果线程`t1`与线程`t2`分别被安排在了不同 的处理器上面,那么`t1`与`t2`对于变量`A`的修改时相互不可见,如果`t1`给`A`赋值,然后`t2`又赋新值,那么`t2`的操作就将`t1`的操作 diff --git "a/JavaKnowledge/hashCode\344\270\216equals.md" "b/JavaKnowledge/hashCode\344\270\216equals.md" index 75bf9067..c73130a0 100644 --- "a/JavaKnowledge/hashCode\344\270\216equals.md" +++ "b/JavaKnowledge/hashCode\344\270\216equals.md" @@ -1,11 +1,12 @@ hashCode与equals === -`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时环境是如何判断`HashSet` -中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? -在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范:在`Java`中任何一个对象都具备`equals(Object obj)` -和`hashcode()`这两个方法,因为他们是在`Object`类中定义的。`equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 -`hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 +`HashSet`和`HashMap`一直都是`JDK`中最常用的两个类,`HashSet`要求不能存储相同的对象,`HashMap`要求不能存储相同的键。 那么`Java`运行时 +环境是如何判断`HashSet`中相同对象、`HashMap`中相同键的呢?当存储了相同的东西之后`Java`运行时环境又将如何来维护呢? +在研究这个问题之前,首先说明一下`JDK`对`equals(Object obj)`和`hashcode()`这两个方法的定义和规范: +在`Java`中任何一个对象都具备`equals(Object obj)`和`hashcode()`这两个方法,因为他们是在`Object`类中定义的: +- `equals(Object obj)`方法用来判断两个对象是否“相同”,如果“相同”则返回`true`,否则返回`false`。 +- `hashcode()`方法返回一个`int`数,在`Object`类中的默认实现是“将该对象的内部地址转换成一个整数返回”。 接下来有两个个关于这两个方法的重要规范: - 若重写`equals(Object obj)`方法,有必要重写`hashcode()`方法,确保通过`equals(Object obj)`方法判断结果为`true`的两个对象具备相等的`hashcode()`返回值。 diff --git "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" index c4ed28a2..82801018 100644 --- "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -26,7 +26,7 @@ > > 多核CPU就像一家公司是由多个合伙人共同创办的,那么,就需要给每个合伙人都设立一套供自己直接领导的高层管理人员,多个合伙人共享使用的是公司的底层员工。 > -> 还有的公司,不断壮大,开始差分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 +> 还有的公司,不断壮大,开始拆分出各个子公司。各个子公司就是多个CPU了,互相使用没有共用的资源。互不影响。 随着计算机能力不断提升,开始支持多线程。那么问题就来了。我们分别来分析下单线程、多线程在单核CPU、多核CPU中的影响。 @@ -34,7 +34,7 @@ **单核CPU,多线程**:进程中的多个线程会同时访问进程中的共享数据,CPU将某块内存加载到缓存后,不同线程在访问相同的物理地址的时候,都会映射到相同的缓存位置,这样即使发生线程的切换,缓存仍然不会失效。但由于任何时刻只能有一个线程在执行,因此不会出现缓存访问冲突。 -**多核CPU,多线程**:每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的caehe中保留一份共享内存的缓冲。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 +**多核CPU,多线程**:每个核都至少有一个L1缓存。多个线程访问进程中的某个共享内存,且这多个线程分别在不同的核心上执行,则每个核心都会在各自的cache中保留一份共享内存的缓存。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的cache之间的数据就有可能不同。 在CPU和主存之间增加缓存,在多线程场景下就可能存在**缓存一致性问题**,也就是说,在多核CPU中,每个核的自己的缓存中,关于同一个数据的缓存内容可能不一致。 @@ -170,14 +170,14 @@ public class DoubleCheckSingleton { ```java memory = allocate(); //1.分配对象的内存空间 ctorInstance(memory); //2.初始化对象 -instance = memory; //3.设置instance指向刚分配的内存地址 +instance = memory; //3.设置instance指向刚分配的内存地址 ``` 上面3行伪代码中的2和3之间可能会发生重排序,排序后的执行顺序如下: ```java memory = allocate(); //1.分配对象的内存空间 -instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false +instance = memory; //3.设置instance指向刚分配的内存地址,此时对象还没有初始化,但instance == null 判断为false ctorInstance(memory); //2.初始化对象 ``` diff --git "a/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" index e8c1277f..a010cb92 100644 --- "a/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" +++ "b/JavaKnowledge/\345\212\250\346\200\201\344\273\243\347\220\206.md" @@ -3,16 +3,11 @@ 有关代理模式已经动态代理和静态代理的区别请查看另一篇文章[设计模式](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) -刚毕业的时候在学习`android`时看到过[张孝祥老师的Java高新技术](http://yun.itheima.com/course/5.html),里面 -讲到了动态代理,当时看完后感觉懂了.但是现在全部都忘了。 -因为动态代理我们平时用的其实并不多,但是作为`Android`开发,你肯定知道`Retrofit`,而`Retrofit`就是基于动态代理实现。 - - +动态代理我们平时用的其实并不多,但是作为`Android`开发,你肯定知道`Retrofit`,而`Retrofit`就是基于动态代理实现。 动态代理的类和接口 --- - - `Proxy`:动态代理机制的主类,提供一组静态方法为一组接口动态的生成对象和代理类。 ```java @@ -32,7 +27,7 @@ public static Object newProxyInstance(ClassLoader loader, ``` -- `InvocationHandler`:调用处理器接口,自定义invokle方法,用于实现对于真正委托类的代理访问。 +- `InvocationHandler`:调用处理器接口,自定义invoke方法,用于实现对于真正委托类的代理访问。 ```java /** 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 769efa0b..48d014de 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" @@ -17,21 +17,19 @@ - 强引用(Strong Reference) - 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 + 你懂的,不要胡乱持有着不放,不然内存泄露、oom有你好看,就像是老板(OOM)的亲儿子一样,在公司可以什么事都不干,但是千万不要老是占用公司的资源为他自己做事,记得用完公司的妹子之后,要让她们去工作(资源要懂得释放) 不然公司很可能会垮掉的。 平时我们编程的时候例如:`Object object=new Object()`;那`object`就是一个强引用了。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当JVM的内存空间不足时,宁愿抛出OutOfMemoryError使得程序异常终止也不愿意回收具有强引用的存活着的对象!记住是存活着,不可能是你new一个对象就永远不会被GC回收。 - 软引用(SoftReference) 描述一些还有用,但并非必需的对象。如果一个对象只具有软引用,那就类似于可有可物的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存,但是system.gc对其无效,有点像老板(OOM)的亲戚,在公司表现不好有可能会被开除,即使你投诉他(调用GC)上班看片,但是只要不被老板看到(被JVM检测到)就不会被开除(被虚拟机回收)。**软引用可用来实现内存敏感的高速缓存**。 软引用可以和一个引用队列`(ReferenceQueue)`联合使用,如果软引用所引用的对象被垃圾回收,`Java`虚拟机就会把这个软引用加入到与之关联的引用队列中。 - - - 弱引用(WeakReference) 同软引用,也用来描述非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。所以弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的声明周期。在对象没有其他引用的情况下,调用system.gc对象可被虚拟机回收,就是一个普通的员工,平常如果表现不佳会被开除(对象没有其他引用的情况下),遇到别人投诉(调用GC)上班看片,那开除是肯定了(被虚拟机回收)。 - 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 + 在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了***只具***有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。弱引用最常见的用途是实现规范映射(canonicalizing mappings,比如哈希表)。 常见的一个例子就是WeakHashMap,在HashMap中,键被置为null,唤醒gc后,不会垃圾回收键为null的键值对。但是在WeakHashMap中,键被置为null,唤醒gc后,键为null的键值对会被回收。 @@ -65,16 +63,9 @@ 一个对象是否有虚引用的存在完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一的用处:能在对象被GC时收到系统通知,主要用于跟踪对象何时被回收,比如防止资源泄漏等。 虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。 虚引用是每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null。 - - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_compare.jpg) - - - - - --- - 邮箱 :charon.chui@gmail.com diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" index b57a2d11..c6ce6686 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -147,7 +147,7 @@ for(i = 0; i < n; i ++) { ### 算法空间复杂度 -我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。还有另一个办法就是,事先建立一个有2 050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。 +我们在写代码时,完全可以用空间来换取时间,比如说,要判断某某年是不是闰年,你可能会花一点心思写了一个算法,而且由于是一个算法,也就意味着,每次给一个年份,都是要通过计算得到是否是闰年的结果。还有另一个办法就是,事先建立一个有2050个元素的数组(年数略比现实多一点),然后把所有的年份按下标的数字对应,如果是闰年,此数组项的值就是1,如果不是值为0。这样,所谓的判断某一年是否是闰年,就变成了查找这个数组的某一项的值是多少的问题。此时,我们的运算是最小化了,但是硬盘上或者内存中需要存储这2050个0和1。这是通过一笔空间上的开销来换取计算时间的小技巧。到底哪一个好,其实要看你用在什么地方。 算法的空间复杂度通过计算算法所需的存储空间实现,算法空间复杂度的计算公式记作:S(n)=O(f(n)),其中,n为问题的规模,f(n)为语句关于n所占存储空间的函数。 @@ -156,7 +156,7 @@ for(i = 0; i < n; i ++) { 算法的空间复杂度并不是计算实际占用的空间,而是计算整个算法的辅助空间单元的个数,与问题的规模没有关系。算法的空间复杂度`S(n)`定义为该算法所耗费空间的数量级。 `S(n)=O(f(n))`若算法执行时所需要的辅助空间相对于输入数据量`n`而言是一个常数,则称这个算法的辅助空间为`O(1)`; -递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间, 如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. +递归算法的空间复杂度:递归深度`N*`每次递归所要的辅助空间,如果每次递归所需的辅助空间是常数,则递归的空间复杂度是`O(N)`. 空间复杂度的分析方法: @@ -197,11 +197,6 @@ for(i = 0; i < n; i ++) { 顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。 - - - - - 算法(Algorithm)是一系列解决问题的清晰指令,也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出。如果一个算法有缺陷,或不适合于某个问题, 执行这个算法将不会解决这个问题。不同的算法可能用不同的时间、空间或效率来完成同样的任务。一个算法的优劣可以用空间复杂度与时间复杂度来衡量。 算法可以理解为有基本运算及规定的运算顺序所构成的完整的解题步骤。或者看成按照要求设计好的有限的确切的计算序列,并且这样的步骤和序列可以解决一类问题。 @@ -293,26 +288,6 @@ public static int[] merge(int[] a, int[] b) { ```` - - - - - - - - - - - - - - - - - - - - --- - 邮箱 :charon.chui@gmail.com - Good Luck! diff --git "a/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" index 27fff875..a638c653 100644 --- "a/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" +++ "b/JavaKnowledge/\347\272\277\347\250\213\346\261\240\347\256\200\344\273\213.md" @@ -22,7 +22,7 @@ - 使用线程池的好处: - 降低资源消耗。通过重复利用减少在创建和销毁线程上所花的时间以及系统资源的开销 - 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源, - 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 + 还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。如不使用线程池,有可能造成系统创建大量线程而导致消耗完系统内存以及”过度切换”。 - 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。 - 工作流程 @@ -135,7 +135,7 @@ public class ThreadPoolExecutor extends AbstractExecutorService { - `Executors.newFixedThreadPool(int);` //创建固定容量大小的缓冲池,固定数量的线程池,没提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成才继续执行 - `Executors.newScheduleThreadExecutor();`// 大小无限制的线程池,能按时间计划来执行任务,允许用户设定计划执行任务的时间。在实际的业务场景中可以使用该线程池定期的同步数据。 -这几个个方法的具体实现: +这几个方法的具体实现: ```java public static ExecutorService newFixedThreadPool(int nThreads) { @@ -161,8 +161,6 @@ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ``` - - 从它们的具体实现来看,它们实际上也是调用了`ThreadPoolExecutor`,只不过参数都已配置好了: - `newFixedThreadPool`创建的线程池`corePoolSize`和`maximumPoolSize`值是相等的,它使用的`LinkedBlockingQueue`; @@ -172,12 +170,9 @@ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 实际中,如果`Executors`提供的三个静态方法能满足要求,就尽量使用它提供的三个方法,因为自己去手动配置`ThreadPoolExecutor`的参数有点麻烦,要根据实际任务的类型和数量来进行配置。 - - ## 线程池执行状态 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_poll_process.png) - 整个过程可以拆分成以下几个部分: - 提交任务 @@ -251,8 +246,6 @@ public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 两个方法都可以向线程池提交任务,`execute()`方法的返回类型是`void`,它定义在`Executor`接口中, 而`submit()`方法可以返回持有计算结果的`Future`对象,它定义在`ExecutorService`接口中,它扩展了`Executor`接口,其它线程池类像`ThreadPoolExecutor`和`ScheduledThreadPoolExecutor`都有这些方法。 - - 线程池的关闭 --- diff --git "a/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" index a499894c..c0551d48 100644 --- "a/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/JavaKnowledge/\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -1,9 +1,11 @@ 设计模式(Design Patterns) === -设计模式`(Design pattern)`是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 +设计模式`(Design pattern)`是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他 +人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。 -项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。 +项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及 +该问题的核心解决方案,这也是它能被广泛应用的原因。 设计模式的分类 @@ -22,41 +24,64 @@ - 开闭原则`(Open Close Principle)` 开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是: 为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。 - 当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定了系统可复用性、可扩展性的基础,也增加了系统的稳定性。 + 当系统升级时,如果为了增强系统功能而需要进行大量的代码修改,则说明这个系统的设计是失败的,是违反开闭原则的。反之,对系统的扩展应该只需添加 + 新的软件模块,系统模式一旦确立就不再修改现有代码,这才是符合开闭原则的优雅设计。其实开闭原则在各种设计模式中都有体现,对抽象的大量运用奠定 + 了系统可复用性、可扩展性的基础,也增加了系统的稳定性。 - 里氏代换原则`(Liskov Substitution Principle)` - 里氏代换原则面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 `LSP`是继承复用的基石,只有当衍生类可以替换掉基类, - 软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 - 里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。 + 里氏代换原则面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 `LSP`是继承复用的基石,只有当衍生类 + 可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。 + 里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实 + 现抽象化的具体步骤的规范。 - 依赖倒转原则`(Dependence Inversion Principle)` - 我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。依赖倒置原则(Dependency InversionPrinciple)指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在“里氏替换原则”中所提到的“面向接口编程”,以达到替换底层实现的目的。 - 举个例子,公司总经理制订了下一年度的目标与计划,为了提高办公效率,总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?我想世界上没有以这种方式管理公司的领导吧。公司高层一定会发动IT部门的上层抽象去执行,调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们,这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。 + 我们知道,面向对象中的依赖是类与类之间的一种关系,如H(高层)类要调用L(底层)类的方法,我们就说H类依赖L类。 + 依赖倒置原则指高层模块不依赖底层模块,也就是说高层模块只依赖上层抽象,而不直接依赖具体的底层实现,从而达到降低耦合的目的。如上面提到的H与L + 的依赖关系必然会导致它们的强耦合,也许L任何细枝末节的变动都可能影响H,这是一种非常死板的设计。而依赖倒置的做法则是反其道而行,我们可以创建 + L的上层抽象A,然后H即可通过抽象A间接地访问L,那么高层H不再依赖底层L,而只依赖上层抽象A。这样一来系统会变得更加松散,这也印证了我们在 + “里氏替换原则”中所提到的“面向接口编程”,以达到替换底层实现的目的。 + 举个例子,公司总经理制订了下一年度的目标与计划,为了提高办公效率,总经理决定年底要上线一套全新的办公自动化软件。那么总经理作为发起方该如何 + 实施这个计划呢?直接发动基层程序员并调用他们的研发方法吗?我想世界上没有以这种方式管理公司的领导吧。公司高层一定会发动IT部门的上层抽象去 + 执行,调用IT部门经理的work方法并传入目标即可,至于这个work方法的具体实现者也许是架构师甲,也可能是程序员乙,总经理也许根本不认识他们, + 这就达到了公司高层与底层员工实现解耦的目的。这就是将“高层依赖底层”倒置为“底层依赖高层”的好处。 - 接口隔离原则`(Interface Segregation Principle)` - 接口隔离原则(Interface Segregation Principle)指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对应一个职能。 - - 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想, - 从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 + 接口隔离原则指的是对高层接口的独立、分化,客户端对类的依赖基于最小接口,而不依赖不需要的接口。简单来说,就是切勿将接口定义成全能型的,否则 + 实现类就必须神通广大,这样便丧失了子类实现的灵活性,降低了系统的向下兼容性。反之,定义接口的时候应该尽量拆分成较小的粒度,往往一个接口只对 + 应一个职能。 + 这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计 + 思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。 - 迪米特法则(最少知识原则)`(Demeter Principle)` - 迪米特法则(law of Demeter)也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、简单化模块间的通信,并达到松耦合的目的。反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变得举步维艰。举个例子,我们买了一台游戏机,主机内部集成了非常复杂的电路及电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理,但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装。 - 之前我们学过的“门面模式”就是极好的范例。例如我们去某单位办理一项业务,来到业务大厅一脸茫然,各种填表、盖章等复杂的办理流程让人一头雾水,有可能来回折腾几个小时。假若有一个提供快速通道服务的“门面”办理窗口,那么我们只需简单地把材料递交过去就可以了,“办理人“与“门面”保持最简单的通信,对于门面里面发生的事情,办理人则知之甚少,更没有必要去亲力亲为。要设计出符合迪米特法则的软件,切勿跨越红线,干涉他人内务。系统模块一定要最大程度地隐藏内部逻辑,大门一定要紧锁,防止陌生人随意访问,而对外只适可而止地暴露最简单的接口,让模块间的通信趋向“简单化”“傻瓜化”。 + 迪米特法则也被称为最少知识原则,它提出一个模块对其他模块应该知之甚少,或者说模块之间应该彼此保持陌生,甚至意识不到对方的存在,以此最小化、 + 简单化模块间的通信,并达到松耦合的目的。反之,模块之间若存在过多的关联,那么一个很小的变动则可能会引发蝴蝶效应般的连锁反应,最终会波及大范 + 围的系统变动。我们说,缺乏良好封装性的系统模块是违反迪米特法则的,牵一发动全身的设计使系统的扩展与维护变的举步维艰。举个例子,我们买了一台 + 游戏机,主机内部集成了非常复杂的电路及电子元件,这些对外部来说完全是不可见的,就像一个黑盒子。虽然我们看不到黑盒子的内部构造与工作原理, + 但它向外部开放了控制接口,让我们可以接上手柄对其进行访问,这便构成了一个完美的封装。 + “门面模式”就是极好的范例。例如我们去某单位办理一项业务,来到业务大厅一脸茫然,各种填表、盖章等复杂的办理流程让人一头雾水,有可能来回折腾 + 几个小时。假若有一个提供快速通道服务的“门面”办理窗口,那么我们只需简单地把材料递交过去就可以了,“办理人“与“门面”保持最简单的通信,对于门面 + 里面发生的事情,办理人则知之甚少,更没有必要去亲力亲为。要设计出符合迪米特法则的软件,切勿跨越红线,干涉他人内务。系统模块一定要最大程度地 + 隐藏内部逻辑,大门一定要紧锁,防止陌生人随意访问,而对外只适可而止地暴露最简单的接口,让模块间的通信趋向“简单化”“傻瓜化”。 - 合成复用原则`(Composite Reuse Principle)` 原则是尽量使用合成/聚合的方式,而不是使用继承。 - 单一职责原则(Single Responsibility Principle) - 我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是单一职责原则(SingleResponsibility Principle)。 - 以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计能使类具备“高内聚性”,让单个模块变得“简单”“易懂”,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。 + 我们知道,一套功能完备的软件系统可能是非常复杂的。既然要利用好面向对象的思想,那么对一个大系统的拆分、模块化是不可或缺的软件设计步骤。 + 面向对象以“类”来划分模块边界,再以“方法”来分隔其功能。我们可以将某业务功能划归到一个类中,也可以拆分为几个类分别实现,但是不管对其负责的业 + 务范围大小做怎样的权衡与调整,这个类的角色职责应该是单一的,或者其方法所完成的功能也应该是单一的。总之,不是自己分内之事绝不该负责,这就是 + 单一职责原则。 + 以最典型的“责任链模式”为例,其环环相扣的每个节点都“各扫门前雪”,这种清晰的职责范围划分就是单一职责原则的最佳实践。符合单一职责原则的设计 + 能使类具备“高内聚性”,让单个模块变得“简单”“易懂”,如此才能增强代码的可读性与可复用性,并提高系统的易维护性与易测试性。 1.工厂方法模式`(Factory Method)` --- -制造业是一个国家工业经济发展的重要支柱,而工厂则是其根基所在。程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法(Factory Method)则可以升华为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。 +制造业是一个国家工业经济发展的重要支柱,而工厂则是其根基所在。程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法则可以升华 +为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。 工厂内部封装的生产逻辑对外部来说像一个黑盒子,外部不需要关心工厂内部细节,外部类只管调用即可。 工厂方法模式分为三种: @@ -139,27 +164,28 @@ sender.Send(); ``` -总体来说,工厂模式适合凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中,第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。 - - +总体来说,工厂模式适合凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。在以上的三种模式中, +第一种如果传入的字符串有误,不能正确创建对象,第三种相对于第二种,不需要实例化工厂类,所以,大多数情况下,我们会选用第三种——静态工厂方法模式。 2.抽象工厂模式`(Abstract Factory)` --- +工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑, +有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。 -工厂方法模式有一个问题就是,类的创建依赖工厂类,也就是说,如果想要拓展程序,必须对工厂类进行修改,这违背了闭包原则,所以,从设计角度考虑,有一定的问题,如何解决?就用到抽象工厂模式,创建多个工厂类,这样一旦需要增加新的功能,直接增加新的工厂类就可以了,不需要修改之前的代码。 - -抽象工厂模式(Abstract Factory)是对工厂的抽象化,而不只是制造方法。我们知道,为了满足不同用户对产品的多样化需求,工厂不会只局限于生产一类产品,但是系统如果按工厂方法那样为每种产品都增加一个新工厂又会造成工厂泛滥。所以,为了调和这种矛盾,抽象工厂模式提供了另一种思路,将各种产品分门别类,基于此来规划各种工厂的制造接口,最终确立产品制造的顶级规范,使其与具体产品彻底脱钩。抽象工厂是建立在制造复杂产品体系需求基础之上的一种设计模式,在某种意义上,我们可以将抽象工厂模式理解为工厂方法模式的高度集群化升级版。 -针对这种情况,我们就需要进行产业规划与整合,对现有工厂进行重构。例如,我们可以基于产品品牌与系列进行生产线规划,按品牌划分A工厂与B工厂。具体以汽车工厂举例,A品牌汽车有轿车、越野车、跑车3个系列的产品,同样地,B品牌汽车也包括以上3个系列的产品,如此便形成了两个产品族,分别由A工厂和B工厂负责生产,每个工厂都有3条生产线,分别生产这3个系列的汽车,如图5-1所示。 - - - +抽象工厂模式是对工厂的抽象化,而不只是制造方法。我们知道,为了满足不同用户对产品的多样化需求,工厂不会只局限于生产一类产品,但是系统如果按工厂 +方法那样为每种产品都增加一个新工厂又会造成工厂泛滥。所以,为了调和这种矛盾,抽象工厂模式提供了另一种思路,将各种产品分门别类,基于此来规划各种 +工厂的制造接口,最终确立产品制造的顶级规范,使其与具体产品彻底脱钩。 +抽象工厂是建立在制造复杂产品体系需求基础之上的一种设计模式,在某种意义上,我们可以将抽象工厂模式理解为工厂方法模式的高度集群化升级版。 +针对这种情况,我们就需要进行产业规划与整合,对现有工厂进行重构。例如,我们可以基于产品品牌与系列进行生产线规划,按品牌划分A工厂与B工厂。 +具体以汽车工厂举例,A品牌汽车有轿车、越野车、跑车3个系列的产品,同样的,B品牌汽车也包括以上3个系列的产品,如此便形成了两个产品族,分别由A工厂 +和B工厂负责生产,每个工厂都有3条生产线,分别生产这3个系列的汽车。 还是用上面的例子,只是在工厂类这里需要改一下,提供两个不同的工厂类,他们要实现同一个接口: ```java public interface IProvider { - public ISender produce(); + ISender produce(); } ``` @@ -171,8 +197,7 @@ public class SendMailFactory implements IProvider { public ISender produce(){ return new MailSender(); } -} - +} public class SendSmsFactory implements IProvider{ @Override @@ -190,13 +215,13 @@ ISender sender = provider.produce(); sender.Send(); ``` -其实这个模式的好处就是,如果你现在想增加一个功能:发送即时信息,则只需做一个实现类,实现`ISender`接口,同时做一个工厂类,实现`IProvider`接口,就`OK`了,无需去改动现成的代码。这样做,拓展性较好! +其实这个模式的好处就是,如果你现在想增加一个功能:发送即时信息,则只需做一个实现类,实现`ISender`接口,同时做一个工厂类,实现`IProvider`接口, +就`OK`了,无需去改动现成的代码。这样做,拓展性较好! 3.单例模式`(Singleton)` --- - 单例模式能保证在一个`JVM`中,该对象只有一个实例存在。 这样的模式有几个好处: @@ -225,7 +250,9 @@ public class SingleTon { } ``` -这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对`getInstance()`方法加`synchronized`关键字,但是,`synchronized`关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用`getInstance()`,都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个: +这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对 +`getInstance()`方法加`synchronized`关键字,但是,`synchronized`关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用 +`getInstance()`,都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个: ```java public class SingleTon { @@ -253,8 +280,11 @@ public class SingleTon { } ``` -似乎解决了之前提到的问题,将`synchronized`关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在`instance`为`null`,并创建对象的时候才需要加锁,性能有一定的提升。 -但是,这样的情况,还是有可能有问题的,看下面的情况:在`Java`指令中创建对象和赋值操作是分开进行的,也就是说`instance = new Singleton();`语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能`JVM`会为新`的Singleton`实例分配空间,然后直接赋值给`instance`成员,然后再去初始化这个`Singleton`实例。这样就可能出错了。 +似乎解决了之前提到的问题,将`synchronized`关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在`instance`为`null`,并创建对象的时候 +才需要加锁,性能有一定的提升。 +但是,这样的情况,还是有可能有问题的,看下面的情况:在`Java`指令中创建对象和赋值操作是分开进行的,也就是说`instance = new Singleton();` +语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能`JVM`会为新`的Singleton`实例分配空间,然后直接赋值给`instance`成员, +然后再去初始化这个`Singleton`实例。这样就可能出错了。 我们以`A`、`B`两个线程为例: @@ -286,18 +316,25 @@ public class SingleTon { } ``` -单例模式使用内部类来维护单例的实现,`JVM`内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用`getInstance()`的时候,`JVM`能够帮我们保证`instance`只被创建一次,并且会保证把赋值给`instance`的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。 -其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。 +单例模式使用内部类来维护单例的实现,`JVM`内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。 +这样当我们第一次调用`getInstance()`的时候,`JVM`能够帮我们保证`instance`只被创建一次,并且会保证把赋值给`instance`的内存初始化完毕, +这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。 +其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况, +选择最适合自己应用场景的实现方法。 4.建造者模式`(Builder)` --- -建造者模式(Builder)所构建的对象一定是庞大而复杂的,并且一定是按照既定的制造工序将组件组装起来的,例如计算机、汽车、建筑物等。我们通常将负责构建这些大型对象的工程师称为建造者。建造者模式又称为生成器模式,主要用于对复杂对象的构建、初始化,它可以将多个简单的组件对象按顺序一步步组装起来,最终构建成一个复杂的成品对象。与工厂系列模式不同的是,建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造工序能够产出不同的产品。 - -工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的`Test`测试类结合起来得到的。 +建造者模式所构建的对象一定是庞大而复杂的,并且一定是按照既定的制造工序将组件组装起来的,例如计算机、汽车、建筑物等。我们通常将负责构建这些大型 +对象的工程师称为建造者。 +建造者模式又称为生成器模式,主要用于对复杂对象的构建、初始化,它可以将多个简单的组件对象按顺序一步步组装起来,最终构建成一个复杂的成品对象。 +与工厂系列模式不同的是,建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造 +工序能够产出不同的产品。 +工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性, +其实建造者模式就是前面抽象工厂模式和最后的`Test`测试类结合起来得到的。 还是前面的例子,一个`ISender`接口,两个实现类`MailSender`和`SmsSender`。最后,建造者类如下: ```java @@ -323,21 +360,24 @@ Builder builder = new Builder(); builder.produceMailSender(10); ``` -建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工程模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。 +建造者模式将很多功能集成到一个类里,这个类可以创造出比较复杂的东西。所以与工程模式的区别就是:工厂模式关注的是创建单个产品,而建造者模式则关注 +创建符合对象,多个部分。因此,是选择工厂模式还是建造者模式,依实际情况而定。 5.原型模式`(Prototype)` --- -在制造业中通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,如果达到了质量要求,即可参照这个原型进行批量生产。原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类。 +在制造业中通常是指大批量生产开始之前研发出的概念模型,并基于各种参数指标对其进行检验,如果达到了质量要求,即可参照这个原型进行批量生产。 +原型模式达到以原型实例创建副本实例的目的即可,并不需要知道其原始类。 也就是说,原型模式可以用对象创建对象,而不是用类创建对象,以此达到效率的提升。 -构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程),再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。 +构造一个对象的过程是耗时耗力的。想必大家一定有过打印和复印的经历,为了节省成本,我们通常会用打印机把电子文档打印到A4纸上(原型实例化过程), +再用复印机把这份纸质文稿复制多份(原型拷贝过程),这样既实惠又高效。 那么,对于第一份打印出来的原文稿,我们可以称之为“原型文件”,而对于复印过程,我们则可以称之为“原型拷贝” 想必大家已经明白了类的实例化与克隆之间的区别,二者都是在造对象,但方法绝对是不同的。 ***原型模式的目的是从原型实例克隆出新的实例,对于那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况,原型模式是更好的选择*** -该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。本小结会通过对象的复制,进行讲解。在`Java`中,复制对象是通过`clone()`实现的,先创建一个原型类: +该模式的思想就是将一个对象作为原型,对其进行复制、克隆,产生一个和原对象类似的新对象。在`Java`中,复制对象是通过`clone()`实现的,先创建一个原型类: ```java public class Prototype implements Cloneable { @@ -348,10 +388,14 @@ public class Prototype implements Cloneable { } ``` -很简单,一个原型类,只需要实现`Cloneable`接口,重写`clone()`方法,此处`clone()`方法可以改成任意的名称,因为`Cloneable`接口是个空接口,你可以任意定义实现类的方法名,如`cloneA`或者`cloneB`,因为此处的重点是`super.clone()`这句话,`super.clone()`调用的是`Object`的`clone()`方法,而在`Object`类中,`clone()`是`native`的。 +很简单,一个原型类,只需要实现`Cloneable`接口,重写`clone()`方法,此处`clone()`方法可以改成任意的名称,因为`Cloneable`接口是个空接口, +你可以任意定义实现类的方法名,如`cloneA`或者`cloneB`,因为此处的重点是`super.clone()`这句话, +`super.clone()`调用的是`Object`的`clone()`方法,而在`Object`类中,`clone()`是`native`的。 -我们都知道,Java中的变量分为原始类型和引用类型,所谓浅拷贝是指只复制原始类型的值,比如横坐标x与纵坐标y这种以原始类型int定义的值,它们会被复制到新克隆出的对象中。 -而引用类型同样会被拷贝,但是请注意这个操作只是拷贝了地址引用(指针),也就是说副本与原型中的对象是同一个,因为两个同样的地址实际指向的内存对象是同一个对象。需要注意的是,克隆方法中调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被真正克隆。 +我们都知道,Java中的变量分为原始类型和引用类型,所谓浅拷贝是指只复制原始类型的值,比如横坐标x与纵坐标y这种以原始类型int定义的值, +它们会被复制到新克隆出的对象中。 +而引用类型同样会被拷贝,但是请注意这个操作只是拷贝了地址引用(指针),也就是说副本与原型中的对象是同一个,因为两个同样的地址实际指向的内存对象是 +同一个对象。需要注意的是,克隆方法中调用父类Object的clone方法进行的是浅拷贝,所以此处的bullet并没有被真正克隆。 在这儿,将结合对象的浅复制和深复制来说一下,首先需要了解对象深、浅复制的概念: @@ -409,14 +453,17 @@ class SerializableObject implements Serializable { 实现深复制,需要采用流的形式读入当前对象的二进制输入,再写出二进制数据对应的对象。 -从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。究其本质,克隆操作时Java虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率远远高于“new”关键字所触发的实例化操作。看尽世间烦扰,拨开云雾见青天,有时候“简单粗暴”也是一种去繁从简、不绕弯路的解决方案。 +从类到对象叫作“创建”,而由本体对象至副本对象则叫作“克隆”,当需要创建多个类似的复杂对象时,我们就可以考虑用原型模式。究其本质,克隆操作时Java +虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率 +远远高于“new”关键字所触发的实例化操作。看尽世间烦扰,拨开云雾见青天,有时候“简单粗暴”也是一种去繁从简、不绕弯路的解决方案。 适配器模式`(Adapter)` --- 适配器模式将某个类的接口转换成客户端期望的另一个接口表示,目的是消除由于接口不匹配所造成的类的兼容性问题。 -适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时,适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。 +适配器模式(Adapter)通常也被称为转换器,顾名思义,它一定是进行适应与匹配工作的物件。当一个对象或类的接口不能匹配用户所期待的接口时, +适配器就充当中间转换的角色,以达到兼容用户接口的目的,同时适配器也实现了客户端与接口的解耦,提高了组件的可复用性。 主要分为三类: @@ -428,36 +475,36 @@ class SerializableObject implements Serializable { ```java public interface Targetable { /* 与原类中的方法相同 */ - public void method1(); - - /* 新类的方法 */ - public void method2(); - } - - public class Source { - public void method1() { - System.out.println("this is original method!"); - } - } - - public class Adapter extends Source implements Targetable { - @Override - public void method2() { - System.out.println("this is the targetable method!"); - } - } - - public class AdapterTest { - public static void main(String[] args) { - Targetable target = new Adapter(); - target.method1(); - target.method2(); - } - } - - 输出结果是: - this is original method! - this is the targetable method! + public void method1(); + + /* 新类的方法 */ + public void method2(); + } + + public class Source { + public void method1() { + System.out.println("this is original method!"); + } + } + + public class Adapter extends Source implements Targetable { + @Override + public void method2() { + System.out.println("this is the targetable method!"); + } + } + + public class AdapterTest { + public static void main(String[] args) { + Targetable target = new Adapter(); + target.method1(); + target.method2(); + } + } + + 输出结果是: + this is original method! + this is the targetable method! ``` `Adapter`类继承`Source`类,实现`Targetable`接口,这样`Targetable`接口的实现类就具有了`Source`类的功能。 @@ -469,31 +516,31 @@ class SerializableObject implements Serializable { 只修改上面的`Adapter`类就可以了。 ```java - public class Adapter implements Targetable { - private Source source; - public Adapter(Source source){ - super(); - this.source = source; - } - @Override - public void method2() { - System.out.println("this is the targetable method!"); - } - - @Override - public void method1() { - source.method1(); - } - } - - public class AdapterTest { - public static void main(String[] args) { - Source source = new Source(); - Targetable target = new Wrapper(source); - target.method1(); - target.method2(); - } - } + public class Adapter implements Targetable { + private Source source; + public Adapter(Source source){ + super(); + this.source = source; + } + @Override + public void method2() { + System.out.println("this is the targetable method!"); + } + + @Override + public void method1() { + source.method1(); + } + } + + public class AdapterTest { + public static void main(String[] args) { + Source source = new Source(); + Targetable target = new Wrapper(source); + target.method1(); + target.method2(); + } + } ``` 输出结果和上面的一样,只是适配器的方法不同。 @@ -505,42 +552,42 @@ class SerializableObject implements Serializable { 而我们不和原始的接口打交道,只和该抽象类取得联系,所以我们写一个类,继承该抽象类,重写我们需要的方法就行。例如`Android ListView`中的`Adapter`就是这样设计的。 ```java - public interface ISourceable { - public void method1(); - public void method2(); - public void method3(); - public void method4(); - public void method5(); - public void method6(); - } - - public abstract class BaseAdapter implements ISourceable{ + public interface ISourceable { + public void method1(); + public void method2(); + public void method3(); + public void method4(); + public void method5(); + public void method6(); + } + + public abstract class BaseAdapter implements ISourceable{ public void method1() { } - public void method2() { - + public void method2() { + } - public void method3() { - + public void method3() { + } - public void method4() { - + public void method4() { + } - public void method5() { - + public void method5() { + } - public void method6() { - + public void method6() { + } - } - - public class ListAdapter extends BaseAdapter { - // 不用全部实现方法,只需要实现自己需要的就可以了 - public void method4(){ - System.out.println("the sourceable interface's first Sub1!"); - } - } + } + + public class ListAdapter extends BaseAdapter { + // 不用全部实现方法,只需要实现自己需要的就可以了 + public void method4(){ + System.out.println("the sourceable interface's first Sub1!"); + } + } ``` @@ -555,7 +602,9 @@ class SerializableObject implements Serializable { --- 装饰模式就是给一个对象增加一些新的功能,而且是动态的,要求装饰对象和被装饰对象实现同一个接口,装饰对象持有被装饰对象的实例。 -装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”,它们都是为了增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始对象动态地“包装”完成,是对类实例(对象)“装饰”的结果。 +装饰器模式(Decorator)能够在运行时动态地为原始对象增加一些额外的功能,使其变得更加强大。从某种程度上讲,装饰器非常类似于“继承”,它们都是为了 +增强原始对象的功能,区别在于方式的不同,后者是在编译时(compile-time)静态地通过对原始类的继承完成,而前者则是在程序运行时(run-time)通过对原始 +对象动态地“包装”完成,是对类实例(对象)“装饰”的结果。 `Source`类是被装饰类,`Decorator`类是一个装饰类,可以为`Source`类动态的添加一些功能,代码如下: @@ -609,18 +658,22 @@ after decorator! 代理模式`(Proxy)` --- -代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来,使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。 -代理模式不仅能增强原业务功能,更重要的是还能对其进行业务管控。对用户来讲,隐藏于代理中的实际业务被透明化了,而暴露出来的是代理业务,以此避免客户端直接进行业务访问所带来的安全隐患,从而保证系统业务的可控性、安全性。 +代理模式(Proxy),顾名思义,有代表打理的意思。某些情况下,当客户端不能或不适合直接访问目标业务对象时,业务对象可以通过代理把自己的业务托管起来, +使客户端间接地通过代理进行业务访问。如此不但能方便用户使用,还能对客户端的访问进行一定的控制。简单来说,就是代理方以业务对象的名义,代理了它的业务。 +代理模式不仅能增强原业务功能,更重要的是还能对其进行业务管控。对用户来讲,隐藏于代理中的实际业务被透明化了,而暴露出来的是代理业务,以此避免客户 +端直接进行业务访问所带来的安全隐患,从而保证系统业务的可控性、安全性。 -代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面,希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作,表达我们的想法。也就是说把专业事情交给专业的人来做。 +代理模式就是多一个代理类出来,替原对象进行一些操作,比如我们在租房子的时候回去找中介,为什么呢?因为你对该地区房屋的信息掌握的不够全面, +希望找一个更熟悉的人去帮你做,此处的代理就是这个意思。再如我们有的时候打官司,我们需要请律师,因为律师在法律方面有专长,可以替我们进行操作, +表达我们的想法。也就是说把专业事情交给专业的人来做。 代理按照代理的创建时期,可以分为两种: - 静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译。在程序运行前代理类的`.class`文件就已经存在了。 - 动态代理:在程序运行时运用反射机制动态创建而成。也就是说我们不需要专门针对某个接口去编写代码实现一个代理类,而是在接口运行时动态生成。 -静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能,主要用的是`JAVA`的反射机制。 - +静态代理是在编译时就将接口、实现类、代理类一股脑儿全部手动完成,但如果我们需要很多的代理,每一个都这么手动的去创建实属浪费时间,而且会有大量的 +重复代码,此时我们就可以采用动态代理,动态代理可以在程序运行期间根据需要动态的创建代理类及其实例,来完成具体的功能,主要用的是`Java`的反射机制。 下面用静态代理的示例: ```java @@ -691,22 +744,20 @@ after proxy! ```java //动态代理类只能代理接口(不支持抽象类),代理类都需要实现InvocationHandler类,实现invoke方法。该invoke方法就是调用被代理接口的所有方法时需要调用的,该invoke方法返回的值是被代理接口的一个实现类 public class DynamicProxy implements InvocationHandler { -  private Object object;//用于接收具体实现类的实例对象 -  //使用带参数的构造器来传递具体实现类的对象 -  public DynamicProxy(Object obj){ -    this.object = obj; -  } -  @Override -  public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { -    System.out.println("前置内容"); -    method.invoke(object, args); -    System.out.println("后置内容"); -    return null; -  } + private Object object;//用于接收具体实现类的实例对象 + //使用带参数的构造器来传递具体实现类的对象 + public DynamicProxy(Object obj){ + this.object = obj; + } + @Override + public Object invoke(Object proxy, Method method, Object[] args)throws Throwable { + System.out.println("前置内容"); + method.invoke(object, args); + System.out.println("后置内容"); + return null; + } } -测试类: - public static void main(String[] args) { ISourceable source = new Source(); InvocationHandler h = new DynamicProxy(source); @@ -715,20 +766,29 @@ public static void main(String[] args) { } ``` +动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理`(InvocationHandler.invoke)`。这样, +在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强 -动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理`(InvocationHandler.invoke)`。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。而且动态代理的应用使我们的类职责更加单一,复用性更强 - -代理对象就是把被代理对象包装一层,在其内部做一些额外的工作,比如用户需要上facebook,而普通网络无法直接访问,网络代理帮助用户先翻墙,然后再访问facebook。这就是代理的作用了。 +代理对象就是把被代理对象包装一层,在其内部做一些额外的工作,比如用户需要上facebook,而普通网络无法直接访问,网络代理帮助用户先翻墙, +然后再访问facebook。这就是代理的作用了。 -纵观静态代理与动态代理,它们都能实现相同的功能,而我们看从静态代理到动态代理的这个过程,我们会发现其实动态代理只是对类做了进一步抽象和封装,使其复用性和易用性得到进一步提升而这不仅仅符合了面向对象的设计理念,其中还有AOP的身影,这也提供给我们对类抽象的一种参考。关于动态代理与AOP的关系,个人觉得AOP是一种思想,而动态代理是一种AOP思想的实现! +纵观静态代理与动态代理,它们都能实现相同的功能,而我们看从静态代理到动态代理的这个过程,我们会发现其实动态代理只是对类做了进一步抽象和封装, +使其复用性和易用性得到进一步提升而这不仅仅符合了面向对象的设计理念,其中还有AOP的身影,这也提供给我们对类抽象的一种参考。关于动态代理与AOP的关系, +个人觉得AOP是一种思想,而动态代理是一种AOP思想的实现! 外观(门面)模式`(Facade)` --- -外观模式是为了解决类与类之家的依赖关系的,像`spring`一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个`Facade`类中,降低了类类之间的耦合度,该模式中没有涉及到接口。 它可能是最简单的结构型设计模式,它能将多个不同的子系统接口封装起来,并对外提供统一的高层接口,使复杂的子系统变得更易使用。 利用门面模式,我们可以把多个子系统“关”在门里面隐藏起来,成为一个整合在一起的大系统,来自外部的访问只需通过这道“门面”(接口)来进行,而不必再关心门面背后隐藏的子系统及其如何运转。总之,无论门面内部如何错综复杂,从门面外部看来总是一目了然,使用起来也很简单。 +外观模式是为了解决类与类之家的依赖关系的,像`spring`一样,可以将类和类之间的关系配置到配置文件中,而外观模式就是将他们的关系放在一个`Facade` +类中,降低了类类之间的耦合度,该模式中没有涉及到接口。 +它可能是最简单的结构型设计模式,它能将多个不同的子系统接口封装起来,并对外提供统一的高层接口,使复杂的子系统变得更易使用。 +利用门面模式,我们可以把多个子系统“关”在门里面隐藏起来,成为一个整合在一起的大系统,来自外部的访问只需通过这道“门面”(接口)来进行,而不必再 +关心门面背后隐藏的子系统及其如何运转。总之,无论门面内部如何错综复杂,从门面外部看来总是一目了然,使用起来也很简单。 -为了更形象地理解门面模式,我们先来看一个例子。早期的相机使用起来是非常麻烦的,拍照前总是要根据场景情况进行一系列复杂的操作,如对焦、调节闪光灯、调光圈等,非专业人士面对这么一大堆的操作按钮根本无从下手,拍出来的照片质量也不高。随着科技的进步,出现了一种相机,叫作“傻瓜相机”,以形容其使用起来的方便性。用户再也不必学习那些复杂的参数调节了,只要按下快门键就可完成所有操作。 +为了更形象地理解门面模式,我们先来看一个例子。早期的相机使用起来是非常麻烦的,拍照前总是要根据场景情况进行一系列复杂的操作,如对焦、调节闪光灯、 +调光圈等,非专业人士面对这么一大堆的操作按钮根本无从下手,拍出来的照片质量也不高。随着科技的进步,出现了一种相机,叫作“傻瓜相机”,以形容其使用 +起来的方便性。用户再也不必学习那些复杂的参数调节了,只要按下快门键就可完成所有操作。 下面以计算机启动过程为例: @@ -797,10 +857,7 @@ public class User { computer.startup(); computer.shutdown(); } -} - -输出: - +} start the computer! cpu startup! memory startup! @@ -813,15 +870,18 @@ disk shutdown! computer closed! ``` -如果我们没有`Computer`类,那么`CPU`、`Memory`、`Disk`他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来其他类的修改,这不是我们想要看到的,有了`Computer`类,他们之间的关系被放在了`Computer`类里,这样就起到了解耦的作用,这就是外观模式! +如果我们没有`Computer`类,那么`CPU`、`Memory`、`Disk`他们之间将会相互持有实例,产生关系,这样会造成严重的依赖,修改一个类,可能会带来 +其他类的修改,这不是我们想要看到的,有了`Computer`类,他们之间的关系被放在了`Computer`类里,这样就起到了解耦的作用,这就是外观模式! 桥接模式`(Bridge)` --- -桥接模式(Bridge)能将抽象与实现分离,使二者可以各自单独变化而不受对方约束,使用时再将它们组合起来,就像架设桥梁一样连接它们的功能,如此降低了抽象与实现这两个可变维度的耦合度,以保证系统的可扩展性。 +桥接模式能将抽象与实现分离,使二者可以各自单独变化而不受对方约束,使用时再将它们组合起来,就像架设桥梁一样连接它们的功能,如此降低了抽象与实现 +这两个可变维度的耦合度,以保证系统的可扩展性。 -桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的`JDBC`桥`DriverManager`一样,`JDBC`进行连接数据库的时候,在各个数据库之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是`JDBC`提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。 +桥接的用意是:将抽象化与实现化解耦,使得二者可以独立变化,像我们常用的`JDBC`桥`DriverManager`一样,`JDBC`进行连接数据库的时候,在各个数据库 +之间进行切换,基本不需要动太多的代码,甚至丝毫不用动,原因就是`JDBC`提供统一接口,每个数据库提供各自的实现,用一个叫做数据库驱动的程序来桥接就行了。 ```java public interface ISourceable { @@ -861,9 +921,9 @@ public abstract class Bridge { } public class MyBridge extends Bridge { public void method(){ - if (getSource() == null) { - return; - } + if (getSource() == null) { + return; + } getSource().method(); } } @@ -895,7 +955,8 @@ this is the second sub! 组合模式`(Composite)` --- -组合模式(Composite)是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。 +组合模式是针对由多个节点对象(部分)组成的树形结构的对象(整体)而发展出的一种结构型设计模式,它能够使客户端在操作整体对象或者其下的每个节点对象 +时做出统一的响应,保证树形结构对象使用方法的一致性,使客户端不必关注对象的整体或部分,最终达到对象复杂的层次结构与客户端解耦的目的。 组合模式有时又叫部分-整体模式在处理类似树形结构的问题时比较方便。 @@ -969,7 +1030,11 @@ public static void main(String[] args) { ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flyweight_1.jpg?raw=true) -`FlyWeightFactory`负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象,如果没有,则创建一个新对象,`FlyWeight`是超类。一提到共享池,我们很容易联想到`Java`里面的`JDBC`连接池,想想每个连接的特点,我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,`url`、`driverClassName`、`username`、`password`及`dbname`,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据,在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。 +`FlyWeightFactory`负责创建和管理享元单元,当一个客户端请求时,工厂需要检查当前对象池中是否有符合条件的对象,如果有,就返回已经存在的对象, +如果没有,则创建一个新对象,`FlyWeight`是超类。一提到共享池,我们很容易联想到`Java`里面的`JDBC`连接池,想想每个连接的特点, +我们不难总结出:适用于作共享的一些个对象,他们有一些共有的属性,就拿数据库连接池来说,`url`、`driverClassName`、`username`、`password` +及`dbname`,这些属性对于每个连接来说都是一样的,所以就适合用享元模式来处理,建一个工厂类,将上述类似属性作为内部数据,其它的作为外部数据, +在方法调用时,当做参数传进来,这样就节省了空间,减少了实例的数量。 下面用数据库连接池的例子: @@ -1027,8 +1092,8 @@ public class ConnectionPool { 策略模式`(strategy)` --- -策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下: - +策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供 +统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数,关系图如下: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/strategy.jpg?raw=true) @@ -1089,28 +1154,33 @@ public class StrategyTest { 输出: 10 ``` -策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用哪个算法即可 +策略模式的决定权在用户,系统本身提供不同算法的实现,新增或者删除算法,对各种算法做封装。因此,策略模式多用在算法决策系统中,外部用户只需要决定用 +哪个算法即可 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/design_mode_celue.png?raw=true) -相信大家对上图中的计算机、USB接口还有各种设备之间的关系以及使用方法都非常熟悉了,这些模块组成的系统正是策略模式的最佳范例。策略接口就是图中的USB接口。 - -我们通过对计算机USB接口的标准化,使计算机系统拥有了无限扩展外设的能力,需要什么功能只需要购买相关的USB设备。可见在策略模式中,USB接口起到了至关重要的解耦作用。如果没有USB接口的存在,我们就不得不将外设直接“焊接”在主机上,致使设备与主机高度耦合,系统将彻底丧失对外设的替换与扩展能力。 - -变化是世界的常态,唯一不变的就是变化本身。拥有顺势而为、随机应变的能力才能立于不败之地。策略模式的运用能让系统的应变能力得到提升,适应随时变化的需求。接口的巧妙运用让一系列的策略可以脱离系统而单独存在,使系统拥有更灵活、更强大的“可插拔”扩展功能。 +相信大家对上图中的计算机、USB接口还有各种设备之间的关系以及使用方法都非常熟悉了,这些模块组成的系统正是策略模式的最佳范例。策略接口就是图中的 +USB接口。 +我们通过对计算机USB接口的标准化,使计算机系统拥有了无限扩展外设的能力,需要什么功能只需要购买相关的USB设备。可见在策略模式中,USB接口起到了 +至关重要的解耦作用。如果没有USB接口的存在,我们就不得不将外设直接“焊接”在主机上,致使设备与主机高度耦合,系统将彻底丧失对外设的替换与扩展能力。 +变化是世界的常态,唯一不变的就是变化本身。拥有顺势而为、随机应变的能力才能立于不败之地。策略模式的运用能让系统的应变能力得到提升,适应随时变化 +的需求。接口的巧妙运用让一系列的策略可以脱离系统而单独存在,使系统拥有更灵活、更强大的“可插拔”扩展功能。 模板方法模式`(Template Method)` --- -模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式(Template Method)则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中,对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。 +模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中, +对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。 -一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类,实现对子类的调用,先看个关系图: +一个抽象类中,有一个主方法,再定义1...n个方法,可以是抽象的,也可以是实际的方法,定义一个类,继承该抽象类,重写抽象方法,通过调用抽象类, +实现对子类的调用,先看个关系图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/template.jpg?raw=true) -就是在`AbstractCalculator`类中定义一个主方法`calculate()`,`calculate()`调用`spilt()`等,`Plus`和`Minus`分别继承`AbstractCalculator`类,通过对`AbstractCalculator`的调用实现对子类的调用,看下面的例子: +就是在`AbstractCalculator`类中定义一个主方法`calculate()`,`calculate()`调用`spilt()`等,`Plus`和`Minus`分别继承 +`AbstractCalculator`类,通过对`AbstractCalculator`的调用实现对子类的调用,看下面的例子: ```java public abstract class AbstractCalculator { @@ -1156,9 +1226,11 @@ public static void main(String[] args) { 观察者模式`(Observer)` --- -观察者模式(Observer)可以针对被观察对象与观察者对象之间一对多的依赖关系建立起一种行为自动触发机制,当被观察对象状态发生变化时主动对外发起广播,以通知所有观察者做出响应。 +观察者模式(Observer)可以针对被观察对象与观察者对象之间一对多的依赖关系建立起一种行为自动触发机制,当被观察对象状态发生变化时主动对外发起广播, +以通知所有观察者做出响应。 -观察者模式很好理解,类似于邮件订阅和`RSS`订阅,当我们浏览一些博客或`wiki`时,经常会看到`RSS`图标,就这的意思是,当你订阅了该文章,如果后续有更新,会及时通知你。简单的说就是当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。 +观察者模式很好理解,类似于邮件订阅和`RSS`订阅,当我们浏览一些博客或`wiki`时,经常会看到`RSS`图标,就这的意思是,当你订阅了该文章,如果后续有 +更新,会及时通知你。简单的说就是当一个对象变化时,其它依赖该对象的对象都会收到通知,并且随着变化!对象之间是一种一对多的关系。 在`Android`中我们常用的`Button.setOnClickListener()`其实就是用的观察者模式。大名鼎鼎的`RxJava`也是基于这种模式。 @@ -1254,9 +1326,11 @@ observer2 has received! 迭代器模式`(Iterator)` --- -迭代,在程序中特指对某集合中各元素逐个取用的行为。迭代器模式(Iterator)提供了一种机制来按顺序访问集合中的各元素,而不需要知道集合内部的构造。换句话讲,迭代器满足了对集合迭代的需求,并向外部提供了一种统一的迭代方式,而不必暴露集合的内部数据结构。 +迭代,在程序中特指对某集合中各元素逐个取用的行为。迭代器模式提供了一种机制来按顺序访问集合中的各元素,而不需要知道集合内部的构造。 +换句话讲,迭代器满足了对集合迭代的需求,并向外部提供了一种统一的迭代方式,而不必暴露集合的内部数据结构。 -迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。 +迭代器模式就是顺序访问聚集中的对象,一般来说,集合中非常常见,如果对集合类比较熟悉的话,理解本模式会十分轻松。 +这句话包含两层意思:一是需要遍历的对象,即聚集对象,二是迭代器对象,用于对聚集对象进行遍历访问。 `MyCollection`中定义了集合的一些操作,`MyIterator`中定义了一系列迭代操作,且持有`Collection`实例,我们来看看实现代码: @@ -1321,7 +1395,7 @@ public class MyIterator implements Iterator { @Override public Object next() { - if(pos Date: Thu, 9 Mar 2023 15:28:51 +0800 Subject: [PATCH 010/128] update notes --- ...00\351\235\242\350\257\225\351\242\230.md" | 2 +- .../Bitmap\344\274\230\345\214\226.md" | 8 ++--- .../Fragment\344\270\223\351\242\230.md" | 2 +- .../Scroller\347\256\200\344\273\213.md" | 4 +-- ...05\345\255\230\346\250\241\345\236\213.md" | 29 ++++--------------- 5 files changed, 13 insertions(+), 32 deletions(-) diff --git "a/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" "b/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" index f5a864c6..319f5ef8 100644 --- "a/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" +++ "b/BasicKnowledge/Android\345\237\272\347\241\200\351\235\242\350\257\225\351\242\230.md" @@ -1,7 +1,7 @@ Android基础面试题 === -没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题画画一顿就写完了,哈哈 +没有删这套题,虽然都是网上找的,在刚开始找工作的时候这套题帮了我很多,那时候`Android`刚起步,很多家都是这一套面试题,我都是直接去了不看题哗哗一顿就写完了,哈哈 现在估计没有公司会用这种笔试题了。还是留下来吧,回忆一下。 1. 下列哪些语句关于内存回收的说明是正确的? (b) diff --git "a/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" "b/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" index c127bf68..42b7954b 100644 --- "a/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" +++ "b/BasicKnowledge/Bitmap\344\274\230\345\214\226.md" @@ -3,9 +3,9 @@ Bitmap优化 1. 一个进程的内存可以由2个部分组成:`native和dalvik` `dalvik`就是我们平常说的`java`堆,我们创建的对象是在这里面分配的,而`bitmap`是直接在`native`上分配的。 - 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`的使用,所以如果`Java`突然占用了一个大块内存, + 一旦内存分配给`Java`后,以后这块内存即使释放后,也只能给`Java`使用,所以如果`Java`突然占用了一个大块内存, 即使很快释放了,`C`能用的内存也是16M减去`Java`最大占用的内存数。 - 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,上述的`4MBitmap`无法生成的原因, + 而`Bitmap`的生成是通过`malloc`进行内存分配的,占用的是`C`的内存,这个也就说明了,有时候`4MBitmap`无法生成的原因, 因为在`13M`被`Java`用过后,剩下`C`能用的只有`3M`了。 2. 在`Android`应用里,最耗费内存的就是图片资源。 @@ -58,7 +58,7 @@ Bitmap优化 // 打印出图片的宽和高 Log.d("example", opts.outWidth + "," + opts.outHeight); ``` - 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要跑缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 + 在实际项目中,可以利用上面的代码,先获取图片真实的宽度和高度,然后判断是否需要缩小。如果不需要缩小,设置inSampleSize的值为1。如果需要缩小,则动态计算并设置inSampleSize的值,对图片进行缩小。需要注意的是,在下次使用BitmapFactory的decodeFile()等方法实例化Bitmap对象前,别忘记将opts.inJustDecodeBound设置回false。否则获取的bitmap对象还是null。 以从Gallery获取一个图片为例讲解缩放: ```java @@ -134,4 +134,4 @@ Bitmap优化 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/BasicKnowledge/Fragment\344\270\223\351\242\230.md" "b/BasicKnowledge/Fragment\344\270\223\351\242\230.md" index d3d89ad8..81ceb228 100644 --- "a/BasicKnowledge/Fragment\344\270\223\351\242\230.md" +++ "b/BasicKnowledge/Fragment\344\270\223\351\242\230.md" @@ -334,4 +334,4 @@ public abstract class BaseFragment extends Fragment { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/BasicKnowledge/Scroller\347\256\200\344\273\213.md" "b/BasicKnowledge/Scroller\347\256\200\344\273\213.md" index 8ed68bfa..bfa12941 100644 --- "a/BasicKnowledge/Scroller\347\256\200\344\273\213.md" +++ "b/BasicKnowledge/Scroller\347\256\200\344\273\213.md" @@ -29,8 +29,8 @@ Scroller简介 public void computeScroll() { } ``` - 通过注释我们可以看到该方法又父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 - 继续往下跟发现在`draw()`方法中回去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 + 通过注释我们可以看到该方法由父类调用根据滚动的值去更新`View`,在使用`Scroller`的时候通常都要实现该方法。来达到子`View`的滚动效果。 + 继续往下跟发现在`draw()`方法中会去调用`computeScroll()`,而`draw()`方法会在父布局调用`drawChild()`的时候使用。 3. 具体关联 通过上面两步大体能得到`Scroller`与`View`的移动要通过`computeScroll()`来完成,但是在究竟如何进行代码实现。 diff --git "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" index 1e2fae6a..8e75224f 100644 --- "a/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" +++ "b/JavaKnowledge/Java\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -28,11 +28,9 @@ Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各 - 堆内存在JVM启动的时候被加载(初始大小: -Xms) - 堆内存在程序运行时会增加或减少 - 最小值: -Xmx -- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先 - 都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制 - 过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 +- 从结构上来分,可以分为新生代和老年代。而新生代又可以分为Eden空间、From Survivor空间(s0)、To Survivor空间(s1)。 所有新生成的对象首先都是放在新生代的。需要注意,Survivor的两个区是对称的,没先后关系,所以同一个区中可能同时存在从Eden复制过来的对象,和从前一个Survivor复制过来的对象,而复制到老年代的只有从第一个Survivor区过来的对象。而且,Survivor区总有一个是空的。 - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_heap_memory.png) +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm_heap_memory.png) 在并发编程中,多个线程之间采取什么机制进行通信(信息交换),什么机制进行数据的同步?在Java语言中,采用的是共享内存模型来实现多线程之间的信息交换和数据同步的。 @@ -44,10 +42,9 @@ Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各 每当一个方法执行完成时,该栈帧就会弹出栈帧的元素作为这个方法的返回值,并且清除这个栈帧,Java栈的栈顶的栈帧就是当前正在执行的活动栈,也就是当前正在执行的方法,PC寄存器也会指向该地址。只有这个活动的栈帧的本地变量可以被操作栈使用,当在这个栈帧中调用另外一个方法时,与之对应的一个新的栈帧被创建,这个新创建的栈帧被放到Java栈的栈顶,变为当前的活动栈。同样现在只有这个栈的本地变量才能被使用,当这个栈帧中所有指令都完成时,这个栈帧被移除Java栈,刚才的那个栈帧变为活动栈帧,前面栈帧的返回值变为这个栈帧的操作栈的一个操作数。 - - 线程stack中同样会包含该线程调用栈中所有方法执行所需要的本地变量,一个线程只能获取到它自己对应的线程stack。一个线程创建的本地变量对于其他任何线程都是不可见的。即使两个线程执行完全相同的代码,这两个线程仍然会在各自自己对应的线程stack中创建自己的本地变量。 所有基础类型的局部变量(boolean,byte,short,char,int,long,float,double)都被保存在自己的线程stack中。一个线程可以将一个基础变量的副本传递给另一个线程,但是它不能共享原始局部变量本身。 + 堆内存包含在Java应用程序中创建的所有对象,而不管创建该对象的线程是什么。这包括基本类型的对象版本(例如Byte,Integer,Long等)。创建对象并将其分配给局部变量,或者将其创建为另一个对象的成员变量都没有关系,该对象仍存储在堆中。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java-memory-model-1.png) @@ -60,12 +57,10 @@ Stack Memory、Method Area、PC、Native Stack Memory),这些区域都有各 - 静态类变量也与类定义一起存储在堆中。 - 引用对象的所有线程都可以访问堆上的对象。当线程可以访问对象时,它也可以访问该对象的成员变量。如果两个线程同时在同一个对象上调用一个方法,则它们都将有权访问该对象的成员变量,但是每个线程将拥有自己的局部变量副本。 - - Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError: - StackOverFlowError: 若Java虚拟机栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前Java虚拟机栈的最大深度的时候,就抛出StackOverFlowError异常。 -- OutOfMemoryError: 若 Java 虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 +- OutOfMemoryError: 若Java虚拟机栈的内存大小允许动态扩展,且当线程请求栈时内存用完了,无法再动态扩展了,此时抛出OutOfMemoryError异常。 ## Method Area @@ -91,8 +86,6 @@ Java 虚拟机栈会出现两种异常:StackOverFlowError 和 OutOfMemoryError 常量池中存储了如字符串、final变量值、类名和方法名常量。常量池在编译期间就被确定,并保存在已编译的.class文件中。一般分为两类:字面量和应用量。字面量就是字符串、final变量等。类名和方法名属于引用量。引用量最常见的是在调用方法的时候,根据方法名找到方法的引用,并以此定为到函数体进行函数代码的执行。引用量包含:类和接口的权限定名、字段的名称和描述符,方法的名称和描述符。 - - JVM会加载、链接、初始化class文件。一个class文件会把其所有所有符号引用都保留在一个位置,即常量池中。 每个class文件都会有一个对应constant pool。但是class文件中的常量池显然是不够的,因为需要再JVM上执行。这种情况下,需要runtime constant pool来服务JVM的运行。 @@ -136,21 +129,17 @@ Double i4 = 1.2; System.out.println(i3 == i4);// 输出false ``` - - ### Method Area & Constant Pool改动 很多人愿意把方法区称为“永久代”(Permanent Generation),本质上两者并不等价,仅仅是因为HotSpot虚拟机的设计团队选择把GC 分代收集扩展至方法区,或者说使用永久代来实现方法区而已。对于其他虚拟机(如BEA JRockit、IBM J9 等)来说是不存在永久代的概念的。 - - 在JDK1.7中,将字符串常量池和静态变量从方法区域中移到堆中,其余的运行时常量池仍在方法区域中,即hotspot中的永久生成。 所有的被intern的String被存储在PermGen区.PermGen区使用-XX:MaxPermSize=N来设置最大大小,但是由于应用程序string.intern通常是不可预测和不可控的,因此不好设置这个大小。设置不好的话,常常会引起java.lang.OutOfMemoryError: PermGen space。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java7_method_constant_pool.jpg) 在 JDK 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域(永久代使用的是JVM的堆内存空间,而元空间使用的是物理内存,直接受到本机的物理内存限制)。string constant pool仍然还是在Heap内存中。runtime constant pool也仍然在方法区。但是方法区的实现已经改成使用Metaspace。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/java8_memory_method.jpg) -为什么要移除永久代,改为元空间嗯? +为什么要移除永久代,改为元空间呢? > Metaspace与PermGen之间最大的区别在于:Metaspace并不在虚拟机中,而是使用本机内存。如果没有使用-XX:MaxMetaspaceSize来设置类的元数据的大小,其最大可利用空间是整个系统内存的可用空间。JVM也可以增加本地内存空间来满足类元数据信息的存储。 > 但是如果没有设置最大值,则可能存在bug导致Metaspace的空间在不停的扩展,会导致机器的内存不足;进而可能出现swap内存被耗尽;最终导致进程直接被系统直接kill掉。 @@ -161,10 +150,6 @@ System.out.println(i3 == i4);// 输出false 在很大程度上,这只是名称的更改。早在引入PermGen时,就没有Java EE或动态类的加载(取消加载),因此,一旦加载了一个类,该类便一直停留在内存中,直到JVM关闭为止,从而实现了永久生成。如今,可以在JVM的生命周期内加载和卸载类,因此对于保留元数据的区域,Metaspace更有意义。 - - - - ## Native Method Stack 本地方法栈和Java栈所发挥的作用非常相似,区别不过是Java栈为JVM执行Java方法服务,而本地方法栈为JVM执行Native方法服务。如Java使用c或c++编写的接口服务时,代码在此区运行,本地方法栈也会抛出StackOverflowError和OutOfMemoryError异常。 @@ -190,7 +175,6 @@ Java线程之间的通信由Java内存模型(JMM)控制,JMM决定一个线程 ## 重排 - 在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。 这里说的重排序可以发生在好几个地方:编译器、运行时、JIT等,比如编译器会觉得把一个变量的写操作放在最后会更有效率,编译后,这个指令就在最后了(前提是只要不改变程序的语义,编译器、执行器就可以这样自由的随意优化),一旦编译器对某个变量的写操作进行优化(放到最后),那么在执行之前,另一个线程将不会看到这个执行结果。 @@ -213,8 +197,6 @@ Class Reordering { ``` 假设这段代码有2个线程并发执行,线程A执行writer方法,线程B执行reader方法,线程B看到y的值为2,因为把y设置成2发生在变量x的写入之后(代码层面),所以能断定线程B这时看到的x就是1吗? - - 当然不行!因为在writer方法中,可能发生了重排序,y的写入动作可能发在x写入之前,这种情况下,线程B就有可能看到x的值还是0。 在Java内存模型中,描述了在多线程代码中,哪些行为是正确的、合法的,以及多线程之间如何进行通信,代码中变量的读写行为如何反应到内存、CPU缓存的底层细节。 @@ -250,7 +232,6 @@ a是在栈内存中的,当main()方法执行结束,a就被清理了。但是 - [Where Has the Java PermGen Gone?](https://www.infoq.com/articles/Java-PERMGEN-Removed/) -​ --- - 邮箱 :charon.chui@gmail.com - Good Luck! From f834ce5534f7ad54be2aad95b3f5235c55407f2e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 13 Mar 2023 21:46:33 +0800 Subject: [PATCH 011/128] update --- AdavancedPart/ApplicationId vs PackageName.md | 4 ++-- ...11\345\205\250\351\227\256\351\242\230.md" | 4 ++-- ...straintLaayout\347\256\200\344\273\213.md" | 7 +------ ...345\217\212ANR\345\210\206\346\236\220.md" | 21 +------------------ .../Zipalign\344\274\230\345\214\226.md" | 2 +- ...mposing builds\347\256\200\344\273\213.md" | 2 +- ...57\345\212\250\350\277\207\347\250\213.md" | 6 ++---- 7 files changed, 10 insertions(+), 36 deletions(-) diff --git a/AdavancedPart/ApplicationId vs PackageName.md b/AdavancedPart/ApplicationId vs PackageName.md index 62829a21..35dc2f0a 100644 --- a/AdavancedPart/ApplicationId vs PackageName.md +++ b/AdavancedPart/ApplicationId vs PackageName.md @@ -7,7 +7,7 @@ ApplicationId vs PackageName 在`Android`官方文档中有一句是这样描述`applicationId`的:`applicationId : the effective packageName`,真是言简意赅,那既然`applicationId`是有效的包明了,`packageName`算啥? -所有`Android`应用都有一个包名。包名在设备上能唯一的标示一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 +所有`Android`应用都有一个包名。包名在设备上能唯一的标识一个应用,它在`Google Play`应用商店中也是唯一的。这就意味着一旦你使用一个包名发布应用后,你就永 远不能改变它的包名;如果你改了包名就会导致你的应用被认为是一个新的应用,并且已经使用你之前应用的用户将不会看到作为更新的新应用包。 之前的`Android Gradle`构建系统中,应用的包名是由你的`manifest`文件中的根元素中的`package`属性定义的: @@ -86,4 +86,4 @@ buildTypes { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" index 1485641a..0a6a7f1a 100644 --- "a/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" +++ "b/AdavancedPart/BroadcastReceiver\345\256\211\345\205\250\351\227\256\351\242\230.md" @@ -5,7 +5,7 @@ BroadcastReceiver安全问题 - 保证发送的广播要发送给指定的对象 当应用程序发送某个广播时系统会将发送的`Intent`与系统中所有注册的`BroadcastReceiver`的`IntentFilter`进行匹配,若匹配成功则执行相应的`onReceive`函数。可以通过类似`sendBroadcast(Intent, String)`的接口在发送广播时指定接收者必须具备的`permission`或通过`Intent.setPackage`设置广播仅对某个程序有效。 -- 保证我接收到的广播室指定对象发送过来的 +- 保证我接收到的广播是指定对象发送过来的 当应用程序注册了某个广播时,即便设置了`IntentFilter`还是会接收到来自其他应用程序的广播进行匹配判断。对于动态注册的广播可以通过类似`registerReceiver(BroadcastReceiver, IntentFilter, String, android.os.Handler)`的接口指定发送者必须具备的`permission`,对于静态注册的广播可以通过`android:exported="false"`属性表示接收者对外部应用程序不可用,即不接受来自外部的广播。 `android.support.v4.content.LocalBroadcastManager`工具类,可以实现在自己的进程内进行局部广播发送与注册,使用它比直接通过sendBroadcast(Intent)发送系统全局广播有以下几个好处: @@ -46,4 +46,4 @@ protected void onDestroy() { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" index d765862d..f46d2dc2 100644 --- "a/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" +++ "b/AdavancedPart/ConstraintLaayout\347\256\200\344\273\213.md" @@ -224,15 +224,10 @@ implementation 'com.android.support.constraint:constraint-layout:1.1.0' 它有点类似于`RelativeLayout`,但远比`RelativeLayout`要更强大。 `ConstraintLayout`在测量/布局阶段的性能比 `RelativeLayout`大约高`40%`。 - - - - - - [Build a Responsive UI with ConstraintLayout](https://developer.android.com/training/constraint-layout/index.html) - [ConstraintLayout文档](https://developer.android.com/reference/android/support/constraint/package-summary.html) --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" index 40db0adf..df4d8ccf 100644 --- "a/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" +++ "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" @@ -15,7 +15,7 @@ 6、再执行handleAppCrashLocked方法: 当1分钟内同一进程连续crash两次时,且非persistent进程,则直接结束该应用所有activity,并杀死该进程以及同一个进程组下的所有进程。然后再恢复栈顶第一个非finishing状态的activity; -当1分钟内同一进程连续crash两次时,且persistent进程,,则只执行恢复栈顶第一个非finishing状态的activity; +当1分钟内同一进程连续crash两次时,且persistent进程,则只执行恢复栈顶第一个非finishing状态的activity; 当1分钟内同一进程未发生连续crash两次时,则执行结束栈顶正在运行activity的流程。 7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框; @@ -35,10 +35,6 @@ - - - - Native Crash 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 @@ -70,10 +66,8 @@ 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关键点解读 @@ -102,7 +96,6 @@ CPU usage from 18101ms to 0ms ago 如果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 @@ -110,7 +103,6 @@ CPU usage from 18101ms to 0ms ago | state=S schedstat=( 5119636327 325064933 4204 ) utm=460 stm=51 core=4 HZ=100 | stack=0xff575000-0xff577000 stackSize=8MB | held mutexes= -复制代码 上述关键字段的含义如下所示: tid:线程号 @@ -123,14 +115,3 @@ stm:该线程在内核态的执行时间(jiffies) sCount:该线程被挂起的次数 dsCount:该线程被调试器挂起的次数 self:线程本身的地址 - - - - - - - - - - - diff --git "a/AppPublish/Zipalign\344\274\230\345\214\226.md" "b/AppPublish/Zipalign\344\274\230\345\214\226.md" index f79fa2cc..c69dedfa 100644 --- "a/AppPublish/Zipalign\344\274\230\345\214\226.md" +++ "b/AppPublish/Zipalign\344\274\230\345\214\226.md" @@ -22,7 +22,7 @@ And any files added to an "aligned" archive will not be aligned. ``` 大意就是它提供了一个灰常重要滴功能来确保所有未压缩的数据都从文件的开始位置以指定的4字节对齐方式排列,例如图片或者 -`raw`文件。当然好处也是大大的,就是能够减少内存的资源消耗。最后他还特意提醒了你一下就是已经在对`apk`签完名之后再用`zipalign` +`raw`文件。当然好处也是大大的,就是能够减少内存的资源消耗。最后他还特意提醒了你一下就是一定在对`apk`签完名之后再用`zipalign` 优化,如果你在之前用,那无效。 废多看用法: diff --git "a/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" "b/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" index e4060a5d..0c6ea44b 100644 --- "a/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" +++ "b/Gradle&Maven/Composing builds\347\256\200\344\273\213.md" @@ -293,7 +293,7 @@ gradlePlugin { 3. 在versionPlugin/src/main/java/包名/目录下新建Deps.kt文件,添加你的依赖配置,如: -```groovy +```kotlin package com.xx.xx.versionplugin class Deps : Plugin { 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 8af22363..5750685e 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" @@ -8,7 +8,7 @@ Activity启动过程 今天就来分析一下,我们开启`Activity`主要有两种方式: -- 通过桌面图标启动,桌面就是`Launcher`其实他也是一个应用程序,他也是继承`Activity`。 +- 通过桌面图标启动,桌面就是`Launcher`,其实他也是一个应用程序,他也是继承`Activity`。 - 在程序内部调用`startActivity()`开启。 而`Launcher`点击图标其实也是调用了`Activity`的`startActivity()`方法,所以我们就从`startActivity()`方法入手了。 @@ -141,9 +141,7 @@ public class Instrumentation { ... } ``` -放狗查了下`Instrumentation`的意思是仪器、工具、装置的意思。我就大体翻一下(英语不好- -~,可能不准确)该类是实现应用程序代码的基类,当该类在 -启动的状态下运行时,该类会在其他任何应用程序运行前进行初始化,允许你件事所有应用程序与系统的交互。一个`Instrumentation`实例会通过`Manifest`文件 -中的` Date: Thu, 16 Mar 2023 22:08:58 +0800 Subject: [PATCH 012/128] update file --- ...07\347\250\213\350\257\246\350\247\243.md" | 6 +-- ...06\345\217\221\350\257\246\350\247\243.md" | 38 +++++++++--------- .../AsyncTask\350\257\246\350\247\243.md" | 40 +++++++++---------- ...20\347\240\201\345\210\206\346\236\220.md" | 2 +- ...pURLConnection\350\257\246\350\247\243.md" | 22 +++++----- ...\350\257\246\350\247\243(\344\270\212).md" | 9 +++-- ...\350\257\246\350\247\243(\344\270\213).md" | 10 ++--- ...20\347\240\201\345\210\206\346\236\220.md" | 6 +-- ...07\347\250\213\350\257\246\350\247\243.md" | 16 ++++---- ...44\271\211View\350\257\246\350\247\243.md" | 10 ++--- "VideoDevelopment/CDN\345\217\212PCDN.md" | 8 ++-- "VideoDevelopment/DNS\345\217\212HTTPDNS.md" | 6 +-- 12 files changed, 88 insertions(+), 85 deletions(-) diff --git "a/SourceAnalysis/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" "b/SourceAnalysis/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" index e71ed344..c686be97 100644 --- "a/SourceAnalysis/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/Activity\347\225\214\351\235\242\347\273\230\345\210\266\350\277\207\347\250\213\350\257\246\350\247\243.md" @@ -41,7 +41,7 @@ public void setContentView(int layoutResID) { // decor, when theme attributes and the like are crystalized. Do not check the feature // before this happens. if (mContentParent == null) { - // 内部会创建mContentParent, 如果mDecor为null就创建,然后如果mContentParent为null,就根据当前要显示的主题去添加对应的布局, + // 内部会创建mContentParent, 如果mDecor为null就创建,然后如果mContentParent为null,就根据当前要显示的主题去添加对应的布局, // 并把该布局中id为content的ViewGroup赋值给mContentParent。 installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { @@ -54,7 +54,7 @@ public void setContentView(int layoutResID) { getContext()); transitionTo(newScene); } else { - // 把布局inflate后添加到mContentParent中 + // 把布局inflate后添加到mContentParent中 mLayoutInflater.inflate(layoutResID, mContentParent); } final Callback cb = getCallback(); @@ -582,4 +582,4 @@ protected ViewGroup generateLayout(DecorView decor) { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! 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 1ef073c8..85b1e5fc 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,12 +3,12 @@ Android Touch事件分发详解 先说一些基本的知识,方便后面分析源码时能更好理解。 - 所有`Touch`事件都被封装成`MotionEvent`对象,包括`Touch`的位置、历史记录、第几个手指等. -- 事件类型分为`ACTION_DOWN`,`ACTION_UP`,`ACTION_MOVE`,`ACTION_POINTER_DOWN`,`ACTION_POINTER_UP`,`ACTION_CANCEL`, 每个 +- 事件类型分为`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()`消费事件.这些方法的返回值如果是true表示事件被当前视图消费掉。 +- 事件的处理分别为`dispatchTouchEvent()`分发事件(`TextView`等这种最小的`View`中不会有该方式)、`onInterceptTouchEvent()`拦截事件(`ViewGroup`中拦截事件)、`onTouchEvent()`消费事件.这些方法的返回值如果是true表示事件被当前视图消费掉。 - 事件从`Activity.dispatchTouchEveent()`开始传递,只要没有停止拦截,就会从最上层(`ViewGroup`)开始一直往下传递,子`View`通过`onTouchEvent()`消费事件。(隧道式向下分发). -- 如果时间从上往下一直传递到最底层的子`View`,但是该`View`没有消费该事件(不是clickable或longclickable),那么该事件会反序网上传递(从该`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`)后该事件 @@ -51,11 +51,11 @@ public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } - // 首先交给本Activity对应的Window来进行分发,如果分发了,就返回true,事件循环结束 + // 首先交给本Activity对应的Window来进行分发,如果分发了,就返回true,事件循环结束 if (getWindow().superDispatchTouchEvent(ev)) { return true; } - // 如果window返回了false,就意味着所有view的ontouchevent都返回了false,那么只能是Activity来决定消费不消费 + // 如果window返回了false,就意味着所有view的ontouchevent都返回了false,那么只能是Activity来决定消费不消费 return onTouchEvent(ev); } ``` @@ -115,8 +115,8 @@ public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } ``` -是调用了`super.dispatchTouchEveent()`,而`DecorView`的父类是`FrameLayout`所以我们找到`FrameLayout.dispatchTouchEveent()`. -我们看到`FrameLayout`中没有重写`dispatchTouchEveent()`方法,所以我们再找到`FrameLayout`的父类`ViewGroup`.看`ViewGroup.dispatchTouchEveent()`实现。 +是调用了`super.dispatchTouchEvent()`,而`DecorView`的父类是`FrameLayout`所以我们找到`FrameLayout.dispatchTouchEvent()`. +我们看到`FrameLayout`中没有重写`dispatchTouchEvent()`方法,所以我们再找到`FrameLayout`的父类`ViewGroup`.看`ViewGroup.dispatchTouchEvent()`实现。 新大陆浮现了... ```java /** @@ -145,21 +145,21 @@ 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 + // 重置FLAG_DISALLOW_INTERCEPT resetTouchState(); // 如果是`Down`,那么`mFirstTouchTarget`到这里肯定是`null`.因为是新一系列手势的开始。 // `mFirstTouchTarget`是处理第一个事件的目标。 } - // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) + // 检查是否拦截该事件(如果`onInterceptTouchEvent()`返回true就拦截该事件) // Check for interception. final boolean intercepted; - // 当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,反之被ViewGroup拦截时,mFirstTouchTarget则为null + // 当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素,反之被ViewGroup拦截时,mFirstTouchTarget则为null if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 标记事件不允许被拦截, 默认是`false`, 该值可以通过`requestDisallowInterceptTouchEvent(true)`方法来设置, // 通知父`View`不要拦截该`View`上的事件。FLG_DISALLOW_INTERCEPT是在View中通过 - // reqeustDisallowInterceptTouchEvent来设置 + // reqeustDisallowInterceptTouchEvent来设置 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { // 判断该`ViewGroup`是否要拦截该事件。`onInterceptTouchEvent()`方法默认返回`false`即不拦截。 @@ -370,7 +370,7 @@ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, // or filtering. The important part is the action, not the contents. final int oldAction = event.getAction(); - // 这就是为什么时间被拦截之后,之前处理过该事件的`View`会收到`CANCEL`. + // 这就是为什么事件被拦截之后,之前处理过该事件的`View`会收到`CANCEL`. if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { @@ -439,7 +439,7 @@ private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, ``` -上面讲了`ViewGroup`的`dispatchTouchEveent()`有些地方会调用`super.dispatchTouchEveent()`,而`ViewGroup`的父类就是`View`,接下来我们看一下`View.dispatchTouchEveent()`方法: +上面讲了`ViewGroup`的`dispatchTouchEvent()`有些地方会调用`super.dispatchTouchEvent()`,而`ViewGroup`的父类就是`View`,接下来我们看一下`View.dispatchTouchEvent()`方法: ```java /** * Pass the touch screen motion event down to the target view, or this @@ -531,7 +531,7 @@ public boolean onTouchEvent(MotionEvent event) { } // 关于TouchDelegate,文档中是这样说的The delegate to handle touch events that are physically in this view - // but should be handled by another view. 就是说如果两个View, View2在View1中,View1比较大,如果我们想点击 + // but should be handled by another view. 就是说如果两个View, View2在View1中,View1比较大,如果我们想点击 // View1的时候,让View2去响应点击事件,这时候就需要使用TouchDelegate来设置。 // 简单的理解就是如果这个View有自己的时间委托处理人,就交给委托人处理。 if (mTouchDelegate != null) { @@ -542,12 +542,12 @@ public boolean onTouchEvent(MotionEvent event) { if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { - // 这个View可点击 + // 这个View可点击 switch (event.getAction()) { case MotionEvent.ACTION_UP: - // 最好先看DOWN后再看MOVE最后看UP。 + // 最好先看DOWN后再看MOVE最后看UP。 // PFLAG_PREPRESSED 表示在一个可滚动的容器中,要稍后才能确定是按下还是滚动. - // PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作. + // PFLAG_PRESSED 表示不是在一个可滚动的容器中,已经可以确定按下这一操作. boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { // 处理点击或长按事件 @@ -651,7 +651,7 @@ public boolean onTouchEvent(MotionEvent event) { // Be lenient about moving outside of buttons, 检查是否移动到View外面了。 if (!pointInView(x, y, mTouchSlop)) { - // 移动到区域外面去了,就要取消点击。 + // 移动到区域外面去了,就要取消点击。 // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { @@ -726,4 +726,4 @@ public boolean performClick() { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/SourceAnalysis/AsyncTask\350\257\246\350\247\243.md" "b/SourceAnalysis/AsyncTask\350\257\246\350\247\243.md" index 6550cc67..9ad20560 100644 --- "a/SourceAnalysis/AsyncTask\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/AsyncTask\350\257\246\350\247\243.md" @@ -1,7 +1,7 @@ AsyncTask详解 === -`AsyncTask`简单的说其实就是`Handler`和`Thread`的结合,就想下面自己写的`MyAsyncTask`一样,这就是它的基本远离,当然它并不止这么简单。 +`AsyncTask`简单的说其实就是`Handler`和`Thread`的结合,就像下面自己写的`MyAsyncTask`一样,这就是它的基本原理,当然它并不止这么简单。 - 经典版异步任务 @@ -69,9 +69,9 @@ new AsyncTask() { super.onPostExecute(result); } }.execute(); -类的构造方法中接收三个参数,这里我们不用参数就都给它传Void,new出来AsyncTask类之后然后重写这三个方法, -最后别忘了执行execute方法,其实它的内部和我们写的经典版的异步任务相同,也是里面写了一个在新的线程中去执行耗时的操作, -然后用handler发送Message对象,主线程收到这个Message之后去执行onPostExecute中的内容。 +// 类的构造方法中接收三个参数,这里我们不用参数就都给它传Void,new出来AsyncTask类之后然后重写这三个方法, +// 最后别忘了执行execute方法,其实它的内部和我们写的经典版的异步任务相同,也是里面写了一个在新的线程中去执行耗时的操作, +// 然后用handler发送Message对象,主线程收到这个Message之后去执行onPostExecute中的内容。 //AsyncTask ,params 异步任务执行(doBackgroud方法)需要的参数这个参数的实参可以由execute()方法的参数传入, @@ -120,10 +120,10 @@ new AsyncTask() { }.execute("backup.xml"); //这里传入的参数就是doInBackgound中的参数,会传入到doInBackground中 -ProgressDialog有个方法 -incrementProgressBy(int num);方法,这个方法能够让进度条自动增加,如果参数为1就是进度条累加1。 +// ProgressDialog有个方法 +// incrementProgressBy(int num);方法,这个方法能够让进度条自动增加,如果参数为1就是进度条累加1。 -可以给ProgressDialog添加一个监听dismiss的监听器。pd.setOnDismisListener(DismisListener listener);让其在取消显示后做什么事 +// 可以给ProgressDialog添加一个监听dismiss的监听器。pd.setOnDismisListener(DismisListener listener);让其在取消显示后做什么事 ``` 经过上面两部分,我们会发现`AsyncTask`太好了,他帮我们封装了`Handler`和`Thread`,当然他内部肯定会有线程池的管理,所以以后我们在开发中对于耗时的操作可以都用`AsyncTask`来搞定的。其实这种做法是错误的。今天发现公司项目中的网络请求都是用`AsyncTask`来做的(刚换的工作)。这样会有严重的问题。 @@ -132,7 +132,7 @@ incrementProgressBy(int num);方法,这个方法能够让进度条自动增加 - `AsyncTask`虽然有`cancel`方法,但是一旦执行了`doInBackground`方法,就算调用取消方法,也会执行完`doInBackground`方法中的内容才会停止。 - 串行还是并行的问题。 - 在`1.6`之前,`AsyncTask`是串行执行任务的。`1.6`的时候开始采用线程池并行处理。但是从`3.0`开始为了解决`AsyncTask`的并发问题,`AsyncTask`又采用一个现成来串行执行任务。(串行啊,每个任务10秒,五个任务,最后一个就要到50秒的时候才执行完) + 在`1.6`之前,`AsyncTask`是串行执行任务的。`1.6`的时候开始采用线程池并行处理。但是从`3.0`开始为了解决`AsyncTask`的并发问题,`AsyncTask`又采用一个线程来串行执行任务。(串行啊,每个任务10秒,五个任务,最后一个就要到50秒的时候才执行完) - 线程池的问题。 @@ -171,7 +171,7 @@ If you truly want parallel execution, you can invoke executeOnExecutor(java.util * Creates a new asynchronous task. This constructor must be invoked on the UI thread. */ public AsyncTask() { - // 初始化mWorker + // 初始化mWorker mWorker = new WorkerRunnable() { public Result call() throws Exception { // 修改该变量值 @@ -190,7 +190,7 @@ public AsyncTask() { @Override protected void done() { try { - // 执行完成后的操作 + // 执行完成后的操作 postResultIfNotInvoked(get()); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); @@ -266,14 +266,14 @@ private static class InternalHandler extends Handler { } } ``` -我们看到如果判断消息类型为`MESSAGE_POST_RESULT`时,回去执行`finish()`方法,接着看一下`result.mTask.finish()`方法的源码: +我们看到如果判断消息类型为`MESSAGE_POST_RESULT`时,会去执行`finish()`方法,接着看一下`result.mTask.finish()`方法的源码: ```java private void finish(Result result) { if (isCancelled()) { - // 如果被取消了就执行onCancelled方法,这就是为什么虽然AsyncTask可以取消,但是doInBackground方法还是会执行完的原因。 + // 如果被取消了就执行onCancelled方法,这就是为什么虽然AsyncTask可以取消,但是doInBackground方法还是会执行完的原因。 onCancelled(result); } else { - // 没被取消就执行oPostExecute方法 + // 没被取消就执行oPostExecute方法 onPostExecute(result); } mStatus = Status.FINISHED; @@ -307,7 +307,7 @@ public final AsyncTask executeOnExecutor(Executor exec mStatus = Status.RUNNING; // 看到我们熟悉的onPreExecute()方法。 onPreExecute(); - // 将参数设置给mWorker变量 + // 将参数设置给mWorker变量 mWorker.mParams = params; // 执行了Executor的execute方法并用mFuture为参数,这个exec就是上面的sDefaultExecutor exec.execute(mFuture); @@ -331,16 +331,16 @@ private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR; 从上面的部分能够看出`sDefaultExecutor`是一个`SerialExecutor`对象,好了,接下来看一下`SerialExecutor`类: ```java private static class SerialExecutor implements Executor { - // 用一个队列来管理所有的runnable。offer是把要执行的添加进来,在scheduleNext中取出来去执行。 + // 用一个队列来管理所有的runnable。offer是把要执行的添加进来,在scheduleNext中取出来去执行。 final ArrayDeque mTasks = new ArrayDeque(); Runnable mActive; public synchronized void execute(final Runnable r) { - // 终于找到了sDefaultExecutor.execute()所真正执行的部分。 + // 终于找到了sDefaultExecutor.execute()所真正执行的部分。 mTasks.offer(new Runnable() { public void run() { try { - // 就是mFuture的run方法,他会去调用mWorker.call方法,这样就会执行doInBackground方法,执行完后会把返回值用Handler发送出去 + // 就是mFuture的run方法,他会去调用mWorker.call方法,这样就会执行doInBackground方法,执行完后会把返回值用Handler发送出去 r.run(); } finally { scheduleNext(); @@ -354,7 +354,7 @@ private static class SerialExecutor implements Executor { protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { - // 去取队列中的runnable去执行,这个mActive其实就是mFuture对象。 + // 去取队列中的runnable去执行,这个mActive其实就是mFuture对象。 THREAD_POOL_EXECUTOR.execute(mActive); } } @@ -388,7 +388,7 @@ public void run() { V result; boolean ran; try { - // 他会去调用 Callable的call()方法,而上面传入的Callable参数是mWorker。所以这里就会调用mWorker的call方法。 + // 他会去调用 Callable的call()方法,而上面传入的Callable参数是mWorker。所以这里就会调用mWorker的call方法。 // 通过这里就和之前我们讲的doInBackground方法联系上了. result = c.call(); ran = true; @@ -930,4 +930,4 @@ public abstract class AsyncTask { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! 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" index 62669286..2d5da989 100644 --- "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" @@ -3,7 +3,7 @@ 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内存分析,更节省内存。 +强烈建议使用LeakCanary 2.x版本,更高效、使用更简单,而且没有任何Java代码,它当泄露引用到达5时才会发起heap dump,同时使用了全新的heap parser,减少内存占用,提升速度。只需要在dependencies中加入leakcanary的依赖即可。而且debugimplementation只在debug模式下有效,所以不用担心用户在正式环境下也会出现LeakCanary收集。而且是完全使用kotlin实现了,同时使用了[see Shark](https://square.github.io/leakcanary/shark/)来进行heap内存分析,更节省内存。 diff --git "a/SourceAnalysis/Netowork/HttpURLConnection\350\257\246\350\247\243.md" "b/SourceAnalysis/Netowork/HttpURLConnection\350\257\246\350\247\243.md" index f944cfa0..2c1e28bf 100644 --- "a/SourceAnalysis/Netowork/HttpURLConnection\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/Netowork/HttpURLConnection\350\257\246\350\247\243.md" @@ -481,10 +481,10 @@ public final class OkHttpClient implements URLStreamHandlerFactory, Cloneable { HttpURLConnection open(URL url, Proxy proxy) { String protocol = url.getProtocol(); - // 将该对象clone后设置一些其他的属性返回,里面会设置一个默认的连接池。 + // 将该对象clone后设置一些其他的属性返回,里面会设置一个默认的连接池。 OkHttpClient copy = copyWithDefaults(); copy.proxy = proxy; - // 返回了HttpURLConnectionImpl,并且把clone后的OKHttpClient对象传递进去。 + // 返回了HttpURLConnectionImpl,并且把clone后的OKHttpClient对象传递进去。 if (protocol.equals("http")) return new HttpURLConnectionImpl(url, copy); if (protocol.equals("https")) return new HttpsURLConnectionImpl(url, copy); throw new IllegalArgumentException("Unexpected protocol: " + protocol); @@ -869,7 +869,7 @@ private void connect() throws IOException { * @throws NoSuchElementException if there are no more routes to attempt. */ public Connection next(String method) throws IOException { - // 使用连接池获取Connection的地方。pool就是OkHttpClient中的连接池。 + // 使用连接池获取Connection的地方。pool就是OkHttpClient中的连接池。 // Always prefer pooled connections over new connections. for (Connection pooled; (pooled = pool.get(address)) != null; ) { // 匹配get方法,或者判断是否可读,http1.x是通过判断socket是否关闭来判断是否可读的。 @@ -902,7 +902,7 @@ public Connection next(String method) throws IOException { // tried last. return next(method); } - // 没有的话也会去创建,并把OkHttpClient中的连接池传递进去。 + // 没有的话也会去创建,并把OkHttpClient中的连接池传递进去。 return new Connection(pool, route); } ``` @@ -946,7 +946,7 @@ public final class Connection implements Closeable { out = socket.getOutputStream(); if (route.address.sslSocketFactory != null) { - // 完成TLS握手和验证 + // 完成TLS握手和验证 upgradeToTls(tunnelRequest); } else { initSourceAndSink(); @@ -1214,7 +1214,7 @@ while (true) { // 看到了吗?他内部会先去调用connect()方法 connect(); -  // 这里可能有人会有说,getOutputStream和request body有什么关系,应该是response body才对啊。 +// 这里可能有人会有说,getOutputStream和request body有什么关系,应该是response body才对啊。 // 不要弄混了啊,getOutputStream是要把post请求的数据输入给请求。 BufferedSink sink = httpEngine.getBufferedRequestBody(); if (sink == null) { @@ -1392,9 +1392,9 @@ public class ConnectionPool { static { String keepAlive = System.getProperty("http.keepAlive"); - // 存活时间 + // 存活时间 String keepAliveDuration = System.getProperty("http.keepAliveDuration"); - // 最大空闲连接数 + // 最大空闲连接数 String maxIdleConnections = System.getProperty("http.maxConnections"); long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration) : DEFAULT_KEEP_ALIVE_DURATION_MS; @@ -1527,7 +1527,7 @@ public class ConnectionPool { if (!connection.isSpdy()) { // 不是spdy连接 try { - // Platforml类对当前Android平台做了适配。 + // Platforml类对当前Android平台做了适配。 Platform.get().tagSocket(connection.getSocket()); } catch (SocketException e) { Util.closeQuietly(connection); @@ -1536,12 +1536,12 @@ public class ConnectionPool { continue; } } - // 找到可复用的Connection + // 找到可复用的Connection foundConnection = connection; break; } - // 针对spdy连接,添加到连接池中 + // 针对spdy连接,添加到连接池中 if (foundConnection != null && foundConnection.isSpdy()) { connections.addFirst(foundConnection); // Add it back after iteration. } diff --git "a/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\212).md" "b/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\212).md" index f40821cd..29bdf7c6 100644 --- "a/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\212).md" +++ "b/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\212).md" @@ -135,7 +135,7 @@ Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody con 这是什么鬼?难道官方给的示例代码有问题? 从`log`上面我们能看出来是返回的数据结构不匹配导致的,返回的是`ResponseBody`的转换器无法转换为`List`。 -`Retrofit`是一个将`API`接口转换成回调对象的类,默认情况下`Retrofit`会根绝平台提供一些默认的配置,但是它是支持配置的。 +`Retrofit`是一个将`API`接口转换成回调对象的类,默认情况下`Retrofit`会根据平台提供一些默认的配置,但是它是支持配置的。 Converters(解析数据) --- @@ -370,6 +370,10 @@ Call getUser(@Header("Authorization") String authorization) -keepattributes Exceptions ``` +------ +- [下一篇:Retrofit详解(下)](https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Netowork/Retrofit%E8%AF%A6%E8%A7%A3(%E4%B8%8B).md) + + [1]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Netowork/volley-retrofit-okhttp%E4%B9%8B%E6%88%91%E4%BB%AC%E8%AF%A5%E5%A6%82%E4%BD%95%E9%80%89%E6%8B%A9%E7%BD%91%E8%B7%AF%E6%A1%86%E6%9E%B6.md "volley-retrofit-okhttp之我们该如何选择网路框架" [2]: https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/Netowork/Volley%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md "Volley源码分析" [3]: https://github.com/CharonChui/AndroidNote/blob/master/AdavancedPart/%E6%B3%A8%E8%A7%A3%E4%BD%BF%E7%94%A8.md "注解使用" @@ -379,5 +383,4 @@ Call getUser(@Header("Authorization") String authorization) --- - 邮箱 :charon.chui@gmail.com -- Good Luck! - +- Good Luck! diff --git "a/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\213).md" "b/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\213).md" index 00e75904..a745ab21 100644 --- "a/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\213).md" +++ "b/SourceAnalysis/Netowork/Retrofit\350\257\246\350\247\243(\344\270\213).md" @@ -2,7 +2,7 @@ Retrofit详解(下) === -上一篇文件介绍了`Retrofit`的基本使用,接下来我们通过从源码的角度分析一下`Retrofit`的实现。 +上一篇文章介绍了`Retrofit`的基本使用,接下来我们通过从源码的角度分析一下`Retrofit`的实现。 首先看一下它的基本使用方法: @@ -49,7 +49,7 @@ Retrofit retrofit = new Retrofit.Builder() 这是典型的建造者模式、外观模式 -就想平时我们写的下载模块,作为一个公共的模块,我们可以对外提供一个`DownloadManager`供外界使用,而对于里面的实现我们完全可以闭门造车。 +就像平时我们写的下载模块,作为一个公共的模块,我们可以对外提供一个`DownloadManager`供外界使用,而对于里面的实现我们完全可以闭门造车。 具体`baseUrl()`、`addConverterFactory()`方法里面的具体实现就不去看了,比较简单。当然这里也用到了工厂设计模式。 @@ -85,7 +85,7 @@ public T create(final Class service) { } // 1,根据动态代理的方法去生成ServiceMethod这里动态代理的方法就是listRepos方法 ServiceMethod serviceMethod = loadServiceMethod(method); - // 2,根绝ServiceMethod和参数去生成OkHttpCall,这里args是CharonChui + // 2,根据ServiceMethod和参数去生成OkHttpCall,这里args是CharonChui OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); // 3, serviceMethod去进行处理并返回Call对象,拿到这个Call对象才能去执行网络请求。 return serviceMethod.callAdapter.adapt(okHttpCall); @@ -96,7 +96,7 @@ public T create(final Class service) { 看到`Proxy.newProxyInstance()`就明白了,这里使用了动态代理。简单的说动态代理是在你要调用某个`Class`的方法前或后,插入你想要执行的代码。那这里要代理的是什么方法? `Call> call = gitHubService.listRepos("CharonChui");`,这里就是`listRepos()`方法。 就是说在调用`listRepos()`方法时会被动态代理所拦截,然后执行`Proxy.newProxyInstance()`里面的`InvocationHandler.invoke()`中的部分。 而`invoke()`方法的三个参数分别是啥? 分别是`Object proxy`: 代理对象,`Method method`:调用的方法,就是`listRepos()`方法,`Object... args`:方法的参数,这里是`CharonChui`。 -有关动态代理介绍可以看[张孝祥老师的java1.5高新技术系列中的动态代理]() +有关动态代理介绍可以看[张孝祥老师的java1.5高新技术系列中的动态代理](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/%E5%8A%A8%E6%80%81%E4%BB%A3%E7%90%86.md) 这里就不仔细介绍动态代理了,上面的代码中又分为三部分: @@ -174,7 +174,7 @@ public T create(final Class service) { throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s", parameterType); } - // 解析对应method的注解,这里是listRepos方法的注解。 + // 解析对应method的注解,这里是listRepos方法的注解。 Annotation[] parameterAnnotations = parameterAnnotationsArray[p]; if (parameterAnnotations == null) { throw parameterError(p, "No Retrofit annotation found."); diff --git "a/SourceAnalysis/Netowork/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" "b/SourceAnalysis/Netowork/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" index 3d081fa4..8b6ca304 100644 --- "a/SourceAnalysis/Netowork/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" +++ "b/SourceAnalysis/Netowork/Volley\346\272\220\347\240\201\345\210\206\346\236\220.md" @@ -202,7 +202,7 @@ public class Volley { ``` -接着来看一上上面提到的new BasicNetwork(HttpStack)方法的实现,可以看到他内部的缓存大小是4k +接着来看一下上面提到的new BasicNetwork(HttpStack)方法的实现,可以看到他内部的缓存大小是4k ```java private static int DEFAULT_POOL_SIZE = 4096; @@ -269,12 +269,12 @@ public class RequestQueue { */ private final Set> mCurrentRequests = new HashSet>(); - // cache队列 + // cache队列 /** The cache triage queue. */ private final PriorityBlockingQueue> mCacheQueue = new PriorityBlockingQueue>(); - // 网络请求队列 + // 网络请求队列 /** The queue of requests that are actually going out to the network. */ private final PriorityBlockingQueue> mNetworkQueue = new PriorityBlockingQueue>(); 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 e4168121..1bb19a51 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" @@ -625,7 +625,7 @@ private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth " during layout: running second layout pass"); view.requestLayout(); } - // desiredWindowWidth和desiredWindowHeight是屏幕的尺寸 + // desiredWindowWidth和desiredWindowHeight是屏幕的尺寸 measureHierarchy(host, lp, mView.getContext().getResources(), desiredWindowWidth, desiredWindowHeight); mInLayout = true; @@ -975,8 +975,8 @@ private void draw(boolean fullRedrawNeeded) { } final Rect dirty = mDirty; - // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 - // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 + // 判断该Surface是否有SurfaceHolder对象,如果有则意味着该Surface是应用程序创建的,因为所有的绘制操作应该由应用程序 + // 自身去负责,于是View系统推出绘制,如果不是,才开始View绘制的内部流程。 if (mSurfaceHolder != null) { // The app owns the surface, we won't draw. dirty.setEmpty(); @@ -1018,10 +1018,10 @@ private void draw(boolean fullRedrawNeeded) { } if (!dirty.isEmpty() || mIsAnimating) { - // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 - // 因此这里需要根据不同的模式,进行不同的操作 + // Surface的底层驱动模式分为两种,一种是使用图形加速支持的Surface,俗称显卡,另一种是使用CPU及内存模拟的Surface。 + // 因此这里需要根据不同的模式,进行不同的操作 if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) { - // 硬件绘制 + // 硬件绘制 // Draw with hardware renderer. mIsAnimating = false; boolean invalidateRoot = false; @@ -1071,7 +1071,7 @@ private void draw(boolean fullRedrawNeeded) { if (animating) { mFullRedrawNeeded = true; - // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 + // 动画就是让画面动起来,如果正在动画过程中,则需要再次发起一个重绘命令,以便接着绘制,直到滚动结束。 scheduleTraversals(); } } @@ -1618,4 +1618,4 @@ MeasureSpec是View类的一个静态内部类,用来说明如何测量这个 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/SourceAnalysis/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" "b/SourceAnalysis/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" index 557f47f9..35eba6a8 100644 --- "a/SourceAnalysis/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" +++ "b/SourceAnalysis/\350\207\252\345\256\232\344\271\211View\350\257\246\350\247\243.md" @@ -1,7 +1,7 @@ 自定义View详解 === -虽然之前也分析过[View绘制过程](https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/View%E7%BB%98%E5%88%B6%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.md),但是如果让我自己集成`ViewGroup`然后自己重新`onMeasure,onLayout,onDraw`方法自定义`View`我还是会头疼。今天索性来系统的学习下。 +虽然之前也分析过[View绘制过程](https://github.com/CharonChui/AndroidNote/blob/master/SourceAnalysis/View%E7%BB%98%E5%88%B6%E8%BF%87%E7%A8%8B%E8%AF%A6%E8%A7%A3.md),但是如果让我自己继承`ViewGroup`然后自己重新`onMeasure,onLayout,onDraw`方法自定义`View`我还是会头疼。今天索性来系统的学习下。 ### `onMeasure` @@ -118,7 +118,7 @@ public static int getSize(int measureSpec) { ### `onLayout` -为了能合理的去绘制定义`View`,你需要制定它的大小。复杂的自定义`View`通常需要根据屏幕的样式和大小来进行复杂的布局计算。你不应该假设你的屏幕上的`View`的大小。即使只有一个应用使用你的自定义`View`,也需要处理不同的屏幕尺寸、屏幕密度和横屏以及竖屏下的多种比率等。 +为了能合理的去绘制定义`View`,你需要指定它的大小。复杂的自定义`View`通常需要根据屏幕的样式和大小来进行复杂的布局计算。你不应该假设你的屏幕上的`View`的大小。即使只有一个应用使用你的自定义`View`,也需要处理不同的屏幕尺寸、屏幕密度和横屏以及竖屏下的多种比率等。 虽然`View`有很多处理测量的方法,但他们中的大部分都不需要被重写。如果你的`View`不需要特别的控制它的大小,你只需要重写一个方法:`onSizeChanged()`。 @@ -448,7 +448,7 @@ public class VerticalLayout extends ViewGroup { } ``` 继承`ViewGroup`必须要重写`onLayout`方法。其实这也很好理解,因为每个`ViewGroup`的排列方式不一样,所以让子类来自己实现是最好的。 -当然畜类重写`onLayout`之外,也要重写`onMeasure`。 +当然子类重写`onLayout`之外,也要重写`onMeasure`。 代码如下,滑动手势处理的部分就不贴了。 ```java @Override @@ -461,7 +461,7 @@ public class VerticalLayout extends ViewGroup { @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { - // 就像猴子捞月一样,让他们一个个的从上往下排就好了 + // 就像猴子捞月一样,让他们一个个的从上往下排就好了 if (changed) { int childCount = getChildCount(); for (int i = 0; i < childCount; i++) { @@ -523,4 +523,4 @@ public class MetrailEditText extends EditText { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! I \ No newline at end of file +- Good Luck! I diff --git "a/VideoDevelopment/CDN\345\217\212PCDN.md" "b/VideoDevelopment/CDN\345\217\212PCDN.md" index 672f475e..bc9d8c36 100644 --- "a/VideoDevelopment/CDN\345\217\212PCDN.md" +++ "b/VideoDevelopment/CDN\345\217\212PCDN.md" @@ -43,7 +43,7 @@ PUSH是一种主动分发的技术。通常,PUSH由内容管理系统发起, 这些内容通过PUSH方式预分发到边缘Cache,可以实现有针对的内容提供。 对于PUSH分发需要考虑的主要问题是分发策略,即在什么时候分发什么内容。一般来说, 内容分发可以由CP(内容提供商)或者CDN内容管理员人工确定,也可以通过智能的方式决定, -即所谓的智能分发,它根据用户访问的统计信息,以及预定义的内容分发的规则,确定内容分发的过程PULL是一种被动的分发技术,PULL分发通常由用户请求驱动。当用户请求的内容在本地的边缘 Cache上不存在(未命中)时,Cache启动PULL方法从内容源或者其他CDN节点实时获取内容。在PULL方式下,内容的分发是按需的。 +即所谓的智能分发,它根据用户访问的统计信息,以及预定义的内容分发的规则,确定内容分发的过程。PULL是一种被动的分发技术,PULL分发通常由用户请求驱动。当用户请求的内容在本地的边缘 Cache上不存在(未命中)时,Cache启动PULL方法从内容源或者其他CDN节点实时获取内容。在PULL方式下,内容的分发是按需的。 - 内容交换机:处于用户接入集中点,可以均衡单点多个内容缓存设备的负载,并对内容进行缓存负载平衡及访问控制。 - 内容路由器:负责将用户的请求调度到适当的设备上。它是整体性的网络负载均衡技术,通过内容路由器中的重定向(DNS)机制,在多个远程POP上均衡用户的请求, 以使用户请求得到最近内容源的响应。CDN负载均衡系统实现CDN的内容路由功能。它的作用是将用户的请求导向 @@ -75,7 +75,7 @@ SLB设备构成)进行。本地内容管理的主要目标是提高内容服 归纳起来,CDN具有以下主要功能: 1. 实现跨运营商、跨地域的全网覆盖 互联不互通、区域ISP地域局限、出口带宽受限制等种种因素都造成了网站的区域性无法访问。 -CDN加速可以覆盖全球的线路,通过和运营商合作,部署IDC资源,在全国骨干节点商,合理部署CDN边缘 +CDN加速可以覆盖全球的线路,通过和运营商合作,部署IDC资源,在全国骨干节点上,合理部署CDN边缘 分发存储节点,充分利用带宽资源,平衡源站流量,这样可以节省骨干网带宽,减少带宽需求量并能 解决由于用户访问量大造成的服务器过载问题。 2. 保障网站安全 @@ -85,7 +85,7 @@ CDN的负载均衡和分布式存储技术,可以加强网站的可靠性, 当某个服务器发生意外故障时,系统将会调用其他临近的健康服务器节点进行服务,进而提供接近100%的可靠性, 这就让你的网站可以做到永不宕机。 4. 为了节约成本投入 -使用CDN加速可以实现网站的全国铺设,你根据不用考虑购买服务器与后续的托管运维,服务器之间镜像同步, +使用CDN加速可以实现网站的全国铺设,你根本不用考虑购买服务器与后续的托管运维,服务器之间镜像同步, 也不用为了管理维护技术人员而烦恼,节省了人力、精力和财力。 ### 特点 @@ -288,4 +288,4 @@ P2P用户将根据这些规则来完成P2P共享。P2P在边缘层的引入大 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/VideoDevelopment/DNS\345\217\212HTTPDNS.md" "b/VideoDevelopment/DNS\345\217\212HTTPDNS.md" index 3c52aea7..0a0323b1 100644 --- "a/VideoDevelopment/DNS\345\217\212HTTPDNS.md" +++ "b/VideoDevelopment/DNS\345\217\212HTTPDNS.md" @@ -150,8 +150,8 @@ DNS权威服务器保存着域名空间中部分区域的数据。如果DNS服 - A:地址记录(Address),返回域名指向的IP地址。 - NS:域名服务器记录(Name Server),返回保存下一级域名信息的服务器地址。该记录只能设置为域名,不能设置为IP地址。 - MX:邮件记录(Mail eXchange),返回接收电子邮件的服务器地址。 -- CNAME:规范名称记录(Canonical Name),返回另一个域名,即当前查询的域名是另一个域名的跳转,详见下文。 -- PTR:逆向查询记录(Pointer Record),只用于从IP地址查询域名,详见下文。 +- CNAME:规范名称记录(Canonical Name),返回另一个域名,即当前查询的域名是另一个域名的跳转。 +- PTR:逆向查询记录(Pointer Record),只用于从IP地址查询域名。 一般来说,为了服务的安全可靠,至少应该有两条NS记录,而A记录和MX记录也可以有多条,这样就提供了服务的冗余性,防止出现单点失败。 @@ -254,4 +254,4 @@ Accept: */* --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From de97a2fcaf80b626bf6e0945bebc3f12ecae1daf Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 20 Mar 2023 14:42:24 +0800 Subject: [PATCH 013/128] update --- .../AudioTrack\347\256\200\344\273\213.md" | 2 +- ...47\350\203\275\344\274\230\345\214\226.md" | 50 ++++--------------- 2 files changed, 12 insertions(+), 40 deletions(-) diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" index 1a4f8e30..ff862a6c 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/AudioTrack\347\256\200\344\273\213.md" @@ -4,7 +4,7 @@ AudioTrack简介 Android系统提供了三种播放音频文件的方式: - SoundPoll:适合播放短促且对反应速度要求比较高的情况(如游戏音效、按键声等) -- AudioTrack:只支持播放解码后的PCM流,如果是文件的话只致辞WAV格式的音频文件,因为WAV格式的音频文件大部分都是PCM流,AudioTrack不创建解码器,所以只能播放不需要解码的WAV文件,但是CPU占用率低,内存消耗也比较小。因此如果是播放比较短时间的WAV音频文件,建议使用AudioTrack。 +- AudioTrack:只支持播放解码后的PCM流,如果是文件的话只支持WAV格式的音频文件,因为WAV格式的音频文件大部分都是PCM流,AudioTrack不创建解码器,所以只能播放不需要解码的WAV文件,但是CPU占用率低,内存消耗也比较小。因此如果是播放比较短时间的WAV音频文件,建议使用AudioTrack。 - MediaPlayer:适合比较长且时间要求不那么高的情况,支持多种文件格式,如MP3、WAV、AAC等。其实MediaPlayer是基于AudioTrack的封装,内部也是使用AudioTrack,MediaPlayer在framework层也实例化了AudioTrack,MediaPlayer在framework层进行解码后,生成PCM流,然后代理委托给AudioTrack,最后AudioTrack传递给AudioFlinger进行混音,然后才传递给硬件播放。 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" index b64ab96e..a77a4510 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\346\222\255\346\224\276\345\231\250\346\200\247\350\203\275\344\274\230\345\214\226.md" @@ -25,7 +25,7 @@ - 相比H264,编码复杂度提升4倍,更消耗系统资源 - 编码时间更长 -通过软解可以支持,但是CPU会占用较高,而且发热和好点。 +通过软解可以支持,但是CPU会占用较高,而且发热和耗电。 对解码能力进行评估,来通过机型打分控制是否使用H265以及是用360p还是480p。 但是H265全部切换成本较高,牵扯到转码成本以及存储成本,所以可以只对热点的视频去进行编码,这样少量的视频就可以带来可观的效果。 @@ -68,15 +68,15 @@ ### 预取策略 -1,修改AndroidVideoCache进行预加载 - 2,线程池并发缓存并控制视频缓存优先级(线程池线程数为3,先加入的先缓存),一次预加载8个视频,item创建时开始预加载,item销毁时,取消预加载 - 3,等待下页第一个视频预加载完成,才会进入下一页视频,保证滑到的视频都是可以立马观看的。(一页视频为8个) - 4,当前视频开始播放之后才会进行预加载 - 5,区分快滑慢滑两种模式,快滑时取消当前所有预加载,慢滑不取消,因为快滑大概率滑到一个未预加载的视频,慢滑大概率滑到一个已经预加载的视频,保证滑到视频尽快播放。 - 6,当视频播放后,根据滑动方向,会将取消的预加载进行恢复,正向滑动就恢复当前之后之后的预加载任务,反选滑动就恢复当前视频之前的预加载任务。 - 7,限制网速,当时视频还没播放的时候,会限制预下载的网速(慢滑不会取消预加载),通过让下载线程sleep来限制网速。 - 8,ijkplayer起播参数配置。 - 9,视频压缩和处理(让视频参数都位于视频首部)。 +1. 修改AndroidVideoCache进行预加载 +2. 线程池并发缓存并控制视频缓存优先级(线程池线程数为3,先加入的先缓存),一次预加载8个视频,item创建时开始预加载,item销毁时,取消预加载 +3. 等待下页第一个视频预加载完成,才会进入下一页视频,保证滑到的视频都是可以立马观看的。(一页视频为8个) +4. 当前视频开始播放之后才会进行预加载 +5. 区分快滑慢滑两种模式,快滑时取消当前所有预加载,慢滑不取消,因为快滑大概率滑到一个未预加载的视频,慢滑大概率滑到一个已经预加载的视频,保证滑到视频尽快播放。 +6. 当视频播放后,根据滑动方向,会将取消的预加载进行恢复,正向滑动就恢复当前之后之后的预加载任务,反选滑动就恢复当前视频之前的预加载任务。 +7. 限制网速,当时视频还没播放的时候,会限制预下载的网速(慢滑不会取消预加载),通过让下载线程sleep来限制网速。 +8. ijkplayer起播参数配置。 +9. 视频压缩和处理(让视频参数都位于视频首部)。 @@ -135,9 +135,6 @@ Conviva在2011年的分享中提到以下观点,缓冲次数始终是最主要 不断优化表现直接与收入关联的主要原因是,收入统计通常计出多门且较为滞后,可采用的替代指标包括注册用户数、活跃用户数、视频观察市场、观看次数、播放占比、付费率、留存率、分享率、满意度等。 - - - ### 监控 监控上报,肯定是不可缺少的,这是一个成熟的项目必备的要素。 @@ -152,32 +149,7 @@ Conviva在2011年的分享中提到以下观点,缓冲次数始终是最主要 - - - - - - - - - - - - - - - - - - - - - - - - - --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 8d4a19c8a88bebe59031cbf025d6e3b6c833b517 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 20 Mar 2023 16:35:35 +0800 Subject: [PATCH 014/128] update --- ...74\345\274\217\350\257\246\350\247\243.md" | 4 +-- .../fMP4 vs ts.md" | 15 ++++------- ...01\350\243\205\346\240\274\345\274\217.md" | 25 ++++++++----------- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" index 2778296c..6e36462a 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/MP4\346\240\274\345\274\217\350\257\246\350\247\243.md" @@ -45,7 +45,7 @@ moov和mdat这两个Atom顺序不固定,一般来说都是moov在前。 #### ftyp(File Type Box) -声明该违建为MP4文件的相关信息,必须在第一个,而且只有一个 +声明该文件为MP4文件的相关信息,必须在第一个,而且只有一个 #### moov(movie box) @@ -102,4 +102,4 @@ mvhd定义了整个movie的特性,通常包含媒体无关的信息,例如 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" index 1e1017b1..1cb7d9c6 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/fMP4 vs ts.md" @@ -12,22 +12,18 @@ MP4,全称MPEG-4 Part 14,是一种使用MPEG-4的多媒体电脑档案格 ### 媒体数据与元数据的分离 -在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。(TODO:介绍ts中的metadata信息怎么存储)。 +在mp4格式中,元数据可以和媒体数据很好地分开存储,后者都在mdat box中,而在ts中,诸多es流和header/metadata信息是复用在一起的。 元数据的分离允许我们在streaming中先读取各个流的元数据,知道他们的媒体内容的属性(比如不同的视频质量、不同的语言等),从而可以更好地在不同的media data之间做自适应切换。 当然,在更实际的应用场景中,比如在dash协议中会直接把这些元数据信息写在mpd中,player可以只读一个mpd就知道各个媒体数据的属性 ### 各个Track独立存储 -在fmp4中,不仅媒体数据和metadata相互独立的存储,音视频track的数据也可以分开存储,这里的“分开”已经不仅仅局限于box层面的分开,而是真的可以分开存储于不同的目录。在这种情况下,player只需要读一个记录了它们各自存储位置的manifest,即可去对应的位置download它们的分片,只要做好音视频分片之间的同步工作,就可以正常的播放。 -举个例子,下面是一个dash中常见的mpd: -![这里写图片描述](https://www.itdaan.com/imgs/5/4/8/5/76/090fc502224c3e083760a2300d06e866.jpe) -在这个mpd中我们看到视频的分片都是存储在video/1/这个目录下,而音频分片都存储在audio/und/mp4a/1这个目录下,而player还是可以将它们拼接到一起完成播放。 -相对地,在streaming TS流时,音视频往往是复用在一起的,以HLS这个应用场景为例的话,server端一定还需要提前将TS切片做好,这样就会带来几个问题: +在fmp4中,不仅媒体数据和metadata相互独立的存储,音视频track的数据也可以分开存储,这里的“分开”已经不仅仅局限于box层面的分开,而是真的可以分开存储于不同的目录。在这种情况下,player只需要读一个记录了它们各自存储位置的manifest,即可去对应的位置download它们的分片,只要做好音视频分片之间的同步工作,就可以正常的播放。 +相对地,在streaming TS流时,音视频往往是复用在一起的,以HLS这个应用场景为例的话,server端一定还需要提前将TS切片做好,这样就会带来几个问题: **1. 媒体文件存储成本和媒资管理成本增加** 假设server端将video track编码为三个质量级别V1, V2, V3,audio track也被编码为三个质量级别A1, A2, A3,那么如果利用fmp4格式的特性,我们只需要存储这6份媒体文件,player在播放时再自主组合不同质量级别的音视频track即可。而对于TS,则不得不提前将3x3=9种不同的音视频复用情况对应的媒体文件都存储到server端,平白无故多出三份文件的存储成本。实际中,因为要考虑到大屏端、小屏端、移动端、桌面端、不同语言、不同字幕等各种情况,使用TS而造成的冗余成本将更加可观。同时,存储文件的增加也意味着媒资管理成本的增加。这也是包括Netflix在内的一些公司选择使用fmp4做streaming格式的原因。 **2. manifest文件更加复杂** -fmp4格式的特性可以确保每一个单独的媒体分片都是可解密可解码的(当然player需要先从moov box中读到它们的编解码等信息),这意味着server端甚至根本不需要真的存储一大堆分片,player可以直接利用byte range request技术从一个大文件中准确地读出一个分片对应的media data,这也使得对应manifest(mpd)文件可以更加简洁,如下: -![这里写图片描述](https://www.itdaan.com/imgs/2/0/5/0/35/0e7a66325da9f454b13b32692004c33d.jpe) +fmp4格式的特性可以确保每一个单独的媒体分片都是可解密可解码的(当然player需要先从moov box中读到它们的编解码等信息),这意味着server端甚至根本不需要真的存储一大堆分片,player可以直接利用byte range request技术从一个大文件中准确地读出一个分片对应的media data,这也使得对应manifest(mpd)文件可以更加简洁。 针对不同语言的音频,都只需要存一个大文件就够了。 相对地,在streaming TS流时,不得不在manifest(m3u8)文件中把成百上千个ts分片文件全都老老实实地记录下来。 **3.服务器的cache效率会降低** @@ -43,7 +39,6 @@ fmp4格式的特性可以确保每一个单独的媒体分片都是可解密可 无缝码流切换实现的关键在于:当第一个码流播放结束时,也就是发生切换的时间,第二个码流一定要以关键帧开始播放。在streaming TS流时,因为不能保证每一个TS chunk一定以关键帧开始,做码流切换时就意味着要同时download两个码流的相应分片,同时解析两个码流,然后找到关键帧对应的位置,才能切换。同时下载、解析两个码流的媒体内容对网络带宽以及设备性能都形成了挑战。而且有意思的是,如果当前网络环境不佳,player想要切换到低码率码流,结果还要在本来就不好的网络环境下同时进行两个码流的下载,可谓是雪上加霜。 而在fmp4中,除了保证各个分片一定以IDR帧开始外,还能保证不同码流的分片之间在时间线上是对齐的。而且streaming fmp4流时因为不要求音视频复用存储,也就意味着视频和音频的同步点可以不一样,视频可以全都以GOP边界作为同步点,音频可以都以sync frame作为同步点,这都使得无缝码流切换更简单。 -TODO:介绍在分片中间进行切换的情况 ### 与DRM的集成 @@ -67,4 +62,4 @@ CENC使用的就是fMP4格式,这是利用了fMP4中音视频可以不复用 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! 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 c98f5a96..f60f3b09 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" @@ -11,20 +11,15 @@ 常用视频封装格式及对应的文件格式: -| 视频封装格式 | 视频文件格式 | - -| ------ | ------ | - -| AVI(Audio Video Interleave) | AVI | -| WMV(Windows Media Video) | WMV | -| MPEG分为MPEG-1,MPEG-2,MPEG-4 | MPG MPEG VOB DAT 3GP MP4 | -| Matroska | MKV | -| Real Video | RM RMVB | -| QuickTime File Format | MOV | -| Flash Video | FLV | - - - + | 视频封装格式 | 视频文件格式 | + | ------ | ------ | + | AVI(Audio Video Interleave) | AVI | + | WMV(Windows Media Video) | WMV | + | MPEG分为MPEG-1,MPEG-2,MPEG-4 | MPG MPEG VOB DAT 3GP MP4 | + | Matroska | MKV | + | Real Video | RM RMVB | + | QuickTime File Format | MOV | + | Flash Video | FLV | ## 音频编码格式 @@ -124,4 +119,4 @@ H.264最大的优势是具有很高的数据压缩比率,在同等图像质量 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 603158f511b0e87f9596578503b965f24d6e7cde Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 21 Mar 2023 09:33:33 +0800 Subject: [PATCH 015/128] update --- ...73\347\273\237\347\256\200\344\273\213.md" | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 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 7014d941..fa3aa962 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" @@ -1,20 +1,42 @@ # 1.操作系统简介 -操作系统(Operating System, 简称OS)是管理和控制计算机硬件与软件资源的计算机程序。 -它是一种由引导程序(bootloader)启动并管理计算机中所有程序生命周期的系统程序。任何其他软件都必须在操作系统的支持下才能运行,操作系统能有效组织和管理系统中的各种软、硬件资源,合理组织计算机系统的工作流程并控制程序的执行,为用户提供一个良好的操作环境。 目前比较为人所知的操作系统有Microsoft的Windows系统、Apple的Mac及以Linux为内核的各种Linux发行版(Centos/Ubuntu等)。 现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口及各种输入\输出设备构成。 - - -操作系统这个术语听上去稀松平常,并不给人任何兴奋的感觉,甚至有点俗气。原因在于中文的“操作”这个词:提到操作员,通常让人想起操作车床、磨床和起重机的穿着油腻工作服的工人,这自然让人兴奋不起来。将英文的Operating翻译为中文的“操作”,是因为翻译的人没有理解英文Operating Systems(OS,操作系统)这个名字所蕴涵的精髓。那么英文的Operating Systems意味着什么呢?各位见过手术过程吗?在手术室里,主刀大夫称为Operating Surgeon。在整个手术过程中,主刀大夫具有至高无上的权威:他说要打麻药,麻醉师便要赶紧打麻药;他说需要手术钳,助理大夫就赶忙递给他手术钳;他说需要止血,护士就要忙不迭地拿止血药棉来止血。整个手术最关键的部分,切开皮肤、拿掉器官、安装移植器官等均由主刀大夫完成。当然,主刀大夫有时候也会将某些任务,如缝合创口,交给助理大夫或护士来做,但整个手术的过程皆由其主控。一句话,Operating Surgeon就是掌控整个手术过程、具有精湛技术和敏锐判断力的医师。 - -引申至非医学领域,Operating Person意思是操刀手,就是掌控事情的人。再将Person这个词换成System,则Operating Systems指的就是掌控局势的一种系统。也就是说计算机里面的一切事情均由Operating Systems来掌控。那么,我们现在面临两个问题:第一个问题是,操作系统到底是什么东西?第二个问题是,操作系统到底操控什么事情?我们先回答第一个问题。既然操作系统是掌控计算机局势的一个系统,自然很重要。但这个说法并不能帮助读者理解操作系统,也无法形成有形的概念。如果我们换个说法:操作系统是介于计算机和应用软件之间的一个软件系统,则概念就具体多了。从这个定义出发,我们知道操作系统的上面和下面都有别的对象存在:下面是硬件平台,上面是应用软件,现在回答第二个问题。我们知道操作系统代表的是掌控事情的系统,掌控什么事情呢?当然是计算机上或计算机里发生的一切事情。最原始的计算机并没有操作系统,而是直接由人来掌控事情,即所谓的单一控制终端、单一操作员模式。但是随着计算机复杂性的增长,人已经不能胜任直接掌控计算机了。于是我们编写出操作系统这个“软件”来掌控计算机,将人类从日益复杂的掌控任务中解脱出来。这个掌控有着多层深远的意义。首先,由于计算机的功能和复杂性不断发生变化(趋向更加复杂),操作系统所掌控的事情也就越来越多,越来越复杂。同时,操作系统本身能够使用的资源也不断增多(如内存容量)。这是早期驱动操作系统不断改善的根本原因。其次,既然操作系统是专门掌控计算机的,那么计算机上发生的所有事情自然需要操作系统的知晓和许可,未经操作系统同意的任何事情均视为非法的,也就是病毒和入侵攻击所试图运作的事情。作为操作系统的设计人员,我们当然要确保计算机不发生任何我们不知情或不同意的事情。但是人的能力是有限的,人的思维也是有缺陷的,我们设计出的系统自然不会十全十美,也会有缺陷,这就给了攻击者可乘之机。操作系统设计人员和攻击者之间的博弈是当前驱动操作系统改善的一个重要动力。再次,掌控事情的水平有高低之分,有效率不同之分。就像手术大夫之间也有水平高低之分。为了更好地掌控事情,同时也为了更好地满足人类永不知足的各种越来越苛刻的要求,操作系统自然需要不断改善。这种改善在过去、现在和将来都会继续下去的。最后,我们可以给操作系统下定义了:操作系统是一个软件系统;操作系统使计算机变得好用(将人类从繁琐、复杂的对机器掌控的任务中解脱);操作系统使计算机运作变得有序(操作系统掌控计算机上所有的事情)。总结起来就是:操作系统是掌控计算机上所有事情的软件系统。 +操作系统(Operating System, 简称OS)是管理和控制计算机硬件与软件资源的计算机程序。 +它是一种由引导程序(bootloader)启动并管理计算机中所有程序生命周期的系统程序。 +任何其他软件都必须在操作系统的支持下才能运行,操作系统能有效组织和管理系统中的各种软、硬件资源,合理组织计算机系统的工作流程并控制程序的执行,为用户提供一个良好的操作环境。 + 目前比较为人所知的操作系统有Microsoft的Windows系统、Apple的Mac及以Linux为内核的各种Linux发行版(Centos/Ubuntu等)。 + 现代计算机系统由一个或多个处理器、主存、打印机、键盘、鼠标、显示器、网络接口及各种输入\输出设备构成。 + + +操作系统这个术语听上去稀松平常,并不给人任何兴奋的感觉,甚至有点俗气。 +原因在于中文的“操作”这个词:提到操作员,通常让人想起操作车床、磨床和起重机的穿着油腻工作服的工人,这自然让人兴奋不起来。 +将英文的Operating翻译为中文的“操作”,是因为翻译的人没有理解英文Operating Systems(OS,操作系统)这个名字所蕴涵的精髓。 +那么英文的Operating Systems意味着什么呢?各位见过手术过程吗?在手术室里,主刀大夫称为Operating Surgeon。在整个手术过程中,主刀大夫具有至高无上的权威:他说要打麻药,麻醉师便要赶紧打麻药;他说需要手术钳,助理大夫就赶忙递给他手术钳;他说需要止血,护士就要忙不迭地拿止血药棉来止血。整个手术最关键的部分,切开皮肤、拿掉器官、安装移植器官等均由主刀大夫完成。当然,主刀大夫有时候也会将某些任务,如缝合创口,交给助理大夫或护士来做,但整个手术的过程皆由其主控。 +一句话,Operating Surgeon就是掌控整个手术过程、具有精湛技术和敏锐判断力的医师。 + +引申至非医学领域,Operating Person意思是操刀手,就是掌控事情的人。再将Person这个词换成System,则Operating Systems指的就是掌控局势的一种系统。也就是说计算机里面的一切事情均由Operating Systems来掌控。 +那么,我们现在面临两个问题: +第一个问题是,操作系统到底是什么东西? +第二个问题是,操作系统到底操控什么事情? +我们先回答第一个问题。既然操作系统是掌控计算机局势的一个系统,自然很重要。但这个说法并不能帮助读者理解操作系统,也无法形成有形的概念。 +如果我们换个说法:操作系统是介于计算机和应用软件之间的一个软件系统,则概念就具体多了。 +从这个定义出发,我们知道操作系统的上面和下面都有别的对象存在:下面是硬件平台,上面是应用软件,现在回答第二个问题。我们知道操作系统代表的是掌控事情的系统,掌控什么事情呢?当然是计算机上或计算机里发生的一切事情。最原始的计算机并没有操作系统,而是直接由人来掌控事情,即所谓的单一控制终端、单一操作员模式。但是随着计算机复杂性的增长,人已经不能胜任直接掌控计算机了。于是我们编写出操作系统这个“软件”来掌控计算机,将人类从日益复杂的掌控任务中解脱出来。 +这个掌控有着多层深远的意义。首先,由于计算机的功能和复杂性不断发生变化(趋向更加复杂),操作系统所掌控的事情也就越来越多,越来越复杂。同时,操作系统本身能够使用的资源也不断增多(如内存容量)。 +这是早期驱动操作系统不断改善的根本原因。其次,既然操作系统是专门掌控计算机的,那么计算机上发生的所有事情自然需要操作系统的知晓和许可,未经操作系统同意的任何事情均视为非法的,也就是病毒和入侵攻击所试图运作的事情。作为操作系统的设计人员,我们当然要确保计算机不发生任何我们不知情或不同意的事情。但是人的能力是有限的,人的思维也是有缺陷的,我们设计出的系统自然不会十全十美,也会有缺陷,这就给了攻击者可乘之机。 +操作系统设计人员和攻击者之间的博弈是当前驱动操作系统改善的一个重要动力。再次,掌控事情的水平有高低之分,有效率不同之分。就像手术大夫之间也有水平高低之分。为了更好地掌控事情,同时也为了更好地满足人类永不知足的各种越来越苛刻的要求,操作系统自然需要不断改善。这种改善在过去、现在和将来都会继续下去的。 +最后,我们可以给操作系统下定义了: +操作系统是一个软件系统; +操作系统使计算机变得好用(将人类从繁琐、复杂的对机器掌控的任务中解脱); +操作系统使计算机运作变得有序(操作系统掌控计算机上所有的事情)。 +总结起来就是:操作系统是掌控计算机上所有事情的软件系统。 作用:它可以帮我们管理计算机的各种资源,协助我们完成各种复杂繁琐的任务。 ### 操作系统三要素: -- 虚拟化(virtualization) - 操作系统将物理(physical)资源(如处理器、内存或磁盘)转换为更通用、更强大且更易于使用的虚拟形式。因此,我们有时将操作系统称为虚拟机(virtual machine)。 +- 虚拟化(virtualization) + 操作系统将物理(physical)资源(如处理器、内存或磁盘)转换为更通用、更强大且更易于使用的虚拟形式。因此,我们有时将操作系统称为虚拟机(virtual machine)。 + 当然,为了让用户可以告诉操作系统做什么,从而利用虚拟机的功能(如运行程序、分配内存或访问文件),操作系统还提供了一些接口(API),供你调用。实际上,典型的操作系统会提供几百个系统调用(system call),让应用程序调用。由于操作系统提供这些调用来运行程序、访问内存和设备,并进行其他相关操作,我们有时也会说操作系统为应用程序提供了一个标准库(standard library)。 最后,因为虚拟化让许多程序运行(从而共享CPU),让许多程序可以同时访问自己的指令和数据(从而共享内存),让许多程序访问设备(从而共享磁盘等),所以操作系统有时被称为资源管理器(resource manager)。每个CPU、内存和磁盘都是系统的资源(resource),因此操作系统扮演的主要角色就是管理(manage)这些资源,以做到高效或公平,或者实际上考虑其他许多可能的目标。 From 3b3b828252af6265ca07379060ce9f15a5fa6731 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 24 Apr 2023 10:41:57 +0800 Subject: [PATCH 016/128] update --- ...47\273\237\347\256\200\344\273\213.md.swp" | Bin 0 -> 49152 bytes ...43\344\270\216\347\241\254\350\247\243.md" | 14 ++--- .../1.FFmpeg\347\256\200\344\273\213.md" | 23 +++++---- ...50\345\221\275\344\273\244\350\241\214.md" | 6 +-- .../\345\205\263\351\224\256\345\270\247.md" | 48 +++--------------- .../HLS.md" | 4 +- .../HTTP FLV.md" | 10 +--- ...05\345\256\271\346\200\273\347\273\223.md" | 4 +- 8 files changed, 37 insertions(+), 72 deletions(-) create mode 100644 "OperatingSystem/.1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md.swp" diff --git "a/OperatingSystem/.1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md.swp" "b/OperatingSystem/.1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md.swp" new file mode 100644 index 0000000000000000000000000000000000000000..60c2cbee68d88f536c7213cd55151f2b89666cdb GIT binary patch literal 49152 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 literal 0 HcmV?d00001 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" index bbeff659..94721c34 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/\350\247\206\351\242\221\350\247\243\347\240\201\344\271\213\350\275\257\350\247\243\344\270\216\347\241\254\350\247\243.md" @@ -8,19 +8,19 @@ 既然有这两种不同的解码方式,我们在开发中该如何进行选择?哪个更好? - 硬解优缺点: - 显卡核心GPU拥有独特的计算方法,解码效率非常高,而且充当解码核心的模块成本并不高。这样不但能够减轻CPU的负担,还有着低功耗、发热少等特点。但是由于硬解码起步比较晚, - 软件和驱动对其的支持度低。硬解码内置有什么样的模块就能够解码什么样的视频,面对网络上杂乱无章的视频编码格式,不可能做到完全兼容同。此外,硬解码的滤镜、字母、画质增强方面都做的十分不足。 + 显卡核心GPU拥有独特的计算方法,解码效率非常高,而且充当解码核心的模块成本并不高。这样不但能够减轻CPU的负担,还有着低功耗、发热少等特点。但是由于硬解码起步比较晚, + 软件和驱动对其的支持度低。硬解码内置有什么样的模块就能够解码什么样的视频,面对网络上杂乱无章的视频编码格式,不可能做到完全兼容同。此外,硬解码的滤镜、字幕、画质增强方面都做的十分不足。 优点:低功耗、发热少、效率高。 - 缺点:视频兼容性差、支持度低。 + 缺点:视频兼容性差、支持度低。 - 软解优缺点: 软解码技术的解码过程中,需要对大量的视频信息进行运算,对CPU性能的要求非常高。尤其是对高清晰度大码率的视频来说,巨大的运算量就会造成转换效率低、发热量大等问题。 - 但是由于软解码的过程中不需要复杂的硬件支持,兼容性非常高。即使是新出的视频编码格式,只要安装好相应的解码器文件,就能顺利播放。而且软解码拥有丰富的滤镜、字幕、画面处理优化等效果, - 如果`CPU`足够强悍的话,能够实现更加出色的画面效果。 + 但是由于软解码的过程中不需要复杂的硬件支持,兼容性非常高。即使是新出的视频编码格式,只要安装好相应的解码器文件,就能顺利播放。而且软解码拥有丰富的滤镜、字幕、画面处理优化等效果, + 如果`CPU`足够强悍的话,能够实现更加出色的画面效果。 优点: 兼容强、全解码、效果好 - 缺点: 对`CPU`要求高、效率低、发热大 + 缺点: 对`CPU`要求高、效率低、发热大 关于软解与硬解究竟哪个更好的问题一直是争论的热点,其实我倒是感觉没有好坏之分,各自有各自的优缺点和使用条件,根据需要去选择才是最合适的。 @@ -31,4 +31,4 @@ --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" "b/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" index 16e0c81b..86536a43 100644 --- "a/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/FFmpeg/1.FFmpeg\347\256\200\344\273\213.md" @@ -4,19 +4,19 @@ FFmpeg是一个开源免费跨平台的视频和音频流方案,属于自由软件,采用LGPL或GPL许可证(依据你选择的组件)。它提供了录制、转换以及流化音视频的完整解决方案。它包含了非常先进的音频/视频编解码库libavcodec,为了保证高可移植性和编解码质量,libavcodec里很多codec都是从头开发的。 FFmpeg框架的基本组成包含: -- AVFormat +- AVFormat AVFormat中实现了目前多媒体领域中的绝大多数媒体封装格式,包括封装和解封装,如MP4、FLV、KV、TS等文件封装格式,RTMP、RTSP、MMS、HLS等网络协议封装格式。FFmpeg是否支持某种媒体封装格式,取决于编译时是否包含了该格式的封装库。根据实际需求,可进行媒体封装格式的扩展,增加自己定制的封装格式,即在AVFormat中增加自己的封装处理模块。 -- AVCodec +- AVCodec AVCodec中实现了目前多媒体领域绝大多数常用的编解码格式,既支持编码,也支持解码。AVCodec除了支持MPEG4、AAC、MJPEG等自带的媒体编解码格式之外,还支持第三方的编解码器,如H.264(AVC)编码,需要使用x264编码器;H.265(HEVC)编码,需要使用x265编码器;MP3(mp3lame)编码,需要使用libmp3lame编码器。如果希望增加自己的编码格式,或者硬件编解码,则需要在AVCodec中增加相应的编解码模块 -- AVFilter +- AVFilter AVFilter库提供了一个通用的音频、视频、字幕等滤镜处理框架。在AVFilter中,滤镜框架可以有多个输入和多个输出。 -- AVDevice +- AVDevice 读取电脑(或者其他设备上)的多媒体设备的数据 或者输出数据到指定的多媒体设备上; -- AVUtil +- AVUtil 包含一些公共的工具函数,包括随机数生成、数据结构、核心多媒体工具等; -- swscale +- swscale swscale模块提供了高级别的图像转换API,例如它允许进行图像缩放和像素格式转换,常见于将图像从1080p转换成720p或者480p等的缩放,或者将图像数据从YUV420P转换成YUYV,或者YUV转RGB等图像格式转换。 -- swresample +- swresample 用于音频采样采样数据(PCM)转换的库,提供了高级别的音频重采样API。 @@ -45,8 +45,11 @@ ffmpeg的主要工作流程相对比较简单,具体如下: ffmpeg整体处理的工作流程与步骤为: -读取文件 → 解封装 → 解码 → 转换参数 → 新编码 → 封装 → 写入文件 -ffmpeg首先读取输入源;然后通过Demuxer将音视频包进行解封装,这个动作通过调用libavformat中的接口即可实现;接下来通过Decoder进行解码,将音视频通过Decoder解包成为YVU或者PCM这样的数据,Decoder通过libavcodec中的接口即可实现;然后通过Encoder将对应的数据进行编码,编码可以通过libavcodec中的接口来实现;接下来将编码后的音视频数据包通过Muxer进行封装,Muxer封装通过libavformat中的接口即可实现,输出成为输出流。 +读取文件 → 解封装 → 解码 → 转换参数 → 新编码 → 封装 → 写入文件 +ffmpeg首先读取输入源; +然后通过Demuxer将音视频包进行解封装,这个动作通过调用libavformat中的接口即可实现; +接下来通过Decoder进行解码,将音视频通过Decoder解包成为YVU或者PCM这样的数据,Decoder通过libavcodec中的接口即可实现; +然后通过Encoder将对应的数据进行编码,编码可以通过libavcodec中的接口来实现; 接下来将编码后的音视频数据包通过Muxer进行封装,Muxer封装通过libavformat中的接口即可实现,输出成为输出流。 @@ -54,4 +57,4 @@ ffmpeg首先读取输入源;然后通过Demuxer将音视频包进行解封装 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" index 4ee12b4e..4b0fe7aa 100644 --- "a/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" +++ "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" @@ -45,8 +45,8 @@ ffmpeg –s w*h –pix_fmt yuv420p –i input.yuv –vcodec mpeg4 output.avi ``` 常用参数说明: -主要参数:-i 设定输入流 -f 设定输出格式 -ss 开始时间 -视频参数:-b 设定视频流量,默认为200Kbit/s -r 设定帧速率,默认为25 -s 设定画面的宽与高 -aspect 设定画面的比例 -vn 不处理视频 -vcodec 设定视频编解码器,未设定时则使用与输入流相同的编解码器 +主要参数:-i 设定输入流 -f 设定输出格式 -ss 开始时间 +视频参数:-b 设定视频流量,默认为200Kbit/s -r 设定帧速率,默认为25 -s 设定画面的宽与高 -aspect 设定画面的比例 -vn 不处理视频 -vcodec 设定视频编解码器,未设定时则使用与输入流相同的编解码器 音频参数:-ar 设定采样率 -ac 设定声音的Channel数 -acodec 设定声音编解码器,未设定时则使用与输入流相同的编解码器 -an 不处理音频 @@ -498,4 +498,4 @@ stream字段说明: --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! 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 a3884aec..d4ad6c16 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" @@ -5,7 +5,7 @@ ### I、P、B帧 -在H.264协议中定义了3中帧,完整编码的帧叫I帧、参考之前的I帧生成的只对差异部分进行编码的帧叫P帧、还有一种参考前后的帧进行编码的帧叫B帧。 +在H.264协议中定义了3种帧,完整编码的帧叫I帧、参考之前的I帧生成的只对差异部分进行编码的帧叫P帧、还有一种参考前后的帧进行编码的帧叫B帧。 - I帧(Intra coded picture):帧内编码图像帧简称关键帧,这一帧画面的完整保留,解码时只需要本帧数据就可以完成.I帧通常是每个GOP(MPEG所使用的一种视频压缩技术)的第一个,GOP就是指两个I帧之间的距离。 @@ -19,7 +19,7 @@ ### GOP -这3中帧用于表示传输的视频画面。在H.264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束,中间部分也被称为一个GOP。 +这3种帧用于表示传输的视频画面。在H.264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束,中间部分也被称为一个GOP。 GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。 编码器将多张图像进行编码后生产成一段一段的 GOP ,解码器在播放时则是读取一段一段的GOP进行解码后读取画面再渲染显示。 @@ -28,15 +28,13 @@ GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组 ### IDR +IDR(Instantaneous Decoding Refresh)是H.264视频编码中的一种特殊的I帧。它包含了所有的视频序列信息,并且通过强制性地要求解码器重新初始化来保证解码正确性。IDR通常用于视频流中断或丢失时,重新恢复视频流的一种方式。 + +总之,I帧是视频序列中的一种关键帧,它能够独立解码,而IDR是一种特殊的I帧,具有重新初始化解码器的特殊作用。 + 一个序列(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帧作为被参考的帧。 - - - + 那么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 参数进行更新。 @@ -55,8 +53,6 @@ I帧 : P帧 : B帧 = 7 :20 : 50 P帧和B帧极大的节省了数据量,节省出来的空间可以用来多保存一些I帧,以实现在相同码率下,提供更好的画质。 - - ### PTS和DTS 【为什么会有PTS和DTS的概念】 @@ -78,33 +74,5 @@ PTS(Presentation Time Stamp):即**显示时间戳**,这个时间戳用 - - - - - - - - - - - - - - - - - - - - - - - - - - ---- - - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! 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 b2f222a2..852cfb03 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" @@ -55,11 +55,11 @@ TS(Transport Stream),全程为MPEG2-TS。它的特点就是要求从视频流 - Media encoder(媒体编码) -媒体编码器获取到音视频设备的实时信号,将其编码后压缩用于传输 + 媒体编码器获取到音视频设备的实时信号,将其编码后压缩用于传输 - Stream segmenter(流切片器) - 通常是一个软件,将流进行切片,切片的同时会创建一个索引文件(index file),索引文件会包含这些切片的引用。 + 通常是一个软件,将流进行切片,切片的同时会创建一个索引文件(index file),索引文件会包含这些切片的引用。 从左到右讲,左下方的inputs的视频源是什么格式都无所谓,他与server之间的通信协议也可以任意(比如RTMP),总之只要把视频数据传输到服务器上即可。这个视频在server服务器上被转换成HLS格式的视频(既TS和m3u8文件)文件。细拆分来看server里面的Media encoder的是一个转码模块负责将视频源中的视频数据转码到目标编码格式(H264)的视频数据,视频源的编码格式可以是任何的视频编码格式。转码成H264视频数据之后用硬件打包到MPEG-2(MPEG-2 Transport Stream)的传输流中,传输流再经过stream segmenter模块,它的工作是把MPEG-2传输流分散为小片段然后保存为一个或多个系列的.ts格式的媒体文件,结果就是index file(m3u8)和ts文件了。图中的Distribution其实只是一个普通的HTTP文件服务器,然后客户端只需要访问一级index文件的路径就会自动播放HLS视频流了。 diff --git "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" index 0f90bba7..9cbdf178 100644 --- "a/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" +++ "b/VideoDevelopment/\346\265\201\345\252\222\344\275\223\345\215\217\350\256\256/HTTP FLV.md" @@ -7,15 +7,9 @@ HTTP FLV FLV(Flash Video)是Adobe公司设计开发的一种流行的流媒体格式,其格式相对简单轻量,不需要很大的媒体头部信息。整个FLV由Header和Body以及其他Tag组成。因此加载速度极快。它是基于HTTP/80传输,可以避免被防火墙拦截的问题,除此之外,它可以通过 HTTP 302 跳转灵活调度/负载均衡,支持使用 HTTPS 加密传输,也能够兼容支持 Android,iOS 的移动端。但是由于它的传输特性,会让流媒体资源缓存在本地客户端,在保密性方面不够好,因为网络流量较大,它也不适合做拉流协议。此外,FLV可以使用Flash Player进行播放,而Flash Player插件已经安装在绝大部分浏览器上,这使得通过网页播放FLV视频十分容易。FLV封装格式的文件后缀通常为“.flv”。 - - -先看看HTTP-FLV长成什么样子:http://ip:port/live/livestream.flv,协议头是http,另外”.flv”这个尾巴是它最明显的特征。 - HttpFlv 就是 http+flv ,将音视频数据封装成FLV格式,然后通过 HTTP 协议传输给客户端。 - - -下的直播平台中大部分的主线路使用的都是HTTP-FLV协议,备线路多为RTMP。小编随便在Safari中打开几个直播平台房间,一抓包就不难发现使用HTTP-FLV协议的身影:熊猫、斗鱼、虎牙、B站。 +当下的直播平台中大部分的主线路使用的都是HTTP-FLV协议,备线路多为RTMP。小编随便在Safari中打开几个直播平台房间,一抓包就不难发现使用HTTP-FLV协议的身影:熊猫、斗鱼、虎牙、B站。 @@ -87,4 +81,4 @@ https://blog.csdn.net/qq_37382077/article/details/103386289 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/VideoDevelopment/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" "b/VideoDevelopment/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" index 7d36e85b..5b7375cd 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\346\222\255\346\224\276\347\233\270\345\205\263\345\206\205\345\256\271\346\200\273\347\273\223.md" @@ -97,7 +97,7 @@ ## `SurfaceHolder`简介 -显示一个`Surface`的抽象接口,使你可以控制`Surface`的大小和格式以及在`Surface`上编辑像素,和监视`Surace`的改变。这个接口通常通过`SurfaceView`类实现。 +显示一个`Surface`的抽象接口,使你可以控制`Surface`的大小和格式以及在`Surface`上编辑像素,和监视`Surface`的改变。这个接口通常通过`SurfaceView`类实现。 简单的说就是我们无法直接操作`Surface`只能通过`SurfaceHolder`这个接口来获取和操作`Surface`。 `SurfaceHolder`中提供了一些`lockCanvas()`:获取一个`Canvas`对象,并锁定之。所得到的`Canvas`对象,其实就是`Surface`中一个成员。加锁的目的其实就是为了在绘制的过程中, `Surface`中的数据不会被改变。`lockCanvas`是为了防止同一时刻多个线程对同一`canvas`写入。 @@ -127,4 +127,4 @@ --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 42f37816cbcd24e6680dfa7eb4cb00ea0c4a4638 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 24 Apr 2023 12:00:36 +0800 Subject: [PATCH 017/128] update OpenGL part1 --- .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 777ec332..89fce18a 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -25,14 +25,14 @@ OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRI OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。 -移动端使用到的基本上都是OpenGl ES,当然Android开发下还专门为OpenGl提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。 +移动端使用到的基本上都是OpenGL ES,当然Android开发下还专门为OpenGL提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。 ### OpenGL的作用 在手机上有两大元件,一个是CPU,一个是GPU。显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU), -图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点匀速,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 +图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 - 图片处理:比如图片色调转换、美颜等。 - 摄像头预览效果处理。比如美颜相机、恶搞相机等。 @@ -175,7 +175,7 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 ### OpenGL Context -OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。OpenGL是个状态机,有很多状态变量,试个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 +OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 @@ -226,9 +226,6 @@ Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。 - - - ### 参考 --- From 8888b24892851fcac44f376f9c44fc4c88616079 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 25 Apr 2023 14:27:11 +0800 Subject: [PATCH 018/128] update OpenGl ES part --- VideoDevelopment/.DS_Store | Bin 6148 -> 0 bytes .../1.OpenGL\347\256\200\344\273\213.md" | 27 ++++++- ...55\346\224\276\350\247\206\351\242\221.md" | 10 +-- .../11.OpenGL ES\346\273\244\351\225\234.md" | 27 +++++++ ....GLSurfaceView\347\256\200\344\273\213.md" | 2 +- ...66\344\270\211\350\247\222\345\275\242.md" | 76 ++++++++++++++++-- ...45\231\250\350\257\255\350\250\200GLSL.md" | 56 ++++++++++++- .../9.OpenGL ES\347\272\271\347\220\206.md" | 20 +++-- 8 files changed, 191 insertions(+), 27 deletions(-) delete mode 100644 VideoDevelopment/.DS_Store diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store deleted file mode 100644 index 5eb6b49d607f8326edeb9163d5c1a73b2c412c90..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z<-5O({YS3OxqA7K~N}@e*S70gUKDr6wfQV45vWY7V84v%Zi|;`2DO zyEy~{-bCz7+5KkcXE*af_lGgY-DP;ln8_G3pdoTp8U)Rat}Pvm$mJXnn+8QT4-yeH zO!OB``0X8b1-}`av-4|j1IiozAkG|XhKryNGl=uOPQYBjR$p*1?O*F$S{ zb_DwI=ybjA8GHK&=hu^$WR{9IMI;B#rR->|;2o4&O|RY}O+-2cdzDkg5)uQ%05L!e zY#sypEHL_;S2|Tp3=ji9Fo64m1r5>BSSplT2XuISMt>U-1$2B%AX*w7jio~HfN+%x zs8YFpVsMoXc1y=O8cT&LopHG`%%fMX9xq(34t7h1Gwvv)o){no>I}5h(8Tlq68 setRender -> onSurfaceCreated回调方法中构造一个SurfaceTexture对象,然后设置到Camera预览或者MediaPlayer中 -> SurfaceTexture中的回调方法onFrameAvailable来得知一帧的数据真好完成 -> requestRender通知Render来绘制数据 -> 在Render的回调方法onDrawFrame中调用SurfaceTexture的updateTexImage方法来获取一帧数据,然后开始使用GL进行绘制预览。 +GLSurfaceView -> setRender -> onSurfaceCreated回调方法中构造一个SurfaceTexture对象,然后设置到Camera预览或者MediaPlayer中 -> SurfaceTexture中的回调方法onFrameAvailable来得知一帧的数据准备完成 -> requestRender通知Render来绘制数据 -> 在Render的回调方法onDrawFrame中调用SurfaceTexture的updateTexImage方法来获取一帧数据,然后开始使用GL进行绘制预览。 -​ 具体步骤: +具体步骤: 1. 在GLSurfaceView.Render中创建一个纹理,再使用该纹理创建一个SurfaceTexture。 2. 使用该SurfaceTexture创建一个Surface传给相机,相机预览数据就会输出到这个纹理上了。 @@ -317,11 +317,11 @@ public class VideoPlayerRender extends BaseGLSurfaceViewRenderer { 下面是具体的效果分别是填充宽和填充高的效果: - + - + diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index ed330195..27d255aa 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -371,6 +371,33 @@ public class BaseFilter { } ``` +#### 常用概念整理 +- OpenGL: 一个定义了函数布局和输出的图形API的正式规范。 +- GLAD: 一个拓展加载库,用来为我们加载并设定所有OpenGL函数指针,从而让我们能够使用所有(现代)OpenGL函数。 +- 视口(Viewport): 我们需要渲染的窗口。 +- 图形管线(Graphics Pipeline): 一个顶点在呈现为像素之前经过的全部过程。 +- 着色器(Shader): 一个运行在显卡上的小型程序。很多阶段的图形管道都可以使用自定义的着色器来代替原有的功能。 +- 标准化设备坐标(Normalized Device Coordinates, NDC): 顶点在通过在剪裁坐标系中剪裁与透视除法后最终呈现在的坐标系。所有位置在NDC下-1.0到1.0的顶点将不会被丢弃并且可见。 +- 顶点缓冲对象(Vertex Buffer Object): 一个调用显存并存储所有顶点数据供显卡使用的缓冲对象。 +- 顶点数组对象(Vertex Array Object): 存储缓冲区和顶点属性状态。 +- 元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO): 一个存储元素索引供索引化绘制使用的缓冲对象。 +- Uniform: 一个特殊类型的GLSL变量。它是全局的(在一个着色器程序中每一个着色器都能够访问uniform变量),并且只需要被设定一次。 +- 纹理(Texture): 一种包裹着物体的特殊类型图像,给物体精细的视觉效果。 +- 纹理缠绕(Texture Wrapping): 定义了一种当纹理顶点超出范围(0, 1)时指定OpenGL如何采样纹理的模式。 +- 纹理过滤(Texture Filtering): 定义了一种当有多种纹素选择时指定OpenGL如何采样纹理的模式。这通常在纹理被放大情况下发生。 +- 多级渐远纹理(Mipmaps): 被存储的材质的一些缩小版本,根据距观察者的距离会使用材质的合适大小。 +- 纹理单元(Texture Units): 通过绑定纹理到不同纹理单元从而允许多个纹理在同一对象上渲染。 +- 向量(Vector): 一个定义了在空间中方向和/或位置的数学实体。 +- 矩阵(Matrix): 一个矩形阵列的数学表达式。 +- GLM: 一个为OpenGL打造的数学库。 +- 局部空间(Local Space): 一个物体的初始空间。所有的坐标都是相对于物体的原点的。 +- 世界空间(World Space): 所有的坐标都相对于全局原点。 +- 观察空间(View Space): 所有的坐标都是从摄像机的视角观察的。 +- 裁剪空间(Clip Space): 所有的坐标都是从摄像机视角观察的,但是该空间应用了投影。这个空间应该是一个顶点坐标最终的空间,作为顶点着色器的输出。OpenGL负责处理剩下的事情(裁剪/透视除法)。 +- 屏幕空间(Screen Space): 所有的坐标都由屏幕视角来观察。坐标的范围是从0到屏幕的宽/高。 +- LookAt矩阵: 一种特殊类型的观察矩阵,它创建了一个坐标系,其中所有坐标都根据从一个位置正在观察目标的用户旋转或者平移。 +- 欧拉角(Euler Angles): 被定义为偏航角(Yaw),俯仰角(Pitch),和滚转角(Roll)从而允许我们通过这三个值构造任何3D方向。 + [上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index dab69c89..65f48e61 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -114,7 +114,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { ``` 效果图就不贴了,就是一个纯绿色的Activity。 - +上面glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色 Render接口重写的三个方法中调用了GLES31的API方法: - glClearColor():设置清空屏幕用的颜色,参数为RGBA。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 6e9adc89..3a94df07 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -4,7 +4,22 @@ OpenGL ES的绘制需要有以下步骤: - 顶点输入 - 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内采取处理它。所有在所谓的标准化设备坐标范围内的坐标才会最终呈现到屏幕上(在这个范围以外的坐标都不会显示)。 + 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内才去处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。 + +由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 +```glsl +float vertices[] = { + -0.5f, -0.5f, 0.0f, + 0.5f, -0.5f, 0.0f, + 0.0f, 0.5f, 0.0f +}; +``` +定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 + +我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。 + +顶点缓冲对象是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。 + - 顶点着色器 @@ -24,6 +39,10 @@ OpenGL ES的绘制需要有以下步骤: 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 +向量(Vector) + +在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。 + 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数中只是将position的值转换后赋值给gl_Position。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。 这个position的值是哪里进行赋值的呢? 是通过后面java代码中的draw函数来进行赋值的。每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 @@ -43,16 +62,20 @@ OpenGL ES的绘制需要有以下步骤: color = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` - +在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色! + 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区。,然后颜色缓冲区再通过EGL窗口显示。 +片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为color。 + - 着色器程序(Shader Program Object) - 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 + 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: ```java + shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader); glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram); @@ -60,6 +83,15 @@ OpenGL ES的绘制需要有以下步骤: 链接完后需要使用glUseProgram方法,用刚创建的程序对象作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 +对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了: +``` +glDeleteShader(vertexShader); +glDeleteShader(fragmentShader); +``` + +现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。下面我们需要告诉OpenGL怎么做。 + + - 链接顶点属性 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 @@ -67,6 +99,10 @@ OpenGL ES的绘制需要有以下步骤: 我们的顶点缓冲数据会被解析成下面的样子: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer.png) +- 位置数据被储存为32位(4字节)浮点值。 +- 每个位置包含3个这样的值。 +- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。 +- 数据中第一个值在缓冲开始的位置。 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: @@ -84,6 +120,28 @@ OpenGL ES的绘制需要有以下步骤: - 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个`GLfloat`之后,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 最后一个参数的类型是`GLvoid*`,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。 +每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。 + +现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样: + +// 0. 复制顶点数组到缓冲中供OpenGL使用 +glBindBuffer(GL_ARRAY_BUFFER, VBO); +glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); +// 1. 设置顶点属性指针 +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); +glEnableVertexAttribArray(0); +// 2. 当我们渲染一个物体时要使用着色器程序 +glUseProgram(shaderProgram); +// 3. 绘制物体 +someOpenGLFunctionThatDrawsOurTriangle(); + +每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? + +顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects.png) + + + 下面需要实现GLSurfaceView.Render接口,实现要绘制的部分: @@ -131,7 +189,7 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 } ``` - 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色漆的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 + 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 - 片段着色器(triangle_fragment_shader.glsl) @@ -424,10 +482,12 @@ public class TriangleRender implements GLSurfaceView.Renderer { ``` +调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成着色器对象时,可以用glDeleteShader删除。 +注意,如果一个着色器连接到一个程序对象,那么调用glDeleteShader不会立即删除着色器,而是将着色器编标记为删除,在着色器不再连接到任何程序对象时,它的内存将被释放。 效果如下: - + 我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为前面说到的OpenGL ES使用的是虚拟坐标导致的。如果想让绘制一个等腰三角形该怎么做呢? @@ -437,7 +497,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { 这里就牵扯到了要对坐标向量进行换算,这里的换算需要使用矩阵来进行。总体分为两部分: -- 或者获得一个矩阵,可以把坐标范围从【-2,2】换算成【-1,1】的范围内。(提供了Matrix.orthoM来处理矩阵) +- 如何获得一个矩阵,可以把坐标范围从【-2,2】换算成【-1,1】的范围内。(提供了Matrix.orthoM来处理矩阵) - 如何将这个矩阵传递给GLSL中。(与获取顶点索引类似,可以在GLSL中声明一个mat4类型的矩阵变量,获取其索引,再传递值给它) @@ -462,7 +522,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { 1. 相机位置:相机在3D空间里面的坐标点。 2. 相机观察方向:相机镜头的朝向,朝前拍、朝后拍、朝左拍、朝右拍。 -3. 相机UP方向:相机顶端志祥的方向,例如斜着拿、反着拿。 +3. 相机UP方向:相机顶端指向的方向,例如斜着拿、反着拿。 Android OpenGL ES程序中,我们可以通过Matrix.setLookAtm来对相机进行设置: @@ -593,7 +653,7 @@ Matrix.multiplyMM (float[] result, //接收相乘结果 ### 绘制等腰三角形 -​ 在上面绘制三角形的基础上进行修改。 +在上面绘制三角形的基础上进行修改。 ### 工具类 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 7e935fbd..4c79a8dd 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -1,5 +1,14 @@ ## 7.OpenGL ES着色器语言GLSL +在计算机图形中,两个基本数据类型组成了变换的基础: +- 向量 +- 矩阵 +这两种数据类型在OpenGL ES着色语言中也是核心,下图具体描述了着色语言中存在的机遇标量、向量和矩阵的数据类型。 + + + + + ### 顶点着色器: ``` @@ -17,7 +26,7 @@ void main() #version 300 es是表示GLSL语言的版本为300,对应的是OpenGL ES 3.0版本。 - + 顶点着色器的输入包括: @@ -28,6 +37,49 @@ void main() 顶点着色器的输出在OpenGL ES 2.0中称作可变(varying)变量,但在OpenGL ES 3.0中改名为顶点着色器输出(out)变量。在图元光栅化阶段,为每个生成的片段计算顶点着色器输出值,并作为输入传递给片段着色器。 +虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。 + + + + + +#### in、out +顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。 + +你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。 + +#### Uniform +Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 + +我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: + +#version 330 core +out vec4 FragColor; + +uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量 + +void main() +{ + FragColor = ourColor; +} +我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 + +如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点! + +这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色: + +float timeValue = glfwGetTime(); +float greenValue = (sin(timeValue) / 2.0f) + 0.5f; +int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); +glUseProgram(shaderProgram); +glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); +首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。 + +接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 + + + + 内建变量包括: - gl_Position:用于输出顶点位置的裁剪坐标。 @@ -114,7 +166,7 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 - 矩阵 - 在GLSL中矩阵拥有2*2、3*3、4*4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 + 在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 - 采样器 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 962345b9..6be99fb8 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -7,7 +7,6 @@ 在OpenGL中简单理解就是一张图片,在学习之前需要明白这几个概念,不然很容易迷糊,不知道为什么要这样去调用api,到底是什么意思. - 纹理Id:句柄,纹理的直接引用 - - 纹理单元:纹理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,纹理单元的数量是有限的,最多16个。所以在最多只能同时操作16个纹理。在切换使用纹理单元的时候,使用glActiveTexture方法。也就是说OpenGL ES中内置了很多个纹理单元,并且是连续的,我们在使用的时候要选择其中一个,一般默认选择第一个(GLES_TEXTURE0),并且如果不选的话OpenGL默认激活的也就是第一个纹理单元。采样器统一变量将加载一个指定纹理绑定的纹理单元的数值,例如,用数值0指定采样器表示从单元GL_TEXTURE0读取,指定数值1表示从GL_TEXTURE1读取,以此类推。激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法将纹理单元,与GLSL中的采样器属性相关联。 - 纹理目标:一个纹理单元中包含了多个类型的纹理目标,有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,将纹理ID绑定到纹理单元0的GL_TEXTURE_2D纹理目标上,之后对纹理目标的操作都是对纹理Id对应的数据进行操作。 @@ -18,7 +17,7 @@ ### 纹理与渐变色的区别 -渐变色:光栅化过程中计算出颜色值,然后再片段着色器的时候可以直接赋值。 +渐变色:光栅化过程中计算出颜色值,然后在使用片段着色器的时候可以直接赋值。 纹理:光栅化过程中,计算出当前片段在纹理上的坐标位置,然后在片段着色器中根据这个纹理上的坐标,去纹理中取出相应的颜色值。 @@ -36,7 +35,7 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 ### 纹理过滤 -当我们通过光栅化将图形处理成一个个小片段的时候,再讲纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 +当我们通过光栅化将图形处理成一个个小片段的时候,再将纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 ### 纹理对象和纹理的加载 @@ -54,7 +53,7 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 GLenum format, GLenum type, const void* pixels) - 加载2D和立方图纹理图像数据。 + 加载2D和立方图纹理图像数据,生成最终纹理。 @@ -216,7 +215,12 @@ public class TextureUtil { - 片段着色器 - 之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色`。 +片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。 + +我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。 + + + 之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色。 ``` #version 300 es @@ -249,7 +253,7 @@ public class TextureUtil { GLES20.glUniform1i(aTextureLocation, 0); ``` - 这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。这事因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 + 这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 @@ -444,7 +448,7 @@ public class TextureUtil { -- 多纹理单元,单词绘制 +- 多纹理单元,单次绘制 OpenGL可以同时操作的纹理单元是16个,那么我们可以利用多个纹理单元来进行绘制同一个图层,从而达到目的。多纹理单元需要修改片段着色器的代码,顶点着色器不用改变,这种方式的优点是可以控制多个纹理的关系,做出复杂的效果。缺点是多个纹理单元的顶点坐标必须是一样的。 @@ -456,7 +460,7 @@ public class TextureUtil { // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为0 uniform sampler2D uTextureUnit; uniform sampler2D uTextureUnit2; - // 接受刚才顶点着色器传入的纹理坐标(s, t) + // 接收刚才顶点着色器传入的纹理坐标(s, t) in vec2 vTexCoord; out vec4 vFragColor; From ac92eb798e89ec60ef1c6ec384026c71049e47c0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 28 Apr 2023 19:46:03 +0800 Subject: [PATCH 019/128] update --- .../1.OpenGL\347\256\200\344\273\213.md" | 4 ++ ....GLSurfaceView\347\256\200\344\273\213.md" | 2 +- ...66\344\270\211\350\247\222\345\275\242.md" | 47 ++++++++++--------- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 0f8f0c43..193f3a83 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -85,10 +85,14 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 + 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着回处理我们在内存中指定数量的顶点。 + 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 - 图元装配(Primitive Assembly) 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状,上图的例子中是一个三角形。 + 也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。 + OpenGL ES中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: - GL_POINTS:以点的形式绘制 diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index 65f48e61..fa6b2098 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -118,7 +118,7 @@ class MyGLRenderer implements GLSurfaceView.Renderer { Render接口重写的三个方法中调用了GLES31的API方法: - glClearColor():设置清空屏幕用的颜色,参数为RGBA。 -- glClear():清空屏幕,清空屏幕后调用glClearColor()中设置的颜色填充屏幕。 +- glClear():清空屏幕,当调用glClear函数清除颜色缓冲之后,整个颜色缓冲都会被填充为glClearColor里所设置的颜色。glClear函数来清空屏幕的颜色缓冲时会接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,可能的缓冲位有GL_COLOR_BUFFER_BIT、GL_DEPTH_BUFFER_BIT和GL_STENCIL_BUFFER_BIT,由于这里我们只关心颜色值,所以我们只清空颜色缓冲。 - glViewport():设置视图的尺寸,告诉OpenGL可以用来渲染surface的大小。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 3a94df07..20bd1b33 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -124,8 +124,14 @@ glDeleteShader(fragmentShader); 现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样: -// 0. 复制顶点数组到缓冲中供OpenGL使用 +// 0. 复制顶点数组到缓冲中供OpenGL使用,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象 +glGenBuffers(1, VBO); +// 使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 glBindBuffer(GL_ARRAY_BUFFER, VBO); +// glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数 +// 它的第一个参数是目标缓冲类型。 第二个参数是指定传输数据的大小(以字节为单位),用一个简单的sizeof计算出顶点数据的大小就行 +// 第三个参数是我们希望发送的实际数据 +// 第四个参数指定了我们希望显卡如何管理这些数据,有三种形式: GL_STATIC_DRAW(数据不会或几乎不会改变), GL_DYNAMIC_DRAW(数据会被改变很多), GL_STREAM_DRAW(数据每次绘制时都会改变) glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 1. 设置顶点属性指针 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); @@ -150,7 +156,7 @@ someOpenGLFunctionThatDrawsOurTriangle(); - 写顶点着色器和片段着色器文件。 - 加载编译顶点着色器和片段着色器。 - 确定需要绘制图形的坐标和颜色数据。 -- 创建program对象,连接顶点和片断着色器,将坐标数据、颜色数据传到OpenGL ES程序中。] +- 创建program对象,连接顶点和片断着色器,将坐标数据、颜色数据传到OpenGL ES程序中。 - 设置视图窗口(viewport)。 - 使颜色缓冲区的内容显示到EGL窗口上。 @@ -169,25 +175,10 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 安装完GLSL插件后,就可以开始了。一般将GLSL文件放到raw或assets目录,我们这里在raw目录上右键New,然后选择GLSL Shader创建就可以了,创建后默认会生成一个main()函数。 - 顶点着色器(triangle_vertex_shader.glsl) - - ```glsl - // 声明着色器的版本 - #version 300 es - // 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 - layout (location = 0) in vec4 vPosition; - // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 - layout (location = 1) in vec4 aColor; - // 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 - out vec4 vColor; - void main() { - // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition - gl_Position = vPosition; - // gl_PointSize为Shader内置变量,为点的直径 - gl_PointSize = 10.0; - // 将输入数据aColor拷贝到vColor的变量中。 - vColor = aColor; - } - ``` +```glsl +// 声明着色器的版本 +#version 300 es +// 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 layout (location = 0) in vec4 vPosition; // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 layout (location = 1) in vec4 aColor; // 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 out vec4 vColor; void main() { // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition gl_Position = vPosition; // gl_PointSize为Shader内置变量,为点的直径 gl_PointSize = 10.0; // 将输入数据aColor拷贝到vColor的变量中。 vColor = aColor; } ``` 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 @@ -207,7 +198,21 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 fragColor = vColor; } ``` +precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 +精度限定符可以用于指定任何浮点数或整数的变量的精度。指定精度的关键字是lowp、mediump和highp。下面是一些带有精度限定符的声明示例: +``` +highp vec4 position; +mediump float specularExp; +``` +除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。默认精度限定符在顶点或片段着色器的开头用以下语法指定: +``` +precision highp float; +precision mediump int; +``` +为float类型指定的精度将用作所有基于浮点值变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。 +在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。 +片段着色器的规则与此不同。在片段着色器中,浮点值没有默认的精度值:每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。 ```java From a75e14c5d312131a70a825564c7093d6c989e1ea Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 28 Apr 2023 21:41:29 +0800 Subject: [PATCH 020/128] update notes --- ...66\344\270\211\350\247\222\345\275\242.md" | 22 ++++++++-- ...45\231\250\350\257\255\350\250\200GLSL.md" | 41 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 20bd1b33..d29e7cdb 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -123,7 +123,7 @@ glDeleteShader(fragmentShader); 每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。 现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样: - +```java // 0. 复制顶点数组到缓冲中供OpenGL使用,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象 glGenBuffers(1, VBO); // 使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 @@ -140,7 +140,7 @@ glEnableVertexAttribArray(0); glUseProgram(shaderProgram); // 3. 绘制物体 someOpenGLFunctionThatDrawsOurTriangle(); - +``` 每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? 顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 @@ -178,7 +178,21 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 ```glsl // 声明着色器的版本 #version 300 es -// 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 layout (location = 0) in vec4 vPosition; // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 layout (location = 1) in vec4 aColor; // 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 out vec4 vColor; void main() { // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition gl_Position = vPosition; // gl_PointSize为Shader内置变量,为点的直径 gl_PointSize = 10.0; // 将输入数据aColor拷贝到vColor的变量中。 vColor = aColor; } ``` +// 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 +layout (location = 0) in vec4 vPosition; +// 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 +layout (location = 1) in vec4 aColor; +// 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 +out vec4 vColor; +void main() { + // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition + gl_Position = vPosition; + // gl_PointSize为Shader内置变量,为点的直径 + gl_PointSize = 10.0; + // 将输入数据aColor拷贝到vColor的变量中。 + vColor = aColor; +} +``` 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 @@ -494,6 +508,8 @@ public class TriangleRender implements GLSurfaceView.Renderer { + + 我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为前面说到的OpenGL ES使用的是虚拟坐标导致的。如果想让绘制一个等腰三角形该怎么做呢? ### 变成等腰三角形的原理 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 4c79a8dd..86fe7b91 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -17,14 +17,14 @@ layout (location = 0) in vec3 position; // position变量的属性位置值为0 out vec4 vertexColor; // 为片段着色器指定一个颜色输出 -void main() -{ +void main() { gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色 } ``` #version 300 es是表示GLSL语言的版本为300,对应的是OpenGL ES 3.0版本。 +#version必须写在文件的第一行,即是第一行是注释也不行。 @@ -48,6 +48,37 @@ void main() 你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。 +如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。 + +顶点着色器 +```glsl +#version 330 core +layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0 + +out vec4 vertexColor; // 为片段着色器指定一个颜色输出 + +void main() +{ + gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数 + vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 +} +``` +片段着色器 +```glsl +#version 330 core +out vec4 FragColor; + +in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) + +void main() +{ + FragColor = vertexColor; +} +``` +你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。 + + + #### Uniform Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 @@ -164,6 +195,12 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。 作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。 + 我们大部分时候使用vecn,因为float足够满足大多数要求了。 + ```glsl +vec2 vect = vec2(0.5, 0.7); +vec4 result = vec4(vect, 0.0, 0.0); + ``` + - 矩阵 在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 From 5a028c457833237665da9714c7ba2ca08123572d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 8 May 2023 21:21:09 +0800 Subject: [PATCH 021/128] update --- .../1.OpenGL\347\256\200\344\273\213.md" | 80 +++++++++++++------ ...45\231\250\350\257\255\350\250\200GLSL.md" | 36 ++++++++- ...\261\273\345\217\212Matrix\347\261\273.md" | 23 ++++++ 3 files changed, 110 insertions(+), 29 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 193f3a83..fdadeeed 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -81,12 +81,31 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 - 顶点着色器(Vertex Shader): 图形渲染管线的第一个部分是顶点着色器,他是用来渲染图形顶点的OpenGL ES代码。它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。 - 着色器(shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。 + 着色器(shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。 - 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 + 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着回处理我们在内存中指定数量的顶点。 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 + + +把数据发送给OpenGL管线还要更加复杂一点,有两种方式: +·通过顶点属性的缓冲区; +·直接发送给统一变量。 +理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 +下面从渲染一个简单的立方体开始。 +##### 缓冲区和顶点属性 +想要绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在C++端放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中。 +(1)创建缓冲区。 +(2)将顶点数据复制到缓冲区。 +每帧都要做的步骤如下,它们一般包含在display()中。 +(1)启用包含顶点数据的缓冲区。 +(2)将这个缓冲区和一个顶点属性相关联。 +(3)启用这个顶点属性。 +(4)使用glDrawArrays()绘制对象。 +所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在C++/OpenGL应用程序中被声明和实例化。一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。 + + - 图元装配(Primitive Assembly) 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 @@ -95,15 +114,15 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 OpenGL ES中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: - - GL_POINTS:以点的形式绘制 - - GL_LINES:以线的形式绘制 - - GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 + - GL_POINTS:以点的形式绘制 + - GL_LINES:以线的形式绘制 + - GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 - 该过程还有两个重要操作:裁剪和淘汰。 + 该过程还有两个重要操作:裁剪和淘汰。 - - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 + - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 - - 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 + - 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 - 几何着色器(Geometry Shader) 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。本例子中,它生成了另一个三角形。 @@ -111,15 +130,15 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。 - 光栅化阶段(Rasterization Stage) - 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 + 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 - 光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 + 光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 - 片段着色器或片元着色器(Fragment Shader):使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 - 主要目的是计算一个像素的最终颜色。光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 + 主要目的是计算一个像素的最终颜色。光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 - 通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 + 通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。 @@ -127,9 +146,9 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 - 逐片段操作 - 具体的细分步骤又分为: + 具体的细分步骤又分为: - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_fragment_opera.jpg) + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_fragment_opera.jpg) - 像素归属测试 @@ -156,37 +175,37 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 用于最小化,因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像,使用少量颜色模拟更宽的颜色范围。 - 帧缓冲区 - - OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) + OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区,然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。 + OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) - 着色器语言 - GLSL(OpenGL Shading Language)是OpenGL着色语言。 + GLSL(OpenGL Shading Language)是OpenGL着色语言。 - 在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分: + 在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分: - 顶点着色器(Vertex Shader) - 片段着色器(Fragment Shader) - 坐标系 - OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 + OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) - OpenGL ES采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。 + OpenGL ES采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。 - 形状面和缠绕 - 在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。 + 在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。 - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_ex_xy_1.jpg) + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_ex_xy_1.jpg) - 三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。 + 三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。 - 程式(Program) - 一个OpenGL ES对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个对象。 + 一个OpenGL ES对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个对象。 @@ -202,6 +221,9 @@ OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中 ### EGL +EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、Mac OSX)之间的一个“结合”层次。在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,成为EAGL。 +因为每个窗口系统都有不同的语音,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL现实的连接。 + OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 @@ -240,6 +262,14 @@ EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 +#### 创建屏幕外渲染区域: EGL Pbuffer +除了可以用OpenGL ES在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 +和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。 +Pbuffer最常用于生成纹理贴图。如果你想要做的事渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。 +创建Pbuffer和创建EGL窗口非常相似,只有少数微小的不同。为了创建Pbuffer,需要和窗口一样找到EGLConfig,并作一处修改:我们需要扩增EGL_SURFACE_TYPE的值,使其包含EGL_PBUFFER_BIT。 + + + #### OpenGL纹理 纹理(Texture)是一个2D图片(也有1D和3D纹理),他可以用来添加物体的细节,你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就有砖墙的外表了。 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 86fe7b91..d2bc64a1 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -78,12 +78,36 @@ void main() 你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。 +#### 精度限定符 + +精度限定符使着色器创作者可以指定着色器变量的计算精度。变量可以声明为低、中或者高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量计算。在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。 +当然,这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 +精度限定符可以用于指定任何基于浮点数或者整数的变量的精度。指定精度的关键字是lowp、mediump和highp。 +```glsl +highp vec4 position; +mediump float specularExp; +``` +除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。默认精度限定符在顶点或片段着色器的开头用如下语法指定: +```glsl +precision highp float; +precision mediump int; +``` +为float类型指定的精度将用作所有基于浮点值的变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。 +在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都为highp,也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。 + +但片段着色器的规则与此不同,在片段着色器中,浮点值没有默认的精度值:每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。 + + #### Uniform Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 -我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: +统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 +但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。 + +我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: +```glsl #version 330 core out vec4 FragColor; @@ -93,22 +117,26 @@ void main() { FragColor = ourColor; } +``` 我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点! 这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色: - +```glsl float timeValue = glfwGetTime(); float greenValue = (sin(timeValue) / 2.0f) + 0.5f; int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUseProgram(shaderProgram); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); +``` 首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。 -接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 - +接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。 +如果glGetUniformLocation返回-1就代表没有找到这个位置值。 +最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 +现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 内建变量包括: diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 2eb24927..67fb2790 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -163,6 +163,29 @@ Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的 获取逆矩阵 + + +顶点着色器可赋予程序员一次操作一个顶点(“按顶点”处理)的能力,片段着色器(稍后会看到)可赋予程序员一次操作一个像素(“按片段”处理)的能力,几何着色器可赋予程序员一次操作一个图元(“按图元”处理)的能力。 + +到目前为止,我们所接触的变换矩阵全都可以在3D空间中操作。但是,我们最终需要将3D空间或它的一部分展示在2D显示器上。为了达成这个目标,我们需要找到一个有利点。正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。 + +观察3D世界需要:(a)将相机放入世界的某个位置;(b)调整相机的角度,通常需要一套它自己的直角坐标轴u、v、n(由向量U,V,N构成);(c)定义一个视体(view volume);(d)将视体内的对象投影到投影平面(projection plane)上。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye.jpg?raw=true) + +OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图所示。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye_00.jpg?raw=true) +为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置(如物体应该在由图3.12所示相机U、V、N向量定义的“相机空间”中的位置)。给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。我们通过计算它在相机空间中的位置PC实现。已知OpenGL相机位置永远固定在点(0,0,0),那么我们如何变换来实现上述功能? +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye_01.jpg?raw=true) + +当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 + +在正射投影中,平行线仍然是平行的,即不使用透视,如下图所示。正射与透视相反,在视体中的物体不因其与相机的距离而改变,可以直接投影。正射投影是一种平行投影,其中所有的投影过程都沿与投影平面垂直的方向进行。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_zheng.jpg?raw=true) + +我们最后要学习的变换矩阵是LookAt矩阵。当你想要把相机放在某处并看向一个特定的位置时,就需要用到它了,LookAt矩阵的元素如图3.19所示。当然,用我们已经学到的方法也可以实现LookAt变换,但是这个操作非常频繁,因此为它专门构建一个矩阵通常比较有用。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_lookat.jpg?raw=true) + - invertM 计算正交投影和透视投影 From d0706b45ee1a70fc61e4bbb6b414aee2334b3719 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 9 May 2023 16:56:33 +0800 Subject: [PATCH 022/128] update opengl part --- .../1.OpenGL\347\256\200\344\273\213.md" | 23 +++++- ...66\344\270\211\350\247\222\345\275\242.md" | 34 +++++++++ ...42\345\217\212\345\234\206\345\275\242.md" | 58 +++++++++++++++ ...45\231\250\350\257\255\350\250\200GLSL.md" | 73 ++++++++++++++++++- 4 files changed, 186 insertions(+), 2 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index fdadeeed..c0ae0d62 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -85,9 +85,30 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 - 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着回处理我们在内存中指定数量的顶点。 + 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 + 顶点缓冲对象是OpenGL中的一个对象,就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象: + ```c++ + unsigned int VBO; + glGenBuffers(1, &VBO) + ``` + 在OpenGL中有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上: + ```c++ + glBindBuffer(GL_ARRAY_BUFFER, VBO); + ``` + 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中: + ```c++ + glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + ``` + glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。 + 他的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。 + 第二个参数是指定传输数据的大小(以字节为单位),用一个简单的sizeof计算顶点数据的大小就可以。 + 第三个参数是我们希望发送的实际数据。 + 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式: + - GL_STATIC_DRAW :数据不会或几乎不会改变。 + - GL_DYNAMIC_DRAW:数据会被改变很多。 + - GL_STREAM_DRAW :数据每次绘制时都会改变。 把数据发送给OpenGL管线还要更加复杂一点,有两种方式: ·通过顶点属性的缓冲区; diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index d29e7cdb..63a69e62 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -144,8 +144,42 @@ someOpenGLFunctionThatDrawsOurTriangle(); 每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? 顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 + +一个顶点数组对象会储存以下这些内容: + +- glEnableVertexAttribArray和glDisableVertexAttribArray的调用。 +- 通过glVertexAttribPointer设置的顶点属性配置。 +- 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。 + ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects.png) +创建一个VAO和创建一个VBO很类似: +```c++ +unsigned int VAO; +glGenVertexArrays(1, &VAO); +``` +要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: +```c++ +// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. +// 1. 绑定VAO +glBindVertexArray(VAO); +// 2. 把顶点数组复制到缓冲中供OpenGL使用 +glBindBuffer(GL_ARRAY_BUFFER, VBO); +glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); +// 3. 设置顶点属性指针 +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); +glEnableVertexAttribArray(0); + +[...] + +// ..:: 绘制代码(渲染循环中) :: .. +// 4. 绘制物体 +glUseProgram(shaderProgram); +glBindVertexArray(VAO); +someOpenGLFunctionThatDrawsOurTriangle(); +``` + +就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index 37e1661c..380db0a9 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -155,6 +155,64 @@ public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Rendere 之前说过OpenGL ES提供的的图元单位是三角形,想要绘制其他多边形,就要利用三角形来拼成。 矩形是两个三角形,而圆形则是由很多个三角形组成,个数越多,圆越圆。 +### 元素缓冲对象(EBO) +在渲染顶点这一话题上我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合: +``` +float vertices[] = { + // 第一个三角形 + 0.5f, 0.5f, 0.0f, // 右上角 + 0.5f, -0.5f, 0.0f, // 右下角 + -0.5f, 0.5f, 0.0f, // 左上角 + // 第二个三角形 + 0.5f, -0.5f, 0.0f, // 右下角 + -0.5f, -0.5f, 0.0f, // 左下角 + -0.5f, 0.5f, 0.0f // 左上角 +}; +``` +可以看到,有几个顶点叠加了。我们指定了右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧? + +值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引: +``` +float vertices[] = { + 0.5f, 0.5f, 0.0f, // 右上角 + 0.5f, -0.5f, 0.0f, // 右下角 + -0.5f, -0.5f, 0.0f, // 左下角 + -0.5f, 0.5f, 0.0f // 左上角 +}; + +unsigned int indices[] = { + // 注意索引从0开始! + // 此例的索引(0,1,2,3)就是顶点数组vertices的下标, + // 这样可以由下标代表顶点组合成矩形 + + 0, 1, 3, // 第一个三角形 + 1, 2, 3 // 第二个三角形 +}; +``` +你可以看到,当使用索引的时候,我们只定义了4个顶点,而不是6个。下一步我们需要创建元素缓冲对象: +``` +unsigned int EBO; +glGenBuffers(1, &EBO); +``` +与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 +``` +glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); +glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); +``` +注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制: +``` +glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); +glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); +``` +第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。 + +glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。碰巧顶点数组对象也跟踪元素缓冲区对象绑定。在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects_ebo.png?raw=true) + +当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。 + + ### 绘制矩形 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index d2bc64a1..28295e70 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -1,5 +1,13 @@ ## 7.OpenGL ES着色器语言GLSL +着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。 + +着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。 + +和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。 + + + 在计算机图形中,两个基本数据类型组成了变换的基础: - 向量 - 矩阵 @@ -136,6 +144,16 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); 如果glGetUniformLocation返回-1就代表没有找到这个位置值。 最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 +因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有: + +- f : 函数需要一个float作为它的值 +- i : 函数需要一个int作为它的值 +- ui: 函数需要一个unsigned int作为它的值 +- 3f: 函数需要3个float作为它的值 +- fv: 函数需要一个float向量/数组作为它的值 + +每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。 + 现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 @@ -231,7 +249,11 @@ vec4 result = vec4(vect, 0.0, 0.0); - 矩阵 - 在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 +在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 +现在我们已经讨论了向量的全部内容,是时候看看矩阵了!简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/juzhen_0.png?raw=true) + +矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行) - 采样器 @@ -363,7 +385,56 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl - not(bvec x):x所有分量执行逻辑非运算 +### 更多 + +在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色: + +float vertices[] = { + // 位置 // 颜色 + 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 + -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 + 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 +}; +由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1: + +#version 330 core +layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 +layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 + +out vec3 ourColor; // 向片段着色器输出一个颜色 +void main() +{ + gl_Position = vec4(aPos, 1.0); + ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 +} +由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器: + +#version 330 core +out vec4 FragColor; +in vec3 ourColor; + +void main() +{ + FragColor = vec4(ourColor, 1.0); +} +因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer_interleaved.png?raw=true) + +知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式, +``` +// 位置属性 +glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); +glEnableVertexAttribArray(0); +// 颜色属性 +glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float))); +// 1是对应上面着色器代码中的位置: layout (location = 1) in vec3 aColor; 颜色变量的属性位置值为 1 +glEnableVertexAttribArray(1); +glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。 +``` +由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。 +同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float),用字节来计算就是12字节。 From 144205bf4384b5c944564cf497385825345bd964 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 9 May 2023 21:48:58 +0800 Subject: [PATCH 023/128] update opengl --- .../1.OpenGL\347\256\200\344\273\213.md" | 95 ++++---- ...66\344\270\211\350\247\222\345\275\242.md" | 215 +++++++++++------- 2 files changed, 180 insertions(+), 130 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index c0ae0d62..effb8b0d 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -89,49 +89,55 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 顶点缓冲对象是OpenGL中的一个对象,就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象: - ```c++ - unsigned int VBO; - glGenBuffers(1, &VBO) + ```java + // 下面只生成一个vbo对象,所以vbo的容量设置为1即可 + IntBuffer vbo = IntBuffer.allocate(1); + // 该方法有两个参数,第一个是输入生成纹理的数量,然后把它们的id储存在第二个参数的IntBuffer数组中 + GLES30.glGenBuffers(1, vbo); ``` 在OpenGL中有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上: - ```c++ - glBindBuffer(GL_ARRAY_BUFFER, VBO); + ```java + GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); ``` 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中: - ```c++ - glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); + ```java + GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * 4, + vertexBuffer, GLES30.GL_STATIC_DRAW); ``` glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。 他的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。 - 第二个参数是指定传输数据的大小(以字节为单位),用一个简单的sizeof计算顶点数据的大小就可以。 + 第二个参数是指定传输数据的大小(以字节为单位),c++中用一个简单的sizeof计算顶点数据的大小就可以,这里float的大小为4个字节。 第三个参数是我们希望发送的实际数据。 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式: - GL_STATIC_DRAW :数据不会或几乎不会改变。 - GL_DYNAMIC_DRAW:数据会被改变很多。 - GL_STREAM_DRAW :数据每次绘制时都会改变。 -把数据发送给OpenGL管线还要更加复杂一点,有两种方式: -·通过顶点属性的缓冲区; -·直接发送给统一变量。 -理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 -下面从渲染一个简单的立方体开始。 -##### 缓冲区和顶点属性 -想要绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在C++端放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中。 -(1)创建缓冲区。 -(2)将顶点数据复制到缓冲区。 -每帧都要做的步骤如下,它们一般包含在display()中。 -(1)启用包含顶点数据的缓冲区。 -(2)将这个缓冲区和一个顶点属性相关联。 -(3)启用这个顶点属性。 -(4)使用glDrawArrays()绘制对象。 -所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在C++/OpenGL应用程序中被声明和实例化。一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。 + 把数据发送给OpenGL管线还要更加复杂一点,有两种方式: + - 通过顶点属性的缓冲区; + - 直接发送给统一变量。 + 理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 + + +##### 缓冲区和顶点属性 + +想要绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中: + +- 创建缓冲区。 +- 将顶点数据复制到缓冲区。 + 每帧都要做的步骤如下,它们一般包含在display()中。 +- 启用包含顶点数据的缓冲区。 +- 将这个缓冲区和一个顶点属性相关联。 +- 启用这个顶点属性。 +- 使用glDrawArrays()绘制对象。 +所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在OpenGL应用程序中被声明和实例化。一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。 - 图元装配(Primitive Assembly) - 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 - 图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状,上图的例子中是一个三角形。 - 也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。 + 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 + 图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状。 + 也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。 OpenGL ES中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: @@ -140,18 +146,18 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 - GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 该过程还有两个重要操作:裁剪和淘汰。 - - - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 - - - 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 + - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 + - 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 - 几何着色器(Geometry Shader) -图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。本例子中,它生成了另一个三角形。 + 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。本例子中,它生成了另一个三角形。 -几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。 + 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。 - 光栅化阶段(Rasterization Stage) - 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 + 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。 + 在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 + 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 @@ -236,23 +242,26 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 ### OpenGL Context -OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 - +OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。 +OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。 +OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 +OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 ### EGL -EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、Mac OSX)之间的一个“结合”层次。在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,成为EAGL。 -因为每个窗口系统都有不同的语音,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL现实的连接。 +在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 -OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 +EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、Mac OSX)之间的一个“结合”层次。 +在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,成为EAGL。 +因为每个窗口系统都有不同的语言,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL显示的连接。 -在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。 补充说明: -应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上到下逐像素的绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,就应用了双缓冲渲染模式,前缓冲保存着最终输出的图像,它会在屏幕上显示;而所有的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交互前缓冲和后缓冲,这样图像就立即呈现出来,之前提到的不真实感就消除了。 +应用程序使用单缓冲绘图时可能会存在图像闪烁的问题。这是因为生成的图像不是一下子被绘制出来的,而是按照从左到右,由上到下逐像素的绘制而成的。最终图像不是在瞬间显示给用户,而是通过一步一步生成的,这会导致渲染的结果很不真实。为了规避这些问题,就应用了双缓冲渲染模式,前缓冲保存着最终输出的图像,它会在屏幕上显示; +而所有的渲染指令都会在后缓冲上绘制。当所有的渲染指令执行完毕后,我们交换前缓冲和后缓冲,这样图像就立即呈现出来,刚才提到的不真实感就消除了。 要在Android平台实现OpenGL渲染,需要完成一系列的EGL操作,主要为下面几步(后面分析GLSurfaceView源码的时候也是这样来实现的): @@ -284,9 +293,9 @@ EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 #### 创建屏幕外渲染区域: EGL Pbuffer -除了可以用OpenGL ES在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 -和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。 -Pbuffer最常用于生成纹理贴图。如果你想要做的事渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。 +除了可以用OpenGL ES在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 +和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。 +Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。 创建Pbuffer和创建EGL窗口非常相似,只有少数微小的不同。为了创建Pbuffer,需要和窗口一样找到EGLConfig,并作一处修改:我们需要扩增EGL_SURFACE_TYPE的值,使其包含EGL_PBUFFER_BIT。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 63a69e62..f8c2d583 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -4,7 +4,10 @@ OpenGL ES的绘制需要有以下步骤: - 顶点输入 - 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内才去处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。 + 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。 + OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。 + OpenGL仅当3D坐标在3个轴(xyz)上都为-1.0到1.0的范围内才去处理它。 + 所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 ```glsl @@ -14,9 +17,12 @@ float vertices[] = { 0.0f, 0.5f, 0.0f }; ``` -定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 +定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。 +它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 -我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。 +我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。 +使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。 +从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。 顶点缓冲对象是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。 @@ -37,15 +43,19 @@ float vertices[] = { 每个着色器都起始于一个版本声明,这里声明的是GLSL ES 300版本,在Android中它对应的OpenGL ES版本为3.0,而GLSL ES 100版本则对应的是OpenGL ES 2.0版本。如果不写版本默认的就是100。 - 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 + 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。 + 由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 向量(Vector) -在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。 +在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。 +在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。 +注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。 - 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数中只是将position的值转换后赋值给gl_Position。由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。 +为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数中只是将position的值转换后赋值给gl_Position。 +由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。 - 这个position的值是哪里进行赋值的呢? 是通过后面java代码中的draw函数来进行赋值的。每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 +这个position的值是哪里进行赋值的呢? 是通过后面java代码中的draw函数来进行赋值的。每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 - 编译着色器 @@ -62,39 +72,44 @@ float vertices[] = { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` -在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。这三种颜色分量的不同调配可以生成超过1600万种不同的颜色! +在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。 +当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。 +这三种颜色分量的不同调配可以生成超过1600万种不同的颜色! - 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区。,然后颜色缓冲区再通过EGL窗口显示。 - -片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。声明输出变量可以使用out关键字,这里我们命名为color。 +片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。 +我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。 +之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区,然后颜色缓冲区再通过EGL窗口显示。 - 着色器程序(Shader Program Object) - 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 +着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。 +如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。 +已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 - 当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: +当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: - ```java - shaderProgram = glCreateProgram(); - glAttachShader(shaderProgram, vertexShader); - glAttachShader(shaderProgram, fragmentShader); - glLinkProgram(shaderProgram); - ``` +```java +final int shaderProgramId = GLES30.glCreateProgram(); +GLES30.glAttachShader(shaderProgramId, vertexShaderId); +GLES30.glAttachShader(shaderProgramId, fragmentShaderId); +GLES30.glLinkProgram(shaderProgramId); +``` - 链接完后需要使用glUseProgram方法,用刚创建的程序对象作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 +链接完后需要使用glUseProgram方法,用刚创建的程序对象的Id作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了: +```java +GLES30.glDeleteShader(vertexShaderId); +GLES30.glDeleteShader(fragmentShaderId); ``` -glDeleteShader(vertexShader); -glDeleteShader(fragmentShader); -``` - -现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。下面我们需要告诉OpenGL怎么做。 +现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。 +就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。下面我们需要告诉OpenGL怎么做。 - 链接顶点属性 - 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 + 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。 + 所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 我们的顶点缓冲数据会被解析成下面的样子: @@ -104,11 +119,16 @@ glDeleteShader(fragmentShader); - 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。 - 数据中第一个值在缓冲开始的位置。 - 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: +有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: ``` - glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0); - glEnableVertexAttribArray(0); +// glVertexAttribPointer是把顶点位置属性赋值给着色器程序 +// 第一个参数0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 +// 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 +// 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz +GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer); +//启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 +GLES30.glEnableVertexAttribArray(0); ``` glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们: @@ -117,33 +137,43 @@ glDeleteShader(fragmentShader); - 第二个参数指定顶点属性的大小。顶点属性是一个`vec3`,它由3个值组成,所以大小是3。 - 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中`vec*`都是由浮点数值组成的)。 - 下个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。我们把它设置为GL_FALSE。 - - 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个`GLfloat`之后,我们把步长设置为`3 * sizeof(GLfloat)`。要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - - 最后一个参数的类型是`GLvoid*`,所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。我们会在后面详细解释这个参数。 + - 第五个参数叫做步长(Stride),它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3个`GLfloat`之后,我们把步长设置为`3 * sizeof(GLfloat)`也就是3*4。 + 要注意的是由于我们知道这个数组是紧密排列的(在两个顶点属性之间没有空隙)我们也可以设置为0来让OpenGL决定具体步长是多少(只有当数值是紧密排列时才可用)。 + 一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 + - 最后一个参数的类型是数据。 -每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。 +每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。 +由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性现在会链接到它的顶点数据。 -现在我们已经定义了OpenGL该如何解释顶点数据,我们现在应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。在OpenGL中绘制一个物体,代码会像是这样: +现在我们已经定义了OpenGL该如何解释顶点数据,接着应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。 +自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。 +在OpenGL中绘制一个物体,代码会像是这样: ```java // 0. 复制顶点数组到缓冲中供OpenGL使用,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象 -glGenBuffers(1, VBO); +IntBuffer vbo = IntBuffer.allocate(1); +GLES30.glGenBuffers(1, vbo); // 使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上 -glBindBuffer(GL_ARRAY_BUFFER, VBO); +GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); // glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数 // 它的第一个参数是目标缓冲类型。 第二个参数是指定传输数据的大小(以字节为单位),用一个简单的sizeof计算出顶点数据的大小就行 // 第三个参数是我们希望发送的实际数据 // 第四个参数指定了我们希望显卡如何管理这些数据,有三种形式: GL_STATIC_DRAW(数据不会或几乎不会改变), GL_DYNAMIC_DRAW(数据会被改变很多), GL_STREAM_DRAW(数据每次绘制时都会改变) -glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); +GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * Float.BYTES, + vertexBuffer, GLES30.GL_STATIC_DRAW); // 1. 设置顶点属性指针 -glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); -glEnableVertexAttribArray(0); +GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * Float.BYTES, vertexBuffer); +GLES30.glEnableVertexAttribArray(0); // 2. 当我们渲染一个物体时要使用着色器程序 -glUseProgram(shaderProgram); +GLES30.glUseProgram(shaderProgram); // 3. 绘制物体 someOpenGLFunctionThatDrawsOurTriangle(); ``` -每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? +每当我们绘制一个物体的时候都必须重复这一过程。这看起来可能不多,但是如果有超过5个顶点属性,上百个不同物体呢(这其实并不罕见)。 +绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? -顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 +顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。 +这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。 +这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 一个顶点数组对象会储存以下这些内容: @@ -154,32 +184,35 @@ someOpenGLFunctionThatDrawsOurTriangle(); ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects.png) 创建一个VAO和创建一个VBO很类似: -```c++ -unsigned int VAO; -glGenVertexArrays(1, &VAO); +```java +IntBuffer arrays = IntBuffer.allocate(1); +GLES30.glGenVertexArrays(1, arrays); ``` -要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: -```c++ +要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。 +当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: +```java // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. // 1. 绑定VAO -glBindVertexArray(VAO); -// 2. 把顶点数组复制到缓冲中供OpenGL使用 -glBindBuffer(GL_ARRAY_BUFFER, VBO); -glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); +GLES30.glBindVertexArray(arrays.get(0)); +// 2. 把顶点数组复制到缓冲中供OpenGL使用。 注意: 这里绑定了VAO后再绑定VBO,这样他们才能关联起来 +GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); +GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, Float.BYTES, vertices, GLES30.GL_STATIC_DRAW); // 3. 设置顶点属性指针 -glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); -glEnableVertexAttribArray(0); +GLES30.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * Float.BYTES, vertexBuffer); +GLES30.glEnableVertexAttribArray(0); [...] // ..:: 绘制代码(渲染循环中) :: .. // 4. 绘制物体 -glUseProgram(shaderProgram); -glBindVertexArray(VAO); +GLES30.glUseProgram(shaderProgram); +// 解绑数据:GLES30.glBindVertexArray(0) +GLES30.glBindVertexArray(0); someOpenGLFunctionThatDrawsOurTriangle(); ``` -就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 +就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。 +注意: 先绑定VAO后再绑定VBO,这样他们才能关联起来当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 @@ -228,7 +261,7 @@ void main() { } ``` - 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出额vColor。 + 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出给vColor。 - 片段着色器(triangle_fragment_shader.glsl) @@ -246,7 +279,8 @@ void main() { fragColor = vColor; } ``` -precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 +precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。 +在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 精度限定符可以用于指定任何浮点数或整数的变量的精度。指定精度的关键字是lowp、mediump和highp。下面是一些带有精度限定符的声明示例: ``` @@ -298,8 +332,6 @@ TriangleRender的实现如下: ```java public class TriangleRender implements GLSurfaceView.Renderer { - //一个Float占用4Byte - private static final int BYTES_PER_FLOAT = 4; //三个顶点 private static final int POSITION_COMPONENT_COUNT = 3; //顶点位置缓存 @@ -329,7 +361,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { /****************2.为顶点位置及颜色申请本地内存 start************/ //将顶点数据拷贝映射到native内存中,以便OpenGL能够访问 //分配本地内存空间,每个浮点型占4字节空间;将坐标数据转换为FloatBuffer,用以传入给OpenGL ES程序 - vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * BYTES_PER_FLOAT) // 直接分配native内存 + vertexBuffer = ByteBuffer.allocateDirect(triangleCoords.length * Float.BYTES) // 直接分配native内存 .order(ByteOrder.nativeOrder()) // 和本地平台保持一致的字节序 .asFloatBuffer(); // 将底层字节映射到FloatBuffer实例,方便使用 vertexBuffer.put(triangleCoords); // 将顶点数据拷贝到native内存中 @@ -337,7 +369,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { vertexBuffer.position(0); //顶点颜色相关 - colorBuffer = ByteBuffer.allocateDirect(color.length * BYTES_PER_FLOAT) + colorBuffer = ByteBuffer.allocateDirect(color.length * Float.BYTES) .order(ByteOrder.nativeOrder()) .asFloatBuffer(); colorBuffer.put(color); @@ -379,7 +411,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { //0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 // 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 // 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz - GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); + GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * Float.BYTES, vertexBuffer); //启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 GLES30.glEnableVertexAttribArray(0); @@ -394,7 +426,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { * ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。 */ // 1是aColor在属性的位置,4是因为我们声明的颜色是4位,r、g、b、a。 - GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); + GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 4 * Float.BYTES, colorBuffer); //启用顶点颜色句柄 GLES30.glEnableVertexAttribArray(1); @@ -479,6 +511,9 @@ public class TriangleRender implements GLSurfaceView.Renderer { GLES30.glAttachShader(programId, fragmentShaderId); //链接着色器程序 GLES30.glLinkProgram(programId); + // 删除 + GLES30.glDeleteShader(vertexShaderId); + GLES30.glDeleteShader(fragmentShaderId); final int[] linkStatus = new int[1]; GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0); @@ -535,12 +570,12 @@ public class TriangleRender implements GLSurfaceView.Renderer { ``` -调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成着色器对象时,可以用glDeleteShader删除。 +调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成链接着色器对象时,可以用glDeleteShader删除。 注意,如果一个着色器连接到一个程序对象,那么调用glDeleteShader不会立即删除着色器,而是将着色器编标记为删除,在着色器不再连接到任何程序对象时,它的内存将被释放。 效果如下: - + @@ -704,7 +739,8 @@ Matrix.multiplyMM (float[] result, //接收相乘结果 -也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。为了应用投影和相机视图,我们创建一个投影矩阵和一个相机视图矩阵,并把他们应用于OpenGL渲染管道中,投影矩阵重新计算你的图形的坐标,使他们正确的映射到Android设备的屏幕,相机视图矩阵创建一个转换,它将从一个特定的位置显示对象。 +也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。 +为了应用投影和相机视图,我们创建一个投影矩阵和一个相机视图矩阵,并把他们应用于OpenGL渲染管道中,投影矩阵重新计算你的图形的坐标,使他们正确的映射到Android设备的屏幕,相机视图矩阵创建一个转换,它将从一个特定的位置显示对象。 ### 绘制等腰三角形 @@ -777,6 +813,9 @@ public class ShaderUtils { GLES30.glAttachShader(programId, fragmentShaderId); //链接着色器程序 GLES30.glLinkProgram(programId); + //删除 + GLES30.glDeleteShader(vertexShaderId); + GLES30.glDeleteShader(fragmentShaderId); final int[] linkStatus = new int[1]; GLES30.glGetProgramiv(programId, GLES30.GL_LINK_STATUS, linkStatus, 0); @@ -840,26 +879,28 @@ public class ResReadUtils { - 修改顶点着色器,增加矩阵变换(修改iso_triangle_vertex_shader.glsl),片段着色器不用修改 - ```glsl - // 声明着色器的版本,OpenGL ES 3.0版本对应的着色器语言版本是 GLSL 300 ES - #version 300 es - // 顶点着色器的顶点位置,输入一个名为vPosition的4分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 - layout (location = 0) in vec4 vPosition; - // 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 - layout (location = 1) in vec4 aColor; - // 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 - out vec4 vColor; - // 变换矩阵4*4 - uniform mat4 u_Matrix; - void main() { - // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition - gl_Position = u_Matrix * vPosition; - // gl_PointSize为Shader内置变量,为点的直径 - gl_PointSize = 10.0; - // 将输入数据aColor拷贝到vColor的变量中。 - vColor = aColor; - } - ``` +```glsl +#version 300 es +// 声明着色器的版本,OpenGL ES 3.0版本对应的着色器语言版本是 GLSL 300 ES +// 顶点着色器的顶点位置,输入一个名为vPosition的3分量向量,layout (location = 0)表示这个变量的位置是顶点属性中的第0个属性。 +layout (location = 0) in vec3 vPosition; +// 顶点着色器的顶点颜色数据,输入一个名为aColor的4分量向量,layout (location = 1)表示这个变量的位置是顶点属性中的第1个属性。 +layout (location = 1) in vec4 aColor; +// 输出一个名为vColor的4分量向量,后面输入到片段着色器中。 +out vec4 vColor; +// 变换矩阵4*4 +uniform mat4 u_Matrix; +void main() { + // 先把vec3转成vec4 + vec4 pos = vec4(vPosition.x, vPosition.y, vPosition.z, 1.0); + // gl_Position为Shader内置变量,为顶点位置,将其赋值为vPosition + gl_Position = u_Matrix * pos; + // gl_PointSize为Shader内置变量,为点的直径 + gl_PointSize = 10.0; + // 将输入数据aColor拷贝到vColor的变量中。 + vColor = aColor; +} +``` - 在GLSurfaceView.Render实现类中定义矩阵变量 @@ -1001,7 +1042,7 @@ public class IsoTriangleRender implements GLTextureView.Renderer { public boolean onDrawFrame(GL10 gl) { //把颜色缓冲区设置为我们预设的颜色 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); - //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertexBuffer中获取vPosition的护具 + //绑定vertex坐标数据,告诉OpenGL可以在缓冲区vertexBuffer中获取vPosition的数据 GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer); //启用顶点位置句柄 GLES30.glEnableVertexAttribArray(0); From c922915da76b83007dc985db663447ad3ebceece Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 9 May 2023 21:50:13 +0800 Subject: [PATCH 024/128] update opengl --- ...237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index 380db0a9..d739b69b 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -1,6 +1,7 @@ ## 6.OpenGL ES绘制矩形及圆形 -写完上面的绘制三角形的部分,手快抽筋了,为啥? 一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。为什么要这样做呢? 我们也可以通过import static来省去GLES30的写法,但我不喜欢那样。另外还弄了BufferUtil、ProjectionMatrixUtil的类: +写完上面的绘制三角形的部分,手快抽筋了,为啥? 一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。 +为什么要这样做呢? 我们也可以通过import static来省去GLES30的写法,但我不喜欢那样。另外还弄了BufferUtil、ProjectionMatrixUtil的类: ```java public class BufferUtil { From e1cf1ac93b7bc22388bfc2b7756bfa343d8fa7e9 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 10 May 2023 21:42:24 +0800 Subject: [PATCH 025/128] update opengl --- ...42\345\217\212\345\234\206\345\275\242.md" | 37 ++--- ...45\231\250\350\257\255\350\250\200GLSL.md" | 135 +++++++----------- 2 files changed, 71 insertions(+), 101 deletions(-) diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index d739b69b..a44d86be 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -1,6 +1,7 @@ ## 6.OpenGL ES绘制矩形及圆形 -写完上面的绘制三角形的部分,手快抽筋了,为啥? 一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。 +写完上面的绘制三角形的部分,手快抽筋了,为啥? +一个就是每个方法都要调用GLES30.还有一个就是我们完全可以把公共的代码再封装到一个Base类里面啊,所以我这里就抽了一个BaseGLSurfaceViewRenderer类,然后把GLES30中一些常用的方法写了一遍。 为什么要这样做呢? 我们也可以通过import static来省去GLES30的写法,但我不喜欢那样。另外还弄了BufferUtil、ProjectionMatrixUtil的类: ```java @@ -151,14 +152,14 @@ public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Rendere - 索引法 - 根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,客读性高。 + 根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,可读性高。 之前说过OpenGL ES提供的的图元单位是三角形,想要绘制其他多边形,就要利用三角形来拼成。 矩形是两个三角形,而圆形则是由很多个三角形组成,个数越多,圆越圆。 ### 元素缓冲对象(EBO) 在渲染顶点这一话题上我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合: -``` +```java float vertices[] = { // 第一个三角形 0.5f, 0.5f, 0.0f, // 右上角 @@ -172,8 +173,9 @@ float vertices[] = { ``` 可以看到,有几个顶点叠加了。我们指定了右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧? -值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引: -``` +值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。 +这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引: +```java float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 0.5f, -0.5f, 0.0f, // 右下角 @@ -181,7 +183,7 @@ float vertices[] = { -0.5f, 0.5f, 0.0f // 左上角 }; -unsigned int indices[] = { +int indices[] = { // 注意索引从0开始! // 此例的索引(0,1,2,3)就是顶点数组vertices的下标, // 这样可以由下标代表顶点组合成矩形 @@ -191,19 +193,22 @@ unsigned int indices[] = { }; ``` 你可以看到,当使用索引的时候,我们只定义了4个顶点,而不是6个。下一步我们需要创建元素缓冲对象: -``` -unsigned int EBO; -glGenBuffers(1, &EBO); +```java +//创建 ebo +GLES30.glGenBuffers(1,ebo,0) +//绑定 ebo 到上下文 +GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,ebo[0]) +//昂丁 +GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER, + indexData.capacity() * 4, + indexData, + GLES30.GL_STATIC_DRAW +) ``` 与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 -``` -glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); -glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); -``` 注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制: -``` -glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); -glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); +```java +GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP,6,GLES30.GL_UNSIGNED_INT,0) ``` 第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 28295e70..b2f3e96d 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -8,15 +8,6 @@ -在计算机图形中,两个基本数据类型组成了变换的基础: -- 向量 -- 矩阵 -这两种数据类型在OpenGL ES着色语言中也是核心,下图具体描述了着色语言中存在的机遇标量、向量和矩阵的数据类型。 - - - - - ### 顶点着色器: ``` @@ -43,7 +34,28 @@ void main() { - 采样器:代表顶点着色器使用纹理的特殊统一变量类型。 - 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。 -顶点着色器的输出在OpenGL ES 2.0中称作可变(varying)变量,但在OpenGL ES 3.0中改名为顶点着色器输出(out)变量。在图元光栅化阶段,为每个生成的片段计算顶点着色器输出值,并作为输入传递给片段着色器。 +这里需要重点讲一下上面的版本: OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关系,如果没有在着色器文件中用#version标明使用版本的时候默认使用的是OpenGL ES 2.0版本(GLSL ES 100): + +- OpenGL ES 2.0版本对应GLSL ES 100版本 +- OpenGL ES 3.0版本对应GLSL ES 300版本 + +我们这里使用的都是GLSL ES 300版本。但是GLSL ES 100 和 300版本中间有一些差异,这就是为什么我们有时候网上的一些代码编译时却报错的原因: + +- GLSL ES 300版本中in和out代替了之前的属性和变化(attribute和varying), + + OpenGL ES 3.0中将2.0的attribute改成了in,顶点着色器的varying改成out,片段着色器的varying改成了in,也就是说顶点着色器的输出就是片段着色器的输入,另外uniform跟2.0用法一样。 + +- OpenGL ES 3.0的shader中没有texture2D()和texture3D等了,全部使用texture()方法替换。 + +- 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4]; + +- 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。 + +- GL_OES_EGL_image_external被废弃 + + 当使用samplerExternalOES是,如果在\#extension GL_OES_EGL_image_external : require会报错,需要变成#extension GL_OES_EGL_image_external_essl3 : require + +- #version 300 es这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。 虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。 @@ -157,62 +169,6 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); 现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 -内建变量包括: - -- gl_Position:用于输出顶点位置的裁剪坐标。 -- gl_PointSize:用于写入以像素标示的点尺寸。 - -### 片段着色器: - -``` -#version 300 es -in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同) - -out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4 - -void main() -{ - color = vertexColor; -} -``` - - - -片段着色器为片段操作提供了通用功能的可编程方法。片段着色器的输入由以下部分组成: - -- 输入变量:光栅化单元用插值为每个片段生成的顶点着色器输出。 -- 统一变量:片段(或者顶点)着色器使用的不变的数据。 -- 采样器:代表顶点着色器使用纹理的特殊统一变量类型。 -- 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。 - -片段着色器的输出是一个或者多个片段颜色,传递到管线的逐片段操作部分。 - -内建变量: gl_FragCoord:片段着色器中的一个只读变量。这个变量保存片段的窗口相对坐标。 - -这里需要重点讲一下上面的版本: OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关系,如果没有在着色器文件中用#version标明使用版本的时候默认使用的是OpenGL ES 2.0版本(GLSL ES 100): - -- OpenGL ES 2.0版本对应GLSL ES 100版本 -- OpenGL ES 3.0版本对应GLSL ES 300版本 - -我们这里使用的都是GLSL ES 300版本。但是GLSL ES 100 和 300版本中间有一些差异,这就是为什么我们有时候网上的一些代码编译时却报错的原因: - -- GLSL ES 300版本中in和out代替了之前的属性和变化(attribute和varying), - - OpenGL ES 3.0中将2.0的attribute改成了in,顶点着色器的varying改成out,片段着色器的varying改成了in,也就是说顶点着色器的输出就是片段着色器的输入,另外uniform跟2.0用法一样。 - -- OpenGL ES 3.0的shader中没有texture2D()和texture3D等了,全部使用texture()方法替换。 - -- 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4]; - -- 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。 - -- GL_OES_EGL_image_external被废弃 - - 当使用samplerExternalOES是,如果在\#extension GL_OES_EGL_image_external : require会报错,需要变成#extension GL_OES_EGL_image_external_essl3 : require - -- #version 300 es这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。 - - ### GLSL的特点 @@ -232,20 +188,25 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 - 标量 - 标量表示的是只有大小没有方向的量,在GLSL中`标量只有bool、int和float三种`。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是`精度`,防止溢出问题。 +标量表示的是只有大小没有方向的量,在GLSL中`标量只有bool、int和float三种`。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是`精度`,防止溢出问题。 - 向量 - 向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四维向量。针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。***需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)***。向量在GPU中由硬件支持运算,比CPU快的多。 - 作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。 - 作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。 - 作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。 +向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四维向量。 +针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。***需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)***。向量在GPU中由硬件支持运算,比CPU快的多。 - 我们大部分时候使用vecn,因为float足够满足大多数要求了。 - ```glsl +作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。 + +作为位置向量时,用xyzw表示分量,xyz分别表示xyz坐标,w表示向量的模。三维坐标向量为xyz表示分量,二维向量为xy表示分量。 + +作为纹理向量时,用stpq表示分量,三维用stp表示分量,二维用st表示分量。 + +我们大部分时候使用vecn,因为float足够满足大多数要求了。 + +```glsl vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); - ``` +``` - 矩阵 @@ -388,15 +349,16 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl ### 更多 在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色: - +```java float vertices[] = { // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 }; +``` 由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1: - +```glsl #version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1 @@ -408,8 +370,9 @@ void main() gl_Position = vec4(aPos, 1.0); ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 } +``` 由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器: - +```glsl #version 330 core out vec4 FragColor; in vec3 ourColor; @@ -418,23 +381,25 @@ void main() { FragColor = vec4(ourColor, 1.0); } +``` 因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer_interleaved.png?raw=true) 知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式, -``` +```java // 位置属性 -glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); -glEnableVertexAttribArray(0); +GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 0); +GLES30.glEnableVertexAttribArray(0); // 颜色属性 -glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float))); +GLES30.glVertexAttribPointer(1, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 3* Float.BYTES); // 1是对应上面着色器代码中的位置: layout (location = 1) in vec3 aColor; 颜色变量的属性位置值为 1 -glEnableVertexAttribArray(1); -glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。 +GLES30.glEnableVertexAttribArray(1); ``` -由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。 -同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float),用字节来计算就是12字节。 +glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。 +由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。 +这使我们的步长值为6乘以float的字节数(=24字节)。 +同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是Float.BYTES,用字节来计算就是12字节。 From e37c170cbe5c539f096d2c8a05ab89ee22531800 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 10 May 2023 22:04:02 +0800 Subject: [PATCH 026/128] update opengl part --- ...55\346\224\276\350\247\206\351\242\221.md" | 4 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 58 ++-- .../9.OpenGL ES\347\272\271\347\220\206.md" | 307 +++++++++--------- 3 files changed, 187 insertions(+), 182 deletions(-) diff --git "a/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" "b/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" index 995b49c2..c1c347da 100644 --- "a/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" +++ "b/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" @@ -317,11 +317,11 @@ public class VideoPlayerRender extends BaseGLSurfaceViewRenderer { 下面是具体的效果分别是填充宽和填充高的效果: - + - + diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 67fb2790..2de853ff 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -96,14 +96,16 @@ GLES作为我们与着色器连接的工具类提供了丰富的api。了解一 ### 获取着色器程序内成员变量的id(句柄、指针) -- mPostionHandler = GLES30.glGetAttribLocation(mProgram, "aPosition"):获取着色器程序中,指定为attribute类型的变量id。 -- mMatrixHnadler = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"):获取着色器程序中,指定为uniform类型的变量id。 +```java +mPostionHandler = GLES30.glGetAttribLocation(mProgram, "aPosition"):获取着色器程序中,指定为attribute类型的变量id。 +mMatrixHnadler = GLES30.glGetUniformLocation(mProgram, "uMVPMatrix"):获取着色器程序中,指定为uniform类型的变量id。 +``` ### 向着色器传递数据 上面获取到指向着色器中相应数据成员的各个id后,就能将我们要设置的顶点数据、颜色数据等传递到着色器中了。 -``` +```java // 将最终变换矩阵传入shader程序 GLES30.glUniformMatrix4fv(muMVPMatrixHandle, 1, false, MatrixState.getFinalMatrix(), 0); // 顶点位置数据传入着色器 @@ -117,18 +119,18 @@ GLES30.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRe ### 定义顶点属性数组 - - /** - * glVertexAttribPointer()方法的参数分别为: - * index:顶点属性的索引.(这里我们的顶点位置和颜色向量在着色器中分别为0和1)layout (location = 0) in vec4 vPosition; layout (location = 1) in vec4 aColor; - * size: 指定每个通用顶点属性的元素个数。必须是1、2、3、4。此外,glvertexattribpointer接受符号常量gl_bgra。初始值为4(也就是涉及颜色的时候必为4)。 - * type:属性的元素类型。(上面都是Float所以使用GLES30.GL_FLOAT); - * normalized:转换的时候是否要经过规范化,true:是;false:直接转化; - * stride:跨距,默认是0。(由于我们将顶点位置和颜色数据分别存放没写在一个数组中,所以使用默认值0) - * ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。 - */ - GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); - +```java +/** + * glVertexAttribPointer()方法的参数分别为: + * index:顶点属性的索引.(这里我们的顶点位置和颜色向量在着色器中分别为0和1)layout (location = 0) in vec4 vPosition; layout (location = 1) in vec4 aColor; + * size: 指定每个通用顶点属性的元素个数。必须是1、2、3、4。此外,glvertexattribpointer接受符号常量gl_bgra。初始值为4(也就是涉及颜色的时候必为4)。 + * type:属性的元素类型。(上面都是Float所以使用GLES30.GL_FLOAT); + * normalized:转换的时候是否要经过规范化,true:是;false:直接转化; + * stride:跨距,默认是0。(由于我们将顶点位置和颜色数据分别存放没写在一个数组中,所以使用默认值0) + * ptr: 本地数据缓存(这里我们的是顶点的位置和颜色数据)。 + */ +GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); +``` ### 启用或禁用顶点属性数组 @@ -138,13 +140,15 @@ GLES30.glVertexAttribPointer(maTextureHandle, 2, GLES20.GL_FLOAT, false, 20, mRe Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算。 -我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例。 +我们知道OpenGl中实现图形的操作大量使用了矩阵,在OpenGL中使用的向量为列向量,我们通过利用矩阵与列向量(颜色、坐标都可看做列向量)相乘,得到一个新的列向量。 +利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。 +Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例。 - multiplyMM 两个4x4矩阵相乘,并将结果存储到第三个4x4矩阵中。 - ``` + ```java public static native void multiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset); ``` @@ -153,7 +157,7 @@ Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的 将一个4x4矩阵和一个四元素向量相乘,得到一个新的四元素向量 - ``` + ```java public static native void multiplyMV(float[] resultVec,int resultVecOffset, float[] lhsMat, int lhsMatOffset, float[] rhsVec, int rhsVecOffset); @@ -164,21 +168,29 @@ Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的 获取逆矩阵 +### 相机 顶点着色器可赋予程序员一次操作一个顶点(“按顶点”处理)的能力,片段着色器(稍后会看到)可赋予程序员一次操作一个像素(“按片段”处理)的能力,几何着色器可赋予程序员一次操作一个图元(“按图元”处理)的能力。 到目前为止,我们所接触的变换矩阵全都可以在3D空间中操作。但是,我们最终需要将3D空间或它的一部分展示在2D显示器上。为了达成这个目标,我们需要找到一个有利点。正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。 -观察3D世界需要:(a)将相机放入世界的某个位置;(b)调整相机的角度,通常需要一套它自己的直角坐标轴u、v、n(由向量U,V,N构成);(c)定义一个视体(view volume);(d)将视体内的对象投影到投影平面(projection plane)上。 +观察3D世界需要: +- 将相机放入世界的某个位置; +- 调整相机的角度,通常需要一套它自己的直角坐标轴u、v、n(由向量U,V,N构成); +- 定义一个视体(view volume);(d)将视体内的对象投影到投影平面(projection plane)上。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye.jpg?raw=true) -OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图所示。 +OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图所示: + ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye_00.jpg?raw=true) -为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置(如物体应该在由图3.12所示相机U、V、N向量定义的“相机空间”中的位置)。给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。我们通过计算它在相机空间中的位置PC实现。已知OpenGL相机位置永远固定在点(0,0,0),那么我们如何变换来实现上述功能? -![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye_01.jpg?raw=true) -当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 +为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置。 +给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。 + + +当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。 +透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 在正射投影中,平行线仍然是平行的,即不使用透视,如下图所示。正射与透视相反,在视体中的物体不因其与相机的距离而改变,可以直接投影。正射投影是一种平行投影,其中所有的投影过程都沿与投影平面垂直的方向进行。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_zheng.jpg?raw=true) diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 6be99fb8..f23fbef6 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -197,7 +197,7 @@ public class TextureUtil { 将之前颜色向量`vec4 aColor`变为了纹理向量`vec2 aTextureCoord`; - ``` + ```glsl #version 300 es layout (location = 0) in vec4 vPosition; layout (location = 1) in vec2 aTextureCoord; @@ -220,40 +220,40 @@ public class TextureUtil { 我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。 - 之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色。 +之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色。 - ``` - #version 300 es - precision mediump float; - // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为纹理单元对应的index值。 - uniform sampler2D uTextureUnit; - //接收刚才顶点着色器传入的纹理坐标(s,t) - in vec2 vTexCoord; - out vec4 vFragColor; - void main() { - // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 - vFragColor = texture(uTextureUnit,vTexCoord); - } - ``` +```glsl +#version 300 es +precision mediump float; +// 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为纹理单元对应的index值。 +uniform sampler2D uTextureUnit; +//接收刚才顶点着色器传入的纹理坐标(s,t) +in vec2 vTexCoord; +out vec4 vFragColor; +void main() { + // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 + vFragColor = texture(uTextureUnit,vTexCoord); +} +``` - render实现类 - 区别就是把前面绘制矩形时的color部分换成了纹理,其他都是一样的。 +区别就是把前面绘制矩形时的color部分换成了纹理,其他都是一样的。 - 纹理的绘制: +纹理的绘制: - ``` - //激活纹理,设置当前活动的纹理单元为单元0 - GLES30.glActiveTexture(GLES30.GL_TEXTURE0); - //绑定纹理,将纹理id绑定到当前活动的纹理单元上 - GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 将纹理单元传递片段着色器的采样器u_TextureUnit,0就代表GL_TEXTURE0, 1代表GL_TEXTURE1,以此类推 - GLES20.glUniform1i(aTextureLocation, 0); - ``` +```java +//激活纹理,设置当前活动的纹理单元为单元0 +GLES30.glActiveTexture(GLES30.GL_TEXTURE0); +//绑定纹理,将纹理id绑定到当前活动的纹理单元上 +GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); +// 将纹理单元传递片段着色器的采样器u_TextureUnit,0就代表GL_TEXTURE0, 1代表GL_TEXTURE1,以此类推 +GLES20.glUniform1i(aTextureLocation, 0); +``` - 这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 +这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 @@ -345,143 +345,136 @@ public class TextureUtil { - 单纹理单元,多次绘制 - 多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别设置这两个坐标,并绑定两次纹理,然后调用两次glDrawArrays进行绘制。 +多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别设置这两个坐标,并绑定两次纹理,然后调用两次glDrawArrays进行绘制。 - ```java - public class MultiTextureRender extends BaseGLSurfaceViewRenderer { - private final FloatBuffer vertextBuffer; - private final FloatBuffer vertextBuffer2; - private final FloatBuffer textureBuffer; - - private int textureId; - private int textureId2; - private int aPositionLocation; - private int aTextureLocation; - private int uSamplerTextureLocation; - - /** - * 坐标占用的向量个数 - */ - private static final int POSITION_COMPONENT_COUNT = 2; - // 逆时针顺序排列 - private static final float[] POINT_DATA = { - -1f, -1f, - -1f, 1f, - 1f, 1f, - 1f, -1f, - }; - - private static final float[] POINT_DATA2 = { - -0.5f, -0.5f, - -0.5f, 0.5f, - 0.5f, 0.5f, - 0.5f, -0.5f, - }; - /** - * 颜色占用的向量个数 - */ - private static final int TEXTURE_COMPONENT_COUNT = 2; - // 纹理坐标(s, t),t坐标方向和顶点y坐标反着 - private static final float[] TEXTURE_DATA = { - 0.0f, 1.0f, - 0.0f, 0.0f, - 1.0f, 0.0f, - 1.0f, 1.0f - }; - - public MultiTextureRender() { - vertextBuffer = BufferUtil.getFloatBuffer(POINT_DATA); - vertextBuffer2 = BufferUtil.getFloatBuffer(POINT_DATA2); - textureBuffer = BufferUtil.getFloatBuffer(TEXTURE_DATA); - } - - @Override - public void onSurfaceCreated(GL10 gl, EGLConfig config) { - glClearColor(1.0f, 1.0f, 1.0f, 1.0f); - handleProgram(MyApplication.getInstance(), R.raw.texture_vertex_shader, R.raw.texture_fragment_shader); - aPositionLocation = glGetAttribLocation("vPosition"); - aTextureLocation = glGetAttribLocation("aTextureCoord"); - uSamplerTextureLocation = glGetUniformLocation("uTextureUnit"); - textureId = TextureUtil.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); - textureId2 = TextureUtil.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); - } - - @Override - public void onSurfaceChanged(GL10 gl, int width, int height) { - glViewport(0, 0, width, height); - orthoM("u_Matrix", width, height); - } - - @Override - public void onDrawFrame(GL10 gl) { - glClear(GLES30.GL_COLOR_BUFFER_BIT); - glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertextBuffer); - glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); - /****************/ - // 中间这一部分代码的意思: 采样器统一变量将加载一个指定纹理绑定的纹理单元的数值, - // 例如,用数值0指定采样器表示从单元GL_TEXTURE0读取,指定数值1表示从GL_TEXTURE1读取,以此类推。 - // 激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中。 - - - // 下面这两句表示纹理如何绑定到纹理单元。设置当前活动的纹理单元为纹理单元0,并将纹理ID绑定到当前活动的纹理单元上 - glActiveTexture(GLES30.GL_TEXTURE0); - glBindTexture(GLES30.GL_TEXTURE_2D, textureId); - // 将采样器绑定到纹理单元,0就表示GLES30.GL_TEXTURE0 - glUniform1i(uSamplerTextureLocation, 0); - /****************/ - glEnableVertexAttribArray(aPositionLocation); - glEnableVertexAttribArray(aTextureLocation); - // 画第一次 - glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); - - //设置第二个纹理的坐标数据 - GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, - false, 0, vertextBuffer2); - //绑定纹理,前面已经激活了,就不用再调了 - GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId2); - // 画第二次 - glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, POINT_DATA2.length / POSITION_COMPONENT_COUNT); - glDisableVertexAttribArray(aPositionLocation); - glDisableVertexAttribArray(aTextureLocation); - } - ``` - - - -- 多纹理单元,单次绘制 +```java +public class MultiTextureRender extends BaseGLSurfaceViewRenderer { + private final FloatBuffer vertextBuffer; + private final FloatBuffer vertextBuffer2; + private final FloatBuffer textureBuffer; - OpenGL可以同时操作的纹理单元是16个,那么我们可以利用多个纹理单元来进行绘制同一个图层,从而达到目的。多纹理单元需要修改片段着色器的代码,顶点着色器不用改变,这种方式的优点是可以控制多个纹理的关系,做出复杂的效果。缺点是多个纹理单元的顶点坐标必须是一样的。 - - 片段着色器代码增加一个纹理采样器(multi2_texture_fragment_shader.glsl): - - ```glsl - #version 300 es - precision mediump float; - // 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为0 - uniform sampler2D uTextureUnit; - uniform sampler2D uTextureUnit2; - // 接收刚才顶点着色器传入的纹理坐标(s, t) - in vec2 vTexCoord; - out vec4 vFragColor; - - void main() { - // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 - vec4 texture1 = texture(uTextureUnit, vTexCoord); - vec4 texture2 = texture(uTextureUnit2, vTexCoord); - if (texture1.a != 0.0) { - vFragColor = texture1; - } else { - vFragColor = texture2; - } - } - ``` - + private int textureId; + private int textureId2; + private int aPositionLocation; + private int aTextureLocation; + private int uSamplerTextureLocation; + + /** + * 坐标占用的向量个数 + */ + private static final int POSITION_COMPONENT_COUNT = 2; + // 逆时针顺序排列 + private static final float[] POINT_DATA = { + -1f, -1f, + -1f, 1f, + 1f, 1f, + 1f, -1f, + }; + + private static final float[] POINT_DATA2 = { + -0.5f, -0.5f, + -0.5f, 0.5f, + 0.5f, 0.5f, + 0.5f, -0.5f, + }; + /** + * 颜色占用的向量个数 + */ + private static final int TEXTURE_COMPONENT_COUNT = 2; + // 纹理坐标(s, t),t坐标方向和顶点y坐标反着 + private static final float[] TEXTURE_DATA = { + 0.0f, 1.0f, + 0.0f, 0.0f, + 1.0f, 0.0f, + 1.0f, 1.0f + }; + + public MultiTextureRender() { + vertextBuffer = BufferUtil.getFloatBuffer(POINT_DATA); + vertextBuffer2 = BufferUtil.getFloatBuffer(POINT_DATA2); + textureBuffer = BufferUtil.getFloatBuffer(TEXTURE_DATA); + } + + @Override + public void onSurfaceCreated(GL10 gl, EGLConfig config) { + glClearColor(1.0f, 1.0f, 1.0f, 1.0f); + handleProgram(MyApplication.getInstance(), R.raw.texture_vertex_shader, R.raw.texture_fragment_shader); + aPositionLocation = glGetAttribLocation("vPosition"); + aTextureLocation = glGetAttribLocation("aTextureCoord"); + uSamplerTextureLocation = glGetUniformLocation("uTextureUnit"); + textureId = TextureUtil.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + textureId2 = TextureUtil.loadTexture(MyApplication.getInstance(), R.drawable.img).getTextureId(); + } + + @Override + public void onSurfaceChanged(GL10 gl, int width, int height) { + glViewport(0, 0, width, height); + orthoM("u_Matrix", width, height); + } + + @Override + public void onDrawFrame(GL10 gl) { + glClear(GLES30.GL_COLOR_BUFFER_BIT); + glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, vertextBuffer); + glVertexAttribPointer(aTextureLocation, TEXTURE_COMPONENT_COUNT, GLES30.GL_FLOAT, false, 0, textureBuffer); + /****************/ + // 中间这一部分代码的意思: 采样器统一变量将加载一个指定纹理绑定的纹理单元的数值, + // 例如,用数值0指定采样器表示从单元GL_TEXTURE0读取,指定数值1表示从GL_TEXTURE1读取,以此类推。 + // 激活纹理单元后需要把它和纹理Id绑定,然后再通过GLES30.glUniform1i()方法传递给着色器中。 + + + // 下面这两句表示纹理如何绑定到纹理单元。设置当前活动的纹理单元为纹理单元0,并将纹理ID绑定到当前活动的纹理单元上 + glActiveTexture(GLES30.GL_TEXTURE0); + glBindTexture(GLES30.GL_TEXTURE_2D, textureId); + // 将采样器绑定到纹理单元,0就表示GLES30.GL_TEXTURE0 + glUniform1i(uSamplerTextureLocation, 0); + /****************/ + glEnableVertexAttribArray(aPositionLocation); + glEnableVertexAttribArray(aTextureLocation); + // 画第一次 + glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT); + + //设置第二个纹理的坐标数据 + GLES30.glVertexAttribPointer(aPositionLocation, POSITION_COMPONENT_COUNT, GLES30.GL_FLOAT, + false, 0, vertextBuffer2); + //绑定纹理,前面已经激活了,就不用再调了 + GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId2); + // 画第二次 + glDrawArrays(GLES30.GL_TRIANGLE_FAN, 0, POINT_DATA2.length / POSITION_COMPONENT_COUNT); + glDisableVertexAttribArray(aPositionLocation); + glDisableVertexAttribArray(aTextureLocation); + } +``` +- 多纹理单元,单次绘制 - - +OpenGL可以同时操作的纹理单元是16个,那么我们可以利用多个纹理单元来进行绘制同一个图层,从而达到目的。多纹理单元需要修改片段着色器的代码,顶点着色器不用改变,这种方式的优点是可以控制多个纹理的关系,做出复杂的效果。缺点是多个纹理单元的顶点坐标必须是一样的。 + +片段着色器代码增加一个纹理采样器(multi2_texture_fragment_shader.glsl): + +```glsl +#version 300 es +precision mediump float; +// 采样器(sampler)是用于从纹理贴图读取的特殊统一变量。采样器统一变量将加载一个指定纹理绑定的纹理单元额数据,java代码里面需要把它设置为0 +uniform sampler2D uTextureUnit; +uniform sampler2D uTextureUnit2; +// 接收刚才顶点着色器传入的纹理坐标(s, t) +in vec2 vTexCoord; +out vec4 vFragColor; + +void main() { + // 100 es版本中是texture2D,texture函数会将传进来的纹理和坐标进行差值采样,输出到颜色缓冲区。 + vec4 texture1 = texture(uTextureUnit, vTexCoord); + vec4 texture2 = texture(uTextureUnit2, vTexCoord); + if (texture1.a != 0.0) { + vFragColor = texture1; + } else { + vFragColor = texture2; + } +} +``` [上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) [下一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) From 29db8a2dd7fc95dae9f3f945bf4e92192869c3db Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 10 May 2023 22:06:08 +0800 Subject: [PATCH 027/128] update opengl part --- .../OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index 27d255aa..675f4d24 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -10,7 +10,7 @@ 和前一篇文章基本一样,只是修改一下片段着色器的代码,原理就是在片段着色器中去处理颜色,让RGB三个通道的颜色取均值: -``` +```glsl #version 300 es #extension GL_OES_EGL_image_external_essl3 : require precision mediump float; From df6b256dd243f148426ab1ac4ad72db5659844fd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 May 2023 15:15:42 +0800 Subject: [PATCH 028/128] update OpenGL part --- ...273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" | 3 ++- .../OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index f8c2d583..36391f49 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -579,7 +579,8 @@ public class TriangleRender implements GLSurfaceView.Renderer { -我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为前面说到的OpenGL ES使用的是虚拟坐标导致的。如果想让绘制一个等腰三角形该怎么做呢? +我们设置的是数据来看,应该是等腰三角形,但是实际效果并不是,这是因为OpenGL假设屏幕采用均匀的方形坐标系,所以在把标准坐标系下的坐标绘制到非方形的屏幕上时,就会出现拉伸。 +如果想让绘制一个等腰三角形该怎么做呢? ### 变成等腰三角形的原理 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index f23fbef6..f780c5cc 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -23,7 +23,8 @@ ### 纹理坐标 -OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1)内,两个维度分别为S、T,所以一般称为ST纹理坐标。有些时候也叫UV坐标。纹理左边的方向性和Android上的canvas移植,都是顶点在左上角。 +OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1)内,两个维度分别为S、T,所以一般称为ST纹理坐标。有些时候也叫UV坐标。纹理左边的方向性和Android上的视图坐标系一致,都是顶点在左上角,范围为0到1之间。 + 纹理上的每个顶点与定点坐标上的顶点一一对应。如下图,左边是顶点坐标,右边是纹理坐标,只要两个坐标的ABCD定义顺序一致,就可以正常地映射出对应的图形。顶点坐标内光栅化后的每个片段,都会在纹理坐标内取得对应的颜色值。 From 6825cb1843eaee0640709df3dce5d6933d8753c8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 19 May 2023 22:39:18 +0800 Subject: [PATCH 029/128] update OpenGL --- VideoDevelopment/.DS_Store | Bin 0 -> 6148 bytes ...72\347\241\200\347\237\245\350\257\206.md" | 13 ++++ .../1.OpenGL\347\256\200\344\273\213.md" | 28 ++++++++ VideoDevelopment/OpenGL/12.FBO.md | 61 ++++++++++++++++++ .../9.OpenGL ES\347\272\271\347\220\206.md" | 31 +++++++++ 5 files changed, 133 insertions(+) create mode 100644 VideoDevelopment/.DS_Store create mode 100644 VideoDevelopment/OpenGL/12.FBO.md diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..2b7b4ca390d8c420538b442b2cba749a3acc071f GIT binary patch literal 6148 zcmeHKOH0E*5T4aqQycIj)762?%!qgZJKM3uU_Q{Z0Nh1pN z3<)6w&tLcNFHe2myub`F1OJTyI=d6F1u+CrfsXU*f&1Y%s%1ZtKKi`+QeOCJnaX5shE-yLBsZB;!l;q4+c$Kp9HN#UAVrNbX%Z_2x_W^qIJOyV~@o1|{Fl zoO<$Ye-n(F0dr=oaVCTrUN^{0K^n7D?yuj35u}|ItCMs zxPu~eD54G(ro|9C9M^5*=NL>h>TnRI`4HyK!gMG?y&a!#D|Zl%Mjn{~W}usaVLi=K z{onn)|L+ds88g5P3={(*wd3v-FeOu4`;wztYolJEl8|4bQH9`O%CR!26wjh6!9AM{ WM8{yF5m!+7M?lfQ12gca415E6(b%&9 literal 0 HcmV?d00001 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 1d617b21..54520ce4 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" @@ -266,6 +266,19 @@ U和V不是基础信号,他俩都是被正交调制的。早期的电视都是 YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 + +YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电视的兼容问题,利用了人类眼睛的生理特性(对亮度敏感,对色度不敏感),允许降低色度的带宽,降低了传输带宽。 + +在计算机系统中应用尤为广泛,利用 YUV 色彩编码模型可以降低图片数据的内存占用,YUV只需要占用RGB一般的带宽,从而提高数据处理效率。 + +另外,YUV 编码模型的图像数据一般不能直接用于显示,还需要将其转换为 RGB(RGBA) 编码模型,才能够正常显示图像。 + + +———————————————— +版权声明:本文为CSDN博主「字节流动」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 +原文链接:https://blog.csdn.net/Kennethdroid/article/details/94031821 + + YUV码流的存储格式其实与其采样的方式密切相关,主流的采样方式有四种, - YUV 4:4:4 YUV三个信道的抽样率相同,因此在生成的图像里,每个象素的三个分量信息完整(每个分量通常8比特),经过8比特量化之后,未经压缩的每个像素占用3个字节。 diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index effb8b0d..eeb9c5dc 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -88,6 +88,13 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 + OpenGLES2.0 编程中,用于绘制的顶点数组数据首先保存在 CPU 内存,在调用 glDrawArrays 或者 glDrawElements 等进行绘制时,需要将顶点数组数据从 CPU 内存拷贝到显存。 + + 但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。 + + OpenGLES3.0 VBO 和 EBO 的出现就是为了解决这个问题。 VBO 和 EBO 的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以提升渲染性能,降低内存带宽和功耗。 + + 顶点缓冲对象是OpenGL中的一个对象,就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象: ```java // 下面只生成一个vbo对象,所以vbo的容量设置为1即可 @@ -256,6 +263,27 @@ EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、 在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,成为EAGL。 因为每个窗口系统都有不同的语言,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL显示的连接。 +EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用: + +- 与设备的原生窗口系统通信; +- 查询绘图表面的可用类型和配置; +- 创建绘图表面; +- 在OpenGL ES 和其他图形渲染API之间同步渲染; +- 管理纹理贴图等渲染资源。 +OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异(Apple 提供了自己的 EGL API 的 iOS 实现,自称 EAGL)。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/egl_interface_1.png?raw=true) +上图中: +- Display(EGLDisplay) 是对实际显示设备的抽象; +- Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer(颜色缓冲区), Stencil Buffer(模板缓冲区) ,Depth Buffer(深度缓冲区); +- Context (EGLContext) 存储 OpenGL ES 绘图的一些状态信息; +在 Android 平台上开发 OpenGL ES 应用时,类 GLSurfaceView 已经为我们提供了对 Display , Surface , Context 的管理,即 GLSurfaceView 内部实现了对 EGL 的封装,可以很方便地利用接口 GLSurfaceView.Renderer 的实现,使用 OpenGL ES API 进行渲染绘制,很大程度上提升了 OpenGLES 开发的便利性。 + + + + +本地窗口相关的 API 提供了访问本地窗口系统的接口,而 EGL 可以创建渲染表面 EGLSurface ,同时提供了图形渲染上下文 EGLContext,用来进行状态管理,接下来 OpenGL ES 就可以在这个渲染表面上绘制。 + EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一个Front Frame Buffer,正常绘制的目标都是Back Frame Buffer,绘制完成后再调用eglSwapBuffer API,将绘制完毕的FrameBuffer交换到Front Frame Buffer并显示出来。 diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md new file mode 100644 index 00000000..728b6430 --- /dev/null +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -0,0 +1,61 @@ +## 12.FBO + +FBO(Frame Buffer Object)即帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO)。 + +FBO 本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了 3 种附着(Attachment),分别是颜色附着、深度附着和模板附着。 + +RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的 2D 图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作 FBO 中的颜色、深度或者模板附着。 + +使用 FBO 作为渲染目标时,首先需要为 FBO 的附着添加连接对象,如颜色附着需要连接纹理或者渲染缓冲区对象的颜色缓冲区。 + + +#### 为什么用 FBO + +默认情况下,OpenGL ES 通过绘制到窗口系统提供的帧缓冲区,然后将帧缓冲区的对应区域复制到纹理来实现渲染到纹理,但是此方法只有在纹理尺寸小于或等于帧缓冲区尺寸才有效。 + +另一种方式是通过使用连接到纹理的 pbuffer 来实现渲染到纹理,但是与上下文和窗口系统提供的可绘制表面切换开销也很大。因此,引入了帧缓冲区对象 FBO 来解决这个问题。 + +Android OpenGL ES 开发中,一般使用 GLSurfaceView 将绘制结果显示到屏幕上,然而在实际应用中,也有许多场景不需要渲染到屏幕上,如利用 GPU 在后台完成一些图像转换、缩放等耗时操作,这个时候利用 FBO 可以方便实现类似需求。 + +使用 FBO 可以让渲染操作不用再渲染到屏幕上,而是渲染到离屏 Buffer 中,然后可以使用 glReadPixels 或者 HardwareBuffer 将渲染后的图像数据读出来,从而实现在后台利用 GPU 完成对图像的处理。 + + + + +[上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index f780c5cc..c82c89d4 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -30,6 +30,37 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_texture_position.jpg) + +在 OpenGL 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 使用的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。 + +2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 + +立方图纹理是一个由 6 个单独的纹理面组成的纹理。立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标。 + +3D 纹理可以看作 2D 纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。 + + +在 OpenGLES 中,纹理映射就是通过为图元的顶点坐标指定恰当的纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上。 + +纹理映射也称为纹理贴图,简单地说就是将纹理坐标(纹理坐标系)所指定的纹理区域,映射到顶点坐标(渲染坐标系或OpenGLES 坐标系)对应的区域。 + +纹理坐标系: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/texture_st_1.png) +渲染坐标系或OpenGLES 坐标系: +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/texture_st_2.png) +4 个纹理坐标分别为 +T0(0,0),T1(0,1),T2(1,1),T3(1,0)。 +4 个纹理坐标对于的顶点坐标分别为 +V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) +由于 OpenGLES 绘制是以三角形为单位的,设置绘制的 2 个三角形为 V0V1V2 和 V0V2V3 。 +当我们调整纹理坐标的顺序顶点坐标顺序不变,如 T0T1T2T3 -> T1T2T3T0 ,绘制后将得到一个顺时针旋转 90 度的纹理贴图。所以调整纹理坐标和顶点坐标的对应关系可以实现纹理图简单的旋转。 + + + + + + + ### 文件读取 OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmap。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 From 55b4f4fe6c80cf18e44fd1ad2590c51be6f0ea9b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 20 May 2023 21:30:25 +0800 Subject: [PATCH 030/128] =?UTF-8?q?update=E2=80=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...44\350\241\214\345\244\247\345\205\250.md" | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index 77830f45..8b25dcf3 100644 --- "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -87,7 +87,7 @@ - `rmdir` - 该命令的功能是删除空目录,一个目录被删除之前必须是空的。删除某目录时也必须具有对父目录的写权限。 + 该命令的功能是删除空目录,一个目录被删除之前必须是空的。删除某目录时也必须具有对父目录的写权限。由于只能删除空目录,一般都是使用rm -f - `mv` @@ -467,6 +467,7 @@ chmod [who] [+ | - | =] [mode] 文件名 ``` chmod 751 file ``` +如果是当前root用户执行,前面需要加 sudo chmod 751 file 性能监控和优化命令 @@ -523,6 +524,43 @@ $ file .bashrc 警告 以root身份使用pkill命令时要格外小心。命令中的通配符很容易意外地将系统的重要进程终止。这可能会导致文件系统损坏。 +### Shell脚本 + +```shell +#!/bin/bash +# This line is a comment +echo "Hello World" +``` +一个shell脚本永远以#!开头,这个是一个脚本开始的标记,它是告诉系统执行这个文件需要使用某个解释器,后面的/bin/bash指明了解释器的具体位置。 +第二行 # 这里表示是一个注解 +第三行是输出Hello World + +#### 运行脚本 +脚本的运行有好几种方式: +- 在该脚本所在的目录中直接bash这个脚本,直接bash一个文件就是指定了使用Bash Shell来解释脚本内容。 +- 给该脚本加上可执行权限,然后使用./来运行,它代表运行的是当前目录下的Hello World.sh脚本, 如果采用这种方式而脚本没有可执行权限则会报错。 + ```shell + sudu chmod 777 HelloWorld.sh + ./HelloWorld.sh + ``` +- 如果不想修改权限可以使用.来运行脚本 + ```shell + . ./HelloWorld.sh + ``` +###### 声明变量: declare、typeset +这两个命令都是用来声明变量的,作用完全相同。 +很多语法严谨的语言对变量的声明都是有严格要求的,变量的使用原则是必须在使用前声明、声明时必须说明变量类型,而shell脚本中对变量声明的要求并不高,因为shell弱化了变量的类概念,所有shell又称为弱类型编程语言,声明变量时并不需要指明类型。 + + +###### 函数 +```shell +function NAME() { + ..... + return x; +} +``` +function关键字可以省略。 + ---- - 邮箱 :charon.chui@gmail.com From ede2581df0dd2b4af4f6d5e38fce9d135648ae42 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 23 May 2023 22:31:53 +0800 Subject: [PATCH 031/128] update FFmpeg --- ...77\347\224\250\346\225\231\347\250\213.md" | 14 ++++++- VideoDevelopment/.DS_Store | Bin 6148 -> 8196 bytes ...72\347\241\200\347\237\245\350\257\206.md" | 18 +++++++-- VideoDevelopment/FFmpeg/AAC.md | 22 +++++++++++ ...26\347\240\201\346\240\274\345\274\217.md" | 36 ++++++++++++++++++ 5 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 VideoDevelopment/FFmpeg/AAC.md create mode 100644 "VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" diff --git "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" index 1ade6bd3..3c2cffc6 100644 --- "a/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" +++ "b/JavaKnowledge/Vim\344\275\277\347\224\250\346\225\231\347\250\213.md" @@ -18,8 +18,11 @@ Vim使用教程 - `i` 进入`insert`模式 - `a(append)` 在光标后进行插入,直接进入`insert`模式 - `o(open a line below)` 在当前行后插入一个新行,直接进入`insert`模式 +- `O` 大写的O是在光标所在的行下面插入一个新航,直接进入编译模式 - `I` 从该行的最前面开始编辑 - `A` 从该行的最后面开始编辑 +- `s` 删除光标后的字符,从光标当前位置插入 +- `S` 删除光标所在当前行,从行首插入 - `cw` 替换从光标位置开始到该单词结束位置的所有字符,直接进入`Insert`模式 在VIM中,有相当一部分命令可以扩展为3部分: @@ -70,6 +73,7 @@ Vim使用教程 - `#X` 删除光标所在位置前的#个字符 - `dd` 删除当前行,并把删除的行存到剪贴板中 - `#dd` 从光标所在行开始删除#行。如`5dd`就是删除5行 +- `v/ctrl+v`: 使用h、j、k、l移动选择内容,然后按d删除。其中v是非列模式,ctrl+v是列模式 复制粘贴 --- @@ -81,7 +85,9 @@ Vim使用教程 - `y$` 拷贝光标至本行结束位置 - `y` 拷贝选中部分,在`Normal`模式下按`v`会进入到可视化模式,这时候可以上下移动进行选中某一部分,然后按`y`就可以复制了。 -- `p` 粘贴 +- `p` 在光标所在的位置向下开辟一行,粘贴 +- `P` 在光标所在的位置向上开辟一行,粘贴 +- 剪切: 按dd或者ndd删除,将删除的行保存到剪贴板中,然后按p/P就可以粘贴了。 替换 @@ -90,6 +96,9 @@ Vim使用教程 - `r` 替换光标所在处的字符 - `R` 替换光标所到之处的字符,直到按下`esc`键为止 - `:%s/old/new/g` 用`new`替换文件中所有的`old` +- `:%s/old/new/gc`,同上,但是每次替换需要用户确认 +- `:s/old/new/g` 光标所在行的所有old替换为new +- `:s/old/new/` 光标所在行的第一个old替换为new 撤销 --- @@ -118,7 +127,8 @@ Vim使用教程 - `:q!` 退出不保存 - `:saveas ` 另存为 - `:e filename` 打开文件 -- `:sav filename` 保存为某文件名 +- `:sav filename 保存为某文件名 +- ZZ: 命令模式使用大写ZZ直接保存并退出 diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store index 2b7b4ca390d8c420538b442b2cba749a3acc071f..d444bf3996b5031376c5e5d08fc3074f41cdbc46 100644 GIT binary patch literal 8196 zcmeHMO^?z*7=CA$WlPo%OkCyI#4Cxg%f}Ku0HR(@NUYIB*HpSC(AaheA7Vm?99{RM zm-Sb8)p%d;Ui>5b2YhFyG~FT%F=j!XAv4b~^S&+5Gwr;63jmPnz{~=q06@XYFu8@z zFAAGyb*8L&Bio1s{Q)RgU>=_ye*5@-###-7VnRtM2|bw_jdGy+>{1i(KxpDV-^I zw*UEqb}OqLP^Tj{Mc(>z4z1O^IHr8nuT;mEPAWlPhnkV|o%Z}=hcpkIe&xIC;uoe^ z{(pD=`~Q{aA%CD$Kq~P23XoV$t5wl|`VbvH6w9?$tS_*#!t5pjxeGQt9Y^HpIHLcD wA@)@?Wj-O(vzdb*Gz0kS MP3PRO > AAC> RealAudio > WMA > MP3 PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制。我们知道在现实生活中,人耳听到的声音是模拟信号,PCM就是要把声音从模拟转换成数字信号的一种技术,它的原理简单地说就是利用一个固定的频率对模拟信号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲,把这些脉冲的幅值按一定的精度进行量化,这些量化后的数值被连续地输出、传输、处理或记录到存储介质中,所有这些组成了数字音频的产生过程。 + +PCM的采集步骤分为: +模拟信号 -> 采样 -> 量化 -> 编码 -> 数字信号 --- @@ -229,7 +235,10 @@ PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制 8位代表2的8次方—256,16位则代表2的16次方—64K。比较一下,一段相同的音乐信息,16位声卡能把它分为64K个精度单位进行处理,而8位声卡只能处理256个精度单位,造成了较大的信号损失,最终的采样效果自然是无法相提并论的。 +#### 采样率 +音频的采样率要大于原声波频率的2倍,人耳能听到的最高频率为20kHz,所以为了满人耳的听觉要求,采样率至少为40kHz以上,通常为44.1kHz。 +44.1kHz就是代表1秒会采样44100次。 采样频率:是指录音设备在一秒钟内对声音信号的采样次数,采样频率越高声音的还原就越真实越自然。在当今的主流采集卡上,采样频率一般共分为22.05KHz、44.1KHz、48KHz三个等级,22.05KHz只能达到FM广播的声音品质,44.1KHz则是理论上的CD音质界限,48KHz则更加精确一些。对于高于48KHz的采样频率人耳已无法辨别出来了,所以在电脑上没有多少使用价值。 @@ -252,14 +261,17 @@ PCM是英文Pulse-code modulation的缩写,中文译名是脉冲编码调制 - 5.1声道:5.1声道已广泛运用于各类传统影院和家庭影院中,一些比较知名的声音录制压缩格式,譬如杜比`AC-3`(`Dolby Digital`)、`DTS`等都是以5.1声音系统为技术蓝本的。其实5.1声音系统来源于4.1环绕,不同之处在于它增加了一个中置单元。这个中置单元负责传送低于`80Hz`的声音信号,在欣赏影片时有利于加强人声,把对话集中在整个声场的中部,以增加整体效果。相信每一个真正体验过`DolbyAC-3`音效的朋友都会为5.1声道所折服。千万不要以为5.1已经是环绕立体声的顶峰了,更强大的7.1系统已经出现了。 它在5.1的基础上又增加了中左和中右两个发音点,以求达到更加完美的境界。由于成本比较高,没有广泛普及。 +##### 音频的码率 +码率是指一个数据流中每秒钟能通过的信息量,单位是bps(bit per second)。 +码率 = 采样率 * 采样位数 * 声道数。 YUV --- YUV(也成YCbCr)是电视系统所采用的一种颜色编码方法,他是一种亮度与色度分离的色彩格式。 - 其中Y表示明亮度(Luminance或Luma)也就是灰阶值,它是基础信号。 -- U表示色度 -- V表示浓度 +- U表示蓝色通道与亮度的差值 +- V表示红色通道与亮度的差值(为什么只有蓝色红色,没有绿色,这是因为红蓝绿加起来为1,所以1减去他俩就是绿) UV的作用是描述影像色彩及饱和度,它们用于指定像素的颜色。 U和V不是基础信号,他俩都是被正交调制的。早期的电视都是黑白的,即只有亮度值(Y),有了彩色电视之后,加入了UV两种色度,形成现在的YUV。 人眼对亮度敏感,对色度不敏感,因此减少部分UV的数据量,人眼也无法感知出来,这样就可以通过压缩UV的分辨率,在不影响观感的前提下,减少视频的体积。 @@ -399,4 +411,4 @@ PAR DAR SAR --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git a/VideoDevelopment/FFmpeg/AAC.md b/VideoDevelopment/FFmpeg/AAC.md new file mode 100644 index 00000000..635c4ab9 --- /dev/null +++ b/VideoDevelopment/FFmpeg/AAC.md @@ -0,0 +1,22 @@ +### AAC + +### 音频编码 + +原始的PCM音频数据也是非常大的数据量,因此也需要对其进行压缩编码。 +和视频编码一样,音频也有许多的编码格式,如:WAV、MP3、WMA、APE、FLAC等等,音乐发烧友应该对这些格式非常熟悉,特别是后两种无损压缩格式。 + +AAC是新一代的音频有损压缩技术,一种高压缩比的音频压缩算法。 +在MP4视频中的音频数据,大多数时候都是采用的AAC压缩格式。 + +AAC格式主要分为两种: ADIF、ADTS。 + +- ADIF:Audio Data Interchange Format。 音频数据交换格式 + 这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。这种格式常用在磁盘文件中。 + ADIF只有一个统一的头,所以必须得到所有的数据后解码。 +- ADTS: Audio Data Transport Stream。音频数据传输流 + 这种格式的特征是它是一个由同步字的比特流,解码可以在这个流中任何位置开始。 + 它的特征类似于mp3数据流格式。 + + ADTS可以在任意帧解码,它每一帧都有头信息。 + ADIF只有一个统一的头,所以必须得到所有的数据后解码。 +且这两种的header的格式也是不同的,目前一般编码后的都是ADTS格式的音频流。 diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" new file mode 100644 index 00000000..63e77d09 --- /dev/null +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" @@ -0,0 +1,36 @@ +## 音频编码格式 + +音频压缩的本质: 消除冗余数据。 + +人耳能察觉到的声音信号频率范围为20Hz ~ 20KHz,在这个频率范围以外的音频信号属于冗余信号。 +- 去除人耳听觉频率范围临界附近的值 +- 大声音附近如果有小的声音可以去除 +- 时域屏蔽效应 +- 高声附近50ms内如果声音比较小可以去掉 +- 无损压缩 + + + +### WAV + +WAV编码是在PCM数据格式的前面加上44字节,分别用来描述PCM的采样率、声道数、数据根式等信息。 +特点: 音质非常好、大量软件都支持。 +使用场景:多媒体开发的中间文件、保存音乐和音效素材等。 + +### MP3 +MP3具有不错的压缩比,使用LAME编码的中高码率的MP3文件,听感上非常接近源WAV文件。 +特点:音质在128Kbps以上表现还不错,压缩比比较高,兼容性好。 +使用场景:高比特率下对兼容性有要求的音乐欣赏。 + +### AAC +AAC是新一代的有损压缩技术,它通过一些附加编码技术(如PS、SBR)等,衍生出LC-AAC、HE-AAC、HE-AAC V2三种主要编码格式。 + +特点:在小于128kbps码率下表现优异,且多用于视频中的音频编码。 +使用场景:128Kbps码率下的音频编码,多用于视频中音频轨的编码。 + +### Ogg + +Ogg编码音质好、完全免费。 +可以用更小的码率达到更好的音质,128Kbps的Ogg比192Kbps甚至更高的MP3还要出色。但是目前媒体软件支持上还不够友好。 +特点:高中低码率下都有良好的表现,兼容性不够好,流媒体特性不支持。 +使用场景:语音聊天的音频消息场景。 From 032bed128b75dca4863ef2f6f652aef0d8849630 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 11:53:15 +0800 Subject: [PATCH 032/128] update --- ...73\347\273\237\347\256\200\344\273\213.md" | 76 +++++--------- ...13\344\270\216\347\272\277\347\250\213.md" | 1 - VideoDevelopment/.DS_Store | Bin 8196 -> 8196 bytes ...72\347\241\200\347\237\245\350\257\206.md" | 1 + ...50\345\221\275\344\273\244\350\241\214.md" | 35 ++++++- VideoDevelopment/WebRTC.md | 63 ++++++++++++ .../FLV.md" | 3 +- .../H264.md" | 97 ++++++++++++++++++ ...26\347\240\201\345\216\237\347\220\206.md" | 54 ++++++++++ .../AAC.md" | 48 +++++++++ 10 files changed, 320 insertions(+), 58 deletions(-) create mode 100644 VideoDevelopment/WebRTC.md create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/\350\247\206\351\242\221\347\274\226\347\240\201\345\216\237\347\220\206.md" create mode 100644 "VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.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 fa3aa962..52f42ab2 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" @@ -44,10 +44,9 @@ - 进程,它是虚拟化的CPU; - 地址空间,它是虚拟化的内存。 -- 并发(concurrency) -- 持久性(persistence) - -操作系统实际上做了什么:它取得CPU、内存或磁盘等物理资源(resources),并对它们进行虚拟化(virtualize)。它处理与并发(concurrency)有关的麻烦且棘手的问题。它持久地(persistently)存储文件,从而使它们长期安全 +- 并发(concurrency) +- 持久性(persistence) + 操作系统实际上做了什么:它取得CPU、内存或磁盘等物理资源(resources),并对它们进行虚拟化(virtualize)。它处理与并发(concurrency)有关的麻烦且棘手的问题。它持久地(persistently)存储文件,从而使它们长期安全 操作系统也是一种软件,但是操作系统是一种非常复杂的软件。操作系统提供了几种抽象模型: @@ -57,20 +56,12 @@ - 虚拟机:对整个操作系统的抽象 - - - - ### 以现代标准而言,一个标准PC的操作系统应该提供以下功能 - - ### 用户界面(User interface) - - -普通用户操作电脑是需要用户界面的,没有用户界面的电脑对于普通用户来说就是灾难。你能想象家里的老人或者上了年纪的人用电脑却没有鼠标的场景吗?你能想象使用黑框框来做 PPT 吗?你能想象使用黑框框来浏览网页吗? -专业的 IT 工作者有时候会使用黑框框纯粹是工作需要,在有些场景下,黑框框比用户界面更有效率一些。而类似于服务器场景的开发工作基本上是没有用户界面的,当然一直强调的是专业场景,这个世界上能流畅使用黑框框的人占总人口的比例太少太少。 +普通用户操作电脑是需要用户界面的,没有用户界面的电脑对于普通用户来说就是灾难。你能想象家里的老人或者上了年纪的人用电脑却没有鼠标的场景吗?你能想象使用黑框框来做 PPT 吗?你能想象使用黑框框来浏览网页吗? +专业的 IT 工作者有时候会使用黑框框纯粹是工作需要,在有些场景下,黑框框比用户界面更有效率一些。而类似于服务器场景的开发工作基本上是没有用户界面的,当然一直强调的是专业场景,这个世界上能流畅使用黑框框的人占总人口的比例太少太少。 用户界面这项伟大的发明诞生于施乐公司,经乔布斯和比尔盖茨商业化运作后得以让世人发现它的伟大之处。用户界面的发明就相当于简体汉字的出现一般,简体汉字的推行让中国的文盲率大幅降低,而用户界面的发明则大幅降低了计算机的使用难度,所以你很难想象现代的操作系统没有用户界面。 @@ -79,59 +70,50 @@ -先来解释一下什么是进程。进程是计算机进行资源管理以及调度的基本单位,是程序的执行实体。这种说法比较正统,或者你可以将一个进程看成是计算机正在进行的一项任务。计算机里面有很多各式各样功能的进程,那么多进程如何比较好的运行是个深奥的学问。 +先来解释一下什么是进程。进程是计算机进行资源管理以及调度的基本单位,是程序的执行实体。这种说法比较正统,或者你可以将一个进程看成是计算机正在进行的一项任务。计算机里面有很多各式各样功能的进程,那么多进程如何比较好的运行是个深奥的学问。 你可以想象一下:你打开了微信、word 文档、音乐软件,你想一边跟同事进行交流文档的内容应该如何写,一边在 word 文档敲下你的构思,然后在这个过程中你还听着音乐。在这个过程中,计算机需要将微信的网络保持连接,持续收发微信的信息,随时保存你的 word 文档到磁盘上,解码音乐流并播放出来。这一系列程序运转如何能让你产生一个假象:你以为它们是在同时运行的,但其实它们在同一时刻只有一个在运行。这就是进程管理和调度。 #### 内存管理(Memory management) -内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是称为虚拟内存,但是完全是两个不同方向的技术。 -进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 +内存是计算机很重要的一个资源,因为程序只有被加载到内存中才可以运行,此外 CPU 所需要的指令与数据也都是来自内存的。内存的并不是无限制的,它受限于硬件和寻址位数。 +但是现代操作系统会让每一个进程都觉得自己在独占整个内存,这就是虚拟内存技术。值得注意的是,这里的虚拟内存与 swap 这种虚拟内存是不一样的,虽然两个都是称为虚拟内存,但是完全是两个不同方向的技术。 +进程的运行需要分配内存,内存分配的快慢都与内存管理方式有着巨大的影响。两个不同进程对应的内存区域是不能相互访问的,操作系统必须得提供这样的保证,否则很容易出问题,比如:运行着的 dota 游戏如果可以被另外一个进程访问它的内存区域的话,那就可以直接将内存区域中的某个数值进行修改,比如将游戏中的玩家生命值变为无限,这样对手怎么打都打不死自己的英雄。例如前段时间火热的吃鸡游戏,外挂软件可以让角色在决赛圈外进行锁血,这个就是游戏内存被修改的最好示例。当然这是通过比较专业的手段来绕过操作系统的限制,这也从另外一个方面来说明,其实现在的操作系统安全性也是有很大提升空间的。 进程退出销毁时,内存的回收也是很重要的,否则很容易就会产生内存溢出,占着茅坑不拉屎,导致其他的进程都憋死。 - - #### 文件系统(File system) - - -文件系统与用户的距离很近,每个人平常在使用计算机的时候或多或少都会留下一些数据,而这些数据通常会保留在磁盘里面。磁盘如果不进行格式化的话,普通人是没法使用的。磁盘里面其实就是一些布满磁性物质的盘片,在计算机的世界里数据是 0 和 1 组成的,那对应的在磁盘里面就是磁性的正负极,也就是说计算机的一个文本数据要保存到磁盘中,那就需要将文本数据的电气化信号 0 和 1 通过磁盘翻译为磁性正负极并保存起来。 +文件系统与用户的距离很近,每个人平常在使用计算机的时候或多或少都会留下一些数据,而这些数据通常会保留在磁盘里面。磁盘如果不进行格式化的话,普通人是没法使用的。磁盘里面其实就是一些布满磁性物质的盘片,在计算机的世界里数据是 0 和 1 组成的,那对应的在磁盘里面就是磁性的正负极,也就是说计算机的一个文本数据要保存到磁盘中,那就需要将文本数据的电气化信号 0 和 1 通过磁盘翻译为磁性正负极并保存起来。 磁盘格式化的过程就是将文件系统架设到磁盘上,这样可以更好的管理磁盘的数据。你可以将磁盘未格式化之前的数据看做是一堆杂乱无章散落在地上的书,而文件系统就是一个有编排顺序的书架,格式化的过程就是将这堆书一本本按编排顺序放到书架上。这个比喻不太恰当,因为格化式操作通常来说会清掉数据,就相当于将书里面的字都清掉了,放到书架上的书里面都是空白页,所以格式化的时候请谨慎。 - - #### 网络通信(Networking) - - -我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 +我们常见的网络通信场景有:微信聊天、浏览网页、玩网络游戏等,可以说现在的操作系统如果不能上网感觉就没了灵魂。网络通信是个很复杂的过程,连很多专业的程序猿都说不清楚当他上网时网页是如何显示出来的,更别说整个网络的拓扑结构了。 整个网络通信其实是一套约定好的通信协议,很多人第一次听说协议时觉得很高级,其实没什么高级的,简单的说,类似于我们军训时当教官喊立正我们必须得做出相应动作一样,一个指令对应一个动作。由于网络太复杂了,某位哲人说过如果某样东西太复杂可以通过分层来解决,于是网络分了 5 层。有人也许会说是 7 层,那是 OSI 标准,在实际应用中一般都是 5 层。 #### 设备管理 - - -计算机上有很多设备,比如CPU、内存、网卡、声卡、显卡、硬盘等。那什么是设备管理? +计算机上有很多设备,比如CPU、内存、网卡、声卡、显卡、硬盘等。那什么是设备管理? 在计算机中除了 CPU 和内存,对于其他一切输入输出设备的管理统称为设备管理。 -计算机中的设备分为输入和输出设备。以 CPU 为中心,凡是向 CPU 输送数据的设备统称为输入设备,例如鼠标、键盘、摄像头等;同样以 CPU 为中心,凡是从 CPU 获取数据的设备统称为输出设备,如显示器等。有些设备既是输入设备也是输入设备,比如网卡等。 +计算机中的设备分为输入和输出设备。以 CPU 为中心,凡是向 CPU 输送数据的设备统称为输入设备,例如鼠标、键盘、摄像头等;同样以 CPU 为中心,凡是从 CPU 获取数据的设备统称为输出设备,如显示器等。有些设备既是输入设备也是输入设备,比如网卡等。 一个比较常见的场景是当我们将 U盘插进电脑的 USB 插孔时,电脑能实时识别出 U 盘设备,那计算机为啥能实时识别出这些设备呢?之后会有章节讨论一下这个话题。 ## 操作系统的基本特征 并发和共享是操作系统的两个最基本的特征,它们又互为存在条件。一方面,资源共享是以程序(进程)的并发执行为条件的,若系统部允许程序并发执行,自然不存在资源共享问题。另一方面,若系统部能对资源共享实施有效管理,也必将影响程序的并发执行,甚至根本无法并发执行。 - - ## 计算机的组成 ### 计算机硬件 计算机的硬件由大量的IC (Integrated Circuit,集成电路)组成。每块IC上都带有许多引脚。这些引脚有的用于输入,有的用于输出。 -从概念上讲,计算机的结构非常简单:首先布置一根总线,然后将各种硬件设备挂在总线上。所有的这些设备都有一个控制设备,外部设备都由这些控制器与CPU通信。而所有设备之间的通信均需通过总线,如果对总线做一个简单的概括,可以认为总线就是数字信号的集合,而这些信号被提供给计算机上的每块电路板: +从概念上讲,计算机的结构非常简单: +首先布置一根总线,然后将各种硬件设备挂在总线上。所有的这些设备都有一个控制设备,外部设备都由这些控制器与CPU通信。 +而所有设备之间的通信均需通过总线,如果对总线做一个简单的概括,可以认为总线就是数字信号的集合,而这些信号被提供给计算机上的每块电路板: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/computer_hardware.jpg?raw=true) @@ -163,7 +145,11 @@ ### 计算机打开电源后的执行 -在每台计算机上有一块双亲板(在政治因素影响到计算机产业之前,它们曾称为“母版”)。在双亲板上有一个称为基本输入输出系统(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。 @@ -181,28 +167,12 @@ - 用户程序的长度是多少? - 装载完用户程序后,应该跳转到哪里,即用户程序的执行入口在哪里? - 理器在上电时自动把程序计数器设置为处理器厂商设计的某个固定值,对于ARM64处理器,这个固定值是0。处理器的内存管理单元(Memory Management Unit, MMU)负责把虚拟地址转换为物理地址,ARM64处理器刚上电的时候没有开启内存管理单元,物理地址和虚拟地址相同,所以ARM64处理器到物理地址0取第一条指令。嵌入式设备通常使用NOR闪存作为只读存储器来存放引导程序。NOR闪存的容量比较小,最小读写单位是字节,程序可以直接在芯片内执行。从物理地址0开始的一段物理地址空间被分配给NOR闪存。综上所述,ARM64处理器到虚拟地址0取指令,就是到物理地址0取指令,也就是到NOR闪存的起始位置取指令。 + 机器在上电时自动把程序计数器设置为处理器厂商设计的某个固定值,对于ARM64处理器,这个固定值是0。处理器的内存管理单元(Memory Management Unit, MMU)负责把虚拟地址转换为物理地址,ARM64处理器刚上电的时候没有开启内存管理单元,物理地址和虚拟地址相同,所以ARM64处理器到物理地址0取第一条指令。嵌入式设备通常使用NOR闪存作为只读存储器来存放引导程序。NOR闪存的容量比较小,最小读写单位是字节,程序可以直接在芯片内执行。从物理地址0开始的一段物理地址空间被分配给NOR闪存。综上所述,ARM64处理器到虚拟地址0取指令,就是到物理地址0取指令,也就是到NOR闪存的起始位置取指令。 嵌入式设备通常使用U-Boot作为引导程序。U-Boot(Universal Boot Loader)是德国DENX软件工程中心开发的引导程序,是遵循GPL条款的开源项目。 U-Boot分为SPL和正常的U-Boot程序两个部分,如果想要编译为SPL,需要开启配置宏CONFIG_SPL_BUILD。SPL是“Secondary Program Loader”的简称,即第二阶段程序加载器,第二阶段是相对于处理器里面的只读存储器中的固化程序来说的,处理器启动时最先执行的是只读存储器中的固化程序。固化程序通过检测启动方式来加载第二阶段程序加载器。为什么需要第二阶段程序加载器?原因是:一些处理器内部集成的静态随机访问存储器比较小,无法装载一个完整的U-Boot镜像,此时需要第二阶段程序加载器,它主要负责初始化内存和存储设备驱动,然后把正常的U-Boot镜像从存储设备读到内存中执行。 -- 操作系统内核初始化 - - 执行内核程序,这一步所说的内核程序在上一步中指的就是”用户程序“。因为从CPU的角度来看,除了Bootloader之外的所有程序都是用户程序,只是从软件的角度来看,用户程序被分为”内核程序“和”应用程序“,而本步执行的是内核程序。 - - 内核程序初始化时执行的操作包括,初始化各种硬件,包括内存、网络接口、显示器、输入设备、然后建立各种内部数据结构,这些数据结构将用于多线程调度及内存的管理等。当内核初始化完毕后就开始运行具体的应用程序了。在一般情况下,习惯于将第一个应用程序称为”Home程序“。 - -- 执行第一个应用程序 - - 运行Home程序,比如Windows系统的桌面。之所以称为Home程序,是因为通过该程序可以方便的启动其他应用程序。而传统的Linux系统启动后第一个运行的程序一般是一个Terminal。 - - - -### Android系统启动过程 - -目前的Android系统大多运行在ARM处理器之上。ARM本身是一个公司的名称,从技术的角度来看,它又是一种微处理器内核的架构。 - -对于ARM处理器,当复位完毕后,处理器首先执行芯片上ROM中的一小块程序。这块ROM的大小一般只有几KB,该段程序就是Bootloader程序,这段程序执行时会根据处理器上一些特定引脚的高低电平状态,选择从何种物理接口上装载用户程序,比如USB口、SD卡、并口Flash等。 +- 操作系统内核初始化 执行内核程序,这一步所说的内核程序在上一步中指的就是”用户程序“。因为从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中写入新的数据,这可用于开发过程中的内核烧写、升级等。 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 6a10819b..99e31602 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" @@ -115,7 +115,6 @@ ### 进程由哪几部分组成? 进程是程序的一次运行过程,它是由程序段、数据段和进程控制块PCB组成的一个实体,其中: - - 程序段:对应程序的操作代码部分,用于描述进程所需要完成的功能。 - 数据段:对应程序执行时所需要的数据部分,包括数据,堆栈和工作区。 - 进程控制块(Process Control Block, PCB) :描述进程的基本信息和运行状态,记录了进程运行时所需要的全部信息,它是进程存在的唯一标识,与进程一一对应。所谓的创建进程和撤销进程,都是指对PCB的操作。 diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store index d444bf3996b5031376c5e5d08fc3074f41cdbc46..632c9571c556ec4c8a1e2ff3037bdc226591462c 100644 GIT binary patch delta 244 zcmZp1XmOa}&nU4mU^hRb#AF_Ux$GuJItoTc7Lzv!L~y1QC+8&P=jSj^ek0&K`KNFx zhnbN9P}R?WfRs(XC1ix5SA;d!F=~>3 zaIk;(I+#v|%}0dU7#SHRpB7ocVQOvyw8#W((d1pCj+6h1l%fi56Z^TDUE({-q;eD<&Tmai1(G=7>;lYHR}3V2-SQGPn4ji475(*(JWSOb!t&B|%dt02X>g{{R30 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 dc4465f8..4dfbb7c1 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" @@ -100,6 +100,7 @@ Presentation Time Stamp,主要用于度量解码后的视频帧什么时候被显示出来。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/gop_dts_pts.jpg) 音视频压缩编码标准 diff --git "a/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" index 4b0fe7aa..a00c00c4 100644 --- "a/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" +++ "b/VideoDevelopment/FFmpeg/2.FFmpeg\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214.md" @@ -490,9 +490,38 @@ stream字段说明: - - - +##### overlay 技术简介 +overlay 技术又称视频叠加技术。overlay 视频技术使用非常广泛,常见的例子有,电视 +屏幕右上角显示的电视台台标,以及画中画功能。 +画中画是指在一个大的视频播放窗口中还存在一个小播放窗口,两个窗口不同的视频内 +容同时播放。 +overlay 技术中涉及两个窗口,通常把较大的窗口称作背景窗口,较小的窗口称作前景 +窗口,背景窗口或前景窗口里都可以播放视频或显示图片。 +FFmpeg 中使用overlay 滤镜可实现视频叠加效果。 +overlay 滤镜说明如下: +描述:前景窗口(第二输入)覆盖在背景窗口(第一输入)的指定位置。 + + + +语法:overlay[=x:y[[:rgb={0, 1}]] +参数x 和y 是可选的,默认为0。 +参数rgb 参数也是可选的,其值为0 或1,默认为0。 +参数说明: +x 从左上角的水平坐标,默认值为0 +y 从左上角的垂直坐标,默认值为0 +rgb 值为0 表示输入颜色空间不改变,默认为0;值为1 表示将输 +入的颜色空间设置为RGB +变量说明:如下变量可用在x 和y 的表达式中 +main_w 或W 主输入(背景窗口)宽度 +main_h 或H 主输入(背景窗口)高度 +overlay_w 或w overlay 输入(前景窗口)宽度 +overlay_h 或h overlay 输入(前景窗口)高度 +//中间 +ffmpeg -i ande_302.mp4 -vf "movie=logo.png[logo]; [in][logo]overlay=W/2-w/2:H/2-h/2[out]" +-vcodec libx264 -acodec aac zzoutput8.mp4 +//右下角 +ffmpeg -i ande_302.mp4 -vf "movie=logo.png[logo]; [in][logo]overlay=W-w:H-h[out]" -vcodec +libx264 -acodec aac zzoutput7.mp4 --- diff --git a/VideoDevelopment/WebRTC.md b/VideoDevelopment/WebRTC.md new file mode 100644 index 00000000..12487397 --- /dev/null +++ b/VideoDevelopment/WebRTC.md @@ -0,0 +1,63 @@ +WebRTC +=== + +WebRTC,名称源自网页实时通信(Web Real-Time Communication)的缩写,是一个支 +持网页浏览器进行实时语音通话或视频聊天的技术,是谷歌2010 年以6820 万美元收购 +Global IP Solutions 公司而获得的一项技术。 +WebRTC 提供了实时音视频的核心技术,包括音视频的采集、编解码、网络传输、显示 +等功能,并且还支持跨平台:windows,linux,mac,android。 +虽然WebRTC 的目标是实现跨平台的Web 端实时音视频通讯,但因为核心层代码的 +Native、高品质和内聚性,开发者很容易进行除Web 平台外的移殖和应用。很长一段时间内 +WebRTC 是业界能免费得到的唯一高品质实时音视频通讯技术。 + +WebRTC 是一项在浏览器内部进行实时视频和音频通信的技术,是谷歌于2010 年以 +wwwwww..hheelllloottoonnggttoonngg..ccoom ©® +6820 万美元收购VoIP 软件开发商Global IT Solutions 公司而获得一项技术,谷歌于2011 年 +6 月3 日开源该项目。 +谷歌在官方博客中称:“我们希望让浏览器成为实时通信的创新地所在,到目前为止, +实时通信需要使用受版权保护的信号处理技术,并通过插件或下载客户端才能实现,而 +WebRTC 则允许开发人员使用HTML 和JavaScript API 来创建实时应用。” + + +1.webrtc 是什么 +浏览器为音视频获取传输提供的接口 +2.webrtc 可以做什么 +浏览器端到端的进行音视频聊天、直播、内容传输 +3.数据传输需要些什么 +IP、端口、协议 +客户端、服务端 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/webrtc_artic_1.png?raw=true) +1.绿色部分是WebRTC 核心部分(核心库) +2.紫色部分是JS 提供的API(应用层) +整体是应用层调用核心层。 +核心层,第一层C++ API +提供给外面的接口。最主要的是(PeerConnedtion 对等连接)。 +核心层,第二层Session + +上下文管理层(音视频)。 +核心层,第三层[最重要的部分] +音视频引擎:编解码;音频缓冲BUFFER 防止音频网络抖动NetEQ;回音消除;降噪;静音检 +测; +视频引擎:编解码;jitter buffer 防止视频网络抖动;图像处理增强; +传输:SRTP 加密后的RTP;多路复用;P2P(STUN+TURN+ICE) +核心层,第四层,硬件相关层 +音视频采集; 网络IO + + +WebRTC 实现了基于网页的视频会议,标准是WHATWG 协议,目的是通过浏览器提供 +简单的javascript 就可以达到实时通讯(Real-Time Communications (RTC))能力。 +WebRTC(Web Real-Time Communication)项目的最终目的主要是让Web 开发者能够基 +于浏览器(Chrome/FireFox/...)轻易快捷开发出丰富的实时多媒体应用,而无需下载安装任 +何插件,Web 开发者也无需关注多媒体的数字信号处理过程,只需编写简单的Javascript 程 +序即可实现,W3C 等组织正在制定Javascript 标准API,目前是WebRTC 1.0 版本,Draft 状 +态;另外WebRTC 还希望能够建立一个多互联网浏览器间健壮的实时通信的平台,形成开发 +者与浏览器厂商良好的生态环境。同时,Google 也希望和致力于让WebRTC 的技术成为 +HTML5 标准之一,可见Google 布局之深远。 +WebRTC 提供了视频会议的核心技术,包括音视频的采集、编解码、网络传输、显示等 +功能,并且还支持跨平台:windows,linux,mac,android。 + + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" index 0eae8fdf..507a0aad 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/FLV.md" @@ -3,7 +3,7 @@ FLV 在网络的直播与点播场景中,FLV也是一种常见的格式,FLV是Adobe发布的一种可以作为直播也可以作为点播的封装格式,其封装格式非常简单,均以FLVTAG的形式存在,并且每一个TAG都是独立存在的. -FLV封装格式是由一个文件头(FLV header)和很多tag组成的(FLV body)二进制文件。 +FLV封装格式是由一个文件头(FLV header, 9字节)和很多tag组成的(FLV body)二进制文件。 Tag中包含了音频数据以及视频数据,每个Tag又有一个preTagSize字段,标记着前面一个Tag的大小,FLV的结构如下图所示。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/flv_tag.jpg?raw=true) @@ -91,6 +91,7 @@ Script Tag通常被称为Metadata Tag,会放一些关于FLV视频和音频的 可以使用FFmpeg中的ffprobe来解析FLV文件,并且其还能够将关键帧索引的相关信息打印出来: `ffprobe -v trace -i output.flv` +[FLV分析工具: FlV Analyzer](http://flv-viewer.softwar.io/) 参考: diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" index 45aa49ec..66c7f6b6 100644 --- "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/H264.md" @@ -94,6 +94,103 @@ H.264是一种高性能的视频编解码技术。目前国际上制定视频编 目前,H.264已被广泛应用于实时视频应用中,相比以往的方案使得在同等速率下,H.264能够比H.263减小50%的码率。也就是说,用户即使是只利用 384kbit/s的带宽,就可以享受H.263下高达 768kbit/s的高质量视频服务。H.264 不但有助于节省庞大开支,还可以提高资源的使用效率,同时令达到商业质量的实时视频服务拥有更多的潜在客户。 +H264 结构中,一个视频图像编码后的数据叫做一帧,一帧由一个片(slice)或多个片组成,一个片由一个或多个宏块(MB)组成,一个宏块由16x16 的yuv数据组成。宏块作为H264 编码的基本单位。 + + +在H264 协议内定义了三种帧,分别是I 帧、B 帧与P 帧: +- I 帧就是之前所说的一个完整的图像帧,而B、帧与P 帧所对应的就是之前说 +的不编码全部图像的帧。 +- P 帧与B 帧的差别就是P 帧是参考之前的I 帧而生成的,而B 帧是参考前后图 +像帧编码生成的。 + +#### GOP(画面组,图像组) +GOP 我个人也理解为跟序列差不多意思,就是一段时间内变化不大的图像集。 +GOP 结构一般有两个数字,如M=3,N=12。 +M 指定I 帧和P 帧之间的距离,N 指定两个I 帧之间的距离。上面的M=3,N=12,GOP 结构为:IBBPBBPBBPBBI。在一个GOP 内I frame 解码不依赖任何的其它帧,p frame 解码则依赖前面的I frame 或P frame,B frame 解码依赖前最近的一个I frame 或P frame 及其后最近的一个P frame。 +#### IDR 帧(关键帧) +在编码解码中为了方便,将GOP 中首个I 帧要和其他I 帧区别开,把第一个I 帧叫IDR,这样方便控制编码和解码流程,所以IDR 帧一定是I 帧,但I 帧不一定是IDR 帧;IDR 帧的作用是立刻刷新,使错误不致传播,从IDR 帧开始算新的序列开始编码。I 帧有被跨帧参考的可能,IDR 不会。I 帧不用参考任何帧,但是之后的P 帧和B 帧是有可能参考这个I 帧之前的帧的。 +IDR 就不允许这样,例如: +IDR1 P4 B2 B3 P7 B5 B6 I10 B8 B9 P13 B11 B12 P16 B14 B15 +这里的B8 可以跨过I10 去参考P7 +------------------------------------------------------------------------ +IDR1 P4 B2 B3 P7 B5 B6 IDR8 P11 B9 B10 P14 B11 B12 +这里的B9 就只能参照IDR8 和P11,不可以参考IDR8 前面的帧 + + +H.264 引入IDR 图像是为了解码的重同步,当解码器解码到IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR 图像之后的图像永远不会使用IDR 之前的图像的数据来解码。 + + +###### 压缩方式说明 + +- Step1:分组,也就是将一系列变换不大的图像归为一个组,也就是一个序列,也可以叫GOP(画面组); +- Step2:定义帧,将每组的图像帧归分为I 帧、P 帧和B 帧三种类型; +- Step3:预测帧, 以I 帧做为基础帧,以I 帧预测P 帧,再由I 帧和P 帧预测B 帧; +- Step4:数据传输, 最后将I 帧数据与预测的差值信息进行存储和传输。 + + +H264 的主要目标是为了有高的视频压缩比和良好的网络亲和性,为了达成这两个目标,H264的解决方案是将系统框架分为两个层面,分别是视频编码层面(VCL)和网络抽象层面(NAL), + + + +### VCL NAL +视频编码中采用的如预测编码、变化量化、熵编码等编码工具主要工作在slice层或以下,这一层通常被称为视频编码层(Video Coding Layer, VCL)。 + +VCL层是对核心算法引擎、块、宏块及片的语法级别的定义,负责有效展示视频数据的内容,最终输出编码完的数据SODB。 + +相对的,在slice以上所进行的数据和算法通常称之为网络抽象层(Network Abstraction Layer, NAL)。 +NAL层定义了片级以上的语法级别(如序列参数集和图像参数集,针对网络传输),负责以网络所要求的恰当方式去格式化数据并提供头信息,以保证数据适合各种信道和存储介质熵的传输。NAL层将SODB打包成RBSP,然后加上NAL头组成一个NALU单元。 + + +SODB:数据比特串,是编码后的原始数据。 +RBSP:原始字节序列载荷,是在原始编码数据后面添加了结尾比特,一个bit 1和若干个比特 0,用于字节对齐. + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/sodb_rbsp.jpg?raw=true) +设计定义NAL层的主要意义在于提升H.264格式的视频对网络传输和数据存储的亲和性。 + + + +##### SODB RBSP EBSP 的区别 +- SODB(String of Data Bits,数据比特串):最原始,未经过处理的编码数据 +- RBSP(Raw Byte Sequence Payload,原始字节序列载荷):在SODB 的后面填加了结尾bit(RBSP trailing bits 一个bit ‘1’)若干bit ‘0’,以便字节对齐。 +- EBSP(Encapsulated Byte Sequence Payload, 扩展字节序列载荷):NALU 的起始码为0x000001 或0x00000001(起始码包括两种:3 字节(0x000001)和4 字节(0x00000001),在SPS、PPS 和Access Unit 的第一个NALU 使用4 字节起始码,其余情况均使用3 字节起始码。) + +#### h264码流结构 + +在经过编码后的H264码流是由一个个的NAL单元组成,其中SPS、PPS、IDR和SLICE是NAL单元某一类型的数据,如下: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/h264_stream_struc_1.jpg?raw=true) + +#### H264 的NAL 结构 + +在实际的网络数据传输过程中H264 的数据结构是以NALU(NAL 单元)进行传输的, +传输数据结构组成为[NALU Header]+[RBSP],如下图所示: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/nalu_1.jpg?raw=true) + +VCL 层编码后的视频帧数据,帧有可能是I/B/P 帧,这些帧也可能是属于不同的序列之中; +同一序列也还有相应的序列参数集与图片参数集; +综上所述,想要完成准确无误视频的解码,除了需要VCL 层编码出来的视频帧数据,同时还需要传输序列参数集和图像参数集等等,所以RBSP不单纯只保存I/B/P 帧的数据编码信息,还有其他信息也可能出现在里面。 +上面知道NAL 单元是作为实际视频数据传输的基本单元,NALU 头是用来标识后面RBSP 是什么类型的数据,同时记录RBSP 数据是否会被其他帧参考以及网络传输是否有错误, + + +###### NAL 头 +NAL 单元的头部是由forbidden_bit(1bit),nal_reference_bit(2bits)(优先级), +nal_unit_type(5bits)(类型)三个部分组成的,组成如图6 所示: +1、F(forbiden):禁止位,占用NAL 头的第一个位,当禁止位值为1 时表示语法错误; +2、NRI:参考级别,占用NAL 头的第二到第三个位;值越大,该NAL 越重要。 +3、Type:Nal 单元数据类型,也就是标识该NAL 单元的数据类型是哪种,占用NAL 头的第 +四到第8 个位; + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/nal_tou_1.jpg?raw=true) + + + +##### h264的结构图 +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/h264_stream_stru_2.jpg?raw=true) + +码流分析工具: elecard streameye + + + + --- diff --git "a/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/\350\247\206\351\242\221\347\274\226\347\240\201\345\216\237\347\220\206.md" "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/\350\247\206\351\242\221\347\274\226\347\240\201\345\216\237\347\220\206.md" new file mode 100644 index 00000000..94a27e30 --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\347\274\226\347\240\201/\350\247\206\351\242\221\347\274\226\347\240\201\345\216\237\347\220\206.md" @@ -0,0 +1,54 @@ +## 视频编码原理 + + +为什么巨大的原始视频可以编码成很小的视频呢?这其中的技术是什么呢? +核心思想就是去除冗余信息: +- 空间冗余: 图像相邻像素之间有较强的相关性 +- 时间冗余:视频序列的相邻图像之间内容相似 +- 编码冗余:不同像素值出现的概率不同 +- 视觉冗余:人的视觉系统对某些细节不敏感 +- 知识冗余:规律性的结构可由先验知识和背景知识得到 + +视频本质上讲是一系列图片连续快速的播放,最简单的压缩方式就是对每一帧图片进行压缩,例如比较古老的MJPEG编码就是这种编码方式,这种编码方式只是帧内编码,利用空间上的取样预测来编码。 +但是帧与帧之间因为时间的相关性,后续开发出了一些比较高级的编码器可以采用帧间编码,简单点说就是通过搜索算法选定了帧上的某些区域,然后通过计算当前帧和前后参考帧的向量差进行编码的一种形式,例如一个人滑雪,前后两帧画面的差异就是人的位置移动了。 +### 编码器中的关键技术 +- 预测: 预测编码可以用于处理视频中的时间和空间域的冗余,主要分为两大类:帧内预测和帧间预测。 + - 帧内预测: 预测值与实际值位于同一帧内,用于消除图像的空间冗余。帧内预测的特点是压缩率相对较低,然而可以独立解码,不依赖其他帧的数据。通常视频中的关键帧都采用帧内预测。 + - 帧间预测: 帧间预测的实际值位于当前帧,预测值位于参考帧,用于消除图像的时间冗余。帧间预测的压缩率高于帧内预测,然而不能独立解码,必须在获取参考帧数据之后才能重建当前帧。 + 通常在视频码流中,I帧全部使用帧内编码,P帧、B帧中的数据可能使用帧内或帧间编码。 +- 变换: 变化编码是指将给定的图像变换到另一个数据域如频域上,使得大量的信息能用较少的数据来表示,从而达到压缩的目的。目前主流的视频编码算法均属于有损编码,通过对视频造成有限而可以容忍的损失,获取相对更高的编码效率。而造成信息损失的部分即在于变换量化这一部分。在进行量化之前,首先需要将图像信息从空间域通过变换编码变换至频域,并计算其变换系数供后续的编码。 +- 量化 +- 熵编码: 视频编码中的熵编码方法主要用于消除视频信息中的统计冗余。由于信源中每一个符号的出现概率并不一致,这就导致使用同样长度的码字表示所有的符号会造成浪费。通过熵编码,针对不同的语法元素分配不同长度的码元,可以有效消除视频信息中由于符号概率导致的冗余。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/encode_decode_1.jpg?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame_inter_1.jpg?raw=true) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rongyu_kongjian_shijian_1.jpg?raw=true) + +- 对I帧的处理,是采用帧内编码方式,只利用本帧图像内的空间相关性。 + 帧内编码虽然只有空间相关性,但整个编码过程并不简单,主要如下: + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/frame_inter_2.jpg?raw=true) + - RGB转YUV:固定公式 + - 图片宏块切割:宏块16*16 + - DCT:离散余弦变换 + - 量化:取样 + - ZigZag:扫描 + - DPCM:差值脉冲编码调制 + - RLE:游程编码 + - 霍夫曼编码 + - 算数编码 +- 对P帧的处理,采用帧间编码(前向运动估计),同时利用空间和时间上的相关性。 +简单来说,采用运动补偿(motion compensation)算法来去掉冗余信息。 + +##### 编解码中的关键流程 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/encode_decode_1.jpg?raw=true) + + + + + + + + + + diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" new file mode 100644 index 00000000..58f79f82 --- /dev/null +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" @@ -0,0 +1,48 @@ +## AAC + +AAC 是高级音频编码(Advanced Audio Coding)的缩写,出现于1997 年,最 +初是基于MPEG-2 的音频编码技术,目的是取代MP3 格式。2000 年,MPEG-4 +标准出台,AAC 重新集成了其它技术(PS,SBR),为区别于传统的MPEG-2 AAC, +故含有SBR 或PS 特性的AAC 又称为MPEG-4 AAC。 +AAC 是新一代的音频有损压缩技术,它通过一些附加的编码技术(比如PS,SBR +等),衍生出了LC-AAC,HE-AAC,HE-AACv2 三种主要的编码。其中LC-AAC 就是比 +较传统的AAC,相对而言,主要用于中高码率(>=80Kbps),HE-AAC(相当于AAC+SBR) +主要用于中低码(<=80Kbps),而新近推出的HE-AACv2(相当于AAC+SBR+PS)主要用 +于低码率(<=48Kbps)。事实上大部分编码器设成<=48Kbps 自动启用PS 技术, +而>48Kbps 就不加PS,就相当于普通的HE-AAC。 + +#### AAC编码特点 +(1). AAC 是一种高压缩比的音频压缩算法,但它的压缩比要远超过较老的音频压 +缩算法,如AC-3、MP3 等。并且其质量可以同未压缩的CD 音质相媲美。 +(2). 同其他类似的音频编码算法一样,AAC 也是采用了变换编码算法,但AAC +使用了分辨率更高的滤波器组,因此它可以达到更高的压缩比。 +(3). AAC 使用了临时噪声重整、后向自适应线性预测、联合立体声技术和量化哈 +夫曼编码等最新技术,这些新技术的使用都使压缩比得到进一步的提高。 +(4). AAC 支持更多种采样率和比特率、支持1 个到48 个音轨、支持多达15 个低 +频音轨、具有多种语言的兼容能力、还有多达15 个内嵌数据流。 +(5). AAC 支持更宽的声音频率范围,最高可达到96kHz,最低可达8KHz,远宽于 +MP3 的16KHz-48kHz 的范围。 +(6). 不同于MP3 及WMA,AAC 几乎不损失声音频率中的甚高、甚低频率成分, +并且比WMA 在频谱结构上更接近于原始音频,因而声音的保真度更好。专业评 +测中表明,AAC 比WMA 声音更清晰,而且更接近原音。 +(7). AAC 采用优化的算法达到了更高的解码效率,解码时只需较少的处理能力。 + + +#### AAC 音频文件格式 + +AAC 的音频文件格式有:ADIF, ADTS +- ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是 +可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码, +即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。 +- ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是 +一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于 +mp3 数据流格式。 + + +简单说,ADTS 可以在任意帧解码,也就是说它每一帧都有头信息。ADIF 只有一个统一的头,所以必须得到所有的数据后解码。这两种的header 的格式也是 +不同的,一般编码后的和抽取出的都是ADTS 格式的音频流。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/AAC_ADIF_ADTS_1.jpg?raw=true) + + From 3770c74c57db6d9c91dade79e670f9c0f3dfb99e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 11:59:52 +0800 Subject: [PATCH 033/128] update --- ...42\221\345\237\272\347\241\200\347\237\245\350\257\206.md" | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 4dfbb7c1..b6a15a82 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" @@ -370,8 +370,10 @@ PAR DAR SAR [常见流媒体协议](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E6%B5%81%E5%AA%92%E4%BD%93%E5%8D%8F%E8%AE%AE/%E6%B5%81%E5%AA%92%E4%BD%93%E9%80%9A%E4%BF%A1%E5%8D%8F%E8%AE%AE.md) +##### 硬件加速 - +硬件加速(Hardware acceleration)就是利用硬件模块来替代软件算法以充分利用硬件固有的快速特性。硬件加速通常比软件算法的效率要高。 +将2D、3D图形计算相关工作交给GPU处理,从而释放CPU的压力,也是属于硬件加速的一种。 From e97ed7b4a6f9454d8b1fdcc43ab4e5c535904d36 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 12:52:24 +0800 Subject: [PATCH 034/128] update OS part --- ...13\344\270\216\347\272\277\347\250\213.md" | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 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 99e31602..355f1329 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" @@ -228,7 +228,8 @@ 人们最常使用的通信手段就是对白。对白的特点就是一方发出声音,另一方接收声音。而声音的传递则通过空气(当面或无线交谈)、线缆(有线电话)进行传递。类似,进程对白就是一个进程发出某种数据信息,另外一方接收数据信息,而这些数据信息通过一片共享的存储空间进行传递。在这种方式下,一个进程向这片存储空间的一端写入信息,另一个进程从存储空间的另外一端读取信息。这看上去像什么?管道。管道所占的空间既可以是内存,也可以是磁盘。就像两人对白的媒介可以是空气,也可以是线缆一样。要创建一个管道,一个进程只需调用管道创建的系统调用即可。该系统调用所做的事情就是在某种存储介质上划出一片空间,赋给其中一个进程写的权利,另一个进程读的权利即可。 - 从根本上说,管道是一个线性字节数组,类似文件,可以使用文件读写的方式进行访问。但却不是文件。因为通过文件系统看不到管道的存在。另外,我们前面说了,管道可以设在内存里,而文件很少设在内存里 + 从根本上说,管道是一个线性字节数组(实为内核缓冲区),是一种伪文件的方式,类似文件,可以使用文件读写的方式进行访问。但却不是文件。因为通过文件系统看不到管道的存在。另外,我们前面说了,管道可以设在内存里,而文件很少设在内存里。 +管道的原理:管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。 在两个进程之间,可以建立一个通道,一个进程向这个通道里写入字节流,另一个进程从这个管道中读取字节流。管道是同步的,当进程尝试从空管道读取数据时,该进程会被阻塞,直到有可用数据为止。发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,二者利用管道进行通信,管道是单向的、先进先出的,它把一个进程的输出和另一个进程的输入连接在一起。一个进程(写进程)在管道的尾部写入数据,另一个进程(读进程)从管道的头部读出数据。每次只有一个进程能够真正地进入管道,其他的只能等待。 @@ -238,7 +239,22 @@ 入先出队列 FIFO 通常被称为 `命名管道(Named Pipes)`,命名管道的工作方式与常规管道非常相似,但是确实有一些明显的区别。未命名的管道没有备份文件:操作系统负责维护内存中的缓冲区,用来将字节从写入器传输到读取器。一旦写入或者输出终止的话,缓冲区将被回收,传输的数据会丢失。相比之下,命名管道具有支持文件和独特 API ,命名管道在文件系统中作为设备的专用文件存在。当所有的进程通信完成后,命名管道将保留在文件系统中以备后用。命名管道具有严格的 FIFO 行为,写入的第一个字节是读取的第一个字节,写入的第二个字节是读取的第二个字节,依此类推。 - + 管道的局限性: + - 数据自己读不能自己写 + - 数据一旦被读走,便不在管道中存在,不可反复读取。 + - 由于管道采用半双工通信(数据只能一个方向写,另一个方向读)方式。因此,数据只能在一个方向上流动。 + - 只能在有公共祖先的进程间使用管道。 + +linux创建管道: +```c +int fd[2]; +int ret = pipe(fd); +if (ret == -1) { + perror("pip error"); + exit(1); +} + +``` - Socket套接字 From 836247beccf934d48d9f1be1eb807534d1817661 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 13:09:25 +0800 Subject: [PATCH 035/128] update OS part --- ...13\344\270\216\347\272\277\347\250\213.md" | 84 ++++++++++++++++++- 1 file changed, 80 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 355f1329..c4ebb267 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" @@ -245,17 +245,68 @@ - 由于管道采用半双工通信(数据只能一个方向写,另一个方向读)方式。因此,数据只能在一个方向上流动。 - 只能在有公共祖先的进程间使用管道。 + +创建管道: +```c: + int pipe(int pipefd[2]); 成功:0;失败:-1,设置errno +``` + +函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。 +管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。 +1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。 +2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。 +3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。 + linux创建管道: ```c -int fd[2]; -int ret = pipe(fd); -if (ret == -1) { - perror("pip error"); +#include +#include +#include +#include +#include + +void sys_err(const char *str) +{ + perror(str); exit(1); } +int main(void) +{ + pid_t pid; + char buf[1024]; + int fd[2]; + char *p = "test for pipe\n"; + + if (pipe(fd) == -1) + sys_err("pipe"); + + pid = fork(); + if (pid < 0) { + sys_err("fork err"); + } else if (pid == 0) { + close(fd[1]); + int len = read(fd[0], buf, sizeof(buf)); + write(STDOUT_FILENO, buf, len); + close(fd[0]); + } else { + close(fd[0]); + write(fd[1], p, strlen(p)); + wait(NULL); + close(fd[1]); + } + + return 0; +} ``` + +管道的优劣 + 优点:简单,相比信号,套接字实现进程间通信,简单很多。 + 缺点:1. 只能单向通信,双向通信需建立两个管道。 + 2. 只能用于父子、兄弟进程(有共同祖先)间通信。该问题后来使用fifo有名管道解决。 + + - Socket套接字 套接字的功能非常强大,可以支持不同层面、不同应用、跨网络的通信。使用套接字进行通信需要双方均创建一个套接字,其中一方作为服务器方,另外一方作为客户方。服务器方必须先创建一个服务区套接字,然后在该套接字上进行监听,等待远方的连接请求。欲与服务器通信的客户则创建一个客户套接字,然后向服务区套接字发送连接请求。服务器套接字在收到连接请求后,将在服务器方机器上创建一个客户套接字,与远方的客户机上的客户套接字形成点到点的通信通道。之后,客户方和服务器方就可以通过send和recv命令在这个创建的套接字通道上进行交流了。 @@ -288,6 +339,31 @@ if (ret == -1) { 共享内存是进程通信中最快的一种方法,因为数据不需要在进程间复制,而可以直接映射到各个进程的地址空间中。在共享内存中要注意的一个问题是:当多个进程对共享内存区域进行访问时,要注意这些进程之间的同步问题。例如,如果server正在向共享内存区域中写数据,那么client进程就不能访问这些数据,直到server全部写完之后,client才可以访问。为了实现进程间的同步问题,通常使用前面介绍过的信号量来实现这一目标。一个共享内存段可以由一个进程创建,然后由任意共享这一内存段的进程对它进行读写。当进程间需要通信时,一个进程可以创建一个共享内存段,然后需要通信的各个进程就可以在信号量的控制下保持同步,在这里交换数据,完成通信。 + +- FIFO + +FIFO +FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据。 + FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。 +创建方式: +1. 命令:mkfifo 管道名 + 2. 库函数:int mkfifo(const char *pathname, mode_t mode); 成功:0; 失败:-1 + 一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。 + + +- 存储映射I/O +       存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。 +       使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。 +总结:使用mmap时务必注意以下事项: +1.     创建映射区的过程中,隐含着一次对映射文件的读操作。 +2.     当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。 +3.     映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。 +4.     特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!   mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。 +5.     munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。 +6.     如果文件偏移量必须为4K的整数倍 +7.     mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。 + + - Message Queue消息传递系统/消息队列 消息队列是一列具有头和尾的消息排列。新来的消息放在队列尾部,而读取消息则从队列头部开始,消息队列乍一看,这不是管道吗?一头读、一头写?没错。这的确看上去像管道,但它不是管道。首先,它无需固定的读写进程,任何进程都可以读写(当然是有权限的进程)。其次,它可以同时支持多个进程,多个进程可以读写消息队列。即所谓的多对多,而不是管道的点对点。另外,消息队列只在内存中实现。最后,它并不是只在UNIX和类UNIX操作系统中实现。几乎所有主流操作系统都支持消息队列。 From ad0a42339b44d8dddd0079ac4ba9134f475508da Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 14:36:26 +0800 Subject: [PATCH 036/128] update OS --- ...3\347\250\213\344\270\216\347\272\277\347\250\213.md" | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 c4ebb267..4fea41c6 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" @@ -315,7 +315,14 @@ int main(void) - Signals信号 - 管道和套接字虽然提供了丰富的通信语义,并且也得到了广泛应用,但它们也存在某些缺点,并且在某些时候,这两种通信机制会显得很不好用。首先,如果使用管道和套接字方式来通信,必须事先在通信的进程间建立连接(创建管道或套接字),这需要消耗系统资源。其次,通信是自愿的。即一方虽然可以随意向管道或套接字发送信息,但对方却可以选择接收的时机。即使对方对此充耳不闻,你也奈何不得。再次,由于建立连接消耗时间,一旦建立,我们就想进行尽可能多的通信。而如果通信的信息量微小,如我们只是想通知一个进程某件事情的发生,则用管道和套接字就有点“杀鸡用牛刀”的味道,效率十分低下。因此,我们需要一种不同的机制来处理如下通信需求:想迫使一方对我们的通信立即做出回应。我们不想事先建立任何连接,而是临时突然觉得需要与某个进程通信。传输的信息量微小,使用管道或套接字不划算。应付上述需求,我们使用的是信号(signal)。那么信号是什么呢?在计算机里,信号就是一个内核对象,或者说是一个内核数据结构。发送方将该数据结构的内容填好,并指明该信号的目标进程后,发出特定的软件中断。操作系统接收到特定的中断请求后,知道是有进程要发送信号,于是到特定的内核数据结构里查找信号接收方,并进行通知。接到通知的进程则对信号进行相应处理。信号非常类似我们生活当中的电报。如果你想给某人发一封电报,就拟好电文,将报文和收报人的信息都交给电报公司。电报公司则将电报发送到收报人所在地的邮局(中断),并通知收报人来取电报。发报时无需收报人事先知道,更无需进行任何协调。如果对方选择不对信号做出响应,则将被操作系统终止运行。 + 管道和套接字虽然提供了丰富的通信语义,并且也得到了广泛应用,但它们也存在某些缺点,并且在某些时候,这两种通信机制会显得很不好用。首先,如果使用管道和套接字方式来通信,必须事先在通信的进程间建立连接(创建管道或套接字),这需要消耗系统资源。其次,通信是自愿的。即一方虽然可以随意向管道或套接字发送信息,但对方却可以选择接收的时机。即使对方对此充耳不闻,你也奈何不得。再次,由于建立连接消耗时间,一旦建立,我们就想进行尽可能多的通信。而如果通信的信息量微小,如我们只是想通知一个进程某件事情的发生,则用管道和套接字就有点“杀鸡用牛刀”的味道,效率十分低下。因此,我们需要一种不同的机制来处理如下通信需求:想迫使一方对我们的通信立即做出回应。我们不想事先建立任何连接,而是临时突然觉得需要与某个进程通信。传输的信息量微小,使用管道或套接字不划算。应付上述需求,我们使用的是信号(signal)。 + +那么信号是什么呢?在计算机里,信号就是一个内核对象,或者说是一个内核数据结构。发送方将该数据结构的内容填好,并指明该信号的目标进程后,发出特定的软件中断。操作系统接收到特定的中断请求后,知道是有进程要发送信号,于是到特定的内核数据结构里查找信号接收方,并进行通知。接到通知的进程则对信号进行相应处理。信号非常类似我们生活当中的电报。如果你想给某人发一封电报,就拟好电文,将报文和收报人的信息都交给电报公司。电报公司则将电报发送到收报人所在地的邮局(中断),并通知收报人来取电报。发报时无需收报人事先知道,更无需进行任何协调。如果对方选择不对信号做出响应,则将被操作系统终止运行。 + +A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,去处理信号,处理完毕再继续执行。与硬件中断类似——异步模式。但信号是软件层面上实现的中断,早期常被称为“软中断”。 + +信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。 +每个进程收到的所有信号,都是由内核负责发送的,内核处理。 From 0488ccc390b6018a15272f0ce396c8c3d838d868 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 25 May 2023 21:54:14 +0800 Subject: [PATCH 037/128] update OS part --- ...44\350\241\214\345\244\247\345\205\250.md" | 4 +++ ...13\344\270\216\347\272\277\347\250\213.md" | 12 +++++++- ...05\345\255\230\347\256\241\347\220\206.md" | 2 +- VideoDevelopment/.DS_Store | Bin 8196 -> 6148 bytes ...47\220\206_NAT\347\251\277\351\200\217.md" | 27 +++++++++++------- 5 files changed, 33 insertions(+), 12 deletions(-) diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index 8b25dcf3..05a700fc 100644 --- "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -526,6 +526,10 @@ $ file .bashrc ### Shell脚本 +Shell的作用就是解释执行用户的命令,用户输入一条命令,Shell就解释执行一条,这种方式称为交互式,Shell还有一种执行命令的方式称为批处理,用户事先写一个Shell脚本,其中有很多条命令,让Shell一次把这些命令执行完,而不是一条一条的敲命令。 +Shell脚本和编程语言很类似,也有变量和流程控制语句,但Shell脚本是解释执行的,不需要编译,Shell程序从脚本中一行一行读取并执行这些命令,相当于一个用户把脚本中的命令一行一行敲到Shell提示符下执行。 + + ```shell #!/bin/bash # This line is a comment 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 4fea41c6..71260d02 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" @@ -336,6 +336,7 @@ A给B发送信号,B收到信号之前执行自己的代码,收到信号后 操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。 + 信号效率非常高,但是可携带的数据有限,只能写到一个标志位,无法携带其他数据。 - 信号量 信号量(semaphore)是由荷兰人E.W.Dijkstra在20世纪60年代所构思出的一种程序设计构造。其原型来源于铁路的运行:在一条单轨铁路上,任何时候只能有一列列车行驶在上面。而管理这条铁路的系统就是信号量。任何一列火车必须等到表明铁路可以行驶的信号后才能进入轨道。当一列列车进入单轨运行后,需要将信号改为禁止进入,从而防止别的火车同时进入轨道。而当列车驶出单轨后,则需要将信号变回允许进入状态。这很像以前的旗语,在计算机里,信号量实际上就是一个简单整数。一个进程在信号变为0或者1的情况下推进,并且将信号变为1或0来防止别的进程推进。当进程完成任务后,则将信号再改变为0或1,从而允许其他进程执行。需要注意的是,信号量不只是一种通信机制,更是一种同步机制。 @@ -358,7 +359,7 @@ FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于 一旦使用mkfifo创建了一个FIFO,就可以使用open打开它,常见的文件I/O函数都可用于fifo。如:close、read、write、unlink等。 -- 存储映射I/O +- 存储映射        存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。        使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。 总结:使用mmap时务必注意以下事项: @@ -370,6 +371,15 @@ FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于 6.     如果文件偏移量必须为4K的整数倍 7.     mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。 +- 匿名映射 + + 使用映射区完成文件读写操作十分方便,父子进程间通信比较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。可以直接使用匿名映射来代替。其实linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。 +需要借助标志位参数来指定: +使用MAP_ANONYMOUS或MAP_ANON,例如: +```c +int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); +// 4是随意举例,该位置表示大小,可依据实际需要填写 +``` - Message Queue消息传递系统/消息队列 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 9caa1e04..0f00ea4f 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" @@ -402,7 +402,7 @@ TLB通常由CPU制造商提供,但TLB的更换算法则有可能由操作系 -## 内存映射 +## 内存映射(共享内存) 进程通过一个系统调用(mmap)将一个文件(或部分)映射到其虚拟地址空间的一部分,访问这个文件就像访问内存中的一个大数组,而不是对文件进行读写。 diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store index 632c9571c556ec4c8a1e2ff3037bdc226591462c..21e4004cc82413cdd66fa1c136dac602a5f414d2 100644 GIT binary patch delta 221 zcmZp1XfcprU|?W$DortDU=RQ@Ie-{Mvv5r;6q~50$jGrVU^g=($7Df4naLUg4IHKh z2098R=BAS`3ffP8FCaNtS5R=WyHF21Lfmn(w6HW6LjZ#jLqJkPy8dJb;RX&vLt~IW zLy$ghRwPMHkvYhQI!xvj<>4s{F3QWv&r1h6ZepSOVs;J=L1rKa2n4u+ge%B}8wsYDNes239w*Jz?O$aV=3mtDe#m=GdIwI{vQ zzrwTeUhiJ~NBR%=-puS~SxPUEY*S~*%x`%gGy8r6^LPsYu*%5F0;B*y!%mpo!QqTX zdg=>pLrTb*p+A5N9k7m1KmY#zb-`K*6a$I@#eiZ!F`yW@2n=A(rlu{)`$DTm#eibq zzhr>U2OT?MN#aZ(KRR&WB>-X!#k}D))&bl{mspZG6Ube0PEkED>B?k@!Gt^NbuvdR zNt_83?!bgQFu5|56$<04qo2;rft3WRQ8AzxSY&`|_bX5a2Q26Z>-Rbsf6VcqXWFib zHb2dt-5CrsnZImfGqx4q*0!}B?TyiP$HuTR91SXs{%d;o+;wf!yKa~-I?ZuCdGn#` z3>zJ%+2;-&tBEEDFFTIqjw|lSu?F1N5DG1!CF;r4WRlP4(t0j;Z<^L8#at$>=XVR! zX+pbo=kEPy^>=pHaleR=m^>EXYAD2`^Ww7xhqj6LOU_G4cyII_+jUxKLw0d^jIS>A zpb4$02`UgNxomDlg63ihk}{`QAfm=eaNr3%jhcwPy=+?F&1P06A`nw3-mPx=)wh~q z)Q)~c9qpR7JrI$%>G%PMfH*`!oF42QuumuL2leYwlaMbK(t5VIzv`9;V+ztR7i@@# z7&(2s{eeYI!rp!!4}kot2Y?@sM*y94zAzm8@#*CX-iyZzp1uSzPiVKY#-ZEokx({Y z*LAB^nIE-Z^&6GxwUaX11oYOq`d)ka+#%AvwbzN8VzLy~|BseG|6k{lYA(fqV&H-? zz+zRSTEUy5Q~#_+wYHA^Ep}>THxtNRaNy-QA}_}g{XY!xT1QjzDM_3OL9)1{CKH8xchz%zpVNHAL3)@wEzGB diff --git "a/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" index 43bcfc89..0b3bd000 100644 --- "a/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" +++ "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" @@ -1,14 +1,11 @@ P2P原理_NAT穿透 === - - ### NAT NAT(Network Address Translation,网络地址转换),也叫做网络掩蔽或者IP掩蔽。NAT是一种网络地址翻译技术,主要是将内部的私有IP地址(private IP)转换成可以在公网使用的公网IP(public IP)。 - 网络地址转换,就是替换IP报文头部的地址信息。NAT通常部署在一个组织的网络出口位置,通过将内部网络IP地址替换为出口的IP地址提供公网可达性和上层协议的连接能力。那么,什么是内部网络IP地址? [RFC1918](https://datatracker.ietf.org/doc/rfc1918/)规定了三个保留地址段落:10.0.0.0-10.255.255.255;172.16.0.0-172.31.255.255;192.168.0.0-192.168.255.255。这三个范围分别处于A,B,C类的地址段,不向特定的用户分配,被IANA作为私有地址保留。这些地址可以在任何组织或企业内部使用,和其他Internet地址的区别就是,仅能在内部使用,不能作为全球路由地址。这就是说,出了组织的管理范围这些地址就不再有意义,无论是作为源地址,还是目的地址。对于一个封闭的组织,如果其网络不连接到Internet,就可以使用这些地址而不用向IANA提出申请,而在内部的路由管理和报文传递方式与其他网络没有差异。 @@ -33,18 +30,28 @@ NAT(Network Address Translation,网络地址转换),也叫做网络掩 简单的背景了解过后,下面介绍下NAT实现的主要方式,以及NAT都有哪些类型。 - - ### NAT的实现方式 - - - - 静态NAT: 就是静态地址转换。是指一个公网IP对应一个私有IP,是一对一的转换,同时注意,这里只进行了IP转换,而没有进行端口的转换。 - NAPT: 端口多路复用技术。与静态NAT的差别是,NAPT不但要转换IP地址,还要进行传输层的端口转换。具体的表现形式就是,对外只有一个公网IP,通过端口来区别不同私有IP主机的数据。 - +### NAT映射 + +表内记录了私有ip和公有ip的对应关系,例如: +``` +192.168.1.35:8000 <--> 123.24.56.78:10000 +.... +``` + +### NAT打洞 +正常两个人发送数据,例如通过QQ聊天,目前的步骤是: +- A使用私有ip经过NAT映射转换为公有IP后将数据发送给腾讯 +- 腾讯将数据转发给B用户 +那后续能不能直接A和B进行链接,这样可以提高效率: +- 直接通讯存在一个问题,路由器内部有一个保护支持,对于陌生的IP第一次发的数据包,路由器会丢弃或屏蔽掉,以此来防止陌生网络进行攻击。 +- 所以要想能够让B的路由器直接接受到A发送的数据,那就必须保证B的路由器要熟悉A的IP地址,这样B才能够进行接收。而A和B首次登录QQ的时候腾讯已经将自己的ip返回给A和B了,所以A和B的路由中都认识腾讯的IP了,这样A和B都能接受腾讯发送的数据包。 +- 那如何让A和B进行直接通信,这里就是腾讯所有的事情,腾讯就通过腾讯的公网IP对AB之间进行了打洞的操作,将对方的IP都下发给对应的AB用户,这样就把他俩形成了一个私有的通路,这个操作就是打洞。 @@ -68,4 +75,4 @@ NAT(Network Address Translation,网络地址转换),也叫做网络掩 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 4bdc2a2d941697ea0e78bf24cbbf9f397fa25d3f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 26 May 2023 10:31:04 +0800 Subject: [PATCH 038/128] update shell part --- ...44\350\241\214\345\244\247\345\205\250.md" | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" index 05a700fc..8609744e 100644 --- "a/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" +++ "b/JavaKnowledge/\345\270\270\347\224\250\345\221\275\344\273\244\350\241\214\345\244\247\345\205\250.md" @@ -565,6 +565,29 @@ function NAME() { ``` function关键字可以省略。 +###### 命令代换 + +```shell +DATE=`date` +echo $DATE +``` +由“`”反引号括起来的也是一条命令,Shell先执行该命令,然后将输出结果立刻代换到当前命令行中,也可以用$()表示: +```shell +DATE = $(date) +``` + + +####### 输入输出 +echo: 显示文本行或变量,或者把字符串输入到文件 + +###### tee +tee 命令把结果输出到标准输出,另一个副本输出到相应文件中。 + + + + + + ---- - 邮箱 :charon.chui@gmail.com From 750893c06bc1446b0a2625ae83e965a811dc6bd8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 29 May 2023 21:55:17 +0800 Subject: [PATCH 039/128] update ffmpeg part --- VideoDevelopment/.DS_Store | Bin 6148 -> 0 bytes .../1.FFmpeg\347\256\200\344\273\213.md" | 2 +- ...57\345\242\203\351\205\215\347\275\256.md" | 206 ++++++++++++++++++ ...70\345\277\203\345\212\237\350\203\275.md" | 145 ++++++++++++ VideoDevelopment/FFmpeg/AAC.md | 22 -- .../AAC.md" | 25 ++- 6 files changed, 369 insertions(+), 31 deletions(-) delete mode 100644 VideoDevelopment/.DS_Store create mode 100644 "VideoDevelopment/FFmpeg/4.\345\274\200\345\217\221\347\216\257\345\242\203\351\205\215\347\275\256.md" create mode 100644 "VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" delete mode 100644 VideoDevelopment/FFmpeg/AAC.md diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store deleted file mode 100644 index 21e4004cc82413cdd66fa1c136dac602a5f414d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKOH0E*5T4aqQy`oQ{ShnhA0FnS;pc2N0aCk?^PueAYYKELl z+=GJ;-reif)BSB%w4TrmXa;^81GIKWU=>2}pad=J*8%(4t(A*zz+H@aaEpOouB6j1 ztgk;d5Fa!KjbY=+KIJvLlCM+|XKv^lpp-XQ-KsU zOH#eHt2pYlCh9RN3B^ScMG6k89Seh6@g%Ac^qFKJS_&hHID?{p1OyFw& + +using namespace std; + +#include + + +int main(int argc, const char * argv[]) { + // insert code here... + std::cout << "Hello, World!\n"; + cout << "avcodec_configuration : " << avcodec_configuration() << endl; + return 0; +} +``` +运行结果: +``` +Hello, World! +avcodec_configuration : --prefix=/usr/local/Cellar/ffmpeg/6.0 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gnutls --enable-gpl --enable-libaom --enable-libaribb24 --enable-libbluray --enable-libdav1d --enable-libmp3lame --enable-libopus --enable-librav1e --enable-librist --enable-librubberband --enable-libsnappy --enable-libsrt --enable-libsvtav1 --enable-libtesseract --enable-libtheora --enable-libvidstab --enable-libvmaf --enable-libvorbis --enable-libvpx --enable-libwebp --enable-libx264 --enable-libx265 --enable-libxml2 --enable-libxvid --enable-lzma --enable-libfontconfig --enable-libfreetype --enable-frei0r --enable-libass --enable-libopencore-amrnb --enable-libopencore-amrwb --enable-libopenjpeg --enable-libspeex --enable-libsoxr --enable-libzmq --enable-libzimg --disable-libjack --disable-indev=jack --enable-videotoolbox +Program ended with exit code: 0 +``` + +### Android Studio配置FFmpeg开发环境 +### 下载ffmpeg源码 +下载ffmpeg的源码 +官网地址:http://www.ffmpeg.org/download.html +里面有Linux、macOS、Windows系统的版本,根据自己的需要下载对应的版本。下载完成后把源码放在你喜欢的路径,如:/Users/ccc/ffmpeg + +### 修改FFmpeg目录下的configuration配置文件: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_source_code_1.png?raw=true) + +然后搜索Toolchain options部分,增加: +``` +--cross-prefix-clang=PREFIX use PREFIX for compilation tools [$cross_prefix_clang] +``` +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_source_code_2.png?raw=true) + +再搜索CMDLINE_SET,也增加上面的自定义参数配置: + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_sourcecode_3.png?raw=true) + +最后再搜索set_default target_os,进行一下修改: +``` +4436 set_default target_os +4437 if test "$target_os" = android; then +4438 cc_default="clang" +4439 # 增长这一行 +4440 cxx_default="clang++" +4441 fi +4442 +4443 ar_default="${cross_prefix}${ar_default}" +4444 # 修改这一行 +4445 cc_default="${cross_prefix_clang}${cc_default}" +4446 # 修改这一行 +4447 cxx_default="${cross_prefix_clang}${cxx_default}" +4448 nm_default="${cross_prefix}${nm_default}" +``` + +这样修改之后咱们在编译时就可使用咱们新增的cross_prefix_clang参数了,而后其余的如nm,ar,ranlib,strip则继续使用cross_prefix参数。 + +为什么要这样改? +首先,需要先了解编译库的流程。 +①编写自定义的编译脚本 +②脚本根据各个配置项,去寻找对应的库工具路径 +③生成动态链接库 +而上面的修改,目的就是为了第二步能正常找到对应的库工具。配置中的nm,ar,ranlib,strip这些的前缀,是arm-linux-androideabi-,而cc,cxx这两个是arm-linux-androideabi21-这样的前缀,而且他们放的位置也不一样。所以增加一个自定义的配置项,用于指向不同的目录。 + +#### 编写编译脚本 +在ffmepg的文件夹中新建一个文件夹build-android,里面再新建一个build-android.sh文件。 + +```shell +#!/bin/bash +#NDK路径 +export NDK=/Users/xxxx/Library/Android/sdk/ndk/21.0.6113669 +#CPU类型 +export CPU=armv8-a +# 目标Android版本 +API=21 +#架构类型 +export ARCH=arm +export SYSROOT=$NDK/toolchains/llvm/prebuilt/darwin-x86_64/sysroot +#交叉编译工具链 +export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/darwin-x86_64 +#编译成功后,存放的路径 +export PREFIX=./build-android/dist + +cd .. #回到ffmpeg根目录 + +echo "start configure" +./configure \ +--prefix=$PREFIX \ +--enable-shared \ +--enable-cross-compile \ +--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ +--cross-prefix-clang=$TOOLCHAIN/bin/armv7a-linux-androideabi21- \ +--target-os=android --arch=$ARCH --cpu=$CPU \ +--enable-gpl --enable-version3 \ +--disable-programs --disable-ffmpeg --disable-ffplay --disable-ffprobe \ +--disable-doc --disable-htmlpages --disable-manpages --disable-podpages --disable-txtpages \ +--disable-postproc \ +--disable-debug \ +--sysroot=$SYSROOT \ +--extra-cflags="-I$SYSROOT/usr/include" \ +--extra-ldflags="-L$SYSROOT/usr/lib" \ +--enable-small \ +--enable-jni \ +--enable-mediacodec +--disable-everything \ +--enable-decoder=hevc --enable-decoder=h264 --enable-decoder=aac \ +--enable-parser=h264 --enable-parser=hevc --enable-parser=aac \ +--enable-demuxer=flv --enable-demuxer=mov --enable-demuxer=avi --enable-demuxer=mpegts \ +--enable-protocol=file --enable-protocol=hls + +echo "make clean start" +make clean +echo "make start" +make -j4 +echo "make finished" +make install +echo "make install finished" +``` + +cd到ffmpeg一级目录,然后执行以下的命令 +``` +chmod 777 build_android.sh //改变脚本权限,使其能够运行,然后 + +./build_android.sh //运行脚本 +``` + +等编译完成后,进入目录就能看到生成的库: + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_sourcecode_5.png?raw=true) + + + +还有一个问题,就是能不能不修改configure文件? +其实也是可以,只需要明确指定cc和cxx的路径就行。 + +--cc=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang \ +--cxx=$TOOLCHAIN/bin/armv7a-linux-androideabi21-clang++ \ +--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \ +脚本部分配置项解释 +1.—prefix:指定编译输出的文件路径 +2.—target-os:指定目标操作系统 +3.--disable-static:禁止生成静态库 +4.—disable-programs:禁止生成ffplay、ffmpeg等可执行文件 +5.—disable-doc:禁止生成文档 +6.—enable-shared:生成动态链接库 +7.enable-cross-compile:开启交叉编译(跨平台编译) +8.make -j12:定义用几个CPU编译 + + +到这里为止,就已经编译出我们需要的so库了,之后就是怎么在android项目中使用了。 + +### Android编译方式2 + +[ffmpeg-android-maker](https://github.com/Javernaut/ffmpeg-android-maker)项目已经把下载、编译都给写好了,我们要做的就是下载下来后, +环境变量中配置ANDROID_SDK_HOME和ANDROID_NDK_HOME: +``` + export ANDROID_SDK_HOME=/Users/xuchuanren/Library/Android/sdk + export ANDROID_NDK_HOME=/Users/xuchuanren/Library/Android/sdk/ndk/23.1.77796 20 +``` +然后运行进入目录执行: +``` +sh ffmpeg-android-maker.sh +``` +它会自动下载并编译android上可用的so,结果为: + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_source_generate_2.png?raw=true) + +#### 项目集成 +新建项目,增加C++支持。手动创建jniLibs文件夹 +然后将上一步生成的所有文件(库文件和头文件)复制到jniLibs文件夹下面 +最终目录结构: + + diff --git "a/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" "b/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" new file mode 100644 index 00000000..e7549020 --- /dev/null +++ "b/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" @@ -0,0 +1,145 @@ +## 5. FFmpeg核心功能 + +FFmpeg共有8个可信开发库: + +- avutil + 包含一些公共的工具函数,主要有数学函数、字符串操作、内存管理相关、数据结构相关、错误码及错误处理、日志输出、其他复制信息比如密钥、哈希值、宏、库版本、常量等。 + +- avformat + 用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能,包含demuxers和muxer库 +- avcodec + 用于各种类型声音/图像编解码 +- avfilter + 在多媒体处理中,filter的意思是被编码到输出文件之前用来修改输入文件内容的一个软件工具 +- avdevice + FFmpeg中有一个和多媒体设备交互的类库:Libavdevice。使用这个库可以读取电脑(或其他设备) 的多媒体设备的数据,或者输出数据到指定的多媒体设备上 +- postproc + 用于后期效果处理 +- swresample + libswresample库功能主要包括高度优化的音频重采样、rematrixing和样本格式转换操作 +- swscale + 用于视频场景比例缩放、色彩映射转换 + + +### FFmpeg重要结构体 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_struct_import.png?raw=true) + + +FFmpeg中的结构体有非常多,其中重要的结构体大概可以分以下几类: + +###### 1.解协议(http,rtsp,rtmp,mms,file,tcp,udp ...) + +AVIOContext,URLProtocol,URLContext主要存储视音频使用的协议的类型以及状态。URLProtocol存储输入视音频使用的封装格式。每种协议都对应一个URLProtocol结构。(注意:FFMPEG中文件也被当做一种协议“file”)。 + +###### 2.解封装(flv,rmvb,mp4) + +AVFormatContext主要存储视音频封装格式中包含的信息;AVInputFormat存储输入视音频使用的封装格式。每种视音频封装格式都对应一个AVInputFormat 结构。 + +###### 3.解码(h264,mpeg2,aac,mp3) + +每个AVStream存储一个视频/音频流的相关数据;每个AVStream对应一个AVCodecContext,存储该视频/音频流使用解码方式的相关数据;每个AVCodecContext中对应一个AVCodec,包含该视频/音频对应的解码器。每种解码器都对应一个AVCodec结构。 + +###### 4.存数据 + +视频的话,每个结构一般是存一帧;音频可能有好几帧 + +解码前数据:AVPacket + +解码后数据:AVFrame + +### FFmpeg 4.x解码流程 + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ffmpeg_stream_11.png?raw=true) + + +FFMpeg 中主要数据结构存在包含关系,如下标题显示的就是包含层级的关系。 + +AVFormatContext ->AVStream-> AVCodecContext -> AVCodec,其中后者是前者的数据成员。 +AVFormatContext 是一个贯穿始终的数据结构,很多函数都用到它作为参数,是输入输 +出相关信息的一个容器。 +主要成员如下: +1. AVInputFormat 和AVOutputFormat,同一时间只能存在一个。当播放视频时AVInputFormat +生效,录制视频时则AVOutputFormat 生效。 +2. AVStream 是继AVFormatContext 之后第二个贯穿始终的数据结构,它保存于数据流相关 +的编解码器、数据段等信息,还包含“流”这个概念中的一些信息。 + +- AVCodecContext 保存AVCodec 指针和与codec 相关的数据。 + +在AVStream 初始化后,AVCodecContext 的初始化时Codec 使用中最重要的一环。 +AVCodecContext 中的codec_type,codec_id 二个变量对于encoder/decoder 的匹配来说, +最为重要。 +AVCodecContext 中有两个成员:AVCodec,AVFrame。 + +- AVCodec 记录了所要使用的Codec 的信息并有5 个函数:init,encoder,close,decode,flush +来完成编解码工作。 +- AVFrame 中主要包饭了编码后的帧信息。 +``` +typedef struct AVFrame { +FF_COMMON_FRAME +} AVFrame; +``` +其中FF_COMMON_FRAME 是以宏出现的,由于编码过程中AVFrame 中的数据是要经常存取 +的,为了加速,采取这样的代码手段。 + + +### API介绍 + +FFmpeg中奖编码帧及未编码帧均称作frame,这里为了方便,将编码帧称为packet,未编码帧称为frame。 +原始图片(yuv、rgb)或声音(pcm流)都是未压缩的,未编码的。 +H264/H265, aac/ac3/mp3:都是压缩的,编码的。 + +- avformat_open_input() +这个函数会打开输入媒体文件,读取文件头,将文件格式信息存储在第一个参数AVFormatContext中。 + +- avformat_find_stream_info() +这个函数会读取一段视频文件数据并尝试解码,将取到的流信息填入AVFormatContext.streams中。 +AVFormatContext.streams是一个指针数组,熟读大小是AVFormatContext.nb_streams。 + +- av_read_frame +本函数用于解复用过程。 +本函数将存储在输入文件中的数据分割为多个packet,每次调用将得到一个packet。packet +可能是视频帧、音频帧或其他数据,解码器只会解码视频帧或音频帧,非音视频数据并不会 +被扔掉、从而能向解码器提供尽可能多的信息。 +对于视频来说,一个packet 只包含一个视频帧; + +对于音频来说,若是帧长固定的格式则一个packet 可包含整数个音频帧,若是帧长可 +变的格式则一个packet 只包含一个音频帧。 +读取到的packet 每次使用完之后应调用av_packet_unref(AVPacket *pkt)清空packet。否则会 +造成内存泄露。 + +- av_write_frame() +本函数用于复用过程,将packet 写入输出媒体。 +packet 交织是指:不同流的packet 在输出媒体文件中应严格按照packet 中dts 递增的顺序交 +错存放。 +本函数直接将packet 写入复用器(muxer),不会缓存或记录任何packet。本函数不负责不同 +流的packet 交织问题。由调用者负责。 +如果调用者不愿处理packet 交织问题,应调用av_interleaved_write_frame()替代本函数。 +- av_interleaved_write_frame() +本函数用于复用过程,将packet 写入输出媒体。 +本函数将按需在内部缓存packet,从而确保输出媒体中不同流的packet 能按照dts 增长的顺 +序正确交织。 +- avio_open() +创建并初始化一个AVIOContext,用于访问输出媒体文件。 +- avformat_write_header() +向输出文件写入文件头信息。 +- av_write_trailer() +向输出文件写入文件尾信息。 + + +### 解封装demuxer +1.分配解复用器上下文avformat_alloc_context +2.根据url 打开本地文件或网络流avformat_open_input +3.读取媒体的部分数据包以获取码流信息avformat_find_stream_info +4.读取码流信息:循环处理 +4.1 从文件中读取数据包av_read_frame +4.2 定位文件avformat_seek_file 或av_seek_frame +5.关闭解复用器avformat_close_input + + + +### 转封装 +从一种视频容器转成另一种视频容器 +所谓的封装格式转换, 就是在AVI , FLV , MKV , MP4 这些格式之间转换( 对 +应.avi,.flv,.mkv,.mp4 文件)。需要注意的是,本程序并不进行视音频的编码和解码工作。 +而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式 +的文件。 diff --git a/VideoDevelopment/FFmpeg/AAC.md b/VideoDevelopment/FFmpeg/AAC.md deleted file mode 100644 index 635c4ab9..00000000 --- a/VideoDevelopment/FFmpeg/AAC.md +++ /dev/null @@ -1,22 +0,0 @@ -### AAC - -### 音频编码 - -原始的PCM音频数据也是非常大的数据量,因此也需要对其进行压缩编码。 -和视频编码一样,音频也有许多的编码格式,如:WAV、MP3、WMA、APE、FLAC等等,音乐发烧友应该对这些格式非常熟悉,特别是后两种无损压缩格式。 - -AAC是新一代的音频有损压缩技术,一种高压缩比的音频压缩算法。 -在MP4视频中的音频数据,大多数时候都是采用的AAC压缩格式。 - -AAC格式主要分为两种: ADIF、ADTS。 - -- ADIF:Audio Data Interchange Format。 音频数据交换格式 - 这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。这种格式常用在磁盘文件中。 - ADIF只有一个统一的头,所以必须得到所有的数据后解码。 -- ADTS: Audio Data Transport Stream。音频数据传输流 - 这种格式的特征是它是一个由同步字的比特流,解码可以在这个流中任何位置开始。 - 它的特征类似于mp3数据流格式。 - - ADTS可以在任意帧解码,它每一帧都有头信息。 - ADIF只有一个统一的头,所以必须得到所有的数据后解码。 -且这两种的header的格式也是不同的,目前一般编码后的都是ADTS格式的音频流。 diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" index 58f79f82..e55d26c6 100644 --- "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/AAC.md" @@ -28,16 +28,25 @@ MP3 的16KHz-48kHz 的范围。 (7). AAC 采用优化的算法达到了更高的解码效率,解码时只需较少的处理能力。 -#### AAC 音频文件格式 -AAC 的音频文件格式有:ADIF, ADTS -- ADIF:Audio Data Interchange Format 音频数据交换格式。这种格式的特征是 -可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码, -即它的解码必须在明确定义的开始处进行。故这种格式常用在磁盘文件中。 -- ADTS:Audio Data Transport Stream 音频数据传输流。这种格式的特征是它是 -一个有同步字的比特流,解码可以在这个流中任何位置开始。它的特征类似于 -mp3 数据流格式。 +原始的PCM音频数据也是非常大的数据量,因此也需要对其进行压缩编码。 +和视频编码一样,音频也有许多的编码格式,如:WAV、MP3、WMA、APE、FLAC等等,音乐发烧友应该对这些格式非常熟悉,特别是后两种无损压缩格式。 +AAC是新一代的音频有损压缩技术,一种高压缩比的音频压缩算法。 +在MP4视频中的音频数据,大多数时候都是采用的AAC压缩格式。 + +AAC格式主要分为两种: ADIF、ADTS。 + +- ADIF:Audio Data Interchange Format。 音频数据交换格式 + 这种格式的特征是可以确定的找到这个音频数据的开始,不需进行在音频数据流中间开始的解码,即它的解码必须在明确定义的开始处进行。这种格式常用在磁盘文件中。 + ADIF只有一个统一的头,所以必须得到所有的数据后解码。 +- ADTS: Audio Data Transport Stream。音频数据传输流 + 这种格式的特征是它是一个由同步字的比特流,解码可以在这个流中任何位置开始。 + 它的特征类似于mp3数据流格式。 + + ADTS可以在任意帧解码,它每一帧都有头信息。 + ADIF只有一个统一的头,所以必须得到所有的数据后解码。 +且这两种的header的格式也是不同的,目前一般编码后的都是ADTS格式的音频流。 简单说,ADTS 可以在任意帧解码,也就是说它每一帧都有头信息。ADIF 只有一个统一的头,所以必须得到所有的数据后解码。这两种的header 的格式也是 不同的,一般编码后的和抽取出的都是ADTS 格式的音频流。 From dbc9126da8c04f84a8d75b0a859209e8ac150bb5 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 1 Jun 2023 10:52:06 +0800 Subject: [PATCH 040/128] update FFmpeg part --- ...70\345\277\203\345\212\237\350\203\275.md" | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git "a/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" "b/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" index e7549020..7a942f32 100644 --- "a/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" +++ "b/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" @@ -143,3 +143,62 @@ packet 交织是指:不同流的packet 在输出媒体文件中应严格按照 应.avi,.flv,.mkv,.mp4 文件)。需要注意的是,本程序并不进行视音频的编码和解码工作。 而是直接将视音频压缩码流从一种封装格式文件中获取出来然后打包成另外一种封装格式 的文件。 + +### 读取视频MetaData信息 +```c++ +#if defined(__cplusplus) +extern "C" +{ +#endif +#include "libavcodec/avcodec.h" +#include "libavutil/avutil.h" +#include "libavutil/file.h" +#include "libavcodec/avcodec.h" +#include "libavformat/avformat.h" +#include "libavformat/avio.h" + +#if defined(__cplusplus) +} +#endif + +#include +using namespace std; + + +int main(int argc, const char * argv[]) { + avformat_network_init(); + AVFormatContext *avFmtCtx = NULL; + char *url = "http://vjs.zencdn.net/v/oceans.mp4"; + int ret = avformat_open_input(&avFmtCtx, url, NULL, NULL); + if (ret < 0) { + cout << "error"; + return 0; + } + avformat_find_stream_info(avFmtCtx, NULL); + av_dump_format(avFmtCtx, 0, url, 0); + avformat_close_input(&avFmtCtx); + return 0; +} +``` +运行结果: +``` +Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'http://vjs.zencdn.net/v/oceans.mp4': + Metadata: + major_brand : isom + minor_version : 1 + compatible_brands: isomavc1 + creation_time : 2013-05-03T22:51:07.000000Z + Duration: 00:00:46.61, start: 0.000000, bitrate: 3949 kb/s + Stream #0:0[0x1](und): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p(progressive), 960x400 [SAR 1:1 DAR 12:5], 3859 kb/s, 23.98 fps, 23.98 tbr, 24k tbn (default) + Metadata: + creation_time : 2013-05-03T22:50:47.000000Z + handler_name : GPAC ISO Video Handler + vendor_id : [0][0][0][0] + Stream #0:1[0x2](und): Audio: aac (LC) (mp4a / 0x6134706D), 48000 Hz, stereo, fltp, 92 kb/s (default) + Metadata: + creation_time : 2013-05-03T22:51:07.000000Z + handler_name : GPAC ISO Audio Handler + vendor_id : [0][0][0][0] +``` + + From 61993dbaf251d9226df125c6ef2db00b6cf93eff Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 7 Jun 2023 17:25:59 +0800 Subject: [PATCH 041/128] update Audio Encode --- ...47\273\237\347\256\200\344\273\213.md.swp" | Bin 49152 -> 0 bytes README.md | 20 ++++++++++- .../9.OpenGL ES\347\272\271\347\220\206.md" | 5 +++ .../PCM.md" | 32 ++++++++++++++++++ .../WAV.md" | 12 +++++++ 5 files changed, 68 insertions(+), 1 deletion(-) delete mode 100644 "OperatingSystem/.1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md.swp" create mode 100644 "VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/PCM.md" create mode 100644 "VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/WAV.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.swp" "b/OperatingSystem/.1.\346\223\215\344\275\234\347\263\273\347\273\237\347\256\200\344\273\213.md.swp" deleted file mode 100644 index 60c2cbee68d88f536c7213cd55151f2b89666cdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 49152 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 diff --git a/README.md b/README.md index 890fcd12..32c75424 100644 --- a/README.md +++ b/README.md @@ -80,9 +80,15 @@ Android学习笔记 - [视频封装格式][256] - [M3U8][321] - [视频编码][257] + - [视频编码原理][331] - [AV1][258] - [H264][259] - [H265][260] + - [音频编码][335] + - [音频编码格式][336] + - [AAC][337] + - [PCM][338] + - [WAV][339] - [关键帧][227] - [CDN及PCDN][228] - [P2P技术][229] @@ -100,12 +106,15 @@ Android学习笔记 - [9.OpenGL ES纹理][240] - [10.GLSurfaceView+MediaPlayer播放视频][241] - [11.OpenGL ES滤镜][242] + - [12.FBO][332] - [弹幕][243] - [Android弹幕实现][244] - [FFmpeg][322] - [1.FFmpeg简介][323] - [2.FFmpeg常用命令行][324] - - [1.FFmpeg切片][325] + - [3.FFmpeg切片][325] + - [4.开发环境配置][333] + - [5. FFmpeg核心功能][334] - [操作系统][263] - [1.操作系统简介][264] - [2.进程与线程][265] @@ -694,6 +703,15 @@ Android学习笔记 [328]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/1.%E9%9F%B3%E8%A7%86%E9%A2%91%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.md "1.音视频基础知识" [329]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/2.%E7%B3%BB%E7%BB%9F%E6%92%AD%E6%94%BE%E5%99%A8MediaPlayer.md "2.系统播放器MediaPlayer" [330]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/11.%E6%92%AD%E6%94%BE%E7%BB%84%E4%BB%B6%E5%B0%81%E8%A3%85.md "11.播放器组件封装" +[331]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81/%E8%A7%86%E9%A2%91%E7%BC%96%E7%A0%81%E5%8E%9F%E7%90%86.md "视频编码原理" +[332]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md "12.FBO" +[333]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/4.%E5%BC%80%E5%8F%91%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE.md "4.开发环境配置" +[334]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/5.%20FFmpeg%E6%A0%B8%E5%BF%83%E5%8A%9F%E8%83%BD.md "5. FFmpeg核心功能" +[335]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "音频编码" +[336]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F.md "音频编码格式" +[337]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/AAC.md "AAC" +[338]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "PCM" +[339]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "WAV" diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index c82c89d4..e86877ad 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -507,6 +507,11 @@ void main() { } } ``` +相关功能介绍: +- 窗口,Window:主窗口,用于界面显示 +- 渲染,Render:GPU,硬件加速 +- 纹理,texture:存储描述信息 +- 画布,surface:内存中的一块地址,存储像素数据 [上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) [下一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/PCM.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/PCM.md" new file mode 100644 index 00000000..32a18811 --- /dev/null +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/PCM.md" @@ -0,0 +1,32 @@ +## PCM + +PCM(Pulse Code Modulation),脉冲编码调制。人耳听到的是模拟信号,PCM是把声音从模拟信号转化为数字信号的技术。 + +原理是用一个固定的频率对模拟信号进行采样,采样后的信号在波形上看就像一串连续的幅值不一的脉冲(脉搏似的短暂起伏的电冲击),把这些脉冲的幅值按一定精度进行量化,这些量化后的数值被连续的输出、传输、处理或记录到存储介质中,所有这些组成了数字音频的产生过程(抽样、量化、编码三个过程)。 + +### 声音的三要素 + +- 音调:音频的快慢 +- 音量:震动的幅度 +- 音色:谐波 + +### 量化 +- 采样大小(采样位数):一个采样数据用多少bit存放,8bit、16bit +- 采样率:采样的频率8k、16k、32k、44.1k、48k +- 声道数:单声道、双声道、多声道 + + +### 大小计算 +一秒数据大小(码率) = 采样大小 * 采样率 * 声道数 (Kb/s)。 +假设采样率为8kHz、声道数、采样为16bit,时长为1s,则音频数据的大小为: +1 * 8000 * 2 * 16 = 256000 bit / 8 = 32000 byte / 1024 = 31.25KB + +### 数据排列方式 + + + +左右声道每个样本点数据交错排列 +存储格式1: 交错、打包、packed + + +存储格式2: 平面、平行、planar diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/WAV.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/WAV.md" new file mode 100644 index 00000000..4f6e825d --- /dev/null +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/WAV.md" @@ -0,0 +1,12 @@ +## WAV + +WAVE文件格式是微软FIFF(Resource Interchange FileFormat)资源交换文件标准的一种,是针对于多媒体文件存储的一种文件格式和标准。 + +一般而言,RIFF文件由文件头和数据两部分组成,一个WAVE文件由一个“WAVE"数据块组成,这个“WAVE”块又由一个fmt子数据块和一个data子数据块组成,也称这种格式为Canonical form(权威/牧师格式),如下图所示: + + + + + + +- [WAV格式](https://docs.fileformat.com/audio/wav/) From 60a6f624927a1ae4c7b5bcbfe77e4c57808b4b03 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 7 Jun 2023 19:10:12 +0800 Subject: [PATCH 042/128] udpate --- VideoDevelopment/.DS_Store | Bin 0 -> 6148 bytes ...4\226\347\240\201\346\240\274\345\274\217.md" | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 VideoDevelopment/.DS_Store diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..fe0016573ba8e19c5945e1875b4ffbb37e84b05b GIT binary patch literal 6148 zcmeHKF-`+P47A~dBGIHml=}sKu!;g7-~$MPXo7--km$IIo(i7BpZEYXwuKZf6cjXQ z9NDw$^X%%TILBt@i-*;T+1$)#aH2gkOpVX!6Wgl{(c_G3yII{|tuHs5^`v@tg4_c; z!|&Ga>>Gc0{djwMI(qb99=iJ~Wu<@=kOERb3P=GcfSyg89VRMD0VyB_z7*j1p}~p0 za7>I(2SSVhzyawntYek{HYR|*a7;u7=1B!6)oaA?q$A&|t{0ApNjHz1aZcUrHKBOi zj(m%9^PZ?E1*E{X0{gk0v;UvMf0+NbN!m#PDe$ip@L_qrT;P?ew@zM;y|%$G;H-Iv r)36Q-LbPLGv}0`8j*p`#>l)X1-V4XXpd%l2p#BU{7nv0JYXv?5; Date: Thu, 8 Jun 2023 11:04:07 +0800 Subject: [PATCH 043/128] update --- VideoDevelopment/.DS_Store | Bin 6148 -> 6148 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store index fe0016573ba8e19c5945e1875b4ffbb37e84b05b..913c247560b75eac4501248dc3b9a92ede877990 100644 GIT binary patch delta 72 zcmZoMXfc=|&e%S&P;8=}A|vy}0Ba!8Bg4za;LlLNkjjw9;LhMPagHn_v+ b1UMMmHy-@XJeglamyuz!qsU?Ai480OzzP$y delta 72 zcmV-O0Jr~yFoZCW7XgNmaTbv-ApruBP&<<_6a e5VHpe5C{Q=vElavlMfUv1pxp60Kl_76vG2zvKL$c From a6cb70ac9ced68a1a5209aaea3af95a48711a2c9 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 8 Jun 2023 15:13:40 +0800 Subject: [PATCH 044/128] update Audio part --- README.md | 4 ++-- ...226\347\240\201\346\240\274\345\274\217.md" | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32c75424..a5a5d450 100644 --- a/README.md +++ b/README.md @@ -710,8 +710,8 @@ Android学习笔记 [335]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "音频编码" [336]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81%E6%A0%BC%E5%BC%8F.md "音频编码格式" [337]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/AAC.md "AAC" -[338]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "PCM" -[339]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81 "WAV" +[338]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/PCM.md "PCM" +[339]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/WAV.md "WAV" diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" index 1834036f..84d9fe63 100644 --- "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" @@ -16,16 +16,27 @@ ### WAV +WAV是微软公司开发的一种声音文件格式,也叫波形声音文件,是最早的数字音频格式,被Windows平台及其应用程序广泛支持,但压缩率比较低。WAV编码是在PCM数据格式的前面加上44B的头部,分别用来描述PCM的采样率、声道数、数据格式等信息。WAV编码的特点是音质非常好、大量软件支持。一般应用于多媒体开发的中间文件、保存音乐和音效素材等。 + WAV编码是在PCM数据格式的前面加上44字节,分别用来描述PCM的采样率、声道数、数据根式等信息。 特点: 音质非常好、大量软件都支持。 使用场景:多媒体开发的中间文件、保存音乐和音效素材等。 ### MP3 + +MP3全称是MPEG-1 Audio Layer 3,它在1992年合并至MPEG规范中。MP3能够以高音质、低采样率对数字音频文件进行压缩,应用最普遍。MP3具有不错的压缩比,使用LAME编码的中高码率的MP3文件,在听感上非常接近源WAV文件。其特点是音质在128kb/s以上表现还不错,压缩比比较高,兼容性好。主要应用于在高比特率下对兼容性有要求的音乐欣赏方面。 MP3具有不错的压缩比,使用LAME编码的中高码率的MP3文件,听感上非常接近源WAV文件。 特点:音质在128Kbps以上表现还不错,压缩比比较高,兼容性好。 使用场景:高比特率下对兼容性有要求的音乐欣赏。 +### MP3Pro + +MP3Pro是由瑞典Coding科技公司开发的,其中包含了两大技术,一是来自于Coding科技公司所特有的解码技术,二是由MP3的专利持有者法国汤姆森多媒体公司和德国Fraunhofer集成电路协会共同研究的一项译码技术。MP3Pro可以在基本不改变文件大小的情况下改善原先的MP3音乐音质。它能够在用较低的比特率压缩音频文件的情况下,最大程度地保持压缩前的音质。 + ### AAC +高级音频编码(Advanced Audio Coding,AAC)是由Fraunhofer IIS-A、杜比和AT&T共同开发的一种音频格式,它是MPEG-2规范的一部分。AAC所采用的运算法则与MP3的运算法则有所不同,AAC通过结合其他的功能来提高编码效率。AAC的音频算法在压缩能力上远远超过了以前的一些压缩算法,如MP3。它还同时支持多达48个音轨、15个低频音轨、更多种采样率和比特率、多种语言的兼容能力、更高的解码效率。总之,AAC可以在比MP3文件缩小30%的前提下提供更好的音质。 +AAC是新一代的音频有损压缩技术,它通过一些附加编码技术(如PS和SBR)衍生出LC-AAC、HE-AAC、HE-AAC V2这3种主要编码格式。在小于128kb/s码率下表现优异,且多用于视频中的音频编码。在128kb/s码率下的音频编码,多用于视频中的音频轨的编码。 + AAC是新一代的有损压缩技术,它通过一些附加编码技术(如PS、SBR)等,衍生出LC-AAC、HE-AAC、HE-AAC V2三种主要编码格式。 特点:在小于128kbps码率下表现优异,且多用于视频中的音频编码。 @@ -33,7 +44,14 @@ AAC是新一代的有损压缩技术,它通过一些附加编码技术(如PS ### Ogg +Ogg Vorbis是一种新的音频压缩格式,类似于MP3等现有的音乐格式,但有一点不同的是,它是完全免费、开放和没有专利限制的。Vorbis是这种音频压缩机制的名字,而Ogg则是一个计划的名字,该计划意图设计一个完全开放性的多媒体系统。Vorbis也是有损压缩,但通过使用更加先进的声学模型以减少损失,因此,同样位速率编码的Ogg与MP3相比听起来更好一些。Ogg编码音质好、完全免费,可以用更小的码率达到更好的音质,128kb/s的Ogg比192kb/s甚至更高的MP3还要出色,但是目前在媒体软件支持上还是不够友好。在高、中、低码率下都有良好的表现,但兼容性不够好,流媒体特性不支持。 + Ogg编码音质好、完全免费。 可以用更小的码率达到更好的音质,128Kbps的Ogg比192Kbps甚至更高的MP3还要出色。但是目前媒体软件支持上还不够友好。 特点:高中低码率下都有良好的表现,兼容性不够好,流媒体特性不支持。 使用场景:语音聊天的音频消息场景。 + + +### APE + +APE是一种无损压缩音频格式,在音质不降低的前提下,可以压缩到传统无损格式WAV文件的一半。简单来讲,APE压缩与WinZip或WinRAR这类专业数据压缩软件压缩原理类似,只是APE等无损压缩数字音乐之后的APE音频文件是可以直接被播放的。APE的压缩速率是动态的,压缩时只压缩可被压缩的部分,不能被压缩的部分还是会被保留下来。 From be31dee8e94b1c4474611ac56ba8640bf3de3dd8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 15 Jun 2023 11:28:32 +0800 Subject: [PATCH 045/128] update --- ...72\347\241\200\347\237\245\350\257\206.md" | 24 ++++++--- ...55\346\224\276\347\256\200\344\273\213.md" | 50 +++++++++++++++++++ .../\345\205\263\351\224\256\345\270\247.md" | 7 +++ .../AVI.md" | 31 ++++++++++++ 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 "VideoDevelopment/FFmpeg/6.\350\247\206\351\242\221\346\222\255\346\224\276\347\256\200\344\273\213.md" create mode 100644 "VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/AVI.md" 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 b6a15a82..664e9d8e 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,6 +5,13 @@ 基本概念 --- +- 图像 + 图像是人类视觉的基础,是自然景物的客观反映,是人类认识世界和人类本身发展的重要源泉。“图”是物体反射或透射光的分布,“像”是人的视觉系统所接收的图在人脑中所形成的印象或认识。图像是客观对象的一种相似性的、生动性的描述或写真,是人类社会活动中最常用的信息载体,或者说图像是客观对象的一种表示,它包含了被描述对象的有关信息。广义上,图像就是所有具有视觉效果的画面,包括纸介质的、底片或照片上的、电视上的、投影仪或计算机屏幕上的画面。在计算机领域,与图像相关的概念非常多,包括像素、PPI、图像位深度等。 + + 图像本质上是由很多“带有颜色的点”组成的,而这些点,就是“像素”。像素的英文叫Pixel,缩写为PX。这个单词是由图像(Picture)和元素(Element)这两个单词所组成的。 + + 像素是图像显示的基本单位,分辨率是指一张图片的宽度和高度的乘积,单位是像素。通常说一张图片的大小,例如1920×1080像素,是指宽度为1920像素,高度为1080像素。它们的乘积是1920×1080=2 073 600,也就是说这张图片是两百万像素的。1920×1080,也被称为这幅图片的分辨率 + - 媒体 是表示,传输,存储信息的载体,常人们见到的文字、声音、图像、图形等都是表示信息的媒体。 @@ -88,10 +95,15 @@ LCD液晶显示器和传统的CRT显示器,分辨率都是重要的参数之一。传统CRT显示器所支持的分辨率较有弹性,而LCD的像素间距已经固定,所以支持的显示模式不像CRT那么多。LCD的最佳分辨率,也叫最大分辨率,在该分辨率下,液晶显示器才能显现最佳影像。 每个像素有RGB三种颜色组成,每色有8bit,取值范围为(0 ~ 255)也就是2的8次方。这种方式表达出来的颜色,也被称为24位色(占用24bit)。 - 1080P的P是指Progressive scan(逐行扫描),即垂直方向像素点,也就是高。所以1920*1080叫1080P,不叫1920P。 +RGB为3个分量,假如每个分量占8b,取值分别为0~255。那么这3个分量的组合取值为256×256×256=16 777 216种,因此也简称为1600万色。这种颜色范围已经超过了人眼可见的全部色彩,所以又叫真彩色。更高的精度,对于人眼来讲,已经没有意义了,完全识别不出来。RGB这3色,每色有8b,这种方式表达出来的颜色,也被称为24位真彩色。 + +- PPI + 每英寸的像素数(Pixels Per Inch,PPI)是手机屏幕(或计算机显示器)上每英寸面积到底能放下多少“像素”。这个值越高,图像就越清晰细腻。 + PPI是像素的密度单位,PPI值越高,画面的细节就越丰富,所以数码相机拍出来的图片因品牌或生产时间不同可能有所不同,常见的有72PPI、180PPI和300PPI。DPI(Dots Per Inch)是指输出分辨率,是针对输出设备而言的,一般的激光打印机的输出分辨率为[300DPI,600DPI],印刷的照排机可达到[1200DPI,2400DPI],常见的冲印一般为[150DPI,300DPI]。 + - DTS Decode Time Stamp,主要用于标识读入内存中的比特流在什么时候开始送入解码器中进行解码。 @@ -279,7 +291,6 @@ U和V不是基础信号,他俩都是被正交调制的。早期的电视都是 YUV和RGB视频信号相比,最大的优点在于只需要占用极少的带宽,YUV只需要占用RGB一般的带宽。 - YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电视的兼容问题,利用了人类眼睛的生理特性(对亮度敏感,对色度不敏感),允许降低色度的带宽,降低了传输带宽。 在计算机系统中应用尤为广泛,利用 YUV 色彩编码模型可以降低图片数据的内存占用,YUV只需要占用RGB一般的带宽,从而提高数据处理效率。 @@ -287,10 +298,6 @@ YUV 色彩编码模型,其设计初衷为了解决彩色电视机与黑白电 另外,YUV 编码模型的图像数据一般不能直接用于显示,还需要将其转换为 RGB(RGBA) 编码模型,才能够正常显示图像。 -———————————————— -版权声明:本文为CSDN博主「字节流动」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 -原文链接:https://blog.csdn.net/Kennethdroid/article/details/94031821 - YUV码流的存储格式其实与其采样的方式密切相关,主流的采样方式有四种, - YUV 4:4:4 @@ -372,8 +379,9 @@ PAR DAR SAR ##### 硬件加速 -硬件加速(Hardware acceleration)就是利用硬件模块来替代软件算法以充分利用硬件固有的快速特性。硬件加速通常比软件算法的效率要高。 -将2D、3D图形计算相关工作交给GPU处理,从而释放CPU的压力,也是属于硬件加速的一种。 + +中央处理器(Central Processing Unit,CPU),包括算术逻辑部件(Arithmetic Logic Unit,ALU)和高速缓冲存储器(Cache)及实现它们之间联系的数据(Data)、控制及状态的总线(Bus)。GPU即图形处理器(Graphics Processing Unit),专为执行复杂的数学和几何计算而设计的,拥有二维或三维图形加速功能。GPU相比于CPU具有更强大的二维、三维图形计算能力,可以让CPU从图形处理的任务中解放出来,执行其他更多的系统任务,这样可以大大提高计算机的整体性能。硬件加速(Hardware Acceleration)就是利用硬件模块来替代软件算法以充分利用硬件所固有的快速特性,硬件加速通常比软件算法的效率要高。例如将与二维、三维图形计算相关的工作交给GPU处理,从而释放CPU的压力,是属于硬件加速的一种。 + diff --git "a/VideoDevelopment/FFmpeg/6.\350\247\206\351\242\221\346\222\255\346\224\276\347\256\200\344\273\213.md" "b/VideoDevelopment/FFmpeg/6.\350\247\206\351\242\221\346\222\255\346\224\276\347\256\200\344\273\213.md" new file mode 100644 index 00000000..8614e0e5 --- /dev/null +++ "b/VideoDevelopment/FFmpeg/6.\350\247\206\351\242\221\346\222\255\346\224\276\347\256\200\344\273\213.md" @@ -0,0 +1,50 @@ +## 视频播放简介 + + +解封装是指将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类有很多,例如MP4、MKV、RMVB、TS、FLV、AVI等,其作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。 + + + +### 1.解协议 +解协议是指将流媒体协议的数据,解析为标准的封装格式数据。音视频在网络上传播的时候,常采用各种流媒体协议,例如HTTP、RTMP、RTMP、MMS等。这些协议在传输音视频数据的同时,也会传输一些信令数据。这些信令数据包括对播放的控制(播放、暂停、停止),或者对网络状态的描述等。在解协议的过程中会去除信令数据而只保留音视频数据。例如采用RTMP协议传输的数据,经过解协议操作后,输出FLV格式的数据。 +注意:“文件”本身也是一种“协议”,常见的流媒体协议有HTTP、RTSP、RTMP等。 + +### 2.解封装 + +解封装是指将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类有很多,例如MP4、MKV、RMVB、TS、FLV、AVI等,其作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。 + +### 3.解码 + +解码是指将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC、MP3、AC-3等,视频的压缩编码标准则包含H.264、MPEG-2、VC-1等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV420P、RGB等。压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。 + +### 4.音视频同步 +根据解封装模块在处理过程中获取的参数信息,同步解码出来的视频和音频数据,被送至系统的显卡和声卡播放出来。为什么需要音视频同步呢?媒体数据经过解复用流程后,音频/视频解码便是独立的,也是独立播放的,而在音频流和视频流中,其播放速度是由相关信息指定的,例如视频是根据帧率,音频是根据采样率。从帧率及采样率即可知道视频/音频播放速度。声卡和显卡均是以一帧数据来作为播放单位,如果单纯依赖帧率及采样率进行播放,在理想条件下,应该是同步的,不会出现偏差。 + +下面以一个44.1kHz的AAC音频流和24f/s的视频流为例来说明。如果一个AAC音频frame每个声道包含1024个采样点,则一个frame的播放时长为(1024/44 100)×1000ms≈23.27ms,而一个视频frame播放时长为1000ms/24≈41.67ms。理想情况下,音视频完全同步,但实际情况下,如果用上面那种简单的方式,慢慢地就会出现音视频不同步的情况,要么是视频播放快了,要么是音频播放快了。可能的原因包括:一帧的播放时间难以精准控制;音视频解码及渲染的耗时不同,可能造成每一帧输出有一点细微差距,长久累计,不同步便越来越明显;音频输出是线性的,而视频输出可能是非线性的,从而导致有偏差;媒体流本身音视频有差距(特别是TS实时流,音视频能播放的第1个帧起点不同),所以解决音视频同步问题引入了时间戳,它包括几个特点:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的),编码时依据参考时钟给每个音视频数据块都打上时间戳。播放时,根据音视频时间戳及参考时钟来调整播放,所以视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。 + +ffplay是使用FFmpeg API开发的功能完善的开源播放器。在ffplay中各个线程如图5-45所示,扮演角色如下:read_thread线程扮演着图中Demuxer的角色;video_thread线程扮演着图中VideoDecoder的角色;audio_thread线程扮演着图中Audio Decoder的角色。主线程中的event_loop函数循环调用refresh_loop_wait_event则扮演着视频渲染的角色。回调函数sdl_audio_callback扮演着图中音频播放的角色。VideoState结构体变量则扮演着各个线程之间的信使。 + +- (1)read_thread线程负责读取文件内容,将video和audio内容分离出来后生成packet,将packet输出到packet队列中,包括Video Packet Queue和Audio Packet Queue,不考虑subtitle。 +- (2)video_thread线程负责读取Video PacketsQueue队列,将video packet解码得到VideoFrame,将Video Frame输出到Video FrameQueue队列中。 +- (3)audio_thread线程负责读取Audio PacketsQueue队列,将audio packet解码得到AudioFrame,将Audio Frame输出到Audio FrameQueue队列中。 +- (4)主线程→event_loop→refresh_loop_wait_event负责读取Video Frame Queue中的video frame,调用SDL进行显示,其中包括了音视频同步控制的相关操作。 +- (5)SDL的回调函数sdl_audio_callback负责读取Audio Frame Queue中的audio frame,对其进行处理后,将数据返回给SDL,然后SDL进行音频播放。 + +FFmpeg解码流程图: + + +- (1)注册所有容器格式和CODEC,使用av_register_all,最新版本中无须调用该函数。 +- (2)打开文件av_open_input_file,最新版本为avformat_open_input。 +- (3)从文件中提取流信息av_find_stream_info。 +- (4)枚举所有流,查找的种类为CODEC_TYPE_VIDEO。 +- (5)查找对应的解码器avcodec_find_decoder。 +- (6)打开编解码器avcodec_open。 +- (7)为解码帧分配内存avcodec_alloc_frame。 +- (8)不停地从码流中提取帧数据av_read_frame。 +- (9)判断帧的类型,对于视频帧则调用avcodec_decode_video。 +- (10)解码完后,释放解码器avcodec_close。 +- (11)关闭输入文件av_close_input_file。 + +注意:该流程图为FFmpeg 2.x的版本,最新的FFmpeg 4.x系列的流程图略有改动。 + + 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 d4ad6c16..878ca9db 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" @@ -26,6 +26,13 @@ GOP(Group of Pictures)是一组连续的画面,由一张I帧和数张B/P帧组 **编码器将多张图像进行编码后生产成一段一段的 GOP ( Group of Pictures ) , 解码器在播放时则是读取一段一段的 GOP 进行解码后读取画面再渲染显示。**GOP ( Group of Pictures) 是一组连续的画面,由一张 I 帧和数张 B / P 帧组成,是视频图像编码器和解码器存取的基本单位,它的排列顺序将会一直重复到影像结束。I 帧是内部编码帧(也称为关键帧),P帧是前向预测帧(前向参考帧),B 帧是双向内插帧(双向参考帧)。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。如果没有 I 帧,P 帧和 B 帧就无法解码。 +GOP是指一组连续的图像,由一个I帧和多个B/P帧组成,是编解码器存取的基本单位。GOP结构常用的两个参数为M和N,M用于指定GOP中首个P帧和I帧之间的距离,N用于指定一个GOP的大小。例如M=1,N=15,GOP结构为IPBBPBBPBBPBBPB IPBBPBB。GOP指两个I帧之间的距离,Reference指两个P帧之间的距离。一个I帧所占用的字节数大于一个P帧,一个P帧所占用的字节数大于一个B帧,所以在码率不变的前提下,GOP值越大,P、B帧的数量就会越多,平均每个I、P、B帧所占用的字节数就越多,也就更容易获取较好的图像质量;Reference越大,B帧的数量就越多,同理也更容易获得较好的图像质量。需要说明的是,通过提高GOP值来提高图像质量是有限度的,在遇到场景切换的情况时,H.264编码器会自动强制插入一个I帧,此时实际的GOP值被缩短了。 + +另一方面,在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP开始才有可能得以恢复,所以GOP值也不宜设置得过大。同时,由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,在解码某个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,Seek响应的时间也越长。 + +GOP的形式通常有两种,包括闭合式GOP和开放式GOP。闭合式GOP只需参考本GOP内的图像,而不需参考前后GOP的数据。这种模式决定了闭合式GOP的显示顺序总是以I帧开始而以P帧结束。开放式GOP中的B帧解码时可能要用到其前一个GOP或后一个GOP的某些帧。码流中包含B帧的时候才会出现开放式GOP。 + + ### IDR IDR(Instantaneous Decoding Refresh)是H.264视频编码中的一种特殊的I帧。它包含了所有的视频序列信息,并且通过强制性地要求解码器重新初始化来保证解码正确性。IDR通常用于视频流中断或丢失时,重新恢复视频流的一种方式。 diff --git "a/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/AVI.md" "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/AVI.md" new file mode 100644 index 00000000..333b78dc --- /dev/null +++ "b/VideoDevelopment/\350\247\206\351\242\221\345\260\201\350\243\205\346\240\274\345\274\217/AVI.md" @@ -0,0 +1,31 @@ +## AVI + +音频视频交错(Audio Video Interleaved,AVI)格式,是一门成熟的老技术,尽管国际学术界公认AVI已经属于被淘汰的技术,但是简单易懂的开发API,还在被广泛使用。AVI符合RIFF(ResourceInterchange File Format)文件规范,使用四字符码(Four-Character Code,FOURCC)表征数据类型。AVI的文件结构分为头部、主体和索引3部分。主体中图像数据和声音数据是交互存放的,从尾部的索引可以索引到想放的位置。AVI本身只提供了这么一个框架,内部的图像数据和声音数据格式可以是任意的编码形式。因为索引放在了文件尾部,所以在播放网络流媒体时已力不从心。例如从网络上下载AVI文件,如果没有下载完成,则很难正常播放出来。 + + +AVI中有两种最基本的数据单元,一个是Chunks,另一个是Lists,结构体如下: + +```c++ +// Chunks +typedef struct { + DWORD dwFourcc; + DWORD dwSize; // data + BYTE data[dwSize]; // contains headers or audio/video data +} CHUNK; + +// Lists +typedef struct { + DWORD dwList; + DWORD dwSize; + DWORD dwFourCC; + BYTE data[dwSize - 4]; +} LIST; +``` + +由如上代码可知,Chunks数据块由一个四字符码、4B的data size(指下面的数据大小)及数据组成。Lists由4部分组成,包括4B的四字符码、4B的数据大小(指后面列的两部分数据大小)、4B的list类型及数据组成。与Chunks数据块不同的是,Lists数据内容可以包含字块(Chunks或Lists)。 + + + + + + From 4592dad939676f1ea40d5732c37a6c5cf1982d66 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 19 Jun 2023 11:14:14 +0800 Subject: [PATCH 046/128] update --- README.md | 7 +++++-- VideoDevelopment/.DS_Store | Bin 6148 -> 0 bytes ...0\270\345\277\203\345\212\237\350\203\275.md" | 0 3 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 VideoDevelopment/.DS_Store rename "VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" => "VideoDevelopment/FFmpeg/5.FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" (100%) diff --git a/README.md b/README.md index a5a5d450..ee9da0dd 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Android学习笔记 - [fMP4格式详解][255] - [视频封装格式][256] - [M3U8][321] + - [AVI][341] - [视频编码][257] - [视频编码原理][331] - [AV1][258] @@ -115,6 +116,7 @@ Android学习笔记 - [3.FFmpeg切片][325] - [4.开发环境配置][333] - [5. FFmpeg核心功能][334] + - [6.视频播放简介][340] - [操作系统][263] - [1.操作系统简介][264] - [2.进程与线程][265] @@ -712,7 +714,8 @@ Android学习笔记 [337]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/AAC.md "AAC" [338]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/PCM.md "PCM" [339]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/WAV.md "WAV" - +[340]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/6.%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E7%AE%80%E4%BB%8B.md "6.视频播放简介" +[341]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/AVI.md "AVI" Developed By @@ -736,4 +739,4 @@ License distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store deleted file mode 100644 index 913c247560b75eac4501248dc3b9a92ede877990..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeH~u}T9$5QhKJBLr+xiRHe6Z?J@@jbPymh(>Ip35a0lb=Fq;p1y$p%uXPB3aKpg zpD_Dh=4N)eueiAZVAF^BAus_jrn}jJDlKjbK~yy{q5yx|IyY#9xoFC5fA|p5CIX`hCnvW@!mGc$`Jt( z_*Vq{{ZQzxHMMUVuMRFT0#G}K!+0ID1hsg8T2uR`jL@u=Qd_Czh+(aq@sfEpwQt(W zVL5zQo@_axSe(xNOQgeUQ>`K(0-p)^>`j|^|39MtF#mgwOaw&Wj}owMc0QZ(;j*_5 y9@Bempk>ast*C@A`^k%An*afEFlvB diff --git "a/VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" "b/VideoDevelopment/FFmpeg/5.FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" similarity index 100% rename from "VideoDevelopment/FFmpeg/5. FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" rename to "VideoDevelopment/FFmpeg/5.FFmpeg\346\240\270\345\277\203\345\212\237\350\203\275.md" From 629415372887d2d09b507119110b4aee32188a40 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 21 Jun 2023 19:54:57 +0800 Subject: [PATCH 047/128] update --- .../1.OpenCV\347\256\200\344\273\213.md" | 18 ++++++++++++++++++ ...347\220\206_NAT\347\251\277\351\200\217.md" | 10 +++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 "VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" diff --git "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" new file mode 100644 index 00000000..e1ab5d76 --- /dev/null +++ "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" @@ -0,0 +1,18 @@ +1.OpenCV简介 +=== + +![image](https://github.com/CharonChui/Pictures/blob/master/cdn.png?raw=true) + +### OpenCV4简介 + +OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在linux、windows、android和Mac OS操作系统上。 + +它轻量级而且高效--由一系列C函数和少量C++类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。 + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" index 0b3bd000..b8ffe932 100644 --- "a/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" +++ "b/VideoDevelopment/P2P\346\212\200\346\234\257/P2P\345\216\237\347\220\206_NAT\347\251\277\351\200\217.md" @@ -5,7 +5,6 @@ P2P原理_NAT穿透 NAT(Network Address Translation,网络地址转换),也叫做网络掩蔽或者IP掩蔽。NAT是一种网络地址翻译技术,主要是将内部的私有IP地址(private IP)转换成可以在公网使用的公网IP(public IP)。 - 网络地址转换,就是替换IP报文头部的地址信息。NAT通常部署在一个组织的网络出口位置,通过将内部网络IP地址替换为出口的IP地址提供公网可达性和上层协议的连接能力。那么,什么是内部网络IP地址? [RFC1918](https://datatracker.ietf.org/doc/rfc1918/)规定了三个保留地址段落:10.0.0.0-10.255.255.255;172.16.0.0-172.31.255.255;192.168.0.0-192.168.255.255。这三个范围分别处于A,B,C类的地址段,不向特定的用户分配,被IANA作为私有地址保留。这些地址可以在任何组织或企业内部使用,和其他Internet地址的区别就是,仅能在内部使用,不能作为全球路由地址。这就是说,出了组织的管理范围这些地址就不再有意义,无论是作为源地址,还是目的地址。对于一个封闭的组织,如果其网络不连接到Internet,就可以使用这些地址而不用向IANA提出申请,而在内部的路由管理和报文传递方式与其他网络没有差异。 @@ -14,6 +13,15 @@ NAT(Network Address Translation,网络地址转换),也叫做网络掩 那么,NAT与此同时也带来一些弊端:首先是,NAT设备会对数据包进行编辑修改,这样就降低了发送数据的效率;此外,各种协议的应用各有不同,有的协议是无法通过NAT的(不能通过NAT的协议还是蛮多的),这就需要通过穿透技术来解决。我们后面会重点讨论穿透技术。 +### NAT原理 +内网主机向外网主机发送的网络包,在经过NAT时,IP和PORT会被替换为NAT为该主机分配的外网IP/PORT,也就是该内网主机在NAT上的出口IP/PORT,外网主机收到该网络包后,会视该网络包是从NAT发送的。 +外网主机只能通过NAT为该内网主机分配的外网IP/PORT,向它发送网络包,内网主机的本地地址对外界不可见,网络包在经过NAT的时候,会被NAT做外网IP/PORT到内网IP/PORT的转换。 +可见,NAT维护内网主机内网地址和在NAT上为它分配的外网地址之间的映射关系,需要维护一张关联表。 +NAT在两个传输方向上做两次地址转化,出方向做源(src)信息转换,入方向做目的(dst)信息替换,内外网地址转换是在NAT上自动完成的。NAT网关的存在对通信双方是透明的。 + +那NAT是符合环节IPv4地址枯竭的问题的呢? +答案是端口多路复用,通过PAT(Port Address Translation),让NAT背后的多台内网主机共享一个外网IP,最大限度的节省外网IP资源。 + **虽然实际过程远比这个复杂,但上面的描述概括了NAT处理报文的几个关键特点:** From ee2b06649d63b6aa0266a1d2b6a88d76a32c7553 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 21 Jun 2023 20:57:31 +0800 Subject: [PATCH 048/128] add OpenCV --- .../1.OpenCV\347\256\200\344\273\213.md" | 181 +++++++++++++++++- 1 file changed, 178 insertions(+), 3 deletions(-) diff --git "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" index e1ab5d76..4523a904 100644 --- "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" @@ -1,13 +1,188 @@ 1.OpenCV简介 === -![image](https://github.com/CharonChui/Pictures/blob/master/cdn.png?raw=true) +[OpenCV官网](https://opencv.org/) + ### OpenCV4简介 -OpenCV是一个基于BSD许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在linux、windows、android和Mac OS操作系统上。 +OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux、Windows、Android和Mac OS操作系统上。 [1] 它轻量级而且高效——由一系列 C 函数和少量 C++ 类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。 + +OpenCV用C++语言编写,它具有C ++,Python,Java和MATLAB接口,并支持Windows,Linux,Android和Mac OS,OpenCV主要倾向于实时视觉应用,并在可用时利用MMX和SSE指令, 如今也提供对于C#、Ch、Ruby,GO的支持。 + + +### OpenCV4的模块架构 + +- calib3d(由calibration(标准)和3D 两个术语缩写组合而成):主要包含相机定位与 +立体视觉等功能,例如物体位姿估计、三维重建、摄像头标定等; +- core(核心功能模块):主要包含OpenCV 库的基础结构以及基本操作,例如OpenCV +基本数据结构、绘图函数、数组操作相关函数、动态数据结构等; +- dnn(深度学习模块):主要包括构建神经网络、加载序列化网络模型等,但目前仅适 +用于正向传递计算; +- features2d(由feature(特征)和2D 两个术语缩写组合而成):主要功能包括处理图 +像特征点,例如特征检测、描述与匹配等; +- flann(快速近似最近邻库):是高维的近似近邻快速搜索算法库,主要包含快速近似 +近邻搜索与聚类等; +- gapi:加速常规的图像处理,与其他模块相比,此模块主要充当框架,而不是某些特定 +的计算机视觉算法; +- highgui(高层GUI):包含创建和操作显示图像的窗口、处理鼠标事件以及键盘命令、 +提供图形交互可视化界面等; +- imgcodecs(图像文件读取与保存模块):主要用于图像文件读取与保存; +- imgproc(重要的图像处理模块):主要包含图像滤波、几何变换、直方图、特征检测 +与目标检测等; +- ml(机器学习模块):主要为统计分类、回归和数据聚类等; +- objdetect(目标检测模块):主要用于图像目标检测,例如检测Haar 特征; +- photo(计算摄影模块):主要包含图像修复和去噪等; +- stitching(图像拼接模块):主要包含特征点寻找与匹配图像、估计旋转、自动校准、 +接缝估计等图像拼接过程的相关内容; +- video(视频分析模块):主要包含运动估计、背景分离、对象跟踪等视频处理相关内 +容; +- videoio(视频输入/输出模块):主要用于读取/写入视频或者图像序列。 + + +### 数据结构:Mat + +API: +- imread(..url...):加载本地图像 + 默认的BGR 彩色图像加载方式,此外支持灰度图像和任意格式。注意:在OpenCV中颜色值写法是BGR,而不是RGB。 +- imshow("namexx", ...url...):显示图像 +- getStructuringElement(...):获取腐蚀的内核矩阵 +- erode(...):腐蚀(源图像、目的图像、内核矩阵) +- blur(...) :模糊 +- cvtColor(...):颜色转换 +- Canny(...):提取边缘 +- waitKey(...):等待按键 +- imwrite(...): 图片保存,支持各种格式 + +```c++ +// 图像模糊(blur) +#include +#include +#include +#include +using namespace std; +using namespace cv; +int main_p002(int argc, const char * argv[]) { + // insert code here... + Mat imgSource = imread("../../images/p1_tutu.jpg");//读取图片 + imshow("source", imgSource);//显示图片在名为"source"的窗口里 + Mat imgDst;//用来存放模糊之后的图片 + blur(imgSource, imgDst, Size(20, 20));//进行均值滤波,在20*20 的像素内取平均值然 + 后低于的或者高于的被替换掉 + imshow("target", imgDst);//显示图片在名为"target"的窗口里 + waitKey(0);//等待按键操作再执行后续按随意键将进行下一步return 程序结束 + return 0; +} +``` + + +#### 重要概念: 图像就是函数 + +把一张图片放到坐标系中后,我们就能把一副图像看作是一个二维函数,定义成 +f(x, y)。 +![image](https://github.com/CharonChui/Pictures/blob/master/opencv_image_two.png?raw=true) + + +任何一对空间坐标(x, y)处f的值看作该坐标点处的强度(intensity)或灰度。 + +我们把每一坐标点处的强度在三维空间中看看。 + +![image](https://github.com/CharonChui/Pictures/blob/master/opencv_image_san.png?raw=true) + + +把图像看作是一个二维函数,将对后续的图像处理理解和计算带来极大的便利,对图像 +的处理就是对函数的处理。需要强调的是把图像作为二维函数时,它是一个离散函数,且取 +值范围有所限定,比如x, y 轴的坐标值,函数取值也限定在某个区间之内(不一定是[0-225])。 +另外,图像作为函数,不可能得到上面类似f(x, y)=x**2+y**2 这样的表达式。 +对于彩色图像,同样可以看作是一个向量函数。f(x, y) = [r(x, y), g(x, y), b(x, y)] + + +## Mat对象 + +Mat 对象:OpenCV2.0 之后引进的图像数据结构、自动分配内存、不存在内存泄漏的问 +题,是面向对象的数据结构,分了两个部分,头部分与数据部分。 + +文档上边说,Mat 的数据成员由头部和数据组成。 + +Mat 类的声明中的数据成员部分: +- Mat 的头部信息:flags,dims,rows,cols,data 指针,refcount 指针。 +- Mat 的数据部分:显然,意味着data 指针指向的空间。 + +而refcount 指针,就是起引用计数的作用。有了引用计数机制,使得我们的Mat 能够很好 +地支持浅拷贝的操作:使用等号操作符给Mat 赋值(或者使用拷贝构造的时候),仅拷贝 +头部信息,如此能够节约许多的空间和时间。而深拷贝的工作,就交给成员方法clone()和 +copyTo()。 + + +### Mat对象的发展史 + +在早期的OpenCV1.x 版本中,图像的处理是通过IplImage(该名称源于Intel 的另一个 +开源库Intel Image Processing Library ,缩写成IplImage)结构来实现的。 + +早期的OpenCV 是用C 语言编写,因此提供的借口也是C 语言接口,其源代码完全是C +的编程风格。IplImage 结构是OpenCV 矩阵运算的基本数据结构。 + +到OpenCV2.x 版本,OpenCV 开源库引入了面向对象编程思想,大量源代码用C++重写。 + +Mat 类(Matrix 的缩写) 是OpenCV 用于处理图像而引入的一个封装类。 +从功能上讲,Mat 类在IplImage 结构的基础上进一步增强,并且,由于引入C++高级编 +程特性,Mat 类的扩展性大大提高,Mat 类的内容在后期的版本中不断丰富。 +该对象会自动分配内存、自动管理内存分配和释放,不存在内存泄漏的问题,OpenCV2.x +版本以后,越来越多的函数实现了MatLab 所具有的功能,甚至干脆连函数名都一模一样(如 +imread, imshow,imwriter 等), 这些属性和方法如果你学过Matlab 掌握起来一定会很快。 + + +#### Mat知识点 + +Mat 对象构造函数与常用方法 +##### 构造函数: +- Mat(); // 默认构造函数 +- Mat(int rows, int cols, int type); //指定行数、列数、类型的构造函数 +- Mat(Size size, int type); // 指定矩阵大小、类型的构造函数 +- Mat(int rows, int cols, int type, const Scalar& s); // 指定矩阵行列、类型、颜色的构造函数 +- Mat(Size size, int type, const Scalar& s);// 指定矩阵大小、类型、颜色的构造函数 +- Mat(int ndims, const int* sizes, int type); +- Mat(int ndims, const int* sizes, int type, const Scalar& s); +- Mat(const Mat& m); +- Mat(int rows, int cols, int type, void* data, size_t step=AUTO_STEP); +- Mat(Size size, int type, void* data, size_t step=AUTO_STEP); +- Mat(int ndims, const int* sizes, int type, void* data, const size_t* steps=0); + +```c++ +// start your code... +Mat m1; // m1 为用默认拷贝构造创建的对象 +Mat m2(500, 500, CV_8UC3);//指定行数、列数、类型的构造函数CV_8UC3:8 位无符号3 通 +道图像 +m2 = Scalar(10, 20, 30); +Mat m3(Size(300, 300), CV_8UC3); // 指定矩阵大小、类型的构造函数 +m3 = Scalar(50, 10, 215); +Mat m4 = Mat(400, 400, CV_8UC3, Scalar(255, 0, 255)); // 指定矩阵行列、类型、颜色 +的构造函数Scalar(255,0,0):颜色列表依次为b g r +Mat m5 = Mat(src.size(), src.type()); // 创建一个和原图大小类型相同的图像 +``` + +##### 常用方法: +- void copyTo(Mat mat) +- void convertTo(Mat dst,int type) +- Mat clone() +- int channels() +- int depth() +- bool empty() +- uchar * ptr(i = 0) + + +##### Mat 对象四个要点 +1. 输出图像内存是自动分配的 +2. 使用OpenCV 的C++接口,不需要考虑内存分配问题 +3. 复制操作和拷贝构造函数只会复制头部分 +4. 使用clone 和copyTo 两个函数实现数据完全复制 + + + + + + -它轻量级而且高效--由一系列C函数和少量C++类构成,同时提供了Python、Ruby、MATLAB等语言的接口,实现了图像处理和计算机视觉方面的很多通用算法。 From ac82bfdb134da8be58220cc9d2308c0d8ca93fe9 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 21 Jun 2023 21:00:45 +0800 Subject: [PATCH 049/128] update --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index ee9da0dd..01ed8394 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,9 @@ Android学习笔记 - [4.开发环境配置][333] - [5. FFmpeg核心功能][334] - [6.视频播放简介][340] + - [OpenCV][342] + - [1.OpenCV简介][343] + - [操作系统][263] - [1.操作系统简介][264] - [2.进程与线程][265] @@ -716,6 +719,8 @@ Android学习笔记 [339]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E9%9F%B3%E9%A2%91%E7%BC%96%E7%A0%81/WAV.md "WAV" [340]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/FFmpeg/6.%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE%E7%AE%80%E4%BB%8B.md "6.视频播放简介" [341]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/AVI.md "AVI" +[342]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenCV "OpenCV" +[343]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenCV/1.OpenCV%E7%AE%80%E4%BB%8B.md "1.OpenCV简介" Developed By From a0d8bbbe03e3fa84876d729b16f11095f3c7f5a0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 28 Jun 2023 15:45:31 +0800 Subject: [PATCH 050/128] update --- .../UML\347\261\273\345\233\276.pdf" | Bin 0 -> 542659 bytes ...70\345\277\203\345\212\237\350\203\275.md" | 2 +- .../1.OpenCV\347\256\200\344\273\213.md" | 4 + ...30\345\210\266\345\233\276\345\275\242.md" | 86 ++++++++++++++++++ ...74\345\274\217\350\257\246\350\247\243.md" | 53 +++++++++++ .../TS.md" | 4 + 6 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 "JavaKnowledge/UML\347\261\273\345\233\276.pdf" create mode 100644 "VideoDevelopment/OpenCV/2.\347\273\230\345\210\266\345\233\276\345\275\242.md" diff --git "a/JavaKnowledge/UML\347\261\273\345\233\276.pdf" "b/JavaKnowledge/UML\347\261\273\345\233\276.pdf" new file mode 100644 index 0000000000000000000000000000000000000000..d7f9f7f791605cda7d6021f4f497abeab1cfc952 GIT binary patch literal 542659 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�+9@R=*PJw+Ed8v>K8^PlaB0af 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

nAx)< z|H*n74f=zHBcFgwb7#k%!u5Q0-^hu!fo01(4xW4QP#3+UaFYU*Q~XNy3HKAqOq~^U zI;~a(A*E6oSQAC5X$5B2st{P#YB4|)B{^aWW9dyMomR3vElO1UxgOn09qrL2bRX(I z(=j?V)XI=4$!^L}zKOyLhR}9eatbV+lO*I?7a4ApP-{VkN|alT1i{?+Nc5)DgQmIM zu_;v?TtbU$A!1f~dNFg2hF4#bS zcm$|{PW*52vm^O`d$>vf9DS_t7vjMN!g2l|vbVWEM-ItFG{p1{yWQvx1dLXf3m7dH zqnuEgY9Qo*My;+u=^n4dd_97qg0HBM1E1H6RTj<(0EB`#U~ibp;rYD=fzN3j9tYZw z4xhsp(BPqP+(}0VnZBZea~y@0y&HsJ4tKNZBGH@*o#a-v8v~e=5S?EVF)$?b zbX8e$?GBP_fw$N(v$-2BfE%Qsdo}}Y!yE`2wZws-Mhl|Ly`gzWLu>E^paxHeXs8nF z;Uh>l0`)*HL08m!nVb3`v=-$c=Gg2~;cY1~M5X395I-%I@n*%TaUxh+J~CELm3jmc zi6J0_hAH@w_eD2U<=@kI!9j3zjksFeey=cu{>%NF^BtFg^W#;w#XGgOZRlgLn*QL_iM=mQ(UJ3Gd#;QTjh}}G*)8t>qU@OO z3>*5J%47`9Y7{^zlYpW$tV~v?iD)XFQH*`6^EQwQUn8Z*7y(0F zqemI>7y^NIO*6(42$T{zb(IoUQ~*s2igPk#7oiWri4_2Bz>7@-EqlB=RZYM<$z-ak z!O zJs}5kX7%TbVo`eiN*};7xCvhmk-;V0Cm0}|5p227SU^8y0q(GFU_WF(V_8=4R1#Dq z_VDG*_OXq#6ywF>9z~i(kBS=3DshxJG|PCTB#1S%s`3NVK$#P>r30S|UEu=X&i! zcG_-FkedG}srhfjiC2v$Py5pYu7C)6lF*P@A$Q87cH(0;>N* zeXp3^NQ*$l9#x{i5Ds#n7P~BjgqBdv0-`uhwh&*og(C1c-8OTdvldptb@_4e&hndb z{^0Tb`4!Of0XRN!L^SW8DC`wKXL{G@QCLZs_RSB0>^64>MGduBPg^bz1l+NhN)oP8 zTF_6Rw!0k;+}!$>)Z7wuq`T#Ir=>ntZSC*wV<~eyZ>kEb)kMLnWjYtEHP=cVOrP1y zqhFcTOMIo96j+3Iv!&9H`w#XhV7+8W@1H3C|V8cgs~Q)+MV zIb<&Y9rQyKo~I_MV9tiAh+D5uTz)b+{y5VPy* zeY(3ab!*)@1%Gk*+Cn$`&m&u+L(9j-_Z}RD+YWB;Tm)8ve=RV(_Ut}*{P>BMgJRn) zuB+d;7M#QgnmDrZ>)g+fmE#%Q8Y&I`N>CXX#%r+Hl(rxr#G{2F%mRws*s`;JD68hH zaS1(!w!zPlW3y0I=(#Ge{7fXpr+y9wsah(59!Y>@LPfkbCJQlMrIRo-#3L3z<8!&{ zZ3KPV!@AFfTs6Co5KB!a<2D$Ljk=p~@yvsG1gEjsIHgHH%WvOWPTH=^8# zc0r#Hxl|81cBP#*Pugi#wD@?ZRb%Ep7bwBiZ@6N#%NQyCmA%Zmof3}&{*9+ODq ziia*~o284>g`^%eH&Q6!FH77c1caF`-c1xyS!9!k<~DhmR4^Xdmr#7f)+Cf>h+M|u zNw6col$j8l;qv^6ySt`1{p7*n{J=t)_vnoCD06@Jma(1Vg}$BJhQ|g9!&|m(*)syp zjJ$X7q&QQ3P<;0jX5kv1rbhq|-xSvYS`=yd*b8gNkAtmUlP~lg+1$VT$j~X|Ge#b^ z$|ktKL-B4H#Ljp;ULCGYYQ1*53s<3BV8xUZFVeHqR)%Fs6THIe)WUEeP*sWUy&{B> z>j}x|SBW-<48>mtnJi@95Pm*PqtDdCa5zL-sDeA^M4zX0xop}1umtcbQ2}ot85j#N zRu~|jIS|eU0u0MDEWz3k?N22`05w{DmSGg$gl(m5gAJ~<9k3mAI# zX+-q|AVfB${t6(i!WWjBx?a1s<< zR8Z8!ofexdO4o#52hXB#he3PYfRd4KPY3t_#%nx7c_|ZEHRYfQfwRitHB>TDg(4Yu zR^bIT2vIL3A!}U<1!saz%1K6VFea{9sLCax?a>WU7>&jfmF<-qDlx+D<=7i|(1nj- zq7Sr<#i=pPz7DHWRERpa~6w^(~*IFPS8fO z+;0(mNkxpgbcdv>x>;nDer^hwY|A2w;%-w3Lm}z%mPzvB{g+Os=2lCmE6IQrM}3Q{ zhR1maWCMk5JFoBYa@k>x+4&3c!i@N1wlETDp?ejDU+{1s+^gtS={+(0BA~$Qg{!Z1^o@b5@bZ)2%^q4Wj(zk|bf2Ud_$%2*+{-BEtp=5X znYG)YQ^f}>c$G063^^&P+NjavO(zR^JpP|!EF%~~exF8r6T+A=jK||-KID?)6<9w| zPzco#^c7XOqol3r^ZVTzokWywXIUqNG_5Z3r22A{0w|LjpiyWP`p_}=hi*D4Db3w; zLk)D3q{)d(54f7_^q>{8N1nvoKdD1ERn1pZ?LlRQ#m^u=5v~5H7@0?kB%hF zdeWw*Pm4NlWP26QXmTc}N{t|?^neyk&lZ&d@i!AfDQw6KX<4P=(OqW;U3Dj|wLbx! z`Sr4O`Ky@O3G4^g{Z`Q|9u!-zZ~BLLGyLYfjzitcm%-@KnZBdn2W~oQ|GP=?TrC`# zm=IqZ7Y7x(?g35(){DP+rJLK)-`BTnBq{E?eT!&E{JZQe?r)GS;|aJFxP3NS<+0hE z7s$->N`ZG6wOTrvsKL{FDK;d>dTTN_`od+NS6IDHSE<7Dh^qvHpLVcS4%Y8y9aT(a zUAVfMQ0Xd=Wl&Tlgp~=rAW3vkH=)IQISGxV^#qSR?)}Z7_(a&#I ztBWk{H8};tP@Z?J09Q!btv%Ka*5|D+TV?IV8y{L(FMblJpj9W8Ky-MM>-ChGl7RXY zVR(IL2J_56H2=g56J|hr%)pEb!Dd_tHe(MUvAoQ+iRMO!%Z8bBW#@G|fx)unwA;9* zS#FVODLy2yg@cxvgvjuq?Sz~qd1@{8M7 z`ro<%s5`GOyKwXQy`Xyh8V2NDANK~QFCKdTHSvqF0j_J=*1mzU7V+@A@17gpw0GND zFg3Di&(`fYPLzueWOunYC_F}1OVv}&;I`m^jg2N#gGHuRvsyz_Q-iLmDyr4#YAfpT z49hup0~-wn!#^jn;~+9=3ap$0bv~?p>t{i-qMFQ7`77P9wx*_~7M(Xq{D?=Wr5r$u zoQ>OyF(eymAy3uvwR8~H*4h%Y-o6eq6Il`#8Y(U5)U<#wW3hM>bzXu_aim`-xv;LN zR5@=}sj^L|%*hODr5Zqwx?N4Hd7UwbYOWeedDkpu7%;?NOmXZ@<0xUNK{iFuzRBw98hK@9 zR1&+IQmGgst}0KWFUsw9l5|BAwM9)e(VD2GPG>Ms7l_!&GQlXY7Q%XsK3YYo)M|fq z#7|IykqB~u$nABYB%&sstc|0uN>8=5TJnTWYbE+JaC7`vf;FcTf|0CFu=~*kZ_4T{ z6eVXlR;m#cYc-?X7{ z0di%ZW@d+RNzXHdRhT5GKe8MBtj02yVoDPXgMswM%jk+(LRSn!r6IjyC4%I&7Lt(M z_}k;(^A`3&Yva+sJT&$Pqf_Fik!IjNdEp}LqYoed{B!Z6Wp69Ymxm_`Yl1tEZ=cx0 z{B-6n2!U@NIw*FFKmF{Zp?|zAUU<;96acUQxPa$4cu`z>=)oQF2EAx4e+cGhy6@iX z|48y+nDy>ul#|p@jZ`bOfS&g=7d|S_#OyKt4P2D~{m4_aqg(B`P2Q%~>y0%PN@ZKJ zI6a|F;+lt{wy`#*ZL6(q)5aKM#wcsy@le^>dEsye-*Fy1eP%w5c?nktS~zb$g!AV? zna3lOJ=TP6rU@)Rml5jf>oY{3qK>L3RCaaqJ34Br+R(qOh3)O$8e5H|gMZS4X+8L%9&FZw=kVWu($fpcq=pjO8uj`{8?0i(;i^Ctj;`u* zwY4E_z!OLWpc)1+*f@YA=zwEhON-KyX=-A%L;MIoidky_&+t5DnjQD37gV_o762{4 z0WH{lk`SPxy#j^k#aWIfA(R`9gU{liHxA?O!p>q3* zEL3EOgA5p#%jQVw^Q;paED0&$Xj_5;mD2Ay#5-na+ejumGJ}{{|DSbb8u!IFm(FR@ z)R(AMi*y#)geAG~xuaW>5uY6pNvEa8zhv!XX8CtC6@#Ry2unjl{? zGkof*cx30&^#|9{Uye*{+cC=g&HEslUA}a$xaNMxqUM3@vY|~&cTR2F-ca~rW@eul zpP?5G@q55Y=E9dtAv1pB;L$sD|Dv(?`aaK3FFU+{&2fs75nbTh+*v9>WvB(z<5V}b zlv+kF6HIb>Uq>LhU`bz}qPM5Vz;pyarQ%hv62$@xib>HK2;5$;6U&mXJOgrk4sC&{ zfyXQ7tH25>O!Xr>>IXUMn^Y9tuS1c;inHx}d%MtzVc0EH7A&87q7$99E2c98Pb@YN z<)}-IEG2S?f!E%(4e1q!e`_U=ghs|>d)rZWJ&yh~Ql3lW-r@Dvl z?Kuz9f;ZQ~w*sowD?#9tt*yNSxo`I4cYFJx8TJ!e6^}og>ko(fbI{x1mDpUo=o6JW zts4()x~(eA=PCGe2AxjzggWcW(xQrRoec!Y7kunI#&9?* z0#u;b2W(n^w?`}|H1!05sxyev6e`%MVpF{o^ui<*Aftqk>J@rNd&hbi9`@oN%=El+;_A_ew6A5>%0F3g=0)t(urb1b|QbSxxkfb2ZD7q+i+x`F*z7*iNual)+;O@j zf2R97XiXQ8$nFCn!becPmjY$+hKYa#*dh;h+0KIE|ANdxhNYhnk1Xow=Z)Ncxt@2w zx%z7-Xtr)nlZ!5T4w`ljG%qT6-Q;-o+LS1-k=c*m4 z?wxG)ilgJh%k%zCo4U3yVxBt&ek|^O{b0Uh#g?rXGR)NGU%U(7&)*RvnHF%KzO=jW zLb7oBD%0JiSw^RlaD441Vf{$k9`TBJW2P>DDd@k%GQ%U=HV@C_ZWGjU}A&z2|y7K_#DWV|(1WGGcmX~;y_fS^)4eY{Vu zX1qEc<>*hJ_3?Z%q9mv%#zGjAl{pk7Ytd0=4w!Km9Lq&QAf)CrW=*H2U$aHCTQi}N zDUF(O%?Zu4hC!dJhM1kh<#x@LZqDW8az!knkPr&<0-z#b3((3h0-!y%GIlHm55$Nf z8#*5X?pP!ea|aljWeE>QB8Q=HG#+X3vKS#ZM#u~00_l8QV;RWu?K*(Mbrq}AQ8eC@ z6go9q>H%s9-yU4~}##)32P&SqkaThrbNW;5Gd{tJdMSCVy29TS> zb`n`HEb%4Ta?_mRQHkqQnoz+NpvtgbC+V@7_)sHdDUbzgGp z4$WMGudf4+p{I7omz6Z8F```n%<-uC!)&r;bU)^nB zIlY&@RoJ`ey~1+iiigyN^^eVmF2+0_&y2jdZ{IsVqUEdD)#4|>SlGP}#XDG`VH{ah zGxFt&sUB(x{i&d}$GKRbD#SIefT> zWCQeJpms9e%QRt3c5`zYz3LT|>Zo8{560U~X|+a7m7#nrl&oXsyWQVhJP#9X2=gC% z>~U`K$AZHK3WR#6>7M{mby z$5_WC0XNHy1l=rDgsQV0b#?8n?GjBvJIbu|xrnmrKdb1fDtirO$Mpd_`m|SaWTM{e z#Gh=krg3N)*s( zVLhokJ3QHXC0)VDEAT&AkxR)tDSp(Zxql&eJ;$0!?npq377<06ln+*oLp|lVVE7{rfoKLT!AJ2OhT>AZa4c$PfiJa zK6BV)#tFf6t^uYYVNzt$wMvze+>EQLc^!`fqItP045e2dP=OxRO4Z9M*p4S3H|XBb zy`zI^6$-b~kP5oAW}u?XNwZ)cGLM)i&76h+7^m%#ta(-!4Sh1lVZIRj$=Y1whty|S zSeg^9g ziWUBEdacJ%s0R6^Lz_0QeVYDm_m<^*)(q3C{GT{H@R|Hbq=tF^L1w{H=8ur=eDKxb zMzFN~#?OWB8IaoZ^mDtHt=_YCcwl5}N9X2E%SKl1dG_hu-x=I9GQ3-SaSyVl;}3ge z?{Xf>No}8EA+9)|si<%oaOK%#ats2e51F;Wls#Mds$)bu{`HdSQPLM9DoW^-#b`1N&A_s!Uq=Og@*?x% z%nEMN$p=ecg73c=4C=dAIK3_6ClB5e-)4Ry-lggB{PJyu3+K0QyXv=J+x`(~!S$i{ z9zG%a1*asc0&eQherBa#C2AHH@a(h$I?QU)w$yqtZT7RT2xbUbHRi`|MLDs>?Uqwo zwOTGmd#3elPHuN$&g@pKK}Bt#o+mCA8=`DZ;rz3r$1Nvbl0^{joa4&L_u*DLS-5th@Ghs!C-)X!+&8f7p1tzp zRa1b1R)Um8-{^n(N8-n_UviUZbuovoTTtj(7I@@(1+=03Z3z8dD0B%Z54sFdK1_g& zj{N{eEe;8aKnU2GDziBj!FSU_RaI5M74Z4&KnR4hKtRbp1$aO!0dV3@d&0iK&RC!w zA6RxfuQ6&!nq+m-xHX9KECo#)IlbO%Od18_JtJeRtfXALi`GDwi>n}&ux6CoQCyM} z(4ja=&cci6EWF7nM$n;n5gm#}!zv$KP84B`a~aXB`V*OVEbiyB=BzZHOqL);U_=Bj z6p(BXZ^lQC5~9qL))9#(1BSN2{~8Bx7QVn#;^3`o-Rl&_5v(9NA-*drCd8LITR;mf z?j0YuGozq#?_Tl!{Pr7MSGNZ3@0r`IogT|q7dDHh7FL2fItzuFn-uBM{08I;YX-P2oB$w{k`w@>k(VKk9SW;OjUCO}MmWcG+t&ZEgxJ<1baIormb!sencm1BqP(+tXQ8K#-8Z^o}l=TI=Or0eN=jmsB; zTH6L2tsb(C+Qw`bZP#q9O>6d;6K2RmvtUF%-mJouSSaP-d6h<^k_LJbns!Z(299V( zHDem6A*BD2EV)KMku?AMH5x>;*0^_QNf)PI=c?wZ#H@UH34qdFDaxFDXAY4`l?@-%${kaLg``hhhA{hdcRtht*-`%?`6e$(wjnk4|GC z42w!mSyNMMw}+~%eJ$s~@R>zzZ3GZmi(+6ET!iygy|TV% zQEly_o_ZzQ^v#M2zhA=CHhEqxl3p|lrrb|lf8(N!WLif_yIfU$-{}1Y0ZFDMraKfn zhnjnOI+{BPf1V<@urm;7cK9hjq1dxhW{G0&T+YDiiiCLI$!QeEW}xhRy7QNv@cYyc zDcXqnq|i>FwX8HF`x(m~V2`1zhb%*}NtRA1Q1}FzJ1JJ7V5!b#HqlF|k}5$3l`0jk z(v0<9>t(F4x2Xyx7>;axeO01L`jN1O;1nZ)BaS(e?yr|+h@b@K)EN97QHL~qFv~?& zKJ2%0uG>fkvlFwQh{J8%X(Zi8Qn(~F%BDo=7U}p%<$I}oiJl-Yl_t_~{4q!1Y@CZa zBrGrcyG)swv)NQa^)`gbO9?w4mj(MRYVj!805Qq)esEk|0l{*xJr8h+36QPFuwOpep5>HPHzdk^vUv620i&R_Jc68DeMFRp?=UGUUs z!|GjczrCw^{pixpUH|(Z+k`*)(e_i~zzz9|jA{f>g>Ce1k)cQF^MCNxU%C~l?k>G* z^YYzWPUH{6k9STY%ikn^Ml9cpGU7IHL(s(IHPvRb-IcDXQC9PqW8B_``S;E!HCTUS z)j7(Itk_+RDdej{!634=+%yV}TRH4eW!R&h1L3A*ZEZdFp(?x5T_potb3=m=!Nwh- zn!qz>h2|h13gVI^vf)r|vX)QQ;x@TDM*(1m0JxoA+ACp=G)kFLMyuIYR$_G2HUKTM zVA#;y(A*-Iw~)T3FN2>P$wjKu&{szFjIbsoQktQq{WG3a9B_hTF7uc=h^CSigPsx@}w5t`(2I z|9<=&vdsqZzhu|ACnzmtq8vaeC^^U}4A87qXiIXbXVB8&Q2UJ9&f9T~#m1}63O$e6 zi_h8&2A9@?UugkP3t_ZAD7rG4Ee1UXThdAjz3!eK+Mvmz*IP_jHcv~D`9^O}uBA|p zMKO3hJfS|IeyC;*#Ub$$=qHqGXL=Z#Vv>x2PIV0ZnPe_9*O;4(VuS#I?lBBgKCoPh zt%{=+vnnp7>T+7js3gh6Vy$g%+%ZwgnMm|IH=2Xjt-)ZSWuQ@y()I?pC4Y;%lm7s2 z&+j^!-^*P1;Pk{tmmcihar5~8!}N1-;o-yi*A5rF^i8np4zLc1SKg)N1LDl*YiIC0 zzZ`jltK8pFI?92kU%~g!I2;Nu7AW*kmFOT;hdk&@Zss_Y<67}}vjK}>zT$@{qTzWk zzXHQRz)Ffj=N8s^G2jBN!B07zl;6N;OgNV|o2wKm6;7K3i=gp%y?UoBREZBd=&E#8 z%Jg~%A(FL@>H&dy_*EF@q0;&T0hzf6%QcZTG(eMpsXI(Etqg}jjVQ$e00q{}J6h1A zeO`M&`?B^8?K@hI(t>t$AK&{>`qj$(M024H7DtZsSd;M2=aDYCW zEUXhp4>9ZO#Bs5u4(y}l)#4#6bh{5*hFkol>=R@eVa(M^Ed=`ndrb|jpdr-D++Obj zAs7mU>ub~L`UU)gpMwFa18e+H_J{YT?RK_FHopShYo!GAyB5GVQTo>i)=bCvSPTd0 zQ;`;@#Nrx`w6`+#6_|={xRJwo$jF5EfEa~f>4{ri(gBnK@-~d zwSvu+t;H>-TI|YeBUBY%g#`#04XiS&r71tNxhJ?XxFHCGdY(#SG zrQTwm&54nJhZ3<&QF#zZ1gbh77A?wvIp`~))O#JKU!pUqFT~46y0>t-?|#QVQ^Q}pC}xVty(2c*0q7uny0zKZ_wh56vkjJ-FftA;lk1U2pkg+ zPF=d3U3lcwAA_EwbSFAB-QuswysMpxQb{l*I3j*eA|7X|QJld<9S)brF1JVg6`T!V zxaTy4F1Z#*b2?qhjzY3$rvz6V|H=|n97SaAoHo&(=t&$)FdjC+o9)>IuQX&6iC84Y zSI5qS)O0T3@p>ffQJq$6svy69O#9AJ?cN>~$D!!5vKm5dHHxOHhpH#5Pgh^8W;KMQ z<%}uq{P9(a*%%$lf+fQld zHO)>sSBBFZ*>-ICwxa-5K&ro>&mfUI5n?C8MoRXWn;DQZr+|z;U3e-CqU);N))fmY ztCM^7?h!w#dT>ns(Oq#Hd{Nx-t6wvP7sNVtXlQJF( zgwLD-7QGo4^{nW~U472VyWL(KKMkJI>5SB?;5(E+@el7!@f0tKZctXNZV8ppYPIXg z>UwfXYE%+ux<}DUj}8!&z7C?mL8ml1@T#GeaX3tbkxNJsh6`H7o4lr^sn;}Q8ZmK{ zNkBJ8O=G5urfa5~COHpHk_H5aqP_EkRHdRqDd;0vB_(m1;8JL8wjolaVlVPFZd_45 zH&aApHYG|VIEeyU$4@?ypduJHN0(-=a7)rKTS&#FDlhv7*6{lL z=`HlN`~~>&iNbm2_Q{jalc^=ufLDy7U>Y+h3RLRhy|XEwFAzX^THD!NERss6BuP+f z+R{Lo@UI>djF=jz1_>F|M2!W&PyhtTMy$*dV2IP|&Z5&uiy2lAP#;o^83xi6l@1_Z zNT#D}s02!ToVjQ;QPW=2Qv+*cayf&AzLZd_WirHtVh&EdvX>cQFc_X;2xd1-)S3p< zztu>Pj?l~_F)?mo>*nS$bj4~w@e%Oo;u2tGakL$G!BVNr+-X@RDgH)YVgWZ`w#r_k`?mm^0s z=2NRDI5|@oI3fO9;a8O{;585|oS33l72afnh1cm0_?bAidefSbP0KRd$5wAyTR6H< zoDsi#X@>3{P>rt`D^$|I5IeujbiD?)i~$bp7m;197eAFPBQ@D^+_dcDVnvmzT95awA0nM?dofCR5|+(PXVf z+O|a!J+ghtkO51a?RqXbvxq%6qk!BC^t~6py2f4j#r3UM-=4{iymw*uNi`VQ5Bm1g zw+r?Mbz8Z0_upK1UR<;K$_JZ1pjgT({&n5UH*j;Kt+-fS{aE16G8BD@WUXD)zgx}(IA6+^4=JR^aNwEpbP4tR%xcpWbVm= zLL3yn%(10tu(X>4>I9W3tE);78Z=ygOE7j>abp^PJGNoxA{$+iJ*uqgPz-F!b|+DB zPppt;rU;!1m?g2m9N@FO|LLLzKv4;x1bx>%qKs7vhV+2UPvbzSD4l4Kw6P4*{>uQ( zFPQ8Xph3KHSbPPD$CJ(I2o(PtoCI0%JjaQbMB3jCJ^_oG#arT*XdBoJDwm4)#T`Kb z-BiIIIx{v_cxx=b5WZGeD_-er6X```bS57acZnApy3sB>1a6A2iTUQX+n^g<6W_aA z*Yz%lqU6vn{-^9F_XA3g@}D?WPqiSAyO7G_i9kr7tBFLSoeLFsUPLLFkpCV;X48vS zr59y7J!q?~5Gn#5PZ*g`JJwM-6Mk#~hlfP;GNrjE5SGcpfgZDxdm_q5@vv-cF~1m@ z%~TfyaQdTFYnSj#&V#1}MW0SLZ(c(~hRdW%(>^hYPZ}Ca25E zlrklyW(moT9X`fl&U!@-7fjGyN{{cVa$1UNPdRvxcS+Xoyl4aSJ?d>Ouacn~zV;N%` zhY-i1;4mJJhcMtnIUWz=Aujb$hM|NqX@--g1H;3i2}78a=A;=Wp=p|vrZk~xnx+{- zXv(Aswl-(2wIx}Q+}m?6LXR!UFYSN*ul4=k*G#?G{A+YaH*1^8+E(ykD`-U{%h0kv z_oBZR?oW$_?1KPd*}(ZHfsXv~g3w2u=-f<=l;SN_ZV?7~$r=Xd`!=2W7YxLm;CHoRf zN@)EGL)Wsf}Rt z@X+!O)ev5qc!g%GOTOKcr-Ttv!E7VI$ncKb|KFzy?AJGnwJjMriVlGB5&jFC+zE2ZQh(BrJ^7Fr1Do znAC0~Wz{1Him6BntC|wbrx0YVumYmp1X1XA+pQLz-c)GR5QR*kqmV6}Dm+;zR8tmx zp+-}vx6q6o?~i+v=>BtZma8Dw+s$%)tFnM7>{&nM zB~U)ih(&)Iyn*M~?}g|9+`V-hNVued8a_)>r1P0$;OEcj^-NEn%@l(rfrPt8k3M_u znM3Cm#YYFGj_#pi7xoM`%^nlD7dI`;?L6tUy*YXfl+jM^0=IEB*aJ3*%eY={b3K@( zO)x16c|Z)PGm4)d3I7-tc0QO3#90j~0yG2kCd#fi*%*mH$DlI67 z7upMheKD($%ZnXqHQ|7s!r>@3qZy)E8b)ylOcxKZ#f;WMAl8GB(zJ|o4K76Z(2k>Wv zzA?2ICV1v$+SX;TYDK&X4Nt+h%;=2)X_1_wCuI;)o{|_jLC&O?NSZs}KGGX{wfl&( z956>nMf!8_5oaPhZtvq>J=8nezN4+}&A@J-?%O*J&T+|E z&N7?+cvjTUO@)r_>z?0wTQ9h2`<%O}Rlkek&YcK6zrP^luo!R=exd@+bb%lJLM4V7 z=?GCk1W@@7QZOi)!TQryilKf9)?-oShmp4AX%J&#h^?fOh`@J7K>z7*f=S?XwicF@ z>*05;#^YN3r{atZ@qoz*HyT&<$m6asC@T>8LQDwdwGvNlC2bW1gAHC3sC2B)=M9Fe zD8r-7=CVM*qjtM_b$E5s8*;ltUdk!eJMkH=jK!q-a0I=qmKu_g2waPV9Db!-E@h;s zhHY8Dbqy9~< zjr)>&^}y<#?CJ@@?ACL~ruQxF-o9^gdXn>QpB$d)6|7o#w)^|vTe{NMM;_&p+)3`( z;mP`ACg;JCpTjjh6S~cPX>on>a^L0G$6f`)-3Oi>JR=}FcTV^1AcTa4Tg0=246KbK z(9&v&CZzpqFP3PHUVCu~Em069)*=%U-xxlvj>QQCg8}eGTq*aRD)+G6gRiE{w91Uhyv)T(v$?Rs z?=LIUIgF@TNgAj^BWe@Fs)b<~co=6ghqX*CJdD+54l9{Tct}=e4q-hH4^>`tXh^xs zl|g%ultQ23(h^!m3tvP1%qq4zRityoLvivVnt9q1+ZB5wM$N?lVkjrkO#^v{r02+^ zY4gVH?v(leogR0KK1G6qPo5WOS7+k6&yc>;V(1IBhI!<5^79o%S zzA~RcJg1qiyW5!pJlh1{%C8D)_d$BYYkRIpv==5fFJ0z7A~$ei@MZc689GSzrH_-2 z^fhw#?cLnV`#LsukJtC?6U;7MrlS0H>BA{~xV)$A4)w*-(t9*Xvi-8*P1K&fJEwP^ zIRkI@?xf1NN2?|`u3o$Owwv0qm%F`}+Q2|(D`PGZ(3v{#n4$guAk z*~?ng(pBBvK2hiW_Y=ixqtRd>%yzN3wX&{;qM$z=hMp)1J!um36Dwd1K617!ny98k z2t-4`>y^Nu8?ppwaMVK-_Its=Vd5MLu(RK&7wJEG^Uhsiv#3i{RNRd)NTW~w6Y zuHc1o%6vZe{Y>Zm=RlbCry49n7SdwrMD3y>nxg4WeqMqih)x4x>^72UXP_#YsKSYV zbpy(}OH#_jP-0i&$;4BMHxfbysnhPNezKZYQPr%)o=j9Hs^ifpvg7AdCau=0(^=ul zI+N05N{X@+BO^LPoor`!C*2hVL=;GM3?Tz@sxzvS$#l_98n{u;?*?(S8cZox(XAL( zPzpr@=mI1Gz&JPwZUPGAbRp38>v9SgAIu@nGL(JGg29<4^fLKHmJ)|A&@&R5xri{7 z!%t%5WIp9UHnIRbCYWiEWugRL8}Qx`&V&X-4oVPFSLf~WqNG0q+=01$PBcWftQ{`$ z2=0v_PE2~I((&YXz(9&}(S*OH=BbnO>B+_0SEsm_LH8U|wq*Bj9vhjM+kM;ut{&VC z_RbpB+LnHcDf%u~{sl?GOlC5BUAk~!a5OqFA-FU-cxra%(&+Hsed9CWH}tl}9rd9P zZi4zfAk4kK=R*DXrm<}U>Gwzz_r)u{A92TaY;E6XaqR0|xJ`~@V^oVukWliQ8SY;o+dwT46SlBw->d){k^m0AeIG!j5#0&{FE* zijk3uhpAXye+<+3MpF2$qYI(%3Q}g1sYox73(%?>Pif@-A`t8c{{Gtjwf&FvKhZCc z!n+^zQ`-KPei9nCH1b5`2a(@K=m=}JCAE>32&wDK2~?%ISj~YVVXisq@#%!_q^F8 z!1HfXMw@JEs$=EeWF5aZm_=6%<%8|`7Te<(T9@gxUn2JQh9NyGuF&l?v;jewjsrs3;u0U?Me-W>om%9Wp5iqM`IJ$7jF z+l5O9Zu;t9rT#+*<^|-6^hI6I_c~`Le(Lgvd>*6C79B2k){GB~#sZtZ8%ht8qnu;L zcyJ1g9?~<=WtdE%8(iO)z2rM-@5Ox^C&N9vsW5k(x-vesXV37`=1qG>cW$DZMtV2) zje`HR@w;DvS(vbzxVL*+u7V?Adi&O`+o!n8e{<>|TDaSnYs$lH&#c9Ds%LgM+&I2A zMoPHu_%`nPj)|sui({tu{1^1}WZD_$xEE~Y=SlI!i`>TU`SXKEK*#FYso}$d#7Ot* zfexf{#{E`wOZZcQCECEBvzn&H)|N(iDJJXcD(QgN>xrObw4l=M_E%Q=J;k`vwb2fn zQmHXE5CoFyUC`Lz1(PK|@AoH-CX*fsWu9U!Ej$Z<3tCVrDON92sg!a#VnBuEZ7ZYJ zC5cw1m1O8vMyg1*%3I})4UJ4w<2evHl@j8XoFP>ii5LZSfk1=b@AJY{?DKg9Ry;A} zP8C}(TCZ41p;c%lnj8+MX}sxV(?z&GZZ-*(O)x+ts@z&}YnRa$?Lcxf+^$ho);n)Y%4@JUffROw} zx<}jf@^_Cs_wRo=8!=m$-X@#NvwQpacOLwo|0_xoJ;(PC&u#h1>!5_(&t2hmb4%QH zar=Pw=sCco@8eFlw1F~mhO`|y_rt%QPk+RH0Yu>bChk%0Z@Ir)xBrk+|Mtv&nYe%5 zPZ#zkgddF#k4|ny`iNW_cWG7;Yrt!)FHu$1Qb~4pw$Uz+$6%Id?7a^@7S)MY zS{jsV?B=MXGE4_T2&5T=cDJ4_Xlttp8lu%qH7dRpH8rVjf-u3lTUcb3V`53xAhcS| zVv`x6ESxE$OeV4S274QNDN5P>78C^Hlb&{vL zS*bVK?ev$rTpkQwaJfporRe(=rh>sP?^E6rUidW^z2r%FbH#hpOM5dZT@D120s1VX z2;#hqlHqRiyeq-ur&+Ruc{v~nVw1{|IP+~<)ED9&9azg<4RGQCP=Q7UCGVzzJz z1~L?gNRP)x)1m}o+g%8V;HAQX-eH4z+z6}JD&WNyip9KMf~c_BTH@hwZ3yuYRj>j< z1&eI15tdWXGsnZoyn?Cs8-ckI6f}bGH2%1eY(x$jm-O1WD;|wg&|gWZ#&~1A&RfSI zsW9lFqcI-s76=3@DuT5^%mU%Vqg?uw&0-OF^m?yWt5LxvpwTFKs;Y&QOgIq*QIztP z!XFZ4x@tieQoRMzXsu5u^wmabqQ0QuVm+9uzgT~z{#rdd&$UA%r9E#(5TqW%TlmEGyPBpZTX*HRWZxzpOV$Ae@Lk&u#0M0j#3s zLf9O}^M}GQA|92-XHfD9w)%}L?wmVt#HIge+hMwA@W+3BAWFZQZW);UAUxLF)G|zcz@4K#qk|KZ z6KI}Hi&?cEB2c#$7=NyP^y4jsZ@~^ z<(DFB8DeFuiYNd`UP0=Y;@NSt-Dt;_CM?CgI$9miUZT{(w`sL%wSi}$@*9O}wb1CN zU4*Mqg7{1c7v}s^W~lLFtS2Rx3!GN#S||9ebJ0mY=>(+GX?F&lyPUL!av}(xlMvva zMp8W7FGw~ZF`ewj866~c5hNPkC3B*;_>tGTm!XWbu4ABa^wmvS#-D-+2ku24(ZaWxf zxTd{}uhQ}H^j_{@3kzCE({`|%d%F)IZtoaG8-zc?8r=cnHY+z+8IQp#axfk&u)Y42 z2#Kt(XQd(^MFnNLLbbwyjuouWZ?^}mg$jkt9FU<%EQe0TiIuEfPQ;j3cPfPytxu)Q z2-SBcWyD+owb0S%RAxqER;kPqitu<*+UDhD(hE}|4bW)vw>gC;5lR#HzAg4_?>qQ- z28_;2d1cxlS(Vesvb^cC#6>0$W~2;IL6Z@?xNB1qugp5;C^l zY|x0P(#BvYyal;5cP0LsZ)uBT^0z3qyA_t7Ams4?U=paQc9HK9j-#c$ZvMRC99+T zewkP+!niq@R*B<{_(6yXhEM@+!MhtlW{BCs2$U4w>QK{+1BML=Jo_PbHJWp=(=XBF zQ}E^unj&aGh=DS0k9Wm~;%~$Scj90;I2D`=9u3k&a2Vd53|F%t zl)DjamRl*4%g}G8N(JQ&@-8_Ql3$dQH+j*lWxa(g6l5qPSczUD@LI=Mu{`aZ?40J^ z)0)fmLEgwFpGn2>o*U0z63I%c@>f-kAnR*Vm3ckmW#0GPWtm#B5(f*s-=YRW5dnDd zLIM^JJzx{}F|@fQK!bye7X{Pl1wg|iHFpDyatFCD=blmPk3Boh%~KuQ1iGV##uhIB z9w=?;;M#7#43;chr*k!!vv7yNBNu1S&A$Hb>^|>2Jl-QB~?K204{eE+*U zXSUBD;d;6E%Im>x^7XyxXSo_*Cpd}}RcUCkN*FPaS{2HKSkT7GonCW=)2sKJD+~z| z^@SnW-dQ8BOuN@bJ5;j$L`QAkm+l`)?$?sXLw8a#0WsyXE;oW;cB zY%mZBpafo-q9}oqki&E$!_|;Dl|t}(=x@-RCA8Ptx{QJ^>q{D4lw!2e>oKtaSB&U-t)M4l3EY3M6_=Tk;EHkgs z0|%GhIzWbBFzL13eKt#s`#e2*GhOrjU7vq`@Z*ms#^^`Bb)kDy!i+TaOzad+a0{S) z=`GOCEeNZYLYvY@j?HeqGe$81v&xc?(Q;vT7c0pBb4W5^ z6gcjzh4~%V>btEW$H$9TKc1)inqvCBVG1?f>1Xz=9{KO2{m>F;=F;JAAO?4O7yUo{ z33m&~4sqP}-6Xkl_nR#fGG-gwJ2Ok2Avs~HXLzP(X?i$)lY2H=4>N`Va`W#m{lHxY zezO-$ax;6mAy>(U8dpjBNAJM+)!Qu@B+dSEO2!GnZGsR&pCA>D3xABWiV9wr87*Cu zU04fHazVzWi7?_C2pVe3#fWIKpgKb1>@is269^izsmAcvQ;h~b)nEz=1cFSeaV8Zm zFXtmtWkqcTeh{xkc?W!{szy@7E1Z&&a+Dt`Yk`6?Gv&2riNsu6POD%J;wnjc5mv*i z(GUcmsi2Zb*QEkz?{iLdQAKyFhE*q3l!`CkbIHa%&LB@)Cja_>lb4`9NVM-5$!xCh zwK;D98Hl*9m`_l?!3X4@d5reYT#*%Aog?YFr5Sj;SiijfAAj^xa!kVX^mOm~78M5* z+$>3>TmHH<115w&=C*(J<;aLooF0Q7;8%N5`m&e%Jy&sj#8sUB;X5=*4kl%Nq|aZ< zSx8U%UD88Na0>7_x?&}Fjp%h49Yipr!L|S^S52X&(kZLd6jCCKQ&t)+4f#-DI3tQy z_(@FQ42vVGs#K*@NG$xy_mr5;rG+|)L~JM(^CBXpcW$07Ep-H4HoWp(E{D?z4W{8# zDqP7IxgttL3Bo?U7%&v#39L{68;3TNHh5Dixx*Lokp||;&>JBVLp8!aLr5+U8GK<_ z>9YzQEGU9}kWiwU7Fm#z=K!KY8HFtr81nY#aGnugx15(|8#i}zG{^s6b5sOC4@#PW>#m##y={;dZ5e@vfNc=@t)= zDvw(Nj*}s-Z}H|lTKn7`(I(*^5~d8!{66A-@CU3~cAoUCfPzu^8C zK?2%`6D1MQi#VMP1ziPXsDLe)D!5p1r9g19;AR0?kZt_kn?&%WmhpA>rk=UoU2HsA zZH)gStH^UDhHSGsg3{BRJ3(fIOVBE`8{YvivhMDUa;r7lic1%s*yg7gE+Qu=G92+~=zD=gsn&_UK6okPxZZCgbnkL2Y z(Ht0u*Y9!LIgX88eC-rCLSFys%vF-4dy}%wU;;rSmWsXL#9lDVP3+~qYj+Mb*`4Vh zzDd(`Pq%E4bd{9WgPo*;o1VY&bFgzBY)35NNK5ry;m@%DX(HN)cJMf>(i9qGPD7!F zYO=_j;dXoWj*D=YJ5~%+ab>QqhVIhdPGWCc;$SO(ntAP zp#j&NPPMFPMfc&*3IRS|Q7XoiX{AI9J|>VOs(U_VsD-gT%3{!1R4Y@-kwdCIosw6S z@xG=4kq=aqRs<=B&FMm34%Nr*BsK&84QD|! z!T&3l^$i(IM8)$vYQM&g#^YzGFzq;)W)3cWGC#w80VD^IXw_p6(UPrWue48!)tg)T zfA|D>;~;nsyc6TLrw`4O-u)J}I@zh$mw%q#$5DsV40$ti{g)-}_U+VEu@n5w9N5F{ znB&%1Z3C>smVV|vk`#7#DmIY%QaAlBce#JxzVB>lnwx5$vKe=GzXlZPOP_Odh_BGW z{fFpn;R(V_)Ile+8lJf!GQ=i2I~%(+Mx)9^KKS4of?2h?B(Or{h%u|0^g3PJ1B$*r z*#p9Y0x7+sWHr?v5QR*zmd73sR^0AD}UrFirQbY@ln}8kt5c#j#GNqMDl4Tim5i8jI@Vo5f!TZJS+4)zz)mCs!kTlBHI56c=}NB%@)}az?dR zEKb5d>cIz1eX48=Kq2-Autm$H(iP(R6}SXzsrT|Csxa$v7Y)@vRez$Mii`dAMMd>~ zaa^G5vL}PduH;a1OH#0m_`&^QL{SGlwaKK{@scn)X!)|#OZqqTq#j{rkOIuTg1E~t zu{PI=N_92- z9`|g7t9YfNTr*(#CA`Ce8J8;a6a_KVg%)`3P4~WG+2p=TlSie^%VKBf5m+$&yBFW? zy^nhy{4rZO9|}@{`>N|FcW(Cm6ZZypi%VG2C-+S+EzN)Q(a>5_^2pH3?OVmnWVrA9 z|BYNtNq3DL7<~-<$?cP!WMwcq&DKQ7$YMG0{Osr-H*u@#o|_o#o9g&>_3!r#Y}qos zX;Xc7`}Ea68hdp9zx{Bbw~1m)yx_0rkiVbf9=6&0n~EIiKZ2D5y}Cy>M#{>4q~heE z)!%($n%uNDRljMoaeDE3Q`4^J*HNj?&D(o-AU_T(ig$#ki6WRqCZH!zf{$3YD;iKq zm8B-y#Wq$#I}Qh0Rm!$fzcisW(S(<1WZ7taBdRUy!5X+cJ+RjFqhVtr3A>YJ1CM13+@pNP}W8oNeQ zq>_^)p)4XA<#_O)&1}+C!N+ZQE4>ZG&;WU(y)oF>)i~6+t5Mj<7JHM*R(or(6{+0s zY8C3KR`gs^V{2<;5mlovui=NBE5agCI9%>4#fgf?=MFdnIGI(daAhOQh#(fH3Isl< zQXo(|eYB-=SNW6WC(0?JJXGFYPD!b9epm+SbeSyZ2U%E|DrrGQY@VuJb^+zpusJ=h z%sN@-l*j8YI}UQsvI*X`YL-=MFnA<89fEKp=y7Jrps$ZCLq~G!BvhgK#xi$#FbicP zJa)Yj>(tyOT_dQ%dewNF(gGs}%6u6B+!&CY8(-k+KY4TWtxM#0T*csxOtk2?SO!?TE&WGtK}%K) zqFAwSmQ&2ngU_ks-~RUMz2i;X!MEQwjlTT!&NHvP`OM_W3rLy4!lgymgnx{4k~(NF ztvE+M!m6q?g)*_GP-Kybo#oE5I<)6wF*0!;1Tf`+4cbZ03S`!}8LqT&_~bT2peu&_u_TF5rZT z3e1XKHNJQ-QdO3S$E(m+s8jZMF#b}UYKWhRzaRfBPRH|Rc_XUGH)JgL<|^@QnvxmL z_UBDc1ar;U<-BEGFqoISEF*ujW%n}wW5zPSDT(uOtHD8^V{DR{Cq#y%F_mctheGBX<269))FLWxR|(1)+kVR7T`jo8=W@vJJZs*Q%4Fo1*YxWBQntD!a& zDzED*KL;8YQe9nb%JvqXb+^5}E$Q+2d~L}#T)T_Byycli1PRPoV&;s#Fp2sCkyEJv z!T0)!026Qox&yR~3IvLao!BWN#62wROcJRCvn?5()XpL z$X{G1jYOn%#eR`crA(q^gpyN9BubkU+U&LrM^l&mNjs^u+wBc@N=DiBa;;JWqe1Q2 zlt#s((y|7oF@Bgn)NdINLeBC8ehWa~nKF85rP zW&|7lf4vasBgc+|Chh{*n?6srTu110ayQ9Ze#iYKIgy?Pdy#2#=fPWZOMUy^TUdPg zzdv!~#{Q2!+B7PZY~A|v?h!FFT%Vknpr~*e_vH-tHtn0cE|<2nGc|KM&8@k`w?Wwq zP|qyBFusj;#5%Z-s%pSr&Z0_kmfI7HZ%EYCr28&QXtJ|U+(E9aO(5^9UOaYDOynMzlrlY;e0Dyzd<7KmEYK?+wHIQ|r6N&w(|kcWioe1A6-5 zmM6CU5~d;Ff;quzq8X;^Omp+M9^SG30jH=@E!UUPljyrjywJ7-Q?5sP`%ss>5A?9| z#^&bH^$$P7J~Z^m=p*Mq^!ZfX#6-h)Ht#$KDp*S@xVdXHNh&wnH#cmiq?5`D} zYL_2I_jLiN!x7}cn^Ug+CtbCU6ATzhA)cx>1A&vLgdzAvXX*2ii>i)Z#V zZC4w~YZ9^eV0}!i4mZ_^C3JeQrZY^wFbTZeJCh$za<75d? zp_gbN)v&cj`dl6x+Xk6#sY;wiL~ z-_HHJ=o8_e6J|8a2)@ti^wFr+>+{7tf^gVaWu#27(1np}{TXXnS^1) zYPFU`$Xz9+g%`0%Bo9q(17B3}p zP$X%0@Qdgv#|eiJJ$%YR5e~S{99mw+Rf}2yYPACMQ7Q--o;z470`9j!Mfy?fQgi=X z^ri4mpiRZ_EXg`z12IPa6MMh-!3QM|XcY>X-sAVX0u`y$+Ta6qU9ksNH8(YNyYzKN zqsi3WCX+}QwR%md*DGA(92jt{skB<{k*-*Kdq>B^fne}q!77R(Q2PlCGJ}l;U@b93 ztOdg`SgwcXbv@`i`%rB|yt55$>aQT|usm@*$~^i@_`XqM4R{pI-m;R&sKYe`1RDhi zRn8VfRy;&K+}*utBZ_AmL5fwg52rjH@0v9+e?>*@Ww~sg{Z36Gy1NoE`ENnwu-R;f@RrZmXfll-C5#GqI=e8Feh0? zSp__lg{`V$Efn>jR;ym6#-K8WP~!p}J(dv?v=#zE^etha48WW-5GeKHHLZNE9@(pnw#)jn~5e6g>N-w0PAtqnUa+@8;#ASGMZVX z5Q~NHGiWm0OfhH+k`b<=R-vD1ObrbVQq|Sn#4rLG+@yem0$miKP$h&U6_HM7FfU_# zJX>mGKw1W$hSy4jXD81*m&+dG*_fot4U5G6{@lEIJfd2b5XCDh!oy-zSv~GZUMRsy zW;|EZXM$8bHx70qJ2zad%8AfM?ykYIK;v+3R4lB@pfK-kjO9WX$gNmDCW&$@wnUw4 ze(@c%%yLFSt5|dQq?ry=N}x8I8%8a-sI$?D7o6kA$^`MFzBsvIXZZDFx7o5=$IfwO zZNT{RV^LqZH}Uae8wi#8<{S?2NynkhTl;oQf507gxPa@m=gzZ<5B1>kJecC95-;o= z-u%q`!snjQ(wp1{;WlosMCN+>=*vTlPk<3@Zf9%SKxcZeqq{sF+kI(18r|F0c5IOCXxh;?Ffc+z z!9|YUv%O_zbbNI8hroDYu7;ht)L}AmecYSC#r3gm+m5yiWUJ@4d&{=RIydj^+l_YU z#dj)29}9m*XoxuR0MSPb5X0n8S&u{{>1=QB?oO_*U9+aA8WA%qTvknIM@O>t!3XQ= zdKxr^D_~(ac&gde@B&s=?Pn$I>gsAiC>WG6NN!h<@}dzkS!vmT$4To4&2~D1n%7Y* zFDVBgS}I@#g$nfwss}T{G$)y4a>QC#rPZ#3x%9wUwlMi%>w_qJYe{u(S_(6DATB}SbyvbEI)U7N#U)57RlRTT|}h!Bucp{TVCNXtN3IG_yM!=!>nD&|qP zIMo#ZaCu8D0YJff*~ zCetu_sWG*39gwbjY~2&>jY+)F4nGl5k6d8Cq$Ac4JL~0G6AItNKLHSnT)P8 znbt)qvD->mfssPedsU}WG9~)^WGR_UBPK~gireXNSWY4)I#MbY6V9h>fUv&1g7oH(Y0gT(K1b)ibq28W6+uI-Qc>dh{1hpE)zy>Mit5I zO(VY%zLe3@8^W}dnbZ|Ij0|-M68plD7k5;6Uu0r_Q=M@kIZ9xD_c1@E{WOP^3nE1W z9v}B9&s32=xt5QZ%cDTVq_<60vqu+-L>r9d=gvowfq(#w4omm z!1otLDvQg&=p1-@>0{ws+DOi&$EodK_PYS_=E82UnPb7lX>f|`1enjm34_um> zIyCj{6xA{ILKvLfJ~=Zpxt;3{Pd4@RZr;%H4)8AQ4fpj-PQTFG-NS)i-^Sr7?#8(d zHEi$dZPOz)MzxwT zF$zUNfkXtb4z`dH3MDY(b0SrQ=b??7wR*kj+?|^XCcDw7x9U+2rzP|-hbu^_VMeOe zGKv>*D|Qxl$?RlD-on8!PxFpFMI3+3_!AG}2I*a& zII0k-Jbqe!#EsCv2l#V893S6Lomp%aiK$nN7CpCckdtV+Bj@x+_0m7b(|;FhAsLUN zApJi2%-efkZa=s%#dUHQxdTkcCg;01Kwo+jH_s-)0GT`j|0a@fasMK^CKN!EEg@cK zB{WT`L|Uy%W}`t3g&m*=(hhQl$bCBzAU6E=MH(&$E=nW-Bao<84cwPOc@i zJc*KoD1;Z4K!whTl2G9mw-Rl*E5&%ugLc#rVYOUSZ$TZH0$= zb4hkE-BZSrJ!jHdnmD!}sAtrSxMM)em^hAmhPzGu_zEBnAHG9eIh-EgW?R~Uj~t#$ ze|}`!)&-00l?|@~EyDJ;-+4rIL1@SG0ZJHycAk|;B?>jFO&2IiCzY#kiZ5Xi)Jh}} zs{|O1D`zsC+H#eg5Q~w7-075qdUwxk~Q3lOQbL=qx^k^l&y%tMnO z4`;v{%PQ)KDkV3kvYg}J4MB@!K4$*QczK~9Mxy5EYO8sge$DcT1P|a?2B|ND4c&+d z%tLJ^PaWa*9wBd$uN(yy?!!C8#5IZj&26*kn)I{F*dK85=<4XE&i#Ua`|8*;7X;hE z3tZd$;$`lZFgeWZqqR$$NFlkG6V`KAe{PIl02XAE3htjpH-v~<+Kn)UV3=j34o87g ztyik`CZ)@yHS+9EVlcc=P#`n~O7Je3Ob*XfP~>#E$~;2MHz;yQr3Fsci`hXV)Y-(N zM3kIz098P$zd9{O3vQQa4fw8r=f{M#n^>&Dyw^2Zh#}+QA%Nr{I=(EA%_P;RXMm}R*nZXOd{D@YXe&0KZ{1X0_C84{7A`0!A8f*d1Oc`Aa-~z;F5Y zoet<3OqXGRDg8!5d*|jUt=5Wa7N5ElJzs#h;|T_&PAf5cW$JXSq)3RNG$N6*z!#u| z5j&=U8dxyILZKQ*6uFBi=wl1m2%3Eo6bbN%TZs$b5gt_J7p4N?aI7k#jDl!16s%%Z zmLw4bG8&T{%aoR6Rj?{3Efpi0%4+t$lvS)Ov6t*Bd833LDtWTxsge^VcS;0^)|`|= zyOxzeuS%3qMXaSHS%mrtMFzcIrbK*E4hC8;!^p}kyhSBmKwRvSL;!8bE#p5;T?ZvgM$k^CS)z(tYjYg5salgxDv4|beJ}Drh zpar%-3#@t`#t3n_3n`UVEx<@rT^Fmv6h%tRg<8d0QbjUxV*}R!8XADS0V!@b7#oN} zy;+N9XR26js!$8Gj7_PwtAlDvMyoBvedq;wN?F@bOUmG{K(w!T5%<#E&^1XFNik+N zRwS=jfCZCq4pE)R73ky?W8*43q#5ldrPndnrjQIwI*9iy&3jX zuNOKd_><12!r@Q|)0xC=k~k(mC`PiFt^KN%qYnA_Oa@+MFQ%lE?K)RKOto+811Fk8+|bLYqp zxDRHz3+|^v?3wg59b0^jzmhAKPWJT)o}Qnd{EoNaTOK{oyAjhc}gX zcTP?K3Ei~E$8eL}0C<}F>}+}L1<-r@%U3|nCFn_Q+^3=sgsWgyZoxdlaiWe#;F7CY zhg@Fnb9%iFUwI-?U1s&TTn=l9fjK8hi|BaR@JldA7>OFNjtHTSa>!R!R#2a8p%|?6 zQB`3^OQFDGQH6{&DMAq41&5=9ZeG!dc+6R{3r5~%4U0Wuz9F?OY^Sgr#+YssI6_63h&x zE1GSBimIv#fvtIkROHg(9L584m@1gB5WL2WRvYuER7#KC6ZF6}=FuU#B(^BkpzG3+ zNKzw|lQ21|6OZdo>MrVT>S!IxGTh#z4o}*y%c(=DazphQlvg&(^z$6xkK^bO|62ON zubyQN3m!+7S#x*Qm^@HD7b*<6EW%+T6Cg6BJ`aJo3qEPUScJ?VgaNN1T98XvEBW3q z-s=(ZyP0U+xG`Q5w|?N-qcZr0*vQCL(S^n9!^0#!xM7An&Tai%Fgm+kF@)`d?$Q&e6-7_)A?MK1XU|RE2W^U3b|aO!D_AOCov&Ow`5pCNwVAS zIWe)UQhR2AA=hijxMyr>cju?s>@*Ta#EijQHxTe0eL`~=a2C-?XXjw}8C&Ec$<;^x z;Dj#*cK?0)AIZ(trO$Xn^w?r&eLJ1}eC!>G_T2d1C5F10y>g4R^nV_1>j2H*`}5!Z zeb1K1=C_S6zV#*9GOU={H**tgyMg-X<=nrEHVW^9UamAd>jp+ysn{x(G1ek?NZ~_y zr8bjS_McW)M+_*O7sZRw{M+eLJn}TeN=l2=TDn9gtIZUkYgsv4A}(>eX*I*p==9Yr z;IoHL9W1A<@k*5474QSHRbfOUcp8SwK;~K03=jH3{Ifnq=2;&orhMVz%uyH~g>_Y! z#!x4g2x$q;$ae>4#mwZUQO;EOs~Yw-a%qZu z=B?4KVrECrMsAzoM7(8zX21uWjlbSLf~H^jtMI6|n$4S6A_?+7hBcqXfU$kBLLqZQDR4|`^6a4)` za>a^D%&Omiy0I~e3fJdlkuq7iUxBAmWcrourKRodYB9907kHaGt9HBf#fU8Ah_|v#DacIb4n_l|(JRL&!{c z=T~6jj;O)lL+5^Ds>IO9vdJMcaG1eoVbGP>k|31{d!ivRlz1xfe&Vx)s3E>3PNF$} zQXXduVTI!dNdJZPcl*cvSNuXHWez9eenT>8@W&HjvxtzaTuE27Ha8;^=Zz0ZK8eNe zOMp@W%t*sh2hFbn{R)PZ)ml_^^P7s}1y~e!@k|~t2yG2#2UvJ?Pj>h+%T2kQwBefB zpJfE#cPldkd%4hsHSsV)2r+!%&CJ|)fZ1I}eVIWl0C=M|(?SVnTPJw4H1dt6il(oS zU{_@%BO#7(bP>LyQS*kH;|M5H14ioEh6xr*=sID*+OTpi4d^-y)|F z-2EbE9`Np+^uKq;$ZB9(`g>~QVQS;j--3DoqJ7}oFophjmfHvp%~D2==Dh9Y z;BU!-d9(vGpMI9Woc8}A`WSy|Zn##;S-o2AkrpZnt;OiWv&rScQiar-i3h2(4ue_C z2oMxph^2N0o-cI>Jys{KEYNY5S6cAE`@hK=+J}0Qh zD-@Eb0YPR&?(V|)M0nl7b-9y(lff*Rjz6nG!1oG*U(*hb@EwC?@ODlI4W21COy>a7 zH_M%&<$I_pJ`<^?u zaqod6i*HWM@7c34(9?1qY)Mm;n0uCcCxP^p`e#YSEO2nwX49X~a{I|0m*&P!9R{lh z_dm0F7IU|7zY`r2{uI_bmBdQ$zgY#tuqBQ}dreGGNSTvpLQ9rp*~Z3L$YHHvYEY3= zO;jPoCFnmDi?t$DO&dXi^$S!95hyD&qmyX{YZnNrt(ppn@hmh^q1WGE5w%*~rA~VY zwFp>vbPhDK=2ROdXKZbC#E7cys=2D8RW!^Es>%w>c=fw75QV2K8i)pDg=`~ox2hD& z)>qfmFv1`_qjdi1O=wW}SpCxfL#snQH&l-V5i`NEi%k)P{AjK%Jv-%kP`qb#x) zK_f}}ii&(miV-Majn?>ls-i-nkjYR}oK>V`tj(E}v2tBfCMZNdfJsp#DJU*}N^n9z zb_)R7FX8#>Bbk}Q>?C47$!YxYJi=paVb$1xaOIB@%WB@w-vuAbNt|Fq%%{Z5bUsnV zfy5Fra^x8qlPppt63-I41T%8AJbsK633E6|#QCMxU?ik^ncp}^{*CyJp&@GE0ga_> zt1wrXtI9(@cWwGmO^h0}$Rxe1-#+r3O#jN!y`ivBp1u^T;&_ON4m2B)xI}%lrEn! z2zxRfJ-F=8vMevlBPRHvfiGIB^S7+B1Nfq97=R0%UuPOIcFnE$Tj^b0juGxh1 z&H75Aolcm|tqqDAb|CPG9Qgg|9Z#zua6x){Eshc6WuD~)zz(AZ_%Wqi^o zlv2k3|G_MeaXCY#e3uw+#^)?Qr{Xz0k;{SpB{ysYt)e=^s*D^_{QvOvF7QoV>AmP$ zdu_cRd+ROBdRdk%$+8}nWLdUld)pXCIF50M1BhVo5K0+J!=seLO&M@0LtVxNm+^3X z7zSt#O;aufh5+F*nZTqul#^xx%`Z(e`Q-pj)08t!2sh2-Bw%gdwbqtoL)shTt-bYd z!24U@`X2x9|EHWUWeA)k&HHT|NtJA)TounWrOXdGAxZF)#4CPDkXi9@UsBYI?@ftk z#D-We{9CwkT0~o>(2nWP;5=~`^ZukbIQifv^vWk?@E2;WwlzJO8_}D$3R~2?T>Pa$ zJcK`%af_eI*AS*b*P`AN@VmwR%og#$m0bUiVRZcV?Q!vMW#^|qU9+C~cST>K!Dy#u#o#8qT}^iw_TzariP;7wo9hxOJlA(5)lTQxQH7pY;@ z2YEu?J@SgY#uIIHqFOrPCO7;2O=fI}4T)-Gr%uEmm|ZSV1xes&OZjNjh`j@=sF!T3 z1shY)>^J+lI%9*8W;F@w2)qRj+fZi%T$q4dLn6_@A>gUzY)}VmkVm$PfwVnrAY^`0 zF5kdFh7ouZHIkLX-!cjDe;_`OyuAN_-qlP_2Qu^aW~vPg`S@xXhnR1_Do-g z&rC1ELD3%aom$p?1U_B-A52>O+rP`__d@&F&3@+2*t__@v7){I=9qXBY2Vs?4N%=~ za$o-vxIhNv@SQR7W~2zYYIw8Vp$N4#*0}BZOK>$%;Tx<{8LcwtAb6PSHolE0_Oya5 z{tahwBHN5yF&?L*g;%20%}F~I%o!1Mtx+xZByM+IeXX~V6iYlpvbC*E>GwA`gDjtA zvRS1T;CC~6q|qpw+%y1bYI2ok)UlI^#$q60kyz}`7wU~{By|xc&K1Hnz8Yc^&O)6p z!m|;dFTx_AS0Tv7*fVaLa%yjZ&WI`0Q+K ze6~SGW8P$Hwk1Zo2HDSMM;Obq<202l*DB}q!^%lZpd5jaZ1Rx^gbae8Vfw^=cusUP z>s}n1T)P84R}@J$!*my4J6ya_eC~C^Di=EN>t7G<+Ohdi&w=%O-Kcx(k)E-wa3{=< z!`AV8hga-7HZEQr7cY(%zhczaU_CrsiYl(=N5|Hl$v=39?Gs=9XfgZg(z7EgPLTb@ zE&iM06Z!uGszC~LfjoRuXkb~nAaNx0^z?#8UQyrZb~~L`izQd%0HCWr5C{gly5(|} z(0dU+OE?jd#7l(vWV|61MoaSDn>rvAz8^vA1(7gTUvg zK98!|hq~I^`xFA%qJ%C7pcN2@!)mqQvkL{Q)veWNsIZgHl<#WSXxh6_r!w7148&Z( zriOh@Hk(e8xeMM~AE}pKpp;{T7NMsnV(5MhQKKU!rB`|*iO7`*@+uQbCZdceovmCN zM>y#q@hJhEr?s3b1OjfS1p0E)J;>*C3!EwM{tx#ciMxEbw6l~64JxFCB*!W{mpKF5 zl!ves*hm5JoT==r#6FiSRobcN$$nNzxupD-M!D`TP5)Gcpl5)yk4{0Qpq{bo_0=x6f%!V8&pc351r@&)LAAc%h>UKDQ>e{5HD z^dyS+#7`!~zZX@pUWSFy=^|P`fwoQm2IlVn2=&$__jR>052o&SOu@Tf-ahke@sCo> zW6gP?ed*%eBP(M0{O-T{?&I_KtbS(O6GHK|!QLbPBDTXT@11NN_`{#?IevW4Kgj-m z{sLjmTJfC|C&a6{mVqBN)BNtAaD?>7I0DXrr{QaYe%`zV3-bAXgF%H5*|dgVR?P3} zCmLefCx$wnc>3wV!9|NS91Vbd=~ycDM53XYyzI#agKd5Q+S>g7>Tq3F@q{m%^*y0r zm$u6Ff2-A<9vmI5~3P-ka(aJxPf4~BwNv__++JD=O7Mnq8-JG?|; zbP-%MI5_G*<7bYMgTJjU`@|F3GkJ*rCmkZt3#pcDnVz@S5;Yjlu@0q&X`Oy78tj{4HjrAjV)9v zzgH!*omI4m7$jzl76<>!RQ#7zG#3B4N=kKC%d_8eL+nykC}E0IL=3OgOsy2*W;vW` zXO)!UND;2|tki<#39=4burzT%Y?gL~sM#964Q7#`E|A)EsXJTorIIY=x1S_G>YV&Y zeMhb@;V&L!riyFfTkuW4S(J;d;?#+4zm6r~nEjwWGzy*5`+j{&JO_7-!L;~RXl!!X z^Bczw-}a(&aK#urEUs?&@Z#WwOJln~NaXIG6yIT1?j2vY)^E=XqBti0n$a)oQS)kO z|9E0hcniL7Kd}Al7;_M=8-H)~!w*N-WtXnqx@JJU2&ZnI5}3pXAB;})Jim3%*mL6B z@WAV5JF@#e#37A$=1Ij(ylxQ3ME(A# z0x@wjAoR+Fl#M~iqF2Zm7VxC$h&M^6O^UvqGxuIGJI&6B!xN#6i#osGqt?;)@U-sa zDd4|9U*%CMCoFKk1(JrOU_l(RSR4+%3I_AQaan%w#= zihQy2#p^q6{<7G%d&}gjKF1s5H=vSqkGwOS;vh-z81~Pne zMk3~}BPOtkqU}0;D}5V$JALxceQ=`>Zty{_-P7LLzO$VrCkxv*wC`^}+Aik^;L$@k z_wF^r{mn<4nR^Vp%s^i|Xl9sZ(C(92;tdU4d|`ZLe1H6C{CZrzA0L=R9CC5e3?l$) zY>GBg*)&PX=EOsOYg4%sRniKP#Nx`dLXrfJ)as^ISql0prw{A#&^B0B`cvkdpS8HK1x?!y(a5XoTUB}X^=ECFGfBq*?4f#bVJlD{TXm_Ytqt$ zJUxbB-7;CdMh$Dle^e>ID4N>`Uf+01ZGL;%j!?D!{)`}gDf{ff>z(X+wPRY@$wuVg zh%ofhxp-Q(>3-wkzo}4}>!wXR>gb@jIA#Vz<@>jd^a&&`Cf<^Kh+gL-h|f)fWlPDV*$ zJUh#?WWGoyGZ5Ie#vbOlhHzal=x`IkTp?`WIJMeJ?a1pRqEV`X+N<_D+`*uPf(Vkk zEMr+l!Tb!)#|Et+v-S#wt4^?o@^u1d$=5k(bwOWnoT-QP_16I`Gr=e0!(||COFUb4 zG~^x|%kbn1bbB^$yDJq9Y2`;E>jWknfZvuhlRQqqY?BIsO$Ju^BK-b?ed4QPW(!=r z+icekEj7C9Z;Sa)iv3Kw_|^?Ls1!eVYiZx}g8VP0&%!F0S||Qke69HN-CL(FcpaA} z-@Y&JD86)X`qa{6D;95E8xsyER>>XMyO?KGROQl06^g?+>u!Sox8MthjrMA*D+EGn z2Dn@%lhI!vkNa`t=T9^>S^N%$$Wj=A1&*AvMjIMD#Ano4Yib?Xvy7ZK`jyINoR$BU zzzcp35*V@*)p$JmsEPdkUBE6}u6oEut znBGM@rE2adtgM48>#x@{qxEO%nHj3~qqdW-MP0q!USEe`0w)Gs0&4gK1PN$I34d*^ zUaeFDy`B)w3l1C!RqG@AKhU$BSr38Ut7lA%USA4|+=c59GMx~5U>xp;C=M6GqwqAm z3}rVUgp{sh^U7KI)ZtlKJX)BM5Vo^xn05|Jc$e9AzmhyvDm#;!G!+LZVf)EazDp;b zBxPP{{^=3XytqWrLCmPBhMCoqAS>Ne9$u!kW~(H=jIRPT3^CCoGsdd5%d=Kq-?7YY z4(-@HxLP^w`(p1we+a$t;H%&NuL<$)(4wsyPmM9#(aGtdBLDu2>(0G;<<%F~Up&P8 zWW2a&vVYlgUpbt2S6!RD#Pq-&&<{5zmn|OuUzc;@qL^3QBOV!qdGz7li9OHnJNP^^ zwrkg(7d8W`t4dUw{}`A+5H#RCuN8E`zY*$Xk%%H}AYg0YL%^QRWK40DLZRb0)Y#eH zo^0#tawgNQtw|5*5eA7MRhJ6epc91g(S)ta1fn=NiJP23lnh;jYtF?Hn%78-uu(|H zqfu@lPR_DNv&^w9T-XV@&JCT64rM#DovOBo!N7&XKqHgs5DAba$V+)r7pH)Bd!Qyj z2NzrxX+&WVjzC6$qwp9+ki^CTGWJme^;~*odS@DO-ZbQ0URT0}fNPX0TCqH$QM5S_ z%fp(q%ayKytfDR1*vRR)pOy40+VD21Qz@hZlLB_&;Q6xZx{6^`g4UE*(TdV+$|u=S zygchmx$CL2V@1flL|y*~*+U7)NMzsVD0`P09a5S{Y_MW?hL+g`uT+Pm6DV`$Z*j29 z$gFcFsqk{FvGC;7Kv~MkX5(uz5z(K3WS6B`U6b^{oMo5&f%xe({N3KPI6ec!JL39u z8t#SaFV-_(6~7cuWjkO4>RI}cT0R4==8Mlf_raGBo*e3*8acHiJMnIJ;**W%UOm>6 zJn;ha7khV$ul?(>$Cl>TUV3e7U+-_fcpG*i?T~ox)$Z0w$bKlUU$&_GhG-POxN!NW zb#5^yUdq7%bbs}!zSg%EGg~J2_GXTa0~Xuszbih(5in7lPU7%qTu@aR^;IspkM)w+ z$>vomRVshj*xb}qpK590>l>2EdKdUPzJUNP$Dh-|br;;c+if#flkl0>2NSVafP~LM zKVR>nn;AA~hG`3v?JOYFV-L150lDUt%^R9eH{+Bl(VS>jrh~1-&sJ4l5Z}&>HhB>y|^E9EUp|d0ftD z@CA@VD=Q~337ao3S9%GVm!K`gapcALygEzXM>v-7Yd$X^2yTh(gjO<#=|m~1cTpkf zI8_fUb2bOXq!?6HeI$%_;4uf+S}$yjW7NydLFd-l~E6Y!-8!pAisE}ann zO#F?}|L1@FrPn7e7GG+GBk0b$7l&eR4l&P<9`DUu*e*r(8pX%*Kf*4?gF})i4zye0 zp9%pWGZ>Hwv?QusZnq|zOeB2Ok#IN=j7FORv2;41Y2jO_$Zi&7u#sip3Y->rNs#Yr zvIjKwtPi1_$!zu$Yw`#IpVw>Vk}H!87ugUw9YH#jj3gs`P-d`N4Kyy7Y68T$QCc>0 zfpcN=aJj0pEiF~BDU+$Hrr5x?f|cj0J=GhkcUH5PtFKqzs-CH4tuR(y9m5u+S{Pgz z+z><@okl?FU;y}eKVw3Ezlm~yn1C4@7FRHt_|hCgEmHBcGNDlk<`8vnsfxbyY+^_3 zYE#L{4R0#PT;+zy%2E+snogcobSitcWT}WMSx`xm@=@EWNYR$Z45Z!F0w7T*^ODp* z=%>@c%>Y7)9|!(NmL=hD@D@8E{$253!l|uSVHytl+~UAJ@#e|3lZ!uH!Kk5Qdb8Td zOfVsL@$Q2gLxU^EPmD9~9}*H%Z}AsTxd*OIva!7@@{=8?Y4VV`MSQ(J@VIf&fKn;{ zXz;>`TN>B~A7jkDi+tI`_0u2LdBsa&uK>5oUZ33FmmQlDcRatxz2?RBH~U9-4Q`h- zIFRCf`FY>~H6R4y1pa|g#DKGNhrv=6jYM1y=n2Q;UIjr~`$8d&x2pQ*a21Z$w1fo% zE*Eqf4Rm>`IFlZ!WHxMFFGJYqAyyU_ysqkMz(qaL>rtfP>Q>e<#%Nu%&f(K=T8}oa z#TKo_@t9WYv(gfK?n1%D8fhIgC*;XQI}7&{$rn!;dX#WR36(;qA+Ho%b@*Vk<=L=O z8D&uy zrk;tMmMurZmD-Y$jtdpIpJOtm!D4EJrEzo$JE;`kD+8}(hlC`9F+IHRDVbx%uNh_W z3bSLys@;1AzQ-Ibp262&Gh_D??=9+oZrjNn`q!s=((6BTOuWvs9O%hxeob7v`s7gT%80~r{V#YQ`xD@$o~8|S z!=DNf2D=GPtJ85-901k&{dP}Fb8|EyS143Tp`#;}>+X)Gn%de@3`a`mPT;`-zbYJw z1WBVe3SvZ!F%eZcQOd#N-~RBqgx!waBhChdlt%$*7$e7FOpD+vBtUE=#>8Tg7OrJO z3&RN;1RQ|LTLhs+j>0*>5L0jzuvfBkIPBsb35VcdHOS#Ghs``8)oZy>;GAxq+D%(^ zgg311d_k*K*ZL{fX>Cmntr|oMUUfpvP}Irgn(H;UYG!I!U{W{L)HJD0K*r^u8s%^@ z6A4FtMoC;EFngg87BrT87?h`ROGxB$Zcibr{MB;rpVpNg!j6(mIFb}nA;q%l4-UEjk}C|El3Z9xChR}*PO>)` z`K9A|>k0wB0r&O1dsE%`^Fyou_e*~Wy}jL{**mgodvPE09J4-C+#@pLyUA{-6u0&b z_OE?$TgTxydK<1BxOD7NFWv$7FBX=qNLu$Km$Iw8LkIlTzq+`7+n#+-i6{TQqX!;F zdW~vYpO#%UE<4=qEcO+Ti<9|Qm}DAud7a`7F`0q8(AABHhhir-Gh3d2u_tqG8{sb^ zeu}7Jb2vYU;fN~(+CdMKKCe>gQjLK0WuVvTrxd(~DvP1Bqa)WEYz@xi=Us%$Ug@&g zx=f}nqFXEI8oKaT%grh957woVI%v@83=oBSv(3#hk{*PGP}k0R`{yz9=5>Z* z4fR~i6N|@?CRQJ-hg>e6V+<&lQ*@>{nu|7;3RDTHR2ifdBR^kjGMnhB*IZVS87w&M zd22y6HIIIDl>Us5Hs5r zFYOx{k)4`8fS!}Sr{7+4=~!RGh2FKh-|b1iT|{3^-+fnDrd_)l?H=1Pw&UEn9pvG` zn{W#}P-hp%#jR-bUh)23=J4hnyBEb?V79GWeW5%3I^Dlrc>n$(;6W{RSPA&^vt}N< z#1$7zYPE?cV9%8oA}0J5z^4A}IX%PZql73YUW@%H;gAz_PPbDZcRJ&Gl=O$lNNX?{ zAg!jLU%>gaKgfkVq0Z3G(B+Vfv~%`TVcP2G5qun-rb z@JtkLi0+K;k0Ob273rc;-VXwNAQ2D(Y%L1VcB!Q*9|#!Sc)zK&8mxqK(oo>k9yMb_ z23~DY8}RO3mU7PEocA;-?qD0%BfGma^XGPhNIe-na{2T1bkbT|m9MWyIF=-)8Z3A~ z9IONzz)o-!oCcS{E$}&*0UB~aPFX;fmn08Z;S!-~3$eb3rX3hS@23;=HMq^0PwF^eRgl^p=N3HK0_OL1a?JXh;#_D%fnaDHv#z zW){-yaWVhZ+9-i=ueu?D&8G?+gE_zXAJH^$|u1>%^zW2K@&@%;p0tMi$4` z$%<3=FWhGscKKp{kIXs!_LtKuX1)-ANBnT84jy8b!3=yy>=&7>rhp~An!XXH>ta{@L8ZC)6qDhUx8l><3KS4>aa~kSS3+v zhM*R%I!ontR=ynY@E%gIbr|)e8KL920E|>=9kdy zF<&t=)FU?H_nXZ+tCh2PY|Jei1h#~2&^BTlwN2RM8k-GAwjAf-;v7znI8H~VrrORH zfPtje=7JK(&t$|EXEH`Y?qOsVKZ9+c02uOD)A4zRS;+2W8I}Od;}wli0Qhl#~-XcF3UYw%TBF7HZrw(--B&aTlVf6gC8vKd7)#Fd2VFjNOm3D!fd%W z@nw-68e-Oq+r+zHyuRqIFRbF-!>dl-Q@(jS`@y^2SKseAcaQAbYVm>M3yM_FfE<_y z7J!A|Y50#q)Ld1i(HV`^8krNhGdR&`ZA_*5=JOm^)9nVJ+h(;^cdIm-?$8iF)TMz- zz*8l(&vMWVp2TNQ!l%y;#{C2>T3Z`#sw0@f8ZZD?f+xV^aQWH3Om&wI8=wN@Dd*sP z{K+1`(6Rn@!Z_X0(^oUtEb9)n@oi)(RMUYTQ#|pMytQ?SK)^x{p&LYQNA5+C4Mn0+ z9A~(PhMx3>Gu{lbfTs&}-h{i+?e)4FQSJQsk3XKxf_9;u(XoTxGSDC+k&4 zzRG5+;*BbqAR8DE@_C`HO(0WJ!jn(RxLU}e+FF^+Nh>%EGa@0IQ?o!IQrn17b1}?m zE|oV%L+Yx7|K*C|WSo3Sua;@}3=*V}znY3)l{9DX&Q+xNto)TKJtSKySmuW_;?zSy zXlb0XELTaJOh76!Q!rsO&2uv42Qs+=YclxnK~f(Qocd9TmPP^?Jw&Eg8p)?eH4N=L z==Q=7_ZIUFJfy=h zJ^de@8@dRU=}XgI_tz_OC$9---)%kfTE{hc{j%-r*KWpUSC2jCd-DGaSa8nd12+YS z%jfghkl8FVaw?U=ppeNtnyTt*D{m!}{x+}2gJaw8ykdjUuhHp@WOo(}6ulaDh4Nl+ zl^G$K5ddQq{<6AU0DsbJ6|93+8IPFWe|e(#KW4=Dif^QQn0{C-{%ZWd4(NvV;JO#bm z#7p8XwlgR8h<|_SCu@7J{m*|t)w}rMhp-Wz?-iGbpTRox&MI-M_-?KTTH&a8LcFsZ z<{{1_0W1DSF@=3m3@n641*2YXsLFTCA}E?mwKn13x`ntBkH4;tL`~5%5wzTA!=BB1p97LnN^HRB#1T++Kz4(il)sY+I$D3yMY++%X>L&OX`WLDr z)#QaLPz|$!yHF+62J=-_YOWtUj(&2{FW6oA{(h~RS4$j9dOh#8L}Zj4#!=|#Kwd^* zm;^pdF!vaCBR=5&u$M_u1dHLZc6G&KK2N; zJi*v7C$1hp(9(idEoXz~s?oX1LkDvRU z+KDx5lEu1u9gzk;*3BCF7!K+tStx;>A{ey$YuAm>m1iaI5^iQTg5QoIG^1cxq zbic?<;oSHGakqG{BL|gmF|2}@#a(yA`!|Hl)emmM{68P{i4*Pf;rp*0!AYbMj@rNX zd+>F!V~;rO4=fe@vFVSQf6hHlCaT5%qxeYvm%s}WU_N-90fI9g>GYeb<|pEj=7hEQ1!NU1ogGJ9ciMhIf24@251@h*mWD5o1 zs9n82Ryl9p6G|@*>WIb4vBeq%rMtS?TcwoCSqsAen-`El9IoK?w)q$I_08v8Fd+4- zRBm71wFC29Xv8Nyz7L<;kO{-w>w#{xbV|Lv zqf!;9RE3vaQnUI0wQ%@Im%~(scF)ZxY;;3){Opw@;kJ(6jlKz;`TGa&-`})Bt=+Qg z;Ge(D9A@go7<}h2{6usx-S?;Kk^eCCiyuxeyP!25*}P-=!;M=e){bswP-iE6@3kWh z4cO545;MLc3VXyAq0mwxoS6P+=3m--nK2j|gI4j^W8(W`;ur6}J0|8y;xVv-ABTJQ zE>DIJFj@IFJds|yGS-VE%aL6TFC1k9~;ZUiRWF43a078{b2RNQ1RMVWUS0}x1 zey<9DW%*2&yw6wgCK9pGhA<2Z)~b9sY>d>^`J#eP@GTh4z(fXSh_%*R9r;X*i*ATA zQ32<|I7jA;PAEZ`SNjAA^fn41JDAht%LMAEQrDdTJDbB3}j zo^{rxIMP_5g)S>WN)+kkc(DXhsf^LnhA&MqQk8^*m~9E<`v~uHnOBo)4;yV#AI={z zNV7_mf)$%>nN?LH)u;%S&$@e4h7zojx8pzZ!QIE7n+k`-mg2Y@wnL40^#FY9Abd+q zpZrnFHwUL*u%@?c^>zPX^qB)2f3W)O!8@TjXRXa*qY3({niNY@a-uW-2C#pB!M@9Q2L)CVW?XH+;8!N*?)KB;=?D zE@>rHLo2GTHWTG^BJmdpa5~*e7eXa5yEQMrfGTB9-t`ydw25^s?4RBxD#$=1q8x40J}WEFnor=;pvT+1rO3 zc5fNkrqwEYMtI(RXYy`%-U)GAIt%wqO^C~pc(~Ylc>3nV5%FuN6z?!M#E&7E0sE#{ zwfK^FKo%9xh%0p%U zoq#tcCd9-<@lGV(-?wfn@qa$?GsTDU-v=fTA$4u|oM2b$k;3E&*V>(Gy%8y(-Dx(O zO$`achTVj7;Nf%4&7Rs;LRpjxHxh5w2$!Er8MQK=j?blXP1yCpPIHZes4JZptP)K$ zwhe2*9E_MtQWUwK_A*8nB26qJL?QRReEUW1gWMpABT zB&i}bVE)mzT#0(2tczXpc8|=)J?g(I#L=l)X)YxP1EDg9Cs1XW}%@74I;w zig)kb`@w!CBi;~qqc>ANI)QGhh$n>=hgw^XdeXo0urRF;VQ9fJ)xP{Fh1i z-sGfMKUw^V**2Jub;wVNPH|mvv3NL>hFNASiFeR;V4GzHf#ZV5 zh@!PYo6~EwYNIw-8&rjzUb~e~v?5tkJ2I1W2cleBNlsMOwni--nu02aPS@p#+wB2& zn9^N39ASGTPAuV-LOgD3=~Q>p?TE;CVxy5ndx(qRbt;{T?TXN+)Z<6-6;9g zriZ!?b3iHOvAa}cb|C9v_hfiPE33>3QI^SSsd%ZmHddb3ooi=A3Tv*NQBe6Ks&9Ll z%_0Lu@yaBdn-s6$Glp4w|9Eltp^5wV#V?pM;uoU0d%IeT^WBTGekh1n#0GLiX8dkp z`crb=1ij->*~cm$eBCF0JucoJhfE;AtQd!1j*pAl@#33-z_RY;YtUK}wBhV|O8k_t z`N~0drbTfLM`%?zkEkK_UXX+j1TVwbBW9J^9u9?k4bf<=uPzd?1C`7!b361%L)r_k zDC|a0HHWMu=g=rnZEYIG8yjV$tRdJ0#Pglng<6KwLLO;JsHD}Z8>uix9J@3Ew~J6{ zZiK=xJL3!E%+5Fjak#S%;vCt4;)JBkWufFzIP-||4U$fKh!2P8NtH}4qlj{!RqhT@ zyLHqM>-%xxas#~506_!aFw$_QfxR4tBjGDy288+WNO&}SCM@IRuv30iep-H6J|mZ{ zlwX(Ml7B8|N90%J43P8kdvet2KI%U0zU*dKy05!$xj%Qaz|FhwxzR$$O2=gf+Thsf zU>d@5x5MF_k>7 zEI%?tDrFrNcM54>S1RklaoNY;j9!Fm#J6GcFd9D$6XFfH=9@Q8q4DW0Gd zaY72Y1>>M`6knV%k|-@f;HcOT5bYYhkXz?a0Ohs5L|kvT|kBQf!oVy!$59Ka2HlwHnu-ej`X z5O%0l7eKuq_?$Y*CuhUEk}X0k8O_;BRH)`Sa zS~yrcT02pTxLQx`!dj%mxtaylhHJwnjr?aN#yP?^#<9?cSi$7X<6kYEDr-Amuvknc zgTzI0+5{QSseN4uELLtVBV{Qa zP^3bETv|-ha+65nDzeu??``2F`8p?HP4Lyko|Im$x# zTKG4)9c~wQe{*Nc7N~^NA`7P{;60HiJ*k{nR9ulq@tFZw%6l4Ic+PC8B33!_GO}0u z$fE~)Z1Siu^R&vVR(KH7s?|XYoy@Ejv`2N2(|L6G->egA{CS;VbLDk9i_s$esu1E7 zP=gc-nOjCmoGbg#sF3hQxiz#9wMQ)b-q z5jWh3U&C7pLv&)88xA-Mj)60jMeiPv58_kC?EnBfJg!8%s{z=XF?M8BtF@e#{{K00 zqc~7by(Bv>vnp;XZ6&iI&s^=o5|c#<=tSHRoHGwBML{L0!PG-M%ygq5cdf%zUCXe|02syH!J$QZfb~XTBK%u{O zKf7MG{6PczE?he%zK&yVe=$i)RM3wtdo#A|N%>y_oy0W*n&9XKhY#MfKt!Y)SdxFQ zJ_nu78YO{RtKwmm$#1CZGLR?(t^j(_NRz2$AOXzqSx^hC_*erVnFN9=eWIo&p;tkf zt~t%8Nntewc(__nQ5S5Bg|x)URSCLOI?)Ikg+_)$>Be-UUtd?}j5??ShqmBwRC!b? zovw;s+!dl@D>cTrZxS4yDl za{DcK8H)64t|4>;LQbA54p1iC49&2b@=6J*xY~59_8c!u!lt z(Xniqc>l`UJ7iz0o0+G$i9Ms_6+NI9E|%t7UqK3_)f$aFEuLFyT-6qYzXD#TgQ~o% zpdgc(OwehQm_lwjA?Nfs8P16vfK#YR=ADAYjt`tE?{w5kyqLEfaG?X9#$n>;ju{8* z5bBcojSlE>EOan{79N=@N~fr2hy-JckS%atqNupgyTL1qdpo@hhtDs2uX{iD&Uob- zuTT@tGhW6kH<_`o13d5oREtbNk|kbgf|5n(WSq<+i_7q9@{(e=TCP-f%AuUratw3e z)+HUMRCz#zOfPMgLd?>%BjHXgk#AAdS47$g<8!vc660Z+=8(!UJuJIKhLIj2Fsg$* zvu^qnT67F;n;v6Eq5JDRJi#u0@U8Dg_U6U$i}=q2-}v}N^umSWhb$IkqRjb(_mIssSD)i4VDg$0MtAI#ERofZP>{qaV~s)6sgoxQ+M;z5ZKLD9IAS zq_T=<7xFChbki&ezeIv&D38qBA0`(m=K=-^2VhIi66emYg#iF5LkU>U=9kB|!p+~h!Wn9aV!StDX=M7ah8B3XC9D-I_Xo#TS;vu9# z-jFw>4pf=x-M2{!9~_W&Rn97tS?Q@xhIodiZ*>JvRlJHJ*rK=%0h?gEVnZXg8@Agv zBoj3Hd_pG37+Hxln?B+!gFf1->7$@AnDZRtVd4yxGZz{;TOR)vqm0YNL}j)rxgkJF z9YB4eUZ_X)^;`gI0$MGngj$5-GlN>1Rb6OUY1m*u2B9{TH-J(lwzQ#3qd9X>nw0H6 zvY8;_CFRvJvTc^0yF8p|OJ?U9OI*?zegv#cNj_|>tgQbnkswsoxydG>1DaWx!cRy| zeb_2ugW|^GPuL$$J~%x2$A4ypud}Nk9G?5V=Q;5s@q>mW%%BU4uz&ZDh+h?3r>4*| zuif5raPq;WrOR2)?dhJrafA^Li?e9q#Tg+Rgc=ZpS|%7wG&M=k z;Z#;qb7Aqll<<;Bivr1%OB$B1ZXmXdEzJ(Blw-bCSX(g3xF%22=S?$BEZ4NNX@ApE ze3J5{I#Z`pms2tizBQex{i&n)Qa(xsYN=AtY1d^JODMQIUHe@}@ul4B+UOc}O}N;5 zE;=)R8lcnQbHI!O9O*#9m@70B7%|>EPRr1K86jarKn6AVpAKX);7T>gw6};{q9@Ae zP*gC5@==45Q|q~k1A9e_{g+nIQroO5kn+zhElS{LMzvS8tXF$RdrynBB|2z_oqxMx zc#+wW4ax{YAjEUO=Eke^Gc>%NgC&+UP7P)0GO74sle!H!bTvrit`fJ6M50wrs>_l# zrkWC4O+^g5OdT%qhgWQzkg3hJ^Bl7(87}@U^LsT8@h9SGEsriS@7u-Y;^yiav}XD{ zp&0w84{DPw?BK+-b>b(#2xZ~Z9~S?esYkz_wm$S-92^=Qcv7nn^7`1AnR{x{=u=OQ z_V-Pj2L=%Tmp|_4`x>UF`}Z;YxcD_?7gdYDQH;y~8hcB^zW9`2_4xg+I;SmSQ#k5E zA>N`j>76kmO7*hB$NNZ+p$63BG~UI#Ty~9vh$5+g5q{^qsm7#_6Tz!j6e=E}Mnd+C z-KxOX_WEAJ8^@mYQDNUtl-jKf)VGm4K~Dw!_B-209Aq`s5OB%03glv0W!m9 zGF8>q1Zzl)V=QR3dXD$-aUOXQ&+8L2bf{s8SNEjM|jw-3dL-OQ>4us?AgyV{Xx#xfm$}v=u zznaBRu3Rxo{#;`5E*p?hV!xuQ%{;?f$?UQf zI|mD3;p&(5*bpeF!fDZa8B-NsMH-DxySI4NF? zC7=&`CIROnSLCmN3=qK6j8*V07#MhLaHyxJcafS=vb~Qz*53d4dLi=@gm;2?Wpz+OXelYirhXQqMxKS16U*Mxd2085V~H zO=g=}N!2xn@uE!seCD zgttd-M9qRLly8nktky_lMB)&OHxl%t)Kw;ht&%r4NHR{PPKjhMl@jC}>|(N9>PXJv zTznXwKU~7U4wt%mW%^A?%2e7ah@yE3(3O;=XDTvf$+83uC4`PY+EjT+<2=zADB)qK z9yy&WCOl)c1`>DBDjMPaFG-cm8m_!dif5^jmBBO>O>_JcnfrJCPr7OK;l1nX>Sn~h zTe>@Ho8Cp|ccPQ0|p89$e zzLEJv{N?%FoFsRJZC={$eyq#I}2chM*?|6CoH1=@v#|breSHchVs z%7+gIOJ1YlIZmNa>6&yj=uS1w8R$!90gctY1r~5-k9mU`apq3*Li15G%cCYU0Olsd zJL1^K39l659;?glbYUlVzTk8S4hpPr6&NP&AT7dj5l9ior=L5}ptN04FoYBZmxJKB z8U3bjH?Patzf_EossJR>$7Vk~OsLFPKj?#Ouu?*hxiT@p5Lc7!zq~X?|d1$#g7)R-}C(XRg>TS zu~&XZd=qA-MZ#<+v%m>)<8*GM*v-5)l7%chv=8>G)jb2&hAAB9UM>}00gU8xpe-hz61 z7A67UgEA&YrbyiJ`beZB;q#&VV~_Rs_4Ul3-_g}AxT>qYxsGfHE$}8OsFp7zlUZF9 z=YAof&R+si7z#!nr1>=ANlUX;UK>S9SPM7=)`a+@>}mFMc7|mE%d?D)gv-@L_l~vM z*bvgT;_Z#-sKm2Khq4wr<)p>0o6TA|r&B7`5=;Xqp#~|HI;~{Xv;(i58#m7p*(whL zlpJYfcsM(&5|3X>O4PR4LVsC;{#PIqDaYFqrDB-?)F4IJb0}B(*k8-!{Ecuw5cjBoA}>6^S*b<~YlT_IJmJTg8j8e{523g*Tz2?(jLtUN|(B zYekv*&AFvRp<-s;>U0R3+M%Uu*RS5U`Jj(|b?W2gyVd&j+m}PI_k~M`j~BBC_Z$%x zAA%ed;N~$H5+$-Jt-K+t*$&+sKWWXJ8+ciF4b+z4UbvW!=I|#N=*9H?L{KPl}VM7ha>3Kl9QvX z4U$n&P(umTV4DL1tH_c|iCw@Zk!RB))jA{;_sJ%T#F6&z3$|D+ zl7aR%yVL8hs|#nc*{}@CA5&e1&w^h3OY8**E&^cY-dWJuN0OI5xJ=OW_lNi)F0`-# zc}Xl?0}`-szMJpvp4WZ}4&s}2?|f@(n~<7ED9V%g!%4T1h=Dtu8f_0=eHU=Ju9Q8k zq0pyf4d`XVTJ4hXlNaGLXCsjTvIIRs7yzyn2fVF(>zP*MM6Ipuc+-f>g*-~PrzPN$ zCp-|AJa;`epnp=UeKLZ7NbAdmp-7e_+8G=uwZ2lwWa3Xz1TYcfnWu`sI{?`M^FYtQ z>VYi-y9eY0q!?+b%U>CQOoUEMhw}8%=8zr@X6aF*&&TO?h(6Mq-#`Q}tpQYrxc1KW zh3zZbcT#rE%k9_OZ?&sAdPy#3+T|M14tdmWx6;48-R^LhrDg_jBpldDI0&K!dlrYI zt}^UIDxNihmxvNuUHO`tmHrKW#$TG8or8g?U^2>3(F%hLiFZgr(2znO1-* zKBVRm&doW5{3fZ}6>O%pql{_xla=Snr9>HXvLV526_rb#*bJFN#oNfQ)at)@{+!;44%SLi7o8(|I>d*GiuwP^5%|K)HfCyt$rWpQNg9bB{#?S69M z(y|3X6)jp$9e2CW|hwpC! z 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 Date: Wed, 28 Jun 2023 17:06:46 +0800 Subject: [PATCH 051/128] update OS part --- ...13\344\270\216\347\272\277\347\250\213.md" | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 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 71260d02..9c1b345b 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" @@ -323,8 +323,9 @@ A给B发送信号,B收到信号之前执行自己的代码,收到信号后 信号的特质:由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。 每个进程收到的所有信号,都是由内核负责发送的,内核处理。 - - +```c +int kill(pid_t pid, int sig); +``` 两个或多个进程可以通过简单的信号进行合作,可以强迫一个进程在某个位置停止,直到它接收到一个特定的信号。信号量是一个特殊的变量。用于进程间传递信息的一个整数值。 @@ -337,16 +338,37 @@ A给B发送信号,B收到信号之前执行自己的代码,收到信号后 操作系统会中断目标程序的进程来向其发送信号、在任何非原子指令中,执行都可以中断,如果进程已经注册了新号处理程序,那么就执行进程,如果没有注册,将采用默认处理的方式。 信号效率非常高,但是可携带的数据有限,只能写到一个标志位,无法携带其他数据。 + + 不建议使用信号完成进程间通信,因为信号的优先级太高,产生信号之后会打断程序的执行。 + + - 信号量 信号量(semaphore)是由荷兰人E.W.Dijkstra在20世纪60年代所构思出的一种程序设计构造。其原型来源于铁路的运行:在一条单轨铁路上,任何时候只能有一列列车行驶在上面。而管理这条铁路的系统就是信号量。任何一列火车必须等到表明铁路可以行驶的信号后才能进入轨道。当一列列车进入单轨运行后,需要将信号改为禁止进入,从而防止别的火车同时进入轨道。而当列车驶出单轨后,则需要将信号变回允许进入状态。这很像以前的旗语,在计算机里,信号量实际上就是一个简单整数。一个进程在信号变为0或者1的情况下推进,并且将信号变为1或0来防止别的进程推进。当进程完成任务后,则将信号再改变为0或1,从而允许其他进程执行。需要注意的是,信号量不只是一种通信机制,更是一种同步机制。 - Shared Memory共享内存 - 管道、套接字、信号、信号量,虽然满足了多种通信需要,但还是有一种需要未能满足。这就是两个进程需要共享大量数据。这就像两个人,他们互相喜欢,并想要一起生活时(共享大量数据量),打电话、握手、对白等就显得不够了,这个时候需要的是拥抱,只有将其紧紧拥抱于怀,感觉才最到位,也才能尽可能地共享。进程的拥抱就是共享内存。共享内存就是两个进程共同拥有同一片内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,一个进程首先需要创建一片内存空间专门作为通信用,而其他进程则将该片内存映射到自己的(虚拟)地址空间。这样,读写自己地址空间中对应共享内存的区域时,就是在和其他进程进行通信。乍一看,共享内存有点像管道,有些管道不也是一片共享内存吗?这是形似而神不似。首先,使用共享内存机制通信的两个进程必须在同一台物理机器上;其次,共享内存的访问方式是随机的,而不是只能从一端写,另一端读,因此其灵活性比管道和套接字大很多,能够传递的信息也复杂得多。共享内存的缺点是管理复杂,且两个进程必须在同一台物理机器上才能使用这种通信方式。共享内存的另外一个缺点是安全性脆弱。因为两个进程存在一片共享的内存,如果一个进程染有病毒,很容易就会传给另外一个进程。就像两个紧密接触的人,一个人的病毒是很容易传染另外一个人的。这里需要注意的是,使用全局变量在同一个进程的进程间实现通信不称为共享内存。 + 管道、套接字、信号、信号量,虽然满足了多种通信需要,但还是有一种需要未能满足。这就是两个进程需要共享大量数据。这就像两个人,他们互相喜欢,并想要一起生活时(共享大量数据量),打电话、握手、对白等就显得不够了,这个时候需要的是拥抱,只有将其紧紧拥抱于怀,感觉才最到位,也才能尽可能地共享。进程的拥抱就是共享内存。 + 共享内存就是两个进程共同拥有同一片内存。对于这片内存中的任何内容,二者均可以访问。要使用共享内存进行通信,一个进程首先需要在内核中创建一片内存空间专门作为通信用,然后该进程与这片内存空间进行关联,而其他进程则将该片内存映射到自己的(虚拟)地址空间。 + 这样,读写自己地址空间中对应共享内存的区域时,就是在和其他进程进行通信。乍一看,共享内存有点像管道,有些管道不也是一片共享内存吗?这是形似而神不似:- 首先,使用共享内存机制通信的两个进程必须在同一台物理机器上; +- 其次,共享内存的访问方式是随机的,而不是只能从一端写,另一端读,因此其灵活性比管道和套接字大很多,能够传递的信息也复杂得多。 + +共享内存的缺点是管理复杂,且两个进程必须在同一台物理机器上才能使用这种通信方式。共享内存的另外一个缺点是安全性脆弱。因为两个进程存在一片共享的内存,如果一个进程染有病毒,很容易就会传给另外一个进程。就像两个紧密接触的人,一个人的病毒是很容易传染另外一个人的。这里需要注意的是,使用全局变量在同一个进程的进程间实现通信不称为共享内存。 - 共享内存是进程通信中最快的一种方法,因为数据不需要在进程间复制,而可以直接映射到各个进程的地址空间中。在共享内存中要注意的一个问题是:当多个进程对共享内存区域进行访问时,要注意这些进程之间的同步问题。例如,如果server正在向共享内存区域中写数据,那么client进程就不能访问这些数据,直到server全部写完之后,client才可以访问。为了实现进程间的同步问题,通常使用前面介绍过的信号量来实现这一目标。一个共享内存段可以由一个进程创建,然后由任意共享这一内存段的进程对它进行读写。当进程间需要通信时,一个进程可以创建一个共享内存段,然后需要通信的各个进程就可以在信号量的控制下保持同步,在这里交换数据,完成通信。 + 共享内存是进程通信中最快的一种方法,因为数据不需要在进程间复制,而可以直接映射到各个进程的地址空间中。在共享内存中要注意的一个问题是:当多个进程对共享内存区域进行访问时,要注意这些进程之间的同步问题。 +例如,如果server正在向共享内存区域中写数据,那么client进程就不能访问这些数据,直到server全部写完之后,client才可以访问。 +为了实现进程间的同步问题,通常使用前面介绍过的信号量来实现这一目标。一个共享内存段可以由一个进程创建,然后由任意共享这一内存段的进程对它进行读写。 +当进程间需要通信时,一个进程可以创建一个共享内存段,然后需要通信的各个进程就可以在信号量的控制下保持同步,在这里交换数据,完成通信。 +创建共享内存: +```c +#include +#include +// 创建或者获得共享内存ID +int shmget(key_t key, size_t size, int shmflg); +// 连接共享内存 +void *shmat(int shmid, const void *shmaddr, int shmflg); +``` - FIFO @@ -360,16 +382,16 @@ FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于 - 存储映射 -       存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。 -       使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。 -总结:使用mmap时务必注意以下事项: -1.     创建映射区的过程中,隐含着一次对映射文件的读操作。 -2.     当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。 -3.     映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。 -4.     特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!   mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。 -5.     munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。 -6.     如果文件偏移量必须为4K的整数倍 -7.     mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。 +   存储映射I/O (Memory-mapped I/O) 使一个磁盘文件与存储空间中的一个缓冲区相映射。于是当从缓冲区中取数据,就相当于读文件中的相应字节。于此类似,将数据存入缓冲区,则相应的字节就自动写入文件。这样,就可在不适用read和write函数的情况下,使用地址(指针)完成I/O操作。 +   使用这种方法,首先应通知内核,将一个指定文件映射到存储区域中。这个映射工作可以通过mmap函数来实现。 +总结:使用mmap时务必注意以下事项: +1. 创建映射区的过程中,隐含着一次对映射文件的读操作。 +2. 当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护)。而MAP_PRIVATE则无所谓,因为mmap中的权限是对内存的限制。 +3. 映射区的释放与文件关闭无关。只要映射建立成功,文件可以立即关闭。 +4. 特别注意,当映射文件大小为0时,不能创建映射区。所以:用于映射的文件必须要有实际大小!!   mmap使用时常常会出现总线错误,通常是由于共享文件存储空间大小引起的。 +5. munmap传入的地址一定是mmap的返回地址。坚决杜绝指针++操作。 +6. 如果文件偏移量必须为4K的整数倍 +7. mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。 - 匿名映射 @@ -388,8 +410,6 @@ int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0); 进程与其它的进程进行通信而不必借助共享数据,通过互相发送和接收消息,建立一条通信链路。 - - ## 进程调度 当一个计算机是多道程序设计系统时,会频繁的有很多进程或者线程来同时竞争 CPU 时间片。当两个或两个以上的进程/线程处于就绪状态时,就会发生这种情况。如果只有一个 CPU 可用,那么必须选择接下来哪个进程/线程可以运行。操作系统中有一个叫做调度程序(scheduler) 的角色存在,它就是做这件事儿的,该程序使用的算法叫做调度算法(scheduling algorithm) 。进程调度就是处理器调度(上下文切换)。 From 1d9e2dcd389a18a9b10e175f1be46f99c13896c4 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 31 Jul 2023 14:38:47 +0800 Subject: [PATCH 052/128] update --- ...3.\345\206\205\345\255\230\347\256\241\347\220\206.md" | 8 ++++++++ 1 file changed, 8 insertions(+) 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 0f00ea4f..a0b3297b 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" @@ -43,6 +43,14 @@ 上述几种内存区域中数据段、BSS和堆通常是被连续存储的——内存位置上是连续的,而代码段和栈往往会被独立存放。有趣的是,堆和栈两个区域关系很“暧昧”,他们一个向下“长”(i386体系结构中栈向下、堆向上),一个向上“长”,相对而生。但你不必担心他们会碰头,因为他们之间间隔很大(到底大到多少,你可以从下面的例子程序计算一下),绝少有机会能碰到一起。 + +##### 虚拟存储技术 +所谓虚拟存储技术是指:当进程运行时,先将其一部分装入内存,另一部分暂留在磁盘,当要执行的指令或访问的数据不在内存时,由操作系统自动完成将它们从磁盘调入内存的工作。 +虚拟地址空间即为分配给进程的虚拟内存。 +虚拟地址是在虚拟内存中指令或数据的位置,该位置可以被访问,仿佛它是内存的一部分。 + + + ## 内存的演变 在早些的操作系统中,并没有引入内存抽象的概念。程序直接访问和操作的都是物理内存,内存的管理也非常简单,除去操作系统所用的内存之外,全部给用户程序使用,想怎么折腾都行,只要别超出最大的容量。这种内存操作方式使得操作系统中存在多进程变得完全不可能,比如MS-DOS,你必须执行完一条指令后才能接着执行下一条。如果是多进程的话,由于直接操作物理内存地址,当一个进程给内存地址1000赋值后,另一个进程也同样给内存地址赋值,那么第二个进程对内存的赋值会覆盖第一个进程所赋的值,这回造成两条进程同时崩溃。随着计算机技术发展,要求操作系统支持多进程的需求,所谓多进程,并不需要同时运行这些进程,只要它们都处于ready 状态,操作系统快速地在它们之间切换,就能达到同时运行的假象。每个进程都需要内存,Context Switch时,之前内存里的内容怎么办?简单粗暴的方式就是先dump到磁盘上,然后再从磁盘上restore之前dump的内容(如果有的话),但效果并不好,太慢了!那怎么才能不慢呢?把进程对应的内存依旧留在物理内存中,需要的时候就切换到特定的区域。这就涉及到了内存的保护机制,毕竟进程之间可以随意读取、写入内容就乱套了,非常不安全。 From b699171bd57a197884f49092f6dd108e3703460c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 7 Aug 2023 11:49:54 +0800 Subject: [PATCH 053/128] update OperationSystem part --- ...73\347\273\237\347\256\200\344\273\213.md" | 20 +++++++++++++++++-- ...13\344\270\216\347\272\277\347\250\213.md" | 11 ++++++++++ 2 files changed, 29 insertions(+), 2 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 52f42ab2..ddfe2b25 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" @@ -250,10 +250,14 @@ Android平台的基础是Linux内核,比如ART虚拟机最终调用底层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“取值-解码-执行”的速度叫时钟速度,单位是赫兹,赫兹是用来表示频率的单位。1赫兹代表1秒1个周期。 为了驱动CPU运转,称为“时钟信号”的电信号必不可少。这种电信号就好像带有一个时钟,滴答滴答地每隔一定时间就变换一次电压的高低。输出时钟信号的元件叫作“时钟发生器”。时钟发生器中带有晶振,根据其自身的频率(振动的次数)产生时钟信号。时钟信号的频率可以衡量CPU的运转速度。这里使用的是2.5MHz(兆赫兹)的时钟发生器。最大时钟频率(maximum clock speed),也称主频,是影响处理器速度的决定性因素之一。时钟频率决定了执行一条指令所需要的时间,处理器的数据位宽也影响处理器的速度。 操作系统通过虚拟化(virtualizing)CPU来提供这种假象。通过让一个进程只运行一个时间片,然后切换到其他进程,操作系统提供了存在多个虚拟CPU的假象。这就是时分共享(time sharing)CPU技术,允许用户如愿运行多个并发进程。潜在的开销就是性能损失,因为如果CPU必须共享,每个进程的运行就会慢一点。 @@ -377,6 +381,11 @@ CPU 的内部由**寄存器、控制器、运算器和时钟**四部分组成, ### 中断/异常 +为什么要引入中断与异常? +中断的引入: 为了支持CPU和设备之间的并行操作。 +当CPU启动设备进行输入/输出后,设备便可以独立工作,CPU转去处理与此次输入/输出不相关的事情。当设备完成输入/输出后,通过向CPU发中断报告此次输入/输出的结果,让CPU决定如何处理以后的事情。 +异常的引入:表示CPU执行指令时本身出现的问题。 +如算数溢出、除零、读取时的奇偶错,访问地址时的越界或执行了“陷入指令”等,这时硬件改变了CPU当前的执行流程,转到相应的错误处理程序或异常处理程序或执行系统调用。 #### 中断的概念 @@ -461,8 +470,15 @@ CPU 的内部由**寄存器、控制器、运算器和时钟**四部分组成, +#### 中断异常机制工作原理 + +中断/异常机制是现代计算机系统的核心机制之一。硬件和软件相互配合而使计算机系统得以充分发挥能力。 +硬件该做什么是? --- 中断/异常响应 +捕获中断源发出的中断/异常请求,以一定方式响应,将处理器控制权交给特定的处理程序。 +软件要做什么是? ---中断/异常处理程序 +识别终端/异常类型并完成相应的处理 ## 存储器 在任何一种计算机中,第二种主要部件都是存储器。在理想情况下,存储器应该极为迅速(快于执行一条指令,这样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 9c1b345b..fd97534b 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" @@ -346,6 +346,12 @@ int kill(pid_t pid, int sig); 信号量(semaphore)是由荷兰人E.W.Dijkstra在20世纪60年代所构思出的一种程序设计构造。其原型来源于铁路的运行:在一条单轨铁路上,任何时候只能有一列列车行驶在上面。而管理这条铁路的系统就是信号量。任何一列火车必须等到表明铁路可以行驶的信号后才能进入轨道。当一列列车进入单轨运行后,需要将信号改为禁止进入,从而防止别的火车同时进入轨道。而当列车驶出单轨后,则需要将信号变回允许进入状态。这很像以前的旗语,在计算机里,信号量实际上就是一个简单整数。一个进程在信号变为0或者1的情况下推进,并且将信号变为1或0来防止别的进程推进。当进程完成任务后,则将信号再改变为0或1,从而允许其他进程执行。需要注意的是,信号量不只是一种通信机制,更是一种同步机制。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/linux_signal_pv_1.png?raw=true) + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/signal_pv_2.png?raw=true) + + + - Shared Memory共享内存 管道、套接字、信号、信号量,虽然满足了多种通信需要,但还是有一种需要未能满足。这就是两个进程需要共享大量数据。这就像两个人,他们互相喜欢,并想要一起生活时(共享大量数据量),打电话、握手、对白等就显得不够了,这个时候需要的是拥抱,只有将其紧紧拥抱于怀,感觉才最到位,也才能尽可能地共享。进程的拥抱就是共享内存。 @@ -681,6 +687,11 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 #### 线程系统调用 +系统调用:用户在编程时可以调用的操作系统功能。 +系统调用是操作系统提供给编程人员的唯一接口。 可以使CPU状态从用户态陷入内核态。每个操作系统都提供几百种系统调用(进程控制、进程通信、文件使用、目录操作、设备管理、信息维护等). + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/linux_system_call_1.png?raw=true) + 进程通常会从当前的某个单线程开始,然后这个线程通过调用一个库函数(比如 `thread_create`)创建新的线程。线程创建的函数会要求指定新创建线程的名称。创建的线程通常都返回一个线程标识符,该标识符就是新线程的名字。 当一个线程完成工作后,可以通过调用一个函数(比如 `thread_exit`)来退出。紧接着线程消失,状态变为终止,不能再进行调度。在某些线程的运行过程中,可以通过调用函数例如 `thread_join` ,表示一个线程可以等待另一个线程退出。这个过程阻塞调用线程直到等待特定的线程退出。在这种情况下,线程的创建和终止非常类似于进程的创建和终止。 From ccb22c1352d5df102a64ac5360e71c80deabfd7a Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 23 Aug 2023 10:55:11 +0800 Subject: [PATCH 054/128] update --- .../\347\256\227\346\263\225.md" | 4 ++++ 1 file changed, 4 insertions(+) diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" index c6ce6686..7c8a9a26 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -17,7 +17,11 @@ 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 + ### 算法时间复杂度 + +一个算法的时间复杂度(Time Complexity)是指算法运行从开始到结束所需要的时间。这个时间就是该算法中每条语句的执行时间之和,而每条语句的执行时间是该语句执行次数(也称为频度)与执行该语句所需时间的乘积。但是,当算法转换为程序之后,一条语句执行一次所需的时间与机器的性能及编译程序生成目标代码的质量有关,是很难确定的。为此,假设执行每条语句所需的时间均为单位时间。在这一假设下,一个算法所花费的时间就等于算法中所有语句的频度之和。这样就可以脱离机器的硬、软件环境而独立地分析算法所消耗的时间。 + 在进行算法分析时,语句总的执行次数T(n)是关于问题规模n的函数,进而分析T(n)随n的变化情况并确定T(n)的数量级。算法的时间复杂度,也就是算法的时间量度,记作:T(n)=O(f(n))。它表示随问题规模n的增大,算法执行时间的增长率和f(n)的增长率相同,称作算法的渐近时间复杂度,简称为时间复杂度。其中f(n)是问题规模n的某个函数。 这样用大写O( )来体现算法时间复杂度的记法,我们称之为大O记法。 From 44f03d76c950a93510a5247dceb9497f3f9f3140 Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 5 Sep 2023 20:52:33 +0800 Subject: [PATCH 055/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 72fdaebe..50112793 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" @@ -381,7 +381,7 @@ class Test{ fun main(){ // 如果 myService 对象还未初始化,则进行初始化 - if(!::myService.isInitialized){ + if(!this::myService.isInitialized){ println("hha") myService = MyService() } From 65f31a3bafb12699141ed967318318561821ab7d Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 5 Sep 2023 21:07:50 +0800 Subject: [PATCH 056/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 50112793..72fdaebe 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" @@ -381,7 +381,7 @@ class Test{ fun main(){ // 如果 myService 对象还未初始化,则进行初始化 - if(!this::myService.isInitialized){ + if(!::myService.isInitialized){ println("hha") myService = MyService() } From 39a717bdba4e77214de07f41fe3374384d956984 Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 5 Sep 2023 21:18:56 +0800 Subject: [PATCH 057/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 72fdaebe..8f2a250c 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" @@ -388,7 +388,9 @@ class Test{ } } ``` -注意: ::myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省 +注意: ::myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省。 +这里可以写成`::myService.isInitialized`或`this::myService.isInitialized`。 +如果在listener或者内部类中,可以这样写`this@OuterClassName::myService.isInitialized` 除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: ```kotlin From e6e90f0d69ec5c9ed656131c6089b3c603a115cf Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 5 Sep 2023 21:32:40 +0800 Subject: [PATCH 058/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 7 +++++-- 1 file changed, 5 insertions(+), 2 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 8f2a250c..f2f97abb 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" @@ -369,7 +369,7 @@ private fun switchFragment(position: Int) { } ``` 会报`kotlin.UninitializedPropertyAccessException: lateinit property test has not been initialized` - +> We’ve added a new reflection API allowing you to check whether a lateinit variable has been initialized: 这里想要判断是否初始化了,需要用isInitialized来判断: ```kotlin class MyService{ @@ -388,9 +388,12 @@ class Test{ } } ``` -注意: ::myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省。 +注意: ::myService.isInitialized可用于判断adapter变量是否已经初始化。虽然语法看上去有点奇怪,但这是固定的写法。`::`前缀不能省。::是一个引用运算符,一般用于反射相关的操作中,可以引用属性或者函数。 这里可以写成`::myService.isInitialized`或`this::myService.isInitialized`。 如果在listener或者内部类中,可以这样写`this@OuterClassName::myService.isInitialized` +那lateinit有什么用呢? 每次使用还要判断isInitialized。 +The primary use-case for lateinit is when you can't initialize a property in the constructor but can guarantee that it's initialized "early enough" in some sense that most uses won't need an isInitialized check. E.g. because some framework calls a method initializing it immediately after construction. + 除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: ```kotlin From 020e8a57b0f3cd4334dd57ddc9ab21bd6fae07fe Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 6 Sep 2023 09:20:04 +0800 Subject: [PATCH 059/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 f2f97abb..0bf3f4f6 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" @@ -394,6 +394,10 @@ class Test{ 那lateinit有什么用呢? 每次使用还要判断isInitialized。 The primary use-case for lateinit is when you can't initialize a property in the constructor but can guarantee that it's initialized "early enough" in some sense that most uses won't need an isInitialized check. E.g. because some framework calls a method initializing it immediately after construction. +Lateinit Initialization is used when you want to initialize a variable at a later stage, especially when it's non-null and must be initialized before use. +It's commonly used in dependency injection and testing. + + 除了使用`lateinit`外还可以使用`by lazy {}`效果是一样的: ```kotlin @@ -417,7 +421,8 @@ test is not null haha - `by lazy{}`只能用在`val`类型而`lateinit`只能用在`var`类型 - `lateinit`不能用在可空的属性上和`java`的基本类型上,否则会报`lateinit`错误 - +- lateinit在分配之前不会初始化变量,而by lazy在第一次访问时初始化它。 +- 如果在初始化之前访问,lateinit会抛出异常,而lazy则可以确保已初始化。 lazy的背后是接收一个lambda并返回一个Lazy实例的函数,第一次访问该属性时,会执行lazy对应的Lambda表达式并记录结果,后续访问该属性时只是返回记录的结果。 另外系统会给lazy属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。 From a408ebbdefda5eb25928bff2ab36c1bd00a7a3fc Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 6 Sep 2023 09:23:21 +0800 Subject: [PATCH 060/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...17\230\351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 2 ++ 1 file changed, 2 insertions(+) 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 0bf3f4f6..36f64c4c 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" @@ -459,6 +459,8 @@ private var lazyValue: Fragment? = null } ``` +当您稍后需要在代码中初始化var时,请选择lateinit,它将被重新分配。当您想要初始化一个val值一次时,特别是当初始化的计算量很大时,请选择by lazy。 + ## 类的定义:使用`class`关键字 From 732cf6d2cf5957f3e8bc297d405ac4fa8a0251e3 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 6 Sep 2023 15:47:32 +0800 Subject: [PATCH 061/128] update --- "Jetpack/architecture/1.\347\256\200\344\273\213.md" | 6 ++---- .../2.ViewBinding\347\256\200\344\273\213.md" | 9 ++++++--- ...351\207\217&\347\261\273&\346\216\245\345\217\243.md" | 5 +++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git "a/Jetpack/architecture/1.\347\256\200\344\273\213.md" "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" index f64448a8..3bf0bd85 100644 --- "a/Jetpack/architecture/1.\347\256\200\344\273\213.md" +++ "b/Jetpack/architecture/1.\347\256\200\344\273\213.md" @@ -37,9 +37,7 @@ - `Room`:一个`SQLite`对象映射库 -平时我们比较熟悉的的架构有`MVC`,`MVP`及`MVVM`.这些结构有各自的优缺点,以现在比较流行的`MVP`为例, 它将不是关于界面的操作分发到`Presenter`中操作,再将结果通知给`View`接口的实现(通常是 `Activity/Fragment`). - -这些结构有各自的优缺点, 以现在比较流行的`MVP`为例, 它将不是关于界面的操作分发到`Presenter`中操作,再将结果通知给`View`接口的实现(通常是`Activity/Fragment`). +平时我们比较熟悉的的架构有`MVC`,`MVP`及`MVVM`.这些结构有各自的优缺点, 以现在比较流行的`MVP`为例, 它将不是关于界面的操作分发到`Presenter`中操作,再将结果通知给`View`接口的实现(通常是`Activity/Fragment`). `MVP`架构,当异步获取结果时,可能`UI`已经销毁,而`Presenter`还持有`UI`的引用,从而导致内存泄漏 @@ -133,4 +131,4 @@ override fun onDestroy() { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! ` \ No newline at end of file +- Good Luck! ` 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" index 012cbdf1..4718b3f9 100644 --- "a/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" +++ "b/Jetpack/architecture/2.ViewBinding\347\256\200\344\273\213.md" @@ -7,7 +7,7 @@ ViewBinding是Google在2019年I/O大会上公布的一款Android视图绑定工 ## 使用方法 ### 1.build.gradle中开启 -在build.gradle文件中的androidj节点添加如下代码: +在build.gradle文件中的android节点添加如下代码: ``` android { ... @@ -134,9 +134,12 @@ class TextAdapter( class TextViewHolder(val binding : ItemTextBinding) : RecyclerView.ViewHolder(binding.root) } ``` -#### includ +#### include -ViewBinding同样可以被用于中。include又分为两种形式,一种是有标签的样式,一种是没有的。 +ViewBinding同样可以被用于include中。 +include又分为两种形式: +- 一种是有标签的样式 +- 一种是没有的 没有标签的时候需要对include指定id,通过id来获取,例如: ```xml 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 36f64c4c..6128245e 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" @@ -755,7 +755,8 @@ data class Artist( var mbid: String) ``` -数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。例如,假设你创建了两个属性值完全相同的Artist对象, +数据类自动覆盖它们的equals方法以改变==操作符的行为,由此通过检查对象的每个属性值来判断是否相等。 +例如,假设你创建了两个属性值完全相同的Artist对象, 使用==操作符对它们进行比较将返回true,因为它们存放了相同的数据:除了提供从Any父类继承的equals方法的新实现,数据类还覆盖了hashCode和toString方法。 通过数据类,会自动提供以下函数: @@ -767,7 +768,7 @@ data class Artist( - `toString()` - `componentN()` -如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的一个或者多个属性被修改的实例。 +如果我们使用不可修改的对象,就像我们之前讲过的,假如我们需要修改这个对象状态,必须要创建一个新的或者多个属性被修改的实例。 这个任务是非常重复且不简洁的。 举个例子,如果要修改`Person`类中`charon`的`age`: From 17218219a84f1d1931d78ae6ea6ccbfdb5e55bd0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 21 Nov 2023 13:07:53 +0800 Subject: [PATCH 062/128] upate --- ...13\344\270\216\347\272\277\347\250\213.md" | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) 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 fd97534b..906d5fd8 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" @@ -675,6 +675,63 @@ Android使用的进程有些不同。活动管理器是Android负责正在运行 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/thread_heap.png?raw=true) +#### 线程的创建 + +```c +#include + +int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); +``` +功能: 创建一个线程 +参数: +- thread: 线程标识符地址 +- attr: 线程属性结构体地址 +- start_routine: 线程函数的入口地址 +- arg: 传给线程函数的参数 + +成功返回0,失败返回非0. + +与进程fork()函数不同的是pthread_create()创建的线程不与父线程在同一点开始运行,而是从指定的函数开始运行,该函数运行完后,该线程也就退出了。 + +```c +#include +#include +#include + +void *thread_fun(void *arg) { + printf("thread start run"); +} + +int main() { + printf("main thread run"); + pthread_t thread; + if (pthread_create(&thread, NULL, thread_fun, NULL) != 0) { + perror("fail to create thread"); + exit(1); + } + // 由于进程结束后,进程中所有的线程都会强制退出,所以现阶段不要让进程退出 + // 等待子线程执行,不然程序到这里直接结束,子线程还没来的及运行 + system("sleep 3"); + return 0; +} +``` + +#### 线程退出 + +```c +#include + +int pthread_join(pthread_t thread, void **retval); +``` +功能: 堵塞等待一个子线程的退出,可以接收到某一个子线程调用pthread_exit时设置的退出状态值。 + +参数: +- thread: 指定线程的id +- retval: 保存子线程的退出状态值,如果不接受则设置为NULL。 + + + + #### 线程和进程的区别 单个线程的状态与进程状态非常类似。线程有一个程序计数器(PC),记录程序从哪里获取指令。每个线程有自己的一组用于计算的寄存器。所以,如果有两个线程运行在一个处理器上,从运行一个线程(T1)切换到另一个线程(T2)时,必定发生上下文切换(context switch)。线程之间的上下文切换类似于进程间的上下文切换。对于进程,我们将状态保存到进程控制块(Process Control Block,PCB)。现在,我们需要一个或多个线程控制块(Thread Control Block,TCB),保存每个线程的状态。但是,与进程相比,线程之间的上下文切换有一点主要区别:地址空间保持不变(即不需要切换当前使用的页表)。 From 99ff3b404f4e92b11253c307c742a6d071263992 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 23 Nov 2023 10:27:28 +0800 Subject: [PATCH 063/128] update opengl part --- .../1.OpenCV\347\256\200\344\273\213.md" | 6 +- .../1.OpenGL\347\256\200\344\273\213.md" | 450 +++++++++++++----- ...66\344\270\211\350\247\222\345\275\242.md" | 28 +- ...42\345\217\212\345\234\206\345\275\242.md" | 31 +- ...45\231\250\350\257\255\350\250\200GLSL.md" | 33 +- 5 files changed, 408 insertions(+), 140 deletions(-) diff --git "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" index df0ceeee..d9b77c6e 100644 --- "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" @@ -42,6 +42,10 @@ OpenCV用C++语言编写,它具有C ++,Python,Java和MATLAB接口,并支 ### 数据结构:Mat +Mat是一个基本图像容器,也是一个类,数据由两个部分组成: +- 矩阵头(包含矩阵尺寸、存储方法、存储地址等信息) +- 一个指向存储所有像素值的矩阵的指针 + API: - imread(..url...):加载本地图像 默认的BGR 彩色图像加载方式,此外支持灰度图像和任意格式。注意:在OpenCV中颜色值写法是BGR,而不是RGB。 @@ -55,7 +59,7 @@ API: - blur(...) :模糊 - cvtColor(...):颜色转换 - Canny(...):提取边缘 -- waitKey(...):等待按键 +- waitKey(int delay = 0):等待按键 - imwrite(...): 图片保存,支持各种格式 ```c++ diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index eeb9c5dc..26a08a44 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -1,38 +1,43 @@ ## 1.OpenGL简介 -最近想要在播放器上做一个效果,需要用到OpenGL,一直以来也没关注学习过,就想着学习学习。在网上已经有很多文章,这里为什么还要写?主要是因为在学的 -时候发现那些文章看完后还是雨里雾里的不明白。要不就是不连贯,要不就是各种错误,各种理解的不对,而且版本不同,最开始看的3.0版本,后面我就都用3.0的来写, -结果网上的例子都是2.0的,GLSL的语法不一样,搞的死活出不来效果,耽误时间,所以干脆我系统学一遍,顺便记录下来,写一个简单的,能从入门开始一步步学习的, -文章里面有一些是从下面写的参考链接中拷贝过来的,也有一些是自己从书上看的。本书所有的例子都是用OpenGL ES3.0版本、GLSL ES 300版本来写。 -文章里面有一些是从下面写的参考链接中拷贝过来的,也有一些是自己从书上看的。 +OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程**接口**。 + + +然而,**OpenGL本身并不是一个API,它仅仅是一个由Khronos组织制定并维护的规范(Specification)**。 + +**OpenGL规范严格规定了每个函数该如何执行,以及它们的输出值。至于内部具体每个函数是如何实现(Implement)的,将由编写OpenGL库的人自行决定。** [OpenGL官网](https://www.opengl.org/) -OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形的跨语言、跨平台的应用程序编程接口。 -OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGl。OpenGl一般用 -于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGl基本带不动。为此,Khronos公司就为OpenGl提供了一个子集,OpenGl ES(OpenGl for Embedded System)。 +OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGL。 +OpenGL一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGL基本带不动。 +为此,Khronos公司就为OpenGL提供了一个子集,OpenGL ES(OpenGL for Embedded System)。 [后面文章所有的源码都在Github上](https://github.com/CharonChui/OpenGLES3.0StudyDemo) +且本文章使用的OpenGL版本为OpenGL 3.3(对应OpenGL ES 版本为OpenGL ES 3.3)。 ### OpenGL ES [OpenGL ES 官网](https://www.khronos.org/opengles/) -OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API,是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。 +OpenGL ES是免费的跨平台的功能完善的2D/3D图形库接口的API, 是OpenGL的一个子集,主要针对手机、Pad和游戏主机等嵌入式设备而设计。 -移动端使用到的基本上都是OpenGL ES,当然Android开发下还专门为OpenGL提供了android.opengl包,并且提供了GlSurfaceView,GLU,GlUtils等工具类。 +移动端使用到的基本上都是OpenGL ES,当然Android开发下还专门为OpenGL提供了android.opengl包,并且提供了GlSurfaceView, GLU, GlUtils等工具类。 +OpenGL ES由OpenGL裁剪而来,因此有必要了解一下两者版本的对应关系: +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_vs_gles.png?raw=true) ### OpenGL的作用 -在手机上有两大元件,一个是CPU,一个是GPU。显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染,但是目前为止最高效的方法就是有效的使用图形处理单元(GPU), -图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 +在手机或电脑上有两大元件,一个是CPU,一个是GPU。 +显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染。 +但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 - 图片处理:比如图片色调转换、美颜等。 - 摄像头预览效果处理。比如美颜相机、恶搞相机等。 @@ -52,136 +57,349 @@ Android中OpenGL ES的版本支持如下: +### OpenGL状态机(State Machine) +>>> As long as you keep in mind that OpenGL is basically one large state machine,most of its functionality will make more sense. + +- OpenGL自身是一个巨大的状态机: 描述该如何操作的所有变量的大集合 +- OpenGL的状态通常被称为上下文(Context) +- 状态设置函数(State-changing Function) +- 状态应用的函数(State-using Function) + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_state_machine.png?raw=true) + +我们通过改变一些**上下文变量**来改变OpenGL的状态,从而告诉OpenGL如何去绘图。 + +既然OpenGL是一个大的状态机,那状态机中肯定会有两种函数: + +- 设置状态的函数,例如`glClearColor(0.2f, 0.3f, 0.3f, 1.0f);` +- 使用状态的函数,例如`glClear(GL_COLOR_BUFFER_BIT);` + + +### OpenGL Context + +OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。 +OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。 +OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 + + +OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 + +### 对象 +OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中的一个。 + +一个对象是指一些选项的集合,代表OpenGL状态的一个子集。 + +可以将OpenGL中的对象看作是C语言中的结构体。 +例如下面可以用一个对象代表绘制窗口的设置,之后就可以设置它的大小、窗口支持的颜色位数等值。 + +``` C +struct object_Window_Target { + float set_window_size; // 这里调用的是OpenGL提供的设置窗口大小的某个方法 + float set_window_color; // 这里调用的事OpenGL提供的设置窗口颜色位数的某个方法 + .... +} +``` + +通常把OpenGL上下文比作一个大的结构体,包含很多子集。 + +```C +// OpenGL的状态 +struct OpenGL_Context { + .... + object *object_Window_Target; // 设置窗口大小、颜色位数等的对象 + .... +} +``` + +但是,这样也有问题: + +- 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦。 +- 这时候我们就需要一些小助理(对象),来帮忙记录某些状态信息,以便复用。 + +如果我们有10中子集,那就需要10个小助理(对象),而当前状态(Context,只有一份),可以通过装配这些对象来完成。 + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_zhuli.jpg?raw=true) + + + + ### 渲染管线 -在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。 +在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。 + +3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。 -图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。 +图形渲染管线可以被划分为两个主要部分: + +- 第一部分把你的3D坐标转换为2D坐标 +- 第二部分是把2D坐标转变为实际的有颜色的像素 + +图形渲染管线接收一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。 + +图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。 +所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。 +正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做**着色器(Shader)**。 简单来说,渲染管线就是一个顶点在呈现为像素之前经过的全部过程。 -OpenGL 1.x系列采用的是固定功能管线。 -OpenGL ES 2.0开始采用了可编程图形管线。 +- OpenGL 1.x系列采用的是固定功能管线。 +- OpenGL ES 2.0开始采用了可编程图形管线。 +- OpenGL ES 3.0兼容了2.0,并丰富了更多功能。 -OpenGL ES 3.0兼容了2.0,并丰富了更多功能。 +OpenGL渲染管线的流程为: +>> 顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲 -OpenGL渲染管线的流程为: 顶点数据 -> 顶点着色器 -> 图元装配 -> 几何着色器 -> 光栅化 -> 片段着色器 -> 逐片段处理 -> 帧缓冲 +OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规范组成: -OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规范组成:OpenGL ES3.0 API规范和OpenGL ES着色语言3.0规范(OpenGL ES SL)。 +- OpenGL ES3.0 API规范 +- OpenGL ES着色语言3.0规范(OpenGL ES SL)。 如下图,展示了OpenGL ES 3.0的图形管线。阴影的表示OpenGL ES 3.0管线中可编程阶段。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/oepngl_es_pip.jpg) -下面,你会看到一个图形渲染管线的每个阶段的抽象展示。要注意蓝色部分代表的是我们可以注入自定义的着色器的部分: +下面,你会看到一个图形渲染管线的每个阶段的抽象展示。 +要注意蓝色部分代表的是我们可以注入自定义的着色器的部分: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_progress.jpg) -首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标的数据的集合。而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。 +首先,我们以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data); +顶点数据是一系列顶点的集合。 +一个顶点(Vertex)是一个3D坐标的数据的集合。 +而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。 + + +### 着色器 + +着色器(shader)是在GPU上运行的小程序。 +从名称可以看出,可通过处理它们来处理顶点。 +此程序使用OpenGL ES SL语言来编写。 +它是一个描述顶点或像素特性的简单程序。 +**OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。** +**理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。** + +任何一种OpenGL程序本质上都可以被分为两部分: + +- CPU端运行的部分,采用C++、Java之类的语言编写 +- GPU端运行的部分,使用GLSL语言编写 + +### 顶点着色器 + + +图形渲染管线的第一个部分是顶点着色器,他是用来渲染图形顶点的OpenGL ES代码。 +它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。 + + +顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 + +顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 + +我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。 + +顶点缓冲对象是OpenGL中的一个对象(小助手),就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象: + +[OpenGL文档](https://docs.gl/) + +glGenBuffers函数的定义为: + +```C +// glGenBuffers — glGenBuffers returns n buffer object names in buffers. +void glGenBuffers(GLsizei n, + GLuint *buffers); +// n : Specifies the number of buffer object names to be generated. +// buffers : Specifies an array in which the generated buffer object names are stored. +``` +C使用: + +```C +GLuint vertex_buffer; +// 创建一个小助理(对象),并且给小助理起一个名字 +glGenBuffers(1, &vertex_buffer); +``` +Java使用: + +```java +// 下面只生成一个vbo对象,所以这里申请一个容量为1的int型缓冲区就可以 +IntBuffer vbo = IntBuffer.allocate(1); +// 该方法有两个参数,第一个是生成的数量,然后把它们的id储存在第二个参数的IntBuffer数组中 +GLES30.glGenBuffers(1, vbo); +``` +在OpenGL中有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 +OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。 + +我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上,glBindBuffer函数的定义为: + +```C +// glBindBuffer — bind a named buffer object + +void glBindBuffer(GLenum target, + GLuint buffer); +// target : Specifies the target to which the buffer object is bound. The symbolic constant must be GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER. +// buffer: Specifies the name of a buffer object. +``` +C使用: + +```C +// 让上下文给这个小助理分配工作任务,让小助理明确这次的工作内容 (绑定小助理(对象)到上下文) +glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); +``` +Java使用: + +```java +GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); +``` +从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。 +然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中(小助理把对应的数据单独记录下来了)。 + +```C +// glBufferData creates a new data store for the buffer object currently bound to target. +// glBufferData为当前绑定到的目标缓冲区对象创建一个新的数据存储 +void glBufferData(GLenum target, + GLsizeiptr size, + const GLvoid *data, + GLenum usage); + +// target : Specifies the target buffer object. The symbolic constant must be GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, or GL_UNIFORM_BUFFER. +// size : Specifies the size in bytes of the buffer object's new data store. +// data : Specifies a pointer to data that will be copied into the data store for initialization, or NULL if no data is to be copied. +// usage : Specifies the expected usage pattern of the data store. The symbolic constant must be GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, or GL_DYNAMIC_COPY. +``` +C使用: +```C +GLuint vertex_buffer; // Save this for later rendering +glGenBuffers(1, &vertex_buffer); +glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer); +glBufferData(GL_ARRAY_BUFFER, data_size_in_bytes, NULL, GL_STATIC_DRAW); +``` +Java使用: + +```java +GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * 4, + vertexBuffer, GLES30.GL_STATIC_DRAW); +``` + +**至此,已经把顶点数据储存在显卡的内存中,用创建的这个VBO顶点缓冲对象管理** + +glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。也就是让小助理来记录需要的数据。 + +- 第一个参数是目标缓冲的类型:这里顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。 +- 第二个参数是指定传输数据的大小(以字节为单位),c++中用一个简单的sizeof计算顶点数据的大小就可以,这里float的大小为4个字节。 +- 第三个参数是我们希望发送的实际数据。 +- 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种常用的形式: + - GL_STATIC_DRAW :数据不会或几乎不会改变。 + - GL_DYNAMIC_DRAW:数据会被改变很多。 + - GL_STREAM_DRAW :数据每次绘制时都会改变。 + + +**使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。** +当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 -- 顶点着色器(Vertex Shader): 图形渲染管线的第一个部分是顶点着色器,他是用来渲染图形顶点的OpenGL ES代码。它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。 +整体总结为: - 着色器(shader)是在GPU上运行的小程序。从名称可以看出,可通过处理它们来处理顶点。此程序使用OpenGL ES SL语言来编写。它是一个描述顶点或像素特性的简单程序。OpenGL最本质的概念之一就是着色器,它是图形硬件设备所执行的一类特殊函数。理解着色器最好的办法就是把它看做是专为图形处理单元(即GPU)编译的一种小型程序。任何一种OpenGL程序本质上都可以被分为两部分:CPU端运行的部分,采用C++、Java之类的语言编写;以及GPU端运行的部分,使用GLSL语言编写。 +- 它会在GPU上创建内存,用于存储我们的顶点数据 + - 通过顶点缓冲对象(Vertex Buffer Objects, VBO)来管理数据,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。 + - OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型(每一个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。 - 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 +- 配置OpenGL如何解释这些内存 + 通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。 + **注意: VAO并不保存实际数据,而是放顶点结构的定义** - 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 - 我们通过顶点缓冲对象(Vertex Buffer Object, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这个是非常快的过程。 +![image](https://github.com/CharonChui/Pictures/blob/master/vao_vbo.jpg?raw=true) - OpenGLES2.0 编程中,用于绘制的顶点数组数据首先保存在 CPU 内存,在调用 glDrawArrays 或者 glDrawElements 等进行绘制时,需要将顶点数组数据从 CPU 内存拷贝到显存。 + - 但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。 +OpenGLES2.0编程中,用于绘制的顶点数组数据首先保存在CPU内存,在调用glDrawArrays或者glDrawElements等进行绘制时,需要将顶点数组数据从CPU内存拷贝到显存。 - OpenGLES3.0 VBO 和 EBO 的出现就是为了解决这个问题。 VBO 和 EBO 的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的 CPU 与 GPU 之间的内存拷贝,可以提升渲染性能,降低内存带宽和功耗。 +但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。 +OpenGLES3.0 VBO和EBO的出现就是为了解决这个问题。VBO和EBO的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的CPU与GPU之间的内存拷贝,可以提升渲染性能,降低内存带宽和功耗。 - 顶点缓冲对象是OpenGL中的一个对象,就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers()函数和一个缓冲ID生成一个VBO对象: - ```java - // 下面只生成一个vbo对象,所以vbo的容量设置为1即可 - IntBuffer vbo = IntBuffer.allocate(1); - // 该方法有两个参数,第一个是输入生成纹理的数量,然后把它们的id储存在第二个参数的IntBuffer数组中 - GLES30.glGenBuffers(1, vbo); - ``` - 在OpenGL中有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上: - ```java - GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); - ``` - 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中: - ```java - GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, POSITION_VERTEX.size * 4, - vertexBuffer, GLES30.GL_STATIC_DRAW); - ``` - glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。 - 他的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。 - 第二个参数是指定传输数据的大小(以字节为单位),c++中用一个简单的sizeof计算顶点数据的大小就可以,这里float的大小为4个字节。 - 第三个参数是我们希望发送的实际数据。 - 第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式: - - GL_STATIC_DRAW :数据不会或几乎不会改变。 - - GL_DYNAMIC_DRAW:数据会被改变很多。 - - GL_STREAM_DRAW :数据每次绘制时都会改变。 +把数据发送给OpenGL管线还要更加复杂一点,有两种方式: - 把数据发送给OpenGL管线还要更加复杂一点,有两种方式: - - 通过顶点属性的缓冲区; - - 直接发送给统一变量。 - 理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 +- 通过顶点属性的缓冲区; +- 直接发送给统一变量。 +理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 -##### 缓冲区和顶点属性 -想要绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中: +### 缓冲区和顶点属性 + +想要绘制一个对象,它的顶点数据需要发送给顶点着色器。 +通常会把顶点数据在放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。 +要完成这件事,有好几个步骤需要做,有些步骤只需要做一次,而对于动画场景,一些步骤则需要每帧做一次。只做一次的步骤如下,它们一般包含在init()中: - 创建缓冲区。 - 将顶点数据复制到缓冲区。 - 每帧都要做的步骤如下,它们一般包含在display()中。 + - 每帧都要做的步骤如下,它们一般包含在display()中。 - 启用包含顶点数据的缓冲区。 - 将这个缓冲区和一个顶点属性相关联。 - 启用这个顶点属性。 -- 使用glDrawArrays()绘制对象。 -所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在OpenGL应用程序中被声明和实例化。一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。 +- 使用glDrawArrays()绘制对象。 + +所有缓冲区通常都在程序开始的时候统一创建,可以在init()中,或者在被init()调用的函数中。 + +在OpenGL中,缓冲区被包含在顶点缓冲对象(Vertex Buffer Object, VBO)中,VBO在OpenGL应用程序中被声明和实例化。 + +一个场景可能需要很多VBO,所以我们常常会在init()中生成并填充若干个VBO,以备程序需要时直接使用。 -- 图元装配(Primitive Assembly) +### 图元装配(Primitive Assembly) - 顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 - 图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状。 - 也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。 +顶点组合成图元的过程叫做图元装配,这里的图元就是指点、线、三角。 +图元装配阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定的图元的形状。 +也就是说图元装配是将顶点着色器中设置的顶点数据装配成指定图元的形状。 - OpenGL ES中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: +OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图形都是由三角形组成的。复杂的图形都可以拆分成多个三角形。比如OpenGL提供给开发者的绘制方法glDrawArrays,这个方法的第一个参数就是指定绘制方式,可选值有: - - GL_POINTS:以点的形式绘制 - - GL_LINES:以线的形式绘制 - - GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 +- GL_POINTS:以点的形式绘制 +- GL_LINES:以线的形式绘制 +- GL_TRIANGLE_STRIP:以三角形的形式绘制,所有二维图像的渲染都会使用这种方式。 - 该过程还有两个重要操作:裁剪和淘汰。 - - 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 - - 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 +该过程还有两个重要操作,裁剪和淘汰: + +- 裁剪是指对于不在视椎体(屏幕上可见的3D区域)内的图元进行裁剪。 +- 淘汰是指根据图元面向前方或后方选择抛弃它们(如事物内部的点)。 -- 几何着色器(Geometry Shader) - 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。本例子中,它生成了另一个三角形。 +### 几何着色器(Geometry Shader) + +图元装配阶段的输出会传递给几何着色器(Geometry Shader)。 + +几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。例如它生成了另一个三角形。 + +几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。 + +在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。 + +### 光栅化阶段(Rasterization Stage) + +这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。 +在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。 +顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 + +这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 - 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。 -- 光栅化阶段(Rasterization Stage) +光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 - 这里会把图元映射为最终屏幕上相应的像素,生成供片段着色器(fragment shader)使用的片段。 - 在图元装配后传递过来的图元数据中,这些图元信息只是顶点而已。顶点处都还没有像素点,直线段端点之间是空的、多边形的边和内部也是空的,光栅化的任务就是构造这些。将图片转化为片段(fragment)的过程叫做光栅化。 - 这个阶段会将图元数据分解成更小的单元并对应于帧缓冲区的各个像素,这些单元成为片元,一个片元可能包含窗口颜色、纹理坐标等属性。 +### 片段着色器或片元着色器(Fragment Shader) - 光栅化其实是一种将几何图元变成二维图像的过程。在这里,虚拟3D世界中的物体投影到平面上,并生成一系列的片段。 +使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 -- 片段着色器或片元着色器(Fragment Shader):使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 +主要目的是计算一个像素的最终颜色。 - 主要目的是计算一个像素的最终颜色。光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 +光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。 +为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。 +一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 - 通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 +通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 可以看到,图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。 -在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。 +**在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。** -- 逐片段操作 +整个处理过程又分为以下几个部分: +- 逐片段操作 具体的细分步骤又分为: - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_fragment_opera.jpg) - 像素归属测试 @@ -208,62 +426,54 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 用于最小化,因为使用有限精度在帧缓冲区中保存颜色值而产生的伪像,使用少量颜色模拟更宽的颜色范围。 -- 帧缓冲区 - OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区,然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。 - OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) +### 帧缓冲区 -- 着色器语言 +OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区,然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。 - GLSL(OpenGL Shading Language)是OpenGL着色语言。 +OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) - 在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分: +### 着色器语言 - - 顶点着色器(Vertex Shader) - - 片段着色器(Fragment Shader) +GLSL(OpenGL Shading Language)是OpenGL着色语言。 -- 坐标系 +在图形卡的GPU上执行,代替了固定的渲染管线的一部分,是渲染管线中不同层次具有可编程性,例如:视图转换、投影转换等。GLSL的着色器代码分为两个部分: - OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 - - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) - - OpenGL ES采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。 - -- 形状面和缠绕 + - 顶点着色器(Vertex Shader) + - 片段着色器(Fragment Shader) - 在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。 +### 坐标系 - ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_ex_xy_1.jpg) +OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 - 三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_xy.png) -- 程式(Program) +OpenGL采用的是右手坐标,选取屏幕中心为原点,从原点到屏幕边缘默认长度为1,也就是默认情况下,从原点到x左边的1和到y左边的1的位置在屏幕上显示的并不相同。 - 一个OpenGL ES对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个对象。 +### 形状面和缠绕 +在OpenGL的世界里,我们只能画点、线、三角形,图元装配中说到所有复杂的图形都是由三角形组成。 +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_ex_xy_1.jpg) -如果想要绘制图像,需要至少一个顶点着色器来定义一个图形顶点,以及一个片元着色器为该图形上色。这些着色器必须被编译然后再添加到一个OpenGL ES Program中,并利用这个Program来绘制形状。 +三角形的点按顺序定义,使得它们以逆时针方向绘制。绘制这些坐标的顺序定义了形状的缠绕方向。默认情况下,在OpenGL中,逆时针绘制的面是正面。 +### 程式(Program) +一个OpenGL对象,包含了你所希望用来绘制图形所要用到的着色器,最后顶点着色器和片元着色器都要放入到程式中,然后才可以使用,简单的说就是将两个着色器变为一个程序对象。 -### OpenGL Context -OpenGL是一个仅仅关注图像渲染的图像接口库,在渲染过程中它需要将顶点信息、纹理信息、编译好的着色器等渲染状态信息存储起来,而存储这些信息调用任何OpenGL函数前,必须先创建OpenGL Context,它存储了OpenGL的状态变量以及其他渲染有关的信息。 -OpenGL是个状态机,有很多状态变量,是个标准的过程式操作过程,改变状态会影响后续所有的操作,这和面向对象的解耦原则不符,毕竟渲染本身就是个复杂的过程。 -OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 +如果想要绘制图像,需要至少一个顶点着色器来定义一个图形顶点,以及一个片元着色器为该图形上色。这些着色器必须被编译然后再添加到一个OpenGL Program中,并利用这个Program来绘制形状。 -OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 ### EGL 在OpenGL的设计中,OpenGL是不负责管理窗口的,窗口的管理交由各个设备自己完成。具体的来说就是iOS平台上使用EAGL提供本地平台对OpenGL的实现,在Android平台上使用EGL提供本地平台对OpenGL的实现。OpenGL其实是通过GPU进行渲染。但是我们的程序是运行在CPU上,要与GPU关联,这就需要通过EGL,它相当于Android上层应用于GPU通讯的中间层。 EGL提供了OpenGL ES和运行于计算机上的原生窗口系统(如Windows、Mac OSX)之间的一个“结合”层次。 -在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,成为EAGL。 +在EGL能够确定可用的绘制表面类型(或者底层系统的其他特性)之前,它必须打开和窗口系统的通信渠道。注意。Apple提供自己的EGL API的iOS实现,称为EAGL。 因为每个窗口系统都有不同的语言,所以EGL提供基本的不透明类型--EGLDisplay,该类型封装了所有系统相关性,用于和原生窗口系统接口。任何使用EGL的应用程序必须执行的第一个操作就是创建和初始化与本地EGL显示的连接。 -EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用: +EGL是OpenGL和本地窗口系统(Native Window System)之间的通信接口,它的主要作用: - 与设备的原生窗口系统通信; - 查询绘图表面的可用类型和配置; @@ -273,7 +483,9 @@ EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信 OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异(Apple 提供了自己的 EGL API 的 iOS 实现,自称 EAGL)。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/egl_interface_1.png?raw=true) -上图中: + +上图中: + - Display(EGLDisplay) 是对实际显示设备的抽象; - Surface(EGLSurface)是对用来存储图像的内存区域 FrameBuffer 的抽象,包括 Color Buffer(颜色缓冲区), Stencil Buffer(模板缓冲区) ,Depth Buffer(深度缓冲区); - Context (EGLContext) 存储 OpenGL ES 绘图的一些状态信息; @@ -281,7 +493,6 @@ OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平 - 本地窗口相关的 API 提供了访问本地窗口系统的接口,而 EGL 可以创建渲染表面 EGLSurface ,同时提供了图形渲染上下文 EGLContext,用来进行状态管理,接下来 OpenGL ES 就可以在这个渲染表面上绘制。 @@ -321,7 +532,7 @@ EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 #### 创建屏幕外渲染区域: EGL Pbuffer -除了可以用OpenGL ES在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 +除了可以用OpenGL在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。 Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。 创建Pbuffer和创建EGL窗口非常相似,只有少数微小的不同。为了创建Pbuffer,需要和窗口一样找到EGLConfig,并作一处修改:我们需要扩增EGL_SURFACE_TYPE的值,使其包含EGL_PBUFFER_BIT。 @@ -333,7 +544,6 @@ Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个 纹理(Texture)是一个2D图片(也有1D和3D纹理),他可以用来添加物体的细节,你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就有砖墙的外表了。 - Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 36391f49..4b9285ba 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -10,6 +10,7 @@ OpenGL ES的绘制需要有以下步骤: 所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 + ```glsl float vertices[] = { -0.5f, -0.5f, 0.0f, @@ -98,6 +99,7 @@ GLES30.glLinkProgram(shaderProgramId); 链接完后需要使用glUseProgram方法,用刚创建的程序对象的Id作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了: + ```java GLES30.glDeleteShader(vertexShaderId); GLES30.glDeleteShader(fragmentShaderId); @@ -121,13 +123,13 @@ GLES30.glDeleteShader(fragmentShaderId); 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: - ``` + ```java // glVertexAttribPointer是把顶点位置属性赋值给着色器程序 // 第一个参数0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 // 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 // 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer); -//启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 +//启用顶点变量,参数为数据中第一个值在缓冲开始的位置。 这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 GLES30.glEnableVertexAttribArray(0); ``` @@ -146,8 +148,10 @@ GLES30.glEnableVertexAttribArray(0); 由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性现在会链接到它的顶点数据。 现在我们已经定义了OpenGL该如何解释顶点数据,接着应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。 + 自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。 在OpenGL中绘制一个物体,代码会像是这样: + ```java // 0. 复制顶点数组到缓冲中供OpenGL使用,使用glGenBuffers函数和一个缓冲ID生成一个VBO对象 IntBuffer vbo = IntBuffer.allocate(1); @@ -184,12 +188,15 @@ someOpenGLFunctionThatDrawsOurTriangle(); ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects.png) 创建一个VAO和创建一个VBO很类似: + ```java IntBuffer arrays = IntBuffer.allocate(1); GLES30.glGenVertexArrays(1, arrays); ``` 要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。 当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: + +VAO小助理偷偷帮我们记录一切: ```java // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. // 1. 绑定VAO @@ -211,9 +218,15 @@ GLES30.glBindVertexArray(0); someOpenGLFunctionThatDrawsOurTriangle(); ``` -就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。 +就这么多了!前面做的一切都是等待这一刻,一个储存了我们顶点属性配置和应使用的VBO的顶点数组对象。 +一般当你打算绘制多个物体时,你首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。 注意: 先绑定VAO后再绑定VBO,这样他们才能关联起来当我们打算绘制物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。 +![image](https://github.com/CharonChui/Pictures/blob/master/vao_vbo.jpg?raw=true) + + +当目前是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用(这也意味着它也会存储解绑调用)。 +当时VAO不会存储GL_ARRAY_BUFFER(VBO)的glBindBuffer的函数调用。 @@ -242,6 +255,7 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 安装完GLSL插件后,就可以开始了。一般将GLSL文件放到raw或assets目录,我们这里在raw目录上右键New,然后选择GLSL Shader创建就可以了,创建后默认会生成一个main()函数。 - 顶点着色器(triangle_vertex_shader.glsl) + ```glsl // 声明着色器的版本 #version 300 es @@ -283,12 +297,14 @@ precision是精度限定符,它可以使着色器创作者指定着色器变 在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 精度限定符可以用于指定任何浮点数或整数的变量的精度。指定精度的关键字是lowp、mediump和highp。下面是一些带有精度限定符的声明示例: -``` + +```glsl highp vec4 position; mediump float specularExp; ``` 除了精度限定符之外,还有默认精度的概念。也就是说,如果变量声明时没有使用精度限定符,它将拥有该类型的默认精度。默认精度限定符在顶点或片段着色器的开头用以下语法指定: -``` + +```glsl precision highp float; precision mediump int; ``` @@ -570,7 +586,7 @@ public class TriangleRender implements GLSurfaceView.Renderer { ``` -调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成链接着色器对象时,可以用glDeleteShader删除。 +调用glCreateShader将根据传入的type参数创建一个新的顶点或片段着色器。返回值是指向新着色器对象的句柄。当完成链接着色器对象时,可以用glDeleteShader删除。 注意,如果一个着色器连接到一个程序对象,那么调用glDeleteShader不会立即删除着色器,而是将着色器编标记为删除,在着色器不再连接到任何程序对象时,它的内存将被释放。 效果如下: diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index a44d86be..c73d718e 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -158,7 +158,11 @@ public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Rendere ### 元素缓冲对象(EBO) -在渲染顶点这一话题上我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合: +在渲染顶点这一话题上我们还有最后一个需要讨论的东西——元素缓冲对象(Element Buffer Object,EBO),也叫索引缓冲对象(Index Buffer Object,IBO)。 + +要解释元素缓冲对象的工作方式最好还是举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。 +我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这会生成下面的顶点的集合: + ```java float vertices[] = { // 第一个三角形 @@ -171,10 +175,17 @@ float vertices[] = { -0.5f, 0.5f, 0.0f // 左上角 }; ``` -可以看到,有几个顶点叠加了。我们指定了右下角和左上角两次!一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧? +可以看到,有几个顶点叠加了。 +我们指定了右下角和左上角两次! + +一个矩形只有4个而不是6个顶点,这样就产生50%的额外开销。 +当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。 + +更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。如果OpenGL提供这个功能就好了,对吧? 值得庆幸的是,元素缓冲区对象的工作方式正是如此。 EBO是一个缓冲区,就像一个顶点缓冲区对象一样,它存储 OpenGL 用来决定要绘制哪些顶点的索引。 这种所谓的索引绘制(Indexed Drawing)正是我们问题的解决方案。首先,我们先要定义(不重复的)顶点,和绘制出矩形所需的索引: + ```java float vertices[] = { 0.5f, 0.5f, 0.0f, // 右上角 @@ -192,21 +203,25 @@ int indices[] = { 1, 2, 3 // 第二个三角形 }; ``` -你可以看到,当使用索引的时候,我们只定义了4个顶点,而不是6个。下一步我们需要创建元素缓冲对象: + +你可以看到,当使用索引的时候,我们只定义了4个顶点,而不是6个。下一步我们需要创建元素缓冲对象: + ```java //创建 ebo GLES30.glGenBuffers(1,ebo,0) //绑定 ebo 到上下文 -GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER,ebo[0]) -//昂丁 +GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0]) +//设置数据 GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER, indexData.capacity() * 4, indexData, GLES30.GL_STATIC_DRAW ) ``` -与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 +与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。 +同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制: + ```java GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP,6,GLES30.GL_UNSIGNED_INT,0) ``` @@ -216,7 +231,9 @@ glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects_ebo.png?raw=true) -当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。 +**注意: EBO的绑定一定要在VAO解绑之前绑定!** +**当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。** +**OpenGL中会解绑VBO和VAO但是不能解绑EBO)** diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index b2f3e96d..b7d82bae 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -2,15 +2,26 @@ 着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。 -着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。 +着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。 +每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。 -和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。 +和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。 +GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。 +GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。 + +与C语言有区别的地方: +- 共享命名空间(Shared Namespace) + Shaders操作是相互独立的,但是为了方便Shaders间通信,在链接成一个shader program时会共享变量的名字。 +- GLSL没有char、char *和string数据类型,也没有字符串操作 +- 不支持隐式类型转换,例如: int(arg) +- vec数据方位: vec4(r,g,b,a / x,y,z,w / s,t,p,q)。 vec3.xy = vec2 +- 新的变量类型: Attribute、Uniform、Varying ### 顶点着色器: -``` +```glsl #version 300 es layout (location = 0) in vec3 position; // position变量的属性位置值为0 @@ -22,8 +33,10 @@ void main() { } ``` +```glsl #version 300 es是表示GLSL语言的版本为300,对应的是OpenGL ES 3.0版本。 #version必须写在文件的第一行,即是第一行是注释也不行。 +``` @@ -34,7 +47,9 @@ void main() { - 采样器:代表顶点着色器使用纹理的特殊统一变量类型。 - 着色器程序:描述顶点上执行操作的顶点着色器的程序源代码或可执行文件。 -这里需要重点讲一下上面的版本: OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关系,如果没有在着色器文件中用#version标明使用版本的时候默认使用的是OpenGL ES 2.0版本(GLSL ES 100): +这里需要重点讲一下上面的版本: + +OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的版本有对应的关联关系,如果没有在着色器文件中用#version标明使用版本的时候默认使用的是OpenGL ES 2.0版本(GLSL ES 100): - OpenGL ES 2.0版本对应GLSL ES 100版本 - OpenGL ES 3.0版本对应GLSL ES 300版本 @@ -55,7 +70,7 @@ void main() { 当使用samplerExternalOES是,如果在\#extension GL_OES_EGL_image_external : require会报错,需要变成#extension GL_OES_EGL_image_external_essl3 : require -- #version 300 es这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。 +- `#version 300 es`这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。 虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。 @@ -120,7 +135,12 @@ precision mediump int; #### Uniform -Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 +Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。 + +首先,uniform是全局的(Global)。 +全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。 + +第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。 @@ -143,6 +163,7 @@ void main() 如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点! 这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色: + ```glsl float timeValue = glfwGetTime(); float greenValue = (sin(timeValue) / 2.0f) + 0.5f; From 71c512348b8e034c0cdcdec4645b6df0e3afa1e4 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 23 Nov 2023 10:40:22 +0800 Subject: [PATCH 064/128] update --- ...45\231\250\350\257\255\350\250\200GLSL.md" | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index b7d82bae..90c75bb4 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -9,6 +9,13 @@ GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。 GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。 +向量(Vector): +- vecn : the default vector of n floats +- bvecn : a vector of n booleans +- ivecn : a vector of n integers +- uvecn : a vector of u unsigned integers +- dvecn : a vector of n double components + 与C语言有区别的地方: - 共享命名空间(Shared Namespace) Shaders操作是相互独立的,但是为了方便Shaders间通信,在链接成一个shader program时会共享变量的名字。 @@ -79,11 +86,15 @@ OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的 #### in、out -顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。 +顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。 +顶点着色器的输入特殊在,它从顶点数据中直接接收输入。 +为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。 +我们已经在前面的教程看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。 你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量。 -如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。 +如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。 +**当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。** 顶点着色器 ```glsl @@ -142,7 +153,10 @@ Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式, 第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 -统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 +统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。 + +因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。 +这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。 @@ -160,7 +174,7 @@ void main() ``` 我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 -如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点! +**如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!** 这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色: From 0f502b6070e6fb6158ae886fa83b8a0868e74a93 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 23 Nov 2023 21:29:27 +0800 Subject: [PATCH 065/128] update opengl part --- .../1.OpenGL\347\256\200\344\273\213.md" | 39 +++++++ .../11.OpenGL ES\346\273\244\351\225\234.md" | 3 +- VideoDevelopment/OpenGL/12.FBO.md | 14 +-- ...66\344\270\211\350\247\222\345\275\242.md" | 104 ++++++++++------- ...42\345\217\212\345\234\206\345\275\242.md" | 29 +++-- ...45\231\250\350\257\255\350\250\200GLSL.md" | 110 +++++++++++------- ...\261\273\345\217\212Matrix\347\261\273.md" | 21 ++-- .../9.OpenGL ES\347\272\271\347\220\206.md" | 65 ++++++----- 8 files changed, 249 insertions(+), 136 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 26a08a44..7578a7eb 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -168,6 +168,45 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。 +### 立即渲染模式(Immediate mode) && 核心模式(Core-profile) +早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。 + +当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。立即渲染模式从OpenGL实际运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。 + +这也是为什么我们的教程面向OpenGL3.3的核心模式。虽然上手更困难,但这份努力是值得的。 + +现今,更高版本的OpenGL已经发布(写作时最新版本为4.5),你可能会问:既然OpenGL 4.5 都出来了,为什么我们还要学习OpenGL 3.3?答案很简单,所有OpenGL的更高的版本都是在3.3的基础上,引入了额外的功能,并没有改动核心架构。新版本只是引入了一些更有效率或更有用的方式去完成同样的功能。因此,所有的概念和技术在现代OpenGL版本里都保持一致。当你的经验足够,你可以轻松使用来自更高版本OpenGL的新特性。 + + + +### 依赖库 + +简要来说,GLAD、GLEW都是一个图形库,可以理解为是在显卡驱动上给渲染用户一个统一的API; +而GLUT、FreeGLUT、GLFW这三个是用于图形开发的辅助工具库,主要用于创建和管理OpenGL环境、操作窗口等。 + +OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。但是在你真正能够在程序中使用OpenGL之前,你需要对他进行初始化,但是由于OpenGL是跨平台的,所以也没有一个标准的方式进行初始化。OpenGL初始化分为两个阶段: + +- 第一个阶段,你需要创建一个OpenGL上下文环境,这个上下文环境存储了所有与OpenGL相关的状态(OpenGL是一个状态机),上下文位于操作系统中某个进程中,一个进程可以创建多个上下文,每一个上下文都可以描绘一个不同的可视界面,就像应用程序中的窗口;简单来理解就是为了创建一个窗口;而GLUT、FreeGLUT、GLFW库就是用于干这件事的。 + +- 第二个阶段,你需要定位所有需要在OpenGL中使用的函数(由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用),而GLEW、GLAD就是干这件事的。 + + + +### GLFW + +GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所需的最低限度的接口。它允许用户创建OpenGL上下文,定义窗口参数以及处理用户输入。 +简单来说,GLFW负责创建窗口,处理窗口相关的事件(如键盘和鼠标输入),并提供一个OpenGL上下文供你的程序使用。 +这些库节省了我们书写操作系统相关代码的时间,提供给我们一个窗口和一个OpenGL上下文用来渲染。 + + + +### GLAD + +然后,我们有GLAD。由于OpenGL驱动版本众多,它大多数函数的位置都无法在编译时确定下来,需要在运行时查询。所以任务就落在了开发者身上,开发者需要在运行时获取函数地址并将其保存在一个函数指针中供以后使用。取得地址的方法因平台而异,代码非常复杂,而且很繁琐,我们需要对每个可能使用的函数都要重复这个过程。幸运的是,有些库能简化此过程,其中GLAD是目前最新,也是最流行的库。GLAD是用来管理OpenGL的函数指针的,所以在调用任何OpenGL的函数之前我们需要初始化GLAD。GLAD也可以使OpenGL基础渲染变得简单。 + + +到了这些感觉不想学了,又是GLFW、又是GLAD,每个都要看,每个都要学,太麻烦了。好在Android上已经帮我们处理好了,我们只要使用GLSurfaceView即可。 + ### 着色器 着色器(shader)是在GPU上运行的小程序。 diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index 675f4d24..e76e826e 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -354,7 +354,8 @@ public class BaseFilter { } ``` -在Render中改变的地方就是onDrawFrame()方法中去调用Filter.onDraw()方法: +在Render中改变的地方就是onDrawFrame()方法中去调用Filter.onDraw()方法: + ```java @Override public void onDrawFrame(GL10 gl) { diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md index 728b6430..4a9632fb 100644 --- a/VideoDevelopment/OpenGL/12.FBO.md +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -2,22 +2,22 @@ FBO(Frame Buffer Object)即帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO)。 -FBO 本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了 3 种附着(Attachment),分别是颜色附着、深度附着和模板附着。 +FBO本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了3种附着(Attachment),分别是颜色附着、深度附着和模板附着。 -RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的 2D 图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作 FBO 中的颜色、深度或者模板附着。 +RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的2D图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作FBO中的颜色、深度或者模板附着。 -使用 FBO 作为渲染目标时,首先需要为 FBO 的附着添加连接对象,如颜色附着需要连接纹理或者渲染缓冲区对象的颜色缓冲区。 +使用FBO作为渲染目标时,首先需要为FBO的附着添加连接对象,如颜色附着需要连接纹理或者渲染缓冲区对象的颜色缓冲区。 #### 为什么用 FBO -默认情况下,OpenGL ES 通过绘制到窗口系统提供的帧缓冲区,然后将帧缓冲区的对应区域复制到纹理来实现渲染到纹理,但是此方法只有在纹理尺寸小于或等于帧缓冲区尺寸才有效。 +默认情况下,OpenGL ES通过绘制到窗口系统提供的帧缓冲区,然后将帧缓冲区的对应区域复制到纹理来实现渲染到纹理,但是此方法只有在纹理尺寸小于或等于帧缓冲区尺寸才有效。 -另一种方式是通过使用连接到纹理的 pbuffer 来实现渲染到纹理,但是与上下文和窗口系统提供的可绘制表面切换开销也很大。因此,引入了帧缓冲区对象 FBO 来解决这个问题。 +另一种方式是通过使用连接到纹理的pbuffer来实现渲染到纹理,但是与上下文和窗口系统提供的可绘制表面切换开销也很大。因此,引入了帧缓冲区对象FBO来解决这个问题。 -Android OpenGL ES 开发中,一般使用 GLSurfaceView 将绘制结果显示到屏幕上,然而在实际应用中,也有许多场景不需要渲染到屏幕上,如利用 GPU 在后台完成一些图像转换、缩放等耗时操作,这个时候利用 FBO 可以方便实现类似需求。 +Android OpenGL ES开发中,一般使用GLSurfaceView将绘制结果显示到屏幕上,然而在实际应用中,也有许多场景不需要渲染到屏幕上,如利用GPU在后台完成一些图像转换、缩放等耗时操作,这个时候利用FBO可以方便实现类似需求。 -使用 FBO 可以让渲染操作不用再渲染到屏幕上,而是渲染到离屏 Buffer 中,然后可以使用 glReadPixels 或者 HardwareBuffer 将渲染后的图像数据读出来,从而实现在后台利用 GPU 完成对图像的处理。 +使用FBO可以让渲染操作不用再渲染到屏幕上,而是渲染到离屏Buffer中,然后可以使用glReadPixels或者HardwareBuffer将渲染后的图像数据读出来,从而实现在后台利用GPU完成对图像的处理。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 4b9285ba..18119b7d 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -2,7 +2,7 @@ OpenGL ES的绘制需要有以下步骤: -- 顶点输入 +### 1. 顶点输入 开始绘制图形之前,我们必须先给OpenGL输入一些顶点数据。 OpenGL是一个3D图形库,所以我们在OpenGL中所指定的所有坐标都是3D坐标(xyz)。OpenGL并不是简单地把所有的3D坐标变换成屏幕上的2D像素。 @@ -11,7 +11,7 @@ OpenGL ES的绘制需要有以下步骤: 由于我们希望渲染一个三角形,我们一共要指定三个顶点,每个顶点都有一个3D位置。我们会将它们以标准化设备坐标的形式(OpenGL的可见区域)定义为一个float数组。 -```glsl +```java float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, @@ -19,7 +19,9 @@ float vertices[] = { }; ``` 定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。 -它会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 +**这里会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。** + +顶点着色器接着会处理我们在内存中指定数量的顶点。 我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(通常被称为显存)中储存大量顶点。 使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。 @@ -28,9 +30,9 @@ float vertices[] = { 顶点缓冲对象是我们在OpenGL教程中第一个出现的OpenGL对象。就像OpenGL中的其它对象一样,这个缓冲有一个独一无二的ID,所以我们可以使用glGenBuffers函数和一个缓冲ID生成一个VBO对象。 -- 顶点着色器 +### 2.顶点着色器 - 着色器都是使用GLSL语言来写的而GLSL版本之间有差异。 + 着色器都是使用GLSL语言来写的,而GLSL不同版本之间会有差异。 ```glsl #version 300 es @@ -44,27 +46,33 @@ float vertices[] = { 每个着色器都起始于一个版本声明,这里声明的是GLSL ES 300版本,在Android中它对应的OpenGL ES版本为3.0,而GLSL ES 100版本则对应的是OpenGL ES 2.0版本。如果不写版本默认的就是100。 - 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。 - 由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)你后面会看到为什么我们会需要这个位置值。 + 下一步,使用in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。现在我们只关心位置(Position)数据,所以我们只需要一个顶点属性。 + GLSL有一个向量数据类型,它包含1到4个float分量,包含的数量可以从它的后缀数字看出来。 + 由于每个顶点都有一个3D坐标,我们就创建一个vec3输入变量position。 + 我们同样也通过layout (location = 0)设定了输入变量的位置值(Location)。 + `layout(location = 0);`用于指定着色器中的变量 position 与 OpenGL程序中顶点数组中数据的关联关系,这里表示该位置数据的位置索引为0。以确保在着色器中能正常的接收输入的数据。 + 你后面会看到为什么我们会需要这个位置值。 + -向量(Vector) - -在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。 -在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。 -注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。我们会在后面的教程中更详细地讨论向量。 +##### 向量(Vector) +在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。 +在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。 +注意vec.w分量不是用作表达空间中的位置的(我们处理的是3D不是4D),而是用在所谓透视除法(Perspective Division)上。 为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的gl_Position变量,它在幕后是vec4类型的。在main函数中只是将position的值转换后赋值给gl_Position。 由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f(我们会在后面解释为什么)来完成这一任务。 -这个position的值是哪里进行赋值的呢? 是通过后面java代码中的draw函数来进行赋值的。每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 +这个position的值是哪里进行赋值的呢? +是通过后面java代码中的draw函数来进行赋值的。 +每个顶点着色器都必须在gl_Position变量中输出一个位置。这个变量定义传递给管线下一个阶段的位置。 -- 编译着色器 +### 3.编译着色器 写完顶点着色器后,为了能让OpenGL使用它,我们必须在运行时动态编译它。 -- 片段着色器 +### 4.片段着色器 - 片断着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。 + 片段着色器全是关于计算你的像素最后的颜色输出。颜色使用RGBA。 ```glsl #version 300 es @@ -73,21 +81,24 @@ float vertices[] = { color = vec4(1.0f, 0.5f, 0.2f, 1.0f); } ``` -在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。 + +在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分量,通常缩写为RGBA。 当在OpenGL或GLSL中定义一个颜色的时候,我们把颜色每个分量的强度设置在0.0到1.0之间。比如说我们设置红为1.0f,绿为1.0f,我们会得到两个颜色的混合色,即黄色。 这三种颜色分量的不同调配可以生成超过1600万种不同的颜色! 片段着色器只需要一个输出变量,这个变量是一个4分量向量,它表示的是最终的输出颜色,我们应该自己将其计算出来。 -我们可以用out关键字声明输出变量,这里我们命名为color。下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。 +我们可以用out关键字声明输出变量,这里我们命名为color。 +下面,我们将一个alpha值为1.0(1.0代表完全不透明)的橘黄色的vec4赋值给颜色输出。 之后也是需要编译着色器。片段着色器声明的这个输出变量color的值会被输出到颜色缓冲区,然后颜色缓冲区再通过EGL窗口显示。 -- 着色器程序(Shader Program Object) +### 5.着色器程序(Shader Program Object) 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本。 如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序。 已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。 -当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: +当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。 +当输出和输入不匹配的时候,你会得到一个连接错误。我们需要把之前编译的着色器附加到程序队形上,然后使用glLinkProgram链接他们: ```java final int shaderProgramId = GLES30.glCreateProgram(); @@ -96,7 +107,8 @@ GLES30.glAttachShader(shaderProgramId, fragmentShaderId); GLES30.glLinkProgram(shaderProgramId); ``` -链接完后需要使用glUseProgram方法,用刚创建的程序对象的Id作为参数,以激活这个程序对象。调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 +链接完后需要使用glUseProgram方法,用刚创建的程序对象的id作为参数,以激活这个程序对象。 +调用glUserProgram方法后,所有后续的渲染将用连接到这个程序对象的顶点和片段着色器进行。 对了,在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了: @@ -108,14 +120,15 @@ GLES30.glDeleteShader(fragmentShaderId); 现在,我们已经把输入顶点数据发送给了GPU,并指示了GPU如何在顶点和片段着色器中处理它。 就快要完成了,但还没结束,OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到顶点着色器的属性上。下面我们需要告诉OpenGL怎么做。 -- 链接顶点属性 +### 6.链接顶点属性 - 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。 + 顶点着色器允许我们指定任何以顶点属性为形式的输入。这使其具有很强的灵活性的同时,它还意味着我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性。 所以,我们必须在渲染前指定OpenGL该如何解释顶点数据。 我们的顶点缓冲数据会被解析成下面的样子: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer.png) + - 位置数据被储存为32位(4字节)浮点值。 - 每个位置包含3个这样的值。 - 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列(Tightly Packed)。 @@ -123,15 +136,15 @@ GLES30.glDeleteShader(fragmentShaderId); 有了这些信息我们就可以使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)了: - ```java +```java // glVertexAttribPointer是把顶点位置属性赋值给着色器程序 // 第一个参数0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 // 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 // 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer); -//启用顶点变量,参数为数据中第一个值在缓冲开始的位置。 这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 +//启用顶点变量,参数为数据中第一个值在缓冲区开始的位置。 这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 GLES30.glEnableVertexAttribArray(0); - ``` +``` glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们: @@ -144,12 +157,13 @@ GLES30.glEnableVertexAttribArray(0); 一旦我们有更多的顶点属性,我们就必须更小心地定义每个顶点属性之间的间隔,我们在后面会看到更多的例子(译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节)。 - 最后一个参数的类型是数据。 -每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。 +每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。 由于在调用glVertexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性现在会链接到它的顶点数据。 -现在我们已经定义了OpenGL该如何解释顶点数据,接着应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性;顶点属性默认是禁用的。 +现在我们已经定义了OpenGL该如何解释顶点数据,接着应该使用glEnableVertexAttribArray,以顶点属性位置值作为参数,启用顶点属性; +**顶点属性默认是禁用的。** -自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。 +自此,所有东西都已经设置好了:我们使用一个顶点缓冲对象将顶点数据初始化至缓冲区中,建立了一个顶点和一个片段着色器,并告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。 在OpenGL中绘制一个物体,代码会像是这样: ```java @@ -197,6 +211,7 @@ GLES30.glGenVertexArrays(1, arrays); 当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: VAO小助理偷偷帮我们记录一切: + ```java // ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: .. // 1. 绑定VAO @@ -275,7 +290,9 @@ void main() { } ``` - 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition,然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor,另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出给vColor。 + 大体意思:使用OpenGL ES3.0版本,将图形顶点数据采用4分向量的数据结构绑定到着色器的第0个属性上,属性的名字是vPosition。 + 然后再有一个颜色的4分向量绑定到做色器的第1个属性上,属性的名字是aColor。 + 另外还会输出一个vColor,着色器执行的时候,会将vPosition的值传递给用来表示顶点最终位置的内建变量gl_Position,将顶点最终大小的gl_PointSize设置为10,并将aColor的值复制给要输出给vColor。 - 片段着色器(triangle_fragment_shader.glsl) @@ -293,8 +310,10 @@ void main() { fragColor = vColor; } ``` -precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。 -在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 +precision是精度限定符,它可以使着色器创作者指定着色器变量的计算精度。变量可以声明为低、中或高精度。 +这些限定符用于提示编译器允许在较低的范围和精度上执行变量的计算。 +在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。 +当然这种效率提升是以精度为代价的,在没有正确使用精度限定符时可能造成伪像。 精度限定符可以用于指定任何浮点数或整数的变量的精度。指定精度的关键字是lowp、mediump和highp。下面是一些带有精度限定符的声明示例: @@ -308,8 +327,9 @@ mediump float specularExp; precision highp float; precision mediump int; ``` -为float类型指定的精度将用作所有基于浮点值变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。 -在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。 +为float类型指定的精度将用作所有基于浮点值变量的默认精度。同样,为int指定的精度将用作所有基于整数的变量的默认精度。 +在顶点着色器中,如果没有指定默认精度,则int和float的默认精度都是highp。 +也就是说,顶点着色器中所有没有精度限定符声明的变量都使用最高的精度。 片段着色器的规则与此不同。在片段着色器中,浮点值没有默认的精度值:每个着色器必须声明一个默认的float精度,或者为每个float变量指定精度。 @@ -611,15 +631,18 @@ public class TriangleRender implements GLSurfaceView.Renderer { ### 向量(Vector) -具有大小和方向的量。它可以形象化的表示为带箭头的线段。箭头代表方向、长度代表大小。在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是4D),而是用在所谓透视划分上。 +具有大小和方向的量。它可以形象化的表示为带箭头的线段。箭头代表方向、长度代表大小。 +在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。 +vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是4D),而是用在所谓透视划分上。 ### 矩阵 -由m*n个数按照一定顺序排列成m行n列的矩形数表称为矩阵,而向量则是由n个有序数组成的数组。 +由m*n个数按照一定顺序排列成m行n列的矩形数表称为矩阵,而向量则是由n个有序数组成的数组。 -所以矩阵中的每一个行可以看做一个行向量,每一列也可以看成一个列向量,所以说向量是矩阵的一部分。 +所以矩阵中的每一个行可以看做一个行向量,每一列也可以看成一个列向量,所以说向量是矩阵的一部分。 -在三维图形学中,一般使用的是4阶矩阵。在DirectX中使用的是行向量,如[xyzw],所以与矩阵相乘时,向量在前矩阵在后。OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,最终通过变换矩阵得到想要的向量。 +在三维图形学中,一般使用的是4阶矩阵。在DirectX中使用的是行向量,如[xyzw],所以与矩阵相乘时,向量在前矩阵在后。 +OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,最终通过变换矩阵得到想要的向量。 @@ -732,7 +755,8 @@ Android OpenGL ES的投影分为两种: ### 变换矩阵 -在OpenGL ES中顶点位置信息的表示都是使用的向量,如下每一行是一个顶点的位置向量(x,y,z)。想要使三角形显示为等腰三角形,就需要在虚拟坐标系中完成对各个顶点位置的变换,也就是对三个向量的变换。而想要实现对向量的变换就要用到变换矩阵。 +在OpenGL ES中顶点位置信息的表示都是使用的向量,如下每一行是一个顶点的位置向量(x,y,z)。 +想要使三角形显示为等腰三角形,就需要在虚拟坐标系中完成对各个顶点位置的变换,也就是对三个向量的变换。而想要实现对向量的变换就要用到变换矩阵。 ```java //三个顶点的位置参数 @@ -756,7 +780,7 @@ Matrix.multiplyMM (float[] result, //接收相乘结果 -也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。 +也就是说为了解决坐标中宽高不一样的问题,我们可以应用OpenGL正确的比例下通过投影模式和相机视图坐标转换图形对象来完成。 为了应用投影和相机视图,我们创建一个投影矩阵和一个相机视图矩阵,并把他们应用于OpenGL渲染管道中,投影矩阵重新计算你的图形的坐标,使他们正确的映射到Android设备的屏幕,相机视图矩阵创建一个转换,它将从一个特定的位置显示对象。 ### 绘制等腰三角形 diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index c73d718e..f9319879 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -152,7 +152,8 @@ public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Rendere - 索引法 - 根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,可读性高。 + 根据索引序列,在顶点序列中找到对应的顶点,并根据绘制的方式,组成相应的图元绘制,用的是GLES30.glDrawElements(),称为索引法。 + 相对于顶点法在复杂图形的绘制中无法避免大量顶点重复的情况,索引法可以减少很多重复顶点占用的空间,所以复杂的图形下推荐使用索引法。顶点复用情况多,可读性高。 之前说过OpenGL ES提供的的图元单位是三角形,想要绘制其他多边形,就要利用三角形来拼成。 矩形是两个三角形,而圆形则是由很多个三角形组成,个数越多,圆越圆。 @@ -208,7 +209,7 @@ int indices[] = { ```java //创建 ebo -GLES30.glGenBuffers(1,ebo,0) +GLES30.glGenBuffers(1,ebo[0]) //绑定 ebo 到上下文 GLES30.glBindBuffer(GLES30.GL_ELEMENT_ARRAY_BUFFER, ebo[0]) //设置数据 @@ -219,22 +220,32 @@ GLES30.glBufferData(GLES30.GL_ELEMENT_ARRAY_BUFFER, ) ``` 与VBO类似,我们先绑定EBO然后用glBufferData把索引复制到缓冲里。 -同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 -注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制: +同样,和VBO类似,我们会把这些函数调用放在绑定和解绑函数调用之间,只不过这次我们把缓冲的类型定义为GL_ELEMENT_ARRAY_BUFFER。 +注意:我们传递了GL_ELEMENT_ARRAY_BUFFER当作缓冲目标。最后一件要做的事是用glDrawElements来替换glDrawArrays函数,表示我们要从索引缓冲区渲染三角形。 +使用glDrawElements时,我们会使用当前绑定的索引缓冲对象中的索引进行绘制: ```java -GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP,6,GLES30.GL_UNSIGNED_INT,0) +GLES30.glDrawElements(GLES30.GL_TRIANGLE_STRIP, 6,GLES30.GL_UNSIGNED_INT, 0) ``` -第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型,这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。 -glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。碰巧顶点数组对象也跟踪元素缓冲区对象绑定。在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO。 +- 第一个参数指定了我们绘制的模式,这个和glDrawArrays的一样。 +- 第二个参数是我们打算绘制顶点的个数,这里填6,也就是说我们一共需要绘制6个顶点。 +- 第三个参数是索引的类型,这里是GL_UNSIGNED_INT。 +- 最后一个参数里我们可以指定EBO中的偏移量(或者传递一个索引数组,但是这是当你不在使用索引缓冲对象的时候),但是我们会在这里填写0。 + +glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。 +这意味着我们每次想要使用索引渲染对象时都必须绑定相应的EBO,这又有点麻烦。 +碰巧顶点数组对象也跟踪元素缓冲区对象绑定。在绑定VAO时,绑定的最后一个元素缓冲区对象存储为VAO的元素缓冲区对象。然后,绑定到VAO也会自动绑定该EBO。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_array_objects_ebo.png?raw=true) -**注意: EBO的绑定一定要在VAO解绑之前绑定!** -**当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。** +**注意: EBO的绑定一定要在VAO解绑之前绑定!** +**当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个EBO配置了。** **OpenGL中会解绑VBO和VAO但是不能解绑EBO)** +到这里我有点懵了,VAO有什么用? + +简单说就是VBO用于存储顶点数据,而VAO用于封装顶点数组的状态,包括顶点属性指针和VBO的绑定状态。这两者的结合使用可以使OpenGL中管理和切换多个顶点数据配置变得更加方便和高效。 ### 绘制矩形 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 90c75bb4..6e1414d8 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -9,22 +9,6 @@ GLSL中包含C等其它语言大部分的默认基础数据类型:int、float、double、uint和bool。 GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix)。 -向量(Vector): -- vecn : the default vector of n floats -- bvecn : a vector of n booleans -- ivecn : a vector of n integers -- uvecn : a vector of u unsigned integers -- dvecn : a vector of n double components - -与C语言有区别的地方: -- 共享命名空间(Shared Namespace) - Shaders操作是相互独立的,但是为了方便Shaders间通信,在链接成一个shader program时会共享变量的名字。 -- GLSL没有char、char *和string数据类型,也没有字符串操作 -- 不支持隐式类型转换,例如: int(arg) -- vec数据方位: vec4(r,g,b,a / x,y,z,w / s,t,p,q)。 vec3.xy = vec2 -- 新的变量类型: Attribute、Uniform、Varying - - ### 顶点着色器: @@ -79,13 +63,17 @@ OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的 - `#version 300 es`这种声明版本的语句,必须放到第一行,并且shader中不能有Tab键,只能用空格替换。 -虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in和out关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。 +虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。 +GLSL定义了in和out关键字专门来实现这个目的。 +**每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。** +但在顶点和片段着色器中会有点不同。 #### in、out + 顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。 顶点着色器的输入特殊在,它从顶点数据中直接接收输入。 为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。 @@ -96,7 +84,8 @@ OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的 如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。 **当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。** -顶点着色器 +顶点着色器: + ```glsl #version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0 @@ -109,7 +98,9 @@ void main() vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色 } ``` -片段着色器 + +片段着色器: + ```glsl #version 330 core out vec4 FragColor; @@ -121,7 +112,10 @@ void main() FragColor = vertexColor; } ``` -你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。 + +你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。 +由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。 +由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。 #### 精度限定符 @@ -148,19 +142,20 @@ precision mediump int; #### Uniform Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。 -首先,uniform是全局的(Global)。 +首先,uniform是全局的(Global)。 全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。 第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 统一变量通常保存在硬件中,这个区域被称作“常量存储”,是硬件中为存储常量值而分配的特殊空间。 -因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。 +因为常量存储的大小一般是固定的,所以程序中可以使用的统一变量数量受到限制。 这种限制可以通过读取内建变量gl_MaxVertexUniformVectors和gl_MaxFragmentUniformVectors的值来确定。 -但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。 +但是对于在编译时已知值的变量应该是常量,而不是统一变量,这样可以提高效率。 + +我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: -我们可以在一个着色器中添加uniform关键字至类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。我们来看看这次是否能通过uniform设置三角形的颜色: ```glsl #version 330 core out vec4 FragColor; @@ -172,7 +167,10 @@ void main() FragColor = ourColor; } ``` -我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 + +我们在片段着色器中声明了一个uniform vec4的ourColor,并把片段着色器的输出颜色设置为uniform值的内容。 +因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。 +顶点着色器中不需要这个uniform,所以我们不用在那里定义它。 **如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点!** @@ -187,9 +185,9 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); ``` 首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。 -接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。 -如果glGetUniformLocation返回-1就代表没有找到这个位置值。 -最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 +接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。 +如果glGetUniformLocation返回-1就代表没有找到这个位置值。 +最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。 因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有: @@ -199,23 +197,31 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); - 3f: 函数需要3个float作为它的值 - fv: 函数需要一个float向量/数组作为它的值 -每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。 +每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。 -现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 +现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 ### GLSL的特点 -OpenGLES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。GLSL主要包含以下特性: +OpenGLES的着色器语言GLSL是一种高级的图形化编程语言,其源自应用广泛的C语言。与传统的C语言不同的是,它提供了更加丰富的针对于图像处理的原生类型,诸如向量、矩阵之类。GLSL主要包含以下特性: -- GLSL是一种面向过程的语言,和c相同 +- GLSL是一种面向过程的语言,和c相同 - GLSL的基本语法与C/C++相同 - 它完美的支持向量和矩阵的操作 - 它是通过限定符操作来管理输入输出类型的 - GLSL提供了大量的内置函数来提供丰富的扩展功能 +与C语言有区别的地方: + +- 共享命名空间(Shared Namespace) + Shaders操作是相互独立的,但是为了方便Shaders间通信,在链接成一个shader program时会共享变量的名字。 +- GLSL没有char、char *和string数据类型,也没有字符串操作 +- 不支持隐式类型转换,例如: int(arg) +- vec数据方位: vec4(r,g,b,a / x,y,z,w / s,t,p,q)。 vec3.xy = vec2 +- 新的变量类型: Attribute、Uniform、Varying ### 基本数据类型 @@ -223,12 +229,19 @@ GLSL中的数据类型主要分为标量、向量、矩阵、采样器、结构 - 标量 -标量表示的是只有大小没有方向的量,在GLSL中`标量只有bool、int和float三种`。对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。对于标量的运算,我们最需要注意的是`精度`,防止溢出问题。 +标量表示的是只有大小没有方向的量,在GLSL中`标量只有bool、int和float三种`。 +对于int,和C一样,可以写为十进制(16)、八进制(020)或者十六进制(0x10)。 +对于标量的运算,我们最需要注意的是`精度`,防止溢出问题。 - 向量 -向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四维向量。 -针对存储的标量类型,可以分为bool、int和float。共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。***需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)***。向量在GPU中由硬件支持运算,比CPU快的多。 +向量我们可以看做是数组,在GLSL通常用于储存颜色、坐标等数据,针对维数,可分为二维、三维和四维向量。 +针对存储的标量类型,可以分为bool、int和float。 +共有vec2、vec3、vec4,ivec2、ivec3、ivec4、bvec2、bvec3和bvec4九种类型,数组代表维数、i表示int类型、b表示bool类型。 + +***需要注意的是,GLSL中的向量表示竖向量,所以与矩阵相乘进行变换时,矩阵在前,向量在后(与DirectX正好相反)*** + +向量在GPU中由硬件支持运算,比CPU快的多。 作为颜色向量时,用rgba表示分量,就如同取数组的中具体数据的索引值。三维颜色向量就用rgb表示分量。比如对于颜色向量vec4 color,color[0]和color.r都表示color向量的第一个值,也就是红色的分量。其他相同。 @@ -245,7 +258,8 @@ vec4 result = vec4(vect, 0.0, 0.0); - 矩阵 -在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 +在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。 +我们可以把矩阵看做是一个二维数组,也可以用二维数组下表的方式取里面具体位置的值。 现在我们已经讨论了向量的全部内容,是时候看看矩阵了!简单来说矩阵就是一个矩形的数字、符号或表达式数组。矩阵中每一项叫做矩阵的元素(Element)。下面是一个2×3矩阵的例子: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/juzhen_0.png?raw=true) @@ -315,7 +329,7 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl 在着色器中有一些特殊的变量,不用声明也可以使用,这些变量叫做内建变量。 他们大致可以分为两种,一种是input类型,负责向硬件(渲染管线)发送数据;另一种是output类型,负责向程序回传数据,以便编程时需要。内建变量相当于着色器硬件的输入和输出点,使用者利用这些输入点输入之后,就会看到屏幕上的输出。通过输出点可以知道输出的某些数据内容。 - #### 顶点着色器的内建变量 +#### 顶点着色器的内建变量 - 输入变量 - gl_Position:顶点坐标信息 @@ -383,7 +397,9 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl ### 更多 -在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色: +在前面的教程中,我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。 +这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色: + ```java float vertices[] = { // 位置 // 颜色 @@ -392,7 +408,9 @@ float vertices[] = { 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 }; ``` -由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把aColor属性的位置值设置为1: +由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。 +需要注意的是我们用layout标识符来把aColor属性的位置值设置为1: + ```glsl #version 330 core layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为 0 @@ -406,7 +424,8 @@ void main() ourColor = aColor; // 将ourColor设置为我们从顶点数据那里得到的输入颜色 } ``` -由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器: +由于我们不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,我们必须再修改一下片段着色器: + ```glsl #version 330 core out vec4 FragColor; @@ -421,7 +440,8 @@ void main() ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/vertex_attribute_pointer_interleaved.png?raw=true) -知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式, +知道了现在使用的布局,我们就可以使用glVertexAttribPointer函数更新顶点格式: + ```java // 位置属性 GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 0); @@ -431,10 +451,10 @@ GLES30.glVertexAttribPointer(1, 3, GLES30.GL_FLOAT, false, 6 * Float.BYTES, 3* F // 1是对应上面着色器代码中的位置: layout (location = 1) in vec3 aColor; 颜色变量的属性位置值为 1 GLES30.glEnableVertexAttribArray(1); ``` -glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。 -由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。 -这使我们的步长值为6乘以float的字节数(=24字节)。 -同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是Float.BYTES,用字节来计算就是12字节。 +glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。 +由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。 +这使我们的步长值为6乘以float的字节数(=24字节)。 +同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是Float.BYTES,用字节来计算就是12字节。 diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 2de853ff..01eb63e1 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -134,7 +134,9 @@ GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); ### 启用或禁用顶点属性数组 -调用GLES30.glEnableVertexAttribArray和GLES30.glDisableVertexAttribArray传入参数index。如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。 +调用GLES30.glEnableVertexAttribArray和GLES30.glDisableVertexAttribArray传入参数index。 + +如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。 ## Matrix @@ -172,12 +174,16 @@ Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都 顶点着色器可赋予程序员一次操作一个顶点(“按顶点”处理)的能力,片段着色器(稍后会看到)可赋予程序员一次操作一个像素(“按片段”处理)的能力,几何着色器可赋予程序员一次操作一个图元(“按图元”处理)的能力。 -到目前为止,我们所接触的变换矩阵全都可以在3D空间中操作。但是,我们最终需要将3D空间或它的一部分展示在2D显示器上。为了达成这个目标,我们需要找到一个有利点。正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。 +到目前为止,我们所接触的变换矩阵全都可以在3D空间中操作。但是,我们最终需要将3D空间或它的一部分展示在2D显示器上。 + +为了达成这个目标,我们需要找到一个有利点。 +正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。 + +观察3D世界需要: -观察3D世界需要: - 将相机放入世界的某个位置; - 调整相机的角度,通常需要一套它自己的直角坐标轴u、v、n(由向量U,V,N构成); -- 定义一个视体(view volume);(d)将视体内的对象投影到投影平面(projection plane)上。 +- 定义一个视体(view volume);将视体内的对象投影到投影平面(projection plane)上。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye.jpg?raw=true) @@ -185,17 +191,18 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_eye_00.jpg?raw=true) -为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置。 +为了应用OpenGL相机,我们需要将它移动到适合的位置和方向。我们需要先找出在世界中的物体与我们期望的相机位置的相对位置。 给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。 当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。 -透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 +透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 在正射投影中,平行线仍然是平行的,即不使用透视,如下图所示。正射与透视相反,在视体中的物体不因其与相机的距离而改变,可以直接投影。正射投影是一种平行投影,其中所有的投影过程都沿与投影平面垂直的方向进行。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_zheng.jpg?raw=true) -我们最后要学习的变换矩阵是LookAt矩阵。当你想要把相机放在某处并看向一个特定的位置时,就需要用到它了,LookAt矩阵的元素如图3.19所示。当然,用我们已经学到的方法也可以实现LookAt变换,但是这个操作非常频繁,因此为它专门构建一个矩阵通常比较有用。 +我们最后要学习的变换矩阵是LookAt矩阵。当你想要把相机放在某处并看向一个特定的位置时,就需要用到它了,LookAt矩阵的元素如下图所示。 +当然,用我们已经学到的方法也可以实现LookAt变换,但是这个操作非常频繁,因此为它专门构建一个矩阵通常比较有用。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_lookat.jpg?raw=true) - invertM diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index e86877ad..3f3fb88b 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -11,7 +11,9 @@ - 纹理目标:一个纹理单元中包含了多个类型的纹理目标,有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,将纹理ID绑定到纹理单元0的GL_TEXTURE_2D纹理目标上,之后对纹理目标的操作都是对纹理Id对应的数据进行操作。 - OpenGL要操作一个纹理,那么是将纹理ID装进纹理单元这个容器里,然后再通过操作纹理单元的方式去实现的。这样的话,我们可以加载出很多很多个纹理ID(但要注意爆内存问题),但只有16个纹理单元,在Fragment Shader里最多同时能操作16个单元。 +OpenGL要操作一个纹理,那么是将纹理ID装进纹理单元这个容器里,然后再通过操作纹理单元的方式去实现的。 + +这样的话,我们可以加载出很多很多个纹理ID(但要注意爆内存问题),但只有16个纹理单元,在Fragment Shader里最多同时能操作16个单元。 @@ -23,7 +25,9 @@ ### 纹理坐标 -OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1)内,两个维度分别为S、T,所以一般称为ST纹理坐标。有些时候也叫UV坐标。纹理左边的方向性和Android上的视图坐标系一致,都是顶点在左上角,范围为0到1之间。 +OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1)内,两个维度分别为S、T,所以一般称为ST纹理坐标。 +有些时候也叫UV坐标。 +纹理左边的方向性和Android上的视图坐标系一致,都是顶点在左上角,范围为0到1之间。 纹理上的每个顶点与定点坐标上的顶点一一对应。如下图,左边是顶点坐标,右边是纹理坐标,只要两个坐标的ABCD定义顺序一致,就可以正常地映射出对应的图形。顶点坐标内光栅化后的每个片段,都会在纹理坐标内取得对应的颜色值。 @@ -31,16 +35,16 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_texture_position.jpg) -在 OpenGL 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 使用的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。 +在OpenGL中,纹理实际上是一个可以被采样的复杂数据集合,是GPU使用的图像数据结构,纹理分为2D纹理、立方图纹理和3D纹理。 -2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 +2D纹理是OpenGLES中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 -立方图纹理是一个由 6 个单独的纹理面组成的纹理。立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标。 +立方图纹理是一个由6个单独的纹理面组成的纹理。立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标。 -3D 纹理可以看作 2D 纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。 +3D纹理可以看作2D纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。 -在 OpenGLES 中,纹理映射就是通过为图元的顶点坐标指定恰当的纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上。 +在OpenGLES中,纹理映射就是通过为图元的顶点坐标指定恰当的纹理坐标,通过纹理坐标在纹理图中选定特定的纹理区域,最后通过纹理坐标与顶点的映射关系,将选定的纹理区域映射到指定图元上。 纹理映射也称为纹理贴图,简单地说就是将纹理坐标(纹理坐标系)所指定的纹理区域,映射到顶点坐标(渲染坐标系或OpenGLES 坐标系)对应的区域。 @@ -48,14 +52,13 @@ OpenGL中,2D纹理也有自己的坐标体系,取值范围在(0,0)到(1,1) ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/texture_st_1.png) 渲染坐标系或OpenGLES 坐标系: ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/texture_st_2.png) -4 个纹理坐标分别为 -T0(0,0),T1(0,1),T2(1,1),T3(1,0)。 -4 个纹理坐标对于的顶点坐标分别为 -V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) -由于 OpenGLES 绘制是以三角形为单位的,设置绘制的 2 个三角形为 V0V1V2 和 V0V2V3 。 -当我们调整纹理坐标的顺序顶点坐标顺序不变,如 T0T1T2T3 -> T1T2T3T0 ,绘制后将得到一个顺时针旋转 90 度的纹理贴图。所以调整纹理坐标和顶点坐标的对应关系可以实现纹理图简单的旋转。 - - +4个纹理坐标分别为: +T0(0,0),T1(0,1),T2(1,1),T3(1,0) +4个纹理坐标对于的顶点坐标分别为: +V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) +由于OpenGLES绘制是以三角形为单位的,设置绘制的2个三角形为V0V1V2和V0V2V3。 +当我们调整纹理坐标的顺序顶点坐标顺序不变,如T0T1T2T3 -> T1T2T3T0,绘制后将得到一个顺时针旋转90度的纹理贴图。 +所以调整纹理坐标和顶点坐标的对应关系可以实现纹理图简单的旋转。 @@ -63,15 +66,18 @@ V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) ### 文件读取 -OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmap。我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 +OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加载原始数据,也就是bitmap。 +我们在内置图片到工程中,应该将图片放到drawable-nodpi中,避免读取时被压缩,通过BitmapFactory解码读取图片时,要设置为非缩放的方式,即options.isScaled=false。 ### 纹理过滤 -当我们通过光栅化将图形处理成一个个小片段的时候,再将纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 +当我们通过光栅化将图形处理成一个个小片段的时候,再将纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。 +这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 ### 纹理对象和纹理的加载 -纹理应用的第一步是创建一个纹理对象。纹理对象是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式和包装模式。在OpenGL ES中,纹理对象用一个无符号整数表示,该整数是纹理对象的一个句柄,用于生成纹理对象的函数是glGenTextures。 +纹理应用的第一步是创建一个纹理对象。纹理对象是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式和包装模式。 +在OpenGL ES中,纹理对象用一个无符号整数表示,该整数是纹理对象的一个句柄,用于生成纹理对象的函数是glGenTextures。 - glGenTextures(GLsizei n, GLunit *textures) @@ -93,7 +99,7 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 下面是一个工具类方法,相对通用,能解决大部分需求,这个方法可以将内置的图片资源加载出对应的纹理ID。 - ```java +```java public class TextureUtil { private static final String TAG = "TextureHelper"; @@ -247,9 +253,12 @@ public class TextureUtil { - 片段着色器 -片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢?GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。 +片段着色器也应该能访问纹理对象,但是我们怎样能把纹理对象传给片段着色器呢? +GLSL有一个供纹理对象使用的内建数据类型,叫做采样器(Sampler),它以纹理类型作为后缀,比如sampler1D、sampler3D,或在我们的例子中的sampler2D。 +我们可以简单声明一个uniform sampler2D把一个纹理添加到片段着色器中,稍后我们会把纹理赋值给这个uniform。 -我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。 +我们使用GLSL内建的texture函数来采样纹理的颜色,它第一个参数是纹理采样器,第二个参数是对应的纹理坐标。 +texture函数会使用之前设置的纹理参数对相应的颜色值进行采样。这个片段着色器的输出就是纹理的(插值)纹理坐标上的(过滤后的)颜色。 之前直接输出顶点着色器来的颜色,现在变为经过纹理处理最终成为输出颜色。 @@ -285,11 +294,12 @@ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(aTextureLocation, 0); ``` -这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 +这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。 +这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。 +通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 - - ```java +```java public class TextureRender extends BaseGLSurfaceViewRenderer { private final FloatBuffer vertextBuffer; private final FloatBuffer textureBuffer; @@ -367,17 +377,18 @@ GLES20.glUniform1i(aTextureLocation, 0); glDisableVertexAttribArray(aTextureLocation); } } - ``` +``` ### 多纹理绘制 - - 单纹理单元,多次绘制 -多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别设置这两个坐标,并绑定两次纹理,然后调用两次glDrawArrays进行绘制。 +多次调用glDrawArrays绘制纹理顶点的方式来实现,这样就是一张一张的按先后顺序,一层一层的绘制到当前的一帧画面。 + +着色器与上面的完全一致,唯一不同的是要提供两个顶点位置的坐标,然后分别设置这两个坐标,并绑定两次纹理,然后调用两次glDrawArrays进行绘制。 ```java public class MultiTextureRender extends BaseGLSurfaceViewRenderer { From d3d4dc1c2dd6b5c00f59db66dc5a59fc43bc78dc Mon Sep 17 00:00:00 2001 From: Charon Date: Thu, 30 Nov 2023 14:58:38 +0800 Subject: [PATCH 066/128] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 01ed8394..5f20491c 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,11 @@ Android学习笔记 目录 === -- [史上最适合Android开发者学习的Go语言教程](https://github.com/CharonChui/GolangStudyNote) -- [史上最适合Android开发者学习的iOS开发教程](https://github.com/CharonChui/iOSStudyNote) +- [史上最适合Android开发者学习的Harmony OS Next语言教程](https://github.com/CharonChui/HarmonyOSNextStudyNote) + - [源码解析][43] - [自定义View详解][1] - - [Activity界面绘制过程详解][2] + - [Activity界面制过程详解][2] - [Activity启动过程][3] - [Android Touch事件分发详解][4] - [AsyncTask详解][5] From 4cfccf0403cb3194c43f7021d1f94af19db028a4 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 26 Jan 2024 16:10:08 +0800 Subject: [PATCH 067/128] update android note --- AdavancedPart/AOP.md | 14 ++++++++++++++ "Gradle&Maven/Gradle\344\270\223\351\242\230.md" | 10 ++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 AdavancedPart/AOP.md diff --git a/AdavancedPart/AOP.md b/AdavancedPart/AOP.md new file mode 100644 index 00000000..3bcc633b --- /dev/null +++ b/AdavancedPart/AOP.md @@ -0,0 +1,14 @@ +AOP +--- + + +AOP(Aspect Oriented Programing),面向切面编程。 +是OOP(Object Oriented Programing)面向对象编程的延续。 + +在OOP思想中,我们会把问题划分为各个模块,如语言、表情等。 +在划分这些模块的过程中,也会出现一些共同特征(如埋点)。它的逻辑被分散到了各个模块,导致了代码复杂度提高,可复用性降低。 + +而AOP,就是将各个模块中的通用逻辑抽离出来。 +我们将这些逻辑视为Aspect(切面),然后动态地把代码插入到类的指定方法、指定位置中。 + + diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 79e9314b..7f32fd81 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -9,7 +9,13 @@ Gradle专题 作用 --- -首先我们要知道它有什么用? Gradle是一个工具,同时它也是一个编程框架。使用这个工具可以完成app的编译打包等工作。 +Gradle是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: +``` + +``` + + +使用这个工具可以完成app的编译打包等工作。 简介 @@ -1712,4 +1718,4 @@ pizzaPrices.pepperoni --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From 252a6aadf3477aa7cde99601cdd6c5559236a482 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 26 Jan 2024 19:58:21 +0800 Subject: [PATCH 068/128] update gradle part --- .../Gradle\344\270\223\351\242\230.md" | 74 +++++++++++++++---- 1 file changed, 58 insertions(+), 16 deletions(-) diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 7f32fd81..5968cbbe 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -1,31 +1,27 @@ Gradle专题 === -随着`Google`对`Eclipse`的无情抛弃以及`Studio`的不断壮大,`Android`开发者逐渐拜倒在`Studio`的石榴裙下。 -而作为`Studio`的默认编译方式,`Gradle`已逐渐普及。我最开始是被它的多渠道打包所吸引。关于多渠道打包,请看之前我写的文章[AndroidStudio使用教程(第七弹)][1] - -接下来我们就系统的学习一下`Gradle`。 - 作用 --- -Gradle是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: -``` +[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: +![image](https://github.com/CharonChui/Pictures/blob/master/gradle_version.png?raw=true) -``` - - -使用这个工具可以完成app的编译打包等工作。 +当前Gradle版本为6.7.1。当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 简介 --- -[Gradle](https://gradle.org/releases/)是以`Groovy`语言为基础,面向`Java`应用为主。基于`DSL(Domain Specific Language)`语法的自动化构建工具。 +[Gradle](https://gradle.org/releases/)是以`Groovy`语言为基础,面向`Java`应用为主。 +基于`DSL(Domain Specific Language)`语法的自动化构建工具。 `Gradle`集合了`Ant`的灵活性和强大功能,同时也集合了`Maven`的依赖管理和约定,从而创造了一个更有效的构建方式。凭借`Groovy`的`DSL`和创新打包方式,`Gradle`提供了一个可声明的方式,并在合理默认值的基础上描述所有类型的构建。 `Gradle`目前已被选作许多开源项目的构建系统。 -[Groovy](http://www.groovy-lang.org/api.html)基于Java并拓展了Java。 Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Javaclass然后启动虚拟机来执行。当然,这些底层的渣活不需要你管。*实际上,由于Groovy Code在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码*。 +[Groovy](http://www.groovy-lang.org/api.html)基于Java并拓展了Java。 +Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Javaclass然后启动虚拟机来执行。 +当然,这些底层的渣活不需要你管。实际上,由于Groovy Code在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码。 + 因为`Gradle`是基于`DSL`语法的,如果想看到`build.gradle`文件中全部可以选项的配置,可以看这里 [DSL Reference](http://google.github.io/android-gradle-dsl/current/) @@ -34,15 +30,61 @@ Gradle是一个开源的自动化构建工具。现在Android项目构建编译 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_build_process.png?raw=true) +### Gradle的生命周期 + +1. Initialization:初始化阶段 + - 解析整个工程中所有的Project,构建所有Project对应的project对象。 + - 初始化阶段执行项目目录下的settings.gradle脚本,用于判断哪些项目需要被构建,并且为对应项目创建Project对象。 + +2. Configuration配置阶段 + - 解析所有的project对象中的Task,构建所有Task的括扑图 + - 配置阶段的任务是执行各module下的build.gradle脚本,从而完成Project的配置,并且构建Task任务依赖关系图以便在执行阶段按照依赖关系执行Task。 + - 这个阶段Gradle会拉取remote repo的依赖(如果本地之前没有下载过依赖的话) +3. Execution执行阶段 + 执行具体的task以及其依赖的task +4. Build Finished + + + + + Gradle与Android Studio的关系 --- Gradle跟Android Studio其实没有关系,但是Gradle官方还是很看重Android开发的,Google在推出AS的时候选中了Gradle作为构建工具,为了支持Gradle -能在AS上使用,Google做了个AS的插件叫Android Gradle Plugin,所以我们能在AS上使用Gradle完全是因为这个插件的原因。在项目的根目录有个build.gradle文件,里面有这么一句代码: +能在AS上使用,Google做了个AS的插件叫Android Gradle Plugin,所以我们能在AS上使用Gradle完全是因为这个插件的原因。 + +### AGP +AGP即Android Gradle Plugin,主要用于管理Android编译相关的Gradle插件集合,包括javac,kotlinc,aapt打包资源,D8/R8等都在AGP中。 + +AGP的版本是在根目录的build.gradle中配置的: +``` +dependencies { + classpath 'com.android.tools.build:gradle:4.2.1' +... +} +``` + +### AGP与Gradle的区别与联系 + +Gradle是构建工具,而AGP是管理Android构建的插件。可以理解为AGP是Gradle构建流程中重要的一环。 + +虽然AGP和Gradle不是一个纬度的事情,但是两者也在一定程度上有所关联:两者的版本号必须匹配上: https://developer.android.com/build/releases/gradle-plugin?hl=zh-cn#updating-gradle + + +![image](https://github.com/CharonChui/Pictures/blob/master/agp_gradle_version.png?raw=true) + + +既然Android编译是通过AGP实现的,AGP就是Gradle的插件,那么这个插件是什么时候被apply的内? +因为一个插件如果没有apply的话,那么压根不会执行的。 ``` -classpath 'com.android.tools.build:gradle:2.1.2' +plugins { + id 'com.android.application' + id 'kotlin-android' + id 'kotlin-kapt' +} ``` -这个就是依赖gradle插件的代码,后面的版本号代表的是android gradle plugin的版本,而不是Gradle的版本,这个是Google定的,跟Gradle官方没关系。 +这就是AGP被apply的地方,也是区分一个module究竟是被打包成app还是一个library的地址。 From 26bdff70b33f399e19c0c1bfb562b7a4a0a50252 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 9 Apr 2024 19:51:26 +0800 Subject: [PATCH 069/128] update --- AdavancedPart/AOP.md | 37 +++++++++ ...71\346\241\210\350\257\246\350\247\243.md" | 2 +- .../Gradle\344\270\223\351\242\230.md" | 7 +- ....8.Kotlin_\345\215\217\347\250\213.md.swp" | Bin 0 -> 65536 bytes ...&\347\261\273&\346\216\245\345\217\243.md" | 14 ++++ .../8.Kotlin_\345\215\217\347\250\213.md" | 78 +++++++++++------- README.md | 3 + .../ARouter\350\247\243\346\236\220.md" | 55 ++++++++++++ ...00\201MediaCodec\343\200\201MediaMuxer.md" | 35 ++++++++ .../MediaMetadataRetriever.md" | 33 ++++++++ 10 files changed, 230 insertions(+), 34 deletions(-) create mode 100644 "KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" create mode 100644 "SourceAnalysis/ARouter\350\247\243\346\236\220.md" create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" diff --git a/AdavancedPart/AOP.md b/AdavancedPart/AOP.md index 3bcc633b..53c518aa 100644 --- a/AdavancedPart/AOP.md +++ b/AdavancedPart/AOP.md @@ -11,4 +11,41 @@ AOP(Aspect Oriented Programing),面向切面编程。 而AOP,就是将各个模块中的通用逻辑抽离出来。 我们将这些逻辑视为Aspect(切面),然后动态地把代码插入到类的指定方法、指定位置中。 +一句话概括: 在运行时,动态的将代码切入到类的指定方法、指定位置上的编程思想就是面相切面的编程。 + + +一般而言,我们管切入到指定类指定方法的代码片段称为切面,而切入到哪些类、哪些方法则叫切入点。有了AOP,我们就可以把几个类共有的代码,抽取到一个切片中,等到需要时再切入对象中去,从而改变其原有的行为。 + + +### AOP的实现方式 + +#### 静态AOP + +在编译器,切面直接以字节码的形式编译到目标字节码文件中。 + +1. AspectJ +AspectJ属于静态AOP,它是在编译时进行增强,会在编译时期将AOP逻辑织入到代码中。 + +由于是在编译器织入,所以它的优点是不影响运行时性能,缺点是不够灵活。 + +2. AbstractProcessor +自定义一个AbstractProcessor,在编译期去解析编译的类,并且根据需求生成一个实现了特定接口的子类(代理类) + +#### 动态AOP +1. JDK动态代理 +通过实现InvocationHandler接口,可以实现对一个类的动态代理,通过动态代理可以生成代理类,从而在代理类方法中,在执行被代理类方法前后,添加自己的实现内容,从而实现AOP。 + +2. 动态字节码生成 +在运行期,目标类加载后,动态构建字节码文件生成目标类的子类,将切面逻辑加入到子类中,没有接口也可以织入,但扩展类的实例方法为final时,则无法进行织入。比如Cglib + +CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。通常可以使用Java的动态代理创建代理,但当要代理的类没有实现接口或者为了更好的性能,CGLIB是一个好的选择。 + +3. 自定义类加载器 +在运行期,目标加载前,将切面逻辑加到目标字节码里。如:Javassist + +Javassist是可以动态编辑Java字节码的类库。它可以在Java程序运行时定义一个新的类,并加载到JVM中;还可以在JVM加载时修改一个类文件。 + +4. ASM +ASM可以在编译期直接修改编译出的字节码文件,也可以像Javassit一样,在运行期,类文件加载前,去修改字节码。 + diff --git "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" index a64d0fe2..e268531a 100644 --- "a/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" +++ "b/AdavancedPart/\345\261\217\345\271\225\351\200\202\351\205\215\344\271\213\347\231\276\345\210\206\346\257\224\346\226\271\346\241\210\350\257\246\350\247\243.md" @@ -557,4 +557,4 @@ public void restoreLayoutParams(ViewGroup.LayoutParams params) { --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 5968cbbe..67820a04 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -4,12 +4,17 @@ Gradle专题 作用 --- -[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的,Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: +[Gradle](https://docs.gradle.org/7.3.3/userguide/what_is_gradle.html)是一个开源的自动化构建工具。现在Android项目构建编译都是通过Gradle进行的。 + +Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: ![image](https://github.com/CharonChui/Pictures/blob/master/gradle_version.png?raw=true) 当前Gradle版本为6.7.1。当我们执行assembleDebug/assembleRelease编译命令的时候,Gradle就会开始进行编译构建流程。 +gradle-wrapper是对Gradle的一层包装,便于在团队开发过程中统一Gradle构建的版本号,这样大家都可以使用统一的Gradle版本进行构建。 +里面的distributionUrl属性是用于配置Gradle发行版压缩包的下载地址。 + 简介 --- diff --git "a/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" "b/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" new file mode 100644 index 0000000000000000000000000000000000000000..996ab1630d5f280941394ce9a4cbef5c514922e6 GIT binary patch literal 65536 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 literal 0 HcmV?d00001 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 6128245e..5e1b19fa 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" @@ -461,6 +461,20 @@ private var lazyValue: Fragment? = null 当您稍后需要在代码中初始化var时,请选择lateinit,它将被重新分配。当您想要初始化一个val值一次时,特别是当初始化的计算量很大时,请选择by lazy。 +```kotlin +val name: String by lazy {getName()} +``` +这样,当第一次使用name引用时,getName()函数只会被调用一次。此外,还可以使用函数引用代替lambda表达式: +```kotlin +val name: String by lazy(::getName) + +fun getName() : String { + println("computing name") + return "Mockey" +} +``` + + ## 类的定义:使用`class`关键字 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 b1e79261..60f01257 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -7,15 +7,22 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 ## 起源 -协程是一个无优先级的子程序调用组件,允许子程序在特定的地方挂起恢复。线程包含于进程,协程包含于线程。只要内存足够, -一个线程中可以有任意多个协程,但某一时刻只能有一个协程在运行,多个协程分享该线程分配到的计算机资源。 - 线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程 是工作在线程之上的,协程的切换可以由程序自己来控制,不需要操作系统进行调度。这样的话就大大降低了开销。 -协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 +协程就像一个轻量级的线程在幕后,启动协程就像启动一个单独的执行线程。线程在其他语言中很常见,例如Java,协程和线程可以并行运行,并互相通信。然而,不同点在于使用协程比使用线程更加高效。在性能方面,启动一个线程并使其保持运行是非常昂贵的。 + +处理器通常只能同时运行有限数量的线程,并且运行尽可能少的线程会更高效。 + +而另一方面,协程默认运行在共享的线程池中,同一个线程可以运行多个协程。由于使用的线程较少,当你想要运行异步任务时,使用协程会更加高效。 + +**也就是说本质上Kotlin协程就是创建了一个可以复用的线程池,并且协程的delay是一个特殊的挂起函数,它不会造成线程堵塞,但是会挂起协程,并且只能在协程中使用。** + +协程是语言层面的东西,线程是系统层面的东西。 +协程就是一段代码块,既然是代码那就离不开CPU的执行,而CPU调度的基本单位是线程。 + ## 进程、线程、协程 进程:一段程序的执行过程,资源分配和调度的基本单位,有其独立地址空间,互相之间不发生干扰 @@ -33,7 +40,15 @@ CPU增加内存管理单元,进行虚拟地址和物理地址的转换 进程是一个实体,包括程序代码以及其相关资源(内存,I/O,文件等),可被操作系统调度。但想一边操作I/O进行输入输出,一边想进行加减计算,就得两个进程,这样写代码,内存就爆表了。于是又想着能否有一轻量级进程呢,只执行程序,不需要独立的内存,I/O等资源,而是共享已有资源,于是产生了线程。 -一个进程可以跑很多个线程处理并发,但是线程进行切换的时候,操作系统会产生中断,线程会切换到相应的内核态,并进行上下文的保存,这个过程不受上层控制,是操作系统进行管理。然而内核态线程会产生性能消耗,因此线程过多,并不一定提升程序执行的效率。正是由于1.线程的调度不能精确控制;2.线程的切换会产生性能消耗。协程出现了。 +一个进程可以跑很多个线程处理并发,但是线程进行切换的时候,操作系统会产生中断,线程会切换到相应的内核态,并进行上下文的保存,这个过程不受上层控制,是操作系统进行管理。 +然而内核态线程会产生性能消耗,因此线程过多,并不一定提升程序执行的效率。 + +正是由于: + +1. 线程的调度不能精确控制; +2. 线程的切换会产生性能消耗。 + +协程出现了。 协程: @@ -43,17 +58,30 @@ CPU增加内存管理单元,进行虚拟地址和物理地址的转换 4. 协程是非抢占式调度,当前协程切换到其他协程是由自己控制;线程则是时间片用完抢占时间片调度 优点: + 1. 用户态,语言级别 2. 无切换性能消耗 3. 非抢占式 4. 同步代码思维 5. 减少同步锁 + 缺点: + 1. 注意全局变量 2. 阻塞操作会导致整个线程被阻塞 -用一句话概括Kotlin Couroutine的特点即是"以同步之名,行异步之实". + +简单地讲,Kotlin 的协程就是一个封装在线程上面的线程框架。 + +它有两个非常关键的亮点: + +- 耗时函数自动后台,从而提高性能; +- 线程的「自动切回」 + +所以,Kotlin 的协程在 Android 开发上的核心好处就是:消除回调地域。 + + ## 使用 如需在 Android 项目中使用协程,请将以下依赖项添加到应用的build.gradle文件中: @@ -400,7 +428,6 @@ main: Now I can quit. 对于回调式的写法,如果并发场景再复杂一些,代码的嵌套可能会更多,这样的话维护起来就非常麻烦。但如果你使用了 Kotlin 协程,多层网络请求只需要这么写: ``` -🏝️ coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 val token = api.getToken() // 网络请求:IO 线程 val user = api.getUser(token) // 网络请求:IO 线程 @@ -413,8 +440,6 @@ coroutineScope.launch(Dispatchers.Main) { // 开始协程:主线程 协程最简单的使用方法,其实在前面章节就已经看到了。我们可以通过一个 `launch` 函数实现线程切换的功能: ``` -🏝️ -// 👇 coroutineScope.launch(Dispatchers.IO) { ... } @@ -426,24 +451,20 @@ coroutineScope.launch(Dispatchers.IO) { 所以,什么时候用协程?当你需要切线程或者指定线程的时候。你要在后台执行任务?切! ``` -🏝️ launch(Dispatchers.IO) { val image = getImage(imageId) } -复制代码 ``` 然后需要在前台更新界面?再切! ``` -🏝️ coroutineScope.launch(Dispatchers.IO) { val image = getImage(imageId) launch(Dispatch.Main) { avatarIv.setImageBitmap(image) } } -复制代码 ``` 好像有点不对劲?这不还是有嵌套嘛。 @@ -451,20 +472,17 @@ coroutineScope.launch(Dispatchers.IO) { 如果只是使用 `launch` 函数,协程并不能比线程做更多的事。不过协程中却有一个很实用的函数:`withContext` 。这个函数可以切换到指定的线程,并在闭包内的逻辑执行结束之后,自动把线程切回去继续执行。那么可以将上面的代码写成这样: ``` -🏝️ coroutineScope.launch(Dispatchers.Main) { // 👈 在 UI 线程开始 val image = withContext(Dispatchers.IO) { // 👈 切换到 IO 线程,并在执行完成后切回 UI 线程 getImage(imageId) // 👈 将会运行在 IO 线程 } avatarIv.setImageBitmap(image) // 👈 回到 UI 线程更新 UI } -复制代码 ``` 这种写法看上去好像和刚才那种区别不大,但如果你需要频繁地进行线程切换,这种写法的优势就会体现出来。可以参考下面的对比: ``` -🏝️ // 第一种写法 coroutineScope.launch(Dispachers.IO) { ... @@ -491,13 +509,11 @@ coroutineScope.launch(Dispachers.Main) { } ... } -复制代码 ``` 由于可以"自动切回来",消除了并发代码在协作时的嵌套。由于消除了嵌套关系,我们甚至可以把 `withContext` 放进一个单独的函数里面: ``` -🏝️ launch(Dispachers.Main) { // 👈 在 UI 线程开始 val image = getImage(imageId) avatarIv.setImageBitmap(image) // 👈 执行结束后,自动切换回 UI 线程 @@ -506,7 +522,6 @@ launch(Dispachers.Main) { // 👈 在 UI 线程开始 fun getImage(imageId: Int) = withContext(Dispatchers.IO) { ... } -复制代码 ``` 这就是之前说的「用同步的方式写异步的代码」了。 @@ -514,22 +529,14 @@ fun getImage(imageId: Int) = withContext(Dispatchers.IO) { 不过如果只是这样写,编译器是会报错的: ``` -🏝️ fun getImage(imageId: Int) = withContext(Dispatchers.IO) { // IDE 报错 Suspend function'withContext' should be called only from a coroutine or another suspend funcion } -复制代码 ``` 意思是说,`withContext` 是一个 `suspend` 函数,它需要在协程或者是另一个 `suspend` 函数中调用。 - - - - - - #### 挂起函数 当我们调用标记有特殊修饰符`suspend`的函数时,会发生挂起: @@ -539,9 +546,13 @@ suspend fun doSomething(foo: Foo): Bar { …… } ``` -这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。挂起函数能够以与普通函数相同的方式 -获取参数和返回值,但它们只能从协程和其他挂起函数中调用。事实上,要启动协程, -必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。让我们来看一个例子,一个简化的`async()`函数 +这样的函数称为挂起函数,因为调用它们可能挂起协程(如果相关调用的结果已经可用,库可以决定继续进行而不挂起)。 + +挂起函数能够以与普通函数相同的方式获取参数和返回值,但它们只能从协程和其他挂起函数中调用。 + +事实上,要启动协程,必须至少有一个挂起函数,它通常是匿名的(即它是一个挂起`lambda`表达式)。 + +让我们来看一个例子,一个简化的`async()`函数 (源自`kotlinx.coroutines`库): ```kotlin @@ -558,7 +569,8 @@ async { } ``` -`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: +`await()`可以是一个挂起函数(因此也可以在一个`async {}`块中调用),该函数挂起一个协程,直到一些计算完成并返回其结果: + ```kotlin async { …… @@ -569,13 +581,15 @@ async { ``` -请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: +请注意,挂起函数`await()`和`doSomething()`不能在像`main()`这样的普通函数中调用: + ```kotlin fun main(args: Array) { doSomething() // 错误:挂起函数从非协程上下文调用 } ``` 还要注意的是,挂起函数可以是虚拟的,当覆盖它们时,必须指定`suspend`修饰符: + ```kotlin interface Base { suspend fun foo() diff --git a/README.md b/README.md index 5f20491c..234020ac 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ Android学习笔记 - [1.音视频基础知识][328] - [2.系统播放器MediaPlayer][329] - [11.播放器组件封装][330] + - [MediaMetadataRetriever][344] - [DNS及HTTPDNS][23] - [流媒体协议][224] - [流媒体协议][246] @@ -721,6 +722,8 @@ Android学习笔记 [341]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/AVI.md "AVI" [342]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenCV "OpenCV" [343]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenCV/1.OpenCV%E7%AE%80%E4%BB%8B.md "1.OpenCV简介" +[344]: "MediaMetadataRetriever" + Developed By diff --git "a/SourceAnalysis/ARouter\350\247\243\346\236\220.md" "b/SourceAnalysis/ARouter\350\247\243\346\236\220.md" new file mode 100644 index 00000000..3468f729 --- /dev/null +++ "b/SourceAnalysis/ARouter\350\247\243\346\236\220.md" @@ -0,0 +1,55 @@ +# ARouter解析 + +[ARouter](https://github.com/alibaba/ARouter) + +> 一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦 + +简单的说: 它是一个路由系统 ——— 给无依赖的组件提供通信和路由的能力。 + +举个例子: +你过节了你想写一个明信片递给远方的朋友,那你就需要通过邮局(ARoter)来把明信片派送给你的朋友(你和你的朋友相当于两个组件)。 + +使用ARouter在进行Activity跳转非常简单: + +- 初始化ARouter `ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化` +- 添加注解@Route + ```java + // 在支持路由的页面上添加注解(必选) + // 这里的路径需要注意的是至少需要有两级,/xx/xx + @Route(path = "/test/activity") + public class YourActivity extend Activity { + ... + } + ``` +- 发起路由 `ARouter.getInstance().build("/test/activity").navigation();` + + + +ARouter框架能将多个服务提供者隔离,减少相互之间的依赖。其实现的流程和我们平常的快递物流管理很类似,每一个具体的快递包裹就是一个独立的服务提供者(IProvider),每一个快递信息单就是一个RouteMeta对象,客户端就是快递的接收方,而使用@Route注解中的path就是快递单号。在初始化流程中,主要完成的工作就是将所有注册的快递信息表都在物流中心(LogisticsCenter)注册,并将数据存储到数据仓库中(Warehouse)。 + +作者:魔焰之 +链接:https://www.jianshu.com/p/11006054f156 +来源:简书 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + +--- + +- 邮箱 :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/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" index e69de29b..d2d01f24 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" @@ -0,0 +1,35 @@ +# MediaExtractor、MediaCodec、MediaMuxer + + +## MediaExtractor + +MediaExtractor是一个Android系统用于从多媒体文件中提取音频和视频数据的类。 + +它可以从本地文件或网络流中读取音频和视频数据,并将其解码为原始的音频和视频帧。它的主要功能就是解封装,也就是从媒体文件中提取出原始的音频和视频数据流。这些数据流可以被送入解码器进行解码,然后进行播放或者其他处理。 + +MediaExtractor可以用于开发音视频播放器、视频编辑器、音频处理器等应用程序。 + + +## MediaMuxter + +MediaMuxter是Android系统提供的一个用于混合音频和视频数据的API。 + +它可以将音频和视频的原始数据流混合封装成媒体文件,例如MP4、WebM等。 + +MediaMuxter通常与MediaExtractor一起使用,MediaExtractor用于从媒体文件中提取音频和视频数据,MediaMuxter用于将这些数据混合成新的媒体文件。 + +简单说就是: MediaExtractor提供了解封装的能力,而MediaMuxer提供了视频封装的能力。 + + +## MediaCodec + +MediaCodec是Android提供的用于对音视频进行编码的类,是Android Media基础框架的一部分,一般和MediaExtractor、MediaMuxer、Surface和AudioTrack一起使用。 + + + + +MediaExtractor仅仅是解封装数据,不会对数据进行解码。要对媒体数据进行解码,需要使用MediaCodec类。 + +而且MediaExtractor只能解封装媒体文件中的音视频等媒体轨道,而不能解析整个媒体文件的结构。如果需要解析整个媒体文件的结构,需要使用其他库或框架。 + + diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" new file mode 100644 index 00000000..a57255ab --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaMetadataRetriever.md" @@ -0,0 +1,33 @@ +# MediaMetadataRetriever + +MediaMetadataRetriever是Android中用于从媒体文件中提取元数据的类,可以获取音频、视频和图片文件的各种信息,例如时长、标题、封面等。 + +```java +MediaMetadataRetriever mRetriever = new MediaMetadataRetriever(); + +mRetriever.setDataSource(mContext, mVideoUri); +``` + +## 获取元数据 + +// 获取歌曲标题 +`mRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);` + +根据key的不同还可以是时长、帧率、分辨率等。 + +## 获取缩略图 + +`public Bitmap getFrameAtTime(long timeUs, int option)` +用于获取音视频文件中的一帧画面,可以用于实现缩略图、视频预览等功能。 + +- timeUs:是获取画面对应的时间戳,单位为微妙 +- option:可以设置是获取离指定时间戳最近的一帧画面或者事最近的关键帧画面等。 + +## 获取专辑封面图 + +```java +byte[] bytes = mRetriever.getEmbeddedPicture(); +Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); +``` +需要注意的是,MediaMetadataRetriever只能用于读取已经完成的音视频文件,无法用于实时处理音视频数据流。 + From 33e2505074417bad384f97ccff56bb03d5b501bd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 11 Apr 2024 10:57:00 +0800 Subject: [PATCH 070/128] update kotlin part --- .../12.Navigation\347\256\200\344\273\213.md" | 28 +++- .../4.ViewModel\347\256\200\344\273\213.md" | 5 + ....8.Kotlin_\345\215\217\347\250\213.md.swp" | Bin 65536 -> 0 bytes ...&\347\261\273&\346\216\245\345\217\243.md" | 139 ++++++++++++++++++ ...2\344\270\276&\345\247\224\346\211\230.md" | 63 +++++++- ...5\345\260\204&\346\211\251\345\261\225.md" | 8 + ...10\346\201\257\346\234\272\345\210\266.md" | 5 + README.md | 2 +- 8 files changed, 241 insertions(+), 9 deletions(-) delete mode 100644 "KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" 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" index dac4d75d..38c42a53 100644 --- "a/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" +++ "b/Jetpack/architecture/12.Navigation\347\256\200\344\273\213.md" @@ -4,6 +4,11 @@ 为此,Jetpack提供了一个名为Navigation的组件,旨在方便我们管理页面和App bar。 + +Navigation 是一个框架,用于在 Android 应用中的“目的地”之间导航,该框架提供一致的 API,无论目的地是作为 fragment、activity 还是其他组件实现。 + + + 它具有以下优势: - 可视化的页面导航图,类似于Apple Xcode中的StoryBoard,便于我们理清页面间的关系。 @@ -15,6 +20,16 @@ Navigation 组件旨在用于具有一个主 Activity 和多个 Fragment 目的地的应用。主 Activity 与导航图相关联,且包含一个负责根据需要交换目的地的 `NavHostFragment`。在具有多个 Activity 目的地的应用中,每个 Activity 均拥有其自己的导航图。 + +在使用过程中,我们感受到如下的优点。 + +- 页面跳转性能更好,在单 Activity 的架构下,都是 fragment 的切换,每次 fragment 被压栈之后,View 被销毁,相比之前 Activity 跳转,更加轻量,需要的内存更少。 +- 通过 Viewmodel 进行数据共享更便捷,不需要页面之间来回传数据。 +- 统一的 Navigation API 来更精细的控制跳转逻辑。 + + + + ## 依赖 如果想要使用Navigation,需要现在build.gradle文件中添加以下依赖: @@ -51,17 +66,20 @@ dependencies { 导航组件由以下三个关键部分组成: -1. Navigation Graph +1. Navigation Graph : 图表 + 一种数据结果,用于定义应用中的所有导航目的地以及它们如何连接在一起。 在一个集中位置包含所有导航相关信息的 XML 资源。这包括应用内所有单个内容区域(称为*目标*)以及用户可以通过应用获取的可能路径。 -2. NavHost + +2. NavHost : 主机 显示导航图中目标的空白容器。导航组件包含一个默认NavHost实现 (NavHostFragment),可显示Fragment目标。 -3. NavController +3. NavController : 控制器 - 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + 在NavHost中管理应用导航的对象。当用户在整个应用中移动时,NavController会安排NavHost中目标内容的交换。 + 该控制器提供了一些方法,可用于在目的地之间导航、处理深层链接、管理返回堆栈等。 在应用中导航时,您告诉NavController,您想沿导航图中的特定路径导航至特定目标,或直接导航至特定目标。NavController便会在NavHost中显示相应目标。 @@ -476,7 +494,9 @@ override fun onCreate(savedInstanceState: Bundle?) { ``` +### 参考 +https://mp.weixin.qq.com/s?src=11×tamp=1712714064&ver=5191&signature=JTMgHGLtMGW*NoSWSrLNVuGzs-KEEDznO-ja7*X*KumZMFAuIRl7WbPYT1gG7AX810nUx6Ftb6nm6Ao92M*GzojPfqBUo1wOFc0gMs1mseTLkUWZ9Q*BIW69MM7ULPDV&new=1 - [上一篇:11.Hilt简介](https://github.com/CharonChui/AndroidNote/blob/master/Jetpack/architecture/11.Hilt%E7%AE%80%E4%BB%8B.md) diff --git "a/Jetpack/architecture/4.ViewModel\347\256\200\344\273\213.md" "b/Jetpack/architecture/4.ViewModel\347\256\200\344\273\213.md" index 7c8a3198..5ec5a6bf 100644 --- "a/Jetpack/architecture/4.ViewModel\347\256\200\344\273\213.md" +++ "b/Jetpack/architecture/4.ViewModel\347\256\200\344\273\213.md" @@ -11,6 +11,11 @@ `ViewModel`是专门用于存放应用程序页面所需的数据。ViewModel将页面所需的数据从页面中剥离出来,页面只需要处理用户交互和展示数据。 + +ViewModel类是一种业务逻辑或屏幕级状态容器。 +它用于将状态公开给界面,以及封装相关的业务逻辑。 +它的主要优点是,它可以缓存状态,并可在配置更改后持久保留相应状态。这意味着在activity之间导航时或进行配置更改后(例如旋转屏幕时),界面将无需重新提取数据。 + Viewmodel解决两个问题: 1. 数据的变更与view隔离,也就是解耦 diff --git "a/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" "b/KotlinCourse/.8.Kotlin_\345\215\217\347\250\213.md.swp" deleted file mode 100644 index 996ab1630d5f280941394ce9a4cbef5c514922e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 65536 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 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 5e1b19fa..72e4986b 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" @@ -324,10 +324,42 @@ var myProperty: String } ``` +自定义set和get的重点在field,field指代当前参数,类似于java的this关键字。 + 这意味着无论何时当你使用点操作符来获取或设置属性值时,实际上你总是调用了属性的getter或是setter。那么,为什么编译器要这么做呢? 为属性添加getter和setter意味着有访问该属性的标准方法。getter处理获取值的所有请求,而setter处理所有属性值设置的请求。 因此,如果你想要改变处理这些请求的方式,你可以在不破坏任何人代码的前提下进行。通过将其包装在getter和setter中来输出对属性的直接访问称为数据隐藏。 + +在某些情况下,无参的函数与只读属性可互换通用。虽然语义相似,但在以下情况中,更多的是选择使用属性而不是方法。 + +- 不会抛出任何异常。 +- 具有O(1)的复杂度。 +- 容易计算(或者运行一次之后缓存结果)。 +- 每次调用返回同样的结果。 + + + +## 后端属性(Blocking Property) + +它实际上是一个隐含的对属性值的初始化声明。能有效避免空指针问题的产生。 + +```kotlin +var size: Int = 2; +private var _table: Map? = null + +val table: Map + get() { + if (_table == null) { + _table = HashMap() + } + return _table ?: throw AssertionError("Set to null by another thread") + } +``` + +在Java中,访问private成员变量需要通过getter和setter来实现,此处通过table来获取_table变量,优化了Java中函数调用带来的开销。 + + ## 延迟初始化 在类内声明的属性必须初始化,如果设置非`null`的属性,应该将此属性在构造器内进行初始化。 @@ -475,6 +507,47 @@ fun getName() : String { ``` +## @操作符 + +在Kotlin中,@操作符主要有两个作用: + +- 限定this的对象类型 +```kotlin +class User { + inner class State { + fun getUser(): User { + return this@User // 返回User + } + + fun getState(): State { + return this@State // 返回State + } + + } +} +``` +- 作为标签使用 +当把@操作符作为标签使用时,可以跳出双层for循环和forEach函数。 +例如: +```kotlin +val listA = listOf(1, 2, 3, 4, 5, 6) +val listB = listOf(2, 3, 4, 5, 6, 7 ) + +loop@ for(itemA in listA) { + var i: Int = 0 + for (itemB in listB) { + i++ + if (item > 2) { + break @loop // 当itemB > 2时,跳出循环 + } + println("itemB: $itemB") + } +} +``` + + + + ## 类的定义:使用`class`关键字 @@ -630,6 +703,25 @@ class Person private constructor(name: String) { } ``` + + +```kotlin +open class Base(p: Int) +class Example(p: Int) : Base(p) +``` + +如果该类有一个主构造函数,那么其基类型可以用主构造函数的参数进行初始化。 +如果该类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其类型,或者委托给另一个构造函数初始化。如: +```kotlin +class Example : View { + constructor(ctx: Context) : super(ctx) + + constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) +} +``` + + + #### 构造方法默认参数 ```kotlin @@ -841,6 +933,48 @@ var age = f1.component2() 对于有些不需要toString()、equals()、hashCode()方法的类如果使用数据类就会导致多生成这些代码,所以在使用数据类的时候不要去为了简单而乱用, 也要去想想是否需要这些方法?是否需要设计成数据类。 + +#### 获取类中成员函数的对象 + +```kotlin +fun main(args: Array) { + var user = User::special + // 调用invoke函数执行 + user.invoke(User("Jack", 30)) + + // 利用反射机制获取指定方法的内容 + var method = User::class.java.getMethod("special") + method.invoke(User("Tom", 20)) +} + +class User(val name: String, val age: Int) { + fun special() { + println("name:${name") age:${age}"); + } +} +``` + +在上面的实例中,User类还有一个special函数,使用User::special可以获取成员函数的对象,然后使用invoke函数调用special函数,以获取该函数的内容。 + + +### 构造函数引用 + +构造函数的引用和属性、方法类似,构造函数可以作用于任何函数类型的对象,该函数的对象与构造函数的参数相同,可以使用::操作符加类名的方式来引用构造函数。 + +```kotlin +class Foo + +fun function(factory: () -> Foo) { + val x: Foo = factory() +} + +// 使用::Foo方式调用类Foo的无参数构造函数 +fun main() { + function(::Foo) +} +``` + + ## 继承 在`Kotlin`中所有类都有一个共同的超类`Any`,这对于没有超类型声明的类是默认超类: @@ -1214,6 +1348,11 @@ val mList2 = vars(0, *myArray, 6, 7) ``` +在Kotlin中,方法调用也被定义为二元操作运算符,而这些方法往往可以转化为invoke函数。 +例如 a(i)方法的对应转换方法为 a.invoke(i) + + + ## 命名风格 如果拿不准的时候,默认使用`Java`的编码规范,比如: 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 9c91998e..1d1ab9da 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" @@ -132,6 +132,8 @@ class Outter{ val outter = Outter() outter.Inner().execute() +var v = this@Outer.testVal // 引用外部类的成员 + // 输出 Inner -> execute : can read testVal=test ``` @@ -142,6 +144,11 @@ val outter = Outter() outter.Inner().execute() ``` +引用外部类中对象的方式为: var 变量名 = this@外部类名 + +在类成员中,this指代该类的当前对象。而在扩展函数或者带接受者的函数中,this表示在点左侧传递的接收者参数。 + + #### 内部类vs嵌套类 在Java中,我们通过在内部类的语法上增加一个static关键词,把它变成一个嵌套类。然而,Kotlin则是相反的思路,默认是一个嵌套类, @@ -154,6 +161,8 @@ outter.Inner().execute() #### 匿名内部类 +没有名字的内部类被称为匿名内部类,使用匿名内部类必须继承一个父类或实现一个接口,匿名内部类可以简化代码编写。 + ```kotlin // 通过对象表达式来创建匿名内部类的对象,可以避免重写抽象类的子类和接口的实现类,这和Java中匿名内部类的是接口和抽象类的延伸一致。 text.setOnClickListener(object : View.OnClickListener{ @@ -179,6 +188,16 @@ mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { }) ``` +如果对象实例是一个函数接口,还可以使用Lambda表达式来实现。 +```kotlin + +val listener = ActionListener { + println("clicked") +} +``` + + + ### 枚举 与Java中的enum语法大体类似,无非多了一个class关键字,表示它是一个枚举类。 @@ -612,12 +631,16 @@ public static Instance getInstance() { 具体的业务逻辑执行者 + + ##### 类委托 在委托模式中,有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。 `kotlin`中的委托可以算是对委托模式的官方支持。 `Kotlin`直接支持委托模式,更加优雅,简洁。`Kotlin`通过关键字`by`实现委托。 +委托模式已被证明是类继承模式的一种很好的替代方案。 + 我们经常会打游戏进行升级,但是有时候因为我们的水平不行,没法升到很高的级别,这就出现了很多的游戏代练,他们帮我们代打,这就是一个委托模式: 1. 定义约束类,定义具体需要委托的业务,这里的业务: @@ -660,6 +683,10 @@ public static Instance getInstance() { ##### 属性委托 +在软件的开发中,属性的getter和setter方法的代码有很多相同或相似的地方,虽然为每个类编写相同的代码是可行的,但这样会造成大量的代码冗余,而委托属性正是为解决这一问题而提出的。 + +属性委托是指一个类的某个属性值不是在类中直接定义,而是将其委托给一个代理类,从而实现对该类属性的统一管理。 + 在Kotlin中,有一些常见的属性类型,虽然我们可以在每次需要的时候手动实现他们,但是很麻烦,为了解决这些问题,Kotlin标准提供了委托属性。 语法是`val/var <属性名>: <类型> by <表达式>`。在`by`后面的表达式是该委托,因为属性对应的`get()`和`set()`会被委托给它的`getValue()` @@ -667,12 +694,15 @@ public static Instance getInstance() { ```kotlin class Example { + // 语法中,关键词by之后的表达式就是委托的类,而且属性的get()以及set()方法将被委托给这个对象的getValue()和setValue()方法。 + // 属性委托不必实现相关接口,但必须提供getValue()方法,如果是var类型的属性,还需要提供setValue()方法。 var property : String by DelegateProperty() } class DelegateProperty { var temp = "old" // getValue和setValue方法的参数是固定的 + // getValue和setValue方法必须用关键字operator标记 operator fun getValue(ref: Any?, p: KProperty<*>): String { return "DelegateProperty --> ${p.name} --> $temp" } @@ -798,8 +828,10 @@ fun main() { +### 标准委托 -`Kotlin`通过属性委托的方式,为我们实现了一些常用的功能,包括: + +`Kotlin`通过属性委托的方式在Kotlin标准库中内置了很多工厂方法来实现属性委托,包括: - 延迟属性`lazy properties` - 可观察属性`observable properties` @@ -810,6 +842,9 @@ fun main() { 延迟属性我们应该不陌生,也就是通常说的懒汉,在定义的时候不进行初始化,把初始化的工作延迟到第一次调用的时候。`kotlin`中实现延迟属性很简单, 来看一下。 +lazy()是一个接收Lambda表达式作为参数并返回一个Lazy实例的函数,返回的实例可以作为实现延迟属性的委托。 +也就是说,延迟属性第一次调用get()时,将会执行传递给lazy()函数的Lambda表达式并记录执行结果,后续所有对get()的调用只会返回记录的结果。 + ```kotlin val lazyValue: String by lazy { Log.d(JTAG, "Just run when first being used") @@ -892,7 +927,24 @@ fun later(block: () -> T) = Later(block) ##### 可观察属性 -如果你要观察一个属性的变化过程,那么可以将属性委托给`Delegates.observable`, `observable`函数原型如下: + +所谓可观察属性(observable),就是当属性发生变化时能自动拦截其变化,实现观察属性值变化的委托函数是Delegates.observable()。 + +该函数接受两个参数: + +- 初始值 +- 属性值变化时间的响应器(handler) + +每当给属性值赋值时都会调用该响应器,该响应器主要有3个参数: + +- 被赋值的属性 +- 赋值前的旧属性值 +- 赋值后的新属性值 + + +如果你要观察一个属性的变化过程,那么可以将属性委托给`Delegates.observable`。 + + `observable`函数原型如下: ```kotlin public inline fun observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit): @@ -947,9 +999,10 @@ fun test(){ -##### map映射 +##### map委托 + +Map委托常常用来将Map中的key-value映射到对象的属性中,这经常出现在解析JSON或者其他动态事情的应用中。 -一个常见的用例是在一个映射`map`里存储属性的值。这经常出现在像解析`JSON`或者做其他“动态”事情的应用中。在这种情况下,你可以使用映射实例自身作为委托来实现委托属性。 ```kotlin class User(val map: Map) { @@ -975,6 +1028,8 @@ class MutableUser(val map: MutableMap) { +委托模式是一种被广泛使用的设计模式,Kotlin的委托主要用来实现代码的重用,Kotlin默认支持委托模式,且不需任何模版方法。 + - [上一篇: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/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 50ff4d0a..a800a8d3 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" @@ -327,6 +327,8 @@ text.panddingH = 100 扩展属性与扩展函数一样,其本质也是对应Java中的静态方法。由于扩展没有实际的将成员插入类中,因此对扩展属性来说幕后字段是无效的。 +扩展属性允许定义在类或者Kotlin文件中,但不允许定义在函数中。 因为扩展属性不能有初始化器,所以只能由显式提供的getter/setter定义,而且扩展属性只能被声明为val。 + ##### 静态扩展 @@ -1205,6 +1207,12 @@ class Group(val name: String) { `KDoc`不支持`@deprecated`这个标签。作为替代,请使用`@Deprecated`注解。 +### 泛型 + +通配符类型参数 `? extends E`表示该方法接收E类型或者E类型的一些子类型,而不仅仅是E类型本身。 +Kotlin抛弃了通配符类型这一概念,转而引入了生产者和消费者的概念,其中,生产者表示那些只能读取数据的对象,使用表达式`out T`标记。 +消费者表示那些只能写入数据的对象,使用表达式`in T`标记。 +为了便于理解,可以简单的将`out T`理解为`? exnteds T`,将`in T`理解为`? super T`。 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 424a31dd..b571b6f3 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" @@ -544,6 +544,11 @@ public final void removeCallbacksAndMessages(@Nullable Object token) { +##### postAtFrontOfQueue():将消息插入到队列头部 + +通过调用sendMessageAtFrontOfQueue计入一个when为0的message到队列,即插入到队列的头部。可以尽量保证快速执行。 + + ## MessageQueue类的常用方法 MessageQueue非常重要,因为quit、next等都最终调用的是MessageQueue类: diff --git a/README.md b/README.md index 234020ac..8604e003 100644 --- a/README.md +++ b/README.md @@ -722,7 +722,7 @@ Android学习笔记 [341]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/%E8%A7%86%E9%A2%91%E5%B0%81%E8%A3%85%E6%A0%BC%E5%BC%8F/AVI.md "AVI" [342]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenCV "OpenCV" [343]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenCV/1.OpenCV%E7%AE%80%E4%BB%8B.md "1.OpenCV简介" -[344]: "MediaMetadataRetriever" +[344]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/MediaMetadataRetriever.md "MediaMetadataRetriever" From 05e352e6a78d9b555831c839eb2a9e9d0f73e237 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 11 Apr 2024 13:23:08 +0800 Subject: [PATCH 071/128] update kotlin part --- ...05\350\201\224\345\207\275\346\225\260.md" | 59 ++++++++++++++++++- .../8.Kotlin_\345\215\217\347\250\213.md" | 16 +++++ 2 files changed, 73 insertions(+), 2 deletions(-) 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 42440cd9..dba30dbf 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" @@ -3,6 +3,51 @@ ## 高阶函数 + +高阶函数(Higher Order Function)是一种特殊的函数,它接收函数作为参数,或者返回一个函数。 +函数的基本格式如下: + +```kotlin +fun 高阶函数名(参数函数名: 参数函数类型): 高阶函数返回类型 { + // 高阶函数体 +} +``` + +高阶函数的一个很好的例子就是lock函数,它的参数是一个Lock类型对象和一个函数。 + +该函数执行时获取锁,运行函数参数,运行结束后再释放锁。 +```kotlin +fun lock(lock: Lock, body: () -> T): T { + lock.lock() + try { + return body() + } finally { + lock.unlock() + } +} +``` +在这个例子中,body为函数的类型对象,该函数是一个无参函数,而且返回值类型是T。 + +调用lock函数时,可以传入另一个函数作为参数。 + +和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面,如: +```kotlin +val result = lock(lock, { sharedResource.operation()}) +// 等同于 +lock(lock) { + sharedResource.operation() +} +``` + + +高阶函数类似C语言的函数指针,它的另一个使用场景是map函数。 + +如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。 + +```kotlin +val doubled = mMap.map {it -> it * 2} +``` + Kotlin天然支持了部分函数式特性。函数式语言一个典型的特征就在于函数是头等公民——我们不仅可以像类一样在底层直接定义一个函数,也可以在一个函数内部定义一个局部函数。 ```kotlin @@ -225,7 +270,9 @@ countryApp.filterCountries(countries, { }) ``` -这就是Lambda表达式,它与匿名函数一样,是一种函数字面量。 +这就是Lambda表达式。 + +Lambda表达式本质上是一个匿名函数,它是一种函数字面值,即一个没有声明的函数,可以把该函数当做普通表达式进行参数传递,它可以访问自己的闭包函数。 首先来看一下Lambda的定义,如果用最直白的语言来阐述的话,Lambda就是一小段可以作为参数传递的代码。从定义上看,这个功能就很厉害了,因为正常情况下,我们向某个函数传参时只能传入变量,而借助Lambda却允许传入一小段代码。这里两次使用了“一小段代码”这种描述,那么到底多少代码才算一小段代码呢?Kotlin对此并没有进行限制,但是通常不建议在Lambda表达式中编写太长的代码,否则可能会影响代码的可读性。 @@ -373,7 +420,7 @@ add = { x: Int, y: Int -> x + y } Lambda类型也被认为是函数类型。 -当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,而是可以使用it关键字来代替. +当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数. ```kotlin val list = listOf("Apple", "Bnana", "Orange", "Pear") val maxLengthFruit = list.maxBy {it.length} @@ -472,6 +519,13 @@ listOf(1, 2, 3).forEach{ foo(it)() } ## 闭包 + +闭包就是能够读取其他函数内部变量的函数。 + +它是函数内部和函数外部信息交换的桥梁。 + +在Kotlin中,Lambda表达式或匿名函数(局部函数、对象表达式等)都可以访问它的必报。 + 在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。 与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下: @@ -479,6 +533,7 @@ listOf(1, 2, 3).forEach{ foo(it)() } ```kotlin var sum = 0 listOf(1, 2, 3).filter { it > 0 }.forEach { + // 外部的变量,且可以修改 sum += it } println(sum) // 6 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 60f01257..8fbfda6e 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -5,6 +5,11 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它我们可以避免在异步编程中使用大量的回调,同时相比传统多线程技术,它更容易提升系统的高并发处理能力。 + +协程(coroutine),又称微线程,是一种无优先级的子程序调度组件,由协程构建器(coroutine builder)启动。 + +协程本质上是一种用户态的轻量级线程 + ## 起源 线程是由操作系统来进行调度的,当操作系统切换线程的时候,会产生一定的消耗。而协程不一样,协程是包含于线程的,也就是说协程 @@ -23,6 +28,7 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 协程是语言层面的东西,线程是系统层面的东西。 协程就是一段代码块,既然是代码那就离不开CPU的执行,而CPU调度的基本单位是线程。 +可以说,只要内存足够,一个线程中可以包含任意多个协程,但某一时刻却只能有一个协程在运行,多个协程分享该线程资源。 ## 进程、线程、协程 进程:一段程序的执行过程,资源分配和调度的基本单位,有其独立地址空间,互相之间不发生干扰 @@ -81,6 +87,16 @@ CPU增加内存管理单元,进行虚拟地址和物理地址的转换 所以,Kotlin 的协程在 Android 开发上的核心好处就是:消除回调地域。 +线程操作需要VM或OS的支持,通过CPU调度来执行生效。 +而协程主要通过编译技术来实现,通过参入代码来生效,不需要VM或OS的额外支持。 + +简单来说,协程是编译器级的,而线程是操作系统级的。 + +协程运行在同一个线程上,没有上下文切换。 +和线程不同,协程有多个入口点,可以在指定的位置挂起和恢复执行,而线程通常只有一个入口点,且只能返回一次。 +其次,协程不需要多线程的锁机制,因为协程只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不需要加锁,只需要判断状态即可,因此执行效率比多线程高很多。 +另外,使用协程后,不再需要像异步编程时那样写很多回调函数,代码结构不再支离破碎,整个代码逻辑看上去和同步代码没有什么区别,代码结构更加优雅简洁。 + ## 使用 From e7a22a35560ca71f8936d41661967a84854173ca Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 12 Apr 2024 11:10:43 +0800 Subject: [PATCH 072/128] update --- .../\347\256\227\346\263\225.md" | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" index 7c8a9a26..3347d083 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -29,7 +29,9 @@ 显然,由此算法时间复杂度的定义可知,我们的三个求和算法的时间复杂度分别为O(n),O(1),O(n²)。 我们分别给它们取了非官方的名称,O(1)叫常数阶、O(n)叫线性阶、O(n²)叫平方阶。 - +- O(1) : 操作所需的时间是常数(n个元素所需的时间与一个元素所需的时间相同) +- O(n) : n个元素的时间是一个元素时间乘以n +- O(n²): n个元素的时间是一个元素时间乘以n² #### 推导大O阶方法 From 1e85414eb8f46e746a64a44b460181c32a152147 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 17 Apr 2024 11:03:28 +0800 Subject: [PATCH 073/128] update OpenGL part --- .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 7578a7eb..ab8a5559 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -588,6 +588,23 @@ Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。 +#### 视频数据流 + +视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为 +已解码数据的载体,保存播放器解码后的数据。 + +在显示时,需要另一个Surface作为已渲染数据的载体,保存OpenGL渲染后的数据,最后通过EGL显示在屏幕上。 + +数据流如下图所示: + +![image](https://github.com/CharonChui/Pictures/blob/master/video_display_process.webp?raw=true) + +- 解码器(MediaCodec或FFmpeg等)将视频文件中的视频编码数据解码成图像流放到Surface中。 +- SurfaceTexture把Surface生成的图像流,转换为纹理 +- OpenGL ES渲染纹理生成图形数据,放到GLSurfaceView中的Surface中 +- EGL从Surface中取出图形数据,显示到屏幕上 + + ### 参考 --- From e853fbd2a2308deee685abd000fff5b718f5ccc0 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 18 Apr 2024 20:48:34 +0800 Subject: [PATCH 074/128] update OpenGL part --- .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 9 +++++++-- ...262\345\231\250\350\257\255\350\250\200GLSL.md" | 13 ++++++++++++- .../OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index ab8a5559..5544fac7 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -340,8 +340,13 @@ glBufferData是一个专门用来把用户定义的数据复制到当前绑定 - OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型(每一个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。 - 配置OpenGL如何解释这些内存 - 通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。 - **注意: VAO并不保存实际数据,而是放顶点结构的定义** + 通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。 + +VAO(Vertex Array Object)是指顶点数组对象,主要用于管理 VBO 或 EBO ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。 + + **注意: VAO并不保存实际数据,而是放顶点结构的定义** + + ![image](https://github.com/CharonChui/Pictures/blob/master/vao_vbo.jpg?raw=true) diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 6e1414d8..e179b27b 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -140,7 +140,18 @@ precision mediump int; #### Uniform -Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。 + +CPU在处理并行线程的时候,每个线程负责给完整图像的一部分配置颜色。 +尽管每个线程和其他线程之间不能有数据交换,但我们能从CPU给每个线程输入数据。 +因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。 + +也就是说,每条线程接收相同的数据,并且是不可改变的数据。这些输入值叫做uniform(统一值). + +按业界传统应该在uniform值的名字前加u_,这样一看便知是uniform。 + +Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。 + +你可以把uniform想象成连通CPU和GPU的许多的小桥梁。 首先,uniform是全局的(Global)。 全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 3f3fb88b..921dcbe4 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -4,6 +4,20 @@ ### 纹理 + +现实生活中,纹理(Texture)最通常的作用是装饰 3D 物体,它就像贴纸一样贴在物体表面,丰富了物体的表面和细节。 + +在 OpenGLES 开发中,纹理除了用于装饰物体表面,还可以用来作为存储数据的容器。 + +那么在 OpenGLES 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。 + +2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 + +立方图纹理是一个由 6 个单独的 2D 纹理面组成的纹理。立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标。 + +3D 纹理可以看作 2D 纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。 + + 在OpenGL中简单理解就是一张图片,在学习之前需要明白这几个概念,不然很容易迷糊,不知道为什么要这样去调用api,到底是什么意思. - 纹理Id:句柄,纹理的直接引用 From 7fa6227b2f8361f157b8d9e430e068a4513a194b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 24 Apr 2024 09:45:46 +0800 Subject: [PATCH 075/128] update Kotlin part --- ...6\236\232\344\270\276&\345\247\224\346\211\230.md" | 5 +++++ .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) 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 1d1ab9da..f3f9117d 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" @@ -155,6 +155,9 @@ outter.Inner().execute() 必须加上inner关键字才是一个内部类,也就是说可以把静态的内部类看成嵌套类。 内部类和嵌套类有明显的差别,具体体现在: + +- 嵌套类相当于Java中的静态内部类,不能使用外部类的this。 +- 内部类必须以inner class定义,相当于Java中的私有非静态内部类,可以访问外部类的this。 - 内部类包含着对其外部类实例的引用,在内部类中我们可以使用外部类中的属性。而在嵌套类中不包含对其外部类实例的引用,所以它无法调用其外部类的属性。 @@ -466,6 +469,8 @@ public class Prize { 伴生对象:“伴生”是相较于一个类而言的,意为伴随某个类的对象,它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样, 全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。 +准确的说是Kotlin使用伴生对象来实现Java静态的效果,但不能说伴生就是静态。 + 现在将上面的例子改写成伴生对象的版本: ```kotlin diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 5544fac7..7f4585a1 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -9,9 +9,6 @@ OpenGL(Open Graphics Library开放图形库)是用于渲染2D、3D矢量图形 [OpenGL官网](https://www.opengl.org/) - -OpenGL的前身是硅谷图形功能(SGI)公司为其图形工作站开发的IRIS GL,后来因为IRIS GL的移植性不好,所以在其基础上,开发出了OpenGL。 - OpenGL一般用于在图形工作站,PC端使用,由于性能各方面原因,在移动端使用OpenGL基本带不动。 为此,Khronos公司就为OpenGL提供了一个子集,OpenGL ES(OpenGL for Embedded System)。 @@ -181,9 +178,15 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 ### 依赖库 +OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区。 +然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。有不少库都可以支持这一部分工作: + +- 使用操作系统提供的窗口管理功能,比如Microsoft Windows API。但这通常不实用,因为需要很多底层的编码工作。 +- FreeGLUT、GLOW、GLFW等库 + + 简要来说,GLAD、GLEW都是一个图形库,可以理解为是在显卡驱动上给渲染用户一个统一的API; 而GLUT、FreeGLUT、GLFW这三个是用于图形开发的辅助工具库,主要用于创建和管理OpenGL环境、操作窗口等。 - OpenGL只是一个标准/规范,具体的实现是由驱动开发商针对特定显卡实现的。但是在你真正能够在程序中使用OpenGL之前,你需要对他进行初始化,但是由于OpenGL是跨平台的,所以也没有一个标准的方式进行初始化。OpenGL初始化分为两个阶段: - 第一个阶段,你需要创建一个OpenGL上下文环境,这个上下文环境存储了所有与OpenGL相关的状态(OpenGL是一个状态机),上下文位于操作系统中某个进程中,一个进程可以创建多个上下文,每一个上下文都可以描绘一个不同的可视界面,就像应用程序中的窗口;简单来理解就是为了创建一个窗口;而GLUT、FreeGLUT、GLFW库就是用于干这件事的。 From 7f8971d27dff0e7ddcd1a0e62b29c29a64c6e028 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 24 Apr 2024 18:56:54 +0800 Subject: [PATCH 076/128] update OpenGL part --- .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 12 ++++++++++++ .../2.GLSurfaceView\347\256\200\344\273\213.md" | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 7f4585a1..75f5aaa5 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -53,6 +53,16 @@ Android中OpenGL ES的版本支持如下: - OpenGL ES 3.1 - 此 API 规范受 Android 5.0(API 级别 21)及更高版本的支持。 +OpenGL ES是一套绘制3D图形的API,OpenGL ES是和平台无关的一套API,主要是为了rendering 2D和3D图形等。 +OpenGL就是一组函数名,并不能直接用,需要底层硬件GPU的支持。 + +OpenGL ES的实现有2中方式: + +- 第一种:是软件实现,用cpu去实现算法模拟GPU渲染、合成工作,就是agl(libGLES_android.so),路径是frameworks/native/opengl/libagl,即the software OpenGL ES library。 + +- 第二种:是硬件厂商根据自己GPU提供的实现,一般都不开放源代码,就是/vendor/lib/egl或/system/lib/egl目录下的库。 + + ### OpenGL状态机(State Machine) >>> As long as you keep in mind that OpenGL is basically one large state machine,most of its functionality will make more sense. @@ -488,6 +498,8 @@ GLSL(OpenGL Shading Language)是OpenGL着色语言。 - 顶点着色器(Vertex Shader) - 片段着色器(Fragment Shader) +通常在CPU上编译、链接成二进制数据,然后将它拷贝到GPU上运行,在GPU上运行起来的就是渲染、合成的命令。它的编译是通过OpenGL API操作的。 + ### 坐标系 OpenGL ES是一个3D的世界,由x、y、z坐标组成顶点坐标。采用虚拟的右手坐标。 diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index fa6b2098..cea103bb 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -1,5 +1,19 @@ ## 2.GLSurfaceView简介 + +### 生产者(Surface)与消费者(SurfaceFlinger) + +在Android系统中,无论开发者调用什么渲染API,一切内容都会渲染到Surface上。 + +Surface表示缓冲区队列中的生产者,而缓冲区队列会被消费者SurfaceFlinger消耗掉。 + +在Android平台上创建的每个窗口都由Surface提供支持。 +用户通过在Surface这张画布上绘制生成图形数据,这些数据会被BufferQueue传输给SurfaceFlinger,所有被渲染的可见Surface都被SurfaceFlinger合成到屏幕。 + + + + + Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。也就是说我们想在Android中使用OpenGL ES的最简单的方法就是将我们要显示的图像渲染到GLSurfaceView上,但它只是一个展示类,至于怎么渲染到上面我们就要自己去实现GLSurfaceView.Renderer来生成我们自己的渲染器从而完成渲染操作,这里是不是感觉Android框架提供的类太少了,怎么没有GLTextureView,后面在分析完GLSurfaceView的源码后,可以直接拷贝自己去实现GLTextureView,[有关SurfaceView与TextureView的区别可以看这篇文章](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/SurfaceView%E4%B8%8ETextureView.md)。 From 9b48142325bea5af9568f13d22d9a9f25c85e8c6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 24 Apr 2024 19:02:12 +0800 Subject: [PATCH 077/128] update Android Framework part --- .../3.Android Framework\346\241\206\346\236\266.md" | 2 ++ 1 file changed, 2 insertions(+) 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 d653e130..bcf041f8 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" @@ -64,6 +64,8 @@ Linux驱动和Framework相关的主要包含两部分: + + ## APK程序的运行过程 1. 首先,ActivityThread从main()函数开始执行,调用prepareMainLooper()为UI线程创建一个消息队列(MessageQueue)。 From a7cd7835302da863234c0c8c5d99c8e271071ed8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 26 Apr 2024 21:38:48 +0800 Subject: [PATCH 078/128] update OpenGL part --- .../1.OpenGL\347\256\200\344\273\213.md" | 16 +++++- ...66\344\270\211\350\247\222\345\275\242.md" | 56 +++++++++++++++++++ ...45\231\250\350\257\255\350\250\200GLSL.md" | 2 +- 3 files changed, 71 insertions(+), 3 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 75f5aaa5..d2156032 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -175,6 +175,8 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 而顶点数据是用顶点属性(Vertex Attribute)表示的,它可以包含任何我们想用的数据,但是简单起见,我们还是假定每个顶点只由一个3D位置和一些颜色值组成的吧。 + + ### 立即渲染模式(Immediate mode) && 核心模式(Core-profile) 早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。 @@ -241,6 +243,8 @@ GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所 它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。 +所有顶点着色器的主要目标都是讲顶点发送给管线(正如之前所说的它会对每个顶点进行处理)。 + 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 顶点着色器会在GPU上创建内存用于存储我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。顶点着色器接着会处理我们在内存中指定数量的顶点。 @@ -441,9 +445,15 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 -主要目的是计算一个像素的最终颜色。 -光栅化操作构造了像素点,这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。 +我们3D世界中的点、三角形、颜色等全都需要展现在一个2D显示器上。 +这个2D屏幕由栅格(即矩形像素阵列)组成。当3D物体栅格化后,OpenGL会将物体中的图元(通常是三角形)转化为片段。 +片段拥有关于像素的信息。栅格化过程确定了为了显示由3个顶点确定的三角形需要绘制的所有像素的位置。片段着色器用于为栅格化的像素指定颜色。 + +主要目的是计算一个像素的最终颜色。 +所有片段着色器的目的都是给为要展示的像素赋予颜色。 + +光栅化操作构造了像素点(图元(点或三角形)转换成了像素的集合),这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。 为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。 一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 @@ -523,6 +533,8 @@ OpenGL采用的是右手坐标,选取屏幕中心为原点,从原点到屏 如果想要绘制图像,需要至少一个顶点着色器来定义一个图形顶点,以及一个片元着色器为该图形上色。这些着色器必须被编译然后再添加到一个OpenGL Program中,并利用这个Program来绘制形状。 +glUseProgram()用于将含有两个已编译着色器的程序载入OpenGL管线阶段(在GPU上)。 +注意,glUseProgram()并没有运行着色器,它只是将着色器加载进硬件。 ### EGL diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 18119b7d..5f91fcac 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -1,5 +1,6 @@ ## 5.OpenGL ES绘制三角形 + OpenGL ES的绘制需要有以下步骤: ### 1. 顶点输入 @@ -628,6 +629,11 @@ public class TriangleRender implements GLSurfaceView.Renderer { - 如何将这个矩阵传递给GLSL中。(与获取顶点索引类似,可以在GLSL中声明一个mat4类型的矩阵变量,获取其索引,再传递值给它) +## 数学概念 + + + + ### 向量(Vector) @@ -635,6 +641,18 @@ public class TriangleRender implements GLSurfaceView.Renderer { 在GLSL中一个向量有最多4个分量,每个分量值都代表空间中的一个坐标,它们可以通过vec.x、vec.y、vec.z和vec.w来获取。 vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是4D),而是用在所谓透视划分上。 + +3D空间中的点可以通过使用形如(2, 8, −3)的符号列出x、y、z的值来表示。 + +不过,如果用齐次坐标—— 一种在19世纪初首次描述的表示法来表示点会更有用。 + +在每个点的齐次坐标有4个值,前3个值表示x、y和z,第四个值w总是非零值,通常为1。因此,我们会将之前的点表示为(2, 8, −3, 1)。 + +用来存储齐次3D坐标的GLSL数据类型是vec4(“vec”代表向量,同时也可以用来表示点)。 + + + + ### 矩阵 由m*n个数按照一定顺序排列成m行n列的矩形数表称为矩阵,而向量则是由n个有序数组成的数组。 @@ -645,6 +663,44 @@ vec.w分量不是用作表达空间中的位置(因为我们处理的是3D不是 OpenGL中使用的是列向量,如[xyzx]T,所以与矩阵相乘时,矩阵在前,向量在后,最终通过变换矩阵得到想要的向量。 +矩阵是矩形的值的阵列,它的元素通常使用下标访问。第一个下标表示行号,第二个下标表示列号,下标从0开始。我们在3D图形计算中要用到的矩阵尺寸常为4×4。 + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_matrix.png?raw=true) + +GLSL中的mat4数据类型用来存储4×4矩阵。 + +单位矩阵中一条对角线的值为1,其余值全为0。任何点或矩阵乘单位矩阵都不会改变。在GLM中,调用构造函数glm::mat4 m(1.0f)可以在变量m中生成单位矩阵。 + + + +**在图形学中,矩阵通常用来进行物体的变换。例如矩阵可以用来将点从一处移动到另一处。** + + +矩阵变换: + +- 平移矩阵 +- 缩放矩阵 +缩放矩阵除了放大缩小之外,还可以用来切换坐标系。 +例如,我们可以用缩放来在给定右手坐标系的情况下确定左手坐标系中的坐标。 通过反转Z坐标就可以在右手坐标系和左手坐标系中切换。 + +- 旋转矩阵 + +数学家欧拉表明,围绕任何轴的旋转都可以表示为绕x轴、y轴、z轴旋转的组合。围绕这3个轴的旋转角度被称为欧拉角。 +这个被称为欧拉定理的发现对我们很有用,因为对于每个坐标轴的旋转可以用矩阵变换来表示。 + +实践中,当在3D空间中旋转轴不穿过原点时,物体使用欧拉角进行旋转需要几个额外的步骤,一般有: + +1. 平移旋转轴以使它经过原点 +2. 绕x轴、y轴、z轴旋转适当的欧拉角 +3. 服用步骤1中的平移 + + +- 投影矩阵 +- LookAt矩阵 + +变换矩阵的重要特性之一就是它们都是4x4的矩阵。这是因为我们决定使用齐次坐标系。 否则,各变换矩阵可能会有不同的纬度并且无法想乘。 + + ### 相机 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index e179b27b..2579b75d 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -349,7 +349,7 @@ GLSL的类型转换与C不同。在GLSL中类型不可以自动提升,比如fl 片段着色器的内建变量 - 输入变量 - - gl_FragCoord:当前片元在framebuffer画面的相对位置 + - gl_FragCoord:当前片元在framebuffer画面的相对位置(输入片段的坐标) - gl_FragFacing:bool型,表示是否为属于光栅化生成此片元的对应图元的正面。 - gl_PointCoord:经过插值计算后的纹理坐标,点的范围是0.0到1.0 - 输出变量 From d8b28c6234fd6ad2e9f337a46ab4ed1b13fe7c50 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 27 Apr 2024 13:32:11 +0800 Subject: [PATCH 079/128] update opengl part --- ...3.\345\256\236\344\276\213\345\214\226.md" | 58 +++++++++++++++++++ ...66\344\270\211\350\247\222\345\275\242.md" | 11 +++- ...42\345\217\212\345\234\206\345\275\242.md" | 9 +++ ...45\231\250\350\257\255\350\250\200GLSL.md" | 45 ++++++++++++++ .../9.OpenGL ES\347\272\271\347\220\206.md" | 25 ++++++++ 5 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 "VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" diff --git "a/VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" "b/VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" new file mode 100644 index 00000000..cf2f5fae --- /dev/null +++ "b/VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" @@ -0,0 +1,58 @@ +## 13.实例化 + +实例化(instancing)提供了一种机制,可以只用一个C++/OpenGL调用就告诉显卡渲染一个对象的多个副本。这可以带来显著的性能优势,特别是在绘制有数千甚至数百万个对象时,例如渲染在场地中的许多花朵。 + +实例化最常见的应用就是做一些粒子效果,例如一些烟花的效果。。 + + +在渲染多个对象时,OpenGL使用Z-buffer算法来进行隐藏面消除。通常情况下,通过选择最接近相机的相应片段的颜色作为像素的颜色,这种方法可决定哪些物体的曲面可见并呈现到屏幕,而位于其他物体后面的曲面不应该被渲染。 +然而,有时候场景中的两个物体表面重叠并位于重合的平面中,这使得深度缓冲区算法难以确定应该渲染两个表面中的哪一个(因为两者都不“最接近”相机)。发生这种情况时,浮点舍入误差可能会导致渲染表面的某些部分使用其中一个对象的颜色,而其他部分则使用另一个对象的颜色。这种不自然的伪影称为Z冲突(Z-fighting)或深度冲突(depth-fighting),是渲染的片段在深度缓冲区中相互对应的像素条目上“斗争”的结果。 + + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_z_fighting.png?raw=true) + + + +提高渲染效率的另一种方法是利用OpenGL的背面剔除能力。当3D模型完全“闭合”时,意味着内部永远不可见(例如对于立方体和四棱锥),那么外表面的那些与观察者背离且呈一定角度的部分将始终被同一模型的其他部分遮挡。也就是说,那些背离观察者的三角形不可能被看到(无论如何它们都会在隐藏面消除的过程中被覆盖),因此没有理由栅格化或渲染它们。 +我们可以使用命令glEnable(GL_CULL_FACE)要求OpenGL识别并“剔除”(不渲染)背向的三角形。我们还可以使用glDisable(GL_CULL_FACE)禁用背面剔除。默认情况下,背面剔除是关闭的,因此如果你希望OpenGL剔除背向三角形,必须手动启用它。 + + + + +[上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 5f91fcac..3fbe1436 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -208,7 +208,7 @@ someOpenGLFunctionThatDrawsOurTriangle(); IntBuffer arrays = IntBuffer.allocate(1); GLES30.glGenVertexArrays(1, arrays); ``` -要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。 +要想使用VAO,要做的只是使用glBindVertexArray绑定VAO(glBindVertexArrays()命令的目的是将指定的VAO标记为活跃,这样生成的缓冲区就会和这个VAO相关联)。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。 当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。这段代码应该看起来像这样: VAO小助理偷偷帮我们记录一切: @@ -272,6 +272,12 @@ Preferences -> Plugins -> 搜GLSL Support安装就可以了。 - 顶点着色器(triangle_vertex_shader.glsl) +关键字in意思是输入(input),表示这个顶点属性将会从缓冲区(C++或Java代码中的VBO、VAO等)中接收数值。 +顶点属性还可以改为被声明为out,这意味着它们会将值发送到管线中的下一个阶段。 + +layout(location=0)称为layout修饰符。也就是我们把顶点属性和特定缓冲区关联起来的方法,这意味着这个顶点属性的识别号是0. + + ```glsl // 声明着色器的版本 #version 300 es @@ -445,11 +451,12 @@ public class TriangleRender implements GLSurfaceView.Renderer { //把颜色缓冲区设置为我们预设的颜色,绘图设计到多种缓冲区类型:颜色、深度和模板。这里只是向颜色缓冲区中绘制图形 GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT); // glVertexAttribPointer是把顶点位置属性赋值给着色器程序 - //0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 + // 0是上面着色器中写的vPosition的变量位置(location = 0)。意思就是绑定vertex坐标数据,然后将在vertextBuffer中的顶点数据传给vPosition变量。 // 你肯定会想,如果我在着色器中不写呢?int vposition = glGetAttribLocation(program, "vPosition");就可以获得他的属性位置了 // 第二个size是3,是因为上面我们triangleCoords声明的属性就是3位,xyz GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * Float.BYTES, vertexBuffer); //启用顶点变量,这个0也是vPosition在着色器变量中的位置,和上面一样,在着色器文件中的location=0声明的 + // 也就是说由于vPosition在着色器中的位置被指定为0,因此可以简单的通过glVertexAttribPointer()函数调用中的第一个参数和在glEnableVertexAttribArray()函数调用中使用0来引用此变量 GLES30.glEnableVertexAttribArray(0); //准备颜色数据 diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index f9319879..9b9d547d 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -514,6 +514,15 @@ public class CircleRender extends BaseGLSurfaceViewRenderer { } ``` + +### OpenGL支持的图元 + + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_tuyuan.png?raw=true) + + + + [上一篇: 5.OpenGL ES绘制三角形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/5.OpenGL%20ES%E7%BB%98%E5%88%B6%E4%B8%89%E8%A7%92%E5%BD%A2.md) [下一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 2579b75d..d91aa94e 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -213,6 +213,14 @@ glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); 现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。 +顶点着色器中的乘法将矩阵变换应用于顶点,将其转换为相机空间(请注意从右到左的计算顺序)。这些值被放入内置的OpenGL输出变量gl_Position中,然后继续通过管线,并由光栅着色器进行插值。 +插值后的像素位置(称为片段)被发送到片段着色器(fragment shader)。回想一下,片段着色器的主要目的是设置输出像素的颜色。与顶点着色器的方式类似,片段着色器逐个处理像素,并对每个像素单独调用。 + + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_progress_11.png?raw=true) + + + ### GLSL的特点 @@ -468,6 +476,43 @@ glVertexAttribPointer函数的前几个参数比较明了。这次我们配置 同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是Float.BYTES,用字节来计算就是12字节。 +### 着色器示例 + +``` +顶点着色器如下: +#version 430 + +layout (location=0) in vec3 position; +uniform mat4 mv_matrix; +uniform mat4 proj_matrix; + +out vec4 varyingColor; + +void main(void) +{ gl_Position = proj_matrix * mv_matrix * vec4(position,1.0); + varyingColor = vec4(position,1.0) * 0.5 + vec4(0.5, 0.5, 0.5, 0.5); +} + + +着色器如下: +#version 430 + +in vec4 varyingColor; + +out vec4 color; +uniform mat4 mv_matrix; +uniform mat4 proj_matrix; + +void main(void) +{ color = varyingColor; +} +请注意,因为颜色是从顶点着色器的顶点属性varyingColor中发出的,所以它们也由光栅着色器进行插值! +``` + +请注意,代码中将位置坐标乘1/2,然后加1/2,以将取值区间从[−1, +1]转换为[0, 1]。此外,由程序员定义的插值顶点属性变量名称中通常包含单词“varying”,这是一种约定俗成的做法。修改的具体位置已突出显示。 + + + [上一篇: 6.OpenGL ES绘制矩形及圆形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/6.OpenGL%20ES%E7%BB%98%E5%88%B6%E7%9F%A9%E5%BD%A2%E5%8F%8A%E5%9C%86%E5%BD%A2.md) [下一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 921dcbe4..45764527 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -31,6 +31,17 @@ OpenGL要操作一个纹理,那么是将纹理ID装进纹理单元这个容器 +纹理贴图是在栅格化的模型表面上覆盖图像的技术。它是为渲染场景添加真实感的最基本和最重要的方法之一。 +纹理贴图非常重要,因此硬件也为它提供了支持,使得它具备实现实时的照片级真实感的超高性能。纹理单元是专为纹理设计的硬件组件,现代显卡通常带有数个纹理单元。 +为了在OpenGL/GLSL中有效地完成纹理贴图,需要协调好以下几个不同的数据集和机制: +·用于保存纹理图像的纹理对象(在本章中我们仅考虑2D图像); +·特殊的统一采样器变量,以便顶点着色器访问纹理; +·用于保存纹理坐标的缓冲区; +·用于将纹理坐标传递给管线的顶点属性; +·显卡上的纹理单元。 + + + ### 纹理与渐变色的区别 渐变色:光栅化过程中计算出颜色值,然后在使用片段着色器的时候可以直接赋值。 @@ -77,6 +88,12 @@ V0(-1,0.5),V1(-1, -0.5),V2(1,-0.5),V3(1,0.5) +纹理坐标是对纹理图像(通常是2D图像)中的像素的引用。纹理图像中的像素被称为纹元(texel),以便将它们与在屏幕上呈现的像素区分开。纹理坐标用于将3D模型上的点映射到纹理中的位置。除了将它定位在3D空间中的坐标(x,y,z)之外,模型表面上的每个点还具有纹理坐标(s,t),用来指定纹理图像中的哪个纹元为它提供颜色。这样,物体的表面被按照纹理图像“涂画”。纹理在对象表面上的朝向由分配给对象顶点的纹理坐标确定。 +要使用纹理贴图,必须为要添加纹理的对象中的每个顶点提供纹理坐标。OpenGL将使用这些纹理坐标,查找存储在纹理图像中的引用的纹元的颜色,来确定模型中每个栅格化像素的颜色。为了确保渲染模型中的每个像素都使用纹理图像中的适当纹元进行绘制,纹理坐标也需要被放入顶点属性中,以便由光栅着色器进行插值。 + + + + ### 文件读取 @@ -291,6 +308,14 @@ void main() { } ``` + +为了最大限度地提高性能,我们希望在硬件中执行纹理处理。这意味着片段着色器需要一种访问我们在C++/OpenGL应用程序中创建的纹理对象的方法。它的实现机制是通过一个叫作统一采样器变量的特殊GLSL工具。这是一个变量,用于指示显卡上的纹理单元,从加载的纹理对象中提取或“采样”纹元。 +要实际执行纹理处理,我们需要修改片段着色器输出颜色的方式。以前,我们的片段着色器要么输出一个固定的颜色常量,要么从顶点属性获取颜色,而这次我们需要使用从顶点着色器(通过光栅着色器)接收的插值纹理坐标来对纹理对象进行采样。调用texture()函数如下: +``` +in vec2 tc; // 纹理坐标 +... +color = texture(samp, tc); +``` - render实现类 From f7fbe08e4c454044ac88c3db8e88fc23dcbc32d1 Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Sun, 28 Apr 2024 11:35:53 +0800 Subject: [PATCH 080/128] update OpenGL part --- .../OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 45764527..37eb449f 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -563,6 +563,15 @@ void main() { - 纹理,texture:存储描述信息 - 画布,surface:内存中的一块地址,存储像素数据 + + +- 几何着色器 + +几何着色器阶段位于曲面细分着色器和栅格化之间,位于用于图元处理的管线内。 +顶点着色器允许一次操作一个顶点,而片段着色器一次可以操作一个片段(实际上是一个像素),但几何着色器却可以一次操作一个图元。 + + + [上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) [下一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) From b87f35344290121a56803ef5765eb19d9ab4009e Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 30 Apr 2024 17:28:55 +0800 Subject: [PATCH 081/128] add python part --- .../python3\345\205\245\351\227\250.md" | 291 ++++++++++++++++++ .../11.OpenGL ES\346\273\244\351\225\234.md" | 1 + VideoDevelopment/OpenGL/12.FBO.md | 3 +- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 43 +++ 4 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 "JavaKnowledge/python3\345\205\245\351\227\250.md" create mode 100644 "VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" new file mode 100644 index 00000000..908e7d8f --- /dev/null +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -0,0 +1,291 @@ +# python3入门 + +## 变量 + +```python +message = "Hello World" +print(message) +``` + +## 字符串 + + + +字符串就是一系列字符。在Python中,用引号括起来的都是字符串,其中的引号可以是单引号,也可以是双引号。例如: +```python +message = 'Hello World' +print(message) +message = "Hello World" +print(message) +``` + +### 大小写 +修改单词中的大小写: + +title()方法是以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。 +upper()方法将字符串改为全部大写。 +lower()方法将字符串改为全部小写。 +```python +message = 'hello world' +print(message.title()) +# 输出结果为Hello World +``` + +注意上面方法lower、upper、title等不会修改存储在变量message中的值。 + +### 拼接 + +合并: Python使用加号(+)来合并字符串。这种合并字符串的方法称为拼接。 +```python +first_name = "ada" +last_name = "lovelace" +full_name = first_name + " " + last_name +print(full_name) +# 输出: ada lovelace +``` + +### 空格 + +rstrip(): 去除字符串末尾空白。注意去除只是暂时的,等你再次访问该变量是,你会发现这个字符串仍然包含末尾空白。 +要永久删除这个字符串中的空白,必须将删除的结果存回到变量中: + + +first_name = "ada " +print(first_name) +first_name = first_name.rstrip() +print(first_name) + +lstrip(): 去除字符串开头空白 +strip(): 同时去除字符串两端的空白 + + +## 整数 + + +可对整数执行加(+)、减(-)、乘(`*`)、除(/)运算。 + +Python使用两个乘号表示乘方运算。 + + +## 浮点数 + +在Python中将带小数点的数字都称为浮点数。 +但是要注意的是,结果包含小数位数可能是不确定的,例如: +```python +>>> 0.2 + 0.1 +0.30000000000000004 +``` + +命令行执行python后执行exit()或quit()可退出。 + +## 类型转换 + +```python +age = 23 +message = "Happy" + age + "Birthday" +print(message) +``` + +执行时会报错: +```python +message = "Happy" + age + "Birthday" + ~~~~~~~~^~~~~ +TypeError: can only concatenate str (not "int") to str +``` + +类型错误,这个时候需要显式的指定希望Python将这个整数用作字符串。 +为此,可调用函数str(),它让Python将非字符串值表示为字符串: + +```python +age = 23 +message = "Happy" + str(age) + "Birthday" +print(message) +``` + + +## 注释 + +使用井号(#)标识注释 + + +## 列表 + +在Python中,用方括号[]来表示列表,并用逗号来分割其中的元素。 +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +print(bicycles) +print(bicycles[0]) +print(bicycles[-1]) +# 设置最后一个元素 +bicycles[-1] = 'honda' +# 在列表最后添加元素 +bicycles.append('ducati') +# 在某个位置添加元素 +bicycles.insert(0, 'a') +print(bicycles) +# 删除某个位置的元素 +del bicycles[0] +# 删除最后一个元素,并返回删除的值 +popValue = bicycles.pop() +print(popValue) +# 通过值删除元素,如果列表中有多个重复的元素,那该方法只会删除第一个指定的值 +bicycles.remove('trek') +# 排序 +bicycles.sort() +# 反序排序 +bicycles.sort(reverse=True) +``` +Python为访问最后一个列表元素提供了一种特殊的语法,通过将索引指定为-1,可让Python返回最后一个列表元素。 +这是为了方便在不知道列表长度的情况下访问最后的元素。 +这种约定也适用于其他负数索引,例如,索引-2返回倒数第二个列表元素,索引-3返回倒数第三个列表元素,以此类推。 + +### 临时排序 + +要保留列表元素原来的排列顺序,同时以特定的顺序呈现它们,可使用函数sorted()。 +函数sorted()让你能够按特定顺序显示列表元素,同时不影响它们在列表中的原始排列顺序。 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.sort(reverse=True) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +# ['cannondale', 'redline', 'specialized', 'trek'] +print(sorted(bicycles)) +# ['trek', 'specialized', 'redline', 'cannondale'] +print(bicycles) +``` + + +### 反转列表 + +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +bicycles.reverse() +print(bicycles) +# 列表长度 +size = len(bicycles) +print(size) +``` + +### 遍历 + +**Python根据缩进来判断代码行与前一个代码行的关系,在较长的Python程序中,你将看到缩进程度各不相同的代码块,这让你对程序的组织结构有大致的认识** + + +注意: + +- for in 后有个冒号`:`,for语句末尾的冒号告诉Python,下一行是循环的第一行。 +- 前面缩进的代码才是for循环中的部分 +- 没有缩进的是for循环之外的 + + +- +```python +bicycles = ['trek', 'cannondale', 'redline', 'specialized'] +for bcy in bicycles: + print(bcy) + print(bcy.title()) +print('end') +``` + + +### 切片 + +你还可以处理列表的部分元素——Python称之为切片。 +要创建切片,可指定要使用的第一个元素和最后一个元素的索引。 +你可以生成列表的任何子集,例如,如果你要提取列表的第2~4个元素,可将起始索引指定为1,并将终止索引指定为4: + +```python +players = ['charles','martina','michael','florence','eli'] +print(players[1:4]) +``` + +例如,如果要提取从第3个元素到列表末尾的所有元素,可将起始索引指定为2,并省略终止索引 + + +### range函数 + +range()函数能够生成一系列的数字,例如range(1, 5)会生成1 2 3 4。 +要创建数字列表,可使用函数list()将range()的结果直接转换为列表: +```ptyhon +numbers = list(range(1, 5)) +print(numbers) + +# +print(min(numbers)) +print(max(numbers)) +print(sum(numbers)) + +``` + + +## 元组(不可变的列表) + +有时候你需要创建一系列不可修改的元素,元组可以满足这种需求。Python将不能修改的值称为不可变的,而不可变的列表被称为元组。 +元组看起来犹如列表,但使用圆括号而不是方括号来标识。定义元组后,就可以使用索引来访问其元素,就像访问列表元素一样。 +元组中的元素不能修改,访问和遍历都和列表一样。 + + +相对于列表,元组是更简单的数据结构。 +如果需要存储的一组值在程序的整个生命周期内都不变,可使用元组。 + + + +## if语句 + +```python +cars = ['audi', 'bmd', 'toyota'] +for car in cars: + if car == 'toyota': + print(car.upper()) + else: + print(car.title()) +``` +每条if语句的核心都是一个值为True或False的表达式,这种表达式被称为条件测试。 + + +```python +age = 12 +if age < 4: + print("cost $0") +elif age < 18: + print("cost $5") +else: + print("cost $10") +``` + +### 相等判断 +```python +car = 'Audi' +# False +car == 'audi' +``` + +Python中使用两个等号(==)检测值是否相等,在Python中检测是否相等时区分大小写。 + +判断两个值不相等,可结合使用惊叹号和等号(!=)来判断。 + +条件语句中可包含各种数学比较,如小于<、小于等于<=、大于>、大于等于>=。 + +### 多条件检测 + +有时候需要判断两个条件都为True或只要求一个条件为True时就执行相应的操作。 + +在这些情况下,可以使用关键字and和or。 + +### 值是否包含在列表中 + +要判断特定的值是否已包含在列表中,可使用关键字in。 +```python +cars = ['audi', 'bmd', 'toyota'] +# True +print('audi' in cars) + +# False +print('audi' not in cars) +``` + + +## 字典(Map) + + diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index e76e826e..f7b28a1c 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -402,6 +402,7 @@ public class BaseFilter { [上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +[下一篇: 12.FBO12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) --- diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md index 4a9632fb..8f83daf4 100644 --- a/VideoDevelopment/OpenGL/12.FBO.md +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -22,7 +22,8 @@ Android OpenGL ES开发中,一般使用GLSurfaceView将绘制结果显示到 -[上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +[上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +[下一篇: 13.LUT滤镜]() --- diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" new file mode 100644 index 00000000..851e25da --- /dev/null +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -0,0 +1,43 @@ +# 13.LUT滤镜 + + + + + +[上一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From cf4c9a1bafe49daff7eee0acce95ea212d93912b Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 30 Apr 2024 17:37:05 +0800 Subject: [PATCH 082/128] update opengl part --- JavaKnowledge/.DS_Store | Bin 6148 -> 0 bytes README.md | 7 ++++++- .../11.OpenGL ES\346\273\244\351\225\234.md" | 2 +- VideoDevelopment/OpenGL/12.FBO.md | 4 ++-- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 4 +++- .../14.\345\256\236\344\276\213\345\214\226.md" | 0 6 files changed, 12 insertions(+), 5 deletions(-) delete mode 100644 JavaKnowledge/.DS_Store rename "VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" => "VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" (100%) diff --git a/JavaKnowledge/.DS_Store b/JavaKnowledge/.DS_Store deleted file mode 100644 index 1e695797205fc3a8c581396873b3ebfb6bf032c3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}N6?5Kh|Krijpkg2#Z@f^8K+ysWjpK)2{YrFOfeUEFS#{#c|`_TWjq`vN|M z58xY!9(@)kKb2}LdQntnVDe2SGs))LCD|c_&|=@s6Oti>Fi?rHG&D0rj!T`AnDHb5 zshp#7J-qQBok2Hr{HbVk{6+@kyGu2~dT{>GyqJ7{!hw-HC-l8;FPD3wv3O!GnPMq6 z&yMVt7}&j9uiq`!ItQ{>5yE%IcFo!I8iQ(PaYqEbnin)WD#3Fb5OTQh1+EwrML%%6 zD%aKnEX~r@%<^zp$mg@Xuvr*o`Eaw4%kuTLjnOF0mR44`%GDFU9f(U!C55j}$%4T# zT!Ha!SAH{Z*F`%VhfGJbMXBWYYTv9J?wmc}-fz!}a=#E2wmw_!)b0en2%50Mco!3o zh9)wA3?KvF%78g{*!;Jyf|DZy$iQD@K%NgARH9`t(x{FOXjBORm;kpDuw^YFbGSjv zV5AX7K&Vay)Tz{z7*wZ&pPM+#V5Cu}6KaYNYF4JELZNDPn4ha~LM@HhA_K_4TL$8~ zo00eb?#K22+a&BE1IWO?Vt^(}cBu$kGI#6L=Hy+=L61SD$hb)3YYG_ZDu!5j6)%G- Z0Y8@ppk*-92p$mp5l}Q>gADvA124mDhn)Zb diff --git a/README.md b/README.md index 8604e003..57ce5f8e 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ Android学习笔记 - [10.GLSurfaceView+MediaPlayer播放视频][241] - [11.OpenGL ES滤镜][242] - [12.FBO][332] + - [13.LUT滤镜][346] + - [14.实例化][347] - [弹幕][243] - [Android弹幕实现][244] - [FFmpeg][322] @@ -297,6 +299,7 @@ Android学习笔记 - [Top-K问题][196] - [Java内存模型][285] - [JVM架构][286] + - [python3入门][345] - [基础部分][54] - [安全退出应用程序][110] - [病毒][111] @@ -723,7 +726,9 @@ Android学习笔记 [342]: https://github.com/CharonChui/AndroidNote/tree/master/VideoDevelopment/OpenCV "OpenCV" [343]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenCV/1.OpenCV%E7%AE%80%E4%BB%8B.md "1.OpenCV简介" [344]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/MediaMetadataRetriever.md "MediaMetadataRetriever" - +[345]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/python3%E5%85%A5%E9%97%A8.md "python3入门" +[346]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md "13.LUT滤镜" +[347]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md "14.实例化" Developed By diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index f7b28a1c..366ebbb3 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -402,7 +402,7 @@ public class BaseFilter { [上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) -[下一篇: 12.FBO12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +[下一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) --- diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md index 8f83daf4..dc4d71bc 100644 --- a/VideoDevelopment/OpenGL/12.FBO.md +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -22,8 +22,8 @@ Android OpenGL ES开发中,一般使用GLSurfaceView将绘制结果显示到 -[上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) -[下一篇: 13.LUT滤镜]() +[上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) +[下一篇: 13.LUT滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md) --- diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index 851e25da..0c6eddae 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -4,7 +4,9 @@ -[上一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +[上一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) +[下一篇: 14.实例化](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) + --- diff --git "a/VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" "b/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" similarity index 100% rename from "VideoDevelopment/OpenGL/13.\345\256\236\344\276\213\345\214\226.md" rename to "VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" From 5dafb43ef788ccf545dceabed0d0c1b32e81ba05 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 30 Apr 2024 18:32:21 +0800 Subject: [PATCH 083/128] update README --- .../python3\345\205\245\351\227\250.md" | 15 ++++++++++++++- README.md | 2 +- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 2 +- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 908e7d8f..eb897476 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -286,6 +286,19 @@ print('audi' not in cars) ``` -## 字典(Map) +## 字典(Key-Value) +在Python中,字典是一系列键-值对。字典用放在花括号{}中的一些列键-值对表示。 +每个键都与一个值相关联,你可以使用键来访问与之相关联的值。 +与键相关联的值可以是数字、字符串、列表乃至字典。事实上,可将任何Python对象用作字典中的值。 +```python +alien = {'color': 'blue', 'points': 5} +# 取值 +print(alien['color']) +print(alien['points']) +# 添加值 +alien['xPos'] = 10 +alien['yPos'] = 20 +print(alien['xPos']) +``` diff --git a/README.md b/README.md index 57ce5f8e..e7e3e4f7 100644 --- a/README.md +++ b/README.md @@ -728,7 +728,7 @@ Android学习笔记 [344]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/Android%E9%9F%B3%E8%A7%86%E9%A2%91%E5%BC%80%E5%8F%91/MediaMetadataRetriever.md "MediaMetadataRetriever" [345]: https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/python3%E5%85%A5%E9%97%A8.md "python3入门" [346]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md "13.LUT滤镜" -[347]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md "14.实例化" +[347]: https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/14.%E5%AE%9E%E4%BE%8B%E5%8C%96.md "14.实例化" Developed By diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index 0c6eddae..9c15dc6c 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -5,7 +5,7 @@ [上一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) -[下一篇: 14.实例化](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) +[下一篇: 14.实例化](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/14.%E5%AE%9E%E4%BE%8B%E5%8C%96.md) --- From de9f5e114e6014552ba97efe300b37fe5ec09c1f Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 30 Apr 2024 18:36:44 +0800 Subject: [PATCH 084/128] update --- VideoDevelopment/.DS_Store | Bin 0 -> 6148 bytes .../.1.OpenGL\347\256\200\344\273\213.md.swp" | Bin 0 -> 69632 bytes .../1.OpenGL\347\256\200\344\273\213.md" | 28 ++++++++---------- ....GLSurfaceView\347\256\200\344\273\213.md" | 24 +++++++++++---- 4 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 VideoDevelopment/.DS_Store create mode 100644 "VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..a4c639de123dfb29e6f4f862ffdadea92d776a31 GIT binary patch literal 6148 zcmeHK%}T>S5Z<-5O({YS3OxqA7ObrlikA>8UIf929#m>Vf(B!@G^sh1LeBa^K8erc z%UfJ=99;7$63S z0b*eD7|>^d(cQe#sbXS)82Et!+#f7xh_=Q;q1-y4!|OBpn}{f&<68pJ(r9Ze6oLnY zt5iUh%Jmb2t8}niI?mQuC{*c;%avgsy>j_@;c|7bTQZz+TOswt05MQyprM8)p8u!t z%hW#dt0^=h28e-w#sF_l-Khgb>9h4)d3e@Z&>o(^b literal 0 HcmV?d00001 diff --git "a/VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" "b/VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" new file mode 100644 index 0000000000000000000000000000000000000000..ec4ff252dcbbc9f4474238143addba1def69c816 GIT binary patch literal 69632 zcmeIb34Gnxb>9icNfW1WV#jUbPV<>OMX?RgBq)-yV@vTukrcz25?PV*5+#K|@QH#2 z0vrI8L_4v;1;j#v0L6V@M2ZwCEEFjc1PHQm8rw;mbS6u;>9mf6|9cPF+;%!mGRaK) zJ?D4-7k@lZwmf#nZu9Wb5rO}5@44rkd+yop{ja<4<0~G?eX#bvDt_KpRrTUe)IRln zPwsqEVO3St^P3x<-dx|@ns2t7um3as-1__@^$icTv^+C!b!$s)Q%ggAQ|7hLO+Pq) zeg4_`8yef6-n?%9;^y_OEsg6RZE4TX|9Ip2e9O}Ov-zf$O&jyg?ekY{$~P}xIe$T| zekl%hOpfoW-MBu}Y~|Mq1u7KyMikiG{?xn=y!G)#3m5oQzxSu^$-U!8AOA*gRqj_P zP@zDD0u>5WC{Uq5g#r}{R47oP!2duLXy5cBRi9@(-xJ34bK&1NmHqp@;qQ-ye^->< ze{1-=Cj7gD8tRCfO@;rcz{-&e}+6B_ZK9}55euVwdtFkJsg`1dPi_kSc@50|TcDiA#V`)`N) z^TNO7_tSoVCfr|D_I^65WC{Uq5g#r}{R47oPK!pMo z3j7VFKs}lCIgEXsIifhZssH_FIIMpPtOpu^bwCj(y8yfk_-Wv;ar*xXcoujE@T0)n zfwuv}IIL@dHNa!QQeX-2R^W$#dK}&-fr~iK7l7r!!@!^7i2n)jFM)pn^a1Yy{*VB` zSAokw18@)UCj<@t82BS#3ixf{w}3OiX<#MrH3AQNfknVVU;!{lFk=ny81P~oCR{gPXfONjRpXtRlU*c4gJi^6??B2I<}UEZWkwplCFuufsyJp z?e*>Xc@0n3H*aWc-jI8Eb8|y`V@q>Q)x2CNbU3<0uT6CvO-4scXD=5oj7*M>O^)_X zj_xWP?5bYU(%hbZzJ2Oa)ti>(w&jg z@cHDWZIcrR3j6vdCyw(-p{Jv8yeH{DJk{0DpS(A9Y-cjiT~+nr4}UnfxGmSz0+#h{ zxh*Z5bI;`Sn{tiKxs8p@>vQc-*S80hbB%4eb@gqH6n!*p$+a}+b4~TF8}hj}8*6T3 zeZ$j@&H1?-TiV)lEl=ed+uL$ag<;n>HMVcbJ=fUO#ADC!Y)dPz<(u2`wYjQ$@4Yvr ziPMpRhlg(Zgmv)-!6;|Opd=;JiV2>$;cHN zod0aTd3{Uk{HDfr^Ya^;3f%|iw{2-_&u>hx6psxh69?dqwLG@(4?DKya=8?<(T<2& zX=2zgOZzVFzu2&8bFyb#SBgW23Re#lE}kjuI%T+yzFIg%TU+5qp?e^?aUPCEoa#3; zRfEs+mCM%D)vJuAwymXx`=uMh$?YAW{ZwoH#{6?FtLO@=kE zTT^{k+n`rc8hsdSv6UAfK3VY5odM-{K?rH5ogQT-heZF?6~q>>n=d z-YQDcbi_bHo}52a96y|NUthE|Id(glILjN0SFfPG?1RY*^m#SuA1NFjZ=&^u-=Ot{wF_sq{>?PLfW{$UZoxMokU#fz$Waw2iI_#ILoX3d z;$gaMbk_qWdZsI;U*Ng;4t#^OiJ*8%n;}?^Woi?pp zc_rN=#Y=~h{uf!h=+Iug2B~_Jtw)pF-O0J5hQ(_`#celBcLq>@v|sAjO;1Tbit;=P zqjYAsy}0#g*<&nQJOHMqC{LVEx_1_KbuMwuF}3|7e;TUZ2r55}cu!ZnSQ@@&FAB*{ z)5y{*XzH+f^q!vTIAuI69KKyVHztA<5A2>gadz_NDabWBdXA@41!(ICE3dWU1Oyee zPM2l3V#l;@YSVtIAY7qm7XrZIQr!PqX~$m3q+VE(p+D1y1uogdCAk88|SZXY-r!yns1xG zuBm0+{EhW2a;;(2sC%}pZbMVPt#(uMhL5z?Klh>b*3Gs6$9Y!Ve?`BSUfi1WT!N7> z#wdR5G+L6eBzHQI3{-9D0s;ZKc0t2ZM`u)p$?(l&Y`+|H1aWFrM^YEM5+j zHJL&pPK}{Hp%L>IqDLgGzlnXt9oK^{U`bEPu zrPGuF!KAaVbnPaHm$1!zO|n7so?6LA&3Jq0`sRWG!`xNtkLZFE7+*T%cnIF zz*kL+%aQu5aEq$N@aeA0pYeroQH_zc>WO4v50g)A%W9=hRlB zEv>m_8`tI6ug|Z~t#ND7#u0>qx3M~znUiCCOlP_cBMYy1`3UW;UJ?bKznEG)Sm?N5 z;xb^08%iX5PBFE(O9N$dL^x=UB%?2xY1?~oS~w%S@5bsDp9RioXfkJx+LC=UIeNZu z`WC+@Lr0l7g<+)Yl1#j5&!Xo7N*lCHf%o?&L#QsZb4s`OC0%1l$1U(EbzQZA@9Z>P z-FahjVqdl?n=bC8$Skg2v2jyV-b%u2XvW4nvJ-+7Zyii_9tk5I+sCcRu`zRm&g@Qh zUIjt=nKK9VvEw?T&THvV5Kx>Y8KZP1M#>~~WIHFvJL7da3bKGPTO9AgdbQ@hLnYcjo5IF0$>rXQ+~~{k&YfxGNd!hcf&!XO1*TKn3$?W+i=< z`Tsrm>kIJb&Hw*t{;{7f{QaK*{yy+O0e=s8C-CFIj{%?O`Og8L0k!~-1IvMjfn~r_ zUUEHE~2-aJAO9{=ko4tG6}TUfh%WhTI;t=``#iW4GyzIi+wu6(_X zVi(Ti;yUoNu51 z>CMfbX{?`*o1fp%(z>PY{rA6r;RoJ-f9=!l8=IyPe&g?04)=As+Cbv<`NsN|md*3) zTiY8On(}O&Z-|DV?P{|-L>{|2-I z{|oSa!1n?}_~C;<9!To|?&bHl0Y3uVB)9+D3$QR~ zS>PW4s{yM6=md5EUnAfDXTTxgAaDTK59|ZB0v*7wQVZ~pfY-?TUjsG+?Z6V?dw@3q z{m{(lw$o_$E&dSOsY;IVpXD(X`za2cDURJCepA>_7~1lv1cpxx6i!&E1l-9Dk<0X?i=uDSAwUmMr{{x5v?Dao+Y1idO_5br3e?g@tL`#NWof8#NV;DjZ({=)Jx5qE z1hzeyGo1{`G@WVJF!Cf_=}s4+FOQ^!WQ5(CMOl)YF&ekJaZ|pDS^^6!l4&`2v@}e< zU`#Bqi#~qK#_aB*F+~QG(>pEvdSO>+`@jfQ22&mTi{nH@EfEC0r^v<)otfIYk46f| zPfZ=V#$6C3fJ@>Co+Y~v3Tu*b#qn;Uqm(3AG}yUFUm6X0T|;JICs#5IgqFuID*Q=? zls+cM$0_WX9NSgwd07E*qK7Y!(XZ|k1y#$W29%Xc`mQmYR75CFLC|QZbmvGie5=%T zO|2URsS0sMC`7HC3was~O853=cp^!h>exmeC(0uYq$KJ)bE zrpDS&Z<>*(BBV@Kw$Qzoa5Qsc!F|RA&!I5nsckP3A&Q;I&zW|H4Fkrb?rL2uc+b5>FNO+DgBFdR2Z&?m^YHYl?{Uwb> z*=C-!A)UUVH0ji#p{cWn(+SLopM3Jks$Z$H++Hm|{!7ccPv;xjb93}>-6yCnYI&}1 zt(6e9KbXtSpPy3&RghX{=?HoJtJr4RGYk*TiTN>i==l~1I(d5+vW2N<2`kUXTaLq3q?kpiRq4z{ z74wL{ahOnc^5!nqI_5-JB~(Z^w4G;!6k?#aR1m`T8Dt^r=?Nzi?5Zdj*_m>7(Asc$ zDIVVibJ2uLlv+VuaxXI2d5Pd=vF2fx15MGXj-IJAmfBU}oMG)_3KHg(YupX5yT(zI zIU3bNru02g&2youIr9>Nr zSI?mO5KznYDQnk#EEykDCoG%ro}4XsQ`tbWzwEkRDcw0~?RSsNMj()?D(!Zvs|yKQ z($q*kaNe4H>$CY*D&~@tyU>c%6-;%7sjF0&k*n{j9!mxfC)>h`nG3pr`)bMt&=rs_ z4c{m@rS)EM9PQnm?CWDq0~?8?QAE`vUX}+JthC|wiPCEarR{Y$tCgeXH>4)^iBpk4 z7(i3|I|{>F;lDjFw4EF`?4wiq5|^?)qFpTyiWWNIWh&tb%V%CvuVnpc2m9WR-+)P00SrTEHA*8A{*;^k8sPsqo*9NWr!uCH)nTSO`F-_%z433J8= zV5S!7TK>ZtDO~kL`u6@nd*-(nD@k-Isi0$Kn(k_ZOd2R1Y7j?uPMx_d(=1~* zu>hw3ymb)UFgZT~a>WxJSi;GXi!3~$JEyN6mZ+6py8+LQE*K6jd=d>kBP}nZEocER z5XQbRJnwW)of%I$yAz*HVU|uyEm*5bRD^CCQW+M9>^pj4{9sDwo=^noMnV~3XVh|p zewcLga-obPRb$HbgP}D0lN*|rH0A4CtCz2=Te529s>kXcdi>#sm+{YvwKWeuY4(ll zY@r5@uk-#ATLr7{t6lii+}wS&i{k$&5`4gi{q1#a$|j1iXAtJJBD18ewbU@ zHf*H|lm({+Tk1d9BQ)#wpo}%ix)ko5IdNuc%8*V!fjefip^V-}i}5|iDzX&jVI({{`SzfxiT&zX1LSFkDN(H}_*JVVtn| zP2D5RnxLk4oGM+Qmang{{c1A!64!0=^>?FPmalyH6DWc7kHt&5qcRmNVYEGFL)jG? zu(!`$l2uG&@Ev`D(ut+1y~&MN@dGj7gT3#5D-WsbaIycAEhk<{I3qH~Vum53sS6Dh z3NS2Ew`Tdu#ng>#Q^Al#aq8tORzK(U#*?=W;o%yR=nwaHv7IF)%uP&b414qL4xP5j zQ0j_Y<*_6c&kWf_Vl8)#7BuGZi_~pWaKi(IQ|C*=RG@BCWhQzp={;l1;(!gG!O6hE z0-*SYxn(%G$-|Y|t(ErNNcs;7vE=wja-xHh8`e9sbMBll%}1NEB3)@>j8Ou|gr~aS zG~d7;5zpvPJGT$15ylvbUXPtt6`oDoTifQU;D74GNj6+)0RlhnQP0NeS-iQmI4}^& z9MGlSPW4ay!<1#kYBwP*5y4-gHIp*nwJWF`Hc!no6Mo*L4gU zwPaj>W!M>4d0K_ML`?xEZ7WPL-dgjot5M3p7%L@r*i{=JeG zdd^;utVJiU**racL076^zx#wmKXxAs0%!l2N#a&EvDxM#Dqbu}^In+SxIFB)sUt z5gf)B;G-=ULMkeDbfoF@u+1W^#jO&G+@VOAhD{BWuAM4fdr?cV+T?~}H^EqTtiXrx zi&yG)vj9x(A1WR@Y!=b2(->M7g6qT_=&^TiUKhf5Ma+cQ18Fj1qbOGyF0foLshm}n zpOL@8g$c$EMx>JkdfGb_sQX|E5LwAxPY050G(6rhIk9Kz$UrjolKq)l`lcf~*}ODr zgzCPLwL~HfZ7k@8n>~Tl{o7C|!S-;?rgt@V*q+*OUO8&RG47{vNLVy=_3xBSY3}29W6IalGrT#w@9P5 ztjR59>ygJhN_UPiNl^SIGCmMp3GTiRffWzQimmaaD{l84QvVcvtykD5hsl=Cun+^| z7JENb{m<(VlmK|Jvb^LH1fXfE8jeL3g8P8~BL-Mxn`JOV)%ZLs4JGAk;n%~h`F!}* zW(V_FFEfp-V)iaP6b&t&mnsH1b@Nr2YUp}}E%>RPmgkryvI?{WvrkTHq}X$l*-QX1 z={uu`B`J#R*_w-5I)17!_@WgepF72sprTlL-C79~)7J}QNu0&QH*rMZjg{x7YllDJ zJ5;>BIZ8veu$5=DLfLBPu?F!bdal}mQPwh=CeS7AE1OzKW&LQ?>k{4t8@te`8fdQ? z?KDhNy4^{DSe5_Mwn|uphb4Ff+C6eFTr@jia*$--8H4|5t05AD#g|!h%#369F8hR; zV<~5qZ3xO;(KHt@M2lzOhFB-wBJhMRg2s0ZF3MR{hn!N_Fn0`%>?(MMN`!?Nv*wdKb~FBEU>OLo4R>?Wt= z@xGm%$!~w?6R$f zVM@pW>ED}-qQbkeuPpkB1@Crc+=6j`?fZfnCa)+H_tsIO`L@S4ZT|S$ty9Ip{e?5#W?;KjXbAfxA!( z%Lhl)xHM>N$LNi{#7G}V+BP}cNv9tUCzf7*b16U5c8Q4i~|E<6e0lyjI z0k&`8lfWl{4+9?peiry5u>h+9_$}a{0*?ZZ00*f7H~hum7h&2{;aX95V-PYY88OXz%K)z1%3f|7IfgM{#4y#%N8%4b@3CAtysG(dv)2$WsfX-bnWamR;|}I+}0ZyfM&qE_E;SXWY+ywPSy7 zogJ!B?|fN5lP7_<(x#g5Vso~S;Mw=~qS1nssKR zP3PhBiW9jFVAUvzJGOMLf|t}VJcZ^r#eQIfOf!`|gjqjlmhJ*MH;t&5ITCM@E(!PzP?&KDCi$$I(e{JZ|B|TYB$kE}J*xM%8Ct8p zIalA>TEE2(Et%F?8}iNhRw!9E&3x*$@q!&(!Z{>pF>AMZMvJaGH?y&`)s(0DOtqh> zRFrFrX}H+O3>7gaAUUlhG7`9arDfK>GAEu`cd<#U40xdXjTqm^U7a_rT@dl;Skus%tb>Y%U?D zc2_LT&Y6!tv`RQUkeithnmyR+kGrJj9&)vwQx1M^juvX<|AdvFrF-zONjch&tSNhw zIn%%|R?F#$r}~)3Hh8#(sUP$NrB+@2>N#qyNcS}B7#l@-Yo@97;C*_sQIvsNY77-a zjhYkK%yAX*h#MgZ@7Z%30O9C30PTa{-DM^S>{5omf3Fp{;aqy805k)oQuGKRl5a62 zJq8$fM3I9xYYN^Ql-i6K($1`bN*M*W6M?;Q66+PQc!0d5B`a`}Rq(+tMNikpI!OgB zSavK{;r_VU`YAc+42OrZyUDQ&R`ZZ~cSf6}gS{ghVdZTZd2+i$DR2c69B+9pWptpo z5Eby1R{l$A=v6(&!2}|siO$%aYQic;U&Y#cItV}WevqIv*`PN=w8M#8J{6r=RAxsARX9~P_V0t-}0 zP)#fkuw4p0(WofxgtS@VMnf4MGpvf?o4PB0Yq&NxB-0OmkX+fw6%q+i4yPl7h>LWr zY%S{yBrGrPu^s%S;AMM+CnBx6L79t^4RjVv6;?qx-DQpXZ40V0+1b6X)R=K0J&;7J zC<(olDm2m-c3!Z9OYZhUD0X@8dl5-rhHviTlnI)g@qQIPg;jn!i-fhJa#mS!jXaqh zk`eqwE3*wXwkcXiZ9H@4C{!8Kq_9kt4oUCH=~H67=$OQ(9mrxP9aSvhXcTcIoE1Zw z5*pMfdkDHh%~k_U4G)+?%{g-{^fbLIAD7tB<#GAROa$Hw;m=gFNT<@b_^!iRC^6tb z3!8y{_CV+mLgDKU^=bu3JB&QE&TZ19u{9l#CYIvrtHN2{G7Tz15pkbxrHX7_h=FDT zpjI4}avAv&qq7o3=gb$*#o}x?vBs#{WfeR8sd|J!QW?@SpJeF97!dbMXD&4ZH*RVZhG!yAE^%9|0Z&?g4VZJAvOJ z7H}Lm3>*UP1%3+XCl0U&_(`AzAK!fb-^RzcbN+q_$N_&J_%Yz^`1o%Fj=s67>IkqM z*amzQn83gPQQ+;szh(SJgHHgTHd=g(KPDSuU=Sv^?PX!4_QPT~N@YH@VLvQhg#tA} z@o+Ih5FG-W4why%D~K{fo5@}jrYq8qJcAZ4ob;Mvi6%0O7FN5oAX^Ivc2%68(NFLNPpD?e99$R~o_90T zjLz6;k;>5^3Tiy-YQ6z~5(;OnyD~!z7Jfrp3}f@aHA}>HP$guV)ZH2QK)6rH$wJ_? z6d79ZIa*Mxzw{AMPqkyxb$asV9xEicybB#%8oFcU*Iv}d!?H~wXsC0zbJ+wZElaR9 zKns*iz$`tjW6D_0xpm+%UFj@@&}5TzK*x-C)~sE;HoP+3A%wFYSecy3;m_Ei=g^a5 z$6|OvCU&f(NJgnNIVNVvikGf24}53_6>L$oYPlZUATesmf8*}NS&Bn(kiwQ9ON}We zV%(HScn_kmz2Qj^5z9uGN%Bv>=x)H|tb4>RPo`Ys8TLXtI zbz^92wtnIju#JI8=^pg&Y$8bx-5NxzYl8^C%=l3dNCdKAv4`(`YQ~vDNF|2#rYT9} zHGOtwz)He<8h~Hx%4Yui0Ck2=O6g z(;)S^O)Z2>Ast~pzw5^!`vJy8{Tf;}H9`)aXelFs6TPvyvAvNak3Zw5gw4&-s|iJ} z@u^&MXb$!$fT*S%3zy9-7DipaXn{Txu9W2K6qiye=RJf6^Ur13U~$E-pWB(yz#CxO zOsQuY80r4|%i>g-u!kg#%Q7>~E_vdL8=ldbWBi`$*F&gwQ+uoPVQx(MIQ^IMTzSd| z%4apaEovEco%o@a+2w_r1-T5_K)M20LG62xPm$tjeV1!!%`>x!VwdsW<nBV+riTr@kTG}vmZg~vTvEfmd)E7pv~BVD(q z*Qzu;SRIXhnvlN}mnLihf?Ha(RF8~}V?KeQX)!3@JvT$C*7)kvk#vufV*oqZD>8Oz zSK*Ut^Ol5y_OvX^&B`*9r}c2P`5VH_kVD00mZ)rU@a{_#xtv;d zKvv&ZgEaE;*ISjm@fm|I8w=U3vLubw|KC>?^4a+RALV!Z`8)Xe?*P7xfBz-(@qu3l z+JIJIBhUn#!N)%he3hL49{@M-^REL>0~>%Pz)u0@|G$LKe;zmoYzMXhbAe0v`oD`` zZ~p!S@B;8K@Do4*AOFjM)d3s>-U<9TV0-;Lf&T{||F40+0{%Jh`#>Y`Dn9>5fsX+1 z#Ml3E;QzwU|2^Ot;M2f-pceSuH*sD7&<3Qr{pI}rP5t;rJB;#)1Jj|H<@uJ4`S#W= zxiyN(*d)qT8E_`_jl+d2_(isbQ0LQIFspNW*f>R@HJdoG^V#IjL7yExg4f5mp~X_q(7AVEA6w{17wD`vNipS`QLnK-!DI0{HM<%@-L6vgkWj+I8{0$te3 z+eCFyHm6S2&>nvZ8O2=HoZF}5MCB_|&J)w36u4X2THFBUy<35c4>8yg9ui$yxAw6W ziyy@tGkIbmz@i9slUcj}mUfscvKV?(Y3xO=#Gk(nw@*}4erq$$&t}0%NN^uNJ zcez%scy!qsYoDNcS<8B0^{N$*uGJIUY~yCO?=;D6NeV)sp~Pr^vY%+D70uZJ0k?13 z8)3s*7(&FE!W-=%u!j{<_6k0$OpfCn-=V}YIM04qzI9<2wW5^452WR*p(9>VZJi>$ zvF<(Yrl8;8mik`c4NXfqt=R)kZ6GN_T-j}V($8;*&YT7%*D*GN%JOAW2|!>>HzAE#;|#ZMGM%W-f_p zdQodeFwQFXP5p2t&`Esm^i9ckLpzXyk0Izo4nEmwO4BLnzih{$`$e@ZE?Lq(Z8@@q z-aXNEhyP2O%1GqJfZ4{XSGP89#OlQMU);Lb64dtXm+V z;7aP;j}i+Y0D&$sE4JD)mdf=QfzDh`Iy+0dII;azWJ#}^1AnedL1#5?l_LJ>2Tfj3 zC)SlI`=C!PoaiGv5JcPRk6)baGp2D=89p=eu;4{Ipb%RwE7Z1@Uqt=xK~E8+m}H0g z?t_txtUE3^5fSBUEufSZJKIdVU^VoNhYtX#o>pcWJ!|%6$z-JTz0X{vbCFMmgmG9q zY(}l*G!=Bzlt`4Fywx?DkunS8(>$9DSdFq~wrHieuo0NM9LVoGkm8VHHaz>VHrAB3 z+8*cFrdXiIG-1Iwwqs8k$}DNa>}cbR5Zf1rG#hOO&re-CXG_(nZIjc|h17BEb>!>% z#uC^;05f(Z90r3kBc%u^d=PO=Cq`fSnb+_LBXvooDene2*3i(?qiXq+U-9jMDy7 z#qLOGEBRArl0xgrPh-e(EzmH{ZtGU;7@DN5y0Xe*mqo-LLQyF#=>wVm-hvVG_!y$0 zNVcpH!^O+Td6-fX>8(PKrTia48q2rnD;O$>Zn)7T+6(9NMW)~tWf#&I;WMd~WN zGM-L#|5%a)Hk*Nn$(T5#uiZL^KA9QHCg2)!w4SY2akLo?y{BagKws26qmh*r?HiYd z)I7ms;4>?xg$pLsB`eSHFboKSlKc1Aj2{D$-hb5r$u#zTpPw>^!En+{RKN1BNqMrkyH z2W1WMw3H6D??L_xz7|qS(VInFjf1`=reNc(4$gsa^d%wEvd>na;2%$0h2ou-nq@j^-@a-g3Ir?A52XkbJ2md;E%vFb zO_p_pG(;aCi)Qn;a<z6vyE z&`5rQkCEKt=>(vwsyq?f9kPenB(0@q-C@g;JLvrE4rh1L)z3~6 z3jsyHKZgysp=nuj{ko>SdR*Mz-rBg%yX77aXD_b~aYsp+xELKTec{G7Is3Q8U~?8v zg(x(^lI|}5QjqHZ%?JKX!1Dk1@Q?ir;`_e>{0Dsee-Hc^@OOco`1+l|F9J^h{|2A` zdEhMm{TZNuKc4^(0gHhj1l|JtV|@Hy1HJ$}349UX{{P00|4ZNwkO$TSt@!l4`0s{~ zox%4H@!!|;Zv&8in{OZYUI03PUjY96O;uI@8TfaAX4^0WWI$d;xI z?)N+~UEGOtSgX&ZY;Mc967Xy%9$5axl=xksnV$s8x7G2HxefMhj=Gjj)YRJStf{ie z9yc8p$K8(&1{UX;Sosla8LR75#{eI-V!yBC7!e{LZl`+Km!(qgD7@j}0xD$8iW9+P z9eLW?N<&{#k(sIi<(WV+Q3h2k>q%xPTb|HPTG{k%#Tk4-BWOuEaY)XDf}iOZh0?aP zux8&x-`S_4G8@a_gw=xBsRC$T&k~|EH0ZFx_+YD5e@ML-mJ4%VqbOYIvf6;b9+l6A z@2%)@c0DlIIlQ_N}YvRb2VJ3>GKifr{<*%=5BEST%sjYkl` zPk)PI%LpM4s?}QafRp8K&smlnQyII8>%oOgt!uR)1!X3wi$Jsw;T3yCF<)re*qw>0 z*%a|81ijO2FpItH3OuLA50{4Qi!66-id6y@WcOQ%w*faDx8n-|DNj_jtlY3tq)`m8 z)%7x*tsc@%PqSk%p?tep#7B+HpY=`;Beb+9&iI%uq--USdw=X2kt^?OIzA@*l{N?z zh8jyRs+17Sm;4w}^d%@inLcAbd zi-~!8oRD5(@y_fGjI72MpPOH`PH!_fDxP24AhnMK!;& zcE)g7jSdopK+_K+{{Ed(-|p^iP8t#N^U#6{@mglbBbd~g&w)CkM>(a0@mLUm0~;ejTNu^zIze%Ke9T5L(DWE^u0Cd?FZ zlTk0Wg*6Z#kHDf+xF%N)pg*`Pf|Op{#ld6ZLToY;GZq%}JdKDES<2h>jEKY*@TsJ9 zcbzxa*iZ2c9DsG{*-$WwPd)hvErt`$f3b5U%(alFBc!>=)Un6d^aPzow#i(S0cpti zdN|!vsNdcuY~rMqc}q7D1K!nJinq-mh@TuXdWrKzb!h7TD|)S(MYnkBWs_NF|J32v zlI}x9P;8n(3f65-N7XqP=}1*l71mNBT^z6G4=|$*1@~=2`Myt?FgZ;+p_5Hwr)wZ| zI9h7a(#g?-oE)G=G{GZ)#PVa^SzH*IhNUA%`SxuY$;71I19tWtOY6<6j6>yQta4I?h&`VS zgu#q&CKG~uNH07(l`=NvaHG zclz{}jnbt6rD{q=Q5h#A>z37+&n?`xrw1_G4G+V)+CDIlsHP0E@VG8MEU>X_8V`M{hauW|oLu-gO zX`FZvxJ=@!QzOtm(3aY59@S0-B+4wS9Sj!>6zyi#Q2;j8@d7Uf7dXQpEz*I=eq9=h zup0PD^H_xL#;chwa>jMy=fUPvbB9kSBXEw-M*3mPaqB+?g?_|hfMq8|?F$W|YkAM_R{rEY(H%iC))>!JpgZ8;j@|-Zlf`*oJFDyr$!yDgfKy3Cr?nGBuV zZIQTTmQPe8VMR-AjO+#)n=RPH9+Xn>PaB9R&Cpw#GiHYSZlf@2VmrP|)6Vl6KNz!H zvO8!n2pzUi>iuOF2~yBoY4frjwl;qb$J>(GfBMu|9^fHd*Sh;&Hw*MaFUPV z`yT~*fNmiD2H+3y`}YG|i3M~3n}K$~_5;iXY(GE_*hOrh7uW(k51b-Ka1!_!@bkd? zf%gIbg_yzr3%nPY1AIU5RpJGI0OWw51l|t34X`?amw}H1j{~0}hOh-#2h;=hy@3B6 z*a>t3w}~Z8051aHJC|%2ZhGSS*8Xl={$gNuQ$(k5<)pGUKj^h-ge*z^za1;}tc&Pl0unlXCDaz{gd2C(VzfiICdbZUJ!&o0_8rcw zNN!w-XQRjX0ji3<0g7uku3DpG`z+Mqrq8m)DOuP(WbrTmwk~UTL{t$mo3`;3#_~4a zSQF4ejYexn^B^o7jP1!BCV9n9+Ly@;9`?e}FkQkIg6Z%NY&^?CBRC@-CZ(|rO^>(bSGTro zXszE!)mC~yO4uxvT72qNE3FyB>2)#{r3nm@W7>Ts`Jjw7+on)5gJIyZ@)%|`aq^5G zC>fiI)S*EZmcTSmjD%GaYPnP08?-rMsQi5(3*6oc+tNWtPNebY?HBWEiBM1(59QE8M`MI*skB z8Kyzz0{JzA%--yN8KPN}qnE-xGlz6iP+C@iKFT(NoO3L=@yQKK95dE`R(louYn=}* zIOg24G@jLE+fLYsIW1VR3tKwn%B7=5y_C_xEKv%6JnLwUu$k``+bb;P9!dns9r@y@ z`Dxeva38vh(-^VPH$EZ)xCl^KL(_E8&`f-lCG?IcW^kFUF)~cYMl4*gn?u;g`1q~Z zOSklI=YD$06pWqf*hzYa=!iu!2KI_7G_PNRf$3j90n=<#7IJX%8^La!E{-2i3%sic zPhop!vh%f&=1w)FND@0$FV-?&u*R+)e34&#aNo92?DKWcZ>d|L0PJ*C1zpPj#1ba?PdwM)~m^-gi+Y}im+A&&_B`05+!qnzg z5QHE}|5Z*2h+DTqQa0G=gPhldbl8ICUJtn@{SH!c`%AZP7H=IEI|x=0xZ(K)CIo4X z(ri{`TX{aY@shQ1bjJeFq}eUg7mR`)sz4LP+PM(_V3oD~@qMP52X=pZ`Pl|P?SN-w zJ!p(&YkY|{#YOBQWh|p)C4C+aZUH_c8Y<4A*{mxju6}gBd~9CN(tMg-ngUnHv5C^S zwN@)BHM*GEcbonF^5|Iv$imB)HbkWI(E98l@&qoll={}%VzQMgbY0OQ>4uKilBH|t zR@F+`^}Q35^W@sNJ9r3c9@O+yBMrgIgYgBZoIhsxAm zT3r-sH(7Gzvf?N7+4)QwksLD1Jy?*)4mQ*=1Rr z0Y1%RwiZ^8#pL>j876v=Ot-b~!AUlc$*of`6h;(0CZazH96%!5T5dE-^|yAe#~t4Y zPMSL5crTu<4&<|e+Tq|U>zg-hHop`sbxTd9pBzt>aoWDFrvhr0omX7S#SX*tKqr=& zt&0~}0>?r;W$>^$)Eo1vM`xn)fK_#GJ^C5vY2ccW}E!(gEmtB;1_HJ3M%A`nnl99c3yog8kiDnaRv&h$webPV|OS`ux$L!2GO`k&VLF%4_ zA(}c4KaWBAG4pfr|K|l~cL6^B z58}ta1+e=5Q^00m888p{_xSF=0lWuzH}D1g_Fn~tfI*-YID^mr3&3CEoBsvy=fHmk z{u93W{|Wp@;1BT2zXJR-;IHw;Yk+FtJ;3Mj$3F)=0oa**zl2Zzi@-|YV?YBquLD*B zhX2n4-^rgvwWN8iP=85F^HYr*l;Lzfs!#*>Yr%cYiu{HZ6wmq=uzWQam#P9X7b+p= z+Rm{jekr%H{?je3bsHO-TR6?ZtGMMC$CGol*ull{qIRf~^KdKLTo@`Eb%;K_MmHhT zt|EEdWMatfrt7$nYwe%?jOBJmu=mFn+MDUVX<{d})MJJd;oa3l*@ZLwnbGcYAhgkH zlE=?XG1ri9m-(5_VeRunhIWYlTrU&p~UF0wdArIL$fl{X<(_r(u7! z`E#fqHW8d0nDG^f!q@;u=^6*U;7!Q@Tp+8OGza-4$tUjCScL_%%Ic<66U`fbfINvf zY%+X7;|;43-vjd9Q5Jb}hZQOis<`W0GMh}E*`4o)7|1ChB1B;lrH5e^!46NGWrV`c zd=3uY9=W3P4U)Ztt@(U~ojhyu&`q$s&*Iwd-I`r~{1l@p?C^~W*Sn{VoWn{p4k>6N zPctlxY%B9I*Z_{*Hp+O)W?K5Z@2NwVirbBJv7pj0V{P%ZIT8<-#Vi~CyaykIWLjhD=;>A%V?j1e<@EN-xiQW;w}uG!}`)rh_5hYvEIkkfDs z)hS#lo_vKEP?&SnuKM2b*rVo)6ovjDk+)|EL%>@93S_Y3tY1EPcbrnjNLjizp|euc z9Ta%lTVuyW85)64X>f|LHr}6YpbG-TC%d$(B;~h^2+U25C2r#oC9O5jBbcMGOItQmb{EcZvl;@z$Pv)$zSuOE+kO802h&TZvX|4Q* zrZvyiZwgx)@Mvt%(dO{^LhFu}o(VIidt^3N7SdD7*hLLKX(}s78e*|Ht({kC($3`0 z#n`eA4Vm$vKD@!?_b)?$7P6N!ZKg<%D)GfwrJ3z!_@1*DKl=GGu?J&m`lVY0Ts3R< z3X7+uu8l^XqLMkMuL3A+%q@_xH%^?-1?PpMmQ2z-=!AHf5K`#xdP>-2wUQYLJd_Iy zM=pkfwS6kvrpHy$1BT9(aW(;GzEGXhaR`W)l-KmwhHs`iJ~ua)(qL}-y}6vpM|#E& z%XN9Ho}85eHh+@FjLf{0jBF)c3Myfow=IrkU-%^C?SVt_W@dY}kNNg~Hmh3;uGVQm z`Ytn4V}G!m^?4ANWb4anzNV{|O|$lOfme9f2+vlS{=KwrsC2)KziQf*1I@(AoUM7` z_nMro;@d7}I<}c`?6$q_dnv?d<#(-eeqKsH%B!XYN|m^fMlzyCr6p{Xs@!u6UGp@| zC+_JZiZ-s@orPVmfF$1>3p1Q9&nl<>|5VF~ z-r0xz9CNpk(kp*Xy`+HJJk}@)3eOT>**EF;Y4%5Y&4%?-*JjhAxI&lwgV(F z$wgOFxr)fJWLS{HU$=H{3?;Ytm}U3kO`A}|ar!6;!~@=%38wu{+K022%qpQ-9wCm9 zv&0W9lDKI~srsdK&2}T=VFeZN+78SeJ;ubJ9*qV1l#I=&BYF&F(x|M#a4w)&7xo*u z$V#v1x;}g*QI176%u>Wt=flvE`~f|WD;xPcJ4eT|%mlC}tDqmm6~eSJTO&ihN8OkW zA6Lk&+0xdY-w0J=PEXB6y{!;huoivu2P-8C(T$f-VW^y)t9%SRUlYT@&5HKG0_-{> z>V}^uBy}VsjDpEi9sPEgxgU%(Z{D<+8{>x02ZFdRLnwlMsO801BL%!5oz>mIpjqD| z)lgt3Wh!>&p^|{hE$WrE>6D6*smZOl%T*S$LXBdm^^$rnutrpN(p$w9@9Nv6CP0V} zH~iSBQwl8vHk)a5m^f%)zYo5e3MxKr7Cv8*uAq`QohoRVSIk`~16xvr_nAf|Ql8-g zI0QD1p5|-fV4^%8zxeDC6XMJPMO)B$%d(~EtWoIACJnEdy_9qR|L0f^f7X0w{QsY^ zwUM8Hj^F?Lz&`@+0O@&te}S+6=fK|rz7Hti=O@6g;pcw=_`|o5zXz@XZv$*U|0DSM zdHnnJKm)K2SPiTK?7Y5T1fBqX1o&a#m+}2S3w#F11M7jW;Q!nCeTRX!06zfyF)@HY z0(yXMz|QOcSB#?vSOq)^JOVrfECxOV*f<{qzAZoV=FJO%y#8&aj_tHL&r}u`d|30* z)cys#a6D%8tb~UM!FqYCyMV5K9gQPOH2tC2<>29aj&9$ln zBw`xwMiXD_CEty+c{%Y4ju)nky5)6B;_Oo^M=n-Bl3(9gzocb-z5#K5_~DJ4@*B{s zd~_I#f9KTMy=*qHeU^@au2|su^j6`kb_#4^ch7|Gg#I+HHm#~=Eo0h_@Va{t)-|=P zn;$kJJlnWF-;z4>b(>mS8uD#zwa?|(ZOX1;LWlkdk6}E54yuRgEeDr*Ew0Ge3l4W$ zhm#`fTgWl{#3Hq5>S2OU|Jj#i<-mxOddB=$td7V!Wmx^Y@KM3gcl2j8GnrI3UJaD< zS7&!pcwx{8d4y9PoKcy6(%GHEA#x5Oky%!mbLsw5);$3GFiNs9=5LPe%xx;-`#KKx=EbFYwgu@4oilf;s&c0xz-^g(uR+FN6zyn@kI zymg|`dpPMoZ8p*X8*4^XF1ToEq5BR23X~BKT0rCU4c*aowu>lDXA7qqK6`|Q2`aPX zAuT>;jzPIM&>(0M7%ld!Ugzf`85x~jh`&}ii5clrjsm4?t2IFoM)opklG{V7$d&0x z7Ya4ZXpwiqbWPVx$ZTP(9#fNaqj;<}O`_JOFvn9;gcc{w?Il2_{1 zJpS0ji0zl}($J`++=C&yijcKf}^WVeSnlAH?UvV&g9F1MW8k zQg{jNE z`3oqz@$Ul3d4Lh_X315 zDWI(-ex{j97sL8u)nJ}q;r1i-&*Ycr95ahI1jl+hr^2=HR&G6x?OdZ#dV!YL`-L=O z(V)mh&4airO1&6?{Od1;9sPXhl%R+{*JPYepD{7HwGEHa65zg3+p^id(^`*(>o-pk zR<$gg?-YRe{6VtZJl#O@n3SU*{?$K48?f9sDGK-bQmVFeKzX(w?j0SBb5}EF~ z&)SROt-2*ESJ*eZIB#_g=dRApEu72U1^0Op?a@__F3bJOgD7d80kKehfGeYsFn)$F1Bw@$IRjFhHj%;V=`_nNwrT{l{QqYBVXJvJ|G$QR?5CIf|674SA=m%M zKp(Ih_#|)-P&D5k_;uiCfd_#q;7h;(U_Vd|ya)JQVgtVed>*hf|9+R)!0!On zfYk=v0KRJV0Kiv(e+T?q;0Tb$1zrKi%Rsh9U)Wau|NkEaEVber$4ob6KJaPN^uQ6; zJxeza?A3hLH#)=DHa+Qv(3Or%VDzEUsrIzOqhljeM{gs*`X21$_(alsDe2{ya)QdY z>~ry6&W9UeAHXyNVR{wQXFR=O`R%$@x^tN)LrU3SA#7}QaIE5f(iT`)YT657?SOcK z>i!s$guD^HcQSU0?NsskY~w86RzJfwD}Q)u|B=%0FrEdU5h@IxwKeJ3ZQ56l>W+3M zZyip0?=U7A;9MzgeXVe9oM0zrtoB|(0|-N2%?OO0e)P6QK>d@c)}SBfAB-;_GH$)b ztGofVlx{!|D#uaSLs`X6Pv0DkJ|hkRM|oSU(c&Gpk3j9)NIgMke4t+Tb~n9qNciL! zCAZ^tdQtzG(3gKm+L%KaW#WK3<@K^5Q;JZy!UM)HLZJF!w9at~?cfJ`SW8uo^b1y~ zC!;F)om}C(Ee^lC` zmkY;FabGwJ{n>;k=)Pj(MhlCz=W`p0j0LKB^{NU$gSF<>kjf&3oTNB6x+@qjP69J0 zJ(C9#>KPQw`qdE%{t;U&J#0q%4Dr!{(xHC&;zl*B>;AnKWhPOF)6AD@iGXxp4^$3J zvQKWSsF9W}d$ONGG=i4XuQO-ZT&PV0gtx7oS+wSrsiXatpQnnRvVuJqix(_9qpysY zuJ;lL;s4^z4BX7}_dK5mFO1DZ{?qmgdr0VBpz+z1_7M9?>^l=y7bbK1I2h!@i$?Pa zi+70Y$+=6M14Sbe@i;dY)h>(1nkP@ShH^r5`AZnDL9wiQv`L&-XnpktDRInkqFp>xC*Erxizod&|3E zWhhY*&h{7IzHBEUq}_*LhC=tssi(EkDsQDV=x0;L>8&wkcVGmKp-%_-386wyXs3L$ zgT{#(dI1d40V3(&wsU9(J)?Y!ui(2BrzwQtYex5s(onWK ztM)9~DpMmtM{N+>(I#hX{5hG|=@KHm}-#NZ}LYx4OQ_&@Jy z4KlJ|Z;$szGlXOjJ6}`|KAM{IdhCcihk%F;iO z6SN#@<%vDJ|Nlw+{9gvl|0k{PKmUclelJv^8XF^`JWH| z|C<6M?eEV4Un19E`KeH#LV*eeDio+tphAHP1u7J%P@qDA3I!?@_^zP<)!?y2GS-<; zDW{w|`TlsIAvt@?P|w;^|1F;jo~eyvy9ND`nI}gbYBMUwg1=fgW!v^=91FheJI*)r z|NkEQ`|APA|5L{AKmQcp|DOOifqlSzzfoV=6@ToF1-6B|Nc6$f`1JBCXLwH zfVMB7@>8Keg#r}{R47oPK!pMo3REaip+JQK|ASEAyXl+Qw(0wC=oh-bn}`10j2mYc K{Eh!``hNw5CD)Sx literal 0 HcmV?d00001 diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index d2156032..61803e2e 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -89,7 +89,9 @@ OpenGL是个状态机,有很多状态变量,是个标准的过程式操作 OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Context,Client提出渲染请求,Server给予响应。之后的渲染工作就要依赖这些渲染状态信息来完成,当一个上下文被销毁时,它所对应的OpenGL渲染工作也将结束。 -OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 +OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。 + +EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 ### 对象 OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中的一个。 @@ -102,8 +104,8 @@ OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中的 ``` C struct object_Window_Target { float set_window_size; // 这里调用的是OpenGL提供的设置窗口大小的某个方法 - float set_window_color; // 这里调用的事OpenGL提供的设置窗口颜色位数的某个方法 - .... + float set_window_color; // 这里调用的是OpenGL提供的设置窗口颜色位数的某个方法 + .... } ``` @@ -123,7 +125,7 @@ struct OpenGL_Context { - 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦。 - 这时候我们就需要一些小助理(对象),来帮忙记录某些状态信息,以便复用。 -如果我们有10中子集,那就需要10个小助理(对象),而当前状态(Context,只有一份),可以通过装配这些对象来完成。 +如果我们有10种子集,那就需要10个小助理(对象),而当前状态(Context,只有一份)可以通过装配这些对象来完成。 ![image](https://github.com/CharonChui/Pictures/blob/master/opengl_zhuli.jpg?raw=true) @@ -303,7 +305,7 @@ Java使用: ```java GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); ``` -从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。 +从调用glBindBuffer()这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。 然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中(小助理把对应的数据单独记录下来了)。 ```C @@ -357,7 +359,9 @@ glBufferData是一个专门用来把用户定义的数据复制到当前绑定 - OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型(每一个缓冲类型类似于前面说的子集,每个VBO是一个小助理)。 - 配置OpenGL如何解释这些内存 - 通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。 + + +通过顶点数组对象(Vertex Array Objects, VAO)来管理,数组中的每一个项都对应一个属性的解析。 VAO(Vertex Array Object)是指顶点数组对象,主要用于管理 VBO 或 EBO ,减少 glBindBuffer 、glEnableVertexAttribArray、 glVertexAttribPointer 这些调用操作,高效地实现在顶点数组配置之间切换。 @@ -455,7 +459,7 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 光栅化操作构造了像素点(图元(点或三角形)转换成了像素的集合),这个阶段就是处理这些像素点,根据自己的业务,例如高亮、饱和度调节、高斯模糊等来变化这个片元的颜色。 为组成点、直线和三角形的每个片元生成最终颜色/纹理,针对每个片元都会执行一次,一个片元是一个小的、单一颜色的长方形区域,类似于计算机屏幕上的一个像素。 -一旦最终颜色生成,OpenGL就会把它们写到一块成为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 +一旦最终颜色生成,OpenGL就会把它们写到一块称为帧缓冲区的内存块中,然后Android就会把这个帧缓冲区显示到屏幕上。 通常在这里对片段进行处理(纹理采样、颜色汇总等),将每个片段的颜色等属性计算出来并送给后续阶段。 @@ -551,7 +555,7 @@ EGL是OpenGL和本地窗口系统(Native Window System)之间的通信接口 - 创建绘图表面; - 在OpenGL ES 和其他图形渲染API之间同步渲染; - 管理纹理贴图等渲染资源。 -OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异(Apple 提供了自己的 EGL API 的 iOS 实现,自称 EAGL)。 +OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/egl_interface_1.png?raw=true) @@ -615,15 +619,9 @@ Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个 纹理(Texture)是一个2D图片(也有1D和3D纹理),他可以用来添加物体的细节,你可以想象纹理是一张绘有砖块的纸,无缝折叠贴合到你的3D房子上,这样你的房子看起来就有砖墙的外表了。 -Android 通过其框架 API 和原生开发套件 (NDK) 来支持 OpenGL。 - -Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建和操控图形:`GLSurfaceView` 和 `GLSurfaceView.Renderer`。 - - #### 视频数据流 -视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为 -已解码数据的载体,保存播放器解码后的数据。 +视频从视频文件到屏幕显示出来,Surface起到了非常重要的作用,在视频解码时,需要一个Surface作为已解码数据的载体,保存播放器解码后的数据。 在显示时,需要另一个Surface作为已渲染数据的载体,保存OpenGL渲染后的数据,最后通过EGL显示在屏幕上。 diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index cea103bb..7a17ce67 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -20,7 +20,7 @@ Android 框架中有如下两个基本类,用于通过 OpenGL ES API 来创建 ### GLSurfaceView(使用OpenGL绘制的图形的视图容器) -SurfaceView在View的基础上创建了独立的Surface,拥有SurfaceHolder来管理它的Surface,渲染的工作可以不再主线程中做。可以通过SurfaceHolder得到Canvas,在单独的线程中,利用Canvas绘制需要显示的内容,然后更新到Surface上。 +SurfaceView在View的基础上创建了独立的Surface,拥有SurfaceHolder来管理它的Surface,渲染的工作可以不在主线程中做。可以通过SurfaceHolder得到Canvas,在单独的线程中,利用Canvas绘制需要显示的内容,然后更新到Surface上。 GLSurfaceView继承自SurfaceView,它主要是在SurfaceView的基础上加入了EGL的管理,并自带了一个GLThread的绘制线程,绘制的工作直接通过OpenGL在绘制线程进行,不会堵塞主线程,绘制的结果输出到SurfaceView所提供的Surface上,这使得GLSurfaceView也拥有了OpenGLES所提供的图形处理能力,通过它定义的Render接口,使更改具体的Render的行为非常灵活,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView既可。也就是说它是对SurfaceView的再次封装,为了方便我们在安卓中使用OpenGL。 @@ -128,7 +128,8 @@ class MyGLRenderer implements GLSurfaceView.Renderer { ``` 效果图就不贴了,就是一个纯绿色的Activity。 -上面glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色 +上面glClearColor函数是一个状态设置函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该清除为的颜色 + Render接口重写的三个方法中调用了GLES31的API方法: - glClearColor():设置清空屏幕用的颜色,参数为RGBA。 @@ -190,11 +191,22 @@ GLSurfaceView默认采用的是RENDERMODE_CONTINUOUSLY连续渲染的方式, 说到GLSurfaceView就一定要提一下SurfaceTexture。 -和SurfaceView功能类似,区别是,SurfaceTexure可以不显示在界面中。使用OpenGl对图片流进行美化,添加水印,滤镜这些操作的时候我们都是通过SurfaceTexre去处理,处理完之后再通过GlSurfaceView显示。缺点,可能会导致个别帧的延迟。本身管理着BufferQueue,所以内存消耗会多一点。 -SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。 SurfaceTexture 可以将 Surface 中最近的图像数据更新到 GL Texture 中。通过 GL Texture 我们就可以拿到视频帧,然后直接渲染到 GLSurfaceView 中。 +和SurfaceView功能类似,区别是,SurfaceTexure可以不显示在界面中。使用OpenGl对图片流进行美化,添加水印,滤镜这些操作的时候我们都是通过SurfaceTexre去处理,处理完之后再通过GlSurfaceView显示。 + +缺点,可能会导致个别帧的延迟。本身管理着BufferQueue,所以内存消耗会多一点。 + + +SurfaceTexture从图像流(来自Camera预览,视频解码,GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象,接下来,就可以像操作普通GL纹理一样操作它了。 + +SurfaceTexture可以将Surface中最近的图像数据更新到GL Texture中。通过GL Texture我们就可以拿到视频帧,然后直接渲染到GLSurfaceView中。 + +通过 **setOnFrameAvailableListener(listener)** 可以向 SurfaceTexture 注册监听事件,当Surface有新的图像可用时,调用SurfaceTexture的**updateTexImage()**方法将图像内容更新到GL Texture中,然后做绘制操作。 + +SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。 + +SurfaceTexture对象可以在任何线程上创建。 -通过 **setOnFrameAvailableListener(listener)** 可以向 SurfaceTexture 注册监听事件,当 Surface 有新的图像可用时,调用 SurfaceTexture 的 **updateTexImage()** 方法将图像内容更新到 GL Texture 中,然后做绘制操作。SurfaceTexture中的attachToGLContext()和detachToGLContext()可以让多个GL context共享同一个内容源。 -SurfaceTexture对象可以在任何线程上创建。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。 在任意线程上调用frame-available回调函数,不与updateTexImage()在同一线程上出现。 +updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。 在任意线程上调用frame-available回调函数,不与updateTexImage()在同一线程上出现。 From 36fa9dabe32769803b8fde2ca707d1103c86ca5a Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 30 Apr 2024 19:39:34 +0800 Subject: [PATCH 085/128] add python part --- .../python3\345\205\245\351\227\250.md" | 194 ++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index eb897476..d0da679a 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -301,4 +301,198 @@ print(alien['points']) alien['xPos'] = 10 alien['yPos'] = 20 print(alien['xPos']) +del alien['points'] ``` + +### 遍历字典 + +- keys()方法返回所有的键列表。 +- items()方法返回一个键-值对列表。 +- values()方法返回一个值列表。 + +```ptyhon +alien = {'color': 'blue', 'points': 5} + +for key,value in alien.items(): + print("key: " + key) + print("value: " + str(value)) + + +for key in alien.keys(): + print("key: " + key) +``` +字典总是明确地记录键和值之间的关联关系,但获取字典的元素时,获取顺序是不可预测的。 + +这不是问题,因为通常你想要的只是获取与键相关联的正确的值。 + +要以特定的顺序返回元素,一种方法是在for循环中对返回的键进行排序。。 + +为此,可使用函数sorted()来获得特定顺序排列的键列表的副本: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +for name in sorted(favorite_languages.keys()): + print(name.title()+",thank you for taking the poll.") +``` + +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in favorite_languages.values(): + print(language.title()) +``` + +这种做法提取字典中所有的值,而没有考虑是否重复。涉及的值很少时,这也许不是问题,但如果被调查者很多,最终的列表可能包含大量的重复项。为剔除重复项,可使用集合(set)。集合类似于列表,但每个元素都必须是独一无二的: +```python +favorite_languages = { + 'jen':'python', + 'sarah':'c', + 'edward':'ruby', + 'phil':'python', + } +print("The following languages have been mentioned:") +for language in set(favorite_languages.values()):❶ + print(language.title()) +``` +通过对包含重复元素的列表调用set(),可让Python找出列表中独一无二的元素,并使用这些元素来创建一个集合。 + +## 用户输入 + +函数input()让程序暂停运行,等待用户输入一些文本。 + +获取用户输入后,Python将其村村在一个变量中,以方便你使用。 +```python3 +age = input("How old are you?") +print(age) +``` +用户输入的是数字21,但我们请求Python提供age的值时,它返回的是`'21'`(用户输入的数值的字符串表示)。 + +为了解决这个问题,可以使用函数int():将数字的字符串表示转换为数值表示,如: +```python +age = input("Please Input") +print(age) +# print(age >= 18) 报错 +age = int(age) +print(age >= 18) +``` + +## while + +```python +current_number = 1 +while current_number <= 5: + print(current_number) + current_number+= 1 +``` + +同样while中也可以结合使用break、continue等,和Java基本一样。 + + +## 函数 + +使用关键字def来定义一个函数,定义以冒号结尾。 +跟在def xxx:后面的所有缩进行构成了函数体。 + +```python +def greet_user(username): + # 返回值 + return 'Hello ' + username + +# 调用函数 +print(greet_user('jack')) +print(greet_user(username='lili')) +``` +同样,在Python中函数也支持参数的默认值。 + +### 函数列表参数副本 + +将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。 + +但是有些时候我们并不想让函数修改原始的列表。 + +为解决这个问题,可向函数传递列表的副本而不是原件;这样函数所做的任何修改都只影响副本,而丝毫不影响原件。 + +要将列表的副本传递给函数,可以像下面这样做: +``` +function_name(list_name[:]) +``` + +切片表示法[:]创建列表的副本。 + +### 函数不定参数 + +有时候,你预先不知道函数需要接受多少个实参,Python允许函数从调用语句中收集任意数量的实参。 +```python +def make_pizza(*toppings): + """打印顾客点的所有配料""" + print(toppings) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` +形参名`*toppings`中的星号让Python创建一个名为toppings的空元组,并将收到的所有值都封装到这个元组中。 + +现在,我们可以将这条print语句替换为一个循环,对配料列表进行遍历,并对顾客点的比萨进行描述: + +```python +def make_pizza(*toppings): + """概述要制作的比萨""" + print("\nMaking a pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +make_pizza('pepperoni') +make_pizza('mushrooms','green peppers','extra cheese') +``` + +### 将函数存储在模块中 + +函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。 + + +要让函数是可导入的,得先创建模块。模块是扩展名为.py的文件,包含要导入到程序中的代码。下面来创建一个包含函数make_pizza()的模块。为此,我们将文件pizza.py中除函数make_pizza()之外的其他代码都删除: + +```python +# pizza.py + +def make_pizza(size,*toppings): + """概述要制作的比萨""" + print("\nMaking a "+str(size)+ + "-inch pizza with the following toppings:") + for topping in toppings: + print("- "+topping) +``` + +接下来,我们在pizza.py所在的目录中创建另一个名为making_pizzas.py的文件,这个文件导入刚创建的模块,再调用make_pizza()两次: + +```python +# making_pizzas.py +import pizza +pizza.make_pizza(16,'pepperoni') ❶ +pizza.make_pizza(12,'mushrooms','green peppers','extra cheese') + +``` +Python读取这个文件时,代码行import pizza让Python打开文件pizza.py,并将其中的所有函数都复制到这个程序中。你看不到复制的代码,因为这个程序运行时,Python在幕后复制这些代码。你只需知道,在making_pizzas.py中,可以使用pizza.py中定义的所有函数。 + +你还可以导入模块中的特定函数,这种导入方法的语法如下: +`from modulname import funcxx` + +```python +from pizza import make_pizza +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +若使用这种语法,调用函数时就无需使用句点。由于我们在import语句中显式地导入了函数make_pizza(),因此调用它时只需指定其名称。 + + + + + + From 25830ce253b27250b165bd15dbd4a2029da5d656 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 30 Apr 2024 20:28:15 +0800 Subject: [PATCH 086/128] add python --- .../python3\345\205\245\351\227\250.md" | 107 ++++++++++++++++++ VideoDevelopment/.DS_Store | Bin 6148 -> 0 bytes 2 files changed, 107 insertions(+) delete mode 100644 VideoDevelopment/.DS_Store diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index d0da679a..18b87df7 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -404,6 +404,7 @@ while current_number <= 5: ```python def greet_user(username): + """函数功能的注释""" # 返回值 return 'Hello ' + username @@ -413,6 +414,8 @@ print(greet_user(username='lili')) ``` 同样,在Python中函数也支持参数的默认值。 +上面三个引号的部分是文档字符串格式,用于简要的阐述其功能的注释。 + ### 函数列表参数副本 将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。 @@ -491,8 +494,112 @@ make_pizza(12,'mushrooms','green peppers','extra cheese') ``` 若使用这种语法,调用函数时就无需使用句点。由于我们在import语句中显式地导入了函数make_pizza(),因此调用它时只需指定其名称。 +还可以使用星号(`*`)导入模块中的所有函数: + +```python +from pizza import * +make_pizza(16,'pepperoni') +make_pizza(12,'mushrooms','green peppers','extra cheese') +``` +`import *`会将模块pizza中的每个函数都复制到这个程序文件中。 +由于导入了每个函数,可通过名称来调用每个函数,而无需使用句点表示法。 +然而,使用并非自己编写的大型模块时,最好不要采用这种导入方法: 如果模块中有函数的名称与你的项目中使用的名称相同,可能导致意想不到的结果,因为Python可能遇到多个名称相同的函数或变量,进而覆盖函数,而不是分别导入所有的函数。 + + +### 别名 + +如果要导入的函数的名称与现有的名称冲突,或者函数的名称太长,可以通过别名的方式进行指定。 + +```python +from pizza import make_pizza as mp +mp(16,'pepperoni') +mp(12,'mushrooms','green peppers','extra cheese') +``` + +同样也可以通过as给模块指定别名: + +```python +import pizza as p + +p.make_pizza(16,'pepperoni') +p.make_pizza(12,'mushrooms','green peppers','extra cheese') +``` + + +## 类 + +在Python中,首字母大写的名称指的是类。 +根据类来创建对象叫实例化。 + +dog.py +```python +class Dog: + def __init__(self, name, age): + self.name = name + self.age = age + + def sit(self): + print(self.name + " Sitting") + + def roll(self): + print(self.name + " Rolling") +``` + +__init__()是一个特殊的方法,每当你根据Dog类创建新实例时,Python都会自动运行它。在这个方法中形参self必不可少,还必须位于其他形参的前面。 + +为什么必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法来创建Dog实例时,将自动传入实参self。 + +每个与类相关联的方法调用都自动传入实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。 + +```python +import dog + +dog = dog.Dog("xiaohei", 1) +# 属性 +print(dog.name) +# 方法 +dog.sit() +``` +### 继承 +一个类继承另一个类时,它将自动获得另一个类的所有属性和方法。 + +创建子类实例时,Python首先需要完成的任务是给父类的所有属性赋值。因此,子类的__init__()方法需要调用父类的方法。 + +```python +class Car: + def __init__(self, make, model, year): + self.make = make + self.model = model + self.year = year + + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + return name.title() + + +class ElectricCar(Car): + def __init__(self, make, model, year, battery): + super().__init__(make, model, year) + self.battery = battery + + # 重写父类方法 + def get_des(self): + name = str(self.year) + str(self.make) + str(self.model) + str(self.battery) + return name.title() + def get_battery(self): + print("battery : " + str(self.battery)) + + +tesla = ElectricCar('Tesla', 'Model S', 2021, 80) +print(tesla.get_des()) +tesla.get_battery() +``` +- 创建子类时,父类必须包含在当前文件中,且位于子类前面。 +- 定义子类时,必须在括号内指定父类的名称 +- 方法__init__()接受创建子类实例所需的信息 +super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让子类包含父类的所有属性。 diff --git a/VideoDevelopment/.DS_Store b/VideoDevelopment/.DS_Store deleted file mode 100644 index a4c639de123dfb29e6f4f862ffdadea92d776a31..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z<-5O({YS3OxqA7ObrlikA>8UIf929#m>Vf(B!@G^sh1LeBa^K8erc z%UfJ=99;7$63S z0b*eD7|>^d(cQe#sbXS)82Et!+#f7xh_=Q;q1-y4!|OBpn}{f&<68pJ(r9Ze6oLnY zt5iUh%Jmb2t8}niI?mQuC{*c;%avgsy>j_@;c|7bTQZz+TOswt05MQyprM8)p8u!t z%hW#dt0^=h28e-w#sF_l-Khgb>9h4)d3e@Z&>o(^b From a902bd854b861f5fd9a53d8d90a3edd02c993154 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 May 2024 13:59:51 +0800 Subject: [PATCH 087/128] update python part --- .../python3\345\205\245\351\227\250.md" | 131 +++++++++++++ .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 182 ++++++++++++++++++ 2 files changed, 313 insertions(+) diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 18b87df7..b3d4ae5f 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -603,3 +603,134 @@ tesla.get_battery() - 方法__init__()接受创建子类实例所需的信息 super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让子类包含父类的所有属性。 + + +## 标准库 + +Python标准库是一组模块,安装的Python都包含它。 +类名应采用驼峰命名法,即将类名中的每个单词的首字母都大写,而不使用下划线。实例名和模块名都采用小写格式,并在单词之间加上下划线。 +每个模块也都应包含一个文档字符串,对其中的类可用于做什么进行描述。 + + +### 读取文件 + +一次性读取整个文件: +```python +with open('test.txt') as file_object: + contents = file_object.read() + print(contents) +``` + +要以每次一行的方式检查文件,可对文件对象使用for循环: + +```python +filename = 'test.txt' +with open(filename) as file_object: + for line in file_object: + print(line) +``` +这里使用了关键字with,让Python负责妥善地打开和关闭文件。 +使用关键字with时,open()返回的文件对象只在with代码块内可用。 + + +要将文本写入文件,你在调用open()时需要提供另一个实参,告诉Python你要写入打开的文件。 + +```python +filename = 'test.txt' + +with open(filename, 'w') as fo: + fo.write("Hello World") +``` +调用open()时提供了两个实参: + +- 第一个实参是要打开的文件的名称 +- 第二个实参('w')告诉Python,我们要以写入模式打开这个文件。 + +打开文件时,可指定读取模式('r')、写入模式('w')、附加模式('a')或让你能够读取和写入文件的模式('r+')。如果你省略了模式实参,Python将以默认的只读模式打开文件。 +如果你要写入的文件不存在,函数open()将自动创建它。然而,以写入('w')模式打开文件时千万要小心,因为如果指定的文件已经存在,Python将在返回文件对象前清空该文件。 +注意 Python只能将字符串写入文本文件。要将数值数据存储到文本文件中,必须先使用函数str()将其转换为字符串格式。 + +如果你要给文件添加内容,而不是覆盖原有的内容,可以附加模式打开文件。你以附加模式打开文件时,Python不会在返回文件对象前清空文件,而你写入到文件的行都将添加到文件末尾。如果指定的文件不存在,Python将为你创建一个空文件。 + + +### 异常 + +异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。 + + +### 分割字符串 + +方法split()以空格为分隔符将字符串分拆成多个部分,并将这些部分都存储到一个列表中。 + +```python +title = "Alice in Wonderland" +title.split() +``` + +['Alice','in','Wonderland'] + + +### pass + +Python有一个pass语句,可在代码块中使用它来让Python什么都不要做。 + + +### json + +函数json.dump()接受两个实参:要存储的数据以及可用于存储数据的文件对象。下面演示了如何使用json.dump()来存储数字列表: + +```python +import json +numbers = [2,3,5,7,11,13] +filename = 'numbers.json' +with open(filename,'w') as f_obj: + json.dump(numbers,f_obj) +``` + +使用json.load()将这个列表读取到内存中: +```python +import json +filename = 'numbers.json' +with open(filename) as f_obj: + numbers = json.load(f_obj) +print(numbers) +``` + +### 网络请求 + +Web API是网站的一部分,用于与使用非常具体的URL请求特定信息的程序交互。这种请求称为API调用。请求的数据将以易于处理的格式(如JSON或CSV)返回。依赖于外部数据源的大多数应用程序都依赖于API调用,如集成社交媒体网站的应用程序。 + + + +requests包让Python程序能够轻松地向网站请求信息以及检查返回的响应。要安装requests,请执行类似于下面的命令: + +$pip install --user requests +或者直接在ide中点击修复安装就可以: + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/python_install_requests.png) + +```python +import requests +# 执行API调用并存储响应 +url = 'https://api.github.com/search/repositories?q=language:python&sort=stars' +r = requests.get(url) +print("Status code:",r.status_code) +# 将API响应存储在一个变量中 +response_dict = r.json() +# 处理结果 +print(response_dict.keys()) +``` + +执行结果为: +``` +Status code: 200 +dict_keys(['total_count', 'incomplete_results', 'items']) +``` + +这个API返回JSON格式的信息,因此我们使用方法json()将这些信息转换为一个Python字典。我们将转换得到的字典存储在response_dict中。 +最后,我们打印response_dict中的键。 + + + + + diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index 9c15dc6c..0bea9e77 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -1,6 +1,188 @@ # 13.LUT滤镜 +LUT全称LookUpTable,也称为颜色查找表,它代表的是一种映射关系,通过LUT可以将输入的像素数组通过映射关系转换输出成另外的像素数组。 + +比如一个像素的颜色值分别是R1 G1 B1,经过一次LUT操作后变为R2 G2 B2。 + +通过这个映射关系就可以将一个像素的颜色转换为另外一种颜色。 + + +LUT从查找方式上可以分为1D LUT和3D LUT: + +- 1D LUT + +对于一张RGB图像,每个通道都可以作为一个输入,用公式可以描述如下: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1d.jpg) + + +由于颜色值的范围是(0~255),我们可以用`256*3` 的表来表示一个1D LUT,在实际操作中,我们通常以一张`256*3*1`的图片来存储这个映射表,如下,这是一张1D LUT在Mac访达中文件信息图: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1d_map.jpg) + + +由于1D LUT各个通道都是相互独立的,无法对其他通道产生影响,因此1D LUT 只能用来调节亮度/伽马/饱和度/色彩平衡等,如果我们希望对其他通道产生影响,就需要了解另一种滤镜查找方式--3D LUT。 + + + + +- 3D LUT + + 3D LUT在滤镜中的影响比1D LUT更为深刻,下面我们用一张图(图是网上找的,参考“参考资料”)来说面3D LUT,假设下图的红色平面发生移动,其中对应的绿色和蓝色分量也会发生改变,也就是说,一个颜色通道改变可以影响其他的颜色通道,理论上 3D LUT 可以在立体色彩空间中描述所有颜色调整行为,所以它可以处理任何显示的非线性属性,从简单的 gamma 值、颜色范围和追踪错误,到修正高级的非线性属性、颜色串扰(去耦)、色相、饱和度、亮度等,3D LUT都可以胜任。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_3d.webp) + +常见的3D LUT滤镜文件与.cube或者.3dl都是把3D坐标二维化后的数据表现,如下图 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/3d_lut.jpg) + + + + + + + + 我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝。 + + 对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为256*256*256,如果我们有一个大小为256*256*256的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张256*256*256的图实在是太大了,至少需要48MB的存储空间,因此,通常会通过降低采样的方式来减少数据量。 + + + + +我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝。 + 对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为256*256*256,如果我们有一个大小为256*256*256的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张256*256*256的图实在是太大了,至少需要48MB的存储空间,因此,通常会通过降低采样的方式来减少数据量。在专业领域一般认为17x17x17的3D LUT足够适用于预览和监看;65x65x65或者更大的3D LUT更适合渲染和调色。3D LUT的实现在计算机环境会相对容易,在嵌入式环境就意味着成本的提高,越大的LUT需要越昂贵的硬件支持。所以行内比较常用的LUT box一般都不是很大,比如说BMD的HDLink Pro采用了17x17x17的3D LUT(495美元);Fujifilm的IS-Mini则采用了26x26x26的3D LUT(865英镑)。 + 下面我们就以一个64*64*64的3D LUT为例,来说明一下3D LUT如何查找和使用的(用glsl来实现),一张3D LUT如下: + + + + + +肉眼可见的有8*8个方格,假设整个图的范围我们定义为0.0~1.0 ,也就是说每个格子所表示的范围是1./8. = 0.125, 我们首先通过蓝色来查找我们在哪个方格,由于涉及到插值,因此通过蓝色进行查找的时候,可能不会那么幸运一定映射到整数的格子上面,因此我们需要通过向下取整和向上进位两种方式来获取蓝色所对应的方格,然后通过蓝色的颜色值来mix这两种的颜色值,从而获取最终的颜色值,glsl实现如下: + + +## 为什么使用LUT滤镜 + + + +在正常情况下,8位的RGB颜色模式可以表示的颜色数量为256X256X256种,如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64X64X64种就可以表示原来256X256X256的颜色数量,我们也将4称为采样步长。 +要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的: +lut映射关系图 +首先这张图的大小是512X512,在横竖方向上这张图都被分成了8个小方格,每个小方格的大小是64X64,也就是一张512X512的图被分割成64个小方格,每个小方格的大小是64X64,这64个小方格就代表了64种B通道的颜色映射, 然后每个B通道的小方格上又是一个64X64像素大小的图像,这个小图像的横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况,这样就刚好这就和采样步长是4的映射表对应上了。 +在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小个子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。 + + + +对输入图颜色转换时,以颜色(rgb)的b值作为索引,找到所属于的小格。 +最后根据r和g的值在小格中定位到映射的目标值上(两个格子上对应的像素进行插值)。 + +最后根据r和g的值在小格中定位到映射的目标值上(两个格子上对应的像素进行插值)。 + + +每个小方格的横向有512 / 8 = 64个像素,表示间距为4的R通道,即R色值序列为0, 4, 8, .....251, 255;纵向同样有64个像素,表示间距为4的G通道;B通道被巧妙的放在大格子中(从左到右,从上到下共64个小方格,表示B通道的64个数值) + + + + +整张图从左到右和从上到下为蓝色的渐变,每个小格子从左到右为红色渐变,从上到下为绿色渐变 + +64只是一种颜色粒度的划分,可以是其它数值,具体的看设计师给出查找表是怎么规划的,此处的粒度为64,也就是将RGB的分量划分为64份,颜色[0.0, 1.0] -> [0.0, 63.0] + + +以一个具体的颜色Color(r = 30.0, g = 30.0, b = 25.4)为例子来说明是如何通过LUT来做映射的。 + + + +蓝色值用来定位两个相邻的小格子 + +第一个小格子: + +float blueColor = textureColor.b * 63.0; +vec2 quad1; +// blueColor = 25.4, 第3行的第1个小格子 +// floor:向下取整 +quad1.y = floor(floor(blueColor) / 8.0); +quad1.x = floor(blueColor) - (quad1.y * 8.0); + +第二个小格子: + + +vec2 quad2; +// blueColor = 25.4,第3行的第2个小格子 +// ceil:向上取整 +quad2.y = floor(ceil(blueColor) / 8.0); +quad2.x = ceil(blueColor) - (quad2.y * 8.0); + +红色值和绿色值用来确定相对于整个LUT的纹理坐标: + +vec2 texPos1; +texPos1.x = (quad1.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); +texPos1.y = (quad1.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); + +vec2 texPos2; +texPos2.x = (quad2.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); +texPos2.y = (quad2.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); + +通过纹理坐标获取两个新的颜色: + +vec4 newColor1 = texture2D(u_LookupTable, texPos1); +vec4 newColor2 = texture2D(u_LookupTable, texPos2); + +然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出: + +// mix(x, y, a) -> x * (1 - a) + y * a +vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); + +完整的顶点着色器: + + +attribute vec4 a_Position; +attribute vec4 a_TextureCoordinate; + +varying vec2 vTextureUnitCoordinate; + +void main() { + gl_Position = a_Position; + vTextureUnitCoordinate = a_TextureCoordinate.xy; +} + + +片元着色器代码 + +precision mediump float; + +uniform sampler2D u_TextureSampler; +uniform sampler2D u_LookupTable; +uniform float u_Intensity; + +varying vec2 vTextureUnitCoordinate; + +void main() { + vec4 textureColor = texture2D(u_TextureSampler, vTextureUnitCoordinate); + float blueColor = textureColor.b * 63.0; + vec2 quad1; + quad1.y = floor(floor(blueColor) / 8.0); + quad1.x = floor(blueColor) - (quad1.y * 8.0); + + vec2 quad2; + quad2.y = floor(ceil(blueColor) / 8.0); + quad2.x = ceil(blueColor) - (quad2.y * 8.0); + + vec2 texPos1; + texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); + texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); + + vec2 texPos2; + texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); + texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); + + vec4 newColor1 = texture2D(u_LookupTable, texPos1); + vec4 newColor2 = texture2D(u_LookupTable, texPos2); + + vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); + gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), u_Intensity); +} + + From 65a5f1438f0c569ef573a98f0d214dfec0f3b4ba Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 May 2024 14:11:57 +0800 Subject: [PATCH 088/128] update --- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index 0bea9e77..d97b22b1 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -1,36 +1,35 @@ # 13.LUT滤镜 -LUT全称LookUpTable,也称为颜色查找表,它代表的是一种映射关系,通过LUT可以将输入的像素数组通过映射关系转换输出成另外的像素数组。 +LUT全称Look Up Table,也称为颜色查找表。 -比如一个像素的颜色值分别是R1 G1 B1,经过一次LUT操作后变为R2 G2 B2。 +它代表的是一种映射关系,通过LUT可以将输入的像素数组通过映射关系转换输出成另外的像素数组。 -通过这个映射关系就可以将一个像素的颜色转换为另外一种颜色。 +比如一个像素的颜色值分别是R1 G1 B1,经过一次LUT操作后变为R2 G2 B2。 LUT从查找方式上可以分为1D LUT和3D LUT: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1dvs3d.webp) + - 1D LUT -对于一张RGB图像,每个通道都可以作为一个输入,用公式可以描述如下: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1d.jpg) +对于一张RGB图像,每个通道都可以作为一个输入,用公式可以描述如下: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1d.jpg) 由于颜色值的范围是(0~255),我们可以用`256*3` 的表来表示一个1D LUT,在实际操作中,我们通常以一张`256*3*1`的图片来存储这个映射表,如下,这是一张1D LUT在Mac访达中文件信息图: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_1d_map.jpg) - -由于1D LUT各个通道都是相互独立的,无法对其他通道产生影响,因此1D LUT 只能用来调节亮度/伽马/饱和度/色彩平衡等,如果我们希望对其他通道产生影响,就需要了解另一种滤镜查找方式--3D LUT。 - - +由于1D LUT各个通道都是相互独立的,无法对其他通道产生影响,因此1D LUT只能用来调节亮度/伽马/饱和度/色彩平衡等,如果我们希望对其他通道产生影响,就需要了解另一种滤镜查找方式--3D LUT。 - 3D LUT - 3D LUT在滤镜中的影响比1D LUT更为深刻,下面我们用一张图(图是网上找的,参考“参考资料”)来说面3D LUT,假设下图的红色平面发生移动,其中对应的绿色和蓝色分量也会发生改变,也就是说,一个颜色通道改变可以影响其他的颜色通道,理论上 3D LUT 可以在立体色彩空间中描述所有颜色调整行为,所以它可以处理任何显示的非线性属性,从简单的 gamma 值、颜色范围和追踪错误,到修正高级的非线性属性、颜色串扰(去耦)、色相、饱和度、亮度等,3D LUT都可以胜任。 +3D LUT在滤镜中的影响比1D LUT更为深刻,假设上图3D中的红色平面发生移动,其中对应的绿色和蓝色分量也会发生改变,也就是说,一个颜色通道改变可以影响其他的颜色通道。 +理论上3D LUT可以在立体色彩空间中描述所有颜色调整行为,所以它可以处理任何显示的非线性属性,从简单的gamma值、颜色范围和追踪错误,到修正高级的非线性属性、颜色串扰(去耦)、色相、饱和度、亮度等,3D LUT都可以胜任。 -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_3d.webp) 常见的3D LUT滤镜文件与.cube或者.3dl都是把3D坐标二维化后的数据表现,如下图 From 2e70e73040908ed779ff54489ad28b1c269e72e2 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 6 May 2024 15:32:30 +0800 Subject: [PATCH 089/128] update LUT --- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 88 ++++++++++--------- 1 file changed, 45 insertions(+), 43 deletions(-) diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index d97b22b1..2653ce59 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -36,83 +36,78 @@ LUT从查找方式上可以分为1D LUT和3D LUT: ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/3d_lut.jpg) +我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝(从左上角的格子到右下角的格子,一共是8行8列,正好是64个格子)。 +### 为什么是64x64 +对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为`256*256*256`,如果我们有一个大小为`256*256*256`的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张`256*256*256`的图实在是太大了,至少需要48MB的存储空间。 +如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64X64X64种就可以表示原来256X256X256的颜色数量,我们也将4称为采样步长。 - 我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝。 +在专业领域一般认为17x17x17的3D LUT足够适用于预览和监看;65x65x65或者更大的3D LUT更适合渲染和调色。 - 对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为256*256*256,如果我们有一个大小为256*256*256的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张256*256*256的图实在是太大了,至少需要48MB的存储空间,因此,通常会通过降低采样的方式来减少数据量。 +### 3D LUT映射 +要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut.png) +首先这张图的大小是512X512,在横竖方向上这张图都被分成了8个小方格(64只是一种颜色粒度的划分,可以是其它数值,具体的看设计师给出查找表是怎么规划的,此处的粒度为64,也就是将RGB的分量划分为64份,颜色[0.0, 1.0] -> [0.0, 63.0]): -我们看到的这些方格子里面的蓝色是固定的,然后每个格子横坐标是红色,纵坐标是绿色,最左上角的格子因为蓝色全无,所以红色和绿色就很明显,而最右下角的那个格子,蓝色色值达到最大,因此整体看上去就非常的蓝。 - 对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为256*256*256,如果我们有一个大小为256*256*256的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张256*256*256的图实在是太大了,至少需要48MB的存储空间,因此,通常会通过降低采样的方式来减少数据量。在专业领域一般认为17x17x17的3D LUT足够适用于预览和监看;65x65x65或者更大的3D LUT更适合渲染和调色。3D LUT的实现在计算机环境会相对容易,在嵌入式环境就意味着成本的提高,越大的LUT需要越昂贵的硬件支持。所以行内比较常用的LUT box一般都不是很大,比如说BMD的HDLink Pro采用了17x17x17的3D LUT(495美元);Fujifilm的IS-Mini则采用了26x26x26的3D LUT(865英镑)。 - 下面我们就以一个64*64*64的3D LUT为例,来说明一下3D LUT如何查找和使用的(用glsl来实现),一张3D LUT如下: +- 每个小方格的大小是64X64,也就是一张512X512的图被分割成64个小方格 +- 每个小方格的大小是64X64,这64个小方格就代表了64种B通道的颜色映射 +- 然后每个B通道的小方格上又是一个64X64像素大小的图像,这个小图像的横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况 +这样就刚好这就和采样步长是4的映射表对应上了。 +**在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小个子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。** -肉眼可见的有8*8个方格,假设整个图的范围我们定义为0.0~1.0 ,也就是说每个格子所表示的范围是1./8. = 0.125, 我们首先通过蓝色来查找我们在哪个方格,由于涉及到插值,因此通过蓝色进行查找的时候,可能不会那么幸运一定映射到整数的格子上面,因此我们需要通过向下取整和向上进位两种方式来获取蓝色所对应的方格,然后通过蓝色的颜色值来mix这两种的颜色值,从而获取最终的颜色值,glsl实现如下: +下面我们就以一个`64*64*64`的3D LUT为例,来说明一下3D LUT如何查找和使用的(用glsl来实现): +- 整张图从左到右和从上到下为蓝色的渐变,每个小格子从左到右为红色渐变,从上到下为绿色渐变 +- 肉眼可见的有8*8个方格,假设整个图的范围我们定义为0.0~1.0 ,也就是说每个格子所表示的范围是1./8. = 0.125 +- 我们首先通过蓝色来查找我们在哪个方格,由于涉及到插值,因此通过蓝色进行查找的时候,可能不会那么幸运一定映射到整数的格子上面,因此我们需要通过向下取整和向上进位两种方式来获取蓝色所对应的方格,然后通过蓝色的颜色值来mix这两种的颜色值,从而获取最终的颜色值。 -## 为什么使用LUT滤镜 +### 映射示例 +以一个具体的颜色Color(r=30.0, g=30.0, b=25.4)为例子来说明是如何通过LUT来做映射的。 -在正常情况下,8位的RGB颜色模式可以表示的颜色数量为256X256X256种,如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64X64X64种就可以表示原来256X256X256的颜色数量,我们也将4称为采样步长。 -要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的: -lut映射关系图 -首先这张图的大小是512X512,在横竖方向上这张图都被分成了8个小方格,每个小方格的大小是64X64,也就是一张512X512的图被分割成64个小方格,每个小方格的大小是64X64,这64个小方格就代表了64种B通道的颜色映射, 然后每个B通道的小方格上又是一个64X64像素大小的图像,这个小图像的横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况,这样就刚好这就和采样步长是4的映射表对应上了。 -在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小个子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。 +#### 1.1 寻找B值的两个格子 +B值对应的是0 ~ 63,共64个格子,在计算时,并不一定会是一个整数,对应到某一个格子,大概率会是一个小数,为了能够更精确的进行映射,需要使用两个格子,然后根据小数(因子)进行插值计算,从而能够更精确。 +##### 蓝色值用来定位两个相邻的小格子 -对输入图颜色转换时,以颜色(rgb)的b值作为索引,找到所属于的小格。 -最后根据r和g的值在小格中定位到映射的目标值上(两个格子上对应的像素进行插值)。 - -最后根据r和g的值在小格中定位到映射的目标值上(两个格子上对应的像素进行插值)。 - - -每个小方格的横向有512 / 8 = 64个像素,表示间距为4的R通道,即R色值序列为0, 4, 8, .....251, 255;纵向同样有64个像素,表示间距为4的G通道;B通道被巧妙的放在大格子中(从左到右,从上到下共64个小方格,表示B通道的64个数值) - - - - -整张图从左到右和从上到下为蓝色的渐变,每个小格子从左到右为红色渐变,从上到下为绿色渐变 - -64只是一种颜色粒度的划分,可以是其它数值,具体的看设计师给出查找表是怎么规划的,此处的粒度为64,也就是将RGB的分量划分为64份,颜色[0.0, 1.0] -> [0.0, 63.0] - - -以一个具体的颜色Color(r = 30.0, g = 30.0, b = 25.4)为例子来说明是如何通过LUT来做映射的。 - - - -蓝色值用来定位两个相邻的小格子 - -第一个小格子: +- 第一个小格子: +```python float blueColor = textureColor.b * 63.0; vec2 quad1; // blueColor = 25.4, 第3行的第1个小格子 // floor:向下取整 quad1.y = floor(floor(blueColor) / 8.0); quad1.x = floor(blueColor) - (quad1.y * 8.0); +``` -第二个小格子: - +- 第二个小格子: +```python vec2 quad2; // blueColor = 25.4,第3行的第2个小格子 // ceil:向上取整 quad2.y = floor(ceil(blueColor) / 8.0); quad2.x = ceil(blueColor) - (quad2.y * 8.0); +``` + +##### 确定每个格子对应的R G值 红色值和绿色值用来确定相对于整个LUT的纹理坐标: +```python vec2 texPos1; texPos1.x = (quad1.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); texPos1.y = (quad1.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); @@ -120,20 +115,26 @@ texPos1.y = (quad1.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); vec2 texPos2; texPos2.x = (quad2.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); texPos2.y = (quad2.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); +``` -通过纹理坐标获取两个新的颜色: +##### 通过纹理坐标获取两个新的颜色 +```python vec4 newColor1 = texture2D(u_LookupTable, texPos1); vec4 newColor2 = texture2D(u_LookupTable, texPos2); +``` + +##### 然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出 -然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出: +```python // mix(x, y, a) -> x * (1 - a) + y * a vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); +``` 完整的顶点着色器: - +```python attribute vec4 a_Position; attribute vec4 a_TextureCoordinate; @@ -143,10 +144,11 @@ void main() { gl_Position = a_Position; vTextureUnitCoordinate = a_TextureCoordinate.xy; } +``` +片元着色器代码: -片元着色器代码 - +```python precision mediump float; uniform sampler2D u_TextureSampler; @@ -180,7 +182,7 @@ void main() { vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), u_Intensity); } - +``` From 13a341266ec8d2a02ca421136dc2bf6eb5e5a47a Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 7 May 2024 19:59:32 +0800 Subject: [PATCH 090/128] update Lut --- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 91 +++++++++++++++---- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index 2653ce59..ed005e2e 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -41,27 +41,34 @@ LUT从查找方式上可以分为1D LUT和3D LUT: ### 为什么是64x64 -对于RGB颜色,每种颜色可以有256种取值,因此一个3D LUT 如果全量表示的话,大小为`256*256*256`,如果我们有一个大小为`256*256*256`的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。但是通常情况下,我们不会这么干,因为一张`256*256*256`的图实在是太大了,至少需要48MB的存储空间。 +对于RGB颜色,每种颜色可以有256(0 ~ 255)种取值,而每个颜色占用3个字节,因此一个3D LUT如果全量表示的话,大小为`256*256*256*3`个字节,即48M内存空间。 -如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(n通常为4)这样只需要64X64X64种就可以表示原来256X256X256的颜色数量,我们也将4称为采样步长。 +如果我们有一个大小为`256*256*256*3`的3D LUT文件,那么颜色映射将非常简单,只要根据RGB的像素值按照用蓝色找到对应的格子,然后用红色和绿色找到对应格子的横列就可以找到映射的颜色值。 -在专业领域一般认为17x17x17的3D LUT足够适用于预览和监看;65x65x65或者更大的3D LUT更适合渲染和调色。 +但是通常情况下,我们不会这么干,因为一张图48M存储空间实在是太大了。 + +如果要完全记录这种映射关系,设备需要耗费大量的内存,并且可能在计算时因为计算量大而产生性能问题, 为了简化计算量,降低内存占用,可以将相近的n种颜色采用一条映射记录并存储,(采样步长,n通常为4),这样只需要`64 * 64 * 64 * 3`个字节就可以表示原来`256 * 256 * 256 * 3`的颜色数量,我们也将4称为采样步长。 + +对于忽略的颜色值可以进行插值获得其相似结果。 + +在专业领域一般认为`16 * 16 * 16`的3D LUT足够适用于预览和监看。 +`64 * 64 * 64`或者更大的3D LUT更适合渲染和调色。 ### 3D LUT映射 -要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的: -![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut.png) +要想熟练使用LUT滤镜,我们首先要了解它是怎么建立颜色映射关系的, 我们看下以下这张图,这张图展示了在LUT中RBG颜色是如何实现映射关系的: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/3d_lut_convert_1.png) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut.jpg) +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/lut_rg.jpg) + -首先这张图的大小是512X512,在横竖方向上这张图都被分成了8个小方格(64只是一种颜色粒度的划分,可以是其它数值,具体的看设计师给出查找表是怎么规划的,此处的粒度为64,也就是将RGB的分量划分为64份,颜色[0.0, 1.0] -> [0.0, 63.0]): +- LUT图在横竖方向上被分成了8 X 8一共64个小方格,每一个小方格内的B(Blue)分量为一个定值,64 个小方格一共表示了B分量的64种取值。 -- 每个小方格的大小是64X64,也就是一张512X512的图被分割成64个小方格 -- 每个小方格的大小是64X64,这64个小方格就代表了64种B通道的颜色映射 -- 然后每个B通道的小方格上又是一个64X64像素大小的图像,这个小图像的横坐标代表R分量的64种映射情况,纵坐标代表了G分量的64种映射情况 +- 对于每一个小方格,横竖方向又各自分为64个小格:以左上角为原点,横向小格的 R(Red)分量依次增加,纵向小格的 G(Green)分量依次增加 -这样就刚好这就和采样步长是4的映射表对应上了。 -**在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小个子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。** +**在使用上面这张LUT表的时候首选需要找对B分量对应的小格子,然后在找到的小格子上再计算出R分量和G分量的映射结果即可得到完整的RGB映射结果。** @@ -82,6 +89,16 @@ B值对应的是0 ~ 63,共64个格子,在计算时,并不一定会是一 ##### 蓝色值用来定位两个相邻的小格子 +- 首先是对原图进行像素采样 + +```glsl` +vec4 textureColor = texture2D(vTexture, v_TextureCoord); +``` + +textureColor.b是一个0 - 1之间的浮点数,乘以63用来确定B分量所在的格子. +因为会出现浮点误差,所以才需要取两个B分量,也就是下一步的取与B分量值最接近的2个小方格的坐标,最后根据小数点进行插值运算。 + + - 第一个小格子: ```python @@ -105,18 +122,51 @@ quad2.x = ceil(blueColor) - (quad2.y * 8.0); ##### 确定每个格子对应的R G值 +每个B格子代表的纹理坐标长度是 1/8 = 0.125 +63 * textureColor.r是将当前实际像素的r值映射到0 ~ 63,再除以512是转化为纹理坐标中实际的点,LUT图的分辨率为`512*512` + 红色值和绿色值用来确定相对于整个LUT的纹理坐标: -```python +```glsl + vec2 texPos1; -texPos1.x = (quad1.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); -texPos1.y = (quad1.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); +texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); +texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); + vec2 texPos2; -texPos2.x = (quad2.x * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.r); -texPos2.y = (quad2.y * 1.0 / 8.0) + (63.0 / 512.0) * textureColor.g); +texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); +texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); ``` +- 上面texPos1.x,texPos1.y代表的是在0-1的纹理坐标中改点(texPos1)的具体坐标。 +- `(quad1.x * 0.125)`指的是在纹理坐标中的左上角的归一化坐标 + - quad1.x代表当前格子在`8*8`的格子中横坐标的第几个,这个`8*8`的格子构成了0-1的纹理空间,所以一个格子代表的纹理坐标长度就是 1/8 = 0.125 + - 所以第几个格子就代表了具有几个0.125这样的纹理长度 + - 所以`(quad1.x * 0.125)`指的就是当前格子的**左上角**在纹理坐标系中的横坐标的具体坐标点。 + + ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/3dlut_zuoshangjiao.jpg) + +- `((0.125 - 1.0/512.0) * textureColor.r)` + +这段代码可能是最看不懂的一段了,其实是这里是一个计算步骤的省略如下: +`((0.125 - 1.0/512.0) * textureColor.r) = ((64-1)* textureColor.r)/512`这样就可以理解了, +`(64-1)* textureColor.r`意思是首先将当前实际像素的r值映射到0-63的范围内。 +除以512是转化为纹理坐标中实际的点。(`纹理的分辨率为512*512`) + +这个步骤结束就在这一个小方格中根据R分量和G分量又确定了更小的一个方格的左上角的坐标点。如下图 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/3dlut_2.jpg) + +- 0.5/512.0 + +这个就很好理解了,因为上面算出来的结果都是小方格的左上角坐标,加上小方格横纵的一半坐标就是将该点移动到了小方格中心。 + + + + + + ##### 通过纹理坐标获取两个新的颜色 ```python @@ -127,11 +177,18 @@ vec4 newColor2 = texture2D(u_LookupTable, texPos2); ##### 然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出 -```python +```glsl // mix(x, y, a) -> x * (1 - a) + y * a +// 使用mix方法对2个边界像素值进行插值混合 +// 根据浮点数到整点的距离来计算其对应的颜色值,fract函数是指小数部分。 vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); + +// 将原图与映射后的图进行插值混合 +// 这次的插值混合是实现我们常见的滤镜调节功能,adjust调节的范围是(0-1),取0时完全使用原图,取1时完全使用映射后的滤镜图 +gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust); ``` + 完整的顶点着色器: ```python From 6d20d9562333beb6e3d0160adf22c9da50fe6ece Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 7 May 2024 20:00:55 +0800 Subject: [PATCH 091/128] remove swp file --- .../.1.OpenGL\347\256\200\344\273\213.md.swp" | Bin 69632 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" diff --git "a/VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" "b/VideoDevelopment/OpenGL/.1.OpenGL\347\256\200\344\273\213.md.swp" deleted file mode 100644 index ec4ff252dcbbc9f4474238143addba1def69c816..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 69632 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 K{Eh!``hNw5CD)Sx From 30c5e78bfd1f4806956daaf382fa877ba3e3d70d Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Wed, 8 May 2024 13:41:56 +0800 Subject: [PATCH 092/128] update opengl part --- .../1.OpenGL\347\256\200\344\273\213.md" | 4 +- ...55\346\224\276\350\247\206\351\242\221.md" | 5 +- .../11.OpenGL ES\346\273\244\351\225\234.md" | 6 +- VideoDevelopment/OpenGL/12.FBO.md | 5 +- .../OpenGL/13.LUT\346\273\244\351\225\234.md" | 106 +++++++----------- ...4.\345\256\236\344\276\213\345\214\226.md" | 3 +- ....GLSurfaceView\347\256\200\344\273\213.md" | 6 +- ...20\347\240\201\350\247\243\346\236\220.md" | 6 +- ....GLTextureView\345\256\236\347\216\260.md" | 7 +- ...66\344\270\211\350\247\222\345\275\242.md" | 6 +- ...42\345\217\212\345\234\206\345\275\242.md" | 5 +- ...45\231\250\350\257\255\350\250\200GLSL.md" | 5 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 6 +- .../9.OpenGL ES\347\272\271\347\220\206.md" | 5 +- 14 files changed, 83 insertions(+), 92 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 61803e2e..5f45c5d2 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -645,9 +645,9 @@ Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个 +--- - -[下一篇: 2.GLSurfaceView简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md) +- [下一篇: 2.GLSurfaceView简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md) --- diff --git "a/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" "b/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" index c1c347da..0eb4f1b9 100644 --- "a/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" +++ "b/VideoDevelopment/OpenGL/10.GLSurfaceView+MediaPlayer\346\222\255\346\224\276\350\247\206\351\242\221.md" @@ -325,9 +325,10 @@ public class VideoPlayerRender extends BaseGLSurfaceViewRenderer { +--- +- [上一篇: 9.OpenGL ES纹理](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md) -[上一篇: 9.OpenGL ES纹理](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md) -[下一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) +- [下一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) --- diff --git "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" index 366ebbb3..ac2ff34d 100644 --- "a/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/11.OpenGL ES\346\273\244\351\225\234.md" @@ -401,8 +401,10 @@ public class BaseFilter { -[上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) -[下一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) +--- +- [上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) + +- [下一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) --- diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md index dc4d71bc..736c7f9c 100644 --- a/VideoDevelopment/OpenGL/12.FBO.md +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -21,9 +21,10 @@ Android OpenGL ES开发中,一般使用GLSurfaceView将绘制结果显示到 +--- +- [上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) -[上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) -[下一篇: 13.LUT滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md) +- [下一篇: 13.LUT滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md) --- diff --git "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" index ed005e2e..d02b6e97 100644 --- "a/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" +++ "b/VideoDevelopment/OpenGL/13.LUT\346\273\244\351\225\234.md" @@ -81,7 +81,6 @@ LUT从查找方式上可以分为1D LUT和3D LUT: ### 映射示例 -以一个具体的颜色Color(r=30.0, g=30.0, b=25.4)为例子来说明是如何通过LUT来做映射的。 #### 1.1 寻找B值的两个格子 @@ -91,18 +90,20 @@ B值对应的是0 ~ 63,共64个格子,在计算时,并不一定会是一 - 首先是对原图进行像素采样 -```glsl` -vec4 textureColor = texture2D(vTexture, v_TextureCoord); +```glsl +vec4 textureColor = texture(s_texture, v_texCoord); ``` -textureColor.b是一个0 - 1之间的浮点数,乘以63用来确定B分量所在的格子. +textureColor.b是一个0 - 1之间的浮点数,乘以63用来确定B分量所在的格子. 因为会出现浮点误差,所以才需要取两个B分量,也就是下一步的取与B分量值最接近的2个小方格的坐标,最后根据小数点进行插值运算。 - 第一个小格子: ```python -float blueColor = textureColor.b * 63.0; +vec4 color = texture(s_texture, v_texCoord); +float blueColor = color.b * 63.0; + vec2 quad1; // blueColor = 25.4, 第3行的第1个小格子 // floor:向下取整 @@ -120,7 +121,9 @@ quad2.y = floor(ceil(blueColor) / 8.0); quad2.x = ceil(blueColor) - (quad2.y * 8.0); ``` -##### 确定每个格子对应的R G值 +##### 确定每个格子内具体点对应的色值 + +通过R和G分量的值确定小方格内目标映射的RGB组合的坐标,然后归一化,转化为纹理坐标。 每个B格子代表的纹理坐标长度是 1/8 = 0.125 63 * textureColor.r是将当前实际像素的r值映射到0 ~ 63,再除以512是转化为纹理坐标中实际的点,LUT图的分辨率为`512*512` @@ -130,16 +133,15 @@ quad2.x = ceil(blueColor) - (quad2.y * 8.0); ```glsl vec2 texPos1; -texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); -texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); - +texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r); +texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g); vec2 texPos2; -texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); -texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); +texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r); +texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g); ``` -- 上面texPos1.x,texPos1.y代表的是在0-1的纹理坐标中改点(texPos1)的具体坐标。 +- 上面texPos1.x,texPos1.y代表的是在0-1的纹理坐标中该点(texPos1)的具体坐标。 - `(quad1.x * 0.125)`指的是在纹理坐标中的左上角的归一化坐标 - quad1.x代表当前格子在`8*8`的格子中横坐标的第几个,这个`8*8`的格子构成了0-1的纹理空间,所以一个格子代表的纹理坐标长度就是 1/8 = 0.125 - 所以第几个格子就代表了具有几个0.125这样的纹理长度 @@ -169,9 +171,9 @@ texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor. ##### 通过纹理坐标获取两个新的颜色 -```python -vec4 newColor1 = texture2D(u_LookupTable, texPos1); -vec4 newColor2 = texture2D(u_LookupTable, texPos2); +```glsl +vec4 newColor1 = texture(textureLUT, texPos1); +vec4 newColor2 = texture(textureLUT, texPos2); ``` ##### 然后根据蓝色值小数部分作为权重做线性混合,获取最终的颜色输出 @@ -182,71 +184,56 @@ vec4 newColor2 = texture2D(u_LookupTable, texPos2); // 使用mix方法对2个边界像素值进行插值混合 // 根据浮点数到整点的距离来计算其对应的颜色值,fract函数是指小数部分。 vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); - // 将原图与映射后的图进行插值混合 // 这次的插值混合是实现我们常见的滤镜调节功能,adjust调节的范围是(0-1),取0时完全使用原图,取1时完全使用映射后的滤镜图 gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), adjust); ``` - -完整的顶点着色器: - -```python -attribute vec4 a_Position; -attribute vec4 a_TextureCoordinate; - -varying vec2 vTextureUnitCoordinate; - -void main() { - gl_Position = a_Position; - vTextureUnitCoordinate = a_TextureCoordinate.xy; -} -``` - 片元着色器代码: -```python +```glsl +#version 300 es precision mediump float; -uniform sampler2D u_TextureSampler; -uniform sampler2D u_LookupTable; -uniform float u_Intensity; +in vec2 v_texCoord; +out vec4 outColor; +uniform sampler2D s_texture; +uniform sampler2D textureLUT; -varying vec2 vTextureUnitCoordinate; +//查找表滤镜 +vec4 lookupTable(vec4 color){ + float blueColor = color.b * 63.0; -void main() { - vec4 textureColor = texture2D(u_TextureSampler, vTextureUnitCoordinate); - float blueColor = textureColor.b * 63.0; vec2 quad1; quad1.y = floor(floor(blueColor) / 8.0); quad1.x = floor(blueColor) - (quad1.y * 8.0); - vec2 quad2; quad2.y = floor(ceil(blueColor) / 8.0); quad2.x = ceil(blueColor) - (quad2.y * 8.0); vec2 texPos1; - texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); - texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); - + texPos1.x = (quad1.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r); + texPos1.y = (quad1.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g); vec2 texPos2; - texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.r); - texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * textureColor.g); - - vec4 newColor1 = texture2D(u_LookupTable, texPos1); - vec4 newColor2 = texture2D(u_LookupTable, texPos2); - + texPos2.x = (quad2.x * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.r); + texPos2.y = (quad2.y * 0.125) + 0.5/512.0 + ((0.125 - 1.0/512.0) * color.g); + vec4 newColor1 = texture(textureLUT, texPos1); + vec4 newColor2 = texture(textureLUT, texPos2); vec4 newColor = mix(newColor1, newColor2, fract(blueColor)); - gl_FragColor = mix(textureColor, vec4(newColor.rgb, textureColor.w), u_Intensity); + return vec4(newColor.rgb, color.w); } -``` - +void main(){ + vec4 tmpColor = texture(s_texture, v_texCoord); + outColor = lookupTable(tmpColor); +} +``` -[上一篇: 12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) -[下一篇: 14.实例化](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/14.%E5%AE%9E%E4%BE%8B%E5%8C%96.md) +--- +- [12.FBO](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/12.FBO.md) +- [14.实例化](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/14.%E5%AE%9E%E4%BE%8B%E5%8C%96.md) --- @@ -263,17 +250,6 @@ void main() { - - - - - - - - - - - diff --git "a/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" "b/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" index cf2f5fae..f0a70991 100644 --- "a/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" +++ "b/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" @@ -19,7 +19,8 @@ -[上一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +--- +[上一篇: 13.LUT滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/13.LUT%E6%BB%A4%E9%95%9C.md) --- diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index 7a17ce67..be25becb 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -209,9 +209,11 @@ SurfaceTexture对象可以在任何线程上创建。 updateTexImage()只能在包含纹理对象的OpenGL ES上下文的线程上调用。 在任意线程上调用frame-available回调函数,不与updateTexImage()在同一线程上出现。 +--- -[上一篇: 1.OpenGL简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/1.OpenGL%E7%AE%80%E4%BB%8B.md) -[下一篇: 3.GLSurfaceView源码解析](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/3.GLSurfaceView%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) +- [上一篇: 1.OpenGL简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/1.OpenGL%E7%AE%80%E4%BB%8B.md) + +- [下一篇: 3.GLSurfaceView源码解析](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/3.GLSurfaceView%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) --- diff --git "a/VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md" "b/VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md" index d5c9e8ab..4453dcc7 100644 --- "a/VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md" +++ "b/VideoDevelopment/OpenGL/3.GLSurfaceView\346\272\220\347\240\201\350\247\243\346\236\220.md" @@ -689,9 +689,11 @@ public void setRenderer(Renderer renderer) { +--- + +- [上一篇: 2.GLSurfaceView简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md) -[上一篇: 2.GLSurfaceView简介](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/2.GLSurfaceView%E7%AE%80%E4%BB%8B.md) -[下一篇: 4.GLTextureView实现](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md) +- [下一篇: 4.GLTextureView实现](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md) --- diff --git "a/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" "b/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" index 86affc03..63981545 100644 --- "a/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" +++ "b/VideoDevelopment/OpenGL/4.GLTextureView\345\256\236\347\216\260.md" @@ -1,4 +1,4 @@ -### 4.GLTextureView实现 +## 4.GLTextureView实现 系统提供了GLSurfaceView,确没有提供GLTextureView,但是我们目前的项目灰常复杂庞大,我不想去封一层接口,然后动态去选择使用GLSurfaceView(实现一些纹理效果)或者TextureView(无OpenGL效果)来播放视频。我只想在目前的基础上去扩展,我想去实现一个GLTextureView。上面分析完GLSurfaceView的源码后我们也可以自己去实现GLTextureView的功能了。具体的实现就是和GLSurfaceView的源码一模一样。 @@ -1977,9 +1977,10 @@ public class GLTextureView extends TextureView implements TextureView.SurfaceTex +--- +- [上一篇: 3.GLSurfaceView源码解析](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/3.GLSurfaceView%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) -[上一篇: 3.GLSurfaceView源码解析](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/3.GLSurfaceView%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90.md) -[下一篇: 5.OpenGL ES绘制三角形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/5.OpenGL%20ES%E7%BB%98%E5%88%B6%E4%B8%89%E8%A7%92%E5%BD%A2.md) +- [下一篇: 5.OpenGL ES绘制三角形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/5.OpenGL%20ES%E7%BB%98%E5%88%B6%E4%B8%89%E8%A7%92%E5%BD%A2.md) --- diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 3fbe1436..c1779157 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -1171,10 +1171,10 @@ public class IsoTriangleRender implements GLTextureView.Renderer { +--- - -[上一篇: 4.GLTextureView实现](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md) -[下一篇: 6.OpenGL ES绘制矩形及圆形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/6.OpenGL%20ES%E7%BB%98%E5%88%B6%E7%9F%A9%E5%BD%A2%E5%8F%8A%E5%9C%86%E5%BD%A2.md) +- [上一篇: 4.GLTextureView实现](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/4.GLTextureView%E5%AE%9E%E7%8E%B0.md) +- [下一篇: 6.OpenGL ES绘制矩形及圆形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/6.OpenGL%20ES%E7%BB%98%E5%88%B6%E7%9F%A9%E5%BD%A2%E5%8F%8A%E5%9C%86%E5%BD%A2.md) --- diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index 9b9d547d..49d52826 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -522,9 +522,10 @@ public class CircleRender extends BaseGLSurfaceViewRenderer { +--- -[上一篇: 5.OpenGL ES绘制三角形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/5.OpenGL%20ES%E7%BB%98%E5%88%B6%E4%B8%89%E8%A7%92%E5%BD%A2.md) -[下一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) +- [上一篇: 5.OpenGL ES绘制三角形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/5.OpenGL%20ES%E7%BB%98%E5%88%B6%E4%B8%89%E8%A7%92%E5%BD%A2.md) +- [下一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) --- diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index d91aa94e..ac66269e 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -513,9 +513,10 @@ void main(void) +--- -[上一篇: 6.OpenGL ES绘制矩形及圆形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/6.OpenGL%20ES%E7%BB%98%E5%88%B6%E7%9F%A9%E5%BD%A2%E5%8F%8A%E5%9C%86%E5%BD%A2.md) -[下一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) +- [上一篇: 6.OpenGL ES绘制矩形及圆形](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/6.OpenGL%20ES%E7%BB%98%E5%88%B6%E7%9F%A9%E5%BD%A2%E5%8F%8A%E5%9C%86%E5%BD%A2.md) +- [下一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) --- diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 01eb63e1..7be018c0 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -253,8 +253,10 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 -[上一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) -[下一篇: 9.OpenGL ES纹理](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md) +--- + +- [上一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) +- [下一篇: 9.OpenGL ES纹理](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/9.OpenGL%20ES%E7%BA%B9%E7%90%86.md) --- diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 37eb449f..0b92ed52 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -571,9 +571,10 @@ void main() { 顶点着色器允许一次操作一个顶点,而片段着色器一次可以操作一个片段(实际上是一个像素),但几何着色器却可以一次操作一个图元。 +--- +- [上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) -[上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) -[下一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) +- [下一篇: 10.GLSurfaceView+MediaPlayer播放视频](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/10.GLSurfaceView%2BMediaPlayer%E6%92%AD%E6%94%BE%E8%A7%86%E9%A2%91.md) --- From 66b1db472308f2efabd1d3c8ea6b42a297062fe3 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 11 May 2024 20:44:09 +0800 Subject: [PATCH 093/128] update rxjava part --- AdavancedPart/ARouter.md | 71 ++++ .../Gradle\344\270\223\351\242\230.md" | 337 ++++++++++++++++++ ...\350\257\246\350\247\243(\344\270\200).md" | 128 ++++--- ...\350\257\246\350\247\243(\344\272\214).md" | 104 +++--- ...\350\257\246\350\247\243(\344\270\211).md" | 33 +- ...\345\216\237\347\220\206(\345\233\233).md" | 3 +- 6 files changed, 569 insertions(+), 107 deletions(-) create mode 100644 AdavancedPart/ARouter.md diff --git a/AdavancedPart/ARouter.md b/AdavancedPart/ARouter.md new file mode 100644 index 00000000..f9d9cc9f --- /dev/null +++ b/AdavancedPart/ARouter.md @@ -0,0 +1,71 @@ +ARouter +--- + + +[ARouter](https://github.com/alibaba/ARouter) +一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦 + + + + +```java +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivity(intent); + +Intent intent = new Intent(mContext, XxxActivity.class); +intent.putExtra("key","value"); +startActivityForResult(intent, 666); +``` +在未使用ARouter路由框架之前的原生页面跳转方式。 + + +## 原生路由方案的缺点 + +1. 显式: 直接的类依赖,耦合严重 +2. 隐式: 会在Manifest文件中进行集中管理,写作困难 + + +## ARouter的优势 + +1. 使用注解,实现了映射关系自动注册与分布式路由管理 +2. 编译期间处理注解,并生成映射文件,没有使用反射,不影响运行时性能 +3. 映射关系按组分类,分级管理,按需初始化。 +4. 灵活的降级策略,每次跳转都会回调跳转结果,避免startActivity()一旦失败会抛出异常 +5. 自定义拦截器,自定义拦截顺序,可以对路由进行拦截,比如登录判断和埋点处理 +6. 支持依赖注入,可单独作为依赖注入框架使用,从而实现跨模块API调用 +7. 支持直接解析标准url进行跳转,并自动注入参数到目标页面中 +8. 支持多模块使用,支持组件化开发 + + + +## 基本使用 + +添加依赖并初始化后的基本使用: + +```java +// 在支持路由的页面上添加注解(必选) +// 这里的路径需要注意的是至少需要有两级,/xx/xx +@Route(path = "/test/activity") +public class YourActivity extend Activity { + ... +} + +// 1. 应用内简单的跳转(通过URL跳转在'进阶用法'中) +ARouter.getInstance().build("/test/activity").navigation(); + +// 2. 跳转并携带参数 +ARouter.getInstance().build("/test/1") + .withLong("key1", 666L) + .withString("key3", "888") + .withObject("key4", new Test("Jack", "Rose")) + .navigation(); +``` + + + + + + + + diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index 67820a04..b2689789 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -1749,6 +1749,343 @@ pizzaPrices.pepperoni + + + +1.Groovy概述 +Groovy是Apache 旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。在语言的设计上它吸纳了Python、Ruby 和 Smalltalk 语言的优秀特性,比如动态类型转换、闭包和元编程支持。 +Groovy与 Java可以很好的互相调用并结合编程 ,比如在写 Groovy 的时候忘记了语法可以直接按Java的语法继续写,也可以在 Java 中调用 Groovy 脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。 + +2.Groovy编写和调试 +Groovy的代码可以在Android Studio和IntelliJ IDEA等IDE中进行编写和调试,缺点是需要配置环境,这里推荐在文本中编写代码并结合命令行进行调试(文本推荐使用Sublime Text)。关于命令行请查看Gradle入门前奏这篇文章。 +具体的操作步骤就是:在一个目录中新建build.gradle文件,在build.gradle中新建一个task,在task中编写Groovy代码,用命令行进入这个build.gradle文件所在的目录,运行gradle task名称 等命令行对代码进行调试,本文中的例子都是这样编写和调试的。 + +3.变量 +Groovy中用def关键字来定义变量,可以不指定变量的类型,默认访问修饰符是public。 + +def a = 1; +def int b = 1; +def c = "hello world"; +4.方法 +方法使用返回类型或def关键字定义,方法可以接收任意数量的参数,这些参数可以不申明类型,如果不提供可见性修饰符,则该方法为public。 +用def关键字定义方法。 + +task method <<{ + add (1,2) + minus 1,2 //1 +} +def add(int a,int b) { + println a+b //3 +} +def minus(a,b) {//2 + println a-b +} +如果指定了方法返回类型,可以不需要def关键字来定义方法。 + +task method <<{ + def number=minus 1,2 + println number +} +int minus(a,b) { + return a-b +} +如果不使用return ,方法的返回值为最后一行代码的执行结果。 + +int minus(a,b) { + a-b //4 +} +从上面两段代码中可以发现Groovy中有很多省略的地方: + +语句后面的分号可以省略。 + +方法的括号可以省略,比如注释1和注释3处。 + +参数类型可以省略,比如注释2处。 + +return可以省略掉,比如注释4处。 + +5.类 +Groovy类非常类似于Java类。 + +task method <<{ + def p = new Person() + p.increaseAge 5 + println p.age +} +class Person { + String name + Integer age =10 + def increaseAge(Integer years) { + this.age += years + } +} +运行 gradle method打印结果为: +15 + +Groovy类与Java类有以下的区别: + +默认类的修饰符为public。 + +没有可见性修饰符的字段会自动生成对应的setter和getter方法。 + +类不需要与它的源文件有相同的名称,但还是建议采用相同的名称。 + +6.语句 +6.1 断言 +Groovy断言和Java断言不同,它一直处于开启状态,是进行单元测试的首选方式。 + +task method <<{ + assert 1+2 == 6 +} +输出结果为: + +Execution failed for task ':method'. +> assert 1+2 == 6 + | | + 3 false +当断言的条件为false时,程序会抛出异常,不再执行下面的代码,从输出可以很清晰的看到发生错误的地方。 + +6.2 for循环 +Groovy支持Java的for(int i=0;i ] statements } +闭包分为两个部分,分别是参数列表部分[closureParameters -> ]和语句部分 statements 。 +参数列表部分是可选的,如果闭包只有一个参数,参数名是可选的,Groovy会隐式指定it作为参数名,如下所示。 + +{ println it } //使用隐式参数it的闭包 +当需要指定参数列表时,需要->将参数列表和闭包体相分离。 + +{ it -> println it } //it是一个显示参数 +{ String a, String b -> + println "${a} is a ${b}" +} +闭包是groovy.lang.Cloush类的一个实例,这使得闭包可以赋值给变量或字段,如下所示。 + +//将闭包赋值给一个变量 +def println ={ it -> println it } +assert println instanceof Closure +//将闭包赋值给Closure类型变量 +Closure do= { println 'do!' } +调用闭包 +闭包既可以当做方法来调用,也可以显示调用call方法。 + +def code = { 123 } +assert code() == 123 //闭包当做方法调用 +assert code.call() == 123 //显示调用call方法 +def isOddNumber = { int i -> i%2 != 0 } +assert isOddNumber(3) == true //调用带参数的闭包 +8. I/O 操作 +Groovy的 I/O 操作要比Java的更为的简洁。 + +8.1 文件读取 +我们可以在PC上新建一个name.txt,在里面输入一些内容,然后用Groovy来读取该文件的内容: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath) ; +file.eachLine { + println it +} +可以看出Groovy的文件读取是很简洁的,还可以更简洁些: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath) ; +println file.text +8.2 文件写入 +文件写入同样十分简洁: + +def filePath = "D:/Android/name.txt" +def file = new File(filePath); + +file.withPrintWriter { + it.println("三井寿") + it.println("仙道彰") +} +9. 其他 +9.1 asType +asType可以用于数据类型转换: + +String a = '23' +int b = a as int +def c = a.asType(Integer) +assert c instanceof java.lang.Integer +9.2 判断是否为真 +if (name != null && name.length > 0) {} +可以替换为 + +if (name) {} +9.3 安全取值 +在Java中,要安全获取某个对象的值可能需要大量的if语句来判空: + +if (school != null) { + if (school.getStudent() != null) { + if (school.getStudent().getName() != null) { + System.out.println(school.getStudent().getName()); + } + } +} +Groovy中可以使用?.来安全的取值: + +println school?.student?.name +9.4 with操作符 +对同一个对象的属性进行赋值时,可以这么做: + +task method <<{ +Person p = new Person() +p.name = "杨影枫" +p.age = 19 +p.sex = "男" +println p.name +} +class Person { + String name + Integer age + String sex +} +使用with来进行简化: + +Person p = new Person() +p.with { + name = "杨影枫" + age= 19 + sex= "男" + } +println p.name + + + + + + + + [1]: https://github.com/CharonChui/AndroidNote/blob/master/AndroidStudioCourse/AndroidStudio%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B(%E7%AC%AC%E4%B8%83%E5%BC%B9).md "AndroidStudio使用教程(第七弹)" diff --git "a/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" "b/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" index 81b665b6..6767b6d9 100644 --- "a/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" +++ "b/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" @@ -1,8 +1,6 @@ RxJava详解(一) === -年初的时候就想学习下`RxJava`然后写一些`RxJava`的教程,无奈发现已经年底了,然而我还么有写。今天有点时间,特别是发布了`RxJava 2.0`后,我决定动笔开始。 - 现在`RxJava`变的越来越流行了,很多项目中都使用了它。特别是大神`JakeWharton`等的加入,以及`RxBinding、Retrofit、RxLifecycle`等众多项目的,然开发越来越方便,但是上手比较难,不过一旦你入门后你就会发现真是太棒了。 @@ -92,7 +90,9 @@ public void rxFetchUserDetails() { ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rx_logo.png?raw=true) -`Rx`基于观察者模式,它是一种编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流。`ReactiveX.io`给的定义是,`Rx`是一个使用可观察数据流进行异步编程的编程接口,`ReactiveX`结合了观察者模式、迭代器模式和函数式编程的精华。`Rx`已经渗透到了各个语言中,有了`Rx`所以才有了`RxJava`、`Rx.NET`、`RxJS`、`RxSwift`、`Rx.rb`、`RxPHP`等等, +`Rx`基于观察者模式,它是一种编程模型,目标是提供一致的编程接口,帮助开发者更方便的处理异步数据流。 + +`ReactiveX.io`给的定义是,`Rx`是一个使用可观察数据流进行异步编程的编程接口,`ReactiveX`结合了观察者模式、迭代器模式和函数式编程的精华。`Rx`已经渗透到了各个语言中,有了`Rx`所以才有了`RxJava`、`Rx.NET`、`RxJS`、`RxSwift`、`Rx.rb`、`RxPHP`等等, 这里先列举一下相关的官网: @@ -107,7 +107,9 @@ public void rxFetchUserDetails() { 这里可能会有人想不就是个异步吗,至于辣么矫情么?用`AsyncTask`、`Handler`甚至自定义一个`BigAsyncTask`分分钟搞定。 -但是`RxJava`的好处是简洁。异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 `Android`创造的`AsyncTask`和`Handler`其实都是为了让异步代码更加简洁。虽然`RxJava`的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。 +但是`RxJava`的好处是简洁。异步操作很关键的一点是程序的简洁性,因为在调度过程比较复杂的情况下,异步代码经常会既难写也难被读懂。 + +`Android`创造的`AsyncTask`和`Handler`其实都是为了让异步代码更加简洁。虽然`RxJava`的优势也是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。 #### 扩展的观察者模式 @@ -115,13 +117,23 @@ public void rxFetchUserDetails() { `RxJava`的异步实现,是通过一种扩展的观察者模式来实现的。 -观察者模式面向的需求是:`A`对象(观察者)对`B`对象(被观察者)的某种变化高度敏感,需要在`B`变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如`A`不需要每过`2ms`就检查一次`B`的状态),而是采用注册(`Register`)或者称为订阅(`Subscribe`)的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 `Android`开发中一个比较典型的例子是点击监听器`OnClickListener`。对设置`OnClickListener`来说,`View`是被观察者,`OnClickListener`是观察者,二者通过 `setOnClickListener()`方法达成订阅关系。订阅之后用户点击按钮的瞬间,`Android Framework`就会将点击事件发送给已经注册的`OnClickListener`。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。 +观察者模式面向的需求是:`A`对象(观察者)对`B`对象(被观察者)的某种变化高度敏感,需要在`B`变化的一瞬间做出反应。 + +举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。 + +程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如`A`不需要每过`2ms`就检查一次`B`的状态),而是采用注册(`Register`)或者称为订阅(`Subscribe`)的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 + +`Android`开发中一个比较典型的例子是点击监听器`OnClickListener`。对设置`OnClickListener`来说,`View`是被观察者,`OnClickListener`是观察者,二者通过 `setOnClickListener()`方法达成订阅关系。 + +订阅之后用户点击按钮的瞬间,`Android Framework`就会将点击事件发送给已经注册的`OnClickListener`。 采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。 `OnClickListener`的模式大致如下图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/btn_onclick.jpg?raw=true) -如图所示,通过`setOnClickListener()`方法,`Button`持有`OnClickListener`的引用(这一过程没有在图上画出);当用户点击时,`Button`自动调用`OnClickListener`的`onClick()` 方法。另外,如果把这张图中的概念抽象出来(`Button` -> 被观察者、`OnClickListener` -> 观察者、`setOnClickListener()` -> 订阅,`onClick()` -> 事件),就由专用的观察者模式(例如只用于监听控件点击)转变成了通用的观察者模式。如下图: +如图所示,通过`setOnClickListener()`方法,`Button`持有`OnClickListener`的引用(这一过程没有在图上画出);当用户点击时,`Button`自动调用`OnClickListener`的`onClick()` 方法。 + +另外,如果把这张图中的概念抽象出来(`Button` -> 被观察者、`OnClickListener` -> 观察者、`setOnClickListener()` -> 订阅,`onClick()` -> 事件),就由专用的观察者模式(例如只用于监听控件点击)转变成了通用的观察者模式。如下图: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/btn_rxjava_observable.jpg?raw=true) @@ -134,7 +146,7 @@ public void rxFetchUserDetails() { `RxJava`的基本概念: - `Observable`(可观察者,即被观察者):产生事件,例如去饭店吃饭的顾客。 -- `Observer`(观察者):接收事件,并给出响应动作,例如去饭店吃饭的厨房,会接受事件,并给出相应。 +- `Observer`(观察者):接收事件,并给出响应动作,例如去饭店吃饭的厨房,会接受事件,并给出响应。 - `subscribe()`(订阅):连接被观察者与观察者,例如去饭店吃饭的服务员。 `Observable`和`Observer`通过`subscribe()` 方法实现订阅关系,从而`Observable`可以在需要的时候发出事件来通知`Observer`。 - `Event`(事件):被观察者与观察者沟通的载体,例如顾客点的菜。 @@ -155,6 +167,18 @@ public void rxFetchUserDetails() { ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observer_1.jpg?raw=true) +RxJava在响应式编程中的基本流程如下: + +这个流程,可以简单的理解为:Observable -> Operator1 -> Operator2 -> Operator3 -> Subscriber + +- Observable发出一系列事件,他是事件的产生者; +- Subscriber负责处理事件,他是事件的消费者; +- Operator是对Observable发出的事件进行修改和变换; +- 若事件从产生到消费不需要其他处理,则可以省略掉中间的Operator,从而流程变为Obsevable -> Subscriber; +- Subscriber通常在主线程执行,所以原则上不要去处理太多的事务,而这些复杂的处理则交给Operator; + + + #### 基本实现 @@ -219,13 +243,14 @@ public void rxFetchUserDetails() { observable.subscribe(subscriber); ``` - > 有人可能会注意到,`subscribe()`这个方法有点怪:它看起来是`observalbe`订阅了`observer/subscriber`而不是`observer/subscriber`订阅了`observalbe`,这看起来就像『杂志订阅了读者』一样颠倒了对象关系。这让人读起来有点别扭,不过如果把`API`设计成`observer.subscribe(observable)/subscriber.subscribe(observable)` ,虽然更加符合思维逻辑,但对流式`API`的设计就造成影响了,比较起来明显是得不偿失的。 + > 有人可能会注意到,`subscribe()`这个方法有点怪:它看起来是`observalbe`订阅了`observer/subscriber`而不是`observer/subscriber`订阅了`observalbe`,这看起来就像『杂志订阅了读者』一样颠倒了对象关系。 + > 这让人读起来有点别扭,不过如果把`API`设计成`observer.subscribe(observable)/subscriber.subscribe(observable)` ,虽然更加符合思维逻辑,但对流式`API`的设计就造成影响了,比较起来明显是得不偿失的。 `RxJava`入门示例 --- -一个`Observable`可以发出零个或者多个事件,知道结束或者出错。每发出一个事件,就会调用它的`Subscriber`的`onNext`方法,最后调用`Subscriber.onComplete()`或者`Subscriber.onError()`结束。 +一个`Observable`可以发出零个或者多个事件,直到结束或者出错。每发出一个事件,就会调用它的`Subscriber`的`onNext`方法,最后调用`Subscriber.onComplete()`或者`Subscriber.onError()`结束。 #### `Hello World` @@ -285,7 +310,7 @@ public Subscription subscribe(Subscriber subscriber) { } ``` -可以看到`subscriber()`做了3件事: +可以看到`subscriber()`做了3件事: - 调用`Subscriber.onStart()`。这个方法在前面已经介绍过,是一个可选的准备方法。 - 调用`Observable`中的`onSubscribe.call(Subscriber)`。在这里,事件发送的逻辑开始运行。从这也可以看出,在`RxJava`中,`Observable` 并不是在创建的时候就立即开始发送事件,而是在它被订阅的时候,即当`subscribe()`方法执行的时候。 @@ -296,8 +321,6 @@ public Subscription subscribe(Subscriber subscriber) { ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observable_list.gif?raw=true) -讲到这里很多人肯定会骂傻`X`,这TM简洁你妹啊...,这里只是个入门`Hello World`,真正的简洁等你看完全部介绍后就明白了。 - `RxJava`内置了很多简化创建`Observable`对象的函数,比如`Observable.just()`就是用来创建只发出一个事件就结束的`Observable`对象,上面创建`Observable`对象的代码可以简化为一行 ```java @@ -330,7 +353,7 @@ Observable.just("Hello ", "World !").subscribe(new Action1() { }); ``` -这里顺便多提一些`subscribe()`的多个`Action`参数: +这里顺便多提一下`subscribe()`的多个`Action`参数: ```java Action1 onNextAction = new Action1() { // onNext() @@ -357,12 +380,27 @@ Action0 onCompletedAction = new Action0() { observable.subscribe(onNextAction, onErrorAction, onCompletedAction); ``` -简单解释一下这段代码中出现的`Action1`和`Action0`。`Action0`是`RxJava`的一个接口,它只有一个方法`call()`,这个方法是无参无返回值的;由于`onCompleted()` 方法也是无参无返回值的,因此`Action0`可以被当成一个包装对象,将`onCompleted()`的内容打包起来将自己作为一个参数传入`subscribe()`以实现不完整定义的回调。这样其实也可以看做将 `onCompleted()`方法作为参数传进了`subscribe()`,相当于其他某些语言中的『闭包』。`Action1`也是一个接口,它同样只有一个方法`call(T param)`,这个方法也无返回值,但有一个参数;与`Action0`同理,由于`onNext(T obj)`和`onError(Throwable error)`也是单参数无返回值的,因此`Action1`可以将`onNext(obj)`和`onError(error)`打包起来传入`subscribe()`以实现不完整定义的回调。事实上,虽然`Action0`和`Action1`在`API`中使用最广泛,但`RxJava`是提供了多个`ActionX`形式的接口(例如`Action2`, `Action3`)的,它们可以被用以包装不同的无返回值的方法。 +简单解释一下这段代码中出现的`Action1`和`Action0`。 + +`Action0`是`RxJava`的一个接口,它只有一个方法`call()`,这个方法是无参无返回值的; + +由于`onCompleted()` 方法也是无参无返回值的,因此`Action0`可以被当成一个包装对象,将`onCompleted()`的内容打包起来将自己作为一个参数传入`subscribe()`以实现不完整定义的回调。 + + +这样其实也可以看做将 `onCompleted()`方法作为参数传进了`subscribe()`,相当于其他某些语言中的『闭包』。 + +`Action1`也是一个接口,它同样只有一个方法`call(T param)`,这个方法也无返回值,但有一个参数; + +与`Action0`同理,由于`onNext(T obj)`和`onError(Throwable error)`也是单参数无返回值的,因此`Action1`可以将`onNext(obj)`和`onError(error)`打包起来传入`subscribe()`以实现不完整定义的回调。 + +事实上,虽然`Action0`和`Action1`在`API`中使用最广泛,但`RxJava`是提供了多个`ActionX`形式的接口(例如`Action2`, `Action3`)的,它们可以被用以包装不同的无返回值的方法。 假设我们的`Observable`是第三方提供的,它提供了大量的用户数据给我们,而我们需要从用户数据中筛选部分有用的信息,那我们该怎么办呢? 从`Observable`中去修改肯定是不现实的?那从`Subscriber`中进行修改呢? 这样好像是可以完成的。但是这种方式并不好,因为我们希望`Subscriber`越轻量越好,因为很有可能我们需要 -在主线程中去执行`Subscriber`。另外,根据响应式函数编程的概念,`Subscribers`更应该做的事情是`响应`,响应`Observable`发出的事件,而不是去修改。 +在主线程中去执行`Subscriber`。 + +另外,根据响应式函数编程的概念,`Subscribers`更应该做的事情是`响应`,响应`Observable`发出的事件,而不是去修改。 那该怎么办呢? 这就要用到下面的部分要讲的操作符。 @@ -381,7 +419,7 @@ observable.subscribe(onNextAction, onErrorAction, onCompletedAction); > 背压是指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略。 -简而言之,背压是流速控制的一种策略。 +简而言之,背压是流速控制的一种策略。 其实`RxJava 2.x`最大的改动就是对于`backpressure`的处理,为此将原来的`Observable`拆分成了新的`Observable`和`Flowable`,同时其他相关部分也同时进行了拆分。 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava1vs2.png?raw=true) @@ -394,7 +432,7 @@ observable.subscribe(onNextAction, onErrorAction, onCompletedAction); `RxJava`提供了对事件序列进行变换的支持,这是它的核心功能之一.所谓变换,就是将事件序列中的对象或整个序列进行加工处理,转换成不同的事件或事件序列。 操作符就是为了解决对`Observable`对象的变换的问题,操作符用于在`Observable`和最终的`Subscriber`之间修改`Observable`发出的事件。`RxJava`提供了很多很有用的操作符。 -比如`map`操作符,就是用来把把一个事件转换为另一个事件的。 +比如`map`操作符,就是用来把一个事件转换为另一个事件的。 #### `map` @@ -456,10 +494,10 @@ map.subscribe(new Action1() { 通过上面的部分我们可以得知: -- `Observable`和`Subscriber`可以做任何事情 +- `Observable`和`Subscriber`可以做任何事情 `Observable`可以是一个数据库查询,`Subscriber`用来显示查询结果;`Observable`可以是屏幕上的点击事件,`Subscriber`用来响应点击事件;`Observable`可以是一个网络请求,`Subscriber`用来显示请求结果。 -- `Observable`和`Subscriber`是独立于中间的变换过程的。 +- `Observable`和`Subscriber`是独立于中间的变换过程的 在`Observable`和`Subscriber`中间 可以增减任何数量的`map`。整个系统是高度可组合的,操作数据是一个很简单的过程。 @@ -532,7 +570,9 @@ Observable.from(students) .subscribe(subscriber); ``` -`map`与`flatmap`在功能上是一致的,它也是把传入的参数转化之后返回另一个对象。区别在于`flatmap`是通过中间`Observable`来进行,而`map`是直接执行.`flatMap()`中返回的是个 `Observable`对象,并且这个`Observable`对象并不是被直接发送到了`Subscriber`的回调方法中。 +`map`与`flatmap`在功能上是一致的,它也是把传入的参数转化之后返回另一个对象。区别在于`flatmap`是通过中间`Observable`来进行,而`map`是直接执行. + +`flatMap()`中返回的是个 `Observable`对象,并且这个`Observable`对象并不是被直接发送到了`Subscriber`的回调方法中。 `flatMap()`的原理是这样的: @@ -585,18 +625,18 @@ Observable.from(s).subscribe(new Action1() { ```java Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9}) - .filter(new Func1() { - @Override - public Boolean call(Integer integer) { - return integer < 5; - } - }) - .subscribe(new Action1() { - @Override - public void call(Integer integer) { - Log.i("@@@", "integer=" + integer); //1,2,3,4 - } - }); + .filter(new Func1() { + @Override + public Boolean call(Integer integer) { + return integer < 5; + } + }) + .subscribe(new Action1() { + @Override + public void call(Integer integer) { + Log.i("@@@", "integer=" + integer); //1,2,3,4 + } + }); ``` #### `timer` @@ -606,12 +646,12 @@ Observable.from(new Integer[]{1, 2, 3, 4, 5, 6, 7, 8, 9}) ```java Observable.timer(3, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()) - .subscribe(new Action1() { - @Override - public void call(Long aLong) { - Log.i("@@@", "aLong=" + aLong); // 延时3s - } - }); + .subscribe(new Action1() { + @Override + public void call(Long aLong) { + Log.i("@@@", "aLong=" + aLong); // 延时3s + } + }); ``` #### `interval` @@ -622,12 +662,12 @@ Observable.timer(3, TimeUnit.SECONDS).observeOn(AndroidSchedulers.mainThread()) ```java Observable.interval(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread()) - .subscribe(new Action1() { - @Override - public void call(Long aLong) { - Log.i("@@@", "aLong=" + aLong); //从0递增,间隔1s 0,1,2,3.... - } - }); + .subscribe(new Action1() { + @Override + public void call(Long aLong) { + Log.i("@@@", "aLong=" + aLong); //从0递增,间隔1s 0,1,2,3.... + } + }); ``` #### `Repeat` diff --git "a/RxJavaPart/2.RxJava\350\257\246\350\247\243(\344\272\214).md" "b/RxJavaPart/2.RxJava\350\257\246\350\247\243(\344\272\214).md" index a689be18..55be3ce4 100644 --- "a/RxJavaPart/2.RxJava\350\257\246\350\247\243(\344\272\214).md" +++ "b/RxJavaPart/2.RxJava\350\257\246\350\247\243(\344\272\214).md" @@ -85,7 +85,7 @@ try{ 这样,我们就可以处理这三个函数中所抛出的任何异常了。如果没有`try catch`语句,我们也可以把异常继续传递下去。 -向异步粗发 +向异步出发 --- 但是,现实世界中我们往往没法等待。有些时候你没法只使用阻塞调用。在`Android`中你需要处理各种异步操作。 @@ -225,7 +225,6 @@ public class CatsHelper { #### 奔向更好的世界 -#### 通用的回调 如果我们仔细的观察下回调接口,我们会发现它们的共性: @@ -318,7 +317,6 @@ public class CatsHelper{ 下面来试试看看这种方式能否有效。 如果我们返回一个临时的对象作为异步操作的回调接口处理方式,我们需要先定义这个对象。由于对象遵守通用的行为(有一个回调接口参数),我们定义一个能用于所有操作的对象。我们称之为`AsyncJob`。 -> 注意: 我非常想把这个名字称之为`AsyncTask`。但是由于`Android`系统已经有个`AsyncTask`类了, 为了避免混淆,所以就用`AsyncJob`了。 该对象如下: ```java @@ -384,28 +382,28 @@ public class CatsHelper { @Override public void start(Callback cutestCatCallback) { apiWrapper.queryCats(query) - .start(new Callback>() { - @Override - public void onResult(List cats) { - Cat cutest = findCutest(cats); - apiWrapper.store(cutest) - .start(new Callback() { - @Override - public void onResult(Uri result) { - cutestCatCallback.onResult(result); - } - - @Override - public void onError(Exception e) { - cutestCatCallback.onError(e); - } - }); - } - - @Override - public void onError(Exception e) { - cutestCatCallback.onError(e); - } + .start(new Callback>() { + @Override + public void onResult(List cats) { + Cat cutest = findCutest(cats); + apiWrapper.store(cutest) + .start(new Callback() { + @Override + public void onResult(Uri result) { + cutestCatCallback.onResult(result); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } }); } }; @@ -466,17 +464,17 @@ public class CatsHelper { @Override public void onResult(Cat cutest) { apiWrapper.store(cutest) - .start(new Callback() { - @Override - public void onResult(Uri result) { - cutestCatCallback.onResult(result); - } - - @Override - public void onError(Exception e) { - cutestCatCallback.onError(e); - } - }); + .start(new Callback() { + @Override + public void onResult(Uri result) { + cutestCatCallback.onResult(result); + } + + @Override + public void onError(Exception e) { + cutestCatCallback.onError(e); + } + }); } @Override @@ -505,21 +503,21 @@ public class CatsHelper { ```java AsyncJob cutestCatAsyncJob = new AsyncJob() { + @Override + public void start(Callback callback) { + catsListAsyncJob.start(new Callback>() { @Override - public void start(Callback callback) { - catsListAsyncJob.start(new Callback>() { - @Override - public void onResult(List result) { - callback.onResult(findCutest(result)); - } - - @Override - public void onError(Exception e) { - callback.onError(e); - } - }); + public void onResult(List result) { + callback.onResult(findCutest(result)); } - }; + + @Override + public void onError(Exception e) { + callback.onError(e); + } + }); + } +}; ``` 这 16 行代码中,只有一行代码是我们的业务逻辑代码: @@ -661,7 +659,7 @@ public class CatsHelper { } } ``` -哎。。。 看起来没这么简单啊, 下面修复返回的类型再试一次: +哎。。。 看起来没这么简单啊,下面修复返回的类型再试一次: ```java public class CatsHelper { @@ -911,7 +909,7 @@ public class CatsHelper { } ``` -把 Observable 替换为 AsyncJob 后 他们的代码是一样的。 +把Observable替换为AsyncJob后他们的代码是一样的。 #### 结论 @@ -926,7 +924,7 @@ public class CatsHelper { 如果嫌上面的代码麻烦,可以通过下面的例子看: -假设有这样一个需求:界面上有一个自定义的视图`imageCollectorView`,它的作用是显示多张图片,并能使用`addImage(Bitmap)` 方法来任意增加显示的图片。现在需要程序将一个给出的目录数组`File[] folders`中每个目录下的`png`图片都加载出来并显示在`imageCollectorView`中。需要注意的是,由于读取图片的这一过程较为耗时,需要放在后台执行,而图片的显示则必须在`UI`线程执行。常用的实现方式有多种,我这里贴出其中一种: +假设有这样一个需求:界面上有一个自定义的视图`imageCollectorView`,它的作用是显示多张图片,并能使用`addImage(Bitmap)` 方法来任意增加显示的图片。现在需要程序将一个给出的目录数组`File[] folders`中每个目录下的`png`图片都加载出来并显示在`imageCollectorView`中。需要注意的是,由于读取图片的这一过程较为耗时,需要放在后台执行,而图片的显示则必须在`UI`线程执行。常用的实现方式有多种,我这里贴出其中一种: ```java @@ -984,7 +982,7 @@ Observable.from(folders) }); ``` -那位说话了:『你这代码明明变多了啊!简洁个毛啊!』大兄弟你消消气,我说的是逻辑的简洁,不是单纯的代码量少(逻辑简洁才是提升读写代码速度的必杀技对不?)。观察一下你会发现, `RxJava`的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。当需求变得复杂时,这种优势将更加明显(试想如果还要求只选取前`10`张图片,常规方式要怎么办?如果有更多这样那样的要求呢?再试想,在这一大堆需求实现完两个月之后需要改功能,当你翻回这里看到自己当初写下的那一片迷之缩进,你能保证自己将迅速看懂,而不是对着代码重新捋一遍思路?)。 +观察一下你会发现, `RxJava`的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。当需求变得复杂时,这种优势将更加明显(试想如果还要求只选取前`10`张图片,常规方式要怎么办?如果有更多这样那样的要求呢?再试想,在这一大堆需求实现完两个月之后需要改功能,当你翻回这里看到自己当初写下的那一片迷之缩进,你能保证自己将迅速看懂,而不是对着代码重新捋一遍思路?)。 更多内容请看下一篇文章[RxJava详解(三)][1] diff --git "a/RxJavaPart/3.RxJava\350\257\246\350\247\243(\344\270\211).md" "b/RxJavaPart/3.RxJava\350\257\246\350\247\243(\344\270\211).md" index e1f39a75..4d0ac650 100644 --- "a/RxJavaPart/3.RxJava\350\257\246\350\247\243(\344\270\211).md" +++ "b/RxJavaPart/3.RxJava\350\257\246\350\247\243(\344\270\211).md" @@ -22,9 +22,11 @@ public Observable lift(Operator operator) { } ``` -这段代码很有意思:它生成了一个新的`Observable`并返回,而且创建新`Observable`所用的参数`OnSubscribe的回调方法`call()`中的实现竟然看起来和前面讲过的`Observable.subscribe()`一样!然而它们并不一样哟~不一样的地方关键就在于第二行`onSubscribe.call(subscriber)`中的`onSubscribe` 所指代的对象不同 +这段代码很有意思:它生成了一个新的`Observable`并返回,而且创建新`Observable`所用的参数`OnSubscribe的回调方法`call()`中的实现竟然看起来和前面讲过的`Observable.subscribe()`一样! +然而它们并不一样哟~不一样的地方关键就在于第二行`onSubscribe.call(subscriber)`中的`onSubscribe` 所指代的对象不同: - `subscribe()`中这句话的`onSubscribe`指的是`Observable`中的`onSubscribe`对象,这个没有问题,但是`lift()`之后的情况就复杂了点。 -- 当含有`lift()`时: +- 当含有`lift()`时: + - `lift()`创建了一个`Observable`后,加上之前的原始`Observable`,已经有两个`Observable`了; - 而同样地,新`Observable`里的新`OnSubscribe`加上之前的原始`Observable`中的原始`OnSubscribe`,也就有了两个`OnSubscribe`; - 当用户调用经过`lift()`后的`Observable`的`subscribe()`的时候,使用的是`lift()`所返回的新的`Observable`,于是它所触发的`onSubscribe.call(subscriber)`,也是用的新`Observable`中的新`OnSubscribe`,即在`lift()`中生成的那个`OnSubscribe`; @@ -69,7 +71,7 @@ observable.lift(new Observable.Operator() { 线程控制`Scheduler` --- -在不指定线程的情况下,`RxJava`遵循的是线程不变的原则,即在哪个线程调用`subscribe()`方法就在哪个线程生产事件;在哪个线程生产事件,就在哪个线程消费事件。也就是说事件的发出和消费都是在同一个线程的。观察者模式本身的目的就是『后台处理,前台回调』的异步机制,因此异步对于`RxJava`是至关重要的。而要实现异步,则需要用到`RxJava`的另一个概念:`Scheduler`。 +在不指定线程的情况下,`RxJava`遵循的是线程不变的原则,即在哪个线程调用`subscribe()`方法就在哪个线程生产事件; 在哪个线程生产事件,就在哪个线程消费事件。也就是说事件的发出和消费都是在同一个线程的。观察者模式本身的目的就是『后台处理,前台回调』的异步机制,因此异步对于`RxJava`是至关重要的。而要实现异步,则需要用到`RxJava`的另一个概念:`Scheduler`。 #### `Scheduler`简介 @@ -98,16 +100,23 @@ Observable.just("Hello ", "World !") }); ``` -上面这段代码中,`subscribeOn(Schedulers.io())`的指定会让创建的事件的内容`Hello `、`World !`将会在`IO`线程发出;而由于`observeOn(AndroidScheculers.mainThread())` 的指定,因此`subscriber()`方法设置后的回调中内容的打印将发生在主线程中。事实上,这种在`subscribe()`之前写上两句`subscribeOn(Scheduler.io())`和`observeOn(AndroidSchedulers.mainThread())`的使用方式非常常见,它适用于多数的***后台线程取数据,主线程显示***的程序策略。 +上面这段代码中,`subscribeOn(Schedulers.io())`的指定会让创建的事件的内容`Hello `、`World !`将会在`IO`线程发出; + +而由于`observeOn(AndroidScheculers.mainThread())` 的指定,因此`subscriber()`方法设置后的回调中内容的打印将发生在主线程中。 +事实上,这种在`subscribe()`之前写上两句`subscribeOn(Scheduler.io())`和`observeOn(AndroidSchedulers.mainThread())`的使用方式非常常见,它适用于多数的***后台线程取数据,主线程显示***的程序策略。 #### `Scheduler`的原理 -`RxJava`的`Scheduler API`很方便,也很神奇(加了一句话就把线程切换了,怎么做到的?而且 subscribe() 不是最外层直接调用的方法吗,它竟然也能被指定线程?)。然而 Scheduler 的原理需要放在后面讲,因为它的原理是以下一节《变换》的原理作为基础的。 +`RxJava`的`Scheduler API`很方便,也很神奇(加了一句话就把线程切换了,怎么做到的?而且subscribe()不是最外层直接调用的方法吗,它竟然也能被指定线程?)。然而Scheduler的原理需要放在后面讲,因为它的原理是以下一节《变换》的原理作为基础的。 好吧这一节其实我屁也没说,只是为了让你安心,让你知道我不是忘了讲原理,而是把它放在了更合适的地方。 -能不能多切换几次线程?答案是:能。因为`observeOn()`指定的是`Subscriber`的线程,而这个`Subscriber`并不是(严格说应该为『不一定是』,但这里不妨理解为『不是』)`subscribe()` 参数中的`Subscriber`,而是`observeOn()`执行时的当前`Observable`所对应的`Subscriber`,即它的直接下级`Subscriber`。换句话说`observeOn()` 指定的是它之后的操作所在的线程。因此如果有多次切换线程的需求,只要在每个想要切换线程的位置调用一次`observeOn()`即可。 +能不能多切换几次线程?答案是:能。 + +因为`observeOn()`指定的是`Subscriber`的线程,而这个`Subscriber`并不是(严格说应该为『不一定是』,但这里不妨理解为『不是』)`subscribe()` 参数中的`Subscriber`,而是`observeOn()`执行时的当前`Observable`所对应的`Subscriber`,即它的直接下级`Subscriber`。 + +换句话说`observeOn()` 指定的是它之后的操作所在的线程。因此如果有多次切换线程的需求,只要在每个想要切换线程的位置调用一次`observeOn()`即可。 上代码: @@ -140,13 +149,19 @@ Observable.just(1, 2, 3, 4) // IO 线程,由 subscribeOn() 指定 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rxjava_observable_list.jpg?raw=true) -图中共有5处含有对事件的操作。由图中可以看出,①和②两处受第一个`subscribeOn()`影响,运行在红色线程;③和④处受第一个`observeOn()`的影响,运行在绿色线程;⑤处受第二个 `onserveOn()`影响,运行在紫色线程;而第二个`subscribeOn()`,由于在通知过程中线程就被第一个`subscribeOn()` 截断,因此对整个流程并没有任何影响。这里也就回答了前面的问题:当使用了多个`subscribeOn()`的时候,只有第一个`subscribeOn()`起作用。 +图中共有5处含有对事件的操作。由图中可以看出,①和②两处受第一个`subscribeOn()`影响,运行在红色线程; +③和④处受第一个`observeOn()`的影响,运行在绿色线程; +⑤处受第二个 `onserveOn()`影响,运行在紫色线程;而第二个`subscribeOn()`,由于在通知过程中线程就被第一个`subscribeOn()` 截断,因此对整个流程并没有任何影响。 + +这里也就回答了前面的问题:当使用了多个`subscribeOn()`的时候,只有第一个`subscribeOn()`起作用。 在前面讲`Subscriber`的时候,提到过`Subscriber`的`onStart()`可以用作流程开始前的初始化。然而`onStart()`由于在`subscribe()`发生时就被调用了,因此不能指定线程,而是只能执行在`subscribe()`被调用时的线程。这就导致如果`onStart()`中含有对线程有要求的代码(例如在界面上显示一个`ProgressBar`,这必须在主线程执行),将会有线程非法的风险,因为有时你无法预测`subscribe()`将会在什么线程执行。 -而与`Subscriber.onStart()`相对应的,有一个方法`Observable.doOnSubscribe()`。它和`Subscriber.onStart()`同样是在`subscribe()`调用后而且在事件发送前执行,但区别在于它可以指定线程。默认情况下,`doOnSubscribe()`执行在`subscribe()`发生的线程;而如果在`doOnSubscribe()`之后有`subscribeOn()`的话,它将执行在离它最近的`subscribeOn()`所指定的线程。 +而与`Subscriber.onStart()`相对应的,有一个方法`Observable.doOnSubscribe()`。 +它和`Subscriber.onStart()`同样是在`subscribe()`调用后而且在事件发送前执行,但区别在于它可以指定线程。 +默认情况下,`doOnSubscribe()`执行在`subscribe()`发生的线程;而如果在`doOnSubscribe()`之后有`subscribeOn()`的话,它将执行在离它最近的`subscribeOn()`所指定的线程。 -示例代码: +示例代码: ```java Observable.create(onSubscribe) diff --git "a/RxJavaPart/4.RxJava\350\257\246\350\247\243\344\271\213\346\211\247\350\241\214\345\216\237\347\220\206(\345\233\233).md" "b/RxJavaPart/4.RxJava\350\257\246\350\247\243\344\271\213\346\211\247\350\241\214\345\216\237\347\220\206(\345\233\233).md" index 52de63e3..913f85d2 100644 --- "a/RxJavaPart/4.RxJava\350\257\246\350\247\243\344\271\213\346\211\247\350\241\214\345\216\237\347\220\206(\345\233\233).md" +++ "b/RxJavaPart/4.RxJava\350\257\246\350\247\243\344\271\213\346\211\247\350\241\214\345\216\237\347\220\206(\345\233\233).md" @@ -300,7 +300,7 @@ public interface Subscription { observable.subscribe(subscriber)源码分析 --- -好了,关系部分出现了,我们继续看`Observable`类中的`subscribe()`方法,上面在将`Observable`源码的时候我们简单带过这部分,没有细说,这里仔细说一下: +好了,关键部分出现了,我们继续看`Observable`类中的`subscribe()`方法,上面在将`Observable`源码的时候我们简单带过这部分,没有细说,这里仔细说一下: ```java public class Observable { final OnSubscribe onSubscribe; @@ -486,6 +486,7 @@ observable.subscribe(subscriber); ``` 总结一下: + - 首先我们调用`Observable.create(OnSubscribe f)`创建一个观察者,同时创建一个`OnSubscribe`接口的实现类作为`create()`方法的参数,并重写该接口的`call()`方法,在`call()`方法中我们调用`subscriber`的`onNext()`、`onComplete()`及`onError()`方法。而这个`call()`方法中的`subscriber`对象就是我们后面调用`observable.subscribe(subscriber);`所传递的`subscribe`对象。 - 然后调用`new Subscriber()`创建一个`Subscriber`类的实例。 - 最后通过`observable.subscribe(subscriber);`将`Observable`和`Subscriber`对象进行绑定。来订阅我们自己创建的观察者`Subscriber`对象。 From 6e62714220a38e976e478731f7946a4636d318d7 Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 14 May 2024 15:37:45 +0800 Subject: [PATCH 094/128] update --- .../Gradle\344\270\223\351\242\230.md" | 27 ++++ Gradle&Maven/kts.md | 124 ++++++++++++++++++ 2 files changed, 151 insertions(+) create mode 100644 Gradle&Maven/kts.md diff --git "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" index b2689789..253a8c33 100644 --- "a/Gradle&Maven/Gradle\344\270\223\351\242\230.md" +++ "b/Gradle&Maven/Gradle\344\270\223\351\242\230.md" @@ -15,6 +15,12 @@ Gradle的版本在`gradle/wrapper/gradle-wrapper.properties`下: gradle-wrapper是对Gradle的一层包装,便于在团队开发过程中统一Gradle构建的版本号,这样大家都可以使用统一的Gradle版本进行构建。 里面的distributionUrl属性是用于配置Gradle发行版压缩包的下载地址。 +Gradle 是一个 运行在 JVM 的通用构建工具,其核心模型是一个由 Task 组成的有向无环图(Directed Acyclic Graphs). + + +![image](https://github.com/CharonChui/Pictures/blob/master/gradle_task_1.png?raw=true) + + 简介 --- @@ -35,6 +41,27 @@ Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/android_build_process.png?raw=true) + +说起来我们一直在使用Gradle,但仔细想想我们在项目中其实没有用gradle命令,而一般是使用gradlew命令,同时如下图所示,找遍整个项目,与gradle有关的就这两个文件夹,却只发现gradle-wrapper.jar。 + + +那么问题来了,gradlew是什么,gradle-wrapper.jar又是什么? + +wrapper的意思:包装。 + +那么可想而已,这是gradle的包装。其实是这样的,因为gradle处于快速迭代阶段,经常发布新版本,如果我们的项目直接去引用,那么更改版本等会变得无比麻烦。而且每个项目又有可能用不一样的gradle版本,这样去手动配置每一个项目对应的gradle版本就会变得麻烦,gradle的引入本来就是想让大家构建项目变得轻松,如果这样的话,岂不是又增加了新的麻烦? + +所以android想到了包装,引入gradle-wrapper,通过读取配置文件中gradle的版本,为每个项目自动的下载和配置gradle,就是这么简单。我们便不用关心如何去下载gradle,如何配置到项目中。 + +至于gradlew也是一样的道理,它共有两个文件,gradlew是在linux,mac下使用的,gradlew.bat是在window下使用的,提供在命令行下执行gradle命令的功能 + +至于为什么不直接执行Gradle,而是执行Gradlew命令呢? + +因为就像wrapper本身的意义,gradle命令行也是善变的,所以wrapper对命令行也进行了一层封装,使用同一的gradlew命令,wrapper会自动去执行具体版本对应的gradle命令。 + +同时如果我们配置了全局的gradle命令,在项目中如果也用gradle容易造成混淆,而gradlew明确就是项目中指定的gradle版本,更加清晰与明确 + + ### Gradle的生命周期 1. Initialization:初始化阶段 diff --git a/Gradle&Maven/kts.md b/Gradle&Maven/kts.md new file mode 100644 index 00000000..a12bb7ca --- /dev/null +++ b/Gradle&Maven/kts.md @@ -0,0 +1,124 @@ +Gradle 支持使用 Groovy DSL 或 Kotlin DSL 来编写脚本。所以在学习具体怎么写脚本时,我们肯定会考虑到底是使用 Kotlin 来写还是 Groovy 来写。 + +不一定说你是 Kotlin Android 开发者就一定要用 Kotlin 来写 Gradle,我们得判断哪种写法更适合项目、更适合开发团队人群(学习成本)。 + +所以下面来学习一下这两种语言的差异。 + +1. Groovy 和 Kotlin 的差异 +1.1 语言差异 +Groovy +Groovy是一种基于 JVM 的面向对象的编程语言,它可以作为常规编程语言,但主要是作为脚本的语言(为了解决 Java 在写脚本时过于死板)。它是一个动态语言,可以不指定变量类型。它的特性是支持闭包,闭包的本质很简单,简单的说就是定义一个匿名作用域,这个作用域内部可以封装函数和变量,外部不可以访问这个作用域内部的东西,但是可以通过调用这个作用域来完成一些任务。 +Kotlin +Kotlin 则是 Java 的优化版,在解决 Kotlin 很多痛点的情况下,不引入过多的新概念。它具有强大的类型推断系统,使得语言有良好的动态性,其次是其语言招牌 —— 语法糖,Kotlin 的代码可以写的非常简洁。这使得 Kotlin 不仅做为常规编程语言能大放异彩,作为脚本语言也深受很多开发者喜爱。 +它们共同特点就是基于JVM,可以和 Java 互操作。Gradle 能提供的东西, Kotlin 也能通过提供(闭包)。在功能上,两者能做的事情都是一样的。此外一些简单的差异有: + +groovy 字符串可以使用单引号,而 kotlin 则必须为双引号 +groovy 在方法调用时可以省略扩号,而 kotlin 不可省略 +groovy 分配属性时可以省略 = 赋值运算符,而 kotlin 不可省略 +groovy 是动态语言,不用导包,而 kotlin 则需要。 + + + +为什么要用Kotlin DSL写gradle脚本 +撇开其他方面,就单从提高程序员生产效率方面就有很多优点: + +脚本代码自动补全 +跳转查看源码 +动态显示注释 +支持重构(Refactoring) +… +怎么样,要是你经历过groovy那令人蛋疼的体验,kotlin会让你爽的起飞,接下来让我们开始吧。 + +从Groovy到Kotlin +让我们使用Android Studio 新建一个Android项目,AS默认会为我们生成3个gradle脚本文件。 + +settings.gradle (属于 project) +build.gradle (属于 project) +build.gradle (属于 module) +我们的目的就是转换这3个文件 + +第一步: 修改groovy语法到严格格式 +groovy既支持双引号""也支持单引号'',而kotlin只支持双引号,所以首先将所有的单引号改为双引号。 + +例如 include ':app' -> include ":app" + +groovy方法调用可以不使用() 但是kotlin方法调用必须使用(),所以将所有方法调用改为()方式。 + +例如 + +implementation "androidx.appcompat:appcompat:1.0.2" +改为 + + implementation ("androidx.appcompat:appcompat:1.0.2") +groovy 属性赋值可以不使用=,但是kotlin属性赋值需要使用=,所以将所有属性赋值添加=。 + +例如 + +applicationId "com.ss007.gradlewithkotlin" +改为 + +applicationId = "com.ss007.gradlewithkotlin" +完成以上几步,准备工作就完成了。 + + + + +1.2 文件差异 +两者编写 Gradle 的文件是有差异的: + +用 Groovy 写的 Gradle 文件是 .gradle 后缀 +用 Kotlin 写的 Gradle 文件是 .gradle.kts 为后缀 +两者的主要区别是: + +代码提示和编译检查 +.kts 内所有都是基于kotlin代码规范的,所以强类型语言的好处就是编译没通过的情况下根本无法运行。此外,IDE 集成后可以提供自动补全代码的能力 +.gradle 则不会有代码提示和编译检查 +源代码、文档查看 +.gradle 被编译后是 JVM 字节码,有时候无法查看其源码 +.kts 的 DSL 是通过扩展函数实现的(可以看这篇:Kotlin DSL 学习),IDE 支持下可以导航到源代码、文档或重构部分 +对于写脚本的人来说,两者的差异不大,因为 Gradle 的 DSL 是 Groovy 提供的,后来的 Kotlin 并没有另起炉灶,而是写了一套 Kotlin 版的。所以两者在代码上也就只有所用语言的差异了,概念啥的都是一样的。 + +作为一名 Kotlin Android 开发者,我之后基本上是使用 Kotlin DSL 来学习写 Gradle 脚本,但是就跟我上面说的一样,了解其中一个后,要搞懂另外一个成本是很低的。 + + +2. 基本命令 +2.1 Project 、 Task 和 Action 介绍 +Gradle 主要是围绕着 Project(项目)、Task(任务)、Action(行为)这几个概念进行的。它们的作用分别是: + +project:每次 build 可以由一个或多个 project 组成。Gradle 为每个 build.gradle 创建一个相应的 project 领域对象,在编写Gradle脚本时,我们实际上是在操作诸如 project 这样的 Gradle 领域对象。 +若要创建多 project 的项目,我们需要在 根工程(root目录)下面新建 settings.gradle 文件,将所有的子 project 都写进去(include)。在 Android 中,每个 Module 都是一个子 project。 +task:每个 project 可以由一个或多个 task 组成。它代表更加细化的构建任务,例如:签名、编译一些java文件等。 +action:每个 task 可以由一个或多个 action 组成,它有 doFirst{} 和 doLast{} 两种类型 +———————————————— + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 3aa5408f12cd8c5bb41410aba3d48f0d1616961f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 14 May 2024 19:14:34 +0800 Subject: [PATCH 095/128] update OpenGL part --- .../1.RxJava\350\257\246\350\247\243(\344\270\200).md" | 9 +++++++++ .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git "a/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" "b/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" index 6767b6d9..9e6e2c14 100644 --- "a/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" +++ "b/RxJavaPart/1.RxJava\350\257\246\350\247\243(\344\270\200).md" @@ -83,6 +83,15 @@ public void rxFetchUserDetails() { 从根本上讲,函数响应式编程是在观察者模式的基础上,增加对`Observables`发送的数据流进行操纵和变换的功能。 + + +特殊注意: + +(1) 若Observable.subscribeOn()多次指定被观察者 生产事件的线程,则只有第一次指定有效,其余的指定线程无效。 +(2) 若Observable.observeOn()多次指定观察者 接收 & 响应事件的线程,则每次指定均有效,即每指定一次,就会进行一次线程的切换。 + + + `RxJava`简介 --- diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 5f45c5d2..9b8ba320 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -245,7 +245,7 @@ GLFW是一个专门针对OpenGL的C语言库,它提供了一些渲染物体所 它把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标(后面会解释)从而生成每个顶点的最终位置,同时顶点着色器允许我们对顶点属性进行一些基本处理。 -所有顶点着色器的主要目标都是讲顶点发送给管线(正如之前所说的它会对每个顶点进行处理)。 +所有顶点着色器的主要目标都是将顶点发送给管线(正如之前所说的它会对每个顶点进行处理)。 顶点着色器可以操作的属性有: 位置、颜色、纹理坐标,但是不能创建新的顶点。最终产生纹理坐标、颜色、点位置等信息送往后续阶段。 @@ -381,7 +381,7 @@ OpenGLES3.0 VBO和EBO的出现就是为了解决这个问题。VBO和EBO的作 把数据发送给OpenGL管线还要更加复杂一点,有两种方式: -- 通过顶点属性的缓冲区; +- 通过顶点属性的缓冲区。 - 直接发送给统一变量。 理解这两种方式的机制非常重要,因为这样我们才能为每个要发送的项目选取合适的方式。 From d903180e00476bac0a045be60e636a6e3a7638c4 Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 14 May 2024 20:09:27 +0800 Subject: [PATCH 096/128] =?UTF-8?q?Update=201.Kotlin=5F=E7=AE=80=E4=BB=8B&?= =?UTF-8?q?=E5=8F=98=E9=87=8F&=E7=B1=BB&=E6=8E=A5=E5=8F=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...&\347\261\273&\346\216\245\345\217\243.md" | 44 +++++++++++-------- 1 file changed, 26 insertions(+), 18 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 72e4986b..6bd6e2fe 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" @@ -673,8 +673,8 @@ class Person{ } ``` -如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 -委托到同一个类的另一个构造函数用`this`关键字即可: +**如果类有一个主构造函数,每个次构造函数都需要委托给主构造函数(不然会报错), 可以直接委托或者通过别的次构造函数间接委托。 +委托到同一个类的另一个构造函数用`this`关键字即可:** ```kotlin class Person constructor(name: String) { constructor(name: String, surName: String) : this(name) { @@ -711,7 +711,7 @@ class Example(p: Int) : Base(p) ``` 如果该类有一个主构造函数,那么其基类型可以用主构造函数的参数进行初始化。 -如果该类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其类型,或者委托给另一个构造函数初始化。如: +**如果该类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其类型,或者委托给另一个构造函数初始化**。如: ```kotlin class Example : View { constructor(ctx: Context) : super(ctx) @@ -954,12 +954,12 @@ class User(val name: String, val age: Int) { } ``` -在上面的实例中,User类还有一个special函数,使用User::special可以获取成员函数的对象,然后使用invoke函数调用special函数,以获取该函数的内容。 +**在上面的实例中,User类还有一个special函数,使用User::special可以获取成员函数的对象,然后使用invoke函数调用special函数,以获取该函数的内容。** ### 构造函数引用 -构造函数的引用和属性、方法类似,构造函数可以作用于任何函数类型的对象,该函数的对象与构造函数的参数相同,可以使用::操作符加类名的方式来引用构造函数。 +**构造函数的引用和属性、方法类似,构造函数可以作用于任何函数类型的对象,该函数的对象与构造函数的参数相同,可以使用::操作符加类名的方式来引用构造函数。** ```kotlin class Foo @@ -1030,14 +1030,17 @@ class SuperPerson(num: Int) : Person(num) 请记住,即使你没有在父类中显式地添加构造函数,编译器也会在编译代码的时候自动创建一个空构造函数。 假如我们不想为Person类添加构造函数,因此编译器在编译代码的时候创建了一个空构造函数。该构造函数通过使用Person()被调用。 -注意: 上面在说到继承的时候`class SuperPerson(num: Int) : Person(num)`在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。 -Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。 -但是如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin -中是允许类中只有次构造函数,没有主构造函数的。当一个类没有显式的定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。 -如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。 +***注意: 上面在说到继承的时候`class SuperPerson(num: Int) : Person(num)`在父类后面必须加上括号,这是为了能够调用到父类的主构造函数。 +Kotlin中规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)。*** +***但是如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 这里很特殊,在Kotlin +中是允许类中只有次构造函数,没有主构造函数的。当一个类没有显式的定义主构造函数且定义了次构造函数时,它就是没有主构造函数的。*** + +***如果该类有一个主构造函数,其基类必须用基类型的主构造函数参数就地初始化。*** + +***如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。*** + -如果类没有主构造函数,那么每个次构造函数必须使用`super`关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数: ```kotlin @@ -1046,11 +1049,15 @@ class MyView : View { constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs) } ``` -也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。那么既然没有主构造函数,继承View类 -的时候也就不需要再在View类后加上括号了。其实原因就是这么简单,只是很多人在刚开始学习Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的 + +也就是MyView类的后面没有显式的定义主构造函数,同时又定义了次构造函数。所以现在MyView类是没有主构造函数的。 + +***那么既然没有主构造函数,继承View类的时候也就不需要再在View类后加上括号了。*** + +其实原因就是这么简单,只是很多人在刚开始学习Kotlin的时候没能理解这对括号的意义和规则,因此总感觉继承的 写法有时候要加上括号,有时候又不要加,搞得晕头转向的,而在你真正理解了规则之后,就会发现其实还是很好懂的。 -另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。 +***另外,由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将this关键字换成了super关键字,这部分就很好理解了。*** ### Any @@ -1283,14 +1290,15 @@ Unit与Int一样,都是一种类型,然而它不代表任何信息,用面 ### 表达式函数体 -如果返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号: +***如果一个函数的返回的结果可以使用一个表达式计算出来,你可以不使用括号而是使用等号***: ```kotlin fun add(x: Int,y: Int) : Int = x + y // 省略了{} ``` -Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。如你所见,在使用表达式函数体 -的情况下我们可以不声明返回值类型,这进一步简化了语法。 +***Kotlin支持这种单行表达式与等号的语法来定义函数,叫做表达式函数体,作为区分,普通的函数声明则可以叫做代码块函数体。*** + +如你所见,在使用表达式函数体的情况下我们可以不声明返回值类型,这进一步简化了语法。 我们可以给参数指定一个默认值使的它们变的可选,这是非常有帮助的。这里有一个例子,在`Activity`中创建了一个函数用来`Toast`一段信息: @@ -1367,7 +1375,7 @@ val mList2 = vars(0, *myArray, 6, 7) ### 类布局 -通常,一个类的内容按以下顺序排列: +通常,一个类的内容按以下顺序排列: - 属性声明与初始化块 - 次构造函数 From 7d12b58dc9bc2400c9a76ef722c243ccda98354d Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 14 May 2024 21:29:59 +0800 Subject: [PATCH 097/128] =?UTF-8?q?Update=202.Kotlin=5F=E9=AB=98=E9=98=B6?= =?UTF-8?q?=E5=87=BD=E6=95=B0&Lambda&=E5=86=85=E8=81=94=E5=87=BD=E6=95=B0.?= =?UTF-8?q?md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...05\350\201\224\345\207\275\346\225\260.md" | 98 ++++++++++++------- 1 file changed, 62 insertions(+), 36 deletions(-) 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 dba30dbf..166994ce 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" @@ -30,7 +30,7 @@ fun lock(lock: Lock, body: () -> T): T { 调用lock函数时,可以传入另一个函数作为参数。 -和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面,如: +***和许多编程语言类似,如果函数的最后一个参数也是函数,则该函数参数还可以定义在括号外面***,如: ```kotlin val result = lock(lock, { sharedResource.operation()}) // 等同于 @@ -42,7 +42,7 @@ lock(lock) { 高阶函数类似C语言的函数指针,它的另一个使用场景是map函数。 -如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。 +***如果函数只有一个参数,则可以忽略声明的函数参数,用it来代替。*** ```kotlin val doubled = mMap.map {it -> it * 2} @@ -183,7 +183,8 @@ class CountryTest { - 需要把isBigEuropeanCountry的方法引用当做参数传递给filterCountries - Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写: + **Kotlin存在一种特殊的语法,通过两个冒号来实现对于某个类的方法进行引用(方法引用表达式)。** + 以上面的代码为例,假如我们有一个CountryTest类的对象实例countryTest,如果要引用它的isBigEuropeanCountry方法,就可以这样写: ```kotlin countryTest::isBigEuropeanCountry @@ -307,26 +308,27 @@ val foo = { x: Int -> ```java // 没有使用Lambda的老方法: button.addActionListener(new ActionListener(){ - public void actionPerformed(ActionEvent ae){ - System.out.println("Actiondetected"); - } + public void actionPerformed(ActionEvent ae){ + System.out.println("Actiondetected"); + } }); // 使用Lambda: -button.addActionListener(()->{ - System.out.println("Actiondetected"); +button.addActionListener(() -> { + System.out.println("Actiondetected"); }); // 不采用Lambda的老方法: -Runnable runnable1=new Runnable(){ - @Override - public void run(){ - System.out.println("RunningwithoutLambda"); - } +Runnable runnable1 = new Runnable() { + @Override + public void run() { + System.out.println("RunningwithoutLambda"); + } }; + // 使用Lambda: -Runnable runnable2=()->{ - System.out.println("RunningfromLambda"); +Runnable runnable2 = () -> { + System.out.println("RunningfromLambda"); }; ``` @@ -357,10 +359,10 @@ max(strings, compare) ``` 就是找出`strings`里面最长的那个。但是我个人觉得`compare`还是很碍眼的,因为我并不想在后面引用他,那我怎么办呢,就是用“匿名函数”方式。 ```kotlin -max(strings, (a,b)->{a.length < b.length}) +max(strings, (a,b) -> {a.length < b.length}) ``` -`(a,b)->{a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: +`(a,b) -> {a.length < b.length}`就是一个没有名字的函数,直接作为参数赋给`max`方法的第二个参数。但这个方法有很多东西都没有写明,如: - 参数的类型 - 返回值的类型 @@ -409,7 +411,7 @@ val msg = { x: Int -> "xxx" } (Int) -> String ``` -如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用: +如果将lambda赋值给一个变量,编译器会根据该lambda来推测变量的类型,如上例所示。然而,就像任何其他类型的对象一样,你可以显式地定义该变量的类型。例如,以下代码定义了一个变量add,该变量可以保存对具有两个Int参数并返回Int类型的lambda的引用: ```kotlin val add: (Int, Int) -> Int @@ -420,7 +422,8 @@ add = { x: Int, y: Int -> x + y } Lambda类型也被认为是函数类型。 -当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数. +**当Lambda表达式的参数列表中只有一个参数时,那么可以不声明唯一的参数名,而是可以使用it关键字来代替,因为Kotlin会隐含的声明一个名为it的参数.这叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数** + ```kotlin val list = listOf("Apple", "Bnana", "Orange", "Pear") val maxLengthFruit = list.maxBy {it.length} @@ -435,13 +438,13 @@ fun foo(int: Int) = { listOf(1, 2, 3).forEach { foo(it) } // 对一个整数列表的元素遍历调用foo ``` -这里,你可定会纳闷it是啥?其实它也是Kotlin简化Lambda表达的一种语法糖,叫做单个参数的隐式名称,代表了这个Lambda所接收的单个参数。这里的调用等价于: +这里的调用等价于: ```kotlin listOf(1, 2, 3).forEach { item -> foo(item) } ``` -如果lambda具有一个单独的参数,而且编译器能够推断其类型,你可以省略该参数,并在lambda的主体中使用关键字it指代它。要了解它是如何工作的,如前所述,假设使用以下代码将lambda赋值给变量: +假设使用以下代码将lambda赋值给变量: ```kotlin val addFive: (Int) -> Int = { x -> x + 5 } @@ -453,7 +456,11 @@ val addFive: (Int) -> Int = { x -> x + 5 } val addFive: (Int) -> Int = { it + 5 } ``` -在上述代码中,{it+5}等价于{x->x+5},但更加简洁。请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。例如,以下代码将无法编译,因为编译器不知道it应该是什么类型: +在上述代码中,{it+5}等价于{x->x+5},但更加简洁。 + +请注意,你只能在编译器能够推断该参数类型的情况下使用it语法。 + +例如,以下代码将无法编译,因为编译器不知道it应该是什么类型: ```kotlin val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断其类型 @@ -463,7 +470,7 @@ val addFive = { it + 5 } // 该代码无法编译,因为编译器不能推断 -我们看一下foo函数用IDE转换后的Java代码: +我们看一下foo函数用IDE转换后的Java代码: ```java @JvmStatic @@ -520,13 +527,14 @@ listOf(1, 2, 3).forEach{ foo(it)() } ## 闭包 -闭包就是能够读取其他函数内部变量的函数。 +**闭包就是能够读取其他函数内部变量的函数。** 它是函数内部和函数外部信息交换的桥梁。 -在Kotlin中,Lambda表达式或匿名函数(局部函数、对象表达式等)都可以访问它的必报。 +在Kotlin中,Lambda表达式或匿名函数(局部函数、对象表达式等)都可以访问它的闭包。 -在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。 +在Kotlin中,你会发现匿名函数体、Lambda在语法上都存在“{}",由这对花括号包裹的代码如果访问了外部环境变量则被称为一个闭包。 +一个闭包可以被当做参数传递或直接使用,它可以简单的看成”访问外部环境变量的函数“。Lambda是Kotlin中最常见的闭包形式。 与Java不一样的地方在于,Kotlin中的闭包不仅可以访问外部变量,还能够对其进行修改(我有点疑惑,Java为啥不能修改?下面说),如下: @@ -539,8 +547,6 @@ listOf(1, 2, 3).filter { it > 0 }.forEach { println(sum) // 6 ``` -看到这里我是懵逼的? 到底什么是闭包? 闭包有什么作用? - 闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。--百度百科 第一句总结的很简洁了:闭包就是能够读取其他函数内部变量的函数。 @@ -623,7 +629,7 @@ What java docs says about “Local Inner class cannot access the non-final local -JVM create create a synthetic field inside the inner class in java - +JVM create a synthetic field inside the inner class in java - As final variable will not change after initialization, when a inner class access final local variables **compiler create a synthetic field inside the inner class and also copy that variable into the heap.** So, these synthetic fields can be accessed inside local inner class even when execution of method is over in java. @@ -680,7 +686,9 @@ fun main() { 是不是发现了新世界的大门,内部函数很轻松地调用了外部变量a。 -这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。不过万变不离其宗,只要记得一句话:**闭包就是能够读取其他函数内部变量的函数**。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。 +这只是一个最简单的闭包实现。按照这种思想,其他的实现例如:函数、条件语句、Lambda表达式等等都可以理解为闭包,这里不再赘述。 + +不过万变不离其宗,只要记得一句话:**闭包就是能够读取其他函数内部变量的函数**。就是一个函数A可以访问另一个函数B的局部变量,即便另一个函数B执行完成了也没关系。目前把满足这样条件的函数A叫做闭包。 @@ -688,13 +696,21 @@ fun main() { ## 内联函数 -刚被闭包搞蒙,这里又没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。 +刚被闭包搞蒙,这里又没搞明白内联函数到底是干什么? 有什么作用?Kotlin中的内联函数其实显得有点尴尬,因为它之所以被设计出来,主要是为了优化Kotlin支持Lambda表达式之后所带来的开销。 + +然而,在Java中我们似乎并不需要特别关注这个问题,因为在Java 7之后,JVM引入了一种叫做invokedynamic的技术,它会自动帮助我们做Lambda优化。但是为什么Kotlin要引入内联函数这种手动的语法呢? 这主要还是因为Kotlin要兼容Java 6。 ## 优化Lambda开销 -在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 +在Kotlin中每声明一个Lambda表达式,就会在字节码中产生一个匿名类(也就是说我们一直使用的Lambda表达式在底层被转换成了匿名类的实现方式)。 + +该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的匿名类对象。 + +可想而知,Lambda语法虽然简洁,但是额外增加的开销也不少。并且,如果Lambda捕捉了某个变量,那么每次调用的时候都会创建一个新的对象,这样导致效率较低。 + +尤其对Kotlin这门语言来说,它当今优先要实现的目标,就是在Android这个平台上提供良好的语言特性支持。Kotlin要在Android中引入Lambda语法,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。 #### 1. invokedynamic @@ -710,7 +726,9 @@ fun main() { invokedynamic固然不错,但Kotlin不支持它的理由似乎也很充分,我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致它无法通过invovkedynamic来解决Android平台的Lambda开销问题。 -因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 +因此,作为另一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。 + +简单的来说,我们可以用inline关键字来修饰函数,这些函数就称为了内联函数。他们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。 所以内联函数的工作原理并不复杂,就是Kotlin编译器会将内敛函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。 所以如果你想在用Kotlin开发时获得尽可能良好的性能支持,以及控制匿名类的生成数量,就有必要来学习下内联函数的相关语法。 @@ -975,7 +993,9 @@ fun main() { println("main end") } ``` -这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下: +这里定义了一个叫作printString()的高阶函数,用于在Lambda表达式中打印传入的字符串参数。但是如果字符串参数为空,那么就不进行打印。 + +注意,Lambda表达式中是不允许直接使用return关键字的,这里使用了return@printString的写法,表示进行局部返回,并且不再执行Lambda表达式的剩余部分代码。现在我们就刚好传入一个空的字符串参数,运行程序,打印结果如下: ``` main start printString begin @@ -1024,7 +1044,9 @@ inline fun runRunnable(block: () -> Unit) { ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/inline_noline_error.png?raw=true) -这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。 +这个错误出现的原因解释起来可能会稍微有点复杂。首先,在runRunnable()函数中,我们创建了一个Runnable对象,并在Runnable的Lambda表达式中调用了传入的函数类型参数。 + +而Lambda表达式在编译的时候会被转换成匿名类的实现方式,也就是说,上述代码实际上是在匿名类中调用了传入的函数类型参数。 而内联函数所引用的Lambda表达式允许使用return关键字进行函数返回,但是由于我们是在匿名类中调用的函数类型参数,此时是不可能进行外层调用函数返回的,最多只能对匿名类中的函数调用进行返回,因此这里就提示了上述错误。 @@ -1064,7 +1086,11 @@ Error: (2, 11) Kotlin: 'return' is not allowed here #### 具体化参数类型 -除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。 +除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型。 + +**Kotlin与Java一样,由于运行时的类型擦除,我们并不能直接获取一个参数的类型。** + +**然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型。我们可以用reified修饰符来实现这一效果。** ```kotlin fun main(args: Array) { From fdfc03caf2d7944992e601e2a3756d5072474d37 Mon Sep 17 00:00:00 2001 From: Charon Date: Tue, 14 May 2024 22:18:12 +0800 Subject: [PATCH 098/128] =?UTF-8?q?Update=203.Kotlin=5F=E6=95=B0=E5=AD=97&?= =?UTF-8?q?=E5=AD=97=E7=AC=A6=E4=B8=B2&=E6=95=B0=E7=BB=84&=E9=9B=86?= =?UTF-8?q?=E5=90=88.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...0\347\273\204&\351\233\206\345\220\210.md" | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) 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 213e4c91..f4d67cad 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" @@ -193,11 +193,10 @@ val str = "$s.length is ${s.length}" // 求值结果为 "abc.length is 3" ### 字符串判等 -Kotlin中的判等性主要有两种类型: +Kotlin中的判等性主要有两种类型: -- 结构相等。通过操作符==来判断两个对象的内容是否相等。 -- 引用相等。通 -引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。如果比较的是运行时的原始类型,比如Int,那么===判断的效果也等价于==。 +- 结构相等: 通过操作符==来判断两个对象的内容是否相等。 +- 引用相等: 引用相等由`===`以及其否定形式`!===`操作判断。`a === b`当且仅当`a`和`b`指向同一个对象时求值为`true`。如果比较的是运行时的原始类型,比如Int,那么===判断的效果也等价于==。 ```kotlin var a = "Java" @@ -367,7 +366,7 @@ val newList = list.map{it * 2} // 对集合遍历,在遍历过程中,给每 val mStudents = students.filter{it.sex == "m"} // 筛选出性别为男的学生 -val scoreTotal = students.sumBy{it.score} // 拥挤和中的sumby实现求和 +val scoreTotal = students.sumBy{it.score} // 用集合中的sumby实现求和 ``` #### 通过序列提高效率 @@ -391,7 +390,7 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() toList方法将序列转为列表。将list转换为序列,在很大程度上就提高了上面操作集合的效率。 因为在使用序列的时候filter方法和map方法的操作都没有创建额外的集合,这样当集合中的元素数量巨大的时候, 就减少了大部分开销。在Kotlin中,序列中元素的求值是惰性的,这就意味着在利用序列进行链式求值的时候, -不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么什么惰性又是什么意思呢? +不需要像操作普通集合那样,每进行一次求值操作,就产生一个新的集合保存中间数据。那么惰性又是什么意思呢? #### 惰性求值 在编程语言理论中,惰性求值(Lazy Evaluation)表示一种在需要时才进行求值的计算方式。 @@ -406,10 +405,10 @@ toList方法将序列转为列表。将list转换为序列,在很大程度上 list.asSequence().filter {it > 2}.map {it * 2}.toList() ``` -在这个例子中,我们序列总共执行了两类操作分别是: +在这个例子中,我们序列总共执行了两类操作分别是: -- `filter{it > 2}.map{it * 2}`:filter和map的操作返回的都是序列,我们将这类操作称为中间操作。 -- `toList()`:这一类操作将序列转换为List,我们将这类操作称为末端操作。 +- `filter{it > 2}.map{it * 2}`: filter和map的操作返回的都是序列,我们将这类操作称为中间操作。 +- `toList()`: 这一类操作将序列转换为List,我们将这类操作称为末端操作。 其实,Kotlin中序列的操作就分为两类: @@ -432,10 +431,10 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() - 末端操作 在对集合进行操作的时候,大部分情况下,我们在意的只是结果,而不是中间过程。 - 末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果, - 比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾, - 在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了, - 我们给上面的例子加上末端操作: + 末端操作就是一个返回结果的操作,它的返回值不能是序列,必须是一个明确的结果, + 比如列表、数字、对象等表意明确的结果。末端操作一般都放在链式操作的末尾, + 在执行末端操作的时候,会去触发中间操作的延迟计算,也就是将”被需要“这个状态打开了, + 我们给上面的例子加上末端操作: ```kotlin list.asSequence().filter { @@ -458,10 +457,10 @@ list.asSequence().filter {it > 2}.map {it * 2}.toList() ``` 可以看到,所有的中间操作都被执行了。从上面执行打印的结果我们发现,它的执行顺序与我们预想的不一样。 - 普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。 - 而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后, - 第二个元素再去执行所有的操作,以此类推。放到我们这个例子上面,就是第一个元素执行了filter之后再去执行map, - 然后第二个元素也是这样。 + 普通集合在进行链式操作的时候会先在list上调用filter,然后产生一个结果列表,接下来map就在这个结果列表上进行操作。 + 而序列则不一样,序列在执行链式操作的时候,会将所有的操作都应用在一个元素上,也就是说,第一个元素执行完所有的操作之后, + 第二个元素再去执行所有的操作,以此类推。放到我们这个例子上面,就是第一个元素执行了filter之后再去执行map, + 然后第二个元素也是这样。 #### 序列可以是无限的 @@ -525,7 +524,7 @@ if(str == null) { } ``` -那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的符号:双感叹号`!!`,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: +那么如果我们需要的就是一个`Int`的结果(事实上大部分情况都是如此),那又该怎么办呢?在`kotlin`中除了`?`表示可为空以外,还有一个新的双感叹号`!!`符号,表示一定不能为空。所以上面的例子,如果要对`result`进行操作,可以这么写: ```kotlin var str : String? = null From 7267cf5dcaefde553324d5d80f49890063067d69 Mon Sep 17 00:00:00 2001 From: Charon Date: Thu, 16 May 2024 17:24:51 +0800 Subject: [PATCH 099/128] =?UTF-8?q?Update=205.Kotlin=5F=E5=86=85=E9=83=A8?= =?UTF-8?q?=E7=B1=BB&=E5=AF=86=E5=B0=81=E7=B1=BB&=E6=9E=9A=E4=B8=BE&?= =?UTF-8?q?=E5=A7=94=E6=89=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...2\344\270\276&\345\247\224\346\211\230.md" | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) 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 f3f9117d..ce13420d 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" @@ -43,7 +43,7 @@ inline fun startActivity(context: Context) { context.startActivity(intent) } ``` -这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来就是神奇的地方了,Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在,如果我们想要启动TestActivity,只需要这样写就可以了:[插图]Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity。 +这里我们定义了一个startActivity()函数,该函数接收一个Context参数,并同时使用inline和reified关键字让泛型T成为了一个被实化的泛型。接下来就是神奇的地方了,Intent接收的第二个参数本来应该是一个具体Activity的Class类型,但由于现在T已经是一个被实化的泛型了,因此这里我们可以直接传入T::class.java。最后调用Context的startActivity()方法来完成Activity的启动。现在,如果我们想要启动TestActivity,只需要像下面这样写就可以了,Kotlin将能够识别出指定泛型的实际类型,并启动相应的Activity: ```kotlin startActivity(context) ``` @@ -55,7 +55,7 @@ intent.putExtra("param1", "data") intent.putExtra("param2", 123) context.startActivity(intent) ``` -而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助之前在第6章学习的高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示: +而经过刚才的封装之后,我们就无法进行传参了。这个问题也不难解决,只需要借助之前学习的高阶函数就可以轻松搞定。回到reified.kt文件当中,这里添加一个新的startActivity()函数重载,如下所示: ```kotlin inline fun startActivity(context: Context, block: Intent.() -> Unit) { val intent = Intent(context, T::class.java) @@ -187,13 +187,11 @@ mViewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { override fun onPageSelected(position: Int) { TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } - }) ``` 如果对象实例是一个函数接口,还可以使用Lambda表达式来实现。 ```kotlin - val listener = ActionListener { println("clicked") } @@ -253,7 +251,7 @@ enum class MessageType(var msg: String) { } ``` -但是使用这种方法存在两个问题: +但是使用这种方法存在两个问题: - 每个值都是一个常量,并且仅作为单个实例存在。 @@ -267,7 +265,11 @@ enum class MessageType(var msg: String) { ### 密封类 -Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承。密封类就像枚举类的加强版本。它允许你将类层次结构限制为一组特定的子类型,每个子类型都可以定义自己的属性和函数。与枚举类不同,你可以创建每种类型的多个实例。 你可以通过使用sealed前缀类名来创建密封类。例如,以下代码创建一个名为MessageType的密封类,其中包含名为MessageSuccess和MessageFailure的子类型。每个子类型都有一个名为msg的String属性,并且MessageFailure子类型中有一个额外的名为e的Exception属性: +Kotlin除了可以利用final来限制类的继承之外,还可以通过密封类的语法来限制一个类的继承。密封类就像枚举类的加强版本。 + +它允许你将类层次结构限制为一组特定的子类型,每个子类型都可以定义自己的属性和函数。与枚举类不同,你可以创建每种类型的多个实例。 + +你可以通过使用sealed前缀类名来创建密封类。例如,以下代码创建一个名为MessageType的密封类,其中包含名为MessageSuccess和MessageFailure的子类型。每个子类型都有一个名为msg的String属性,并且MessageFailure子类型中有一个额外的名为e的Exception属性: ```kotlin sealed class MessageType @@ -276,7 +278,7 @@ class MessageSuccess(var msg: String) : MessageType() class MessageFailure(var msg: String, var e: Exception) : MessageType() ``` -由于MessageType是一个有限子类的密封类。你可以使用when来检查每个子类型,这样可以避免使用额外的else子句,如以下代码所示: +由于MessageType是一个有限子类的密封类。你可以使用when来检查每个子类型,这样可以避免使用额外的else子句,如以下代码所示: ```kotlin fun main(args: Array) { @@ -328,7 +330,8 @@ public abstract class Bird { ``` -密封类用来表示受限的类继承结构:当一个值为有限几种的类型、而不能有任何其他类型时。在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。 +密封类用来表示受限的类继承结构: 当一个值为有限几种的类型、而不能有任何其他类型时。 +在某种意义上,他们是枚举类的扩展:枚举类型的值集合也是受限的,但每个枚举常量只存在一个实例,而密封类的一个子类可以有可包含状态的多个实例。 虽然密封类也可以有子类,但是所有子类都必须在与密封类自身相同的文件中声明。(在`Kotlin 1.1`之前,该规则更加严格:子类必须嵌套在密封类声明的内部)。 @@ -359,7 +362,7 @@ fun eval(expr: Expr): Double = when(expr) { ### 异常 -在`Kotlin`中,所有的`Exception`都是实现了`Throwable`,含有一个`message`且未经检查。这表示我们不会强迫我们在任何地方使用`try/catch`。 +在`Kotlin`中,所有的`Exception`都是实现了`Throwable`,含有一个`message`且未经检查。这表示我们不会强迫我们在任何地方使用`try/catch`。 这与`Java`中不太一样,比如在抛出`IOException`的方法,我们需要使用`try-catch`包围代码块。但是通过检查`exception`来处理显示并不是一个 好的方法。 @@ -372,11 +375,9 @@ throw MyException("Exception message") ```kotlin try{ // 一些代码 -} -catch (e: SomeException) { +} catch (e: SomeException) { // 处理 -} -finally { +} finally { // 可选的finally块 } ``` @@ -401,7 +402,8 @@ val s = try { x as String } catch(e: ClassCastException) { null } 在Kotlin中,你将告别static这种语法,因为它引入了全新的关键字object,可以完美的代替使用static的所有场景。 当然除了代替static的场景之外,它还能实现更多的功能,比如单例对象及简化匿名表达式等。 -声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问它们。事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 +声明对象就如同声明一个类,你只需要用保留字`object`替代`class`,其他都相同。只需要考虑到对象不能有构造函数,因为我们不调用任何构造函数来访问它们。 +事实上,对象就是具有单一实现的数据类型。object声明的内容可以看成没有构造方法的类,它会在系统或者类加载时进行初始化。 ```kotlin object Resource { @@ -427,7 +429,8 @@ recycler.adapter = object : RecyclerView.Adapter() { ``` 例如,每次想要创建一个接口的内联实现,或者扩展另一个类时,你将使用上面的符号。 -object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? 当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 +object表达式和匿名内部类很像,那对象表达式与Lambda表达式哪个更适合代替匿名内部类呢? +当你的匿名内部类使用的类接口只需要实现一个方法时,使用Lambda表达式更适合。当匿名内部类内有多个方法实现的时候,使用object表达式更适合。 #### 伴生对象 @@ -543,9 +546,12 @@ class App : Application() { `lateinit`表示这个属性开始是没有值的,但是,在使用前将被赋值(否则,就会抛出异常)。 -不过,上面companion object中的isRedpack()方法其实也并不是静态方法,companionobject这个关键字实际上会在类的内部创建一个伴生类,而isRedpack方法就是定义在这个伴生类里面的实例方法。只是Kotlin会保证类始终只会存在一个伴生类对象,因此调用Prize.isRedpack()方法实际上就是调用了Prize类中伴生对象的isRedpack()方法。由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。 +不过,上面companion object中的isRedpack()方法其实也并不是静态方法,companion object这个关键字实际上会在类的内部创建一个伴生类,而isRedpack方法就是定义在这个伴生类里面的实例方法。 + +只是Kotlin会保证类始终只会存在一个伴生类对象,因此调用Prize.isRedpack()方法实际上就是调用了Prize类中伴生对象的isRedpack()方法。 +由此可以看出,Kotlin确实没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,这些语法特性基本可以满足我们平时的开发需求了。然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。 -先来看注解,前面使用的单例类和companion object都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在Java代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。而如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法 +先来看注解,前面使用的单例类和companion object都只是在语法的形式上模仿了静态方法的调用方式,实际上它们都不是真正的静态方法。因此如果你在Java代码中以静态方法的形式去调用的话,你会发现这些方法并不存在。而如果我们给单例类或companion object中的方法加上@JvmStatic注解,那么Kotlin编译器就会将这些方法编译成真正的静态方法。 注意,@JvmStatic注解只能加在单例类或companion object中的方法上,如果你尝试加在一个普通方法上,会直接提示语法错误。那么现在不管是在Kotlin中还是在Java中,都可以使用Prize.isRedpack()的写法来调用了。 From 776b49b11bbc7ea4a9ad810c34b35105394e74a7 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 20 May 2024 17:43:45 +0800 Subject: [PATCH 100/128] =?UTF-8?q?Update=208.Kotlin=5F=E5=8D=8F=E7=A8=8B.?= =?UTF-8?q?md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 8fbfda6e..2c319266 100644 --- "a/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" +++ "b/KotlinCourse/8.Kotlin_\345\215\217\347\250\213.md" @@ -31,11 +31,12 @@ Kotlin引入了协程(Coroutine)来支持更好的异步操作,利用它 可以说,只要内存足够,一个线程中可以包含任意多个协程,但某一时刻却只能有一个协程在运行,多个协程分享该线程资源。 ## 进程、线程、协程 -进程:一段程序的执行过程,资源分配和调度的基本单位,有其独立地址空间,互相之间不发生干扰 -线程:轻量级进程,资源调度的最小单位,共享父进程地址空间和资源,其调度和进程一样要切换到内核态 -并行:同时发生,在多核CPU中,多个任务可同时在不同CPU上面同一时间执行 -并发:宏观并行,微观串行,操作系统根据相关算法,分配时间片来调度,从而达到一种宏观上并行的方式 -上下文:程序执行的状态,通常用调用栈记录程序执行的当前状态以及其相关的环境信息 + +- 进程:一段程序的执行过程,资源分配和调度的基本单位,有其独立地址空间,互相之间不发生干扰 +- 线程:轻量级进程,资源调度的最小单位,共享父进程地址空间和资源,其调度和进程一样要切换到内核态 +- 并行:同时发生,在多核CPU中,多个任务可同时在不同CPU上面同一时间执行 +- 并发:宏观并行,微观串行,操作系统根据相关算法,分配时间片来调度,从而达到一种宏观上并行的方式 +- 上下文:程序执行的状态,通常用调用栈记录程序执行的当前状态以及其相关的环境信息 早期,CPU是单核,无法真正并行,为了产生共享CPU的假象,提出了时间片概念,将时间分割成连续的时间片段,多个程序交替获得CPU使用权限。 而管理时间片分配调度的调度器则成为操作系统的核心组件。 From 3d564a70e0fbc37e6e3e65b60e8d6e07b7684b18 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 22 May 2024 20:59:20 +0800 Subject: [PATCH 101/128] update --- ...2\344\270\276&\345\247\224\346\211\230.md" | 4 +-- ...45\231\250\350\257\255\350\250\200GLSL.md" | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) 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 ce13420d..d738c4b2 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" @@ -520,7 +520,7 @@ class Prize private constructor(val name: String, val count: Int, val type: Int) fun main(args: Array) { val redpackPrize = Prize.newRedpackPrize("hongbao", 10) val couponPrize = Prize.newCouponPrize("shiyuan", 10) - vval commonPrize = Prize.defaultCommonPrize() + val commonPrize = Prize.defaultCommonPrize() } } ``` @@ -903,7 +903,7 @@ void test(){ ```kotlin val p by lazy { ... } ``` -现在再来看这段代码,是不是觉得更有头绪了呢?实际上,bylazy并不是连在一起的关键字,只有by才是Kotlin中的关键字, +现在再来看这段代码,是不是觉得更有头绪了呢?实际上,by lazy并不是连在一起的关键字,只有by才是Kotlin中的关键字, lazy在这里只是一个高阶函数而已。在lazy函数中会创建并返回一个Delegate对象,当我们调用p属性的时候, 其实调用的是Delegate对象的getValue()方法,然后getValue()方法中又会调用lazy函数传入的Lambda表达式, 这样表达式中的代码就可以得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index ac66269e..cac05def 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -53,9 +53,9 @@ OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的 - OpenGL ES 3.0的shader中没有texture2D()和texture3D等了,全部使用texture()方法替换。 -- 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4]; +- 可直接使用layout制定属性值。 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4];,如果不限定该属性的位置为2,需要通过glGetAttribuLocation()查询 -- 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。 +- 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。不过保留了gl_Position - GL_OES_EGL_image_external被废弃 @@ -275,6 +275,24 @@ vec2 vect = vec2(0.5, 0.7); vec4 result = vec4(vect, 0.0, 0.0); ``` +#### vec4表示矩形 + +通常,一个矩形可以用其左上角位置(x, y),宽度(width),高度(height)来定义。我们可以将这些值存储在一个 vec4 中,如下: + +```glsl +vec4 rect = vec4(x, y, width, height); +``` + +这里的 rect 向量表示: + +- rect.x 是矩形的左上角的 x 坐标。 +- rect.y 是矩形的左上角的 y 坐标。 +- rect.z 是矩形的宽度。 +- rect.w 是矩形的高度。 + + + + - 矩阵 在GLSL中矩阵拥有2 * 2、3 * 3、4 * 4三种类型的矩阵,分别用mat2、mat3、mat4表示。 @@ -284,6 +302,19 @@ vec4 result = vec4(vect, 0.0, 0.0); 矩阵可以通过(i, j)进行索引,i是行,j是列,这就是上面的矩阵叫做2×3矩阵的原因(3列2行,也叫做矩阵的维度(Dimension))。这与你在索引2D图像时的(x, y)相反,获取4的索引是(2, 1)(第二行,第一列)(译注:如果是图像索引应该是(1, 2),先算列,再算行) +矩阵的构造方法则更加灵活,有以下规则: + +- 如果对矩阵构造器只提供了一个标量参数,该值会作为矩阵的对角线上的值。例如 mat4(1.0) 可以构造一个 4 × 4 的单位矩阵 +- 矩阵可以通过多个向量作为参数来构造,例如一个 mat2 可以通过两个 vec2 来构造 +- 矩阵可以通过多个标量作为参数来构造,矩阵中每个值对应一个标量,按照从左到右的顺序 + +```glsl +mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列 + 0.0, 1.0, 0.0, // 第二列 + 0.0, 1.0, 1.0); // 第三列 +``` + + - 采样器 采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 From 3012d7c8d202ccd750dae9bc98984d1743186ac5 Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Tue, 28 May 2024 12:19:35 +0800 Subject: [PATCH 102/128] update OpenGL --- ...45\231\250\350\257\255\350\250\200GLSL.md" | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index cac05def..8dbd031b 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -1,5 +1,16 @@ ## 7.OpenGL ES着色器语言GLSL +### 齐次坐标(Homogeneous coordinates) + +三维顶点视为三元组(x,y,z)。现在引入一个新的分量w,得到向量(x,y,z,w)。请先记住以下两点: + +若w==1,则向量(x, y, z, 1)为空间中的点。 +若w==0,则向量(x, y, z, 0)为方向。 + +齐次坐标使得我们可以用同一个公式对点和方向作运算。 + + + 着色器是使用一种叫GLSL的类C语言写成的。GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。 着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。 @@ -315,6 +326,37 @@ mat3 myMat3 = mat3(1.0, 0.0, 0.0, // 第一列 ``` +三维图形学中我们只用到4x4矩阵,它能对顶点(x,y,z,w)作变换。这一变换是用矩阵左乘顶点来实现的: + +矩阵x顶点(记住顺序!!矩阵左乘顶点,顶点用列向量表示)= 变换后的顶点 + +![image](https://github.com/CharonChui/Pictures/blob/master/MatrixXVect.gif?raw=true) + +这看上去复杂,实则不然。左手指着a,右手指着x,得到ax。 左手移向右边一个数b,右手移向下一个数y,得到by。依次类推,得到cz、dw。最后求和ax + by + cz + dw,就得到了新的x!每一行都这么算下去,就得到了新的(x, y, z, w)向量。 + + +这种重复无聊的计算就让计算机代劳吧。 + +用C++,GLM表示: +```c++ +glm::mat4 myMatrix; +glm::vec4 myVector; +// fill myMatrix and myVector somehow +glm::vec4 transformedVector = myMatrix * myVector; // Again, in this order ! this is important. +``` + +用GLSL表示: + +```glsl +mat4 myMatrix; +vec4 myVector; +// fill myMatrix and myVector somehow +vec4 transformedVector = myMatrix * myVector; // Yeah, it's pretty much the same than GLM +``` + + + + - 采样器 采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 From 77a3f3b3f2571fbb50ec361d95a1dd37e31adf51 Mon Sep 17 00:00:00 2001 From: xuchuanren Date: Thu, 6 Jun 2024 16:36:05 +0800 Subject: [PATCH 103/128] update OperatingSystem --- OperatingSystem/{5.I:O.md => 5.IO.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename OperatingSystem/{5.I:O.md => 5.IO.md} (100%) diff --git a/OperatingSystem/5.I:O.md b/OperatingSystem/5.IO.md similarity index 100% rename from OperatingSystem/5.I:O.md rename to OperatingSystem/5.IO.md From f29e3a6f3a2c287fdac48a462082a7f179f4784f Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 6 Jun 2024 16:51:08 +0800 Subject: [PATCH 104/128] update OpenGL part --- .../OpenGL/1.OpenGL\347\256\200\344\273\213.md" | 6 +++++- ...30\345\210\266\344\270\211\350\247\222\345\275\242.md" | 2 ++ ...51\345\275\242\345\217\212\345\234\206\345\275\242.md" | 2 ++ ...50\211\262\345\231\250\350\257\255\350\250\200GLSL.md" | 8 ++++++++ 4 files changed, 17 insertions(+), 1 deletion(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index 9b8ba320..a347c9e6 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -179,7 +179,8 @@ OpenGL ES 3.0实现了具有可编程着色功能的图形管线,有两个规 -### 立即渲染模式(Immediate mode) && 核心模式(Core-profile) +### 立即渲染模式(Immediate mode) && 核心模式(Core-profile) + 早期的OpenGL使用立即渲染模式(Immediate mode,也就是固定渲染管线),这个模式下绘制图形很方便。OpenGL的大多数功能都被库隐藏起来,开发者很少有控制OpenGL如何进行计算的自由。而开发者迫切希望能有更多的灵活性。随着时间推移,规范越来越灵活,开发者对绘图细节有了更多的掌控。立即渲染模式确实容易使用和理解,但是效率太低。因此从OpenGL3.2开始,规范文档开始废弃立即渲染模式,并鼓励开发者在OpenGL的核心模式(Core-profile)下进行开发,这个分支的规范完全移除了旧的特性。 当使用OpenGL的核心模式时,OpenGL迫使我们使用现代的函数。当我们试图使用一个已废弃的函数时,OpenGL会抛出一个错误并终止绘图。现代函数的优势是更高的灵活性和效率,然而也更难于学习。立即渲染模式从OpenGL实际运作中抽象掉了很多细节,因此它在易于学习的同时,也很难让人去把握OpenGL具体是如何运作的。现代函数要求使用者真正理解OpenGL和图形编程,它有一些难度,然而提供了更多的灵活性,更高的效率,更重要的是可以更深入的理解图形编程。 @@ -467,6 +468,9 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 **在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。** + +光栅化后产生了多少个片元,就会插值计算出多少套in变量。同时,渲染管线就会调用多少次片元着色器。可以看出,一般情况下对一个3D物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。 因此,GPU硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量,通过这些硬件单元的并行执行,提高渲染速度。 + 整个处理过程又分为以下几个部分: - 逐片段操作 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index c1779157..1fb4a6e7 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -55,6 +55,8 @@ float vertices[] = { 你后面会看到为什么我们会需要这个位置值。 +layout限定符是从OpenGL ES 3.0开始出现的,其主要用于设置变量的存储索引(即引用)值. + ##### 向量(Vector) 在图形编程中我们经常会使用向量这个数学概念,因为它简明地表达了任意空间中的位置和方向,并且它有非常有用的数学属性。 diff --git "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" index 49d52826..ff975060 100644 --- "a/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/6.OpenGL ES\347\273\230\345\210\266\347\237\251\345\275\242\345\217\212\345\234\206\345\275\242.md" @@ -143,6 +143,8 @@ public abstract class BaseGLSurfaceViewRenderer implements GLSurfaceView.Rendere ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_es_gl_triangle.jpg) +OpenGL ES中仅允许采用三角形来搭建物体。其实这从构造能力上来说并没有区别,因为任何多边形都可以拆分为多个三角形。OpenGL ES中之所以仅支持三角形而不支持任意多边形是出于性能的考虑,就目前移动嵌入式设备的硬件性能情况来看,这是必然的选择了。 + ### 顶点法和索引法 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 8dbd031b..7d5b2fb6 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -129,6 +129,14 @@ void main() 由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。 + + +顶点着色器中用in限定符修饰的变量其值实质是由宿主程序(本书中为Java、C++)批量传入渲染管线的,管线进行基本处理后再传递给顶点着色器。 数据中有多少个顶点,管线就调用多少次顶点着色器,每次将一个顶点的各种属性数据传递给顶点着色器中对应的in变量。因此,顶点着色器每次执行将完成对一个顶点各项属性数据的处理。 + + +顶点着色器每顶点执行一次,而片元着色器每片元执行一次,片元着色器的执行次数明显大于顶点着色器的执行次数。因此在开发中,应尽量减少片元着色器的运算量,可以将一些复杂运算尽量放在顶点着色器中执行。 + + #### 精度限定符 精度限定符使着色器创作者可以指定着色器变量的计算精度。变量可以声明为低、中或者高精度。这些限定符用于提示编译器允许在较低的范围和精度上执行变量计算。在较低的精度上,有些OpenGL ES实现在运行着色器时可能更快,或者电源效率更高。 From b2ad165cbfa21840b4a3755fbed3acb6ffe14216 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 12 Jun 2024 11:51:20 +0800 Subject: [PATCH 105/128] update OpenGL part --- ...ES\347\261\273\345\217\212Matrix\347\261\273.md" | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 7be018c0..103439cb 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -146,6 +146,11 @@ Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的 利用这点,我们构建一个的矩阵,与图形所有的顶点坐标坐标相乘,得到新的顶点坐标集合,当这个矩阵构造恰当的话,新得到的顶点坐标集合形成的图形相对原图形就会出现平移、旋转、缩放或拉伸、抑或扭曲的效果。 Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都是static的,不需要初始化Matrix实例。 + + +OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时,只能再列向量前面乘以矩阵。 +而行向量则反之,否则乘法没有意义。 + - multiplyMM 两个4x4矩阵相乘,并将结果存储到第三个4x4矩阵中。 @@ -253,6 +258,14 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 +### 旋转 + +OpenGL ES中,旋转角度的正负可以用右手螺旋定则来确定。 +所谓右手螺旋定则是指:右手握住旋转轴,使大姆指指向旋转轴的正方向,4指环绕的方向即为旋转的正方向,也就是旋转角度为正值。 + +![](https://raw.githubusercontent.com/CharonChui/Pictures/master/opengl_rotate_angle.png) + + --- - [上一篇: 7.OpenGL ES着色器语言GLSL](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/7.OpenGL%20ES%E7%9D%80%E8%89%B2%E5%99%A8%E8%AF%AD%E8%A8%80GLSL.md) From 99dba190379050fa40eacda9578bf438b94e2417 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 9 Jul 2024 21:52:24 +0800 Subject: [PATCH 106/128] Add Leetcode part --- .../python3\345\205\245\351\227\250.md" | 19 +- ...44\346\225\260\344\271\213\345\222\214.md" | 173 +++++++++++++++ ...44\346\225\260\347\233\270\345\212\240.md" | 210 ++++++++++++++++++ ...00\351\225\277\345\255\220\344\270\262.md" | 190 ++++++++++++++++ ...05\350\201\224\345\207\275\346\225\260.md" | 11 + 5 files changed, 593 insertions(+), 10 deletions(-) create mode 100644 "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" create mode 100644 "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" create mode 100644 "JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index b3d4ae5f..985895a7 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -9,8 +9,6 @@ print(message) ## 字符串 - - 字符串就是一系列字符。在Python中,用引号括起来的都是字符串,其中的引号可以是单引号,也可以是双引号。例如: ```python message = 'Hello World' @@ -20,11 +18,13 @@ print(message) ``` ### 大小写 + 修改单词中的大小写: -title()方法是以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。 -upper()方法将字符串改为全部大写。 -lower()方法将字符串改为全部小写。 +- title()方法是以首字母大写的方式显示每个单词,即将每个单词的首字母都改为大写。 +- upper()方法将字符串改为全部大写。 +- lower()方法将字符串改为全部小写。 + ```python message = 'hello world' print(message.title()) @@ -46,7 +46,7 @@ print(full_name) ### 空格 -rstrip(): 去除字符串末尾空白。注意去除只是暂时的,等你再次访问该变量是,你会发现这个字符串仍然包含末尾空白。 +rstrip(): 去除字符串末尾空白。注意去除只是暂时的,等你再次访问该变量时,你会发现这个字符串仍然包含末尾空白。 要永久删除这个字符串中的空白,必须将删除的结果存回到变量中: @@ -55,8 +55,8 @@ print(first_name) first_name = first_name.rstrip() print(first_name) -lstrip(): 去除字符串开头空白 -strip(): 同时去除字符串两端的空白 +- lstrip(): 去除字符串开头空白 +- strip(): 同时去除字符串两端的空白 ## 整数 @@ -179,7 +179,6 @@ print(size) - 没有缩进的是for循环之外的 -- ```python bicycles = ['trek', 'cannondale', 'redline', 'specialized'] for bcy in bicycles: @@ -369,7 +368,7 @@ for language in set(favorite_languages.values()):❶ 函数input()让程序暂停运行,等待用户输入一些文本。 -获取用户输入后,Python将其村村在一个变量中,以方便你使用。 +获取用户输入后,Python将其存在一个变量中,以方便你使用。 ```python3 age = input("How old are you?") print(age) diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..72102238 --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,173 @@ +1. LeetCode_两数之和 +=== + +给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 + +你可以按任意顺序返回答案。 + + + +示例 1: + +输入:nums = [2,7,11,15], target = 9 +输出:[0,1] +解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 +示例 2: + +输入:nums = [3,2,4], target = 6 +输出:[1,2] +示例 3: + +输入:nums = [3,3], target = 6 +输出:[0,1] + + + +提示: + +2 <= nums.length <= 104 +-109 <= nums[i] <= 109 +-109 <= target <= 109 +只会存在一个有效答案 + + +进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗? + + +### 方法一:暴力枚举 + + +最简单的方法就是对数组中的每一个数x,都去遍历找后面是否存在target - x的值是否存在。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int size = nums.length; + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return new int[]{i, j}; + } + } + } + return new int[0]; + } +} +``` + +```c++ +#include +using namespace std; + + +class Solution { +public: + vector twoSum(vector &nums, int target) { + int size = nums.size(); + + for (int i = 0; i < size; i++) { + for (int j = i + 1; j < size; j++) { + if (nums[i] + nums[j] == target) { + return {i, j}; + } + } + } + return {}; + } +}; +``` + +复杂度分析: + +- 时间复杂度:O(N²),其中N是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。 + +- 空间复杂度:O(1)。 + + +### 方法二: 哈希表 + +注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。 + +因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 + +使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。 + + +--- + +如果想要快速确定某个元素是否在nums数组中,并且可以快速的获取所在下表index。 +我们的第一反应就是将数组维护成一个Map结构: + +- key: 存储数组里的值 +- value: 存储数组的下标index + +这样我们只需要通过target - nums[i]的值去Map中查找即可。 +但是这样存在一个问题,就是你需要先把数组中的值都放到Map中,需要多一步循环。 + + + + +--- + +其实我们可以先创建一个Map,在Map的初始化过程中什么元素都不放。 + +对于每一个 x,我们首先查询哈希表中是否存在 target - x,如果已存在就返回,如果不存在那再将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。 + +这样只需要一次循环就可以了,而且Map数组不用提前初始化,在性能和内存占用率都比较低。 + + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map hashtable = new HashMap(); + for (int i = 0; i < nums.length; ++i) { + if (hashtable.containsKey(target - nums[i])) { + return new int[]{hashtable.get(target - nums[i]), i}; + } + hashtable.put(nums[i], i); + } + return new int[0]; + } +} + +``` + +```c++ +#include +#include + +using namespace std; + +class Solution { +public: + vector twoSum(vector &nums, int target) { + unordered_map map; + for (int i = 0; i < nums.size(); i ++) { + auto it = map.find(target - nums[i]); + if (it != map.end()) { + return {i, it->second}; + } + + map[nums[i]] = i; + } + return {}; + } +}; +``` + + +复杂度分析: + +- 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。 + +- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" new file mode 100644 index 00000000..789a2a9f --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -0,0 +1,210 @@ +2. LeetCode_两数相加 +=== + +给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 + +请你将两个数相加,并以相同形式返回一个表示和的链表。 + +你可以假设除了数字 0 之外,这两个数都不会以 0 开头。 + + +输入:l1 = [2,4,3], l2 = [5,6,4] +输出:[7,0,8] +解释:342 + 465 = 807. + + + +输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9] +输出:[8,9,9,9,0,0,0,1] + + +提示: + +- 每个链表中的节点数在范围 [1, 100] 内 +- 0 <= Node.val <= 9 +- 题目数据保证列表表示的数字不含前导零 + + + +想法: + +我的想法是先把两个链表转成两个整数相加,然后把这个整数转成字符串再次生成链表。 + +这种方案是不可行的,因为链表的长度可能会很长,整数是操作不了的,例如: + + +方法: + +由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。 + +我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry; +其中,答案链表处相应位置的数字为 (n1+n2+carry)mod10,而新的进位值为 [(n1+n2+carry) / 10] + +如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。 + +此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。 + + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum.png?raw=true) + + +- 将链表反过来看,头结点在右侧 +- 横线上的数字为进位 +- 2 + 5 + 0(第一个进位默认为0) = 7 + + - 7 % 10得到新节点中的元素为7 + - 7 / 10得到下一个进位为0 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum_2.png?raw=true) + + +```java +class ListNode { + int val; + ListNode next; + + ListNode() { + this.val = 0; + this.next = null; + } + + ListNode(int val) { + this.val = val; + this.next = null; + } + + ListNode(int val, ListNode next) { + this.val = val; + this.next = next; + } +} + +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + ListNode head = null; + ListNode tail = null; + + int carry = 0; + + while(l1 != null || l2 != null) { + int n1 = l1 != null ? l1.val : 0; + int n2 = l2 != null ? l2.val : 0; + + int sum = n1 + n2 + carry; + + if (head == null) { + head = tail = new ListNode(sum % 10); + } else { + tail.next = new ListNode(sum % 10); + tail = tail.next; + } + + carry = sum / 10; + + if (l1 != null) { + l1 = l1.next; + } + + if (l2 != null) { + l2 = l2.next; + } + } + + if (carry > 0) { + tail.next = new ListNode(carry); + } + return head; + } +} +``` + + + +复杂度分析: + +- 时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。 + +- 空间复杂度:O(1)。注意返回值不计入空间复杂度。 + + +### 改进: 递归 + +```java +class Solution { + public ListNode addTwoNumbers(ListNode l1, ListNode l2) { + return add(l1, l2, 0); + } +} + + public ListNode add(ListNode l1, ListNode l2, int carry) { + if (l1 == null && l2 == null && carry == 0) { + return null; + } + int val = carry; + if (l1 != null) { + val += l1.val; + l1 = l1.next; + } + if (l2 != null) { + val += l2.val; + l2 = l2.next; + } + ListNode node = new ListNode(val % 10); + node.next = add(l1, l2, val / 10); + return node; + } +} +``` + +使用C++实现: + +```c++ +struct ListNode { + int val; + ListNode *next; + + ListNode() : val(0), next(nullptr) { + } + + ListNode(int x) : val(x), next(nullptr) { + } + + ListNode(int x, ListNode *next) : val(x), next(next) { + } +}; + +class Solution { +public: + ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) { + int carry = 0; + + ListNode *answer = new ListNode(); + ListNode *head = answer; + + while (l1 || l2) { + int number = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + carry; + carry = number / 10; + number %= 10; + head->next = new ListNode(number); + head = head->next; + + l1 ? l1 = l1->next : 0; + l2 ? l2 = l2->next : 0; + } + if (carry) { + head->next = new ListNode(carry); + } + return answer->next; + } +}; +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" new file mode 100644 index 00000000..748e420e --- /dev/null +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/3. LeetCode_\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" @@ -0,0 +1,190 @@ +3. LeetCode_无重复字符的最长子串 +=== + +给定一个字符串s,请你找出其中不含有重复字符的 最长子串 的长度。 + + +示例 1: + +输入: s = "abcabcbb" +输出: 3 +解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 +示例 2: + +输入: s = "bbbbb" +输出: 1 +解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 +示例 3: + +输入: s = "pwwkew" +输出: 3 +解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + + +提示: + +0 <= s.length <= 5 * 104 +s 由英文字母、数字、符号和空格组成 + + + +方式一: + +暴力破解,通过多个循环以穷举的方式 找出答案。简单且易想到的方案,包括之前我们刷的两道算法题也可以使用暴力破解来解决,缺点就是虽然简单,但是效率非常低,并非是最优方案。 + + +具体思路: + +- 将字符串中每个字符作为一个循环,和子串的组合进行比较,从而获取最长字串长度。 + +如字符串abc,则第一轮比较为: 字符a和子串a、ab、abc比较,第二轮则为字符b和子串b、bc比较,以此类推,最后获取不重复的子串长度。 + +```java +public class Solution { + public static int lengthOfLongestSubstring(String s) { + int maxLength = 0; + + for (int i = 0; i < s.length(); i++) { + for (int j = i + 1; j < s.length(); j++) { + if (!judgeCharacterExist(i, j, s)) { + maxLength = Math.max(maxLength, j - i); + } + } + } + return maxLength; + } + + + public static boolean judgeCharacterExist(int start, int end, String param) { + HashSet hashSet = new HashSet<>(); + for (int i = start; i < end; i++) { + if (hashSet.contains(param.charAt(i))) { + return true; + } + hashSet.add(param.charAt(i)); + } + + return false; + } +} +``` + +该方法效率低,时间福再度为O(n³)。 + + +方法二: 滑动窗口法 + +滑动窗口: 是数组和字符串中一个抽象的概念,可分为滑动和窗口两个概念理解。 + +窗口:即表示一个范围,通常是字符串和数组从开始到结束两个索引范围中间包含的一系列元素集合。如字符串abcd,如果开始索引和结束索引分别为0、2的话,这个窗口包含的字符则为: abc。 + +滑动:它表示窗口的开始和结束索引是可以往某个方向移动的。 +如上面的例子开始索引和结束索引分别为0、2的话,当开始索引和结束索引都往右移动一位时,它们的索引值分别为1、3,这个窗口包含的字符为:bcd。 + +示例: + +- abcdef +- 字符串开始索引: 0 +- 字符串结束索引: 5 +- 开始和结束索引范围包含的字符(abcdef)就可以看作是一个窗口 + +思路: + +- 使用一个HashSet来实现滑动窗口,用来检查重复字符 +- 维护开始和结束两个索引,默认都是从0开始 +- 随着循环 向右移动结束索引 + + - 遇到不是重复的字符则放入窗口里 + - 遇到重复字符则向右移动开始索引 + +最终得到结果。 + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int maxLength = 0; + int leftPoint = 0; + int rightPoint = 0; + + Set set = new HashSet<>(); + + while(leftPoint < s.length() && rightPoint < s.length()) { + if (!set.contains(s.charAt(rightPoint))) { + set.add(s.charAt(rightPoint++)); + maxLength = Math.max(maxLength, rightPoint - leftPoint); + } else { + set.remove(s.charAt(leftPoint++)); + } + } + return maxLength; + } +} +``` + +滑动窗口算法的时间复杂度为O(n),相比于暴力破解,它的效率大大提高了。 + +方式三: + +虽然方式使用了滑动窗口时间复杂度只有O(n),但是如果存在重复字符还需要移动开始索引,因此我们可以考虑借助之前其他算法谈到的“空间换时间”的想法,通过借助HashMap建立字符和索引映射,避免手动移动索引。 + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + char[] charArry = s.toCharArray(); + + HashMap keyMap = new HashMap<>(); + int maxLength = 0; + + int leftPoint = 0; + for (int i = 0; i < charArry.length; i++) { + if (keyMap.containsKey(charArry[i])) { + // 存在重复数据则获取索引值最大的 + leftPoint = Math.max(leftPoint, keyMap.get(charArry[i])); + } + // 值重复就覆盖,不重复就添加 + keyMap.put(charArry[i], i + 1); + maxLength = Math.max(maxLength, i + 1 - leftPoint); + } + return maxLength; + } +} +``` + +时间复杂度为O(n),虽然和上一种方案的时间复杂度是一样的,但是效率还是有一定的提高(思考问题时要思考是否还有其他有优质的方案,培养发散思维)。 + + +方式四: + + +上面题目分析的时候就提到了要注意题目中提到的:【字符串英文字母、数字、符号和空格组成】,这些字符是可以使用ASCII表示(如字符a的ASCII值为97,想具体了解的可以百度下),那么我们就可以建立字符与ASCII的映射关系,从而实现重复字符的排除。 + + + +```java +public class Solution { + public static Integer lengthOfLongestSubstring(String s) { + int[] index = new int[128]; + int maxLength = 0; + int length = s.length(); + int i = 0; + for (int j = 0; j < length; j++) { + i = Math.max(index[s.charAt(j)], i); + index[s.charAt(j)] = j + 1; + maxLength = Math.max(maxLength, j + 1 - i); + } + return maxLength; + } +} +``` + + + + + +--- +- 邮箱 :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" index 166994ce..a3f2b9c9 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,6 +1,17 @@ 2.Kotlin_高阶函数&Lambda&内联函数 === +## 函数还是属性 + +在某些情况下,不带参数的函数可与只读属性互换。 虽然语义相似,但是在某种程度上有一些风格上的约定。 + +底层算法优先使用属性而不是函数: + +- 不会抛异常 +- 计算开销小(或者在首次运行时缓存) +- 如果对象状态没有改变,那么多次调用都会返回相同结果 + + ## 高阶函数 From da95ba3cb9deaea08ca7daf28f86a029604fd1aa Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 14 Sep 2024 17:38:48 +0800 Subject: [PATCH 107/128] update --- ...60\346\215\256\347\273\223\346\236\204.md" | 31 +++ .../\347\256\227\346\263\225.md" | 36 +++- Jetpack/ui/Jetpack Compose_Modifier.md | 180 ++++++++++++++++++ ...etpack Compose\347\256\200\344\273\213.md" | 104 +++++++++- MobileAIModel/TensorFlow Lite | 33 ++++ 5 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 Jetpack/ui/Jetpack Compose_Modifier.md create mode 100644 MobileAIModel/TensorFlow Lite diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" index f8e8413f..1a75ec60 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -10,8 +10,24 @@ 按照视点的不同,我们把数据结构分为逻辑结构和物理结构。 +数据结构(data structure)是组织和存储数据的方式,涵盖数据内容、数据之间关系和数据操作方法,它具有以下设计目标。 + +- 空间占用尽量少,以节省计算机内存。 +- 数据操作尽可能快速,涵盖数据访问、添加、删除、更新等。 +- 提供简洁的数据表示和逻辑信息,以便算法高效运行。 +- 数据结构设计是一个充满权衡的过程。如果想在某方面取得提升,往往需要在另一方面作出妥协。 + +下面举两个例子: + +- 链表相较于数组,在数据添加和删除操作上更加便捷,但牺牲了数据访问速度。 +- 图相较于链表,提供了更丰富的逻辑信息,但需要占用更大的内存空间。 + + + #### 逻辑结构 + 逻辑结构:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种: + - 集合结构 集合结构:集合结构中的数据元素除了同属于一个集合外,它们之间没有其他关系。各个数据元素是“平等”的,它们的共同属性是"同属于一个集合"。 数据结构中的集合关系就类似于数学中的集合 @@ -23,7 +39,9 @@ 图形结构:图形结构的数据元素是多对多的关系 #### 物理结构 + 物理结构(也称为存储结构):是指数据的逻辑结构在计算机中的存储形式。 + - 顺序存储结构 顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的 这种存储结构其实很简单,说白了,就是排队占位。大家都按顺序排好,每个人占一小段空间,大家谁也别插谁的队。数组就是这 @@ -168,8 +186,21 @@ int[] arr = new int[10]; #### 栈的应用-递归 把一个直接调用自己或通过一系列的调用语句间接地调用自己的函数,称做递归函数。 + 当然,写递归程序最怕的就是陷入永不结束的无穷递归中,所以,每个递归定义必须至少有一个条件,满足时递归不再进行,即不再引用自身而是返回值退出。 +递归(recursion)是一种算法策略,通过函数调用自身来解决问题。它主要包含两个阶段: + +- 递:程序不断深入地调用自身,通常传入更小或更简化的参数,直到达到“终止条件”。 +- 归:触发“终止条件”后,程序从最深层的递归函数开始逐层返回,汇聚每一层的结果。 + +而从实现的角度看,递归代码主要包含三个要素: + +- 终止条件:用于决定什么时候由“递”转“归”。 +- 递归调用:对应“递”,函数调用自身,通常输入更小或更简化的参数。 +- 返回结果:对应“归”,将当前递归层级的结果返回至上一层。 + + 那么我们讲了这么多递归的内容,和栈有什么关系呢?这得从计算机系统的内部说起。前面我们已经看到递归是如何执行它的前行和退回阶段的。 递归过程退回的顺序是它前行顺序的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行过程中存储起来的某些数据。这种存储某些数据, 并在后面又以存储的逆序恢复这些数据,以提供之后使用的需求,显然很符合栈这样的数据结构,因此,编译器使用栈实现递归就没什么好惊讶的了。 diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" index 3347d083..b33352bd 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\347\256\227\346\263\225.md" @@ -1,7 +1,12 @@ 算法 === -算法(Algorithm)是解决特定问题求解步骤的描述,在计算机中表现为指令的有限序列,并且每条指令表示一个或多个操作。 + +算法(algorithm)是在有限时间内解决特定问题的一组指令或操作步骤,它具有以下特性: + +- 问题是明确的,包含清晰的输入和输出定义。 +- 具有可行性,能够在有限步骤、时间和内存空间下完成。 +- 各步骤都有确定的含义,在相同的输入和运行条件下,输出始终相同。 算法具有五个基本特性: @@ -17,6 +22,21 @@ 算法的每一步都必须是可行的,也就是说,每一步都能够通过执行有限次数完成 +在算法设计中,我们先后追求以下两个层面的目标。 + +- 找到问题解法:算法需要在规定的输入范围内可靠地求得问题的正确解。 +- 寻求最优解法:同一个问题可能存在多种解法,我们希望找到尽可能高效的算法。 + +也就是说,在能够解决问题的前提下,算法效率已成为衡量算法优劣的主要评价指标,它包括以下两个维度: + +- 时间效率:算法运行时间的长短。 +- 空间效率:算法占用内存空间的大小。 + +简而言之,我们的目标是设计“既快又省”的数据结构与算法。而有效地评估算法效率至关重要,因为只有这样,我们才能将各种算法进行对比,进而指导算法设计与优化过程。 + + + + ### 算法时间复杂度 @@ -291,7 +311,19 @@ public static int[] merge(int[] a, int[] b) { } return result; } -```` +``` + + +数据结构与算法高度相关、紧密结合,具体表现在以下三个方面: + +- 数据结构是算法的基石。数据结构为算法提供了结构化存储的数据,以及操作数据的方法。 +- 算法是数据结构发挥作用的舞台。数据结构本身仅存储数据信息,结合算法才能解决特定问题。 +- 算法通常可以基于不同的数据结构实现,但执行效率可能相差很大,选择合适的数据结构是关键。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/algorithm_datasturcture.jpg?raw=true) + --- diff --git a/Jetpack/ui/Jetpack Compose_Modifier.md b/Jetpack/ui/Jetpack Compose_Modifier.md new file mode 100644 index 00000000..fefee653 --- /dev/null +++ b/Jetpack/ui/Jetpack Compose_Modifier.md @@ -0,0 +1,180 @@ +# Jetpack Compose_Modifier + +在传统开发中,使用XML文件来描述组件的样式,而Jetpack Compose设计了一个精妙的东西,它叫作Modifier。Modifier允许我们通过链式调用的写法来为组件应用一系列的样式设置,如边距、字体、位移等。在Compose中,每个基础的Composable组件都有一个modifier参数,通过传入自定义的Modifier来修改组件的样式。 + + + +- Modifier.size + +设置被修饰组件的大小。 + +- Modifier.background + +添加背景色,支持使用color设置纯色背景,也可以使用brush设置渐变色背景。 + +传统视图中View的background属性可以用来设置图片格式的背景,Compose的background修饰符只能设置颜色背景,图片背景需要使用Box布局配合Image组件实现。 + +- Modifier.fillMaxSize + +组件在高度或宽度上填满父空间,此时可以使用fillMaxXXX系列方法: + +Box(Modifier.fillMaxSize().background(Color.Red)) +Box(Modifier.fillMaxWidth().height(60.dp)) +Box(Modifier.fillMaxHeight().width(60.dp)) + + +```kotlin +setContent { + Image( + painter = painterResource(id = R.drawable.ic_launcher_background), + contentDescription = null, + modifier = Modifier + .size(width = 60.dp, height = 50.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Red, + Color.Yellow, + Color.White + ) + ) + ) + ) +} +``` + + +- Modifier.border & Modifier.padding + +border用来为组件添加边框。 +边框可以指定颜色、粗细以及通过Shape指定形状,比如圆角矩形等。 + +padding用来为被修饰组件增加间隙,可以在border前后各插入一个padding用来为为组件增加对外和对内的间距,例如: + +```kotlin +@Preview +@Composable +fun BoxTest() { + Box( + modifier = Modifier + .padding(10.dp) + .border(2.dp, Color.Red, shape = RoundedCornerShape(2.dp)) + .padding(20.dp) + ) { + Spacer( + modifier = Modifier + .size(10.dp, 10.dp) + .background(Color.Green) + ) + } +} +``` + +相对于传统布局有Margin和Padding之分,Compose中只有padding这一种修饰符,根据在调用链中的位置不同发挥不同的作用。 + +Modifier调用顺序会影响最终UI的呈现效果。 +这是因为Modifier会由于调用顺序不同而产生不同的Modifier链,Compose会按照Modifier链来顺序完成页面测量布局和渲染。 + + +## 作用域限定 + +Compose充分发挥了Kotlin的语法特性,让某些Modifier修饰符只能在特定作用域中使用,有利于类型安全的调用它们。 + +所谓的作用域,在Kotlin中就是一个带有Receiver的代码块。 + +例如Box组件参数中的content就是一个Receiver类型为BoxScope的代码块,因此其子组件都处于BoxScope作用域中。 + +```kotlin +inline fun Box( + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, + propagateMinConstraints: Boolean = false, + content: @Composable BoxScope.() -> Unit +) + +Box { + // 该代码块Receiver类型即为BoxScope +} +``` + + +- matchParentSize + +matchParentSize是只能在BoxScope中使用的作用域限定修饰符。 +当使用matchParentSize设置尺寸时,可以保证当前组件的尺寸与父组件相同。 +而父组件默认的是wrapContent,会根据子组件的尺寸确定自身的尺寸。 + +而如果使用fillMaxSize取代matchParentSize,那么该组件的尺寸会被设置为父组件所允许的最大尺寸,这样会导致铺满整个屏幕。 +```kotlin +@Composable +// 只把UserInfo区域设置背景 +fun MatchParentModifierDemo() { + Box { + Box(modifier = Modifier + .matchParentSize() + .background(Color.RED) + ) + UserInfo() + } +} + + +// 使用fillMaxSize取代matchParentSize,那么该组件的尺寸会被设置为父组件所允许的最大尺寸,这样会导致整个背景铺满整个屏幕 +@Composable +fun MatchParentModifierDemo() { + Box { + Box(modifier = Modifier + .fillMaxSize() + .background(Color.READ) + ) + UserInfo() + } +} +``` + + +- weight + +在RowScope和ColumnScope中,可以使用专属的weight修饰符来设置尺寸。 + +与size修饰符不同的是,weight修饰符允许组件通过百分比设置尺寸,也就是允许组件可以自适应适配各种屏幕尺寸。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git "a/Jetpack/ui/Jetpack Compose\347\256\200\344\273\213.md" "b/Jetpack/ui/Jetpack Compose\347\256\200\344\273\213.md" index 150ccb9e..ada5182a 100644 --- "a/Jetpack/ui/Jetpack Compose\347\256\200\344\273\213.md" +++ "b/Jetpack/ui/Jetpack Compose\347\256\200\344\273\213.md" @@ -15,8 +15,104 @@ GogleAndroid团队的Anna-Chiara表示,他们对已经实现的一些API感到 这就是为什么Jetpack Compose 让我们看到了曙光。 +### 精简代码 -作者:依然范特稀西 -链接:https://www.jianshu.com/p/19c4e10370ba -来源:简书 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 \ No newline at end of file +编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现bug的可能性也更小。 + +与使用Android View系统(按钮、列表或动画)相比,Compose可让您使用更少的代码实现更多的功能。 +而且编写代码只需要采用Kotlin,而不必拆分成Kotlin和XML部分。 + +### 直观易读 + +Compose使用声明性API,这意味着您只需描述界面,Compose会负责完成其余工作。 + + +### 加速开发 + +Compose与您所有的现有代码兼容:您可以从View调用Compose代码,也可以从Compose调用View。 + + +### 旧项目支持Compose + +以Android平台而言,Compose实际上都承载在ComposeView上,如果想要在旧项目中使用Compose开发,就需要在使用处添加一个ComposeView。 + +可以在XML中静态声明或在程序中动态构造出一个ComposeView实例,例如在activity_main.xml中添加ComposeView,如下所示: + +```xml + + + + +``` + +```kotlin +class MainActivity2 : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main2) + findViewById(R.id.root).setContent { + Text("Hello World") + } + } +} +``` + + +### 简单的可组合函数 + +使用Compose,您可以通过定义一组接受数据而发出界面元素的可组合函数来构建界面。 +下面是一个简单的示例,接受一个字符串并现实一个问号信息: + +```kotlin +@Composable +fun Greeting(name: String) { + Text("Hello $name) +} +``` + +- 此函数带有@Composable注释。所有可组合函数都必须带有此注释。此注释可告知Compose编译器:此函数旨在将数据转换为界面。 +- 此函数可以在界面中显示文本。为此,它会调用Text()可组合函数,该函数实际上会创建文本界面元素。可组合函数通过调用其他可组合函数来发出界面层次结构。 + + + +### 在Composet中使用View组件 + +不少功能性的传统视图控件在Compose中没有对应的Composable实现,例如SurfaceView、 +WebView、MapView等。 + +因此在Compose中可能会有使用传统View控件的需求。 +Compose提供了名为AndroidView的Composable组件,允许在Composable中插入任意基于继承自View的传统试图控件。 + +```kotlin +class MainActivity2 : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + AndroidView(factory = {context -> + WebView(context).apply { + settings.javaScriptEnabled = true + webViewClient = WebViewClient() + loadUrl("https://www.baidu.com") + } + }, modifier = Modifier.fillMaxSize()) + } + } +} +``` +AndroidView中传入一个工厂的实现,工厂方法中可以获取Composable所在的Context,并基于其构建View视图控件。 + +### Modifier + +Modifier是一个装饰或者添加行为的有序的,不变的集合。 +例如background、padding、宽高、焦点点击事件等。 +例如给Text设置单行、给Button设置个中点击状态等行为。 +其实就是所有控件的通用属性都在Modifier中。 + + +在Compose中,每个组件都是一个带有@Composable注解的函数,被称为Composable。 +Compose中已经预置了很多基础的Composable组件,他们都是基于Material Design规范设计,例如Button、TextField、TopAppBar等。 + +在布局方面,Compose提供了Column、Row、Box三种布局组件,类似于传统视图开发中的LinearLayout(Vertical)、LinearLayout(Horizontal)、RelativeLayout。 diff --git a/MobileAIModel/TensorFlow Lite b/MobileAIModel/TensorFlow Lite new file mode 100644 index 00000000..dc7054e3 --- /dev/null +++ b/MobileAIModel/TensorFlow Lite @@ -0,0 +1,33 @@ +TensorFlow Lite +--- + +[TensorFlow Lite](https://tensorflow.google.cn/lite?hl=zh-cn) 是一个移动端库,可用于在移动设备、微控制器和其他边缘设备上部署模型。 + + +[TensorFlow Lite](https://tensorflow.google.cn/lite?hl=zh-cn) 是一组工具,可帮助开发者在移动设备、嵌入式设备和 loT 设备上运行模型,以便实现设备端机器学习。 + +主要特性: + +- 通过解决以下 5 项约束条件,针对设备端机器学习进行了优化:延时(数据无需往返服务器)、隐私(没有任何个人数据离开设备)、连接性(无需连接互联网)、大小(缩减了模型和二进制文件的大小)和功耗(高效推断,且无需网络连接)。 +- 支持多种平台,涵盖 Android 和 iOS 设备、嵌入式 Linux 和微控制器。 +- 支持多种语言,包括 Java、Swift、Objective-C、C++ 和 Python。 +高性能,支持硬件加速和模型优化。 +- 提供多种平台上的常见机器学习任务的端到端示例,例如图像分类、对象检测、姿势估计、问题回答、文本分类等。 + + +1. 创建 TensorFlow Lite 模型 +TensorFlow Lite 模型以名为 FlatBuffer 的专用高效可移植格式(由“.tflite”文件扩展名标识)表示。与 TensorFlow 的协议缓冲区模型格式相比,这种格式具有多种优势,例如可缩减大小(代码占用的空间较小)以及提高推断速度(可直接访问数据,无需执行额外的解析/解压缩步骤),这样一来,TensorFlow Lite 即可在计算和内存资源有限的设备上高效地运行。 + +TensorFlow Lite 模型可以选择包含元数据,并在元数据中添加人类可读的模型说明和机器可读的数据,以便在设备推断过程中自动生成处理前和处理后流水线。如需了解详情,请参阅添加元数据。 + +您可以通过以下方式生成 TensorFlow Lite 模型: + +使用现有的 TensorFlow Lite 模型:若要选择现有模型,请参阅 TensorFlow Lite 示例。模型可能包含元数据,也可能不含元数据。 + +创建 TensorFlow Lite 模型:使用 TensorFlow Lite Model Maker,利用您自己的自定义数据集创建模型。默认情况下,所有模型都包含元数据。 + +将 TensorFlow 模型转换为 TensorFlow Lite 模型:使用 TensorFlow Lite Converter 将 TensorFlow 模型转换为 TensorFlow Lite 模型。在转换过程中,您可以应用量化等优化措施,以缩减模型大小和缩短延时,并最大限度降低或完全避免准确率损失。默认情况下,所有模型都不含元数据。 + + + + From e6bd892680d9e26a2784190076bb90cb8d650ae2 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 27 Sep 2024 17:00:54 +0800 Subject: [PATCH 108/128] update --- .../Parcelable\345\217\212Serializable.md" | 63 ++++++++++- ...\347\256\200\344\273\213(\344\270\213).md" | 27 +++++ ...etpack Compose\347\273\204\344\273\266.md" | 106 ++++++++++++++++++ ...7&\345\205\263\351\224\256\345\255\227.md" | 36 ++++++ ...2\344\270\276&\345\247\224\346\211\230.md" | 21 ++++ MobileAIModel/TensorFlow Lite | 7 +- 6 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 "Jetpack/ui/Jetpack Compose\347\273\204\344\273\266.md" diff --git "a/BasicKnowledge/Parcelable\345\217\212Serializable.md" "b/BasicKnowledge/Parcelable\345\217\212Serializable.md" index 73a927a1..de220416 100644 --- "a/BasicKnowledge/Parcelable\345\217\212Serializable.md" +++ "b/BasicKnowledge/Parcelable\345\217\212Serializable.md" @@ -1,6 +1,15 @@ Parcelable及Serializable === + + +### Serializable + +在Java中Serializable接口是一个允许将对象转换为字节流(序列化)然后重新构造回对象(反序列化)的标记接口。 + +它会使用反射,并且会创建许多临时对象,导致内存使用率升高,并可能产生性能问题。 + + `Serializable`的作用是为了保存对象的属性到本地文件、数据库、网络流、`rmi`以方便数据传输, 当然这种传输可以是程序内的也可以是两个程序间的。而`Parcelable`的设计初衷是因为`Serializable`效率过慢, 为了在程序内不同组件间以及不同`Android`程序间(`AIDL`)高效的传输数据而设计,这些数据仅在内存中存在,`Parcelable`是通过`IBinder`通信的消息的载体。 @@ -11,6 +20,58 @@ Parcelable及Serializable Parcelable不同于将对象进行序列化,Parcelable方式的实现原理是将一个完整的对象进行分解,而分解后的每一部分都是Intent所支持的数据类型,这样也就实现传递对象的功能了。 + +### Parcelable实现 + +```kotlin +data class Developer(val name: String, val age: Int) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readInt() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(name) + parcel.writeInt(age) + } + + // code removed for brevity + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): Developer { + return Developer(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + +``` +上面是实现Parcelable的代码,可以看到有很多重复的代码。 +为了避免写这些重复的代码,可以使用kotlin-parcelize插件,并在类上使用@Parcelize注解。 + +```kotlin +@Parcelize +data class Developer(val name: String, val age: Int) : Parcelable +``` + +当在一个类上声明`@Parcelize`注解后,就会自动生成对应的代码。 + + + + + +Parcelable不会使用反射,并且在序列化过程中会产生更少的临时对象,这样就会减少垃圾回收的压力: + +- Parcelable不会使用反射 +- Parcelable是Android平台特定的接口 + +所以Parcelable比Serializable更快。 + + 区别: - Parcelable is faster than serializable interface - Parcelable interface takes more time for implemetation compared to serializable interface @@ -20,4 +81,4 @@ Parcelable不同于将对象进行序列化,Parcelable方式的实现原理是 ---- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" "b/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" index 5456759f..7c246365 100644 --- "a/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" +++ "b/ImageLoaderLibrary/Glide\347\256\200\344\273\213(\344\270\213).md" @@ -420,6 +420,33 @@ builder.setDiskCache( - `Disk cache needs to implement: DiskCache` + + +Let's take Glide as an example. To optimize memory usage and use less memory, Glide does downsampling. + +Downsampling means scaling the bitmap(image) to a smaller size which is actually required by the view. + +Assume that we have an image of size 2000*2000, but the view size is 400*400. So why load an image of 2000*2000, Glide down-samples the bitmap to 400*400, and then show it into the view. + +We use Glide like this: +``` +Glide.with(fragment) + .load(url) + .into(imageView); +``` +As we are passing the imageView as a parameter to the Glide, it knows the dimension of the imageView. + +Glide down-samples the image without loading the whole image into the memory. + +This way, the bitmap takes less memory, and the out-of-memory error is solved. Similarly, other Image Loading libraries like Fresco also do it. + +This was all about how the Android Image Loading library optimizes memory usage. + + + + + + 参考 === diff --git "a/Jetpack/ui/Jetpack Compose\347\273\204\344\273\266.md" "b/Jetpack/ui/Jetpack Compose\347\273\204\344\273\266.md" new file mode 100644 index 00000000..3ffcb46e --- /dev/null +++ "b/Jetpack/ui/Jetpack Compose\347\273\204\344\273\266.md" @@ -0,0 +1,106 @@ +# Jetpack Compose组件 + +### Text组件 + +```kotlin +Text(text = "Hello World") +Text( + text = stringResource(id = R.string.next), + style = TextStyle( + fontSize = 25.sp, + fontWeight = FontWeight.Bold, + lineHeight = 35.sp, + color = Color.Red, + textDecoration = TextDecoration.LineThrough, + fontStyle = FontStyle.Italic + ) +) +``` + +#### AnnotatedString多样式文字 + +在很多应用场景中,我们需要在一段文字中对局部内容应用特别格式以示突出,比如一个超链接或者一个电话号码等,此时需要用到AnnotatedString。AnnotatedString是一个数据类,除了文本值,它还包含了一个SpanStyle和ParagraphStyle的Range列表。SpanStyle用于描述在文本中子串的文字样式,ParagraphStyle则用于描述文本中子串的段落样式,Range确定子串的范围。 + +```kotlin +val annotedText = buildAnnotatedString { + withStyle(style = SpanStyle(fontSize = 23.sp)) { + pushStringAnnotation(tag = "url", annotation = "https://www.baidu.com") + append("haha") + } + + withStyle(SpanStyle(fontSize = 30.sp)) { + append("A") + } +} + +ClickableText( + text = annotedText, + onClick = { offset -> + annotedText.getStringAnnotations(tag = "url", start = offset, end = offset) + .firstOrNull()?.let { + Log.e("@@@", it.item) + } + } +) +``` + +Compose提供了一种可点击文本组件ClickedText,可以响应我们对文字的点击,并返回点击位置。可以让AnnotatdString子串在相应的ClickedText中点击后,做出不同的动作。 + + + + + +### SelectionContainer + +选中文字Text自身默认是不能被长按选择的,否则在Button中使用时,又会出现那种“可粘贴的Button”的例子。 +Compose提供了专门的SelectionContainer组件,对包裹的Text进行选中。可见Compose在组件设计上,将关注点分离的原则发挥到了极致。 + + +```kotlin +SelectionContainer { + Text("hahah") +} +``` + + + +### TextField输入框 + + +TextField有两种风格,一种是默认的,也就是filled,另一种是OutlinedTextField。 +```kotlin +var text by remember { mutableStateOf("") } + +TextField(value = text, onValueChange = { + text = it +}, label = { Text("请输入用户名") }) +``` + +这个text是一个可以变化的文本,用来显示TextField输入框中当前输入的文本内容。 在onValueChange回调中可以获取来自软键盘的最新输入,我们利用这个信息来更新可变状态text,驱动界面刷新显示最新的输入文本。 + + +***来自软键盘的输入内容不会直接更新TextField, TextField需要通过观察额外的状态更新自身,这也体现了声明式UI中“状态驱动UI”的基本理念。*** + + + + + +### Column组件 + +很多产品中都有展示一组数据的需求场景,如果数据数量是可以枚举的,则仅需通过Column组件来枚举列出。 + + +然而很多时候,列表中的项目会非常多,例如通讯录、短信、音乐列表等,我们需要滑动列表来查看所有的内容,可以通过Column的Modifier添加verticalScroll()方法来让列表实现滑动。 + + +#### LazyComposables + +给Column的Modifier添加verticalScroll()方法可以让列表实现滑动。 + +但是如果列表过长,众多的内容会占用大量的内存。然而更多的内容对于用户其实都是不可见的,没必要记载到内存。 + +所以Compose提供了专门用于处理长列表的组件,这些组件指挥在我们能看到的列表部分进行重组和布局,它们分别是LazyColumn和LazyRow。其作用类似于传统视图中的ListView或者RecyclerView。 + + + + 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 f922d625..159e419a 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,42 @@ for(num in nums) { +### infix关键字 + +Kotlin中infix的中缀表示法允许在不使用点和括号的情况下调用函数。 + + +例如: + +```kotlin +infix fun Int.add(value: Int): Int = this + value + +val sum = 5 add 10 +``` + +平时在使用map的时候经常会这样用: + +```kotlin +val map = mapOf( + 1 to "A", + 2 to "B", + 3 to "C" +) +``` +而mapOf需要一个Pair的列表。这里可以使用to关键字来实现就是因为infix。 + +我们可以检查一下Kotlin中的Pair的源码,就会发现: +```kotlin +infix fun A.to(that: B): Pair = Pair(this, that) +``` + +使用infix的时候有个点需要注意: + +- 它必须是一个函数 +- 必须只有一个参数 +- 参数不能是可变参数 +- 参数不能有默认值 + - [上一篇: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) 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 d738c4b2..7f4030b1 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" @@ -411,6 +411,27 @@ object Resource { } ``` + +Kotlin中除了object之外还有一个companion object,也可以替代Java中的static关键字,那他俩有什么区别呢? + + +#### Companion object + +- 必须在一个类中定义 +- companion object会在类第一次加载的时候就创建好,这就意味着我们可能还没使用的时候就初始化好了 +- 它等同于Java中的static +- 如果我们不手动指定名字的话会有一个默认的Companion的名字 +- 在调用方法或变量的时候可以省略名字 + +#### object + +- 可以被定义到任何地方 +- 在我们第一次使用它时才会创建。 +- 主要被用作提供单例行为 +- 必须提供一个命名 +- 如果在一个类中定义object,那在调用方法或者变量的时候不能省略名字 + + ### 对象表达式 对象也能用于创建匿名类实现。 diff --git a/MobileAIModel/TensorFlow Lite b/MobileAIModel/TensorFlow Lite index dc7054e3..3e3be658 100644 --- a/MobileAIModel/TensorFlow Lite +++ b/MobileAIModel/TensorFlow Lite @@ -6,6 +6,9 @@ TensorFlow Lite [TensorFlow Lite](https://tensorflow.google.cn/lite?hl=zh-cn) 是一组工具,可帮助开发者在移动设备、嵌入式设备和 loT 设备上运行模型,以便实现设备端机器学习。 + +TensorFlow Lite是TensorFlow在手机和嵌入设备上的一个轻量级框架。 + 主要特性: - 通过解决以下 5 项约束条件,针对设备端机器学习进行了优化:延时(数据无需往返服务器)、隐私(没有任何个人数据离开设备)、连接性(无需连接互联网)、大小(缩减了模型和二进制文件的大小)和功耗(高效推断,且无需网络连接)。 @@ -16,7 +19,9 @@ TensorFlow Lite 1. 创建 TensorFlow Lite 模型 -TensorFlow Lite 模型以名为 FlatBuffer 的专用高效可移植格式(由“.tflite”文件扩展名标识)表示。与 TensorFlow 的协议缓冲区模型格式相比,这种格式具有多种优势,例如可缩减大小(代码占用的空间较小)以及提高推断速度(可直接访问数据,无需执行额外的解析/解压缩步骤),这样一来,TensorFlow Lite 即可在计算和内存资源有限的设备上高效地运行。 +TensorFlow Lite 模型以名为 FlatBuffer 的专用高效可移植格式(由“.tflite”文件扩展名标识)表示。 + +与 TensorFlow 的协议缓冲区模型格式相比,这种格式具有多种优势,例如可缩减大小(代码占用的空间较小)以及提高推断速度(可直接访问数据,无需执行额外的解析/解压缩步骤),这样一来,TensorFlow Lite 即可在计算和内存资源有限的设备上高效地运行。 TensorFlow Lite 模型可以选择包含元数据,并在元数据中添加人类可读的模型说明和机器可读的数据,以便在设备推断过程中自动生成处理前和处理后流水线。如需了解详情,请参阅添加元数据。 From 071c6288b4b424df948f06f29f2c17c8388c4e02 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Sat, 12 Oct 2024 17:29:06 +0800 Subject: [PATCH 109/128] update --- ...44\346\225\260\344\271\213\345\222\214.md" | 27 ++++++------- ...44\346\225\260\347\233\270\345\212\240.md" | 40 +++++++++---------- 2 files changed, 33 insertions(+), 34 deletions(-) diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" index 72102238..b5d71dba 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/1. LeetCode_\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -1,24 +1,28 @@ 1. LeetCode_两数之和 === -给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 +给定一个整数数组nums和一个整数目标值target,请你在该数组中找出和为目标值target的那两个整数,并返回它们的数组下标。 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。 你可以按任意顺序返回答案。 +--- -示例 1: +示例 1 输入:nums = [2,7,11,15], target = 9 输出:[0,1] 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + + 示例 2: 输入:nums = [3,2,4], target = 6 输出:[1,2] -示例 3: + +示例 3: 输入:nums = [3,3], target = 6 输出:[0,1] @@ -27,9 +31,6 @@ 提示: -2 <= nums.length <= 104 --109 <= nums[i] <= 109 --109 <= target <= 109 只会存在一个有效答案 @@ -89,11 +90,11 @@ public: ### 方法二: 哈希表 -注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。 +注意到方法一的时间复杂度较高的原因是寻找target - x的时间复杂度过高。 因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 -使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。 +使用哈希表,可以将寻找target - x的时间复杂度降低到从O(N)降低到O(1)。 --- @@ -104,17 +105,15 @@ public: - key: 存储数组里的值 - value: 存储数组的下标index -这样我们只需要通过target - nums[i]的值去Map中查找即可。 -但是这样存在一个问题,就是你需要先把数组中的值都放到Map中,需要多一步循环。 - - +这样我们只需要通过target - nums[i]的值去Map中查找即可。 +但是这样存在一个问题,就是你需要先把数组中的值都放到Map中,需要多一步循环。 --- -其实我们可以先创建一个Map,在Map的初始化过程中什么元素都不放。 +其实我们可以先创建一个Map,在Map的初始化过程中什么元素都不放。 -对于每一个 x,我们首先查询哈希表中是否存在 target - x,如果已存在就返回,如果不存在那再将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。 +对于每一个x,我们首先查询哈希表中是否存在target-x,如果已存在就返回,如果不存在那再将x插入到哈希表中,即可保证不会让x和自己匹配。 这样只需要一次循环就可以了,而且Map数组不用提前初始化,在性能和内存占用率都比较低。 diff --git "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" index 789a2a9f..03581f19 100644 --- "a/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" +++ "b/JavaKnowledge/\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/2. LeetCode_\344\270\244\346\225\260\347\233\270\345\212\240.md" @@ -1,11 +1,11 @@ -2. LeetCode_两数相加 +2. LeetCode_两数相加 === -给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。 +给你两个非空的链表,表示两个非负的整数。它们每位数字都是按照逆序的方式存储的,并且每个节点只能存储一位数字。 请你将两个数相加,并以相同形式返回一个表示和的链表。 -你可以假设除了数字 0 之外,这两个数都不会以 0 开头。 +你可以假设除了数字0之外,这两个数都不会以0开头。 输入:l1 = [2,4,3], l2 = [5,6,4] @@ -18,31 +18,31 @@ 输出:[8,9,9,9,0,0,0,1] -提示: +提示: -- 每个链表中的节点数在范围 [1, 100] 内 +- 每个链表中的节点数在范围[1, 100]内 - 0 <= Node.val <= 9 - 题目数据保证列表表示的数字不含前导零 -想法: +想法: 我的想法是先把两个链表转成两个整数相加,然后把这个整数转成字符串再次生成链表。 这种方案是不可行的,因为链表的长度可能会很长,整数是操作不了的,例如: -方法: +方法: -由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。 +- 由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。 -我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry; +- 我们同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。具体而言,如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry; 其中,答案链表处相应位置的数字为 (n1+n2+carry)mod10,而新的进位值为 [(n1+n2+carry) / 10] -如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。 +- 如果两个链表的长度不同,则可以认为长度短的链表的后面有若干个 0 。 -此外,如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。 +- 此外,如果链表遍历结束后,有carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。 @@ -50,12 +50,12 @@ ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum.png?raw=true) -- 将链表反过来看,头结点在右侧 -- 横线上的数字为进位 -- 2 + 5 + 0(第一个进位默认为0) = 7 +- 将链表反过来看,头结点在右侧 +- 横线上的数字为进位 +- 2 + 5 + 0(第一个进位默认为0) = 7 - - 7 % 10得到新节点中的元素为7 - - 7 / 10得到下一个进位为0 + - 7 % 10得到新节点中的元素为7 + - 7 / 10得到下一个进位为0 ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_2_twoaddsum_2.png?raw=true) @@ -123,11 +123,11 @@ class Solution { -复杂度分析: +复杂度分析: -- 时间复杂度:O(max(m,n)),其中 m 和 n 分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要 O(1) 的时间。 +- 时间复杂度: O(max(m,n)),其中m和n分别为两个链表的长度。我们要遍历两个链表的全部位置,而处理每个位置只需要O(1)的时间。 -- 空间复杂度:O(1)。注意返回值不计入空间复杂度。 +- 空间复杂度: O(1)。注意返回值不计入空间复杂度。 ### 改进: 递归 @@ -139,7 +139,7 @@ class Solution { } } - public ListNode add(ListNode l1, ListNode l2, int carry) { +public ListNode add(ListNode l1, ListNode l2, int carry) { if (l1 == null && l2 == null && carry == 0) { return null; } From 15e18a0332746dde75530ce0564a757c498eb8ab Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 14 Oct 2024 16:31:43 +0800 Subject: [PATCH 110/128] =?UTF-8?q?Update=20Android=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=AF=A6=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...12\250\346\250\241\345\274\217\350\257\246\350\247\243.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index 6c5ef656..eb958f00 100644 --- "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -33,7 +33,7 @@ Android启动模式详解 顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! 它与`singleTask`有相同之处,也有不同之处。 相同之处:任意时刻,最多只允许存在一个实例。 -不同之处: +不同之处: - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, @@ -59,4 +59,4 @@ Android启动模式详解 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! From a9eac5a7f869e78bbac9bb32093db41f78c9809f Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 14 Oct 2024 16:32:16 +0800 Subject: [PATCH 111/128] =?UTF-8?q?Update=20Android=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=AF=A6=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...250\346\250\241\345\274\217\350\257\246\350\247\243.md" | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index eb958f00..76a6fd8a 100644 --- "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -31,9 +31,10 @@ Android启动模式详解 - `singleInstance` 顾名思义,是单一实例的意思,即任意时刻只允许存在唯一的`Activity`实例,而且该`Activity`所在的`task`不能容纳除该`Activity`之外的其他`Activity`实例! -它与`singleTask`有相同之处,也有不同之处。 -相同之处:任意时刻,最多只允许存在一个实例。 -不同之处: +它与`singleTask`有相同之处,也有不同之处。 +相同之处: 任意时刻,最多只允许存在一个实例。 +不同之处: + - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, From dd55b6b78472c40f5602859468226060af302836 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 14 Oct 2024 16:34:06 +0800 Subject: [PATCH 112/128] =?UTF-8?q?Update=20Android=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E8=AF=A6=E8=A7=A3.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...250\241\345\274\217\350\257\246\350\247\243.md" | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" index 76a6fd8a..5008840f 100644 --- "a/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" +++ "b/AdavancedPart/Android\345\220\257\345\212\250\346\250\241\345\274\217\350\257\246\350\247\243.md" @@ -35,15 +35,17 @@ Android启动模式详解 相同之处: 任意时刻,最多只允许存在一个实例。 不同之处: - - `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 - - `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 - - 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时, - 不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 +- `singleTask`受`android:taskAffinity`属性的影响,而`singleInstance`不受`android:taskAffinity`的影响。 +- `singleTask`所在的`task`中能有其它的`Activity`,而`singleInstance`的`task`中不能有其他`Activity`。 +- 当跳转到`singleTask`类型的`Activity`,并且该`Activity`实例已经存在时,会删除该`Activity`所在`task`中位于该`Activity`之上的全部`Activity`实例;而跳转到`singleInstance`类型的`Activity`,并且该`Activity`已经存在时,不需要删除其他`Activity`,因为它所在的`task`只有该`Activity`唯一一个`Activity`实例。 -假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢?使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。 +假设我们的程序中有一个Activity是允许其他程序调用的,如果想实现其他程序和我们的程序可以共享这个Activity的实例,应该如何实现呢? -假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。 +使用前面3种启动模式肯定是做不到的,因为每个应用程序都会有自己的返回栈,同一个Activity在不同的返回栈中入栈时必然创建了新的实例。 +而使用singleInstance模式就可以解决这个问题,在这种模式下,会有一个单独的返回栈来管理这个Activity,不管是哪个应用程序来访问这个Activity,都共用同一个返回栈,也就解决了共享Activity实例的问题。 + +假设现在有FirstActivity、SecondActivity、ThirdActivity三个Activity, SecondActivity的启动模式是SingleInstance。 现在FirstActivity 启动SecondActivity,SecondActivity再启动ThirdActivity。 然后我们按下Back键进行返回,你会发现ThirdActivity竟然直接返回到了FirstActivity,再按下Back键又会返回到SecondActivity,再按下Back键才会退出程序,这是为什么呢?其实原理很简单,由于FirstActivity和ThirdActivity是存放在同一个返回栈里的,当在ThirdActivity的界面按下Back键时,ThirdActivity会从返回栈中出栈,那么FirstActivity就成为了栈顶Activity显示在界面上,因此也就出现了从ThirdActivity直接返回到FirstActivity的情况。然后在FirstActivity界面再次按下Back键,这时当前的返回栈已经空了,于是就显示了另一个返回栈的栈顶Activity,即SecondActivity。最后再次按下Back键,这时所有返回栈都已经空了,也就自然退出了程序。 From df2d625c4a3adc807babfb97ad21ed03d8c4d4df Mon Sep 17 00:00:00 2001 From: Charon Date: Fri, 18 Oct 2024 17:37:52 +0800 Subject: [PATCH 113/128] =?UTF-8?q?Update=201.OpenCV=E7=AE=80=E4=BB=8B.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- "VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" index d9b77c6e..0a7a5eb1 100644 --- "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" @@ -51,8 +51,8 @@ API: 默认的BGR 彩色图像加载方式,此外支持灰度图像和任意格式。注意:在OpenCV中颜色值写法是BGR,而不是RGB。 imread() 函数默认加载图像文件,加载进来的是3 通道彩色图像,色彩空间是RGB 色彩空间。 通道顺序是:BGR (蓝色、绿色、红色)。 - 通道分离函数:split() - 通道合并函数:merge() + 通道分离函数:split() + 通道合并函数:merge() - imshow("namexx", ...url...):显示图像 - getStructuringElement(...):获取腐蚀的内核矩阵 - erode(...):腐蚀(源图像、目的图像、内核矩阵) From 7e957935d1d7c69ad1f87c383b9a0637d9809086 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 20 Dec 2024 14:19:45 +0800 Subject: [PATCH 114/128] update --- ...345\217\212ANR\345\210\206\346\236\220.md" | 20 +++-- AdavancedPart/Java | 0 Gradle&Maven/kts.md | 52 ++++++++----- ...37\347\220\206\345\210\206\346\236\220.md" | 29 +++++--- .../1.OpenCV\347\256\200\344\273\213.md" | 2 +- ...16\351\242\234\346\273\244\351\225\234.md" | 43 +++++++++++ ...01\351\245\261\345\222\214\345\272\246.md" | 74 +++++++++++++++++++ 7 files changed, 184 insertions(+), 36 deletions(-) create mode 100644 AdavancedPart/Java create mode 100644 "VideoDevelopment/OpenGL/15.\347\276\216\351\242\234\346\273\244\351\225\234.md" create mode 100644 "VideoDevelopment/OpenGL/16.\350\211\262\346\270\251\343\200\201\350\211\262\350\260\203\343\200\201\351\245\261\345\222\214\345\272\246.md" diff --git "a/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" index df4d8ccf..54dcdcfa 100644 --- "a/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" +++ "b/AdavancedPart/Crash\345\217\212ANR\345\210\206\346\236\220.md" @@ -4,9 +4,13 @@ ## 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,并忽略当前应用的广播; @@ -19,8 +23,11 @@ 当1分钟内同一进程未发生连续crash两次时,则执行结束栈顶正在运行activity的流程。 7、通过mUiHandler发送消息SHOW_ERROR_MSG,弹出crash对话框; + 8、到此,system_server进程执行完成。回到crash进程开始执行杀掉当前进程的操作; + 9、当crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked(); + 10、最后,执行清理应用相关的四大组件信息。 @@ -35,12 +42,15 @@ -Native Crash +Native Crash: + +- 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 + +- 定位:so 库剥离调试信息的话,只有相对位置没有具体行号,可以使用 NDK 提供的 addr2line 或 ndk-stack 来定位 + +- addr2line:根据有调试信息的 so 和相对位置定位实际的代码处 - 崩溃过程:native crash 时操作系统会向进程发送信号,崩溃信息会写入到 data/tombstones 下,并在 logcat 输出崩溃日志 - 定位:so 库剥离调试信息的话,只有相对位置没有具体行号,可以使用 NDK 提供的 addr2line 或 ndk-stack 来定位 - addr2line:根据有调试信息的 so 和相对位置定位实际的代码处 - ndk-stack:可以分析 tombstone 文件,得到实际的代码调用栈 +- ndk-stack:可以分析 tombstone 文件,得到实际的代码调用栈 diff --git a/AdavancedPart/Java b/AdavancedPart/Java new file mode 100644 index 00000000..e69de29b diff --git a/Gradle&Maven/kts.md b/Gradle&Maven/kts.md index a12bb7ca..c6a2e26c 100644 --- a/Gradle&Maven/kts.md +++ b/Gradle&Maven/kts.md @@ -4,60 +4,72 @@ Gradle 支持使用 Groovy DSL 或 Kotlin DSL 来编写脚本。所以在学习 所以下面来学习一下这两种语言的差异。 -1. Groovy 和 Kotlin 的差异 +1. Groovy 和 Kotlin 的差异 + 1.1 语言差异 -Groovy -Groovy是一种基于 JVM 的面向对象的编程语言,它可以作为常规编程语言,但主要是作为脚本的语言(为了解决 Java 在写脚本时过于死板)。它是一个动态语言,可以不指定变量类型。它的特性是支持闭包,闭包的本质很简单,简单的说就是定义一个匿名作用域,这个作用域内部可以封装函数和变量,外部不可以访问这个作用域内部的东西,但是可以通过调用这个作用域来完成一些任务。 -Kotlin -Kotlin 则是 Java 的优化版,在解决 Kotlin 很多痛点的情况下,不引入过多的新概念。它具有强大的类型推断系统,使得语言有良好的动态性,其次是其语言招牌 —— 语法糖,Kotlin 的代码可以写的非常简洁。这使得 Kotlin 不仅做为常规编程语言能大放异彩,作为脚本语言也深受很多开发者喜爱。 + +Groovy是一种基于 JVM 的面向对象的编程语言,它可以作为常规编程语言,但主要是作为脚本的语言(为了解决 Java 在写脚本时过于死板)。 + +它是一个动态语言,可以不指定变量类型。它的特性是支持闭包,闭包的本质很简单,简单的说就是定义一个匿名作用域,这个作用域内部可以封装函数和变量,外部不可以访问这个作用域内部的东西,但是可以通过调用这个作用域来完成一些任务。 + +Kotlin则是Java的优化版,在解决Kotlin很多痛点的情况下,不引入过多的新概念。它具有强大的类型推断系统,使得语言有良好的动态性,其次是其语言招牌 —— 语法糖,Kotlin 的代码可以写的非常简洁。这使得 Kotlin 不仅做为常规编程语言能大放异彩,作为脚本语言也深受很多开发者喜爱。 + 它们共同特点就是基于JVM,可以和 Java 互操作。Gradle 能提供的东西, Kotlin 也能通过提供(闭包)。在功能上,两者能做的事情都是一样的。此外一些简单的差异有: -groovy 字符串可以使用单引号,而 kotlin 则必须为双引号 -groovy 在方法调用时可以省略扩号,而 kotlin 不可省略 -groovy 分配属性时可以省略 = 赋值运算符,而 kotlin 不可省略 +groovy 字符串可以使用单引号,而 kotlin 则必须为双引号。 +groovy 在方法调用时可以省略扩号,而 kotlin 不可省略。 +groovy 分配属性时可以省略 = 赋值运算符,而 kotlin 不可省略。 groovy 是动态语言,不用导包,而 kotlin 则需要。 -为什么要用Kotlin DSL写gradle脚本 +为什么要用Kotlin DSL写gradle脚本? + 撇开其他方面,就单从提高程序员生产效率方面就有很多优点: -脚本代码自动补全 -跳转查看源码 -动态显示注释 -支持重构(Refactoring) +- 脚本代码自动补全 +- 跳转查看源码 +- 动态显示注释 +- 支持重构(Refactoring) … 怎么样,要是你经历过groovy那令人蛋疼的体验,kotlin会让你爽的起飞,接下来让我们开始吧。 从Groovy到Kotlin + 让我们使用Android Studio 新建一个Android项目,AS默认会为我们生成3个gradle脚本文件。 -settings.gradle (属于 project) -build.gradle (属于 project) -build.gradle (属于 module) +- settings.gradle (属于 project) +- build.gradle (属于 project) +- build.gradle (属于 module) + 我们的目的就是转换这3个文件 -第一步: 修改groovy语法到严格格式 +- 第一步: 修改groovy语法到严格格式 + groovy既支持双引号""也支持单引号'',而kotlin只支持双引号,所以首先将所有的单引号改为双引号。 例如 include ':app' -> include ":app" groovy方法调用可以不使用() 但是kotlin方法调用必须使用(),所以将所有方法调用改为()方式。 -例如 +例如: +```java implementation "androidx.appcompat:appcompat:1.0.2" 改为 implementation ("androidx.appcompat:appcompat:1.0.2") groovy 属性赋值可以不使用=,但是kotlin属性赋值需要使用=,所以将所有属性赋值添加=。 +``` -例如 - +例如: +``` applicationId "com.ss007.gradlewithkotlin" 改为 applicationId = "com.ss007.gradlewithkotlin" +``` + 完成以上几步,准备工作就完成了。 diff --git "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" index a5192325..02f9c51c 100644 --- "a/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" +++ "b/JavaKnowledge/HashMap\345\256\236\347\216\260\345\216\237\347\220\206\345\210\206\346\236\220.md" @@ -49,10 +49,20 @@ HashMap是无序的,因为HashMap无法保证内部存储的键值对的有序 获取对象时,我们将K传给get()方法,它调用hashCode()计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。 扩容前后,哈希桶的长度一定会是2的次方。这样在根据key的hash值寻找对应的哈希桶时,可以用位运算替代取余操作,更加高效。 + 而key的hash值,并不仅仅只是key对象的hashCode()方法的返回值,还会经过扰动函数的扰动,以使hash值更加均衡。 -因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 但就算原本的hashCode()取的很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。因此只有hashCode()的低位参加运算, -发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 -扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 high位= low位+原哈希桶容量如果追加节点后,链表数量 >=8,且只有数组长度大于64才处理,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, + +因为hashCode()是int类型,取值范围是40多亿,只要哈希函数映射的比较均匀松散,碰撞几率是很小的。 + +但就算原本的hashCode()取的很好,每个key的hashCode()不同,但是由于HashMap的哈希桶的长度远比hash取值范围小,默认是16,所以当对hash值以桶的长度取余,以找到存放该key的桶的下标时,由于取余是通过与操作完成的,会忽略hash值的高位。 + +因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。即碰撞率会增大。 + +扰动函数就是为了解决hash碰撞的。它会综合hash值高位和低位的特征,并存放在低位,因此在与运算时,相当于高低位一起参与了运算,以减少hash碰撞的概率。(在JDK8之前,扰动函数会扰动四次,JDK8简化了这个操作)扩容操作时,会new一个新的Node数组作为哈希桶,然后将原哈希表中的所有数据(Node节点)移动到新的哈希桶中,相当于对原哈希表中所有的数据重新做了一个put操作。所以性能消耗很大,可想而知,在哈希表的容量越大时,性能消耗越明显。 + +扩容时,如果发生过哈希碰撞,节点数小于8个。则要根据链表上每个节点的哈希值,依次放入新哈希桶对应下标位置。因为扩容是容量翻倍,所以原链表上的每个节点,现在可能存放在原来的下标,即low位, 或者扩容后的下标,即high位。 + +high位= low位+原哈希桶容量,如果追加节点后,链表数量>=8,且只有数组长度大于64才处理,则转化为红黑树由迭代器的实现可以看出,遍历HashMap时, 顺序是按照哈希桶从低到高,链表从前往后,依次遍历的。 数组的特点:查询效率高,插入删除效率低。 @@ -70,7 +80,7 @@ HashMap在JDK1.8中发生了改变,下面的部分是基于JDK1.7的分析。H ```java transient Entry[] table; ``` -可以看到Map是通过数组的方式来储存Entry那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: +可以看到Map是通过数组的方式来储存Entry,那Entry是神马呢?就是HashMap存储数据所用的类,它拥有的属性如下: ```java static class Entry implements Map.Entry { final K key; @@ -177,12 +187,11 @@ public V get(Object key) { ## JDK1.8 -在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方, -数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,且只有数组长度大于64才处理,将链表转换为红黑树。 -利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。HashMap中, -如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1), -如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中, -或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 +在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变的,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,且只有数组长度大于64才处理,将链表转换为红黑树。 + +利用红黑树快速增删改查的特点来提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。 + +HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的话,getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算法极其差,所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn)。 ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/hashmap_data_structure.jpg) diff --git "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" index 0a7a5eb1..f9dd0282 100644 --- "a/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenCV/1.OpenCV\347\256\200\344\273\213.md" @@ -127,7 +127,7 @@ copyTo()。 在早期的OpenCV1.x 版本中,图像的处理是通过IplImage(该名称源于Intel 的另一个 开源库Intel Image Processing Library ,缩写成IplImage)结构来实现的。 -早期的OpenCV 是用C 语言编写,因此提供的借口也是C 语言接口,其源代码完全是C +早期的OpenCV 是用C 语言编写,因此提供的接口也是C 语言接口,其源代码完全是C 的编程风格。IplImage 结构是OpenCV 矩阵运算的基本数据结构。 到OpenCV2.x 版本,OpenCV 开源库引入了面向对象编程思想,大量源代码用C++重写。 diff --git "a/VideoDevelopment/OpenGL/15.\347\276\216\351\242\234\346\273\244\351\225\234.md" "b/VideoDevelopment/OpenGL/15.\347\276\216\351\242\234\346\273\244\351\225\234.md" new file mode 100644 index 00000000..043db19f --- /dev/null +++ "b/VideoDevelopment/OpenGL/15.\347\276\216\351\242\234\346\273\244\351\225\234.md" @@ -0,0 +1,43 @@ +美颜滤镜 +--- + +美颜的基本原理就是深度学习加计算机图形学。 + +- 深度学习用来人脸检测和人脸关键点检测。 +- 计算机图形学用来磨皮、瘦脸和化妆容。 +- 一般在Android上使用OpenGL ES,iOS使用Metal。 + +## 美颜算法的原理 + + +美颜算法主要是基于图像处理技术,包括人脸检测、皮肤平滑、肤色调整、瘦脸大眼瞪。 +其中,人脸检测是美颜的第一步,通过算法识别出照片中的人脸区域,然后针对该区域进行后续的美化处理。 + +### 关键技术点 + +- 人脸检测:利用OpenCV(强大的计算机视觉库,支持人脸检测等)、Dlib等库实现。 +- 皮肤平滑:通过高斯模糊、双边滤波等技术减少皮肤瑕疵。 +- 肤色调整:调整肤色饱和度、亮度等,使肤色更加自然。 +- 瘦脸大眼:基于人脸关键点定位,通过变形算法实现。 + + + +### 人脸检测 + +- 人脸检测指的是对图片或者视频流中的人脸进行检测,并定位到图片中的人脸。 +- 人脸关键点检测是对人脸中五官和脸的轮廓进行关键点定位,一般情况下它紧接在人脸检测后。 + + +- 加载OpenCV库,在项目中集成OpenCV库,并加载本地人脸检测模型。 +- 实时处理:在相机预览的回调中,对每一帧图像进行人脸检测,并应用美颜算法。 + + + + + +https://zhuanlan.zhihu.com/p/163604590 + + + + + diff --git "a/VideoDevelopment/OpenGL/16.\350\211\262\346\270\251\343\200\201\350\211\262\350\260\203\343\200\201\351\245\261\345\222\214\345\272\246.md" "b/VideoDevelopment/OpenGL/16.\350\211\262\346\270\251\343\200\201\350\211\262\350\260\203\343\200\201\351\245\261\345\222\214\345\272\246.md" new file mode 100644 index 00000000..8dff2153 --- /dev/null +++ "b/VideoDevelopment/OpenGL/16.\350\211\262\346\270\251\343\200\201\350\211\262\350\260\203\343\200\201\351\245\261\345\222\214\345\272\246.md" @@ -0,0 +1,74 @@ + +- 色温: 图片的色温指的是图片中呈现出来的整体颜色偏暖或偏冷的程度。 + +色温可以影响图像的分为和情感表达。简单理解就是色彩的温度,越低越冷如蓝色,越高越暖如红色。 + +- 色调: 色调是指图片色彩的整体质感,包括主要色调的选择和组合。 + +通过调整色调,可以改变图像的整体色彩效果,营造特定的视觉风格或情绪。简单理解就是色彩倾向,倾向于红橙还是黄绿。 + +- 亮度: 亮度描述了图片中各个部分的明暗程度。增加亮度将使整体图像看起来更明亮,减小亮度则会使图片变得更暗。 + +亮度的调整可以影响图像的整体视觉效果和光线感。 +增加就是给图片所有色彩增加白色,减少黑色。注意是只加黑白两种颜色,不然容易跟纯度弄混。 + +- 对比度: 对比度是描述图像中不同区域之间亮度差异的程度。 + +提高对比度会使图像中的明暗部分更加突出,增强图像的清晰度和视觉冲击力。 +适当的对比度可以让图像更具有立体感和层次感。 +增加就是让白的更白,黑的更黑。减少就是白的不那么白,黑的不那么黑。 + +- 饱和度: 饱和度表示图片中颜色的纯度和鲜艳程度。 + +增加饱和度会使颜色更加丰富饱满,减少饱和度会使颜色变得更加灰暗。 +调整饱和度可以改变图像的整体色彩和视觉吸引力。 + +简单理解就是增加图片各种颜色的纯度。 +比如蓝色,增加纯度就是在蓝色上加蓝色,降低纯度就是键入蓝色的对比色,让它变灰色或黑色。 + +- 高光: 高光是指图片中最亮的部分,通常实在光线照射下产生的明亮区域。 + +调整高光可以改变图像中亮部的强度和反射效果,从而影响图像的细节和光影效果。 +增加就是给图片白色的部分再加点白色,减少就是减少点白色。 + + + + + +### RGB颜色模型 + +RGB颜色模型是一种用于创建各种颜色的方法,它基于红色、绿色、蓝色三种颜色的组合。 + +通过调节这三种颜色的强度和比例,可以生成多种不同的颜色。 + +### HSV颜色模型 + +HSV代表色相(Hue)、饱和度(Saturation)、明度(Value),或色相(Hue)、饱和度(Saturation)、亮度(Brightness)。 + +在HSV模型中,色相同样表示颜色本身,饱和度表示颜色的纯度或浓淡程度,而明度或亮度则表示颜色的亮度程度。 + +HSV模型有时也被称为HSB(色相、饱和度、亮度)模型。 + + +HSV(HSB)颜色模型根据下图对应说明可以得知: + +- H(Hue):色调,用角度度量,取值范围为0°~360° ,从红色开始按逆时针方向计算 +- S(Saturation):饱和度,表示颜色接近光谱色的程度.一种颜色,可以看成是某种光谱色不白色混合的结果.通常取值范围为0%~100%,值越大,颜色越饱和.光谱色的白光成分为0,饱和度达到最高. +- V(Value或Brightness):明度,表示颜色明亮的程度. + + + +### HSL颜色模型 + +HSL代表色相(Hue)、饱和度(Stauration)、亮度(Lightness)。 + +在HSL模型中,色相表示颜色本身,饱和度表示颜色纯度或浓淡程度,亮度则表示颜色的明暗程度。 + +- H: 色相,使用不水平轴之间的角度来表示,范围是从(0 ~ 360)度。从蓝色开始。 +- S: 饱和度,说明颜色的相对浓度。 +- L: 亮度,在L=0处为黑色,在L=1处为白色。灰色沿着L轴分布。 + + + + + From a782cd797f46c5a4826445c37dc04097499e9243 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 11 Feb 2025 15:51:04 +0800 Subject: [PATCH 115/128] add notes --- ...36\346\224\266\346\234\272\345\210\266.md" | 38 +++- ...14Synchronized\345\214\272\345\210\253.md" | 6 + ...01\350\231\232\345\274\225\347\224\250.md" | 15 ++ .../1.OpenGL\347\256\200\344\273\213.md" | 74 +++++++- VideoDevelopment/OpenGL/12.FBO.md | 165 ++++++++++++++++ ....GLSurfaceView\347\256\200\344\273\213.md" | 11 +- ...66\344\270\211\350\247\222\345\275\242.md" | 4 + ...45\231\250\350\257\255\350\250\200GLSL.md" | 16 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 176 +++++++++++++++++- .../9.OpenGL ES\347\272\271\347\220\206.md" | 71 ++++++- ...26\347\240\201\346\240\274\345\274\217.md" | 5 + 11 files changed, 562 insertions(+), 19 deletions(-) diff --git "a/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" index 7c964b63..6e1a10a4 100644 --- "a/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" +++ "b/JavaKnowledge/JVM\345\236\203\345\234\276\345\233\236\346\224\266\346\234\272\345\210\266.md" @@ -1,5 +1,41 @@ # JVM垃圾回收机制 + +当前主流的商用程序语言(Java、C#)的内存管理子系统都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。 + +这个算法的基本思路就是通过一系列被称为"GC Roots"的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所做过的路径称为"引用链"(Reference Chain), +如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。 + + +如下图: 对象object5、object6、object7虽然互有关联,但是它们到GC Roots是不可达的,因此它们将会被判定为可回收的对象。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/reachability_analysis.png) + + +在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: + +- 在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。 + +- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 + +- 在方法区中常量引用的对象,譬如字符串常量池(String Table)中的引用。 + +- 在本地方法栈中JNI(即通常所说的Native方法)引用的对象。 + +- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointException、OutOfMemoryError)等,还有系统类加载器。 + +- 所有被同步锁(synchronized关键字)持有的对象。 + +- 反应Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。 + + +除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合。譬如后文将会提到的分代收集和局部回收(Partial GC),如果只针对Java堆中某一块区域发起垃圾收集时(如最典型的只针对新生代的垃圾收集)​,必须考虑到内存区域是虚拟机自己的实现细节(在用户视角里任何内存区域都是不可见的)​,更不是孤立封闭的,所以某个区域里的对象完全有可能被位于堆中其他区域的对象所引用,这时候就需要将这些关联区域的对象也一并加入GC Roots集合中去,才能保证可达性分析的正确性。 + + + + + [Java内存模型](https://github.com/CharonChui/AndroidNote/blob/master/JavaKnowledge/Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.md)如下图所示,**堆和方法区是所有线程共有的**,而虚拟机栈,本地方法栈和程序计数器则是线程私有的。 ![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jvm.png) @@ -460,4 +496,4 @@ NUMA 是一种多核服务器的架构,简单来讲,一个多核服务器( --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! diff --git "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" index 82801018..8894decc 100644 --- "a/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" +++ "b/JavaKnowledge/volatile\345\222\214Synchronized\345\214\272\345\210\253.md" @@ -183,6 +183,12 @@ ctorInstance(memory); //2.初始化对象 指令重排序后,在多线程情况下,可能会发生A线程正在new对象,执行了3,但还没有执行2。此时B线程进入方法获取单例对象,执行同步代码块外的非空判断,发现变量非空,但此时对象还未初始化,B线程获取到的是一个未被初始化的对象。使用volatile修饰后,禁止指令重排序。即,先初始化对象后,再设置instance指向刚分配的内存地址。这样就就不存在获取到未被初始化的对象。 + + +那为何说它禁止指令重排序呢?从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况保障程序能得出正确的执行结果。譬如指令1把地址A中的值加10,指令2把地址A中的值乘以2,指令3把地址B中的值减去3,这时指令1和指令2是有依赖的,它们之间的顺序不能重排——(A+10)*2与A*2+10显然不相等,但指令3可以重排到指令1、2之前或者中间,只要保证处理器执行后面依赖到A、B值的操作时能获取正确的A和B值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl$0x0,(%esp)指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了“指令重排序无法越过内存屏障”的效果。 + + + ## synchronized **synchronized实现同步的基础是:Java中的每个对象都可作为锁。所以synchronized锁的都对象,只不过不同形式下锁的对象不一样。** 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 48d014de..c656ea7f 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" @@ -66,6 +66,21 @@ ![](https://raw.githubusercontent.com/CharonChui/Pictures/master/reference_compare.jpg) + +无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否引用链可达,判定对象是否存活都和“引用”离不开关系。在JDK 1.2版之前,Java里面的引用是很传统的定义:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。 + + +在JDK 1.2版之后,Java对引用的概念进行了扩充,将引用分为强引用(Strongly Re-ference)、软引用(Soft Reference)、弱引用(Weak Reference)和虚引用(Phantom Reference)4种,这4种引用强度依次逐渐减弱。 + + +- 强引用是最传统的“引用”的定义,是指在程序代码之中普遍存在的引用赋值,即类似“Object obj=new Object()”这种引用关系。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 + +- 软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2版之后提供了SoftReference类来实现软引用。 + +- 弱引用也是用来描述那些非必须对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2版之后提供了WeakReference类来实现弱引用。 + +- 虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版之后提供了PhantomReference类来实现虚引用。 + --- - 邮箱 :charon.chui@gmail.com diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index a347c9e6..e84f18ce 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -43,6 +43,10 @@ OpenGL ES由OpenGL裁剪而来,因此有必要了解一下两者版本的对 +图像数据无非是一个个的像素点,对图像数据的处理无非是对每个像素点进行计算后重新赋值,一般来说对每个像素点的计算都比较独立,计算也相对简单。CPU虽然计算能力强大,但是并行处理能力有限,对一张720P的图片,一共包含720*1280=921600个像素,要进行这么多次运算,CPU也要望洋兴叹了。GPU与CPU相比最大的优势就是并行处理能力,一般移动端的GPU也要包含数千个处理单元,这些处理单元虽然计算能力比不上CPU,但是却可以同时处理几千个像素点。像素点数据的计算相对简单,而且可以同时处理几千个像素点,图像数据用GPU来做计算就非常适合了。而怎么使用GPU呢?这就要介绍到目前使用最广泛的2D、3D矢量图形沉浸API:OpenGL了。 + + + ### Android中的OpenGL ES Android中OpenGL ES的版本支持如下: @@ -69,8 +73,8 @@ OpenGL ES的实现有2中方式: - OpenGL自身是一个巨大的状态机: 描述该如何操作的所有变量的大集合 - OpenGL的状态通常被称为上下文(Context) -- 状态设置函数(State-changing Function) -- 状态应用的函数(State-using Function) +- 状态设置函数(State-changing Function): 该类函数将会改变上下文 +- 状态应用的函数(State-using Function): 该类函数会根据当前OpenGL的状态执行一些操作。 ![image](https://github.com/CharonChui/Pictures/blob/master/opengl_state_machine.png?raw=true) @@ -109,7 +113,21 @@ struct object_Window_Target { } ``` -通常把OpenGL上下文比作一个大的结构体,包含很多子集。 + +但是,这样也有问题: + +- 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦。 +- 这时候我们就需要一些小助理(对象),来帮忙记录某些状态信息,以便复用。 + +如果我们有10种子集,那就需要10个小助理(对象),而当前状态(Context,只有一份)可以通过装配这些对象来完成。 + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_zhuli.jpg?raw=true) + + +通常把OpenGL上下文比作一个大的结构体,包含很多子集。 + +在OpenGL中一个对象是指一些选项的集合,它代表OpenGL状态的一个子集。 +比如,我们可以用一个对象来代表绘制窗口的设置,之后我们就可以设置它的大小等。 ```C // OpenGL的状态 @@ -120,20 +138,48 @@ struct OpenGL_Context { } ``` -但是,这样也有问题: -- 当前状态只有一份,如果每次显示不同的效果,都重新配置会很麻烦。 -- 这时候我们就需要一些小助理(对象),来帮忙记录某些状态信息,以便复用。 -如果我们有10种子集,那就需要10个小助理(对象),而当前状态(Context,只有一份)可以通过装配这些对象来完成。 +```c++ +// 创建对象 +unsigned int objectId = 0; +glGenObject(1, &objectId); -![image](https://github.com/CharonChui/Pictures/blob/master/opengl_zhuli.jpg?raw=true) +// 绑定对象至上下文 +glBindObject(GL_WINDOW_TARGET, objectId); +// 设置当前绑定到GL_WINDOW_TARGET的对象的一些选项 +glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800); +glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600); +// 将上下文对象设回默认 +glBindObject(GL_WINDOW_TARGET, 0); +``` +上面这一小段代码展示了以后使用OpenGL时常见的工作流。 + +- 我们首先创建一个对象,然后用一个id保存它的引用(实际数据被存储在后台)。 +- 然后我们将对象绑定至上下文的目标位置(例子中窗口对象目标的位置被定义成GL_WINDOW_TARGET) +- 接下来我们设置窗口的选项。 +- 最后我们将目标位置的对象id设回0,解绑这个对象。 +**设置的选项将被保存在objectId所引用的对象中,一旦我们重新绑定这个对象到GL_WINDOW_TARGET位置,这些选项就会重新生效。** + +使用对象的一个好处是在程序中,我们不止可以定义一个对象,并设置它们的选项,每个对象都可以是不同的设置。 + +在我们执行一个使用OpenGL状态的操作的时候,只需要绑定含有需要的设置的对象即可。 ### 渲染管线 + +渲染管线有时也称为渲染流水线,一般由显示芯片(GPU)内部处理图形信号的并行处理单元组成。 +这些并行处理单元两两之间是相互独立的,在不同型号的硬件上独立处理单元的数量也有很大的差异。一般越高端型号的硬件,其中独立处理单元的数量也就越多。 + + +与普通应用程序通过CPU串行执行不同的是,渲染工作是通过渲染管线由多个相互独立的处理单元进行并行处理的,这种模式极大的提升了渲染效率。 + +从另一个角度看,OpenGLES中的渲染管线实际上指的是一系列绘制过程。 +这些过程输入的是待渲染3D物体的相关描述信息数据,经过渲染管线,输出的是一帧想要的图像。 + 在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。 3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。 @@ -448,6 +494,16 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 ### 片段着色器或片元着色器(Fragment Shader) + +显示设备屏幕都是离散化的(由一个一个的像素组成),因此还需要将投影的结果离散化。 +将其分解为一个个离散化的小单元,这些小单元一版称为片元。 + +其实每个片元都对应于帧缓冲中的一个像素,之所以不直接称为像素是因为3D空间中的物体是可以相互遮挡的。而一个3D场景最终显示到屏幕上虽然是一个整体,但每个3D物体的每个图元是独立处理的。这就可能出现这样的情况,系统先处理的是位于离观察点较远的图元,其光栅化成为了一组图元,暂时送入帧缓冲的对应位置。 + +但后面继续处理离观察点较近的图元时也光栅化出了一组片元,两组片元中有对应到帧缓冲中同一个位置的,这时距离近的片元将覆盖距离远的片元(如何覆盖的检测是在深度检测阶段完成的)。 + +因此,某片元就不一定能成为最终屏幕上的像素,称为像素就不准确了,可以将其理解为候选像素。 + 使用颜色或纹理(texture)渲染图形表面的OpenGLES代码。 @@ -606,7 +662,7 @@ EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一 6. 设置OpenGL的渲染环境 - eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被成为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。 + eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被称为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 diff --git a/VideoDevelopment/OpenGL/12.FBO.md b/VideoDevelopment/OpenGL/12.FBO.md index 736c7f9c..dcbb6993 100644 --- a/VideoDevelopment/OpenGL/12.FBO.md +++ b/VideoDevelopment/OpenGL/12.FBO.md @@ -1,14 +1,107 @@ ## 12.FBO + +什么是 EGL + +EGL 是 OpenGL ES 和本地窗口系统(Native Window System)之间的通信接口,它的主要作用: +与设备的原生窗口系统通信; + +查询绘图表面的可用类型和配置; + +创建绘图表面; + +在OpenGL ES 和其他图形渲染API之间同步渲染; + +管理纹理贴图等渲染资源。 + +OpenGL ES 的平台无关性正是借助 EGL 实现的,EGL 屏蔽了不同平台的差异(Apple 提供了自己的 EGL API 的 iOS 实现,自称 EAGL)。 + +本地窗口相关的 API 提供了访问本地窗口系统的接口,而 EGL 可以创建渲染表面 EGLSurface ,同时提供了图形渲染上下文 EGLContext,用来进行状态管理 + + +当前屏幕渲染(onscreen rendering)和离屏渲染(offscreen rendering) 默认的测试其实就是onscreen rendering,顾名思义,渲染的场景是需要呈现在屏幕上眼睛可见的,而offscreen则是不需要呈现的。 当前屏幕渲染绘制流程是: ``` eglCreateXXXSurface eglCreateContext Draw eglSwapbuffers ``` 离屏渲染流程则是: ``` glGenFramebuffers glBindFramebuffer +glFramebufferTexture2D/glFramebufferRenderbuffer + +Draw + +glFlush (or internal flush) +``` + + +我们知道eglSwapbuffer是为了把EGL当前渲染的surface给呈现到屏幕上,glFlush是为了把GLES当前渲染的结果绘制到和FBO绑定的Texture或者Render Buffer里面。所以简单的说,onscreen rendering是EGL surface的渲染,而offscreen rendering是GLES FBO的渲染。 + + + +当然并不是所有的EGL surface都是onscreen的,一个例外就是EGL pbuffer surface,这是EGL为了offscreen rendering专门定制的。 + + + +### Onscreen和Offscreen的关系 + + + +那么问题来了,这两者到底有什么关系呢? + +下图是一个benchmark某一帧的流程图, + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/onscreen_offscreen.webp?raw=true) + + + +图中的FBO0是最终的屏幕渲染输出,而其他用于离屏渲染的FBO都是为了叠加最后的Blur效果和提供背景模版。通常对于复杂的场景,最终的onscreen rendering是由众多的offscreen rendering的结果组合而成的。 + +Offscreen有什么好处吗 + + + +Onscreen rendering的一个限制因素是当前屏幕的分辨率,比如显示器最多只支持1080p,那么EGL surface大小最大只能是1080p。而offscreen rendering则宽松多了,它只受GPU硬件本身的限制,通常可以支持4K分辨率。所以如果需要不同于当前屏幕分辨率的渲染,反锯齿或者添加新的renderpass,通常会先渲染到offscreen来获取更好的渲染效果。 + + + +因为offscreen是不可见的,GPU可能会有一定的优化。举个例子,原点(0, 0)对于DirectX来讲,通常在左上角,而对于OpenGLES则在左下角。如果GPU是为DirectX设计的,GPU默认的(0, 0)当然是左上角,那么对于OpenGLES的onscreen rendering,就需要做一个Y坐标的翻转,让原点从左下角映射到左上角。然而对于offscreen rendering,GPU就可以默认其原点在左上角,从而避免坐标转换的损耗。 + + + + + FBO(Frame Buffer Object)即帧缓冲区对象,实际上是一个可添加缓冲区的容器,可以为其添加纹理或渲染缓冲区对象(RBO)。 + +顾名思义,它就是能缓存一帧的这么个东西,它有什么用呢? + +大家回想我们之前的教程,我们都是通过一次渲染把内容渲染到屏幕(严格来说是渲染到GLSurfaceview上)。 +如果我们的渲染由多个步骤组成,而每个步骤的渲染结果会给到下一个步骤作为输入,那么就要用到frame buffer。 + +比如一个效果:先把图片的蓝色通道全都设置为0.5,得到的结果再去做一个水平方向的模糊,这时渲染过程就由2步组成,第一步的操作不应该显示到屏幕上,应该有个地方存着它的结果,作为第二步的输入,然后第二步的渲染结果才直接显示到屏幕上。实际上这两步可以合成一步,大家可以思考一下如何用一步实现,这里分成两步主要是为了展示如果使用frame buffer。 + + +FBO从名字上看,往往很容易让人误解这是一个缓存空间,但实际上,FBO很重要的在后面的Object上。 + +这是一个缓存对象,包含了多个缓冲索引,分别是颜色缓冲(Color buffers)、深度缓冲(Depth buffer)、模板缓冲(Stencil buffer)。之所以说是缓冲索引,是因为FBO并不包含这些缓冲数据,仅仅保存了缓冲数据的索引地址。 + +FBO和这些缓冲区则通过附着点进行连接。 + +frame buffer本身其实并不会存储数据,都是通过attachment去绑定别的东西来存储相应的数据,我们今天要讲的就是color attachment,我们会将frame buffer中的一个attachment绑定到一个texture上,然后先将第一步的效果渲染到这个frame buffer上作为中间结果,然后将这个texture作为第二步的输入。 + + FBO本身不能用于渲染,只有添加了纹理或者渲染缓冲区之后才能作为渲染目标,它仅且提供了3种附着(Attachment),分别是颜色附着、深度附着和模板附着。 +只有color buffer用于最后的像素显示,其他的都是用来辅助fragment的处理。 + + RBO(Render Buffer Object)即渲染缓冲区对象,是一个由应用程序分配的2D图像缓冲区。渲染缓冲区可以用于分配和存储颜色、深度或者模板值,可以用作FBO中的颜色、深度或者模板附着。 使用FBO作为渲染目标时,首先需要为FBO的附着添加连接对象,如颜色附着需要连接纹理或者渲染缓冲区对象的颜色缓冲区。 +FBO可以绑定到纹理对象或者RenderBuffer对象,RenderBuffer是以内部格式存储的经过渲染优化的对象,它的渲染速度更快,缺点是无法对渲染进果进行重采样。如果不需要对FBO的输出再做下一步采样处理,就可以用RenderBuffer。在我们的例子中,因为我们要暂存相机流处理着色器的渲染结果,并作为灰度黑着色器程序的输入,即要对此输出结果进行采样,所以我们必须要用FBO绑定纹理对象的方式。 + + +GLSurfaceView的onDrawFrame回调中,默认是绑定了window系统生成的FBO的,这个FBO对应屏幕显示,即0号FBO。只要我们中间不切换FBO,所有的glDrawArray或glDrawElements指令调用都是将目标渲染到这个0号FBO的。 + + + + #### 为什么用 FBO 默认情况下,OpenGL ES通过绘制到窗口系统提供的帧缓冲区,然后将帧缓冲区的对应区域复制到纹理来实现渲染到纹理,但是此方法只有在纹理尺寸小于或等于帧缓冲区尺寸才有效。 @@ -20,6 +113,78 @@ Android OpenGL ES开发中,一般使用GLSurfaceView将绘制结果显示到 使用FBO可以让渲染操作不用再渲染到屏幕上,而是渲染到离屏Buffer中,然后可以使用glReadPixels或者HardwareBuffer将渲染后的图像数据读出来,从而实现在后台利用GPU完成对图像的处理。 +在OpenGL ES中,调用: +```java +GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); +``` +表示将当前的帧缓冲绑定切换回默认帧缓冲,即屏幕帧缓冲。这意味着后续的绘图操作将直接渲染到屏幕,而不是一个自定义的 FrameBuffer Object (FBO)。 + +1. 默认帧缓冲 + • 默认帧缓冲由 OpenGL 创建,并与窗口系统关联。 + • 默认帧缓冲的内容直接显示在屏幕上。 + • 调用 glBindFramebuffer 将目标设置为 0 时: + • GL_FRAMEBUFFER 绑定到默认的屏幕渲染目标。 + +2. 切换渲染目标 + +在 OpenGL 中,你可以使用自定义的帧缓冲对象(FBO)来进行离屏渲染,例如将渲染结果存储到纹理或处理后的数据中。当你完成对自定义 FBO 的渲染任务后,通常需要将绑定切换回默认帧缓冲,这样后续的渲染操作会直接绘制到屏幕。 + +3. 操作流程 + +通常操作流程如下: + 1. 创建并绑定 FBO,进行离屏渲染。 + 2. 渲染完成后,解绑 FBO,将绘图切换回屏幕。 + 3. 渲染其他内容或将离屏渲染的结果作为纹理在屏幕上显示。 + +示例代码: + +```java +// 绑定自定义 FBO,进行离屏渲染 +GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, customFbo); +renderToTexture(); + +// 解绑 FBO,切换回屏幕渲染 +GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0); +renderToScreen(); +``` + + 解除FBO绑定,将窗口大小、纹理坐标、矩阵都恢复回原来的配置。 +将渲染重新切换到原来的系统窗口上,画面将重新显示到系统窗口上。 + + +作用场景 + +1. 屏幕显示 + +完成离屏渲染后,使用默认帧缓冲可以将最终结果显示到屏幕上。 + +2. 渲染链切换 + +当使用多个渲染目标(如多个 FBO)时,通过解绑 FBO,可以轻松地切换回屏幕渲染或默认的绘图目标。 + +3. 调试和测试 + +解绑 FBO 也方便直接在屏幕上观察渲染结果,便于调试。 + + + + +首先,Fbo 的概念性的东西,大家可以上网查查,这里就直接说说Fbo的作用 + +Oes纹理转换2D纹理 + +预览相机、播放视频等这些通过SurfaceTexture方式渲染的,一般都是使用Oes纹理,而当需要在相机预览或者播放视频中添加水印/贴纸,则需要先将Oes纹理转化成2D纹理,因为Oes纹理和2D纹理是不能同时使用 + +保留帧 + +让当前渲染的纹理保留在一个帧缓存里,而不显示在屏幕上 + + + + + + + --- - [上一篇: 11.OpenGL ES滤镜](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/11.OpenGL%20ES%E6%BB%A4%E9%95%9C.md) diff --git "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" index be25becb..0647b7ea 100644 --- "a/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/2.GLSurfaceView\347\256\200\344\273\213.md" @@ -40,7 +40,12 @@ GLSurfaceView继承自SurfaceView,它主要是在SurfaceView的基础上加入 GLSurfaceView.Renderer接口要求您实现以下方法: - onSurfaceCreated():系统会在创建GLSurface时调用一次此方法。使用此方法可执行仅需发生一次的操作,例如设置OpenGL环境参数或初始化OpenGL图形对象。 -- onDrawFrame():完成绘制工作,每一帧图像的渲染都要在这里完成,系统会在每次重新绘制GLSurfaceView时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 +在GLSurfaceView attach到父View后,此方法会被调用。 + +- onDrawFrame(): 完成绘制工作,每一帧图像的渲染都要在这里完成,系统会在每次重新绘制GLSurfaceView时调用此方法。请将此方法作为绘制(和重新绘制)图形对象的主要执行点。 + +在任意时间调用GLSurfaceView的requestRender()方法后,GLSurfaceView会优先执行已保存在GL线程队列中的Runnable,然后调用此onDrawFrame()方法渲染图像。GL线程队列中的所有Runnable和onDrawFrame()方法的调用都执行在GL线程中。 + - onSurfaceChanged():系统会在GLSurfaceView几何图形发生变化(包括GLSurfaceView大小发生变化或设备屏幕方向发生变化)时调用此方法。例如,系统会在设备屏幕方向由纵向变为横向时调用此方法。使用此方法可响应GLSurfaceView容器中的更改。 使用GLSurfaceView和GLSurfaceView.Renderer为OpenGL ES建立容器视图后,您便可以开始使用以下类调用 OpenGL API: @@ -69,7 +74,7 @@ GLSurfaceView的特性: - 管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。 - 管理一个EGL Display,它能让OpenGL把内容渲染到surface上 - 用户自定义渲染器render -- 让渲染器在独立的变成里运作(与UI线程分离) +- 让渲染器在独立的线程里运作(与UI线程分离) - 支持按需渲染(on-demand)和连续渲染(continuous) - 可以封装、跟踪并且排查渲染器的问题 @@ -191,7 +196,7 @@ GLSurfaceView默认采用的是RENDERMODE_CONTINUOUSLY连续渲染的方式, 说到GLSurfaceView就一定要提一下SurfaceTexture。 -和SurfaceView功能类似,区别是,SurfaceTexure可以不显示在界面中。使用OpenGl对图片流进行美化,添加水印,滤镜这些操作的时候我们都是通过SurfaceTexre去处理,处理完之后再通过GlSurfaceView显示。 +和SurfaceView功能类似,区别是,SurfaceTexure可以不显示在界面中。使用OpenGL对图片流进行美化,添加水印,滤镜这些操作的时候我们都是通过SurfaceTexre去处理,处理完之后再通过GLSurfaceView显示。 缺点,可能会导致个别帧的延迟。本身管理着BufferQueue,所以内存消耗会多一点。 diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index 1fb4a6e7..ab251b38 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -149,6 +149,10 @@ GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 3 * 4, vertexBuffer); GLES30.glEnableVertexAttribArray(0); ``` + +glEnableVertexAttribArray调用后允许顶点着色器读取句柄对应的GPU数据。默认情况下,出于性能考虑,所有顶点着色器的attribute变量都是关闭的,意味着数据在着色器端是不可见的,哪怕数据已经上传到GPU.由glEnableVertexAttribArray启用指定属性,才可在顶点着色器中访问逐顶点的attribute数据。glVertexAttribPointer或VBO只是建立CPU和GPU之间的逻辑连接,从而实现了CPU数据上传至GPU。但是,数据在GPU端是否可见,即着色器能否读取到数据,由是否启用了对应的属性决定,这就是glEnableVertexAttribArray的功能,允许顶点着色器读取GPU数据。 + + glVertexAttribPointer函数的参数非常多,所以我会逐一介绍它们: - 第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用`layout(location = 0)`定义了position顶点属性的位置值(Location)吗?它可以把顶点属性的位置值设置为`0`。因为我们希望把数据传递到这一个顶点属性中,所以这里我们传入`0`。 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 7d5b2fb6..3bfe537b 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -66,6 +66,20 @@ OpenGL ES版本有自己的着色器语言,其中OpenGL ES的版本与GLSL的 - 可直接使用layout制定属性值。 300版本中布局限定符可以声明顶点着色器输入和片段着色器输出的问题,例如layout(location = 2) in vec3 values[4];,如果不限定该属性的位置为2,需要通过glGetAttribuLocation()查询 +片元着色器中,layout限定符通过location值将输出变量和指定编号的绘制缓冲绑定起来。每一个输出变量的索引(引用)值都会对应到一个相应编号的绘制缓冲,而这个输出变量的值将写入相应缓冲。 + +layout限定符的location值是有范围的,其范围为[0, MAX_DRAW_BUFFERS-1]。不同手持设备的范围有可能不同,最基本的范围是[0, 3]。 + +```glsl +// 此输出变量写入到0号绘制缓冲 +layout(location = 0) out vec4 fragColor; +// 此输出变量写入到1号绘制缓冲 +layout(location = 1) out vec4 colors[2]; + +``` +顶点着色器不允许有layout输出限定符。 如果在片元着色器中只有一个输出变量,则不需要用layout修饰符说明其对应绘制缓冲,在这种情况下,默认值为0。 +如果片元着色器中有多个输出变量,则不允许重复使用相同的location值。 + - 舍弃了gl_FragColor和gl_FragData内置属性,变成我们需要自己使用out关键字定义的属性,例如out vec4 fragColor,这就是为什么你从网还是哪个找到的代码换成GLSL ES 300版本后报错的原因。不过保留了gl_Position - GL_OES_EGL_image_external被废弃 @@ -367,7 +381,7 @@ vec4 transformedVector = myMatrix * myVector; // Yeah, it's pretty much the same - 采样器 - 采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 + 采样器是着色语言中不同于C语言的一种特殊的基本数据类型,采样器是专门用来对纹理进行采样工作的,在GLSL中一般来说,一个采样器变量表示一副或者一套纹理贴图。所谓的纹理贴图可以理解为我们看到的物体上的皮肤。 - 结构体 diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 103439cb..f4278a1c 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -148,7 +148,7 @@ Matrix是专门为处理4*4矩阵和4元素向量设计的,其中的方法都 -OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时,只能再列向量前面乘以矩阵。 +OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时,只能在列向量前面乘以矩阵。 而行向量则反之,否则乘法没有意义。 - multiplyMM @@ -175,6 +175,40 @@ OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时, 获取逆矩阵 + +平移、旋转、缩放等基本变换都是通过将表示点坐标的向量与特定的变换矩阵相乘完成的,进行基于矩阵的变换时,三维空间中点的位置需要表示成齐次坐标形式。 + +所谓齐次坐标形式也就是在x、y、z 3个坐标值后面增加第四个量w,未变换时w值一般为1。 + +所谓齐次坐标表示就是用N+1维坐标表示N维坐标。采用齐次坐标是由于很多在N维空间中难以解决的问题在N+1维空间中会变得比较简单。 + + +##### 旋转变换 + +OpenGL ES中,旋转角度的正负可以用右手螺旋定则来确定。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_rotate_youshou.png?raw=true) + +所谓右手螺旋定则是指: + +右手握住旋转轴,使大拇指指向旋转轴的正方向,4指环绕的方向即为旋转的正方向,也就是旋转角度为正值。 + + + +### 基本变换的实质 + +上面说到的3中基本变换的实现方法,给人的感觉是通过矩阵直接实现了对物体的变换。 + +但在OpenGL ES中这并不完全准确,若仅仅这样来理解在非常简单的情况下可能没有问题,但在多个物体的组合变换中可能就会产生问题了。 + +这是因为变换实际上并不是直接针对物体的,而是针对坐标系进行的。 + +OpenGL ES中变换的实现机制可以理解为首先通过矩阵对坐标系进行变换,然后根据传入渲染管线的原始顶点坐标在最终变换结果坐标系中的位置来绘制。 + + + + + ### 相机 顶点着色器可赋予程序员一次操作一个顶点(“按顶点”处理)的能力,片段着色器(稍后会看到)可赋予程序员一次操作一个像素(“按片段”处理)的能力,几何着色器可赋予程序员一次操作一个图元(“按图元”处理)的能力。 @@ -184,6 +218,8 @@ OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时, 为了达成这个目标,我们需要找到一个有利点。 正如我们在现实世界通过眼睛从一点观察一样,我们也必须找到一点并确立观察方向作为我们观察虚拟世界的窗口。这个点叫作视图或视觉空间,或“合成相机”(简称相机)。 + + 观察3D世界需要: - 将相机放入世界的某个位置; @@ -200,6 +236,28 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 给定世界空间中的点PW,我们需要通过变换将它转换成相应相机空间中的点,从而让它看起来好像是从我们期望的相机位置CW看到的样子。 +从日常生活的经验中可以很容易的了解到,随着摄像机位置、姿态的不同,就算是对同一个场景进行拍摄,得到的画面也是迥然不同的。 + +因此摄像机的位置、姿态在OpenGL应用程序的开发中就显得非常重要。 + +摄像机的设置需要给出3个方面的信息,包括摄像机的位置、观察的方向以及up方向, + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_shexiangji_1.png?raw=true) + +- 摄像机的位置很容易理解,用其在3D空间中的坐标来表示。 +- 摄像机观察的方向可以理解为摄像机镜头的指向,用一个观察目标点来表示(通过摄像机位置与观察目标点可以确定一个向量,此向量即代表了摄像机观察的方向)。 +- 摄像机的up方向可以理解为摄像机顶端的指向,用一个向量来表示。 + +通过摄像机拍摄场景与人眼观察现实世界很类似,因此,通过人眼对现实世界观察的切身感受可以理解摄像机的哥哥参数: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_renyan_1.png?raw=true) + +从上图可以看出,摄像机的位置、朝向、up方向可以有很多不同的组合。 +例如: 同样的位置可以有不同的朝向,不同的up方向。 + + + + 当我们设置好相机之后,就可以学习投影矩阵了。我们需要学习的两个重要的投影矩阵:透视投影矩阵和正射投影矩阵。 透视投影通过使用透视概念模仿我们看真实世界的方式,尝试让2D图像看起来像是3D的。物体近大远小,3D空间中有的平行线用透视法画出来就不再平行。我们可以通过使用变换矩阵将平行线变为恰当的不平行线来实现这个效果,这个矩阵叫作透视矩阵或者透视变换。 @@ -214,9 +272,109 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 计算正交投影和透视投影 + + +### 正交投影 + - orthoM - 计算正交投影矩阵 + 计算正交投影矩阵 + +OpenGL ES 3.0中,根据应用程序中提供的投影矩阵,管线会确定一个可视空间区域,称为视景体。视景体是由6个平面确定的,这6个平面分别为:上平面(up)、下平面(down)、左平面(left)、右平面(right)、远平面(far)、近平面(near)。场景中处于视景体内的物体会被投影到近平面上(视景体外面的物体将被裁剪掉),然后再将近平面上投影出的内容映射到屏幕上的视口中。 + + +OpenGL ES 3.0中,根据应用程序中提供的投影矩阵,管线会确定一个可视空间区域,称为视景体。视景体是由6个平面确定的,这6个平面分别为:上平面(up)、下平面(down)、左平面(left)、右平面(right)、远平面(far)、近平面(near)。场景中处于视景体内的物体会被投影到近平面上(视景体外面的物体将被裁剪掉),然后再将近平面上投影出的内容映射到屏幕上的视口中。 + + +对于正交投影而言,视景体及近平面的情况如下图所示: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_zhengjiao_1.png?raw=true) + +视点为摄像机的位置;离视点较近(距离为near)​,垂直于观察方向向量的平面为近平面,离视点较远(距离为far)​,垂直于观察方向向量的平面为远平面。与观察向量平行,从上下左右4个方向约束视景体范围的4个平面分别为上平面、下平面、左平面、右平面,这4个平面与视景体中心轴线的距离分别为top、bottom、left、right。 + +由于正交投影是平行投影的一种,其投影线(物体的顶点与近平面上投影点的连线)是平行的。故其视景体为长方体,投影到近平面上的图形不会产生真实世界中“近大远小”的效果: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_zhengjiao_2.png?raw=true) + +通过Matrix类的orthoM方法完成对正交投影的设置,其基本代码如下: + +```glsl +Matrix.orthoM ( + mProjMatrix, // 生成矩阵元素的float[]类型数据 + 0, // 起始偏移量 + left, right, // near面的left right + bottom, top, // near面的bottom, top + near, far, // near面、far面与视点的距离 +) +``` + + +orthoM方法的功能为根据接收的6个正交投影相关参数产生正交投影矩阵,并将矩阵的元素填充到指定的数组中。 + +参数left, right为近平面左右侧边对应的x坐标,top、bottom为近平面上下侧边对应的y坐标,分别用来确定左平面、右平面、上平面、下平面的位置。 + +参数near、far粉笔嗯为视景体近平面与远平面距视点的距离。 + + +场景中的物体投影到近平面后,最终会映射到显示屏上的视口中。 + +视口也就是显示屏上指定的矩形区域,通过如下代码进行设置: + +```glsl +GLES30.glViewport(x, y, width, height); +``` + +从近平面到视口的映射是由渲染管线自动完成的。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_zhengjiao_sample.png?raw=true) + +上图是由一组距离观察点原来越远的相同尺寸的六角星构成。由于采用的投影方式为正交投影,因此,最终显示在屏幕上的每个六角星尺寸都相同。 + + +### 透视投影 + +现实世界中人眼观察物体时会有“近大远小”的效果。 + +因此,想要开发出更加真实的场景,仅使用正交投影是远远不够的,这时可以采用透视投影。 + +透视投影的投影线是不平行的,他们相交于视点。 + +通过透视投影,可以产生现实世界中“近大远小”的效果,大部分3D游戏采用的都是透视投影。 + +透视投影中,视景体位锥台形区域,如下图: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_toushi_1.png?raw=true) + +视点为摄像机的位置;离视点较近(距离为near),垂直于观察方向向量的平面为近平面;离视点较远(距离为far),垂直于观察方向向量的平面为远平面。近平面左侧距中心的距离为left,右侧为right,上侧为top,下侧为bottom。由观察点与近平面左上、左下、右上、右下4个的点连线与远平面的交点可以确定上、下、左、右4个斜面,这4个斜面及远近平面确定了视景体的范围。 + + + +透视投影的投影线互不平行,都相交于视点。因此,同样尺寸的物体,近处的投影出来大,远处的投影出来小,从而产生了现实世界中“近大远小”的效果 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_toushi_2.png?raw=true) + +通过Matrix类的frustumM方法完成对透视投影的设置: + +```glsl +Matrix.frustumM ( + mProjMatrix, // 要填充矩阵元素的float[]类型数组 + 0, // 填充起始偏移量 + left, right, // near面的left、right + bottom, top, // near面的bottom、top + near, far // near面、far面与视点的距离 +) +``` + +从上述代码中可以看出,frustumM方法的功能是,根据接收的6个透视投影相关参数产生透视投影矩阵,并将矩阵的元素依次填充到指定的数组中。 + + +透视投影: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_toushi_sample.png?raw=true) + + +上图由一组距离观察点越来越远的相同尺寸的六角星构成。由于采用的投影方式为透视投影,因此,最终显示在屏幕上的多个六角星近大远小。 + + - frustumM @@ -251,8 +409,20 @@ OpenGL有一个固定在原点(0,0,0)并朝向z轴负方向的相机,如下图 用来进行旋转变换的。第一个参数是需要变换的矩阵;第二参数是偏移量;第三个参数是旋转角度,这边是以角度制,也就是说是0-360这个范围;第四、五、六个参数分别代表旋转轴向量的x,y,z值。如果x=0,y=0,z = 1 就相当于以z轴为旋转轴进行旋转,其他类似。 - setLookAtm + +```glsl +Matrix.setLookAtm( + mVMatrix, // 存储生成矩阵元素的float[]类型数组 + 0, // 填充起始偏移量 + cx, cy, cz, // 摄像机位置的x、y、z坐标 + tx, ty, tz, // 观察目标点的x、y、z坐标 + upx, upy, upz, // up向量在x、y、z轴上的分量 +) +``` + + 定义相机视图: 从上面代码中可以看出,setLookAtM方法的功能为根据接收的9个摄像机相关参数产生摄像机的观察矩阵,并将矩阵的元素填充到指定的数组中。 - 定义相机视图 +之所以产生矩阵是因为OpenGL ES渲染管线的需要。 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 0b92ed52..6ada2cb3 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -5,8 +5,8 @@ ### 纹理 -现实生活中,纹理(Texture)最通常的作用是装饰 3D 物体,它就像贴纸一样贴在物体表面,丰富了物体的表面和细节。 - +现实生活中,纹理(Texture)最通常的作用是装饰 3D 物体,它就像贴纸一样贴在物体表面,丰富了物体的表面和细节。 + 在 OpenGLES 开发中,纹理除了用于装饰物体表面,还可以用来作为存储数据的容器。 那么在 OpenGLES 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。 @@ -18,6 +18,33 @@ 3D 纹理可以看作 2D 纹理作为切面的一个数组,类似于立方图纹理,使用三维坐标对其进行访问。 + +### 基本原理 + +启用纹理映射功能后,如果想把一副纹理应用到相应的几何图元,就必须告知渲染系统如何进行纹理的映射。 + +告知的方式就是为图元中的顶点指定恰当的纹理坐标,纹理坐标用浮点数来表示,范围一般从0.0到1.0。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gl_texture_0.png?raw=true) + + +- 左侧是一幅纹理图,其位于纹理坐标系中。纹理坐标系原点在左上侧,向右为S轴,向下为T轴,两个轴的取值范围都是0.0~1.0。也就是说不论实际纹理图的尺寸如何,其横向、纵向坐标最大值都是1。若实际图为512像素×256像素,则横边第512个像素对应纹理坐标为1,竖边第256个像素对应纹理坐标为1,其他情况依此类推。 + +- 右侧是一个三角形图元,其3个顶点A、B、C都指定了纹理坐标,3组纹理坐标正好在右侧的纹理图中确定了需要映射的三角形纹理区域。 + +从上述两点可以看出,纹理映射的基本思想就是,首先为图元中的每个顶点指定恰当的纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中纹理区域中的内容根据纹理坐标映射到指定的图元上。 + +从上述两点可以看出,纹理映射的基本思想就是,首先为图元中的每个顶点指定恰当的纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中纹理区域中的内容根据纹理坐标映射到指定的图元上。 + +进行纹理映射的过程实际上就是为右侧三角形图元中的每个片元着色,用于着色的颜色需要从左侧的纹理图中提取,具体过程如下: +- 首先图元中的每个顶点都需要在顶点着色器中通过out变量将纹理坐标传入片元着色器。 + +- 经过顶点着色器后渲染管线的固定功能部分会根据情况进行插值计算,产生对应到每个片元的用于记录纹理坐标的out变量值。 + +- 最后每个片元在片元着色器中根据其接收到的记录纹理坐标的in变量值到纹理图中提取出对应位置的颜色即可,提取颜色的过程一般称为纹理采样。 + + + 在OpenGL中简单理解就是一张图片,在学习之前需要明白这几个概念,不然很容易迷糊,不知道为什么要这样去调用api,到底是什么意思. - 纹理Id:句柄,纹理的直接引用 @@ -571,6 +598,46 @@ void main() { 顶点着色器允许一次操作一个顶点,而片段着色器一次可以操作一个片段(实际上是一个像素),但几何着色器却可以一次操作一个图元。 + + +#### 纹理色彩通道灵活组合 + +纹理色彩通道的灵活组合。通过其可以对输入的RGBA纹理颜色分量如何映射到着色器中对应的颜色分量进行细节控制。实现纹理色彩通道的灵活组合主要是通过glTexParameterf方法进行设置。 + +实际设置时,glTexParameterf方法的第一个参数一般为“GLES30.GL_TEXTURE_2D”,表示对2D纹理进行设置。第二个参数有4种选择,具体含义如下所列: +- GL_TEXTURE_SWIZZLE_R表示映射到着色器中对应采样器的红色通道。 +- GL_TEXTURE_SWIZZLE_G表示映射到着色器中对应采样器的绿色通道。 +- GL_TEXTURE_SWIZZLE_B表示映射到着色器中对应采样器的蓝色通道。 +- GL_TEXTURE_SWIZZLE_A表示映射到着色器中对应采样器的透明度通道。 + +第三个参数则有6种选择,具体含义如下所列: +- GL_RED表示是映射纹理图中的红色通道。 +- GL_GREEN表示是映射纹理图中的绿色通道。 +- GL_BLUE表示是映射纹理图中的蓝色通道。 +- GL_ALPHA表示是映射纹理图中的透明度通道。 +- GL_ONE表示采用1值进行映射。 +- GL_ZERO表示采用0值进行映射。 + + + +#### 2D纹理数组 + +从OpenGL ES 3.0开始支持的纹理类型——2D纹理数组。通过使用2D纹理数组,在同一个着色器中需要使用多个2D纹理的情况下可以简化开发。可以想象出,如果没有2D纹理数组技术,当一个着色器中需要使用多个2D纹理时就需要声明多个采样器变量。 + +而使用2D纹理数组实现,同样功能时只需声明一个sampler2DArray类型的变量即可,方便高效。对于2D纹理数组进行采样时,需要提供的纹理坐标有3个分量,前两个与普通2D纹理坐标含义相同,为S、T分量,第三个分量则为2D纹理数组中的索引。 + +需要注意的是,使用2D纹理数组时,一般会使数组中的纹理保持相同的尺寸。 + + +需要注意的是,glBindTexture方法和glTexParameterf方法的第一个参数由原来的GLES30.GL_TEXTURE_2D变为GLES30.GL_TEXTURE_2D_ARRAY。 + + + + + + + + --- - [上一篇: 8.GLES类及Matrix类](https://github.com/CharonChui/AndroidNote/blob/master/VideoDevelopment/OpenGL/8.GLES%E7%B1%BB%E5%8F%8AMatrix%E7%B1%BB.md) diff --git "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" index 84d9fe63..a0d2f54b 100644 --- "a/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" +++ "b/VideoDevelopment/\351\237\263\351\242\221\347\274\226\347\240\201/\351\237\263\351\242\221\347\274\226\347\240\201\346\240\274\345\274\217.md" @@ -1,5 +1,10 @@ ## 音频编码格式 +多媒体要想发声,就得加个喇叭,但是喇叭是靠模拟信号驱动的,所以在喇叭之前需要增加一个数字信号转模拟信号的“解码器”, +将数字信号转换为模拟信号。 + +其次还需要增加一个信号放大器,将微弱的电流放大成足以驱动喇叭磁铁产生足够磁力从而震动纸盆的足够强的电流。 + 原始的PCM音频数据也是非常大的数据量,因此也需要对其进行压缩编码。 和视频编码一样,音频也有许多的编码格式,如:WAV、MP3、WMA、APE、FLAC等等,音乐发烧友应该对这些格式非常熟悉,特别是后两种无损压缩格式。 From 4458de9a1467924f1bff9287e40677f1d4f5a803 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 19 Feb 2025 16:46:24 +0800 Subject: [PATCH 116/128] udapte video development part --- .../python3\345\205\245\351\227\250.md" | 9 ++ .../CameraX\347\273\223\345\220\210OpenGL.md" | 84 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 "VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/CameraX\347\273\223\345\220\210OpenGL.md" diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 985895a7..3408c295 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -732,4 +732,13 @@ dict_keys(['total_count', 'incomplete_results', 'items']) +在Python中,还可以定义可变参数。可变参数也称不定长参数,即传入函数中的实际参数可以是零个、一个、两个到任意个。 +定义可变参数时,主要有两种形式,一种是*parameter,另一种是**parameter。下面分别进行介绍。 +1.*parameter +这种形式表示接收任意多个实际参数并将其放到一个元组中。 +2.**parameter +这种形式表示接收任意多个类似关键字参数一样显式赋值的实际参数,并将其放到一个字典中。 + + +在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“()”,这样可以让代码更加简洁。 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/CameraX\347\273\223\345\220\210OpenGL.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/CameraX\347\273\223\345\220\210OpenGL.md" new file mode 100644 index 00000000..276f664d --- /dev/null +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/CameraX\347\273\223\345\220\210OpenGL.md" @@ -0,0 +1,84 @@ +CameraX结合OpenGL +=== + + +CameraX 是一个 Jetpack 库,旨在帮助您更轻松地开发相机应用。如果您要开发新应用,我们建议您从 CameraX 开始。它提供了一个一致且易于使用的 API,该 API 适用于绝大多数 Android 设备,并向后兼容 Android 5.0(API 级别 21)。 + + +CameraX 着重于用例,使您可以专注于需要完成的任务,而无需花时间处理不同设备之间的细微差别。CameraX 支持大多数常见的相机用例: + +预览:在屏幕上查看图片。接受用于显示预览的 Surface,例如 PreviewView。 +图片分析:无缝访问缓冲区中的图片以便在算法中使用,例如将其传递到机器学习套件。为分析(例如机器学习)提供 CPU 可访问的缓冲区。 +图片拍摄:保存图片。拍摄并保存照片。 +视频拍摄:保存视频和音频。通过 VideoCapture 拍摄视频和音频 + + + +版本 1.4.0 +2024 年 10 月 30 日 + +发布了 androidx.camera:camera-*:1.4.0。版本 1.4.0 包含这些提交内容。 + +自 1.3.0 以来的重要变更 + +CameraX 1.4.0 包含众多精彩更新!下面是摘要: + +主打功能:10 位 HDR: + +轻松拍摄出令人惊艳的 HDR 照片和视频。 +支持 HLG 和 10 位 HEVC 编码。 +享受 10 位 HDR 预览,并查询设备功能。 +可在越来越多的设备上与 UltraHDR 图片和 HDR 视频搭配使用。 +其他酷炫功能: + +Kotlin 扩展:添加了 takePicture 和 awaitInstance 挂起函数。 +实时特效:应用水印和对象突出显示等特效。 +CameraController API:新增了视频拍摄配置控件。 +预览防抖:查询设备功能并启用防抖。 +增强了 VideoCapture 功能:可更精细地控制画质,并支持更高分辨率。 +CameraX 扩展程序集成:与 VideoCapture 和新的 ImageCapture 功能无缝集成。 +Shutter Sound API:轻松查看各个地区的快门提示音要求。 +屏幕闪光灯:改进了前置摄像头在弱光环境下的拍照效果。 +Camera Extensions Metadata API:支持在 ExtensionMode#AUTO 中调整扩展程序强度和获取当前扩展程序模式通知的 API。如需了解更多 bug 修复,请参阅我们的Beta 版和RC 版公告。 + + + + +同时选择多个摄像头 +从 CameraX 1.3 开始,您还可以同时选择多个摄像头。 例如,您可以对前置和后置摄像头进行绑定,以便从两个视角同时拍摄照片或录制视频。 + +使用并发摄像头功能时,设备可以同时运行两个不同镜头方向的摄像头,或同时运行两个后置摄像头。以下代码块展示了如何在调用 bindToLifecycle 时设置两个摄像头,以及如何从返回的 ConcurrentCamera 对象中获取两个 Camera 对象。 + +Kotlin +Java + +// Build ConcurrentCameraConfig +val primary = ConcurrentCamera.SingleCameraConfig( + primaryCameraSelector, + useCaseGroup, + lifecycleOwner +) + +val secondary = ConcurrentCamera.SingleCameraConfig( + secondaryCameraSelector, + useCaseGroup, + lifecycleOwner +) + +val concurrentCamera = cameraProvider.bindToLifecycle( + listOf(primary, secondary) +) + +val primaryCamera = concurrentCamera.cameras[0] +val secondaryCamera = concurrentCamera.cameras[1] + + + + + + +​ +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file From c9b2e7169d95207379c1b072c2c7ce539f4e348c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Thu, 13 Mar 2025 17:08:41 +0800 Subject: [PATCH 117/128] update --- .../python3\345\205\245\351\227\250.md" | 56 ++++ JavaKnowledge/shell.md | 91 ++++++ ...00\201MediaCodec\343\200\201MediaMuxer.md" | 2 + .../SurfaceView\344\270\216TextureView.md" | 46 ++- .../1.OpenGL\347\256\200\344\273\213.md" | 37 ++- ...\345\222\214GL\347\272\277\347\250\213.md" | 306 ++++++++++++++++++ .../OpenGL/18.\345\205\266\344\273\226.md" | 68 ++++ .../9.OpenGL ES\347\272\271\347\220\206.md" | 2 +- 8 files changed, 597 insertions(+), 11 deletions(-) create mode 100644 JavaKnowledge/shell.md create mode 100644 "VideoDevelopment/OpenGL/17.EGL\345\222\214GL\347\272\277\347\250\213.md" create mode 100644 "VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 3408c295..293961fb 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -454,6 +454,22 @@ make_pizza('pepperoni') make_pizza('mushrooms','green peppers','extra cheese') ``` + +简单来说,Python中所有的函数参数传递,统统都是基于传递对象的引用进行的。这是因为,在Python中,一切皆对象。而传对象,实质上传的是对象的内存地址,而地址即引用。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + +看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 + + ### 将函数存储在模块中 函数的优点之一是,使用它们可将代码块与主程序分离。通过给函数指定描述性名称,可让主程序容易理解得多。你还可以更进一步,将函数存储在被称为模块的独立文件中,再将模块导入到主程序中。import语句允许在当前运行的程序文件中使用模块中的代码。 @@ -673,6 +689,14 @@ title.split() Python有一个pass语句,可在代码块中使用它来让Python什么都不要做。 +```python +def donothing(): + pass #空语句 + +``` + + +pass语句如果啥事都不做,那要它有何用呢?实际上,pass可以作为一个占位符来用——视为未成熟代码的“预留地”。比如说,如果我们还没想好函数的内部实现,就可以先放一个pass,让代码先跑起来。 ### json @@ -733,6 +757,27 @@ dict_keys(['total_count', 'incomplete_results', 'items']) 在Python中,还可以定义可变参数。可变参数也称不定长参数,即传入函数中的实际参数可以是零个、一个、两个到任意个。 + +Python中的可变参数的表现形式为,在形参前添加一个星号(*),意为函数传递过来的实参个数不定,可能为0个、1个,也可能为n个(n≥2)。需要注意的是,不管可变参数有多少个,在函数内部,它们都被“收集”起来并统一存放在以形参名为某个特定标识符的元组之中。因此,可变参数也被称为“收集参数”。 + + +除了用单个星号(*)表示可变参数,其实还有另一种标定可变参数的形式,即用两个星号(**)来标定。通过前文的介绍,我们知道,一个星号(*)将多个参数打包为一个元组,而两个星号(**)的作用是什么呢?它的作用就是把可变参数打包成字典模样。 + +这时调用函数则需要采用如“arg1=value1,arg2=value2”这样的形式。等号左边的参数好比字典中的键(key)[插图],等号右边的数值好比字典中的值(value)​, + +```python +def varFun(**x): + if len(x) == 0: + print("None") + else: + print(x) + + +varFun(a = 1, b = 3) + + +``` + 定义可变参数时,主要有两种形式,一种是*parameter,另一种是**parameter。下面分别进行介绍。 1.*parameter 这种形式表示接收任意多个实际参数并将其放到一个元组中。 @@ -740,5 +785,16 @@ dict_keys(['total_count', 'incomplete_results', 'items']) 这种形式表示接收任意多个类似关键字参数一样显式赋值的实际参数,并将其放到一个字典中。 +除了用等号给可变关键字参数赋值,事实上,我们还可以直接用字典给可变关键字参数赋值: + +```python +def some_kwargs(name, age, sex): + print(f"name: ${name}") + +kdic = {'name' : 'Alice', 'age' : 11, 'sex' : 'nv'} +some_kwargs(**kdic) + +``` + 在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“()”,这样可以让代码更加简洁。 diff --git a/JavaKnowledge/shell.md b/JavaKnowledge/shell.md new file mode 100644 index 00000000..c6bf4a42 --- /dev/null +++ b/JavaKnowledge/shell.md @@ -0,0 +1,91 @@ +shell +--- + + + + +### ls: + +ls x* y*: 过滤文件中x和y开头的文件 +ls -F: 区分文件还是目录 +ls -l: 显示详细信息 + +### touch: 创建或修改文件时间 + + +### cp: 复制文件 + +cp sourceFile destinationFile,但是如果源文件存在会直接被覆盖,也不会提醒, +如果想要提醒需要加上-i, 例如cp -i 1.txt 2.txt + + +### mv移动重新命名 + +用法和cp一致 + + +### rm删除 + +rm -i xxx: -i会询问是否真的要删除 +rm -f xxx: 强制删除,不会询问 + +注意 对于rm命令,-r选项和-R选项的效果是一样的,都可以递归地删除目录中的文件。shell命令很少会对相同的功能使用大小写不同的选项。 +一口气删除目录树的最终解决方案是使用rm -rf命令。该命令不声不响,能够直接删除指定目录及其所有内容。当然,这肯定是一个非常危险的命令,所以务必谨慎使用,并再三检查你要进行的操作是否符合预期。 + + +### mkdir 创建目录 + +mkdir命令的-p选项可以根据需要创建缺失的父目录。父目录是包含目录树中下一级目录的目录。 + +### rmdir 删除空目录 + +### file + +file命令是一个方便的小工具,能够探测文件的内部并判断文件类型: +$ file .bashrc +.bashrc: ASCII text + + +### cat 显示文本文件 + +cat fileName + +-n 加上行号 + +### more +cat的缺点是其开始运行后无法控制后续的操作,为了解决这个问题,有了more命令。 + +more命令会显示文件内容,但会在显示每页数据之后暂停下来。 + +### ps 显示当前用户进程 + +ps -ef显示系统中运行的所有进程 + +### top 实时监测进程 + +ps命令虽然在收集系统中运行进程的信息时非常有用,但也存在不足之处:只能显示某个特定时间点的信息。如果想观察那些被频繁换入和换出内存的进程,ps命令就不太方便了。这正是top命令的用武之地。与ps命令相似,top命令也可以显示进程信息,但采用的是实时方式。 + + + +### kill pid, 通过pid发送信号 + +### pkill pname: pkill命令可以使用程序名代替PID来终止进程。除此之外,pkill命令也允许使用通配符。 + + +### grep数据搜索 + +经常需要在大文件中搜索 + +### gzip压缩 + +gzip: 压缩 +gzcat: 查看压缩过的文本文件内容 +gunzip:解压 + +### tar归档 + +ar命令最开始是用于将文件写入磁带设备以作归档,但它也可以将输出写入文件,这种用法成了在Linux中归档数据的普遍做法。tar命令的格式如下 + +// 该命令创建了一个名为test.tar的归档文件,包含目录test和test2的内容。 +tar -cvf test.tar test/ test2/ + diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" index d2d01f24..204676f3 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/MediaExtractor\343\200\201MediaCodec\343\200\201MediaMuxer.md" @@ -26,6 +26,8 @@ MediaMuxter通常与MediaExtractor一起使用,MediaExtractor用于从媒体 MediaCodec是Android提供的用于对音视频进行编码的类,是Android Media基础框架的一部分,一般和MediaExtractor、MediaMuxer、Surface和AudioTrack一起使用。 +MediaCodec可以处理原始的音视频数据,包括编码(将原始数据转换为压缩格式)和解码(将压缩格式转换为原始数据)。 + MediaExtractor仅仅是解封装数据,不会对数据进行解码。要对媒体数据进行解码,需要使用MediaCodec类。 diff --git "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" index de765b86..23739606 100644 --- "a/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" +++ "b/VideoDevelopment/Android\351\237\263\350\247\206\351\242\221\345\274\200\345\217\221/SurfaceView\344\270\216TextureView.md" @@ -2,13 +2,21 @@ SurfaceView与TextureView === -## SurfaceView +Android系统中图形系统是相当复杂的,包括WindowManager、SurfaceFlinger、OpenGL等。 + +其中SurfaceFlinger(表面合成器或表面渲染器)作为负责绘制应用UI的核心。 + +但是不论使用什么渲染API,所有的东西最终都渲染到Surface上,Android平台所创建的Window都由Surface所支持,所有可见的Surface渲染到显示设备都是通过SurfaceFlinger来完成。 + + + -在说SurfaceView之前,需要先说一下几个相关的部分。 ### `Surface`简介 -- `Surface`就是“表面”的意思,可以简单理解为内存中的一段绘图缓冲区。在`SDK`的文档中,对`Surface`的描述是这样的:“`Handle onto a raw buffer that is being managed by the screen compositor`”, +- `Surface`就是“表面”的意思,可以简单理解为内存中的一段绘图缓冲区。 + + 在`SDK`的文档中,对`Surface`的描述是这样的:“`Handle onto a raw buffer that is being managed by the screen compositor`”, 翻译成中文就是“由屏幕显示内容合成器`(screen compositor)`所管理的原生缓冲器的句柄”, 这句话包括下面两个意思: - 通过`Surface`(因为`Surface`是句柄)就可以获得原生缓冲器以及其中的内容。就像在`C`语言中,可以通过一个文件的句柄,就可以获得文件的内容一样; @@ -19,6 +27,9 @@ SurfaceView与TextureView - `Surface`是一个用来画图形的地方,但是我们知道画图都是在一个`Canvas`对象上面进行的,*`Surface`中的`Canvas`成员,是专门用于提供画图的地方,就像黑板一样,其中的原始缓冲区是用来保存数据的地方, `Surface`本身的作用类似一个句柄,得到了这个句柄就可以得到其中的`Canvas`、原始缓冲区以及其他方面的内容,所以简单的说`Surface`是用来管理数据的(句柄)*。 + +Surface是一个绘制目标,可以被系统的SurfaceFlinger直接合成显示在屏幕上。 + ### `SurfaceView`简介 - 简单的说`SurfaceView`就是一个有`Surface`的`View`里面内嵌了一个专门用于绘制的`Surface`,`SurfaceView`控制这个`Surface`的格式和尺寸以及绘制位置。 @@ -84,16 +95,40 @@ TextureView是一个可以把内容流作为外部纹理输出在上面的View, ### SurfaceTexture -SurfaceTexture是Surface和OpenGL ES(GLES)纹理的组合。SurfaceTexture用于提供输出到GLES 纹理的Surface。SurfaceTexture是从Android 3.0开始加入,与SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此用于图像流数据的二次处理。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。首先,SurfaceTexture从图像流(来自Camera预览、视频解码、GL绘制场景等)中获得帧数据,当调用updateTexImage()时,根据内容流中最近的图像更新SurfaceTexture对应的GL纹理对象。 +SurfaceTexture 是一种 纹理对象,可以作为 OpenGL ES 纹理(OES 纹理)进行处理。它不会直接显示在屏幕上,而是可以将内容传递给 OpenGL 进行进一步处理或绘制。 + +SurfaceTexture用于提供输出到GLES 纹理的Surface。SurfaceTexture是从Android 3.0开始加入,与SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此用于图像流数据的二次处理。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。 + +在Android系统重,SurfaceTexture是一个特殊的类,它将来自硬件纹理缓冲区(如相机预览流或视频解码输出)的图像数据转换为OpenGL ES可以直接使用的纹理。 + +updateTexImage()方法是SurfaceTexture类的核心方法之一,此方法的主要作用是从SurfaceTexture内部特有的图像缓冲区中取出最新一帧, +并将其内容复制到与SurfaceTexture关联的OpenGL纹理上。 + +这对于实时图形渲染、视频播放以及从相机捕获并实时处理图像等场景至关重要。 SurfaceTexture 包含一个应用是其使用方的BufferQueue。当生产方将新的缓冲区排入队列时,onFrameAvailable() 回调会通知应用。然后,应用调用updateTexImage(),这会释放先前占有的缓冲区,从队列中获取新缓冲区并执行EGL调用,从而使GLES可将此缓冲区作为外部纹理使用。 + +##### Surface与SurfaceTexture的关系 + +- SurfaceTexture是一个OpenGL纹理对象,用于GPU处理视频流或相机数据,它不会直接显示在屏幕上,需要OpenGL渲染后才能看到。 +- Surface是一个绘制目标,可以直接用于屏幕渲染,它可以基于SurfaceTexture创建,这样Surface就可以间接获取SurfaceTexture的数据。 + +例如相机使用OpenGL的渲染过程: + +- 在相机预览时,Camera可以将相机数据输出到SurfaceTexture上,然后 +- SurfaceTexture采集相机数据,并将其存储到OpenGL纹理中(OES纹理) +- SurfaceTexture触发onFrameAvailable()回调,通知OpenGL纹理更新。 +- OpenGL使用该纹理进行渲染,最终显示在GLSurfaceView或TextureView上。 + + ## SurfaceView vs TextureView + 简单地说,SurfaceView是一个有自己Surface的View。它的渲染可以放在单独线程而不是主线程中。其缺点是不能做变形和动画。SurfaceTexture可以用作非直接输出的内容流,这样就提供二次处理的机会。与SurfaceView直接输出相比,这样会有若干帧的延迟。同时,由于它本身管理BufferQueue,因此内存消耗也会稍微大一些。TextureView是一个可以把内容流作为外部纹理输出在上面的View。它本身需要是一个硬件加速层。事实上TextureView本身也包含了SurfaceTexture。它与SurfaceView+SurfaceTexture组合相比可以完成类似的功能(即把内容流上的图像转成纹理,然后输出)。区别在于TextureView是在View hierachy中做绘制,因此一般它是在主线程上做的(在Android 5.0引入渲染线程后,它是在渲染线程中做的)。而SurfaceView+SurfaceTexture在单独的Surface上做绘制,可以是用户提供的线程,而不是系统的主线程或是渲染线程。 @@ -123,4 +158,5 @@ SurfaceTexture 包含一个应用是其使用方的BufferQueue。当生产方将 --- - 邮箱 :charon.chui@gmail.com -- Good Luck! \ No newline at end of file +- Good Luck! + diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index e84f18ce..b560e1f5 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -32,9 +32,22 @@ OpenGL ES由OpenGL裁剪而来,因此有必要了解一下两者版本的对 ### OpenGL的作用 -在手机或电脑上有两大元件,一个是CPU,一个是GPU。 +在手机或电脑上有两大元件,一个是CPU,一个是GPU。 + 显示图形界面也有两种方式,一个是使用CPU渲染,一个是使用GPU渲染。 -但是目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 + +CPU和GPU因为涉及之初需求就不一样,所以它们的组成不同,在计算机中的分工也不同。 +GPU有更多的ALU(算术逻辑单元,能实现多组算术运算和逻辑运算的组合逻辑电路),而CPU有Control单元和Cache单元,普通的业务代码因为逻辑复杂,数据类型复杂、跳转复杂、依赖性高等,就更适合CPU来做。 + +而GPU不擅长处理这些复杂的逻辑,它更擅长处理单一的运算,大量的算术逻辑单元可以保证做到真正的高并发,所以大量的计算任务是适合它的工作,就比如GLSL代码。 + +简单说就是CPU更像管理者,负责调度和控制,也进行小量的计算,而GPU则是单纯的打工人,负责大量的计算工作。 + + + + +目前为止最高效的方法就是有效的使用图形处理单元(GPU),图像的处理和渲染就是在将要渲染到窗口上的像素上做很多的浮点运算,而GPU可以并行的做浮点运算,所以用GPU来分担CPU的部分,可以提高效率,可以说GPU渲染其实是一种硬件加速。 + - 图片处理:比如图片色调转换、美颜等。 - 摄像头预览效果处理。比如美颜相机、恶搞相机等。 @@ -96,7 +109,6 @@ OpenGL采用Client-Server模型来解释OpenGL程序,即Server存储OpenGL Con OpenGL ES API没有提及如何创建渲染上下文,或者渲染上下文如何连接到原生窗口系统。 EGL是Khronos渲染API(如OpenGL ES)和原生窗口系统之间的接口。 - ### 对象 OpenGL在开发的时候引入了一些抽象层,对象(Object)就是其中的一个。 @@ -525,7 +537,9 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 **在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。** -光栅化后产生了多少个片元,就会插值计算出多少套in变量。同时,渲染管线就会调用多少次片元着色器。可以看出,一般情况下对一个3D物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。 因此,GPU硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量,通过这些硬件单元的并行执行,提高渲染速度。 +光栅化后产生了多少个片元,就会插值计算出多少套in变量。同时,渲染管线就会调用多少次片元着色器。可以看出,一般情况下对一个3D物体的渲染中,片元着色器执行的次数会大大超过顶点着色器。 + +因此,GPU硬件中配置的片元着色器硬件数量往往多于顶点着色器硬件数量,通过这些硬件单元的并行执行,提高渲染速度。 整个处理过程又分为以下几个部分: @@ -559,6 +573,9 @@ OpenGL中最基础且唯一的多边形就是三角形,所有更复杂的图 ### 帧缓冲区 +像素(pixel)是显示器上最小的可见单元。计算机系统将所有的像素保存到帧缓存(framebuffer)当中,后者是由图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上。 + + OpenGL实际上并不是把图像直接绘制到计算机屏幕上,而是将之渲染到一个帧缓冲区,然后由计算机来负责把帧缓冲区中的内容绘制到屏幕上的一个窗口中。 OpenGL管线的最终渲染目的地被称为帧缓存(framebuffer也被记做FBO) @@ -662,11 +679,21 @@ EGL为双缓冲工作模式(Double Buffer),既有一个Back Frame Buffer和一 6. 设置OpenGL的渲染环境 - eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被称为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行。 + eglMakeCurrent(EGLDisplay display, EGLSurface draw, EGLSurface read, EGLContext context);该方法的参数意义很明确,该方法在异步线程中被调用,该线程也会被称为GL线程,一旦设定后,所有OpenGL渲染相关的操作都必须放在该线程中执行并且指定OpenGL绘制的结果最后输出到这个EGLSurface上。 通过上述操作,就完成了EGL的初始化设置,便可以进行OpenGL的渲染操作。所有EGL命令都是以egl前缀开始,对组成命令名的每个单词使用首字母大写(如eglCreateWindowSurface)。 + +简单总结: + +- EGLDisplay: 代表物理屏幕,可能支持多种配置(EGLConfig) +- EGLContext: 线程相关上下文环境,存储数据如图片、顶点、着色器等,使用的时候只需要拿他们的id取用。 +- EGLSurface: 绘制的模板Surface,用于指定在哪个view上显示OpenGL的绘制结果 + + + #### 创建屏幕外渲染区域: EGL Pbuffer + 除了可以用OpenGL在屏幕上的窗口渲染之外,还可以渲染称作pbuffer(像素缓冲区Pixel buffer的缩写)的不可见屏幕外表面。 和窗口一样,Pbuffer可以利用OpenGL ES3.0中的任何硬件加速。 Pbuffer最常用于生成纹理贴图。如果你想要做的是渲染到一个纹理,那么我们建议使用帧缓冲区对象代替Pbuffer,因为帧缓冲区更高效。不过,在某些帧缓冲区对象无法使用的情况下,Pbuffer仍然有用,例如用OpenGL ES在屏幕外表面上渲染,然后将其作为其他API(如OpenVG)中的纹理。 diff --git "a/VideoDevelopment/OpenGL/17.EGL\345\222\214GL\347\272\277\347\250\213.md" "b/VideoDevelopment/OpenGL/17.EGL\345\222\214GL\347\272\277\347\250\213.md" new file mode 100644 index 00000000..fd4db0e9 --- /dev/null +++ "b/VideoDevelopment/OpenGL/17.EGL\345\222\214GL\347\272\277\347\250\213.md" @@ -0,0 +1,306 @@ +EGL和GL线程 +--- + + +EGL是连接OpenGL ES与本地窗口系统的桥梁。 + +OpenGL是跨平台的,但是不同平台上的窗口系统是不一样的,它就需要一个东西帮助OpenGL与本地窗口系统进行对接、管理及执行GL命令等。 + +OpenGL ES的平台无关性正是借助EGL实现的,EGL屏蔽了不同平台的差异(Apple提供了自己的EGL API的iOS实现,自称EAGL)。 + +EGL可以创建渲染表面EGLSurface,同时提供了图形渲染上下文EGLContext,用来进行状态管理,接下来OpenGL ES就可以在这个渲染表面上绘制。 + + +GL线程是一个与EGL环境绑定了的线程,绑定后可以在这个线程中执行GL操作。 + + +比如将原视频进行OpenGL ES特效渲染然后编码保存,或者是解码原视频然后进行OpenGL ES特效渲染再显示出来。 + +编码时需要将要编码的帧渲染到MediaCodec给你的一块surface上,而这些操作需要有EGL才能做,而解码时是解码到一块你自己指定的surface上,此时你也没有一个现成的EGL环境,如果你想解码出来先用OpenGL ES做些特效处理再显示出来,那么这时也需要EGL环境。 + + +### GLContext + + +在我们的程序运行时,每一个方法的调用都是在CPU上的。 +OpenGL也不例外,与普通调用的区别在于这些调用会被转换为GPU驱动指令在GPU上执行,而CPU和GPU作为两个不同的处理单元,它们之间的指令是并行执行的。 + +OpenGL的设计模式是Client/Server,这里Client是指CPU、Server是指GPU。 + +在执行gl命令时会有一个队列用于暂存还未发送到GPU的命令,实际上我们调用的绝大多数OpenGL方法,只是在往命令队列中插入命令,在CPU中并不一定会等待命令执行完。 + + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_cpu_gpu.png?raw=true) + +但并不是所有的线程都对应同一个命令队列,命令队列是和EGL Context对应的,而一个线程又只能同时绑定到一个EGL Context。 +因此可以理解为命令队列是和绑定的EGL Context的线程对应的。 + +CommandQueue:在CPU和GPU分别有各自的CommanQueue,当一个OpenGL命令在CPU执行时,会先添加到CPU的CommanQueue,在CPU切换到burst mode后,CPU +CommanQueue中的命令被依次发送到GPU CommanQueue,GPU会在合适的时机执行CommanQueue里的命令。 + +GLContext: OpenGL的状态机,保存有渲染管线用到的状态和资源,OpenGL命令的宿主环境。 + +GLContext是和线程强相关的,这是因为绝大部分驱动的设计中,都是一个线程一个CommanQueue,而这个线程中的当前GLContext中发生的命令才会被添加到CommanQueue。 + +绝大多数OpenGL命令在CPU上是异步执行的,在CPU上是同步执行的。 + +这一切得益于CommanQueue的存在,让CPU可以更高效地完成命令发送,毕竟总线的资源是珍贵的,尤其在移动端的总线带宽是非常小的(内存64/128bit,显存128/256bit)。 +但这样的代价是在渲染有问题时,在CPU无法定位到真正的现场。 + + + + + +为了让大家直接感觉一下EGL所起的作用,我们来试试几段代码,一段是我们很熟悉的GLSurfaceView的Renderer的代码,我们可以在回调中做GL操作,比如这里我们创建一个texture: + +```kotlin +glSurfaceView.setRenderer(object : GLSurfaceView.Renderer { + + override fun onDrawFrame(gl: GL10?) { + } + + override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) { + } + + override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) { + + val textures = IntArray(1) + GLES30.glGenTextures(textures.size, textures, 0) + val imageTexture = textures[0] + + } + +}) + +``` +如果你查看这个texture的值,会发现它大于0,也就是创建成功了,另外也可以通过OpenGL ES的方法GLES30.glIsTexture(imageTexture)来判断一个texture是不是合法的texture。 + +如果我们把上述创建texture的操作放到主线程中会怎样? + +```kotlin +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val textures = IntArray(1) + GLES30.glGenTextures(textures.size, textures, 0) + val imageTexture = textures[0] + } +} +``` + +我们会发现,这时创建出来的texture是0,也就是创建失败了,其实不只是创建texture失败,其它GL操作一律会失败。 + +如果放到一个子线程中呢? + +效果是一样的,也会失败,为什么? + +**原因就是在一个没有EGL环境的线程中调用了OpenGL ES API。** + + 那如何让一个线程拥有EGL环境?创建EGL环境,步骤还不少,一共有以下几个步骤: + + +- 获取显示设备 +``` +eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY) +``` + +这里获取的是default的显示设备,大多数情况下我们都是获取default,因为大多数情况下设备只有一个屏幕,本文也只讨论这种情况。 + +- 初始化显示设备 + +``` +val version = IntArray(2) +EGL14.eglInitialize(eglDisplay, version, 0, version, 1) +``` +这里初始化完成后,会返回给我们支持的EGL的最大和最小版本号。 + + +这里初始化完成后,会返回给我们支持的EGL的最大和最小版本号。 + +- 选择config + +``` +val attribList = intArrayOf( + EGL14.EGL_RED_SIZE, + 8, + EGL14.EGL_GREEN_SIZE, + 8, + EGL14.EGL_BLUE_SIZE, + 8, + EGL14.EGL_ALPHA_SIZE, + 8, + EGL14.EGL_RENDERABLE_TYPE, + EGL14.EGL_OPENGL_ES2_BIT or EGLExt.EGL_OPENGL_ES3_BIT_KHR, + EGL14.EGL_NONE +) +val eglConfig = arrayOfNulls(1) +val numConfigs = IntArray(1) +EGL14.eglChooseConfig( + eglDisplay, + attribList, + 0, + eglConfig, + 0, + eglConfig.size, + numConfigs, + 0 +) +``` + +这步操作告诉系统我们期望的EGL配置,然后系统返回给我们一个列表,按配置的匹配程度排序,因为系统不一定有我们期望的配置,因此要通过查询让系统返回尽可能接近的配置。 + +attribList是我们期望的配置,我们这里的配置是将RGBA颜色深度设置为8位,并将OpenGL ES版本设置为2和3,表示同时支持OpenGL 2和OpenGL 3,最后以一个EGL14.EGL_NONE作为结束符。 + +eglConfig是返回的尽可能接近我们期望的配置的列表,通常我们取第0个来使用,即最符合我们期望配置。 + +- 创建EGL Context + +``` +eglContext = EGL14.eglCreateContext( + eglDisplay, + eglConfig[0], + EGL14.EGL_NO_CONTEXT, + intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE), + 0 +) +``` + +eglDisplay即是之前创建的显示设备,注意第三个参数,它是指定一个共享的EGL Context,共享后,2个EGL Context可以相互使用对方创建的texture等资源,默认情况下是不共享的,但不是所有资源都能共享,例如program就是不共享的。 + + +- 创建EGL Surface + +``` +val surfaceAttribs = intArrayOf(EGL14.EGL_NONE) +eglSurface = EGL14.eglCreatePbufferSurface( + eglDisplay, + eglConfig[0], + surfaceAttribs, + 0 +) +``` + +EGL Surface是什么东西?可以理解成是一个用于承载显示内容的东西,这里有2种EGL Surface可以选择,一种是window surface,一种是pbuffer surface。 + +如果我们创建这个EGL环境是为了跟一块Surface绑定,例如希望给Surface View创建一个EGL环境,使用OpenGL ES渲染到Surface View上,那么就要选择window surface,对应的创建方法为: +``` +EGL14.eglCreateWindowSurface( + eglDisplay, + eglConfig[0], + surface, + surfaceAttribs, + 0 +) +``` +其中surface就是这个Surface View对应的surface。如果我们不需要渲染出来看,那么就可以创建一个pbuffer surface,它不和surface绑定,不需要传surface给它,这也称为离屏渲染,本文中将创建pbuffer surface。 + +这里提一个细节,现在的surface所对应的buffer一般都是双buffer,以便于一个buffer正在显示的时候,有另一个buffer可用于渲染, 正在显示的buffer称为front buffer,正在渲染的buffer称为back buffer。 + +如果要渲染到surface上,必须在渲染后调EGL14.eglSwapBuffers(eglDisplay, eglSurface) 将显示buffer和渲染buffer进行交换才会生效,否则会一直渲染到back buffer上,这个buffer无法变成front buffer显示到surface上。 + + + +- 绑定EGL +前面的几个步骤,我们把一些需要创建的东西都创建好了,下面就要将EGL绑定到线程上让它具体有EGL环境: +``` +EGL14.eglMakeCurrent( + eglDisplay, + eglSurface, + eglSurface, + eglContext +) +``` + +注意,一个线程只能绑定一个EGL环境,如果之前绑过其它的,后面又绑了一个,那就会是最后绑的那个。至此,就能让一个线程拥有EGL环境了,此后就可以顺利地做GL操作了。 + +好,我们看一下我们的例子代码: +``` +Thread { + + val egl = EGL() + egl.init() + + egl.bind() + + val textures = IntArray(1) + GLES30.glGenTextures(textures.size, textures, 0) + val imageTexture = textures[0] + assert(GLES30.glIsTexture(imageTexture)) + + egl.release() + +}.start() +``` +代码很简单,只是为了验证是否有了EGL环境,就不写复杂的操作了,EGL就是对前文所述的操作的一个封装类,init()方法对应了获取显示设备、初始化显示设备、选择config、创建EGL Context和创建EGL Surface,bind()方法对应了eglMakeCurrent()。 + + + + +### 共享上下文 + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_share_context.png?raw=true) + +共享上下文时,可以跨线程共享哪些资源? + +可共享的资源: + +- 纹理 +- shader +- program着色器程序 +- buffer类对象,如VBO、EBO、RBO等 + +不可以共享的资源: + +- FBO帧缓冲区对象(不属于buffer类) +- VAO顶点数组对象(不属于buffer类) + + +在不可以共享的资源中,FBO和VAO属于资源管理型对象,FBO负责管理几种缓冲区,本身不占用资源,VAO负责管理VBO或EBO,本身也不占用资源。 + + +注意:每个线程同时只能绑定(eglMakeCurrent)一个Context,但可以按顺序切换不同Context。 + + +### glFinish()、glFlush()、glFence() + +但是在跨线程的共享中也会带来一些同步问题,比如两个线程对同一个纹理进行操作,thread0在纹理上绘制,thread1使用thread0提供的纹理,这时候如果thread0的线程没有渲染完,而thread1就开始使用,就会造成黑屏或者花屏的问题。 + +要解决这个问题有一个简单的方案就是使用glFinish(),glFinish()是一个同步方法,运行时会等待该线程中所有的GL命令(包括所有改变GL状态的命令,所有改变连接状态的命令和所有改变Frame buffer内容的命令)都执行完,从而保证绘制的完整性。 + + +与glFinish()比较相近的还有一个glFlush()方法,它的作用是将命令队列中的命令全部刷到GPU,但并不等它执行完成。 + +在实际使用中,各个厂家的GPU实现差异很大,但都要求符合OpenGL标准,glFlush可以异步实现,也可以同步实现,但glFinish必须同步实现,所以调用glFlush并返回时,只能说是渲染命令都下发了,但不能保证一定完成,而glFinish返回时是保证了命令已经执行完成的。 + +glFinish虽然能保证命令的完整执行,但是会对性能有一定的影响,因为它会使得GPU和CPU之间的并行性丧失。 + +一般,我们提交给驱动的任务被分组,然后被送到硬件上(再缓冲区交换的时候),如果调用glFinish,就强制驱动将命令送到GPU。然后CPU等待直到被传送的命令全部执行完成。这样在GPU工作的整个期间内,CPU没有工作(至少在这个线程上)。而在CPU工作时(通常是在对命令分组),GPU没有工作,因此造成性能上的下降。 + +有些厂商在实现中已经做了一些保证操作完整性的处理(比如高通部分Soc),并不需要使用到glFinish,过度的使用glFinish反而降低了渲染的效率,相比于glFinish在OpenGL 3.0中有了更好的方案glFence。 + + + +GLFence 的原理是在同步过程中创建一个 Fence 同步对象并将 Fence 命令插入 GL 命令队列中,当 Fence 命令满足同步对象的指定条件时,GL 发出解除信号,解除由glWaitSync 或 glClientWaitSync命令造成的阻塞。 + + + +同时,glFenceSync 或相关 Fence 命令的执行不会影响其他 GL 状态。 + + +glFence 中包含两个 Sync 命令 glWaitSync 和 glClientWaitSync,glWaitSync 是在 GPU 上等待,而 glClientWaitSync 是在 CPU 上等待,实际使用中只有 glClientWaitSync 起到了阻止闪屏的作用。 + + + + + + + +如果说OpenGL ES是画笔,那Android的Surface就是画布。我们是可以使用OpenGL ES将图像画到 Surface中去的。但是OpenGL并不能直接在Android的Surface中画画,需要借助 EGLSurface 来将 Android的Surface关联起来。 + + + + + + diff --git "a/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" "b/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" new file mode 100644 index 00000000..72202177 --- /dev/null +++ "b/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" @@ -0,0 +1,68 @@ +18.其他 +--- + + +视觉暂留: 人眼在观察景物时,光信号传入大脑神经,需经过一段短暂的时间,光的作用结束后,视觉形象并不立即消失,这种残留的视觉称“后像”,视觉的这一现象则被称为“视觉暂留”。 + +物体在快速运动时,当人眼所看到的影像消失后,人眼仍能继续保留其影像0.1 ~ 0.4秒左右的图像,所以当图像的刷新帧率高于每秒16帧时,在人眼看来就是不间断的显示。 + + +#### 光栅扫描 + + +光栅扫描显示是目前的主流。 + +在光栅扫描显示中,电子束横向扫描屏幕,一次一行,从顶到底依次进行。 + +电子束在屏幕上逐点移动时,从被称为帧缓存的存储区取出每个像素点的强度值控制其显示状态。 +这种实现原理决定了光栅扫描显示的刷新频率和图像复杂度无关,刷新频率可以是恒定的。 + + +光栅扫描显示对屏幕上的每个点都有存储强度信息的能力(除颜色信息外,其他的像素信息也存在帧缓存中),从而使得适用于包含阴影和彩色模式的逼真场景。 + + +GPU的渲染流程: + +GPU进行渲染时,会把数据先存储在帧缓冲区里,然后视频控制器读取帧缓冲区里的数据,完成数模转化,逐行扫描显示画面。 + +理论上,完美情况时,每扫描一张图,就显示一张,然后下一张也扫描完成等待刷新。 + +一次反复,屏幕就会保持流畅,那为什么有时候屏幕还会出现不流畅的现象? + + +- 撕裂: + +当视频控制器读取完一帧画面显示后,会去帧缓存读取下一帧,如果帧缓存仍然保持着上一帧的数据没有被刷新,视频控制器照常工作读取帧缓存中的数据。 + +当寄存器工作到某一行时,帧缓存的数据被更新为下一帧,视频控制器就读取到新的数据,而此时已经显示的画面是上一帧的数据,后续将要显示的是下一帧的数据,撕裂也就产生了。 + +所以,撕裂的产生是因为帧缓存的刷新频率没有跟上屏幕的刷新频率,根本原因就是CPU或GPU工作超时。 + + +为了解决撕裂的问题,会使用垂直同步Vsync + 双缓冲的机制。 + + + +在VSync信号到来后,会在CPU中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。 + +然后CPU会将计算好的数据提交到GPU,由GPU进行变换、合成、渲染。然后GPU会把渲染结果提交到帧缓存,等待下一次VSync信号到来时显示到屏幕上。 + +由于垂直同步的机制,如果在一个VSync时间内,CPU或者GPU没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时屏幕会刷新显示上一帧的内容。 + + + +通过这种机制,就解决了撕裂的问题,但同时引入了新的问题 -- 掉帧。 + + + +当CPU或GPU渲染流⽔线耗时过⻓,前后显示同一帧时,也就出现了掉帧现象(所以掉帧不是不渲染数据,而是重复渲染同一帧数据)。 + + + +至于掉帧无法被完全解决,因为CPU或GPU的工作是不可预估的。只能尽量合理的使用CPU或GPU减少掉帧次数。 + + + + + + diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index 6ada2cb3..b1d5de88 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -9,7 +9,7 @@ 在 OpenGLES 开发中,纹理除了用于装饰物体表面,还可以用来作为存储数据的容器。 -那么在 OpenGLES 中,纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。 +那么在 OpenGLES 中,***纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。*** 2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 From 428cb4eed395020e4c6828c1e96ab35a0c17dad6 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 14 Mar 2025 17:03:51 +0800 Subject: [PATCH 118/128] update --- .../1.OpenGL\347\256\200\344\273\213.md" | 15 ++++- ...45\231\250\350\257\255\350\250\200GLSL.md" | 7 ++ ...\261\273\345\217\212Matrix\347\261\273.md" | 64 +++++++++++++++++++ .../9.OpenGL ES\347\272\271\347\220\206.md" | 28 +++++++- 4 files changed, 111 insertions(+), 3 deletions(-) diff --git "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" index b560e1f5..ec827301 100644 --- "a/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" +++ "b/VideoDevelopment/OpenGL/1.OpenGL\347\256\200\344\273\213.md" @@ -366,7 +366,20 @@ GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, vbo.get(0)); ``` 从调用glBindBuffer()这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。 然后我们可以调用glBufferData()函数,它会把之前定义的顶点数据复制到缓冲的内存中(小助理把对应的数据单独记录下来了)。 - + + + +绑定对象的过程有点类似设置铁路的道岔开关。 +一旦设置了开关,从这条线路通过的所有列车都会驶向对应的轨道。 +如果我们将开关设置到另一个状态,那么所有之后经过的列车都会驶向另一条轨道。OpenGL的对象也是如此。 + +总体上来说,在两种情况下我们需要绑定一个对象: +- 创建对象并初始化它所对应的数据是。 +- 每次我们准备使用这个对象,而它并不是当前绑定的对象时。 + + + + ```C // glBufferData creates a new data store for the buffer object currently bound to target. // glBufferData为当前绑定到的目标缓冲区对象创建一个新的数据存储 diff --git "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" index 3bfe537b..12bb9706 100644 --- "a/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" +++ "b/VideoDevelopment/OpenGL/7.OpenGL ES\347\235\200\350\211\262\345\231\250\350\257\255\350\250\200GLSL.md" @@ -174,6 +174,13 @@ precision mediump int; #### Uniform +Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同: + +- uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。 + +- 无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。 + + CPU在处理并行线程的时候,每个线程负责给完整图像的一部分配置颜色。 尽管每个线程和其他线程之间不能有数据交换,但我们能从CPU给每个线程输入数据。 因为显卡的架构,所有线程的输入值必须统一(uniform),而且必须设为只读。 diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index f4278a1c..0df4bcff 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -138,6 +138,70 @@ GLES30.glVertexAttribPointer(1, 4, GLES30.GL_FLOAT, false, 0, colorBuffer); 如果启用,那么当GLES30.glDrawArrays或者GLES30.glDrawElements被调用时,顶点属性数组会被使用。 + + + +尽管现在已经知道了如何创建一个物体、着色、加入纹理,给它们一些细节的表现,但因为它们都还是静态的物体,仍不够有趣。 + +我们可以尝试着在每一帧改变物体的顶点并且重配置缓冲区从而使它移动,但这太繁琐了,而且会消耗很多的处理时间。 +我们现在有一个更好的解决方案,使用(多个)矩阵(Matrix)对象可以更好的变换(Transform)一个物体。 + +为了更好的了解矩阵,需要先了解向量. + +## 向量 + +向量有一个方向(Direction)和大小(Magnitude,也叫强度或长度)。 + +你可以把向量想象成一个藏宝图上的指示: 向左走10步,向北走3步,然后向右走5步。 + +这里面左就是方向,10步就是向量的长度。 + +向量可以在任何维度(Dimension)上,但是我们通常只使用2至4维。 +如果一个向量有2个维度,它表示一个平面的方向(想象一下2D的图像)。 +当它有3个维度的时候它可以表达一个3D世界的方向。 + + + +![image](https://github.com/CharonChui/Pictures/blob/master/glsl_vector_0.png?raw=true) + +上图中有3个向量,每个向量在2D图像中都用一个箭头(x, y)表示。 +***由于向量表示的是方向,起始于何处并不会改变它的值。*** +例如上面的向量v和w是相等的,尽管他们的起始点不同。 + + +由于向量是一个方向,所以有些时候会很难形象的将它们用位置(Position)表示出来。 +为了让其更为直观,我们通常设定这个方向的原点为(0, 0, 0),然后指定一个方向,对应一个点,使其变为位置向量(Position Vector)。 +你也可以把起点设置为其他的点,然后说这个向量从这个点起始指向另一个点。 + +比如说位置向量(3, 5)在图像中的起点会是(0, 0),并会指向(3, 5)。我们可以使用向量在2D或3D空间中表示方向与位置。 + + +### 向量相乘 + +两个向量相乘是一种很奇怪的情况。 +普通的乘法在向量上是没有定义的,因为它在视觉上是没有意义的。 + +但是在相乘的时候我们有两种特定情况可以选择: + +- 一个是点乘(Dot Product) +- 一个是叉乘(Cross Product) + + +##### 点乘 + +两个向量的点乘等于它们的数乘结果乘以两个向量之间夹角的余弦值。 + +##### 叉乘 + +叉乘只在3D空间中有定义,它需要两个不平行向量作为输入,生成一个正交于两个输入向量的第三个向量。 +如果输入的两个向量也是正交的,那么叉乘之后将会产生3个相互正交的向量。 +下面的图就是3D控件中叉乘的样子: + +![image](https://github.com/CharonChui/Pictures/blob/master/opengl_chacheng.png?raw=true) + + + + ## Matrix Matrix就是专门设计出来帮助我们简化矩阵和向量运算操作的,里面所有的实现原理都是线性代数中的运算。 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index b1d5de88..d8258366 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -11,7 +11,7 @@ 那么在 OpenGLES 中,***纹理实际上是一个可以被采样的复杂数据集合,是 GPU 的图像数据结构,纹理分为 2D 纹理、 立方图纹理和 3D 纹理。*** -2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。 +***2D 纹理是 OpenGLES 中最常用和最常见的纹理形式,是一个图像数据的二维数组。纹理中的一个单独数据元素称为纹素或纹理像素。*** 立方图纹理是一个由 6 个单独的 2D 纹理面组成的纹理。立方图纹理像素的读取通过使用一个三维坐标(s,t,r)作为纹理坐标。 @@ -129,9 +129,29 @@ OpenGL不能直接加载jpg或者png这类被编码的压缩格式,需要加 ### 纹理过滤 +纹理坐标不依赖于分辨率,它可以是任意浮点值,所以OpenGL需要知道怎样将纹理像素映射到纹理坐标。 + +当你有一个很大的物体但是纹理的分辨率很低的时候这就变得很重要了。 + + 当我们通过光栅化将图形处理成一个个小片段的时候,再将纹理采样,渲染到指定位置上时,通常会遇到纹理元素和小片段并非一一对应。 这时候,会出现纹理的压缩或者放大。那么在这两种情况下,就会有不同的处理方案,这就是纹理过滤了。 + +纹理过滤有很多选项,这里只说最重要的两项: GL_NEAREST和GL_LINEAR。 + +- GL_NEAREST: 临近过滤,是OpenGL默认的纹理过滤方式,会选择中心点最接近纹理坐标的那个像素。 + +- GL_LINEAR: 线性过滤,会基于纹理坐标附近的纹理像素,计算出一个插值,近似出这些纹理像素之间的颜色。一个纹理像素的中心距离纹理坐标越近,那么这个纹理像素的颜色对最终的样本颜色的贡献越大。 + + + +如果在一个很大的物体上应用一张低分辨率的纹理会发生什么呢(纹理被放大了,每个纹理像素都能看到)? + +GL_NEAREST产生了颗粒状的图案,我们能够清晰看到组成纹理的像素。 +而GL_LINEAR能够产生更平滑的图案,很难看出单个的纹理像素。GL_LINEAR可以产生更真实的输出。 + + ### 纹理对象和纹理的加载 纹理应用的第一步是创建一个纹理对象。纹理对象是一个容器对象,保存渲染所需的纹理数据,例如图像数据、过滤模式和包装模式。 @@ -352,7 +372,7 @@ color = texture(samp, tc); 纹理的绘制: ```java -//激活纹理,设置当前活动的纹理单元为单元0 +//激活纹理单元,设置当前活动的纹理单元为单元0 GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //绑定纹理,将纹理id绑定到当前活动的纹理单元上 GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); @@ -365,6 +385,10 @@ GLES20.glUniform1i(aTextureLocation, 0); 通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 +OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活从GL_TEXTURE0到GL_TEXTURE15。 +它们都是按顺序定义的,所以我们也可以通过GL_TEXUTURE0 + 8的方式获得GL_TEXTURE8,这在需要循环一些纹理单元的时候会很有用。 + + ```java public class TextureRender extends BaseGLSurfaceViewRenderer { private final FloatBuffer vertextBuffer; From 0e3f60b52ddf3a40c4e4755d1fbb2f282490731d Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 16 Jun 2025 19:45:42 +0800 Subject: [PATCH 119/128] add algorithm part --- ...11\345\272\217\346\225\260\347\273\204.md" | 85 +++++++ ...\350\267\203\346\270\270\346\210\217II.md" | 64 +++++ "Algorithm/11.H\346\214\207\346\225\260.md" | 231 ++++++++++++++++++ ...17\346\234\272\345\205\203\347\264\240.md" | 105 ++++++++ ...04\347\232\204\344\271\230\347\247\257.md" | 127 ++++++++++ ...4.\345\212\240\346\262\271\347\253\231.md" | 140 +++++++++++ ...06\345\217\221\347\263\226\346\236\234.md" | 174 +++++++++++++ ...6.\346\216\245\351\233\250\346\260\264.md" | 148 +++++++++++ ...73\351\231\244\345\205\203\347\264\240.md" | 118 +++++++++ ...04\351\207\215\345\244\215\351\241\271.md" | 78 ++++++ ...\351\207\215\345\244\215\351\241\271II.md" | 80 ++++++ ...32\346\225\260\345\205\203\347\264\240.md" | 86 +++++++ ...56\350\275\254\346\225\260\347\273\204.md" | 87 +++++++ ...00\344\275\263\346\227\266\346\234\272.md" | 76 ++++++ ...\344\275\263\346\227\266\346\234\272II.md" | 105 ++++++++ ...63\350\267\203\346\270\270\346\210\217.md" | 90 +++++++ .../python3\345\205\245\351\227\250.md" | 10 +- ...15\347\224\250\345\216\237\347\220\206.md" | 15 ++ ...4.\345\256\236\344\276\213\345\214\226.md" | 16 ++ .../OpenGL/18.\345\205\266\344\273\226.md" | 6 + ...70\346\234\272\351\242\204\350\247\210.md" | 43 ++++ ...25\345\210\266\350\247\206\351\242\221.md" | 50 ++++ ...66\344\270\211\350\247\222\345\275\242.md" | 20 +- ...\261\273\345\217\212Matrix\347\261\273.md" | 8 +- .../9.OpenGL ES\347\272\271\347\220\206.md" | 41 ++++ 25 files changed, 1992 insertions(+), 11 deletions(-) create mode 100644 "Algorithm/1.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" create mode 100644 "Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" create mode 100644 "Algorithm/11.H\346\214\207\346\225\260.md" create mode 100644 "Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" create mode 100644 "Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" create mode 100644 "Algorithm/14.\345\212\240\346\262\271\347\253\231.md" create mode 100644 "Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" create mode 100644 "Algorithm/16.\346\216\245\351\233\250\346\260\264.md" create mode 100644 "Algorithm/2.\347\247\273\351\231\244\345\205\203\347\264\240.md" create mode 100644 "Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" create mode 100644 "Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" create mode 100644 "Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" create mode 100644 "Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" create mode 100644 "Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" create mode 100644 "Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" create mode 100644 "Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" create mode 100644 "SourceAnalysis/RecyclerView\345\244\215\347\224\250\345\216\237\347\220\206.md" create mode 100644 "VideoDevelopment/OpenGL/19.\347\233\270\346\234\272\351\242\204\350\247\210.md" create mode 100644 "VideoDevelopment/OpenGL/20.\351\200\232\350\277\207OpenGL\351\242\204\350\247\210\345\271\266\345\275\225\345\210\266\350\247\206\351\242\221.md" diff --git "a/Algorithm/1.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" "b/Algorithm/1.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" new file mode 100644 index 00000000..2dc0dda4 --- /dev/null +++ "b/Algorithm/1.\345\220\210\345\271\266\344\270\244\344\270\252\346\234\211\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,85 @@ +1.合并两个有序数组 +=== + + +### 题目 + +给你两个按`非递减顺序`排列的整数数组`nums1`和`nums2`,另有两个整数`m`和`n`,分别表示`nums1`和`nums2`中的元素数目。 + +请你`合并`nums2到nums1中,使合并后的数组同样按非递减顺序排列。 + +注意:最终,合并后数组不应由函数返回,而是存储在数组nums1中。为了应对这种情况,nums1的初始长度为`m + n`,其中前m个元素表示应合并的元素,后n个元素为0,应忽略。nums2的长度为n。 + + + +示例 1: + +输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 +输出:[1,2,2,3,5,6] +解释:需要合并 [1,2,3] 和 [2,5,6] 。 +合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。 + +示例 2: + +输入:nums1 = [1], m = 1, nums2 = [], n = 0 +输出:[1] +解释:需要合并 [1] 和 [] 。 +合并结果是 [1] 。 + +示例 3: + +输入:nums1 = [0], m = 0, nums2 = [1], n = 1 +输出:[1] +解释:需要合并的数组是 [] 和 [1] 。 +合并结果是 [1] 。 +注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。 + + + +### 思路 + +逆向双指针: +- nums1中是非递减的数组。 +- nums2中也是非递减数组。 +- 所以我们要做的就是把nums1中前m个元素与nums2中的元素进行倒序比较,将大的值倒序放到nums1中的后面。 +- 因为nums1中的数据已经是非递减的了,所以等nums2中的内容都放置完就可以结束。 + +```python +class Solution: + def merge(self, nums1: List[int], m: int, nums2: List[int], n: int) -> None: + index = m + n - 1 + p1 = m - 1 + p2 = n - 1 + while p2 >= 0: + if p1 >= 0 and nums1[p1] >= nums2[p2]: + nums1[index] = nums1[p1] + index += 1 + p1 -= 1 + else: + nums1[index] = nums2[p2] + index += 1 + p2 -= 1 +``` + +```kotlin +class Solution { + fun merge(nums1: IntArray, m: Int, nums2: IntArray, n: Int): Unit { + var index = nums1.lastIndex + var r1 = m - 1 + var r2 = n - 1 + while(r2 >= 0) { + if (r1 >= 0 && nums1[r1] >= nums2[r2]) { + nums1[index --] = nums1[r1--] + } else { + nums1[index --] = nums2[r2 --] + } + } + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" new file mode 100644 index 00000000..cd9ecad4 --- /dev/null +++ "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" @@ -0,0 +1,64 @@ +10.跳跃游戏II +=== + + +### 题目 + +给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 + +每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: + +0 <= j <= nums[i] +i + j < n +返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。 + + + +示例 1: + +输入: nums = [2,3,1,1,4] +输出: 2 +解释: 跳到最后一个位置的最小跳跃数是 2。 + 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 +示例 2: + +输入: nums = [2,3,0,1,4] +输出: 2 + + +### 思路 + + +贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 + +所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数! + +这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖。 + +- 如下图,开始的位置是 2,可跳的范围是橙色的。然后因为 3 可以跳的更远,所以跳到 3 的位置。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jump_2_1.png?raw=true) +- 然后现在的位置就是 3 了,能跳的范围是橙色的,然后因为 4 可以跳的更远,所以下次跳到 4 的位置。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jump_2_2.png?raw=true) + + +```python + +class Solution: + def jump(self, nums: List[int]) -> int: + end = 0 + maxPosition = 0 + steps = 0 + for i in range(len(nums) - 1): + maxPosition = max(maxPosition, i + nums[i]) + if i == end: + steps += 1 + end = maxPosition + + return steps +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/11.H\346\214\207\346\225\260.md" "b/Algorithm/11.H\346\214\207\346\225\260.md" new file mode 100644 index 00000000..5be403e0 --- /dev/null +++ "b/Algorithm/11.H\346\214\207\346\225\260.md" @@ -0,0 +1,231 @@ +11.H指数 +=== + + +### 题目 + +给你一个整数数组 citations ,其中 citations[i] 表示研究者的第 i 篇论文被引用的次数。计算并返回该研究者的 h 指数。 + +根据维基百科上 h 指数的定义:h 代表“高引用次数” ,一名科研人员的 h 指数 是指他(她)至少发表了 h 篇论文,并且 至少 有 h 篇论文被引用次数大于等于 h 。如果 h 有多种可能的值,h 指数 是其中最大的那个。 + + + +示例 1: + +输入:citations = [3,0,6,1,5] +输出:3 +解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 + 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。 +示例 2: + +输入:citations = [1,3,1] +输出:1 + +### 思路 + +翻译: 数组中有h个不小于h的值,求最大的h + + +至少有h篇论文,每一篇至少被引用次数为h,听起来很绕,但是以用例[0, 1, 3, 5, 6]来分析: + +- h指数无非就是5或4或3或2或1或0 + +- 那么如果数组中最小的引用次数都大于等于5,那么其他的不用看了,h就是取最大值5 + +- 如果最小值不满足,那么如果数组中倒数第二小值如果大于等于4,那么一定有4个是大于等于4的 + +- 这就是h指数 + + + + + +##### 方法一:排序 + +h肯定不会超过数组的长度。 + +[0, 1, 3, 5, 6] + +- 从小到大排序。 +- 排序后从头开始遍历,如果最小的值,都大于数组的size,那就是size +- 如果上面不满足,那第二小的值如果大于数组的size - 1,那就是size - 1 +- 所以遍历条件就是 for (int i = 0; i < size; i ++ ) { if (nums[i] >= size - 1)} +- 遍历排序后的数组,如果数组中该位置的值>h,那就h++然后继续遍历下一个 + +```java +public int hIndex(int[] citations) { + Arrays.sort(citations); + for(int i = 0; i < citations.length; i++){ + if( citations[i] >= (citations.length -i)){ + return citations.length -i; + } + } + return 0; +} +``` + + +最终的时间复杂度与排序算法的时间复杂度有关 + +复杂度分析 + +- 时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。即为排序的时间复杂度。 + +- 空间复杂度:O(logn),其中 n 为数组 citations 的长度。即为排序的空间复杂度。 + + +##### 方法二:计数排序 + +根据上述解法我们发现,最终的时间复杂度与排序算法的时间复杂度有关。 + +所以我们可以使用计数排序算法,新建并维护一个数组 counter 用来记录当前引用次数的论文有几篇。它的值可以是0 ~ n,所以数组的长度是n + 1 + +[0, 1, 3, 5, 6] + + +- 我们遍历数组 citations,将引用次数大于 n 的论文都当作引用次数为 n 的论文,然后将每篇论文的引用次数作为下标,将 cnt 中对应的元素值加 1。这样我们就统计出了每个引用次数对应的论文篇数。 + +``` +counter[0] = 1 +counter[1] = 1 +counter[2] = 0 +counter[3] = 1 +// 无值,默认0 +counter[4] = 0 +// 引用次数为5的论文有2篇 +counter[5] = 2 +``` + +- 最后我们可以从后向前遍历数组 counter,因为要找最大的H值,所以这个时候要倒序从n到0遍历遍历,找出从后往前遍历时第一个满足引用次数>i的值,就是最大的h值。 + +- 注意要用累加,因为引用次数为5的2篇,一定也能满足引用次数>=3的条件 + +```java +public class Solution { + public int hIndex(int[] citations) { + int n = citations.length, tot = 0; + int[] counter = new int[n + 1]; + for (int i = 0; i < n; i++) { + if (citations[i] >= n) { + counter[n]++; + } else { + counter[citations[i]]++; + } + } + for (int i = n; i >= 0; i--) { + tot += counter[i]; + if (tot >= i) { + return i; + } + } + return 0; + } +} +``` + +复杂度分析 + +时间复杂度:O(n),其中 n 为数组 citations 的长度。需要遍历数组 citations 一次,以及遍历长度为 n+1 的数组 counter 一次。 + +空间复杂度:O(n),其中 n 为数组 citations 的长度。需要创建长度为 n+1 的数组 counter。 + + +##### 方法三:二分法 + +所谓的 h 指数是指一个具体的数值,该数值为“最大”的满足「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」定义的合法数,重点是“最大”。 + +给定所有论文的引用次数情况为[3,0,6,1,5],可统计满足定义的数值有哪些: + +``` +h=0,含义为「至少发表了 0 篇,且这 0 篇论文至少被引用 0 次」,空集即满足,恒成立; + +h=1,含义为「至少发表了 1 篇,且这 1 篇论文至少被引用 1 次」,可以找到这样的组合,如 [3],成立; + +h=2,含义为「至少发表了 2 篇,且这 2 篇论文至少被引用 2 次」,可以找到这样的组合,如 [3, 6],成立; + +h=3,含义为「至少发表了 3 篇,且这 3 篇论文至少被引用 3 次」,可以找到这样的组合,如 [3, 6, 5],成立; + +h=4,含义为「至少发表了 4 篇,且这 4 篇论文至少被引用 4 次」,找不到这样的组合,不成立; + +h=5,含义为「至少发表了 5 篇,且这 5 篇论文至少被引用 5 次」,找不到这样的组合,不成立; +``` +实际上,当遇到第一个无法满足的数时,更大的数值就没必要找了。 + + +基于此分析,我们发现对于任意的 citations数组(论文总数量为该数组长度 n),都必然对应了一个最大的 h 值,且小于等于该 h 值的情况均满足,大于该 h 值的均不满足。 + +那么,在以最大 h 值为分割点的数轴上具有「二段性」,可通过「二分」求解该分割点(答案)。 + +最后考虑在什么值域范围内进行二分? + +一个合格的二分范围,仅需确保答案在此范围内即可。 + +再回看我们关于 h 的定义「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」 +综上,我们只需要在 [0,n] 范围进行二分即可。 + + +设查找范围的初始左边界 left 为 0,初始右边界 right 为 n。每次在查找范围内取中点 mid,同时扫描整个数组,判断是否至少有 mid 个数大于 mid。如果有,说明要寻找的 h 在搜索区间的右边,反之则在左边。 + +二分的本质:前半部分符合要求、后半部分不符合要求,找出符合要求的最大索引 + + +```java +class Solution { + public int hIndex(int[] cs) { + int n = cs.length; + int l = 0, r = n; + while (l < r) { + // 加1 是为了防止死循环 + int mid = (l + r + 1) >> 1; + if (check(cs, mid)) l = mid; + else r = mid - 1; + } + return r; + } + // 判断是否存在至少mid篇论文的引用次数至少为mid。 + boolean check(int[] cs, int mid) { + int ans = 0; + for (int i : cs) if (i >= mid) ans++; + return ans >= mid; + } +} +``` + + + +###### 上面为什么要加1 + +- 这是因为整数除法的向下取整特性。 +- 在计算mid时,如果使用(l+r)/2,当l和r相邻时(例如l=2, r=3),则mid=(2+3)/2=2(整数除法向下取整)。 +- 如果此时check(mid)为真,那么我们会执行l=mid,即l=2,然后循环继续,再次计算mid=(2+3)/2=2,这样就进入了死循环。 + +为了避免这种死循环,我们使用(l + r + 1) / 2,使得当l和r相邻时,mid会等于r(因为(2+3+1)/2=6/2=3),然后无论走哪个分支,循环都会结束(因为执行l=3后l等于r,或执行r=mid-1=2后l等于r)。 +所以,加1是为了避免在只剩两个数时陷入死循环,因为我们要找的是右边界(最大值)。 + + + +###### 例子: + +- citations = [3,0,6,1,5],用上述二分法: +- 初始化:l=0, r=5 +- mid = (0+5+1)/2 = 3,检查check(3): 统计>=3的个数,为3(3,5,6)-> 满足,所以l=3 +- 接下来:l=3, r=5,mid=(3+5+1)/2=9/2=4(整数除法取整为4),检查check(4):统计>=4的个数,有2篇(5,6)不满足4(因为需要至少4篇) +- 所以r=3循环结束,返回3。 + + + + +复杂度分析 + +时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。需要进行 logn 次二分搜索,每次二分搜索需要遍历数组 citations 一次。 +空间复杂度:O(1),只需要常数个变量来进行二分搜索。 + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" new file mode 100644 index 00000000..469f6e99 --- /dev/null +++ "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" @@ -0,0 +1,105 @@ +12.O(1) 时间插入、删除和获取随机元素 +=== + + +### 题目 + +实现RandomizedSet 类: + +RandomizedSet() 初始化 RandomizedSet 对象 +bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 +bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 +int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 +你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。 + + + +示例: + +输入 +["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] +[[], [1], [2], [2], [], [1], [2], []] +输出 +[null, true, false, true, 2, true, false, 2] + +解释 +RandomizedSet randomizedSet = new RandomizedSet(); +randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。 +randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。 +randomizedSet.insert(2); // 向集合中插入 2 。返回 true 。集合现在包含 [1,2] 。 +randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。 +randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。 +randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。 +randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。 + + +提示: + +- -231 <= val <= 231 - 1 +- 最多调用 insert、remove 和 getRandom 函数 2 * 105 次 +- 在调用 getRandom 方法时,数据结构中 至少存在一个 元素。 + +### 思路 + + +题目要求插入、删除、随机的时间复杂度为 O(1) 。 + +- 变长数组可以在 O(1) 的时间内完成获取随机元素操作,但是由于无法在 O(1) 的时间内判断元素是否存在,因此不能在 O(1) 的时间内完成插入和删除操作。 +- 哈希表可以在 O(1) 的时间内完成插入和删除操作,但是由于无法根据下标定位到特定元素,因此不能在 O(1) 的时间内完成获取随机元素操作。 +- 为了满足插入、删除和获取随机元素操作的时间复杂度都是 O(1),需要将变长数组和哈希表结合,变长数组中存储元素,哈希表中存储每个元素在变长数组中的下标。 + +```java +class RandomizedSet { + List nums; + Map indices; + Random random; + + public RandomizedSet() { + nums = new ArrayList(); + indices = new HashMap(); + random = new Random(); + } + + public boolean insert(int val) { + if (indices.containsKey(val)) { + return false; + } + int index = nums.size(); + nums.add(val); + indices.put(val, index); + return true; + } + + public boolean remove(int val) { + if (!indices.containsKey(val)) { + return false; + } + int index = indices.get(val); + int last = nums.get(nums.size() - 1); + nums.set(index, last); + indices.put(last, index); + nums.remove(nums.size() - 1); + indices.remove(val); + return true; + } + + public int getRandom() { + int randomIndex = random.nextInt(nums.size()); + return nums.get(randomIndex); + } +} +``` + + +复杂度分析: + +- 时间复杂度:初始化和各项操作的时间复杂度都是 O(1)。 + +- 空间复杂度:O(n),其中 n 是集合中的元素个数。存储元素的数组和哈希表需要 O(n) 的空间。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" new file mode 100644 index 00000000..6b555aeb --- /dev/null +++ "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" @@ -0,0 +1,127 @@ +13.除自身以外数组的乘积 +=== + + +### 题目 + +给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 + +题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 + +请 不要使用除法,且在 O(n) 时间复杂度内完成此题。 + + + +示例 1: + +输入: nums = [1,2,3,4] +输出: [24,12,8,6] +示例 2: + +输入: nums = [-1,1,0,-3,3] +输出: [0,0,9,0,0] + + +提示: + +- 2 <= nums.length <= 105 +- -30 <= nums[i] <= 30 +- 输入 保证 数组 answer[i] 在 32 位 整数范围内 + +### 思路 + + +先计算数组中所有元素的乘积,然后将总的乘积除以数组的中每个元素x就是除自身值以外数组的乘积。 + +但是这样有个问题,如果数组中有一个元素是0,那这个方法就失效了,而且题目中说了不能使用除法运算。 + +我们可以分解为两部分: + +- 得到索引左侧所有数字的乘积L +- 得到索引右侧所有数字的乘积R +- 两部分相乘 + + +```java + +class Solution { + public int[] productExceptSelf(int[] nums) { + int length = nums.length; + + // L 和 R 分别表示左右两侧的乘积列表 + int[] L = new int[length]; + int[] R = new int[length]; + + int[] answer = new int[length]; + + // L[i] 为索引 i 左侧所有元素的乘积 + // 对于索引为 '0' 的元素,因为左侧没有元素,所以 L[0] = 1 + L[0] = 1; + for (int i = 1; i < length; i++) { + L[i] = nums[i - 1] * L[i - 1]; + } + + // R[i] 为索引 i 右侧所有元素的乘积 + // 对于索引为 'length-1' 的元素,因为右侧没有元素,所以 R[length-1] = 1 + R[length - 1] = 1; + for (int i = length - 2; i >= 0; i--) { + R[i] = nums[i + 1] * R[i + 1]; + } + + // 对于索引 i,除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积 + for (int i = 0; i < length; i++) { + answer[i] = L[i] * R[i]; + } + + return answer; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(N),其中 N 指的是数组 nums 的大小。预处理 L 和 R 数组以及最后的遍历计算都是 O(N) 的时间复杂度。 +- 空间复杂度:O(N),其中 N 指的是数组 nums 的大小。使用了 L 和 R 数组去构造答案,L 和 R 数组的长度为数组 nums 的大小。 + + +尽管上面的方法已经能够很好的解决这个问题,但是空间复杂度并不为常数。 + +- 由于输出数组不算在空间复杂度内,那么我们可以将 L 或 R 数组用输出数组来计算。也就是不再新申请L和R数组,而只是用一个变量记录索引右侧元素的乘积之和。 +- 第一遍遍历的时候将answer数组的值都填充为索引左侧元素的值(也就是上面方法中L的值) +- 再一次后续遍历的时候取answer中的值和数组索引右侧元素乘积的和值相乘并赋值到answer中。这个时候answer中的值就已经是乘积的和值了。 + + +```java +class Solution { + public int[] productExceptSelf(int[] nums) { + int n = nums.length; + int[] ans = new int[n]; + //初始化ans都为1 + for (int i = 0; i < n; i++) { + ans[i] = 1; + } + //左侧 + int L = 1; + for(int i = 0; i < n; i++){ + ans[i] *= L; + L *= nums[i]; + } + //右侧 + int R = 1; + for(int i = n - 1; i >= 0; i--){ + ans[i] *= R; + R *= nums[i]; + } + return ans; + } +} +``` + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" new file mode 100644 index 00000000..b39a9edd --- /dev/null +++ "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" @@ -0,0 +1,140 @@ +14.加油站 +=== + + +### 题目 + +在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 + +你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 + +给定两个整数数组 gas 和 cost ,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。 + + + +示例 1: + +输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] +输出: 3 +解释: +从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 +开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 +开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 +开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 +开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 +开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 +因此,3 可为起始索引。 +示例 2: + +输入: gas = [2,3,4], cost = [3,4,3] +输出: -1 +解释: +你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 +我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 +开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 +开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 +你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 +因此,无论怎样,你都不可能绕环路行驶一周。 + + +提示: + +n == gas.length == cost.length +1 <= n <= 105 +0 <= gas[i], cost[i] <= 104 +输入保证答案唯一。 + +### 思路 + +- 题目有一点很重要: 如果存在解,则 保证 它是 唯一 的。 +- 能跑一圈的前提有两个: + - 每一站的油量都能达到下一站 + - 一圈下来剩下的油量是大于等于0的 + +- 我们首先检查第 0 个加油站,并试图判断能否环绕一周;如果不能,就从第一个无法到达的加油站开始继续检查。 + + +```python +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + n = len(gas) + start = 0 + while start < n: + cur = 0 + step = 0 + while step < n: + nex = (start + step) % n + cur += gas[nex] - cost[nex] + # 走到这一步的时候当前的油量< 0了。需要更新起始的站,重新从下一站开始来计算了 + if cur < 0: + break + # 走下一步 + step += 1 + # 走了n步,也就是一圈了 + if step == n: + return start + else: + # 没走了,一圈,从当前已经走的步数继续遍历找 + start = start + step + 1 + return -1 +``` + + +##### 注意: 为什么使用 start += step + 1 而不是 start += 1? + +​如果从起点 start 出发,在走了 step 步后失败(无法到达第 step+1 个加油站),那么从 start 到 start+step 之间的任意一个加油站作为新起点都一定会失败。 +对于任意k在[start + 1, start + step]: + +- 如果从 k 出发,我们失去了 start → k 这段路的油量积累(由问题性质决定)。 +- 由于 start → k 这段的油量积累 ​必然是非负的​(因为如果这段是负的,那在 k 之前就已经失败了)。 +- 因此,当从 k 开始时,油量比从 start 开始更少,不可能完成接下来的路程。 + +使用 start += step + 1 是基于数学性质的优化,避免了无效的重复检查。 +改为 start += 1 虽然逻辑正确,但会退化为 O(n²) 的暴力解法,在大型数据集上效率极低。 + + + +复杂度分析: + +- 时间复杂度:O(N),其中 N 为数组的长度。我们对数组进行了单次遍历。 + +- 空间复杂度:O(1)。 + + +##### 方法二 + + + +允许油量为负,但是总剩余油量应该大于等于0,否则不存在解的。 + +存在解的情况下,利用贪心法的思想,找到最低点,它的下一个点出发的话,可以保证前期得到剩余油量最大,所以可以跑完全程。 + +```java +public int canCompleteCircuit2(int[] gas, int[] cost) { + // 一圈下来的总剩余油量 + int totalNum = 0; + // 从某一站开始时每一站剩余的油量 + int curNum = 0; + int idx = 0; + + for (int i = 0; i < gas.length; i++) { + curNum += gas[i] - cost[i]; + totalNum += gas[i] - cost[i]; + if (curNum < 0) { + idx = (i+1) % gas.length; + curNum = 0; + } + + } + + if(totalNum < 0) return -1; + return idx; +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" new file mode 100644 index 00000000..66db9e01 --- /dev/null +++ "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" @@ -0,0 +1,174 @@ +15.分发糖果 +=== + + +### 题目 + +n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 + +你需要按照以下要求,给这些孩子分发糖果: + +每个孩子至少分配到 1 个糖果。 +相邻两个孩子评分更高的孩子会获得更多的糖果。 +请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。 + + + +示例 1: + +- 输入:ratings = [1,0,2] +- 输出:5 +- 解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。 + +示例 2: + +- 输入:ratings = [1,2,2] +- 输出:4 +- 解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。 + 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。 + + +提示: + +- n == ratings.length +- 1 <= n <= 2 * 104 +- 0 <= ratings[i] <= 2 * 104 + +### 思路 + +注意题目要求的是最少的糖果数目,且每个孩子至少分配1个糖果。 + +我们默认把所有小孩的糖果都是1。 + +那么本题我采用了两次贪心的策略: + + +- 第一次从左到右遍历,如果当前孩子评分高于左边孩子,那么当前孩子比左边多一个糖果;否则不处理,只保留一个糖果。 + - 左规则:当`ratings[i−1]ratings[i+1]`时,i号学生的糖果数量将比i+1号孩子的糖果数量多。 + + +我们遍历该数组两次,处理出每一个学生分别满足左规则或右规则时,最少需要被分得的糖果数量。每个人最终分得的糖果数量即为这两个数量的最大值。 + +在实际代码中,我们先计算出左规则left数组,在计算右规则的时候只需要用单个变量记录当前位置的右规则,同时计算答案即可。 + +```java +class Solution { + public int candy(int[] ratings) { + int n = ratings.length; + int[] left = new int[n]; + for (int i = 0; i < n; i++) { + if (i > 0 && ratings[i] > ratings[i - 1]) { + left[i] = left[i - 1] + 1; + } else { + left[i] = 1; + } + } + int right = 0, ret = 0; + for (int i = n - 1; i >= 0; i--) { + if (i < n - 1 && ratings[i] > ratings[i + 1]) { + right++; + } else { + right = 1; + } + ret += Math.max(left[i], right); + } + return ret; + } +} +``` + +```python +class Solution: + def candy(self, ratings: List[int]) -> int: + n = len(ratings) + candies = [1] * n # 每个孩子至少一个糖果 + + # 从左到右:右边评分高,则右边糖果 = 左边糖果 + 1 + for i in range(1, n): + if ratings[i] > ratings[i - 1]: + candies[i] = candies[i - 1] + 1 + + # 从右到左:左边评分高,则左边糖果 = max(当前左边糖果, 右边糖果 + 1) + for i in range(n - 2, -1, -1): + if ratings[i] > ratings[i + 1]: + candies[i] = max(candies[i], candies[i + 1] + 1) + + return sum(candies) # 返回总糖果数 +``` + + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是孩子的数量。我们需要遍历两次数组以分别计算满足左规则或右规则的最少糖果数量。 + +- 空间复杂度:O(n),其中 n 是孩子的数量。我们需要保存所有的左规则对应的糖果数量。 + +##### 方法二: 一次遍历 + + + +在上面的方法中,我们只考虑了递增段的处理,没有考虑如何处理递减段,所以才必须使用两次遍历,那该如何将递减段的处理一同加入呢? + +分析一下,如果当前孩子的ratings[i]比左侧孩子的ratings[i-1]更小,到底该分配多大的值?这取决于这个递减段的长度。 + +- 显然,如果i是最右侧的孩子,他可以只分配1颗糖果; +- 如果i是倒数第二个孩子,右侧还有一个递减,那么他最少分配2颗糖果。 + +核心思路:遍历数组时,跟踪当前是上升趋势、下降趋势,还是平;对于连续下降段,不立即分糖果,而是等下降结束后一次性累加。 +观察下降的规律,因为不处理的话就是默认分配1。如果下降长度为k,则需要补上1+2+....+k,即k * (k + 1) / 2颗糖果。 + +举个例子,假设ratings=[1,3,4,1],从左往右处理时,得到cadies=[1,2,3]。我们发现,下降段是[4,1],长度为1,所以补上1颗糖果,将数组变为 +candies=[1,2,3,1]。 + +特别的,如果下降长度 >= 上升长度,需要为“山峰”孩子多加一个糖果。 + +举个例子,假设ratings=[1,2,4,3,2,1],从左往右处理时,得到cadies=[1,2,3]。 +我们发现,下降段的长度为3,比上升段还长。 + +如果仅按照之前的处理,得到candies=[1,2,3,3,2,1]。实际上,第一个3应该变为4,也即山峰更高才对,正确的结果是candies=[1,2,4,3,2,1]。 + +代码如下: + + +```java +class Solution { + public int candy(int[] ratings) { + int n = ratings.length; + int total = 1; // 第一个孩子至少一个糖果 + int up = 0; // 上升序列长度 + int down = 0; // 下降序列长度 + int peak = 0; // 当前上升段的长度峰值 + + for (int i = 1; i < n; i++) { + if (ratings[i] > ratings[i - 1]) { // 上升 + up++; + peak = up; + down = 0; + total += 1 + up; // 当前孩子比前一个多一颗糖果 + } else if (ratings[i] == ratings[i - 1]) { // 平 + up = down = peak = 0; + total += 1; // 持平,默认为1 + } else { // 下降 + up = 0; + down++; + // 如果下降长度超过了之前的上升峰值,需要额外补偿 + total += 1 + down - (peak >= down ? 1 : 0); + } + } + + return total; + } +} +``` + +- 时间复杂度: O(n),其中 n为孩子总数 +- 空间复杂度: O(1),仅使用常数个额外变量 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" new file mode 100644 index 00000000..853cadbd --- /dev/null +++ "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" @@ -0,0 +1,148 @@ +16.接雨水 +=== + + +### 题目 + +给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 + +示例 1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rainwatertrap_1.png?raw=true) + +输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +输出:6 +解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 +示例 2: + +输入:height = [4,2,0,3,2,5] +输出:9 + + +### 思路 + +##### 方法一:动态规划 + +- 对于下标i,下雨后水能到达的***最大高度***等于下标i两边的最大高度的最小值, +- 下标i处能接的雨水量等于下标i处的水能到达的***最大高度***减去height[i]。 +- 朴素的做法是对于数组height中的每个元素,分别向左和向右扫描并记录左边和右边的最大高度,然后计算每个下标位置能接的雨水量。 + + +上述做法的时间复杂度较高是因为需要对每个下标位置都向两边扫描。如果已经知道每个位置两边的最大高度,则可以在 O(n) 的时间内得到能接的雨水总量。使用动态规划的方法,可以在 O(n) 的时间内预处理得到每个位置两边的最大高度。 + +- 创建两个长度为n的数组leftMax(存储每个位置左侧的最高柱子高度)和rightMax(存储每个位置右侧的最高柱子高度)。 +- 对于leftMax[i]表示下标i及其左边的位置中,柱子的最大高度 +- rightMax[i]表示下标i及其右边的位置中,柱子的最大高度。 +- 显然,leftMax[0]=height[0],rightMax[n−1]=height[n−1]。 +- 两个数组的其余元素的计算如下: + - 从左到右遍历,保障每次leftMax[i-1]是截止当前左边最大的。当1≤i≤n−1时,leftMax[i]=max(leftMax[i−1],height[i]); + - 从右到左遍历,保障每次rightMax[i+1]是截止当前右边最大的。当0≤i≤n−2时,rightMax[i]=max(rightMax[i+1],height[i])。 + +- 在得到数组leftMax和rightMax的每个元素值之后,对于下标i处能接的雨水量等于`min(leftMax[i],rightMax[i])−height[i]`。遍历累加每个下标位置即可得到能接的雨水总量。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rainbow_2.png?raw=true) + + +```java +class Solution { + public int trap(int[] height) { + int n = height.length; + if (n == 0) { + return 0; + } + + int[] leftMax = new int[n]; + leftMax[0] = height[0]; + for (int i = 1; i < n; i++) { + leftMax[i] = Math.max(leftMax[i - 1], height[i]); + } + + int[] rightMax = new int[n]; + rightMax[n - 1] = height[n - 1]; + for (int i = n - 2; i >= 0; i--) { + rightMax[i] = Math.max(rightMax[i + 1], height[i]); + } + + int ans = 0; + for (int i = 0; i < n; i++) { + // 左右最小边界决定能存水的高度 + int minBound = Math.min(leftMax[i], rightMax[i]); + // 当前柱子能存的水高度 = 最小边界 - 当前高度 + ans += minBound - height[i]; + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中n是数组height的长度。计算数组leftMax和rightMax的元素值各需要遍历数组height一次,计算能接的雨水总量还需要遍历一次。 +- 空间复杂度:O(n),其中n是数组height的长度。需要创建两个长度为n的数组leftMax和rightMax。 + + + +##### 方法二 + +动态规划的做法中,需要维护两个数组leftMax和rightMax,因此空间复杂度是O(n)。是否可以将空间复杂度降到O(1)? + +注意到下标i处能接的雨水量由leftMax[i]和rightMax[i]中的最小值决定。 + +- 由于数组leftMax是从左往右计算,数组rightMax是从右往左计算,因此可以使用双指针和两个变量代替两个数组。 + +- 维护两个指针left和right,以及两个变量leftMax(左侧已扫描的最大高度)和rightMax(右侧已扫描的最大高度),初始时left=0,right=n−1,leftMax=0,rightMax=0。 +- 指针left只会向右移动,指针right只会向左移动,在移动指针的过程中维护两个变量leftMax和rightMax的值。 + +当两个指针没有相遇时,进行如下操作: + +- 使用height[left]和height[right]的值更新leftMax和rightMax的值 + +- 如果`height[left] int: + k = 0 + for num in nums: + if num != val: + nums[k] = num + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeElement(nums: IntArray, `val`: Int): Int { + var k = 0 + for (num in nums) { + if (num != `val`) { + nums[k] = num + k++ + } + } + return k + } +} +``` + +##### 双指针优化 + +上面的方案存在一个问题,就是例如数组为[1, 2, 3, 4, 5],而val为1时。我们需要把每一个元素都左移一位。 + +注意到题目上说:元素的顺序可以改变。 + +实际上我们只需要将最后一个元素5移动到序列开头,取代元素1,得到序列[5, 2, 3, 4]就可以。 + + +思路: +- 还是用双指针,一个从前往后left,一个从后往前right +- 从前往后的指针left的判断方式还是如同上面的思路 +- 如果left上的值等于val,那就把left位置的值换成right位置的值,同时移动right继续循环 +- 直到left > right相等,那就都遍历完了 + +```python +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + left = 0 + right = len(nums) - 1 + while left <= right: + if (nums[left] == val): + nums[left] = nums[right] + right -= 1 + else: + left += 1 + return left +``` + +```Kotlin +class Solution { + fun removeElement(nums: IntArray, `val`: Int): Int { + var left = 0 + var right = nums.size - 1 + while (left <= right) { + if (nums[left] == `val`) { + nums[left] = nums[right] + right -- + } else { + left ++ + } + } + + return left + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" "b/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" new file mode 100644 index 00000000..8465bf39 --- /dev/null +++ "b/Algorithm/3.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271.md" @@ -0,0 +1,78 @@ +3.删除有序数组中的重复项 +=== + + +### 题目 + +给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。 + +考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过: + +更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。 +返回 k 。 + + + +示例 1: + +输入:nums = [1,1,2] +输出:2, nums = [1,2,_] +解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。 +示例 2: + +输入:nums = [0,0,1,1,1,2,2,3,3,4] +输出:5, nums = [0,1,2,3,4] +解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。 + + + +### 思路 + +双指针 +- 既然非严格递增队列,那么重复的元素一定会相邻,那我们可以从0开始遍历,将没有重复的数值替换放到数组的开头 + + +```python +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + # 第一个位置的不用判断 + k = 1 + for i in range(len(nums)): + # 当前的数值与已放置的数值不同就重新放置 + if nums[i] != nums[k - 1]: + nums[k] = nums[i] + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeDuplicates(nums: IntArray): Int { + var k = 1 + for(i in nums){ + if (i != nums[k - 1]) { + nums[k] = i + k ++ + } + } + return k + } +} +``` + +优化: + +假如数组是[0, 1, 2, 3, 4, 5] +此时数组中没有重复元素,按照上面的方法,每次比较时nums[i]都不等于nums[k - 1],因此就会将k指向的元素原地复制一遍,这个操作其实是不必要的。 + +因此我们可以添加一个小判断,当 i - k > 1 时,才进行复制。 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" "b/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" new file mode 100644 index 00000000..549b9a20 --- /dev/null +++ "b/Algorithm/4.\345\210\240\351\231\244\346\234\211\345\272\217\346\225\260\347\273\204\344\270\255\347\232\204\351\207\215\345\244\215\351\241\271II.md" @@ -0,0 +1,80 @@ +4.删除有序数组中的重复项II +=== + + +### 题目 + +给你一个有序数组 nums ,请你 原地 删除重复出现的元素,使得出现次数超过两次的元素只出现两次 ,返回删除后数组的新长度。 + +不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 + + + +示例 1: + +输入:nums = [1,1,1,2,2,3] +输出:5, nums = [1,1,2,2,3] +解释:函数应返回新长度 length = 5, 并且原数组的前五个元素被修改为 1, 1, 2, 2, 3。 不需要考虑数组中超出新长度后面的元素。 +示例 2: + +输入:nums = [0,0,1,1,1,1,2,3,3] +输出:7, nums = [0,0,1,1,2,3,3] +解释:函数应返回新长度 length = 7, 并且原数组的前七个元素被修改为 0, 0, 1, 1, 2, 3, 3。不需要考虑数组中超出新长度后面的元素。 + + + +### 思路 + +双指针 + +- 因为给定数组是有序的,所以相同元素必然连续。 +- 我们可以使用双指针解决本题,遍历数组检查每一个元素是否应该被保留,如果应该被保留,就将其移动到指定位置。具体地,我们定义两个指针 slow 和 fast 分别为慢指针和快指针,其中慢指针表示处理出的数组的长度,快指针表示已经检查过的数组的长度,即 nums[fast] 表示待检查的第一个元素,nums[slow−1] 为上一个应该被保留的元素所移动到的指定位置。 + +- 因为本题要求相同元素最多出现两次而非一次,所以我们需要检查上上个应该被保留的元素 nums[slow−2] 是否和当前待检查元素 nums[fast] 相同。 + +- 当且仅当 nums[slow−2]=nums[fast] 时,当前待检查元素 nums[fast] 不应该被保留(因为此时必然有 nums[slow−2]=nums[slow−1]=nums[fast])。最后,slow 即为处理好的数组的长度。 + +特别地,数组的前两个数必然可以被保留,因此对于长度不超过 2 的数组,我们无需进行任何处理,对于长度超过 2 的数组,我们直接将双指针的初始值设为 2 即可。 + + +```python + +class Solution: + def removeDuplicates(self, nums: List[int]) -> int: + if len(nums) < 2: + return len(nums) + k = 2 + for i in range(2, len(nums)): + if nums[i] == nums[k - 2] : + continue + else: + nums[k] = nums[i] + k += 1 + return k +``` + + +```kotlin +class Solution { + fun removeDuplicates(nums: IntArray): Int { + if (nums.size < 2) { + return nums.size + } + var k = 2 + for (i in 2..nums.size - 1) { + if (nums[i] != nums[k - 2]) { + nums[k] = nums[i] + k ++ + } + } + return k + } +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" new file mode 100644 index 00000000..71e6953d --- /dev/null +++ "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" @@ -0,0 +1,86 @@ +5.多数元素 +=== + + +### 题目 + +给定一个大小为 n 的数组 nums ,返回其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。 + +你可以假设数组是非空的,并且给定的数组总是存在多数元素。 + + +数组中出现次数超过一半的数字被称为众数。 + +示例 1: + +输入:nums = [3,2,3] +输出:3 +示例 2: + +输入:nums = [2,2,1,1,1,2,2] +输出:2 + + + +### 思路 + +##### 哈希表 + +遍历数组nums,用HashMap统计各数字的数量,即可找出众数。 +此方法时间和空间复杂度均为O(N)。 + + +##### 排序法 + +将数组nums排序,数组中心点的元素,一定是众数 + +##### 摩尔投票法 + +核心理念是票数正负抵消。此方法的时间和空间复杂度分别为O(N)和O(1)。 +为本题最佳解法。 + +若记 众数 的票数为 +1 ,非众数 的票数为 −1 ,则一定有所有数字的 票数和 >0 。 + +```python + +class Solution: + def majorityElement(self, nums: List[int]) -> int: + votes = 0 + for num in nums: + if votes == 0: + x = num + if num == x: + votes += 1 + else: + votes -= 1 + return x +``` + + +```kotlin +class Solution { + fun majorityElement(nums: IntArray): Int { + var vote = 0 + var k = 0 + for (num in nums) { + if (vote == 0) { + k = num + } + if (num == k) { + vote ++ + } else { + vote -- + } + } + return k + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" new file mode 100644 index 00000000..eb2940c2 --- /dev/null +++ "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" @@ -0,0 +1,87 @@ +6.轮转数组 +=== + + +### 题目 + +给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 + + +示例 1: + +输入: nums = [1,2,3,4,5,6,7], k = 3 +输出: [5,6,7,1,2,3,4] +解释: +向右轮转 1 步: [7,1,2,3,4,5,6] +向右轮转 2 步: [6,7,1,2,3,4,5] +向右轮转 3 步: [5,6,7,1,2,3,4] +示例 2: + +输入:nums = [-1,-100,3,99], k = 2 +输出:[3,99,-1,-100] +解释: +向右轮转 1 步: [99,-1,-100,3] +向右轮转 2 步: [3,99,-1,-100] + + + +### 思路 + + +##### 双层循环 + +外层循环k此,不断把最后一位移到第一位,然后内层循环每个元素后移一位 + +```kotlin +class Solution { + fun rotate(nums: IntArray, k: Int): Unit { + for (i in 0.. None: + def reverse(i: int, j: int) -> None: + while i < j: + nums[i], nums[j] = nums[j], nums[i] + i += 1 + j -= 1 + + n = len(nums) + k %= n # 轮转 k 次等于轮转 k % n 次 + reverse(0, n - 1) + reverse(0, k - 1) + reverse(k, n - 1) +``` + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" "b/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" new file mode 100644 index 00000000..5e4f9722 --- /dev/null +++ "b/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" @@ -0,0 +1,76 @@ +7.买卖股票的最佳时机 +=== + + +### 题目 + +给定一个数组prices,它的第i个元素prices[i]表示一支给定股票第i天的价格。 + +你只能选择`某一天`买入这只股票,并选择在`未来的某一个不同的日子`卖出该股票。设计一个算法来计算你所能获取的最大利润。 + +返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回0。 + + + +示例 1: + +- 输入:[7,1,5,3,6,4] +- 输出:5 +- 解释:在第2天(股票价格 = 1)的时候买入,在第5天(股票价格=6)的时候卖出,最大利润= `6-1 = 5` 。 + 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 +示例 2: + +输入:prices = [7,6,4,3,1] +输出:0 +解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 + + +### 思路 + + +##### 暴力遍历 + +先考虑最简单的「暴力遍历」,即枚举出所有情况,并从中选择最大利润。设数组 prices 的长度为 n ,由于只能先买入后卖出,因此第 1 天买可在未来 n−1 天卖出,第 2 天买可在未来 n−2 天卖出……以此类推,要思考更优解法。 + +然而,暴力法会产生许多冗余计算。例如,若第 1 天价格低于第 2 天价格,即第 1 天成本更低,那么我们一定不会选择在第 2 天买入。进一步的,若在前 i 天选择买入,若想达到最高利润,则一定选择价格最低的交易日买入。 + +##### 贪心思想 + +考虑根据此贪心思想,遍历价格列表 prices 并执行两步: + + +- 更新前 i 天的最低价格,即最低买入成本 cost; +- 更新前 i 天的最高利润 profit ,即选择「前 i−1 天最高利润 profit 」和「第 i 天卖出的最高利润 price - cost 」中的最大值 ; + + +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + minPrice = prices[0] + cost = 0 + for i in prices: + if i < minPrice: + minPrice = i + elif i - minPrice > cost: + cost = i - minPrice + return cost +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" "b/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" new file mode 100644 index 00000000..05b07c7d --- /dev/null +++ "b/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" @@ -0,0 +1,105 @@ +8.买卖股票的最佳时机II +=== + + +### 题目 + +给你一个整数数组prices,其中prices[i]表示某支股票第i天的价格。 + +在每一天,你可以决定是否购买和/或出售股票。你在任何时候最多只能持有一股股票。你也可以先购买,然后在同一天出售。 + +返回 你能获得的 最大 利润 。 + + + +示例 1: + +输入:prices = [7,1,5,3,6,4] +输出:7 +解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 +最大总利润为 4 + 3 = 7 。 +示例 2: + +输入:prices = [1,2,3,4,5] +输出:4 +解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +最大总利润为 4 。 +示例 3: + +输入:prices = [7,6,4,3,1] +输出:0 +解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。 + + +### 思路 + +这个的改动点时可以交易多次,怎么能保证利益最大? +下面两种方式其实一样 + +##### 方式一 + +只要第二天价格比第一天价格高就卖 + +```kotlin +class Solution { + fun maxProfit(prices: IntArray): Int { + var buyPirce = prices[0] + var totalIncome = 0 + for (i in prices) { + if (i > buyPirce) { + totalIncome += (i - buyPirce) + buyPirce = i + } else if ( < buyPrice) { + buyPirce = i + } + } + return totalIncome + } +} +``` +##### 方式二 + +- 记录当前可卖的最大收入及买入的价格 +- 如果最新的价格低于买入的价格或者当前已经有收入了且当前的价格小于最大收入时的卖价(i - buyPrice < income),这个时候需要卖了再买 + + +```python3 +class Solution: + def maxProfit(self, prices: List[int]) -> int: + buyPrice = prices[0] + income = 0 + totalIncome = 0 + for i in prices: + + if i < buyPrice or i - buyPrice < income: # 有更低的价格,或者说现在的价格比目前收益最大时可卖出的价格更低 + # 有更低的购买价格了,这个时候要判断当前有没有利润,有就卖,没有就使用该价格买 + if (income > 0): + totalIncome += income + income = 0 + buyPrice = i + elif (i - buyPrice > income): + income = i - buyPrice + + totalIncome += income + return totalIncome +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" new file mode 100644 index 00000000..705396c1 --- /dev/null +++ "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -0,0 +1,90 @@ +9.跳跃游戏 +=== + + +### 题目 + +给你一个非负整数数组nums,你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个下标,如果可以,返回true;否则,返回false。 + + + +示例 1: + +输入:nums = [2,3,1,1,4] +输出:true +解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 +示例 2: + +输入:nums = [3,2,1,0,4] +输出:false +解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 + + + +### 思路 + +##### 方式一 + +题目中说:数组中的每个元素代表你在该位置可以跳跃的最大长度。 +所以我们只要保证能够跳跃的最大长度超过了数组的长度就可以。 + + +- 每走一步就记录最远可以到哪里 +- 最远可达位置够不够你继续往前走 + + +```python +class Solution: + def canJump(self, nums: List[int]) -> bool: + rightmost = 0 + for i in range(len(nums)): + # 在可走的最大步数范围内走 + if i <= rightmost: + # 每走一步更新一下可继续走的最大步 + rightmost = max(rightmost, i + nums[i]) + if rightmost >= len(nums) - 1: + return True + return False +``` + + + + +##### 方式二 + + +- 假设在一个格子上,每个格子有对应的能量值。 +- 每前进一步需要消耗一个能量; +- 而到达一个格子后, 如果当前格子储存的能量值较大,则更新为较大的能量值; +- 如果当前格子能量值小于现有的能量值,则无需更新; +- 如果出现能量值正好消耗完,那就没能量继续走了,最远的步数就是这里了 +- 判断最远的步数与数组的长度一样不一样 + +```python +class Solution: + def canJump(self, nums: List[int]) -> bool: + cur = nums[0] + if len(nums) == 1: + return True + + for i in range(len(nums)): + cur -= 1 + if cur < nums[i]: + cur = nums[i] + if cur <= 0: + break + return i == (len(nums) - 1) +``` + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 293961fb..1f65e19f 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -461,14 +461,6 @@ make_pizza('mushrooms','green peppers','extra cheese') 看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 -看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 - - -看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 - - -看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 - ### 将函数存储在模块中 @@ -642,7 +634,7 @@ with open('test.txt') as file_object: filename = 'test.txt' with open(filename) as file_object: for line in file_object: - print(line) + print(line) ``` 这里使用了关键字with,让Python负责妥善地打开和关闭文件。 使用关键字with时,open()返回的文件对象只在with代码块内可用。 diff --git "a/SourceAnalysis/RecyclerView\345\244\215\347\224\250\345\216\237\347\220\206.md" "b/SourceAnalysis/RecyclerView\345\244\215\347\224\250\345\216\237\347\220\206.md" new file mode 100644 index 00000000..bf99eba6 --- /dev/null +++ "b/SourceAnalysis/RecyclerView\345\244\215\347\224\250\345\216\237\347\220\206.md" @@ -0,0 +1,15 @@ +RecyclerView复用原理 +=== + +但是很多时候我们的使用方式并不是最完美的,将Adapter和数据进行了强绑定。尽管在很多实际项目中,将数据存储在 Adapter 内部,甚至将数据作为了Adapter的成员变量是一种常见的做法: + +这种实现方式就是把数据给Adapter,然后Adapter进过处理,完成数据的展示,不过,从严格的单一职责原则角度出发, Adapter 只应该负责“映射”而不是“管理”数据。也就是说,数据的维护(如数据的获取、更新和业务逻辑)可以交给 Activity、Fragment 或 ViewModel,而 Adapter 仅仅负责根据外部传入的数据来创建和绑定视图 + + + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! diff --git "a/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" "b/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" index f0a70991..e04ca224 100644 --- "a/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" +++ "b/VideoDevelopment/OpenGL/14.\345\256\236\344\276\213\345\214\226.md" @@ -1,9 +1,25 @@ ## 13.实例化 + +OpenGL ES实例化(Instancing)是一种只调用一次渲染函数就能绘制出很多物体的技术,可以实现将数据一次性发送给GPU,告诉OpenGL ES使用一个绘制函数,将这些数据绘制成多个物体。 + + +实例化(Instancing)避免了CPU多次向GPU下达渲染命令(避免多次调用glDrawArrays或glDrawElements等绘制函数),节省了绘制多个物体时CPU和GPU之间的通信时间,提升了渲染性能。 + + + + + 实例化(instancing)提供了一种机制,可以只用一个C++/OpenGL调用就告诉显卡渲染一个对象的多个副本。这可以带来显著的性能优势,特别是在绘制有数千甚至数百万个对象时,例如渲染在场地中的许多花朵。 +### 粒子 + 实例化最常见的应用就是做一些粒子效果,例如一些烟花的效果。。 +实例化的目标并不是实现同一个物体绘制多次,而是能基于某一物体绘制出位置、大小、形状或颜色不同的多个物体。 + +OpenGL ES着色器中有一个与实例化绘制相关的内建变量gl_InstanceID,表示当前正在绘制实例的ID,每个实例对应一个唯一的ID,通过这个ID可以轻易实现基于一个物体而绘制出位置、大小、形状或颜色不同的多个物体(实例)。 + 在渲染多个对象时,OpenGL使用Z-buffer算法来进行隐藏面消除。通常情况下,通过选择最接近相机的相应片段的颜色作为像素的颜色,这种方法可决定哪些物体的曲面可见并呈现到屏幕,而位于其他物体后面的曲面不应该被渲染。 然而,有时候场景中的两个物体表面重叠并位于重合的平面中,这使得深度缓冲区算法难以确定应该渲染两个表面中的哪一个(因为两者都不“最接近”相机)。发生这种情况时,浮点舍入误差可能会导致渲染表面的某些部分使用其中一个对象的颜色,而其他部分则使用另一个对象的颜色。这种不自然的伪影称为Z冲突(Z-fighting)或深度冲突(depth-fighting),是渲染的片段在深度缓冲区中相互对应的像素条目上“斗争”的结果。 diff --git "a/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" "b/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" index 72202177..723b4dae 100644 --- "a/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" +++ "b/VideoDevelopment/OpenGL/18.\345\205\266\344\273\226.md" @@ -64,5 +64,11 @@ GPU进行渲染时,会把数据先存储在帧缓冲区里,然后视频控 +### 混合 + +OpenGL ES混合本质上是将2个片元颜色进行调和产生一个新的颜色。 + +OpenGL ES混合发生在片元通过各项测试之后,准备进入帧缓冲区的片元和原有的片元按照特定比例加权计算出最终片元的颜色值 + diff --git "a/VideoDevelopment/OpenGL/19.\347\233\270\346\234\272\351\242\204\350\247\210.md" "b/VideoDevelopment/OpenGL/19.\347\233\270\346\234\272\351\242\204\350\247\210.md" new file mode 100644 index 00000000..fec81043 --- /dev/null +++ "b/VideoDevelopment/OpenGL/19.\347\233\270\346\234\272\351\242\204\350\247\210.md" @@ -0,0 +1,43 @@ +# 19.相机预览 + +相机预览可以使用OpenGL方便的实现相机美颜、滤镜、塑性以及一些特效。 + + +相机的预览实现一般有2种方式: + +- 一种是基于Android原生SurfaceTexture的纯GPU实现方式 +- 一种是通过相机的预览回调接口获取帧的YUV数据,利用CPU算法处理完成之后,传入显存,再利用GPU实现YUV转RGBA进行渲染,即CPU + GPU的实现方式 + +基于Android原生SurfaceTexture的纯GPU实现方式,相机可以使用SurfaceTexture作为预览载体。 +SurfaceTexture可以来自于GLSurfaceView、TextureView或SurfaceView这些独立拥有Surface的封装类,也可以自定义实现。 + + +作为预览载体的SurfaceTexture绑定的纹理需要是OES纹理。 + +使用OES纹理后,我们不需要在片段着色器中自己做YUV to RGBA的转换,因为OES纹理可以直接接收YUV数据或者直接输出YUV数据。 + + +### YUV + +YUV和RGBA是两种不同的颜色编码格式,主要区别于在数据组织和应用场景。 + +- YUV主要用于视频压缩、传输(例如H264/HEVC),优势为亮度和色度分离适合压缩(人眼对亮度更敏感,色度可降采样)。 +YUV为视频压缩设计,牺牲直接显示兼容性换取高压缩率。 + +- RGBA主要用于图像显示、图形渲染(如屏幕绘制),每个像素的RGBA值连续存储,可直接兼容显示设备。 +RGBA为显示优化,直接支持像素渲染和透明通道。 + + +### OES与YUV的关系 + +OES纹理(GL_TEXTURE_EXTERNAL_OES)是OpenGL ES的一种扩展纹理类型,主要用于直接操作外部设备(如摄像头、视频解码器)的纹理数据。它与YUV的关系如下: + +- 视频解码后的YUV数据: 移动端视频解码器(如Android的MediaCodec)输出的YUV帧数据,通常通过OES纹理直接传递给GPU,避免CPU到GPU的数据拷贝。 + + + +使用OES纹理需要修改片段着色器,在着色器脚本的头部增加扩展纹理的声明: + +`#extension GL_OES_EGL_image_external : require` + +并且纹理采样器不再使用sample2D,需要换成samplerExternalOES作为纹理采样器。 diff --git "a/VideoDevelopment/OpenGL/20.\351\200\232\350\277\207OpenGL\351\242\204\350\247\210\345\271\266\345\275\225\345\210\266\350\247\206\351\242\221.md" "b/VideoDevelopment/OpenGL/20.\351\200\232\350\277\207OpenGL\351\242\204\350\247\210\345\271\266\345\275\225\345\210\266\350\247\206\351\242\221.md" new file mode 100644 index 00000000..64e06971 --- /dev/null +++ "b/VideoDevelopment/OpenGL/20.\351\200\232\350\277\207OpenGL\351\242\204\350\247\210\345\271\266\345\275\225\345\210\266\350\247\206\351\242\221.md" @@ -0,0 +1,50 @@ +## 通过OpenGL预览并录制视频 + + +Camera将预览的画面通过SurfaceTexture输出给屏幕的GLSurfaceView以及视频编码的MediaCodec的Surface上。 + + +将渲染的结果输出到不同的目标,我们需要使用一种称为离屏渲染的技术。 + +离屏渲染:就是让OpenGL不将渲染的结果直接输出到屏幕上,而是输出到一个中间缓冲区(一块GPU空间),然后再将中间缓冲区的内容输出到屏幕或编码器等目标上,这就称为离屏渲染。 + + +在Android系统下可以使用三种方法实现同时将OpenGL ES的内容输出给多个目标(屏幕和编码器): + +- 二次渲染法 +收到数据后调用Shader程序进行渲染;将渲染结果输出到SurfaceView中显示到屏幕;然后我们需要切换当前渲染的EGLSurface,通过调用EGL的eglMakeCurrent方法,将默认SurfaceView的EGLSurface切换到MediaCodec的EGLSurface上,然后再次调用Shader程序进行渲染,并将渲染结果输出给MediaCodec的Surface进行编码。 + +因为调用了两次Shader程序进行渲染,每次渲染后的结果输送给不同的Surface,因此称为二次渲染法。 +- FBO +二次渲染会调用两次滤镜程序的draw方法,将同一个纹理绘制到不同的Surface上,但是如果要对图像进行一些美颜、高斯模糊等变换操作,那么我们就要对同一个纹理进行两次计算,这无疑是对GPU的浪费,且效率低下。 + +那如何只计算一次,把结果输出给不同的Surface呢? +OpenGL提供了一种高效的方法,即FBO(FrameBufferObject)。 + +FBO法中我们操作步骤如下: + +- 将渲染结果绘制到FBO中 +- 将FBO数据输送到屏幕中 +- 将FBO数据输送到编码器 + +FBO法中,我们不直接将OpenGL ES的渲染结果输送给不同的Surface,而是将结果输出到FBO中,FBO可以理解为一块显存区域,用于存放OpenGL ES的渲染结果。 +我们知道CameraFilter着色器程序是将SurfaceTexture纹理渲染到EGLSurface中的,如何将纹理渲染到FBO帧缓冲区中呢,我们需要一个渲染到FBO中的着色器程序。 +渲染结果输出到FBO后,我们可以将FBO结果分别输出给不同的目标,FBO->屏幕,FBO->MediaCodec。而FBO输出到不同的目标也需要一个新的着色器去绘制。 + +FBO高效的原理在于,纹理渲染到FBO中的前置操作。如果需要对纹理进行美颜、高斯模糊等复杂的计算,FBO确实能提高效率。如果没有复杂的计算,那么FBO和二次渲染法效率差不多。 + + +- glBlitFramebuffer: copy a block of pixels from the read framebuffer to the draw framebuffer + + +该方式不再使用FBO做缓存,而是像二次渲染法一样,先将渲染的内容输出到当前Surface中,但并不展示到屏幕上。我们来看下这种方式的流程: + +- 先将渲染结果输送给SurfaceView的Surface +- 切换Surface为MediaCodec的Surface,此时当前Surface为MediaCodec的 +- 利用OpenGL3.0提供的API glBlitFramebuffer从原来的Surface拷贝数据到当前Surface中,再调用EGL的eglSwapBuffers将Surface中的内容送编码器编码 +- 最后将当前Surface切回原来的Surface,也就是SurfaceView的Surface,同样调用EGL的eglSwapBuffers方法,将其内容显示到屏幕上 + +该方式的效率是最高的,一次渲染输出给多个目标,但是只有OpenGL3.0才有该方法 + + + diff --git "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" index ab251b38..be6c7acb 100644 --- "a/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" +++ "b/VideoDevelopment/OpenGL/5.OpenGL ES\347\273\230\345\210\266\344\270\211\350\247\222\345\275\242.md" @@ -19,7 +19,24 @@ float vertices[] = { 0.0f, 0.5f, 0.0f }; ``` -定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。 +定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。 + + + +在OpenGL ES2.0编程中,用于绘制的顶点数组数据首先保存在CPU内存,在调用glDrawArrays或glDrawElements等进行绘制时,需要将顶点数组数据从CPU内存拷贝到显存。 + +但是很多时候我们没必要每次绘制的时候都去进行内存拷贝,如果可以在显存中缓存这些数据,就可以在很大程度上降低内存拷贝带来的开销。 + +OpenGL ES3.0编程中,VBO和EBO的出现就是为了解决这个问题。 + +VBO和EBO的作用是在显存中提前开辟好一块内存,用于缓存顶点数据或者图元索引数据,从而避免每次绘制时的CPU和GPU之间的内存拷贝,可以改进渲染性能,降低内存带宽和功耗。 +OpenGL ES3.0支持两类缓冲区对象: 顶点数组缓冲区对象、图元索引缓冲区对象。 + +GL_ARRAY_BUFFER标志指定的缓冲区对象用于保存顶点数组。 + +GL_EMELEMNT_ARRAY_BUFFER标志指定的缓存区对象用于保存图元索引。 + + **这里会在GPU上创建内存用于储存我们的顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。** 顶点着色器接着会处理我们在内存中指定数量的顶点。 @@ -197,6 +214,7 @@ someOpenGLFunctionThatDrawsOurTriangle(); 绑定正确的缓冲对象,为每个物体配置所有顶点属性很快就变成一件麻烦事。有没有一些方法可以使我们把所有这些状态配置储存在一个对象中,并且可以通过绑定这个对象来恢复状态呢? 顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。 + 这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。 这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中。 diff --git "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" index 0df4bcff..4bc0353a 100644 --- "a/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" +++ "b/VideoDevelopment/OpenGL/8.GLES\347\261\273\345\217\212Matrix\347\261\273.md" @@ -244,7 +244,13 @@ OpenGL ES中使用的是列向量。列向量和矩阵相乘实现变换时, 所谓齐次坐标形式也就是在x、y、z 3个坐标值后面增加第四个量w,未变换时w值一般为1。 -所谓齐次坐标表示就是用N+1维坐标表示N维坐标。采用齐次坐标是由于很多在N维空间中难以解决的问题在N+1维空间中会变得比较简单。 +所谓齐次坐标表示就是用N+1维坐标表示N维坐标。 + +齐次坐标是为了兼容点的平移操作,使得我们可以用同一个公式对点和方向做运算。 y +采用齐次坐标是由于很多在N维空间中难以解决的问题在N+1维空间中会变得比较简单。 + + + ##### 旋转变换 diff --git "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" index d8258366..e6a24893 100644 --- "a/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" +++ "b/VideoDevelopment/OpenGL/9.OpenGL ES\347\272\271\347\220\206.md" @@ -19,6 +19,9 @@ +使用纹理坐标来获取纹理颜色就称为采样(sampling)。 + + ### 基本原理 启用纹理映射功能后,如果想把一副纹理应用到相应的几何图元,就必须告知渲染系统如何进行纹理的映射。 @@ -380,6 +383,33 @@ GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId); GLES20.glUniform1i(aTextureLocation, 0); ``` + + +#### 为什么要激活纹理单元0?(glActiveTexture(GL_TEXTURE_0)) + + +- 纹理单元系统: OpenGL通过纹理单元(Texture Unit)管理多个纹理。 + +每个纹理单元都是一个独立的“插槽”,可以绑定不同类型的纹理(GL_TEXTURE_2D, GL_TEXTURE_CUBE_MAP等) + +- 多纹理支持: 现代GPU支持同时使用多个纹理(例如同时使用漫反射贴纸+法线贴图+高光贴图)。通过激活不同纹理单元(GL_TEXTURE0、GL_TEXTURE1...)来实现这一点。 + +- 状态机特性: OpenGL是状态机,glActiveTexture选择当前操作的纹理单元,后续的glBindTexture和glTexParameter等操作都作用于这个单元。 + +#### 为什么要绑定纹理? (glBindTexture(GL_TEXTURE_2D, m_TextureId)) + +- 关联操作目标: 将具体的纹理对象(textureId)与当前激活的纹理单元(GL_TEXTURE0)关联。 + +- 多类型支持: 同一个纹理单元可以绑定不同类型的纹理(例如GL_TEXTURE_2D和GL_TEXTURE_CUBE_MAP互不影响)。 + +- 渲染时使用: 绘制时,着色器通过纹理单元索引(这里是0)访问绑定的纹理数据。 + +#### 为什么要设置采样器为0? glUniform1i(m_SamplerLoc, 0) + +- 着色器(uniform sampler2D texture1)与纹理单元的桥梁: 在着色器中sampler2D类型的uniform变量需要知道从哪个纹理单元读取数据。 0表示使用纹理单元GL_TEXTURE0(1对应GL_TEXTURE1,以此类推)。也就是告诉OpenGL每个着色器采样器从哪个纹理单元采样数据。 + +那这里为什么参数是0,而不是GL_TEXTURE0? 这是因为glUniform1i传递的是索引值,而不是OpenGL枚举常量。 + 这里有没有很奇怪,sampler2D的变量是uniform的,但是我们并不是用glUniform()方法给他赋值,而是使用glUniform1i()。 这是因为可以给纹理采样器分配一个位置值,这样我们就能够在一个片段着色器中设置多个纹理单元。一个纹理的话,纹理单元是默认为0,它是默认激活的,纹理单元的主要目的就是给着色器多一个使用的纹理。 通过纹理单元赋值给采样器,我们可以一次绑定多个纹理,只要我们在使用的时候激活纹理。 @@ -389,6 +419,17 @@ OpenGL至少保证有16个纹理单元供你使用,也就是说你可以激活 它们都是按顺序定义的,所以我们也可以通过GL_TEXUTURE0 + 8的方式获得GL_TEXTURE8,这在需要循环一些纹理单元的时候会很有用。 +#### 完整的流程 + +1. 激活单元: 选择操作目标(GL_TEXTURE0)。 类比: 选择电视机第0号HDMI输入口。 +2. 绑定纹理: 将具体纹理放入该单元。 类比: 给第0号HDMI口连接一个播放器。 +3. 设置采样器: 告诉着色器从哪个“HDMI口”读取数据。 类比: 告诉观众观看第0 HDMI口的画面。 + + + + + + ```java public class TextureRender extends BaseGLSurfaceViewRenderer { private final FloatBuffer vertextBuffer; From 84b244c71851c2a69c5487e32a67a46bbe2a454c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 16 Jun 2025 20:35:27 +0800 Subject: [PATCH 120/128] update --- ...\350\267\203\346\270\270\346\210\217II.md" | 23 ++++---- "Algorithm/11.H\346\214\207\346\225\260.md" | 33 +++++------ ...17\346\234\272\345\205\203\347\264\240.md" | 26 ++++----- ...04\347\232\204\344\271\230\347\247\257.md" | 19 ++++--- ...4.\345\212\240\346\262\271\347\253\231.md" | 55 ++++++++++--------- ...06\345\217\221\347\263\226\346\236\234.md" | 16 +++--- ...6.\346\216\245\351\233\250\346\260\264.md" | 15 ++--- ...32\346\225\260\345\205\203\347\264\240.md" | 13 +++-- ...56\350\275\254\346\225\260\347\273\204.md" | 33 +++++------ ...00\344\275\263\346\227\266\346\234\272.md" | 8 +-- ...\344\275\263\346\227\266\346\234\272II.md" | 34 ++++++------ ...63\350\267\203\346\270\270\346\210\217.md" | 19 ++++--- 12 files changed, 153 insertions(+), 141 deletions(-) diff --git "a/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" index cd9ecad4..0e95d5fa 100644 --- "a/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" +++ "b/Algorithm/10.\350\267\263\350\267\203\346\270\270\346\210\217II.md" @@ -6,30 +6,31 @@ 给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。 -每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: +每个元素 nums[i] 表示从索引 i 向后跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处: -0 <= j <= nums[i] -i + j < n +- 0 <= j <= nums[i] +- i + j < n 返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。 示例 1: -输入: nums = [2,3,1,1,4] -输出: 2 -解释: 跳到最后一个位置的最小跳跃数是 2。 - 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 -示例 2: +- 输入: nums = [2,3,1,1,4] +- 输出: 2 +- 解释: 跳到最后一个位置的最小跳跃数是 2。 + - 从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 -输入: nums = [2,3,0,1,4] -输出: 2 +示例 2: + +- 输入: nums = [2,3,0,1,4] +- 输出: 2 ### 思路 -贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 +贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数! diff --git "a/Algorithm/11.H\346\214\207\346\225\260.md" "b/Algorithm/11.H\346\214\207\346\225\260.md" index 5be403e0..20afa38a 100644 --- "a/Algorithm/11.H\346\214\207\346\225\260.md" +++ "b/Algorithm/11.H\346\214\207\346\225\260.md" @@ -10,16 +10,17 @@ -示例 1: +示例 1: -输入:citations = [3,0,6,1,5] -输出:3 -解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 - 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。 -示例 2: +- 输入:citations = [3,0,6,1,5] +- 输出:3 +- 解释:给定数组表示研究者总共有 5 篇论文,每篇论文相应的被引用了 3, 0, 6, 1, 5 次。 + - 由于研究者有 3 篇论文每篇 至少 被引用了 3 次,其余两篇论文每篇被引用 不多于 3 次,所以她的 h 指数是 3。 -输入:citations = [1,3,1] -输出:1 +示例 2: + +- 输入:citations = [1,3,1] +- 输出:1 ### 思路 @@ -67,7 +68,7 @@ public int hIndex(int[] citations) { 最终的时间复杂度与排序算法的时间复杂度有关 -复杂度分析 +复杂度分析: - 时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。即为排序的时间复杂度。 @@ -123,18 +124,18 @@ public class Solution { } ``` -复杂度分析 +复杂度分析: -时间复杂度:O(n),其中 n 为数组 citations 的长度。需要遍历数组 citations 一次,以及遍历长度为 n+1 的数组 counter 一次。 +- 时间复杂度:O(n),其中 n 为数组 citations 的长度。需要遍历数组 citations 一次,以及遍历长度为 n+1 的数组 counter 一次。 -空间复杂度:O(n),其中 n 为数组 citations 的长度。需要创建长度为 n+1 的数组 counter。 +- 空间复杂度:O(n),其中 n 为数组 citations 的长度。需要创建长度为 n+1 的数组 counter。 ##### 方法三:二分法 所谓的 h 指数是指一个具体的数值,该数值为“最大”的满足「至少发表了 x 篇论文,且每篇论文至少被引用 x 次」定义的合法数,重点是“最大”。 -给定所有论文的引用次数情况为[3,0,6,1,5],可统计满足定义的数值有哪些: +给定所有论文的引用次数情况为[3,0,6,1,5],可统计满足定义的数值有哪些: ``` h=0,含义为「至少发表了 0 篇,且这 0 篇论文至少被引用 0 次」,空集即满足,恒成立; @@ -215,10 +216,10 @@ class Solution { -复杂度分析 +复杂度分析: -时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。需要进行 logn 次二分搜索,每次二分搜索需要遍历数组 citations 一次。 -空间复杂度:O(1),只需要常数个变量来进行二分搜索。 +- 时间复杂度:O(nlogn),其中 n 为数组 citations 的长度。需要进行 logn 次二分搜索,每次二分搜索需要遍历数组 citations 一次。 +- 空间复杂度:O(1),只需要常数个变量来进行二分搜索。 diff --git "a/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" index 469f6e99..68b25643 100644 --- "a/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" +++ "b/Algorithm/12.O(1) \346\227\266\351\227\264\346\217\222\345\205\245\343\200\201\345\210\240\351\231\244\345\222\214\350\216\267\345\217\226\351\232\217\346\234\272\345\205\203\347\264\240.md" @@ -4,25 +4,25 @@ ### 题目 -实现RandomizedSet 类: +实现RandomizedSet类: + +- RandomizedSet() 初始化 RandomizedSet 对象 +- bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 +- bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 +- int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 -RandomizedSet() 初始化 RandomizedSet 对象 -bool insert(int val) 当元素 val 不存在时,向集合中插入该项,并返回 true ;否则,返回 false 。 -bool remove(int val) 当元素 val 存在时,从集合中移除该项,并返回 true ;否则,返回 false 。 -int getRandom() 随机返回现有集合中的一项(测试用例保证调用此方法时集合中至少存在一个元素)。每个元素应该有 相同的概率 被返回。 你必须实现类的所有函数,并满足每个函数的 平均 时间复杂度为 O(1) 。 -示例: +示例: -输入 -["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] -[[], [1], [2], [2], [], [1], [2], []] -输出 -[null, true, false, true, 2, true, false, 2] +- 输入["RandomizedSet", "insert", "remove", "insert", "getRandom", "remove", "insert", "getRandom"] +- [[], [1], [2], [2], [], [1], [2], []] +- 输出[null, true, false, true, 2, true, false, 2] -解释 +解释: +``` RandomizedSet randomizedSet = new RandomizedSet(); randomizedSet.insert(1); // 向集合中插入 1 。返回 true 表示 1 被成功地插入。 randomizedSet.remove(2); // 返回 false ,表示集合中不存在 2 。 @@ -31,7 +31,7 @@ randomizedSet.getRandom(); // getRandom 应随机返回 1 或 2 。 randomizedSet.remove(1); // 从集合中移除 1 ,返回 true 。集合现在包含 [2] 。 randomizedSet.insert(2); // 2 已在集合中,所以返回 false 。 randomizedSet.getRandom(); // 由于 2 是集合中唯一的数字,getRandom 总是返回 2 。 - +``` 提示: diff --git "a/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" index 6b555aeb..abff28d0 100644 --- "a/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" +++ "b/Algorithm/13.\351\231\244\350\207\252\350\272\253\344\273\245\345\244\226\346\225\260\347\273\204\347\232\204\344\271\230\347\247\257.md" @@ -4,22 +4,23 @@ ### 题目 -给你一个整数数组 nums,返回 数组 answer ,其中 answer[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积 。 +给你一个整数数组nums,返回数组answer,其中answer[i]等于nums中除nums[i]之外其余各元素的乘积。 -题目数据 保证 数组 nums之中任意元素的全部前缀元素和后缀的乘积都在 32 位 整数范围内。 +题目数据保证数组nums之中任意元素的全部前缀元素和后缀的乘积都在32位整数范围内。 -请 不要使用除法,且在 O(n) 时间复杂度内完成此题。 +请***不要使用除法***,且在O(n)时间复杂度内完成此题。 -示例 1: +示例 1: -输入: nums = [1,2,3,4] -输出: [24,12,8,6] -示例 2: +- 输入: nums = [1,2,3,4] +- 输出: [24,12,8,6] -输入: nums = [-1,1,0,-3,3] -输出: [0,0,9,0,0] +示例 2: + +- 输入: nums = [-1,1,0,-3,3] +- 输出: [0,0,9,0,0] 提示: diff --git "a/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" index b39a9edd..8debb624 100644 --- "a/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" +++ "b/Algorithm/14.\345\212\240\346\262\271\347\253\231.md" @@ -12,37 +12,40 @@ -示例 1: - -输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] -输出: 3 -解释: -从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 -开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 -开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 -开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 -开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 -开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 +示例 1: + +- 输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] +- 输出: 3 +- 解释: + - 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 + - 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 + - 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 + - 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 + - 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 + - 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 + 因此,3 可为起始索引。 -示例 2: - -输入: gas = [2,3,4], cost = [3,4,3] -输出: -1 -解释: -你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 -我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 -开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 -开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 -你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 + +示例 2: + +- 输入: gas = [2,3,4], cost = [3,4,3] +- 输出: -1 +- 解释: + - 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 + - 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 + - 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 + - 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 + - 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 + 因此,无论怎样,你都不可能绕环路行驶一周。 -提示: +提示: -n == gas.length == cost.length -1 <= n <= 105 -0 <= gas[i], cost[i] <= 104 -输入保证答案唯一。 +- n == gas.length == cost.length +- 1 <= n <= 105 +- 0 <= gas[i], cost[i] <= 104 +- 输入保证答案唯一。 ### 思路 diff --git "a/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" index 66db9e01..b74a91ed 100644 --- "a/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" +++ "b/Algorithm/15.\345\210\206\345\217\221\347\263\226\346\236\234.md" @@ -4,17 +4,17 @@ ### 题目 -n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 - -你需要按照以下要求,给这些孩子分发糖果: +n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。 + +你需要按照以下要求,给这些孩子分发糖果: -每个孩子至少分配到 1 个糖果。 -相邻两个孩子评分更高的孩子会获得更多的糖果。 -请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。 +- 每个孩子至少分配到 1 个糖果。 +- 相邻两个孩子评分更高的孩子会获得更多的糖果。 +- 请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。 -示例 1: +示例 1: - 输入:ratings = [1,0,2] - 输出:5 @@ -28,7 +28,7 @@ n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的 第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。 -提示: +提示: - n == ratings.length - 1 <= n <= 2 * 104 diff --git "a/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" index 853cadbd..0a86815c 100644 --- "a/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" +++ "b/Algorithm/16.\346\216\245\351\233\250\346\260\264.md" @@ -6,17 +6,18 @@ 给定n个非负整数表示每个宽度为1的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 -示例 1: +示例 1: ![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rainwatertrap_1.png?raw=true) -输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] -输出:6 -解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 -示例 2: +- 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +- 输出:6 +- 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 -输入:height = [4,2,0,3,2,5] -输出:9 +示例 2: + +- 输入:height = [4,2,0,3,2,5] +- 输出:9 ### 思路 diff --git "a/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" index 71e6953d..f99c600a 100644 --- "a/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" +++ "b/Algorithm/5.\345\244\232\346\225\260\345\205\203\347\264\240.md" @@ -11,14 +11,15 @@ 数组中出现次数超过一半的数字被称为众数。 -示例 1: +示例 1: -输入:nums = [3,2,3] -输出:3 -示例 2: +- 输入:nums = [3,2,3] +- 输出:3 -输入:nums = [2,2,1,1,1,2,2] -输出:2 +示例 2: + +- 输入:nums = [2,2,1,1,1,2,2] +- 输出:2 diff --git "a/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" index eb2940c2..4f9e6d02 100644 --- "a/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" +++ "b/Algorithm/6.\350\275\256\350\275\254\346\225\260\347\273\204.md" @@ -7,21 +7,25 @@ 给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。 -示例 1: +示例 1: -输入: nums = [1,2,3,4,5,6,7], k = 3 -输出: [5,6,7,1,2,3,4] -解释: -向右轮转 1 步: [7,1,2,3,4,5,6] -向右轮转 2 步: [6,7,1,2,3,4,5] -向右轮转 3 步: [5,6,7,1,2,3,4] -示例 2: +- 输入: nums = [1,2,3,4,5,6,7], k = 3 +- 输出: [5,6,7,1,2,3,4] -输入:nums = [-1,-100,3,99], k = 2 -输出:[3,99,-1,-100] -解释: -向右轮转 1 步: [99,-1,-100,3] -向右轮转 2 步: [3,99,-1,-100] +解释: +- 向右轮转 1 步: [7,1,2,3,4,5,6] +- 向右轮转 2 步: [6,7,1,2,3,4,5] +- 向右轮转 3 步: [5,6,7,1,2,3,4] + +示例 2: + +- 输入:nums = [-1,-100,3,99], k = 2 +- 输出:[3,99,-1,-100] + +解释: + +- 向右轮转 1 步: [99,-1,-100,3] +- 向右轮转 2 步: [3,99,-1,-100] @@ -77,9 +81,6 @@ class Solution: ``` - - - --- - 邮箱 :charon.chui@gmail.com - Good Luck! diff --git "a/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" "b/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" index 5e4f9722..18fa2d74 100644 --- "a/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" +++ "b/Algorithm/7.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" @@ -18,11 +18,11 @@ - 输出:5 - 解释:在第2天(股票价格 = 1)的时候买入,在第5天(股票价格=6)的时候卖出,最大利润= `6-1 = 5` 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 -示例 2: +示例 2: -输入:prices = [7,6,4,3,1] -输出:0 -解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 +- 输入:prices = [7,6,4,3,1] +- 输出:0 +- 解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 ### 思路 diff --git "a/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" "b/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" index 05b07c7d..00e0fa54 100644 --- "a/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" +++ "b/Algorithm/8.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" @@ -12,24 +12,26 @@ -示例 1: +示例 1: -输入:prices = [7,1,5,3,6,4] -输出:7 -解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 -随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 -最大总利润为 4 + 3 = 7 。 -示例 2: +- 输入:prices = [7,1,5,3,6,4] +- 输出:7 +- 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +- 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 +- 最大总利润为 4 + 3 = 7 。 -输入:prices = [1,2,3,4,5] -输出:4 -解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 -最大总利润为 4 。 -示例 3: +示例 2: -输入:prices = [7,6,4,3,1] -输出:0 -解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。 +- 输入:prices = [1,2,3,4,5] +- 输出:4 +- 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 +- 最大总利润为 4。 + +示例 3: + +- 输入:prices = [7,6,4,3,1] +- 输出:0 +- 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0。 ### 思路 @@ -50,7 +52,7 @@ class Solution { if (i > buyPirce) { totalIncome += (i - buyPirce) buyPirce = i - } else if ( < buyPrice) { + } else if (i < buyPrice) { buyPirce = i } } diff --git "a/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" index 705396c1..3bb87d3a 100644 --- "a/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" +++ "b/Algorithm/9.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -10,16 +10,17 @@ -示例 1: +示例 1: -输入:nums = [2,3,1,1,4] -输出:true -解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 -示例 2: +- 输入:nums = [2,3,1,1,4] +- 输出:true +- 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。 -输入:nums = [3,2,1,0,4] -输出:false -解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 +示例 2: + +- 输入:nums = [3,2,1,0,4] +- 输出:false +- 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。 @@ -74,7 +75,7 @@ class Solution: if cur < nums[i]: cur = nums[i] if cur <= 0: - break + break return i == (len(nums) - 1) ``` From c19a90be805f24af29b1bf52c8065ee444400d03 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 17 Jun 2025 18:57:13 +0800 Subject: [PATCH 121/128] add notes --- ...27\350\275\254\346\225\264\346\225\260.md" | 116 +++++++ ...27\351\251\254\346\225\260\345\255\227.md" | 114 ++++++ ...15\347\232\204\351\225\277\345\272\246.md" | 78 +++++ ...54\345\205\261\345\211\215\347\274\200.md" | 68 ++++ ...55\347\232\204\345\215\225\350\257\215.md" | 84 +++++ ...27\345\275\242\350\275\254\346\215\242.md" | 106 ++++++ ...71\347\232\204\344\270\213\346\240\207.md" | 324 ++++++++++++++++++ ...46\345\217\263\345\257\271\351\275\220.md" | 80 +++++ ...01\345\233\236\346\226\207\344\270\262.md" | 48 +++ Algorithm/26..md | 17 + Algorithm/27..md | 17 + 11 files changed, 1052 insertions(+) create mode 100644 "Algorithm/17.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" create mode 100644 "Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" create mode 100644 "Algorithm/19.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" create mode 100644 "Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" create mode 100644 "Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" create mode 100644 "Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" create mode 100644 "Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" create mode 100644 "Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" create mode 100644 "Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" create mode 100644 Algorithm/26..md create mode 100644 Algorithm/27..md diff --git "a/Algorithm/17.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" "b/Algorithm/17.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" new file mode 100644 index 00000000..f6765dd5 --- /dev/null +++ "b/Algorithm/17.\347\275\227\351\251\254\346\225\260\345\255\227\350\275\254\346\225\264\346\225\260.md" @@ -0,0 +1,116 @@ +17.罗马数字转整数 +=== + + +### 题目 + +罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 + +``` +字符 数值 +I 1 +V 5 +X 10 +L 50 +C 100 +D 500 +M 1000 +``` +例如, 罗马数字 2 写做 II ,即为两个并列的 1 。12 写做 XII ,即为 X + II 。 27 写做 XXVII, 即为 XX + V + II 。 + +通常情况下,罗马数字中小的数字在大的数字的右边。但也存在特例,例如 4 不写做 IIII,而是 IV。数字 1 在数字 5 的左边,所表示的数等于大数 5 减小数 1 得到的数值 4 。同样地,数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况: + +- I 可以放在 V (5) 和 X (10) 的左边,来表示 4 和 9。 +- X 可以放在 L (50) 和 C (100) 的左边,来表示 40 和 90。 +- C 可以放在 D (500) 和 M (1000) 的左边,来表示 400 和 900。 +- 给定一个罗马数字,将其转换成整数。 + + + +示例 1: + +- 输入: s = "III" +- 输出: 3 + +示例 2: + +- 输入: s = "IV" +- 输出: 4 + +示例 3: + +- 输入: s = "IX" +- 输出: 9 + +示例 4: + +- 输入: s = "LVIII" +- 输出: 58 +- 解释: L = 50, V= 5, III = 3. + +示例 5: + +- 输入: s = "MCMXCIV" +- 输出: 1994 +- 解释: M = 1000, CM = 900, XC = 90, IV = 4. + + +提示: + +- 1 <= s.length <= 15 +- s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M') +- 题目数据保证 s 是一个有效的罗马数字,且表示整数在范围 [1, 3999] 内 +- 题目所给测试用例皆符合罗马数字书写规则,不会出现跨位等情况。 +- IL 和 IM 这样的例子并不符合题目要求,49 应该写作 XLIX,999 应该写作 CMXCIX 。 + +### 思路 + +通常情况下,罗马数字中小的数字在大的数字的右边。若输入的字符串满足该情况,那么可以将每个字符视作一个单独的值,累加每个字符对应的数值即可。 + +例如 XXVII 可视作 X+X+V+I+I=10+10+5+1+1=27。 + +若存在小的数字在大的数字的左边的情况,根据规则需要减去小的数字。对于这种情况,我们也可以将每个字符视作一个单独的值,若一个数字右侧的数字比它大,则将该数字的符号取反。 + +例如 XIV 可视作 X−I+V=10−1+5=14。 + +所以我们可以从左遍历,如果一个字符右边的比当前的大,那这个就要减去该值,否则就加。 + +```java +class Solution { + Map symbolValues = new HashMap() {{ + put('I', 1); + put('V', 5); + put('X', 10); + put('L', 50); + put('C', 100); + put('D', 500); + put('M', 1000); + }}; + + public int romanToInt(String s) { + int ans = 0; + int n = s.length(); + for (int i = 0; i < n; ++i) { + int value = symbolValues.get(s.charAt(i)); + if (i < n - 1 && value < symbolValues.get(s.charAt(i + 1))) { + ans -= value; + } else { + ans += value; + } + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是字符串 s 的长度。 + +- 空间复杂度:O(1)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" "b/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" new file mode 100644 index 00000000..cce5ae83 --- /dev/null +++ "b/Algorithm/18.\346\225\264\346\225\260\350\275\254\347\275\227\351\251\254\346\225\260\345\255\227.md" @@ -0,0 +1,114 @@ +18.整数转罗马数字 +=== + + +### 题目 + +七个不同的符号代表罗马数字,其值如下: +``` +符号 值 +I 1 +V 5 +X 10 +L 50 +C 100 +D 500 +M 1000 +``` +罗马数字是通过添加从最高到最低的小数位值的转换而形成的。将小数位值转换为罗马数字有以下规则: + +- 如果该值不是以 4 或 9 开头,请选择可以从输入中减去的最大值的符号,将该符号附加到结果,减去其值,然后将其余部分转换为罗马数字。 +- 如果该值以 4 或 9 开头,使用 减法形式,表示从以下符号中减去一个符号,例如 4 是 5 (V) 减 1 (I): IV ,9 是 10 (X) 减 1 (I):IX。仅使用以下减法形式:4 (IV),9 (IX),40 (XL),90 (XC),400 (CD) 和 900 (CM)。 +- 只有 10 的次方(I, X, C, M)最多可以连续附加 3 次以代表 10 的倍数。你不能多次附加 5 (V),50 (L) 或 500 (D)。如果需要将符号附加4次,请使用 减法形式。 +- 给定一个整数,将其转换为罗马数字。 + + + +示例 1: + +- 输入:num = 3749 + +- 输出: "MMMDCCXLIX" + +- 解释: + + - 3000 = MMM 由于 1000 (M) + 1000 (M) + 1000 (M) + - 700 = DCC 由于 500 (D) + 100 (C) + 100 (C) + - 40 = XL 由于 50 (L) 减 10 (X) + - 9 = IX 由于 10 (X) 减 1 (I) +注意:49 不是 50 (L) 减 1 (I) 因为转换是基于小数位 + +示例 2: + +- 输入:num = 58 + +- 输出:"LVIII" + +解释: + + - 50 = L + - 8 = VIII + +示例 3: + +- 输入:num = 1994 + +- 输出:"MCMXCIV" + +- 解释: + + - 1000 = M + - 900 = CM + - 90 = XC + - 4 = IV + + +提示: + +- 1 <= num <= 3999 + +### 思路 + +题目中说了num <= 3999 + +所以: + +- 千位数只能由M表示,分别为 M,MM,MMM +- 百位数只能由C、CC、CCC、CD、D、DC、DCC、DCCC、CM表示 +- 十位数只能由X、XX、XXX、XL、L、LX、LXX、LXXX、XC表示 +- 个位数只能由I、II、III、IV、V、VI、VII、VIII、IV表示 + + +所以可以利用模运算和除法运算,得到num每个位上的数字,然后去取对应的罗马数字就可以了。 + +```java + +class Solution { + String[] thousands = {"", "M", "MM", "MMM"}; + String[] hundreds = {"", "C", "CC", "CCC", "CD", "D", "DC", "DCC", "DCCC", "CM"}; + String[] tens = {"", "X", "XX", "XXX", "XL", "L", "LX", "LXX", "LXXX", "XC"}; + String[] ones = {"", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX"}; + + public String intToRoman(int num) { + StringBuffer roman = new StringBuffer(); + roman.append(thousands[num / 1000]); + roman.append(hundreds[num % 1000 / 100]); + roman.append(tens[num % 100 / 10]); + roman.append(ones[num % 10]); + return roman.toString(); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(1)。计算量与输入数字的大小无关。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/19.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" "b/Algorithm/19.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" new file mode 100644 index 00000000..95e2c32f --- /dev/null +++ "b/Algorithm/19.\346\234\200\345\220\216\344\270\200\344\270\252\345\215\225\350\257\215\347\232\204\351\225\277\345\272\246.md" @@ -0,0 +1,78 @@ +19.最后一个单词的长度 +=== + + +### 题目 + +给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 + +单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 + + + +示例 1: + +- 输入:s = "Hello World" +- 输出:5 +- 解释:最后一个单词是“World”,长度为 5。 + +示例 2: + +- 输入:s = " fly me to the moon " +- 输出:4 +- 解释:最后一个单词是“moon”,长度为 4。 + +示例 3: + +- 输入:s = "luffy is still joyboy" +- 输出:6 +- 解释:最后一个单词是长度为 6 的“joyboy”。 + + +提示: + +- 1 <= s.length <= 104 +- s 仅有英文字母和空格 ' ' 组成 +- s 中至少存在一个单词 + +### 思路 + +从最后一个字母开始往前遍历,并开始计数,找到第一个空格的时候停止。要注意没有空格的情况,例如"ab",应该返回2。 + +```java +class Solution { + public int lengthOfLastWord(String s) { + int end = s.length() - 1; + while(end >= 0 && s.charAt(end) == ' ') end--; + if(end < 0) return 0; + int start = end; + while(start >= 0 && s.charAt(start) != ' ') start--; + return end - start; + } +} +``` + + +```python +class Solution: + def lengthOfLastWord(self, s: str) -> int: + length = 0 + for i in reversed(range(len(s.rstrip()))): + if s.rstrip()[i] == " ": + return length + else: + length += 1 + return length +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是字符串的长度。最多需要反向遍历字符串一次。 + +- 空间复杂度:O(1)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" "b/Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" new file mode 100644 index 00000000..ce72e6a5 --- /dev/null +++ "b/Algorithm/20.\346\234\200\351\225\277\345\205\254\345\205\261\345\211\215\347\274\200.md" @@ -0,0 +1,68 @@ +20.最长公共前缀 +=== + + +### 题目 + +编写一个函数来查找字符串数组中的最长公共前缀。 + +如果不存在公共前缀,返回空字符串 ""。 + + + +示例 1: + +- 输入:strs = ["flower","flow","flight"] +- 输出:"fl" + +示例 2: + +- 输入:strs = ["dog","racecar","car"] +- 输出:"" +- 解释:输入不存在公共前缀。 + + +提示: + +- 1 <= strs.length <= 200 +- 0 <= strs[i].length <= 200 +- strs[i] 如果非空,则仅由小写英文字母组成 + +### 思路 + + +###### 遍历每次找重合的前缀部分 + +横向扫描,依次遍历每个字符串,更新最长公共前缀 + +```python +class Solution: + def longestCommonPrefix(self, strs: List[str]) -> str: + ans = strs[0] + def compare(s1, s2) -> str: + result = "" + for i in range(min(len(s1), len(s2))): + if s1[i] == s2[i]: + result += s1[i] + else: + break + return result + + for i in range(1, len(strs)): + ans = compare(ans, strs[i]) + return ans +``` + + +复杂度分析: + +- 时间复杂度:O(mn),其中 m 是字符串数组中的字符串的平均长度,n 是字符串的数量。最坏情况下,字符串数组中的每个字符串的每个字符都会被比较一次。 + +- 空间复杂度:O(1)。使用的额外空间复杂度为常数。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" "b/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" new file mode 100644 index 00000000..1a5754c5 --- /dev/null +++ "b/Algorithm/21.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\345\215\225\350\257\215.md" @@ -0,0 +1,84 @@ +21.反转字符串中的单词 +=== + + +### 题目 + + +给你一个字符串 s ,请你反转字符串中 单词 的顺序。 + +单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。 + +返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。 + +注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。 + + + +示例 1: + +- 输入:s = "the sky is blue" +- 输出:"blue is sky the" + +示例 2: + +- 输入:s = " hello world " +- 输出:"world hello" +- 解释:反转后的字符串中不能存在前导空格和尾随空格。 + +示例 3: + +- 输入:s = "a good example" +- 输出:"example good a" +- 解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。 + + +提示: + +- 1 <= s.length <= 104 +- s 包含英文大小写字母、数字和空格 ' ' +- s 中 至少存在一个 单词 + + +### 思路 + +- 倒序遍历,记录每个单词的起始和结束长度 +- 结果中累加每个单词,注意在加下一个单词的时候需要提前加上空格 +- 遍历到最后一个空格或者第一个元素截止 +- 注意如果是第一个元素截止的时候需要考虑第一个元素是不是空格" ",例如: " asdasd df f" + - 如果是空格,需要从i + 1开始 + - 如果不是空格,需要从0开始 + +```python +class Solution: + def reverseWords(self, s: str) -> str: + length = len(s) + result = "" + last = -1 + + for i in reversed(range(length)): + if last == -1 and s[i] != " ": + last = i + if last > -1 and (s[i] == " " or i == 0): + if result != "": + result += " " + if i == 0 and s[i] != " ": + result += s[0: last + 1] + else: + result += s[i + 1: last + 1] + last = -1 + + return result +``` + + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 s 的长度,线性遍历字符串。 +- 空间复杂度 O(N) : 新建的 list(Python) 或 StringBuilder(Java) 中的字符串总长度 ≤N ,占用 O(N) 大小的额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" "b/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" new file mode 100644 index 00000000..d4f3e661 --- /dev/null +++ "b/Algorithm/22.Z\345\255\227\345\275\242\350\275\254\346\215\242.md" @@ -0,0 +1,106 @@ +22.Z字形转换 +=== + + +### 题目 + +将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。 + +比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下: +``` +P A H N +A P L S I I G +Y I R +``` +之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"。 + +请你实现这个将字符串进行指定行数变换的函数: + +string convert(string s, int numRows); + + +示例 1: + +- 输入:s = "PAYPALISHIRING", numRows = 3 +- 输出:"PAHNAPLSIIGYIR" + +示例 2: +- 输入:s = "PAYPALISHIRING", numRows = 4 +- 输出:"PINALSIGYAHRPI" +- 解释: +``` +P I N +A L S I G +Y A H R +P I +``` +示例 3: + +- 输入:s = "A", numRows = 1 +- 输出:"A" + + +提示: + +- 1 <= s.length <= 1000 +- s 由英文字母(小写和大写)、',' 和 '.' 组成 +- 1 <= numRows <= 1000 + +### 思路 + +找规律,假设s = "PAYPALISHIRING", numRows = 3 + +- 从前往后遍历s +- s[0] : 第一行 +- s[1] : 第二行 +- s[2] : 第三行, 大于等于numRows了,行要开始递减了 +- s[3] : 第二行, +- s[4] : 第一行, 到最小行了,行要开始递加了 +- s[5] : 第二行 +- ... + + +```python + +class Solution: + def convert(self, s: str, numRows: int) -> str: + if len(s) < 3 or numRows < 2: + return s + row = 1 + add = True + # 用一个数组记录每一行的字符内容 + rowArray = [""] * numRows + for i in s: + rowArray[row - 1] += i + + if add: + row += 1 + else: + row -= 1 + if row > numRows: + add = False + # 本来要减1因为上面刚加了1,所以这里要减2 + row -= 2 + elif row < 1: + add = True + row += 2 + + result = "" + for i in rowArray: + result += i + return result +``` + + + + +复杂度分析: + +- 时间复杂度 O(N) :遍历一遍字符串 s; +- 空间复杂度 O(N) :各行字符串共占用 O(N) 额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" "b/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" new file mode 100644 index 00000000..b3f118f9 --- /dev/null +++ "b/Algorithm/23.\346\211\276\345\207\272\345\255\227\347\254\246\344\270\262\344\270\255\347\254\254\344\270\200\344\270\252\345\214\271\351\205\215\351\241\271\347\232\204\344\270\213\346\240\207.md" @@ -0,0 +1,324 @@ +23.找出字符串中第一个匹配项的下标 +=== + + +### 题目 + +给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。 + + + +示例 1: + +- 输入:haystack = "sadbutsad", needle = "sad" +- 输出:0 +- 解释:"sad" 在下标 0 和 6 处匹配。 +- 第一个匹配项的下标是 0 ,所以返回 0 。 + +示例 2: + +- 输入:haystack = "leetcode", needle = "leeto" +- 输出:-1 +- 解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。 + + +提示: + +- 1 <= haystack.length, needle.length <= 104 +- haystack 和 needle 仅由小写英文字符组成 + +### 思路 + +##### 方法一: 普通对比 + +```python + +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + length = len(needle) + for i in range(len(haystack)): + if haystack[i] == needle[0]: + if haystack[i: i+length] == needle: + return i + return -1 +``` + + +复杂度分析 + +- 时间复杂度:O(n×m),其中 n 是字符串 haystack 的长度,m 是字符串 needle 的长度。最坏情况下我们需要将字符串 needle 与字符串 haystack 的所有长度为 m 的子串均匹配一次。 + +- 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。 + + +##### 方法二: KMP + + + +上述的朴素解法,不考虑剪枝的话复杂度是 O(m∗n) 的,而 KMP 算法的复杂度为 O(m+n)。 + +KMP算法是一种字符串匹配算法,可以在 O(n+m) 的时间复杂度内实现两个字符串的匹配。 + +KMP 之所以能够在 O(m+n) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。 + + +KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。 + +###### 算法原理 + +从主字符串的第一个字符开始:KMP算法从主字符串的第一个字符开始,将其与子字符串的第一个字符进行比较。 相等与不等的情况:如果字符相等,则继续比较后续字符;如果不等,则根据部分匹配表(即next数组)的值,将子字符串向右移动若干个字符,然后再次进行比较。 + + +###### 算法效率 + +- 时间复杂度:KMP算法的时间复杂度为O(m+n),其中m和n分别是模式串和主串的长度。相较于O(n^2)的暴力匹配算法,KMP算法具有较高的效率。 + + + +###### KMP算法的本质 + +理解计算next数组是核心。 + + +next数组是匹配串的一个查找表,它的定义可以用下面一句话来解释。就是kmp算法的本质: + +***next数组的每个元素表示匹配串中从起始到以当前字符结尾的子串中以当前字符结尾的连续重复最长串长度。*** + +字符串abcdabe,len是每个子串以最后一个字符结尾的连续重复最长串长度: + +- next[0] = 0 // 子串'a'中没有包含以'a'结尾的连续重复子串,len = 0 +- next[1] = 0 // 子串'ab'中没有包含以'b'结尾的连续重复子串,len = 0 +- next[2] = 0 // 子串'abc'中没有包含以'c'结尾的连续重复子串,len = 0 +- next[3] = 0 // 子串'abcd'中没有包含以'd'结尾的连续重复子串,len = 0 +- next[4] = 1 // 子串'abcda'中包含以'a'结尾的连续重复子串是'a',len = 1 +- next[5] = 2 // 子串'abcdab'中包含以'b'结尾的连续重复子串'ab',len = 2 +- next[6] = 0 // 子串'abcdabe'中没有包含以'e'结尾的连续重复子串,len = 0 + +匹配过程与计算next的思路相似,如果当前字符不匹配,就往回跳,跳多少呢,就是前面已比较串的以最后一个字符结尾的连续最长重复长度,最长也就一半,不用跳到开头,即next[j-1]的长度,不用再重头比较, + + + + + +所谓字符串匹配,是这样一种问题: 字符串P是否为字符串S的子串?如果是,它出现在S的哪个位置。 + +- S称为主串。 +- P称为模式串。 + + +最简单的方法就是上面的方法一,不断的去遍历查找。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force.png?raw=true) + + +这就是Brute-Force 算法。现在,我们需要对它的时间复杂度做一点讨论,它的时间复杂度是O(n×m)。 + +我们很难降低字符串比较的复杂度(因为比较两个字符串,真的只能逐个比较字符)。 + +因此,我们考虑降低比较的趟数。如果比较的趟数能降到足够低,那么总的复杂度也将会下降很多。 + + +要优化一个算法,首先要回答的问题是“我手上有什么信息?” 我们手上的信息是否足够、是否有效,决定了我们能把算法优化到何种程度。请记住:尽可能利用残余的信息,是KMP算法的思想所在。 + + +在Brute-Force中,如果从S[i]开始的那一趟比较失败了,算法会直接开始尝试从S[i+1]开始比较。 + +这种行为,属于典型的“没有从之前的错误中学到东西”。 + +我们应当注意到,一次失败的匹配,会给我们提供宝贵的信息: + +- 如果 S[i : i+len(P)] 与 P 的匹配是在第 r 个位置失败的,那么从 S[i] 开始的 (r-1) 个连续字符,一定与 P 的前 (r-1) 个字符一模一样! +- 为什么呢? 因为不然得话你在r之前就失败了。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_2.png?raw=true) + + +需要实现的任务是“字符串匹配”,而每一次失败都会给我们换来一些信息——能告诉我们,主串的某一个子串等于模式串的某一个前缀。但是这又有什么用呢? + +有些趟字符串比较是有可能会成功的;有些则毫无可能。 + +我们刚刚提到过,优化 Brute-Force的路线是“尽量减少比较的趟数”,而如果我们跳过那些绝不可能成功的字符串比较,则可以希望复杂度降低到能接受的范围。   + +那么,哪些字符串比较是不可能成功的?来看一个例子。已知信息如下: + +- 模式串 P = "abcabd". +- 和主串从S[0]开始匹配时,在 P[5] 处失配。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_3.png?raw=true) + +- 既然是在 P[5] 失配的,那么说明 S[0:5] 等于 P[0:5],即"abcab". + +- 现在我们来考虑:从 S[1]、S[2]、S[3] 开始的匹配尝试,有没有可能成功?   + +- 从 S[1] 开始肯定没办法成功,因为 S[1] = P[1] = 'b',和 P[0] 并不相等。 + +- 从 S[2] 开始也是没戏的,因为 S[2] = P[2] = 'c',并不等于P[0]. + +- 但是从 S[3] 开始是有可能成功的——至少按照已知的信息,我们推不出矛盾。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_4.png?raw=true) + + +也就是说当主串的某个字符c发生不匹配时,如果主串回退,最终还是会重新匹配到字符c上。 +那干脆不回退,岂不美哉! +也就是说主串一直遍历不回退。 +主串不回退,那模式串必须回退尽可能少,并且模式串回退位置的前面那段已经和主串匹配,这样主串才能不用回退。 + +如何找到模式串回退的位置呢? + +在不匹配发生时,前面匹配的那一小段字符对于主串和模式串都是相同的(如果不相同,在这之前会就匹配失败了)。 +那既然这一小段是主串和模式串相同的。那我就用这个串的头部去匹配这个串的尾部,最长的那段就是答案,也就是模式串改回退到的位置。 + + +***带着“跳过不可能成功的尝试”的思想,我们来看next数组。*** + +那就假设模式串在其所有位置上都发生了不匹配,模式串在和主串匹配前把其所有位置的最长匹配都算出来(算个长度就行),生成一张表,之后我俩发生不匹配时直接查这张表就行。这就是next数组。 + +###### next数组 + +next数组是对于***模式串***而言的。 + +P 的 next 数组定义为:next[i] 表示 P[0] ~ P[i] 这一个子串,使得***前k个字符恰等于后k个字符的最大的k***. 特别地,k不能取i+1(因为这个子串一共才 i+1 个字符,自己肯定与自己相等,就没有意义了)。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_5.png?raw=true) + +上图给出了一个例子。P="abcabd"时,next[4]=2,这是因为P[0] ~ P[4] 这个子串是"abcab",前两个字符与后两个字符相等,因此next[4]取2. + +而next[5]=0,是因为"abcabd"找不到前缀与后缀相同,因此只能取0. + +如果把模式串视为一把标尺,在主串上移动,那么 Brute-Force 就是每次失配之后只右移一位;改进算法则是每次失配之后,移很多位,跳过那些不可能匹配成功的位置。但是该如何确定要移多少位呢? + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_6.png?raw=true) + + +- 在 S[0] 尝试匹配,失配于 S[3] != P[3] 之后,我们直接把模式串往右移了两位,让 S[3] 对准 P[1]. + +- 接着继续匹配,失配于 S[8] != P[6], 接下来我们把 P 往右平移了三位,把 S[8] 对准 P[3]. + +- 此后继续匹配直到成功。   + + +我们应该如何移动这把标尺?很明显,如图中蓝色箭头所示,旧的后缀要与新的前缀一致(如果不一致,那就肯定没法匹配上了)!   + + +--- +回忆next数组的性质:P[0] 到 P[i] 这一段子串中,前next[i]个字符与后next[i]个字符一模一样。 + +既然如此,如果失配在`P[r]`, 那么`P[0]~P[r-1]`这一段里面,前`next[r-1]`个字符恰好和后`next[r-1]`个字符相等——也就是说,我们可以拿长度为`next[r-1]`的那一段前缀,来顶替当前后缀的位置,让匹配继续下去。 + + +您可以验证一下上面的匹配例子:P[3]失配后,把P[next[3-1]]也就是P[1]对准了主串刚刚失配的那一位;P[6]失配后,把P[next[6-1]]也就是P[3]对准了主串刚刚失配的那一位。 + +--- + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_7.png?raw=true) + + + +如上图所示,绿色部分是成功匹配,失配于红色部分。深绿色手绘线条标出了相等的前缀和后缀,其长度为next[右端]. 由于手绘线条部分的字符是一样的,所以直接把前面那条移到后面那条的位置。因此说,next数组为我们如何移动标尺提供了依据。接下来,我们实现这个优化的算法。 + +了解了利用next数组加速字符串匹配的原理,我们接下来代码实现之。分为两个部分: + +- 建立next数组 + +- 利用next数组进行匹配。 + + +首先是建立next数组。我们暂且用最朴素的做法,以后再回来优化: + +```python + +def getNxt(x): + for i in range(x, 0, -1): + if p[0: 1] == p[x-i+1:x+1]: + return i + return 0 + +nxt = [getNxt(x) for x in range(len(p))] +``` + +如上图代码所示,直接根据next数组的定义来建立next数组。不难发现它的复杂度是 的。 +  接下来,实现利用next数组加速字符串匹配。代码如下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_8.png?raw=true) + + + +###### 快速求next数组 + + +终于来到了我们最后一个问题——如何快速构建next数组。   + +- 首先说一句:快速构建next数组,是KMP算法的精髓所在,核心思想是“P自己与自己做匹配”。  + +为什么这样说呢?回顾next数组的完整定义: + +- 定义 “k-前缀” 为一个字符串的前 k 个字符; “k-后缀” 为一个字符串的后 k 个字符。k 必须小于字符串长度。 + +- next[x]定义为:`P[0]~P[x]`这一段字符串,使得k-前缀恰等于k-后缀的最大的k.   + +这个定义中,不知不觉地就包含了一个匹配——前缀和后缀相等。 + +接下来,我们考虑采用递推的方式求出next数组。如果next[0], next[1], ... next[x-1]均已知,那么如何求出 next[x] 呢?   + +来分情况讨论。首先,已经知道了 next[x-1](以下记为now),如果 P[x] 与 P[now] 一样,那最长相等前后缀的长度就可以扩展一位,很明显 next[x] = now + 1. 图示如下。 +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_9.png?raw=true) + +刚刚解决了 P[x] = P[now] 的情况。那如果 P[x] 与 P[now] 不一样,又该怎么办? + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_10.png?raw=true) + +如图。 + +长度为now的子串A和子串B是`P[0]~P[x-1]`中最长的公共前后缀。 + +可惜 A 右边的字符和 B 右边的那个字符不相等,next[x]不能改成now+1了。 + +因此,我们应该缩短这个now,把它改成小一点的值,再来试试 P[x] 是否等于 P[now].   + +now该缩小到多少呢?显然,我们不想让now缩小太多。因此我们决定,在保持`P[0]~P[x-1]`的now-前缀仍然等于now-后缀”的前提下,让这个新的now尽可能大一点。 + + +`P[0]~P[x-1]`的公共前后缀,前缀一定落在串A里面、后缀一定落在串B里面。 + +换句话讲:接下来now应该改成:使得 A的k-前缀等于B的k-后缀 的最大的k.   + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_14.png?raw=true) + +也就是说:假设这个时候最大的前缀和后缀是字符串X,那将这个前缀和后缀去掉一个字符(去掉c)后,得到的两个新的串也必然是相等的。 +也就是只可能是现有最大串的子串。也就是a的前缀和b的后缀(而这个时候a和b是一样的,所以就变成找A中的前缀和后缀一样的子串)。 + +您应该已经注意到了一个非常强的性质——串A和串B是相同的!B的后缀等于A的后缀!因此,使得A的k-前缀等于B的k-后缀的最大的k,其实就是串A的最长公共前后缀的长度 —— next[now-1]! + +简单说就是: 次大匹配必定在最大匹配中 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_11.png?raw=true) + + +来看上面的例子。当P[now]与P[x]不相等的时候,我们需要缩小now——把now变成next[now-1],直到P[now]=P[x]为止。P[now]=P[x]时,就可以直接向右扩展了。 + + +代码实现如下: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_12.png?raw=true) + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/Brute-Force_13.png?raw=true) + + + +https://www.zhihu.com/question/21923021/answer/281346746 + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" new file mode 100644 index 00000000..322b9ff6 --- /dev/null +++ "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" @@ -0,0 +1,80 @@ +24.文本左右对齐 +=== + + +### 题目 + +给定一个单词数组 words 和一个长度 maxWidth ,重新排版单词,使其成为每行恰好有 maxWidth 个字符,且左右两端对齐的文本。 + +你应该使用 “贪心算法” 来放置给定的单词;也就是说,尽可能多地往每行中放置单词。必要时可用空格 ' ' 填充,使得每行恰好有 maxWidth 个字符。 + +要求尽可能均匀分配单词间的空格数量。如果某一行单词间的空格不能均匀分配,则左侧放置的空格数要多于右侧的空格数。 + +文本的最后一行应为左对齐,且单词之间不插入额外的空格。 + +注意: + +- 单词是指由非空格字符组成的字符序列。 +- 每个单词的长度大于 0,小于等于 maxWidth。 +- 输入单词数组 words 至少包含一个单词。 + + +示例 1: + +- 输入: words = ["This", "is", "an", "example", "of", "text", "justification."], maxWidth = 16 +- 输出: +``` +[ + "This is an", + "example of text", + "justification. " +] +``` +示例 2: + +- 输入:words = ["What","must","be","acknowledgment","shall","be"], maxWidth = 16 +- 输出: +``` +[ + "What must be", + "acknowledgment ", + "shall be " +] +``` +- 解释: + - 注意最后一行的格式应为 "shall be " 而不是 "shall be", + - 因为最后一行应为左对齐,而不是左右两端对齐。 + - 第二行同样为左对齐,这是因为这行只包含一个单词。 + +示例 3: + +- 输入:words = ["Science","is","what","we","understand","well","enough","to","explain","to","a","computer.","Art","is","everything","else","we","do"],maxWidth = 20 +- 输出: +``` +[ + "Science is what we", + "understand well", + "enough to explain to", + "a computer. Art is", + "everything else we", + "do " +] +``` + +提示: + +- 1 <= words.length <= 300 +- 1 <= words[i].length <= 20 +- words[i] 由小写英文字母和符号组成 +- 1 <= maxWidth <= 100 +- words[i].length <= maxWidth + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" new file mode 100644 index 00000000..29134c67 --- /dev/null +++ "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" @@ -0,0 +1,48 @@ +25.验证回文串 +=== + + +### 题目 + +如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。 + +字母和数字都属于字母数字字符。 + +给你一个字符串 s,如果它是 回文串 ,返回 true ;否则,返回 false 。 + + + +示例 1: + +- 输入: s = "A man, a plan, a canal: Panama" +- 输出:true +- 解释:"amanaplanacanalpanama" 是回文串。 + +示例 2: + +- 输入:s = "race a car" +- 输出:false +- 解释:"raceacar" 不是回文串。 +示例 3: + +- 输入:s = " " +- 输出:true +- 解释:在移除非字母数字字符之后,s 是一个空字符串 "" 。 +- 由于空字符串正着反着读都一样,所以是回文串。 + + +提示: + +- 1 <= s.length <= 2 * 105 +- s 仅由可打印的 ASCII 字符组成 + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/26..md b/Algorithm/26..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/26..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/27..md b/Algorithm/27..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/27..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + From 540c62788838176d8acb2e9080b16f1a5dcf74a8 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 18 Jun 2025 15:20:59 +0800 Subject: [PATCH 122/128] add notes --- ...46\345\217\263\345\257\271\351\275\220.md" | 80 +++++++++++ ...01\345\233\236\346\226\207\344\270\262.md" | 56 ++++++++ ...55\345\255\220\345\272\217\345\210\227.md" | 129 ++++++++++++++++++ ...11\345\272\217\346\225\260\347\273\204.md" | 97 +++++++++++++ ...64\347\232\204\345\256\271\345\231\250.md" | 73 ++++++++++ ...11\346\225\260\344\271\213\345\222\214.md" | 109 +++++++++++++++ ...04\345\255\220\346\225\260\347\273\204.md" | 102 ++++++++++++++ ...00\351\225\277\345\255\220\344\270\262.md" | 107 +++++++++++++++ ...15\347\232\204\345\255\220\344\270\262.md" | 59 ++++++++ Algorithm/{26..md => 33..md} | 0 Algorithm/{27..md => 34..md} | 0 Algorithm/35..md | 17 +++ 12 files changed, 829 insertions(+) create mode 100644 "Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" create mode 100644 "Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" create mode 100644 "Algorithm/28.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" create mode 100644 "Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" create mode 100644 "Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" create mode 100644 "Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" create mode 100644 "Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" rename Algorithm/{26..md => 33..md} (100%) rename Algorithm/{27..md => 34..md} (100%) create mode 100644 Algorithm/35..md diff --git "a/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" index 322b9ff6..ffde6522 100644 --- "a/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" +++ "b/Algorithm/24.\346\226\207\346\234\254\345\267\246\345\217\263\345\257\271\351\275\220.md" @@ -71,6 +71,86 @@ ### 思路 +字符串大模拟,分情况讨论即可: + +- 如果当前行只有一个单词,特殊处理为左对齐; +- 如果当前行为最后一行,特殊处理为左对齐; +- 其余为一般情况,分别计算「当前行单词总长度」、「当前行空格总长度」和「往下取整后的单位空格长度」,然后依次进行拼接。当空格无法均分时,每次往靠左的间隙多添加一个空格,直到剩余的空格能够被后面的间隙所均分。 + +```java + +class Solution { + public List fullJustify(String[] words, int maxWidth) { + List ans = new ArrayList<>(); + int n = words.length; + List list = new ArrayList<>(); + for (int i = 0; i < n; ) { + // list 装载当前行的所有 word + list.clear(); + list.add(words[i]); + int cur = words[i++].length(); + while (i < n && cur + 1 + words[i].length() <= maxWidth) { + cur += 1 + words[i].length(); + list.add(words[i++]); + } + + // 当前行为最后一行,特殊处理为左对齐 + if (i == n) { + StringBuilder sb = new StringBuilder(list.get(0)); + for (int k = 1; k < list.size(); k++) { + sb.append(" ").append(list.get(k)); + } + while (sb.length() < maxWidth) sb.append(" "); + ans.add(sb.toString()); + break; + } + + // 如果当前行只有一个 word,特殊处理为左对齐 + int cnt = list.size(); + if (cnt == 1) { + String str = list.get(0); + while (str.length() != maxWidth) str += " "; + ans.add(str); + continue; + } + + /** + * 其余为一般情况 + * wordWidth : 当前行单词总长度; + * spaceWidth : 当前行空格总长度; + * spaceItem : 往下取整后的单位空格长度 + */ + int wordWidth = cur - (cnt - 1); + int spaceWidth = maxWidth - wordWidth; + int spaceItemWidth = spaceWidth / (cnt - 1); + String spaceItem = ""; + for (int k = 0; k < spaceItemWidth; k++) spaceItem += " "; + StringBuilder sb = new StringBuilder(); + for (int k = 0, sum = 0; k < cnt; k++) { + String item = list.get(k); + sb.append(item); + if (k == cnt - 1) break; + sb.append(spaceItem); + sum += spaceItemWidth; + // 剩余的间隙数量(可填入空格的次数) + int remain = cnt - k - 1 - 1; + // 剩余间隙数量 * 最小单位空格长度 + 当前空格长度 < 单词总长度,则在当前间隙多补充一个空格 + if (remain * spaceItemWidth + sum < spaceWidth) { + sb.append(" "); + sum++; + } + } + ans.add(sb.toString()); + } + return ans; + } +} +``` + +复杂度分析: + +- 时间复杂度:会对 words 做线性扫描,最坏情况下每个 words[i] 独占一行,此时所有字符串的长度为 n∗maxWidth。复杂度为 O(n∗maxWidth) +- 空间复杂度:最坏情况下每个 words[i] 独占一行,复杂度为 O(n∗maxWidth) --- diff --git "a/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" index 29134c67..b8720d78 100644 --- "a/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" +++ "b/Algorithm/25.\351\252\214\350\257\201\345\233\236\346\226\207\344\270\262.md" @@ -39,7 +39,63 @@ ### 思路 +##### 方法一 +最简单的方法是对字符串 s 进行一次遍历,并将其中的字母和数字字符进行保留,放在另一个字符串 sgood 中。这样我们只需要判断 sgood 是否是一个普通的回文串即可。 + +判断的方法有两种: + +- 第一种是使用语言中的字符串翻转 API 得到 sgood 的逆序字符串 sgood_rev,只要这两个字符串相同,那么 sgood 就是回文串。 +- 第二种是使用双指针。初始时,左右指针分别指向 sgood 的两侧,随后我们不断地将这两个指针相向移动,每次移动一步,并判断这两个指针指向的字符是否相同。当这两个指针相遇时,就说明 sgood 时回文串。 + +```java +class Solution { + public boolean isPalindrome(String s) { + StringBuffer sgood = new StringBuffer(); + int length = s.length(); + for (int i = 0; i < length; i++) { + char ch = s.charAt(i); + if (Character.isLetterOrDigit(ch)) { + sgood.append(Character.toLowerCase(ch)); + } + } + int n = sgood.length(); + int left = 0, right = n - 1; + while (left < right) { + if (Character.toLowerCase(sgood.charAt(left)) != Character.toLowerCase(sgood.charAt(right))) { + return false; + } + ++left; + --right; + } + return true; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中n是字符串s的长度。 + +- 空间复杂度:O(n)。由于我们需要将所有的字母和数字字符存放在另一个字符串中,在最坏情况下,新的字符串sgood与原字符串s完全相同,因此需要使用 O(n) 的空间。 + + +##### 方法二:在原字符串上直接判断 + +我们可以对方法一中第二种判断回文串的方法进行优化,就可以得到只使用 O(1) 空间的算法。 + +我们直接在原字符串 s 上使用双指针。在移动任意一个指针时,需要不断地向另一指针的方向移动,直到遇到一个字母或数字字符,或者两指针重合为止。 + +也就是说,我们每次将指针移到下一个字母字符或数字字符,再判断这两个指针指向的字符是否相同。 + + + + +复杂度分析: + +- 时间复杂度:O(n),其中n是字符串s的长度。 + +- 空间复杂度:O(1)。 --- - 邮箱 :charon.chui@gmail.com diff --git "a/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" "b/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" new file mode 100644 index 00000000..77ea7063 --- /dev/null +++ "b/Algorithm/26.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,129 @@ +26.判断子序列 +=== + + +### 题目 + +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + +进阶: + +如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码? + + +示例 1: + +- 输入:s = "abc", t = "ahbgdc" +- 输出:true + +示例 2: + +- 输入:s = "axc", t = "ahbgdc" +- 输出:false + + +提示: + +- 0 <= s.length <= 100 +- 0 <= t.length <= 10^4 +- 两个字符串都只由小写字符组成。 + +### 思路 + + +首先,如果 s 是空串,直接返回 true,因为空串是任何字符串的子序列。 +设置双指针 i , j 分别指向字符串 s , t 的首个字符,遍历字符串 t: + +- 当 s[i] == t[j] 时,代表匹配成功,此时同时 i++ , j++ ; + - 进而,若 i 已走过 s 尾部,代表 s 是 t 的子序列,此时应提前返回 true ; +- 当 s[i] != t[j] 时,代表匹配失败,此时仅 j++ ; + +若遍历完字符串 t 后,字符串 s 仍未遍历完,代表 s 不是 t 的子序列,此时返回 false 。 + +```java + +class Solution { + public boolean isSubsequence(String s, String t) { + int i = 0, j = 0; + while (i < s.length() && j < t.length()) { + if (s.charAt(i) == t.charAt(j)) { + i++; + } + j++; + } + return i == s.length(); + } +} +``` + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 t 的长度。最差情况下需完整遍历 t 。 +- 空间复杂度 O(1) : i , j 变量使用常数大小空间。 + + +##### 进阶问题解法 + +如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码? + + +这种类似对同一个长字符串做很多次匹配的 ,可以像 KMP 算法一样,先用一些时间将长字符串中的数据 提取出来,磨刀不误砍柴功。有了提取好的数据,就可以快速的进行匹配。 + +因为S非常多,所以可以通过一次性对T进行简化处理,这样来减少后续每一次S匹配T的遍历时间: + +- 这里需要的数据就是匹配到某一点时,待匹配的字符在长字符串中 下一次 出现的位置。 + +- 所以我们前期多做一点工作,将长字符串研究透彻,假如长字符串的长度为 n,建立一个 n∗26 大小的矩阵,表示每个位置上26个字符下一次出现的位置。实现如下: + +- 对于要匹配的短字符串,遍历每一个字符,不断地寻找该字符在长字符串中的位置,然后将位置更新,寻找下一个字符,相当于在长字符串上“跳跃”。 + +- 如果下一个位置为 -1,表示长字符串再没有该字符了,返回 false 即可。 + +- 如果能正常遍历完毕,则表示可行,返回 true + +- 需要注意的一点 + + - 对于 "abc" 在 "ahbgdc" 上匹配的时候,由于长字符串第一个 a 的下一个出现 a 的位置为 -1(不出现),会导致一个 bug。 + + - 所以在生成数组时在长字符串前插入一个空字符即可。 + +```java + +//进阶问题的解决 +public boolean isSubsequence(String s, String t) { + + //考虑到 对第一个字符的处理 ,在t 之前一个空字符 + t=' '+t; + + //对t长字符串 做预处理 + int[][] dp = new int[t.length()][26];//存储每一个位置上 a--z的下一个字符出现的位置 + for (char c = 'a'; c <= 'z'; c++) {//依次对每个字符作处理 + int nextPos = -1;//表示接下来不会在出现该字符 + + for (int i = t.length() - 1; i >= 0; i--) {//从最后一位开始处理 + dp[i][c - 'a'] = nextPos;//dp[i][c-'a'] 加上外层循环 就是对每一个位置的a---z字符的处理了 + if (t.charAt(i) == c) {//表示当前位置有该字符 那么指向下一个该字符出现的位置就要被更新 为i + nextPos = i; + } + } + } + + //数据的利用 ,开始匹配 + int index=0; + for (char c:s.toCharArray()){ + index=dp[index][c-'a'];//因为加了' ',所以之后在处理第一个字符的时候 如果是在第一行,就会去第一行,不影响之后字符的判断 + if(index==-1){ + return false; + } + } + return true; +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" "b/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" new file mode 100644 index 00000000..b879cd8b --- /dev/null +++ "b/Algorithm/27.\344\270\244\346\225\260\344\271\213\345\222\214 II - \350\276\223\345\205\245\346\234\211\345\272\217\346\225\260\347\273\204.md" @@ -0,0 +1,97 @@ +27.两数之和 II - 输入有序数组 +=== + + +### 题目 + +给你一个下标从1开始的整数数组numbers,该数组已按非递减顺序排列,请你从数组中找出满足相加之和等于目标数target的两个数。 + +如果设这两个数分别是numbers[index1]和numbers[index2],则`1 <= index1 < index2 <= numbers.length`。 + +以长度为`2`的整数数组`[index1, index2]`的形式返回这两个整数的下标`index1`和`index2`。 + +你可以假设每个输入只对应唯一的答案,而且你不可以重复使用相同的元素。 + +你所设计的解决方案必须只使用常量级的额外空间。 + + +示例 1: + +- 输入:numbers = [2,7,11,15], target = 9 +- 输出:[1,2] +- 解释:2 与 7 之和等于目标数 9 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。 + +示例 2: + +- 输入:numbers = [2,3,4], target = 6 +- 输出:[1,3] +- 解释:2 与 4 之和等于目标数 6 。因此 index1 = 1, index2 = 3 。返回 [1, 3] 。 + +示例 3: + +- 输入:numbers = [-1,0], target = -1 +- 输出:[1,2] +- 解释:-1 与 0 之和等于目标数 -1 。因此 index1 = 1, index2 = 2 。返回 [1, 2] 。 + + +提示: + +- 2 <= numbers.length <= 3 * 104 +- -1000 <= numbers[i] <= 1000 +- numbers 按 非递减顺序 排列 +- -1000 <= target <= 1000 +- 仅存在一个有效答案 + +### 思路 + +注意题目说仅存在一个有效答案 + + +初始时两个指针分别指向第一个元素位置和最后一个元素的位置。每次计算两个指针指向的两个元素之和,并和目标值比较。如果两个元素之和等于目标值,则发现了唯一解。如果两个元素之和小于目标值,则将左侧指针右移一位。如果两个元素之和大于目标值,则将右侧指针左移一位。移动指针之后,重复上述操作,直到找到答案。 + +使用双指针的实质是缩小查找范围。那么会不会把可能的解过滤掉?答案是不会。 + +假设`numbers[i]+numbers[j]=target`是唯一解,其中`0≤itarget`,因此一定是右指针左移,左指针不可能移到 i 的右侧。 + +- 如果右指针先到达下标 j 的位置,此时左指针还在下标 i 的左侧,`sum List[int]: + left = 0 + right = len(numbers) - 1 + result = [-1]*2 + while left < right: + sum = numbers[left] + numbers[right] + if sum < target: + left += 1 + elif sum > target: + right -= 1 + else: + return [left+1, right+1] + + return [-1, -1] +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组的长度。两个指针移动的总次数最多为 n 次。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/28.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" "b/Algorithm/28.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" new file mode 100644 index 00000000..d8f7ea6e --- /dev/null +++ "b/Algorithm/28.\347\233\233\346\234\200\345\244\232\346\260\264\347\232\204\345\256\271\345\231\250.md" @@ -0,0 +1,73 @@ +28.盛最多水的容器 +=== + + +### 题目 + +给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。 + +找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。 + +返回容器可以储存的最大水量。 + +说明:你不能倾斜容器。 + + + +示例 1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_28_1.png?raw=true) + +- 输入:[1,8,6,2,5,4,8,3,7] +- 输出:49 +- 解释:图中垂直线代表输入数组 [1,8,6,2,5,4,8,3,7]。在此情况下,容器能够容纳水(表示为蓝色部分)的最大值为 49。 + +示例 2: + +- 输入:height = [1,1] +- 输出:1 + + +提示: + +- n == height.length +- 2 <= n <= 105 +- 0 <= height[i] <= 104 + +### 思路 + +- 求出两个index1和index2,其中index < index2,使得min(height[index1], height[index2]) * (index2 - index1)最大 +- 核心就是:优先移动短板指针,因为面积是由短板的高度决定。 + +在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 −1​ 变短: + +- 若向内 移动短板 ,水槽的短板 min(h[i],h[j]) 可能变大,因此下个水槽的面积 可能增大 。 +- 若向内 移动长板 ,水槽的短板 min(h[i],h[j])​ 不变或变小,因此下个水槽的面积 一定变小 。 + +因此,初始化双指针分列水槽左右两端,循环每轮将短板向内移动一格,并更新面积最大值,直到两指针相遇时跳出;即可获得最大面积。 + + +```python +class Solution: + def maxArea(self, height: List[int]) -> int: + i, j, res = 0, len(height) - 1, 0 + while i < j: + if height[i] < height[j]: + res = max(res, height[i] * (j - i)) + i += 1 + else: + res = max(res, height[j] * (j - i)) + j -= 1 + return res +``` + +复杂度分析: + +- 时间复杂度 O(N)​ : 双指针遍历一次底边宽度 N​​ 。 +- 空间复杂度 O(1)​ : 变量 i , j , res 使用常数额外空间。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..fae1fd31 --- /dev/null +++ "b/Algorithm/29.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,109 @@ +29.三数之和 +=== + + +### 题目 + + +给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请你返回所有和为 0 且不重复的三元组。 + +注意:答案中不可以包含重复的三元组。 + + + + + +示例 1: + +- 输入:nums = [-1,0,1,2,-1,-4] +- 输出:[[-1,-1,2],[-1,0,1]] +- 解释: + - nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。 + - nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。 + - nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。 +- 不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。 +注意,输出的顺序和三元组的顺序并不重要。 + +示例 2: + +- 输入:nums = [0,1,1] +- 输出:[] +- 解释:唯一可能的三元组和不为 0 。 + +示例 3: + +- 输入:nums = [0,0,0] +- 输出:[[0,0,0]] +- 解释:唯一可能的三元组和为 0 。 + + +提示: + +- 3 <= nums.length <= 3000 +- -105 <= nums[i] <= 105 + + +### 思路 + + +- 特判,对于数组长度 n,如果数组为 null 或者数组长度小于 3,返回 []。 +- 对数组进行从小到大的排序。 +- 从0到nums.length - 2(因为一共三个数,后面还有有两个数)遍历排序后数组: + - 若 nums[i]>0:因为已经排序好,所以后面不可能有三个数加和等于 0,直接返回结果。 + - 对于重复元素:跳过,避免出现重复解 + - 令左指针 L=i+1,右指针 R=n−1,当 L List[List[int]]: + res = [] + if not nums or len(nums) < 3: + return res + nums = sorted(nums) + for i in range(len(nums) - 2): + if nums[i] > 0: + return res + if i > 0 and nums[i] == nums[i - 1]: + continue + + left = i + 1 + right = len(nums) - 1 + while left < right: + result = nums[i] + nums[left] + nums[right] + if result > 0: + # 需要right减小 + right -= 1 + elif result < 0: + left += 1 + else: + res.append([nums[i], nums[left], nums[right]]) + # 去重复,判断左边或右边的元素是否与当前的相同,相同就夸脱 + while left < right and nums[left] == nums[left + 1]: + left += 1 + + while left < right and nums[right] == nums[right - 1]: + right -= 1 + # 移动指针 + left += 1 + right -= 1 + return res + +``` + + +复杂度分析: + +- 时间复杂度:O(n²),数组排序 O(NlogN),遍历数组 O(n),双指针遍历 O(n),总体 O(NlogN)+O(n)∗O(n),O(n²) +- 空间复杂度:O(1) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" new file mode 100644 index 00000000..c04f8f5d --- /dev/null +++ "b/Algorithm/30.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" @@ -0,0 +1,102 @@ +30.长度最小的子数组 +=== + + +### 题目 + +给定一个含有 n 个正整数的数组和一个正整数 target 。 + +找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。 + + + +示例 1: + +- 输入:target = 7, nums = [2,3,1,2,4,3] +- 输出:2 +- 解释:子数组 [4,3] 是该条件下的长度最小的子数组。 + +示例 2: + +- 输入:target = 4, nums = [1,4,4] +- 输出:1 + +示例 3: + +- 输入:target = 11, nums = [1,1,1,1,1,1,1,1] +- 输出:0 + + +提示: + +- 1 <= target <= 109 +- 1 <= nums.length <= 105 +- 1 <= nums[i] <= 104 + + +进阶: + +如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。 + + + +### 思路 + +注意: +子数组 是数组中连续的 非空 元素序列。 + +所以下面排序的方法是错误的。 因为不连续 +```python +class Solution: + def minSubArrayLen(self, target, nums) -> int: + nums = sorted(nums, reverse=True) + result = 0 + total = 0 + for i in nums: + print(i) + total += i + result += 1 + print(total) + if total >= target: + print(result) + return result + return result +``` + + +2.使用队列相加(实际上我们也可以把它称作是滑动窗口,这里的队列其实就相当于一个窗口) + +我们把数组中的元素不停的入队,直到总和大于等于 s 为止,接着记录下队列中元素的个数,然后再不停的出队,直到队列中元素的和小于 s 为止(如果不小于 s,也要记录下队列中元素的个数,这个个数其实就是不小于 s 的连续子数组长度,我们要记录最小的即可)。接着再把数组中的元素添加到队列中……重复上面的操作,直到数组中的元素全部使用完为止。 + +```java +class Solution { + public int minSubArrayLen(int s, int[] nums) { + int leftIndex = 0; + int rightIndex = 0; + int sum = 0; + int min = Integer.MAX_VALUE; + while (rightIndex < nums.length) { + sum += nums[rightIndex++]; + while (sum >= s) { + min = Math.min(min, rightIndex - leftIndex); + sum -= nums[leftIndex++]; + } + } + return min == Integer.MAX_VALUE ? 0 : min; + } +} +``` + + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组的长度。指针 start 和 end 最多各移动 n 次。 + +- 空间复杂度:O(1)。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" "b/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" new file mode 100644 index 00000000..32e4a899 --- /dev/null +++ "b/Algorithm/31.\346\227\240\351\207\215\345\244\215\345\255\227\347\254\246\347\232\204\346\234\200\351\225\277\345\255\220\344\270\262.md" @@ -0,0 +1,107 @@ +31.无重复字符的最长子串 +=== + + +### 题目 + +给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串 的长度。 + + + +示例 1: + +- 输入: s = "abcabcbb" +- 输出: 3 +- 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。 + +示例 2: + +- 输入: s = "bbbbb" +- 输出: 1 +- 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。 + +示例 3: + +- 输入: s = "pwwkew" +- 输出: 3 +- 解释: + - 因为无重复字符的最长子串是 "wke",所以其长度为 3。 + - 请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。 + + +提示: + +- 0 <= s.length <= 5 * 104 +- s 由英文字母、数字、符号和空格组成 + + +### 思路 + +这道题主要用到思路是:滑动窗口 + +什么是滑动窗口? + +其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列! + +如何移动? + +我们只要把队列的左边的元素移出就行了,直到满足题目要求! + +一直维持这样的队列,找出队列出现最长的长度时候,求出解! + + + +```python +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + leftIndex = 0 + rightIndex = 0 + + # pwwkew + # "abcabcbb" + result = 0 + while rightIndex < len(s): + index = s[leftIndex: rightIndex].find(s[rightIndex]) + if index >= 0: + leftIndex += index + 1 + rightIndex += 1 + result = max(result, rightIndex - leftIndex) + return result +``` + +这个实现的时间复杂度是O(n²),显然不满足。 + +可以使用Set或者HashMap进行优化: + + + +```python +class Solution: + def lengthOfLongestSubstring(self, s: str) -> int: + leftIndex = 0 + rightIndex = 0 + result = 0 + data = set() + while rightIndex < len(s): + while s[rightIndex] in data: + data.remove(s[leftIndex]) + leftIndex += 1 + data.add(s[rightIndex]) + rightIndex += 1 + result = max(result, rightIndex - leftIndex) + return result +``` + + + +- 时间复杂度:O(n) + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" new file mode 100644 index 00000000..89302165 --- /dev/null +++ "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" @@ -0,0 +1,59 @@ +32.串联所有单词的子串 +=== + + +### 题目 + +给定一个字符串 s 和一个字符串数组 words。 words 中所有字符串 长度相同。 + +s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺序排列连接起来的子串。 + +例如,如果 words = ["ab","cd","ef"], 那么 "abcdef", "abefcd","cdabef", "cdefab","efabcd", 和 "efcdab" 都是串联子串。 "acdbef" 不是串联子串,因为他不是任何 words 排列的连接。 + +返回所有串联子串在 s 中的开始索引。你可以以 任意顺序 返回答案。 + + + +示例 1: + +- 输入:s = "barfoothefoobarman", words = ["foo","bar"] +- 输出:[0,9] +- 解释:因为 words.length == 2 同时 words[i].length == 3,连接的子字符串的长度必须为 6。 +- 子串 "barfoo" 开始位置是 0。它是 words 中以 ["bar","foo"] 顺序排列的连接。 +- 子串 "foobar" 开始位置是 9。它是 words 中以 ["foo","bar"] 顺序排列的连接。 +- 输出顺序无关紧要。返回 [9,0] 也是可以的。 + +示例 2: + +- 输入:s = "wordgoodgoodgoodbestword", words = ["word","good","best","word"] +- 输出:[] +- 解释:因为 words.length == 4 并且 words[i].length == 4,所以串联子串的长度必须为 16。 +- s 中没有子串长度为 16 并且等于 words 的任何顺序排列的连接。 +- 所以我们返回一个空数组。 + +示例 3: + +- 输入:s = "barfoofoobarthefoobarman", words = ["bar","foo","the"] +- 输出:[6,9,12] +- 解释:因为 words.length == 3 并且 words[i].length == 3,所以串联子串的长度必须为 9。 +- 子串 "foobarthe" 开始位置是 6。它是 words 中以 ["foo","bar","the"] 顺序排列的连接。 +- 子串 "barthefoo" 开始位置是 9。它是 words 中以 ["bar","the","foo"] 顺序排列的连接。 +- 子串 "thefoobar" 开始位置是 12。它是 words 中以 ["the","foo","bar"] 顺序排列的连接。 + + +提示: + +- 1 <= s.length <= 104 +- 1 <= words.length <= 5000 +- 1 <= words[i].length <= 30 +- words[i] 和 s 由小写英文字母组成 + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/26..md b/Algorithm/33..md similarity index 100% rename from Algorithm/26..md rename to Algorithm/33..md diff --git a/Algorithm/27..md b/Algorithm/34..md similarity index 100% rename from Algorithm/27..md rename to Algorithm/34..md diff --git a/Algorithm/35..md b/Algorithm/35..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/35..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + From 2a894a6f09bf7a6e7ccdf3ede750bc008772cb80 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Fri, 20 Jun 2025 16:26:44 +0800 Subject: [PATCH 123/128] add notes --- ...15\347\232\204\345\255\220\344\270\262.md" | 159 ++++++++++++++++++ Algorithm/33..md | 17 -- ...06\347\233\226\345\255\220\344\270\262.md" | 109 ++++++++++++ 3 files changed, 268 insertions(+), 17 deletions(-) delete mode 100644 Algorithm/33..md create mode 100644 "Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" diff --git "a/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" index 89302165..73f0d47d 100644 --- "a/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" +++ "b/Algorithm/32.\344\270\262\350\201\224\346\211\200\346\234\211\345\215\225\350\257\215\347\232\204\345\255\220\344\270\262.md" @@ -50,6 +50,165 @@ s 中的 串联子串 是指一个包含 words 中所有字符串以任意顺 ### 思路 +##### 方法一,从头到尾遍历s + +- words中每个字符的长度是m、words整个数组的长度是n +- 那我们每次可以从s中从头开始去遍历,每次是取 `m*n` 个字符(窗口) +- 然后把这个`m*n`个字符按照words中每个字符的长度m进行分割,然后把分割后的单词与words中的单词进行比较 +- 如果和words中的都一样,那当前的索引就是。否则不是 +- 在与words中的单词进行比较的时候,这个时候是不用关心顺序的,所以我们可以用一个哈希表来表示单词以及频次 + - 当窗口中出现一个单词时,我们就把该单词加到哈希表中,如果已经存在,那就在后面的值+1 + - 然后用哈希表中的内容与words中的单词进行对比,如果words中出现一个单词且也在哈希表中,那就将哈希表中该单词的频次减1,如果是0了就一次该单词 + - 等到最后都遍历完,如果哈希表的长度是0,那就说明完全与words重点额一样 + +```python + +class Solution: + + def findSubstring(self, s: str, words: List[str]) -> List[int]: + res = [] + if len(s) == 0 or len(words) == 0 or len(words[0]) == 0: + return[] + + wordsLength = len(words[0]) + slideLength = wordsLength * len(words) + wordsCount = len(words) + + wordsMap = dict() + for word in words: + wordsMap[word] = wordsMap.get(word, 0) + 1 + + tempMap = dict() + for index in range(len(s) - slideLength + 1): + currentString = s[index: index+slideLength] + print(currentString) + for currentStringIndex in range(wordsCount): + currentWord = currentString[currentStringIndex * wordsLength: (currentStringIndex + 1) * wordsLength] + tempMap[currentWord] = tempMap.get(currentWord, 0) + 1 + + print(tempMap) + print(wordsMap) + if tempMap == wordsMap: + res.append(index) + tempMap.clear() + + + return res +``` + + + +时间复杂度:O(n×m×k),n为s的长度,m为每个words中单词的长度,也就是len(words[0]),k为words的长度,也就是len(words) + + +空间复杂度:每次循环都新建一个字典tempMap,但循环结束就清除,所以空间为O(m)(m是words中不同单词的个数)?实际上每次循环都重新统计,所以空间上是O(m),但要注意,我们每次循环都重新统计整个子串,所以没有利用滑动窗口的特性。 + + +而下面方法二的时间复杂度是: +- O(n×k): n为s的长度,k为words中单词的个数 + + + +##### 方法二 + + +上面方法一种当仅使用一个从0开始的滑动窗口时,为了避免漏掉一些子串,在缩短窗口时,每次只能缩小一格,而且这还会导致窗口内所维护的单词计数无效。那么,希望有一个方法,可以保证: +- 不遗漏子串 +- 缩小窗口时,不会使窗口内的单词计数无效 + +那么,在使用滑动窗口时,每次都移动 sz = len(words[0]) 个字符,那么我们就可以按照单词的纬度进行统计。 +但是,这样会导致某一些子串没有枚举到(即[1, sz-1]为起点的子串都被忽略了),所以为了保证不遗漏子串,可以枚举以[0, sz-1]为起点的所有滑动窗口,并且每一个滑窗都是互相独立的。 + +--- + +- 建立滑动窗口 + + - 如何建立:计算窗口长度为:words中所有串拼接后的长度len,第一个窗口为[0,len-1]。 + - 如果窗口中的字符串和words中所有串拼接后相等,则说明满足要求。 + - 如何判断: + - 将words中的所有word放入hashmap。key为word,value为个数。 + - 对窗口进行substr操作,每隔d,substr一次。d为words中word的长度。将substr的结果作为key放入另一个hashmap,个数为value。 + - 当窗口中没有剩余字符时,对两个map进行判断,如果相等。说明满足要求。此时记录窗口的起点。 + +- 建立多起点的滑动窗口。 + + - 何为多起点: + - 一般理解滑动窗口从0或者某个数值开始,向右滑动不断滑动一个步长。 + - 此处需要建立多个滑动窗口,数量为d。起点分别为0,1,2...d; + + - 为何需要多起点: + - 如果只建立一个滑动窗口,那么每次就只能滑动一格,因为需要找到所有的可能。但是这样的操作意味着之前建立的map需要重新构建 + - 例如:foobarfoobar [foo][bar]。当foobar完成匹配后,向右滑动一格,oobarf。这时候需要重新构建map。插入oob ,arf。时间复杂度很高,而滑动窗口应该是线性时间复杂度, + - 理想状态是,滑动d格,删去左侧foo,加入右侧foo。这个时候不需要重新构建map,map中原本的foo的计数先-1再+1即可,然后判断即可。 + + 那么如果按照上面的方法,每次都滑动d,又会漏检。例如afoobarfoobar [foo][bar] + + 因此,需要多起点,afoobar,foobar。。个数为d个。这样每个窗口每次都是滑动d格。map的效率最高 + + - 如何建立多起点 + + - 从0开始初始化d个滑动窗口。每个滑动窗口每次都是滑动d格。建立相应的map。 + + - 最后得到vector>; + +- 滑动 + + - 由于已经建立了多起点的滑动窗口,所以不会存在漏检的情况。同时map的效率最高。 + + + + + + +```java + +class Solution { + public List findSubstring(String s, String[] words) { + // 记录所有满足的结果索引 + List res = new ArrayList<>(); + if (s == null || s.length() == 0 || words == null || words.length == 0) { + return res; + } + + HashMap map = new HashMap<>(); + // 每个单词的长度是固定的 + int one_word = words[0].length(); + int word_num = words.length; + // 窗口长度 + int all_len = one_word * word_num; + // 把words中的内容和次数都记录到HashMap中 + for (String word : words) { + map.put(word, map.getOrDefault(word, 0) + 1); + } + + for (int i = 0; i < one_word; i++) { + int left = i, right = i, count = 0; + HashMap tmp_map = new HashMap<>(); + while (right + one_word <= s.length()) { + String w = s.substring(right, right + one_word); + right += one_word; + if (!map.containsKey(w)) { + count = 0; + left = right; + tmp_map.clear(); + } else { + tmp_map.put(w, tmp_map.getOrDefault(w, 0) + 1); + count++; + while (tmp_map.getOrDefault(w, 0) > map.getOrDefault(w, 0)) { + String t_w = s.substring(left, left + one_word); + count--; + tmp_map.put(t_w, tmp_map.getOrDefault(t_w, 0) - 1); + left += one_word; + } + if (count == word_num) res.add(left); + } + } + } + return res; + } +} +``` + --- diff --git a/Algorithm/33..md b/Algorithm/33..md deleted file mode 100644 index 0970b4ab..00000000 --- a/Algorithm/33..md +++ /dev/null @@ -1,17 +0,0 @@ -18.整数转罗马数字 -=== - - -### 题目 - - - -### 思路 - - - ---- -- 邮箱 :charon.chui@gmail.com -- Good Luck! - - diff --git "a/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" "b/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" new file mode 100644 index 00000000..c4f31bed --- /dev/null +++ "b/Algorithm/33.\346\234\200\345\260\217\350\246\206\347\233\226\345\255\220\344\270\262.md" @@ -0,0 +1,109 @@ +33.最小覆盖子串 +=== + + +### 题目 + +给你一个字符串s、一个字符串t。返回s中涵盖t所有字符的最小子串。如果s中不存在涵盖t所有字符的子串,则返回空字符串 "" 。 + + + +注意: + +- 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 +- 如果 s 中存在这样的子串,我们保证它是唯一的答案。 + + +示例 1: + +- 输入:s = "ADOBECODEBANC", t = "ABC" +- 输出:"BANC" +- 解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A'、'B' 和 'C'。 + +示例 2: + +- 输入:s = "a", t = "a" +- 输出:"a" +- 解释:整个字符串 s 是最小覆盖子串。 + +示例 3: + +- 输入: s = "a", t = "aa" +- 输出: "" +- 解释: t 中两个字符 'a' 均应包含在 s 的子串中,因此没有符合条件的子字符串,返回空字符串。 + + +提示: + +- m == s.length +- n == t.length +- 1 <= m, n <= 105 +- s 和 t 由英文字母组成 + + +进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗? + +### 思路 + +题目中说如果 s 中存在这样的子串,我们保证它是唯一的答案。 + +双指针 + +- 初始化ansLeft=−1, ansRight=m,用来记录最短子串的左右端点,其中m是s的长度。 +- 用一个哈希表(或者数组)cntT 统计 t 中每个字母的出现次数。 +- 初始化 left=0,以及一个空哈希表(或者数组)cntS,用来统计 s 子串中每个字母的出现次数。 +- 遍历 s,设当前枚举的子串右端点为 right,把 s[right] 的出现次数加一。 +- 遍历 cntS 中的每个字母及其出现次数,如果出现次数都大于等于 cntT 中的字母出现次数: + - 如果`right−left cnt = new HashMap<>(); + for (char c : t.toCharArray()) { + cnt.put(c, cnt.getOrDefault(c, 0) + 1); + } + int ans_l = -1; + int ans_r = s.length(); + int l = 0; + int count = cnt.size(); + for (int r = 0; r < s.length(); r++) { + char c = s.charAt(r); + if (cnt.containsKey(c)) { + cnt.put(c, cnt.get(c) - 1); + if (cnt.get(c) == 0) { + count--; + } + } + while (count == 0) { + if (ans_r - ans_l > r - l) { + ans_l = l; + ans_r = r; + } + char ch = s.charAt(l); + if (cnt.containsKey(ch)) { + if (cnt.get(ch) == 0) { + count++; + } + cnt.put(ch, cnt.get(ch) + 1); + } + l++; + } + } + return ans_l == -1 ? "" : s.substring(ans_l, ans_r+1); + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + From 14691c5bfaba919ed8f9082a695e5a9383b95b89 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 2 Jul 2025 10:31:25 +0800 Subject: [PATCH 124/128] add notes --- ...10\347\232\204\346\225\260\347\213\254.md" | 142 ++++++++++ ...72\346\227\213\347\237\251\351\230\265.md" | 99 +++++++ ...13\350\275\254\345\233\276\345\203\217.md" | 252 ++++++++++++++++++ Algorithm/{34..md => 37..md} | 0 Algorithm/{35..md => 38..md} | 0 Algorithm/39..md | 17 ++ Algorithm/40..md | 17 ++ 7 files changed, 527 insertions(+) create mode 100644 "Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" create mode 100644 "Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" create mode 100644 "Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" rename Algorithm/{34..md => 37..md} (100%) rename Algorithm/{35..md => 38..md} (100%) create mode 100644 Algorithm/39..md create mode 100644 Algorithm/40..md diff --git "a/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" "b/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" new file mode 100644 index 00000000..e1c6fefa --- /dev/null +++ "b/Algorithm/34.\346\234\211\346\225\210\347\232\204\346\225\260\347\213\254.md" @@ -0,0 +1,142 @@ +34.有效的数独 +=== + + +### 题目 + +请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。 + +数字 1-9 在每一行只能出现一次。 +数字 1-9 在每一列只能出现一次。 +数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图) + + +注意: + +- 一个有效的数独(部分已被填充)不一定是可解的。 +- 只需要根据以上规则,验证已经填入的数字是否有效即可。 +- 空白格用 '.' 表示。 + + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/isValidSudoku.png?raw=true) + + +``` +输入:board = +[["5","3",".",".","7",".",".",".","."] +,["6",".",".","1","9","5",".",".","."] +,[".","9","8",".",".",".",".","6","."] +,["8",".",".",".","6",".",".",".","3"] +,["4",".",".","8",".","3",".",".","1"] +,["7",".",".",".","2",".",".",".","6"] +,[".","6",".",".",".",".","2","8","."] +,[".",".",".","4","1","9",".",".","5"] +,[".",".",".",".","8",".",".","7","9"]] +``` +输出:true + +``` +输入:board = +[["8","3",".",".","7",".",".",".","."] +,["6",".",".","1","9","5",".",".","."] +,[".","9","8",".",".",".",".","6","."] +,["8",".",".",".","6",".",".",".","3"] +,["4",".",".","8",".","3",".",".","1"] +,["7",".",".",".","2",".",".",".","6"] +,[".","6",".",".",".",".","2","8","."] +,[".",".",".","4","1","9",".",".","5"] +,[".",".",".",".","8",".",".","7","9"]] +``` +输出:false +解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。 + + +提示: + +- board.length == 9 +- board[i].length == 9 +- board[i][j] 是一位数字(1-9)或者 '.' + + +### 思路 + +有效的数独满足以下三个条件: + +- 同一个数字在每一行只能出现一次; + +- 同一个数字在每一列只能出现一次; + +- 同一个数字在每一个小九宫格只能出现一次。 + +--- + +- i是行标 +- j是列标 +- 数字从char直接转换成int,会变成对应的ASCII数字码,而不是原来的数值。解决方法就是当前char数字减'0':用两个char数字的ASCII码相减,差值就是原来char数值直接对应的int数值。 + - 为什么不是减'0',而是减'1'? + - 若运行了大佬的代码你会发现,减'0'的话会出现`ArrayIndexOutOfBoundsException`的异常。因为后面的代码将这个`num`作为了数组的下标。本身数组设置的就是9个位置,下标范围是`[0~8]`,那么遇到数字9作为下标的时候,不就越位了吗,所以就要减1,char转换成int的同时还能解决后面越位的情况。 +- boolean数组的巧妙建立 + - 第一个[]存放第?行/列/块 + - 第二个[]存放 相应数字 + - 结合起来解释就是:第?行/列/块 是否 出现过相应数字 + +- 行标决定一组block的起始位置(因为block为3行,所以除3取整得到组号,又因为每组block为3个,所以需要乘3),列标再细分出是哪个block(因为block是3列,所以除3取整) + +``` +blockIndex = i / 3 * 3 + j / 3的原因: +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[0, 0, 0, 1, 1, 1, 2, 2, 2] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[3, 3, 3, 4, 4, 4, 5, 5, 5] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +[6, 6, 6, 7, 7, 7, 8, 8, 8] +``` + +- blockIndex的规律探寻 + - 微观`9x9` -> 宏观`3x3` + - (1)`i/3`为行号,`j/3`为列号 + - (2)二维数组思路:`行号*列数+列号`,即位置 + + + + +```java + +class Solution { + public boolean isValidSudoku(char[][] board) { + // 记录某行,某位数字是否已经被摆放 + boolean[][] row = new boolean[9][9]; + // 记录某列,某位数字是否已经被摆放 + boolean[][] col = new boolean[9][9]; + // 记录某 3x3 宫格内,某位数字是否已经被摆放 + boolean[][] block = new boolean[9][9]; + + for (int i = 0; i < 9; i++) { + for (int j = 0; j < 9; j++) { + if (board[i][j] != '.') { // 只处理数字格子 + int num = board[i][j] - '1'; + int blockIndex = i / 3 * 3 + j / 3; + if (row[i][num] || col[j][num] || block[blockIndex][num]) { + return false; + } else { + row[i][num] = true; + col[j][num] = true; + block[blockIndex][num] = true; + } + } + } + } + return true; + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" "b/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" new file mode 100644 index 00000000..cdde4f92 --- /dev/null +++ "b/Algorithm/35.\350\236\272\346\227\213\347\237\251\351\230\265.md" @@ -0,0 +1,99 @@ +35.螺旋矩阵 +=== + + +### 题目 + + +给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。 + + +示例 1: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/spiralOrder_1.png?raw=true) + +- 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +- 输出:[1,2,3,6,9,8,7,4,5] + +示例 2: +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/spiralOrder_2.png?raw=true) + +- 输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]] +- 输出:[1,2,3,4,8,12,11,10,9,5,6,7] + + +提示: + +- m == matrix.length +- n == matrix[i].length +- 1 <= m, n <= 10 +- -100 <= matrix[i][j] <= 100 + + +### 思路 + +- 对于这种螺旋遍历的方法,重要的是要确定上下左右四条边的位置 +- 初始化的时候,上边up就是0,下边down就是m-1,左边left是0,右边right是n-1。 +- 然后我们进行while循环 + - 先遍历上边第一行up ,将所有元素加入结果res,然后上边下移一位,如果此时上边大于下边,说明此时已经遍历完成了,直接break。 + + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + List res=new ArrayList(); + + if(matrix.length==0 || matrix[0].length==0) { + return res; + } + + int m = matrix.length; + int n =matrix[0].length; + + int up = 0; + int down = m-1; + int left = 0; + int right = n-1; + + while(true) { + for(int i=left; i<=right; i++) { + res.add(matrix[up][i]); + } + + if (++up > down) { + break; + } + + for (int i = up; i <= down; i++) { + res.add(matrix[i][right]); + } + + if (--right < left) { + break; + } + + for (int i = right; i >= left; i--) { + res.add(matrix[down][i]); + } + + if (--down < up) { + break; + } + + for (int i = down; i >= up; i--) { + res.add(matrix[i][left]); + } + + if (++left > right) { + break; + } + } + return res; + } +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" "b/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" new file mode 100644 index 00000000..ddda4601 --- /dev/null +++ "b/Algorithm/36.\346\227\213\350\275\254\345\233\276\345\203\217.md" @@ -0,0 +1,252 @@ +36.旋转图像 +=== + + +### 题目 + + +给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。 + +你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 + +示例1: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_1.png?raw=true) + +- 输入:matrix = [[1,2,3],[4,5,6],[7,8,9]] +- 输出:[[7,4,1],[8,5,2],[9,6,3]] + +示例2: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_2.png?raw=true) + +- 输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]] +- 输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]] + + +提示: + +- n == matrix.length == matrix[i].length +- 1 <= n <= 20 +- -1000 <= matrix[i][j] <= 1000 + + + + +### 思路 + +##### 方法一: 辅助矩阵 + +如下图所示,矩阵顺时针旋转 90º 后,可找到以下规律: + +- 「第 i 行」元素旋转到「第 n−1−i 列」元素; +- 「第 j 列」元素旋转到「第 j 行」元素; + +因此,对于矩阵任意第 i 行、第 j 列元素 matrix[i][j] ,矩阵旋转 90º 后「元素位置旋转公式」为: + +``` +matrix[i][j] → matrix[j][n−1−i] +原索引位置 →旋转后索引位置 +​``` + + + +根据以上「元素旋转公式」,考虑遍历矩阵,将各元素依次写入到旋转后的索引位置。但仍存在问题:在写入一个元素 matrix[i][j]→matrix[j][n−1−i] 后,原矩阵元素 matrix[j][n−1−i] 就会被覆盖(即丢失),而此丢失的元素就无法被写入到旋转后的索引位置了。 + +为解决此问题,考虑借助一个「辅助矩阵」暂存原矩阵,通过遍历辅助矩阵所有元素,将各元素填入「原矩阵」旋转后的新索引位置即可。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_3.png?raw=true) + +```python3 +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + n = len(matrix) + # 深拷贝 matrix -> tmp + tmp = copy.deepcopy(matrix) + # 根据元素旋转公式,遍历修改原矩阵 matrix 的各元素 + for i in range(n): + for j in range(n): + matrix[j][n - 1 - i] = tmp[i][j] +``` + +复杂度分析: +​ +- 遍历矩阵所有元素的时间复杂度为O(N²) +- 由于借助了一个辅助矩阵,空间复杂度为O(N²)。 + + +##### 方法二:原地修改 + + +考虑不借助辅助矩阵,通过在原矩阵中直接「原地修改」,实现空间复杂度 O(1) 的解法。 + +以位于矩阵四个角点的元素为例,设矩阵左上角元素A、右上角元素B、右下角元素C、左下角元素D。 + +矩阵旋转90º后,相当于依次先后执行D→A,C→D,B→C,A→B修改元素,即如下「首尾相接」的元素旋转操作: +``` +A←D←C←B←A +``` + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_4.png?raw=true) + + +如上图所示: + +- 由于第1步D→A已经将A覆盖(导致A丢失),此丢失导致最后第4步A→B无法赋值。为解决此问题,考虑借助一个「辅助变量 tmp 」预先存储 A ,此时的旋转操作变为: + +``` +暂存 tmp=A +A←D←C←B←tmp +``` + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_rotate_5.png?raw=true) + + + +如上图所示,一轮可以完成矩阵 4 个元素的旋转。因而,只要分别以矩阵左上角 1/4 的各元素为起始点执行以上旋转操作,即可完整实现矩阵旋转。 + +- 当矩阵大小 n 为偶数时,取前½ * n行、前 ½ * n 列的元素为起始点; + +- 当矩阵大小 n 为奇数时,取前 ½ * n 行、前 ½ * (n + 1)列的元素为起始点。 + +令 matrix[i][j]=A ,根据文章开头的元素旋转公式,可推导得适用于任意起始点的元素旋转操作: + +``` +暂存tmp=matrix[i][j] +matrix[i][j]←matrix[n−1−j][i]←matrix[n−1−i][n−1−j]←matrix[j][n−1−i]←tmp +``` + + + +```python3 +class Solution: + def rotate(self, matrix: List[List[int]]) -> None: + n = len(matrix) + for i in range(n // 2): + for j in range((n + 1) // 2): + tmp = matrix[i][j] + matrix[i][j] = matrix[n - 1 - j][i] + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j] + matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i] + matrix[j][n - 1 - i] = tmp +``` + + +```java +class Solution { + public void rotate(int[][] matrix) { + // 设矩阵行列数为 n + int n = matrix.length; + // 起始点范围为 0 <= i < n / 2 , 0 <= j < (n + 1) / 2 + // 其中 '/' 为整数除法 + for (int i = 0; i < n / 2; i++) { + for (int j = 0; j < (n + 1) / 2; j++) { + // 暂存 A 至 tmp + int tmp = matrix[i][j]; + // 元素旋转操作 A <- D <- C <- B <- tmp + matrix[i][j] = matrix[n - 1 - j][i]; + matrix[n - 1 - j][i] = matrix[n - 1 - i][n - 1 - j]; + matrix[n - 1 - i][n - 1 - j] = matrix[j][n - 1 - i]; + matrix[j][n - 1 - i] = tmp; + } + } + } +} +``` + +复杂度分析: + +- 时间复杂度 O(N²): 其中 N 为输入矩阵的行(列)数。需要将矩阵中每个元素旋转到新的位置,即对矩阵所有元素操作一次,使用O(N²)时间。 +- 空间复杂度 O(1): 临时变量 tmp 使用常数大小的额外空间。值得注意,当循环中进入下轮迭代,上轮迭代初始化的 tmp 占用的内存就会被自动释放,因此无累计使用空间。 + + +##### 方法三: 先转置再左右翻转 + +``` +很明显可以发现,对二位矩阵顺时针旋转90度,等价于先对二位矩阵进行转置操作,再对转置后的数组进行左右翻转。 +如: +原矩阵: +1 2 3 +4 5 6 +7 8 9 + +转置后: +1 4 7 +2 5 8 +3 6 9 + +左右翻转: +7 4 1 +8 5 2 +9 6 3 + +原矩阵: +1 2 3 +4 5 6 +7 8 9 + +顺时针旋转90度: +7 4 1 +8 5 2 +9 6 3 + +可以发现,顺时针旋转90度后的矩阵,于先转置再左右翻转的矩阵相同 +``` + +1、矩阵转置: + +矩阵的转置是将矩阵的行和列互换,即将元素 matrix[i][j] 和 matrix[j][i] 交换。 +性质: +- 转置操作不会改变主对角线上的元素(即 i=j 时的元素)。 +- 转置完成后,矩阵变为关于主对角线对称。 + +本质:将矩阵对角线两边的元素互换 + + +2、逐行翻转: + +- 对转置后的矩阵,每一行进行翻转,即从左到右交换元素,得到最终旋转后的结果。 + +性质: +转置只是调整了行列的关系,但要实现顺时针旋转 90 度,还需要将转置后的行元素顺序反转。 + + +全部代码: + +```python3 +class Solution: + def transpose(self, matrix:List[List[int]]) -> None: + length = len(matrix) + for i in range(0, length): + for j in range(0, i): + temp = matrix[i][j] + matrix[i][j] = matrix[j][i] + matrix[j][i] = temp + + def overturn(self, matrix:List[List[int]]) -> None: + length = len(matrix) + for i in range(0, length//2): + for j in range(0, length): + temp = matrix[j][i] + matrix[j][i] = matrix[j][length - 1 - i] + matrix[j][length - 1 - i] = temp + + def rotate(self, matrix: List[List[int]]) -> None: + """ + Do not return anything, modify matrix in-place instead. + """ + self.transpose(matrix) + self.overturn(matrix) +``` + + +- 时间复杂度: O(N²) +- 空间复杂度: O(1) + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/34..md b/Algorithm/37..md similarity index 100% rename from Algorithm/34..md rename to Algorithm/37..md diff --git a/Algorithm/35..md b/Algorithm/38..md similarity index 100% rename from Algorithm/35..md rename to Algorithm/38..md diff --git a/Algorithm/39..md b/Algorithm/39..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/39..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/40..md b/Algorithm/40..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/40..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + From 57a26f717ca5f03971fc8e0a6de075817da60e3c Mon Sep 17 00:00:00 2001 From: CharonChui Date: Tue, 16 Sep 2025 11:01:21 +0800 Subject: [PATCH 125/128] add algorithm --- .../KMP\345\274\200\345\217\221.md" | 34 +++ ...51\351\230\265\347\275\256\351\233\266.md" | 165 +++++++++++++++ ...37\345\221\275\346\270\270\346\210\217.md" | 137 ++++++++++++ ...9.\350\265\216\351\207\221\344\277\241.md" | 97 +++++++++ ...04\345\255\227\347\254\246\344\270\262.md" | 98 +++++++++ ...25\350\257\215\350\247\204\345\276\213.md" | 114 ++++++++++ ...15\345\274\202\344\275\215\350\257\215.md" | 194 +++++++++++++++++ ...15\350\257\215\345\210\206\347\273\204.md" | 118 +++++++++++ ...44\346\225\260\344\271\213\345\222\214.md" | 121 +++++++++++ ...5.\345\277\253\344\271\220\346\225\260.md" | 198 ++++++++++++++++++ ...345\244\215\345\205\203\347\264\240 II.md" | 112 ++++++++++ ...36\347\273\255\345\272\217\345\210\227.md" | 133 ++++++++++++ ...07\346\200\273\345\214\272\351\227\264.md" | 134 ++++++++++++ ...10\345\271\266\345\214\272\351\227\264.md" | 82 ++++++++ ...22\345\205\245\345\214\272\351\227\264.md" | 102 +++++++++ ...25\347\210\206\346\260\224\347\220\203.md" | 110 ++++++++++ ...10\347\232\204\346\213\254\345\217\267.md" | 140 +++++++++++++ ...00\345\214\226\350\267\257\345\276\204.md" | 127 +++++++++++ ...4.\346\234\200\345\260\217\346\240\210.md" | 103 +++++++++ ...76\345\274\217\346\261\202\345\200\274.md" | 137 ++++++++++++ ...54\350\256\241\347\256\227\345\231\250.md" | 163 ++++++++++++++ Algorithm/{37..md => 57..md} | 0 Algorithm/{38..md => 58..md} | 0 Algorithm/{39..md => 59..md} | 0 Algorithm/{40..md => 60..md} | 0 Algorithm/61..md | 17 ++ Algorithm/62..md | 17 ++ 27 files changed, 2653 insertions(+) create mode 100644 "AdavancedPart/KMP\345\274\200\345\217\221.md" create mode 100644 "Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" create mode 100644 "Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" create mode 100644 "Algorithm/39.\350\265\216\351\207\221\344\277\241.md" create mode 100644 "Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" create mode 100644 "Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" create mode 100644 "Algorithm/42.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" create mode 100644 "Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" create mode 100644 "Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" create mode 100644 "Algorithm/45.\345\277\253\344\271\220\346\225\260.md" create mode 100644 "Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" create mode 100644 "Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" create mode 100644 "Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" create mode 100644 "Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" create mode 100644 "Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" create mode 100644 "Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" create mode 100644 "Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" create mode 100644 "Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" create mode 100644 "Algorithm/54.\346\234\200\345\260\217\346\240\210.md" create mode 100644 "Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" create mode 100644 "Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" rename Algorithm/{37..md => 57..md} (100%) rename Algorithm/{38..md => 58..md} (100%) rename Algorithm/{39..md => 59..md} (100%) rename Algorithm/{40..md => 60..md} (100%) create mode 100644 Algorithm/61..md create mode 100644 Algorithm/62..md diff --git "a/AdavancedPart/KMP\345\274\200\345\217\221.md" "b/AdavancedPart/KMP\345\274\200\345\217\221.md" new file mode 100644 index 00000000..b4d7e54c --- /dev/null +++ "b/AdavancedPart/KMP\345\274\200\345\217\221.md" @@ -0,0 +1,34 @@ +KMP开发 +=== + +Kotlin Multiplatform(简称 KMP)是 JetBrains 推出的开源跨平台开发框架。 + +它可以通过共享 Kotlin 编写的业务逻辑代码实现多平台复用。 + + +从应用场景来看,KMP 不仅局限于移动端,它支持 iOS、Android、Web、桌面端(Windows/macOS/Linux)以及服务器端的代码共享。这种扩展性使得开发者能够用同一套代码库构建全平台应用,大幅提升开发效率。 + + + +KMP 有三大编译目标,分别是: Kotlin/JVM、Kotlin/Native、Kotlin/JS。通过编译不同的目标文件实现各端的跨平台能力。除此之外,KMP 还实验性地支持 WebAssembly(Kotlin/Wasm)编译目标,不过目前实际应用场景相对较少。 + + +### KMP编译器 + + +我们知道,一个语言的编译需要经过词法分析和语法分析,解析成抽象语法树 AST。 +而 KMP 为了将 Kotlin 源代码编译成不同的目标平台代码,就需要将 Kotlin 的编译产物进一步向不同的平台转化。 +Kotlin 语言的编译,与向不同的平台转化,明显是不同的职责,需要解耦,所以 KMP 的编译器必然有两个部分,也就是编译器前端(Frontend)与编译器后端(Backend)。 +Frontend 会将 AST 进一步转换为 Kotlin IR(Kotlin Intermediate Representation),是 Kotlin 源代码的中间表示形式,Kotlin IR 是编译器前端的输出,也是编译器后端的输入。 +Backend 则会吧 Kotlin IR 转换为不同平台的中间表示形式,最终生成目标代码。 + +作者:A0微声z +链接:https://juejin.cn/post/7507888457705275455 +来源:稀土掘金 +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" "b/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" new file mode 100644 index 00000000..774d552c --- /dev/null +++ "b/Algorithm/37.\347\237\251\351\230\265\347\275\256\351\233\266.md" @@ -0,0 +1,165 @@ +37.矩阵置零 +=== + + + +### 题目 + +给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。 + + +示例: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_setzeros_1.png?raw=true) + +- 输入: matrix = [[0,1,2,0],[3,4,5,2],[1,3,1,5]] +- 输出: [[0,0,0,0],[0,4,5,0],[0,3,1,0]] + +提示: + +- m == matrix.length +- n == matrix[0].length +- 1 <= m, n <= 200 +- -231 <= matrix[i][j] <= 231 - 1 + + +进阶: + +- 一个直观的解决方案是使用 O(mn) 的额外空间,但这并不是一个好的解决方案。 +- 一个简单的改进方案是使用 O(m + n) 的额外空间,但这仍然不是最好的解决方案。 + +你能想出一个仅使用常量空间的解决方案吗? + + +### 思路 + + +思路一: 用 O(m+n)额外空间 + +- 两遍扫matrix,第一遍用集合记录哪些行,哪些列有0;第二遍置0 + +```java +class Solution { + public void setZeroes(int[][] matrix) { + Set row_zero = new HashSet<>(); + Set col_zero = new HashSet<>(); + int row = matrix.length; + int col = matrix[0].length; + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (matrix[i][j] == 0) { + row_zero.add(i); + col_zero.add(j); + } + } + } + for (int i = 0; i < row; i++) { + for (int j = 0; j < col; j++) { + if (row_zero.contains(i) || col_zero.contains(j)) matrix[i][j] = 0; + } + } + } +} +``` + +思路二: 用O(1)空间 + +关键思想: 用matrix第一行和第一列记录该行该列是否有0,作为标志位 + +但是对于第一行,和第一列要设置一个标志位,为了防止自己这一行(一列)也有0的情况.注释写在代码里,直接看代码很好理解! + +思路:第一次循环 +1、0行数组的每个元素临时标识该元素所在列是否有0,0列数组的每个元素临时标识该元素所在行是否有0。 +2、判断每行的第0列是否为0,有则赋值额外的标识字段,该字段的作用是用于后续对称遍历的时候,赋值所有行的0列是否应该赋值0使用 + +``` +比如原始数组为 +[ + [2,1,2,3], + [2,1,2,3], + [3,0,5,2], + [1,3,0,5] +] + + +// 如果当前遍历位置为0,则该行0列设置标识,以便后续使用 +// 如果当前遍历位置为0,则0行该列设置标识,以便后续使用 +[ + [2,0,0,3], + [2,1,2,3], + [0,0,5,2], + [0,3,0,5] +] +// 第二次循环-从右下角遍历处理:行从最后一行遍历(直到0行),列从最后一列遍历(直到第一列) +// 使用0行数组、0列数组判断当前遍历位置是否应该置0 +// 注意需要额外使用标识字段判断该行0列是否应该置0 +// 处理完成之后 +[ + [2,0,0,3], + [2,0,0,3], + [0,0,0,0], + [0,0,0,0] +] + +``` + + +```java + +class Solution { + public void setZeroes(int[][] matrix) { + int row = matrix.length; + int col = matrix[0].length; + boolean row0_flag = false; + boolean col0_flag = false; + // 第一行是否有零 + for (int j = 0; j < col; j++) { + if (matrix[0][j] == 0) { + row0_flag = true; + break; + } + } + // 第一列是否有零 + for (int i = 0; i < row; i++) { + if (matrix[i][0] == 0) { + col0_flag = true; + break; + } + } + // 把第一行第一列作为标志位 + for (int i = 1; i < row; i++) { + for (int j = 1; j < col; j++) { + if (matrix[i][j] == 0) { + matrix[i][0] = matrix[0][j] = 0; + } + } + } + // 置0 + for (int i = 1; i < row; i++) { + for (int j = 1; j < col; j++) { + if (matrix[i][0] == 0 || matrix[0][j] == 0) { + matrix[i][j] = 0; + } + } + } + if (row0_flag) { + for (int j = 0; j < col; j++) { + matrix[0][j] = 0; + } + } + if (col0_flag) { + for (int i = 0; i < row; i++) { + matrix[i][0] = 0; + } + } + } +} +``` + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" "b/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" new file mode 100644 index 00000000..f2e79bb5 --- /dev/null +++ "b/Algorithm/38.\347\224\237\345\221\275\346\270\270\346\210\217.md" @@ -0,0 +1,137 @@ +38.生命游戏 +=== + + +### 题目 + + +生命游戏,简称为 生命 ,是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。 + +给定一个包含 m × n 个格子的面板,每一个格子都可以看成是一个细胞。每个细胞都具有一个初始状态: 1 即为 活细胞 (live),或 0 即为 死细胞 (dead)。每个细胞与其八个相邻位置(水平,垂直,对角线)的细胞都遵循以下四条生存定律: + +- 如果活细胞周围八个位置的活细胞数少于两个,则该位置活细胞死亡; +- 如果活细胞周围八个位置有两个或三个活细胞,则该位置活细胞仍然存活; +- 如果活细胞周围八个位置有超过三个活细胞,则该位置活细胞死亡; +- 如果死细胞周围正好有三个活细胞,则该位置死细胞复活; + +下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的,其中细胞的出生和死亡是 同时 发生的。给你 m x n 网格面板 board 的当前状态,返回下一个状态。 + +给定当前 board 的状态,更新 board 到下一个状态。 + +注意 你不需要返回任何东西。 + + +示例1: + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_gameOfLife_1.jpg?raw=true) + +- 输入:board = [[0,1,0],[0,0,1],[1,1,1],[0,0,0]] +- 输出:[[0,0,0],[1,0,1],[0,1,1],[0,1,0]] + + +示例2: + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_gameOfLife_1.jpg?raw=true) + +- 输入:board = [[1,1],[1,0]] +- 输出:[[1,1],[1,1]] + + +提示: + +- m == board.length +- n == board[i].length +- 1 <= m, n <= 25 +- board[i][j] 为 0 或 1 + + +进阶: + +你可以使用原地算法解决本题吗?请注意,面板上所有格子需要同时被更新:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 +本题中,我们使用二维数组来表示面板。原则上,面板是无限的,但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题? + + +### 思路 + +简化规则: +1. 原来是活的,周围有2-3个活的,成为活的 +2. 原来是死的,周围有3个活的,成为活的 +3. 其他都是死了 + +这道题主要就是模拟,遍历每一个格子,然后统计其周围八个格子的活细胞个数,来看这个格子的状态是否改变。 +但难点在于:如果这个格子的状态改变,不能直接改变。这样会影响后面格子的统计。即题目中说的:你不能先更新某些格子,然后使用它们的更新后的值再更新其他格子。 + +因此我们需要使用特殊值去标记发生改变的格子,从而根据特殊值可以知道这个格子原状态是什么,要更新的状态是什么。 + + +- 可以使用 2表示活细胞变成死细胞,3表示死细胞变成活细胞。【这样的好处是最终是死细胞的都是偶数,活细胞的都是奇数,模2即结果;】 + +也可以用下面的方式: + +- 由于每个位置的细胞的状态是取决于当前四周其他状态的,而且每个细胞的状态是同时变化的,所以不能一个一个地更新,只能在一个新的数组里创建新的状态。 + +- 当然上面所说的也不是绝对的,因为这道题目的输入是int[][],矩阵是 int 类型的,有 32 位,而状态只有 0,1 两种,只需一位且只有最低位用上了,我们用其他位存储下一个状态即可,相当于用原矩阵当一个复制的矩阵。 + +- 所以可以,原有的最低位存储的是当前状态,那倒数第二低位存储下一个状态就行了。 + + +```java +class Solution { + public void gameOfLife(int[][] board) { + int m = board.length; // 行数 + int n = board[0].length; // 列数 + int count = 0; // 统计每个格子周围八个位置的活细胞数 + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + count = 0; // 每个格子计数重置为0 + for(int x = -1; x <= 1; x++){ // -1 0 1 分别代表当前位置左边的格子、当前格子、当前位置右边的格子 + for(int y = -1; y <= 1; y++){ // -1 0 1 分别代表当前位置下边的格子、当前格子、当前位置上边的格子 + // 枚举周围八个位置,其中去掉本身(x = y = 0)和越界(靠近边缘)的情况 + if((x == 0 && y == 0) || i + x < 0 || i + x >= m || j + y < 0 || j + y >= n)continue; + // 如果周围格子是活细胞(1)或者是活细胞变死细胞(2)的,都算一个活细胞 + if(board[i + x][j + y] == 1 || board[i + x][j + y] == 2) { + count++; + } + } + } + + if(board[i][j] == 1 && (count < 2 || count > 3)) { + board[i][j] = 2; // 格子本身是活细胞,周围满足变成死细胞的条件,标记为2 + } + + if(board[i][j] == 0 && count == 3) { + board[i][j] = 3; // 格子本身是死细胞,周围满足复活条件,标记为3 + } + } + } + + for(int i = 0; i < m; i++){ + for(int j = 0; j < n; j++){ + // 死细胞为0,活细胞变成死细胞为2,都为偶数,模2为0,刚好是死细胞 + // 活细胞为1,死细胞变成活细胞为3,都为奇数,模2为1,刚好是活细胞 + board[i][j] %= 2; + } + } + } +} +``` + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" "b/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" new file mode 100644 index 00000000..63cff051 --- /dev/null +++ "b/Algorithm/39.\350\265\216\351\207\221\344\277\241.md" @@ -0,0 +1,97 @@ +39.赎金信 +=== + + +### 题目 + + +给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。 + +如果可以,返回 true ;否则返回 false 。 + +magazine 中的每个字符只能在 ransomNote 中使用一次。 + + + +示例 1: + +- 输入:ransomNote = "a", magazine = "b" +- 输出:false + +示例 2: + +- 输入:ransomNote = "aa", magazine = "ab" +- 输出:false + +示例 3: + +- 输入:ransomNote = "aa", magazine = "aab" +- 输出:true + + +提示: + +- 1 <= ransomNote.length, magazine.length <= 105 +- ransomNote 和 magazine 由小写英文字母组成 + + +### 思路 + +用hash表还是一个大小为26的数组,记录26个单词每一个出现的次数 + +```java +class Solution { + public boolean canConstruct(String ransomNote, String magazine) { + //记录杂志字符串出现的次数 + int[] arr = new int[26]; + int temp; + for (int i = 0; i < magazine.length(); i++) { + temp = magazine.charAt(i) - 'a'; + arr[temp]++; + } + for (int i = 0; i < ransomNote.length(); i++) { + temp = ransomNote.charAt(i) - 'a'; + //对于金信中的每一个字符都在数组中查找 + //找到相应位减一,否则找不到返回false + if (arr[temp] > 0) { + arr[temp]--; + } else { + return false; + } + } + return true; + } +} +``` + + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + if len(ransomNote) > len(magazine): + return False + + arr = [0] * 26 + for i in magazine: + arr[ord(i) -ord('a')] += 1 + + for i in ransomNote: + index = ord(i) - ord('a') + arr[index] -= 1 + if arr[index] < 0: + return False + + return True +``` + + +复杂度: + +- 时间复杂度: O(N) +- 空间复杂度: O(1) + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" "b/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 00000000..aa801292 --- /dev/null +++ "b/Algorithm/40.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,98 @@ +40.同构字符串 +=== + + +### 题目 + +给定两个字符串 s 和 t ,判断它们是否是同构的。 + +如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + + + +示例 1: + +- 输入:s = "egg", t = "add" +- 输出:true + +示例 2: + +- 输入:s = "foo", t = "bar" +- 输出:false + +示例 3: + +- 输入:s = "paper", t = "title" +- 输出:true + + +提示: + +- 1 <= s.length <= 5 * 104 +- t.length == s.length +- s 和 t 由任意有效的 ASCII 字符组成 + +### 思路 + + + + +- “每个出现的字符都应当映射到另一个字符”。代表字符集合 s , t 之间是「满射」。 +- “相同字符只能映射到同一个字符上,不同字符不能映射到同一个字符上”。代表字符集合 s , t 之间是「单射」。 +- 因此, s 和 t 之间是「双射」,满足一一对应。考虑遍历字符串,使用哈希表 s2t , t2s 分别记录 s→t , t→s 的映射,当发现任意「一对多」的关系时返回 false 即可。 + + +```java + +class Solution { + public boolean isIsomorphic(String s, String t) { + Map s2t = new HashMap<>(), t2s = new HashMap<>(); + for (int i = 0; i < s.length(); i++) { + char a = s.charAt(i), b = t.charAt(i); + // 对于已有映射 a -> s2t[a],若和当前字符映射 a -> b 不匹配, + // 说明有一对多的映射关系,则返回 false ; + // 对于映射 b -> a 也同理 + if (s2t.containsKey(a) && s2t.get(a) != b || + t2s.containsKey(b) && t2s.get(b) != a) { + return false; + } + s2t.put(a, b); + t2s.put(b, a); + } + return true; + } +} +``` + + +```python +class Solution: + def isIsomorphic(self, s: str, t: str) -> bool: + s2t = {} + t2s = {} + + for i in range(len(s)): + a = s[i] + b = t[i] + + if (a in s2t and s2t.get(a) != b) or (b in t2s and t2s.get(b) != a): + return False + s2t[a] = b + t2s[b] = a + + return True +``` + +复杂度分析: + +- 时间复杂度 O(N) : 其中 N 为字符串 s , t 的长度。遍历字符串 s , t 使用线性时间,hashmap 查询操作使用 O(1) 时间。 +- 空间复杂度 O(1) : 题目说明 s 和 t 由任意有效的 ASCII 字符组成。由于 ASCII 字符共 128 个,因此 hashmap s2t , t2s 使用 O(128)=O(1) 空间。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" "b/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" new file mode 100644 index 00000000..33cae539 --- /dev/null +++ "b/Algorithm/41.\345\215\225\350\257\215\350\247\204\345\276\213.md" @@ -0,0 +1,114 @@ +41.单词规律 +=== + + +### 题目 + +给定一种规律pattern和一个字符串s,判断s是否遵循相同的规律。 + +这里的遵循指完全匹配,例如,pattern里的每个字母和字符串s中的每个非空单词之间存在着双向连接的对应规律。 + + + +示例1: + +- 输入: pattern = "abba", s = "dog cat cat dog" +- 输出: true + +示例 2: + +- 输入:pattern = "abba", s = "dog cat cat fish" +- 输出: false + +示例 3: + +- 输入: pattern = "aaaa", s = "dog cat cat dog" +- 输出: false + + +提示: + +- 1 <= pattern.length <= 300 +- pattern 只包含小写英文字母 +- 1 <= s.length <= 3000 +- s 只包含小写英文字母和 ' ' +- s 不包含 任何前导或尾随对空格 +- s 中每个单词都被 单个空格 分隔 + + +### 思路 + +和上一题的思路完全一样,由字符之间的一一映射升级成了字符与字符串之间的一一映射。 +首先本质是一样的,要实现一一映射,就要用到两个哈希表分别记录字符到字符串的映射和字符串到字符的映射。 +其次,我们要对s中的单词进行提取,比较单词数量和pattern中的数量是否一致,如果数量上不一致,二者一定不匹配; + + + +```java + +class Solution { + public boolean wordPattern(String pattern, String s) { + Map p2s = new HashMap<>(); // pattern中的字符到s中的字符子串的映射表 + Map s2p = new HashMap<>(); // s中的字符字串到pattern中的字符的映射表 + String[] words = s.split(" "); // 根据空格,提取s中的单词 + int n = pattern.length(); + int m = words.length; + if(n != m){ + return false; // 字符数和单词数不一致,一定不匹配 + } + char ch; + String word; + for(int i = 0; i < n; i++){ + ch = pattern.charAt(i); + word = words[i]; + if((p2s.containsKey(ch) && !p2s.get(ch).equals(word)) || (s2p.containsKey(word) && s2p.get(word) != ch)){ + // 字符与单词没有一一映射:即字符记录的映射不是当前单词或单词记录的映射不是当前字符 + return false; + } + // 更新映射,已存在的映射更新后仍然是不变的;不存在的映射将被加入 + p2s.put(ch, word); + s2p.put(word, ch); + } + return true; + } +} +``` + + +```python + +class Solution: + def wordPattern(self, pattern: str, s: str) -> bool: + words = s.split(' ') + length = len(words) + if length != len(pattern): + return False + + p2s = {} + s2p = {} + + for i in range(length): + word = words[i] + p = pattern[i] + + if (word in s2p and s2p[word] != p) or (p in p2s and p2s[p] != word): + return False + + p2s[p] = word + s2p[word] = p + + return True +``` + +复杂度分析: + +- 时间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。插入和查询哈希表的均摊时间复杂度均为 O(n+m)。每一个字符至多只被遍历一次。 + +- 空间复杂度:O(n+m),其中 n 为 pattern 的长度,m 为 str 的长度。最坏情况下,我们需要存储 pattern 中的每一个字符和 str 中的每一个字符串。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/42.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" "b/Algorithm/42.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" new file mode 100644 index 00000000..b72a4c92 --- /dev/null +++ "b/Algorithm/42.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" @@ -0,0 +1,194 @@ +42.有效的字母异位词 +=== + + +### 题目 + +给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的 字母异位词。 + + + +示例 1: + +- 输入: s = "anagram", t = "nagaram" +- 输出: true + +示例 2: + +- 输入: s = "rat", t = "car" +- 输出: false + + +提示: + +- 1 <= s.length, t.length <= 5 * 104 +- s 和 t 仅包含小写字母 + + +进阶: 如果输入字符串包含 unicode 字符怎么办?你能否调整你的解法来应对这种情况? + + +### 思路 + +设两字符串 s +1 +​ + , s +2 +​ + ,则两者互为重排的「充要条件」为:两字符串 s +1 +​ + , s +2 +​ + 包含的字符是一致的,即 s +1 +​ + , s +2 +​ + 所有对应字符数量都相同,仅排列顺序不同。 + +根据以上分析,可借助「哈希表」分别统计 s +1 +​ + , s +2 +​ + 中各字符数量 key: 字符, value: 数量 ,分为以下情况: + +若 s +1 +​ + , s +2 +​ + 字符串长度不相等,则「不互为重排」; +若 s +1 +​ + , s +2 +​ + 某对应字符数量不同,则「不互为重排」; +否则,若 s +1 +​ + , s +2 +​ + 所有对应字符数量都相同,则「互为重排」; +具体上看,我们可以统计 s +1 +​ + 各字符时执行 +1 ,统计 s +2 +​ + 各字符时 −1 。若两字符串互为重排,则最终哈希表中所有字符统计数值都应为 0 。 + +作者:Krahets +链接:https://leetcode.cn/problems/valid-anagram/solutions/2362065/242-you-xiao-de-zi-mu-yi-wei-ci-ha-xi-bi-cch7/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + +```java +class Solution { + public boolean isAnagram(String s, String t) { + int len1 = s.length(), len2 = t.length(); + if (len1 != len2) + return false; + HashMap dic = new HashMap<>(); + for (int i = 0; i < len1; i++) { + dic.put(s.charAt(i) , dic.getOrDefault(s.charAt(i), 0) + 1); + } + for (int i = 0; i < len2; i++) { + dic.put(t.charAt(i) , dic.getOrDefault(t.charAt(i), 0) - 1); + } + for (int val : dic.values()) { + if (val != 0) + return false; + } + return true; + } +} +``` + + +```python +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + if len(s) != len(t): + return False + dic = defaultdict(int) + for c in s: + dic[c] += 1 + for c in t: + dic[c] -= 1 + for val in dic.values(): + if val != 0: + return False + return True +``` + + +复杂度分析: + +- 时间复杂度 O(M+N) : 其 M , N 分别为字符串 s1, s2长度。当 s1, s2无相同字符时,三轮循环的总迭代次数最多为 2M+2N ,使用 O(M+N) 线性时间。 + +- 空间复杂度 O(1) : 由于字符种类是有限的(常量),一般 ASCII 码共包含 128 个字符,因此可假设使用 O(1) 大小的额外空间。 + + +对于进阶问题,Unicode 是为了解决传统字符编码的局限性而产生的方案,它为每个语言中的字符规定了一个唯一的二进制编码。而 Unicode 中可能存在一个字符对应多个字节的问题,为了让计算机知道多少字节表示一个字符,面向传输的编码方式的 UTF−8 和 UTF−16 也随之诞生逐渐广泛使用,具体相关的知识读者可以继续查阅相关资料拓展视野,这里不再展开。 + +回到本题,进阶问题的核心点在于「字符是离散未知的」,因此我们用哈希表维护对应字符的频次即可。同时读者需要注意 Unicode 一个字符可能对应多个字节的问题,不同语言对于字符串读取处理的方式是不同的。 + + + +```java + +class Solution { + public boolean isAnagram(String s, String t) { + if (s.length() != t.length()) { + return false; + } + Map table = new HashMap(); + for (int i = 0; i < s.length(); i++) { + char ch = s.charAt(i); + table.put(ch, table.getOrDefault(ch, 0) + 1); + } + for (int i = 0; i < t.length(); i++) { + char ch = t.charAt(i); + table.put(ch, table.getOrDefault(ch, 0) - 1); + if (table.get(ch) < 0) { + return false; + } + } + return true; + } +} + +作者:力扣官方题解 +链接:https://leetcode.cn/problems/valid-anagram/solutions/493231/you-xiao-de-zi-mu-yi-wei-ci-by-leetcode-solution/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 +``` + +复杂度分析 + +时间复杂度:O(n),其中 n 为 s 的长度。 + +空间复杂度:O(S),其中 S 为字符集大小,此处 S=26。 + + + +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + return Counter(s) == Counter(t) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" "b/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" new file mode 100644 index 00000000..69eee016 --- /dev/null +++ "b/Algorithm/43.\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215\345\210\206\347\273\204.md" @@ -0,0 +1,118 @@ +43.字母异位词分组 +=== + + +### 题目 + +给你一个字符串数组,请你将 字母异位词(字母异位词是通过重新排列不同单词或短语的字母而形成的单词或短语,并使用所有原字母一次) 组合在一起。可以按任意顺序返回结果列表。 + + + +示例 1: + +输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"] + +输出: [["bat"],["nat","tan"],["ate","eat","tea"]] + +解释: + +- 在 strs 中没有字符串可以通过重新排列来形成 "bat"。 +- 字符串 "nat" 和 "tan" 是字母异位词,因为它们可以重新排列以形成彼此。 +- 字符串 "ate" ,"eat" 和 "tea" 是字母异位词,因为它们可以重新排列以形成彼此。 + +示例 2: + +- 输入: strs = [""] + +- 输出: [[""]] + +示例 3: + +- 输入: strs = ["a"] + +- 输出: [["a"]] + + + +提示: + +- 1 <= strs.length <= 104 +- 0 <= strs[i].length <= 100 +- strs[i] 仅包含小写字母 + +### 思路 + +##### 方法一: 排序 + +由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的,故可以将排序之后的字符串作为哈希表的键。 + +```java +class Solution { + public List> groupAnagrams(String[] strs) { + Map> map = new HashMap>(); + for (String str : strs) { + char[] array = str.toCharArray(); + Arrays.sort(array); + String key = new String(array); + List list = map.getOrDefault(key, new ArrayList()); + list.add(str); + map.put(key, list); + } + return new ArrayList>(map.values()); + } +} +``` + +复杂度分析 + +- 时间复杂度:O(nklogk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要遍历 n 个字符串,对于每个字符串,需要 O(klogk) 的时间进行排序以及 O(1) 的时间更新哈希表,因此总时间复杂度是 O(nklogk)。 + +- 空间复杂度:O(nk),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度。需要用哈希表存储全部字符串。 + +##### 方法二: 计数 + +- 由于互为字母异位词的两个字符串包含的字母相同,因此两个字符串中的相同字母出现的次数一定是相同的 +- 所以可以将每个字母出现的次数使用字符串表示,作为哈希表的键 + +```java + +class Solution { + public List> groupAnagrams(String[] strs) { + Map> map = new HashMap>(); + for (String str : strs) { + int[] counts = new int[26]; + int length = str.length(); + for (int i = 0; i < length; i++) { + counts[str.charAt(i) - 'a']++; + } + // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串,作为哈希表的键 + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < 26; i++) { + if (counts[i] != 0) { + sb.append((char) ('a' + i)); + sb.append(counts[i]); + } + } + String key = sb.toString(); + List list = map.getOrDefault(key, new ArrayList()); + list.add(str); + map.put(key, list); + } + return new ArrayList>(map.values()); + } +} +``` + + +复杂度分析: + +- 时间复杂度:O(nk)),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的的最大长度. + +- 空间复杂度:O(nk)),其中 n 是 strs 中的字符串的数量,k 是 strs 中的字符串的最大长度. + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100644 index 00000000..9e1fba5d --- /dev/null +++ "b/Algorithm/44.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,121 @@ +44.两数之和 +=== + + +### 题目 + +给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。 + +你可以假设每种输入只会对应一个答案,并且你不能使用两次相同的元素。 + +你可以按任意顺序返回答案。 + + + +示例 1: + +- 输入:nums = [2,7,11,15], target = 9 +- 输出:[0,1] +- 解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。 + +示例 2: + +- 输入:nums = [3,2,4], target = 6 +- 输出:[1,2] + +示例 3: + +- 输入:nums = [3,3], target = 6 +- 输出:[0,1] + + +提示: + +- 2 <= nums.length <= 104 +- -109 <= nums[i] <= 109 +- -109 <= target <= 109 +- 只会存在一个有效答案 + + +进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗? + + + +### 思路 + +这道题本身如果通过暴力遍历的话也是很容易解决的,时间复杂度在 O(n²) + +##### 方法一:暴力枚举 + + +最容易想到的方法是枚举数组中的每一个数 x,寻找数组中是否存在 target - x。 + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + int n = nums.length; + for (int i = 0; i < n; ++i) { + for (int j = i + 1; j < n; ++j) { + if (nums[i] + nums[j] == target) { + return new int[]{i, j}; + } + } + } + return new int[0]; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(N²),其中 N 是数组中的元素数量。最坏情况下数组中任意两个数都要被匹配一次。 + +- 空间复杂度:O(1)。 + + +##### 方法二: 哈希表 + + +- 注意到方法一的时间复杂度较高的原因是寻找 target - x 的时间复杂度过高。 +- 因此,我们需要一种更优秀的方法,能够快速寻找数组中是否存在目标元素。如果存在,我们需要找出它的索引。 +- 使用哈希表,可以将寻找 target - x 的时间复杂度降低到从 O(N) 降低到 O(1)。 +- 这样我们创建一个哈希表,对于每一个 x,我们首先查询哈希表中是否存在 target - x,然后将 x 插入到哈希表中,即可保证不会让 x 和自己匹配。 + + +```java +class Solution { + public int[] twoSum(int[] nums, int target) { + Map map = new HashMap<>(); + for(int i = 0; i< nums.length; i++) { + if(map.containsKey(target - nums[i])) { + return new int[] {map.get(target-nums[i]),i}; + } + map.put(nums[i], i); + } + throw new IllegalArgumentException("No two sum solution"); + } +} +``` + +```python +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + hashTable = dict() + for i, num in enumerate(nums): + if target - num in hashTable: + return [hashTable[target-num], i] + hashTable[nums[i]] = i + return [] +``` + +复杂度分析: + +- 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。 + +- 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" "b/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" new file mode 100644 index 00000000..bd557384 --- /dev/null +++ "b/Algorithm/45.\345\277\253\344\271\220\346\225\260.md" @@ -0,0 +1,198 @@ +45.快乐数 +=== + + +### 题目 + +编写一个算法来判断一个数 n 是不是快乐数。 + +「快乐数」 定义为: + +- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。 +- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。 +- 如果这个过程 结果为 1,那么这个数就是快乐数。 +- 如果 n 是 快乐数 就返回 true ;不是,则返回 false 。 + + + +示例 1: + +- 输入:n = 19 +- 输出:true + +- 解释: + - 1² + 9² = 82 + - 8² + 2² = 68 + - 6² + 8² = 100 + - 1² + 0² + 0² = 1 + +示例 2: + +- 输入:n = 2 +- 输出:false + + +提示: + +- 1 <= n <= 231 - 1 + + +### 思路 + +- 快乐数的定义是基于一个计算过程,即对一个正整数,不断将其替换为它各个位上数字的平方和,如果最终这个过程能够收敛到1,则这个数被称为快乐数。 + +- 相反,如果在这个过程中形成了一个不包含1的循环,则该数不是快乐数。 + +- 对于非快乐数,它们的平方和序列会进入一个固定的循环,例如4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4。 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_happy_1.png?raw=true) + +根据我们的探索,我们猜测会有以下三种可能: + +- 最终会得到 1。 +- 最终会进入循环。 +- 值会越来越大,最后接近无穷大(不会发生,因为假设9999999999999,那他每个位置的数值平方相加后的值是1053,再往后循环只会越来越小)。 + + +第三个情况比较难以检测和处理。我们怎么知道它会继续变大,而不是最终得到 1 呢?我们可以仔细想一想,每一位数的最大数字的下一位数是多少。 + +| 位数 | 最大值 | Next | +|--------|----------------|------| +| 1 | 9 | 81 | +| 2 | 99 | 162 | +| 3 | 999 | 243 | +| 4 | 9999 | 324 | +| 13 | 9999999999999 | 1053 | + + +- 对于 3 位数的数字,它不可能大于 243。这意味着它要么被困在 243 以下的循环内,要么跌到 1。 + +- 4 位或 4 位以上的数字在每一步都会丢失一位,直到降到 3 位为止。 + +- 所以我们知道,最坏的情况下,算法可能会在 243 以下的所有数字上循环,然后回到它已经到过的一个循环或者回到 1。但它不会无限期地进行下去,所以我们排除第三种选择。 + + + +##### 方法一:哈希表检测 + +所以这道题的解法主要是两部分: + +- 按照题目的要求做数位分离,求平方和。 + +- 使用哈希集合完成。每次生成链中的下一个数字时,我们都会检查它是否已经在哈希集合中。 + + - 如果它不在哈希集合中,我们应该添加它。 + - 如果它在哈希集合中,这意味着我们处于一个循环中,因此应该返回 false。 + + + +```java +class Solution { + private int getNext(int n) { + int totalSum = 0; + while (n > 0) { + int d = n % 10; + n = n / 10; + totalSum += d * d; + } + return totalSum; + } + + public boolean isHappy(int n) { + Set seen = new HashSet<>(); + while (n != 1 && !seen.contains(n)) { + seen.add(n); + n = getNext(n); + } + return n == 1; + } +} +``` + + +- 时间复杂度: O(Logn) +- 空间复杂度: O(Logn) + +##### 方法二:快慢指针法 + +通过反复调用 getNext(n) 得到的链是一个隐式的链表。隐式意味着我们没有实际的链表节点和指针,但数据仍然形成链表结构。起始数字是链表的头 “节点”,链中的所有其他数字都是节点。next 指针是通过调用 getNext(n) 函数获得。 + +意识到我们实际有个链表,那么这个问题就可以转换为检测一个链表是否有环。因此我们在这里可以使用弗洛伊德循环查找算法。这个算法是两个奔跑选手,一个跑的快,一个跑得慢。在龟兔赛跑的寓言中,跑的慢的称为 “乌龟”,跑得快的称为 “兔子”。 + +不管乌龟和兔子在循环中从哪里开始,它们最终都会相遇。这是因为兔子每走一步就向乌龟靠近一个节点(在它们的移动方向上)。 + +```java +class Solution { + + public int getNext(int n) { + int totalSum = 0; + while (n > 0) { + int d = n % 10; + n = n / 10; + totalSum += d * d; + } + return totalSum; + } + + public boolean isHappy(int n) { + int slowRunner = n; + int fastRunner = getNext(n); + while (fastRunner != 1 && slowRunner != fastRunner) { + slowRunner = getNext(slowRunner); + fastRunner = getNext(getNext(fastRunner)); + } + return fastRunner == 1; + } +} + +``` + +复杂度分析: + +- 时间复杂度:O(logn)。该分析建立在对前一种方法的分析的基础上,但是这次我们需要跟踪两个指针而不是一个指针来分析,以及在它们相遇前需要绕着这个循环走多少次。 +如果没有循环,那么快跑者将先到达 1,慢跑者将到达链表中的一半。我们知道最坏的情况下,成本是 O(2⋅logn)=O(logn)。 +一旦两个指针都在循环中,在每个循环中,快跑者将离慢跑者更近一步。一旦快跑者落后慢跑者一步,他们就会在下一步相遇。假设循环中有 k 个数字。如果他们的起点是相隔 k−1 的位置(这是他们可以开始的最远的距离),那么快跑者需要 k−1 步才能到达慢跑者,这对于我们的目的来说也是不变的。因此,主操作仍然在计算起始 n 的下一个值,即 O(logn)。 +- 空间复杂度:O(1),对于这种方法,我们不需要哈希集来检测循环。指针需要常数的额外空间。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" "b/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" new file mode 100644 index 00000000..2051ee00 --- /dev/null +++ "b/Algorithm/46.\345\255\230\345\234\250\351\207\215\345\244\215\345\205\203\347\264\240 II.md" @@ -0,0 +1,112 @@ +46.存在重复元素 II +=== + + +### 题目 + +给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。 + + + +示例 1: + +- 输入:nums = [1,2,3,1], k = 3 +- 输出:true + +示例 2: + +- 输入:nums = [1,0,1,1], k = 1 +- 输出:true + +示例 3: + +- 输入:nums = [1,2,3,1,2,3], k = 2 +- 输出:false + + + + +提示: + +- 1 <= nums.length <= 105 +- -109 <= nums[i] <= 109 +- 0 <= k <= 105 + + +### 思路 + +##### 方法一: 哈希表 + +```python +class Solution: + def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: + map = defaultdict() + for i, num in enumerate(nums): + if num in map: + m = map[num] + if i - m > k: + return True + map[num] = i + return False +``` + +复杂度分析: + +- 时间复杂度:O(n) +- 空间复杂度:O(n) + + +##### 方法二: 定长滑动窗口 + + +整理题意:是否存在长度不超过的 k+1 窗口,窗口内有相同元素。 + + +我们可以从前往后遍历 nums,同时使用 Set 记录遍历当前滑窗内出现过的元素。 + +假设当前遍历的元素为 nums[i]: + +- 下标小于等于 k(起始滑窗长度还不足 k+1):直接往滑窗加数,即将当前元素加入 Set 中; +- 下标大于 k:将上一滑窗的左端点元素 nums[i−k−1] 移除,判断当前滑窗的右端点元素 nums[i] 是否存在 Set 中,若存在,返回 True,否则将当前元素 nums[i] 加入 Set 中。 +- 重复上述过程,若整个 nums 处理完后仍未找到,返回 False。 + +```python +class Solution: + def containsNearbyDuplicate(self, nums: List[int], k: int) -> bool: + n = len(nums) + s = set() + for i in range(n): + if i > k: + s.remove(nums[i - k - 1]) + if nums[i] in s: + return True + s.add(nums[i]) + return False +``` + +```java +class Solution { + public boolean containsNearbyDuplicate(int[] nums, int k) { + int n = nums.length; + Set set = new HashSet<>(); + for (int i = 0; i < n; i++) { + if (i > k) set.remove(nums[i - k - 1]); + if (set.contains(nums[i])) return true; + set.add(nums[i]); + } + return false; + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n) +- 空间复杂度:O(k) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" "b/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" new file mode 100644 index 00000000..0dd4b5f5 --- /dev/null +++ "b/Algorithm/47.\346\234\200\351\225\277\350\277\236\347\273\255\345\272\217\345\210\227.md" @@ -0,0 +1,133 @@ +47.最长连续序列 +=== + + +### 题目 + +给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。 + +请你设计并实现时间复杂度为 O(n) 的算法解决此问题。 + + + +示例 1: + +- 输入:nums = [100,4,200,1,3,2] +- 输出:4 +- 解释:最长数字连续序列是 [1, 2, 3, 4]。它的长度为 4。 + +示例 2: + +- 输入:nums = [0,3,7,2,5,8,4,6,0,1] +- 输出:9 + +示例 3: + +- 输入:nums = [1,0,1,2] +- 输出:3 + + +提示: + +- 0 <= nums.length <= 105 +- -109 <= nums[i] <= 109 + + +### 思路 + +首先,本题是不能排序的,因为排序的时间复杂度是 O(nlogn),不符合题目 O(n) 的要求。 + + +题解说的比较复杂,不太容易懂,简单来说就是每个数都判断一次这个数是不是连续序列的开头那个数。 + +怎么判断呢,就是用哈希表查找这个数前面一个数是否存在,即num-1在序列中是否存在。 + +为了做到 O(n) 的时间复杂度,需要两个关键优化: + +- 把 nums 中的数都放入一个哈希集合中,这样可以 O(1) 判断数字是否在 nums 中。 +- 存在那这个数肯定不是开头,直接跳过。 +- 因此只需要对每个开头的数进行循环,直到这个序列不再连续,因此复杂度是O(n)。 + +以题解中的序列举例: +[100,4,200,1,3,4,2] +去重后的哈希序列为: +[100,4,200,1,3,2] + +按照上面逻辑进行判断: + +- 元素100是开头,因为没有99,且以100开头的序列长度为1 +- 元素4不是开头,因为有3存在,过 +- 元素200是开头,因为没有199,且以200开头的序列长度为1 +- 元素1是开头,因为没有0,且以1开头的序列长度为4,因为依次累加,2,3,4都存在。 +- 元素3不是开头,因为2存在,过, +- 元素2不是开头,因为1存在,过。 + + +```java +class Solution { + public int longestConsecutive(int[] nums) { + Set num_set = new HashSet(); + for (int num : nums) { + num_set.add(num); + } + + int longestStreak = 0; + + for (int num : num_set) { + if (!num_set.contains(num - 1)) { + int currentNum = num; + int currentStreak = 1; + + while (num_set.contains(currentNum + 1)) { + currentNum += 1; + currentStreak += 1; + } + + longestStreak = Math.max(longestStreak, currentStreak); + } + } + + return longestStreak; + } +} +``` + +```python +class Solution: + def longestConsecutive(self, nums: List[int]) -> int: + longest = 0 + numSet = set(nums) + + for num in numSet: + if num -1 not in numSet: + # 这就是开头,可以开始计数 + currentNum = num + currentStreak = 1 + + while currentNum + 1 in numSet: + currentNum += 1 + currentStreak += 1 + + + longest = max(longest, currentStreak) + + return longest +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 为数组的长度。具体分析已在上面正文中给出。 + +- 空间复杂度:O(n)。哈希表存储数组中所有的数需要 O(n) 的空间。 + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" "b/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" new file mode 100644 index 00000000..60983af9 --- /dev/null +++ "b/Algorithm/48.\346\261\207\346\200\273\345\214\272\351\227\264.md" @@ -0,0 +1,134 @@ +48.汇总区间 +=== + + +### 题目 + + +给定一个 无重复元素 的 有序 整数数组 nums 。 + +区间 [a,b] 是从 a 到 b(包含)的所有整数的集合。 + +返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说,nums 的每个元素都恰好被某个区间范围所覆盖,并且不存在属于某个区间但不属于 nums 的数字 x 。 + +列表中的每个区间范围 [a,b] 应该按如下格式输出: + +- "a->b" ,如果 a != b +- "a" ,如果 a == b + + +示例 1: + +- 输入:nums = [0,1,2,4,5,7] +- 输出:["0->2","4->5","7"] + +解释:区间范围是: +- [0,2] --> "0->2" +- [4,5] --> "4->5" +- [7,7] --> "7" + +示例 2: + +- 输入:nums = [0,2,3,4,6,8,9] +- 输出:["0","2->4","6","8->9"] + +解释:区间范围是: + +- [0,0] --> "0" +- [2,4] --> "2->4" +- [6,6] --> "6" +- [8,9] --> "8->9" + + +提示: + +- 0 <= nums.length <= 20 +- -231 <= nums[i] <= 231 - 1 +- nums 中的所有值都 互不相同 +- nums 按升序排列 + + +### 思路 + + +- 我们从数组的位置 0 出发,向右遍历。 +- 每次遇到相邻元素之间的差值大于 1 时,我们就找到了一个区间。遍历完数组之后,就能得到一系列的区间的列表。 +- 在遍历过程中,维护下标low和high分别记录区间的起点和终点,对于任何区间都有`low≤high`。当得到一个区间时,根据`low`和`high`的值生成区间的字符串表示。 +- 当`low summaryRanges(int[] nums) { + List ret = new ArrayList(); + int i = 0; + int n = nums.length; + while (i < n) { + int low = i; + i++; + while (i < n && nums[i] == nums[i - 1] + 1) { + i++; + } + int high = i - 1; + StringBuffer temp = new StringBuffer(Integer.toString(nums[low])); + if (low < high) { + temp.append("->"); + temp.append(Integer.toString(nums[high])); + } + ret.add(temp.toString()); + } + return ret; + } +} +``` + + +```python +class Solution: + def summaryRanges(self, nums: List[int]) -> List[str]: + def f(i: int, j: int) -> str: + return str(nums[i]) if i == j else f'{nums[i]}->{nums[j]}' + + i = 0 + n = len(nums) + ans = [] + while i < n: + j = i + while j + 1 < n and nums[j + 1] == nums[j] + 1: + j += 1 + ans.append(f(i, j)) + i = j + 1 + return ans +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 为数组的长度。 + +- 空间复杂度:O(1)。除了用于输出的空间外,额外使用的空间为常数。 + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" "b/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" new file mode 100644 index 00000000..8040592e --- /dev/null +++ "b/Algorithm/49.\345\220\210\345\271\266\345\214\272\351\227\264.md" @@ -0,0 +1,82 @@ +49.合并区间 +=== + + +### 题目 + +以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间 。 + + + +示例 1: + +- 输入:intervals = [[2,6],[1,3],[8,10],[15,18]] +- 输出:[[1,6],[8,10],[15,18]] +- 解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. + +示例 2: + +- 输入:intervals = [[1,4],[4,5]] +- 输出:[[1,5]] +- 解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。 + + +提示: + +- 1 <= intervals.length <= 104 +- intervals[i].length == 2 +- 0 <= starti <= endi <= 104 + + +### 思路 + +如果我们按照区间的左端点排序,那么在排完序的列表中,可以合并的区间一定是连续的。如下图所示,标记为蓝色、黄色和绿色的区间分别可以合并成一个大区间,它们在排完序的列表中是连续的: + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_merge_qujian_1.png?raw=true) + +- 我们用数组 merged 存储最终的答案。 + +- 首先,我们将列表中的区间按照左端点升序排序。然后我们将第一个区间加入 merged 数组中,并按顺序依次考虑之后的每个区间: + +- 如果当前区间的左端点在数组 merged 中最后一个区间的右端点之后,那么它们不会重合,我们可以直接将这个区间加入数组 merged 的末尾; + +- 否则,它们重合,我们需要用当前区间的右端点更新数组 merged 中最后一个区间的右端点,将其置为二者的较大值。 + +```java +class Solution { + public int[][] merge(int[][] intervals) { + if (intervals.length == 0) { + return new int[0][2]; + } + Arrays.sort(intervals, new Comparator() { + public int compare(int[] interval1, int[] interval2) { + return interval1[0] - interval2[0]; + } + }); + List merged = new ArrayList(); + for (int i = 0; i < intervals.length; ++i) { + int L = intervals[i][0], R = intervals[i][1]; + if (merged.size() == 0 || merged.get(merged.size() - 1)[1] < L) { + merged.add(new int[]{L, R}); + } else { + merged.get(merged.size() - 1)[1] = Math.max(merged.get(merged.size() - 1)[1], R); + } + } + return merged.toArray(new int[merged.size()][]); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)。 + +- 空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,使用的额外空间。O(logn) 即为排序所需要的空间复杂度。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" "b/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" new file mode 100644 index 00000000..6fbe536e --- /dev/null +++ "b/Algorithm/50.\346\217\222\345\205\245\345\214\272\351\227\264.md" @@ -0,0 +1,102 @@ +50.插入区间 +=== + + +### 题目 + +给你一个 无重叠的 ,按照区间起始端点排序的区间列表 intervals,其中 intervals[i] = [starti, endi] 表示第 i 个区间的开始和结束,并且 intervals 按照 starti 升序排列。同样给定一个区间 newInterval = [start, end] 表示另一个区间的开始和结束。 + +在 intervals 中插入区间 newInterval,使得 intervals 依然按照 starti 升序排列,且区间之间不重叠(如果有必要的话,可以合并区间)。 + +返回插入之后的 intervals。 + +注意 你不需要原地修改 intervals。你可以创建一个新数组然后返回它。 + + + +示例 1: + +- 输入:intervals = [[1,3],[6,9]], newInterval = [2,5] +- 输出:[[1,5],[6,9]] + +示例 2: + +- 输入:intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]], newInterval = [4,8] +- 输出:[[1,2],[3,10],[12,16]] +- 解释:这是因为新的区间 [4,8] 与 [3,5],[6,7],[8,10] 重叠。 + + +提示: + +- 0 <= intervals.length <= 104 +- intervals[i].length == 2 +- 0 <= starti <= endi <= 105 +- intervals 根据 starti 按 升序 排列 +- newInterval.length == 2 +- 0 <= start <= end <= 105 + +### 思路 + +- 题目说了是无重叠的按照区间起始端点排序的,所以不用排序了。 +- 用指针去扫 intervals,最多可能有三个阶段: + + - 不重叠的绿区间,在蓝区间的左边 + - 有重叠的绿区间 + - 不重叠的绿区间,在蓝区间的右边 + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_insert_qujian_1.png?raw=true) + +- 逐个分析 + + - 不重叠,需满足:绿区间的右端,位于蓝区间的左端的左边,如 [1,2]。 + + - 则当前绿区间,推入 res 数组,指针 +1,考察下一个绿区间。 + - 循环结束时,当前绿区间的屁股,就没落在蓝区间之前,有重叠了,如 [3,5]。 + - 现在看重叠的。我们反过来想,没重叠,就要满足:绿区间的左端,落在蓝区间的屁股的后面,反之就有重叠:绿区间的左端 <= 蓝区间的右端,极端的例子就是 [8,10]。 + + - 和蓝有重叠的区间,会合并成一个区间:左端取蓝绿左端的较小者,右端取蓝绿右端的较大者,不断更新给蓝区间。 + - 循环结束时,将蓝区间(它是合并后的新区间)推入 res 数组。 + - 剩下的,都在蓝区间右边,不重叠。不用额外判断,依次推入 res 数组。 + + + + +```java +class Solution { + public int[][] insert(int[][] intervals, int[] newInterval) { + ArrayList res = new ArrayList<>(); + int len = intervals.length; + int i = 0; + // 判断左边不重合 + while (i < len && intervals[i][1] < newInterval[0]) { + res.add(intervals[i]); + i++; + } + // 判断重合 + while (i < len && intervals[i][0] <= newInterval[1]) { + newInterval[0] = Math.min(intervals[i][0], newInterval[0]); + newInterval[1] = Math.max(intervals[i][1], newInterval[1]); + i++; + } + res.add(newInterval); + // 判断右边不重合 + while (i < len && intervals[i][0] > newInterval[1]) { + res.add(intervals[i]); + i++; + } + return res.toArray(new int[0][]); + } +} +``` + +复杂度分析: + +- 时间复杂度:O(n),其中 n 是数组 intervals 的长度,即给定的区间个数。 + +- 空间复杂度:O(n)。 + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" "b/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" new file mode 100644 index 00000000..a6fcbec6 --- /dev/null +++ "b/Algorithm/51.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" @@ -0,0 +1,110 @@ +51.用最少数量的箭引爆气球 +=== + + +### 题目 + +有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstart 和 xend之间的气球。你不知道气球的确切 y 坐标。 + +一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足 xstart ≤ x ≤ xend,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。 + +给你一个数组 points ,返回引爆所有气球所必须射出的 最小 弓箭数 。 + + +示例 1: + +- 输入:points = [[10,16],[2,8],[1,6],[7,12]] +- 输出:2 +- 解释:气球可以用2支箭来爆破: + - 在x = 6处射出箭,击破气球[2,8]和[1,6]。 + - 在x = 11处发射箭,击破气球[10,16]和[7,12]。 + +示例 2: + +- 输入:points = [[1,2],[3,4],[5,6],[7,8]] +- 输出:4 +- 解释:每个气球需要射出一支箭,总共需要4支箭。 + +示例 3: + +- 输入:points = [[1,2],[2,3],[3,4],[4,5]] +- 输出:2 +- 解释:气球可以用2支箭来爆破: + - 在x = 2处发射箭,击破气球[1,2]和[2,3]。 + - 在x = 4处射出箭,击破气球[3,4]和[4,5]。 + + +提示: + +- 1 <= points.length <= 105 +- points[i].length == 2 +- -231 <= xstart < xend <= 231 - 1 + + +### 思路 + +##### 方法一:区间合并 + +```java +//思路:区间重叠则合并,合并为交集 +//先排序 +class Solution { + public int findMinArrowShots(int[][] points) { + //防止相减式的比较造成溢出 + Arrays.sort(points,(o1,o2)->Integer.compare(o1[0],o2[0])); + int n = points.length; // 合并一次,n-1 + for(int i=1;i a[1] > b[1] ? 1 : -1); + //获取排序后第一个气球右边界的位置,我们可以认为是箭射入的位置 + int last = points[0][1]; + //统计箭的数量 + int count = 1; + for (int i = 1; i < points.length; i++) { + //如果箭射入的位置小于下标为i这个气球的左边位置,说明这支箭不能 + //击爆下标为i的这个气球,需要再拿出一支箭,并且要更新这支箭射入的 + //位置 + if (last < points[i][0]) { + last = points[i][1]; + count++; + } + } + return count; +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" "b/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" new file mode 100644 index 00000000..a9518cdc --- /dev/null +++ "b/Algorithm/52.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" @@ -0,0 +1,140 @@ +52.有效的括号 +=== + + +### 题目 + +给定一个只包括 '(',')','{','}','[',']' 的字符串s,判断字符串是否有效。 + +有效字符串需满足: + +- 左括号必须用相同类型的右括号闭合。 +- 左括号必须以正确的顺序闭合。 +- 每个右括号都有一个对应的相同类型的左括号。 + + +示例 1: + +- 输入:s = "()" + +- 输出:true + +示例 2: + +- 输入:s = "()[]{}" + +- 输出:true + +示例 3: + +- 输入:s = "(]" + +- 输出:false + +示例 4: + +- 输入:s = "([])" + +- 输出:true + + + +提示: + +- 1 <= s.length <= 104 +- s 仅由括号`()[]{}`组成 + + +### 思路 + +要求是“以正确的顺序闭合”,所以`([)]`这种是错误的。 + +要判断一个仅包含括号字符`()[]{}`的字符串是否有效(即括号是否按正确顺序闭合),可通过 ​栈`Stack`这一数据结构高效解决。 + + +括号的闭合需满足 ​​“后进先出”​​ 的嵌套关系: 最后出现的左括号需优先匹配右括号,栈的`LIFO后进先出`特性完美契合此需求。 + + + +​- 遍历字符​: 逐个处理字符串中的字符。 +​- 左括号入栈​: 遇到`(, {, [`时压入栈。 +​- 右括号匹配​: 遇到`), }, ]`时: + - 若栈为空 `→` 无效(右括号多余)。 + - 若栈顶左括号与当前右括号不匹配 `→` 无效。 + - 匹配则弹出栈顶元素。 + +- 最终校验​: 遍历结束后栈必须为空(左括号全被匹配)。 + +使用`ArrayDeque`替代传统`Stack`类,因前者在`push/pop`操作上效率更高(无同步开销)。 + + +​```java +import java.util.ArrayDeque; +import java.util.Deque; +public class Solution { + public boolean isValid(String s) { + // 边界处理:空字符串有效,null 或奇数长度直接无效 + if (s == null) { + return false; + } + if (s.isEmpty()) { + return true; + } + if (s.length() % 2 != 0) { + // 奇数长度不可能完全匹配 + return false; + } + + // 使用双端队列模拟栈(高效) + Deque stack = new ArrayDeque<>(); + + for (char c : s.toCharArray()) { + // 左括号:入栈 + if (c == '(' || c == '{' || c == '[') { + stack.push(c); + } + // 右括号:检查匹配 + else { + if (stack.isEmpty()) return false; // 栈空说明右括号多余 + char top = stack.pop(); // 弹出栈顶左括号 + // 检查括号类型是否匹配 + if ((c == ')' && top != '(') || + (c == '}' && top != '{') || + (c == ']' && top != '[')) { + return false; + } + } + } + return stack.isEmpty(); // 栈空说明所有左括号均被匹配 + } +} +``` + + + + +```python +# 映射表优化:用字典 bracket_map 存储括号对应关系,避免冗长的 if-else 分支 +def isValid(s: str) -> bool: + stack = [] + bracket_map = {')': '(', ']': '[', '}': '{'} # 右括号到左括号的映射 + for char in s: + if char in bracket_map.values(): # 左括号入栈 + stack.append(char) + elif char in bracket_map: # 右括号匹配 + if not stack or stack.pop() != bracket_map[char]: + return False + return not stack +``` + +复杂度分析: + +- 时间复杂度: O(N) +- 空间复杂度: O(N) + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" "b/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" new file mode 100644 index 00000000..a9ace618 --- /dev/null +++ "b/Algorithm/53.\347\256\200\345\214\226\350\267\257\345\276\204.md" @@ -0,0 +1,127 @@ +53.简化路径 +=== + + +### 题目 + +给你一个字符串path,表示指向某一文件或目录的 Unix 风格 绝对路径(以 '/' 开头),请你将其转化为 更加简洁的规范路径。 + +在Unix风格的文件系统中规则如下: + +- 一个点 '.' 表示当前目录本身。 +- 此外,两个点 '..' 表示将目录切换到上一级(指向父目录)。 +- 任意多个连续的斜杠(即,'//' 或 '///')都被视为单个斜杠 '/'。 +- 任何其他格式的点(例如,'...' 或 '....')均被视为有效的文件/目录名称。 + +返回的 简化路径 必须遵循下述格式: + +- 始终以斜杠 '/' 开头。 +- 两个目录名之间必须只有一个斜杠 '/' 。 +- 最后一个目录名(如果存在)不能 以 '/' 结尾。 +- 此外,路径仅包含从根目录到目标文件或目录的路径上的目录(即,不含 '.' 或 '..')。 + +返回简化后得到的 规范路径 。 + + + +示例 1: + +- 输入:path = "/home/" + +- 输出:"/home" + +解释: + +- 应删除尾随斜杠。 + +示例 2: + +- 输入:path = "/home//foo/" + +- 输出:"/home/foo" + +解释: + +- 多个连续的斜杠被单个斜杠替换。 + +示例 3: + +- 输入:path = "/home/user/Documents/../Pictures" + +- 输出:"/home/user/Pictures" + +解释: + +- 两个点 ".." 表示上一级目录(父目录)。 + +示例 4: + +- 输入:path = "/../" + +- 输出:"/" + +解释: + +- 不可能从根目录上升一级目录。 + +示例 5: + +- 输入:path = "/.../a/../b/c/../d/./" + +- 输出:"/.../b/d" + +解释: + +- "..." 在这个问题中是一个合法的目录名。 + + + +提示: + +- 1 <= path.length <= 3000 +- path 由英文字母,数字,'.','/' 或 '_' 组成。 +- path 是一个有效的 Unix 风格绝对路径。 + +### 思路 + + +根据题意,使用栈进行模拟即可。 + +具体的,从前往后处理 path,每次以 item 为单位进行处理(有效的文件名),根据 item 为何值进行分情况讨论: + +- item 为有效值 : 存入栈中 +- item 为 .. : 弹出栈顶元素(若存在) +- item 为 . : 不作处理 + +```java +public String simplifyPath(String path) { + Deque deque = new ArrayDeque<>(); + int n = path.length(); + for (int i = 1; i < n; i++) { + if (path.charAt(i) == '/') continue; // 找到下一个不是"/"的位置 + int j = i + 1; // j指向下一个位置,双指针! + while (j < n && path.charAt(j) != '/') j++; // 直到j指向的位置是"/" + String temp = path.substring(i, j); // 左闭右开,[i.j) + if (temp.equals("..")) { //..的时候,栈内有元素才删 + if (!deque.isEmpty()) { + deque.pollLast(); + } + } else if (!(temp.equals("."))) { // .的时候就continue,这里省略了,不是.的时候就进栈 + deque.addLast(temp); + } + i = j; //j是指向"/"的,令i=j开始下一轮循环 + } + StringBuilder ans = new StringBuilder(); + while (!deque.isEmpty()) { // 还原栈内的路径 + ans.append("/"); + ans.append(deque.pollFirst()); + } + return ans.length() == 0 ? "/" : ans.toString(); // 栈为空就直接返回"/",否则返回ans.toString() +} +``` + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" "b/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" new file mode 100644 index 00000000..11a8c8c2 --- /dev/null +++ "b/Algorithm/54.\346\234\200\345\260\217\346\240\210.md" @@ -0,0 +1,103 @@ +54.最小栈 +=== + + +### 题目 + +设计一个支持 push ,pop ,top 操作,并能在常数时间内检索到最小元素的栈。 + +实现 MinStack 类: + +- MinStack() 初始化堆栈对象。 +- void push(int val) 将元素val推入堆栈。 +- void pop() 删除堆栈顶部的元素。 +- int top() 获取堆栈顶部的元素。 +- int getMin() 获取堆栈中的最小元素。 + + +示例 1: + +- 输入: +``` +["MinStack","push","push","push","getMin","pop","top","getMin"] +[[],[-2],[0],[-3],[],[],[],[]] +``` +输出: +[null,null,null,null,-3,null,0,-2] + +解释: +``` +MinStack minStack = new MinStack(); +minStack.push(-2); +minStack.push(0); +minStack.push(-3); +minStack.getMin(); --> 返回 -3. +minStack.pop(); +minStack.top(); --> 返回 0. +minStack.getMin(); --> 返回 -2. +``` + +提示: + +- -231 <= val <= 231 - 1 +- pop、top 和 getMin 操作总是在 非空栈 上调用 +- push, pop, top, and getMin最多被调用 3 * 104 次 + +### 思路 + + +解题思路: + +借用一个辅助栈min_stack,用于存获取stack中最小值。 + +算法流程: + +- push()方法: 每当push()新值进来时,如果小于等于min_stack栈顶值,则一起push()到min_stack,即更新了栈顶最小值 +- pop()方法: 判断将pop()出去的元素值是否是min_stack栈顶元素值(即最小值),如果是则将min_stack栈顶元素一起pop(),这样可以保证min_stack栈顶元素始终是stack中的最小值 +- getMin()方法: 返回min_stack栈顶即可 + +min_stack作用分析: + +min_stack等价于遍历stack所有元素,把升序的数字都删除掉,留下一个从栈底到栈顶降序的栈。 +相当于给stack中的降序元素做了标记,每当pop()这些降序元素,min_stack会将相应的栈顶元素pop()出去,保证其栈顶元素始终是stack中的最小元素。 +复杂度分析: + +- 时间复杂度 O(1) : 压栈,出栈,获取最小值的时间复杂度都为 O(1) 。 +- 空间复杂度 O(N) : 包含 N 个元素辅助栈占用线性大小的额外空间。 + +```java +class MinStack { + private Stack stack; + private Stack min_stack; + public MinStack() { + stack = new Stack<>(); + min_stack = new Stack<>(); + } + public void push(int x) { + stack.push(x); + if(min_stack.isEmpty() || x <= min_stack.peek()) { + min_stack.push(x); + } + } + public void pop() { + if(stack.pop().equals(min_stack.peek())) { + min_stack.pop(); + } + } + + public int top() { + return stack.peek(); + } + + public int getMin() { + return min_stack.peek(); + } +} +``` + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" "b/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" new file mode 100644 index 00000000..11250060 --- /dev/null +++ "b/Algorithm/55.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" @@ -0,0 +1,137 @@ +55.逆波兰表达式求值 +=== + + +### 题目 + +给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。 + +请你计算该表达式。返回一个表示表达式值的整数。 + +注意: + +- 有效的算符为 '+'、'-'、'*' 和 '/' 。 +- 每个操作数(运算对象)都可以是一个整数或者另一个表达式。 +- 两个整数之间的除法总是 向零截断 。 +- 表达式中不含除零运算。 +- 输入是一个根据逆波兰表示法表示的算术表达式。 +- 答案及所有中间计算结果可以用 32 位 整数表示。 + + +示例 1: + +- 输入:tokens = ["2","1","+","3","*"] +- 输出:9 +- 解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 + +示例 2: + +- 输入:tokens = ["4","13","5","/","+"] +- 输出:6 +- 解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 + +示例 3: + +- 输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"] +- 输出:22 +- 解释:该算式转化为常见的中缀算术表达式为: +``` + ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 += ((10 * (6 / (12 * -11))) + 17) + 5 += ((10 * (6 / -132)) + 17) + 5 += ((10 * 0) + 17) + 5 += (0 + 17) + 5 += 17 + 5 += 22 +``` + +提示: + +- 1 <= tokens.length <= 104 +- tokens[i]是一个算符(`+`、`-`、`*`或`/`),或是在范围`[-200, 200]`内的一个整数 + + +逆波兰表达式: + +- 逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。 + +- 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 +- 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。 + +逆波兰表达式主要有以下两个优点: + +- 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 +- 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中 + +### 思路 + +逆波兰表达式,也叫做后缀表达式。 + +我们平时见到的运算表达式是中缀表达式,即 "操作数① 运算符② 操作数③" 的顺序,运算符在两个操作数中间。 +但是后缀表达式是 "操作数① 操作数③ 运算符②" 的顺序,运算符在两个操作数之后。 + + +逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时,使用一个栈存储操作数,从左到右遍历逆波兰表达式,进行如下操作: + +- 如果遇到操作数,则将操作数入栈; + +- 如果遇到运算符,则将两个操作数出栈,其中先出栈的是右操作数,后出栈的是左操作数,使用运算符对两个操作数进行运算,将运算得到的新操作数入栈。 + +整个逆波兰表达式遍历完毕之后,栈内只有一个元素,该元素即为逆波兰表达式的值。 + + +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN1.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN2.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN3.png?raw=true) +![image](https://raw.githubusercontent.com/CharonChui/Pictures/master/leetcode_evalRPN4.png?raw=true) + +```java +class Solution { + public int evalRPN(String[] tokens) { + Deque stack = new LinkedList(); + int n = tokens.length; + for (int i = 0; i < n; i++) { + String token = tokens[i]; + if (isNumber(token)) { + stack.push(Integer.parseInt(token)); + } else { + int num2 = stack.pop(); + int num1 = stack.pop(); + switch (token) { + case "+": + stack.push(num1 + num2); + break; + case "-": + stack.push(num1 - num2); + break; + case "*": + stack.push(num1 * num2); + break; + case "/": + stack.push(num1 / num2); + break; + default: + } + } + } + return stack.pop(); + } + + public boolean isNumber(String token) { + return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token)); + } +} +``` + +复杂度分析: + +- 时间复杂度: O(n),其中 n 是数组 tokens 的长度。需要遍历数组 tokens 一次,计算逆波兰表达式的值。 + +- 空间复杂度: O(n),其中 n 是数组 tokens 的长度。使用栈存储计算过程中的数,栈内元素个数不会超过逆波兰表达式的长度。 + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git "a/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" "b/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" new file mode 100644 index 00000000..10656199 --- /dev/null +++ "b/Algorithm/56.\345\237\272\346\234\254\350\256\241\347\256\227\345\231\250.md" @@ -0,0 +1,163 @@ +56.基本计算器 +=== + + +### 题目 + +给你一个字符串表达式 s ,请你实现一个基本计算器来计算并返回它的值。 + +注意:不允许使用任何将字符串作为数学表达式计算的内置函数,比如 eval() 。 + + + +示例 1: + +- 输入:s = "1 + 1" +- 输出:2 + +示例 2: + +- 输入:s = " 2-1 + 2 " +- 输出:3 + +示例 3: + +- 输入:s = "(1+(4+5+2)-3)+(6+8)" +- 输出:23 + + +提示: + +- 1 <= s.length <= 3 * 105 +- s 由数字、'+'、'-'、'('、')'、和 ' ' 组成 +- s 表示一个有效的表达式 +- '+' 不能用作一元运算(例如, "+1" 和 "+(2 + 3)" 无效) +- '-' 可以用作一元运算(即 "-1" 和 "-(2 + 3)" 是有效的) +- 输入中不存在两个连续的操作符 +- 每个数字和运行的计算将适合于一个有符号的 32位 整数 + +### 思路 + + +【进阶补充】双栈解决通用「表达式计算」问题 + + +宫水三叶 +76872 +2021.03.10 +发布于 上海市 +栈 +数学 +C++ +Java +双栈解法 + +我们可以使用两个栈 nums 和 ops 。 + +nums : 存放所有的数字 +ops :存放所有的数字以外的操作,+/- 也看做是一种操作 +然后从前往后做,对遍历到的字符做分情况讨论: + +空格 : 跳过 +( : 直接加入 ops 中,等待与之匹配的 ) +) : 使用现有的 nums 和 ops 进行计算,直到遇到左边最近的一个左括号为止,计算结果放到 nums +数字 : 从当前位置开始继续往后取,将整一个连续数字整体取出,加入 nums ++/- : 需要将操作放入 ops 中。在放入之前先把栈内可以算的都算掉,使用现有的 nums 和 ops 进行计算,直到没有操作或者遇到左括号,计算结果放到 nums +一些细节: + +由于第一个数可能是负数,为了减少边界判断。一个小技巧是先往 nums 添加一个 0 +为防止 () 内出现的首个字符为运算符,将所有的空格去掉,并将 (- 替换为 (0-,(+ 替换为 (0+(当然也可以不进行这样的预处理,将这个处理逻辑放到循环里去做) + + + +class Solution { + public int calculate(String s) { + // 存放所有的数字 + Deque nums = new ArrayDeque<>(); + // 为了防止第一个数为负数,先往 nums 加个 0 + nums.addLast(0); + // 将所有的空格去掉 + s = s.replaceAll(" ", ""); + // 存放所有的操作,包括 +/- + Deque ops = new ArrayDeque<>(); + int n = s.length(); + char[] cs = s.toCharArray(); + for (int i = 0; i < n; i++) { + char c = cs[i]; + if (c == '(') { + ops.addLast(c); + } else if (c == ')') { + // 计算到最近一个左括号为止 + while (!ops.isEmpty()) { + char op = ops.peekLast(); + if (op != '(') { + calc(nums, ops); + } else { + ops.pollLast(); + break; + } + } + } else { + if (isNum(c)) { + int u = 0; + int j = i; + // 将从 i 位置开始后面的连续数字整体取出,加入 nums + while (j < n && isNum(cs[j])) u = u * 10 + (int)(cs[j++] - '0'); + nums.addLast(u); + i = j - 1; + } else { + if (i > 0 && (cs[i - 1] == '(' || cs[i - 1] == '+' || cs[i - 1] == '-')) { + nums.addLast(0); + } + // 有一个新操作要入栈时,先把栈内可以算的都算了 + while (!ops.isEmpty() && ops.peekLast() != '(') calc(nums, ops); + ops.addLast(c); + } + } + } + while (!ops.isEmpty()) calc(nums, ops); + return nums.peekLast(); + } + void calc(Deque nums, Deque ops) { + if (nums.isEmpty() || nums.size() < 2) return; + if (ops.isEmpty()) return; + int b = nums.pollLast(), a = nums.pollLast(); + char op = ops.pollLast(); + nums.addLast(op == '+' ? a + b : a - b); + } + boolean isNum(char c) { + return Character.isDigit(c); + } +} + +作者:宫水三叶 +链接:https://leetcode.cn/problems/basic-calculator/solutions/646865/shuang-zhan-jie-jue-tong-yong-biao-da-sh-olym/ +来源:力扣(LeetCode) +著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 + + + + + + + + + + + + + + + + + + + + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/37..md b/Algorithm/57..md similarity index 100% rename from Algorithm/37..md rename to Algorithm/57..md diff --git a/Algorithm/38..md b/Algorithm/58..md similarity index 100% rename from Algorithm/38..md rename to Algorithm/58..md diff --git a/Algorithm/39..md b/Algorithm/59..md similarity index 100% rename from Algorithm/39..md rename to Algorithm/59..md diff --git a/Algorithm/40..md b/Algorithm/60..md similarity index 100% rename from Algorithm/40..md rename to Algorithm/60..md diff --git a/Algorithm/61..md b/Algorithm/61..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/61..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + diff --git a/Algorithm/62..md b/Algorithm/62..md new file mode 100644 index 00000000..0970b4ab --- /dev/null +++ b/Algorithm/62..md @@ -0,0 +1,17 @@ +18.整数转罗马数字 +=== + + +### 题目 + + + +### 思路 + + + +--- +- 邮箱 :charon.chui@gmail.com +- Good Luck! + + From 94ae56aebb00c697e7846788aa9326e2a8f86a72 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 17 Sep 2025 14:35:37 +0800 Subject: [PATCH 126/128] add notes --- VideoDevelopment/JPEG 2.md | 102 ++++++++ VideoDevelopment/JPEG.md | 102 ++++++++ .../JPEG.md" | 117 +++++++++ .../PNG.md" | 247 ++++++++++++++++++ 4 files changed, 568 insertions(+) create mode 100644 VideoDevelopment/JPEG 2.md create mode 100644 VideoDevelopment/JPEG.md create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" diff --git a/VideoDevelopment/JPEG 2.md b/VideoDevelopment/JPEG 2.md new file mode 100644 index 00000000..fa200838 --- /dev/null +++ b/VideoDevelopment/JPEG 2.md @@ -0,0 +1,102 @@ +# JPEG + +JPEG(Joint Photographic Experts Group),全称为联合图像专家小组,是国际标准化组织(ISO)制定的静态图像压缩标准,文件扩展名为.jpg或.jpeg,适用于连续色调静止图像处理。 + + +JPEG只描述一副图像如何转换成一组数据流,而不论这些字节存储在何种介质上。由独立JPEG组创立的另一个进阶标准,JFIF(JPEGFileInterchangeFormat,JPEG文件交换格式)则描述JPEG数据流如何生成适于电脑存储或传送的图像。在一般应用中,我们从数码相机等来源获得的“JPEG文件”,指的就是JFIF文件,有时是ExifJPEG文件。 + + +该格式采用有损压缩算法,通过牺牲部分画质换取较小文件体积,压缩过程包含色彩空间转换(RGB转YCbCr)、离散余弦变换(DCT)、量化和熵编码等步骤。 + +支持标准JPEG、渐进式JPEG和JPEG2000三种格式,其中渐进式格式可实现图像由模糊到清晰的渐进加载. + +JPEG压缩技术通过分离高频与低频信息并对高频部分进行压缩,压缩比可通过量化表参数调节,典型压缩率为原始大小的10%. + + +### 说明 +1. jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 +2. jpeg文件中有一些形如0xFF**这样的数据,它们被称为标志(Markeer),它表示jpeg信息数据段。例如0xFFD8代表SOI(Startof image)。OxFFD9代表EOI(End of image)。 +4. jpeg图像由多个maker组成,多个maker+compressed组成了jpeg。 +5. jiff是一种在万维网上进行jpeg传输的格式,可以理解是对jpeg图片的封装,符合jpeg标准,jiff的maker是app0,记录了图像的基本信息,也可能有缩略图。jiff格式比较老,老式的数码相机使用此格式。 +6. exif新比较新的jpeg封装格式,exif的maker是app1,记录了更多的东西,如ISP信息、GPS信息、相机信息,图像旋转等等 +7. jiff和exif可以共存,很多jpeg图像都有app0的jiff段和app1的exif段 + + + +## 压缩流程 + +1. 原始图像 +2. 8x8分块 +3. DCT(Discrete Cosine Transform,离散余弦变换)变换: 把数据从时域转化到频域的数学方法,把图像数据转换到频域之后,可以从中分离出各种频率的信息。 +4. 量化 +5. Z字形扫描 +6. 对系数编码 +7. 熵编码 +8. 压缩数据 + + + + + +JPEG文件除了图像数据之外,还保存了与图片相关的各种信息,这些信息通过不同类型的TAG存储在文件中。 + + +## TAG + +JPEG通过TAG编辑压缩数据之外的信息。 + +所有的TAG都包含一个TAG类型,TAG类型的大小为两个字节,位于一个TAG的最前面。TAG类型的第一个字节一定为0xFF + +一般情况下,是按照这个顺序排列的: + + +| TAG 类型 | 数值(十六进制标识符) | 全称 | 其他备注 | +|-------------------|----------|----------------------------|-----------------------------------| +| SOI | 0xFFD8 | Start of Image | 必带 | +| APP0 | 0xFFE0 | application0 | 必带 | +| APPn | 0xFFEn | applicationn | 可选带(APP1一般为Exif信息) | +| DQT | 0xFFDB | Define Quantization Table | 必带 | +| SOF | 0xFFC0 | Start of Frame | 必带 | +| DHT | 0xFFC4 | Define Huffman Table | 必带 | +| SOS | 0xFFDA | Start of Scan | 必带 | +| compress data | ... | ... | 必带 | +| EOI | 0xFFD9 | End of Image | 必带 | + +标志OxFFE0~OxFFEF被称为"Application Marker",它们不是解码JPEG文件必须得,可以被用来存储配置信息等。 +EXIF也是利用这个标志段来插入信息的。具体来说,是APP1(0xFFE1)Marker,所有的EXIF信息都存储在该数据段。 +​EXIF(Exchangeable Image File Format)​​ 是嵌入在 JPEG 文件中的 ​元数据标准,记录拍摄设备、参数和场景信息,相当于照片的“数字身份证”。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_1.png?raw=true) + + +#### 典型JPEG文件结构 + +[SOI] -> [APP0] -> [APP1(EXIF)] -> [DQT] -> [SOF] -> [DHT] -> [SOS] -> [压缩数据] -> [EOI] + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_2.png?raw=true) + +##### 怎么判断是不是JPEG图片? + +jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 + + +其实很简单,就是判断前面3个字节是什么,如果发现是FF D8 FF开始,那就认为它是JEPG图片。(注意android不是根据后缀名来判断是什么文件的,当然你必须是图片的后缀名文件管理器才可以打开 + + + + + + + + + + + + + + + + + diff --git a/VideoDevelopment/JPEG.md b/VideoDevelopment/JPEG.md new file mode 100644 index 00000000..fa200838 --- /dev/null +++ b/VideoDevelopment/JPEG.md @@ -0,0 +1,102 @@ +# JPEG + +JPEG(Joint Photographic Experts Group),全称为联合图像专家小组,是国际标准化组织(ISO)制定的静态图像压缩标准,文件扩展名为.jpg或.jpeg,适用于连续色调静止图像处理。 + + +JPEG只描述一副图像如何转换成一组数据流,而不论这些字节存储在何种介质上。由独立JPEG组创立的另一个进阶标准,JFIF(JPEGFileInterchangeFormat,JPEG文件交换格式)则描述JPEG数据流如何生成适于电脑存储或传送的图像。在一般应用中,我们从数码相机等来源获得的“JPEG文件”,指的就是JFIF文件,有时是ExifJPEG文件。 + + +该格式采用有损压缩算法,通过牺牲部分画质换取较小文件体积,压缩过程包含色彩空间转换(RGB转YCbCr)、离散余弦变换(DCT)、量化和熵编码等步骤。 + +支持标准JPEG、渐进式JPEG和JPEG2000三种格式,其中渐进式格式可实现图像由模糊到清晰的渐进加载. + +JPEG压缩技术通过分离高频与低频信息并对高频部分进行压缩,压缩比可通过量化表参数调节,典型压缩率为原始大小的10%. + + +### 说明 +1. jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 +2. jpeg文件中有一些形如0xFF**这样的数据,它们被称为标志(Markeer),它表示jpeg信息数据段。例如0xFFD8代表SOI(Startof image)。OxFFD9代表EOI(End of image)。 +4. jpeg图像由多个maker组成,多个maker+compressed组成了jpeg。 +5. jiff是一种在万维网上进行jpeg传输的格式,可以理解是对jpeg图片的封装,符合jpeg标准,jiff的maker是app0,记录了图像的基本信息,也可能有缩略图。jiff格式比较老,老式的数码相机使用此格式。 +6. exif新比较新的jpeg封装格式,exif的maker是app1,记录了更多的东西,如ISP信息、GPS信息、相机信息,图像旋转等等 +7. jiff和exif可以共存,很多jpeg图像都有app0的jiff段和app1的exif段 + + + +## 压缩流程 + +1. 原始图像 +2. 8x8分块 +3. DCT(Discrete Cosine Transform,离散余弦变换)变换: 把数据从时域转化到频域的数学方法,把图像数据转换到频域之后,可以从中分离出各种频率的信息。 +4. 量化 +5. Z字形扫描 +6. 对系数编码 +7. 熵编码 +8. 压缩数据 + + + + + +JPEG文件除了图像数据之外,还保存了与图片相关的各种信息,这些信息通过不同类型的TAG存储在文件中。 + + +## TAG + +JPEG通过TAG编辑压缩数据之外的信息。 + +所有的TAG都包含一个TAG类型,TAG类型的大小为两个字节,位于一个TAG的最前面。TAG类型的第一个字节一定为0xFF + +一般情况下,是按照这个顺序排列的: + + +| TAG 类型 | 数值(十六进制标识符) | 全称 | 其他备注 | +|-------------------|----------|----------------------------|-----------------------------------| +| SOI | 0xFFD8 | Start of Image | 必带 | +| APP0 | 0xFFE0 | application0 | 必带 | +| APPn | 0xFFEn | applicationn | 可选带(APP1一般为Exif信息) | +| DQT | 0xFFDB | Define Quantization Table | 必带 | +| SOF | 0xFFC0 | Start of Frame | 必带 | +| DHT | 0xFFC4 | Define Huffman Table | 必带 | +| SOS | 0xFFDA | Start of Scan | 必带 | +| compress data | ... | ... | 必带 | +| EOI | 0xFFD9 | End of Image | 必带 | + +标志OxFFE0~OxFFEF被称为"Application Marker",它们不是解码JPEG文件必须得,可以被用来存储配置信息等。 +EXIF也是利用这个标志段来插入信息的。具体来说,是APP1(0xFFE1)Marker,所有的EXIF信息都存储在该数据段。 +​EXIF(Exchangeable Image File Format)​​ 是嵌入在 JPEG 文件中的 ​元数据标准,记录拍摄设备、参数和场景信息,相当于照片的“数字身份证”。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_1.png?raw=true) + + +#### 典型JPEG文件结构 + +[SOI] -> [APP0] -> [APP1(EXIF)] -> [DQT] -> [SOF] -> [DHT] -> [SOS] -> [压缩数据] -> [EOI] + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_2.png?raw=true) + +##### 怎么判断是不是JPEG图片? + +jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 + + +其实很简单,就是判断前面3个字节是什么,如果发现是FF D8 FF开始,那就认为它是JEPG图片。(注意android不是根据后缀名来判断是什么文件的,当然你必须是图片的后缀名文件管理器才可以打开 + + + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" new file mode 100644 index 00000000..bf05b544 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" @@ -0,0 +1,117 @@ +# JPEG + +[JPEG](https://www.w3.org/Graphics/JPEG/itu-t81.pdf) +JPEG(Joint Photographic Experts Group),全称为联合图像专家小组,是国际标准化组织(ISO)制定的静态图像压缩标准,文件扩展名为.jpg或.jpeg,适用于连续色调静止图像处理。 + + + +JPEG只描述一副图像如何转换成一组数据流,而不论这些字节存储在何种介质上。由独立JPEG组创立的另一个进阶标准,JFIF(JPEGFileInterchangeFormat,JPEG文件交换格式)则描述JPEG数据流如何生成适于电脑存储或传送的图像。在一般应用中,我们从数码相机等来源获得的“JPEG文件”,指的就是JFIF文件,有时是ExifJPEG文件。 + + +该格式采用有损压缩算法,通过牺牲部分画质换取较小文件体积,压缩过程包含色彩空间转换(RGB转YCbCr)、离散余弦变换(DCT)、量化和熵编码等步骤。 + +支持标准JPEG、渐进式JPEG和JPEG2000三种格式,其中渐进式格式可实现图像由模糊到清晰的渐进加载. + +JPEG压缩技术通过分离高频与低频信息并对高频部分进行压缩,压缩比可通过量化表参数调节,典型压缩率为原始大小的10%. + + +### 说明 +1. jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 +2. jpeg文件中有一些形如0xFF**这样的数据,它们被称为标志(Markeer),它表示jpeg信息数据段。例如0xFFD8代表SOI(Startof image)。OxFFD9代表EOI(End of image)。 +4. jpeg图像由多个maker组成,多个maker+compressed组成了jpeg。 +5. jiff是一种在万维网上进行jpeg传输的格式,可以理解是对jpeg图片的封装,符合jpeg标准,jiff的maker是app0,记录了图像的基本信息,也可能有缩略图。jiff格式比较老,老式的数码相机使用此格式。 +6. exif新比较新的jpeg封装格式,exif的maker是app1,记录了更多的东西,如ISP信息、GPS信息、相机信息,图像旋转等等 +7. jiff和exif可以共存,很多jpeg图像都有app0的jiff段和app1的exif段 + + + +## 压缩流程 + +1. 原始图像 +2. 8x8分块 +3. DCT(Discrete Cosine Transform,离散余弦变换)变换: 把数据从时域转化到频域的数学方法,把图像数据转换到频域之后,可以从中分离出各种频率的信息。 +4. 量化 +5. Z字形扫描 +6. 对系数编码 +7. 熵编码 +8. 压缩数据 + + + + + +JPEG文件除了图像数据之外,还保存了与图片相关的各种信息,这些信息通过不同类型的TAG存储在文件中。 + + +## TAG + +JPEG通过TAG编辑压缩数据之外的信息。 + +所有的TAG都包含一个TAG类型,TAG类型的大小为两个字节,位于一个TAG的最前面。TAG类型的第一个字节一定为0xFF + +一般情况下,是按照这个顺序排列的: + + +| TAG 类型 | 数值(十六进制标识符) | 全称 | 其他备注 | +|-------------------|----------|----------------------------|-----------------------------------| +| SOI | 0xFFD8 | Start of Image | 必带 | +| APP0 | 0xFFE0 | application0 | 必带 | +| APPn | 0xFFEn | applicationn | 可选带(APP1一般为Exif信息) | +| DQT | 0xFFDB | Define Quantization Table | 必带 | +| SOF | 0xFFC0 | Start of Frame | 必带 | +| DHT | 0xFFC4 | Define Huffman Table | 必带 | +| SOS | 0xFFDA | Start of Scan | 必带 | +| compress data | ... | ... | 必带 | +| EOI | 0xFFD9 | End of Image | 必带 | + +标志OxFFE0~OxFFEF被称为"Application Marker",它们不是解码JPEG文件必须得,可以被用来存储配置信息等。 +EXIF也是利用这个标志段来插入信息的。具体来说,是APP1(0xFFE1)Marker,所有的EXIF信息都存储在该数据段。 +​EXIF(Exchangeable Image File Format)​​ 是嵌入在 JPEG 文件中的 ​元数据标准,记录拍摄设备、参数和场景信息,相当于照片的“数字身份证”。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_1.png?raw=true) + + +#### 典型JPEG文件结构 + +[SOI] -> [APP0] -> [APP1(EXIF)] -> [DQT] -> [SOF] -> [DHT] -> [SOS] -> [压缩数据] -> [EOI] + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_2.png?raw=true) + +##### 怎么判断是不是JPEG图片? + +jpeg是一种压缩标准,大幅度缩小数据流,jpeg以FF D8开头,FF D9结束。 + + +其实很简单,就是判断前面3个字节是什么,如果发现是FF D8 FF开始,那就认为它是JEPG图片。(注意android不是根据后缀名来判断是什么文件的,当然你必须是图片的后缀名文件管理器才可以打开 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/jpeg_3.png?raw=true) + + + + +### Exif是什么 + + +Exif(Exchangeable Image File可交换图像文件)就是用来记录拍摄图像时的各种信息: +- 图像信息(厂商、分辨率等) +- 相机拍摄记录(ISO、白平衡、饱和度、锐度等) +- 缩略图(缩略图宽度、高度等) +- gps(拍摄时的经度、纬度、高度等) + +将这些信息按照JPEG文件标准放在图像文件头部。 + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" new file mode 100644 index 00000000..e85ed436 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" @@ -0,0 +1,247 @@ +# PNG + +[PNG](https://www.w3.org/TR/png/#4Concepts.Format): 便携式网络图形(Portable Network Graphics)是一种无损压缩的位图图形格式,被设计为取代古老的 GIF 格式,其核心目标是无损压缩、支持真彩色和完全透明。 + + +### 文件结构 + +PNG图像格式文件(或者称为数据流)由一个8字节的PNG文件署名(PNG file signature)域和按照特定结构组织的3个以上的数据块(chunk)组成。 + + + +1. 文件署名域(Magic Number)​ + + +8字节的PNG文件署名域用来识别该文件是不是PNG文件。该域的值是: + +十进制数137 80 78 71 13 10 26 10 +十六进制数 89 50 4e 47 0d 0a 1a 0a + + + +​固定 8 字节:89 50 4E 47 0D 0A 1A 0A: +- 89:高位设置,防止被误判为文本文件。 +- 50 4E 47:ASCII "PNG"。 +- 0D 0A:DOS 风格的换行符,用于换行符转换检测。 +- 1A:DOS 的 EOF(文件结束)字符。 +- 0A:Unix 风格的换行符。 + +​任何 PNG 解析器首先都必须检查这 8 个字节。​​ + + +PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是必需的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。 + +2. 数据块(Chunk)结构​ + +每个数据块都有统一的格式,如下所示: + +``` +// 数据块结构 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Length (4 bytes) | // 数据字段长度 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Chunk Type (4 bytes ASCII) | // 块类型,如 IHDR ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| Chunk Data (Length bytes) | // 数据内容 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +| CRC32 (4 bytes) | // 对整个块类型+数据的校验 ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +``` + + + + +​- Length​:Data字段的字节数(32 位无符号整数,​大端序)。 +​- Chunk Type​:4 个 ASCII 字符,定义了块的用途。​大小写敏感​: +​ - 大写首字母​:关键块(解析器必须识别) + - 小写首字母​:辅助块(可忽略) + 例如:IHDR是关键块,tEXt是辅助块。 +​- CRC​:循环冗余校验码,用于检测数据在传输过程中是否出错。 + + +3. 关键数据块(Critical Chunks)​​ + +解析必须按顺序处理这些块: + +| 块类型 | 必须 | 说明 | 结构(Data 字段内容) | + +| :--- | :--- | :--- | :--- | + +| ​IHDR​ | 是 | 文件头,包含图像基本信息 | Width(4B), Height(4B), BitDepth(1B), ColorType(1B), Compression(1B), Filter(1B), Interlace(1B) | + +| ​PLTE​ | 可选 | 调色板,定义索引颜色 | 一系列 RGB 三元组(R, G, B) | + +| ​IDAT​ | 是 | 图像数据,可存在多个 | 由 DEFLATE 压缩过的、经过过滤的像素数据流 | + +| ​IEND​ | 是 | 图像结束标记 | 无数据 | + + +- IHDR:文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。 +文件头数据块由13字节组成,包含图像宽度(4字节)、图像高度(4字节)、图像深度(1字节)、颜色类型(1字节)、压缩方法(1字节)、滤波器方法(1字节)、隔行扫描方法(1字节) + +- PLTE:调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成。 +- IDAT:图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。 +- IEND:图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的: +0000000049454E44AE426082,不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82。 + +除了表示数据块开始的IHDR必须放在最前面, 表示PNG文件结束的IEND数据块放在最后面之外,其他数据块的存放顺序没有限制。 + + +4. 辅助数据块(Ancillary Chunks)​​ + +常见的有: + +| 块类型 | 说明 | + +| :--- | :--- | + +| ​tEXt​| 文本信息数据块(textual data),可存储任何文本信息(关键字+值),如软件、作者、版权。​你之前操作的 aigc:{}就可以写在这里。​​ | + +| ​zTXt​ | 压缩的文本数据块(comporessed textual data)。 | + +| ​iTXt​ | 国际化的 UTF-8 文本,可包含语言标签。例如中文、日文要写入该区域 | + +| ​tRNS​ | 透明度信息数据块(transparency)。对于索引色,它是调色板索引的 Alpha 数组;对于灰度/真彩色,它指定单一颜色为透明色。 | + +| ​gAMA​ | 图像y数据块(image gamma)指定图像伽马值,用于跨平台显示校正。 | + +| ​pHYs​ | 物理像素尺寸数据块(physical pixel dimensions)指定像素的物理尺寸(像素比/DPI)。 | + +| ​tIME​ | 图像最后修改时间数据块tIME(image last-modification time) | + + +4.8.2 Chunk types +Chunk types are four-byte sequences chosen so that they correspond to readable labels when interpreted in the ISO 646.IRV:1991 [ISO646] character set. The first four are termed critical chunks, which shall be understood and correctly interpreted according to the provisions of this specification. These are: + +- IHDR: image header, which is the first chunk in a PNG datastream. +- PLTE: palette table associated with indexed PNG images. +- IDAT: image data chunks. +- IEND: image trailer, which is the last chunk in a PNG datastream. + +The remaining chunk types are termed ancillary chunk types, which encoders may generate and decoders may interpret. + +- Transparency information: tRNS (see 11.3.1 Transparency information). +- Color space information: cHRM, gAMA, iCCP, sBIT, sRGB, cICP, mDCV (see 11.3.2 Color space information). +- Textual information: iTXt, tEXt, zTXt (see 11.3.3 Textual information). +- Miscellaneous information: bKGD, hIST, pHYs, sPLT, eXIf (see 11.3.4 Miscellaneous information). +- Time information: tIME (see 11.3.5 Time stamp information). +- Animation information: acTL, fcTL, fdAT (see 11.3.6 Animation information). + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/png_1.png?raw=true) + + +#### Textual information + +PNG provides the tEXt, iTXt, and zTXt chunks for storing text strings associated with the image, such as an image description or copyright notice. Keywords are used to indicate what each text string represents. Any number of such text chunks may appear, and more than one with the same keyword is permitted. + +11.3.3.1 Keywords and text strings +The following keywords are predefined and should be used where appropriate. + +Table 21 Predefined keywords +Keyword value Description +Title Short (one line) title or caption for image +Author Name of image's creator +Description Description of image (possibly long) +Copyright Copyright notice +Creation Time Time of original image creation +Software Software used to create the image +Disclaimer Legal disclaimer +Warning Warning of nature of content +Source Device used to create the image +Comment Miscellaneous comment +XML:com.adobe.xmp Extensible Metadata Platform (XMP) information, formatted as required by the XMP specification [XMP]. The use of iTXt, with Compression Flag set to 0, and both Language Tag and Translated Keyword set to the null string, are recommended for XMP compliance. +Collection Name of a collection to which the image belongs. An image may belong to one or more collections, each named by a separate text chunk. +Other keywords MAY be defined by any application for private or general interest. + +Keywords SHOULD be . + +- reasonably self-explanatory, since the aim is to let other human users understand what the chunk contains; and +- chosen to minimize the chance that the same keyword is used for incompatible purposes by different applications. + +## 所有块 +The constraints on the positioning of the individual chunks are listed in Table 7 and illustrated diagrammatically for static images in Figure 11 and Figure 12, for animated images where the static image forms the first frame in Figure 13 and Figure 14, and for animated images where the static image is not part of the animation in Figure 15 and Figure 16. These lattice diagrams represent the constraints on positioning imposed by this specification. The lines in the diagrams define partial ordering relationships. Chunks higher up shall appear before chunks lower down. Chunks which are horizontally aligned and appear between two other chunk types (higher and lower than the horizontally aligned chunks) may appear in any order between the two higher and lower chunk types to which they are connected. The superscript associated with the chunk type is defined in Table 8. It indicates whether the chunk is mandatory, optional, or may appear more than once. A vertical bar between two chunk types indicates alternatives. + +Table 7 Chunk ordering rules +Critical chunks +(shall appear in this order, except PLTE is optional) +Chunk name Multiple allowed Ordering constraints +IHDR No Shall be first +PLTE No Before first IDAT +IDAT Yes Multiple IDAT chunks shall be consecutive +IEND No Shall be last +Ancillary chunks +(need not appear in this order) +Chunk name Multiple allowed Ordering constraints +acTL No Before IDAT +cHRM No Before PLTE and IDAT +cICP No Before PLTE and IDAT +gAMA No Before PLTE and IDAT +iCCP No Before PLTE and IDAT. If the iCCP chunk is present, the sRGB chunk should not be present. +mDCV No Before PLTE and IDAT. +cLLI No Before PLTE and IDAT. +sBIT No Before PLTE and IDAT +sRGB No Before PLTE and IDAT. If the sRGB chunk is present, the iCCP chunk should not be present. +bKGD No After PLTE; before IDAT +hIST No After PLTE; before IDAT +tRNS No After PLTE; before IDAT +eXIf No Before IDAT +fcTL Yes One may occur before IDAT; all others shall be after IDAT +pHYs No Before IDAT +sPLT Yes Before IDAT +fdAT Yes After IDAT +tIME No None +iTXt Yes None +tEXt Yes None +zTXt Yes None + + +我们发现上面也有eXif块: +#### eXif(Exchangeable Image File) + +The data segment of the eXIf chunk contains an Exif profile in the format specified in "4.7.2 Interoperability Structure of APP1 in Compressed Data" of [CIPA-DC-008] except that the JPEG APP1 marker, length, and the "Exif ID code" described in 4.7.2(C), i.e., "Exif", NULL, and padding byte, are not included. + +The eXIf chunk size is constrained only by the maximum of 231-1 bytes imposed by the PNG specification. Only one eXIf chunk is allowed in a PNG datastream. + +The eXIf chunk contains metadata concerning the original image data. If the image has been edited subsequent to creation of the Exif profile, this data might no longer apply to the PNG image data. It is recommended that unless a decoder has independent knowledge of the validity of the Exif data, the data should be considered to be of historical value only. It is beyond the scope of this specification to resolve potential conflicts between data in the eXIf chunk and in other PNG chunks. + + + +PNG 格式中的 ​eXIf 块​ 和 ​zTXt 块​ 虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 + + + +好的,这是一个非常核心的问题。PNG 格式中的 ​eXIf 块​ 和 ​zTXt 块​ 虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 + +为了更直观地理解它们的区别,我们可以通过以下流程图来快速判断哪种块更适合你的需求: +​eXIf 块 (Chunk Type: 65 58 49 66 -> ASCII "eXIf")​​ + +​1. 作用​ +• +​唯一目的​:用于在 PNG 图像中嵌入 ​Exif(Exchangeable Image File Format)数据。 +• +​内容​:Exif 数据是一套高度标准化的元数据规范,主要用于记录数码照片的拍摄参数和设备信息。例如: +• +​相机参数​:光圈、快门速度、ISO、焦距、镜头型号 +• +​设备信息​:相机制造商、型号 +• +​时间信息​:照片原始拍摄时间(DateTimeOriginal) +• +​位置信息​:GPS 坐标(经度、纬度、高度) +• +​版权信息​:作者、版权说明 +• +​缩略图​:嵌入式的小尺寸预览图 + + + +你应该选择哪一个?​​ +• +​要保存相机的拍摄信息(光圈、快门、GPS等)?​​ +• +-> 使用 ​eXIf 块。(例如:用 exiftool -tagsFromFile source.jpg -all:all拷贝) +• +​要写入自己定义的文字、配置或标识(如 aigc:{"model":"GPT-4"})?​​ +• +-> 使用 ​zTXt 块。(例如:用 exiftool -zTXt:YourKey="Your Value"写入) +​简单总结:eXIf 用于“相机说什么”,zTXt 用于“你想说什么”。​ From bc41ff07ee9ff28573c403d226c5ac6e5f98cbdd Mon Sep 17 00:00:00 2001 From: CharonChui Date: Mon, 22 Sep 2025 17:26:23 +0800 Subject: [PATCH 127/128] add pics --- .../GIF.md" | 105 ++++++++++++++++ .../HEIC.md" | 42 +++++++ .../JPEG.md" | 4 +- .../PNG.md" | 72 +++++------ .../WebP.md" | 87 +++++++++++++ ...07\345\205\203\346\225\260\346\215\256.md" | 118 ++++++++++++++++++ 6 files changed, 389 insertions(+), 39 deletions(-) create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/GIF.md" create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/HEIC.md" create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/WebP.md" create mode 100644 "VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/\345\233\276\347\211\207\345\205\203\346\225\260\346\215\256.md" diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/GIF.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/GIF.md" new file mode 100644 index 00000000..2631cf56 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/GIF.md" @@ -0,0 +1,105 @@ +# GIF + +GIF(Graphics Interchange Format,图像文件存储格式)是CompuServe公司开发的。 + +它的核心特点是支持动画和透明度,但牺牲了色彩深度。 + +1987年开发的GIF文件格式版本号是GIF87a,1989年进行了扩充,扩充后的版本号定义为GIF89a。 + + +GIF图像文件以数据块(block)为单位来存储图像的相关信息。 + + +一个GIF文件由表示图形/图像的数据块、数据子块以及显示图形/图像的控制信息块组成,称为GIF数据流(Data Stream)。 + + +数据流中的所有控制信息块和数据块都必须在文件头(Header)和文件结束块(Trailer)之间 。 + +GIF文件格式采用了LZW(Lempel-Ziv Walch)压缩算法来存储图像数据,定义了允许用户为图像设置背景的透明(transparency)属性。 + +此外,GIF文件格式可在一个文件中存放多幅彩色图形/图像。如果在GIF文件中存放有多幅图,它们可以像演幻灯片那样显示或者像动画那样演示。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gif_1.png?raw=true) + + +## GIF文件结构 + +GIF格式的文件结构整体上分为三部分:文件头、GIF数据流、文件结尾。其中,GIF数据流分为全局配置和图像块。 + + +GIF 文件由一系列数据块(Blocks)和扩展块(Extension Blocks)顺序组成。理解这个结构是解析元数据的关键。 +​- 文件头 (Header)​​:GIF87a或 GIF89a,标识文件版本: GIF署名(Signature)和版本号(Version):GIF的前6个字节内容是GIF的署名和版本号。我们可以通过前3个字节判断文件是否为GIF格式,后3个字节判断GIF格式的版本。 +​- 逻辑屏幕描述符 (Logical Screen Descriptor)​​:定义画布大小和全局调色板信息。 +​- 全局调色板 (Global Color Table)​​:可选,定义所有帧共用的颜色表。 +​- 数据块​: +​ - 图像描述符 (Image Descriptor)​​:定义帧的图像区域和局部调色板信息。 +​ - 基于调色板的图像数据​:使用 LZW 压缩后的像素数据。 +​- 扩展块 (Extension Blocks)​​: +​ - 图形控制扩展 (Graphic Control Extension)​​:​最重要的扩展,包含帧的延迟时间(动画速度)、透明色索引和处置方法。 +​ - 注释扩展 (Comment Extension)​​:​这是存储纯文本元数据的主要位置!​​ +​ - 应用扩展 (Application Extension)​​:用于存储应用程序的特定信息(如 Netscape 的循环次数)。 + - 明文扩展、等。 +​- 文件终结器 (Trailer)​​:一个分号 ;,标识文件结束。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/gif_2.png?raw=true) + + + +数据块可分成3类:控制块(Control Block),图形描绘块(Graphic-RenderingBlock)和专用块(Special Purpose Block)。 + + +- 控制块: 控制块包含有用来控制数据流(Data Stream)或者设置硬件参数的信息,其成员包括: + - GIF文件头(Header) + - 逻辑屏幕描述块(LogicalScreen Descriptor) + - 图形控制扩展块(GraphicControl Extension) + - 文件结束块(Trailer) + +- 图形描绘块:包含有用来描绘在显示设备上显示图形的信息和数据,其成员包括: + - 图像描述块(ImageDescriptor) + - 无格式文本扩展块(PlainText Extension) + - 全局调色板、局部调色板、图像压缩数据和图像说明扩充块。 + + - 特殊用途数据块;包含有与图像处理无关的信息,其成员包括: + - 注释扩展块(CommentExtension) + - 应用扩展块(ApplicationExtension) + - 除了在控制块中的逻辑屏幕描述块(Logical Screen Descriptor)和全局彩色表(GlobalColor Table)的作用范围是整个数据流(Data Stream)之外, 所有其他控制块仅跟在它们后面的图形描绘块。 + + + +第二部分:元数据与 XMP 在 GIF 中的存储位置​ +这是一个非常关键的点,也是 GIF 与 JPEG、PNG 等格式最大的不同。 +​1. 元数据存储在哪里?​​ +GIF 格式没有为 EXIF、IPTC 或 XMP 这类现代、结构化的元数据设计标准的存储位置。它的元数据能力非常有限。 +​主要位置:注释扩展块 (Comment Extension)​​ +​块标识符​:0x21 0xFE +​内容​:可以包含任意纯文本信息。这是存储版权信息、作者、描述等简单元数据的传统位置。 +​限制​:​只能是纯文本,没有键值对或结构化的标准。不同软件写入的格式完全不同。 +​次要位置:应用扩展块 (Application Extension)​​ +​块标识符​:0x21 0xFF +​内容​:通常被特定应用程序用于存储私有数据。例如,NETSCAPE2.0应用扩展用于指定动画循环次数。 +​理论上,某个软件可以自定义一个应用扩展来存储 XMP 数据,但这绝非标准,其他软件很可能无法识别。 + + +​2. XMP 存储在哪里?​​ +​标准的、符合 Adobe XMP 规范的元数据通常不存在于 GIF 文件中。​​ +​原因​:GIF 格式诞生时,XMP 规范还不存在。GIF 的结构没有为容纳一大段 XML 数据而设计。 +​变通方案​:极少数专业软件(如 Adobe 的部分产品)​可能会将 XMP 数据作为一串很长的 XML 文本字符串写入到注释扩展 (Comment Extension)​​ 中。 +​现实情况​:​99.9% 的 GIF 文件不包含 XMP 数据。​​ 如果您的目标是读取 XMP,遇到 GIF 格式的概率极低。 + + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/HEIC.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/HEIC.md" new file mode 100644 index 00000000..f2630848 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/HEIC.md" @@ -0,0 +1,42 @@ +# HEIC + + +HEIC(High Efficiency Image Container​ 高效图像容器)是苹果公司基于HEIF(高效图像文件格式)和HEVC(高效视频编码)技术开发的一种图像存储格式。自2017年iOS 11系统更新起,该格式被设定为iPhone 7及后续机型的默认照片存储格式,在相同画质下可将文件体积较JPEG减少约50%。 + + +High Efficiency Image File Format (HEIF, 发音为:heef),由 Moving Picture Experts Group ( MPEG,即动态图像专家组) 于2013年开发,它基于ISOBMFF标准。HEIF是一个容器的图片格式,它可以包含图片和图片序列(一个文件可以包含不止一个图片)。当前的编码格式有:HEVC 和H.264/MPEG-4 AVC 两种,并且未来可能有新的编码格式加入。 + + + +### HEIF和HEIC的关系 + +- HEIF是规则、HEIC是实例。 +- HEIF定义了如何存储图像、图像序列(动画、连拍)、音频和元数据。 +- 而HEIC文件是遵循HEIF规则,并使用HEVC(H.265)编码来压缩图像数据的具体文件。 +- .HEIC只是一个HEIF文件格式的一种扩展名,言外之意是:HEIF不仅有.HEIC这种扩展名,还有其它的,比如说: .HEIF和.avci,它们都是属于HEIF文件格式。当然,常见的只有.heif和.heic这两种,而.avci 很少见。 + + + +HEIF 的强大源于其现代的设计理念,它本质上是一个基于 ISO 基础媒体文件格式(ISOBMFF)的容器,该格式也是 MP4 视频的基础。 + + +​1. 核心结构:'box' (盒子) 体系​ + +HEIF/HEIC 文件由一系列嵌套的 ​​'box'​​(或称 'atom') 组成。每个 'box' 都有一个头部(声明类型和大小)和负载(数据)。 +​- ftyp​:文件类型盒,标识这是一个 HEIC 文件。 +​- meta​:元数据盒,是最重要的盒子,包含了描述图像数据的各种信息。 +​- mdat​:媒体数据盒,存储实际的、经过压缩的图像比特流。 + + + + + + + + + + + + + + diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" index bf05b544..ba93a123 100644 --- "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/JPEG.md" @@ -5,7 +5,9 @@ JPEG(Joint Photographic Experts Group),全称为联合图像专家小组, -JPEG只描述一副图像如何转换成一组数据流,而不论这些字节存储在何种介质上。由独立JPEG组创立的另一个进阶标准,JFIF(JPEGFileInterchangeFormat,JPEG文件交换格式)则描述JPEG数据流如何生成适于电脑存储或传送的图像。在一般应用中,我们从数码相机等来源获得的“JPEG文件”,指的就是JFIF文件,有时是ExifJPEG文件。 +JPEG只描述一副图像如何转换成一组数据流,而不论这些字节存储在何种介质上。 + +由独立JPEG组创立的另一个进阶标准,JFIF(JPEG File Interchange Format,JPEG文件交换格式)则描述JPEG数据流如何生成适于电脑存储或传送的图像。在一般应用中,我们从数码相机等来源获得的“JPEG文件”,指的就是JFIF文件,有时是ExifJPEG文件。 该格式采用有损压缩算法,通过牺牲部分画质换取较小文件体积,压缩过程包含色彩空间转换(RGB转YCbCr)、离散余弦变换(DCT)、量化和熵编码等步骤。 diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" index e85ed436..3732f554 100644 --- "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/PNG.md" @@ -12,24 +12,27 @@ PNG图像格式文件(或者称为数据流)由一个8字节的PNG文件署 1. 文件署名域(Magic Number)​ -8字节的PNG文件署名域用来识别该文件是不是PNG文件。该域的值是: +8字节的PNG文件署名域用来识别该文件是不是PNG文件。该域的值是: -十进制数137 80 78 71 13 10 26 10 -十六进制数 89 50 4e 47 0d 0a 1a 0a +- 十进制数137 80 78 71 13 10 26 10 +- 十六进制数 89 50 4e 47 0d 0a 1a 0a -​固定 8 字节:89 50 4E 47 0D 0A 1A 0A: +​固定8字节:89 50 4E 47 0D 0A 1A 0A: - 89:高位设置,防止被误判为文本文件。 - 50 4E 47:ASCII "PNG"。 - 0D 0A:DOS 风格的换行符,用于换行符转换检测。 - 1A:DOS 的 EOF(文件结束)字符。 - 0A:Unix 风格的换行符。 -​任何 PNG 解析器首先都必须检查这 8 个字节。​​ +​任何PNG解析器首先都必须检查这8个字节。​​ -PNG定义了两种类型的数据块,一种是称为关键数据块(critical chunk),这是必需的数据块,另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。 +PNG定义了两种类型的数据块: + +- 一种是称为关键数据块(critical chunk),这是必需的数据块 +- 另一种叫做辅助数据块(ancillary chunks),这是可选的数据块。 2. 数据块(Chunk)结构​ @@ -76,12 +79,14 @@ PNG定义了两种类型的数据块,一种是称为关键数据块(critical | ​IEND​ | 是 | 图像结束标记 | 无数据 | -- IHDR:文件头数据块IHDR(header chunk):它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。 +- IHDR:文件头数据块IHDR(header chunk): + +它包含有PNG文件中存储的图像数据的基本信息,并要作为第一个数据块出现在PNG数据流中,而且一个PNG数据流中只能有一个文件头数据块。 文件头数据块由13字节组成,包含图像宽度(4字节)、图像高度(4字节)、图像深度(1字节)、颜色类型(1字节)、压缩方法(1字节)、滤波器方法(1字节)、隔行扫描方法(1字节) -- PLTE:调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成。 -- IDAT:图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。 -- IEND:图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的: +- PLTE: 调色板数据块PLTE(palette chunk)包含有与索引彩色图像(indexed-color image)相关的彩色变换数据,它仅与索引彩色图像有关,而且要放在图像数据块(image data chunk)之前。PLTE数据块是定义图像的调色板信息,PLTE可以包含1~256个调色板信息,每一个调色板信息由3个字节组成。 +- IDAT: 图像数据块IDAT(image data chunk):它存储实际的数据,在数据流中可包含多个连续顺序的图像数据块。IDAT存放着图像真正的数据信息,因此,如果能够了解IDAT的结构,我们就可以很方便的生成PNG图像。 +- IEND: 图像结束数据IEND(image trailer chunk):它用来标记PNG文件或者数据流已经结束,并且必须要放在文件的尾部。如果我们仔细观察PNG文件,我们会发现,文件的结尾12个字符看起来总应该是这样的: 0000000049454E44AE426082,不难明白,由于数据块结构的定义,IEND数据块的长度总是0(00 00 00 00,除非人为加入信息),数据标识总是IEND(49 45 4E 44),因此,CRC码也总是AE 42 60 82。 除了表示数据块开始的IHDR必须放在最前面, 表示PNG文件结束的IEND数据块放在最后面之外,其他数据块的存放顺序没有限制。 @@ -89,7 +94,7 @@ PNG定义了两种类型的数据块,一种是称为关键数据块(critical 4. 辅助数据块(Ancillary Chunks)​​ -常见的有: +常见的有: | 块类型 | 说明 | @@ -159,6 +164,7 @@ Keywords SHOULD be . - chosen to minimize the chance that the same keyword is used for incompatible purposes by different applications. ## 所有块 + The constraints on the positioning of the individual chunks are listed in Table 7 and illustrated diagrammatically for static images in Figure 11 and Figure 12, for animated images where the static image forms the first frame in Figure 13 and Figure 14, and for animated images where the static image is not part of the animation in Figure 15 and Figure 16. These lattice diagrams represent the constraints on positioning imposed by this specification. The lines in the diagrams define partial ordering relationships. Chunks higher up shall appear before chunks lower down. Chunks which are horizontally aligned and appear between two other chunk types (higher and lower than the horizontally aligned chunks) may appear in any order between the two higher and lower chunk types to which they are connected. The superscript associated with the chunk type is defined in Table 8. It indicates whether the chunk is mandatory, optional, or may appear more than once. A vertical bar between two chunk types indicates alternatives. Table 7 Chunk ordering rules @@ -206,42 +212,32 @@ The eXIf chunk contains metadata concerning the original image data. If the imag -PNG 格式中的 ​eXIf 块​ 和 ​zTXt 块​ 虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 +PNG格式中的eXIf块​和​zTXt块​虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 - -好的,这是一个非常核心的问题。PNG 格式中的 ​eXIf 块​ 和 ​zTXt 块​ 虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 +PNG 格式中的eXIf块​和zTXt块​ 虽然都是用于存储元数据,但其设计目的、内部结构和适用场景有本质区别。 为了更直观地理解它们的区别,我们可以通过以下流程图来快速判断哪种块更适合你的需求: ​eXIf 块 (Chunk Type: 65 58 49 66 -> ASCII "eXIf")​​ ​1. 作用​ -• -​唯一目的​:用于在 PNG 图像中嵌入 ​Exif(Exchangeable Image File Format)数据。 -• -​内容​:Exif 数据是一套高度标准化的元数据规范,主要用于记录数码照片的拍摄参数和设备信息。例如: -• -​相机参数​:光圈、快门速度、ISO、焦距、镜头型号 -• -​设备信息​:相机制造商、型号 -• -​时间信息​:照片原始拍摄时间(DateTimeOriginal) -• -​位置信息​:GPS 坐标(经度、纬度、高度) -• -​版权信息​:作者、版权说明 -• -​缩略图​:嵌入式的小尺寸预览图 - - - -你应该选择哪一个?​​ -• +唯一目的​:用于在 PNG 图像中嵌入 ​Exif(Exchangeable Image File Format)数据。 +​内容​:Exif 数据是一套高度标准化的元数据规范,主要用于记录数码照片的拍摄参数和设备信息。例如: +​- 相机参数​:光圈、快门速度、ISO、焦距、镜头型号 +​- 设备信息​:相机制造商、型号 +​- 时间信息​:照片原始拍摄时间(DateTimeOriginal) +​- 位置信息​:GPS 坐标(经度、纬度、高度) +​- 版权信息​:作者、版权说明 +​- 缩略图​:嵌入式的小尺寸预览图 + + + +### 应该选择哪一个?​​ + ​要保存相机的拍摄信息(光圈、快门、GPS等)?​​ -• -> 使用 ​eXIf 块。(例如:用 exiftool -tagsFromFile source.jpg -all:all拷贝) -• + ​要写入自己定义的文字、配置或标识(如 aigc:{"model":"GPT-4"})?​​ -• -> 使用 ​zTXt 块。(例如:用 exiftool -zTXt:YourKey="Your Value"写入) + ​简单总结:eXIf 用于“相机说什么”,zTXt 用于“你想说什么”。​ diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/WebP.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/WebP.md" new file mode 100644 index 00000000..fa55a758 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/WebP.md" @@ -0,0 +1,87 @@ +# WebP + +WebP是一种图片格式,它使用VP8关键帧编码,以有损方式压缩图像数据或WebP无损编码。 + +除了 WebP 的基本用例(即包含编码为 VP8 关键帧的单个图片的文件)之外,WebP 容器(即 WebP 的 RIFF 容器)还支持其他功能。WebP 容器提供 对以下各项的支持: + +- 无损压缩:可以使用 WebP 无损格式对图片进行无损压缩。 + +- 元数据:图片中可能有存储于可交换图片文件中的元数据 格式 (Exif) 或可扩展元数据平台 (XMP) 格式。 + +- 透明度:图片可以具有透明度,即 alpha 通道。 + +- 颜色配置文件:图片可能具有如国际色彩联盟所述的嵌入式 ICC 配置文件。 + +- 动画:同一张图片可能包含多个帧,帧之间会暂停播放; 使其成为动画 + + +在图片(和多媒体)文件格式中,​RIFF​ 是一个容器格式,或者说是一种文件结构规范,而不是一种图片编码格式本身。 + +您可以把它理解为一个盒子或文件夹,里面可以装各种不同的东西(图片数据、音频数据、元数据等)。 + +### RIFF + +RIFF​ 的全称是 ​Resource Interchange File Format​(资源交换文件格式),由微软公司在1991年提出。 + +它的核心思想是:​所有数据都存储在称为“块(Chunks)”的结构中。 + +#### RIFF 文件的结构 + +一个典型的 RIFF 文件由以下几个部分组成: + +​- RIFF 头(Header)​​: + - RIFF(4字节):固定标识,表明这是一个RIFF格式的文件。 + - 文件大小(4字节):从下一个字节开始到文件结尾的总大小。 + - 格式类型(4字节):用四个字符代码(FOURCC)定义文件的具体类型,例如 AVI或 WAVE。 +​- 块(Chunks)​​: + +在文件头之后,就是一系列的数据块。 +每个块都有自己的头和信息: +- 块ID(4字节):用四个字符代码标识这个块是什么(例如 fmt是格式块,data是数据块)。 +- 块大小(4字节):该块数据部分的大小。 +- 块数据(可变长度):实际的数据内容。 + +​- 列表块(LIST Chunks)​​: + +这是一种特殊的块,它本身内部又可以包含其他多个子块,用于更复杂的数据组织。 + +​用个比喻来说: + +一个 RIFF 文件就像一个行李箱(RIFF Container)​,上面贴着一个标签写着“摄影器材”(格式类型= PHOT)。箱子里面有几个收纳盒(Chunks)​,每个收纳盒上又贴着“镜头”、“机身”、“电池”等标签(块ID)。这样就可以井井有条地存放所有东西。 + + +#### 与图片格式的关系 + +RIFF 作为一种容器,被用于多种图片格式。最著名的例子就是WebP: + +一个典型的 WebP 文件结构如下: +``` +RIFF Header (标识这是一个RIFF文件,格式类型为 'WEBP') +| ++-- VP8 (或 VP8L, VP8X) Chunk (存储实际的图片编码数据) +| ++-- ANIM Chunk (可选,如果存在则表示是动画WebP) +| ++-- ANMF Chunk (可选,动画帧数据) +| ++-- EXIF Chunk (可选,存储EXIF元数据) +| ++-- XMP Chunk (可选,存储XMP元数据) +| ++-- ... (其他可选块) +``` + + +WebP的强大之处在于它利用了RIFF容器的可扩展性,可以轻松的在一个文件中包含静态图、动画、Alpha透明度、元数据等多种信息。 + + + +简单来说: + +- RIFF: 是一种文件结构或容器规范,可以比喻成行李箱 +- JPEG、PNG: 是一种图片编码格式,可以比喻成一种折叠衣服的方法。注意PNG拥有自己独特的、与RIFF无关的容器结构。 +- WebP: 使用RIFF容器来包装VP8编码数据的图片格式,可以比喻成用行李箱装了特定方法折叠好的衣服 + + + + diff --git "a/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/\345\233\276\347\211\207\345\205\203\346\225\260\346\215\256.md" "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/\345\233\276\347\211\207\345\205\203\346\225\260\346\215\256.md" new file mode 100644 index 00000000..1748eb73 --- /dev/null +++ "b/VideoDevelopment/\345\233\276\347\211\207\346\240\274\345\274\217/\345\233\276\347\211\207\345\205\203\346\225\260\346\215\256.md" @@ -0,0 +1,118 @@ +# 图片元数据 + +简单来说,元数据就是有关数据的数据。 + + +实际上,元数据是关于文件的一组标准化信息,如作者姓名、分辨率、色彩空间、版权以及应用于文件的关键字。例如,多数摄像机会在视频文件中附加一些基本信息,如日期、持续时间和文件类型。 + + + + +## 元数据分类 + +对于数码图像,目前常见的元数据有EXIF、IPTC、XMP三种: + +- EXIF: 通常被数码相机在拍摄照片时自动添加,比如相机型号、镜头、曝光、图片尺寸等信息 +- IPTC: 比如图片标题、关键字、说明、作者、版权等信息。主要是由人工在后期通过软件写入的数据。 +- XMP: XMP实际上是一种元数据存储和管理的标准,可以将Exif,IPTC或其他的数据都按XMP统一的格式存放在图像文件中。 +- ICC: 色彩管理、色彩空间 + +### 1.EXIF/标准元数据 + +EXIF(Exchangeable Image File Format,可交换图像文件格式) + +这是最常见的元数据类型,主要记录拍摄参数、设备信息、时间、位置等数据。以下是常见 EXIF 信息分类: + +- 拍摄参数类: + - 曝光时间 + - 焦距 + - ISO感光度等 + +- 时间与位置 + - 拍摄时间 + - 文件创建时间 + - GPS经纬度 + - 海拔 + +- 设备信息 + - Make: 相机/手机制造商(例如Apple) + - Model: 设备型号(如iPhone14) + - LensModel: 镜头型号 + +- 图片属性 + - 图片宽高 + - Orientation: 拍摄方向 + - 压缩方式 + - 缩略图数据(如有) + +### 2. ICC/色彩管理的核心 +ICC(International Color Consortium,国际色彩联盟)元数据是嵌入在图像文件中的色彩管理数据,用于确保不同设备(相机、显示器、打印机)呈现的色彩一致性。 + +### 3. XMP/图像的可扩展元数据 + +[XMP](https://helpx.adobe.com/cn/after-effects/using/xmp-metadata.html)Extensible Metadata Platform,可扩展元数据平台)是 Adobe 开发的一种标准化元数据格式,用于嵌入到图片、视频、PDF 等文件中。它比 EXIF 更灵活,支持自定义字段和结构化数据,具有高度可扩展性。 + +核心特点​: + +- 基于RDF/XML格式 + - XML: 语法规则,提供了语法(如何写) + - RDF: 数据模型,提供了语义(什么意思),确保不同系统、不同厂商、不同标准的元数据都能够共存且被正确理解 + - 总结: RDF在XML提供的语法基础上,增加了语义层,使得数据不仅仅是结构化的,更是有意义且可互联的。 +- 支持自定义命名空间和属性 +- 可以嵌入到多种文件格式中 +- 支持复杂数据结构(数组、嵌套对象) + + +例如: 在元数据中增加一个信息,如果没有 RDF,只是自定义 XML:: +``` + + {"Label":"value1"} + +``` +问题​:其他系统看不懂 TC260:AIGC是什么意思,无法理解和重用这个数据。 + +使用 RDF (XMP 的实际做法): + +``` + + + {"Label":"value1"} + + +``` +优势​: +​ +- 明确语义​:通过 xmlns:tc260="http://www.tc260.org.cn/ns/AIGC/1.0/"明确指出了 tc260:AIGC的含义是由该命名空间定义的。任何人都可以访问这个 URI 来了解 AIGC属性的正式定义。 +​- 可扩展性​:可以轻松混合来自不同来源的元数据。例如,可以同时包含 Dublin Core 的标题和您自定义的 TC260 标签,而不会产生冲突。 +​- 机器可读​:计算机程序可以自动解析和理解这些关系,实现数据的互联和智能处理。 + + + +### 4. IPTC + +IPTC(International Press Telecommunications Council), +最初为新闻行业设计,主要用于描述图片内容和版权信息。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/metadata_1.png?raw=true) + + +EXIF里面保存的是二进制数据,而且基本都是固定的字段,主要应用于相机参数、时间等。 + +XMP中则是XML文本而且支持扩展性,可用于编辑元数据、版权等。 + + + + + + + + + + + + + + From 01f25a054328f167105ca5f725efbb5ea7da1e39 Mon Sep 17 00:00:00 2001 From: CharonChui Date: Wed, 24 Dec 2025 16:08:48 +0800 Subject: [PATCH 128/128] update --- .../Componse\344\275\277\347\224\250.md" | 130 +++++ .../KMP\345\274\200\345\217\221.md" | 4 - .../python3\345\205\245\351\227\250.md" | 458 +++++++++++++++++- JavaKnowledge/shell.md | 10 + "Jetpack/ui/\345\257\271\346\257\224.md" | 68 +++ MobileAIModel/AIGC.md | 49 ++ MobileAIModel/RAG.md | 75 +++ MobileAIModel/TensorFlow Lite | 5 + .../\345\244\247\346\250\241\345\236\213.md" | 394 +++++++++++++++ .../\351\242\204\350\256\255\347\273\203.md" | 11 + 10 files changed, 1198 insertions(+), 6 deletions(-) create mode 100644 "AdavancedPart/Componse\344\275\277\347\224\250.md" create mode 100644 "Jetpack/ui/\345\257\271\346\257\224.md" create mode 100644 MobileAIModel/AIGC.md create mode 100644 MobileAIModel/RAG.md create mode 100644 "MobileAIModel/\345\244\247\346\250\241\345\236\213.md" create mode 100644 "MobileAIModel/\351\242\204\350\256\255\347\273\203.md" diff --git "a/AdavancedPart/Componse\344\275\277\347\224\250.md" "b/AdavancedPart/Componse\344\275\277\347\224\250.md" new file mode 100644 index 00000000..6a2f0449 --- /dev/null +++ "b/AdavancedPart/Componse\344\275\277\347\224\250.md" @@ -0,0 +1,130 @@ +Compose使用 +=== + +传统 Android UI 开发采用命令式模型,即通过命令驱动视图变化:findViewById 查找控件、设置属性、处理交互逻辑。代码经常伴随着多个职责耦合在一起,结构混乱,易错难测。 + +Compose 则采用声明式模型:界面即状态的函数表达。当状态改变时,对应的 Composable 自动重新组合(Recompose)并刷新界面。这种模式更贴近现代前端(如 React/Vue)的理念。 + +无需关心视图更新逻辑,只要状态变化,界面自然重绘,大幅降低 UI 层复杂度。 + + +1.3 开发效率提升点 + +代码量平均减少 40%-60% + +无需 ViewHolder、Adapter 逻辑 + +状态与 UI 同步更新,避免 UI 状态丢失 + +支持实时预览(@Preview)、热重载、即时调试 + + +我们在公司项目中构建了性能对比 Benchmark(测试设备为 Pixel 6、Android 13): + +2.1 滚动列表对比:RecyclerView vs LazyColumn + +指标 RecyclerView LazyColumn (Compose) 差异 +平均帧率 +48 fps +58 fps ++20% +内存占用 +28 MB +22 MB +-21% +首次绘制耗时 +320 ms +210 ms +-34% + + +2.2 原因解析:Compose 更快的秘密 + +SlotTable:结构快照树 +Compose 编译器会将 Composable 函数转换为组装 SlotTable 的代码。SlotTable 是一种高效的数据结构,存储了 Composable 树的结构快照。当状态发生变化时,Compose 通过对比 SlotTable 的版本,精确地定位变化范围,从而进行最小代价的重组操作(recomposition)。这一过程通过 Composer 对 Slot 表的操作实现,避免了冗余 UI 节点更新。 + +重组与 Group 管理机制 +Compose 使用 Group(startGroup/endGroup)对 Composable 调用进行打包与标识,每个重组区域会通过重新执行对应的 Group 来进行更新,确保仅变更部分被执行。此机制在 RecomposeScopeImpl 中有体现,它能追踪每个状态依赖的作用域,从而提升重组精度。 + +无需 ViewHolder 回收 +传统 RecyclerView 需要手动管理视图缓存与回收,而 Compose 自动处理 Composition 节点生命周期。Compose Compiler 会生成高效的 Slot 操作指令,通过“skip、reuse”策略对 UI 层进行精准控制,避免重复创建与销毁。 + +Skia 图形引擎与 RenderNode +Compose 绘制层基于 Skia 引擎,使用 DrawModifier 直接对 Canvas 进行渲染。它不会像传统 View 那样层层嵌套测量布局与绘制流程,而是采用测量(MeasurePass)-> 布局(LayoutPass)-> 绘制(DrawPass)的管线逻辑,通过 LayoutNode 驱动 Compose UI 树的变化。同时 Compose Layout 使用 SubcomposeLayout 实现异步测量能力,提高复杂嵌套组件的性能表现。 + +渲染流程对比 +阶段 View System Compose +布局树管理 +View/ViewGroup 层级 +LayoutNode 节点 +渲染方式 +Choreographer + RenderThread +FrameClock + Skia 渲染 +状态追踪 +手动触发 invalidate +Snapshot 自动追踪 + Diff Patch +更新路径 +requestLayout → measure/layout +Recomposer + SlotTable 重组 + +注意:Compose 并非所有场景都一定更快,特别是复杂嵌套、过度组合场景仍需谨慎使用。 + +### 简介 + +Jetpack Compose 是用于构建 Android 界面的新款工具包。 + + +Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发,打造生动而精彩的应用。它可让您更快速、更轻松地构建 Android 界面。 + + +编写更少的代码会影响到所有开发阶段:作为代码撰写者,需要测试和调试的代码会更少,出现 bug 的可能性也更小,您就可以专注于解决手头的问题;作为审核人员或维护人员,您需要阅读、理解、审核和维护的代码就更少。 + +与使用 Android View 系统(按钮、列表或动画)相比,Compose 可让您使用更少的代码实现更多的功能。无论您需要构建什么内容,现在需要编写的代码都更少了。以下是我们的一些合作伙伴的感想: + +“对于相同的 Button 类,代码的体量要小 10 倍。”(Twitter) +“使用 RecyclerView 构建的任何屏幕(我们的大部分屏幕都使用它构建)的大小也显著减小。”(Monzo) +““只需要很少几行代码就可以在应用中创建列表或动画,这一点令我们非常满意。对于每项功能,我们编写的代码行更少了,这让我们能够将更多精力放在为客户提供价值上。”(Cuvva) +编写代码只需要采用 Kotlin,而不必拆分成 Kotlin 和 XML 部分:“当所有代码都使用同一种语言编写并且通常位于同一文件中(而不是在 Kotlin 和 XML 语言之间来回切换)时,跟踪变得更容易”(Monzo) + +无论您要构建什么,使用 Compose 编写的代码都很简洁且易于维护。“Compose 的布局系统在概念上更简单,因此可以更轻松地推断。查看复杂组件的代码也更轻松。”(Square) + + + +Compose 使用声明性 API,这意味着您只需描述界面,Compose 会负责完成其余工作。这类 API 十分直观 - 易于探索和使用:“我们的主题层更加直观,也更加清晰。我们能够在单个 Kotlin 文件中完成之前需要在多个 XML 文件中完成的任务,这些 XML 文件负责通过多个分层主题叠加层定义和分配属性。”(Twitter) + + +Compose 与您所有的现有代码兼容:您可以从 View 调用 Compose 代码,也可以从 Compose 调用 View。大多数常用库(如 Navigation、ViewModel 和 Kotlin 协程)都适用于 Compose,因此您可以随时随地开始采用。“我们集成 Compose 的初衷是实现互操作性,我们发现这件事情已经‘水到渠成’。我们不必考虑浅色模式和深色模式等问题,整个体验无比顺畅。”(Cuvva) + + +为现有应用设置 Compose + +首先,使用 Compose Compiler Gradle 插件配置 Compose 编译器。 + +然后,将以下定义添加到应用的 build.gradle 文件中: +``` +android { + buildFeatures { + compose = true + } +} +``` +在 Android BuildFeatures 代码块内将 compose 标志设置为 true 会在 Android Studio 中启用 Compose 功能。 + + +Compose vs HarmonyOS ArkUI 对比分析 + + +Jetpack Compose 和 HarmonyOS ArkUI 均采用声明式 UI 编程范式,面向多设备场景的响应式 UI 构建,二者在理念相通的同时,在架构设计、状态模型、渲染机制等方面有显著区别。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/compose_vs_arkui.png?raw=true) + + +Jetpack Compose 是围绕可组合函数构建的。这些函数可让您以程序化方式定义应用的界面,只需描述应用界面的外观并提供数据依赖项,而不必关注界面的构建过程(初始化元素、将其附加到父项等)。如需创建可组合函数,只需将 @Composable 注解添加到函数名称中即可。 + + + +--- + +- 邮箱 :charon.chui@gmail.com +- Good Luck! \ No newline at end of file diff --git "a/AdavancedPart/KMP\345\274\200\345\217\221.md" "b/AdavancedPart/KMP\345\274\200\345\217\221.md" index b4d7e54c..d4faa6f6 100644 --- "a/AdavancedPart/KMP\345\274\200\345\217\221.md" +++ "b/AdavancedPart/KMP\345\274\200\345\217\221.md" @@ -22,10 +22,6 @@ Kotlin 语言的编译,与向不同的平台转化,明显是不同的职责 Frontend 会将 AST 进一步转换为 Kotlin IR(Kotlin Intermediate Representation),是 Kotlin 源代码的中间表示形式,Kotlin IR 是编译器前端的输出,也是编译器后端的输入。 Backend 则会吧 Kotlin IR 转换为不同平台的中间表示形式,最终生成目标代码。 -作者:A0微声z -链接:https://juejin.cn/post/7507888457705275455 -来源:稀土掘金 -著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 --- diff --git "a/JavaKnowledge/python3\345\205\245\351\227\250.md" "b/JavaKnowledge/python3\345\205\245\351\227\250.md" index 1f65e19f..35233217 100644 --- "a/JavaKnowledge/python3\345\205\245\351\227\250.md" +++ "b/JavaKnowledge/python3\345\205\245\351\227\250.md" @@ -7,6 +7,37 @@ message = "Hello World" print(message) ``` +常量: python中一版约定名字全为大写的就是常量。 + +## bool + + +bool类型只有True和False,用于真假判断。 + +python3中,bool是int的子类,True和False可以和数字相加。 + +True == 1、False == 0 会返回True + +is运算符用于比较两个对象的身份(即它们是否是同一个对象,是否在内存中占据相同的问题),而不是比较它们的值。 + +```python3 +print(True == 1) # True +print(False == 0) # True + +print(True is 1) # False +print(False is 0) # False + +``` + + +在python中,能够解释为假的值不只有False,还有: + +- None +- 0 +- 0.0 +- False +- 所有的空容器(空列表、空元组、空字典、空集合、空字符串) + ## 字符串 字符串就是一系列字符。在Python中,用引号括起来的都是字符串,其中的引号可以是单引号,也可以是双引号。例如: @@ -102,6 +133,33 @@ message = "Happy" + str(age) + "Birthday" print(message) ``` +### 自动类型转换(隐式转换) + +对两种不同类型的数据进行运算,较小的数据类型(整数)就会转换为较大的数据类型(浮点数)以免数据丢失,计算结果为浮点型: + +```python3 +num1 = 2 +num2 = 3.0 + +print(num1 + num2) # 5.0 + + + +num1 = 9 +num2 = 1 +print(num1 / num2) #9.0 +``` +注意: 特别的,两个整形进行除法运算时结果也是浮点型。 + + + + + + + + + + ## 注释 @@ -187,6 +245,15 @@ for bcy in bicycles: print('end') ``` +#### 使用enumerate()可以同时获取列表的下标和元素。 + +```python +lst = [1, 2, 3] + +for i, value in enumerate(lst): + print(i, value) +``` + ### 切片 @@ -253,6 +320,20 @@ else: print("cost $10") ``` +### match case语句 + +```python3 +match x: + case a: + xxxx + case b: + xxxx + case _: + xxx +``` +在python3.10版本新增了match case的条件判断方式,match后的对象会依次与case后的内容匹配,匹配成功则执行相应的语句,否则跳过,其中_可以匹配一切。 + + ### 相等判断 ```python car = 'Audi' @@ -303,6 +384,14 @@ print(alien['xPos']) del alien['points'] ``` +字典的其他写法: + +```python3 +dict4 = dict(name="Bob", age=20, gender="female") +dict5 = dict([("name", "Tom"), ("age", 22), ("gender", "male")]) +``` + + ### 遍历字典 - keys()方法返回所有的键列表。 @@ -395,6 +484,10 @@ while current_number <= 5: 同样while中也可以结合使用break、continue等,和Java基本一样。 +## 缩进 + +在python3中,代码块的结束不像其他一些编程语言(如c、java等)使用大括号{}来明确界定,而是通过缩进来表示。PEP8建议每级缩进都使用四个空格。 + ## 函数 @@ -415,6 +508,18 @@ print(greet_user(username='lili')) 上面三个引号的部分是文档字符串格式,用于简要的阐述其功能的注释。 +### 函数的参数传递 + +- 不可变类型 + 类似c++的值传递,如整数、字符串、元祖。如fun(a),传递的只是a的值,没有影响a对象本身,比如在fun(a)内部修改a的值,只是修改了另一个复制的对象,不会影响a本身。 +- 可变类型 + 类似c++的引用传递,如列表、字典。如fun(la),则是将la真正的传过去,修改后fun外部的la也会受影响 + + +python中一切都是对象,严格意义上我们不能说值传递还是引用传递,我们应该说传可变对象和不可变对象。 + + + ### 函数列表参数副本 将列表传递给函数后,函数就可对其进行修改。在函数中对这个列表所做的任何修改都是永久性的,这让你能够高效地处理大量的数据。 @@ -461,6 +566,23 @@ make_pizza('mushrooms','green peppers','extra cheese') 看起来,Python的参数传递方式是整齐划一的,但具体情况还得具体分析。在Python中,对象大致分为两类,即可变对象和不可变对象。可变对象包括字典、列表及集合等。不可变对象包括数值、字符串、不变集合等。如果参数传递的是可变对象,传递的就是地址,形参的地址就是实参的地址,如同“两套班子,一套人马”一样,修改了函数中的形参,就等同于修改了实参。如果参数传递的是不可变对象,为了维护它的“不可变”属性,函数内部不得不“重构”一个实参的副本。此时,实参的副本(即形参)和主调用函数提供的实参在内存中分处于不同的位置,因此对函数形参的修改,并不会对实参造成任何影响,在结果上看起来和传值一样。 +### 解包传参 + +若函数的形参是定长参数,可以通过`*`和`**`对列表、元组、字典等解包传参。 + +```python3 +def func(a, b, c): + return a + b + c + +tuple1 = (1, 2, 3) + +print(func(*tuple)) +dict1 = {"a":1, "b":2, "c":3} + +print(func(**dict1)) +``` + + ### 将函数存储在模块中 @@ -532,6 +654,70 @@ p.make_pizza(16,'pepperoni') p.make_pizza(12,'mushrooms','green peppers','extra cheese') ``` +### __name__ + +在python中,__name__是一个特殊的内置变量: + +- 当一个python文件被直接运行时,该文件的__name__属性为"__main__" +- 当一个python文件作为模块被导入时,__name__属性会被设置为该模块的名称(即文件名,不包括.py后缀) + +例如有时我们在模块中写一些测试代码,当模块被其他文件导入时这些测试代码会被执行。 +例如我们在main.py中导入my_add.py中,会发现my_add中的测试代码被执行了。这个时候使用__name__ == "__main__"判断就能避免测试代码被执行。 + + + + + + +### 全局变量和局部变量 + + +定义在函数内部的变量拥有一个局部作用域,定义在函数外的变量拥有全局作用域,局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。 + +#### global关键字 + +使用global关键字修改全局变量 +```python +def function_a(): + global var1 + var1 = 200 + print("var1:", var1) + +var1 = 100 + +``` +当全局变量为可变类型时,函数内部使用global声明,也可以对其进行修改。 +```python +def function_a(): + list1[0] = 1000 + +list1 = [1, 2, 3] +``` +在函数中不使用global声明全局变量时不能修改全局变量的本质是不能修改全局变量的指向,即不能将全局变量指向新的数据。 + +- 不可变类型的全局变量其指向的数据不能修改,所以部使用global无法修改全局变量。 +- 可变类型的全局变量其指向的数据可以修改,所以不使用global也可修改全局变量。 + +#### nonlocal关键字 + +nonlocal也用作内部作用域修改外部作用域的变量的场景,不过此时外部作用域不是全局作用域,而是嵌套作用域 + +```python +def function_outer(): + var = 1 + print(var) + + def function_inner(): + nonlocal var + var = 200 + function_inner() + print(var) + + +function_outer() +``` + + ## 类 @@ -541,8 +727,10 @@ p.make_pizza(12,'mushrooms','green peppers','extra cheese') dog.py ```python class Dog: + home = "xx" # 类属性 + def __init__(self, name, age): - self.name = name + self.name = name # 实例属性 self.age = age def sit(self): @@ -552,12 +740,14 @@ class Dog: print(self.name + " Rolling") ``` -__init__()是一个特殊的方法,每当你根据Dog类创建新实例时,Python都会自动运行它。在这个方法中形参self必不可少,还必须位于其他形参的前面。 +__init__()是一个特殊的方法(构造方法),每当你根据Dog类创建新实例时,Python都会自动运行它(__init__()方法的调用时机在实例(通过__new__()被创建之后,返回调用者之前调用,一般用于初始化一些数据))。在这个方法中形参self必不可少,还必须位于其他形参的前面。 为什么必须在方法定义中包含形参self呢?因为Python调用这个__init__()方法来创建Dog实例时,将自动传入实参self。 每个与类相关联的方法调用都自动传入实参self,它是一个指向实例本身的引用,让实例能够访问类中的属性和方法。 + + ```python import dog @@ -568,6 +758,103 @@ print(dog.name) dog.sit() ``` +类属性可以直接通过类名.类属性调用也可以通过实例对象.类属性调用,如`Dog.home或dog.home` + +```python +class Person: + home = "class home" + + def __init__(self, name) -> None: + self.name = name + + +person = Person("name") +print(person.name) +print(person.home) +person.home = "object home" +print(person.home) +print(Person.home) +Person.home = "new class home" +print(Person.home) + +# 结果 +name +class home +object home +class home +new class home + +``` +可以看到所有类的实力都会共享同一个类属性,如果通过对象实例.类属性调用则会直接创建一个实例属性(并不是修改了类属性的值) + +#### 动态添加属性和方法 + +```python +class Person: + def __init__(self, name: str) -> None: + self.name = name + +p = Person("John") +p.age = 20 +print(p.name) +print(p.age) + +``` + + + +### 方法 + +Python的类中有三种方法: 实例方法、静态方法、类方法。 + +- 实例方法在类中定义,第一个参数是self,代表实例本身 +- 实例方法只能被实例对象调用 +- 可以访问实例属性、类属性、类方法 + +#### 类方法 + +- 类方法通过@classmethod标注,方法的第一个参数是cls +- 类方法可以通过类来调用也可以通过实例来调用 +- 可以访问类属性 +- 类方法可以在不创建实例的情况下调用,通过类名直接调用,非常方便,适合一些和类整体相关的操作 + + +```python +class Dog: + + @classmethod + def wangwang(cls): + print"wangwang" + + +``` + +#### 静态方法 + +- 静态方法通过@staticmethod定义 +- 不访问实例属性或类属性,只依赖传入的参数 +- 可以通过类名或实例调用,但它不会访问类或实例的内部信息,更像一个工具函数 + + +#### 特殊方法 + + +方法名中有两个前缀下划线和两个后缀下划线的方法为特殊方法,也叫魔法方法。 +上面的__init__()就是一个特殊方法,这些方法会在进行特定的操作时自动被调用。 + +几个常见的特殊方法: + +- __new__(): 对象实例化时第一个调用的方法 +- __init__(): 类的初始化方法 +- __del__(): 对象的销毁器,定义了当对象被垃圾回收时的行为,使用del xxx时不会主动调用__del__(),除非此时引用计数=0 +- __str__(): 定义了对类的实例调用str()时的行为 +- __repr__(): 定义了对类的实例调用repr()时的行为,str()和repr()最主要的差别在于目标用户。repr()的作用是产生机器可读的输出,而str()则产生人类可读的输出。 +- __getattribute__():属性访问拦截器,定义了属性被访问前的操作。 + + + + + ### 继承 @@ -612,6 +899,69 @@ tesla.get_battery() super()是一个特殊函数,帮助Python将父类和子类关联起来。这行代码让子类包含父类的所有属性。 +多继承: 调用方法时先在子类中查找,若不存在则从左到右依次查找父类中是否包含方法。 + +#### 私有属性和方法 + +有时为了限制属性和方法只能在类内访问,外部无法访问。或父类中某些属性和方法不希望被子类继承,可以将其私有化: + +- 单下划线: 非公开API + + 大多数python代码都遵循这样一个约定,有一个前缀下划线的变量或方法应被视为非公开的API,例如_var1。但这种约定不具有强制力。 + +- 双下划线: 名称改写 + 有两个前缀下划线,并至多一个后缀下划线的标识符,例如__x,会被改写为_类名__x,只有在类内部可以通过__x访问,其他地方无法访问或只能通过_类名__x访问。 + + +#### property装饰器 + +可通过@property装饰器将一个方法转换为属性来调用。转换后可直接使用.方法名来使用,而无需使用.方法名()。 + +注意: @property修饰的方法不要和变量重名,否则可能导致无限递归 +#### 只读属性 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + +p = Person("zhangsan") +print(p.name) + +.name = "lisi" # 报错 +``` + +#### 读写属性 + +使用属性名.setter装饰 + +```python +class Person: + def __init__(self, name: str) -> None: + self.__name = name + + @property + def name(self): + return self.__name + + @name.setter + def name(self, name: str) -> None: + self.__name = name + +p = Person("zhangsan") +print(p.name) + +p.name = "lisi" +print(p.name) +``` + + + + ## 标准库 Python标准库是一组模块,安装的Python都包含它。 @@ -664,6 +1014,54 @@ with open(filename, 'w') as fo: 异常是使用try-except代码块处理的。try-except代码块让Python执行指定的操作,同时告诉Python发生异常时怎么办。使用了try-except代码块时,即便出现异常,程序也将继续运行:显示你编写的友好的错误消息,而不是令用户迷惑的traceback。 +```python +try: + 可能发生异常的代码 +except 异常类型1 as 变量名1: + 异常1处理代码 +except 异常类型2 as 变量名2: + 异常2处理代码 +else: + 没有异常时执行的代码 +finally: + 无论是否发生异常都会执行的代码 +``` + +#### 抛出异常 + +当想在代码中明确表示发生了错误或异常时,可以使用raise来抛出异常。 +```python +def int_add(a, b): + if isinstance(a, int) and isinstance(b, int): + return a + b + else: + raise TypeError("参数类型错误") +``` + +### with关键字 + + +python中的with语句用于异常处理,封装了try except finally编码范式,提供了一种简洁的方式来确保资源的正确获取和释放,同时处理可能发生的异常,提高了易用性,使代码更清晰、更具可读性,简化了文件流等公共资源的管理 + +```python +with expression as variable: + # 代码块 +``` + +- expression: 通常是一个对象或函数调用,该对象需要一个上下文管理器,即实现了__enter__和__exit__方法 + +- variable: 是可选的,用于存储expression的__enter__方法的返回值。 + +工作原理: + +- 使用with关键字系统会自动调用f.close()方法,with的作用等效于try finally语句。 - 当执行with语句时,会调用expression对象的__enter__方法 +- __enter__方法的返回值可以被存储在variable中(如果有),以供with代码块中使用 +- 然后执行with语句内部的代码块 +- 无论在代码块中是否发生异常,都会调用expression对象的__exit__方法,以确保资源的释放或清理工作,这类似于try - except -finally中的finally语句。 + + + + ### 分割字符串 @@ -790,3 +1188,59 @@ some_kwargs(**kdic) 在Python中,可以通过@property(装饰器)将一个方法转换为属性,从而实现用于计算的属性。将方法转换为属性后,可以直接通过方法名来访问方法,而不需要再添加一对小括号“()”,这样可以让代码更加简洁。 + + +#### lambda + +Python使用lambda来定义匿名函数,所谓匿名,指其不用def的标准形式定义函数。 + + +Lambda 表达式的基本语法是:`lambda 参数列表: 表达式`。 +关键字与参数:以 lambda关键字开头,后跟参数列表(如 x或 x, y),参数之间用逗号分隔。这些参数是函数的输入。 +表达式与返回值:冒号后面是一个单一的表达式。这个表达式会被求值,其结果就是 lambda 函数的返回值。注意,lambda 函数体内只能有一个表达式,不能包含复杂的语句或多行代码块。 +Lambda 函数是匿名的,也就是说,它们没有像用 def定义的函数那样的名称。它们的优势在于简洁,特别适合定义那些逻辑简单、可能只使用一次的短小函数。 + + +### var1 *= 2与var1 = var1 * 2的区别 + + +- var1 *= 2使用原地址 +- var1 = var1 * 2开辟了新的空间 +- 同样的对于类似,var1 += 2和var1 = var1 + 2也是同理 + +```python +def test(var1): + print("函数内var1 id:", id(var1)) + var1 *= 2 + print("var1 *= 2后函数内var1 id:", id(var1)) + var1 = var1 * 2 + print("var1 = var1 * 2后函数内var1 id:", id(var1)) + +函数内var1 id: 2302584035712 +var1 *= 2后,函数内var1 id: 2302584035712 +var1 = var1 * 2后,函数内var1 id: 2302584033664 +``` + + + + + +---- + + + + + + +计算机高级运行方式: + +- 编译型: c、c++ +- 解释型: python、JavaScript +- 先编译再解释: java + + +其实python也是先编译再解释,但是python的编译时机和java的不一样。 +- java是在代码解释执行前,先进行预编译,编译后生成字节码文件存在硬盘中。 +- python是在对代码进行逐行解释执行的时候进行的编译操作,默认生成的字节码文件存在内存中。 + + diff --git a/JavaKnowledge/shell.md b/JavaKnowledge/shell.md index c6bf4a42..1c2fb339 100644 --- a/JavaKnowledge/shell.md +++ b/JavaKnowledge/shell.md @@ -89,3 +89,13 @@ ar命令最开始是用于将文件写入磁带设备以作归档,但它也可 // 该命令创建了一个名为test.tar的归档文件,包含目录test和test2的内容。 tar -cvf test.tar test/ test2/ + +### 切换到root用户 + + +`sudo su -` + + - sudo : super user doing + - su : switch user + - `-` : root用户 + diff --git "a/Jetpack/ui/\345\257\271\346\257\224.md" "b/Jetpack/ui/\345\257\271\346\257\224.md" new file mode 100644 index 00000000..644118ec --- /dev/null +++ "b/Jetpack/ui/\345\257\271\346\257\224.md" @@ -0,0 +1,68 @@ +# 对比 + + +当前,Android开发正经历着从\"代码工人\"到\"智能场景架构师\"的转型。传统的XML布局开发模式正被声明式UI取代,云端AI计算快速向设备端迁移,跨平台技术也从\"能用\"向\"好用\"演进。 + + +1. 界面开发革命:Compose主导的声明式转型 +Jetpack Compose已不再是\"未来技术\",而是现在的必备技能。数据显示,使用Compose后,页面开发周期可缩短70% 。更有性能测试表明,在折叠屏设备上,传统的RecyclerView滑动帧率下降至45fps,而Compose的LazyColumn能稳定在60fps 。 + + + +2. 设备端AI:从云端到终端的算力迁移 +设备端AI正成为移动开发的新战场。2025年三大杀手级应用已落地:实时视频抠图、离线语音合成、智能代码补全。 +成本对比数据令人震惊:对于月活100万的应用,使用设备端AI进行图片风格迁移的成本几乎为零,而云端方案成本约为2000美元/月 。这种成本优势,加上设备端AI在延迟和隐私保护上的优势,使得端侧智能成为必然趋势。 + + + +传统的 Android UI 开发采用的是 View 视图体系,它是一种命令式的编程模型。开发者需要通过编写大量的代码来描述 View 的外观和行为,包括布局、样式、交互等。这种方式需要开发者花费大量的时间和精力,而且代码量也非常庞大,维护和扩展也比较困难。 + +与之相比,Jetpack Compose 是一种声明式的编程模型,采用的是函数式编程的思想。开发者只需要描述 UI 的样式和行为,然后 Jetpack Compose 会自动处理 View 的布局和绘制。这种方式不仅减少了代码量,还可以提高开发效率,同时也使得代码更加易于维护和扩展。 + +那 Data Binding 呢?用过的人自然能明白其中的痛苦,在 xml 文件中编写代码。并且还需要先解析 xml 文件,效率自然会有影响。而 Jetpack Compose 代码完全基于 Kotlin DSL 实现,相较于 Data Binding 需要有 xml 的依赖,Compose 只依赖单一语言,避免了因为语言间交互而带来的性能开销及安全问题。 + + +传统的 View 视图是基于继承的结构体系,也已经随着 Android 发展了十多年。View.java 本身也变得十分臃肿,目前已经超过三万行。臃肿的父类控件,也会造成子类视图的功能不合理。以 Button 为例,为了能让 Button 具备显示文字的功能,Button 被设计成了继承自 TextView 的子类。这样有很多不适用于按钮的功能也被继承下来了,TextView 的剪贴板功能,对 Button 来说显得不合理。而且随着 TextView 自身功能的迭代,Button 可能引入更多不必要的功能。 +另一方面,想 Button 这类基础控件,只能跟随系统升级而更新,及使发现了问题,也得不到及时的修复。这也是为什么如今很多新的视图都以 Jetpack 扩展库的形式单独发布的原因。 +而 Jetpack Compose 则是作为函数,相互之间没有继承关系。让开发这更多的是以组合的思想去思考问题。 + + + +3.1 声明式 UI 的编程范式发展的趋势 +从编程范式的维度来说,声明式 UI 相对传统的命令式 UI,更加先进, React, iOS 的 SwiftUI, flutter, Jetpack Compose, 声明式 UI 开发思想已经席卷了整个前端开发领域。如果你还在使用传统 View 视图体系开发 Android 原生界面,那你应该尝试声明式 UI。 + + +如同 Kotlin 取代 Java 成为 Google 推荐的 Android 官方推荐的编程语言一样,Jetpack Compose 也是 Google 推荐的 Android 官方 UI 框架。并且,新版的 Android Studio 中集成了大量的特性来支持 Jetpack Compose 更高效的开发。这表明 Jetpack Compose 在未来的 Android 开发中将扮演重要的角色。目前来看,Jetpack Compose 并不会完全取代传统的 View 视图体系,而是会和传统 View 视图体系共存, 但是 Jetpack Compose 在未来会能成为 Android 开发的主流。从 Android 14 Framework 中我们能看到 Compose 的影子了。在 2018 年,SystemUI 模块中,我们能看到 Kotlin 改写的部分模块,在 Android 14 中,我们再 Settings 中再次看到了 Kotlin 的代码了。并且 Google 新建了一个 spa 包,这个包里面尝试把 “应用管理”、“应用管理通知”、“使用情况” 页面用 Jetpack Compose 重写了。 + + + +四、Jetpack Compose 与 View 视图体系对比 +Compose 重新定义了 Android UI 开发方式,大幅提升了开发效率,主要体现在: + +声明式 UI, 不需要手动刷新数据 +取消 XML, 完全解除了混合写法的(XML + java、Kotlin) 的局限性,以及性能开销 +超强兼容性,大多数 jetpack 库(如 Navigation、ViewModel) 以及 Kotlin 协程都适用于 Compose, Compose 能够与现有 View 体系并存,你可以为一个既有项目引入 Compose。 +加速开发,更简单的动画和触摸事件Api, Compose 为我们提供了很多开箱即用的 Material 组件,如果的APP是使用的material设计的话,那么使用Jetpack Compose 能让你节省不少精力。 +精简代码数量,减少 bug 的出现 +强大的预览功能, Compose 预览机制可以实时预览动画,交互式预览,真正所见即所得。 + + + +4.1.1 APK 尺寸缩减 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/tvi_compose_apk_size.png?raw=true) + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/tvi_compose_method_count.png?raw=true) + +在将迁移后的应用与接入 Compose 前的应用做比较后,我们发现 APK 大小缩减了 41%,方法数减少了 17%。 + + +4.1.3 构建速度 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/tvi_compose_build_time.png?raw=true) + + + + + diff --git a/MobileAIModel/AIGC.md b/MobileAIModel/AIGC.md new file mode 100644 index 00000000..e37d7149 --- /dev/null +++ b/MobileAIModel/AIGC.md @@ -0,0 +1,49 @@ +AIGC +--- + + +### AIGC概念 + +AIGC全称为AI-Generated Content,人工智能内容生成。 + + +### 技术划分 + +AIGC从技术上的应用可以划分为: + +- 内容孪生: 是数字内容映射到另一个模态的技术,包括智能增强与智能转译,应用场景包括图像超分、语音转字幕、文字转语音等。 +- 内容编辑: 通过理解内容及属性控制实现修改,应用场景: 场景剪辑、虚拟试衣、人声分离等。 +- 内容生成: 通过学习海量数据中的抽象概念,并生成全新的内容,应用广泛,包括图像生成(AI绘画)、文本生成(AI写作),视频生成、多模态生成等。 + + +### 从生成内容划分 + +- 文本生成 +- 图像生成 +- 音频生成 +- 视频生成 +- 多模态生成 + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MobileAIModel/RAG.md b/MobileAIModel/RAG.md new file mode 100644 index 00000000..c50f2dfc --- /dev/null +++ b/MobileAIModel/RAG.md @@ -0,0 +1,75 @@ +RAG +--- + + +### RAG概念 + + +RAG(Retrieval Augmented Generation):检索增强生成。 + +顾名思义,就是通过检索外部数据,增强大模型的生成效果。 + +RAG为LLM提供了从某些数据源检索到的信息,并基于此修正生成的答案。 + +RAG基本上是Search + LLM提示,可以通过大模型回答查询,并将搜索算法所找到的信息作为大模型的上下文。查询和检索到的上下文都会被注入发送到LLM的提示语中。 + + + + +将大模型应用于实际业务场景时会发现,通用的基础大模型基本无法满足我们的实际业务需求,主要有以下几个方面的原因: + +- LLM的知识不是实时的,不具备知识更新 +- LLM可能不知道你私有的领域/业务知识 +- LLM有时会在回答中生成看似合理但实际上是错误的信息 + + +为什么会用RAG + +- 提高准确性: 通过检索相关的信息,RAG可以提高生成文本的准确性 +- 减少训练成本: 与需要大量数据来训练的大型生成模型相比,RAG可以通过检索机制来减少所需的训练数据量,从而降低训练成本。 +- 适应性强: RAG模型可以适应新的或不断变化的数据,由于它们能够检索最新的消息,因此在新数据和事件出现时,它们能够快速适应并生成相关的文本。 + + +RAG系统兼具检索与生成的双重能力,可视为传统生成系统的升级版:既有效减少了幻觉现象,又显著提升了回答的事实准确性。该技术还支持“与数据对话”(chat with my data)的应用场景,使企业和个人能够将LLM与内部数据或特定数据源(如书籍内容)对接。 + +RAG技术是一种结合信息检索与生成模型的新型架构,其核心思想是利用外部知识库或文档集合为大模型提供实时、准确的背景信息,从而弥补大模型的局限性。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/rag.png?raw=true) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/MobileAIModel/TensorFlow Lite b/MobileAIModel/TensorFlow Lite index 3e3be658..0f9a7adf 100644 --- a/MobileAIModel/TensorFlow Lite +++ b/MobileAIModel/TensorFlow Lite @@ -1,12 +1,17 @@ TensorFlow Lite --- + + [TensorFlow Lite](https://tensorflow.google.cn/lite?hl=zh-cn) 是一个移动端库,可用于在移动设备、微控制器和其他边缘设备上部署模型。 [TensorFlow Lite](https://tensorflow.google.cn/lite?hl=zh-cn) 是一组工具,可帮助开发者在移动设备、嵌入式设备和 loT 设备上运行模型,以便实现设备端机器学习。 +TensorFlow模型通常不是为移动场景而设计的,在这些场景中,必须考虑大小、电池消耗以及可能影响移动用户体验的所有其他因素。为此,TensorFlow Lite的创建有两个主要目标。第一,它可用于将现有的TensorFlow模型转换为更小、更紧凑的格式,着眼于针对移动设备进行优化。第二则是为可用于模型推理的各种移动平台提供高效的运行环境。 + + TensorFlow Lite是TensorFlow在手机和嵌入设备上的一个轻量级框架。 主要特性: diff --git "a/MobileAIModel/\345\244\247\346\250\241\345\236\213.md" "b/MobileAIModel/\345\244\247\346\250\241\345\236\213.md" new file mode 100644 index 00000000..7a19ef92 --- /dev/null +++ "b/MobileAIModel/\345\244\247\346\250\241\345\236\213.md" @@ -0,0 +1,394 @@ +大模型简介 +--- + +### 人工智能 + +人工智能(AI, Artificial Inteligence)通常用于描述致力于执行接近人类智能任务(如语音识别、语音翻译和视觉感知)的计算机系统。它是指软件产生的智能,而不是人类的智能。 + +目标是研究、设计和构建具备智能、学习、推理和行动能力的计算机和机器。 + +例如: 2016年Alphago击败李世石围棋。 + +### 生成式人工智能 + +GAI (Generative Aritificial Interligence) + +目标是让机器能够产生复杂有结构的物件。 例如chatGPT、deepseek。 + +### 机器学习 + +ML(Machine Learning) + +提到机器学习,追根溯源,我们需要先知道什么是“学习”。著名学者、1975年图灵奖获得者、1978年诺贝尔经济学奖获得者,赫伯特·西蒙(Herbert Simon)教授曾对“学习”下过一个定义:如果一个系统,能够通过执行某个过程,就此改进它的性能,那么这个过程就是学习。 +在西蒙看来,学习的核心目的就是改善性能。其实对于人而言,这个定义也是适用的。如果我们仅仅进行低层次的重复性学习,而没有达到认知升级的目的,那么即使表面看起来非常勤奋,其实也仅仅是一个“伪学习者”,因为我们的性能并没有得到改善。 +西蒙认为,对于计算机系统而言,通过运用数据及某种特定的方法(比如统计方法或推理方法)来提升机器系统的性能,就是机器学习(Machine Learning,简称ML)。 + +所谓机器学习,在形式上可近似等同于,在数据对象中通过统计或推理的方法,寻找一个有关特定输入和预期输出的功能函数f + + +所有的监督学习(Supervised Learning),基本上都是分类(Classification)的代名词。它从有标签的训练数据中学习模型,然后对某个给定的新数据利用模型预测它的标签。 +比如,小时候父母告诉我们某个动物是猫、狗或猪,然后在我们的大脑里就会形成或猫或狗或猪的印象(相当于模型构建),当面前来了一只“新”小狗时,如果你能叫出“这是一只小狗”,那么恭喜你,标签分类成功! +但如果你回答说“这是一头小猪”,这时你的父母就会纠正你的偏差:“乖,不对,这是一只小狗。”这样一来二去地进行训练,不断更新你大脑的认知体系,当下次再遇到这类新的猫、狗、猪时,你就会在很大概率上做出正确的“预测”分类。 +监督学习,就是先利用有标签的训练数据学习得到一个模型,然后使用这个模型对新样本进行预测。在本质上,监督学习的目标在于,构建一个由输入到输出的映射,该映射用模型来表示。 +### 深度学习 + +DL(Deep Learning): 深度学习算法试图模拟人类大脑的工作方式,其灵感来源于神经生物学,它通过对大量数据的学习,自动提取出数据的高层次特征和模式,从而实现图像识别、语音识别、自然 +语言处理等任务。 + +按照架构的不同,神经网络可以分为: + +- 卷积神经网络(CNNS) +- 循环神经网络(RNNS) +- Transformer网络等 + +同样是区分不同水果,这次你带孩子去了超市,那里有各种不同的水果,你没有解释每种水果的特点,只是给孩子指出了哪些是苹果、哪些是香蕉,他通过观察和比较,慢慢学会了辨认各种水果。 + +在这个过程中,孩子的大脑(在这里比喻为深度学习模型)自动从复杂的视觉、嗅觉等信号中提取层次化的特征。比如圆形、条纹、颜色深浅、气味等,从而达到识别水果的目的。 + + +### LoRA + +低秩自适应(Low-Rank Adaptation,LoRA)是一种针对大型预训练语言模型(如GPT-3)的微调技术。LoRA的核心思想是通过对模型权重进行低秩更新,以实现有效的参数适应,而无须重新训练模型的全部参数。这种技术在保留预训练模型的泛化能力的同时,允许模型快速适应特定任务。 + + +### 大语言模型 + +LLM(Large Language Model) + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/ai_relation..png?raw=true) + + + +### 多模态 + +提及LLM时,我们通常不会第一时间联想到多模态。毕竟,LLM本质上是语言模型。但我们很快会发现,若能处理文本之外的数据类型,其应用价值将大幅提升。例如,当语言模型可“看到”图像并回答相关问题时,其效用将显著增强。这种能够处理文本与图像(每种数据类型称为一种模态)的模型,即被称为多模态(multimodal)模型 + +能处理多种数据模态(如图像、音频、视频或传感器数据)的模型称为多模态模型。模型接收某种模态作为输入,但不一定能生成对应模态的输出 + +### 什么是大模型 + +大语言模型(large language model,LLM)[插图]已经对世界产生了深远的影响。通过使机器更好地理解和生成人类语言,LLM在人工智能(artificial inteligence,AI)领域开创了新的可能性,并影响了众多行业。 + + +大模型,一般也称为"大预言模型", 是一种基于深度学习技术训练出来的人工智能系统,主要用于处理和生成人类语言。 + +模型规模: 通常包含数十亿到数千亿个参数,这些参数就像是模型的"大脑神经元"。 +训练数据: 使用海量文本数据进行训练,包括书籍、文章、网页等各种形式的文字内容。 + +在深度学习和像GPT这样的大语言模型中,“参数”指的是模型的可训练权重。这些权重本质上是模型的内部变量,在训练过程中通过调整和优化来最小化特定的损失函数。 + + +### 模型蒸馏 + +大模型的运行需要极高的硬件资源,通常都是服务器集群并挂载数量众多的GPU(显卡)。 +为了满足低性能设备的运行,可以对大模型进行蒸馏。 例如deepseek r1的 7b模型、70b模型。 + +模型蒸馏: 对模型进行简化,在尽量保证模型性能的前提下,尽可能减少对硬件资源的需求。 + +简单理解就是假设你有一个超强的老师(大模型),他能讲解很复杂的知识。 +你想把这些知识传授给一群学生(小模型)。为了让学生能在不需要过多时间和资源的情况下,快速掌握老师的知识。你可以通过“蒸馏的方式”,让学生只学习精华、最重要的部分。 +这样,学生虽然没有老师那么强大,单依然能做出优秀表现。 + + + + +### 大模型训练 + +大模型整体的训练主要分为三个阶段: + +- 预训练: 注入领域知识 +- SFT(监督微调): +- RLHF(基于人类反馈的强化学习) + + + +### 模型部署 + + +#### ollama + +[ollama](https://ollama.com/)是一款旨在简化大型语言模型本地部署和运行过程的开源软件。 + + +- 本地部署(ollama) + - 后端和模型部署(例如ollama本地部署千问模型) + - 前端部署(交互界面,例如chatbox ai、Stremlit) + - 用户使用 + +#### LangChain + + +[LangChain](https://www.langchain.com/)是一个用于构建和管理基于语言模型(Language Models,LM)的应用程序框架。 + +它提供了一系列工具和组件,帮助开发者更高效的构建、训练、部署和管理语言模型应用。 + +LangChain的设计目标是简化语言模型的使用过程,使其更加容易被集成到各种应用场景中。 +它本质上是一个Python框架 + +LangChain的名称源自其核心方法之一——链(chain)。虽然我们可以独立运行LLM,但链的真正威力在与其他组件协同工作,甚至在多条链相互配合时才能充分展现。这种架构不仅能拓展LLM的能力,还能实现链与链之间的无缝衔接。 + + + +LangChain主要组件: + +- Prompts: 提示,包括提示管理、提示优化和提示序列化 +- Models: 模型,各种类型的模型和模型集成,比如gpt-4、deepseek +- Memory: 记忆,用来保存和模型交互时的上下文状态 +- Indexes: 索引,用来结构化文档,以便和模型交互 +- Chains: 链,一系列对各种组件的调用 +- Agents: 代理,决定模型采取哪些行动,执行并且观察流程,直到完成为止 + + + +- 云端部署(LangChain) + - 后端部署(例如阿里云百炼平台部署千问模型) + - 前端部署(例如Stremlit) + - 用户使用 + + + +模型部署总结: + +Ollama是本地模型运行的入门工具。 +LangChain是覆盖入门 + 进阶的全功能框架。 + +除了Ollama的功能外,LangChain额外多出了: + +- 记忆能力: 基于ConversationBufferMemory等组件完成多轮对话历史记录 +- 文档对接能力: 可实现PDF/Word等本地文档对打,构建私有知识库 +- 工具调用与智能体能力: 通过Tool、Agent组件让模型自动调用工具 +- 流程编排能力: SequentialChain、LCEL等实现任务自动化与并行模型 + +以及,针对云平台的API调用支持(如阿里云的通义千问百炼平台、腾讯云的混元等) + + + +## 端模型 + +不是所有模型都奔着“更大更强”去才有价值。 + +相反,像Qwen3-0.6b这种小模型才是真正能在实际场景中跑起来、用得起的模型。 + +Qwen3-0.6b的表现: + +- 推理速度快、延迟低:典型场景延迟在几十毫秒,适合边缘设备部署。 +- 资源占用小: 内存带宽压力低,功耗控制出色,支持长时间稳定运行。 +- 兼容性强: 广泛适用于query改写、文本匹配、语义检索等任务。 + + +很多人认为Qwen3-0.6b不如7b、13b强大,因此没用。但现实是,在高并发、低延迟、受限资源的实际业务场景中,大模型往往根本上不了场。 + +总结: 轻、小、稳,才是能落地的关键。 + +举个例子: + +普通小货车的运载能力相比较那些拉上十几吨、几十吨的大货车相差太多,难道它就没用用处吗? + +当然不是,因为它的定位就是面对相对狭小的场景、相对少的货物做出更敏捷的选择。 + +Qwen3-0.6b的定位,从来也不是生成长文本或复杂推理,而是在多模态系统中提供“一点点启发性的信号”,用于放大系统整体效能。 + +这“一点点信号”在推荐、搜索、识别系统重,往往正是决定点击、转化、留存的关键。 + + +我们选择部署Qwen3-0.6b,不是"凑合",而是精准选择--它足够快、足够轻、足够稳,也足够使用。 + +在生产环境中,能跑、能稳、能提效的模型,才是真正有价值的模型。 + + + +### 大模型的工作流程 + + +大多数人与语言模型交互的方式是通过网页平台,它提供用户与语言模型之间的聊天界面。你可能会注意到,模型并不是一次性生成所有输出,而是一次生成一个词元。 +词元不仅是模型的输出单位,也是模型查看输入的方式。发送给模型的提示词首先被分解成词元。 + + + +对计算机来说,语言是一个复杂的概念。 + + +文本本质上是非结构化的,当用0和1(单个字符)表示时就会失去其含义。因此,在语言人工智能的发展历史中,人们一直非常关注如何以结构化的方式标识语言,使计算机能够更容易的使用。 + +语言人工智能历史始于一种名为词袋(bag of words)的技术,这是一种表示非结构化文本的方法。 + +词袋模式的工作原理如下: 假设我们有两个句子需要创建数值表示。 + +词袋模型的第一步是分词(tokenization),即将句子拆分为单个词或子词(词元(token))。 + +最常见的分词方法是通过空格分割来把句子分割成词。 + +然而,这种方法也有其缺点,因为某些语言(如汉语)的词之间没有空格。 +在分词之后,我们将每个句子中所有不同的词组合起来,创建一个可用于表示句子的词表(vocabulary)。 +使用词表,我们只需计算每个句子中词出现的次数,就创建了一个词袋。 +因此,词袋模型旨在以数字形式创建文本的表示(representation),也称为向量或向量的标识。我们将这类模型称为表示模型(representation model) + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/respresentation_1.png?raw=true) + +通过计算单个词出现的次数创建词袋,这些值被称为向量表示。 + + +词袋虽然是一种优雅的方法,但存在一个明显的缺陷。它仅仅把语言视为一个几乎字面意义上的"词袋",而忽略了文本的语音特性和含义。 +这是因为词袋模型就像把一段话拆成一堆词,装进袋子里,只数数量,不看顺序,不懂意思,不管谁和谁有关系。这让它在理解句子真正含义时,显的很呆。 +比如狗追猫和猫追狗。 词袋模型看不出来到底谁追谁。 + + +word2vec(词向量)于2013年发布,是首批成功利用嵌入(embeding)这个概念来捕捉文本含义的技术之一。 + +嵌入是数据的向量表示,试图捕捉数据的含义。为此word2vec通过在大量文本数据(如整个维基百科)上训练来学习词的语义表示。 + +为了生成这些语义表示,word2vec利用了神经网络(neural network)技术。 + +神经网络由处理信息的多层互连节点组成。神经网络可以有多个分层,每个连接有一定的权重,这些权重通常被称为模型的参数。 + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/neural_network_1.png?raw=true) + +在训练过程中,word2vec会学习词与词之间的关系,并将这些信息提炼到词嵌入中。如果两个词各自的相邻词集合有更大的交集,它们的词嵌入向量就会更接近,反之亦然。 + +词嵌入可以用多种属性来表示一个词的含义。由于嵌入向量的大小是固定的,这些属性需要经过精心选择,以构建用来代表词的“心智表征”的抽象标识。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/word2vec_2.png?raw=true) + +在实践中,这些属性通常相当抽象,很少与单一实体或人类可识别的概念相关。然而,这些属性组合在一起对计算机来说是有意义的,是将人类语言转换为计算机语言行之有效的方式。 + +词嵌入非常有用,因为它使我们能够衡量两个词的语义相似度。 + +上面word2vec的训练过程会创建静态的、可下载的词标识。例如,bank这个词无论在什么上下文中使用,都会有相同的词嵌入。 + +然而,bank既可以指银行,也可以指河岸。它的含义应该根据上下文而变化,因此它的嵌入也应该根据上下文而变化。 +使用RNN(Recurrent Neural Network,循环神经网络),可以实现文本编码的一个步骤。这些神经网络的变体可以将序列作为补充输入进行建模。 +为此,这些RNN被用于两个任务: +- 编码: 也就是表示输入句子 +- 解码: 也就是输出句子 + +该架构中的每个步骤都是自回归(auto - regressvie)的,在生成下一个词时,该架构需要使用所有先前生成的词作为输入。 +然而,这种上下文嵌入方式存在局限性,因为它仅用一个嵌入向量来表示整个输入,使得处理较长的句子变得困难。 + +在2014年,研究人员提出了注意力(attention)解决方案,大大改善了原始架构。 +注意力云讯模型关注输入序列中彼此相关(相互注意)的部分,并放大它们的信号,注意力机制通过选择性地聚焦于句子中最关键的词,来突出其重要性。 + +### Transformer + +在2017年著名论文"Attention is All You Need"首次探讨了注意力机制的真正威力,并提出了一种被称为Transformer的网络架构。 + +Transformer完全基于注意力机制,摒弃了此前提到的RNN。与RNN相比,Transformer支持并行训练,这大大加快了训练速度。 +在Transformer中,编码和解码组件相互堆叠。这种架构仍然是自回归的,每个新生成的词都被模型用于生成下一个词。 + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/transformer_1.png?raw=true) + +Transformer由堆叠的编码器和解码器块组合而成,输入依次轮流经每个编码器和解码器. + +要理解Transformer LLM的行为,最常见的方式是将其视为一个接收文本输入并生成响应文本的软件系统。 + +通过分词后大模型会生成一个个的新词元的概率列表,然后从生成的词元列表中选择某一个词元的过程就叫做解码策略。 + +每次都选择概率分数最高的词元的策略被称为贪心解码。这就是在LLM中将温度(temperature)参数设为零时会发生的情况。 + + + +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/transformer_2.png?raw=true) + +· Transformer LLM 每次生成一个词元。 +· 生成的词元会被追加到提示词中,然后,这个更新后的提示词会再次被输入模型进行下一次前向传播,以生成下一个词元。 +· Transformer LLM的三个主要组件是分词器、一系列Transformer块和语言建模头。 +· 分词器包含模型的词元词表。模型中包含与这些词元相关联的词元嵌入。将文本分解成词元,然后使用这些词元的嵌入向量,是词元生成过程的第一步。 +· 前向传播会依次经过所有阶段。 +· 在处理接近尾声时,语言建模头会对下一个可能的词元进行概率评分。解码策略决定了在这一生成步骤中选择哪个实际词元作为输出(有时是概率最高的下一个词元,但并非总是如此)。 +· Transformer表现出色的原因之一是它能够并行处理词元。每个输入词元都流入其独立的计算流(也称为处理路径)。这些流的数量就是模型的“上下文长度”,代表模型可以处理的最大词元数量。 +· 由于Transformer LLM通过循环来一次生成一个词元的文本,因此缓存每个步骤的处理结果是一种很好的策略,这样可以避免重复处理工作(这些结果以各种矩阵的形式存储在层中)。 +· 大部分处理发生在Transformer块中。这些块由两个组件组成,其中一个是前馈神经网络,它能够存储信息,并根据训练数据进行预测和插值。 +· Transformer块的另一个主要组件是自注意力。自注意力整合了上下文信息,使模型能够更好地捕捉语言的细微差别。 +· 注意力过程分为两个主要步骤:相关性评分;信息组合。 +· Transformer的自注意力层并行执行多个注意力操作,每个操作都发生在注意力头内,它们的输出被聚合成自注意力层的输出。 +· 通过在所有注意力头或一组注意力头(分组查询注意力)之间共享键矩阵和值矩阵,可以加速注意力计算。 +· Flash Attention等方法通过优化在GPU不同显存系统上的操作方式来加速注意力计算。 + + + + + + + +##### 1. 分词化与词表映射 + +分词化(Tokenization)是自然语言处理(NLP)中的重要概念,它是将段落和句子分割成更小的分词(Token)的过程。 + +举一个实际的例子,以下是一个英文句子: + +I want to study LLM. + +为了让机器理解这个句子,对字符串执行分词化,将其分解为独立的单元。使用分词化,我们会得到这样的结果: + +`["I", "want", "to", "study", "LLM", "."]` + +将一个句子分解成更小的、独立的部分可以帮助计算机理解句子的各个部分,以及它们在上下文中的作用,这对于进行大量上下文的分析尤其重要。 + +每一个分词(token)都会通过预先设置好的词表,映射为一个tokenid,这是token的身份证,这样一句话最终会被表示为一个元素为tokenid的列表,供给计算机进行下一步处理。 + + + +##### 2. 文本生成过程 + + +大模型的工作概括来说是根据给定的文本预测下一个token。 + +对于我们来说,看似像在对大模型提问,但实际上是给了大模型一串提示文本,让它可以对后续的文本进行推理。 + +大模型的推理过程不是一步到位的,当大模型进行推理时,它会基于现有的token,根据概率最大原则预测出下一个最有可能的token,然后将该预测的token加入到输入序列中,并将更新 +后的输入序列继续输入大模型预测下一个token。 +这个概率则是我们用很多的文本,分词为token以后训练大模型得来的。 + + + +### LLM + + +LLM大模型训练流程: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/llm_training_process.png?raw=true) + + + + + +### 提示工程 + +提示工程(Prompt Engineering)是一项通过优化提示词(Prompt)和生成策略,从而获得更好的模型返回结果的工程技术。 + +提示词(Prompt) --> 大语言模型(LLM) --> 返回结果(Completion) + +- 好的Prompt需要不断优化 +- 说清楚自己到底想要什么,要具体 +- 不要让机器去猜测太多,为了不让机器去猜测,我们就需要告诉细节 +- 提示工程有一些技巧,灵活掌握,事半功倍。 + + +一个包含多个组件的复杂提示词示例: +![Image](https://raw.githubusercontent.com/CharonChui/Pictures/master/prompt_engie_1.png?raw=true) + + + +### GPU + +计算资源通常是指系统中可用的GPU(graphics processing unit,图形处理单元,通常称显卡)资源。 + +强大的GPU可以加速LLM的训练和使用。在选择GPU时,一个重要的因素是可用的VRAM(video random access memory,视频随机存储器,通常称显存)容量,即GPU上可用的内存量。实践中,显存越大越好。原因是如果没有足够的显存,某些模型根本无法使用。 + + + +### Agent + + +Agent = LLM + memory + planning skills + tool use + +目前我们构建的系统均按照预设流程执行操作。LLM最具突破性的发展方向在于其自主决策能力。这类能够自主规划行动及其序列的系统被称为智能体(agent),其核心在于利用语言模型自主制定行动决策。 + + +使用语言模型的一个重要步骤是选择模型。查找和下载LLM的一个重要网站是Hugging Face。Hugging Face是著名的Transformers软件包背后的组织,多年来一直推动着语言模型的整体发展。正如其名称所示,该软件包是建立在我们在此前提到过的transformers框架之上的。 + + + + diff --git "a/MobileAIModel/\351\242\204\350\256\255\347\273\203.md" "b/MobileAIModel/\351\242\204\350\256\255\347\273\203.md" new file mode 100644 index 00000000..87bf60d2 --- /dev/null +++ "b/MobileAIModel/\351\242\204\350\256\255\347\273\203.md" @@ -0,0 +1,11 @@ +预训练 +--- + + + +### 文本分类 + + +文本分类是NLP中的一项常见任务。 + +该任务的目标是训练一个模型,为输入的文本分配标签或类别。从情感分析和意图识别,到实体提取和语言检测,文本分类在全球范围内被广泛应用